kaizen-agent 1.48.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 (183) hide show
  1. package/README.md +2 -2
  2. package/cli/build-web.ts +4 -2
  3. package/cli/hermes.ts +65 -0
  4. package/cli/install.ts +18 -5
  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/package.json +1 -1
  15. package/skills/kaizen/config.default.yml +4 -0
  16. package/skills/media/kaizen-book/SKILL.md +107 -0
  17. package/skills/media/kaizen-book/assets/reader.html +440 -0
  18. package/skills/media/kaizen-book/assets/styles.json +45 -0
  19. package/skills/media/kaizen-book/examples/book.json +14 -0
  20. package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
  21. package/skills/media/kaizen-book/examples/manuscript.md +127 -0
  22. package/skills/media/kaizen-book/references/interview.md +85 -0
  23. package/skills/media/kaizen-book/references/proofread.md +47 -0
  24. package/skills/media/kaizen-book/references/writing.md +53 -0
  25. package/skills/media/kaizen-book/scripts/build_book.py +279 -0
  26. package/skills/media/kaizen-book/scripts/check.sh +10 -0
  27. package/skills/media/kaizen-book/scripts/check_book.py +128 -0
  28. package/skills/media/kaizen-book/scripts/deploy.py +58 -0
  29. package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
  30. package/skills/media/kaizen-book/scripts/setup.sh +9 -0
  31. package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
  32. package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
  33. package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
  34. package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
  35. package/skills/media/kaizen-video/SKILL.md +112 -0
  36. package/skills/{kaizen-video → media/kaizen-video}/assets/CREDITS.txt +5 -0
  37. package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
  38. package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
  39. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
  40. package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
  41. package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
  42. package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
  43. package/skills/media/kaizen-video/film.html +246 -0
  44. package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
  45. package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
  46. package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
  47. package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
  48. package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
  49. package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
  50. package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
  51. package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
  52. package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
  53. package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
  54. package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
  55. package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
  56. package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
  57. package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
  58. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
  59. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
  60. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
  61. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
  62. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
  63. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
  64. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
  65. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
  66. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
  67. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
  68. package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
  69. package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
  70. package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
  71. package/skills/{kaizen-video → media/kaizen-video}/references/presets.md +22 -0
  72. package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
  73. package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
  74. package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
  75. package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
  76. package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
  77. package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
  78. package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
  79. package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
  80. package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
  81. package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
  82. package/skills/{kaizen-video → media/kaizen-video}/scripts/check.sh +8 -0
  83. package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
  84. package/skills/media/kaizen-video/scripts/make_film.py +263 -0
  85. package/skills/{kaizen-video → media/kaizen-video}/scripts/make_tiktok.py +8 -3
  86. package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
  87. package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
  88. package/skills/media/kaizen-video/scripts/srt.py +9 -0
  89. package/skills/media/kaizen-video/scripts/still.sh +7 -0
  90. package/web/board/views.ts +1 -1
  91. package/web/board.html +2 -0
  92. package/web/board.js +2 -2
  93. package/web/notebook.js +1 -1
  94. package/web/notes.html +2 -0
  95. package/web/term.js +1 -1
  96. package/web/video.html +298 -0
  97. package/web/video.js +81 -0
  98. package/web/video.ts +610 -0
  99. package/skills/kaizen-video/SKILL.md +0 -73
  100. package/skills/story-video/SKILL.md +0 -108
  101. package/skills/story-video/engine/package.json +0 -12
  102. package/skills/story-video/engine/src/Root.tsx +0 -24
  103. package/skills/story-video/engine/src/engine/Film.tsx +0 -405
  104. package/skills/story-video/engine/src/engine/presets.json +0 -58
  105. package/skills/story-video/engine/src/index.ts +0 -3
  106. package/skills/story-video/engine/tsconfig.json +0 -1
  107. package/skills/story-video/examples/sample-film/README.md +0 -12
  108. package/skills/story-video/examples/sample-film/film.json +0 -16
  109. package/skills/story-video/references/presets.md +0 -21
  110. package/skills/story-video/scripts/check.sh +0 -19
  111. package/skills/story-video/scripts/setup.sh +0 -25
  112. package/skills/story-video/scripts/srt.py +0 -30
  113. package/skills/story-video/scripts/still.sh +0 -10
  114. /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
  115. /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
  116. /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
  117. /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
  118. /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
  119. /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
  120. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Fredoka.ttf +0 -0
  121. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Montserrat.ttf +0 -0
  122. /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/SpecialElite.ttf +0 -0
  123. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/investigations.mp3 +0 -0
  124. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/lights.mp3 +0 -0
  125. /package/skills/{kaizen-video → media/kaizen-video}/assets/music/sunny.mp3 +0 -0
  126. /package/skills/{kaizen-video → media/kaizen-video}/examples/config.json +0 -0
  127. /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
  128. /package/skills/{kaizen-video → media/kaizen-video}/hyperframes.json +0 -0
  129. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/CREDITS.txt +0 -0
  130. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/README.md +0 -0
  131. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/ending.wav +0 -0
  132. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/reveal.wav +0 -0
  133. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/shutter.wav +0 -0
  134. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/stamp.wav +0 -0
  135. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/index.html +0 -0
  136. /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/package.json +0 -0
  137. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/CREDITS.txt +0 -0
  138. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/README.md +0 -0
  139. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
  140. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
  141. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
  142. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
  143. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
  144. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
  145. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
  146. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
  147. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
  148. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/index.html +0 -0
  149. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/package.json +0 -0
  150. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/CREDITS.txt +0 -0
  151. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/README.md +0 -0
  152. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
  153. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
  154. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
  155. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
  156. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
  157. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
  158. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
  159. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
  160. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
  161. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/index.html +0 -0
  162. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/package.json +0 -0
  163. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/CREDITS.txt +0 -0
  164. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/README.md +0 -0
  165. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
  166. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
  167. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
  168. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
  169. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
  170. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
  171. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
  172. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
  173. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/index.html +0 -0
  174. /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/package.json +0 -0
  175. /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
  176. /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
  177. /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
  178. /package/skills/{kaizen-video → media/kaizen-video}/scripts/render_cut.py +0 -0
  179. /package/skills/{kaizen-video → media/kaizen-video}/scripts/setup.sh +0 -0
  180. /package/skills/{kaizen-video → media/kaizen-video}/scripts/transcribe.py +0 -0
  181. /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
  182. /package/skills/{kaizen-video → media/kaizen-video}/tools/carve.mjs +0 -0
  183. /package/skills/{kaizen-video → media/kaizen-video}/tools/lib/main-module.mjs +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>