@lemoncat7/dsh-partner 2.8.0 → 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (793) hide show
  1. package/docs/THIRD_PARTY_LICENSES.md +10 -0
  2. package/docs/blocked-delivery-recovery.md +7 -0
  3. package/docs/concern-attention.md +19 -0
  4. package/docs/observation-contract.md +11 -0
  5. package/docs/releases/2.8.1.md +11 -0
  6. package/docs/releases/2.8.2.md +7 -0
  7. package/lib/agent-runtime.d.ts +7 -0
  8. package/lib/agent-runtime.d.ts.map +1 -1
  9. package/lib/agent-runtime.js +110 -32
  10. package/lib/agent-runtime.js.map +1 -1
  11. package/lib/api.js +28 -6
  12. package/lib/api.js.map +1 -1
  13. package/lib/autonomy.d.ts.map +1 -1
  14. package/lib/autonomy.js +5 -0
  15. package/lib/autonomy.js.map +1 -1
  16. package/lib/channel-message.d.ts +2 -0
  17. package/lib/channel-message.d.ts.map +1 -1
  18. package/lib/channel-message.js.map +1 -1
  19. package/lib/channels/manager.d.ts.map +1 -1
  20. package/lib/channels/manager.js +4 -3
  21. package/lib/channels/manager.js.map +1 -1
  22. package/lib/channels/notification-delivery.d.ts.map +1 -1
  23. package/lib/channels/notification-delivery.js +10 -0
  24. package/lib/channels/notification-delivery.js.map +1 -1
  25. package/lib/channels/weixin/connect.d.ts.map +1 -1
  26. package/lib/client-api.d.ts +7 -2
  27. package/lib/client-api.d.ts.map +1 -1
  28. package/lib/client-api.js +8 -1
  29. package/lib/client-api.js.map +1 -1
  30. package/lib/client.d.ts.map +1 -1
  31. package/lib/client.js +2115 -1884
  32. package/lib/client.js.map +4 -4
  33. package/lib/collaboration/composition.js +1 -1
  34. package/lib/collaboration/composition.js.map +1 -1
  35. package/lib/concern-attention.d.ts +19 -0
  36. package/lib/concern-attention.d.ts.map +1 -0
  37. package/lib/concern-attention.js +36 -0
  38. package/lib/concern-attention.js.map +1 -0
  39. package/lib/concern-domain.d.ts +7 -1
  40. package/lib/concern-domain.d.ts.map +1 -1
  41. package/lib/concern-domain.js +4 -0
  42. package/lib/concern-domain.js.map +1 -1
  43. package/lib/concern-execution-notes.d.ts +18 -0
  44. package/lib/concern-execution-notes.d.ts.map +1 -0
  45. package/lib/concern-execution-notes.js +41 -0
  46. package/lib/concern-execution-notes.js.map +1 -0
  47. package/lib/concern-execution-receipt.d.ts +4 -0
  48. package/lib/concern-execution-receipt.d.ts.map +1 -0
  49. package/lib/concern-execution-receipt.js +28 -0
  50. package/lib/concern-execution-receipt.js.map +1 -0
  51. package/lib/concern-execution.d.ts +30 -0
  52. package/lib/concern-execution.d.ts.map +1 -0
  53. package/lib/concern-execution.js +86 -0
  54. package/lib/concern-execution.js.map +1 -0
  55. package/lib/concern-management-tool.d.ts.map +1 -1
  56. package/lib/concern-management-tool.js +21 -19
  57. package/lib/concern-management-tool.js.map +1 -1
  58. package/lib/concern-note-source.d.ts +7 -0
  59. package/lib/concern-note-source.d.ts.map +1 -0
  60. package/lib/concern-note-source.js +25 -0
  61. package/lib/concern-note-source.js.map +1 -0
  62. package/lib/concern-resources.d.ts +27 -0
  63. package/lib/concern-resources.d.ts.map +1 -0
  64. package/lib/concern-resources.js +36 -0
  65. package/lib/concern-resources.js.map +1 -0
  66. package/lib/concern-sources.d.ts +1 -1
  67. package/lib/concern-sources.d.ts.map +1 -1
  68. package/lib/concern-store.d.ts +9 -1
  69. package/lib/concern-store.d.ts.map +1 -1
  70. package/lib/concern-store.js +111 -34
  71. package/lib/concern-store.js.map +1 -1
  72. package/lib/concern-tool.d.ts +1 -0
  73. package/lib/concern-tool.d.ts.map +1 -1
  74. package/lib/concern-tool.js +8 -4
  75. package/lib/concern-tool.js.map +1 -1
  76. package/lib/decision/conversation.d.ts +11 -0
  77. package/lib/decision/conversation.d.ts.map +1 -0
  78. package/lib/decision/conversation.js +52 -0
  79. package/lib/decision/conversation.js.map +1 -0
  80. package/lib/decision/events.d.ts +16 -0
  81. package/lib/decision/events.d.ts.map +1 -0
  82. package/lib/decision/events.js +9 -0
  83. package/lib/decision/events.js.map +1 -0
  84. package/lib/domain.d.ts +2 -0
  85. package/lib/domain.d.ts.map +1 -1
  86. package/lib/domain.js.map +1 -1
  87. package/lib/heartbeat.d.ts +12 -10
  88. package/lib/heartbeat.d.ts.map +1 -1
  89. package/lib/heartbeat.js +54 -26
  90. package/lib/heartbeat.js.map +1 -1
  91. package/lib/index.d.ts.map +1 -1
  92. package/lib/index.js +3 -0
  93. package/lib/index.js.map +1 -1
  94. package/lib/memory-prompt-context.js +1 -0
  95. package/lib/memory-prompt-context.js.map +1 -1
  96. package/lib/memory-reflection.js +1 -1
  97. package/lib/observation-contract.d.ts +9 -0
  98. package/lib/observation-contract.d.ts.map +1 -0
  99. package/lib/observation-contract.js +19 -0
  100. package/lib/observation-contract.js.map +1 -0
  101. package/lib/observation-executor.d.ts +5 -0
  102. package/lib/observation-executor.d.ts.map +1 -1
  103. package/lib/observation-executor.js +17 -1
  104. package/lib/observation-executor.js.map +1 -1
  105. package/lib/observation-loop.d.ts +5 -0
  106. package/lib/observation-loop.d.ts.map +1 -1
  107. package/lib/observation-loop.js +36 -3
  108. package/lib/observation-loop.js.map +1 -1
  109. package/lib/observation-sources.d.ts +14 -0
  110. package/lib/observation-sources.d.ts.map +1 -0
  111. package/lib/observation-sources.js +12 -0
  112. package/lib/observation-sources.js.map +1 -0
  113. package/lib/requirements/service.d.ts.map +1 -1
  114. package/lib/requirements/service.js +2 -0
  115. package/lib/requirements/service.js.map +1 -1
  116. package/lib/requirements/worker.d.ts.map +1 -1
  117. package/lib/requirements/worker.js +1 -0
  118. package/lib/requirements/worker.js.map +1 -1
  119. package/lib/scheduler/continuations.d.ts.map +1 -1
  120. package/lib/scheduler/continuations.js +17 -3
  121. package/lib/scheduler/continuations.js.map +1 -1
  122. package/lib/skills/builtin.d.ts.map +1 -1
  123. package/lib/skills/builtin.js +2 -0
  124. package/lib/skills/builtin.js.map +1 -1
  125. package/lib/skills/drama-builtin.d.ts.map +1 -1
  126. package/lib/skills/drama-builtin.js +13 -1
  127. package/lib/skills/drama-builtin.js.map +1 -1
  128. package/lib/skills/story-builtins.d.ts +3 -0
  129. package/lib/skills/story-builtins.d.ts.map +1 -0
  130. package/lib/skills/story-builtins.js +72 -0
  131. package/lib/skills/story-builtins.js.map +1 -0
  132. package/lib/skills/viora-canvas-builtin.d.ts.map +1 -1
  133. package/lib/skills/viora-canvas-builtin.js +12 -1
  134. package/lib/skills/viora-canvas-builtin.js.map +1 -1
  135. package/lib/snapshot-cache.d.ts +7 -0
  136. package/lib/snapshot-cache.d.ts.map +1 -0
  137. package/lib/snapshot-cache.js +26 -0
  138. package/lib/snapshot-cache.js.map +1 -0
  139. package/lib/tasks/result.js +1 -1
  140. package/lib/tasks/result.js.map +1 -1
  141. package/lib/ui/memory/concern-board.d.ts +1 -1
  142. package/lib/ui/memory/concern-board.d.ts.map +1 -1
  143. package/lib/ui/memory/concern-board.js +12 -6
  144. package/lib/ui/memory/concern-board.js.map +1 -1
  145. package/lib/ui/memory/concern-deletion.d.ts.map +1 -1
  146. package/lib/ui/memory/concern-deletion.js +2 -1
  147. package/lib/ui/memory/concern-deletion.js.map +1 -1
  148. package/lib/ui/memory/concern-editor.d.ts.map +1 -1
  149. package/lib/ui/memory/concern-editor.js +5 -3
  150. package/lib/ui/memory/concern-editor.js.map +1 -1
  151. package/lib/ui/memory/concern-execution-fields.d.ts +7 -0
  152. package/lib/ui/memory/concern-execution-fields.d.ts.map +1 -0
  153. package/lib/ui/memory/concern-execution-fields.js +8 -0
  154. package/lib/ui/memory/concern-execution-fields.js.map +1 -0
  155. package/lib/ui/memory/concern-feedback.d.ts +15 -0
  156. package/lib/ui/memory/concern-feedback.d.ts.map +1 -0
  157. package/lib/ui/memory/concern-feedback.js +13 -0
  158. package/lib/ui/memory/concern-feedback.js.map +1 -0
  159. package/lib/ui/memory/concern-panel.d.ts.map +1 -1
  160. package/lib/ui/memory/concern-panel.js +11 -3
  161. package/lib/ui/memory/concern-panel.js.map +1 -1
  162. package/package.json +6 -1
  163. package/resources/skills/novel-game-creation/manifest.json +393 -0
  164. package/resources/skills/novel-game-creation/upstream/LICENSE +21 -0
  165. package/resources/skills/novel-game-creation/upstream/README.md +420 -0
  166. package/resources/skills/novel-game-creation/upstream/README_ZH.md +421 -0
  167. package/resources/skills/novel-game-creation/upstream/VERSION +1 -0
  168. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/BUILD_BRIEF.md +219 -0
  169. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/.vercelignore +3 -0
  170. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/RUN.md +138 -0
  171. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/bg/compound_new_guofeng.webp +0 -0
  172. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/fate_coda.webp +0 -0
  173. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/meng.webp +0 -0
  174. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/pan.webp +0 -0
  175. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/pinger.webp +0 -0
  176. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/xuee.webp +0 -0
  177. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/finale/yue.webp +0 -0
  178. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_accord_five.webp +0 -0
  179. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_afterglow_five.webp +0 -0
  180. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/inner_court_alliance.webp +0 -0
  181. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day10.webp +0 -0
  182. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day15.webp +0 -0
  183. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/public_day5.webp +0 -0
  184. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/group/title_new_guofeng.webp +0 -0
  185. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/pan_pinger.webp +0 -0
  186. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/pan_xuee.webp +0 -0
  187. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_meng.webp +0 -0
  188. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pan.webp +0 -0
  189. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/joint/yue_pinger.webp +0 -0
  190. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/meng/explicit.webp +0 -0
  191. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/meng/prelude.webp +0 -0
  192. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/meng.webp +0 -0
  193. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/pan.webp +0 -0
  194. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/pinger.webp +0 -0
  195. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/xuee.webp +0 -0
  196. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/milestone/yue.webp +0 -0
  197. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pan/explicit.webp +0 -0
  198. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pan/prelude.webp +0 -0
  199. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pinger/explicit.webp +0 -0
  200. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/pinger/prelude.webp +0 -0
  201. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/xuee/explicit.webp +0 -0
  202. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/xuee/prelude.webp +0 -0
  203. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/yue/explicit.webp +0 -0
  204. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/cg/yue/prelude.webp +0 -0
  205. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/meng/night.webp +0 -0
  206. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/pan/night.webp +0 -0
  207. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/pinger/night.webp +0 -0
  208. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/xuee/night.webp +0 -0
  209. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/heroine/yue/night.webp +0 -0
  210. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/household/li_jiaoer.webp +0 -0
  211. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/assets/scene/gate_collector.webp +0 -0
  212. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/css/new-guofeng.css +1494 -0
  213. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/css/style.css +2508 -0
  214. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/index.html +18 -0
  215. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/assets.js +147 -0
  216. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/audio.js +332 -0
  217. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/data.js +7470 -0
  218. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/engine.js +13066 -0
  219. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/main.js +3051 -0
  220. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/js/text.js +32 -0
  221. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/package.json +11 -0
  222. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/app/vercel.json +23 -0
  223. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/build/art/generated-art.json +161 -0
  224. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/example.json +12 -0
  225. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/README.md +26 -0
  226. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/playable-model.json +99 -0
  227. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/verification.json +41 -0
  228. package/resources/skills/novel-game-creation/upstream/examples/jin-ping-mei/qa/verify_playable_model.mjs +152 -0
  229. package/resources/skills/novel-game-creation/upstream/manifest.json +498 -0
  230. package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/SKILL.md +47 -0
  231. package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/agents/openai.yaml +4 -0
  232. package/resources/skills/novel-game-creation/upstream/skills/game-art-direction/references/art-direction-method.md +40 -0
  233. package/resources/skills/novel-game-creation/upstream/skills/game-build/SKILL.md +66 -0
  234. package/resources/skills/novel-game-creation/upstream/skills/game-build/agents/openai.yaml +4 -0
  235. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/blender-asset-workflow.md +24 -0
  236. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/build-brief-contract.md +72 -0
  237. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/generative-media-pipeline.md +25 -0
  238. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/playable-model-contract.md +46 -0
  239. package/resources/skills/novel-game-creation/upstream/skills/game-build/references/tts-production-contract.md +23 -0
  240. package/resources/skills/novel-game-creation/upstream/skills/game-concept/SKILL.md +54 -0
  241. package/resources/skills/novel-game-creation/upstream/skills/game-concept/agents/openai.yaml +4 -0
  242. package/resources/skills/novel-game-creation/upstream/skills/game-concept/references/concept-method.md +47 -0
  243. package/resources/skills/novel-game-creation/upstream/skills/game-qa/SKILL.md +39 -0
  244. package/resources/skills/novel-game-creation/upstream/skills/game-qa/agents/openai.yaml +4 -0
  245. package/resources/skills/novel-game-creation/upstream/skills/game-qa/references/qa-contract.md +66 -0
  246. package/resources/skills/novel-game-creation/upstream/skills/game-qa/references/test-design-method.md +49 -0
  247. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/SKILL.md +50 -0
  248. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/agents/openai.yaml +4 -0
  249. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/narrative-design-method.md +35 -0
  250. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/numeric-design-method.md +22 -0
  251. package/resources/skills/novel-game-creation/upstream/skills/game-world-design/references/world-design-method.md +57 -0
  252. package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/SKILL.md +68 -0
  253. package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/agents/openai.yaml +4 -0
  254. package/resources/skills/novel-game-creation/upstream/skills/novel-game-analyze/references/gameability-protocol.md +65 -0
  255. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/SKILL.md +54 -0
  256. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/agents/openai.yaml +4 -0
  257. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/references/intake-method.md +50 -0
  258. package/resources/skills/novel-game-creation/upstream/skills/novel-to-game/references/pipeline-contract.md +60 -0
  259. package/resources/skills/story-creation/manifest.json +1477 -0
  260. package/resources/skills/story-creation/upstream/LICENSE.upstream +21 -0
  261. package/resources/skills/story-creation/upstream/manifest.json +1861 -0
  262. package/resources/skills/story-creation/upstream/roles/chapter-extractor.md +254 -0
  263. package/resources/skills/story-creation/upstream/roles/character-designer.md +168 -0
  264. package/resources/skills/story-creation/upstream/roles/consistency-checker.md +192 -0
  265. package/resources/skills/story-creation/upstream/roles/narrative-writer.md +105 -0
  266. package/resources/skills/story-creation/upstream/roles/story-architect.md +139 -0
  267. package/resources/skills/story-creation/upstream/roles/story-explorer.md +262 -0
  268. package/resources/skills/story-creation/upstream/roles/story-researcher.md +303 -0
  269. package/resources/skills/story-creation/upstream/skills/browser-cdp/SKILL.md +175 -0
  270. package/resources/skills/story-creation/upstream/skills/story/SKILL.md +129 -0
  271. package/resources/skills/story-creation/upstream/skills/story/VERSION +1 -0
  272. package/resources/skills/story-creation/upstream/skills/story/references/author-memory-maintenance.md +75 -0
  273. package/resources/skills/story-creation/upstream/skills/story/references/author-memory.md +117 -0
  274. package/resources/skills/story-creation/upstream/skills/story/scripts/author_memory_commit.py +1635 -0
  275. package/resources/skills/story-creation/upstream/skills/story-cover/SKILL.md +336 -0
  276. package/resources/skills/story-creation/upstream/skills/story-cover/references/cover-styles.md +210 -0
  277. package/resources/skills/story-creation/upstream/skills/story-deslop/SKILL.md +274 -0
  278. package/resources/skills/story-creation/upstream/skills/story-deslop/references/agent-calls.md +5 -0
  279. package/resources/skills/story-creation/upstream/skills/story-deslop/references/anti-ai-writing.md +556 -0
  280. package/resources/skills/story-creation/upstream/skills/story-deslop/references/author-memory-maintenance.md +75 -0
  281. package/resources/skills/story-creation/upstream/skills/story-deslop/references/author-memory.md +117 -0
  282. package/resources/skills/story-creation/upstream/skills/story-deslop/references/banned-words.md +151 -0
  283. package/resources/skills/story-creation/upstream/skills/story-deslop/references/deslop-gates.md +171 -0
  284. package/resources/skills/story-creation/upstream/skills/story-deslop/references/style-resolution.md +27 -0
  285. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/author_memory_commit.py +1635 -0
  286. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/check-ai-patterns.js +1586 -0
  287. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/check-degeneration.js +349 -0
  288. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/normalize-punctuation.js +415 -0
  289. package/resources/skills/story-creation/upstream/skills/story-deslop/scripts/style-whitelist.js +39 -0
  290. package/resources/skills/story-creation/upstream/skills/story-import/SKILL.md +127 -0
  291. package/resources/skills/story-creation/upstream/skills/story-import/references/character-state-reverse.md +82 -0
  292. package/resources/skills/story-creation/upstream/skills/story-import/references/deep-analysis.md +47 -0
  293. package/resources/skills/story-creation/upstream/skills/story-import/references/format-and-structure.md +168 -0
  294. package/resources/skills/story-creation/upstream/skills/story-import/references/import-report.md +64 -0
  295. package/resources/skills/story-creation/upstream/skills/story-import/references/import-special-cases.md +34 -0
  296. package/resources/skills/story-creation/upstream/skills/story-import/references/import-tracking.md +95 -0
  297. package/resources/skills/story-creation/upstream/skills/story-import/references/length-routing.md +134 -0
  298. package/resources/skills/story-creation/upstream/skills/story-import/references/outline-reverse-rules.md +79 -0
  299. package/resources/skills/story-creation/upstream/skills/story-import/references/outline-reverse.md +24 -0
  300. package/resources/skills/story-creation/upstream/skills/story-import/references/state-tracking.md +43 -0
  301. package/resources/skills/story-creation/upstream/skills/story-import/references/structure-mapping-long.md +448 -0
  302. package/resources/skills/story-creation/upstream/skills/story-import/references/structure-mapping-short.md +187 -0
  303. package/resources/skills/story-creation/upstream/skills/story-import/references/tracking-initialization.md +31 -0
  304. package/resources/skills/story-creation/upstream/skills/story-import/references/tracking-transaction.md +146 -0
  305. package/resources/skills/story-creation/upstream/skills/story-import/scripts/build_outline_brief.py +332 -0
  306. package/resources/skills/story-creation/upstream/skills/story-import/scripts/check-outline-contract.js +520 -0
  307. package/resources/skills/story-creation/upstream/skills/story-import/scripts/tracking_commit.py +1691 -0
  308. package/resources/skills/story-creation/upstream/skills/story-import/scripts/wordcount_core.py +405 -0
  309. package/resources/skills/story-creation/upstream/skills/story-long-analyze/SKILL.md +140 -0
  310. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/author-facing.md +252 -0
  311. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/deconstruction-notes.md +270 -0
  312. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/final-checks.md +19 -0
  313. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/index-rebuild.md +11 -0
  314. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/inspiration-library.md +105 -0
  315. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/pipeline-ops.md +154 -0
  316. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/semantic-acceptance-fixtures.md +84 -0
  317. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage1-golden-chapters.md +128 -0
  318. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage2-extraction.md +229 -0
  319. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage3-plot-rhythm.md +480 -0
  320. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage4-characters-settings.md +195 -0
  321. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/stage5-report.md +33 -0
  322. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/style-profile-generator.md +76 -0
  323. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/style-profile-protocol.md +133 -0
  324. package/resources/skills/story-creation/upstream/skills/story-long-analyze/references/synthesis-inputs.md +39 -0
  325. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/build_chapter_index.py +466 -0
  326. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/inspect_existing_assets.py +702 -0
  327. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/inspiration_index.py +864 -0
  328. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/manage_analysis_run.py +1145 -0
  329. package/resources/skills/story-creation/upstream/skills/story-long-analyze/scripts/render_relation_chart.py +425 -0
  330. package/resources/skills/story-creation/upstream/skills/story-long-scan/SKILL.md +193 -0
  331. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/genre-trends.md +197 -0
  332. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-ciweimao.md +19 -0
  333. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-fanqie.md +45 -0
  334. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-jjwxc.md +28 -0
  335. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-qidian.md +35 -0
  336. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/platform-qimao.md +27 -0
  337. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/publishing-guide.md +114 -0
  338. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/reader-profiling.md +105 -0
  339. package/resources/skills/story-creation/upstream/skills/story-long-scan/references/topic-decision.md +82 -0
  340. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/aggregate-rank.js +682 -0
  341. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/cdp-utils.js +242 -0
  342. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/ciweimao-rank-scraper.js +240 -0
  343. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/fanqie-rank-scraper.js +658 -0
  344. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/jjwxc-rank-scraper.js +400 -0
  345. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/qidian-rank-scraper.js +592 -0
  346. package/resources/skills/story-creation/upstream/skills/story-long-scan/scripts/qimao-rank-scraper.js +435 -0
  347. package/resources/skills/story-creation/upstream/skills/story-long-write/SKILL.md +136 -0
  348. package/resources/skills/story-creation/upstream/skills/story-long-write/references/agent-calls.md +47 -0
  349. package/resources/skills/story-creation/upstream/skills/story-long-write/references/anti-ai-writing.md +556 -0
  350. package/resources/skills/story-creation/upstream/skills/story-long-write/references/artifact-protocols.md +183 -0
  351. package/resources/skills/story-creation/upstream/skills/story-long-write/references/author-memory-maintenance.md +75 -0
  352. package/resources/skills/story-creation/upstream/skills/story-long-write/references/author-memory.md +117 -0
  353. package/resources/skills/story-creation/upstream/skills/story-long-write/references/banned-words.md +151 -0
  354. package/resources/skills/story-creation/upstream/skills/story-long-write/references/benchmark-recall.md +16 -0
  355. package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-basics.md +445 -0
  356. package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-design-methods.md +428 -0
  357. package/resources/skills/story-creation/upstream/skills/story-long-write/references/character-relations.md +433 -0
  358. package/resources/skills/story-creation/upstream/skills/story-long-write/references/commercial-core-methods.md +502 -0
  359. package/resources/skills/story-creation/upstream/skills/story-long-write/references/cross-book-recall.md +88 -0
  360. package/resources/skills/story-creation/upstream/skills/story-long-write/references/deslop-gates.md +171 -0
  361. package/resources/skills/story-creation/upstream/skills/story-long-write/references/dialogue-mastery.md +288 -0
  362. package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotion-on-page.md +116 -0
  363. package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotional-arc-design.md +313 -0
  364. package/resources/skills/story-creation/upstream/skills/story-long-write/references/emotional-methods.md +209 -0
  365. package/resources/skills/story-creation/upstream/skills/story-long-write/references/female-audience-writing.md +188 -0
  366. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
  367. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
  368. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
  369. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
  370. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
  371. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
  372. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
  373. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
  374. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
  375. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
  376. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
  377. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
  378. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
  379. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
  380. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
  381. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
  382. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
  383. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
  384. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
  385. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
  386. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
  387. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
  388. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
  389. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
  390. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
  391. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
  392. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
  393. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
  394. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
  395. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
  396. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
  397. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
  398. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-prose-cards.md +91 -0
  399. package/resources/skills/story-creation/upstream/skills/story-long-write/references/genre-readers.md +235 -0
  400. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-chapter-hooks.md +225 -0
  401. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-chapter-quality.md +177 -0
  402. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-format.md +36 -0
  403. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-genre-catalog.md +394 -0
  404. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-genre-mechanics.md +415 -0
  405. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-reversal.md +335 -0
  406. package/resources/skills/story-creation/upstream/skills/story-long-write/references/long-suspense.md +352 -0
  407. package/resources/skills/story-creation/upstream/skills/story-long-write/references/opening-design.md +349 -0
  408. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-conflict.md +457 -0
  409. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-methods.md +287 -0
  410. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-rhythm.md +389 -0
  411. package/resources/skills/story-creation/upstream/skills/story-long-write/references/outline-structure-theory.md +273 -0
  412. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-core-methods.md +522 -0
  413. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-emotion-system.md +354 -0
  414. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-frameworks.md +711 -0
  415. package/resources/skills/story-creation/upstream/skills/story-long-write/references/plot-special-topics.md +622 -0
  416. package/resources/skills/story-creation/upstream/skills/story-long-write/references/project-files.md +114 -0
  417. package/resources/skills/story-creation/upstream/skills/story-long-write/references/reader-contract-and-progression.md +117 -0
  418. package/resources/skills/story-creation/upstream/skills/story-long-write/references/reference-index.md +86 -0
  419. package/resources/skills/story-creation/upstream/skills/story-long-write/references/stop-and-fix.md +9 -0
  420. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-combat-face.md +352 -0
  421. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-craft.md +327 -0
  422. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-genre-modules.md +510 -0
  423. package/resources/skills/story-creation/upstream/skills/story-long-write/references/style-resolution.md +27 -0
  424. package/resources/skills/story-creation/upstream/skills/story-long-write/references/tracking-initialization.md +31 -0
  425. package/resources/skills/story-creation/upstream/skills/story-long-write/references/tracking-transaction.md +146 -0
  426. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-chapter.md +104 -0
  427. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-daily.md +75 -0
  428. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-outline.md +157 -0
  429. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-revision.md +89 -0
  430. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-setup.md +170 -0
  431. package/resources/skills/story-creation/upstream/skills/story-long-write/references/workflow-volume.md +142 -0
  432. package/resources/skills/story-creation/upstream/skills/story-long-write/references/writer-prompt-fallback.md +24 -0
  433. package/resources/skills/story-creation/upstream/skills/story-long-write/references/writing-craft.md +442 -0
  434. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/author_memory_commit.py +1635 -0
  435. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/build_architect_brief.py +260 -0
  436. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/build_writer_prompt.py +611 -0
  437. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-ai-patterns.js +1586 -0
  438. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-degeneration.js +349 -0
  439. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-outline-contract.js +520 -0
  440. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/check-outline-copy.js +313 -0
  441. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/normalize-punctuation.js +415 -0
  442. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/outline_view.py +286 -0
  443. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/storyctl.py +561 -0
  444. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/style-whitelist.js +39 -0
  445. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/tracking_commit.py +1691 -0
  446. package/resources/skills/story-creation/upstream/skills/story-long-write/scripts/wordcount_core.py +405 -0
  447. package/resources/skills/story-creation/upstream/skills/story-review/SKILL.md +231 -0
  448. package/resources/skills/story-creation/upstream/skills/story-review/references/agent-prompts.md +184 -0
  449. package/resources/skills/story-creation/upstream/skills/story-review/references/anti-ai-writing.md +556 -0
  450. package/resources/skills/story-creation/upstream/skills/story-review/references/author-memory-maintenance.md +75 -0
  451. package/resources/skills/story-creation/upstream/skills/story-review/references/author-memory.md +117 -0
  452. package/resources/skills/story-creation/upstream/skills/story-review/references/banned-words.md +151 -0
  453. package/resources/skills/story-creation/upstream/skills/story-review/references/batch-review.md +19 -0
  454. package/resources/skills/story-creation/upstream/skills/story-review/references/character-relations.md +433 -0
  455. package/resources/skills/story-creation/upstream/skills/story-review/references/dialogue-mastery.md +288 -0
  456. package/resources/skills/story-creation/upstream/skills/story-review/references/plot-core-methods.md +522 -0
  457. package/resources/skills/story-creation/upstream/skills/story-review/references/quality-rubric.md +55 -0
  458. package/resources/skills/story-creation/upstream/skills/story-review/references/review-quality.md +246 -0
  459. package/resources/skills/story-creation/upstream/skills/story-review/references/review-tracking.md +10 -0
  460. package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/fanqie.md +18 -0
  461. package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/qidian.md +19 -0
  462. package/resources/skills/story-creation/upstream/skills/story-review/references/rubrics/zhihu.md +20 -0
  463. package/resources/skills/story-creation/upstream/skills/story-review/references/solo.md +52 -0
  464. package/resources/skills/story-creation/upstream/skills/story-review/references/style-resolution.md +27 -0
  465. package/resources/skills/story-creation/upstream/skills/story-review/references/tracking-initialization.md +31 -0
  466. package/resources/skills/story-creation/upstream/skills/story-review/references/tracking-transaction.md +146 -0
  467. package/resources/skills/story-creation/upstream/skills/story-review/scripts/author_memory_commit.py +1635 -0
  468. package/resources/skills/story-creation/upstream/skills/story-review/scripts/check-ai-patterns.js +1586 -0
  469. package/resources/skills/story-creation/upstream/skills/story-review/scripts/check-degeneration.js +349 -0
  470. package/resources/skills/story-creation/upstream/skills/story-review/scripts/normalize-punctuation.js +415 -0
  471. package/resources/skills/story-creation/upstream/skills/story-review/scripts/style-whitelist.js +39 -0
  472. package/resources/skills/story-creation/upstream/skills/story-review/scripts/tracking_commit.py +1691 -0
  473. package/resources/skills/story-creation/upstream/skills/story-review/scripts/wordcount_core.py +405 -0
  474. package/resources/skills/story-creation/upstream/skills/story-setup/SKILL.md +165 -0
  475. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/agent-quality.md +72 -0
  476. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/agent-reference-profiles.md +55 -0
  477. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/anti-ai-writing.md +556 -0
  478. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/banned-words.md +151 -0
  479. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/benchmark-style-load.md +107 -0
  480. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-basics.md +445 -0
  481. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-design-methods.md +428 -0
  482. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/character-relations.md +433 -0
  483. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/deslop-gates.md +171 -0
  484. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/dialogue-mastery.md +288 -0
  485. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/emotion-on-page.md +116 -0
  486. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/emotional-arc-design.md +313 -0
  487. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/format-and-structure.md +168 -0
  488. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//344/270/234/346/226/271/344/273/231/344/276/240.md +51 -0
  489. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//344/274/240/347/273/237/347/216/204/345/271/273.md +51 -0
  490. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/345/217/244/344/273/243.md +51 -0
  491. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/216/206/345/217/262/350/204/221/346/264/236.md +51 -0
  492. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/214/347/224/267/344/270/273.md +53 -0
  493. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/350/250/200/350/204/221/346/264/236.md +51 -0
  494. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/217/244/351/243/216/344/270/226/346/203/205.md +51 -0
  495. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/346/202/254/347/226/221.md +51 -0
  496. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/245/263/351/242/221/347/247/215/347/224/260.md +51 -0
  497. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/256/253/346/226/227/345/256/205/346/226/227.md +51 -0
  498. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/271/264/344/273/243.md +53 -0
  499. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//345/277/253/347/251/277.md +51 -0
  500. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/347/201/265/345/274/202.md +51 -0
  501. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/202/254/347/226/221/350/204/221/346/264/236.md +51 -0
  502. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/210/230/347/245/236/350/265/230/345/251/277.md +51 -0
  503. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/212/227/346/210/230/350/260/215/346/210/230.md +51 -0
  504. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/230/237/345/205/211/347/222/200/347/222/250.md +51 -0
  505. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/260/221/345/233/275/350/250/200/346/203/205.md +51 -0
  506. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//346/270/270/346/210/217/344/275/223/350/202/262.md +51 -0
  507. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/204/221/346/264/236.md +53 -0
  508. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/204/345/271/273/350/250/200/346/203/205.md +51 -0
  509. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/216/260/350/250/200/350/204/221/346/264/236.md +51 -0
  510. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//347/247/221/345/271/273/346/234/253/344/270/226.md +51 -0
  511. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/201/214/345/234/272/345/251/232/346/201/213.md +51 -0
  512. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/245/277/346/226/271/345/245/207/345/271/273.md +51 -0
  513. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//350/261/252/351/227/250/346/200/273/350/243/201.md +53 -0
  514. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/344/277/256/347/234/237.md +51 -0
  515. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/346/227/245/345/270/270.md +53 -0
  516. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/347/247/215/347/224/260.md +51 -0
  517. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/350/204/221/346/264/236.md +53 -0
  518. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/203/275/345/270/202/351/253/230/346/255/246.md +53 -0
  519. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards//351/235/222/346/230/245/347/224/234/345/256/240.md +51 -0
  520. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-prose-cards.md +91 -0
  521. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/genre-readers.md +235 -0
  522. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-chapter-hooks.md +225 -0
  523. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-emotional-methods.md +209 -0
  524. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-genre-catalog.md +394 -0
  525. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-genre-mechanics.md +415 -0
  526. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-quality.md +42 -0
  527. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-reversal.md +335 -0
  528. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/long-suspense.md +352 -0
  529. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/opening-design.md +349 -0
  530. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-conflict.md +457 -0
  531. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-methods.md +287 -0
  532. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/outline-rhythm.md +389 -0
  533. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/plot-core-methods.md +522 -0
  534. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-chapter-hooks.md +225 -0
  535. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-emotional-methods.md +179 -0
  536. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-genre-formulas.md +417 -0
  537. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-paragraph-hooks.md +146 -0
  538. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-quality.md +39 -0
  539. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-reversal.md +355 -0
  540. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/short-suspense.md +352 -0
  541. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-combat-face.md +352 -0
  542. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-genre-modules.md +510 -0
  543. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/style-resolution.md +27 -0
  544. package/resources/skills/story-creation/upstream/skills/story-setup/references/agent-references/writing-craft.md +442 -0
  545. package/resources/skills/story-creation/upstream/skills/story-setup/references/diagnostics.md +38 -0
  546. package/resources/skills/story-creation/upstream/skills/story-short-analyze/SKILL.md +224 -0
  547. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-basics.md +453 -0
  548. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-design.md +438 -0
  549. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-character-relations.md +395 -0
  550. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-method.md +42 -0
  551. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-paragraph-hooks.md +146 -0
  552. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-reader-profile.md +245 -0
  553. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-report-style.md +45 -0
  554. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-genres.md +65 -0
  555. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-hooks.md +77 -0
  556. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-mechanics.md +82 -0
  557. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-patterns.md +50 -0
  558. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-short-suspense.md +98 -0
  559. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/analysis-writing-techniques.md +477 -0
  560. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/deconstruction-examples.md +79 -0
  561. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/optional-modules.md +97 -0
  562. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/output-contract.md +127 -0
  563. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/quality-checklist.md +80 -0
  564. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/real-market-data.md +90 -0
  565. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/source-story-quality.md +221 -0
  566. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/stage2-3-structure-emotion.md +308 -0
  567. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/stage4-6-reversal-summary.md +611 -0
  568. package/resources/skills/story-creation/upstream/skills/story-short-analyze/references/zhihu-style.md +54 -0
  569. package/resources/skills/story-creation/upstream/skills/story-short-scan/SKILL.md +182 -0
  570. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/manual-rank-input.md +36 -0
  571. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/platform-dianzhong.md +24 -0
  572. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/platform-heiyan.md +21 -0
  573. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/real-market-data.md +90 -0
  574. package/resources/skills/story-creation/upstream/skills/story-short-scan/references/topic-match.md +38 -0
  575. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/aggregate-rank.js +682 -0
  576. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/cdp-utils.js +242 -0
  577. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/dz-browse-scraper.js +232 -0
  578. package/resources/skills/story-creation/upstream/skills/story-short-scan/scripts/heiyan-booklist-scraper.js +345 -0
  579. package/resources/skills/story-creation/upstream/skills/story-short-write/SKILL.md +136 -0
  580. package/resources/skills/story-creation/upstream/skills/story-short-write/references/agent-calls.md +13 -0
  581. package/resources/skills/story-creation/upstream/skills/story-short-write/references/author-memory-maintenance.md +75 -0
  582. package/resources/skills/story-creation/upstream/skills/story-short-write/references/author-memory.md +117 -0
  583. package/resources/skills/story-creation/upstream/skills/story-short-write/references/banned-words.md +151 -0
  584. package/resources/skills/story-creation/upstream/skills/story-short-write/references/benchmark-recall.md +40 -0
  585. package/resources/skills/story-creation/upstream/skills/story-short-write/references/cross-book-recall.md +62 -0
  586. package/resources/skills/story-creation/upstream/skills/story-short-write/references/dialogue-mastery.md +288 -0
  587. package/resources/skills/story-creation/upstream/skills/story-short-write/references/emotional-methods.md +179 -0
  588. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//344/270/226/346/203/205/346/211/223/350/204/270.md +159 -0
  589. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/217/214/347/224/267/344/270/273.md +156 -0
  590. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/244/215/344/273/207/346/211/223/350/204/270.md +163 -0
  591. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//345/256/205/346/226/227/345/256/253/346/226/227.md +167 -0
  592. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/200/273/350/243/201/350/261/252/351/227/250.md +178 -0
  593. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/202/254/347/226/221.md +166 -0
  594. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/260/221/344/277/227/346/200/252/350/260/210.md +159 -0
  595. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//346/262/231/351/233/225/350/204/221/346/264/236.md +151 -0
  596. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//347/224/234/345/256/240.md +153 -0
  597. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-styles//350/277/275/345/246/273/347/201/253/350/221/254/345/234/272.md +210 -0
  598. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-writing-formulas.md +417 -0
  599. package/resources/skills/story-creation/upstream/skills/story-short-write/references/genre-writing-techniques.md +490 -0
  600. package/resources/skills/story-creation/upstream/skills/story-short-write/references/hooks-chapter.md +225 -0
  601. package/resources/skills/story-creation/upstream/skills/story-short-write/references/hooks-paragraph.md +146 -0
  602. package/resources/skills/story-creation/upstream/skills/story-short-write/references/output-contract.md +127 -0
  603. package/resources/skills/story-creation/upstream/skills/story-short-write/references/reference-index.md +23 -0
  604. package/resources/skills/story-creation/upstream/skills/story-short-write/references/scene-craft.md +40 -0
  605. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-craft.md +147 -0
  606. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-deslop.md +133 -0
  607. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-format.md +199 -0
  608. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-prose-quality.md +224 -0
  609. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-reversal.md +355 -0
  610. package/resources/skills/story-creation/upstream/skills/story-short-write/references/short-suspense.md +352 -0
  611. package/resources/skills/story-creation/upstream/skills/story-short-write/references/style-resolution.md +27 -0
  612. package/resources/skills/story-creation/upstream/skills/story-short-write/references/submission-craft.md +35 -0
  613. package/resources/skills/story-creation/upstream/skills/story-short-write/references/villain-and-reveal.md +153 -0
  614. package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-design.md +85 -0
  615. package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-draft.md +116 -0
  616. package/resources/skills/story-creation/upstream/skills/story-short-write/references/workflow-revision.md +99 -0
  617. package/resources/skills/story-creation/upstream/skills/story-short-write/references/writing-workflow.md +50 -0
  618. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/author_memory_commit.py +1635 -0
  619. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-ai-patterns.js +1586 -0
  620. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-degeneration.js +349 -0
  621. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-delivery-contract.js +241 -0
  622. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-outline-copy.js +313 -0
  623. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/check-phase2-contract.js +363 -0
  624. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/normalize-punctuation.js +415 -0
  625. package/resources/skills/story-creation/upstream/skills/story-short-write/scripts/style-whitelist.js +39 -0
  626. package/resources/skills/video-recap-production/manifest.json +673 -0
  627. package/resources/skills/video-recap-production/upstream/CHANGELOG.md +371 -0
  628. package/resources/skills/video-recap-production/upstream/LICENSE +21 -0
  629. package/resources/skills/video-recap-production/upstream/README.en.md +468 -0
  630. package/resources/skills/video-recap-production/upstream/README.md +459 -0
  631. package/resources/skills/video-recap-production/upstream/manifest.json +839 -0
  632. package/resources/skills/video-recap-production/upstream/skills/video-assemble/SKILL.md +112 -0
  633. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/audio-modes.md +63 -0
  634. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/explicit-audio-mix.md +75 -0
  635. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/foreground-compose.md +118 -0
  636. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/LICENSE.duo-video +21 -0
  637. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/SOURCE.md +15 -0
  638. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_draft_meta_info.json +69 -0
  639. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_combination_segment.json +84 -0
  640. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_combination_video_material.json +98 -0
  641. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_draft.json +204 -0
  642. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_material_video.json +83 -0
  643. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_meta_material_value.json +23 -0
  644. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_project_info.json +133 -0
  645. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_segment.json +70 -0
  646. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_jy_text_styles.json +30 -0
  647. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_yj_material_audio.json +41 -0
  648. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/jianying/empty_yj_material_text.json +109 -0
  649. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/narration-adoption.md +103 -0
  650. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/packaging.md +45 -0
  651. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/pair-media.md +74 -0
  652. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/source-score.md +147 -0
  653. package/resources/skills/video-recap-production/upstream/skills/video-assemble/references/subtitle-track.md +202 -0
  654. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/__init__.py +1 -0
  655. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/audio_mix_binding.py +279 -0
  656. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/frozen_audio.py +187 -0
  657. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/narration_binding.py +323 -0
  658. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/strict_inputs.py +109 -0
  659. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/adoption/strict_publish.py +130 -0
  660. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/artifacts.py +47 -0
  661. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assemble.py +591 -0
  662. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assemble_constants.py +51 -0
  663. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assembly_contract.py +318 -0
  664. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/assembly_settings.py +138 -0
  665. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/audio_automation.py +195 -0
  666. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/audio_mix.py +465 -0
  667. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/compose_foreground.py +293 -0
  668. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/export_jianying.py +254 -0
  669. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/__init__.py +1 -0
  670. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/builders.py +718 -0
  671. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/model.py +105 -0
  672. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/optional.py +23 -0
  673. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/schema.py +53 -0
  674. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/templates.py +37 -0
  675. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/timeline_contract.py +223 -0
  676. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/tracks.py +72 -0
  677. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/jianying/writer.py +383 -0
  678. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/lib.py +218 -0
  679. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/media.py +190 -0
  680. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/narration_audio.py +413 -0
  681. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/packaging.py +122 -0
  682. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/pair_media.py +216 -0
  683. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/render_preflight.py +33 -0
  684. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/source_score.py +469 -0
  685. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/source_subtitles.py +423 -0
  686. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/__init__.py +1 -0
  687. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/core.py +330 -0
  688. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/render.py +82 -0
  689. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/track.py +324 -0
  690. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/subtitles/track_binding.py +200 -0
  691. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/timeline.py +217 -0
  692. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/timeline_emit.py +167 -0
  693. package/resources/skills/video-recap-production/upstream/skills/video-assemble/scripts/visual_render.py +448 -0
  694. package/resources/skills/video-recap-production/upstream/skills/video-cut/SKILL.md +108 -0
  695. package/resources/skills/video-recap-production/upstream/skills/video-cut/references/shot-review.md +50 -0
  696. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut.py +41 -0
  697. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_cli.py +308 -0
  698. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_contract.py +424 -0
  699. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/cut_render.py +304 -0
  700. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/lib.py +114 -0
  701. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/media_geometry.py +238 -0
  702. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/narration_mapping.py +71 -0
  703. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/narrative_selection.py +244 -0
  704. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/sentence_boundaries.py +585 -0
  705. package/resources/skills/video-recap-production/upstream/skills/video-cut/scripts/shot_review.py +357 -0
  706. package/resources/skills/video-recap-production/upstream/skills/video-recap/SKILL.md +260 -0
  707. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/audio-routing.md +126 -0
  708. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/config-playbook.md +65 -0
  709. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/data-schema.md +545 -0
  710. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/resource-library.md +151 -0
  711. package/resources/skills/video-recap-production/upstream/skills/video-recap/references/shift-left-qc-schema.md +23 -0
  712. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/doctor.py +556 -0
  713. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/final_qc.py +527 -0
  714. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/lib.py +220 -0
  715. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/library.py +509 -0
  716. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/materials.py +406 -0
  717. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/mimo_qc.py +33 -0
  718. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/project_binding.py +286 -0
  719. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/__init__.py +1 -0
  720. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_client.py +13 -0
  721. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_contract.py +9 -0
  722. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_evidence.py +287 -0
  723. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_observations.py +175 -0
  724. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_payload.py +206 -0
  725. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_report.py +318 -0
  726. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc/mimo_runner.py +130 -0
  727. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/qc_contract.py +320 -0
  728. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap.py +9 -0
  729. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_cli.py +209 -0
  730. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_inspect.py +452 -0
  731. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_review.py +70 -0
  732. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_runner.py +696 -0
  733. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_runtime.py +260 -0
  734. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_source.py +278 -0
  735. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_stage_qc.py +160 -0
  736. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/recap_timeline.py +621 -0
  737. package/resources/skills/video-recap-production/upstream/skills/video-recap/scripts/resource_lock.py +162 -0
  738. package/resources/skills/video-recap-production/upstream/skills/video-script/SKILL.md +251 -0
  739. package/resources/skills/video-recap-production/upstream/skills/video-script/references/creative-editing-playbook.md +229 -0
  740. package/resources/skills/video-recap-production/upstream/skills/video-script/references/promotional-copy.md +68 -0
  741. package/resources/skills/video-recap-production/upstream/skills/video-script/references/research-guide.md +75 -0
  742. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/agent_text.py +301 -0
  743. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/deslop_qc.py +211 -0
  744. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/evidence_bundle.py +440 -0
  745. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/lib.py +254 -0
  746. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/narration_lint.py +707 -0
  747. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review.py +40 -0
  748. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_grounding.py +238 -0
  749. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_response.py +669 -0
  750. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/review_runner.py +186 -0
  751. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/speech_ownership.py +143 -0
  752. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/timeline_fusion.py +108 -0
  753. package/resources/skills/video-recap-production/upstream/skills/video-script/scripts/validate.py +175 -0
  754. package/resources/skills/video-recap-production/upstream/skills/video-understanding/SKILL.md +77 -0
  755. package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/data-schema.md +193 -0
  756. package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/prompt-templates.md +37 -0
  757. package/resources/skills/video-recap-production/upstream/skills/video-understanding/references/research-guide.md +74 -0
  758. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/agent_text.py +301 -0
  759. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/asr.py +279 -0
  760. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/asr_timing_evidence.py +270 -0
  761. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/brief.py +9 -0
  762. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/__init__.py +1 -0
  763. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/builder.py +402 -0
  764. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/context.py +317 -0
  765. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/inputs.py +268 -0
  766. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/briefing/timeline.py +326 -0
  767. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/consolidate.py +596 -0
  768. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/deslop_qc.py +211 -0
  769. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/detect.py +504 -0
  770. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/extract.py +62 -0
  771. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/lib.py +419 -0
  772. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/narration_lint.py +707 -0
  773. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/speech_ownership.py +143 -0
  774. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/storyboard.py +445 -0
  775. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/timeline_fusion.py +108 -0
  776. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understand.py +9 -0
  777. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_brief.py +190 -0
  778. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_cache.py +345 -0
  779. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_runner.py +387 -0
  780. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/understanding_storyboard.py +167 -0
  781. package/resources/skills/video-recap-production/upstream/skills/video-understanding/scripts/vlm.py +674 -0
  782. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/SKILL.md +91 -0
  783. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/references/index-tts.md +31 -0
  784. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/approved_text_policy.py +123 -0
  785. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/dub.py +623 -0
  786. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/lib.py +352 -0
  787. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/__init__.py +1 -0
  788. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/fish_audio.py +91 -0
  789. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/providers/index_tts.py +184 -0
  790. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/tts_audio.py +102 -0
  791. package/resources/skills/video-recap-production/upstream/skills/video-voiceover/scripts/voiceover.py +771 -0
  792. package/resources/skills/viora-canvas-production/manifest.json +5 -1
  793. package/resources/skills/viora-canvas-production/references/story-handoff.md +49 -0
@@ -0,0 +1,3051 @@
1
+ import { TEXT } from './text.js';
2
+ import {
3
+ HEROINES, HEROINE_IDS, HOUSEHOLD, HOUSEHOLD_IDS, HOUSEHOLD_EVENTS,
4
+ OPENING_CHOICES, ROUTE_CHOICES, ROUTE_BRANCHES, ACCORD_CHOICES, JOINT_ACTIONS, SHARED_NIGHT_CHOICES, DAY_AGENDAS,
5
+ ALLIANCE_ASSEMBLY_RESPONSES, ALLIANCE_VOICES, SHARED_AFTERGLOW_BEATS, PUBLIC_EVENTS, PUBLIC_EVIDENCE_CHAIN, PUBLIC_FOLLOWUPS, ACT_TRANSITIONS, EXTERNAL_REBUTTALS, FINAL_RECKONING, COUNCIL_EVENTS, DUSK_INVITATIONS, DUSK_INVITATION_AFTERMATHS, PERSONAL_AFTERGLOW_AFTERMATHS, NIGHT_TEXT, SCENES,
6
+ } from './data.js';
7
+ import * as E from './engine.js';
8
+ import { ASSET_PATHS, loadAssets, preloadSafeAssets, preloadFinaleAssets, assetReport, urlFor, assertCriticalAssetSchema } from './assets.js';
9
+ import { audio } from './audio.js';
10
+
11
+ const SAVE_KEY = 'jpm_fengyue_save_v66';
12
+ const GALLERY_KEY = 'jpm_fengyue_gallery_v1';
13
+ const ENDING_ARCHIVE_KEY = 'jpm_fengyue_endings_v1';
14
+ const ENDING_ARCHIVE_MAX_CHARS = 2400000;
15
+ const AGE_KEY = 'jpm_fengyue_age_session';
16
+ const params = new URLSearchParams(location.search);
17
+ const SEED = params.has('seed') ? Number(params.get('seed')) : (Date.now() % 100000);
18
+ const FAST = params.get('fast') === '1';
19
+ if (FAST) document.documentElement.classList.add('fast');
20
+ const app = document.getElementById('app');
21
+
22
+ let state = null;
23
+ let assets = null;
24
+ let galleryOpen = false;
25
+ let gallerySceneId = null;
26
+ let galleryVariantIndex = 0;
27
+ let resultCard = null;
28
+ let toastTimer = null;
29
+ let audioReady = false;
30
+ let safePreloadStarted = false;
31
+ let finalePreloadStarted = false;
32
+ const narrativeAssetLoads = new Set();
33
+ let pendingFocusSelector = null;
34
+ let galleryReturnSelector = null;
35
+ let newGameConfirmOpen = false;
36
+ let rosterOpen = false;
37
+ let epilogueOpen = false;
38
+ let epilogueIndex = 0;
39
+ let epilogueReturnSelector = null;
40
+ let archivedEpilogueKey = null;
41
+ let fateCodaOpen = false;
42
+ let fateCodaReturnSelector = null;
43
+ let archivedFateKey = null;
44
+ let archivedFateVariantIndex = 0;
45
+ let archivedFatePageIndex = 0;
46
+
47
+ assertCriticalAssetSchema();
48
+ assets = assetReport();
49
+ const bootPromise = boot();
50
+
51
+ async function boot() {
52
+ // 年龄门先可读、可操作;确认前只允许浏览器请求安全背景和标题图。
53
+ render();
54
+ assets = await loadAssets();
55
+ if (!assets.bootReady && params.get('dev') !== '1') {
56
+ renderAssetFailure(assets.missingCritical);
57
+ return;
58
+ }
59
+ render();
60
+ if (ageConfirmed()) scheduleSafePreload();
61
+ }
62
+
63
+ function scheduleSafePreload() {
64
+ if (safePreloadStarted) return;
65
+ safePreloadStarted = true;
66
+ const start = () => preloadSafeAssets().then((report) => {
67
+ assets = report;
68
+ if (report.missingCritical.length && params.get('dev') !== '1') renderAssetFailure(report.missingCritical);
69
+ });
70
+ if ('requestIdleCallback' in window) window.requestIdleCallback(start, { timeout: 1800 });
71
+ else setTimeout(start, 120);
72
+ }
73
+
74
+ function scheduleFinalePreload(immediate = false) {
75
+ if (finalePreloadStarted) return;
76
+ finalePreloadStarted = true;
77
+ const start = () => preloadFinaleAssets().then((report) => {
78
+ assets = report;
79
+ if (report.missingCritical.length && params.get('dev') !== '1') renderAssetFailure(report.missingCritical);
80
+ });
81
+ if (!immediate && 'requestIdleCallback' in window) window.requestIdleCallback(start, { timeout: 1800 });
82
+ else if (!immediate) setTimeout(start, 120);
83
+ else start();
84
+ }
85
+
86
+ function ensureNarrativeAsset(key) {
87
+ if (!key || narrativeAssetLoads.has(key)) return;
88
+ narrativeAssetLoads.add(key);
89
+ loadAssets([key]).then((report) => {
90
+ assets = report;
91
+ if (report.missingCritical.includes(key) && params.get('dev') !== '1') renderAssetFailure([key]);
92
+ });
93
+ }
94
+
95
+ function renderAssetFailure(missing) {
96
+ app.innerHTML = `<main class="fatal-card" id="asset-error"><h1>有几页画没有装进来</h1><p>先不让你看残页。缺的是:${missing.map(escapeHtml).join('、')}</p></main>`;
97
+ }
98
+
99
+ function ageConfirmed() {
100
+ return sessionStorage.getItem(AGE_KEY) === 'yes';
101
+ }
102
+
103
+ function loadGallery() {
104
+ try {
105
+ const value = JSON.parse(localStorage.getItem(GALLERY_KEY) || '[]');
106
+ return Array.isArray(value) ? value.filter((id) => SCENES[id]) : [];
107
+ } catch {
108
+ return [];
109
+ }
110
+ }
111
+
112
+ const ENDING_GOALS = Object.freeze([
113
+ { id: 'balanced', label: '五院同灯', hint: '让五院都留下,再由余夜与天明选择一种共处制度。' },
114
+ { id: 'pair', label: '双院同灯', hint: '不强求五院圆满,让一对真正互信的女人结成有限同盟。' },
115
+ { id: 'triad', label: '三院成盟', hint: '三人内部每一对都能同桌,才不是靠一条强关系硬拉成团。' },
116
+ { id: 'quad', label: '四院共席', hint: '四个人都由本人留下,系统不再替她们裁掉一席。' },
117
+ { id: 'exclusive', label: '一院灯深', hint: '走深一人的情与欲,也承担另外四院收回钥匙的结果。' },
118
+ { id: 'intrigue', label: '借局翻身', hint: '用证据、银势和官面赢下外账,接受宅内关系留下的代价。' },
119
+ { id: 'unstable', label: '五门各闭', hint: '没有制度托住的偏爱会怎样散掉,也会被总账如实记下。' },
120
+ ]);
121
+
122
+ function endingFamily(ending) {
123
+ if (ending?.id === 'alliance') return ({ 2:'pair', 3:'triad', 4:'quad' })[ending.alliance?.length] ?? null;
124
+ return ending?.id ?? null;
125
+ }
126
+
127
+ function endingArchiveEntry(gameState) {
128
+ const ending = gameState?.ending;
129
+ if (!ending?.id || gameState.phase !== 'ending') return null;
130
+ const family = endingFamily(ending);
131
+ const collapseVariant = ending.collapseResult?.choice ? `:${ending.collapseResult.choice}` : '';
132
+ const sceneArchives = E.endingSceneArchives(gameState);
133
+ const scenes = Object.fromEntries(sceneArchives.map((record) => [record.scene, record]));
134
+ const sceneVariant = sceneArchives.map((record) => record.key).join('::');
135
+ const variant = ending.id === 'balanced' ? `${ending.coalitionStyle}:${ending.sharedAfterglowTableau?.key ?? 'no-afterglow'}${sceneVariant ? `:${sceneVariant}` : ''}`
136
+ : ending.id === 'alliance' ? `${ending.alliance?.join('+')}:${ending.allianceTableau?.key ?? 'no-tableau'}${sceneVariant ? `:${sceneVariant}` : ''}`
137
+ : ending.id === 'exclusive' ? ending.heroine
138
+ : ending.id === 'intrigue' ? `${ending.intrigueCost}${collapseVariant}`
139
+ : `${ending.missedBy}${collapseVariant}`;
140
+ return {
141
+ key: `${ending.id}:${variant || 'default'}`,
142
+ family,
143
+ title: ending.title,
144
+ tag: ending.routeResult || ending.tag,
145
+ cast: ending.allianceName || ending.heroineName || '',
146
+ text: ending.text,
147
+ seed: gameState.seed,
148
+ scenes,
149
+ afterstory:E.endingAfterstoryArchive(gameState),
150
+ fates:[E.endingFateArchive(gameState)].filter(Boolean),
151
+ };
152
+ }
153
+
154
+ function loadEndingArchive() {
155
+ try {
156
+ const value = JSON.parse(localStorage.getItem(ENDING_ARCHIVE_KEY) || '[]');
157
+ if (!Array.isArray(value)) return [];
158
+ return value.filter((row) => row && typeof row.key === 'string' && typeof row.title === 'string' && typeof row.text === 'string');
159
+ } catch {
160
+ return [];
161
+ }
162
+ }
163
+
164
+ function rememberEnding(gameState) {
165
+ const entry = endingArchiveEntry(gameState);
166
+ if (!entry) return;
167
+ const archive = loadEndingArchive();
168
+ const previous = archive.find((row) => row.key === entry.key);
169
+ const seeds = [...new Set([...(previous?.seeds ?? (Number.isInteger(previous?.seed) ? [previous.seed] : [])), entry.seed])].slice(-6);
170
+ const previousFates = Array.isArray(previous?.fates) ? previous.fates : previous?.fate ? [previous.fate] : [];
171
+ const fates = [...entry.fates, ...previousFates]
172
+ .filter((fate, index, rows) => fate?.key && rows.findIndex((row) => row?.key === fate.key) === index)
173
+ .slice(0, 9);
174
+ const current = { ...previous, ...entry, seeds, fates };
175
+ delete current.fate;
176
+ const candidates = [current, ...archive.filter((row) => row.key !== entry.key)].slice(0, 24);
177
+ const next = [];
178
+ for (const candidate of candidates) {
179
+ const trial = [...next, candidate];
180
+ if (JSON.stringify(trial).length <= ENDING_ARCHIVE_MAX_CHARS) next.push(candidate);
181
+ }
182
+ localStorage.setItem(ENDING_ARCHIVE_KEY, JSON.stringify(next));
183
+ }
184
+
185
+ function save() {
186
+ if (!state) return;
187
+ localStorage.setItem(SAVE_KEY, E.serialize(state));
188
+ const merged = [...new Set([...loadGallery(), ...state.unlocked])];
189
+ localStorage.setItem(GALLERY_KEY, JSON.stringify(merged));
190
+ rememberEnding(state);
191
+ }
192
+
193
+ function loadSave() {
194
+ const raw = localStorage.getItem(SAVE_KEY);
195
+ const loaded = E.deserialize(raw);
196
+ if (!loaded) {
197
+ localStorage.removeItem(SAVE_KEY);
198
+ return null;
199
+ }
200
+ return loaded;
201
+ }
202
+
203
+ function startNew() {
204
+ newGameConfirmOpen = false;
205
+ epilogueOpen = false;
206
+ archivedEpilogueKey = null;
207
+ fateCodaOpen = false;
208
+ archivedFateKey = null;
209
+ archivedFateVariantIndex = 0;
210
+ archivedFatePageIndex = 0;
211
+ state = E.newGame(SEED);
212
+ localStorage.removeItem(SAVE_KEY);
213
+ save();
214
+ render();
215
+ }
216
+
217
+ function continueGame() {
218
+ newGameConfirmOpen = false;
219
+ const loaded = loadSave();
220
+ if (!loaded) return showToast('这本旧账接不上了,只好从第一日重开。');
221
+ state = loaded;
222
+ rememberEnding(state);
223
+ render();
224
+ focusSoon(firstPhaseActionSelector());
225
+ }
226
+
227
+ function restart(sameSeed = false) {
228
+ const seed = sameSeed ? state.seed : E.nextSeed(state.seed);
229
+ epilogueOpen = false;
230
+ epilogueIndex = 0;
231
+ archivedEpilogueKey = null;
232
+ fateCodaOpen = false;
233
+ archivedFateKey = null;
234
+ archivedFateVariantIndex = 0;
235
+ archivedFatePageIndex = 0;
236
+ state = E.newGame(seed);
237
+ localStorage.removeItem(SAVE_KEY);
238
+ save();
239
+ render();
240
+ }
241
+
242
+ function selectorForButton(button) {
243
+ if (button.id) return `#${CSS.escape(button.id)}`;
244
+ const key = Object.keys(button.dataset)[0];
245
+ if (!key) return null;
246
+ const attr = key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
247
+ return `[data-${attr}="${CSS.escape(button.dataset[key])}"]`;
248
+ }
249
+
250
+ function focusSoon(selector) {
251
+ pendingFocusSelector = selector;
252
+ requestAnimationFrame(() => {
253
+ const target = pendingFocusSelector ? document.querySelector(pendingFocusSelector) : null;
254
+ if (target instanceof HTMLElement && !target.hasAttribute('disabled')) target.focus();
255
+ pendingFocusSelector = null;
256
+ });
257
+ }
258
+
259
+ function firstPhaseActionSelector() {
260
+ return '#phase-stage button:not([disabled])';
261
+ }
262
+
263
+ function inertBackgroundExcept(overlay) {
264
+ for (const child of app.children) {
265
+ if (child !== overlay) child.inert = true;
266
+ }
267
+ }
268
+
269
+ function sfxForPhase() {
270
+ if (!state) return;
271
+ const map = { opening_aftermath: 'paper', day: 'wang', day_aftermath: 'paper', crisis: 'plank', crisis_aftermath: 'plank', pair_interlude: 'qing', favor_reckoning: 'submit', memory_reckoning: 'paper', act_transition: 'submit', act_aftermath: 'paper', joint_result: 'submit', portable_precedent: 'paper', household: 'paper', household_aftermath: 'paper', council: 'submit', council_aftermath: 'paper', visit: 'paper', route_aftermath: 'submit', night: 'watch', morning: 'plank', scene: 'qing', personal_afterglow: 'watch', personal_afterglow_aftermath: 'watch', personal_finale: 'qing', personal_finale_result: 'qing', banquet: 'submit', public_evidence: 'paper', public_followup: 'paper', public_aftermath: 'paper', five_private_prices: 'paper', final_reckoning: 'submit', final_aftermath: 'paper', dusk_invitation: 'watch', dusk_invitation_aftermath: 'watch', alliance_assembly: 'paper', alliance_night: 'qing', alliance_night_result: 'qing', shared_night: 'submit', shared_afterglow: 'watch', shared_afterglow_result: 'watch', shared_dawn: 'plank', shared_dawn_result: 'paper', collapse_finale: 'submit', collapse_finale_result: 'paper', ending: 'qing' };
272
+ audio.sfx(map[state.phase] ?? 'click');
273
+ }
274
+
275
+ function bgmCue() {
276
+ if (!state) return 'title';
277
+ if (state.phase === 'ending') {
278
+ if (['balanced', 'alliance', 'exclusive'].includes(state.ending?.id)) return 'ending_liyanei';
279
+ if (state.ending?.id === 'unstable') return 'ending_liuluo';
280
+ return 'ending_other';
281
+ }
282
+ return `act${Math.min(4, Math.ceil(state.day / 5))}`;
283
+ }
284
+
285
+ function syncBGM() {
286
+ if (audioReady) audio.playBGM(bgmCue());
287
+ }
288
+
289
+ function act(fn, label = '') {
290
+ resultCard = null;
291
+ const result = fn();
292
+ if (!result?.ok) {
293
+ showToast(result?.error || '这一步走不通。');
294
+ return;
295
+ }
296
+ save();
297
+ sfxForPhase();
298
+ // 延迟追账本身就是上一选择的完整剧情回收;再叠一张晨间结果卡会在窄屏
299
+ // 遮住第三个回应,反而把“有后果”重新做成必须先清掉的 UI 噪声。
300
+ if (result.text && !['opening_aftermath', 'scene', 'day_aftermath', 'joint_result', 'portable_precedent', 'morning_settlement', 'council_aftermath', 'public_evidence', 'public_aftermath', 'act_aftermath', 'five_private_prices', 'final_aftermath', 'crisis', 'crisis_aftermath', 'pair_interlude', 'household', 'household_aftermath', 'council', 'banquet', 'public_followup', 'final_reckoning', 'morning', 'act_transition', 'route_aftermath', 'favor_reckoning', 'memory_reckoning', 'dusk_invitation', 'dusk_invitation_aftermath', 'personal_afterglow_aftermath', 'personal_finale', 'personal_finale_result', 'alliance_night', 'alliance_night_result', 'shared_afterglow_result', 'shared_dawn_result', 'collapse_finale', 'collapse_finale_result', 'ending'].includes(state.phase)) {
301
+ resultCard = { title: label || '这句话落下了', text: result.text };
302
+ } else if (result.text && result.scene) {
303
+ resultCard = { title: label || '这一笔有了回声', text: result.text };
304
+ }
305
+ render();
306
+ if (result.announcement) {
307
+ const announcer = document.getElementById('announcer');
308
+ if (announcer) announcer.textContent = result.announcement;
309
+ }
310
+ if (fateCodaOpen) {
311
+ const fate = E.currentFateCoda(state);
312
+ focusSoon(fate?.awaitingChoice ? '[data-fate-coda]' : fate?.page === fate?.count - 1 ? '#btn-fate-coda-finish' : '[data-fate-coda-continue]');
313
+ } else if (state.phase === 'ending') focusSoon('#ending-view h1');
314
+ else if (['five_private_prices', 'portable_precedent'].includes(state.phase)) focusSoon('#phase-stage .phase-header h2');
315
+ else if (state.phase === 'personal_finale_result' && E.currentPersonalFinaleResult(state)?.departure) focusSoon('#phase-stage .phase-header h2');
316
+ else focusSoon(firstPhaseActionSelector());
317
+ }
318
+
319
+ function render() {
320
+ if (ageConfirmed() && state?.day >= 16) {
321
+ scheduleFinalePreload(['personal_finale', 'personal_finale_result', 'ending'].includes(state.phase));
322
+ }
323
+ if (!ageConfirmed()) renderAgeGate();
324
+ else if (!assets?.bootReady) {
325
+ app.innerHTML = '<div class="loading">只装订封面与宅院,稍候。</div>';
326
+ } else if (!state) renderTitle();
327
+ else renderGame();
328
+ syncBGM();
329
+ }
330
+
331
+ function renderAgeGate() {
332
+ app.innerHTML = `
333
+ <main class="age-gate" id="age-gate">
334
+ <div class="age-seal" aria-hidden="true">18+</div>
335
+ <p class="eyebrow">${TEXT.rating}</p>
336
+ <h1>${TEXT.ageTitle}</h1>
337
+ <p>${TEXT.ageBody}</p>
338
+ <div class="button-row">
339
+ <button class="ink-button primary" id="btn-age-yes">${TEXT.ageYes}</button>
340
+ <button class="ink-button" id="btn-age-no">${TEXT.ageNo}</button>
341
+ </div>
342
+ </main>`;
343
+ focusSoon('#btn-age-yes');
344
+ }
345
+
346
+ function renderTitle() {
347
+ const saved = loadSave();
348
+ const hasSave = !!saved;
349
+ const titleCast = [
350
+ ['月娘', '定账'],
351
+ ['金莲', '识谎'],
352
+ ['瓶儿', '私钥'],
353
+ ['玉楼', '名帖'],
354
+ ['雪娥', '灶权'],
355
+ ];
356
+ app.innerHTML = `
357
+ <main class="title-screen">
358
+ <div class="title-art" style="background-image:url('${urlFor('cover')}')" role="img" aria-label="月娘、金莲、瓶儿、玉楼与雪娥各据一处,五道视线都落在你身上"></div>
359
+ <aside class="title-folio" aria-hidden="true"><b>风月总账</b><span>二十日 · 四幕</span></aside>
360
+ <section class="title-copy">
361
+ <p class="eyebrow">成人后宫关系游戏 · 二十日四幕</p>
362
+ <h1>${TEXT.title}</h1>
363
+ <p class="title-subtitle">${TEXT.subtitle}</p>
364
+ <p class="identity-line">${TEXT.identity}</p>
365
+ <div class="title-actions">
366
+ <button class="ink-button primary" id="btn-start">${hasSave ? '另开新局' : TEXT.start}</button>
367
+ <button class="ink-button" id="btn-continue" ${hasSave ? '' : 'disabled'}>${hasSave ? `接着第 ${saved.day} 日` : TEXT.continue}</button>
368
+ <button class="ink-button" id="btn-gallery">${TEXT.gallery}</button>
369
+ </div>
370
+ <p class="rating-line">${TEXT.rating}</p>
371
+ <p class="save-note">${TEXT.saveNote}</p>
372
+ </section>
373
+ <ol class="title-cast" aria-label="五院与各自握有的权力">
374
+ ${titleCast.map(([name, role], index) => `<li><i>${index + 1}</i><span>${name}</span><b>${role}</b></li>`).join('')}
375
+ </ol>
376
+ </main>`;
377
+ if (newGameConfirmOpen && hasSave) appendNewGameConfirm(saved);
378
+ if (galleryOpen) appendGallery();
379
+ if (epilogueOpen) appendEpilogue();
380
+ if (fateCodaOpen) appendFateCoda();
381
+ if (!newGameConfirmOpen && !galleryOpen) focusSoon(hasSave ? '#btn-continue' : '#btn-start');
382
+ }
383
+
384
+ function appendNewGameConfirm(saved) {
385
+ const overlay = document.createElement('div');
386
+ overlay.className = 'confirm-overlay';
387
+ overlay.id = 'new-game-confirm';
388
+ overlay.innerHTML = `<section class="confirm-card" role="dialog" aria-modal="true" aria-labelledby="new-game-title">
389
+ <p class="eyebrow">这一步会改写当前账页</p>
390
+ <h2 id="new-game-title">另开一局?</h2>
391
+ <p>现有进度停在第 ${saved.day} 日。另开后,这一局的自动存档会被覆盖;已经收入场景册的册页仍会保留。</p>
392
+ <div class="button-row"><button class="ink-button primary" id="btn-confirm-new">确认另开</button><button class="ink-button" id="btn-cancel-new">保留旧局</button></div>
393
+ </section>`;
394
+ app.appendChild(overlay);
395
+ inertBackgroundExcept(overlay);
396
+ focusSoon('#btn-cancel-new');
397
+ }
398
+
399
+ function renderGame() {
400
+ const day = E.dayDef(state);
401
+ const content = renderPhase();
402
+ const obligations = E.activeObligations(state);
403
+ const notice = hudNotice(obligations);
404
+ const noticeMarkup = notice.text
405
+ ? `<p class="hud-notice ${notice.tone}" aria-live="polite">${notice.text}</p>`
406
+ : '<p class="hud-notice quiet" aria-live="polite"></p>';
407
+ app.innerHTML = `
408
+ <main class="game-shell" id="game-shell" data-phase="${state.phase}" data-act="${Math.min(4, Math.ceil(state.day / 5))}">
409
+ <header class="topbar">
410
+ <div class="day-mark"><b id="day-num">第 ${state.day} 日</b><span>${escapeHtml(day.act)}</span></div>
411
+ ${noticeMarkup}
412
+ <div class="top-actions">
413
+ <button class="plain-button roster-button" id="btn-roster" aria-label="打开完整账册${obligations.length ? ',有旧话待结' : ''}">账册${obligations.length ? ' · 待办' : ''}</button>
414
+ <button class="plain-button" id="btn-gallery">${TEXT.gallery}</button>
415
+ <button class="plain-button" id="btn-mute">${audio.muted ? TEXT.muteOn : TEXT.muteOff}</button>
416
+ </div>
417
+ </header>
418
+ <div class="play-area">
419
+ <section class="phase-stage" id="phase-stage">
420
+ ${content}
421
+ </section>
422
+ </div>
423
+ </main>`;
424
+ collapseSupportingContext(document.getElementById('phase-stage'));
425
+ if (rosterOpen) appendRoster();
426
+ if (resultCard && !rosterOpen && !galleryOpen && !epilogueOpen && !fateCodaOpen) appendResultCard();
427
+ if (galleryOpen) appendGallery();
428
+ if (epilogueOpen) appendEpilogue();
429
+ if (fateCodaOpen) appendFateCoda();
430
+ }
431
+
432
+ function collapseSupportingContext(root) {
433
+ if (!root) return;
434
+ const selector = [
435
+ '.council-external-echo', '.opening-public-echo', '.act-source-echo', '.public-evidence-echo',
436
+ '.route-branch-memory', '.intimacy-memory', '.ordinary-night-memory', '.night-conversation-life-memory',
437
+ '.pair-interlude-memory', '.household-memory', '.crisis-reply-ledger', '.pair-aftermath-transcript',
438
+ '.route-resolution-transcript', '.aftermath-echo', '.aftermath-pair-memory', '.aftermath-route-stake',
439
+ '.memory-thread', '.favor-thread', '.private-price-ledger', '.final-reckoning-cast',
440
+ '.public-followup-cast', '.external-witnesses', '.crisis-ledger', '.crisis-rule',
441
+ '.pair-trust', '.pair-rule', '.collapse-result-transcript', '.route-beat-archive',
442
+ '.public-evidence-long-brief', '.final-aftermath-long-brief',
443
+ ].join(',');
444
+ root.querySelectorAll('.decision-panel').forEach((panel) => {
445
+ if (panel.closest('.roster-overlay, .gallery-overlay, .epilogue-overlay')) return;
446
+ const rows = [...panel.children].filter((child) => child.matches(selector));
447
+ if (!rows.length) return;
448
+ const details = document.createElement('details');
449
+ details.className = 'supporting-context';
450
+ let label = '前情与依据';
451
+ if (rows.some((row) => row.matches('.route-beat-archive'))) label = '本拍完整叙述';
452
+ else if (rows.some((row) => row.matches('.public-evidence-long-brief'))) label = '完整案情与前证';
453
+ else if (rows.some((row) => row.matches('.final-aftermath-long-brief'))) label = '完整来路与旧账';
454
+ else if (panel.classList.contains('dialogue-panel')) label = '此前关系史';
455
+ else if (panel.classList.contains('household-panel')) label = '影响这笔交易的旧账';
456
+ else if (panel.classList.contains('final-reckoning-panel') || panel.classList.contains('final-aftermath-panel')) label = '走到终局的旧账';
457
+ else if (panel.classList.contains('public-evidence-panel') || panel.classList.contains('public-followup-panel') || panel.classList.contains('public-aftermath-panel')) label = '影响本场裁决的前情';
458
+ details.innerHTML = `<summary><b>${label}</b><span>${rows.length} 笔旧事</span></summary><div class="supporting-context-pages"></div>`;
459
+ rows[0].before(details);
460
+ details.querySelector('.supporting-context-pages').append(...rows);
461
+ });
462
+ }
463
+
464
+ function appendRoster() {
465
+ const overlay = document.createElement('div');
466
+ overlay.className = 'roster-overlay';
467
+ overlay.id = 'roster-modal';
468
+ overlay.innerHTML = `<section class="roster-sheet" role="dialog" aria-modal="true" aria-labelledby="roster-title">
469
+ <header><div><p class="eyebrow">需要时再翻,不挤占正戏</p><h2 id="roster-title">宅门账册</h2></div><button class="plain-button" id="btn-roster-close">合上</button></header>
470
+ <div class="roster-scroll">
471
+ <section class="ledger-status" aria-label="外账风向"><header><b>外账风向</b><span>只记当前处境,不把数目压在戏上</span></header><div class="ledger-resources">
472
+ ${resourceChip('银', state.resources.silver, 'silver')}
473
+ ${resourceChip('势', state.resources.power, 'power')}
474
+ ${resourceChip('声', state.resources.repute, 'repute')}
475
+ ${resourceChip('宅', state.resources.house, 'house')}
476
+ ${resourceChip('露', state.resources.exposure, 'exposure')}
477
+ ${resourceChip('耗', state.resources.strain, 'strain')}
478
+ </div></section>
479
+ <section class="roster-relations" aria-label="五院人物账">${HEROINE_IDS.map(renderRelationCard).join('')}</section>
480
+ ${renderHaremOutlook()}${renderObligationBoard(false)}${renderBondBoard(true)}
481
+ <section class="ledger-book">${renderLedger()}</section>
482
+ <section class="household-roster"><p>宅中人</p>${HOUSEHOLD_IDS.map(renderHouseholdRow).join('')}</section>
483
+ </div>
484
+ </section>`;
485
+ app.appendChild(overlay);
486
+ inertBackgroundExcept(overlay);
487
+ focusSoon('#btn-roster-close');
488
+ }
489
+
490
+ function resourceCondition(key, value) {
491
+ if (key === 'silver') return value <= 0 ? '见底' : value < 40 ? '告急' : value < 100 ? '吃紧' : value < 180 ? '可周转' : '宽裕';
492
+ if (key === 'power') return value <= 0 ? '无势' : value <= 2 ? '势薄' : value <= 4 ? '可借' : '得力';
493
+ if (key === 'repute') return value <= 0 ? '无声' : value <= 2 ? '声轻' : value <= 4 ? '有人听' : '传得开';
494
+ if (key === 'house') return value < 25 ? '将散' : value < 50 ? '动荡' : value < 75 ? '尚稳' : '齐整';
495
+ if (key === 'exposure') return value <= 0 ? '无风' : value < 20 ? '微闻' : value < 45 ? '风声起' : value < 70 ? '已见光' : '满城闻';
496
+ return value <= 0 ? '从容' : value < 20 ? '尚轻' : value < 45 ? '劳累' : value < 70 ? '难支' : '将溃';
497
+ }
498
+
499
+ function hudNotice(obligations) {
500
+ if (obligations.some((row) => ['overdue', 'due'].includes(row.status))) return { tone: 'urgent', text: '有人今日等你还话' };
501
+ if (obligations.some((row) => row.status === 'locked')) return { tone: 'watch', text: '有一扇门暂不留话' };
502
+ if (state.resources.silver < 40) return { tone: 'urgent', text: '银钱已经告急' };
503
+ if (state.resources.house < 50) return { tone: 'urgent', text: '宅门正在动荡' };
504
+ if (state.resources.exposure >= 45) return { tone: 'watch', text: '外头风声已紧' };
505
+ if (state.resources.strain >= 45) return { tone: 'watch', text: '众人已经难支' };
506
+ return { tone: 'quiet', text: '' };
507
+ }
508
+
509
+ function resourceChip(glyph, value, key) {
510
+ const condition = resourceCondition(key, value);
511
+ return `<div class="resource-chip ${key}" data-resource="${key}" data-resource-value="${value}" aria-label="${glyph}:${condition}"><span>${glyph}</span><b>${condition}</b></div>`;
512
+ }
513
+
514
+ function narrativeProgress(value, target, labels = ['尚未成形', '正在成形', '已经落定']) {
515
+ if (value <= 0) return labels[0];
516
+ if (value >= target) return labels[2];
517
+ return labels[1];
518
+ }
519
+
520
+ function narrativeStep(index, count, labels = ['起笔', '转折', '收束']) {
521
+ if (count <= 1 || index <= 1) return labels[0];
522
+ if (index >= count) return labels[2];
523
+ return labels[1];
524
+ }
525
+
526
+ function narrativeBeatMark(index) {
527
+ return ['起', '承', '转', '结'][index] ?? '续';
528
+ }
529
+
530
+ function narrativeChoiceMeta(value) {
531
+ if (!value) return '';
532
+ const resourceEffects = {
533
+ 银: { '+': '银钱回笼', '-': '要花银钱' },
534
+ 势: { '+': '官面更有余地', '-': '官面借势收窄' },
535
+ 声: { '+': '名声传开', '-': '名声受损' },
536
+ 宅: { '+': '宅门更稳', '-': '宅门更乱' },
537
+ 露: { '+': '风声见光', '-': '风声暂歇' },
538
+ 耗: { '+': '耗损加重', '-': '得以喘息' },
539
+ };
540
+ const normalized = String(value)
541
+ .replace(/([银势声宅露耗])\s*([++−-])\s*\d+(?:\s*两)?/g, (_, key, sign) => resourceEffects[key][sign === '+' || sign === '+' ? '+' : '-'])
542
+ .replace(/(?:两院|院间)互信\s*([++−-])\s*\d+/g, (_, sign) => (sign === '+' || sign === '+' ? '两院更能互信' : '两院更相提防'))
543
+ .replace(/情\s*([++−-])\s*\d+/g, (_, sign) => (sign === '+' || sign === '+' ? '情意更深' : '情意转淡'))
544
+ .replace(/欲\s*([++−-])\s*\d+/g, (_, sign) => (sign === '+' || sign === '+' ? '更愿亲近' : '亲近退后'))
545
+ .replace(/妒\s*([++−-])\s*\d+/g, (_, sign) => (sign === '+' || sign === '+' ? '妒意上升' : '妒意缓下'));
546
+ const seen = new Set();
547
+ return normalized.split('·').map((part) => part.trim().replace(/\s*[++−-]\s*\d+\s*$/, '').trim()).filter((part) => {
548
+ if (!part || seen.has(part)) return false;
549
+ seen.add(part);
550
+ return true;
551
+ }).join(' · ');
552
+ }
553
+
554
+ // 档位变化的一次性提示:按 state 记忆上一次渲染的档位与数值,
555
+ // 跨档时给该格加 data-changed="up|down"(方向按数值增减),由 CSS 做墨渗 + 朱印。
556
+ const tierMemory = new WeakMap();
557
+
558
+ function tierChanges(id, rel) {
559
+ let memory = tierMemory.get(state);
560
+ if (!memory) {
561
+ memory = {};
562
+ tierMemory.set(state, memory);
563
+ }
564
+ const changed = {};
565
+ for (const kind of ['qing', 'yu', 'du']) {
566
+ const key = `${id}.${kind}`;
567
+ const tier = E.relationTier(rel[kind], kind);
568
+ const before = memory[key];
569
+ if (before && before.tier !== tier) changed[kind] = rel[kind] > before.value ? 'up' : 'down';
570
+ memory[key] = { tier, value: rel[kind] };
571
+ }
572
+ return changed;
573
+ }
574
+
575
+ function renderRelationCard(id) {
576
+ const heroine = HEROINES[id];
577
+ const rel = state.relations[id];
578
+ const stance = E.routeStance(state, id);
579
+ const habit = E.nightRelationshipPattern(state, id);
580
+ const arrangement = E.latestIntimacyArrangement(state, id);
581
+ const reason = rel.reasons[0] || heroine.want;
582
+ const changed = tierChanges(id, rel);
583
+ const mark = (kind) => (changed[kind] ? ` data-changed="${changed[kind]}"` : '');
584
+ return `
585
+ <article class="relation-card relation-${id}" data-heroine="${id}" data-qing="${rel.qing}" data-yu="${rel.yu}" data-du="${rel.du}">
586
+ <div class="relation-name"><span class="shape-mark">${heroine.glyph}</span><div><b>${heroine.name}</b><small>${heroine.house} · ${stance.tone}</small></div></div>
587
+ <div class="relation-tiers">
588
+ <span${mark('qing')}>情 <b>${E.relationTier(rel.qing, 'qing')}</b></span>
589
+ <span${mark('yu')}>欲 <b>${E.relationTier(rel.yu, 'yu')}</b></span>
590
+ <span${mark('du')}>妒 <b>${E.relationTier(rel.du, 'du')}</b></span>
591
+ </div>
592
+ <span class="relation-habit ${habit.settled ? 'settled' : ''}" data-night-pattern="${habit.id || 'forming'}" ${arrangement ? `data-intimacy-arrangement="${arrangement.id}"` : ''} title="${escapeHtml([habit.summary, arrangement?.future].filter(Boolean).join(' '))}">夜谈 · ${escapeHtml(habit.label)} · ${narrativeProgress(habit.chapters, 4, ['尚在试探', '话已接上', '已成条款'])}${arrangement ? ` · 约:${escapeHtml(arrangement.label)}` : ''}</span>
593
+ <p>${escapeHtml(reason)}</p>
594
+ </article>`;
595
+ }
596
+
597
+ function intimacyMemory(heroineId) {
598
+ const arrangements = E.intimacyArrangements(state, heroineId);
599
+ if (!arrangements.length) return '';
600
+ return `<aside class="intimacy-memory" aria-label="此前亲密约定"><header><b>她仍按以前的约定生活</b><span>${arrangements.length > 1 ? '两项都在执行' : '已有一项约定'}</span></header>${arrangements.map((row) => `<div data-intimacy-memory="${row.id}"><small>${row.tier === 'explicit' ? '留宿后约' : '前奏后约'} · 第 ${row.day} 夜</small><b>${escapeHtml(row.label)} · ${escapeHtml(row.title)}</b><p>${escapeHtml(row.outcome)}</p><em>次晨:${escapeHtml(row.morning)} 此后:${escapeHtml(row.future)}</em></div>`).join('')}</aside>`;
601
+ }
602
+
603
+ function ordinaryNightMemory(heroineId) {
604
+ const memories = E.ordinaryNightMemories(state, heroineId);
605
+ if (!memories.length) return '';
606
+ return memories.map((memory) => `<aside class="ordinary-night-memory" data-ordinary-night-memory="${memory.event}" aria-label="她记得这段普通夜章"><small>普通夜章 · ${memory.count > 1 ? `第 ${memory.firstDay}—${memory.day} 夜反复发生` : `第 ${memory.day} 夜`} · ${escapeHtml(memory.actionLabel)}</small><b>${escapeHtml(memory.title)}</b><p>${escapeHtml(memory.morning)}</p></aside>`).join('');
607
+ }
608
+
609
+ function nightConversationMemory(heroineId) {
610
+ const memories = E.nightConversationMemories(state, heroineId);
611
+ const memory = memories.at(-1);
612
+ if (!memory) return '';
613
+ return `<aside class="night-conversation-life-memory" data-night-conversation-memory="${memory.event}:${memory.mode}" aria-label="专属夜谈已经进入长期生活"><small>专属夜谈 · ${narrativeProgress(memory.chapter, 4, ['刚刚起话', '已经接续', '已成相处条款'])} · ${escapeHtml(memory.prop)} · ${escapeHtml(memory.modeLabel)}</small><b>${escapeHtml(memory.title)}</b><span class="stake">实际执行 · ${escapeHtml(memory.stakeLabel)} · ${escapeHtml(narrativeChoiceMeta(memory.stakeResourceText))}</span><p>${escapeHtml(memory.future)}</p><em>${escapeHtml(memory.observerName)}也接到了这一笔:${escapeHtml(memory.observerLine)}</em></aside>`;
614
+ }
615
+
616
+ function routeReckoningMemory(heroineId) {
617
+ const memories = E.routeReckoningMemories(state, heroineId);
618
+ if (!memories.length) return '';
619
+ return memories.map((memory) => `<aside class="night-conversation-life-memory route-reckoning-life-memory" data-route-reckoning-memory="${memory.event}:${memory.sourceDay}:${memory.choice}" aria-label="两日后旧话裁决仍在执行"><small>已结旧话 · 第 ${memory.sourceDay} 日后约 · 第 ${memory.day} 日由${escapeHtml(memory.observerName)}见证</small><b>${escapeHtml(memory.stakeLabel)} · ${escapeHtml(memory.choiceLabel)}</b><span class="stake">当时执行 · ${escapeHtml(memory.stakeText)} · ${escapeHtml(narrativeChoiceMeta(memory.stakeResourceText))}</span><p>${escapeHtml(memory.incident)} ${escapeHtml(memory.outcome)}</p><em>${escapeHtml(memory.observerName)}当日这样质询:${escapeHtml(memory.observerText)} 只追这一件:${escapeHtml(memory.question)}</em></aside>`).join('');
620
+ }
621
+
622
+ function favorReckoningMemory(heroineId) {
623
+ const memories = E.favorReckoningMemories(state, heroineId);
624
+ if (!memories.length) return '';
625
+ return memories.map((memory) => `<aside class="night-conversation-life-memory favor-reckoning-life-memory" data-favor-reckoning-memory="${memory.event}:${memory.sourceDay}:${memory.choice}" aria-label="人情还账仍在执行"><small>已结人情 · 第 ${memory.sourceDay} 日借力 · 第 ${memory.day} 日由${escapeHtml(memory.observerName)}见证</small><b>${escapeHtml(memory.debtTitle)} · ${escapeHtml(memory.choiceLabel)}</b><span class="stake">当时借力 · ${escapeHtml(memory.sourceLabel)} · ${escapeHtml(memory.sourceText)}</span><p>${escapeHtml(memory.debtBody)} ${escapeHtml(memory.outcome)}</p><em>${escapeHtml(memory.observerName)}当日也把代价摆上长案:${escapeHtml(memory.observerLine)}</em></aside>`).join('');
626
+ }
627
+
628
+ function pairInterludeMemory(heroineId) {
629
+ const memories = E.pairInterludeMemories(state, heroineId);
630
+ if (!memories.length) return '';
631
+ return memories.map((memory) => `<aside class="pair-interlude-memory" data-pair-memory="${memory.event}:${memory.choice}" aria-label="她与旁院已经建立的关系"><small>双院关系 · 第 ${memory.day} 日 · 与${escapeHtml(HEROINES[memory.partner].short)} · ${escapeHtml(memory.label)}</small><b>${escapeHtml(memory.title)}</b><p>${escapeHtml(memory.memory)}</p></aside>`).join('');
632
+ }
633
+
634
+ function duskInvitationMemory(heroineId) {
635
+ const memory = E.duskInvitationMemory(state, heroineId);
636
+ if (!memory) return '';
637
+ return `<aside class="pair-interlude-memory dusk-invitation-memory" data-dusk-invitation-memory="${memory.event}:${memory.approach}:${memory.choice}" aria-label="她主动提出的邀约已经成为长期安排"><small>主动邀约 · 第 ${memory.day} 日 · ${escapeHtml(memory.approachLabel)} · ${escapeHtml(memory.witnessName)}见证</small><b>${escapeHtml(memory.title)}</b><span class="stake">她当时主动说 · ${escapeHtml(memory.heroineLine)}</span><p>${escapeHtml(memory.outcome)}</p><em>${escapeHtml(memory.witnessName)}没有退出后果:${escapeHtml(memory.witnessQuestion)}</em></aside>`;
638
+ }
639
+
640
+ function rivalryMorningMemory(heroineId) {
641
+ const memories = E.rivalryMorningMemories(state, heroineId);
642
+ if (!memories.length) return '';
643
+ return memories.map((memory) => `<aside class="pair-interlude-memory rivalry-morning-memory" data-rivalry-memory="${memory.day}:${memory.actor}:${memory.visited}:${memory.choice}" aria-label="偏宠对峙仍是双方关系史"><small>偏宠对峙 · 第 ${memory.day} 日 · ${memory.role === 'challenger' ? '她发难' : '她是昨夜被选择的一院'} · 与${escapeHtml(memory.otherName)}</small><b>${escapeHtml(memory.title)} · ${escapeHtml(memory.choiceLabel)}</b><span class="stake">她当面说 · ${escapeHtml(memory.role === 'challenger' ? memory.opening : memory.visitedReply)}</span><p>${escapeHtml(memory.outcome)}</p></aside>`).join('');
644
+ }
645
+
646
+ function renderHouseholdRow(id) {
647
+ const person = HOUSEHOLD[id];
648
+ const row = state.household[id];
649
+ const ledger = id === 'li_jiaoer' ? E.jiaoerLedger(state) : null;
650
+ return `<div class="household-row" data-household="${id}" data-regard="${row.regard}">
651
+ <span>${person.glyph}</span><b>${person.name}</b><small>${ledger ? `${escapeHtml(ledger.label)}${ledger.outstanding ? ` · 欠 ${ledger.outstanding} 两` : ''}` : E.householdTier(row.regard)}</small>
652
+ </div>`;
653
+ }
654
+
655
+ function renderHaremOutlook() {
656
+ const outlook = E.haremOutlook(state);
657
+ return `<section class="harem-outlook" aria-label="后宫局势"><header><b>后宫局势</b><span>${outlook.pairCount ? '横向关系已经相接' : '横向关系尚未成形'}</span></header><p>${escapeHtml(outlook.summary)}</p></section>`;
658
+ }
659
+
660
+ function renderBondBoard(showAll = false) {
661
+ const byId = new Map(E.bondStatus(state).map((row) => [row.id, row]));
662
+ const pairs = showAll
663
+ ? E.bondStatus(state)
664
+ : JOINT_ACTIONS.map((action) => byId.get(E.bondKey(action.participants[0], action.participants[1]))).filter(Boolean);
665
+ return `<section class="bond-board ${showAll ? 'all-pairs' : ''}" aria-label="院间关系网"><header><b>院间关系网</b><span>${showAll ? '所有关系都会影响成盟' : '关键联办组合'}</span></header>${pairs.map((row) => `<div data-bond="${row.id}" data-bond-value="${row.value}"><span>${HEROINES[row.left].short} ↔ ${HEROINES[row.right].short}</span><b>${row.tier}</b></div>`).join('')}</section>`;
666
+ }
667
+
668
+ function renderObligationBoard(compact = false) {
669
+ const rows = E.activeObligations(state);
670
+ if (!rows.length) {
671
+ return compact ? '' : '<section class="obligation-board empty" aria-label="待兑现总账"><header><b>待兑现</b><span>没有悬账</span></header><p>已经说出口的话都结到了具体结果。</p></section>';
672
+ }
673
+ const urgent = rows.filter((row) => ['overdue', 'due', 'locked'].includes(row.status)).length;
674
+ const visible = compact ? rows.slice(0, 1) : rows;
675
+ return `<section class="obligation-board ${compact ? 'compact' : ''}" aria-label="待兑现总账">
676
+ <header><b>待兑现</b><span>${urgent ? '旧账正在催' : '这些话都有归期'}</span></header>
677
+ ${visible.map((row) => `<article data-obligation="${row.id}" data-obligation-type="${row.type}" data-obligation-status="${row.status}">
678
+ <div><span>${row.label} · ${HEROINES[row.heroine].short}${row.observer ? ` × ${HEROINES[row.observer].short}` : ''}</span><em>${row.statusLabel}</em></div>
679
+ <b>${escapeHtml(row.title)}</b>
680
+ ${compact ? '' : `<p>${escapeHtml(row.detail)}</p>`}
681
+ </article>`).join('')}
682
+ ${compact && rows.length > visible.length ? '<small>人物账内还有旧话待结</small>' : ''}
683
+ </section>`;
684
+ }
685
+
686
+ // 账簿页:左栏中段原是一大块纯黑空白,如今填成 state.history 最近 4 条的竖排墨字。
687
+ // 用 WeakMap 按 state 记住上次渲染到的条目数,新记上的几行带 data-fresh 渗入。
688
+ const ledgerMemory = new WeakMap();
689
+
690
+ function renderLedger() {
691
+ const total = state.history.length;
692
+ const seen = ledgerMemory.get(state) ?? 0;
693
+ ledgerMemory.set(state, total);
694
+ const entries = state.history.slice(-4);
695
+ if (!entries.length) return '<p class="ledger-empty">账页还空着</p>';
696
+ const dayGlyph = (day) => `${E.silverText(day)}日`;
697
+ return entries.map((entry, index) => {
698
+ const fresh = total - entries.length + index >= seen ? ' data-fresh="1"' : '';
699
+ return `<p class="ledger-line"${fresh}><b>${dayGlyph(entry.day)}</b>${escapeHtml(ledgerLine(entry))}</p>`;
700
+ }).join('');
701
+ }
702
+
703
+ function ledgerLine(entry) {
704
+ const cap = (text, max = 5) => {
705
+ const chars = [...String(text ?? '')];
706
+ return chars.length > max ? `${chars.slice(0, max).join('')}…` : chars.join('');
707
+ };
708
+ switch (entry.type) {
709
+ case 'opening':
710
+ return entry.choice === 'opening_open_ledger' ? '五院同开真账' : '先听完五人';
711
+ case 'day_action': {
712
+ const label = DAY_AGENDAS[entry.day - 1]?.actions?.[entry.action]?.label;
713
+ return cap(label ?? { ledger: '翻账', office: '走官面', listen: '问口风', banquet: '整席面' }[entry.action] ?? '白日办事', 7);
714
+ }
715
+ case 'joint_action': {
716
+ const label = JOINT_ACTIONS.find((item) => item.id === entry.action)?.label ?? '联院差事';
717
+ return cap(label, 7);
718
+ }
719
+ case 'portable_precedent':
720
+ return `院外援例·${cap(({honor_precedent:'照规矩',named_exception:'具名限次',inside_only:'只留宅内'})[entry.choice] ?? '第二张契', 6)}`;
721
+ case 'household': {
722
+ const event = Object.values(HOUSEHOLD_EVENTS).find((item) => item.id === entry.event);
723
+ const label = event?.choices.find((item) => item.id === entry.choice)?.label ?? '廊下一句';
724
+ return `${HOUSEHOLD[entry.actor]?.short ?? '宅中人'}·${cap(label)}`;
725
+ }
726
+ case 'council': {
727
+ const event = Object.values(COUNCIL_EVENTS).find((item) => item.id === entry.event);
728
+ const label = event?.choices.find((item) => item.id === entry.choice)?.label ?? '院议裁决';
729
+ return `院议·${cap(label, 6)}`;
730
+ }
731
+ case 'banquet': {
732
+ const event = Object.values(PUBLIC_EVENTS).find((item) => item.id === entry.event);
733
+ const label = event?.choices.find((item) => item.id === entry.choice)?.label ?? '举杯';
734
+ return cap(label, 6);
735
+ }
736
+ case 'public_evidence_chain':
737
+ return `证链·${{ complete:'闭合', rebuttable:'留缝', broken:'断裂' }[entry.result] ?? '三步'}`;
738
+ case 'visit_start':
739
+ return `黄昏进${HEROINES[entry.heroine]?.house ?? '内院'}`;
740
+ case 'visit_choice': {
741
+ // 路线按拜访次数走,条目里没有当时的拍号;选项 id 每人唯一,跨拍平铺查找。
742
+ const rows = [
743
+ ...Object.values(ROUTE_CHOICES[entry.heroine] ?? {}).flat(),
744
+ ...Object.values(ROUTE_BRANCHES[entry.heroine] ?? {}).flatMap((branch) => Object.values(branch).flat()),
745
+ ];
746
+ const label = rows.find((item) => item.id === entry.choice)?.label ?? '夜话';
747
+ return cap(label, 6);
748
+ }
749
+ case 'act_transition': {
750
+ const label = ACT_TRANSITIONS[entry.day]?.choices.find((row) => row.id === entry.choice)?.label ?? '换幕揭底';
751
+ return `换幕·${cap(label, 6)}`;
752
+ }
753
+ case 'external_rebuttal': {
754
+ const label = EXTERNAL_REBUTTALS[entry.sourceResult]?.choices.find((row) => row.id === entry.choice)?.label ?? '三口复案';
755
+ return `复案·${cap(label, 6)}`;
756
+ }
757
+ case 'route_aftermath': {
758
+ const short = HEROINES[entry.observer]?.short ?? '旁院';
759
+ return `${short}接进此事`;
760
+ }
761
+ case 'memory_reckoning':
762
+ return `${HEROINES[entry.heroine]?.short ?? '她'}追两日前旧话`;
763
+ case 'favor_reckoning':
764
+ return `${HEROINES[entry.heroine]?.short ?? '她'}来收人情账`;
765
+ case 'dusk_invitation':
766
+ return `${HEROINES[entry.heroine]?.short ?? '她'}主动来请`;
767
+ case 'dusk_invitation_aftermath': {
768
+ const choice = DUSK_INVITATION_AFTERMATHS[entry.heroine]?.[entry.approach]?.choices.find((row) => row.id === entry.choice);
769
+ return `${HEROINES[entry.heroine]?.short ?? '她'}·${cap(choice?.label ?? '邀约落字', 6)}`;
770
+ }
771
+ case 'personal_afterglow_aftermath': {
772
+ const choice = PERSONAL_AFTERGLOW_AFTERMATHS[entry.heroine]?.[entry.tier]?.choices.find((row) => row.id === entry.choice);
773
+ return `${HEROINES[entry.heroine]?.short ?? '她'}·${cap(choice?.label ?? '余夜落字', 6)}`;
774
+ }
775
+ case 'personal_finale':
776
+ return `${HEROINES[entry.heroine]?.short ?? '她'}·个人终章`;
777
+ case 'alliance_night_start':
778
+ return `${entry.members.map((id) => HEROINES[id]?.short ?? '她').join('、')}共同留下`;
779
+ case 'alliance_night':
780
+ return `联盟终章·${cap(entry.choice, 6)}`;
781
+ case 'accord_term': {
782
+ const choice = ACCORD_CHOICES[entry.heroine];
783
+ return `${HEROINES[entry.heroine]?.short ?? '她'}·${cap(choice?.label ?? '立约', 5)}`;
784
+ }
785
+ case 'shared_night_start':
786
+ return `五院同席·${entry.accordCount >= E.ACCORD_KEYS.length ? '院约已齐' : '院约仍在落字'}`;
787
+ case 'shared_night': {
788
+ const label = SHARED_NIGHT_CHOICES.find((item) => item.id === entry.choice)?.label ?? '同席定议';
789
+ return cap(label, 7);
790
+ }
791
+ case 'collapse_finale_start':
792
+ return entry.endingId === 'intrigue' ? '权谋代价上门' : '五院破局清算';
793
+ case 'collapse_finale':
794
+ return `最后保住·${cap(entry.choice, 6)}`;
795
+ case 'night': {
796
+ const short = HEROINES[entry.heroine]?.short ?? '她';
797
+ return {
798
+ leave: `${short}屋·掩门出`,
799
+ talk: `${short}屋·坐更漏`,
800
+ prelude: `${short}点了头`,
801
+ explicit: `宿${HEROINES[entry.heroine]?.house ?? '内院'}`,
802
+ }[entry.action] ?? '夜话一回';
803
+ }
804
+ case 'night_conversation':
805
+ return `${HEROINES[entry.heroine]?.short ?? '她'}夜谈·第${entry.chapter}章`;
806
+ case 'morning': {
807
+ const short = HEROINES[entry.actor]?.short ?? '她';
808
+ return {
809
+ jealousy: `${short}来敲门`, rivalry: `${short}当面追问偏爱`, pan_claim: `${short}堵门讨话`,
810
+ yue_delayed: '月娘记前话', yue_help: '月娘留账人',
811
+ pinger_help: '瓶儿递货单', meng_invitation: '玉楼署名帖',
812
+ xuee_breakfast: '雪娥呈食单', quiet: '一盏醒酒茶',
813
+ }[entry.event] ?? '天亮一回';
814
+ }
815
+ case 'public_followup': {
816
+ const label = PUBLIC_FOLLOWUPS[entry.day]?.choices.find((row) => row.id === entry.choice)?.label ?? '再定一笔';
817
+ return `公议后·${cap(label, 6)}`;
818
+ }
819
+ case 'final_reckoning': {
820
+ const label = FINAL_RECKONING.choices.find((row) => row.id === entry.choice)?.label ?? '终局对账';
821
+ return `外账·${cap(label, 6)}`;
822
+ }
823
+ case 'route_break':
824
+ return entry.heroine ? `${HEROINES[entry.heroine]?.short ?? '她'}门冷一日` : '各门冷一日';
825
+ case 'upkeep_short':
826
+ return '场面塌一角';
827
+ case 'morning_settlement':
828
+ return `${HEROINES[entry.heroine]?.short ?? '本院'}·晨簿落名`;
829
+ case 'morning_settlement_use':
830
+ return `${HEROINES[entry.heroine]?.short ?? '本院'}·用过一次窄权`;
831
+ case 'morning_settlement_restore':
832
+ return `${HEROINES[entry.heroine]?.short ?? '本院'}·具名恢复`;
833
+ case 'collector':
834
+ return entry.paid ? '打发收账人' : '收账人闹上门';
835
+ default:
836
+ return '记下一笔';
837
+ }
838
+ }
839
+
840
+ function phaseHeader(kicker, title, body) {
841
+ return `<header class="phase-header"><p class="eyebrow">${kicker}</p><h2 tabindex="-1">${title}</h2><p>${escapeHtml(body)}</p></header>`;
842
+ }
843
+
844
+ function narrativeExcerpt(value, limit = 150) {
845
+ const text = String(value ?? '').trim();
846
+ if (text.length <= limit) return { preview: text, full: '' };
847
+ const candidate = text.slice(0, limit + 1);
848
+ const stops = [...candidate.matchAll(/[。!?;]/g)].map((match) => match.index + 1);
849
+ const sentenceEnd = stops.filter((index) => index >= Math.floor(limit * .58)).at(-1);
850
+ const end = sentenceEnd || limit;
851
+ return { preview: `${text.slice(0, end).trim()}${sentenceEnd ? '' : '……'}`, full: text };
852
+ }
853
+
854
+ function narrativeArchive(copy, className, title) {
855
+ if (!copy.full) return '';
856
+ return `<aside class="council-external-echo ${className}"><span>完整叙述</span><b>${escapeHtml(title)}</b><p>${escapeHtml(copy.full)}</p></aside>`;
857
+ }
858
+
859
+ function renderPhase() {
860
+ switch (state.phase) {
861
+ case 'opening': return renderOpening();
862
+ case 'opening_aftermath': return renderOpeningAftermath();
863
+ case 'crisis': return renderHouseCrisis();
864
+ case 'crisis_aftermath': return renderHouseCrisisAftermath();
865
+ case 'pair_interlude': return renderPairInterlude();
866
+ case 'favor_reckoning': return renderFavorReckoning();
867
+ case 'memory_reckoning': return renderMemoryReckoning();
868
+ case 'act_transition': return renderActTransition();
869
+ case 'act_aftermath': return renderActAftermath();
870
+ case 'day': return renderDay();
871
+ case 'day_aftermath': return renderDayAftermath();
872
+ case 'joint_result': return renderJointResult();
873
+ case 'portable_precedent': return renderPortablePrecedent();
874
+ case 'household': return renderHousehold();
875
+ case 'household_aftermath': return renderHouseholdAftermath();
876
+ case 'council': return renderCouncil();
877
+ case 'council_aftermath': return renderCouncilAftermath();
878
+ case 'banquet': return renderBanquet();
879
+ case 'public_evidence': return renderPublicEvidence();
880
+ case 'public_followup': return renderPublicFollowup();
881
+ case 'public_aftermath': return renderPublicAftermath();
882
+ case 'five_private_prices': return renderFivePrivatePrices();
883
+ case 'final_reckoning': return renderFinalReckoning();
884
+ case 'final_aftermath': return renderFinalReckoningAftermath();
885
+ case 'dusk_invitation': return renderDuskInvitation();
886
+ case 'dusk_invitation_aftermath': return renderDuskInvitationAftermath();
887
+ case 'choose_visit': return renderVisitHub();
888
+ case 'shared_night': return renderSharedNight();
889
+ case 'shared_afterglow': return renderSharedAfterglow();
890
+ case 'shared_afterglow_result': return renderSharedAfterglowResult();
891
+ case 'shared_dawn': return renderSharedDawn();
892
+ case 'shared_dawn_result': return renderSharedDawnResult();
893
+ case 'visit': return renderVisit();
894
+ case 'route_aftermath': return renderRouteAftermath();
895
+ case 'night': return renderNight();
896
+ case 'morning_settlement': return renderMorningSettlement();
897
+ case 'morning': return renderMorning();
898
+ case 'scene': return renderScene();
899
+ case 'personal_afterglow': return renderPersonalAfterglow();
900
+ case 'personal_afterglow_aftermath': return renderPersonalAfterglowAftermath();
901
+ case 'personal_finale': return renderPersonalFinale();
902
+ case 'personal_finale_result': return renderPersonalFinaleResult();
903
+ case 'alliance_assembly': return renderAllianceAssembly();
904
+ case 'alliance_night': return renderAllianceNight();
905
+ case 'alliance_night_result': return renderAllianceNightResult();
906
+ case 'collapse_finale': return renderCollapseFinale();
907
+ case 'collapse_finale_result': return renderCollapseFinaleResult();
908
+ case 'ending': return renderEnding();
909
+ default: return '<div class="fatal-card">这页账断了。</div>';
910
+ }
911
+ }
912
+
913
+ function renderOpening() {
914
+ return `
915
+ <div class="opening-scene visual-stage" style="--scene-bg:url('${urlFor('cover')}')">
916
+ <div class="decision-panel opening-panel">
917
+ <div class="opening-case" aria-hidden="true"><span>初日</span><b>一</b><i>正堂·灯下</i></div>
918
+ ${phaseHeader('正堂 · 五更尽', '账上空了五十两', '窗纸还没有亮。月娘把两本账摊在灯下,其中一页有一行新墨。金莲坐在桌另一头,指尖还按着一只没人喝的酒杯。')}
919
+ <p class="speaker-line"><b>月娘</b>“你先看账。”<br/><b>金莲</b>“也先听听人怎么说。”</p>
920
+ <div class="choice-grid">${OPENING_CHOICES.map((choice) => choiceButton(choice, 'opening')).join('')}</div>
921
+ </div>
922
+ </div>`;
923
+ }
924
+
925
+ function renderOpeningAftermath() {
926
+ const story = E.currentOpeningAftermath(state);
927
+ const beat = story?.current;
928
+ const source = OPENING_CHOICES.find((choice) => choice.id === story?.choice);
929
+ if (!story || !beat || !source) return '<div class="fatal-card">正堂第一笔没有接到后章。</div>';
930
+ const active = new Set(beat.speakers);
931
+ const last = story.beat + 1 === story.count;
932
+ return `
933
+ <div class="opening-aftermath-stage visual-stage opening-after-${story.choice}" data-opening-aftermath="${story.choice}:${story.beat}" style="--scene-bg:url('${urlFor('cover')}')">
934
+ <div class="opening-aftermath-cast" aria-label="五个人正在回应你的第一笔选择">
935
+ ${HEROINE_IDS.map((id) => `<figure class="${active.has(id) ? 'speaking' : 'listening'}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><i>${HEROINES[id].glyph}</i><b>${HEROINES[id].short}</b></figcaption></figure>`).join('')}
936
+ </div>
937
+ <div class="decision-panel opening-aftermath-panel">
938
+ ${phaseHeader(`第一日 · 正堂起手后章 · ${narrativeStep(story.beat + 1, story.count)}`, beat.title, beat.body)}
939
+ <p class="opening-origin"><span>你刚才选择</span><b>${escapeHtml(source.label)}</b><small>${escapeHtml(source.text)}</small></p>
940
+ ${story.previous.length ? `<ol class="opening-aftermath-transcript" aria-label="开局已经发生的回应">${story.previous.map((row, index) => `<li><span>${narrativeBeatMark(index)}</span><div><b>${escapeHtml(row.title)}</b><p>${escapeHtml(row.body)}</p></div></li>`).join('')}</ol>` : ''}
941
+ <div class="opening-aftermath-voices">${beat.speakers.map((id) => `<blockquote data-opening-speaker="${id}"><span>${HEROINES[id].house}</span><b>${HEROINES[id].name}</b><p>${escapeHtml(beat.voices[id])}</p></blockquote>`).join('')}</div>
942
+ <button class="ink-button story-continue opening-aftermath-continue" data-opening-aftermath-continue="1">${last ? '带着五条线索进入第一日' : story.beat === 0 ? '听另外三院拿出证物' : '看五十两怎样分出三条去路'}</button>
943
+ </div>
944
+ </div>`;
945
+ }
946
+
947
+ function renderHouseCrisis() {
948
+ const event = E.currentHouseCrisis(state);
949
+ if (!event) return '<div class="fatal-card">这场宅门危机没有接上。</div>';
950
+ const pair = event.rescuePair;
951
+ const currentReply = event.currentReply;
952
+ const replyLedger = event.previousReplies?.length
953
+ ? `<ol class="crisis-reply-ledger" aria-label="三个人已经作出的自主答复">${event.previousReplies.map((reply, index) => `<li data-crisis-reply-record="${reply.heroine}:${reply.outcome}"><span>${narrativeBeatMark(index)}</span><div><b>${escapeHtml(HEROINES[reply.heroine].short)} · ${escapeHtml(reply.title)}</b><p>${escapeHtml(reply.action)}</p><small>${escapeHtml(reply.reason)}</small></div></li>`).join('')}</ol>`
954
+ : '';
955
+ const replyPanel = currentReply
956
+ ? `<section class="crisis-current-reply" data-crisis-current-reply="${currentReply.heroine}:${currentReply.outcome}" aria-live="polite">
957
+ <header><span>本人答复 · ${narrativeStep(event.replyBeat + 1, event.responses.length)}</span><b>${escapeHtml(HEROINES[currentReply.heroine].name)} · ${escapeHtml(currentReply.title)}</b></header>
958
+ <blockquote>${escapeHtml(currentReply.line)}</blockquote>
959
+ <dl><div><dt>她处分的原物</dt><dd>${escapeHtml(currentReply.object)}</dd></div><div><dt>她实际会做</dt><dd>${escapeHtml(currentReply.action)}</dd></div></dl>
960
+ <p>${escapeHtml(currentReply.reason)}</p>
961
+ <button class="ink-button story-continue crisis-reply-continue" data-house-crisis-reply="1">${event.replyBeat + 1 < event.responses.length ? '听下一人处分本人之物' : '把三份答复并到救法桌上'}</button>
962
+ </section>`
963
+ : `<p class="crisis-rule">三份本人答复已经落齐。现在只能从仍愿开放的原物与劳动里组成救法;撤回者不会因危局被重新算作同意。</p>
964
+ <div class="choice-stack crisis-structure-choices">${E.houseCrisisOptions(state).map((choice) => choiceButton(choice, 'house-crisis')).join('')}</div>`;
965
+ return `
966
+ <div class="house-crisis-stage visual-stage crisis-${event.type}" data-house-crisis="${event.event}" style="--scene-bg:url('${urlFor('compound')}')">
967
+ <div class="crisis-cast" aria-label="被卷入危机的三个人">${event.participants.map((id) => `<figure class="${pair?.left === id || pair?.right === id ? 'rescue-ready' : ''} ${currentReply?.heroine === id ? 'answering' : ''}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].short}</b><small>${currentReply?.heroine === id ? '正在处分本人之物' : event.previousReplies?.some((reply) => reply.heroine === id) ? '已经留下本人答复' : pair?.left === id || pair?.right === id ? '当前最可能共同接手' : '仍在等候本人答复'}</small></figcaption></figure>`).join('')}</div>
968
+ <div class="decision-panel house-crisis-panel">
969
+ ${phaseHeader(`${event.kicker} · 第 ${event.act} 幕`, event.title, event.body)}
970
+ <div class="crisis-ledger"><span>应对局面 <b>${E.pressureMomentum(state).missed ? '旧办法已被看穿' : '尚未露怯'}</b></span><span>现银 <b>${resourceCondition('silver', state.resources.silver)}</b></span><span>宅门 <b>${resourceCondition('house', state.resources.house)}</b></span><span>耗损 <b>${resourceCondition('strain', state.resources.strain)}</b></span></div>
971
+ <p class="crisis-rule">危局先问三个人各自愿意开放什么。她们依据真实前史站住、收窄或撤回;玩家不能替她们改答,也不能拿好感数值顶替本人边界。</p>
972
+ ${replyLedger}
973
+ ${replyPanel}
974
+ </div>
975
+ </div>`;
976
+ }
977
+
978
+ function renderHouseCrisisAftermath() {
979
+ const story = E.currentHouseCrisisAftermath(state);
980
+ const beat = story?.current;
981
+ if (!story || !beat) return '<div class="fatal-card">危机后的补救没有接到下一页。</div>';
982
+ const approachLabel = story.approach === 'crisis_pair'
983
+ ? `${story.pair?.map((id) => HEROINES[id].short).join('与') ?? '两院'}接手`
984
+ : story.event.choices.find((choice) => choice.id === story.approach)?.label ?? '危机已止血';
985
+ const action = story.awaitingChoice
986
+ ? `<div class="choice-grid crisis-aftermath-choices">${E.houseCrisisAftermathOptions(state).map((choice) => choiceButton(choice, 'house-crisis-aftermath')).join('')}</div>`
987
+ : `<button class="ink-button story-continue" data-house-crisis-aftermath-continue="1">${story.beat === 3 ? '把新规带进这一日' : story.beat === 0 ? '看谁承担了救法的代价' : '把临时补救问成一条规矩'}</button>`;
988
+ return `
989
+ <div class="crisis-aftermath-stage visual-stage crisis-aftermath-${story.event.type} crisis-aftermath-beat-${story.beat + 1}" data-crisis-aftermath="${story.event.type}" data-crisis-aftermath-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor('compound')}')">
990
+ <div class="crisis-aftermath-cast" aria-label="危机后的三人现场">
991
+ ${story.participants.map((id) => `<figure class="${story.speaker === id ? 'speaking' : ''} ${story.pair?.includes(id) ? 'rescue-pair' : ''}" data-crisis-cast="${id}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><i>${HEROINES[id].glyph}</i><b>${HEROINES[id].name}</b><small>${story.pair?.includes(id) ? '参与救局' : '检查谁承担后果'}</small></figcaption></figure>`).join('')}
992
+ </div>
993
+ <div class="decision-panel crisis-aftermath-panel">
994
+ ${phaseHeader(`可恢复危机 · 后续${narrativeStep(story.beat + 1, story.count)}`, beat.title, beat.body)}
995
+ <div class="crisis-aftermath-ledger">
996
+ <span><small>初步救法</small><b>${escapeHtml(approachLabel)}</b></span>
997
+ <span><small>当前宅门</small><b>${resourceCondition('house', state.resources.house)}</b></span>
998
+ <span><small>当前耗损</small><b>${resourceCondition('strain', state.resources.strain)}</b></span>
999
+ </div>
1000
+ <p class="crisis-aftermath-rule">止住今天不等于解决以后。当前说话的人正在决定:这次救局会不会再次制造单点依赖、无名劳动或不可撤回的证词。</p>
1001
+ ${action}
1002
+ </div>
1003
+ </div>`;
1004
+ }
1005
+
1006
+ function renderPairInterlude() {
1007
+ const event = E.currentPairInterlude(state);
1008
+ if (!event) return '<div class="fatal-card">这场双院夜话没有接上。</div>';
1009
+ const left = HEROINES[event.left];
1010
+ const right = HEROINES[event.right];
1011
+ if (event.resolutionBeat) {
1012
+ const witness = HEROINES[event.witness];
1013
+ const previousBeats = event.resolutionBeats.slice(0, event.resolution.beat);
1014
+ const witnessSpeaking = event.resolutionBeat.speaker === event.witness;
1015
+ const closingBeat = event.resolution.beat + 1 === event.resolutionCount;
1016
+ const chapterLead = event.resolution.beat === 0
1017
+ ? '这次选择没有停在口头。两院必须先把它做成一件第三个人能够检验的事。'
1018
+ : witnessSpeaking
1019
+ ? '旁院不替她们喝彩,只说明这份新关系怎样改动了自己的处境。'
1020
+ : '当事两院把刚才的异议写回原约,这段关系从一句回应变成往后会被追问的事实。';
1021
+ return `
1022
+ <div class="pair-interlude-stage pair-aftermath-stage visual-stage pair-${event.left}-${event.right}" data-pair-aftermath="${event.event}:${event.resolutionChoice}:${event.resolution.beat}">
1023
+ <div class="pair-portraits pair-aftermath-cast" aria-label="${left.name}与${right.name}正在执行刚才定下的关系">
1024
+ <figure class="${witnessSpeaking ? 'settled' : 'speaking'}"><img src="${urlFor(left.close)}" alt="${left.name}"/><figcaption><span>${left.house}</span><b>${left.name}</b><p>${witnessSpeaking ? '她已经做完自己的那一部分,正在听第三院指出代价。' : closingBeat ? '她把第三院指出的代价写回刚才的约定,不让结盟停在好听处。' : '她没有把决定留给明天,正与另一院共同执行。'}</p></figcaption></figure>
1025
+ <figure class="${witnessSpeaking ? 'settled' : 'speaking'}"><img src="${urlFor(right.close)}" alt="${right.name}"/><figcaption><span>${right.house}</span><b>${right.name}</b><p>${witnessSpeaking ? '两院的新关系已经落地,现在要看它怎样改变旁院。' : closingBeat ? '她没有撤回共同动作,而是同另一院一起完成最后落字。' : '她不再经由你转述,直接接住了共同动作。'}</p></figcaption></figure>
1026
+ </div>
1027
+ <figure class="pair-aftermath-witness ${witnessSpeaking ? 'speaking' : closingBeat ? 'settled' : 'waiting'}"><img src="${urlFor(witness.portrait)}" alt="${witness.name}"/><figcaption><span>${witness.house}</span><b>${witness.name}</b><small>${witnessSpeaking ? '她不替两院喝彩,只说明这项新关系怎样落到自己身上。' : closingBeat ? '她的异议没有被压下,已经被当事两院写进最后结果。' : '她会在下一拍检验这份新关系是否真的改变了后宫。'}</small></figcaption></figure>
1028
+ <div class="decision-panel pair-interlude-panel pair-aftermath-panel">
1029
+ ${phaseHeader(`双院落约 · ${escapeHtml(event.resolutionLabel)} · 后章${narrativeStep(event.resolution.beat + 1, event.resolutionCount)}`, event.resolutionTitle, chapterLead)}
1030
+ <p class="pair-aftermath-origin"><span>你刚才选择</span><b>${escapeHtml(event.resolutionLabel)}</b><small>${escapeHtml(event.results[event.resolutionChoice])}</small></p>
1031
+ ${previousBeats.length ? `<ol class="pair-aftermath-transcript" aria-label="这次双院选择已经发生的动作">${previousBeats.map((beat, index) => `<li><span>${narrativeBeatMark(index)}</span><div><b>${escapeHtml(beat.title)}</b><p>${escapeHtml(beat.body)}</p></div></li>`).join('')}</ol>` : ''}
1032
+ <blockquote class="pair-aftermath-current ${witnessSpeaking ? 'witness-line' : 'pair-line'}"><span>${escapeHtml(event.resolutionBeat.speakerHouse)}</span><b>${escapeHtml(event.resolutionBeat.title)}</b><p>${escapeHtml(event.resolutionBeat.body)}</p></blockquote>
1033
+ <button class="ink-button story-continue" data-pair-story="1">${event.resolution.beat === 0 ? `看${witness.short}怎样接住` : witnessSpeaking ? '听她们把异议写回原约' : '把这份关系带到次晨'}</button>
1034
+ </div>
1035
+ </div>`;
1036
+ }
1037
+ const active = event.storyBeat?.speaker;
1038
+ const atChoice = event.beat === event.count - 1;
1039
+ const portraitLine = (id, line) => active === id
1040
+ ? escapeHtml(line)
1041
+ : atChoice
1042
+ ? '她没有收回刚才的话,正等这份横向关系怎样落地。'
1043
+ : event.beat > 0
1044
+ ? '上一句话仍留在桌上;她没有替对方抢答。'
1045
+ : '她先听对方把条件说完,没有把沉默当成同意。';
1046
+ return `
1047
+ <div class="pair-interlude-stage visual-stage pair-${event.left}-${event.right}" data-pair-interlude="${event.event}" data-pair-beat="${event.beat}">
1048
+ <div class="pair-portraits" aria-label="${left.name}与${right.name}正在直接交谈">
1049
+ <figure class="${active === event.left || atChoice ? 'speaking' : 'waiting'}"><img src="${urlFor(left.close)}" alt="${left.name}"/><figcaption><span>${left.house}</span><b>${left.name}</b><p>${portraitLine(event.left, event.leftLine)}</p></figcaption></figure>
1050
+ <figure class="${active === event.right || atChoice ? 'speaking' : 'waiting'}"><img src="${urlFor(right.close)}" alt="${right.name}"/><figcaption><span>${right.house}</span><b>${right.name}</b><p>${portraitLine(event.right, event.rightLine)}</p></figcaption></figure>
1051
+ </div>
1052
+ <div class="decision-panel pair-interlude-panel">
1053
+ ${phaseHeader(`${event.kicker} · 双院关系事件`, event.title, event.body)}
1054
+ <p class="pair-progress">双院私议 · ${narrativeStep(event.beat + 1, event.count)} · ${atChoice ? '两个人共同把问题交还给你' : active === event.left ? `${left.short}先开口` : `${right.short}接住这句话`}</p>
1055
+ ${!atChoice ? `<blockquote class="pair-current-line"><span>${active === event.left ? left.name : right.name}</span>${escapeHtml(event.storyBeat.text)}</blockquote>` : ''}
1056
+ ${atChoice ? `<blockquote class="pair-joint-line"><span>她们共同追问</span>${escapeHtml(event.jointLine)}</blockquote>` : ''}
1057
+ <div class="pair-trust"><span>她们不经你转述也能谈到这里</span><b>当前关系 · ${E.bondTier(event.trust)}</b></div>
1058
+ ${atChoice
1059
+ ? `<p class="pair-rule">支持她们形成横向关系,更容易出现双院、三院或五院结局;把话重新拉回你身边,则会提高个人情欲,却削弱她们彼此结盟的可能。</p><div class="choice-stack">${E.pairInterludeOptions(state).map((choice) => choiceButton(choice, 'pair-interlude')).join('')}</div>`
1060
+ : '<button class="ink-button story-continue" data-pair-story="1">听她把这句话说完</button>'}
1061
+ </div>
1062
+ </div>`;
1063
+ }
1064
+
1065
+ function renderFavorReckoning() {
1066
+ const event = E.currentFavorReckoning(state);
1067
+ if (!event) return '<div class="fatal-card">这笔人情债没有找到当日借力的原页。</div>';
1068
+ const heroine = HEROINES[event.heroine];
1069
+ const observer = HEROINES[event.observer];
1070
+ const castState = (id) => event.speaker === id ? 'speaking' : event.speaker ? 'listening' : event.resolved ? 'settled' : 'weighing';
1071
+ const continueLabel = event.resolved
1072
+ ? '把双院结果记进总账,进入白日'
1073
+ : event.beat === 0
1074
+ ? `让${observer.short}把旁院代价摆出来`
1075
+ : '两院都已说完,轮到你裁决';
1076
+ return `
1077
+ <div class="favor-reckoning-stage visual-stage favor-${event.heroine} favor-step-${event.step}" data-favor-reckoning-event="${event.event}" data-favor-reckoning-step="${event.step + 1}">
1078
+ <div class="favor-cast" aria-label="${heroine.name}与${observer.name}正在演完人情追账第${event.step + 1}拍">
1079
+ <figure class="favor-lead ${castState(event.heroine)}"><img src="${urlFor(heroine.close)}" alt="${heroine.name}"/><figcaption><span>当日先押出自己的人</span><b>${heroine.name}</b><p>${event.speaker === event.heroine ? '她正在开账。' : event.resolved ? '她等你把结果正式记下。' : '她没有收回刚才的话。'}</p></figcaption></figure>
1080
+ <figure class="${castState(event.observer)}"><img src="${urlFor(observer.close)}" alt="${observer.name}"/><figcaption><span>看见横向代价的人</span><b>${observer.name}</b><p>${event.speaker === event.observer ? '她正在举证。' : event.resolved ? '她也会记住你如何收尾。' : '她在等自己的那一页。'}</p></figcaption></figure>
1081
+ </div>
1082
+ <div class="decision-panel favor-reckoning-panel">
1083
+ <div class="favor-progress" aria-label="人情追账四拍进度"><span class="${event.step >= 0 ? 'done' : ''}">开账</span><span class="${event.step >= 1 ? 'done' : ''}">举证</span><span class="${event.step >= 2 ? 'done' : ''}">裁决</span><span class="${event.step >= 3 ? 'done' : ''}">结果</span></div>
1084
+ ${phaseHeader(`人情追账 · ${narrativeStep(event.step + 1, event.count)} · 第 ${event.sourceDay} 日 → 第 ${state.day} 日`, event.current.title, event.current.body)}
1085
+ <div class="favor-thread"><span>那天危局确实收住了,但不是免费收住</span><b>你没有顺着征兆,而是借${heroine.short}的“${escapeHtml(event.sourceLabel)}”先压住局面</b><small>${escapeHtml(event.sourceText)}</small><em>${event.daysLater} 日后,${observer.short}也到场;这次选择会同时改变个人情分与两院互信。</em></div>
1086
+ ${event.awaitingChoice
1087
+ ? `<div class="favor-verdict-note">两个人已经把来路和代价都说完。现在没有“什么都不做”:你必须决定这笔人情由谁、用什么方式偿还。</div><div class="choice-stack">${E.favorReckoningOptions(state).map((choice) => choiceButton(choice, 'favor-reckoning')).join('')}</div>`
1088
+ : `<button class="ink-button story-continue favor-story-continue" data-favor-story="1">${continueLabel}</button>`}
1089
+ </div>
1090
+ </div>`;
1091
+ }
1092
+
1093
+ function renderMemoryReckoning() {
1094
+ const event = E.currentMemoryReckoning(state);
1095
+ if (!event) return '<div class="fatal-card">这笔旧话没有找到原页。</div>';
1096
+ const heroine = HEROINES[event.heroine];
1097
+ const observer = HEROINES[event.observer];
1098
+ const castState = (id) => event.speaker === id ? 'speaking' : event.speaker ? 'listening' : event.resolved ? 'settled' : 'weighing';
1099
+ const promise = {
1100
+ public: '那晚你选择:把后果拿到长案',
1101
+ direct: '那晚你选择:让她们自己谈',
1102
+ private: '那晚你选择:把后果留在门内',
1103
+ }[event.promise];
1104
+ const continueLabel = event.resolved
1105
+ ? '把这次兑现写回人物总账,进入白日'
1106
+ : event.beat === 0
1107
+ ? `让${observer.short}说出旁院真正承受的部分`
1108
+ : '原话与代价都已摆齐,轮到你决定';
1109
+ return `
1110
+ <div class="memory-reckoning-stage visual-stage memory-${event.heroine} memory-step-${event.step}" data-memory-reckoning-event="${event.event}" data-memory-reckoning-step="${event.step + 1}">
1111
+ <div class="memory-cast" aria-label="${heroine.name}与${observer.name}正在演完旧话追账第${event.step + 1}拍">
1112
+ <figure class="memory-lead ${castState(event.heroine)}"><img src="${urlFor(heroine.close)}" alt="${heroine.name}"/><figcaption><span>${heroine.house} · 记住原话的人</span><b>${heroine.name}</b><p>${event.speaker === event.heroine ? '她正在逐字复述。' : event.resolved ? '她在看这句承诺如何落地。' : '她没有替你改写原话。'}</p></figcaption></figure>
1113
+ <figure class="${castState(event.observer)}"><img src="${urlFor(observer.close)}" alt="${observer.name}"/><figcaption><span>${observer.house} · 承受外溢的人</span><b>${observer.name}</b><p>${event.speaker === event.observer ? '她正在说明旁院代价。' : event.resolved ? '她也会按这次结果安排自己。' : '她等原话先被完整念完。'}</p></figcaption></figure>
1114
+ </div>
1115
+ <div class="decision-panel memory-reckoning-panel">
1116
+ <div class="memory-progress" aria-label="旧话追账四拍进度"><span class="${event.step >= 0 ? 'done' : ''}">复述</span><span class="${event.step >= 1 ? 'done' : ''}">质询</span><span class="${event.step >= 2 ? 'done' : ''}">裁决</span><span class="${event.step >= 3 ? 'done' : ''}">结果</span></div>
1117
+ ${phaseHeader(`${event.kicker} · ${narrativeStep(event.step + 1, event.count)} · 第 ${event.sourceDay} 日 → 第 ${state.day} 日`, event.current.title, event.current.body)}
1118
+ <div class="memory-thread"><span>原话没有随那一夜结束</span><b>你先选了“${escapeHtml(event.sourceLabel)}”</b><small>${escapeHtml(event.sourceText)}</small><strong>当时实际执行 · ${escapeHtml(event.sourceStake.label)} · ${escapeHtml(narrativeChoiceMeta(event.sourceStake.resourceText))}</strong><small>${escapeHtml(event.sourceStake.text)}</small><strong>今日只追这一件</strong><small>${escapeHtml(event.sourceReturn.question)}</small><em>${promise} · ${event.daysLater} 日后,这件原物与代价一起回来追账。</em></div>
1119
+ ${event.awaitingChoice
1120
+ ? `<div class="memory-verdict-note">她们争的不是你还记不记得,而是这句话到了今日是否仍能约束你。兑现、重写或否认,都会成为以后她们判断你口头承诺的依据。</div><div class="choice-stack">${E.memoryReckoningOptions(state).map((choice) => choiceButton(choice, 'memory-reckoning')).join('')}</div>`
1121
+ : `<button class="ink-button story-continue memory-story-continue" data-memory-story="1">${continueLabel}</button>`}
1122
+ </div>
1123
+ </div>`;
1124
+ }
1125
+
1126
+ function renderDuskInvitation() {
1127
+ const event = E.currentDuskInvitation(state);
1128
+ if (!event) return '<div class="fatal-card">黄昏这份邀约没有找到来处。</div>';
1129
+ const heroine = HEROINES[event.heroine];
1130
+ const witness = HEROINES[event.witness];
1131
+ return `
1132
+ <div class="dusk-invitation-stage visual-stage invite-${event.heroine}" data-dusk-invitation-event="${event.event}">
1133
+ <div class="dusk-invite-cast">
1134
+ <figure class="invite-lead"><img src="${urlFor(heroine.close)}" alt="${heroine.name}"/><figcaption><span>她主动来请</span><b>${heroine.name}</b></figcaption></figure>
1135
+ <figure class="invite-witness"><img src="${urlFor(witness.portrait)}" alt="${witness.name}"/><figcaption><span>旁院见证</span><b>${witness.name}</b></figcaption></figure>
1136
+ </div>
1137
+ <div class="decision-panel dusk-invitation-panel">
1138
+ ${phaseHeader(`${event.kicker} · 第 ${state.day} 日黄昏`, event.title, event.body)}
1139
+ <div class="dusk-invite-voices"><p><b>${heroine.short}</b>${escapeHtml(event.heroineLine)}</p><p><b>${witness.short}</b>${escapeHtml(event.witnessLine)}</p></div>
1140
+ <p class="dusk-invite-rule">她不是免费加好感的随机事件。赴约会直接改变今晚去处;让要求见光会强化两院关系;拒绝也可以,但不能再用一份含糊的“改日”把她留在原地。</p>
1141
+ <div class="choice-stack">${E.duskInvitationOptions(state).map((choice) => choiceButton(choice, 'dusk-invitation')).join('')}</div>
1142
+ </div>
1143
+ </div>`;
1144
+ }
1145
+
1146
+ function renderDuskInvitationAftermath() {
1147
+ const story = E.currentDuskInvitationAftermath(state);
1148
+ if (!story?.current) return '<div class="fatal-card">这次主动邀约没有接上后话。</div>';
1149
+ const heroine = HEROINES[story.heroine];
1150
+ const witness = HEROINES[story.witness];
1151
+ const event = DUSK_INVITATIONS[story.heroine];
1152
+ const approachLabel = { accept:'赴她的约', open:'请她当面说完', decline:'诚实说明今夜不去' }[story.approach];
1153
+ const action = story.awaitingChoice
1154
+ ? `<div class="choice-grid invitation-aftermath-choices">${E.duskInvitationAftermathOptions(state).map((choice) => choiceButton(choice, 'dusk-invitation-aftermath')).join('')}</div>`
1155
+ : `<button class="ink-button story-continue" data-dusk-invitation-aftermath-continue="1">${story.resolved ? (story.approach === 'accept' ? `随${heroine.short}真正进门` : '带着这句话回到五院岔口') : story.step === 0 ? `听${witness.short}把旁院代价问出来` : '让她把今夜的具体安排说清'}</button>`;
1156
+ return `
1157
+ <div class="invitation-aftermath-stage visual-stage invite-after-${story.heroine} ${story.resolved ? 'resolved' : ''}" data-dusk-invitation-aftermath="${story.approach}" data-invitation-aftermath-beat="${story.step + 1}">
1158
+ <div class="invitation-aftermath-cast" aria-label="${heroine.name}与${witness.name}把主动邀约说到底">
1159
+ <figure class="${story.speaker === story.heroine ? 'speaking' : 'listening'}"><img src="${urlFor(heroine.close)}" alt="${heroine.name}"/><figcaption><span>主动邀约者</span><b>${heroine.name}</b></figcaption></figure>
1160
+ <figure class="${story.speaker === story.witness ? 'speaking' : 'listening'}"><img src="${urlFor(witness.close)}" alt="${witness.name}"/><figcaption><span>旁院见证者</span><b>${witness.name}</b></figcaption></figure>
1161
+ </div>
1162
+ <div class="decision-panel invitation-aftermath-panel">
1163
+ ${phaseHeader(`主动邀约后续 · ${approachLabel} · ${narrativeStep(story.step + 1, story.count)}`, story.current.title, story.current.body)}
1164
+ <div class="invitation-aftermath-ledger" aria-label="这次邀约已经发生的事实">
1165
+ <span><small>她先说了</small><b>${escapeHtml(event.title)}</b></span>
1166
+ <span><small>你先回答</small><b>${escapeHtml(approachLabel)}</b></span>
1167
+ <span><small>现在要决定</small><b>${story.resolved ? escapeHtml(story.choice.label) : story.awaitingChoice ? '怎样让偏爱落到可执行的安排' : story.step === 0 ? '旁院是否只能靠猜' : '今晚的钥匙、记录与退出权'}</b></span>
1168
+ </div>
1169
+ <p class="invitation-aftermath-rule">${story.resolved ? '这项安排已经同时进入个人关系与两院横向关系;它不会在进门以后被一笔好感值抹掉。' : story.awaitingChoice ? '第一次回答决定去不去;这一次决定她们以后怎样追问、拒绝和记住今晚。' : '邀约者与见证者都会把话说完,第三拍以前不会把你送回普通院门。'}</p>
1170
+ ${action}
1171
+ </div>
1172
+ </div>`;
1173
+ }
1174
+
1175
+ function renderActTransition() {
1176
+ const event = E.currentActTransition(state);
1177
+ if (!event) return '<div class="fatal-card">换幕这页没有接上。</div>';
1178
+ const previousPublic = PUBLIC_EVENTS[state.day - 1];
1179
+ const background = previousPublic ? SCENES[previousPublic.scene].asset : 'compound';
1180
+ return `
1181
+ <div class="act-transition-stage visual-stage act-transition-${event.act}" data-act-transition="${event.id}" style="--scene-bg:url('${urlFor(background)}')">
1182
+ <div class="act-transition-cast" aria-label="五个人一起看见的真相">${event.participants.map((id) => `<figure><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption>${HEROINES[id].short}</figcaption></figure>`).join('')}</div>
1183
+ <div class="decision-panel act-transition-panel">
1184
+ ${phaseHeader(event.kicker, event.title, event.body)}
1185
+ ${event.day5Opening ? `<aside class="act-source-echo day5-public-opening" data-act-day5-opening="${event.day5Opening.choice}"><span>${event.day5Opening.choice === 'public_5_favor' ? '已认偏宠继续限制假账处理' : '逐手经办继续限制假账处理'}</span><b>${escapeHtml(event.day5Opening.label)}</b><p>${escapeHtml(event.day5Opening.day6Text)}</p></aside>` : ''}
1186
+ ${event.sourceEcho ? `<aside class="act-source-echo" data-act-source="${event.sourceEcho.id}"><span>第十日真正留下的处置</span><b>${escapeHtml(event.sourceEcho.label)}</b><p>${escapeHtml(event.sourceEcho.text)}</p></aside>` : ''}
1187
+ ${event.day10Opening ? `<aside class="act-source-echo day10-public-opening" data-act-day10-opening="${event.day10Opening.choice}"><span>第十日原页状态也进入第三幕</span><b>${escapeHtml(event.day10Opening.label)}</b><p>${escapeHtml(event.day10Opening.day11Text)}</p></aside>` : ''}
1188
+ ${event.draftEcho ? `<aside class="act-source-echo" data-act-draft-source="${event.draftEcho.aftermath}"><span>第十四日银票仍在门外起作用</span><b>${escapeHtml(event.draftEcho.label)}</b><p>${escapeHtml(event.draftEcho.day16Text)}</p></aside>` : ''}
1189
+ <blockquote class="act-reveal"><span>这一幕终于看清</span>${escapeHtml(event.reveal)}</blockquote>
1190
+ <div class="choice-grid">${E.actTransitionOptions(state).map((choice) => choiceButton(choice, 'act-transition')).join('')}</div>
1191
+ </div>
1192
+ </div>`;
1193
+ }
1194
+
1195
+ function renderActAftermath() {
1196
+ const story = E.currentActAftermath(state);
1197
+ const beat = story?.current;
1198
+ const previousPublic = PUBLIC_EVENTS[state.day - 1];
1199
+ const background = previousPublic ? SCENES[previousPublic.scene].asset : 'compound';
1200
+ if (!story || !beat) return '<div class="fatal-card">换幕后果没有接上下一页。</div>';
1201
+ const last = story.resolved || story.beat === story.count - 1;
1202
+ const rebuttal = story.externalRebuttal;
1203
+ const internalPractice = story.internalPractice
1204
+ ? `<aside class="act-internal-practice"><span>五院原先定下的新做法</span><b>${escapeHtml(story.internalPractice.title)}</b><p>${escapeHtml(story.internalPractice.body)}</p></aside>`
1205
+ : '';
1206
+ const rebuttalBlock = rebuttal && !story.resolved ? `
1207
+ <section class="external-rebuttal" data-external-rebuttal="${rebuttal.sourceResult}">
1208
+ <details class="supporting-context external-rebuttal-context"><summary><b>三口复案的来路</b><span>三人证言</span></summary><div class="supporting-context-pages"><div class="external-rebuttal-context-pages">
1209
+ <header><div><small>第十五日原案</small><b>${escapeHtml(rebuttal.sourceOutcome.label)}</b></div><div><small>第十六日第一选择</small><b>${escapeHtml(rebuttal.actChoice.label)}</b></div><div><small>当时未递</small><b>${escapeHtml(rebuttal.missingEvidence?.label ?? '无')}</b></div></header>
1210
+ ${rebuttal.publicOpening ? `<aside class="act-source-echo day15-public-opening ${rebuttal.publicOpening.falseScapegoat ? 'is-contaminated' : ''}" data-day16-public-opening="${rebuttal.publicOpening.choice}"><span>${rebuttal.publicOpening.falseScapegoat ? '复案先拆错误预断' : '复案仍不得预填犯人'}</span><b>${escapeHtml(rebuttal.publicOpening.label)}</b><p>${escapeHtml(rebuttal.publicOpening.rebuttalText)}</p></aside>` : ''}
1211
+ ${rebuttal.hearingEcho ? `<aside class="council-external-echo external-hearing-echo" data-day15-hearing-echo="${rebuttal.hearingEcho.sourceAction}"><span>昨日白日经手留下的物件也在受问</span><b>${escapeHtml(rebuttal.hearingEcho.label)}</b><p>${escapeHtml(rebuttal.hearingEcho.rebuttalText)}</p><small>${escapeHtml(rebuttal.hearingEcho.object)}</small></aside>` : ''}
1212
+ <ol class="external-rebuttal-voices" aria-label="三名外部人物接成一线的反问">${rebuttal.voices.map((voice, index) => {
1213
+ const actor = rebuttal.actors.find((person) => person.id === voice.actor);
1214
+ return `<li data-external-rebuttal-actor="${voice.actor}"><i>${narrativeBeatMark(index)}</i><div><span>${escapeHtml(actor?.role ?? '')}</span><b>${escapeHtml(actor?.name ?? voice.actor)}</b><p>${escapeHtml(voice.line)}</p></div></li>`;
1215
+ }).join('')}</ol></div></div></details>
1216
+ <p class="external-rebuttal-question">${escapeHtml(rebuttal.question)}</p>
1217
+ <div class="choice-grid external-rebuttal-choices">${E.actAftermathOptions(state).map((choice) => choiceButton(choice, 'act-aftermath-choice')).join('')}</div>
1218
+ </section>` : '';
1219
+ const resolutionBlock = rebuttal && story.resolved ? `
1220
+ <section class="external-rebuttal-result" data-external-rebuttal-result="${rebuttal.resolution.id}">
1221
+ <span>三口复案已经落字</span><b>${escapeHtml(rebuttal.resolution.label)}</b>
1222
+ <p>${escapeHtml(rebuttal.resolution.day17Echo)}</p>
1223
+ </section>` : '';
1224
+ const action = story.awaitingChoice
1225
+ ? ''
1226
+ : `<button class="ink-button story-continue" data-act-aftermath-continue="1">${story.resolved ? '把复案结果带进第四幕' : last ? `真正进入第 ${story.act} 幕` : story.beat === 0 ? '让五院拿现实来验' : story.beat === 1 && state.day === 16 ? '听三人怎样接成反问' : '看她们怎样定下新做法'}</button>`;
1227
+ return `
1228
+ <div class="act-aftermath-stage visual-stage act-aftermath-${story.act} act-beat-${story.beat + 1}" data-act-aftermath="${story.choice}" data-act-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor(background)}')">
1229
+ <div class="act-aftermath-cast" aria-label="一起验证下一幕真相的五个人">${story.participants.map((id) => `<figure class="${beat.speaker === id ? 'speaking' : 'listening'}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].name}</b></figcaption></figure>`).join('')}</div>
1230
+ <div class="decision-panel act-aftermath-panel">
1231
+ ${phaseHeader(`第 ${story.act} 幕开启 · ${story.label} · ${narrativeStep(story.beat + 1, story.count)}`, beat.title, beat.body)}
1232
+ ${story.day5Opening ? `<aside class="act-source-echo day5-public-opening" data-act-aftermath-day5-opening="${story.day5Opening.choice}:${story.beat + 1}"><span>${story.day5Opening.choice === 'public_5_favor' ? '已认偏宠仍限制这一拍' : '逐手经办仍限制这一拍'}</span><b>${escapeHtml(story.day5Opening.label)}</b><p>${escapeHtml(story.day5Opening.text)}</p></aside>` : ''}
1233
+ ${story.day10Opening ? `<aside class="act-source-echo day10-public-opening" data-act-aftermath-day10-opening="${story.day10Opening.choice}:${story.beat + 1}"><span>${story.day10Opening.choice === 'public_10_hide' ? '原页缺口仍限制这一拍' : '五份本人原页仍限制这一拍'}</span><b>${escapeHtml(story.day10Opening.label)}</b><p>${escapeHtml(story.day10Opening.text)}</p></aside>` : ''}
1234
+ ${story.draftEcho && story.beat === 0 ? `<aside class="act-source-echo" data-act-aftermath-draft="${story.draftEcho.aftermath}"><span>换幕选择仍要承担银票的生活后果</span><b>${escapeHtml(story.draftEcho.label)}</b><p>${escapeHtml(story.draftEcho.day16Text)}</p></aside>` : ''}
1235
+ ${internalPractice}
1236
+ ${rebuttalBlock}
1237
+ ${resolutionBlock}
1238
+ ${!story.awaitingChoice ? `<div class="act-aftermath-rule"><span>${story.beat === 0 ? '先承认你刚选的代价' : story.resolved ? '第二次选择已经留下后续问题' : story.beat === 1 ? '让证物与人物反过来检验它' : '把下一幕真正会执行的办法写清'}</span><b>${last ? '五个人没有靠一句漂亮话收束复案;第十七日会继续读取这次守住和放弃的东西。' : '下一拍不会简单赞成;另一院会拿自己的货、名、工或口供检查这套办法是否站得住。'}</b></div>` : ''}
1239
+ ${action}
1240
+ </div>
1241
+ </div>`;
1242
+ }
1243
+
1244
+ function renderDay() {
1245
+ const def = E.dayDef(state);
1246
+ const jointOptions = E.jointActionOptions(state);
1247
+ const jointDone = Math.min(E.jointActionCount(state), E.JOINT_ACTION_TARGET);
1248
+ const opening = state.day === 1 ? E.openingMemory(state) : null;
1249
+ const contextRows = [
1250
+ opening ? `<aside class="opening-memory-slip" data-opening-memory="${opening.choice}"><span>正堂起手仍在影响这一日</span><b>${escapeHtml(opening.title)}</b><p>${escapeHtml(opening.memory)}</p></aside>` : '',
1251
+ def.earlyEcho ? `<aside class="council-external-echo day-early-living-echo" data-early-day-source="${def.earlyEcho.sourceDay}:${def.earlyEcho.sourceAction}"><span>昨日手上留下的物件没有退场</span><b>${escapeHtml(def.earlyEcho.label)}</b><p>${escapeHtml(def.earlyEcho.text)}</p><small>${escapeHtml(def.earlyEcho.object)}</small></aside>` : '',
1252
+ def.accountEcho ? `<aside class="council-external-echo day-account-living-echo" data-day6-account-echo="${def.accountEcho.sourceAction}"><span>昨日两本流水正在把食盒接到车路</span><b>${escapeHtml(def.accountEcho.label)}</b><p>${escapeHtml(def.accountEcho.text)}</p><small>${escapeHtml(def.accountEcho.object)}</small></aside>` : '',
1253
+ def.driverEcho ? `<aside class="council-external-echo day-driver-living-echo" data-day7-driver-echo="${def.driverEcho.sourceAction}"><span>昨日车夫留下的物件正在拆分四次催火</span><b>${escapeHtml(def.driverEcho.label)}</b><p>${escapeHtml(def.driverEcho.text)}</p><small>${escapeHtml(def.driverEcho.object)}</small></aside>` : '',
1254
+ def.stoveEcho ? `<aside class="council-external-echo day-stove-living-echo" data-day8-stove-echo="${def.stoveEcho.sourceAction}"><span>昨日停灶留下的物件正在把旧箱接回生活账</span><b>${escapeHtml(def.stoveEcho.label)}</b><p>${escapeHtml(def.stoveEcho.text)}</p><small>${escapeHtml(def.stoveEcho.object)}</small></aside>` : '',
1255
+ def.saltEcho ? `<aside class="council-external-echo day-salt-living-echo" data-day11-salt-echo="${def.saltEcho.sourceAction}"><span>昨日一瓮盐正在改变今日箱路</span><b>${escapeHtml(def.saltEcho.label)}</b><p>${escapeHtml(def.saltEcho.text)}</p><small>${escapeHtml(def.saltEcho.object)}</small></aside>` : '',
1256
+ def.emergencyEcho ? `<aside class="council-external-echo day-emergency-living-echo" data-day13-emergency-echo="${def.emergencyEcho.sourceAction}"><span>昨日五件急用决定今日银票会压住谁</span><b>${escapeHtml(def.emergencyEcho.label)}</b><p>${escapeHtml(def.emergencyEcho.text)}</p><small>${escapeHtml(def.emergencyEcho.object)}</small></aside>` : '',
1257
+ def.hearingEcho ? `<aside class="council-external-echo day-hearing-living-echo" data-day15-hearing-echo="${def.hearingEcho.sourceAction}"><span>昨日堂前留下的实物正在拆分真债、雇声与饥饿</span><b>${escapeHtml(def.hearingEcho.label)}</b><p>${escapeHtml(def.hearingEcho.text)}</p><small>${escapeHtml(def.hearingEcho.object)}</small></aside>` : '',
1258
+ def.crowdEcho ? `<aside class="council-external-echo day-crowd-living-echo" data-day16-crowd-echo="${def.crowdEcho.sourceAction}"><span>昨日拆围门留下的票、鞋与饭正在接出柜坊入口</span><b>${escapeHtml(def.crowdEcho.label)}</b><p>${escapeHtml(def.crowdEcho.text)}</p><small>${escapeHtml(def.crowdEcho.object)}</small></aside>` : '',
1259
+ def.vaultEcho ? `<aside class="council-external-echo day-vault-living-echo" data-day17-vault-echo="${def.vaultEcho.sourceAction}"><span>昨夜柜坊留下的实物正在限制逃路报价</span><b>${escapeHtml(def.vaultEcho.label)}</b><p>${escapeHtml(def.vaultEcho.text)}</p><small>${escapeHtml(def.vaultEcho.object)}</small></aside>` : '',
1260
+ def.externalPressure ? `<aside class="council-external-echo day-external-pressure" data-external-pressure="${def.externalPressure.sourceResult}"><span>昨日三口复案改变了今日要补的证</span><b>${escapeHtml(def.externalPressure.label)}</b><p>${escapeHtml(def.externalPressure.text)}</p></aside>` : '',
1261
+ def.deedEcho ? `<aside class="council-external-echo day-deed-echo" data-day10-deed-echo="${def.deedEcho.aftermath}"><span>第九日旧契真的去了哪里</span><b>${escapeHtml(def.deedEcho.label)}</b><p>${escapeHtml(def.deedEcho.text)}</p></aside>` : '',
1262
+ def.councilEcho ? `<aside class="council-external-echo day-council-echo" data-day13-council-echo="${def.councilEcho.choice}" data-day10-opening="${def.councilEcho.publicOpeningChoice ?? ''}"><span>第十二日院议先变成今日用度</span><b>${escapeHtml(def.councilEcho.label)}</b><p>${escapeHtml(def.councilEcho.text)}</p><small>${escapeHtml(def.councilEcho.materialText ?? '')}</small></aside>` : '',
1263
+ def.nightLedger ? `<aside class="council-external-echo day-night-ledger" data-night-ledger-day13="${def.nightLedger.choice}" data-day5-opening-long="${def.nightLedger.publicOpeningChoice}"><span>行动权限,不是新增证物</span><b>${escapeHtml(def.nightLedger.title)}</b><p>${escapeHtml(def.nightLedger.text)}</p><small>${escapeHtml(def.nightLedger.permission)} · 第五日仍按“${escapeHtml(def.nightLedger.publicOpeningLabel)}”追责。</small></aside>` : '',
1264
+ ].filter(Boolean);
1265
+ const contextSection = contextRows.length
1266
+ ? `<details class="day-context-details"><summary><b>旧事正在影响今日</b><span>${contextRows.length} 笔旧事</span></summary><div>${contextRows.join('')}</div></details>`
1267
+ : '';
1268
+ const jointSection = state.day < 6
1269
+ ? ''
1270
+ : jointDone >= E.JOINT_ACTION_TARGET
1271
+ ? ''
1272
+ : `<details class="joint-offers" aria-label="联院差事"><summary><b>改做一桩联院差事</b><span>${narrativeProgress(jointDone, E.JOINT_ACTION_TARGET, ['尚未合办', '已有搭档共事', '五组搭档都已共事'])}</span></summary><div class="joint-actions">${jointOptions.map((choice) => choiceButton({ ...choice, meta: '' }, 'joint-action')).join('')}</div></details>`;
1273
+ const dayOptions = E.dayOptions(state);
1274
+ return `
1275
+ <div class="hub-stage visual-stage" style="--scene-bg:url('${urlFor('compound')}')">
1276
+ <div class="decision-panel day-panel">
1277
+ <div class="day-scene-mark" aria-hidden="true"><span>DAY</span><b>${String(state.day).padStart(2, '0')}</b><i>${escapeHtml(def.act)}</i></div>
1278
+ ${phaseHeader(`第 ${state.day} 日 · 院中`, def.name, `${def.pressure} ${def.tell}`)}
1279
+ ${contextSection}
1280
+ ${renderEvidenceBoard()}
1281
+ <div class="day-actions">${dayOptions.map((choice) => choiceButton({ ...choice, hint: `${choice.actor ? `${HEROINES[choice.actor].short} · ` : ''}${choice.hint}`, meta: '' }, 'day-action')).join('')}</div>
1282
+ ${jointSection}
1283
+ </div>
1284
+ </div>`;
1285
+ }
1286
+
1287
+ function renderEvidenceBoard() {
1288
+ const rows = E.secretInventory(state);
1289
+ if (!rows.length) return '';
1290
+ return `<details class="evidence-board" aria-label="证据板"><summary><b>证据板 · ${rows.length} 条</b><span>${state.selectedSecret ? '已有一条压在官面选项上' : '走官面时再挑'}</span></summary><div>${rows.map((row) => `<button data-secret-select="${row.id}" class="evidence-slip ${row.selected ? 'selected' : ''}" aria-pressed="${row.selected}" ${row.expired ? 'disabled' : ''}><b>${escapeHtml(row.label)}</b><span>${escapeHtml(row.source)}</span><em>${row.confidence}${row.expiresOn ? ` · 第${row.expiresOn}日后失效` : ' · 不限日'}</em></button>`).join('')}</div>${state.selectedSecret ? '<button class="evidence-pay" id="btn-secret-clear">收回证据,改用现银</button>' : ''}</details>`;
1291
+ }
1292
+
1293
+ function renderDayAftermath() {
1294
+ const story = E.currentDayAftermath(state);
1295
+ const beat = story?.current;
1296
+ if (!story || !beat) return '<div class="fatal-card">白日后果没有接上。</div>';
1297
+ const actor = HEROINES[story.actor];
1298
+ const last = story.beat === story.count - 1;
1299
+ const reactionCast = beat.reactions.length
1300
+ ? `<div class="day-aftermath-reactions" aria-label="旁院回应">${beat.reactions.map((row) => {
1301
+ const heroine = HEROINES[row.heroine];
1302
+ return `<figure class="${row.delta > 0 ? 'accepts' : 'resists'}"><img src="${urlFor(heroine.portrait)}" alt="${heroine.name}"/><figcaption><span>${escapeHtml(row.tone)}</span><b>${heroine.name}</b><p>${escapeHtml(row.text)}</p></figcaption></figure>`;
1303
+ }).join('')}</div>`
1304
+ : '';
1305
+ const consequence = beat.resolved === null ? '' : `<div class="day-consequence ${beat.resolved ? 'resolved' : 'missed'}"><span>${beat.resolved ? '这件事暂时稳住了' : '还有一处没有堵住'}</span><b>${beat.resolved ? '人、物和原话都留在眼前,明日还能顺着查下去。' : '眼前的好处拿到了,门外那只手却已经换了一条路。'}</b></div>`;
1306
+ return `
1307
+ <div class="day-aftermath-stage visual-stage dialogue-${story.actor} beat-${beat.id}" data-day-aftermath="${beat.id}" style="--scene-bg:url('${urlFor(actor.close || actor.portrait)}')">
1308
+ <div class="day-aftermath-lead" aria-hidden="true" style="background-image:url('${urlFor(actor.close || actor.portrait)}')"></div>
1309
+ ${reactionCast}
1310
+ <div class="decision-panel day-aftermath-panel">
1311
+ ${phaseHeader(beat.kicker, beat.title, beat.body)}
1312
+ <div class="story-progress"><span>${narrativeStep(story.beat + 1, story.count)}</span><i></i><b>${['她先动手', '旁院接话', '危局回手'][story.beat]}</b></div>
1313
+ ${beat.speaker ? `<blockquote class="day-speaker"><span>${HEROINES[beat.speaker].name}</span>${escapeHtml(HEROINES[beat.speaker].voice)}</blockquote>` : ''}
1314
+ ${consequence}
1315
+ <button class="ink-button story-continue day-aftermath-continue" data-day-aftermath-continue="1">${last ? '把后果记进总账' : story.beat === 0 ? '看另外两院怎么接' : '看真正的缺口'}</button>
1316
+ </div>
1317
+ </div>`;
1318
+ }
1319
+
1320
+ function renderJointResult() {
1321
+ const choice = E.currentJointAction(state);
1322
+ const beat = choice?.storyBeat;
1323
+ if (!choice || !beat) return '<div class="fatal-card">这桩联院差事断了页。</div>';
1324
+ const last = choice.beat === choice.count - 1;
1325
+ return `
1326
+ <div class="joint-result-stage visual-stage joint-beat-${choice.beat + 1}" style="--scene-bg:url('${urlFor(choice.asset)}')" data-joint-result="${choice.id}" data-joint-beat="${choice.beat + 1}">
1327
+ <div class="decision-panel joint-result-panel">
1328
+ ${phaseHeader(`联院差事 · ${choice.participants.map((id) => HEROINES[id].short).join('与')} · ${narrativeStep(choice.beat + 1, choice.count)}`, beat.title, beat.body)}
1329
+ <div class="joint-story-row"><div class="joint-pair-voices">${choice.participants.map((id) => `<span class="${beat.speaker === id ? 'speaking' : ''}"><i>${HEROINES[id].glyph}</i>${HEROINES[id].short}</span>`).join('')}</div><p class="joint-payoff">${last ? `${choice.hint} · 两个人都把对方的方法写进自己的证链` : beat.speaker ? `${HEROINES[beat.speaker].short}主导这一幕,另一人仍保留反对与改手的权利` : '方法已经合流,但两人的边界没有被抹成同一种'}</p></div>
1330
+ <button class="ink-button story-continue" data-joint-continue="1">${last ? '把这桩合办记进总账' : choice.beat === 0 ? `听${HEROINES[choice.participants[1]].short}怎么接` : '看两种法子怎样扣在一起'}</button>
1331
+ </div>
1332
+ </div>`;
1333
+ }
1334
+
1335
+ function renderPortablePrecedent() {
1336
+ const story = E.currentPortablePrecedent(state);
1337
+ if (!story?.current) return '<div class="fatal-card">第二张契没有接上第一桩联院差事。</div>';
1338
+ const activeReply = story.stage === 'reply' ? story.replies.find((row) => row.heroine === story.reply.heroine) : null;
1339
+ const cast = story.participants.map((id) => {
1340
+ const person = HEROINES[id];
1341
+ const reply = story.replies.find((row) => row.heroine === id);
1342
+ const label = ({stand:'守原规',narrow:'收窄',withdraw:'撤回外推'})[reply.outcome];
1343
+ return `<figure class="${activeReply?.heroine === id ? 'speaking' : ''}" data-precedent-heroine="${id}"><img src="${urlFor(person.portrait)}" alt="${person.name}"/><figcaption><i>${person.glyph}</i><b>${person.name}</b><span>${story.beat >= story.participants.indexOf(id) + 1 ? label : '尚未作答'}</span></figcaption></figure>`;
1344
+ }).join('');
1345
+ let content = '';
1346
+ if (story.stage === 'opening') {
1347
+ content = `<section class="precedent-object" aria-label="院外经手人与第二张契"><p><b>${escapeHtml(story.outsider.name)}</b> · ${escapeHtml(story.outsider.role)} · 成年经手人</p><span>他只为自己的交易、证言或工资来援引,不知道院门内的私物和关系史。</span></section><button class="ink-button story-continue" data-portable-precedent-continue="1">让两位原经手人分别答</button>`;
1348
+ } else if (story.stage === 'reply') {
1349
+ content = `<blockquote class="precedent-reply"><span>${HEROINES[activeReply.heroine].name}自主作答 · ${escapeHtml(story.response.title)}</span>${escapeHtml(story.response.line)}</blockquote><ul class="precedent-reasons" aria-label="她这样回答的真实前史">${activeReply.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ul><button class="ink-button story-continue" data-portable-precedent-continue="1">${story.beat === 1 ? `听${HEROINES[story.participants[1]].short}独立作答` : '两份答复都不许由你改口'}</button>`;
1350
+ } else if (story.stage === 'decision') {
1351
+ content = `<section class="precedent-boundary" aria-label="两位原经手人的授权边界">${story.replies.map((reply) => `<article><b>${HEROINES[reply.heroine].name}</b><span>${({stand:'守原规',narrow:'只放窄界',withdraw:'撤回外推'})[reply.outcome]}</span><p>${escapeHtml(reply.reasons[2])}</p></article>`).join('')}</section><div class="choice-grid">${E.portablePrecedentOptions(state).map((choice) => choiceButton(choice, 'portable-precedent-choice')).join('')}</div>`;
1352
+ } else {
1353
+ content = `<section class="precedent-scope"><span>实际适用范围</span><b>${escapeHtml(story.resolution.scopeLabel)}</b></section><ol class="precedent-steps" aria-label="第二张契实际执行">${(story.resolution.steps ?? []).map((step) => `<li>${escapeHtml(step)}</li>`).join('')}</ol><button class="ink-button story-continue" data-portable-precedent-continue="1">把第二张契的去处写进总账</button>`;
1354
+ }
1355
+ return `<div class="portable-precedent-stage visual-stage stage-${story.stage}" data-portable-precedent="${story.action}" data-portable-precedent-stage="${story.stage}" data-portable-precedent-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor('compound')}')"><div class="precedent-cast" aria-label="两位原经手人">${cast}</div><div class="decision-panel portable-precedent-panel">${phaseHeader(`联院差事的院外回声 · ${narrativeStep(story.beat + 1, story.count)}`, story.current.title, story.current.body)}${content}</div></div>`;
1356
+ }
1357
+
1358
+ function renderHousehold() {
1359
+ const event = E.currentHouseholdEvent(state);
1360
+ const person = HOUSEHOLD[event.actor];
1361
+ const row = state.household[event.actor];
1362
+ return `
1363
+ <div class="household-stage visual-stage household-${event.actor}" data-household-event="${event.id}" data-household-actor="${event.actor}" style="--scene-bg:url('${urlFor(person.portrait)}')">
1364
+ <div class="decision-panel household-panel">
1365
+ ${phaseHeader(`${person.house} · ${person.glyph}`, event.title, event.text)}
1366
+ <p class="household-voice">${person.voice}</p>
1367
+ ${event.memory ? `<blockquote class="household-memory"><span>她把上一笔原样拿回来</span>${escapeHtml(event.memory)}</blockquote>` : ''}
1368
+ ${event.earlyDayEcho ? `<aside class="council-external-echo household-early-day-echo" data-household-early-day="${event.earlyDayEcho.sourceAction}"><span>第三日实物把娇儿带进这笔报价</span><b>${escapeHtml(event.earlyDayEcho.label)}</b><p>${escapeHtml(event.earlyDayEcho.object)}</p><small>她只认自己能认的来路;这件物没有自动变成免费口供。</small></aside>` : ''}
1369
+ ${event.stoveEcho ? `<aside class="council-external-echo household-stove-echo" data-household-stove-echo="${event.stoveEcho.sourceAction}"><span>第八日停灶留下的生活证物把旧箱带到桌前</span><b>${escapeHtml(event.stoveEcho.label)}</b><p>${escapeHtml(event.stoveEcho.object)}</p><small>它只证明一段工钱、寄存或亲见;原契、抄本与押名仍由娇儿在这笔交易里处分。</small></aside>` : ''}
1370
+ ${event.emergencyEcho ? `<aside class="council-external-echo household-emergency-echo" data-household-emergency-echo="${event.emergencyEcho.sourceAction}"><span>第十三日归名方式限定这张票能补什么</span><b>${escapeHtml(event.emergencyEcho.label)}</b><p>${escapeHtml(event.emergencyEcho.object)}</p><small>这件纸物只限定责任与用途;银票成交、拒兑及二次处分仍等娇儿亲手落字。</small></aside>` : ''}
1371
+ ${event.vaultEcho ? `<aside class="council-external-echo household-vault-echo" data-household-vault-echo="${event.vaultEcho.sourceAction}"><span>第十七日夜查决定她能验真的哪一程</span><b>${escapeHtml(event.vaultEcho.label)}</b><p>${escapeHtml(event.vaultEcho.object)}</p><small>这件实物只能限定一段来路;完整站名、证言与席位仍由娇儿此刻处分。</small></aside>` : ''}
1372
+ ${event.transactionPreparation ? `<aside class="council-external-echo household-transaction-preparation" data-household-preparation="${event.transactionPreparation.sourceAction}"><span>今日白日只把交易条件备齐</span><b>${escapeHtml(event.transactionPreparation.title)}</b><p>${escapeHtml(event.transactionPreparation.object)}</p><small>${escapeHtml(event.transactionPreparation.sourceActors.map((id) => HEROINES[id].short).join('、'))}完成“${escapeHtml(event.transactionPreparation.label)}”;具名底价、授权抄本或原件转移仍等娇儿此刻决定。</small></aside>` : ''}
1373
+ ${(event.earlyLedgers ?? []).map((echo) => `<aside class="council-external-echo household-early-ledger" data-household-early-ledger="${escapeHtml(echo.factId)}"><span>早先纸物仍按原位回读</span><b>${escapeHtml(echo.title)}</b><p>${escapeHtml(echo.text)}</p><small>${escapeHtml(echo.permission)}</small></aside>`).join('')}
1374
+ ${event.councilRule ? `<aside class="council-external-echo household-council-rule" data-household-council-rule="${event.councilRule.sourceChoice}"><span>${state.day === 14 ? '第十二日的三钥规矩正在限定这张票' : '第十七日的追问规矩正在限定这笔交易'}</span><b>${escapeHtml(event.councilRule.title)}</b><p>${escapeHtml(event.councilRule.text)}</p><small>${event.councilRule.preparation ? `今日先由${escapeHtml(HEROINES[event.councilRule.preparation.actor].short)}完成“${escapeHtml(event.councilRule.preparation.label)}”,并未提前成交。` : '这项权限只从真实院议历史派生。'}</small></aside>` : ''}
1375
+ ${event.nightLedger ? `<aside class="council-external-echo household-night-ledger" data-night-ledger-day18="${event.nightLedger.choice}" data-day5-opening-long="${event.nightLedger.publicOpeningChoice}"><span>第七夜留下的不是长期同意</span><b>${escapeHtml(event.nightLedger.title)}</b><p>${escapeHtml(event.nightLedger.text)}</p><small>${escapeHtml(event.nightLedger.permission)} · 第五日仍按“${escapeHtml(event.nightLedger.publicOpeningLabel)}”划分责任。</small></aside>` : ''}
1376
+ <div class="household-standing">她如今${E.householdTier(row.regard)} · ${escapeHtml(event.ledger.label)}${event.ledger.outstanding ? ` · 未清 ${event.ledger.outstanding} 两` : ''}。</div>
1377
+ <div class="choice-grid">${E.householdOptions(state).map((choice) => choiceButton(choice, 'household')).join('')}</div>
1378
+ </div>
1379
+ </div>`;
1380
+ }
1381
+
1382
+ function renderHouseholdAftermath() {
1383
+ const story = E.currentHouseholdAftermath(state);
1384
+ const beat = story?.current;
1385
+ if (!story || !beat) return '<div class="fatal-card">娇儿这笔交易没有写到下一页。</div>';
1386
+ const castIds = ['li_jiaoer', ...story.witnesses];
1387
+ const castPerson = (id) => id === 'li_jiaoer' ? HOUSEHOLD.li_jiaoer : HEROINES[id];
1388
+ const ledger = E.jiaoerLedger(state);
1389
+ const action = story.awaitingChoice
1390
+ ? `<div class="choice-grid jiaoer-aftermath-choices">${E.householdAftermathOptions(state).map((choice) => choiceButton(choice, 'household-aftermath')).join('')}</div>`
1391
+ : `<button class="ink-button story-continue" data-household-aftermath-continue="1">${story.beat === 3 ? '把这笔写进跨幕总账' : story.beat === 0 ? '让五院见证人看这笔价' : '听她们把隐含代价问完'}</button>`;
1392
+ return `
1393
+ <div class="jiaoer-aftermath-stage visual-stage jiaoer-aftermath-beat-${story.beat + 1}" data-household-aftermath="${story.approach}" data-household-aftermath-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor(HOUSEHOLD.li_jiaoer.portrait)}')">
1394
+ <div class="jiaoer-aftermath-cast" aria-label="交易现场人物">
1395
+ ${castIds.map((id) => {
1396
+ const person = castPerson(id);
1397
+ return `<figure class="${story.speaker === id ? 'speaking' : ''}" data-jiaoer-cast="${id}"><img src="${urlFor(person.portrait)}" alt="${person.name}"/><figcaption><i>${person.glyph}</i>${person.name}</figcaption></figure>`;
1398
+ }).join('')}
1399
+ </div>
1400
+ <div class="decision-panel jiaoer-aftermath-panel">
1401
+ ${phaseHeader(`西厢跨幕交易 · ${narrativeStep(story.beat + 1, story.count)}`, beat.title, beat.body)}
1402
+ ${story.publicOpeningLongEcho ? `<aside class="council-external-echo household-day5-opening-long" data-household-day5-opening-long="${story.publicOpeningLongEcho.choice}:${story.beat + 1}"><span>第五日责任时序仍限制这一拍</span><b>${escapeHtml(story.publicOpeningLongEcho.label)}</b><p>${escapeHtml(story.publicOpeningLongEcho.text)}</p></aside>` : ''}
1403
+ <div class="jiaoer-aftermath-ledger" aria-label="娇儿交易总账">
1404
+ <span><small>本次初选</small><b>${escapeHtml(story.openingLabel ?? HOUSEHOLD_EVENTS[state.day].choices.find((choice) => choice.id === story.approach)?.label ?? '已落价')}</b></span>
1405
+ <span><small>旧款</small><b>${ledger.outstanding ? `${ledger.outstanding} 两未清` : '已分栏处理'}</b></span>
1406
+ <span><small>当前结法</small><b>${escapeHtml(ledger.label)}</b></span>
1407
+ </div>
1408
+ <p class="jiaoer-aftermath-speaker">${story.speaker === 'li_jiaoer' ? '娇儿守着价格边界' : `${HEROINES[story.speaker]?.name ?? '五院见证人'}正在追问这笔交易`}</p>
1409
+ ${action}
1410
+ </div>
1411
+ </div>`;
1412
+ }
1413
+
1414
+ function renderCouncil() {
1415
+ const event = E.currentCouncilEvent(state);
1416
+ if (!event) return '<div class="fatal-card">院议这页断了。</div>';
1417
+ return `
1418
+ <div class="council-stage visual-stage" data-council-event="${event.id}" style="--scene-bg:url('${urlFor('compound')}')">
1419
+ <div class="decision-panel council-panel">
1420
+ ${phaseHeader(`第 ${state.day} 日 · ${event.title}`, event.heading, event.body)}
1421
+ ${event.day5Opening ? `<aside class="council-external-echo day5-public-opening" data-council-day5-opening="${event.day5Opening.choice}"><span>${event.day5Opening.choice === 'public_5_favor' ? '已认偏宠正在限定夜簿' : '逐手经办正在限定夜簿'}</span><b>${escapeHtml(event.day5Opening.label)}</b><p>${escapeHtml(event.day5Opening.day7Text)}</p></aside>` : ''}
1422
+ ${event.day10Opening ? `<aside class="council-external-echo day10-public-opening" data-council-day10-opening="${event.day10Opening.choice}"><span>${event.day10Opening.choice === 'public_10_hide' ? '四份原页与收讫条正在限定分存' : '五份本人原页正在限定分存'}</span><b>${escapeHtml(event.day10Opening.label)}</b><p>${escapeHtml(event.day10Opening.day12Text)}</p></aside>` : ''}
1423
+ ${event.sourceChoice && event.object ? `<aside class="council-external-echo council-night-ledger-source" data-night-ledger-source="${event.sourceChoice}:${event.actChoice}"><span>第五日的描述权与第六日的互见方式一起落到这件物上</span><b>${escapeHtml(event.object)}</b><p>${escapeHtml(PUBLIC_FOLLOWUPS[5].choices.find((choice) => choice.id === event.sourceChoice)?.label ?? event.sourceChoice)} · ${escapeHtml(ACT_TRANSITIONS[6].choices.find((choice) => choice.id === event.actChoice)?.label ?? event.actChoice)}</p></aside>` : ''}
1424
+ ${event.accountEcho ? `<aside class="council-external-echo council-account-echo" data-council-account-echo="${event.accountEcho.sourceAction}"><span>第六日假账处理给这场院议留下一件实物</span><b>${escapeHtml(event.accountEcho.label)}</b><p>${escapeHtml(event.accountEcho.object)}</p><small>它只证明纸、话或食盒怎样走,不替任何人取得总代表权。</small></aside>` : ''}
1425
+ ${event.externalEcho ? `<aside class="council-external-echo" data-external-echo="${event.externalEcho.sourceResult}"><span>昨日三口复案仍在追问这条规矩</span><b>${escapeHtml(event.externalEcho.choice)}</b><p>${escapeHtml(event.externalEcho.text)}</p></aside>` : ''}
1426
+ ${event.publicOpening ? `<aside class="council-external-echo day15-public-opening ${event.publicOpening.falseScapegoat ? 'is-contaminated' : ''}" data-council-day15-opening="${event.publicOpening.choice}"><span>${event.publicOpening.falseScapegoat ? '先押与撤押必须先于问答规则落字' : '空白罪名栏仍先于问答规则'}</span><b>${escapeHtml(event.publicOpening.label)}</b><p>${escapeHtml(event.publicOpening.day17Text)}</p></aside>` : ''}
1427
+ ${event.investigationEcho ? `<aside class="council-external-echo council-investigation-echo" data-investigation-echo="${event.investigationEcho.sourceChoice}:${event.investigationEcho.actChoice}"><span>第十日至第十一日留下的物件没有复原</span><b>${escapeHtml(event.investigationEcho.label)}</b><p>${escapeHtml(event.investigationEcho.text)}</p></aside>` : ''}
1428
+ ${event.saltEcho ? `<aside class="council-external-echo council-salt-echo" data-council-salt-echo="${event.saltEcho.sourceAction}"><span>第十一日盐瓮处理正在限定这场院议</span><b>${escapeHtml(event.saltEcho.label)}</b><p>${escapeHtml(event.saltEcho.object)}</p><small>它能接出一段箱路,不能替任何人补成换箱主谋。</small></aside>` : ''}
1429
+ ${event.crowdEcho ? `<aside class="council-external-echo council-crowd-echo" data-council-crowd-echo="${event.crowdEcho.sourceAction}"><span>第十六日围门处理给问话规矩留下一件原物</span><b>${escapeHtml(event.crowdEcho.label)}</b><p>${escapeHtml(event.crowdEcho.object)}</p><small>它只接出一段柜坊入口;问话权不能借机收走原票、原物或停问权。</small></aside>` : ''}
1430
+ <div class="council-cast" aria-label="参与院议的人">${event.participants.map((id) => `<figure><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption>${HEROINES[id].short}</figcaption></figure>`).join('')}</div>
1431
+ <p class="council-note">这不是替她们判输赢。你定的是:以后她们能怎样问、怎样拒绝、怎样彼此交账。</p>
1432
+ <div class="choice-grid">${E.councilOptions(state).map((choice) => choiceButton(choice, 'council')).join('')}</div>
1433
+ </div>
1434
+ </div>`;
1435
+ }
1436
+
1437
+ function renderCouncilAftermath() {
1438
+ const story = E.currentCouncilAftermath(state);
1439
+ const beat = story?.current;
1440
+ if (!story || !beat) return '<div class="fatal-card">院议裁决没有落到下一页。</div>';
1441
+ const last = story.beat === story.count - 1;
1442
+ return `
1443
+ <div class="council-aftermath-stage visual-stage council-resolution-${state.day}" data-council-aftermath="${story.choice}" data-council-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor('compound')}')">
1444
+ <div class="council-resolution-cast" aria-label="正在试用这条规矩的人">${story.participants.map((id) => `<figure class="${beat.speaker === id ? 'speaking' : 'listening'}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].name}</b></figcaption></figure>`).join('')}</div>
1445
+ <div class="decision-panel council-aftermath-panel">
1446
+ ${phaseHeader(`院议裁决 · ${story.label} · ${narrativeStep(story.beat + 1, story.count)}`, beat.title, beat.body)}
1447
+ ${story.day5Opening ? `<aside class="council-external-echo day5-public-opening" data-council-aftermath-day5-opening="${story.day5Opening.choice}:${story.beat + 1}"><span>${story.day5Opening.choice === 'public_5_favor' ? '已认偏宠仍限制这一拍' : '逐手经办仍限制这一拍'}</span><b>${escapeHtml(story.day5Opening.label)}</b><p>${escapeHtml(story.day5Opening.text)}</p></aside>` : ''}
1448
+ ${story.day10Opening ? `<aside class="council-external-echo day10-public-opening" data-council-aftermath-day10-opening="${story.day10Opening.choice}:${story.beat + 1}"><span>${story.day10Opening.choice === 'public_10_hide' ? '缺页仍不能被分钥复活' : '五份本人原页仍各守开封权'}</span><b>${escapeHtml(story.day10Opening.label)}</b><p>${escapeHtml(story.day10Opening.text)}</p></aside>` : ''}
1449
+ ${story.day15Opening ? `<aside class="council-external-echo day15-public-opening ${story.day15Opening.falseScapegoat ? 'is-contaminated' : ''}" data-council-aftermath-day15-opening="${story.day15Opening.choice}:${story.beat + 1}"><span>${story.day15Opening.falseScapegoat ? '错误归罪与撤回仍限制这一拍' : '没有预填犯人仍限制这一拍'}</span><b>${escapeHtml(story.day15Opening.label)}</b><p>${escapeHtml(story.day15Opening.text)}</p></aside>` : ''}
1450
+ <div class="council-rule-slip"><span>${story.beat === 0 ? '先说到明处' : story.beat === 1 ? '让受影响的人试用' : '写成能被追问的规矩'}</span><b>${last ? '这条规矩从今夜开始进入宅门日常;后面的选择会读取它,而不是只留一个旗标。' : '任何人都可以在下一拍指出它真正伤到谁,规矩还没有定稿。'}</b></div>
1451
+ <button class="ink-button story-continue" data-council-aftermath-continue="1">${last ? '让这条规矩真正生效' : story.beat === 0 ? '听受影响的人怎么说' : '看她们怎样把边界写清'}</button>
1452
+ </div>
1453
+ </div>`;
1454
+ }
1455
+
1456
+ function renderVisitHub() {
1457
+ const status = E.sharedNightStatus(state);
1458
+ const alliance = E.allianceNightStatus(state);
1459
+ const accordRows = E.accordStatus(state);
1460
+ return `
1461
+ <div class="hub-stage visual-stage evening ${status.visible ? 'has-shared' : ''}" style="--scene-bg:url('${urlFor('compound')}')">
1462
+ <div class="visit-prompt">
1463
+ <div>${phaseHeader(`第 ${state.day} 日 · 黄昏`, '院门亮了灯', TEXT.chooseVisit)}</div>
1464
+ <aside class="accord-panel" aria-label="五院共约">
1465
+ <div class="accord-heading"><b>想让五盏灯一起亮</b><span>${narrativeProgress(status.complete, status.total, ['边界尚未落字', '边界正在逐院落字', '五院边界已齐'])} · ${narrativeProgress(status.covenantComplete, status.covenantTotal, ['尚未共同担事', '共担已有根基', '共担已经坐实'])} · ${narrativeProgress(status.networkComplete, status.networkTotal, ['院间仍彼此隔着', '院间开始互信', '院间互信已成网'])} · ${narrativeProgress(status.pressureComplete, status.pressureTotal, ['危局仍压着宅门', '多桩危局已经收住', '宅门经住了最后一问'])}</span></div>
1466
+ <div class="accord-seals">${accordRows.map((row) => `<span class="accord-seal ${row.complete ? 'complete' : ''}" data-accord="${row.key}"><i>${row.glyph}</i>${row.label}</span>`).join('')}</div>
1467
+ ${status.visible ? `<button class="shared-invite" data-shared-start="1" ${status.ready ? '' : 'disabled'}><b>请五个人都别走</b><span>${status.ready ? '五份边界、五份亲历、五桩合办事和三场公开口径都在,这回她们愿意一起留下。' : status.reason}</span></button>` : '<small>先听清五个人各自要什么,再让她们真的一起做成五件事。</small>'}
1468
+ ${alliance.visible ? `<button class="shared-invite alliance-invite" data-alliance-start="1" ${alliance.ready ? '' : 'disabled'}><b>${alliance.ready ? `逐院问${alliance.candidates.map((id) => HEROINES[id].short).join('、')}是否留下` : '逐院问灯'}</b><span>${alliance.ready ? '第十九夜真正入席的互证者会分别再答;你不能删名、排序或劝改。' : alliance.reason}</span></button>` : ''}
1469
+ </aside>
1470
+ </div>
1471
+ <div class="heroine-doors">
1472
+ ${HEROINE_IDS.map((id) => {
1473
+ const h = HEROINES[id];
1474
+ const r = state.relations[id];
1475
+ const knock = (state.visits?.[id] ?? 0) + 1;
1476
+ const complete = E.routeComplete(state, id);
1477
+ const cooling = E.routeCooling(state, id);
1478
+ const forecast = E.visitForecast(state, id);
1479
+ const observer = forecast.observer ? HEROINES[forecast.observer] : null;
1480
+ const jealous = forecast.mostJealous ? HEROINES[forecast.mostJealous.id] : null;
1481
+ const forecastRows = !complete && !cooling ? `
1482
+ <dl class="door-forecast">
1483
+ <div><dt>同场</dt><dd title="${escapeHtml(forecast.trustGoal)}">${observer ? `${observer.short} · ${E.bondTier(forecast.observerTrust)}` : '本院独章'}</dd></div>
1484
+ <div><dt>明早</dt><dd>${jealous ? `${jealous.short}会来追问这份偏爱` : '暂无旁院追问'}</dd></div>
1485
+ <div><dt>可走</dt><dd>${forecast.choiceKinds.length ? forecast.choiceKinds.join('/') : '等待前置条件'}</dd></div>
1486
+ </dl>
1487
+ ${forecast.obligation ? `<i class="door-due">${forecast.obligation.label} · ${forecast.obligation.statusLabel}</i>` : ''}` : '';
1488
+ return `<button class="door-card door-${id} ${complete ? 'complete' : ''} ${cooling ? 'cooling' : ''}" data-visit="${id}" data-visit-observer="${forecast.observer ?? ''}" ${complete || cooling ? 'disabled' : ''}><span>${h.house}</span><img src="${urlFor(h.portrait)}" alt="${h.name}"/><div><b>${complete ? `${h.short}这条路已有结果` : cooling ? `${h.short}今日没有开门` : `去${h.short}屋里`}</b><small>${cooling ? '失信还在门内,今夜不能用一句软话抹掉。' : h.want}</small><em>情 ${E.relationTier(r.qing, 'qing')} · 妒 ${E.relationTier(r.du, 'du')}</em><i class="door-knock">${complete ? '不再重复消耗这一条线' : cooling ? `到第 ${forecast.reopenDay} 日再来把话说清` : `${knock <= 1 ? '初次叩门' : knock <= 3 ? '旧话续上' : '深线将定'} · ${forecast.chapter}`}</i>${forecastRows}</div></button>`;
1489
+ }).join('')}
1490
+ </div>
1491
+ </div>`;
1492
+ }
1493
+
1494
+ function renderPersonalFinale() {
1495
+ const beat = E.personalFinaleBeat(state);
1496
+ if (!beat) return '<div class="fatal-card">个人终章这页没有接上。</div>';
1497
+ const heroine = HEROINES[beat.heroine];
1498
+ const others = HEROINE_IDS.filter((id) => id !== beat.heroine);
1499
+ return `
1500
+ <div class="personal-finale-stage visual-stage dialogue-${beat.heroine}" data-personal-finale="${beat.finale}" data-personal-finale-beat="${beat.id}">
1501
+ <div class="personal-finale-main" style="background-image:url('${urlFor(beat.asset)}')" role="img" aria-label="${heroine.name}在第二十夜处理最后的边界与善后"></div>
1502
+ <div class="personal-finale-others" aria-label="另外四院仍有各自的去处与权利">
1503
+ ${others.map((id) => `<figure><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption>${HEROINES[id].short}<small>${HEROINES[id].house}</small></figcaption></figure>`).join('')}
1504
+ </div>
1505
+ <div class="decision-panel personal-finale-panel">
1506
+ ${phaseHeader(`${beat.kicker} · ${heroine.house}`, beat.title, beat.body)}
1507
+ <blockquote class="personal-finale-voice"><b>${heroine.name}</b><p>${escapeHtml(beat.heroineLine)}</p></blockquote>
1508
+ ${beat.previousText ? `<blockquote class="personal-finale-last"><span>上一拍已经发生</span>${escapeHtml(beat.previousText)}</blockquote>` : ''}
1509
+ <p class="shared-proof">个人终章 · ${narrativeStep(beat.index + 1, 3)} · 选择她,不替另外四个人决定输赢</p>
1510
+ <div class="choice-grid">${E.personalFinaleOptions(state).map((choice) => choiceButton(choice, 'personal-finale')).join('')}</div>
1511
+ </div>
1512
+ </div>`;
1513
+ }
1514
+
1515
+ function renderPersonalFinaleResult() {
1516
+ const result = E.currentPersonalFinaleResult(state);
1517
+ if (!result) return '<div class="fatal-card">这项终章回答没有接到她的回应。</div>';
1518
+ const heroine = HEROINES[result.heroine];
1519
+ const respondent = HEROINES[result.respondent];
1520
+ const others = HEROINE_IDS.filter((id) => id !== result.heroine);
1521
+ const agreements = !result.departure && result.arrangements.length
1522
+ ? `<section class="personal-finale-agreements"><header><b>以前说过的话也来到终章</b><span>${result.arrangements.length > 1 ? '旧约都在执行' : '旧约仍在执行'}</span></header>${result.arrangements.map((row) => `<div data-finale-agreement="${row.id}"><small>${row.tier === 'explicit' ? '留宿旧约' : '前奏旧约'} · 第 ${row.day} 夜</small><b>${escapeHtml(row.label)}</b><p>${escapeHtml(row.future)}</p></div>`).join('')}</section>`
1523
+ : !result.departure
1524
+ ? '<p class="personal-finale-no-agreement">此前没有共同处理过亲密之后的生活;这一夜不会替空白编造旧约。</p>'
1525
+ : '';
1526
+ const outcomeLabel = result.outcome ? ({ accept:'接下程序', amend:'亲手改约', refuse:'自行收回' })[result.outcome] : null;
1527
+ const selectedReasonLedger = !result.departure && result.reasons.length
1528
+ ? `<section class="personal-departure-reasons personal-selected-reasons" aria-label="${heroine.name}接住这一答的真实历史依据"><header><b>她为何接住这一答</b><span>从二十日旧事来</span></header><ol>${result.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ol></section>`
1529
+ : '';
1530
+ const reasonLedger = result.departure
1531
+ ? `<section class="personal-departure-reasons" aria-label="${respondent.name}作出这个决定的真实历史依据"><header><b>为何这样答</b><span>不由好感代答</span></header><ol>${result.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ol></section>`
1532
+ : `${selectedReasonLedger}${agreements}`;
1533
+ const kicker = result.departure
1534
+ ? `第二十夜 · 四院善后 · ${narrativeStep(result.departureIndex + 1, result.departureCount)} · ${respondent.house}`
1535
+ : `第二十夜 · ${heroine.house} · 回应${narrativeStep(result.index + 1, result.count)}`;
1536
+ const body = result.departure
1537
+ ? `${heroine.name}的第三答提出“${result.procedure.label}”:${result.procedure.summary}${respondent.name}现在只就${result.procedure.focus}处分自己的物件与权利。`
1538
+ : result.choice.text;
1539
+ const continueLabel = result.departure
1540
+ ? result.final ? '听完四院,让专情结局落下' : '听下一院自己作答'
1541
+ : result.index === result.count - 1 ? '让另外四院自己回应善后' : '听她继续问下一件事';
1542
+ return `
1543
+ <div class="personal-finale-result-stage visual-stage dialogue-${result.respondent}${result.departure ? ' personal-departure-stage' : ''}" data-personal-finale-result="${result.choice.id}" data-result-index="${result.index}"${result.departure ? ` data-personal-departure="${result.respondent}" data-departure-outcome="${result.outcome}"` : ''}>
1544
+ <div class="personal-finale-main personal-finale-result-main" style="background-image:url('${urlFor(result.asset)}')" ${result.departure ? 'aria-hidden="true"' : `role="img" aria-label="${heroine.name}听完终章回答后的反应"`}></div>
1545
+ <div class="personal-finale-others result-witnesses" aria-label="另外四院依次处分自己的物件与权利">
1546
+ ${others.map((id) => `<figure class="${result.departure && id === result.respondent ? 'is-responding' : ''}" data-departure-heroine="${id}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption>${HEROINES[id].short}<small>${HEROINES[id].house}</small></figcaption></figure>`).join('')}
1547
+ </div>
1548
+ <div class="decision-panel personal-finale-panel personal-finale-result-panel">
1549
+ ${phaseHeader(kicker, result.response.title, body)}
1550
+ <blockquote class="personal-finale-response"><b>${respondent.name}</b><p>${escapeHtml(result.response.line)}</p></blockquote>
1551
+ ${reasonLedger}
1552
+ <div class="personal-finale-result-ledger"><span>${result.departure ? '本人决定' : '这一答'}</span><b>${escapeHtml(outcomeLabel ?? result.choice.label)}</b><em>${result.departure ? `她的回应走到${narrativeStep(result.departureIndex + 1, result.departureCount)} · 被选者不得代答` : `${result.choice.style === 'open' ? '明账相守' : '私门相守'} · 回应已走到${narrativeStep(result.index + 1, result.count)}`}</em></div>
1553
+ <button class="ink-button story-continue personal-finale-result-continue" data-personal-finale-result-continue="1">${continueLabel}</button>
1554
+ </div>
1555
+ </div>`;
1556
+ }
1557
+
1558
+ function renderAllianceAssembly() {
1559
+ const story = E.currentAllianceAssembly(state);
1560
+ if (!story) return '<div class="fatal-card">逐院问灯没有接上第十九夜的有限互证。</div>';
1561
+ const outcomeLabel = { join:'本人留下', amend:'收窄后留下', withdraw:'自行回院' };
1562
+ const currentHeroine = story.reply ? HEROINES[story.reply.heroine] : null;
1563
+ const roster = story.replies.map((reply) => {
1564
+ const heroine = HEROINES[reply.heroine];
1565
+ const voice = ALLIANCE_ASSEMBLY_RESPONSES[reply.heroine][reply.outcome];
1566
+ const revealed = story.beat > story.candidates.indexOf(reply.heroine);
1567
+ return `<figure class="${story.reply?.heroine === reply.heroine ? 'is-responding' : ''} ${revealed || story.final ? `outcome-${reply.outcome}` : 'is-pending'}" data-alliance-candidate="${reply.heroine}"${revealed || story.final ? ` data-alliance-outcome="${reply.outcome}"` : ''}>
1568
+ <img src="${urlFor(heroine.close)}" alt="${heroine.name}"/>
1569
+ <figcaption><span>${heroine.house}</span><b>${heroine.name}</b><small>${revealed || story.final ? outcomeLabel[reply.outcome] : '本人尚未开口'}</small>${story.final ? `<em>${escapeHtml(voice.object)}</em>` : ''}</figcaption>
1570
+ </figure>`;
1571
+ }).join('');
1572
+ const replyBlock = story.reply && story.response ? `
1573
+ <blockquote class="alliance-assembly-answer" aria-live="polite">
1574
+ <span>${escapeHtml(story.response.object)}</span>
1575
+ <b>${currentHeroine.name} · ${escapeHtml(outcomeLabel[story.reply.outcome])}</b>
1576
+ <p>${escapeHtml(story.response.line)}</p>
1577
+ <em>${escapeHtml(story.response.action)}</em>
1578
+ </blockquote>
1579
+ <div class="alliance-assembly-reasons" aria-label="她从真实前史作答的依据">${story.reasons.map((reason) => `<p>${escapeHtml(reason)}</p>`).join('')}</div>` : '';
1580
+ const summary = story.final ? `<div class="alliance-assembly-summary" aria-label="逐院问灯结果">
1581
+ ${story.replies.map((reply) => {
1582
+ const voice = ALLIANCE_ASSEMBLY_RESPONSES[reply.heroine][reply.outcome];
1583
+ return `<p data-assembly-summary="${reply.heroine}:${reply.outcome}"><b>${HEROINES[reply.heroine].short} · ${outcomeLabel[reply.outcome]}</b><span>${escapeHtml(voice.action)}</span><em>${escapeHtml(voice.object)}</em></p>`;
1584
+ }).join('')}
1585
+ </div>` : '';
1586
+ const button = story.final
1587
+ ? story.members.length >= 2 ? '进入联盟三问' : '回到五院门'
1588
+ : story.stage === 'opening' ? '听第一院本人作答' : story.beat === story.candidates.length ? '看真实落席名单' : '听下一院本人作答';
1589
+ return `
1590
+ <div class="alliance-assembly-stage visual-stage" data-alliance-assembly-beat="${story.beat + 1}" data-alliance-assembly-stage="${story.stage}">
1591
+ <div class="alliance-assembly-cast alliance-count-${story.candidates.length}" aria-label="第十九夜有限互证者逐院作答">${roster}</div>
1592
+ <div class="decision-panel alliance-assembly-panel">
1593
+ ${phaseHeader(`第二十夜 · 逐院问灯 · ${narrativeStep(story.beat + 1, story.count)}`, story.current.title, story.current.body)}
1594
+ ${replyBlock}
1595
+ ${summary}
1596
+ <p class="shared-proof">候选来自第十九夜真实有限互证;答复冻结在邀请发出前,不读取前一人的回答,也不按好感排序。</p>
1597
+ <button class="ink-button story-continue" data-alliance-assembly-continue="1">${button}</button>
1598
+ </div>
1599
+ </div>`;
1600
+ }
1601
+
1602
+ function renderAllianceNight() {
1603
+ const beat = E.allianceNightBeat(state);
1604
+ if (!beat) return '<div class="fatal-card">联盟终章这页没有接上。</div>';
1605
+ const names = beat.members.map((id) => HEROINES[id].short).join('、');
1606
+ return `
1607
+ <div class="alliance-night-stage visual-stage alliance-size-${beat.members.length}" data-alliance-beat="${beat.id}">
1608
+ <div class="alliance-cast" aria-label="${names}正在共同商定联盟">
1609
+ ${beat.members.map((id) => {
1610
+ const heroine = HEROINES[id];
1611
+ const line = ALLIANCE_VOICES[id]?.[beat.voice] ?? '';
1612
+ return `<figure><img src="${urlFor(heroine.close)}" alt="${heroine.name}"/><figcaption><span>${heroine.house}</span><b>${heroine.name}</b><p>${escapeHtml(line)}</p></figcaption></figure>`;
1613
+ }).join('')}
1614
+ </div>
1615
+ <div class="decision-panel alliance-night-panel">
1616
+ ${phaseHeader(`${beat.kicker} · ${names}`, beat.title, beat.body)}
1617
+ <div class="alliance-voices-inline">${beat.members.map((id) => `<p><b>${HEROINES[id].short}</b>${escapeHtml(ALLIANCE_VOICES[id]?.[beat.voice] ?? '')}</p>`).join('')}</div>
1618
+ ${beat.previousText ? `<blockquote class="alliance-last-result"><span>上一句话已经落地</span>${escapeHtml(beat.previousText)}</blockquote>` : ''}
1619
+ ${beat.assembly ? `<div class="alliance-assembly-chain"><b>第十九夜互证 → 第二十夜本人答复</b>${beat.assembly.replies.map((reply) => {
1620
+ const voice = ALLIANCE_ASSEMBLY_RESPONSES[reply.heroine][reply.outcome];
1621
+ const label = ({join:'留下',amend:'改约后留下',withdraw:'自行回院'})[reply.outcome];
1622
+ return `<span data-alliance-chain="${reply.heroine}:${reply.outcome}">${HEROINES[reply.heroine].short} · ${label}<em>${escapeHtml(voice.object)}</em></span>`;
1623
+ }).join('')}</div>` : ''}
1624
+ <p class="shared-proof">联盟终章 · ${narrativeStep(beat.index + 1, 3)} · ${beat.members.length === 4 ? '四人全部由本人落席,任何冲突都必须具名处理' : beat.members.length === 3 ? '三个人的关系不由一条强边替另一人作答' : '两个人不是共享同一位恋人的陌生人'}</p>
1625
+ <div class="choice-grid">${E.allianceNightOptions(state).map((choice) => choiceButton(choice, 'alliance-night')).join('')}</div>
1626
+ </div>
1627
+ </div>`;
1628
+ }
1629
+
1630
+ function renderAllianceNightResult() {
1631
+ const result = E.currentAllianceNightResult(state);
1632
+ if (!result) return '<div class="fatal-card">这项联盟回答没有接到在场人的回应。</div>';
1633
+ const names = result.members.map((id) => HEROINES[id].short).join('、');
1634
+ const memberReasonLedger = `<section class="personal-departure-reasons alliance-member-reasons" aria-label="真实成员各自接住这项联盟安排的依据"><header><b>她为何在这一拍接住同盟</b><span>成员相同,旧事与边界并不相同</span></header><ol>${result.memberReasons.map((member) => `<li data-alliance-member-reasons="${result.choice.id}:${member.heroine}"><b>${escapeHtml(HEROINES[member.heroine].short)}</b><ol>${member.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ol></li>`).join('')}</ol></section>`;
1635
+ return `
1636
+ <div class="alliance-night-stage alliance-night-result-stage visual-stage alliance-size-${result.members.length}" data-alliance-result="${result.choice.id}">
1637
+ <div class="alliance-cast alliance-result-cast" aria-label="${names}逐一回应刚才的安排">
1638
+ ${result.members.map((id) => `<figure><img src="${urlFor(HEROINES[id].close)}" alt="${HEROINES[id].name}"/><figcaption><span>${HEROINES[id].house}</span><b>${HEROINES[id].name}</b><p>${escapeHtml(result.response.lines[id])}</p></figcaption></figure>`).join('')}
1639
+ </div>
1640
+ <div class="decision-panel alliance-night-panel alliance-result-panel">
1641
+ ${phaseHeader(`第二十夜 · ${names} · 回应${narrativeStep(result.index + 1, result.count)}`, result.response.title, result.response.lead)}
1642
+ <div class="alliance-result-voices">${result.members.map((id) => `<blockquote data-alliance-speaker="${id}"><b>${HEROINES[id].short}</b><p>${escapeHtml(result.response.lines[id])}</p></blockquote>`).join('')}</div>
1643
+ ${memberReasonLedger}
1644
+ <div class="alliance-result-ledger"><span>刚才的安排</span><b>${escapeHtml(result.choice.label)}</b><em>${escapeHtml(result.choice.text)}</em></div>
1645
+ <div class="alliance-result-bonds">${result.bonds.map((row) => `<span>${HEROINES[row.left].short} ⇄ ${HEROINES[row.right].short}<b>${E.bondTier(row.trust)}</b></span>`).join('')}</div>
1646
+ <button class="ink-button story-continue alliance-result-continue" data-alliance-result-continue="1">${result.final ? '让这份有限同盟落下' : '听她们继续谈下一件事'}</button>
1647
+ </div>
1648
+ </div>`;
1649
+ }
1650
+
1651
+ function renderSharedNight() {
1652
+ const rows = E.accordStatus(state);
1653
+ const lastPublic = Object.values(PUBLIC_EVENTS).at(-1);
1654
+ const backdrop = SCENES[lastPublic?.scene]?.asset ?? 'compound';
1655
+ return `
1656
+ <div class="shared-stage visual-stage" style="--scene-bg:url('${urlFor(backdrop)}')">
1657
+ <div class="decision-panel shared-panel">
1658
+ ${phaseHeader('第二十夜 · 五人都没走', '茶还热着,五把钥匙各在主人手里', TEXT.sharedNightLead)}
1659
+ <div class="accord-seals shared-seals">${rows.map((row) => `<span class="accord-seal ${row.complete ? 'complete' : ''}" data-accord="${row.key}"><i>${row.glyph}</i>${row.label}</span>`).join('')}</div>
1660
+ <p class="shared-proof">${narrativeProgress(Math.min(E.jointActionCount(state), E.JOINT_ACTION_TARGET), E.JOINT_ACTION_TARGET, ['她们还没有一起做过事', '已有人并肩做完一桩事', '五组搭档都真正共过事'])} · ${narrativeProgress(state.resolvedPressures.length, E.PRESSURE_TARGET, ['门外的事还在逼近', '她们已一起撑过几回', '今夜终于有余地谈自己'])}</p>
1661
+ <div class="choice-grid">${E.sharedNightOptions(state).map((choice) => choiceButton(choice, 'shared-night')).join('')}</div>
1662
+ </div>
1663
+ </div>`;
1664
+ }
1665
+
1666
+ function renderSharedAfterglow() {
1667
+ const beat = E.sharedAfterglowBeat(state);
1668
+ if (!beat) return '<div class="fatal-card">灯下这页断了。</div>';
1669
+ return `
1670
+ <div class="afterglow-stage visual-stage" data-shared-beat="${beat.id}" style="--scene-bg:url('${urlFor('cg/group/inner_court_accord')}')">
1671
+ <div class="decision-panel afterglow-panel">
1672
+ ${phaseHeader(beat.kicker, beat.title, beat.body)}
1673
+ <p class="shared-proof">灯还亮着 · ${narrativeStep(state.sharedAfterglowChoices.length + 1, SHARED_AFTERGLOW_BEATS.length)} · 这一步要等五个人都开口</p>
1674
+ <div class="choice-grid">${E.sharedAfterglowOptions(state).map((choice) => choiceButton(choice, 'shared-afterglow')).join('')}</div>
1675
+ </div>
1676
+ </div>`;
1677
+ }
1678
+
1679
+ function renderSharedAfterglowResult() {
1680
+ const result = E.currentSharedAfterglowResult(state);
1681
+ if (!result) return '<div class="fatal-card">这项余夜安排没有接到五个人的回应。</div>';
1682
+ const reasonLedger = result.memberReasons?.length
1683
+ ? `<section class="personal-departure-reasons shared-finale-reasons" aria-label="五个人各自依据真实前史回应这项共守安排"><header><b>她为何在这一拍接住共守</b><span>同席不等于同一份前史</span></header><ol>${result.memberReasons.map((member) => `<li data-shared-afterglow-reasons="${result.choice.id}:${member.heroine}"><b>${escapeHtml(HEROINES[member.heroine].short)}</b><ol>${member.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ol></li>`).join('')}</ol></section>`
1684
+ : '';
1685
+ return `
1686
+ <div class="shared-afterglow-result-stage visual-stage" data-shared-afterglow-result="${result.choice.id}" style="--scene-bg:url('${urlFor('cg/group/inner_court_accord')}')">
1687
+ <div class="shared-result-cast" aria-label="五个人分别回应这项余夜安排">
1688
+ ${result.members.map((id) => `<figure><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].short}</b><span>${HEROINES[id].house}</span></figcaption></figure>`).join('')}
1689
+ </div>
1690
+ <div class="decision-panel shared-afterglow-result-panel">
1691
+ ${phaseHeader(`第二十夜 · 五院余夜 · 回应${narrativeStep(result.index + 1, result.count)}`, result.response.title, result.response.lead)}
1692
+ <div class="shared-result-voices">${result.members.map((id) => `<blockquote data-shared-speaker="${id}"><b>${HEROINES[id].short}</b><p>${escapeHtml(result.response.lines[id])}</p></blockquote>`).join('')}</div>
1693
+ ${reasonLedger}
1694
+ <div class="shared-result-choice"><span>这一拍</span><b>${escapeHtml(result.choice.label)}</b><em>${escapeHtml(result.choice.text)}</em></div>
1695
+ <button class="ink-button story-continue shared-result-continue" data-shared-afterglow-result-continue="1">${result.final ? '让五个人一起走进余夜' : '听下一件事怎样落地'}</button>
1696
+ </div>
1697
+ </div>`;
1698
+ }
1699
+
1700
+ function renderCollapseFinale() {
1701
+ const story = E.currentCollapseFinale(state);
1702
+ if (!story) return '<div class="fatal-card">破局清算这页没有接上。</div>';
1703
+ const current = story.current;
1704
+ const choicePanel = story.awaitingChoice
1705
+ ? `<div class="collapse-final-question"><span>最后一笔</span><b>圆满已经失去,你还要保住哪一种可执行的东西?</b><p>这项选择不会把坏局翻成好局。它决定天亮以后仍由谁掌握证据、钥匙、退路与追讨权。</p></div><div class="choice-grid collapse-final-choices">${E.collapseFinaleOptions(state).map((choice) => choiceButton(choice, 'collapse-choice')).join('')}</div>`
1706
+ : `<blockquote class="collapse-current-voice" data-collapse-speaker="${current.speaker}"><b>${HEROINES[current.speaker].name}</b><p>${escapeHtml(current.text)}</p></blockquote><button class="ink-button story-continue collapse-continue" data-collapse-continue="1">${story.beat === 0 ? '让下一人把代价接上' : story.beat === 1 ? '看这局还压住了谁' : '亲手决定最后保住什么'}</button>`;
1707
+ return `
1708
+ <div class="collapse-finale-stage visual-stage collapse-cause-${story.cause}" data-collapse-cause="${story.cause}" data-collapse-step="${story.beat + 1}" style="--scene-bg:url('${urlFor('compound')}')">
1709
+ <div class="collapse-cast" aria-label="五个人正在清算这场破局">
1710
+ ${HEROINE_IDS.map((id) => `<figure class="${current?.speaker === id ? 'speaking' : ''}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].short}</b></figcaption></figure>`).join('')}
1711
+ </div>
1712
+ <div class="decision-panel collapse-finale-panel">
1713
+ ${phaseHeader(`${story.kicker} · 清算${narrativeStep(story.beat + 1, story.count)}`, current?.title ?? '最后还能留下什么', current ? story.title : '前面三笔已经说清。现在没有圆满可补,只能决定哪一项权利与事实不再被继续抵押。')}
1714
+ <div class="collapse-cause-ledger"><span>破局来源</span><b>${story.cause === 'shared_buy_quiet' ? '拿银买静' : story.cause === 'shared_false_only' ? '复制唯一承诺' : story.endingId === 'intrigue' ? '权谋代价追上门' : '关系与过程同时失守'}</b><em>${story.endingId === 'intrigue' ? '外账或许赢了,借来的人情仍会要价' : '关系不会因最后一次点击恢复,只能停止继续伤人'}</em></div>
1715
+ ${choicePanel}
1716
+ </div>
1717
+ </div>`;
1718
+ }
1719
+
1720
+ function renderCollapseFinaleResult() {
1721
+ const result = E.currentCollapseFinaleResult(state);
1722
+ if (!result) return '<div class="fatal-card">这项最后选择还没有接到五个人的回应。</div>';
1723
+ const heroine = HEROINES[result.heroine];
1724
+ const memoryLedger = result.memory
1725
+ ? `<section class="personal-departure-reasons collapse-history-reasons collapse-current-memory" data-collapse-memory="${result.memory.choice}:${result.memory.heroine}" aria-label="${escapeHtml(heroine.name)}带到破局清算的真实旧事"><header><b>这件旧事为何追到今夜</b><span>最后取舍不能抹掉她的二十日历史</span></header><div><b>${escapeHtml(heroine.short)} · ${escapeHtml(result.memory.label)}</b><p>${escapeHtml(result.memory.text)}</p><em>${escapeHtml(result.memory.conclusion)}</em></div></section>`
1726
+ : '';
1727
+ const transcript = result.previous.length
1728
+ ? `<ol class="collapse-result-transcript" aria-label="前面已经落下的本人回应">${result.previous.map((row, index) => `<li data-collapse-result-record="${row.heroine}"><span>${narrativeBeatMark(index)}</span><div><b>${escapeHtml(HEROINES[row.heroine].short)}</b><p>${escapeHtml(row.line)}</p></div></li>`).join('')}</ol>`
1729
+ : '';
1730
+ return `
1731
+ <div class="collapse-result-stage visual-stage collapse-cause-${result.cause}" data-collapse-result="${result.choice.id}" data-collapse-result-step="${result.index + 1}" style="--scene-bg:url('${urlFor('compound')}')">
1732
+ <div class="collapse-result-cast" aria-label="五个人分别回应最后留下的这一笔">
1733
+ ${result.members.map((id) => `<figure class="${id === result.heroine ? 'speaking' : result.previous.some((row) => row.heroine === id) ? 'heard' : ''}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].short}</b><span>${id === result.heroine ? '正在处分本人这一笔' : result.previous.some((row) => row.heroine === id) ? '本人回应已落账' : '仍在等候本人开口'}</span></figcaption></figure>`).join('')}
1734
+ </div>
1735
+ <div class="decision-panel collapse-result-panel">
1736
+ ${phaseHeader(`第二十夜 · 破局清算 · 本人回应${narrativeStep(result.index + 1, result.count)}`, `${heroine.name} · ${result.response.title}`, result.index === 0 ? result.response.lead : '前面的回应已经逐份留账。现在只听这一院怎样处置本人的原物、旧事与离开权。')}
1737
+ ${transcript}
1738
+ <blockquote class="collapse-result-current-voice" data-collapse-result-speaker="${result.heroine}"><b>${escapeHtml(heroine.short)}</b><p>${escapeHtml(result.line)}</p></blockquote>
1739
+ ${memoryLedger}
1740
+ <div class="collapse-result-ledger"><span>最后保住</span><b>${escapeHtml(result.choice.label)}</b><em>${escapeHtml(result.choice.text)}</em></div>
1741
+ <button class="ink-button story-continue collapse-result-continue" data-collapse-result-continue="1">${result.index + 1 < result.count ? '听下一人处分本人这一笔' : '带着五份真实后果走到天亮'}</button>
1742
+ </div>
1743
+ </div>`;
1744
+ }
1745
+
1746
+ function renderSharedDawn() {
1747
+ return `
1748
+ <div class="shared-dawn-stage visual-stage" style="--scene-bg:url('${urlFor('cg/group/inner_court_afterglow')}')">
1749
+ <div class="decision-panel shared-dawn-panel">
1750
+ ${phaseHeader('第二十日 · 晨光进了纱帐', '天亮以后,昨夜依然算数', '月娘已披衣理账,金莲把扇子压在枕边,瓶儿重新系好钥匙,玉楼将五人的条款分别收好,雪娥起身收住最后一点炉火。你现在怎样走出这间屋,决定昨夜是一场酒,还是一个开始。')}
1751
+ <div class="choice-grid">${E.sharedDawnOptions(state).map((choice) => choiceButton(choice, 'shared-dawn')).join('')}</div>
1752
+ </div>
1753
+ </div>`;
1754
+ }
1755
+
1756
+ function renderSharedDawnResult() {
1757
+ const result = E.currentSharedDawnResult(state);
1758
+ if (!result) return '<div class="fatal-card">次晨这项安排还没有接到五个人的白日回应。</div>';
1759
+ const reasonLedger = result.memberReasons?.length
1760
+ ? `<section class="personal-departure-reasons shared-finale-reasons" aria-label="五个人分别说明昨夜三拍怎样进入白日"><header><b>昨夜三拍为何能进入白日</b><span>次晨安排仍逐人核验</span></header><ol>${result.memberReasons.map((member) => `<li data-shared-dawn-reasons="${result.choice.id}:${member.heroine}"><b>${escapeHtml(HEROINES[member.heroine].short)}</b><ol>${member.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ol></li>`).join('')}</ol></section>`
1761
+ : '';
1762
+ return `
1763
+ <div class="shared-dawn-result-stage visual-stage" data-shared-dawn-result="${result.choice.id}" style="--scene-bg:url('${urlFor('cg/group/inner_court_afterglow')}')">
1764
+ <div class="shared-dawn-result-cast" aria-label="五个人分别确认昨夜怎样进入白日">
1765
+ ${result.members.map((id) => `<figure><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].short}</b><span>${HEROINES[id].house}</span></figcaption></figure>`).join('')}
1766
+ </div>
1767
+ <div class="decision-panel shared-dawn-result-panel">
1768
+ ${phaseHeader('第二十日 · 晨光已经见人 · 五院回应', result.response.title, result.response.lead)}
1769
+ <div class="shared-dawn-result-voices">${result.members.map((id) => `<blockquote data-shared-dawn-speaker="${id}"><b>${HEROINES[id].short}</b><p>${escapeHtml(result.response.lines[id])}</p></blockquote>`).join('')}</div>
1770
+ ${reasonLedger}
1771
+ <div class="shared-dawn-result-ledger"><span>次晨安排</span><b>${escapeHtml(result.choice.label)}</b><em>${escapeHtml(result.response.future)}</em></div>
1772
+ <button class="ink-button story-continue shared-dawn-result-continue" data-shared-dawn-result-continue="1">让这项白日安排进入五院结局</button>
1773
+ </div>
1774
+ </div>`;
1775
+ }
1776
+
1777
+ function renderVisit() {
1778
+ const id = state.currentHeroine;
1779
+ const h = HEROINES[id];
1780
+ const choices = E.visitChoices(state, id);
1781
+ const stance = E.routeStance(state, id);
1782
+ const branch = E.routeBranchContext(state, id);
1783
+ return `
1784
+ <div class="dialogue-stage visual-stage dialogue-${id}">
1785
+ <div class="close-cg" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${h.name}近景"></div>
1786
+ <div class="decision-panel dialogue-panel">
1787
+ <div class="character-seal" aria-hidden="true"><i>${h.glyph}</i><span>${h.house}</span></div>
1788
+ ${phaseHeader(`第 ${state.day} 夜 · ${h.house}`, h.name, h.voice)}
1789
+ <p class="want-line"><b>她今夜要你回答</b>${h.want}<br/><span>${h.gives}</span><br/><small>${stance.covenant === stance.private ? (stance.covenant ? '你们既谈家中的事,也留过只在门内的话。' : '这是你们第一次认真谈今后怎样相处。') : stance.covenant > stance.private ? '她记得你更常在人前接住她的事。' : '她记得你更常把话留在门内。'}</small></p>
1790
+ ${branch ? `<aside class="route-branch-memory branch-${branch.lane}" data-route-branch="${branch.lane}:${branch.step}"><span>${escapeHtml(branch.label)} · ${branch.lane === 'covenant' ? '她记得你在人前怎样做' : '她记得你在门内怎样说'}</span><b>今夜从这件旧事往下谈</b><p>${escapeHtml(branch.body)}</p></aside>` : ''}
1791
+ ${favorReckoningMemory(id)}
1792
+ ${routeReckoningMemory(id)}
1793
+ ${intimacyMemory(id)}
1794
+ ${nightConversationMemory(id)}
1795
+ ${ordinaryNightMemory(id)}
1796
+ ${duskInvitationMemory(id)}
1797
+ ${rivalryMorningMemory(id)}
1798
+ ${pairInterludeMemory(id)}
1799
+ <div class="choice-stack">${choices.map((choice) => choiceButton(choice, 'route-choice')).join('')}</div>
1800
+ </div>
1801
+ </div>`;
1802
+ }
1803
+
1804
+ function routeContinuityCard(event) {
1805
+ const memory = event.continuityMemory;
1806
+ if (!memory) return '';
1807
+ return `<p class="aftermath-pair-memory aftermath-continuity-memory continuity-${memory.kind}" data-route-continuity="${memory.kind}:${memory.day}"><span>最近一件关系事实进入当前冲突 · ${escapeHtml(memory.label)}</span><b>${escapeHtml(memory.title)}</b><small>${escapeHtml(memory.text)}</small></p>`;
1808
+ }
1809
+
1810
+ function renderRouteAftermath() {
1811
+ const event = E.currentRouteAftermath(state);
1812
+ if (!event) return '<div class="fatal-card">这段夜话没有接上。</div>';
1813
+ ensureNarrativeAsset(event.asset);
1814
+ const heroine = HEROINES[event.heroine];
1815
+ const observer = HEROINES[event.observer];
1816
+ if (event.resolutionBeat) {
1817
+ const speaker = HEROINES[event.resolutionBeat.speaker];
1818
+ const listener = event.resolutionBeat.speaker === event.heroine ? observer : heroine;
1819
+ const previousBeats = event.resolutionBeats.slice(0, event.resolution.beat);
1820
+ const beatCopy = narrativeExcerpt(event.resolutionBeat.text);
1821
+ return `
1822
+ <div class="aftermath-stage aftermath-resolution visual-stage dialogue-${event.resolutionBeat.speaker}" data-aftermath-resolution="${event.event}:${event.resolutionChoice}:${event.resolution.beat}">
1823
+ <div class="aftermath-main" style="background-image:url('${urlFor(event.asset || speaker.close)}')" role="img" aria-label="${speaker.name}正在把你的处置做完"></div>
1824
+ <figure class="aftermath-observer"><img src="${urlFor(listener.portrait)}" alt="${listener.name}"/><figcaption><span>${listener.house}</span><b>${listener.name}</b></figcaption></figure>
1825
+ <div class="decision-panel aftermath-panel aftermath-resolution-panel">
1826
+ ${phaseHeader(`${escapeHtml(event.resolutionChoiceLabel)} · ${narrativeStep(event.resolution.beat + 1, event.resolutionCount)}`, event.resolutionTitle, event.body)}
1827
+ <p class="aftermath-echo"><span>这项处置从哪里来</span><b>${escapeHtml(event.sourceLabel)}</b><small>${escapeHtml(event.sourceText)}</small></p>
1828
+ <aside class="aftermath-route-stake" data-route-stake="${event.act}:${event.resolutionChoice}"><span>这一幕实际付出的后约成本</span><b>${escapeHtml(event.resolutionStake.label)} · ${escapeHtml(narrativeChoiceMeta(event.resolutionStake.resourceText))}</b><p>${escapeHtml(event.resolutionStake.text)}</p></aside>
1829
+ ${previousBeats.length ? `<ol class="route-resolution-transcript" aria-label="这项处置已经发生的动作">${previousBeats.map((beat, index) => `<li><span>${narrativeBeatMark(index)}</span><div><b>${escapeHtml(beat.speakerName)}</b><p>${escapeHtml(beat.text)}</p></div></li>`).join('')}</ol>` : ''}
1830
+ ${narrativeArchive(beatCopy, 'route-beat-archive', `${event.resolutionBeat.speakerName}把这项处置说完`)}
1831
+ <blockquote class="aftermath-story-quote route-resolution-quote"><span>${escapeHtml(event.resolutionBeat.speakerHouse)}</span><b>${escapeHtml(event.resolutionBeat.speakerName)}</b><p>${escapeHtml(beatCopy.preview)}</p></blockquote>
1832
+ <button class="ink-button story-continue aftermath-continue" data-route-story="1">${event.resolution.beat + 1 < event.resolutionCount ? `看${listener.short}怎样接住` : '把这项处置带进今夜'}</button>
1833
+ </div>
1834
+ </div>`;
1835
+ }
1836
+ if (event.storyBeat) {
1837
+ const speaker = HEROINES[event.storyBeat.speaker];
1838
+ const listener = event.storyBeat.speaker === event.heroine ? observer : heroine;
1839
+ const lead = event.beat === 0
1840
+ ? event.body
1841
+ : `${speaker.short}没有让刚才那句话停在漂亮处。她把自己真正会失去的东西摆了出来,等另一人听完。`;
1842
+ const beatCopy = narrativeExcerpt(event.storyBeat.text);
1843
+ return `
1844
+ <div class="aftermath-stage aftermath-story ${event.milestone ? 'milestone-chapter' : ''} visual-stage dialogue-${event.storyBeat.speaker}" data-aftermath="${event.event}" data-story-beat="${event.beat}">
1845
+ <div class="aftermath-main" style="background-image:url('${urlFor(event.asset || speaker.close)}')" role="img" aria-label="${speaker.name}正在说话"></div>
1846
+ <figure class="aftermath-observer"><img src="${urlFor(listener.portrait)}" alt="${listener.name}"/><figcaption><span>${listener.house}</span><b>${listener.name}</b></figcaption></figure>
1847
+ <div class="decision-panel aftermath-panel aftermath-story-panel">
1848
+ ${phaseHeader(event.milestone ? '再次进门的关键章' : `路线小章 · ${narrativeStep(event.act, 4)}`, event.title, lead)}
1849
+ <p class="aftermath-echo"><span>你的上一拍</span><b>${escapeHtml(event.sourceLabel)}</b><small>${escapeHtml(event.sourceHint)}</small></p>
1850
+ ${routeContinuityCard(event)}
1851
+ ${narrativeArchive(beatCopy, 'route-beat-archive', `${event.storyBeat.speakerName}把这一拍说完`)}
1852
+ <blockquote class="aftermath-story-quote"><span>${escapeHtml(event.storyBeat.speakerHouse)}</span><b>${escapeHtml(event.storyBeat.speakerName)}</b><p>${escapeHtml(beatCopy.preview)}</p></blockquote>
1853
+ <button class="ink-button story-continue aftermath-continue" data-route-story="1">${event.beat + 1 < event.beatCount ? '听另一人说完' : '轮到你回应'}</button>
1854
+ </div>
1855
+ </div>`;
1856
+ }
1857
+ return `
1858
+ <div class="aftermath-stage ${event.milestone ? 'milestone-chapter' : ''} visual-stage dialogue-${event.heroine}" data-aftermath="${event.event}">
1859
+ <div class="aftermath-main" style="background-image:url('${urlFor(event.asset || heroine.close)}')" role="img" aria-label="${heroine.name}刚回应完你的选择"></div>
1860
+ <figure class="aftermath-observer"><img src="${urlFor(observer.portrait)}" alt="${observer.name}"/><figcaption><span>${observer.house}</span><b>${observer.name}</b></figcaption></figure>
1861
+ <div class="decision-panel aftermath-panel">
1862
+ ${phaseHeader(event.milestone ? '再次进门的关键章' : `路线小章 · ${narrativeStep(event.act, 4)}`, event.title, event.body)}
1863
+ <p class="aftermath-echo"><span>上一拍</span><b>${escapeHtml(event.sourceLabel)}</b><small>${escapeHtml(event.sourceText)}</small></p>
1864
+ ${routeContinuityCard(event)}
1865
+ <p class="aftermath-question">另一处院门已经听见了。你是把话拿到人前,让她们当面说,还是只护住今夜这扇门?</p>
1866
+ <div class="choice-stack">${E.routeAftermathOptions(state).map((choice) => choiceButton(choice, 'route-aftermath')).join('')}</div>
1867
+ </div>
1868
+ </div>`;
1869
+ }
1870
+
1871
+ function renderNight() {
1872
+ const id = state.currentHeroine;
1873
+ const h = HEROINES[id];
1874
+ const coda = E.currentOrdinaryNightCoda(state);
1875
+ if (coda) {
1876
+ const actionLabel = coda.action === 'leave' ? '停在她愿意的位置' : '把未完的话听到底';
1877
+ const previousBeats = coda.beats.slice(0, coda.beat);
1878
+ return `
1879
+ <div class="night-talk-stage ordinary-night-coda visual-stage dialogue-${id}" data-night-coda="${coda.event}:${coda.beat}">
1880
+ <div class="close-cg closer night-talk-cg" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${h.name}正在把今夜的话说到底"></div>
1881
+ <div class="night-talk-prop" aria-hidden="true"><span>${escapeHtml(coda.prop)}</span></div>
1882
+ <div class="decision-panel night-talk-panel ordinary-night-coda-panel">
1883
+ ${phaseHeader(`第 ${state.day} 夜 · ${actionLabel} · ${narrativeStep(coda.beat + 1, coda.count)}`, coda.current.title, coda.current.body)}
1884
+ ${intimacyMemory(id)}
1885
+ ${previousBeats.length ? `<ol class="night-coda-transcript" aria-label="今夜已经说过的话">${previousBeats.map((beat, index) => `<li><span>${narrativeBeatMark(index)}</span><div><b>${escapeHtml(beat.title)}</b><p>${escapeHtml(beat.body)}</p></div></li>`).join('')}</ol>` : ''}
1886
+ <p class="night-coda-context"><b>${h.name}</b><span>${coda.action === 'leave' ? '门停在她亲口划下的位置。今夜不欠下一步,明日却会照这个停处重新安排。' : '茶还温着,她要把这句话问到能见明日,再让你带出这扇门。'}</span></p>
1887
+ <button class="ink-button story-continue night-talk-continue" data-night-coda-continue="1">${coda.beat + 1 < coda.count ? (coda.beat === 0 ? '让她把话接下去' : '听她说到天亮以后') : '把这句话带出门'}</button>
1888
+ </div>
1889
+ </div>`;
1890
+ }
1891
+ const conversation = E.currentNightConversation(state);
1892
+ if (conversation?.resolution) {
1893
+ const result = conversation.resolution;
1894
+ const previousBeats = result.beats.slice(0, result.beat);
1895
+ return `
1896
+ <div class="night-talk-stage night-talk-result visual-stage dialogue-${id}" data-night-talk-result="${result.id}:${result.beat}">
1897
+ <div class="close-cg closer night-talk-cg" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${h.name}听见你的回答"></div>
1898
+ <div class="night-talk-prop" aria-hidden="true"><span>${escapeHtml(conversation.prop)}</span></div>
1899
+ <div class="decision-panel night-talk-panel night-talk-result-panel">
1900
+ ${phaseHeader(`第 ${state.day} 夜 · 夜谈${narrativeProgress(conversation.chapter, 4, ['刚刚起话', '正在接续', '已成条款'])} · 回答后章${narrativeStep(result.beat + 1, result.count)}`, result.current.title, result.current.body)}
1901
+ ${intimacyMemory(id)}
1902
+ ${result.beat === 0 && conversation.memoryEcho ? `<aside class="night-talk-memory"><span>上一章没有消失</span><p>${escapeHtml(conversation.memoryEcho)}</p></aside>` : ''}
1903
+ <aside class="night-talk-stake" data-night-stake="${result.mode}"><span>天亮以后会留下</span><b>${escapeHtml(result.stake.label)} · ${escapeHtml(narrativeChoiceMeta(result.stake.resourceText))}</b><p>${escapeHtml(result.stake.text)}</p></aside>
1904
+ ${result.beat > 0 ? `<p class="night-result-origin"><span>你刚才回答</span><b>${escapeHtml(result.label)}</b></p>` : ''}
1905
+ ${previousBeats.length ? `<ol class="night-result-transcript" aria-label="这次回答已经发生的后果">${previousBeats.map((beat, index) => `<li><span>${narrativeBeatMark(index)}</span><div><b>${escapeHtml(beat.title)}</b><p>${escapeHtml(beat.body)}</p></div></li>`).join('')}</ol>` : ''}
1906
+ ${result.beat + 1 === result.count ? `<div class="night-talk-continuity continuity-${result.continuity.kind}">
1907
+ <b>${escapeHtml(result.continuity.label)}</b>
1908
+ <p>${escapeHtml(result.continuity.text)}</p>
1909
+ </div>` : ''}
1910
+ ${result.beat + 1 === result.count && result.observerReaction ? `<aside class="night-talk-observer-reaction" data-night-observer="${result.observerReaction.heroine}"><span>天亮后真正被这项做法影响的人</span><b>${escapeHtml(result.observerReaction.name)}</b><p>${escapeHtml(result.observerReaction.line)}</p></aside>` : ''}
1911
+ ${result.beat === 0 ? `<p class="night-talk-consequence">${{
1912
+ honest: '实话见光 · 情意更深 · 妒意下降',
1913
+ listen: '边界由她决定 · 宅门与喘息都更稳',
1914
+ private: '门内亲近升温 · 旁院会记得这份私情',
1915
+ }[result.mode]}</p>` : ''}
1916
+ <button class="ink-button story-continue night-talk-continue" data-night-conversation-result="1">${result.beat === 0 ? '看她怎样试这句话' : result.beat + 1 < result.count ? '把明早的做法说清' : '让这三幅话一起到天亮'}</button>
1917
+ </div>
1918
+ </div>`;
1919
+ }
1920
+ if (conversation) {
1921
+ return `
1922
+ <div class="night-talk-stage visual-stage dialogue-${id}" data-night-talk="${conversation.id}:${conversation.beat}">
1923
+ <div class="close-cg closer night-talk-cg" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${h.name}正在说一段只属于今夜的话"></div>
1924
+ <div class="night-talk-prop" aria-hidden="true"><span>${escapeHtml(conversation.prop)}</span></div>
1925
+ <div class="decision-panel night-talk-panel">
1926
+ ${phaseHeader(`第 ${state.day} 夜 · 夜谈${narrativeProgress(conversation.chapter, 4, ['刚刚起话', '正在接续', '已成条款'])} · ${narrativeStep(conversation.beat + 1, 2)}`, conversation.title, conversation.context)}
1927
+ ${intimacyMemory(id)}
1928
+ ${conversation.memoryEcho ? `<aside class="night-talk-memory"><span>她记得上一章</span><p>${escapeHtml(conversation.memoryEcho)}</p></aside>` : ''}
1929
+ <blockquote class="night-talk-voice"><span>${h.house}</span><b>${h.name}</b><p>${escapeHtml(conversation.storyText)}</p></blockquote>
1930
+ ${conversation.beat === 0
1931
+ ? '<button class="ink-button story-continue night-talk-continue" data-night-story="1">让她把真正的问题说完</button>'
1932
+ : `<p class="night-talk-question">这一夜不会由一条通用结果收尾。你现在的回答,会决定她把这段话当作明说、共同定界,还是只留门内的私情。</p><div class="choice-stack">${E.nightConversationOptions(state).map((choice) => choiceButton(choice, 'night-conversation')).join('')}</div>`}
1933
+ </div>
1934
+ </div>`;
1935
+ }
1936
+ const choices = E.nightOptions(state).map((option) => ({ ...NIGHT_TEXT[option.id], ...option }));
1937
+ return `
1938
+ <div class="dialogue-stage visual-stage night dialogue-${id}">
1939
+ <div class="close-cg closer" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${h.name}夜间近景"></div>
1940
+ <div class="decision-panel dialogue-panel">
1941
+ <div class="character-seal" aria-hidden="true"><i>${h.glyph}</i><span>${h.house}</span></div>
1942
+ ${phaseHeader(`第 ${state.day} 夜 · 灯下`, h.name, TEXT.nightLead)}
1943
+ ${intimacyMemory(id)}
1944
+ <div class="choice-stack">${choices.map((choice) => choiceButton(choice, 'night')).join('')}</div>
1945
+ <p class="consent-note">她往前一步,才有下一步。她若停下,门便停在这里。</p>
1946
+ </div>
1947
+ </div>`;
1948
+ }
1949
+
1950
+ function renderMorningSettlement() {
1951
+ const story = E.currentMorningSettlement(state);
1952
+ if (!story) return '<div class="fatal-card">晨簿上的具名缺口断了页。</div>';
1953
+ const h = HEROINES[story.heroine.id];
1954
+ if (story.resolved) {
1955
+ return `
1956
+ <div class="morning-stage visual-stage tone-backing" data-morning-settlement="${story.cause}" data-morning-settlement-choice="${story.choice.id}" style="--scene-bg:url('${urlFor('compound')}')">
1957
+ <div class="morning-cg" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${escapeHtml(story.imageLabel)}"></div>
1958
+ <div class="decision-panel morning-panel">
1959
+ ${phaseHeader(`${story.kicker} · 结算收束`, story.resolution.title, story.resolution.body)}
1960
+ <section class="precedent-scope"><span>本人之物</span><b>${escapeHtml(story.heroine.object)}</b></section>
1961
+ <ol class="precedent-steps" aria-label="这项限制如何真正执行">
1962
+ ${story.steps.map((step) => `<li>${escapeHtml(step)}</li>`).join('')}
1963
+ </ol>
1964
+ <button class="ink-button story-continue" data-morning-settlement-continue="1">把限制写上白日行动卡</button>
1965
+ </div>
1966
+ </div>`;
1967
+ }
1968
+ return `
1969
+ <div class="morning-stage visual-stage tone-warning" data-morning-settlement="${story.cause}" style="--scene-bg:url('${urlFor('compound')}')">
1970
+ <div class="morning-cg" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${escapeHtml(story.imageLabel)}"></div>
1971
+ <div class="decision-panel morning-panel">
1972
+ ${phaseHeader(`${story.kicker} · 本人先处分`, story.title, story.body)}
1973
+ <section class="precedent-object" aria-label="缺口来源与本人处分">
1974
+ <p><b>${escapeHtml(story.sourceText)}</b> · ${h.name}</p>
1975
+ <span>${escapeHtml(story.heroine.opening.body)}</span>
1976
+ </section>
1977
+ <blockquote class="precedent-reply"><span>${escapeHtml(story.heroine.opening.title)}</span>${escapeHtml(story.heroine.opening.line)}</blockquote>
1978
+ <section class="precedent-boundary" aria-label="她已经收回的物件与恢复条件">
1979
+ <article><b>${escapeHtml(story.heroine.object)}</b><span>${escapeHtml(story.heroine.restriction.label)}</span><p>${escapeHtml(story.heroine.recovery.label)}</p></article>
1980
+ </section>
1981
+ <p class="rivalry-question">她已经处分本人之物。你不能选谁少领,只能决定宅子怎样承认这道缺口。</p>
1982
+ <div class="choice-grid">${E.morningSettlementOptions(state).map((choice) => choiceButton(choice, 'morning-settlement-choice')).join('')}</div>
1983
+ </div>
1984
+ </div>`;
1985
+ }
1986
+
1987
+ function renderMorning() {
1988
+ const event = state.morning;
1989
+ const h = HEROINES[event.actor];
1990
+ const resolution = E.currentMorningResolution(state);
1991
+ if (resolution) {
1992
+ const actor = HEROINES[resolution.actor];
1993
+ const visited = HEROINES[resolution.visited];
1994
+ return `
1995
+ <div class="morning-stage rivalry-stage rivalry-resolved visual-stage tone-rivalry" data-rivalry-result="${resolution.choice}" style="--scene-bg:url('${urlFor('compound')}')">
1996
+ <div class="morning-cg rivalry-cg" style="background-image:url('${urlFor(actor.close)}')" role="img" aria-label="${actor.name}听见你的最终表态"></div>
1997
+ <figure class="rivalry-listener"><img src="${urlFor(visited.portrait)}" alt="${visited.name}"/><figcaption><span>${visited.house}</span><b>${visited.name}</b><small>她也承担了这次选择的后果</small></figcaption></figure>
1998
+ <div class="decision-panel morning-panel rivalry-panel rivalry-result-panel">
1999
+ ${phaseHeader(`第 ${state.day} 日 · 对峙结果收束`, resolution.title, resolution.text)}
2000
+ <p class="rivalry-consequence">${escapeHtml(resolution.consequence)}</p>
2001
+ <div class="rivalry-result-cast" aria-label="这次选择改变了两院关系"><span>${actor.short}</span><i>⇄</i><span>${visited.short}</span></div>
2002
+ <button class="ink-button story-continue rivalry-continue" data-morning-resolution="1">收下这次站队的后果</button>
2003
+ </div>
2004
+ </div>`;
2005
+ }
2006
+ const story = E.currentMorningStory(state);
2007
+ if (story) {
2008
+ const speaker = HEROINES[story.speaker];
2009
+ const listener = HEROINES[story.listener];
2010
+ return `
2011
+ <div class="morning-stage rivalry-stage visual-stage tone-rivalry" data-rivalry="${event.actor}:${event.visited}:${story.index}" style="--scene-bg:url('${urlFor('compound')}')">
2012
+ <div class="morning-cg rivalry-cg" style="background-image:url('${urlFor(speaker.close)}')" role="img" aria-label="${speaker.name}正在廊下说话"></div>
2013
+ <figure class="rivalry-listener"><img src="${urlFor(listener.portrait)}" alt="${listener.name}"/><figcaption><span>${listener.house}</span><b>${listener.name}</b></figcaption></figure>
2014
+ <div class="decision-panel morning-panel rivalry-panel">
2015
+ ${phaseHeader(`第 ${state.day} 日 · 三人对峙 · ${narrativeStep(story.index + 1, story.count)}`, event.title, event.text)}
2016
+ ${story.index === 0 ? (event.notes ?? []).map((note) => `<p class="morning-note">${escapeHtml(note)}</p>`).join('') : ''}
2017
+ <blockquote class="rivalry-voice"><span>${speaker.house}</span><b>${speaker.name}</b><p>${escapeHtml(story.text)}</p></blockquote>
2018
+ ${story.index < story.count - 1
2019
+ ? `<button class="ink-button story-continue rivalry-continue" data-morning-story="1">${story.index === 0 ? `听${listener.short}亲口回应` : '让这场问话说到底'}</button>`
2020
+ : `<p class="rivalry-question">她们都已亲口说完。现在才轮到你决定,偏爱要见光、交还给她们,还是继续只留在一扇门内。</p><div class="choice-stack">${E.morningOptions(state).map((choice) => choiceButton(choice, 'morning')).join('')}</div>`}
2021
+ </div>
2022
+ </div>`;
2023
+ }
2024
+ return `
2025
+ <div class="morning-stage visual-stage tone-${event.tone}" style="--scene-bg:url('${urlFor('compound')}')">
2026
+ <div class="morning-cg" style="background-image:url('${urlFor(h.close)}')" role="img" aria-label="${h.name}次晨近景"></div>
2027
+ <div class="decision-panel morning-panel">
2028
+ ${phaseHeader(`第 ${state.day} 日 · 天刚亮`, event.title, event.text)}
2029
+ ${event.scene ? `<div class="morning-scene" style="background-image:url('${urlFor(event.scene)}')" role="img" aria-label="院门外,收账人抱着账册等着"></div>` : ''}
2030
+ ${(event.notes ?? []).map((note) => `<p class="morning-note">${escapeHtml(note)}</p>`).join('')}
2031
+ <p class="phase-lead">${TEXT.morningLead}</p>
2032
+ <div class="choice-stack">${E.morningOptions(state).map((choice) => choiceButton(choice, 'morning')).join('')}</div>
2033
+ </div>
2034
+ </div>`;
2035
+ }
2036
+
2037
+ function renderBanquet() {
2038
+ const current = E.currentPublicEvent(state);
2039
+ if (!current) return '<div class="fatal-card">这场公开问责断了页。</div>';
2040
+ // 选择前的公开席面只用宅院界画加暖灯遮罩:五人同框的群像留给选择之后的场景册,
2041
+ // 它第一次出现必须发生在玩家按下那个按钮之后。
2042
+ return `
2043
+ <div class="banquet-stage visual-stage" style="--scene-bg:url('${urlFor('compound')}')">
2044
+ <div class="decision-panel banquet-panel">
2045
+ ${phaseHeader(`第 ${state.day} 日 · ${current.title}`, current.heading ?? current.title, current.body ?? current.text ?? TEXT.banquetLead)}
2046
+ ${current.mealMemory ? `<aside class="opening-public-echo" data-meal-memory="${current.mealMemory.heroine}"><span>昨夜去处已经进入今日饭食</span><b>${escapeHtml(current.mealMemory.dish)} · ${escapeHtml(current.mealMemory.house)}</b><p>${escapeHtml(current.mealMemory.trace)}</p></aside>` : ''}
2047
+ ${current.dayPreparation ? `<aside class="opening-public-echo public-day-preparation" data-public-day-preparation="${current.dayPreparation.sourceDay}:${current.dayPreparation.sourceAction}"><span>今日白日做法已经进入公议</span><b>${escapeHtml(current.dayPreparation.label)}</b><p>${escapeHtml(current.dayPreparation.text)}</p><small>${escapeHtml(current.dayPreparation.object)}</small></aside>` : ''}
2048
+ ${current.draftEcho ? `<aside class="opening-public-echo" data-day15-draft-echo="${current.draftEcho.aftermath}"><span>昨日银票已成为堂前可见事实</span><b>${escapeHtml(current.draftEcho.label)}</b><p>${escapeHtml(current.draftEcho.text)}</p></aside>` : ''}
2049
+ ${current.openingMemory ? `<aside class="opening-public-echo first-day-opening-memory" data-day15-first-opening="${current.openingMemory.choice}"><span>第一日的起手原则回到堂前</span><b>${escapeHtml(current.openingMemory.title)}</b><p>${escapeHtml(current.openingMemory.day15Text)}</p></aside>` : ''}
2050
+ <div class="choice-grid">${E.banquetOptions(state).map((choice) => choiceButton(choice, 'banquet')).join('')}</div>
2051
+ </div>
2052
+ </div>`;
2053
+ }
2054
+
2055
+ function renderPublicEvidence() {
2056
+ const chain = E.currentPublicEvidence(state);
2057
+ if (!chain) return '<div class="fatal-card">堂前的三步证链断了页。</div>';
2058
+ const introCopy = narrativeExcerpt(chain.body, 110);
2059
+ const slots = Array.from({ length:chain.count }, (_, index) => {
2060
+ const picked = chain.selectedEvidence[index];
2061
+ const step = chain.steps[index];
2062
+ return picked
2063
+ ? `<li class="filled strength-${step.strength}" data-public-evidence-slot="${index + 1}"><i>${narrativeBeatMark(index)}</i><span>${escapeHtml(picked.label)}</span><b>${step.strength === 2 ? (picked.id === 'cross_words' ? '收口成立' : '承接成立') : step.strength === 1 ? (picked.id === 'grain_measure' ? '事实迟到' : '旁证补强') : '承接悬空'}</b></li>`
2064
+ : `<li data-public-evidence-slot="${index + 1}"><i>${narrativeBeatMark(index)}</i><span>${index === 0 ? '先定事实' : index === 1 ? '再接经手' : '最后收口'}</span><b>等待证物</b></li>`;
2065
+ }).join('');
2066
+ const last = chain.selectedEvidence.at(-1);
2067
+ const result = chain.result;
2068
+ return `
2069
+ <div class="public-evidence-stage visual-stage ${result ? `result-${result.id}` : ''}" data-public-evidence-step="${chain.step}" style="--scene-bg:url('${urlFor('cg/group/public_day15')}')">
2070
+ <div class="decision-panel public-evidence-panel">
2071
+ ${phaseHeader(`第十五日 · 堂前举证 · ${narrativeStep(Math.min(chain.step + 1, chain.count), chain.count)}`, chain.title, introCopy.preview)}
2072
+ ${narrativeArchive(introCopy, 'public-evidence-long-brief', '三步成证的完整案情')}
2073
+ ${chain.publicOpening ? `<aside class="opening-public-echo day15-public-opening ${chain.publicOpening.falseScapegoat ? 'is-contaminated' : ''}" data-day15-public-opening="${chain.publicOpening.choice}"><span>${chain.publicOpening.falseScapegoat ? '公审已经先押了一个答案' : '公审没有预填罪名'}</span><b>${escapeHtml(chain.publicOpening.label)}</b><p>${escapeHtml(chain.publicOpening.evidenceText)}</p></aside>` : ''}
2074
+ ${chain.openingEvidence ? `<aside class="opening-public-echo day15-opening-evidence" data-day15-opening-evidence="${chain.openingEvidence.sourceAction}"><span>白日行动已经递出第一证,不再重选</span><b>${escapeHtml(chain.openingEvidence.label)}</b><p>${escapeHtml(chain.openingEvidence.openingText)}</p><small>${escapeHtml(chain.openingEvidence.object)}</small></aside>` : ''}
2075
+ ${chain.draftEcho ? `<aside class="opening-public-echo" data-day15-evidence-draft="${chain.draftEcho.aftermath}"><span>银票只能证明自己的那一段</span><b>${escapeHtml(chain.draftEcho.label)}</b><p>${escapeHtml(chain.draftEcho.evidenceText)}</p></aside>` : ''}
2076
+ <div class="external-witnesses" aria-label="会反问证链的外部人物">${chain.witnesses.map((person) => `<article data-external-witness="${person.id}"><small>${escapeHtml(person.role)}</small><b>${escapeHtml(person.name)}</b><p>${escapeHtml(person.stance)}</p></article>`).join('')}</div>
2077
+ <ol class="evidence-chain-slots" aria-label="已经排出的证据次序">${slots}</ol>
2078
+ ${last && !result ? `<aside class="evidence-resistance"><span>这一手立刻遭到反问</span><p>${escapeHtml(last.resistance)}</p></aside>` : ''}
2079
+ ${result
2080
+ ? `<section class="evidence-verdict" data-evidence-result="${result.id}"><small>${escapeHtml(result.label)} · ${chain.score >= 5 ? '证链稳固' : chain.score >= 3 ? '证链可用' : '证链仍显吃力'}</small><h3>${escapeHtml(result.title)}</h3><p>${escapeHtml(result.body)}</p><button class="ink-button story-continue" data-public-evidence-complete="1">带证链进入主签</button></section>`
2081
+ : `<p class="public-evidence-rule">${chain.openingEvidence ? '第一证已由白日行动落案;现在只决定第二、三证怎样承接。' : chain.step ? '上一证已经落案;现在只决定下一证怎样承接。' : '次序不能撤回;第一证若只有口供或自家账,后两证会一直替它补理由。'}</p><div class="choice-grid public-evidence-choices">${E.publicEvidenceOptions(state).map((choice) => choiceButton(choice, 'public-evidence')).join('')}</div>`}
2082
+ </div>
2083
+ </div>`;
2084
+ }
2085
+
2086
+ function renderPublicFollowup() {
2087
+ const event = E.currentPublicFollowup(state);
2088
+ const publicEvent = PUBLIC_EVENTS[state.day];
2089
+ if (!event || !publicEvent) return '<div class="fatal-card">这场公议的后半幕断了页。</div>';
2090
+ const scene = SCENES[publicEvent.scene];
2091
+ return `
2092
+ <div class="public-followup-stage visual-stage" data-public-followup="${event.id}" style="--scene-bg:url('${urlFor(scene.asset)}')">
2093
+ <div class="decision-panel public-followup-panel">
2094
+ ${phaseHeader(event.kicker, event.title, event.body)}
2095
+ ${event.mealMemory ? `<aside class="opening-public-echo" data-meal-memory="${event.mealMemory.heroine}"><span>这碗热汤的原始去处</span><b>${escapeHtml(event.mealMemory.short)} · ${escapeHtml(event.mealMemory.dish)}</b><p>${escapeHtml(event.mealMemory.trace)}</p></aside>` : ''}
2096
+ ${event.day5Opening ? `<aside class="opening-public-echo day5-public-opening" data-day5-followup-opening="${event.day5Opening.choice}"><span>${event.day5Opening.choice === 'public_5_favor' ? '已认偏宠正在限制第二裁决' : '逐手经办正在限制第二裁决'}</span><b>${escapeHtml(event.day5Opening.label)}</b><p>${escapeHtml(event.day5Opening.followupText)}</p></aside>` : ''}
2097
+ ${event.dayPreparation ? `<aside class="opening-public-echo public-day-preparation" data-public-followup-preparation="${event.dayPreparation.sourceDay}:${event.dayPreparation.sourceAction}"><span>白日原物正在限制第二裁决</span><b>${escapeHtml(event.dayPreparation.label)}</b><p>${escapeHtml(event.dayPreparation.text)}</p><small>${escapeHtml(event.dayPreparation.object)}</small></aside>` : ''}
2098
+ ${event.day10Opening ? `<aside class="opening-public-echo day10-public-opening" data-day10-followup-opening="${event.day10Opening.choice}"><span>莲池开场留下的原页状态</span><b>${escapeHtml(event.day10Opening.label)}</b><p>${escapeHtml(event.day10Opening.followupText)}</p></aside>` : ''}
2099
+ ${event.draftEcho ? `<aside class="opening-public-echo" data-day15-followup-draft="${event.draftEcho.aftermath}"><span>主签不能改写昨日银票</span><b>${escapeHtml(event.draftEcho.label)}</b><p>${escapeHtml(event.draftEcho.text)}</p></aside>` : ''}
2100
+ ${event.publicOpening ? `<aside class="opening-public-echo day15-public-opening ${event.publicOpening.falseScapegoat ? 'is-contaminated' : ''}" data-day15-followup-public-opening="${event.publicOpening.choice}"><span>${event.publicOpening.falseScapegoat ? '错误预断尚未撤回' : '罪名栏仍保持空白'}</span><b>${escapeHtml(event.publicOpening.label)}</b><p>${escapeHtml(event.publicOpening.followupText)}</p></aside>` : ''}
2101
+ ${event.openingEvidence ? `<aside class="opening-public-echo day15-opening-evidence" data-day15-followup-opening="${event.openingEvidence.sourceAction}"><span>白日首证继续限制主签</span><b>${escapeHtml(event.openingEvidence.label)}</b><p>${escapeHtml(event.openingEvidence.followupText)}</p><small>${escapeHtml(event.openingEvidence.object)}</small></aside>` : ''}
2102
+ ${event.openingMemory ? `<aside class="opening-public-echo" data-opening-memory="${event.openingMemory.choice}"><span>第五日拿回第一笔选择</span><b>${escapeHtml(event.openingMemory.title)}</b><p>${escapeHtml(event.openingMemory.publicEcho)}</p></aside>` : ''}
2103
+ ${event.evidenceChain ? `<aside class="public-evidence-echo" data-public-evidence-echo="${event.evidenceChain.id}"><span>刚才排出的三步证链 · ${escapeHtml(event.evidenceChain.outcome.label)}</span><b>${event.evidenceChain.chain.map((id) => escapeHtml(PUBLIC_EVIDENCE_CHAIN.evidence.find((item) => item.id === id)?.label ?? id)).join(' → ')}</b><p>${escapeHtml(event.evidenceChain.outcome.echo)}</p></aside>` : ''}
2104
+ <div class="public-followup-cast" aria-label="仍留在现场的五个人">${event.participants.map((id) => `<figure><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption>${HEROINES[id].short}</figcaption></figure>`).join('')}</div>
2105
+ <p class="public-followup-note">前一拍解决了“当众说什么”,这一拍决定说完以后谁承担、谁被漏掉,以及五院是否仍愿意互相接证。</p>
2106
+ <div class="choice-grid">${E.publicFollowupOptions(state).map((choice) => choiceButton(choice, 'public-followup')).join('')}</div>
2107
+ </div>
2108
+ </div>`;
2109
+ }
2110
+
2111
+ function renderPublicAftermath() {
2112
+ const story = E.currentPublicAftermath(state);
2113
+ const beat = story?.current;
2114
+ const publicEvent = PUBLIC_EVENTS[state.day];
2115
+ if (!story || !beat || !publicEvent) return '<div class="fatal-card">这场公议的后果没有接住。</div>';
2116
+ const scene = SCENES[publicEvent.scene];
2117
+ const last = story.beat === story.count - 1;
2118
+ return `
2119
+ <div class="public-aftermath-stage visual-stage public-day-${state.day} public-beat-${story.beat + 1}" data-public-aftermath="${story.choice}" data-public-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor(scene.asset)}')">
2120
+ <div class="public-aftermath-cast" aria-label="仍在承担这份公开决定的五个人">${story.participants.map((id) => `<figure class="${beat.speaker === id ? 'speaking' : 'listening'}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].name}</b></figcaption></figure>`).join('')}</div>
2121
+ <div class="decision-panel public-aftermath-panel">
2122
+ ${phaseHeader(`公开问责 · ${story.label} · ${narrativeStep(story.beat + 1, story.count)}`, beat.title, beat.body)}
2123
+ ${story.dayPreparation ? `<aside class="opening-public-echo public-day-preparation" data-public-aftermath-preparation="${story.dayPreparation.sourceDay}:${story.dayPreparation.sourceAction}:${story.beat + 1}"><span>白日原物仍限制这一拍</span><b>${escapeHtml(story.dayPreparation.label)}</b><p>${escapeHtml(story.dayPreparation.text)}</p><small>${escapeHtml(story.dayPreparation.object)}</small></aside>` : ''}
2124
+ ${story.day5Opening ? `<aside class="opening-public-echo day5-public-opening" data-day5-aftermath-opening="${story.day5Opening.choice}:${story.beat + 1}"><span>${story.day5Opening.choice === 'public_5_favor' ? '已认偏宠仍限制这一拍' : '逐手经办仍限制这一拍'}</span><b>${escapeHtml(story.day5Opening.label)}</b><p>${escapeHtml(story.day5Opening.text)}</p></aside>` : ''}
2125
+ ${story.day10Opening ? `<aside class="opening-public-echo day10-public-opening" data-day10-aftermath-opening="${story.day10Opening.choice}:${story.beat + 1}"><span>原页完整或缺失仍限制这一拍</span><b>${escapeHtml(story.day10Opening.label)}</b><p>${escapeHtml(story.day10Opening.text)}</p></aside>` : ''}
2126
+ ${story.publicOpening ? `<aside class="opening-public-echo day15-public-opening ${story.publicOpening.falseScapegoat ? 'is-contaminated' : ''}" data-day15-aftermath-public-opening="${story.publicOpening.choice}:${story.beat + 1}"><span>${story.publicOpening.falseScapegoat ? '先押人的后果仍在这一拍' : '未预填罪名仍限制这一拍'}</span><b>${escapeHtml(story.publicOpening.label)}</b><p>${escapeHtml(story.publicOpening.text)}</p></aside>` : ''}
2127
+ ${story.openingEvidence ? `<aside class="opening-public-echo day15-opening-evidence" data-day15-aftermath-opening="${story.openingEvidence.sourceAction}:${story.beat + 1}"><span>第一证仍按真实次序留在这一拍</span><b>${escapeHtml(story.openingEvidence.label)}</b><p>${escapeHtml(story.openingEvidence.text)}</p><small>${escapeHtml(story.openingEvidence.object)}</small></aside>` : ''}
2128
+ <div class="public-aftermath-ledger"><span>${story.beat === 0 ? '公开决定已经落下' : story.beat === 1 ? '受影响的人开始追问' : '这份决定终于进入共同记录'}</span><b>${last ? '人名、证物、责任与撤回权都已写明;今晚以后,五院会按这份真实后果继续相处。' : '席面还没有散。下一拍会由另一院检查:这句话究竟保护了谁,又把代价留给了谁。'}</b></div>
2129
+ <button class="ink-button story-continue" data-public-aftermath-continue="1">${last ? '把这场公议完整记入总账' : story.beat === 0 ? '让受影响的人接着说' : '看五院最后怎样收这份决定'}</button>
2130
+ </div>
2131
+ </div>`;
2132
+ }
2133
+
2134
+ function renderFivePrivatePrices() {
2135
+ const story = E.currentFivePrivatePrices(state);
2136
+ if (!story?.current) return '<div class="fatal-card">五封私价没有按前史接上。</div>';
2137
+ const protocol = story.protocol ? story.protocols.find((row) => row.id === story.protocol) : null;
2138
+ const offers = Object.values(story.offers);
2139
+ const jiaoerEcho = story.jiaoerEcho
2140
+ ? `<aside class="council-external-echo five-price-jiaoer-echo" data-jiaoer-aftermath="${story.jiaoerEcho.aftermath}"><span>第十八日娇儿交易仍在桌边</span><b>${escapeHtml(story.jiaoerEcho.label)}</b><p>${escapeHtml(story.jiaoerEcho.object)}</p></aside>`
2141
+ : '';
2142
+ const dayPreparation = story.dayPreparation
2143
+ ? `<aside class="council-external-echo five-price-day-preparation" data-day19-price-preparation="${story.dayPreparation.sourceAction}"><span>第十九日白日准备仍在这一屏</span><b>${escapeHtml(story.dayPreparation.label)}</b><p>${escapeHtml(story.dayPreparation.object)}</p></aside>`
2144
+ : '';
2145
+ let content = '';
2146
+ if (story.stage === 'overview') {
2147
+ content = `<section class="private-price-offers" aria-label="五封分别开给五个人的价">${offers.map((offer) => `<article data-private-offer="${offer.id}"><img src="${urlFor(HEROINES[offer.target].portrait)}" alt="${HEROINES[offer.target].name}"/><div><span>${HEROINES[offer.target].short} · ${escapeHtml(offer.actor === 'dai_an' ? '玳安' : offer.actor === 'han_daoguo' ? '韩道国' : '应伯爵')}</span><b>${escapeHtml(offer.title)}</b><p>${escapeHtml(offer.offer)}</p></div></article>`).join('')}</section><button class="ink-button story-continue" data-five-price-continue="1">让五个人各自取回写着名字的信</button>`;
2148
+ } else if (story.stage === 'protocol') {
2149
+ content = `<p class="private-price-rule">这里只分配答复权,不替任何人选择接受、反价、拒绝或公开。</p><div class="choice-grid">${E.fivePrivatePriceOptions(state).map((choice) => choiceButton(choice, 'five-price-protocol')).join('')}</div>`;
2150
+ } else if (story.stage === 'reply') {
2151
+ content = `<section class="private-price-reply" data-private-reply="${story.reply.heroine}:${story.reply.outcome}">
2152
+ <figure><img src="${urlFor(HEROINES[story.reply.heroine].close)}" alt="${HEROINES[story.reply.heroine].name}"/><figcaption>${HEROINES[story.reply.heroine].house}</figcaption></figure>
2153
+ <article><span>${escapeHtml(story.offer.title)}</span><blockquote>${escapeHtml(story.outcome.line)}</blockquote><p>${escapeHtml(story.outcome.body)}</p><dl><dt>她为何这样答</dt>${story.reply.reasons.map((reason) => `<dd>${escapeHtml(reason)}</dd>`).join('')}</dl><small>${escapeHtml(protocol?.label ?? '')}只决定寄法,没有改掉她的答案。</small></article>
2154
+ </section><button class="ink-button story-continue" data-five-price-continue="1">${story.beat < 6 ? '让下一封由本人作答' : '看外面怎样沿第十六日旧路反招'}</button>`;
2155
+ } else if (story.stage === 'right') {
2156
+ content = `<section class="private-price-counter"><span>第十六日旧路回手 · ${escapeHtml(story.counter.actor === 'dai_an' ? '玳安' : story.counter.actor === 'han_daoguo' ? '韩道国' : '应伯爵')}</span><p>${escapeHtml(story.counter.body)}</p></section><p class="private-price-rule">三项都是真权利,也都会让另外两项在明日更难保全。</p><div class="choice-grid">${E.fivePrivatePriceOptions(state).map((choice) => choiceButton(choice, 'five-price-right')).join('')}</div>`;
2157
+ } else {
2158
+ const memberSet = new Set(story.coalition.members);
2159
+ const continueLabel = story.coalition.kind === 'full' ? '带着五封互证进入第十九夜' : story.coalition.kind === 'limited' ? '带着候选与缺口进入第十九夜' : '带着权利底线进入第十九夜';
2160
+ content = `<section class="private-price-coalition" data-private-coalition="${story.coalition.kind}"><div>${HEROINE_IDS.map((id) => `<figure class="${memberSet.has(id) ? 'inside' : 'self-held'}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption>${memberSet.has(id) ? story.coalition.kind === 'limited' ? '候选互证' : '五院互证' : '本人自持'} · ${HEROINES[id].short}</figcaption></figure>`).join('')}</div><p>${story.coalition.kind === 'full' ? '五封答复互相可核,但正文仍各归本人。' : story.coalition.kind === 'limited' ? '候选间互证、非候选者自持;尚无人因一纸外账预签今夜同席。' : '只保权利底线,不伪造五院圆满。'}</p></section><button class="ink-button story-continue" data-five-price-continue="1">${continueLabel}</button>`;
2161
+ }
2162
+ return `<div class="five-private-prices-stage visual-stage stage-${story.stage}" data-five-price-stage="${story.stage}" data-five-price-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor('compound')}')"><div class="decision-panel five-private-prices-panel">${phaseHeader(`${story.kicker} · ${narrativeStep(story.beat + 1, story.count)}`, story.current.title, story.current.body)}${dayPreparation}${jiaoerEcho}${content}</div></div>`;
2163
+ }
2164
+
2165
+ function renderFinalReckoning() {
2166
+ const event = E.currentFinalReckoning(state);
2167
+ if (!event) return '<div class="fatal-card">终局外账没有接上。</div>';
2168
+ const priceLedger = event.fivePrice?.statuses?.length
2169
+ ? `<section class="private-price-ledger" aria-label="第十九夜五封私价答复">${event.fivePrice.statuses.map((reply) => `<article data-price-reply="${reply.heroine}:${reply.outcome}"><img src="${urlFor(HEROINES[reply.heroine].portrait)}" alt=""/><span>${HEROINES[reply.heroine].short}</span><b>${escapeHtml(reply.label)}</b><small>${escapeHtml(reply.title)}</small></article>`).join('')}</section>`
2170
+ : '';
2171
+ return `
2172
+ <div class="final-reckoning-stage visual-stage" data-final-reckoning="${event.id}" style="--scene-bg:url('${urlFor('cg/group/public_day15')}')">
2173
+ <div class="reckoning-ledgers" aria-hidden="true"><i>银</i><i>名</i></div>
2174
+ <div class="decision-panel final-reckoning-panel">
2175
+ ${phaseHeader(event.kicker, event.title, '外柜两本总账已经摊开。先决定外账怎样结清,再进入黄昏。')}
2176
+ <aside class="council-external-echo final-reckoning-long-brief"><span>完整来路收入旧账</span><b>二十日外账怎样走到门槛</b><p>${escapeHtml(event.body)}</p></aside>
2177
+ ${priceLedger}
2178
+ ${event.dayPreparation ? `<aside class="council-external-echo final-day-preparation" data-day20-reckoning-preparation="${event.dayPreparation.sourceAction}"><span>第二十日白日准备仍在两本总账前</span><b>${escapeHtml(event.dayPreparation.label)}</b><p>${escapeHtml(event.dayPreparation.object)}</p></aside>` : ''}
2179
+ ${event.publicOpening ? `<aside class="council-external-echo day15-public-opening ${event.publicOpening.falseScapegoat ? 'is-contaminated' : ''}" data-final-day15-opening="${event.publicOpening.choice}"><span>${event.publicOpening.falseScapegoat ? '终局不能用强证洗掉先押' : '终局仍不预填承担全案的人'}</span><b>${escapeHtml(event.publicOpening.label)}</b><p>${escapeHtml(event.evidenceEcho)}</p></aside>` : ''}
2180
+ ${event.openingMemory ? `<aside class="council-external-echo first-day-opening-memory" data-final-first-opening="${event.openingMemory.choice}"><span>第二十日正在回答第一日怎样起手</span><b>${escapeHtml(event.openingMemory.title)}</b><p>${escapeHtml(event.openingMemory.finalText)}</p></aside>` : ''}
2181
+ ${event.jiaoerEcho ? `<aside class="council-external-echo final-jiaoer-echo" data-final-jiaoer-aftermath="${event.jiaoerEcho.aftermath}"><span>第十八日娇儿交易仍约束两本总账</span><b>${escapeHtml(event.jiaoerEcho.label)}</b><p>${escapeHtml(event.jiaoerEcho.object)}</p></aside>` : ''}
2182
+ ${event.councilRule ? `<aside class="council-external-echo final-council-rule" data-final-council-rule="${event.councilRule.sourceChoice}"><span>第十七日追问规则仍在约束终局</span><b>${escapeHtml(event.councilRule.title)}</b><p>${escapeHtml(event.councilRule.permission)}</p></aside>` : ''}
2183
+ <div class="final-reckoning-cast">${event.participants.map((id) => `<figure><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption>${HEROINES[id].short}<small>${HEROINES[id].want}</small></figcaption></figure>`).join('')}</div>
2184
+ <p class="reckoning-note">先结清外账,黄昏后才轮到五院决定谁留下、怎样留下。这里选的是敌人以后还能拿什么追讨,不是替任何女人决定去留。</p>
2185
+ <div class="choice-grid">${E.finalReckoningOptions(state).map((choice) => choiceButton(choice, 'final-reckoning')).join('')}</div>
2186
+ </div>
2187
+ </div>`;
2188
+ }
2189
+
2190
+ function renderFinalReckoningAftermath() {
2191
+ const story = E.currentFinalReckoningAftermath(state);
2192
+ const beat = story?.current;
2193
+ if (!story || !beat) return '<div class="fatal-card">终局外账的接管现场断了页。</div>';
2194
+ const beatCopy = narrativeExcerpt(beat.body);
2195
+ const resolved = Boolean(story.resolution);
2196
+ const options = story.awaitingChoice ? E.finalReckoningAftermathOptions(state) : [];
2197
+ return `
2198
+ <div class="final-aftermath-stage visual-stage final-approach-${story.approach}" data-final-aftermath="${story.approach}" data-final-beat="${story.beat + 1}" style="--scene-bg:url('${urlFor('cg/group/public_day15')}')">
2199
+ <div class="final-aftermath-ledgers" aria-hidden="true"><i>银</i><i>名</i><i>证</i></div>
2200
+ <div class="final-aftermath-cast" aria-label="正在接管终局外账的五个人">${story.participants.map((id) => `<figure class="${beat.speaker === id ? 'speaking' : 'listening'}"><img src="${urlFor(HEROINES[id].portrait)}" alt="${HEROINES[id].name}"/><figcaption><b>${HEROINES[id].name}</b></figcaption></figure>`).join('')}</div>
2201
+ <div class="decision-panel final-aftermath-panel">
2202
+ ${phaseHeader(`第二十日 · 外账接管 · ${narrativeStep(story.beat + 1, story.count)}`, beat.title, beatCopy.preview)}
2203
+ ${narrativeArchive(beatCopy, 'final-aftermath-long-brief', '这一拍的完整来路')}
2204
+ ${story.dayPreparation ? `<aside class="council-external-echo final-aftermath-day-preparation" data-final-day-preparation="${story.dayPreparation.sourceAction}"><span>白日留下的物件仍限制这一拍</span><b>${escapeHtml(story.dayPreparation.label)}</b><p>${escapeHtml(story.dayPreparation.object)}</p></aside>` : ''}
2205
+ ${story.jiaoerEcho ? `<aside class="council-external-echo final-aftermath-jiaoer-echo" data-final-aftermath-jiaoer="${story.jiaoerEcho.aftermath}"><span>第十八日留下的物件没有被总账吞掉</span><b>${escapeHtml(story.jiaoerEcho.label)}</b><p>${escapeHtml(story.jiaoerEcho.object)}</p></aside>` : ''}
2206
+ ${story.publicOpening ? `<aside class="council-external-echo day15-public-opening ${story.publicOpening.falseScapegoat ? 'is-contaminated' : ''}" data-final-aftermath-day15-opening="${story.publicOpening.choice}:${story.beat + 1}"><span>${story.publicOpening.falseScapegoat ? '撤押页与证物页仍然分栏' : '空白罪名栏仍然保留'}</span><b>${escapeHtml(story.publicOpening.label)}</b><p>${escapeHtml(story.publicOpening.text)}</p></aside>` : ''}
2207
+ ${story.openingMemory ? `<aside class="council-external-echo first-day-opening-memory" data-final-aftermath-first-opening="${story.openingMemory.choice}:${story.beat + 1}"><span>开局原则仍在这一拍兑现</span><b>${escapeHtml(story.openingMemory.title)}</b><p>${escapeHtml(story.openingMemory.text)}</p></aside>` : ''}
2208
+ <div class="final-custody-rule"><span>${story.beat === 0 ? '先执行你选定的大方向' : story.beat === 1 ? '外柜拿私话和单一主簿反扑' : story.beat === 2 ? '现在决定证据与保管权怎样落地' : '五个人已经把最后一项权力写清'}</span><b>${resolved ? '外账不再只是清掉一个数值。它已经明确谁能出证、谁能撤回、谁保管原件,以及任何共同查阅需要什么条件。' : story.awaitingChoice ? '两种办法都能成立,但保护隐私、共同查证与行动速度的代价不同;这一步会进入终局总账。' : '下一拍会有人用官面规则反咬这项决定,五院必须亲手守住各自的名字与证据。'}</b></div>
2209
+ ${story.awaitingChoice
2210
+ ? `<div class="choice-grid final-aftermath-choices">${options.map((choice) => choiceButton(choice, 'final-reckoning-aftermath')).join('')}</div>`
2211
+ : `<button class="ink-button story-continue" data-final-aftermath-continue="1">${resolved ? '带着这份外账进入最后黄昏' : story.beat === 0 ? '看外柜怎样反咬' : '让五个人提出最后两种办法'}</button>`}
2212
+ </div>
2213
+ </div>`;
2214
+ }
2215
+
2216
+ function renderScene() {
2217
+ const scene = SCENES[state.pendingScene];
2218
+ const chapter = E.currentSceneChapter(state);
2219
+ if (!scene || !chapter) return '<div class="fatal-card">这段册页没有接上。</div>';
2220
+ ensureNarrativeAsset(scene.asset);
2221
+ const adult = ['prelude', 'explicit', 'ensemble-intimate'].includes(scene.tier);
2222
+ const ensemble = scene.tier === 'ensemble';
2223
+ const allianceOverview = chapter.allianceTableau && !chapter.allianceTableauBeat
2224
+ ? `<section class="alliance-tableau-overview" aria-label="有限同盟三次回答怎样形成真实成员群像"><header><b>${escapeHtml(chapter.allianceTableau.title)}</b><span>真实成员本人落席 · 每一问都已执行</span></header><p>${escapeHtml(chapter.allianceTableau.lead)}</p><div class="alliance-tableau-members">${chapter.allianceTableau.members.map((heroine) => `<figure data-alliance-tableau-member="${heroine}"><img src="${urlFor(HEROINES[heroine].portrait)}" alt="${HEROINES[heroine].name}"/><figcaption><b>${escapeHtml(HEROINES[heroine].short)}</b><span>${escapeHtml(HEROINES[heroine].house)}</span></figcaption></figure>`).join('')}</div><div class="alliance-tableau-path">${chapter.allianceTableau.beats.map((beat) => `<span data-alliance-tableau-overview="${beat.choice}"><small>${escapeHtml(beat.beatTitle)}</small><b>${escapeHtml(beat.choiceLabel)}</b><em>${escapeHtml(beat.title)}</em></span>`).join('')}</div><aside class="alliance-tableau-nonmembers"><b>没有被补画进成员席的人</b>${chapter.allianceTableau.nonmembers.map((row) => `<p data-alliance-tableau-nonmember="${row.heroine}:${row.kind}"><strong>${escapeHtml(HEROINES[row.heroine].short)} · ${escapeHtml(row.label)}</strong><span>${escapeHtml(row.text)}</span></p>`).join('')}</aside></section>`
2225
+ : '';
2226
+ const allianceEntry = chapter.allianceTableauBeat
2227
+ ? `<section class="alliance-tableau-entry" data-alliance-tableau-entry="${chapter.allianceTableauBeat.choice}" aria-label="${escapeHtml(chapter.allianceTableauBeat.choiceLabel)}怎样被真实成员实际执行"><header><span>${escapeHtml(chapter.allianceTableauBeat.beatTitle)}</span><b>${escapeHtml(chapter.allianceTableauBeat.title)}</b></header><p>${escapeHtml(chapter.allianceTableauBeat.body)}</p><div class="alliance-tableau-actions">${chapter.allianceTableau.members.map((heroine) => `<article data-alliance-tableau-action="${chapter.allianceTableauBeat.choice}:${heroine}"><img src="${urlFor(HEROINES[heroine].portrait)}" alt=""/><div><b>${escapeHtml(HEROINES[heroine].short)}</b><p>${escapeHtml(chapter.allianceTableauBeat.actions[heroine])}</p></div></article>`).join('')}</div><blockquote>${escapeHtml(chapter.allianceTableauBeat.transition)}</blockquote></section>`
2228
+ : '';
2229
+ const accordOverview = chapter.sharedAccord?.length && !chapter.accordEntry
2230
+ ? `<section class="shared-night-accord-overview" aria-label="五院同灯成立前的五份本人契据"><header><b>同灯为何能成立</b><span>第19日本人答复 → 第20日真实保护 → 今夜各自落印</span></header><div>${chapter.sharedAccord.map((entry) => `<span data-shared-night-accord-overview="${entry.heroine}"><b>${escapeHtml(HEROINES[entry.heroine].short)}</b><em>${escapeHtml(entry.day19.outcomeLabel)}</em><small>${escapeHtml(entry.day20.protectionLabel)}</small></span>`).join('')}</div></section>`
2231
+ : '';
2232
+ const accordEntry = chapter.accordEntry
2233
+ ? `<section class="shared-night-accord-entry" data-shared-night-accord="${chapter.accordEntry.heroine}" aria-label="${escapeHtml(HEROINES[chapter.accordEntry.heroine].name)}为何能亲自进入五院同灯"><header><b>同灯为何能成立 · ${escapeHtml(HEROINES[chapter.accordEntry.heroine].name)}</b><span>${escapeHtml(chapter.accordEntry.day20.protectionLabel)}</span></header><div class="shared-night-accord-ledger"><article><small>第19日 · 本人答复</small><b>${escapeHtml(chapter.accordEntry.day19.offerTitle)} · ${escapeHtml(chapter.accordEntry.day19.outcomeLabel)}</b><blockquote>${escapeHtml(chapter.accordEntry.day19.responseLine)}</blockquote><p>${escapeHtml(chapter.accordEntry.day19.result)}</p></article><article><small>第20日 · 两步执行</small><b>${escapeHtml(chapter.accordEntry.day20.choiceLabel)} → ${escapeHtml(chapter.accordEntry.day20.aftermathLabel)}</b><p>${escapeHtml(chapter.accordEntry.day20.protectionText)}</p></article><article><small>本人旧约与具名实绩</small><b>${escapeHtml(chapter.accordEntry.accord.label)} · ${escapeHtml(chapter.accordEntry.proof.label)}</b><p>${escapeHtml(chapter.accordEntry.accord.text)}</p></article></div><p class="shared-night-accord-conclusion">${escapeHtml(chapter.accordEntry.conclusion)}</p></section>`
2234
+ : '';
2235
+ const tableauOverview = chapter.sharedAfterglowTableau && !chapter.tableauBeat
2236
+ ? `<section class="shared-afterglow-tableau-overview" aria-label="三拍余夜怎样共同形成这一幅成人群像"><header><b>三拍怎样走进这一幅</b><span>${escapeHtml(chapter.sharedAfterglowTableau.title)}</span></header><div>${chapter.sharedAfterglowTableau.beats.map((beat) => `<span data-shared-afterglow-tableau-overview="${beat.choice}"><small>${escapeHtml(beat.beatTitle)}</small><b>${escapeHtml(beat.choiceLabel)}</b><em>${escapeHtml(beat.title)}</em></span>`).join('')}</div></section>`
2237
+ : '';
2238
+ const tableauEntry = chapter.tableauBeat
2239
+ ? `<section class="shared-afterglow-tableau-entry" data-shared-afterglow-tableau-entry="${chapter.tableauBeat.choice}" aria-label="${escapeHtml(chapter.tableauBeat.choiceLabel)}怎样改变五人成人群像"><header><span>${escapeHtml(chapter.tableauBeat.beatTitle)}</span><b>${escapeHtml(chapter.tableauBeat.title)}</b></header><p>${escapeHtml(chapter.tableauBeat.body)}</p><div class="shared-afterglow-tableau-actions">${HEROINE_IDS.map((heroine) => `<article data-shared-afterglow-tableau-action="${chapter.tableauBeat.choice}:${heroine}"><b>${escapeHtml(HEROINES[heroine].short)}</b><p>${escapeHtml(chapter.tableauBeat.actions[heroine])}</p></article>`).join('')}</div><blockquote>${escapeHtml(chapter.tableauBeat.transition)}</blockquote></section>`
2240
+ : '';
2241
+ return `
2242
+ <article class="scene-view" id="scene-view" data-scene-id="${scene.id}" data-scene-tier="${scene.tier}" data-scene-beat="${chapter.index}">
2243
+ <img id="scene-image" src="${urlFor(scene.asset)}" alt="${scene.title}"/>
2244
+ <div class="scene-scrim"></div>
2245
+ <div class="scene-caption">
2246
+ <p class="eyebrow">${scene.tier === 'alliance' ? '有限同盟 · 真实成员本人落席' : scene.tier === 'ensemble-intimate' ? '18+ · 五个人都独立点了头' : adult ? '18+ · 她点了头' : ensemble ? '五院共约 · 五个人都在' : '公开席面 · 满桌人都在'}${chapter.count > 1 ? ` · 册页${narrativeStep(chapter.index + 1, chapter.count)}` : ''}</p>
2247
+ <h2>${escapeHtml(chapter.allianceTableau?.title ?? scene.title)}</h2>
2248
+ ${chapter.kicker ? `<p class="scene-chapter-kicker">${escapeHtml(chapter.kicker)}</p>` : ''}
2249
+ ${chapter.allianceTableau || chapter.accordEntry || chapter.tableauBeat ? '' : `<p>${escapeHtml(chapter.text)}</p>`}
2250
+ ${allianceOverview}
2251
+ ${allianceEntry}
2252
+ ${accordOverview}
2253
+ ${accordEntry}
2254
+ ${tableauOverview}
2255
+ ${tableauEntry}
2256
+ <div class="scene-meta"><span>这一页留下了</span><span>${chapter.allianceTableau ? chapter.allianceTableau.members.map((id) => HEROINES[id].name).join('、') : scene.participants.length ? scene.participants.map((id) => HEROINES[id].name).join('、') : '中秋同席'}</span></div>
2257
+ <button class="ink-button story-continue" id="btn-scene-close">${escapeHtml(chapter.button)}</button>
2258
+ </div>
2259
+ </article>`;
2260
+ }
2261
+
2262
+ function renderPersonalAfterglow() {
2263
+ const event = E.currentPersonalAfterglow(state);
2264
+ if (!event) return '<div class="fatal-card">这段余夜没有接上。</div>';
2265
+ const heroine = HEROINES[event.heroine];
2266
+ const scene = SCENES[event.scene];
2267
+ ensureNarrativeAsset(scene.asset);
2268
+ return `
2269
+ <div class="personal-afterglow-stage visual-stage dialogue-${event.heroine}" data-personal-afterglow="${event.event}" style="--scene-bg:url('${urlFor(heroine.close)}')">
2270
+ <div class="afterglow-close" style="background-image:url('${urlFor(heroine.close)}')" role="img" aria-label="${heroine.name}在余夜里的近景"></div>
2271
+ <figure class="afterglow-memory"><img src="${urlFor(scene.asset)}" alt="${scene.title}"/><figcaption>${event.tier === 'explicit' ? '刚才那一夜' : '刚才那一步'} · ${scene.title}</figcaption></figure>
2272
+ <div class="decision-panel personal-afterglow-panel">
2273
+ ${phaseHeader(`${event.kicker} · ${event.tier === 'explicit' ? '留宿余夜' : '帘前余夜'}`, event.title, event.body)}
2274
+ <p class="afterglow-lead">靠近已经发生。现在这句话会决定:她明早记住的是被索取、被理解,还是终于能按自己的方式留下。</p>
2275
+ <div class="choice-stack">${E.personalAfterglowOptions(state).map((choice) => choiceButton(choice, 'personal-afterglow')).join('')}</div>
2276
+ </div>
2277
+ </div>`;
2278
+ }
2279
+
2280
+ function renderPersonalAfterglowAftermath() {
2281
+ const story = E.currentPersonalAfterglowAftermath(state);
2282
+ if (!story) return '<div class="fatal-card">这段余夜后章没有接上。</div>';
2283
+ const heroine = HEROINES[story.heroine];
2284
+ const scene = SCENES[story.scene];
2285
+ ensureNarrativeAsset(scene.asset);
2286
+ const action = story.awaitingChoice
2287
+ ? `<p class="afterglow-aftermath-question">亲近已经发生;现在才决定,它明日会成为旧凭据、共同制度,还是她仍可改口的一次选择。</p><div class="personal-afterglow-aftermath-choices">${E.personalAfterglowAftermathOptions(state).map((choice) => choiceButton(choice, 'personal-afterglow-aftermath')).join('')}</div>`
2288
+ : `<button class="ink-button story-continue personal-afterglow-aftermath-continue" data-personal-afterglow-aftermath-continue="1">${story.resolved ? '让这项安排带到天亮' : '听她把最后一问说完'}</button>`;
2289
+ return `
2290
+ <div class="personal-afterglow-aftermath-stage visual-stage dialogue-${story.heroine} ${story.resolved ? 'is-resolved' : ''}" data-personal-afterglow-aftermath="${story.event}" data-story-step="${story.step}">
2291
+ <div class="personal-afterglow-aftermath-close" style="background-image:url('${urlFor(heroine.close)}')" role="img" aria-label="${heroine.name}在亲近之后继续和你谈天亮后的安排"></div>
2292
+ <figure class="personal-afterglow-aftermath-memory"><img src="${urlFor(scene.asset)}" alt="${scene.title}"/><figcaption><span>刚才靠近</span><b>${escapeHtml(scene.title)}</b></figcaption></figure>
2293
+ <div class="decision-panel personal-afterglow-aftermath-panel">
2294
+ ${phaseHeader(`${story.kicker} · ${story.tier === 'explicit' ? '留宿后章' : '前奏后章'} · ${narrativeStep(story.step + 1, story.count)}`, story.current.title, story.current.body)}
2295
+ <div class="personal-afterglow-aftermath-ledger">
2296
+ <span><small>第一次回答</small><b>${escapeHtml(story.approachChoice.label)}</b><em>${escapeHtml(story.approachChoice.hint)}</em></span>
2297
+ <span class="${story.awaitingChoice ? 'current' : story.resolved ? 'settled' : ''}"><small>${story.resolved ? '带到明日' : '现在要决定'}</small><b>${story.resolved ? escapeHtml(story.choice.label) : story.awaitingChoice ? '她要一项能实行的安排' : '这段话还没有说完'}</b><em>${story.resolved ? escapeHtml(story.choice.hint) : '不是再点一次好感,而是决定以后怎样相处'}</em></span>
2298
+ </div>
2299
+ ${action}
2300
+ </div>
2301
+ </div>`;
2302
+ }
2303
+
2304
+ function renderEnding() {
2305
+ const end = state.ending;
2306
+ const top = HEROINE_IDS.slice().sort((a, b) => state.relations[b].qing - state.relations[a].qing)[0];
2307
+ const endingLead = ({
2308
+ balanced: '天亮时,五盏灯还在同一张桌边。月娘合上总账,金莲把最后一句假话撤掉,瓶儿、玉楼与雪娥各自收回钥匙、名帖和工簿。她们愿意留下,不等于把自己交给一本总账。',
2309
+ alliance: '天亮时,留在桌边的人只答应了自己亲手做过的那一部分。没有坐进来的院门仍各自收着钥匙;今夜的亲近,不替任何人补一个没说出口的“愿意”。',
2310
+ exclusive: `天亮时,${HEROINES[top].name}的门还开着。其余四院没有因为你做了选择就自动原谅;她们把各自的原话、钥匙和去处收回,留下的人也不替你遮掉这份代价。`,
2311
+ intrigue: '天亮时,门外的账暂时压住了,门里却没有因此重新热起来。你用银子、名字和口供换来一口气;谁被暴露,谁收回了手,仍然一笔笔留在桌上。',
2312
+ unstable: '天亮时,五处院门依次灭了灯。月娘封账,金莲收走你说过的话,瓶儿扣回钥匙,玉楼把名帖收进匣里,雪娥合上工簿。门外的债还在,这一回没有人再替你把空话接下去。',
2313
+ })[end.id] ?? end.text;
2314
+ const relationshipSummary = end.id === 'balanced'
2315
+ ? '<span>五院关系 <b>都还在桌上</b></span>'
2316
+ : end.id === 'alliance'
2317
+ ? `<span>同盟院门 <b>${escapeHtml(end.allianceName)}</b></span>`
2318
+ : `<span>最深关系 <b>${HEROINES[top].name} · ${E.relationTier(state.relations[top].qing, 'qing')}</b></span>`;
2319
+ const nightPatterns = end.nightPatterns ?? [];
2320
+ const collapseMemoryLedger = end.collapseResult?.memories?.length
2321
+ ? `<section class="personal-departure-reasons ending-collapse-memories" aria-label="破局清算中五个人各自追讨的真实旧事"><header><b>散局没有抹掉的五件旧事</b><span>${escapeHtml(end.collapseResult.label)} · 各归本人</span></header><ol>${end.collapseResult.memories.map((memory) => `<li data-ending-collapse-memory="${memory.choice}:${memory.heroine}"><b>${escapeHtml(HEROINES[memory.heroine].short)} · ${escapeHtml(memory.label)}</b><p>${escapeHtml(memory.text)}</p><em>${escapeHtml(memory.conclusion)}</em></li>`).join('')}</ol></section>`
2322
+ : '';
2323
+ const allianceMemoryLedger = end.allianceMemberMemories?.length
2324
+ ? `<section class="personal-departure-reasons ending-alliance-memories" aria-label="有限同盟真实成员各自落定的三拍"><header><b>同盟不是一个风格名</b><span>三拍都由真实成员逐项接住</span></header><ol>${end.allianceMemberMemories.map((memory) => `<li data-ending-alliance-memory="${memory.heroine}"><b>${escapeHtml(HEROINES[memory.heroine].short)}</b><ol>${memory.choices.map((choice) => `<li><strong>${escapeHtml(choice.choiceLabel)}</strong><p>${escapeHtml(choice.response)}</p></li>`).join('')}</ol></li>`).join('')}</ol></section>`
2325
+ : '';
2326
+ const allianceTableauLedger = end.allianceTableau
2327
+ ? `<section class="personal-departure-reasons ending-alliance-tableau" aria-label="有限同盟三问形成的最终共同生活群像"><header><b>${escapeHtml(end.allianceTableau.title)}</b><span>真实成员逐院落席 · 结构由她们亲手形成</span></header><ol>${end.allianceTableau.beats.map((beat) => `<li data-ending-alliance-tableau="${beat.choice}"><strong>${escapeHtml(beat.choiceLabel)} · ${escapeHtml(beat.title)}</strong><p>${escapeHtml(beat.body)}</p></li>`).join('')}</ol></section>`
2328
+ : '';
2329
+ const sharedNightAccordLedger = end.sharedNightAccord?.length
2330
+ ? `<section class="personal-departure-reasons ending-shared-night-accord" aria-label="五院同灯成立前五个人各自留下的两日契据"><header><b>同灯为何能成立</b><span>不是五个人被一项结局条件自动收编</span></header><ol>${end.sharedNightAccord.map((entry) => `<li data-ending-shared-night-accord="${entry.heroine}"><b>${escapeHtml(HEROINES[entry.heroine].short)} · ${escapeHtml(entry.day19.outcomeLabel)}</b><p>${escapeHtml(entry.day19.responseLine)}</p><em>${escapeHtml(entry.day20.choiceLabel)} → ${escapeHtml(entry.day20.aftermathLabel)} · ${escapeHtml(entry.day20.protectionLabel)}</em></li>`).join('')}</ol></section>`
2331
+ : '';
2332
+ const sharedFinaleMemoryLedger = end.sharedFinaleMemories?.length
2333
+ ? `<section class="personal-departure-reasons ending-shared-finale-memories" aria-label="五院共守四拍由五个人分别留下的真实记忆"><header><b>共守不是一句同灯</b><span>三拍余夜与一拍次晨均逐人落字</span></header><ol>${end.sharedFinaleMemories.map((memory) => `<li data-ending-shared-finale-memory="${memory.heroine}"><b>${escapeHtml(HEROINES[memory.heroine].short)}</b><ol>${[...memory.afterglow, memory.dawn].map((choice) => `<li><strong>${escapeHtml(choice.choiceLabel)}</strong><p>${escapeHtml(choice.response)}</p></li>`).join('')}</ol></li>`).join('')}</ol></section>`
2334
+ : '';
2335
+ // 结局图跟着结局走:专一给该女主的立绘近景,五院同灯用真正协作的群像,
2336
+ // 权谋与不稳退回夜色宅院,由 CSS 按 data-ending 分别调色。
2337
+ const allianceArt = end.id === 'alliance' && end.alliance?.length === 2
2338
+ ? JOINT_ACTIONS.find((choice) => choice.participants.every((id) => end.alliance.includes(id)))?.asset
2339
+ : null;
2340
+ const artUrl = end.id === 'exclusive' && end.finaleAsset
2341
+ ? urlFor(end.finaleAsset)
2342
+ : allianceArt
2343
+ ? urlFor(allianceArt)
2344
+ : end.id === 'balanced'
2345
+ ? urlFor('cg/group/inner_court_accord')
2346
+ : urlFor('compound');
2347
+ return `
2348
+ <article class="ending-view" id="ending-view" data-ending="${end.id}">
2349
+ <div class="ending-art" style="background-image:linear-gradient(90deg,rgba(16,12,10,.9),rgba(16,12,10,.25)),url('${artUrl}')"></div>
2350
+ <div class="ending-copy">
2351
+ <p class="eyebrow">第 20 日 · 风月总账</p>
2352
+ <h1 tabindex="-1">${end.title}</h1>
2353
+ <p class="ending-tag">${end.tag}${end.heroineName ? ` · ${end.heroineName}` : ''}${end.reckoningResult ? ` · ${end.reckoningResult}` : ''}${end.routeResult ? ` · ${end.routeResult}` : ''}</p>
2354
+ <p>${endingLead}</p>
2355
+ <details class="ending-old-account"><summary>翻看这二十日的来路</summary><p>${end.text}</p></details>
2356
+ <div class="ending-ledger">
2357
+ ${relationshipSummary}
2358
+ <span>结局总账 <b>${loadEndingArchive().length ? '已有旧卷' : '首卷落定'}</b></span>
2359
+ <span>场景册 <b>${loadGallery().length ? '已有册页' : '尚未开册'}</b></span>
2360
+ <span>秘密去向 <b>${state.secretsUsed.length ? '曾经动用' : '未曾动用'}</b></span>
2361
+ <span>未开题签 <b>${end.unseen.length ? '仍有旧页待翻' : '已经翻全'}</b></span>
2362
+ </div>
2363
+ ${nightPatterns.length ? `<section class="ending-habits" aria-label="五院夜谈形成的相处习惯"><header><b>夜谈留下的相处习惯</b><span>不是好感档位,而是以后怎样过</span></header><div>${nightPatterns.map((pattern) => `<article class="${pattern.mode ? '' : 'unformed'}" data-ending-pattern="${pattern.id || `forming-${pattern.heroine}`}"><span>${HEROINES[pattern.heroine].short} · ${narrativeProgress(pattern.chapters, 4, ['刚刚起话', '仍在磨合', '已成相处条款'])}</span><b>${escapeHtml(pattern.label)}</b><small>${escapeHtml(pattern.summary)}</small></article>`).join('')}</div></section>` : ''}
2364
+ ${sharedNightAccordLedger}
2365
+ ${sharedFinaleMemoryLedger}
2366
+ ${allianceTableauLedger}
2367
+ ${allianceMemoryLedger}
2368
+ ${collapseMemoryLedger}
2369
+ <div class="household-ending">${end.householdResults.map((item) => `<span><b>${item.name} · ${escapeHtml(item.result)}</b><small>${escapeHtml(item.detail)}</small></span>`).join('')}</div>
2370
+ <p class="ending-note">这一种共处方式已经记进结局总账,却还没有走到原著命数的尽头。可先读一个月后的五院生活,再翻开第 30—100 回不会被好感改写的三页余账。</p>
2371
+ <div class="button-row"><button class="ink-button story-continue" id="btn-fate-coda">翻命数三页</button><button class="ink-button" id="btn-epilogue">读五封一月笺</button><button class="ink-button" id="btn-restart">换一套暗线</button><button class="ink-button" id="btn-replay-seed">重走同一局</button><button class="ink-button" id="btn-gallery">看结局总账</button></div>
2372
+ </div>
2373
+ </article>`;
2374
+ }
2375
+
2376
+ function epiloguePages() {
2377
+ if (archivedEpilogueKey) {
2378
+ const entry = loadEndingArchive().find((row) => row.key === archivedEpilogueKey);
2379
+ const pages = entry?.afterstory?.pages;
2380
+ return Array.isArray(pages) ? pages : [];
2381
+ }
2382
+ const ending = state?.ending;
2383
+ const heroinePages = (ending?.epilogues ?? []).map((page) => ({
2384
+ ...page,
2385
+ kicker: `${HEROINES[page.heroine].house} · 一个月后`,
2386
+ name: HEROINES[page.heroine].name,
2387
+ asset: HEROINES[page.heroine].close,
2388
+ }));
2389
+ if (!ending) return heroinePages;
2390
+ return [...heroinePages, {
2391
+ heroine: null,
2392
+ kicker: '末页 · 一个月后',
2393
+ name: '宅门仍在运转',
2394
+ title: ending.title,
2395
+ body: ending.text,
2396
+ routeNote: `外账结法:${ending.reckoningResult || '尚未真正结清'}。关系结法:${ending.routeResult || ending.tag}。这不是“从此无事”,而是她们愿意怎样继续处理下一次偏爱、欠账与争执。`,
2397
+ relation: null,
2398
+ asset: ending.id === 'balanced' ? 'cg/group/inner_court_accord' : 'compound',
2399
+ }];
2400
+ }
2401
+
2402
+ function appendEpilogue() {
2403
+ const pages = epiloguePages();
2404
+ if (!pages.length) return;
2405
+ epilogueIndex = Math.max(0, Math.min(epilogueIndex, pages.length - 1));
2406
+ const page = pages[epilogueIndex];
2407
+ const relation = page.relation
2408
+ ? `<div class="epilogue-relations"><span>情 <b>${escapeHtml(page.relation.qing)}</b></span><span>欲 <b>${escapeHtml(page.relation.yu)}</b></span><span>妒 <b>${escapeHtml(page.relation.du)}</b></span></div>`
2409
+ : '';
2410
+ const habit = page.habit?.mode
2411
+ ? `<section class="epilogue-habit" data-epilogue-pattern="${page.habit.id}"><span>${page.habit.settled ? '夜谈形成的关系条款' : '夜谈正在形成的关系条款'} · ${narrativeProgress(page.habit.chapters, 4, ['刚刚起话', '仍在磨合', '已经落定'])}</span><b>${escapeHtml(page.habit.label)}</b><p>${escapeHtml(page.habit.summary)}</p></section>`
2412
+ : '';
2413
+ const personalDepartureOutcome = page.personalDeparture
2414
+ ? ({ accept:'接下程序', amend:'亲手改约', refuse:'自行收回' })[page.personalDeparture.outcome]
2415
+ : '';
2416
+ const personalDepartureLedger = page.personalDeparture
2417
+ ? `<section class="epilogue-night-conversations epilogue-personal-departure" aria-label="个人终章中由她本人落定的善后"><header><span>专情没有让四院排队成全</span><b>${escapeHtml(personalDepartureOutcome)} · ${escapeHtml(page.personalDeparture.response.title)}</b></header><ol><li data-epilogue-personal-departure="${page.personalDeparture.procedure.id}:${page.personalDeparture.outcome}"><small>真实程序 · ${escapeHtml(page.personalDeparture.procedure.label)}</small><b>${escapeHtml(page.personalDeparture.procedure.focus)}</b><span class="stake">怎样执行 · ${escapeHtml(page.personalDeparture.procedure.summary)}</span><p>${escapeHtml(page.personalDeparture.response.line)}</p><em>当时逐项核验</em><ol>${page.personalDeparture.reasons.map((reason, index) => `<li data-epilogue-personal-departure-reason="${index + 1}">${escapeHtml(reason)}</li>`).join('')}</ol></li></ol></section>`
2418
+ : '';
2419
+ const collapseMemoryLedger = page.collapseMemory
2420
+ ? `<section class="epilogue-night-conversations epilogue-collapse-memory" aria-label="破局清算中由她本人追到今夜的旧事"><header><span>散局没有替她销掉这件旧事</span><b>${escapeHtml(page.collapseMemory.choiceLabel)} · ${escapeHtml(page.collapseMemory.label)}</b></header><ol><li data-epilogue-collapse-memory="${page.collapseMemory.choice}:${page.collapseMemory.heroine}"><small>${page.collapseMemory.day ? `第 ${page.collapseMemory.day} 日 · ` : ''}${escapeHtml(page.collapseMemory.kind)}</small><b>${escapeHtml(page.collapseMemory.label)}</b><p>${escapeHtml(page.collapseMemory.text)}</p><em>${escapeHtml(page.collapseMemory.conclusion)}</em></li></ol></section>`
2421
+ : '';
2422
+ const allianceMemoryLedger = page.allianceMemory
2423
+ ? `<section class="epilogue-night-conversations epilogue-alliance-memory" aria-label="有限同盟中由她本人接住并实际执行的三拍"><header><span>入盟不只剩一个相处风格</span><b>${escapeHtml(HEROINES[page.allianceMemory.heroine].short)} · 三拍均由本人落定</b></header><ol>${page.allianceMemory.choices.map((choice) => `<li data-epilogue-alliance-memory="${page.allianceMemory.heroine}:${choice.choice}"><small>${escapeHtml(choice.beatTitle)}</small><b>${escapeHtml(choice.choiceLabel)}</b><p>${escapeHtml(choice.response)}</p><span class="stake">群像中的实际动作 · ${escapeHtml(choice.tableauAction)}</span><em>当时逐项依据</em><ol>${choice.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ol></li>`).join('')}</ol></section>`
2424
+ : '';
2425
+ const sharedNightAccordLedger = page.sharedNightAccord
2426
+ ? `<section class="epilogue-night-conversations epilogue-shared-night-accord" aria-label="五院同灯成立前由她本人留下的两日契据"><header><span>同灯先经过她本人的两日答复</span><b>${escapeHtml(HEROINES[page.sharedNightAccord.heroine].short)} · ${escapeHtml(page.sharedNightAccord.day20.protectionLabel)}</b></header><ol><li data-epilogue-shared-night-accord="${page.sharedNightAccord.heroine}"><small>第19日 · ${escapeHtml(page.sharedNightAccord.day19.offerTitle)}</small><b>${escapeHtml(page.sharedNightAccord.day19.outcomeLabel)}</b><p>${escapeHtml(page.sharedNightAccord.day19.responseLine)} ${escapeHtml(page.sharedNightAccord.day19.result)}</p><em>第20日真实执行 · ${escapeHtml(page.sharedNightAccord.day20.choiceLabel)} → ${escapeHtml(page.sharedNightAccord.day20.aftermathLabel)}</em><span class="stake">${escapeHtml(page.sharedNightAccord.day20.protectionText)}</span><p>${escapeHtml(page.sharedNightAccord.conclusion)}</p></li></ol></section>`
2427
+ : '';
2428
+ const sharedFinaleMemoryLedger = page.sharedFinaleMemory
2429
+ ? `<section class="epilogue-night-conversations epilogue-shared-finale-memory" aria-label="五院共守中由她本人接住的四拍"><header><span>同灯没有合并她的前史</span><b>${escapeHtml(HEROINES[page.sharedFinaleMemory.heroine].short)} · 四拍均由本人落定</b></header><ol>${[...page.sharedFinaleMemory.afterglow, page.sharedFinaleMemory.dawn].map((choice) => `<li data-epilogue-shared-finale-memory="${page.sharedFinaleMemory.heroine}:${choice.choice}"><small>${escapeHtml(choice.beatTitle ?? '次晨见光')}</small><b>${escapeHtml(choice.choiceLabel)}</b><p>${escapeHtml(choice.response)}</p><em>当时逐项依据</em><ol>${choice.reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('')}</ol></li>`).join('')}</ol></section>`
2430
+ : '';
2431
+ const intimacyArrangementLedger = page.arrangements?.length
2432
+ ? `<section class="epilogue-night-conversations epilogue-intimacy-arrangements" aria-label="亲密之后仍在执行的具名约定"><header><span>亲密没有替天亮后的生活作答</span><b>${page.arrangements.length > 1 ? '旧约都仍有效' : '这项旧约仍有效'}</b></header><ol>${page.arrangements.map((arrangement) => `<li data-epilogue-intimacy-arrangement="${arrangement.id}"><small>第 ${arrangement.day} 夜 · ${arrangement.tier === 'explicit' ? '留宿后约' : '前奏后约'}</small><b>${escapeHtml(arrangement.label)} · ${escapeHtml(arrangement.title)}</b><p>${escapeHtml(arrangement.outcome)}</p><em>次晨:${escapeHtml(arrangement.morning)} 此后:${escapeHtml(arrangement.future)}</em></li>`).join('')}</ol></section>`
2433
+ : '';
2434
+ const nightConversationLedger = page.nightConversations?.length
2435
+ ? `<section class="epilogue-night-conversations" aria-label="专属夜谈留下的长期生活事实"><header><span>说过的话怎样继续生活</span><b>夜谈已经落到一月后</b></header><ol>${page.nightConversations.map((memory) => `<li data-epilogue-night-conversation="${memory.event}:${memory.mode}"><small>${narrativeProgress(memory.chapter, 4, ['刚刚起话', '已经接续', '已成相处条款'])} · ${escapeHtml(memory.prop)} · ${escapeHtml(memory.modeLabel)}</small><b>${escapeHtml(memory.choiceLabel)}</b><span class="stake">实际执行 · ${escapeHtml(memory.stakeLabel)} · ${escapeHtml(narrativeChoiceMeta(memory.stakeResourceText))}</span><p>${escapeHtml(memory.future)}</p><em>${escapeHtml(memory.observerName)}怎样接住:${escapeHtml(memory.observerLine)}</em></li>`).join('')}</ol></section>`
2436
+ : '';
2437
+ const ordinaryNightLedger = page.ordinaryNights?.length
2438
+ ? `<section class="epilogue-night-conversations epilogue-ordinary-nights" aria-label="普通夜章留下的长期生活事实"><header><span>停下或把茶喝完以后</span><b>这些夜章仍在生活里</b></header><ol>${page.ordinaryNights.map((memory) => `<li data-epilogue-ordinary-night="${memory.event}"><small>${memory.count > 1 ? `第 ${memory.firstDay}—${memory.day} 夜 · 反复发生` : `第 ${memory.day} 夜`} · ${escapeHtml(memory.actionLabel)}</small><b>${escapeHtml(memory.title)}</b><p>${escapeHtml(memory.closing)}</p><em>次晨真正发生:${escapeHtml(memory.morning)}</em></li>`).join('')}</ol></section>`
2439
+ : '';
2440
+ const pairMemoryLedger = page.pairMemories?.length
2441
+ ? `<section class="epilogue-night-conversations epilogue-pair-memories" aria-label="双院私议留下的横向关系事实"><header><span>她与旁院怎样继续相处</span><b>横向关系已经落字</b></header><ol>${page.pairMemories.map((memory) => `<li data-epilogue-pair-memory="${memory.event}:${memory.choice}"><small>第 ${memory.day} 日 · 与${escapeHtml(memory.partnerName)} · ${escapeHtml(memory.label)}</small><b>${escapeHtml(memory.title)}</b><p>${escapeHtml(memory.memory)}</p><em>${escapeHtml(memory.witnessName)}见证这项关系怎样外溢。</em></li>`).join('')}</ol></section>`
2442
+ : '';
2443
+ const invitationMemoryLedger = page.invitationMemory
2444
+ ? `<section class="epilogue-night-conversations epilogue-invitation-memory" aria-label="她主动提出的邀约留下的长期安排"><header><span>不是被挑中,而是她主动来请</span><b>第 ${page.invitationMemory.day} 日 · ${escapeHtml(page.invitationMemory.approachLabel)}</b></header><ol><li data-epilogue-invitation-memory="${page.invitationMemory.event}:${page.invitationMemory.approach}:${page.invitationMemory.choice}"><small>${escapeHtml(page.invitationMemory.invitationTitle)} · ${escapeHtml(page.invitationMemory.witnessName)}见证</small><b>${escapeHtml(page.invitationMemory.title)} · ${escapeHtml(page.invitationMemory.choiceLabel)}</b><span class="stake">她亲口提出 · ${escapeHtml(page.invitationMemory.heroineLine)}</span><p>${escapeHtml(page.invitationMemory.outcome)}</p><em>${escapeHtml(page.invitationMemory.witnessName)}当日这样追问:${escapeHtml(page.invitationMemory.witnessQuestion)}</em></li></ol></section>`
2445
+ : '';
2446
+ const rivalryMemoryLedger = page.rivalryMemories?.length
2447
+ ? `<section class="epilogue-night-conversations epilogue-rivalry-memories" aria-label="偏宠对峙留下的双方关系史"><header><span>偏宠曾经怎样被当面追问</span><b>这些裁决没有被妒意终值覆盖</b></header><ol>${page.rivalryMemories.map((memory) => `<li data-epilogue-rivalry-memory="${memory.day}:${memory.actor}:${memory.visited}:${memory.choice}"><small>第 ${memory.day} 日 · ${memory.role === 'challenger' ? '她发难' : '她是昨夜被选择的一院'} · 与${escapeHtml(memory.otherName)}</small><b>${escapeHtml(memory.title)} · ${escapeHtml(memory.choiceLabel)}</b><span class="stake">她当面说 · ${escapeHtml(memory.role === 'challenger' ? memory.opening : memory.visitedReply)}</span><p>${escapeHtml(memory.outcome)}</p></li>`).join('')}</ol></section>`
2448
+ : '';
2449
+ const routeReckoningLedger = page.routeReckonings?.length
2450
+ ? `<section class="epilogue-night-conversations epilogue-route-reckoning" aria-label="两日后旧话裁决留下的长期结果"><header><span>旧话裁决怎样继续生活</span><b>原物账已经落定</b></header><ol>${page.routeReckonings.map((memory) => `<li data-epilogue-route-reckoning="${memory.event}:${memory.sourceDay}:${memory.choice}"><small>第 ${memory.sourceDay} 日原物 · 第 ${memory.day} 日落定 · ${escapeHtml(memory.promiseLabel)} · ${escapeHtml(memory.observerName)}见证</small><b>${escapeHtml(memory.stakeLabel)} · ${escapeHtml(memory.choiceLabel)}</b><span class="stake">实际执行 · ${escapeHtml(memory.stakeText)} · ${escapeHtml(narrativeChoiceMeta(memory.stakeResourceText))}</span><p>${escapeHtml(memory.incident)} ${escapeHtml(memory.outcome)}</p><em>${escapeHtml(memory.observerName)}当日这样质询:${escapeHtml(memory.observerText)} 只追这一件:${escapeHtml(memory.question)}</em></li>`).join('')}</ol></section>`
2451
+ : '';
2452
+ const favorReckoningLedger = page.favorReckonings?.length
2453
+ ? `<section class="epilogue-night-conversations epilogue-favor-reckoning" aria-label="借人情收危局留下的长期还账结果"><header><span>借来的名、话与实物怎样还</span><b>人情账已经落定</b></header><ol>${page.favorReckonings.map((memory) => `<li data-epilogue-favor-reckoning="${memory.event}:${memory.sourceDay}:${memory.choice}"><small>第 ${memory.sourceDay} 日借力 · 第 ${memory.day} 日落定 · ${escapeHtml(memory.observerName)}见证</small><b>${escapeHtml(memory.debtTitle)} · ${escapeHtml(memory.choiceLabel)}</b><span class="stake">当时借力 · ${escapeHtml(memory.sourceLabel)} · ${escapeHtml(memory.sourceText)}</span><p>${escapeHtml(memory.debtBody)} ${escapeHtml(memory.outcome)}</p><em>${escapeHtml(memory.observerName)}当日怎样举证:${escapeHtml(memory.observerLine)}</em></li>`).join('')}</ol></section>`
2454
+ : '';
2455
+ const overlay = document.createElement('div');
2456
+ overlay.className = 'epilogue-overlay';
2457
+ overlay.id = 'epilogue-modal';
2458
+ overlay.innerHTML = `<section class="epilogue-book" role="dialog" aria-modal="true" aria-labelledby="epilogue-title" ${archivedEpilogueKey ? `data-archived-ending="${escapeHtml(archivedEpilogueKey)}"` : ''}>
2459
+ <div class="epilogue-art"><img src="${urlFor(page.asset)}" alt="${escapeHtml(page.name)}的后日谈"/><span>笺</span></div>
2460
+ <article class="epilogue-copy">
2461
+ <header><p class="eyebrow">${archivedEpilogueKey ? '永久总账重看 · ' : ''}${escapeHtml(page.kicker)}</p><button class="plain-button" id="btn-epilogue-close">${archivedEpilogueKey ? '回到结局总账' : '合上后日谈'}</button></header>
2462
+ <div class="epilogue-scroll">
2463
+ <p class="epilogue-progress">后日谈 · ${narrativeStep(epilogueIndex + 1, pages.length)}${page.heroine ? ` · ${escapeHtml(page.name)}` : ''}</p>
2464
+ <h2 id="epilogue-title">${escapeHtml(page.title)}</h2>
2465
+ <p class="epilogue-body">${escapeHtml(page.body)}</p>
2466
+ ${relation}
2467
+ ${habit}
2468
+ ${personalDepartureLedger}
2469
+ ${sharedNightAccordLedger}
2470
+ ${sharedFinaleMemoryLedger}
2471
+ ${allianceMemoryLedger}
2472
+ ${collapseMemoryLedger}
2473
+ ${intimacyArrangementLedger}
2474
+ ${favorReckoningLedger}
2475
+ ${routeReckoningLedger}
2476
+ ${nightConversationLedger}
2477
+ ${ordinaryNightLedger}
2478
+ ${invitationMemoryLedger}
2479
+ ${rivalryMemoryLedger}
2480
+ ${pairMemoryLedger}
2481
+ <p class="epilogue-route-note">${escapeHtml(page.routeNote)}</p>
2482
+ </div>
2483
+ <footer><button class="ink-button" id="btn-epilogue-prev" ${epilogueIndex === 0 ? 'disabled' : ''}>上一封</button><span>${pages.map((_, index) => `<i class="${index === epilogueIndex ? 'current' : ''}"></i>`).join('')}</span><button class="ink-button story-continue" id="btn-epilogue-next" ${epilogueIndex === pages.length - 1 ? 'disabled' : ''}>下一封</button></footer>
2484
+ </article>
2485
+ </section>`;
2486
+ app.appendChild(overlay);
2487
+ inertBackgroundExcept(overlay);
2488
+ focusSoon(epilogueIndex === pages.length - 1 ? '#btn-epilogue-close' : '#btn-epilogue-next');
2489
+ }
2490
+
2491
+ function archivedFateContext() {
2492
+ if (!archivedFateKey) return null;
2493
+ const entry = loadEndingArchive().find((row) => row.key === archivedFateKey);
2494
+ const variants = (entry?.fates ?? []).filter((fate) => (
2495
+ fate?.key
2496
+ && Array.isArray(fate.pages)
2497
+ && fate.pages.length === 3
2498
+ && fate.pages.every((page, index) => page?.page === index && page.count === 3)
2499
+ ));
2500
+ if (!variants.length) return null;
2501
+ archivedFateVariantIndex = Math.min(Math.max(0, archivedFateVariantIndex), variants.length - 1);
2502
+ archivedFatePageIndex = Math.min(Math.max(0, archivedFatePageIndex), variants[archivedFateVariantIndex].pages.length - 1);
2503
+ return { entry, variants, fate:variants[archivedFateVariantIndex] };
2504
+ }
2505
+
2506
+ function appendFateCoda() {
2507
+ const archive = archivedFateContext();
2508
+ const page = archive ? archive.fate.pages[archivedFatePageIndex] : E.currentFateCoda(state);
2509
+ if (!page) return;
2510
+ const options = archive ? [] : E.fateCodaOptions(state);
2511
+ const finalSections = page.finalSections.length
2512
+ ? `<div class="fate-coda-sections">${page.finalSections.map((section) => `<section class="fate-coda-ledger-section" aria-label="${escapeHtml(section.title)}">
2513
+ <header><span>${escapeHtml(section.kicker)}</span><h3>${escapeHtml(section.title)}</h3></header>
2514
+ <div>${section.entries.map((entry) => `<article><span>${escapeHtml(entry.label)}</span><b>${escapeHtml(entry.title)}</b><p>${escapeHtml(entry.text)}</p></article>`).join('')}</div>
2515
+ </section>`).join('')}</div>`
2516
+ : '';
2517
+ const combination = page.combination
2518
+ ? `<div class="fate-coda-combination"><span>两页合看</span><b>${escapeHtml(page.combination.title)}</b><p>${escapeHtml(page.combination.text)}</p></div>`
2519
+ : '';
2520
+ const settled = (archive || page.resolved) && page.result
2521
+ ? `<section class="fate-coda-result" aria-label="这一页的取舍已经落定"><span>这一页已经落定</span><b>${escapeHtml(page.result.title)}</b><p>${escapeHtml(page.result.body)}</p>${combination}</section>`
2522
+ : '';
2523
+ const decision = !archive && page.awaitingChoice
2524
+ ? `<section class="fate-coda-decision"><p>${escapeHtml(page.question)}</p><div class="choice-stack">${options.map((choice) => choiceButton(choice, 'fate-coda')).join('')}</div></section>`
2525
+ : settled;
2526
+ const isLast = page.page === page.count - 1;
2527
+ const variantNav = archive?.variants.length > 1
2528
+ ? `<nav class="fate-coda-variants" aria-label="同一结局的命数处置版本"><span>本结局另有不同的命数处置</span><div>${archive.variants.map((fate, index) => `<button class="${index === archivedFateVariantIndex ? 'current' : ''}" data-fate-archive-variant="${index}"><b>${index === archivedFateVariantIndex ? '当前余账' : '另一余账'}</b><small>${escapeHtml(fate.combination?.title ?? fate.summary)}</small></button>`).join('')}</div></nav>`
2529
+ : '';
2530
+ const footerAction = archive
2531
+ ? `<button class="ink-button" id="btn-fate-archive-prev" ${page.page === 0 ? 'disabled' : ''}>上一页</button><span>永久余账 · ${narrativeStep(page.page + 1, page.count)}</span><button class="ink-button story-continue" id="btn-fate-archive-next" ${isLast ? 'disabled' : ''}>下一页</button>`
2532
+ : page.awaitingChoice
2533
+ ? '<span>这里没有能把死亡改成成功的选项</span>'
2534
+ : isLast
2535
+ ? '<button class="ink-button story-continue" id="btn-fate-coda-finish">把余账留在这里</button>'
2536
+ : '<button class="ink-button story-continue" data-fate-coda-continue="1">翻下一页</button>';
2537
+ const overlay = document.createElement('div');
2538
+ overlay.className = 'fate-coda-overlay';
2539
+ overlay.id = 'fate-coda-modal';
2540
+ overlay.innerHTML = `<section class="fate-coda-book" role="dialog" aria-modal="true" aria-labelledby="fate-coda-title" data-fate-page="${page.page}" ${archive ? `data-archived-fate="${escapeHtml(archive.entry.key)}"` : ''}>
2541
+ <div class="fate-coda-art" style="background-image:url('${urlFor('cg/finale/fate_coda')}')" role="img" aria-label="黎明空宅里的总账、钥匙、私契、药碗与五张分开的名签"></div>
2542
+ <article class="fate-coda-copy">
2543
+ <header><div><p class="eyebrow">${archive ? '永久总账重看 · ' : ''}原著命数 · ${narrativeStep(page.page + 1, page.count)}</p><p>${escapeHtml(page.kicker)}</p></div><button class="plain-button" id="btn-fate-coda-close">${archive ? '回到结局总账' : '暂合余账'}</button></header>
2544
+ <div class="fate-coda-scroll">
2545
+ <div class="fate-coda-progress" aria-label="命数三页进度">${Array.from({ length: page.count }, (_, index) => `<i class="${index === page.page ? 'current' : index < page.page ? 'past' : ''}"><span>${['起', '转', '结'][index] ?? '续'}</span></i>`).join('')}</div>
2546
+ ${variantNav}
2547
+ <h2 id="fate-coda-title">${escapeHtml(page.title)}</h2>
2548
+ <p class="fate-coda-body">${escapeHtml(page.lead)}</p>
2549
+ ${finalSections}
2550
+ ${decision}
2551
+ </div>
2552
+ <footer>${footerAction}</footer>
2553
+ </article>
2554
+ </section>`;
2555
+ app.appendChild(overlay);
2556
+ inertBackgroundExcept(overlay);
2557
+ focusSoon(archive ? (isLast ? '#btn-fate-coda-close' : '#btn-fate-archive-next') : page.awaitingChoice ? '[data-fate-coda]' : isLast ? '#btn-fate-coda-finish' : '[data-fate-coda-continue]');
2558
+ }
2559
+
2560
+ function choiceButton(choice, dataName) {
2561
+ const id = choice.id;
2562
+ const focusableLock = choice.disabled && dataName === 'morning-settlement-choice';
2563
+ const disabled = choice.disabled && !focusableLock ? 'disabled' : '';
2564
+ const ariaDisabled = focusableLock ? 'aria-disabled="true"' : '';
2565
+ const locked = choice.disabled ? (choice.locked || choice.hint || '前事未到') : (choice.hint || '');
2566
+ const meta = narrativeChoiceMeta(choice.meta);
2567
+ const accessibleLabel = [choice.label || id, locked, meta].filter(Boolean).join(';');
2568
+ const title = meta ? ` title="${escapeHtml(meta)}"` : '';
2569
+ return `<button class="choice-button" data-${dataName}="${id}" ${disabled} ${ariaDisabled} aria-label="${escapeHtml(accessibleLabel)}"${title}><b>${escapeHtml(choice.label || id)}</b><span>${escapeHtml(locked)}</span></button>`;
2570
+ }
2571
+
2572
+ function appendResultCard() {
2573
+ const overlay = document.createElement('aside');
2574
+ overlay.className = 'result-feedback';
2575
+ overlay.id = 'result-feedback';
2576
+ overlay.setAttribute('aria-label', resultCard.title);
2577
+ overlay.innerHTML = `<section class="result-card">
2578
+ <p class="eyebrow">她听见了,也给了你回应</p>
2579
+ <h2>${escapeHtml(resultCard.title)}</h2>
2580
+ <p>${escapeHtml(resultCard.text)}</p>
2581
+ <button class="plain-button" id="btn-result-dismiss" aria-label="收起这条回应">收起</button>
2582
+ </section>`;
2583
+ app.appendChild(overlay);
2584
+ const announcer = document.getElementById('announcer');
2585
+ if (announcer) announcer.textContent = `${resultCard.title}。${resultCard.text}`;
2586
+ }
2587
+
2588
+ function appendGallery() {
2589
+ const unlocked = new Set(loadGallery());
2590
+ const endings = loadEndingArchive();
2591
+ const endingFamilies = new Set(endings.map((row) => row.family));
2592
+ const sceneRows = Object.values(SCENES);
2593
+ sceneRows.filter((scene) => unlocked.has(scene.id)).forEach((scene) => ensureNarrativeAsset(scene.asset));
2594
+ const groups = [
2595
+ ...HEROINE_IDS.map((id) => [HEROINES[id].name, sceneRows.filter((scene) => scene.heroine === id).map((scene) => scene.id)]),
2596
+ ['公开问责', sceneRows.filter((scene) => scene.tier === 'public').map((scene) => scene.id)],
2597
+ ['五人同灯', sceneRows.filter((scene) => ['ensemble', 'ensemble-intimate'].includes(scene.tier)).map((scene) => scene.id)],
2598
+ ].filter(([, ids]) => ids.length);
2599
+ const overlay = document.createElement('div');
2600
+ overlay.className = 'gallery-overlay';
2601
+ overlay.id = 'gallery-modal';
2602
+ overlay.innerHTML = `<section class="gallery-book" role="dialog" aria-modal="true" aria-label="场景册" ${gallerySceneId ? 'inert' : ''}>
2603
+ <header><div><p class="eyebrow">不会丢的结局与册页</p><h2>风月总账 <span>${endings.length ? '旧卷已收入总账' : '首卷尚未结页'} · ${unlocked.size ? '已有册页' : '题签尚未翻开'}</span></h2><p>换一套暗线不会抹掉已经走成的关系制度,也不会收回已经翻开的册页。</p></div><button class="plain-button" id="btn-gallery-close">${TEXT.close}</button></header>
2604
+ <section class="ending-archive" aria-label="结局总账">
2605
+ <div class="ending-goals">${ENDING_GOALS.map((goal) => `<article class="ending-goal ${endingFamilies.has(goal.id) ? 'complete' : ''}"><i>${endingFamilies.has(goal.id) ? '已' : '未'}</i><div><b>${goal.label}</b><small>${goal.hint}</small></div></article>`).join('')}</div>
2606
+ <div class="ending-records">${endings.length ? endings.map(endingArchiveCard).join('') : '<p>第一本总账还没有结页。二十日后,你留下的不是一个分数,而是一种她们是否愿意共同生活的办法。</p>'}</div>
2607
+ </section>
2608
+ <div class="gallery-grid">${groups.map(([name, ids]) => `<section><h3>${name}</h3><div>${ids.map((id) => galleryCard(SCENES[id], unlocked.has(id), gallerySceneVariants(id, endings).length)).join('')}</div></section>`).join('')}</div>
2609
+ </section>${gallerySceneId ? galleryReplay(SCENES[gallerySceneId], gallerySceneVariants(gallerySceneId, endings), galleryVariantIndex) : ''}`;
2610
+ app.appendChild(overlay);
2611
+ inertBackgroundExcept(overlay);
2612
+ focusSoon(gallerySceneId ? '#btn-gallery-replay-close' : '#btn-gallery-close');
2613
+ }
2614
+
2615
+ function endingArchiveCard(entry) {
2616
+ const family = ENDING_GOALS.find((row) => row.id === entry.family)?.label ?? '旧账结页';
2617
+ const seeds = Array.isArray(entry.seeds) && entry.seeds.length ? entry.seeds : [entry.seed].filter(Number.isInteger);
2618
+ const hasAfterstory = Array.isArray(entry.afterstory?.pages) && entry.afterstory.pages.length === HEROINE_IDS.length + 1;
2619
+ const fates = (entry.fates ?? []).filter((fate) => Array.isArray(fate?.pages) && fate.pages.length === 3);
2620
+ const content = `
2621
+ <p><span>${escapeHtml(family)}</span><small>${seeds.length > 1 ? '不同暗线都曾走到这里' : seeds.length ? '这一套暗线留下的旧局' : '旧局'}</small></p>
2622
+ <h3>${escapeHtml(entry.title)}${entry.cast ? `<em>${escapeHtml(entry.cast)}</em>` : ''}</h3>
2623
+ <b>${escapeHtml(entry.tag || '')}</b>
2624
+ <small>${escapeHtml(entry.text)}</small>`;
2625
+ const actions = [
2626
+ hasAfterstory ? `<button data-ending-open="${escapeHtml(entry.key)}">重读五封一月笺与末页</button>` : '',
2627
+ fates.length ? `<button data-fate-ending-open="${escapeHtml(entry.key)}">重读命数三页 · 另有处置</button>` : '',
2628
+ ].filter(Boolean).join('');
2629
+ return `<article class="ending-record" data-ending-record="${escapeHtml(entry.key)}">${content}${actions ? `<div class="ending-record-actions">${actions}</div>` : ''}</article>`;
2630
+ }
2631
+
2632
+ function gallerySceneVariants(sceneId, endings) {
2633
+ const variants = new Map();
2634
+ for (const ending of endings) {
2635
+ const record = ending?.scenes?.[sceneId];
2636
+ if (!record || record.scene !== sceneId || typeof record.key !== 'string' || !Array.isArray(record.pages) || !record.pages.length) continue;
2637
+ if (variants.has(record.key)) continue;
2638
+ variants.set(record.key, {
2639
+ ...record,
2640
+ endingKey:ending.key,
2641
+ endingTitle:ending.title,
2642
+ endingCast:ending.cast,
2643
+ seeds:Array.isArray(ending.seeds) ? ending.seeds : [ending.seed].filter(Number.isInteger),
2644
+ });
2645
+ }
2646
+ return [...variants.values()];
2647
+ }
2648
+
2649
+ function galleryCard(scene, open, variantCount = 0) {
2650
+ const archiveCount = variantCount ? ' · 另有真实版本' : '';
2651
+ return `<button class="gallery-card ${open ? 'unlocked' : 'locked'}" data-gallery-scene="${scene.id}" ${open ? `data-gallery-open="${scene.id}"` : 'disabled'}>
2652
+ ${open ? `<img src="${urlFor(scene.asset)}" alt="${scene.title}"/>` : '<div class="locked-art" aria-label="未解锁">未</div>'}
2653
+ <div><b>${open ? scene.title : '题签未开'}</b><small>${open ? `${scene.tier === 'public' ? '公开问责' : scene.tier === 'ensemble' ? '五院共约' : scene.tier === 'ensemble-intimate' ? '五院余夜' : scene.tier === 'explicit' ? '那夜留宿' : '帘前一步'}${archiveCount}` : lockedHint(scene.id)}</small></div>
2654
+ </button>`;
2655
+ }
2656
+
2657
+ function galleryArchivePage(page, index) {
2658
+ const details = Array.isArray(page.details) && page.details.length
2659
+ ? `<ul class="gallery-archive-details">${page.details.map((detail) => `<li>${escapeHtml(detail)}</li>`).join('')}</ul>` : '';
2660
+ const actions = Array.isArray(page.actions) && page.actions.length
2661
+ ? `<section class="gallery-archive-actions" aria-label="本页具名动作"><h4>具名动作</h4>${page.actions.map((action) => `<p${action.heroine ? ` data-heroine="${escapeHtml(action.heroine)}"` : ''}><b>${escapeHtml(action.label)}</b><span>${escapeHtml(action.text)}</span></p>`).join('')}</section>` : '';
2662
+ const nonmembers = Array.isArray(page.nonmembers) && page.nonmembers.length
2663
+ ? `<section class="gallery-archive-nonmembers" aria-label="未入席者边界"><h4>没有被补画进成员席的人</h4>${page.nonmembers.map((entry) => `<p data-heroine="${escapeHtml(entry.heroine)}"><b>${escapeHtml(entry.label)} · ${escapeHtml(entry.status)}</b><span>${escapeHtml(entry.object ? `${entry.object}。${entry.text}` : entry.text)}</span></p>`).join('')}</section>` : '';
2664
+ return `<article class="gallery-archive-page" data-gallery-archive-page="${index}">
2665
+ <p class="gallery-archive-kicker">${escapeHtml(page.kicker)}</p>
2666
+ <h3>${escapeHtml(page.title)}</h3>
2667
+ <p>${escapeHtml(page.text)}</p>
2668
+ ${details}${actions}${nonmembers}
2669
+ </article>`;
2670
+ }
2671
+
2672
+ function galleryReplay(scene, variants = [], selectedIndex = 0) {
2673
+ const index = variants.length ? Math.min(Math.max(selectedIndex, 0), variants.length - 1) : 0;
2674
+ galleryVariantIndex = index;
2675
+ const variant = variants[index] ?? null;
2676
+ const variantNav = variants.length > 1
2677
+ ? `<nav class="gallery-variant-nav" aria-label="真实路线版本">${variants.map((entry, variantIndex) => `<button class="plain-button ${variantIndex === index ? 'active' : ''}" data-gallery-variant="${variantIndex}" aria-pressed="${variantIndex === index}">${escapeHtml(entry.title)}</button>`).join('')}</nav>` : '';
2678
+ const archivedCopy = variant ? `
2679
+ <p class="eyebrow">真实路线档案 · ${escapeHtml(variant.endingTitle)}</p>
2680
+ <h2>${escapeHtml(variant.title)}</h2>
2681
+ <p class="gallery-archive-summary">${escapeHtml(variant.summary)}</p>
2682
+ ${variantNav}
2683
+ <div class="gallery-archive-pages">${variant.pages.map(galleryArchivePage).join('')}</div>
2684
+ <p class="replay-note">这是本局真实走成的成员、答复、物件与动作;重看不会改写已经走过的路。</p>` : `
2685
+ <p class="eyebrow">${scene.tier === 'public' ? '再看公开问责' : scene.tier === 'ensemble' ? '再看五院同灯' : scene.tier === 'ensemble-intimate' ? '18+ · 再看灯下余夜' : '18+ · 翻回那一夜'}</p>
2686
+ <h2>${escapeHtml(scene.title)}</h2>
2687
+ <p>${escapeHtml(scene.body)}</p>
2688
+ <p class="replay-note">这页没有结局动态档案;重看只翻静态册页,不改已经走过的路。</p>`;
2689
+ return `<article class="gallery-replay" id="gallery-replay" data-replay-scene="${scene.id}" role="dialog" aria-modal="true" aria-label="重看${scene.title}">
2690
+ <img id="gallery-replay-image" src="${urlFor(scene.asset)}" alt="${scene.title}"/>
2691
+ <div class="gallery-replay-copy">
2692
+ ${archivedCopy}
2693
+ <button class="ink-button story-continue" id="btn-gallery-replay-close">合上这一页</button>
2694
+ </div>
2695
+ </article>`;
2696
+ }
2697
+
2698
+ function lockedHint(sceneId) {
2699
+ if (SCENES[sceneId]?.tier === 'public') return '等对应的公开问责落席';
2700
+ if (sceneId === 'inner_court_accord') return '听完五条院约,再请五人同席';
2701
+ if (sceneId === 'inner_court_afterglow') return '共同办完外账,再把三拍余夜走完';
2702
+ if (sceneId.startsWith('yue_')) return '先把答应月娘的事办了';
2703
+ if (sceneId.startsWith('pan_')) return '先还金莲那杯酒';
2704
+ if (sceneId.startsWith('pinger_')) return '先护住瓶儿的账';
2705
+ if (sceneId.startsWith('meng_')) return '先把玉楼的条件和功劳写清';
2706
+ return '先当众还雪娥管灶与作证的权限';
2707
+ }
2708
+
2709
+ function showToast(text) {
2710
+ let node = document.getElementById('toast');
2711
+ if (!node) {
2712
+ node = document.createElement('div');
2713
+ node.id = 'toast';
2714
+ node.className = 'toast';
2715
+ app.appendChild(node);
2716
+ }
2717
+ node.textContent = text;
2718
+ const announcer = document.getElementById('announcer');
2719
+ if (announcer) announcer.textContent = text;
2720
+ node.classList.add('show');
2721
+ clearTimeout(toastTimer);
2722
+ toastTimer = setTimeout(() => node.classList.remove('show'), FAST ? 150 : 2200);
2723
+ }
2724
+
2725
+ function escapeHtml(value) {
2726
+ return String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' })[char]);
2727
+ }
2728
+
2729
+ app.addEventListener('click', async (event) => {
2730
+ const button = event.target.closest('button');
2731
+ if (!button) return;
2732
+ if (button.getAttribute('aria-disabled') === 'true') {
2733
+ const reason = button.querySelector('span')?.textContent?.trim() || '前事未到,这项选择暂不可用。';
2734
+ showToast(reason);
2735
+ const announcer = document.getElementById('announcer');
2736
+ if (announcer) announcer.textContent = reason;
2737
+ return;
2738
+ }
2739
+ const actionLabel = button.querySelector('b')?.textContent?.trim() || button.textContent.trim();
2740
+ if (!audioReady) { audioReady = true; audio.unlock(); audio.playBGM(bgmCue()); }
2741
+ audio.sfx('click');
2742
+
2743
+ if (button.id === 'btn-result-dismiss') { resultCard = null; render(); focusSoon(firstPhaseActionSelector()); }
2744
+ else if (button.id === 'btn-age-yes') {
2745
+ sessionStorage.setItem(AGE_KEY, 'yes');
2746
+ await bootPromise;
2747
+ scheduleSafePreload();
2748
+ render();
2749
+ focusSoon('#btn-start');
2750
+ } else if (button.id === 'btn-age-no') {
2751
+ app.innerHTML = '<main class="age-gate"><h1>这道门不往里开</h1><p>本作只供成年人。你可以直接关闭页面。</p></main>';
2752
+ } else if (button.id === 'btn-start') {
2753
+ if (loadSave()) { newGameConfirmOpen = true; render(); }
2754
+ else { startNew(); focusSoon(firstPhaseActionSelector()); }
2755
+ } else if (button.id === 'btn-confirm-new') { startNew(); focusSoon(firstPhaseActionSelector()); }
2756
+ else if (button.id === 'btn-cancel-new') { newGameConfirmOpen = false; render(); focusSoon('#btn-start'); }
2757
+ else if (button.id === 'btn-continue') continueGame();
2758
+ else if (button.id === 'btn-gallery') { galleryReturnSelector = selectorForButton(button); galleryOpen = true; gallerySceneId = null; galleryVariantIndex = 0; archivedEpilogueKey = null; archivedFateKey = null; render(); }
2759
+ else if (button.id === 'btn-fate-coda') {
2760
+ fateCodaReturnSelector = selectorForButton(button);
2761
+ epilogueOpen = false;
2762
+ archivedFateKey = null;
2763
+ archivedFateVariantIndex = 0;
2764
+ archivedFatePageIndex = 0;
2765
+ if (!E.currentFateCoda(state)) {
2766
+ const result = E.startFateCoda(state);
2767
+ if (!result.ok) return showToast(result.error);
2768
+ save();
2769
+ }
2770
+ fateCodaOpen = true;
2771
+ render();
2772
+ }
2773
+ else if (button.id === 'btn-fate-coda-close' || button.id === 'btn-fate-coda-finish') { const target = fateCodaReturnSelector || '#btn-fate-coda'; fateCodaOpen = false; archivedFateKey = null; archivedFateVariantIndex = 0; archivedFatePageIndex = 0; render(); focusSoon(target); }
2774
+ else if (button.id === 'btn-epilogue') { fateCodaOpen = false; archivedEpilogueKey = null; epilogueReturnSelector = selectorForButton(button); epilogueOpen = true; epilogueIndex = 0; render(); }
2775
+ else if (button.id === 'btn-epilogue-close') { const target = epilogueReturnSelector || '#btn-epilogue'; epilogueOpen = false; archivedEpilogueKey = null; render(); focusSoon(target); }
2776
+ else if (button.id === 'btn-epilogue-prev') { epilogueIndex = Math.max(0, epilogueIndex - 1); render(); }
2777
+ else if (button.id === 'btn-epilogue-next') { epilogueIndex = Math.min(epiloguePages().length - 1, epilogueIndex + 1); render(); }
2778
+ else if (button.id === 'btn-roster') { rosterOpen = true; render(); }
2779
+ else if (button.id === 'btn-roster-close') { rosterOpen = false; render(); focusSoon('#btn-roster'); }
2780
+ else if (button.id === 'btn-gallery-close') { galleryOpen = false; gallerySceneId = null; galleryVariantIndex = 0; archivedEpilogueKey = null; archivedFateKey = null; render(); focusSoon(galleryReturnSelector || '#btn-gallery'); }
2781
+ else if (button.id === 'btn-gallery-replay-close') { const closed = gallerySceneId; gallerySceneId = null; galleryVariantIndex = 0; render(); focusSoon(`[data-gallery-open="${CSS.escape(closed)}"]`); }
2782
+ else if (button.dataset.galleryVariant !== undefined) { galleryVariantIndex = Number(button.dataset.galleryVariant) || 0; render(); }
2783
+ else if (button.dataset.galleryOpen) { gallerySceneId = button.dataset.galleryOpen; galleryVariantIndex = 0; render(); }
2784
+ else if (button.dataset.endingOpen) { archivedFateKey = null; archivedEpilogueKey = button.dataset.endingOpen; epilogueReturnSelector = selectorForButton(button); epilogueIndex = 0; epilogueOpen = true; render(); }
2785
+ else if (button.dataset.fateEndingOpen) { archivedEpilogueKey = null; archivedFateKey = button.dataset.fateEndingOpen; archivedFateVariantIndex = 0; archivedFatePageIndex = 0; fateCodaReturnSelector = selectorForButton(button); fateCodaOpen = true; render(); }
2786
+ else if (button.id === 'btn-fate-archive-prev') { archivedFatePageIndex = Math.max(0, archivedFatePageIndex - 1); render(); }
2787
+ else if (button.id === 'btn-fate-archive-next') { const archive = archivedFateContext(); archivedFatePageIndex = Math.min((archive?.fate.pages.length ?? 1) - 1, archivedFatePageIndex + 1); render(); }
2788
+ else if (button.dataset.fateArchiveVariant !== undefined) { archivedFateVariantIndex = Number(button.dataset.fateArchiveVariant) || 0; archivedFatePageIndex = 0; render(); }
2789
+ else if (button.id === 'btn-mute') { audio.unlock(); audio.toggleMuted(); render(); }
2790
+ else if (button.id === 'btn-secret-clear') {
2791
+ const result = E.selectSecret(state, null); if (!result.ok) showToast(result.error); else { save(); render(); focusSoon('[data-day-action="office"]'); }
2792
+ }
2793
+ else if (button.dataset.secretSelect) {
2794
+ const secretId = button.dataset.secretSelect; const result = E.selectSecret(state, secretId);
2795
+ if (!result.ok) showToast(result.error); else { save(); render(); focusSoon(`[data-secret-select="${CSS.escape(secretId)}"]`); }
2796
+ }
2797
+ else if (button.id === 'btn-restart') { restart(); focusSoon(firstPhaseActionSelector()); }
2798
+ else if (button.id === 'btn-replay-seed') { restart(true); focusSoon(firstPhaseActionSelector()); }
2799
+ else if (button.id === 'btn-scene-close') act(() => E.closeScene(state), actionLabel);
2800
+ else if (button.dataset.opening) act(() => E.chooseOpening(state, button.dataset.opening), actionLabel);
2801
+ else if (button.dataset.openingAftermathContinue) act(() => E.advanceOpeningAftermath(state), actionLabel);
2802
+ else if (button.dataset.houseCrisisReply) act(() => E.advanceHouseCrisisReply(state), actionLabel);
2803
+ else if (button.dataset.houseCrisis) act(() => E.resolveHouseCrisis(state, button.dataset.houseCrisis), actionLabel);
2804
+ else if (button.dataset.houseCrisisAftermath) act(() => E.resolveHouseCrisisAftermath(state, button.dataset.houseCrisisAftermath), actionLabel);
2805
+ else if (button.dataset.houseCrisisAftermathContinue) act(() => E.advanceHouseCrisisAftermath(state), actionLabel);
2806
+ else if (button.dataset.pairStory) act(() => E.advancePairInterlude(state), actionLabel);
2807
+ else if (button.dataset.pairInterlude) act(() => E.resolvePairInterlude(state, button.dataset.pairInterlude), actionLabel);
2808
+ else if (button.dataset.favorStory) act(() => E.advanceFavorReckoning(state), actionLabel);
2809
+ else if (button.dataset.favorReckoning) act(() => E.resolveFavorReckoning(state, button.dataset.favorReckoning), actionLabel);
2810
+ else if (button.dataset.memoryStory) act(() => E.advanceMemoryReckoning(state), actionLabel);
2811
+ else if (button.dataset.memoryReckoning) act(() => E.resolveMemoryReckoning(state, button.dataset.memoryReckoning), actionLabel);
2812
+ else if (button.dataset.duskInvitation) act(() => E.resolveDuskInvitation(state, button.dataset.duskInvitation), actionLabel);
2813
+ else if (button.dataset.duskInvitationAftermath) act(() => E.resolveDuskInvitationAftermath(state, button.dataset.duskInvitationAftermath), actionLabel);
2814
+ else if (button.dataset.duskInvitationAftermathContinue) act(() => E.advanceDuskInvitationAftermath(state), actionLabel);
2815
+ else if (button.dataset.actTransition) act(() => E.resolveActTransition(state, button.dataset.actTransition), actionLabel);
2816
+ else if (button.dataset.actAftermathChoice) act(() => E.resolveActAftermath(state, button.dataset.actAftermathChoice), actionLabel);
2817
+ else if (button.dataset.actAftermathContinue) act(() => E.advanceActAftermath(state), actionLabel);
2818
+ else if (button.dataset.dayAction) act(() => E.chooseDayAction(state, button.dataset.dayAction), actionLabel);
2819
+ else if (button.dataset.dayAftermathContinue) act(() => E.advanceDayAftermath(state), actionLabel);
2820
+ else if (button.dataset.jointAction) act(() => E.chooseJointAction(state, button.dataset.jointAction), actionLabel);
2821
+ else if (button.dataset.jointContinue) act(() => E.continueJointAction(state), actionLabel);
2822
+ else if (button.dataset.portablePrecedentContinue) act(() => E.advancePortablePrecedent(state), actionLabel);
2823
+ else if (button.dataset.portablePrecedentChoice) act(() => E.choosePortablePrecedent(state, button.dataset.portablePrecedentChoice), actionLabel);
2824
+ else if (button.dataset.household) act(() => E.resolveHouseholdEvent(state, button.dataset.household), actionLabel);
2825
+ else if (button.dataset.householdAftermath) act(() => E.resolveHouseholdAftermath(state, button.dataset.householdAftermath), actionLabel);
2826
+ else if (button.dataset.householdAftermathContinue) act(() => E.advanceHouseholdAftermath(state), actionLabel);
2827
+ else if (button.dataset.council) act(() => E.resolveCouncil(state, button.dataset.council), actionLabel);
2828
+ else if (button.dataset.councilAftermathContinue) act(() => E.advanceCouncilAftermath(state), actionLabel);
2829
+ else if (button.dataset.banquet) act(() => E.chooseBanquet(state, button.dataset.banquet), actionLabel);
2830
+ else if (button.dataset.publicEvidence) act(() => E.choosePublicEvidence(state, button.dataset.publicEvidence), actionLabel);
2831
+ else if (button.dataset.publicEvidenceComplete) act(() => E.completePublicEvidence(state), actionLabel);
2832
+ else if (button.dataset.publicFollowup) act(() => E.resolvePublicFollowup(state, button.dataset.publicFollowup), actionLabel);
2833
+ else if (button.dataset.publicAftermathContinue) act(() => E.advancePublicAftermath(state), actionLabel);
2834
+ else if (button.dataset.fivePriceContinue) act(() => E.advanceFivePrivatePrices(state), actionLabel);
2835
+ else if (button.dataset.fivePriceProtocol) act(() => E.chooseFivePrivatePriceProtocol(state, button.dataset.fivePriceProtocol), actionLabel);
2836
+ else if (button.dataset.fivePriceRight) act(() => E.chooseFivePrivatePriceRight(state, button.dataset.fivePriceRight), actionLabel);
2837
+ else if (button.dataset.finalReckoning) act(() => E.resolveFinalReckoning(state, button.dataset.finalReckoning), actionLabel);
2838
+ else if (button.dataset.finalReckoningAftermath) act(() => E.resolveFinalReckoningAftermath(state, button.dataset.finalReckoningAftermath), actionLabel);
2839
+ else if (button.dataset.finalAftermathContinue) act(() => E.advanceFinalReckoningAftermath(state), actionLabel);
2840
+ else if (button.dataset.personalFinaleResultContinue) act(() => E.continuePersonalFinaleResult(state), actionLabel);
2841
+ else if (button.dataset.personalFinale) act(() => E.choosePersonalFinale(state, button.dataset.personalFinale), actionLabel);
2842
+ else if (button.dataset.allianceStart) act(() => E.startAllianceNight(state), actionLabel);
2843
+ else if (button.dataset.allianceAssemblyContinue) act(() => E.advanceAllianceAssembly(state), actionLabel);
2844
+ else if (button.dataset.allianceResultContinue) act(() => E.continueAllianceNightResult(state), actionLabel);
2845
+ else if (button.dataset.allianceNight) act(() => E.chooseAllianceNight(state, button.dataset.allianceNight), actionLabel);
2846
+ else if (button.dataset.sharedStart) act(() => E.startSharedNight(state), actionLabel);
2847
+ else if (button.dataset.sharedNight) act(() => E.chooseSharedNight(state, button.dataset.sharedNight), actionLabel);
2848
+ else if (button.dataset.sharedAfterglowResultContinue) act(() => E.continueSharedAfterglowResult(state), actionLabel);
2849
+ else if (button.dataset.sharedAfterglow) act(() => E.chooseSharedAfterglow(state, button.dataset.sharedAfterglow), actionLabel);
2850
+ else if (button.dataset.sharedDawn) act(() => E.chooseSharedDawn(state, button.dataset.sharedDawn), actionLabel);
2851
+ else if (button.dataset.sharedDawnResultContinue) act(() => E.continueSharedDawnResult(state), actionLabel);
2852
+ else if (button.dataset.collapseContinue) act(() => E.advanceCollapseFinale(state), actionLabel);
2853
+ else if (button.dataset.collapseChoice) act(() => E.chooseCollapseFinale(state, button.dataset.collapseChoice), actionLabel);
2854
+ else if (button.dataset.collapseResultContinue) act(() => E.continueCollapseFinaleResult(state), actionLabel);
2855
+ else if (button.dataset.fateCoda) act(() => E.chooseFateCoda(state, button.dataset.fateCoda), actionLabel);
2856
+ else if (button.dataset.fateCodaContinue) act(() => E.advanceFateCoda(state), actionLabel);
2857
+ else if (button.dataset.visit) act(() => E.startVisit(state, button.dataset.visit), actionLabel);
2858
+ else if (button.dataset.routeChoice) act(() => E.chooseVisit(state, button.dataset.routeChoice), actionLabel);
2859
+ else if (button.dataset.routeStory) act(() => E.advanceRouteAftermath(state), actionLabel);
2860
+ else if (button.dataset.nightCodaContinue) act(() => E.advanceOrdinaryNightCoda(state), actionLabel);
2861
+ else if (button.dataset.nightStory) act(() => E.advanceNightConversation(state), actionLabel);
2862
+ else if (button.dataset.nightConversation) act(() => E.chooseNightConversation(state, button.dataset.nightConversation), actionLabel);
2863
+ else if (button.dataset.nightConversationResult) act(() => E.continueNightConversation(state), actionLabel);
2864
+ else if (button.dataset.morningSettlementContinue) act(() => E.advanceMorningSettlement(state), actionLabel);
2865
+ else if (button.dataset.morningSettlementChoice) act(() => E.chooseMorningSettlement(state, button.dataset.morningSettlementChoice), actionLabel);
2866
+ else if (button.dataset.morningStory) act(() => E.advanceMorningStory(state), actionLabel);
2867
+ else if (button.dataset.morningResolution) act(() => E.continueMorningResolution(state), actionLabel);
2868
+ else if (button.dataset.routeAftermath) act(() => E.resolveRouteAftermath(state, button.dataset.routeAftermath), actionLabel);
2869
+ else if (button.dataset.night) act(() => E.chooseNight(state, button.dataset.night), actionLabel);
2870
+ else if (button.dataset.personalAfterglowAftermath) act(() => E.resolvePersonalAfterglowAftermath(state, button.dataset.personalAfterglowAftermath), actionLabel);
2871
+ else if (button.dataset.personalAfterglowAftermathContinue) act(() => E.advancePersonalAfterglowAftermath(state), actionLabel);
2872
+ else if (button.dataset.personalAfterglow) act(() => E.choosePersonalAfterglow(state, button.dataset.personalAfterglow), actionLabel);
2873
+ else if (button.dataset.morning) act(() => E.resolveMorning(state, button.dataset.morning), actionLabel);
2874
+ });
2875
+
2876
+ document.addEventListener('keydown', (event) => {
2877
+ const dialog = document.querySelector('#gallery-replay, #new-game-confirm .confirm-card, #roster-modal .roster-sheet, #gallery-modal .gallery-book, #epilogue-modal .epilogue-book, #fate-coda-modal .fate-coda-book');
2878
+ if (event.key === 'Tab' && dialog) {
2879
+ const focusable = [...dialog.querySelectorAll('button:not([disabled]), [href], [tabindex]:not([tabindex="-1"])')];
2880
+ if (!focusable.length) return;
2881
+ const first = focusable[0]; const last = focusable.at(-1);
2882
+ if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
2883
+ else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
2884
+ return;
2885
+ }
2886
+ if (event.key !== 'Escape') return;
2887
+ if (newGameConfirmOpen) {
2888
+ event.preventDefault(); newGameConfirmOpen = false; render(); focusSoon('#btn-start');
2889
+ } else if (fateCodaOpen) {
2890
+ event.preventDefault();
2891
+ const target = fateCodaReturnSelector || '#btn-fate-coda';
2892
+ fateCodaOpen = false; archivedFateKey = null; archivedFateVariantIndex = 0; archivedFatePageIndex = 0; render(); focusSoon(target);
2893
+ } else if (epilogueOpen) {
2894
+ event.preventDefault();
2895
+ const target = epilogueReturnSelector || '#btn-epilogue';
2896
+ epilogueOpen = false; archivedEpilogueKey = null; render(); focusSoon(target);
2897
+ } else if (rosterOpen) {
2898
+ event.preventDefault(); rosterOpen = false; render(); focusSoon('#btn-roster');
2899
+ } else if (galleryOpen) {
2900
+ event.preventDefault();
2901
+ if (gallerySceneId) {
2902
+ const closed = gallerySceneId; gallerySceneId = null; galleryVariantIndex = 0; render(); focusSoon(`[data-gallery-open="${CSS.escape(closed)}"]`);
2903
+ } else {
2904
+ galleryOpen = false; galleryVariantIndex = 0; render(); focusSoon(galleryReturnSelector || '#btn-gallery');
2905
+ }
2906
+ }
2907
+ });
2908
+
2909
+ window.__game = Object.freeze({
2910
+ state: () => state ? E.snapshot(state) : null,
2911
+ activeObligations: () => state ? E.activeObligations(state) : [],
2912
+ dayDef: () => state ? E.dayDef(state) : null,
2913
+ pressurePlan: () => state ? { current: E.dayPressureRule(state), next: state.day < E.MAX_DAY ? E.dayPressureRule(state, state.day + 1) : null } : null,
2914
+ dayOptions: () => state ? E.dayOptions(state) : [],
2915
+ dayAftermath: () => state ? E.currentDayAftermath(state) : null,
2916
+ fateCoda: () => state ? E.currentFateCoda(state) : null,
2917
+ nightConversation: () => state ? E.currentNightConversation(state) : null,
2918
+ nightCoda: () => state ? E.currentOrdinaryNightCoda(state) : null,
2919
+ householdOptions: () => state ? E.householdOptions(state) : [],
2920
+ householdEvent: () => state ? E.currentHouseholdEvent(state) : null,
2921
+ householdAftermath: () => state ? E.currentHouseholdAftermath(state) : null,
2922
+ householdAftermathOptions: () => state ? E.householdAftermathOptions(state) : [],
2923
+ jiaoerFloorDisposition: () => state ? E.recordedJiaoerFloorDisposition(state) : null,
2924
+ jiaoerDeedDisposition: () => state ? E.recordedJiaoerDeedDisposition(state) : null,
2925
+ jiaoerLedger: () => state ? E.jiaoerLedger(state) : null,
2926
+ councilOptions: () => state ? E.councilOptions(state) : [],
2927
+ councilAftermath: () => state ? E.currentCouncilAftermath(state) : null,
2928
+ publicFollowupOptions: () => state ? E.publicFollowupOptions(state) : [],
2929
+ publicAftermath: () => state ? E.currentPublicAftermath(state) : null,
2930
+ secretInventory: () => state ? E.secretInventory(state) : [],
2931
+ gallery: () => loadGallery(),
2932
+ endingArchive: () => loadEndingArchive(),
2933
+ assets: () => assetReport(),
2934
+ preloadAllAssets: () => loadAssets(Object.keys(ASSET_PATHS)),
2935
+ assetPaths: ASSET_PATHS,
2936
+ saveKey: SAVE_KEY,
2937
+ seed: SEED,
2938
+ audioCue: () => bgmCue(),
2939
+ audioScene: () => audio.currentScene,
2940
+ newGame: () => startNew(),
2941
+ restart,
2942
+ replaySeed: () => restart(true),
2943
+ chooseOpening: (id) => act(() => E.chooseOpening(state, id)),
2944
+ openingAftermath: () => state ? E.currentOpeningAftermath(state) : null,
2945
+ advanceOpeningAftermath: () => act(() => E.advanceOpeningAftermath(state)),
2946
+ currentHouseCrisis: () => state ? E.currentHouseCrisis(state) : null,
2947
+ houseCrisis: (id) => act(() => E.resolveHouseCrisis(state, id)),
2948
+ advanceHouseCrisisReply: () => act(() => E.advanceHouseCrisisReply(state)),
2949
+ houseCrisisAftermath: () => state ? E.currentHouseCrisisAftermath(state) : null,
2950
+ houseCrisisAftermathOptions: () => state ? E.houseCrisisAftermathOptions(state) : [],
2951
+ chooseHouseCrisisAftermath: (id) => act(() => E.resolveHouseCrisisAftermath(state, id)),
2952
+ advanceHouseCrisisAftermath: () => act(() => E.advanceHouseCrisisAftermath(state)),
2953
+ advancePairInterlude: () => act(() => E.advancePairInterlude(state)),
2954
+ pairInterlude: (id) => act(() => E.resolvePairInterlude(state, id)),
2955
+ currentFavorReckoning: () => state ? E.currentFavorReckoning(state) : null,
2956
+ favorReckoningOptions: () => state ? E.favorReckoningOptions(state) : [],
2957
+ advanceFavorReckoning: () => act(() => E.advanceFavorReckoning(state)),
2958
+ favorReckoning: (id) => act(() => E.resolveFavorReckoning(state, id)),
2959
+ currentMemoryReckoning: () => state ? E.currentMemoryReckoning(state) : null,
2960
+ memoryReckoningOptions: () => state ? E.memoryReckoningOptions(state) : [],
2961
+ advanceMemoryReckoning: () => act(() => E.advanceMemoryReckoning(state)),
2962
+ memoryReckoning: (id) => act(() => E.resolveMemoryReckoning(state, id)),
2963
+ duskInvitation: (id) => act(() => E.resolveDuskInvitation(state, id)),
2964
+ duskInvitationAftermath: () => state ? E.currentDuskInvitationAftermath(state) : null,
2965
+ duskInvitationAftermathOptions: () => state ? E.duskInvitationAftermathOptions(state) : [],
2966
+ chooseDuskInvitationAftermath: (id) => act(() => E.resolveDuskInvitationAftermath(state, id)),
2967
+ advanceDuskInvitationAftermath: () => act(() => E.advanceDuskInvitationAftermath(state)),
2968
+ actTransitionOptions: () => state ? E.actTransitionOptions(state) : [],
2969
+ actTransition: (id) => act(() => E.resolveActTransition(state, id)),
2970
+ actAftermath: () => state ? E.currentActAftermath(state) : null,
2971
+ actAftermathOptions: () => state ? E.actAftermathOptions(state) : [],
2972
+ chooseActAftermath: (id) => act(() => E.resolveActAftermath(state, id)),
2973
+ advanceActAftermath: () => act(() => E.advanceActAftermath(state)),
2974
+ chooseDay: (id) => act(() => E.chooseDayAction(state, id)),
2975
+ morningSettlement: () => state ? E.currentMorningSettlement(state) : null,
2976
+ morningSettlementOptions: () => state ? E.morningSettlementOptions(state) : [],
2977
+ chooseMorningSettlement: (id) => act(() => E.chooseMorningSettlement(state, id)),
2978
+ advanceMorningSettlement: () => act(() => E.advanceMorningSettlement(state)),
2979
+ chooseJointAction: (id) => act(() => E.chooseJointAction(state, id)),
2980
+ continueJointAction: () => act(() => E.continueJointAction(state)),
2981
+ portablePrecedent: () => state ? E.currentPortablePrecedent(state) : null,
2982
+ portablePrecedentOptions: () => state ? E.portablePrecedentOptions(state) : [],
2983
+ choosePortablePrecedent: (id) => act(() => E.choosePortablePrecedent(state, id)),
2984
+ advancePortablePrecedent: () => act(() => E.advancePortablePrecedent(state)),
2985
+ recordedPortablePrecedent: () => state ? E.recordedPortablePrecedent(state) : null,
2986
+ household: (id) => act(() => E.resolveHouseholdEvent(state, id)),
2987
+ chooseHouseholdAftermath: (id) => act(() => E.resolveHouseholdAftermath(state, id)),
2988
+ advanceHouseholdAftermath: () => act(() => E.advanceHouseholdAftermath(state)),
2989
+ council: (id) => act(() => E.resolveCouncil(state, id)),
2990
+ chooseBanquet: (id) => act(() => E.chooseBanquet(state, id)),
2991
+ publicEvidence: () => state ? E.currentPublicEvidence(state) : null,
2992
+ publicEvidenceOptions: () => state ? E.publicEvidenceOptions(state) : [],
2993
+ choosePublicEvidence: (id) => act(() => E.choosePublicEvidence(state, id)),
2994
+ completePublicEvidence: () => act(() => E.completePublicEvidence(state)),
2995
+ publicFollowup: (id) => act(() => E.resolvePublicFollowup(state, id)),
2996
+ advancePublicAftermath: () => act(() => E.advancePublicAftermath(state)),
2997
+ fivePrivatePrices: () => state ? E.currentFivePrivatePrices(state) : null,
2998
+ fivePrivatePriceOptions: () => state ? E.fivePrivatePriceOptions(state) : [],
2999
+ chooseFivePrivatePriceProtocol: (id) => act(() => E.chooseFivePrivatePriceProtocol(state, id)),
3000
+ chooseFivePrivatePriceRight: (id) => act(() => E.chooseFivePrivatePriceRight(state, id)),
3001
+ advanceFivePrivatePrices: () => act(() => E.advanceFivePrivatePrices(state)),
3002
+ finalReckoningOptions: () => state ? E.finalReckoningOptions(state) : [],
3003
+ currentFinalReckoning: () => state ? E.currentFinalReckoning(state) : null,
3004
+ finalReckoning: (id) => act(() => E.resolveFinalReckoning(state, id)),
3005
+ finalReckoningAftermath: () => state ? E.currentFinalReckoningAftermath(state) : null,
3006
+ finalReckoningAftermathOptions: () => state ? E.finalReckoningAftermathOptions(state) : [],
3007
+ chooseFinalReckoningAftermath: (id) => act(() => E.resolveFinalReckoningAftermath(state, id)),
3008
+ advanceFinalReckoningAftermath: () => act(() => E.advanceFinalReckoningAftermath(state)),
3009
+ personalFinaleBeat: () => state ? E.personalFinaleBeat(state) : null,
3010
+ personalFinaleResult: () => state ? E.currentPersonalFinaleResult(state) : null,
3011
+ choosePersonalFinale: (id) => act(() => E.choosePersonalFinale(state, id)),
3012
+ continuePersonalFinaleResult: () => act(() => E.continuePersonalFinaleResult(state)),
3013
+ allianceNightStatus: () => state ? E.allianceNightStatus(state) : null,
3014
+ startAllianceNight: () => act(() => E.startAllianceNight(state)),
3015
+ allianceAssembly: () => state ? E.currentAllianceAssembly(state) : null,
3016
+ advanceAllianceAssembly: () => act(() => E.advanceAllianceAssembly(state)),
3017
+ allianceNightResult: () => state ? E.currentAllianceNightResult(state) : null,
3018
+ chooseAllianceNight: (id) => act(() => E.chooseAllianceNight(state, id)),
3019
+ continueAllianceNightResult: () => act(() => E.continueAllianceNightResult(state)),
3020
+ startSharedNight: () => act(() => E.startSharedNight(state)),
3021
+ chooseSharedNight: (id) => act(() => E.chooseSharedNight(state, id)),
3022
+ sharedDawnResult: () => state ? E.currentSharedDawnResult(state) : null,
3023
+ continueSharedDawnResult: () => act(() => E.continueSharedDawnResult(state)),
3024
+ collapseFinale: () => state ? E.currentCollapseFinale(state) : null,
3025
+ collapseFinaleOptions: () => state ? E.collapseFinaleOptions(state) : [],
3026
+ advanceCollapseFinale: () => act(() => E.advanceCollapseFinale(state)),
3027
+ chooseCollapseFinale: (id) => act(() => E.chooseCollapseFinale(state, id)),
3028
+ collapseFinaleResult: () => state ? E.currentCollapseFinaleResult(state) : null,
3029
+ continueCollapseFinaleResult: () => act(() => E.continueCollapseFinaleResult(state)),
3030
+ sharedAfterglowResult: () => state ? E.currentSharedAfterglowResult(state) : null,
3031
+ chooseSharedAfterglow: (id) => act(() => E.chooseSharedAfterglow(state, id)),
3032
+ continueSharedAfterglowResult: () => act(() => E.continueSharedAfterglowResult(state)),
3033
+ chooseSharedDawn: (id) => act(() => E.chooseSharedDawn(state, id)),
3034
+ visit: (id) => act(() => E.startVisit(state, id)),
3035
+ chooseVisit: (id) => act(() => E.chooseVisit(state, id)),
3036
+ advanceRouteStory: () => act(() => E.advanceRouteAftermath(state)),
3037
+ advanceNightConversation: () => act(() => E.advanceNightConversation(state)),
3038
+ chooseNightConversation: (id) => act(() => E.chooseNightConversation(state, id)),
3039
+ continueNightConversation: () => act(() => E.continueNightConversation(state)),
3040
+ advanceMorningStory: () => act(() => E.advanceMorningStory(state)),
3041
+ continueMorningResolution: () => act(() => E.continueMorningResolution(state)),
3042
+ routeAftermath: (id) => act(() => E.resolveRouteAftermath(state, id)),
3043
+ chooseNight: (id) => act(() => E.chooseNight(state, id)),
3044
+ personalAfterglow: (id) => act(() => E.choosePersonalAfterglow(state, id)),
3045
+ personalAfterglowAftermath: () => state ? E.currentPersonalAfterglowAftermath(state) : null,
3046
+ personalAfterglowAftermathOptions: () => state ? E.personalAfterglowAftermathOptions(state) : [],
3047
+ choosePersonalAfterglowAftermath: (id) => act(() => E.resolvePersonalAfterglowAftermath(state, id)),
3048
+ advancePersonalAfterglowAftermath: () => act(() => E.advancePersonalAfterglowAftermath(state)),
3049
+ morning: (id) => act(() => E.resolveMorning(state, id)),
3050
+ closeScene: () => act(() => E.closeScene(state)),
3051
+ });