kaizen-agent 1.47.0 → 1.49.0

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 (184) hide show
  1. package/README.md +111 -84
  2. package/cli/build-web.ts +4 -2
  3. package/cli/hermes.ts +65 -0
  4. package/cli/install.ts +24 -10
  5. package/cli/localsend.test.ts +84 -0
  6. package/cli/localsend.ts +131 -0
  7. package/cli/notes.test.ts +6 -4
  8. package/cli/settings.ts +1 -0
  9. package/cli/state.test.ts +37 -0
  10. package/cli/state.ts +35 -20
  11. package/cli/video.test.ts +479 -0
  12. package/cli/video.ts +507 -0
  13. package/cli/web.ts +127 -1
  14. package/cli/worktree.test.ts +3 -1
  15. package/package.json +1 -1
  16. package/skills/kaizen/config.default.yml +4 -0
  17. package/skills/media/kaizen-book/SKILL.md +107 -0
  18. package/skills/media/kaizen-book/assets/reader.html +440 -0
  19. package/skills/media/kaizen-book/assets/styles.json +45 -0
  20. package/skills/media/kaizen-book/examples/book.json +14 -0
  21. package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
  22. package/skills/media/kaizen-book/examples/manuscript.md +127 -0
  23. package/skills/media/kaizen-book/references/interview.md +85 -0
  24. package/skills/media/kaizen-book/references/proofread.md +47 -0
  25. package/skills/media/kaizen-book/references/writing.md +53 -0
  26. package/skills/media/kaizen-book/scripts/build_book.py +279 -0
  27. package/skills/media/kaizen-book/scripts/check.sh +10 -0
  28. package/skills/media/kaizen-book/scripts/check_book.py +128 -0
  29. package/skills/media/kaizen-book/scripts/deploy.py +58 -0
  30. package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
  31. package/skills/media/kaizen-book/scripts/setup.sh +9 -0
  32. package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
  33. package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
  34. package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
  35. package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
  36. package/skills/media/kaizen-video/SKILL.md +112 -0
  37. package/skills/media/kaizen-video/assets/CREDITS.txt +16 -0
  38. package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
  39. package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
  40. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
  41. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
  42. package/skills/media/kaizen-video/assets/fonts/Fredoka.ttf +0 -0
  43. package/skills/media/kaizen-video/assets/fonts/Montserrat.ttf +0 -0
  44. package/skills/media/kaizen-video/assets/fonts/SpecialElite.ttf +0 -0
  45. package/skills/media/kaizen-video/assets/music/investigations.mp3 +0 -0
  46. package/skills/media/kaizen-video/assets/music/lights.mp3 +0 -0
  47. package/skills/media/kaizen-video/assets/music/sunny.mp3 +0 -0
  48. package/skills/media/kaizen-video/examples/config.json +152 -0
  49. package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
  50. package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
  51. package/skills/media/kaizen-video/film.html +246 -0
  52. package/skills/media/kaizen-video/hyperframes.json +12 -0
  53. package/skills/media/kaizen-video/presets/educational-warning/CREDITS.txt +3 -0
  54. package/skills/media/kaizen-video/presets/educational-warning/README.md +26 -0
  55. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/ending.wav +0 -0
  56. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/reveal.wav +0 -0
  57. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/shutter.wav +0 -0
  58. package/skills/media/kaizen-video/presets/educational-warning/assets/sfx/stamp.wav +0 -0
  59. package/skills/media/kaizen-video/presets/educational-warning/index.html +236 -0
  60. package/skills/media/kaizen-video/presets/educational-warning/package.json +10 -0
  61. package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
  62. package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
  63. package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
  64. package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
  65. package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
  66. package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
  67. package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
  68. package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
  69. package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
  70. package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
  71. package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
  72. package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
  73. package/skills/media/kaizen-video/presets/tiktok-cheerful/CREDITS.txt +3 -0
  74. package/skills/media/kaizen-video/presets/tiktok-cheerful/README.md +35 -0
  75. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
  76. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
  77. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
  78. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
  79. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
  80. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
  81. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
  82. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
  83. package/skills/media/kaizen-video/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
  84. package/skills/media/kaizen-video/presets/tiktok-cheerful/index.html +260 -0
  85. package/skills/media/kaizen-video/presets/tiktok-cheerful/package.json +10 -0
  86. package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
  87. package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
  88. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
  89. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
  90. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
  91. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
  92. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
  93. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
  94. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
  95. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
  96. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
  97. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
  98. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
  99. package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
  100. package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
  101. package/skills/media/kaizen-video/presets/tiktok-mystery/CREDITS.txt +12 -0
  102. package/skills/media/kaizen-video/presets/tiktok-mystery/README.md +51 -0
  103. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
  104. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
  105. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
  106. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
  107. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
  108. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
  109. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
  110. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
  111. package/skills/media/kaizen-video/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
  112. package/skills/media/kaizen-video/presets/tiktok-mystery/index.html +260 -0
  113. package/skills/media/kaizen-video/presets/tiktok-mystery/package.json +10 -0
  114. package/skills/media/kaizen-video/presets/tiktok-scam-alert/CREDITS.txt +8 -0
  115. package/skills/media/kaizen-video/presets/tiktok-scam-alert/README.md +24 -0
  116. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
  117. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
  118. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
  119. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
  120. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
  121. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
  122. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
  123. package/skills/media/kaizen-video/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
  124. package/skills/media/kaizen-video/presets/tiktok-scam-alert/index.html +266 -0
  125. package/skills/media/kaizen-video/presets/tiktok-scam-alert/package.json +10 -0
  126. package/skills/media/kaizen-video/references/presets.md +76 -0
  127. package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
  128. package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
  129. package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
  130. package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
  131. package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
  132. package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
  133. package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
  134. package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
  135. package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
  136. package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
  137. package/skills/media/kaizen-video/scripts/check.sh +22 -0
  138. package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
  139. package/skills/media/kaizen-video/scripts/make_film.py +263 -0
  140. package/skills/media/kaizen-video/scripts/make_tiktok.py +213 -0
  141. package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
  142. package/skills/media/kaizen-video/scripts/render_cut.py +88 -0
  143. package/skills/media/kaizen-video/scripts/setup.sh +14 -0
  144. package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
  145. package/skills/media/kaizen-video/scripts/srt.py +9 -0
  146. package/skills/media/kaizen-video/scripts/still.sh +7 -0
  147. package/skills/media/kaizen-video/scripts/transcribe.py +45 -0
  148. package/skills/media/kaizen-video/tools/carve.mjs +560 -0
  149. package/skills/media/kaizen-video/tools/lib/main-module.mjs +13 -0
  150. package/web/board/views.ts +1 -1
  151. package/web/board.html +3 -1
  152. package/web/board.js +2 -2
  153. package/web/notebook.js +1 -1
  154. package/web/notes.html +2 -0
  155. package/web/term.js +38 -36
  156. package/web/term.ts +43 -10
  157. package/web/video.html +298 -0
  158. package/web/video.js +81 -0
  159. package/web/video.ts +610 -0
  160. package/skills/story-video/SKILL.md +0 -108
  161. package/skills/story-video/engine/package.json +0 -12
  162. package/skills/story-video/engine/src/Root.tsx +0 -24
  163. package/skills/story-video/engine/src/engine/Film.tsx +0 -405
  164. package/skills/story-video/engine/src/engine/presets.json +0 -58
  165. package/skills/story-video/engine/src/index.ts +0 -3
  166. package/skills/story-video/engine/tsconfig.json +0 -1
  167. package/skills/story-video/examples/sample-film/README.md +0 -12
  168. package/skills/story-video/examples/sample-film/film.json +0 -16
  169. package/skills/story-video/references/presets.md +0 -21
  170. package/skills/story-video/scripts/check.sh +0 -19
  171. package/skills/story-video/scripts/setup.sh +0 -25
  172. package/skills/story-video/scripts/srt.py +0 -30
  173. package/skills/story-video/scripts/still.sh +0 -10
  174. /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
  175. /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
  176. /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
  177. /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
  178. /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
  179. /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
  180. /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
  181. /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
  182. /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
  183. /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
  184. /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
@@ -0,0 +1,440 @@
1
+ <!DOCTYPE html>
2
+ <html lang="@@LANG@@">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
6
+ <title>@@TITLE@@</title>
7
+ <meta name="description" content="@@DESC@@">
8
+ <!--@@FONTLINK@@-->
9
+ <style>
10
+ /*@@FONTS@@*/
11
+ :root{--paper:#F7F1E6;--ink:#2A211B;--accent:#8A5A2B;--desk:#1B2433;--muted:color-mix(in srgb,var(--ink) 55%,transparent);--fs:18px;
12
+ --f-body:Georgia,serif;--f-display:Georgia,serif;--f-ui:system-ui,sans-serif;--cv-bg:#0B1626;--cv-ink:#F2CF7A;--cv-accent:#F2CF7A}
13
+ html[data-theme=sepia]{--paper:#F1E4C9;--ink:#3A2C1C;--accent:#8A5A2B}
14
+ html[data-theme=night]{--paper:#17191E;--ink:#D9D3C7;--accent:#D9A866;--desk:#08090B}
15
+ *{box-sizing:border-box}
16
+ html,body{margin:0;height:100%;background:var(--desk);color:var(--ink);font-family:var(--f-ui);-webkit-tap-highlight-color:transparent}
17
+ body{overflow:hidden}
18
+ button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;min-width:44px;min-height:44px;border-radius:10px}
19
+ button:focus-visible,a:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
20
+ .view{position:fixed;inset:0;display:none}
21
+ body[data-view=cover] #cover,body[data-view=toc] #toc,body[data-view=read] #read{display:flex}
22
+ .paper{background-color:var(--paper);background-image:radial-gradient(ellipse at center,transparent 60%,rgba(60,40,20,.10)),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 .35 0 0 0 0 .25 0 0 0 0 .15 0 0 0 .07 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")}
23
+ html[data-theme=night] .paper{background-image:radial-gradient(ellipse at center,transparent 60%,rgba(0,0,0,.35))}
24
+
25
+ /* ---------- cover */
26
+ #cover{flex-direction:column;align-items:center;justify-content:center;gap:22px;padding:24px 16px;background:radial-gradient(ellipse at 50% 40%,color-mix(in srgb,var(--desk) 70%,#fff 8%),var(--desk))}
27
+ .book3d{position:relative;width:min(78vw,52vh,380px);aspect-ratio:2/3;perspective:2200px}
28
+ .pageblock{position:absolute;inset:4px -10px 4px 6px;border-radius:2px 6px 6px 2px;background:repeating-linear-gradient(90deg,#efe6d4 0 1px,#d9ceb8 1px 2px);box-shadow:0 30px 60px rgba(0,0,0,.45)}
29
+ .halftitle{position:absolute;inset:4px -10px 4px 6px;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:12%;border-radius:2px 6px 6px 2px;color:var(--ink);font-family:var(--f-display)}
30
+ .halftitle b{font-size:1.5em;line-height:1.1}.halftitle i{margin-top:.6em;color:var(--muted)}
31
+ .board{position:absolute;inset:0;transform-origin:left center;transition:transform 1s cubic-bezier(.6,.05,.3,1);transform-style:preserve-3d;border-radius:3px 8px 8px 3px;overflow:hidden;cursor:pointer;box-shadow:inset 6px 0 10px -4px rgba(0,0,0,.5),0 18px 40px rgba(0,0,0,.4)}
32
+ .board.open{transform:rotateY(-165deg)}
33
+ .board img{width:100%;height:100%;object-fit:cover;display:block}
34
+ .board::after{content:"";position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.35),rgba(255,255,255,.08) 3%,transparent 7%),url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence baseFrequency='.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .08 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");pointer-events:none}
35
+ .hint{margin:0;color:color-mix(in srgb,#fff 60%,transparent);font-size:13px;letter-spacing:.08em}
36
+ .cta{padding:12px 28px;border-radius:999px;background:var(--cv-accent);color:var(--cv-bg);font-weight:700;letter-spacing:.04em}
37
+ /* typeset cover, shared by screen, print and og */
38
+ .tc{position:relative;width:100%;height:100%;display:flex;flex-direction:column;padding:11% 10% 9%;background:var(--cv-bg);color:var(--cv-ink);font-family:var(--f-display);container-type:inline-size;overflow:hidden}
39
+ .tc .t{font-size:12.5cqi;font-weight:700;line-height:1.02;margin-top:8%;letter-spacing:.01em}
40
+ .tc .s{font-size:5.6cqi;font-style:italic;margin-top:4%;opacity:.9}
41
+ .tc .g{font-size:4.2cqi;margin-top:8%;opacity:.8;font-family:var(--f-body)}
42
+ .tc .v{font-size:4.6cqi;margin-top:6%;letter-spacing:.2em;text-transform:uppercase;font-weight:700;color:var(--cv-accent)}
43
+ .tc .a{margin-top:auto;font-size:4.4cqi;letter-spacing:.22em;text-transform:uppercase;font-family:var(--f-ui);font-weight:600}
44
+ .tc.frame::before{content:"";position:absolute;inset:4.5%;border:1.5px solid var(--cv-accent);outline:1px solid var(--cv-accent);outline-offset:-7px;pointer-events:none}
45
+ .tc.grid{background:linear-gradient(transparent 96%,color-mix(in srgb,var(--cv-accent) 30%,transparent) 0) 0 0/100% 8%,linear-gradient(90deg,transparent 96%,color-mix(in srgb,var(--cv-accent) 30%,transparent) 0) 0 0/8% 100%,radial-gradient(circle at 70% 80%,color-mix(in srgb,var(--cv-accent) 45%,transparent),transparent 55%),var(--cv-bg)}
46
+ .tc.petals{background:radial-gradient(circle at 85% 12%,color-mix(in srgb,var(--cv-accent) 35%,transparent) 0 14%,transparent 15%),radial-gradient(circle at 12% 88%,color-mix(in srgb,var(--cv-accent) 25%,transparent) 0 20%,transparent 21%),var(--cv-bg)}
47
+ .tc.slash{background:linear-gradient(115deg,transparent 62%,var(--cv-accent) 62% 66%,transparent 66%),var(--cv-bg)}
48
+ .tc.dots{background:radial-gradient(circle,color-mix(in srgb,var(--cv-accent) 28%,transparent) 16%,transparent 18%) 0 0/9% 6%,var(--cv-bg)}
49
+
50
+ /* ---------- contents */
51
+ #toc{justify-content:center;overflow:auto;padding:16px}
52
+ .sheet{width:min(100%,640px);margin:auto 0;padding:28px 22px 40px;border-radius:4px;box-shadow:0 20px 50px rgba(0,0,0,.4);font-family:var(--f-body)}
53
+ .sheet h1{font-family:var(--f-display);text-align:center;font-size:30px;margin:.2em 0 .1em}
54
+ .sheet .by{text-align:center;color:var(--muted);margin:0 0 22px;font-style:italic}
55
+ .sheet h2{font-family:var(--f-ui);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--accent);margin:22px 0 6px;font-weight:700}
56
+ .row{display:flex;width:100%;align-items:baseline;gap:6px;padding:9px 2px;text-align:left;font-size:17px;border-radius:6px;min-height:44px}
57
+ .row:hover{background:color-mix(in srgb,var(--accent) 10%,transparent)}
58
+ .row .lead{flex:1;border-bottom:1px dotted var(--muted);transform:translateY(-4px);min-width:12px}
59
+ .row small{color:var(--muted);font-family:var(--f-ui);font-size:12px}
60
+ .toplinks{display:flex;justify-content:space-between;font-family:var(--f-ui);font-size:14px}
61
+ .toplinks button{color:var(--accent);padding:0 8px}
62
+
63
+ /* ---------- reader */
64
+ #read{flex-direction:column}
65
+ .bar{display:flex;align-items:center;gap:6px;padding:0 8px;height:52px;flex:none;color:color-mix(in srgb,#fff 82%,transparent);font-size:14px}
66
+ .bar .rh{flex:1;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-family:var(--f-display);font-size:15px;letter-spacing:.02em}
67
+ .bar .pg{flex:1;text-align:center;font-variant-numeric:tabular-nums}
68
+ .stage{flex:1;display:flex;align-items:center;justify-content:center;position:relative;touch-action:pan-y;user-select:none;-webkit-user-select:none}
69
+ .book{position:relative;perspective:2400px}
70
+ .area{position:relative;overflow:hidden;border-radius:2px 4px 4px 2px;box-shadow:0 14px 34px rgba(0,0,0,.42)}
71
+ .area::after{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(90deg,rgba(0,0,0,.16),transparent 5%)}
72
+ .spread .area::after{background:linear-gradient(90deg,transparent 46%,rgba(0,0,0,.18) 50%,transparent 54%)}
73
+ .flow{position:absolute;column-fill:auto;font-family:var(--f-body);font-size:var(--fs);line-height:1.62;color:var(--ink);will-change:transform;hyphens:auto;-webkit-hyphens:auto}
74
+ .flow.justify{text-align:justify}
75
+ .flow p{margin:0;text-indent:1.4em;orphans:2;widows:2}
76
+ .flow h2+p,.flow .chhead+p,.flow h3+p,.flow h4+p,.flow .scene+p,.flow blockquote+p,.flow figure+p,.flow ul+p,.flow ol+p,.flow pre+p,.flow p:first-child{text-indent:0}
77
+ .chhead{margin:2.2em 0 1.6em;text-align:center;break-inside:avoid}
78
+ .chhead .k{display:block;font-family:var(--f-ui);font-size:.62em;letter-spacing:.2em;text-transform:uppercase;color:var(--accent)}
79
+ .chhead h2{font-family:var(--f-display);font-size:1.55em;line-height:1.15;margin:.35em 0 0;font-weight:700}
80
+ .chhead .sub{font-style:italic;color:var(--muted);margin-top:.5em;text-indent:0}
81
+ .chhead::after{content:"";display:block;width:3em;height:1px;background:var(--accent);margin:1.1em auto 0}
82
+ .chhead+p::first-letter{font-family:var(--f-display);float:left;font-size:3.2em;line-height:.85;padding:.06em .08em 0 0;color:var(--accent)}
83
+ .partop{break-after:column;height:100%;display:flex;flex-direction:column;justify-content:center;text-align:center;font-family:var(--f-display)}
84
+ .lead-in{break-after:column}
85
+ .partop b{font-size:1.7em;line-height:1.15}.partop p{text-indent:0;font-family:var(--f-body);font-size:.92em;color:var(--muted);margin-top:1em;text-align:center}
86
+ .partop::before{content:"❦";color:var(--accent);font-size:1.6em;margin-bottom:.4em}
87
+ .flow h3{font-family:var(--f-display);font-size:1.15em;margin:1.3em 0 .5em;break-after:avoid}
88
+ .flow h4{font-size:1em;margin:1.1em 0 .4em;break-after:avoid;font-style:italic}
89
+ .flow blockquote{margin:.8em 0 .8em 1em;padding-left:.9em;border-left:2px solid color-mix(in srgb,var(--accent) 60%,transparent);font-style:italic}
90
+ .flow blockquote p{text-indent:0}
91
+ .flow ul,.flow ol{margin:.5em 0 .7em;padding-left:1.4em}
92
+ .flow li{margin:.2em 0}
93
+ .flow .scene{text-align:center;text-indent:0;margin:.8em 0;color:var(--accent);letter-spacing:.5em}
94
+ .flow figure{margin:1em 0;text-align:center;break-inside:avoid}
95
+ .flow img{max-width:100%;height:auto;max-height:var(--imgmax,60vh);border-radius:2px}
96
+ .flow figcaption{font-size:.8em;color:var(--muted);font-style:italic;margin-top:.4em}
97
+ .flow code{font-size:.88em}
98
+ .flow pre{clear:both;font-size:.78em;line-height:1.45;white-space:pre-wrap;overflow-wrap:anywhere;margin:.8em 0;padding:.7em .8em;border-radius:4px;background:color-mix(in srgb,var(--ink) 7%,transparent);break-inside:avoid}
99
+ .flow a{color:var(--accent)}
100
+ .flow sup.fn a{text-decoration:none;font-family:var(--f-ui);font-weight:700;font-size:.72em;padding:0 .15em}
101
+ .flow .notes{font-size:.82em;line-height:1.45;border-top:1px solid color-mix(in srgb,var(--ink) 25%,transparent);padding-top:.6em;margin-top:1.4em;color:var(--muted)}
102
+ .deco{position:absolute;inset:0;pointer-events:none;font-family:var(--f-ui);font-size:11px;letter-spacing:.14em;color:var(--muted)}
103
+ .deco span{position:absolute;text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-transform:uppercase}
104
+ .deco .pn{letter-spacing:0;text-transform:none;font-size:12px}
105
+ .leaf{position:absolute;inset:0;margin:0;backface-visibility:hidden;pointer-events:none;box-shadow:none}
106
+ .leaf .shade{position:absolute;inset:0;background:linear-gradient(90deg,rgba(0,0,0,.28),rgba(0,0,0,0) 60%);opacity:0;pointer-events:none}
107
+ .panel{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);width:min(calc(100% - 32px),420px);padding:18px;border-radius:16px;box-shadow:0 20px 60px rgba(0,0,0,.5);display:none;z-index:10;font-family:var(--f-ui)}
108
+ .panel.on{display:block}
109
+ .panel h3{margin:0 0 8px;font-size:12px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted)}
110
+ .seg{display:flex;gap:8px;margin-bottom:16px}
111
+ .seg button{flex:1;border:1px solid color-mix(in srgb,var(--ink) 25%,transparent)}
112
+ .seg button[aria-pressed=true]{border-color:var(--accent);color:var(--accent);font-weight:700}
113
+ .panel .wide{width:100%;border:1px solid var(--accent);color:var(--accent);font-weight:700}
114
+ .note{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:min(calc(100% - 32px),460px);max-height:70vh;overflow:auto;padding:20px 20px 10px;border-radius:12px;box-shadow:0 20px 60px rgba(0,0,0,.55);z-index:11;display:none;font-family:var(--f-body);line-height:1.5}
115
+ .note.on{display:block}
116
+ .note .x{float:right;margin:-12px -12px 0 0}
117
+ @media (prefers-reduced-motion:reduce){.board{transition:none}}
118
+
119
+ /* ---------- print + og */
120
+ #print,#og{display:none}
121
+ body.printing{overflow:visible;background:#fff}
122
+ body.printing #app{display:none}
123
+ body.printing #print{display:block}
124
+ body.og #app{display:none}
125
+ body.og #og{display:flex}
126
+ #og{width:1200px;height:630px;align-items:center;gap:56px;padding:0 80px;background:radial-gradient(ellipse at 30% 50%,color-mix(in srgb,var(--cv-bg) 70%,#fff 10%),var(--cv-bg));color:var(--cv-ink);font-family:var(--f-display)}
127
+ #og .ogc{width:340px;height:510px;flex:none;box-shadow:0 26px 60px rgba(0,0,0,.55);border-radius:3px 8px 8px 3px;overflow:hidden}
128
+ #og .ogc img{width:100%;height:100%;object-fit:cover}
129
+ #og h1{font-size:64px;line-height:1.02;margin:0}#og p{font-size:30px;font-style:italic;margin:16px 0 0;opacity:.85}#og .au{font-family:var(--f-ui);font-size:22px;letter-spacing:.2em;text-transform:uppercase;margin-top:40px;font-style:normal;color:var(--cv-accent)}
130
+ @page{size:A5;margin:16mm 14mm 17mm;@bottom-center{content:counter(page);font-family:var(--f-ui);font-size:8.5pt;color:#666}}
131
+ @page cover{margin:0;@bottom-center{content:none}}
132
+ @page front{@bottom-center{content:none}}
133
+ @media print{
134
+ html,body{height:auto;background:#fff!important;-webkit-print-color-adjust:exact;print-color-adjust:exact}
135
+ #app,#og{display:none!important}#print{display:block!important}
136
+ }
137
+ #print{font-family:var(--f-body);font-size:10.6pt;line-height:1.48;color:#1a1a1a;background:#fff;hyphens:auto;-webkit-hyphens:auto}
138
+ #print .pp{break-before:page}
139
+ #print .p-cover{page:cover;width:148mm;height:210mm;overflow:hidden;break-after:page}
140
+ #print .p-cover{position:relative}#print .vband{position:absolute;left:0;right:0;bottom:18mm;padding:3mm;text-align:center;background:var(--cv-bg);color:var(--cv-accent);font-family:var(--f-ui);font-weight:700;letter-spacing:.25em;text-transform:uppercase;font-size:12pt}
141
+ #print .p-cover img{width:100%;height:100%;object-fit:cover;display:block}
142
+ #print .front{page:front}
143
+ #print .p-title{height:170mm;display:flex;flex-direction:column;justify-content:center;text-align:center;font-family:var(--f-display)}
144
+ #print .p-title b{font-size:26pt;line-height:1.1}#print .p-title i{font-size:13pt;margin-top:4mm;color:#555}#print .p-title span{margin-top:auto;letter-spacing:.2em;text-transform:uppercase;font-family:var(--f-ui);font-size:9pt}
145
+ #print .p-copy{height:170mm;display:flex;flex-direction:column;justify-content:flex-end;font-size:8.5pt;color:#555;white-space:pre-line}
146
+ #print .p-ded{height:150mm;display:flex;align-items:center;justify-content:center;text-align:center;font-style:italic;white-space:pre-line}
147
+ #print .p-toc h2{font-family:var(--f-display);text-align:center;font-size:18pt;margin:0 0 8mm}
148
+ #print .p-toc h3 a{display:inline;margin:0}#print .p-toc h3{font-family:var(--f-ui);font-size:8pt;letter-spacing:.18em;text-transform:uppercase;margin:5mm 0 1.5mm;color:#444}
149
+ #print .p-toc a{display:flex;gap:2mm;align-items:baseline;color:inherit;text-decoration:none;margin:1.2mm 0;break-inside:avoid}
150
+ #print .p-toc a .lead{flex:1;border-bottom:.5pt dotted #888;transform:translateY(-1mm)}
151
+ #print .p-toc a .n{min-width:9mm;text-align:right;font-variant-numeric:tabular-nums}
152
+ #print .p-part{page:front;height:170mm;display:flex;flex-direction:column;justify-content:center;text-align:center;font-family:var(--f-display);break-after:page}
153
+ #print .p-part b{font-size:22pt;line-height:1.15}#print .p-part div{font-family:var(--f-body);font-size:10pt;color:#555;margin-top:6mm}
154
+ #print .chhead{margin:22mm 0 9mm}
155
+ #print .chhead .k{font-size:8pt}#print .chhead h2{font-size:19pt}
156
+ #print p{margin:0;text-indent:1.3em;text-align:justify;orphans:2;widows:2}
157
+ #print .chhead+p,#print h3+p,#print h4+p,#print .scene+p,#print blockquote+p,#print figure+p,#print ul+p,#print ol+p,#print pre+p{text-indent:0}
158
+ #print .chhead+p::first-letter{font-family:var(--f-display);float:left;font-size:3.1em;line-height:.85;padding:.05em .08em 0 0;color:var(--accent)}
159
+ #print h3{font-family:var(--f-display);font-size:12.5pt;margin:5mm 0 2mm;break-after:avoid}
160
+ #print h4{font-size:10.6pt;font-style:italic;margin:4mm 0 1.5mm;break-after:avoid}
161
+ #print blockquote{margin:3mm 0 3mm 5mm;padding-left:4mm;border-left:.8pt solid var(--accent);font-style:italic}
162
+ #print blockquote p{text-indent:0}
163
+ #print .scene{text-align:center;text-indent:0;margin:3mm 0;letter-spacing:.5em;color:var(--accent)}
164
+ #print figure{margin:4mm 0;text-align:center;break-inside:avoid}#print img{max-width:100%;max-height:120mm}
165
+ #print figcaption{font-size:8.5pt;font-style:italic;color:#555;margin-top:1.5mm}
166
+ #print sup.fn a{color:inherit;text-decoration:none;font-size:.75em}
167
+ #print .notes{font-size:8.6pt;line-height:1.35;border-top:.5pt solid #999;padding-top:2mm;margin-top:6mm;color:#333}
168
+ #print a{color:inherit}
169
+ #print pre{clear:both;font-size:8.5pt;line-height:1.35;white-space:pre-wrap;overflow-wrap:anywhere;margin:3mm 0;padding:2mm 3mm;border-left:.8pt solid #999;break-inside:avoid}
170
+ #print .tc{width:148mm;height:210mm}
171
+ html.mono #print *,html.mono #print *::before,html.mono #print *::after,html.mono #print *::first-letter,html.mono #print *::marker{color:#000!important;-webkit-text-fill-color:currentColor!important;border-color:#000!important;text-decoration-color:#000!important;box-shadow:none!important}
172
+ html.mono #print *:not(img){background:#fff!important;background-image:none!important}
173
+ html.mono #print .tc{border:0}
174
+ html.mono #print .chhead::after{background:#000!important}
175
+ html.mono #print .tc.frame::before{outline-color:#000}
176
+ html.mono #print img,html.mono #print .gs{filter:grayscale(1)}
177
+ </style>
178
+ </head>
179
+ <body data-view="cover">
180
+ <div id="app">
181
+ <section id="cover" class="view">
182
+ <div class="book3d"><div class="pageblock"></div><div class="halftitle paper"></div><div class="board" role="button" tabindex="0"></div></div>
183
+ <p class="hint"></p>
184
+ <button class="cta"></button>
185
+ </section>
186
+ <section id="toc" class="view"><div class="sheet paper"></div></section>
187
+ <section id="read" class="view">
188
+ <div class="bar"><button data-act="toc" aria-label="contents">☰</button><div class="rh"></div><button data-act="settings">Aa</button></div>
189
+ <div class="stage"><div class="book"><div class="area paper"><div class="flow"></div><div class="deco"></div></div></div></div>
190
+ <div class="bar"><button data-act="prev">‹</button><div class="pg"></div><button data-act="next">›</button></div>
191
+ </section>
192
+ <div class="panel paper" role="dialog"></div>
193
+ <div class="note paper" role="dialog"></div>
194
+ </div>
195
+ <div id="print"></div>
196
+ <div id="og"></div>
197
+ <script>
198
+ const B = "@@BOOK@@";
199
+ const L = B.labels, $ = (s, r = document) => r.querySelector(s), esc = s => String(s ?? "").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
200
+ const root = document.documentElement, st = B.style;
201
+ const vars = {"--paper": st.paper, "--ink": st.ink, "--accent": st.accent, "--desk": st.desk, "--cv-bg": st.cover.bg, "--cv-ink": st.cover.ink, "--cv-accent": st.cover.accent,
202
+ "--f-body": `"${st.fonts.body}",Georgia,serif`, "--f-display": `"${st.fonts.display}",Georgia,serif`, "--f-ui": `"${st.fonts.ui}",system-ui,sans-serif`};
203
+ // a :root rule, not inline style, so html[data-theme=…] still overrides paper and ink
204
+ document.head.insertAdjacentHTML("beforeend", `<style>:root{${Object.entries(vars).map(([k, v]) => `${k}:${v}`).join(";")}}</style>`);
205
+
206
+ // Flat chapter list: {p: part index, c: chapter index, t, sub, html, first: first chapter of a part with a title or lead-in text}
207
+ const CH = [];
208
+ let lead = ""; // lead-in text of a part with no chapters (e.g. a preface before the first # part) goes before the next chapter
209
+ B.parts.forEach((p, pi) => { if (!p.chs.length) { if (!p.title && p.intro) lead += `<div class="lead-in">${p.intro}</div>`; return; }
210
+ p.chs.forEach((c, ci) => { CH.push({...c, p: pi, first: ci === 0 && !!(p.title || p.intro), lead}); lead = ""; }); });
211
+ const KEY = "kaizen-book:" + B.key;
212
+ let S = {idx: 0, page: 0, fs: Math.round(18 * (st.fontScale || 1)), theme: "light"};
213
+ try { Object.assign(S, JSON.parse(localStorage.getItem(KEY)) || {}); } catch (e) {}
214
+ let hasSaved = false; try { hasSaved = !!localStorage.getItem(KEY); } catch (e) {}
215
+ const save = () => { try { localStorage.setItem(KEY, JSON.stringify(S)); } catch (e) {} };
216
+ const applyTheme = () => { root.dataset.theme = S.theme; root.style.setProperty("--fs", S.fs + "px"); };
217
+ applyTheme();
218
+
219
+ function coverHTML(vol, forceType) {
220
+ if (B.cover && !forceType) return `<img src="${B.cover}" alt="${esc(B.title)}">${vol ? `<div class="vband">${esc(vol)}</div>` : ""}`;
221
+ return `<div class="tc ${esc(st.cover.pattern || "none")}"><div class="t">${esc(B.title)}</div>${B.subtitle ? `<div class="s">${esc(B.subtitle)}</div>` : ""}${B.tagline ? `<div class="g">${esc(B.tagline)}</div>` : ""}${vol ? `<div class="v">${esc(vol)}</div>` : ""}<div class="a">${esc(B.author)}</div></div>`;
222
+ }
223
+ const partHead = p => !p.title ? `<div class="lead-in">${p.intro}</div>` : `<div class="partop"><b>${esc(p.title)}</b>${p.intro ? `<div>${p.intro}</div>` : ""}</div>`;
224
+ const chHead = c => `<header class="chhead"><span class="k">${esc(B.parts[c.p].title || "")}</span><h2>${esc(c.t)}</h2>${c.sub ? `<p class="sub">${esc(c.sub)}</p>` : ""}</header>`;
225
+
226
+ // ---------- views
227
+ function show(v) { document.body.dataset.view = v; $(".panel").classList.remove("on"); }
228
+ function initCover() {
229
+ $(".board").innerHTML = coverHTML();
230
+ $(".halftitle").innerHTML = `<b>${esc(B.title)}</b><i>${esc(B.author)}</i>`;
231
+ $(".hint").textContent = L.tap;
232
+ $(".cta").textContent = hasSaved ? L.resume : L.start;
233
+ const open = then => { const b = $(".board"); if (b.classList.contains("open")) return then();
234
+ b.classList.add("open"); setTimeout(then, matchMedia("(prefers-reduced-motion:reduce)").matches ? 0 : 1050); };
235
+ $(".board").onclick = $(".board").onkeydown = e => { if (e.type === "keydown" && !/Enter| /.test(e.key)) return; open(() => show("toc")); };
236
+ $(".cta").onclick = () => open(() => openChapter(hasSaved ? S.idx : 0, hasSaved ? S.page : 0));
237
+ }
238
+ function initToc() {
239
+ let h = `<div class="toplinks"><button data-go="cover">← ${esc(L.cover)}</button>${hasSaved ? `<button data-go="resume">${esc(L.resume)} →</button>` : ""}</div>
240
+ <h1>${esc(B.title)}</h1><p class="by">${esc(B.subtitle || "")}${B.subtitle ? "<br>" : ""}${esc(B.author)}</p>`;
241
+ let last = -1;
242
+ CH.forEach((c, i) => {
243
+ if (c.p !== last) { last = c.p; if (B.parts[c.p].title) h += `<h2>${esc(B.parts[c.p].title)}</h2>`; }
244
+ h += `<button class="row" data-ch="${i}"><span>${esc(c.t)}</span><span class="lead"></span><small>${i + 1}</small></button>`;
245
+ });
246
+ const sh = $("#toc .sheet"); sh.innerHTML = h;
247
+ sh.onclick = e => { const r = e.target.closest("[data-ch],[data-go]"); if (!r) return;
248
+ if (r.dataset.go === "cover") { $(".board").classList.remove("open"); show("cover"); }
249
+ else if (r.dataset.go === "resume") openChapter(S.idx, S.page);
250
+ else openChapter(+r.dataset.ch, 0); };
251
+ }
252
+
253
+ // ---------- pagination
254
+ let G = {W: 0, H: 0, pad: 0, spread: false, n: 1};
255
+ function layout() {
256
+ const stage = $(".stage").getBoundingClientRect();
257
+ G.spread = innerWidth >= 900 && innerWidth > innerHeight;
258
+ const maxH = stage.height - (innerWidth < 500 ? 12 : 36);
259
+ let W = G.spread ? Math.min((innerWidth - 80) / 2, maxH / 1.42, 560) : Math.min(innerWidth - 24, 600, maxH / 1.25);
260
+ W = Math.floor(W); const H = Math.floor(G.spread ? Math.min(maxH, W * 1.42) : maxH);
261
+ const pad = Math.round(Math.max(18, Math.min(44, W * .085)));
262
+ Object.assign(G, {W, H, pad, top: pad + 14, bottom: pad + 14});
263
+ const area = $(".area"), flow = $(".flow");
264
+ $("#read").classList.toggle("spread", G.spread);
265
+ area.style.width = (G.spread ? 2 * W : W) + "px"; area.style.height = H + "px";
266
+ const cw = W - 2 * pad, ch = H - G.top - G.bottom;
267
+ Object.assign(flow.style, {left: pad + "px", top: G.top + "px", width: cw + "px", height: ch + "px", columnWidth: cw + "px", columnGap: 2 * pad + "px"});
268
+ flow.classList.toggle("justify", cw >= 380);
269
+ flow.style.setProperty("--imgmax", Math.max(120, ch - 130) + "px");
270
+ }
271
+ function measure() {
272
+ const flow = $(".flow"), end = $(".end", flow);
273
+ G.n = Math.max(1, Math.floor((end.getBoundingClientRect().left - flow.getBoundingClientRect().left + 1) / G.W) + 1);
274
+ }
275
+ // Pages of every chapter at the current size, so page numbers run through the whole book.
276
+ // In a spread each chapter starts on a left page, so an odd count takes a blank right page.
277
+ let mzKey = "";
278
+ function measureAll() {
279
+ const key = [G.W, G.H, G.spread, S.fs, S.theme].join();
280
+ if (key === mzKey) return; mzKey = key;
281
+ const mz = $(".area").cloneNode(false), f = $(".flow").cloneNode(false);
282
+ mz.style.cssText += ";position:fixed;left:-99999px;top:0;visibility:hidden"; f.style.transform = "none";
283
+ mz.append(f); document.body.append(mz);
284
+ G.counts = CH.map((c, i) => { f.innerHTML = chapterHTML(i);
285
+ const n = Math.max(1, Math.floor(($(".end", f).getBoundingClientRect().left - f.getBoundingClientRect().left + 1) / G.W) + 1);
286
+ return G.spread ? n + (n % 2) : n; });
287
+ mz.remove();
288
+ G.off = [0]; G.counts.forEach((n, i) => G.off.push(G.off[i] + n));
289
+ }
290
+ const chapterHTML = i => { const c = CH[i]; return (c.lead || "") + (c.first ? partHead(B.parts[c.p]) : "") + chHead(c) + c.html + '<span class="end"></span>'; };
291
+ function renderChapter(i) {
292
+ const c = CH[i];
293
+ $(".flow").innerHTML = chapterHTML(i);
294
+ $("#read .rh").textContent = c.t;
295
+ measure(); measureAll();
296
+ }
297
+ function step() { return G.spread ? 2 : 1; }
298
+ function clampPage(p) { p = Math.max(0, Math.min(G.n - 1, p)); return G.spread ? p - (p % 2) : p; }
299
+ function place() {
300
+ $(".flow").style.transform = `translateX(${-S.page * G.W}px)`;
301
+ const vis = G.spread ? [S.page, S.page + 1] : [S.page];
302
+ $(".deco").innerHTML = vis.filter(p => p < G.n).map((p, k) => {
303
+ const x = k * G.W, head = (G.spread && k === 0) ? B.title : CH[S.idx].t;
304
+ return `<span style="left:${x + G.pad}px;width:${G.W - 2 * G.pad}px;top:${Math.round(G.pad * .55)}px">${p ? esc(head) : ""}</span><span class="pn" style="left:${x}px;width:${G.W}px;bottom:${Math.round(G.pad * .5)}px">${G.off[S.idx] + p + 1}</span>`;
305
+ }).join("");
306
+ const g = G.off[S.idx] + S.page + 1;
307
+ $(".pg").textContent = `${g}${G.spread && S.page + 1 < G.n ? "–" + (g + 1) : ""} / ${G.off[CH.length]} · ${S.idx + 1}/${CH.length}`;
308
+ save();
309
+ }
310
+ async function openChapter(i, page) {
311
+ show("read"); S.idx = Math.max(0, Math.min(CH.length - 1, i | 0)); layout(); renderChapter(S.idx); S.page = clampPage(page === -1 ? G.n - 1 : page); place();
312
+ await document.fonts.ready; measure(); mzKey = ""; measureAll(); S.page = clampPage(page === -1 ? G.n - 1 : S.page); place();
313
+ }
314
+
315
+ // ---------- flip
316
+ let busy = false;
317
+ const reduced = () => matchMedia("(prefers-reduced-motion:reduce)").matches;
318
+ function snap(clip) {
319
+ const a = $(".area").cloneNode(true); a.classList.add("leaf"); a.removeAttribute("id");
320
+ a.style.width = $(".area").style.width; a.style.height = $(".area").style.height;
321
+ if (clip) a.style.clipPath = clip;
322
+ a.insertAdjacentHTML("beforeend", '<div class="shade"></div>');
323
+ $(".book").append(a); return a;
324
+ }
325
+ const turn = (el, origin, from, to, ease, ms) => { el.style.transformOrigin = origin;
326
+ el.querySelector(".shade").animate([{opacity: from ? .5 : 0}, {opacity: from ? 0 : .5}], {duration: ms, easing: ease, fill: "forwards"});
327
+ return el.animate([{transform: `rotateY(${from}deg)`}, {transform: `rotateY(${to}deg)`}], {duration: ms, easing: ease, fill: "forwards"}).finished; };
328
+ async function flip(dir) {
329
+ if (busy) return;
330
+ let idx = S.idx, page = S.page + dir * step();
331
+ if (page >= G.n) { if (idx + 1 >= CH.length) return; idx++; page = 0; }
332
+ else if (page < 0) { if (idx === 0) return; idx--; page = -1; }
333
+ const apply = () => { if (idx !== S.idx) { S.idx = idx; renderChapter(idx); } S.page = clampPage(page === -1 ? G.n - 1 : page); place(); };
334
+ if (reduced()) { const o = snap(); apply(); await o.animate([{opacity: 1}, {opacity: 0}], 200).finished; o.remove(); return; }
335
+ busy = true;
336
+ const L_ = "inset(0 50% 0 0)", R_ = "inset(0 0 0 50%)", ms = 380, done = [];
337
+ const old = [], neu = [];
338
+ if (!G.spread) {
339
+ if (dir > 0) { const lf = snap(); apply(); done.push(lf); await turn(lf, "left center", 0, -90, "ease-in", ms * 1.4); }
340
+ else { const base = snap(); apply(); const lf = snap(); base.after(lf); done.push(base, lf);
341
+ lf.style.zIndex = 2; await turn(lf, "left center", -90, 0, "ease-out", ms * 1.4); }
342
+ } else {
343
+ const keep = snap(dir > 0 ? L_ : R_), lf1 = snap(dir > 0 ? R_ : L_);
344
+ apply(); const lf2 = snap(dir > 0 ? L_ : R_); lf2.style.visibility = "hidden"; done.push(keep, lf1, lf2);
345
+ await turn(lf1, "50% 50%", 0, dir > 0 ? -90 : 90, "ease-in", ms);
346
+ lf1.style.visibility = "hidden"; lf2.style.visibility = "visible";
347
+ await turn(lf2, "50% 50%", dir > 0 ? 90 : -90, 0, "ease-out", ms);
348
+ }
349
+ done.forEach(e => e.remove()); busy = false;
350
+ }
351
+
352
+ // ---------- input
353
+ function initReader() {
354
+ $("#read").addEventListener("click", e => {
355
+ const a = e.target.closest("[data-act]");
356
+ if (a) { const k = a.dataset.act;
357
+ if (k === "toc") show("toc"); else if (k === "settings") togglePanel(); else flip(k === "next" ? 1 : -1); return; }
358
+ const fn = e.target.closest("[data-fn]");
359
+ if (fn) { e.preventDefault(); const li = $(`.flow .notes li[data-n="${fn.dataset.fn}"]`);
360
+ const n = $(".note"); n.innerHTML = `<button class="x" aria-label="${esc(L.close)}">×</button><p><b>${fn.dataset.fn}.</b> ${li ? li.innerHTML : ""}</p>`; n.classList.add("on");
361
+ n.querySelector(".x").onclick = () => n.classList.remove("on"); return; }
362
+ if (e.target.closest("a")) return;
363
+ const r = $(".area").getBoundingClientRect(), x = (e.clientX - r.left) / r.width;
364
+ if (x > .7) flip(1); else if (x < .3) flip(-1);
365
+ });
366
+ let sx = 0, sy = 0;
367
+ $(".stage").addEventListener("touchstart", e => { sx = e.touches[0].clientX; sy = e.touches[0].clientY; }, {passive: true});
368
+ $(".stage").addEventListener("touchend", e => { const dx = e.changedTouches[0].clientX - sx, dy = e.changedTouches[0].clientY - sy;
369
+ if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy) * 1.3) { flip(dx < 0 ? 1 : -1); e.preventDefault(); } });
370
+ addEventListener("keydown", e => { if (document.body.dataset.view !== "read") return;
371
+ if (["ArrowRight", "PageDown", " "].includes(e.key)) { e.preventDefault(); flip(1); }
372
+ else if (["ArrowLeft", "PageUp"].includes(e.key)) { e.preventDefault(); flip(-1); }
373
+ else if (e.key === "Escape") { $(".panel").classList.remove("on"); $(".note").classList.remove("on"); } });
374
+ let t; addEventListener("resize", () => { clearTimeout(t); t = setTimeout(() => { if (document.body.dataset.view !== "read") return;
375
+ const frac = G.n > 1 ? S.page / (G.n - 1) : 0; layout(); measure(); measureAll(); S.page = clampPage(Math.round(frac * (G.n - 1))); place(); }, 150); });
376
+ }
377
+ function togglePanel() {
378
+ const p = $(".panel"); if (p.classList.toggle("on") === false) return;
379
+ const th = [["light", L.light], ["sepia", L.sepia], ["night", L.night]];
380
+ p.innerHTML = `<h3>${esc(L.theme)}</h3><div class="seg">${th.map(([k, n]) => `<button data-th="${k}" aria-pressed="${S.theme === k}">${esc(n)}</button>`).join("")}</div>
381
+ <h3>${esc(L.size)}</h3><div class="seg"><button data-fs="-1" aria-label="smaller">A−</button><button disabled>${S.fs}</button><button data-fs="1" aria-label="larger">A+</button></div>
382
+ <button class="wide" data-pdf>${esc(L.print)}</button>`;
383
+ p.onclick = async e => { const b = e.target.closest("button"); if (!b) return;
384
+ if (b.dataset.th) { S.theme = b.dataset.th; applyTheme(); }
385
+ else if (b.dataset.fs) { S.fs = Math.max(13, Math.min(28, S.fs + +b.dataset.fs)); applyTheme(); }
386
+ else if ("pdf" in b.dataset) return printBook();
387
+ else return;
388
+ const frac = G.n > 1 ? S.page / (G.n - 1) : 0; measure(); measureAll(); S.page = clampPage(Math.round(frac * (G.n - 1))); place(); p.classList.remove("on"); togglePanel(); };
389
+ }
390
+ // iOS ignores @page and adds white margins, so a deployed book opens its prebuilt book.pdf; offline it prints the print layout.
391
+ async function printBook() {
392
+ try { const r = await fetch("book.pdf", {method: "HEAD"}); if (r.ok) { location.href = "book.pdf"; return; } } catch (e) {}
393
+ location.hash = "print"; buildPrint({}); setTimeout(() => print(), 300);
394
+ }
395
+
396
+ // ---------- print layout (also used by pdf.mjs)
397
+ function buildPrint(o) {
398
+ const mono = !!o.mono, vol = o.vol || "";
399
+ root.classList.toggle("mono", mono); document.body.classList.add("printing");
400
+ let css = "", h = `<section class="p-cover">${coverHTML(vol, mono)}</section>`;
401
+ h += `<section class="front p-title"><b>${esc(B.title)}</b>${B.subtitle ? `<i>${esc(B.subtitle)}</i>` : ""}<span>${esc(B.author)}</span></section>`;
402
+ if (B.copyright) h += `<section class="front pp p-copy">${esc(B.copyright)}</section>`;
403
+ if (B.dedication) h += `<section class="front pp p-ded">${esc(B.dedication)}</section>`;
404
+ let toc = `<section class="front pp p-toc"><h2>${esc(L.contents)}</h2>`, last = -1;
405
+ CH.forEach((c, i) => { if (c.p !== last) { last = c.p; if (B.parts[c.p].title) toc += `<h3><a href="#part-${c.p}">${esc(B.parts[c.p].title)}</a></h3>`; }
406
+ toc += `<a href="#ch-${i}"><span>${esc(c.t)}</span><span class="lead"></span><span class="n" data-for="ch-${i}"></span></a>`; });
407
+ h += toc + "</section>";
408
+ B.parts.forEach((p, pi) => {
409
+ if (p.title) h += `<section class="pp p-part" id="part-${pi}"><b>${esc(p.title)}</b>${p.intro ? `<div>${p.intro}</div>` : ""}</section>`;
410
+ else if (p.intro) h += `<section class="pp">${p.intro}</section>`;
411
+ CH.forEach((c, i) => { if (c.p !== pi) return;
412
+ css += `@page c${i}{@top-center{content:"${c.t.replace(/["\\]/g, "\\$&")}";font-family:var(--f-ui);font-size:7.5pt;letter-spacing:.15em;text-transform:uppercase;color:#777}}#ch-${i}{page:c${i}}`;
413
+ h += `<section class="pp" id="ch-${i}">${chHead(c)}${c.html}</section>`; });
414
+ });
415
+ const el = $("#print"); el.innerHTML = h;
416
+ let s = $("#pagecss"); if (!s) { s = document.createElement("style"); s.id = "pagecss"; document.head.append(s); }
417
+ s.textContent = css;
418
+ if (mono) { // colour emoji ignore `color`: wrap each glyph so only it is greyscaled
419
+ const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT), re = /\p{Emoji_Presentation}|\p{Extended_Pictographic}\uFE0F/u, ns = [];
420
+ while (w.nextNode()) if (re.test(w.currentNode.data)) ns.push(w.currentNode);
421
+ for (const n of ns) { const sp = document.createElement("span");
422
+ sp.innerHTML = esc(n.data).replace(/\p{Emoji_Presentation}\uFE0F?|\p{Extended_Pictographic}\uFE0F/gu, m => `<span class="gs">${m}</span>`); n.replaceWith(...sp.childNodes); }
423
+ }
424
+ }
425
+ window.__fillToc = map => { for (const s of document.querySelectorAll("#print [data-for]")) s.textContent = map[s.dataset.for] || ""; };
426
+ window.__buildPrint = buildPrint;
427
+
428
+ // ---------- boot
429
+ function boot() {
430
+ const hp = new URLSearchParams(location.hash.slice(1));
431
+ if (hp.has("og")) { document.body.classList.add("og");
432
+ $("#og").innerHTML = `<div class="ogc">${coverHTML()}</div><div><h1>${esc(B.title)}</h1>${B.subtitle ? `<p>${esc(B.subtitle)}</p>` : ""}<p class="au">${esc(B.author)}</p></div>`; }
433
+ else if (hp.has("print")) buildPrint({mono: hp.has("mono"), vol: hp.get("vol")});
434
+ initCover(); initToc(); initReader();
435
+ window.__ready = document.fonts.ready.then(() => true);
436
+ }
437
+ boot();
438
+ </script>
439
+ </body>
440
+ </html>
@@ -0,0 +1,45 @@
1
+ {
2
+ "classic": {
3
+ "note": "Non-fiction, essays, history, religious writing. Warm paper, gilt on navy.",
4
+ "fonts": { "body": "EB Garamond", "display": "Cormorant Garamond", "ui": "Plus Jakarta Sans" },
5
+ "cover": { "bg": "#0B1626", "ink": "#F2CF7A", "accent": "#F2CF7A", "pattern": "frame" },
6
+ "paper": "#F7F1E6", "ink": "#2A211B", "accent": "#8A5A2B", "desk": "#1B2433"
7
+ },
8
+ "romance": {
9
+ "note": "Romance, women's fiction. Blush paper, script display, rose accents.",
10
+ "fonts": { "body": "Lora", "display": "Playfair Display", "ui": "Nunito Sans" },
11
+ "cover": { "bg": "#F4D9D6", "ink": "#6B2737", "accent": "#B4475B", "pattern": "petals" },
12
+ "paper": "#FFF7F3", "ink": "#3B2328", "accent": "#B4475B", "desk": "#4A2A31"
13
+ },
14
+ "scifi": {
15
+ "note": "Science fiction, techno-thriller. Cool paper, geometric display, cyan glow.",
16
+ "fonts": { "body": "Source Serif 4", "display": "Orbitron", "ui": "Space Grotesk" },
17
+ "cover": { "bg": "#05070D", "ink": "#E6F6FF", "accent": "#3FD0FF", "pattern": "grid" },
18
+ "paper": "#F2F4F6", "ink": "#151A22", "accent": "#0A7FA8", "desk": "#0A0E16"
19
+ },
20
+ "fantasy": {
21
+ "note": "Fantasy, myth, adventure. Parchment paper, blackletter-flavoured display, deep green.",
22
+ "fonts": { "body": "Crimson Pro", "display": "Cinzel Decorative", "ui": "Alegreya Sans" },
23
+ "cover": { "bg": "#14301F", "ink": "#E9D9A6", "accent": "#C9A44C", "pattern": "frame" },
24
+ "paper": "#F3E9D2", "ink": "#2B2418", "accent": "#7A5A1E", "desk": "#1E2A1F"
25
+ },
26
+ "thriller": {
27
+ "note": "Mystery, crime, thriller, horror. Stark paper, condensed display, blood red.",
28
+ "fonts": { "body": "Libre Caslon Text", "display": "Bebas Neue", "ui": "Inter" },
29
+ "cover": { "bg": "#0E0E0E", "ink": "#F5F5F5", "accent": "#C8102E", "pattern": "slash" },
30
+ "paper": "#F6F5F2", "ink": "#121212", "accent": "#C8102E", "desk": "#161616"
31
+ },
32
+ "literary": {
33
+ "note": "Literary fiction, memoir, poetry. Off-white paper, quiet typography, no ornament.",
34
+ "fonts": { "body": "Newsreader", "display": "Newsreader", "ui": "Inter" },
35
+ "cover": { "bg": "#EDE8DF", "ink": "#1D1D1B", "accent": "#9C3D2E", "pattern": "none" },
36
+ "paper": "#FBF9F4", "ink": "#1D1D1B", "accent": "#9C3D2E", "desk": "#2E2B27"
37
+ },
38
+ "children": {
39
+ "note": "Children's and middle-grade. Big type, rounded fonts, bright colours.",
40
+ "fonts": { "body": "Andika", "display": "Baloo 2", "ui": "Nunito" },
41
+ "cover": { "bg": "#FFD23F", "ink": "#1B3B6F", "accent": "#EE4266", "pattern": "dots" },
42
+ "paper": "#FFFDF5", "ink": "#1B2A41", "accent": "#EE4266", "desk": "#3BCEAC",
43
+ "fontScale": 1.15
44
+ }
45
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "title": "The Lighthouse at Kepler Reach",
3
+ "subtitle": "A Novella",
4
+ "author": "A. N. Author",
5
+ "tagline": "The last keeper of the last light.",
6
+ "description": "A short science-fiction novella. Read it like a book, right on your phone.",
7
+ "lang": "en",
8
+ "style": "scifi",
9
+ "cover": "",
10
+ "url": "https://example.com/",
11
+ "copyright": "Copyright © 2026 A. N. Author\nAll rights reserved.\n\nFirst edition.",
12
+ "dedication": "For everyone who keeps a light on.",
13
+ "volumes": []
14
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 220"><rect width="400" height="220" fill="#0b1220"/><circle cx="300" cy="70" r="38" fill="#3fd0ff" opacity=".35"/><path d="M0 190 Q200 150 400 190 V220 H0Z" fill="#1d2a3a"/><rect x="186" y="80" width="28" height="112" fill="#e6f6ff"/><polygon points="180,80 220,80 200,58" fill="#3fd0ff"/><path d="M200 66 L400 20 L400 110Z" fill="#3fd0ff" opacity=".18"/></svg>