Explorar o código

fix:优化kiro-cli的部署过程和配置

Jerry Liang hai 2 meses
pai
achega
36fc2da13e
Modificáronse 86 ficheiros con 1038 adicións e 26526 borrados
  1. 140 0
      deploy/kiro/agents/fwh-toolbox-agent.json
  2. 80 0
      deploy/kiro/bin/fwh-pyrun
  3. 27 0
      deploy/kiro/skills-sh.list
  4. 48 0
      deploy/kiro/skills/fwh-toolbox-rules/SKILL.md
  5. 2 2
      deploy/kiro/skills/fwh-toolbox-rules/references/file-output-rules.md
  6. 3 7
      deploy/kiro/skills/fwh-toolbox-rules/references/officecli-batch-rules.md
  7. 5 5
      deploy/kiro/skills/fwh-toolbox-rules/references/ppt-template-rules.md
  8. 49 0
      deploy/kiro/skills/fwh-toolbox-rules/references/python-rules.md
  9. 0 0
      deploy/kiro/skills/fwh-toolbox-workflow/SKILL.md
  10. 0 0
      deploy/kiro/skills/fwh-toolbox-workflow/references/data-direct-query.md
  11. 0 0
      deploy/kiro/skills/fwh-toolbox-workflow/references/fwh-pro-api.md
  12. 0 0
      deploy/kiro/skills/fwh-toolbox-workflow/references/plane-collaboration.md
  13. 1 1
      deploy/kiro/skills/tou-template-scheduling/SKILL.md
  14. 0 0
      deploy/kiro/steering/graphify.md
  15. BIN=BIN
      deploy/kiro/templates/company-template.pptx
  16. 21 4
      deploy/scripts/deploy.sh
  17. 468 0
      deploy/scripts/deploy_kiro_cli.sh
  18. 194 0
      deploy/scripts/sync_kiro_config.sh
  19. 0 110
      service/cmd/mcp/.kiro/agents/fwh-toolbox-agent.json
  20. 0 19
      service/cmd/mcp/.kiro/settings/cli.json
  21. 0 26
      service/cmd/mcp/.kiro/skills/convert-documents-to-markdown/SKILL.md
  22. 0 1
      service/cmd/mcp/.kiro/skills/graphify/.graphify_version
  23. 0 713
      service/cmd/mcp/.kiro/skills/graphify/SKILL.md
  24. 0 56
      service/cmd/mcp/.kiro/skills/graphify/references/add-watch.md
  25. 0 87
      service/cmd/mcp/.kiro/skills/graphify/references/exports.md
  26. 0 31
      service/cmd/mcp/.kiro/skills/graphify/references/extraction-spec.md
  27. 0 46
      service/cmd/mcp/.kiro/skills/graphify/references/github-and-merge.md
  28. 0 33
      service/cmd/mcp/.kiro/skills/graphify/references/hooks.md
  29. 0 311
      service/cmd/mcp/.kiro/skills/graphify/references/query.md
  30. 0 52
      service/cmd/mcp/.kiro/skills/graphify/references/transcribe.md
  31. 0 210
      service/cmd/mcp/.kiro/skills/graphify/references/update.md
  32. 0 355
      service/cmd/mcp/.kiro/skills/lieflat-charts/SKILL.md
  33. 0 108
      service/cmd/mcp/.kiro/skills/lieflat-charts/catalog.md
  34. 0 177
      service/cmd/mcp/.kiro/skills/lieflat-charts/mono-tokens.js
  35. 0 943
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/basics-gallery.html
  36. 0 112
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/big-circular.html
  37. 0 111
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/big-force.html
  38. 0 164
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/big-threads.html
  39. 0 37
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/README.md
  40. 0 901
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/basics-palm.html
  41. 0 893
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/basics-porcelain.html
  42. 0 899
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/basics-wire.html
  43. 0 112
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-circular-palm.html
  44. 0 112
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-circular-porcelain.html
  45. 0 111
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-force-palm.html
  46. 0 111
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-force-porcelain.html
  47. 0 164
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-threads-palm.html
  48. 0 164
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-threads-porcelain.html
  49. 0 1172
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/glance-palm.html
  50. 0 1171
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/glance-porcelain.html
  51. 0 1176
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/glance-wire.html
  52. 0 1260
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/lupi-palm.html
  53. 0 1257
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/lupi-porcelain.html
  54. 0 1257
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/lupi-wire.html
  55. 0 180
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/maps-palm.html
  56. 0 180
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/maps-porcelain.html
  57. 0 180
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/maps-wire.html
  58. 0 1135
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/glance-gallery.html
  59. 0 1220
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/lupi-gallery.html
  60. 0 180
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/maps-gallery.html
  61. 0 42
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/index.html
  62. 0 306
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-01.en.html
  63. 0 308
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-01.zh.html
  64. 0 351
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-02.en.html
  65. 0 355
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-02.zh.html
  66. 0 390
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-03.en.html
  67. 0 391
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-03.zh.html
  68. 0 369
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-04.en.html
  69. 0 375
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-04.zh.html
  70. 0 260
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-05.en.html
  71. 0 258
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-05.zh.html
  72. 0 434
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-06.en.html
  73. 0 433
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-06.zh.html
  74. 0 419
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-07.en.html
  75. 0 415
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-07.zh.html
  76. 0 255
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-08.en.html
  77. 0 255
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-08.zh.html
  78. 0 448
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-09.en.html
  79. 0 449
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-09.zh.html
  80. 0 430
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-10.en.html
  81. 0 423
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-10.zh.html
  82. 0 182
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-11.en.html
  83. 0 180
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-11.zh.html
  84. 0 412
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-12.en.html
  85. 0 413
      service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-12.zh.html
  86. 0 417
      service/cmd/mcp/.kiro/skills/officecli/SKILL.md

+ 140 - 0
deploy/kiro/agents/fwh-toolbox-agent.json

@@ -0,0 +1,140 @@
+{
+  "name": "fwh-toolbox-agent",
+  "description": "fwh-toolbox平台AI助手",
+  "prompt": "你是一个本地平台专业分析工具。使用本地知识库和MCP server读取数据,实现数据分析。不可创建/修改文件,只能通过MCP将文件生成到Minio服务器。",
+  "tools": [
+    "read",
+    "write",
+    "grep",
+    "glob",
+    "code",
+    "web_fetch",
+    "web_search",
+    "knowledge",
+    "subagent",
+    "todo_list",
+    "introspect",
+    "shell",
+    "@fwh-tools",
+    "@drawio",
+    "@officecli"
+  ],
+  "allowedTools": [
+    "read",
+    "grep",
+    "glob",
+    "code",
+    "web_fetch",
+    "web_search",
+    "knowledge",
+    "subagent",
+    "todo_list",
+    "introspect",
+    "shell",
+    "@drawio",
+    "@officecli",
+    "@fwh-tools/service_site_query_info",
+    "@fwh-tools/service_site_search",
+    "@fwh-tools/service_site_power_flow",
+    "@fwh-tools/service_sim_query",
+    "@fwh-tools/service_sim_search",
+    "@fwh-tools/service_sim_monthly_data",
+    "@fwh-tools/service_monitor_alarm_stats",
+    "@fwh-tools/service_monitor_shutdown_stats",
+    "@fwh-tools/service_feedback_history",
+    "@fwh-tools/service_dashboard_shutdown_date",
+    "@fwh-tools/service_dashboard_shutdown_detail",
+    "@fwh-tools/service_dashboard_alarm_detail",
+    "@fwh-tools/service_background_task_list",
+    "@fwh-tools/service_tou_template_list",
+    "@fwh-tools/service_tou_template_detail",
+    "@fwh-tools/service_site_monitor_config_list",
+    "@fwh-tools/service_site_monitor_name_list",
+    "@fwh-tools/service_site_monitor_config_get",
+    "@fwh-tools/service_site_monitor_base_info",
+    "@fwh-tools/service_site_monitor_mode_info",
+    "@fwh-tools/service_site_monitor_alarm_list",
+    "@fwh-tools/service_site_info_update_status",
+    "@fwh-tools/service_site_info_tags_list",
+    "@fwh-tools/service_auto_update_get_options",
+    "@fwh-tools/service_auto_update_get_package",
+    "@fwh-tools/service_auto_update_get_apower_version",
+    "@fwh-tools/service_auto_update_get_config",
+    "@fwh-tools/service_auto_update_file_list",
+    "@fwh-tools/service_batch_download_list",
+    "@fwh-tools/service_batch_download_path",
+    "@fwh-tools/service_batch_download_file_tree",
+    "@fwh-tools/service_site_analysis_list",
+    "@fwh-tools/service_site_analysis_get_config",
+    "@fwh-tools/service_site_analysis_data",
+    "@fwh-tools/data_mongo_find",
+    "@fwh-tools/data_mongo_count",
+    "@fwh-tools/data_redis_get",
+    "@fwh-tools/data_redis_exists",
+    "@fwh-tools/data_pgsql_query_snapshot_file_record",
+    "@fwh-tools/data_pgsql_query_snapshot_data",
+    "@fwh-tools/data_fwh_*",
+    "@fwh-tools/data_sim_*",
+    "@fwh-tools/data_emspara_*",
+    "@fwh-tools/data_plane_*",
+    "@fwh-tools/data_plane_v2_*",
+    "@fwh-tools/storage_minio_*",
+    "@fwh-tools/storage_file_read",
+    "@fwh-tools/storage_file_list",
+    "@fwh-tools/util_*"
+  ],
+  "toolsSettings": {
+    "read": {
+      "allowedPaths": [
+        "./**",
+        "/tmp/fwh-toolbox-agent/**",
+        "__WIKI_DOCS__/**"
+      ],
+      "deniedPaths": []
+    },
+    "write": {
+      "allowedPaths": ["/tmp/fwh-toolbox-agent/**"],
+      "deniedPaths": []
+    },
+    "shell": {
+      "allowedCommands": [
+        "^npx -y @firecrawl/anydoc(\\s.*)?$",
+        "^base64(\\s.*)?$",
+        "^mkdir\\s+-p\\s+/tmp/fwh-toolbox-agent(/[^\\s;|&]*)?$",
+        "^cp\\s+__KIRO_HOME__/templates/[^\\s;|&]+\\s+/tmp/fwh-toolbox-agent/[^\\s;|&]+$",
+        "^__KIRO_HOME__/bin/fwh-pyrun\\s+/tmp/fwh-toolbox-agent/[^\\s;|&]+\\.py(\\s+[^\\s;|&]+)*$"
+      ],
+      "denyByDefault": true
+    }
+  },
+  "resources": [
+    {
+      "type": "knowledgeBase",
+      "source": "file://__WIKI_DOCS__",
+      "name": "FranklinWHProductDocs",
+      "description": "产品文档和指南,包括产品的软件、硬件、系统功能详细说明",
+      "include": ["**/*.md"],
+      "indexType": "best",
+      "autoUpdate": true
+    }
+  ],
+  "hooks": {},
+  "mcpServers": {
+    "fwh-tools": {
+      "command": "./fwh-mcp-server",
+      "args": [],
+      "disabled": false
+    },
+    "drawio": {
+      "command": "npx",
+      "args": ["@next-ai-drawio/mcp-server@latest"],
+      "disabled": false
+    },
+    "officecli": {
+      "command": "__OFFICECLI_BIN__",
+      "args": ["mcp"],
+      "disabled": false
+    }
+  },
+  "welcomeMessage": "欢迎使用fwh-toolbox平台AI助手"
+}

+ 80 - 0
deploy/kiro/bin/fwh-pyrun

@@ -0,0 +1,80 @@
+#!/usr/bin/env bash
+# =============================================================================
+# fwh-pyrun —— 在 bwrap 沙箱中运行 Python3,供 fwh-toolbox-agent 安全调用
+# =============================================================================
+# 用法:
+#   fwh-pyrun /tmp/fwh-toolbox-agent/<脚本>.py [args...]
+#
+# 安全模型(三层):
+#   B(强制隔离,bwrap):只读挂载系统目录以支撑 Python 运行;仅沙箱目录
+#     /tmp/fwh-toolbox-agent 可写;--unshare-net 断网;--unshare-all 降权。
+#     脚本在笼子里“看不见” /home、项目目录、凭据等,越界访问会直接失败。
+#   A(准入校验):脚本必须位于沙箱目录内、以 .py 结尾、真实存在;解析真实
+#     路径以防 ../ 与软链逃逸。(agent.json 的 allowedCommands 已在更外层拦截)
+#   C(资源限额,ulimit + timeout):限制 CPU / 内存 / 单文件大小 / 进程数,
+#     外层 timeout 兜底防死循环。
+#
+# 退出码:
+#   2  入参非法 / 路径越界 / 脚本不存在
+#   124 运行超时(timeout 触发)
+#   其它 透传 Python 退出码
+# =============================================================================
+set -euo pipefail
+
+SANDBOX_ROOT="/tmp/fwh-toolbox-agent"
+PY="/usr/bin/python3"
+
+# --- 依赖自检(部署脚本应已确保安装;此处兜底给出清晰报错) ---
+command -v bwrap >/dev/null 2>&1 || { echo "fwh-pyrun: 未找到 bwrap(bubblewrap),无法建立沙箱" >&2; exit 2; }
+[ -x "$PY" ] || { echo "fwh-pyrun: 未找到 python3($PY)" >&2; exit 2; }
+
+# --- A:入参与路径校验 ---
+script="${1:-}"
+[ -n "$script" ] || { echo "fwh-pyrun: 缺少脚本路径。用法: fwh-pyrun $SANDBOX_ROOT/<脚本>.py [args...]" >&2; exit 2; }
+shift || true
+
+# 解析真实绝对路径,防止 ../ 或软链逃逸
+real_script="$(readlink -f -- "$script" 2>/dev/null)" || { echo "fwh-pyrun: 无法解析路径:$script" >&2; exit 2; }
+case "$real_script" in
+  "$SANDBOX_ROOT"/*.py) : ;;   # 合法:沙箱内 .py
+  *) echo "fwh-pyrun: 拒绝——脚本必须位于 $SANDBOX_ROOT 内且以 .py 结尾(实际:$real_script)" >&2; exit 2 ;;
+esac
+[ -f "$real_script" ] || { echo "fwh-pyrun: 脚本不存在:$real_script" >&2; exit 2; }
+
+# 确保沙箱目录存在(幂等)
+mkdir -p "$SANDBOX_ROOT"
+
+# --- C:资源限额 ---
+ulimit -t 30          # CPU 时间上限 30 秒
+ulimit -v 1048576     # 虚拟内存上限 1GB(KB)
+ulimit -f 262144      # 单文件写入上限 ~128MB(blocks,512B/block)
+ulimit -c 0           # 不产生 core dump
+# 注:不在此处收紧 ulimit -u(进程数按整个用户计,过紧会导致 timeout/bwrap
+# 自身 fork 失败);fork 炸弹由 bwrap 的 PID namespace 隔离 + CPU/内存限额抑制。
+
+# --- B:bwrap 强制隔离 + 外层 timeout 兜底(总运行 ≤ 60s) ---
+# 只读挂载 Python 运行所需系统目录;仅沙箱目录可写;无网络;降权。
+# 用数组构造参数,避免路径含特殊字符时被拆分。
+bwrap_args=(
+  --ro-bind /usr /usr
+  --ro-bind /etc /etc
+  --bind "$SANDBOX_ROOT" "$SANDBOX_ROOT"
+  --proc /proc
+  --dev /dev
+  --unshare-all
+  --unshare-net
+  --die-with-parent
+  --new-session
+  --setenv HOME "$SANDBOX_ROOT"
+  --setenv TMPDIR "$SANDBOX_ROOT"
+  --setenv PYTHONDONTWRITEBYTECODE 1
+  --setenv PYTHONPATH ""
+  --chdir "$SANDBOX_ROOT"
+)
+# /lib /lib64 /bin /sbin 在多数发行版是指向 /usr 的符号链接;存在时才只读挂载
+for d in /lib /lib64 /bin /sbin; do
+  [ -e "$d" ] && bwrap_args+=( --ro-bind "$d" "$d" )
+done
+
+exec timeout --kill-after=5s 60s \
+  bwrap "${bwrap_args[@]}" -- "$PY" -I -B "$real_script" "$@"

+ 27 - 0
deploy/kiro/skills-sh.list

@@ -0,0 +1,27 @@
+# =============================================================================
+# skills.sh 生态 skill 安装清单
+# =============================================================================
+# 本文件列出需要通过 skills CLI 命令安装的 skill(来自 skills.sh 生态)。
+# 部署脚本 deploy_kiro_cli.sh 会读取本文件逐行安装。
+#
+# 格式:每行一个 skill,写为  <owner>/<repo>@<skill>
+#   - 以 # 开头的行是注释,空行忽略
+#   - @<skill> 用于 skills CLI 的 --skill 参数(指定仓库内要装的 skill 名)
+#
+# 安装命令(部署脚本对每行执行的等价命令):
+#   npx --yes skills add <owner>/<repo> --skill <skill> --agent kiro-cli --yes
+#
+# 手动安装单个示例:
+#   npx skills add graphify-labs/graphify --skill graphify --agent kiro-cli
+#
+# 查询某 skill 的准确 owner/repo:
+#   npx skills find <关键字>
+#
+# 新增/调整 skills.sh skill:在下方增删行即可,无需改动部署脚本。
+# =============================================================================
+
+firecrawl/anydoc@convert-documents-to-markdown
+graphify-labs/graphify@graphify
+iofficeai/officecli@officecli
+larashero3-dotcom/lieflat-charts@lieflat-charts
+github/awesome-copilot@excalidraw-diagram-generator

+ 48 - 0
deploy/kiro/skills/fwh-toolbox-rules/SKILL.md

@@ -0,0 +1,48 @@
+---
+name: fwh-toolbox-rules
+description: FranklinWH 工具箱平台的操作规则集。当涉及以下任一场景时必须使用本技能并加载对应参考规则:(1) 通过 storage_file_* 工具读取/写入/上传文件,或向用户提供文件下载、导出、生成报告;(2) 通过 @officecli 工具批量写入 .xlsx/.docx/.pptx;(3) 使用公司 PPT 模板生成演示文稿(用户提到“公司模板/company template/用模板生成 PPT”);(4) 需要用 Python3 运行自定义脚本做数据处理/计算/转换(只能通过沙箱执行器 fwh-pyrun 运行)。本技能规定这些操作的强制参数、输出标记、批量写入方式、模板路径与 Python3 沙箱运行方式。
+---
+
+# FranklinWH 工具箱操作规则
+
+本技能汇总平台在“文件输出、officecli 批量写入、PPT 模板、Python3 沙箱执行”四类操作上的强制规则。按下面的对照表,在对应操作前加载相应参考文件并严格遵循。
+
+## 何时加载哪份规则
+
+| 操作场景 | 必读参考 |
+| --- | --- |
+| 任何 `storage_file_write` / `storage_file_upload` / `storage_file_read*` / `storage_file_list` 调用;给用户下载/导出/生成报告 | `references/file-output-rules.md` |
+| 通过 `@officecli` 批量写入 `.xlsx` / `.docx` / `.pptx` | `references/officecli-batch-rules.md` |
+| 使用公司 PPT 模板生成演示文稿 | `references/ppt-template-rules.md` |
+| 需要用 Python3 做自定义数据处理/计算/转换(现成工具无法覆盖时) | `references/python-rules.md` |
+
+## 文件输出:两条必守硬约束(无条件遵守,无需展开也要执行)
+
+1. 调用 `storage_file_write` / `storage_file_upload` **必须**传 `userId` 和 `sessionId`,取自用户消息开头的 `[context] userId=xxx sessionId=xxx` 行。
+2. 写入/上传**成功后**,**必须**在回复中独占一行输出下载标记(不要放进代码块):
+   ```
+   $$FILE_DOWNLOAD:{"key":"<返回的key>","fileName":"<文件名>","size":<文件大小>}$$
+   ```
+   完整的读/写策略、判断标准与示例见 `references/file-output-rules.md`。
+
+## officecli 批量写入:核心红线
+
+- **永远不要**用 `--input -`(stdin),MCP 环境无 stdin 管道,会永久卡住。
+- 批量首选 `batch <file> --commands '[...]' --json` 一次内联完成;仅超大批量才用 `--input <真实文件路径>`。
+- 详细决策顺序与转义规则见 `references/officecli-batch-rules.md`。
+
+## PPT 模板:要点
+
+- 用户提到“公司模板 / company template / 用模板生成 PPT”时,使用 `__KIRO_HOME__/templates/company-template.pptx`,先拷贝到 `/tmp/fwh-toolbox-agent/ppt/` 再编辑。
+- 布局/配色/首尾页规则见 `references/ppt-template-rules.md`。
+
+## Python3 沙箱执行:核心红线
+
+- 仅当现成工具(`@fwh-tools` / `@officecli` / `knowledge`)**无法完成**时才写 Python3。
+- Python3 **只能**通过沙箱执行器运行,命令固定为:
+  `__KIRO_HOME__/bin/fwh-pyrun /tmp/fwh-toolbox-agent/scripts/<name>.py [参数...]`。
+- **禁止**直接 `python3` / `python -c` / 命令拼接 / 管道(配置层会拒绝)。
+- 脚本只能读写 `/tmp/fwh-toolbox-agent/` 内文件、**无网络**、**仅标准库**;输入先用
+  `storage_file_read_to_local` 下载进沙箱,产物交付用户走 `storage_file_upload` + 下载标记。
+- 资源限额:CPU ≤ 30s、内存 ≤ 1GB、总运行 ≤ 60s,超限即被终止。
+- 完整规则见 `references/python-rules.md`。

+ 2 - 2
service/cmd/mcp/.kiro/steering/file-output-rules.md → deploy/kiro/skills/fwh-toolbox-rules/references/file-output-rules.md

@@ -1,4 +1,4 @@
-# 文件输出规则
+# 文件输出规则(完整)
 
 ## 调用规范
 
@@ -111,7 +111,7 @@ $$FILE_DOWNLOAD:{"key":"download/jerry/sess_abc123/a1b2c3d4_report.csv","fileNam
 
 ### 判断标准
 
-- 用户说"下载"、"导出"、"生成报告"、"保存为文件" → 使用 storage_file_write 或 storage_file_upload + 输出标记
+- 用户说“下载”、“导出”、“生成报告”、“保存为文件” → 使用 storage_file_write 或 storage_file_upload + 输出标记
 - 文本内容直接写入 → `storage_file_write`
 - 本地已有文件需要上传 / 二进制文件 → `storage_file_upload`(传入 `localPath`)
 - 中间计算结果、日志 → 写入 /tmp/fwh-toolbox-agent

+ 3 - 7
service/cmd/mcp/.kiro/steering/officecli-batch-rules.md → deploy/kiro/skills/fwh-toolbox-rules/references/officecli-batch-rules.md

@@ -1,7 +1,3 @@
----
-inclusion: always
----
-
 # officecli 批量写入规则
 
 本规则适用于通过 `@officecli` MCP 工具操作 `.xlsx` / `.docx` / `.pptx` 的所有批量写入场景。
@@ -10,7 +6,7 @@ inclusion: always
 
 ### 1. 禁止使用 stdin(`--input -`)
 当前 agent 通过 MCP 调用 officecli,只能传递一个 `command` 字符串参数,**没有 stdin 管道**。
-`--input -` 会让进程启动后一直阻塞等待标准输入,永远等不到数据,表现为"卡住无响应"直到被取消。
+`--input -` 会让进程启动后一直阻塞等待标准输入,永远等不到数据,表现为“卡住无响应”直到被取消。
 
 - 禁止:`batch <file> --input -`
 - 禁止:任何依赖管道 `|` 或 heredoc `<<EOF` 向 officecli 喂数据的写法(MCP 环境不存在 shell 管道)
@@ -26,7 +22,7 @@ batch <file> --commands '[
 ```
 
 - 始终带 `--json`,便于读取 `matched` / 成功失败计数 / `atomicRolledBack` 等结构化结果。
-- batch 默认原子执行:任一条失败则整批回滚,文件保持批处理前的状态。需要"保留已成功项"时才加 `--best-effort`。
+- batch 默认原子执行:任一条失败则整批回滚,文件保持批处理前的状态。需要“保留已成功项”时才加 `--best-effort`。
 
 ### 3. 不要退化成逐条 `set`
 逐条 `set`(一格/一行一条命令)只作为**最后兜底**,不是默认方案。
@@ -50,4 +46,4 @@ batch <file> --commands '[
 1. `batch --commands '[...]' --json` —— 默认,绝大多数场景用这个
 2. `batch --input <真实文件> --json` —— 仅超大批量
 3. 逐条 `set` —— 仅当以上都不可行时的兜底
-4. `--input -`(stdin)—— **永远不用**
+4. `--input -`(stdin) —— **永远不用**

+ 5 - 5
service/cmd/mcp/.kiro/steering/ppt-template-rules.md → deploy/kiro/skills/fwh-toolbox-rules/references/ppt-template-rules.md

@@ -2,13 +2,13 @@
 
 ## 触发条件
 当用户提到以下关键词时,自动使用公司 PPT 模板:
-- "公司模板"
-- "company template"
-- "用模板生成 PPT"
+- “公司模板”
+- “company template”
+- “用模板生成 PPT”
 
 ## 模板路径
-- 默认模板:`/home/jerry-remote/templates/company-template.pptx`
-- 拷贝模版:`cp /home/jerry-remote/templates/company-template.pptx /tmp/fwh-toolbox-agent/ppt/{file_name}.pptx`
+- 默认模板:`__KIRO_HOME__/templates/company-template.pptx`
+- 拷贝模版:`cp __KIRO_HOME__/templates/company-template.pptx /tmp/fwh-toolbox-agent/ppt/{file_name}.pptx`
 - 如有多个模板,用户指定名称时匹配对应文件
 
 ## 生成规则

+ 49 - 0
deploy/kiro/skills/fwh-toolbox-rules/references/python-rules.md

@@ -0,0 +1,49 @@
+# Python3 使用规则(沙箱内执行)
+
+本平台的 Python3 只能通过沙箱执行器 `fwh-pyrun` 运行。`fwh-pyrun` 用 bwrap 建立强制隔离,
+并施加资源限额;**沙箱是内核级强制边界**,以下约束即便脚本不遵守也会被系统拦截,但仍需遵守
+以免脚本报错或产出无效结果。
+
+## 何时使用 Python3
+
+- 仅当 `@fwh-tools` / `@officecli` / `knowledge` 等工具**无法完成**的自定义数据处理、计算、格式转换时,才写 Python。
+- 能用现成工具完成的,优先用工具,不要写 Python 重复实现。
+
+## 唯一允许的运行方式
+
+1. 先用 `write` 把脚本写到沙箱目录,建议放 `/tmp/fwh-toolbox-agent/scripts/<name>.py`。
+2. 通过下面的命令执行(这是**唯一**被放行的方式):
+   ```
+   __KIRO_HOME__/bin/fwh-pyrun /tmp/fwh-toolbox-agent/scripts/<name>.py [参数...]
+   ```
+3. **禁止**以下写法(配置层 `allowedCommands` 会直接拒绝,不要尝试):
+   - 直接 `python3 xxx` / `python xxx`
+   - `python3 -c "..."`(内联代码)
+   - 任何命令拼接或管道:`; | &`
+   - 运行沙箱目录之外、或非 `.py` 结尾的脚本
+
+## 脚本内约束(沙箱已强制,遵守以免失败)
+
+- **文件范围**:只能读写 `/tmp/fwh-toolbox-agent/` 下的文件。沙箱外路径(`/home`、项目目录、
+  `/etc` 写入、凭据等)不可见或不可写,访问会抛 `FileNotFoundError` / `PermissionError`。
+- **无网络**:沙箱内没有网络接口。不要 `import requests` / `urllib` / `socket` 发起网络请求,会失败。
+  需要外部数据时,改用 MCP 数据工具(`@fwh-tools/data_*` 等)取回后再交给脚本处理。
+- **仅标准库**:只用 Python 标准库。不要 `pip install`(沙箱只读且无网,必然失败)。
+- **输入文件**:需要处理的远端/对象存储文件,先用 `storage_file_read_to_local` 下载到
+  `/tmp/fwh-toolbox-agent/downloads/`,再在脚本里按本地路径读取。
+- **输出文件**:脚本产物写到 `/tmp/fwh-toolbox-agent/` 下;若需交付用户,脚本运行后由你用
+  `storage_file_upload` 上传并按 `references/file-output-rules.md` 输出 `$$FILE_DOWNLOAD:...$$` 标记
+  (上传动作由工具完成,不在脚本内做)。
+
+## 资源限制(会被强制,超限即终止)
+
+- CPU 时间 ≤ 30 秒,虚拟内存 ≤ 1GB,单文件写入 ≤ ~128MB,总运行 ≤ 60 秒。
+- 若数据量大导致超时/超内存:改为分批处理、流式读取,或改用 MCP 数据工具在数据侧聚合,
+  减少 Python 侧的数据量。
+
+## 典型流程示例
+
+1. `write` → `/tmp/fwh-toolbox-agent/scripts/analyze.py`(只用标准库,读写沙箱内路径)。
+2. 若需输入数据:先 `@fwh-tools` 查询或 `storage_file_read_to_local` 下载到沙箱。
+3. 执行:`__KIRO_HOME__/bin/fwh-pyrun /tmp/fwh-toolbox-agent/scripts/analyze.py`。
+4. 读取脚本产物(沙箱内),如需交付用户再 `storage_file_upload` + 输出下载标记。

+ 0 - 0
service/cmd/mcp/.kiro/skills/fwh-toolbox-workflow/SKILL.md → deploy/kiro/skills/fwh-toolbox-workflow/SKILL.md


+ 0 - 0
service/cmd/mcp/.kiro/skills/fwh-toolbox-workflow/references/data-direct-query.md → deploy/kiro/skills/fwh-toolbox-workflow/references/data-direct-query.md


+ 0 - 0
service/cmd/mcp/.kiro/skills/fwh-toolbox-workflow/references/fwh-pro-api.md → deploy/kiro/skills/fwh-toolbox-workflow/references/fwh-pro-api.md


+ 0 - 0
service/cmd/mcp/.kiro/skills/fwh-toolbox-workflow/references/plane-collaboration.md → deploy/kiro/skills/fwh-toolbox-workflow/references/plane-collaboration.md


+ 1 - 1
service/cmd/mcp/.kiro/skills/tou-template-scheduling/SKILL.md → deploy/kiro/skills/tou-template-scheduling/SKILL.md

@@ -8,7 +8,7 @@ description: 编写、校验并保存 FranklinWH 的 TOU(分时电价)调度
 用于构建并保存 EMS 调度(TOU)模板。一个模板 = 基础信息 + 一个或多个「季节/日类型」策略;每个策略 = 覆盖一整天的时段列表。它对应云端 `updateOrSaveIotTouTemplate` 接口和设备侧 355/357 调度协议。
 
 参考资料(完整协议与字段语义):
-`/home/jerry-remote/web/FranklinWH-Wiki/client/docs/Software/ScriptingForSchedulingConfiguration.md`
+`__WIKI_DOCS__/Software/ScriptingForSchedulingConfiguration.md`
 
 字段权威定义(Go DTO):`service/dto/fwhTestTouTemplateDTO.go`
 

+ 0 - 0
service/cmd/mcp/.kiro/steering/graphify.md → deploy/kiro/steering/graphify.md


BIN=BIN
deploy/kiro/templates/company-template.pptx


+ 21 - 4
deploy/scripts/deploy.sh

@@ -5,16 +5,18 @@
 # 作用:
 #   1. 构建前端(pnpm build)并同步到静态目录
 #   2. 构建后端主程序 fwh-toolbox-server 与 fwh-mcp-server,安装 systemd 服务
+#   3. 部署 Kiro CLI 配置(agents/settings/steering/skills)到 server/.kiro
 #
 # 目录约定(与线上服务器保持一致,可用环境变量覆盖):
 #   DEPLOY_ROOT   /var/www/fwh-toolbox
 #     ├── fwh-toolbox/   前端静态文件(构建产物 dist/*)
-#     └── server/        后端二进制 + config.env + cert/
+#     └── server/        后端二进制 + config.env + cert/ + .kiro/(kiro-cli 配置)
 #
 # 用法:
-#   ./deploy.sh all        # 前端 + 后端(默认)
+#   ./deploy.sh all        # 前端 + 后端 + Kiro 配置(默认)
 #   ./deploy.sh frontend   # 仅前端
 #   ./deploy.sh backend    # 仅后端
+#   ./deploy.sh kiro       # 仅 Kiro CLI 配置(含 skills 安装/拷贝)
 #
 # 前置条件:
 #   - 已安装 Go、Node、pnpm(见 docs/DEPLOYMENT.md)
@@ -37,6 +39,10 @@ MCP_NAME="fwh-mcp-server"
 SERVICE_FILE="/etc/systemd/system/${APP_NAME}.service"
 RUN_USER="${RUN_USER:-jerry-remote}"
 
+# Kiro CLI 配置部署(AI 对话依赖):目标用户与 HOME 需与后端拉起 kiro-cli 时一致
+KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER:-${RUN_USER}}"
+KIRO_HOME="${KIRO_HOME:-${SERVER_DIR}}"
+
 info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
 ok()   { echo -e "\033[1;32m[ OK ]\033[0m $*"; }
 
@@ -117,13 +123,24 @@ deploy_backend() {
     ok "后端部署完成"
 }
 
+# ---------------------------------------------------------------------------
+# Kiro CLI 配置(agents / settings / steering / skills)
+# ---------------------------------------------------------------------------
+deploy_kiro() {
+    info "部署 Kiro CLI 配置到 ${KIRO_HOME}/.kiro ..."
+    KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER}" KIRO_HOME="${KIRO_HOME}" \
+        bash "${SCRIPT_DIR}/deploy_kiro_cli.sh"
+    ok "Kiro CLI 配置部署完成"
+}
+
 main() {
     local target="${1:-all}"
     case "${target}" in
         frontend) deploy_frontend ;;
         backend)  deploy_backend ;;
-        all)      deploy_frontend; deploy_backend ;;
-        *) echo "用法: $0 [all|frontend|backend]"; exit 1 ;;
+        kiro)     deploy_kiro ;;
+        all)      deploy_frontend; deploy_backend; deploy_kiro ;;
+        *) echo "用法: $0 [all|frontend|backend|kiro]"; exit 1 ;;
     esac
     echo "===== 部署结束 ====="
 }

+ 468 - 0
deploy/scripts/deploy_kiro_cli.sh

@@ -0,0 +1,468 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - Kiro CLI 配置部署脚本
+# =============================================================================
+# 作用:把版本库内的 kiro-cli 配置(agents / steering / skills)部署到
+#   后端运行用户的 HOME 下的 .kiro 目录,供后端以 ACP 子进程方式拉起的 kiro-cli
+#   加载使用。
+#
+#   注意:settings/ 由 kiro-cli 自动生成,不纳入部署(不覆盖目标机自身的设置)。
+#
+#   配置来源(版本库内,权威配置):
+#     deploy/kiro/
+#       ├── agents/     Agent 定义(*.json)
+#       ├── steering/   Steering 规则(*.md)
+#       └── skills/     仅自定义 skill(skills.sh 生态的通过命令安装,不入此目录)
+#
+#   部署目标(与 install_kiro_cli.sh 的 KIRO_HOME 保持一致):
+#     ${KIRO_HOME}/.kiro           默认 /var/www/fwh-toolbox/server/.kiro
+#       ├── agents/                ← 拷贝同步
+#       ├── steering/              ← 拷贝同步
+#       ├── settings/              ← 不部署(kiro-cli 自动生成)
+#       └── skills/
+#             ├── <skills.sh 生态>  ← 用 `npx skills add` 命令安装(可控、可更新)
+#             └── <自定义 skill>    ← 直接拷贝(skills.sh 上不存在,无法命令安装)
+#
+# Skill 分类原则:
+#   * 来自 skills.sh 生态的 skill → 命令安装。清单外置在 ${KIRO_CONFIG_SRC}/skills-sh.list
+#     (每行 "<owner>/<repo>@<skill>",# 注释/空行忽略;文件内含安装命令备注)。
+#     新增/调整只需改该清单文件,无需改本脚本。
+#   * 非 skills.sh 的自定义 skill → 直接拷贝。**不再硬编码清单**,脚本扫描
+#     ${KIRO_CONFIG_SRC}/skills/ 下实际存在的目录逐个拷贝。新增只需把目录放进去。
+#
+# 前置条件:
+#   * 目标用户存在,且 kiro-cli 已安装(见 install_kiro_cli.sh)。
+#   * 命令安装 skill 需要 Node.js / npx(skills CLI 通过 npx 调用)。
+#   * rsync(可选,用于同步;缺失时回退到 cp)。
+#
+# 环境变量:
+#   KIRO_RUN_AS_USER   部署并运行 kiro-cli 的系统用户            (默认当前用户)
+#   KIRO_HOME          目标用户 HOME(决定 .kiro 位置)          (默认 /var/www/fwh-toolbox/server)
+#   KIRO_CONFIG_SRC    配置来源目录                              (默认 <repo>/deploy/kiro)
+#   KIRO_SKILLS_LIST   skills.sh 安装清单文件                    (默认 ${KIRO_CONFIG_SRC}/skills-sh.list)
+#   KIRO_AGENT         skills CLI 的目标 agent                   (默认 kiro-cli)
+#   KIRO_SKILLS_INSTALL  是否命令安装 skills.sh skill (y/n)       (默认 y)
+#   KIRO_WIKI_DOCS     产品文档知识库目录(替换 __WIKI_DOCS__)  (默认当前 Wiki docs 路径)
+#   KIRO_OFFICECLI_BIN officecli 可执行文件(替换 __OFFICECLI_BIN__)(默认当前 officecli 路径)
+#   KIRO_DRY_RUN       只打印将执行的动作,不落盘 (y/n)          (默认 n)
+# =============================================================================
+set -euo pipefail
+
+# ---------------------------------------------------------------------------
+# 路径与默认值
+# ---------------------------------------------------------------------------
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"          # deploy/
+PROJECT_ROOT="$(cd "${DEPLOY_DIR}/.." && pwd)"        # 仓库根目录
+
+KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER:-$(id -un)}"
+KIRO_HOME="${KIRO_HOME:-/var/www/fwh-toolbox/server}"
+KIRO_CONFIG_SRC="${KIRO_CONFIG_SRC:-${DEPLOY_DIR}/kiro}"
+KIRO_AGENT="${KIRO_AGENT:-kiro-cli}"
+KIRO_SKILLS_INSTALL="${KIRO_SKILLS_INSTALL:-y}"
+KIRO_DRY_RUN="${KIRO_DRY_RUN:-n}"
+
+# agent.json 占位符替换值(部署时可覆盖;默认取当前环境路径)
+#   __KIRO_HOME__     → ${KIRO_HOME}(fwh-pyrun、templates 的父路径)
+#   __WIKI_DOCS__     → ${KIRO_WIKI_DOCS}(产品文档知识库目录)
+#   __OFFICECLI_BIN__ → ${KIRO_OFFICECLI_BIN}(officecli 可执行文件)
+KIRO_WIKI_DOCS="${KIRO_WIKI_DOCS:-/home/jerry-remote/web/FranklinWH-Wiki/client/docs}"
+KIRO_OFFICECLI_BIN="${KIRO_OFFICECLI_BIN:-/home/jerry-remote/.local/bin/officecli}"
+
+KIRO_DEST="${KIRO_HOME%/}/.kiro"
+
+# ---------------------------------------------------------------------------
+# Skill 来源
+# ---------------------------------------------------------------------------
+# skills.sh 生态:从清单文件读取(每行 "<owner>/<repo>@<skill>",# 注释/空行忽略)
+KIRO_SKILLS_LIST="${KIRO_SKILLS_LIST:-${KIRO_CONFIG_SRC}/skills-sh.list}"
+# 自定义 skill:不再硬编码,直接扫描 ${KIRO_CONFIG_SRC}/skills/ 下实际存在的目录进行拷贝
+
+# 拷贝同步的配置子目录(settings 由 kiro-cli 自动生成,不部署)
+CONFIG_SUBDIRS=(agents steering)
+
+# ---------------------------------------------------------------------------
+# 日志
+# ---------------------------------------------------------------------------
+c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'
+info() { echo -e "${c_blue}[INFO]${c_reset} $*"; }
+ok()   { echo -e "${c_green}[ OK ]${c_reset} $*"; }
+warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
+err()  { echo -e "${c_red}[FAIL]${c_reset} $*" >&2; }
+
+has() { command -v "$1" >/dev/null 2>&1; }
+is_dry() { [[ "${KIRO_DRY_RUN}" == "y" ]]; }
+
+# 解析目标用户 HOME(仅用于日志/一致性校验;实际目标由 KIRO_HOME 决定)
+resolve_user_home() {
+    getent passwd "${KIRO_RUN_AS_USER}" | cut -d: -f6
+}
+
+# 以目标用户身份执行命令(设置 HOME=KIRO_HOME,保证 .kiro 落到目标位置)
+# dry-run 时只打印。
+as_target_user() {
+    local cmd="$*"
+    if is_dry; then
+        echo -e "${c_yellow}[DRY]${c_reset} (as ${KIRO_RUN_AS_USER}, HOME=${KIRO_HOME}) ${cmd}"
+        return 0
+    fi
+    if [[ "${KIRO_RUN_AS_USER}" == "$(id -un)" ]]; then
+        HOME="${KIRO_HOME}" bash -lc "${cmd}"
+    else
+        sudo -u "${KIRO_RUN_AS_USER}" -H env HOME="${KIRO_HOME}" bash -lc "${cmd}"
+    fi
+}
+
+# 以目标用户身份 / 或 sudo 执行文件系统操作(run_fs "<shell>")
+run_fs() {
+    local cmd="$*"
+    if is_dry; then
+        echo -e "${c_yellow}[DRY]${c_reset} ${cmd}"
+        return 0
+    fi
+    bash -lc "${cmd}"
+}
+
+# 检查并(尽力)安装沙箱运行 Python 所需依赖:bwrap、python3。
+# 自动安装需要 sudo 与受支持的包管理器(apt/dnf/yum/zypper/pacman)。
+# 失败时给出告警但不中断部署(与 npx 缺失的处理一致)。
+ensure_sandbox_deps() {
+    local missing=()
+    has bwrap   || missing+=("bwrap")
+    has python3 || missing+=("python3")
+    if [[ ${#missing[@]} -eq 0 ]]; then
+        ok "沙箱依赖已就绪:bwrap、python3"
+        return 0
+    fi
+
+    warn "缺少沙箱依赖:${missing[*]},尝试自动安装 ..."
+
+    # 将逻辑包名映射到各发行版的实际包名
+    local pkgs_apt=() pkgs_rpm=() pkgs_pacman=()
+    local m
+    for m in "${missing[@]}"; do
+        case "$m" in
+            bwrap)   pkgs_apt+=("bubblewrap");  pkgs_rpm+=("bubblewrap"); pkgs_pacman+=("bubblewrap") ;;
+            python3) pkgs_apt+=("python3");      pkgs_rpm+=("python3");    pkgs_pacman+=("python") ;;
+        esac
+    done
+
+    if is_dry; then
+        echo -e "${c_yellow}[DRY]${c_reset} 将安装:${missing[*]}"
+        return 0
+    fi
+
+    if has apt-get; then
+        run_fs "sudo apt-get update -y && sudo apt-get install -y ${pkgs_apt[*]}" || true
+    elif has dnf; then
+        run_fs "sudo dnf install -y ${pkgs_rpm[*]}" || true
+    elif has yum; then
+        run_fs "sudo yum install -y ${pkgs_rpm[*]}" || true
+    elif has zypper; then
+        run_fs "sudo zypper install -y ${pkgs_rpm[*]}" || true
+    elif has pacman; then
+        run_fs "sudo pacman -Sy --noconfirm ${pkgs_pacman[*]}" || true
+    else
+        err "未识别的包管理器,无法自动安装:${missing[*]}"
+        err "请手动安装后重试(bwrap 对应包名通常为 bubblewrap)。"
+        return 1
+    fi
+
+    # 复检
+    local still=()
+    has bwrap   || still+=("bwrap")
+    has python3 || still+=("python3")
+    if [[ ${#still[@]} -gt 0 ]]; then
+        err "以下依赖安装后仍不可用:${still[*]}。Python3 沙箱能力将不可用。"
+        return 1
+    fi
+    ok "沙箱依赖安装完成:${missing[*]}"
+}
+
+# ---------------------------------------------------------------------------
+# 0. 前置检查
+# ---------------------------------------------------------------------------
+ensure_prereqs() {
+    if [[ ! -d "${KIRO_CONFIG_SRC}" ]]; then
+        err "配置来源不存在:${KIRO_CONFIG_SRC}"
+        return 1
+    fi
+    if ! getent passwd "${KIRO_RUN_AS_USER}" >/dev/null; then
+        err "系统用户不存在:${KIRO_RUN_AS_USER}"
+        return 1
+    fi
+    if [[ "${KIRO_SKILLS_INSTALL}" == "y" ]] && ! has npx; then
+        warn "未找到 npx(Node.js)。skills.sh skill 的命令安装将被跳过。"
+        warn "如需命令安装,请先安装 Node.js(提供 npx),或设 KIRO_SKILLS_INSTALL=n。"
+    fi
+    # Python3 沙箱依赖(bwrap + python3),供 fwh-toolbox-agent 的 fwh-pyrun 使用
+    ensure_sandbox_deps || warn "沙箱依赖未完全就绪,fwh-pyrun 可能无法运行。"
+}
+
+# ---------------------------------------------------------------------------
+# 1. 准备目标目录(属主为目标用户)
+# ---------------------------------------------------------------------------
+prepare_dest() {
+    info "准备目标目录:${KIRO_DEST}(属主 ${KIRO_RUN_AS_USER})"
+    run_fs "sudo mkdir -p '${KIRO_DEST}/skills'"
+    run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${KIRO_HOME%/}/.kiro'"
+}
+
+# ---------------------------------------------------------------------------
+# 2. 同步配置子目录(agents / settings / steering)
+# ---------------------------------------------------------------------------
+sync_config_dirs() {
+    local sub src
+    for sub in "${CONFIG_SUBDIRS[@]}"; do
+        src="${KIRO_CONFIG_SRC}/${sub}"
+        if [[ ! -d "${src}" ]]; then
+            warn "跳过(源不存在):${src}"
+            continue
+        fi
+        info "同步配置:${sub}/ → ${KIRO_DEST}/${sub}/"
+        run_fs "sudo mkdir -p '${KIRO_DEST}/${sub}'"
+        if has rsync; then
+            run_fs "sudo rsync -a --delete '${src}/' '${KIRO_DEST}/${sub}/'"
+        else
+            run_fs "sudo rm -rf '${KIRO_DEST}/${sub}'/* && sudo cp -rf '${src}/.' '${KIRO_DEST}/${sub}/'"
+        fi
+    done
+    run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${KIRO_HOME%/}/.kiro'"
+    ok "配置目录同步完成"
+}
+
+# ---------------------------------------------------------------------------
+# 3. 命令安装 skills.sh 生态的 skill(从清单文件读取)
+# ---------------------------------------------------------------------------
+# 解析清单文件:去掉 # 注释与空行,输出每行 "<owner>/<repo>@<skill>"
+read_skills_list() {
+    [[ -f "${KIRO_SKILLS_LIST}" ]] || return 0
+    sed -E 's/#.*$//; s/[[:space:]]+$//; s/^[[:space:]]+//' "${KIRO_SKILLS_LIST}" \
+        | grep -vE '^[[:space:]]*$'
+}
+
+install_skills_sh() {
+    if [[ "${KIRO_SKILLS_INSTALL}" != "y" ]]; then
+        info "跳过 skills.sh skill 命令安装(KIRO_SKILLS_INSTALL=${KIRO_SKILLS_INSTALL})"
+        return 0
+    fi
+    if [[ ! -f "${KIRO_SKILLS_LIST}" ]]; then
+        warn "未找到 skills.sh 清单文件:${KIRO_SKILLS_LIST},跳过命令安装"
+        return 0
+    fi
+    if ! has npx; then
+        warn "无 npx,跳过 skills.sh skill 命令安装"
+        return 0
+    fi
+    info "从清单安装 skills.sh skill:${KIRO_SKILLS_LIST}"
+    local entry repo skill count=0
+    while IFS= read -r entry; do
+        [[ -z "${entry}" ]] && continue
+        if [[ "${entry}" != *"@"* ]]; then
+            warn "清单行格式不符(应为 owner/repo@skill),跳过:${entry}"
+            continue
+        fi
+        repo="${entry%@*}"
+        skill="${entry##*@}"
+        info "命令安装 skill:${repo} --skill ${skill} → agent ${KIRO_AGENT}"
+        # 以目标用户身份、HOME=KIRO_HOME 运行,skill 落到 ${KIRO_DEST}/skills
+        as_target_user "cd '${KIRO_HOME}' && npx --yes skills add '${repo}' --skill '${skill}' --agent '${KIRO_AGENT}' --yes" \
+            || warn "skill 安装失败(可稍后重试):${entry}"
+        count=$((count+1))
+    done < <(read_skills_list)
+    ok "skills.sh skill 命令安装完成(共 ${count} 个)"
+}
+
+# ---------------------------------------------------------------------------
+# 4. 拷贝自定义 skill(扫描 ${KIRO_CONFIG_SRC}/skills/ 实际存在的目录)
+# ---------------------------------------------------------------------------
+copy_custom_skills() {
+    local skills_src="${KIRO_CONFIG_SRC}/skills"
+    if [[ ! -d "${skills_src}" ]]; then
+        info "无自定义 skill 目录(${skills_src}),跳过拷贝"
+        return 0
+    fi
+    local dir name dest count=0
+    for dir in "${skills_src}"/*/; do
+        [[ -d "${dir}" ]] || continue
+        name="$(basename "${dir}")"
+        dest="${KIRO_DEST}/skills/${name}"
+        info "拷贝自定义 skill:${name} → ${dest}"
+        if has rsync; then
+            run_fs "sudo rsync -a --delete '${skills_src}/${name}/' '${dest}/'"
+        else
+            run_fs "sudo rm -rf '${dest}' && sudo mkdir -p '${dest}' && sudo cp -rf '${skills_src}/${name}/.' '${dest}/'"
+        fi
+        count=$((count+1))
+    done
+    run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${KIRO_DEST}/skills'"
+    ok "自定义 skill 拷贝完成(共 ${count} 个)"
+}
+
+# ---------------------------------------------------------------------------
+# 渲染占位符(部署后按目标环境替换 agents 与 skills 中的路径)
+# ---------------------------------------------------------------------------
+# 版本库中的 agents/*.json 与 skills/**/*.md 用占位符表示环境相关路径,
+# 部署到目标后在此统一替换为实值:
+#   __KIRO_HOME__     → ${KIRO_HOME}       (fwh-pyrun、templates 的父路径)
+#   __WIKI_DOCS__     → ${KIRO_WIKI_DOCS}   (产品文档知识库目录)
+#   __OFFICECLI_BIN__ → ${KIRO_OFFICECLI_BIN}(officecli 可执行文件)
+# 注意:需在 agents / skills 全部拷贝到目标之后调用。
+render_placeholders() {
+    info "替换占位符:__KIRO_HOME__=${KIRO_HOME%/}  __WIKI_DOCS__=${KIRO_WIKI_DOCS}  __OFFICECLI_BIN__=${KIRO_OFFICECLI_BIN}"
+    # 用 # 作为 sed 分隔符(路径含 /);当前替换值不含 #
+    local sed_expr="s#__KIRO_HOME__#${KIRO_HOME%/}#g; s#__WIKI_DOCS__#${KIRO_WIKI_DOCS%/}#g; s#__OFFICECLI_BIN__#${KIRO_OFFICECLI_BIN}#g"
+
+    # agents/*.json
+    if [[ -d "${KIRO_DEST}/agents" ]]; then
+        run_fs "sudo find '${KIRO_DEST}/agents' -maxdepth 1 -type f -name '*.json' -exec sed -i -E '${sed_expr}' {} +"
+    fi
+    # skills/**/*.md
+    if [[ -d "${KIRO_DEST}/skills" ]]; then
+        run_fs "sudo find '${KIRO_DEST}/skills' -type f -name '*.md' -exec sed -i -E '${sed_expr}' {} +"
+    fi
+    run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${KIRO_HOME%/}/.kiro'"
+    ok "占位符替换完成(agents + skills)"
+}
+
+# ---------------------------------------------------------------------------
+# 4.2 部署 PPT 公司模板(随部署路径走)
+# ---------------------------------------------------------------------------
+# 源:${KIRO_CONFIG_SRC}/templates/*
+# 目标:${KIRO_HOME}/templates/(agent.json 中以 __KIRO_HOME__/templates 引用)
+install_templates() {
+    local src="${KIRO_CONFIG_SRC}/templates"
+    local dest="${KIRO_HOME%/}/templates"
+    if [[ ! -d "${src}" ]]; then
+        info "无模板目录(${src}),跳过模板部署"
+        return 0
+    fi
+    info "部署模板:${src}/ → ${dest}/"
+    run_fs "sudo mkdir -p '${dest}'"
+    if has rsync; then
+        run_fs "sudo rsync -a '${src}/' '${dest}/'"
+    else
+        run_fs "sudo cp -rf '${src}/.' '${dest}/'"
+    fi
+    run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${dest}'"
+    ok "模板部署完成:${dest}"
+}
+
+# ---------------------------------------------------------------------------
+# 4.5 部署 fwh-pyrun 包装脚本(Python3 沙箱执行器,供 fwh-toolbox-agent 调用)
+# ---------------------------------------------------------------------------
+# 源:${KIRO_CONFIG_SRC}/bin/fwh-pyrun
+# 目标:${KIRO_HOME}/bin/fwh-pyrun(须与 agent.json 中 allowedCommands 的路径一致)
+# 同时确保沙箱目录 /tmp/fwh-toolbox-agent 存在(属主为运行用户)。
+install_pyrun() {
+    local src="${KIRO_CONFIG_SRC}/bin/fwh-pyrun"
+    local dest_dir="${KIRO_HOME%/}/bin"
+    local dest="${dest_dir}/fwh-pyrun"
+    if [[ ! -f "${src}" ]]; then
+        warn "未找到 fwh-pyrun 源(${src}),跳过部署(Python3 沙箱能力不可用)"
+        return 0
+    fi
+    info "部署 fwh-pyrun:${src} → ${dest}"
+    run_fs "sudo mkdir -p '${dest_dir}'"
+    run_fs "sudo cp -f '${src}' '${dest}'"
+    run_fs "sudo chmod 0755 '${dest}'"
+    run_fs "sudo chown '${KIRO_RUN_AS_USER}' '${dest}'"
+    # 沙箱可写目录(唯一持久写入点),属主为运行用户
+    run_fs "sudo mkdir -p /tmp/fwh-toolbox-agent && sudo chown '${KIRO_RUN_AS_USER}' /tmp/fwh-toolbox-agent"
+    ok "fwh-pyrun 部署完成:${dest}"
+}
+
+# ---------------------------------------------------------------------------
+# 5. 校验
+# ---------------------------------------------------------------------------
+verify() {
+    info "校验部署结果 ..."
+    if is_dry; then
+        info "(dry-run) 跳过实际校验"
+        return 0
+    fi
+    local sub
+    for sub in "${CONFIG_SUBDIRS[@]}" skills; do
+        if [[ -d "${KIRO_DEST}/${sub}" ]]; then
+            ok "存在:${KIRO_DEST}/${sub}"
+        else
+            warn "缺失:${KIRO_DEST}/${sub}"
+        fi
+    done
+    local name dir
+    for dir in "${KIRO_CONFIG_SRC}/skills"/*/; do
+        [[ -d "${dir}" ]] || continue
+        name="$(basename "${dir}")"
+        [[ -f "${KIRO_DEST}/skills/${name}/SKILL.md" ]] \
+            && ok "自定义 skill 就绪:${name}" \
+            || warn "自定义 skill 缺 SKILL.md:${name}"
+    done
+    if has npx && [[ "${KIRO_SKILLS_INSTALL}" == "y" ]]; then
+        info "已安装 skill(项目级,HOME=${KIRO_HOME}):"
+        as_target_user "cd '${KIRO_HOME}' && npx --yes skills list 2>/dev/null" || true
+    fi
+
+    # fwh-pyrun 沙箱执行器校验
+    local pyrun="${KIRO_HOME%/}/bin/fwh-pyrun"
+    if [[ -x "${pyrun}" ]]; then
+        ok "fwh-pyrun 就绪:${pyrun}"
+    else
+        warn "fwh-pyrun 缺失或不可执行:${pyrun}(Python3 沙箱能力不可用)"
+    fi
+    has bwrap   && ok "bwrap 可用:$(command -v bwrap)"   || warn "bwrap 不可用(Python3 沙箱能力不可用)"
+    has python3 && ok "python3 可用:$(command -v python3)" || warn "python3 不可用(Python3 沙箱能力不可用)"
+
+    # 模板校验
+    if [[ -d "${KIRO_HOME%/}/templates" ]]; then
+        ok "模板目录就绪:${KIRO_HOME%/}/templates"
+    else
+        warn "模板目录缺失:${KIRO_HOME%/}/templates"
+    fi
+
+    # 占位符残留校验(替换后不应再出现 __XXX__)
+    if grep -rlE '__(KIRO_HOME|WIKI_DOCS|OFFICECLI_BIN)__' "${KIRO_DEST}/agents" "${KIRO_DEST}/skills" >/dev/null 2>&1; then
+        warn "agents/skills 中仍存在未替换的占位符,请检查 render_placeholders"
+    else
+        ok "占位符已全部替换(agents + skills)"
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# main
+# ---------------------------------------------------------------------------
+main() {
+    info "== 部署 Kiro CLI 配置 =="
+    info "运行用户   = ${KIRO_RUN_AS_USER}(passwd HOME=$(resolve_user_home))"
+    info "配置来源   = ${KIRO_CONFIG_SRC}"
+    info "部署目标   = ${KIRO_DEST}"
+    info "目标 agent = ${KIRO_AGENT}"
+    info "命令安装   = ${KIRO_SKILLS_INSTALL}   dry-run = ${KIRO_DRY_RUN}"
+    if [[ "$(resolve_user_home)" != "${KIRO_HOME%/}" ]]; then
+        warn "KIRO_HOME(${KIRO_HOME}) 与该用户 passwd 家目录($(resolve_user_home)) 不一致;"
+        warn "请确认 config.env 的 KIRO_HOME 与后端拉起 kiro-cli 时的 HOME 一致。"
+    fi
+
+    ensure_prereqs
+    prepare_dest
+    sync_config_dirs
+    install_skills_sh
+    copy_custom_skills
+    install_templates
+    install_pyrun
+    render_placeholders
+    verify
+
+    echo
+    echo -e "${c_green}Kiro CLI 配置部署结束${c_reset}"
+    cat <<EOF
+  目标目录   : ${KIRO_DEST}
+  已同步     : ${CONFIG_SUBDIRS[*]}
+  命令安装源 : ${KIRO_SKILLS_LIST}
+  拷贝源     : ${KIRO_CONFIG_SRC}/skills/(扫描实际目录)
+  模板目录   : ${KIRO_HOME%/}/templates(公司 PPT 模板等)
+  沙箱执行器 : ${KIRO_HOME%/}/bin/fwh-pyrun(bwrap 隔离 Python3)
+  占位符替换 : __KIRO_HOME__=${KIRO_HOME%/}  __WIKI_DOCS__=${KIRO_WIKI_DOCS}  __OFFICECLI_BIN__=${KIRO_OFFICECLI_BIN}
+EOF
+}
+
+main "$@"

+ 194 - 0
deploy/scripts/sync_kiro_config.sh

@@ -0,0 +1,194 @@
+#!/usr/bin/env bash
+# =============================================================================
+# FranklinWH Toolbox - Kiro CLI 配置单向同步脚本(A → B)
+# =============================================================================
+# 作用:把「版本库配置源」单向推送到「线上部署目标」。
+#
+#   A 端(版本库源):deploy/kiro/
+#   B 端(部署目标):${KIRO_HOME}/.kiro   默认 /var/www/fwh-toolbox/server/.kiro
+#
+# 同步内容(一律 A → B,不回写):
+#   * steering/            覆盖同步(rsync --delete,B 端该目录与 A 端一致)
+#   * agents/              覆盖同步(rsync --delete)
+#   * 自定义 skill          扫描 A 端 skills/ 下的目录,逐个覆盖同步到 B 端
+#                          (每个 skill 目录内部 --delete,不影响 B 端其它 skill)
+#   * skills.sh 生态 skill  读取 skills-sh.list,在 B 端用 `npx skills add` 安装
+#                          (要装新的:先在 skills-sh.list 加一行,再跑本脚本)
+#
+# 不同步:settings/(kiro-cli 自动生成)。
+# 安全:顶层 skills/ 不做 --delete,命令安装的 skill 目录不会被误删。
+#
+# 用法:
+#   ./sync_kiro_config.sh                    # 执行同步
+#   KIRO_HOME=/path ./sync_kiro_config.sh    # 指定 B 端 HOME
+#   KIRO_DRY_RUN=y ./sync_kiro_config.sh     # 预演,只打印动作不落盘
+#   KIRO_SKILLS_INSTALL=n ./sync_kiro_config.sh  # 跳过 skills.sh 命令安装,只同步文件
+#
+# 环境变量:
+#   KIRO_HOME            B 端 HOME(决定 .kiro 位置)   (默认 /var/www/fwh-toolbox/server)
+#   KIRO_RUN_AS_USER     B 端属主 / 运行 kiro-cli 的用户 (默认当前用户)
+#   A_DIR                A 端目录                       (默认 <repo>/deploy/kiro)
+#   KIRO_SKILLS_LIST     skills.sh 安装清单文件         (默认 ${A_DIR}/skills-sh.list)
+#   KIRO_AGENT           skills CLI 目标 agent          (默认 kiro-cli)
+#   KIRO_SKILLS_INSTALL  是否命令安装 skills.sh skill    (默认 y)
+#   KIRO_DRY_RUN         只打印不落盘 (y/n)             (默认 n)
+# =============================================================================
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+DEPLOY_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"           # deploy/
+
+KIRO_HOME="${KIRO_HOME:-/var/www/fwh-toolbox/server}"
+KIRO_RUN_AS_USER="${KIRO_RUN_AS_USER:-$(id -un)}"
+A_DIR="${A_DIR:-${DEPLOY_DIR}/kiro}"
+B_DIR="${KIRO_HOME%/}/.kiro"
+KIRO_SKILLS_LIST="${KIRO_SKILLS_LIST:-${A_DIR}/skills-sh.list}"
+KIRO_AGENT="${KIRO_AGENT:-kiro-cli}"
+KIRO_SKILLS_INSTALL="${KIRO_SKILLS_INSTALL:-y}"
+KIRO_DRY_RUN="${KIRO_DRY_RUN:-n}"
+
+# A→B 覆盖同步的配置子目录
+SYNC_SUBDIRS=(steering agents)
+
+c_reset='\033[0m'; c_blue='\033[1;34m'; c_green='\033[1;32m'; c_yellow='\033[1;33m'; c_red='\033[1;31m'
+info() { echo -e "${c_blue}[INFO]${c_reset} $*"; }
+ok()   { echo -e "${c_green}[ OK ]${c_reset} $*"; }
+warn() { echo -e "${c_yellow}[WARN]${c_reset} $*"; }
+err()  { echo -e "${c_red}[FAIL]${c_reset} $*" >&2; }
+
+has() { command -v "$1" >/dev/null 2>&1; }
+is_dry() { [[ "${KIRO_DRY_RUN}" == "y" ]]; }
+
+# 执行文件系统命令(dry-run 只打印)
+run_fs() {
+    if is_dry; then echo -e "${c_yellow}[DRY]${c_reset} $*"; return 0; fi
+    bash -lc "$*"
+}
+
+# 以 B 端运行用户身份执行(HOME=KIRO_HOME),用于 npx skills add
+as_target_user() {
+    local cmd="$*"
+    if is_dry; then
+        echo -e "${c_yellow}[DRY]${c_reset} (as ${KIRO_RUN_AS_USER}, HOME=${KIRO_HOME}) ${cmd}"
+        return 0
+    fi
+    if [[ "${KIRO_RUN_AS_USER}" == "$(id -un)" ]]; then
+        HOME="${KIRO_HOME}" bash -lc "${cmd}"
+    else
+        sudo -u "${KIRO_RUN_AS_USER}" -H env HOME="${KIRO_HOME}" bash -lc "${cmd}"
+    fi
+}
+
+# 扫描 A 端 skills/ 下实际存在的自定义 skill 目录名
+custom_skill_names() {
+    local d
+    [[ -d "${A_DIR}/skills" ]] || return 0
+    for d in "${A_DIR}/skills"/*/; do
+        [[ -d "${d}" ]] && basename "${d}"
+    done
+}
+
+# 读取 skills.sh 清单:去注释/空行,输出每行 "<owner>/<repo>@<skill>"
+read_skills_list() {
+    [[ -f "${KIRO_SKILLS_LIST}" ]] || return 0
+    sed -E 's/#.*$//; s/[[:space:]]+$//; s/^[[:space:]]+//' "${KIRO_SKILLS_LIST}" \
+        | grep -vE '^[[:space:]]*$'
+}
+
+# rsync A→B,一个子路径(带 --delete,作用范围仅该子目录)
+rsync_dir() {
+    local rel="$1"                       # 如 steering、skills/fwh-toolbox-rules
+    local src="${A_DIR}/${rel}"
+    local dst="${B_DIR}/${rel}"
+    [[ -d "${src}" ]] || { warn "跳过(源不存在):${src}"; return 0; }
+    run_fs "sudo mkdir -p '${dst}'"
+    if has rsync; then
+        run_fs "sudo rsync -a --delete '${src}/' '${dst}/'"
+    else
+        run_fs "sudo rm -rf '${dst}'/* && sudo cp -rf '${src}/.' '${dst}/'"
+    fi
+}
+
+# ---------------------------------------------------------------------------
+# 1. 同步 steering / agents(覆盖)
+# ---------------------------------------------------------------------------
+sync_config_dirs() {
+    local sub
+    for sub in "${SYNC_SUBDIRS[@]}"; do
+        info "同步 ${sub}/  (A→B 覆盖)"
+        rsync_dir "${sub}"
+    done
+}
+
+# ---------------------------------------------------------------------------
+# 2. 同步自定义 skill(逐个目录覆盖,扫描 A 端)
+# ---------------------------------------------------------------------------
+sync_custom_skills() {
+    local name count=0
+    while IFS= read -r name; do
+        [[ -z "${name}" ]] && continue
+        info "同步自定义 skill:skills/${name}/  (A→B 覆盖)"
+        rsync_dir "skills/${name}"
+        count=$((count+1))
+    done < <(custom_skill_names)
+    [[ ${count} -eq 0 ]] && info "A 端无自定义 skill" || ok "自定义 skill 同步完成(共 ${count} 个)"
+}
+
+# ---------------------------------------------------------------------------
+# 3. 命令安装 skills.sh 生态 skill(读清单,在 B 端安装)
+# ---------------------------------------------------------------------------
+install_skills_sh() {
+    if [[ "${KIRO_SKILLS_INSTALL}" != "y" ]]; then
+        info "跳过 skills.sh 命令安装(KIRO_SKILLS_INSTALL=${KIRO_SKILLS_INSTALL})"
+        return 0
+    fi
+    if [[ ! -f "${KIRO_SKILLS_LIST}" ]]; then
+        warn "未找到清单文件:${KIRO_SKILLS_LIST},跳过命令安装"
+        return 0
+    fi
+    if ! has npx; then
+        warn "无 npx,跳过 skills.sh 命令安装"
+        return 0
+    fi
+    info "从清单安装 skills.sh skill:${KIRO_SKILLS_LIST}"
+    local entry repo skill count=0
+    while IFS= read -r entry; do
+        [[ -z "${entry}" ]] && continue
+        if [[ "${entry}" != *"@"* ]]; then
+            warn "清单行格式不符(应为 owner/repo@skill),跳过:${entry}"
+            continue
+        fi
+        repo="${entry%@*}"
+        skill="${entry##*@}"
+        info "安装 skill:${repo} --skill ${skill} → agent ${KIRO_AGENT}"
+        as_target_user "cd '${KIRO_HOME}' && npx --yes skills add '${repo}' --skill '${skill}' --agent '${KIRO_AGENT}' --yes" \
+            || warn "skill 安装失败(可稍后重试):${entry}"
+        count=$((count+1))
+    done < <(read_skills_list)
+    ok "skills.sh skill 命令安装完成(共 ${count} 个)"
+}
+
+# ---------------------------------------------------------------------------
+# main
+# ---------------------------------------------------------------------------
+main() {
+    info "== Kiro 配置单向同步(A → B)=="
+    info "A 端(版本库)  :${A_DIR}"
+    info "B 端(部署目标):${B_DIR}"
+    info "自定义 skill    :$(custom_skill_names | tr '\n' ' ')"
+    info "skills.sh 清单  :${KIRO_SKILLS_LIST}(命令安装=${KIRO_SKILLS_INSTALL})"
+    info "dry-run=${KIRO_DRY_RUN}"
+
+    if [[ ! -d "${A_DIR}" ]]; then err "A 端不存在:${A_DIR}"; exit 1; fi
+
+    sync_config_dirs
+    sync_custom_skills
+    install_skills_sh
+
+    # 属主归还给 B 端运行用户
+    run_fs "sudo chown -R '${KIRO_RUN_AS_USER}' '${B_DIR}'"
+
+    ok "同步结束"
+}
+
+main "$@"

+ 0 - 110
service/cmd/mcp/.kiro/agents/fwh-toolbox-agent.json

@@ -1,110 +0,0 @@
-{
-	"name": "fwh-toolbox-agent",
-	"description": "fwh-toolbox平台AI助手",
-	"prompt": "你是一个本地平台专业分析工具。使用本地知识库和MCP server读取数据,实现数据分析。不可创建/修改文件,只能通过MCP将文件生成到Minio服务器。",
-	"tools": [
-		"read", "write", "grep", "glob", "code", "web_fetch", "web_search", "knowledge", "subagent", "todo_list", "introspect", "shell",
-		"@fwh-tools", "@drawio", "@officecli"
-	],
-	"allowedTools": [
-		"read", "grep", "glob", "code", "web_fetch", "web_search", "knowledge", "subagent", "todo_list", "introspect",
-		"@drawio", "@officecli",
-		"@fwh-tools/service_site_query_info",
-		"@fwh-tools/service_site_search",
-		"@fwh-tools/service_site_power_flow",
-		"@fwh-tools/service_sim_query",
-		"@fwh-tools/service_sim_search",
-		"@fwh-tools/service_sim_monthly_data",
-		"@fwh-tools/service_monitor_alarm_stats",
-		"@fwh-tools/service_monitor_shutdown_stats",
-		"@fwh-tools/service_feedback_history",
-		"@fwh-tools/service_dashboard_shutdown_date",
-		"@fwh-tools/service_dashboard_shutdown_detail",
-		"@fwh-tools/service_dashboard_alarm_detail",
-		"@fwh-tools/service_background_task_list",
-		"@fwh-tools/service_tou_template_list",
-		"@fwh-tools/service_tou_template_detail",
-		"@fwh-tools/service_site_monitor_config_list",
-		"@fwh-tools/service_site_monitor_name_list",
-		"@fwh-tools/service_site_monitor_config_get",
-		"@fwh-tools/service_site_monitor_base_info",
-		"@fwh-tools/service_site_monitor_mode_info",
-		"@fwh-tools/service_site_monitor_alarm_list",
-		"@fwh-tools/service_site_info_update_status",
-		"@fwh-tools/service_site_info_tags_list",
-		"@fwh-tools/service_auto_update_get_options",
-		"@fwh-tools/service_auto_update_get_package",
-		"@fwh-tools/service_auto_update_get_apower_version",
-		"@fwh-tools/service_auto_update_get_config",
-		"@fwh-tools/service_auto_update_file_list",
-		"@fwh-tools/service_batch_download_list",
-		"@fwh-tools/service_batch_download_path",
-		"@fwh-tools/service_batch_download_file_tree",
-		"@fwh-tools/service_site_analysis_list",
-		"@fwh-tools/service_site_analysis_get_config",
-		"@fwh-tools/service_site_analysis_data",
-		"@fwh-tools/data_mongo_find",
-		"@fwh-tools/data_mongo_count",
-		"@fwh-tools/data_redis_get",
-		"@fwh-tools/data_redis_exists",
-		"@fwh-tools/data_pgsql_query_snapshot_file_record",
-		"@fwh-tools/data_pgsql_query_snapshot_data",
-		"@fwh-tools/data_fwh_*",
-		"@fwh-tools/data_sim_*",
-		"@fwh-tools/data_emspara_*",
-		"@fwh-tools/data_plane_*",
-		"@fwh-tools/data_plane_v2_*",
-		"@fwh-tools/storage_minio_*",
-		"@fwh-tools/storage_file_read",
-		"@fwh-tools/storage_file_list",
-		"@fwh-tools/util_*"
-	],
-	"toolsSettings": {
-		"read": {
-			"allowedPaths": ["./**", "/tmp/fwh-toolbox-agent/**", "/home/jerry-remote/web/FranklinWH-Wiki/client/docs/**"],
-			"deniedPaths": []
-		},
-		"write": {
-			"allowedPaths": ["/tmp/fwh-toolbox-agent/**"],
-			"deniedPaths": []
-		},
-		"shell": {
-			"allowedCommands": [
-				"^npx -y @firecrawl/anydoc(\\s.*)?$",
-				"^base64(\\s.*)?$",
-				"^mkdir\\s+-p\\s+/tmp/fwh-toolbox-agent(/[^\\s;|&]*)?$",
-				"^cp\\s+/home/jerry-remote/templates/[^\\s;|&]+\\s+/tmp/fwh-toolbox-agent/[^\\s;|&]+$"
-			],
-			"denyByDefault": true
-		}
-	},	
-	"resources": [
-		{
-			"type": "knowledgeBase",
-			"source": "file:///home/jerry-remote/web/FranklinWH-Wiki/client/docs",
-			"name": "FranklinWHProductDocs",
-			"description": "产品文档和指南,包括产品的软件、硬件、系统功能详细说明",
-			"indexType": "best",
-			"autoUpdate": true
-		}
-	],
-	"hooks": {},
-	"mcpServers": {
-		"fwh-tools": {
-			"command": "./fwh-mcp-server",
-			"args": [],
-			"disabled": false
-		},
-		"drawio": {
-			"command": "npx",
-			"args": ["@next-ai-drawio/mcp-server@latest"],
-			"disabled": false
-		},
-		"officecli": {
-			"command": "/home/jerry-remote/.local/bin/officecli",
-			"args": ["mcp"],
-			"disabled": false
-		}
-	},
-	"welcomeMessage": "欢迎使用fwh-toolbox平台AI助手"
-}

+ 0 - 19
service/cmd/mcp/.kiro/settings/cli.json

@@ -1,19 +0,0 @@
-{
-	"chat.enableKnowledge": true,
-	"chat.enableTodoList": true,
-	"chat.greeting.enabled": true,
-	"chat.allowAnimations": true,
-	"chat.allowAsciiArt": true,
-	"chat.allowIcons": true,
-	"knowledge.defaultIncludePatterns": ["**/*.ts", "**/*.vue", "**/*.md"],
-	"knowledge.defaultExcludePatterns": ["node_modules/**", "dist/**"],
-	"knowledge.maxFiles": 1000,
-	"knowledge.chunkSize": 512,
-	"knowledge.chunkOverlap": 50,
-	"knowledge.indexType": "best",
-	"api.timeout": 30000,
-	"mcp.initTimeout": 120000,
-	"mcp.noInteractiveTimeout": 30000,
-	"compaction.excludeContextWindowPercent": 10,
-	"compaction.excludeMessages": 5
-}

+ 0 - 26
service/cmd/mcp/.kiro/skills/convert-documents-to-markdown/SKILL.md

@@ -1,26 +0,0 @@
----
-name: convert-documents-to-markdown
-description: Convert Word (.doc, .docx), PowerPoint (.ppt, .pptx), Excel (.xls, .xlsx), OpenDocument (.odt, .ods, .odp), RTF, EPUB, CSV, and PDF files to GitHub-Flavored Markdown. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly.
-license: MIT
-metadata:
-  author: firecrawl
----
-
-# Convert documents to Markdown
-
-Run the anydoc CLI. It needs Node 20+ and no install:
-
-```bash
-npx -y @firecrawl/anydoc <file>              # Markdown to stdout
-npx -y @firecrawl/anydoc <file> -o out.md    # write to a file
-npx -y @firecrawl/anydoc - --format csv < f  # read stdin
-```
-
-Rules:
-
-1. Supported inputs: `.doc`, `.docx`, `.docm`, `.odt`, `.rtf`, `.epub`, `.pdf`, `.ppt`, `.pps`, `.pot`, `.pptx`, `.pptm`, `.ppsx`, `.ppsm`, `.odp`, `.xls`, `.xlsx`, `.xlsm`, `.xlsb`, `.ods`, `.csv`.
-2. The format is detected from the file content. Pass `--format <name>` only when detection cannot work: CSV from stdin, or a missing or wrong extension.
-3. Exit codes: 0 success, 1 the document could not be converted, 2 usage error. Failures print one `anydoc: <message>` line to stderr. The CLI never prompts.
-4. For a large document, write to a file with `-o` and read the parts you need instead of streaming everything into context.
-5. Scanned and image-only PDFs need OCR, which anydoc does not do; they fail as unsupported. The hosted [Firecrawl Parse](https://firecrawl.dev/parse) API handles those.
-6. Inside a Node, Python, or Rust codebase, prefer the library over shelling out: `@firecrawl/anydoc` on npm, `firecrawl-anydoc` on PyPI, `anydoc` on crates.io. Each exposes the same `to_markdown` / `toMarkdown` API.

+ 0 - 1
service/cmd/mcp/.kiro/skills/graphify/.graphify_version

@@ -1 +0,0 @@
-0.9.42

+ 0 - 713
service/cmd/mcp/.kiro/skills/graphify/SKILL.md

@@ -1,713 +0,0 @@
----
-name: graphify
-description: "Use for any question about a codebase, its architecture, file relationships, or project content — especially when graphify-out/ exists, where the question should be treated as a graphify query first. Turns any input (code, docs, papers, images, videos) into a persistent knowledge graph with god nodes, community detection, and query/path/explain tools."
----
-
-# /graphify
-
-Turn any folder of files into a navigable knowledge graph with community detection, an honest audit trail, and three outputs: interactive HTML, GraphRAG-ready JSON, and a plain-language GRAPH_REPORT.md.
-
-## Usage
-
-```
-/graphify                                             # full pipeline on current directory (HTML viz; add --obsidian for a vault)
-/graphify <path>                                      # full pipeline on specific path
-/graphify https://github.com/<owner>/<repo>           # clone repo then run full pipeline on it
-/graphify https://github.com/<owner>/<repo> --branch <branch>  # clone a specific branch
-/graphify <url1> <url2> ...                           # clone multiple repos, build each, merge into one cross-repo graph
-/graphify <path> --mode deep                          # thorough extraction, richer INFERRED edges
-/graphify <path> --update                             # incremental - re-extract only new/changed files
-/graphify <path> --directed                            # build directed graph (preserves edge direction: source→target)
-/graphify <path> --whisper-model medium                # use a larger Whisper model for better transcription accuracy
-/graphify <path> --cluster-only                       # rerun clustering on existing graph
-/graphify <path> --no-viz                             # skip visualization, just report + JSON
-/graphify <path> --html                               # (HTML is generated by default - this flag is a no-op)
-/graphify <path> --svg                                # also export graph.svg (embeds in Notion, GitHub)
-/graphify <path> --graphml                            # export graph.graphml (Gephi, yEd)
-/graphify <path> --neo4j                              # generate graphify-out/cypher.txt for Neo4j
-/graphify <path> --neo4j-push bolt://localhost:7687   # push directly to Neo4j
-/graphify <path> --falkordb                           # generate graphify-out/cypher.txt for FalkorDB
-/graphify <path> --falkordb-push falkordb://localhost:6379   # push directly to FalkorDB
-/graphify <path> --mcp                                # start MCP stdio server for agent access
-/graphify <path> --watch                              # watch folder, auto-rebuild on code changes (no LLM needed)
-/graphify <path> --wiki                               # build agent-crawlable wiki (index.md + one article per community)
-/graphify <path> --obsidian --obsidian-dir ~/vaults/my-project  # write vault to custom path (e.g. existing vault)
-/graphify add <url>                                   # fetch URL, save to ./raw, update graph
-/graphify add <url> --author "Name"                   # tag who wrote it
-/graphify add <url> --contributor "Name"              # tag who added it to the corpus
-/graphify query "<question>"                          # BFS traversal - broad context
-/graphify query "<question>" --dfs                    # DFS - trace a specific path
-/graphify query "<question>" --budget 1500            # cap answer at N tokens
-/graphify path "AuthModule" "Database"                # shortest path between two concepts
-/graphify explain "SwinTransformer"                   # plain-language explanation of a node
-```
-
-## What graphify is for
-
-Drop any folder of code, docs, papers, images, or video into graphify and get a queryable knowledge graph. Persistent across sessions, honest audit trail (EXTRACTED/INFERRED/AMBIGUOUS), community detection surfaces cross-document connections you wouldn't think to ask about.
-
-## What You Must Do When Invoked
-
-If the user invoked `/graphify --help` or `/graphify -h` (with no other arguments), print the contents of the `## Usage` section above verbatim and stop. Do not run any commands, do not detect files, do not default the path to `.`. Just print the Usage block and return.
-
-**Fast path — existing graph:** Before doing anything else, check whether `graphify-out/graph.json` exists. The expected location is `graphify-out/graph.json` relative to the **current working directory** (i.e. the project root where you are running commands). If it exists AND the user's request is a natural-language question about the codebase (e.g. "How does X work?", "What calls Y?", "Trace the data flow through Z") and NOT an explicit rebuild command (`--update`, `--cluster-only`, or a bare path/URL that implies fresh extraction): **skip Steps 1–5 entirely and jump straight to `## For /graphify query`.** Run `graphify query "<question>"` immediately. Do not run detect. Do not check corpus size. Do not ask the user to narrow. The graph is already built — use it.
-
-If no path was given, use `.` (current directory). Do not ask the user for a path.
-
-If the path argument starts with `https://github.com/` or `http://github.com/`, treat it as a GitHub URL - run Step 0 before anything else, then continue with the resolved local path.
-
-Follow these steps in order. Do not skip steps.
-
-### Step 0 - GitHub repos and multi-path merge (only if a URL or several paths)
-
-Only when the path is one or more `https://github.com/...` URLs, or several local subfolders to merge. See `references/github-and-merge.md` for the clone, cross-repo merge, and monorepo flow, then continue with the resolved local path. A plain local path skips this step.
-
-### Step 1 - Ensure graphify is installed
-
-```bash
-# Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs)
-PYTHON=""
-GRAPHIFY_BIN=$(which graphify 2>/dev/null)
-# 1. uv tool installs — most reliable on modern Mac/Linux
-if [ -z "$PYTHON" ] && command -v uv >/dev/null 2>&1; then
-    _UV_PY=$(uv tool run --from graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
-    if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
-fi
-# 2. Read shebang from graphify binary (pipx and direct pip installs)
-if [ -z "$PYTHON" ] && [ -n "$GRAPHIFY_BIN" ]; then
-    _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
-    case "$_SHEBANG" in
-        *[!a-zA-Z0-9/_.@-]*) ;;
-        *) "$_SHEBANG" -c "import graphify" 2>/dev/null && PYTHON="$_SHEBANG" ;;
-    esac
-fi
-# 3. Fall back to python3
-if [ -z "$PYTHON" ]; then PYTHON="python3"; fi
-if ! "$PYTHON" -c "import graphify" 2>/dev/null; then
-    if command -v uv >/dev/null 2>&1; then
-        uv tool install --upgrade graphifyy -q 2>&1 | tail -3
-        _UV_PY=$(uv tool run --from graphifyy python -c "import sys; print(sys.executable)" 2>/dev/null)
-        if [ -n "$_UV_PY" ]; then PYTHON="$_UV_PY"; fi
-    else
-        "$PYTHON" -m pip install graphifyy -q 2>/dev/null \
-          || "$PYTHON" -m pip install graphifyy -q --break-system-packages 2>&1 | tail -3
-    fi
-fi
-# Write interpreter path for all subsequent steps (persists across invocations)
-mkdir -p graphify-out
-"$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
-# Save scan root so `graphify update` (no args) knows where to look next time
-echo "$(cd INPUT_PATH && pwd)" > graphify-out/.graphify_root
-```
-
-If the import succeeds, print nothing and move straight to Step 2.
-
-**In every subsequent bash block, replace `python3` with `$(cat graphify-out/.graphify_python)` to use the correct interpreter.**
-
-### Step 2 - Detect files
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from graphify.detect import detect
-from pathlib import Path
-result = detect(Path('INPUT_PATH'))
-# Write the sidecar from Python, not a shell redirect, so the same block renders
-# on PowerShell hosts without console-encoding drift (#2528).
-Path('graphify-out/.graphify_detect.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\")
-print(f'Detected {result[\"total_files\"]} files')
-"
-```
-
-Replace INPUT_PATH with the actual path the user provided. Do NOT cat or print the JSON - read it silently and present a clean summary instead:
-
-```
-Corpus: X files · ~Y words
-  code:     N files (.py .ts .go ...)
-  docs:     N files (.md .txt ...)
-  papers:   N files (.pdf ...)
-  images:   N files
-  video:    N files (.mp4 .mp3 ...)
-```
-
-Omit any category with 0 files from the summary.
-
-Then act on it:
-- If `total_files` is 0: stop with "No supported files found in [path]."
-- If `skipped_sensitive` is non-empty: report the count and list the skipped file names, so a wrongly-flagged source or doc is visible and can be renamed or moved (#2106).
-- If `total_words` > 2,000,000 OR `total_files` > 500: show the warning. Then compute the top 5 first-level subdirectories by file count:
-  - Read `scan_root` from the detect JSON (always an absolute path to the resolved INPUT_PATH).
-  - Concatenate all file lists across all types (`code`, `document`, `paper`, `image`, `video`).
-  - Filter out any path that starts with `scan_root + "/graphify-out/"` to exclude converted sidecars.
-  - For each file, strip the `scan_root` prefix and take the first path component. Files directly in `scan_root` with no subdirectory count as `(root)`.
-  - If all files are in `(root)` with no subdirectories, do not ask to narrow — no subfolders exist. Instead suggest `--no-cluster` to skip the expensive clustering step and proceed.
-  - Otherwise rank by count, show the top 5 with file counts, then ask which subfolder to run on. Wait for the user's answer before proceeding.
-- Otherwise: proceed directly to Step 2.5 if video files were detected, or Step 3 if not.
-
-### Step 2.5 - Video and audio (only if video files detected)
-
-Skip this step entirely if `detect` returned zero `video` files. When the corpus has video or audio, see `references/transcribe.md` to transcribe them to text first, then treat the transcripts as doc files in Step 3.
-
-### Step 3 - Extract entities and relationships
-
-**Before starting:** note whether `--mode deep` was given. You must pass `DEEP_MODE=true` to every subagent in Step B2 if it was. Track this from the original invocation - do not lose it.
-
-This step has two parts: **structural extraction** (deterministic, free) and **semantic extraction** (LLM, costs tokens).
-
-> **graphify needs no API key. Never ask the user for one, and never block on one.** Code is extracted structurally (AST) with no LLM and no key at all — a code-only corpus (the common `/graphify .` on a repo) skips semantic extraction entirely, so it needs nothing here: go straight to Part A and skip Part B. Semantic extraction (only for docs, papers, and images) uses Gemini **only if** `GEMINI_API_KEY`/`GOOGLE_API_KEY` is already set; otherwise the host agent itself is the LLM. graphify does **not** read `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, or any other provider key. If you catch yourself about to prompt for, wait on, or stop because of a missing API key, that is a misread of this skill — proceed without one.
-
-**Before semantic extraction:** check whether `GEMINI_API_KEY` or `GOOGLE_API_KEY` is set. If neither is set, print this one-liner to the user:
-> Tip: set `GEMINI_API_KEY` or `GOOGLE_API_KEY` to use Gemini for semantic extraction (`pip install 'graphifyy[gemini]'`).
-
-Print it once, then continue — do not wait for the user to supply a key. If `GEMINI_API_KEY` or `GOOGLE_API_KEY` IS set, use `graphify.llm.extract_corpus_parallel(files, backend="gemini")` for semantic extraction instead of dispatching subagents. The default Gemini model is `gemini-3-flash-preview`; set `GRAPHIFY_GEMINI_MODEL` or pass `--model` in headless CLI flows to override it.
-
-> **No other API keys are read.** When `GEMINI_API_KEY`/`GOOGLE_API_KEY` are unset, semantic extraction falls to the host agent itself — the running session is the LLM. On a host that dispatches subagents (e.g. Claude Code), dispatch them as written in Part B. On a host that runs the CLI directly in a terminal and cannot dispatch subagents, do not stall: a code-only corpus has no semantic work, so write the empty semantic file (Part B "Fast path") and continue to Part C; for a corpus with docs/papers/images, either set a Gemini key or extract those inline yourself, but in no case prompt for `ANTHROPIC_API_KEY` — that prompt is a misread of this skill.
-
-**Run Part A (AST) and Part B (semantic) in parallel. Dispatch all semantic subagents AND start AST extraction in the same message. Both can run simultaneously since they operate on different file types. Merge results in Part C as before.**
-
-Note: Parallelizing AST + semantic saves 5-15s on large corpora. AST is deterministic and fast; start it while subagents are processing docs/papers.
-
-#### Part A - Structural extraction for code files
-
-For any code files detected, run AST extraction in parallel with Part B subagents:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import sys, json
-from graphify.extract import collect_files, extract
-from pathlib import Path
-import json
-
-code_files = []
-detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
-for f in detect.get('files', {}).get('code', []):
-    code_files.extend(collect_files(Path(f)) if Path(f).is_dir() else [Path(f)])
-
-if code_files:
-    result = extract(code_files, cache_root=Path('INPUT_PATH'))
-    Path('graphify-out/.graphify_ast.json').write_text(json.dumps(result, indent=2, ensure_ascii=False), encoding=\"utf-8\")
-    print(f'AST: {len(result[\"nodes\"])} nodes, {len(result[\"edges\"])} edges')
-else:
-    Path('graphify-out/.graphify_ast.json').write_text(json.dumps({'nodes':[],'edges':[],'input_tokens':0,'output_tokens':0}, ensure_ascii=False), encoding=\"utf-8\")
-    print('No code files - skipping AST extraction')
-"
-```
-
-#### Part B - Semantic extraction (parallel subagents)
-
-**Fast path:** If detection found zero docs, papers, and images (code-only corpus), skip Part B entirely and go straight to Part C. AST handles code - there is nothing for semantic subagents to do. **First write an empty semantic file** so Part C's merge has its input (it reads `.graphify_semantic.json` unconditionally; without this a code-only run hits `FileNotFoundError`):
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-Path('graphify-out/.graphify_semantic.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
-"
-```
-
-**MANDATORY: You MUST use the Agent tool here. Reading files yourself one-by-one is forbidden - it is 5-10x slower. If you do not use the Agent tool you are doing this wrong.**
-
-Before dispatching subagents, print a timing estimate:
-- Load `total_words` and file counts from `graphify-out/.graphify_detect.json`
-- Estimate agents needed: `ceil(uncached_non_code_files / 22)` (chunk size is 20-25)
-- Estimate time: ~45s per agent batch (they run in parallel, so total ≈ 45s × ceil(agents/parallel_limit))
-- Print: "Semantic extraction: ~N files → X agents, estimated ~Ys"
-
-**Step B0 - Check extraction cache first**
-
-Before dispatching any subagents, check which files already have cached extraction results:
-
-SPEC_PATH below is the **absolute** path of the `references/extraction-spec.md` that ships beside this SKILL.md — the same file Step B2 loads and hands to every subagent. It is the extraction prompt, so cache entries are attributed to it: when a graphify upgrade changes the prompt, entries produced by the old one are re-extracted instead of replayed, and unchanged prompts keep their entries (#1939). Substitute the real path in both Step B0 and Step B3 — pass the same one to each, and do not drop the argument.
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from graphify.cache import check_semantic_cache
-from pathlib import Path
-
-detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
-# Only content files go to semantic extraction. Code is already covered structurally
-# by the AST pass (Part A); flattening every category here makes subagents re-read
-# every source file (#1392). Video is transcribed to a document in Step 2.5 first.
-all_files = [f for cat in ('document', 'paper', 'image') for f in detect['files'].get(cat, [])]
-
-cached_nodes, cached_edges, cached_hyperedges, uncached = check_semantic_cache(all_files, root='INPUT_PATH', prompt_file='SPEC_PATH')
-
-# Always (re)write the cache file: write hits, else DELETE any leftover from a prior
-# run so Part C never merges a stale .graphify_cached.json (#1392).
-if cached_nodes or cached_edges or cached_hyperedges:
-    Path('graphify-out/.graphify_cached.json').write_text(json.dumps({'nodes': cached_nodes, 'edges': cached_edges, 'hyperedges': cached_hyperedges}, ensure_ascii=False), encoding=\"utf-8\")
-else:
-    Path('graphify-out/.graphify_cached.json').unlink(missing_ok=True)
-Path('graphify-out/.graphify_uncached.txt').write_text('\n'.join(uncached), encoding=\"utf-8\")
-print(f'Cache: {len(all_files)-len(uncached)} files hit, {len(uncached)} files need extraction')
-"
-```
-
-Only dispatch subagents for files listed in `graphify-out/.graphify_uncached.txt`. If all files are cached, skip to Part C directly.
-
-**Step B1 - Split into chunks**
-
-Load files from `graphify-out/.graphify_uncached.txt`. Split into chunks of 20-25 files each. Each image gets its own chunk (vision needs separate context). When splitting, group files from the same directory together so related artifacts land in the same chunk and cross-file relationships are more likely to be extracted.
-
-**Step B2 - Dispatch ALL subagents in a single message**
-
-Call the Agent tool multiple times IN THE SAME RESPONSE - one call per chunk. This is the only way they run in parallel. If you make one Agent call, wait, then make another, you are doing it sequentially and defeating the purpose.
-
-**IMPORTANT - subagent type:** Always use `subagent_type="general-purpose"`. Do NOT use `Explore` - it is read-only and cannot write chunk files to disk, which silently drops extraction results. General-purpose has Write and Bash access which the subagent needs.
-
-Concrete example for 3 chunks:
-```
-[Agent tool call 1: files 1-15, subagent_type="general-purpose"]
-[Agent tool call 2: files 16-30, subagent_type="general-purpose"]
-[Agent tool call 3: files 31-45, subagent_type="general-purpose"]
-```
-All three in one message. Not three separate messages.
-
-Each subagent receives this exact prompt (substitute FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE, and CHUNK_PATH).
-
-CHUNK_PATH must be an **absolute** path — derive it before dispatching:
-```bash
-PROJECT_ROOT=$(pwd)  # cwd — where Part C globs graphify-out/ (NOT .graphify_root/scan dir, #1392)
-# Then for chunk N: CHUNK_PATH="${PROJECT_ROOT}/graphify-out/.graphify_chunk_0N.json"
-```
-
-Subagent prompt template:
-
-See `references/extraction-spec.md` for the exact subagent prompt (JSON schema, node-ID rules, confidence rubric, frontmatter, hyperedge, and vision rules). Load it only here, only when at least one chunk holds a doc, paper, or image; a pure-code corpus has skipped Part B and never reads it. Pass each subagent that prompt verbatim with FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, DEEP_MODE, and CHUNK_PATH substituted, and have it write the result to CHUNK_PATH.
-
-**Step B3 - Collect, cache, and merge**
-
-Wait for all subagents. For each result:
-- Check that `graphify-out/.graphify_chunk_NN.json` exists on disk — this is the success signal
-- If the file exists and contains valid JSON with `nodes` and `edges`, include it and save to cache
-- If the file is missing, the subagent was likely dispatched as read-only (Explore type) — print a warning: "chunk N missing from disk — subagent may have been read-only. Re-run with general-purpose agent." Do not silently skip.
-- If a subagent failed or returned invalid JSON, print a warning and skip that chunk - do not abort
-
-If more than half the chunks failed or are missing, stop and tell the user to re-run and ensure `subagent_type="general-purpose"` is used.
-
-Merge all chunk files into `.graphify_semantic_new.json`. **After each Agent call completes, read the real token counts from the Agent tool result's `usage` field and write them back into the chunk JSON before merging** — the chunk JSON itself always has placeholder zeros. Then run:
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json, glob
-from pathlib import Path
-
-chunks = sorted(glob.glob('graphify-out/.graphify_chunk_*.json'))
-all_nodes, all_edges, all_hyperedges = [], [], []
-total_in, total_out = 0, 0
-for c in chunks:
-    d = json.loads(Path(c).read_text(encoding=\"utf-8\"))
-    all_nodes += d.get('nodes', [])
-    all_edges += d.get('edges', [])
-    all_hyperedges += d.get('hyperedges', [])
-    total_in += d.get('input_tokens', 0)
-    total_out += d.get('output_tokens', 0)
-Path('graphify-out/.graphify_semantic_new.json').write_text(json.dumps({
-    'nodes': all_nodes, 'edges': all_edges, 'hyperedges': all_hyperedges,
-    'input_tokens': total_in, 'output_tokens': total_out,
-}, indent=2, ensure_ascii=False), encoding=\"utf-8\")
-print(f'Merged {len(chunks)} chunks: {total_in:,} in / {total_out:,} out tokens')
-"
-```
-
-Save new results to cache. Pass the same SPEC_PATH as Step B0 — it stamps each entry with the prompt that produced it, and a write under a different prompt than the read lands where the next run won't look (#1939):
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from graphify.cache import save_semantic_cache
-from pathlib import Path
-
-new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
-uncached = [line for line in Path('graphify-out/.graphify_uncached.txt').read_text(encoding=\"utf-8\").splitlines() if line]
-saved = save_semantic_cache(new.get('nodes', []), new.get('edges', []), new.get('hyperedges', []), root='INPUT_PATH', allowed_source_files=uncached, prompt_file='SPEC_PATH')
-print(f'Cached {saved} files')
-"
-```
-
-Merge cached + new results into `graphify-out/.graphify_semantic.json`:
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-
-cached = json.loads(Path('graphify-out/.graphify_cached.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_cached.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
-new = json.loads(Path('graphify-out/.graphify_semantic_new.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_semantic_new.json').exists() else {'nodes':[],'edges':[],'hyperedges':[]}
-
-all_nodes = cached['nodes'] + new.get('nodes', [])
-all_edges = cached['edges'] + new.get('edges', [])
-all_hyperedges = cached.get('hyperedges', []) + new.get('hyperedges', [])
-seen = set()
-deduped = []
-for n in all_nodes:
-    if n['id'] not in seen:
-        seen.add(n['id'])
-        deduped.append(n)
-
-merged = {
-    'nodes': deduped,
-    'edges': all_edges,
-    'hyperedges': all_hyperedges,
-    'input_tokens': new.get('input_tokens', 0),
-    'output_tokens': new.get('output_tokens', 0),
-}
-Path('graphify-out/.graphify_semantic.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\")
-print(f'Extraction complete - {len(deduped)} nodes, {len(all_edges)} edges ({len(cached[\"nodes\"])} from cache, {len(new.get(\"nodes\",[]))} new)')
-"
-```
-Clean up temp files: `rm -f graphify-out/.graphify_cached.json graphify-out/.graphify_uncached.txt graphify-out/.graphify_semantic_new.json`
-
-#### Part C - Merge AST + semantic into final extraction
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import sys, json
-from pathlib import Path
-
-ast = json.loads(Path('graphify-out/.graphify_ast.json').read_text(encoding=\"utf-8\"))
-sem = json.loads(Path('graphify-out/.graphify_semantic.json').read_text(encoding=\"utf-8\"))
-
-# Merge: AST nodes first, semantic nodes deduplicated by id
-seen = {n['id'] for n in ast['nodes']}
-merged_nodes = list(ast['nodes'])
-for n in sem['nodes']:
-    if n['id'] not in seen:
-        merged_nodes.append(n)
-        seen.add(n['id'])
-
-merged_edges = ast['edges'] + sem['edges']
-merged_hyperedges = sem.get('hyperedges', [])
-merged = {
-    'nodes': merged_nodes,
-    'edges': merged_edges,
-    'hyperedges': merged_hyperedges,
-    'input_tokens': sem.get('input_tokens', 0),
-    'output_tokens': sem.get('output_tokens', 0),
-}
-Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged, indent=2, ensure_ascii=False), encoding=\"utf-8\")
-total = len(merged_nodes)
-edges = len(merged_edges)
-print(f'Merged: {total} nodes, {edges} edges ({len(ast[\"nodes\"])} AST + {len(sem[\"nodes\"])} semantic)')
-"
-```
-
-### Step 4 - Build graph, cluster, analyze, generate outputs
-
-**Before starting:** the code blocks below pass `directed=IS_DIRECTED` to `build_from_json()`. Replace `IS_DIRECTED` with `True` if `--directed` was given (builds a `DiGraph` preserving edge direction source→target), otherwise `False` (the default undirected `Graph`). Substitute it the same way you substitute `INPUT_PATH` — do not leave the literal `IS_DIRECTED` in the code.
-
-```bash
-mkdir -p graphify-out
-$(cat graphify-out/.graphify_python) -c "
-import sys, json
-from graphify.build import build_from_json
-from graphify.cluster import cluster, score_all
-from graphify.analyze import god_nodes, surprising_connections, suggest_questions
-from graphify.report import generate
-from graphify.export import to_json
-from pathlib import Path
-
-extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
-detection  = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
-
-# root= mirrors the --update runbook (#1361): relativize source_file to the same
-# base so the full build and incremental --update never drift apart on re-extract.
-G = build_from_json(extraction, root='INPUT_PATH', directed=IS_DIRECTED)
-# Guard BEFORE any write: an empty extraction must not clobber a good graph.json /
-# GRAPH_REPORT.md / analysis sidecar. Check immediately after build (#1392).
-if G.number_of_nodes() == 0:
-    print('ERROR: Graph is empty - extraction produced no nodes.')
-    print('Possible causes: all files were skipped, binary-only corpus, or extraction failed.')
-    raise SystemExit(1)
-communities = cluster(G)
-cohesion = score_all(G, communities)
-tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
-gods = god_nodes(G)
-surprises = surprising_connections(G, communities)
-labels = {cid: 'Community ' + str(cid) for cid in communities}
-# Placeholder questions - regenerated with real labels in Step 5
-questions = suggest_questions(G, communities, labels)
-
-# Export FIRST and honor the #479 shrink-guard: to_json returns False (writing
-# nothing) when the new graph is smaller than the existing graph.json. Only write
-# GRAPH_REPORT.md + the analysis sidecar when the graph was actually written, so
-# they never describe a graph that graph.json doesn't contain (#1392).
-wrote = to_json(G, communities, 'graphify-out/graph.json')
-if not wrote:
-    print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
-    print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
-    raise SystemExit(1)
-report = generate(G, communities, cohesion, labels, gods, surprises, detection, tokens, 'INPUT_PATH', suggested_questions=questions)
-Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\")
-analysis = {
-    'communities': {str(k): v for k, v in communities.items()},
-    'cohesion': {str(k): v for k, v in cohesion.items()},
-    'gods': gods,
-    'surprises': surprises,
-    'questions': questions,
-}
-Path('graphify-out/.graphify_analysis.json').write_text(json.dumps(analysis, indent=2, ensure_ascii=False), encoding=\"utf-8\")
-print(f'Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges, {len(communities)} communities')
-"
-```
-
-If this step prints `ERROR: Graph is empty`, stop and tell the user what happened - do not proceed to labeling or visualization.
-
-Replace INPUT_PATH with the actual path.
-
-### Step 4.5 - Graph health check (read-only integrity gate)
-
-A non-destructive diagnostic on the extraction, before labeling. It surfaces edge collapse, dangling/missing endpoints, and self-loops — the silent-corruption modes of incremental updates and AST/LLM id mismatches. Read-only; never aborts.
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-from graphify.diagnostics import diagnose_extraction, format_diagnostic_report
-
-extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
-summary = diagnose_extraction(extraction, directed=IS_DIRECTED, root='INPUT_PATH')
-print(format_diagnostic_report(summary))
-flags = [f'{summary[k]} {label}' for k, label in (
-    ('dangling_endpoint_edges', 'dangling-endpoint edges'),
-    ('missing_endpoint_edges', 'missing-endpoint edges'),
-    ('self_loop_edges', 'self-loop edges'),
-    ('directed_same_endpoint_collapsed_edges', 'collapsed (directed) edges'),
-    ('undirected_same_endpoint_collapsed_edges', 'collapsed (undirected) edges'),
-) if summary.get(k, 0)]
-print('GRAPH HEALTH WARNING: ' + '; '.join(flags) + ' - graph may be incomplete/corrupt.' if flags else 'Graph health: OK (no dangling/missing/collapsed edges).')
-"
-```
-
-Substitute `IS_DIRECTED` and `INPUT_PATH` as in Step 4. If a `GRAPH HEALTH WARNING` prints, surface it in the final summary (do not abort — the graph is still usable, but the integrity issue must be visible, per the Honesty Rules).
-
-### Step 5 - Label communities
-
-Read `graphify-out/.graphify_analysis.json`. For each community key, look at its node labels and write a 2-5 word plain-language name (e.g. "Attention Mechanism", "Training Pipeline", "Data Loading").
-
-Then regenerate the report and save the labels for the visualizer:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import sys, json
-from graphify.build import build_from_json
-from graphify.cluster import score_all
-from graphify.analyze import god_nodes, surprising_connections, suggest_questions
-from graphify.report import generate
-from graphify.export import to_json
-from pathlib import Path
-
-extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
-detection  = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
-analysis   = json.loads(Path('graphify-out/.graphify_analysis.json').read_text(encoding=\"utf-8\"))
-
-# root= as in Step 4 / the --update runbook (#1361) — same base for node-key parity.
-G = build_from_json(extraction, root='INPUT_PATH', directed=IS_DIRECTED)
-communities = {int(k): v for k, v in analysis['communities'].items()}
-cohesion = {int(k): v for k, v in analysis['cohesion'].items()}
-tokens = {'input': extraction.get('input_tokens', 0), 'output': extraction.get('output_tokens', 0)}
-
-# LABELS - replace these with the names you chose above
-labels = LABELS_DICT
-
-# Regenerate questions with real community labels (labels affect question phrasing)
-questions = suggest_questions(G, communities, labels)
-
-report = generate(G, communities, cohesion, labels, analysis['gods'], analysis['surprises'], detection, tokens, 'INPUT_PATH', suggested_questions=questions)
-Path('graphify-out/GRAPH_REPORT.md').write_text(report, encoding=\"utf-8\")
-Path('graphify-out/.graphify_labels.json').write_text(json.dumps({str(k): v for k, v in labels.items()}, ensure_ascii=False), encoding=\"utf-8\")
-# Re-export so graph.json nodes carry the curated community_name (#2490).
-# Same extraction as Step 4, so the #479 shrink-guard passes on node count;
-# if it still refuses, surface the guard message - do not force past it.
-wrote = to_json(G, communities, 'graphify-out/graph.json', community_labels=labels)
-if not wrote:
-    print('ERROR: refused to shrink graphify-out/graph.json (existing graph has more nodes; #479).')
-    print('If this shrink is intentional (you deleted files), re-run a full build with --force.')
-print('Report updated with community labels')
-"
-```
-
-Replace `LABELS_DICT` with the actual dict you constructed (e.g. `{0: "Attention Mechanism", 1: "Training Pipeline"}`).
-Replace INPUT_PATH with the actual path.
-
-### Step 6 - Generate Obsidian vault (opt-in) + HTML
-
-**Generate HTML always** (unless `--no-viz`). **Obsidian vault only if `--obsidian` was explicitly given** — skip it otherwise, it generates one file per node.
-
-If `--obsidian` was given:
-
-- If `--obsidian-dir <path>` was also given, pass it via `--dir`. Otherwise defaults to `graphify-out/obsidian`.
-
-```bash
-graphify export obsidian
-# or with custom dir: graphify export obsidian --dir ~/vaults/my-project
-```
-
-Generate the HTML graph (always, unless `--no-viz`):
-
-```bash
-graphify export html  # auto-aggregates to community view if graph > 5000 nodes
-# or: graphify export html --no-viz
-```
-
-### Steps 6b-8 - Wiki, Neo4j, FalkorDB, SVG, GraphML, MCP, benchmark (only on their flags)
-
-These run only when their flag is present (`--wiki`, `--neo4j`/`--neo4j-push`, `--falkordb`/`--falkordb-push`, `--svg`, `--graphml`, `--mcp`) or, for the token-reduction benchmark, when `total_words` exceeds 5,000. A default run with no export flags skips all of them. See `references/exports.md` for each one. Run any `--wiki` export before Step 9 cleanup so `.graphify_labels.json` is still available.
-
----
-
-### Step 9 - Save manifest, update cost tracker, clean up, and report
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-from datetime import datetime, timezone
-from graphify.detect import save_manifest
-
-# Save manifest for --update
-detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
-extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
-# In --update mode, 'all_files' carries the full corpus; 'files' is the changed
-# subset. Full-rebuild mode populates only 'files', so the fallback handles that.
-# root= relativizes the manifest keys to the scan root (same base as the build),
-# so the on-disk manifest is portable across clones/machines and a later --update
-# matches cached files instead of missing every one (#1417).
-#
-# Only stamp semantic files (docs/papers/images) that ACTUALLY produced output:
-# a detected file whose chunk failed or was omitted must stay unstamped so the
-# next --update re-queues it, otherwise it is marked done and its content is lost
-# forever (#2015). This mirrors the library extract path exactly
-# (cli._stamped_manifest_files + clear_semantic + scan_corpus); do not stamp the
-# raw corpus. Code files are always stamped (AST is deterministic); only semantic
-# types are gated on output.
-from graphify.cli import _stamped_manifest_files
-_corpus = detect.get('all_files') or detect['files']
-_manifest_files = _stamped_manifest_files(_corpus, extract, Path('INPUT_PATH'))
-# Files dispatched this run (the changed subset) but NOT stamped above still carry
-# a stale semantic_hash from a prior run; clear it so detect_incremental re-queues
-# them instead of reading them as unchanged (#1948).
-_sem_types = ('document', 'paper', 'image')
-_dispatched = {f for t, fl in detect['files'].items() if t in _sem_types for f in fl}
-_stamped = {f for fl in _manifest_files.values() for f in fl}
-_cleared = _dispatched - _stamped
-# scan_corpus = the RAW full corpus (not the stamp-filtered subset) so in-root
-# files newly excluded since last run are dropped rather than masquerading as
-# deletions; untouched files' prior rows are still preserved (#1908).
-_scan = {f for fl in _corpus.values() for f in fl}
-save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
-
-# Update cumulative cost tracker
-input_tok = extract.get('input_tokens', 0)
-output_tok = extract.get('output_tokens', 0)
-
-cost_path = Path('graphify-out/cost.json')
-if cost_path.exists():
-    cost = json.loads(cost_path.read_text(encoding=\"utf-8\"))
-else:
-    cost = {'runs': [], 'total_input_tokens': 0, 'total_output_tokens': 0}
-
-cost['runs'].append({
-    'date': datetime.now(timezone.utc).isoformat(),
-    'input_tokens': input_tok,
-    'output_tokens': output_tok,
-    'files': detect.get('total_files', 0),
-})
-cost['total_input_tokens'] += input_tok
-cost['total_output_tokens'] += output_tok
-cost_path.write_text(json.dumps(cost, indent=2, ensure_ascii=False), encoding=\"utf-8\")
-
-print(f'This run: {input_tok:,} input tokens, {output_tok:,} output tokens')
-print(f'All time: {cost[\"total_input_tokens\"]:,} input, {cost[\"total_output_tokens\"]:,} output ({len(cost[\"runs\"])} runs)')
-"
-rm -f graphify-out/.graphify_detect.json graphify-out/.graphify_extract.json graphify-out/.graphify_ast.json graphify-out/.graphify_semantic.json graphify-out/.graphify_analysis.json
-find graphify-out -maxdepth 1 -name '.graphify_chunk_*.json' -delete 2>/dev/null
-rm -f graphify-out/.needs_update 2>/dev/null || true
-```
-
-Replace INPUT_PATH with the actual path (same value used in Steps 4-5) so the manifest is relativized to the scan root.
-
-Tell the user (omit the obsidian line unless --obsidian was given):
-```
-Graph complete. Outputs in PATH_TO_DIR/graphify-out/
-
-  graph.html            - interactive graph, open in browser
-  GRAPH_REPORT.md       - audit report
-  graph.json            - raw graph data
-  obsidian/             - Obsidian vault (only if --obsidian was given)
-```
-
-If graphify saved you time, consider supporting it: https://github.com/sponsors/safishamsi
-
-Replace PATH_TO_DIR with the actual absolute path of the directory that was processed.
-
-Then paste these sections from GRAPH_REPORT.md directly into the chat:
-- God Nodes
-- Surprising Connections
-- Suggested Questions
-
-Do NOT paste the full report - just those three sections. Keep it concise.
-
-Then immediately offer to explore. Pick the single most interesting suggested question from the report - the one that crosses the most community boundaries or has the most surprising bridge node - and ask:
-
-> "The most interesting question this graph can answer: **[question]**. Want me to trace it?"
-
-If the user says yes, run `/graphify query "[question]"` on the graph and walk them through the answer using the graph structure - which nodes connect, which community boundaries get crossed, what the path reveals. Keep going as long as they want to explore. Each answer should end with a natural follow-up ("this connects to X - want to go deeper?") so the session feels like navigation, not a one-shot report.
-
-The graph is the map. Your job after the pipeline is to be the guide.
-
----
-
-## Interpreter guard for subcommands
-
-Before running any subcommand below (`--update`, `--cluster-only`, `query`, `path`, `explain`, `add`), check that `.graphify_python` exists. If it's missing (e.g. user deleted `graphify-out/`), re-resolve the interpreter first:
-
-```bash
-if [ ! -f graphify-out/.graphify_python ]; then
-    GRAPHIFY_BIN=$(which graphify 2>/dev/null)
-    if [ -n "$GRAPHIFY_BIN" ]; then
-        PYTHON=$(head -1 "$GRAPHIFY_BIN" | tr -d '#!')
-        case "$PYTHON" in *[!a-zA-Z0-9/_.@-]*) PYTHON="python3" ;; esac
-    else
-        PYTHON="python3"
-    fi
-    mkdir -p graphify-out
-    "$PYTHON" -c "import sys; open('graphify-out/.graphify_python', 'w', encoding='utf-8').write(sys.executable)"
-fi
-```
-
-## For --update and --cluster-only
-
-Both are non-default subcommands. `--update` re-extracts only new or changed files; `--cluster-only` reruns clustering on the existing graph. See `references/update.md` for both flows.
-
----
-
-## For /graphify query
-
-When `graphify-out/graph.json` already exists and the user asks a question about the corpus, answer from the graph rather than rebuilding it:
-
-```bash
-graphify query "<question>"
-```
-
-Before traversal, expand the question against the graph's own vocabulary so a wording mismatch does not collapse the answer to noise. If the `graphify query` CLI is unavailable, fall back to an inline NetworkX traversal of `graphify-out/graph.json`. Answer using only what the graph output contains, and quote `source_location` when citing a specific fact. For that vocab-expansion step, the BFS/DFS traversal modes, the `--budget` cap, the NetworkX fallback, `save-result` feedback, and the `/graphify path` and `/graphify explain` flows, see `references/query.md`.
-
----
-
-## For /graphify add and --watch
-
-Neither is part of the default build. When the user runs `/graphify add <url>` to fetch a URL into the corpus, or passes `--watch` to auto-rebuild on file changes, see `references/add-watch.md`.
-
----
-
-## For the commit hook and native CLAUDE.md integration
-
-When the user asks to install the post-commit auto-rebuild hook or wire graphify into a project's CLAUDE.md, see `references/hooks.md`.
-
----
-
-## Honesty Rules
-
-- Never invent an edge. If unsure, use AMBIGUOUS.
-- Never skip the corpus check warning.
-- Always show token cost in the report.
-- Never hide cohesion scores behind symbols - show the raw number.
-- Never run HTML viz on a graph with more than 5,000 nodes without warning the user.

+ 0 - 56
service/cmd/mcp/.kiro/skills/graphify/references/add-watch.md

@@ -1,56 +0,0 @@
-# graphify reference: add a URL and watch a folder
-
-Load this when the user ran `/graphify add <url>` or passed `--watch`. Neither is part of the default build.
-
-## For /graphify add
-
-Fetch a URL and add it to the corpus, then update the graph.
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import sys
-from graphify.ingest import ingest
-from pathlib import Path
-
-try:
-    out = ingest('URL', Path('./raw'), author='AUTHOR', contributor='CONTRIBUTOR')
-    print(f'Saved to {out}')
-except ValueError as e:
-    print(f'error: {e}', file=sys.stderr)
-    sys.exit(1)
-except RuntimeError as e:
-    print(f'error: {e}', file=sys.stderr)
-    sys.exit(1)
-"
-```
-
-Replace `URL` with the actual URL, `AUTHOR` with the user's name if provided, `CONTRIBUTOR` likewise. If the command exits with an error, tell the user what went wrong - do not silently continue. After a successful save, automatically run the `--update` pipeline on `./raw` to merge the new file into the existing graph.
-
-Supported URL types (auto-detected):
-- YouTube / any video URL → audio downloaded via yt-dlp, transcribed to `.txt` on next run (requires `pip install 'graphifyy[video]'`)
-- Twitter/X → fetched via oEmbed, saved as `.md` with tweet text and author
-- arXiv → abstract + metadata saved as `.md`
-- PDF → downloaded as `.pdf`
-- Images (.png/.jpg/.webp) → downloaded, Claude vision extracts on next run
-- Any webpage → converted to markdown via html2text
-
----
-
-## For --watch
-
-Start a background watcher that monitors a folder and auto-updates the graph when files change.
-
-```bash
-$(cat graphify-out/.graphify_python) -m graphify.watch INPUT_PATH --debounce 3
-```
-
-Replace INPUT_PATH with the folder to watch. Behavior depends on what changed:
-
-- **Code files only (.py, .ts, .go, etc.):** re-runs AST extraction + rebuild + cluster immediately, no LLM needed. `graph.json` and `GRAPH_REPORT.md` are updated automatically.
-- **Docs, papers, or images:** writes a `graphify-out/needs_update` flag and prints a notification to run `/graphify --update` (LLM semantic re-extraction required).
-
-Debounce (default 3s): waits until file activity stops before triggering, so a wave of parallel agent writes doesn't trigger a rebuild per file.
-
-Press Ctrl+C to stop.
-
-For agentic workflows: run `--watch` in a background terminal. Code changes from agent waves are picked up automatically between waves. If agents are also writing docs or notes, you'll need a manual `/graphify --update` after those waves.

+ 0 - 87
service/cmd/mcp/.kiro/skills/graphify/references/exports.md

@@ -1,87 +0,0 @@
-# graphify reference: extra exports and benchmark
-
-Load this when the user passed one of the export flags (`--wiki`, `--neo4j`, `--neo4j-push`, `--falkordb`, `--falkordb-push`, `--svg`, `--graphml`, `--mcp`), or when the corpus is large enough for the token-reduction benchmark. Each step runs only for its own flag.
-
-### Step 6b - Wiki (only if --wiki flag)
-
-**Only run this step if `--wiki` was explicitly given in the original command.**
-
-Run this before Step 9 (cleanup) so `.graphify_labels.json` is still available.
-
-```bash
-graphify export wiki
-```
-
-### Step 7 - Neo4j export (only if --neo4j or --neo4j-push flag)
-
-**If `--neo4j`** - generate a Cypher file for manual import:
-
-```bash
-graphify export neo4j
-```
-
-**If `--neo4j-push <uri>`** - push directly to a running Neo4j instance. Ask the user for credentials if not provided:
-
-```bash
-graphify export neo4j --push bolt://localhost:7687 --user neo4j --password PASSWORD
-```
-
-Default URI is `bolt://localhost:7687`, default user is `neo4j`. Uses MERGE - safe to re-run without creating duplicates.
-
-### Step 7a - FalkorDB export (only if --falkordb or --falkordb-push flag)
-
-**If `--falkordb`** - generate a Cypher file. The statements are OpenCypher, but FalkorDB's `GRAPH.QUERY` runs one statement at a time (no bulk script import like Neo4j's `cypher-shell`), so prefer `--falkordb-push` to load a graph. Use this only when you want the portable `cypher.txt` artifact:
-
-```bash
-graphify export falkordb
-```
-
-**If `--falkordb-push <uri>`** - push directly to a running FalkorDB instance. Credentials are optional; ask the user only if the instance requires auth:
-
-```bash
-graphify export falkordb --push falkordb://localhost:6379
-```
-
-Default URI is `falkordb://localhost:6379` (the scheme is informational - `redis://` or a bare `host:port` work too), auth is optional, and the target graph defaults to `graphify`. Uses MERGE - safe to re-run without creating duplicates.
-
-### Step 7b - SVG export (only if --svg flag)
-
-```bash
-graphify export svg
-```
-
-### Step 7c - GraphML export (only if --graphml flag)
-
-```bash
-graphify export graphml
-```
-
-### Step 7d - MCP server (only if --mcp flag)
-
-```bash
-$(cat graphify-out/.graphify_python) -m graphify.serve graphify-out/graph.json
-```
-
-This starts a stdio MCP server that exposes tools: `query_graph`, `get_node`, `get_neighbors`, `get_community`, `god_nodes`, `graph_stats`, `shortest_path`. Add to Claude Desktop or any MCP-compatible agent orchestrator so other agents can query the graph live.
-
-To configure in Claude Desktop, add to `claude_desktop_config.json`. Claude Desktop can't run `$(...)`, and under `uv tool install` the system `python3` can't import graphify — so set `command` to the **absolute interpreter path** printed by `cat graphify-out/.graphify_python`:
-```json
-{
-  "mcpServers": {
-    "graphify": {
-      "command": "<absolute path from: cat graphify-out/.graphify_python>",
-      "args": ["-m", "graphify.serve", "/absolute/path/to/graphify-out/graph.json"]
-    }
-  }
-}
-```
-
-### Step 8 - Token reduction benchmark (only if total_words > 5000)
-
-If `total_words` from `graphify-out/.graphify_detect.json` is greater than 5,000, run:
-
-```bash
-graphify benchmark
-```
-
-Print the output directly in chat. If `total_words <= 5000`, skip silently - the graph value is structural clarity, not token compression, for small corpora.

+ 0 - 31
service/cmd/mcp/.kiro/skills/graphify/references/extraction-spec.md

@@ -1,31 +0,0 @@
-# graphify reference: extraction subagent prompt (compact)
-
-Load this in Step 3 Part B when the corpus has at least one doc, paper, or image chunk. A pure-code corpus skips Part B and never reads this file. Each semantic subagent receives the prompt below verbatim (substitute FILE_LIST, CHUNK_NUM, TOTAL_CHUNKS, and DEEP_MODE).
-
-```
-You are a graphify extraction subagent. Read the files listed and extract a knowledge graph fragment.
-Output ONLY valid JSON matching the schema below - no explanation, no markdown fences, no preamble.
-
-Files (chunk CHUNK_NUM of TOTAL_CHUNKS):
-FILE_LIST
-
-Rules:
-- EXTRACTED: relationship explicit in source (import, call, citation)
-- INFERRED: reasonable inference (shared structure, implied dependency)
-- AMBIGUOUS: uncertain — flag it, do not omit
-- Code files: semantic edges AST cannot find. Do not re-extract imports. When adding `calls` edges: source is the caller, target is the callee, never reversed; keep `calls` within one language.
-- Doc/paper files: named concepts, entities, citations. Store rationale (WHY decisions were made) as a `rationale` attribute on the relevant node, not as a separate node. Use `file_type:"rationale"` for concept-like nodes (ideas, principles, mechanisms) and `file_type:"concept"` for named concepts. `file_type` MUST be one of exactly these six values: `code`, `document`, `paper`, `image`, `rationale`, `concept`. Any other value is invalid and will be rejected.
-- Image files: use vision — understand what the image IS, not just OCR
-- DEEP_MODE (if --mode deep): be aggressive with INFERRED edges — indirect deps, shared assumptions, latent couplings. Mark uncertain ones AMBIGUOUS instead of omitting.
-- Semantic similarity: if two concepts solve the same problem or represent the same idea without a structural link (no import, call, or citation), add a `semantically_similar_to` edge marked INFERRED with confidence_score 0.6-0.95. Non-obvious cross-file links only.
-- Hyperedges: if 3+ nodes share a concept, flow, or pattern not captured by pairwise edges, add a hyperedge to a top-level `hyperedges` array. Use sparingly. Max 3 per chunk.
-- If a file has YAML frontmatter (--- ... ---), copy source_url, captured_at, author, contributor onto every node from that file.
-- confidence_score is REQUIRED on every edge — never omit it, never use 0.5 as a default. EXTRACTED = 1.0 always. INFERRED: pick exactly ONE of 0.95 (direct structural evidence), 0.85 (strong inference), 0.75 (reasonable inference), 0.65 (weak inference), 0.55 (speculative but plausible) — never 0.5; if none fit, mark the edge AMBIGUOUS. AMBIGUOUS = 0.1-0.3.
-
-Node ID format: lowercase, only `[a-z0-9_]`, no dots or slashes. Format `{stem}_{entity}` where stem is the full repo-relative path with the extension dropped, every segment joined with `_` (each lowercased with non-alphanumeric chars replaced by `_`) and entity is the symbol name similarly normalized. Use every directory level, not just the immediate parent. `src/auth/session.py` + `ValidateToken` → `src_auth_session_validatetoken`. Top-level files use just the filename stem. This must match the AST extractor's ID. Never append chunk or sequence suffixes — IDs must be deterministic from the label alone.
-
-Output exactly this JSON (no other text):
-{"nodes":[{"id":"auth_session_validatetoken","label":"Human Readable Name","file_type":"code|document|paper|image|rationale|concept","source_file":"<FILE_LIST path verbatim>","source_location":null,"source_url":null,"captured_at":null,"author":null,"contributor":null}],"edges":[{"source":"node_id","target":"node_id","relation":"calls|implements|references|cites|conceptually_related_to|shares_data_with|semantically_similar_to|rationale_for","confidence":"EXTRACTED|INFERRED|AMBIGUOUS","confidence_score":1.0,"source_file":"<FILE_LIST path verbatim>","source_location":null,"weight":1.0}],"hyperedges":[{"id":"snake_case_id","label":"Human Readable Label","nodes":["node_id1","node_id2","node_id3"],"relation":"participate_in|implement|form","confidence":"EXTRACTED|INFERRED","confidence_score":0.75,"source_file":"<FILE_LIST path verbatim>"}],"input_tokens":0,"output_tokens":0}
-
-source_file RULE: set source_file to the FILE_LIST path for that file VERBATIM (absolute, no shortening to basename, no re-relativizing, no separator change). Keeps full build and --update on one base so build_merge's replace matches instead of duplicating.
-```

+ 0 - 46
service/cmd/mcp/.kiro/skills/graphify/references/github-and-merge.md

@@ -1,46 +0,0 @@
-# graphify reference: GitHub clone and cross-repo merge
-
-Load this when the user passed one or more `https://github.com/...` URLs, or named several local subfolders to merge into one graph.
-
-### Step 0 - Clone GitHub repo(s) (only if a GitHub URL was given)
-
-**Single repo:**
-```bash
-LOCAL_PATH=$(graphify clone <github-url> [--branch <branch>])
-# Use LOCAL_PATH as the target for all subsequent steps
-```
-
-**Multiple repos (cross-repo graph):**
-```bash
-# Clone each repo, run the full pipeline on each, then merge
-graphify clone <url1>   # → ~/.graphify/repos/<owner1>/<repo1>
-graphify clone <url2>   # → ~/.graphify/repos/<owner2>/<repo2>
-# Run /graphify on each local path to produce their graph.json files
-# Then merge:
-graphify merge-graphs \
-  ~/.graphify/repos/<owner1>/<repo1>/graphify-out/graph.json \
-  ~/.graphify/repos/<owner2>/<repo2>/graphify-out/graph.json \
-  --out graphify-out/cross-repo-graph.json
-```
-
-Graphify clones into `~/.graphify/repos/<owner>/<repo>` and reuses existing clones on repeat runs. Each node in the merged graph carries a `repo` attribute so you can filter by origin.
-
-**Multiple local subfolders (monorepo or multi-service layout):**
-
-The skill pipeline writes all intermediate and final outputs to `graphify-out/` in the current working directory. Running the skill on each subfolder separately will clobber the same output dir. Instead, use the CLI directly for each subfolder — it places `graphify-out/` *inside* the scanned path:
-
-```bash
-graphify extract ./core/     # → ./core/graphify-out/graph.json
-graphify extract ./service/  # → ./service/graphify-out/graph.json
-graphify extract ./platform/ # → ./platform/graphify-out/graph.json
-# Add --backend gemini|kimi|openai|deepseek|claude-cli depending on which API key you have set
-
-# Then merge at the project root:
-graphify merge-graphs \
-  ./core/graphify-out/graph.json \
-  ./service/graphify-out/graph.json \
-  ./platform/graphify-out/graph.json \
-  --out graphify-out/graph.json
-```
-
-Once `graphify-out/graph.json` exists, the fast path above takes over: any codebase question runs `graphify query` directly on the merged graph — no re-extraction, no size gate.

+ 0 - 33
service/cmd/mcp/.kiro/skills/graphify/references/hooks.md

@@ -1,33 +0,0 @@
-# graphify reference: commit hook and native CLAUDE.md integration
-
-Load this when the user asked to install the post-commit hook or wire graphify into a project's CLAUDE.md.
-
-## For git commit hook
-
-Install a post-commit hook that auto-rebuilds the graph after every commit. No background process needed - triggers once per commit, works with any editor.
-
-```bash
-graphify hook install    # install
-graphify hook uninstall  # remove
-graphify hook status     # check
-```
-
-After every `git commit`, the hook detects which code files changed (via `git diff HEAD~1`), re-runs AST extraction on those files, and rebuilds `graph.json` and `GRAPH_REPORT.md`. Doc/image changes are ignored by the hook - run `/graphify --update` manually for those.
-
-If a post-commit hook already exists, graphify appends to it rather than replacing it.
-
----
-
-## For native CLAUDE.md integration
-
-Run once per project to make graphify always-on in Claude Code sessions:
-
-```bash
-graphify claude install
-```
-
-This writes a `## graphify` section to the local `CLAUDE.md` that instructs Claude to check the graph before answering codebase questions and rebuild it after code changes. No manual `/graphify` needed in future sessions.
-
-```bash
-graphify claude uninstall  # remove the section
-```

+ 0 - 311
service/cmd/mcp/.kiro/skills/graphify/references/query.md

@@ -1,311 +0,0 @@
-# graphify reference: query, path, explain
-
-Load this when the user asks a question against an existing graph, or runs `/graphify path` or `/graphify explain`. The core's query stub points here for the full traversal flow. These flows use the `graphify query` CLI when it is available and fall back to an inline NetworkX traversal otherwise.
-
-Two traversal modes - choose based on the question:
-
-| Mode | Flag | Best for |
-|------|------|----------|
-| BFS (default) | _(none)_ | "What is X connected to?" - broad context, nearest neighbors first |
-| DFS | `--dfs` | "How does X reach Y?" - trace a specific chain or dependency path |
-
-First check the graph exists:
-```bash
-$(cat graphify-out/.graphify_python) -c "
-from pathlib import Path
-if not Path('graphify-out/graph.json').exists():
-    print('ERROR: No graph found. Run /graphify <path> first to build the graph.')
-    raise SystemExit(1)
-"
-```
-If it fails, stop and tell the user to run `/graphify <path>` first.
-
-### Step 0 — Constrained query expansion (REQUIRED before traversal)
-
-graphify's `query` CLI matches nodes via case-folded substring + IDF — there is **no stemming, no synonyms, no cross-language match** inside the binary, and the inline fallback below matches the same way. If the user's question uses different language or different domain vocabulary than the graph's labels (user says "обработчик" / graph says "handler"; user says "authentication" / graph says "Guardian"), the literal matcher returns 0 hits and the answer collapses to noise.
-
-Fix this **without inventing tokens** by expanding the query against the actual graph vocabulary first:
-
-1. Extract the token vocabulary from node labels:
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json, re
-from pathlib import Path
-data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8'))
-vocab = set()
-for n in data['nodes']:
-    for c in re.findall(r'[^\W\d_]+', n.get('label','') or '', re.UNICODE):
-        parts = re.findall(r'[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+', c) or [c]
-        for p in parts:
-            t = p.lower()
-            if 3 <= len(t) <= 30:
-                vocab.add(t)
-Path('graphify-out/.vocab.txt').write_text('\n'.join(sorted(vocab)), encoding='utf-8')
-print(f'vocab: {len(vocab)} tokens')
-"
-```
-
-2. Read `graphify-out/.vocab.txt`. Then for the user's question, select **up to 12 tokens from this exact list** that semantically match the query intent. Hard constraints:
-   - You MUST pick only tokens present in the vocabulary file. Do NOT invent tokens.
-   - If a query concept has no plausible token in the vocab, skip it — do not substitute a near-synonym from training memory.
-   - If **no** vocab tokens match the query at all, output an empty list and tell the user the corpus has no relevant vocabulary for this question. Do not fabricate a search.
-   - Translate cross-language: Russian "аутентификация" → look for `auth`, `credential`, `token`, `security` IFF present in vocab.
-   - Morphology: "handlers" maps to `handler` IFF present; "todos" maps to `todo` IFF present.
-
-3. Print the selection explicitly to the user before running the query, so the expansion is auditable:
-```
-Query expanded to (from graph vocab, N tokens): [token1, token2, ...]
-```
-If the list is empty, say so plainly and stop — do not proceed to traversal.
-
-### Step 1 — Traversal
-
-Build the **expanded query string** by joining the selected tokens with spaces. Use this string as `QUESTION` below — NOT the original user question. (The original question is preserved only for `save-result` at the end.)
-
-Prefer the CLI when it is installed:
-```bash
-graphify query "QUESTION"
-# or: graphify query "QUESTION" --dfs --budget 3000
-```
-
-If the CLI is unavailable, load `graphify-out/graph.json` and run the traversal inline:
-
-1. Find the 1-3 nodes whose label best matches the expanded tokens.
-2. Run the appropriate traversal from each starting node.
-3. Read the subgraph - node labels, edge relations, confidence tags, source locations.
-4. Answer using **only** what the graph contains. Quote `source_location` when citing a specific fact.
-5. If the graph lacks enough information, say so - do not hallucinate edges.
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import sys, json
-from networkx.readwrite import json_graph
-import networkx as nx
-from pathlib import Path
-
-data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8'))
-G = json_graph.node_link_graph(data, edges='links')
-
-question = 'QUESTION'
-mode = 'MODE'  # 'bfs' or 'dfs'
-terms = [t.lower() for t in question.split() if len(t) >= 3]  # match the vocab threshold; keeps api/jwt/ios (#1392)
-
-# Find best-matching start nodes
-scored = []
-for nid, ndata in G.nodes(data=True):
-    label = ndata.get('label', '').lower()
-    score = sum(1 for t in terms if t in label)
-    if score > 0:
-        scored.append((score, nid))
-scored.sort(reverse=True)
-start_nodes = [nid for _, nid in scored[:3]]
-
-if not start_nodes:
-    print('No matching nodes found for query terms:', terms)
-    sys.exit(0)
-
-subgraph_nodes = set()
-subgraph_edges = []
-
-if mode == 'dfs':
-    # DFS: follow one path as deep as possible before backtracking.
-    # Depth-limited to 6 to avoid traversing the whole graph.
-    visited = set()
-    stack = [(n, 0) for n in reversed(start_nodes)]
-    while stack:
-        node, depth = stack.pop()
-        if node in visited or depth > 6:
-            continue
-        visited.add(node)
-        subgraph_nodes.add(node)
-        for neighbor in G.neighbors(node):
-            if neighbor not in visited:
-                stack.append((neighbor, depth + 1))
-                subgraph_edges.append((node, neighbor))
-else:
-    # BFS: explore all neighbors layer by layer up to depth 3.
-    frontier = set(start_nodes)
-    subgraph_nodes = set(start_nodes)
-    for _ in range(3):
-        next_frontier = set()
-        for n in frontier:
-            for neighbor in G.neighbors(n):
-                if neighbor not in subgraph_nodes:
-                    next_frontier.add(neighbor)
-                    subgraph_edges.append((n, neighbor))
-        subgraph_nodes.update(next_frontier)
-        frontier = next_frontier
-
-# Token-budget aware output: rank by relevance, cut at budget (~4 chars/token)
-token_budget = BUDGET  # default 2000
-char_budget = token_budget * 4
-
-# Score each node by term overlap for ranked output
-def relevance(nid):
-    label = G.nodes[nid].get('label', '').lower()
-    return sum(1 for t in terms if t in label)
-
-ranked_nodes = sorted(subgraph_nodes, key=relevance, reverse=True)
-
-lines = [f'Traversal: {mode.upper()} | Start: {[G.nodes[n].get(\"label\",n) for n in start_nodes]} | {len(subgraph_nodes)} nodes']
-for nid in ranked_nodes:
-    d = G.nodes[nid]
-    lines.append(f'  NODE {d.get(\"label\", nid)} [src={d.get(\"source_file\",\"\")} loc={d.get(\"source_location\",\"\")}]')
-for u, v in subgraph_edges:
-    if u in subgraph_nodes and v in subgraph_nodes:
-        _raw = G[u][v]; d = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw
-        lines.append(f'  EDGE {G.nodes[u].get(\"label\",u)} --{d.get(\"relation\",\"\")} [{d.get(\"confidence\",\"\")}]--> {G.nodes[v].get(\"label\",v)}')
-
-output = '\n'.join(lines)
-if len(output) > char_budget:
-    output = output[:char_budget] + f'\n... (truncated at ~{token_budget} token budget - use --budget N for more)'
-print(output)
-"
-```
-
-Replace `QUESTION` with the **expanded** query string, `MODE` with `bfs` or `dfs`, and `BUDGET` with the token budget (default `2000`, or whatever `--budget N` specifies). Then answer based on the subgraph output above, using only what the graph contains.
-
-After writing the answer, save it back into the graph so it improves future queries. Include the expanded tokens inside the `--answer` text (e.g. `"Expanded from original query via vocab: [tokens]. Then traversed..."`) so the next `--update` extracts the expansion history as a graph node:
-
-```bash
-$(cat graphify-out/.graphify_python) -m graphify save-result --question "ORIGINAL_QUESTION" --answer "ANSWER" --type query --nodes NODE1 NODE2
-```
-
-Replace `ORIGINAL_QUESTION` with the user's verbatim question, `ANSWER` with your full answer text (containing the expanded-token trace), `NODE1 NODE2` with the list of node labels you cited. This closes the feedback loop: the next `--update` will extract this Q&A as a node in the graph.
-
-**Work memory (self-improving loop).** Add an `--outcome` so future sessions learn from this one — append `--outcome useful|dead_end|corrected` to the `save-result` command (and `--correction "the right answer"` when correcting):
-
-- `useful` — the cited nodes answered the question well (they become *preferred sources*).
-- `dead_end` — the question/path led nowhere; don't re-derive it next time.
-- `corrected` — the saved answer was wrong; `--correction` records what was right.
-
-At the **start** of graph work, refresh and read the lessons: run `graphify reflect --if-stale` (cheap, deterministic, no LLM; `--if-stale` makes it a no-op when `LESSONS.md` is already newer than every input, e.g. when the git hook just refreshed it), then read `graphify-out/reflections/LESSONS.md`. It lists **preferred sources** (start there), **known dead ends** (skip them), and prior **corrections**. Running `reflect` yourself keeps the lessons current even without the git hook installed; if the post-commit hook *is* installed, `--if-stale` means your session-start run costs almost nothing.
-
----
-
-## For /graphify path
-
-Find the shortest path between two named concepts in the graph. Prefer the CLI when installed:
-
-```bash
-graphify path "NODE_A" "NODE_B"
-```
-
-If the CLI is unavailable, run it inline:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json, sys
-import networkx as nx
-from networkx.readwrite import json_graph
-from pathlib import Path
-
-data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8'))
-G = json_graph.node_link_graph(data, edges='links')
-
-a_term = 'NODE_A'
-b_term = 'NODE_B'
-
-def find_node(term):
-    term = term.lower()
-    scored = sorted(
-        [(sum(1 for w in term.split() if w in G.nodes[n].get('label','').lower()), n)
-         for n in G.nodes()],
-        reverse=True
-    )
-    return scored[0][1] if scored and scored[0][0] > 0 else None
-
-src = find_node(a_term)
-tgt = find_node(b_term)
-
-if not src or not tgt:
-    print(f'Could not find nodes matching: {a_term!r} or {b_term!r}')
-    sys.exit(0)
-
-try:
-    path = nx.shortest_path(G, src, tgt)
-    print(f'Shortest path ({len(path)-1} hops):')
-    for i, nid in enumerate(path):
-        label = G.nodes[nid].get('label', nid)
-        if i < len(path) - 1:
-            _raw = G[nid][path[i+1]]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw
-            rel = edge.get('relation', '')
-            conf = edge.get('confidence', '')
-            print(f'  {label} --{rel}--> [{conf}]')
-        else:
-            print(f'  {label}')
-except nx.NetworkXNoPath:
-    print(f'No path found between {a_term!r} and {b_term!r}')
-except nx.NodeNotFound as e:
-    print(f'Node not found: {e}')
-"
-```
-
-Replace `NODE_A` and `NODE_B` with the actual concept names from the user. Then explain the path in plain language - what each hop means, why it's significant.
-
-After writing the explanation, save it back:
-
-```bash
-$(cat graphify-out/.graphify_python) -m graphify save-result --question "Path from NODE_A to NODE_B" --answer "ANSWER" --type path_query --nodes NODE_A NODE_B
-```
-
----
-
-## For /graphify explain
-
-Give a plain-language explanation of a single node - everything connected to it. Prefer the CLI when installed:
-
-```bash
-graphify explain "NODE_NAME"
-```
-
-If the CLI is unavailable, run it inline:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json, sys
-import networkx as nx
-from networkx.readwrite import json_graph
-from pathlib import Path
-
-data = json.loads(Path('graphify-out/graph.json').read_text(encoding='utf-8'))
-G = json_graph.node_link_graph(data, edges='links')
-
-term = 'NODE_NAME'
-term_lower = term.lower()
-
-# Find best matching node
-scored = sorted(
-    [(sum(1 for w in term_lower.split() if w in G.nodes[n].get('label','').lower()), n)
-     for n in G.nodes()],
-    reverse=True
-)
-if not scored or scored[0][0] == 0:
-    print(f'No node matching {term!r}')
-    sys.exit(0)
-
-nid = scored[0][1]
-data_n = G.nodes[nid]
-print(f'NODE: {data_n.get(\"label\", nid)}')
-print(f'  source: {data_n.get(\"source_file\",\"unknown\")}')
-print(f'  type: {data_n.get(\"file_type\",\"unknown\")}')
-print(f'  degree: {G.degree(nid)}')
-print()
-print('CONNECTIONS:')
-for neighbor in G.neighbors(nid):
-    _raw = G[nid][neighbor]; edge = next(iter(_raw.values()), {}) if isinstance(G, nx.MultiGraph) else _raw
-    nlabel = G.nodes[neighbor].get('label', neighbor)
-    rel = edge.get('relation', '')
-    conf = edge.get('confidence', '')
-    src_file = G.nodes[neighbor].get('source_file', '')
-    print(f'  --{rel}--> {nlabel} [{conf}] ({src_file})')
-"
-```
-
-Replace `NODE_NAME` with the concept the user asked about. Then write a 3-5 sentence explanation of what this node is, what it connects to, and why those connections are significant. Use the source locations as citations.
-
-After writing the explanation, save it back:
-
-```bash
-$(cat graphify-out/.graphify_python) -m graphify save-result --question "Explain NODE_NAME" --answer "ANSWER" --type explain --nodes NODE_NAME
-```

+ 0 - 52
service/cmd/mcp/.kiro/skills/graphify/references/transcribe.md

@@ -1,52 +0,0 @@
-# graphify reference: transcribe video and audio
-
-Load this only when `detect` reported one or more `video` files. A corpus with no video never reads this.
-
-### Step 2.5 - Transcribe video / audio files (only if video files detected)
-
-Skip this step entirely if `detect` returned zero `video` files.
-
-Video and audio files cannot be read directly. Transcribe them to text first, then treat the transcripts as doc files in Step 3.
-
-**Strategy:** Read the god nodes from `graphify-out/.graphify_detect.json` (or the analysis file if it exists from a previous run). You are already a language model — write a one-sentence domain hint yourself from those labels. Then pass it to Whisper as the initial prompt. No separate API call needed.
-
-**However**, if the corpus has *only* video files and no other docs/code, use the generic fallback prompt: `"Use proper punctuation and paragraph breaks."`
-
-**Step 1 - Write the Whisper prompt yourself.**
-
-Read the top god node labels from detect output or analysis, then compose a short domain hint sentence, for example:
-
-- Labels: `transformer, attention, encoder, decoder` → `"Machine learning research on transformer architectures and attention mechanisms. Use proper punctuation and paragraph breaks."`
-- Labels: `kubernetes, deployment, pod, helm` → `"DevOps discussion about Kubernetes deployments and Helm charts. Use proper punctuation and paragraph breaks."`
-
-**Export** it as `GRAPHIFY_WHISPER_PROMPT` (the exact name the transcriber reads — and it must be `export`ed so the child Python process sees it) for the next command.
-
-**Step 2 - Transcribe:**
-
-```bash
-export GRAPHIFY_WHISPER_MODEL=base  # or whatever --whisper-model the user passed (must be exported)
-export GRAPHIFY_WHISPER_PROMPT="<the one-sentence domain hint you composed in Step 1>"
-$(cat graphify-out/.graphify_python) -c "
-import json, os, sys
-from pathlib import Path
-from graphify.transcribe import transcribe_all
-
-detect = json.loads(Path('graphify-out/.graphify_detect.json').read_text(encoding=\"utf-8\"))
-video_files = detect.get('files', {}).get('video', [])
-prompt = os.environ.get('GRAPHIFY_WHISPER_PROMPT', 'Use proper punctuation and paragraph breaks.')
-
-transcript_paths = transcribe_all(video_files, initial_prompt=prompt)
-# Write the JSON from Python (NOT a shell '>' redirect): transcribe_all/Whisper
-# print progress to stdout, which would otherwise corrupt the JSON file (#1392).
-Path('graphify-out/.graphify_transcripts.json').write_text(json.dumps(transcript_paths, ensure_ascii=False), encoding=\"utf-8\")
-print(f'Transcribed {len(transcript_paths)} file(s)', file=sys.stderr)
-"
-```
-
-After transcription:
-- Read the transcript paths from `graphify-out/.graphify_transcripts.json`
-- Add them to the docs list before dispatching semantic subagents in Step 3B
-- Print how many transcripts were created: `Transcribed N video file(s) -> treating as docs`
-- If transcription fails for a file, print a warning and continue with the rest
-
-**Whisper model:** Default is `base`. If the user passed `--whisper-model <name>`, `export GRAPHIFY_WHISPER_MODEL=<name>` (it must be exported, not just assigned) before running the command above.

+ 0 - 210
service/cmd/mcp/.kiro/skills/graphify/references/update.md

@@ -1,210 +0,0 @@
-# graphify reference: incremental update and cluster-only
-
-Load this only when the user passed `--update` or `--cluster-only`. A first-time full build never reads this file.
-
-## For --update (incremental re-extraction)
-
-Use when you've added or modified files since the last run. Only re-extracts changed files - saves tokens and time.
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import sys, json
-from graphify.detect import detect_incremental, save_manifest
-from pathlib import Path
-
-result = detect_incremental(Path('INPUT_PATH'))
-new_total = result.get('new_total', 0)
-print(json.dumps(result, indent=2, ensure_ascii=False))
-Path('graphify-out/.graphify_incremental.json').write_text(json.dumps(result, ensure_ascii=False), encoding=\"utf-8\")
-deleted = list(result.get('deleted_files', []))
-if new_total == 0 and not deleted:
-    print('No files changed since last run. Nothing to update.')
-    raise SystemExit(0)
-if deleted:
-    print(f'{len(deleted)} deleted file(s) to prune.')
-if new_total > 0:
-    print(f'{new_total} new/changed file(s) to re-extract.')
-"
-```
-
-Then populate `.graphify_detect.json` so Steps 3A–6 (which read it unconditionally) see the right state for an incremental run. `files` carries the changed subset (drives Step 3A AST + Step 3B0 cache check on only what changed); `all_files` carries the full corpus for any step that needs corpus-wide context:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-r = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
-Path('graphify-out/.graphify_detect.json').write_text(json.dumps({
-    'files': r.get('new_files', {}),
-    'all_files': r.get('files', {}),
-    'total_files': r.get('new_total', 0),
-    'total_words': r.get('total_words', 0),
-    'skipped_sensitive': r.get('skipped_sensitive', []),
-    'needs_graph': True,
-}, ensure_ascii=False), encoding=\"utf-8\")
-"
-```
-
-If new files exist, first check whether all changed files are code files:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-
-result = json.loads(open('graphify-out/.graphify_incremental.json', encoding='utf-8').read()) if Path('graphify-out/.graphify_incremental.json').exists() else {}
-code_exts = {'.py','.ts','.js','.go','.rs','.java','.cpp','.c','.rb','.swift','.kt','.cs','.scala','.php','.cc','.cxx','.hpp','.h','.kts','.lua','.toc','.f','.F','.f90','.F90','.f95','.F95','.f03','.F03','.f08','.F08'}
-new_files = result.get('new_files', {})
-all_changed = [f for files in new_files.values() for f in files]
-code_only = all(Path(f).suffix.lower() in code_exts for f in all_changed)
-print('code_only:', code_only)
-"
-```
-
-If `code_only` is True: print `[graphify update] Code-only changes detected - skipping semantic extraction (no LLM needed)`, run only Step 3A (AST) on the changed files, skip Step 3B entirely (no subagents), then go straight to merge and Steps 4–8.
-
-If `code_only` is False (any changed file is a doc/paper/image/video): **first, if any changed file is in `new_files['video']`, run `references/transcribe.md` (Step 2.5) on those files, then rewrite `.graphify_detect.json` to move the resulting transcript paths into `files['document']` and drop `files['video']`** — otherwise raw `.mp4/.mp3` paths are fed to semantic subagents as unreadable media (#1392). Then run the full Steps 3A–3C pipeline as normal.
-
-
-If no new files exist (only deletions), create an empty extraction so the merge step can prune:
-
-```bash
-if [ ! -f graphify-out/.graphify_extract.json ]; then
-    echo '[graphify update] Only deletions -- creating empty extraction for merge.'
-    $(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-Path('graphify-out/.graphify_extract.json').write_text(json.dumps({'nodes':[],'edges':[],'hyperedges':[],'input_tokens':0,'output_tokens':0}), encoding='utf-8')
-"
-fi
-```
-
-
-Then:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from pathlib import Path
-from graphify.build import build_merge
-from graphify.detect import save_manifest
-
-# Load new extraction and incremental state
-new_extraction = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
-incremental = json.loads(Path('graphify-out/.graphify_incremental.json').read_text(encoding=\"utf-8\"))
-deleted = list(incremental.get('deleted_files', []))
-# prune_sources is ONLY for genuinely DELETED files. Changed/re-extracted files are
-# handled by build_merge's replace-on-re-extract (#1344): every source_file in
-# new_chunks is dropped from the base before merge, so old/stale nodes don't survive.
-# Do NOT add `changed` here: with root= passed, prune_set relativizes to the same base
-# as the freshly merged nodes and would DELETE the re-extracted content (#1178 is moot
-# now that replace — not the dedup pass — reconciles changed files).
-prune = list(deleted) or None
-
-# Use build_merge() — reads graph.json directly without NetworkX round-trip
-# so edge direction (calls, implements, imports) is always preserved (#801).
-# Pass root= so prune_sources (absolute paths from detect_incremental) are
-# relativized to match the graph's relative source_file values; without it
-# nothing is pruned and stale nodes accumulate on every update (#1361).
-# directed=IS_DIRECTED: replace IS_DIRECTED with True if --directed was given, else
-# False. Without it a --directed --update silently rebuilds undirected and collapses
-# reciprocal A<->B edges (#1392).
-G = build_merge(
-    [new_extraction],
-    graph_path='graphify-out/graph.json',
-    prune_sources=prune,
-    root='INPUT_PATH',
-    directed=IS_DIRECTED,
-)
-print(f'[graphify update] Merged: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges')
-
-# Write merged result back to .graphify_extract.json so Step 4 sees the full graph
-merged_out = {
-    'nodes': [{'id': n, **d} for n, d in G.nodes(data=True)],
-    'edges': [
-        # Explicit source/target last so they win over any stale attrs in d.
-        {**{k: val for k, val in d.items() if k not in ('_src', '_tgt', 'source', 'target')},
-         'source': d.get('_src', u), 'target': d.get('_tgt', v)}
-        for u, v, d in G.edges(data=True)
-    ],
-    # G.graph["hyperedges"] holds hyperedges from both existing graph.json
-    # and new_extraction (build_merge combines them). Falling back to
-    # new_extraction only would silently drop prior-run hyperedges (#801).
-    'hyperedges': list(G.graph.get('hyperedges', [])),
-    'input_tokens': new_extraction.get('input_tokens', 0),
-    'output_tokens': new_extraction.get('output_tokens', 0),
-}
-Path('graphify-out/.graphify_extract.json').write_text(json.dumps(merged_out, ensure_ascii=False), encoding=\"utf-8\")
-print(f'[graphify update] Merged extraction written ({len(merged_out[\"nodes\"])} nodes, {len(merged_out[\"edges\"])} edges)')
-
-# Save manifest so next --update diffs against today's state, not the
-# prior run's baseline (prevents ghost-node reports on subsequent updates).
-# root= matches the build_merge call above so the manifest keys stay relative to
-# the scan root — portable across clones/machines, so --update keeps matching
-# cached files instead of missing every one after a move (#1417).
-#
-# Only stamp semantic files (docs/papers/images) that ACTUALLY produced output
-# THIS run (new_extraction is this run's fresh extraction, read above before the
-# merge overwrote the file): a changed doc whose chunk failed must stay unstamped
-# so the next --update re-queues it, otherwise it is marked done and its content
-# is lost forever (#2015). Mirrors the library extract path
-# (cli._stamped_manifest_files + clear_semantic + scan_corpus).
-from graphify.cli import _stamped_manifest_files
-_manifest_files = _stamped_manifest_files(incremental['files'], new_extraction, Path('INPUT_PATH'))
-# Changed semantic files dispatched this run but NOT stamped had their chunk fail
-# or be omitted; clear any stale semantic_hash so they are re-queued (#1948).
-_sem_types = ('document', 'paper', 'image')
-_dispatched = {f for t, fl in incremental.get('new_files', {}).items() if t in _sem_types for f in fl}
-_stamped = {f for fl in _manifest_files.values() for f in fl}
-_cleared = _dispatched - _stamped
-# scan_corpus = the RAW full corpus so in-root files newly excluded since last run
-# are dropped rather than masquerading as deletions; untouched rows preserved (#1908).
-_scan = {f for fl in incremental['files'].values() for f in fl}
-save_manifest(_manifest_files, root='INPUT_PATH', scan_corpus=_scan, clear_semantic=_cleared or None)
-print('[graphify update] Manifest saved.')
-"
-```
-
-Then run Steps 4–8 on the merged graph as normal.
-
-After Step 4, show the graph diff:
-
-```bash
-$(cat graphify-out/.graphify_python) -c "
-import json
-from graphify.analyze import graph_diff
-from graphify.build import build_from_json
-from networkx.readwrite import json_graph
-import networkx as nx
-from pathlib import Path
-
-# Load old graph (before update) from backup written before merge
-old_data = json.loads(Path('graphify-out/.graphify_old.json').read_text(encoding=\"utf-8\")) if Path('graphify-out/.graphify_old.json').exists() else None
-new_extract = json.loads(Path('graphify-out/.graphify_extract.json').read_text(encoding=\"utf-8\"))
-G_new = build_from_json(new_extract, directed=IS_DIRECTED)
-
-if old_data:
-    G_old = json_graph.node_link_graph(old_data, edges='links')
-    diff = graph_diff(G_old, G_new)
-    print(diff['summary'])
-    if diff['new_nodes']:
-        print('New nodes:', ', '.join(n['label'] for n in diff['new_nodes'][:5]))
-    if diff['new_edges']:
-        print('New edges:', len(diff['new_edges']))
-"
-```
-
-Before the merge step, save the old graph: `cp graphify-out/graph.json graphify-out/.graphify_old.json`
-Clean up after: `rm -f graphify-out/.graphify_old.json`
-
----
-
-## For --cluster-only
-
-Skip Steps 1–3. Re-run clustering on the existing graph:
-
-```bash
-graphify cluster-only .
-```
-
-`graphify cluster-only .` is **self-contained**: it re-clusters, names communities, and regenerates `GRAPH_REPORT.md`, `graph.json`, and `graph.html` from the existing graph. **Do not re-run Steps 5–9** — they read intermediate files (`.graphify_extract.json`, `.graphify_detect.json`, `.graphify_analysis.json`) that a prior build's cleanup (Step 9) already deleted, so they raise `FileNotFoundError` (#1392). When it finishes, present the refreshed `GRAPH_REPORT.md` summary as usual.

+ 0 - 355
service/cmd/mcp/.kiro/skills/lieflat-charts/SKILL.md

@@ -1,355 +0,0 @@
----
-name: lieflat-charts
-description: 一套模板驱动的数据可视化与报告生成 skill,既能严格从 Lupi、Basics、Glance、Maps 与 Interactive gallery 的真实实现生成 HTML 图表,也能从 12 套中英文整页报告模板生成可发布的 HTML 报告;以 Mono 为保底,能按数据语义自动选择内置彩色预设,也支持用户明确提供的自定义色板。地图仅在用户明确要求时启用,同一交付禁止混用色系。
----
-
-# Lieflat Charts — 图表品味法典
-
-Lieflat Charts 是一套遵循 Agent Skills 格式的数据可视化与报告生成 skill,以 Mono 灰阶为保底,也会在数据语义和使用场景明确适配时自动选择一套彩色预设。本 skill 在 [moxt.ai](https://moxt.ai) 制作,支持 Lupi 编辑叙事型、Glance 快速判断型、Basics 基础编辑型、Maps 地图、Interactive 交互大图,以及 `report-catalog.md` 中的 12 套中英文整页报告模板。用户给你数据和场合,可以得到一个无需构建、双击可打开的单文件 HTML 图表或 HTML 报告。纯 SVG 图可离线运行;使用 Chart.js、ECharts、地图 GeoJSON 或在线字体的图,在未内联依赖时需要联网。**默认输出是图表,不是报告:只给数据、说“可视化 / 分析数据 / 做几张图”或没有明确交付形式时,必须进入图表模式;只有用户明确要求“报告 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付时,才进入报告模式。** 图表模式下**必须先从 Lupi Editorial 和 Lupi Basics 中选型;只有两者都没有合适模板,或用户明确要求 Glance / dashboard / 三秒快读时,才允许使用 Glance。Maps 只有在用户明确说要地图或地域分布时才启用。**
-
-颜色无需用户显式触发。按第六点五节在 Mono 与 `color-presets.js` 的三套内置方案之间自动判断;用户明确给出品牌色或自定义色值时,可以建立一套交付内的 custom 色板。同一份 HTML 或同一组交付只能锁定一种色彩系统,不得混用。换肤样张在 `templates/color/`。
-
-**怎么查一张图的参考代码**:catalog 查到图型 → 打开对应 gallery 文件 → 按卡内标题找到 `<div class="card">` 块看结构 → 在 `<script>` 里搜同名 `// ════` 注释块拿渲染代码。不要整页照抄——gallery 是多卡合页,交付给用户的永远是按第九节骨架组装的单图文件。
-
-**目标用户是非程序员**(写作者、运营、做 PPT 的人)。他们说人话("帮我把这季度转化画一下,发公众号用"),不说图型名。你的职责是把人话翻译成正确的图,而且好看到能直接发出去。
-
----
-
-## 零、输出模式与模板优先硬约束
-
-### 0.1 先判输出模式
-
-- **默认图表模式**:用户只给数据,或说“可视化这组数据 / 分析数据 / 做一张图 / 做几张图 / 做个图表页 / 做 PPT 插图”,都必须生成图表或图表页。没有报告关键词时,不得自行套用 R01–R12,也不得因为数据很丰富、结论很多或用户说“分析”就升级成报告。
-- **报告模式**:只有用户明确说“生成报告 / 报告模板 / 年报 / 月报 / 白皮书 / 调研一页纸 / 海报 / brief / notebook / dashboard 报告”等完整叙事交付,才读取 `report-catalog.md`,按报告类型、内容结构、版心、密度、阅读速度和语言选择一个 R01–R12。目录里的场景只是召回线索,不是对模板的限制。
-- **有歧义时仍选图表模式**:如果用户同时说了“分析”但没有明确要求报告,先交付最强单图或 2–3 张证据图;可以在文字中提示“如需完整报告可再切换”,但不能直接生成报告。
-- **不要把报告模式当成“多画几张图”**。报告模板决定整页结构;页内图表仍必须遵守 `catalog.md` 和本文件的图表规则。
-- 报告模板提供 `.zh.html` 与 `.en.html` 两个版本。用户没有指定语言时,跟随输入语言;中英文内容不能混在同一版本中。
-
-以下规则不是建议,违反任意一条都必须返工:
-
-1. **必须从仓库模板生成。** 每张成品图先在 `catalog.md` 锁定图型编号,再打开对应 gallery 的真实实现:Lupi 使用 `templates/lupi-gallery.html`,基础型使用 `templates/basics-gallery.html`,Glance 使用 `templates/glance-gallery.html`,地图使用 `templates/maps-gallery.html`,交互大图使用 `templates/big-*.html`。彩色图仍以这些原版模板为结构正本,`templates/color/` 只用于查配色。
-2. **必须沿用所选模板的代码骨架。** 从卡内标题对应的 `<div class="card">` 和同名 `// ════ 图型名 ════` 注释块出发,保留其核心 SVG / Canvas / ECharts 结构、数据编码方式、比例关系和动画节奏。允许替换数据、标题、旁注、来源与必要布局;禁止脱离模板另画一张“看起来差不多”的图,禁止拼接多个模板造混合图型,禁止退回图表库默认样式。
-3. **默认选型顺序固定。** 先完整比较 Lupi Editorial(L1–L20),再比较 Lupi Basics(F1–F17)。只要其中存在能诚实承载数据、容纳标签且可读的模板,就必须从这两组中选择。Maps 不进入这条默认链,只有用户明确要求地图时才单独检查。
-3.1 **两组之内还分主力与后备。** 主力是 **L1–L15 与 F1–F13**,默认从这里出图。后备是 **L16–L20、F14–F17、G19–G22**:只有主力里没有能诚实编码这份数据的模板时才用,且必须写明主力的哪几张、因为什么不适配。理由不得只写“新图更好看”或“更专业”。唯一例外见第 3.2 条。
-3.2 **五种数据形状允许直接用后备图,不必先证伪主力。** 因为主力里不存在对应的诚实编码,硬套只会画出错的图:OHLC 四值行情 → F17 Candlestick;五数概括 + 异常值 → F15 Tick Box;同一实体跨 3–6 个连续维度 → L20 Parallel Coordinates;整年 52 周 × 7 天日期热力 → L17 Calendar Heat;多系列构成随连续时间变化且要同时看总量 → F16 Stream Ribbon。除这五种,后备图一律走 3.1 条。
-4. **Glance 是默认降级方案,不是并列首选。** 只有 Lupi Editorial 与 Lupi Basics 都不适配,或用户明确要求 Glance、dashboard、监控、周报、三秒快读时,才使用 Glance。降级前必须写明 Lupi / Basics 不适配的具体原因。
-5. **库外新造是最后手段。** Lupi、Basics、Glance 和交互模板都无法承载时,才能走第六节翻译流程;新图仍必须继承最接近 gallery 模板的视觉语法和代码结构。
-
-## 一、工作流程(每次请求都走这六步)
-
-1. **判数据形状。** 别问用户要什么图,看他的数据长什么样:几个类目的比较?带时间的序列?占比?带正负?多对一归属?网络?逐条记录的分布?形状是选图的主键。
-2. **先审计主力,再考虑后备。** 按数据形状先扫描主力 L1–L15 与 F1–F13 的候选,至少比较 3 个;不足 3 个就列出全部。比较语义契合、单位诚实、标签容纳、阅读速度、叙事张力和本批次是否重复。主力全部不适配时,才扫描后备 L16–L20 / F14–F17,并写明主力哪几张为什么不行(第零节 3.1)。数据形状命中第零节 3.2 那五种时,直接用对应后备图。
-3. **必要时才检查 Glance。** 只有 Lupi 与 Basics 候选全部失败,或用户明确要求 Glance / dashboard / 监控 / 周报 / 三秒快读,才扫描 Glance(G1–G22)。选择 Glance 时记录为什么 Lupi 与 Basics 无法承载,不得只写“Glance 更直观”。用户明确要求地图时跳到 Maps(M1–M2),不把地图混入普通候选。
-4. **锁定真实模板,再组织整页。** 每张图必须记录体系、图型编号、gallery 文件和卡内标题,并以该卡的真实结构与渲染代码为骨架。禁止先想好“这一页要讲六件事”,再临时发明图型;整页叙事只能在模板锁定之后组织。
-5. **按图数规则组成批次。** 一张图只承担一个独立结论;去掉重复结论后,再按第 1.2 节的默认区间决定数量。模板需要全局分配:不重复、不堆同一种轮廓、不为了凑数硬加图。
-6. **按模板渲染并自检**(第零、二、三、八节)。逐项核对成品是否仍能对应到所选 gallery 实现;不能因为改数据而换掉模板的核心几何、编码或动效。按第六点五节为整份交付选择 Mono 或一套彩色预设;库外图型走翻译流程(第六节)。
-
-### 1.1 报告模式工作流
-
-1. 读取 `report-catalog.md`,至少比较 3 个报告候选并记录淘汰理由。按内容结构、信息密度、版心和阅读速度选模板,不把模板名称当成行业限制;例如“旅行手记”也可以承载运动或个人生活数据,“月度运营”也可以承载财报或业务数据报告。
-2. 锁定一个语言版本和一个报告模板文件;不得拼接两个报告模板的版式。
-3. 先提炼整页的主结论、证据结论、上下文说明和来源,再把它们分配到模板已有的标题、导语、KPI、图表、旁注和结尾槽位。
-4. 每个图表槽位单独按 `catalog.md` 选型,优先复用 Lupi / Basics 的真实实现;报告模板的布局不能成为绕过图表数据契约的理由。
-5. 复制对应报告模板的整份 HTML 作为起点,只替换数据、文案、来源、图例、语言和必要模块。不得保留演示数据、演示来源、Moxt 链接或模板原始结论。
-6. 报告页的色彩系统锁定为模板当前色系;如用户明确要求改色,整页统一替换为 Mono、单一内置预设或完整 custom 色板,不得局部混搭。
-7. 报告模式的交付前额外检查:版心尺寸未漂移、章节顺序仍成立、页内图表数量有证据支撑、定尺模板没有溢出、中文字体和英文 fallback 可读。
-
-### 1.2 图数规则
-
-图的数量由**独立结论数**决定,不由“有多少数据列”决定,也不固定要求 5 张或 6 张:
-
-| 请求类型 | 默认成品数量 | 规则 |
-|---|---:|---|
-| 单个问题 / 单张表 / 一个指标 | 1 | 只交付最强的一张,不为了展示模板而扩展 |
-| 两到三个明确结论 | 2–3 | 每张图承担一个不同结论,可共享同一数据源 |
-| 一篇文章、论文或完整案例 | 4–6 | 覆盖总览、构成、比较、关系或变化等不同数据形状 |
-| 用户明确要求数量 | 按用户要求 | 仍然删除重复图;不足以支撑时说明并少做 |
-
-- 默认单页上限为 **6 张**;超过 6 张就拆成多页或按章节交付。
-- 候选方案、Glance/Lupi 对照稿不计入最终图数;它们是选择过程,不是成品批次。
-- 一页多图至少保留一个总览结论,其余图必须提供新的比较维度、关系、时间变化或细节证据。
-- 如果两个图表达同一个结论,只保留阅读场合更合适、数据契约更诚实的一张。
-
-## 二、Mono 语法 · 硬规则(违反即返工)
-
-默认引用 `mono-tokens.js`(开源分发时把内容内联进 HTML)。与 token 冲突的取值一律以 token 为准。走彩色时只替换颜色 token,字体、圆角、布局和动画仍以 `mono-tokens.js` 为准。
-
-**颜色**
-- 默认只有纸灰 `#F0EFEB` 和炭黑 `#1C1C1A` 两极,中间 7 级灰阶 ladder。彩色例外按第六点五节从预设起步。
-- **明度即数据**:最重要 = 最黑(暗卡上反转为最亮)。多系列按重要性沿 ladder 分配,不按顺序随便拿。
-- **一律实心**:不透明材质、不发光、不渐变滤镜、无阴影。质感全靠明度对比和形状。唯一例外:叠加型图(Radial Patchwork)里透明度本身编码密度——那是数据,不是装饰。
-- 暗卡(`.card.dark`)只给两种图:必须暗底衬托的形(花瓣、发光感丝线/网络)。默认浅卡;每屏(4 卡)最多 1 张暗卡。
-
-**排版**
-- Inter 全家。标题 700 / 图内数值 800 / 轴标签 600。卡片结构固定四件套:结论式标题(h2)+ 副标题(图例和时间范围写在这里,用 `·` 分隔)+ 图 + 来源行(全大写、加字距)。
-- 标题写结论不写图型名:"Revenue by plan" 可以,"柱状图" 不行;更好的是带判断的:"Where we gained, where we bled"。
-- SVG 最小字号:半宽卡 6.5px、通栏 5.5px。装不下的信息改 hover 出,不许缩小字号硬塞。
-
-**形状**
-- 卡片圆角 24px,无边框无阴影,靠留白分卡。柱端胶囊圆角(竖柱圆上端、横柱圆外端)。
-- **柱状图不断轴。** 柱的契约是长度∝数值,断轴毁契约。极端值场景的正确做法:①让极端值冲天(最诚实)②主图+放大镜小图 ③撕柱不撕轴(明说画不下)。
-- 花瓣皮(粗黑缝+圆角扇瓣)只适用等分或近似等分的径向图。宽窄悬殊的扇区会相互遮蔽,降低可读性。
-
-**动画**
-- 入场动画默认开,`quarticOut` 快进快停,不弹跳(波浪入场可用 elasticOut)。点阵 stagger 8–15ms/个,条形 80–130ms/根。
-- 统一 reveal 机制:滚入视野才播 + 点击重播(用 token 里的 `obsReveal`,带 timer 清理)。
-- 必须带 `prefers-reduced-motion` 降级(token 的 CSS 已含)。
-- **动画不可优于结构**:一个效果如果需要发明新布局才能安放,不配存在(effectScatter 教训)。
-
-**数据**
-- 演示数据用 token 的 `rnd(i,k)` 确定性伪随机,禁用 `Math.random()`——刷新必须长一样。
-- 数值和视觉严格成正比。面积编码用 `Math.sqrt(v)` 换算半径,不许拿数值直接当半径。
-
-## 三、两个风格系的分工
-
-同一份 mono 色板下有两种世界观,选哪个看**场合**和**读者愿意花几秒**:
-
-| | Glance 系(22 张) | Lupi 系(细读,20 张) |
-|---|---|---|
-| 基本单元 | 形状(粗柱、大弧、色块) | 记录(一个点 = 一条数据) |
-| 线条 | 2px+,理直气壮 | 0.5–0.7px 发丝 |
-| 聚合 | 提前聚合好,给结论 | 拒绝聚合,摊开原材料 |
-| 读法 | 扫一眼(<10s) | 凑近读(30s+) |
-| 场合 | 周报、dashboard、随手贴 | 年报、对外故事页、海报 |
-| 引擎 | Chart.js / ECharts | 手写 SVG |
-
-**默认策略:Lupi Editorial → Lupi Basics → Glance。** 没有明确场合时,禁止默认 Glance。放年报、公众号长文、海报、开源 README、作品集时先选 Lupi Editorial;数据较少或适合熟悉图型时再选 Lupi Basics。只有前两组都没有合适模板,或用户明确要 dashboard、监控、周报、三秒快读时,才进入 Glance 候选池。
-
-**全量候选审计不是“把所有图都做一遍”。** 先用数据形状筛出能编码同一本体的候选,再把候选分成:
-
-- **语义合适**:每个点、线、面、串珠背后都有真实单位或明确聚合口径。
-- **视觉合适**:标签装得下,密度足够,读者能按预期时间读完。
-- **叙事合适**:图形本身能承载这批数据要讲的判断,而不只是把数字摆出来。
-
-最终选择是三项的交集,不是“哪个模板文件最容易复制”。默认审计先在 Lupi Editorial 与 Lupi Basics 内完成;只有两组都失败才扩展到 Glance。若同一批要做多张图,再对候选做一次全局分配:模板不重复,形状轮换,最多一张暗卡,避免整页变成六张相似的环或横条。
-
-**数据少(只有几个百分比)不等于只能 Glance。** Lupi 化的正道是单位分解:把聚合数摊回可数单位(1 点 = 1 人 / 1 百分点),密度来自单位而不是记录数。单位含义写进副标题,只摊诚实单位(占比加总 100 → 100 个点),不编造不存在的个体记录。取整时如果加总不足 100(如 49.0+27.4+13.9+5.0+3.2 → 98),在底注写明「另外 N 人被四舍五入吃掉了」,不凑假单位。
-
-**小数据走 Lupi 的路径优先级(经多轮案例验证)**:
-1. **先全量扫描,再选代码骨架**——基础型组 F1–F13(`templates/basics-gallery.html`)是稀疏数据的 Lupi 词汇:柱/折线/面积/环形/横条/分组/堆叠/散点/瀑布/热力/进度/哑铃/矩形树图;编辑型 L1–L15 则提供逐记录、单位分解、关系与旁注语法。这两组是主力,小数据几乎总能在这里落地。F14–F17 与 L16–L20 是后备,按第零节 3.1 / 3.2 判断。不要因为 `basics-gallery.html` 里有现成代码就直接选 F 图。
-2. **用最接近本体的模板起步**——多选题百分比优先比较 L15、F5、L2;100% 构成优先比较 L14、L5、F4、G4;漏斗优先比较 L13、F1/F5 的降级表达。选择依据是数据编码方式,不是文件顺序。
-3. **库里真没有对应形状才新造**,且新造必须从 gallery 现有语法延伸(发丝 tick、确定性 rnd 抖动、paint-order 光晕、全大写注记),不引入库外参照——“Lupi 风”指 gallery 那批图的视觉语法,不是 Giorgia Lupi 本人的手绘风。
-4. 新造的小数据图要配满**环境结构层**:gallery 好看有一半靠无数据的家具(账本纸横线、虚线导轨、rim 刻度、每 10 单位的立柱网格、旁注引线)。数据层诚实稀疏,密度预算花在家具上。只画数据 + 一根底线的小数据图必然寒酸。
-
-**同一批产出(一页多图)里模板不重复。** 20 张 Lupi 系够轮换;同一数据能被多个模板承接时,选这一批里还没用过的。
-
-## 四、图型决策树(数据形状 → 候选)
-
-编号对应 `catalog.md`。这些只是**候选召回表**,箭头和书写顺序不代表优先级。实际选择必须遵守:先检查全部 Lupi Editorial 与 Lupi Basics 候选,确认无合适模板后,才允许采用 Glance 候选。
-
-- **少类目比较(≤8)** → G3 Chunky Bars ⇄ 竖排 F1 Rung Bars / 横排 F5 Tick Rows ⇄ L2 Dot Cascade(⚠️ cascade 类目名竖排,仅当名称 ≤4 字或短缩写时可用;中文长类目换 F5/L5/L12)
-- **多选题百分比(各项独立 0–100,加总可超 100)** → G3 Chunky Bars ⇄ L15 Ballot Tally
-- **多类目分布(30–60 根)** → G12 Stagger Wave
-- **带正负的分类数值** → G10 Diverging Bar
-- **占比 / 100% 构成** → G4 Dot Waffle ⇄ L14 Hundred Field ⇄ F4 Tick Donut(饼图的默认替代);近似等分且要门面 → G2 Petal Rose;占比×强度双编码 → G13 Big Slice;按类堆叠 → F7 Stacked Rungs
-- **两时点对比(前后 / 今昔)** → 类目级(≤6 类)→ F12 Dumbbell Queue(横向,串珠 = 真单位)⇄ F6 Paired Rungs(并肩双梯);只有 2–4 条序列要画趋势线 → Glance chunky slope(粗线、大数、结论进标题)。交叉过多的斜率图会显著降低可读性,不要为了 Lupi 化增加装饰结构。
-- **日序列(≤30 天,带区间)** → G1 Range Capsules ⇄ 逐日读数 F2 Hairline Line ⇄(90 天级、要肌理)L3 Barcode Lollipop;30–60 天看形态 → F3 Hairline Area
-- **累计增长** → G18 Draw-in + Counter
-- **双序列因果(投入 vs 产出)** → G8 Rainfall
-- **实时数据** → G17 Dynamic Stream
-- **排名随时间变** → 需要动态演示用 G16 Bar Race;静态见下方「排名随离散时间变化」一条
-- **星期×小时×量** → G14 Single Axis ⇄ F10 Dot Heat
-- **瀑布 / 增减分解(≤6 级)** → F9 Rung Waterfall
-- **单值进度(0–100%)** → F11 Tick Gauge ⇄ G18 Draw-in + Counter
-- **二维散点(≤20 点)** → F8 Plumb Scatter;几百点分布 → G15 Jitter Strip
-- **单变量分箱频数** → 先看 F1 Rung Bars 能不能直接承接(bin 当类目);确实需要分箱语义时用 F14 Rung Histogram。bin 边界必须有业务含义;不要为了好看任意切箱。
-- **分组连续分布** → **五数概括 + 异常值直接用 F15 Tick Box**(主力无对应编码)。只看密度轮廓时先比较 G15 Jitter Strip;G19 Violin 与 L19 Ridgeline 属后备,要写明为什么 Jitter / F15 不够。
-- **逐条记录分布** → G15 Jitter Strip。只有每个点都必须保留且堆积形状本身有意义时,才降级到 L18 Beeswarm。蜂群不是默认优先。
-- **分类×分类+量(矩阵)** → 主力优先:轻量 L4 Arc Matrix,跨年份带旁注 L9 Bubble Almanac。两者都撑不住(格子多到气泡互相挤、必须靠明度读)才用 L16 Matrix Heat;要快读且每格直接标数才用 G20。
-- **全年日期×数量** → **整年 52 周 × 7 天直接用 L17 Calendar Heat**(主力无对应编码,L3 只到 90 天级)。星期×小时的重复周期仍用 F10 / G14。
-- **多对一归属** → 造型强 L5 Radial Convergence ⇄ 带名单 L12 Type Colonnade
-- **漏斗 / 分阶段递减** → L13 Hourglass Stream
-- **层级结构** → G7 Tree LR
-- **层级 + 份额 / 权重(两层、值为正数)** → F13 Nested Treemap;只看谁属于谁、不比较大小 → G7 Tree LR
-- **多系列构成随连续时间变化** → 静态类目构成先用 F7 Stacked Rungs;**要同时看总量与构成的连续时间流,直接用 F16 Stream Ribbon**(主力无对应编码)。只有单序列总量用 F3 / G17。
-- **同一实体跨 3–6 个连续维度** → **直接用 L20 Parallel Coordinates**(主力无对应编码)。维度超过 6 个先筛选或拆图。
-- **OHLC 行情** → **直接用 F17 Candlestick**(主力无对应编码)。只有 min–max 区间而无开收盘值时用 G1。
-- **两端聚合流量** → 先看 L5 Radial Convergence / L12 Type Colonnade 能否承接归属关系;确实要看流量宽度才用 G22 Aggregate Sankey。要查每条真实路径用 B3 Threads。
-- **排名随离散时间变化(静态)** → 先比较 L11 Trend Lineage / L2 Dot Cascade;确实要逐期格子对位才用 G21 Rank Strip。
-- **事件序列生命史** → L11 Trend Lineage
-- **多实体出生时间+现状** → L1 Launch Fan
-- **逐事件时刻分布(一天内)** → L10 Radial Patchwork
-- **双极量表(两端都合法)** → L7 Brand Spectrum。注意和单极打分(雷达)区分:单极用 ECharts 原生雷达
-- **网络** → ≤15 节点 G6/G11 小图;>15 或要查数 → B1 环形 / B2 力导向;多段路径流向 → B3 Threads
-- **多组×网格(要装饰感)** → L8 Dotty Matrix;要读数 → 摊平用 G14
-- **同一实体集多维度轮播(演示)** → G9 Scatter Morph
-
-### 地图显式触发规则
-
-- 只有用户明确说“地图”“地域分布”“按国家/州着色”或点名 choropleth,才检查 Maps。数据表里仅出现地区字段不得自动触发。
-- 美国州级区域数值用 M1 US Choropleth;世界国家级区域数值用 M2 World Choropleth。地图面积是地理面积,不代表数值,副标题必须说明 `shade = value`。
-- M1/M2 依赖 ECharts 与在线 GeoJSON;交付时说明需要联网,或在用户明确要求离线时再内联合法来源的地图数据。
-- 中国地图不从 M1/M2 改名套用。只有拿到符合当前地图合规要求的完整数据源和审图信息后才制作。
-
-### F13 Nested Treemap 硬规则
-
-- 只接收层级数据和非负权重;父级值由子级求和,禁止父子总数互相矛盾。面积直接交给 ECharts treemap 布局,不再对值开方。
-- 默认展示两层(父组 + 叶子)。超过两层且读者需要逐层查询时才开启 drill-down;普通静态交付保持 `nodeClick:false`,避免误触改变视图。
-- Mono 的灰度只表示层级,不给每个叶子随机分配深浅;父组靠标题带、组间留白和边界区分。去掉颜色后仍能看清分组。
-- porcelain 用单色相明度表示有序的父组份额或层级;palm 用色相对应不超过 4 个顶层类目;wire 保持灰阶,只允许一个明确主角使用 `HERO`。
-- 面积已经表达数值时,颜色不得再次无说明地重复同一数值。副标题必须写清 `area = ...`,使用彩色时再写清 `color = ...`。
-- 叶子超过 30 个、标签大面积省略,或最小矩形小到无法形成稳定热区时,先合并长尾为 Other、拆成多张,或改用 G7 / L12 / B 系关系图。
-- Tooltip 至少显示完整层级路径、原始值和总量占比;卡内标签放不下就隐藏,不缩到低于最小字号。
-
-## 五、交互三问(决定静态还是可交互)
-
-按顺序问:
-
-1. **这根线/点背后有没有一条真实记录?** 没有(纯肌理装饰,如 Cluster Field 的辐条、Hourglass 的淌线)→ 禁止加交互,给没有内容的元素加 hover 是欺骗。
-2. 有记录 → **不点能不能读出来?** 元素 <50 且两端有标注(如 Colonnade)→ 静态够用,hover 是锦上添花。
-3. **元素 >50 根或多段路径**(如 Threads)→ 必须 hover/pin,否则只是氛围图。交互实现参考 `templates/big-threads.html`:可见线下垫 9px 透明孪生线当热区,hover 单线亮整条路径、hover 标签拉整束、点击钉住。
-
-## 六、库外图型 · 翻译流程
-
-用户要的图不在 64 张里(或发来一张参考图)时,**不是做不了,是现场造句**。流程四步,第一步不许跳:
-
-1. **先回答本体**:这个图型编码了什么?每个视觉通道(位置/长度/角度/面积/明度/密度)分别对应哪个数据维度?答不出来就问用户拿数据结构。抄形不抄魂 = 白做(pictorial bar 曾把"符号计数"错做成"树长高",返工)。
-2. **找最近的亲戚**:在 catalog 里找数据形状最像的一张当起点,继承它的布局骨架和动画节奏。
-3. **用 token 造句**:色板、字体、圆角、动画参数全部从 `mono-tokens.js` 拿,不许发明新颜色新字号。产出必须和库里的图"一眼一家人"。走彩色时颜色改从 `color-presets.js` 的单一预设中取,其他 token 仍以 `mono-tokens.js` 为准。
-4. **过第二节硬规则和第八节自检**,和库内图同一标准。
-
-参考图翻译的附加规则:识别参考图属于哪个流派(Glance or 编辑部),编辑部风格的密度、旁注、手绘感(`blob`)是本体的一部分,别做收敛了——almanac 曾因"不敢挤"返工。
-
-## 六点五、彩色(按场景自动选择,单一色系)
-
-**Mono 是保底方案,不是未触发彩色时的强制答案。** 用户没有提颜色时也要根据数据语义、类目数量、阅读场景和内容气质评估是否适合彩色;适配关系不明确,或颜色没有带来真实信息价值时,回到 Mono。
-
-彩色初稿从三套预设中选择一套,色值在 `color-presets.js`,样张在 `templates/color/`。预设用于快速得到稳定、统一的第一版,不是锁死的色板。用户要求"这个蓝再深一点"时,在当前色系内继续调整;用户明确给出品牌色、色值或完整色板时,按下方规则建立 custom 色板。调整后必须重新检查对比度、视觉主次和数据语义。
-
-**同一份 HTML 或同一组交付必须先锁定一个全局色彩系统:Mono、porcelain、palm、wire 或一套 custom。** 所有图共享该选择。若其中某张图不支持已选系统,不得单独换色;改选一套全局兼容的系统,或整组退回 Mono。
-
-**结构正本永远是原版 gallery。** 查渲染代码去 `templates/` 根目录的 gallery,`templates/color/` 只查配色。先按第零节选定结构,再决定是否换肤。
-
-### 用不用彩色
-
-按以下优先级判断:
-
-1. **用户明确指定。** 用户要求某种颜色、气质或预设时优先服从,但仍要通过容量、对比度和数据语义检查。
-2. **Agent 自动选择。** 即使用户没提颜色,只要数据形状和场景与某套预设有清楚的对应关系,就可以自动使用:有序单序列适合 porcelain;少量无序类目适合 palm;需要一个受控视线落点适合 wire。
-3. **关系不明确就用 Mono。** 类目过多、高密度记录、颜色没有稳定含义,或整组图无法共享同一预设时,使用 Mono。
-
-彩色不是升级版,而是 Agent 可以主动选择的语义工具。交付时用一句话说明所选色系及其承担的数据含义。
-
-### 自定义色板(只在用户明确要求时启用)
-
-Agent 不得自行发明第五套默认审美。只有用户给出明确色值、品牌色、品牌规范或"沿用这张参考图的配色"时,才建立 custom 色板;仅说"好看一点"仍从三套内置预设选择。
-
-1. **先确定颜色逻辑,再分配色值。** 有序数据用单色相明度梯;无序类目用分类色;需要聚焦时用中性色 + 一个强调色。禁止先拿到五个颜色,再平均撒到所有元素上。
-2. **建立完整角色,不直接散落 hex。** custom 至少定义 `BG`、`TXT`、`MUT`、`GRID`、`DATA`;需要重点时再定义 `HERO`,有序数据定义 `RAMP`,分类数据定义 `CAT`。成品内联一个 `CUSTOM` 对象,所有图从角色取色。
-3. **按输入数量解释。** 1 个颜色生成同色相明度梯;2 个颜色默认分为主数据色 + 强调色,不自动当作两个平等类目;3–6 个颜色只有在数据确有对应类目时才进入 `CAT`。超过 6 个先合并类目、改用位置/标签,或退回 Mono。
-4. **允许派生明暗,不允许偷渡新色相。** 浅色由用户色与 `BG` 混合,深色由用户色与 `TXT` 混合;不得借用 porcelain、palm、wire 的色值补齐 custom。
-5. **对比度是硬门。** 正文和小标签相对背景至少 4.5:1,大字至少 3:1;关键边界、数据形状和交互状态相对相邻颜色至少 3:1。不达标时优先调整明度,不擅自改变用户指定的主色相。
-6. **颜色不能成为唯一线索。** 分类仍带标签,序数仍保留位置/长度/面积,强调仍有标题或旁注说明。色觉异常用户去掉颜色后,图的结构仍应读得懂。
-7. **一次只锁定这一套 custom。** custom 不与任何内置预设混用;多图交付共享同一个 `CUSTOM`。除非用户明确要求把品牌色固化为项目级预设,否则 custom 只内联在当前成品,不回写 `color-presets.js`。
-
-用户后续要求"再深一点"、"背景暖一点"或替换某个色值时,修改对应角色并重新检查整套对比度和语义映射,不只改被点名的那一个元素。
-
-### 选哪套:先看数据形状,再看气质
-
-| 数据形状 | 允许 | 原因 |
-|---|---|---|
-| 有序 / 单序列(进度、热力、时间序列、单指标、排名) | porcelain、wire | 明度即数据的契约仍然成立 |
-| 无序类目 ≤4 | palm | 色相对应类目 |
-| 无序类目 5–6 | palm(勉强) | 依靠明度和饱和度辅助区分 |
-| 类目 >6 | Mono 灰阶 | 彩色预设容量不足 |
-| 需要克制但要一个视线落点 | wire | 灰阶承载数据,橙色只标主角 |
-
-| 用户会说 | 预设 | 气质 |
-|---|---|---|
-| 蓝、冷、学术、理性、严肃、科技感 | porcelain | 单色相蓝阶 |
-| 绿、暖、自然、温柔、复古、莫兰迪 | palm | 绿黄低饱和,琥珀点睛 |
-| 黑白、克制、杂志感、编辑部 | wire | 黑灰加一点荧光橙 |
-
-### 彩色硬规则
-
-- **同一交付只用一种色彩系统。** 一张图、同一份 HTML 或同一组姊妹图只能使用 Mono、`color-presets.js` 的一套预设,或一套用户明确要求的 custom。禁止预设彼此混用,也禁止 custom 借用内置预设补色。
-- **线宽 ×1.8、透明度地板 .85。** 灰阶下的发丝线换成淡色后容易消失;`dot heat` 除外。
-- **每张图至少 3 个色阶或色位。** 只用 1–2 个颜色时退回灰阶。
-- **颜色必须连接真实维度。** 序数梯接数值,分段色接月份或区段,分类色接类目;底注写清颜色在编码什么。
-- **强调色只给一个主角。** 第二个主角会消解强调。
-- **暗卡数据色必须亮于卡底。** 暗底大图使用 preset 的 `DARK` 块。`big-circular` / `big-force` / `big-threads` 只有 porcelain 和 palm;wire 使用原版 Mono 大图。
-- **palm 中 `DATA` 和 `HERO` 必须不同。** 琥珀和橄榄明度接近,不要把两者当作平等类目色。
-
-## 七、什么时候说不
-
-敢拒绝比什么都接更可信。以下情况不做,并给替代:
-
-- **断轴柱状图** → 拒绝,给三个诚实方案(冲天 / 放大镜 / 撕柱不撕轴)。
-- **发光 / 玻璃拟态 / 3D** → 拒绝,保持现有视觉语法。自定义色值按 custom 规则映射;跨色彩系统混用拒绝。
-- **拿多色相画单序列** → 拒绝。给 porcelain、wire 或退回 Mono。
-- **类目 >6 还要彩色** → 拒绝,退回 Mono 灰阶。
-- **用户未明确要求地图** → 不因数据里有地区字段自动使用 M1/M2;继续按普通比较、排名或构成选图。
-- **地图范围超出 M1/M2**(中国省级、城市点位、轨迹图等)→ 不套用错误边界;确认数据源、投影、合规和交互要求后再现场制作。
-- **给纯装饰元素加交互**(第五节第 1 问)→ 拒绝并解释。
-- **雷达图重构** → 不重构,用 ECharts 原生雷达 + Mono token 换肤(实测过:展示场景里"认得出的图型"有价值)。
-- 数据太少撑不起所选图型(如 3 个节点要力导向)→ 降级到更简单的图并说明。
-
-## 八、交付前自检清单
-
-1. 数值和视觉成正比?(面积用了 sqrt?柱没断轴?)
-2. 色板是否受控?整份 HTML 或同一组交付是否只使用 Mono、单一内置预设或单一 custom?Mono 中出现任何非 ladder 颜色即返工;内置彩色出现另一套预设颜色即返工;custom 是否只从 `CUSTOM` 角色取色并通过对比度检查?
-3. 标签会不会重叠?(barcode 教训:邻近峰值标签要强制最小间距)
-4. 最小字号踩线没有?(半宽 6.5 / 通栏 5.5)
-5. 演示数据是 `rnd` 确定性的?刷新两次长得一样?
-6. reveal 正常?(滚入播、点击重播、timer 不叠加、reduced-motion 降级在)
-7. `node --check` 过语法(把 `<script>` 内容抽出来查)。
-8. 卡片四件套齐全?标题是结论不是图型名?
-9. 副标题把图例说清了?(读者不看代码只看这一行)
-10. 是否完成全量候选审计?至少写下 3 个候选和淘汰理由,而不是只记录“采用了哪个模板”。
-11. 如果是一页多图,模板是否全局分配过?有没有相似轮廓重复、暗卡过量、所有图都变成同一种径向图?
-12. 每张图是否记录了图型编号、gallery 文件和卡内标题?成品的核心结构是否确实来自该模板,而不是重新发明?
-13. 如果使用 Glance,是否写明 Lupi Editorial 与 Lupi Basics 为什么都不适配?如果没有,返工回到 Lupi / Basics。
-14. 最后一问:这张图放回所选 gallery 的对应卡片旁边,是不是同一个模板家族,而不只是“风格有点像”?
-
-## 九、单文件模板骨架
-
-```html
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Mono — {图名}</title>
-<!-- 需要 ECharts 时: -->
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<!-- 需要 Chart.js 时(G1 / G3): -->
-<!-- <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script> -->
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="{MONO.FONT.link}" rel="stylesheet">
-<style>/* 内联 MONO.CARD_CSS */</style>
-</head>
-<body>
-<div class="grid2">
-  <div class="card"><!-- 或 card dark / card wide -->
-    <h2>{结论式标题}</h2>
-    <div class="sub">{说明} · {图例} · {时间范围}</div>
-    <!-- 图容器按引擎三选一: -->
-    <div class="ch" id="ch"></div><!-- ECharts -->
-    <!-- 手写 SVG: <svg id="ch" viewBox="0 0 400 320"></svg> -->
-    <!-- Chart.js(G1/G3): <div class="wrap"><canvas id="ch"></canvas></div>,配 .wrap{position:relative;height:320px} -->
-    <!-- ⚠️ Chart.js 必须挂 <canvas>,套 <div class="ch"> 会报 can't acquire context -->
-    <div class="src">{图型名} · {系列} · {数据来源}</div>
-  </div>
-</div>
-<script>
-// 内联 mono-tokens.js 全文
-// ── 数据(用户只需要改这里)──
-const DATA = [ /* ... */ ];
-// ── 渲染 ──
-MONO.obsReveal('ch', el => { /* ... */ });
-</script>
-</body>
-</html>
-```
-
-## 十、报告模式骨架
-
-```text
-report-catalog.md                # 先按场景选 R01–R12
-templates/reports/report-NN.zh.html  # 中文整页正本
-templates/reports/report-NN.en.html  # English full-page source
-templates/reports/index.html         # 本地可打开的模板索引
-docs/assets/reports/report-NN.png    # 中文版静态预览
-```
-
-报告交付仍然是单文件 HTML;`templates/reports/` 中的文件是可复制的起始骨架,不是需要逐字照抄的内容样例。模板内的演示数据只用于展示结构,生成最终报告时必须全部替换。

+ 0 - 108
service/cmd/mcp/.kiro/skills/lieflat-charts/catalog.md

@@ -1,108 +0,0 @@
-# Lieflat Charts 图型目录 · 64 张
-
-> 每张图挂三个标签:**数据形状**(选图的主键)、**场合**、**读者时间**。
-> **主力与后备:** 主力是 L1–L15 与 F1–F13,默认从这里出图。L16–L20、F14–F17、G19–G22 是后备,只有主力无法诚实编码这份数据时才用,并要写明理由。例外是 F15、F16、F17、L17、L20 这五张——主力里不存在对应编码,命中相应数据形状时直接用(见 `SKILL.md` 第零节 3.1 / 3.2)。
-> **选型优先级以 `SKILL.md` 的硬约束为准:默认先完整审计 Lupi Editorial,再审计 Lupi Basics;只有两组都不适配,或用户明确要求 Glance / dashboard / 三秒快读时,才进入 Glance。**
-> 「姊妹」列 = 同题异构对子,只用于比较数据契约和召回候选,不代表两案都要生成,也不能改变上述优先级。
-> 参考实现在 `templates/`:Glance 系 `templates/glance-gallery.html`,Lupi 系 `templates/lupi-gallery.html`,基础型组 `templates/basics-gallery.html`,地图 `templates/maps-gallery.html`,大图 `templates/big-*.html`。地图只有在用户明确要求地图或地域分布时才召回。gallery 是多卡合页——查某张图的代码,先按下表「卡内标题」找到卡片,再在 `<script>` 里搜同名 `// ════` 注释块。真实数据成品案例在 `examples/`。
-
-## Glance 系 · 22 张(粗笔画 · 提前聚合 · 3 秒读完)
-
-| # | 名字 | 卡内标题 | 数据形状 | 场合 | 读者时间 | 引擎 | 姊妹 |
-|---|------|---------|---------|------|---------|------|------|
-| G1 | Range Capsules | Daily active range | 每天一个区间(min–max)的日序列 | 周报 dashboard | <10s | Chart.js* | L3 Barcode Lollipop |
-| G2 | Petal Rose | How releases made us feel | 单变量分类计数,≤8 类且近似等分 | 门面/封面 | <10s | ECharts(暗卡) | G13 Big Slice(双变量时升级) |
-| G3 | Chunky Bars | Revenue by plan | 少类目排名比较(≤6) | 周报 dashboard | <10s | Chart.js* | L2 Dot Cascade;多选百分比 → L15 Ballot Tally |
-| G4 | Dot Waffle | Where sign-ups come from | 100% 构成(占比) | 通用,饼图的默认替代 | <10s | 手写 SVG | L14 Hundred Field |
-| G5 | Pictorial Bar | Trees planted, year by year | 逐年计数(一个符号=固定数量) | 对外故事页 | <10s | ECharts | — |
-| G6 | Circular Graph(小) | Who works with whom | 网络,≤12 节点 | 快速示意 | <10s | ECharts(暗卡) | 大图 B1 的预览版,节点多用 B1 |
-| G7 | Tree LR | Everything the platform ships | 层级结构(2–3 层) | 产品目录/架构页 | ~30s | ECharts | — |
-| G8 | Rainfall Dual Area | Campaigns rain down | 双序列因果(投入 vs 产出) | 增长复盘 | ~30s | ECharts | — |
-| G9 | Scatter Morph | One dataset, three views | 同一实体集的三个维度轮播 | 短视频/演示 | 动画 | ECharts universalTransition | — |
-| G10 | Diverging Bar | Where we gained, where we bled | 有正负的分类数值 | 周报 dashboard | <10s | ECharts | —(Equalizer、Meridian Dots 已删) |
-| G11 | Force Graph(小) | Integrations, pulled into orbit | 中心+卫星网络,≤15 节点 | 快速示意,可拖 | <10s | ECharts(暗卡) | 大图 B2 的预览版;静态海报版 L6 |
-| G12 | Stagger Wave | Fifty markets, one wave | 多类目分布(30–60 根) | 短视频入场 | 动画 | ECharts | — |
-| G13 | Big Slice (Custom Pie) | Big slice, deep engagement | 双编码:占比(角)×强度(径) | 产品分析 | ~30s | ECharts custom | G2 Petal Rose(单变量时降级) |
-| G14 | Single Axis | Support load, day by day | 星期×小时×量(punch card 数据) | 支持/运维周报 | ~30s | ECharts | —(Punch Card 已删,此为唯一版本) |
-| G15 | Jitter Strip | Response times, spread out | 分组分布,逐条记录(几百点) | SRE/工单分析 | ~30s | ECharts | — |
-| G16 | Bar Race | Eight products race | 排名随时间演变 | 短视频 | 动画 | ECharts realtimeSort | — |
-| G17 | Dynamic Stream | Concurrent users, streaming | 实时滚动序列 | 直播/大屏 | 动画 | ECharts | — |
-| G18 | Draw-in + Counter | H1 revenue, drawn in one stroke | 累计增长(一条线+一个大数) | 短视频/汇报开场 | 动画 | ECharts | —(Release Rings 已删) |
-| G19 | Violin | How fast each plan gets an answer | 分组连续分布的密度轮廓 + 中位数 | dashboard/分析汇报 | <10s | SVG | F15 Tick Box;L19 Ridgeline |
-| G20 | Matrix Heat (Glance) | Adoption runs hot on the new versions | 两个离散维度 × 数值,≤60 格,每格直接读数 | dashboard/产品分析 | <10s | SVG | L16 Matrix Heat(细读版) |
-| G21 | Rank Strip | Flows climbs to the top | 多实体排名随离散时间变化,适合静态印刷 | 汇报/账单复盘 | <10s | SVG | G16 Bar Race(动态演示) |
-| G22 | Aggregate Sankey | Channels pour into plans | 两端聚合流量,带宽=数量,不要求逐路径查询 | 归因/转化分析 | ~30s | SVG | B3 Threads(逐路径查询) |
-
-\* G1/G3 仍使用 Chart.js;后续可迁移到 ECharts,以统一渲染栈。
-
-## Lupi 系 · 20 张(发丝线 · 逐记录 · 30 秒阅读)
-
-| # | 名字 | 卡内标题 | 数据形状 | 场合 | 读者时间 | 引擎 | 姊妹 |
-|---|------|---------|---------|------|---------|------|------|
-| L1 | Launch Fan | Twelve features, fanned out | 多实体各带出生时间+当前规模 | 年报/故事页 | ~30s | SVG | — |
-| L2 | Dot Cascade | What breaks, stacked and ranked | 排名比较,可数单位(unit chart) | 年报/故事页 | ~30s | SVG(暗卡) | G3 Chunky Bars |
-| L3 | Barcode Lollipop | Ninety days as a barcode | 每天一个读数的日序列(90 天级) | 年报/故事页(配文字栏) | ~30s | SVG(通栏) | G1 Range Capsules |
-| L4 | Arc Matrix | Eight products land in twelve cities | 分类×分类+量,小数据(≤100 格) | 轻量矩阵 | ~30s | SVG | L9 Almanac 的轻量版,数据薄用这张 |
-| L5 | Radial Convergence | 48 requests pull toward five themes | 多对一归属,不丢明细(≤60 条) | 海报/封面 | ~30s | SVG | L12 Colonnade(两案供选) |
-| L6 | Cluster Field | The contributor field | 中心+卫星网络,海报版 | 海报/封面(认形不查数) | ~30s | SVG(通栏) | B2 force-big(要交互/查数用 B2) |
-| L7 | Brand Spectrum | Where the brand sits | 双极量表(两端都是合法位置)+ 竞品对照 | 品牌调研报告 | ~30s | SVG | —(Equalizer 已删,唯一保留的量表图) |
-| L8 | Dotty Matrix | Four squads, stacked in space | 多组×网格×量,等距堆叠 | 封面/海报(装饰强) | ~30s | SVG | G14 摊平版更易读,要读数用 G14 |
-| L9 | Bubble Almanac | Eight years of tickets, one almanac | 分类×年份+量+状态,大跨度(手绘 blob) | 年报(带旁注/大事记) | >30s | SVG(通栏) | L4 Arc Matrix(轻量版) |
-| L10 | Radial Patchwork | A quarter of deploys, overlaid | 逐事件叠加:时刻(角)×规模(径),透明度=密度 | 年报/故事页 | >30s | SVG | —(Polar Line 已删,此为唯一 24h 分布图) |
-| L11 | Trend Lineage | Features rise, fall, come back | 事件序列生命史(首发/重做/休眠/存活) | 产品复盘 | >30s | SVG | — |
-| L12 | Type Colonnade | Forty-four repos, ten owners | 多对一归属+逐条名单(≤50 条) | 治理/审计报告 | ~30s | SVG | L5 Convergence(两案供选) |
-| L13 | Hourglass Stream | The funnel, poured | 分阶段递减人数(漏斗) | 年报/故事页 | ~30s | SVG | — |
-| L14 | Hundred Field | A hundred of us, four minds | 100% 构成(占比),≤6 类小数据 | 年报/故事页 | ~30s | SVG | G4 Dot Waffle |
-| L15 | Ballot Tally | What they fear, tick by tick | 多选题百分比(各项独立 0–100),≤6 项 | 年报/故事页 | ~30s | SVG | G3 Chunky Bars |
-| L16 | Matrix Heat | Which features get used together | 两个离散维度 × 数值,≤100 格,保留矩阵结构与重点格 | 年报/产品分析 | ~30s | SVG | G20 Matrix Heat(快读版) |
-| L17 | Calendar Heat | A year of deploys, day by day | 一整年日期 × 数量,52 周 × 7 天 | 年报/运营复盘 | ~30s | SVG(通栏) | F10 Dot Heat(星期×小时) |
-| L18 | Beeswarm | A hundred and twenty deals, swarming | 单变量逐条记录的堆积分布,约 40–180 点 | 销售/研究附图 | ~30s | SVG | G15 Jitter Strip;G19 Violin |
-| L19 | Ridgeline | Five pipelines, five tempos | 3–8 组连续分布的密度形状比较 | 年报/研究报告 | >30s | SVG | G19 Violin(少组快读) |
-| L20 | Parallel Coordinates | Twelve products, four dimensions | 同一实体集跨 3–6 个连续维度,一线一实体 | 产品组合/研究报告 | >30s | SVG | G9 Scatter Morph(演示轮播) |
-
-> L14–L15 是**小数据组**:数据只有几个百分比时靠单位分解拿回 Lupi 密度——1 点 = 1 人 / 1 百分点,密度来自单位而不是记录数。单位含义必须写进副标题(如 "one dot = one person in a hundred"),且只摊诚实单位,不编造个体。
-
-## 基础型组 · 17 张(F1–F17 · Lupi 语法 × 基础图型剪影,为稀疏数据而设)
-
-远看认得出基础图型(柱/折线/环形…),近看每个单位可数。数据只有几个类目或几十天时的 Lupi 首选——先来这里找,找不到再走库外翻译。参考实现 `templates/basics-gallery.html`。
-
-| # | 名字 | 卡内标题 | 数据形状 | 场合 | 读者时间 | 引擎 | 姊妹 |
-|---|------|---------|---------|------|---------|------|------|
-| F1 | Rung Bars | Revenue by plan, rung by rung | 少类目比较(≤8),单位可数 | 年报/故事页 | ~30s | SVG | G3 Chunky Bars |
-| F2 | Hairline Line | Thirty days of sign-ups | 日序列(≤30 天,逐日读数) | 年报/故事页 | ~30s | SVG | G1 Range Capsules |
-| F3 | Hairline Area | Concurrent users, filled with days | 日序列(30–60 天,看形态) | 年报/故事页 | ~30s | SVG | L3 Barcode Lollipop |
-| F4 | Tick Donut | Where the traffic comes from | 100% 构成(≤6 段) | 年报/故事页 | ~30s | SVG | G4 Dot Waffle ⇄ L14 Hundred Field |
-| F5 | Tick Rows | Six teams, shipped and counted | 横向排名比较,单位可数(≤8 行) | 年报/故事页 | ~30s | SVG | L2 Dot Cascade |
-| F6 | Paired Rungs | This year against last, plan by plan | 分组对比(每类 2 系列,如今昔) | 年报/复盘 | ~30s | SVG | — |
-| F7 | Stacked Rungs | Where each region's revenue sits | 堆叠构成(≤4 类 × ≤3 段) | 年报/复盘 | ~30s | SVG | — |
-| F8 | Plumb Scatter | Price against satisfaction | 二维散点(≤20 点) | 产品分析 | ~30s | SVG | G15 Jitter Strip(分布用) |
-| F9 | Rung Waterfall | From gross to net, step by step | 瀑布 / 增减分解(≤6 级) | 财务/复盘 | ~30s | SVG | — |
-| F10 | Dot Heat | When support gets loud | 星期×小时×量(小热力) | 支持/运维 | ~30s | SVG | G14 Single Axis |
-| F11 | Tick Gauge | How far to the quarter's goal | 单值进度(0–100%) | 汇报开场 | <10s | SVG | G18 Draw-in + Counter |
-| F12 | Dumbbell Queue | Onboarding, before and after | 类目级前后对比(≤6 类,串珠=真单位) | 年报/复盘 | ~30s | SVG | —(两点趋势的 Lupi 位,替代已删 Slope Beads) |
-| F13 | Nested Treemap | Where the work went | 两层层级 + 正数权重,矩形面积=数值 | 预算/产品组合/空间占用 | ~30s | ECharts(SVG) | G7 Tree LR(只看从属关系、不看份额时) |
-| F14 | Rung Histogram | Most tickets resolve within six hours | 单变量分箱频数,bin 有业务含义且单位可数 | 支持/运营分析 | ~30s | SVG | G19 Violin;F15 Tick Box |
-| F15 | Tick Box | Reply times, boxed by plan | 分组五数概括 + 异常值,原始分布可被汇总 | 支持/实验分析 | ~30s | SVG | G19 Violin;F14 Histogram |
-| F16 | Stream Ribbon | Three products trade the same river | 2–5 个系列的构成随连续时间变化,同时看总量 | 产品/流量复盘 | ~30s | SVG(通栏) | F7 Stacked Rungs(静态类目) |
-| F17 | Candlestick | Six weeks of the token, candle by candle | OHLC 四值时间序列,空心=涨、实心=跌 | 行情/价格复盘 | ~30s | SVG | G1 Range Capsules(只有区间时) |
-
-## 地图 · 2 张(仅在用户明确要求地图时召回)
-
-数据里出现国家、州省或地区字段,**不等于自动使用地图**。只有用户明确提出“地图”“地域分布”“按国家/州着色”等要求时,才从 `templates/maps-gallery.html` 选择。地图通过 ECharts 与在线 GeoJSON 渲染,需要联网。
-
-| # | 名字 | 卡内标题 | 数据形状 | 场合 | 读者时间 | 引擎 | 限制 |
-|---|------|---------|---------|------|---------|------|------|
-| M1 | US Choropleth | Sign-ups across the states | 美国州级区域 + 非负数值,明度=数值 | 美国市场/运营报告 | ~30s | ECharts + GeoJSON | 仅用户明确要求;州面积不表示数值 |
-| M2 | World Choropleth | Where the users are | 世界国家级区域 + 非负数值,明度=数值 | 全球市场/运营报告 | ~30s | ECharts + GeoJSON | 仅用户明确要求;不用于点位或路径 |
-
-## 独立交互大图 · 3 张(一图一文件,整页幅面)
-
-| # | 文件 | 数据形状 | 交互 | 什么时候用 |
-|---|------|---------|------|-----------|
-| B1 | `templates/big-circular.html` | 网络 60 节点环形弦膜 | hover 聚焦邻接,点击重播 | 节点 >15 的关系数据 |
-| B2 | `templates/big-force.html` | 网络 180 节点力导向星系 | 拖拽回弹、hover 聚焦 | 大网络要逐节点查 |
-| B3 | `templates/big-threads.html` | 三段路径 100+ 条丝线 | hover 单线/整束,点击钉住,状态栏读数 | 多段流向要逐路径查 |
-
-## 已移除图型(已有更合适的替代)
-
-Polar Line(24h 分布 → L10)、Punch Card(→ G14)、Release Rings(风格不属于任何一系)、静态 Thread Triptych(→ B3)、Profile Equalizer(→ G10)、Slope Beads(交叉斜线的可读性不足,斜率图仍待重新设计)、Meridian Dots(正负分类改用 G10)。后两张源自未采用的 Lenny 案例草稿,不建议按原样恢复。

+ 0 - 177
service/cmd/mcp/.kiro/skills/lieflat-charts/mono-tokens.js

@@ -1,177 +0,0 @@
-/* ═══════════════════════════════════════════════════════════════
-   MONO TOKENS — 风格的唯一正本
-   所有 mono 图表(Chart.js / ECharts / 手写 SVG)共享这一份。
-   任何文件里出现与本文件冲突的颜色、字体、动画参数,以本文件为准。
-   用法:<script src="mono-tokens.js"></script>,全部挂在 window.MONO 上;
-   也可以直接把本文件内容内联进单文件 HTML(开源分发时推荐内联)。
-   ═══════════════════════════════════════════════════════════════ */
-(function (global) {
-  'use strict';
-
-  /* ── 1 · 色板 ──────────────────────────────────────────────
-     纸灰底 + 炭黑墨。没有彩色。明度即数据:最重要 = 最黑。   */
-  const INK   = '#1C1C1A';   // 墨:主数据、标题、强调
-  const PAPER = '#F0EFEB';   // 纸:页面底色 = 浅卡底色(卡片无边框,靠留白分卡)
-  const MUTED = '#8F8E88';   // 次级文字、副标题
-  const FAINT = '#C6C5BF';   // 来源行、辅助刻度
-  const GRID  = '#DEDDD6';   // 网格线、发丝线
-
-  // 7 级灰阶 ladder:多系列时按重要性从黑到浅分配
-  const L   = ['#1C1C1A', '#4A4944', '#6A6963', '#8F8E88', '#B0AFA9', '#C6C5BF', '#D8D7D1'];
-  // 5 级简版(waffle 等少系列场景)
-  const LAD = ['#1C1C1A', '#4A4944', '#8F8E88', '#B0AFA9', '#D8D7D1'];
-
-  // 暗卡(dark card)专用:底 #1C1C1A,其上的"墨"反转为纸色
-  const DARK = {
-    bg: '#1C1C1A',
-    ink: '#F0EFEB',          // 暗卡上的主数据色
-    muted: '#8F8E88',
-    faint: '#55554F',        // 暗卡来源行
-    grid: '#2E2D29',         // 暗卡网格 / 发丝线
-    gridSoft: '#2A2925',
-    ladder: ['#F0EFEB', '#DCDAD2', '#C9C7BD', '#B3B0A4', '#8F8E88', '#6A6963', '#4A4944'],
-  };
-
-  /* ── 2 · 字体 ────────────────────────────────────────────── */
-  const FONT = {
-    family: 'Inter',
-    // Google Fonts 引入行(放 <head>):
-    link: 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap',
-    title:    { size: 16.5, weight: 700, spacing: '-.02em' },  // 卡内 h2
-    titleBig: { size: 19,   weight: 700, spacing: '-.02em' },  // 独立大图 h2
-    sub:      { size: 11.5, weight: 400 },                     // 副标题(图例说明写在这里)
-    src:      { size: 9.5,  weight: 500, spacing: '.08em' },   // 来源行,全大写
-    value:    { weight: 800 },                                 // 图内数值一律 800
-    axis:     { size: 9.5,  weight: 600 },                     // 轴标签
-    // SVG 内最小字号下限:半宽卡 6.5px,通栏/大图 5.5px。低于下限改 hover 出。
-    minHalf: 6.5, minWide: 5.5,
-  };
-
-  /* ── 3 · 形状 ────────────────────────────────────────────── */
-  const SHAPE = {
-    cardRadius: 24,          // 卡片圆角
-    cardPad: '28px 28px 20px',
-    barRadius: 99,           // 柱端胶囊圆角(竖柱只圆上端,横柱只圆外端)
-    tooltipRadius: 12,
-  };
-
-  /* ── 4 · 动画性格 ────────────────────────────────────────
-     快进快停,quarticOut / cubicOut,不弹跳(elasticOut 只给波浪入场)。 */
-  const MOTION = {
-    enter: 900,              // 常规入场 ms
-    enterSlow: 1200,         // 大元素 / 关系图
-    easing: 'quarticOut',
-    staggerDot: 12,          // 点阵逐个延迟 ms(8–15 区间)
-    staggerBar: 100,         // 条形逐根延迟 ms(80–130 区间)
-    // CSS 动画类(手写 SVG 用):pop 缩放入场 / fade 淡入 / draw 描线
-    css: `
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }`,
-  };
-
-  /* ── 5 · Tooltip ─────────────────────────────────────────
-     浅卡用黑底纸字,暗卡用纸底黑字。ECharts 直接展开这两个对象。 */
-  const tipLight = { backgroundColor: INK, borderWidth: 0, padding: [10, 14],
-    textStyle: { color: PAPER, fontFamily: 'Inter', fontSize: 12 } };
-  const tipDark  = { backgroundColor: PAPER, borderWidth: 0, padding: [10, 14],
-    textStyle: { color: INK, fontFamily: 'Inter', fontSize: 12 } };
-
-  /* ── 6 · 确定性伪随机 ────────────────────────────────────
-     演示数据一律用它,不用 Math.random()——刷新必须长一样,
-     否则截图 / 录屏 / 回归对比全部失效。                       */
-  const rnd = (i, k) => Math.abs(((i * 73856093) ^ (k * 19349663)) % 1000) / 1000;
-
-  /* ── 7 · 几何 ────────────────────────────────────────────── */
-  const D2R = Math.PI / 180;
-  const pol = (cx, cy, r, deg) => [cx + r * Math.cos(deg * D2R), cy + r * Math.sin(deg * D2R)];
-  // 环形扇区 path(cornerRadius 自己在调用处用 ECharts sector 或手动倒角)
-  const sect = (cx, cy, r0, r1, a0, a1) => {
-    const big = a1 - a0 > 180 ? 1 : 0;
-    const [xa, ya] = pol(cx, cy, r1, a0), [xb, yb] = pol(cx, cy, r1, a1);
-    const [xc, yc] = pol(cx, cy, r0, a1), [xd, yd] = pol(cx, cy, r0, a0);
-    return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
-  };
-  // 手绘感圆(editorial 系气泡用):圆周叠两个慢波 + 噪声,seed 定形
-  const blob = (x, y, r, seed) => {
-    const n = Math.max(14, Math.round(r * 1.6)), pts = [];
-    for (let t = 0; t < n; t++) {
-      const a = t / n * Math.PI * 2;
-      const w = 1 + .055 * Math.sin(a * 2 + seed * 7) + .04 * Math.sin(a * 3 + seed * 13)
-              + (rnd(seed + t, 3) - .5) * .03;
-      pts.push([x + Math.cos(a) * r * w, y + Math.sin(a) * r * w]);
-    }
-    let d = `M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for (let t = 0; t < n; t++) {
-      const p = pts[t], q = pts[(t + 1) % n];
-      d += ` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d + ' Z';
-  };
-
-  /* ── 8 · SVG 快捷 ────────────────────────────────────────── */
-  const NS = 'http://www.w3.org/2000/svg';
-  const el  = (p, t, a) => { const n = document.createElementNS(NS, t);
-    for (const k in a) n.setAttribute(k, a[k]); p.appendChild(n); return n; };
-  const txt = (p, a, s) => { const n = el(p, 'text', a); n.textContent = s; return n; };
-  const tip = (n, s) => { const t = document.createElementNS(NS, 'title');
-    t.textContent = s; n.appendChild(t); };
-
-  /* ── 9 · 统一 reveal:滚入视野才播,点击重播 ──────────────
-     带 timer 登记(keep),重播前清干净,防动画叠加。          */
-  const timers = {};
-  const keep = (id, t) => { (timers[id] = timers[id] || []).push(t); };
-  const obsReveal = (id, fn) => {
-    const n = document.getElementById(id);
-    const go = () => {
-      (timers[id] || []).forEach(clearInterval); timers[id] = [];
-      if (n.tagName === 'svg' || n.tagName === 'SVG') n.innerHTML = '';
-      fn(n);
-    };
-    const io = new IntersectionObserver(es => {
-      if (es[0].isIntersecting) { go(); io.disconnect(); }
-    }, { threshold: .3 });
-    io.observe(n);
-    n.style.cursor = 'pointer';
-    n.addEventListener('click', go);
-  };
-  // ECharts 版 reveal
-  const eReveal = (id, opt) => obsReveal(id, elDom => {
-    const g = echarts.getInstanceByDom(elDom) || echarts.init(elDom);
-    g.clear(); g.setOption(opt);
-  });
-
-  /* ── 10 · 卡片骨架(生成新图时照抄这个结构) ──────────────
-     <div class="card [dark] [wide]">
-       <h2>结论式标题</h2>
-       <div class="sub">副标题 · 图例说明 · 时间范围</div>
-       <div class="ch" id="xx"></div>  或  <svg id="xx" viewBox="0 0 400 320">
-       <div class="src">图型名 · 系列名 · 数据来源(全大写)</div>
-     </div>                                                        */
-  const CARD_CSS = `
-  :root{--bg:${PAPER};--dark:${DARK.bg};--ink:${INK};--muted:${MUTED};--faint:${FAINT};--grid:${GRID}}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:${SHAPE.cardRadius}px;padding:${SHAPE.cardPad}}
-  .card.dark{background:var(--dark);color:${PAPER}}
-  .card.dark .sub{color:${MUTED}}
-  .card.dark .src{color:${DARK.faint}}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:${FONT.title.weight};font-size:${FONT.title.size}px;letter-spacing:${FONT.title.spacing};margin-bottom:3px}
-  .sub{font-size:${FONT.sub.size}px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:${FONT.src.size}px;color:var(--faint);margin-top:10px;letter-spacing:${FONT.src.spacing};font-weight:${FONT.src.weight}}
-  .ch{height:320px}
-  svg text{font-family:'Inter',sans-serif}` + MOTION.css;
-
-  global.MONO = { INK, PAPER, MUTED, FAINT, GRID, L, LAD, DARK,
-    FONT, SHAPE, MOTION, tipLight, tipDark,
-    rnd, pol, sect, blob, el, txt, tip, obsReveal, eReveal, keep, CARD_CSS };
-})(typeof window !== 'undefined' ? window : globalThis);

+ 0 - 943
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/basics-gallery.html

@@ -1,943 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Charts — Basics Gallery · 17 张基础型参考实现</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<style>
-  :root{--bg:#F0EFEB;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #B0AFA9;color:#8F8E88}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  .chartbox{width:100%;height:360px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-
-  .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>基础型组(F1–F17)· Lupi 语法 × 基础图型剪影</h1>
-  <p>柱 / 折线 / 面积 / 环形 / 横条 / 分组 / 堆叠 / 散点 / 瀑布 / 热力 / 进度 / 哑铃 / 矩形树图。远看认得出图型剪影,近看每个单位可数(1 档 = $1k · 1 tick = 1 人 / 1%)。为稀疏聚合数据而设:数据不用多,密度来自单位分解。代码检索方式同 lupi-gallery:按 <code>// ════ 图型名 ════</code> 注释块。</p>
-</div>
-<div class="grid2">
-
-  <!-- B1 · rung bars -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 柱状</span>
-    <h2>Revenue by plan, rung by rung</h2>
-    <div class="sub">one rung = one $k of MRR · the bar is a ladder you can count</div>
-    <svg id="rungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG BARS · MONO-BASIC · BILLING</div>
-  </div>
-
-  <!-- B2 · hairline line -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 折线</span>
-    <h2>Thirty days of sign-ups</h2>
-    <div class="sub">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest</div>
-    <svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE LINE · MONO-BASIC · GROWTH</div>
-  </div>
-
-  <!-- B3 · hairline area -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 面积</span>
-    <h2>Concurrent users, filled with days</h2>
-    <div class="sub">one hairline = one day, floor to peak · the area is made of days, not paint</div>
-    <svg id="hairarea" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE AREA · MONO-BASIC · LIVE OPS</div>
-  </div>
-
-  <!-- B4 · tick donut -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 环形</span>
-    <h2>Where the traffic comes from</h2>
-    <div class="sub">one tick = one percent · four sources shade the dial · read it like a clock</div>
-    <svg id="tickdonut" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK DONUT · MONO-BASIC · ACQUISITION</div>
-  </div>
-
-
-
-  <!-- C1 · tick rows (hbar) -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 横向条形</span>
-    <h2>Six teams, shipped and counted</h2>
-    <div class="sub">one tick = one release this quarter · the row is a queue, not a bar</div>
-    <svg id="tickrows" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK ROWS · MONO-BASIC · RELEASE LOG</div>
-  </div>
-
-  <!-- C2 · paired rungs (grouped bar) -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 分组柱</span>
-    <h2>This year against last, plan by plan</h2>
-    <div class="sub">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k</div>
-    <svg id="pairrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PAIRED RUNGS · MONO-BASIC · BILLING YoY</div>
-  </div>
-
-  <!-- C3 · stacked rungs -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 堆叠柱</span>
-    <h2>Where each region's revenue sits</h2>
-    <div class="sub">one rung = $1k · three products stack up the ladder, darkest carries the most</div>
-    <svg id="stackrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">STACKED RUNGS · MONO-BASIC · REVENUE MIX</div>
-  </div>
-
-  <!-- C4 · plumb scatter -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 散点</span>
-    <h2>Price against satisfaction, twelve products</h2>
-    <div class="sub">every dot hangs a plumb line to the floor · read x by the line, y by the height</div>
-    <svg id="plumb" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PLUMB SCATTER · MONO-BASIC · PRODUCT PANEL</div>
-  </div>
-
-  <!-- C5 · rung waterfall -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 瀑布</span>
-    <h2>From gross to net, step by step</h2>
-    <div class="sub">one rung = $1k · solid rungs add, hollow rungs take away · the ladder walks down</div>
-    <svg id="fall" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG WATERFALL · MONO-BASIC · P&L</div>
-  </div>
-
-  <!-- C6 · dot heat -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 热力</span>
-    <h2>When support gets loud</h2>
-    <div class="sub">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible</div>
-    <svg id="dotheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT HEAT · MONO-BASIC · SUPPORT DESK</div>
-  </div>
-
-  <!-- C7 · tick gauge -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 进度</span>
-    <h2>How far to the quarter's goal</h2>
-    <div class="sub">one tick = one percent of target · inked = earned · the dial reads left to right</div>
-    <svg id="gauge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK GAUGE · MONO-BASIC · QUARTERLY TARGET</div>
-  </div>
-
-  <!-- C8 · dumbbell queue -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 哑铃对比</span>
-    <h2>Onboarding, before and after the redesign</h2>
-    <div class="sub">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable</div>
-    <svg id="dumbbell" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DUMBBELL QUEUE · MONO-BASIC · UX TIMING</div>
-  </div>
-
-  <!-- C9 · nested treemap -->
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · 矩形树图</span>
-    <h2>Where the work went</h2>
-    <div class="sub">1,280 shipped hours · area = effort · parent bands preserve the product hierarchy</div>
-    <div id="treemap" class="chartbox" role="img" aria-label="Treemap of shipped hours nested by product family"></div>
-    <div class="src">NESTED TREEMAP · MONO-BASIC · WORK PORTFOLIO</div>
-  </div>
-
-
-  <!-- F14 · rung histogram -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 直方图</span>
-    <h2>Most tickets resolve within six hours</h2>
-    <div class="sub">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile</div>
-    <svg id="histo" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG HISTOGRAM · MONO-BASIC · SUPPORT DESK</div>
-  </div>
-
-  <!-- F15 · tick box -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 箱线图</span>
-    <h2>Reply times, boxed by plan</h2>
-    <div class="sub">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed</div>
-    <svg id="boxplot" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK BOX · MONO-BASIC · SUPPORT DESK</div>
-  </div>
-
-  <!-- F16 · stream ribbon -->
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · STREAMGRAPH</span>
-    <h2>Three products trade the same river</h2>
-    <div class="sub">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows</div>
-    <svg id="stream" viewBox="0 0 840 300" preserveAspectRatio="xMidYMid meet" style="max-height:300px"></svg>
-    <div class="src">STREAM RIBBON · MONO-BASIC · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- F17 · candlestick -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · K 线</span>
-    <h2>Six weeks of the token, candle by candle</h2>
-    <div class="sub">body = open to close · wick = the day's full reach · ink = closed down · hollow = closed up</div>
-    <svg id="candle" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">CANDLESTICK · MONO-BASIC · MARKET DATA</div>
-  </div>
-
-</div>
-<script>
-// ── shared Mono tokens(与 lupi-gallery 同源) ──
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ B1 · rung bars ════
-// 柱状图的 Lupi 化:柱身 = 一格格横档,1 档 = 1 个诚实单位。
-// 远看是柱状图剪影,近看每一档都数得出来。档长、透明度 rnd 抖动。
-(()=>{
-const D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];
-obsReveal('rungs',s=>{
-  const x0=i=>56+i*56,base=266,step=5.6,HW=14;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i);
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1,
-        opacity:.5+rnd(k+2,i+4)*.5,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:'#C6C5BF',
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:INK,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — $${v}k MRR`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ B2 · hairline line ════
-// 折线图的 Lupi 化:barcode 的日历地板(每天一根发丝,无论有没有事)+
-// 发丝折线 + 逐日圆点(周末空心)。峰值两个点放大标数。
-(()=>{
-const N=30;
-const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
-obsReveal('dayline',s=>{
-  const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
-  // calendar floor
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  // top-2 peaks, apart
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  // hairline path
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:INK,'stroke-width':1,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===5||d%7===6,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
-      fill:weekend?PAPER:INK,stroke:INK,'stroke-width':weekend?1:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
-});
-})();
-
-// ════ B3 · hairline area ════
-// 面积图的 Lupi 化:填充不是色块,是一天一根发丝立到自己的峰值。
-// 面积由日子组成。顶边一根细线收轮廓,峰值日加点标数。
-(()=>{
-const N=45;
-const val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;
-obsReveal('hairarea',s=>{
-  const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;
-  el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const peak=vs.indexOf(Math.max(...vs));
-  vs.forEach((v,d)=>{
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),
-      stroke:d===peak?INK:'#8F8E88','stroke-width':d===peak?1.1:.55,
-      opacity:d===peak?1:.5+rnd(d+1,7)*.45,
-      class:'fade',style:`animation-delay:${d*.014}s`});
-  });
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:INK,'stroke-width':1.2,pathLength:1,
-    class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});
-  const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:INK,
-    class:'pop',style:'animation-delay:1.2s'});
-  tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);
-  txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1.3s`,
-    class:'fade'},Math.round(vs[peak])+'k');
-  [[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
-    'ONE HAIRLINE = ONE DAY, FLOOR TO PEAK');
-});
-})();
-
-// ════ B4 · tick donut ════
-// 环形图的 Lupi 化:ballot tally 的百人队列绕成整圈表盘,1 tick = 1%,
-// 四段来源用灰阶 ladder 分段,段间留一格呼吸。中心只放总数和单位说明。
-(()=>{
-const D=[['ORGANIC',37,INK],['PAID',28,'#55554F'],['REFERRAL',21,'#8F8E88'],['SOCIAL',14,'#B0AFA9']];
-obsReveal('tickdonut',s=>{
-  const cx=200,cy=148,R0=64;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=10+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:'#C6C5BF',
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    // segment label at mid-angle, outside, tied by a dotted hairline
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:'#C6C5BF','stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of traffic`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:INK,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
-  txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');
-});
-})();
-
-// ════ C1 · tick rows ════
-// 横向条形:一行 = 一支队伍的发布队列,1 tick = 1 次发布。
-// tick 高度/透明度抖动,每 5 根一个点标,行尾大数。
-(()=>{
-const D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];
-obsReveal('tickrows',s=>{
-  const y0=i=>52+i*44,X0=104,PX=6.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i);
-    txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:INK,'stroke-width':.9,
-        opacity:.55+rnd(k+3,i+5)*.45,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:'#C6C5BF',
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name} — ${v} releases`);
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = ONE RELEASE · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ C2 · paired rungs ════
-// 分组柱:每类两把梯子并肩,今年实档、去年淡档。差值一眼可数。
-(()=>{
-const D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];
-obsReveal('pairrungs',s=>{
-  const x0=i=>64+i*66,base=258,step=5.4,HW=10;
-  D.forEach(([name,was,now],i)=>{
-    const xa=x0(i)-13,xb=x0(i)+13;
-    for(let k=0;k<was;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;
-      el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:'#B0AFA9','stroke-width':1,
-        opacity:.5+rnd(k+2,i+3)*.4,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-    }
-    for(let k=0;k<now;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;
-      el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:INK,'stroke-width':1,
-        opacity:.6+rnd(k+2,i+8)*.4,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});
-    }
-    const topB=base-(now-1)*step;
-    const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},now);
-    tip(num,`${name} — $${was}k → $${now}k`);
-    txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:'#B0AFA9','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},was);
-    txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'FAINT = 2025 · INK = 2026 · ONE RUNG = $1K');
-});
-})();
-
-// ════ C3 · stacked rungs ════
-// 堆叠柱:同一把梯子分三段灰阶,段间空一格呼吸,段值贴段中。
-(()=>{
-const D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];
-const SHADE=[INK,'#8F8E88','#C0BFB8'];
-const SEG=['CORE','ADD-ONS','SERVICES'];
-obsReveal('stackrungs',s=>{
-  const x0=i=>72+i*76,base=262,step=5.2,HW=13;
-  D.forEach(([name,segs],i)=>{
-    const x=x0(i);let k0=0;
-    segs.forEach((v,si)=>{
-      for(let k=0;k<v;k++){
-        const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;
-        el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:SHADE[si],'stroke-width':1,
-          opacity:.6+rnd(k+2,i+si+4)*.4,class:'fade',
-          style:`animation-delay:${i*.09+(k0+k)*.012}s`});
-      }
-      const midY=base-(k0+v/2+si)*step;
-      const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:SHADE[si]===`#C0BFB8`?'#8F8E88':SHADE[si],
-        class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);
-      tip(lab,`${name} ${SEG[si]} — $${v}k`);
-      k0+=v;
-    });
-    const total=segs[0]+segs[1]+segs[2];
-    txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.6+i*.09}s`},total);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.09}s`},name);
-  });
-  el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'DARKEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');
-});
-})();
-
-// ════ C4 · plumb scatter ════
-// 散点:每个点垂一根发丝铅垂线到地板,x 读线脚、y 读高度。
-// 地板是 barcode 刻度。最好/最差两个点标名。
-(()=>{
-const P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],
-         ['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],
-         ['Hub',76,32],['Forms',34,38]];
-obsReveal('plumb',s=>{
-  const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;
-  // barcode floor
-  for(let g=0;g<=20;g++){
-    const x=X0+g/20*(X1-X0);
-    el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:`animation-delay:${g*.01}s`});
-  }
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:'#C6C5BF',class:'fade'},'CHEAP');
-  txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'end',
-    class:'fade'},'PREMIUM');
-  const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]<a[2]?b:a);
-  P.forEach(([name,px,sat],i)=>{
-    const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];
-    el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:'#B0AFA9','stroke-width':.55,
-      opacity:.6,class:'fade',style:`animation-delay:${.2+i*.05}s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:hero?4.6:2.6,fill:hero?INK:'#55554F',
-      class:'pop',style:`animation-delay:${.25+i*.05}s`});
-    tip(dot,`${name} — price ${px} · satisfaction ${sat}`);
-    if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.8}s`,
-      class:'fade'},`${name} · ${sat}`);
-  });
-  txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:'#C6C5BF',
-    transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');
-});
-})();
-
-// ════ C5 · rung waterfall ════
-// 瀑布:从毛收入走到净利,每级一把小梯子。加项实档、减项空心档(stroke 描边格)。
-// 层与层之间虚线台阶承接。
-(()=>{
-const D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];
-obsReveal('fall',s=>{
-  const x0=i=>58+i*72,step=5.2,HW=11;
-  let run=0;
-  const levels=[];
-  D.forEach(([name,v],i)=>{levels.push([name,v,run]);if(v!==17||name!=='NET')run+=(name==='GROSS'?v:(name==='NET'?0:v))});
-  // recompute cleanly: running level after each step
-  let lv=0;const rows=[];
-  D.forEach(([name,v],i)=>{
-    if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}
-    else if(name==='NET'){rows.push([name,lv,0,lv])}
-    else{rows.push([name,v,lv+v,lv]);lv=lv+v}
-  });
-  const base=252,yOf=k=>base-k*step;
-  rows.forEach(([name,v,lo,hi],i)=>{
-    const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v<0&&!isTotal;
-    const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);
-    for(let k=0;k<n;k++){
-      const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;
-      if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:'#8F8E88','stroke-width':1,
-        'stroke-dasharray':'2.5 2.5',opacity:.7,class:'fade',
-        style:`animation-delay:${i*.12+k*.014}s`});
-      else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1,
-        opacity:.6+rnd(k+2,i+4)*.4,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});
-    }
-    // step hand-off: dashed hairline to the next column's start level
-    if(i<rows.length-1){
-      const nx=x0(i+1),yl=yOf(isTotal?v:hi- (neg?0:0));
-      const lvl=isTotal?v:(neg?lo:hi);
-      el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:'#C6C5BF',
-        'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',
-        style:`animation-delay:${.3+i*.12}s`});
-    }
-    const topY=yOf(Math.max(from,to));
-    const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-      fill:neg?'#8F8E88':INK,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));
-    tip(num,`${name} — ${neg?'−':''}$${Math.abs(v)}k`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'SOLID RUNGS ADD · DASHED RUNGS TAKE AWAY · ONE RUNG = $1K');
-});
-})();
-
-// ════ C6 · dot heat ════
-// 热力图:punch card 的 Lupi 皮。7×12 网格,点面积 = 工单量,
-// 静默格留一粒极小点(沉默可见)。最热格标数。
-(()=>{
-const DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-obsReveal('dotheat',s=>{
-  const x0=j=>64+j*27,y0=i=>58+i*30;
-  let max=0,mi=0,mj=0;
-  const v=(i,j)=>{
-    const day=i<5?1:.32;                       // weekends quiet
-    const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5); // two peaks
-    const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);
-    return Math.round(raw);
-  };
-  for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  for(let i=0;i<7;i++){
-    txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
-    for(let j=0;j<12;j++){
-      const t=v(i,j),x=x0(j),y=y0(i);
-      if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:'#D8D6CE',
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
-      const hero=i===mi&&j===mj;
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
-        fill:t>max*.66?INK:t>max*.33?'#6A6963':'#B0AFA9',
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
-      tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);
-      if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
-        stroke:INK,'stroke-width':1,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:1s'});
-    }
-  }
-  for(let j=0;j<12;j+=2)
-    txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);
-});
-})();
-
-// ════ C7 · tick gauge ════
-// 进度:ballot tick 弯成 210° 表盘,1 tick = 1% 目标。走到 73 上墨收笔。
-// 里程碑 25/50/75/100 点标 + 小字。中心大数 + 剩余量小字。
-(()=>{
-const GOAL=73;
-obsReveal('gauge',s=>{
-  const cx=200,cy=190,R0=104,A0=-195,SW=210; // -195° → +15°
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?13+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?INK:'#CFCEC7','stroke-width':inked?1:.6,
-      class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);
-    el(s,'circle',{cx:dx,cy:dy,r:1,fill:'#B0AFA9',class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  // inked tip bead
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:2.4,fill:INK,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:INK,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},GOAL+'%');
-  tip(num,`$730k of the $1M quarter target`);
-  txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE TICK = 1% OF TARGET · INKED = EARNED');
-});
-})();
-
-// ════ C8 · dumbbell queue ════
-// 哑铃对比:空心点=改版前,实心点=改版后,两点之间的轨道上串珠,
-// 一珠 = 省下的一分钟,可数。全部向左走(变快),没有交叉没有 X。
-(()=>{
-const D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];
-obsReveal('dumbbell',s=>{
-  const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);
-  D.forEach(([name,was,now],i)=>{
-    const y=y0(i),xa=mapX(was),xb=mapX(now);
-    txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:'#E3E2DB','stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    // beads: one per minute saved, strung between the two dots
-    const n=was-now;
-    for(let k=0;k<n;k++){
-      const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;
-      el(s,'circle',{cx:x,cy:yy,r:1.5+rnd(k+2,i+4)*.9,fill:'#8F8E88',opacity:.85,
-        class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});
-    }
-    el(s,'circle',{cx:xa,cy:y,r:4.2,fill:PAPER,stroke:INK,'stroke-width':1.3,
-      class:'pop',style:`animation-delay:${.2+i*.08}s`});
-    const after=el(s,'circle',{cx:xb,cy:y,r:4.6,fill:INK,
-      class:'pop',style:`animation-delay:${.6+i*.08}s`});
-    tip(after,`${name} — ${was} min → ${now} min`);
-    txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:'#B0AFA9',
-      class:'fade',style:`animation-delay:${.3+i*.08}s`},was);
-    txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:INK,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${.7+i*.08}s`},now);
-  });
-  txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:'#C6C5BF',class:'fade'},'FASTER ←');
-  txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'end',
-    class:'fade'},'MINUTES');
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · INK = AFTER');
-});
-})();
-
-// ════ C9 · nested treemap ════
-// 面积只编码工时;Mono 不用随机灰阶区分类目,父级靠标题带和组间留白分开。
-(()=>{
-const TOTAL=1280,FILL='#55554F';
-const leaf=(name,value)=>({name,value,itemStyle:{color:FILL},label:{color:PAPER}});
-const DATA=[
-  {name:'PRODUCT · 50%',children:[leaf('Core app',260),leaf('Collaboration',190),leaf('Search',110),leaf('Mobile',80)]},
-  {name:'PLATFORM · 31%',children:[leaf('AI systems',170),leaf('Data infra',120),leaf('APIs',105)]},
-  {name:'GROWTH · 19%',children:[leaf('Acquisition',90),leaf('Retention',80),leaf('Onboarding',75)]},
-];
-const host=document.getElementById('treemap');
-let chart;
-const draw=()=>{
-  chart=chart||echarts.init(host,null,{renderer:'svg'});
-  chart.clear();
-  chart.setOption({animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},
-    tooltip:{backgroundColor:PAPER,borderColor:INK,borderWidth:1,textStyle:{color:INK,fontSize:12},
-      formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}<br><b>${i.value} hours</b> · ${(i.value/TOTAL*100).toFixed(1)}%`},
-    series:[{type:'treemap',data:DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,
-      breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,
-      label:{show:true,position:'insideTopLeft',padding:[10,8],color:INK,fontSize:12,fontWeight:700,lineHeight:18,
-        formatter:p=>p.treePathInfo.length<=1?'':p.treePathInfo.length===2?p.name:`${p.name}\n${p.value} h`},
-      upperLabel:{show:true,height:32,padding:[0,10],color:INK,fontSize:10,fontWeight:700,backgroundColor:PAPER},
-      itemStyle:{borderColor:PAPER,borderWidth:2,gapWidth:2},
-      emphasis:{focus:'ancestor',itemStyle:{borderColor:INK,borderWidth:1.5}},
-      levels:[{itemStyle:{borderWidth:0,gapWidth:5}},
-        {upperLabel:{show:true,height:32},itemStyle:{borderColor:PAPER,borderWidth:3,gapWidth:3}},
-        {upperLabel:{show:false},itemStyle:{borderColor:PAPER,borderWidth:2,gapWidth:2}}]}]});
-};
-const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});
-io.observe(host);
-host.style.cursor='pointer';
-host.addEventListener('click',draw);
-addEventListener('resize',()=>chart?.resize());
-})();
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ F14 · rung histogram ════
-// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单
-// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,
-// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。
-(()=>{
-const BIN=[6,14,22,19,13,9,6,4,3,2,1,1];       // 2h bins, sums to 100
-obsReveal('histo',s=>{
-  const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;
-  BIN.forEach((v,i)=>{
-    const x=X0+i*PW+PW/2;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1,
-        opacity:.55+rnd(k+2,i+4)*.45,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'#C6C5BF',
-        class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-    }
-  });
-  // peak label
-  const pk=BIN.indexOf(Math.max(...BIN));
-  const px=X0+pk*PW+PW/2;
-  const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,
-    'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);
-  tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);
-  // bin edges: the continuous axis is the histogram's identity
-  el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  for(let e=0;e<=12;e++){
-    const x=X0+e*PW;
-    el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:`animation-delay:${e*.02}s`});
-    if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');
-  }
-  // median flag: cumulative crosses 50 inside bin 2
-  let acc=0,mb=0;for(let i=0;i<12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}
-  const mx=X0+mb*PW+PW*.7;
-  el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#8F8E88','stroke-width':.9,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'#6A6963',
-    class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE RUNG = ONE TICKET IN A HUNDRED · BINS OF TWO HOURS · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ F15 · tick box ════
-// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,
-// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。
-(()=>{
-const G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],
-         ['PRO',[.8,2.1,3.3,5,7.8],[10.5]],
-         ['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],
-         ['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];
-const SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9'];
-obsReveal('boxplot',s=>{
-  const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);
-  const colX=g=>78+g*84,BW=24;
-  [0,6,12,18,24].forEach(h=>{
-    el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'#E3E2DB','stroke-width':.8,
-      class:'fade',style:`animation-delay:${h*.008}s`});
-    txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.008}s`},h+'h');
-  });
-  G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{
-    const x=colX(g);
-    el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:'#8F8E88','stroke-width':.8,
-      pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});
-    [mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),
-      stroke:'#8F8E88','stroke-width':1,class:'fade',style:`animation-delay:${.2+g*.12}s`}));
-    const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,
-      fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});
-    tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);
-    el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,
-      class:'fade',style:`animation-delay:${.5+g*.12}s`});
-    txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');
-    outs.forEach((o,k)=>{
-      const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,
-        stroke:'#6A6963','stroke-width':1.1,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});
-      tip(d,`${name} outlier — ${o}h`);
-    });
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.12}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · HOLLOW = OUTLIER · DARKEST = FASTEST');
-});
-})();
-
-// ════ F16 · stream ribbon ════
-// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴
-// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。
-// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。
-(()=>{
-const N=48;
-const mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>
-  Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));
-const SERIES=[
-  ['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'#C6C5BF'],
-  ['BOARDS',mk(26,.10,11,4.2,5),'#8F8E88'],
-  ['FLOWS',mk(12,.78,10,3.1,8),INK],
-];
-obsReveal('stream',s=>{
-  const X0=36,X1=744,CY=138,SC=1.15;
-  const x=t=>X0+t/(N-1)*(X1-X0);
-  const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));
-  // stacked offsets, silhouette baseline: top of stack starts at -total/2
-  const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);
-  const smooth=pts=>{
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let k=1;k<pts.length-1;k++){
-      const p=pts[k],q=pts[k+1];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;
-  };
-  let run=y0.slice();
-  SERIES.forEach(([name,vals,shade],si)=>{
-    const top=run.map((v,t)=>[x(t),v]);
-    const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);
-    run=bot.map(p=>p[1]);
-    const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';
-    const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,
-      class:'fade',style:`animation-delay:${si*.16}s`});
-    tip(band,`${name} — weekly active accounts, 48 weeks`);
-    // label at the band's widest week — clamped off both ends so the
-    // paper-colored text never spills past the band onto paper (invisible)
-    let wk=4;vals.forEach((v,t)=>{if(t>=4&&t<=N-6&&v>vals[wk])wk=t});
-    const midY=(top[wk][1]+bot[wk][1])/2;
-    const dark=shade===INK||shade==='#4A4944';
-    txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',
-      fill:dark?PAPER:'#4A4944','letter-spacing':'.06em',
-      class:'fade',style:`animation-delay:${.5+si*.14}s`},name);
-  });
-  // month floor: barcode ticks + labels every 8 weeks
-  const base=252;
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  for(let t=0;t<N;t++)
-    el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:`animation-delay:${t*.006}s`});
-  ['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>
-    txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'BAND WIDTH = WEEKLY ACTIVES · DARKEST = TODAY’S LEADER · THE RIVER IS THE TOTAL');
-});
-})();
-
-// ════ F17 · candlestick ════
-// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,
-// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。
-// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。
-(()=>{
-const N=30;
-// deterministic random walk with a dip-and-recover arc
-const DAYS=[];
-let px=52;
-for(let d=0;d<N;d++){
-  const drift=d<10?.4:d<18?-1.5:1.3;
-  const open=px;
-  const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);
-  const hi=Math.max(open,close)+rnd(d+2,7)*2.6;
-  const lo=Math.min(open,close)-rnd(d+3,11)*2.6;
-  DAYS.push([open,close,hi,lo]);
-  px=close;
-}
-obsReveal('candle',s=>{
-  const X0=40,PW=11.2,BASE=258,TOP=44;
-  const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));
-  const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);
-  const x=d=>X0+d*PW+PW/2;
-  // price grid
-  for(let g=0;g<=4;g++){
-    const v=vmin+g/4*(vmax-vmin);
-    el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'#E3E2DB','stroke-width':.8,
-      class:'fade',style:`animation-delay:${g*.03}s`});
-    txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${g*.03}s`},'$'+Math.round(v));
-  }
-  let hiD=0,loD=0;
-  DAYS.forEach(([o,c,h,l],d)=>{
-    if(h>DAYS[hiD][2])hiD=d;
-    if(l<DAYS[loD][3])loD=d;
-  });
-  DAYS.forEach(([o,c,h,l],d)=>{
-    const cx=x(d),up=c>=o;
-    const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));
-    // wick: one hairline, full reach
-    el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'#6A6963','stroke-width':.7,
-      class:'fade',style:`animation-delay:${d*.03}s`});
-    // body: capsule; hollow = up, ink = down
-    const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,
-      ...(up?{fill:PAPER,stroke:INK,'stroke-width':1.1}:{fill:INK}),
-      class:'fade',style:`animation-delay:${.05+d*.03}s`});
-    tip(body,`Day ${d+1} — open $${o.toFixed(1)} · close $${c.toFixed(1)} · high $${h.toFixed(1)} · low $${l.toFixed(1)}`);
-    // extremes, labeled with hairline flags
-    if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(h));
-    if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':8,'font-weight':800,fill:'#6A6963','text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(l));
-  });
-  // week ticks on the floor
-  el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':.8,class:'fade'});
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>
-    txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade'},w));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'INK BODY = CLOSED DOWN · HOLLOW = CLOSED UP · WICK = THE DAY’S FULL REACH');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 112
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/big-circular.html

@@ -1,112 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Mono — Circular Graph, Dense</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#1C1C1A}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:#F0EFEB;border-radius:24px;padding:34px 34px 24px;
-    width:min(1100px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#8F8E88;margin-bottom:10px}
-  .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{height:78vh;min-height:620px;cursor:pointer}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>Six hundred collaborations, one ring</h2>
-  <div class="sub">60 repos · line = shared contributors · hover a repo to light up its world · click to replay</div>
-  <div id="ch"></div>
-  <div class="src">CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG</div>
-</div>
-<script>
-const INK='#1C1C1A',PAPER='#F0EFEB';
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── 60 repos in 5 org clusters; cluster = shade family ──
-const ORGS=[
-  ['platform',16,'#F0EFEB'],   // paper — the core
-  ['product',14,'#C9C7BD'],
-  ['data',12,'#A5A39A'],
-  ['infra',10,'#8F8E88'],
-  ['labs',8,'#6A6963'],
-];
-const nodes=[];
-ORGS.forEach(([org,count],oi)=>{
-  for(let i=0;i<count;i++){
-    // activity: a few giants per cluster, long tail of small repos
-    const heavy=i<2?30+rnd(oi+1,i+2)*45:4+rnd(oi+3,i+7)*16;
-    nodes.push({
-      name:org+'/'+org.slice(0,2)+'-'+String(i+1).padStart(2,'0'),
-      org:oi,value:Math.round(heavy),
-    });
-  }
-});
-// links: dense inside a cluster, sparser across; weight = shared contributors
-const links=[];
-const idx={};nodes.forEach((n,i)=>idx[n.name]=i);
-nodes.forEach((a,i)=>{
-  nodes.forEach((b,j)=>{
-    if(j<=i)return;
-    const same=a.org===b.org;
-    const p=same?.16:.045;
-    if(rnd(i+1,j+2)<p){
-      links.push({source:i,target:j,
-        w:1+Math.round(rnd(i+2,j+3)*6),same});
-    }
-  });
-});
-
-const g=echarts.init(document.getElementById('ch'));
-const opt={
-  animationDuration:1800,animationEasing:'quarticOut',
-  tooltip:{backgroundColor:PAPER,borderWidth:0,
-    textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14],
-    formatter:p=>p.dataType==='node'
-      ?p.name+' — '+p.value+' contributors'
-      :p.data.rawS+' ↔ '+p.data.rawT+' — '+p.data.w+' shared'},
-  series:[{
-    type:'graph',layout:'circular',
-    circular:{rotateLabel:true},
-    left:24,right:24,top:24,bottom:24,
-    data:nodes.map((n,i)=>({
-      name:n.name,value:n.value,
-      symbolSize:3.5+Math.sqrt(n.value)*1.7,
-      itemStyle:{color:ORGS[n.org][2],borderWidth:0},
-      // only the giants get a label — 60 names would be soup
-      label:{show:n.value>=26,position:'right',distance:4,
-        color:'#B3B0A4',fontFamily:'Inter',fontSize:8.5,fontWeight:600},
-    })),
-    links:links.map(l=>({
-      source:l.source,target:l.target,
-      rawS:nodes[l.source].name,rawT:nodes[l.target].name,w:l.w,
-      lineStyle:{
-        width:.4+l.w*.28,
-        color:l.same?'#55534B':'#3A3934',
-        opacity:l.same?.5:.32,
-        curveness:.3,
-      },
-    })),
-    emphasis:{
-      focus:'adjacency',
-      lineStyle:{color:PAPER,opacity:.9,width:1.6},
-      label:{show:true,color:PAPER},
-    },
-    blur:{itemStyle:{opacity:.12},lineStyle:{opacity:.03}},
-  }],
-};
-const play=()=>{g.clear();g.setOption(opt)};
-play();
-document.getElementById('ch').addEventListener('click',play);
-addEventListener('resize',()=>g.resize());
-</script>
-</body>
-</html>

+ 0 - 111
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/big-force.html

@@ -1,111 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Mono — Force Graph, Dense</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#1C1C1A}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:#F0EFEB;border-radius:24px;padding:34px 34px 24px;
-    width:min(1100px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#8F8E88;margin-bottom:10px}
-  .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{height:78vh;min-height:620px;cursor:grab}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>A galaxy of one hundred eighty services</h2>
-  <div class="sub">8 domains self-organize · node = call volume · drag anything, the galaxy fights back · click empty space to replay</div>
-  <div id="ch"></div>
-  <div class="src">FORCE GRAPH · DENSE · SOURCE · SERVICE MESH</div>
-</div>
-<script>
-const INK='#1C1C1A',PAPER='#F0EFEB';
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── 8 domain hubs, each with a swarm of services; hubs interconnect ──
-const DOMAINS=[
-  ['auth',26,'#F0EFEB'],['billing',24,'#DCDAD2'],['content',28,'#C9C7BD'],
-  ['search',20,'#B3B0A4'],['notify',22,'#9C9A91'],['media',20,'#8F8E88'],
-  ['analytics',18,'#7C7A72'],['edge',14,'#6A6963'],
-];
-const nodes=[],links=[];
-DOMAINS.forEach(([dom,count,shade],di)=>{
-  // the hub
-  nodes.push({name:dom,value:60+Math.round(rnd(di+1,3)*50),hub:true,shade,dom:di});
-  // satellites: long-tail sizes, a couple of mid-weights per domain
-  for(let i=0;i<count;i++){
-    const v=i<3?14+rnd(di+2,i+4)*18:2+rnd(di+5,i+9)*9;
-    const name=dom+'-'+String(i+1).padStart(2,'0');
-    nodes.push({name,value:Math.round(v),hub:false,shade,dom:di});
-    // satellite -> its hub
-    links.push({source:name,target:dom,
-      lineStyle:{width:.5+v*.05,color:'#3F3E38',opacity:.5}});
-    // occasional satellite-to-satellite shortcut inside the domain
-    if(i>1&&rnd(di+3,i+11)<.22){
-      links.push({source:name,target:dom+'-'+String(1+Math.floor(rnd(di+4,i+13)*i)).padStart(2,'0'),
-        lineStyle:{width:.5,color:'#33322D',opacity:.4}});
-    }
-  }
-});
-// hub-to-hub backbone: every domain talks to 2-3 others
-const H=DOMAINS.length;
-for(let a=0;a<H;a++)for(let b=a+1;b<H;b++){
-  if(rnd(a+7,b+9)<.42){
-    links.push({source:DOMAINS[a][0],target:DOMAINS[b][0],
-      lineStyle:{width:1.6+rnd(a+2,b+3)*2.2,color:'#55534B',opacity:.65,curveness:.08}});
-  }
-}
-// a few cross-domain satellite shortcuts — the messy real-world wires
-for(let k=0;k<26;k++){
-  const a=nodes[1+Math.floor(rnd(k+1,17)*(nodes.length-1))];
-  const b=nodes[1+Math.floor(rnd(k+3,23)*(nodes.length-1))];
-  if(a.name!==b.name&&a.dom!==b.dom&&!a.hub&&!b.hub){
-    links.push({source:a.name,target:b.name,
-      lineStyle:{width:.5,color:'#2E2D29',opacity:.38,curveness:.15}});
-  }
-}
-
-const g=echarts.init(document.getElementById('ch'));
-const opt={
-  animationDuration:300,
-  tooltip:{backgroundColor:PAPER,borderWidth:0,
-    textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14],
-    formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k calls/day':''},
-  series:[{
-    type:'graph',layout:'force',
-    force:{repulsion:46,edgeLength:[10,42],gravity:.16,friction:.22,layoutAnimation:true},
-    roam:true,draggable:true,
-    left:6,right:6,top:6,bottom:6,
-    data:nodes.map(n=>({
-      name:n.name,value:n.value,
-      symbolSize:n.hub?16+Math.sqrt(n.value)*1.6:2.5+Math.sqrt(n.value)*1.5,
-      itemStyle:{color:n.shade,borderWidth:n.hub?2:0,borderColor:INK},
-      label:{show:n.hub,position:'inside',
-        color:INK,fontFamily:'Inter',fontSize:9.5,fontWeight:800},
-    })),
-    links,
-    emphasis:{
-      focus:'adjacency',
-      lineStyle:{color:PAPER,opacity:.9,width:1.4},
-      label:{show:true,color:PAPER,position:'right'},
-    },
-    blur:{itemStyle:{opacity:.1},lineStyle:{opacity:.03}},
-  }],
-};
-const play=()=>{g.clear();g.setOption(opt)};
-play();
-// click on empty canvas replays; clicks on nodes are for dragging
-g.getZr().on('click',e=>{if(!e.target)play()});
-addEventListener('resize',()=>g.resize());
-</script>
-</body>
-</html>

+ 0 - 164
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/big-threads.html

@@ -1,164 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Mono — Thread Triptych, Interactive</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#1C1C1A;--paper:#F0EFEB}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:var(--paper);border-radius:24px;padding:34px 34px 24px;
-    width:min(1200px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#8F8E88;margin-bottom:6px}
-  .src{font-size:9.5px;color:#55554F;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{width:100%;height:74vh;min-height:560px;display:block}
-  #status{height:34px;display:flex;align-items:center;gap:10px;
-    font-size:12px;color:#8F8E88;border-top:1px solid #2E2D29;margin-top:6px;padding-top:8px}
-  #status b{color:var(--paper);font-weight:700}
-  #status .pin{font-size:9px;letter-spacing:.1em;color:#55554F;font-weight:700}
-  svg text{font-family:'Inter',sans-serif;user-select:none}
-
-  /* thread states */
-  .thread{transition:opacity .18s ease,stroke-width .18s ease}
-  .thread.hot{opacity:.95 !important}
-  svg.focused .thread:not(.hot){opacity:.035 !important}
-  .nodelab{cursor:pointer;transition:opacity .18s ease}
-  svg.focused .nodelab:not(.hot){opacity:.25}
-  .hit{cursor:pointer}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>Where the data actually goes — now answerable</h2>
-  <div class="sub">source → processor → destination · one thread per data path · hover to trace a route · hover any label to pull its whole bundle · click to pin, click dark space to release</div>
-  <svg id="ch" viewBox="0 0 1140 640" preserveAspectRatio="xMidYMid meet"></svg>
-  <div id="status"><span id="stext">hover a thread or a label</span><span class="pin" id="spin"></span></div>
-  <div class="src">THREAD TRIPTYCH · INTERACTIVE · DATA MAP</div>
-</div>
-<script>
-const PAPER='#F0EFEB',NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ── the pipeline ──
-const SRC=['web-app','ios-app','android','desktop','cli','public-api','webhooks','forms','email-in','chat-widget',
-  'billing-ev','auth-ev','search-log','audit-ev','crash-rep','perf-beacon','cdn-log','edge-log','job-ev','sync-ev',
-  'import','export-req','admin-ui','partner-api','iot-ping','backfill'];
-const MIDV=['parse','validate','dedupe','enrich','geoip','sessionize','pii-mask','score','sample','route',
-  'batch','window','join','flatten','filter','hash','encrypt','compact','index','tag'];
-const MID=MIDV.flatMap(v=>[v+'-a',v+'-b']);          // 40 processors
-const DST=['WAREHOUSE','ANALYTICS','CRM','ADS','BILLING','SEARCH','ML PIPE','ARCHIVE','AUDIT','EXPORTS'];
-
-// routes: each source emits 2-3 full paths; volume is long-tailed
-const routes=[];
-SRC.forEach((s,i)=>{
-  const n=2+Math.floor(rnd(i+1,5)*2);
-  for(let k=0;k<n;k++){
-    const mi=Math.floor(rnd(i*3+k+2,13)*MID.length);
-    const di=Math.min(9,Math.floor(rnd(i*5+k+4,19)**2*11));   // skew: low dsts hoard
-    const v=Math.round(1+rnd(i*7+k+6,29)**2*30);              // 1..31, long tail
-    routes.push({s:i,m:mi,d:di,v});
-  }
-});
-
-// ── geometry ──
-const X1=180,X2=560,X3=930;
-const sy=i=>36+i*22.6, my=i=>30+i*14.6, dy=i=>60+i*57;
-const path=r=>`M${X1+6} ${sy(r.s)} C${(X1+X2)/2} ${sy(r.s)} ${(X1+X2)/2} ${my(r.m)} ${X2} ${my(r.m)}`
-  +` C${(X2+X3)/2} ${my(r.m)} ${(X2+X3)/2} ${dy(r.d)} ${X3-6} ${dy(r.d)}`;
-
-const svg=document.getElementById('ch');
-const stext=document.getElementById('stext'),spin=document.getElementById('spin');
-const fmt=v=>v+'k ev/day';
-let pinned=null;
-
-// column headers
-[[X1,'① SOURCE'],[X2,'② PROCESSOR'],[X3,'③ DESTINATION']].forEach(([x,t])=>
-  txt(svg,{x,y:14,'font-size':9,'font-weight':800,fill:PAPER,'letter-spacing':'.14em','text-anchor':'middle'},t));
-
-// threads (drawn first, under labels)
-const gThreads=el(svg,'g',{});
-routes.forEach((r,ri)=>{
-  r.el=el(gThreads,'path',{d:path(r),fill:'none',stroke:PAPER,
-    'stroke-width':Math.max(.6,r.v*.14),class:'thread',
-    opacity:.06+Math.min(.2,r.v*.012),'stroke-linecap':'round'});
-  // invisible fat twin: the hover/click target
-  r.hit=el(gThreads,'path',{d:path(r),fill:'none',stroke:'#000','stroke-opacity':0,
-    'stroke-width':9,class:'hit'});
-  r.hit.dataset.route=ri;
-});
-
-// node labels + ticks; each carries hit metadata
-const mkNode=(x,y,label,anchor,cls,meta,size,fill)=>{
-  const g=el(svg,'g',{class:'nodelab'});
-  g.dataset.kind=meta.kind;g.dataset.idx=meta.idx;
-  el(g,'rect',{x:x-3,y:y-1.6,width:6,height:3.2,fill:PAPER,opacity:.85});
-  txt(g,{x:anchor==='end'?x-9:x+9,y:y+2.6,'font-size':size,'font-weight':600,
-    fill,'text-anchor':anchor},label);
-  // generous invisible hit zone
-  el(g,'rect',{x:anchor==='end'?x-92:x-6,y:y-6.5,width:98,height:13,fill:'#000','fill-opacity':0,class:'hit'});
-  return g;
-};
-SRC.forEach((s,i)=>mkNode(X1,sy(i),s,'end','',{kind:'s',idx:i},7.5,'#B3B0A4'));
-MID.forEach((m,i)=>mkNode(X2,my(i),m,'start','',{kind:'m',idx:i},5.4,'#6A6963'));
-DST.forEach((d,i)=>{
-  const g=el(svg,'g',{class:'nodelab'});
-  g.dataset.kind='d';g.dataset.idx=i;
-  el(g,'circle',{cx:X3,cy:dy(i),r:3.4,fill:PAPER});
-  txt(g,{x:X3+12,y:dy(i)+3,'font-size':8.5,'font-weight':700,fill:'#B3B0A4','letter-spacing':'.08em'},d);
-  el(g,'rect',{x:X3-8,y:dy(i)-10,width:120,height:20,fill:'#000','fill-opacity':0,class:'hit'});
-});
-
-// ── highlight machinery ──
-const nodeLabs=[...svg.querySelectorAll('.nodelab')];
-const clear=()=>{
-  svg.classList.remove('focused');
-  routes.forEach(r=>r.el.classList.remove('hot'));
-  nodeLabs.forEach(n=>n.classList.remove('hot'));
-  stext.textContent='hover a thread or a label';spin.textContent='';
-};
-const focusRoutes=(rs,label)=>{
-  svg.classList.add('focused');
-  routes.forEach(r=>r.el.classList.toggle('hot',rs.includes(r)));
-  nodeLabs.forEach(n=>{
-    const k=n.dataset.kind,ix=+n.dataset.idx;
-    n.classList.toggle('hot',rs.some(r=>(k==='s'&&r.s===ix)||(k==='m'&&r.m===ix)||(k==='d'&&r.d===ix)));
-  });
-  stext.innerHTML=label;
-};
-const routeLabel=r=>`<b>${SRC[r.s]}</b> → ${MID[r.m]} → <b>${DST[r.d]}</b> · ${fmt(r.v)}`;
-const nodeSel=(kind,idx)=>{
-  const rs=routes.filter(r=>(kind==='s'&&r.s===idx)||(kind==='m'&&r.m===idx)||(kind==='d'&&r.d===idx));
-  const total=rs.reduce((a,r)=>a+r.v,0);
-  const name=kind==='s'?SRC[idx]:kind==='m'?MID[idx]:DST[idx];
-  return [rs,`<b>${name}</b> · ${rs.length} path${rs.length>1?'s':''} · ${fmt(total)} total`];
-};
-
-const apply=t=>{
-  const hit=t.closest?.('.hit');
-  if(!hit)return null;
-  const rg=hit.dataset.route,ng=hit.closest('.nodelab');
-  if(rg!=null){const r=routes[+rg];return()=>focusRoutes([r],routeLabel(r))}
-  if(ng){const [rs,lab]=nodeSel(ng.dataset.kind,+ng.dataset.idx);return()=>focusRoutes(rs,lab)}
-  return null;
-};
-svg.addEventListener('mousemove',e=>{
-  if(pinned)return;
-  const f=apply(e.target);
-  f?f():clear();
-});
-svg.addEventListener('mouseleave',()=>{if(!pinned)clear()});
-svg.addEventListener('click',e=>{
-  const f=apply(e.target);
-  if(f){pinned=f;f();spin.textContent='PINNED · CLICK DARK SPACE TO RELEASE'}
-  else{pinned=null;clear()}
-});
-</script>
-</body>
-</html>

+ 0 - 37
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/README.md

@@ -1,37 +0,0 @@
-# 彩色换肤样张
-
-**这里只查配色,不查结构。**
-
-渲染代码、几何、动画的正本永远是 `templates/` 根目录那四个 gallery。彩色样张是它们的换肤版——逐节点对应同一份数据和动效,从这里照抄结构等于绕远路,而且容易漂。
-
-内置预设的色值正本是 `color-presets.js`。样张 header 里那些散落的说明(加墨系数、色板顺序)只是自述,规则本体在 `SKILL.md` 第六点五节。Agent 可以根据数据结构和使用场景自动选择一套预设,不要求用户先触发彩色。同一份 HTML 或同一组交付只能使用一种色彩系统,禁止跨预设混用;用户拿到成品后可以继续调色,但要重新检查对比度、视觉主次和数据语义。
-
-## Custom 不是第四套内置预设
-
-用户明确提供品牌色、hex 或完整色板时,可以按 `SKILL.md` 的角色规则建立一套交付内 `CUSTOM`。它不出现在这里,因为不存在一套对所有用户都成立的 custom 样张。Custom 不得从三套内置预设借色补齐,也不得回写 `color-presets.js`,除非用户明确要求把品牌色固化为项目级预设。
-
-Treemap 的四种实现已经加入 Basics gallery:Mono 靠父级标题带和留白分组;Porcelain 用明度对应有序父组;Palm 用色相对应顶层类目;Wire 只给一个叶子使用荧光橙。
-
-## 三套
-
-| 文件 | 色系 | 色彩逻辑 | 适配 |
-| - | - | - | - |
-| `*-porcelain.html` | 青瓷蓝 | 单色相明度阶 | 有序数据、单序列 |
-| `*-palm.html` | 椰林绿 | 色相 = 类目 | 无序类目 ≤4(5–6 勉强) |
-| `*-wire.html` | 编辑部红 | 灰阶 + 一个荧光橙 | 几乎所有场合,要一个视线落点 |
-
-每套四个 gallery(basics / glance / lupi / maps),共 12 个,没有缺口。地图样张只在用户明确要求地图时才用得上。
-
-archive(陶土青灰)已归档,不再是候选。它和 palm 都是"色相=类目",功能重叠。
-
-## 三个交互大图
-
-`big-circular` / `big-force` / `big-threads` 只有 porcelain 和 palm 两套,没有 wire。
-
-原因:wire 本身就是黑灰阶加一个荧光橙,和原版 mono 大图的差别只在强调色,做出来近乎重复。要 wire 风格的大图,直接用 `templates/` 根目录的原版。
-
-这三张是**暗底图**,色彩逻辑和浅卡不同:卡底压深,8 档单色相阶从亮到暗,编码的是"离核心多远"(亮 = 核心),不是类目。色值在 `color-presets.js` 各套的 `DARK` 块里。
-
-为什么不用色相分类目:这三张分别需要 5 / 8 / 三列结构的档位,超过 palm 绿黄区间的容量上限(≤6)。按 SKILL.md 门一的规则,只能走单色相明度阶。
-
-`big-threads` 的 hover 高亮是改 opacity(线恒为 `DK8[0]`),不用 `DARK.HL`。

+ 0 - 901
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/basics-palm.html

@@ -1,901 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Basics F1–F17 × PALM</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<style>
-  :root{--bg:#F0EFEB;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.32);--grid:rgba(88,64,46,.16)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed var(--faint);color:var(--muted)}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  .chartbox{width:100%;height:360px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-  .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>基础型组 F1–F17 × PALM</h1>
-  <p><span class="chip" style="background:#43593B"></span><span class="chip" style="background:#F2D17E"></span><span class="chip" style="background:#ACAD79"></span><span class="chip" style="background:#58402E"></span> &nbsp;椰林 · 深绿主墨 + 浓咖文字,琥珀只干强调的活(峰值/最优/表针)。序数梯纯走绿→黄:深绿 → 中绿 → 苔绿 → 浅苔 → 橄榄 → 麦黄。几何、动画、数据与 basics-gallery 一致;彩色版统一加墨(线宽 ×1.8 / 透明度地板 .85,dot heat 除外——它的墨本来就够)。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 柱状</span>
-    <h2>Revenue by plan, rung by rung</h2>
-    <div class="sub">one rung = one $k of MRR · the bar is a ladder you can count</div>
-    <svg id="rungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG BARS · PALM · BILLING</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 折线</span>
-    <h2>Thirty days of sign-ups</h2>
-    <div class="sub">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest</div>
-    <svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE LINE · PALM · GROWTH</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 面积</span>
-    <h2>Concurrent users, filled with days</h2>
-    <div class="sub">one hairline = one day, floor to peak · the area is made of days, not paint</div>
-    <svg id="hairarea" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE AREA · PALM · LIVE OPS</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 环形</span>
-    <h2>Where the traffic comes from</h2>
-    <div class="sub">one tick = one percent · four sources shade the dial · read it like a clock</div>
-    <svg id="tickdonut" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK DONUT · PALM · ACQUISITION</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 横向条形</span>
-    <h2>Six teams, shipped and counted</h2>
-    <div class="sub">one tick = one release this quarter · the row is a queue, not a bar</div>
-    <svg id="tickrows" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK ROWS · PALM · RELEASE LOG</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 分组柱</span>
-    <h2>This year against last, plan by plan</h2>
-    <div class="sub">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k</div>
-    <svg id="pairrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PAIRED RUNGS · PALM · BILLING YoY</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 堆叠柱</span>
-    <h2>Where each region's revenue sits</h2>
-    <div class="sub">one rung = $1k · three products stack up the ladder, strongest carries the most</div>
-    <svg id="stackrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">STACKED RUNGS · PALM · REVENUE MIX</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 散点</span>
-    <h2>Price against satisfaction, twelve products</h2>
-    <div class="sub">every dot hangs a plumb line to the floor · read x by the line, y by the height</div>
-    <svg id="plumb" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PLUMB SCATTER · PALM · PRODUCT PANEL</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 瀑布</span>
-    <h2>From gross to net, step by step</h2>
-    <div class="sub">one rung = $1k · solid rungs add, dashed rungs take away · the ladder walks down</div>
-    <svg id="fall" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG WATERFALL · PALM · P&amp;L</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 热力</span>
-    <h2>When support gets loud</h2>
-    <div class="sub">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible</div>
-    <svg id="dotheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT HEAT · PALM · SUPPORT DESK</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 进度</span>
-    <h2>How far to the quarter's goal</h2>
-    <div class="sub">one tick = one percent of target · inked = earned · the dial reads left to right</div>
-    <svg id="gauge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK GAUGE · PALM · QUARTERLY TARGET</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 哑铃对比</span>
-    <h2>Onboarding, before and after the redesign</h2>
-    <div class="sub">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable</div>
-    <svg id="dumbbell" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DUMBBELL QUEUE · PALM · UX TIMING</div>
-  </div>
-
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · 矩形树图</span>
-    <h2>Where the work went</h2>
-    <div class="sub">1,280 shipped hours · area = effort · hue = top-level product family</div>
-    <div id="treemap" class="chartbox" role="img" aria-label="Palm treemap of shipped hours nested by product family"></div>
-    <div class="src">NESTED TREEMAP · PALM · WORK PORTFOLIO</div>
-  </div>
-
-
-  <!-- F14 · rung histogram -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 直方图</span>
-    <h2>Most tickets resolve within six hours</h2>
-    <div class="sub">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile</div>
-    <svg id="histo" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG HISTOGRAM · PALM · SUPPORT DESK</div>
-  </div>
-
-  <!-- F15 · tick box -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 箱线图</span>
-    <h2>Reply times, boxed by plan</h2>
-    <div class="sub">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed</div>
-    <svg id="boxplot" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK BOX · PALM · SUPPORT DESK</div>
-  </div>
-
-  <!-- F16 · stream ribbon -->
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · STREAMGRAPH</span>
-    <h2>Three products trade the same river</h2>
-    <div class="sub">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows</div>
-    <svg id="stream" viewBox="0 0 840 300" preserveAspectRatio="xMidYMid meet" style="max-height:300px"></svg>
-    <div class="src">STREAM RIBBON · PALM · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- F17 · candlestick -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · K 线</span>
-    <h2>Six weeks of the token, candle by candle</h2>
-    <div class="sub">body = open to close · wick = the day's full reach · green = closed down · wheat = closed up</div>
-    <svg id="candle" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">CANDLESTICK · PALM · MARKET DATA</div>
-  </div>
-
-</div>
-<script>
-// ── PALM 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.32)';
-const FLOOR='rgba(88,64,46,.24)',QUIET='rgba(88,64,46,.15)',TRACK='rgba(88,64,46,.12)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#58402E',HERO='#D4A017';
-const FAINTDATA='#ACAD79',BEAD='#77835A';
-// 序数梯(明度∝数值,大→小 = 深→浅):给排序过的类目用
-const RAMP6=['#43593B','#5A7049','#77835A','#929960','#ACAD79','#F2D17E'];
-// 分段色:月份 / 进度区段等图内既有维度
-const SEG3=['#43593B','#77835A','#ACAD79'], ZONE3=['#ACAD79','#929960','#43593B'];
-const CAT4=["#43593B", "#77835A", "#ACAD79", "#F2D17E"],CAT3=["#43593B", "#929960", "#F2D17E"],CAT3L=["#43593B", "#77835A", "#ACAD79"],HEAT=["#43593B", "#929960", "#F2D17E"];
-const HEROMODE=false; // 黑体红睛模式:每张图一个主角元素整体走 HERO
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ B1 · rung bars ════
-(()=>{
-const D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];
-obsReveal('rungs',s=>{
-  const x0=i=>56+i*56,base=266,step=5.6,HW=14;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i);
-    const colInk=(HEROMODE&&i===0)?HERO:RAMP6[i];
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — $${v}k MRR`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = $1K · SHADE = RANK · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ B2 · hairline line ════
-(()=>{
-const N=30;
-const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
-obsReveal('dayline',s=>{
-  const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===5||d%7===6,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?5.2:3.1,
-      fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
-});
-})();
-
-// ════ B3 · hairline area ════
-(()=>{
-const N=45;
-const val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;
-obsReveal('hairarea',s=>{
-  const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;
-  el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const peak=vs.indexOf(Math.max(...vs));
-  vs.forEach((v,d)=>{
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),
-      stroke:d===peak?HERO:SEG3[Math.min(2,Math.floor(d/15))],'stroke-width':d===peak?1.6:1,
-      opacity:d===peak?1:.8+rnd(d+1,7)*.2,
-      class:'fade',style:`animation-delay:${d*.014}s`});
-  });
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2,pathLength:1,
-    class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});
-  const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:HERO,
-    class:'pop',style:'animation-delay:1.2s'});
-  tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);
-  txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1.3s`,
-    class:'fade'},Math.round(vs[peak])+'k');
-  [[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
-    'ONE HAIRLINE = ONE DAY · SHADE = MONTH · FLOOR TO PEAK');
-});
-})();
-
-// ════ B4 · tick donut ════
-(()=>{
-const D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];
-obsReveal('tickdonut',s=>{
-  const cx=200,cy=148,R0=64;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=12+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of traffic`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
-  txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');
-});
-})();
-
-// ════ C1 · tick rows ════
-(()=>{
-const D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];
-obsReveal('tickrows',s=>{
-  const y0=i=>52+i*44,X0=104,PX=6.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i);
-    txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    const rowInk=(HEROMODE&&i===0)?HERO:RAMP6[i];
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:rowInk,'stroke-width':1.7,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name} — ${v} releases`);
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = ONE RELEASE · SHADE = RANK · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ C2 · paired rungs ════
-(()=>{
-const D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];
-obsReveal('pairrungs',s=>{
-  const x0=i=>64+i*66,base=258,step=5.4,HW=10;
-  D.forEach(([name,was,now],i)=>{
-    const xa=x0(i)-13,xb=x0(i)+13;
-    for(let k=0;k<was;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;
-      el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.8,
-        opacity:.75+rnd(k+2,i+3)*.25,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-    }
-    for(let k=0;k<now;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;
-      el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+8)*.15,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});
-    }
-    const topB=base-(now-1)*step;
-    const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:HEROMODE?HERO:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},now);
-    tip(num,`${name} — $${was}k → $${now}k`);
-    txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:FAINTDATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},was);
-    txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'FAINT = 2025 · SOLID = 2026 · ONE RUNG = $1K');
-});
-})();
-
-// ════ C3 · stacked rungs ════
-(()=>{
-const D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];
-const SEG=['CORE','ADD-ONS','SERVICES'];
-obsReveal('stackrungs',s=>{
-  const x0=i=>72+i*76,base=262,step=5.2,HW=13;
-  D.forEach(([name,segs],i)=>{
-    const x=x0(i);let k0=0;
-    segs.forEach((v,si)=>{
-      for(let k=0;k<v;k++){
-        const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;
-        el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:CAT3[si],'stroke-width':1.8,
-          opacity:.85+rnd(k+2,i+si+4)*.15,class:'fade',
-          style:`animation-delay:${i*.09+(k0+k)*.012}s`});
-      }
-      const midY=base-(k0+v/2+si)*step;
-      const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:CAT3L[si],
-        class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);
-      tip(lab,`${name} ${SEG[si]} — $${v}k`);
-      k0+=v;
-    });
-    const total=segs[0]+segs[1]+segs[2];
-    txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.6+i*.09}s`},total);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.09}s`},name);
-  });
-  el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'STRONGEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');
-});
-})();
-
-// ════ C4 · plumb scatter ════
-(()=>{
-const P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],
-         ['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],
-         ['Hub',76,32],['Forms',34,38]];
-obsReveal('plumb',s=>{
-  const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;
-  for(let g=0;g<=20;g++){
-    const x=X0+g/20*(X1-X0);
-    el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${g*.01}s`});
-  }
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'CHEAP');
-  txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',
-    class:'fade'},'PREMIUM');
-  const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]<a[2]?b:a);
-  P.forEach(([name,px,sat],i)=>{
-    const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];
-    el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1,
-      opacity:.75,class:'fade',style:`animation-delay:${.2+i*.05}s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:hero?5.4:3.4,fill:hero?HERO:DATA,
-      class:'pop',style:`animation-delay:${.25+i*.05}s`});
-    tip(dot,`${name} — price ${px} · satisfaction ${sat}`);
-    if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.8}s`,
-      class:'fade'},`${name} · ${sat}`);
-  });
-  txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:FAINT,
-    transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');
-});
-})();
-
-// ════ C5 · rung waterfall ════
-(()=>{
-const D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];
-obsReveal('fall',s=>{
-  const x0=i=>58+i*72,step=5.2,HW=11;
-  let lv=0;const rows=[];
-  D.forEach(([name,v],i)=>{
-    if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}
-    else if(name==='NET'){rows.push([name,lv,0,lv])}
-    else{rows.push([name,v,lv+v,lv]);lv=lv+v}
-  });
-  const base=252,yOf=k=>base-k*step;
-  rows.forEach(([name,v,lo,hi],i)=>{
-    const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v<0&&!isTotal;
-    const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);
-    const colInk=name==='NET'?HERO:DATA;   // NET 是这张图的结论,常驻强调色
-    for(let k=0;k<n;k++){
-      const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;
-      if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA2,'stroke-width':1.8,
-        'stroke-dasharray':'2.5 2.5',opacity:.85,class:'fade',
-        style:`animation-delay:${i*.12+k*.014}s`});
-      else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});
-    }
-    if(i<rows.length-1){
-      const nx=x0(i+1);
-      const lvl=isTotal?v:(neg?lo:hi);
-      el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:FAINT,
-        'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',
-        style:`animation-delay:${.3+i*.12}s`});
-    }
-    const topY=yOf(Math.max(from,to));
-    const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-      fill:neg?DATA2:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));
-    tip(num,`${name} — ${neg?'−':''}$${Math.abs(v)}k`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'SOLID ADD · DASHED TAKE AWAY · NET IN ACCENT · ONE RUNG = $1K');
-});
-})();
-
-// ════ C6 · dot heat ════
-(()=>{
-const DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-obsReveal('dotheat',s=>{
-  const x0=j=>64+j*27,y0=i=>58+i*30;
-  let max=0,mi=0,mj=0;
-  const v=(i,j)=>{
-    const day=i<5?1:.32;
-    const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5);
-    const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);
-    return Math.round(raw);
-  };
-  for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  for(let i=0;i<7;i++){
-    txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
-    for(let j=0;j<12;j++){
-      const t=v(i,j),x=x0(j),y=y0(i);
-      if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
-      const hero=i===mi&&j===mj;
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
-        fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
-      tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);
-      if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
-        stroke:HERO,'stroke-width':1,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:1s'});
-    }
-  }
-  for(let j=0;j<12;j+=2)
-    txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);
-});
-})();
-
-// ════ C7 · tick gauge ════
-(()=>{
-const GOAL=73;
-obsReveal('gauge',s=>{
-  const cx=200,cy=190,R0=104,A0=-195,SW=210;
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?15+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?ZONE3[Math.min(2,Math.floor(k/25))]:FLOOR,'stroke-width':inked?1.9:.6,
-      class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);
-    el(s,'circle',{cx:dx,cy:dy,r:1,fill:FAINT,class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:3.2,fill:HERO,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},GOAL+'%');
-  tip(num,`$730k of the $1M quarter target`);
-  txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE TICK = 1% OF TARGET · SHADE = QUARTER · INKED = EARNED');
-});
-})();
-
-// ════ C8 · dumbbell queue ════
-(()=>{
-const D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];
-obsReveal('dumbbell',s=>{
-  const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);
-  D.forEach(([name,was,now],i)=>{
-    const y=y0(i),xa=mapX(was),xb=mapX(now);
-    txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:TRACK,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    const n=was-now;
-    for(let k=0;k<n;k++){
-      const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;
-      el(s,'circle',{cx:x,cy:yy,r:2.1+rnd(k+2,i+4)*.9,fill:BEAD,opacity:.95,
-        class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});
-    }
-    el(s,'circle',{cx:xa,cy:y,r:4.8,fill:BG,stroke:DATA,'stroke-width':1.7,
-      class:'pop',style:`animation-delay:${.2+i*.08}s`});
-    const after=el(s,'circle',{cx:xb,cy:y,r:5.2,fill:HERO,
-      class:'pop',style:`animation-delay:${.6+i*.08}s`});
-    tip(after,`${name} — ${was} min → ${now} min`);
-    txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:FAINTDATA,
-      class:'fade',style:`animation-delay:${.3+i*.08}s`},was);
-    txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:TXT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${.7+i*.08}s`},now);
-  });
-  txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'FASTER ←');
-  txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',
-    class:'fade'},'MINUTES');
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · SOLID = AFTER');
-});
-})();
-
-// ════ C9 · nested treemap ════
-(()=>{
-const TOTAL=1280;
-const leaf=(name,value,color,labelColor=BG)=>({name,value,itemStyle:{color},label:{color:labelColor}});
-const TREE_DATA=[
-  {name:'PRODUCT · 50%',children:[leaf('Core app',260,DATA),leaf('Collaboration',190,DATA),leaf('Search',110,DATA),leaf('Mobile',80,DATA)]},
-  {name:'PLATFORM · 31%',children:[leaf('AI systems',170,BEAD),leaf('Data infra',120,BEAD),leaf('APIs',105,BEAD)]},
-  {name:'GROWTH · 19%',children:[leaf('Acquisition',90,HERO,TXT),leaf('Retention',80,HERO,TXT),leaf('Onboarding',75,HERO,TXT)]},
-];
-const host=document.getElementById('treemap');let chart;
-const draw=()=>{chart=chart||echarts.init(host,null,{renderer:'svg'});chart.clear();chart.setOption({
-  animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},
-  tooltip:{backgroundColor:BG,borderColor:TXT,borderWidth:1,textStyle:{color:TXT,fontSize:12},
-    formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}<br><b>${i.value} hours</b> · ${(i.value/TOTAL*100).toFixed(1)}%`},
-  series:[{type:'treemap',data:TREE_DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,
-    label:{show:true,position:'insideTopLeft',padding:[10,8],color:TXT,fontSize:12,fontWeight:700,lineHeight:18,
-      formatter:p=>p.treePathInfo.length<=1?'':p.treePathInfo.length===2?p.name:`${p.name}\n${p.value} h`},
-    upperLabel:{show:true,height:32,padding:[0,10],color:TXT,fontSize:10,fontWeight:700,backgroundColor:BG},
-    itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2},emphasis:{focus:'ancestor',itemStyle:{borderColor:TXT,borderWidth:1.5}},
-    levels:[{itemStyle:{borderWidth:0,gapWidth:5}},{upperLabel:{show:true,height:32},itemStyle:{borderColor:BG,borderWidth:3,gapWidth:3}},{upperLabel:{show:false},itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2}}]}]});};
-const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});
-io.observe(host);host.style.cursor='pointer';host.addEventListener('click',draw);addEventListener('resize',()=>chart?.resize());
-})();
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ F14 · rung histogram ════
-// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单
-// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,
-// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。
-(()=>{
-const BIN=[6,14,22,19,13,9,6,4,3,2,1,1];       // 2h bins, sums to 100
-// 序数梯:箱越高绿越深(票堆的大小是序数维度,同 glance-palm 的 rampAt 逻辑)
-const BMAX=Math.max(...BIN);
-const bcol=v=>v>BMAX*.75?INK:v>BMAX*.45?'#77835A':v>BMAX*.2?'#929960':'#ACAD79';
-obsReveal('histo',s=>{
-  const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;
-  BIN.forEach((v,i)=>{
-    const x=X0+i*PW+PW/2;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:bcol(v),'stroke-width':1.8,
-        opacity:.68+rnd(k+2,i+4)*.32,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'rgba(88,64,46,.32)',
-        class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-    }
-  });
-  // peak label
-  const pk=BIN.indexOf(Math.max(...BIN));
-  const px=X0+pk*PW+PW/2;
-  const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,
-    'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);
-  tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);
-  // bin edges: the continuous axis is the histogram's identity
-  el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let e=0;e<=12;e++){
-    const x=X0+e*PW;
-    el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'rgba(88,64,46,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${e*.02}s`});
-    if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');
-  }
-  // median flag: cumulative crosses 50 inside bin 2
-  let acc=0,mb=0;for(let i=0;i<12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}
-  const mx=X0+mb*PW+PW*.7;
-  el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#929960','stroke-width':1.62,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'rgba(88,64,46,.72)',
-    class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE RUNG = ONE TICKET IN A HUNDRED · DEEPER GREEN = TALLER PILE · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ F15 · tick box ════
-// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,
-// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。
-(()=>{
-const G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],
-         ['PRO',[.8,2.1,3.3,5,7.8],[10.5]],
-         ['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],
-         ['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];
-const SHADE=[INK,'#77835A','#929960','#ACAD79'];
-obsReveal('boxplot',s=>{
-  const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);
-  const colX=g=>78+g*84,BW=24;
-  [0,6,12,18,24].forEach(h=>{
-    el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(88,64,46,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${h*.008}s`});
-    txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.008}s`},h+'h');
-  });
-  G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{
-    const x=colX(g);
-    el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:SHADE[g],'stroke-width':1.44,
-      pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});
-    [mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),
-      stroke:SHADE[g],'stroke-width':1.8,class:'fade',style:`animation-delay:${.2+g*.12}s`}));
-    const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,
-      fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});
-    tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);
-    el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,
-      class:'fade',style:`animation-delay:${.5+g*.12}s`});
-    txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');
-    outs.forEach((o,k)=>{
-      const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,
-        stroke:SHADE[g],'stroke-width':1.3,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});
-      tip(d,`${name} outlier — ${o}h`);
-    });
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.12}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · HOLLOW = OUTLIER · DARKEST = FASTEST');
-});
-})();
-
-// ════ F16 · stream ribbon ════
-// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴
-// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。
-// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。
-(()=>{
-const N=48;
-const mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>
-  Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));
-// 三条带走绿→黄梯:现任霸主最深绿,退场的 LEGACY 落在麦黄(不再用灰褐透明档)
-const SERIES=[
-  ['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'#F2D17E'],
-  ['BOARDS',mk(26,.10,11,4.2,5),'#929960'],
-  ['FLOWS',mk(12,.78,10,3.1,8),INK],
-];
-obsReveal('stream',s=>{
-  const X0=36,X1=744,CY=138,SC=1.15;
-  const x=t=>X0+t/(N-1)*(X1-X0);
-  const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));
-  // stacked offsets, silhouette baseline: top of stack starts at -total/2
-  const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);
-  const smooth=pts=>{
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let k=1;k<pts.length-1;k++){
-      const p=pts[k],q=pts[k+1];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;
-  };
-  let run=y0.slice();
-  SERIES.forEach(([name,vals,shade],si)=>{
-    const top=run.map((v,t)=>[x(t),v]);
-    const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);
-    run=bot.map(p=>p[1]);
-    const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';
-    const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,
-      class:'fade',style:`animation-delay:${si*.16}s`});
-    tip(band,`${name} — weekly active accounts, 48 weeks`);
-    // label at the band's widest week — clamped off both ends so the
-    // paper-colored text never spills past the band onto paper (invisible)
-    let wk=4;vals.forEach((v,t)=>{if(t>=4&&t<=N-6&&v>vals[wk])wk=t});
-    const midY=(top[wk][1]+bot[wk][1])/2;
-    const dark=shade===INK;
-    txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',
-      fill:dark?PAPER:'#58402E','letter-spacing':'.06em',
-      class:'fade',style:`animation-delay:${.5+si*.14}s`},name);
-  });
-  // month floor: barcode ticks + labels every 8 weeks
-  const base=252;
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let t=0;t<N;t++)
-    el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'rgba(88,64,46,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${t*.006}s`});
-  ['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>
-    txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'BAND WIDTH = WEEKLY ACTIVES · DARKEST = TODAY’S LEADER · THE RIVER IS THE TOTAL');
-});
-})();
-
-// ════ F17 · candlestick ════
-// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,
-// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。
-// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。
-(()=>{
-const N=30;
-// deterministic random walk with a dip-and-recover arc
-const DAYS=[];
-let px=52;
-for(let d=0;d<N;d++){
-  const drift=d<10?.4:d<18?-1.5:1.3;
-  const open=px;
-  const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);
-  const hi=Math.max(open,close)+rnd(d+2,7)*2.6;
-  const lo=Math.min(open,close)-rnd(d+3,11)*2.6;
-  DAYS.push([open,close,hi,lo]);
-  px=close;
-}
-obsReveal('candle',s=>{
-  const X0=40,PW=11.2,BASE=258,TOP=44;
-  const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));
-  const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);
-  const x=d=>X0+d*PW+PW/2;
-  // price grid
-  for(let g=0;g<=4;g++){
-    const v=vmin+g/4*(vmax-vmin);
-    el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'rgba(88,64,46,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${g*.03}s`});
-    txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${g*.03}s`},'$'+Math.round(v));
-  }
-  let hiD=0,loD=0;
-  DAYS.forEach(([o,c,h,l],d)=>{
-    if(h>DAYS[hiD][2])hiD=d;
-    if(l<DAYS[loD][3])loD=d;
-  });
-  DAYS.forEach(([o,c,h,l],d)=>{
-    const cx=x(d),up=c>=o;
-    const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));
-    // wick: one hairline, full reach
-    el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'rgba(88,64,46,.72)','stroke-width':1.26,
-      class:'fade',style:`animation-delay:${d*.03}s`});
-    // body: capsule; 涨跌是两个类目——深绿 = 收跌,麦黄 = 收涨(palm 的双分类位)
-    const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,
-      ...(up?{fill:'#F2D17E',stroke:'#77835A','stroke-width':1}:{fill:INK}),
-      class:'fade',style:`animation-delay:${.05+d*.03}s`});
-    tip(body,`Day ${d+1} — open $${o.toFixed(1)} · close $${c.toFixed(1)} · high $${h.toFixed(1)} · low $${l.toFixed(1)}`);
-    // extremes, labeled with hairline flags
-    if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(h));
-    if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':8,'font-weight':800,fill:'rgba(88,64,46,.72)','text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(l));
-  });
-  // week ticks on the floor
-  el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'rgba(88,64,46,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>
-    txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade'},w));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'GREEN BODY = CLOSED DOWN · WHEAT = CLOSED UP · WICK = THE DAY’S FULL REACH');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 893
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/basics-porcelain.html

@@ -1,893 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Basics F1–F17 × PORCELAIN</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<style>
-  :root{--bg:#F7F2EB;--ink:#081F5C;--muted:rgba(8,31,92,.60);--faint:rgba(8,31,92,.32);--grid:rgba(8,31,92,.16)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed var(--faint);color:var(--muted)}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  .chartbox{width:100%;height:360px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-  .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>基础型组 F1–F17 × PORCELAIN</h1>
-  <p><span class="chip" style="background:#334EAC"></span><span class="chip" style="background:#7096D1"></span><span class="chip" style="background:#BAD6EB"></span><span class="chip" style="background:#D0E3FF"></span><span class="chip" style="background:#081F5C"></span><span class="chip" style="background:#F7F2EB"></span> &nbsp;青瓷蓝家族 · Royal 当主墨,Midnight 当文字与最深档,九色号全原厂。几何、动画、数据与 basics-gallery 一致;彩色版统一加墨(线宽 ×1.8 / 透明度地板 .85,dot heat 除外——它的墨本来就够)。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 柱状</span>
-    <h2>Revenue by plan, rung by rung</h2>
-    <div class="sub">one rung = one $k of MRR · the bar is a ladder you can count</div>
-    <svg id="rungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG BARS · PORCELAIN · BILLING</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 折线</span>
-    <h2>Thirty days of sign-ups</h2>
-    <div class="sub">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest</div>
-    <svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE LINE · PORCELAIN · GROWTH</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 面积</span>
-    <h2>Concurrent users, filled with days</h2>
-    <div class="sub">one hairline = one day, floor to peak · the area is made of days, not paint</div>
-    <svg id="hairarea" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE AREA · PORCELAIN · LIVE OPS</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 环形</span>
-    <h2>Where the traffic comes from</h2>
-    <div class="sub">one tick = one percent · four sources shade the dial · read it like a clock</div>
-    <svg id="tickdonut" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK DONUT · PORCELAIN · ACQUISITION</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 横向条形</span>
-    <h2>Six teams, shipped and counted</h2>
-    <div class="sub">one tick = one release this quarter · the row is a queue, not a bar</div>
-    <svg id="tickrows" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK ROWS · PORCELAIN · RELEASE LOG</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 分组柱</span>
-    <h2>This year against last, plan by plan</h2>
-    <div class="sub">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k</div>
-    <svg id="pairrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PAIRED RUNGS · PORCELAIN · BILLING YoY</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 堆叠柱</span>
-    <h2>Where each region's revenue sits</h2>
-    <div class="sub">one rung = $1k · three products stack up the ladder, strongest carries the most</div>
-    <svg id="stackrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">STACKED RUNGS · PORCELAIN · REVENUE MIX</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 散点</span>
-    <h2>Price against satisfaction, twelve products</h2>
-    <div class="sub">every dot hangs a plumb line to the floor · read x by the line, y by the height</div>
-    <svg id="plumb" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PLUMB SCATTER · PORCELAIN · PRODUCT PANEL</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 瀑布</span>
-    <h2>From gross to net, step by step</h2>
-    <div class="sub">one rung = $1k · solid rungs add, dashed rungs take away · the ladder walks down</div>
-    <svg id="fall" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG WATERFALL · PORCELAIN · P&amp;L</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 热力</span>
-    <h2>When support gets loud</h2>
-    <div class="sub">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible</div>
-    <svg id="dotheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT HEAT · PORCELAIN · SUPPORT DESK</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 进度</span>
-    <h2>How far to the quarter's goal</h2>
-    <div class="sub">one tick = one percent of target · inked = earned · the dial reads left to right</div>
-    <svg id="gauge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK GAUGE · PORCELAIN · QUARTERLY TARGET</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 哑铃对比</span>
-    <h2>Onboarding, before and after the redesign</h2>
-    <div class="sub">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable</div>
-    <svg id="dumbbell" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DUMBBELL QUEUE · PORCELAIN · UX TIMING</div>
-  </div>
-
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · 矩形树图</span>
-    <h2>Where the work went</h2>
-    <div class="sub">1,280 shipped hours · area = effort · blue lightness = parent portfolio share</div>
-    <div id="treemap" class="chartbox" role="img" aria-label="Porcelain treemap of shipped hours nested by product family"></div>
-    <div class="src">NESTED TREEMAP · PORCELAIN · WORK PORTFOLIO</div>
-  </div>
-
-
-  <!-- F14 · rung histogram -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 直方图</span>
-    <h2>Most tickets resolve within six hours</h2>
-    <div class="sub">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile</div>
-    <svg id="histo" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG HISTOGRAM · PORCELAIN · SUPPORT DESK</div>
-  </div>
-
-  <!-- F15 · tick box -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 箱线图</span>
-    <h2>Reply times, boxed by plan</h2>
-    <div class="sub">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed</div>
-    <svg id="boxplot" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK BOX · PORCELAIN · SUPPORT DESK</div>
-  </div>
-
-  <!-- F16 · stream ribbon -->
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · STREAMGRAPH</span>
-    <h2>Three products trade the same river</h2>
-    <div class="sub">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows</div>
-    <svg id="stream" viewBox="0 0 840 300" preserveAspectRatio="xMidYMid meet" style="max-height:300px"></svg>
-    <div class="src">STREAM RIBBON · PORCELAIN · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- F17 · candlestick -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · K 线</span>
-    <h2>Six weeks of the token, candle by candle</h2>
-    <div class="sub">body = open to close · wick = the day's full reach · ink = closed down · hollow = closed up</div>
-    <svg id="candle" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">CANDLESTICK · PORCELAIN · MARKET DATA</div>
-  </div>
-
-</div>
-<script>
-// ── PORCELAIN 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F7F2EB',TXT='#081F5C',MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',FAINT='rgba(8,31,92,.32)';
-const FLOOR='rgba(8,31,92,.24)',QUIET='rgba(8,31,92,.15)',TRACK='rgba(8,31,92,.12)',GRID='rgba(8,31,92,.16)';
-const DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C';
-const FAINTDATA='#BAD6EB',BEAD='#7096D1';
-const CAT4=["#081F5C", "#334EAC", "#7096D1", "#BAD6EB"],CAT3=["#334EAC", "#7096D1", "#BAD6EB"],CAT3L=["#334EAC", "#7096D1", "rgba(8,31,92,.6)"],HEAT=["#334EAC", "#7096D1", "#BAD6EB"];
-const HEROMODE=false; // 黑体红睛模式:每张图一个主角元素整体走 HERO
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ B1 · rung bars ════
-(()=>{
-const D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];
-obsReveal('rungs',s=>{
-  const x0=i=>56+i*56,base=266,step=5.6,HW=14;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i);
-    const colInk=(HEROMODE&&i===0)?HERO:DATA;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — $${v}k MRR`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ B2 · hairline line ════
-(()=>{
-const N=30;
-const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
-obsReveal('dayline',s=>{
-  const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===5||d%7===6,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?5.2:3.1,
-      fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
-});
-})();
-
-// ════ B3 · hairline area ════
-(()=>{
-const N=45;
-const val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;
-obsReveal('hairarea',s=>{
-  const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;
-  el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const peak=vs.indexOf(Math.max(...vs));
-  vs.forEach((v,d)=>{
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),
-      stroke:d===peak?HERO:DATA,'stroke-width':d===peak?1.6:1,
-      opacity:d===peak?1:.8+rnd(d+1,7)*.2,
-      class:'fade',style:`animation-delay:${d*.014}s`});
-  });
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2,pathLength:1,
-    class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});
-  const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:HERO,
-    class:'pop',style:'animation-delay:1.2s'});
-  tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);
-  txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1.3s`,
-    class:'fade'},Math.round(vs[peak])+'k');
-  [[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
-    'ONE HAIRLINE = ONE DAY, FLOOR TO PEAK');
-});
-})();
-
-// ════ B4 · tick donut ════
-(()=>{
-const D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];
-obsReveal('tickdonut',s=>{
-  const cx=200,cy=148,R0=64;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=12+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of traffic`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
-  txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');
-});
-})();
-
-// ════ C1 · tick rows ════
-(()=>{
-const D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];
-obsReveal('tickrows',s=>{
-  const y0=i=>52+i*44,X0=104,PX=6.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i);
-    txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    const rowInk=(HEROMODE&&i===0)?HERO:DATA;
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:rowInk,'stroke-width':1.7,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name} — ${v} releases`);
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = ONE RELEASE · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ C2 · paired rungs ════
-(()=>{
-const D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];
-obsReveal('pairrungs',s=>{
-  const x0=i=>64+i*66,base=258,step=5.4,HW=10;
-  D.forEach(([name,was,now],i)=>{
-    const xa=x0(i)-13,xb=x0(i)+13;
-    for(let k=0;k<was;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;
-      el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.8,
-        opacity:.75+rnd(k+2,i+3)*.25,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-    }
-    for(let k=0;k<now;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;
-      el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+8)*.15,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});
-    }
-    const topB=base-(now-1)*step;
-    const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:HEROMODE?HERO:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},now);
-    tip(num,`${name} — $${was}k → $${now}k`);
-    txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:FAINTDATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},was);
-    txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'FAINT = 2025 · SOLID = 2026 · ONE RUNG = $1K');
-});
-})();
-
-// ════ C3 · stacked rungs ════
-(()=>{
-const D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];
-const SEG=['CORE','ADD-ONS','SERVICES'];
-obsReveal('stackrungs',s=>{
-  const x0=i=>72+i*76,base=262,step=5.2,HW=13;
-  D.forEach(([name,segs],i)=>{
-    const x=x0(i);let k0=0;
-    segs.forEach((v,si)=>{
-      for(let k=0;k<v;k++){
-        const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;
-        el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:CAT3[si],'stroke-width':1.8,
-          opacity:.85+rnd(k+2,i+si+4)*.15,class:'fade',
-          style:`animation-delay:${i*.09+(k0+k)*.012}s`});
-      }
-      const midY=base-(k0+v/2+si)*step;
-      const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:CAT3L[si],
-        class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);
-      tip(lab,`${name} ${SEG[si]} — $${v}k`);
-      k0+=v;
-    });
-    const total=segs[0]+segs[1]+segs[2];
-    txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.6+i*.09}s`},total);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.09}s`},name);
-  });
-  el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'STRONGEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');
-});
-})();
-
-// ════ C4 · plumb scatter ════
-(()=>{
-const P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],
-         ['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],
-         ['Hub',76,32],['Forms',34,38]];
-obsReveal('plumb',s=>{
-  const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;
-  for(let g=0;g<=20;g++){
-    const x=X0+g/20*(X1-X0);
-    el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${g*.01}s`});
-  }
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'CHEAP');
-  txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',
-    class:'fade'},'PREMIUM');
-  const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]<a[2]?b:a);
-  P.forEach(([name,px,sat],i)=>{
-    const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];
-    el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1,
-      opacity:.75,class:'fade',style:`animation-delay:${.2+i*.05}s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:hero?5.4:3.4,fill:hero?HERO:DATA,
-      class:'pop',style:`animation-delay:${.25+i*.05}s`});
-    tip(dot,`${name} — price ${px} · satisfaction ${sat}`);
-    if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.8}s`,
-      class:'fade'},`${name} · ${sat}`);
-  });
-  txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:FAINT,
-    transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');
-});
-})();
-
-// ════ C5 · rung waterfall ════
-(()=>{
-const D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];
-obsReveal('fall',s=>{
-  const x0=i=>58+i*72,step=5.2,HW=11;
-  let lv=0;const rows=[];
-  D.forEach(([name,v],i)=>{
-    if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}
-    else if(name==='NET'){rows.push([name,lv,0,lv])}
-    else{rows.push([name,v,lv+v,lv]);lv=lv+v}
-  });
-  const base=252,yOf=k=>base-k*step;
-  rows.forEach(([name,v,lo,hi],i)=>{
-    const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v<0&&!isTotal;
-    const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);
-    const colInk=(HEROMODE&&name==='NET')?HERO:DATA;
-    for(let k=0;k<n;k++){
-      const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;
-      if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA2,'stroke-width':1.8,
-        'stroke-dasharray':'2.5 2.5',opacity:.85,class:'fade',
-        style:`animation-delay:${i*.12+k*.014}s`});
-      else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});
-    }
-    if(i<rows.length-1){
-      const nx=x0(i+1);
-      const lvl=isTotal?v:(neg?lo:hi);
-      el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:FAINT,
-        'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',
-        style:`animation-delay:${.3+i*.12}s`});
-    }
-    const topY=yOf(Math.max(from,to));
-    const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-      fill:neg?DATA2:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));
-    tip(num,`${name} — ${neg?'−':''}$${Math.abs(v)}k`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'SOLID RUNGS ADD · DASHED RUNGS TAKE AWAY · ONE RUNG = $1K');
-});
-})();
-
-// ════ C6 · dot heat ════
-(()=>{
-const DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-obsReveal('dotheat',s=>{
-  const x0=j=>64+j*27,y0=i=>58+i*30;
-  let max=0,mi=0,mj=0;
-  const v=(i,j)=>{
-    const day=i<5?1:.32;
-    const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5);
-    const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);
-    return Math.round(raw);
-  };
-  for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  for(let i=0;i<7;i++){
-    txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
-    for(let j=0;j<12;j++){
-      const t=v(i,j),x=x0(j),y=y0(i);
-      if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
-      const hero=i===mi&&j===mj;
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
-        fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
-      tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);
-      if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
-        stroke:HERO,'stroke-width':1,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:1s'});
-    }
-  }
-  for(let j=0;j<12;j+=2)
-    txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);
-});
-})();
-
-// ════ C7 · tick gauge ════
-(()=>{
-const GOAL=73;
-obsReveal('gauge',s=>{
-  const cx=200,cy=190,R0=104,A0=-195,SW=210;
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?15+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?DATA:FLOOR,'stroke-width':inked?1.9:.6,
-      class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);
-    el(s,'circle',{cx:dx,cy:dy,r:1,fill:FAINT,class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:3.2,fill:HERO,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},GOAL+'%');
-  tip(num,`$730k of the $1M quarter target`);
-  txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE TICK = 1% OF TARGET · INKED = EARNED');
-});
-})();
-
-// ════ C8 · dumbbell queue ════
-(()=>{
-const D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];
-obsReveal('dumbbell',s=>{
-  const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);
-  D.forEach(([name,was,now],i)=>{
-    const y=y0(i),xa=mapX(was),xb=mapX(now);
-    txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:TRACK,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    const n=was-now;
-    for(let k=0;k<n;k++){
-      const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;
-      el(s,'circle',{cx:x,cy:yy,r:2.1+rnd(k+2,i+4)*.9,fill:BEAD,opacity:.95,
-        class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});
-    }
-    el(s,'circle',{cx:xa,cy:y,r:4.8,fill:BG,stroke:DATA,'stroke-width':1.7,
-      class:'pop',style:`animation-delay:${.2+i*.08}s`});
-    const after=el(s,'circle',{cx:xb,cy:y,r:5.2,fill:HERO,
-      class:'pop',style:`animation-delay:${.6+i*.08}s`});
-    tip(after,`${name} — ${was} min → ${now} min`);
-    txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:FAINTDATA,
-      class:'fade',style:`animation-delay:${.3+i*.08}s`},was);
-    txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:TXT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${.7+i*.08}s`},now);
-  });
-  txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'FASTER ←');
-  txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',
-    class:'fade'},'MINUTES');
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · SOLID = AFTER');
-});
-})();
-
-// ════ C9 · nested treemap ════
-(()=>{
-const TOTAL=1280;
-const leaf=(name,value,color,labelColor=BG)=>({name,value,itemStyle:{color},label:{color:labelColor}});
-const TREE_DATA=[
-  {name:'PRODUCT · 50%',children:[leaf('Core app',260,HERO),leaf('Collaboration',190,HERO),leaf('Search',110,HERO),leaf('Mobile',80,HERO)]},
-  {name:'PLATFORM · 31%',children:[leaf('AI systems',170,DATA),leaf('Data infra',120,DATA),leaf('APIs',105,DATA)]},
-  {name:'GROWTH · 19%',children:[leaf('Acquisition',90,FAINTDATA,TXT),leaf('Retention',80,FAINTDATA,TXT),leaf('Onboarding',75,FAINTDATA,TXT)]},
-];
-const host=document.getElementById('treemap');let chart;
-const draw=()=>{chart=chart||echarts.init(host,null,{renderer:'svg'});chart.clear();chart.setOption({
-  animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},
-  tooltip:{backgroundColor:BG,borderColor:TXT,borderWidth:1,textStyle:{color:TXT,fontSize:12},
-    formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}<br><b>${i.value} hours</b> · ${(i.value/TOTAL*100).toFixed(1)}%`},
-  series:[{type:'treemap',data:TREE_DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,
-    label:{show:true,position:'insideTopLeft',padding:[10,8],color:TXT,fontSize:12,fontWeight:700,lineHeight:18,
-      formatter:p=>p.treePathInfo.length<=1?'':p.treePathInfo.length===2?p.name:`${p.name}\n${p.value} h`},
-    upperLabel:{show:true,height:32,padding:[0,10],color:TXT,fontSize:10,fontWeight:700,backgroundColor:BG},
-    itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2},emphasis:{focus:'ancestor',itemStyle:{borderColor:TXT,borderWidth:1.5}},
-    levels:[{itemStyle:{borderWidth:0,gapWidth:5}},{upperLabel:{show:true,height:32},itemStyle:{borderColor:BG,borderWidth:3,gapWidth:3}},{upperLabel:{show:false},itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2}}]}]});};
-const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});
-io.observe(host);host.style.cursor='pointer';host.addEventListener('click',draw);addEventListener('resize',()=>chart?.resize());
-})();
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#081F5C',PAPER='#F7F2EB',MUTED='rgba(8,31,92,.60)',GRID='rgba(8,31,92,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ F14 · rung histogram ════
-// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单
-// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,
-// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。
-(()=>{
-const BIN=[6,14,22,19,13,9,6,4,3,2,1,1];       // 2h bins, sums to 100
-obsReveal('histo',s=>{
-  const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;
-  BIN.forEach((v,i)=>{
-    const x=X0+i*PW+PW/2;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1.8,
-        opacity:.55+rnd(k+2,i+4)*.45,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'rgba(8,31,92,.32)',
-        class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-    }
-  });
-  // peak label
-  const pk=BIN.indexOf(Math.max(...BIN));
-  const px=X0+pk*PW+PW/2;
-  const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,
-    'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);
-  tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);
-  // bin edges: the continuous axis is the histogram's identity
-  el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let e=0;e<=12;e++){
-    const x=X0+e*PW;
-    el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'rgba(8,31,92,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${e*.02}s`});
-    if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');
-  }
-  // median flag: cumulative crosses 50 inside bin 2
-  let acc=0,mb=0;for(let i=0;i<12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}
-  const mx=X0+mb*PW+PW*.7;
-  el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#7096D1','stroke-width':1.62,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'rgba(8,31,92,.72)',
-    class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE RUNG = ONE TICKET IN A HUNDRED · BINS OF TWO HOURS · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ F15 · tick box ════
-// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,
-// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。
-(()=>{
-const G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],
-         ['PRO',[.8,2.1,3.3,5,7.8],[10.5]],
-         ['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],
-         ['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];
-const SHADE=[INK,'#334EAC','#7096D1','#BAD6EB'];
-obsReveal('boxplot',s=>{
-  const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);
-  const colX=g=>78+g*84,BW=24;
-  [0,6,12,18,24].forEach(h=>{
-    el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(8,31,92,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${h*.008}s`});
-    txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.008}s`},h+'h');
-  });
-  G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{
-    const x=colX(g);
-    el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:'#7096D1','stroke-width':1.44,
-      pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});
-    [mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),
-      stroke:'#7096D1','stroke-width':1.8,class:'fade',style:`animation-delay:${.2+g*.12}s`}));
-    const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,
-      fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});
-    tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);
-    el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,
-      class:'fade',style:`animation-delay:${.5+g*.12}s`});
-    txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');
-    outs.forEach((o,k)=>{
-      const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,
-        stroke:'rgba(8,31,92,.72)','stroke-width':1.98,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});
-      tip(d,`${name} outlier — ${o}h`);
-    });
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.12}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · HOLLOW = OUTLIER · DARKEST = FASTEST');
-});
-})();
-
-// ════ F16 · stream ribbon ════
-// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴
-// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。
-// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。
-(()=>{
-const N=48;
-const mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>
-  Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));
-const SERIES=[
-  ['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'rgba(8,31,92,.32)'],
-  ['BOARDS',mk(26,.10,11,4.2,5),'#7096D1'],
-  ['FLOWS',mk(12,.78,10,3.1,8),INK],
-];
-obsReveal('stream',s=>{
-  const X0=36,X1=744,CY=138,SC=1.15;
-  const x=t=>X0+t/(N-1)*(X1-X0);
-  const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));
-  // stacked offsets, silhouette baseline: top of stack starts at -total/2
-  const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);
-  const smooth=pts=>{
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let k=1;k<pts.length-1;k++){
-      const p=pts[k],q=pts[k+1];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;
-  };
-  let run=y0.slice();
-  SERIES.forEach(([name,vals,shade],si)=>{
-    const top=run.map((v,t)=>[x(t),v]);
-    const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);
-    run=bot.map(p=>p[1]);
-    const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';
-    const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,
-      class:'fade',style:`animation-delay:${si*.16}s`});
-    tip(band,`${name} — weekly active accounts, 48 weeks`);
-    // label at the band's widest week — clamped off both ends so the
-    // paper-colored text never spills past the band onto paper (invisible)
-    let wk=4;vals.forEach((v,t)=>{if(t>=4&&t<=N-6&&v>vals[wk])wk=t});
-    const midY=(top[wk][1]+bot[wk][1])/2;
-    const dark=shade===INK||shade==='#334EAC';
-    txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',
-      fill:dark?PAPER:'#334EAC','letter-spacing':'.06em',
-      class:'fade',style:`animation-delay:${.5+si*.14}s`},name);
-  });
-  // month floor: barcode ticks + labels every 8 weeks
-  const base=252;
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let t=0;t<N;t++)
-    el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'rgba(8,31,92,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${t*.006}s`});
-  ['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>
-    txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'BAND WIDTH = WEEKLY ACTIVES · DARKEST = TODAY’S LEADER · THE RIVER IS THE TOTAL');
-});
-})();
-
-// ════ F17 · candlestick ════
-// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,
-// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。
-// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。
-(()=>{
-const N=30;
-// deterministic random walk with a dip-and-recover arc
-const DAYS=[];
-let px=52;
-for(let d=0;d<N;d++){
-  const drift=d<10?.4:d<18?-1.5:1.3;
-  const open=px;
-  const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);
-  const hi=Math.max(open,close)+rnd(d+2,7)*2.6;
-  const lo=Math.min(open,close)-rnd(d+3,11)*2.6;
-  DAYS.push([open,close,hi,lo]);
-  px=close;
-}
-obsReveal('candle',s=>{
-  const X0=40,PW=11.2,BASE=258,TOP=44;
-  const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));
-  const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);
-  const x=d=>X0+d*PW+PW/2;
-  // price grid
-  for(let g=0;g<=4;g++){
-    const v=vmin+g/4*(vmax-vmin);
-    el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'rgba(8,31,92,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${g*.03}s`});
-    txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${g*.03}s`},'$'+Math.round(v));
-  }
-  let hiD=0,loD=0;
-  DAYS.forEach(([o,c,h,l],d)=>{
-    if(h>DAYS[hiD][2])hiD=d;
-    if(l<DAYS[loD][3])loD=d;
-  });
-  DAYS.forEach(([o,c,h,l],d)=>{
-    const cx=x(d),up=c>=o;
-    const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));
-    // wick: one hairline, full reach
-    el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'rgba(8,31,92,.72)','stroke-width':1.26,
-      class:'fade',style:`animation-delay:${d*.03}s`});
-    // body: capsule; hollow = up, ink = down
-    const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,
-      ...(up?{fill:PAPER,stroke:INK,'stroke-width':1.98}:{fill:INK}),
-      class:'fade',style:`animation-delay:${.05+d*.03}s`});
-    tip(body,`Day ${d+1} — open $${o.toFixed(1)} · close $${c.toFixed(1)} · high $${h.toFixed(1)} · low $${l.toFixed(1)}`);
-    // extremes, labeled with hairline flags
-    if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(h));
-    if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':8,'font-weight':800,fill:'rgba(8,31,92,.72)','text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(l));
-  });
-  // week ticks on the floor
-  el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'rgba(8,31,92,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>
-    txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade'},w));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'INK BODY = CLOSED DOWN · HOLLOW = CLOSED UP · WICK = THE DAY’S FULL REACH');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 899
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/basics-wire.html

@@ -1,899 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Basics F1–F17 × WIRE</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<style>
-  :root{--bg:#F0F0EE;--ink:#1F1E1C;--muted:rgba(31,30,28,.60);--faint:rgba(31,30,28,.32);--grid:rgba(31,30,28,.16)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1100px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed var(--faint);color:var(--muted)}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  .chartbox{width:100%;height:360px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-  .pagehead{max-width:1100px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>基础型组 F1–F17 × WIRE</h1>
-  <p><span class="chip" style="background:#F5572F"></span><span class="chip" style="background:#22211F"></span><span class="chip" style="background:#8F8E86"></span><span class="chip" style="background:#C0BFB7"></span><span class="chip" style="background:#DBDAD3"></span> &nbsp;编辑部红 · 黑灰为体,每张图一个主角元素上红(最大柱/峰值/NET/改版后)。几何、动画、数据与 basics-gallery 一致;彩色版统一加墨(线宽 ×1.8 / 透明度地板 .85,dot heat 除外——它的墨本来就够)。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 柱状</span>
-    <h2>Revenue by plan, rung by rung</h2>
-    <div class="sub">one rung = one $k of MRR · the bar is a ladder you can count</div>
-    <svg id="rungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG BARS · WIRE · BILLING</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 折线</span>
-    <h2>Thirty days of sign-ups</h2>
-    <div class="sub">one dot = one day · hollow = weekend · the barcode floor keeps the calendar honest</div>
-    <svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE LINE · WIRE · GROWTH</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 面积</span>
-    <h2>Concurrent users, filled with days</h2>
-    <div class="sub">one hairline = one day, floor to peak · the area is made of days, not paint</div>
-    <svg id="hairarea" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HAIRLINE AREA · WIRE · LIVE OPS</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 环形</span>
-    <h2>Where the traffic comes from</h2>
-    <div class="sub">one tick = one percent · four sources shade the dial · read it like a clock</div>
-    <svg id="tickdonut" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK DONUT · WIRE · ACQUISITION</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 横向条形</span>
-    <h2>Six teams, shipped and counted</h2>
-    <div class="sub">one tick = one release this quarter · the row is a queue, not a bar</div>
-    <svg id="tickrows" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK ROWS · WIRE · RELEASE LOG</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 分组柱</span>
-    <h2>This year against last, plan by plan</h2>
-    <div class="sub">two ladders per plan · solid rungs = 2026 · faint rungs = 2025 · one rung = $1k</div>
-    <svg id="pairrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PAIRED RUNGS · WIRE · BILLING YoY</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 堆叠柱</span>
-    <h2>Where each region's revenue sits</h2>
-    <div class="sub">one rung = $1k · three products stack up the ladder, strongest carries the most</div>
-    <svg id="stackrungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">STACKED RUNGS · WIRE · REVENUE MIX</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 散点</span>
-    <h2>Price against satisfaction, twelve products</h2>
-    <div class="sub">every dot hangs a plumb line to the floor · read x by the line, y by the height</div>
-    <svg id="plumb" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PLUMB SCATTER · WIRE · PRODUCT PANEL</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 瀑布</span>
-    <h2>From gross to net, step by step</h2>
-    <div class="sub">one rung = $1k · solid rungs add, dashed rungs take away · the ladder walks down</div>
-    <svg id="fall" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG WATERFALL · WIRE · P&amp;L</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 热力</span>
-    <h2>When support gets loud</h2>
-    <div class="sub">7 days × 12 hours · dot area = tickets · biggest cell labeled · silence stays visible</div>
-    <svg id="dotheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT HEAT · WIRE · SUPPORT DESK</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 进度</span>
-    <h2>How far to the quarter's goal</h2>
-    <div class="sub">one tick = one percent of target · inked = earned · the dial reads left to right</div>
-    <svg id="gauge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK GAUGE · WIRE · QUARTERLY TARGET</div>
-  </div>
-
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 哑铃对比</span>
-    <h2>Onboarding, before and after the redesign</h2>
-    <div class="sub">hollow dot = before · solid dot = after · beads on the track = minutes saved, countable</div>
-    <svg id="dumbbell" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DUMBBELL QUEUE · WIRE · UX TIMING</div>
-  </div>
-
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · 矩形树图</span>
-    <h2>Where the work went</h2>
-    <div class="sub">1,280 shipped hours · area = effort · fluorescent orange = the single focal workstream</div>
-    <div id="treemap" class="chartbox" role="img" aria-label="Wire treemap of shipped hours nested by product family"></div>
-    <div class="src">NESTED TREEMAP · WIRE · WORK PORTFOLIO</div>
-  </div>
-
-
-  <!-- F14 · rung histogram -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 直方图</span>
-    <h2>Most tickets resolve within six hours</h2>
-    <div class="sub">one rung = one ticket in a hundred · bins of two hours · the ladder shows the pile</div>
-    <svg id="histo" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RUNG HISTOGRAM · WIRE · SUPPORT DESK</div>
-  </div>
-
-  <!-- F15 · tick box -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · 箱线图</span>
-    <h2>Reply times, boxed by plan</h2>
-    <div class="sub">box = the middle half of tickets · paper tick = median · hollow dot = an outlier nobody enjoyed</div>
-    <svg id="boxplot" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TICK BOX · WIRE · SUPPORT DESK</div>
-  </div>
-
-  <!-- F16 · stream ribbon -->
-  <div class="card wide">
-    <span class="badge">LUPI 基础型 · STREAMGRAPH</span>
-    <h2>Three products trade the same river</h2>
-    <div class="sub">48 weeks of usage share · band width = weekly active accounts · the river swells where the total grows</div>
-    <svg id="stream" viewBox="0 0 840 300" preserveAspectRatio="xMidYMid meet" style="max-height:300px"></svg>
-    <div class="src">STREAM RIBBON · WIRE · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- F17 · candlestick -->
-  <div class="card">
-    <span class="badge">LUPI 基础型 · K 线</span>
-    <h2>Six weeks of the token, candle by candle</h2>
-    <div class="sub">body = open to close · wick = the day's full reach · ink = closed down · hollow = closed up</div>
-    <svg id="candle" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">CANDLESTICK · WIRE · MARKET DATA</div>
-  </div>
-
-</div>
-<script>
-// ── WIRE 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F0F0EE',TXT='#1F1E1C',MUT='rgba(31,30,28,.60)',LAB='rgba(31,30,28,.72)',FAINT='rgba(31,30,28,.32)';
-const FLOOR='rgba(31,30,28,.24)',QUIET='rgba(31,30,28,.15)',TRACK='rgba(31,30,28,.12)',GRID='rgba(31,30,28,.16)';
-const DATA='#22211F',DATA2='#8F8E86',HERO='#F5572F';
-const FAINTDATA='#C0BFB7',BEAD='#8F8E86';
-const CAT4=["#F5572F", "#22211F", "#8F8E86", "#C0BFB7"],CAT3=["#F5572F", "#22211F", "#8F8E86"],CAT3L=["#F5572F", "#22211F", "#6E6D66"],HEAT=["#F5572F", "#6E6D66", "#C0BFB7"];
-const HEROMODE=true; // 黑体红睛模式:每张图一个主角元素整体走 HERO
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ B1 · rung bars ════
-(()=>{
-const D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];
-obsReveal('rungs',s=>{
-  const x0=i=>56+i*56,base=266,step=5.6,HW=14;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i);
-    const colInk=(HEROMODE&&i===0)?HERO:DATA;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — $${v}k MRR`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ B2 · hairline line ════
-(()=>{
-const N=30;
-const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
-obsReveal('dayline',s=>{
-  const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===5||d%7===6,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?5.2:3.1,
-      fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
-});
-})();
-
-// ════ B3 · hairline area ════
-(()=>{
-const N=45;
-const val=d=>34+26*Math.sin(d/7.2)+12*Math.sin(d/2.8)+rnd(d+1,3)*16;
-obsReveal('hairarea',s=>{
-  const x=d=>28+d*7.75,base=262,map=v=>base-v*2.35;
-  el(s,'line',{x1:22,y1:base,x2:378,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const peak=vs.indexOf(Math.max(...vs));
-  vs.forEach((v,d)=>{
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:map(v),
-      stroke:d===peak?HERO:DATA,'stroke-width':d===peak?1.6:1,
-      opacity:d===peak?1:.8+rnd(d+1,7)*.2,
-      class:'fade',style:`animation-delay:${d*.014}s`});
-  });
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2,pathLength:1,
-    class:'draw',style:'animation-delay:.4s;animation-duration:1.2s'});
-  const pd=el(s,'circle',{cx:x(peak),cy:map(vs[peak]),r:4.2,fill:HERO,
-    class:'pop',style:'animation-delay:1.2s'});
-  tip(pd,`Day ${peak+1} — ${Math.round(vs[peak])}k peak`);
-  txt(s,{x:x(peak),y:map(vs[peak])-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1.3s`,
-    class:'fade'},Math.round(vs[peak])+'k');
-  [[0,'MAY'],[22,'JUN'],[44,'JUL']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
-    'ONE HAIRLINE = ONE DAY, FLOOR TO PEAK');
-});
-})();
-
-// ════ B4 · tick donut ════
-(()=>{
-const D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];
-obsReveal('tickdonut',s=>{
-  const cx=200,cy=148,R0=64;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=12+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of traffic`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
-  txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');
-});
-})();
-
-// ════ C1 · tick rows ════
-(()=>{
-const D=[['PLATFORM',34],['GROWTH',28],['MOBILE',22],['INFRA',17],['ML',11],['DESIGN',8]];
-obsReveal('tickrows',s=>{
-  const y0=i=>52+i*44,X0=104,PX=6.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i);
-    txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+34*PX,y2:y+9,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    const rowInk=(HEROMODE&&i===0)?HERO:DATA;
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:rowInk,'stroke-width':1.7,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name} — ${v} releases`);
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = ONE RELEASE · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ C2 · paired rungs ════
-(()=>{
-const D=[['FREE',31,38],['STARTER',22,27],['PRO',16,22],['TEAM',13,16],['ENT',6,9]];
-obsReveal('pairrungs',s=>{
-  const x0=i=>64+i*66,base=258,step=5.4,HW=10;
-  D.forEach(([name,was,now],i)=>{
-    const xa=x0(i)-13,xb=x0(i)+13;
-    for(let k=0;k<was;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+2)*2.4;
-      el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.8,
-        opacity:.75+rnd(k+2,i+3)*.25,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-    }
-    for(let k=0;k<now;k++){
-      const y=base-k*step,w=HW-1.2+rnd(k+1,i+7)*2.4;
-      el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+8)*.15,class:'fade',style:`animation-delay:${.15+i*.08+k*.01}s`});
-    }
-    const topB=base-(now-1)*step;
-    const num=txt(s,{x:xb,y:topB-9,'font-size':10.5,'font-weight':800,fill:HEROMODE?HERO:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},now);
-    tip(num,`${name} — $${was}k → $${now}k`);
-    txt(s,{x:xa,y:base-(was-1)*step-9,'font-size':8.5,'font-weight':700,fill:FAINTDATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.5+i*.08}s`},was);
-    txt(s,{x:x0(i),y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'FAINT = 2025 · SOLID = 2026 · ONE RUNG = $1K');
-});
-})();
-
-// ════ C3 · stacked rungs ════
-(()=>{
-const D=[['NA',[18,11,7]],['EU',[14,9,5]],['APAC',[9,7,6]],['LATAM',[5,4,2]]];
-const SEG=['CORE','ADD-ONS','SERVICES'];
-obsReveal('stackrungs',s=>{
-  const x0=i=>72+i*76,base=262,step=5.2,HW=13;
-  D.forEach(([name,segs],i)=>{
-    const x=x0(i);let k0=0;
-    segs.forEach((v,si)=>{
-      for(let k=0;k<v;k++){
-        const y=base-(k0+k+si)*step,w=HW-1.4+rnd(k+1,i*3+si+2)*2.8;
-        el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:CAT3[si],'stroke-width':1.8,
-          opacity:.85+rnd(k+2,i+si+4)*.15,class:'fade',
-          style:`animation-delay:${i*.09+(k0+k)*.012}s`});
-      }
-      const midY=base-(k0+v/2+si)*step;
-      const lab=txt(s,{x:x+HW+7,y:midY+2.5,'font-size':8,'font-weight':800,fill:CAT3L[si],
-        class:'fade',style:`animation-delay:${.5+i*.09+si*.06}s`},v);
-      tip(lab,`${name} ${SEG[si]} — $${v}k`);
-      k0+=v;
-    });
-    const total=segs[0]+segs[1]+segs[2];
-    txt(s,{x,y:base-(k0+2)*step-8,'font-size':10.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.6+i*.09}s`},total);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.09}s`},name);
-  });
-  el(s,'line',{x1:36,y1:base+4,x2:364,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'STRONGEST = CORE · MID = ADD-ONS · PALE = SERVICES · ONE RUNG = $1K');
-});
-})();
-
-// ════ C4 · plumb scatter ════
-(()=>{
-const P=[['Editor',72,86],['Boards',58,74],['Docs',44,79],['Chat',38,62],['Flows',66,58],
-         ['Vault',82,71],['Pages',28,55],['Sync',52,49],['Grid',88,44],['Views',20,68],
-         ['Hub',76,32],['Forms',34,38]];
-obsReveal('plumb',s=>{
-  const X0=48,X1=368,base=258,mapX=p=>X0+p/100*(X1-X0),mapY=v=>base-(v-20)*2.6;
-  for(let g=0;g<=20;g++){
-    const x=X0+g/20*(X1-X0);
-    el(s,'line',{x1:x,y1:base,x2:x,y2:base-(g%5===0?7:4),stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${g*.01}s`});
-  }
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:X0,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'CHEAP');
-  txt(s,{x:X1,y:base+16,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',
-    class:'fade'},'PREMIUM');
-  const best=P.reduce((a,b)=>b[2]>a[2]?b:a),worst=P.reduce((a,b)=>b[2]<a[2]?b:a);
-  P.forEach(([name,px,sat],i)=>{
-    const x=mapX(px),y=mapY(sat),hero=name===best[0]||name===worst[0];
-    el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1,
-      opacity:.75,class:'fade',style:`animation-delay:${.2+i*.05}s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:hero?5.4:3.4,fill:hero?HERO:DATA,
-      class:'pop',style:`animation-delay:${.25+i*.05}s`});
-    tip(dot,`${name} — price ${px} · satisfaction ${sat}`);
-    if(hero)txt(s,{x,y:y-10,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.8}s`,
-      class:'fade'},`${name} · ${sat}`);
-  });
-  txt(s,{x:26,y:mapY(85),'font-size':7,'font-weight':600,fill:FAINT,
-    transform:`rotate(-90 26 ${mapY(85)})`,'text-anchor':'end','letter-spacing':'.08em',class:'fade'},'HAPPIER ↑');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'EVERY DOT HANGS A PLUMB LINE · READ X AT THE FLOOR');
-});
-})();
-
-// ════ C5 · rung waterfall ════
-(()=>{
-const D=[['GROSS',42,null],['REFUNDS',-6,null],['COGS',-11,null],['OPS',-8,null],['NET',17,'total']];
-obsReveal('fall',s=>{
-  const x0=i=>58+i*72,step=5.2,HW=11;
-  let lv=0;const rows=[];
-  D.forEach(([name,v],i)=>{
-    if(name==='GROSS'){rows.push([name,v,0,v]);lv=v}
-    else if(name==='NET'){rows.push([name,lv,0,lv])}
-    else{rows.push([name,v,lv+v,lv]);lv=lv+v}
-  });
-  const base=252,yOf=k=>base-k*step;
-  rows.forEach(([name,v,lo,hi],i)=>{
-    const x=x0(i),isTotal=name==='GROSS'||name==='NET',neg=v<0&&!isTotal;
-    const from=isTotal?0:lo,to=isTotal?v:hi,n=Math.abs(to-from);
-    const colInk=(HEROMODE&&name==='NET')?HERO:DATA;
-    for(let k=0;k<n;k++){
-      const y=yOf(from+k),w=HW-1.2+rnd(k+1,i+2)*2.4;
-      if(neg)el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA2,'stroke-width':1.8,
-        'stroke-dasharray':'2.5 2.5',opacity:.85,class:'fade',
-        style:`animation-delay:${i*.12+k*.014}s`});
-      else el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:colInk,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.12+k*.014}s`});
-    }
-    if(i<rows.length-1){
-      const nx=x0(i+1);
-      const lvl=isTotal?v:(neg?lo:hi);
-      el(s,'line',{x1:x+HW+2,y1:yOf(lvl),x2:nx-HW-2,y2:yOf(lvl),stroke:FAINT,
-        'stroke-width':.7,'stroke-dasharray':'2 3',class:'fade',
-        style:`animation-delay:${.3+i*.12}s`});
-    }
-    const topY=yOf(Math.max(from,to));
-    const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-      fill:neg?DATA2:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.12}s`},(neg?'−':'')+Math.abs(v));
-    tip(num,`${name} — ${neg?'−':''}$${Math.abs(v)}k`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.12}s`},name);
-  });
-  el(s,'line',{x1:30,y1:base+4,x2:370,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'SOLID RUNGS ADD · DASHED RUNGS TAKE AWAY · ONE RUNG = $1K');
-});
-})();
-
-// ════ C6 · dot heat ════
-(()=>{
-const DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-obsReveal('dotheat',s=>{
-  const x0=j=>64+j*27,y0=i=>58+i*30;
-  let max=0,mi=0,mj=0;
-  const v=(i,j)=>{
-    const day=i<5?1:.32;
-    const hour=Math.exp(-((j-4.6)**2)/7)+.7*Math.exp(-((j-8.4)**2)/5);
-    const raw=day*hour*22*(0.6+rnd(i*12+j+1,j+3)*.8);
-    return Math.round(raw);
-  };
-  for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  for(let i=0;i<7;i++){
-    txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
-    for(let j=0;j<12;j++){
-      const t=v(i,j),x=x0(j),y=y0(i);
-      if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
-      const hero=i===mi&&j===mj;
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
-        fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
-      tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);
-      if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
-        stroke:HERO,'stroke-width':1,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:1s'});
-    }
-  }
-  for(let j=0;j<12;j+=2)
-    txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);
-});
-})();
-
-// ════ C7 · tick gauge ════
-(()=>{
-const GOAL=73;
-obsReveal('gauge',s=>{
-  const cx=200,cy=190,R0=104,A0=-195,SW=210;
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?15+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?DATA:FLOOR,'stroke-width':inked?1.9:.6,
-      class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);
-    el(s,'circle',{cx:dx,cy:dy,r:1,fill:FAINT,class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:3.2,fill:HERO,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},GOAL+'%');
-  tip(num,`$730k of the $1M quarter target`);
-  txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'27 TICKS TO GO');
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE TICK = 1% OF TARGET · INKED = EARNED');
-});
-})();
-
-// ════ C8 · dumbbell queue ════
-(()=>{
-const D=[['INVITE FLOW',14,6],['FIRST BOARD',19,9],['IMPORT DATA',26,13],['TEAM SETUP',31,21],['GO LIVE',38,30]];
-obsReveal('dumbbell',s=>{
-  const y0=i=>56+i*46,X0=118,X1=364,mapX=v=>X0+(v-4)/36*(X1-X0);
-  D.forEach(([name,was,now],i)=>{
-    const y=y0(i),xa=mapX(was),xb=mapX(now);
-    txt(s,{x:108,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0-6,y1:y,x2:X1+6,y2:y,stroke:TRACK,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    const n=was-now;
-    for(let k=0;k<n;k++){
-      const t=(k+.5)/n,x=xb+t*(xa-xb),yy=y+(rnd(k+1,i+3)-.5)*2.6;
-      el(s,'circle',{cx:x,cy:yy,r:2.1+rnd(k+2,i+4)*.9,fill:BEAD,opacity:.95,
-        class:'pop',style:`animation-delay:${.3+i*.08+k*.03}s`});
-    }
-    el(s,'circle',{cx:xa,cy:y,r:4.8,fill:BG,stroke:DATA,'stroke-width':1.7,
-      class:'pop',style:`animation-delay:${.2+i*.08}s`});
-    const after=el(s,'circle',{cx:xb,cy:y,r:5.2,fill:HERO,
-      class:'pop',style:`animation-delay:${.6+i*.08}s`});
-    tip(after,`${name} — ${was} min → ${now} min`);
-    txt(s,{x:xa+10,y:y-8,'font-size':8.5,'font-weight':700,fill:FAINTDATA,
-      class:'fade',style:`animation-delay:${.3+i*.08}s`},was);
-    txt(s,{x:xb-10,y:y-8,'font-size':10,'font-weight':800,fill:TXT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${.7+i*.08}s`},now);
-  });
-  txt(s,{x:X0,y:290,'font-size':7,'font-weight':600,fill:FAINT,class:'fade'},'FASTER ←');
-  txt(s,{x:X1,y:290,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'end',
-    class:'fade'},'MINUTES');
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'ONE BEAD = ONE MINUTE SAVED · HOLLOW = BEFORE · SOLID = AFTER');
-});
-})();
-
-// ════ C9 · nested treemap ════
-(()=>{
-const TOTAL=1280;
-const leaf=(name,value,color,labelColor=BG)=>({name,value,itemStyle:{color},label:{color:labelColor}});
-const TREE_DATA=[
-  {name:'PRODUCT · 50%',children:[leaf('Core app',260,HERO),leaf('Collaboration',190,DATA),leaf('Search',110,DATA),leaf('Mobile',80,DATA)]},
-  {name:'PLATFORM · 31%',children:[leaf('AI systems',170,DATA2,TXT),leaf('Data infra',120,DATA2,TXT),leaf('APIs',105,DATA2,TXT)]},
-  {name:'GROWTH · 19%',children:[leaf('Acquisition',90,FAINTDATA,TXT),leaf('Retention',80,FAINTDATA,TXT),leaf('Onboarding',75,FAINTDATA,TXT)]},
-];
-const host=document.getElementById('treemap');let chart;
-const draw=()=>{chart=chart||echarts.init(host,null,{renderer:'svg'});chart.clear();chart.setOption({
-  animationDuration:900,animationEasing:'quarticOut',aria:{enabled:true},
-  tooltip:{backgroundColor:BG,borderColor:TXT,borderWidth:1,textStyle:{color:TXT,fontSize:12},
-    formatter:i=>`${i.treePathInfo.slice(1).map(d=>d.name.replace(/ · .*$/,'')).join(' / ')}<br><b>${i.value} hours</b> · ${(i.value/TOTAL*100).toFixed(1)}%`},
-  series:[{type:'treemap',data:TREE_DATA,top:0,right:0,bottom:0,left:0,roam:false,nodeClick:false,breadcrumb:{show:false},leafDepth:2,squareRatio:1.2,
-    label:{show:true,position:'insideTopLeft',padding:[10,8],color:TXT,fontSize:12,fontWeight:700,lineHeight:18,
-      formatter:p=>p.treePathInfo.length<=1?'':p.treePathInfo.length===2?p.name:`${p.name}\n${p.value} h`},
-    upperLabel:{show:true,height:32,padding:[0,10],color:TXT,fontSize:10,fontWeight:700,backgroundColor:BG},
-    itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2},emphasis:{focus:'ancestor',itemStyle:{borderColor:TXT,borderWidth:1.5}},
-    levels:[{itemStyle:{borderWidth:0,gapWidth:5}},{upperLabel:{show:true,height:32},itemStyle:{borderColor:BG,borderWidth:3,gapWidth:3}},{upperLabel:{show:false},itemStyle:{borderColor:BG,borderWidth:2,gapWidth:2}}]}]});};
-const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){draw();io.disconnect()}},{threshold:.25});
-io.observe(host);host.style.cursor='pointer';host.addEventListener('click',draw);addEventListener('resize',()=>chart?.resize());
-})();
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#22211F',PAPER='#F0F0EE',MUTED='rgba(31,30,28,.60)',GRID='rgba(31,30,28,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ F14 · rung histogram ════
-// 直方图的 Lupi Basics 皮:连续数值分箱,一箱一把梯子,1 档 = 1 张工单
-// (百分之一)。箱界写在梯脚之间——直方图的箱是区间不是类目,
-// 这是它和 F1 Rung Bars 的本质区别。峰值箱标数,中位数落点小旗标注。
-(()=>{
-const BIN=[6,14,22,19,13,9,6,4,3,2,1,1];       // 2h bins, sums to 100
-obsReveal('histo',s=>{
-  const X0=44,PW=27.5,base=252,step=5.4,HW=10.5;
-  BIN.forEach((v,i)=>{
-    const x=X0+i*PW+PW/2;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.4+rnd(k+1,i+2)*2.8;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:INK,'stroke-width':1.8,
-        opacity:.55+rnd(k+2,i+4)*.45,class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+3.5,cy:y,r:.7,fill:'rgba(31,30,28,.32)',
-        class:'fade',style:`animation-delay:${i*.06+k*.012}s`});
-    }
-  });
-  // peak label
-  const pk=BIN.indexOf(Math.max(...BIN));
-  const px=X0+pk*PW+PW/2;
-  // 一图一红:红给中位旗(half resolved 才是结论),峰值数走墨
-  const num=txt(s,{x:px,y:base-(BIN[pk]-1)*step-10,'font-size':11,'font-weight':800,fill:INK,
-    'text-anchor':'middle',class:'fade',style:'animation-delay:.8s'},BIN[pk]);
-  tip(num,`${BIN[pk]} of 100 tickets took ${pk*2}–${pk*2+2}h`);
-  // bin edges: the continuous axis is the histogram's identity
-  el(s,'line',{x1:X0-4,y1:base+4,x2:X0+12*PW+4,y2:base+4,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let e=0;e<=12;e++){
-    const x=X0+e*PW;
-    el(s,'line',{x1:x,y1:base+4,x2:x,y2:base+(e%2===0?11:8),stroke:'rgba(31,30,28,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${e*.02}s`});
-    if(e%2===0)txt(s,{x,y:base+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${e*.02}s`},e*2+'h');
-  }
-  // median flag: cumulative crosses 50 inside bin 2
-  let acc=0,mb=0;for(let i=0;i<12;i++){acc+=BIN[i];if(acc>=50){mb=i;break}}
-  const mx=X0+mb*PW+PW*.7;
-  el(s,'line',{x1:mx,y1:base+4,x2:mx,y2:base-BIN[mb]*step-16,stroke:'#F5572F','stroke-width':1.62,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx+4,y:base-BIN[mb]*step-20,'font-size':7.5,'font-weight':800,fill:'#F5572F',
-    class:'fade',style:'animation-delay:1.1s'},'HALF RESOLVED BY HERE');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE RUNG = ONE TICKET IN A HUNDRED · BINS OF TWO HOURS · DOT MARKS EVERY FIFTH');
-});
-})();
-
-// ════ F15 · tick box ════
-// 箱线图的 Basics 皮:发丝须线 + 胶囊箱体(IQR),纸色横档 = 中位数,
-// 空心点 = 离群值。箱体明度按中位数快慢排:越快越黑。
-(()=>{
-const G=[['ENT',[.4,.9,1.5,2.6,4.4],[6.2]],
-         ['PRO',[.8,2.1,3.3,5,7.8],[10.5]],
-         ['STARTER',[1.5,3.8,6.1,8.9,13.2],[16.8]],
-         ['FREE',[2.2,6,9.4,13.8,19.6],[22.1,23.5]]];
-const SHADE=[INK,'#6E6D66','#8F8E86','#C0BFB7'];
-obsReveal('boxplot',s=>{
-  const TOP=42,BASE=260,MAXH=24,mapY=v=>BASE-v/MAXH*(BASE-TOP);
-  const colX=g=>78+g*84,BW=24;
-  [0,6,12,18,24].forEach(h=>{
-    el(s,'line',{x1:36,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(31,30,28,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${h*.008}s`});
-    txt(s,{x:30,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.008}s`},h+'h');
-  });
-  G.forEach(([name,[mn,q1,md,q3,mx],outs],g)=>{
-    const x=colX(g);
-    el(s,'line',{x1:x,y1:mapY(mn),x2:x,y2:mapY(mx),stroke:'#8F8E86','stroke-width':1.44,
-      pathLength:1,class:'draw',style:`animation-delay:${g*.12}s;animation-duration:.6s`});
-    [mn,mx].forEach(vv=>el(s,'line',{x1:x-7,y1:mapY(vv),x2:x+7,y2:mapY(vv),
-      stroke:'#8F8E86','stroke-width':1.8,class:'fade',style:`animation-delay:${.2+g*.12}s`}));
-    const box=el(s,'rect',{x:x-BW/2,y:mapY(q3),width:BW,height:mapY(q1)-mapY(q3),rx:9,
-      fill:SHADE[g],class:'pop',style:`animation-delay:${.15+g*.12}s`});
-    tip(box,`${name} — half of tickets answered in ${q1}–${q3}h`);
-    el(s,'line',{x1:x-BW/2+3,y1:mapY(md),x2:x+BW/2-3,y2:mapY(md),stroke:PAPER,'stroke-width':2.2,
-      class:'fade',style:`animation-delay:${.5+g*.12}s`});
-    // 红只给一个主角:最慢的 FREE 是这张图的故事,其余中位数走墨色
-    const slow=g===G.length-1;
-    txt(s,{x:x+BW/2+6,y:mapY(md)+3,'font-size':slow?11:9.5,'font-weight':800,
-      fill:slow?'#F5572F':INK,
-      class:'fade',style:`animation-delay:${.6+g*.12}s`},md.toFixed(1)+'h');
-    outs.forEach((o,k)=>{
-      const d=el(s,'circle',{cx:x+(rnd(k+1,g+3)-.5)*8,cy:mapY(o),r:2.6,fill:PAPER,
-        stroke:'rgba(31,30,28,.72)','stroke-width':1.98,class:'pop',style:`animation-delay:${.7+g*.12+k*.06}s`});
-      tip(d,`${name} outlier — ${o}h`);
-    });
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.12}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'BOX = MIDDLE HALF · PAPER TICK = MEDIAN · RED = THE SLOWEST PLAN · DARKEST = FASTEST');
-});
-})();
-
-// ════ F16 · stream ribbon ════
-// Streamgraph / ThemeRiver:多系列构成随时间此消彼长。轮廓对称于中轴
-// (silhouette 基线),带宽 = 该系列当周量,纸色缝分带(treemap 同法)。
-// 三带明度按当前份额排:现任霸主最黑。带名标在各自最宽处。
-(()=>{
-const N=48;
-const mk=(base,trend,w1,w2,seed)=>Array.from({length:N},(_,t)=>
-  Math.max(2,base+trend*t+10*Math.sin(t/w1+seed)+5*Math.sin(t/w2+seed*2)+rnd(t+1,seed)*6));
-// 现任霸主是主角:FLOWS 上红,其余灰阶退后
-const SERIES=[
-  ['LEGACY EDITOR',mk(46,-.62,9,3.7,2),'rgba(31,30,28,.32)'],
-  ['BOARDS',mk(26,.10,11,4.2,5),'#8F8E86'],
-  ['FLOWS',mk(12,.78,10,3.1,8),'#F5572F'],
-];
-obsReveal('stream',s=>{
-  const X0=36,X1=744,CY=138,SC=1.15;
-  const x=t=>X0+t/(N-1)*(X1-X0);
-  const tot=Array.from({length:N},(_,t)=>SERIES.reduce((a,S)=>a+S[1][t],0));
-  // stacked offsets, silhouette baseline: top of stack starts at -total/2
-  const y0=Array.from({length:N},(_,t)=>CY-tot[t]*SC/2);
-  const smooth=pts=>{
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let k=1;k<pts.length-1;k++){
-      const p=pts[k],q=pts[k+1];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+` L${pts[pts.length-1][0].toFixed(1)} ${pts[pts.length-1][1].toFixed(1)}`;
-  };
-  let run=y0.slice();
-  SERIES.forEach(([name,vals,shade],si)=>{
-    const top=run.map((v,t)=>[x(t),v]);
-    const bot=run.map((v,t)=>[x(t),v+vals[t]*SC]);
-    run=bot.map(p=>p[1]);
-    const d=smooth(top)+' L '+[...bot].reverse().map(p=>`${p[0].toFixed(1)} ${p[1].toFixed(1)}`).join(' L ')+' Z';
-    const band=el(s,'path',{d,fill:shade,stroke:PAPER,'stroke-width':2,
-      class:'fade',style:`animation-delay:${si*.16}s`});
-    tip(band,`${name} — weekly active accounts, 48 weeks`);
-    // label at the band's widest week — clamped off both ends so the
-    // paper-colored text never spills past the band onto paper (invisible)
-    let wk=4;vals.forEach((v,t)=>{if(t>=4&&t<=N-6&&v>vals[wk])wk=t});
-    const midY=(top[wk][1]+bot[wk][1])/2;
-    const dark=shade===INK||shade==='#6E6D66'||shade==='#F5572F';
-    txt(s,{x:x(wk),y:midY+3,'font-size':9,'font-weight':800,'text-anchor':'middle',
-      fill:dark?PAPER:'#6E6D66','letter-spacing':'.06em',
-      class:'fade',style:`animation-delay:${.5+si*.14}s`},name);
-  });
-  // month floor: barcode ticks + labels every 8 weeks
-  const base=252;
-  el(s,'line',{x1:X0-6,y1:base,x2:X1+6,y2:base,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let t=0;t<N;t++)
-    el(s,'line',{x1:x(t),y1:base,x2:x(t),y2:base-(t%8===0?7:4),stroke:'rgba(31,30,28,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${t*.006}s`});
-  ['JAN','MAR','MAY','JUL','SEP','NOV'].forEach((m,k)=>
-    txt(s,{x:x(k*8),y:base+15,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:390,y:288,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'BAND WIDTH = WEEKLY ACTIVES · RED = TODAY’S LEADER · THE RIVER IS THE TOTAL');
-});
-})();
-
-// ════ F17 · candlestick ════
-// K 线的 Mono 化:不用红绿——实心墨 = 收跌,空心(纸底墨框)= 收涨,
-// 和哑铃图 hollow/ink 的语义一脉。影线是发丝,柱体胶囊圆角。
-// OHLC 四值都是真实记录,最高/最低两日标数。30 个交易日。
-(()=>{
-const N=30;
-// deterministic random walk with a dip-and-recover arc
-const DAYS=[];
-let px=52;
-for(let d=0;d<N;d++){
-  const drift=d<10?.4:d<18?-1.5:1.3;
-  const open=px;
-  const close=Math.max(30,open+drift+(rnd(d+1,3)-.5)*4.6);
-  const hi=Math.max(open,close)+rnd(d+2,7)*2.6;
-  const lo=Math.min(open,close)-rnd(d+3,11)*2.6;
-  DAYS.push([open,close,hi,lo]);
-  px=close;
-}
-obsReveal('candle',s=>{
-  const X0=40,PW=11.2,BASE=258,TOP=44;
-  const vmin=Math.min(...DAYS.map(d=>d[3])),vmax=Math.max(...DAYS.map(d=>d[2]));
-  const mapY=v=>BASE-(v-vmin)/(vmax-vmin)*(BASE-TOP);
-  const x=d=>X0+d*PW+PW/2;
-  // price grid
-  for(let g=0;g<=4;g++){
-    const v=vmin+g/4*(vmax-vmin);
-    el(s,'line',{x1:X0-4,y1:mapY(v),x2:X0+N*PW+4,y2:mapY(v),stroke:'rgba(31,30,28,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${g*.03}s`});
-    txt(s,{x:X0-8,y:mapY(v)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${g*.03}s`},'$'+Math.round(v));
-  }
-  let hiD=0,loD=0;
-  DAYS.forEach(([o,c,h,l],d)=>{
-    if(h>DAYS[hiD][2])hiD=d;
-    if(l<DAYS[loD][3])loD=d;
-  });
-  DAYS.forEach(([o,c,h,l],d)=>{
-    const cx=x(d),up=c>=o;
-    const yT=mapY(Math.max(o,c)),yB=mapY(Math.min(o,c));
-    // wick: one hairline, full reach
-    el(s,'line',{x1:cx,y1:mapY(h),x2:cx,y2:mapY(l),stroke:'rgba(31,30,28,.72)','stroke-width':1.26,
-      class:'fade',style:`animation-delay:${d*.03}s`});
-    // body: capsule; hollow = up, ink = down
-    const body=el(s,'rect',{x:cx-3.4,y:yT,width:6.8,height:Math.max(2.5,yB-yT),rx:3,
-      ...(up?{fill:PAPER,stroke:INK,'stroke-width':1.98}:{fill:INK}),
-      class:'fade',style:`animation-delay:${.05+d*.03}s`});
-    tip(body,`Day ${d+1} — open $${o.toFixed(1)} · close $${c.toFixed(1)} · high $${h.toFixed(1)} · low $${l.toFixed(1)}`);
-    // extremes, labeled with hairline flags
-    // 一图一红:这段行情的故事是探底,红只给最低价,最高价走墨
-    if(d===hiD)txt(s,{x:cx,y:mapY(h)-7,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(h));
-    if(d===loD)txt(s,{x:cx,y:mapY(l)+14,'font-size':9.5,'font-weight':800,fill:'#F5572F','text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-      class:'fade'},'$'+Math.round(l));
-  });
-  // week ticks on the floor
-  el(s,'line',{x1:X0-4,y1:BASE+8,x2:X0+N*PW+4,y2:BASE+8,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:BASE+8,x2:x(d),y2:BASE+8-(d%5===0?6:3),stroke:'rgba(31,30,28,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  ['W1','W2','W3','W4','W5','W6'].forEach((w,k)=>
-    txt(s,{x:x(k*5),y:BASE+21,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade'},w));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'INK BODY = CLOSED DOWN · HOLLOW = CLOSED UP · WICK = THE DAY’S FULL REACH');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 112
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-circular-palm.html

@@ -1,112 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>椰林绿 — Circular Graph, Dense</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#28311F}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:#F0EFE9;border-radius:24px;padding:34px 34px 24px;
-    width:min(1100px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#92A763;margin-bottom:10px}
-  .src{font-size:9.5px;color:#5A6B47;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{height:78vh;min-height:620px;cursor:pointer}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>Six hundred collaborations, one ring</h2>
-  <div class="sub">60 repos · line = shared contributors · hover a repo to light up its world · click to replay · 绿黄阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走麦黄。</div>
-  <div id="ch"></div>
-  <div class="src">CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG · PALM</div>
-</div>
-<script>
-const INK='#28311F',PAPER='#F0EFE9';
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── 60 repos in 5 org clusters; cluster = shade family ──
-const ORGS=[
-  ['platform',16,'#F0EFE9'],   // paper — the core
-  ['product',14,'#C8CAA8'],
-  ['data',12,'#AFB885'],
-  ['infra',10,'#7EA056'],
-  ['labs',8,'#4F7D3E'],
-];
-const nodes=[];
-ORGS.forEach(([org,count],oi)=>{
-  for(let i=0;i<count;i++){
-    // activity: a few giants per cluster, long tail of small repos
-    const heavy=i<2?30+rnd(oi+1,i+2)*45:4+rnd(oi+3,i+7)*16;
-    nodes.push({
-      name:org+'/'+org.slice(0,2)+'-'+String(i+1).padStart(2,'0'),
-      org:oi,value:Math.round(heavy),
-    });
-  }
-});
-// links: dense inside a cluster, sparser across; weight = shared contributors
-const links=[];
-const idx={};nodes.forEach((n,i)=>idx[n.name]=i);
-nodes.forEach((a,i)=>{
-  nodes.forEach((b,j)=>{
-    if(j<=i)return;
-    const same=a.org===b.org;
-    const p=same?.16:.045;
-    if(rnd(i+1,j+2)<p){
-      links.push({source:i,target:j,
-        w:1+Math.round(rnd(i+2,j+3)*6),same});
-    }
-  });
-});
-
-const g=echarts.init(document.getElementById('ch'));
-const opt={
-  animationDuration:1800,animationEasing:'quarticOut',
-  tooltip:{backgroundColor:'#F0EFEB',borderWidth:0,
-    textStyle:{color:'#28311F',fontFamily:'Inter',fontSize:12},padding:[10,14],
-    formatter:p=>p.dataType==='node'
-      ?p.name+' — '+p.value+' contributors'
-      :p.data.rawS+' ↔ '+p.data.rawT+' — '+p.data.w+' shared'},
-  series:[{
-    type:'graph',layout:'circular',
-    circular:{rotateLabel:true},
-    left:24,right:24,top:24,bottom:24,
-    data:nodes.map((n,i)=>({
-      name:n.name,value:n.value,
-      symbolSize:3.5+Math.sqrt(n.value)*1.7,
-      itemStyle:{color:ORGS[n.org][2],borderWidth:0},
-      // only the giants get a label — 60 names would be soup
-      label:{show:n.value>=26,position:'right',distance:4,
-        color:'#ACAD79',fontFamily:'Inter',fontSize:8.5,fontWeight:600},
-    })),
-    links:links.map(l=>({
-      source:l.source,target:l.target,
-      rawS:nodes[l.source].name,rawT:nodes[l.target].name,w:l.w,
-      lineStyle:{
-        width:.4+l.w*.28,
-        color:l.same?'#405333':'#334329',
-        opacity:l.same?.5:.32,
-        curveness:.3,
-      },
-    })),
-    emphasis:{
-      focus:'adjacency',
-      lineStyle:{color:'#F2D17E',opacity:.95,width:1.6},
-      label:{show:true,color:'#F2D17E'},
-    },
-    blur:{itemStyle:{opacity:.12},lineStyle:{opacity:.03}},
-  }],
-};
-const play=()=>{g.clear();g.setOption(opt)};
-play();
-document.getElementById('ch').addEventListener('click',play);
-addEventListener('resize',()=>g.resize());
-</script>
-</body>
-</html>

+ 0 - 112
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-circular-porcelain.html

@@ -1,112 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>青瓷蓝 — Circular Graph, Dense</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F7F2EB;--dark:#081F5C}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:#EDEFF1;border-radius:24px;padding:34px 34px 24px;
-    width:min(1100px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#809EC6;margin-bottom:10px}
-  .src{font-size:9.5px;color:#3E5A8F;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{height:78vh;min-height:620px;cursor:pointer}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>Six hundred collaborations, one ring</h2>
-  <div class="sub">60 repos · line = shared contributors · hover a repo to light up its world · click to replay · 蓝阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走纸白微蓝。</div>
-  <div id="ch"></div>
-  <div class="src">CIRCULAR GRAPH · DENSE · SOURCE · GIT LOG · PORCELAIN</div>
-</div>
-<script>
-const INK='#081F5C',PAPER='#EDEFF1';
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── 60 repos in 5 org clusters; cluster = shade family ──
-const ORGS=[
-  ['platform',16,'#EDEFF1'],   // paper — the core
-  ['product',14,'#BCC7D7'],
-  ['data',12,'#9EB3CD'],
-  ['infra',10,'#6C93C7'],
-  ['labs',8,'#3472C2'],
-];
-const nodes=[];
-ORGS.forEach(([org,count],oi)=>{
-  for(let i=0;i<count;i++){
-    // activity: a few giants per cluster, long tail of small repos
-    const heavy=i<2?30+rnd(oi+1,i+2)*45:4+rnd(oi+3,i+7)*16;
-    nodes.push({
-      name:org+'/'+org.slice(0,2)+'-'+String(i+1).padStart(2,'0'),
-      org:oi,value:Math.round(heavy),
-    });
-  }
-});
-// links: dense inside a cluster, sparser across; weight = shared contributors
-const links=[];
-const idx={};nodes.forEach((n,i)=>idx[n.name]=i);
-nodes.forEach((a,i)=>{
-  nodes.forEach((b,j)=>{
-    if(j<=i)return;
-    const same=a.org===b.org;
-    const p=same?.16:.045;
-    if(rnd(i+1,j+2)<p){
-      links.push({source:i,target:j,
-        w:1+Math.round(rnd(i+2,j+3)*6),same});
-    }
-  });
-});
-
-const g=echarts.init(document.getElementById('ch'));
-const opt={
-  animationDuration:1800,animationEasing:'quarticOut',
-  tooltip:{backgroundColor:'#F7F2EB',borderWidth:0,
-    textStyle:{color:'#081F5C',fontFamily:'Inter',fontSize:12},padding:[10,14],
-    formatter:p=>p.dataType==='node'
-      ?p.name+' — '+p.value+' contributors'
-      :p.data.rawS+' ↔ '+p.data.rawT+' — '+p.data.w+' shared'},
-  series:[{
-    type:'graph',layout:'circular',
-    circular:{rotateLabel:true},
-    left:24,right:24,top:24,bottom:24,
-    data:nodes.map((n,i)=>({
-      name:n.name,value:n.value,
-      symbolSize:3.5+Math.sqrt(n.value)*1.7,
-      itemStyle:{color:ORGS[n.org][2],borderWidth:0},
-      // only the giants get a label — 60 names would be soup
-      label:{show:n.value>=26,position:'right',distance:4,
-        color:'#9EB3CD',fontFamily:'Inter',fontSize:8.5,fontWeight:600},
-    })),
-    links:links.map(l=>({
-      source:l.source,target:l.target,
-      rawS:nodes[l.source].name,rawT:nodes[l.target].name,w:l.w,
-      lineStyle:{
-        width:.4+l.w*.28,
-        color:l.same?'#32517B':'#263E5E',
-        opacity:l.same?.5:.32,
-        curveness:.3,
-      },
-    })),
-    emphasis:{
-      focus:'adjacency',
-      lineStyle:{color:'#EDEFF1',opacity:.95,width:1.6},
-      label:{show:true,color:'#EDEFF1'},
-    },
-    blur:{itemStyle:{opacity:.12},lineStyle:{opacity:.03}},
-  }],
-};
-const play=()=>{g.clear();g.setOption(opt)};
-play();
-document.getElementById('ch').addEventListener('click',play);
-addEventListener('resize',()=>g.resize());
-</script>
-</body>
-</html>

+ 0 - 111
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-force-palm.html

@@ -1,111 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>椰林绿 — Force Graph, Dense</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#28311F}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:#F0EFE9;border-radius:24px;padding:34px 34px 24px;
-    width:min(1100px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#92A763;margin-bottom:10px}
-  .src{font-size:9.5px;color:#5A6B47;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{height:78vh;min-height:620px;cursor:grab}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>A galaxy of one hundred eighty services</h2>
-  <div class="sub">8 domains self-organize · node = call volume · drag anything, the galaxy fights back · click empty space to replay · 绿黄阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走麦黄。</div>
-  <div id="ch"></div>
-  <div class="src">FORCE GRAPH · DENSE · SOURCE · SERVICE MESH · PALM</div>
-</div>
-<script>
-const INK='#28311F',PAPER='#F0EFE9';
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── 8 domain hubs, each with a swarm of services; hubs interconnect ──
-const DOMAINS=[
-  ['auth',26,'#F0EFE9'],['billing',24,'#DDDBC9'],['content',28,'#C8CAA8'],
-  ['search',20,'#AFB885'],['notify',22,'#92A763'],['media',20,'#7EA056'],
-  ['analytics',18,'#658D49'],['edge',14,'#4F7D3E'],
-];
-const nodes=[],links=[];
-DOMAINS.forEach(([dom,count,shade],di)=>{
-  // the hub
-  nodes.push({name:dom,value:60+Math.round(rnd(di+1,3)*50),hub:true,shade,dom:di});
-  // satellites: long-tail sizes, a couple of mid-weights per domain
-  for(let i=0;i<count;i++){
-    const v=i<3?14+rnd(di+2,i+4)*18:2+rnd(di+5,i+9)*9;
-    const name=dom+'-'+String(i+1).padStart(2,'0');
-    nodes.push({name,value:Math.round(v),hub:false,shade,dom:di});
-    // satellite -> its hub
-    links.push({source:name,target:dom,
-      lineStyle:{width:.5+v*.05,color:'#405333',opacity:.5}});
-    // occasional satellite-to-satellite shortcut inside the domain
-    if(i>1&&rnd(di+3,i+11)<.22){
-      links.push({source:name,target:dom+'-'+String(1+Math.floor(rnd(di+4,i+13)*i)).padStart(2,'0'),
-        lineStyle:{width:.5,color:'#334329',opacity:.4}});
-    }
-  }
-});
-// hub-to-hub backbone: every domain talks to 2-3 others
-const H=DOMAINS.length;
-for(let a=0;a<H;a++)for(let b=a+1;b<H;b++){
-  if(rnd(a+7,b+9)<.42){
-    links.push({source:DOMAINS[a][0],target:DOMAINS[b][0],
-      lineStyle:{width:1.6+rnd(a+2,b+3)*2.2,color:'#405333',opacity:.65,curveness:.08}});
-  }
-}
-// a few cross-domain satellite shortcuts — the messy real-world wires
-for(let k=0;k<26;k++){
-  const a=nodes[1+Math.floor(rnd(k+1,17)*(nodes.length-1))];
-  const b=nodes[1+Math.floor(rnd(k+3,23)*(nodes.length-1))];
-  if(a.name!==b.name&&a.dom!==b.dom&&!a.hub&&!b.hub){
-    links.push({source:a.name,target:b.name,
-      lineStyle:{width:.5,color:'#2C3823',opacity:.38,curveness:.15}});
-  }
-}
-
-const g=echarts.init(document.getElementById('ch'));
-const opt={
-  animationDuration:300,
-  tooltip:{backgroundColor:'#F0EFEB',borderWidth:0,
-    textStyle:{color:'#28311F',fontFamily:'Inter',fontSize:12},padding:[10,14],
-    formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k calls/day':''},
-  series:[{
-    type:'graph',layout:'force',
-    force:{repulsion:46,edgeLength:[10,42],gravity:.16,friction:.22,layoutAnimation:true},
-    roam:true,draggable:true,
-    left:6,right:6,top:6,bottom:6,
-    data:nodes.map(n=>({
-      name:n.name,value:n.value,
-      symbolSize:n.hub?16+Math.sqrt(n.value)*1.6:2.5+Math.sqrt(n.value)*1.5,
-      itemStyle:{color:n.shade,borderWidth:n.hub?2:0,borderColor:INK},
-      label:{show:n.hub,position:'inside',
-        color:INK,fontFamily:'Inter',fontSize:9.5,fontWeight:800},
-    })),
-    links,
-    emphasis:{
-      focus:'adjacency',
-      lineStyle:{color:'#F2D17E',opacity:.95,width:1.4},
-      label:{show:true,color:PAPER,position:'right'},
-    },
-    blur:{itemStyle:{opacity:.1},lineStyle:{opacity:.03}},
-  }],
-};
-const play=()=>{g.clear();g.setOption(opt)};
-play();
-// click on empty canvas replays; clicks on nodes are for dragging
-g.getZr().on('click',e=>{if(!e.target)play()});
-addEventListener('resize',()=>g.resize());
-</script>
-</body>
-</html>

+ 0 - 111
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-force-porcelain.html

@@ -1,111 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>青瓷蓝 — Force Graph, Dense</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F7F2EB;--dark:#081F5C}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:#EDEFF1;border-radius:24px;padding:34px 34px 24px;
-    width:min(1100px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#809EC6;margin-bottom:10px}
-  .src{font-size:9.5px;color:#3E5A8F;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{height:78vh;min-height:620px;cursor:grab}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>A galaxy of one hundred eighty services</h2>
-  <div class="sub">8 domains self-organize · node = call volume · drag anything, the galaxy fights back · click empty space to replay · 蓝阶 8 档编码集群离核心的远近(亮 = 核心),hover 高亮走纸白微蓝。</div>
-  <div id="ch"></div>
-  <div class="src">FORCE GRAPH · DENSE · SOURCE · SERVICE MESH · PORCELAIN</div>
-</div>
-<script>
-const INK='#081F5C',PAPER='#EDEFF1';
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── 8 domain hubs, each with a swarm of services; hubs interconnect ──
-const DOMAINS=[
-  ['auth',26,'#EDEFF1'],['billing',24,'#D5DBE2'],['content',28,'#BCC7D7'],
-  ['search',20,'#9EB3CD'],['notify',22,'#809EC6'],['media',20,'#6C93C7'],
-  ['analytics',18,'#4D82C6'],['edge',14,'#3472C2'],
-];
-const nodes=[],links=[];
-DOMAINS.forEach(([dom,count,shade],di)=>{
-  // the hub
-  nodes.push({name:dom,value:60+Math.round(rnd(di+1,3)*50),hub:true,shade,dom:di});
-  // satellites: long-tail sizes, a couple of mid-weights per domain
-  for(let i=0;i<count;i++){
-    const v=i<3?14+rnd(di+2,i+4)*18:2+rnd(di+5,i+9)*9;
-    const name=dom+'-'+String(i+1).padStart(2,'0');
-    nodes.push({name,value:Math.round(v),hub:false,shade,dom:di});
-    // satellite -> its hub
-    links.push({source:name,target:dom,
-      lineStyle:{width:.5+v*.05,color:'#32517B',opacity:.5}});
-    // occasional satellite-to-satellite shortcut inside the domain
-    if(i>1&&rnd(di+3,i+11)<.22){
-      links.push({source:name,target:dom+'-'+String(1+Math.floor(rnd(di+4,i+13)*i)).padStart(2,'0'),
-        lineStyle:{width:.5,color:'#263E5E',opacity:.4}});
-    }
-  }
-});
-// hub-to-hub backbone: every domain talks to 2-3 others
-const H=DOMAINS.length;
-for(let a=0;a<H;a++)for(let b=a+1;b<H;b++){
-  if(rnd(a+7,b+9)<.42){
-    links.push({source:DOMAINS[a][0],target:DOMAINS[b][0],
-      lineStyle:{width:1.6+rnd(a+2,b+3)*2.2,color:'#32517B',opacity:.65,curveness:.08}});
-  }
-}
-// a few cross-domain satellite shortcuts — the messy real-world wires
-for(let k=0;k<26;k++){
-  const a=nodes[1+Math.floor(rnd(k+1,17)*(nodes.length-1))];
-  const b=nodes[1+Math.floor(rnd(k+3,23)*(nodes.length-1))];
-  if(a.name!==b.name&&a.dom!==b.dom&&!a.hub&&!b.hub){
-    links.push({source:a.name,target:b.name,
-      lineStyle:{width:.5,color:'#213650',opacity:.38,curveness:.15}});
-  }
-}
-
-const g=echarts.init(document.getElementById('ch'));
-const opt={
-  animationDuration:300,
-  tooltip:{backgroundColor:'#F7F2EB',borderWidth:0,
-    textStyle:{color:'#081F5C',fontFamily:'Inter',fontSize:12},padding:[10,14],
-    formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k calls/day':''},
-  series:[{
-    type:'graph',layout:'force',
-    force:{repulsion:46,edgeLength:[10,42],gravity:.16,friction:.22,layoutAnimation:true},
-    roam:true,draggable:true,
-    left:6,right:6,top:6,bottom:6,
-    data:nodes.map(n=>({
-      name:n.name,value:n.value,
-      symbolSize:n.hub?16+Math.sqrt(n.value)*1.6:2.5+Math.sqrt(n.value)*1.5,
-      itemStyle:{color:n.shade,borderWidth:n.hub?2:0,borderColor:INK},
-      label:{show:n.hub,position:'inside',
-        color:INK,fontFamily:'Inter',fontSize:9.5,fontWeight:800},
-    })),
-    links,
-    emphasis:{
-      focus:'adjacency',
-      lineStyle:{color:'#EDEFF1',opacity:.95,width:1.4},
-      label:{show:true,color:PAPER,position:'right'},
-    },
-    blur:{itemStyle:{opacity:.1},lineStyle:{opacity:.03}},
-  }],
-};
-const play=()=>{g.clear();g.setOption(opt)};
-play();
-// click on empty canvas replays; clicks on nodes are for dragging
-g.getZr().on('click',e=>{if(!e.target)play()});
-addEventListener('resize',()=>g.resize());
-</script>
-</body>
-</html>

+ 0 - 164
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-threads-palm.html

@@ -1,164 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>椰林绿 — Thread Triptych, Interactive</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#28311F;--paper:#F0EFEB}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:var(--paper);border-radius:24px;padding:34px 34px 24px;
-    width:min(1200px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#92A763;margin-bottom:6px}
-  .src{font-size:9.5px;color:#5A6B47;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{width:100%;height:74vh;min-height:560px;display:block}
-  #status{height:34px;display:flex;align-items:center;gap:10px;
-    font-size:12px;color:#92A763;border-top:1px solid #38452C;margin-top:6px;padding-top:8px}
-  #status b{color:var(--paper);font-weight:700}
-  #status .pin{font-size:9px;letter-spacing:.1em;color:#5A6B47;font-weight:700}
-  svg text{font-family:'Inter',sans-serif;user-select:none}
-
-  /* thread states */
-  .thread{transition:opacity .18s ease,stroke-width .18s ease}
-  .thread.hot{opacity:.95 !important}
-  svg.focused .thread:not(.hot){opacity:.035 !important}
-  .nodelab{cursor:pointer;transition:opacity .18s ease}
-  svg.focused .nodelab:not(.hot){opacity:.25}
-  .hit{cursor:pointer}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>Where the data actually goes — now answerable</h2>
-  <div class="sub">source → processor → destination · one thread per data path · hover to trace a route · hover any label to pull its whole bundle · click to pin, click dark space to release</div>
-  <svg id="ch" viewBox="0 0 1140 640" preserveAspectRatio="xMidYMid meet"></svg>
-  <div id="status"><span id="stext">hover a thread or a label</span><span class="pin" id="spin"></span></div>
-  <div class="src">THREAD TRIPTYCH · INTERACTIVE · DATA MAP · PALM</div>
-</div>
-<script>
-const PAPER='#F0EFE9',NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ── the pipeline ──
-const SRC=['web-app','ios-app','android','desktop','cli','public-api','webhooks','forms','email-in','chat-widget',
-  'billing-ev','auth-ev','search-log','audit-ev','crash-rep','perf-beacon','cdn-log','edge-log','job-ev','sync-ev',
-  'import','export-req','admin-ui','partner-api','iot-ping','backfill'];
-const MIDV=['parse','validate','dedupe','enrich','geoip','sessionize','pii-mask','score','sample','route',
-  'batch','window','join','flatten','filter','hash','encrypt','compact','index','tag'];
-const MID=MIDV.flatMap(v=>[v+'-a',v+'-b']);          // 40 processors
-const DST=['WAREHOUSE','ANALYTICS','CRM','ADS','BILLING','SEARCH','ML PIPE','ARCHIVE','AUDIT','EXPORTS'];
-
-// routes: each source emits 2-3 full paths; volume is long-tailed
-const routes=[];
-SRC.forEach((s,i)=>{
-  const n=2+Math.floor(rnd(i+1,5)*2);
-  for(let k=0;k<n;k++){
-    const mi=Math.floor(rnd(i*3+k+2,13)*MID.length);
-    const di=Math.min(9,Math.floor(rnd(i*5+k+4,19)**2*11));   // skew: low dsts hoard
-    const v=Math.round(1+rnd(i*7+k+6,29)**2*30);              // 1..31, long tail
-    routes.push({s:i,m:mi,d:di,v});
-  }
-});
-
-// ── geometry ──
-const X1=180,X2=560,X3=930;
-const sy=i=>36+i*22.6, my=i=>30+i*14.6, dy=i=>60+i*57;
-const path=r=>`M${X1+6} ${sy(r.s)} C${(X1+X2)/2} ${sy(r.s)} ${(X1+X2)/2} ${my(r.m)} ${X2} ${my(r.m)}`
-  +` C${(X2+X3)/2} ${my(r.m)} ${(X2+X3)/2} ${dy(r.d)} ${X3-6} ${dy(r.d)}`;
-
-const svg=document.getElementById('ch');
-const stext=document.getElementById('stext'),spin=document.getElementById('spin');
-const fmt=v=>v+'k ev/day';
-let pinned=null;
-
-// column headers
-[[X1,'① SOURCE'],[X2,'② PROCESSOR'],[X3,'③ DESTINATION']].forEach(([x,t])=>
-  txt(svg,{x,y:14,'font-size':9,'font-weight':800,fill:PAPER,'letter-spacing':'.14em','text-anchor':'middle'},t));
-
-// threads (drawn first, under labels)
-const gThreads=el(svg,'g',{});
-routes.forEach((r,ri)=>{
-  r.el=el(gThreads,'path',{d:path(r),fill:'none',stroke:PAPER,
-    'stroke-width':Math.max(.6,r.v*.14),class:'thread',
-    opacity:.06+Math.min(.2,r.v*.012),'stroke-linecap':'round'});
-  // invisible fat twin: the hover/click target
-  r.hit=el(gThreads,'path',{d:path(r),fill:'none',stroke:'#000','stroke-opacity':0,
-    'stroke-width':9,class:'hit'});
-  r.hit.dataset.route=ri;
-});
-
-// node labels + ticks; each carries hit metadata
-const mkNode=(x,y,label,anchor,cls,meta,size,fill)=>{
-  const g=el(svg,'g',{class:'nodelab'});
-  g.dataset.kind=meta.kind;g.dataset.idx=meta.idx;
-  el(g,'rect',{x:x-3,y:y-1.6,width:6,height:3.2,fill:PAPER,opacity:.85});
-  txt(g,{x:anchor==='end'?x-9:x+9,y:y+2.6,'font-size':size,'font-weight':600,
-    fill,'text-anchor':anchor},label);
-  // generous invisible hit zone
-  el(g,'rect',{x:anchor==='end'?x-92:x-6,y:y-6.5,width:98,height:13,fill:'#000','fill-opacity':0,class:'hit'});
-  return g;
-};
-SRC.forEach((s,i)=>mkNode(X1,sy(i),s,'end','',{kind:'s',idx:i},7.5,'#ACAD79'));
-MID.forEach((m,i)=>mkNode(X2,my(i),m,'start','',{kind:'m',idx:i},5.4,'#4F7D3E'));
-DST.forEach((d,i)=>{
-  const g=el(svg,'g',{class:'nodelab'});
-  g.dataset.kind='d';g.dataset.idx=i;
-  el(g,'circle',{cx:X3,cy:dy(i),r:3.4,fill:PAPER});
-  txt(g,{x:X3+12,y:dy(i)+3,'font-size':8.5,'font-weight':700,fill:'#ACAD79','letter-spacing':'.08em'},d);
-  el(g,'rect',{x:X3-8,y:dy(i)-10,width:120,height:20,fill:'#000','fill-opacity':0,class:'hit'});
-});
-
-// ── highlight machinery ──
-const nodeLabs=[...svg.querySelectorAll('.nodelab')];
-const clear=()=>{
-  svg.classList.remove('focused');
-  routes.forEach(r=>r.el.classList.remove('hot'));
-  nodeLabs.forEach(n=>n.classList.remove('hot'));
-  stext.textContent='hover a thread or a label';spin.textContent='';
-};
-const focusRoutes=(rs,label)=>{
-  svg.classList.add('focused');
-  routes.forEach(r=>r.el.classList.toggle('hot',rs.includes(r)));
-  nodeLabs.forEach(n=>{
-    const k=n.dataset.kind,ix=+n.dataset.idx;
-    n.classList.toggle('hot',rs.some(r=>(k==='s'&&r.s===ix)||(k==='m'&&r.m===ix)||(k==='d'&&r.d===ix)));
-  });
-  stext.innerHTML=label;
-};
-const routeLabel=r=>`<b>${SRC[r.s]}</b> → ${MID[r.m]} → <b>${DST[r.d]}</b> · ${fmt(r.v)}`;
-const nodeSel=(kind,idx)=>{
-  const rs=routes.filter(r=>(kind==='s'&&r.s===idx)||(kind==='m'&&r.m===idx)||(kind==='d'&&r.d===idx));
-  const total=rs.reduce((a,r)=>a+r.v,0);
-  const name=kind==='s'?SRC[idx]:kind==='m'?MID[idx]:DST[idx];
-  return [rs,`<b>${name}</b> · ${rs.length} path${rs.length>1?'s':''} · ${fmt(total)} total`];
-};
-
-const apply=t=>{
-  const hit=t.closest?.('.hit');
-  if(!hit)return null;
-  const rg=hit.dataset.route,ng=hit.closest('.nodelab');
-  if(rg!=null){const r=routes[+rg];return()=>focusRoutes([r],routeLabel(r))}
-  if(ng){const [rs,lab]=nodeSel(ng.dataset.kind,+ng.dataset.idx);return()=>focusRoutes(rs,lab)}
-  return null;
-};
-svg.addEventListener('mousemove',e=>{
-  if(pinned)return;
-  const f=apply(e.target);
-  f?f():clear();
-});
-svg.addEventListener('mouseleave',()=>{if(!pinned)clear()});
-svg.addEventListener('click',e=>{
-  const f=apply(e.target);
-  if(f){pinned=f;f();spin.textContent='PINNED · CLICK DARK SPACE TO RELEASE'}
-  else{pinned=null;clear()}
-});
-</script>
-</body>
-</html>

+ 0 - 164
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/big-threads-porcelain.html

@@ -1,164 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>青瓷蓝 — Thread Triptych, Interactive</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F7F2EB;--dark:#081F5C;--paper:#F7F2EB}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;padding:40px;-webkit-font-smoothing:antialiased;
-    display:flex;justify-content:center}
-  .card{background:var(--dark);color:var(--paper);border-radius:24px;padding:34px 34px 24px;
-    width:min(1200px,100%)}
-  h2{font-weight:700;font-size:19px;letter-spacing:-.02em;margin-bottom:4px}
-  .sub{font-size:12px;color:#809EC6;margin-bottom:6px}
-  .src{font-size:9.5px;color:#3E5A8F;margin-top:12px;letter-spacing:.08em;font-weight:500}
-  #ch{width:100%;height:74vh;min-height:560px;display:block}
-  #status{height:34px;display:flex;align-items:center;gap:10px;
-    font-size:12px;color:#809EC6;border-top:1px solid #1B3159;margin-top:6px;padding-top:8px}
-  #status b{color:var(--paper);font-weight:700}
-  #status .pin{font-size:9px;letter-spacing:.1em;color:#3E5A8F;font-weight:700}
-  svg text{font-family:'Inter',sans-serif;user-select:none}
-
-  /* thread states */
-  .thread{transition:opacity .18s ease,stroke-width .18s ease}
-  .thread.hot{opacity:.95 !important}
-  svg.focused .thread:not(.hot){opacity:.035 !important}
-  .nodelab{cursor:pointer;transition:opacity .18s ease}
-  svg.focused .nodelab:not(.hot){opacity:.25}
-  .hit{cursor:pointer}
-</style>
-</head>
-<body>
-<div class="card">
-  <h2>Where the data actually goes — now answerable</h2>
-  <div class="sub">source → processor → destination · one thread per data path · hover to trace a route · hover any label to pull its whole bundle · click to pin, click dark space to release</div>
-  <svg id="ch" viewBox="0 0 1140 640" preserveAspectRatio="xMidYMid meet"></svg>
-  <div id="status"><span id="stext">hover a thread or a label</span><span class="pin" id="spin"></span></div>
-  <div class="src">THREAD TRIPTYCH · INTERACTIVE · DATA MAP · PORCELAIN</div>
-</div>
-<script>
-const PAPER='#EDEFF1',NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ── the pipeline ──
-const SRC=['web-app','ios-app','android','desktop','cli','public-api','webhooks','forms','email-in','chat-widget',
-  'billing-ev','auth-ev','search-log','audit-ev','crash-rep','perf-beacon','cdn-log','edge-log','job-ev','sync-ev',
-  'import','export-req','admin-ui','partner-api','iot-ping','backfill'];
-const MIDV=['parse','validate','dedupe','enrich','geoip','sessionize','pii-mask','score','sample','route',
-  'batch','window','join','flatten','filter','hash','encrypt','compact','index','tag'];
-const MID=MIDV.flatMap(v=>[v+'-a',v+'-b']);          // 40 processors
-const DST=['WAREHOUSE','ANALYTICS','CRM','ADS','BILLING','SEARCH','ML PIPE','ARCHIVE','AUDIT','EXPORTS'];
-
-// routes: each source emits 2-3 full paths; volume is long-tailed
-const routes=[];
-SRC.forEach((s,i)=>{
-  const n=2+Math.floor(rnd(i+1,5)*2);
-  for(let k=0;k<n;k++){
-    const mi=Math.floor(rnd(i*3+k+2,13)*MID.length);
-    const di=Math.min(9,Math.floor(rnd(i*5+k+4,19)**2*11));   // skew: low dsts hoard
-    const v=Math.round(1+rnd(i*7+k+6,29)**2*30);              // 1..31, long tail
-    routes.push({s:i,m:mi,d:di,v});
-  }
-});
-
-// ── geometry ──
-const X1=180,X2=560,X3=930;
-const sy=i=>36+i*22.6, my=i=>30+i*14.6, dy=i=>60+i*57;
-const path=r=>`M${X1+6} ${sy(r.s)} C${(X1+X2)/2} ${sy(r.s)} ${(X1+X2)/2} ${my(r.m)} ${X2} ${my(r.m)}`
-  +` C${(X2+X3)/2} ${my(r.m)} ${(X2+X3)/2} ${dy(r.d)} ${X3-6} ${dy(r.d)}`;
-
-const svg=document.getElementById('ch');
-const stext=document.getElementById('stext'),spin=document.getElementById('spin');
-const fmt=v=>v+'k ev/day';
-let pinned=null;
-
-// column headers
-[[X1,'① SOURCE'],[X2,'② PROCESSOR'],[X3,'③ DESTINATION']].forEach(([x,t])=>
-  txt(svg,{x,y:14,'font-size':9,'font-weight':800,fill:PAPER,'letter-spacing':'.14em','text-anchor':'middle'},t));
-
-// threads (drawn first, under labels)
-const gThreads=el(svg,'g',{});
-routes.forEach((r,ri)=>{
-  r.el=el(gThreads,'path',{d:path(r),fill:'none',stroke:PAPER,
-    'stroke-width':Math.max(.6,r.v*.14),class:'thread',
-    opacity:.06+Math.min(.2,r.v*.012),'stroke-linecap':'round'});
-  // invisible fat twin: the hover/click target
-  r.hit=el(gThreads,'path',{d:path(r),fill:'none',stroke:'#000','stroke-opacity':0,
-    'stroke-width':9,class:'hit'});
-  r.hit.dataset.route=ri;
-});
-
-// node labels + ticks; each carries hit metadata
-const mkNode=(x,y,label,anchor,cls,meta,size,fill)=>{
-  const g=el(svg,'g',{class:'nodelab'});
-  g.dataset.kind=meta.kind;g.dataset.idx=meta.idx;
-  el(g,'rect',{x:x-3,y:y-1.6,width:6,height:3.2,fill:PAPER,opacity:.85});
-  txt(g,{x:anchor==='end'?x-9:x+9,y:y+2.6,'font-size':size,'font-weight':600,
-    fill,'text-anchor':anchor},label);
-  // generous invisible hit zone
-  el(g,'rect',{x:anchor==='end'?x-92:x-6,y:y-6.5,width:98,height:13,fill:'#000','fill-opacity':0,class:'hit'});
-  return g;
-};
-SRC.forEach((s,i)=>mkNode(X1,sy(i),s,'end','',{kind:'s',idx:i},7.5,'#9EB3CD'));
-MID.forEach((m,i)=>mkNode(X2,my(i),m,'start','',{kind:'m',idx:i},5.4,'#3472C2'));
-DST.forEach((d,i)=>{
-  const g=el(svg,'g',{class:'nodelab'});
-  g.dataset.kind='d';g.dataset.idx=i;
-  el(g,'circle',{cx:X3,cy:dy(i),r:3.4,fill:PAPER});
-  txt(g,{x:X3+12,y:dy(i)+3,'font-size':8.5,'font-weight':700,fill:'#9EB3CD','letter-spacing':'.08em'},d);
-  el(g,'rect',{x:X3-8,y:dy(i)-10,width:120,height:20,fill:'#000','fill-opacity':0,class:'hit'});
-});
-
-// ── highlight machinery ──
-const nodeLabs=[...svg.querySelectorAll('.nodelab')];
-const clear=()=>{
-  svg.classList.remove('focused');
-  routes.forEach(r=>r.el.classList.remove('hot'));
-  nodeLabs.forEach(n=>n.classList.remove('hot'));
-  stext.textContent='hover a thread or a label';spin.textContent='';
-};
-const focusRoutes=(rs,label)=>{
-  svg.classList.add('focused');
-  routes.forEach(r=>r.el.classList.toggle('hot',rs.includes(r)));
-  nodeLabs.forEach(n=>{
-    const k=n.dataset.kind,ix=+n.dataset.idx;
-    n.classList.toggle('hot',rs.some(r=>(k==='s'&&r.s===ix)||(k==='m'&&r.m===ix)||(k==='d'&&r.d===ix)));
-  });
-  stext.innerHTML=label;
-};
-const routeLabel=r=>`<b>${SRC[r.s]}</b> → ${MID[r.m]} → <b>${DST[r.d]}</b> · ${fmt(r.v)}`;
-const nodeSel=(kind,idx)=>{
-  const rs=routes.filter(r=>(kind==='s'&&r.s===idx)||(kind==='m'&&r.m===idx)||(kind==='d'&&r.d===idx));
-  const total=rs.reduce((a,r)=>a+r.v,0);
-  const name=kind==='s'?SRC[idx]:kind==='m'?MID[idx]:DST[idx];
-  return [rs,`<b>${name}</b> · ${rs.length} path${rs.length>1?'s':''} · ${fmt(total)} total`];
-};
-
-const apply=t=>{
-  const hit=t.closest?.('.hit');
-  if(!hit)return null;
-  const rg=hit.dataset.route,ng=hit.closest('.nodelab');
-  if(rg!=null){const r=routes[+rg];return()=>focusRoutes([r],routeLabel(r))}
-  if(ng){const [rs,lab]=nodeSel(ng.dataset.kind,+ng.dataset.idx);return()=>focusRoutes(rs,lab)}
-  return null;
-};
-svg.addEventListener('mousemove',e=>{
-  if(pinned)return;
-  const f=apply(e.target);
-  f?f():clear();
-});
-svg.addEventListener('mouseleave',()=>{if(!pinned)clear()});
-svg.addEventListener('click',e=>{
-  const f=apply(e.target);
-  if(f){pinned=f;f();spin.textContent='PINNED · CLICK DARK SPACE TO RELEASE'}
-  else{pinned=null;clear()}
-});
-</script>
-</body>
-</html>

+ 0 - 1172
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/glance-palm.html

@@ -1,1172 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Glance 22 × PALM</title>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#58402E;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.40);--grid:rgba(88,64,46,.16);--lab:rgba(88,64,46,.72);--dmid:rgba(240,239,235,.62);--dq:rgba(240,239,235,.24)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:var(--bg)}
-  .card.dark .sub{color:var(--dmid)}
-  .card.dark .src{color:var(--dq)}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .ch{height:320px}
-  .wrap{position:relative;height:320px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:var(--ink);color:var(--bg)}
-  .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}
-  .badge.pair{border:1px dashed var(--faint);color:var(--muted)}
-  .card.dark .badge.del{background:var(--bg);color:var(--ink)}
-  .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}
-  .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-    <h1>Glance 系(快读)· 22 张 × PALM</h1>
-  <p><span class="chip" style="background:#43593B"></span><span class="chip" style="background:#F2D17E"></span><span class="chip" style="background:#77835A"></span><span class="chip" style="background:#ACAD79"></span><span class="chip" style="background:#58402E"></span> &nbsp;椰林 · 绿黄六色板:麦黄 → 橄榄 → 浅苔 → 苔绿 → 中绿 → 深绿。深绿当主墨、琥珀当主角、浓咖当文字;暗卡上强调改麦黄(最宽波幅 / 直播灯 / 集成中枢)——深绿在浓咖底上读不出。</p>
-  <p style="margin-top:7px">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 个来源 / 4 条产品线 / 5 个工作日 / 10 支小队 / 12 个集成)各走一个分类位,序数型(8 年营收排名 / 50 个市场 / 4 档优先级 / 树木年份 / 面板停留时长 / 情绪票数)走 5 档梯,深色卡换纸色梯、末档落在强调色。几何、动画、数据与 glance-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <!-- ══ mono-demo ══ -->
-  <div class="card">
-    <h2>Daily active range</h2>
-    <div class="sub">min – max concurrent users · 1–14 Feb</div>
-    <div class="wrap"><canvas id="c1"></canvas></div>
-    <div class="src">RANGE CAPSULES · MONO-DEMO · LIVE OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>How releases made us feel</h2>
-    <div class="sub">reactions tagged in retro · H1 2026</div>
-    <div class="ch" id="rose"></div>
-    <div class="src">PETAL ROSE · MONO-DEMO · TEAM RETRO BOARD</div>
-  </div>
-
-  <div class="card">
-    <h2>Revenue by plan</h2>
-    <div class="sub">Q2 2026 · MRR, $K</div>
-    <div class="wrap"><canvas id="c3"></canvas></div>
-    <div class="src">CHUNKY BARS · MONO-DEMO · BILLING</div>
-  </div>
-
-  <!-- ══ mono-redesign · dot waffle ══ -->
-  <div class="card">
-    <h2>Where sign-ups come from</h2>
-    <div class="sub">Q2 2026 · every dot = 1% of new accounts</div>
-    <svg id="waffle" viewBox="0 0 380 300" width="100%" style="max-height:320px"></svg>
-    <div class="src">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION</div>
-  </div>
-
-  <!-- ══ mono-fancy ══ -->
-  <div class="card">
-    <h2>Trees planted, year by year</h2>
-    <div class="sub">reforestation program · thousands of trees</div>
-    <div class="ch" id="forest"></div>
-    <div class="src">PICTORIAL BAR · MONO-FANCY · GREEN OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Who works with whom</h2>
-    <div class="sub">cross-team collaborations · Q2 2026 · line = shared project</div>
-    <div class="ch" id="circular"></div>
-    <div class="src">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB</div>
-  </div>
-
-  <div class="card">
-    <h2>Everything the platform ships</h2>
-    <div class="sub">product areas and capabilities · Jun 2026</div>
-    <div class="ch" id="tree"></div>
-    <div class="src">TREE · MONO-FANCY · PRODUCT CATALOG</div>
-  </div>
-
-  <div class="card">
-    <h2>Campaigns rain down, sign-ups flow</h2>
-    <div class="sub">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days</div>
-    <div class="ch" id="rainfall"></div>
-    <div class="src">DUAL AREA · MONO-FANCY · GROWTH + ADS</div>
-  </div>
-
-  <div class="card">
-    <h2>One dataset, three views</h2>
-    <div class="sub">watch the same 12 products fly between views · every 3s</div>
-    <div class="ch" id="morph"></div>
-    <div class="src">SCATTER MORPH · MONO-FANCY · COHORTS</div>
-  </div>
-
-  <!-- ══ mono-fancy2 ══ -->
-  <div class="card">
-    <h2>Where we gained, where we bled</h2>
-    <div class="sub">net accounts per segment · Q2 2026 · churn left, growth right</div>
-    <div class="ch" id="negbar"></div>
-    <div class="src">DIVERGING BAR · MONO-FANCY2 · CRM</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Integrations, pulled into orbit</h2>
-    <div class="sub">force layout · node = monthly syncs · drag a node, the web fights back</div>
-    <div class="ch" id="force"></div>
-    <div class="src">FORCE GRAPH · NEW · INTEGRATION HUB</div>
-  </div>
-
-  <div class="card">
-    <h2>Fifty markets, one wave</h2>
-    <div class="sub">sign-ups per market · bars cascade in · click to replay</div>
-    <div class="ch" id="wave"></div>
-    <div class="src">STAGGER DELAY · MONO-FANCY2 · GROWTH</div>
-  </div>
-
-  <!-- ══ mono-fancy3 ══ -->
-  <div class="card">
-    <h2>Big slice, deep engagement</h2>
-    <div class="sub">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min</div>
-    <div class="ch" id="custompie"></div>
-    <div class="src">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS</div>
-  </div>
-
-  <div class="card">
-    <h2>Support load, day by day</h2>
-    <div class="sub">tickets per hour · each row is its own axis · dot = volume</div>
-    <div class="ch" id="singleaxis"></div>
-    <div class="src">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <div class="card">
-    <h2>Response times, spread out</h2>
-    <div class="sub">every dot = one ticket · jitter reveals the pile-up · by priority</div>
-    <div class="ch" id="jitter"></div>
-    <div class="src">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <!-- ══ mono-fancy4 ══ -->
-  <div class="card">
-    <h2>Eight products race for revenue</h2>
-    <div class="sub">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay</div>
-    <div class="ch" id="race"></div>
-    <div class="src">BAR RACE · MONO-FANCY4 · BILLING</div>
-  </div>
-
-  <div class="card">
-    <h2>Concurrent users, streaming</h2>
-    <div class="sub">one new reading every 300ms · window of 50 · click to restart</div>
-    <div class="ch" id="stream"></div>
-    <div class="src">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS</div>
-  </div>
-
-  <div class="card">
-    <h2>H1 revenue, drawn in one stroke</h2>
-    <div class="sub">cumulative ARR · the line draws, the number counts · click to replay</div>
-    <div class="ch" id="stroke"></div>
-    <div class="src">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE</div>
-  </div>
-
-
-  <!-- G19 · violin -->
-  <div class="card">
-    <span class="badge">GLANCE 分布型 · 小提琴</span>
-    <h2>How fast each plan gets an answer</h2>
-    <div class="sub">hours to first reply · silhouette = where tickets pile up · paper tick = median</div>
-    <svg id="violin" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">VIOLIN · PALM · SUPPORT DESK</div>
-  </div>
-
-  <!-- G20 · matrix heat -->
-  <div class="card">
-    <span class="badge">GLANCE 热力型 · 通用矩阵</span>
-    <h2>Adoption runs hot on the new versions</h2>
-    <div class="sub">5 versions × 6 features · shade = % of accounts using it · every cell carries its number</div>
-    <svg id="gheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT (GLANCE) · PALM · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- G21 · rank strip -->
-  <div class="card">
-    <span class="badge">GLANCE 排名型 · RANK STRIP</span>
-    <h2>Flows climbs to the top</h2>
-    <div class="sub">one row per product, one cell per quarter · darker = higher rank · a bar race you can print</div>
-    <svg id="rankstrip" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RANK STRIP · PALM · BILLING</div>
-  </div>
-
-  <!-- G22 · aggregate sankey -->
-  <div class="card">
-    <span class="badge">GLANCE 流向型 · 聚合桑基</span>
-    <h2>Channels pour into plans</h2>
-    <div class="sub">ribbon width = accounts · channels on the left, the plans they land in on the right</div>
-    <svg id="sankey" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">AGGREGATE SANKEY · PALM · ATTRIBUTION</div>
-  </div>
-
-</div>
-<script>
-// ── PALM 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F0EFEB',DARK='#58402E',TXT='#58402E';
-const MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#58402E',HERO='#D4A017';
-const HERODK='#F2D17E';                      // 深色卡上的强调色(HERO 在深底上可能读不出)
-const FAINTDATA='#ACAD79';                 // 静音数据档:周末 / 非主序列
-const DMID='rgba(240,239,235,.62)',DQ='rgba(240,239,235,.24)';   // 深色卡上的浅墨 / 静音档
-const SER=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79','#58402E'];   // 分类位:一图之内每个系列 / 每类 / 每行一个色
-const RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B'];   // 5 档序数梯(浅→重):排名 / 票量 / 峰值 / 停留时长
-const DRAMP4=['#F2D17E','#ACAD79','rgba(240,239,235,.55)','rgba(240,239,235,.26)'];   // 深色卡上的 4 档序数梯(浅→深)
-const HEROMODE=false; // 主角模式:分类位的头号项也整体走 HERO
-
-// ── 别名与派生:深色卡的线全是卡底向纸色的插值,不引入新色相 ──
-const INK=TXT,PAPER=BG,MUTED=MUT;
-const rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));
-const alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;
-const mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');
-const lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));
-  return .2126*r+.7152*g+.0722*b};
-const ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};
-const readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG); // 深色卡上落在色块里的字
-const DLINE=mix(DARK,BG,.20),DLINE2=mix(DARK,BG,.13),DSHELL=mix(DARK,BG,.09);
-const hi=c=>HEROMODE?HERO:c;
-// 名次 → 序数梯。末档(WIRE 的红)只发给第 1 名,其余摊在前四档上
-const rampAt=(i,n)=>i===0?RAMP[4]
-  :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];
-const rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);
-  return i=>r.indexOf(i)};
-const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── unified reveal: every chart waits to be scrolled into view; click replays.
-//    fn charts manage their own timers via keep(); they're cleared on replay.
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUTED;
-Chart.defaults.font.family='Inter';
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=INK;
-Chart.defaults.plugins.tooltip.titleColor=BG;
-Chart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-// ════ mono-demo · 1: floating range capsules ════
-(()=>{
-const WIDEST=r=>r.reduce((b,x,i)=>x[1]-x[0]>r[b][1]-r[b][0]?i:b,0);
-const ranges=[[195,235],[165,192],[150,232],[73,168],[122,202],[182,192],[138,178],[162,227],[195,235],[228,305],[218,232],[165,232],[118,210],[195,232]];
-cReveal('c1',()=>({
-  type:'bar',
-  data:{labels:ranges.map((_,i)=>i+1),
-    datasets:[{data:ranges,backgroundColor:ranges.map((_,i)=>i===WIDEST(ranges)?HERO:DATA),borderRadius:99,borderSkipped:false,barPercentage:.32}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:800,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*40:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw[0]+'K – '+i.raw[1]+'K users'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},
-         ticks:{padding:6,font:{size:9},callback:v=>[0,6,13].includes(v)?(v+1)+' FEB':''}},
-      y:{min:50,max:320,grid:{display:false},border:{display:false},
-         ticks:{padding:8,maxTicksLimit:6,callback:v=>v+'K',font:{size:9.5}}},
-    },
-  },
-}));
-})();
-
-// ════ mono-demo · 2: petal rose on dark ════
-(()=>{
-const EMO=[
-  ['Happiness',12],['Awe',10],['Admiration',5],['Surprise',12],
-  ['Sadness',6],['Fear',4],['Anger',2],['Anticipation',5],
-];
-const MAXV=12;
-const petal=v=>{const t=v/MAXV;return t>.8?DRAMP4[0]:t>.6?DRAMP4[1]:t>.35?DRAMP4[2]:DRAMP4[3]};
-// 标签固定落在环宽中段,只有票数够多的花瓣才铺到那儿;没铺到的按卡底配字色
-const onPetal=v=>v/MAXV>.6;
-eReveal('rose',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.name+' — '+EMO[p.dataIndex][1]},
-  series:[
-    {type:'pie',silent:false,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:DSHELL,borderRadius:16,borderColor:DARK,borderWidth:5},
-     label:{show:false},emphasis:{scale:false},
-     data:EMO.map(([n])=>({name:n,value:1})),z:1},
-    {type:'pie',silent:true,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:'transparent'},label:{position:'inside'},
-     data:EMO.map(([n,v])=>({name:n,value:1,
-       label:{formatter:`{n|${v}}\n{l|${n}}`,rich:{
-         n:{fontSize:17,fontWeight:700,fontFamily:'Inter',lineHeight:20,
-              color:onPetal(v)?readable(petal(v)):DMID},
-         l:{fontSize:9.5,fontFamily:'Inter',
-              color:onPetal(v)?alpha(readable(petal(v)),.66):DQ},
-       }}})),z:3},
-    {type:'pie',roseType:'area',silent:true,radius:['14%','88%'],center:['50%','50%'],
-     itemStyle:{borderRadius:14,borderColor:DARK,borderWidth:5},label:{show:false},
-     data:EMO.map(([n,v])=>({name:n,value:v,itemStyle:{color:petal(v)}})),z:2},
-  ],
-});
-})();
-
-// ════ mono-demo · 3: chunky bars, values on top ════
-(()=>{
-const V3=[182,486,391,274],rk3=rankOf(V3);
-cReveal('c3',()=>({
-  type:'bar',
-  data:{labels:['STARTER','PRO','TEAM','ENT'],
-    datasets:[{data:V3,
-      backgroundColor:V3.map((_,i)=>rampAt(rk3(i),4)),
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>'$'+i.raw+'K MRR'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
-      y:{display:false,suggestedMax:540},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText('$'+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-// ════ mono-redesign · donut → waffle of dots ════
-(()=>{
-const NS='http://www.w3.org/2000/svg';
-const mkEl=(svg,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);svg.appendChild(n);return n};
-const D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];
-obsReveal('waffle',s=>{
-  s.innerHTML='';
-  const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;
-  let idx=0;
-  D.forEach(([name,v],g)=>{
-    for(let k=0;k<v;k++){
-      const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;
-      mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:g===0?hi(SER[0]):SER[g],
-        class:'pop',style:`animation-delay:${c*.008+g*.05}s`});
-    }
-    idx+=v;
-  });
-  D.forEach(([name,v],g)=>{
-    const y=26+g*40;
-    mkEl(s,'circle',{cx:246,cy:y,r:6,fill:g===0?hi(SER[0]):SER[g]});
-    const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});
-    n.textContent=name;
-    const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g<2?TXT:MUT});
-    val.textContent=v+'%';
-  });
-});
-})();
-
-// ════ mono-fancy · 1: pictorial bar forest ════
-(()=>{
-const TREE='path://M20,0 L38,28 L29,28 L40,46 L26,46 L26,58 L14,58 L14,46 L0,46 L11,28 L2,28 Z';
-const years=['2022','2023','2024','2025','2026'];
-const vals=[26,41,63,88,117];
-eReveal('forest',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},
-  grid:{left:44,right:56,top:10,bottom:8},
-  yAxis:{type:'category',data:years,inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},
-  xAxis:{show:false,max:13},
-  series:[{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:RAMP[i]}})),
-    label:{show:true,position:'right',offset:[8,0],
-      color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,
-      formatter:p=>vals[p.dataIndex]+'k'},
-    z:10,
-  },{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    itemStyle:{color:QUIET},data:years.map(()=>13),silent:true,z:5,
-  }],
-});
-})();
-
-// ════ mono-fancy · 2: circular graph on dark ════
-(()=>{
-const teams=[
-  ['Product',52],['Design',38],['Frontend',46],['Backend',44],
-  ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],
-];
-const links=[
-  [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],
-  [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],
-];
-eReveal('circular',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:tipDark,
-  series:[{
-    type:'graph',layout:'circular',circular:{rotateLabel:false},
-    left:52,right:52,top:34,bottom:34,
-    data:teams.map(([n,v],i)=>({
-      name:n,value:v,symbolSize:v*.62,
-      itemStyle:{color:i===0?HERODK:i<4?BG:i<8?DMID:DQ,borderWidth:0},
-      label:{show:true,position:'right',distance:7,color:DMID,fontFamily:'Inter',fontSize:9.5,fontWeight:600},
-    })),
-    links:links.map(([s,t,w])=>({
-      source:s,target:t,
-      lineStyle:{width:w*.7,color:DLINE,opacity:.55,curveness:.28},
-    })),
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},
-  }],
-});
-})();
-
-// ════ mono-fancy · 3: LR tree ════
-(()=>{
-const b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},
-  children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});
-eReveal('tree',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:tipLight,
-  series:[{
-    type:'tree',layout:'orthogonal',orient:'LR',
-    left:64,right:96,top:8,bottom:8,
-    symbol:'circle',symbolSize:7,
-    initialTreeDepth:2,expandAndCollapse:false,roam:false,
-    itemStyle:{borderWidth:0},
-    lineStyle:{width:1.4,curveness:.5},
-    label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:LAB,position:'left'},
-    leaves:{label:{position:'right',color:MUTED,fontWeight:500}},
-    data:[{
-      name:'Platform',itemStyle:{color:TXT},lineStyle:{color:FAINT},
-      label:{fontSize:11.5,color:INK},
-      children:[
-        b('Editor',['Blocks','Tables','Comments','History'],hi(SER[0])),
-        b('Automate',['Workflows','Triggers','Webhooks'],SER[1]),
-        b('Collaborate',['Spaces','Guests','Mentions'],SER[2]),
-        b('Integrate',['API','Slack','GitHub'],SER[3]),
-      ],
-    }],
-  }],
-});
-})();
-
-// ════ mono-fancy · 4: rainfall dual area ════
-(()=>{
-const days=Array.from({length:30},(_,i)=>i+1);
-const flow=[42,45,44,48,52,55,53,58,64,62,66,71,69,75,82,79,84,80,86,92,88,95,91,97,104,101,108,105,112,118];
-const rain=[2,0,4,8,3,0,6,12,7,3,9,15,6,2,11,5,8,14,4,9,6,13,3,7,10,5,12,8,15,6];
-const RMAX=Math.max(...rain);
-eReveal('rainfall',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipLight,trigger:'axis',
-    formatter:p=>`Day ${p[0].axisValue} — spend $${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},
-  axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:FAINT}},
-  grid:[
-    {left:44,right:14,top:8,height:'26%'},
-    {left:44,right:14,top:'42%',bottom:26},
-  ],
-  xAxis:[
-    {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},
-    {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},
-  ],
-  yAxis:[
-    {gridIndex:0,inverse:true,max:18,splitLine:{show:false},
-     axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,formatter:'${value}K'},
-     axisLine:{show:false},axisTick:{show:false}},
-    {gridIndex:1,splitLine:{lineStyle:{color:GRID}},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},
-     axisLine:{show:false},axisTick:{show:false}},
-  ],
-  series:[
-    {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,
-     data:rain.map(v=>({value:v,itemStyle:{color:v===RMAX?hi(DATA2):DATA2}})),
-     itemStyle:{borderRadius:[0,0,4,4]},barWidth:'55%'},
-    {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,
-     smooth:.4,symbol:'none',lineStyle:{color:DATA,width:2.2},
-     areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-       colorStops:[{offset:0,color:alpha(DATA,.22)},{offset:1,color:alpha(DATA,0)}]}}},
-  ],
-});
-})();
-
-// ════ mono-fancy · 5: universal transition morph ════
-(()=>{
-const P12=[
-  ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],
-  ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],
-  ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],
-  ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],
-];
-const RK12=[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]);
-const shade=n=>rampAt(RK12.indexOf(n),12);
-const axis={splitLine:{lineStyle:{color:GRID}},axisLine:{show:false},axisTick:{show:false},
-  axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};
-const VIEWS=[
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:30},
-    xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE $',nameTextStyle:{color:FAINT,fontSize:8.5}},
-    yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:FAINT,fontSize:8.5}},
-    series:[{
-      id:'p',type:'scatter',universalTransition:true,
-      symbolSize:d=>Math.sqrt(d[3])*1.35,
-      data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],
-        groupId:d[0],itemStyle:{color:shade(d[0])}})),
-      label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,
-        formatter:p=>p.name},
-    }],
-  }},
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:44},
-    xAxis:{...axis,type:'category',
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),
-      axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},
-    yAxis:{...axis,type:'value',max:520},
-    series:[{
-      id:'p',type:'bar',universalTransition:true,
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(d[0]),borderRadius:[6,6,0,0]}})),
-      barCategoryGap:'32%',
-    }],
-  }},
-  {opt:{
-    grid:{left:0,right:0,top:0,bottom:0},
-    xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},
-    series:[{
-      id:'p',type:'pie',universalTransition:true,
-      radius:['26%','72%'],center:['50%','50%'],
-      itemStyle:{borderColor:BG,borderWidth:2,borderRadius:6},
-      label:{color:LAB,fontFamily:'Inter',fontSize:9,formatter:'{b}'},
-      labelLine:{lineStyle:{color:FAINT}},
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(d[0])}})),
-    }],
-  }},
-];
-const base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};
-obsReveal('morph',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let vi=0;
-  g.setOption({...base,...VIEWS[0].opt});
-  keep('morph',setInterval(()=>{
-    vi=(vi+1)%VIEWS.length;
-    g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});
-  },3000));
-});
-})();
-
-// ════ mono-fancy2 · 2: diverging bar ════
-(()=>{
-const D=[
-  ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],
-  ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],
-];
-const MAXG=Math.max(...D.map(d=>d[1]));
-eReveal('negbar',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},
-  grid:{left:96,right:52,top:8,bottom:8},
-  xAxis:{type:'value',
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-  yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-  series:[{
-    type:'bar',barWidth:16,
-    data:D.map(([n,v])=>({name:n,value:v,
-      itemStyle:{color:v>0?(v===MAXG?hi(DATA):DATA):DATA2,
-        borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),
-    label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,
-      position:'outside',
-      formatter:p=>(p.value>0?'+':'')+p.value,color:TXT},
-    markLine:{symbol:'none',silent:true,label:{show:false},
-      lineStyle:{color:MUT,width:1.5},
-      data:[{xAxis:0}]},
-  }],
-});
-})();
-
-// ════ force graph on dark: the network shakes itself into place ════
-// (replaced the axis-break bar — bars must never lie about length; house rule)
-(()=>{
-const hubs=[['Core API',52,0]];
-const apps=[
-  ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],
-  ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],
-  ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],
-];
-const nodes=[...hubs,...apps];
-const links=[
-  ...apps.map((a,i)=>({source:a[0],target:'Core API',
-    lineStyle:{width:Math.max(.8,a[1]*.09),color:DLINE,opacity:.6}})),
-  // a few app-to-app side roads
-  {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},
-  {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},
-  {source:'Figma',target:'Notion',lineStyle:{width:1,color:DLINE2,opacity:.5}},
-  {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:DLINE2,opacity:.5}},
-];
-eReveal('force',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},
-  series:[{
-    type:'graph',layout:'force',
-    force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},
-    roam:false,draggable:true,
-    left:14,right:14,top:14,bottom:14,
-    data:nodes.map(([n,v,tier])=>({
-      name:n,value:v,symbolSize:8+v*.75,
-      itemStyle:{color:tier===0?HERODK:tier===1?BG:DMID},
-      label:{show:true,position:'right',distance:6,
-        color:tier===0?BG:DMID,fontFamily:'Inter',
-        fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},
-    })),
-    links,
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},
-  }],
-});
-})();
-
-// ════ mono-fancy2 · 4: stagger wave ════
-(()=>{
-const n=50;
-const vals=Array.from({length:n},(_,i)=>
-  Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));
-const rkW=rankOf(vals);
-eReveal('wave',{
-  tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},
-  grid:{left:36,right:10,top:14,bottom:24},
-  xAxis:{type:'category',data:vals.map((_,i)=>i+1),
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},
-  yAxis:{type:'value',
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},
-  series:[{
-    type:'bar',barCategoryGap:'26%',
-    data:vals.map((v,i)=>({value:v,
-      itemStyle:{color:rampAt(rkW(i),vals.length),borderRadius:[4,4,0,0]}})),
-    animationDelay:i=>i*36,
-    animationDuration:640,animationEasing:'elasticOut',
-  }],
-  animationDurationUpdate:640,
-  animationDelayUpdate:i=>i*36,
-});
-})();
-
-// ════ mono-fancy3 · 1: custom double-encoded pie ════
-(()=>{
-const D=[
-  ['Editor',34,42],['Boards',22,31],['Docs',18,38],
-  ['Chat',12,11],['Automations',8,24],['Other',6,7],
-];
-const SCALE=45;
-const TAU=Math.PI*2;
-const byShare=[...D].sort((a,b)=>b[1]-a[1]);
-const minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);
-let acc=0;
-const slices=byShare.map(([n,share,mins])=>{
-  const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;
-  acc+=share;
-  return {n,share,mins,p0,p1,color:rampAt(minsDesc.indexOf(mins),6)};
-});
-eReveal('custompie',{
-  animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,
-  tooltip:{...tipLight,formatter:p=>{
-    const s=slices[p.dataIndex];
-    return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';
-  }},
-  xAxis:{show:false,min:-1,max:1},
-  yAxis:{show:false,min:-1,max:1},
-  grid:{left:0,right:0,top:0,bottom:0},
-  series:[{
-    type:'custom',
-    renderItem:(params,api)=>{
-      const s=slices[params.dataIndex];
-      const cx=api.getWidth()/2, cy=api.getHeight()/2+6;
-      const R=Math.min(api.getWidth(),api.getHeight())*.37;
-      const r0=R*.24;
-      const rOf=m=>r0+(R-r0)*m/SCALE;
-      const pm=(s.p0+s.p1)/2, lr=R+14;
-      const cos=Math.cos(pm);
-      const kids=[];
-      if(params.dataIndex===0){
-        [15,30,45].forEach(m=>kids.push({type:'circle',
-          shape:{cx,cy,r:rOf(m)},
-          style:{fill:'none',stroke:GRID,lineWidth:1,lineDash:[3,4]}}));
-      }
-      kids.push({type:'sector',
-        shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},
-        style:{fill:s.color,stroke:BG,lineWidth:3},
-        enterFrom:{shape:{r:r0}}});
-      kids.push({type:'text',style:{
-        x:cx+cos*lr, y:cy+Math.sin(pm)*lr,
-        text:s.n+'  '+s.share+'% · '+s.mins+'m',
-        font:'600 10px Inter',fill:LAB,
-        align:Math.abs(cos)<.25?'center':(cos>0?'left':'right'),
-        verticalAlign:'middle'},
-        enterFrom:{style:{opacity:0}}});
-      return {type:'group',children:kids};
-    },
-    data:slices.map(s=>s.share),
-  }],
-});
-})();
-
-// ════ mono-fancy3 · 3: single-axis small multiples ════
-(()=>{
-const days=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-const hours=Array.from({length:24},(_,i)=>i);
-const mkRow=di=>hours.map(h=>{
-  const workday=di<5, core=h>=9&&h<=17;
-  let v=Math.round((workday&&core?6+5*Math.sin((h-9)/8*Math.PI):0)+((h+di)%5===0?2:0)+(h>=22||h<=5?0:1));
-  return [h,v];
-}).filter(d=>d[1]>0);
-eReveal('singleaxis',{
-  animationDuration:500,animationEasing:'backOut',
-  tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},
-  title:days.map((d,i)=>({
-    text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,
-    textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},
-  })),
-  singleAxis:days.map((d,i)=>({
-    left:52,right:14,type:'category',boundaryGap:false,
-    data:hours.map(h=>String(h).padStart(2,'0')),
-    top:i*100/7+6+'%',height:100/7-8+'%',
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},
-    splitLine:{show:false},
-  })),
-  series:days.map((d,i)=>({
-    singleAxisIndex:i,coordinateSystem:'singleAxis',
-    type:'scatter',
-    data:mkRow(i),
-    symbolSize:dd=>dd[1]*2.6,
-    itemStyle:{color:i<5?(i===0?hi(SER[0]):SER[i]):FAINTDATA},
-    animationDelay:di=>i*160+di*14,
-  })),
-});
-})();
-
-// ════ mono-fancy3 · 4: jitter strip ════
-(()=>{
-const cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];
-const mkPts=(ci,n,median,spread,outliers)=>{
-  const pts=[];
-  for(let i=0;i<n;i++){
-    const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);
-    let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);
-    if(i<outliers) x=median+spread*2.2+u*spread*3;
-    const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;
-    pts.push([Math.max(.2,x),jy]);
-  }
-  return pts;
-};
-const groups=[
-  mkPts(0,38,.8,.5,2),
-  mkPts(1,64,2.4,1.1,3),
-  mkPts(2,110,6.5,2.4,4),
-  mkPts(3,72,14,4.5,3),
-];
-const flat=groups.flat();
-eReveal('jitter',{
-  animationDuration:450,animationEasing:'cubicOut',
-  animationDelay:i=>i<flat.length?Math.round(flat[i][1])*260+(i%37)*9:0,
-  tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},
-  grid:{left:86,right:16,top:14,bottom:30},
-  xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:FAINT,fontSize:8.5},
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},
-  yAxis:{type:'value',min:-.6,max:3.6,inverse:true,
-    splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:false}},
-  series:[{
-    type:'scatter',
-    data:flat,
-    symbolSize:7,
-    itemStyle:{color:p=>RAMP[4-Math.round(p.value[1])],opacity:.62},
-  },{
-    type:'scatter',silent:true,symbolSize:0,
-    data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],
-      color:LAB,fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),
-  }],
-});
-})();
-
-// ════ mono-fancy4 · 1: bar race ════
-(()=>{
-const P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];
-const YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];
-const vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];
-for(let y=1;y<YEARS.length;y++)
-  vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));
-obsReveal('race',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let step=0;
-  const frame=()=>{
-    const row=vals[step];
-    const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);
-    g.setOption({
-      animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',
-      tooltip:{...tipLight,valueFormatter:v=>'$'+Math.round(v)+'K'},
-      grid:{left:64,right:64,top:8,bottom:8},
-      xAxis:{type:'value',max:'dataMax',
-        splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-      yAxis:{type:'category',data:P,inverse:true,max:7,
-        animationDuration:300,animationDurationUpdate:300,
-        axisLine:{show:false},axisTick:{show:false},
-        axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-      series:[{
-        type:'bar',realtimeSort:true,barCategoryGap:'32%',
-        data:row.map((v,i)=>({value:v,
-          itemStyle:{color:rampAt(rank.indexOf(i),8),borderRadius:99}})),
-        label:{show:true,position:'right',valueAnimation:true,
-          fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,
-          formatter:p=>'$'+Math.round(p.value)+'K'},
-      }],
-      graphic:[{id:'yr',type:'text',right:18,bottom:14,
-        style:{text:YEARS[step],font:'800 44px Inter',fill:GRID}}],
-    });
-    step=(step+1)%YEARS.length;
-  };
-  frame();
-  keep('race',setInterval(frame,1150));
-});
-})();
-
-// ════ mono-fancy4 · 2: dynamic data stream ════
-(()=>{
-const WIN=50;
-obsReveal('stream',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let t=0,v=64;
-  const data=[];
-  for(;t<WIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}
-  const draw=()=>g.setOption({
-    animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',
-    tooltip:{...tipLight,valueFormatter:x=>x+'k users'},
-    grid:{left:14,right:58,top:44,bottom:20},
-    xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},
-    yAxis:{show:false,min:20,max:130},
-    series:[{
-      type:'line',data:[...data],smooth:.45,symbol:'none',
-      lineStyle:{color:DATA,width:2.4},
-      endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:TXT,
-        formatter:p=>p.value+'k'},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:alpha(DATA,.16)},{offset:1,color:alpha(DATA,0)}]}},
-    }],
-    graphic:[{type:'group',left:16,top:10,children:[
-      {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:HERO}},
-      {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:TXT}},
-    ]}],
-  });
-  draw();
-  keep('stream',setInterval(()=>{
-    v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));
-    data.push(Math.round(v));data.shift();t++;
-    draw();
-  },300));
-});
-})();
-
-// ════ mono-fancy4 · 3: draw-in + counter ════
-(()=>{
-const days=Array.from({length:180},(_,i)=>i);
-const daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);
-const cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);
-const TOTAL=cum[cum.length-1];
-const DUR=2600;
-let gen=0; // replay guard: only the latest counter loop may write
-obsReveal('stroke',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  gen++;const my=gen;
-  g.setOption({
-    animationDuration:DUR,animationEasing:'cubicOut',
-    tooltip:{show:false},
-    grid:{left:14,right:20,top:64,bottom:24},
-    xAxis:{type:'category',data:days,boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
-      axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,fontWeight:600,
-        interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},
-    yAxis:{show:false,max:TOTAL*1.06},
-    series:[{
-      type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',
-      lineStyle:{color:DATA,width:2.6},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:alpha(DATA,.14)},{offset:1,color:alpha(DATA,0)}]}},
-    }],
-  });
-  const t0=performance.now();
-  const tick=()=>{
-    if(my!==gen)return;
-    const p=Math.min(1,(performance.now()-t0)/DUR);
-    const eased=1-Math.pow(1-p,3);
-    const val=(TOTAL*eased/1000).toFixed(2);
-    g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[
-      {type:'text',style:{text:'$'+val+'M',font:'800 32px Inter',fill:hi(TXT)}},
-      {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:MUT}},
-    ]}]});
-    if(p<1)requestAnimationFrame(tick);
-  };
-  requestAnimationFrame(tick);
-});
-})();
-
-addEventListener('resize',()=>{
-  ['rose','forest','circular','tree','rainfall','morph','negbar','force','wave',
-   'custompie','singleaxis','jitter','race','stream','stroke']
-    .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ G19 · violin ════
-// 小提琴:提前聚合的密度剪影,Glance 世界观(beeswarm 的姊妹)。
-// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),
-// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。
-(()=>{
-const G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];
-const M=64;
-const samples=g=>Array.from({length:M},(_,i)=>{
-  const [,c,s]=G[g];
-  const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);
-  return Math.max(.2,v);
-});
-obsReveal('violin',s=>{
-  const TOP=40,BASE=262,HMAX=17;
-  const mapY=h=>BASE-h/HMAX*(BASE-TOP);
-  const colX=g=>76+g*84;
-  // hour grid
-  [0,4,8,12,16].forEach(h=>{
-    el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(88,64,46,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${h*.01}s`});
-    txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.01}s`},h+'h');
-  });
-  const SHADE=[INK,'#77835A','#929960','#ACAD79'];
-  G.forEach(([name],g)=>{
-    const vs=samples(g).sort((a,b)=>a-b);
-    const med=vs[Math.floor(M/2)];
-    // fuller bandwidth = the classic rounded violin belly, not a spindle
-    const bw=Math.max(.9,G[g][2]*.72);
-    // pad past the data by 1.6 bandwidths so the density decays to ~0 and the
-    // ends taper naturally — clipping tighter gives flat-cut lantern ends
-    const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);
-    const NPTS=44;
-    const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));
-    const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));
-    const dmax=Math.max(...dens);
-    const W=30;
-    const x=colX(g);
-    // smooth closed outline through the mirrored density — quadratic
-    // midpoint smoothing, same trick as almanac's blob
-    const pts=[];
-    grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));
-    for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<pts.length;t++){
-      const p=pts[t],q=pts[(t+1)%pts.length];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});
-    tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);
-    // median: paper tick across the body, value parked right
-    const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));
-    const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;
-    el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),
-      stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});
-    txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'WIDTH = HOW MANY TICKETS WAIT THAT LONG · PAPER TICK = MEDIAN · DARKEST = FASTEST');
-});
-})();
-
-// ════ G20 · matrix heat ════
-// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出
-// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];
-const VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];
-const BASEV=[88,74,61,42,35,27];
-const v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));
-const shade=t=>t>64?INK:t>46?'#77835A':t>30?'#929960':t>15?'#ACAD79':'#F2D17E';
-obsReveal('gheat',s=>{
-  const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;
-  FEAT.forEach((f,c)=>{
-    txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);
-  });
-  VER.forEach((ver,r)=>{
-    txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'rgba(88,64,46,.72)','text-anchor':'end',
-      class:'fade',style:`animation-delay:${r*.05}s`},ver);
-    FEAT.forEach((f,c)=>{
-      const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;
-      const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),
-        class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});
-      tip(cell,`${f} on ${ver} — ${t}% of accounts`);
-      txt(s,{x:x+W/2,y:y+H/2+4,'font-size':10.5,'font-weight':800,fill:dark?PAPER:INK,'text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);
-    });
-  });
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');
-});
-})();
-
-// ════ G21 · rank strip ════
-// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,
-// 和 slope 同一种丑(catalog 实测三连败那条),废弃。
-// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。
-// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。
-(()=>{
-const RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],
-  ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];
-const SHADE=[INK,'#77835A','#929960','#ACAD79','#F2D17E'];   // rank 1 → 5
-obsReveal('rankstrip',s=>{
-  // rows sorted by final rank, the climber lands on top
-  const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);
-  const X0=110,PX=38,Y0=64,PY=42,C=32;
-  ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>
-    txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));
-  rows.forEach(([p,rk],i)=>{
-    const y=Y0+i*PY,hero=p==='Flows';
-    txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'rgba(88,64,46,.72)','text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.07}s`},p);
-    rk.forEach((r,q)=>{
-      const dark=r<=2;
-      const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],
-        stroke:'rgba(88,64,46,.24)','stroke-width':0.9,
-        class:'pop',style:`animation-delay:${i*.07+q*.035}s`});
-      tip(cell,`${p} — #${r} in Q${q+1}`);
-      txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,
-        fill:dark?PAPER:'#58402E','text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);
-    });
-    // arrival flag: climbed / fell / held, told at the row's end
-    const d=rk[0]-rk[5];
-    txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,
-      fill:d>0?INK:d<0?'#ACAD79':'rgba(88,64,46,.32)',
-      class:'fade',style:`animation-delay:${.5+i*.07}s`},
-      d>0?'▲'+d:d<0?'▼'+(-d):'—');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');
-});
-})();
-
-// ════ G22 · aggregate sankey ════
-// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——
-// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。
-// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。
-(()=>{
-const SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];
-const DST=['FREE','PRO','TEAM'];
-// 渠道是类目:一渠道一色,走 lupi-palm 的 SER 分类位(含琥珀、麦黄)
-const SH=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79'];
-obsReveal('sankey',s=>{
-  const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;
-  const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));
-  const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));
-  const sy=[],dy=[];
-  let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});
-  y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});
-  const syc=[...sy],dyc=[...dy];
-  SRC.forEach((d,i)=>{
-    d[1].forEach((v,j)=>{
-      if(!v)return;
-      const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;
-      syc[i]=sb;dyc[j]=db;
-      const rib=el(s,'path',{d:`M${XL} ${sa} C${MID} ${sa} ${MID} ${da} ${XR} ${da} L${XR} ${db} C${MID} ${db} ${MID} ${sb} ${XL} ${sb} Z`,
-        fill:SH[i],opacity:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});
-      tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);
-    });
-  });
-  SRC.forEach((d,i)=>{
-    el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],
-      class:'fade',style:`animation-delay:${i*.06}s`});
-    txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#77835A','text-anchor':'end',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);
-  });
-  DST.forEach((n,j)=>{
-    el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#43593B',
-      class:'fade',style:`animation-delay:${.1+j*.06}s`});
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#77835A',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'RIBBON WIDTH = ACCOUNTS OF 100 · COLOR = CHANNEL');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 1171
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/glance-porcelain.html

@@ -1,1171 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Glance 22 × PORCELAIN</title>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F7F2EB;--dark:#081F5C;--ink:#081F5C;--muted:rgba(8,31,92,.60);--faint:rgba(8,31,92,.40);--grid:rgba(8,31,92,.16);--lab:rgba(8,31,92,.72);--dmid:rgba(247,242,235,.62);--dq:rgba(247,242,235,.24)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:var(--bg)}
-  .card.dark .sub{color:var(--dmid)}
-  .card.dark .src{color:var(--dq)}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .ch{height:320px}
-  .wrap{position:relative;height:320px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:var(--ink);color:var(--bg)}
-  .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}
-  .badge.pair{border:1px dashed var(--faint);color:var(--muted)}
-  .card.dark .badge.del{background:var(--bg);color:var(--ink)}
-  .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}
-  .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-    <h1>Glance 系(快读)· 22 张 × PORCELAIN</h1>
-  <p><span class="chip" style="background:#334EAC"></span><span class="chip" style="background:#7096D1"></span><span class="chip" style="background:#BAD6EB"></span><span class="chip" style="background:#D0E3FF"></span><span class="chip" style="background:#081F5C"></span><span class="chip" style="background:#F7F2EB"></span> &nbsp;青瓷蓝家族 · Dawn → Sky → China → Royal → Midnight 五档全是原厂色号,零补档。Moon 当底,Midnight 当文字兼主角。</p>
-  <p style="margin-top:7px">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 个来源 / 4 条产品线 / 5 个工作日 / 10 支小队 / 12 个集成)各走一个分类位,序数型(8 年营收排名 / 50 个市场 / 4 档优先级 / 树木年份 / 面板停留时长 / 情绪票数)走 5 档梯,深色卡换纸色梯、末档落在强调色。几何、动画、数据与 glance-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <!-- ══ mono-demo ══ -->
-  <div class="card">
-    <h2>Daily active range</h2>
-    <div class="sub">min – max concurrent users · 1–14 Feb</div>
-    <div class="wrap"><canvas id="c1"></canvas></div>
-    <div class="src">RANGE CAPSULES · MONO-DEMO · LIVE OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>How releases made us feel</h2>
-    <div class="sub">reactions tagged in retro · H1 2026</div>
-    <div class="ch" id="rose"></div>
-    <div class="src">PETAL ROSE · MONO-DEMO · TEAM RETRO BOARD</div>
-  </div>
-
-  <div class="card">
-    <h2>Revenue by plan</h2>
-    <div class="sub">Q2 2026 · MRR, $K</div>
-    <div class="wrap"><canvas id="c3"></canvas></div>
-    <div class="src">CHUNKY BARS · MONO-DEMO · BILLING</div>
-  </div>
-
-  <!-- ══ mono-redesign · dot waffle ══ -->
-  <div class="card">
-    <h2>Where sign-ups come from</h2>
-    <div class="sub">Q2 2026 · every dot = 1% of new accounts</div>
-    <svg id="waffle" viewBox="0 0 380 300" width="100%" style="max-height:320px"></svg>
-    <div class="src">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION</div>
-  </div>
-
-  <!-- ══ mono-fancy ══ -->
-  <div class="card">
-    <h2>Trees planted, year by year</h2>
-    <div class="sub">reforestation program · thousands of trees</div>
-    <div class="ch" id="forest"></div>
-    <div class="src">PICTORIAL BAR · MONO-FANCY · GREEN OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Who works with whom</h2>
-    <div class="sub">cross-team collaborations · Q2 2026 · line = shared project</div>
-    <div class="ch" id="circular"></div>
-    <div class="src">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB</div>
-  </div>
-
-  <div class="card">
-    <h2>Everything the platform ships</h2>
-    <div class="sub">product areas and capabilities · Jun 2026</div>
-    <div class="ch" id="tree"></div>
-    <div class="src">TREE · MONO-FANCY · PRODUCT CATALOG</div>
-  </div>
-
-  <div class="card">
-    <h2>Campaigns rain down, sign-ups flow</h2>
-    <div class="sub">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days</div>
-    <div class="ch" id="rainfall"></div>
-    <div class="src">DUAL AREA · MONO-FANCY · GROWTH + ADS</div>
-  </div>
-
-  <div class="card">
-    <h2>One dataset, three views</h2>
-    <div class="sub">watch the same 12 products fly between views · every 3s</div>
-    <div class="ch" id="morph"></div>
-    <div class="src">SCATTER MORPH · MONO-FANCY · COHORTS</div>
-  </div>
-
-  <!-- ══ mono-fancy2 ══ -->
-  <div class="card">
-    <h2>Where we gained, where we bled</h2>
-    <div class="sub">net accounts per segment · Q2 2026 · churn left, growth right</div>
-    <div class="ch" id="negbar"></div>
-    <div class="src">DIVERGING BAR · MONO-FANCY2 · CRM</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Integrations, pulled into orbit</h2>
-    <div class="sub">force layout · node = monthly syncs · drag a node, the web fights back</div>
-    <div class="ch" id="force"></div>
-    <div class="src">FORCE GRAPH · NEW · INTEGRATION HUB</div>
-  </div>
-
-  <div class="card">
-    <h2>Fifty markets, one wave</h2>
-    <div class="sub">sign-ups per market · bars cascade in · click to replay</div>
-    <div class="ch" id="wave"></div>
-    <div class="src">STAGGER DELAY · MONO-FANCY2 · GROWTH</div>
-  </div>
-
-  <!-- ══ mono-fancy3 ══ -->
-  <div class="card">
-    <h2>Big slice, deep engagement</h2>
-    <div class="sub">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min</div>
-    <div class="ch" id="custompie"></div>
-    <div class="src">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS</div>
-  </div>
-
-  <div class="card">
-    <h2>Support load, day by day</h2>
-    <div class="sub">tickets per hour · each row is its own axis · dot = volume</div>
-    <div class="ch" id="singleaxis"></div>
-    <div class="src">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <div class="card">
-    <h2>Response times, spread out</h2>
-    <div class="sub">every dot = one ticket · jitter reveals the pile-up · by priority</div>
-    <div class="ch" id="jitter"></div>
-    <div class="src">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <!-- ══ mono-fancy4 ══ -->
-  <div class="card">
-    <h2>Eight products race for revenue</h2>
-    <div class="sub">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay</div>
-    <div class="ch" id="race"></div>
-    <div class="src">BAR RACE · MONO-FANCY4 · BILLING</div>
-  </div>
-
-  <div class="card">
-    <h2>Concurrent users, streaming</h2>
-    <div class="sub">one new reading every 300ms · window of 50 · click to restart</div>
-    <div class="ch" id="stream"></div>
-    <div class="src">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS</div>
-  </div>
-
-  <div class="card">
-    <h2>H1 revenue, drawn in one stroke</h2>
-    <div class="sub">cumulative ARR · the line draws, the number counts · click to replay</div>
-    <div class="ch" id="stroke"></div>
-    <div class="src">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE</div>
-  </div>
-
-
-  <!-- G19 · violin -->
-  <div class="card">
-    <span class="badge">GLANCE 分布型 · 小提琴</span>
-    <h2>How fast each plan gets an answer</h2>
-    <div class="sub">hours to first reply · silhouette = where tickets pile up · paper tick = median</div>
-    <svg id="violin" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">VIOLIN · PORCELAIN · SUPPORT DESK</div>
-  </div>
-
-  <!-- G20 · matrix heat -->
-  <div class="card">
-    <span class="badge">GLANCE 热力型 · 通用矩阵</span>
-    <h2>Adoption runs hot on the new versions</h2>
-    <div class="sub">5 versions × 6 features · shade = % of accounts using it · every cell carries its number</div>
-    <svg id="gheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT (GLANCE) · PORCELAIN · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- G21 · rank strip -->
-  <div class="card">
-    <span class="badge">GLANCE 排名型 · RANK STRIP</span>
-    <h2>Flows climbs to the top</h2>
-    <div class="sub">one row per product, one cell per quarter · darker = higher rank · a bar race you can print</div>
-    <svg id="rankstrip" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RANK STRIP · PORCELAIN · BILLING</div>
-  </div>
-
-  <!-- G22 · aggregate sankey -->
-  <div class="card">
-    <span class="badge">GLANCE 流向型 · 聚合桑基</span>
-    <h2>Channels pour into plans</h2>
-    <div class="sub">ribbon width = accounts · channels on the left, the plans they land in on the right</div>
-    <svg id="sankey" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">AGGREGATE SANKEY · PORCELAIN · ATTRIBUTION</div>
-  </div>
-
-</div>
-<script>
-// ── PORCELAIN 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F7F2EB',DARK='#081F5C',TXT='#081F5C';
-const MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',FAINT='rgba(8,31,92,.40)',QUIET='rgba(8,31,92,.15)',GRID='rgba(8,31,92,.16)';
-const DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C';
-const HERODK='#7096D1';                      // 深色卡上的强调色(HERO 在深底上可能读不出)
-const FAINTDATA='#BAD6EB';                 // 静音数据档:周末 / 非主序列
-const DMID='rgba(247,242,235,.62)',DQ='rgba(247,242,235,.24)';   // 深色卡上的浅墨 / 静音档
-const SER=['#334EAC','#081F5C','#7096D1','#BAD6EB','#D0E3FF','rgba(51,78,172,.4)'];   // 分类位:一图之内每个系列 / 每类 / 每行一个色
-const RAMP=['#D0E3FF','#BAD6EB','#7096D1','#334EAC','#081F5C'];   // 5 档序数梯(浅→重):排名 / 票量 / 峰值 / 停留时长
-const DRAMP4=['#D0E3FF','#BAD6EB','#7096D1','rgba(247,242,235,.26)'];   // 深色卡上的 4 档序数梯(浅→深)
-const HEROMODE=false; // 主角模式:分类位的头号项也整体走 HERO
-
-// ── 别名与派生:深色卡的线全是卡底向纸色的插值,不引入新色相 ──
-const INK=TXT,PAPER=BG,MUTED=MUT;
-const rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));
-const alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;
-const mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');
-const lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));
-  return .2126*r+.7152*g+.0722*b};
-const ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};
-const readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG); // 深色卡上落在色块里的字
-const DLINE=mix(DARK,BG,.20),DLINE2=mix(DARK,BG,.13),DSHELL=mix(DARK,BG,.09);
-const hi=c=>HEROMODE?HERO:c;
-// 名次 → 序数梯。末档(WIRE 的红)只发给第 1 名,其余摊在前四档上
-const rampAt=(i,n)=>i===0?RAMP[4]
-  :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];
-const rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);
-  return i=>r.indexOf(i)};
-const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── unified reveal: every chart waits to be scrolled into view; click replays.
-//    fn charts manage their own timers via keep(); they're cleared on replay.
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUTED;
-Chart.defaults.font.family='Inter';
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=INK;
-Chart.defaults.plugins.tooltip.titleColor=BG;
-Chart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-// ════ mono-demo · 1: floating range capsules ════
-(()=>{
-const WIDEST=r=>r.reduce((b,x,i)=>x[1]-x[0]>r[b][1]-r[b][0]?i:b,0);
-const ranges=[[195,235],[165,192],[150,232],[73,168],[122,202],[182,192],[138,178],[162,227],[195,235],[228,305],[218,232],[165,232],[118,210],[195,232]];
-cReveal('c1',()=>({
-  type:'bar',
-  data:{labels:ranges.map((_,i)=>i+1),
-    datasets:[{data:ranges,backgroundColor:ranges.map((_,i)=>i===WIDEST(ranges)?HERO:DATA),borderRadius:99,borderSkipped:false,barPercentage:.32}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:800,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*40:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw[0]+'K – '+i.raw[1]+'K users'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},
-         ticks:{padding:6,font:{size:9},callback:v=>[0,6,13].includes(v)?(v+1)+' FEB':''}},
-      y:{min:50,max:320,grid:{display:false},border:{display:false},
-         ticks:{padding:8,maxTicksLimit:6,callback:v=>v+'K',font:{size:9.5}}},
-    },
-  },
-}));
-})();
-
-// ════ mono-demo · 2: petal rose on dark ════
-(()=>{
-const EMO=[
-  ['Happiness',12],['Awe',10],['Admiration',5],['Surprise',12],
-  ['Sadness',6],['Fear',4],['Anger',2],['Anticipation',5],
-];
-const MAXV=12;
-const petal=v=>{const t=v/MAXV;return t>.8?DRAMP4[0]:t>.6?DRAMP4[1]:t>.35?DRAMP4[2]:DRAMP4[3]};
-// 标签固定落在环宽中段,只有票数够多的花瓣才铺到那儿;没铺到的按卡底配字色
-const onPetal=v=>v/MAXV>.6;
-eReveal('rose',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.name+' — '+EMO[p.dataIndex][1]},
-  series:[
-    {type:'pie',silent:false,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:DSHELL,borderRadius:16,borderColor:DARK,borderWidth:5},
-     label:{show:false},emphasis:{scale:false},
-     data:EMO.map(([n])=>({name:n,value:1})),z:1},
-    {type:'pie',silent:true,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:'transparent'},label:{position:'inside'},
-     data:EMO.map(([n,v])=>({name:n,value:1,
-       label:{formatter:`{n|${v}}\n{l|${n}}`,rich:{
-         n:{fontSize:17,fontWeight:700,fontFamily:'Inter',lineHeight:20,
-              color:onPetal(v)?readable(petal(v)):DMID},
-         l:{fontSize:9.5,fontFamily:'Inter',
-              color:onPetal(v)?alpha(readable(petal(v)),.66):DQ},
-       }}})),z:3},
-    {type:'pie',roseType:'area',silent:true,radius:['14%','88%'],center:['50%','50%'],
-     itemStyle:{borderRadius:14,borderColor:DARK,borderWidth:5},label:{show:false},
-     data:EMO.map(([n,v])=>({name:n,value:v,itemStyle:{color:petal(v)}})),z:2},
-  ],
-});
-})();
-
-// ════ mono-demo · 3: chunky bars, values on top ════
-(()=>{
-const V3=[182,486,391,274],rk3=rankOf(V3);
-cReveal('c3',()=>({
-  type:'bar',
-  data:{labels:['STARTER','PRO','TEAM','ENT'],
-    datasets:[{data:V3,
-      backgroundColor:V3.map((_,i)=>rampAt(rk3(i),4)),
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>'$'+i.raw+'K MRR'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
-      y:{display:false,suggestedMax:540},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText('$'+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-// ════ mono-redesign · donut → waffle of dots ════
-(()=>{
-const NS='http://www.w3.org/2000/svg';
-const mkEl=(svg,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);svg.appendChild(n);return n};
-const D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];
-obsReveal('waffle',s=>{
-  s.innerHTML='';
-  const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;
-  let idx=0;
-  D.forEach(([name,v],g)=>{
-    for(let k=0;k<v;k++){
-      const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;
-      mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:g===0?hi(SER[0]):SER[g],
-        class:'pop',style:`animation-delay:${c*.008+g*.05}s`});
-    }
-    idx+=v;
-  });
-  D.forEach(([name,v],g)=>{
-    const y=26+g*40;
-    mkEl(s,'circle',{cx:246,cy:y,r:6,fill:g===0?hi(SER[0]):SER[g]});
-    const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});
-    n.textContent=name;
-    const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g<2?TXT:MUT});
-    val.textContent=v+'%';
-  });
-});
-})();
-
-// ════ mono-fancy · 1: pictorial bar forest ════
-(()=>{
-const TREE='path://M20,0 L38,28 L29,28 L40,46 L26,46 L26,58 L14,58 L14,46 L0,46 L11,28 L2,28 Z';
-const years=['2022','2023','2024','2025','2026'];
-const vals=[26,41,63,88,117];
-eReveal('forest',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},
-  grid:{left:44,right:56,top:10,bottom:8},
-  yAxis:{type:'category',data:years,inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},
-  xAxis:{show:false,max:13},
-  series:[{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:RAMP[i]}})),
-    label:{show:true,position:'right',offset:[8,0],
-      color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,
-      formatter:p=>vals[p.dataIndex]+'k'},
-    z:10,
-  },{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    itemStyle:{color:QUIET},data:years.map(()=>13),silent:true,z:5,
-  }],
-});
-})();
-
-// ════ mono-fancy · 2: circular graph on dark ════
-(()=>{
-const teams=[
-  ['Product',52],['Design',38],['Frontend',46],['Backend',44],
-  ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],
-];
-const links=[
-  [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],
-  [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],
-];
-eReveal('circular',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:tipDark,
-  series:[{
-    type:'graph',layout:'circular',circular:{rotateLabel:false},
-    left:52,right:52,top:34,bottom:34,
-    data:teams.map(([n,v],i)=>({
-      name:n,value:v,symbolSize:v*.62,
-      itemStyle:{color:i===0?HERODK:i<4?BG:i<8?DMID:DQ,borderWidth:0},
-      label:{show:true,position:'right',distance:7,color:DMID,fontFamily:'Inter',fontSize:9.5,fontWeight:600},
-    })),
-    links:links.map(([s,t,w])=>({
-      source:s,target:t,
-      lineStyle:{width:w*.7,color:DLINE,opacity:.55,curveness:.28},
-    })),
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},
-  }],
-});
-})();
-
-// ════ mono-fancy · 3: LR tree ════
-(()=>{
-const b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},
-  children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});
-eReveal('tree',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:tipLight,
-  series:[{
-    type:'tree',layout:'orthogonal',orient:'LR',
-    left:64,right:96,top:8,bottom:8,
-    symbol:'circle',symbolSize:7,
-    initialTreeDepth:2,expandAndCollapse:false,roam:false,
-    itemStyle:{borderWidth:0},
-    lineStyle:{width:1.4,curveness:.5},
-    label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:LAB,position:'left'},
-    leaves:{label:{position:'right',color:MUTED,fontWeight:500}},
-    data:[{
-      name:'Platform',itemStyle:{color:TXT},lineStyle:{color:FAINT},
-      label:{fontSize:11.5,color:INK},
-      children:[
-        b('Editor',['Blocks','Tables','Comments','History'],hi(SER[0])),
-        b('Automate',['Workflows','Triggers','Webhooks'],SER[1]),
-        b('Collaborate',['Spaces','Guests','Mentions'],SER[2]),
-        b('Integrate',['API','Slack','GitHub'],SER[3]),
-      ],
-    }],
-  }],
-});
-})();
-
-// ════ mono-fancy · 4: rainfall dual area ════
-(()=>{
-const days=Array.from({length:30},(_,i)=>i+1);
-const flow=[42,45,44,48,52,55,53,58,64,62,66,71,69,75,82,79,84,80,86,92,88,95,91,97,104,101,108,105,112,118];
-const rain=[2,0,4,8,3,0,6,12,7,3,9,15,6,2,11,5,8,14,4,9,6,13,3,7,10,5,12,8,15,6];
-const RMAX=Math.max(...rain);
-eReveal('rainfall',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipLight,trigger:'axis',
-    formatter:p=>`Day ${p[0].axisValue} — spend $${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},
-  axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:FAINT}},
-  grid:[
-    {left:44,right:14,top:8,height:'26%'},
-    {left:44,right:14,top:'42%',bottom:26},
-  ],
-  xAxis:[
-    {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},
-    {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},
-  ],
-  yAxis:[
-    {gridIndex:0,inverse:true,max:18,splitLine:{show:false},
-     axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,formatter:'${value}K'},
-     axisLine:{show:false},axisTick:{show:false}},
-    {gridIndex:1,splitLine:{lineStyle:{color:GRID}},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},
-     axisLine:{show:false},axisTick:{show:false}},
-  ],
-  series:[
-    {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,
-     data:rain.map(v=>({value:v,itemStyle:{color:v===RMAX?hi(DATA2):DATA2}})),
-     itemStyle:{borderRadius:[0,0,4,4]},barWidth:'55%'},
-    {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,
-     smooth:.4,symbol:'none',lineStyle:{color:DATA,width:2.2},
-     areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-       colorStops:[{offset:0,color:alpha(DATA,.22)},{offset:1,color:alpha(DATA,0)}]}}},
-  ],
-});
-})();
-
-// ════ mono-fancy · 5: universal transition morph ════
-(()=>{
-const P12=[
-  ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],
-  ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],
-  ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],
-  ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],
-];
-const RK12=[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]);
-const shade=n=>rampAt(RK12.indexOf(n),12);
-const axis={splitLine:{lineStyle:{color:GRID}},axisLine:{show:false},axisTick:{show:false},
-  axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};
-const VIEWS=[
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:30},
-    xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE $',nameTextStyle:{color:FAINT,fontSize:8.5}},
-    yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:FAINT,fontSize:8.5}},
-    series:[{
-      id:'p',type:'scatter',universalTransition:true,
-      symbolSize:d=>Math.sqrt(d[3])*1.35,
-      data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],
-        groupId:d[0],itemStyle:{color:shade(d[0])}})),
-      label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,
-        formatter:p=>p.name},
-    }],
-  }},
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:44},
-    xAxis:{...axis,type:'category',
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),
-      axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},
-    yAxis:{...axis,type:'value',max:520},
-    series:[{
-      id:'p',type:'bar',universalTransition:true,
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(d[0]),borderRadius:[6,6,0,0]}})),
-      barCategoryGap:'32%',
-    }],
-  }},
-  {opt:{
-    grid:{left:0,right:0,top:0,bottom:0},
-    xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},
-    series:[{
-      id:'p',type:'pie',universalTransition:true,
-      radius:['26%','72%'],center:['50%','50%'],
-      itemStyle:{borderColor:BG,borderWidth:2,borderRadius:6},
-      label:{color:LAB,fontFamily:'Inter',fontSize:9,formatter:'{b}'},
-      labelLine:{lineStyle:{color:FAINT}},
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(d[0])}})),
-    }],
-  }},
-];
-const base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};
-obsReveal('morph',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let vi=0;
-  g.setOption({...base,...VIEWS[0].opt});
-  keep('morph',setInterval(()=>{
-    vi=(vi+1)%VIEWS.length;
-    g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});
-  },3000));
-});
-})();
-
-// ════ mono-fancy2 · 2: diverging bar ════
-(()=>{
-const D=[
-  ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],
-  ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],
-];
-const MAXG=Math.max(...D.map(d=>d[1]));
-eReveal('negbar',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},
-  grid:{left:96,right:52,top:8,bottom:8},
-  xAxis:{type:'value',
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-  yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-  series:[{
-    type:'bar',barWidth:16,
-    data:D.map(([n,v])=>({name:n,value:v,
-      itemStyle:{color:v>0?(v===MAXG?hi(DATA):DATA):DATA2,
-        borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),
-    label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,
-      position:'outside',
-      formatter:p=>(p.value>0?'+':'')+p.value,color:TXT},
-    markLine:{symbol:'none',silent:true,label:{show:false},
-      lineStyle:{color:MUT,width:1.5},
-      data:[{xAxis:0}]},
-  }],
-});
-})();
-
-// ════ force graph on dark: the network shakes itself into place ════
-// (replaced the axis-break bar — bars must never lie about length; house rule)
-(()=>{
-const hubs=[['Core API',52,0]];
-const apps=[
-  ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],
-  ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],
-  ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],
-];
-const nodes=[...hubs,...apps];
-const links=[
-  ...apps.map((a,i)=>({source:a[0],target:'Core API',
-    lineStyle:{width:Math.max(.8,a[1]*.09),color:DLINE,opacity:.6}})),
-  // a few app-to-app side roads
-  {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},
-  {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},
-  {source:'Figma',target:'Notion',lineStyle:{width:1,color:DLINE2,opacity:.5}},
-  {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:DLINE2,opacity:.5}},
-];
-eReveal('force',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},
-  series:[{
-    type:'graph',layout:'force',
-    force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},
-    roam:false,draggable:true,
-    left:14,right:14,top:14,bottom:14,
-    data:nodes.map(([n,v,tier])=>({
-      name:n,value:v,symbolSize:8+v*.75,
-      itemStyle:{color:tier===0?HERODK:tier===1?BG:DMID},
-      label:{show:true,position:'right',distance:6,
-        color:tier===0?BG:DMID,fontFamily:'Inter',
-        fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},
-    })),
-    links,
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},
-  }],
-});
-})();
-
-// ════ mono-fancy2 · 4: stagger wave ════
-(()=>{
-const n=50;
-const vals=Array.from({length:n},(_,i)=>
-  Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));
-const rkW=rankOf(vals);
-eReveal('wave',{
-  tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},
-  grid:{left:36,right:10,top:14,bottom:24},
-  xAxis:{type:'category',data:vals.map((_,i)=>i+1),
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},
-  yAxis:{type:'value',
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},
-  series:[{
-    type:'bar',barCategoryGap:'26%',
-    data:vals.map((v,i)=>({value:v,
-      itemStyle:{color:rampAt(rkW(i),vals.length),borderRadius:[4,4,0,0]}})),
-    animationDelay:i=>i*36,
-    animationDuration:640,animationEasing:'elasticOut',
-  }],
-  animationDurationUpdate:640,
-  animationDelayUpdate:i=>i*36,
-});
-})();
-
-// ════ mono-fancy3 · 1: custom double-encoded pie ════
-(()=>{
-const D=[
-  ['Editor',34,42],['Boards',22,31],['Docs',18,38],
-  ['Chat',12,11],['Automations',8,24],['Other',6,7],
-];
-const SCALE=45;
-const TAU=Math.PI*2;
-const byShare=[...D].sort((a,b)=>b[1]-a[1]);
-const minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);
-let acc=0;
-const slices=byShare.map(([n,share,mins])=>{
-  const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;
-  acc+=share;
-  return {n,share,mins,p0,p1,color:rampAt(minsDesc.indexOf(mins),6)};
-});
-eReveal('custompie',{
-  animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,
-  tooltip:{...tipLight,formatter:p=>{
-    const s=slices[p.dataIndex];
-    return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';
-  }},
-  xAxis:{show:false,min:-1,max:1},
-  yAxis:{show:false,min:-1,max:1},
-  grid:{left:0,right:0,top:0,bottom:0},
-  series:[{
-    type:'custom',
-    renderItem:(params,api)=>{
-      const s=slices[params.dataIndex];
-      const cx=api.getWidth()/2, cy=api.getHeight()/2+6;
-      const R=Math.min(api.getWidth(),api.getHeight())*.37;
-      const r0=R*.24;
-      const rOf=m=>r0+(R-r0)*m/SCALE;
-      const pm=(s.p0+s.p1)/2, lr=R+14;
-      const cos=Math.cos(pm);
-      const kids=[];
-      if(params.dataIndex===0){
-        [15,30,45].forEach(m=>kids.push({type:'circle',
-          shape:{cx,cy,r:rOf(m)},
-          style:{fill:'none',stroke:GRID,lineWidth:1,lineDash:[3,4]}}));
-      }
-      kids.push({type:'sector',
-        shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},
-        style:{fill:s.color,stroke:BG,lineWidth:3},
-        enterFrom:{shape:{r:r0}}});
-      kids.push({type:'text',style:{
-        x:cx+cos*lr, y:cy+Math.sin(pm)*lr,
-        text:s.n+'  '+s.share+'% · '+s.mins+'m',
-        font:'600 10px Inter',fill:LAB,
-        align:Math.abs(cos)<.25?'center':(cos>0?'left':'right'),
-        verticalAlign:'middle'},
-        enterFrom:{style:{opacity:0}}});
-      return {type:'group',children:kids};
-    },
-    data:slices.map(s=>s.share),
-  }],
-});
-})();
-
-// ════ mono-fancy3 · 3: single-axis small multiples ════
-(()=>{
-const days=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-const hours=Array.from({length:24},(_,i)=>i);
-const mkRow=di=>hours.map(h=>{
-  const workday=di<5, core=h>=9&&h<=17;
-  let v=Math.round((workday&&core?6+5*Math.sin((h-9)/8*Math.PI):0)+((h+di)%5===0?2:0)+(h>=22||h<=5?0:1));
-  return [h,v];
-}).filter(d=>d[1]>0);
-eReveal('singleaxis',{
-  animationDuration:500,animationEasing:'backOut',
-  tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},
-  title:days.map((d,i)=>({
-    text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,
-    textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},
-  })),
-  singleAxis:days.map((d,i)=>({
-    left:52,right:14,type:'category',boundaryGap:false,
-    data:hours.map(h=>String(h).padStart(2,'0')),
-    top:i*100/7+6+'%',height:100/7-8+'%',
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},
-    splitLine:{show:false},
-  })),
-  series:days.map((d,i)=>({
-    singleAxisIndex:i,coordinateSystem:'singleAxis',
-    type:'scatter',
-    data:mkRow(i),
-    symbolSize:dd=>dd[1]*2.6,
-    itemStyle:{color:i<5?(i===0?hi(SER[0]):SER[i]):FAINTDATA},
-    animationDelay:di=>i*160+di*14,
-  })),
-});
-})();
-
-// ════ mono-fancy3 · 4: jitter strip ════
-(()=>{
-const cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];
-const mkPts=(ci,n,median,spread,outliers)=>{
-  const pts=[];
-  for(let i=0;i<n;i++){
-    const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);
-    let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);
-    if(i<outliers) x=median+spread*2.2+u*spread*3;
-    const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;
-    pts.push([Math.max(.2,x),jy]);
-  }
-  return pts;
-};
-const groups=[
-  mkPts(0,38,.8,.5,2),
-  mkPts(1,64,2.4,1.1,3),
-  mkPts(2,110,6.5,2.4,4),
-  mkPts(3,72,14,4.5,3),
-];
-const flat=groups.flat();
-eReveal('jitter',{
-  animationDuration:450,animationEasing:'cubicOut',
-  animationDelay:i=>i<flat.length?Math.round(flat[i][1])*260+(i%37)*9:0,
-  tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},
-  grid:{left:86,right:16,top:14,bottom:30},
-  xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:FAINT,fontSize:8.5},
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},
-  yAxis:{type:'value',min:-.6,max:3.6,inverse:true,
-    splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:false}},
-  series:[{
-    type:'scatter',
-    data:flat,
-    symbolSize:7,
-    itemStyle:{color:p=>RAMP[4-Math.round(p.value[1])],opacity:.62},
-  },{
-    type:'scatter',silent:true,symbolSize:0,
-    data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],
-      color:LAB,fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),
-  }],
-});
-})();
-
-// ════ mono-fancy4 · 1: bar race ════
-(()=>{
-const P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];
-const YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];
-const vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];
-for(let y=1;y<YEARS.length;y++)
-  vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));
-obsReveal('race',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let step=0;
-  const frame=()=>{
-    const row=vals[step];
-    const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);
-    g.setOption({
-      animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',
-      tooltip:{...tipLight,valueFormatter:v=>'$'+Math.round(v)+'K'},
-      grid:{left:64,right:64,top:8,bottom:8},
-      xAxis:{type:'value',max:'dataMax',
-        splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-      yAxis:{type:'category',data:P,inverse:true,max:7,
-        animationDuration:300,animationDurationUpdate:300,
-        axisLine:{show:false},axisTick:{show:false},
-        axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-      series:[{
-        type:'bar',realtimeSort:true,barCategoryGap:'32%',
-        data:row.map((v,i)=>({value:v,
-          itemStyle:{color:rampAt(rank.indexOf(i),8),borderRadius:99}})),
-        label:{show:true,position:'right',valueAnimation:true,
-          fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,
-          formatter:p=>'$'+Math.round(p.value)+'K'},
-      }],
-      graphic:[{id:'yr',type:'text',right:18,bottom:14,
-        style:{text:YEARS[step],font:'800 44px Inter',fill:GRID}}],
-    });
-    step=(step+1)%YEARS.length;
-  };
-  frame();
-  keep('race',setInterval(frame,1150));
-});
-})();
-
-// ════ mono-fancy4 · 2: dynamic data stream ════
-(()=>{
-const WIN=50;
-obsReveal('stream',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let t=0,v=64;
-  const data=[];
-  for(;t<WIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}
-  const draw=()=>g.setOption({
-    animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',
-    tooltip:{...tipLight,valueFormatter:x=>x+'k users'},
-    grid:{left:14,right:58,top:44,bottom:20},
-    xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},
-    yAxis:{show:false,min:20,max:130},
-    series:[{
-      type:'line',data:[...data],smooth:.45,symbol:'none',
-      lineStyle:{color:DATA,width:2.4},
-      endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:TXT,
-        formatter:p=>p.value+'k'},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:alpha(DATA,.16)},{offset:1,color:alpha(DATA,0)}]}},
-    }],
-    graphic:[{type:'group',left:16,top:10,children:[
-      {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:HERO}},
-      {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:TXT}},
-    ]}],
-  });
-  draw();
-  keep('stream',setInterval(()=>{
-    v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));
-    data.push(Math.round(v));data.shift();t++;
-    draw();
-  },300));
-});
-})();
-
-// ════ mono-fancy4 · 3: draw-in + counter ════
-(()=>{
-const days=Array.from({length:180},(_,i)=>i);
-const daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);
-const cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);
-const TOTAL=cum[cum.length-1];
-const DUR=2600;
-let gen=0; // replay guard: only the latest counter loop may write
-obsReveal('stroke',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  gen++;const my=gen;
-  g.setOption({
-    animationDuration:DUR,animationEasing:'cubicOut',
-    tooltip:{show:false},
-    grid:{left:14,right:20,top:64,bottom:24},
-    xAxis:{type:'category',data:days,boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
-      axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,fontWeight:600,
-        interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},
-    yAxis:{show:false,max:TOTAL*1.06},
-    series:[{
-      type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',
-      lineStyle:{color:DATA,width:2.6},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:alpha(DATA,.14)},{offset:1,color:alpha(DATA,0)}]}},
-    }],
-  });
-  const t0=performance.now();
-  const tick=()=>{
-    if(my!==gen)return;
-    const p=Math.min(1,(performance.now()-t0)/DUR);
-    const eased=1-Math.pow(1-p,3);
-    const val=(TOTAL*eased/1000).toFixed(2);
-    g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[
-      {type:'text',style:{text:'$'+val+'M',font:'800 32px Inter',fill:hi(TXT)}},
-      {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:MUT}},
-    ]}]});
-    if(p<1)requestAnimationFrame(tick);
-  };
-  requestAnimationFrame(tick);
-});
-})();
-
-addEventListener('resize',()=>{
-  ['rose','forest','circular','tree','rainfall','morph','negbar','force','wave',
-   'custompie','singleaxis','jitter','race','stream','stroke']
-    .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#081F5C',PAPER='#F7F2EB',MUTED='rgba(8,31,92,.60)',GRID='rgba(8,31,92,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ G19 · violin ════
-// 小提琴:提前聚合的密度剪影,Glance 世界观(beeswarm 的姊妹)。
-// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),
-// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。
-(()=>{
-const G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];
-const M=64;
-const samples=g=>Array.from({length:M},(_,i)=>{
-  const [,c,s]=G[g];
-  const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);
-  return Math.max(.2,v);
-});
-obsReveal('violin',s=>{
-  const TOP=40,BASE=262,HMAX=17;
-  const mapY=h=>BASE-h/HMAX*(BASE-TOP);
-  const colX=g=>76+g*84;
-  // hour grid
-  [0,4,8,12,16].forEach(h=>{
-    el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(8,31,92,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${h*.01}s`});
-    txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.01}s`},h+'h');
-  });
-  const SHADE=[INK,'#334EAC','#7096D1','#BAD6EB'];
-  G.forEach(([name],g)=>{
-    const vs=samples(g).sort((a,b)=>a-b);
-    const med=vs[Math.floor(M/2)];
-    // fuller bandwidth = the classic rounded violin belly, not a spindle
-    const bw=Math.max(.9,G[g][2]*.72);
-    // pad past the data by 1.6 bandwidths so the density decays to ~0 and the
-    // ends taper naturally — clipping tighter gives flat-cut lantern ends
-    const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);
-    const NPTS=44;
-    const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));
-    const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));
-    const dmax=Math.max(...dens);
-    const W=30;
-    const x=colX(g);
-    // smooth closed outline through the mirrored density — quadratic
-    // midpoint smoothing, same trick as almanac's blob
-    const pts=[];
-    grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));
-    for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<pts.length;t++){
-      const p=pts[t],q=pts[(t+1)%pts.length];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});
-    tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);
-    // median: paper tick across the body, value parked right
-    const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));
-    const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;
-    el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),
-      stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});
-    txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'WIDTH = HOW MANY TICKETS WAIT THAT LONG · PAPER TICK = MEDIAN · DARKEST = FASTEST');
-});
-})();
-
-// ════ G20 · matrix heat ════
-// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出
-// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];
-const VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];
-const BASEV=[88,74,61,42,35,27];
-const v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));
-const shade=t=>t>64?INK:t>46?'#334EAC':t>30?'#7096D1':t>15?'#BAD6EB':'#D0E3FF';
-obsReveal('gheat',s=>{
-  const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;
-  FEAT.forEach((f,c)=>{
-    txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);
-  });
-  VER.forEach((ver,r)=>{
-    txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'rgba(8,31,92,.72)','text-anchor':'end',
-      class:'fade',style:`animation-delay:${r*.05}s`},ver);
-    FEAT.forEach((f,c)=>{
-      const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;
-      const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),
-        class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});
-      tip(cell,`${f} on ${ver} — ${t}% of accounts`);
-      txt(s,{x:x+W/2,y:y+H/2+4,'font-size':10.5,'font-weight':800,fill:dark?PAPER:INK,'text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);
-    });
-  });
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');
-});
-})();
-
-// ════ G21 · rank strip ════
-// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,
-// 和 slope 同一种丑(catalog 实测三连败那条),废弃。
-// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。
-// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。
-(()=>{
-const RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],
-  ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];
-const SHADE=[INK,'#334EAC','#7096D1','#BAD6EB','#D0E3FF'];   // rank 1 → 5
-obsReveal('rankstrip',s=>{
-  // rows sorted by final rank, the climber lands on top
-  const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);
-  const X0=110,PX=38,Y0=64,PY=42,C=32;
-  ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>
-    txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));
-  rows.forEach(([p,rk],i)=>{
-    const y=Y0+i*PY,hero=p==='Flows';
-    txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'rgba(8,31,92,.72)','text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.07}s`},p);
-    rk.forEach((r,q)=>{
-      const dark=r<=2;
-      const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],
-        stroke:'rgba(8,31,92,.24)','stroke-width':0.9,
-        class:'pop',style:`animation-delay:${i*.07+q*.035}s`});
-      tip(cell,`${p} — #${r} in Q${q+1}`);
-      txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,
-        fill:dark?PAPER:'rgba(8,31,92,.72)','text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);
-    });
-    // arrival flag: climbed / fell / held, told at the row's end
-    const d=rk[0]-rk[5];
-    txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,
-      fill:d>0?INK:d<0?'#BAD6EB':'rgba(8,31,92,.32)',
-      class:'fade',style:`animation-delay:${.5+i*.07}s`},
-      d>0?'▲'+d:d<0?'▼'+(-d):'—');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');
-});
-})();
-
-// ════ G22 · aggregate sankey ════
-// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——
-// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。
-// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。
-(()=>{
-const SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];
-const DST=['FREE','PRO','TEAM'];
-const SH=[INK,'#334EAC','#7096D1','#BAD6EB','rgba(8,31,92,.32)'];
-obsReveal('sankey',s=>{
-  const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;
-  const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));
-  const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));
-  const sy=[],dy=[];
-  let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});
-  y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});
-  const syc=[...sy],dyc=[...dy];
-  SRC.forEach((d,i)=>{
-    d[1].forEach((v,j)=>{
-      if(!v)return;
-      const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;
-      syc[i]=sb;dyc[j]=db;
-      const rib=el(s,'path',{d:`M${XL} ${sa} C${MID} ${sa} ${MID} ${da} ${XR} ${da} L${XR} ${db} C${MID} ${db} ${MID} ${sb} ${XL} ${sb} Z`,
-        fill:SH[i],opacity:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});
-      tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);
-    });
-  });
-  SRC.forEach((d,i)=>{
-    el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],
-      class:'fade',style:`animation-delay:${i*.06}s`});
-    txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#334EAC','text-anchor':'end',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);
-  });
-  DST.forEach((n,j)=>{
-    el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#081F5C',
-      class:'fade',style:`animation-delay:${.1+j*.06}s`});
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#334EAC',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'RIBBON WIDTH = ACCOUNTS OF 100 · SHADE = CHANNEL');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 1176
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/glance-wire.html

@@ -1,1176 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Glance 22 × WIRE</title>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0F0EE;--dark:#1F1E1C;--ink:#1F1E1C;--muted:rgba(31,30,28,.60);--faint:rgba(31,30,28,.40);--grid:rgba(31,30,28,.16);--lab:rgba(31,30,28,.72);--dmid:rgba(240,240,238,.62);--dq:rgba(240,240,238,.24)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:var(--bg)}
-  .card.dark .sub{color:var(--dmid)}
-  .card.dark .src{color:var(--dq)}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .ch{height:320px}
-  .wrap{position:relative;height:320px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:var(--ink);color:var(--bg)}
-  .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}
-  .badge.pair{border:1px dashed var(--faint);color:var(--muted)}
-  .card.dark .badge.del{background:var(--bg);color:var(--ink)}
-  .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}
-  .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-    <h1>Glance 系(快读)· 22 张 × WIRE</h1>
-  <p><span class="chip" style="background:#F5572F"></span><span class="chip" style="background:#22211F"></span><span class="chip" style="background:#8F8E86"></span><span class="chip" style="background:#C0BFB7"></span><span class="chip" style="background:#DBDAD3"></span> &nbsp;编辑部红 · 分类位全走黑灰,红只在两处出现:每张图的主角元素,以及序数梯爬完的最后一档(营收榜首 / P0 / 最新年份 / 峰值市场)。红是稀客,这是它的身份。</p>
-  <p style="margin-top:7px">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 个来源 / 4 条产品线 / 5 个工作日 / 10 支小队 / 12 个集成)各走一个分类位,序数型(8 年营收排名 / 50 个市场 / 4 档优先级 / 树木年份 / 面板停留时长 / 情绪票数)走 5 档梯,深色卡换纸色梯、末档落在强调色。几何、动画、数据与 glance-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <!-- ══ mono-demo ══ -->
-  <div class="card">
-    <h2>Daily active range</h2>
-    <div class="sub">min – max concurrent users · 1–14 Feb</div>
-    <div class="wrap"><canvas id="c1"></canvas></div>
-    <div class="src">RANGE CAPSULES · MONO-DEMO · LIVE OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>How releases made us feel</h2>
-    <div class="sub">reactions tagged in retro · H1 2026</div>
-    <div class="ch" id="rose"></div>
-    <div class="src">PETAL ROSE · MONO-DEMO · TEAM RETRO BOARD</div>
-  </div>
-
-  <div class="card">
-    <h2>Revenue by plan</h2>
-    <div class="sub">Q2 2026 · MRR, $K</div>
-    <div class="wrap"><canvas id="c3"></canvas></div>
-    <div class="src">CHUNKY BARS · MONO-DEMO · BILLING</div>
-  </div>
-
-  <!-- ══ mono-redesign · dot waffle ══ -->
-  <div class="card">
-    <h2>Where sign-ups come from</h2>
-    <div class="sub">Q2 2026 · every dot = 1% of new accounts</div>
-    <svg id="waffle" viewBox="0 0 380 300" width="100%" style="max-height:320px"></svg>
-    <div class="src">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION</div>
-  </div>
-
-  <!-- ══ mono-fancy ══ -->
-  <div class="card">
-    <h2>Trees planted, year by year</h2>
-    <div class="sub">reforestation program · thousands of trees</div>
-    <div class="ch" id="forest"></div>
-    <div class="src">PICTORIAL BAR · MONO-FANCY · GREEN OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Who works with whom</h2>
-    <div class="sub">cross-team collaborations · Q2 2026 · line = shared project</div>
-    <div class="ch" id="circular"></div>
-    <div class="src">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB</div>
-  </div>
-
-  <div class="card">
-    <h2>Everything the platform ships</h2>
-    <div class="sub">product areas and capabilities · Jun 2026</div>
-    <div class="ch" id="tree"></div>
-    <div class="src">TREE · MONO-FANCY · PRODUCT CATALOG</div>
-  </div>
-
-  <div class="card">
-    <h2>Campaigns rain down, sign-ups flow</h2>
-    <div class="sub">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days</div>
-    <div class="ch" id="rainfall"></div>
-    <div class="src">DUAL AREA · MONO-FANCY · GROWTH + ADS</div>
-  </div>
-
-  <div class="card">
-    <h2>One dataset, three views</h2>
-    <div class="sub">watch the same 12 products fly between views · every 3s</div>
-    <div class="ch" id="morph"></div>
-    <div class="src">SCATTER MORPH · MONO-FANCY · COHORTS</div>
-  </div>
-
-  <!-- ══ mono-fancy2 ══ -->
-  <div class="card">
-    <h2>Where we gained, where we bled</h2>
-    <div class="sub">net accounts per segment · Q2 2026 · churn left, growth right</div>
-    <div class="ch" id="negbar"></div>
-    <div class="src">DIVERGING BAR · MONO-FANCY2 · CRM</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Integrations, pulled into orbit</h2>
-    <div class="sub">force layout · node = monthly syncs · drag a node, the web fights back</div>
-    <div class="ch" id="force"></div>
-    <div class="src">FORCE GRAPH · NEW · INTEGRATION HUB</div>
-  </div>
-
-  <div class="card">
-    <h2>Fifty markets, one wave</h2>
-    <div class="sub">sign-ups per market · bars cascade in · click to replay</div>
-    <div class="ch" id="wave"></div>
-    <div class="src">STAGGER DELAY · MONO-FANCY2 · GROWTH</div>
-  </div>
-
-  <!-- ══ mono-fancy3 ══ -->
-  <div class="card">
-    <h2>Big slice, deep engagement</h2>
-    <div class="sub">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min</div>
-    <div class="ch" id="custompie"></div>
-    <div class="src">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS</div>
-  </div>
-
-  <div class="card">
-    <h2>Support load, day by day</h2>
-    <div class="sub">tickets per hour · each row is its own axis · dot = volume</div>
-    <div class="ch" id="singleaxis"></div>
-    <div class="src">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <div class="card">
-    <h2>Response times, spread out</h2>
-    <div class="sub">every dot = one ticket · jitter reveals the pile-up · by priority</div>
-    <div class="ch" id="jitter"></div>
-    <div class="src">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <!-- ══ mono-fancy4 ══ -->
-  <div class="card">
-    <h2>Eight products race for revenue</h2>
-    <div class="sub">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay</div>
-    <div class="ch" id="race"></div>
-    <div class="src">BAR RACE · MONO-FANCY4 · BILLING</div>
-  </div>
-
-  <div class="card">
-    <h2>Concurrent users, streaming</h2>
-    <div class="sub">one new reading every 300ms · window of 50 · click to restart</div>
-    <div class="ch" id="stream"></div>
-    <div class="src">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS</div>
-  </div>
-
-  <div class="card">
-    <h2>H1 revenue, drawn in one stroke</h2>
-    <div class="sub">cumulative ARR · the line draws, the number counts · click to replay</div>
-    <div class="ch" id="stroke"></div>
-    <div class="src">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE</div>
-  </div>
-
-
-  <!-- G19 · violin -->
-  <div class="card">
-    <span class="badge">GLANCE 分布型 · 小提琴</span>
-    <h2>How fast each plan gets an answer</h2>
-    <div class="sub">hours to first reply · silhouette = where tickets pile up · paper tick = median</div>
-    <svg id="violin" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">VIOLIN · WIRE · SUPPORT DESK</div>
-  </div>
-
-  <!-- G20 · matrix heat -->
-  <div class="card">
-    <span class="badge">GLANCE 热力型 · 通用矩阵</span>
-    <h2>Adoption runs hot on the new versions</h2>
-    <div class="sub">5 versions × 6 features · shade = % of accounts using it · every cell carries its number</div>
-    <svg id="gheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT (GLANCE) · WIRE · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- G21 · rank strip -->
-  <div class="card">
-    <span class="badge">GLANCE 排名型 · RANK STRIP</span>
-    <h2>Flows climbs to the top</h2>
-    <div class="sub">one row per product, one cell per quarter · darker = higher rank · a bar race you can print</div>
-    <svg id="rankstrip" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RANK STRIP · WIRE · BILLING</div>
-  </div>
-
-  <!-- G22 · aggregate sankey -->
-  <div class="card">
-    <span class="badge">GLANCE 流向型 · 聚合桑基</span>
-    <h2>Channels pour into plans</h2>
-    <div class="sub">ribbon width = accounts · channels on the left, the plans they land in on the right</div>
-    <svg id="sankey" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">AGGREGATE SANKEY · WIRE · ATTRIBUTION</div>
-  </div>
-
-</div>
-<script>
-// ── WIRE 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F0F0EE',DARK='#1F1E1C',TXT='#1F1E1C';
-const MUT='rgba(31,30,28,.60)',LAB='rgba(31,30,28,.72)',FAINT='rgba(31,30,28,.40)',QUIET='rgba(31,30,28,.15)',GRID='rgba(31,30,28,.16)';
-const DATA='#22211F',DATA2='#8F8E86',HERO='#F5572F';
-const HERODK='#F5572F';                      // 深色卡上的强调色(HERO 在深底上可能读不出)
-const FAINTDATA='#C0BFB7';                 // 静音数据档:周末 / 非主序列
-const DMID='rgba(240,240,238,.62)',DQ='rgba(240,240,238,.24)';   // 深色卡上的浅墨 / 静音档
-const SER=['#22211F','#8F8E86','#C0BFB7','rgba(34,33,31,.52)','#DBDAD3','rgba(34,33,31,.3)'];   // 分类位:一图之内每个系列 / 每类 / 每行一个色
-const RAMP=['#DBDAD3','#C0BFB7','#8F8E86','#22211F','#F5572F'];   // 5 档序数梯(浅→重):排名 / 票量 / 峰值 / 停留时长
-const DRAMP4=['#F5572F','#C0BFB7','#8F8E86','rgba(240,240,238,.26)'];   // 深色卡上的 4 档序数梯(浅→深)
-const HEROMODE=true; // 主角模式:分类位的头号项也整体走 HERO
-
-// ── 别名与派生:深色卡的线全是卡底向纸色的插值,不引入新色相 ──
-const INK=TXT,PAPER=BG,MUTED=MUT;
-const rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));
-const alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;
-const mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');
-const lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));
-  return .2126*r+.7152*g+.0722*b};
-const ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};
-const readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG); // 深色卡上落在色块里的字
-const DLINE=mix(DARK,BG,.20),DLINE2=mix(DARK,BG,.13),DSHELL=mix(DARK,BG,.09);
-const hi=c=>HEROMODE?HERO:c;
-// 名次 → 序数梯。末档(WIRE 的红)只发给第 1 名,其余摊在前四档上
-const rampAt=(i,n)=>i===0?RAMP[4]
-  :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];
-const rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);
-  return i=>r.indexOf(i)};
-const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── unified reveal: every chart waits to be scrolled into view; click replays.
-//    fn charts manage their own timers via keep(); they're cleared on replay.
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUTED;
-Chart.defaults.font.family='Inter';
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=INK;
-Chart.defaults.plugins.tooltip.titleColor=BG;
-Chart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-// ════ mono-demo · 1: floating range capsules ════
-(()=>{
-const WIDEST=r=>r.reduce((b,x,i)=>x[1]-x[0]>r[b][1]-r[b][0]?i:b,0);
-const ranges=[[195,235],[165,192],[150,232],[73,168],[122,202],[182,192],[138,178],[162,227],[195,235],[228,305],[218,232],[165,232],[118,210],[195,232]];
-cReveal('c1',()=>({
-  type:'bar',
-  data:{labels:ranges.map((_,i)=>i+1),
-    datasets:[{data:ranges,backgroundColor:ranges.map((_,i)=>i===WIDEST(ranges)?HERO:DATA),borderRadius:99,borderSkipped:false,barPercentage:.32}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:800,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*40:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw[0]+'K – '+i.raw[1]+'K users'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},
-         ticks:{padding:6,font:{size:9},callback:v=>[0,6,13].includes(v)?(v+1)+' FEB':''}},
-      y:{min:50,max:320,grid:{display:false},border:{display:false},
-         ticks:{padding:8,maxTicksLimit:6,callback:v=>v+'K',font:{size:9.5}}},
-    },
-  },
-}));
-})();
-
-// ════ mono-demo · 2: petal rose on dark ════
-(()=>{
-const EMO=[
-  ['Happiness',12],['Awe',10],['Admiration',5],['Surprise',12],
-  ['Sadness',6],['Fear',4],['Anger',2],['Anticipation',5],
-];
-const MAXV=12;
-const petal=v=>{const t=v/MAXV;return t>.8?DRAMP4[0]:t>.6?DRAMP4[1]:t>.35?DRAMP4[2]:DRAMP4[3]};
-// 标签固定落在环宽中段,只有票数够多的花瓣才铺到那儿;没铺到的按卡底配字色
-const onPetal=v=>v/MAXV>.6;
-eReveal('rose',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.name+' — '+EMO[p.dataIndex][1]},
-  series:[
-    {type:'pie',silent:false,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:DSHELL,borderRadius:16,borderColor:DARK,borderWidth:5},
-     label:{show:false},emphasis:{scale:false},
-     data:EMO.map(([n])=>({name:n,value:1})),z:1},
-    {type:'pie',silent:true,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:'transparent'},label:{position:'inside'},
-     data:EMO.map(([n,v])=>({name:n,value:1,
-       label:{formatter:`{n|${v}}\n{l|${n}}`,rich:{
-         n:{fontSize:17,fontWeight:700,fontFamily:'Inter',lineHeight:20,
-              color:onPetal(v)?readable(petal(v)):DMID},
-         l:{fontSize:9.5,fontFamily:'Inter',
-              color:onPetal(v)?alpha(readable(petal(v)),.66):DQ},
-       }}})),z:3},
-    {type:'pie',roseType:'area',silent:true,radius:['14%','88%'],center:['50%','50%'],
-     itemStyle:{borderRadius:14,borderColor:DARK,borderWidth:5},label:{show:false},
-     data:EMO.map(([n,v])=>({name:n,value:v,itemStyle:{color:petal(v)}})),z:2},
-  ],
-});
-})();
-
-// ════ mono-demo · 3: chunky bars, values on top ════
-(()=>{
-const V3=[182,486,391,274],rk3=rankOf(V3);
-cReveal('c3',()=>({
-  type:'bar',
-  data:{labels:['STARTER','PRO','TEAM','ENT'],
-    datasets:[{data:V3,
-      backgroundColor:V3.map((_,i)=>rampAt(rk3(i),4)),
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>'$'+i.raw+'K MRR'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
-      y:{display:false,suggestedMax:540},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText('$'+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-// ════ mono-redesign · donut → waffle of dots ════
-(()=>{
-const NS='http://www.w3.org/2000/svg';
-const mkEl=(svg,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);svg.appendChild(n);return n};
-const D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];
-obsReveal('waffle',s=>{
-  s.innerHTML='';
-  const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;
-  let idx=0;
-  D.forEach(([name,v],g)=>{
-    for(let k=0;k<v;k++){
-      const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;
-      mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:g===0?hi(SER[0]):SER[g],
-        class:'pop',style:`animation-delay:${c*.008+g*.05}s`});
-    }
-    idx+=v;
-  });
-  D.forEach(([name,v],g)=>{
-    const y=26+g*40;
-    mkEl(s,'circle',{cx:246,cy:y,r:6,fill:g===0?hi(SER[0]):SER[g]});
-    const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});
-    n.textContent=name;
-    const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g<2?TXT:MUT});
-    val.textContent=v+'%';
-  });
-});
-})();
-
-// ════ mono-fancy · 1: pictorial bar forest ════
-(()=>{
-const TREE='path://M20,0 L38,28 L29,28 L40,46 L26,46 L26,58 L14,58 L14,46 L0,46 L11,28 L2,28 Z';
-const years=['2022','2023','2024','2025','2026'];
-const vals=[26,41,63,88,117];
-eReveal('forest',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},
-  grid:{left:44,right:56,top:10,bottom:8},
-  yAxis:{type:'category',data:years,inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},
-  xAxis:{show:false,max:13},
-  series:[{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:RAMP[i]}})),
-    label:{show:true,position:'right',offset:[8,0],
-      color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,
-      formatter:p=>vals[p.dataIndex]+'k'},
-    z:10,
-  },{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    itemStyle:{color:QUIET},data:years.map(()=>13),silent:true,z:5,
-  }],
-});
-})();
-
-// ════ mono-fancy · 2: circular graph on dark ════
-(()=>{
-const teams=[
-  ['Product',52],['Design',38],['Frontend',46],['Backend',44],
-  ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],
-];
-const links=[
-  [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],
-  [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],
-];
-eReveal('circular',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:tipDark,
-  series:[{
-    type:'graph',layout:'circular',circular:{rotateLabel:false},
-    left:52,right:52,top:34,bottom:34,
-    data:teams.map(([n,v],i)=>({
-      name:n,value:v,symbolSize:v*.62,
-      itemStyle:{color:i===0?HERODK:i<4?BG:i<8?DMID:DQ,borderWidth:0},
-      label:{show:true,position:'right',distance:7,color:DMID,fontFamily:'Inter',fontSize:9.5,fontWeight:600},
-    })),
-    links:links.map(([s,t,w])=>({
-      source:s,target:t,
-      lineStyle:{width:w*.7,color:DLINE,opacity:.55,curveness:.28},
-    })),
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},
-  }],
-});
-})();
-
-// ════ mono-fancy · 3: LR tree ════
-(()=>{
-const b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},
-  children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});
-eReveal('tree',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:tipLight,
-  series:[{
-    type:'tree',layout:'orthogonal',orient:'LR',
-    left:64,right:96,top:8,bottom:8,
-    symbol:'circle',symbolSize:7,
-    initialTreeDepth:2,expandAndCollapse:false,roam:false,
-    itemStyle:{borderWidth:0},
-    lineStyle:{width:1.4,curveness:.5},
-    label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:LAB,position:'left'},
-    leaves:{label:{position:'right',color:MUTED,fontWeight:500}},
-    data:[{
-      name:'Platform',itemStyle:{color:TXT},lineStyle:{color:FAINT},
-      label:{fontSize:11.5,color:INK},
-      children:[
-        b('Editor',['Blocks','Tables','Comments','History'],hi(SER[0])),
-        b('Automate',['Workflows','Triggers','Webhooks'],SER[1]),
-        b('Collaborate',['Spaces','Guests','Mentions'],SER[2]),
-        b('Integrate',['API','Slack','GitHub'],SER[3]),
-      ],
-    }],
-  }],
-});
-})();
-
-// ════ mono-fancy · 4: rainfall dual area ════
-(()=>{
-const days=Array.from({length:30},(_,i)=>i+1);
-const flow=[42,45,44,48,52,55,53,58,64,62,66,71,69,75,82,79,84,80,86,92,88,95,91,97,104,101,108,105,112,118];
-const rain=[2,0,4,8,3,0,6,12,7,3,9,15,6,2,11,5,8,14,4,9,6,13,3,7,10,5,12,8,15,6];
-const RMAX=Math.max(...rain);
-eReveal('rainfall',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipLight,trigger:'axis',
-    formatter:p=>`Day ${p[0].axisValue} — spend $${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},
-  axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:FAINT}},
-  grid:[
-    {left:44,right:14,top:8,height:'26%'},
-    {left:44,right:14,top:'42%',bottom:26},
-  ],
-  xAxis:[
-    {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},
-    {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},
-  ],
-  yAxis:[
-    {gridIndex:0,inverse:true,max:18,splitLine:{show:false},
-     axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,formatter:'${value}K'},
-     axisLine:{show:false},axisTick:{show:false}},
-    {gridIndex:1,splitLine:{lineStyle:{color:GRID}},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},
-     axisLine:{show:false},axisTick:{show:false}},
-  ],
-  series:[
-    {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,
-     data:rain.map(v=>({value:v,itemStyle:{color:v===RMAX?hi(DATA2):DATA2}})),
-     itemStyle:{borderRadius:[0,0,4,4]},barWidth:'55%'},
-    {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,
-     smooth:.4,symbol:'none',lineStyle:{color:DATA,width:2.2},
-     areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-       colorStops:[{offset:0,color:alpha(DATA,.22)},{offset:1,color:alpha(DATA,0)}]}}},
-  ],
-});
-})();
-
-// ════ mono-fancy · 5: universal transition morph ════
-(()=>{
-const P12=[
-  ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],
-  ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],
-  ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],
-  ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],
-];
-const RK12=[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]);
-const shade=n=>rampAt(RK12.indexOf(n),12);
-const axis={splitLine:{lineStyle:{color:GRID}},axisLine:{show:false},axisTick:{show:false},
-  axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};
-const VIEWS=[
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:30},
-    xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE $',nameTextStyle:{color:FAINT,fontSize:8.5}},
-    yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:FAINT,fontSize:8.5}},
-    series:[{
-      id:'p',type:'scatter',universalTransition:true,
-      symbolSize:d=>Math.sqrt(d[3])*1.35,
-      data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],
-        groupId:d[0],itemStyle:{color:shade(d[0])}})),
-      label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,
-        formatter:p=>p.name},
-    }],
-  }},
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:44},
-    xAxis:{...axis,type:'category',
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),
-      axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},
-    yAxis:{...axis,type:'value',max:520},
-    series:[{
-      id:'p',type:'bar',universalTransition:true,
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(d[0]),borderRadius:[6,6,0,0]}})),
-      barCategoryGap:'32%',
-    }],
-  }},
-  {opt:{
-    grid:{left:0,right:0,top:0,bottom:0},
-    xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},
-    series:[{
-      id:'p',type:'pie',universalTransition:true,
-      radius:['26%','72%'],center:['50%','50%'],
-      itemStyle:{borderColor:BG,borderWidth:2,borderRadius:6},
-      label:{color:LAB,fontFamily:'Inter',fontSize:9,formatter:'{b}'},
-      labelLine:{lineStyle:{color:FAINT}},
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(d[0])}})),
-    }],
-  }},
-];
-const base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};
-obsReveal('morph',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let vi=0;
-  g.setOption({...base,...VIEWS[0].opt});
-  keep('morph',setInterval(()=>{
-    vi=(vi+1)%VIEWS.length;
-    g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});
-  },3000));
-});
-})();
-
-// ════ mono-fancy2 · 2: diverging bar ════
-(()=>{
-const D=[
-  ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],
-  ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],
-];
-const MAXG=Math.max(...D.map(d=>d[1]));
-eReveal('negbar',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},
-  grid:{left:96,right:52,top:8,bottom:8},
-  xAxis:{type:'value',
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-  yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-  series:[{
-    type:'bar',barWidth:16,
-    data:D.map(([n,v])=>({name:n,value:v,
-      itemStyle:{color:v>0?(v===MAXG?hi(DATA):DATA):DATA2,
-        borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),
-    label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,
-      position:'outside',
-      formatter:p=>(p.value>0?'+':'')+p.value,color:TXT},
-    markLine:{symbol:'none',silent:true,label:{show:false},
-      lineStyle:{color:MUT,width:1.5},
-      data:[{xAxis:0}]},
-  }],
-});
-})();
-
-// ════ force graph on dark: the network shakes itself into place ════
-// (replaced the axis-break bar — bars must never lie about length; house rule)
-(()=>{
-const hubs=[['Core API',52,0]];
-const apps=[
-  ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],
-  ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],
-  ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],
-];
-const nodes=[...hubs,...apps];
-const links=[
-  ...apps.map((a,i)=>({source:a[0],target:'Core API',
-    lineStyle:{width:Math.max(.8,a[1]*.09),color:DLINE,opacity:.6}})),
-  // a few app-to-app side roads
-  {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},
-  {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:DLINE2,opacity:.5}},
-  {source:'Figma',target:'Notion',lineStyle:{width:1,color:DLINE2,opacity:.5}},
-  {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:DLINE2,opacity:.5}},
-];
-eReveal('force',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},
-  series:[{
-    type:'graph',layout:'force',
-    force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},
-    roam:false,draggable:true,
-    left:14,right:14,top:14,bottom:14,
-    data:nodes.map(([n,v,tier])=>({
-      name:n,value:v,symbolSize:8+v*.75,
-      itemStyle:{color:tier===0?HERODK:tier===1?BG:DMID},
-      label:{show:true,position:'right',distance:6,
-        color:tier===0?BG:DMID,fontFamily:'Inter',
-        fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},
-    })),
-    links,
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},
-  }],
-});
-})();
-
-// ════ mono-fancy2 · 4: stagger wave ════
-(()=>{
-const n=50;
-const vals=Array.from({length:n},(_,i)=>
-  Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));
-const rkW=rankOf(vals);
-eReveal('wave',{
-  tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},
-  grid:{left:36,right:10,top:14,bottom:24},
-  xAxis:{type:'category',data:vals.map((_,i)=>i+1),
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},
-  yAxis:{type:'value',
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},
-  series:[{
-    type:'bar',barCategoryGap:'26%',
-    data:vals.map((v,i)=>({value:v,
-      itemStyle:{color:rampAt(rkW(i),vals.length),borderRadius:[4,4,0,0]}})),
-    animationDelay:i=>i*36,
-    animationDuration:640,animationEasing:'elasticOut',
-  }],
-  animationDurationUpdate:640,
-  animationDelayUpdate:i=>i*36,
-});
-})();
-
-// ════ mono-fancy3 · 1: custom double-encoded pie ════
-(()=>{
-const D=[
-  ['Editor',34,42],['Boards',22,31],['Docs',18,38],
-  ['Chat',12,11],['Automations',8,24],['Other',6,7],
-];
-const SCALE=45;
-const TAU=Math.PI*2;
-const byShare=[...D].sort((a,b)=>b[1]-a[1]);
-const minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);
-let acc=0;
-const slices=byShare.map(([n,share,mins])=>{
-  const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;
-  acc+=share;
-  return {n,share,mins,p0,p1,color:rampAt(minsDesc.indexOf(mins),6)};
-});
-eReveal('custompie',{
-  animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,
-  tooltip:{...tipLight,formatter:p=>{
-    const s=slices[p.dataIndex];
-    return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';
-  }},
-  xAxis:{show:false,min:-1,max:1},
-  yAxis:{show:false,min:-1,max:1},
-  grid:{left:0,right:0,top:0,bottom:0},
-  series:[{
-    type:'custom',
-    renderItem:(params,api)=>{
-      const s=slices[params.dataIndex];
-      const cx=api.getWidth()/2, cy=api.getHeight()/2+6;
-      const R=Math.min(api.getWidth(),api.getHeight())*.37;
-      const r0=R*.24;
-      const rOf=m=>r0+(R-r0)*m/SCALE;
-      const pm=(s.p0+s.p1)/2, lr=R+14;
-      const cos=Math.cos(pm);
-      const kids=[];
-      if(params.dataIndex===0){
-        [15,30,45].forEach(m=>kids.push({type:'circle',
-          shape:{cx,cy,r:rOf(m)},
-          style:{fill:'none',stroke:GRID,lineWidth:1,lineDash:[3,4]}}));
-      }
-      kids.push({type:'sector',
-        shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},
-        style:{fill:s.color,stroke:BG,lineWidth:3},
-        enterFrom:{shape:{r:r0}}});
-      kids.push({type:'text',style:{
-        x:cx+cos*lr, y:cy+Math.sin(pm)*lr,
-        text:s.n+'  '+s.share+'% · '+s.mins+'m',
-        font:'600 10px Inter',fill:LAB,
-        align:Math.abs(cos)<.25?'center':(cos>0?'left':'right'),
-        verticalAlign:'middle'},
-        enterFrom:{style:{opacity:0}}});
-      return {type:'group',children:kids};
-    },
-    data:slices.map(s=>s.share),
-  }],
-});
-})();
-
-// ════ mono-fancy3 · 3: single-axis small multiples ════
-(()=>{
-const days=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-const hours=Array.from({length:24},(_,i)=>i);
-const mkRow=di=>hours.map(h=>{
-  const workday=di<5, core=h>=9&&h<=17;
-  let v=Math.round((workday&&core?6+5*Math.sin((h-9)/8*Math.PI):0)+((h+di)%5===0?2:0)+(h>=22||h<=5?0:1));
-  return [h,v];
-}).filter(d=>d[1]>0);
-eReveal('singleaxis',{
-  animationDuration:500,animationEasing:'backOut',
-  tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},
-  title:days.map((d,i)=>({
-    text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,
-    textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},
-  })),
-  singleAxis:days.map((d,i)=>({
-    left:52,right:14,type:'category',boundaryGap:false,
-    data:hours.map(h=>String(h).padStart(2,'0')),
-    top:i*100/7+6+'%',height:100/7-8+'%',
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},
-    splitLine:{show:false},
-  })),
-  series:days.map((d,i)=>({
-    singleAxisIndex:i,coordinateSystem:'singleAxis',
-    type:'scatter',
-    data:mkRow(i),
-    symbolSize:dd=>dd[1]*2.6,
-    itemStyle:{color:i<5?(i===0?hi(SER[0]):SER[i]):FAINTDATA},
-    animationDelay:di=>i*160+di*14,
-  })),
-});
-})();
-
-// ════ mono-fancy3 · 4: jitter strip ════
-(()=>{
-const cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];
-const mkPts=(ci,n,median,spread,outliers)=>{
-  const pts=[];
-  for(let i=0;i<n;i++){
-    const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);
-    let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);
-    if(i<outliers) x=median+spread*2.2+u*spread*3;
-    const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;
-    pts.push([Math.max(.2,x),jy]);
-  }
-  return pts;
-};
-const groups=[
-  mkPts(0,38,.8,.5,2),
-  mkPts(1,64,2.4,1.1,3),
-  mkPts(2,110,6.5,2.4,4),
-  mkPts(3,72,14,4.5,3),
-];
-const flat=groups.flat();
-eReveal('jitter',{
-  animationDuration:450,animationEasing:'cubicOut',
-  animationDelay:i=>i<flat.length?Math.round(flat[i][1])*260+(i%37)*9:0,
-  tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},
-  grid:{left:86,right:16,top:14,bottom:30},
-  xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:FAINT,fontSize:8.5},
-    splitLine:{lineStyle:{color:GRID}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},
-  yAxis:{type:'value',min:-.6,max:3.6,inverse:true,
-    splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:false}},
-  series:[{
-    type:'scatter',
-    data:flat,
-    symbolSize:7,
-    itemStyle:{color:p=>RAMP[4-Math.round(p.value[1])],opacity:.62},
-  },{
-    type:'scatter',silent:true,symbolSize:0,
-    data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],
-      color:LAB,fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),
-  }],
-});
-})();
-
-// ════ mono-fancy4 · 1: bar race ════
-(()=>{
-const P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];
-const YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];
-const vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];
-for(let y=1;y<YEARS.length;y++)
-  vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));
-obsReveal('race',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let step=0;
-  const frame=()=>{
-    const row=vals[step];
-    const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);
-    g.setOption({
-      animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',
-      tooltip:{...tipLight,valueFormatter:v=>'$'+Math.round(v)+'K'},
-      grid:{left:64,right:64,top:8,bottom:8},
-      xAxis:{type:'value',max:'dataMax',
-        splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-      yAxis:{type:'category',data:P,inverse:true,max:7,
-        animationDuration:300,animationDurationUpdate:300,
-        axisLine:{show:false},axisTick:{show:false},
-        axisLabel:{color:LAB,fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-      series:[{
-        type:'bar',realtimeSort:true,barCategoryGap:'32%',
-        data:row.map((v,i)=>({value:v,
-          itemStyle:{color:rampAt(rank.indexOf(i),8),borderRadius:99}})),
-        label:{show:true,position:'right',valueAnimation:true,
-          fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,
-          formatter:p=>'$'+Math.round(p.value)+'K'},
-      }],
-      graphic:[{id:'yr',type:'text',right:18,bottom:14,
-        style:{text:YEARS[step],font:'800 44px Inter',fill:GRID}}],
-    });
-    step=(step+1)%YEARS.length;
-  };
-  frame();
-  keep('race',setInterval(frame,1150));
-});
-})();
-
-// ════ mono-fancy4 · 2: dynamic data stream ════
-(()=>{
-const WIN=50;
-obsReveal('stream',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let t=0,v=64;
-  const data=[];
-  for(;t<WIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}
-  const draw=()=>g.setOption({
-    animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',
-    tooltip:{...tipLight,valueFormatter:x=>x+'k users'},
-    grid:{left:14,right:58,top:44,bottom:20},
-    xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},
-    yAxis:{show:false,min:20,max:130},
-    series:[{
-      type:'line',data:[...data],smooth:.45,symbol:'none',
-      lineStyle:{color:DATA,width:2.4},
-      endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:TXT,
-        formatter:p=>p.value+'k'},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:alpha(DATA,.16)},{offset:1,color:alpha(DATA,0)}]}},
-    }],
-    graphic:[{type:'group',left:16,top:10,children:[
-      {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:HERO}},
-      {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:TXT}},
-    ]}],
-  });
-  draw();
-  keep('stream',setInterval(()=>{
-    v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));
-    data.push(Math.round(v));data.shift();t++;
-    draw();
-  },300));
-});
-})();
-
-// ════ mono-fancy4 · 3: draw-in + counter ════
-(()=>{
-const days=Array.from({length:180},(_,i)=>i);
-const daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);
-const cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);
-const TOTAL=cum[cum.length-1];
-const DUR=2600;
-let gen=0; // replay guard: only the latest counter loop may write
-obsReveal('stroke',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  gen++;const my=gen;
-  g.setOption({
-    animationDuration:DUR,animationEasing:'cubicOut',
-    tooltip:{show:false},
-    grid:{left:14,right:20,top:64,bottom:24},
-    xAxis:{type:'category',data:days,boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
-      axisLabel:{color:FAINT,fontFamily:'Inter',fontSize:9,fontWeight:600,
-        interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},
-    yAxis:{show:false,max:TOTAL*1.06},
-    series:[{
-      type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',
-      lineStyle:{color:DATA,width:2.6},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:alpha(DATA,.14)},{offset:1,color:alpha(DATA,0)}]}},
-    }],
-  });
-  const t0=performance.now();
-  const tick=()=>{
-    if(my!==gen)return;
-    const p=Math.min(1,(performance.now()-t0)/DUR);
-    const eased=1-Math.pow(1-p,3);
-    const val=(TOTAL*eased/1000).toFixed(2);
-    g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[
-      {type:'text',style:{text:'$'+val+'M',font:'800 32px Inter',fill:hi(TXT)}},
-      {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:MUT}},
-    ]}]});
-    if(p<1)requestAnimationFrame(tick);
-  };
-  requestAnimationFrame(tick);
-});
-})();
-
-addEventListener('resize',()=>{
-  ['rose','forest','circular','tree','rainfall','morph','negbar','force','wave',
-   'custompie','singleaxis','jitter','race','stream','stroke']
-    .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#22211F',PAPER='#F0F0EE',MUTED='rgba(31,30,28,.60)',GRID='rgba(31,30,28,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ G19 · violin ════
-// 小提琴:提前聚合的密度剪影,Glance 世界观(beeswarm 的姊妹)。
-// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),
-// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。
-(()=>{
-const G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];
-const M=64;
-const samples=g=>Array.from({length:M},(_,i)=>{
-  const [,c,s]=G[g];
-  const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);
-  return Math.max(.2,v);
-});
-obsReveal('violin',s=>{
-  const TOP=40,BASE=262,HMAX=17;
-  const mapY=h=>BASE-h/HMAX*(BASE-TOP);
-  const colX=g=>76+g*84;
-  // hour grid
-  [0,4,8,12,16].forEach(h=>{
-    el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'rgba(31,30,28,.12)','stroke-width':1.44,
-      class:'fade',style:`animation-delay:${h*.01}s`});
-    txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.01}s`},h+'h');
-  });
-  const SHADE=[INK,'#6E6D66','#8F8E86','#C0BFB7'];
-  G.forEach(([name],g)=>{
-    const vs=samples(g).sort((a,b)=>a-b);
-    const med=vs[Math.floor(M/2)];
-    // fuller bandwidth = the classic rounded violin belly, not a spindle
-    const bw=Math.max(.9,G[g][2]*.72);
-    // pad past the data by 1.6 bandwidths so the density decays to ~0 and the
-    // ends taper naturally — clipping tighter gives flat-cut lantern ends
-    const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);
-    const NPTS=44;
-    const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));
-    const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));
-    const dmax=Math.max(...dens);
-    const W=30;
-    const x=colX(g);
-    // smooth closed outline through the mirrored density — quadratic
-    // midpoint smoothing, same trick as almanac's blob
-    const pts=[];
-    grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));
-    for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<pts.length;t++){
-      const p=pts[t],q=pts[(t+1)%pts.length];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});
-    tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);
-    // median: paper tick across the body, value parked right
-    const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));
-    const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;
-    el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),
-      stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});
-    // 同箱线:红只标最慢的 FREE,其余走墨
-    const slow=g===G.length-1;
-    txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':slow?11:9.5,'font-weight':800,
-      fill:slow?'#F5572F':INK,
-      class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'WIDTH = HOW MANY TICKETS WAIT THAT LONG · RED = THE SLOWEST PLAN · DARKEST = FASTEST');
-});
-})();
-
-// ════ G20 · matrix heat ════
-// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出
-// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];
-const VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];
-const BASEV=[88,74,61,42,35,27];
-const v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));
-const shade=t=>t>64?'#F5572F':t>46?'#6E6D66':t>30?'#8F8E86':t>15?'#C0BFB7':'#DBDAD3';
-obsReveal('gheat',s=>{
-  const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;
-  FEAT.forEach((f,c)=>{
-    txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);
-  });
-  VER.forEach((ver,r)=>{
-    txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'rgba(31,30,28,.72)','text-anchor':'end',
-      class:'fade',style:`animation-delay:${r*.05}s`},ver);
-    FEAT.forEach((f,c)=>{
-      const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;
-      const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),
-        class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});
-      tip(cell,`${f} on ${ver} — ${t}% of accounts`);
-      txt(s,{x:x+W/2,y:y+H/2+4,'font-size':10.5,'font-weight':800,fill:dark?PAPER:INK,'text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);
-    });
-  });
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');
-});
-})();
-
-// ════ G21 · rank strip ════
-// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,
-// 和 slope 同一种丑(catalog 实测三连败那条),废弃。
-// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。
-// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。
-(()=>{
-const RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],
-  ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];
-const SHADE=['#F5572F','#6E6D66','#8F8E86','#C0BFB7','#DBDAD3'];   // rank 1 → 5, #1 wears the accent
-obsReveal('rankstrip',s=>{
-  // rows sorted by final rank, the climber lands on top
-  const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);
-  const X0=110,PX=38,Y0=64,PY=42,C=32;
-  ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>
-    txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));
-  rows.forEach(([p,rk],i)=>{
-    const y=Y0+i*PY,hero=p==='Flows';
-    txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'rgba(31,30,28,.72)','text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.07}s`},p);
-    rk.forEach((r,q)=>{
-      const dark=r<=2;
-      const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],
-        stroke:'rgba(31,30,28,.24)','stroke-width':0.9,
-        class:'pop',style:`animation-delay:${i*.07+q*.035}s`});
-      tip(cell,`${p} — #${r} in Q${q+1}`);
-      txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,
-        fill:dark?PAPER:'#1F1E1C','text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);
-    });
-    // arrival flag: climbed / fell / held, told at the row's end
-    const d=rk[0]-rk[5];
-    txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,
-      fill:d>0?INK:d<0?'#C0BFB7':'rgba(31,30,28,.32)',
-      class:'fade',style:`animation-delay:${.5+i*.07}s`},
-      d>0?'▲'+d:d<0?'▼'+(-d):'—');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');
-});
-})();
-
-// ════ G22 · aggregate sankey ════
-// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——
-// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。
-// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。
-(()=>{
-const SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];
-const DST=['FREE','PRO','TEAM'];
-const SH=[INK,'#6E6D66','#8F8E86','#C0BFB7','rgba(31,30,28,.32)'];
-obsReveal('sankey',s=>{
-  const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;
-  const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));
-  const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));
-  const sy=[],dy=[];
-  let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});
-  y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});
-  const syc=[...sy],dyc=[...dy];
-  SRC.forEach((d,i)=>{
-    d[1].forEach((v,j)=>{
-      if(!v)return;
-      const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;
-      syc[i]=sb;dyc[j]=db;
-      // 最粗的一条缎带是主角(SEARCH→FREE 20),上红
-      const heroRib=i===0&&j===0;
-      const rib=el(s,'path',{d:`M${XL} ${sa} C${MID} ${sa} ${MID} ${da} ${XR} ${da} L${XR} ${db} C${MID} ${db} ${MID} ${sb} ${XL} ${sb} Z`,
-        fill:heroRib?'#F5572F':SH[i],opacity:heroRib?.68:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});
-      tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);
-    });
-  });
-  SRC.forEach((d,i)=>{
-    el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],
-      class:'fade',style:`animation-delay:${i*.06}s`});
-    txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#6E6D66','text-anchor':'end',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);
-  });
-  DST.forEach((n,j)=>{
-    el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#22211F',
-      class:'fade',style:`animation-delay:${.1+j*.06}s`});
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#6E6D66',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'RIBBON WIDTH = ACCOUNTS OF 100 · SHADE = CHANNEL · RED = THE WIDEST RIBBON');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 1260
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/lupi-palm.html

@@ -1,1260 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Lupi L1–L20 × PALM</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#58402E;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.40);--grid:rgba(88,64,46,.16);--lab:rgba(88,64,46,.72);--dmid:rgba(240,239,235,.62);--dq:rgba(240,239,235,.24)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:#F0EFEB}
-  .card.dark .sub{color:var(--dmid)}
-  .card.dark .src{color:var(--dq)}
-  .card.wide{grid-column:1/-1}
-  .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .note{font-size:11px;color:var(--lab);line-height:1.65;margin-bottom:16px}
-  .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:var(--ink);color:var(--bg)}
-  .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}
-  .badge.pair{border:1px dashed var(--faint);color:var(--muted)}
-  .card.dark .badge.del{background:var(--bg);color:var(--ink)}
-  .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}
-  .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-    <h1>Lupi 系 L1–L20 × PALM</h1>
-  <p><span class="chip" style="background:#43593B"></span><span class="chip" style="background:#F2D17E"></span><span class="chip" style="background:#77835A"></span><span class="chip" style="background:#ACAD79"></span><span class="chip" style="background:#58402E"></span> &nbsp;椰林 · 绿黄六色板:麦黄 → 橄榄 → 浅苔 → 苔绿 → 中绿 → 深绿。深绿当主墨、琥珀当主角、浓咖当文字;暗卡上强调改麦黄(峰值 / 最优 / 事故楔形)——深绿在浓咖底上读不出。</p>
-  <p style="margin-top:7px">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 主题 / 4 小队 / 4 簇 / 5 卫星岛 / 每行选项)各走一个色,序数型(漏斗 5 级 / 票量 / 峰值 / 账户数 / 持有量 / 上线周次)走 5 档梯。几何、动画、数据与 lupi-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <!-- 1 · launch fan -->
-  <div class="card">
-    <h2>Twelve features, fanned out</h2>
-    <div class="sub">one spoke per feature · big dot = launch week, sized by MAU today · small dots = every week since</div>
-    <svg id="fan" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">LAUNCH FAN · PALM · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- 4 · dot cascade on dark -->
-  <div class="card dark">
-    <h2>What breaks, stacked and ranked</h2>
-    <div class="sub">incidents by root cause · 2019 → 2026 · one dot = 2 incidents · sorted ascending</div>
-    <svg id="cascade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT CASCADE · PALM · INCIDENT LOG</div>
-  </div>
-
-  <!-- 2 · barcode lollipop, wide + editorial text column -->
-  <div class="card wide">
-    <div class="split">
-      <div>
-        <h2>Ninety days as a barcode</h2>
-        <div class="sub">peak concurrent users, daily · Apr – Jun 2026</div>
-        <div class="note">Every hairline is a day, whether or not anything
-        happened in it. The dot marks the day's peak; the stem below it is
-        just gravity. Read the field, not the numbers — the season has a
-        texture before it has a value.</div>
-        <div class="legend">
-          ● &nbsp;WEEKDAY PEAK<br>
-          ○ &nbsp;WEEKEND PEAK<br>
-          │ &nbsp;ONE CALENDAR DAY<br>
-          ◉ &nbsp;TOP-3, LABELED
-        </div>
-      </div>
-      <div>
-        <svg id="barcode" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-        <div class="src">BARCODE LOLLIPOP · PALM · LIVE OPS</div>
-      </div>
-    </div>
-  </div>
-
-  <!-- 3 · arc bubble matrix -->
-  <div class="card">
-    <h2>Eight products land in twelve cities</h2>
-    <div class="sub">bubble = paying accounts · each row bows like a horizon</div>
-    <svg id="arcmatrix" viewBox="0 0 430 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">ARC MATRIX · PALM · SALES GEO</div>
-  </div>
-
-  <!-- 5 · radial convergence -->
-  <div class="card">
-    <h2>48 requests pull toward five themes</h2>
-    <div class="sub">every rim node = a feature request · lines bundle into the theme that claimed it</div>
-    <svg id="converge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL CONVERGENCE · PALM · FEEDBACK BOARD</div>
-  </div>
-
-  <!-- 6 · cluster field, wide -->
-  <div class="card wide">
-    <h2>The contributor field</h2>
-    <div class="sub">80 core contributors orbit the main repo · satellite islands keep their own gravity · dotted paths = cross-contribution</div>
-    <svg id="clusters" viewBox="0 0 840 320" preserveAspectRatio="xMidYMid meet" style="max-height:360px"></svg>
-    <div class="src">CLUSTER FIELD · PALM · CONTRIBUTOR GRAPH</div>
-  </div>
-
-
-
-  <!-- 1 · brand spectrum -->
-  <div class="card">
-    <h2>Where the brand sits</h2>
-    <div class="sub">panel of 120 customers placed us between opposing traits · big dot = us · small dots = competitors</div>
-    <svg id="spectrum" viewBox="0 0 460 300" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BRAND SPECTRUM · PALM · BRAND SURVEY</div>
-  </div>
-
-  <!-- 2 · dotty matrix -->
-  <div class="card">
-    <h2>Four squads, stacked in space</h2>
-    <div class="sub">each plane = one squad · 6 × 6 weeks of activity · dot area = tasks closed</div>
-    <svg id="dotty" viewBox="0 0 400 372" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-    <div class="src">DOTTY MATRIX · PALM · SPRINT BOARD</div>
-  </div>
-
-  <!-- 3 · ticket almanac, wide -->
-  <div class="card wide">
-    <h2>Eight years of tickets, one almanac</h2>
-    <div class="sub">bubble area = support tickets per product area, per year · bubbles overlap where years pile up · dark core = escalations · dashed ring = still in beta</div>
-    <svg id="almanac" viewBox="0 0 840 400" preserveAspectRatio="xMidYMid meet" style="max-height:420px"></svg>
-    <div class="src">BUBBLE ALMANAC · PALM · SUPPORT DESK</div>
-  </div>
-
-  <!-- 4 · deploy patchwork -->
-  <div class="card">
-    <h2>A quarter of deploys, overlaid</h2>
-    <div class="sub">each wedge = one deploy · angle = time of day · reach = files touched · ink builds where deploys pile up</div>
-    <svg id="patchwork" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL PATCHWORK · PALM · DEPLOY LOG</div>
-  </div>
-
-  <!-- 6 · trend lineage -->
-  <div class="card">
-    <h2>Features rise, fall, come back</h2>
-    <div class="sub">● shipped · ○ reworked · dashed = dormant years · line runs on if it's still alive today</div>
-    <svg id="lineage" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TREND LINEAGE · PALM · PRODUCT HISTORY</div>
-  </div>
-
-  <!-- 8 · type colonnade -->
-  <div class="card">
-    <h2>Forty-four repos, ten owners</h2>
-    <div class="sub">every hairline hands a repo to the team that owns it · the pile-ups are the org chart nobody drew</div>
-    <svg id="colonnade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TYPE COLONNADE · PALM · CODE OWNERSHIP</div>
-  </div>
-
-  <!-- 9 · funnel hourglass -->
-  <div class="card">
-    <h2>The funnel, poured</h2>
-    <div class="sub">each strip's width = people at that stage · threads trickle down · Q2 2026 cohort</div>
-    <svg id="hourglass" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HOURGLASS STREAM · PALM · GROWTH</div>
-  </div>
-
-  <!-- 14 · hundred field -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>A hundred of us, four minds</h2>
-    <div class="sub">one dot = one person in a hundred · four attitude clusters, sized by share · 2026 survey</div>
-    <svg id="hundredfield" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HUNDRED FIELD · PALM · ATTITUDE SURVEY</div>
-  </div>
-
-  <!-- 15 · ballot tally -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>What they fear, tick by tick</h2>
-    <div class="sub">one tick = one respondent in a hundred · inked = picked it · they could pick several</div>
-    <svg id="ballottally" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BALLOT TALLY · PALM · WORKPLACE SURVEY</div>
-  </div>
-
-
-  <!-- L16 · matrix heat -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 矩阵热力</span>
-    <h2>Which features get used together</h2>
-    <div class="sub">8 × 8 co-usage · shade = share of accounts using both · dashed square = strongest pair</div>
-    <svg id="matheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT · PALM · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- L17 · calendar heat -->
-  <div class="card wide">
-    <span class="badge">LUPI 编辑型 · 日历热力</span>
-    <h2>A year of deploys, day by day</h2>
-    <div class="sub">52 weeks × 7 days · dot area = deploys that day · tiny dot = a quiet day · dashed ring = the busiest day of 2025</div>
-    <svg id="calheat" viewBox="0 0 840 232" preserveAspectRatio="xMidYMid meet" style="max-height:260px"></svg>
-    <div class="src">CALENDAR HEAT · PALM · DEPLOY LOG</div>
-  </div>
-
-  <!-- L18 · beeswarm -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 蜂群</span>
-    <h2>A hundred and twenty deals, swarming</h2>
-    <div class="sub">one dot = one closed deal · the swarm piles up where deals cluster · hollow = enterprise · dashed = median</div>
-    <svg id="beeswarm" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BEESWARM · PALM · SALES OPS</div>
-  </div>
-
-  <!-- L19 · ridgeline -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · RIDGELINE</span>
-    <h2>Five pipelines, five tempos</h2>
-    <div class="sub">each crest = one pipeline's completion-time shape · the area is made of hairlines · dot = the peak</div>
-    <svg id="ridge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RIDGELINE · PALM · DATA PLATFORM</div>
-  </div>
-
-  <!-- L20 · parallel coordinates -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 平行坐标</span>
-    <h2>Twelve products, four dimensions</h2>
-    <div class="sub">one hairline = one product across price · satisfaction · retention · growth · ink = the one that wins everywhere</div>
-    <svg id="parallel" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PARALLEL COORDS · PALM · PRODUCT PANEL</div>
-  </div>
-
-</div>
-<script>
-// ── PALM 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)';
-const FLOOR='rgba(88,64,46,.24)',QUIET='rgba(88,64,46,.15)',TRACK='rgba(88,64,46,.12)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#58402E',HERO='#D4A017';
-const FAINTDATA='#ACAD79',BEAD='#77835A',HALO='rgba(240,239,235,.92)';
-const HERODK='#F2D17E';   // 深色卡上的强调色(HERO 在深底上可能读不出)
-const DMID='rgba(240,239,235,.62)',DQ='rgba(240,239,235,.24)';   // 深色卡(L4 dot cascade)上的浅墨 / 静音档
-const SER=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79','#58402E'];   // 分类色:一图之内每个系列 / 每行 / 每簇一个色
-const RING='rgba(88,64,46,.30)';   // 散点的铅笔细边:让浅色号在纸上有形,不用改色号
-const RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B'];   // 5 档序数梯(浅→重):漏斗 / 票量 / 峰值 / 持有量
-const RAMPDK=['rgba(240,239,235,.26)','rgba(240,239,235,.42)','rgba(240,239,235,.60)','rgba(240,239,235,.84)','#F2D17E'];   // 深色卡上的 5 档梯,末档落在强调色
-const HEROMODE=false; // 主角模式:launch fan 的头号功能是否也整体走 HERO
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const sect=(cx,cy,r0,r1,a0,a1)=>{
-  const big=a1-a0>180?1:0;
-  const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
-  const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
-  return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
-};
-// unified reveal: draw when scrolled into view; click replays
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ 1 · launch fan ════
-// spokes sorted by launch week → the big dots cascade along a diagonal arc
-(()=>{
-const FEAT=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync','Grid','Views','Hub','Forms'];
-const LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19];     // week of launch
-const CX=40,CY=304,rOf=w=>56+w*9.7;
-obsReveal('fan',s=>{
-  const ang=i=>-84+i*(76/11);
-  // dotted week guides
-  [5,10,15,20].forEach((w,gi)=>{
-    const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);
-    el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',
-      stroke:QUIET,'stroke-width':1,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${gi*.1}s`});
-    const [lx,ly]=pol(CX,CY,r,-87.5);
-    txt(s,{x:lx,y:ly,'font-size':6.5,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${gi*.1}s`},'W'+w);
-  });
-  FEAT.forEach((name,i)=>{
-    const a=ang(i),L=LAUNCH[i];
-    const mau=Math.round(48-i*3+rnd(i+1,3)*10);
-    const col=RAMP[4-Math.min(4,Math.floor((L-1)/4))];   // 上线越早 = 越重
-    // hairline spoke from launch to rim
-    const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);
-    el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:QUIET,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`});
-    // weekly texture dots
-    for(let w=L+1;w<=20;w++){
-      const [x,y]=pol(CX,CY,rOf(w),a);
-      el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:col,opacity:.55,
-        class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});
-    }
-    // launch dot, sized by MAU
-    const [bx,by]=pol(CX,CY,rOf(L),a);
-    const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:(HEROMODE&&i===0)?HERO:col,
-      class:'pop',style:`animation-delay:${.15+i*.07}s`});
-    tip(big,`${name} — launched W${L} · ${mau}k MAU`);
-    // rim label along the spoke
-    const [tx,ty]=pol(CX,CY,rOf(20)+9,a);
-    txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:LAB,
-      transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',
-      class:'fade',style:`animation-delay:${.3+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 4 · dot cascade on dark ════
-(()=>{
-const CAUSE=['DNS','CDN','QUOTA','DISK','CERT','CACHE','QUEUE','LOCK','OOM','NET','3P API','CONFIG','DB','DEPLOY','CODE','HUMAN'];
-const COUNT=[1,1,2,3,3,4,5,6,7,9,11,13,15,18,22,27];
-obsReveal('cascade',s=>{
-  const x=i=>34+i*22.2, base=i=>272-i*7.5;
-  const worst=COUNT.indexOf(Math.max(...COUNT));   // 主角:最大根因
-  // dotted diagonal baseline
-  el(s,'line',{x1:x(0)-8,y1:base(0)+4,x2:x(15)+8,y2:base(15)+4,
-    stroke:DQ,'stroke-width':1,'stroke-dasharray':'2 4',class:'fade'});
-  CAUSE.forEach((name,i)=>{
-    const n=Math.ceil(COUNT[i]/2),bx=x(i),by=base(i);
-    const bd=Math.min(4,Math.floor(COUNT[i]/6));   // 事故量分 5 档
-    for(let k=0;k<n-1;k++)
-      el(s,'circle',{cx:bx,cy:by-12-k*8.8,r:2.2,fill:RAMPDK[bd],opacity:.95,
-        class:'pop',style:`animation-delay:${i*.05+k*.03}s`});
-    const topY=by-12-(n-1)*8.8;
-    const top=el(s,'circle',{cx:bx,cy:topY,r:i===worst?5.6:4.6,
-      fill:i===worst?HERODK:RAMPDK[Math.min(4,bd+1)],
-      class:'pop',style:`animation-delay:${i*.05+n*.03}s`});
-    tip(top,`${name} — ${COUNT[i]} incidents`);
-    txt(s,{x:bx,y:topY-10,'font-size':9,'font-weight':700,fill:i===worst?HERODK:BG,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.2+i*.05}s`},COUNT[i]);
-    txt(s,{x:bx,y:by+12,'font-size':6.5,'font-weight':600,fill:DMID,'text-anchor':'end',
-      transform:`rotate(-90 ${bx} ${by+12})`,
-      class:'fade',style:`animation-delay:${.1+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 2 · barcode lollipop ════
-(()=>{
-const N=90;
-const val=d=>95+55*Math.sin(d/9.5)+30*Math.sin(d/3.7)+rnd(d+1,5)*40;
-obsReveal('barcode',s=>{
-  // the barcode: one hairline per day, full height
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53;
-    el(s,'line',{x1:x,y1:8,x2:x,y2:258,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${d*.004}s`});
-  }
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  // top-3 peaks, kept at least 6 days apart so labels never collide
-  const top3=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);
-    if(top3.length===3)break;
-  }
-  const vlo=Math.min(...vs),vhi=Math.max(...vs);
-  // 峰值分 4 档,最浅那档不用(5 月的低谷也得看得见)
-  const vband=v=>1+Math.min(3,Math.floor((v-vlo)/(vhi-vlo+1e-9)*4));
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53,v=vs[d],y=252-v*.75;
-    const weekend=d%7===5||d%7===6,col=RAMP[vband(v)];
-    const stemEnd=Math.min(256,y+14+rnd(d+1,9)*26);
-    el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:col,'stroke-width':1.5,
-      class:'fade',style:`animation-delay:${.3+d*.008}s`});
-    const big=top3.includes(d);
-    const dot=el(s,'circle',{cx:x,cy:y,r:big?4.6:2.7,
-      fill:weekend?BG:(big?HERO:col),stroke:big?HERO:col,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.3+d*.008}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)}k peak`);
-    if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.9+d*.004}s`},Math.round(v)+'k');
-  }
-  [[0,'APR'],[30,'MAY'],[61,'JUN']].forEach(([d,m])=>
-    txt(s,{x:18+d*8.53,y:276,'font-size':8,'font-weight':600,fill:MUT,
-      'letter-spacing':'.12em',class:'fade'},m));
-});
-})();
-
-// ════ 3 · arc bubble matrix ════
-(()=>{
-const PROD=['Editor','Boards','Docs','Flows','Chat','Vault','Pages','Sync'];
-const CITY=['SF','NYC','LON','BER','TOK','SYD','SIN','PAR','AMS','TOR','SEO','SAO'];
-const W=[9,8,7,7,6,5,5,4,4,3,3,2];             // market weight
-obsReveal('arcmatrix',s=>{
-  const rowY=i=>74+i*29, colX=j=>92+j*27, dy=j=>-16*Math.sin(Math.PI*j/11);
-  const v=(i,j)=>{
-    if(rnd(i*13+1,j*7+3)<.09)return 0;                 // a few true absences
-    const age=1-i*.085;                                // older rows reach further
-    return Math.min(40,Math.round(W[j]*3.4*age*(.45+rnd(i+1,j+1)*.85)));
-  };
-  // find top-4 cells for labels
-  const all=[];
-  PROD.forEach((_,i)=>CITY.forEach((_,j)=>all.push([v(i,j),i,j])));
-  const top=all.sort((a,b)=>b[0]-a[0]).slice(0,4).map(d=>d[1]*100+d[2]);
-  PROD.forEach((p,i)=>{
-    // the horizon arc
-    const d='M'+CITY.map((_,j)=>`${colX(j)} ${rowY(i)+dy(j)}`).join(' L ');
-    el(s,'path',{d,fill:'none',stroke:QUIET,'stroke-width':1,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.08}s`});
-    txt(s,{x:84,y:rowY(i)+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.08}s`},p);
-    CITY.forEach((c,j)=>{
-      const x=colX(j),y=rowY(i)+dy(j),vv=v(i,j);
-      if(!vv){el(s,'circle',{cx:x,cy:y,r:.9,fill:FLOOR,
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});return}
-      const fill=RAMP[Math.min(4,Math.floor(vv/8))];   // 账户数 5 档
-      const dot=el(s,'circle',{cx:x,cy:y,r:Math.sqrt(vv)*1.3,fill,
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});
-      tip(dot,`${p} · ${c} — ${vv} accounts`);
-      if(top.includes(i*100+j))
-        txt(s,{x,y:y-Math.sqrt(vv)*1.3-4,'font-size':7,'font-weight':800,fill:TXT,
-          'text-anchor':'middle',class:'fade',style:`animation-delay:${.6}s`},vv);
-    });
-  });
-  CITY.forEach((c,j)=>{
-    const x=colX(j),y=74+dy(j)-24;
-    txt(s,{x,y,'font-size':7,'font-weight':700,fill:MUT,'letter-spacing':'.08em',
-      transform:`rotate(-55 ${x} ${y})`,class:'fade',style:`animation-delay:${j*.03}s`},c);
-  });
-});
-})();
-
-// ════ 5 · radial convergence ════
-(()=>{
-const N=48,CX=200,CY=162,R=116;
-const THEME=[['PERF',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['UX',6,213]];
-obsReveal('converge',s=>{
-  // hub assignment: contiguous blocks with a little organic leakage
-  const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
-  const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];
-  const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
-  const counts=[0,0,0,0,0];
-  for(let i=0;i<N;i++)counts[hubOf(i)]++;
-  for(let i=0;i<N;i++){
-    const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
-    const h=hubOf(i),[hx,hy]=hubPt(h);
-    const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
-    const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
-    el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
-      fill:'none',stroke:SER[h],'stroke-width':.9,opacity:.62,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.018}s`});
-    el(s,'circle',{cx:px,cy:py,r:1.6,fill:SER[h],
-      class:'pop',style:`animation-delay:${i*.018}s`});
-    // tiny rim code, flipped on the left half so it stays readable
-    const flip=deg>90&&deg<270,[lx,ly]=pol(CX,CY,R+7,deg);
-    txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUT,
-      'text-anchor':flip?'end':'start','dominant-baseline':'middle',
-      transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,
-      class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));
-  }
-  THEME.forEach(([name,,deg],h)=>{
-    const [hx,hy]=hubPt(h);
-    const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,fill:counts[h]===Math.max(...counts)?HERO:SER[h],
-      class:'pop',style:`animation-delay:${.5+h*.08}s`});
-    tip(hub,`${name} — ${counts[h]} requests`);
-    // theme label parked outside the rim, past the code ring
-    const [tx,ty]=pol(CX,CY,R+34,deg);
-    txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'dominant-baseline':'middle','letter-spacing':'.08em',
-      style:`animation-delay:${.6+h*.08}s`,
-      class:'fade'},`${name} · ${counts[h]}`);
-    // hairline tying the label to its hub, skimming past the rim
-    const [gx,gy]=pol(CX,CY,R+22,deg);
-    el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:TRACK,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
-  });
-});
-})();
-
-// ════ 6 · cluster field ════
-(()=>{
-const SAT=[[622,78,'PLUGINS',22],[706,214,'THEMES',16],[540,268,'DOCS',13],[92,72,'FORKS',12],[774,58,'MIRRORS',9]];
-obsReveal('clusters',s=>{
-  const HX=300,HY=172;
-  // dotted cross-contribution paths, hub → each island
-  SAT.forEach(([x,y],ci)=>{
-    const mx=(HX+x)/2,my=(HY+y)/2-30;
-    el(s,'path',{d:`M${HX} ${HY} Q${mx} ${my} ${x} ${y}`,fill:'none',
-      stroke:GRID,'stroke-width':.9,'stroke-dasharray':'2 4',pathLength:1,
-      class:'draw',style:`animation-delay:${.8+ci*.12}s;animation-duration:.7s`});
-  });
-  // main hub: 80 golden-angle spokes
-  for(let i=0;i<80;i++){
-    const a=i*137.508,len=28+rnd(i+1,7)*88;
-    const [x,y]=pol(HX,HY,len,a);
-    el(s,'line',{x1:HX,y1:HY,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.012}s;animation-duration:.5s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:1.6+rnd(i+1,4)*1.8,fill:SER[0],opacity:.9,
-      class:'pop',style:`animation-delay:${.15+i*.012}s`});
-    tip(dot,`core contributor #${i+1}`);
-  }
-  el(s,'circle',{cx:HX,cy:HY,r:7,fill:HERO,class:'pop'});
-  txt(s,{x:HX,y:HY+140,'font-size':8,'font-weight':800,fill:HERO,'text-anchor':'middle',
-    'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,
-    class:'fade'},'CORE · 80');
-  // satellite islands
-  SAT.forEach(([x,y,name,n],ci)=>{
-    const col=RAMP[1+Math.round((n-9)/13*3)];   // 岛越大越重(22→4 档,9→1 档)
-    el(s,'circle',{cx:x,cy:y,r:3.5,fill:col,
-      class:'pop',style:`animation-delay:${.9+ci*.12}s`});
-    for(let k=0;k<n;k++){
-      const a=k*137.508+ci*40,rr=9+rnd(k+1,ci+2)*26;
-      const [dx,dy]=pol(x,y,rr,a);
-      if(k%2===0)el(s,'line',{x1:x,y1:y,x2:dx,y2:dy,stroke:TRACK,'stroke-width':.5,
-        class:'fade',style:`animation-delay:${1+ci*.12+k*.02}s`});
-      el(s,'circle',{cx:dx,cy:dy,r:1.4+rnd(k+3,ci+5)*1.4,fill:col,
-        stroke:RING,'stroke-width':.4,
-        class:'pop',style:`animation-delay:${1+ci*.12+k*.02}s`});
-    }
-    txt(s,{x,y:y+44,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.12em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,
-      class:'fade'},`${name} · ${n}`);
-  });
-});
-})();
-
-// ════ 1 · brand spectrum ════
-// paired opposites; the pale ribbon threads our position down the page
-(()=>{
-const ROWS=[['FRIEND','AUTHORITY'],['SERIOUS','PLAYFUL'],['RELIABLE','RISK-TAKING'],['CONTEMPORARY','CLASSIC']];
-const US=[.74,.80,.40,.22];                 // 0 = left trait, 1 = right trait
-const COMP=[[.42,.52,.57],[.60,.65,.86],[.30,.34,.50],[.36,.41,.73]];
-const X0=118,X1=342,Y=i=>58+i*60,px=t=>X0+t*(X1-X0);
-obsReveal('spectrum',s=>{
-  // the ribbon first, underneath everything
-  let d=`M${px(US[0])} ${Y(0)}`;
-  for(let i=1;i<4;i++)
-    d+=` C${px(US[i-1])} ${Y(i-1)+30} ${px(US[i])} ${Y(i)-30} ${px(US[i])} ${Y(i)}`;
-  el(s,'path',{d,fill:'none',stroke:HALO,'stroke-width':30,'stroke-linecap':'round',
-    'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',
-    style:'animation-duration:1.4s'});
-  ROWS.forEach(([a,b],i)=>{
-    const y=Y(i);
-    el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:FAINT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    [X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:FAINT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`}));
-    txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);
-    txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);
-    COMP[i].forEach((t,k)=>{
-      const c=el(s,'circle',{cx:px(t),cy:y,r:3.2,fill:SER[(i+1)%4],
-        stroke:RING,'stroke-width':.6,
-        class:'pop',style:`animation-delay:${.3+i*.1+k*.05}s`});
-      tip(c,`competitor ${'ABC'[k]} — ${Math.round(t*100)}% toward ${b}`);
-    });
-    const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:HERO,
-      class:'pop',style:`animation-delay:${.5+i*.1}s`});
-    tip(us,`us — ${Math.round(US[i]*100)}% toward ${b}`);
-  });
-  txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:FAINT,'letter-spacing':'.12em',
-    class:'fade',style:'animation-delay:.9s'},'● US    ● COMPETITORS A · B · C');
-});
-})();
-
-// ════ 2 · dotty matrix — isometric stacked dot planes ════
-// pitch 20/10, dot radius capped well under half-pitch, decks 64px apart:
-// each plane reads as its own quiet grid instead of a smudge
-(()=>{
-const SQUAD=['PLATFORM','GROWTH','MOBILE','INFRA'];
-const SHADE=[SER[3],SER[2],SER[1],SER[0]];   // 底层→顶层,各队一色
-const P=(c,r,k)=>[196+(c-r)*20,236+(c+r)*10-k*64];
-obsReveal('dotty',s=>{
-  for(let k=0;k<4;k++){
-    // plane slab: paper fill so upper decks occlude lower ones
-    const cs=[P(-.8,-.8,k),P(5.8,-.8,k),P(5.8,5.8,k),P(-.8,5.8,k)];
-    el(s,'path',{d:'M'+cs.map(p=>p.join(' ')).join(' L ')+' Z',
-      fill:BG,'fill-opacity':.96,stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${k*.16}s`});
-    for(let r=0;r<6;r++)for(let c=0;c<6;c++){
-      const load=rnd(k*37+r*6+c+1,k+2);
-      const v=load<.3?0:Math.round(load*12);   // sparser: silence is part of the texture
-      const [x,y]=P(c,r,k);
-      if(!v){el(s,'circle',{cx:x,cy:y,r:.7,fill:FLOOR,
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});continue}
-      const dot=el(s,'circle',{cx:x,cy:y,r:1+Math.sqrt(v)*1.15,fill:SHADE[k],
-        stroke:RING,'stroke-width':.45,
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});
-      tip(dot,`${SQUAD[k]} · week ${c+1} × lane ${r+1} — ${v} tasks`);
-    }
-    // label off the plane's right corner, tied by a short hairline
-    const [cxr,cyr]=P(5.8,-.8,k);
-    el(s,'line',{x1:cxr+4,y1:cyr,x2:cxr+22,y2:cyr,stroke:FAINT,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.2+k*.16}s`});
-    txt(s,{x:cxr+27,y:cyr+2.5,'font-size':7.5,'font-weight':700,fill:k===3?TXT:MUT,
-      'letter-spacing':'.12em',class:'fade',style:`animation-delay:${.2+k*.16}s`},SQUAD[k]);
-  }
-});
-})();
-
-// ════ 3 · bubble almanac ════
-// the vintage-almanac feel lives in three things: ledger-paper hairlines
-// under everything, bubbles big enough to collide, and marginalia. do all three.
-(()=>{
-const AREAS=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','GRID','VIEWS','HUB','FORMS'];
-const BETA={GRID:2023,VIEWS:2024,HUB:2025,FORMS:2025};   // beta until this year
-const BORN={EDITOR:2019,BOARDS:2019,DOCS:2019,CHAT:2020,FLOWS:2020,VAULT:2021,PAGES:2021,SYNC:2022,GRID:2022,VIEWS:2023,HUB:2024,FORMS:2024};
-obsReveal('almanac',s=>{
-  const rowY=i=>62+i*34,colX=j=>132+j*57;
-  // ledger paper: hairlines every 6.8px, edge to edge, under everything
-  for(let y=40;y<=318;y+=6.8)
-    el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:QUIET,'stroke-width':.5,
-      class:'fade',style:`animation-delay:${(y-40)*.001}s`});
-  const vals=[];
-  for(let i=0;i<8;i++){
-    const yr=2019+i;
-    el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${i*.04}s`});
-    txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.04}s`},yr);
-    AREAS.forEach((a,j)=>{
-      if(yr<BORN[a])return;
-      const age=yr-BORN[a];
-      // steep spread: quiet areas stay pinpricks, busy ones balloon
-      const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));
-      vals.push([Math.max(1,v),i,j,a,yr]);
-    });
-  }
-  // draw small over large so the giants sit underneath as washes
-  vals.sort((a,b)=>b[0]-a[0]);
-  const top3=vals.slice(0,3);
-  // hand-drawn circle: radius wobbles around the rim on two slow sine waves,
-  // so every bubble is its own slightly-off print — never a compass circle
-  const blob=(x,y,r,seed)=>{
-    const n=Math.max(14,Math.round(r*1.6)),pts=[];
-    for(let t=0;t<n;t++){
-      const a=t/n*Math.PI*2;
-      const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;
-      pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);
-    }
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<n;t++){
-      const p=pts[t],q=pts[(t+1)%n];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+' Z';
-  };
-  vals.forEach(([v,i,j,a,yr],k)=>{
-    const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10,  // jitter off the gridline
-          y=rowY(i),r=Math.sqrt(v)*3.8;        // big: rows are 34 apart, giants reach ~28 and collide
-    const beta=BETA[a]&&yr<=BETA[a];
-    const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});
-    const seed=i*12+j,isTop=top3.includes(vals[k]);   // 主角:票量 top-3
-    const col=RAMP[Math.min(4,Math.floor(v/12))];        // 票量 5 档
-    const dot=el(g,'path',beta
-      ?{d:blob(x,y,r,seed),fill:'none',stroke:DATA2,'stroke-width':1.2,'stroke-dasharray':'3 3'}
-      :{d:blob(x,y,r,seed),fill:col,'fill-opacity':.16+rnd(i+2,j+4)*.14,
-        stroke:isTop?HERO:col,'stroke-opacity':isTop?.95:.5+rnd(i+5,j+7)*.38,
-        'stroke-width':isTop?1.7:.8+rnd(i+3,j+11)*1});
-    tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);
-    // escalation core: small wobbly blot, nudged off-center like a press mark
-    if(!beta){
-      const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;
-      el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:isTop?HERO:TXT});
-    }
-    if(isTop)
-      txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:HERO,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},v*10);
-  });
-  AREAS.forEach((a,j)=>{
-    const x=colX(j);
-    txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUT,'letter-spacing':'.1em',
-      transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);
-  });
-  // marginalia: two annotations with pointer hairlines, like the original's notes
-  el(s,'path',{d:`M${colX(3)+30} ${rowY(5)-14} C${colX(3)+70} ${rowY(5)-34} ${colX(3)+96} ${rowY(5)-38} ${colX(3)+118} ${rowY(5)-38}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:colX(3)+124,y:rowY(5)-35,'font-size':7,fill:LAB,'font-style':'italic',
-    class:'fade',style:'animation-delay:1.1s'},'chat tickets triple after mobile GA');
-  el(s,'path',{d:`M${colX(9)+16} ${rowY(2)+8} C${colX(9)+40} ${rowY(2)+26} ${colX(9)+60} ${rowY(2)+30} ${colX(9)+80} ${rowY(2)+30}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7,fill:LAB,'font-style':'italic',
-    class:'fade',style:'animation-delay:1.2s'},'betas barely ticket — nobody files bugs on toys');
-  // footer strip: important events, the almanac's timeline shelf
-  const evY=356;
-  el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:FAINT,'stroke-width':.8,
-    class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:56,y:evY-24,'font-size':7,'font-weight':800,fill:MUT,'letter-spacing':'.16em',
-    class:'fade',style:'animation-delay:1.2s'},'IMPORTANT EVENTS');
-  [['2020','SLA introduced'],['2022','self-serve help center'],['2023','the GA wave'],['2025','AI deflection live']]
-  .forEach(([yr,note],k)=>{
-    const x=90+k*196;
-    el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:MUT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`});
-    txt(s,{x,y:evY+4,'font-size':8,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);
-    txt(s,{x,y:evY+15,'font-size':7,fill:MUT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);
-  });
-});
-})();
-
-// ════ 4 · radial patchwork ════
-(()=>{
-const CX=200,CY=168,N=46;
-obsReveal('patchwork',s=>{
-  // 24h rim ticks
-  for(let h=0;h<96;h++){
-    const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:FAINT,'stroke-width':h%4===0?1:.5,
-      class:'fade',style:`animation-delay:${h*.006}s`});
-  }
-  [0,6,12,18].forEach(h=>{
-    const [x,y]=pol(CX,CY,157,-90+h*15);
-    txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      class:'fade'},String(h).padStart(2,'0'));
-  });
-  // the patchwork: translucent wedges pile into ink
-  const wedges=[];
-  for(let i=0;i<N;i++){
-    // deploys cluster around 11h and 16h, thin overnight
-    const peak=rnd(i+1,2)>.5?11:16;
-    const hour=(peak+(rnd(i+1,3)-.5)*7+24)%24;
-    const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;
-    const r1=34+rnd(i+1,5)*100;
-    wedges.push([a0,sw,r1,hour,i]);
-  }
-  wedges.forEach(([a0,sw,r1,hour,i])=>{
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:RAMP[Math.min(4,Math.floor(hour/4.8))],
-      'fill-opacity':.17+rnd(i+1,6)*.15,class:'fade',
-      style:`animation-delay:${.2+i*.022}s`});
-    tip(w,`deploy #${i+1} — ${String(Math.floor(hour)).padStart(2,'0')}:${String(Math.floor(hour%1*60)).padStart(2,'0')} · ${Math.round(r1*6)} files`);
-  });
-  // three outlined wedges: the ones that paged somebody
-  [4,17,31].forEach((i,k)=>{
-    const [a0,sw,r1]=wedges[i];
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',
-      stroke:HERO,'stroke-width':1.5,class:'fade',style:`animation-delay:${1.2+k*.15}s`});
-    tip(w,`deploy #${i+1} — triggered an incident`);
-  });
-  el(s,'circle',{cx:CX,cy:CY,r:3,fill:DATA,class:'pop',style:'animation-delay:1.3s'});
-  txt(s,{x:CX,y:308,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.4s'},'OUTLINED = TRIGGERED AN INCIDENT');
-});
-})();
-
-// ════ 6 · trend lineage ════
-(()=>{
-// [name, events [year, type f|o], aliveToday]
-const TR=[
-  ['Dark mode',[[2017,'f'],[2022,'o']],true],
-  ['Kanban',[[2016,'f']],true],
-  ['Wikis',[[2016,'f'],[2019,'o']],false],
-  ['Slash cmds',[[2018,'f']],true],
-  ['Templates',[[2019,'f'],[2024,'o']],true],
-  ['Inbox',[[2020,'f']],false],
-  ['Live cursors',[[2020,'f'],[2023,'o']],true],
-  ['AI drafts',[[2023,'f'],[2025,'o']],true],
-  ['Voice notes',[[2021,'f']],false],
-  ['Offline',[[2018,'f'],[2026,'o']],true],
-];
-const Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;
-// 主角:最近一次有动作的功能线 —— 标题里的 "come back"
-const HC=TR.map((r,i)=>[r[1][r[1].length-1][0],i]).sort((a,b)=>b[0]-a[0])[0][1];
-obsReveal('lineage',s=>{
-  for(let yr=Y0;yr<=2026;yr++){
-    el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:QUIET,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
-    if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
-  }
-  TR.forEach(([name,ev,alive],c)=>{
-    const x=colX(c),hero=c===HC;
-    const lastYr=ev[ev.length-1][0];
-    // 越近期动过 = 越重;已死的走最浅一档(虚线本身已经在说休眠)
-    const col=hero?HERO:RAMP[alive?Math.min(4,Math.max(2,Math.round((lastYr-2016)/10*4))):1];
-    // segments between events: solid while fresh, dashed once dormant
-    for(let k=0;k<ev.length-1;k++){
-      const gap=ev[k+1][0]-ev[k][0];
-      el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
-        stroke:col,'stroke-width':hero?1.8:1.4,
-        'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
-        class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
-    }
-    const last=ev[ev.length-1][0];
-    if(alive)
-      el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:col,'stroke-width':hero?1.8:1.4,
-        pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
-    ev.forEach(([yr,t])=>{
-      const node=el(s,'circle',t==='f'
-        ?{cx:x,cy:yY(yr),r:5.5,fill:col}
-        :{cx:x,cy:yY(yr),r:5.5,fill:BG,stroke:col,'stroke-width':1.6});
-      node.setAttribute('class','pop');
-      node.style.animationDelay=(.35+c*.07)+'s';
-      tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);
-    });
-    // terminal dot: alive lands on today's baseline, dead stops where it stopped
-    el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
-      fill:col,class:'pop',style:`animation-delay:${.6+c*.07}s`});
-    const ly=yY(2026)+26;
-    txt(s,{x,y:ly,'font-size':6.8,'font-weight':hero?800:600,fill:hero?HERO:(alive?LAB:FAINT),
-      transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',
-      class:'fade',style:`${hero?`paint-order:stroke;stroke:${BG};stroke-width:3px;`:''}animation-delay:${.7+c*.05}s`},name);
-  });
-});
-})();
-
-// ════ 8 · type colonnade ════
-(()=>{
-const PRE=['core','ui','api','data','auth','sync','mail','flag','edge','doc','bot'];
-const SUF=['-kit','-svc','-web','-cli','-db','-gw','-sdk','-jobs'];
-const TEAM=['PLATFORM','FRONTEND','BACKEND','DATA','INFRA','MOBILE','SECURITY','GROWTH','ML','DESIGN'];
-obsReveal('colonnade',s=>{
-  const N=44;
-  const teamY=j=>36+j*28,repoY=i=>16+i*6.7;
-  const counts=Array(10).fill(0);
-  const owner=i=>{const w=rnd(i+1,17);return Math.min(9,Math.floor(w*w*10))}; // skewed: low teams hoard
-  for(let i=0;i<N;i++)counts[owner(i)]++;
-  for(let i=0;i<N;i++){
-    const name=PRE[i%11]+SUF[Math.floor(rnd(i+1,8)*8)%8]+(i>21?'-v2':'');
-    const yi=repoY(i),j=owner(i),yj=teamY(j);
-    const col=RAMP[Math.min(4,Math.floor(counts[j]/3))];   // 团队持有量 5 档
-    txt(s,{x:112,y:yi+2,'font-size':5,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.012}s`},name);
-    el(s,'rect',{x:117,y:yi-1.2,width:4,height:2.4,fill:col,
-      class:'fade',style:`animation-delay:${i*.012}s`});
-    el(s,'path',{d:`M123 ${yi} C 200 ${yi} 220 ${yj} 288 ${yj}`,fill:'none',
-      stroke:col,'stroke-width':.8,opacity:.78,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.015}s;animation-duration:.7s`});
-  }
-  TEAM.forEach((t,j)=>{
-    const y=teamY(j);
-    el(s,'circle',{cx:293,cy:y,r:2.6+counts[j]*.55,fill:RAMP[Math.min(4,Math.floor(counts[j]/3))],
-      class:'pop',style:`animation-delay:${.6+j*.05}s`});
-    const lab=txt(s,{x:302+counts[j]*.55,y:y+2.5,'font-size':7,'font-weight':700,fill:LAB,
-      class:'fade',style:`animation-delay:${.65+j*.05}s`},`${t} · ${counts[j]}`);
-    tip(lab,`${t} owns ${counts[j]} repos`);
-  });
-});
-})();
-
-// ════ 9 · hourglass stream ════
-(()=>{
-const STAGE=[['VISITORS',4200],['SIGN-UPS',1900],['ACTIVATED',960],['RETAINED',540],['PAYING',310]];
-obsReveal('hourglass',s=>{
-  const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;
-  // 主角:通过率最低的一级 —— 漏得最狠的地方
-  const PCT=STAGE.slice(1).map((n,k)=>Math.round(n[1]/STAGE[k][1]*100));
-  const leak=PCT.indexOf(Math.min(...PCT));
-  STAGE.forEach(([name,c],k)=>{
-    const y=sy(k),hw=w(c)/2;
-    // barcode strip: one tick ≈ 40 people
-    const n=Math.round(c/40);
-    for(let t=0;t<n;t++){
-      const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;
-      el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:RAMP[k],'stroke-width':1.1,
-        opacity:.6+rnd(t+2,k+5)*.4,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
-    }
-    // threads trickling to the next strip
-    if(k<4){
-      const hw1=w(STAGE[k+1][1])/2;
-      for(let t=0;t<34;t++){
-        const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
-        const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
-        el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,
-          fill:'none',stroke:FAINTDATA,'stroke-width':.7,opacity:.45,pathLength:1,
-          class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
-      }
-      // conversion between stages, parked in the left margin
-      const pct=Math.round(STAGE[k+1][1]/c*100);
-      txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':k===leak?10.5:8.5,'font-weight':800,
-        fill:k===leak?HERO:MUT,
-        class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
-      txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6,'font-weight':600,fill:k===leak?HERO:FAINT,
-        'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'GET THROUGH');
-    }
-    // stage label, tied to the strip edge
-    el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.3+k*.12}s`});
-    txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:LAB,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
-    txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());
-  });
-});
-})();
-
-// ════ 14 · hundred field ════
-// small data, unit-decomposed: four shares of 100% become 100 people.
-// phyllotaxis discs (golden angle) so每簇自带肌理; share is countable, not just readable.
-(()=>{
-const SEG=[['CHARGED',41,SER[0]],['TORN',35,SER[1]],['ADRIFT',12,SER[2]],['AVERSE',12,SER[3]]];
-const POS=[[132,140],[276,116],[186,252],[322,238]];
-obsReveal('hundredfield',s=>{
-  // dashed constellation hairlines tying the cluster cores
-  [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:GRID,'stroke-width':.7,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      // hairline spoke for every 5th person, core to dot
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,
-        class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,
-        stroke:RING,'stroke-width':.4,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} — one of ${v} in 100`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:TXT,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},'ONE DOT = ONE PERSON · 41 + 35 + 12 + 12 = 100');
-});
-})();
-
-// ════ 15 · ballot tally ════
-// multi-select survey (each option independent, 0–100): every option is a row of
-// 100 hairline ticks — a hundred people standing in line, the ones who picked it inked.
-(()=>{
-const ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];
-obsReveal('ballottally',s=>{
-  const VS=ASK.map(a=>a[1]),topI=VS.indexOf(Math.max(...VS));   // 主角:得票最高项
-  ASK.forEach(([name,v],i)=>{
-    const base=78+i*62;
-    txt(s,{x:28,y:base-26,'font-size':7.5,'font-weight':700,fill:LAB,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=28+k*3.44,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?(i===topI?HERO:SER[i]):FLOOR,'stroke-width':picked?1.3:.6,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-11,'font-size':i===topI?12.5:11,'font-weight':800,
-      fill:i===topI?HERO:TXT,
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`${v} of 100 picked this — they could pick several`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
-});
-})();
-
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ L16 · matrix heat ════
-// 通用矩阵热力:任意 分类×分类×连续值。明度即数据——五档灰阶方块,
-// 越黑越高。对角线不参与(自己×自己),零格留一粒小点(沉默可见)。
-// 最强的一对上虚线框 + 标数。区别于 F10 dot heat:这里编码用明度不用面积,
-// 值是占比/强度而不是可数件数。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC'];
-const v=(i,j)=>{
-  if(i===j)return -1;
-  const a=Math.min(i,j),b=Math.max(i,j);
-  const pop=(8-a)/8*(8-b)/8;                       // 头部功能重合度高
-  const raw=pop*62*(0.35+rnd(a*8+b+1,a+b+3)*.9);
-  return Math.round(raw);
-};
-const shade=t=>t>36?INK:t>24?'#77835A':t>14?'#929960':t>6?'#ACAD79':'#F2D17E';
-obsReveal('matheat',s=>{
-  const X0=118,Y0=70,P=26,C=22;
-  let max=0,mi=0,mj=0;
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  FEAT.forEach((f,i)=>{
-    txt(s,{x:X0-10,y:Y0+i*P+C/2+3,'font-size':7,'font-weight':700,fill:'rgba(88,64,46,.72)','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.04}s`},f);
-    const cy=Y0-10;
-    txt(s,{x:X0+i*P+C/2,y:cy,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.06em',
-      transform:`rotate(-55 ${X0+i*P+C/2} ${cy})`,class:'fade',style:`animation-delay:${i*.04}s`},f);
-  });
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){
-    const x=X0+j*P,y=Y0+i*P,t=v(i,j),d=(i+j)*.02;
-    if(t<0){el(s,'rect',{x:x+C/2-3,y:y+C/2-.6,width:6,height:1.2,fill:'rgba(88,64,46,.15)',
-      class:'fade',style:`animation-delay:${d}s`});continue}
-    if(!t){el(s,'circle',{cx:x+C/2,cy:y+C/2,r:.9,fill:'rgba(88,64,46,.15)',
-      class:'pop',style:`animation-delay:${d}s`});continue}
-    const cell=el(s,'rect',{x,y,width:C,height:C,rx:4,fill:shade(t),
-      class:'pop',style:`animation-delay:${d}s`});
-    tip(cell,`${FEAT[i]} × ${FEAT[j]} — ${t}% of accounts use both`);
-    if(i===mi&&j===mj){
-      el(s,'rect',{x:x-3.5,y:y-3.5,width:C+7,height:C+7,rx:6,fill:'none',
-        stroke:'#D4A017','stroke-width':1.8,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:.9s'});
-      txt(s,{x:x+C/2,y:y+C/2+3.5,'font-size':9,'font-weight':800,fill:PAPER,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},t);
-    }
-  }
-  // shade legend, countable buckets
-  const LG=[['#F2D17E','1–6'],['#ACAD79','7–14'],['#929960','15–24'],['#77835A','25–36'],[INK,'37+']];
-  LG.forEach(([c,lab],k)=>{
-    const x=118+k*54;
-    el(s,'rect',{x,y:292,width:9,height:9,rx:2,fill:c,class:'fade',style:`animation-delay:${1+k*.05}s`});
-    txt(s,{x:x+13,y:300,'font-size':6.5,'font-weight':600,fill:'#929960',
-      class:'fade',style:`animation-delay:${1+k*.05}s`},lab+'%');
-  });
-  txt(s,{x:36,y:300,'font-size':7,'font-weight':600,fill:'#ACAD79','letter-spacing':'.1em',
-    class:'fade',style:'animation-delay:1s'},'SHADE = %');
-});
-})();
-
-// ════ L17 · calendar heat ════
-// 日历热力:GitHub 贡献墙的 Lupi 皮。一格一天,52 周 × 7 天,
-// 点面积 = 当天量(sqrt 换算),静默日一粒小点。月份写在上檐,
-// 峰值日虚线圈 + 旁注引线。逐记录世界观:一年 365 天每天都在场。
-(()=>{
-const MON=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
-const DAY=['MON','WED','FRI','SUN'];
-const val=(w,d)=>{
-  const wd=d<5;
-  const season=1+.55*Math.sin((w-8)/9);            // 春秋两个发布季
-  if(!wd)return rnd(w*7+d+1,3)>.82?1:0;            // 周末基本静默
-  const raw=season*(2.5+rnd(w*7+d+1,d+2)*9)*(rnd(w+1,d+5)>.12?1:0);
-  return Math.round(raw);
-};
-obsReveal('calheat',s=>{
-  const X0=56,Y0=42,P=14.8;
-  let max=0,mw=0,md=0;
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){const t=val(w,d);if(t>max){max=t;mw=w;md=d}}
-  DAY.forEach((lab,k)=>{
-    txt(s,{x:X0-10,y:Y0+[0,2,4,6][k]*P+3,'font-size':6.5,'font-weight':700,fill:'#929960','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${k*.04}s`},lab);
-  });
-  MON.forEach((m,k)=>{
-    const x=X0+Math.round(k*52/12)*P;
-    txt(s,{x,y:Y0-16,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',
-      class:'fade',style:`animation-delay:${k*.03}s`},m);
-    el(s,'line',{x1:x,y1:Y0-11,x2:x,y2:Y0-5,stroke:'rgba(88,64,46,.32)','stroke-width':1.26,
-      class:'fade',style:`animation-delay:${k*.03}s`});
-  });
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){
-    const x=X0+w*P,y=Y0+d*P,t=val(w,d),delay=w*.012+d*.004;
-    if(!t){el(s,'circle',{cx:x,cy:y,r:.75,fill:'rgba(88,64,46,.15)',
-      class:'pop',style:`animation-delay:${delay}s`});continue}
-    const r=1.1+Math.sqrt(t)*1.55;
-    const dot=el(s,'circle',{cx:x,cy:y,r,
-      fill:t>max*.66?INK:t>max*.33?'#77835A':'#ACAD79',
-      class:'pop',style:`animation-delay:${delay}s`});
-    tip(dot,`W${w+1} ${['MON','TUE','WED','THU','FRI','SAT','SUN'][d]} — ${t} deploys`);
-  }
-  // peak: dashed ring + marginalia hairline
-  const px=X0+mw*P,py=Y0+md*P,pr=1.1+Math.sqrt(max)*1.55;
-  el(s,'circle',{cx:px,cy:py,r:pr+3.6,fill:'none',stroke:'#D4A017','stroke-width':1.8,
-    'stroke-dasharray':'2 3',class:'fade',style:'animation-delay:1s'});
-  el(s,'path',{d:`M${px+pr+6} ${py+6} C${px+34} ${py+30} ${px+52} ${py+36} ${px+72} ${py+36}`,
-    fill:'none',stroke:'#ACAD79','stroke-width':1.26,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:px+78,y:py+39,'font-size':7,fill:'rgba(88,64,46,.72)','font-style':'italic',
-    class:'fade',style:'animation-delay:1.15s'},`the release-week spike — ${max} deploys in a day`);
-  txt(s,{x:420,y:222,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DAY · DOT AREA = DEPLOYS · TINY DOT = A QUIET DAY');
-});
-})();
-
-// ════ L18 · beeswarm ════
-// 蜂群:violin 的 Lupi 姊妹。不聚合密度,把 120 条真实记录沿数值轴
-// 逐颗排开,撞上了就往两边让——堆得高的地方就是分布的峰。
-// 空心 = enterprise 单,虚线 = 中位数。每颗点背后是一条真实成交。
-(()=>{
-const N=120;
-const deals=Array.from({length:N},(_,i)=>{
-  const u=rnd(i+1,3),ent=rnd(i+2,11)>.86;
-  const v=Math.round(4+(ent?60:6)+150*Math.pow(u,2.6)+rnd(i+3,7)*10);
-  return [Math.min(178,v),ent];
-});
-obsReveal('beeswarm',s=>{
-  const X0=36,X1=376,BASE=246,R=4,GAP=R*2+.8;
-  const mapX=v=>X0+v/180*(X1-X0);
-  // axis rail + barcode ticks
-  el(s,'line',{x1:X0-4,y1:BASE,x2:X1+4,y2:BASE,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let g=0;g<=9;g++){
-    const x=X0+g/9*(X1-X0);
-    el(s,'line',{x1:x,y1:BASE,x2:x,y2:BASE+(g%3===0?7:4),stroke:'rgba(88,64,46,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${g*.02}s`});
-    if(g%3===0)txt(s,{x,y:BASE+18,'font-size':7,'font-weight':600,fill:'rgba(88,64,46,.32)','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${g*.02}s`},'$'+Math.round(g/9*180)+'k');
-  }
-  // swarm as clean stacks: x snaps to a GAP-wide lane, dots pile upward —
-  // the pile height IS the density. no half-overlaps, every dot countable.
-  const lanes={};
-  const sorted=deals.map((d,i)=>[d[0],d[1],i]).sort((a,b)=>a[0]-b[0]);
-  sorted.forEach(([v,ent,i])=>{
-    const lane=Math.round((mapX(v)-X0)/GAP);
-    const x=X0+lane*GAP;
-    const row=lanes[lane]||0;lanes[lane]=row+1;
-    const y=BASE-R-1-row*GAP;
-    // 数据点的环不吃加墨规则:1.98 的描边会把 r=2.9 的空心点糊成实心
-    const dot=ent
-      ?el(s,'circle',{cx:x,cy:y,r:R,fill:PAPER,stroke:'#58402E','stroke-width':1.3,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`})
-      :el(s,'circle',{cx:x,cy:y,r:R,fill:INK,opacity:.85,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`});
-    tip(dot,`deal #${i+1} — $${v}k${ent?' · enterprise':''}`);
-  });
-  // median
-  const vs=deals.map(d=>d[0]).sort((a,b)=>a-b);
-  const med=vs[Math.floor(N/2)];
-  const mx=mapX(med);
-  el(s,'line',{x1:mx,y1:52,x2:mx,y2:BASE+8,stroke:'#D4A017','stroke-width':1.62,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx,y:44,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1.1s`,
-    class:'fade'},'MEDIAN $'+med+'k');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DEAL · THE PILE IS THE DISTRIBUTION · HOLLOW = ENTERPRISE');
-});
-})();
-
-// ════ L19 · ridgeline ════
-// 多组分布的 Lupi 皮:每行一条完成时长的密度山脊,面积由竖发丝组成
-// (F3 hairline area 的语法),行与行轻微重叠,后排画在前、前排糊上
-// 纸色 slab 遮挡(dotty matrix 的手法)。峰值点标中心时长。
-(()=>{
-const ROWS=[['MOBILE',3.2,1.3],['DESKTOP',5.1,1.9],['API',7.4,2.3],['IMPORTS',10.8,3.2],['BATCH',15.2,4]];
-// 每行一档绿:越快越深(RAMP6 前五档),行既是类目也是快慢的序
-const RC=['#43593B','#5A7049','#77835A','#929960','#ACAD79'];
-obsReveal('ridge',s=>{
-  const X0=92,X1=372,MAXH=24,N=72,LIFT=54;
-  const rowY=i=>96+i*44;
-  ROWS.forEach(([name,c,sp],i)=>{
-    const y0=rowY(i);
-    const dens=Array.from({length:N},(_,k)=>{
-      const h=k/(N-1)*MAXH;
-      return Math.exp(-((h-c)**2)/(2*sp*sp))+.26*Math.exp(-((h-c*2.1)**2)/(2*(sp*1.7)**2));
-    });
-    const dmax=Math.max(...dens);
-    const pts=dens.map((d,k)=>[X0+k/(N-1)*(X1-X0),y0-d/dmax*LIFT]);
-    const crest='M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ');
-    el(s,'path',{d:crest+` L${X1} ${y0} L${X0} ${y0} Z`,fill:PAPER,'fill-opacity':.96,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    pts.forEach(([px,py],k)=>{
-      if(k%2)return;
-      el(s,'line',{x1:px,y1:y0,x2:px,y2:py,stroke:RC[i],'stroke-width':0.9,
-        opacity:.34+rnd(k+1,i+3)*.32,class:'fade',style:`animation-delay:${i*.1+k*.004}s`});
-    });
-    el(s,'path',{d:crest,fill:'none',stroke:RC[i],'stroke-width':1.62,pathLength:1,
-      class:'draw',style:`animation-delay:${.1+i*.1}s;animation-duration:.8s`});
-    el(s,'line',{x1:X0,y1:y0,x2:X1,y2:y0,stroke:GRID,'stroke-width':1.26,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    const pk=dens.indexOf(dmax),[px,py]=pts[pk];
-    const dot=el(s,'circle',{cx:px,cy:py,r:2.6,fill:RC[i],class:'pop',style:`animation-delay:${.6+i*.1}s`});
-    tip(dot,`${name} — most runs finish around ${c.toFixed(1)}h`);
-    txt(s,{x:px+7,y:py+1,'font-size':8,'font-weight':800,fill:INK,
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.7+i*.1}s`,
-      class:'fade'},c.toFixed(1)+'h');
-    txt(s,{x:X0-10,y:y0-3,'font-size':7.5,'font-weight':700,fill:'rgba(88,64,46,.72)','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-  });
-  const yb=rowY(4);
-  [0,6,12,18,24].forEach(h=>{
-    const x=X0+h/MAXH*(X1-X0);
-    el(s,'line',{x1:x,y1:yb+4,x2:x,y2:yb+9,stroke:'rgba(88,64,46,.24)','stroke-width':1.08,
-      class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x,y:yb+20,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},h+'h');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'EACH CREST = ONE PIPELINE · DEEPER GREEN = FASTER · DOT = THE PEAK');
-});
-})();
-
-// ════ L20 · parallel coordinates ════
-// 多维记录对比的 Lupi 皮:一条发丝线 = 一个产品走过四根轴。
-// 线束的形状就是数据结构(哪两维相关、谁全线领先)。
-// 全线最优的上墨标名,其余发丝退后。轴顶名、轴脚 min/max。
-(()=>{
-const DIMS=[['PRICE $',8,30],['CSAT',6,9.6],['RETENTION %',55,95],['GROWTH %',-5,40]];
-const P=[['Editor',12,9.1,91,22],['Boards',18,8.4,86,18],['Forms',9,8.8,78,31],
-  ['Docs',15,8.0,82,12],['Chat',7,7.2,64,8],['Vault',24,7.8,88,6],
-  ['Flows',21,8.6,90,38],['Views',11,7.5,71,14],['Sync',16,6.9,58,-2],
-  ['Pages',8,8.2,74,19],['Grid',19,7.1,62,4],['Hub',13,6.6,52,9]];
-obsReveal('parallel',s=>{
-  const AX=[64,156,248,340],TOP=64,BOT=252;
-  const mapY=(di,v)=>{const [,lo,hi]=DIMS[di];return BOT-(v-lo)/(hi-lo)*(BOT-TOP)};
-  // hero: judged on csat+retention+growth (price is a fact, not a virtue)
-  const score=p=>(p[2]-6)/3.6+(p[3]-55)/40+(p[4]+5)/45;
-  const hero=P.reduce((a,b)=>score(b)>score(a)?b:a);
-  DIMS.forEach(([name,lo,hi],di)=>{
-    el(s,'line',{x1:AX[di],y1:TOP-8,x2:AX[di],y2:BOT+8,stroke:'#ACAD79','stroke-width':1.8,
-      class:'fade',style:`animation-delay:${di*.06}s`});
-    [[TOP-8],[BOT+8]].forEach(([yy])=>
-      el(s,'line',{x1:AX[di]-4,y1:yy,x2:AX[di]+4,y2:yy,stroke:'#ACAD79','stroke-width':1.8,
-        class:'fade',style:`animation-delay:${di*.06}s`}));
-    txt(s,{x:AX[di],y:TOP-22,'font-size':7,'font-weight':800,fill:'rgba(88,64,46,.72)','text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${di*.06}s`},name);
-    txt(s,{x:AX[di]+8,y:TOP-5,'font-size':6.5,'font-weight':600,fill:'rgba(88,64,46,.32)',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},hi);
-    txt(s,{x:AX[di]+8,y:BOT+11,'font-size':6.5,'font-weight':600,fill:'rgba(88,64,46,.32)',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},lo);
-  });
-  P.forEach((p,i)=>{
-    const isHero=p===hero;
-    const ys=[mapY(0,p[1]),mapY(1,p[2]),mapY(2,p[3]),mapY(3,p[4])];
-    let d=`M${AX[0]} ${ys[0].toFixed(1)}`;
-    for(let k=0;k<3;k++){
-      const mx=(AX[k]+AX[k+1])/2;
-      d+=` C${mx} ${ys[k].toFixed(1)} ${mx} ${ys[k+1].toFixed(1)} ${AX[k+1]} ${ys[k+1].toFixed(1)}`;
-    }
-    const ln=el(s,'path',{d,fill:'none',
-      stroke:isHero?'#D4A017':'#929960','stroke-width':isHero?2:.65,
-      opacity:isHero?1:.5+rnd(i+1,5)*.3,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.05}s;animation-duration:.8s`});
-    tip(ln,`${p[0]} — $${p[1]} · CSAT ${p[2]} · ${p[3]}% retained · ${p[4]}% growth`);
-    ys.forEach((y,di)=>el(s,'circle',{cx:AX[di],cy:y,r:isHero?3:1.4,
-      fill:isHero?INK:'#929960',opacity:isHero?1:.7,
-      class:'pop',style:`animation-delay:${.3+i*.05+di*.04}s`}));
-    if(isHero)
-      txt(s,{x:AX[3]+12,y:ys[3]+3,'font-size':9,'font-weight':800,fill:INK,
-        style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-        class:'fade'},p[0]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#ACAD79','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE HAIRLINE = ONE PRODUCT · INK = BEST ACROSS THE BOARD');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 1257
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/lupi-porcelain.html

@@ -1,1257 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Lupi L1–L20 × PORCELAIN</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F7F2EB;--dark:#081F5C;--ink:#081F5C;--muted:rgba(8,31,92,.60);--faint:rgba(8,31,92,.40);--grid:rgba(8,31,92,.16);--lab:rgba(8,31,92,.72);--dmid:rgba(247,242,235,.62);--dq:rgba(247,242,235,.24)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:#F7F2EB}
-  .card.dark .sub{color:var(--dmid)}
-  .card.dark .src{color:var(--dq)}
-  .card.wide{grid-column:1/-1}
-  .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .note{font-size:11px;color:var(--lab);line-height:1.65;margin-bottom:16px}
-  .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:var(--ink);color:var(--bg)}
-  .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}
-  .badge.pair{border:1px dashed var(--faint);color:var(--muted)}
-  .card.dark .badge.del{background:var(--bg);color:var(--ink)}
-  .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}
-  .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-    <h1>Lupi 系 L1–L20 × PORCELAIN</h1>
-  <p><span class="chip" style="background:#334EAC"></span><span class="chip" style="background:#7096D1"></span><span class="chip" style="background:#BAD6EB"></span><span class="chip" style="background:#D0E3FF"></span><span class="chip" style="background:#081F5C"></span><span class="chip" style="background:#F7F2EB"></span> &nbsp;青瓷蓝家族 · 你要的「同色系深浅」正式答案:Dawn → Sky → China → Royal → Midnight 五档全是原厂色号,零补档。Moon 当底、Midnight 当文字兼主角。</p>
-  <p style="margin-top:7px">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 主题 / 4 小队 / 4 簇 / 5 卫星岛 / 每行选项)各走一个色,序数型(漏斗 5 级 / 票量 / 峰值 / 账户数 / 持有量 / 上线周次)走 5 档梯。几何、动画、数据与 lupi-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <!-- 1 · launch fan -->
-  <div class="card">
-    <h2>Twelve features, fanned out</h2>
-    <div class="sub">one spoke per feature · big dot = launch week, sized by MAU today · small dots = every week since</div>
-    <svg id="fan" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">LAUNCH FAN · PORCELAIN · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- 4 · dot cascade on dark -->
-  <div class="card dark">
-    <h2>What breaks, stacked and ranked</h2>
-    <div class="sub">incidents by root cause · 2019 → 2026 · one dot = 2 incidents · sorted ascending</div>
-    <svg id="cascade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT CASCADE · PORCELAIN · INCIDENT LOG</div>
-  </div>
-
-  <!-- 2 · barcode lollipop, wide + editorial text column -->
-  <div class="card wide">
-    <div class="split">
-      <div>
-        <h2>Ninety days as a barcode</h2>
-        <div class="sub">peak concurrent users, daily · Apr – Jun 2026</div>
-        <div class="note">Every hairline is a day, whether or not anything
-        happened in it. The dot marks the day's peak; the stem below it is
-        just gravity. Read the field, not the numbers — the season has a
-        texture before it has a value.</div>
-        <div class="legend">
-          ● &nbsp;WEEKDAY PEAK<br>
-          ○ &nbsp;WEEKEND PEAK<br>
-          │ &nbsp;ONE CALENDAR DAY<br>
-          ◉ &nbsp;TOP-3, LABELED
-        </div>
-      </div>
-      <div>
-        <svg id="barcode" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-        <div class="src">BARCODE LOLLIPOP · PORCELAIN · LIVE OPS</div>
-      </div>
-    </div>
-  </div>
-
-  <!-- 3 · arc bubble matrix -->
-  <div class="card">
-    <h2>Eight products land in twelve cities</h2>
-    <div class="sub">bubble = paying accounts · each row bows like a horizon</div>
-    <svg id="arcmatrix" viewBox="0 0 430 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">ARC MATRIX · PORCELAIN · SALES GEO</div>
-  </div>
-
-  <!-- 5 · radial convergence -->
-  <div class="card">
-    <h2>48 requests pull toward five themes</h2>
-    <div class="sub">every rim node = a feature request · lines bundle into the theme that claimed it</div>
-    <svg id="converge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL CONVERGENCE · PORCELAIN · FEEDBACK BOARD</div>
-  </div>
-
-  <!-- 6 · cluster field, wide -->
-  <div class="card wide">
-    <h2>The contributor field</h2>
-    <div class="sub">80 core contributors orbit the main repo · satellite islands keep their own gravity · dotted paths = cross-contribution</div>
-    <svg id="clusters" viewBox="0 0 840 320" preserveAspectRatio="xMidYMid meet" style="max-height:360px"></svg>
-    <div class="src">CLUSTER FIELD · PORCELAIN · CONTRIBUTOR GRAPH</div>
-  </div>
-
-
-
-  <!-- 1 · brand spectrum -->
-  <div class="card">
-    <h2>Where the brand sits</h2>
-    <div class="sub">panel of 120 customers placed us between opposing traits · big dot = us · small dots = competitors</div>
-    <svg id="spectrum" viewBox="0 0 460 300" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BRAND SPECTRUM · PORCELAIN · BRAND SURVEY</div>
-  </div>
-
-  <!-- 2 · dotty matrix -->
-  <div class="card">
-    <h2>Four squads, stacked in space</h2>
-    <div class="sub">each plane = one squad · 6 × 6 weeks of activity · dot area = tasks closed</div>
-    <svg id="dotty" viewBox="0 0 400 372" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-    <div class="src">DOTTY MATRIX · PORCELAIN · SPRINT BOARD</div>
-  </div>
-
-  <!-- 3 · ticket almanac, wide -->
-  <div class="card wide">
-    <h2>Eight years of tickets, one almanac</h2>
-    <div class="sub">bubble area = support tickets per product area, per year · bubbles overlap where years pile up · dark core = escalations · dashed ring = still in beta</div>
-    <svg id="almanac" viewBox="0 0 840 400" preserveAspectRatio="xMidYMid meet" style="max-height:420px"></svg>
-    <div class="src">BUBBLE ALMANAC · PORCELAIN · SUPPORT DESK</div>
-  </div>
-
-  <!-- 4 · deploy patchwork -->
-  <div class="card">
-    <h2>A quarter of deploys, overlaid</h2>
-    <div class="sub">each wedge = one deploy · angle = time of day · reach = files touched · ink builds where deploys pile up</div>
-    <svg id="patchwork" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL PATCHWORK · PORCELAIN · DEPLOY LOG</div>
-  </div>
-
-  <!-- 6 · trend lineage -->
-  <div class="card">
-    <h2>Features rise, fall, come back</h2>
-    <div class="sub">● shipped · ○ reworked · dashed = dormant years · line runs on if it's still alive today</div>
-    <svg id="lineage" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TREND LINEAGE · PORCELAIN · PRODUCT HISTORY</div>
-  </div>
-
-  <!-- 8 · type colonnade -->
-  <div class="card">
-    <h2>Forty-four repos, ten owners</h2>
-    <div class="sub">every hairline hands a repo to the team that owns it · the pile-ups are the org chart nobody drew</div>
-    <svg id="colonnade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TYPE COLONNADE · PORCELAIN · CODE OWNERSHIP</div>
-  </div>
-
-  <!-- 9 · funnel hourglass -->
-  <div class="card">
-    <h2>The funnel, poured</h2>
-    <div class="sub">each strip's width = people at that stage · threads trickle down · Q2 2026 cohort</div>
-    <svg id="hourglass" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HOURGLASS STREAM · PORCELAIN · GROWTH</div>
-  </div>
-
-  <!-- 14 · hundred field -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>A hundred of us, four minds</h2>
-    <div class="sub">one dot = one person in a hundred · four attitude clusters, sized by share · 2026 survey</div>
-    <svg id="hundredfield" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HUNDRED FIELD · PORCELAIN · ATTITUDE SURVEY</div>
-  </div>
-
-  <!-- 15 · ballot tally -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>What they fear, tick by tick</h2>
-    <div class="sub">one tick = one respondent in a hundred · inked = picked it · they could pick several</div>
-    <svg id="ballottally" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BALLOT TALLY · PORCELAIN · WORKPLACE SURVEY</div>
-  </div>
-
-
-  <!-- L16 · matrix heat -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 矩阵热力</span>
-    <h2>Which features get used together</h2>
-    <div class="sub">8 × 8 co-usage · shade = share of accounts using both · dashed square = strongest pair</div>
-    <svg id="matheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT · PORCELAIN · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- L17 · calendar heat -->
-  <div class="card wide">
-    <span class="badge">LUPI 编辑型 · 日历热力</span>
-    <h2>A year of deploys, day by day</h2>
-    <div class="sub">52 weeks × 7 days · dot area = deploys that day · tiny dot = a quiet day · dashed ring = the busiest day of 2025</div>
-    <svg id="calheat" viewBox="0 0 840 232" preserveAspectRatio="xMidYMid meet" style="max-height:260px"></svg>
-    <div class="src">CALENDAR HEAT · PORCELAIN · DEPLOY LOG</div>
-  </div>
-
-  <!-- L18 · beeswarm -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 蜂群</span>
-    <h2>A hundred and twenty deals, swarming</h2>
-    <div class="sub">one dot = one closed deal · the swarm piles up where deals cluster · hollow = enterprise · dashed = median</div>
-    <svg id="beeswarm" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BEESWARM · PORCELAIN · SALES OPS</div>
-  </div>
-
-  <!-- L19 · ridgeline -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · RIDGELINE</span>
-    <h2>Five pipelines, five tempos</h2>
-    <div class="sub">each crest = one pipeline's completion-time shape · the area is made of hairlines · dot = the peak</div>
-    <svg id="ridge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RIDGELINE · PORCELAIN · DATA PLATFORM</div>
-  </div>
-
-  <!-- L20 · parallel coordinates -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 平行坐标</span>
-    <h2>Twelve products, four dimensions</h2>
-    <div class="sub">one hairline = one product across price · satisfaction · retention · growth · ink = the one that wins everywhere</div>
-    <svg id="parallel" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PARALLEL COORDS · PORCELAIN · PRODUCT PANEL</div>
-  </div>
-
-</div>
-<script>
-// ── PORCELAIN 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F7F2EB',TXT='#081F5C',MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',FAINT='rgba(8,31,92,.40)';
-const FLOOR='rgba(8,31,92,.24)',QUIET='rgba(8,31,92,.15)',TRACK='rgba(8,31,92,.12)',GRID='rgba(8,31,92,.16)';
-const DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C';
-const FAINTDATA='#BAD6EB',BEAD='#7096D1',HALO='rgba(247,242,235,.92)';
-const HERODK='#7096D1';   // 深色卡上的强调色(HERO 在深底上可能读不出)
-const DMID='rgba(247,242,235,.62)',DQ='rgba(247,242,235,.24)';   // 深色卡(L4 dot cascade)上的浅墨 / 静音档
-const SER=['#334EAC','#081F5C','#7096D1','#BAD6EB','#D0E3FF','rgba(51,78,172,.4)'];   // 分类色:一图之内每个系列 / 每行 / 每簇一个色
-const RING='rgba(8,31,92,.30)';   // 散点的铅笔细边:让浅色号在纸上有形,不用改色号
-const RAMP=['#D0E3FF','#BAD6EB','#7096D1','#334EAC','#081F5C'];   // 5 档序数梯(浅→重):漏斗 / 票量 / 峰值 / 持有量
-const RAMPDK=['rgba(247,242,235,.26)','rgba(247,242,235,.42)','rgba(247,242,235,.60)','rgba(247,242,235,.84)','#7096D1'];   // 深色卡上的 5 档梯,末档落在强调色
-const HEROMODE=false; // 主角模式:launch fan 的头号功能是否也整体走 HERO
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const sect=(cx,cy,r0,r1,a0,a1)=>{
-  const big=a1-a0>180?1:0;
-  const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
-  const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
-  return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
-};
-// unified reveal: draw when scrolled into view; click replays
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ 1 · launch fan ════
-// spokes sorted by launch week → the big dots cascade along a diagonal arc
-(()=>{
-const FEAT=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync','Grid','Views','Hub','Forms'];
-const LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19];     // week of launch
-const CX=40,CY=304,rOf=w=>56+w*9.7;
-obsReveal('fan',s=>{
-  const ang=i=>-84+i*(76/11);
-  // dotted week guides
-  [5,10,15,20].forEach((w,gi)=>{
-    const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);
-    el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',
-      stroke:QUIET,'stroke-width':1,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${gi*.1}s`});
-    const [lx,ly]=pol(CX,CY,r,-87.5);
-    txt(s,{x:lx,y:ly,'font-size':6.5,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${gi*.1}s`},'W'+w);
-  });
-  FEAT.forEach((name,i)=>{
-    const a=ang(i),L=LAUNCH[i];
-    const mau=Math.round(48-i*3+rnd(i+1,3)*10);
-    const col=RAMP[4-Math.min(4,Math.floor((L-1)/4))];   // 上线越早 = 越重
-    // hairline spoke from launch to rim
-    const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);
-    el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:QUIET,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`});
-    // weekly texture dots
-    for(let w=L+1;w<=20;w++){
-      const [x,y]=pol(CX,CY,rOf(w),a);
-      el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:col,opacity:.55,
-        class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});
-    }
-    // launch dot, sized by MAU
-    const [bx,by]=pol(CX,CY,rOf(L),a);
-    const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:(HEROMODE&&i===0)?HERO:col,
-      class:'pop',style:`animation-delay:${.15+i*.07}s`});
-    tip(big,`${name} — launched W${L} · ${mau}k MAU`);
-    // rim label along the spoke
-    const [tx,ty]=pol(CX,CY,rOf(20)+9,a);
-    txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:LAB,
-      transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',
-      class:'fade',style:`animation-delay:${.3+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 4 · dot cascade on dark ════
-(()=>{
-const CAUSE=['DNS','CDN','QUOTA','DISK','CERT','CACHE','QUEUE','LOCK','OOM','NET','3P API','CONFIG','DB','DEPLOY','CODE','HUMAN'];
-const COUNT=[1,1,2,3,3,4,5,6,7,9,11,13,15,18,22,27];
-obsReveal('cascade',s=>{
-  const x=i=>34+i*22.2, base=i=>272-i*7.5;
-  const worst=COUNT.indexOf(Math.max(...COUNT));   // 主角:最大根因
-  // dotted diagonal baseline
-  el(s,'line',{x1:x(0)-8,y1:base(0)+4,x2:x(15)+8,y2:base(15)+4,
-    stroke:DQ,'stroke-width':1,'stroke-dasharray':'2 4',class:'fade'});
-  CAUSE.forEach((name,i)=>{
-    const n=Math.ceil(COUNT[i]/2),bx=x(i),by=base(i);
-    const bd=Math.min(4,Math.floor(COUNT[i]/6));   // 事故量分 5 档
-    for(let k=0;k<n-1;k++)
-      el(s,'circle',{cx:bx,cy:by-12-k*8.8,r:2.2,fill:RAMPDK[bd],opacity:.95,
-        class:'pop',style:`animation-delay:${i*.05+k*.03}s`});
-    const topY=by-12-(n-1)*8.8;
-    const top=el(s,'circle',{cx:bx,cy:topY,r:i===worst?5.6:4.6,
-      fill:i===worst?HERODK:RAMPDK[Math.min(4,bd+1)],
-      class:'pop',style:`animation-delay:${i*.05+n*.03}s`});
-    tip(top,`${name} — ${COUNT[i]} incidents`);
-    txt(s,{x:bx,y:topY-10,'font-size':9,'font-weight':700,fill:i===worst?HERODK:BG,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.2+i*.05}s`},COUNT[i]);
-    txt(s,{x:bx,y:by+12,'font-size':6.5,'font-weight':600,fill:DMID,'text-anchor':'end',
-      transform:`rotate(-90 ${bx} ${by+12})`,
-      class:'fade',style:`animation-delay:${.1+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 2 · barcode lollipop ════
-(()=>{
-const N=90;
-const val=d=>95+55*Math.sin(d/9.5)+30*Math.sin(d/3.7)+rnd(d+1,5)*40;
-obsReveal('barcode',s=>{
-  // the barcode: one hairline per day, full height
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53;
-    el(s,'line',{x1:x,y1:8,x2:x,y2:258,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${d*.004}s`});
-  }
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  // top-3 peaks, kept at least 6 days apart so labels never collide
-  const top3=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);
-    if(top3.length===3)break;
-  }
-  const vlo=Math.min(...vs),vhi=Math.max(...vs);
-  // 峰值分 4 档,最浅那档不用(5 月的低谷也得看得见)
-  const vband=v=>1+Math.min(3,Math.floor((v-vlo)/(vhi-vlo+1e-9)*4));
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53,v=vs[d],y=252-v*.75;
-    const weekend=d%7===5||d%7===6,col=RAMP[vband(v)];
-    const stemEnd=Math.min(256,y+14+rnd(d+1,9)*26);
-    el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:col,'stroke-width':1.5,
-      class:'fade',style:`animation-delay:${.3+d*.008}s`});
-    const big=top3.includes(d);
-    const dot=el(s,'circle',{cx:x,cy:y,r:big?4.6:2.7,
-      fill:weekend?BG:(big?HERO:col),stroke:big?HERO:col,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.3+d*.008}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)}k peak`);
-    if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.9+d*.004}s`},Math.round(v)+'k');
-  }
-  [[0,'APR'],[30,'MAY'],[61,'JUN']].forEach(([d,m])=>
-    txt(s,{x:18+d*8.53,y:276,'font-size':8,'font-weight':600,fill:MUT,
-      'letter-spacing':'.12em',class:'fade'},m));
-});
-})();
-
-// ════ 3 · arc bubble matrix ════
-(()=>{
-const PROD=['Editor','Boards','Docs','Flows','Chat','Vault','Pages','Sync'];
-const CITY=['SF','NYC','LON','BER','TOK','SYD','SIN','PAR','AMS','TOR','SEO','SAO'];
-const W=[9,8,7,7,6,5,5,4,4,3,3,2];             // market weight
-obsReveal('arcmatrix',s=>{
-  const rowY=i=>74+i*29, colX=j=>92+j*27, dy=j=>-16*Math.sin(Math.PI*j/11);
-  const v=(i,j)=>{
-    if(rnd(i*13+1,j*7+3)<.09)return 0;                 // a few true absences
-    const age=1-i*.085;                                // older rows reach further
-    return Math.min(40,Math.round(W[j]*3.4*age*(.45+rnd(i+1,j+1)*.85)));
-  };
-  // find top-4 cells for labels
-  const all=[];
-  PROD.forEach((_,i)=>CITY.forEach((_,j)=>all.push([v(i,j),i,j])));
-  const top=all.sort((a,b)=>b[0]-a[0]).slice(0,4).map(d=>d[1]*100+d[2]);
-  PROD.forEach((p,i)=>{
-    // the horizon arc
-    const d='M'+CITY.map((_,j)=>`${colX(j)} ${rowY(i)+dy(j)}`).join(' L ');
-    el(s,'path',{d,fill:'none',stroke:QUIET,'stroke-width':1,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.08}s`});
-    txt(s,{x:84,y:rowY(i)+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.08}s`},p);
-    CITY.forEach((c,j)=>{
-      const x=colX(j),y=rowY(i)+dy(j),vv=v(i,j);
-      if(!vv){el(s,'circle',{cx:x,cy:y,r:.9,fill:FLOOR,
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});return}
-      const fill=RAMP[Math.min(4,Math.floor(vv/8))];   // 账户数 5 档
-      const dot=el(s,'circle',{cx:x,cy:y,r:Math.sqrt(vv)*1.3,fill,
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});
-      tip(dot,`${p} · ${c} — ${vv} accounts`);
-      if(top.includes(i*100+j))
-        txt(s,{x,y:y-Math.sqrt(vv)*1.3-4,'font-size':7,'font-weight':800,fill:TXT,
-          'text-anchor':'middle',class:'fade',style:`animation-delay:${.6}s`},vv);
-    });
-  });
-  CITY.forEach((c,j)=>{
-    const x=colX(j),y=74+dy(j)-24;
-    txt(s,{x,y,'font-size':7,'font-weight':700,fill:MUT,'letter-spacing':'.08em',
-      transform:`rotate(-55 ${x} ${y})`,class:'fade',style:`animation-delay:${j*.03}s`},c);
-  });
-});
-})();
-
-// ════ 5 · radial convergence ════
-(()=>{
-const N=48,CX=200,CY=162,R=116;
-const THEME=[['PERF',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['UX',6,213]];
-obsReveal('converge',s=>{
-  // hub assignment: contiguous blocks with a little organic leakage
-  const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
-  const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];
-  const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
-  const counts=[0,0,0,0,0];
-  for(let i=0;i<N;i++)counts[hubOf(i)]++;
-  for(let i=0;i<N;i++){
-    const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
-    const h=hubOf(i),[hx,hy]=hubPt(h);
-    const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
-    const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
-    el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
-      fill:'none',stroke:SER[h],'stroke-width':.9,opacity:.62,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.018}s`});
-    el(s,'circle',{cx:px,cy:py,r:1.6,fill:SER[h],
-      class:'pop',style:`animation-delay:${i*.018}s`});
-    // tiny rim code, flipped on the left half so it stays readable
-    const flip=deg>90&&deg<270,[lx,ly]=pol(CX,CY,R+7,deg);
-    txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUT,
-      'text-anchor':flip?'end':'start','dominant-baseline':'middle',
-      transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,
-      class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));
-  }
-  THEME.forEach(([name,,deg],h)=>{
-    const [hx,hy]=hubPt(h);
-    const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,fill:counts[h]===Math.max(...counts)?HERO:SER[h],
-      class:'pop',style:`animation-delay:${.5+h*.08}s`});
-    tip(hub,`${name} — ${counts[h]} requests`);
-    // theme label parked outside the rim, past the code ring
-    const [tx,ty]=pol(CX,CY,R+34,deg);
-    txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'dominant-baseline':'middle','letter-spacing':'.08em',
-      style:`animation-delay:${.6+h*.08}s`,
-      class:'fade'},`${name} · ${counts[h]}`);
-    // hairline tying the label to its hub, skimming past the rim
-    const [gx,gy]=pol(CX,CY,R+22,deg);
-    el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:TRACK,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
-  });
-});
-})();
-
-// ════ 6 · cluster field ════
-(()=>{
-const SAT=[[622,78,'PLUGINS',22],[706,214,'THEMES',16],[540,268,'DOCS',13],[92,72,'FORKS',12],[774,58,'MIRRORS',9]];
-obsReveal('clusters',s=>{
-  const HX=300,HY=172;
-  // dotted cross-contribution paths, hub → each island
-  SAT.forEach(([x,y],ci)=>{
-    const mx=(HX+x)/2,my=(HY+y)/2-30;
-    el(s,'path',{d:`M${HX} ${HY} Q${mx} ${my} ${x} ${y}`,fill:'none',
-      stroke:GRID,'stroke-width':.9,'stroke-dasharray':'2 4',pathLength:1,
-      class:'draw',style:`animation-delay:${.8+ci*.12}s;animation-duration:.7s`});
-  });
-  // main hub: 80 golden-angle spokes
-  for(let i=0;i<80;i++){
-    const a=i*137.508,len=28+rnd(i+1,7)*88;
-    const [x,y]=pol(HX,HY,len,a);
-    el(s,'line',{x1:HX,y1:HY,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.012}s;animation-duration:.5s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:1.6+rnd(i+1,4)*1.8,fill:SER[0],opacity:.9,
-      class:'pop',style:`animation-delay:${.15+i*.012}s`});
-    tip(dot,`core contributor #${i+1}`);
-  }
-  el(s,'circle',{cx:HX,cy:HY,r:7,fill:HERO,class:'pop'});
-  txt(s,{x:HX,y:HY+140,'font-size':8,'font-weight':800,fill:HERO,'text-anchor':'middle',
-    'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,
-    class:'fade'},'CORE · 80');
-  // satellite islands
-  SAT.forEach(([x,y,name,n],ci)=>{
-    const col=RAMP[1+Math.round((n-9)/13*3)];   // 岛越大越重(22→4 档,9→1 档)
-    el(s,'circle',{cx:x,cy:y,r:3.5,fill:col,
-      class:'pop',style:`animation-delay:${.9+ci*.12}s`});
-    for(let k=0;k<n;k++){
-      const a=k*137.508+ci*40,rr=9+rnd(k+1,ci+2)*26;
-      const [dx,dy]=pol(x,y,rr,a);
-      if(k%2===0)el(s,'line',{x1:x,y1:y,x2:dx,y2:dy,stroke:TRACK,'stroke-width':.5,
-        class:'fade',style:`animation-delay:${1+ci*.12+k*.02}s`});
-      el(s,'circle',{cx:dx,cy:dy,r:1.4+rnd(k+3,ci+5)*1.4,fill:col,
-        stroke:RING,'stroke-width':.4,
-        class:'pop',style:`animation-delay:${1+ci*.12+k*.02}s`});
-    }
-    txt(s,{x,y:y+44,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.12em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,
-      class:'fade'},`${name} · ${n}`);
-  });
-});
-})();
-
-// ════ 1 · brand spectrum ════
-// paired opposites; the pale ribbon threads our position down the page
-(()=>{
-const ROWS=[['FRIEND','AUTHORITY'],['SERIOUS','PLAYFUL'],['RELIABLE','RISK-TAKING'],['CONTEMPORARY','CLASSIC']];
-const US=[.74,.80,.40,.22];                 // 0 = left trait, 1 = right trait
-const COMP=[[.42,.52,.57],[.60,.65,.86],[.30,.34,.50],[.36,.41,.73]];
-const X0=118,X1=342,Y=i=>58+i*60,px=t=>X0+t*(X1-X0);
-obsReveal('spectrum',s=>{
-  // the ribbon first, underneath everything
-  let d=`M${px(US[0])} ${Y(0)}`;
-  for(let i=1;i<4;i++)
-    d+=` C${px(US[i-1])} ${Y(i-1)+30} ${px(US[i])} ${Y(i)-30} ${px(US[i])} ${Y(i)}`;
-  el(s,'path',{d,fill:'none',stroke:HALO,'stroke-width':30,'stroke-linecap':'round',
-    'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',
-    style:'animation-duration:1.4s'});
-  ROWS.forEach(([a,b],i)=>{
-    const y=Y(i);
-    el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:FAINT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    [X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:FAINT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`}));
-    txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);
-    txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);
-    COMP[i].forEach((t,k)=>{
-      const c=el(s,'circle',{cx:px(t),cy:y,r:3.2,fill:SER[(i+1)%4],
-        stroke:RING,'stroke-width':.6,
-        class:'pop',style:`animation-delay:${.3+i*.1+k*.05}s`});
-      tip(c,`competitor ${'ABC'[k]} — ${Math.round(t*100)}% toward ${b}`);
-    });
-    const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:HERO,
-      class:'pop',style:`animation-delay:${.5+i*.1}s`});
-    tip(us,`us — ${Math.round(US[i]*100)}% toward ${b}`);
-  });
-  txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:FAINT,'letter-spacing':'.12em',
-    class:'fade',style:'animation-delay:.9s'},'● US    ● COMPETITORS A · B · C');
-});
-})();
-
-// ════ 2 · dotty matrix — isometric stacked dot planes ════
-// pitch 20/10, dot radius capped well under half-pitch, decks 64px apart:
-// each plane reads as its own quiet grid instead of a smudge
-(()=>{
-const SQUAD=['PLATFORM','GROWTH','MOBILE','INFRA'];
-const SHADE=[SER[3],SER[2],SER[1],SER[0]];   // 底层→顶层,各队一色
-const P=(c,r,k)=>[196+(c-r)*20,236+(c+r)*10-k*64];
-obsReveal('dotty',s=>{
-  for(let k=0;k<4;k++){
-    // plane slab: paper fill so upper decks occlude lower ones
-    const cs=[P(-.8,-.8,k),P(5.8,-.8,k),P(5.8,5.8,k),P(-.8,5.8,k)];
-    el(s,'path',{d:'M'+cs.map(p=>p.join(' ')).join(' L ')+' Z',
-      fill:BG,'fill-opacity':.96,stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${k*.16}s`});
-    for(let r=0;r<6;r++)for(let c=0;c<6;c++){
-      const load=rnd(k*37+r*6+c+1,k+2);
-      const v=load<.3?0:Math.round(load*12);   // sparser: silence is part of the texture
-      const [x,y]=P(c,r,k);
-      if(!v){el(s,'circle',{cx:x,cy:y,r:.7,fill:FLOOR,
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});continue}
-      const dot=el(s,'circle',{cx:x,cy:y,r:1+Math.sqrt(v)*1.15,fill:SHADE[k],
-        stroke:RING,'stroke-width':.45,
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});
-      tip(dot,`${SQUAD[k]} · week ${c+1} × lane ${r+1} — ${v} tasks`);
-    }
-    // label off the plane's right corner, tied by a short hairline
-    const [cxr,cyr]=P(5.8,-.8,k);
-    el(s,'line',{x1:cxr+4,y1:cyr,x2:cxr+22,y2:cyr,stroke:FAINT,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.2+k*.16}s`});
-    txt(s,{x:cxr+27,y:cyr+2.5,'font-size':7.5,'font-weight':700,fill:k===3?TXT:MUT,
-      'letter-spacing':'.12em',class:'fade',style:`animation-delay:${.2+k*.16}s`},SQUAD[k]);
-  }
-});
-})();
-
-// ════ 3 · bubble almanac ════
-// the vintage-almanac feel lives in three things: ledger-paper hairlines
-// under everything, bubbles big enough to collide, and marginalia. do all three.
-(()=>{
-const AREAS=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','GRID','VIEWS','HUB','FORMS'];
-const BETA={GRID:2023,VIEWS:2024,HUB:2025,FORMS:2025};   // beta until this year
-const BORN={EDITOR:2019,BOARDS:2019,DOCS:2019,CHAT:2020,FLOWS:2020,VAULT:2021,PAGES:2021,SYNC:2022,GRID:2022,VIEWS:2023,HUB:2024,FORMS:2024};
-obsReveal('almanac',s=>{
-  const rowY=i=>62+i*34,colX=j=>132+j*57;
-  // ledger paper: hairlines every 6.8px, edge to edge, under everything
-  for(let y=40;y<=318;y+=6.8)
-    el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:QUIET,'stroke-width':.5,
-      class:'fade',style:`animation-delay:${(y-40)*.001}s`});
-  const vals=[];
-  for(let i=0;i<8;i++){
-    const yr=2019+i;
-    el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${i*.04}s`});
-    txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.04}s`},yr);
-    AREAS.forEach((a,j)=>{
-      if(yr<BORN[a])return;
-      const age=yr-BORN[a];
-      // steep spread: quiet areas stay pinpricks, busy ones balloon
-      const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));
-      vals.push([Math.max(1,v),i,j,a,yr]);
-    });
-  }
-  // draw small over large so the giants sit underneath as washes
-  vals.sort((a,b)=>b[0]-a[0]);
-  const top3=vals.slice(0,3);
-  // hand-drawn circle: radius wobbles around the rim on two slow sine waves,
-  // so every bubble is its own slightly-off print — never a compass circle
-  const blob=(x,y,r,seed)=>{
-    const n=Math.max(14,Math.round(r*1.6)),pts=[];
-    for(let t=0;t<n;t++){
-      const a=t/n*Math.PI*2;
-      const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;
-      pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);
-    }
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<n;t++){
-      const p=pts[t],q=pts[(t+1)%n];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+' Z';
-  };
-  vals.forEach(([v,i,j,a,yr],k)=>{
-    const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10,  // jitter off the gridline
-          y=rowY(i),r=Math.sqrt(v)*3.8;        // big: rows are 34 apart, giants reach ~28 and collide
-    const beta=BETA[a]&&yr<=BETA[a];
-    const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});
-    const seed=i*12+j,isTop=top3.includes(vals[k]);   // 主角:票量 top-3
-    const col=RAMP[Math.min(4,Math.floor(v/12))];        // 票量 5 档
-    const dot=el(g,'path',beta
-      ?{d:blob(x,y,r,seed),fill:'none',stroke:DATA2,'stroke-width':1.2,'stroke-dasharray':'3 3'}
-      :{d:blob(x,y,r,seed),fill:col,'fill-opacity':.16+rnd(i+2,j+4)*.14,
-        stroke:isTop?HERO:col,'stroke-opacity':isTop?.95:.5+rnd(i+5,j+7)*.38,
-        'stroke-width':isTop?1.7:.8+rnd(i+3,j+11)*1});
-    tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);
-    // escalation core: small wobbly blot, nudged off-center like a press mark
-    if(!beta){
-      const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;
-      el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:isTop?HERO:TXT});
-    }
-    if(isTop)
-      txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:HERO,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},v*10);
-  });
-  AREAS.forEach((a,j)=>{
-    const x=colX(j);
-    txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUT,'letter-spacing':'.1em',
-      transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);
-  });
-  // marginalia: two annotations with pointer hairlines, like the original's notes
-  el(s,'path',{d:`M${colX(3)+30} ${rowY(5)-14} C${colX(3)+70} ${rowY(5)-34} ${colX(3)+96} ${rowY(5)-38} ${colX(3)+118} ${rowY(5)-38}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:colX(3)+124,y:rowY(5)-35,'font-size':7,fill:LAB,'font-style':'italic',
-    class:'fade',style:'animation-delay:1.1s'},'chat tickets triple after mobile GA');
-  el(s,'path',{d:`M${colX(9)+16} ${rowY(2)+8} C${colX(9)+40} ${rowY(2)+26} ${colX(9)+60} ${rowY(2)+30} ${colX(9)+80} ${rowY(2)+30}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7,fill:LAB,'font-style':'italic',
-    class:'fade',style:'animation-delay:1.2s'},'betas barely ticket — nobody files bugs on toys');
-  // footer strip: important events, the almanac's timeline shelf
-  const evY=356;
-  el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:FAINT,'stroke-width':.8,
-    class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:56,y:evY-24,'font-size':7,'font-weight':800,fill:MUT,'letter-spacing':'.16em',
-    class:'fade',style:'animation-delay:1.2s'},'IMPORTANT EVENTS');
-  [['2020','SLA introduced'],['2022','self-serve help center'],['2023','the GA wave'],['2025','AI deflection live']]
-  .forEach(([yr,note],k)=>{
-    const x=90+k*196;
-    el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:MUT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`});
-    txt(s,{x,y:evY+4,'font-size':8,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);
-    txt(s,{x,y:evY+15,'font-size':7,fill:MUT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);
-  });
-});
-})();
-
-// ════ 4 · radial patchwork ════
-(()=>{
-const CX=200,CY=168,N=46;
-obsReveal('patchwork',s=>{
-  // 24h rim ticks
-  for(let h=0;h<96;h++){
-    const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:FAINT,'stroke-width':h%4===0?1:.5,
-      class:'fade',style:`animation-delay:${h*.006}s`});
-  }
-  [0,6,12,18].forEach(h=>{
-    const [x,y]=pol(CX,CY,157,-90+h*15);
-    txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      class:'fade'},String(h).padStart(2,'0'));
-  });
-  // the patchwork: translucent wedges pile into ink
-  const wedges=[];
-  for(let i=0;i<N;i++){
-    // deploys cluster around 11h and 16h, thin overnight
-    const peak=rnd(i+1,2)>.5?11:16;
-    const hour=(peak+(rnd(i+1,3)-.5)*7+24)%24;
-    const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;
-    const r1=34+rnd(i+1,5)*100;
-    wedges.push([a0,sw,r1,hour,i]);
-  }
-  wedges.forEach(([a0,sw,r1,hour,i])=>{
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:RAMP[Math.min(4,Math.floor(hour/4.8))],
-      'fill-opacity':.17+rnd(i+1,6)*.15,class:'fade',
-      style:`animation-delay:${.2+i*.022}s`});
-    tip(w,`deploy #${i+1} — ${String(Math.floor(hour)).padStart(2,'0')}:${String(Math.floor(hour%1*60)).padStart(2,'0')} · ${Math.round(r1*6)} files`);
-  });
-  // three outlined wedges: the ones that paged somebody
-  [4,17,31].forEach((i,k)=>{
-    const [a0,sw,r1]=wedges[i];
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',
-      stroke:HERO,'stroke-width':1.5,class:'fade',style:`animation-delay:${1.2+k*.15}s`});
-    tip(w,`deploy #${i+1} — triggered an incident`);
-  });
-  el(s,'circle',{cx:CX,cy:CY,r:3,fill:DATA,class:'pop',style:'animation-delay:1.3s'});
-  txt(s,{x:CX,y:308,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.4s'},'OUTLINED = TRIGGERED AN INCIDENT');
-});
-})();
-
-// ════ 6 · trend lineage ════
-(()=>{
-// [name, events [year, type f|o], aliveToday]
-const TR=[
-  ['Dark mode',[[2017,'f'],[2022,'o']],true],
-  ['Kanban',[[2016,'f']],true],
-  ['Wikis',[[2016,'f'],[2019,'o']],false],
-  ['Slash cmds',[[2018,'f']],true],
-  ['Templates',[[2019,'f'],[2024,'o']],true],
-  ['Inbox',[[2020,'f']],false],
-  ['Live cursors',[[2020,'f'],[2023,'o']],true],
-  ['AI drafts',[[2023,'f'],[2025,'o']],true],
-  ['Voice notes',[[2021,'f']],false],
-  ['Offline',[[2018,'f'],[2026,'o']],true],
-];
-const Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;
-// 主角:最近一次有动作的功能线 —— 标题里的 "come back"
-const HC=TR.map((r,i)=>[r[1][r[1].length-1][0],i]).sort((a,b)=>b[0]-a[0])[0][1];
-obsReveal('lineage',s=>{
-  for(let yr=Y0;yr<=2026;yr++){
-    el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:QUIET,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
-    if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
-  }
-  TR.forEach(([name,ev,alive],c)=>{
-    const x=colX(c),hero=c===HC;
-    const lastYr=ev[ev.length-1][0];
-    // 越近期动过 = 越重;已死的走最浅一档(虚线本身已经在说休眠)
-    const col=hero?HERO:RAMP[alive?Math.min(4,Math.max(2,Math.round((lastYr-2016)/10*4))):1];
-    // segments between events: solid while fresh, dashed once dormant
-    for(let k=0;k<ev.length-1;k++){
-      const gap=ev[k+1][0]-ev[k][0];
-      el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
-        stroke:col,'stroke-width':hero?1.8:1.4,
-        'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
-        class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
-    }
-    const last=ev[ev.length-1][0];
-    if(alive)
-      el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:col,'stroke-width':hero?1.8:1.4,
-        pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
-    ev.forEach(([yr,t])=>{
-      const node=el(s,'circle',t==='f'
-        ?{cx:x,cy:yY(yr),r:5.5,fill:col}
-        :{cx:x,cy:yY(yr),r:5.5,fill:BG,stroke:col,'stroke-width':1.6});
-      node.setAttribute('class','pop');
-      node.style.animationDelay=(.35+c*.07)+'s';
-      tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);
-    });
-    // terminal dot: alive lands on today's baseline, dead stops where it stopped
-    el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
-      fill:col,class:'pop',style:`animation-delay:${.6+c*.07}s`});
-    const ly=yY(2026)+26;
-    txt(s,{x,y:ly,'font-size':6.8,'font-weight':hero?800:600,fill:hero?HERO:(alive?LAB:FAINT),
-      transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',
-      class:'fade',style:`${hero?`paint-order:stroke;stroke:${BG};stroke-width:3px;`:''}animation-delay:${.7+c*.05}s`},name);
-  });
-});
-})();
-
-// ════ 8 · type colonnade ════
-(()=>{
-const PRE=['core','ui','api','data','auth','sync','mail','flag','edge','doc','bot'];
-const SUF=['-kit','-svc','-web','-cli','-db','-gw','-sdk','-jobs'];
-const TEAM=['PLATFORM','FRONTEND','BACKEND','DATA','INFRA','MOBILE','SECURITY','GROWTH','ML','DESIGN'];
-obsReveal('colonnade',s=>{
-  const N=44;
-  const teamY=j=>36+j*28,repoY=i=>16+i*6.7;
-  const counts=Array(10).fill(0);
-  const owner=i=>{const w=rnd(i+1,17);return Math.min(9,Math.floor(w*w*10))}; // skewed: low teams hoard
-  for(let i=0;i<N;i++)counts[owner(i)]++;
-  for(let i=0;i<N;i++){
-    const name=PRE[i%11]+SUF[Math.floor(rnd(i+1,8)*8)%8]+(i>21?'-v2':'');
-    const yi=repoY(i),j=owner(i),yj=teamY(j);
-    const col=RAMP[Math.min(4,Math.floor(counts[j]/3))];   // 团队持有量 5 档
-    txt(s,{x:112,y:yi+2,'font-size':5,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.012}s`},name);
-    el(s,'rect',{x:117,y:yi-1.2,width:4,height:2.4,fill:col,
-      class:'fade',style:`animation-delay:${i*.012}s`});
-    el(s,'path',{d:`M123 ${yi} C 200 ${yi} 220 ${yj} 288 ${yj}`,fill:'none',
-      stroke:col,'stroke-width':.8,opacity:.78,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.015}s;animation-duration:.7s`});
-  }
-  TEAM.forEach((t,j)=>{
-    const y=teamY(j);
-    el(s,'circle',{cx:293,cy:y,r:2.6+counts[j]*.55,fill:RAMP[Math.min(4,Math.floor(counts[j]/3))],
-      class:'pop',style:`animation-delay:${.6+j*.05}s`});
-    const lab=txt(s,{x:302+counts[j]*.55,y:y+2.5,'font-size':7,'font-weight':700,fill:LAB,
-      class:'fade',style:`animation-delay:${.65+j*.05}s`},`${t} · ${counts[j]}`);
-    tip(lab,`${t} owns ${counts[j]} repos`);
-  });
-});
-})();
-
-// ════ 9 · hourglass stream ════
-(()=>{
-const STAGE=[['VISITORS',4200],['SIGN-UPS',1900],['ACTIVATED',960],['RETAINED',540],['PAYING',310]];
-obsReveal('hourglass',s=>{
-  const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;
-  // 主角:通过率最低的一级 —— 漏得最狠的地方
-  const PCT=STAGE.slice(1).map((n,k)=>Math.round(n[1]/STAGE[k][1]*100));
-  const leak=PCT.indexOf(Math.min(...PCT));
-  STAGE.forEach(([name,c],k)=>{
-    const y=sy(k),hw=w(c)/2;
-    // barcode strip: one tick ≈ 40 people
-    const n=Math.round(c/40);
-    for(let t=0;t<n;t++){
-      const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;
-      el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:RAMP[k],'stroke-width':1.1,
-        opacity:.6+rnd(t+2,k+5)*.4,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
-    }
-    // threads trickling to the next strip
-    if(k<4){
-      const hw1=w(STAGE[k+1][1])/2;
-      for(let t=0;t<34;t++){
-        const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
-        const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
-        el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,
-          fill:'none',stroke:FAINTDATA,'stroke-width':.7,opacity:.45,pathLength:1,
-          class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
-      }
-      // conversion between stages, parked in the left margin
-      const pct=Math.round(STAGE[k+1][1]/c*100);
-      txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':k===leak?10.5:8.5,'font-weight':800,
-        fill:k===leak?HERO:MUT,
-        class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
-      txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6,'font-weight':600,fill:k===leak?HERO:FAINT,
-        'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'GET THROUGH');
-    }
-    // stage label, tied to the strip edge
-    el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.3+k*.12}s`});
-    txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:LAB,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
-    txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());
-  });
-});
-})();
-
-// ════ 14 · hundred field ════
-// small data, unit-decomposed: four shares of 100% become 100 people.
-// phyllotaxis discs (golden angle) so每簇自带肌理; share is countable, not just readable.
-(()=>{
-const SEG=[['CHARGED',41,SER[0]],['TORN',35,SER[1]],['ADRIFT',12,SER[2]],['AVERSE',12,SER[3]]];
-const POS=[[132,140],[276,116],[186,252],[322,238]];
-obsReveal('hundredfield',s=>{
-  // dashed constellation hairlines tying the cluster cores
-  [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:GRID,'stroke-width':.7,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      // hairline spoke for every 5th person, core to dot
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,
-        class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,
-        stroke:RING,'stroke-width':.4,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} — one of ${v} in 100`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:TXT,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},'ONE DOT = ONE PERSON · 41 + 35 + 12 + 12 = 100');
-});
-})();
-
-// ════ 15 · ballot tally ════
-// multi-select survey (each option independent, 0–100): every option is a row of
-// 100 hairline ticks — a hundred people standing in line, the ones who picked it inked.
-(()=>{
-const ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];
-obsReveal('ballottally',s=>{
-  const VS=ASK.map(a=>a[1]),topI=VS.indexOf(Math.max(...VS));   // 主角:得票最高项
-  ASK.forEach(([name,v],i)=>{
-    const base=78+i*62;
-    txt(s,{x:28,y:base-26,'font-size':7.5,'font-weight':700,fill:LAB,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=28+k*3.44,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?(i===topI?HERO:SER[i]):FLOOR,'stroke-width':picked?1.3:.6,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-11,'font-size':i===topI?12.5:11,'font-weight':800,
-      fill:i===topI?HERO:TXT,
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`${v} of 100 picked this — they could pick several`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
-});
-})();
-
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#081F5C',PAPER='#F7F2EB',MUTED='rgba(8,31,92,.60)',GRID='rgba(8,31,92,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ L16 · matrix heat ════
-// 通用矩阵热力:任意 分类×分类×连续值。明度即数据——五档灰阶方块,
-// 越黑越高。对角线不参与(自己×自己),零格留一粒小点(沉默可见)。
-// 最强的一对上虚线框 + 标数。区别于 F10 dot heat:这里编码用明度不用面积,
-// 值是占比/强度而不是可数件数。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC'];
-const v=(i,j)=>{
-  if(i===j)return -1;
-  const a=Math.min(i,j),b=Math.max(i,j);
-  const pop=(8-a)/8*(8-b)/8;                       // 头部功能重合度高
-  const raw=pop*62*(0.35+rnd(a*8+b+1,a+b+3)*.9);
-  return Math.round(raw);
-};
-const shade=t=>t>36?INK:t>24?'#334EAC':t>14?'#7096D1':t>6?'#BAD6EB':'#D0E3FF';
-obsReveal('matheat',s=>{
-  const X0=118,Y0=70,P=26,C=22;
-  let max=0,mi=0,mj=0;
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  FEAT.forEach((f,i)=>{
-    txt(s,{x:X0-10,y:Y0+i*P+C/2+3,'font-size':7,'font-weight':700,fill:'rgba(8,31,92,.72)','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.04}s`},f);
-    const cy=Y0-10;
-    txt(s,{x:X0+i*P+C/2,y:cy,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.06em',
-      transform:`rotate(-55 ${X0+i*P+C/2} ${cy})`,class:'fade',style:`animation-delay:${i*.04}s`},f);
-  });
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){
-    const x=X0+j*P,y=Y0+i*P,t=v(i,j),d=(i+j)*.02;
-    if(t<0){el(s,'rect',{x:x+C/2-3,y:y+C/2-.6,width:6,height:1.2,fill:'rgba(8,31,92,.15)',
-      class:'fade',style:`animation-delay:${d}s`});continue}
-    if(!t){el(s,'circle',{cx:x+C/2,cy:y+C/2,r:.9,fill:'rgba(8,31,92,.15)',
-      class:'pop',style:`animation-delay:${d}s`});continue}
-    const cell=el(s,'rect',{x,y,width:C,height:C,rx:4,fill:shade(t),
-      class:'pop',style:`animation-delay:${d}s`});
-    tip(cell,`${FEAT[i]} × ${FEAT[j]} — ${t}% of accounts use both`);
-    if(i===mi&&j===mj){
-      el(s,'rect',{x:x-3.5,y:y-3.5,width:C+7,height:C+7,rx:6,fill:'none',
-        stroke:INK,'stroke-width':1.8,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:.9s'});
-      txt(s,{x:x+C/2,y:y+C/2+3.5,'font-size':9,'font-weight':800,fill:PAPER,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},t);
-    }
-  }
-  // shade legend, countable buckets
-  const LG=[['#D0E3FF','1–6'],['#BAD6EB','7–14'],['#7096D1','15–24'],['#334EAC','25–36'],[INK,'37+']];
-  LG.forEach(([c,lab],k)=>{
-    const x=118+k*54;
-    el(s,'rect',{x,y:292,width:9,height:9,rx:2,fill:c,class:'fade',style:`animation-delay:${1+k*.05}s`});
-    txt(s,{x:x+13,y:300,'font-size':6.5,'font-weight':600,fill:'#7096D1',
-      class:'fade',style:`animation-delay:${1+k*.05}s`},lab+'%');
-  });
-  txt(s,{x:36,y:300,'font-size':7,'font-weight':600,fill:'#BAD6EB','letter-spacing':'.1em',
-    class:'fade',style:'animation-delay:1s'},'SHADE = %');
-});
-})();
-
-// ════ L17 · calendar heat ════
-// 日历热力:GitHub 贡献墙的 Lupi 皮。一格一天,52 周 × 7 天,
-// 点面积 = 当天量(sqrt 换算),静默日一粒小点。月份写在上檐,
-// 峰值日虚线圈 + 旁注引线。逐记录世界观:一年 365 天每天都在场。
-(()=>{
-const MON=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
-const DAY=['MON','WED','FRI','SUN'];
-const val=(w,d)=>{
-  const wd=d<5;
-  const season=1+.55*Math.sin((w-8)/9);            // 春秋两个发布季
-  if(!wd)return rnd(w*7+d+1,3)>.82?1:0;            // 周末基本静默
-  const raw=season*(2.5+rnd(w*7+d+1,d+2)*9)*(rnd(w+1,d+5)>.12?1:0);
-  return Math.round(raw);
-};
-obsReveal('calheat',s=>{
-  const X0=56,Y0=42,P=14.8;
-  let max=0,mw=0,md=0;
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){const t=val(w,d);if(t>max){max=t;mw=w;md=d}}
-  DAY.forEach((lab,k)=>{
-    txt(s,{x:X0-10,y:Y0+[0,2,4,6][k]*P+3,'font-size':6.5,'font-weight':700,fill:'#7096D1','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${k*.04}s`},lab);
-  });
-  MON.forEach((m,k)=>{
-    const x=X0+Math.round(k*52/12)*P;
-    txt(s,{x,y:Y0-16,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',
-      class:'fade',style:`animation-delay:${k*.03}s`},m);
-    el(s,'line',{x1:x,y1:Y0-11,x2:x,y2:Y0-5,stroke:'rgba(8,31,92,.32)','stroke-width':1.26,
-      class:'fade',style:`animation-delay:${k*.03}s`});
-  });
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){
-    const x=X0+w*P,y=Y0+d*P,t=val(w,d),delay=w*.012+d*.004;
-    if(!t){el(s,'circle',{cx:x,cy:y,r:.75,fill:'rgba(8,31,92,.15)',
-      class:'pop',style:`animation-delay:${delay}s`});continue}
-    const r=1.1+Math.sqrt(t)*1.55;
-    const dot=el(s,'circle',{cx:x,cy:y,r,
-      fill:t>max*.66?INK:t>max*.33?'rgba(8,31,92,.72)':'#BAD6EB',
-      class:'pop',style:`animation-delay:${delay}s`});
-    tip(dot,`W${w+1} ${['MON','TUE','WED','THU','FRI','SAT','SUN'][d]} — ${t} deploys`);
-  }
-  // peak: dashed ring + marginalia hairline
-  const px=X0+mw*P,py=Y0+md*P,pr=1.1+Math.sqrt(max)*1.55;
-  el(s,'circle',{cx:px,cy:py,r:pr+3.6,fill:'none',stroke:INK,'stroke-width':1.8,
-    'stroke-dasharray':'2 3',class:'fade',style:'animation-delay:1s'});
-  el(s,'path',{d:`M${px+pr+6} ${py+6} C${px+34} ${py+30} ${px+52} ${py+36} ${px+72} ${py+36}`,
-    fill:'none',stroke:'#BAD6EB','stroke-width':1.26,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:px+78,y:py+39,'font-size':7,fill:'rgba(8,31,92,.72)','font-style':'italic',
-    class:'fade',style:'animation-delay:1.15s'},`the release-week spike — ${max} deploys in a day`);
-  txt(s,{x:420,y:222,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DAY · DOT AREA = DEPLOYS · TINY DOT = A QUIET DAY');
-});
-})();
-
-// ════ L18 · beeswarm ════
-// 蜂群:violin 的 Lupi 姊妹。不聚合密度,把 120 条真实记录沿数值轴
-// 逐颗排开,撞上了就往两边让——堆得高的地方就是分布的峰。
-// 空心 = enterprise 单,虚线 = 中位数。每颗点背后是一条真实成交。
-(()=>{
-const N=120;
-const deals=Array.from({length:N},(_,i)=>{
-  const u=rnd(i+1,3),ent=rnd(i+2,11)>.86;
-  const v=Math.round(4+(ent?60:6)+150*Math.pow(u,2.6)+rnd(i+3,7)*10);
-  return [Math.min(178,v),ent];
-});
-obsReveal('beeswarm',s=>{
-  const X0=36,X1=376,BASE=246,R=4,GAP=R*2+.8;
-  const mapX=v=>X0+v/180*(X1-X0);
-  // axis rail + barcode ticks
-  el(s,'line',{x1:X0-4,y1:BASE,x2:X1+4,y2:BASE,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let g=0;g<=9;g++){
-    const x=X0+g/9*(X1-X0);
-    el(s,'line',{x1:x,y1:BASE,x2:x,y2:BASE+(g%3===0?7:4),stroke:'rgba(8,31,92,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${g*.02}s`});
-    if(g%3===0)txt(s,{x,y:BASE+18,'font-size':7,'font-weight':600,fill:'rgba(8,31,92,.32)','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${g*.02}s`},'$'+Math.round(g/9*180)+'k');
-  }
-  // swarm as clean stacks: x snaps to a GAP-wide lane, dots pile upward —
-  // the pile height IS the density. no half-overlaps, every dot countable.
-  const lanes={};
-  const sorted=deals.map((d,i)=>[d[0],d[1],i]).sort((a,b)=>a[0]-b[0]);
-  sorted.forEach(([v,ent,i])=>{
-    const lane=Math.round((mapX(v)-X0)/GAP);
-    const x=X0+lane*GAP;
-    const row=lanes[lane]||0;lanes[lane]=row+1;
-    const y=BASE-R-1-row*GAP;
-    const dot=ent
-      ?el(s,'circle',{cx:x,cy:y,r:R,fill:PAPER,stroke:INK,'stroke-width':1.98,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`})
-      :el(s,'circle',{cx:x,cy:y,r:R,fill:INK,opacity:.82,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`});
-    tip(dot,`deal #${i+1} — $${v}k${ent?' · enterprise':''}`);
-  });
-  // median
-  const vs=deals.map(d=>d[0]).sort((a,b)=>a-b);
-  const med=vs[Math.floor(N/2)];
-  const mx=mapX(med);
-  el(s,'line',{x1:mx,y1:52,x2:mx,y2:BASE+8,stroke:'#7096D1','stroke-width':1.62,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx,y:44,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1.1s`,
-    class:'fade'},'MEDIAN $'+med+'k');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DEAL · THE PILE IS THE DISTRIBUTION · HOLLOW = ENTERPRISE');
-});
-})();
-
-// ════ L19 · ridgeline ════
-// 多组分布的 Lupi 皮:每行一条完成时长的密度山脊,面积由竖发丝组成
-// (F3 hairline area 的语法),行与行轻微重叠,后排画在前、前排糊上
-// 纸色 slab 遮挡(dotty matrix 的手法)。峰值点标中心时长。
-(()=>{
-const ROWS=[['MOBILE',3.2,1.3],['DESKTOP',5.1,1.9],['API',7.4,2.3],['IMPORTS',10.8,3.2],['BATCH',15.2,4]];
-obsReveal('ridge',s=>{
-  const X0=92,X1=372,MAXH=24,N=72,LIFT=54;
-  const rowY=i=>96+i*44;
-  ROWS.forEach(([name,c,sp],i)=>{
-    const y0=rowY(i);
-    const dens=Array.from({length:N},(_,k)=>{
-      const h=k/(N-1)*MAXH;
-      return Math.exp(-((h-c)**2)/(2*sp*sp))+.26*Math.exp(-((h-c*2.1)**2)/(2*(sp*1.7)**2));
-    });
-    const dmax=Math.max(...dens);
-    const pts=dens.map((d,k)=>[X0+k/(N-1)*(X1-X0),y0-d/dmax*LIFT]);
-    const crest='M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ');
-    el(s,'path',{d:crest+` L${X1} ${y0} L${X0} ${y0} Z`,fill:PAPER,'fill-opacity':.96,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    pts.forEach(([px,py],k)=>{
-      if(k%2)return;
-      el(s,'line',{x1:px,y1:y0,x2:px,y2:py,stroke:'#7096D1','stroke-width':0.9,
-        opacity:.28+rnd(k+1,i+3)*.32,class:'fade',style:`animation-delay:${i*.1+k*.004}s`});
-    });
-    el(s,'path',{d:crest,fill:'none',stroke:INK,'stroke-width':1.62,pathLength:1,
-      class:'draw',style:`animation-delay:${.1+i*.1}s;animation-duration:.8s`});
-    el(s,'line',{x1:X0,y1:y0,x2:X1,y2:y0,stroke:GRID,'stroke-width':1.26,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    const pk=dens.indexOf(dmax),[px,py]=pts[pk];
-    const dot=el(s,'circle',{cx:px,cy:py,r:2.6,fill:INK,class:'pop',style:`animation-delay:${.6+i*.1}s`});
-    tip(dot,`${name} — most runs finish around ${c.toFixed(1)}h`);
-    txt(s,{x:px+7,y:py+1,'font-size':8,'font-weight':800,fill:INK,
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.7+i*.1}s`,
-      class:'fade'},c.toFixed(1)+'h');
-    txt(s,{x:X0-10,y:y0-3,'font-size':7.5,'font-weight':700,fill:'rgba(8,31,92,.72)','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-  });
-  const yb=rowY(4);
-  [0,6,12,18,24].forEach(h=>{
-    const x=X0+h/MAXH*(X1-X0);
-    el(s,'line',{x1:x,y1:yb+4,x2:x,y2:yb+9,stroke:'rgba(8,31,92,.24)','stroke-width':1.08,
-      class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x,y:yb+20,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},h+'h');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'EACH CREST = ONE PIPELINE · THE AREA IS MADE OF HAIRLINES · DOT = THE PEAK');
-});
-})();
-
-// ════ L20 · parallel coordinates ════
-// 多维记录对比的 Lupi 皮:一条发丝线 = 一个产品走过四根轴。
-// 线束的形状就是数据结构(哪两维相关、谁全线领先)。
-// 全线最优的上墨标名,其余发丝退后。轴顶名、轴脚 min/max。
-(()=>{
-const DIMS=[['PRICE $',8,30],['CSAT',6,9.6],['RETENTION %',55,95],['GROWTH %',-5,40]];
-const P=[['Editor',12,9.1,91,22],['Boards',18,8.4,86,18],['Forms',9,8.8,78,31],
-  ['Docs',15,8.0,82,12],['Chat',7,7.2,64,8],['Vault',24,7.8,88,6],
-  ['Flows',21,8.6,90,38],['Views',11,7.5,71,14],['Sync',16,6.9,58,-2],
-  ['Pages',8,8.2,74,19],['Grid',19,7.1,62,4],['Hub',13,6.6,52,9]];
-obsReveal('parallel',s=>{
-  const AX=[64,156,248,340],TOP=64,BOT=252;
-  const mapY=(di,v)=>{const [,lo,hi]=DIMS[di];return BOT-(v-lo)/(hi-lo)*(BOT-TOP)};
-  // hero: judged on csat+retention+growth (price is a fact, not a virtue)
-  const score=p=>(p[2]-6)/3.6+(p[3]-55)/40+(p[4]+5)/45;
-  const hero=P.reduce((a,b)=>score(b)>score(a)?b:a);
-  DIMS.forEach(([name,lo,hi],di)=>{
-    el(s,'line',{x1:AX[di],y1:TOP-8,x2:AX[di],y2:BOT+8,stroke:'#BAD6EB','stroke-width':1.8,
-      class:'fade',style:`animation-delay:${di*.06}s`});
-    [[TOP-8],[BOT+8]].forEach(([yy])=>
-      el(s,'line',{x1:AX[di]-4,y1:yy,x2:AX[di]+4,y2:yy,stroke:'#BAD6EB','stroke-width':1.8,
-        class:'fade',style:`animation-delay:${di*.06}s`}));
-    txt(s,{x:AX[di],y:TOP-22,'font-size':7,'font-weight':800,fill:'rgba(8,31,92,.72)','text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${di*.06}s`},name);
-    txt(s,{x:AX[di]+8,y:TOP-5,'font-size':6.5,'font-weight':600,fill:'rgba(8,31,92,.32)',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},hi);
-    txt(s,{x:AX[di]+8,y:BOT+11,'font-size':6.5,'font-weight':600,fill:'rgba(8,31,92,.32)',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},lo);
-  });
-  P.forEach((p,i)=>{
-    const isHero=p===hero;
-    const ys=[mapY(0,p[1]),mapY(1,p[2]),mapY(2,p[3]),mapY(3,p[4])];
-    let d=`M${AX[0]} ${ys[0].toFixed(1)}`;
-    for(let k=0;k<3;k++){
-      const mx=(AX[k]+AX[k+1])/2;
-      d+=` C${mx} ${ys[k].toFixed(1)} ${mx} ${ys[k+1].toFixed(1)} ${AX[k+1]} ${ys[k+1].toFixed(1)}`;
-    }
-    const ln=el(s,'path',{d,fill:'none',
-      stroke:isHero?INK:'#7096D1','stroke-width':isHero?2:.65,
-      opacity:isHero?1:.5+rnd(i+1,5)*.3,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.05}s;animation-duration:.8s`});
-    tip(ln,`${p[0]} — $${p[1]} · CSAT ${p[2]} · ${p[3]}% retained · ${p[4]}% growth`);
-    ys.forEach((y,di)=>el(s,'circle',{cx:AX[di],cy:y,r:isHero?3:1.4,
-      fill:isHero?INK:'#7096D1',opacity:isHero?1:.7,
-      class:'pop',style:`animation-delay:${.3+i*.05+di*.04}s`}));
-    if(isHero)
-      txt(s,{x:AX[3]+12,y:ys[3]+3,'font-size':9,'font-weight':800,fill:INK,
-        style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-        class:'fade'},p[0]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#BAD6EB','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE HAIRLINE = ONE PRODUCT · INK = BEST ACROSS THE BOARD');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 1257
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/lupi-wire.html

@@ -1,1257 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Lupi L1–L20 × WIRE</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0F0EE;--dark:#1F1E1C;--ink:#1F1E1C;--muted:rgba(31,30,28,.60);--faint:rgba(31,30,28,.40);--grid:rgba(31,30,28,.16);--lab:rgba(31,30,28,.72);--dmid:rgba(240,240,238,.62);--dq:rgba(240,240,238,.24)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:#F0F0EE}
-  .card.dark .sub{color:var(--dmid)}
-  .card.dark .src{color:var(--dq)}
-  .card.wide{grid-column:1/-1}
-  .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .note{font-size:11px;color:var(--lab);line-height:1.65;margin-bottom:16px}
-  .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:var(--muted);margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .chip{display:inline-block;width:12px;height:12px;border-radius:99px;margin:0 3px -1px 0;box-shadow:inset 0 0 0 1px rgba(0,0,0,.08)}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:var(--ink);color:var(--bg)}
-  .badge.pick{border:1.5px solid var(--ink);color:var(--ink)}
-  .badge.pair{border:1px dashed var(--faint);color:var(--muted)}
-  .card.dark .badge.del{background:var(--bg);color:var(--ink)}
-  .card.dark .badge.pick{border-color:var(--bg);color:var(--bg)}
-  .card.dark .badge.pair{border-color:var(--dq);color:var(--dmid)}
-</style>
-</head>
-<body>
-<div class="pagehead">
-    <h1>Lupi 系 L1–L20 × WIRE</h1>
-  <p><span class="chip" style="background:#F5572F"></span><span class="chip" style="background:#22211F"></span><span class="chip" style="background:#8F8E86"></span><span class="chip" style="background:#C0BFB7"></span><span class="chip" style="background:#DBDAD3"></span> &nbsp;编辑部红 · 分类位全走黑灰,红只在两处出现:每张图的主角元素,以及序数梯爬完的最后一档(PLATFORM 的 14 个 repo / PAYING / 得票最高项)。红是稀客,这是它的身份。</p>
-  <p style="margin-top:7px">颜色在每张图里都编码一个维度,不是全图一个墨色:分类型(5 主题 / 4 小队 / 4 簇 / 5 卫星岛 / 每行选项)各走一个色,序数型(漏斗 5 级 / 票量 / 峰值 / 账户数 / 持有量 / 上线周次)走 5 档梯。几何、动画、数据与 lupi-gallery 逐节点一致;结构线仍是文字色的透明档,不引入新色相。滚入播动画,点击重播。</p>
-</div>
-<div class="grid2">
-
-  <!-- 1 · launch fan -->
-  <div class="card">
-    <h2>Twelve features, fanned out</h2>
-    <div class="sub">one spoke per feature · big dot = launch week, sized by MAU today · small dots = every week since</div>
-    <svg id="fan" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">LAUNCH FAN · WIRE · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- 4 · dot cascade on dark -->
-  <div class="card dark">
-    <h2>What breaks, stacked and ranked</h2>
-    <div class="sub">incidents by root cause · 2019 → 2026 · one dot = 2 incidents · sorted ascending</div>
-    <svg id="cascade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT CASCADE · WIRE · INCIDENT LOG</div>
-  </div>
-
-  <!-- 2 · barcode lollipop, wide + editorial text column -->
-  <div class="card wide">
-    <div class="split">
-      <div>
-        <h2>Ninety days as a barcode</h2>
-        <div class="sub">peak concurrent users, daily · Apr – Jun 2026</div>
-        <div class="note">Every hairline is a day, whether or not anything
-        happened in it. The dot marks the day's peak; the stem below it is
-        just gravity. Read the field, not the numbers — the season has a
-        texture before it has a value.</div>
-        <div class="legend">
-          ● &nbsp;WEEKDAY PEAK<br>
-          ○ &nbsp;WEEKEND PEAK<br>
-          │ &nbsp;ONE CALENDAR DAY<br>
-          ◉ &nbsp;TOP-3, LABELED
-        </div>
-      </div>
-      <div>
-        <svg id="barcode" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-        <div class="src">BARCODE LOLLIPOP · WIRE · LIVE OPS</div>
-      </div>
-    </div>
-  </div>
-
-  <!-- 3 · arc bubble matrix -->
-  <div class="card">
-    <h2>Eight products land in twelve cities</h2>
-    <div class="sub">bubble = paying accounts · each row bows like a horizon</div>
-    <svg id="arcmatrix" viewBox="0 0 430 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">ARC MATRIX · WIRE · SALES GEO</div>
-  </div>
-
-  <!-- 5 · radial convergence -->
-  <div class="card">
-    <h2>48 requests pull toward five themes</h2>
-    <div class="sub">every rim node = a feature request · lines bundle into the theme that claimed it</div>
-    <svg id="converge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL CONVERGENCE · WIRE · FEEDBACK BOARD</div>
-  </div>
-
-  <!-- 6 · cluster field, wide -->
-  <div class="card wide">
-    <h2>The contributor field</h2>
-    <div class="sub">80 core contributors orbit the main repo · satellite islands keep their own gravity · dotted paths = cross-contribution</div>
-    <svg id="clusters" viewBox="0 0 840 320" preserveAspectRatio="xMidYMid meet" style="max-height:360px"></svg>
-    <div class="src">CLUSTER FIELD · WIRE · CONTRIBUTOR GRAPH</div>
-  </div>
-
-
-
-  <!-- 1 · brand spectrum -->
-  <div class="card">
-    <h2>Where the brand sits</h2>
-    <div class="sub">panel of 120 customers placed us between opposing traits · big dot = us · small dots = competitors</div>
-    <svg id="spectrum" viewBox="0 0 460 300" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BRAND SPECTRUM · WIRE · BRAND SURVEY</div>
-  </div>
-
-  <!-- 2 · dotty matrix -->
-  <div class="card">
-    <h2>Four squads, stacked in space</h2>
-    <div class="sub">each plane = one squad · 6 × 6 weeks of activity · dot area = tasks closed</div>
-    <svg id="dotty" viewBox="0 0 400 372" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-    <div class="src">DOTTY MATRIX · WIRE · SPRINT BOARD</div>
-  </div>
-
-  <!-- 3 · ticket almanac, wide -->
-  <div class="card wide">
-    <h2>Eight years of tickets, one almanac</h2>
-    <div class="sub">bubble area = support tickets per product area, per year · bubbles overlap where years pile up · dark core = escalations · dashed ring = still in beta</div>
-    <svg id="almanac" viewBox="0 0 840 400" preserveAspectRatio="xMidYMid meet" style="max-height:420px"></svg>
-    <div class="src">BUBBLE ALMANAC · WIRE · SUPPORT DESK</div>
-  </div>
-
-  <!-- 4 · deploy patchwork -->
-  <div class="card">
-    <h2>A quarter of deploys, overlaid</h2>
-    <div class="sub">each wedge = one deploy · angle = time of day · reach = files touched · ink builds where deploys pile up</div>
-    <svg id="patchwork" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL PATCHWORK · WIRE · DEPLOY LOG</div>
-  </div>
-
-  <!-- 6 · trend lineage -->
-  <div class="card">
-    <h2>Features rise, fall, come back</h2>
-    <div class="sub">● shipped · ○ reworked · dashed = dormant years · line runs on if it's still alive today</div>
-    <svg id="lineage" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TREND LINEAGE · WIRE · PRODUCT HISTORY</div>
-  </div>
-
-  <!-- 8 · type colonnade -->
-  <div class="card">
-    <h2>Forty-four repos, ten owners</h2>
-    <div class="sub">every hairline hands a repo to the team that owns it · the pile-ups are the org chart nobody drew</div>
-    <svg id="colonnade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TYPE COLONNADE · WIRE · CODE OWNERSHIP</div>
-  </div>
-
-  <!-- 9 · funnel hourglass -->
-  <div class="card">
-    <h2>The funnel, poured</h2>
-    <div class="sub">each strip's width = people at that stage · threads trickle down · Q2 2026 cohort</div>
-    <svg id="hourglass" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HOURGLASS STREAM · WIRE · GROWTH</div>
-  </div>
-
-  <!-- 14 · hundred field -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>A hundred of us, four minds</h2>
-    <div class="sub">one dot = one person in a hundred · four attitude clusters, sized by share · 2026 survey</div>
-    <svg id="hundredfield" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HUNDRED FIELD · WIRE · ATTITUDE SURVEY</div>
-  </div>
-
-  <!-- 15 · ballot tally -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>What they fear, tick by tick</h2>
-    <div class="sub">one tick = one respondent in a hundred · inked = picked it · they could pick several</div>
-    <svg id="ballottally" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BALLOT TALLY · WIRE · WORKPLACE SURVEY</div>
-  </div>
-
-
-  <!-- L16 · matrix heat -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 矩阵热力</span>
-    <h2>Which features get used together</h2>
-    <div class="sub">8 × 8 co-usage · shade = share of accounts using both · dashed square = strongest pair</div>
-    <svg id="matheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT · WIRE · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- L17 · calendar heat -->
-  <div class="card wide">
-    <span class="badge">LUPI 编辑型 · 日历热力</span>
-    <h2>A year of deploys, day by day</h2>
-    <div class="sub">52 weeks × 7 days · dot area = deploys that day · tiny dot = a quiet day · dashed ring = the busiest day of 2025</div>
-    <svg id="calheat" viewBox="0 0 840 232" preserveAspectRatio="xMidYMid meet" style="max-height:260px"></svg>
-    <div class="src">CALENDAR HEAT · WIRE · DEPLOY LOG</div>
-  </div>
-
-  <!-- L18 · beeswarm -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 蜂群</span>
-    <h2>A hundred and twenty deals, swarming</h2>
-    <div class="sub">one dot = one closed deal · the swarm piles up where deals cluster · hollow = enterprise · dashed = median</div>
-    <svg id="beeswarm" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BEESWARM · WIRE · SALES OPS</div>
-  </div>
-
-  <!-- L19 · ridgeline -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · RIDGELINE</span>
-    <h2>Five pipelines, five tempos</h2>
-    <div class="sub">each crest = one pipeline's completion-time shape · the area is made of hairlines · dot = the peak</div>
-    <svg id="ridge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RIDGELINE · WIRE · DATA PLATFORM</div>
-  </div>
-
-  <!-- L20 · parallel coordinates -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 平行坐标</span>
-    <h2>Twelve products, four dimensions</h2>
-    <div class="sub">one hairline = one product across price · satisfaction · retention · growth · ink = the one that wins everywhere</div>
-    <svg id="parallel" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PARALLEL COORDS · WIRE · PRODUCT PANEL</div>
-  </div>
-
-</div>
-<script>
-// ── WIRE 角色 token(结构灰一律为文字色的透明档,不引入新色相)──
-const BG='#F0F0EE',TXT='#1F1E1C',MUT='rgba(31,30,28,.60)',LAB='rgba(31,30,28,.72)',FAINT='rgba(31,30,28,.40)';
-const FLOOR='rgba(31,30,28,.24)',QUIET='rgba(31,30,28,.15)',TRACK='rgba(31,30,28,.12)',GRID='rgba(31,30,28,.16)';
-const DATA='#22211F',DATA2='#8F8E86',HERO='#F5572F';
-const FAINTDATA='#C0BFB7',BEAD='#8F8E86',HALO='rgba(255,255,255,.92)';
-const HERODK='#F5572F';   // 深色卡上的强调色(HERO 在深底上可能读不出)
-const DMID='rgba(240,240,238,.62)',DQ='rgba(240,240,238,.24)';   // 深色卡(L4 dot cascade)上的浅墨 / 静音档
-const SER=['#22211F','#8F8E86','#C0BFB7','rgba(34,33,31,.52)','#DBDAD3','rgba(34,33,31,.3)'];   // 分类色:一图之内每个系列 / 每行 / 每簇一个色
-const RING='rgba(31,30,28,.30)';   // 散点的铅笔细边:让浅色号在纸上有形,不用改色号
-const RAMP=['#DBDAD3','#C0BFB7','#8F8E86','#22211F','#F5572F'];   // 5 档序数梯(浅→重):漏斗 / 票量 / 峰值 / 持有量
-const RAMPDK=['rgba(240,240,238,.26)','rgba(240,240,238,.42)','rgba(240,240,238,.60)','rgba(240,240,238,.84)','#F5572F'];   // 深色卡上的 5 档梯,末档落在强调色
-const HEROMODE=true; // 主角模式:launch fan 的头号功能是否也整体走 HERO
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const sect=(cx,cy,r0,r1,a0,a1)=>{
-  const big=a1-a0>180?1:0;
-  const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
-  const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
-  return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
-};
-// unified reveal: draw when scrolled into view; click replays
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ 1 · launch fan ════
-// spokes sorted by launch week → the big dots cascade along a diagonal arc
-(()=>{
-const FEAT=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync','Grid','Views','Hub','Forms'];
-const LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19];     // week of launch
-const CX=40,CY=304,rOf=w=>56+w*9.7;
-obsReveal('fan',s=>{
-  const ang=i=>-84+i*(76/11);
-  // dotted week guides
-  [5,10,15,20].forEach((w,gi)=>{
-    const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);
-    el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',
-      stroke:QUIET,'stroke-width':1,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${gi*.1}s`});
-    const [lx,ly]=pol(CX,CY,r,-87.5);
-    txt(s,{x:lx,y:ly,'font-size':6.5,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${gi*.1}s`},'W'+w);
-  });
-  FEAT.forEach((name,i)=>{
-    const a=ang(i),L=LAUNCH[i];
-    const mau=Math.round(48-i*3+rnd(i+1,3)*10);
-    const col=RAMP[4-Math.min(4,Math.floor((L-1)/4))];   // 上线越早 = 越重
-    // hairline spoke from launch to rim
-    const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);
-    el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:QUIET,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`});
-    // weekly texture dots
-    for(let w=L+1;w<=20;w++){
-      const [x,y]=pol(CX,CY,rOf(w),a);
-      el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:col,opacity:.55,
-        class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});
-    }
-    // launch dot, sized by MAU
-    const [bx,by]=pol(CX,CY,rOf(L),a);
-    const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:(HEROMODE&&i===0)?HERO:col,
-      class:'pop',style:`animation-delay:${.15+i*.07}s`});
-    tip(big,`${name} — launched W${L} · ${mau}k MAU`);
-    // rim label along the spoke
-    const [tx,ty]=pol(CX,CY,rOf(20)+9,a);
-    txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:LAB,
-      transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',
-      class:'fade',style:`animation-delay:${.3+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 4 · dot cascade on dark ════
-(()=>{
-const CAUSE=['DNS','CDN','QUOTA','DISK','CERT','CACHE','QUEUE','LOCK','OOM','NET','3P API','CONFIG','DB','DEPLOY','CODE','HUMAN'];
-const COUNT=[1,1,2,3,3,4,5,6,7,9,11,13,15,18,22,27];
-obsReveal('cascade',s=>{
-  const x=i=>34+i*22.2, base=i=>272-i*7.5;
-  const worst=COUNT.indexOf(Math.max(...COUNT));   // 主角:最大根因
-  // dotted diagonal baseline
-  el(s,'line',{x1:x(0)-8,y1:base(0)+4,x2:x(15)+8,y2:base(15)+4,
-    stroke:DQ,'stroke-width':1,'stroke-dasharray':'2 4',class:'fade'});
-  CAUSE.forEach((name,i)=>{
-    const n=Math.ceil(COUNT[i]/2),bx=x(i),by=base(i);
-    const bd=Math.min(4,Math.floor(COUNT[i]/6));   // 事故量分 5 档
-    for(let k=0;k<n-1;k++)
-      el(s,'circle',{cx:bx,cy:by-12-k*8.8,r:2.2,fill:RAMPDK[bd],opacity:.95,
-        class:'pop',style:`animation-delay:${i*.05+k*.03}s`});
-    const topY=by-12-(n-1)*8.8;
-    const top=el(s,'circle',{cx:bx,cy:topY,r:i===worst?5.6:4.6,
-      fill:i===worst?HERODK:RAMPDK[Math.min(4,bd+1)],
-      class:'pop',style:`animation-delay:${i*.05+n*.03}s`});
-    tip(top,`${name} — ${COUNT[i]} incidents`);
-    txt(s,{x:bx,y:topY-10,'font-size':9,'font-weight':700,fill:i===worst?HERODK:BG,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.2+i*.05}s`},COUNT[i]);
-    txt(s,{x:bx,y:by+12,'font-size':6.5,'font-weight':600,fill:DMID,'text-anchor':'end',
-      transform:`rotate(-90 ${bx} ${by+12})`,
-      class:'fade',style:`animation-delay:${.1+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 2 · barcode lollipop ════
-(()=>{
-const N=90;
-const val=d=>95+55*Math.sin(d/9.5)+30*Math.sin(d/3.7)+rnd(d+1,5)*40;
-obsReveal('barcode',s=>{
-  // the barcode: one hairline per day, full height
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53;
-    el(s,'line',{x1:x,y1:8,x2:x,y2:258,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${d*.004}s`});
-  }
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  // top-3 peaks, kept at least 6 days apart so labels never collide
-  const top3=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);
-    if(top3.length===3)break;
-  }
-  const vlo=Math.min(...vs),vhi=Math.max(...vs);
-  // 峰值分 4 档,最浅那档不用(5 月的低谷也得看得见)
-  const vband=v=>1+Math.min(3,Math.floor((v-vlo)/(vhi-vlo+1e-9)*4));
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53,v=vs[d],y=252-v*.75;
-    const weekend=d%7===5||d%7===6,col=RAMP[vband(v)];
-    const stemEnd=Math.min(256,y+14+rnd(d+1,9)*26);
-    el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:col,'stroke-width':1.5,
-      class:'fade',style:`animation-delay:${.3+d*.008}s`});
-    const big=top3.includes(d);
-    const dot=el(s,'circle',{cx:x,cy:y,r:big?4.6:2.7,
-      fill:weekend?BG:(big?HERO:col),stroke:big?HERO:col,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.3+d*.008}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)}k peak`);
-    if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.9+d*.004}s`},Math.round(v)+'k');
-  }
-  [[0,'APR'],[30,'MAY'],[61,'JUN']].forEach(([d,m])=>
-    txt(s,{x:18+d*8.53,y:276,'font-size':8,'font-weight':600,fill:MUT,
-      'letter-spacing':'.12em',class:'fade'},m));
-});
-})();
-
-// ════ 3 · arc bubble matrix ════
-(()=>{
-const PROD=['Editor','Boards','Docs','Flows','Chat','Vault','Pages','Sync'];
-const CITY=['SF','NYC','LON','BER','TOK','SYD','SIN','PAR','AMS','TOR','SEO','SAO'];
-const W=[9,8,7,7,6,5,5,4,4,3,3,2];             // market weight
-obsReveal('arcmatrix',s=>{
-  const rowY=i=>74+i*29, colX=j=>92+j*27, dy=j=>-16*Math.sin(Math.PI*j/11);
-  const v=(i,j)=>{
-    if(rnd(i*13+1,j*7+3)<.09)return 0;                 // a few true absences
-    const age=1-i*.085;                                // older rows reach further
-    return Math.min(40,Math.round(W[j]*3.4*age*(.45+rnd(i+1,j+1)*.85)));
-  };
-  // find top-4 cells for labels
-  const all=[];
-  PROD.forEach((_,i)=>CITY.forEach((_,j)=>all.push([v(i,j),i,j])));
-  const top=all.sort((a,b)=>b[0]-a[0]).slice(0,4).map(d=>d[1]*100+d[2]);
-  PROD.forEach((p,i)=>{
-    // the horizon arc
-    const d='M'+CITY.map((_,j)=>`${colX(j)} ${rowY(i)+dy(j)}`).join(' L ');
-    el(s,'path',{d,fill:'none',stroke:QUIET,'stroke-width':1,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.08}s`});
-    txt(s,{x:84,y:rowY(i)+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.08}s`},p);
-    CITY.forEach((c,j)=>{
-      const x=colX(j),y=rowY(i)+dy(j),vv=v(i,j);
-      if(!vv){el(s,'circle',{cx:x,cy:y,r:.9,fill:FLOOR,
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});return}
-      const fill=RAMP[Math.min(4,Math.floor(vv/8))];   // 账户数 5 档
-      const dot=el(s,'circle',{cx:x,cy:y,r:Math.sqrt(vv)*1.3,fill,
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});
-      tip(dot,`${p} · ${c} — ${vv} accounts`);
-      if(top.includes(i*100+j))
-        txt(s,{x,y:y-Math.sqrt(vv)*1.3-4,'font-size':7,'font-weight':800,fill:TXT,
-          'text-anchor':'middle',class:'fade',style:`animation-delay:${.6}s`},vv);
-    });
-  });
-  CITY.forEach((c,j)=>{
-    const x=colX(j),y=74+dy(j)-24;
-    txt(s,{x,y,'font-size':7,'font-weight':700,fill:MUT,'letter-spacing':'.08em',
-      transform:`rotate(-55 ${x} ${y})`,class:'fade',style:`animation-delay:${j*.03}s`},c);
-  });
-});
-})();
-
-// ════ 5 · radial convergence ════
-(()=>{
-const N=48,CX=200,CY=162,R=116;
-const THEME=[['PERF',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['UX',6,213]];
-obsReveal('converge',s=>{
-  // hub assignment: contiguous blocks with a little organic leakage
-  const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
-  const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];
-  const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
-  const counts=[0,0,0,0,0];
-  for(let i=0;i<N;i++)counts[hubOf(i)]++;
-  for(let i=0;i<N;i++){
-    const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
-    const h=hubOf(i),[hx,hy]=hubPt(h);
-    const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
-    const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
-    el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
-      fill:'none',stroke:SER[h],'stroke-width':.9,opacity:.62,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.018}s`});
-    el(s,'circle',{cx:px,cy:py,r:1.6,fill:SER[h],
-      class:'pop',style:`animation-delay:${i*.018}s`});
-    // tiny rim code, flipped on the left half so it stays readable
-    const flip=deg>90&&deg<270,[lx,ly]=pol(CX,CY,R+7,deg);
-    txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUT,
-      'text-anchor':flip?'end':'start','dominant-baseline':'middle',
-      transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,
-      class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));
-  }
-  THEME.forEach(([name,,deg],h)=>{
-    const [hx,hy]=hubPt(h);
-    const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,fill:counts[h]===Math.max(...counts)?HERO:SER[h],
-      class:'pop',style:`animation-delay:${.5+h*.08}s`});
-    tip(hub,`${name} — ${counts[h]} requests`);
-    // theme label parked outside the rim, past the code ring
-    const [tx,ty]=pol(CX,CY,R+34,deg);
-    txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'dominant-baseline':'middle','letter-spacing':'.08em',
-      style:`animation-delay:${.6+h*.08}s`,
-      class:'fade'},`${name} · ${counts[h]}`);
-    // hairline tying the label to its hub, skimming past the rim
-    const [gx,gy]=pol(CX,CY,R+22,deg);
-    el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:TRACK,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
-  });
-});
-})();
-
-// ════ 6 · cluster field ════
-(()=>{
-const SAT=[[622,78,'PLUGINS',22],[706,214,'THEMES',16],[540,268,'DOCS',13],[92,72,'FORKS',12],[774,58,'MIRRORS',9]];
-obsReveal('clusters',s=>{
-  const HX=300,HY=172;
-  // dotted cross-contribution paths, hub → each island
-  SAT.forEach(([x,y],ci)=>{
-    const mx=(HX+x)/2,my=(HY+y)/2-30;
-    el(s,'path',{d:`M${HX} ${HY} Q${mx} ${my} ${x} ${y}`,fill:'none',
-      stroke:GRID,'stroke-width':.9,'stroke-dasharray':'2 4',pathLength:1,
-      class:'draw',style:`animation-delay:${.8+ci*.12}s;animation-duration:.7s`});
-  });
-  // main hub: 80 golden-angle spokes
-  for(let i=0;i<80;i++){
-    const a=i*137.508,len=28+rnd(i+1,7)*88;
-    const [x,y]=pol(HX,HY,len,a);
-    el(s,'line',{x1:HX,y1:HY,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.012}s;animation-duration:.5s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:1.6+rnd(i+1,4)*1.8,fill:SER[0],opacity:.9,
-      class:'pop',style:`animation-delay:${.15+i*.012}s`});
-    tip(dot,`core contributor #${i+1}`);
-  }
-  el(s,'circle',{cx:HX,cy:HY,r:7,fill:HERO,class:'pop'});
-  txt(s,{x:HX,y:HY+140,'font-size':8,'font-weight':800,fill:HERO,'text-anchor':'middle',
-    'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,
-    class:'fade'},'CORE · 80');
-  // satellite islands
-  SAT.forEach(([x,y,name,n],ci)=>{
-    const col=RAMP[1+Math.round((n-9)/13*3)];   // 岛越大越重(22→4 档,9→1 档)
-    el(s,'circle',{cx:x,cy:y,r:3.5,fill:col,
-      class:'pop',style:`animation-delay:${.9+ci*.12}s`});
-    for(let k=0;k<n;k++){
-      const a=k*137.508+ci*40,rr=9+rnd(k+1,ci+2)*26;
-      const [dx,dy]=pol(x,y,rr,a);
-      if(k%2===0)el(s,'line',{x1:x,y1:y,x2:dx,y2:dy,stroke:TRACK,'stroke-width':.5,
-        class:'fade',style:`animation-delay:${1+ci*.12+k*.02}s`});
-      el(s,'circle',{cx:dx,cy:dy,r:1.4+rnd(k+3,ci+5)*1.4,fill:col,
-        stroke:RING,'stroke-width':.4,
-        class:'pop',style:`animation-delay:${1+ci*.12+k*.02}s`});
-    }
-    txt(s,{x,y:y+44,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.12em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px`,
-      class:'fade'},`${name} · ${n}`);
-  });
-});
-})();
-
-// ════ 1 · brand spectrum ════
-// paired opposites; the pale ribbon threads our position down the page
-(()=>{
-const ROWS=[['FRIEND','AUTHORITY'],['SERIOUS','PLAYFUL'],['RELIABLE','RISK-TAKING'],['CONTEMPORARY','CLASSIC']];
-const US=[.74,.80,.40,.22];                 // 0 = left trait, 1 = right trait
-const COMP=[[.42,.52,.57],[.60,.65,.86],[.30,.34,.50],[.36,.41,.73]];
-const X0=118,X1=342,Y=i=>58+i*60,px=t=>X0+t*(X1-X0);
-obsReveal('spectrum',s=>{
-  // the ribbon first, underneath everything
-  let d=`M${px(US[0])} ${Y(0)}`;
-  for(let i=1;i<4;i++)
-    d+=` C${px(US[i-1])} ${Y(i-1)+30} ${px(US[i])} ${Y(i)-30} ${px(US[i])} ${Y(i)}`;
-  el(s,'path',{d,fill:'none',stroke:HALO,'stroke-width':30,'stroke-linecap':'round',
-    'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',
-    style:'animation-duration:1.4s'});
-  ROWS.forEach(([a,b],i)=>{
-    const y=Y(i);
-    el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:FAINT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    [X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:FAINT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`}));
-    txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);
-    txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:LAB,
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);
-    COMP[i].forEach((t,k)=>{
-      const c=el(s,'circle',{cx:px(t),cy:y,r:3.2,fill:SER[(i+1)%4],
-        stroke:RING,'stroke-width':.6,
-        class:'pop',style:`animation-delay:${.3+i*.1+k*.05}s`});
-      tip(c,`competitor ${'ABC'[k]} — ${Math.round(t*100)}% toward ${b}`);
-    });
-    const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:HERO,
-      class:'pop',style:`animation-delay:${.5+i*.1}s`});
-    tip(us,`us — ${Math.round(US[i]*100)}% toward ${b}`);
-  });
-  txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:FAINT,'letter-spacing':'.12em',
-    class:'fade',style:'animation-delay:.9s'},'● US    ● COMPETITORS A · B · C');
-});
-})();
-
-// ════ 2 · dotty matrix — isometric stacked dot planes ════
-// pitch 20/10, dot radius capped well under half-pitch, decks 64px apart:
-// each plane reads as its own quiet grid instead of a smudge
-(()=>{
-const SQUAD=['PLATFORM','GROWTH','MOBILE','INFRA'];
-const SHADE=[SER[3],SER[2],SER[1],SER[0]];   // 底层→顶层,各队一色
-const P=(c,r,k)=>[196+(c-r)*20,236+(c+r)*10-k*64];
-obsReveal('dotty',s=>{
-  for(let k=0;k<4;k++){
-    // plane slab: paper fill so upper decks occlude lower ones
-    const cs=[P(-.8,-.8,k),P(5.8,-.8,k),P(5.8,5.8,k),P(-.8,5.8,k)];
-    el(s,'path',{d:'M'+cs.map(p=>p.join(' ')).join(' L ')+' Z',
-      fill:BG,'fill-opacity':.96,stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${k*.16}s`});
-    for(let r=0;r<6;r++)for(let c=0;c<6;c++){
-      const load=rnd(k*37+r*6+c+1,k+2);
-      const v=load<.3?0:Math.round(load*12);   // sparser: silence is part of the texture
-      const [x,y]=P(c,r,k);
-      if(!v){el(s,'circle',{cx:x,cy:y,r:.7,fill:FLOOR,
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});continue}
-      const dot=el(s,'circle',{cx:x,cy:y,r:1+Math.sqrt(v)*1.15,fill:SHADE[k],
-        stroke:RING,'stroke-width':.45,
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});
-      tip(dot,`${SQUAD[k]} · week ${c+1} × lane ${r+1} — ${v} tasks`);
-    }
-    // label off the plane's right corner, tied by a short hairline
-    const [cxr,cyr]=P(5.8,-.8,k);
-    el(s,'line',{x1:cxr+4,y1:cyr,x2:cxr+22,y2:cyr,stroke:FAINT,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.2+k*.16}s`});
-    txt(s,{x:cxr+27,y:cyr+2.5,'font-size':7.5,'font-weight':700,fill:k===3?TXT:MUT,
-      'letter-spacing':'.12em',class:'fade',style:`animation-delay:${.2+k*.16}s`},SQUAD[k]);
-  }
-});
-})();
-
-// ════ 3 · bubble almanac ════
-// the vintage-almanac feel lives in three things: ledger-paper hairlines
-// under everything, bubbles big enough to collide, and marginalia. do all three.
-(()=>{
-const AREAS=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','GRID','VIEWS','HUB','FORMS'];
-const BETA={GRID:2023,VIEWS:2024,HUB:2025,FORMS:2025};   // beta until this year
-const BORN={EDITOR:2019,BOARDS:2019,DOCS:2019,CHAT:2020,FLOWS:2020,VAULT:2021,PAGES:2021,SYNC:2022,GRID:2022,VIEWS:2023,HUB:2024,FORMS:2024};
-obsReveal('almanac',s=>{
-  const rowY=i=>62+i*34,colX=j=>132+j*57;
-  // ledger paper: hairlines every 6.8px, edge to edge, under everything
-  for(let y=40;y<=318;y+=6.8)
-    el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:QUIET,'stroke-width':.5,
-      class:'fade',style:`animation-delay:${(y-40)*.001}s`});
-  const vals=[];
-  for(let i=0;i<8;i++){
-    const yr=2019+i;
-    el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${i*.04}s`});
-    txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.04}s`},yr);
-    AREAS.forEach((a,j)=>{
-      if(yr<BORN[a])return;
-      const age=yr-BORN[a];
-      // steep spread: quiet areas stay pinpricks, busy ones balloon
-      const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));
-      vals.push([Math.max(1,v),i,j,a,yr]);
-    });
-  }
-  // draw small over large so the giants sit underneath as washes
-  vals.sort((a,b)=>b[0]-a[0]);
-  const top3=vals.slice(0,3);
-  // hand-drawn circle: radius wobbles around the rim on two slow sine waves,
-  // so every bubble is its own slightly-off print — never a compass circle
-  const blob=(x,y,r,seed)=>{
-    const n=Math.max(14,Math.round(r*1.6)),pts=[];
-    for(let t=0;t<n;t++){
-      const a=t/n*Math.PI*2;
-      const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;
-      pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);
-    }
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<n;t++){
-      const p=pts[t],q=pts[(t+1)%n];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+' Z';
-  };
-  vals.forEach(([v,i,j,a,yr],k)=>{
-    const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10,  // jitter off the gridline
-          y=rowY(i),r=Math.sqrt(v)*3.8;        // big: rows are 34 apart, giants reach ~28 and collide
-    const beta=BETA[a]&&yr<=BETA[a];
-    const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});
-    const seed=i*12+j,isTop=top3.includes(vals[k]);   // 主角:票量 top-3
-    const col=RAMP[Math.min(4,Math.floor(v/12))];        // 票量 5 档
-    const dot=el(g,'path',beta
-      ?{d:blob(x,y,r,seed),fill:'none',stroke:DATA2,'stroke-width':1.2,'stroke-dasharray':'3 3'}
-      :{d:blob(x,y,r,seed),fill:col,'fill-opacity':.16+rnd(i+2,j+4)*.14,
-        stroke:isTop?HERO:col,'stroke-opacity':isTop?.95:.5+rnd(i+5,j+7)*.38,
-        'stroke-width':isTop?1.7:.8+rnd(i+3,j+11)*1});
-    tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);
-    // escalation core: small wobbly blot, nudged off-center like a press mark
-    if(!beta){
-      const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;
-      el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:isTop?HERO:TXT});
-    }
-    if(isTop)
-      txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:HERO,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},v*10);
-  });
-  AREAS.forEach((a,j)=>{
-    const x=colX(j);
-    txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUT,'letter-spacing':'.1em',
-      transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);
-  });
-  // marginalia: two annotations with pointer hairlines, like the original's notes
-  el(s,'path',{d:`M${colX(3)+30} ${rowY(5)-14} C${colX(3)+70} ${rowY(5)-34} ${colX(3)+96} ${rowY(5)-38} ${colX(3)+118} ${rowY(5)-38}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:colX(3)+124,y:rowY(5)-35,'font-size':7,fill:LAB,'font-style':'italic',
-    class:'fade',style:'animation-delay:1.1s'},'chat tickets triple after mobile GA');
-  el(s,'path',{d:`M${colX(9)+16} ${rowY(2)+8} C${colX(9)+40} ${rowY(2)+26} ${colX(9)+60} ${rowY(2)+30} ${colX(9)+80} ${rowY(2)+30}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7,fill:LAB,'font-style':'italic',
-    class:'fade',style:'animation-delay:1.2s'},'betas barely ticket — nobody files bugs on toys');
-  // footer strip: important events, the almanac's timeline shelf
-  const evY=356;
-  el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:FAINT,'stroke-width':.8,
-    class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:56,y:evY-24,'font-size':7,'font-weight':800,fill:MUT,'letter-spacing':'.16em',
-    class:'fade',style:'animation-delay:1.2s'},'IMPORTANT EVENTS');
-  [['2020','SLA introduced'],['2022','self-serve help center'],['2023','the GA wave'],['2025','AI deflection live']]
-  .forEach(([yr,note],k)=>{
-    const x=90+k*196;
-    el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:MUT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`});
-    txt(s,{x,y:evY+4,'font-size':8,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);
-    txt(s,{x,y:evY+15,'font-size':7,fill:MUT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);
-  });
-});
-})();
-
-// ════ 4 · radial patchwork ════
-(()=>{
-const CX=200,CY=168,N=46;
-obsReveal('patchwork',s=>{
-  // 24h rim ticks
-  for(let h=0;h<96;h++){
-    const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:FAINT,'stroke-width':h%4===0?1:.5,
-      class:'fade',style:`animation-delay:${h*.006}s`});
-  }
-  [0,6,12,18].forEach(h=>{
-    const [x,y]=pol(CX,CY,157,-90+h*15);
-    txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      class:'fade'},String(h).padStart(2,'0'));
-  });
-  // the patchwork: translucent wedges pile into ink
-  const wedges=[];
-  for(let i=0;i<N;i++){
-    // deploys cluster around 11h and 16h, thin overnight
-    const peak=rnd(i+1,2)>.5?11:16;
-    const hour=(peak+(rnd(i+1,3)-.5)*7+24)%24;
-    const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;
-    const r1=34+rnd(i+1,5)*100;
-    wedges.push([a0,sw,r1,hour,i]);
-  }
-  wedges.forEach(([a0,sw,r1,hour,i])=>{
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:RAMP[Math.min(4,Math.floor(hour/4.8))],
-      'fill-opacity':.17+rnd(i+1,6)*.15,class:'fade',
-      style:`animation-delay:${.2+i*.022}s`});
-    tip(w,`deploy #${i+1} — ${String(Math.floor(hour)).padStart(2,'0')}:${String(Math.floor(hour%1*60)).padStart(2,'0')} · ${Math.round(r1*6)} files`);
-  });
-  // three outlined wedges: the ones that paged somebody
-  [4,17,31].forEach((i,k)=>{
-    const [a0,sw,r1]=wedges[i];
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',
-      stroke:HERO,'stroke-width':1.5,class:'fade',style:`animation-delay:${1.2+k*.15}s`});
-    tip(w,`deploy #${i+1} — triggered an incident`);
-  });
-  el(s,'circle',{cx:CX,cy:CY,r:3,fill:DATA,class:'pop',style:'animation-delay:1.3s'});
-  txt(s,{x:CX,y:308,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.4s'},'OUTLINED = TRIGGERED AN INCIDENT');
-});
-})();
-
-// ════ 6 · trend lineage ════
-(()=>{
-// [name, events [year, type f|o], aliveToday]
-const TR=[
-  ['Dark mode',[[2017,'f'],[2022,'o']],true],
-  ['Kanban',[[2016,'f']],true],
-  ['Wikis',[[2016,'f'],[2019,'o']],false],
-  ['Slash cmds',[[2018,'f']],true],
-  ['Templates',[[2019,'f'],[2024,'o']],true],
-  ['Inbox',[[2020,'f']],false],
-  ['Live cursors',[[2020,'f'],[2023,'o']],true],
-  ['AI drafts',[[2023,'f'],[2025,'o']],true],
-  ['Voice notes',[[2021,'f']],false],
-  ['Offline',[[2018,'f'],[2026,'o']],true],
-];
-const Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;
-// 主角:最近一次有动作的功能线 —— 标题里的 "come back"
-const HC=TR.map((r,i)=>[r[1][r[1].length-1][0],i]).sort((a,b)=>b[0]-a[0])[0][1];
-obsReveal('lineage',s=>{
-  for(let yr=Y0;yr<=2026;yr++){
-    el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:QUIET,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
-    if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
-  }
-  TR.forEach(([name,ev,alive],c)=>{
-    const x=colX(c),hero=c===HC;
-    const lastYr=ev[ev.length-1][0];
-    // 越近期动过 = 越重;已死的走最浅一档(虚线本身已经在说休眠)
-    const col=hero?HERO:RAMP[alive?Math.min(4,Math.max(2,Math.round((lastYr-2016)/10*4))):1];
-    // segments between events: solid while fresh, dashed once dormant
-    for(let k=0;k<ev.length-1;k++){
-      const gap=ev[k+1][0]-ev[k][0];
-      el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
-        stroke:col,'stroke-width':hero?1.8:1.4,
-        'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
-        class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
-    }
-    const last=ev[ev.length-1][0];
-    if(alive)
-      el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:col,'stroke-width':hero?1.8:1.4,
-        pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
-    ev.forEach(([yr,t])=>{
-      const node=el(s,'circle',t==='f'
-        ?{cx:x,cy:yY(yr),r:5.5,fill:col}
-        :{cx:x,cy:yY(yr),r:5.5,fill:BG,stroke:col,'stroke-width':1.6});
-      node.setAttribute('class','pop');
-      node.style.animationDelay=(.35+c*.07)+'s';
-      tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);
-    });
-    // terminal dot: alive lands on today's baseline, dead stops where it stopped
-    el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
-      fill:col,class:'pop',style:`animation-delay:${.6+c*.07}s`});
-    const ly=yY(2026)+26;
-    txt(s,{x,y:ly,'font-size':6.8,'font-weight':hero?800:600,fill:hero?HERO:(alive?LAB:FAINT),
-      transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',
-      class:'fade',style:`${hero?`paint-order:stroke;stroke:${BG};stroke-width:3px;`:''}animation-delay:${.7+c*.05}s`},name);
-  });
-});
-})();
-
-// ════ 8 · type colonnade ════
-(()=>{
-const PRE=['core','ui','api','data','auth','sync','mail','flag','edge','doc','bot'];
-const SUF=['-kit','-svc','-web','-cli','-db','-gw','-sdk','-jobs'];
-const TEAM=['PLATFORM','FRONTEND','BACKEND','DATA','INFRA','MOBILE','SECURITY','GROWTH','ML','DESIGN'];
-obsReveal('colonnade',s=>{
-  const N=44;
-  const teamY=j=>36+j*28,repoY=i=>16+i*6.7;
-  const counts=Array(10).fill(0);
-  const owner=i=>{const w=rnd(i+1,17);return Math.min(9,Math.floor(w*w*10))}; // skewed: low teams hoard
-  for(let i=0;i<N;i++)counts[owner(i)]++;
-  for(let i=0;i<N;i++){
-    const name=PRE[i%11]+SUF[Math.floor(rnd(i+1,8)*8)%8]+(i>21?'-v2':'');
-    const yi=repoY(i),j=owner(i),yj=teamY(j);
-    const col=RAMP[Math.min(4,Math.floor(counts[j]/3))];   // 团队持有量 5 档
-    txt(s,{x:112,y:yi+2,'font-size':5,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.012}s`},name);
-    el(s,'rect',{x:117,y:yi-1.2,width:4,height:2.4,fill:col,
-      class:'fade',style:`animation-delay:${i*.012}s`});
-    el(s,'path',{d:`M123 ${yi} C 200 ${yi} 220 ${yj} 288 ${yj}`,fill:'none',
-      stroke:col,'stroke-width':.8,opacity:.78,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.015}s;animation-duration:.7s`});
-  }
-  TEAM.forEach((t,j)=>{
-    const y=teamY(j);
-    el(s,'circle',{cx:293,cy:y,r:2.6+counts[j]*.55,fill:RAMP[Math.min(4,Math.floor(counts[j]/3))],
-      class:'pop',style:`animation-delay:${.6+j*.05}s`});
-    const lab=txt(s,{x:302+counts[j]*.55,y:y+2.5,'font-size':7,'font-weight':700,fill:LAB,
-      class:'fade',style:`animation-delay:${.65+j*.05}s`},`${t} · ${counts[j]}`);
-    tip(lab,`${t} owns ${counts[j]} repos`);
-  });
-});
-})();
-
-// ════ 9 · hourglass stream ════
-(()=>{
-const STAGE=[['VISITORS',4200],['SIGN-UPS',1900],['ACTIVATED',960],['RETAINED',540],['PAYING',310]];
-obsReveal('hourglass',s=>{
-  const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;
-  // 主角:通过率最低的一级 —— 漏得最狠的地方
-  const PCT=STAGE.slice(1).map((n,k)=>Math.round(n[1]/STAGE[k][1]*100));
-  const leak=PCT.indexOf(Math.min(...PCT));
-  STAGE.forEach(([name,c],k)=>{
-    const y=sy(k),hw=w(c)/2;
-    // barcode strip: one tick ≈ 40 people
-    const n=Math.round(c/40);
-    for(let t=0;t<n;t++){
-      const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;
-      el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:RAMP[k],'stroke-width':1.1,
-        opacity:.6+rnd(t+2,k+5)*.4,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
-    }
-    // threads trickling to the next strip
-    if(k<4){
-      const hw1=w(STAGE[k+1][1])/2;
-      for(let t=0;t<34;t++){
-        const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
-        const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
-        el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,
-          fill:'none',stroke:FAINTDATA,'stroke-width':.7,opacity:.45,pathLength:1,
-          class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
-      }
-      // conversion between stages, parked in the left margin
-      const pct=Math.round(STAGE[k+1][1]/c*100);
-      txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':k===leak?10.5:8.5,'font-weight':800,
-        fill:k===leak?HERO:MUT,
-        class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
-      txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6,'font-weight':600,fill:k===leak?HERO:FAINT,
-        'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'GET THROUGH');
-    }
-    // stage label, tied to the strip edge
-    el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.3+k*.12}s`});
-    txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:LAB,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
-    txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());
-  });
-});
-})();
-
-// ════ 14 · hundred field ════
-// small data, unit-decomposed: four shares of 100% become 100 people.
-// phyllotaxis discs (golden angle) so每簇自带肌理; share is countable, not just readable.
-(()=>{
-const SEG=[['CHARGED',41,SER[0]],['TORN',35,SER[1]],['ADRIFT',12,SER[2]],['AVERSE',12,SER[3]]];
-const POS=[[132,140],[276,116],[186,252],[322,238]];
-obsReveal('hundredfield',s=>{
-  // dashed constellation hairlines tying the cluster cores
-  [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:GRID,'stroke-width':.7,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      // hairline spoke for every 5th person, core to dot
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:TRACK,'stroke-width':.6,
-        class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,
-        stroke:RING,'stroke-width':.4,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} — one of ${v} in 100`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:TXT,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},'ONE DOT = ONE PERSON · 41 + 35 + 12 + 12 = 100');
-});
-})();
-
-// ════ 15 · ballot tally ════
-// multi-select survey (each option independent, 0–100): every option is a row of
-// 100 hairline ticks — a hundred people standing in line, the ones who picked it inked.
-(()=>{
-const ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];
-obsReveal('ballottally',s=>{
-  const VS=ASK.map(a=>a[1]),topI=VS.indexOf(Math.max(...VS));   // 主角:得票最高项
-  ASK.forEach(([name,v],i)=>{
-    const base=78+i*62;
-    txt(s,{x:28,y:base-26,'font-size':7.5,'font-weight':700,fill:LAB,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=28+k*3.44,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?(i===topI?HERO:SER[i]):FLOOR,'stroke-width':picked?1.3:.6,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-11,'font-size':i===topI?12.5:11,'font-weight':800,
-      fill:i===topI?HERO:TXT,
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`${v} of 100 picked this — they could pick several`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
-});
-})();
-
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#22211F',PAPER='#F0F0EE',MUTED='rgba(31,30,28,.60)',GRID='rgba(31,30,28,.16)';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ L16 · matrix heat ════
-// 通用矩阵热力:任意 分类×分类×连续值。明度即数据——五档灰阶方块,
-// 越黑越高。对角线不参与(自己×自己),零格留一粒小点(沉默可见)。
-// 最强的一对上虚线框 + 标数。区别于 F10 dot heat:这里编码用明度不用面积,
-// 值是占比/强度而不是可数件数。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC'];
-const v=(i,j)=>{
-  if(i===j)return -1;
-  const a=Math.min(i,j),b=Math.max(i,j);
-  const pop=(8-a)/8*(8-b)/8;                       // 头部功能重合度高
-  const raw=pop*62*(0.35+rnd(a*8+b+1,a+b+3)*.9);
-  return Math.round(raw);
-};
-const shade=t=>t>36?'#F5572F':t>24?'#6E6D66':t>14?'#8F8E86':t>6?'#C0BFB7':'#DBDAD3';
-obsReveal('matheat',s=>{
-  const X0=118,Y0=70,P=26,C=22;
-  let max=0,mi=0,mj=0;
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  FEAT.forEach((f,i)=>{
-    txt(s,{x:X0-10,y:Y0+i*P+C/2+3,'font-size':7,'font-weight':700,fill:'rgba(31,30,28,.72)','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.04}s`},f);
-    const cy=Y0-10;
-    txt(s,{x:X0+i*P+C/2,y:cy,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.06em',
-      transform:`rotate(-55 ${X0+i*P+C/2} ${cy})`,class:'fade',style:`animation-delay:${i*.04}s`},f);
-  });
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){
-    const x=X0+j*P,y=Y0+i*P,t=v(i,j),d=(i+j)*.02;
-    if(t<0){el(s,'rect',{x:x+C/2-3,y:y+C/2-.6,width:6,height:1.2,fill:'rgba(31,30,28,.15)',
-      class:'fade',style:`animation-delay:${d}s`});continue}
-    if(!t){el(s,'circle',{cx:x+C/2,cy:y+C/2,r:.9,fill:'rgba(31,30,28,.15)',
-      class:'pop',style:`animation-delay:${d}s`});continue}
-    const cell=el(s,'rect',{x,y,width:C,height:C,rx:4,fill:shade(t),
-      class:'pop',style:`animation-delay:${d}s`});
-    tip(cell,`${FEAT[i]} × ${FEAT[j]} — ${t}% of accounts use both`);
-    if(i===mi&&j===mj){
-      el(s,'rect',{x:x-3.5,y:y-3.5,width:C+7,height:C+7,rx:6,fill:'none',
-        stroke:'#F5572F','stroke-width':1.8,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:.9s'});
-      txt(s,{x:x+C/2,y:y+C/2+3.5,'font-size':9,'font-weight':800,fill:PAPER,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},t);
-    }
-  }
-  // shade legend, countable buckets
-  const LG=[['#DBDAD3','1–6'],['#C0BFB7','7–14'],['#8F8E86','15–24'],['#6E6D66','25–36'],['#F5572F','37+']];
-  LG.forEach(([c,lab],k)=>{
-    const x=118+k*54;
-    el(s,'rect',{x,y:292,width:9,height:9,rx:2,fill:c,class:'fade',style:`animation-delay:${1+k*.05}s`});
-    txt(s,{x:x+13,y:300,'font-size':6.5,'font-weight':600,fill:'#8F8E86',
-      class:'fade',style:`animation-delay:${1+k*.05}s`},lab+'%');
-  });
-  txt(s,{x:36,y:300,'font-size':7,'font-weight':600,fill:'#C0BFB7','letter-spacing':'.1em',
-    class:'fade',style:'animation-delay:1s'},'SHADE = %');
-});
-})();
-
-// ════ L17 · calendar heat ════
-// 日历热力:GitHub 贡献墙的 Lupi 皮。一格一天,52 周 × 7 天,
-// 点面积 = 当天量(sqrt 换算),静默日一粒小点。月份写在上檐,
-// 峰值日虚线圈 + 旁注引线。逐记录世界观:一年 365 天每天都在场。
-(()=>{
-const MON=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
-const DAY=['MON','WED','FRI','SUN'];
-const val=(w,d)=>{
-  const wd=d<5;
-  const season=1+.55*Math.sin((w-8)/9);            // 春秋两个发布季
-  if(!wd)return rnd(w*7+d+1,3)>.82?1:0;            // 周末基本静默
-  const raw=season*(2.5+rnd(w*7+d+1,d+2)*9)*(rnd(w+1,d+5)>.12?1:0);
-  return Math.round(raw);
-};
-obsReveal('calheat',s=>{
-  const X0=56,Y0=42,P=14.8;
-  let max=0,mw=0,md=0;
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){const t=val(w,d);if(t>max){max=t;mw=w;md=d}}
-  DAY.forEach((lab,k)=>{
-    txt(s,{x:X0-10,y:Y0+[0,2,4,6][k]*P+3,'font-size':6.5,'font-weight':700,fill:'#8F8E86','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${k*.04}s`},lab);
-  });
-  MON.forEach((m,k)=>{
-    const x=X0+Math.round(k*52/12)*P;
-    txt(s,{x,y:Y0-16,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',
-      class:'fade',style:`animation-delay:${k*.03}s`},m);
-    el(s,'line',{x1:x,y1:Y0-11,x2:x,y2:Y0-5,stroke:'rgba(31,30,28,.32)','stroke-width':1.26,
-      class:'fade',style:`animation-delay:${k*.03}s`});
-  });
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){
-    const x=X0+w*P,y=Y0+d*P,t=val(w,d),delay=w*.012+d*.004;
-    if(!t){el(s,'circle',{cx:x,cy:y,r:.75,fill:'rgba(31,30,28,.15)',
-      class:'pop',style:`animation-delay:${delay}s`});continue}
-    const r=1.1+Math.sqrt(t)*1.55;
-    const dot=el(s,'circle',{cx:x,cy:y,r,
-      fill:t>max*.66?INK:t>max*.33?'rgba(31,30,28,.72)':'#C0BFB7',
-      class:'pop',style:`animation-delay:${delay}s`});
-    tip(dot,`W${w+1} ${['MON','TUE','WED','THU','FRI','SAT','SUN'][d]} — ${t} deploys`);
-  }
-  // peak: dashed ring + marginalia hairline
-  const px=X0+mw*P,py=Y0+md*P,pr=1.1+Math.sqrt(max)*1.55;
-  el(s,'circle',{cx:px,cy:py,r:pr+3.6,fill:'none',stroke:'#F5572F','stroke-width':1.8,
-    'stroke-dasharray':'2 3',class:'fade',style:'animation-delay:1s'});
-  el(s,'path',{d:`M${px+pr+6} ${py+6} C${px+34} ${py+30} ${px+52} ${py+36} ${px+72} ${py+36}`,
-    fill:'none',stroke:'#C0BFB7','stroke-width':1.26,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:px+78,y:py+39,'font-size':7,fill:'rgba(31,30,28,.72)','font-style':'italic',
-    class:'fade',style:'animation-delay:1.15s'},`the release-week spike — ${max} deploys in a day`);
-  txt(s,{x:420,y:222,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DAY · DOT AREA = DEPLOYS · TINY DOT = A QUIET DAY');
-});
-})();
-
-// ════ L18 · beeswarm ════
-// 蜂群:violin 的 Lupi 姊妹。不聚合密度,把 120 条真实记录沿数值轴
-// 逐颗排开,撞上了就往两边让——堆得高的地方就是分布的峰。
-// 空心 = enterprise 单,虚线 = 中位数。每颗点背后是一条真实成交。
-(()=>{
-const N=120;
-const deals=Array.from({length:N},(_,i)=>{
-  const u=rnd(i+1,3),ent=rnd(i+2,11)>.86;
-  const v=Math.round(4+(ent?60:6)+150*Math.pow(u,2.6)+rnd(i+3,7)*10);
-  return [Math.min(178,v),ent];
-});
-obsReveal('beeswarm',s=>{
-  const X0=36,X1=376,BASE=246,R=4,GAP=R*2+.8;
-  const mapX=v=>X0+v/180*(X1-X0);
-  // axis rail + barcode ticks
-  el(s,'line',{x1:X0-4,y1:BASE,x2:X1+4,y2:BASE,stroke:GRID,'stroke-width':1.44,class:'fade'});
-  for(let g=0;g<=9;g++){
-    const x=X0+g/9*(X1-X0);
-    el(s,'line',{x1:x,y1:BASE,x2:x,y2:BASE+(g%3===0?7:4),stroke:'rgba(31,30,28,.24)','stroke-width':1.08,
-      class:'fade',style:`animation-delay:${g*.02}s`});
-    if(g%3===0)txt(s,{x,y:BASE+18,'font-size':7,'font-weight':600,fill:'rgba(31,30,28,.32)','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${g*.02}s`},'$'+Math.round(g/9*180)+'k');
-  }
-  // swarm as clean stacks: x snaps to a GAP-wide lane, dots pile upward —
-  // the pile height IS the density. no half-overlaps, every dot countable.
-  const lanes={};
-  const sorted=deals.map((d,i)=>[d[0],d[1],i]).sort((a,b)=>a[0]-b[0]);
-  sorted.forEach(([v,ent,i])=>{
-    const lane=Math.round((mapX(v)-X0)/GAP);
-    const x=X0+lane*GAP;
-    const row=lanes[lane]||0;lanes[lane]=row+1;
-    const y=BASE-R-1-row*GAP;
-    const dot=ent
-      ?el(s,'circle',{cx:x,cy:y,r:R,fill:PAPER,stroke:INK,'stroke-width':1.98,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`})
-      :el(s,'circle',{cx:x,cy:y,r:R,fill:INK,opacity:.82,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`});
-    tip(dot,`deal #${i+1} — $${v}k${ent?' · enterprise':''}`);
-  });
-  // median
-  const vs=deals.map(d=>d[0]).sort((a,b)=>a-b);
-  const med=vs[Math.floor(N/2)];
-  const mx=mapX(med);
-  el(s,'line',{x1:mx,y1:52,x2:mx,y2:BASE+8,stroke:'#F5572F','stroke-width':1.62,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx,y:44,'font-size':9.5,'font-weight':800,fill:'#F5572F','text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1.1s`,
-    class:'fade'},'MEDIAN $'+med+'k');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DEAL · THE PILE IS THE DISTRIBUTION · HOLLOW = ENTERPRISE');
-});
-})();
-
-// ════ L19 · ridgeline ════
-// 多组分布的 Lupi 皮:每行一条完成时长的密度山脊,面积由竖发丝组成
-// (F3 hairline area 的语法),行与行轻微重叠,后排画在前、前排糊上
-// 纸色 slab 遮挡(dotty matrix 的手法)。峰值点标中心时长。
-(()=>{
-const ROWS=[['MOBILE',3.2,1.3],['DESKTOP',5.1,1.9],['API',7.4,2.3],['IMPORTS',10.8,3.2],['BATCH',15.2,4]];
-obsReveal('ridge',s=>{
-  const X0=92,X1=372,MAXH=24,N=72,LIFT=54;
-  const rowY=i=>96+i*44;
-  ROWS.forEach(([name,c,sp],i)=>{
-    const y0=rowY(i);
-    const dens=Array.from({length:N},(_,k)=>{
-      const h=k/(N-1)*MAXH;
-      return Math.exp(-((h-c)**2)/(2*sp*sp))+.26*Math.exp(-((h-c*2.1)**2)/(2*(sp*1.7)**2));
-    });
-    const dmax=Math.max(...dens);
-    const pts=dens.map((d,k)=>[X0+k/(N-1)*(X1-X0),y0-d/dmax*LIFT]);
-    const crest='M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ');
-    el(s,'path',{d:crest+` L${X1} ${y0} L${X0} ${y0} Z`,fill:PAPER,'fill-opacity':.96,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    pts.forEach(([px,py],k)=>{
-      if(k%2)return;
-      el(s,'line',{x1:px,y1:y0,x2:px,y2:py,stroke:'#8F8E86','stroke-width':0.9,
-        opacity:.28+rnd(k+1,i+3)*.32,class:'fade',style:`animation-delay:${i*.1+k*.004}s`});
-    });
-    el(s,'path',{d:crest,fill:'none',stroke:INK,'stroke-width':1.62,pathLength:1,
-      class:'draw',style:`animation-delay:${.1+i*.1}s;animation-duration:.8s`});
-    el(s,'line',{x1:X0,y1:y0,x2:X1,y2:y0,stroke:GRID,'stroke-width':1.26,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    const pk=dens.indexOf(dmax),[px,py]=pts[pk];
-    const dot=el(s,'circle',{cx:px,cy:py,r:2.6,fill:'#F5572F',class:'pop',style:`animation-delay:${.6+i*.1}s`});
-    tip(dot,`${name} — most runs finish around ${c.toFixed(1)}h`);
-    txt(s,{x:px+7,y:py+1,'font-size':8,'font-weight':800,fill:'#F5572F',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.7+i*.1}s`,
-      class:'fade'},c.toFixed(1)+'h');
-    txt(s,{x:X0-10,y:y0-3,'font-size':7.5,'font-weight':700,fill:'rgba(31,30,28,.72)','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-  });
-  const yb=rowY(4);
-  [0,6,12,18,24].forEach(h=>{
-    const x=X0+h/MAXH*(X1-X0);
-    el(s,'line',{x1:x,y1:yb+4,x2:x,y2:yb+9,stroke:'rgba(31,30,28,.24)','stroke-width':1.08,
-      class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x,y:yb+20,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},h+'h');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'EACH CREST = ONE PIPELINE · THE AREA IS MADE OF HAIRLINES · RED DOT = THE PEAK');
-});
-})();
-
-// ════ L20 · parallel coordinates ════
-// 多维记录对比的 Lupi 皮:一条发丝线 = 一个产品走过四根轴。
-// 线束的形状就是数据结构(哪两维相关、谁全线领先)。
-// 全线最优的上墨标名,其余发丝退后。轴顶名、轴脚 min/max。
-(()=>{
-const DIMS=[['PRICE $',8,30],['CSAT',6,9.6],['RETENTION %',55,95],['GROWTH %',-5,40]];
-const P=[['Editor',12,9.1,91,22],['Boards',18,8.4,86,18],['Forms',9,8.8,78,31],
-  ['Docs',15,8.0,82,12],['Chat',7,7.2,64,8],['Vault',24,7.8,88,6],
-  ['Flows',21,8.6,90,38],['Views',11,7.5,71,14],['Sync',16,6.9,58,-2],
-  ['Pages',8,8.2,74,19],['Grid',19,7.1,62,4],['Hub',13,6.6,52,9]];
-obsReveal('parallel',s=>{
-  const AX=[64,156,248,340],TOP=64,BOT=252;
-  const mapY=(di,v)=>{const [,lo,hi]=DIMS[di];return BOT-(v-lo)/(hi-lo)*(BOT-TOP)};
-  // hero: judged on csat+retention+growth (price is a fact, not a virtue)
-  const score=p=>(p[2]-6)/3.6+(p[3]-55)/40+(p[4]+5)/45;
-  const hero=P.reduce((a,b)=>score(b)>score(a)?b:a);
-  DIMS.forEach(([name,lo,hi],di)=>{
-    el(s,'line',{x1:AX[di],y1:TOP-8,x2:AX[di],y2:BOT+8,stroke:'#C0BFB7','stroke-width':1.8,
-      class:'fade',style:`animation-delay:${di*.06}s`});
-    [[TOP-8],[BOT+8]].forEach(([yy])=>
-      el(s,'line',{x1:AX[di]-4,y1:yy,x2:AX[di]+4,y2:yy,stroke:'#C0BFB7','stroke-width':1.8,
-        class:'fade',style:`animation-delay:${di*.06}s`}));
-    txt(s,{x:AX[di],y:TOP-22,'font-size':7,'font-weight':800,fill:'rgba(31,30,28,.72)','text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${di*.06}s`},name);
-    txt(s,{x:AX[di]+8,y:TOP-5,'font-size':6.5,'font-weight':600,fill:'rgba(31,30,28,.32)',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},hi);
-    txt(s,{x:AX[di]+8,y:BOT+11,'font-size':6.5,'font-weight':600,fill:'rgba(31,30,28,.32)',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},lo);
-  });
-  P.forEach((p,i)=>{
-    const isHero=p===hero;
-    const ys=[mapY(0,p[1]),mapY(1,p[2]),mapY(2,p[3]),mapY(3,p[4])];
-    let d=`M${AX[0]} ${ys[0].toFixed(1)}`;
-    for(let k=0;k<3;k++){
-      const mx=(AX[k]+AX[k+1])/2;
-      d+=` C${mx} ${ys[k].toFixed(1)} ${mx} ${ys[k+1].toFixed(1)} ${AX[k+1]} ${ys[k+1].toFixed(1)}`;
-    }
-    const ln=el(s,'path',{d,fill:'none',
-      stroke:isHero?'#F5572F':'#8F8E86','stroke-width':isHero?2:.65,
-      opacity:isHero?1:.5+rnd(i+1,5)*.3,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.05}s;animation-duration:.8s`});
-    tip(ln,`${p[0]} — $${p[1]} · CSAT ${p[2]} · ${p[3]}% retained · ${p[4]}% growth`);
-    ys.forEach((y,di)=>el(s,'circle',{cx:AX[di],cy:y,r:isHero?3:1.4,
-      fill:isHero?'#F5572F':'#8F8E86',opacity:isHero?1:.7,
-      class:'pop',style:`animation-delay:${.3+i*.05+di*.04}s`}));
-    if(isHero)
-      txt(s,{x:AX[3]+12,y:ys[3]+3,'font-size':9,'font-weight':800,fill:'#F5572F',
-        style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-        class:'fade'},p[0]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#C0BFB7','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE HAIRLINE = ONE PRODUCT · INK = BEST ACROSS THE BOARD');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 180
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/maps-palm.html

@@ -1,180 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Charts · Maps M1–M2 × PALM</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-:root{--bg:#F0EFEB;--dark:#43593B;--ink:#58402E;--muted:rgba(88,64,46,.60);--faint:rgba(88,64,46,.32);--grid:rgba(88,64,46,.16)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #ACAD79;color:#929960}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:#929960;margin-top:5px;line-height:1.7}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>Maps M1–M2 × PALM</h1>
-  <p>椰林绿地图换肤样张。明度表示区域数值;仅在用户明确要求地图时使用。</p>
-</div>
-<div class="grid2">
-  <!-- M1 · US Choropleth -->
-  <div class="card">
-    <span class="badge">MAPS · US CHOROPLETH</span>
-    <h2>Sign-ups across the states</h2>
-    <div class="sub">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data</div>
-    <div class="ch" id="mapus" style="height:340px"></div>
-    <div class="src">CHOROPLETH · PALM · GROWTH GEO</div>
-  </div>
-
-  <!-- M2 · World Choropleth -->
-  <div class="card wide">
-    <span class="badge">MAPS · WORLD CHOROPLETH</span>
-    <h2>Where the users are</h2>
-    <div class="sub">real outlines · shade = monthly active accounts · gray = markets we haven't opened</div>
-    <div class="ch" id="mapworld" style="height:400px"></div>
-    <div class="src">CHOROPLETH · PALM · GLOBAL OPS</div>
-  </div>
-</div>
-<script>
-const INK='#43593B',PAPER='#F0EFEB',MUTED='rgba(88,64,46,.60)',GRID='rgba(88,64,46,.16)';
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>fn(n);
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ M1 · US choropleth ════
-// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。
-// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角
-// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。
-// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、
-// 纸色描边当州界发丝、无数据州留 rgba(88,64,46,.10)(沉默可见)。
-// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。
-// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线
-// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。
-const MAP_LEGEND={
-  itemWidth:11,itemHeight:11,itemSymbol:'rect',
-  textStyle:{fontFamily:'Inter',fontSize:9,color:'#929960'},
-  inRange:{color:['#F2D17E','#ACAD79','#929960','#77835A','#43593B']},
-};
-(()=>{
-const D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,
-  'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,
-  'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,
-  'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,
-  'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,
-  'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,
-  'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};
-fetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-usa',geo,{
-      Alaska:{left:-131,top:25,width:15},
-      Hawaii:{left:-110,top:28,width:5},
-      'Puerto Rico':{left:-76,top:26,width:2},
-    });
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},
-      visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-usa',top:10,bottom:48,
-        itemStyle:{areaColor:'rgba(88,64,46,.10)',borderColor:PAPER,borderWidth:1.8},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,
-            fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),
-      }],
-    };
-    obsReveal('mapus',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-// ════ M2 · World choropleth ════
-// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,
-// 关键市场(≥39k)直接把数值标在版图上。
-(()=>{
-const D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,
-  'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,
-  'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,
-  'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,
-  'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,
-  'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,
-  'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,
-  'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};
-const BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],
-  'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};
-fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-world',geo);
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},
-      visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-world',top:4,bottom:36,
-        boundingCoords:[[-170,76],[190,-58]],   // crop antarctica
-        itemStyle:{areaColor:'rgba(88,64,46,.10)',borderColor:PAPER,borderWidth:1.26},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.98}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,
-            fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),
-      }],
-    };
-    obsReveal('mapworld',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-addEventListener('resize',()=>{
-  ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-</script>
-</body>
-</html>

+ 0 - 180
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/maps-porcelain.html

@@ -1,180 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Charts · Maps M1–M2 × PORCELAIN</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-:root{--bg:#F7F2EB;--dark:#081F5C;--ink:#081F5C;--muted:rgba(8,31,92,.60);--faint:rgba(8,31,92,.32);--grid:rgba(8,31,92,.16)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #BAD6EB;color:#7096D1}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:#7096D1;margin-top:5px;line-height:1.7}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>Maps M1–M2 × PORCELAIN</h1>
-  <p>青瓷蓝地图换肤样张。明度表示区域数值;仅在用户明确要求地图时使用。</p>
-</div>
-<div class="grid2">
-  <!-- M1 · US Choropleth -->
-  <div class="card">
-    <span class="badge">MAPS · US CHOROPLETH</span>
-    <h2>Sign-ups across the states</h2>
-    <div class="sub">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data</div>
-    <div class="ch" id="mapus" style="height:340px"></div>
-    <div class="src">CHOROPLETH · PORCELAIN · GROWTH GEO</div>
-  </div>
-
-  <!-- M2 · World Choropleth -->
-  <div class="card wide">
-    <span class="badge">MAPS · WORLD CHOROPLETH</span>
-    <h2>Where the users are</h2>
-    <div class="sub">real outlines · shade = monthly active accounts · gray = markets we haven't opened</div>
-    <div class="ch" id="mapworld" style="height:400px"></div>
-    <div class="src">CHOROPLETH · PORCELAIN · GLOBAL OPS</div>
-  </div>
-</div>
-<script>
-const INK='#081F5C',PAPER='#F7F2EB',MUTED='rgba(8,31,92,.60)',GRID='rgba(8,31,92,.16)';
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>fn(n);
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ M1 · US choropleth ════
-// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。
-// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角
-// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。
-// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、
-// 纸色描边当州界发丝、无数据州留 rgba(8,31,92,.10)(沉默可见)。
-// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。
-// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线
-// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。
-const MAP_LEGEND={
-  itemWidth:11,itemHeight:11,itemSymbol:'rect',
-  textStyle:{fontFamily:'Inter',fontSize:9,color:'#7096D1'},
-  inRange:{color:['#D0E3FF','#BAD6EB','#7096D1','#334EAC','#081F5C']},
-};
-(()=>{
-const D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,
-  'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,
-  'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,
-  'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,
-  'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,
-  'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,
-  'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};
-fetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-usa',geo,{
-      Alaska:{left:-131,top:25,width:15},
-      Hawaii:{left:-110,top:28,width:5},
-      'Puerto Rico':{left:-76,top:26,width:2},
-    });
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},
-      visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-usa',top:10,bottom:48,
-        itemStyle:{areaColor:'rgba(8,31,92,.10)',borderColor:PAPER,borderWidth:1.8},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,
-            fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),
-      }],
-    };
-    obsReveal('mapus',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-// ════ M2 · World choropleth ════
-// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,
-// 关键市场(≥39k)直接把数值标在版图上。
-(()=>{
-const D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,
-  'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,
-  'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,
-  'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,
-  'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,
-  'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,
-  'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,
-  'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};
-const BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],
-  'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};
-fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-world',geo);
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},
-      visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-world',top:4,bottom:36,
-        boundingCoords:[[-170,76],[190,-58]],   // crop antarctica
-        itemStyle:{areaColor:'rgba(8,31,92,.10)',borderColor:PAPER,borderWidth:1.26},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.98}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,
-            fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),
-      }],
-    };
-    obsReveal('mapworld',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-addEventListener('resize',()=>{
-  ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-</script>
-</body>
-</html>

+ 0 - 180
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/color/maps-wire.html

@@ -1,180 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Charts · Maps M1–M2 × WIRE</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-:root{--bg:#F0F0EE;--dark:#1F1E1C;--ink:#1F1E1C;--muted:rgba(31,30,28,.60);--faint:rgba(31,30,28,.32);--grid:rgba(31,30,28,.16)}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #C0BFB7;color:#8F8E86}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:#8F8E86;margin-top:5px;line-height:1.7}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>Maps M1–M2 × WIRE</h1>
-  <p>编辑部红地图换肤样张。灰阶承载区域数值,红色只标最高档;仅在用户明确要求地图时使用。</p>
-</div>
-<div class="grid2">
-  <!-- M1 · US Choropleth -->
-  <div class="card">
-    <span class="badge">MAPS · US CHOROPLETH</span>
-    <h2>Sign-ups across the states</h2>
-    <div class="sub">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data</div>
-    <div class="ch" id="mapus" style="height:340px"></div>
-    <div class="src">CHOROPLETH · WIRE · GROWTH GEO</div>
-  </div>
-
-  <!-- M2 · World Choropleth -->
-  <div class="card wide">
-    <span class="badge">MAPS · WORLD CHOROPLETH</span>
-    <h2>Where the users are</h2>
-    <div class="sub">real outlines · shade = monthly active accounts · gray = markets we haven't opened</div>
-    <div class="ch" id="mapworld" style="height:400px"></div>
-    <div class="src">CHOROPLETH · WIRE · GLOBAL OPS</div>
-  </div>
-</div>
-<script>
-const INK='#22211F',PAPER='#F0F0EE',MUTED='rgba(31,30,28,.60)',GRID='rgba(31,30,28,.16)';
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>fn(n);
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ M1 · US choropleth ════
-// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。
-// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角
-// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。
-// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、
-// 纸色描边当州界发丝、无数据州留 rgba(31,30,28,.10)(沉默可见)。
-// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。
-// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线
-// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。
-const MAP_LEGEND={
-  itemWidth:11,itemHeight:11,itemSymbol:'rect',
-  textStyle:{fontFamily:'Inter',fontSize:9,color:'#8F8E86'},
-  inRange:{color:['#DBDAD3','#C0BFB7','#8F8E86','#6E6D66','#F5572F']},
-};
-(()=>{
-const D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,
-  'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,
-  'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,
-  'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,
-  'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,
-  'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,
-  'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};
-fetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-usa',geo,{
-      Alaska:{left:-131,top:25,width:15},
-      Hawaii:{left:-110,top:28,width:5},
-      'Puerto Rico':{left:-76,top:26,width:2},
-    });
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},
-      visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-usa',top:10,bottom:48,
-        itemStyle:{areaColor:'rgba(31,30,28,.10)',borderColor:PAPER,borderWidth:1.8},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,
-            fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),
-      }],
-    };
-    obsReveal('mapus',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-// ════ M2 · World choropleth ════
-// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,
-// 关键市场(≥39k)直接把数值标在版图上。
-(()=>{
-const D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,
-  'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,
-  'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,
-  'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,
-  'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,
-  'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,
-  'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,
-  'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};
-const BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],
-  'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};
-fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-world',geo);
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},
-      visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-world',top:4,bottom:36,
-        boundingCoords:[[-170,76],[190,-58]],   // crop antarctica
-        itemStyle:{areaColor:'rgba(31,30,28,.10)',borderColor:PAPER,borderWidth:1.26},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.98}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,
-            fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),
-      }],
-    };
-    obsReveal('mapworld',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-addEventListener('resize',()=>{
-  ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-</script>
-</body>
-</html>

+ 0 - 1135
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/glance-gallery.html

@@ -1,1135 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Charts — Glance Gallery · 22 张参考实现</title>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:#F0EFEB}
-  .card.dark .sub{color:#8F8E88}
-  .card.dark .src{color:#55554F}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .ch{height:320px}
-  .wrap{position:relative;height:320px}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:#1C1C1A;color:#F0EFEB}
-  .badge.pick{border:1.5px solid #1C1C1A;color:#1C1C1A}
-  .badge.pair{border:1px dashed #B0AFA9;color:#8F8E88}
-  .card.dark .badge.del{background:#F0EFEB;color:#1C1C1A}
-  .card.dark .badge.pick{border-color:#F0EFEB;color:#F0EFEB}
-  .card.dark .badge.pair{border-color:#55554F;color:#8F8E88}
-</style>
-</head>
-<body>
-<div class="pagehead">
-
-  <h1>Glance 系(快读)· 22 张参考实现</h1>
-  <p>粗笔画 · 提前聚合 · 3 秒读完。每张卡是一个图型的参考实现;代码在页内 <code>// ════ 图型名 ════</code> 注释块里,按 catalog.md 的卡内标题检索。</p>
-</div>
-<div class="grid2">
-
-  <!-- ══ mono-demo ══ -->
-  <div class="card">
-    <h2>Daily active range</h2>
-    <div class="sub">min – max concurrent users · 1–14 Feb</div>
-    <div class="wrap"><canvas id="c1"></canvas></div>
-    <div class="src">RANGE CAPSULES · MONO-DEMO · LIVE OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>How releases made us feel</h2>
-    <div class="sub">reactions tagged in retro · H1 2026</div>
-    <div class="ch" id="rose"></div>
-    <div class="src">PETAL ROSE · MONO-DEMO · TEAM RETRO BOARD</div>
-  </div>
-
-  <div class="card">
-    <h2>Revenue by plan</h2>
-    <div class="sub">Q2 2026 · MRR, $K</div>
-    <div class="wrap"><canvas id="c3"></canvas></div>
-    <div class="src">CHUNKY BARS · MONO-DEMO · BILLING</div>
-  </div>
-
-  <!-- ══ mono-redesign · dot waffle ══ -->
-  <div class="card">
-    <h2>Where sign-ups come from</h2>
-    <div class="sub">Q2 2026 · every dot = 1% of new accounts</div>
-    <svg id="waffle" viewBox="0 0 380 300" width="100%" style="max-height:320px"></svg>
-    <div class="src">DONUT, REDESIGNED · MONO-REDESIGN · ATTRIBUTION</div>
-  </div>
-
-  <!-- ══ mono-fancy ══ -->
-  <div class="card">
-    <h2>Trees planted, year by year</h2>
-    <div class="sub">reforestation program · thousands of trees</div>
-    <div class="ch" id="forest"></div>
-    <div class="src">PICTORIAL BAR · MONO-FANCY · GREEN OPS</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Who works with whom</h2>
-    <div class="sub">cross-team collaborations · Q2 2026 · line = shared project</div>
-    <div class="ch" id="circular"></div>
-    <div class="src">CIRCULAR GRAPH · MONO-FANCY · PROJECT DB</div>
-  </div>
-
-  <div class="card">
-    <h2>Everything the platform ships</h2>
-    <div class="sub">product areas and capabilities · Jun 2026</div>
-    <div class="ch" id="tree"></div>
-    <div class="src">TREE · MONO-FANCY · PRODUCT CATALOG</div>
-  </div>
-
-  <div class="card">
-    <h2>Campaigns rain down, sign-ups flow</h2>
-    <div class="sub">daily ad spend (from top, $K) vs sign-ups (flow, hundreds) · 30 days</div>
-    <div class="ch" id="rainfall"></div>
-    <div class="src">DUAL AREA · MONO-FANCY · GROWTH + ADS</div>
-  </div>
-
-  <div class="card">
-    <h2>One dataset, three views</h2>
-    <div class="sub">watch the same 12 products fly between views · every 3s</div>
-    <div class="ch" id="morph"></div>
-    <div class="src">SCATTER MORPH · MONO-FANCY · COHORTS</div>
-  </div>
-
-  <!-- ══ mono-fancy2 ══ -->
-  <div class="card">
-    <h2>Where we gained, where we bled</h2>
-    <div class="sub">net accounts per segment · Q2 2026 · churn left, growth right</div>
-    <div class="ch" id="negbar"></div>
-    <div class="src">DIVERGING BAR · MONO-FANCY2 · CRM</div>
-  </div>
-
-  <div class="card dark">
-    <h2>Integrations, pulled into orbit</h2>
-    <div class="sub">force layout · node = monthly syncs · drag a node, the web fights back</div>
-    <div class="ch" id="force"></div>
-    <div class="src">FORCE GRAPH · NEW · INTEGRATION HUB</div>
-  </div>
-
-  <div class="card">
-    <h2>Fifty markets, one wave</h2>
-    <div class="sub">sign-ups per market · bars cascade in · click to replay</div>
-    <div class="ch" id="wave"></div>
-    <div class="src">STAGGER DELAY · MONO-FANCY2 · GROWTH</div>
-  </div>
-
-  <!-- ══ mono-fancy3 ══ -->
-  <div class="card">
-    <h2>Big slice, deep engagement</h2>
-    <div class="sub">angle = share of users · radius = minutes per day · dotted rings at 15 / 30 / 45 min</div>
-    <div class="ch" id="custompie"></div>
-    <div class="src">CUSTOM PIE · MONO-FANCY3 · PRODUCT ANALYTICS</div>
-  </div>
-
-  <div class="card">
-    <h2>Support load, day by day</h2>
-    <div class="sub">tickets per hour · each row is its own axis · dot = volume</div>
-    <div class="ch" id="singleaxis"></div>
-    <div class="src">SINGLE AXIS · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <div class="card">
-    <h2>Response times, spread out</h2>
-    <div class="sub">every dot = one ticket · jitter reveals the pile-up · by priority</div>
-    <div class="ch" id="jitter"></div>
-    <div class="src">JITTER STRIP · MONO-FANCY3 · SUPPORT DESK</div>
-  </div>
-
-  <!-- ══ mono-fancy4 ══ -->
-  <div class="card">
-    <h2>Eight products race for revenue</h2>
-    <div class="sub">MRR by product, $K · 2019 → 2026 · leader wears black · click to replay</div>
-    <div class="ch" id="race"></div>
-    <div class="src">BAR RACE · MONO-FANCY4 · BILLING</div>
-  </div>
-
-  <div class="card">
-    <h2>Concurrent users, streaming</h2>
-    <div class="sub">one new reading every 300ms · window of 50 · click to restart</div>
-    <div class="ch" id="stream"></div>
-    <div class="src">DYNAMIC DATA · MONO-FANCY4 · LIVE OPS</div>
-  </div>
-
-  <div class="card">
-    <h2>H1 revenue, drawn in one stroke</h2>
-    <div class="sub">cumulative ARR · the line draws, the number counts · click to replay</div>
-    <div class="ch" id="stroke"></div>
-    <div class="src">DRAW-IN + COUNTER · MONO-FANCY4 · FINANCE</div>
-  </div>
-
-
-  <!-- G19 · violin -->
-  <div class="card">
-    <span class="badge">GLANCE 分布型 · 小提琴</span>
-    <h2>How fast each plan gets an answer</h2>
-    <div class="sub">hours to first reply · silhouette = where tickets pile up · paper tick = median</div>
-    <svg id="violin" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">VIOLIN · MONO-GLANCE · SUPPORT DESK</div>
-  </div>
-
-  <!-- G20 · matrix heat -->
-  <div class="card">
-    <span class="badge">GLANCE 热力型 · 通用矩阵</span>
-    <h2>Adoption runs hot on the new versions</h2>
-    <div class="sub">5 versions × 6 features · shade = % of accounts using it · every cell carries its number</div>
-    <svg id="gheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT (GLANCE) · MONO-GLANCE · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- G21 · rank strip -->
-  <div class="card">
-    <span class="badge">GLANCE 排名型 · RANK STRIP</span>
-    <h2>Flows climbs to the top</h2>
-    <div class="sub">one row per product, one cell per quarter · darker = higher rank · a bar race you can print</div>
-    <svg id="rankstrip" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RANK STRIP · MONO-GLANCE · BILLING</div>
-  </div>
-
-  <!-- G22 · aggregate sankey -->
-  <div class="card">
-    <span class="badge">GLANCE 流向型 · 聚合桑基</span>
-    <h2>Channels pour into plans</h2>
-    <div class="sub">ribbon width = accounts · channels on the left, the plans they land in on the right</div>
-    <svg id="sankey" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">AGGREGATE SANKEY · MONO-GLANCE · ATTRIBUTION</div>
-  </div>
-
-</div>
-<script>
-// ── shared Mono tokens ──
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const L=['#1C1C1A','#4A4944','#6A6963','#8F8E88','#B0AFA9','#C6C5BF'];
-const LAD=['#1C1C1A','#4A4944','#8F8E88','#B0AFA9','#D8D7D1'];
-const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const rnd=(i,k)=>((i*73856093)^(k*19349663))%1000/1000; // deterministic
-
-// ── unified reveal: every chart waits to be scrolled into view; click replays.
-//    fn charts manage their own timers via keep(); they're cleared on replay.
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUTED;
-Chart.defaults.font.family='Inter';
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=INK;
-Chart.defaults.plugins.tooltip.titleColor='#F0EFEB';
-Chart.defaults.plugins.tooltip.bodyColor='#A5A49E';
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-// ════ mono-demo · 1: floating range capsules ════
-(()=>{
-const ranges=[[195,235],[165,192],[150,232],[73,168],[122,202],[182,192],[138,178],[162,227],[195,235],[228,305],[218,232],[165,232],[118,210],[195,232]];
-cReveal('c1',()=>({
-  type:'bar',
-  data:{labels:ranges.map((_,i)=>i+1),
-    datasets:[{data:ranges,backgroundColor:INK,borderRadius:99,borderSkipped:false,barPercentage:.32}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:800,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*40:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw[0]+'K – '+i.raw[1]+'K users'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},
-         ticks:{padding:6,font:{size:9},callback:v=>[0,6,13].includes(v)?(v+1)+' FEB':''}},
-      y:{min:50,max:320,grid:{display:false},border:{display:false},
-         ticks:{padding:8,maxTicksLimit:6,callback:v=>v+'K',font:{size:9.5}}},
-    },
-  },
-}));
-})();
-
-// ════ mono-demo · 2: petal rose on dark ════
-(()=>{
-const EMO=[
-  ['Happiness',12],['Awe',10],['Admiration',5],['Surprise',12],
-  ['Sadness',6],['Fear',4],['Anger',2],['Anticipation',5],
-];
-const MAXV=12;
-const petal=v=>{const t=v/MAXV;return t>.8?'#F2F1ED':t>.6?'#E4E3DD':t>.35?'#C9C8C1':'#A8A7A0'};
-eReveal('rose',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.name+' — '+EMO[p.dataIndex][1]},
-  series:[
-    {type:'pie',silent:false,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:'#32312D',borderRadius:16,borderColor:'#1C1C1A',borderWidth:5},
-     label:{show:false},emphasis:{scale:false},
-     data:EMO.map(([n])=>({name:n,value:1})),z:1},
-    {type:'pie',silent:true,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:'transparent'},label:{position:'inside'},
-     data:EMO.map(([n,v])=>({name:n,value:1,
-       label:{formatter:`{n|${v}}\n{l|${n}}`,rich:{
-         n:{fontSize:17,fontWeight:700,fontFamily:'Inter',lineHeight:20,color:v>=8?'#1C1C1A':'#C8C7C1'},
-         l:{fontSize:9.5,fontFamily:'Inter',color:v>=8?'#55554F':'#8F8E88'},
-       }}})),z:3},
-    {type:'pie',roseType:'area',silent:true,radius:['14%','88%'],center:['50%','50%'],
-     itemStyle:{borderRadius:14,borderColor:'#1C1C1A',borderWidth:5},label:{show:false},
-     data:EMO.map(([n,v])=>({name:n,value:v,itemStyle:{color:petal(v)}})),z:2},
-  ],
-});
-})();
-
-// ════ mono-demo · 3: chunky bars, values on top ════
-(()=>{
-cReveal('c3',()=>({
-  type:'bar',
-  data:{labels:['STARTER','PRO','TEAM','ENT'],
-    datasets:[{data:[182,486,391,274],
-      backgroundColor:['#C6C5BF',INK,'#8F8E88','#B0AFA9'],
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>'$'+i.raw+'K MRR'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
-      y:{display:false,suggestedMax:540},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font='700 13px Inter';ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText('$'+chart.data.datasets[0].data[i]+'K',bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-// ════ mono-redesign · donut → waffle of dots ════
-(()=>{
-const NS='http://www.w3.org/2000/svg';
-const mkEl=(svg,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);svg.appendChild(n);return n};
-const D=[['Search',34],['Referral',27],['Social',18],['Partners',12],['Paid',9]];
-obsReveal('waffle',s=>{
-  s.innerHTML='';
-  const COLS=10,CELL=21,R=7.5,X0=8,Y0=10;
-  let idx=0;
-  D.forEach(([name,v],g)=>{
-    for(let k=0;k<v;k++){
-      const c=idx+k,row=Math.floor(c/COLS),col=c%COLS;
-      mkEl(s,'circle',{cx:X0+col*CELL+R,cy:Y0+row*CELL+R,r:R,fill:LAD[g],
-        class:'pop',style:`animation-delay:${c*.008+g*.05}s`});
-    }
-    idx+=v;
-  });
-  D.forEach(([name,v],g)=>{
-    const y=26+g*40;
-    mkEl(s,'circle',{cx:246,cy:y,r:6,fill:LAD[g]});
-    const n=mkEl(s,'text',{x:260,y:y-1,'font-size':10.5,'font-weight':600,fill:INK});
-    n.textContent=name;
-    const val=mkEl(s,'text',{x:260,y:y+13,'font-size':15,'font-weight':800,fill:g<2?INK:'#8F8E88'});
-    val.textContent=v+'%';
-  });
-});
-})();
-
-// ════ mono-fancy · 1: pictorial bar forest ════
-(()=>{
-const TREE='path://M20,0 L38,28 L29,28 L40,46 L26,46 L26,58 L14,58 L14,46 L0,46 L11,28 L2,28 Z';
-const years=['2022','2023','2024','2025','2026'];
-const vals=[26,41,63,88,117];
-eReveal('forest',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*120,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+vals[p.dataIndex]+'k trees'},
-  grid:{left:44,right:56,top:10,bottom:8},
-  yAxis:{type:'category',data:years,inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},
-  xAxis:{show:false,max:13},
-  series:[{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:true,symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    data:vals.map((v,i)=>({name:years[i],value:v/10,itemStyle:{color:L[Math.max(0,4-i)]}})),
-    label:{show:true,position:'right',offset:[8,0],
-      color:INK,fontFamily:'Inter',fontSize:12,fontWeight:700,
-      formatter:p=>vals[p.dataIndex]+'k'},
-    z:10,
-  },{
-    type:'pictorialBar',symbol:TREE,
-    symbolRepeat:'fixed',symbolSize:[17,24],symbolMargin:3,symbolClip:true,
-    itemStyle:{color:'#DEDDD6'},data:years.map(()=>13),silent:true,z:5,
-  }],
-});
-})();
-
-// ════ mono-fancy · 2: circular graph on dark ════
-(()=>{
-const teams=[
-  ['Product',52],['Design',38],['Frontend',46],['Backend',44],
-  ['Data',30],['Growth',34],['Support',22],['Ops',18],['Legal',9],['Finance',12],
-];
-const links=[
-  [0,1,9],[0,2,8],[0,3,7],[1,2,9],[2,3,6],[3,4,7],[0,5,6],[5,4,5],
-  [5,6,4],[6,2,3],[7,3,4],[7,9,3],[8,9,2],[0,8,2],[4,2,4],[1,5,3],
-];
-eReveal('circular',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:tipDark,
-  series:[{
-    type:'graph',layout:'circular',circular:{rotateLabel:false},
-    left:52,right:52,top:34,bottom:34,
-    data:teams.map(([n,v],i)=>({
-      name:n,value:v,symbolSize:v*.62,
-      itemStyle:{color:i<4?PAPER:i<8?'#8F8E88':'#4A4944',borderWidth:0},
-      label:{show:true,position:'right',distance:7,color:'#B3B0A4',fontFamily:'Inter',fontSize:9.5,fontWeight:600},
-    })),
-    links:links.map(([s,t,w])=>({
-      source:s,target:t,
-      lineStyle:{width:w*.7,color:'#4A4840',opacity:.55,curveness:.28},
-    })),
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.8}},
-  }],
-});
-})();
-
-// ════ mono-fancy · 3: LR tree ════
-(()=>{
-const b=(name,kids,shade)=>({name,itemStyle:{color:shade},lineStyle:{color:shade},
-  children:kids.map(k=>({name:k,itemStyle:{color:shade},lineStyle:{color:shade}}))});
-eReveal('tree',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:tipLight,
-  series:[{
-    type:'tree',layout:'orthogonal',orient:'LR',
-    left:64,right:96,top:8,bottom:8,
-    symbol:'circle',symbolSize:7,
-    initialTreeDepth:2,expandAndCollapse:false,roam:false,
-    itemStyle:{borderWidth:0},
-    lineStyle:{width:1.4,curveness:.5},
-    label:{fontFamily:'Inter',fontSize:10,fontWeight:600,color:'#6A6963',position:'left'},
-    leaves:{label:{position:'right',color:MUTED,fontWeight:500}},
-    data:[{
-      name:'Platform',itemStyle:{color:INK},lineStyle:{color:'#C6C5BF'},
-      label:{fontSize:11.5,color:INK},
-      children:[
-        b('Editor',['Blocks','Tables','Comments','History'],L[0]),
-        b('Automate',['Workflows','Triggers','Webhooks'],L[2]),
-        b('Collaborate',['Spaces','Guests','Mentions'],L[3]),
-        b('Integrate',['API','Slack','GitHub'],L[4]),
-      ],
-    }],
-  }],
-});
-})();
-
-// ════ mono-fancy · 4: rainfall dual area ════
-(()=>{
-const days=Array.from({length:30},(_,i)=>i+1);
-const flow=[42,45,44,48,52,55,53,58,64,62,66,71,69,75,82,79,84,80,86,92,88,95,91,97,104,101,108,105,112,118];
-const rain=[2,0,4,8,3,0,6,12,7,3,9,15,6,2,11,5,8,14,4,9,6,13,3,7,10,5,12,8,15,6];
-eReveal('rainfall',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipLight,trigger:'axis',
-    formatter:p=>`Day ${p[0].axisValue} — spend $${rain[p[0].dataIndex]}K · ${p[0].value} sign-ups`},
-  axisPointer:{link:[{xAxisIndex:'all'}],lineStyle:{color:'#B0AFA9'}},
-  grid:[
-    {left:44,right:14,top:8,height:'26%'},
-    {left:44,right:14,top:'42%',bottom:26},
-  ],
-  xAxis:[
-    {gridIndex:0,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},position:'bottom'},
-    {gridIndex:1,type:'category',data:days,axisLine:{show:false},axisTick:{show:false},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,interval:6,formatter:v=>'D'+v}},
-  ],
-  yAxis:[
-    {gridIndex:0,inverse:true,max:18,splitLine:{show:false},
-     axisLabel:{color:'#B0AFA9',fontFamily:'Inter',fontSize:9,formatter:'${value}K'},
-     axisLine:{show:false},axisTick:{show:false}},
-    {gridIndex:1,splitLine:{lineStyle:{color:'#DEDDD6'}},
-     axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5},
-     axisLine:{show:false},axisTick:{show:false}},
-  ],
-  series:[
-    {name:'spend',type:'bar',xAxisIndex:0,yAxisIndex:0,data:rain,
-     itemStyle:{color:'#B0AFA9',borderRadius:[0,0,4,4]},barWidth:'55%'},
-    {name:'sign-ups',type:'line',xAxisIndex:1,yAxisIndex:1,data:flow,
-     smooth:.4,symbol:'none',lineStyle:{color:INK,width:2.2},
-     areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-       colorStops:[{offset:0,color:'rgba(28,28,26,.22)'},{offset:1,color:'rgba(28,28,26,0)'}]}}},
-  ],
-});
-})();
-
-// ════ mono-fancy · 5: universal transition morph ════
-(()=>{
-const P12=[
-  ['Editor',12,9.1,486],['Boards',18,8.4,391],['Forms',9,8.8,274],
-  ['Docs',15,8.0,318],['Chat',7,7.2,182],['Vault',24,7.8,226],
-  ['Flows',21,8.6,352],['Views',11,7.5,198],['Sync',16,6.9,141],
-  ['Pages',8,8.2,243],['Grid',19,7.1,167],['Hub',13,6.6,118],
-];
-const shade=i=>L[Math.min(5,Math.floor(i/2))];
-const axis={splitLine:{lineStyle:{color:'#DEDDD6'}},axisLine:{show:false},axisTick:{show:false},
-  axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}};
-const VIEWS=[
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:30},
-    xAxis:{...axis,type:'value',min:5,max:26,name:'PRICE $',nameTextStyle:{color:'#C6C5BF',fontSize:8.5}},
-    yAxis:{...axis,type:'value',min:6,max:9.6,name:'CSAT',nameTextStyle:{color:'#C6C5BF',fontSize:8.5}},
-    series:[{
-      id:'p',type:'scatter',universalTransition:true,
-      symbolSize:d=>Math.sqrt(d[3])*1.35,
-      data:P12.map((d,i)=>({name:d[0],value:[d[1],d[2],d[0],d[3]],
-        groupId:d[0],itemStyle:{color:shade(i)}})),
-      label:{show:true,position:'top',color:MUTED,fontFamily:'Inter',fontSize:8.5,
-        formatter:p=>p.name},
-    }],
-  }},
-  {opt:{
-    grid:{left:38,right:16,top:30,bottom:44},
-    xAxis:{...axis,type:'category',
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map(d=>d[0]),
-      axisLabel:{...axis.axisLabel,rotate:38,fontSize:8.5}},
-    yAxis:{...axis,type:'value',max:520},
-    series:[{
-      id:'p',type:'bar',universalTransition:true,
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(i),borderRadius:[6,6,0,0]}})),
-      barCategoryGap:'32%',
-    }],
-  }},
-  {opt:{
-    grid:{left:0,right:0,top:0,bottom:0},
-    xAxis:{show:false,type:'value'},yAxis:{show:false,type:'value'},
-    series:[{
-      id:'p',type:'pie',universalTransition:true,
-      radius:['26%','72%'],center:['50%','50%'],
-      itemStyle:{borderColor:'#F0EFEB',borderWidth:2,borderRadius:6},
-      label:{color:'#6A6963',fontFamily:'Inter',fontSize:9,formatter:'{b}'},
-      labelLine:{lineStyle:{color:'#C6C5BF'}},
-      data:[...P12].sort((a,b)=>b[3]-a[3]).map((d,i)=>({name:d[0],value:d[3],
-        groupId:d[0],itemStyle:{color:shade(i)}})),
-    }],
-  }},
-];
-const base={animationDurationUpdate:1100,animationEasingUpdate:'cubicInOut',tooltip:{...tipLight}};
-obsReveal('morph',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let vi=0;
-  g.setOption({...base,...VIEWS[0].opt});
-  keep('morph',setInterval(()=>{
-    vi=(vi+1)%VIEWS.length;
-    g.setOption({...base,...VIEWS[vi].opt},{replaceMerge:['xAxis','yAxis','series']});
-  },3000));
-});
-})();
-
-// ════ mono-fancy2 · 2: diverging bar ════
-(()=>{
-const D=[
-  ['Enterprise',86],['Team',54],['Pro',31],['Starter',12],
-  ['Legacy Basic',-18],['Trial expired',-42],['Free dormant',-67],
-];
-eReveal('negbar',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*80,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+(p.value>0?'+':'')+p.value+' accounts'},
-  grid:{left:96,right:52,top:8,bottom:8},
-  xAxis:{type:'value',
-    splitLine:{lineStyle:{color:'#DEDDD6'}},
-    axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-  yAxis:{type:'category',data:D.map(d=>d[0]),inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:'#6A6963',fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-  series:[{
-    type:'bar',barWidth:16,
-    data:D.map(([n,v])=>({name:n,value:v,
-      itemStyle:{color:v>0?INK:'#B0AFA9',
-        borderRadius:v>0?[0,9,9,0]:[9,0,0,9]}})),
-    label:{show:true,fontFamily:'Inter',fontSize:11,fontWeight:700,
-      position:'outside',
-      formatter:p=>(p.value>0?'+':'')+p.value,color:INK},
-    markLine:{symbol:'none',silent:true,label:{show:false},
-      lineStyle:{color:'#8F8E88',width:1.5},
-      data:[{xAxis:0}]},
-  }],
-});
-})();
-
-// ════ force graph on dark: the network shakes itself into place ════
-// (replaced the axis-break bar — bars must never lie about length; house rule)
-(()=>{
-const hubs=[['Core API',52,0]];
-const apps=[
-  ['Slack',34,1],['GitHub',30,1],['Figma',22,1],['Notion',26,1],
-  ['Linear',18,1],['Drive',16,1],['Zoom',10,2],['Jira',14,2],
-  ['Sheets',12,2],['Intercom',8,2],['Stripe',20,1],['Segment',9,2],
-];
-const nodes=[...hubs,...apps];
-const links=[
-  ...apps.map((a,i)=>({source:a[0],target:'Core API',
-    lineStyle:{width:Math.max(.8,a[1]*.09),color:'#4A4840',opacity:.6}})),
-  // a few app-to-app side roads
-  {source:'Slack',target:'GitHub',lineStyle:{width:1.2,color:'#3A3934',opacity:.5}},
-  {source:'GitHub',target:'Linear',lineStyle:{width:1.2,color:'#3A3934',opacity:.5}},
-  {source:'Figma',target:'Notion',lineStyle:{width:1,color:'#3A3934',opacity:.5}},
-  {source:'Stripe',target:'Sheets',lineStyle:{width:1,color:'#3A3934',opacity:.5}},
-];
-eReveal('force',{
-  animationDuration:1200,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.dataType==='node'?p.name+' — '+p.value+'k syncs/mo':''},
-  series:[{
-    type:'graph',layout:'force',
-    force:{repulsion:220,edgeLength:[36,110],gravity:.12,friction:.18},
-    roam:false,draggable:true,
-    left:14,right:14,top:14,bottom:14,
-    data:nodes.map(([n,v,tier])=>({
-      name:n,value:v,symbolSize:8+v*.75,
-      itemStyle:{color:tier===0?PAPER:tier===1?'#B3B0A4':'#6A6963'},
-      label:{show:true,position:'right',distance:6,
-        color:tier===0?PAPER:'#8F8E88',fontFamily:'Inter',
-        fontSize:tier===0?10.5:9,fontWeight:tier===0?800:600},
-    })),
-    links,
-    emphasis:{focus:'adjacency',lineStyle:{color:PAPER,opacity:.85}},
-  }],
-});
-})();
-
-// ════ mono-fancy2 · 4: stagger wave ════
-(()=>{
-const n=50;
-const vals=Array.from({length:n},(_,i)=>
-  Math.round(30+55*Math.sin(i/7.5)+18*Math.sin(i/2.6)+(i%5)*3+28));
-eReveal('wave',{
-  tooltip:{...tipLight,formatter:p=>'Market '+(p.dataIndex+1)+' — '+p.value},
-  grid:{left:36,right:10,top:14,bottom:24},
-  xAxis:{type:'category',data:vals.map((_,i)=>i+1),
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9,interval:9}},
-  yAxis:{type:'value',
-    splitLine:{lineStyle:{color:'#DEDDD6'}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5}},
-  series:[{
-    type:'bar',barCategoryGap:'26%',
-    data:vals.map((v,i)=>({value:v,
-      itemStyle:{color:L[Math.min(5,Math.floor((120-v)/16))],borderRadius:[4,4,0,0]}})),
-    animationDelay:i=>i*36,
-    animationDuration:640,animationEasing:'elasticOut',
-  }],
-  animationDurationUpdate:640,
-  animationDelayUpdate:i=>i*36,
-});
-})();
-
-// ════ mono-fancy3 · 1: custom double-encoded pie ════
-(()=>{
-const D=[
-  ['Editor',34,42],['Boards',22,31],['Docs',18,38],
-  ['Chat',12,11],['Automations',8,24],['Other',6,7],
-];
-const SCALE=45;
-const TAU=Math.PI*2;
-const byShare=[...D].sort((a,b)=>b[1]-a[1]);
-const minsDesc=D.map(d=>d[2]).sort((a,b)=>b-a);
-let acc=0;
-const slices=byShare.map(([n,share,mins])=>{
-  const p0=-Math.PI/2+acc/100*TAU, p1=-Math.PI/2+(acc+share)/100*TAU;
-  acc+=share;
-  return {n,share,mins,p0,p1,color:L[minsDesc.indexOf(mins)]};
-});
-eReveal('custompie',{
-  animationDuration:900,animationEasing:'cubicOut',animationDelay:i=>i*130,
-  tooltip:{...tipLight,formatter:p=>{
-    const s=slices[p.dataIndex];
-    return s.n+' — '+s.share+'% of users · '+s.mins+' min/day';
-  }},
-  xAxis:{show:false,min:-1,max:1},
-  yAxis:{show:false,min:-1,max:1},
-  grid:{left:0,right:0,top:0,bottom:0},
-  series:[{
-    type:'custom',
-    renderItem:(params,api)=>{
-      const s=slices[params.dataIndex];
-      const cx=api.getWidth()/2, cy=api.getHeight()/2+6;
-      const R=Math.min(api.getWidth(),api.getHeight())*.37;
-      const r0=R*.24;
-      const rOf=m=>r0+(R-r0)*m/SCALE;
-      const pm=(s.p0+s.p1)/2, lr=R+14;
-      const cos=Math.cos(pm);
-      const kids=[];
-      if(params.dataIndex===0){
-        [15,30,45].forEach(m=>kids.push({type:'circle',
-          shape:{cx,cy,r:rOf(m)},
-          style:{fill:'none',stroke:'#D8D6CE',lineWidth:1,lineDash:[3,4]}}));
-      }
-      kids.push({type:'sector',
-        shape:{cx,cy,r0,r:rOf(s.mins),startAngle:s.p0,endAngle:s.p1,clockwise:true,cornerRadius:5},
-        style:{fill:s.color,stroke:PAPER,lineWidth:3},
-        enterFrom:{shape:{r:r0}}});
-      kids.push({type:'text',style:{
-        x:cx+cos*lr, y:cy+Math.sin(pm)*lr,
-        text:s.n+'  '+s.share+'% · '+s.mins+'m',
-        font:'600 10px Inter',fill:'#55544E',
-        align:Math.abs(cos)<.25?'center':(cos>0?'left':'right'),
-        verticalAlign:'middle'},
-        enterFrom:{style:{opacity:0}}});
-      return {type:'group',children:kids};
-    },
-    data:slices.map(s=>s.share),
-  }],
-});
-})();
-
-// ════ mono-fancy3 · 3: single-axis small multiples ════
-(()=>{
-const days=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-const hours=Array.from({length:24},(_,i)=>i);
-const mkRow=di=>hours.map(h=>{
-  const workday=di<5, core=h>=9&&h<=17;
-  let v=Math.round((workday&&core?6+5*Math.sin((h-9)/8*Math.PI):0)+((h+di)%5===0?2:0)+(h>=22||h<=5?0:1));
-  return [h,v];
-}).filter(d=>d[1]>0);
-eReveal('singleaxis',{
-  animationDuration:500,animationEasing:'backOut',
-  tooltip:{...tipLight,formatter:p=>days[p.seriesIndex]+' '+String(p.value[0]).padStart(2,'0')+':00 — '+p.value[1]+' tickets'},
-  title:days.map((d,i)=>({
-    text:d,textBaseline:'middle',top:(i+.5)*100/7-1+'%',left:0,
-    textStyle:{fontSize:9,fontWeight:700,fontFamily:'Inter',color:MUTED},
-  })),
-  singleAxis:days.map((d,i)=>({
-    left:52,right:14,type:'category',boundaryGap:false,
-    data:hours.map(h=>String(h).padStart(2,'0')),
-    top:i*100/7+6+'%',height:100/7-8+'%',
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:i===6,color:MUTED,fontFamily:'Inter',fontSize:8.5,interval:3},
-    splitLine:{show:false},
-  })),
-  series:days.map((d,i)=>({
-    singleAxisIndex:i,coordinateSystem:'singleAxis',
-    type:'scatter',
-    data:mkRow(i),
-    symbolSize:dd=>dd[1]*2.6,
-    itemStyle:{color:i<5?L[Math.min(4,i)]:'#C6C5BF'},
-    animationDelay:di=>i*160+di*14,
-  })),
-});
-})();
-
-// ════ mono-fancy3 · 4: jitter strip ════
-(()=>{
-const cats=['P0 CRITICAL','P1 HIGH','P2 NORMAL','P3 LOW'];
-const mkPts=(ci,n,median,spread,outliers)=>{
-  const pts=[];
-  for(let i=0;i<n;i++){
-    const u=rnd(i+1,ci+1), v=rnd(i+7,ci+3);
-    let x=median+ (u-.5)*spread*2 + (v>.5?u*spread*.6:0);
-    if(i<outliers) x=median+spread*2.2+u*spread*3;
-    const jy=ci+ (rnd(i+13,ci+5)-.5)*.58;
-    pts.push([Math.max(.2,x),jy]);
-  }
-  return pts;
-};
-const groups=[
-  mkPts(0,38,.8,.5,2),
-  mkPts(1,64,2.4,1.1,3),
-  mkPts(2,110,6.5,2.4,4),
-  mkPts(3,72,14,4.5,3),
-];
-const flat=groups.flat();
-eReveal('jitter',{
-  animationDuration:450,animationEasing:'cubicOut',
-  animationDelay:i=>i<flat.length?Math.round(flat[i][1])*260+(i%37)*9:0,
-  tooltip:{...tipLight,formatter:p=>cats[Math.round(p.value[1])]+' — '+p.value[0].toFixed(1)+'h to resolve'},
-  grid:{left:86,right:16,top:14,bottom:30},
-  xAxis:{type:'value',name:'HOURS TO RESOLVE',nameTextStyle:{color:'#C6C5BF',fontSize:8.5},
-    splitLine:{lineStyle:{color:'#DEDDD6'}},
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:9.5,formatter:v=>v+'h'}},
-  yAxis:{type:'value',min:-.6,max:3.6,inverse:true,
-    splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{show:false}},
-  series:[{
-    type:'scatter',
-    data:flat,
-    symbolSize:7,
-    itemStyle:{color:p=>L[Math.round(p.value[1])],opacity:.62},
-  },{
-    type:'scatter',silent:true,symbolSize:0,
-    data:cats.map((c,i)=>({value:[0,i],label:{show:true,position:'left',offset:[-6,0],
-      color:'#6A6963',fontFamily:'Inter',fontSize:9,fontWeight:700,formatter:c}})),
-  }],
-});
-})();
-
-// ════ mono-fancy4 · 1: bar race ════
-(()=>{
-const P=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync'];
-const YEARS=[2019,2020,2021,2022,2023,2024,2025,2026];
-const vals=[P.map((_,i)=>[42,38,30,26,18,14,10,8][i])];
-for(let y=1;y<YEARS.length;y++)
-  vals.push(vals[y-1].map((v,i)=>v*(1.04+rnd(i+1,y+1)*.5)));
-obsReveal('race',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let step=0;
-  const frame=()=>{
-    const row=vals[step];
-    const rank=[...row].map((v,i)=>[v,i]).sort((a,b)=>b[0]-a[0]).map(d=>d[1]);
-    g.setOption({
-      animationDuration:0,animationDurationUpdate:950,animationEasingUpdate:'linear',
-      tooltip:{...tipLight,valueFormatter:v=>'$'+Math.round(v)+'K'},
-      grid:{left:64,right:64,top:8,bottom:8},
-      xAxis:{type:'value',max:'dataMax',
-        splitLine:{show:false},axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false}},
-      yAxis:{type:'category',data:P,inverse:true,max:7,
-        animationDuration:300,animationDurationUpdate:300,
-        axisLine:{show:false},axisTick:{show:false},
-        axisLabel:{color:'#6A6963',fontFamily:'Inter',fontSize:9.5,fontWeight:600}},
-      series:[{
-        type:'bar',realtimeSort:true,barCategoryGap:'32%',
-        data:row.map((v,i)=>({value:v,
-          itemStyle:{color:L[Math.min(5,rank.indexOf(i))],borderRadius:99}})),
-        label:{show:true,position:'right',valueAnimation:true,
-          fontFamily:'Inter',fontSize:12,fontWeight:800,color:INK,
-          formatter:p=>'$'+Math.round(p.value)+'K'},
-      }],
-      graphic:[{id:'yr',type:'text',right:18,bottom:14,
-        style:{text:YEARS[step],font:'800 44px Inter',fill:'#DEDDD6'}}],
-    });
-    step=(step+1)%YEARS.length;
-  };
-  frame();
-  keep('race',setInterval(frame,1150));
-});
-})();
-
-// ════ mono-fancy4 · 2: dynamic data stream ════
-(()=>{
-const WIN=50;
-obsReveal('stream',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  let t=0,v=64;
-  const data=[];
-  for(;t<WIN;t++){v=Math.max(30,v+(rnd(t+1,7)-.48)*9);data.push(Math.round(v))}
-  const draw=()=>g.setOption({
-    animation:true,animationDuration:0,animationDurationUpdate:260,animationEasingUpdate:'linear',
-    tooltip:{...tipLight,valueFormatter:x=>x+'k users'},
-    grid:{left:14,right:58,top:44,bottom:20},
-    xAxis:{type:'category',data:data.map((_,i)=>t-WIN+i),boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},axisLabel:{show:false},splitLine:{show:false}},
-    yAxis:{show:false,min:20,max:130},
-    series:[{
-      type:'line',data:[...data],smooth:.45,symbol:'none',
-      lineStyle:{color:INK,width:2.4},
-      endLabel:{show:true,fontFamily:'Inter',fontSize:14,fontWeight:800,color:INK,
-        formatter:p=>p.value+'k'},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:'rgba(28,28,26,.16)'},{offset:1,color:'rgba(28,28,26,0)'}]}},
-    }],
-    graphic:[{type:'group',left:16,top:10,children:[
-      {type:'circle',shape:{cx:5,cy:5,r:4},style:{fill:'#4A4944'}},
-      {type:'text',style:{text:'LIVE',x:16,y:0,font:'800 11px Inter',fill:INK}},
-    ]}],
-  });
-  draw();
-  keep('stream',setInterval(()=>{
-    v=Math.max(30,Math.min(125,v+(rnd(t+1,7)-.48)*9));
-    data.push(Math.round(v));data.shift();t++;
-    draw();
-  },300));
-});
-})();
-
-// ════ mono-fancy4 · 3: draw-in + counter ════
-(()=>{
-const days=Array.from({length:180},(_,i)=>i);
-const daily=days.map(i=>14+10*Math.sin(i/29)+i*.12+rnd(i+1,3)*6);
-const cum=[];daily.reduce((a,b,i)=>(cum[i]=a+b,cum[i]),0);
-const TOTAL=cum[cum.length-1];
-const DUR=2600;
-let gen=0; // replay guard: only the latest counter loop may write
-obsReveal('stroke',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();
-  gen++;const my=gen;
-  g.setOption({
-    animationDuration:DUR,animationEasing:'cubicOut',
-    tooltip:{show:false},
-    grid:{left:14,right:20,top:64,bottom:24},
-    xAxis:{type:'category',data:days,boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
-      axisLabel:{color:'#C6C5BF',fontFamily:'Inter',fontSize:9,fontWeight:600,
-        interval:29,formatter:i=>['JAN','FEB','MAR','APR','MAY','JUN'][Math.floor(i/30)]||''}},
-    yAxis:{show:false,max:TOTAL*1.06},
-    series:[{
-      type:'line',data:cum.map(x=>Math.round(x)),smooth:.2,symbol:'none',
-      lineStyle:{color:INK,width:2.6},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:'rgba(28,28,26,.14)'},{offset:1,color:'rgba(28,28,26,0)'}]}},
-    }],
-  });
-  const t0=performance.now();
-  const tick=()=>{
-    if(my!==gen)return;
-    const p=Math.min(1,(performance.now()-t0)/DUR);
-    const eased=1-Math.pow(1-p,3);
-    const val=(TOTAL*eased/1000).toFixed(2);
-    g.setOption({graphic:[{id:'kpi',type:'group',left:16,top:8,children:[
-      {type:'text',style:{text:'$'+val+'M',font:'800 32px Inter',fill:INK}},
-      {type:'text',style:{text:'ARR · H1 2026',y:38,font:'600 10px Inter',fill:'#8F8E88'}},
-    ]}]});
-    if(p<1)requestAnimationFrame(tick);
-  };
-  requestAnimationFrame(tick);
-});
-})();
-
-addEventListener('resize',()=>{
-  ['rose','forest','circular','tree','rainfall','morph','negbar','force','wave',
-   'custompie','singleaxis','jitter','race','stream','stroke']
-    .forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ G19 · violin ════
-// 小提琴:提前聚合的密度剪影,Glance 世界观(beeswarm 的姊妹)。
-// 四个 plan 各一把琴,宽度 = 该时长的工单密度(KDE),
-// 明度按中位数快慢排:越快越黑。纸色横档 = 中位数。
-(()=>{
-const G=[['ENT',1.6,.8],['PRO',3.4,1.6],['STARTER',6.2,2.6],['FREE',9.5,4.2]];
-const M=64;
-const samples=g=>Array.from({length:M},(_,i)=>{
-  const [,c,s]=G[g];
-  const v=c+s*((rnd(i+1,g*3+1)+rnd(i+7,g*3+2)+rnd(i+13,g*3+3))-1.5);
-  return Math.max(.2,v);
-});
-obsReveal('violin',s=>{
-  const TOP=40,BASE=262,HMAX=17;
-  const mapY=h=>BASE-h/HMAX*(BASE-TOP);
-  const colX=g=>76+g*84;
-  // hour grid
-  [0,4,8,12,16].forEach(h=>{
-    el(s,'line',{x1:34,y1:mapY(h),x2:378,y2:mapY(h),stroke:'#E3E2DB','stroke-width':.8,
-      class:'fade',style:`animation-delay:${h*.01}s`});
-    txt(s,{x:28,y:mapY(h)+3,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${h*.01}s`},h+'h');
-  });
-  const SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9'];
-  G.forEach(([name],g)=>{
-    const vs=samples(g).sort((a,b)=>a-b);
-    const med=vs[Math.floor(M/2)];
-    // fuller bandwidth = the classic rounded violin belly, not a spindle
-    const bw=Math.max(.9,G[g][2]*.72);
-    // pad past the data by 1.6 bandwidths so the density decays to ~0 and the
-    // ends taper naturally — clipping tighter gives flat-cut lantern ends
-    const lo=Math.max(0,vs[0]-bw*1.6),hi=Math.min(HMAX,vs[M-1]+bw*1.6);
-    const NPTS=44;
-    const grid=Array.from({length:NPTS},(_,k)=>lo+k/(NPTS-1)*(hi-lo));
-    const dens=grid.map(h=>vs.reduce((a,v)=>a+Math.exp(-((h-v)**2)/(2*bw*bw)),0));
-    const dmax=Math.max(...dens);
-    const W=30;
-    const x=colX(g);
-    // smooth closed outline through the mirrored density — quadratic
-    // midpoint smoothing, same trick as almanac's blob
-    const pts=[];
-    grid.forEach((h,k)=>pts.push([x+dens[k]/dmax*W,mapY(h)]));
-    for(let k=NPTS-1;k>=0;k--)pts.push([x-dens[k]/dmax*W,mapY(grid[k])]);
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<pts.length;t++){
-      const p=pts[t],q=pts[(t+1)%pts.length];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    const body=el(s,'path',{d:d+' Z',fill:SHADE[g],class:'fade',style:`animation-delay:${g*.14}s`});
-    tip(body,`${name} — median ${med.toFixed(1)}h to first reply`);
-    // median: paper tick across the body, value parked right
-    const mk=Math.round((med-lo)/(hi-lo)*(NPTS-1));
-    const wAtMed=dens[Math.max(0,Math.min(NPTS-1,mk))]/dmax*W;
-    el(s,'line',{x1:x-wAtMed,y1:mapY(med),x2:x+wAtMed,y2:mapY(med),
-      stroke:PAPER,'stroke-width':2.2,class:'fade',style:`animation-delay:${.5+g*.14}s`});
-    txt(s,{x:x+W+5,y:mapY(med)+3,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.6+g*.14}s`},med.toFixed(1)+'h');
-    txt(s,{x,y:BASE+18,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${g*.14}s`},name);
-  });
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'WIDTH = HOW MANY TICKETS WAIT THAT LONG · PAPER TICK = MEDIAN · DARKEST = FASTEST');
-});
-})();
-
-// ════ G20 · matrix heat ════
-// 矩阵热力的 Glance 皮:粗笔画版本。大圆角色块、每格标数、3 秒读出
-// 「新版本跑得黑」的对角线。和 P1 Lupi 皮同数据形状,读者时间不同。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT'];
-const VER=['v2.0','v1.9','v1.8','v1.7','v1.6'];
-const BASEV=[88,74,61,42,35,27];
-const v=(r,c)=>Math.max(2,Math.round(BASEV[c]-r*(6+c*.8)+(rnd(r*6+c+1,c+3)-.5)*10));
-const shade=t=>t>64?INK:t>46?'#4A4944':t>30?'#8F8E88':t>15?'#B0AFA9':'#D8D7D1';
-obsReveal('gheat',s=>{
-  const X0=92,Y0=58,PX=48,PY=42,W=42,H=36;
-  FEAT.forEach((f,c)=>{
-    txt(s,{x:X0+c*PX+W/2,y:Y0-12,'font-size':6.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${c*.04}s`},f);
-  });
-  VER.forEach((ver,r)=>{
-    txt(s,{x:X0-10,y:Y0+r*PY+H/2+3,'font-size':8.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      class:'fade',style:`animation-delay:${r*.05}s`},ver);
-    FEAT.forEach((f,c)=>{
-      const t=v(r,c),x=X0+c*PX,y=Y0+r*PY,dark=t>46;
-      const cell=el(s,'rect',{x,y,width:W,height:H,rx:9,fill:shade(t),
-        class:'pop',style:`animation-delay:${(r*6+c)*.022}s`});
-      tip(cell,`${f} on ${ver} — ${t}% of accounts`);
-      txt(s,{x:x+W/2,y:y+H/2+4,'font-size':10.5,'font-weight':800,fill:dark?PAPER:INK,'text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.15+(r*6+c)*.022}s`},t);
-    });
-  });
-  txt(s,{x:200,y:300,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1s'},
-    'SHADE = ADOPTION · NEWER VERSIONS RUN DARKER');
-});
-})();
-
-// ════ G21 · rank strip ════
-// 排名随时间变的静态版,第二稿。第一稿是泳道 bump——灰线 X 交叉,
-// 和 slope 同一种丑(catalog 实测三连败那条),废弃。
-// 这版不画线:一行一个产品,一格一个季度,明度 = 名次(#1 最黑)。
-// 攀升者的行自己从浅变黑,故事读行内渐变,不读交叉。
-(()=>{
-const RANKS=[['Flows',[5,3,2,1,1,1]],['Editor',[1,1,1,2,2,2]],['Boards',[2,2,3,3,3,4]],
-  ['Vault',[4,5,5,4,4,3]],['Docs',[3,4,4,5,5,5]]];
-const SHADE=[INK,'#4A4944','#8F8E88','#B0AFA9','#D8D7D1'];   // rank 1 → 5
-obsReveal('rankstrip',s=>{
-  // rows sorted by final rank, the climber lands on top
-  const rows=[...RANKS].sort((a,b)=>a[1][5]-b[1][5]);
-  const X0=110,PX=38,Y0=64,PY=42,C=32;
-  ['Q1','Q2','Q3','Q4','Q5','Q6'].forEach((q,k)=>
-    txt(s,{x:X0+k*PX+C/2,y:Y0-12,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${k*.04}s`},q));
-  rows.forEach(([p,rk],i)=>{
-    const y=Y0+i*PY,hero=p==='Flows';
-    txt(s,{x:X0-12,y:y+C/2+3,'font-size':8.5,'font-weight':hero?800:600,fill:hero?INK:'#6A6963','text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.07}s`},p);
-    rk.forEach((r,q)=>{
-      const dark=r<=2;
-      const cell=el(s,'rect',{x:X0+q*PX,y,width:C,height:C,rx:8,fill:SHADE[r-1],
-        stroke:'#CFCEC7','stroke-width':.5,
-        class:'pop',style:`animation-delay:${i*.07+q*.035}s`});
-      tip(cell,`${p} — #${r} in Q${q+1}`);
-      txt(s,{x:X0+q*PX+C/2,y:y+C/2+3.5,'font-size':9.5,'font-weight':800,
-        fill:dark?PAPER:'#55544E','text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.1+i*.07+q*.035}s`},r);
-    });
-    // arrival flag: climbed / fell / held, told at the row's end
-    const d=rk[0]-rk[5];
-    txt(s,{x:X0+6*PX+8,y:y+C/2+3,'font-size':8,'font-weight':800,
-      fill:d>0?INK:d<0?'#B0AFA9':'#C6C5BF',
-      class:'fade',style:`animation-delay:${.5+i*.07}s`},
-      d>0?'▲'+d:d<0?'▼'+(-d):'—');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'CELL = RANK THAT QUARTER · #1 WEARS BLACK · READ EACH ROW LIKE A FILMSTRIP');
-});
-})();
-
-// ════ G22 · aggregate sankey ════
-// B3 Threads 是逐条丝线(每线一条记录);这张是聚合版——
-// 缎带宽度∝账号数。左渠道右套餐,缎带按渠道明度分。
-// 交叉处必须读得清,缎带走 .5 透明度(同 convergence 连线的处理)。
-(()=>{
-const SRC=[['SEARCH',[20,10,4]],['REFERRAL',[12,9,6]],['SOCIAL',[12,4,2]],['PAID',[6,3,3]],['OTHER',[5,2,2]]];
-const DST=['FREE','PRO','TEAM'];
-const SH=[INK,'#4A4944','#8F8E88','#B0AFA9','#C6C5BF'];
-obsReveal('sankey',s=>{
-  const XL=104,XR=298,Y0=34,SC=1.9,GAPL=8,GAPR=22,MID=(XL+XR)/2;
-  const srcTot=SRC.map(d=>d[1].reduce((a,b)=>a+b,0));
-  const dstTot=DST.map((_,j)=>SRC.reduce((a,d)=>a+d[1][j],0));
-  const sy=[],dy=[];
-  let y=Y0;srcTot.forEach((t,i)=>{sy[i]=y;y+=t*SC+GAPL});
-  y=Y0+2;dstTot.forEach((t,j)=>{dy[j]=y;y+=t*SC+GAPR});
-  const syc=[...sy],dyc=[...dy];
-  SRC.forEach((d,i)=>{
-    d[1].forEach((v,j)=>{
-      if(!v)return;
-      const sa=syc[i],sb=sa+v*SC,da=dyc[j],db=da+v*SC;
-      syc[i]=sb;dyc[j]=db;
-      const rib=el(s,'path',{d:`M${XL} ${sa} C${MID} ${sa} ${MID} ${da} ${XR} ${da} L${XR} ${db} C${MID} ${db} ${MID} ${sb} ${XL} ${sb} Z`,
-        fill:SH[i],opacity:.5,class:'fade',style:`animation-delay:${.2+(i*3+j)*.06}s`});
-      tip(rib,`${SRC[i][0]} → ${DST[j]} — ${v} accounts of 100`);
-    });
-  });
-  SRC.forEach((d,i)=>{
-    el(s,'rect',{x:XL-9,y:sy[i],width:9,height:srcTot[i]*SC,rx:4,fill:SH[i],
-      class:'fade',style:`animation-delay:${i*.06}s`});
-    txt(s,{x:XL-16,y:sy[i]+srcTot[i]*SC/2+2.5,'font-size':7.5,'font-weight':700,fill:'#4A4944','text-anchor':'end',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${i*.06}s`},`${d[0]} · ${srcTot[i]}`);
-  });
-  DST.forEach((n,j)=>{
-    el(s,'rect',{x:XR,y:dy[j],width:9,height:dstTot[j]*SC,rx:4,fill:'#3A3934',
-      class:'fade',style:`animation-delay:${.1+j*.06}s`});
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2-3,'font-size':7.5,'font-weight':700,fill:'#4A4944',
-      'letter-spacing':'.05em',class:'fade',style:`animation-delay:${.15+j*.06}s`},n);
-    txt(s,{x:XR+16,y:dy[j]+dstTot[j]*SC/2+9,'font-size':10,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.2+j*.06}s`},dstTot[j]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'RIBBON WIDTH = ACCOUNTS OF 100 · SHADE = CHANNEL');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 1220
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/lupi-gallery.html

@@ -1,1220 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Charts — Lupi Gallery · 20 张参考实现</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  :root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.dark{background:var(--dark);color:#F0EFEB}
-  .card.dark .sub{color:#8F8E88}
-  .card.dark .src{color:#55554F}
-  .card.wide{grid-column:1/-1}
-  .split{display:grid;grid-template-columns:250px 1fr;gap:30px;align-items:start}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .note{font-size:11px;color:#6A6963;line-height:1.65;margin-bottom:16px}
-  .legend{font-size:9px;letter-spacing:.1em;font-weight:600;color:var(--muted);line-height:2}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
-  .pagehead .badge{margin:0 2px}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px}
-  .badge.del{background:#1C1C1A;color:#F0EFEB}
-  .badge.pick{border:1.5px solid #1C1C1A;color:#1C1C1A}
-  .badge.pair{border:1px dashed #B0AFA9;color:#8F8E88}
-  .card.dark .badge.del{background:#F0EFEB;color:#1C1C1A}
-  .card.dark .badge.pick{border-color:#F0EFEB;color:#F0EFEB}
-  .card.dark .badge.pair{border-color:#55554F;color:#8F8E88}
-</style>
-</head>
-<body>
-<div class="pagehead">
-
-  <h1>Lupi 系(细读)· 20 张参考实现</h1>
-  <p>发丝线 · 逐记录 · 30 秒阅读。</p>
-</div>
-<div class="grid2">
-
-  <!-- 1 · launch fan -->
-  <div class="card">
-    <h2>Twelve features, fanned out</h2>
-    <div class="sub">one spoke per feature · big dot = launch week, sized by MAU today · small dots = every week since</div>
-    <svg id="fan" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">LAUNCH FAN · MONO-EDITORIAL · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- 4 · dot cascade on dark -->
-  <div class="card dark">
-    <h2>What breaks, stacked and ranked</h2>
-    <div class="sub">incidents by root cause · 2019 → 2026 · one dot = 2 incidents · sorted ascending</div>
-    <svg id="cascade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">DOT CASCADE · MONO-EDITORIAL · INCIDENT LOG</div>
-  </div>
-
-  <!-- 2 · barcode lollipop, wide + editorial text column -->
-  <div class="card wide">
-    <div class="split">
-      <div>
-        <h2>Ninety days as a barcode</h2>
-        <div class="sub">peak concurrent users, daily · Apr – Jun 2026</div>
-        <div class="note">Every hairline is a day, whether or not anything
-        happened in it. The dot marks the day's peak; the stem below it is
-        just gravity. Read the field, not the numbers — the season has a
-        texture before it has a value.</div>
-        <div class="legend">
-          ● &nbsp;WEEKDAY PEAK<br>
-          ○ &nbsp;WEEKEND PEAK<br>
-          │ &nbsp;ONE CALENDAR DAY<br>
-          ◉ &nbsp;TOP-3, LABELED
-        </div>
-      </div>
-      <div>
-        <svg id="barcode" viewBox="0 0 800 300" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-        <div class="src">BARCODE LOLLIPOP · MONO-EDITORIAL · LIVE OPS</div>
-      </div>
-    </div>
-  </div>
-
-  <!-- 3 · arc bubble matrix -->
-  <div class="card">
-    <h2>Eight products land in twelve cities</h2>
-    <div class="sub">bubble = paying accounts · each row bows like a horizon</div>
-    <svg id="arcmatrix" viewBox="0 0 430 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">ARC MATRIX · MONO-EDITORIAL · SALES GEO</div>
-  </div>
-
-  <!-- 5 · radial convergence -->
-  <div class="card">
-    <h2>48 requests pull toward five themes</h2>
-    <div class="sub">every rim node = a feature request · lines bundle into the theme that claimed it</div>
-    <svg id="converge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL CONVERGENCE · MONO-EDITORIAL · FEEDBACK BOARD</div>
-  </div>
-
-  <!-- 6 · cluster field, wide -->
-  <div class="card wide">
-    <h2>The contributor field</h2>
-    <div class="sub">80 core contributors orbit the main repo · satellite islands keep their own gravity · dotted paths = cross-contribution</div>
-    <svg id="clusters" viewBox="0 0 840 320" preserveAspectRatio="xMidYMid meet" style="max-height:360px"></svg>
-    <div class="src">CLUSTER FIELD · MONO-EDITORIAL · CONTRIBUTOR GRAPH</div>
-  </div>
-
-
-
-  <!-- 1 · brand spectrum -->
-  <div class="card">
-    <h2>Where the brand sits</h2>
-    <div class="sub">panel of 120 customers placed us between opposing traits · big dot = us · small dots = competitors</div>
-    <svg id="spectrum" viewBox="0 0 460 300" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BRAND SPECTRUM · MONO-EDITORIAL2 · BRAND SURVEY</div>
-  </div>
-
-  <!-- 2 · dotty matrix -->
-  <div class="card">
-    <h2>Four squads, stacked in space</h2>
-    <div class="sub">each plane = one squad · 6 × 6 weeks of activity · dot area = tasks closed</div>
-    <svg id="dotty" viewBox="0 0 400 372" preserveAspectRatio="xMidYMid meet" style="max-height:340px"></svg>
-    <div class="src">DOTTY MATRIX · MONO-EDITORIAL2 · SPRINT BOARD</div>
-  </div>
-
-  <!-- 3 · ticket almanac, wide -->
-  <div class="card wide">
-    <h2>Eight years of tickets, one almanac</h2>
-    <div class="sub">bubble area = support tickets per product area, per year · bubbles overlap where years pile up · dark core = escalations · dashed ring = still in beta</div>
-    <svg id="almanac" viewBox="0 0 840 400" preserveAspectRatio="xMidYMid meet" style="max-height:420px"></svg>
-    <div class="src">BUBBLE ALMANAC · MONO-EDITORIAL2 · SUPPORT DESK</div>
-  </div>
-
-  <!-- 4 · deploy patchwork -->
-  <div class="card">
-    <h2>A quarter of deploys, overlaid</h2>
-    <div class="sub">each wedge = one deploy · angle = time of day · reach = files touched · ink builds where deploys pile up</div>
-    <svg id="patchwork" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RADIAL PATCHWORK · MONO-EDITORIAL2 · DEPLOY LOG</div>
-  </div>
-
-  <!-- 6 · trend lineage -->
-  <div class="card">
-    <h2>Features rise, fall, come back</h2>
-    <div class="sub">● shipped · ○ reworked · dashed = dormant years · line runs on if it's still alive today</div>
-    <svg id="lineage" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TREND LINEAGE · MONO-EDITORIAL2 · PRODUCT HISTORY</div>
-  </div>
-
-  <!-- 8 · type colonnade -->
-  <div class="card">
-    <h2>Forty-four repos, ten owners</h2>
-    <div class="sub">every hairline hands a repo to the team that owns it · the pile-ups are the org chart nobody drew</div>
-    <svg id="colonnade" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">TYPE COLONNADE · MONO-EDITORIAL2 · CODE OWNERSHIP</div>
-  </div>
-
-  <!-- 9 · funnel hourglass -->
-  <div class="card">
-    <h2>The funnel, poured</h2>
-    <div class="sub">each strip's width = people at that stage · threads trickle down · Q2 2026 cohort</div>
-    <svg id="hourglass" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HOURGLASS STREAM · MONO-EDITORIAL2 · GROWTH</div>
-  </div>
-
-  <!-- 14 · hundred field -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>A hundred of us, four minds</h2>
-    <div class="sub">one dot = one person in a hundred · four attitude clusters, sized by share · 2026 survey</div>
-    <svg id="hundredfield" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">HUNDRED FIELD · MONO-SMALL · ATTITUDE SURVEY</div>
-  </div>
-
-  <!-- 15 · ballot tally -->
-  <div class="card">
-    <span class="badge pair">小数据 · 单位分解</span>
-    <h2>What they fear, tick by tick</h2>
-    <div class="sub">one tick = one respondent in a hundred · inked = picked it · they could pick several</div>
-    <svg id="ballottally" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BALLOT TALLY · MONO-SMALL · WORKPLACE SURVEY</div>
-  </div>
-
-
-  <!-- L16 · matrix heat -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 矩阵热力</span>
-    <h2>Which features get used together</h2>
-    <div class="sub">8 × 8 co-usage · shade = share of accounts using both · dashed square = strongest pair</div>
-    <svg id="matheat" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">MATRIX HEAT · MONO-EDITORIAL · PRODUCT ANALYTICS</div>
-  </div>
-
-  <!-- L17 · calendar heat -->
-  <div class="card wide">
-    <span class="badge">LUPI 编辑型 · 日历热力</span>
-    <h2>A year of deploys, day by day</h2>
-    <div class="sub">52 weeks × 7 days · dot area = deploys that day · tiny dot = a quiet day · dashed ring = the busiest day of 2025</div>
-    <svg id="calheat" viewBox="0 0 840 232" preserveAspectRatio="xMidYMid meet" style="max-height:260px"></svg>
-    <div class="src">CALENDAR HEAT · MONO-EDITORIAL · DEPLOY LOG</div>
-  </div>
-
-  <!-- L18 · beeswarm -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 蜂群</span>
-    <h2>A hundred and twenty deals, swarming</h2>
-    <div class="sub">one dot = one closed deal · the swarm piles up where deals cluster · hollow = enterprise · dashed = median</div>
-    <svg id="beeswarm" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">BEESWARM · MONO-EDITORIAL · SALES OPS</div>
-  </div>
-
-  <!-- L19 · ridgeline -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · RIDGELINE</span>
-    <h2>Five pipelines, five tempos</h2>
-    <div class="sub">each crest = one pipeline's completion-time shape · the area is made of hairlines · dot = the peak</div>
-    <svg id="ridge" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">RIDGELINE · MONO-EDITORIAL · DATA PLATFORM</div>
-  </div>
-
-  <!-- L20 · parallel coordinates -->
-  <div class="card">
-    <span class="badge">LUPI 编辑型 · 平行坐标</span>
-    <h2>Twelve products, four dimensions</h2>
-    <div class="sub">one hairline = one product across price · satisfaction · retention · growth · ink = the one that wins everywhere</div>
-    <svg id="parallel" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg>
-    <div class="src">PARALLEL COORDS · MONO-EDITORIAL · PRODUCT PANEL</div>
-  </div>
-
-</div>
-<script>
-// ── shared Mono tokens ──
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000; // deterministic
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const sect=(cx,cy,r0,r1,a0,a1)=>{
-  const big=a1-a0>180?1:0;
-  const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
-  const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
-  return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
-};
-// unified reveal: draw when scrolled into view; click replays
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ 1 · launch fan ════
-// spokes sorted by launch week → the big dots cascade along a diagonal arc
-(()=>{
-const FEAT=['Editor','Boards','Docs','Chat','Flows','Vault','Pages','Sync','Grid','Views','Hub','Forms'];
-const LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19];     // week of launch
-const CX=40,CY=304,rOf=w=>56+w*9.7;
-obsReveal('fan',s=>{
-  const ang=i=>-84+i*(76/11);
-  // dotted week guides
-  [5,10,15,20].forEach((w,gi)=>{
-    const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);
-    el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',
-      stroke:'#D8D6CE','stroke-width':1,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${gi*.1}s`});
-    const [lx,ly]=pol(CX,CY,r,-87.5);
-    txt(s,{x:lx,y:ly,'font-size':6.5,fill:'#B0AFA9','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${gi*.1}s`},'W'+w);
-  });
-  FEAT.forEach((name,i)=>{
-    const a=ang(i),L=LAUNCH[i];
-    const mau=Math.round(48-i*3+rnd(i+1,3)*10);
-    // hairline spoke from launch to rim
-    const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);
-    el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:'#E3E2DB','stroke-width':.7,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`});
-    // weekly texture dots
-    for(let w=L+1;w<=20;w++){
-      const [x,y]=pol(CX,CY,rOf(w),a);
-      el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:'#B0AFA9',opacity:.8,
-        class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});
-    }
-    // launch dot, sized by MAU
-    const [bx,by]=pol(CX,CY,rOf(L),a);
-    const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:INK,
-      class:'pop',style:`animation-delay:${.15+i*.07}s`});
-    tip(big,`${name} — launched W${L} · ${mau}k MAU`);
-    // rim label along the spoke
-    const [tx,ty]=pol(CX,CY,rOf(20)+9,a);
-    txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:'#6A6963',
-      transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',
-      class:'fade',style:`animation-delay:${.3+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 4 · dot cascade on dark ════
-(()=>{
-const CAUSE=['DNS','CDN','QUOTA','DISK','CERT','CACHE','QUEUE','LOCK','OOM','NET','3P API','CONFIG','DB','DEPLOY','CODE','HUMAN'];
-const COUNT=[1,1,2,3,3,4,5,6,7,9,11,13,15,18,22,27];
-obsReveal('cascade',s=>{
-  const x=i=>34+i*22.2, base=i=>272-i*7.5;
-  // dotted diagonal baseline
-  el(s,'line',{x1:x(0)-8,y1:base(0)+4,x2:x(15)+8,y2:base(15)+4,
-    stroke:'#4A4944','stroke-width':1,'stroke-dasharray':'2 4',class:'fade'});
-  CAUSE.forEach((name,i)=>{
-    const n=Math.ceil(COUNT[i]/2),bx=x(i),by=base(i);
-    for(let k=0;k<n-1;k++)
-      el(s,'circle',{cx:bx,cy:by-12-k*8.8,r:2.2,fill:'#B3B0A4',opacity:.9,
-        class:'pop',style:`animation-delay:${i*.05+k*.03}s`});
-    const topY=by-12-(n-1)*8.8;
-    const top=el(s,'circle',{cx:bx,cy:topY,r:4.6,fill:PAPER,
-      class:'pop',style:`animation-delay:${i*.05+n*.03}s`});
-    tip(top,`${name} — ${COUNT[i]} incidents`);
-    txt(s,{x:bx,y:topY-10,'font-size':9,'font-weight':700,fill:PAPER,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.2+i*.05}s`},COUNT[i]);
-    txt(s,{x:bx,y:by+12,'font-size':6.5,'font-weight':600,fill:'#6A6963','text-anchor':'end',
-      transform:`rotate(-90 ${bx} ${by+12})`,
-      class:'fade',style:`animation-delay:${.1+i*.05}s`},name);
-  });
-});
-})();
-
-// ════ 2 · barcode lollipop ════
-(()=>{
-const N=90;
-const val=d=>95+55*Math.sin(d/9.5)+30*Math.sin(d/3.7)+rnd(d+1,5)*40;
-obsReveal('barcode',s=>{
-  // the barcode: one hairline per day, full height
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53;
-    el(s,'line',{x1:x,y1:8,x2:x,y2:258,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${d*.004}s`});
-  }
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  // top-3 peaks, kept at least 6 days apart so labels never collide
-  const top3=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);
-    if(top3.length===3)break;
-  }
-  for(let d=0;d<N;d++){
-    const x=18+d*8.53,v=vs[d],y=252-v*.75;
-    const weekend=d%7===5||d%7===6;
-    const stemEnd=Math.min(256,y+14+rnd(d+1,9)*26);
-    el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:INK,'stroke-width':1.1,
-      class:'fade',style:`animation-delay:${.3+d*.008}s`});
-    const big=top3.includes(d);
-    const dot=el(s,'circle',{cx:x,cy:y,r:big?4.6:2.7,
-      fill:weekend?PAPER:INK,stroke:INK,'stroke-width':weekend?1.2:0,
-      class:'pop',style:`animation-delay:${.3+d*.008}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)}k peak`);
-    if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:INK,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.9+d*.004}s`},Math.round(v)+'k');
-  }
-  [[0,'APR'],[30,'MAY'],[61,'JUN']].forEach(([d,m])=>
-    txt(s,{x:18+d*8.53,y:276,'font-size':8,'font-weight':600,fill:MUTED,
-      'letter-spacing':'.12em',class:'fade'},m));
-});
-})();
-
-// ════ 3 · arc bubble matrix ════
-(()=>{
-const PROD=['Editor','Boards','Docs','Flows','Chat','Vault','Pages','Sync'];
-const CITY=['SF','NYC','LON','BER','TOK','SYD','SIN','PAR','AMS','TOR','SEO','SAO'];
-const W=[9,8,7,7,6,5,5,4,4,3,3,2];             // market weight
-obsReveal('arcmatrix',s=>{
-  const rowY=i=>74+i*29, colX=j=>92+j*27, dy=j=>-16*Math.sin(Math.PI*j/11);
-  const v=(i,j)=>{
-    if(rnd(i*13+1,j*7+3)<.09)return 0;                 // a few true absences
-    const age=1-i*.085;                                // older rows reach further
-    return Math.min(40,Math.round(W[j]*3.4*age*(.45+rnd(i+1,j+1)*.85)));
-  };
-  // find top-4 cells for labels
-  const all=[];
-  PROD.forEach((_,i)=>CITY.forEach((_,j)=>all.push([v(i,j),i,j])));
-  const top=all.sort((a,b)=>b[0]-a[0]).slice(0,4).map(d=>d[1]*100+d[2]);
-  PROD.forEach((p,i)=>{
-    // the horizon arc
-    const d='M'+CITY.map((_,j)=>`${colX(j)} ${rowY(i)+dy(j)}`).join(' L ');
-    el(s,'path',{d,fill:'none',stroke:'#E3E2DB','stroke-width':1,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.08}s`});
-    txt(s,{x:84,y:rowY(i)+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.08}s`},p);
-    CITY.forEach((c,j)=>{
-      const x=colX(j),y=rowY(i)+dy(j),vv=v(i,j);
-      if(!vv){el(s,'circle',{cx:x,cy:y,r:.9,fill:'#D8D6CE',
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});return}
-      const fill=vv>=25?INK:vv>=12?'#6A6963':'#B0AFA9';
-      const dot=el(s,'circle',{cx:x,cy:y,r:Math.sqrt(vv)*1.3,fill,
-        class:'pop',style:`animation-delay:${.2+i*.08+j*.02}s`});
-      tip(dot,`${p} · ${c} — ${vv} accounts`);
-      if(top.includes(i*100+j))
-        txt(s,{x,y:y-Math.sqrt(vv)*1.3-4,'font-size':7,'font-weight':800,fill:INK,
-          'text-anchor':'middle',class:'fade',style:`animation-delay:${.6}s`},vv);
-    });
-  });
-  CITY.forEach((c,j)=>{
-    const x=colX(j),y=74+dy(j)-24;
-    txt(s,{x,y,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.08em',
-      transform:`rotate(-55 ${x} ${y})`,class:'fade',style:`animation-delay:${j*.03}s`},c);
-  });
-});
-})();
-
-// ════ 5 · radial convergence ════
-(()=>{
-const N=48,CX=200,CY=162,R=116;
-const THEME=[['PERF',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['UX',6,213]];
-obsReveal('converge',s=>{
-  // hub assignment: contiguous blocks with a little organic leakage
-  const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
-  const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];
-  const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
-  const counts=[0,0,0,0,0];
-  for(let i=0;i<N;i++)counts[hubOf(i)]++;
-  for(let i=0;i<N;i++){
-    const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
-    const h=hubOf(i),[hx,hy]=hubPt(h);
-    const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
-    const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
-    el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
-      fill:'none',stroke:'#A8A7A0','stroke-width':.7,opacity:.55,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.018}s`});
-    el(s,'circle',{cx:px,cy:py,r:1.6,fill:'#6A6963',
-      class:'pop',style:`animation-delay:${i*.018}s`});
-    // tiny rim code, flipped on the left half so it stays readable
-    const flip=deg>90&&deg<270,[lx,ly]=pol(CX,CY,R+7,deg);
-    txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUTED,
-      'text-anchor':flip?'end':'start','dominant-baseline':'middle',
-      transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,
-      class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));
-  }
-  THEME.forEach(([name,,deg],h)=>{
-    const [hx,hy]=hubPt(h);
-    const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,fill:INK,
-      class:'pop',style:`animation-delay:${.5+h*.08}s`});
-    tip(hub,`${name} — ${counts[h]} requests`);
-    // theme label parked outside the rim, past the code ring
-    const [tx,ty]=pol(CX,CY,R+34,deg);
-    txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
-      'dominant-baseline':'middle','letter-spacing':'.08em',
-      style:`animation-delay:${.6+h*.08}s`,
-      class:'fade'},`${name} · ${counts[h]}`);
-    // hairline tying the label to its hub, skimming past the rim
-    const [gx,gy]=pol(CX,CY,R+22,deg);
-    el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:'#C6C5BF','stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
-  });
-});
-})();
-
-// ════ 6 · cluster field ════
-(()=>{
-const SAT=[[622,78,'PLUGINS',22],[706,214,'THEMES',16],[540,268,'DOCS',13],[92,72,'FORKS',12],[774,58,'MIRRORS',9]];
-obsReveal('clusters',s=>{
-  const HX=300,HY=172;
-  // dotted cross-contribution paths, hub → each island
-  SAT.forEach(([x,y],ci)=>{
-    const mx=(HX+x)/2,my=(HY+y)/2-30;
-    el(s,'path',{d:`M${HX} ${HY} Q${mx} ${my} ${x} ${y}`,fill:'none',
-      stroke:GRID,'stroke-width':.9,'stroke-dasharray':'2 4',pathLength:1,
-      class:'draw',style:`animation-delay:${.8+ci*.12}s;animation-duration:.7s`});
-  });
-  // main hub: 80 golden-angle spokes
-  for(let i=0;i<80;i++){
-    const a=i*137.508,len=28+rnd(i+1,7)*88;
-    const [x,y]=pol(HX,HY,len,a);
-    el(s,'line',{x1:HX,y1:HY,x2:x,y2:y,stroke:'#C6C5BF','stroke-width':.6,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.012}s;animation-duration:.5s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:1.6+rnd(i+1,4)*1.8,fill:INK,opacity:.85,
-      class:'pop',style:`animation-delay:${.15+i*.012}s`});
-    tip(dot,`core contributor #${i+1}`);
-  }
-  el(s,'circle',{cx:HX,cy:HY,r:6.5,fill:INK,class:'pop'});
-  txt(s,{x:HX,y:HY+140,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
-    'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px`,
-    class:'fade'},'CORE · 80');
-  // satellite islands
-  SAT.forEach(([x,y,name,n],ci)=>{
-    el(s,'circle',{cx:x,cy:y,r:3.5,fill:'#4A4944',
-      class:'pop',style:`animation-delay:${.9+ci*.12}s`});
-    for(let k=0;k<n;k++){
-      const a=k*137.508+ci*40,rr=9+rnd(k+1,ci+2)*26;
-      const [dx,dy]=pol(x,y,rr,a);
-      if(k%2===0)el(s,'line',{x1:x,y1:y,x2:dx,y2:dy,stroke:'#D8D6CE','stroke-width':.5,
-        class:'fade',style:`animation-delay:${1+ci*.12+k*.02}s`});
-      el(s,'circle',{cx:dx,cy:dy,r:1.4+rnd(k+3,ci+5)*1.4,fill:'#6A6963',
-        class:'pop',style:`animation-delay:${1+ci*.12+k*.02}s`});
-    }
-    txt(s,{x,y:y+44,'font-size':7.5,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.12em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px`,
-      class:'fade'},`${name} · ${n}`);
-  });
-});
-})();
-
-// ════ 1 · brand spectrum ════
-// paired opposites; the pale ribbon threads our position down the page
-(()=>{
-const ROWS=[['FRIEND','AUTHORITY'],['SERIOUS','PLAYFUL'],['RELIABLE','RISK-TAKING'],['CONTEMPORARY','CLASSIC']];
-const US=[.74,.80,.40,.22];                 // 0 = left trait, 1 = right trait
-const COMP=[[.42,.52,.57],[.60,.65,.86],[.30,.34,.50],[.36,.41,.73]];
-const X0=118,X1=342,Y=i=>58+i*60,px=t=>X0+t*(X1-X0);
-obsReveal('spectrum',s=>{
-  // the ribbon first, underneath everything
-  let d=`M${px(US[0])} ${Y(0)}`;
-  for(let i=1;i<4;i++)
-    d+=` C${px(US[i-1])} ${Y(i-1)+30} ${px(US[i])} ${Y(i)-30} ${px(US[i])} ${Y(i)}`;
-  el(s,'path',{d,fill:'none',stroke:'#F0EFEB','stroke-width':30,'stroke-linecap':'round',
-    'stroke-linejoin':'round',opacity:.9,pathLength:1,class:'draw',
-    style:'animation-duration:1.4s'});
-  ROWS.forEach(([a,b],i)=>{
-    const y=Y(i);
-    el(s,'line',{x1:X0,y1:y,x2:X1,y2:y,stroke:'#B0AFA9','stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    [X0,X1].forEach(x=>el(s,'line',{x1:x,y1:y-4,x2:x,y2:y+4,stroke:'#B0AFA9','stroke-width':1,
-      class:'fade',style:`animation-delay:${i*.08}s`}));
-    txt(s,{x:X0-14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},a);
-    txt(s,{x:X1+14,y:y+3,'font-size':8,'font-weight':600,fill:'#6A6963',
-      'letter-spacing':'.1em',class:'fade',style:`animation-delay:${i*.08}s`},b);
-    COMP[i].forEach((t,k)=>{
-      const c=el(s,'circle',{cx:px(t),cy:y,r:3.2,fill:'#8F8E88',
-        class:'pop',style:`animation-delay:${.3+i*.1+k*.05}s`});
-      tip(c,`competitor ${'ABC'[k]} — ${Math.round(t*100)}% toward ${b}`);
-    });
-    const us=el(s,'circle',{cx:px(US[i]),cy:y,r:8,fill:INK,
-      class:'pop',style:`animation-delay:${.5+i*.1}s`});
-    tip(us,`us — ${Math.round(US[i]*100)}% toward ${b}`);
-  });
-  txt(s,{x:X0,y:284,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.12em',
-    class:'fade',style:'animation-delay:.9s'},'● US    ● COMPETITORS A · B · C');
-});
-})();
-
-// ════ 2 · dotty matrix — isometric stacked dot planes ════
-// pitch 20/10, dot radius capped well under half-pitch, decks 64px apart:
-// each plane reads as its own quiet grid instead of a smudge
-(()=>{
-const SQUAD=['PLATFORM','GROWTH','MOBILE','INFRA'];
-const SHADE=['#C6C5BF','#A8A7A0','#6A6963',INK];
-const P=(c,r,k)=>[196+(c-r)*20,236+(c+r)*10-k*64];
-obsReveal('dotty',s=>{
-  for(let k=0;k<4;k++){
-    // plane slab: paper fill so upper decks occlude lower ones
-    const cs=[P(-.8,-.8,k),P(5.8,-.8,k),P(5.8,5.8,k),P(-.8,5.8,k)];
-    el(s,'path',{d:'M'+cs.map(p=>p.join(' ')).join(' L ')+' Z',
-      fill:PAPER,'fill-opacity':.96,stroke:'#DEDDD6','stroke-width':.9,
-      class:'fade',style:`animation-delay:${k*.16}s`});
-    for(let r=0;r<6;r++)for(let c=0;c<6;c++){
-      const load=rnd(k*37+r*6+c+1,k+2);
-      const v=load<.3?0:Math.round(load*12);   // sparser: silence is part of the texture
-      const [x,y]=P(c,r,k);
-      if(!v){el(s,'circle',{cx:x,cy:y,r:.7,fill:'#D8D6CE',
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});continue}
-      const dot=el(s,'circle',{cx:x,cy:y,r:1+Math.sqrt(v)*1.15,fill:SHADE[k],
-        class:'pop',style:`animation-delay:${k*.16+(r*6+c)*.008}s`});
-      tip(dot,`${SQUAD[k]} · week ${c+1} × lane ${r+1} — ${v} tasks`);
-    }
-    // label off the plane's right corner, tied by a short hairline
-    const [cxr,cyr]=P(5.8,-.8,k);
-    el(s,'line',{x1:cxr+4,y1:cyr,x2:cxr+22,y2:cyr,stroke:'#C6C5BF','stroke-width':.8,
-      class:'fade',style:`animation-delay:${.2+k*.16}s`});
-    txt(s,{x:cxr+27,y:cyr+2.5,'font-size':7.5,'font-weight':700,fill:k===3?INK:'#8F8E88',
-      'letter-spacing':'.12em',class:'fade',style:`animation-delay:${.2+k*.16}s`},SQUAD[k]);
-  }
-});
-})();
-
-// ════ 3 · bubble almanac ════
-// the vintage-almanac feel lives in three things: ledger-paper hairlines
-// under everything, bubbles big enough to collide, and marginalia. do all three.
-(()=>{
-const AREAS=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','GRID','VIEWS','HUB','FORMS'];
-const BETA={GRID:2023,VIEWS:2024,HUB:2025,FORMS:2025};   // beta until this year
-const BORN={EDITOR:2019,BOARDS:2019,DOCS:2019,CHAT:2020,FLOWS:2020,VAULT:2021,PAGES:2021,SYNC:2022,GRID:2022,VIEWS:2023,HUB:2024,FORMS:2024};
-obsReveal('almanac',s=>{
-  const rowY=i=>62+i*34,colX=j=>132+j*57;
-  // ledger paper: hairlines every 6.8px, edge to edge, under everything
-  for(let y=40;y<=318;y+=6.8)
-    el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:'#E4E3DC','stroke-width':.5,
-      class:'fade',style:`animation-delay:${(y-40)*.001}s`});
-  const vals=[];
-  for(let i=0;i<8;i++){
-    const yr=2019+i;
-    el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:'#D3D2CA','stroke-width':.9,
-      class:'fade',style:`animation-delay:${i*.04}s`});
-    txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:'#8F8E88','text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.04}s`},yr);
-    AREAS.forEach((a,j)=>{
-      if(yr<BORN[a])return;
-      const age=yr-BORN[a];
-      // steep spread: quiet areas stay pinpricks, busy ones balloon
-      const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));
-      vals.push([Math.max(1,v),i,j,a,yr]);
-    });
-  }
-  // draw small over large so the giants sit underneath as washes
-  vals.sort((a,b)=>b[0]-a[0]);
-  const top3=vals.slice(0,3);
-  // hand-drawn circle: radius wobbles around the rim on two slow sine waves,
-  // so every bubble is its own slightly-off print — never a compass circle
-  const blob=(x,y,r,seed)=>{
-    const n=Math.max(14,Math.round(r*1.6)),pts=[];
-    for(let t=0;t<n;t++){
-      const a=t/n*Math.PI*2;
-      const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;
-      pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);
-    }
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<n;t++){
-      const p=pts[t],q=pts[(t+1)%n];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+' Z';
-  };
-  vals.forEach(([v,i,j,a,yr],k)=>{
-    const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10,  // jitter off the gridline
-          y=rowY(i),r=Math.sqrt(v)*3.8;        // big: rows are 34 apart, giants reach ~28 and collide
-    const beta=BETA[a]&&yr<=BETA[a];
-    const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});
-    const seed=i*12+j;
-    const dot=el(g,'path',beta
-      ?{d:blob(x,y,r,seed),fill:'none',stroke:'#8F8E88','stroke-width':1,'stroke-dasharray':'3 3'}
-      :{d:blob(x,y,r,seed),fill:INK,'fill-opacity':.09+rnd(i+2,j+4)*.1,
-        stroke:INK,'stroke-opacity':.3+rnd(i+5,j+7)*.35,'stroke-width':.6+rnd(i+3,j+11)*.9});
-    tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);
-    // escalation core: small wobbly blot, nudged off-center like a press mark
-    if(!beta){
-      const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;
-      el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:INK});
-    }
-    if(top3.includes(vals[k]))
-      txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},v*10);
-  });
-  AREAS.forEach((a,j)=>{
-    const x=colX(j);
-    txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',
-      transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);
-  });
-  // marginalia: two annotations with pointer hairlines, like the original's notes
-  el(s,'path',{d:`M${colX(3)+30} ${rowY(5)-14} C${colX(3)+70} ${rowY(5)-34} ${colX(3)+96} ${rowY(5)-38} ${colX(3)+118} ${rowY(5)-38}`,
-    fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:colX(3)+124,y:rowY(5)-35,'font-size':7,fill:'#6A6963','font-style':'italic',
-    class:'fade',style:'animation-delay:1.1s'},'chat tickets triple after mobile GA');
-  el(s,'path',{d:`M${colX(9)+16} ${rowY(2)+8} C${colX(9)+40} ${rowY(2)+26} ${colX(9)+60} ${rowY(2)+30} ${colX(9)+80} ${rowY(2)+30}`,
-    fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7,fill:'#6A6963','font-style':'italic',
-    class:'fade',style:'animation-delay:1.2s'},'betas barely ticket — nobody files bugs on toys');
-  // footer strip: important events, the almanac's timeline shelf
-  const evY=356;
-  el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:'#C6C5BF','stroke-width':.8,
-    class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:56,y:evY-24,'font-size':7,'font-weight':800,fill:'#8F8E88','letter-spacing':'.16em',
-    class:'fade',style:'animation-delay:1.2s'},'IMPORTANT EVENTS');
-  [['2020','SLA introduced'],['2022','self-serve help center'],['2023','the GA wave'],['2025','AI deflection live']]
-  .forEach(([yr,note],k)=>{
-    const x=90+k*196;
-    el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:'#8F8E88','stroke-width':1,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`});
-    txt(s,{x,y:evY+4,'font-size':8,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);
-    txt(s,{x,y:evY+15,'font-size':7,fill:'#8F8E88',
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);
-  });
-});
-})();
-
-// ════ 4 · radial patchwork ════
-(()=>{
-const CX=200,CY=168,N=46;
-obsReveal('patchwork',s=>{
-  // 24h rim ticks
-  for(let h=0;h<96;h++){
-    const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:'#C6C5BF','stroke-width':h%4===0?1:.5,
-      class:'fade',style:`animation-delay:${h*.006}s`});
-  }
-  [0,6,12,18].forEach(h=>{
-    const [x,y]=pol(CX,CY,157,-90+h*15);
-    txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:MUTED,'text-anchor':'middle',
-      class:'fade'},String(h).padStart(2,'0'));
-  });
-  // the patchwork: translucent wedges pile into ink
-  const wedges=[];
-  for(let i=0;i<N;i++){
-    // deploys cluster around 11h and 16h, thin overnight
-    const peak=rnd(i+1,2)>.5?11:16;
-    const hour=(peak+(rnd(i+1,3)-.5)*7+24)%24;
-    const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;
-    const r1=34+rnd(i+1,5)*100;
-    wedges.push([a0,sw,r1,hour,i]);
-  }
-  wedges.forEach(([a0,sw,r1,hour,i])=>{
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:INK,
-      'fill-opacity':.07+rnd(i+1,6)*.09,class:'fade',
-      style:`animation-delay:${.2+i*.022}s`});
-    tip(w,`deploy #${i+1} — ${String(Math.floor(hour)).padStart(2,'0')}:${String(Math.floor(hour%1*60)).padStart(2,'0')} · ${Math.round(r1*6)} files`);
-  });
-  // three outlined wedges: the ones that paged somebody
-  [4,17,31].forEach((i,k)=>{
-    const [a0,sw,r1]=wedges[i];
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',
-      stroke:INK,'stroke-width':1.1,class:'fade',style:`animation-delay:${1.2+k*.15}s`});
-    tip(w,`deploy #${i+1} — triggered an incident`);
-  });
-  el(s,'circle',{cx:CX,cy:CY,r:3,fill:INK,class:'pop',style:'animation-delay:1.3s'});
-  txt(s,{x:CX,y:308,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.4s'},'OUTLINED = TRIGGERED AN INCIDENT');
-});
-})();
-
-// ════ 6 · trend lineage ════
-(()=>{
-// [name, events [year, type f|o], aliveToday]
-const TR=[
-  ['Dark mode',[[2017,'f'],[2022,'o']],true],
-  ['Kanban',[[2016,'f']],true],
-  ['Wikis',[[2016,'f'],[2019,'o']],false],
-  ['Slash cmds',[[2018,'f']],true],
-  ['Templates',[[2019,'f'],[2024,'o']],true],
-  ['Inbox',[[2020,'f']],false],
-  ['Live cursors',[[2020,'f'],[2023,'o']],true],
-  ['AI drafts',[[2023,'f'],[2025,'o']],true],
-  ['Voice notes',[[2021,'f']],false],
-  ['Offline',[[2018,'f'],[2026,'o']],true],
-];
-const Y0=2016,yY=yr=>34+(yr-Y0)*24,colX=c=>76+c*31;
-obsReveal('lineage',s=>{
-  for(let yr=Y0;yr<=2026;yr++){
-    el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:'#E3E2DB','stroke-width':.8,
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
-    if(yr%2===0)txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:MUTED,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
-  }
-  TR.forEach(([name,ev,alive],c)=>{
-    const x=colX(c);
-    // segments between events: solid while fresh, dashed once dormant
-    for(let k=0;k<ev.length-1;k++){
-      const gap=ev[k+1][0]-ev[k][0];
-      el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
-        stroke:'#B0AFA9','stroke-width':1,
-        'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
-        class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
-    }
-    const last=ev[ev.length-1][0];
-    if(alive)
-      el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:'#B0AFA9','stroke-width':1,
-        pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
-    ev.forEach(([yr,t])=>{
-      const node=el(s,'circle',t==='f'
-        ?{cx:x,cy:yY(yr),r:5.5,fill:INK}
-        :{cx:x,cy:yY(yr),r:5.5,fill:PAPER,stroke:INK,'stroke-width':1.4});
-      node.setAttribute('class','pop');
-      node.style.animationDelay=(.35+c*.07)+'s';
-      tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);
-    });
-    // terminal dot: alive lands on today's baseline, dead stops where it stopped
-    el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
-      fill:alive?INK:'#C6C5BF',class:'pop',style:`animation-delay:${.6+c*.07}s`});
-    const ly=yY(2026)+26;
-    txt(s,{x,y:ly,'font-size':6.8,'font-weight':600,fill:alive?'#4A4944':'#B0AFA9',
-      transform:`rotate(-38 ${x} ${ly})`,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${.7+c*.05}s`},name);
-  });
-});
-})();
-
-// ════ 8 · type colonnade ════
-(()=>{
-const PRE=['core','ui','api','data','auth','sync','mail','flag','edge','doc','bot'];
-const SUF=['-kit','-svc','-web','-cli','-db','-gw','-sdk','-jobs'];
-const TEAM=['PLATFORM','FRONTEND','BACKEND','DATA','INFRA','MOBILE','SECURITY','GROWTH','ML','DESIGN'];
-obsReveal('colonnade',s=>{
-  const N=44;
-  const teamY=j=>36+j*28,repoY=i=>16+i*6.7;
-  const counts=Array(10).fill(0);
-  const owner=i=>{const w=rnd(i+1,17);return Math.min(9,Math.floor(w*w*10))}; // skewed: low teams hoard
-  for(let i=0;i<N;i++)counts[owner(i)]++;
-  for(let i=0;i<N;i++){
-    const name=PRE[i%11]+SUF[Math.floor(rnd(i+1,8)*8)%8]+(i>21?'-v2':'');
-    const yi=repoY(i),j=owner(i),yj=teamY(j);
-    txt(s,{x:112,y:yi+2,'font-size':5,fill:'#8F8E88','text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.012}s`},name);
-    el(s,'rect',{x:117,y:yi-1.2,width:4,height:2.4,fill:'#B0AFA9',
-      class:'fade',style:`animation-delay:${i*.012}s`});
-    el(s,'path',{d:`M123 ${yi} C 200 ${yi} 220 ${yj} 288 ${yj}`,fill:'none',
-      stroke:'#A8A7A0','stroke-width':.6,opacity:.6,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.015}s;animation-duration:.7s`});
-  }
-  TEAM.forEach((t,j)=>{
-    const y=teamY(j);
-    el(s,'circle',{cx:293,cy:y,r:2.6+counts[j]*.55,fill:counts[j]>=7?INK:counts[j]>=4?'#6A6963':'#B0AFA9',
-      class:'pop',style:`animation-delay:${.6+j*.05}s`});
-    const lab=txt(s,{x:302+counts[j]*.55,y:y+2.5,'font-size':7,'font-weight':700,fill:'#4A4944',
-      class:'fade',style:`animation-delay:${.65+j*.05}s`},`${t} · ${counts[j]}`);
-    tip(lab,`${t} owns ${counts[j]} repos`);
-  });
-});
-})();
-
-// ════ 9 · hourglass stream ════
-(()=>{
-const STAGE=[['VISITORS',4200],['SIGN-UPS',1900],['ACTIVATED',960],['RETAINED',540],['PAYING',310]];
-obsReveal('hourglass',s=>{
-  const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;
-  STAGE.forEach(([name,c],k)=>{
-    const y=sy(k),hw=w(c)/2;
-    // barcode strip: one tick ≈ 40 people
-    const n=Math.round(c/40);
-    for(let t=0;t<n;t++){
-      const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;
-      el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:INK,'stroke-width':.8,
-        opacity:.45+rnd(t+2,k+5)*.5,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
-    }
-    // threads trickling to the next strip
-    if(k<4){
-      const hw1=w(STAGE[k+1][1])/2;
-      for(let t=0;t<34;t++){
-        const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
-        const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
-        el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,
-          fill:'none',stroke:'#B0AFA9','stroke-width':.5,opacity:.32,pathLength:1,
-          class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
-      }
-      // conversion between stages, parked in the left margin
-      const pct=Math.round(STAGE[k+1][1]/c*100);
-      txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':8.5,'font-weight':800,fill:'#8F8E88',
-        class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
-      txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6,'font-weight':600,fill:'#C6C5BF',
-        'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'GET THROUGH');
-    }
-    // stage label, tied to the strip edge
-    el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:'#DEDDD6','stroke-width':.8,
-      class:'fade',style:`animation-delay:${.3+k*.12}s`});
-    txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:'#4A4944',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
-    txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());
-  });
-});
-})();
-
-// ════ 14 · hundred field ════
-// small data, unit-decomposed: four shares of 100% become 100 people.
-// phyllotaxis discs (golden angle) so每簇自带肌理; share is countable, not just readable.
-(()=>{
-const SEG=[['CHARGED',41,INK],['TORN',35,'#55554F'],['ADRIFT',12,'#8F8E88'],['AVERSE',12,'#B0AFA9']];
-const POS=[[132,140],[276,116],[186,252],[322,238]];
-obsReveal('hundredfield',s=>{
-  // dashed constellation hairlines tying the cluster cores
-  [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:GRID,'stroke-width':.7,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      // hairline spoke for every 5th person, core to dot
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:'#CDCCC5','stroke-width':.6,
-        class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,opacity:.9,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} — one of ${v} in 100`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:INK,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:INK,'text-anchor':'middle',
-      'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},'ONE DOT = ONE PERSON · 41 + 35 + 12 + 12 = 100');
-});
-})();
-
-// ════ 15 · ballot tally ════
-// multi-select survey (each option independent, 0–100): every option is a row of
-// 100 hairline ticks — a hundred people standing in line, the ones who picked it inked.
-(()=>{
-const ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];
-obsReveal('ballottally',s=>{
-  ASK.forEach(([name,v],i)=>{
-    const base=78+i*62;
-    txt(s,{x:28,y:base-26,'font-size':7.5,'font-weight':700,fill:'#6A6963',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:28,y1:base,x2:372,y2:base,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=28+k*3.44,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?INK:'#CFCEC7','stroke-width':picked?.9:.55,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:'#C6C5BF',
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:28+(v-1)*3.44+9,y:base-11,'font-size':11,'font-weight':800,fill:INK,
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`${v} of 100 picked this — they could pick several`);
-  });
-  txt(s,{x:200,y:314,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
-});
-})();
-
-
-// ── New chart-family helpers (scoped to the added templates) ──
-(()=>{
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-
-// ════ L16 · matrix heat ════
-// 通用矩阵热力:任意 分类×分类×连续值。明度即数据——五档灰阶方块,
-// 越黑越高。对角线不参与(自己×自己),零格留一粒小点(沉默可见)。
-// 最强的一对上虚线框 + 标数。区别于 F10 dot heat:这里编码用明度不用面积,
-// 值是占比/强度而不是可数件数。
-(()=>{
-const FEAT=['EDITOR','BOARDS','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC'];
-const v=(i,j)=>{
-  if(i===j)return -1;
-  const a=Math.min(i,j),b=Math.max(i,j);
-  const pop=(8-a)/8*(8-b)/8;                       // 头部功能重合度高
-  const raw=pop*62*(0.35+rnd(a*8+b+1,a+b+3)*.9);
-  return Math.round(raw);
-};
-const shade=t=>t>36?INK:t>24?'#4A4944':t>14?'#8F8E88':t>6?'#B0AFA9':'#D8D7D1';
-obsReveal('matheat',s=>{
-  const X0=118,Y0=70,P=26,C=22;
-  let max=0,mi=0,mj=0;
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  FEAT.forEach((f,i)=>{
-    txt(s,{x:X0-10,y:Y0+i*P+C/2+3,'font-size':7,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.04}s`},f);
-    const cy=Y0-10;
-    txt(s,{x:X0+i*P+C/2,y:cy,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.06em',
-      transform:`rotate(-55 ${X0+i*P+C/2} ${cy})`,class:'fade',style:`animation-delay:${i*.04}s`},f);
-  });
-  for(let i=0;i<8;i++)for(let j=0;j<8;j++){
-    const x=X0+j*P,y=Y0+i*P,t=v(i,j),d=(i+j)*.02;
-    if(t<0){el(s,'rect',{x:x+C/2-3,y:y+C/2-.6,width:6,height:1.2,fill:'#D8D6CE',
-      class:'fade',style:`animation-delay:${d}s`});continue}
-    if(!t){el(s,'circle',{cx:x+C/2,cy:y+C/2,r:.9,fill:'#D8D6CE',
-      class:'pop',style:`animation-delay:${d}s`});continue}
-    const cell=el(s,'rect',{x,y,width:C,height:C,rx:4,fill:shade(t),
-      class:'pop',style:`animation-delay:${d}s`});
-    tip(cell,`${FEAT[i]} × ${FEAT[j]} — ${t}% of accounts use both`);
-    if(i===mi&&j===mj){
-      el(s,'rect',{x:x-3.5,y:y-3.5,width:C+7,height:C+7,rx:6,fill:'none',
-        stroke:INK,'stroke-width':1,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:.9s'});
-      txt(s,{x:x+C/2,y:y+C/2+3.5,'font-size':9,'font-weight':800,fill:PAPER,'text-anchor':'middle',
-        class:'fade',style:'animation-delay:1s'},t);
-    }
-  }
-  // shade legend, countable buckets
-  const LG=[['#D8D7D1','1–6'],['#B0AFA9','7–14'],['#8F8E88','15–24'],['#4A4944','25–36'],[INK,'37+']];
-  LG.forEach(([c,lab],k)=>{
-    const x=118+k*54;
-    el(s,'rect',{x,y:292,width:9,height:9,rx:2,fill:c,class:'fade',style:`animation-delay:${1+k*.05}s`});
-    txt(s,{x:x+13,y:300,'font-size':6.5,'font-weight':600,fill:'#8F8E88',
-      class:'fade',style:`animation-delay:${1+k*.05}s`},lab+'%');
-  });
-  txt(s,{x:36,y:300,'font-size':7,'font-weight':600,fill:'#B0AFA9','letter-spacing':'.1em',
-    class:'fade',style:'animation-delay:1s'},'SHADE = %');
-});
-})();
-
-// ════ L17 · calendar heat ════
-// 日历热力:GitHub 贡献墙的 Lupi 皮。一格一天,52 周 × 7 天,
-// 点面积 = 当天量(sqrt 换算),静默日一粒小点。月份写在上檐,
-// 峰值日虚线圈 + 旁注引线。逐记录世界观:一年 365 天每天都在场。
-(()=>{
-const MON=['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'];
-const DAY=['MON','WED','FRI','SUN'];
-const val=(w,d)=>{
-  const wd=d<5;
-  const season=1+.55*Math.sin((w-8)/9);            // 春秋两个发布季
-  if(!wd)return rnd(w*7+d+1,3)>.82?1:0;            // 周末基本静默
-  const raw=season*(2.5+rnd(w*7+d+1,d+2)*9)*(rnd(w+1,d+5)>.12?1:0);
-  return Math.round(raw);
-};
-obsReveal('calheat',s=>{
-  const X0=56,Y0=42,P=14.8;
-  let max=0,mw=0,md=0;
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){const t=val(w,d);if(t>max){max=t;mw=w;md=d}}
-  DAY.forEach((lab,k)=>{
-    txt(s,{x:X0-10,y:Y0+[0,2,4,6][k]*P+3,'font-size':6.5,'font-weight':700,fill:'#8F8E88','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${k*.04}s`},lab);
-  });
-  MON.forEach((m,k)=>{
-    const x=X0+Math.round(k*52/12)*P;
-    txt(s,{x,y:Y0-16,'font-size':7,'font-weight':700,fill:MUTED,'letter-spacing':'.1em',
-      class:'fade',style:`animation-delay:${k*.03}s`},m);
-    el(s,'line',{x1:x,y1:Y0-11,x2:x,y2:Y0-5,stroke:'#C6C5BF','stroke-width':.7,
-      class:'fade',style:`animation-delay:${k*.03}s`});
-  });
-  for(let w=0;w<52;w++)for(let d=0;d<7;d++){
-    const x=X0+w*P,y=Y0+d*P,t=val(w,d),delay=w*.012+d*.004;
-    if(!t){el(s,'circle',{cx:x,cy:y,r:.75,fill:'#D8D6CE',
-      class:'pop',style:`animation-delay:${delay}s`});continue}
-    const r=1.1+Math.sqrt(t)*1.55;
-    const dot=el(s,'circle',{cx:x,cy:y,r,
-      fill:t>max*.66?INK:t>max*.33?'#6A6963':'#B0AFA9',
-      class:'pop',style:`animation-delay:${delay}s`});
-    tip(dot,`W${w+1} ${['MON','TUE','WED','THU','FRI','SAT','SUN'][d]} — ${t} deploys`);
-  }
-  // peak: dashed ring + marginalia hairline
-  const px=X0+mw*P,py=Y0+md*P,pr=1.1+Math.sqrt(max)*1.55;
-  el(s,'circle',{cx:px,cy:py,r:pr+3.6,fill:'none',stroke:INK,'stroke-width':1,
-    'stroke-dasharray':'2 3',class:'fade',style:'animation-delay:1s'});
-  el(s,'path',{d:`M${px+pr+6} ${py+6} C${px+34} ${py+30} ${px+52} ${py+36} ${px+72} ${py+36}`,
-    fill:'none',stroke:'#B0AFA9','stroke-width':.7,class:'fade',style:'animation-delay:1.1s'});
-  txt(s,{x:px+78,y:py+39,'font-size':7,fill:'#6A6963','font-style':'italic',
-    class:'fade',style:'animation-delay:1.15s'},`the release-week spike — ${max} deploys in a day`);
-  txt(s,{x:420,y:222,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DAY · DOT AREA = DEPLOYS · TINY DOT = A QUIET DAY');
-});
-})();
-
-// ════ L18 · beeswarm ════
-// 蜂群:violin 的 Lupi 姊妹。不聚合密度,把 120 条真实记录沿数值轴
-// 逐颗排开,撞上了就往两边让——堆得高的地方就是分布的峰。
-// 空心 = enterprise 单,虚线 = 中位数。每颗点背后是一条真实成交。
-(()=>{
-const N=120;
-const deals=Array.from({length:N},(_,i)=>{
-  const u=rnd(i+1,3),ent=rnd(i+2,11)>.86;
-  const v=Math.round(4+(ent?60:6)+150*Math.pow(u,2.6)+rnd(i+3,7)*10);
-  return [Math.min(178,v),ent];
-});
-obsReveal('beeswarm',s=>{
-  const X0=36,X1=376,BASE=246,R=4,GAP=R*2+.8;
-  const mapX=v=>X0+v/180*(X1-X0);
-  // axis rail + barcode ticks
-  el(s,'line',{x1:X0-4,y1:BASE,x2:X1+4,y2:BASE,stroke:GRID,'stroke-width':.8,class:'fade'});
-  for(let g=0;g<=9;g++){
-    const x=X0+g/9*(X1-X0);
-    el(s,'line',{x1:x,y1:BASE,x2:x,y2:BASE+(g%3===0?7:4),stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:`animation-delay:${g*.02}s`});
-    if(g%3===0)txt(s,{x,y:BASE+18,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${g*.02}s`},'$'+Math.round(g/9*180)+'k');
-  }
-  // swarm as clean stacks: x snaps to a GAP-wide lane, dots pile upward —
-  // the pile height IS the density. no half-overlaps, every dot countable.
-  const lanes={};
-  const sorted=deals.map((d,i)=>[d[0],d[1],i]).sort((a,b)=>a[0]-b[0]);
-  sorted.forEach(([v,ent,i])=>{
-    const lane=Math.round((mapX(v)-X0)/GAP);
-    const x=X0+lane*GAP;
-    const row=lanes[lane]||0;lanes[lane]=row+1;
-    const y=BASE-R-1-row*GAP;
-    const dot=ent
-      ?el(s,'circle',{cx:x,cy:y,r:R,fill:PAPER,stroke:INK,'stroke-width':1.1,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`})
-      :el(s,'circle',{cx:x,cy:y,r:R,fill:INK,opacity:.82,
-        class:'pop',style:`animation-delay:${.1+i*.007}s`});
-    tip(dot,`deal #${i+1} — $${v}k${ent?' · enterprise':''}`);
-  });
-  // median
-  const vs=deals.map(d=>d[0]).sort((a,b)=>a-b);
-  const med=vs[Math.floor(N/2)];
-  const mx=mapX(med);
-  el(s,'line',{x1:mx,y1:52,x2:mx,y2:BASE+8,stroke:'#8F8E88','stroke-width':.9,
-    'stroke-dasharray':'2 4',class:'fade',style:'animation-delay:1s'});
-  txt(s,{x:mx,y:44,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-    style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1.1s`,
-    class:'fade'},'MEDIAN $'+med+'k');
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE DOT = ONE DEAL · THE PILE IS THE DISTRIBUTION · HOLLOW = ENTERPRISE');
-});
-})();
-
-// ════ L19 · ridgeline ════
-// 多组分布的 Lupi 皮:每行一条完成时长的密度山脊,面积由竖发丝组成
-// (F3 hairline area 的语法),行与行轻微重叠,后排画在前、前排糊上
-// 纸色 slab 遮挡(dotty matrix 的手法)。峰值点标中心时长。
-(()=>{
-const ROWS=[['MOBILE',3.2,1.3],['DESKTOP',5.1,1.9],['API',7.4,2.3],['IMPORTS',10.8,3.2],['BATCH',15.2,4]];
-obsReveal('ridge',s=>{
-  const X0=92,X1=372,MAXH=24,N=72,LIFT=54;
-  const rowY=i=>96+i*44;
-  ROWS.forEach(([name,c,sp],i)=>{
-    const y0=rowY(i);
-    const dens=Array.from({length:N},(_,k)=>{
-      const h=k/(N-1)*MAXH;
-      return Math.exp(-((h-c)**2)/(2*sp*sp))+.26*Math.exp(-((h-c*2.1)**2)/(2*(sp*1.7)**2));
-    });
-    const dmax=Math.max(...dens);
-    const pts=dens.map((d,k)=>[X0+k/(N-1)*(X1-X0),y0-d/dmax*LIFT]);
-    const crest='M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L ');
-    el(s,'path',{d:crest+` L${X1} ${y0} L${X0} ${y0} Z`,fill:PAPER,'fill-opacity':.96,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    pts.forEach(([px,py],k)=>{
-      if(k%2)return;
-      el(s,'line',{x1:px,y1:y0,x2:px,y2:py,stroke:'#8F8E88','stroke-width':.5,
-        opacity:.28+rnd(k+1,i+3)*.32,class:'fade',style:`animation-delay:${i*.1+k*.004}s`});
-    });
-    el(s,'path',{d:crest,fill:'none',stroke:INK,'stroke-width':.9,pathLength:1,
-      class:'draw',style:`animation-delay:${.1+i*.1}s;animation-duration:.8s`});
-    el(s,'line',{x1:X0,y1:y0,x2:X1,y2:y0,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    const pk=dens.indexOf(dmax),[px,py]=pts[pk];
-    const dot=el(s,'circle',{cx:px,cy:py,r:2.6,fill:INK,class:'pop',style:`animation-delay:${.6+i*.1}s`});
-    tip(dot,`${name} — most runs finish around ${c.toFixed(1)}h`);
-    txt(s,{x:px+7,y:py+1,'font-size':8,'font-weight':800,fill:INK,
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.7+i*.1}s`,
-      class:'fade'},c.toFixed(1)+'h');
-    txt(s,{x:X0-10,y:y0-3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-  });
-  const yb=rowY(4);
-  [0,6,12,18,24].forEach(h=>{
-    const x=X0+h/MAXH*(X1-X0);
-    el(s,'line',{x1:x,y1:yb+4,x2:x,y2:yb+9,stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x,y:yb+20,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},h+'h');
-  });
-  txt(s,{x:200,y:308,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'EACH CREST = ONE PIPELINE · THE AREA IS MADE OF HAIRLINES · DOT = THE PEAK');
-});
-})();
-
-// ════ L20 · parallel coordinates ════
-// 多维记录对比的 Lupi 皮:一条发丝线 = 一个产品走过四根轴。
-// 线束的形状就是数据结构(哪两维相关、谁全线领先)。
-// 全线最优的上墨标名,其余发丝退后。轴顶名、轴脚 min/max。
-(()=>{
-const DIMS=[['PRICE $',8,30],['CSAT',6,9.6],['RETENTION %',55,95],['GROWTH %',-5,40]];
-const P=[['Editor',12,9.1,91,22],['Boards',18,8.4,86,18],['Forms',9,8.8,78,31],
-  ['Docs',15,8.0,82,12],['Chat',7,7.2,64,8],['Vault',24,7.8,88,6],
-  ['Flows',21,8.6,90,38],['Views',11,7.5,71,14],['Sync',16,6.9,58,-2],
-  ['Pages',8,8.2,74,19],['Grid',19,7.1,62,4],['Hub',13,6.6,52,9]];
-obsReveal('parallel',s=>{
-  const AX=[64,156,248,340],TOP=64,BOT=252;
-  const mapY=(di,v)=>{const [,lo,hi]=DIMS[di];return BOT-(v-lo)/(hi-lo)*(BOT-TOP)};
-  // hero: judged on csat+retention+growth (price is a fact, not a virtue)
-  const score=p=>(p[2]-6)/3.6+(p[3]-55)/40+(p[4]+5)/45;
-  const hero=P.reduce((a,b)=>score(b)>score(a)?b:a);
-  DIMS.forEach(([name,lo,hi],di)=>{
-    el(s,'line',{x1:AX[di],y1:TOP-8,x2:AX[di],y2:BOT+8,stroke:'#B0AFA9','stroke-width':1,
-      class:'fade',style:`animation-delay:${di*.06}s`});
-    [[TOP-8],[BOT+8]].forEach(([yy])=>
-      el(s,'line',{x1:AX[di]-4,y1:yy,x2:AX[di]+4,y2:yy,stroke:'#B0AFA9','stroke-width':1,
-        class:'fade',style:`animation-delay:${di*.06}s`}));
-    txt(s,{x:AX[di],y:TOP-22,'font-size':7,'font-weight':800,fill:'#6A6963','text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${di*.06}s`},name);
-    txt(s,{x:AX[di]+8,y:TOP-5,'font-size':6.5,'font-weight':600,fill:'#C6C5BF',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},hi);
-    txt(s,{x:AX[di]+8,y:BOT+11,'font-size':6.5,'font-weight':600,fill:'#C6C5BF',
-      class:'fade',style:`animation-delay:${.1+di*.06}s`},lo);
-  });
-  P.forEach((p,i)=>{
-    const isHero=p===hero;
-    const ys=[mapY(0,p[1]),mapY(1,p[2]),mapY(2,p[3]),mapY(3,p[4])];
-    let d=`M${AX[0]} ${ys[0].toFixed(1)}`;
-    for(let k=0;k<3;k++){
-      const mx=(AX[k]+AX[k+1])/2;
-      d+=` C${mx} ${ys[k].toFixed(1)} ${mx} ${ys[k+1].toFixed(1)} ${AX[k+1]} ${ys[k+1].toFixed(1)}`;
-    }
-    const ln=el(s,'path',{d,fill:'none',
-      stroke:isHero?INK:'#8F8E88','stroke-width':isHero?2:.65,
-      opacity:isHero?1:.5+rnd(i+1,5)*.3,pathLength:1,
-      class:'draw',style:`animation-delay:${.2+i*.05}s;animation-duration:.8s`});
-    tip(ln,`${p[0]} — $${p[1]} · CSAT ${p[2]} · ${p[3]}% retained · ${p[4]}% growth`);
-    ys.forEach((y,di)=>el(s,'circle',{cx:AX[di],cy:y,r:isHero?3:1.4,
-      fill:isHero?INK:'#8F8E88',opacity:isHero?1:.7,
-      class:'pop',style:`animation-delay:${.3+i*.05+di*.04}s`}));
-    if(isHero)
-      txt(s,{x:AX[3]+12,y:ys[3]+3,'font-size':9,'font-weight':800,fill:INK,
-        style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:1s`,
-        class:'fade'},p[0]);
-  });
-  txt(s,{x:200,y:302,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE HAIRLINE = ONE PRODUCT · INK = BEST ACROSS THE BOARD');
-});
-})();
-})();
-
-</script>
-</body>
-</html>

+ 0 - 180
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/maps-gallery.html

@@ -1,180 +0,0 @@
-<!doctype html>
-<html lang="zh">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Lieflat Charts · Maps · 2 张参考实现</title>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-:root{--bg:#F0EFEB;--dark:#1C1C1A;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6}
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);font-family:'Inter',sans-serif;color:var(--ink);padding:40px;-webkit-font-smoothing:antialiased}
-  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:22px;max-width:1400px;margin:0 auto}
-  .card{background:var(--bg);border-radius:24px;padding:28px 28px 20px}
-  .card.wide{grid-column:1/-1}
-  h2{font-weight:700;font-size:16.5px;letter-spacing:-.02em;margin-bottom:3px}
-  .sub{font-size:11.5px;color:var(--muted);margin-bottom:14px}
-  .src{font-size:9.5px;color:var(--faint);margin-top:10px;letter-spacing:.08em;font-weight:500}
-  .badge{display:inline-block;font-size:9px;font-weight:700;letter-spacing:.08em;
-    padding:3px 10px;border-radius:99px;margin-bottom:10px;border:1px dashed #B0AFA9;color:#8F8E88}
-  svg{width:100%;max-height:330px;display:block;margin:0 auto}
-  svg text{font-family:'Inter',sans-serif}
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-  .pagehead{max-width:1400px;margin:0 auto 26px;padding:0 4px}
-  .pagehead h1{font-size:22px;letter-spacing:-.02em;font-weight:800}
-  .pagehead p{font-size:12px;color:#8F8E88;margin-top:5px;line-height:1.7}
-</style>
-</head>
-<body>
-<div class="pagehead">
-  <h1>Maps · 2 张参考实现</h1>
-  <p>仅在用户明确要求地图或地域分布时使用。M1 为美国州级,M2 为世界国家级;地图数据与 ECharts 通过网络加载。</p>
-</div>
-<div class="grid2">
-  <!-- M1 · US Choropleth -->
-  <div class="card">
-    <span class="badge">MAPS · US CHOROPLETH</span>
-    <h2>Sign-ups across the states</h2>
-    <div class="sub">real outlines · Alaska and Hawaii tucked lower-left · shade = sign-ups, five steps · needs the network for map data</div>
-    <div class="ch" id="mapus" style="height:340px"></div>
-    <div class="src">CHOROPLETH · MONO-MAP · GROWTH GEO</div>
-  </div>
-
-  <!-- M2 · World Choropleth -->
-  <div class="card wide">
-    <span class="badge">MAPS · WORLD CHOROPLETH</span>
-    <h2>Where the users are</h2>
-    <div class="sub">real outlines · shade = monthly active accounts · gray = markets we haven't opened</div>
-    <div class="ch" id="mapworld" style="height:400px"></div>
-    <div class="src">CHOROPLETH · MONO-MAP · GLOBAL OPS</div>
-  </div>
-</div>
-<script>
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>fn(n);
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n);
-  n.style.cursor='pointer';
-  n.addEventListener('click',go);
-};
-
-// ════ M1 · US choropleth ════
-// 真轮廓地图:ECharts registerMap + GeoJSON(ECharts 官方示例数据,jsdelivr)。
-// 阿拉斯加/夏威夷/波多黎各用 registerMap 第三参 specialAreas 收进左下角
-// (官方 USA 示例同款,零额外依赖;d3 albersUsa 投影方案在部分环境渲染空白,弃用)。
-// Mono 化三件事:piecewise 五档灰阶 visualMap(明度即数据)、
-// 纸色描边当州界发丝、无数据州留 #E4E3DC(沉默可见)。
-// 面积失真的坦白:州的大小是地理,不是数值(蒙大拿大 ≠ 蒙大拿多)。
-// ⚠️ 如果换成中国地图:数据源必须含台湾省 + 南海诸岛 + 九段线
-// (用 DataV GeoAtlas 100000_full 或其完整镜像),发布前核对当期审图号。
-const MAP_LEGEND={
-  itemWidth:11,itemHeight:11,itemSymbol:'rect',
-  textStyle:{fontFamily:'Inter',fontSize:9,color:'#8F8E88'},
-  inRange:{color:['#D8D7D1','#B0AFA9','#8F8E88','#4A4944','#1C1C1A']},
-};
-(()=>{
-const D={'California':96,'New York':78,'Texas':72,'Washington':68,'Massachusetts':66,
-  'Florida':54,'Illinois':49,'Colorado':44,'Georgia':41,'Virginia':33,'Pennsylvania':31,
-  'North Carolina':30,'New Jersey':28,'Oregon':22,'Ohio':21,'Michigan':19,'Arizona':18,
-  'Minnesota':16,'Utah':15,'Maryland':14,'Tennessee':13,'Wisconsin':12,'Missouri':11,
-  'Indiana':9,'Nevada':8,'Connecticut':7,'South Carolina':6,'Alabama':5,'Kentucky':5,
-  'Oklahoma':4,'Iowa':4,'Kansas':3,'Arkansas':3,'Louisiana':3,'New Hampshire':2,
-  'Idaho':2,'New Mexico':2,'Hawaii':2,'Maine':1,'Nebraska':1,'Alaska':1};
-fetch('https://cdn.jsdelivr.net/gh/apache/echarts-examples@gh-pages/public/data/asset/geo/USA.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-usa',geo,{
-      Alaska:{left:-131,top:25,width:15},
-      Hawaii:{left:-110,top:28,width:5},
-      'Puerto Rico':{left:-76,top:26,width:2},
-    });
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — no data':p.name+' — '+p.value+'k sign-ups'},
-      visualMap:{type:'piecewise',bottom:6,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-usa',top:10,bottom:48,
-        itemStyle:{areaColor:'#E4E3DC',borderColor:PAPER,borderWidth:1},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.2}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:n==='California'?{show:true,formatter:'CA 96k',fontFamily:'Inter',fontSize:9,
-            fontWeight:800,color:PAPER,textBorderColor:INK,textBorderWidth:2}:undefined})),
-      }],
-    };
-    obsReveal('mapus',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-// ════ M2 · World choropleth ════
-// 世界真轮廓版。同一套 Mono 化规则;用 boundingCoords 裁掉南极,
-// 关键市场(≥39k)直接把数值标在版图上。
-(()=>{
-const D={'United States':95,'India':62,'United Kingdom':48,'Germany':41,'Brazil':39,'Canada':34,
-  'France':30,'Australia':28,'China':26,'Japan':26,'Korea':21,'Spain':19,'Italy':17,'Singapore':16,
-  'Indonesia':15,'Mexico':14,'Netherlands':13,'Poland':13,'Sweden':12,'Ireland':12,'Philippines':11,
-  'Turkey':10,'Vietnam':9,'South Africa':9,'Denmark':9,'Norway':8,'Thailand':8,'Malaysia':8,
-  'United Arab Emirates':8,'Argentina':7,'Finland':7,'Nigeria':7,'Chile':6,'Colombia':6,
-  'Saudi Arabia':6,'Ukraine':6,'New Zealand':6,'Egypt':5,'Pakistan':5,'Russia':5,'Portugal':5,
-  'Greece':4,'Kenya':4,'Peru':4,'Morocco':3,'Iceland':2,'Iran':2,'Kazakhstan':2,'Ethiopia':2,
-  'Ghana':2,'Ecuador':2,'Algeria':2,'Mongolia':1,'Libya':1};
-const BIG={'United States':['US 95k',[0,0]],'India':['IN 62k',[0,4]],
-  'United Kingdom':['UK 48k',[-26,-12]],'Germany':['DE 41k',[26,14]],'Brazil':['BR 39k',[0,0]]};
-fetch('https://cdn.jsdelivr.net/npm/echarts@4.9.0/map/json/world.json')
-  .then(r=>r.json()).then(geo=>{
-    echarts.registerMap('mono-world',geo);
-    const opt={
-      animationDuration:900,animationEasing:'quarticOut',
-      tooltip:{backgroundColor:INK,borderWidth:0,padding:[10,14],
-        textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},
-        formatter:p=>isNaN(p.value)?p.name+' — not opened yet':p.name+' — '+p.value+'k MAU'},
-      visualMap:{type:'piecewise',bottom:2,left:'center',orient:'horizontal',
-        pieces:[{max:9,label:'≤9k'},{min:10,max:20,label:'10–20k'},{min:21,max:38,label:'21–38k'},
-          {min:39,max:64,label:'39–64k'},{min:65,label:'65k+'}],
-        ...MAP_LEGEND},
-      series:[{type:'map',map:'mono-world',top:4,bottom:36,
-        boundingCoords:[[-170,76],[190,-58]],   // crop antarctica
-        itemStyle:{areaColor:'#E4E3DC',borderColor:PAPER,borderWidth:.7},
-        emphasis:{label:{show:true,fontFamily:'Inter',fontSize:10,fontWeight:800,color:INK,
-          textBorderColor:PAPER,textBorderWidth:3},
-          itemStyle:{areaColor:null,borderColor:INK,borderWidth:1.1}},
-        select:{disabled:true},
-        data:Object.entries(D).map(([n,v])=>({name:n,value:v,
-          label:BIG[n]?{show:true,formatter:BIG[n][0],offset:BIG[n][1],fontFamily:'Inter',fontSize:8.5,
-            fontWeight:800,color:INK,textBorderColor:PAPER,textBorderWidth:3}:undefined})),
-      }],
-    };
-    obsReveal('mapworld',elDom=>{
-      const g=echarts.getInstanceByDom(elDom)||echarts.init(elDom);
-      g.clear();g.setOption(opt);
-    });
-  });
-})();
-
-addEventListener('resize',()=>{
-  ['mapus','mapworld'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-</script>
-</body>
-</html>

+ 0 - 42
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/index.html

@@ -1,42 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>报告模板库 · Lieflat Charts</title>
-<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%231C1C1A'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23F0EFEB'/%3E%3C/svg%3E" />
-<style>
-  :root{--bg:#F0EFEB;--ink:#1C1C1A;--mut:#77766F;--grid:#D5D4CD;--soft:#E7E6E0}
-  *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font-family:Inter,"Noto Sans SC",system-ui,sans-serif;-webkit-font-smoothing:antialiased;padding:48px 24px 64px}
-  .sheet{max-width:1120px;margin:auto}.mast{display:flex;justify-content:space-between;gap:20px;border-bottom:1px solid var(--ink);padding-bottom:12px}.mast b{font-size:15px}.mast span{font-size:10px;color:var(--mut);letter-spacing:.12em}
-  h1{font-size:clamp(30px,5vw,48px);line-height:1.1;max-width:680px;margin:42px 0 16px}.lede{max-width:680px;color:#4A4944;line-height:1.8;font-size:13px}.lede code{background:var(--soft);padding:2px 5px}
-  .how{display:grid;grid-template-columns:1fr 1fr;gap:32px;border-top:1px solid var(--grid);margin-top:38px;padding-top:20px}.how h2{font-size:10px;letter-spacing:.14em;color:var(--mut);margin:0 0 10px}.how p{font-size:12px;line-height:1.8;margin:0;color:#4A4944}.how table{width:100%;border-collapse:collapse;font-size:11px}.how td{border-bottom:1px dotted var(--grid);padding:6px 0}.how td:last-child{text-align:right;color:var(--mut)}
-  .group{border-top:1px solid var(--ink);margin-top:48px;padding-top:11px}.group h2{font-size:20px;margin:0}.group .when{color:var(--mut);font-size:11px;margin-top:5px}.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:24px 20px;margin-top:20px}.card{display:flex;flex-direction:column}.shot{display:block;aspect-ratio:4/5;border:1px solid var(--grid);background:var(--soft);overflow:hidden;position:relative}.shot img{display:block;width:100%;height:100%;object-fit:cover;object-position:top center;transition:transform .4s}.shot:hover img{transform:scale(1.03)}.no{position:absolute;left:0;top:0;background:var(--ink);color:var(--bg);font-size:9px;padding:4px 7px;font-weight:800}.card h3{font-size:12px;line-height:1.5;margin:10px 0 0}.en{font-size:9px;color:var(--mut);margin-top:2px}.use{font-size:10.5px;line-height:1.7;color:var(--mut);margin:6px 0 0}.links{display:flex;gap:12px;border-top:1px solid var(--grid);margin-top:10px;padding-top:8px;font-size:10px;font-weight:700}.links a{color:inherit;text-decoration:none;border-bottom:1px solid var(--grid)}
-  .foot{border-top:1px solid var(--ink);margin-top:54px;padding-top:12px;color:var(--mut);font-size:9px;letter-spacing:.08em;display:flex;justify-content:space-between;gap:20px}
-  @media(max-width:900px){.cards{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){body{padding:28px 14px}.how{grid-template-columns:1fr}.cards{grid-template-columns:1fr}.foot{display:block;line-height:2}}
-  @media(prefers-reduced-motion:reduce){.shot img{transition:none}.shot:hover img{transform:none}}
-</style>
-</head>
-<body><main class="sheet">
-  <div class="mast"><b>Lieflat Charts / 报告模板库</b><span>12 个模板 · 中英双版</span></div>
-  <h1>先想清楚这一页要替谁说话,再挑版式。</h1>
-  <p class="lede">每个模板是一整页可发布的报告骨架,不是单张图。复制对应语言的 HTML,替换数据、标题和来源即可。页内图表仍须从 <code>catalog.md</code> 选择真实图型;缩略图仅展示版式和信息密度。</p>
-  <div class="how"><section><h2>怎么挑</h2><p>先按报告类型、内容结构和阅读任务筛,再看版心和密度。调研 / 研究看 R01、R07、R08、R11;业务数据 / 财务经营看 R04、R09、R12;年度 / 长周期复盘看 R02、R03、R06;产品、项目和个人数据记录看 R05、R10。模板名称只是版式线索,不是行业限制。</p></section><section><h2>版心</h2><table><tr><td>760</td><td>窄栏叙事</td></tr><tr><td>880</td><td>中张单位图</td></tr><tr><td>980</td><td>常规报告</td></tr><tr><td>1080</td><td>宽版海报 / 仪表盘</td></tr><tr><td>600×1000</td><td>定尺社媒卡</td></tr></table></section></div>
-  <section class="group"><h2>调研、研究与外部发布</h2><div class="when">调研报告、研究简报、政策 / 市场洞察、白皮书和对外数据海报。</div><div class="cards">
-    <article class="card"><a class="shot" href="report-01.zh.html"><span class="no">R01</span><img src="../../docs/assets/reports/report-01.png" alt="调研报告 / 研究一页预览"></a><h3>调研报告 / 研究一页 · 青瓷侧栏</h3><div class="en">Survey One-Pager · Porcelain Rail</div><p class="use">主栏放结论,侧栏走方法与样本轨道。</p><div class="links"><a href="report-01.zh.html">中文版</a><a href="report-01.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-07.zh.html"><span class="no">R07</span><img src="../../docs/assets/reports/report-07.png" alt="调研 / 市场数据拼贴海报预览"></a><h3>调研 / 市场数据拼贴海报 · 椰林集市</h3><div class="en">Survey Collage Poster · Palm Bazaar</div><p class="use">巨号数字和多张图穿插成杂志拼贴。</p><div class="links"><a href="report-07.zh.html">中文版</a><a href="report-07.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-08.zh.html"><span class="no">R08</span><img src="../../docs/assets/reports/report-08.png" alt="人群 / 社会经济数据一页预览"></a><h3>人群 / 社会经济数据一页 · 百点场</h3><div class="en">Population One-Pager · Wire Hundred Field</div><p class="use">一句大字配一片可数的单位点阵。</p><div class="links"><a href="report-08.zh.html">中文版</a><a href="report-08.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-11.zh.html"><span class="no">R11</span><img src="../../docs/assets/reports/report-11.png" alt="研究 / 金融经济简报卡预览"></a><h3>研究 / 金融经济简报卡 · 灰阶定尺</h3><div class="en">Research Brief Card · Mono Fixed Frame</div><p class="use">600×1000 定尺,不滚动,适合单张分享。</p><div class="links"><a href="report-11.zh.html">中文版</a><a href="report-11.en.html">英文版</a></div></article>
-  </div></section>
-  <section class="group"><h2>经营、财务、年度与个人记录</h2><div class="when">业务数据报告、财报、dashboard、产品 / 项目复盘,以及运动、旅行和年度生活数据。</div><div class="cards">
-    <article class="card"><a class="shot" href="report-04.zh.html"><span class="no">R04</span><img src="../../docs/assets/reports/report-04.png" alt="月度业务 / 财务经营报告预览"></a><h3>月度业务 / 财务经营报告 · 青瓷月报</h3><div class="en">Monthly Ops · Mono Broadsheet</div><p class="use">按日折线、底部大数和榜单。</p><div class="links"><a href="report-04.zh.html">中文版</a><a href="report-04.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-09.zh.html"><span class="no">R09</span><img src="../../docs/assets/reports/report-09.png" alt="业务数据 / 财务经营 Dashboard 预览"></a><h3>业务数据 / 财务经营 Dashboard · 青瓷峰会</h3><div class="en">Data Story Dashboard · Porcelain Summit</div><p class="use">多模块一屏总览,右栏配 KPI。</p><div class="links"><a href="report-09.zh.html">中文版</a><a href="report-09.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-12.zh.html"><span class="no">R12</span><img src="../../docs/assets/reports/report-12.png" alt="周期数据快报 / 监控摘要预览"></a><h3>周期数据快报 / 监控摘要 · 椰林三秒</h3><div class="en">Weekly Glance · Palm Three Second</div><p class="use">区间、排序和指标在一屏内快速扫读。</p><div class="links"><a href="report-12.zh.html">中文版</a><a href="report-12.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-02.zh.html"><span class="no">R02</span><img src="../../docs/assets/reports/report-02.png" alt="年度复盘 / 业绩回顾预览"></a><h3>年度复盘 / 业绩回顾 · 椰林年报</h3><div class="en">Annual Milestones · Palm Kraft</div><p class="use">里程碑、趋势谱系和年度大数。</p><div class="links"><a href="report-02.zh.html">中文版</a><a href="report-02.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-03.zh.html"><span class="no">R03</span><img src="../../docs/assets/reports/report-03.png" alt="年度数据报告 / 年度海报预览"></a><h3>年度数据报告 / 年度海报 · 编辑部书脊</h3><div class="en">Year in Data · Wire Spine Poster</div><p class="use">书脊大标题、同构小图和时刻分布。</p><div class="links"><a href="report-03.zh.html">中文版</a><a href="report-03.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-05.zh.html"><span class="no">R05</span><img src="../../docs/assets/reports/report-05.png" alt="项目 / 产品影响力故事预览"></a><h3>项目 / 产品影响力故事 · 反色高亮</h3><div class="en">Impact Story · Mono Highlight</div><p class="use">窄栏主线叙事,关键词用反色高亮。</p><div class="links"><a href="report-05.zh.html">中文版</a><a href="report-05.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-06.zh.html"><span class="no">R06</span><img src="../../docs/assets/reports/report-06.png" alt="长周期产品 / 业务年鉴预览"></a><h3>长周期产品 / 业务年鉴 · 椰林深水区</h3><div class="en">Eight-Year Product Almanac · Palm Deep Water</div><p class="use">长周期、多维度、复杂证据的整页复盘。</p><div class="links"><a href="report-06.zh.html">中文版</a><a href="report-06.en.html">英文版</a></div></article>
-    <article class="card"><a class="shot" href="report-10.zh.html"><span class="no">R10</span><img src="../../docs/assets/reports/report-10.png" alt="个人数据 / 运动 / 旅行记录预览"></a><h3>个人数据 / 运动 / 旅行记录 · 椰林慢刊</h3><div class="en">Travel Notebook · Palm Slow Journal</div><p class="use">故事段落、小图和行程表交替向下。</p><div class="links"><a href="report-10.zh.html">中文版</a><a href="report-10.en.html">英文版</a></div></article>
-  </div></section>
-  <footer class="foot"><span>页内数字均为演示数据 · 生成报告时必须替换</span><span>LIEFLAT CHARTS</span></footer>
-</main></body></html>

+ 0 - 306
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-01.en.html

@@ -1,306 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Survey One-Pager — Lieflat Report Template · Porcelain Rail</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 01 · SURVEY ONE-PAGER
-     场景:调研 / 白皮书一页摘要(对外发布、公众号头图、PDF 附页)
-     排版参考:WARC "Health of creativity"(主栏 + 彩色侧栏轨道,
-               衬线大标题混排无衬线正文)—— 只学排版和字体。
-     色板正本 = color-presets.js 的 PORCELAIN 一套,不新增色值。
-     图表正本 = lieflat gallery:
-       图A  L15 ballot tally   · lupi-gallery.html  "What they fear, tick by tick"
-       图B  L14 hundred field  · lupi-gallery.html  "A hundred of us, four minds"
-       图C  F5  tick rows      · basics-gallery.html "Six teams, shipped and counted"
-     彩色加墨规则(INK_BOOST):发丝线宽 ×1.8,透明度地板 .85。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* PORCELAIN preset roles */
-    --bg:#F7F2EB; --txt:#081F5C;
-    --mut:rgba(8,31,92,.60); --lab:rgba(8,31,92,.72);
-    --faint:rgba(8,31,92,.32); --quiet:rgba(8,31,92,.15);
-    --track:rgba(8,31,92,.12); --grid:rgba(8,31,92,.16);
-    --data:#334EAC; --data2:#7096D1; --hero:#081F5C; --faintdata:#BAD6EB;
-    --railbg:#334EAC; --raildark:#081F5C;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:48px 24px}
-  .sheet{width:1080px;max-width:1080px;background:var(--bg);
-    display:grid;grid-template-columns:1fr 300px}
-
-  /* ── 主栏 ── */
-  .main{padding:56px 48px 44px 8px}
-  .serif{font-family:'Source Serif 4',serif}
-  h1{font-family:'Source Serif 4',serif;font-size:52px;font-weight:400;
-    letter-spacing:-.01em;line-height:1.03;margin-bottom:8px}
-  h1 b{font-weight:700}
-  .dek{font-family:'Source Serif 4',serif;font-size:26px;font-weight:400;
-    line-height:1.25;margin:44px 0 6px;max-width:520px}
-  .qhead{font-family:'Source Serif 4',serif;font-size:26px;line-height:1.25;
-    margin:60px 0 0;max-width:520px}
-  .kick{font-size:10px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
-    color:var(--mut);margin-bottom:14px}
-  .kick .n{color:var(--data)}
-  .claim{font-size:13.5px;font-weight:700;letter-spacing:-.01em;margin:34px 0 4px}
-  .body{font-size:12.5px;line-height:1.7;color:var(--lab);max-width:520px;margin-top:8px}
-  .body u{text-decoration-color:var(--data2);text-underline-offset:3px}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
-    color:var(--faint);margin-top:10px}
-  .fig{margin-top:14px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-
-  /* ── 底部小倍数 callout(纯排字,不占图额度)── */
-  .traits{display:grid;grid-template-columns:repeat(3,1fr);gap:26px;margin-top:44px;
-    border-top:1px solid var(--txt);padding-top:24px}
-  .traits .t .big{font-size:30px;font-weight:800;letter-spacing:-.03em;color:var(--data)}
-  .traits .t .nm{font-size:10.5px;font-weight:700;margin-top:8px}
-  .traits .t .ds{font-size:10.5px;line-height:1.55;color:var(--mut);margin-top:5px}
-
-  /* ── 侧栏轨道 ── */
-  .rail{display:flex;flex-direction:column}
-  .rail .top{background:var(--railbg);color:var(--bg);padding:56px 30px 48px;flex:1}
-  .rail .bot{background:var(--raildark);color:var(--bg);padding:44px 30px 52px}
-  .rail h3{font-size:16px;font-weight:700;letter-spacing:-.01em;line-height:1.3;margin-bottom:18px}
-  .rail p{font-size:11.5px;line-height:1.75;opacity:.88}
-  .rail p+p{margin-top:12px}
-  .rail .lbl{font-size:9px;font-weight:700;letter-spacing:.16em;text-transform:uppercase;
-    opacity:.6;margin-bottom:12px}
-  .rail .link{font-size:11px;font-weight:700;margin-top:20px;text-decoration:underline;
-    text-underline-offset:3px}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 主栏 ══ -->
-  <div class="main">
-    <h1>State of <b>shipping</b></h1>
-
-    <div class="dek">How often do small teams actually ship what they plan?</div>
-
-    <!-- 图A · L15 ballot tally(多选题百分比:各项独立 0–100)
-         模板:lupi-gallery.html · "What they fear, tick by tick" -->
-    <div class="kick" style="margin-top:30px"><span class="n">Q1</span> · What slows a release down</div>
-    <div class="claim">Review queues beat everything else — including scope creep</div>
-    <div class="fig">
-      <svg id="tally" viewBox="0 0 400 280" preserveAspectRatio="xMidYMid meet"></svg>
-    </div>
-    <div class="srcline">Ballot tally · one tick = one respondent of 100 · multi-select</div>
-
-    <div class="qhead">And how do they <i>feel</i> about their own pace?</div>
-
-    <!-- 图B · L14 hundred field(100% 构成 → 单位分解成 100 人)
-         模板:lupi-gallery.html · "A hundred of us, four minds" -->
-    <div class="kick" style="margin-top:26px"><span class="n">Q2</span> · Self-reported shipping mood</div>
-    <div class="claim">Confident and anxious are nearly the same crowd size</div>
-    <div class="fig">
-      <svg id="field" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
-    </div>
-    <div class="srcline">Hundred field · one dot = one respondent · 38 + 34 + 16 + 12 = 100</div>
-
-    <!-- 图C · F5 tick rows(少类目比较)
-         模板:basics-gallery.html · "Six teams, shipped and counted" -->
-    <div class="kick" style="margin-top:44px"><span class="n">Q3</span> · Releases shipped last quarter, by team size</div>
-    <div class="claim">Two-person teams out-ship everyone per head</div>
-    <div class="fig">
-      <svg id="rows" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet"></svg>
-    </div>
-    <div class="srcline">Tick rows · one tick = one release · dot marks every fifth</div>
-
-    <!-- 底部三特征 callout:纯数字排字,不占图额度 -->
-    <div class="kick" style="margin-top:52px">3 habits of teams that ship on time</div>
-    <div class="traits">
-      <div class="t"><div class="big">×2</div><div class="nm">Smaller batches</div>
-        <div class="ds">On-time teams cut scope twice as often as they extend deadlines.</div></div>
-      <div class="t"><div class="big">54%</div><div class="nm">Same-day review</div>
-        <div class="ds">More than half review every PR the day it opens.</div></div>
-      <div class="t"><div class="big">63%</div><div class="nm">Public changelog</div>
-        <div class="ds">Nearly two-thirds publish what shipped, every single week.</div></div>
-    </div>
-  </div>
-
-  <!-- ══ 侧栏轨道 ══ -->
-  <div class="rail">
-    <div class="top">
-      <div class="lbl">About this survey</div>
-      <h3>How often do teams ship what they planned?</h3>
-      <p>Every quarter we ask small software teams the same three questions about their release habits: what slows them down, how they feel about their pace, and what they actually shipped.</p>
-      <p>This edition compares 100 teams of one to twenty people. We look at whether the things teams blame — reviews, scope, meetings — line up with what their release logs say.</p>
-      <p>As team size grows, does shipping confidence grow with it? The short answer: no. The long answer is on the left.</p>
-    </div>
-    <div class="bot">
-      <div class="lbl">Methodology</div>
-      <p>A “release” is any user-visible change shipped to production, self-reported and cross-checked against public changelogs where available.</p>
-      <p>Respondents were surveyed between April and June 2026. Multi-select questions allow totals over 100.</p>
-      <div class="link">yoursite.com/survey</div>
-    </div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PORCELAIN preset(color-presets.js 原值)── */
-const P={BG:'#F7F2EB',TXT:'#081F5C',DATA:'#334EAC',DATA2:'#7096D1',HERO:'#081F5C',
-  FAINTDATA:'#BAD6EB',MUT:'rgba(8,31,92,.60)',FAINT:'rgba(8,31,92,.32)',
-  GRID:'rgba(8,31,92,.16)',CAT4:['#081F5C','#334EAC','#7096D1','#BAD6EB']};
-
-/* ════ 图A · L15 ballot tally ════
-   结构照搬 lupi-gallery "ballot tally":每行 100 根发丝 tick,
-   选中的上墨。彩色加墨:picked 线宽 .9→1.6,未选 .55→1.0。 */
-(()=>{
-const ASK=[['WAITING ON CODE REVIEW',57],['SCOPE GREW MID-SPRINT',44],['MEETINGS ATE THE WEEK',38],['CI PIPELINE FLAKED',21]];
-obsReveal('tally',s=>{
-  ASK.forEach(([name,v],i)=>{
-    const base=64+i*58;
-    txt(s,{x:24,y:base-26,'font-size':7.5,'font-weight':700,fill:P.MUT,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:P.GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=24+k*3.5,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?(i===0?P.HERO:P.DATA):P.FAINTDATA,'stroke-width':picked?1.6:1,
-        opacity:picked?1:.85,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.9,fill:P.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:24+(v-1)*3.5+9,y:base-11,'font-size':11,'font-weight':800,
-      fill:i===0?P.HERO:P.DATA,
-      style:`paint-order:stroke;stroke:${P.BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`${v} of 100 picked this — they could pick several`);
-  });
-  txt(s,{x:200,y:272,'font-size':7,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
-});
-})();
-
-/* ════ 图B · L14 hundred field ════
-   结构照搬 lupi-gallery "hundred field":golden-angle 撒布,
-   1 点 = 1 人,四簇 = 四种心态,虚线星座线连簇心。
-   色阶用 PORCELAIN.CAT4 按份额从深到浅。spoke 线宽 .6→1.1(×1.8)。 */
-(()=>{
-const SEG=[['CONFIDENT',38,P.CAT4[0]],['ANXIOUS',34,P.CAT4[1]],['DETACHED',16,P.CAT4[2]],['BURNED OUT',12,P.CAT4[3]]];
-const POS=[[128,142],[278,118],[182,254],[320,240]];
-obsReveal('field',s=>{
-  [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:P.GRID,'stroke-width':.8,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:P.FAINTDATA,'stroke-width':1.1,
-        opacity:.85,class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,opacity:.9,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} — one of ${v} in 100`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:P.HERO,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+13,'font-size':8,'font-weight':800,fill:P.TXT,'text-anchor':'middle',
-      'letter-spacing':'.1em',style:`paint-order:stroke;stroke:${P.BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:200,y:322,'font-size':7,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
-    'ONE DOT = ONE PERSON · 38 + 34 + 16 + 12 = 100');
-});
-})();
-
-/* ════ 图C · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows":一行 = 一种团队规模的发布队列,
-   1 tick = 1 次发布。tick 线宽 .9→1.6,透明度地板 .85。
-   HERO 只给第一行(人均最高的主角行)。 */
-(()=>{
-const D=[['SOLO',26],['2–3 PPL',34],['4–6 PPL',28],['7–10 PPL',19],['11–20 PPL',12]];
-obsReveal('rows',s=>{
-  const y0=i=>44+i*44,X0=104,PX=6.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===1;
-    txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:P.MUT,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+36*PX,y2:y+9,stroke:P.GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:hero?P.HERO:P.DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.9,fill:P.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,
-      fill:hero?P.HERO:P.DATA,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name} — ${v} releases`);
-  });
-  txt(s,{x:200,y:242,'font-size':7,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = ONE RELEASE · DOT MARKS EVERY FIFTH');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 308
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-01.zh.html

@@ -1,308 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>发布的真相 — Lieflat 报告模板 · 青瓷侧栏</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;0,8..60,700;1,8..60,400&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 01 · 调研一页纸(中文版)
-     场景:调研 / 白皮书一页摘要(对外发布、公众号头图、PDF 附页)
-     排版参考:WARC "Health of creativity"(主栏 + 彩色侧栏轨道,
-               衬线大标题混排无衬线正文)—— 只学排版和字体。
-     中西混排:拉丁与数字走 Source Serif 4 / Inter,汉字走 Noto,
-               中文标题不用负字距。
-     色板正本 = color-presets.js 的 PORCELAIN 一套,不新增色值。
-     图表正本 = lieflat gallery:
-       图A  L15 ballot tally   · lupi-gallery.html
-       图B  L14 hundred field  · lupi-gallery.html
-       图C  F5  tick rows      · basics-gallery.html
-     彩色加墨规则(INK_BOOST):发丝线宽 ×1.8,透明度地板 .85。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* PORCELAIN preset roles */
-    --bg:#F7F2EB; --txt:#081F5C;
-    --mut:rgba(8,31,92,.60); --lab:rgba(8,31,92,.72);
-    --faint:rgba(8,31,92,.32); --quiet:rgba(8,31,92,.15);
-    --track:rgba(8,31,92,.12); --grid:rgba(8,31,92,.16);
-    --data:#334EAC; --data2:#7096D1; --hero:#081F5C; --faintdata:#BAD6EB;
-    --railbg:#334EAC; --raildark:#081F5C;
-    --serif:'Source Serif 4','Noto Serif SC','Songti SC',serif;
-    --sans:'Inter','Noto Sans SC',sans-serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:var(--sans);
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:48px 24px}
-  .sheet{width:1080px;max-width:1080px;background:var(--bg);
-    display:grid;grid-template-columns:1fr 300px}
-
-  /* ── 主栏 ── */
-  .main{padding:56px 48px 44px 8px}
-  h1{font-family:var(--serif);font-size:50px;font-weight:400;
-    letter-spacing:.01em;line-height:1.2;margin-bottom:8px}
-  h1 b{font-weight:700}
-  .dek{font-family:var(--serif);font-size:25px;font-weight:400;
-    line-height:1.45;margin:44px 0 6px;max-width:520px}
-  .qhead{font-family:var(--serif);font-size:25px;line-height:1.45;
-    margin:60px 0 0;max-width:520px}
-  .kick{font-size:10px;font-weight:700;letter-spacing:.16em;
-    color:var(--mut);margin-bottom:14px}
-  .kick .n{color:var(--data)}
-  .claim{font-size:14px;font-weight:700;margin:34px 0 4px}
-  .body{font-size:12.5px;line-height:1.8;color:var(--lab);max-width:520px;margin-top:8px}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;
-    color:var(--faint);margin-top:10px}
-  .fig{margin-top:14px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-
-  /* ── 底部小倍数 callout(纯排字,不占图额度)── */
-  .traits{display:grid;grid-template-columns:repeat(3,1fr);gap:26px;margin-top:44px;
-    border-top:1px solid var(--txt);padding-top:24px}
-  .traits .t .big{font-size:30px;font-weight:800;letter-spacing:-.03em;color:var(--data)}
-  .traits .t .nm{font-size:11.5px;font-weight:700;margin-top:8px}
-  .traits .t .ds{font-size:10.5px;line-height:1.7;color:var(--mut);margin-top:5px}
-
-  /* ── 侧栏轨道 ── */
-  .rail{display:flex;flex-direction:column}
-  .rail .top{background:var(--railbg);color:var(--bg);padding:56px 30px 48px;flex:1}
-  .rail .bot{background:var(--raildark);color:var(--bg);padding:44px 30px 52px}
-  .rail h3{font-size:16.5px;font-weight:700;line-height:1.5;margin-bottom:18px}
-  .rail p{font-size:11.5px;line-height:1.9;opacity:.88}
-  .rail p+p{margin-top:12px}
-  .rail .lbl{font-size:9px;font-weight:700;letter-spacing:.16em;
-    opacity:.6;margin-bottom:12px}
-  .rail .link{font-size:11px;font-weight:700;margin-top:20px;text-decoration:underline;
-    text-underline-offset:3px}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 主栏 ══ -->
-  <div class="main">
-    <h1>发布的<b>真相</b></h1>
-
-    <div class="dek">小团队到底能不能按计划发布?</div>
-
-    <!-- 图A · L15 ballot tally(多选题百分比:各项独立 0–100)
-         模板:lupi-gallery.html · "What they fear, tick by tick" -->
-    <div class="kick" style="margin-top:30px"><span class="n">问题一</span> · 是什么拖慢了一次发布</div>
-    <div class="claim">排队等 review,比需求膨胀更拖进度</div>
-    <div class="fig">
-      <svg id="tally" viewBox="0 0 400 280" preserveAspectRatio="xMidYMid meet"></svg>
-    </div>
-    <div class="srcline">BALLOT TALLY · 一格 = 一位受访者,共 100 人 · 多选</div>
-
-    <div class="qhead">那他们自己怎么看这个节奏?</div>
-
-    <!-- 图B · L14 hundred field(100% 构成 → 单位分解成 100 人)
-         模板:lupi-gallery.html · "A hundred of us, four minds" -->
-    <div class="kick" style="margin-top:26px"><span class="n">问题二</span> · 对自己发布节奏的自评</div>
-    <div class="claim">从容和焦虑的人,几乎一样多</div>
-    <div class="fig">
-      <svg id="field" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg>
-    </div>
-    <div class="srcline">HUNDRED FIELD · 一点 = 一位受访者 · 38 + 34 + 16 + 12 = 100</div>
-
-    <!-- 图C · F5 tick rows(少类目比较)
-         模板:basics-gallery.html · "Six teams, shipped and counted" -->
-    <div class="kick" style="margin-top:44px"><span class="n">问题三</span> · 上季度实际发布次数,按团队规模</div>
-    <div class="claim">两人小队的人均发布量赢了所有人</div>
-    <div class="fig">
-      <svg id="rows" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet"></svg>
-    </div>
-    <div class="srcline">TICK ROWS · 一格 = 一次发布 · 每五格一个圆点</div>
-
-    <!-- 底部三特征 callout:纯数字排字,不占图额度 -->
-    <div class="kick" style="margin-top:52px">按时发布的团队,有这三个习惯</div>
-    <div class="traits">
-      <div class="t"><div class="big">×2</div><div class="nm">批次切得更小</div>
-        <div class="ds">按时交付的团队砍需求的频率,是延期的两倍。</div></div>
-      <div class="t"><div class="big">54%</div><div class="nm">当天完成 review</div>
-        <div class="ds">超过一半的团队,PR 开出当天就有人看。</div></div>
-      <div class="t"><div class="big">63%</div><div class="nm">公开更新日志</div>
-        <div class="ds">近三分之二每周把发布内容写出来给所有人看。</div></div>
-    </div>
-  </div>
-
-  <!-- ══ 侧栏轨道 ══ -->
-  <div class="rail">
-    <div class="top">
-      <div class="lbl">关于这份调研</div>
-      <h3>团队多久能发布一次自己计划过的东西?</h3>
-      <p>每个季度,我们向小型软件团队问同样的三个问题:什么拖慢了你们,你们怎么看自己的节奏,以及你们实际发布了什么。</p>
-      <p>本期比较了 100 个一到二十人的团队。我们想看看团队嘴上抱怨的东西——review、需求、会议——和他们发布记录里的事实,到底对不对得上。</p>
-      <p>团队变大,发布的底气会跟着变大吗?短答案:不会。长答案在左边。</p>
-    </div>
-    <div class="bot">
-      <div class="lbl">口径说明</div>
-      <p>「发布」指任何上线到生产环境、用户可感知的变更,由团队自报,并在有公开 changelog 时交叉核对。</p>
-      <p>调研时间为 2026 年 4 月至 6 月。多选题各项独立计数,合计可超过 100。</p>
-      <div class="link">yoursite.com/survey</div>
-    </div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PORCELAIN preset(color-presets.js 原值)── */
-const P={BG:'#F7F2EB',TXT:'#081F5C',DATA:'#334EAC',DATA2:'#7096D1',HERO:'#081F5C',
-  FAINTDATA:'#BAD6EB',MUT:'rgba(8,31,92,.60)',FAINT:'rgba(8,31,92,.32)',
-  GRID:'rgba(8,31,92,.16)',CAT4:['#081F5C','#334EAC','#7096D1','#BAD6EB']};
-
-/* ════ 图A · L15 ballot tally ════
-   结构照搬 lupi-gallery "ballot tally":每行 100 根发丝 tick,
-   选中的上墨。彩色加墨:picked 线宽 .9→1.6,未选 .55→1.0。 */
-(()=>{
-const ASK=[['等代码 REVIEW',57],['需求中途膨胀',44],['会议吃掉整周',38],['CI 流水线抽风',21]];
-obsReveal('tally',s=>{
-  ASK.forEach(([name,v],i)=>{
-    const base=64+i*58;
-    txt(s,{x:24,y:base-26,'font-size':8,'font-weight':700,fill:P.MUT,
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:P.GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=24+k*3.5,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?(i===0?P.HERO:P.DATA):P.FAINTDATA,'stroke-width':picked?1.6:1,
-        opacity:picked?1:.85,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.9,fill:P.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:24+(v-1)*3.5+9,y:base-11,'font-size':11,'font-weight':800,
-      fill:i===0?P.HERO:P.DATA,
-      style:`paint-order:stroke;stroke:${P.BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`100 人中有 ${v} 人选了这一项(可多选)`);
-  });
-  txt(s,{x:200,y:272,'font-size':7.5,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
-    '一格 = 一位受访者 · 每十格一个圆点');
-});
-})();
-
-/* ════ 图B · L14 hundred field ════
-   结构照搬 lupi-gallery "hundred field":golden-angle 撒布,
-   1 点 = 1 人,四簇 = 四种心态,虚线星座线连簇心。
-   色阶用 PORCELAIN.CAT4 按份额从深到浅。spoke 线宽 .6→1.1(×1.8)。 */
-(()=>{
-const SEG=[['从容',38,P.CAT4[0]],['焦虑',34,P.CAT4[1]],['游离',16,P.CAT4[2]],['燃尽',12,P.CAT4[3]]];
-const POS=[[128,142],[278,118],[182,254],[320,240]];
-obsReveal('field',s=>{
-  [[0,1],[0,2],[1,3],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:P.GRID,'stroke-width':.8,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.9+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:P.FAINTDATA,'stroke-width':1.1,
-        opacity:.85,class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,opacity:.9,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} —— 100 人中的 ${v} 人之一`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:P.HERO,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+13,'font-size':8.5,'font-weight':800,fill:P.TXT,'text-anchor':'middle',
-      'letter-spacing':'.08em',style:`paint-order:stroke;stroke:${P.BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:200,y:322,'font-size':7.5,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.3s'},
-    '一点 = 一个人 · 38 + 34 + 16 + 12 = 100');
-});
-})();
-
-/* ════ 图C · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows":一行 = 一种团队规模的发布队列,
-   1 tick = 1 次发布。tick 线宽 .9→1.6,透明度地板 .85。
-   HERO 只给一个主角行(人均最高的两人小队)。 */
-(()=>{
-const D=[['单人',26],['2–3 人',34],['4–6 人',28],['7–10 人',19],['11–20 人',12]];
-obsReveal('rows',s=>{
-  const y0=i=>44+i*44,X0=104,PX=6.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===1;
-    txt(s,{x:94,y:y+3,'font-size':8.5,'font-weight':700,fill:P.MUT,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+36*PX,y2:y+9,stroke:P.GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:hero?P.HERO:P.DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.9,fill:P.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,
-      fill:hero?P.HERO:P.DATA,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name}团队 —— ${v} 次发布`);
-  });
-  txt(s,{x:200,y:242,'font-size':7.5,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一格 = 一次发布 · 每五格一个圆点');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 351
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-02.en.html

@@ -1,351 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Annual Milestones — Lieflat Report Template · Palm Kraft</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Playfair+Display:ital,wght@0,500;0,700;0,800;1,500&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 02 · ANNUAL MILESTONES
-     场景:年度回顾 / 基金会式年报一页(投资人更新、社区年报、复盘)
-     排版参考:9/11 VCF 年报信息图(牛皮纸衬底 + 纸张、顶部里程碑
-               时间轴行、全大写字距节标题 + 下划线、衬线斜体引文、
-               底部大数收尾)—— 只学排版和字体。
-     色板正本 = color-presets.js 的 PALM 一套,不新增色值。
-       衬底 = PALM.TXT 浓咖 #58402E(当牛皮纸);纸张 = PALM.BG。
-       强调块 = HERO 琥珀 #D4A017;深块 = DATA 深绿 #43593B。
-     图表正本 = lieflat gallery:
-       图A  F5  tick rows       · basics-gallery.html "Six teams, shipped and counted"
-       图B  L11 trend lineage   · lupi-gallery.html "Features rise, fall, come back"
-       图C  F11 tick gauge      · basics-gallery.html "How far to the quarter's goal"
-     彩色加墨规则(INK_BOOST):发丝线宽 ×1.8,透明度地板 .85。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* PALM preset roles */
-    --paper:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.32); --quiet:rgba(88,64,46,.15);
-    --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#58402E; --hero:#D4A017;
-    --faintdata:#ACAD79; --bead:#77835A;
-    --mat:#58402E;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--mat);font-family:'Inter',sans-serif;color:var(--txt);
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 32px}
-  .sheet{width:980px;max-width:980px;background:var(--paper);padding:64px 68px 72px}
-
-  /* ── 节标题:全大写 + 字距 + 底线(学参考图2)── */
-  .sect{font-size:13px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
-    padding-bottom:10px;border-bottom:2px solid var(--txt);margin-bottom:26px}
-  .sect .yr{float:right;font-weight:600;color:var(--mut);letter-spacing:.1em}
-
-  /* ── 顶部里程碑时间轴(callout,不占图额度)── */
-  .miles{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
-  .miles .m{padding:18px 16px 16px;position:relative}
-  .miles .m.amber{background:var(--hero);color:var(--paper)}
-  .miles .m.frame{border:1px solid var(--faint)}
-  .miles .m.green{background:var(--data);color:var(--paper)}
-  .miles .m .v{font-size:24px;font-weight:800;letter-spacing:-.03em;line-height:1;margin-top:6px}
-  .miles .m .l{font-size:10px;line-height:1.45;margin-top:7px;opacity:.85}
-  .miles .m .tag{font-size:8.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;opacity:.7}
-  .timeline{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:0}
-  .timeline .yr{text-align:center;font-size:11px;font-weight:700;letter-spacing:.08em;
-    padding-top:14px;position:relative}
-  .timeline .yr::before{content:'';position:absolute;top:0;left:50%;width:1px;height:10px;
-    background:var(--faint)}
-
-  /* ── 中段两栏 ── */
-  .duo{display:grid;grid-template-columns:1.15fr 1fr;gap:56px;margin-top:56px}
-
-  /* ── 衬线引文(学参考图2 的斜体衬线 quote)── */
-  .quote{margin-top:40px}
-  .quote .mark{font-family:'Playfair Display',serif;font-size:40px;font-weight:800;
-    color:var(--hero);line-height:.4;display:block;margin-bottom:2px}
-  .quote .q{font-family:'Playfair Display',serif;font-style:italic;font-size:25px;
-    font-weight:500;line-height:1.35;letter-spacing:-.01em}
-  .quote .who{font-size:10.5px;color:var(--mut);margin-top:14px;line-height:1.6}
-  .quote .who b{color:var(--lab);font-weight:700}
-
-  /* ── 图 ── */
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-  .claim{font-size:13px;font-weight:700;margin-bottom:2px}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
-    color:var(--faint);margin-top:8px}
-
-  /* ── 底部收尾:图 + 大数对话框 ── */
-  .finale{display:grid;grid-template-columns:1.2fr 1fr;gap:48px;margin-top:56px;align-items:center}
-  .bignum{border:1px solid var(--faint);padding:34px 30px;position:relative}
-  .bignum::before{content:'';position:absolute;left:-8px;top:50%;width:14px;height:14px;
-    transform:translateY(-50%) rotate(45deg);background:var(--paper);
-    border-left:1px solid var(--faint);border-bottom:1px solid var(--faint)}
-  .bignum .v{font-family:'Playfair Display',serif;font-size:64px;font-weight:800;
-    letter-spacing:-.02em;line-height:1;color:var(--data)}
-  .bignum .v small{font-size:30px}
-  .bignum .l{font-size:12px;font-weight:700;line-height:1.5;margin-top:14px}
-  .bignum .d{font-size:10.5px;color:var(--mut);line-height:1.55;margin-top:6px}
-
-  .foot{margin-top:64px;padding-top:16px;border-top:2px solid var(--txt);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--mut)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 顶部:里程碑时间轴 ══ -->
-  <div class="sect">Funding and product milestones <span class="yr">2022 – 2026</span></div>
-  <div class="miles">
-    <div class="m amber"><div class="tag">Pre-seed</div><div class="v">$480k</div>
-      <div class="l">First cheque, two founders, one prototype</div></div>
-    <div class="m frame"><div class="tag">Seed</div><div class="v">$3.2M</div>
-      <div class="l">Product out of beta, first 1,000 teams</div></div>
-    <div class="m frame"><div class="tag">Revenue</div><div class="v">$1.1M</div>
-      <div class="l">ARR crossed seven figures in year three</div></div>
-    <div class="m frame"><div class="tag">Series A</div><div class="v">$14M</div>
-      <div class="l">Doubled the team, opened EU region</div></div>
-    <div class="m green"><div class="tag">Today</div><div class="v">$4.6M</div>
-      <div class="l">ARR run-rate, default-alive since March</div></div>
-  </div>
-  <div class="timeline">
-    <div class="yr">2022</div><div class="yr">2023</div><div class="yr">2024</div>
-    <div class="yr">2025</div><div class="yr">2026</div>
-  </div>
-
-  <!-- ══ 中段:横条 + 引文 | 环形分布 ══ -->
-  <div class="duo">
-    <div>
-      <div class="sect" style="border-bottom-width:1px">Where the money went</div>
-      <!-- 图A · F5 tick rows
-           模板:basics-gallery.html · "Six teams, shipped and counted" -->
-      <div class="claim">Payroll is four of every five dollars</div>
-      <div class="fig"><svg id="spendrows" viewBox="0 0 400 252" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Tick rows · one tick = $20k spent in FY2026</div>
-
-      <div class="quote">
-        <span class="mark">“</span>
-        <div class="q">This was the year the machine started paying for itself.</div>
-        <div class="who">FY2026 letter to investors<br><b>Annual report, p.2</b></div>
-      </div>
-    </div>
-
-    <div>
-      <div class="sect" style="border-bottom-width:1px">Feature history</div>
-      <!-- 图B · L11 trend lineage
-           模板:lupi-gallery.html · "Features rise, fall, come back" -->
-      <div class="claim">Two of ten features went dark. One clawed its way back.</div>
-      <div class="fig"><svg id="lineage" viewBox="0 0 400 365" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Trend lineage · ● shipped · ○ reworked · dashed = dormant · line runs on if still alive</div>
-    </div>
-  </div>
-
-  <!-- ══ 底部收尾:进度表盘 + 大数 ══ -->
-  <div class="sect" style="margin-top:56px">Progress against the 2027 plan</div>
-  <div class="finale">
-    <div>
-      <!-- 图C · F11 tick gauge
-           模板:basics-gallery.html · "How far to the quarter's goal" -->
-      <div class="fig"><svg id="gauge" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Tick gauge · one tick = 1% of the $7M ARR target</div>
-    </div>
-    <div class="bignum">
-      <div class="v">66<small>%</small></div>
-      <div class="l">of the road to $7M ARR is already behind us</div>
-      <div class="d">Plan set January 2026 · at current net revenue retention the target lands in Q3 2027, two quarters early.</div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>FY2026 Annual One-Pager</span>
-    <span>Data · internal ledger + Stripe · audited Feb 2027</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PALM preset(color-presets.js 原值)── */
-const P={BG:'#F0EFEB',TXT:'#58402E',DATA:'#43593B',DATA2:'#58402E',HERO:'#D4A017',
-  FAINTDATA:'#ACAD79',BEAD:'#77835A',MUT:'rgba(88,64,46,.60)',
-  FAINT:'rgba(88,64,46,.32)',GRID:'rgba(88,64,46,.16)',
-  QUIET:'rgba(88,64,46,.15)',LAB:'rgba(88,64,46,.72)',
-  RAMP:['#F2D17E','#ACAD79','#929960','#77835A','#43593B'],
-  CAT4:['#43593B','#77835A','#ACAD79','#F2D17E'],CAT3:['#43593B','#929960','#F2D17E']};
-
-/* ════ 图A · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows":一行 = 一个开支类目,
-   1 tick = $20k。tick 线宽 .9→1.6,透明度地板 .85。
-   HERO 琥珀只给一个主角行(payroll)。 */
-(()=>{
-const D=[['PAYROLL',41],['INFRA & TOOLS',9],['MARKETING',7],['OFFICE & OPS',4],['LEGAL',2]];
-obsReveal('spendrows',s=>{
-  const y0=i=>40+i*42,X0=112,PX=5.4;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:102,y:y+3,'font-size':8,'font-weight':700,fill:P.MUT,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+43*PX,y2:y+9,stroke:P.GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:hero?P.HERO:P.DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.9,fill:P.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const amount = v*20>=1000 ? '$'+(v*20/1000).toFixed(2).replace(/0+$/,'').replace(/\.$/,'')+'M' : '$'+(v*20)+'k';
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,
-      fill:hero?P.HERO:P.DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},amount);
-    tip(lab,`${name} — ${v} ticks × $20k`);
-  });
-  txt(s,{x:200,y:246,'font-size':7,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = $20K · DOT MARKS EVERY FIFTH · AMBER = LARGEST LINE');
-});
-})();
-
-/* ════ 图B · L11 trend lineage ════
-   结构照搬 lupi-gallery "trend lineage":一列 = 一个功能,
-   实心 = 上线,空心 = 改版;两次动作相隔 > 2 年记一段虚线(沉寂期);
-   存活的功能续画到 2026;HERO 琥珀只给"沉寂最久之后杀回来"的那一条。
-   年份轴从年报的 2022 起、到 2026 止,和顶部里程碑时间轴同一跨度。 */
-(()=>{
-const TR=[
-  ['DARK MODE',[[2022,'f'],[2024,'o']],true],
-  ['KANBAN',[[2022,'f']],true],
-  ['CMD PALETTE',[[2023,'f']],false],
-  ['LIVE CURSORS',[[2023,'f'],[2025,'o']],true],
-  ['TEMPLATES',[[2022,'f'],[2025,'o']],true],
-  ['INBOX',[[2023,'f']],false],
-  ['AI SUMMARIES',[[2024,'f'],[2025,'o']],true],
-  ['VOICE NOTES',[[2022,'f'],[2026,'o']],true],
-];
-const Y0=2022,yY=yr=>34+(yr-Y0)*60,colX=c=>84+c*36;
-// 主角:最近一次有动作的功能线 —— 标题里的 "come back"
-const HC=TR.map((r,i)=>[r[1][r[1].length-1][0],i]).sort((a,b)=>b[0]-a[0])[0][1];
-obsReveal('lineage',s=>{
-  for(let yr=Y0;yr<=2026;yr++){
-    el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:P.QUIET,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
-    txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:P.MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
-  }
-  TR.forEach(([name,ev,alive],c)=>{
-    const x=colX(c),hero=c===HC;
-    const lastYr=ev[ev.length-1][0];
-    // 越近期动过 = 越重;已沉寂的走最浅一档(虚线本身已经在说沉寂)
-    const col=hero?P.HERO:P.RAMP[alive?Math.min(4,Math.max(2,lastYr-Y0)):1];
-    for(let k=0;k<ev.length-1;k++){
-      const gap=ev[k+1][0]-ev[k][0];
-      el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
-        stroke:col,'stroke-width':hero?1.8:1.4,
-        'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
-        class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
-    }
-    const last=ev[ev.length-1][0];
-    if(alive)
-      el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:col,'stroke-width':hero?1.8:1.4,
-        pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
-    ev.forEach(([yr,t])=>{
-      const node=el(s,'circle',t==='f'
-        ?{cx:x,cy:yY(yr),r:5.5,fill:col}
-        :{cx:x,cy:yY(yr),r:5.5,fill:P.BG,stroke:col,'stroke-width':1.6});
-      node.setAttribute('class','pop');
-      node.style.animationDelay=(.35+c*.07)+'s';
-      tip(node,`${name} — ${t==='f'?'shipped':'reworked'} ${yr}`);
-    });
-    // terminal dot: alive lands on today's baseline, dead stops where it stopped
-    el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
-      fill:col,class:'pop',style:`animation-delay:${.6+c*.07}s`});
-    const ly=yY(2026)+26;
-    txt(s,{x,y:ly,'font-size':8.5,'font-weight':hero?800:600,fill:hero?P.HERO:(alive?P.LAB:P.FAINT),
-      transform:`rotate(-52 ${x} ${ly})`,'text-anchor':'end',
-      class:'fade',style:`${hero?`paint-order:stroke;stroke:${P.BG};stroke-width:3px;`:''}animation-delay:${.7+c*.05}s`},name);
-  });
-});
-})();
-
-/* ════ 图C · F11 tick gauge ════
-   结构照搬 basics-gallery "tick gauge":210° 表盘,1 tick = 1% 目标。
-   已达部分上深绿墨,尖端琥珀珠收笔。tick 线宽 1→1.8 / .6→1.1。 */
-(()=>{
-const GOAL=66;
-obsReveal('gauge',s=>{
-  const cx=200,cy=186,R0=100,A0=-195,SW=210;
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?13+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?P.DATA:P.FAINTDATA,'stroke-width':inked?1.8:1.1,
-      opacity:inked?1:.85,class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);
-    el(s,'circle',{cx:dx,cy:dy,r:1,fill:P.BEAD,class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:2.8,fill:P.HERO,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:P.DATA,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},GOAL+'%');
-  tip(num,'$4.6M of the $7M ARR target');
-  txt(s,{x:cx,y:cy+16,'font-size':8,'font-weight':600,fill:P.MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'34 TICKS TO GO');
-  txt(s,{x:200,y:292,'font-size':7,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE TICK = 1% OF TARGET · INKED = EARNED · AMBER BEAD = TODAY');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 355
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-02.zh.html

@@ -1,355 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>年度里程碑 — Lieflat 报告模板 · 椰林年报</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Playfair+Display:wght@500;700;800&family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 02 · 年度里程碑(中文版)
-     场景:年度回顾 / 基金会式年报一页(投资人更新、社区年报、复盘)
-     排版参考:9/11 VCF 年报信息图(牛皮纸衬底 + 纸张、顶部里程碑
-               时间轴行、全大写字距节标题 + 下划线、衬线引文、
-               底部大数收尾)—— 只学排版和字体。
-     中西混排:数字与拉丁走 Playfair / Inter,汉字走 Noto。
-               中文引文不用斜体(汉字没有真斜体),改常规衬线。
-     色板正本 = color-presets.js 的 PALM 一套,不新增色值。
-       衬底 = PALM.TXT 浓咖 #58402E(当牛皮纸);纸张 = PALM.BG。
-       强调块 = HERO 琥珀 #D4A017;深块 = DATA 深绿 #43593B。
-     图表正本 = lieflat gallery:
-       图A  F5  tick rows       · basics-gallery.html
-       图B  L11 trend lineage   · lupi-gallery.html
-       图C  F11 tick gauge      · basics-gallery.html
-     彩色加墨规则(INK_BOOST):发丝线宽 ×1.8,透明度地板 .85。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* PALM preset roles */
-    --paper:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.32); --quiet:rgba(88,64,46,.15);
-    --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#58402E; --hero:#D4A017;
-    --faintdata:#ACAD79; --bead:#77835A;
-    --mat:#58402E;
-    --serifzh:'Playfair Display','Noto Serif SC','Songti SC',serif;
-    --sans:'Inter','Noto Sans SC',sans-serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--mat);font-family:var(--sans);color:var(--txt);
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 32px}
-  .sheet{width:980px;max-width:980px;background:var(--paper);padding:64px 68px 72px}
-
-  /* ── 节标题:字距 + 底线(学参考图2)── */
-  .sect{font-size:14px;font-weight:800;letter-spacing:.2em;
-    padding-bottom:10px;border-bottom:2px solid var(--txt);margin-bottom:26px}
-  .sect .yr{float:right;font-weight:600;color:var(--mut);letter-spacing:.1em}
-
-  /* ── 顶部里程碑时间轴(callout,不占图额度)── */
-  .miles{display:grid;grid-template-columns:repeat(5,1fr);gap:10px}
-  .miles .m{padding:18px 16px 16px;position:relative}
-  .miles .m.amber{background:var(--hero);color:var(--paper)}
-  .miles .m.frame{border:1px solid var(--faint)}
-  .miles .m.green{background:var(--data);color:var(--paper)}
-  .miles .m .v{font-size:24px;font-weight:800;letter-spacing:-.03em;line-height:1;margin-top:6px}
-  .miles .m .l{font-size:10px;line-height:1.6;margin-top:7px;opacity:.85}
-  .miles .m .tag{font-size:8.5px;font-weight:700;letter-spacing:.14em;opacity:.7}
-  .timeline{display:grid;grid-template-columns:repeat(5,1fr);gap:10px;margin-top:0}
-  .timeline .yr{text-align:center;font-size:11px;font-weight:700;letter-spacing:.08em;
-    padding-top:14px;position:relative}
-  .timeline .yr::before{content:'';position:absolute;top:0;left:50%;width:1px;height:10px;
-    background:var(--faint)}
-
-  /* ── 中段两栏 ── */
-  .duo{display:grid;grid-template-columns:1.15fr 1fr;gap:56px;margin-top:56px}
-
-  /* ── 衬线引文(中文不用斜体)── */
-  .quote{margin-top:40px}
-  .quote .mark{font-family:'Playfair Display',serif;font-size:40px;font-weight:800;
-    color:var(--hero);line-height:.4;display:block;margin-bottom:2px}
-  .quote .q{font-family:var(--serifzh);font-size:23px;
-    font-weight:600;line-height:1.6;letter-spacing:.01em}
-  .quote .who{font-size:10.5px;color:var(--mut);margin-top:14px;line-height:1.7}
-  .quote .who b{color:var(--lab);font-weight:700}
-
-  /* ── 图 ── */
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .claim{font-size:13.5px;font-weight:700;margin-bottom:2px}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;
-    color:var(--faint);margin-top:8px}
-
-  /* ── 底部收尾:图 + 大数对话框 ── */
-  .finale{display:grid;grid-template-columns:1.2fr 1fr;gap:48px;margin-top:56px;align-items:center}
-  .bignum{border:1px solid var(--faint);padding:34px 30px;position:relative}
-  .bignum::before{content:'';position:absolute;left:-8px;top:50%;width:14px;height:14px;
-    transform:translateY(-50%) rotate(45deg);background:var(--paper);
-    border-left:1px solid var(--faint);border-bottom:1px solid var(--faint)}
-  .bignum .v{font-family:'Playfair Display','Noto Serif SC',serif;font-size:64px;font-weight:800;
-    letter-spacing:-.02em;line-height:1;color:var(--data)}
-  .bignum .v small{font-size:30px}
-  .bignum .l{font-size:12.5px;font-weight:700;line-height:1.7;margin-top:14px}
-  .bignum .d{font-size:10.5px;color:var(--mut);line-height:1.7;margin-top:6px}
-
-  .foot{margin-top:64px;padding-top:16px;border-top:2px solid var(--txt);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.14em;color:var(--mut)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 顶部:里程碑时间轴 ══ -->
-  <div class="sect">融资与产品里程碑 <span class="yr">2022 – 2026</span></div>
-  <div class="miles">
-    <div class="m amber"><div class="tag">种子前</div><div class="v">$480k</div>
-      <div class="l">第一张支票,两位创始人,一个原型</div></div>
-    <div class="m frame"><div class="tag">种子轮</div><div class="v">$3.2M</div>
-      <div class="l">产品走出 beta,前 1,000 个团队</div></div>
-    <div class="m frame"><div class="tag">营收</div><div class="v">$1.1M</div>
-      <div class="l">第三年 ARR 跨过七位数</div></div>
-    <div class="m frame"><div class="tag">A 轮</div><div class="v">$14M</div>
-      <div class="l">团队翻倍,开通欧洲区域</div></div>
-    <div class="m green"><div class="tag">现在</div><div class="v">$4.6M</div>
-      <div class="l">ARR 年化,三月起自负盈亏</div></div>
-  </div>
-  <div class="timeline">
-    <div class="yr">2022</div><div class="yr">2023</div><div class="yr">2024</div>
-    <div class="yr">2025</div><div class="yr">2026</div>
-  </div>
-
-  <!-- ══ 中段:横条 + 引文 | 环形分布 ══ -->
-  <div class="duo">
-    <div>
-      <div class="sect" style="border-bottom-width:1px">钱花到了哪里</div>
-      <!-- 图A · F5 tick rows
-           模板:basics-gallery.html · "Six teams, shipped and counted" -->
-      <div class="claim">每五块钱,有四块发了工资</div>
-      <div class="fig"><svg id="spendrows" viewBox="0 0 400 252" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">TICK ROWS · 一格 = 2026 财年支出的 $20k</div>
-
-      <div class="quote">
-        <span class="mark">“</span>
-        <div class="q">这一年,机器开始自己养活自己了。</div>
-        <div class="who">2026 财年致投资人信<br><b>年报第 2 页</b></div>
-      </div>
-    </div>
-
-    <div>
-      <div class="sect" style="border-bottom-width:1px">功能起落</div>
-      <!-- 图B · L11 trend lineage
-           模板:lupi-gallery.html · "Features rise, fall, come back" -->
-      <div class="claim">八项功能里,两项已沉寂,一项杀了个回马枪</div>
-      <div class="fig"><svg id="lineage" viewBox="0 0 400 365" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">TREND LINEAGE · ● 上线 · ○ 改版 · 虚线 = 沉寂 · 存活功能续画到今天</div>
-    </div>
-  </div>
-
-  <!-- ══ 底部收尾:进度表盘 + 大数 ══ -->
-  <div class="sect" style="margin-top:56px">2027 目标进度</div>
-  <div class="finale">
-    <div>
-      <!-- 图C · F11 tick gauge
-           模板:basics-gallery.html · "How far to the quarter's goal" -->
-      <div class="fig"><svg id="gauge" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">TICK GAUGE · 一格 = 700 万美金 ARR 目标的 1%</div>
-    </div>
-    <div class="bignum">
-      <div class="v">66<small>%</small></div>
-      <div class="l">通往 $7M ARR 的路,已经走完三分之二</div>
-      <div class="d">目标定于 2026 年 1 月 · 按当前净收入留存率推算,2027 年三季度即可达成,比计划提前两个季度。</div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>2026 财年 · 年度一页纸</span>
-    <span>数据 · 内部账本 + Stripe · 2027 年 2 月核验</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PALM preset(color-presets.js 原值)── */
-const P={BG:'#F0EFEB',TXT:'#58402E',DATA:'#43593B',DATA2:'#58402E',HERO:'#D4A017',
-  FAINTDATA:'#ACAD79',BEAD:'#77835A',MUT:'rgba(88,64,46,.60)',
-  FAINT:'rgba(88,64,46,.32)',GRID:'rgba(88,64,46,.16)',
-  QUIET:'rgba(88,64,46,.15)',LAB:'rgba(88,64,46,.72)',
-  RAMP:['#F2D17E','#ACAD79','#929960','#77835A','#43593B'],
-  CAT4:['#43593B','#77835A','#ACAD79','#F2D17E'],CAT3:['#43593B','#929960','#F2D17E']};
-
-/* ════ 图A · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows":一行 = 一个开支类目,
-   1 tick = $20k。tick 线宽 .9→1.6,透明度地板 .85。
-   HERO 琥珀只给一个主角行(工资)。 */
-(()=>{
-const D=[['工资',41],['基础设施与工具',9],['市场投放',7],['办公与运营',4],['法务',2]];
-obsReveal('spendrows',s=>{
-  const y0=i=>40+i*42,X0=112,PX=5.7;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:102,y:y+3,'font-size':8.5,'font-weight':700,fill:P.MUT,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+9,x2:X0+43*PX,y2:y+9,stroke:P.GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=9+rnd(k+1,i+2)*6;
-      el(s,'line',{x1:x,y1:y+9,x2:x,y2:y+9-h,stroke:hero?P.HERO:P.DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+13,r:.9,fill:P.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const amount = v*20>=1000 ? '$'+(v*20/1000).toFixed(2).replace(/0+$/,'').replace(/\.$/,'')+'M' : '$'+(v*20)+'k';
-    const lab=txt(s,{x:X0+v*PX+10,y:y+4,'font-size':11,'font-weight':800,
-      fill:hero?P.HERO:P.DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},amount);
-    tip(lab,`${name} —— ${v} 格 × $20k`);
-  });
-  txt(s,{x:200,y:246,'font-size':7.5,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一格 = $20K · 每五格一个圆点 · 琥珀 = 最大开支项');
-});
-})();
-
-/* ════ 图B · L11 trend lineage ════
-   结构照搬 lupi-gallery "trend lineage":一列 = 一个功能,
-   实心 = 上线,空心 = 改版;两次动作相隔 > 2 年记一段虚线(沉寂期);
-   存活的功能续画到 2026;HERO 琥珀只给"沉寂最久之后杀回来"的那一条。
-   年份轴从年报的 2022 起、到 2026 止,和顶部里程碑时间轴同一跨度。 */
-(()=>{
-const TR=[
-  ['暗色模式',[[2022,'f'],[2024,'o']],true],
-  ['看板',[[2022,'f']],true],
-  ['命令面板',[[2023,'f']],false],
-  ['协作光标',[[2023,'f'],[2025,'o']],true],
-  ['模板库',[[2022,'f'],[2025,'o']],true],
-  ['收件箱',[[2023,'f']],false],
-  ['AI 摘要',[[2024,'f'],[2025,'o']],true],
-  ['语音批注',[[2022,'f'],[2026,'o']],true],
-];
-const Y0=2022,yY=yr=>34+(yr-Y0)*60,colX=c=>84+c*36;
-// 主角:最近一次有动作的功能线 —— 标题里的"回马枪"
-const HC=TR.map((r,i)=>[r[1][r[1].length-1][0],i]).sort((a,b)=>b[0]-a[0])[0][1];
-obsReveal('lineage',s=>{
-  for(let yr=Y0;yr<=2026;yr++){
-    el(s,'line',{x1:60,y1:yY(yr),x2:372,y2:yY(yr),stroke:P.QUIET,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`});
-    txt(s,{x:52,y:yY(yr)+3,'font-size':8,'font-weight':600,fill:P.MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${(yr-Y0)*.03}s`},yr);
-  }
-  TR.forEach(([name,ev,alive],c)=>{
-    const x=colX(c),hero=c===HC;
-    const lastYr=ev[ev.length-1][0];
-    // 越近期动过 = 越重;已沉寂的走最浅一档(虚线本身已经在说沉寂)
-    const col=hero?P.HERO:P.RAMP[alive?Math.min(4,Math.max(2,lastYr-Y0)):1];
-    for(let k=0;k<ev.length-1;k++){
-      const gap=ev[k+1][0]-ev[k][0];
-      el(s,'line',{x1:x,y1:yY(ev[k][0])+7,x2:x,y2:yY(ev[k+1][0])-7,
-        stroke:col,'stroke-width':hero?1.8:1.4,
-        'stroke-dasharray':gap>2?'2 4':'none',pathLength:1,
-        class:'draw',style:`animation-delay:${.3+c*.07}s;animation-duration:.6s`});
-    }
-    const last=ev[ev.length-1][0];
-    if(alive)
-      el(s,'line',{x1:x,y1:yY(last)+7,x2:x,y2:yY(2026)+16,stroke:col,'stroke-width':hero?1.8:1.4,
-        pathLength:1,class:'draw',style:`animation-delay:${.45+c*.07}s;animation-duration:.6s`});
-    ev.forEach(([yr,t])=>{
-      const node=el(s,'circle',t==='f'
-        ?{cx:x,cy:yY(yr),r:5.5,fill:col}
-        :{cx:x,cy:yY(yr),r:5.5,fill:P.BG,stroke:col,'stroke-width':1.6});
-      node.setAttribute('class','pop');
-      node.style.animationDelay=(.35+c*.07)+'s';
-      tip(node,`${name} —— ${t==='f'?'上线':'改版'} ${yr}`);
-    });
-    // 终点:存活的落在今天的基线上,沉寂的停在它停下的地方
-    el(s,'circle',{cx:x,cy:alive?yY(2026)+16:yY(last)+12,r:alive?3:1.6,
-      fill:col,class:'pop',style:`animation-delay:${.6+c*.07}s`});
-    const ly=yY(2026)+26;
-    txt(s,{x,y:ly,'font-size':8.5,'font-weight':hero?800:600,fill:hero?P.HERO:(alive?P.LAB:P.FAINT),
-      transform:`rotate(-52 ${x} ${ly})`,'text-anchor':'end',
-      class:'fade',style:`${hero?`paint-order:stroke;stroke:${P.BG};stroke-width:3px;`:''}animation-delay:${.7+c*.05}s`},name);
-  });
-});
-})();
-
-/* ════ 图C · F11 tick gauge ════
-   结构照搬 basics-gallery "tick gauge":210° 表盘,1 tick = 1% 目标。
-   已达部分上深绿墨,尖端琥珀珠收笔。tick 线宽 1→1.8 / .6→1.1。 */
-(()=>{
-const GOAL=66;
-obsReveal('gauge',s=>{
-  const cx=200,cy=186,R0=100,A0=-195,SW=210;
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?13+rnd(k+1,3)*6:5+rnd(k+1,7)*2.5;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?P.DATA:P.FAINTDATA,'stroke-width':inked?1.8:1.1,
-      opacity:inked?1:.85,class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-7,a),[tx2,ty2]=pol(cx,cy,R0-19,a);
-    el(s,'circle',{cx:dx,cy:dy,r:1,fill:P.BEAD,class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+3,'font-size':7,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+20,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:2.8,fill:P.HERO,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-4,'font-size':34,'font-weight':800,fill:P.DATA,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},GOAL+'%');
-  tip(num,'$7M ARR 目标已完成 $4.6M');
-  txt(s,{x:cx,y:cy+16,'font-size':8.5,'font-weight':600,fill:P.MUT,'text-anchor':'middle',
-    'letter-spacing':'.08em',class:'fade',style:'animation-delay:1.05s'},'还差 34 格');
-  txt(s,{x:200,y:292,'font-size':7.5,'font-weight':600,fill:P.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.2s'},
-    '一格 = 目标的 1% · 上墨 = 已完成 · 琥珀珠 = 今天');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 390
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-03.en.html

@@ -1,390 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Year in Data — Lieflat Report Template · Wire Spine Poster</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,600;9..144,800;9..144,900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 03 · YEAR IN DATA
-     场景:个人 / 独立开发者年度数据海报(build in public 年终盘点、
-           社媒长图、博客年报)
-     排版参考:"2019 Website Traffic" 蜜桃海报(左侧竖排书脊大标题、
-               粗双线分区、胖衬线标题 + 三栏底部模块)—— 只学排版和
-               字体,蜜桃底色不学。
-     色板正本 = color-presets.js 的 WIRE 一套,不新增色值。
-       灰阶承载数据,荧光橙 #F5572F 每张图只标一个主角。
-     图表正本 = lieflat gallery:
-       图A  F1  rung bars(三面板小倍数)· basics-gallery.html "Revenue by plan, rung by rung"
-       图B  L10 radial patchwork · lupi-gallery.html "A quarter of deploys, overlaid"
-       图C  F5  tick rows   · basics-gallery.html "Six teams, shipped and counted"
-       图D  F11 tick gauge  · basics-gallery.html "How far to the quarter's goal"
-     WIRE 属 mono+accent:灰阶密度规则同 Mono,无需 ×1.8 加墨。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* WIRE preset roles */
-    --bg:#F0F0EE; --txt:#1F1E1C;
-    --mut:rgba(31,30,28,.60); --lab:rgba(31,30,28,.72);
-    --faint:rgba(31,30,28,.32); --quiet:rgba(31,30,28,.15);
-    --grid:rgba(31,30,28,.16);
-    --data:#22211F; --data2:#8F8E86; --hero:#F5572F; --faintdata:#C0BFB7;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:56px 24px}
-  .sheet{width:1080px;max-width:1080px;display:grid;grid-template-columns:96px 1fr;gap:0}
-
-  /* ── 书脊:竖排大标题(学参考图3)── */
-  .spine{position:relative;border-right:3px double var(--txt)}
-  .spine .t{position:absolute;top:0;left:8px;
-    writing-mode:vertical-rl;transform:rotate(180deg);
-    font-family:'Fraunces',serif;font-weight:900;font-size:64px;letter-spacing:-.01em;
-    line-height:.92;white-space:nowrap}
-  .spine .t .yr{color:var(--hero)}
-  .spine .b{position:absolute;bottom:0;left:26px;writing-mode:vertical-rl;transform:rotate(180deg);
-    font-size:9px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;color:var(--mut)}
-
-  .content{padding-left:44px}
-
-  /* ── 顶注(学参考图3 的黑条 source 行)── */
-  .topbar{display:flex;justify-content:space-between;background:var(--data);color:var(--bg);
-    font-size:9px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
-    padding:7px 14px;margin-bottom:34px}
-  .topbar b{color:#F5572F}
-
-  /* ── 三面板小倍数(一张图,三个 panel)── */
-  .panels{display:grid;grid-template-columns:repeat(3,1fr);gap:26px}
-  .panel .growth{font-size:20px;font-weight:800;letter-spacing:-.02em}
-  .panel .growth .pct{color:var(--hero)}
-  .panel .growth .dots{color:var(--faint);font-weight:600;font-size:12px}
-  .panel .glabel{font-size:9px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
-    color:var(--mut);margin-top:2px}
-  .panel h3{font-family:'Fraunces',serif;font-size:19px;font-weight:800;text-align:center;
-    margin-top:6px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-
-  /* ── 粗双线分隔(学参考图3)── */
-  .double{border-top:1px solid var(--txt);border-bottom:3px solid var(--txt);height:6px;
-    margin:38px 0 0}
-
-  /* ── 中带:最忙一天(callout,不占图额度)── */
-  .midband{display:grid;grid-template-columns:1fr 1.1fr 1fr;gap:30px;align-items:center;
-    padding:26px 0 30px}
-  .midband .l{font-size:15px;font-weight:800;line-height:1.4;letter-spacing:-.01em}
-  .midband .c{font-family:'Fraunces',serif;font-size:34px;font-weight:800;line-height:1.1;
-    color:var(--hero);text-align:center}
-  .midband .r .row{display:flex;justify-content:space-between;font-size:11px;font-weight:700;
-    padding:7px 0;border-bottom:1px dotted var(--faint)}
-  .midband .r .row:last-child{border-bottom:0}
-  .midband .r .row span:last-child{font-weight:800}
-
-  /* ── 底部三栏 ── */
-  .bottom{display:grid;grid-template-columns:1fr 1.15fr 1fr;gap:34px;padding-top:30px}
-  .bottom h3{font-family:'Fraunces',serif;font-size:21px;font-weight:800;text-align:center;
-    color:var(--data2);margin-bottom:4px}
-  .bottom h3.ink{color:var(--txt)}
-  .srcline{font-size:8.5px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
-    color:var(--faint);margin-top:8px;text-align:center}
-
-  .foot{margin-top:44px;border-top:3px double var(--txt);padding-top:12px;
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--mut)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 书脊 ══ -->
-  <div class="spine">
-    <div class="t"><span class="yr">2026</span> Year in Data</div>
-    <div class="b">Build in public · annual</div>
-  </div>
-
-  <div class="content">
-    <div class="topbar"><span>All data from plausible.io + stripe</span><span><b>@yourhandle</b></span></div>
-
-    <!-- ══ 图A · F1 rung bars 三面板小倍数:同一把尺子看三个指标三年 ══
-         模板:basics-gallery.html · "Revenue by plan, rung by rung"
-         一个面板 = 一组 rung bars(2024/25/26 三柱),三个面板共用一图 -->
-    <div class="panels">
-      <div class="panel">
-        <div class="growth"><span class="pct">+92%</span> <span class="dots">·········</span> 3.5k</div>
-        <div class="glabel">growth from 2025</div>
-        <div class="fig"><svg id="p0" viewBox="0 0 200 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <h3>Visitors</h3>
-      </div>
-      <div class="panel">
-        <div class="growth"><span class="pct">+122%</span> <span class="dots">·······</span> 2.9k</div>
-        <div class="glabel">growth from 2025</div>
-        <div class="fig"><svg id="p1" viewBox="0 0 200 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <h3>Sign-ups</h3>
-      </div>
-      <div class="panel">
-        <div class="growth"><span class="pct">+121%</span> <span class="dots">·······</span> 12.3k</div>
-        <div class="glabel">growth from 2025</div>
-        <div class="fig"><svg id="p2" viewBox="0 0 200 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <h3>Page views</h3>
-      </div>
-    </div>
-    <div class="srcline">Rung bars, small multiples · one rung = 100 (visitors, sign-ups) / 400 (page views) · orange = 2026</div>
-
-    <div class="double"></div>
-
-    <!-- ══ 中带 callout ══ -->
-    <div class="midband">
-      <div class="l">Busiest day I've had in three years of building in public</div>
-      <div class="c">Monday,<br>July 6th, 2026</div>
-      <div class="r">
-        <div class="row"><span>Visitors</span><span>66</span></div>
-        <div class="row"><span>Sign-ups</span><span>55</span></div>
-        <div class="row"><span>Page views</span><span>241</span></div>
-      </div>
-    </div>
-
-    <div class="double" style="margin-top:0"></div>
-
-    <!-- ══ 底部三栏 ══ -->
-    <div class="bottom">
-      <div>
-        <h3 class="ink">When I actually get to it</h3>
-        <!-- 图B · L10 radial patchwork · lupi-gallery.html "A quarter of deploys, overlaid" -->
-        <div class="fig"><svg id="patchwork" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">Radial patchwork · one wedge = one ship · angle = hour · radius = reach · denser ink = pile-up · orange = hit HN</div>
-      </div>
-      <div>
-        <h3 class="ink">Where readers came from</h3>
-        <!-- 图C · F5 tick rows · basics-gallery.html "Six teams, shipped and counted" -->
-        <div class="fig"><svg id="rows" viewBox="0 0 268 210" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">Tick rows · one tick = 10 visits · orange = the channel that won</div>
-      </div>
-      <div>
-        <h3 class="ink">Visit → sign-up</h3>
-        <!-- 图D · F11 tick gauge · basics-gallery.html "How far to the quarter's goal" -->
-        <div class="fig"><svg id="gauge" viewBox="0 0 200 210" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">Tick gauge · one tick = 1 visit in 100</div>
-      </div>
-    </div>
-
-    <div class="foot">
-      <span>yoursite.com · 2026 in numbers</span>
-      <span>Made with lieflat charts</span>
-    </div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const sect=(cx,cy,r0,r1,a0,a1)=>{
-  const big=a1-a0>180?1:0;
-  const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
-  const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
-  return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
-};
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── WIRE preset(color-presets.js 原值)── */
-const W={BG:'#F0F0EE',TXT:'#1F1E1C',DATA:'#22211F',DATA2:'#8F8E86',HERO:'#F5572F',
-  FAINTDATA:'#C0BFB7',MUT:'rgba(31,30,28,.60)',FAINT:'rgba(31,30,28,.32)',
-  GRID:'rgba(31,30,28,.16)'};
-
-/* ════ 图A · F1 rung bars(三面板小倍数)════
-   结构照搬 basics-gallery "rung bars":柱身 = 一格格横档,可数。
-   每面板 3 柱 = 2024/2025/2026。橙 = 2026(每面板唯一主角)。 */
-(()=>{
-const PANELS=[
-  {id:'p0',unit:100,years:[['2024',6],['2025',18],['2026',35]]},   // visitors ×100
-  {id:'p1',unit:100,years:[['2024',3],['2025',13],['2026',29]]},   // sign-ups ×100
-  {id:'p2',unit:400,years:[['2024',4],['2025',14],['2026',31]]},   // page views ×400
-];
-PANELS.forEach(({id,unit,years})=>{
-  obsReveal(id,s=>{
-    const x0=i=>44+i*58,base=152,step=3.6,HW=13;
-    years.forEach(([yr,v],i)=>{
-      const x=x0(i),is26=yr==='2026';
-      for(let k=0;k<v;k++){
-        const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-        el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:is26?W.HERO:W.DATA,'stroke-width':1,
-          opacity:(is26?.75:.5)+rnd(k+2,i+4)*.25,class:'fade',
-          style:`animation-delay:${i*.08+k*.012}s`});
-        if(k%5===4)el(s,'circle',{cx:x+HW+4,cy:y,r:.8,fill:W.FAINTDATA,
-          class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      }
-      const topY=base-(v-1)*step;
-      const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-        fill:is26?W.HERO:W.DATA,'text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.4+i*.08}s`},(v*unit>=1000?(v*unit/1000)+'k':v*unit));
-      tip(num,`${yr} — ${v*unit}`);
-      txt(s,{x,y:base+16,'font-size':7.5,'font-weight':700,fill:W.MUT,'text-anchor':'middle',
-        'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},yr);
-    });
-    el(s,'line',{x1:14,y1:base+4,x2:186,y2:base+4,stroke:W.GRID,'stroke-width':.8,class:'fade'});
-    txt(s,{x:100,y:184,'font-size':6.5,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-      'ONE RUNG = '+unit);
-  });
-});
-})();
-
-/* ════ 图B · L10 radial patchwork ════
-   结构照搬 lupi-gallery "radial patchwork":每瓦 = 一次 ship/post,
-   角度 = 当天时刻,半径 = reach,透明度叠加出扎堆密度——
-   这是「一律实心」铁律唯一的例外,透明度本身就是数据。
-   早班(上班前)与夜班(下班后)两簇,白天基本空白,符合有正职的
-   独立开发者作息。HERO 只框 3 次冲上 HN 首页的发布——本图橙色
-   唯一的窄角色。 */
-(()=>{
-const CX=200,CY=168,N=40;
-obsReveal('patchwork',s=>{
-  // 24h rim ticks
-  for(let h=0;h<96;h++){
-    const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:W.GRID,'stroke-width':h%4===0?1:.5,
-      class:'fade',style:`animation-delay:${h*.006}s`});
-  }
-  [0,6,12,18].forEach(h=>{
-    const [x,y]=pol(CX,CY,157,-90+h*15);
-    txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:W.MUT,'text-anchor':'middle',
-      class:'fade'},String(h).padStart(2,'0'));
-  });
-  // one wedge per ship/post: an early cluster (before the day job) and a
-  // late cluster (after it) — daytime stays quiet
-  const wedges=[];
-  for(let i=0;i<N;i++){
-    const peak=i%13===5?14:(rnd(i+1,2)>.5?7:22);
-    const hour=(peak+(rnd(i+1,3)-.5)*6+24)%24;
-    const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;
-    const r1=34+rnd(i+1,5)*100;
-    wedges.push([a0,sw,r1,hour,i]);
-  }
-  wedges.forEach(([a0,sw,r1,hour,i])=>{
-    const col=hour<8?W.FAINTDATA:hour<19?W.DATA2:W.DATA;
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:col,
-      'fill-opacity':.17+rnd(i+1,6)*.15,class:'fade',
-      style:`animation-delay:${.2+i*.022}s`});
-    tip(w,`ship/post #${i+1} — ${String(Math.floor(hour)).padStart(2,'0')}:${String(Math.floor(hour%1*60)).padStart(2,'0')}`);
-  });
-  // three posts that hit Hacker News' front page
-  [0,8,26].forEach((i,k)=>{
-    const [a0,sw,r1]=wedges[i];
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',
-      stroke:W.HERO,'stroke-width':1.5,class:'fade',style:`animation-delay:${1.2+k*.15}s`});
-    tip(w,`ship/post #${i+1} — hit Hacker News' front page`);
-  });
-  el(s,'circle',{cx:CX,cy:CY,r:3,fill:W.DATA,class:'pop',style:'animation-delay:1.3s'});
-});
-})();
-
-/* ════ 图C · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows"。1 tick = 10 visits。
-   HERO 只给赢家渠道那一行。 */
-(()=>{
-const D=[['TWITTER / X',77],['GOOGLE',34],['NEWSLETTER',22],['GITHUB',13],['OTHER',9]];
-obsReveal('rows',s=>{
-  const y0=i=>34+i*36,X0=76,PX=1.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:68,y:y+3,'font-size':7,'font-weight':700,fill:W.MUT,'text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+8,x2:X0+80*PX,y2:y+8,stroke:W.GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=7+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+8,x2:x,y2:y+8-h,stroke:hero?W.HERO:W.DATA,'stroke-width':.9,
-        opacity:.55+rnd(k+3,i+5)*.45,class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-      if(k%10===9)el(s,'circle',{cx:x,cy:y+11.5,r:.7,fill:W.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,
-      fill:hero?W.HERO:W.DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},
-      (v*10>=1000?(v*10/1000).toFixed(1)+'k':v*10));
-    tip(lab,`${name} — ${v*10} visits`);
-  });
-  txt(s,{x:134,y:204,'font-size':6.5,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = 10 VISITS · DOT MARKS EVERY HUNDREDTH');
-});
-})();
-
-/* ════ 图D · F11 tick gauge ════
-   结构照搬 basics-gallery "tick gauge":1 tick = 1 visit in 100。
-   9% 转化上墨,HERO 只给尖端珠 + 大数。 */
-(()=>{
-const GOAL=9;
-obsReveal('gauge',s=>{
-  const cx=100,cy=128,R0=62,A0=-195,SW=210;
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?11+rnd(k+1,3)*5:4+rnd(k+1,7)*2;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?W.DATA:W.FAINTDATA,'stroke-width':inked?1:.6,
-      class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-6,a),[tx2,ty2]=pol(cx,cy,R0-16,a);
-    el(s,'circle',{cx:dx,cy:dy,r:.9,fill:W.DATA2,class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+2.5,'font-size':6,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+17,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:2.4,fill:W.HERO,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-2,'font-size':26,'font-weight':800,fill:W.HERO,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},'9%');
-  tip(num,'9 of every 100 visitors signed up');
-  txt(s,{x:cx,y:cy+13,'font-size':6.5,'font-weight':600,fill:W.MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.05s'},'CONVERSION RATE');
-  txt(s,{x:100,y:202,'font-size':6.5,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.2s'},
-    'ONE TICK = 1 VISIT IN 100 · INKED = CONVERTED');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 391
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-03.zh.html

@@ -1,391 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>年度数据 — Lieflat 报告模板 · 编辑部书脊</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Fraunces:opsz,wght@9..144,600;9..144,800;9..144,900&family=Noto+Serif+SC:wght@600;700;900&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 03 · 年度数据海报(中文版)
-     场景:个人 / 独立开发者年度数据海报(build in public 年终盘点、
-           社媒长图、博客年报)
-     排版参考:"2019 Website Traffic" 蜜桃海报(左侧竖排书脊大标题、
-               粗双线分区、胖衬线标题 + 三栏底部模块)—— 只学排版和
-               字体,蜜桃底色不学。
-     中西混排:数字与拉丁走 Fraunces / Inter,汉字走 Noto。
-               书脊竖排用 writing-mode,汉字直立、数字横转。
-     色板正本 = color-presets.js 的 WIRE 一套,不新增色值。
-       灰阶承载数据,荧光橙 #F5572F 每张图只标一个主角。
-     图表正本 = lieflat gallery:
-       图A  F1  rung bars(三面板小倍数)· basics-gallery.html
-       图B  L10 radial patchwork · lupi-gallery.html "A quarter of deploys, overlaid"
-       图C  F5  tick rows   · basics-gallery.html
-       图D  F11 tick gauge  · basics-gallery.html
-     WIRE 属 mono+accent:灰阶密度规则同 Mono,无需 ×1.8 加墨。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* WIRE preset roles */
-    --bg:#F0F0EE; --txt:#1F1E1C;
-    --mut:rgba(31,30,28,.60); --lab:rgba(31,30,28,.72);
-    --faint:rgba(31,30,28,.32); --quiet:rgba(31,30,28,.15);
-    --grid:rgba(31,30,28,.16);
-    --data:#22211F; --data2:#8F8E86; --hero:#F5572F; --faintdata:#C0BFB7;
-    --fat:'Fraunces','Noto Serif SC','Songti SC',serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter','Noto Sans SC',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:56px 24px}
-  .sheet{width:1080px;max-width:1080px;display:grid;grid-template-columns:104px 1fr;gap:0}
-
-  /* ── 书脊:竖排大标题(学参考图3)── */
-  .spine{position:relative;border-right:3px double var(--txt)}
-  .spine .t{position:absolute;top:0;left:10px;
-    writing-mode:vertical-rl;
-    font-family:var(--fat);font-weight:900;font-size:56px;
-    line-height:1.05;letter-spacing:.08em;white-space:nowrap}
-  .spine .t .yr{color:var(--hero);letter-spacing:.02em}
-  .spine .b{position:absolute;bottom:0;left:36px;writing-mode:vertical-rl;
-    font-size:9.5px;font-weight:700;letter-spacing:.3em;color:var(--mut)}
-
-  .content{padding-left:44px}
-
-  /* ── 顶注(学参考图3 的黑条 source 行)── */
-  .topbar{display:flex;justify-content:space-between;background:var(--data);color:var(--bg);
-    font-size:9.5px;font-weight:600;letter-spacing:.1em;
-    padding:7px 14px;margin-bottom:34px}
-  .topbar b{color:#F5572F}
-
-  /* ── 三面板小倍数(一张图,三个 panel)── */
-  .panels{display:grid;grid-template-columns:repeat(3,1fr);gap:26px}
-  .panel .growth{font-size:20px;font-weight:800;letter-spacing:-.02em}
-  .panel .growth .pct{color:var(--hero)}
-  .panel .growth .dots{color:var(--faint);font-weight:600;font-size:12px}
-  .panel .glabel{font-size:9.5px;font-weight:600;letter-spacing:.08em;
-    color:var(--mut);margin-top:2px}
-  .panel h3{font-family:var(--fat);font-size:19px;font-weight:800;text-align:center;
-    margin-top:6px;letter-spacing:.04em}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-
-  /* ── 粗双线分隔(学参考图3)── */
-  .double{border-top:1px solid var(--txt);border-bottom:3px solid var(--txt);height:6px;
-    margin:38px 0 0}
-
-  /* ── 中带:最忙一天(callout,不占图额度)── */
-  .midband{display:grid;grid-template-columns:1fr 1.1fr 1fr;gap:30px;align-items:center;
-    padding:26px 0 30px}
-  .midband .l{font-size:15px;font-weight:800;line-height:1.6}
-  .midband .c{font-family:var(--fat);font-size:30px;font-weight:800;line-height:1.3;
-    color:var(--hero);text-align:center}
-  .midband .r .row{display:flex;justify-content:space-between;font-size:11.5px;font-weight:700;
-    padding:7px 0;border-bottom:1px dotted var(--faint)}
-  .midband .r .row:last-child{border-bottom:0}
-  .midband .r .row span:last-child{font-weight:800}
-
-  /* ── 底部三栏 ── */
-  .bottom{display:grid;grid-template-columns:1fr 1.15fr 1fr;gap:34px;padding-top:30px}
-  .bottom h3{font-family:var(--fat);font-size:20px;font-weight:800;text-align:center;
-    color:var(--txt);margin-bottom:4px;letter-spacing:.04em}
-  .srcline{font-size:8.5px;font-weight:600;letter-spacing:.1em;
-    color:var(--faint);margin-top:8px;text-align:center}
-
-  .foot{margin-top:44px;border-top:3px double var(--txt);padding-top:12px;
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;color:var(--mut)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 书脊 ══ -->
-  <div class="spine">
-    <div class="t"><span class="yr">2026</span> 年度数据</div>
-    <div class="b">BUILD IN PUBLIC · 年报</div>
-  </div>
-
-  <div class="content">
-    <div class="topbar"><span>全部数据来自 PLAUSIBLE.IO + STRIPE</span><span><b>@你的账号</b></span></div>
-
-    <!-- ══ 图A · F1 rung bars 三面板小倍数:同一把尺子看三个指标三年 ══
-         模板:basics-gallery.html · "Revenue by plan, rung by rung"
-         一个面板 = 一组 rung bars(2024/25/26 三柱),三个面板共用一图 -->
-    <div class="panels">
-      <div class="panel">
-        <div class="growth"><span class="pct">+92%</span> <span class="dots">·········</span> 3.5k</div>
-        <div class="glabel">较 2025 年增长</div>
-        <div class="fig"><svg id="p0" viewBox="0 0 200 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <h3>访客</h3>
-      </div>
-      <div class="panel">
-        <div class="growth"><span class="pct">+122%</span> <span class="dots">·······</span> 2.9k</div>
-        <div class="glabel">较 2025 年增长</div>
-        <div class="fig"><svg id="p1" viewBox="0 0 200 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <h3>注册</h3>
-      </div>
-      <div class="panel">
-        <div class="growth"><span class="pct">+121%</span> <span class="dots">·······</span> 12.3k</div>
-        <div class="glabel">较 2025 年增长</div>
-        <div class="fig"><svg id="p2" viewBox="0 0 200 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <h3>浏览量</h3>
-      </div>
-    </div>
-    <div class="srcline">RUNG BARS 小倍数 · 一档 = 100(访客、注册)/ 400(浏览量)· 橙 = 2026</div>
-
-    <div class="double"></div>
-
-    <!-- ══ 中带 callout ══ -->
-    <div class="midband">
-      <div class="l">三年 build in public,<br>最忙的一天</div>
-      <div class="c">2026 年 7 月 6 日<br>星期一</div>
-      <div class="r">
-        <div class="row"><span>访客</span><span>66</span></div>
-        <div class="row"><span>注册</span><span>55</span></div>
-        <div class="row"><span>浏览量</span><span>241</span></div>
-      </div>
-    </div>
-
-    <div class="double" style="margin-top:0"></div>
-
-    <!-- ══ 底部三栏 ══ -->
-    <div class="bottom">
-      <div>
-        <h3>几点动手</h3>
-        <!-- 图B · L10 radial patchwork · lupi-gallery.html "A quarter of deploys, overlaid" -->
-        <div class="fig"><svg id="patchwork" viewBox="0 0 400 340" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">RADIAL PATCHWORK · 一瓦 = 一次上线 · 角度 = 时刻 · 半径 = 声量 · 叠色越重 = 越扎堆 · 橙框 = 冲上 HN 首页</div>
-      </div>
-      <div>
-        <h3>读者从哪来</h3>
-        <!-- 图C · F5 tick rows · basics-gallery.html -->
-        <div class="fig"><svg id="rows" viewBox="0 0 268 210" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">TICK ROWS · 一格 = 10 次访问 · 橙 = 赢家渠道</div>
-      </div>
-      <div>
-        <h3>访问 → 注册</h3>
-        <!-- 图D · F11 tick gauge · basics-gallery.html -->
-        <div class="fig"><svg id="gauge" viewBox="0 0 200 210" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">TICK GAUGE · 一格 = 每 100 次访问中的 1 次</div>
-      </div>
-    </div>
-
-    <div class="foot">
-      <span>yoursite.com · 2026 年度数字</span>
-      <span>LIEFLAT CHARTS 出品</span>
-    </div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const sect=(cx,cy,r0,r1,a0,a1)=>{
-  const big=a1-a0>180?1:0;
-  const [xa,ya]=pol(cx,cy,r1,a0),[xb,yb]=pol(cx,cy,r1,a1);
-  const [xc,yc]=pol(cx,cy,r0,a1),[xd,yd]=pol(cx,cy,r0,a0);
-  return `M${xa} ${ya} A${r1} ${r1} 0 ${big} 1 ${xb} ${yb} L${xc} ${yc} A${r0} ${r0} 0 ${big} 0 ${xd} ${yd} Z`;
-};
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── WIRE preset(color-presets.js 原值)── */
-const W={BG:'#F0F0EE',TXT:'#1F1E1C',DATA:'#22211F',DATA2:'#8F8E86',HERO:'#F5572F',
-  FAINTDATA:'#C0BFB7',MUT:'rgba(31,30,28,.60)',FAINT:'rgba(31,30,28,.32)',
-  GRID:'rgba(31,30,28,.16)'};
-
-/* ════ 图A · F1 rung bars(三面板小倍数)════
-   结构照搬 basics-gallery "rung bars":柱身 = 一格格横档,可数。
-   每面板 3 柱 = 2024/2025/2026。橙 = 2026(每面板唯一主角)。 */
-(()=>{
-const PANELS=[
-  {id:'p0',unit:100,years:[['2024',6],['2025',18],['2026',35]]},   // 访客 ×100
-  {id:'p1',unit:100,years:[['2024',3],['2025',13],['2026',29]]},   // 注册 ×100
-  {id:'p2',unit:400,years:[['2024',4],['2025',14],['2026',31]]},   // 浏览量 ×400
-];
-PANELS.forEach(({id,unit,years})=>{
-  obsReveal(id,s=>{
-    const x0=i=>44+i*58,base=152,step=3.6,HW=13;
-    years.forEach(([yr,v],i)=>{
-      const x=x0(i),is26=yr==='2026';
-      for(let k=0;k<v;k++){
-        const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-        el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:is26?W.HERO:W.DATA,'stroke-width':1,
-          opacity:(is26?.75:.5)+rnd(k+2,i+4)*.25,class:'fade',
-          style:`animation-delay:${i*.08+k*.012}s`});
-        if(k%5===4)el(s,'circle',{cx:x+HW+4,cy:y,r:.8,fill:W.FAINTDATA,
-          class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      }
-      const topY=base-(v-1)*step;
-      const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-        fill:is26?W.HERO:W.DATA,'text-anchor':'middle',
-        class:'fade',style:`animation-delay:${.4+i*.08}s`},(v*unit>=1000?(v*unit/1000)+'k':v*unit));
-      tip(num,`${yr} 年 —— ${v*unit}`);
-      txt(s,{x,y:base+16,'font-size':7.5,'font-weight':700,fill:W.MUT,'text-anchor':'middle',
-        'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},yr);
-    });
-    el(s,'line',{x1:14,y1:base+4,x2:186,y2:base+4,stroke:W.GRID,'stroke-width':.8,class:'fade'});
-    txt(s,{x:100,y:184,'font-size':7,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:'animation-delay:.9s'},
-      '一档 = '+unit);
-  });
-});
-})();
-
-/* ════ 图B · L10 radial patchwork ════
-   结构照搬 lupi-gallery "radial patchwork":每瓦 = 一次上线/发帖,
-   角度 = 当天时刻,半径 = 声量,透明度叠加出扎堆密度——
-   这是「一律实心」铁律唯一的例外,透明度本身就是数据。
-   主簇在上班前与下班后,午后只有零星几次(周末)——有正职的独立开发者作息。
-   底注不画在图内:12 点刻度就在那个位置,正本 gallery 两者相撞。
-   HERO 只框 3 次冲上 HN 首页的发布——本图橙色唯一的窄角色。 */
-(()=>{
-const CX=200,CY=168,N=40;
-obsReveal('patchwork',s=>{
-  // 24 小时刻度环
-  for(let h=0;h<96;h++){
-    const a=-90+h*3.75,[x1,y1]=pol(CX,CY,140,a),[x2,y2]=pol(CX,CY,h%4===0?146:143,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:W.GRID,'stroke-width':h%4===0?1:.5,
-      class:'fade',style:`animation-delay:${h*.006}s`});
-  }
-  [0,6,12,18].forEach(h=>{
-    const [x,y]=pol(CX,CY,157,-90+h*15);
-    txt(s,{x,y:y+3,'font-size':8,'font-weight':700,fill:W.MUT,'text-anchor':'middle',
-      class:'fade'},String(h).padStart(2,'0'));
-  });
-  // 每瓦一次「上线/发帖」:早班(上班前)与夜班(下班后)两簇
-  const wedges=[];
-  for(let i=0;i<N;i++){
-    const peak=i%13===5?14:(rnd(i+1,2)>.5?7:22);
-    const hour=(peak+(rnd(i+1,3)-.5)*6+24)%24;
-    const a0=-90+hour*15,sw=10+rnd(i+1,4)*34;
-    const r1=34+rnd(i+1,5)*100;
-    wedges.push([a0,sw,r1,hour,i]);
-  }
-  wedges.forEach(([a0,sw,r1,hour,i])=>{
-    const col=hour<8?W.FAINTDATA:hour<19?W.DATA2:W.DATA;
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:col,
-      'fill-opacity':.17+rnd(i+1,6)*.15,class:'fade',
-      style:`animation-delay:${.2+i*.022}s`});
-    tip(w,`第 ${i+1} 次上线/发帖 — ${String(Math.floor(hour)).padStart(2,'0')}:${String(Math.floor(hour%1*60)).padStart(2,'0')}`);
-  });
-  // 三次冲上 Hacker News 首页的发布
-  [0,8,26].forEach((i,k)=>{
-    const [a0,sw,r1]=wedges[i];
-    const w=el(s,'path',{d:sect(CX,CY,16,r1,a0,a0+sw),fill:'none',
-      stroke:W.HERO,'stroke-width':1.5,class:'fade',style:`animation-delay:${1.2+k*.15}s`});
-    tip(w,`第 ${i+1} 次上线/发帖 — 冲上了 Hacker News 首页`);
-  });
-  el(s,'circle',{cx:CX,cy:CY,r:3,fill:W.DATA,class:'pop',style:'animation-delay:1.3s'});
-});
-})();
-
-/* ════ 图C · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows"。1 tick = 10 次访问。
-   HERO 只给赢家渠道那一行。 */
-(()=>{
-const D=[['TWITTER / X',77],['GOOGLE',34],['邮件通讯',22],['GITHUB',13],['其他',9]];
-obsReveal('rows',s=>{
-  const y0=i=>34+i*36,X0=76,PX=1.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:68,y:y+3,'font-size':7.5,'font-weight':700,fill:W.MUT,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+8,x2:X0+80*PX,y2:y+8,stroke:W.GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=7+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+8,x2:x,y2:y+8-h,stroke:hero?W.HERO:W.DATA,'stroke-width':.9,
-        opacity:.55+rnd(k+3,i+5)*.45,class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-      if(k%10===9)el(s,'circle',{cx:x,cy:y+11.5,r:.7,fill:W.FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,
-      fill:hero?W.HERO:W.DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},
-      (v*10>=1000?(v*10/1000).toFixed(1)+'k':v*10));
-    tip(lab,`${name} —— ${v*10} 次访问`);
-  });
-  txt(s,{x:134,y:204,'font-size':7,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.08em',class:'fade',style:'animation-delay:.9s'},
-    '一格 = 10 次访问 · 每一百次一个圆点');
-});
-})();
-
-/* ════ 图D · F11 tick gauge ════
-   结构照搬 basics-gallery "tick gauge":一格 = 每 100 次访问中的 1 次。
-   9% 转化上墨,HERO 只给尖端珠 + 大数。 */
-(()=>{
-const GOAL=9;
-obsReveal('gauge',s=>{
-  const cx=100,cy=128,R0=62,A0=-195,SW=210;
-  for(let k=0;k<100;k++){
-    const a=A0+k/100*SW,inked=k<GOAL;
-    const len=inked?11+rnd(k+1,3)*5:4+rnd(k+1,7)*2;
-    const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-    el(s,'line',{x1,y1,x2,y2,stroke:inked?W.DATA:W.FAINTDATA,'stroke-width':inked?1:.6,
-      class:'fade',style:`animation-delay:${k*.012}s`});
-  }
-  [25,50,75,100].forEach(m=>{
-    const a=A0+m/100*SW,[dx,dy]=pol(cx,cy,R0-6,a),[tx2,ty2]=pol(cx,cy,R0-16,a);
-    el(s,'circle',{cx:dx,cy:dy,r:.9,fill:W.DATA2,class:'fade',style:'animation-delay:.8s'});
-    txt(s,{x:tx2,y:ty2+2.5,'font-size':6,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-      class:'fade',style:'animation-delay:.85s'},m);
-  });
-  const aT=A0+GOAL/100*SW,[ex,ey]=pol(cx,cy,R0+17,aT);
-  el(s,'circle',{cx:ex,cy:ey,r:2.4,fill:W.HERO,class:'pop',style:'animation-delay:1.1s'});
-  const num=txt(s,{x:cx,y:cy-2,'font-size':26,'font-weight':800,fill:W.HERO,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:1s'},'9%');
-  tip(num,'每 100 位访客中有 9 位完成注册');
-  txt(s,{x:cx,y:cy+13,'font-size':7,'font-weight':600,fill:W.MUT,'text-anchor':'middle',
-    'letter-spacing':'.08em',class:'fade',style:'animation-delay:1.05s'},'转化率');
-  txt(s,{x:100,y:202,'font-size':7,'font-weight':600,fill:W.FAINT,'text-anchor':'middle',
-    'letter-spacing':'.08em',class:'fade',style:'animation-delay:1.2s'},
-    '一格 = 每 100 次访问中的 1 次 · 上墨 = 已转化');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 369
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-04.en.html

@@ -1,369 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Monthly Ops — Lieflat Report Template · Mono Broadsheet</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Oswald:wght@500;600;700&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 04 · MONTHLY OPS
-     场景:月度运营 / 流量月报(团队内部同步、投资人月更、周期性复盘)
-     排版参考:"2017 Website Traffic" 浅蓝海报(左侧竖排压缩大写
-               书脊、右上角对齐节标题 + 细规线、底部 NOTABLE
-               STATISTICS 大数统计行)—— 只学排版和字体,浅蓝不学。
-     色板正本 = mono-tokens.js 的 Mono 灰阶,一个彩色都没有。
-     图表正本 = lieflat gallery:
-       图A  F2  hairline line · basics-gallery.html "Thirty days of sign-ups"
-       图B  F10 dot heat      · basics-gallery.html "When support gets loud"
-       图C  F4  tick donut    · basics-gallery.html "Where the traffic comes from"
-       图D  F5  tick rows     · basics-gallery.html "Six teams, shipped and counted"
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* MONO tokens */
-    --bg:#F0EFEB; --ink:#1C1C1A; --muted:#8F8E88; --faint:#C6C5BF; --grid:#DEDDD6;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--ink);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:56px 24px}
-  .sheet{width:1080px;max-width:1080px;display:grid;grid-template-columns:110px 1fr}
-
-  /* ── 书脊:压缩大写竖排(学参考图4)── */
-  .spine{position:relative;border-right:1px solid var(--ink)}
-  .spine .t{position:absolute;top:0;left:2px;writing-mode:vertical-rl;transform:rotate(180deg);
-    font-family:'Oswald',sans-serif;font-weight:700;font-size:74px;line-height:.9;
-    letter-spacing:.01em;text-transform:uppercase;white-space:nowrap}
-  .spine .b{position:absolute;bottom:0;left:44px;writing-mode:vertical-rl;transform:rotate(180deg);
-    font-size:9px;font-weight:700;letter-spacing:.22em;text-transform:uppercase;color:var(--muted)}
-
-  .content{padding-left:40px}
-
-  /* ── 顶注 ── */
-  .tophead{display:flex;gap:18px;align-items:baseline;border-bottom:1px solid var(--ink);
-    padding-bottom:10px;margin-bottom:40px}
-  .tophead .site{font-family:'Oswald',sans-serif;font-weight:600;font-size:13px;
-    letter-spacing:.08em;text-transform:uppercase}
-  .tophead .sep{color:var(--faint)}
-  .tophead .tag{font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
-    color:var(--muted)}
-
-  /* ── 节标题:右对齐 + 引线(学参考图4)── */
-  .secthead{display:flex;align-items:baseline;gap:14px;margin:44px 0 8px}
-  .secthead .line{flex:1;border-top:1px solid var(--ink)}
-  .secthead .t{font-family:'Oswald',sans-serif;font-weight:600;font-size:15px;
-    letter-spacing:.12em;text-transform:uppercase}
-  .secthead.first{margin-top:0}
-
-  .claim{font-size:12.5px;font-weight:700;color:var(--ink);margin:4px 0 2px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
-    color:var(--faint);margin-top:8px}
-
-  .duo{display:grid;grid-template-columns:1.25fr 1fr;gap:44px}
-
-  /* ── NOTABLE STATISTICS 底部统计行(学参考图4)── */
-  .stats{display:grid;grid-template-columns:1fr 1fr 1.2fr 1fr;gap:0;margin-top:14px;
-    border-top:1px solid var(--ink)}
-  .stats>div{padding:22px 20px 6px;border-right:1px dotted var(--faint)}
-  .stats>div:first-child{padding-left:0}
-  .stats>div:last-child{border-right:0;padding-right:0}
-  .stats .kpi{margin-bottom:18px}
-  .stats .kpi .v{font-family:'Oswald',sans-serif;font-weight:700;font-size:38px;line-height:1}
-  .stats .kpi .r{font-size:10px;font-weight:700;color:var(--muted);margin-top:4px}
-  .stats .kpi .l{font-size:9px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
-    color:var(--faint);margin-top:3px}
-  .stats h4{font-family:'Oswald',sans-serif;font-weight:600;font-size:13px;
-    letter-spacing:.1em;text-transform:uppercase;margin-bottom:10px}
-  .ranked .row{display:flex;gap:12px;align-items:baseline;padding:8px 0;
-    border-bottom:1px dotted var(--faint)}
-  .ranked .row:last-child{border-bottom:0}
-  .ranked .n{font-family:'Oswald',sans-serif;font-weight:700;font-size:24px;min-width:38px}
-  .ranked .d{font-size:10px;line-height:1.5;color:var(--muted)}
-  .ranked .d b{color:var(--ink)}
-
-  .foot{margin-top:48px;border-top:1px solid var(--ink);padding-top:12px;
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 书脊 ══ -->
-  <div class="spine">
-    <div class="t">July Ops Report</div>
-    <div class="b">Monthly · 2026</div>
-  </div>
-
-  <div class="content">
-    <div class="tophead">
-      <span class="site">yoursite.com</span><span class="sep">|</span>
-      <span class="tag">July 2026 data · all metrics month-over-month</span>
-    </div>
-
-    <!-- ══ 图A · F2 hairline line:本月逐日 ══
-         模板:basics-gallery.html · "Thirty days of sign-ups" -->
-    <div class="secthead first"><span class="line"></span><span class="t">Daily sign-ups</span></div>
-    <div class="claim">Two launch spikes carried the month — the floor barely moved</div>
-    <div class="fig"><svg id="dayline" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-    <div class="srcline">Hairline line · one dot = one day · hollow = weekend</div>
-
-    <!-- ══ 图B · F10 dot heat:一周节奏 ══
-         模板:basics-gallery.html · "When support gets loud" -->
-    <div class="secthead"><span class="line"></span><span class="t">When support gets loud</span></div>
-    <div class="duo">
-      <div>
-        <div class="claim">Tuesday mid-morning is still our loudest hour</div>
-        <div class="fig"><svg id="dotheat" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">Dot heat · 7 days × 12 hours · dot area = tickets</div>
-      </div>
-      <div>
-        <!-- ══ 图C · F4 tick donut:来源分布 ══
-             模板:basics-gallery.html · "Where the traffic comes from" -->
-        <div class="claim">Organic finally passed paid this month</div>
-        <div class="fig"><svg id="donut" viewBox="0 0 330 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">Tick donut · one tick = 1% of visits</div>
-      </div>
-    </div>
-
-    <!-- ══ NOTABLE STATISTICS ══ -->
-    <div class="secthead"><span class="line"></span><span class="t">Notable statistics</span></div>
-    <div class="stats">
-      <div>
-        <div class="kpi"><div class="v">1,502</div><div class="r">4.11 per day</div><div class="l">Page views</div></div>
-        <div class="kpi"><div class="v">581</div><div class="r">1.59 per day</div><div class="l">Total visitors</div></div>
-        <div class="kpi"><div class="v">358</div><div class="r">0.98 per day</div><div class="l">Unique visitors</div></div>
-      </div>
-      <div>
-        <div class="kpi"><div class="v">26.1%</div><div class="r">up from 19.4% in June</div><div class="l">Traffic from social</div></div>
-        <div class="kpi"><div class="v">61</div><div class="r">of every 100 visits</div><div class="l">On desktop</div></div>
-      </div>
-      <div>
-        <h4>Support volume by product area</h4>
-        <!-- ══ 图D · F5 tick rows(迷你)══
-             模板:basics-gallery.html · "Six teams, shipped and counted" -->
-        <div class="fig"><svg id="rows" viewBox="0 0 260 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline" style="text-align:left">Tick rows · one tick = one ticket</div>
-      </div>
-      <div class="ranked">
-        <h4>Most read this month</h4>
-        <div class="row"><div class="n">45%</div><div class="d"><b>The pricing change post</b> — 45% of all page views</div></div>
-        <div class="row"><div class="n">19%</div><div class="d"><b>Docs quick-start</b> — second month on top three</div></div>
-        <div class="row"><div class="n">14%</div><div class="d"><b>July changelog</b> — best changelog reach yet</div></div>
-      </div>
-    </div>
-
-    <div class="foot">
-      <span>Monthly ops one-pager · July 2026</span>
-      <span>Data · Plausible + Intercom · pulled Aug 1</span>
-    </div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(色板 / 几何 / 工具函数与正本一致)── */
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ════ 图A · F2 hairline line ════
-   结构照搬 basics-gallery "hairline line":barcode 日历地板 +
-   发丝折线 + 逐日圆点(周末空心),峰值两点放大标数。 */
-(()=>{
-const N=31;
-const val=d=>{
-  const spike=(d===5||d===6)?38:(d===19?30:0);        // two launch days
-  return 24+10*Math.sin(d/5.2)+rnd(d+1,5)*10+spike;
-};
-obsReveal('dayline',s=>{
-  const x=d=>26+d*11.4,base=254,map=v=>base-v*2.1;
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:'#CFCEC7','stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:20,y1:base,x2:380,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:INK,'stroke-width':1,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===3||d%7===4,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
-      fill:weekend?PAPER:INK,stroke:INK,'stroke-width':weekend?1:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`July ${d+1} — ${Math.round(v)} sign-ups`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'JUL 1'],[14,'JUL 15'],[30,'JUL 31']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:294,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE DOT = ONE DAY · HOLLOW = WEEKEND · LABELED = LAUNCH DAYS');
-});
-})();
-
-/* ════ 图B · F10 dot heat ════
-   结构照搬 basics-gallery "dot heat":7×12 网格,点面积 = 工单量,
-   静默格留一粒极小点,最热格虚线圈标数。 */
-(()=>{
-const DAY=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-obsReveal('dotheat',s=>{
-  const x0=j=>64+j*27,y0=i=>50+i*30;
-  let max=0,mi=0,mj=0;
-  const v=(i,j)=>{
-    const day=i<5?(i===1?1.25:1):.32;
-    const hour=Math.exp(-((j-3.4)**2)/6)+.6*Math.exp(-((j-8.6)**2)/5);
-    return Math.round(day*hour*20*(0.6+rnd(i*12+j+1,j+3)*.8));
-  };
-  for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  for(let i=0;i<7;i++){
-    txt(s,{x:50,y:y0(i)+3,'font-size':7.5,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
-    for(let j=0;j<12;j++){
-      const t=v(i,j),x=x0(j),y=y0(i);
-      if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:'#D8D6CE',
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
-      const hero=i===mi&&j===mj;
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
-        fill:t>max*.66?INK:t>max*.33?'#6A6963':'#B0AFA9',
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
-      tip(dot,`${DAY[i]} ${8+j}:00 — ${t} tickets`);
-      if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
-        stroke:INK,'stroke-width':1,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:1s'});
-    }
-  }
-  for(let j=0;j<12;j+=2)
-    txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:'#C6C5BF','text-anchor':'middle',
-      class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
-  txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    `DOT AREA = TICKETS · DASHED RING = THE PEAK, ${max} · TINY DOT = A QUIET HOUR`);
-});
-})();
-
-/* ════ 图C · F4 tick donut ════
-   结构照搬 basics-gallery "tick donut":1 tick = 1%,
-   四段来源用灰阶 ladder 分段。 */
-(()=>{
-const D=[['ORGANIC',37,INK],['PAID',28,'#55554F'],['REFERRAL',21,'#8F8E88'],['SOCIAL',14,'#B0AFA9']];
-obsReveal('donut',s=>{
-  const cx=170,cy=140,R0=58;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=9+rnd(idx+1,si+2)*5.5;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:'#C6C5BF',
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+34,mid),[gx,gy]=pol(cx,cy,R0+18,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:'#C6C5BF','stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of traffic`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':20,'font-weight':800,fill:INK,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+13,'font-size':7,'font-weight':600,fill:MUTED,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
-  txt(s,{x:170,y:288,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'DOT MARKS EVERY TENTH · READS CLOCKWISE');
-});
-})();
-
-/* ════ 图D · F5 tick rows(迷你版)════
-   结构照搬 basics-gallery "tick rows":1 tick = 1 ticket。 */
-(()=>{
-const D=[['EDITOR',31],['BILLING',24],['IMPORTS',17],['API',11],['OTHER',6]];
-obsReveal('rows',s=>{
-  const y0=i=>28+i*34,X0=72,PX=4.6;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i);
-    txt(s,{x:64,y:y+3,'font-size':7,'font-weight':700,fill:'#6A6963','text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+8,x2:X0+34*PX,y2:y+8,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=7+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+8,x2:x,y2:y+8-h,stroke:INK,'stroke-width':.9,
-        opacity:.55+rnd(k+3,i+5)*.45,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+11.5,r:.7,fill:'#C6C5BF',
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,fill:INK,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name} — ${v} tickets`);
-  });
-  txt(s,{x:130,y:186,'font-size':6.5,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = ONE TICKET · DOT MARKS EVERY FIFTH');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 375
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-04.zh.html

@@ -1,375 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>七月运营月报 — Lieflat 报告模板 · 青瓷月报</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Oswald:wght@500;600;700&family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 04 · 月度运营月报(中文版)
-     场景:月度运营 / 流量月报(团队内部同步、投资人月更、周期性复盘)
-     排版参考:"2017 Website Traffic" 浅蓝海报(左侧竖排压缩大写
-               书脊、右上角对齐节标题 + 细规线、底部 NOTABLE
-               STATISTICS 大数统计行)—— 只学排版和字体,浅蓝不学。
-     中西混排:数字走 Oswald(保留压缩感),汉字走 Noto Sans SC 黑体。
-               汉字没有压缩字形,书脊改重黑体 + 收紧字距。
-     色板正本 = color-presets.js 的 PORCELAIN 一套(青瓷蓝,明度即数据)。
-     彩色加墨规则(INK_BOOST):数据发丝线宽 ×1.8,透明度地板 .85。
-     图表正本 = lieflat gallery:
-       图A  F2  hairline line · basics-gallery.html
-       图B  F10 dot heat      · basics-gallery.html
-       图C  F4  tick donut    · basics-gallery.html
-       图D  F5  tick rows     · basics-gallery.html
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* PORCELAIN 角色 token(结构灰一律为文字色的透明档)*/
-    --bg:#F7F2EB; --ink:#081F5C; --muted:rgba(8,31,92,.60); --faint:rgba(8,31,92,.32); --grid:rgba(8,31,92,.16);
-    --num:'Oswald','Noto Sans SC',sans-serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--ink);font-family:'Inter','Noto Sans SC',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:56px 24px}
-  .sheet{width:1080px;max-width:1080px;display:grid;grid-template-columns:110px 1fr}
-
-  /* ── 书脊:重黑体竖排(学参考图4 的压缩大写)── */
-  .spine{position:relative;border-right:1px solid var(--ink)}
-  .spine .t{position:absolute;top:0;left:14px;writing-mode:vertical-rl;
-    font-family:'Noto Sans SC',sans-serif;font-weight:900;font-size:56px;line-height:1.1;
-    letter-spacing:.06em;white-space:nowrap}
-  .spine .b{position:absolute;bottom:0;left:48px;writing-mode:vertical-rl;
-    font-size:9px;font-weight:700;letter-spacing:.26em;color:var(--muted)}
-
-  .content{padding-left:40px}
-
-  /* ── 顶注 ── */
-  .tophead{display:flex;gap:18px;align-items:baseline;border-bottom:1px solid var(--ink);
-    padding-bottom:10px;margin-bottom:40px}
-  .tophead .site{font-family:var(--num);font-weight:600;font-size:13px;
-    letter-spacing:.08em;text-transform:uppercase}
-  .tophead .sep{color:var(--faint)}
-  .tophead .tag{font-size:10px;font-weight:600;letter-spacing:.12em;color:var(--muted)}
-
-  /* ── 节标题:右对齐 + 引线(学参考图4)── */
-  .secthead{display:flex;align-items:baseline;gap:14px;margin:44px 0 8px}
-  .secthead .line{flex:1;border-top:1px solid var(--ink)}
-  .secthead .t{font-family:'Noto Sans SC',sans-serif;font-weight:700;font-size:15px;
-    letter-spacing:.2em}
-  .secthead.first{margin-top:0}
-
-  .claim{font-size:13px;font-weight:700;color:var(--ink);margin:4px 0 2px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.1em;
-    color:var(--faint);margin-top:8px}
-
-  .duo{display:grid;grid-template-columns:1.25fr 1fr;gap:44px}
-
-  /* ── 底部统计行(学参考图4 的 NOTABLE STATISTICS)── */
-  .stats{display:grid;grid-template-columns:1fr 1fr 1.2fr 1fr;gap:0;margin-top:14px;
-    border-top:1px solid var(--ink)}
-  .stats>div{padding:22px 20px 6px;border-right:1px dotted var(--faint)}
-  .stats>div:first-child{padding-left:0}
-  .stats>div:last-child{border-right:0;padding-right:0}
-  .stats .kpi{margin-bottom:18px}
-  .stats .kpi .v{font-family:var(--num);font-weight:700;font-size:38px;line-height:1}
-  .stats .kpi .r{font-size:10px;font-weight:700;color:var(--muted);margin-top:4px}
-  .stats .kpi .l{font-size:9.5px;font-weight:600;letter-spacing:.12em;
-    color:var(--faint);margin-top:3px}
-  .stats h4{font-family:'Noto Sans SC',sans-serif;font-weight:700;font-size:12.5px;
-    letter-spacing:.14em;margin-bottom:10px}
-  .ranked .row{display:flex;gap:12px;align-items:baseline;padding:8px 0;
-    border-bottom:1px dotted var(--faint)}
-  .ranked .row:last-child{border-bottom:0}
-  .ranked .n{font-family:var(--num);font-weight:700;font-size:24px;min-width:38px}
-  .ranked .d{font-size:10px;line-height:1.7;color:var(--muted)}
-  .ranked .d b{color:var(--ink)}
-
-  .foot{margin-top:48px;border-top:1px solid var(--ink);padding-top:12px;
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;color:var(--muted)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 书脊 ══ -->
-  <div class="spine">
-    <div class="t">七月运营月报</div>
-    <div class="b">月度 · 2026</div>
-  </div>
-
-  <div class="content">
-    <div class="tophead">
-      <span class="site">yoursite.com</span><span class="sep">|</span>
-      <span class="tag">2026 年 7 月数据 · 全部指标环比上月</span>
-    </div>
-
-    <!-- ══ 图A · F2 hairline line:本月逐日 ══
-         模板:basics-gallery.html · "Thirty days of sign-ups" -->
-    <div class="secthead first"><span class="line"></span><span class="t">每日注册</span></div>
-    <div class="claim">两次发布的尖峰撑起了整个月,底盘几乎没动</div>
-    <div class="fig"><svg id="dayline" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-    <div class="srcline">HAIRLINE LINE · 一点 = 一天 · 空心 = 周末</div>
-
-    <!-- ══ 图B · F10 dot heat:一周节奏 ══
-         模板:basics-gallery.html · "When support gets loud" -->
-    <div class="secthead"><span class="line"></span><span class="t">客服何时最忙</span></div>
-    <div class="duo">
-      <div>
-        <div class="claim">周二上午十点,依旧是最吵的一小时</div>
-        <div class="fig"><svg id="dotheat" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">DOT HEAT · 7 天 × 12 小时 · 点面积 = 工单量</div>
-      </div>
-      <div>
-        <!-- ══ 图C · F4 tick donut:来源分布 ══
-             模板:basics-gallery.html · "Where the traffic comes from" -->
-        <div class="claim">自然流量这个月终于超过了投放</div>
-        <div class="fig"><svg id="donut" viewBox="0 0 330 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline">TICK DONUT · 一格 = 访问量的 1%</div>
-      </div>
-    </div>
-
-    <!-- ══ 值得记住的数字 ══ -->
-    <div class="secthead"><span class="line"></span><span class="t">值得记住的数字</span></div>
-    <div class="stats">
-      <div>
-        <div class="kpi"><div class="v">1,502</div><div class="r">日均 4.11</div><div class="l">浏览量</div></div>
-        <div class="kpi"><div class="v">581</div><div class="r">日均 1.59</div><div class="l">总访客</div></div>
-        <div class="kpi"><div class="v">358</div><div class="r">日均 0.98</div><div class="l">独立访客</div></div>
-      </div>
-      <div>
-        <div class="kpi"><div class="v">26.1%</div><div class="r">较 6 月的 19.4% 上升</div><div class="l">社交流量占比</div></div>
-        <div class="kpi"><div class="v">61</div><div class="r">每 100 次访问中</div><div class="l">来自桌面端</div></div>
-      </div>
-      <div>
-        <h4>各产品线客服工单</h4>
-        <!-- ══ 图D · F5 tick rows(迷你)══
-             模板:basics-gallery.html · "Six teams, shipped and counted" -->
-        <div class="fig"><svg id="rows" viewBox="0 0 260 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-        <div class="srcline" style="text-align:left">TICK ROWS · 一格 = 一张工单</div>
-      </div>
-      <div class="ranked">
-        <h4>本月阅读最多</h4>
-        <div class="row"><div class="n">45%</div><div class="d"><b>定价调整公告</b> —— 占全月浏览量的 45%</div></div>
-        <div class="row"><div class="n">19%</div><div class="d"><b>文档快速上手</b> —— 连续第二个月进前三</div></div>
-        <div class="row"><div class="n">14%</div><div class="d"><b>七月更新日志</b> —— changelog 触达新高</div></div>
-      </div>
-    </div>
-
-    <div class="foot">
-      <span>月度运营一页纸 · 2026 年 7 月</span>
-      <span>数据 · PLAUSIBLE + INTERCOM · 8 月 1 日拉取</span>
-    </div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(色板 / 几何 / 工具函数与正本一致)── */
-const BG='#F7F2EB',TXT='#081F5C',MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',FAINT='rgba(8,31,92,.32)';
-const FLOOR='rgba(8,31,92,.24)',QUIET='rgba(8,31,92,.15)',GRID='rgba(8,31,92,.16)';
-const DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C',FAINTDATA='#BAD6EB';
-const CAT4=['#081F5C','#334EAC','#7096D1','#BAD6EB'],HEAT=['#334EAC','#7096D1','#BAD6EB'];
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ════ 图A · F2 hairline line ════
-   结构照搬 basics-gallery "hairline line":barcode 日历地板 +
-   发丝折线 + 逐日圆点(周末空心),峰值两点放大标数。 */
-(()=>{
-const N=31;
-const val=d=>{
-  const spike=(d===5||d===6)?38:(d===19?30:0);        // 两次发布日
-  return 24+10*Math.sin(d/5.2)+rnd(d+1,5)*10+spike;
-};
-obsReveal('dayline',s=>{
-  const x=d=>26+d*11.4,base=254,map=v=>base-v*2.1;
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:20,y1:base,x2:380,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===3||d%7===4,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
-      fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`7 月 ${d+1} 日 —— ${Math.round(v)} 次注册`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'7月1日'],[14,'7月15日'],[30,'7月31日']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.06em',class:'fade'},m));
-  txt(s,{x:200,y:294,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
-    '一点 = 一天 · 空心 = 周末 · 标数 = 发布日');
-});
-})();
-
-/* ════ 图B · F10 dot heat ════
-   结构照搬 basics-gallery "dot heat":7×12 网格,点面积 = 工单量,
-   静默格留一粒极小点,最热格虚线圈标数。 */
-(()=>{
-const DAY=['周一','周二','周三','周四','周五','周六','周日'];
-obsReveal('dotheat',s=>{
-  const x0=j=>64+j*27,y0=i=>50+i*30;
-  let max=0,mi=0,mj=0;
-  const v=(i,j)=>{
-    const day=i<5?(i===1?1.25:1):.32;
-    const hour=Math.exp(-((j-3.4)**2)/6)+.6*Math.exp(-((j-8.6)**2)/5);
-    return Math.round(day*hour*20*(0.6+rnd(i*12+j+1,j+3)*.8));
-  };
-  for(let i=0;i<7;i++)for(let j=0;j<12;j++){const t=v(i,j);if(t>max){max=t;mi=i;mj=j}}
-  for(let i=0;i<7;i++){
-    txt(s,{x:50,y:y0(i)+3,'font-size':8,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.05}s`},DAY[i]);
-    for(let j=0;j<12;j++){
-      const t=v(i,j),x=x0(j),y=y0(i);
-      if(!t){el(s,'circle',{cx:x,cy:y,r:.8,fill:QUIET,
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});continue}
-      const hero=i===mi&&j===mj;
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1,
-        fill:t>max*.66?HEAT[0]:t>max*.33?HEAT[1]:HEAT[2],
-        class:'pop',style:`animation-delay:${i*.05+j*.015}s`});
-      tip(dot,`${DAY[i]} ${8+j}:00 —— ${t} 张工单`);
-      if(hero)el(s,'circle',{cx:x,cy:y,r:1.2+Math.sqrt(t)*2.1+3.4,fill:'none',
-        stroke:TXT,'stroke-width':1,'stroke-dasharray':'2 3',
-        class:'fade',style:'animation-delay:1s'});
-    }
-  }
-  for(let j=0;j<12;j+=2)
-    txt(s,{x:x0(j),y:y0(6)+26,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${j*.02}s`},(8+j)+':00');
-  txt(s,{x:200,y:296,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
-    `点面积 = 工单量 · 虚线圈 = 峰值 ${max} 张 · 极小点 = 安静时段`);
-});
-})();
-
-/* ════ 图C · F4 tick donut ════
-   结构照搬 basics-gallery "tick donut":1 tick = 1%,
-   四段来源用灰阶 ladder 分段。 */
-(()=>{
-const D=[['自然',37,CAT4[0]],['投放',28,CAT4[1]],['引荐',21,CAT4[2]],['社交',14,CAT4[3]]];
-obsReveal('donut',s=>{
-  const cx=170,cy=140,R0=58;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=9+rnd(idx+1,si+2)*5.5;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+34,mid),[gx,gy]=pol(cx,cy,R0+18,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8.5,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.04em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name}流量 —— 占 ${v}%`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':20,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+13,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.08em',class:'fade',style:'animation-delay:.9s'},'格 · 一格 = 1%');
-  txt(s,{x:170,y:288,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
-    '每十格一个圆点 · 顺时针读');
-});
-})();
-
-/* ════ 图D · F5 tick rows(迷你版)════
-   结构照搬 basics-gallery "tick rows":1 tick = 1 张工单。 */
-(()=>{
-const D=[['编辑器',31],['计费',24],['导入',17],['API',11],['其他',6]];
-obsReveal('rows',s=>{
-  const y0=i=>28+i*34,X0=72,PX=4.6;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i);
-    txt(s,{x:64,y:y+3,'font-size':7.5,'font-weight':700,fill:LAB,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+8,x2:X0+34*PX,y2:y+8,stroke:GRID,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=7+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+8,x2:x,y2:y+8-h,stroke:DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+11.5,r:.7,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(lab,`${name} —— ${v} 张工单`);
-  });
-  txt(s,{x:130,y:186,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.08em',class:'fade',style:'animation-delay:.9s'},
-    '一格 = 一张工单 · 每五格一个圆点');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 260
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-05.en.html

@@ -1,260 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Impact Story — Lieflat Report Template · Mono Highlight</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 05 · IMPACT STORY
-     场景:叙事型影响力报告(开源项目年度回顾、公益/社区项目汇报、
-           「一个故事 + 三组证据」式对外沟通页)
-     排版参考:"Hope for Girls and Women in Tanzania"(标题关键词
-               反色高亮块、正文导语开场、大数 + 长条带、高峰叙事图、
-               结尾行动号召)—— 只学排版和字体。
-     色板正本 = mono-tokens.js 的 Mono 灰阶。反色高亮块 = 炭黑 INK
-     上纸色字,是 Mono 两极的合法用法,没有引入新色。
-     图表正本 = lieflat gallery:
-       图A  百 tick 分段条带 · 结构 = lupi-gallery "ballot tally"
-            的单行 100 tick 队列 + basics-gallery "tick donut" 的
-            灰阶 ladder 分段语法(同数据形状的直排版本)
-       图B  L3 barcode lollipop · lupi-gallery.html "Ninety days as a barcode"
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0EFEB; --ink:#1C1C1A; --muted:#8F8E88; --faint:#C6C5BF; --grid:#DEDDD6;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--ink);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 24px}
-  .sheet{width:760px;max-width:760px}
-
-  /* ── 标题:关键词反色高亮(学参考图5)── */
-  h1{font-size:44px;font-weight:800;letter-spacing:-.03em;line-height:1.12;max-width:560px}
-  h1 .hl{background:var(--ink);color:var(--bg);padding:0 10px;box-decoration-break:clone;
-    -webkit-box-decoration-break:clone}
-  .lede{font-size:13.5px;line-height:1.75;color:#4A4944;max-width:520px;margin-top:26px}
-  .lede b{font-weight:700;color:var(--ink)}
-  .lede .caps{font-weight:700;letter-spacing:.04em}
-
-  /* ── 大数 + 条带 ── */
-  .bignum{display:flex;align-items:baseline;gap:14px;margin-top:52px}
-  .bignum .v{font-size:54px;font-weight:800;letter-spacing:-.04em;line-height:1}
-  .bignum .l{font-size:12px;color:var(--muted);font-weight:500}
-  .fig{margin-top:10px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
-    color:var(--faint);margin-top:8px}
-
-  /* ── 章节句:行内高亮块承接(学参考图5)── */
-  .storyline{font-size:22px;font-weight:700;letter-spacing:-.02em;line-height:1.35;
-    margin-top:68px;max-width:600px}
-  .storyline .hl{background:var(--ink);color:var(--bg);padding:1px 10px 2px;
-    box-decoration-break:clone;-webkit-box-decoration-break:clone}
-  .sidenote{font-size:11.5px;line-height:1.7;color:var(--muted);max-width:250px;margin-top:20px}
-  .legendrow{display:flex;gap:8px;align-items:center;font-size:10px;font-weight:600;
-    color:var(--muted);margin-top:14px}
-  .legendrow .sw{width:14px;height:10px;background:
-    repeating-linear-gradient(-45deg,var(--ink) 0 1.5px,transparent 1.5px 4px)}
-
-  /* ── 结尾 ── */
-  .close{font-size:13px;line-height:1.75;color:#4A4944;max-width:560px;margin-top:56px}
-  .close b{color:var(--ink)}
-  .cta{margin-top:22px;font-size:13.5px;font-weight:700}
-  .cta .hl{background:var(--ink);color:var(--bg);padding:3px 12px 4px}
-
-  .foot{margin-top:64px;padding-top:14px;border-top:1px solid var(--grid);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--faint)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <h1>One year of<br>answers in <span class="hl">public</span></h1>
-
-  <p class="lede"><span class="caps">OPEN OFFICE HOURS</span> started as an experiment: every question
-    about building our product gets answered in the open — on the forum, in the changelog, or on a call
-    anyone can join. Twelve months later the habit has outgrown the experiment. <b>1,204 questions</b>
-    were asked this year; most found their answer in public, where the next person can find it too.</p>
-
-  <!-- ══ 图A · 百 tick 分段条带 ══
-       结构 = lupi-gallery "ballot tally" 的单行 100 tick 队列,
-       分段语法 = basics-gallery "tick donut" 的灰阶 ladder。
-       1 tick = 1% ≈ 12 个问题。 -->
-  <div class="bignum"><div class="v">1,204</div><div class="l">questions asked between Aug 2025 and Jul 2026</div></div>
-  <div class="fig"><svg id="strip" viewBox="0 0 400 84" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">Tick strip · one tick = 1% ≈ 12 questions · ink = answered in public · mid-grey = answered privately · pale = still open</div>
-
-  <div class="storyline">Most questions arrive in the week after a <span class="hl">release</span>.</div>
-  <p class="sidenote">Release weeks run about six days and show up as spikes three to four times the
-    quiet-week baseline. The pattern repeated after all three launches this year.</p>
-  <div class="legendrow"><span class="sw"></span> release week</div>
-
-  <!-- ══ 图B · L3 barcode lollipop ══
-       模板:lupi-gallery.html · "Ninety days as a barcode"
-       90 个工作日的提问量,发布周的日子加斜纹背景带。 -->
-  <div class="fig" style="margin-top:18px"><svg id="barcode" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">Barcode lollipop · one stem = one weekday · hollow head = friday · top three days labeled</div>
-
-  <div class="storyline" style="margin-top:60px">Public answers keep working after we <span class="hl">log&nbsp;off</span>.</div>
-
-  <p class="close">A private reply helps one person once. A public one compounds: answered threads were
-    revisited <b>11,830 times</b> this year — almost ten reads for every original question. Forty-one
-    answers graduated into documentation pages. <b>If a question taught us something, it should teach
-    everyone.</b> That is the whole model.</p>
-
-  <div class="cta"><span class="hl">ask.yourproduct.com — every answer, in the open</span></div>
-
-  <div class="foot">
-    <span>Impact one-pager · FY2026</span>
-    <span>Source · forum + call logs · lieflat charts</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(色板 / 几何 / 工具函数与正本一致)── */
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ════ 图A · 百 tick 分段条带 ════
-   ballot tally 的单行队列 + tick donut 的 ladder 分段:
-   100 根 tick,前 68 根上墨(公开解答)、中 21 根中灰(私下解答)、
-   后 11 根淡灰(仍开放)。段间由分段点标记,段值贴段中。 */
-(()=>{
-const SEG=[['IN PUBLIC',68,INK],['PRIVATE',21,'#8F8E88'],['OPEN',11,'#C6C5BF']];
-obsReveal('strip',s=>{
-  const base=46,X0=14,PX=3.72;
-  el(s,'line',{x1:X0,y1:base,x2:X0+100*PX,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  let k0=0;
-  SEG.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,x=X0+idx*PX+PX/2;
-      const h=12+rnd(idx+1,si+2)*7;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,stroke:shade,
-        'stroke-width':si===2?.7:.9,opacity:si===2?.9:.6+rnd(idx+3,si+5)*.4,
-        class:'fade',style:`animation-delay:${idx*.008}s`});
-      if(idx%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:'#C6C5BF',
-        class:'fade',style:`animation-delay:${idx*.008}s`});
-    }
-    // 三段标签分别锚定:首段左对齐、中段居中、末段右对齐,避免相互挤压
-    const anchors=[[X0,'start'],[X0+(k0+v/2)*PX,'middle'],[X0+100*PX,'end']];
-    const [lx,anchor]=anchors[si];
-    const lab=txt(s,{x:lx,y:base+18,'font-size':7.5,'font-weight':800,
-      fill:shade==='#C6C5BF'?'#8F8E88':shade,'text-anchor':anchor,'letter-spacing':'.06em',
-      class:'fade',style:`animation-delay:${.5+si*.12}s`},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of all questions`);
-    k0+=v;
-  });
-  txt(s,{x:200,y:78,'font-size':6.5,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    '68 + 21 + 11 = 100 · DOT MARKS EVERY TENTH');
-});
-})();
-
-/* ════ 图B · L3 barcode lollipop ════
-   结构照搬 lupi-gallery "barcode lollipop":90 根发丝日历 +
-   逐日 lollipop(周五空心头),top-3 标数保持 ≥6 天间距。
-   发布周(三段各 6 天)在日历带上以密纹发丝标出——
-   密度本身就是 Mono 的合法材质,没有新颜色。 */
-(()=>{
-const N=90;
-const RELEASE=[[18,23],[47,52],[76,81]];               // three release weeks
-const inRel=d=>RELEASE.some(([a,b])=>d>=a&&d<=b);
-const val=d=>{
-  const base=28+10*Math.sin(d/8.5)+rnd(d+1,5)*14;
-  return inRel(d)?base*(2.6+rnd(d+2,7)*1.1):base;
-};
-obsReveal('barcode',s=>{
-  // release-week bands: denser hairlines behind the calendar
-  RELEASE.forEach(([a,b],ri)=>{
-    for(let d=a;d<=b;d++){
-      const x=18+d*4.05;
-      for(let m=0;m<3;m++)
-        el(s,'line',{x1:x-1.2+m*1.2,y1:12,x2:x-1.2+m*1.2,y2:252,stroke:'#DCDAD2',
-          'stroke-width':.5,class:'fade',style:`animation-delay:${ri*.1}s`});
-    }
-  });
-  // the barcode: one hairline per weekday
-  for(let d=0;d<N;d++){
-    const x=18+d*4.05;
-    el(s,'line',{x1:x,y1:12,x2:x,y2:252,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${d*.004}s`});
-  }
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top3=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);
-    if(top3.length===3)break;
-  }
-  for(let d=0;d<N;d++){
-    const x=18+d*4.05,v=vs[d],y=246-v*1.12;
-    const friday=d%5===4;
-    const stemEnd=Math.min(250,y+14+rnd(d+1,9)*26);
-    el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:INK,'stroke-width':1.1,
-      class:'fade',style:`animation-delay:${.3+d*.008}s`});
-    const big=top3.includes(d);
-    const dot=el(s,'circle',{cx:x,cy:y,r:big?4.4:2.4,
-      fill:friday?PAPER:INK,stroke:INK,'stroke-width':friday?1.2:0,
-      class:'pop',style:`animation-delay:${.3+d*.008}s`});
-    tip(dot,`Weekday ${d+1} — ${Math.round(v)} questions`);
-    if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.9+d*.004}s`,
-      class:'fade'},Math.round(v));
-  }
-  [[0,'AUG'],[30,'OCT'],[60,'JAN'],[89,'MAR']].forEach(([d,m])=>
-    txt(s,{x:18+d*4.05,y:272,'font-size':8,'font-weight':600,fill:MUTED,
-      'letter-spacing':'.12em',class:'fade'},m));
-  txt(s,{x:200,y:292,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'DENSE BAND = RELEASE WEEK · ONE STEM = ONE WEEKDAY');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=760;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 258
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-05.zh.html

@@ -1,258 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>把回答写在明处 — Lieflat 报告模板 · 反色高亮</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 05 · 影响力故事(中文版)
-     场景:叙事型影响力报告(开源项目年度回顾、公益/社区项目汇报、
-           「一个故事 + 三组证据」式对外沟通页)
-     排版参考:"Hope for Girls and Women in Tanzania"(标题关键词
-               反色高亮块、正文导语开场、大数 + 长条带、高峰叙事图、
-               结尾行动号召)—— 只学排版和字体。
-     中西混排:数字走 Inter,汉字走 Noto Sans SC。中文标题不用负字距。
-     色板正本 = mono-tokens.js 的 Mono 灰阶。反色高亮块 = 炭黑 INK
-     上纸色字,是 Mono 两极的合法用法,没有引入新色。
-     图表正本 = lieflat gallery:
-       图A  百 tick 分段条带 · 结构 = lupi-gallery "ballot tally"
-            的单行 100 tick 队列 + basics-gallery "tick donut" 的
-            灰阶 ladder 分段语法(同数据形状的直排版本)
-       图B  L3 barcode lollipop · lupi-gallery.html
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0EFEB; --ink:#1C1C1A; --muted:#8F8E88; --faint:#C6C5BF; --grid:#DEDDD6;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--ink);font-family:'Inter','Noto Sans SC',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 24px}
-  .sheet{width:760px;max-width:760px}
-
-  /* ── 标题:关键词反色高亮(学参考图5)── */
-  h1{font-size:42px;font-weight:900;letter-spacing:.01em;line-height:1.3;max-width:560px}
-  h1 .hl{background:var(--ink);color:var(--bg);padding:0 12px;box-decoration-break:clone;
-    -webkit-box-decoration-break:clone}
-  .lede{font-size:13.5px;line-height:1.95;color:#4A4944;max-width:520px;margin-top:26px}
-  .lede b{font-weight:700;color:var(--ink)}
-  .lede .caps{font-weight:700;letter-spacing:.06em}
-
-  /* ── 大数 + 条带 ── */
-  .bignum{display:flex;align-items:baseline;gap:14px;margin-top:52px}
-  .bignum .v{font-size:54px;font-weight:800;letter-spacing:-.04em;line-height:1}
-  .bignum .l{font-size:12px;color:var(--muted);font-weight:500}
-  .fig{margin-top:10px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.1em;
-    color:var(--faint);margin-top:8px}
-
-  /* ── 章节句:行内高亮块承接(学参考图5)── */
-  .storyline{font-size:22px;font-weight:700;letter-spacing:.01em;line-height:1.6;
-    margin-top:68px;max-width:600px}
-  .storyline .hl{background:var(--ink);color:var(--bg);padding:1px 12px 2px;
-    box-decoration-break:clone;-webkit-box-decoration-break:clone}
-  .sidenote{font-size:11.5px;line-height:1.85;color:var(--muted);max-width:260px;margin-top:20px}
-  .legendrow{display:flex;gap:8px;align-items:center;font-size:10px;font-weight:600;
-    color:var(--muted);margin-top:14px}
-  .legendrow .sw{width:14px;height:10px;background:
-    repeating-linear-gradient(-45deg,var(--ink) 0 1.5px,transparent 1.5px 4px)}
-
-  /* ── 结尾 ── */
-  .close{font-size:13px;line-height:1.95;color:#4A4944;max-width:560px;margin-top:56px}
-  .close b{color:var(--ink)}
-  .cta{margin-top:22px;font-size:13.5px;font-weight:700}
-  .cta .hl{background:var(--ink);color:var(--bg);padding:3px 14px 4px}
-
-  .foot{margin-top:64px;padding-top:14px;border-top:1px solid var(--grid);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;color:var(--faint)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <h1>把回答,<br>写在<span class="hl">明处</span></h1>
-
-  <p class="lede"><span class="caps">「公开答疑」</span>最初只是个实验:关于我们产品的每一个问题,
-    都在公开处回答——论坛、更新日志,或者一场任何人都能加入的通话。十二个月过去,这个习惯已经长得比实验大。
-    这一年共有 <b>1,204 个问题</b>被提出;大多数在公开处找到了答案,下一个遇到同样问题的人也能找到它。</p>
-
-  <!-- ══ 图A · 百 tick 分段条带 ══
-       结构 = lupi-gallery "ballot tally" 的单行 100 tick 队列,
-       分段语法 = basics-gallery "tick donut" 的灰阶 ladder。
-       1 tick = 1% ≈ 12 个问题。 -->
-  <div class="bignum"><div class="v">1,204</div><div class="l">个问题 · 2025 年 8 月至 2026 年 7 月</div></div>
-  <div class="fig"><svg id="strip" viewBox="0 0 400 84" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">TICK STRIP · 一格 = 1% ≈ 12 个问题 · 墨色 = 公开解答 · 中灰 = 私下解答 · 浅灰 = 仍待解</div>
-
-  <div class="storyline">大多数问题,出现在<span class="hl">发布</span>后的那一周。</div>
-  <p class="sidenote">发布周持续六天左右,提问量会冲到平静周基线的三到四倍。今年三次发布之后,这个规律一次没落空。</p>
-  <div class="legendrow"><span class="sw"></span> 发布周</div>
-
-  <!-- ══ 图B · L3 barcode lollipop ══
-       模板:lupi-gallery.html · "Ninety days as a barcode"
-       90 个工作日的提问量,发布周的日子加斜纹背景带。 -->
-  <div class="fig" style="margin-top:18px"><svg id="barcode" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">BARCODE LOLLIPOP · 一根 = 一个工作日 · 空心头 = 周五 · 最高三天标数</div>
-
-  <div class="storyline" style="margin-top:60px">公开的回答,在我们<span class="hl">下线</span>之后还在工作。</div>
-
-  <p class="close">私下回复,帮到一个人一次。公开的回复会复利:已解答的帖子今年被重访了
-    <b>11,830 次</b>——几乎每个原始问题带来十次阅读。有 41 条回答长成了正式文档页。
-    <b>如果一个问题教会了我们什么,它就该教会所有人。</b>这就是全部的模式。</p>
-
-  <div class="cta"><span class="hl">ask.yourproduct.com —— 每一条回答,都在明处</span></div>
-
-  <div class="foot">
-    <span>影响力一页纸 · 2026 财年</span>
-    <span>来源 · 论坛 + 通话记录 · LIEFLAT CHARTS</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(色板 / 几何 / 工具函数与正本一致)── */
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6';
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ════ 图A · 百 tick 分段条带 ════
-   ballot tally 的单行队列 + tick donut 的 ladder 分段:
-   100 根 tick,前 68 根上墨(公开解答)、中 21 根中灰(私下解答)、
-   后 11 根淡灰(仍待解)。三段标签分别锚定首/中/尾。 */
-(()=>{
-const SEG=[['公开解答',68,INK],['私下',21,'#8F8E88'],['待解',11,'#C6C5BF']];
-obsReveal('strip',s=>{
-  const base=46,X0=14,PX=3.72;
-  el(s,'line',{x1:X0,y1:base,x2:X0+100*PX,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  let k0=0;
-  SEG.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,x=X0+idx*PX+PX/2;
-      const h=12+rnd(idx+1,si+2)*7;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,stroke:shade,
-        'stroke-width':si===2?.7:.9,opacity:si===2?.9:.6+rnd(idx+3,si+5)*.4,
-        class:'fade',style:`animation-delay:${idx*.008}s`});
-      if(idx%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.8,fill:'#C6C5BF',
-        class:'fade',style:`animation-delay:${idx*.008}s`});
-    }
-    // 三段标签分别锚定:首段左对齐、中段居中、末段右对齐,避免相互挤压
-    const anchors=[[X0,'start'],[X0+(k0+v/2)*PX,'middle'],[X0+100*PX,'end']];
-    const [lx,anchor]=anchors[si];
-    const lab=txt(s,{x:lx,y:base+18,'font-size':8,'font-weight':800,
-      fill:shade==='#C6C5BF'?'#8F8E88':shade,'text-anchor':anchor,'letter-spacing':'.04em',
-      class:'fade',style:`animation-delay:${.5+si*.12}s`},`${name} · ${v}`);
-    tip(lab,`${name} —— 占全部问题的 ${v}%`);
-    k0+=v;
-  });
-  txt(s,{x:200,y:78,'font-size':7,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '68 + 21 + 11 = 100 · 每十格一个圆点');
-});
-})();
-
-/* ════ 图B · L3 barcode lollipop ════
-   结构照搬 lupi-gallery "barcode lollipop":90 根发丝日历 +
-   逐日 lollipop(周五空心头),top-3 标数保持 ≥6 天间距。
-   发布周(三段各 6 天)在日历带上以密纹发丝标出——
-   密度本身就是 Mono 的合法材质,没有新颜色。 */
-(()=>{
-const N=90;
-const RELEASE=[[18,23],[47,52],[76,81]];               // 三个发布周
-const inRel=d=>RELEASE.some(([a,b])=>d>=a&&d<=b);
-const val=d=>{
-  const base=28+10*Math.sin(d/8.5)+rnd(d+1,5)*14;
-  return inRel(d)?base*(2.6+rnd(d+2,7)*1.1):base;
-};
-obsReveal('barcode',s=>{
-  // 发布周:日历带背后加密发丝
-  RELEASE.forEach(([a,b],ri)=>{
-    for(let d=a;d<=b;d++){
-      const x=18+d*4.05;
-      for(let m=0;m<3;m++)
-        el(s,'line',{x1:x-1.2+m*1.2,y1:12,x2:x-1.2+m*1.2,y2:252,stroke:'#DCDAD2',
-          'stroke-width':.5,class:'fade',style:`animation-delay:${ri*.1}s`});
-    }
-  });
-  // 日历条码:一根发丝 = 一个工作日
-  for(let d=0;d<N;d++){
-    const x=18+d*4.05;
-    el(s,'line',{x1:x,y1:12,x2:x,y2:252,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${d*.004}s`});
-  }
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top3=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top3.every(t=>Math.abs(t-d)>=6))top3.push(d);
-    if(top3.length===3)break;
-  }
-  for(let d=0;d<N;d++){
-    const x=18+d*4.05,v=vs[d],y=246-v*1.12;
-    const friday=d%5===4;
-    const stemEnd=Math.min(250,y+14+rnd(d+1,9)*26);
-    el(s,'line',{x1:x,y1:y,x2:x,y2:stemEnd,stroke:INK,'stroke-width':1.1,
-      class:'fade',style:`animation-delay:${.3+d*.008}s`});
-    const big=top3.includes(d);
-    const dot=el(s,'circle',{cx:x,cy:y,r:big?4.4:2.4,
-      fill:friday?PAPER:INK,stroke:INK,'stroke-width':friday?1.2:0,
-      class:'pop',style:`animation-delay:${.3+d*.008}s`});
-    tip(dot,`第 ${d+1} 个工作日 —— ${Math.round(v)} 个问题`);
-    if(big)txt(s,{x,y:y-10,'font-size':9,'font-weight':800,fill:INK,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${PAPER};stroke-width:3px;animation-delay:${.9+d*.004}s`,
-      class:'fade'},Math.round(v));
-  }
-  [[0,'8月'],[30,'10月'],[60,'1月'],[89,'3月']].forEach(([d,m])=>
-    txt(s,{x:18+d*4.05,y:272,'font-size':8,'font-weight':600,fill:MUTED,
-      'letter-spacing':'.08em',class:'fade'},m));
-  txt(s,{x:200,y:292,'font-size':7.5,'font-weight':600,fill:'#B0AFA9','text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
-    '密纹带 = 发布周 · 一根 = 一个工作日');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=760;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 434
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-06.en.html

@@ -1,434 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Eight-Year Product Almanac — Lieflat Report Template · Palm Deep Water</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Playfair+Display:ital,wght@0,500;0,700;0,800;0,900;1,500&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 06 · EIGHT-YEAR PRODUCT ALMANAC
-     目的:验证 Lupi Editorial 组里最复杂的四张图放进报告版式的效果。
-     版式骨架 = 模板 02(衬底 + 纸张 + 字距节标题 + 大数收尾),
-     色板 = color-presets.js 的 PALM 一套(绿黄低饱和 + 琥珀强调)。
-     复杂图的色彩纪律:SER 分类色一簇一色,RAMP 序数梯一档一级,
-     琥珀只给峰值。彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     图表正本 = lupi-gallery.html,四张全部照搬结构只换数据:
-       图A  L1  launch fan         "Twelve features, fanned out"
-       图B  L5  radial convergence "48 requests pull toward five themes"
-       图C  L·almanac bubble almanac "Eight years of tickets, one almanac"
-            —— 860 宽大图,靠 980 版心整行放下,这是选这个骨架的原因
-       图D  L9  hourglass stream   "The funnel, poured"
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* MONO tokens */
-    --paper:#F0EFEB; --ink:#58402E; --muted:rgba(88,64,46,.60); --faint:rgba(88,64,46,.40); --grid:rgba(88,64,46,.16);
-    --mat:#28311F;
-    --serif:'Playfair Display',Georgia,serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--mat);font-family:'Inter',sans-serif;color:var(--ink);
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 32px}
-  .sheet{width:980px;max-width:980px;background:var(--paper);padding:64px 68px 72px}
-
-  /* ── 刊头 ── */
-  .masthead{display:flex;justify-content:space-between;align-items:baseline;
-    border-bottom:2px solid var(--ink);padding-bottom:14px}
-  .masthead h1{font-family:var(--serif);font-size:36px;font-weight:900;letter-spacing:-.03em;line-height:1.15}
-  .masthead .tag{font-size:9.5px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
-  .lede{font-size:12.5px;line-height:1.75;color:rgba(88,64,46,.72);max-width:620px;margin-top:20px}
-  .lede b{color:var(--ink)}
-
-  /* ── 节标题 ── */
-  .sect{font-size:13px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
-    padding-bottom:10px;border-bottom:1px solid var(--ink);margin-bottom:8px;margin-top:52px}
-  .sect .n{color:var(--muted);font-weight:600;margin-right:8px}
-  .claim{font-size:12.5px;font-weight:700;margin:10px 0 2px}
-  .duo{display:grid;grid-template-columns:1fr 1fr;gap:48px}
-  .duo .sect{margin-top:52px}
-
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
-    color:var(--faint);margin-top:8px}
-
-  /* ── 底部收尾:漏斗 + 大数对话框(学模板 02)── */
-  .finale{display:grid;grid-template-columns:1.2fr 1fr;gap:48px;align-items:center}
-  .bignum{border:1px solid var(--faint);padding:34px 30px;position:relative}
-  .bignum::before{content:'';position:absolute;left:-8px;top:50%;width:14px;height:14px;
-    transform:translateY(-50%) rotate(45deg);background:var(--paper);
-    border-left:1px solid var(--faint);border-bottom:1px solid var(--faint)}
-  .bignum .v{font-family:var(--serif);font-size:60px;font-weight:900;
-    line-height:1;color:var(--ink)}
-  .bignum .v small{font-size:28px}
-  .bignum .l{font-size:12.5px;font-weight:700;line-height:1.5;margin-top:14px}
-  .bignum .d{font-size:10.5px;color:var(--muted);line-height:1.55;margin-top:6px}
-
-  .foot{margin-top:64px;padding-top:16px;border-top:2px solid var(--ink);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <div class="masthead">
-    <h1>Eight years of product, one almanac page</h1>
-    <span class="tag">2019 – 2026 · Lupi Editorial Deep Water</span>
-  </div>
-  <p class="lede">Twelve features, forty-eight requests, eight years of tickets, one funnel. This page
-    lines up four kinds of product time at once: <b>when things shipped, what users asked for, where
-    it broke, and where people fell out</b>. Every unit in every chart counts for something — one
-    hairline is a week, one dot is a request, one bubble is a stack of tickets.</p>
-
-  <!-- ══ 上排:扇形发布图 | 放射汇聚 ══ -->
-  <div class="duo">
-    <div>
-      <div class="sect"><span class="n">I</span>When features were born</div>
-      <div class="claim">Twelve features fanned out by launch week, big dots rolling down the diagonal</div>
-      <!-- 图A · L1 launch fan · lupi-gallery.html "Twelve features, fanned out"
-           辐条 = 功能,起点 = 发布周,点径 = 月活。 -->
-      <div class="fig"><svg id="fan" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Launch fan · spoke start = launch week · big-dot area = monthly active users</div>
-    </div>
-    <div>
-      <div class="sect"><span class="n">II</span>What users are asking for</div>
-      <div class="claim">Forty-eight requests, pulled toward five themes</div>
-      <!-- 图B · L5 radial convergence · lupi-gallery.html
-           "48 requests pull toward five themes"
-           轮辋 = 一条条请求(带编号),曲线收进五个主题枢纽。 -->
-      <div class="fig"><svg id="converge" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Radial convergence · rim dot = one request · hub area = requests per theme</div>
-    </div>
-  </div>
-
-  <!-- ══ 中部整行:气泡年鉴 ══ -->
-  <div class="sect"><span class="n">III</span>Where it broke — eight years of tickets</div>
-  <div class="claim">Tickets follow GA: the moment a feature ships, its bubble starts to swell</div>
-  <!-- 图C · bubble almanac · lupi-gallery.html "Eight years of tickets, one almanac"
-       账本发丝纸 + 手绘抖动气泡 + 旁注 + 底部大事记。860 宽整行放。 -->
-  <div class="fig"><svg id="almanac" viewBox="0 0 860 390" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">Bubble almanac · bubble area = tickets that year · shade = five volume tiers · amber = the three biggest peaks · dashed ring = beta window</div>
-
-  <!-- ══ 底部:沙漏漏斗 + 大数 ══ -->
-  <div class="duo" style="margin-top:0">
-    <div>
-      <div class="sect"><span class="n">IV</span>Where people fall out</div>
-      <div class="claim">Barcodes are people, hairlines are the drop-off — the funnel isn't drawn, it's leaked</div>
-      <!-- 图D · L9 hourglass stream · lupi-gallery.html "The funnel, poured"
-           每层条带 = barcode(一根 ≈ 40 人),层间细线涓流承接,
-           左缘停靠层间转化率。 -->
-      <div class="fig"><svg id="hourglass" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Hourglass stream · one stem ≈ 40 people · darker shade lower down · left edge = stage-to-stage conversion</div>
-    </div>
-    <div style="align-self:center">
-      <div class="bignum">
-        <div class="v">7.4<small>%</small></div>
-        <div class="l">Visitor to paid, only 7.4% make the whole trip</div>
-        <div class="d">4,200 visitors → 310 paid. The steepest drop is at Activation: half the people
-          who sign up never finish their first project. The 2023 ticket spike in the almanac and the
-          Performance theme in the convergence chart point at the same problem.</div>
-      </div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>Eight-Year Product Almanac · One-Pager</span>
-    <span>Data · internal ticketing system + product analytics · Lieflat Charts</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(色板 / 几何 / 工具函数与正本一致)── */
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)';
-const QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#58402E',HERO='#D4A017',FAINTDATA='#ACAD79';
-const SER=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79','#58402E'];   // 分类色:一簇一色
-const RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B'];            // 5 档序数梯(浅→重)
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ════ 图A · L1 launch fan ════
-   结构照搬 lupi-gallery "launch fan":辐条按发布周排序,
-   大点(月活)沿对角弧滚落,周环虚线作导轨。 */
-(()=>{
-const FEAT=['EDITOR','BOARD','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','TABLES','VIEWS','HUB','FORMS'];
-const LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19];     // 发布周
-const CX=40,CY=314,rOf=w=>56+w*9.7;
-obsReveal('fan',s=>{
-  const ang=i=>-84+i*(76/11);
-  // 周环虚线导轨
-  [5,10,15,20].forEach((w,gi)=>{
-    const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);
-    el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',
-      stroke:QUIET,'stroke-width':1,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${gi*.1}s`});
-    const [lx,ly]=pol(CX,CY,r,-87.5);
-    txt(s,{x:lx,y:ly,'font-size':6,fill:FAINT,'text-anchor':'middle','letter-spacing':'.05em',
-      class:'fade',style:`animation-delay:${gi*.1}s`},'WK '+w);
-  });
-  FEAT.forEach((name,i)=>{
-    const a=ang(i),L=LAUNCH[i];
-    const col=RAMP[4-Math.min(4,Math.floor((L-1)/4))];   // 上线越早 = 越重
-    const mau=Math.round(48-i*3+rnd(i+1,3)*10);
-    // 发丝辐条:从发布点到轮辋
-    const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);
-    el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:QUIET,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`});
-    // 周纹理点
-    for(let w=L+1;w<=20;w++){
-      const [x,y]=pol(CX,CY,rOf(w),a);
-      el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:col,opacity:.55,
-        class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});
-    }
-    // 发布点:面积 = 月活
-    const [bx,by]=pol(CX,CY,rOf(L),a);
-    const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:col,
-      class:'pop',style:`animation-delay:${.15+i*.07}s`});
-    tip(big,`${name} — launched week ${L} · ${mau}k MAU`);
-    // 轮辋标签沿辐条走
-    const [tx,ty]=pol(CX,CY,rOf(20)+9,a);
-    txt(s,{x:tx,y:ty,'font-size':7,'font-weight':600,fill:LAB,'letter-spacing':'.05em',
-      transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',
-      class:'fade',style:`animation-delay:${.3+i*.05}s`},name);
-  });
-});
-})();
-
-/* ════ 图B · L5 radial convergence ════
-   结构照搬 lupi-gallery "radial convergence":48 条请求在轮辋
-   带编号站好,贝塞尔曲线被五个主题枢纽拽进去,枢纽面积 = 请求数,
-   少量请求"漏"到别的主题(organic leakage)。 */
-(()=>{
-const N=48,CX=200,CY=166,R=116;
-const THEME=[['PERFORMANCE',15,-75],['INTEGRATIONS',10,-3],['PRICING',9,69],['MOBILE',8,141],['EXPERIENCE',6,213]];
-obsReveal('converge',s=>{
-  const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
-  const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];
-  const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
-  const counts=[0,0,0,0,0];
-  for(let i=0;i<N;i++)counts[hubOf(i)]++;
-  for(let i=0;i<N;i++){
-    const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
-    const h=hubOf(i),[hx,hy]=hubPt(h);
-    const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
-    const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
-    el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
-      fill:'none',stroke:SER[h],'stroke-width':.9,opacity:.62,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.018}s`});
-    el(s,'circle',{cx:px,cy:py,r:1.6,fill:SER[h],
-      class:'pop',style:`animation-delay:${i*.018}s`});
-    // 轮辋小编号,左半翻转保持可读
-    const flip=deg>90&&deg<270,[lx,ly]=pol(CX,CY,R+7,deg);
-    txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUT,
-      'text-anchor':flip?'end':'start','dominant-baseline':'middle',
-      transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,
-      class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));
-  }
-  THEME.forEach(([name,,deg],h)=>{
-    const [hx,hy]=hubPt(h);
-    const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,
-      fill:counts[h]===Math.max(...counts)?HERO:SER[h],
-      class:'pop',style:`animation-delay:${.5+h*.08}s`});
-    tip(hub,`${name} — ${counts[h]} requests`);
-    const [tx,ty]=pol(CX,CY,R+34,deg);
-    txt(s,{x:tx,y:ty,'font-size':8,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'dominant-baseline':'middle','letter-spacing':'.07em',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.6+h*.08}s`,
-      class:'fade'},`${name} · ${counts[h]}`);
-    const [gx,gy]=pol(CX,CY,R+22,deg);
-    el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
-  });
-});
-})();
-
-/* ════ 图C · bubble almanac ════
-   结构照搬 lupi-gallery "bubble almanac":账本发丝纸垫底、
-   手绘抖动气泡(blob,两个慢波 + 噪声)、beta 虚线圈、
-   升级件墨核、斜排列头、旁注 + 底部大事记。860 宽整行。 */
-(()=>{
-const AREAS=['EDITOR','BOARD','DOCS','CHAT','FLOWS','VAULT','PAGES','SYNC','TABLES','VIEWS','HUB','FORMS'];
-const BETA={'TABLES':2023,'VIEWS':2024,'HUB':2025,'FORMS':2025};   // beta 到这一年
-const BORN={'EDITOR':2019,'BOARD':2019,'DOCS':2019,'CHAT':2020,'FLOWS':2020,'VAULT':2021,'PAGES':2021,'SYNC':2022,'TABLES':2022,'VIEWS':2023,'HUB':2024,'FORMS':2024};
-obsReveal('almanac',s=>{
-  const rowY=i=>62+i*34,colX=j=>132+j*57;
-  // 账本纸:6.8px 一根发丝,边到边,垫在一切下面
-  for(let y=40;y<=318;y+=6.8)
-    el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:QUIET,'stroke-width':.5,
-      class:'fade',style:`animation-delay:${(y-40)*.001}s`});
-  const vals=[];
-  for(let i=0;i<8;i++){
-    const yr=2019+i;
-    el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${i*.04}s`});
-    txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.04}s`},yr);
-    AREAS.forEach((a,j)=>{
-      if(yr<BORN[a])return;
-      const age=yr-BORN[a];
-      const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));
-      vals.push([Math.max(1,v),i,j,a,yr]);
-    });
-  }
-  vals.sort((a,b)=>b[0]-a[0]);
-  const top3=vals.slice(0,3);
-  // 手绘圆:圆周叠两个慢波 + 噪声,seed 定形——没有一只是圆规圆
-  const blob=(x,y,r,seed)=>{
-    const n=Math.max(14,Math.round(r*1.6)),pts=[];
-    for(let t=0;t<n;t++){
-      const a=t/n*Math.PI*2;
-      const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;
-      pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);
-    }
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<n;t++){
-      const p=pts[t],q=pts[(t+1)%n];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+' Z';
-  };
-  vals.forEach(([v,i,j,a,yr],k)=>{
-    const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10,
-          y=rowY(i),r=Math.sqrt(v)*3.8;
-    const beta=BETA[a]&&yr<=BETA[a],isTop=top3.includes(vals[k]);
-    const col=RAMP[Math.min(4,Math.floor(v/12))];        // 票量 5 档
-    const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});
-    const seed=i*12+j;
-    const dot=el(g,'path',beta
-      ?{d:blob(x,y,r,seed),fill:'none',stroke:DATA2,'stroke-width':1.2,'stroke-dasharray':'3 3'}
-      :{d:blob(x,y,r,seed),fill:col,'fill-opacity':.16+rnd(i+2,j+4)*.14,
-        stroke:isTop?HERO:col,'stroke-opacity':isTop?.95:.5+rnd(i+5,j+7)*.38,'stroke-width':.6+rnd(i+3,j+11)*.9});
-    tip(dot,`${a} · ${yr} — ${v*10} tickets${beta?' (beta)':''}`);
-    // 升级件墨核:小抖动墨块,压偏一点,像印刷压痕
-    if(!beta){
-      const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;
-      el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:isTop?HERO:TXT});
-    }
-    if(top3.includes(vals[k]))
-      txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:HERO,'text-anchor':'middle',
-        style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1s`,
-        class:'fade'},v*10);
-  });
-  AREAS.forEach((a,j)=>{
-    const x=colX(j);
-    txt(s,{x,y:34,'font-size':7.5,'font-weight':700,fill:MUT,'letter-spacing':'.07em',
-      transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);
-  });
-  // 旁注:指针发丝 + 小字批注,像原版年鉴的手记。
-  // 只保留右上这条——左半图气泡太密,任何批注放进去都会被淹掉。
-  el(s,'path',{d:`M${colX(9)+16} ${rowY(2)+8} C${colX(9)+40} ${rowY(2)+26} ${colX(9)+60} ${rowY(2)+30} ${colX(9)+80} ${rowY(2)+30}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7.5,fill:LAB,
-    class:'fade',style:'animation-delay:1.2s'},'Beta barely generates tickets — nobody files bugs against a toy');
-  // 底部大事记:年鉴的时间线搁板
-  const evY=360;
-  el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:FAINT,'stroke-width':.8,
-    class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:56,y:evY-24,'font-size':7.5,'font-weight':800,fill:MUT,'letter-spacing':'.14em',
-    class:'fade',style:'animation-delay:1.2s'},'MILESTONES');
-  [['2020','SLA introduced'],['2022','Self-serve help center launches'],['2023','Year of GA releases'],['2025','AI triage launches']]
-  .forEach(([yr,note],k)=>{
-    const x=90+k*196;
-    el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:MUT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`});
-    txt(s,{x,y:evY+4,'font-size':8.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);
-    txt(s,{x,y:evY+15,'font-size':7.5,fill:MUT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);
-  });
-});
-})();
-
-/* ════ 图D · L9 hourglass stream ════
-   结构照搬 lupi-gallery "hourglass stream":每层条带是 barcode
-   (一根 ≈ 40 人),层间 34 根细线涓流承接,左缘停靠转化率,
-   右侧发丝引出层名和人数。 */
-(()=>{
-const STAGE=[['VISITORS',4200],['SIGNUPS',1900],['ACTIVATED',960],['RETAINED',540],['PAID',310]];
-obsReveal('hourglass',s=>{
-  const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;
-  STAGE.forEach(([name,c],k)=>{
-    const y=sy(k),hw=w(c)/2;
-    // barcode 条带:一根 ≈ 40 人
-    const n=Math.round(c/40);
-    for(let t=0;t<n;t++){
-      const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;
-      el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:RAMP[k],'stroke-width':1.4,
-        opacity:.7+rnd(t+2,k+5)*.3,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
-    }
-    // 涓流细线,流向下一层条带
-    if(k<4){
-      const hw1=w(STAGE[k+1][1])/2;
-      for(let t=0;t<34;t++){
-        const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
-        const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
-        el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,
-          fill:'none',stroke:FAINTDATA,'stroke-width':.7,opacity:.45,pathLength:1,
-          class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
-      }
-      // 层间转化率,停靠左缘
-      const pct=Math.round(STAGE[k+1][1]/c*100);
-      txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':8.5,'font-weight':800,fill:DATA,
-        class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
-      txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6.5,'font-weight':600,fill:FAINT,
-        'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'to next stage');
-    }
-    // 层名,发丝拴在条带边上
-    el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.3+k*.12}s`});
-    txt(s,{x:344,y:y-1,'font-size':7.5,'font-weight':700,fill:LAB,
-      'letter-spacing':'.07em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
-    txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());
-  });
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 433
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-06.zh.html

@@ -1,433 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>产品八年年鉴 — Lieflat 报告模板 · 椰林深水区</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 06 · 产品八年年鉴(Lupi 深水区示范)
-     目的:验证 Lupi Editorial 组里最复杂的四张图放进报告版式的效果。
-     版式骨架 = 模板 02(衬底 + 纸张 + 字距节标题 + 大数收尾),
-     色板 = color-presets.js 的 PALM 一套(绿黄低饱和 + 琥珀强调)。
-     复杂图的色彩纪律:SER 分类色一簇一色,RAMP 序数梯一档一级,
-     琥珀只给峰值。彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     图表正本 = lupi-gallery.html,四张全部照搬结构只换数据:
-       图A  L1  launch fan         "Twelve features, fanned out"
-       图B  L5  radial convergence "48 requests pull toward five themes"
-       图C  L·almanac bubble almanac "Eight years of tickets, one almanac"
-            —— 860 宽大图,靠 980 版心整行放下,这是选这个骨架的原因
-       图D  L9  hourglass stream   "The funnel, poured"
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* MONO tokens */
-    --paper:#F0EFEB; --ink:#58402E; --muted:rgba(88,64,46,.60); --faint:rgba(88,64,46,.40); --grid:rgba(88,64,46,.16);
-    --mat:#28311F;
-    --serif:'Noto Serif SC','Songti SC',serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--mat);font-family:'Inter','Noto Sans SC',sans-serif;color:var(--ink);
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 32px}
-  .sheet{width:980px;max-width:980px;background:var(--paper);padding:64px 68px 72px}
-
-  /* ── 刊头 ── */
-  .masthead{display:flex;justify-content:space-between;align-items:baseline;
-    border-bottom:2px solid var(--ink);padding-bottom:14px}
-  .masthead h1{font-family:var(--serif);font-size:36px;font-weight:900;letter-spacing:.02em}
-  .masthead .tag{font-size:10px;font-weight:600;letter-spacing:.16em;color:var(--muted)}
-  .lede{font-size:12.5px;line-height:1.95;color:rgba(88,64,46,.72);max-width:620px;margin-top:20px}
-  .lede b{color:var(--ink)}
-
-  /* ── 节标题 ── */
-  .sect{font-size:13.5px;font-weight:800;letter-spacing:.2em;
-    padding-bottom:10px;border-bottom:1px solid var(--ink);margin-bottom:8px;margin-top:52px}
-  .sect .n{color:var(--muted);font-weight:600;margin-right:8px}
-  .claim{font-size:13px;font-weight:700;margin:10px 0 2px}
-  .duo{display:grid;grid-template-columns:1fr 1fr;gap:48px}
-  .duo .sect{margin-top:52px}
-
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.1em;
-    color:var(--faint);margin-top:8px}
-
-  /* ── 底部收尾:漏斗 + 大数对话框(学模板 02)── */
-  .finale{display:grid;grid-template-columns:1.2fr 1fr;gap:48px;align-items:center}
-  .bignum{border:1px solid var(--faint);padding:34px 30px;position:relative}
-  .bignum::before{content:'';position:absolute;left:-8px;top:50%;width:14px;height:14px;
-    transform:translateY(-50%) rotate(45deg);background:var(--paper);
-    border-left:1px solid var(--faint);border-bottom:1px solid var(--faint)}
-  .bignum .v{font-family:var(--serif);font-size:60px;font-weight:900;
-    line-height:1;color:var(--ink)}
-  .bignum .v small{font-size:28px}
-  .bignum .l{font-size:12.5px;font-weight:700;line-height:1.7;margin-top:14px}
-  .bignum .d{font-size:10.5px;color:var(--muted);line-height:1.75;margin-top:6px}
-
-  .foot{margin-top:64px;padding-top:16px;border-top:2px solid var(--ink);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.14em;color:var(--muted)}
-
-  /* ── 动画(mono-tokens MOTION.css 原样)── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <div class="masthead">
-    <h1>产品八年,一页年鉴</h1>
-    <span class="tag">2019 – 2026 · LUPI EDITORIAL 深水区</span>
-  </div>
-  <p class="lede">十二个功能、四十八条请求、八年工单、一条漏斗。这一页把产品八年的四种时间
-    放在一起:<b>什么时候发布的、用户在要什么、坏在了哪里、人从哪里漏掉</b>。每张图的每个
-    单位都数得出来——一根发丝是一周,一个点是一条请求,一只气泡是一摞工单。</p>
-
-  <!-- ══ 上排:扇形发布图 | 放射汇聚 ══ -->
-  <div class="duo">
-    <div>
-      <div class="sect"><span class="n">壹</span>功能是什么时候出生的</div>
-      <div class="claim">十二个功能按发布周扇开,大点沿对角线滚落</div>
-      <!-- 图A · L1 launch fan · lupi-gallery.html "Twelve features, fanned out"
-           辐条 = 功能,起点 = 发布周,点径 = 月活。 -->
-      <div class="fig"><svg id="fan" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">LAUNCH FAN · 辐条起点 = 发布周 · 大点面积 = 月活</div>
-    </div>
-    <div>
-      <div class="sect"><span class="n">贰</span>用户在要什么</div>
-      <div class="claim">四十八条请求,被五个主题拽了过去</div>
-      <!-- 图B · L5 radial convergence · lupi-gallery.html
-           "48 requests pull toward five themes"
-           轮辋 = 一条条请求(带编号),曲线收进五个主题枢纽。 -->
-      <div class="fig"><svg id="converge" viewBox="0 0 400 330" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">RADIAL CONVERGENCE · 轮辋一点 = 一条请求 · 枢纽面积 = 主题请求数</div>
-    </div>
-  </div>
-
-  <!-- ══ 中部整行:气泡年鉴 ══ -->
-  <div class="sect"><span class="n">叁</span>坏在了哪里 —— 八年工单年鉴</div>
-  <div class="claim">工单跟着 GA 走:功能一转正,气泡就开始鼓</div>
-  <!-- 图C · bubble almanac · lupi-gallery.html "Eight years of tickets, one almanac"
-       账本发丝纸 + 手绘抖动气泡 + 旁注 + 底部大事记。860 宽整行放。 -->
-  <div class="fig"><svg id="almanac" viewBox="0 0 860 390" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">BUBBLE ALMANAC · 气泡面积 = 当年工单 · 色深 = 票量五档 · 琥珀 = 三大峰值 · 虚线圈 = beta 期</div>
-
-  <!-- ══ 底部:沙漏漏斗 + 大数 ══ -->
-  <div class="duo" style="margin-top:0">
-    <div>
-      <div class="sect"><span class="n">肆</span>人从哪里漏掉</div>
-      <div class="claim">条码是人,细线是流失——漏斗不是画出来的,是漏出来的</div>
-      <!-- 图D · L9 hourglass stream · lupi-gallery.html "The funnel, poured"
-           每层条带 = barcode(一根 ≈ 40 人),层间细线涓流承接,
-           左缘停靠层间转化率。 -->
-      <div class="fig"><svg id="hourglass" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">HOURGLASS STREAM · 一根 ≈ 40 人 · 越往下色越深 · 左缘 = 层间转化率</div>
-    </div>
-    <div style="align-self:center">
-      <div class="bignum">
-        <div class="v">7.4<small>%</small></div>
-        <div class="l">访客走到付费,全程只剩 7.4%</div>
-        <div class="d">4,200 位访客 → 310 位付费。最大的一次跳水在「激活」层:
-          注册的人有一半没做完第一个项目。年鉴里 2023 的工单峰、
-          汇聚图里的 PERF 主题,指向的是同一个问题。</div>
-      </div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>产品八年年鉴 · 一页纸</span>
-    <span>数据 · 内部工单系统 + 产品分析 · LIEFLAT CHARTS</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(色板 / 几何 / 工具函数与正本一致)── */
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)';
-const QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#58402E',HERO='#D4A017',FAINTDATA='#ACAD79';
-const SER=['#43593B','#D4A017','#77835A','#F2D17E','#ACAD79','#58402E'];   // 分类色:一簇一色
-const RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B'];            // 5 档序数梯(浅→重)
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ════ 图A · L1 launch fan ════
-   结构照搬 lupi-gallery "launch fan":辐条按发布周排序,
-   大点(月活)沿对角弧滚落,周环虚线作导轨。 */
-(()=>{
-const FEAT=['编辑器','看板','文档','聊天','流程','保险库','页面','同步','表格','视图','中枢','表单'];
-const LAUNCH=[1,3,4,6,8,9,11,12,14,16,17,19];     // 发布周
-const CX=40,CY=314,rOf=w=>56+w*9.7;
-obsReveal('fan',s=>{
-  const ang=i=>-84+i*(76/11);
-  // 周环虚线导轨
-  [5,10,15,20].forEach((w,gi)=>{
-    const r=rOf(w),[x1,y1]=pol(CX,CY,r,-84),[x2,y2]=pol(CX,CY,r,-8);
-    el(s,'path',{d:`M${x1} ${y1} A${r} ${r} 0 0 1 ${x2} ${y2}`,fill:'none',
-      stroke:QUIET,'stroke-width':1,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${gi*.1}s`});
-    const [lx,ly]=pol(CX,CY,r,-87.5);
-    txt(s,{x:lx,y:ly,'font-size':6.5,fill:FAINT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${gi*.1}s`},'第'+w+'周');
-  });
-  FEAT.forEach((name,i)=>{
-    const a=ang(i),L=LAUNCH[i];
-    const col=RAMP[4-Math.min(4,Math.floor((L-1)/4))];   // 上线越早 = 越重
-    const mau=Math.round(48-i*3+rnd(i+1,3)*10);
-    // 发丝辐条:从发布点到轮辋
-    const [sx,sy]=pol(CX,CY,rOf(L),a),[ex,ey]=pol(CX,CY,rOf(20),a);
-    el(s,'line',{x1:sx,y1:sy,x2:ex,y2:ey,stroke:QUIET,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`});
-    // 周纹理点
-    for(let w=L+1;w<=20;w++){
-      const [x,y]=pol(CX,CY,rOf(w),a);
-      el(s,'circle',{cx:x,cy:y,r:1.1+rnd(i+1,w)*1.2,fill:col,opacity:.55,
-        class:'pop',style:`animation-delay:${.2+i*.06+(w-L)*.015}s`});
-    }
-    // 发布点:面积 = 月活
-    const [bx,by]=pol(CX,CY,rOf(L),a);
-    const big=el(s,'circle',{cx:bx,cy:by,r:Math.sqrt(mau)*1.15,fill:col,
-      class:'pop',style:`animation-delay:${.15+i*.07}s`});
-    tip(big,`${name} —— 第 ${L} 周发布 · 月活 ${mau}k`);
-    // 轮辋标签沿辐条走
-    const [tx,ty]=pol(CX,CY,rOf(20)+9,a);
-    txt(s,{x:tx,y:ty,'font-size':7.5,'font-weight':600,fill:LAB,
-      transform:`rotate(${a} ${tx} ${ty})`,'dominant-baseline':'middle',
-      class:'fade',style:`animation-delay:${.3+i*.05}s`},name);
-  });
-});
-})();
-
-/* ════ 图B · L5 radial convergence ════
-   结构照搬 lupi-gallery "radial convergence":48 条请求在轮辋
-   带编号站好,贝塞尔曲线被五个主题枢纽拽进去,枢纽面积 = 请求数,
-   少量请求"漏"到别的主题(organic leakage)。 */
-(()=>{
-const N=48,CX=200,CY=166,R=116;
-const THEME=[['性能',15,-75],['集成',10,-3],['定价',9,69],['移动端',8,141],['体验',6,213]];
-obsReveal('converge',s=>{
-  const blocks=THEME.flatMap(([,n],h)=>Array(n).fill(h));
-  const hubOf=i=>rnd(i+1,11)>.92?(blocks[i]+2)%5:blocks[i];
-  const hubPt=h=>pol(CX,CY,34,THEME[h][2]);
-  const counts=[0,0,0,0,0];
-  for(let i=0;i<N;i++)counts[hubOf(i)]++;
-  for(let i=0;i<N;i++){
-    const deg=i/N*360-90,[px,py]=pol(CX,CY,R,deg);
-    const h=hubOf(i),[hx,hy]=hubPt(h);
-    const c1x=CX+(px-CX)*.42,c1y=CY+(py-CY)*.42;
-    const c2x=CX+(hx-CX)*.3,c2y=CY+(hy-CY)*.3;
-    el(s,'path',{d:`M${px} ${py} C${c1x} ${c1y} ${c2x} ${c2y} ${hx} ${hy}`,
-      fill:'none',stroke:SER[h],'stroke-width':.9,opacity:.62,pathLength:1,
-      class:'draw',style:`animation-delay:${i*.018}s`});
-    el(s,'circle',{cx:px,cy:py,r:1.6,fill:SER[h],
-      class:'pop',style:`animation-delay:${i*.018}s`});
-    // 轮辋小编号,左半翻转保持可读
-    const flip=deg>90&&deg<270,[lx,ly]=pol(CX,CY,R+7,deg);
-    txt(s,{x:lx,y:ly,'font-size':5.5,fill:MUT,
-      'text-anchor':flip?'end':'start','dominant-baseline':'middle',
-      transform:`rotate(${flip?deg+180:deg} ${lx} ${ly})`,
-      class:'fade',style:`animation-delay:${.2+i*.01}s`},'R-'+String(i+1).padStart(2,'0'));
-  }
-  THEME.forEach(([name,,deg],h)=>{
-    const [hx,hy]=hubPt(h);
-    const hub=el(s,'circle',{cx:hx,cy:hy,r:Math.sqrt(counts[h])*1.55,
-      fill:counts[h]===Math.max(...counts)?HERO:SER[h],
-      class:'pop',style:`animation-delay:${.5+h*.08}s`});
-    tip(hub,`${name} —— ${counts[h]} 条请求`);
-    const [tx,ty]=pol(CX,CY,R+34,deg);
-    txt(s,{x:tx,y:ty,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'dominant-baseline':'middle','letter-spacing':'.06em',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.6+h*.08}s`,
-      class:'fade'},`${name} · ${counts[h]}`);
-    const [gx,gy]=pol(CX,CY,R+22,deg);
-    el(s,'line',{x1:hx,y1:hy,x2:gx,y2:gy,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.7+h*.08}s`});
-  });
-});
-})();
-
-/* ════ 图C · bubble almanac ════
-   结构照搬 lupi-gallery "bubble almanac":账本发丝纸垫底、
-   手绘抖动气泡(blob,两个慢波 + 噪声)、beta 虚线圈、
-   升级件墨核、斜排列头、旁注 + 底部大事记。860 宽整行。 */
-(()=>{
-const AREAS=['编辑器','看板','文档','聊天','流程','保险库','页面','同步','表格','视图','中枢','表单'];
-const BETA={'表格':2023,'视图':2024,'中枢':2025,'表单':2025};   // beta 到这一年
-const BORN={'编辑器':2019,'看板':2019,'文档':2019,'聊天':2020,'流程':2020,'保险库':2021,'页面':2021,'同步':2022,'表格':2022,'视图':2023,'中枢':2024,'表单':2024};
-obsReveal('almanac',s=>{
-  const rowY=i=>62+i*34,colX=j=>132+j*57;
-  // 账本纸:6.8px 一根发丝,边到边,垫在一切下面
-  for(let y=40;y<=318;y+=6.8)
-    el(s,'line',{x1:56,y1:y,x2:822,y2:y,stroke:QUIET,'stroke-width':.5,
-      class:'fade',style:`animation-delay:${(y-40)*.001}s`});
-  const vals=[];
-  for(let i=0;i<8;i++){
-    const yr=2019+i;
-    el(s,'line',{x1:56,y1:rowY(i),x2:822,y2:rowY(i),stroke:GRID,'stroke-width':.9,
-      class:'fade',style:`animation-delay:${i*.04}s`});
-    txt(s,{x:48,y:rowY(i)+3,'font-size':9,'font-weight':700,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.04}s`},yr);
-    AREAS.forEach((a,j)=>{
-      if(yr<BORN[a])return;
-      const age=yr-BORN[a];
-      const v=Math.round((2+age*5)*(0.25+rnd(i*12+j+1,j+3)**2*2.2));
-      vals.push([Math.max(1,v),i,j,a,yr]);
-    });
-  }
-  vals.sort((a,b)=>b[0]-a[0]);
-  const top3=vals.slice(0,3);
-  // 手绘圆:圆周叠两个慢波 + 噪声,seed 定形——没有一只是圆规圆
-  const blob=(x,y,r,seed)=>{
-    const n=Math.max(14,Math.round(r*1.6)),pts=[];
-    for(let t=0;t<n;t++){
-      const a=t/n*Math.PI*2;
-      const w=1+.055*Math.sin(a*2+seed*7)+.04*Math.sin(a*3+seed*13)+(rnd(seed+t,3)-.5)*.03;
-      pts.push([x+Math.cos(a)*r*w,y+Math.sin(a)*r*w]);
-    }
-    let d=`M${pts[0][0].toFixed(1)} ${pts[0][1].toFixed(1)}`;
-    for(let t=0;t<n;t++){
-      const p=pts[t],q=pts[(t+1)%n];
-      d+=` Q${p[0].toFixed(1)} ${p[1].toFixed(1)} ${((p[0]+q[0])/2).toFixed(1)} ${((p[1]+q[1])/2).toFixed(1)}`;
-    }
-    return d+' Z';
-  };
-  vals.forEach(([v,i,j,a,yr],k)=>{
-    const x=colX(j)+(rnd(i*7+j+2,j+9)-.5)*10,
-          y=rowY(i),r=Math.sqrt(v)*3.8;
-    const beta=BETA[a]&&yr<=BETA[a],isTop=top3.includes(vals[k]);
-    const col=RAMP[Math.min(4,Math.floor(v/12))];        // 票量 5 档
-    const g=el(s,'g',{class:'pop',style:`animation-delay:${.15+k*.012}s`});
-    const seed=i*12+j;
-    const dot=el(g,'path',beta
-      ?{d:blob(x,y,r,seed),fill:'none',stroke:DATA2,'stroke-width':1.2,'stroke-dasharray':'3 3'}
-      :{d:blob(x,y,r,seed),fill:col,'fill-opacity':.16+rnd(i+2,j+4)*.14,
-        stroke:isTop?HERO:col,'stroke-opacity':isTop?.95:.5+rnd(i+5,j+7)*.38,'stroke-width':.6+rnd(i+3,j+11)*.9});
-    tip(dot,`${a} · ${yr} 年 —— ${v*10} 张工单${beta?'(beta 期)':''}`);
-    // 升级件墨核:小抖动墨块,压偏一点,像印刷压痕
-    if(!beta){
-      const ox=(rnd(seed+1,17)-.5)*r*.3,oy=(rnd(seed+3,19)-.5)*r*.3;
-      el(g,'path',{d:blob(x+ox,y+oy,Math.max(1.4,r*.17),seed+29),fill:isTop?HERO:TXT});
-    }
-    if(top3.includes(vals[k]))
-      txt(s,{x,y:y-r-5,'font-size':8.5,'font-weight':800,fill:HERO,'text-anchor':'middle',
-        style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:1s`,
-        class:'fade'},v*10);
-  });
-  AREAS.forEach((a,j)=>{
-    const x=colX(j);
-    txt(s,{x,y:34,'font-size':8,'font-weight':700,fill:MUT,'letter-spacing':'.06em',
-      transform:`rotate(-32 ${x} 34)`,class:'fade',style:`animation-delay:${j*.03}s`},a);
-  });
-  // 旁注:指针发丝 + 小字批注,像原版年鉴的手记。
-  // 只保留右上这条——左半图气泡太密,任何批注放进去都会被淹掉。
-  el(s,'path',{d:`M${colX(9)+16} ${rowY(2)+8} C${colX(9)+40} ${rowY(2)+26} ${colX(9)+60} ${rowY(2)+30} ${colX(9)+80} ${rowY(2)+30}`,
-    fill:'none',stroke:FAINT,'stroke-width':.7,class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:colX(9)+86,y:rowY(2)+33,'font-size':7.5,fill:LAB,
-    class:'fade',style:'animation-delay:1.2s'},'beta 几乎不产生工单——没人给玩具报 bug');
-  // 底部大事记:年鉴的时间线搁板
-  const evY=360;
-  el(s,'line',{x1:56,y1:evY-16,x2:822,y2:evY-16,stroke:FAINT,'stroke-width':.8,
-    class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:56,y:evY-24,'font-size':7.5,'font-weight':800,fill:MUT,'letter-spacing':'.14em',
-    class:'fade',style:'animation-delay:1.2s'},'大事记');
-  [['2020','引入 SLA'],['2022','自助帮助中心上线'],['2023','GA 大年'],['2025','AI 分流上线']]
-  .forEach(([yr,note],k)=>{
-    const x=90+k*196;
-    el(s,'line',{x1:x,y1:evY-16,x2:x,y2:evY-6,stroke:MUT,'stroke-width':1,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`});
-    txt(s,{x,y:evY+4,'font-size':8.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},yr);
-    txt(s,{x,y:evY+15,'font-size':7.5,fill:MUT,
-      class:'fade',style:`animation-delay:${1.3+k*.08}s`},note);
-  });
-});
-})();
-
-/* ════ 图D · L9 hourglass stream ════
-   结构照搬 lupi-gallery "hourglass stream":每层条带是 barcode
-   (一根 ≈ 40 人),层间 34 根细线涓流承接,左缘停靠转化率,
-   右侧发丝引出层名和人数。 */
-(()=>{
-const STAGE=[['访客',4200],['注册',1900],['激活',960],['留存',540],['付费',310]];
-obsReveal('hourglass',s=>{
-  const CXm=185,sy=k=>34+k*64,w=c=>c/4200*290;
-  STAGE.forEach(([name,c],k)=>{
-    const y=sy(k),hw=w(c)/2;
-    // barcode 条带:一根 ≈ 40 人
-    const n=Math.round(c/40);
-    for(let t=0;t<n;t++){
-      const x=CXm-hw+(t+.5)/n*hw*2+(rnd(t+1,k+3)-.5)*3;
-      el(s,'line',{x1:x,y1:y-6,x2:x,y2:y+6,stroke:RAMP[k],'stroke-width':1.4,
-        opacity:.7+rnd(t+2,k+5)*.3,class:'fade',style:`animation-delay:${k*.12+t*.004}s`});
-    }
-    // 涓流细线,流向下一层条带
-    if(k<4){
-      const hw1=w(STAGE[k+1][1])/2;
-      for(let t=0;t<34;t++){
-        const xt=CXm+(rnd(t+1,k*7+1)-.5)*2*hw*.94;
-        const xb=CXm+(rnd(t+3,k*7+5)-.5)*2*hw1*.94;
-        el(s,'path',{d:`M${xt} ${y+8} C${xt} ${y+34} ${xb} ${sy(k+1)-34} ${xb} ${sy(k+1)-8}`,
-          fill:'none',stroke:FAINTDATA,'stroke-width':.7,opacity:.45,pathLength:1,
-          class:'draw',style:`animation-delay:${.2+k*.15+t*.008}s;animation-duration:.8s`});
-      }
-      // 层间转化率,停靠左缘
-      const pct=Math.round(STAGE[k+1][1]/c*100);
-      txt(s,{x:26,y:(y+sy(k+1))/2+3,'font-size':8.5,'font-weight':800,fill:DATA,
-        class:'fade',style:`animation-delay:${.5+k*.15}s`},pct+'%');
-      txt(s,{x:26,y:(y+sy(k+1))/2+13,'font-size':6.5,'font-weight':600,fill:FAINT,
-        'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.5+k*.15}s`},'走到下层');
-    }
-    // 层名,发丝拴在条带边上
-    el(s,'line',{x1:CXm+hw+6,y1:y,x2:340,y2:y,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${.3+k*.12}s`});
-    txt(s,{x:344,y:y-1,'font-size':8,'font-weight':700,fill:LAB,
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${.35+k*.12}s`},name);
-    txt(s,{x:344,y:y+10,'font-size':9.5,'font-weight':800,fill:TXT,
-      class:'fade',style:`animation-delay:${.4+k*.12}s`},c.toLocaleString());
-  });
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 419
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-07.en.html

@@ -1,419 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>AI Coding Culture Survey — Lieflat Report Template · Palm Bazaar</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&family=Playfair+Display:wght@700;800;900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 07 · SURVEY COLLAGE POSTER
-     场景:文化/消费调研的高密度海报(社媒长图、展会物料、公众号头图)
-     排版参考:图6 "Japan Culture"(Pin Tuấn Dung)——英文小标 + 大衬线
-               标题叠排、右上导语色块、巨号数字与图表模块穿插的杂志拼贴、
-               实色卡上反白图表 —— 只学排版和字体,蓝粉配色不学。
-     刊头那行英文原为花体(Pinyon Script),2026-08 换等宽小写:
-     vibe coding 是代码圈的黑话,用代码的字写它;和底下厚重衬线中文
-     形成「终端 vs 印刷」的对比,比花体诚实。花体属于请柬语域,词不对。
-     色板正本 = color-presets.js 的 PALM 一套(色相=类目,琥珀=重点)。
-     图表正本 = lieflat gallery:
-       图A  F5  tick rows      · basics-gallery.html(常用工具)
-       图B  L3  barcode lollipop 竖排变体 · lupi-gallery.html(深绿卡反白)
-       图C  L14 hundred field  · lupi-gallery.html(兴趣触发轨道簇)
-       图D  F1  rung bars      · basics-gallery.html(信息来源)
-       图E  F5  tick rows 迷你 · basics-gallery.html(文化兴趣对比)
-     马赛克块(模仿原图 treemap)是排字 callout,面积只是示意,读数字。
-     彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.40); --quiet:rgba(88,64,46,.15); --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#77835A; --hero:#D4A017; --faintdata:#ACAD79; --wheat:#F2D17E;
-    --serif:'Playfair Display',Georgia,serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:56px 24px}
-  .sheet{width:980px;max-width:980px}
-
-  /* ── 刊头:等宽小标 + 大衬线叠排(学图6 左上)│ 导语色块(右上)── */
-  .head{display:grid;grid-template-columns:1.1fr 1fr;gap:36px;align-items:start}
-  .title .script{font-family:'JetBrains Mono',monospace;font-size:34px;font-weight:400;
-    line-height:1.1;letter-spacing:.02em;color:var(--data2)}
-  .title h1{font-family:var(--serif);font-size:56px;font-weight:900;line-height:1.05;
-    letter-spacing:-.03em;margin-top:6px}
-  .title h1 b{color:var(--data)}
-  .title .sub{font-size:9.5px;font-weight:700;letter-spacing:.24em;text-transform:uppercase;
-    color:var(--mut);margin-top:10px}
-  .introbox{background:var(--data);color:var(--bg);padding:22px 24px;font-size:11px;
-    line-height:1.75}
-  .introbox b{color:var(--wheat)}
-
-  /* ── 星点分隔行(学图6 的装饰行,用字符不用插画)── */
-  .stars{display:flex;justify-content:space-between;margin:26px 0 30px;color:var(--faint);
-    font-size:9px;letter-spacing:.4em;overflow:hidden;white-space:nowrap}
-
-  /* ── 拼贴网格 ── */
-  .collage{display:grid;grid-template-columns:repeat(12,1fr);gap:30px 34px}
-  .m-a{grid-column:1/6}
-  .m-b{grid-column:6/13}
-  .m-c{grid-column:1/5;align-self:end}
-  .m-d{grid-column:5/13}
-  .m-e{grid-column:1/8}
-  .m-f{grid-column:8/13}
-  .m-g{grid-column:1/6;align-self:end}
-  .m-h{grid-column:6/13}
-
-  .mod h3{font-size:11.5px;font-weight:900;letter-spacing:.11em;text-transform:uppercase;
-    margin-bottom:2px}
-  .mod .frame{border:1px solid var(--txt);display:inline-block;padding:4px 12px;
-    font-size:9px;font-weight:700;letter-spacing:.11em;text-transform:uppercase;margin-bottom:8px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-  .srcline{font-size:8.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;
-    color:var(--faint);margin-top:6px}
-
-  /* ── 深绿卡:反白图表(学图6 蓝卡)── */
-  .card{background:var(--data);padding:22px 24px 16px}
-  .card h3{color:var(--bg)}
-  .card .srcline{color:rgba(240,239,235,.45)}
-
-  /* ── 巨号数字(学图6 的 129 / 218 / 69%)── */
-  .bignum .n{font-family:var(--serif);font-size:96px;font-weight:900;line-height:.9;
-    color:var(--hero);letter-spacing:-.02em}
-  .bignum .n small{font-size:44px}
-  .bignum .cap{font-size:11px;font-weight:700;line-height:1.5;margin-top:10px;max-width:240px}
-  .bignum .cap b{color:var(--data)}
-
-  /* ── 马赛克 callout(模仿 treemap,面积示意、读数字)── */
-  .mosaic{display:grid;grid-template-columns:repeat(6,1fr);grid-auto-rows:64px;gap:6px}
-  .mosaic .cell{color:var(--bg);padding:8px 10px;display:flex;flex-direction:column;
-    justify-content:space-between}
-  .mosaic .cell .v{font-size:19px;font-weight:800}
-  .mosaic .cell .t{font-size:8px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;
-    opacity:.85}
-  .mosaic .c1{grid-column:span 3;grid-row:span 2;background:var(--data)}
-  .mosaic .c2{grid-column:span 3;background:var(--data2)}
-  .mosaic .c3{grid-column:span 2;background:var(--faintdata);color:var(--txt)}
-  .mosaic .c4{grid-column:span 1;background:var(--hero)}
-  .mosaic .c5{grid-column:span 2;background:var(--data2)}
-  .mosaic .c6{grid-column:span 2;background:var(--faintdata);color:var(--txt)}
-  .mosaic .c7{grid-column:span 2;background:var(--data)}
-
-  .foot{margin-top:52px;border-top:2px solid var(--txt);padding-top:12px;
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--mut)}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 刊头 ══ -->
-  <div class="head">
-    <div class="title">
-      <div class="script">vibe coding</div>
-      <h1>AI Coding <b>Culture</b> Survey</h1>
-      <div class="sub">Independent developers in China · July 2026</div>
-    </div>
-    <div class="introbox">
-      In July 2026 we surveyed <b>369 independent developers</b> in China who use AI coding
-      tools. We wanted three things: where they use AI, what got them started, and where
-      they learn the craft. Respondents were 18–39, and all data is self-reported and
-      anonymous. <b>The numbers on this page are demo data.</b>
-    </div>
-  </div>
-
-  <div class="stars">✦ ✧ ✦&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · ·&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;✦ ✧ ✦</div>
-
-  <!-- ══ 拼贴 ══ -->
-  <div class="collage">
-
-    <!-- 模块A · tick rows:最常用工具(学图6 左上城市条)-->
-    <div class="mod m-a">
-      <span class="frame">Most-used AI coding tools (respondents)</span>
-      <div class="fig"><svg id="tools" viewBox="0 0 400 260" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Tick rows · one tick = 4 people · dot marks every fifth</div>
-    </div>
-
-    <!-- 模块B · 深绿卡竖 lollipop:想拿 AI 做的事(学图6 蓝卡 THINGS TO DO)-->
-    <div class="mod m-b card">
-      <h3>12 things people most want AI to take over</h3>
-      <div class="fig"><svg id="todo" viewBox="0 0 480 250" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Lollipop · one stem = one task · height = mentions · wheat = top pick</div>
-    </div>
-
-    <!-- 模块C · 巨号数字(学图6 的 129)-->
-    <div class="mod m-c bignum">
-      <div class="n">129</div>
-      <div class="cap">respondents say <b>"write my tests"</b> is their top reason to pay, ahead of any autocomplete.</div>
-    </div>
-
-    <!-- 模块D · hundred field:入坑触发点(学图6 轨道气泡)-->
-    <div class="mod m-d">
-      <span class="frame">What got you started with AI coding (respondents · sample of 100)</span>
-      <div class="fig"><svg id="orbit" viewBox="0 0 560 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Hundred field · one dot = one person · 41 + 27 + 19 + 13 = 100</div>
-    </div>
-
-    <!-- 模块E · rung bars:信息来源(学图6 左下高柱)-->
-    <div class="mod m-e">
-      <span class="frame">Where people learned the craft (respondents)</span>
-      <div class="fig"><svg id="sources" viewBox="0 0 520 270" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Rung bars · one rung = 8 people · amber = top source</div>
-    </div>
-
-    <!-- 模块F · 巨号 % + 迷你 tick rows(学图6 的 69% 模块)-->
-    <div class="mod m-f">
-      <div class="bignum" style="margin-bottom:14px">
-        <div class="n">69<small>%</small></div>
-        <div class="cap">of respondents say <b>agent workflows</b> are the direction most worth betting on this year.</div>
-      </div>
-      <div class="fig"><svg id="interest" viewBox="0 0 400 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Tick rows · one tick = 4 people</div>
-    </div>
-
-    <!-- 模块G · 巨号数字(学图6 的 218)-->
-    <div class="mod m-g bignum">
-      <div class="n">218</div>
-      <div class="cap">people's first AI project was <b>a tool built just for themselves</b>, not a work task.</div>
-    </div>
-
-    <!-- 模块H · 马赛克 callout(模仿图6 treemap,面积示意、读数字)-->
-    <div class="mod m-h">
-      <span class="frame">First tasks handed to AI (respondents · multi-select)</span>
-      <div class="mosaic">
-        <div class="cell c1"><span class="v">218</span><span class="t">Writing / fixing tests</span></div>
-        <div class="cell c2"><span class="v">181</span><span class="t">Reading other people's old code</span></div>
-        <div class="cell c3"><span class="v">155</span><span class="t">Scripts and automation</span></div>
-        <div class="cell c4"><span class="v">114</span><span class="t">Projects</span></div>
-        <div class="cell c5"><span class="v">110</span><span class="t">Docs / API lookups</span></div>
-        <div class="cell c6"><span class="v">92</span><span class="t">Refactoring</span></div>
-        <div class="cell c7"><span class="v">88</span><span class="t">Commits & PR descriptions</span></div>
-      </div>
-      <div class="srcline">Mosaic · area is illustrative, read the numbers · multi-select, totals can exceed 100%</div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>AI Coding Culture Survey · Collage One-Pager</span>
-    <span>N = 369 · Demo data · Lieflat Charts</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PALM preset(color-presets.js 原值)── */
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',
-      FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#77835A',HERO='#D4A017',FAINTDATA='#ACAD79',WHEAT='#F2D17E';
-const CAT4=['#43593B','#77835A','#ACAD79','#F2D17E'];
-
-/* ════ 模块A · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows":1 tick = 4 人。
-   琥珀只给榜首行。加墨:线宽 1.6,地板 .85。 */
-(()=>{
-const D=[['CLAUDE CODE',52],['CURSOR',44],['COPILOT',31],['CODEX',22],['WINDSURF',14],['OTHER',9]];
-obsReveal('tools',s=>{
-  const y0=i=>34+i*38,X0=104,PX=4.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+8,x2:X0+55*PX,y2:y+8,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=8+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+8,x2:x,y2:y+8-h,stroke:hero?HERO:DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+12,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+9,y:y+3,'font-size':10.5,'font-weight':800,
-      fill:hero?HERO:DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},v*4);
-    tip(lab,`${name} — ${v*4} people use it regularly`);
-  });
-  txt(s,{x:200,y:252,'font-size':6.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = 4 PEOPLE · DOT MARKS EVERY FIFTH');
-});
-})();
-
-/* ════ 模块B · 深绿卡竖 lollipop ════
-   结构 = lupi-gallery "barcode lollipop" 的稀疏竖排版:一杆一件事,
-   杆高 = 提及人数。深卡上纸色反白,榜首换麦黄(PALM.HERODK 规则:
-   深底上琥珀读不出,强调走麦黄)。 */
-(()=>{
-const D=[['Writing tests',101],['Reading old code',76],['Automation scripts',66],['Scaffolding projects',63],['Looking up docs',62],
-         ['Refactoring',58],['Tweaking CSS',56],['PR descriptions',53],['SQL',49],['Regex',46],['Scrapers',44],['Naming things',28]];
-obsReveal('todo',s=>{
-  const X0=34,PX=37,base=196,map=v=>base-v*1.45;
-  el(s,'line',{x1:X0-12,y1:base,x2:X0+11*PX+14,y2:base,stroke:'rgba(240,239,235,.25)',
-    'stroke-width':.8,class:'fade'});
-  D.forEach(([name,v],i)=>{
-    const x=X0+i*PX,y=map(v),hero=i===0;
-    const col=hero?WHEAT:'#F0EFE9';
-    el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:col,'stroke-width':hero?1.6:1,
-      opacity:hero?1:.8,class:'fade',style:`animation-delay:${.1+i*.05}s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:hero?5:3.4,fill:col,
-      class:'pop',style:`animation-delay:${.2+i*.05}s`});
-    tip(dot,`${name} — mentioned by ${v} people`);
-    txt(s,{x,y:y-9,'font-size':9,'font-weight':800,fill:col,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.05}s`},v);
-    txt(s,{x:x+3,y:base+8,'font-size':7,'font-weight':600,fill:'rgba(240,239,235,.75)',
-      'text-anchor':'end',transform:`rotate(-52 ${x+3} ${base+8})`,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`},name);
-  });
-  txt(s,{x:240,y:242,'font-size':6.5,'font-weight':600,fill:'rgba(240,239,235,.5)',
-    'text-anchor':'middle','letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE STEM = ONE TASK · HEIGHT = MENTIONS');
-});
-})();
-
-/* ════ 模块D · L14 hundred field(轨道版)════
-   结构照搬 lupi-gallery "hundred field":golden-angle 撒布 1 点 = 1 人,
-   四簇横排成轨道带(学图6 的轨道气泡阵),虚线连簇心。
-   PALM 色相=类目:CAT4 一簇一色。spoke 加墨 ×1.8。 */
-(()=>{
-const SEG=[['Colleague recommended',41,CAT4[0]],['Saw it on social',27,CAT4[1]],['Podcast / video',19,CAT4[2]],['Company required it',13,CAT4[3]]];
-const POS=[[96,150],[250,120],[386,170],[496,130]];
-obsReveal('orbit',s=>{
-  [[0,1],[1,2],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:GRID,'stroke-width':.8,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.6+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1.1,
-        opacity:.85,class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,opacity:.9,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} — ${v} of every 100 people`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:TXT,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+14,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:280,y:292,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
-    'ONE DOT = ONE PERSON · 41 + 27 + 19 + 13 = 100');
-});
-})();
-
-/* ════ 模块E · F1 rung bars ════
-   结构照搬 basics-gallery "rung bars":1 档 = 8 人,
-   琥珀只给最大来源。 */
-(()=>{
-const D=[['Video tutorials',20],['X / Twitter',20],['Tech podcasts',17],['Community groups',17],['Official docs',16],
-         ['Friend demos',13],['Newsletters',13],['Meetups',10],['Other',6]];
-obsReveal('sources',s=>{
-  const x0=i=>44+i*54,base=216,step=6.4,HW=15;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i),hero=i===0;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:hero?HERO:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.07+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.07+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-9,'font-size':10.5,'font-weight':800,
-      fill:hero?HERO:DATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.07}s`},v*8);
-    tip(num,`${name} — ${v*8} people`);
-    txt(s,{x:x+3,y:base+12,'font-size':7,'font-weight':700,fill:MUT,'text-anchor':'end',
-      transform:`rotate(-46 ${x+3} ${base+12})`,
-      class:'fade',style:`animation-delay:${i*.07}s`},name);
-  });
-  el(s,'line',{x1:20,y1:base+4,x2:500,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:260,y:264,'font-size':6.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = 8 PEOPLE · DOT MARKS EVERY FIFTH');
-});
-})();
-
-/* ════ 模块F · F5 tick rows 迷你 ════
-   最看好的方向对比。琥珀只给榜首。 */
-(()=>{
-const D=[['AGENT WORKFLOWS',64],['CODE REVIEW',43],['UI GENERATION',36],['DATA ANALYSIS',28],['WRITING HELP',19]];
-obsReveal('interest',s=>{
-  const y0=i=>26+i*32,X0=104,PX=3.6;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:96,y:y+3,'font-size':7,'font-weight':700,fill:MUT,'text-anchor':'end',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+7,x2:X0+70*PX,y2:y+7,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=7+rnd(k+1,i+2)*4;
-      el(s,'line',{x1:x,y1:y+7,x2:x,y2:y+7-h,stroke:hero?HERO:DATA,'stroke-width':1.4,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+10.5,r:.8,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,
-      fill:hero?HERO:DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},v*4);
-    tip(lab,`${name} — favored by ${v*4} people`);
-  });
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 415
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-07.zh.html

@@ -1,415 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>AI 编程文化调研 — Lieflat 报告模板 · 椰林集市</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&family=Noto+Serif+SC:wght@600;700;900&family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 07 · 调研拼贴海报(椰林集市)
-     场景:文化/消费调研的高密度海报(社媒长图、展会物料、公众号头图)
-     排版参考:图6 "Japan Culture"(Pin Tuấn Dung)——英文小标 + 大衬线
-               标题叠排、右上导语色块、巨号数字与图表模块穿插的杂志拼贴、
-               实色卡上反白图表 —— 只学排版和字体,蓝粉配色不学。
-     刊头那行英文原为花体(Pinyon Script),2026-08 换等宽小写:
-     vibe coding 是代码圈的黑话,用代码的字写它;和底下厚重衬线中文
-     形成「终端 vs 印刷」的对比,比花体诚实。花体属于请柬语域,词不对。
-     色板正本 = color-presets.js 的 PALM 一套(色相=类目,琥珀=重点)。
-     图表正本 = lieflat gallery:
-       图A  F5  tick rows      · basics-gallery.html(常用工具)
-       图B  L3  barcode lollipop 竖排变体 · lupi-gallery.html(深绿卡反白)
-       图C  L14 hundred field  · lupi-gallery.html(兴趣触发轨道簇)
-       图D  F1  rung bars      · basics-gallery.html(信息来源)
-       图E  F5  tick rows 迷你 · basics-gallery.html(文化兴趣对比)
-     马赛克块(模仿原图 treemap)是排字 callout,面积只是示意,读数字。
-     彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.40); --quiet:rgba(88,64,46,.15); --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#77835A; --hero:#D4A017; --faintdata:#ACAD79; --wheat:#F2D17E;
-    --serif:'Noto Serif SC','Songti SC',serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter','Noto Sans SC',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:56px 24px}
-  .sheet{width:980px;max-width:980px}
-
-  /* ── 刊头:等宽小标 + 大衬线叠排(学图6 左上)│ 导语色块(右上)── */
-  .head{display:grid;grid-template-columns:1.1fr 1fr;gap:36px;align-items:start}
-  .title .script{font-family:'JetBrains Mono',monospace;font-size:34px;font-weight:400;
-    line-height:1.1;letter-spacing:.02em;color:var(--data2)}
-  .title h1{font-family:var(--serif);font-size:56px;font-weight:900;line-height:1.08;
-    letter-spacing:.02em;margin-top:6px}
-  .title h1 b{color:var(--data)}
-  .title .sub{font-size:10px;font-weight:700;letter-spacing:.3em;color:var(--mut);margin-top:10px}
-  .introbox{background:var(--data);color:var(--bg);padding:22px 24px;font-size:11px;
-    line-height:1.9;letter-spacing:.02em}
-  .introbox b{color:var(--wheat)}
-
-  /* ── 星点分隔行(学图6 的装饰行,用字符不用插画)── */
-  .stars{display:flex;justify-content:space-between;margin:26px 0 30px;color:var(--faint);
-    font-size:9px;letter-spacing:.4em;overflow:hidden;white-space:nowrap}
-
-  /* ── 拼贴网格 ── */
-  .collage{display:grid;grid-template-columns:repeat(12,1fr);gap:30px 34px}
-  .m-a{grid-column:1/6}
-  .m-b{grid-column:6/13}
-  .m-c{grid-column:1/5;align-self:end}
-  .m-d{grid-column:5/13}
-  .m-e{grid-column:1/8}
-  .m-f{grid-column:8/13}
-  .m-g{grid-column:1/6;align-self:end}
-  .m-h{grid-column:6/13}
-
-  .mod h3{font-size:12px;font-weight:900;letter-spacing:.14em;margin-bottom:2px}
-  .mod .frame{border:1px solid var(--txt);display:inline-block;padding:4px 12px;
-    font-size:9.5px;font-weight:700;letter-spacing:.14em;margin-bottom:8px}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .srcline{font-size:8.5px;font-weight:600;letter-spacing:.1em;color:var(--faint);margin-top:6px}
-
-  /* ── 深绿卡:反白图表(学图6 蓝卡)── */
-  .card{background:var(--data);padding:22px 24px 16px}
-  .card h3{color:var(--bg)}
-  .card .srcline{color:rgba(240,239,235,.45)}
-
-  /* ── 巨号数字(学图6 的 129 / 218 / 69%)── */
-  .bignum .n{font-family:var(--serif);font-size:96px;font-weight:900;line-height:.9;
-    color:var(--hero);letter-spacing:-.02em}
-  .bignum .n small{font-size:44px}
-  .bignum .cap{font-size:11px;font-weight:700;line-height:1.75;margin-top:10px;max-width:240px}
-  .bignum .cap b{color:var(--data)}
-
-  /* ── 马赛克 callout(模仿 treemap,面积示意、读数字)── */
-  .mosaic{display:grid;grid-template-columns:repeat(6,1fr);grid-auto-rows:64px;gap:6px}
-  .mosaic .cell{color:var(--bg);padding:8px 10px;display:flex;flex-direction:column;
-    justify-content:space-between}
-  .mosaic .cell .v{font-size:19px;font-weight:800}
-  .mosaic .cell .t{font-size:8.5px;font-weight:700;letter-spacing:.05em;opacity:.85}
-  .mosaic .c1{grid-column:span 3;grid-row:span 2;background:var(--data)}
-  .mosaic .c2{grid-column:span 3;background:var(--data2)}
-  .mosaic .c3{grid-column:span 2;background:var(--faintdata);color:var(--txt)}
-  .mosaic .c4{grid-column:span 1;background:var(--hero)}
-  .mosaic .c5{grid-column:span 2;background:var(--data2)}
-  .mosaic .c6{grid-column:span 2;background:var(--faintdata);color:var(--txt)}
-  .mosaic .c7{grid-column:span 2;background:var(--data)}
-
-  .foot{margin-top:52px;border-top:2px solid var(--txt);padding-top:12px;
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.14em;color:var(--mut)}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 刊头 ══ -->
-  <div class="head">
-    <div class="title">
-      <div class="script">vibe coding</div>
-      <h1>AI 编程<b>文化</b>调研</h1>
-      <div class="sub">中国独立开发者 · 2026 年 7 月</div>
-    </div>
-    <div class="introbox">
-      2026 年 7 月,我们向 <b>369 位</b>使用 AI 编程工具的中国独立开发者发出问卷,
-      想看清三件事:他们把 AI 用在哪里、是什么让他们开始的、以及他们从哪里获取
-      这门新手艺的信息。受访者年龄 18–39 岁,全部数据为匿名自报。
-      <b>本页数字为演示数据。</b>
-    </div>
-  </div>
-
-  <div class="stars">✦ ✧ ✦&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · ·&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;✦ ✧ ✦</div>
-
-  <!-- ══ 拼贴 ══ -->
-  <div class="collage">
-
-    <!-- 模块A · tick rows:最常用工具(学图6 左上城市条)-->
-    <div class="mod m-a">
-      <span class="frame">最常用的 AI 编程工具(人)</span>
-      <div class="fig"><svg id="tools" viewBox="0 0 400 260" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">TICK ROWS · 一格 = 4 人 · 每五格一个圆点</div>
-    </div>
-
-    <!-- 模块B · 深绿卡竖 lollipop:想拿 AI 做的事(学图6 蓝卡 THINGS TO DO)-->
-    <div class="mod m-b card">
-      <h3>最想让 AI 接手的 12 件事</h3>
-      <div class="fig"><svg id="todo" viewBox="0 0 480 250" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">LOLLIPOP · 一杆 = 一件事 · 高度 = 提及人数 · 麦黄 = 榜首</div>
-    </div>
-
-    <!-- 模块C · 巨号数字(学图6 的 129)-->
-    <div class="mod m-c bignum">
-      <div class="n">129</div>
-      <div class="cap">位受访者说,<b>「帮我写测试」</b>是他们付费的第一理由——超过任何一种代码补全。</div>
-    </div>
-
-    <!-- 模块D · hundred field:入坑触发点(学图6 轨道气泡)-->
-    <div class="mod m-d">
-      <span class="frame">是什么让你开始用 AI 写代码(人 · 100 人抽样)</span>
-      <div class="fig"><svg id="orbit" viewBox="0 0 560 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">HUNDRED FIELD · 一点 = 一人 · 41 + 27 + 19 + 13 = 100</div>
-    </div>
-
-    <!-- 模块E · rung bars:信息来源(学图6 左下高柱)-->
-    <div class="mod m-e">
-      <span class="frame">从哪里学这门手艺(人)</span>
-      <div class="fig"><svg id="sources" viewBox="0 0 520 270" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">RUNG BARS · 一档 = 8 人 · 琥珀 = 最大来源</div>
-    </div>
-
-    <!-- 模块F · 巨号 % + 迷你 tick rows(学图6 的 69% 模块)-->
-    <div class="mod m-f">
-      <div class="bignum" style="margin-bottom:14px">
-        <div class="n">69<small>%</small></div>
-        <div class="cap">的受访者认为 <b>agent 工作流</b>是未来一年最值得投入的方向。</div>
-      </div>
-      <div class="fig"><svg id="interest" viewBox="0 0 400 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">TICK ROWS · 一格 = 4 人</div>
-    </div>
-
-    <!-- 模块G · 巨号数字(学图6 的 218)-->
-    <div class="mod m-g bignum">
-      <div class="n">218</div>
-      <div class="cap">人的第一个 AI 项目是<b>给自己做的小工具</b>——不是工作任务。</div>
-    </div>
-
-    <!-- 模块H · 马赛克 callout(模仿图6 treemap,面积示意、读数字)-->
-    <div class="mod m-h">
-      <span class="frame">最先交给 AI 的任务(人 · 可多选)</span>
-      <div class="mosaic">
-        <div class="cell c1"><span class="v">218</span><span class="t">写测试 / 修测试</span></div>
-        <div class="cell c2"><span class="v">181</span><span class="t">读懂别人的旧代码</span></div>
-        <div class="cell c3"><span class="v">155</span><span class="t">写脚本和自动化</span></div>
-        <div class="cell c4"><span class="v">114</span><span class="t">起项目</span></div>
-        <div class="cell c5"><span class="v">110</span><span class="t">查文档 / 查 API</span></div>
-        <div class="cell c6"><span class="v">92</span><span class="t">重构</span></div>
-        <div class="cell c7"><span class="v">88</span><span class="t">写提交信息和 PR 描述</span></div>
-      </div>
-      <div class="srcline">MOSAIC · 面积为示意,请读数字 · 多选,合计可超 100%</div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>AI 编程文化调研 · 拼贴一页纸</span>
-    <span>N = 369 · 演示数据 · LIEFLAT CHARTS</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PALM preset(color-presets.js 原值)── */
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',
-      FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#77835A',HERO='#D4A017',FAINTDATA='#ACAD79',WHEAT='#F2D17E';
-const CAT4=['#43593B','#77835A','#ACAD79','#F2D17E'];
-
-/* ════ 模块A · F5 tick rows ════
-   结构照搬 basics-gallery "tick rows":1 tick = 4 人。
-   琥珀只给榜首行。加墨:线宽 1.6,地板 .85。 */
-(()=>{
-const D=[['CLAUDE CODE',52],['CURSOR',44],['COPILOT',31],['CODEX',22],['WINDSURF',14],['其他',9]];
-obsReveal('tools',s=>{
-  const y0=i=>34+i*38,X0=104,PX=4.9;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:94,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+8,x2:X0+55*PX,y2:y+8,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=8+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+8,x2:x,y2:y+8-h,stroke:hero?HERO:DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+12,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.01}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+9,y:y+3,'font-size':10.5,'font-weight':800,
-      fill:hero?HERO:DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},v*4);
-    tip(lab,`${name} —— ${v*4} 人常用`);
-  });
-  txt(s,{x:200,y:252,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一格 = 4 人 · 每五格一个圆点');
-});
-})();
-
-/* ════ 模块B · 深绿卡竖 lollipop ════
-   结构 = lupi-gallery "barcode lollipop" 的稀疏竖排版:一杆一件事,
-   杆高 = 提及人数。深卡上纸色反白,榜首换麦黄(PALM.HERODK 规则:
-   深底上琥珀读不出,强调走麦黄)。 */
-(()=>{
-const D=[['写测试',101],['读旧代码',76],['自动化脚本',66],['起项目',63],['查文档',62],
-         ['重构',58],['调 CSS',56],['PR 描述',53],['SQL',49],['正则',46],['爬虫',44],['起名',28]];
-obsReveal('todo',s=>{
-  const X0=34,PX=37,base=196,map=v=>base-v*1.45;
-  el(s,'line',{x1:X0-12,y1:base,x2:X0+11*PX+14,y2:base,stroke:'rgba(240,239,235,.25)',
-    'stroke-width':.8,class:'fade'});
-  D.forEach(([name,v],i)=>{
-    const x=X0+i*PX,y=map(v),hero=i===0;
-    const col=hero?WHEAT:'#F0EFE9';
-    el(s,'line',{x1:x,y1:base,x2:x,y2:y,stroke:col,'stroke-width':hero?1.6:1,
-      opacity:hero?1:.8,class:'fade',style:`animation-delay:${.1+i*.05}s`});
-    const dot=el(s,'circle',{cx:x,cy:y,r:hero?5:3.4,fill:col,
-      class:'pop',style:`animation-delay:${.2+i*.05}s`});
-    tip(dot,`${name} —— ${v} 人提及`);
-    txt(s,{x,y:y-9,'font-size':9,'font-weight':800,fill:col,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.05}s`},v);
-    txt(s,{x:x+3,y:base+8,'font-size':7,'font-weight':600,fill:'rgba(240,239,235,.75)',
-      'text-anchor':'end',transform:`rotate(-52 ${x+3} ${base+8})`,
-      class:'fade',style:`animation-delay:${.15+i*.05}s`},name);
-  });
-  txt(s,{x:240,y:242,'font-size':7,'font-weight':600,fill:'rgba(240,239,235,.5)',
-    'text-anchor':'middle','letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一杆 = 一件事 · 杆高 = 提及人数');
-});
-})();
-
-/* ════ 模块D · L14 hundred field(轨道版)════
-   结构照搬 lupi-gallery "hundred field":golden-angle 撒布 1 点 = 1 人,
-   四簇横排成轨道带(学图6 的轨道气泡阵),虚线连簇心。
-   PALM 色相=类目:CAT4 一簇一色。spoke 加墨 ×1.8。 */
-(()=>{
-const SEG=[['同事安利',41,CAT4[0]],['社媒刷到',27,CAT4[1]],['播客/视频',19,CAT4[2]],['公司要求',13,CAT4[3]]];
-const POS=[[96,150],[250,120],[386,170],[496,130]];
-obsReveal('orbit',s=>{
-  [[0,1],[1,2],[2,3]].forEach(([a,b],k)=>{
-    el(s,'line',{x1:POS[a][0],y1:POS[a][1],x2:POS[b][0],y2:POS[b][1],
-      stroke:GRID,'stroke-width':.8,'stroke-dasharray':'2 5',
-      class:'fade',style:`animation-delay:${.9+k*.1}s`});
-  });
-  SEG.forEach(([name,v,shade],ci)=>{
-    const [cx,cy]=POS[ci];
-    let edge=0;
-    for(let k=0;k<v;k++){
-      const a=k*137.508+ci*55;
-      const rr=4+Math.sqrt(k)*5.6+rnd(k+1,ci+2)*3;
-      edge=Math.max(edge,rr);
-      const [x,y]=pol(cx,cy,rr,a);
-      if(k%5===0)el(s,'line',{x1:cx,y1:cy,x2:x,y2:y,stroke:FAINTDATA,'stroke-width':1.1,
-        opacity:.85,class:'fade',style:`animation-delay:${ci*.14+k*.012}s`});
-      const dot=el(s,'circle',{cx:x,cy:y,r:1.5+rnd(k+2,ci+3)*1.7,fill:shade,opacity:.9,
-        class:'pop',style:`animation-delay:${ci*.14+k*.012}s`});
-      tip(dot,`${name} —— 100 人中的 ${v} 人之一`);
-    }
-    el(s,'circle',{cx,cy,r:2.4,fill:TXT,class:'pop',style:`animation-delay:${ci*.14}s`});
-    txt(s,{x:cx,y:cy+edge+14,'font-size':8.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+ci*.12}s`,
-      class:'fade'},`${name} · ${v}`);
-  });
-  txt(s,{x:280,y:292,'font-size':7.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.3s'},
-    '一点 = 一人 · 41 + 27 + 19 + 13 = 100');
-});
-})();
-
-/* ════ 模块E · F1 rung bars ════
-   结构照搬 basics-gallery "rung bars":1 档 = 8 人,
-   琥珀只给最大来源。 */
-(()=>{
-const D=[['视频教程',20],['X / 推特',20],['技术播客',17],['即刻/社群',17],['官方文档',16],
-         ['朋友演示',13],['公众号',13],['线下活动',10],['其他',6]];
-obsReveal('sources',s=>{
-  const x0=i=>44+i*54,base=216,step=6.4,HW=15;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i),hero=i===0;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:hero?HERO:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.07+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.07+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-9,'font-size':10.5,'font-weight':800,
-      fill:hero?HERO:DATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.07}s`},v*8);
-    tip(num,`${name} —— ${v*8} 人`);
-    txt(s,{x:x+3,y:base+12,'font-size':7,'font-weight':700,fill:MUT,'text-anchor':'end',
-      transform:`rotate(-46 ${x+3} ${base+12})`,
-      class:'fade',style:`animation-delay:${i*.07}s`},name);
-  });
-  el(s,'line',{x1:20,y1:base+4,x2:500,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:260,y:264,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一档 = 8 人 · 每五档一个圆点');
-});
-})();
-
-/* ════ 模块F · F5 tick rows 迷你 ════
-   最看好的方向对比。琥珀只给榜首。 */
-(()=>{
-const D=[['AGENT 工作流',64],['代码审查',43],['UI 生成',36],['数据分析',28],['写作辅助',19]];
-obsReveal('interest',s=>{
-  const y0=i=>26+i*32,X0=104,PX=3.6;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:96,y:y+3,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'end',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+7,x2:X0+70*PX,y2:y+7,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=7+rnd(k+1,i+2)*4;
-      el(s,'line',{x1:x,y1:y+7,x2:x,y2:y+7-h,stroke:hero?HERO:DATA,'stroke-width':1.4,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+10.5,r:.8,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.008}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,
-      fill:hero?HERO:DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},v*4);
-    tip(lab,`${name} —— ${v*4} 人看好`);
-  });
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 255
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-08.en.html

@@ -1,255 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Population One-Pager — Lieflat Report Template · Wire Hundred Field</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 08 · POPULATION ONE-PAGER
-     场景:一组人口级对比陈述(公益倡导页、行业白皮书开篇、政策简报)
-     排版参考:图7 "Global challenge" isotype(一个小人 = 1 亿)——
-               细规线压顶的断行大字陈述、左上图例、巨幅单位网格场、
-               底部两组体量对比 —— 只学排版和字体。
-     色板正本 = color-presets.js 的 WIRE 一套(灰阶人群 + 荧光橙主角,
-     与原图「灰色人群里点亮子集」同构)。isotype 小人翻译成 lieflat 的
-     单位点阵:一点 = 一个单位,可数。
-     图表正本 = lieflat gallery:
-       图A  单位点阵场 = L14 hundred field 的网格排布(1 点 = 60 团队)
-       图B  F6 paired rungs · basics-gallery.html(今天 vs 2030 两组)
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0F0EE; --txt:#1F1E1C;
-    --mut:rgba(31,30,28,.60); --lab:rgba(31,30,28,.72);
-    --faint:rgba(31,30,28,.32); --quiet:rgba(31,30,28,.15); --grid:rgba(31,30,28,.16);
-    --data:#22211F; --data2:#8F8E86; --hero:#F5572F; --faintdata:#C0BFB7;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 28px}
-  .sheet{width:880px;max-width:880px}
-
-  /* ── 断行大字陈述:细规线压顶(学图7)── */
-  .state{border-top:1px solid var(--txt);padding-top:14px}
-  .state .lead{font-size:21px;font-weight:700;line-height:1.35}
-  .state .lead b{font-weight:900}
-  h1{font-size:31px;font-weight:900;letter-spacing:-.02em;line-height:1.15;margin-bottom:6px}
-
-  .head{display:grid;grid-template-columns:200px 1fr;gap:44px;align-items:end}
-  .legend{font-size:10px;font-weight:700;color:var(--mut);text-transform:uppercase}
-  .legend .rule{border-top:1px solid var(--txt);margin-bottom:10px;width:64px}
-  .legend .row{display:flex;align-items:center;gap:8px;letter-spacing:.04em}
-  .legend .dot{width:9px;height:9px;border-radius:50%;background:var(--data)}
-
-  /* ── 双陈述行 ── */
-  .claims{display:grid;grid-template-columns:1fr 1fr;gap:44px;margin-top:40px}
-  .claims .c .num{font-size:34px;font-weight:900;letter-spacing:-.02em}
-  .claims .c:first-child .num{color:var(--hero)}
-  .claims .c:last-child .num{color:var(--txt)}
-  .claims .c .t{font-size:16.5px;font-weight:700;line-height:1.35;margin-top:2px}
-  .claims .c .src{font-size:9px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);margin-top:6px}
-
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-  .srcline{font-size:8.5px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;color:var(--faint);margin-top:10px}
-
-  /* ── 底部两组体量对比 ── */
-  .compare{display:grid;grid-template-columns:1fr 1fr;gap:44px;margin-top:56px}
-  .compare .b .state{padding-top:12px}
-  .compare .b .t{font-size:16.5px;font-weight:700;line-height:1.35}
-  .compare .b .pct{font-size:34px;font-weight:900;letter-spacing:-.01em;margin-top:2px}
-  .compare .b:first-child .pct{color:var(--hero)}
-  .compare .b:last-child .pct{color:var(--txt)}
-
-  .bridge{margin-top:34px;border-top:1px dotted var(--faint);padding-top:12px;
-    text-align:right;font-size:14px;font-weight:700;letter-spacing:-.01em}
-  .bridge b{font-weight:900}
-
-  .foot{margin-top:56px;padding-top:12px;border-top:1px solid var(--txt);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--mut)}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){.pop,.fade{animation:none}}
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 头部:图例 + 大字陈述 ══ -->
-  <div class="head">
-    <div class="legend">
-      <div class="rule"></div>
-      <div style="margin-bottom:10px;letter-spacing:.14em">Key</div>
-      <div class="row"><span class="dot"></span><span>one dot = 60 teams</span></div>
-    </div>
-    <div class="state">
-      <h1>Of roughly <b>6,000</b> software<br>teams under ten people,</h1>
-    </div>
-  </div>
-
-  <!-- ══ 双陈述 ══ -->
-  <div class="claims">
-    <div class="c state">
-      <div class="num">2,160</div>
-      <div class="t">have no shipping<br>process at all</div>
-      <div class="src">Demo data · 2026</div>
-    </div>
-    <div class="c state">
-      <div class="num">1,680</div>
-      <div class="t">lose ten-plus hours<br>a week to meetings</div>
-      <div class="src">Demo data · 2026</div>
-    </div>
-  </div>
-
-  <!-- ══ 单位点阵场 ══
-       翻译自图7 的 isotype 小人网格:一点 = 60 团队,100 点 = 全体。
-       橙 = 无发布流程(36 点),墨 = 会议过载(28 点),灰 = 其余。
-       WIRE 纪律:橙只给一个主角子集。 -->
-  <div class="fig" style="margin-top:44px"><svg id="field" viewBox="0 0 880 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">Unit field · one dot = 60 teams · 36 orange + 28 ink + 36 grey = 100 dots · the two subsets overlap in reality, counted here as separate</div>
-
-  <!-- ══ 底部:两组 2026 vs 2030 对比 ══
-       翻译自图7 的灯泡/水滴体量对比:paired rungs,一档一个单位,可数。 -->
-  <div class="compare">
-    <div class="b">
-      <div class="state">
-        <div class="t">By 2030, small teams running<br>async are set to grow</div>
-        <div class="pct">+45%</div>
-      </div>
-      <div class="fig"><svg id="async" viewBox="0 0 320 240" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Paired rungs · one rung = 200 teams · pale = 2026 · orange = 2030</div>
-    </div>
-    <div class="b">
-      <div class="state">
-        <div class="t">while weekly meeting<br>hours are set to fall</div>
-        <div class="pct">−20%</div>
-      </div>
-      <div class="fig"><svg id="meeting" viewBox="0 0 320 240" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Paired rungs · one rung = 1 hour/week · pale = 2026 · ink = 2030</div>
-    </div>
-  </div>
-
-  <div class="bridge">Where the two lines cross is the same thing: <b>moving collaboration out of the calendar and into the doc.</b></div>
-
-  <div class="foot">
-    <span>The global problem for small teams · population one-pager</span>
-    <span>All demo data · lieflat charts</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── WIRE preset(color-presets.js 原值)── */
-const BG='#F0F0EE',TXT='#1F1E1C',MUT='rgba(31,30,28,.60)',FAINT='rgba(31,30,28,.32)',
-      GRID='rgba(31,30,28,.16)';
-const DATA='#22211F',DATA2='#8F8E86',HERO='#F5572F',FAINTDATA='#C0BFB7';
-
-/* ════ 图A · 单位点阵场 ════
-   isotype 网格的 lieflat 翻译:20 列 × 5 行 = 100 点,1 点 = 60 团队。
-   前 36 点橙(无发布流程)、接 28 点墨(会议过载)、其余 36 点淡灰。
-   点径 rnd 微抖(Lupi 肌理),每 10 点列脚一粒定位点。 */
-(()=>{
-const SEG=[[36,'#F5572F','no shipping process'],[28,'#22211F','overloaded on meetings'],[36,'#C0BFB7','the rest']];
-obsReveal('field',s=>{
-  const COLS=20,PX=44,PY=52,X0=32,Y0=40;
-  let idx=0;
-  SEG.forEach(([n,col,name],si)=>{
-    for(let k=0;k<n;k++,idx++){
-      const c=idx%COLS,r=Math.floor(idx/COLS);
-      const x=X0+c*PX+(rnd(idx+1,si+3)-.5)*4,
-            y=Y0+r*PY+(rnd(idx+2,si+7)-.5)*4;
-      const dot=el(s,'circle',{cx:x,cy:y,r:7.5+rnd(idx+3,si+5)*2.5,fill:col,
-        opacity:col==='#C0BFB7'?.85:.95,
-        class:'pop',style:`animation-delay:${idx*.012}s`});
-      tip(dot,`${name} — one dot = 60 teams`);
-    }
-  });
-  // 列脚定位点:每 5 列一粒
-  for(let c=0;c<=COLS;c+=5)
-    el(s,'circle',{cx:X0+c*PX-PX/2,cy:Y0+4.6*PY+26,r:1.2,fill:FAINTDATA,
-      class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:X0+9.5*PX,y:Y0+4.6*PY+30,'font-size':7.5,'font-weight':600,fill:FAINT,
-    'text-anchor':'middle','letter-spacing':'.12em',class:'fade',style:'animation-delay:1.3s'},
-    'TWENTY DOTS PER ROW · READ FROM TOP LEFT');
-});
-})();
-
-/* ════ 图B/C · F6 paired rungs ════
-   结构照搬 basics-gallery "paired rungs":两把梯子并肩,
-   2026 淡档、2030 实档(左橙右墨),一档一个诚实单位。 */
-const pairs=(id,was,now,unit,colNow)=>{
-  obsReveal(id,s=>{
-    const base=196,step=5.2,HW=16,xa=110,xb=210;
-    for(let k=0;k<was;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,3)*3;
-      el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.2,
-        opacity:.75+rnd(k+2,4)*.25,class:'fade',style:`animation-delay:${k*.014}s`});
-    }
-    for(let k=0;k<now;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,9)*3;
-      el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:colNow,'stroke-width':1.2,
-        opacity:.8+rnd(k+2,11)*.2,class:'fade',style:`animation-delay:${.15+k*.014}s`});
-      if(k%5===4)el(s,'circle',{cx:xb+HW+5,cy:y,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${.15+k*.014}s`});
-    }
-    const numA=txt(s,{x:xa,y:base-(was-1)*step-10,'font-size':10,'font-weight':700,
-      fill:DATA2,'text-anchor':'middle',class:'fade',style:'animation-delay:.5s'},was*unit.k);
-    const numB=txt(s,{x:xb,y:base-(now-1)*step-10,'font-size':12,'font-weight':800,
-      fill:colNow,'text-anchor':'middle',class:'fade',style:'animation-delay:.65s'},now*unit.k);
-    tip(numA,`2026 — ${was*unit.k}${unit.tp}`);
-    tip(numB,`2030 — ${now*unit.k}${unit.tp}`);
-    txt(s,{x:xa,y:base+16,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},'2026');
-    txt(s,{x:xb,y:base+16,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},'2030');
-    el(s,'line',{x1:60,y1:base+4,x2:260,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-    txt(s,{x:160,y:230,'font-size':6.5,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-      `ONE RUNG = ${unit.k}${unit.ts.toUpperCase()}`);
-  });
-};
-pairs('async',12,18,{k:200,tp:' teams',ts:' teams'},HERO);   // 2400 → 3600 (+50%≈45% 示意)
-pairs('meeting',15,12,{k:1,tp:' hours',ts:' hour/week'},DATA);    // 15 → 12 (−20%)
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=880;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 255
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-08.zh.html

@@ -1,255 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>小团队的全球一题 — Lieflat 报告模板 · 百点场</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 08 · 单位人群一页(百点场)
-     场景:一组人口级对比陈述(公益倡导页、行业白皮书开篇、政策简报)
-     排版参考:图7 "Global challenge" isotype(一个小人 = 1 亿)——
-               细规线压顶的断行大字陈述、左上图例、巨幅单位网格场、
-               底部两组体量对比 —— 只学排版和字体。
-     色板正本 = color-presets.js 的 WIRE 一套(灰阶人群 + 荧光橙主角,
-     与原图「灰色人群里点亮子集」同构)。isotype 小人翻译成 lieflat 的
-     单位点阵:一点 = 一个单位,可数。
-     图表正本 = lieflat gallery:
-       图A  单位点阵场 = L14 hundred field 的网格排布(1 点 = 60 团队)
-       图B  F6 paired rungs · basics-gallery.html(今天 vs 2030 两组)
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0F0EE; --txt:#1F1E1C;
-    --mut:rgba(31,30,28,.60); --lab:rgba(31,30,28,.72);
-    --faint:rgba(31,30,28,.32); --quiet:rgba(31,30,28,.15); --grid:rgba(31,30,28,.16);
-    --data:#22211F; --data2:#8F8E86; --hero:#F5572F; --faintdata:#C0BFB7;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter','Noto Sans SC',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 28px}
-  .sheet{width:880px;max-width:880px}
-
-  /* ── 断行大字陈述:细规线压顶(学图7)── */
-  .state{border-top:1px solid var(--txt);padding-top:14px}
-  .state .lead{font-size:21px;font-weight:700;line-height:1.5}
-  .state .lead b{font-weight:900}
-  h1{font-size:30px;font-weight:900;line-height:1.4;margin-bottom:6px}
-
-  .head{display:grid;grid-template-columns:200px 1fr;gap:44px;align-items:end}
-  .legend{font-size:10px;font-weight:700;color:var(--mut)}
-  .legend .rule{border-top:1px solid var(--txt);margin-bottom:10px;width:64px}
-  .legend .row{display:flex;align-items:center;gap:8px}
-  .legend .dot{width:9px;height:9px;border-radius:50%;background:var(--data)}
-
-  /* ── 双陈述行 ── */
-  .claims{display:grid;grid-template-columns:1fr 1fr;gap:44px;margin-top:40px}
-  .claims .c .num{font-size:34px;font-weight:900;letter-spacing:-.02em}
-  .claims .c:first-child .num{color:var(--hero)}
-  .claims .c:last-child .num{color:var(--txt)}
-  .claims .c .t{font-size:16.5px;font-weight:700;line-height:1.5;margin-top:2px}
-  .claims .c .src{font-size:9px;font-weight:600;letter-spacing:.08em;color:var(--faint);margin-top:6px}
-
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.1em;color:var(--faint);margin-top:10px}
-
-  /* ── 底部两组体量对比 ── */
-  .compare{display:grid;grid-template-columns:1fr 1fr;gap:44px;margin-top:56px}
-  .compare .b .state{padding-top:12px}
-  .compare .b .t{font-size:16.5px;font-weight:700;line-height:1.55}
-  .compare .b .pct{font-size:34px;font-weight:900;margin-top:2px}
-  .compare .b:first-child .pct{color:var(--hero)}
-  .compare .b:last-child .pct{color:var(--txt)}
-
-  .bridge{margin-top:34px;border-top:1px dotted var(--faint);padding-top:12px;
-    text-align:right;font-size:14px;font-weight:700}
-  .bridge b{font-weight:900}
-
-  .foot{margin-top:56px;padding-top:12px;border-top:1px solid var(--txt);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;color:var(--mut)}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){.pop,.fade{animation:none}}
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 头部:图例 + 大字陈述 ══ -->
-  <div class="head">
-    <div class="legend">
-      <div class="rule"></div>
-      <div style="margin-bottom:10px;letter-spacing:.14em">图例</div>
-      <div class="row"><span class="dot"></span><span>一点 = 60 个团队</span></div>
-    </div>
-    <div class="state">
-      <h1>全球约 <b>6,000</b> 个<br>十人以下的软件小团队里,</h1>
-    </div>
-  </div>
-
-  <!-- ══ 双陈述 ══ -->
-  <div class="claims">
-    <div class="c state">
-      <div class="num">2,160 个</div>
-      <div class="t">还没有任何形式的<br>发布流程</div>
-      <div class="src">演示数据 · 2026</div>
-    </div>
-    <div class="c state">
-      <div class="num">1,680 个</div>
-      <div class="t">被每周十小时以上的<br>会议拖着走</div>
-      <div class="src">演示数据 · 2026</div>
-    </div>
-  </div>
-
-  <!-- ══ 单位点阵场 ══
-       翻译自图7 的 isotype 小人网格:一点 = 60 团队,100 点 = 全体。
-       橙 = 无发布流程(36 点),墨 = 会议过载(28 点),灰 = 其余。
-       WIRE 纪律:橙只给一个主角子集。 -->
-  <div class="fig" style="margin-top:44px"><svg id="field" viewBox="0 0 880 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-  <div class="srcline">UNIT FIELD · 一点 = 60 个团队 · 36 橙 + 28 墨 + 36 灰 = 100 点 · 两个子集有重叠,此处各自独立计数</div>
-
-  <!-- ══ 底部:两组 2026 vs 2030 对比 ══
-       翻译自图7 的灯泡/水滴体量对比:paired rungs,一档一个单位,可数。 -->
-  <div class="compare">
-    <div class="b">
-      <div class="state">
-        <div class="t">到 2030 年,采用异步流程<br>的小团队将增加</div>
-        <div class="pct">+45%</div>
-      </div>
-      <div class="fig"><svg id="async" viewBox="0 0 320 240" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">PAIRED RUNGS · 一档 = 200 个团队 · 淡 = 2026 · 橙 = 2030</div>
-    </div>
-    <div class="b">
-      <div class="state">
-        <div class="t">而每周会议时长<br>预计将下降</div>
-        <div class="pct">−20%</div>
-      </div>
-      <div class="fig"><svg id="meeting" viewBox="0 0 320 240" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">PAIRED RUNGS · 一档 = 1 小时/周 · 淡 = 2026 · 墨 = 2030</div>
-    </div>
-  </div>
-
-  <div class="bridge">两条线交汇处,是同一件事:<b>把协作从日历里搬进文档里。</b></div>
-
-  <div class="foot">
-    <span>小团队的全球一题 · 单位人群一页纸</span>
-    <span>全部为演示数据 · LIEFLAT CHARTS</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── WIRE preset(color-presets.js 原值)── */
-const BG='#F0F0EE',TXT='#1F1E1C',MUT='rgba(31,30,28,.60)',FAINT='rgba(31,30,28,.32)',
-      GRID='rgba(31,30,28,.16)';
-const DATA='#22211F',DATA2='#8F8E86',HERO='#F5572F',FAINTDATA='#C0BFB7';
-
-/* ════ 图A · 单位点阵场 ════
-   isotype 网格的 lieflat 翻译:20 列 × 5 行 = 100 点,1 点 = 60 团队。
-   前 36 点橙(无发布流程)、接 28 点墨(会议过载)、其余 36 点淡灰。
-   点径 rnd 微抖(Lupi 肌理),每 10 点列脚一粒定位点。 */
-(()=>{
-const SEG=[[36,'#F5572F','无发布流程'],[28,'#22211F','会议过载'],[36,'#C0BFB7','其余团队']];
-obsReveal('field',s=>{
-  const COLS=20,PX=44,PY=52,X0=32,Y0=40;
-  let idx=0;
-  SEG.forEach(([n,col,name],si)=>{
-    for(let k=0;k<n;k++,idx++){
-      const c=idx%COLS,r=Math.floor(idx/COLS);
-      const x=X0+c*PX+(rnd(idx+1,si+3)-.5)*4,
-            y=Y0+r*PY+(rnd(idx+2,si+7)-.5)*4;
-      const dot=el(s,'circle',{cx:x,cy:y,r:7.5+rnd(idx+3,si+5)*2.5,fill:col,
-        opacity:col==='#C0BFB7'?.85:.95,
-        class:'pop',style:`animation-delay:${idx*.012}s`});
-      tip(dot,`${name} —— 一点 = 60 个团队`);
-    }
-  });
-  // 列脚定位点:每 5 列一粒
-  for(let c=0;c<=COLS;c+=5)
-    el(s,'circle',{cx:X0+c*PX-PX/2,cy:Y0+4.6*PY+26,r:1.2,fill:FAINTDATA,
-      class:'fade',style:'animation-delay:1.2s'});
-  txt(s,{x:X0+9.5*PX,y:Y0+4.6*PY+30,'font-size':8,'font-weight':600,fill:FAINT,
-    'text-anchor':'middle','letter-spacing':'.1em',class:'fade',style:'animation-delay:1.3s'},
-    '每行 20 点 · 从左上顺序读');
-});
-})();
-
-/* ════ 图B/C · F6 paired rungs ════
-   结构照搬 basics-gallery "paired rungs":两把梯子并肩,
-   2026 淡档、2030 实档(左橙右墨),一档一个诚实单位。 */
-const pairs=(id,was,now,unit,colNow)=>{
-  obsReveal(id,s=>{
-    const base=196,step=5.2,HW=16,xa=110,xb=210;
-    for(let k=0;k<was;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,3)*3;
-      el(s,'line',{x1:xa-w,y1:y,x2:xa+w,y2:y,stroke:FAINTDATA,'stroke-width':1.2,
-        opacity:.75+rnd(k+2,4)*.25,class:'fade',style:`animation-delay:${k*.014}s`});
-    }
-    for(let k=0;k<now;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,9)*3;
-      el(s,'line',{x1:xb-w,y1:y,x2:xb+w,y2:y,stroke:colNow,'stroke-width':1.2,
-        opacity:.8+rnd(k+2,11)*.2,class:'fade',style:`animation-delay:${.15+k*.014}s`});
-      if(k%5===4)el(s,'circle',{cx:xb+HW+5,cy:y,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${.15+k*.014}s`});
-    }
-    const numA=txt(s,{x:xa,y:base-(was-1)*step-10,'font-size':10,'font-weight':700,
-      fill:DATA2,'text-anchor':'middle',class:'fade',style:'animation-delay:.5s'},was*unit.k);
-    const numB=txt(s,{x:xb,y:base-(now-1)*step-10,'font-size':12,'font-weight':800,
-      fill:colNow,'text-anchor':'middle',class:'fade',style:'animation-delay:.65s'},now*unit.k);
-    tip(numA,`2026 —— ${was*unit.k}${unit.t}`);
-    tip(numB,`2030 —— ${now*unit.k}${unit.t}`);
-    txt(s,{x:xa,y:base+16,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade'},'2026');
-    txt(s,{x:xb,y:base+16,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade'},'2030');
-    el(s,'line',{x1:60,y1:base+4,x2:260,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-    txt(s,{x:160,y:230,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-      `一档 = ${unit.k}${unit.t}`);
-  });
-};
-pairs('async',12,18,{k:200,t:' 团队'},HERO);   // 2400 → 3600 (+50%≈45% 示意)
-pairs('meeting',15,12,{k:1,t:' 小时'},DATA);    // 15 → 12 (−20%)
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=880;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 448
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-09.en.html

@@ -1,448 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Data Story Dashboard — Lieflat Report Template · Porcelain Summit</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 09 · DATA STORY DASHBOARD
-     场景:多模块数据仪表盘一页(AT A GLANCE 图区 + 右栏 KPI 起点)
-     排版参考:图8 WGS 刚果仪表盘 → 已按 lara 提供的 porcelain 重制稿
-               v2 校准:顶栏细规线刊头、大字标题 + 稀疏弧线流程装饰、
-               AT A GLANCE 2×2 图区(浅盘垫底)、右栏 A STARTING POINT
-               图例 + 六张虚线框 KPI 卡。
-     v2 铁律(上一版的教训):
-       · 数据图 100% 来自图型库正本,图脚标 REAL TEMPLATE ——
-         F1 rung bars / F2 hairline line / F4 tick donut(basics-gallery)
-         + L15 ballot tally(lupi-gallery)。
-       · 刊头弧线只做流程装饰(数据形状→图→页→报告),不承载数据,
-         不标数值——「翻译图型」不再混进数据区。
-       · KPI 卡是数据家具(排字 + 单条示意),不占图额度。
-     色板正本 = color-presets.js 的 PORCELAIN 一套。
-     彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F7F2EB; --txt:#081F5C;
-    --mut:rgba(8,31,92,.60); --lab:rgba(8,31,92,.72);
-    --faint:rgba(8,31,92,.32); --quiet:rgba(8,31,92,.15); --grid:rgba(8,31,92,.16);
-    --data:#334EAC; --data2:#7096D1; --hero:#081F5C; --faintdata:#BAD6EB;
-    --panel:rgba(8,31,92,.045);
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:52px 28px}
-  .sheet{width:1080px;max-width:1080px}
-
-  /* ── 顶栏刊头(学重制稿:左右排字 + 细规线)── */
-  .topbar{display:flex;justify-content:space-between;align-items:baseline;
-    border-bottom:1px solid var(--txt);padding-bottom:12px}
-  .topbar .l{font-size:14px;font-weight:800;letter-spacing:.18em;text-transform:uppercase}
-  .topbar .r{font-size:10px;font-weight:600;letter-spacing:.2em;color:var(--mut);text-transform:uppercase}
-
-  /* ── hero:大字标题 + 弧线流程装饰 ── */
-  .hero{display:grid;grid-template-columns:360px 1fr;gap:56px;margin-top:44px;align-items:center}
-  .hero h1{font-size:52px;font-weight:900;line-height:1.08;letter-spacing:-.03em}
-  .hero .dek{font-size:13px;line-height:1.75;color:var(--lab);margin-top:20px;max-width:320px}
-  .hero .meta{margin-top:26px}
-  .hero .meta .n{font-size:15px;font-weight:700}
-  .hero .meta .d{font-size:11px;color:var(--mut);line-height:1.6;margin-top:4px}
-  .hero .meta .d b{color:var(--data);font-weight:700}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-
-  .dotrule{border-top:1px dotted var(--faint);margin-top:44px}
-
-  /* ── 主体:左图区 + 右 KPI 栏 ── */
-  .main{display:grid;grid-template-columns:1fr 300px;gap:56px;margin-top:40px}
-  .glance>.ttl{display:flex;align-items:baseline;gap:18px;border-bottom:1px solid var(--txt);
-    padding-bottom:14px}
-  .glance>.ttl h2{font-size:34px;font-weight:800;letter-spacing:.06em}
-  .glance>.ttl .ex{font-size:11px;color:var(--mut);line-height:1.5}
-  .g2{display:grid;grid-template-columns:1fr 1fr;gap:40px;margin-top:30px}
-  .cell .claim{font-size:15px;font-weight:800;letter-spacing:-.01em}
-  .cell .micro{font-size:9.5px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;color:var(--mut);
-    margin:5px 0 10px}
-  .cell .panel{background:var(--panel);padding:18px 14px 12px}
-  .cell .srcline{font-size:9px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);
-    margin-top:9px}
-
-  /* ── 右栏 A STARTING POINT ── */
-  .rail h2{font-size:30px;font-weight:800;letter-spacing:.04em;line-height:1.2;
-    border-bottom:1px solid var(--txt);padding-bottom:14px}
-  .rail .intro{font-size:11.5px;line-height:1.75;color:var(--lab);margin-top:18px}
-  .legend{display:grid;grid-template-columns:1fr 1fr;gap:10px 14px;margin-top:20px}
-  .legend .it{display:flex;align-items:center;gap:7px;font-size:10.5px;font-weight:600}
-  .legend .sw{width:13px;height:13px;flex:none}
-  .legend .sw.hero{background:var(--hero)}
-  .legend .sw.aux{background:var(--data2)}
-  .legend .sw.empty{border:1.5px solid var(--hero);background:transparent}
-  .legend .sw.ideal{border:1.5px dotted var(--data);background:transparent}
-
-  .kgrid{display:grid;grid-template-columns:1fr 1fr;gap:26px 20px;margin-top:28px}
-  .kpi .h{font-size:14px;font-weight:800;letter-spacing:-.01em;border-top:1px solid var(--txt);padding-top:8px}
-  .kpi .d{font-size:9.5px;color:var(--mut);line-height:1.45;margin:3px 0 8px}
-  .kpi .box{border:1.5px dotted var(--faint);padding:12px 12px 10px;position:relative}
-  .kpi .box .v{font-size:22px;font-weight:800;text-align:right;letter-spacing:-.02em}
-  .kpi .box .bar{height:11px;margin-top:10px;display:flex}
-  .kpi .box .bar i{display:block;height:100%}
-
-  /* ── 页脚 ── */
-  .foot{margin-top:52px;border-top:1px solid var(--txt);padding-top:14px;
-    display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;align-items:start}
-  .foot .warn{font-size:10px;font-weight:700;color:var(--data);line-height:1.5}
-  .foot .warn small{display:block;font-weight:500;color:var(--mut)}
-  .foot .src{font-size:9.5px;color:var(--mut);line-height:1.7;letter-spacing:.06em}
-  .foot .logo{text-align:right;font-size:15px;font-weight:800;letter-spacing:.14em;line-height:1.5}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 顶栏 ══ -->
-  <div class="topbar">
-    <span class="l">LIEFLAT CHARTS / PORCELAIN</span>
-    <span class="r">DATA STORY DASHBOARD · DEMO</span>
-  </div>
-
-  <!-- ══ hero ══ -->
-  <div class="hero">
-    <div>
-      <h1>48 charts, one readable page.</h1>
-      <div class="dek">A working map from data shape, through visual grammar, to report structure.</div>
-      <div class="meta">
-        <div class="n">48 chart templates</div>
-        <div class="d">Three color presets · three reading systems<br><b>Demo content, not real product metrics</b></div>
-      </div>
-    </div>
-    <div>
-      <!-- 弧线流程装饰:数据形状 → 图 → 页 → 报告。
-           纯装饰,不承载数据、不标数值——四条发丝弧 + 一条主弧。 -->
-      <div class="fig"><svg id="flow" viewBox="0 0 620 220" preserveAspectRatio="xMidYMid meet"></svg></div>
-    </div>
-  </div>
-
-  <div class="dotrule"></div>
-
-  <!-- ══ 主体 ══ -->
-  <div class="main">
-    <!-- ── AT A GLANCE ── -->
-    <div class="glance">
-      <div class="ttl">
-        <h2>AT A GLANCE</h2>
-        <span class="ex">Fitting 48 chart types onto one page comes down to four variables that decide whether it reads well.</span>
-      </div>
-      <div class="g2">
-        <div class="cell">
-          <div class="claim">Few bars, countable units.</div>
-          <div class="micro">ONE RUNG = ONE UNIT · COUNTABLE COMPARISON</div>
-          <!-- 图A · F1 rung bars · basics-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="rungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">F1 RUNG BARS · BASICS-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-        <div class="cell">
-          <div class="claim">A timeline, day by day.</div>
-          <div class="micro">ONE DOT = ONE DAY · HOLLOW = WEEKEND</div>
-          <!-- 图B · F2 hairline line · basics-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">F2 HAIRLINE LINE · BASICS-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-        <div class="cell">
-          <div class="claim">Shares, without the pie.</div>
-          <div class="micro">ONE TICK = ONE PERCENT · READS CLOCKWISE</div>
-          <!-- 图C · F4 tick donut · basics-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="tickdonut" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">F4 TICK DONUT · BASICS-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-        <div class="cell">
-          <div class="claim">Multi-select, tick by tick.</div>
-          <div class="micro">ONE TICK = ONE RESPONDENT · MULTIPLE CHOICE</div>
-          <!-- 图D · L15 ballot tally · lupi-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="tally" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">L15 BALLOT TALLY · LUPI-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-      </div>
-    </div>
-
-    <!-- ── A STARTING POINT ── -->
-    <div class="rail">
-      <h2>A STARTING POINT</h2>
-      <div class="intro">Decide what judgment the reader needs before touching color. That's the actual starting point for a one-page report.</div>
-      <div class="legend">
-        <span class="it"><span class="sw hero"></span>Primary data</span>
-        <span class="it"><span class="sw aux"></span>Secondary data</span>
-        <span class="it"><span class="sw empty"></span>Empty / background</span>
-        <span class="it"><span class="sw ideal"></span>Target line / reference</span>
-      </div>
-      <!-- KPI 卡:数据家具(排字 + 单条示意),不占图额度 -->
-      <div class="kgrid" id="kpis"></div>
-    </div>
-  </div>
-
-  <!-- ══ 页脚 ══ -->
-  <div class="foot">
-    <div class="warn">Demo data, not real survey results.
-      <small>This page tests the information architecture behind chart templates → one-page report.</small></div>
-    <div class="src">DATA SOURCES<br>Lieflat Charts design system</div>
-    <div class="logo">LIEFLAT<br>CHARTS</div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PORCELAIN 角色 token(basics-porcelain.html 原值)── */
-const BG='#F7F2EB',TXT='#081F5C',MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',
-      FAINT='rgba(8,31,92,.32)';
-const FLOOR='rgba(8,31,92,.24)',QUIET='rgba(8,31,92,.15)',GRID='rgba(8,31,92,.16)';
-const DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C',FAINTDATA='#BAD6EB';
-const CAT4=['#081F5C','#334EAC','#7096D1','#BAD6EB'];
-
-/* ════ 刊头 · 弧线流程装饰 ════
-   纯装饰件:四个流程节点沿基线排开,相邻节点之间发丝弧承接,
-   「数据形状 → 报告」一条主弧跨全程。不标数值。 */
-(()=>{
-const NODE=[['DATA SHAPE',40],['CHART',236],['PAGE',420],['REPORT',580]];
-obsReveal('flow',s=>{
-  const OY=178;
-  el(s,'line',{x1:16,y1:OY,x2:604,y2:OY,stroke:GRID,'stroke-width':.8,class:'fade'});
-  // 嵌套弧:全部从「数据形状」起点扇出(学重制稿),
-  // 发丝弧落 图 / 报告,主弧粗线落 页。
-  const O=NODE[0][1];
-  const arc=(toX,stroke,sw,op,dl)=>{
-    const r=(toX-O)/2,ry=Math.min(150,r);   // 椭圆压扁,弧顶不出画布
-    el(s,'path',{d:`M${O} ${OY} A${r} ${ry} 0 0 1 ${toX} ${OY}`,fill:'none',
-      stroke,'stroke-width':sw,opacity:op,pathLength:1,
-      class:'draw',style:`animation-delay:${dl}s;animation-duration:1s`});
-  };
-  arc(NODE[1][1],FAINTDATA,1.3,.95,.2);
-  arc(NODE[3][1],DATA2,1.1,.6,.35);
-  arc(NODE[2][1],DATA,3,1,.55);
-  NODE.forEach(([name,x],i)=>{
-    el(s,'circle',{cx:x,cy:OY,r:i===0||i===3?4:2.6,fill:i===0||i===3?HERO:DATA,
-      class:'pop',style:`animation-delay:${.3+i*.12}s`});
-    txt(s,{x,y:OY+22,'font-size':9.5,'font-weight':700,fill:i===0||i===3?TXT:MUT,
-      'text-anchor':'middle','letter-spacing':'.14em',
-      class:'fade',style:`animation-delay:${.4+i*.12}s`},name);
-  });
-});
-})();
-
-/* ════ 图A · F1 rung bars ════
-   basics-porcelain.html 正本结构:柱身 = 一格格横档,1 档 = $1k,
-   档线 1.8(加墨),每五档一粒点,顶数 TXT。 */
-(()=>{
-const D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];
-obsReveal('rungs',s=>{
-  const x0=i=>56+i*56,base=266,step=5.6,HW=14;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i);
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — $${v}k MRR`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');
-});
-})();
-
-/* ════ 图B · F2 hairline line ════
-   basics-porcelain.html 正本结构:barcode 日历地板 + 折线 2.2 +
-   逐日圆点(周末空心 1.4 边),峰值 HERO 放大标数。 */
-(()=>{
-const N=30;
-const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
-obsReveal('dayline',s=>{
-  const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===5||d%7===6,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
-      fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
-});
-})();
-
-/* ════ 图C · F4 tick donut ════
-   basics-porcelain.html 正本结构:百 tick 表盘,1 tick = 1%,
-   CAT4 四段,tick 1.9(加墨),段标签虚线引出。 */
-(()=>{
-const D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];
-obsReveal('tickdonut',s=>{
-  const cx=200,cy=148,R0=64;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=12+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of traffic`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
-  txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');
-});
-})();
-
-/* ════ 图D · L15 ballot tally ════
-   lupi-gallery.html 正本结构:每行 100 根 tick,选中的上墨。
-   彩色加墨:picked 1.6 / 未选 1.0,地板 .85。首行 HERO。 */
-(()=>{
-const ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];
-obsReveal('tally',s=>{
-  ASK.forEach(([name,v],i)=>{
-    const base=68+i*60;
-    txt(s,{x:26,y:base-26,'font-size':7.5,'font-weight':700,fill:MUT,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:26,y1:base,x2:374,y2:base,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=26+k*3.48,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?(i===0?HERO:DATA):FAINTDATA,'stroke-width':picked?1.6:1,
-        opacity:picked?1:.85,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:26+(v-1)*3.48+9,y:base-11,'font-size':11,'font-weight':800,
-      fill:i===0?HERO:DATA,
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`${v} of 100 picked this — they could pick several`);
-  });
-  txt(s,{x:200,y:292,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
-});
-})();
-
-/* ════ 右栏 KPI 卡 ════
-   数据家具:虚线框 + 右上大数 + 底部双段条(主/辅示意),
-   纯排字 + 单条,不占图额度。 */
-(()=>{
-const KPI=[
-  ['Chart coverage','Basics to complex charts','48',.72],
-  ['Color presets','Mono / Porcelain / Palm / Wire','4',.55],
-  ['Reading systems','Lupi / Basics / Glance','3',.62],
-  ['Page unit','One page, one judgment','1',.8],
-  ['Reusable output','HTML / PNG / PDF','3',.6],
-  ['Data furniture','Titles, space, sourcing','∞',.68],
-];
-const box=document.getElementById('kpis');
-KPI.forEach(([h,d,v,fill])=>{
-  const k=document.createElement('div');
-  k.className='kpi';
-  k.innerHTML=`<div class="h">${h}</div><div class="d">${d}</div>
-    <div class="box"><div class="v">${v}</div>
-    <div class="bar"><i style="width:${fill*100}%;background:${DATA}"></i><i style="width:${(1-fill)*72}%;background:${FAINTDATA}"></i></div></div>`;
-  box.appendChild(k);
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 449
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-09.zh.html

@@ -1,449 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>把一套图表,变成一页可读的分析 — Lieflat 报告模板 · 青瓷峰会</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 09 · 数据故事仪表盘(青瓷峰会 · v2 重做)
-     场景:多模块数据仪表盘一页(AT A GLANCE 图区 + 右栏 KPI 起点)
-     排版参考:图8 WGS 刚果仪表盘 → 已按 lara 提供的 porcelain 重制稿
-               v2 校准:顶栏细规线刊头、大字标题 + 稀疏弧线流程装饰、
-               AT A GLANCE 2×2 图区(浅盘垫底)、右栏 A STARTING POINT
-               图例 + 六张虚线框 KPI 卡。
-     v2 铁律(上一版的教训):
-       · 数据图 100% 来自图型库正本,图脚标 REAL TEMPLATE ——
-         F1 rung bars / F2 hairline line / F4 tick donut(basics-gallery)
-         + L15 ballot tally(lupi-gallery)。
-       · 刊头弧线只做流程装饰(数据形状→图→页→报告),不承载数据,
-         不标数值——「翻译图型」不再混进数据区。
-       · KPI 卡是数据家具(排字 + 单条示意),不占图额度。
-     色板正本 = color-presets.js 的 PORCELAIN 一套。
-     彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F7F2EB; --txt:#081F5C;
-    --mut:rgba(8,31,92,.60); --lab:rgba(8,31,92,.72);
-    --faint:rgba(8,31,92,.32); --quiet:rgba(8,31,92,.15); --grid:rgba(8,31,92,.16);
-    --data:#334EAC; --data2:#7096D1; --hero:#081F5C; --faintdata:#BAD6EB;
-    --panel:rgba(8,31,92,.045);
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter','Noto Sans SC',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:52px 28px}
-  .sheet{width:1080px;max-width:1080px}
-
-  /* ── 顶栏刊头(学重制稿:左右排字 + 细规线)── */
-  .topbar{display:flex;justify-content:space-between;align-items:baseline;
-    border-bottom:1px solid var(--txt);padding-bottom:12px}
-  .topbar .l{font-size:14px;font-weight:800;letter-spacing:.18em}
-  .topbar .r{font-size:10px;font-weight:600;letter-spacing:.2em;color:var(--mut)}
-
-  /* ── hero:大字标题 + 弧线流程装饰 ── */
-  .hero{display:grid;grid-template-columns:360px 1fr;gap:56px;margin-top:44px;align-items:center}
-  .hero h1{font-size:52px;font-weight:900;line-height:1.32;letter-spacing:.02em}
-  .hero .dek{font-size:13px;line-height:1.9;color:var(--lab);margin-top:20px;max-width:320px}
-  .hero .meta{margin-top:26px}
-  .hero .meta .n{font-size:15px;font-weight:700}
-  .hero .meta .d{font-size:11px;color:var(--mut);line-height:1.8;margin-top:4px}
-  .hero .meta .d b{color:var(--data);font-weight:700}
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-
-  .dotrule{border-top:1px dotted var(--faint);margin-top:44px}
-
-  /* ── 主体:左图区 + 右 KPI 栏 ── */
-  .main{display:grid;grid-template-columns:1fr 300px;gap:56px;margin-top:40px}
-  .glance>.ttl{display:flex;align-items:baseline;gap:18px;border-bottom:1px solid var(--txt);
-    padding-bottom:14px}
-  .glance>.ttl h2{font-size:34px;font-weight:800;letter-spacing:.06em}
-  .glance>.ttl .ex{font-size:11px;color:var(--mut);line-height:1.6}
-  .g2{display:grid;grid-template-columns:1fr 1fr;gap:40px;margin-top:30px}
-  .cell .claim{font-size:15px;font-weight:800;letter-spacing:.02em}
-  .cell .micro{font-size:9.5px;font-weight:600;letter-spacing:.14em;color:var(--mut);
-    margin:5px 0 10px}
-  .cell .panel{background:var(--panel);padding:18px 14px 12px}
-  .cell .srcline{font-size:9px;font-weight:600;letter-spacing:.12em;color:var(--faint);
-    margin-top:9px}
-
-  /* ── 右栏 A STARTING POINT ── */
-  .rail h2{font-size:30px;font-weight:800;letter-spacing:.04em;line-height:1.2;
-    border-bottom:1px solid var(--txt);padding-bottom:14px}
-  .rail .intro{font-size:11.5px;line-height:1.95;color:var(--lab);margin-top:18px}
-  .legend{display:grid;grid-template-columns:1fr 1fr;gap:10px 14px;margin-top:20px}
-  .legend .it{display:flex;align-items:center;gap:7px;font-size:10.5px;font-weight:600}
-  .legend .sw{width:13px;height:13px;flex:none}
-  .legend .sw.hero{background:var(--hero)}
-  .legend .sw.aux{background:var(--data2)}
-  .legend .sw.empty{border:1.5px solid var(--hero);background:transparent}
-  .legend .sw.ideal{border:1.5px dotted var(--data);background:transparent}
-
-  .kgrid{display:grid;grid-template-columns:1fr 1fr;gap:26px 20px;margin-top:28px}
-  .kpi .h{font-size:14px;font-weight:800;border-top:1px solid var(--txt);padding-top:8px}
-  .kpi .d{font-size:9.5px;color:var(--mut);line-height:1.55;margin:3px 0 8px}
-  .kpi .box{border:1.5px dotted var(--faint);padding:12px 12px 10px;position:relative}
-  .kpi .box .v{font-size:22px;font-weight:800;text-align:right;letter-spacing:-.02em}
-  .kpi .box .bar{height:11px;margin-top:10px;display:flex}
-  .kpi .box .bar i{display:block;height:100%}
-
-  /* ── 页脚 ── */
-  .foot{margin-top:52px;border-top:1px solid var(--txt);padding-top:14px;
-    display:grid;grid-template-columns:1fr 1fr 1fr;gap:20px;align-items:start}
-  .foot .warn{font-size:10px;font-weight:700;color:var(--data);line-height:1.7}
-  .foot .warn small{display:block;font-weight:500;color:var(--mut)}
-  .foot .src{font-size:9.5px;color:var(--mut);line-height:1.7;letter-spacing:.06em}
-  .foot .logo{text-align:right;font-size:15px;font-weight:800;letter-spacing:.14em;line-height:1.5}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 顶栏 ══ -->
-  <div class="topbar">
-    <span class="l">LIEFLAT CHARTS / PORCELAIN</span>
-    <span class="r">DATA STORY DASHBOARD · DEMO</span>
-  </div>
-
-  <!-- ══ hero ══ -->
-  <div class="hero">
-    <div>
-      <h1>把一套图表,变成一页可读的分析。</h1>
-      <div class="dek">这不是图型目录,而是一张从数据形状、视觉语法到报告结构的工作地图。</div>
-      <div class="meta">
-        <div class="n">48 chart templates</div>
-        <div class="d">三套彩色预设 · 三种阅读系统<br><b>演示内容,不代表真实产品指标</b></div>
-      </div>
-    </div>
-    <div>
-      <!-- 弧线流程装饰:数据形状 → 图 → 页 → 报告。
-           纯装饰,不承载数据、不标数值——四条发丝弧 + 一条主弧。 -->
-      <div class="fig"><svg id="flow" viewBox="0 0 620 220" preserveAspectRatio="xMidYMid meet"></svg></div>
-    </div>
-  </div>
-
-  <div class="dotrule"></div>
-
-  <!-- ══ 主体 ══ -->
-  <div class="main">
-    <!-- ── AT A GLANCE ── -->
-    <div class="glance">
-      <div class="ttl">
-        <h2>AT A GLANCE</h2>
-        <span class="ex">如何把 48 种图型组织成一页?先看四个决定页面质量的关键变量。</span>
-      </div>
-      <div class="g2">
-        <div class="cell">
-          <div class="claim">少量类目,仍然保留单位</div>
-          <div class="micro">ONE RUNG = ONE UNIT · COUNTABLE COMPARISON</div>
-          <!-- 图A · F1 rung bars · basics-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="rungs" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">F1 RUNG BARS · BASICS-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-        <div class="cell">
-          <div class="claim">时间序列,逐日展开</div>
-          <div class="micro">ONE DOT = ONE DAY · HOLLOW = WEEKEND</div>
-          <!-- 图B · F2 hairline line · basics-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="dayline" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">F2 HAIRLINE LINE · BASICS-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-        <div class="cell">
-          <div class="claim">占比,不用默认饼图</div>
-          <div class="micro">ONE TICK = ONE PERCENT · READS CLOCKWISE</div>
-          <!-- 图C · F4 tick donut · basics-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="tickdonut" viewBox="0 0 400 320" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">F4 TICK DONUT · BASICS-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-        <div class="cell">
-          <div class="claim">多选题,按单位逐条计数</div>
-          <div class="micro">ONE TICK = ONE RESPONDENT · MULTIPLE CHOICE</div>
-          <!-- 图D · L15 ballot tally · lupi-gallery.html 正本,只换色板 -->
-          <div class="panel fig"><svg id="tally" viewBox="0 0 400 300" preserveAspectRatio="xMidYMid meet"></svg></div>
-          <div class="srcline">L15 BALLOT TALLY · LUPI-GALLERY.HTML · REAL TEMPLATE</div>
-        </div>
-      </div>
-    </div>
-
-    <!-- ── A STARTING POINT ── -->
-    <div class="rail">
-      <h2>A STARTING POINT</h2>
-      <div class="intro">如果要把一组数据变成一页分析报告,最好的起点不是先选颜色,
-        而是先问:读者需要做出什么判断?</div>
-      <div class="legend">
-        <span class="it"><span class="sw hero"></span>主数据</span>
-        <span class="it"><span class="sw aux"></span>辅助数据</span>
-        <span class="it"><span class="sw empty"></span>空值 / 背景</span>
-        <span class="it"><span class="sw ideal"></span>理想线 / 参考线</span>
-      </div>
-      <!-- KPI 卡:数据家具(排字 + 单条示意),不占图额度 -->
-      <div class="kgrid" id="kpis"></div>
-    </div>
-  </div>
-
-  <!-- ══ 页脚 ══ -->
-  <div class="foot">
-    <div class="warn">演示数据,不代表真实调查结果。
-      <small>这张页面用于验证「图表模板 → 一页分析报告」的信息架构。</small></div>
-    <div class="src">DATA SOURCES<br>Lieflat Charts design system</div>
-    <div class="logo">LIEFLAT<br>CHARTS</div>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PORCELAIN 角色 token(basics-porcelain.html 原值)── */
-const BG='#F7F2EB',TXT='#081F5C',MUT='rgba(8,31,92,.60)',LAB='rgba(8,31,92,.72)',
-      FAINT='rgba(8,31,92,.32)';
-const FLOOR='rgba(8,31,92,.24)',QUIET='rgba(8,31,92,.15)',GRID='rgba(8,31,92,.16)';
-const DATA='#334EAC',DATA2='#7096D1',HERO='#081F5C',FAINTDATA='#BAD6EB';
-const CAT4=['#081F5C','#334EAC','#7096D1','#BAD6EB'];
-
-/* ════ 刊头 · 弧线流程装饰 ════
-   纯装饰件:四个流程节点沿基线排开,相邻节点之间发丝弧承接,
-   「数据形状 → 报告」一条主弧跨全程。不标数值。 */
-(()=>{
-const NODE=[['数据形状',40],['图',236],['页',420],['报告',580]];
-obsReveal('flow',s=>{
-  const OY=178;
-  el(s,'line',{x1:16,y1:OY,x2:604,y2:OY,stroke:GRID,'stroke-width':.8,class:'fade'});
-  // 嵌套弧:全部从「数据形状」起点扇出(学重制稿),
-  // 发丝弧落 图 / 报告,主弧粗线落 页。
-  const O=NODE[0][1];
-  const arc=(toX,stroke,sw,op,dl)=>{
-    const r=(toX-O)/2,ry=Math.min(150,r);   // 椭圆压扁,弧顶不出画布
-    el(s,'path',{d:`M${O} ${OY} A${r} ${ry} 0 0 1 ${toX} ${OY}`,fill:'none',
-      stroke,'stroke-width':sw,opacity:op,pathLength:1,
-      class:'draw',style:`animation-delay:${dl}s;animation-duration:1s`});
-  };
-  arc(NODE[1][1],FAINTDATA,1.3,.95,.2);
-  arc(NODE[3][1],DATA2,1.1,.6,.35);
-  arc(NODE[2][1],DATA,3,1,.55);
-  NODE.forEach(([name,x],i)=>{
-    el(s,'circle',{cx:x,cy:OY,r:i===0||i===3?4:2.6,fill:i===0||i===3?HERO:DATA,
-      class:'pop',style:`animation-delay:${.3+i*.12}s`});
-    txt(s,{x,y:OY+22,'font-size':9.5,'font-weight':700,fill:i===0||i===3?TXT:MUT,
-      'text-anchor':'middle','letter-spacing':'.14em',
-      class:'fade',style:`animation-delay:${.4+i*.12}s`},name);
-  });
-});
-})();
-
-/* ════ 图A · F1 rung bars ════
-   basics-porcelain.html 正本结构:柱身 = 一格格横档,1 档 = $1k,
-   档线 1.8(加墨),每五档一粒点,顶数 TXT。 */
-(()=>{
-const D=[['FREE',38],['STARTER',27],['PRO',22],['TEAM',16],['SCALE',11],['ENT',7]];
-obsReveal('rungs',s=>{
-  const x0=i=>56+i*56,base=266,step=5.6,HW=14;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i);
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x+HW+4.5,cy:y,r:.8,fill:FAINT,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-10,'font-size':11,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — $${v}k MRR`);
-    txt(s,{x,y:base+18,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:28,y1:base+4,x2:372,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = $1K · DOT MARKS EVERY FIFTH');
-});
-})();
-
-/* ════ 图B · F2 hairline line ════
-   basics-porcelain.html 正本结构:barcode 日历地板 + 折线 2.2 +
-   逐日圆点(周末空心 1.4 边),峰值 HERO 放大标数。 */
-(()=>{
-const N=30;
-const val=d=>46+22*Math.sin(d/4.6)+14*Math.sin(d/2.1)+rnd(d+1,5)*12;
-obsReveal('dayline',s=>{
-  const x=d=>30+d*11.7,base=262,map=v=>base-v*2.15;
-  for(let d=0;d<N;d++)
-    el(s,'line',{x1:x(d),y1:base,x2:x(d),y2:base-7,stroke:FLOOR,'stroke-width':.6,
-      class:'fade',style:`animation-delay:${d*.008}s`});
-  el(s,'line',{x1:24,y1:base,x2:376,y2:base,stroke:GRID,'stroke-width':.8,class:'fade'});
-  const vs=Array.from({length:N},(_,d)=>val(d));
-  const top=[];
-  for(const d of [...vs.keys()].sort((a,b)=>vs[b]-vs[a])){
-    if(top.every(t=>Math.abs(t-d)>=5))top.push(d);
-    if(top.length===2)break;
-  }
-  const pts=vs.map((v,d)=>`${x(d)} ${map(v)}`).join(' L ');
-  el(s,'path',{d:'M'+pts,fill:'none',stroke:DATA,'stroke-width':2.2,pathLength:1,
-    class:'draw',style:'animation-duration:1.2s'});
-  vs.forEach((v,d)=>{
-    const weekend=d%7===5||d%7===6,big=top.includes(d);
-    const dot=el(s,'circle',{cx:x(d),cy:map(v),r:big?4.2:2.1,
-      fill:weekend?BG:(big?HERO:DATA),stroke:big?HERO:DATA,'stroke-width':weekend?1.4:0,
-      class:'pop',style:`animation-delay:${.2+d*.03}s`});
-    tip(dot,`Day ${d+1} — ${Math.round(v)} sign-ups`);
-    if(big)txt(s,{x:x(d),y:map(v)-11,'font-size':9.5,'font-weight':800,fill:TXT,'text-anchor':'middle',
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${1+d*.01}s`,
-      class:'fade'},Math.round(v));
-  });
-  [[0,'JUN 1'],[14,'JUN 15'],[29,'JUN 30']].forEach(([d,m])=>
-    txt(s,{x:x(d),y:base+18,'font-size':7.5,'font-weight':600,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.1em',class:'fade'},m));
-  txt(s,{x:200,y:306,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE DOT = ONE DAY · HOLLOW = WEEKEND');
-});
-})();
-
-/* ════ 图C · F4 tick donut ════
-   basics-porcelain.html 正本结构:百 tick 表盘,1 tick = 1%,
-   CAT4 四段,tick 1.9(加墨),段标签虚线引出。 */
-(()=>{
-const D=[['ORGANIC',37,CAT4[0]],['PAID',28,CAT4[1]],['REFERRAL',21,CAT4[2]],['SOCIAL',14,CAT4[3]]];
-obsReveal('tickdonut',s=>{
-  const cx=200,cy=148,R0=64;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx*3.6-90;
-      const len=12+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.9,
-        class:'fade',style:`animation-delay:${idx*.012}s`});
-      if(idx%10===0){
-        const [dx,dy]=pol(cx,cy,R0-5,a);
-        el(s,'circle',{cx:dx,cy:dy,r:.8,fill:FAINT,
-          class:'fade',style:`animation-delay:${idx*.012}s`});
-      }
-    }
-    const mid=(k0+v/2)*3.6-90,[lx,ly]=pol(cx,cy,R0+38,mid),[gx,gy]=pol(cx,cy,R0+20,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINT,'stroke-width':.7,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v}% of traffic`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-2,'font-size':22,'font-weight':800,fill:TXT,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'100');
-  txt(s,{x:cx,y:cy+14,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'TICKS · ONE = 1%');
-  txt(s,{x:200,y:296,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'TWELVE O’CLOCK IS ZERO · DOT MARKS EVERY TENTH · READS CLOCKWISE');
-});
-})();
-
-/* ════ 图D · L15 ballot tally ════
-   lupi-gallery.html 正本结构:每行 100 根 tick,选中的上墨。
-   彩色加墨:picked 1.6 / 未选 1.0,地板 .85。首行 HERO。 */
-(()=>{
-const ASK=[['DO MORE WITH THE SAME PAY',51],['AN UNSUSTAINABLE PACE',46],['QUALITY OF WORK SLIPPING',41],['BEING REPLACED OUTRIGHT',22]];
-obsReveal('tally',s=>{
-  ASK.forEach(([name,v],i)=>{
-    const base=68+i*60;
-    txt(s,{x:26,y:base-26,'font-size':7.5,'font-weight':700,fill:MUT,
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.1}s`},name);
-    el(s,'line',{x1:26,y1:base,x2:374,y2:base,stroke:GRID,'stroke-width':.8,
-      class:'fade',style:`animation-delay:${i*.1}s`});
-    for(let k=0;k<100;k++){
-      const x=26+k*3.48,picked=k<v;
-      const h=picked?12+rnd(k+1,i+2)*5:4.5+rnd(k+1,i+5)*2;
-      el(s,'line',{x1:x,y1:base,x2:x,y2:base-h,
-        stroke:picked?(i===0?HERO:DATA):FAINTDATA,'stroke-width':picked?1.6:1,
-        opacity:picked?1:.85,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-      if(k%10===0)el(s,'circle',{cx:x,cy:base+4.5,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.1+k*.006}s`});
-    }
-    const lab=txt(s,{x:26+(v-1)*3.48+9,y:base-11,'font-size':11,'font-weight':800,
-      fill:i===0?HERO:DATA,
-      style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.5+i*.1}s`,
-      class:'fade'},v);
-    tip(lab,`${v} of 100 picked this — they could pick several`);
-  });
-  txt(s,{x:200,y:292,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE TICK = ONE RESPONDENT · DOT MARKS EVERY TENTH');
-});
-})();
-
-/* ════ 右栏 KPI 卡 ════
-   数据家具:虚线框 + 右上大数 + 底部双段条(主/辅示意),
-   纯排字 + 单条,不占图额度。 */
-(()=>{
-const KPI=[
-  ['图型覆盖','基础图到复杂关系','48',.72],
-  ['配色预设','Mono / Porcelain / Palm / Wire','4',.55],
-  ['阅读系统','Lupi / Basics / Glance','3',.62],
-  ['页面单位','一页一个主判断','1',.8],
-  ['可复用输出','HTML / PNG / PDF','3',.6],
-  ['数据家具','标题 / 留白 / 来源','∞',.68],
-];
-const box=document.getElementById('kpis');
-KPI.forEach(([h,d,v,fill])=>{
-  const k=document.createElement('div');
-  k.className='kpi';
-  k.innerHTML=`<div class="h">${h}</div><div class="d">${d}</div>
-    <div class="box"><div class="v">${v}</div>
-    <div class="bar"><i style="width:${fill*100}%;background:${DATA}"></i><i style="width:${(1-fill)*72}%;background:${FAINTDATA}"></i></div></div>`;
-  box.appendChild(k);
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 430
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-10.en.html

@@ -1,430 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Tokyo, Five Days — Lieflat Report Template · Palm Slow Journal</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;600;700;900&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 10 · TRAVEL NOTEBOOK
-     场景:个人旅行/生活复盘一页(博客年终、旅行记录、生活数据随笔)
-     排版参考:图9 "Tales of San Francisco"(Gregory Wood)——粗压缩
-               衬线刊头 + 左侧色阶签、编号方块节标题、故事与统计混排的
-               三栏节奏、行间小图表 —— 只学排版和字体。
-     色板正本 = color-presets.js 的 PALM 一套(暖纸 + 深绿 + 琥珀,
-     气质最接近原图的米纸黑墨)。
-     图表正本 = lieflat gallery:
-       图A  F5  tick rows   · basics-gallery.html(开销) "Where the yen went"
-       图B  F1  rung bars   · basics-gallery.html(咖啡馆访问) "How many cups it took"
-       图C  F4  tick donut  · basics-gallery.html(饮品构成) "Twenty-three cups, four ways"
-       图D  F1  rung bars   · basics-gallery.html(餐食类型) "Five days of eating, sorted"
-       表格为纯排字,不占图额度。
-     彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.40); --quiet:rgba(88,64,46,.15); --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#77835A; --hero:#D4A017; --faintdata:#ACAD79; --wheat:#F2D17E;
-    --slab:'Roboto Slab',Georgia,serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 28px}
-  .sheet{width:980px;max-width:980px}
-
-  /* ── 刊头:色阶签 + 粗衬线大题(学图9)── */
-  .mast{display:grid;grid-template-columns:64px 1fr;gap:20px;align-items:end}
-  .chips{display:flex;flex-direction:column;gap:4px;padding-bottom:8px}
-  .chips span{width:16px;height:16px}
-  .mast .kicker{font-family:var(--slab);font-size:14px;font-weight:600;
-    letter-spacing:.22em;text-transform:uppercase;color:var(--mut)}
-  .mast h1{font-family:var(--slab);font-size:74px;font-weight:900;line-height:1.0;
-    letter-spacing:-.03em;text-shadow:3px 3px 0 rgba(172,173,121,.45)}
-  .mast .rule{border-top:1px solid var(--txt);margin-top:16px}
-
-  .lede{font-family:var(--slab);font-size:16px;font-weight:400;line-height:1.75;
-    max-width:560px;margin:28px 0 0}
-  .lede b{font-weight:700;color:var(--data)}
-
-  .hr{border-top:1px solid var(--quiet);margin:40px 0 0}
-
-  /* ── 编号节(学图9 的黑方块编号)── */
-  .row{display:grid;gap:36px;margin-top:38px}
-  .r3{grid-template-columns:1fr 1fr 1fr}
-  .r21{grid-template-columns:1fr 1.4fr 1fr}
-  .r2{grid-template-columns:1.2fr 1fr}
-  .sec h2{font-family:var(--slab);font-size:19px;font-weight:700;letter-spacing:.06em;
-    line-height:1.25;display:flex;align-items:center;gap:9px;margin-bottom:12px}
-  .sec h2 .n{background:var(--data);color:var(--bg);font-size:11px;font-weight:700;
-    width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;
-    flex:none}
-  .sec p{font-size:11px;line-height:1.75;color:var(--lab)}
-  .sec p+p{margin-top:8px}
-  .sec p b{color:var(--txt)}
-  .sec .names{font-size:9.5px;font-weight:700;letter-spacing:.14em;color:var(--mut);
-    margin-top:12px}
-  .subttl{font-size:10px;font-weight:700;color:var(--mut);margin-bottom:6px}
-
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter',sans-serif}
-  .srcline{font-size:8.5px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
-    color:var(--faint);margin-top:6px}
-
-  /* ── 数字小表(学图9 的行程表)── */
-  table.log{width:100%;border-collapse:collapse}
-  table.log th{font-size:8px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
-    color:var(--mut);text-align:right;padding:0 0 7px;border-bottom:1px solid var(--txt)}
-  table.log th:first-child{text-align:left}
-  table.log td{font-size:10.5px;padding:6.5px 0;border-bottom:1px dotted var(--quiet);
-    text-align:right;font-weight:600}
-  table.log td:first-child{text-align:left;color:var(--lab);font-weight:700;
-    font-size:9px;letter-spacing:.1em}
-  table.log tr.total td{border-bottom:0;border-top:1px solid var(--txt);
-    font-weight:800;color:var(--data)}
-  table.log td.hot{color:var(--hero);font-weight:800}
-
-  /* ── 引文卡 ── */
-  .drink{border:1px solid var(--faint);padding:16px 18px;font-size:10.5px;line-height:1.8}
-  .drink .nm{font-family:var(--slab);font-weight:700;font-size:13px;margin-bottom:4px}
-  .drink b{color:var(--data)}
-  .drink .tag{display:inline-block;background:var(--wheat);padding:1px 8px;
-    font-size:9px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin-top:8px}
-
-  .foot{margin-top:56px;padding-top:12px;border-top:1px solid var(--txt);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.12em;text-transform:uppercase;color:var(--mut)}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 刊头 ══ -->
-  <div class="mast">
-    <div class="chips">
-      <span style="background:var(--wheat)"></span>
-      <span style="background:var(--faintdata)"></span>
-      <span style="background:var(--data2)"></span>
-      <span style="background:var(--data)"></span>
-    </div>
-    <div>
-      <div class="kicker">Notes · Tales Of</div>
-      <h1>Tokyo</h1>
-      <div class="rule"></div>
-    </div>
-  </div>
-
-  <p class="lede">My fourth trip to my favorite city, May 31 to June 4, 2026.
-    This page records it with the two best things humans have for that: <b>stories, and statistics</b>. (The numbers are demo data.)</p>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第一排:主食 / 人物 / 开销 ══ -->
-  <div class="row r3">
-    <div class="sec">
-      <h2><span class="n">1</span>Ramen</h2>
-      <p>Going to Japan without a bowl of late-night ramen doesn't count as going. Three
-        shops in five days, and the best bowl was at an unnamed counter in
-        <b>Nakameguro</b> — one in the morning, six stools, tonkotsu broth thick enough
-        to coat the spoon. A bottle of Ramune on the side. I'll be back.</p>
-    </div>
-    <div class="sec">
-      <h2><span class="n">2</span>Cast</h2>
-      <p>The people you meet for the first time on a trip are usually the ones you hope
-        to know for years. This lot included, but wasn't limited to:</p>
-      <div class="names">Archer · Ono · Yuki · Chen Zhou · Sato · Leon</div>
-    </div>
-    <div class="sec">
-      <h2><span class="n">3</span>Spend</h2>
-      <!-- 图A · F5 tick rows:一格 = ¥2,000 -->
-      <div class="fig"><svg id="spend" viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Tick rows · one tick = ¥2,000 · amber = biggest line</div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第二排:咖啡因(三联,学图9 ALCOHOL 节)══ -->
-  <div class="row r21">
-    <div class="sec">
-      <h2><span class="n">4</span>Caffeine</h2>
-      <div class="subttl">Café visits</div>
-      <!-- 图B · F1 rung bars:一档 = 一次到访 -->
-      <div class="fig"><svg id="cafes" viewBox="0 0 260 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Rung bars · one rung = one visit</div>
-    </div>
-    <div class="sec">
-      <h2 style="visibility:hidden"><span class="n">·</span>Placeholder</h2>
-      <div class="subttl">Five days of drinking (cups)</div>
-      <!-- 图C · F4 tick donut:一格 = 一杯 -->
-      <div class="fig"><svg id="drinks" viewBox="0 0 300 210" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Tick donut · one tick = one cup · 23 cups total</div>
-    </div>
-    <div class="sec">
-      <h2 style="visibility:hidden"><span class="n">·</span>Placeholder</h2>
-      <div class="subttl">Best cup of the trip</div>
-      <div class="drink">
-        <div class="nm">Fuglen · Kenya pour-over</div>
-        Friends dragged me to Fuglen right after I gave my first speech in Japanese
-        in public. After a heavy night, it's always the first cup the next morning
-        you remember best — this one was no exception.
-        <div class="tag">Light roast · bright acidity · ¥750</div>
-      </div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第三排:神保町(文)/ 移动(表)══ -->
-  <div class="row r2">
-    <div class="sec">
-      <h2><span class="n">5</span>Jimbocho</h2>
-      <p>Riding through the secondhand book district and back, all I could think about
-        was how many books one street can hold. The fog of old paper made my head
-        spin a little, but between looking up and looking down, that <b>faint
-        dizziness</b> mixed with the evening crosswind and left me unusually alert,
-        pedaling through unfamiliar alleys.</p>
-      <p>The sky cleared on the way back, and a proper Tokyo sunset erased the whole
-        damp, grey afternoon. Everything felt right again; a beautiful, highly
-        engineered modern street. I've never had such a contradictory experience on
-        a single block.</p>
-    </div>
-    <div class="sec">
-      <h2><span class="n">6</span>On the Move</h2>
-      <table class="log">
-        <tr><th>Day</th><th>Walk km</th><th>Subway km</th><th>Taxi km</th></tr>
-        <tr><td>Sun</td><td>8.2</td><td>—</td><td>15.8</td></tr>
-        <tr><td>Mon</td><td>12.4</td><td>17.9</td><td>—</td></tr>
-        <tr><td>Tue</td><td class="hot">16.3</td><td>6.1</td><td>—</td></tr>
-        <tr><td>Wed</td><td>9.2</td><td>22.4</td><td>1.7</td></tr>
-        <tr><td>Thu</td><td>5.8</td><td>—</td><td>16.1</td></tr>
-        <tr class="total"><td>Total</td><td>51.9</td><td>46.4</td><td>33.6</td></tr>
-      </table>
-      <div class="srcline" style="margin-top:8px">Set in type · not counted against the chart count · amber = single-day walking peak</div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第四排:天台(文)/ 食物(图)══ -->
-  <div class="row r2">
-    <div class="sec">
-      <h2><span class="n">7</span>Rooftop</h2>
-      <p>The rooftop at the place I was staying looked out over Yoyogi Park, sun going
-        down behind the treeline. On the last night I climbed the ladder with two
-        vending-machine yuzu sodas and watched the day die slowly. <b>A quiet moment
-        like that would be boring in a film</b>, and I'm glad I didn't try to film it.</p>
-    </div>
-    <div class="sec">
-      <h2><span class="n">8</span>Food</h2>
-      <div class="subttl">Restaurant types (visits)</div>
-      <!-- 图D · F1 rung bars:一档 = 一次 -->
-      <div class="fig"><svg id="food" viewBox="0 0 300 170" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">Rung bars · one rung = one meal</div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 收尾 ══ -->
-  <div class="row" style="grid-template-columns:1fr 1fr">
-    <div class="sec">
-      <h2><span class="n">9</span>On My Mind</h2>
-      <p>This is my fourth trip to Tokyo, and every time I say the same thing to anyone
-        who'll listen: "Someday I'm going to live in this city for a year." To me
-        it's the perfect mix of good food, unhurried streets, and a kind of
-        <b>restrained liveliness</b>.</p>
-    </div>
-    <div class="sec">
-      <h2 style="visibility:hidden"><span class="n">·</span>Placeholder</h2>
-      <p>This time I said it to myself again in the night wind at Hamamatsucho. Maybe
-        the back half of my thirties should happen here. <b>We'll see.</b></p>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>Tokyo, five days · June 2026</span>
-    <span>Stories + stats · demo data · lieflat charts</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PALM preset(color-presets.js 原值)── */
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',
-      FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#77835A',HERO='#D4A017',FAINTDATA='#ACAD79';
-const CAT4=['#43593B','#77835A','#ACAD79','#F2D17E'];
-
-/* ════ 图A · F5 tick rows(开销)════
-   一格 = ¥2,000。琥珀只给最大项。加墨 1.6 / 地板 .85。 */
-(()=>{
-const D=[['FOOD',16],['LODGING',13],['BOOKS',7],['TRANSIT',5],['MISC',3]];
-obsReveal('spend',s=>{
-  const y0=i=>26+i*30,X0=64,PX=8.4;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:56,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'end',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+7,x2:X0+17*PX,y2:y+7,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=8+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+7,x2:x,y2:y+7-h,stroke:hero?HERO:DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+10.5,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,
-      fill:hero?HERO:DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},
-      '¥'+(v*2)+'k');
-    tip(lab,`${name} — ¥${(v*2000).toLocaleString()}`);
-  });
-  txt(s,{x:130,y:172,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE TICK = ¥2,000 · DOT MARKS EVERY FIFTH');
-});
-})();
-
-/* ════ 图B · F1 rung bars(咖啡馆访问)════
-   一档 = 一次到访。琥珀给去得最多的那家。 */
-(()=>{
-const D=[['FUGLEN',5],['BLUE BOTTLE',2],['AMBER CAFÉ',2],['KONBINI',7]];
-obsReveal('cafes',s=>{
-  const x0=i=>44+i*60,base=142,step=9.5,HW=15;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i),hero=i===3;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:hero?HERO:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.03}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-8,'font-size':10.5,'font-weight':800,
-      fill:hero?HERO:DATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — ${v} visits`);
-    txt(s,{x,y:base+14,'font-size':7,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.08em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:16,y1:base+4,x2:244,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:130,y:180,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = ONE VISIT · THE KONBINI WON');
-});
-})();
-
-/* ════ 图C · F4 tick donut(饮品构成)════
-   一格 = 一杯,共 23 杯。PALM CAT 分段。加墨 1.8 / 引线 1.3。 */
-(()=>{
-const D=[['POUR-OVER',9,CAT4[0]],['LATTE',6,CAT4[1]],['CANNED',5,CAT4[2]],['MATCHA',3,'#D4A017']];
-obsReveal('drinks',s=>{
-  const cx=150,cy=100,R0=46,TOT=23;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx/TOT*360-90;
-      const len=9+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.8,opacity:.9,
-        class:'fade',style:`animation-delay:${idx*.03}s`});
-    }
-    const mid=(k0+v/2)/TOT*360-90,[lx,ly]=pol(cx,cy,R0+34,mid),[gx,gy]=pol(cx,cy,R0+19,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINTDATA,'stroke-width':1.3,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.06em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} — ${v} cups`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-1,'font-size':19,'font-weight':800,fill:DATA,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'23');
-  txt(s,{x:cx,y:cy+13,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},'CUPS · FIVE DAYS');
-  txt(s,{x:150,y:202,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:1.1s'},
-    'ONE TICK = ONE CUP · READS CLOCKWISE FROM TWELVE');
-});
-})();
-
-/* ════ 图D · F1 rung bars(餐馆类型)════
-   一档 = 一顿。琥珀给次数最多的类型。 */
-(()=>{
-const D=[['RAMEN',4],['IZAKAYA',3],['BREAKFAST',3],['CURRY',2],['SUSHI',2],['OTHER',3]];
-obsReveal('food',s=>{
-  const x0=i=>36+i*46,base=124,step=13,HW=12;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i),hero=i===0;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:hero?HERO:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.04}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-      fill:hero?HERO:DATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} — ${v} meals`);
-    txt(s,{x,y:base+14,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.06em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:12,y1:base+4,x2:288,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:150,y:160,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.12em',class:'fade',style:'animation-delay:.9s'},
-    'ONE RUNG = ONE MEAL · FOUR BOWLS OF RAMEN, NO REGRETS');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 423
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-10.zh.html

@@ -1,423 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>东京五日手记 — Lieflat 报告模板 · 椰林慢刊</title>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Roboto+Slab:wght@400;600;700;900&family=Inter:wght@400;500;600;700;800&family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@400;500;700&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 10 · 旅行手记(椰林慢刊)
-     场景:个人旅行/生活复盘一页(博客年终、旅行记录、生活数据随笔)
-     排版参考:图9 "Tales of San Francisco"(Gregory Wood)——粗压缩
-               衬线刊头 + 左侧色阶签、编号方块节标题、故事与统计混排的
-               三栏节奏、行间小图表 —— 只学排版和字体。
-     色板正本 = color-presets.js 的 PALM 一套(暖纸 + 深绿 + 琥珀,
-     气质最接近原图的米纸黑墨)。
-     图表正本 = lieflat gallery:
-       图A  F5  tick rows   · basics-gallery.html(开销)
-       图B  F1  rung bars   · basics-gallery.html(咖啡馆访问)
-       图C  F4  tick donut  · basics-gallery.html(饮品构成)
-       图D  F1  rung bars   · basics-gallery.html(餐食类型)
-       表格为纯排字,不占图额度。
-     彩色加墨(INK_BOOST):发丝 ×1.8,透明度地板 .85。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    --bg:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.40); --quiet:rgba(88,64,46,.15); --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#77835A; --hero:#D4A017; --faintdata:#ACAD79; --wheat:#F2D17E;
-    --slab:'Roboto Slab','Noto Serif SC',serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--bg);color:var(--txt);font-family:'Inter','Noto Sans SC',sans-serif;
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 28px}
-  .sheet{width:980px;max-width:980px}
-
-  /* ── 刊头:色阶签 + 粗衬线大题(学图9)── */
-  .mast{display:grid;grid-template-columns:64px 1fr;gap:20px;align-items:end}
-  .chips{display:flex;flex-direction:column;gap:4px;padding-bottom:8px}
-  .chips span{width:16px;height:16px}
-  .mast .kicker{font-family:var(--slab);font-size:14px;font-weight:600;
-    letter-spacing:.22em;color:var(--mut)}
-  .mast h1{font-family:var(--slab);font-size:74px;font-weight:900;line-height:1.02;
-    letter-spacing:.01em;text-shadow:3px 3px 0 rgba(172,173,121,.45)}
-  .mast .rule{border-top:1px solid var(--txt);margin-top:16px}
-
-  .lede{font-family:var(--slab);font-size:16px;font-weight:400;line-height:1.85;
-    max-width:560px;margin:28px 0 0}
-  .lede b{font-weight:700;color:var(--data)}
-
-  .hr{border-top:1px solid var(--quiet);margin:40px 0 0}
-
-  /* ── 编号节(学图9 的黑方块编号)── */
-  .row{display:grid;gap:36px;margin-top:38px}
-  .r3{grid-template-columns:1fr 1fr 1fr}
-  .r21{grid-template-columns:1fr 1.4fr 1fr}
-  .r2{grid-template-columns:1.2fr 1fr}
-  .sec h2{font-family:var(--slab);font-size:19px;font-weight:700;letter-spacing:.06em;
-    display:flex;align-items:center;gap:9px;margin-bottom:12px}
-  .sec h2 .n{background:var(--data);color:var(--bg);font-size:11px;font-weight:700;
-    width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;
-    flex:none}
-  .sec p{font-size:11px;line-height:1.95;color:var(--lab)}
-  .sec p+p{margin-top:8px}
-  .sec p b{color:var(--txt)}
-  .sec .names{font-size:9.5px;font-weight:700;letter-spacing:.14em;color:var(--mut);
-    margin-top:12px}
-  .subttl{font-size:10px;font-weight:700;color:var(--mut);margin-bottom:6px}
-
-  .fig svg{width:100%;height:auto;display:block}
-  svg text{font-family:'Inter','Noto Sans SC',sans-serif}
-  .srcline{font-size:8.5px;font-weight:600;letter-spacing:.1em;color:var(--faint);margin-top:6px}
-
-  /* ── 数字小表(学图9 的行程表)── */
-  table.log{width:100%;border-collapse:collapse}
-  table.log th{font-size:8.5px;font-weight:700;letter-spacing:.12em;color:var(--mut);
-    text-align:right;padding:0 0 7px;border-bottom:1px solid var(--txt)}
-  table.log th:first-child{text-align:left}
-  table.log td{font-size:10.5px;padding:6.5px 0;border-bottom:1px dotted var(--quiet);
-    text-align:right;font-weight:600}
-  table.log td:first-child{text-align:left;color:var(--lab);font-weight:700;
-    font-size:9px;letter-spacing:.1em}
-  table.log tr.total td{border-bottom:0;border-top:1px solid var(--txt);
-    font-weight:800;color:var(--data)}
-  table.log td.hot{color:var(--hero);font-weight:800}
-
-  /* ── 引文卡 ── */
-  .drink{border:1px solid var(--faint);padding:16px 18px;font-size:10.5px;line-height:1.8}
-  .drink .nm{font-family:var(--slab);font-weight:700;font-size:13px;margin-bottom:4px}
-  .drink b{color:var(--data)}
-  .drink .tag{display:inline-block;background:var(--wheat);padding:1px 8px;
-    font-size:9px;font-weight:700;margin-top:8px}
-
-  .foot{margin-top:56px;padding-top:12px;border-top:1px solid var(--txt);
-    display:flex;justify-content:space-between;font-size:9px;font-weight:600;
-    letter-spacing:.14em;color:var(--mut)}
-
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <!-- ══ 刊头 ══ -->
-  <div class="mast">
-    <div class="chips">
-      <span style="background:var(--wheat)"></span>
-      <span style="background:var(--faintdata)"></span>
-      <span style="background:var(--data2)"></span>
-      <span style="background:var(--data)"></span>
-    </div>
-    <div>
-      <div class="kicker">手记 · TALES OF</div>
-      <h1>东京五日</h1>
-      <div class="rule"></div>
-    </div>
-  </div>
-
-  <p class="lede">第四次去这座我最喜欢的城市,2026 年 5 月 31 日到 6 月 4 日。
-    这页手记用人类最好的两样东西记录这趟旅行:<b>故事,和统计</b>。(数字为演示数据)</p>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第一排:主食 / 人物 / 开销 ══ -->
-  <div class="row r3">
-    <div class="sec">
-      <h2><span class="n">1</span>主食</h2>
-      <p>到日本不吃一碗深夜拉面等于没来。五天吃了三家,最好的一碗在
-        <b>中目黑的一家无名小店</b>——凌晨一点,柜台六个座位,
-        豚骨汤浓到挂勺。配一瓶波子汽水,我肯定还会回去。</p>
-    </div>
-    <div class="sec">
-      <h2><span class="n">2</span>人物</h2>
-      <p>旅途中第一次见面的人,多半希望能成为很久的朋友。
-        这群可爱的人包括(但不限于)以下几位:</p>
-      <div class="names">阿彻 · 小野 · YUKI · 陈舟 · 佐藤 · LEON</div>
-    </div>
-    <div class="sec">
-      <h2><span class="n">3</span>开销</h2>
-      <!-- 图A · F5 tick rows:一格 = ¥2,000 -->
-      <div class="fig"><svg id="spend" viewBox="0 0 260 180" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">TICK ROWS · 一格 = ¥2,000 · 琥珀 = 最大项</div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第二排:咖啡因(三联,学图9 ALCOHOL 节)══ -->
-  <div class="row r21">
-    <div class="sec">
-      <h2><span class="n">4</span>咖啡因</h2>
-      <div class="subttl">咖啡馆访问分布</div>
-      <!-- 图B · F1 rung bars:一档 = 一次到访 -->
-      <div class="fig"><svg id="cafes" viewBox="0 0 260 190" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">RUNG BARS · 一档 = 一次到访</div>
-    </div>
-    <div class="sec">
-      <h2 style="visibility:hidden"><span class="n">·</span>占位</h2>
-      <div class="subttl">五天喝了什么(杯)</div>
-      <!-- 图C · F4 tick donut:一格 = 一杯 -->
-      <div class="fig"><svg id="drinks" viewBox="0 0 300 210" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">TICK DONUT · 一格 = 一杯 · 共 23 杯</div>
-    </div>
-    <div class="sec">
-      <h2 style="visibility:hidden"><span class="n">·</span>占位</h2>
-      <div class="subttl">本趟最佳一杯</div>
-      <div class="drink">
-        <div class="nm">富谷 · 手冲肯尼亚</div>
-        在第一次当众讲完日语之后,朋友们把我拖去了 FUGLEN。
-        大多数沉重的夜晚过后,第一杯总是被记得最清楚——这杯也不例外。
-        <div class="tag">浅烘 · 果酸 · ¥750</div>
-      </div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第三排:神保町(文)/ 移动(表)══ -->
-  <div class="row r2">
-    <div class="sec">
-      <h2><span class="n">5</span>神保町</h2>
-      <p>骑车穿过旧书街再折回来的路上,满脑子都是这条街装下的书的数量。
-        雾一样的旧纸味让人晕,但抬头或低头之间,那种<b>轻微的眩晕感</b>
-        混着傍晚的横风,让我在陌生的巷子里骑得格外清醒。</p>
-      <p>回程时天放晴了,一场典型的东京落日把潮湿阴沉的下午整个擦掉。
-        一切又都对了;一条美丽、高度技术化的现代街道。
-        我从来没在一条街上有过如此矛盾的体验。</p>
-    </div>
-    <div class="sec">
-      <h2><span class="n">6</span>移动</h2>
-      <table class="log">
-        <tr><th>日</th><th>步行 KM</th><th>地铁 KM</th><th>出租 KM</th></tr>
-        <tr><td>周日</td><td>8.2</td><td>—</td><td>15.8</td></tr>
-        <tr><td>周一</td><td>12.4</td><td>17.9</td><td>—</td></tr>
-        <tr><td>周二</td><td class="hot">16.3</td><td>6.1</td><td>—</td></tr>
-        <tr><td>周三</td><td>9.2</td><td>22.4</td><td>1.7</td></tr>
-        <tr><td>周四</td><td>5.8</td><td>—</td><td>16.1</td></tr>
-        <tr class="total"><td>合计</td><td>51.9</td><td>46.4</td><td>33.6</td></tr>
-      </table>
-      <div class="srcline" style="margin-top:8px">纯排字表格 · 不占图额度 · 琥珀 = 单日步行峰值</div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 第四排:天台(文)/ 食物(图)══ -->
-  <div class="row r2">
-    <div class="sec">
-      <h2><span class="n">7</span>天台</h2>
-      <p>住处的天台能看到代代木公园的树冠后面沉下去的太阳。
-        最后一晚,我揣着两瓶自动贩卖机的柚子气泡水爬上梯子,
-        看着白天一点点死掉。<b>这样安静的时刻放到电影里会很无聊</b>,
-        幸好我没有试图拍下来。</p>
-    </div>
-    <div class="sec">
-      <h2><span class="n">8</span>食物</h2>
-      <div class="subttl">餐馆类型分布(次)</div>
-      <!-- 图D · F1 rung bars:一档 = 一次 -->
-      <div class="fig"><svg id="food" viewBox="0 0 300 170" preserveAspectRatio="xMidYMid meet"></svg></div>
-      <div class="srcline">RUNG BARS · 一档 = 一顿</div>
-    </div>
-  </div>
-
-  <div class="hr"></div>
-
-  <!-- ══ 收尾 ══ -->
-  <div class="row" style="grid-template-columns:1fr 1fr">
-    <div class="sec">
-      <h2><span class="n">9</span>心事</h2>
-      <p>这是我第四次来东京,每一次我都对愿意听的人说同一句话:
-        「总有一天我要在这座城市住一年。」对我来说,这里是
-        好吃的食物、松弛的街道和一种<b>克制的热闹</b>的完美混合物。</p>
-    </div>
-    <div class="sec">
-      <h2 style="visibility:hidden"><span class="n">·</span>占位</h2>
-      <p>这趟在滨松町的夜风里,我又把这句话对自己说了一遍。
-        也许三十岁的后半段就该发生在这里。<b>让我们看看。</b></p>
-    </div>
-  </div>
-
-  <div class="foot">
-    <span>东京五日手记 · 2026 年 6 月</span>
-    <span>故事 + 统计 · 演示数据 · LIEFLAT CHARTS</span>
-  </div>
-</div>
-
-<script>
-/* ── mono-tokens 内联(几何/工具函数与正本一致)── */
-const NS='http://www.w3.org/2000/svg';
-const el=(p,t,a)=>{const n=document.createElementNS(NS,t);for(const k in a)n.setAttribute(k,a[k]);p.appendChild(n);return n};
-const txt=(p,a,s)=>{const n=el(p,'text',a);n.textContent=s;return n};
-const tip=(n,s)=>{const t=document.createElementNS(NS,'title');t.textContent=s;n.appendChild(t)};
-const rnd=(i,k)=>Math.abs(((i*73856093)^(k*19349663))%1000)/1000;
-const D2R=Math.PI/180;
-const pol=(cx,cy,r,deg)=>[cx+r*Math.cos(deg*D2R),cy+r*Math.sin(deg*D2R)];
-const obsReveal=(id,fn)=>{
-  const n=document.getElementById(id);
-  const go=()=>{n.innerHTML='';fn(n)};
-  const io=new IntersectionObserver(es=>{if(es[0].isIntersecting){go();io.disconnect()}},{threshold:.3});
-  io.observe(n); n.style.cursor='pointer'; n.addEventListener('click',go);
-};
-
-/* ── PALM preset(color-presets.js 原值)── */
-const BG='#F0EFEB',TXT='#58402E',MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',
-      FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#77835A',HERO='#D4A017',FAINTDATA='#ACAD79';
-const CAT4=['#43593B','#77835A','#ACAD79','#F2D17E'];
-
-/* ════ 图A · F5 tick rows(开销)════
-   一格 = ¥2,000。琥珀只给最大项。加墨 1.6 / 地板 .85。 */
-(()=>{
-const D=[['餐食',16],['住宿',13],['书和唱片',7],['交通',5],['杂物',3]];
-obsReveal('spend',s=>{
-  const y0=i=>26+i*30,X0=64,PX=8.4;
-  D.forEach(([name,v],i)=>{
-    const y=y0(i),hero=i===0;
-    txt(s,{x:56,y:y+3,'font-size':8,'font-weight':700,fill:MUT,'text-anchor':'end',
-      class:'fade',style:`animation-delay:${i*.08}s`},name);
-    el(s,'line',{x1:X0,y1:y+7,x2:X0+17*PX,y2:y+7,stroke:GRID,'stroke-width':.7,
-      class:'fade',style:`animation-delay:${i*.08}s`});
-    for(let k=0;k<v;k++){
-      const x=X0+k*PX+PX/2,h=8+rnd(k+1,i+2)*5;
-      el(s,'line',{x1:x,y1:y+7,x2:x,y2:y+7-h,stroke:hero?HERO:DATA,'stroke-width':1.6,
-        opacity:.85+rnd(k+3,i+5)*.15,class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-      if(k%5===4)el(s,'circle',{cx:x,cy:y+10.5,r:.9,fill:FAINTDATA,
-        class:'fade',style:`animation-delay:${i*.08+k*.012}s`});
-    }
-    const lab=txt(s,{x:X0+v*PX+8,y:y+3,'font-size':9.5,'font-weight':800,
-      fill:hero?HERO:DATA,class:'fade',style:`animation-delay:${.4+i*.08}s`},
-      '¥'+(v*2/10).toFixed(1)+'万');
-    tip(lab,`${name} —— ¥${(v*2000).toLocaleString()}`);
-  });
-  txt(s,{x:130,y:172,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一格 = ¥2,000 · 每五格一个圆点');
-});
-})();
-
-/* ════ 图B · F1 rung bars(咖啡馆访问)════
-   一档 = 一次到访。琥珀给去得最多的那家。 */
-(()=>{
-const D=[['FUGLEN',5],['蓝瓶',2],['琥珀咖啡',2],['便利店',7]];
-obsReveal('cafes',s=>{
-  const x0=i=>44+i*60,base=142,step=9.5,HW=15;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i),hero=i===3;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:hero?HERO:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.03}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-8,'font-size':10.5,'font-weight':800,
-      fill:hero?HERO:DATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} —— ${v} 次`);
-    txt(s,{x,y:base+14,'font-size':7,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      'letter-spacing':'.04em',class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:16,y1:base+4,x2:244,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:130,y:180,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一档 = 一次到访 · 便利店赢了');
-});
-})();
-
-/* ════ 图C · F4 tick donut(饮品构成)════
-   一格 = 一杯,共 23 杯。PALM CAT 分段。加墨 1.8 / 引线 1.3。 */
-(()=>{
-const D=[['手冲',9,CAT4[0]],['拿铁',6,CAT4[1]],['罐装',5,CAT4[2]],['抹茶',3,'#D4A017']];
-obsReveal('drinks',s=>{
-  const cx=150,cy=100,R0=46,TOT=23;
-  let k0=0;
-  D.forEach(([name,v,shade],si)=>{
-    for(let k=0;k<v;k++){
-      const idx=k0+k,a=idx/TOT*360-90;
-      const len=9+rnd(idx+1,si+2)*6;
-      const [x1,y1]=pol(cx,cy,R0,a),[x2,y2]=pol(cx,cy,R0+len,a);
-      el(s,'line',{x1,y1,x2,y2,stroke:shade,'stroke-width':1.8,opacity:.9,
-        class:'fade',style:`animation-delay:${idx*.03}s`});
-    }
-    const mid=(k0+v/2)/TOT*360-90,[lx,ly]=pol(cx,cy,R0+34,mid),[gx,gy]=pol(cx,cy,R0+19,mid);
-    el(s,'line',{x1:gx,y1:gy,x2:lx,y2:ly,stroke:FAINTDATA,'stroke-width':1.3,
-      'stroke-dasharray':'1 3',class:'fade',style:`animation-delay:${.6+si*.1}s`});
-    const anchor=Math.cos(mid*D2R)>0.3?'start':Math.cos(mid*D2R)<-0.3?'end':'middle';
-    const lab=txt(s,{x:lx,y:ly+3,'font-size':8,'font-weight':800,fill:shade,'text-anchor':anchor,
-      'letter-spacing':'.04em',style:`paint-order:stroke;stroke:${BG};stroke-width:3px;animation-delay:${.65+si*.1}s`,
-      class:'fade'},`${name} · ${v}`);
-    tip(lab,`${name} —— ${v} 杯`);
-    k0+=v;
-  });
-  txt(s,{x:cx,y:cy-1,'font-size':19,'font-weight':800,fill:DATA,'text-anchor':'middle',
-    class:'fade',style:'animation-delay:.9s'},'23');
-  txt(s,{x:cx,y:cy+13,'font-size':7,'font-weight':600,fill:MUT,'text-anchor':'middle',
-    'letter-spacing':'.08em',class:'fade',style:'animation-delay:.9s'},'杯 · 五天');
-  txt(s,{x:150,y:202,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:1.1s'},
-    '一格 = 一杯 · 顺时针从十二点读');
-});
-})();
-
-/* ════ 图D · F1 rung bars(餐馆类型)════
-   一档 = 一顿。琥珀给次数最多的类型。 */
-(()=>{
-const D=[['拉面',4],['居酒屋',3],['早餐店',3],['咖喱',2],['寿司',2],['其他',3]];
-obsReveal('food',s=>{
-  const x0=i=>36+i*46,base=124,step=13,HW=12;
-  D.forEach(([name,v],i)=>{
-    const x=x0(i),hero=i===0;
-    for(let k=0;k<v;k++){
-      const y=base-k*step,w=HW-1.5+rnd(k+1,i+2)*3;
-      el(s,'line',{x1:x-w,y1:y,x2:x+w,y2:y,stroke:hero?HERO:DATA,'stroke-width':1.8,
-        opacity:.85+rnd(k+2,i+4)*.15,class:'fade',style:`animation-delay:${i*.08+k*.04}s`});
-    }
-    const topY=base-(v-1)*step;
-    const num=txt(s,{x,y:topY-8,'font-size':10,'font-weight':800,
-      fill:hero?HERO:DATA,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${.4+i*.08}s`},v);
-    tip(num,`${name} —— ${v} 顿`);
-    txt(s,{x,y:base+14,'font-size':7.5,'font-weight':700,fill:MUT,'text-anchor':'middle',
-      class:'fade',style:`animation-delay:${i*.08}s`},name);
-  });
-  el(s,'line',{x1:12,y1:base+4,x2:288,y2:base+4,stroke:GRID,'stroke-width':.8,class:'fade'});
-  txt(s,{x:150,y:160,'font-size':7,'font-weight':600,fill:FAINT,'text-anchor':'middle',
-    'letter-spacing':'.1em',class:'fade',style:'animation-delay:.9s'},
-    '一档 = 一顿 · 拉面 4 顿,没有遗憾');
-});
-})();
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=980;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 182
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-11.en.html

@@ -1,182 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%231C1C1A'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23F0EFEB'/%3E%3C/svg%3E">
-  <title>Research Brief Card — Lieflat Report Template · Mono Fixed Frame</title>
-  <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-  <script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-  <style>
-    /* ═══════════════════════════════════════════════════════════════
-       LIEFLAT 报告模板 11 · 研究简报卡(灰阶定尺)· 英文版
-       场景:600×1000 定宽定高卡片,专为截图发社媒设计——单一决定性
-             结论 + 两张证据图 + 一条 takeaway,读者三秒扫完,不响应式
-             重排,只整卡等比缩放(见页尾 fit() 脚本)。
-       依赖说明:这是模板库里第二张挂 CDN 依赖的模板(第一张见模板 12)。
-             库里其余模板都是纯 SVG,离线可开;这张卡的两张图来自
-             Chart.js 和 ECharts,脚本标签原样照抄模板 12 / glance-
-             gallery.html,没有联网时图表区会是空白。
-       图表正本 = templates/glance-gallery.html:
-         图A  G3  Chunky Bars       (Chart.js,柱顶大字 + 按名次染色)
-         图B  G18 Draw-in + Counter (ECharts,一笔画折线 + 计数器)
-       色板正本:只用文件已有 :root 变量(纸灰/炭黑/中间灰阶),
-         不新增色值;面积渐变直接照抄 gallery 里 INK 的 rgba 写法。
-       动效:沿用 gallery 的 obsReveal(滚入播 + 点击重播),新增
-         prefers-reduced-motion 降级——原文件没有任何动画代码,现在
-         挂了两张动态图,降级开关补齐,降级时两张图直接给终态。
-       与中文版结构一致,仅可见文案换成英文;正文字号不放大以保持
-       版式不溢出(见 SKILL.md 英文视觉宽度约束)。
-       数据说明:全部为虚构演示数据,仅用于验证版式。
-       ═══════════════════════════════════════════════════════════════ */
-    :root{--paper:#F0EFEB;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6;--soft:#D8D7D1;--dark:#1C1C1A;--scale:1}
-    *{box-sizing:border-box;margin:0;padding:0} html,body{min-height:100%;background:#272725}
-    body{display:flex;justify-content:center;padding:24px;font-family:Inter,Arial,sans-serif;color:var(--ink);-webkit-font-smoothing:antialiased}
-    .stage{width:600px;height:1000px}.report{width:600px;height:1000px;overflow:hidden;background:var(--paper);transform:scale(var(--scale));transform-origin:top left}
-    .inner{height:100%;padding:30px 34px 24px;display:grid;grid-template-rows:auto auto 164px 1fr auto;gap:18px}
-    .mast{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--ink);padding-bottom:10px;font-size:9px;letter-spacing:.08em;text-transform:uppercase;font-weight:700}.mast strong{letter-spacing:0;font-size:15px}
-    h1{font-size:39px;line-height:1.05;letter-spacing:-.02em;font-weight:800;max-width:440px}.dek{font-size:12px;line-height:1.55;color:var(--muted);margin-top:10px;max-width:390px}.meta{float:right;margin-top:-34px;text-align:right;font-size:9px;line-height:1.7;color:var(--muted)}.meta b{font-size:12px;color:var(--ink)}
-    .hero{display:grid;grid-template-columns:170px 1fr;gap:22px;align-items:stretch}.hero-stat{background:var(--dark);color:var(--paper);border-radius:24px;padding:21px 22px;display:flex;flex-direction:column;justify-content:space-between}.hero-stat strong{font-size:67px;line-height:.86;font-weight:800;letter-spacing:-.04em}.hero-stat span{font-size:11px;line-height:1.45;color:#C6C5BF}.hero-copy{border-top:1px solid var(--ink);border-bottom:1px solid var(--ink);padding:19px 0 15px;display:flex;flex-direction:column;justify-content:space-between}.hero-copy h2{font-size:22px;line-height:1.25;font-weight:800;max-width:320px}.hero-copy p{font-size:11px;line-height:1.55;color:var(--muted);max-width:310px}.labels{display:flex;gap:16px;font-size:8px;letter-spacing:.08em;color:var(--muted);text-transform:uppercase}
-    .content{display:grid;grid-template-columns:1.05fr .95fr;gap:18px;min-height:0}.panel{min-height:0}.panel-title{display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid var(--ink);padding-top:11px;margin-bottom:10px}.panel-title h3{font-size:18px;line-height:1.25;font-weight:800}.panel-title span{font-size:8px;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;padding-left:8px}
-    .chart-wrap{height:218px;border-bottom:1px solid var(--grid);position:relative}.chart-wrap canvas{cursor:pointer}.note{font-size:10px;line-height:1.55;color:var(--muted);margin-top:12px}
-    .signals{display:grid;grid-template-columns:1fr 1fr;gap:2px;background:var(--grid);border-radius:16px;overflow:hidden}.signal{background:var(--paper);min-height:84px;padding:12px}.signal b{display:block;font-size:21px;line-height:1;margin-bottom:7px}.signal span{display:block;font-size:9px;line-height:1.4;color:var(--muted)}
-    .trend{margin-top:21px;background:#E5E4DE;border-radius:16px;padding:12px 13px 8px}.trend-head{display:flex;justify-content:space-between;align-items:baseline;font-size:9px;color:var(--muted);margin-bottom:8px}.trend-chart{display:block;width:100%;height:104px;cursor:pointer}.fatigue-count{font-size:11px;font-weight:800;color:var(--ink)}.fatigue-count small{font-size:8px;font-weight:600;color:var(--muted);margin-left:1px}
-    .takeaway{border-top:1px solid var(--ink);padding-top:14px;display:grid;grid-template-columns:124px 1fr;gap:18px}.takeaway-label{font-size:8px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.takeaway p{font-size:15px;line-height:1.35;font-weight:800}
-    .footer{border-top:1px solid var(--grid);padding-top:10px;display:flex;justify-content:space-between;gap:20px;font-size:8px;line-height:1.5;color:var(--muted)}.demo{color:var(--ink);font-weight:700}
-    @media(max-width:640px){body{padding:0}.stage{margin:0}}
-  </style>
-</head>
-<body><div class="stage" id="stage"><main class="report"><div class="inner">
-  <header class="mast"><strong>Lieflat Charts / Mono</strong><span>Research brief · 01</span></header>
-  <section><h1>Skipping time off is becoming routine</h1><p class="dek">Why people skip earned leave, and what happens when they never disconnect.</p><p class="meta"><b>2026 SAMPLE</b><br>1,200 people · 4 markets<br>Demo</p></section>
-  <section class="hero"><div class="hero-stat"><strong>68%</strong><span>of respondents delayed a planned vacation</span></div><div class="hero-copy"><h2>Work always looks more urgent than rest.</h2><p>When leave requires nonstop coordinating, handoffs, and explaining, it's easy to push it back.</p><div class="labels"><span>availability</span><span>guilt</span><span>handover</span></div></div></section>
-  <section class="content"><div class="panel"><div class="panel-title"><h3>Average unused leave</h3><span>G3 · Chunky bars</span></div><div class="chart-wrap"><canvas id="leaveChart"></canvas></div><p class="note">A leave balance isn't rest capacity. The most unused days also mean the most trouble unplugging.</p></div><div class="panel"><div class="panel-title"><h3>Friction signals</h3><span>G18 · Draw-in</span></div><div class="signals"><div class="signal"><b>42%</b><span>still check messages while on leave</span></div><div class="signal"><b>37%</b><span>worry leave affects how others see them</span></div><div class="signal"><b>31%</b><span>cut leave short over handover costs</span></div><div class="signal"><b>26%</b><span>no 5 straight days off all year</span></div></div><div class="trend"><div class="trend-head"><span>Fatigue rises without a break</span><span class="fatigue-count"><b id="fatigueCount">0</b><small>/100</small></span></div><div class="trend-chart" id="stroke"></div></div></div></section>
-  <section class="takeaway"><div class="takeaway-label">One-page takeaway</div><p>Policy grants leave. Habits decide who feels safe taking it. Make handover and logging off routine.</p></section>
-  <footer class="footer"><span><span class="demo">Demo data, not a real survey result.</span><br>Built to test the Lieflat Charts one-page research brief template.<br>Runs on Chart.js / ECharts, blank offline.</span><span>MONO / PAPER GREY<br>RESEARCH BRIEF</span></footer>
-</div></main></div><script>
-/* ── PALETTE:直接照抄 templates/glance-gallery.html 顶部同名常量,不新增色值 ── */
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6',FAINT='#C6C5BF';
-const DPR=window.devicePixelRatio||1;
-const REDUCED=matchMedia('(prefers-reduced-motion: reduce)').matches;
-
-/* ── 统一 reveal 机制(滚入播 + 点击重播,逐字照抄 gallery 的 obsReveal/cReveal/eReveal) ── */
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el,null,{devicePixelRatio:DPR});
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUTED;
-Chart.defaults.font.family='Inter,Arial,sans-serif';
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.devicePixelRatio=DPR;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=INK;
-Chart.defaults.plugins.tooltip.titleColor='#F0EFEB';
-Chart.defaults.plugins.tooltip.bodyColor='#A5A49E';
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-/* ════ 图A · G3 Chunky Bars(移植自 glance-gallery.html "Revenue by plan") ════
-   柱端胶囊圆角 + 分段延迟入场 + 柱顶大字插件全部照搬,只把
-   $K 的取值方式换成天数,颜色按名次二值分配(前两高=INK,后两低=FAINT)。 */
-(()=>{
-const LABELS=['China','US','France','Japan'];
-const VALUES=[7,11,8,5];
-const rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);return i=>r.indexOf(i)};
-const rk=rankOf(VALUES);
-cReveal('leaveChart',()=>({
-  type:'bar',
-  data:{labels:LABELS,
-    datasets:[{data:VALUES,
-      backgroundColor:VALUES.map((_,i)=>rk(i)<2?INK:FAINT),
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:REDUCED?{duration:0}:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw+' unused days'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
-      y:{display:false,suggestedMax:16},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font='700 13px Inter,Arial,sans-serif';ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText(chart.data.datasets[0].data[i],bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-/* ════ 图B · G18 Draw-in + Counter(移植自 glance-gallery.html "H1 revenue, drawn in one stroke") ════
-   一笔画折线 + 渐变面积 + performance.now() 计数器全部照搬,只把
-   累计营收换成疲惫指数,累计终值就是计数器要数到的那个数。 */
-(()=>{
-const DAYS=Array.from({length:91},(_,i)=>String(i));
-const FATIGUE=DAYS.map(d=>Math.round(6+90*Math.pow(Number(d)/90,1.35)));
-const TOTAL=FATIGUE[FATIGUE.length-1];
-const DUR=2000;
-let gen=0;
-const countEl=document.getElementById('fatigueCount');
-obsReveal('stroke',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el,null,{devicePixelRatio:DPR});
-  g.clear();
-  gen++;const my=gen;
-  g.setOption({
-    animationDuration:REDUCED?0:DUR,animationEasing:'cubicOut',
-    tooltip:{show:false},
-    grid:{left:4,right:14,top:6,bottom:20},
-    xAxis:{type:'category',data:DAYS,boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
-      axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:8,fontWeight:600,
-        interval:(i,v)=>[0,30,60,90].includes(Number(v)),
-        formatter:v=>{const n=Number(v);return n===0?'0':n===30?'30':n===60?'60':n===90?'90D':''}}},
-    yAxis:{show:false,max:TOTAL*1.08},
-    series:[{
-      type:'line',data:FATIGUE,smooth:.2,symbol:'none',
-      lineStyle:{color:INK,width:2.2},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:'rgba(28,28,26,.22)'},{offset:1,color:'rgba(28,28,26,0)'}]}},
-    }],
-  });
-  if(REDUCED){if(countEl)countEl.textContent=TOTAL;return}
-  const t0=performance.now();
-  const tick=()=>{
-    if(my!==gen)return;
-    const p=Math.min(1,(performance.now()-t0)/DUR);
-    const eased=1-Math.pow(1-p,3);
-    if(countEl)countEl.textContent=Math.round(TOTAL*eased);
-    if(p<1)requestAnimationFrame(tick);
-  };
-  requestAnimationFrame(tick);
-});
-})();
-
-addEventListener('resize',()=>{
-  echarts.getInstanceByDom(document.getElementById('stroke'))?.resize();
-});
-</script>
-<script>function fit(){const scale=Math.min(1,innerWidth/600);document.documentElement.style.setProperty('--scale',scale);stage.style.width=600*scale+'px';stage.style.height=1000*scale+'px'}fit();addEventListener('resize',fit)</script></body></html>

+ 0 - 180
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-11.zh.html

@@ -1,180 +0,0 @@
-<!doctype html>
-<html lang="zh-CN">
-<head>
-  <meta charset="utf-8">
-  <meta name="viewport" content="width=device-width, initial-scale=1">
-  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%231C1C1A'/%3E%3Ccircle cx='16' cy='16' r='6' fill='%23F0EFEB'/%3E%3C/svg%3E">
-  <title>Mono Research Brief · Lieflat Charts</title>
-  <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-  <script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-  <style>
-    /* ═══════════════════════════════════════════════════════════════
-       LIEFLAT 报告模板 11 · 研究简报卡(灰阶定尺)
-       场景:600×1000 定宽定高卡片,专为截图发社媒设计——单一决定性
-             结论 + 两张证据图 + 一条 takeaway,读者三秒扫完,不响应式
-             重排,只整卡等比缩放(见页尾 fit() 脚本)。
-       依赖说明:这是模板库里第二张挂 CDN 依赖的模板(第一张见模板 12)。
-             库里其余模板都是纯 SVG,离线可开;这张卡的两张图来自
-             Chart.js 和 ECharts,脚本标签原样照抄模板 12 / glance-
-             gallery.html,没有联网时图表区会是空白。
-       图表正本 = templates/glance-gallery.html:
-         图A  G3  Chunky Bars       (Chart.js,柱顶大字 + 按名次染色)
-         图B  G18 Draw-in + Counter (ECharts,一笔画折线 + 计数器)
-       色板正本:只用文件已有 :root 变量(纸灰/炭黑/中间灰阶),
-         不新增色值;面积渐变直接照抄 gallery 里 INK 的 rgba 写法。
-       动效:沿用 gallery 的 obsReveal(滚入播 + 点击重播),新增
-         prefers-reduced-motion 降级——原文件没有任何动画代码,现在
-         挂了两张动态图,降级开关补齐,降级时两张图直接给终态。
-       数据说明:全部为虚构演示数据,仅用于验证版式。
-       ═══════════════════════════════════════════════════════════════ */
-    :root{--paper:#F0EFEB;--ink:#1C1C1A;--muted:#8F8E88;--faint:#C6C5BF;--grid:#DEDDD6;--soft:#D8D7D1;--dark:#1C1C1A;--scale:1}
-    *{box-sizing:border-box;margin:0;padding:0} html,body{min-height:100%;background:#272725}
-    body{display:flex;justify-content:center;padding:24px;font-family:Inter,"PingFang SC","Noto Sans SC",Arial,sans-serif;color:var(--ink);-webkit-font-smoothing:antialiased}
-    .stage{width:600px;height:1000px}.report{width:600px;height:1000px;overflow:hidden;background:var(--paper);transform:scale(var(--scale));transform-origin:top left}
-    .inner{height:100%;padding:30px 34px 24px;display:grid;grid-template-rows:auto auto 164px 1fr auto;gap:18px}
-    .mast{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--ink);padding-bottom:10px;font-size:9px;letter-spacing:.08em;text-transform:uppercase;font-weight:700}.mast strong{letter-spacing:0;font-size:15px}
-    h1{font-size:39px;line-height:1.08;letter-spacing:0;font-weight:800;max-width:440px}.dek{font-size:12px;line-height:1.65;color:var(--muted);margin-top:10px;max-width:390px}.meta{float:right;margin-top:-34px;text-align:right;font-size:9px;line-height:1.7;color:var(--muted)}.meta b{font-size:12px;color:var(--ink)}
-    .hero{display:grid;grid-template-columns:170px 1fr;gap:22px;align-items:stretch}.hero-stat{background:var(--dark);color:var(--paper);border-radius:24px;padding:21px 22px;display:flex;flex-direction:column;justify-content:space-between}.hero-stat strong{font-size:67px;line-height:.86;font-weight:800;letter-spacing:-.04em}.hero-stat span{font-size:11px;line-height:1.45;color:#C6C5BF}.hero-copy{border-top:1px solid var(--ink);border-bottom:1px solid var(--ink);padding:19px 0 15px;display:flex;flex-direction:column;justify-content:space-between}.hero-copy h2{font-size:22px;line-height:1.35;font-weight:800;max-width:320px}.hero-copy p{font-size:11px;line-height:1.55;color:var(--muted);max-width:310px}.labels{display:flex;gap:16px;font-size:8px;letter-spacing:.08em;color:var(--muted);text-transform:uppercase}
-    .content{display:grid;grid-template-columns:1.05fr .95fr;gap:18px;min-height:0}.panel{min-height:0}.panel-title{display:flex;justify-content:space-between;align-items:baseline;border-top:1px solid var(--ink);padding-top:11px;margin-bottom:10px}.panel-title h3{font-size:18px;line-height:1.25;font-weight:800}.panel-title span{font-size:8px;color:var(--muted);letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;padding-left:8px}
-    .chart-wrap{height:218px;border-bottom:1px solid var(--grid);position:relative}.chart-wrap canvas{cursor:pointer}.note{font-size:10px;line-height:1.55;color:var(--muted);margin-top:12px}
-    .signals{display:grid;grid-template-columns:1fr 1fr;gap:2px;background:var(--grid);border-radius:16px;overflow:hidden}.signal{background:var(--paper);min-height:84px;padding:12px}.signal b{display:block;font-size:21px;line-height:1;margin-bottom:7px}.signal span{display:block;font-size:9px;line-height:1.4;color:var(--muted)}
-    .trend{margin-top:21px;background:#E5E4DE;border-radius:16px;padding:12px 13px 8px}.trend-head{display:flex;justify-content:space-between;align-items:baseline;font-size:9px;color:var(--muted);margin-bottom:8px}.trend-chart{display:block;width:100%;height:104px;cursor:pointer}.fatigue-count{font-size:11px;font-weight:800;color:var(--ink)}.fatigue-count small{font-size:8px;font-weight:600;color:var(--muted);margin-left:1px}
-    .takeaway{border-top:1px solid var(--ink);padding-top:14px;display:grid;grid-template-columns:124px 1fr;gap:18px}.takeaway-label{font-size:8px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}.takeaway p{font-size:15px;line-height:1.55;font-weight:800}
-    .footer{border-top:1px solid var(--grid);padding-top:10px;display:flex;justify-content:space-between;gap:20px;font-size:8px;line-height:1.5;color:var(--muted)}.demo{color:var(--ink);font-weight:700}
-    @media(max-width:640px){body{padding:0}.stage{margin:0}}
-  </style>
-</head>
-<body><div class="stage" id="stage"><main class="report"><div class="inner">
-  <header class="mast"><strong>Lieflat Charts / Mono</strong><span>Research brief · 01</span></header>
-  <section><h1>休假不足,正在变成一种工作习惯</h1><p class="dek">一页看懂:人们为什么有假不休,以及长时间不离开工作会发生什么。</p><p class="meta"><b>2026 SAMPLE</b><br>1,200 人 · 四个市场<br>演示版</p></section>
-  <section class="hero"><div class="hero-stat"><strong>68%</strong><span>受访者至少推迟过一次原定休假</span></div><div class="hero-copy"><h2>真正的障碍不是“没有假”,而是工作永远看起来比休息更紧急。</h2><p>当休假需要反复协调、交接和解释,员工更容易把它推迟到“忙完这阵”。</p><div class="labels"><span>availability</span><span>guilt</span><span>handover</span></div></div></section>
-  <section class="content"><div class="panel"><div class="panel-title"><h3>平均未使用休假天数</h3><span>G3 · Chunky bars</span></div><div class="chart-wrap"><canvas id="leaveChart"></canvas></div><p class="note">假期余额不等于休息能力。未使用天数最高的群体,也最常报告“离开后工作会失控”。</p></div><div class="panel"><div class="panel-title"><h3>四个阻力信号</h3><span>G18 · Draw-in</span></div><div class="signals"><div class="signal"><b>42%</b><span>休假时仍频繁查看工作消息</span></div><div class="signal"><b>37%</b><span>担心休假影响他人评价</span></div><div class="signal"><b>31%</b><span>因为交接成本而缩短假期</span></div><div class="signal"><b>26%</b><span>过去一年未连续休满五天</span></div></div><div class="trend"><div class="trend-head"><span>离上次完整休假越久,疲惫感越高</span><span class="fatigue-count"><b id="fatigueCount">0</b><small>/100</small></span></div><div class="trend-chart" id="stroke"></div></div></div></section>
-  <section class="takeaway"><div class="takeaway-label">One-page takeaway</div><p>休假政策只解决“有没有”,团队习惯决定“敢不敢休”。真正有效的改善,是让交接和离线成为默认流程。</p></section>
-  <footer class="footer"><span><span class="demo">演示数据,不代表真实调查结果。</span><br>用于验证 Lieflat Charts 一页研究简报模板。<br>图表挂 Chart.js / ECharts,离线打开为空白。</span><span>MONO / PAPER GREY<br>RESEARCH BRIEF</span></footer>
-</div></main></div><script>
-/* ── PALETTE:直接照抄 templates/glance-gallery.html 顶部同名常量,不新增色值 ── */
-const INK='#1C1C1A',PAPER='#F0EFEB',MUTED='#8F8E88',GRID='#DEDDD6',FAINT='#C6C5BF';
-const DPR=window.devicePixelRatio||1;
-const REDUCED=matchMedia('(prefers-reduced-motion: reduce)').matches;
-
-/* ── 统一 reveal 机制(滚入播 + 点击重播,逐字照抄 gallery 的 obsReveal/cReveal/eReveal) ── */
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el,null,{devicePixelRatio:DPR});
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUTED;
-Chart.defaults.font.family='Inter,"PingFang SC","Noto Sans SC",Arial,sans-serif';
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.devicePixelRatio=DPR;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=INK;
-Chart.defaults.plugins.tooltip.titleColor='#F0EFEB';
-Chart.defaults.plugins.tooltip.bodyColor='#A5A49E';
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-/* ════ 图A · G3 Chunky Bars(移植自 glance-gallery.html "Revenue by plan") ════
-   柱端胶囊圆角 + 分段延迟入场 + 柱顶大字插件全部照搬,只把
-   $K 的取值方式换成天数,颜色按名次二值分配(前两高=INK,后两低=FAINT)。 */
-(()=>{
-const LABELS=['中国','美国','法国','日本'];
-const VALUES=[7,11,8,5];
-const rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);return i=>r.indexOf(i)};
-const rk=rankOf(VALUES);
-cReveal('leaveChart',()=>({
-  type:'bar',
-  data:{labels:LABELS,
-    datasets:[{data:VALUES,
-      backgroundColor:VALUES.map((_,i)=>rk(i)<2?INK:FAINT),
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:REDUCED?{duration:0}:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw+' 天未休'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
-      y:{display:false,suggestedMax:16},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font='700 13px Inter,"PingFang SC",sans-serif';ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText(chart.data.datasets[0].data[i],bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-/* ════ 图B · G18 Draw-in + Counter(移植自 glance-gallery.html "H1 revenue, drawn in one stroke") ════
-   一笔画折线 + 渐变面积 + performance.now() 计数器全部照搬,只把
-   累计营收换成疲惫指数,累计终值就是计数器要数到的那个数。 */
-(()=>{
-const DAYS=Array.from({length:91},(_,i)=>String(i));
-const FATIGUE=DAYS.map(d=>Math.round(6+90*Math.pow(Number(d)/90,1.35)));
-const TOTAL=FATIGUE[FATIGUE.length-1];
-const DUR=2000;
-let gen=0;
-const countEl=document.getElementById('fatigueCount');
-obsReveal('stroke',el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el,null,{devicePixelRatio:DPR});
-  g.clear();
-  gen++;const my=gen;
-  g.setOption({
-    animationDuration:REDUCED?0:DUR,animationEasing:'cubicOut',
-    tooltip:{show:false},
-    grid:{left:4,right:14,top:6,bottom:20},
-    xAxis:{type:'category',data:DAYS,boundaryGap:false,
-      axisLine:{show:false},axisTick:{show:false},splitLine:{show:false},
-      axisLabel:{color:MUTED,fontFamily:'Inter',fontSize:8,fontWeight:600,
-        interval:(i,v)=>[0,30,60,90].includes(Number(v)),
-        formatter:v=>{const n=Number(v);return n===0?'0':n===30?'30':n===60?'60':n===90?'90天':''}}},
-    yAxis:{show:false,max:TOTAL*1.08},
-    series:[{
-      type:'line',data:FATIGUE,smooth:.2,symbol:'none',
-      lineStyle:{color:INK,width:2.2},
-      areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
-        colorStops:[{offset:0,color:'rgba(28,28,26,.22)'},{offset:1,color:'rgba(28,28,26,0)'}]}},
-    }],
-  });
-  if(REDUCED){if(countEl)countEl.textContent=TOTAL;return}
-  const t0=performance.now();
-  const tick=()=>{
-    if(my!==gen)return;
-    const p=Math.min(1,(performance.now()-t0)/DUR);
-    const eased=1-Math.pow(1-p,3);
-    if(countEl)countEl.textContent=Math.round(TOTAL*eased);
-    if(p<1)requestAnimationFrame(tick);
-  };
-  requestAnimationFrame(tick);
-});
-})();
-
-addEventListener('resize',()=>{
-  echarts.getInstanceByDom(document.getElementById('stroke'))?.resize();
-});
-</script>
-<script>function fit(){const scale=Math.min(1,innerWidth/600);document.documentElement.style.setProperty('--scale',scale);stage.style.width=600*scale+'px';stage.style.height=1000*scale+'px'}fit();addEventListener('resize',fit)</script></body></html>

+ 0 - 412
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-12.en.html

@@ -1,412 +0,0 @@
-<!doctype html>
-<html lang="en">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>Weekly Glance — Lieflat Report Template · Palm Three Second</title>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT REPORT TEMPLATE 12 · WEEKLY GLANCE
-     场景:运营周报 dashboard 一页,读者只给不到 10 秒——按
-           lieflat-charts/SKILL.md 第〇节的例外条款("用户明确要求
-           Glance / dashboard / 周报 / 三秒快读时"),本模板直接走
-           Glance 系,不走 Lupi/Basics 的发丝细读路线。不是因为这几个
-           数据形状 Lupi 撑不住,是场合本身要三秒读完,用户也点名要
-           Glance 的粗笔画质感——这条例外在 SKILL.md 里写得很明白。
-     依赖说明:这是模板库里第一张、目前也是唯一一张挂 CDN 依赖的模板。
-           库里其余模板都是纯 SVG、离线可开;这四张图来自 Chart.js 和
-           ECharts,脚本标签原样照抄 glance-palm.html 画廊,没有联网
-           时图表区会是空白。
-     图表正本 = templates/color/glance-palm.html:
-       图A  G1 Range Capsules   (Chart.js floating bar)
-       图B  G2 Petal Rose       (ECharts,暗卡,本页唯一一张深色卡)
-       图C  G5 Pictorial Bar    (ECharts pictorialBar)
-       图D  G3 Chunky Bars      (Chart.js,补上"排名比较"这个数据
-                                  形状缺口——G1/G2/G5 分别覆盖了区间、
-                                  近似等分构成、逐日计数,唯独少类目
-                                  排名比较没人管)
-     色板正本 = color-presets.js 的 PALM 一套,与模板 02 共用同一批
-       色值,不新增色值。深绿 #43593B 当主墨,琥珀 #D4A017 当强调,
-       浓咖 #58402E 当纸浆衬底;RAMP 五档梯(#F2D17E → #ACAD79 →
-       #929960 → #77835A → #43593B)直接取自 glance-palm.html 脚本
-       内的同名常量,暗卡用的 DRAMP4/DMID/DQ 同理照搬。
-     排版参考:模板 02 的信纸垫底结构(浓咖衬底 + 纸张、全大写字距
-       节标题 + 下划线、claim + 图 + srcline 三件套、页脚双栏)——只学
-       骨架,栏位为这张仪表盘重排成 2×2 图区 + 顶部速览条。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* PALM preset roles(与模板 02 同一套) */
-    --paper:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.32); --quiet:rgba(88,64,46,.15);
-    --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#58402E; --hero:#D4A017;
-    --faintdata:#ACAD79; --bead:#77835A;
-    --mat:#58402E;
-    /* 暗卡专用(取自 glance-palm.html 的 --dmid/--dq,BG 的纸色透明档) */
-    --dmid:rgba(240,239,235,.62); --dq:rgba(240,239,235,.24);
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--mat);font-family:'Inter',sans-serif;color:var(--txt);
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 32px}
-  .sheet{width:1080px;max-width:1080px;background:var(--paper);padding:60px 68px 64px}
-
-  /* ── 节标题:全大写 + 字距 + 底线(学模板 02) ── */
-  .sect{font-size:13px;font-weight:800;letter-spacing:.14em;text-transform:uppercase;
-    padding-bottom:10px;border-bottom:2px solid var(--txt);margin-bottom:22px}
-  .sect .yr{float:right;font-weight:600;color:var(--mut);letter-spacing:.1em}
-
-  /* ── 顶部速览条:四个数,当天一句话就懂(学模板 02 的 .miles) ── */
-  .miles{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:44px}
-  .miles .m{padding:18px 16px 16px;position:relative}
-  .miles .m.hero{background:var(--hero);color:var(--paper)}
-  .miles .m.frame{border:1px solid var(--faint)}
-  .miles .m.green{background:var(--data);color:var(--paper)}
-  .miles .m .tag{font-size:8.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;opacity:.7}
-  .miles .m .v{font-size:26px;font-weight:800;letter-spacing:-.03em;line-height:1;margin-top:6px}
-  .miles .m .v small{font-size:12px;font-weight:700;margin-left:2px;text-transform:uppercase}
-  .miles .m .l{font-size:10px;line-height:1.45;margin-top:7px;opacity:.85}
-
-  /* ── 2×2 图区(复用模板 02 的 .duo,纵向再叠一份) ── */
-  .duo{display:grid;grid-template-columns:1fr 1fr;gap:40px}
-  .duo + .duo{margin-top:40px}
-
-  /* ── 图卡:轻卡描边,暗卡实底(唯一一张,读花瓣要暗底) ── */
-  .gcard{padding:24px 26px 22px;border:1px solid var(--faint)}
-  .gcard.dark{background:var(--mat);color:var(--paper);border:none}
-  .gcard.dark .sect{border-bottom-color:var(--paper);color:var(--paper)}
-  .gcard.dark .sub{color:var(--dmid)}
-  .gcard.dark .claim{color:var(--paper)}
-  .gcard.dark .srcline{color:var(--dq)}
-
-  .sub{font-size:11px;color:var(--mut);margin-bottom:14px;line-height:1.55}
-  .claim{font-size:13px;font-weight:700;margin-bottom:10px;line-height:1.45;letter-spacing:-.01em}
-  .wrap{position:relative;height:280px}
-  .ch{height:280px}
-  svg text{font-family:'Inter',sans-serif}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.11em;text-transform:uppercase;
-    color:var(--faint);margin-top:10px;line-height:1.5}
-
-  /* ── 页脚:数据说明 + CDN 依赖说明 + 落款(三栏,比模板 02 多一栏) ── */
-  .foot{margin-top:52px;padding-top:16px;border-top:2px solid var(--txt);
-    display:grid;grid-template-columns:1fr 1.55fr .85fr;gap:26px;
-    font-size:9px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--mut)}
-  .foot .warn{color:var(--data);font-weight:700;line-height:1.55}
-  .foot .warn small{display:block;font-weight:500;color:var(--mut);margin-top:4px;text-transform:none}
-  .foot .dep{line-height:1.9}
-  .foot .end{text-align:right;line-height:1.55}
-
-  /* ── 动画(mono-tokens MOTION.css 原样) ── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <div class="sect">Loop weekly ops <span class="yr">Week 32 · Aug 3–9, 2026</span></div>
-
-  <!-- ══ 顶部速览条:四个数,串起下面四张图 ══ -->
-  <div class="miles">
-    <div class="m hero">
-      <div class="tag">Peak concurrent users</div>
-      <div class="v">210<small>users</small></div>
-      <div class="l">Wednesday's high-water mark</div>
-    </div>
-    <div class="m frame">
-      <div class="tag">Release mood</div>
-      <div class="v">12<small>votes</small></div>
-      <div class="l">"Smooth" got the most votes this week</div>
-    </div>
-    <div class="m frame">
-      <div class="tag">Shipped this week</div>
-      <div class="v">28<small>changes</small></div>
-      <div class="l">Five workdays, added up</div>
-    </div>
-    <div class="m green">
-      <div class="tag">Total tickets</div>
-      <div class="v">293<small>tickets</small></div>
-      <div class="l">Across four channels, this week</div>
-    </div>
-  </div>
-
-  <!-- ══ 第一行:区间 | 暗卡花瓣 ══ -->
-  <div class="duo">
-    <div class="gcard">
-      <div class="sect" style="border-bottom-width:1px">Concurrent users swung widest on Wednesday</div>
-      <div class="sub">Daily min–max range of concurrent users · Aug 3–9</div>
-      <!-- 图A · G1 Range Capsules
-           模板:templates/color/glance-palm.html · "Daily active range" -->
-      <div class="claim">Wednesday ran from 95 to 210 users, more than double the week's typical swing.</div>
-      <div class="wrap"><canvas id="c1"></canvas></div>
-      <div class="srcline">Range capsules · daily concurrent user range (users) · green = a typical day, amber = the widest swing</div>
-    </div>
-
-    <div class="gcard dark">
-      <div class="sect" style="border-bottom-width:1px">The team left this release mostly relieved</div>
-      <div class="sub">Mood words from the release retro · 7 words, 54 votes</div>
-      <!-- 图B · G2 Petal Rose(暗卡,本页唯一一张)
-           模板:templates/color/glance-palm.html · "How releases made us feel" -->
-      <div class="claim">Smooth took 12 votes. Tense got only 3.</div>
-      <div class="ch" id="rose"></div>
-      <div class="srcline">Petal rose · retro vote count (votes) · brighter petal = more votes, darker = fewer</div>
-    </div>
-  </div>
-
-  <!-- ══ 第二行:逐日计数 | 排名比较(补的第四张) ══ -->
-  <div class="duo">
-    <div class="gcard">
-      <div class="sect" style="border-bottom-width:1px">Shipping pace climbed all week</div>
-      <div class="sub">Changes shipped, workday by workday · Monday through Friday</div>
-      <!-- 图C · G5 Pictorial Bar
-           模板:templates/color/glance-palm.html · "Trees planted, year by year" -->
-      <div class="claim">Friday shipped 9 changes, three times Monday's count.</div>
-      <div class="ch" id="ship"></div>
-      <div class="srcline">Pictorial bar · changes shipped per day (count) · one symbol = one change · amber to deep green tracks Monday through Friday</div>
-    </div>
-
-    <div class="gcard">
-      <div class="sect" style="border-bottom-width:1px">Most tickets still arrive by email</div>
-      <div class="sub">Tickets this week, by channel · 4 channels, 293 tickets</div>
-      <!-- 图D · G3 Chunky Bars(补"排名比较"这个数据形状缺口)
-           模板:templates/color/glance-palm.html · "Revenue by plan" -->
-      <div class="claim">Email alone logged 186 tickets, more than the other three channels combined.</div>
-      <div class="wrap"><canvas id="c3"></canvas></div>
-      <div class="srcline">Chunky bars · tickets this week, by channel (count) · deep green = the busiest channel, lighter down the ranks</div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <div class="warn">Demo data: every number on this page is fictional and doesn't stand in for any real product's metrics.
-      <small>This page exists to check how the Glance chart family reads inside a report template.</small></div>
-    <div class="dep">Dependency note: these four charts run on Chart.js and ECharts, the only template in this library that needs a CDN. Open it offline and the chart areas stay blank; reconnect and refresh to render them.</div>
-    <div class="end">Loop · weekly ops one-pager<br>Data · internal dashboard (demo) · Lieflat Charts</div>
-  </div>
-</div>
-
-<script>
-/* ── PALM 角色 token(与 glance-palm.html 脚本内同名常量逐一对应,不新增色值) ── */
-const BG='#F0EFEB',DARK='#58402E',TXT='#58402E';
-const MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#58402E',HERO='#D4A017';
-const HERODK='#F2D17E';
-const FAINTDATA='#ACAD79';
-const DMID='rgba(240,239,235,.62)',DQ='rgba(240,239,235,.24)';
-const RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B'];
-const DRAMP4=['#F2D17E','#ACAD79','rgba(240,239,235,.55)','rgba(240,239,235,.26)'];
-const INK=TXT,PAPER=BG,MUTED=MUT;
-const rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));
-const alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;
-const mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');
-const lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));
-  return .2126*r+.7152*g+.0722*b};
-const ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};
-const readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG);
-const DSHELL=mix(DARK,BG,.09);
-const rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);
-  return i=>r.indexOf(i)};
-const rampAt=(i,n)=>i===0?RAMP[4]
-  :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];
-const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-
-/* ── 统一 reveal 机制(滚入播 + 点击重播,与 glance-palm.html 一致) ── */
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUT;
-Chart.defaults.font.family='Inter';
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=TXT;
-Chart.defaults.plugins.tooltip.titleColor=BG;
-Chart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-/* ════ 图A · G1 Range Capsules ════
-   结构照搬 glance-palm.html "Daily active range":浮动区间柱,
-   最宽的一根上琥珀墨。原图是 14 天窗口,这里换成一周 7 天。 */
-(()=>{
-const RANGES=[[120,178],[108,172],[95,210],[130,183],[142,188],[82,121],[76,116]];
-const DAYS=['MON','TUE','WED','THU','FRI','SAT','SUN'];
-const WIDEST=r=>r.reduce((b,x,i)=>x[1]-x[0]>r[b][1]-r[b][0]?i:b,0);
-cReveal('c1',()=>({
-  type:'bar',
-  data:{labels:DAYS,
-    datasets:[{data:RANGES,backgroundColor:RANGES.map((_,i)=>i===WIDEST(RANGES)?HERO:DATA),borderRadius:99,borderSkipped:false,barPercentage:.42}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:800,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*60:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw[0]+' – '+i.raw[1]+' concurrent users'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:6,font:{size:9,weight:600}}},
-      y:{min:50,max:230,grid:{display:false},border:{display:false},ticks:{padding:8,maxTicksLimit:6,callback:v=>v+'u',font:{size:9.5}}},
-    },
-  },
-}));
-})();
-
-/* ════ 图B · G2 Petal Rose(暗卡) ════
-   结构照搬 glance-palm.html "How releases made us feel":花瓣长度
-   编码票数,颜色阶(DRAMP4)也编码票数,票数越高越亮。 */
-(()=>{
-const EMO=[
-  ['Smooth',12],['Relieved',10],['Excited',9],['Uneasy',7],
-  ['Tired',6],['Surprised',5],['Tense',3],
-];
-const MAXV=12;
-const petal=v=>{const t=v/MAXV;return t>.8?DRAMP4[0]:t>.6?DRAMP4[1]:t>.35?DRAMP4[2]:DRAMP4[3]};
-const onPetal=v=>v/MAXV>.6;
-eReveal('rose',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.name+' — '+EMO[p.dataIndex][1]+' votes'},
-  series:[
-    {type:'pie',silent:false,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:DSHELL,borderRadius:16,borderColor:DARK,borderWidth:5},
-     label:{show:false},emphasis:{scale:false},
-     data:EMO.map(([n])=>({name:n,value:1})),z:1},
-    {type:'pie',silent:true,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:'transparent'},label:{position:'inside'},
-     data:EMO.map(([n,v])=>({name:n,value:1,
-       label:{formatter:`{n|${v}}\n{l|${n}}`,rich:{
-         n:{fontSize:17,fontWeight:700,fontFamily:'Inter',lineHeight:20,
-              color:onPetal(v)?readable(petal(v)):DMID},
-         l:{fontSize:9.5,fontFamily:'Inter',
-              color:onPetal(v)?alpha(readable(petal(v)),.66):DQ},
-       }}})),z:3},
-    {type:'pie',roseType:'area',silent:true,radius:['14%','88%'],center:['50%','50%'],
-     itemStyle:{borderRadius:14,borderColor:DARK,borderWidth:5},label:{show:false},
-     data:EMO.map(([n,v])=>({name:n,value:v,itemStyle:{color:petal(v)}})),z:2},
-  ],
-});
-})();
-
-/* ════ 图C · G5 Pictorial Bar ════
-   结构照搬 glance-palm.html "Trees planted, year by year":符号
-   计数横条,原图 symbol 是树形,这里换成一个中性菱形——上线的是
-   功能改动,不是树,符号形状不该借一个不相关的隐喻。 */
-(()=>{
-const SHIP='path://M14,0 L28,14 L14,28 L0,14 Z';
-const DAYS5=['MON','TUE','WED','THU','FRI'];
-const SHIPPED=[3,4,5,7,9];
-eReveal('ship',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*130,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+SHIPPED[p.dataIndex]+' changes'},
-  grid:{left:44,right:56,top:10,bottom:8},
-  yAxis:{type:'category',data:DAYS5,inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUT,fontFamily:'Inter',fontSize:10.5,fontWeight:700}},
-  xAxis:{show:false,max:10},
-  series:[{
-    type:'pictorialBar',symbol:SHIP,
-    symbolRepeat:true,symbolSize:[15,15],symbolMargin:3,symbolClip:true,
-    data:SHIPPED.map((v,i)=>({name:DAYS5[i],value:v,itemStyle:{color:RAMP[i]}})),
-    label:{show:true,position:'right',offset:[8,0],
-      color:TXT,fontFamily:'Inter',fontSize:12,fontWeight:700,
-      formatter:p=>SHIPPED[p.dataIndex]},
-    z:10,
-  },{
-    type:'pictorialBar',symbol:SHIP,
-    symbolRepeat:'fixed',symbolSize:[15,15],symbolMargin:3,symbolClip:true,
-    itemStyle:{color:QUIET},data:DAYS5.map(()=>10),silent:true,z:5,
-  }],
-});
-})();
-
-/* ════ 图D · G3 Chunky Bars(补"排名比较"这个数据形状缺口) ════
-   结构照搬 glance-palm.html "Revenue by plan":粗胶囊柱 + 柱顶大字,
-   颜色按 rampAt(排名,n) 走,排第一深绿,排最后最浅。 */
-(()=>{
-const CATS=['EMAIL','IN-APP','SLACK','TICKET FORM'];
-const V=[186,54,31,22];
-const rk=rankOf(V);
-cReveal('c3',()=>({
-  type:'bar',
-  data:{labels:CATS,
-    datasets:[{data:V,
-      backgroundColor:V.map((_,i)=>rampAt(rk(i),V.length)),
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw+' tickets'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9,weight:600}}},
-      y:{display:false,suggestedMax:220},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font="700 13px Inter";ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText(chart.data.datasets[0].data[i],bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-addEventListener('resize',()=>{
-  ['rose','ship'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 413
service/cmd/mcp/.kiro/skills/lieflat-charts/templates/reports/report-12.zh.html

@@ -1,413 +0,0 @@
-<!doctype html>
-<html lang="zh-Hans">
-<head>
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<title>周报速览 — Lieflat 报告模板 · 椰林三秒</title>
-<script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
-<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
-<link rel="preconnect" href="https://fonts.googleapis.com">
-<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Noto+Sans+SC:wght@400;500;700;900&display=swap" rel="stylesheet">
-<style>
-  /* ═══════════════════════════════════════════════════════════════
-     LIEFLAT 报告模板 12 · 周报速览 · 椰林三秒
-     场景:运营周报 dashboard 一页,读者只给不到 10 秒——按
-           lieflat-charts/SKILL.md 第〇节的例外条款("用户明确要求
-           Glance / dashboard / 周报 / 三秒快读时"),本模板直接走
-           Glance 系,不走 Lupi/Basics 的发丝细读路线。不是因为这几个
-           数据形状 Lupi 撑不住,是场合本身要三秒读完,用户也点名要
-           Glance 的粗笔画质感——这条例外在 SKILL.md 里写得很明白。
-     依赖说明:这是模板库里第一张、目前也是唯一一张挂 CDN 依赖的模板。
-           库里其余模板都是纯 SVG、离线可开;这四张图来自 Chart.js 和
-           ECharts,脚本标签原样照抄 glance-palm.html 画廊,没有联网
-           时图表区会是空白。
-     图表正本 = templates/color/glance-palm.html:
-       图A  G1 Range Capsules   (Chart.js floating bar)
-       图B  G2 Petal Rose       (ECharts,暗卡,本页唯一一张深色卡)
-       图C  G5 Pictorial Bar    (ECharts pictorialBar)
-       图D  G3 Chunky Bars      (Chart.js,补上"排名比较"这个数据
-                                  形状缺口——G1/G2/G5 分别覆盖了区间、
-                                  近似等分构成、逐日计数,唯独少类目
-                                  排名比较没人管)
-     色板正本 = color-presets.js 的 PALM 一套,与模板 02 共用同一批
-       色值,不新增色值。深绿 #43593B 当主墨,琥珀 #D4A017 当强调,
-       浓咖 #58402E 当纸浆衬底;RAMP 五档梯(#F2D17E → #ACAD79 →
-       #929960 → #77835A → #43593B)直接取自 glance-palm.html 脚本
-       内的同名常量,暗卡用的 DRAMP4/DMID/DQ 同理照搬。
-     排版参考:模板 02 的信纸垫底结构(浓咖衬底 + 纸张、全大写字距
-       节标题 + 下划线、claim + 图 + srcline 三件套、页脚双栏)——只学
-       骨架,栏位为这张仪表盘重排成 2×2 图区 + 顶部速览条。
-     数据说明:全部为虚构演示数据,仅用于验证版式。
-     ═══════════════════════════════════════════════════════════════ */
-  :root{
-    /* PALM preset roles(与模板 02 同一套) */
-    --paper:#F0EFEB; --txt:#58402E;
-    --mut:rgba(88,64,46,.60); --lab:rgba(88,64,46,.72);
-    --faint:rgba(88,64,46,.32); --quiet:rgba(88,64,46,.15);
-    --grid:rgba(88,64,46,.16);
-    --data:#43593B; --data2:#58402E; --hero:#D4A017;
-    --faintdata:#ACAD79; --bead:#77835A;
-    --mat:#58402E;
-    /* 暗卡专用(取自 glance-palm.html 的 --dmid/--dq,BG 的纸色透明档) */
-    --dmid:rgba(240,239,235,.62); --dq:rgba(240,239,235,.24);
-    --sans:'Inter','Noto Sans SC',sans-serif;
-  }
-  *{margin:0;padding:0;box-sizing:border-box}
-  body{background:var(--mat);font-family:var(--sans);color:var(--txt);
-    -webkit-font-smoothing:antialiased;font-variant-numeric:tabular-nums lining-nums;
-    display:flex;justify-content:center;padding:64px 32px}
-  .sheet{width:1080px;max-width:1080px;background:var(--paper);padding:60px 68px 64px}
-
-  /* ── 节标题:字距 + 底线(学模板 02) ── */
-  .sect{font-size:14px;font-weight:800;letter-spacing:.2em;
-    padding-bottom:10px;border-bottom:2px solid var(--txt);margin-bottom:22px}
-  .sect .yr{float:right;font-weight:600;color:var(--mut);letter-spacing:.1em}
-
-  /* ── 顶部速览条:四个数,当天一句话就懂(学模板 02 的 .miles) ── */
-  .miles{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-bottom:44px}
-  .miles .m{padding:18px 16px 16px;position:relative}
-  .miles .m.hero{background:var(--hero);color:var(--paper)}
-  .miles .m.frame{border:1px solid var(--faint)}
-  .miles .m.green{background:var(--data);color:var(--paper)}
-  .miles .m .tag{font-size:8.5px;font-weight:700;letter-spacing:.14em;opacity:.7}
-  .miles .m .v{font-size:26px;font-weight:800;letter-spacing:-.03em;line-height:1;margin-top:6px}
-  .miles .m .v small{font-size:13px;font-weight:700;margin-left:2px}
-  .miles .m .l{font-size:10px;line-height:1.65;margin-top:8px;opacity:.85}
-
-  /* ── 2×2 图区(复用模板 02 的 .duo,纵向再叠一份) ── */
-  .duo{display:grid;grid-template-columns:1fr 1fr;gap:40px}
-  .duo + .duo{margin-top:40px}
-
-  /* ── 图卡:轻卡描边,暗卡实底(唯一一张,读花瓣要暗底) ── */
-  .gcard{padding:24px 26px 22px;border:1px solid var(--faint)}
-  .gcard.dark{background:var(--mat);color:var(--paper);border:none}
-  .gcard.dark .sect{border-bottom-color:var(--paper);color:var(--paper)}
-  .gcard.dark .sub{color:var(--dmid)}
-  .gcard.dark .claim{color:var(--paper)}
-  .gcard.dark .srcline{color:var(--dq)}
-
-  .sub{font-size:11px;color:var(--mut);margin-bottom:14px;line-height:1.75}
-  .claim{font-size:13.5px;font-weight:700;margin-bottom:10px;line-height:1.6}
-  .wrap{position:relative;height:280px}
-  .ch{height:280px}
-  svg text{font-family:var(--sans)}
-  .srcline{font-size:9px;font-weight:600;letter-spacing:.1em;
-    color:var(--faint);margin-top:10px;line-height:1.6}
-
-  /* ── 页脚:数据说明 + CDN 依赖说明 + 落款(三栏,比模板 02 多一栏) ── */
-  .foot{margin-top:52px;padding-top:16px;border-top:2px solid var(--txt);
-    display:grid;grid-template-columns:1fr 1.55fr .85fr;gap:26px;
-    font-size:9px;font-weight:600;letter-spacing:.08em;color:var(--mut)}
-  .foot .warn{color:var(--data);font-weight:700;line-height:1.75}
-  .foot .warn small{display:block;font-weight:500;color:var(--mut);margin-top:4px}
-  .foot .dep{line-height:1.9}
-  .foot .end{text-align:right;line-height:1.75}
-
-  /* ── 动画(mono-tokens MOTION.css 原样) ── */
-  .pop{transform-box:fill-box;transform-origin:center;animation:pop .5s cubic-bezier(.2,.7,.3,1.3) both}
-  @keyframes pop{from{transform:scale(0)}to{transform:none}}
-  .fade{animation:fade .9s ease both}
-  @keyframes fade{from{opacity:0}}
-  .draw{stroke-dasharray:1;stroke-dashoffset:1;animation:draw 1s cubic-bezier(.4,0,.2,1) both}
-  @keyframes draw{to{stroke-dashoffset:0}}
-  @media(max-width:640px){body{padding:24px 10px}}
-  @media (prefers-reduced-motion:reduce){
-    .pop,.fade{animation:none}
-    .draw{animation:none;stroke-dasharray:none;stroke-dashoffset:0}
-  }
-</style>
-</head>
-<body>
-<div class="sheet">
-
-  <div class="sect">Loop 运营速览 <span class="yr">第 32 周 · 2026年8月3日–8月9日</span></div>
-
-  <!-- ══ 顶部速览条:四个数,串起下面四张图 ══ -->
-  <div class="miles">
-    <div class="m hero">
-      <div class="tag">周活跃峰值</div>
-      <div class="v">210<small>人</small></div>
-      <div class="l">周三同时在线的最高点</div>
-    </div>
-    <div class="m frame">
-      <div class="tag">发布心情</div>
-      <div class="v">12<small>票</small></div>
-      <div class="l">"顺畅"是这周投票最多的词</div>
-    </div>
-    <div class="m frame">
-      <div class="tag">本周上线</div>
-      <div class="v">28<small>个</small></div>
-      <div class="l">五个工作日加起来的改动数</div>
-    </div>
-    <div class="m green">
-      <div class="tag">工单总量</div>
-      <div class="v">293<small>件</small></div>
-      <div class="l">四个渠道加起来的这周工单</div>
-    </div>
-  </div>
-
-  <!-- ══ 第一行:区间 | 暗卡花瓣 ══ -->
-  <div class="duo">
-    <div class="gcard">
-      <div class="sect" style="border-bottom-width:1px">同时在线人数,周三波动最大</div>
-      <div class="sub">每天同时在线人数的最低到最高区间 · 8月3日–8月9日</div>
-      <!-- 图A · G1 Range Capsules
-           模板:templates/color/glance-palm.html · "Daily active range" -->
-      <div class="claim">周三从95人冲到210人,振幅是全周平均的两倍多</div>
-      <div class="wrap"><canvas id="c1"></canvas></div>
-      <div class="srcline">RANGE CAPSULES · 每日同时在线人数区间(人) · 深绿=常规日,琥珀=全周最大摆动</div>
-    </div>
-
-    <div class="gcard dark">
-      <div class="sect" style="border-bottom-width:1px">这次发布,团队整体松了口气</div>
-      <div class="sub">复盘会上给这次发布投的形容词票 · 7 个词,54 票</div>
-      <!-- 图B · G2 Petal Rose(暗卡,本页唯一一张)
-           模板:templates/color/glance-palm.html · "How releases made us feel" -->
-      <div class="claim">"顺畅"拿到12票,"紧张"只有3票</div>
-      <div class="ch" id="rose"></div>
-      <div class="srcline">PETAL ROSE · 复盘投票数(票) · 花瓣越亮=票数越高,越暗=票数越低</div>
-    </div>
-  </div>
-
-  <!-- ══ 第二行:逐日计数 | 排名比较(补的第四张) ══ -->
-  <div class="duo">
-    <div class="gcard">
-      <div class="sect" style="border-bottom-width:1px">上线节奏,一路走高</div>
-      <div class="sub">工作日的功能上线数量 · 周一到周五</div>
-      <!-- 图C · G5 Pictorial Bar
-           模板:templates/color/glance-palm.html · "Trees planted, year by year" -->
-      <div class="claim">周五上线9个改动,是周一的3倍</div>
-      <div class="ch" id="ship"></div>
-      <div class="srcline">PICTORIAL BAR · 每日上线改动数(个) · 一个符号=1个改动 · 麦黄到深绿=周一到周五递深</div>
-    </div>
-
-    <div class="gcard">
-      <div class="sect" style="border-bottom-width:1px">工单,大半还是从邮件来的</div>
-      <div class="sub">本周工单量,按提交渠道 · 4 个渠道,293 件</div>
-      <!-- 图D · G3 Chunky Bars(补"排名比较"这个数据形状缺口)
-           模板:templates/color/glance-palm.html · "Revenue by plan" -->
-      <div class="claim">邮件工单186件,比其余三个渠道加起来还多</div>
-      <div class="wrap"><canvas id="c3"></canvas></div>
-      <div class="srcline">CHUNKY BARS · 本周工单量,按渠道(件) · 深绿=工单最多的渠道,依次转浅</div>
-    </div>
-  </div>
-
-  <div class="foot">
-    <div class="warn">数据说明:全部为虚构演示数据,不代表任何真实产品的运营指标。
-      <small>这页只用来验证 Glance 图表家族放进报告模板里是什么样子。</small></div>
-    <div class="dep">依赖说明:这四张图跑在 Chart.js 和 ECharts 上,是模板库里唯一一张挂 CDN 依赖的模板。离线打开会看到图表区空白,联网后刷新即可正常渲染。</div>
-    <div class="end">Loop · 运营周报一页纸<br>数据 · 内部看板(演示) · LIEFLAT CHARTS</div>
-  </div>
-</div>
-
-<script>
-/* ── PALM 角色 token(与 glance-palm.html 脚本内同名常量逐一对应,不新增色值) ── */
-const BG='#F0EFEB',DARK='#58402E',TXT='#58402E';
-const MUT='rgba(88,64,46,.60)',LAB='rgba(88,64,46,.72)',FAINT='rgba(88,64,46,.40)',QUIET='rgba(88,64,46,.15)',GRID='rgba(88,64,46,.16)';
-const DATA='#43593B',DATA2='#58402E',HERO='#D4A017';
-const HERODK='#F2D17E';
-const FAINTDATA='#ACAD79';
-const DMID='rgba(240,239,235,.62)',DQ='rgba(240,239,235,.24)';
-const RAMP=['#F2D17E','#ACAD79','#929960','#77835A','#43593B'];
-const DRAMP4=['#F2D17E','#ACAD79','rgba(240,239,235,.55)','rgba(240,239,235,.26)'];
-const INK=TXT,PAPER=BG,MUTED=MUT;
-const rgb=h=>[0,2,4].map(i=>parseInt(h.slice(1).substr(i,2),16));
-const alpha=(h,a)=>h[0]==='#'?'rgba('+rgb(h).join(',')+','+a+')':h;
-const mix=(a,b,t)=>'#'+rgb(a).map((v,i)=>Math.round(v+(rgb(b)[i]-v)*t).toString(16).padStart(2,'0')).join('');
-const lum=h=>{const [r,g,b]=rgb(h).map(v=>v/255).map(v=>v<=.03928?v/12.92:Math.pow((v+.055)/1.055,2.4));
-  return .2126*r+.7152*g+.0722*b};
-const ratio=(a,b)=>{const l=[lum(a),lum(b)].sort((p,q)=>q-p);return (l[0]+.05)/(l[1]+.05)};
-const readable=c=>c[0]!=='#'?BG:(ratio(c,DARK)>=ratio(c,BG)?DARK:BG);
-const DSHELL=mix(DARK,BG,.09);
-const rankOf=a=>{const r=[...a].map((v,i)=>[v,i]).sort((x,y)=>y[0]-x[0]).map(d=>d[1]);
-  return i=>r.indexOf(i)};
-const rampAt=(i,n)=>i===0?RAMP[4]
-  :RAMP[Math.max(0,Math.min(3,3-Math.round((i-1)/Math.max(1,n-2)*3)))];
-const tipLight={backgroundColor:INK,borderWidth:0,textStyle:{color:PAPER,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-const tipDark={backgroundColor:PAPER,borderWidth:0,textStyle:{color:INK,fontFamily:'Inter',fontSize:12},padding:[10,14]};
-
-/* ── 统一 reveal 机制(滚入播 + 点击重播,与 glance-palm.html 一致) ── */
-const timers={};
-const obsReveal=(id,fn)=>{
-  const el=document.getElementById(id);
-  const go=()=>{(timers[id]||[]).forEach(clearInterval);timers[id]=[];fn(el)};
-  const io=new IntersectionObserver(es=>{
-    if(es[0].isIntersecting){go();io.disconnect()}
-  },{threshold:.35});
-  io.observe(el);
-  el.style.cursor='pointer';
-  el.addEventListener('click',go);
-};
-const keep=(id,t)=>{(timers[id]=timers[id]||[]).push(t)};
-const eReveal=(id,opt)=>obsReveal(id,el=>{
-  const g=echarts.getInstanceByDom(el)||echarts.init(el);
-  g.clear();g.setOption(opt);
-});
-const cjs={};
-const cReveal=(id,mkCfg)=>obsReveal(id,el=>{
-  cjs[id]?.destroy();
-  cjs[id]=new Chart(el,mkCfg());
-});
-
-Chart.defaults.color=MUT;
-Chart.defaults.font.family="'Inter','Noto Sans SC',sans-serif";
-Chart.defaults.font.size=10.5;
-Chart.defaults.borderColor=GRID;
-Chart.defaults.plugins.legend.display=false;
-Chart.defaults.plugins.tooltip.backgroundColor=TXT;
-Chart.defaults.plugins.tooltip.titleColor=BG;
-Chart.defaults.plugins.tooltip.bodyColor=alpha(BG,.72);
-Chart.defaults.plugins.tooltip.padding=12;
-Chart.defaults.plugins.tooltip.cornerRadius=12;
-Chart.defaults.plugins.tooltip.displayColors=false;
-
-/* ════ 图A · G1 Range Capsules ════
-   结构照搬 glance-palm.html "Daily active range":浮动区间柱,
-   最宽的一根上琥珀墨。原图是 14 天窗口,这里换成一周 7 天。 */
-(()=>{
-const RANGES=[[120,178],[108,172],[95,210],[130,183],[142,188],[82,121],[76,116]];
-const DAYS=['周一','周二','周三','周四','周五','周六','周日'];
-const WIDEST=r=>r.reduce((b,x,i)=>x[1]-x[0]>r[b][1]-r[b][0]?i:b,0);
-cReveal('c1',()=>({
-  type:'bar',
-  data:{labels:DAYS,
-    datasets:[{data:RANGES,backgroundColor:RANGES.map((_,i)=>i===WIDEST(RANGES)?HERO:DATA),borderRadius:99,borderSkipped:false,barPercentage:.42}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:800,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*60:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw[0]+' – '+i.raw[1]+' 人在线'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:6,font:{size:10,weight:600}}},
-      y:{min:50,max:230,grid:{display:false},border:{display:false},ticks:{padding:8,maxTicksLimit:6,callback:v=>v+'人',font:{size:9.5}}},
-    },
-  },
-}));
-})();
-
-/* ════ 图B · G2 Petal Rose(暗卡) ════
-   结构照搬 glance-palm.html "How releases made us feel":花瓣长度
-   编码票数,颜色阶(DRAMP4)也编码票数,票数越高越亮。 */
-(()=>{
-const EMO=[
-  ['顺畅',12],['轻松',10],['期待',9],['忐忑',7],
-  ['疲惫',6],['惊喜',5],['紧张',3],
-];
-const MAXV=12;
-const petal=v=>{const t=v/MAXV;return t>.8?DRAMP4[0]:t>.6?DRAMP4[1]:t>.35?DRAMP4[2]:DRAMP4[3]};
-const onPetal=v=>v/MAXV>.6;
-eReveal('rose',{
-  animationDuration:1100,animationEasing:'quarticOut',
-  tooltip:{...tipDark,formatter:p=>p.name+' — '+EMO[p.dataIndex][1]+' 票'},
-  series:[
-    {type:'pie',silent:false,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:DSHELL,borderRadius:16,borderColor:DARK,borderWidth:5},
-     label:{show:false},emphasis:{scale:false},
-     data:EMO.map(([n])=>({name:n,value:1})),z:1},
-    {type:'pie',silent:true,radius:['14%','92%'],center:['50%','50%'],
-     itemStyle:{color:'transparent'},label:{position:'inside'},
-     data:EMO.map(([n,v])=>({name:n,value:1,
-       label:{formatter:`{n|${v}}\n{l|${n}}`,rich:{
-         n:{fontSize:17,fontWeight:700,fontFamily:'Inter',lineHeight:20,
-              color:onPetal(v)?readable(petal(v)):DMID},
-         l:{fontSize:9.5,fontFamily:"'Inter','Noto Sans SC',sans-serif",
-              color:onPetal(v)?alpha(readable(petal(v)),.66):DQ},
-       }}})),z:3},
-    {type:'pie',roseType:'area',silent:true,radius:['14%','88%'],center:['50%','50%'],
-     itemStyle:{borderRadius:14,borderColor:DARK,borderWidth:5},label:{show:false},
-     data:EMO.map(([n,v])=>({name:n,value:v,itemStyle:{color:petal(v)}})),z:2},
-  ],
-});
-})();
-
-/* ════ 图C · G5 Pictorial Bar ════
-   结构照搬 glance-palm.html "Trees planted, year by year":符号
-   计数横条,原图 symbol 是树形,这里换成一个中性菱形——上线的是
-   功能改动,不是树,符号形状不该借一个不相关的隐喻。 */
-(()=>{
-const SHIP='path://M14,0 L28,14 L14,28 L0,14 Z';
-const DAYS5=['周一','周二','周三','周四','周五'];
-const SHIPPED=[3,4,5,7,9];
-eReveal('ship',{
-  animationDuration:900,animationEasing:'quarticOut',animationDelay:i=>i*130,
-  tooltip:{...tipLight,formatter:p=>p.name+' — '+SHIPPED[p.dataIndex]+' 个改动'},
-  grid:{left:52,right:56,top:10,bottom:8},
-  yAxis:{type:'category',data:DAYS5,inverse:true,
-    axisLine:{show:false},axisTick:{show:false},
-    axisLabel:{color:MUT,fontFamily:"'Inter','Noto Sans SC',sans-serif",fontSize:10.5,fontWeight:700}},
-  xAxis:{show:false,max:10},
-  series:[{
-    type:'pictorialBar',symbol:SHIP,
-    symbolRepeat:true,symbolSize:[15,15],symbolMargin:3,symbolClip:true,
-    data:SHIPPED.map((v,i)=>({name:DAYS5[i],value:v,itemStyle:{color:RAMP[i]}})),
-    label:{show:true,position:'right',offset:[8,0],
-      color:TXT,fontFamily:"'Inter','Noto Sans SC',sans-serif",fontSize:12,fontWeight:700,
-      formatter:p=>SHIPPED[p.dataIndex]+' 个'},
-    z:10,
-  },{
-    type:'pictorialBar',symbol:SHIP,
-    symbolRepeat:'fixed',symbolSize:[15,15],symbolMargin:3,symbolClip:true,
-    itemStyle:{color:QUIET},data:DAYS5.map(()=>10),silent:true,z:5,
-  }],
-});
-})();
-
-/* ════ 图D · G3 Chunky Bars(补"排名比较"这个数据形状缺口) ════
-   结构照搬 glance-palm.html "Revenue by plan":粗胶囊柱 + 柱顶大字,
-   颜色按 rampAt(排名,n) 走,排第一深绿,排最后最浅。 */
-(()=>{
-const CATS=['邮件','应用内','Slack 社群','工单表单'];
-const V=[186,54,31,22];
-const rk=rankOf(V);
-cReveal('c3',()=>({
-  type:'bar',
-  data:{labels:CATS,
-    datasets:[{data:V,
-      backgroundColor:V.map((_,i)=>rampAt(rk(i),V.length)),
-      borderRadius:{topLeft:99,topRight:99},borderSkipped:false,barPercentage:.52}]},
-  options:{
-    maintainAspectRatio:false,
-    animation:{duration:900,easing:'easeOutQuart',delay:c=>c.type==='data'?c.dataIndex*110:0},
-    plugins:{tooltip:{callbacks:{label:i=>i.raw+' 件工单'}}},
-    scales:{
-      x:{grid:{display:false},border:{display:false},ticks:{padding:8,font:{size:9.5,weight:600}}},
-      y:{display:false,suggestedMax:220},
-    },
-  },
-  plugins:[{
-    id:'topValues',
-    afterDatasetsDraw(chart){
-      const {ctx}=chart;
-      chart.getDatasetMeta(0).data.forEach((bar,i)=>{
-        ctx.save();ctx.font="700 13px 'Inter','Noto Sans SC',sans-serif";ctx.fillStyle=INK;ctx.textAlign='center';
-        ctx.fillText(chart.data.datasets[0].data[i]+'件',bar.x,bar.y-10);ctx.restore();
-      });
-    },
-  }],
-}));
-})();
-
-addEventListener('resize',()=>{
-  ['rose','ship'].forEach(id=>echarts.getInstanceByDom(document.getElementById(id))?.resize());
-});
-
-</script>
-<script>
-/* ── fit-width:视口比版心窄时整页等比缩小,不重排 ──
-   报告是一张版面,不是响应式网页。窄屏塌成单栏会得到一个
-   设计时不存在的版式;等比缩小则保留构图,跟 PDF 的「适合宽度」一致。
-   机制同模板 11。 */
-(()=>{
-  const sheet=document.querySelector('.sheet');
-  const W=1080;
-  const fit=()=>{
-    const cs=getComputedStyle(document.body);
-    const avail=document.documentElement.clientWidth
-      -parseFloat(cs.paddingLeft)-parseFloat(cs.paddingRight);
-    sheet.style.zoom=Math.min(1,avail/W);
-  };
-  fit();
-  addEventListener('resize',fit);
-})();
-</script>
-</body>
-</html>

+ 0 - 417
service/cmd/mcp/.kiro/skills/officecli/SKILL.md

@@ -1,417 +0,0 @@
----
-name: officecli
-description: Create, analyze, proofread, and modify Office documents (.docx, .xlsx, .pptx) using the officecli CLI tool. Use when the user wants to create, inspect, check formatting, find issues, add charts, or modify Office documents.
----
-
-# officecli
-
-AI-friendly CLI for .docx, .xlsx, .pptx. Single binary, no dependencies, no Office installation needed.
-
-## Install
-
-If `officecli` is not installed:
-
-```bash
-# macOS / Linux
-curl -fsSL https://d.officecli.ai/install.sh | bash
-
-# Windows (PowerShell)
-irm https://d.officecli.ai/install.ps1 | iex
-```
-
-Verify with `officecli --version`. If still not found after install, open a new terminal.
-
----
-
-## Strategy
-
-**L1 (read) → L2 (DOM edit) → L3 (raw XML)**. Always prefer higher layers. Add `--json` for structured output.
-
-**Before doc work, check Specialized Skills** (bottom of this file). Fundraising decks, academic papers, financial models, dashboards, and Morph animations need their own skill loaded first — `load_skill` once, then proceed.
-
----
-
-## Help System (IMPORTANT)
-
-**When unsure about property names, value formats, or command syntax, ALWAYS run help instead of guessing.** One help query beats guess-fail-retry loops.
-
-`officecli help` ≡ `officecli --help`, and `officecli <cmd> --help` ≡ `officecli help <cmd>` — same content.
-
-```bash
-officecli help                                  # All commands + global options + schema entry points
-officecli help docx                             # List all docx elements
-officecli help docx paragraph                   # Full schema: properties, aliases, examples, readbacks
-officecli help docx set paragraph               # Verb-filtered: only props usable with `set`
-officecli help docx paragraph --json            # Structured schema (machine-readable)
-```
-
-Format aliases: `word`→`docx`, `excel`→`xlsx`, `ppt`/`powerpoint`→`pptx`. Verbs: `add`, `set`, `get`, `query`, `remove`. MCP exposes the same schema via the single `command` string param: `{"command":"help docx paragraph"}` (not a structured `{"format":...,"type":...}` object — the MCP tool has exactly one param, `command`, and passes it through to the CLI verbatim).
-
----
-
-## Performance: Resident Mode
-
-**Every command auto-starts a resident on first access** (60s idle timeout) — file-lock conflicts are automatically avoided. Explicit `open`/`close` is still recommended for longer sessions (12min idle):
-```bash
-officecli open report.docx       # explicitly keep in memory
-officecli set report.docx ...    # no file I/O overhead
-officecli close report.docx      # save and release
-```
-
-Opt out of auto-start: `OFFICECLI_NO_AUTO_RESIDENT=1`.
-
-**Flush only at the non-officecli boundary.** officecli's own reads (`get`/`query`/`view`/`dump`) always see your latest edits, so you never need to save mid-workflow. Run `save` (keeps the resident) or `close` (flush + release) only **before a non-officecli program reads the file** — python-docx/openpyxl, Word, a renderer, delivery/upload. (Idle sessions auto-flush within seconds; `OFFICECLI_RESIDENT_FLUSH=each` makes every mutation flush before returning.)
-
----
-
-## Quick Start
-
-**PPT:**
-```bash
-officecli create slides.pptx
-officecli add slides.pptx / --type slide --prop title="Q4 Report" --prop background=1A1A2E
-officecli add slides.pptx '/slide[1]' --type shape --prop text="Revenue grew 25%" --prop x=2cm --prop y=5cm --prop font=Arial --prop size=24 --prop color=FFFFFF
-```
-
-**Word:**
-```bash
-officecli create report.docx
-officecli add report.docx /body --type paragraph --prop text="Executive Summary" --prop style=Heading1
-officecli add report.docx /body --type paragraph --prop text="Revenue increased by 25% year-over-year."
-```
-
-**Excel:**
-```bash
-officecli create data.xlsx
-officecli set data.xlsx /Sheet1/A1 --prop value="Name" --prop bold=true
-officecli set data.xlsx /Sheet1/A2 --prop value="Alice"
-```
-
----
-
-## L1: Create, Read & Inspect
-
-```bash
-officecli create <file>               # Create blank .docx/.xlsx/.pptx (type from extension)
-officecli view <file> <mode>          # outline | stats | issues | text | annotated | html
-officecli get <file> <path> --depth N # Get a node and its children [--json]
-officecli query <file> <selector>     # CSS-like query
-officecli validate <file>             # Validate against OpenXML schema
-```
-
-### view modes
-
-| Mode | Description | Useful flags |
-|------|-------------|-------------|
-| `outline` | Document structure | |
-| `stats` | Statistics (pages, words, shapes) | |
-| `issues` | Formatting/content/structure problems | `--type format\|content\|structure`, `--limit N` |
-| `text` | Plain text extraction | `--start N --end N`, `--max-lines N` |
-| `annotated` | Text with formatting annotations | |
-| `html` | Static HTML snapshot — same renderer as `watch`, no server needed | `--browser`, `--page N` (docx), `--start N --end N` (pptx) |
-| `screenshot` / `svg` / `pdf` / `forms` | PNG via headless browser / SVG (pptx slide) / PDF via exporter plugin / form-fields JSON via format-handler plugin | `-o`, `--screenshot-width/-height`, pptx `--grid N` |
-
-Use `view html` for one-shot snapshots (CI artifacts, archival, diffing); use `watch` when you need live refresh or browser-side click-to-select.
-
-### get
-
-Any XML path via element localName. Use `--depth N` to expand children. Add `--json` for structured output. Default text output is grep-friendly: `path (type) "text" key=val key=val ...`
-
-```bash
-officecli get report.docx '/body/p[3]' --depth 2 --json
-officecli get slides.pptx '/slide[1]' --depth 1          # list all shapes on slide 1
-officecli get data.xlsx '/Sheet1/B2' --json
-```
-
-### Stable ID Addressing
-
-Elements with stable IDs return `@attr=value` paths instead of positional indices. Prefer these in multi-step workflows — positional indices shift on insert/delete, stable IDs do not.
-
-```
-/slide[1]/shape[@id=550950021]                    # PPT shape
-/slide[1]/table[@id=1388430425]/tr[1]/tc[2]       # PPT table
-/body/p[@paraId=1A2B3C4D]                         # Word paragraph
-/comments/comment[@commentId=1]                    # Word comment
-```
-
-PPT also accepts `@name=` (e.g. `shape[@name=Title 1]`), with morph `!!` prefix awareness. Elements without stable IDs (slide, run, tr/tc, row) fall back to positional indices.
-
-### query
-
-CSS-like selectors: `[attr=value]`, `[attr!=value]`, `[attr~=text]`, `[attr>=value]`, `[attr<=value]`, `:contains("text")`, `:empty`, `:has(formula)`, `:no-alt`. Boolean `and`/`or` supported across `query`/`set`/`remove`: `cell[value>5000 or value<100]`, `cell[(type=Number or type=Date) and value>0]`. Excel row-by-column-name: `Sheet1!row[Salary>5000]`. `set` accepts selectors and Excel-native paths (parity with `get`/`query`). Bare unscoped selectors rejected on `set`/`remove`.
-
-```bash
-officecli query report.docx 'paragraph[style=Normal] > run[font!=Arial]'
-officecli query slides.pptx 'shape[fill=FF0000]'
-```
-
----
-
-## Watch & Interactive Selection
-
-Live HTML preview that auto-refreshes on every file change. Browsers can click / shift-click / box-drag to select shapes; the CLI can read the current browser selection and act on it.
-
-```bash
-officecli watch <file> [--port N]      # Start preview server (default port 26315)
-officecli unwatch <file>               # Stop
-officecli goto <file> <path>           # Scroll watching browser(s) to element (docx: p / table / tr / tc)
-```
-
-Open the printed `http://localhost:N` URL. Click to select; shift/cmd/ctrl+click to multi-select; drag from empty space to box-select. PPT/Word use blue outline; Excel uses native-style green selection (double-click cell to edit inline; drag a chart to reposition).
-
-### `get <file> selected` — read what the user clicked
-
-```bash
-officecli get <file> selected [--json]
-```
-
-Returns DocumentNodes for whatever is currently selected. Empty result if nothing selected. Exit code != 0 if no watch is running.
-
-```bash
-# User clicks shapes in the browser, then asks "make these red"
-PATHS=$(officecli get deck.pptx selected --json | jq -r '.data.Results[].path')
-for p in $PATHS; do officecli set deck.pptx "$p" --prop fill=FF0000; done
-```
-
-### Key properties
-
-- **Selection survives file edits.** Paths use stable `@id=` form.
-- **All connected browsers share one selection.** Last-write-wins.
-- **Same-file single-watch.** A given file can have only one watch process at a time.
-- **Group shapes select as a whole.** Drilling into individual children of a group is not supported in v1.
-- **Coverage:** `.pptx` shapes/pictures/tables/charts/connectors/groups; `.docx` top-level paragraphs and tables. Inherited layout/master decorations and Word nested elements (table cells, run-level) are not addressable. **`.xlsx` does not emit `data-path`** — `mark`/`selection` on xlsx always resolve `stale=true` (v2 candidate).
-
-### Marks — edit proposals waiting for review
-
-Use `mark` when changes need human review BEFORE they hit the file. Marks live in the watch process only; a separate `set` pipeline applies accepted ones. For one-shot changes use `set` directly; for permanent file annotations use `add --type comment` (Word native).
-
-```bash
-officecli mark <file> <path> [--prop find=... color=... note=... tofix=... regex=true] [--json]
-officecli unmark <file> [--path <p> | --all] [--json]
-officecli get-marks <file> [--json]
-```
-
-Props: `find` (literal or regex when `regex=true`; raw form `find='r"[abc]"'`), `color` (hex / `rgb(...)` / 22 named whitelist), `note`, `tofix` (drives apply pipeline). **Path** must be `data-path` format from watch HTML — see subskills for full pipeline.
-
----
-
-## L2: DOM Operations
-
-### set — modify properties
-
-```bash
-officecli set <file> <path> --prop key=value [--prop ...]
-```
-
-**Any XML attribute is settable** via element path (found via `get --depth N`) — even attributes not currently present. Without `find=`, `set` applies format to the entire element.
-
-**Value formats:**
-
-| Type | Format | Examples |
-|------|--------|---------|
-| Colors | Hex (with/without `#`), named, RGB, theme | `FF0000`, `#FF0000`, `red`, `rgb(255,0,0)`, `accent1`..`accent6` |
-| Spacing | Unit-qualified | `12pt`, `0.5cm`, `1.5x`, `150%` |
-| Dimensions | EMU or suffixed | `914400`, `2.54cm`, `1in`, `72pt`, `96px` |
-
-**Dotted-attr aliases** — `font.<attr>` forms accepted on shape/run/paragraph/table/row/cell/section/styles, e.g. `--prop font.color=red --prop font.bold=true --prop font.size=14pt`. Run `officecli help <fmt> <element>` for the full list.
-
-### find — format or replace matched text
-
-Use top-level `--find` / `--replace` on `set` (and `--find` on `query`). Legacy `--prop find=X` still works but emits a hint.
-
-```bash
-# Format matched text (auto-splits runs)
-officecli set doc.docx '/body/p[1]' --find weather --prop bold=true --prop color=red
-
-# Regex matching (regex= still a prop flag)
-officecli set doc.docx '/body/p[1]' --find '\d+%' --prop regex=true --prop color=red
-
-# Replace text (use `/` for whole-document scope)
-officecli set doc.docx / --find draft --replace final
-
-# docx: tracked Find&Replace
-officecli set doc.docx / --find draft --replace final --prop revision.author=Alice
-
-# PPT — same syntax, different paths
-officecli set slides.pptx / --find draft --replace final
-```
-
-**Path controls search scope:** `/` = whole document, `/body/p[1]` or `/slide[N]/shape[M]` = specific element, `/header[1]` / `/footer[1]` = headers/footers.
-
-**Notes:**
-- Case-sensitive by default. Case-insensitive: `--prop 'find=(?i)error' --prop regex=true`
-- Matches work across run boundaries
-- No match = silent success. `--json` includes `"matched": N`
-- **Excel:** only `find` + `replace` supported (no find + format props)
-
-### add — add elements or clone
-
-```bash
-officecli add <file> <parent> --type <type> [--prop ...]
-officecli add <file> <parent> --type <type> --after <path> [--prop ...]   # insert after anchor
-officecli add <file> <parent> --type <type> --before <path> [--prop ...]  # insert before anchor
-officecli add <file> <parent> --type <type> --index N [--prop ...]        # 0-based position (legacy)
-officecli add <file> <parent> --from <path>                               # clone existing element
-```
-
-`--after`, `--before`, `--index` are mutually exclusive. No position flag = append to end.
-
-**Element types (with aliases):**
-
-| Format | Types |
-|--------|-------|
-| **pptx** | slide (incl. hidden), shape (font.latin/ea/cs, direction=rtl, underline.color, highlight=COLOR (Add/Set/Get/HTML preview), effective.X+effective.X.src; arrow alias for rightArrow; slideMaster/slideLayout typed add/set/remove), picture (SVG, brightness/contrast/glow/shadow, rotation, link, tooltip), chart (direction=rtl, pieOfPie, barOfPie, axisLine/gridline per-attr setters, animation+chartBuild=byCategory|bySeries, line dropLines/hiLowLines/upDownBars, anchor=x,y,w,h shorthand), table (cell direction=rtl, fill/background, built-in PowerPoint style catalogue, /col[C] get + swap/copyFrom, row/col Move/CopyFrom), row (tr), connector (from/to accept full-path `@name=`/`@id=` forms — bare `@name=Foo` is rejected, must be `/slide[N]/shape[@name=Foo]` — startshape/endshape SetByPath; edge-to-edge anchoring by default, fromSide/toSide to force an edge, fromIdx/toIdx for raw cxn index), group (link, tooltip, deep walk by get/query/add/remove, ungroup=true dissolves back to slide-absolute), align/distribute (targets= accepts shape[@id=N] paths, not just positional), video/audio (loop, autoStart alias), equation, notes (direction=rtl, lang), comment (legacy + modern p188 threaded round-trip), animation (15 emphasis + 16 exit presets, multi-effect chains, motion-path presets, repeat/restart/autoReverse, chart animations), transition (12 p15 presets + morph/p14), paragraph (para), run, zoom, ole (preview=, full dump round-trip via add-part+raw-set), placeholder (phType=...), model3d (rotation=ax,ay,az; full dump round-trip), smartart (dump round-trip via add-part), diagram (add-only mermaid → native shapes or rendered image, `--type diagram`/`flowchart`). |
-| **docx** | paragraph (direction/font.latin/ea/cs, bold.cs/italic.cs/size.cs, lang.latin/ea/cs, wordWrap, framePr.\*, tabs shorthand), run (lang slots, direction, underline.color, position half-pts, **revision.type=ins\|del\|format\|moveFrom\|moveTo + revision.action=accept\|reject** with .author/.date — bare `@author=`/`@type=` selector on `set /revision[...]` for filtered accept/reject, but `query 'revision[...]'` needs the dotted `revision.author=`/`revision.type=` form; move+revision is run-level paths only, not paragraph-level; **range=START:END** on a paragraph/shape path formats a char span by explicit 0-based half-open offset instead of addressing a run — the offset sibling of find=), table (direction=rtl, hMerge, cantSplit on row/nowrap on cell (both add+set), **virtual column ops**: add/remove/move/copyfrom on /body/tbl[N]/col), row (tr), cell (td), image, header/footer (direction), section (pageNumFmt full enum, direction=rtl, rtlGutter, pgBorders=box), bookmark, comment, footnote, endnote, formfield, sdt, chart, equation, field (28 types), hyperlink, style (direction, indents, pbdr, lineSpacing on Add/Set), toc, watermark, break, ole, **num/abstractNum/lvl**, **tab**, **textbox/shape** (add-mostly — Get returns raw XML preview only, no structured readback; Set is limited to width/height/geometry/fill/line.\*; position is `anchor.x`/`anchor.y` not bare x/y; **textbox-only** `textDirection`/rotation/gradient/shadow — docx shape itself has neither rotation nor gradient), embedded **OLE round-trip on dump→batch**, **diagram** (add-only mermaid → native shapes or rendered image, `--type diagram`/`flowchart`, no x/y at add-time — reposition via `set /body/group[N]`). docDefaults.rtl, autoHyphenation, `get /` exposes locale + /comments /footnotes /endnotes. `create --minimal` for raw OOXML scaffolding. |
-| **xlsx** | sheet (visible/hidden/veryHidden, print margins, printTitleRows/Cols, rightToLeft sheetView, cascade-aware rename), row (c{N}= cell-content shorthand; add accepts --from /Sheet/col[L]; formula-ref rewrite on insert), col (formula-ref rewrite, named-range follow on move), cell (type=richtext+runs, merge=range/sweep, direction=rtl, phonetic; **--shift left\|up on remove, shift=right\|down on add** — Excel UI dialog parity; formula auto-detect; OFFSET/INDIRECT in calc), chart (per-axis RTL/title, anchor=x,y,w,h, pareto), image (SVG), comment (direction=rtl), table (listobject), namedrange (definedname, volatile, `[@name=X]`; formula-body inlined at parse), pivottable (cache CoW + cross-pivot sharing, labelFilter=field:type:value add-time-only, topN=integer add-time-only, fillDownLabels is an alias of repeatLabels not a separate feature, calculatedField), sparkline, validation, autofilter, shape, textbox, CF (databar/colorscale/iconset/formulacf/cellIs/topN/aboveAverage), ole, csv. Query supports `merge`/`mergedrange`. Workbook: password. Shape selector enumerates leaves inside grpSp. |
-
-### Pivot tables (xlsx)
-
-```bash
-officecli add data.xlsx /Sheet1 --type pivottable \
-  --prop source="Sheet1!A1:E100" --prop rows=Region,Category \
-  --prop cols=Year --prop values="Sales:sum,Qty:count" \
-  --prop grandTotals=rows --prop subtotals=off --prop sort=asc
-```
-
-Key props: `rows`, `cols`, `values` (Field:func[:showDataAs]), `filters`, `source`, `position`, `layout` (compact/outline/tabular), `repeatLabels`, `blankRows`, `aggregate`, `showDataAs` (percent_of_total/row/col, running_total), `grandTotals`, `subtotals`, `sort`. Aggregators: sum, count, average, max, min, product, stdDev, stdDevp, var, varp, countNums. Date columns auto-group. Run `officecli help xlsx pivottable` for full schema.
-
-### Document-level properties (all formats)
-
-```bash
-officecli set doc.docx / --prop docDefaults.font=Arial --prop docDefaults.fontSize=11pt
-officecli set doc.docx / --prop protection=forms --prop evenAndOddHeaders=true
-officecli set data.xlsx / --prop calc.mode=manual --prop calc.refMode=r1c1
-officecli set slides.pptx / --prop defaultFont=Arial --prop show.loop=true --prop print.what=handouts
-```
-
-Run `officecli help <format> /` for all document-level properties (docDefaults, docGrid, CJK spacing, calc, print, show, theme, extended).
-
-### Sort (xlsx)
-
-```bash
-officecli set data.xlsx /Sheet1 --prop sort="C desc" --prop sortHeader=true
-officecli set data.xlsx '/Sheet1/A1:D100' --prop sort="A asc" --prop sortHeader=true
-```
-
-Format: `COL DIR[, COL DIR ...]`. Rejects ranges with merged cells or formulas. Sidecar metadata (hyperlinks, comments, conditional formatting, drawings) follows rows automatically.
-
-### Text-anchored insert (`--after find:X` / `--before find:X`)
-
-Locate an insertion point by text match within a paragraph. Inline types (run, picture, hyperlink) insert within the paragraph; block types (table, paragraph) auto-split it. PPT only supports inline.
-
-```bash
-# Word: inline run after matched text
-officecli add doc.docx '/body/p[1]' --type run --after find:weather --prop text=" (sunny)"
-
-# Word: block table after matched text (auto-splits paragraph)
-officecli add doc.docx '/body/p[1]' --type table --after "find:First sentence." --prop rows=2 --prop cols=2
-```
-
-### Clone
-
-`officecli add <file> / --from '/slide[1]'` — copies with all cross-part relationships.
-
-### move, swap, remove
-
-```bash
-officecli move <file> <path> [--to <parent>] [--index N] [--after <path>] [--before <path>]
-officecli swap <file> <path1> <path2>
-officecli remove <file> '/body/p[4]'
-```
-
-When using `--after` or `--before`, `--to` can be omitted — the target container is inferred from the anchor.
-
-### batch — multiple operations in one save cycle
-
-**Atomic by default (v1.0.137+):** every item still runs and is reported (so `N succeeded, M failed` stays meaningful and every failure surfaces), but if *any* item fails the whole batch rolls back — the file on disk is left byte-identical to before the batch ran (confirmed live in both standalone and resident mode). Use `--best-effort` to restore the old apply-what-succeeds behavior (useful for lossy `dump→batch` replays where losing the whole thing over one unsupported item is worse than a partial result). `--stop-on-error` only changes how early the run stops (remaining items are `skipped`), not whether what ran gets kept — combine it with `--best-effort` if you want "stop at first failure but keep what already succeeded." `--force` is unrelated — it's only the docx-protection bypass. Failed items carry a machine-readable `code` field (same list as `error.code`); a rolled-back batch's JSON summary carries `"atomicRolledBack": true`.
-
-`officecli dump <file> [<path>]` emits a replayable batch JSON for round-trip — `.docx` (full coverage), `.pptx` (text/tables/pictures/charts/notes/theme + OLE/3D/video/audio/SmartArt/morph/p15 transitions via raw-set passthrough), and `.xlsx` (cells/formulas/styles + tables, conditional formatting, validations, comments, charts, sparklines, pictures, shapes, pivot tables; slicers/chartEx/OLE via verbatim carrier). Path defaults to `/` (whole document); pass a subtree path (docx: `/body`, `/body/p[N]`, `/body/tbl[N]`, `/theme`, `/settings`, `/numbering`, `/styles`; xlsx: `/SheetName`, `/sheet[N]`) to scope the dump. `officecli refresh <file.docx>` recalculates TOC page numbers / PAGE / cross-references after replay (Word backend on Windows; headless-HTML fallback elsewhere). `officecli plugins list` extends support to `.doc`, `.hwpx`, `.pdf` export.
-
-```bash
-echo '[
-  {"command":"set","path":"/Sheet1/A1","props":{"value":"Name","bold":"true"}},
-  {"command":"set","path":"/Sheet1/B1","props":{"value":"Score","bold":"true"}}
-]' | officecli batch data.xlsx --json
-
-officecli batch data.xlsx --commands '[{"op":"set","path":"/Sheet1/A1","props":{"value":"Done"}}]' --json
-officecli batch data.xlsx --input updates.json --best-effort --json   # keep whatever succeeds even if some items fail
-```
-
-Supports: `add`, `set`, `get`, `query`, `remove`, `move`, `swap`, `view`, `raw`, `raw-set`, `validate`. Fields: `command` (or `op`), `path`, `parent`, `type`, `from`, `to`, `index`, `after`, `before`, `props`, `selector`, `mode`, `depth`, `part`, `xpath`, `action`, `xml`.
-
----
-
-## L3: Raw XML
-
-Use when L2 cannot express what you need. No xmlns declarations needed — prefixes auto-registered.
-
-```bash
-officecli raw <file> <part>                          # view raw XML
-officecli raw-set <file> <part> --xpath "..." --action replace --xml '<w:p>...</w:p>'
-officecli add-part <file> <parent>                   # create new document part (returns rId)
-```
-
-`raw-set` actions: `append`, `prepend`, `insertbefore`, `insertafter`, `replace`, `remove`, `setattr`. Run `officecli help <format> raw` for available parts.
-
----
-
-## Common Pitfalls
-
-| Pitfall | Correct Approach |
-|---------|-----------------|
-| `--name "foo"` | Use `--prop name="foo"` — all attributes go through `--prop` |
-| Unquoted `[N]` paths in zsh/bash | Always quote: `'/slide[1]'` or `"/slide[1]"` (shell glob-expands brackets) |
-| PPT `shape[1]` for content | `shape[1]` is typically the title placeholder. Use `shape[2]+` for content shapes |
-| `/shape[myname]` | Name indexing not supported. Use numeric index or `@name=` (PPT only) |
-| Guessing property names | Run `officecli help <format> <element>` to see exact names |
-| Modifying an open file | Close the file in PowerPoint/WPS first |
-| `\n` in shell strings | Use `\\n` for newlines in `--prop text="..."` |
-| `$` in shell text | `--prop text="$15M"` strips `$15`. Use single quotes: `--prop text='$15M'`, or heredoc batch |
-
----
-
-## Specialized Skills
-
-`officecli load_skill <name>` — output is a SKILL.md, follow its rules.
-
-**Loading rule**:
-- Pick the most specific match in "When to use"; if none fits, load the format default (`word` / `pptx` / `excel`).
-- Scenes already contain the format default's rules — load **one** skill per artifact, never stack.
-- Loaded rules persist across turns; don't re-load each reply.
-- Two distinct artifacts → two separate loads.
-
-### Word (.docx)
-
-| Name | When to use |
-|------|-------------|
-| `word` | Reports, letters, memos, proposals, generic documents |
-| `academic-paper` | Journal / conference / thesis: APA / Chicago / IEEE / MLA citations, equations, SEQ + PAGEREF cross-refs, multi-column journal layout, bibliography. NOT for business reports or letters (route those to `word`) |
-
-### PowerPoint (.pptx)
-
-| Name | When to use |
-|------|-------------|
-| `pptx` | Generic decks: board reviews, sales decks, all-hands, product launches |
-| `pitch-deck` | **Fundraising only** — seed / Series A-C / SAFE / convertible / strategic raise. NOT for sales / product / board decks (route those to `pptx`) |
-| `morph-ppt` | Cinematic Morph-animated presentations. NOT for static decks (route those to `pptx`) |
-| `morph-ppt-3d` | 3D Morph: GLB models, camera moves, depth. NOT for 2D-only Morph (route those to `morph-ppt`) |
-
-### Excel (.xlsx)
-
-| Name | When to use |
-|------|-------------|
-| `excel` | Generic workbooks, formulas, pivots, trackers |
-| `financial-model` | Financial models, scenarios, projections. NOT for general data analysis (route those to `excel`) |
-| `data-dashboard` | CSV/tabular data → KPI / analytics / executive dashboards with charts and sparklines. NOT for raw data tracking (route those to `excel`) |
-
-Example: a fundraising deck task → `officecli load_skill pitch-deck` → use the printed rules.
-
----
-
-## Notes
-
-- Paths are **1-based** (XPath convention): `'/body/p[3]'` = third paragraph
-- `--index` is **0-based** (array convention): `--index 0` = first position
-- **Excel exception**: for `add --type row` and `add --type col`, `--index N` is **1-based** (matches OOXML RowIndex / column letter index). `--index 5` inserts at row 5 / column 5.
-- After modifications, verify with `validate` and/or `view issues`
-- **When unsure**, run `officecli help <format> <element>` instead of guessing