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.
- package/README.md +2 -2
- package/cli/build-web.ts +4 -2
- package/cli/hermes.ts +65 -0
- package/cli/install.ts +18 -5
- package/cli/localsend.test.ts +84 -0
- package/cli/localsend.ts +131 -0
- package/cli/notes.test.ts +6 -4
- package/cli/settings.ts +1 -0
- package/cli/state.test.ts +37 -0
- package/cli/state.ts +35 -20
- package/cli/video.test.ts +479 -0
- package/cli/video.ts +507 -0
- package/cli/web.ts +127 -1
- package/package.json +1 -1
- package/skills/kaizen/config.default.yml +4 -0
- package/skills/media/kaizen-book/SKILL.md +107 -0
- package/skills/media/kaizen-book/assets/reader.html +440 -0
- package/skills/media/kaizen-book/assets/styles.json +45 -0
- package/skills/media/kaizen-book/examples/book.json +14 -0
- package/skills/media/kaizen-book/examples/images/reach.svg +1 -0
- package/skills/media/kaizen-book/examples/manuscript.md +127 -0
- package/skills/media/kaizen-book/references/interview.md +85 -0
- package/skills/media/kaizen-book/references/proofread.md +47 -0
- package/skills/media/kaizen-book/references/writing.md +53 -0
- package/skills/media/kaizen-book/scripts/build_book.py +279 -0
- package/skills/media/kaizen-book/scripts/check.sh +10 -0
- package/skills/media/kaizen-book/scripts/check_book.py +128 -0
- package/skills/media/kaizen-book/scripts/deploy.py +58 -0
- package/skills/media/kaizen-book/scripts/pdf.mjs +99 -0
- package/skills/media/kaizen-book/scripts/setup.sh +9 -0
- package/skills/media/kaizen-book/scripts/verify.mjs +95 -0
- package/skills/{story-deck → media/kaizen-deck}/SKILL.md +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/check.sh +1 -1
- package/skills/{story-deck → media/kaizen-deck}/scripts/install_fonts.py +2 -2
- package/skills/media/kaizen-video/SKILL.md +112 -0
- package/skills/{kaizen-video → media/kaizen-video}/assets/CREDITS.txt +5 -0
- package/skills/media/kaizen-video/assets/film-fonts/Fraunces.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/Inter.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay-Italic.ttf +0 -0
- package/skills/media/kaizen-video/assets/film-fonts/PlayfairDisplay.ttf +0 -0
- package/skills/media/kaizen-video/examples/sample-film/README.md +10 -0
- package/skills/media/kaizen-video/examples/sample-film/film.json +63 -0
- package/skills/media/kaizen-video/film.html +246 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-cinematic-drama/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-fun-energetic/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-promo-booth/style.json +39 -0
- package/skills/media/kaizen-video/presets/film-viral-social/CREDITS.txt +1 -0
- package/skills/media/kaizen-video/presets/film-viral-social/README.md +2 -0
- package/skills/media/kaizen-video/presets/film-viral-social/style.json +39 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/CREDITS.txt +3 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/README.md +52 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/InstrumentSerif.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/fonts/SpaceMono.ttf +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/boing.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ding.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/ending.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/pop.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/reveal.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/shutter.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/stamp.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/tick.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/assets/sfx/whoosh.wav +0 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/index.html +272 -0
- package/skills/media/kaizen-video/presets/tiktok-explainer/package.json +10 -0
- package/skills/{kaizen-video → media/kaizen-video}/references/presets.md +22 -0
- package/skills/{story-video → media/kaizen-video}/references/production.md +2 -2
- package/skills/{story-video → media/kaizen-video}/references/revisions.md +9 -9
- package/skills/media/kaizen-video/scripts/__pycache__/intake.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_film.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/make_tiktok.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/render_cut.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/sfx.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/srt.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/transcribe.cpython-314.pyc +0 -0
- package/skills/media/kaizen-video/scripts/__pycache__/vo.cpython-314.pyc +0 -0
- package/skills/{kaizen-video → media/kaizen-video}/scripts/check.sh +8 -0
- package/skills/{story-video → media/kaizen-video}/scripts/intake.py +2 -2
- package/skills/media/kaizen-video/scripts/make_film.py +263 -0
- package/skills/{kaizen-video → media/kaizen-video}/scripts/make_tiktok.py +8 -3
- package/skills/{story-video → media/kaizen-video}/scripts/master.sh +12 -11
- package/skills/{story-video → media/kaizen-video}/scripts/sfx.py +3 -3
- package/skills/media/kaizen-video/scripts/srt.py +9 -0
- package/skills/media/kaizen-video/scripts/still.sh +7 -0
- package/web/board/views.ts +1 -1
- package/web/board.html +2 -0
- package/web/board.js +2 -2
- package/web/notebook.js +1 -1
- package/web/notes.html +2 -0
- package/web/term.js +1 -1
- package/web/video.html +298 -0
- package/web/video.js +81 -0
- package/web/video.ts +610 -0
- package/skills/kaizen-video/SKILL.md +0 -73
- package/skills/story-video/SKILL.md +0 -108
- package/skills/story-video/engine/package.json +0 -12
- package/skills/story-video/engine/src/Root.tsx +0 -24
- package/skills/story-video/engine/src/engine/Film.tsx +0 -405
- package/skills/story-video/engine/src/engine/presets.json +0 -58
- package/skills/story-video/engine/src/index.ts +0 -3
- package/skills/story-video/engine/tsconfig.json +0 -1
- package/skills/story-video/examples/sample-film/README.md +0 -12
- package/skills/story-video/examples/sample-film/film.json +0 -16
- package/skills/story-video/references/presets.md +0 -21
- package/skills/story-video/scripts/check.sh +0 -19
- package/skills/story-video/scripts/setup.sh +0 -25
- package/skills/story-video/scripts/srt.py +0 -30
- package/skills/story-video/scripts/still.sh +0 -10
- /package/skills/{story-deck → media/kaizen-deck}/examples/build-example.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/rules.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/references/story-arc.md +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/kit.js +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/prep_images.py +0 -0
- /package/skills/{story-deck → media/kaizen-deck}/scripts/qa.sh +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Fredoka.ttf +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/Montserrat.ttf +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/fonts/SpecialElite.ttf +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/music/investigations.mp3 +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/music/lights.mp3 +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/assets/music/sunny.mp3 +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/examples/config.json +0 -0
- /package/skills/{story-video → media/kaizen-video}/examples/sample-film/lines.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/hyperframes.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/educational-warning/package.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/boing.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/chime.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ding.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/pop.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/assets/sfx/whoosh.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-cheerful/package.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/clock.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ding.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/heart.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/projector.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/assets/sfx/whoosh.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-mystery/package.json +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/CREDITS.txt +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/README.md +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/clock.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ding.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/ending.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/heart.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/reveal.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/shutter.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/stamp.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/assets/sfx/whoosh.wav +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/index.html +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/presets/tiktok-scam-alert/package.json +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/licensing.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/references/script.md +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/music.sh +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/scripts/render_cut.py +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/scripts/setup.sh +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/scripts/transcribe.py +0 -0
- /package/skills/{story-video → media/kaizen-video}/scripts/vo.py +0 -0
- /package/skills/{kaizen-video → media/kaizen-video}/tools/carve.mjs +0 -0
- /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 => ({"&":"&","<":"<",">":">",'"':"""}[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>
|