sovereign-in-violet 1.0.3 → 1.0.4

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 (2) hide show
  1. package/dist/game.js +130 -17
  2. package/package.json +1 -1
package/dist/game.js CHANGED
@@ -1,10 +1,10 @@
1
- /* sovereign-in-violet 1.0.3, built from violet-hour.html */
1
+ /* sovereign-in-violet 1.0.4, built from violet-hour.html */
2
2
  (function(){
3
- document.head.insertAdjacentHTML("beforeend", "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=Zen+Antique&family=Italianno&family=Caveat:wght@500&family=Yuji+Syuku&family=Zen+Kurenaido&family=VT323&family=Shippori+Mincho:wght@400;600&display=swap\" rel=\"stylesheet\">" + "<style>" + "\n/* paranoia: whisperling margin notes */\n.note.whisperling{opacity:.85;letter-spacing:.04em}\n.note.whisperling .g{filter:blur(1px);opacity:.8}\n.note.whisperling .clear{text-shadow:0 0 6px rgba(201,162,74,.6)}\n:root{\n --bg:#EFE8F3; --surface:#F8F4FA; --raised:#E4D9EC; --ink:#2A1B38; --muted:#6A5680;\n --gold:#83620C; --gold-soft:#C9A33A; --violet:#5B3A82; --line:#D3C4DF; --danger:#8E2F4A;\n --display:\"IM Fell English\", \"Iowan Old Style\", \"Palatino Linotype\", Georgia, serif;\n --body:\"Spectral\", \"Iowan Old Style\", Georgia, \"Times New Roman\", serif;\n box-sizing:border-box;\n padding-top:env(safe-area-inset-top,0px);\n padding-bottom:env(safe-area-inset-bottom,0px);\n}\n@media (prefers-color-scheme: dark){\n :root:not([data-theme=\"light\"]){\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n }\n}\n:root[data-theme=\"dark\"]{\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n}\n*,*::before,*::after{box-sizing:inherit}\nhtml{scroll-padding-top:env(safe-area-inset-top,0px)}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--body);font-size:17px;line-height:1.62;-webkit-font-smoothing:antialiased}\nmain{max-width:640px;margin:0 auto;padding:20px 20px 120px}\nh1,h2,h3{font-family:var(--display);font-weight:400;line-height:1.15;margin:0}\nh1{font-size:2.3rem}\nh2{font-size:1.65rem;margin-bottom:.35rem}\nh3{font-size:1.2rem;color:var(--violet);margin:1.4rem 0 .5rem}\np{margin:0 0 1rem}\n.muted{color:var(--muted)}\n.small{font-size:.9rem}\nbutton{font:inherit;color:inherit;cursor:pointer}\nbutton:focus-visible,textarea:focus-visible,input:focus-visible{outline:2px solid var(--gold);outline-offset:2px}\n.btn{background:var(--violet);color:var(--surface);border:0;border-radius:3px;padding:.7rem 1.1rem;font-size:1rem}\n.btn:disabled{opacity:.5;cursor:default}\n.btn-quiet{background:transparent;border:1px solid var(--line);border-radius:3px;padding:.55rem .9rem;color:var(--ink);font-size:.95rem}\n.btn-link{background:none;border:0;padding:0;color:var(--violet);text-decoration:underline;text-underline-offset:3px;font-size:.95rem}\n.row{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}\n\n/* Title */\n.title{min-height:70vh;display:flex;flex-direction:column;justify-content:center;gap:1.2rem;padding-top:2rem}\n.sigil{width:92px;height:92px;color:var(--gold)}\n.title h1{font-size:3rem;letter-spacing:.01em}\n.title .lede{font-style:italic;color:var(--muted);max-width:30ch;font-size:1.1rem}\n.saves{margin-top:1rem;display:grid;gap:.5rem}\n.save{display:flex;justify-content:space-between;align-items:center;gap:.5rem;border-left:3px solid var(--gold-soft);background:var(--surface);padding:.7rem .9rem}\n.save button.open{background:none;border:0;text-align:left;padding:0;flex:1}\n.save .name{font-family:var(--display);font-size:1.2rem;display:block}\n\n/* Creation */\n.progress{display:flex;gap:4px;margin:0 0 1.4rem}\n.progress span{flex:1;height:3px;background:var(--line)}\n.progress span.done{background:var(--gold)}\n.step-kicker{font-style:italic;color:var(--gold);margin-bottom:.2rem}\n.picked{border-left:2px solid var(--line);padding-left:.8rem;margin:1.2rem 0;color:var(--muted);font-size:.92rem}\n.picked b{color:var(--ink);font-weight:500}\n\n/* Choices */\n.choices{display:grid;gap:.55rem;margin:1.2rem 0 .6rem}\n.choice{display:block;width:100%;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.8rem 1rem;line-height:1.4}\n.choice:hover{border-color:var(--gold-soft)}\n.choice[aria-pressed=\"true\"]{border-color:var(--gold);box-shadow:inset 3px 0 0 var(--gold)}\n.choice .hint{display:block;font-size:.82rem;color:var(--muted);font-style:italic;margin-top:.2rem}\n.extra{display:grid;grid-template-columns:1fr 1fr;gap:.55rem}\n.extra button{background:transparent;border:1px dashed var(--line);border-radius:3px;padding:.65rem;color:var(--muted)}\n.custom{margin-top:.6rem;display:grid;gap:.5rem}\n[hidden]{display:none !important}\ntextarea,input[type=text]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem;resize:vertical}\n\n/* Story */\n.scene{font-size:1.08rem;line-height:1.72}\n.scene p{margin-bottom:1.05rem}\n.action{font-style:italic;color:var(--muted);border-top:1px solid var(--line);padding-top:.8rem;margin:1.4rem 0 .4rem}\n.whisper{display:none}\n.scale.main .ends{color:var(--ink);font-size:.95rem}\n.scale.main .bar{height:3px}\n.omen{font-family:var(--display);font-style:italic;color:var(--gold);margin:0 0 1rem}\n.thinking{display:flex;gap:.8rem;align-items:center;color:var(--muted);font-style:italic;margin:1.5rem 0}\n.thinking svg{width:34px;height:34px;color:var(--gold);flex:none}\n@media (prefers-reduced-motion:no-preference){ .thinking svg{animation:breathe 2.6s ease-in-out infinite} }\n@keyframes breathe{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}\n.card{background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:1rem 1.1rem;margin:1.2rem 0}\n.card.gold{border-color:var(--gold-soft);box-shadow:inset 3px 0 0 var(--gold)}\n.card.door{border-color:var(--danger);box-shadow:inset 3px 0 0 var(--danger)}\n.card h3{margin-top:0}\n.error{color:var(--danger)}\n\n/* Sheet */\n.traits{display:grid;grid-template-columns:1fr 1fr;gap:.4rem 1.2rem}\n.trait{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding:.35rem 0}\n.pips{display:flex;gap:3px}\n.pips i{width:9px;height:9px;border-radius:50%;border:1px solid var(--gold);display:block}\n.pips i.on{background:var(--gold)}\n.track{display:flex;gap:5px;align-items:center}\n.track i{width:18px;height:18px;border:1.5px solid var(--muted);display:block;border-radius:2px}\n.track i.on{background:var(--danger);border-color:var(--danger)}\n.track.stab i.on{background:var(--violet);border-color:var(--violet)}\n.scale{margin:.7rem 0 1rem}\n.scale .ends{display:flex;justify-content:space-between;font-size:.85rem;color:var(--muted);gap:1rem}\n.scale .ends span:last-child{text-align:right}\n.scale .bar{position:relative;height:2px;background:var(--line);margin:.55rem 0 .2rem}\n.scale .bar::after{content:\"\";position:absolute;left:50%;top:-4px;width:1px;height:10px;background:var(--muted)}\n.scale .dot{position:absolute;top:-6px;width:14px;height:14px;border-radius:50%;background:var(--gold);transform:translateX(-50%);border:2px solid var(--surface)}\nul.plain{list-style:none;padding:0;margin:0}\nul.plain li{padding:.45rem 0;border-bottom:1px solid var(--line)}\nul.plain li:last-child{border-bottom:0}\n.empty{color:var(--muted);font-style:italic}\n\n/* Nav */\nnav.tabs{position:fixed;left:0;right:0;bottom:0;background:var(--surface);border-top:1px solid var(--line);padding-bottom:env(safe-area-inset-bottom,0px);z-index:10}\nnav.tabs .inner{max-width:640px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr)}\nnav.tabs button{background:none;border:0;padding:.75rem .2rem;font-family:var(--display);font-size:1.02rem;color:var(--muted);border-top:2px solid transparent}\nnav.tabs button[aria-current=\"page\"]{color:var(--gold);border-top-color:var(--gold)}\n.seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden}\n.seg button{background:none;border:0;padding:.45rem .8rem;font-size:.92rem;color:var(--muted)}\n.seg button[aria-pressed=\"true\"]{background:var(--violet);color:var(--surface)}\n\n/* Japanese: mincho faces matching the English serif look, no fake italics, Japanese line-breaking */\nhtml[lang=\"ja\"]{\n --display:\"Zen Antique\",\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",serif;\n --body:\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",\"Noto Serif JP\",serif;\n}\nhtml[lang=\"ja\"] body{line-height:1.75;line-break:strict;overflow-wrap:anywhere}\nhtml[lang=\"ja\"] *{font-style:normal !important}\nhtml[lang=\"ja\"] h1,html[lang=\"ja\"] h2,html[lang=\"ja\"] h3{line-height:1.35;letter-spacing:.04em}\nhtml[lang=\"ja\"] .title .lede{max-width:16em}\nhtml[lang=\"ja\"] .step-kicker,html[lang=\"ja\"] .omen,html[lang=\"ja\"] .whisper,html[lang=\"ja\"] .action{letter-spacing:.03em}\n\n/* Page effects: always marked by his eye, always brief, never change what you chose */\n#pfxEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity .5s ease;z-index:60}\n#pfxEye svg{width:100%;height:100%}\n#pfxEye.on{opacity:.9}\n.pfx-flip{transform:scaleX(-1);transition:transform .3s ease}\n.pfx-unflip{transition:transform .3s ease}\n.pfx-mirror{display:block;transform:scaleX(-1)}\n.pfx-glow{border-color:var(--violet) !important;box-shadow:inset 0 0 0 2px var(--violet) !important}\n.pfx-voice{color:var(--violet) !important;font-family:var(--display);font-style:italic}\n.pfx-line{color:var(--violet);font-family:var(--display);font-style:italic;opacity:0;transition:opacity .8s ease;margin:.6rem 0 0}\n.pfx-line.on{opacity:1}\n\n/* Hidden race hints: a faint watching eye and a violet tint that deepen with his Manifestation; your resolve as a small steady light */\nbody{background-image:linear-gradient(rgba(106,64,160,var(--taint,0)),rgba(106,64,160,var(--taint,0)));background-attachment:fixed}\n#watchEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity 2s ease;z-index:55}\n#watchEye svg{width:100%;height:100%}\n.resolve-light{display:inline-block;width:.55rem;height:.55rem;border-radius:50%;background:var(--gold);vertical-align:middle;margin-left:.5rem;opacity:calc(.15 + var(--r,0)*.85);box-shadow:0 0 calc(2px + var(--r,0)*10px) var(--gold)}\n\n.faded-end{animation:fadeAway 9s ease forwards}\n@keyframes fadeAway{from{opacity:1}to{opacity:.45}}\n@media (prefers-reduced-motion:reduce){.faded-end{animation:none;opacity:.6}}\n.hint.pull{color:var(--gold);font-style:normal;margin-bottom:.2rem}\n.learned{margin-top:1rem;padding-top:.8rem;border-top:1px solid var(--line)}\n.learned li{margin:.25rem 0}\n\n/* Pictures: VHS-tape stills */\n.vhs{position:relative;margin:0 0 1rem;border-radius:6px;overflow:hidden;background:#0b0714;box-shadow:0 0 0 1px var(--line)}\n.vhs canvas{display:block;width:100%;height:auto;aspect-ratio:4/3}\n.vhs .osd{position:absolute;top:.5rem;left:.7rem;font-family:\"VT323\",ui-monospace,monospace;font-size:1.25rem;color:#f1ecf8;text-shadow:1px 1px 0 #000,-1px 0 0 rgba(255,0,80,.5);letter-spacing:.06em;font-style:normal !important}\n.vhs .osd.ts{top:auto;bottom:.4rem;left:auto;right:.7rem}\n.vhs .osd.rec{color:#e0a0ff}\n.title .vhs{max-width:26rem}\n.more-pulls summary{cursor:pointer;color:var(--violet);margin:.6rem 0}\n.choice.gift-choice{font-family:var(--his);font-size:1.45rem;line-height:1.1;color:var(--gold);border-color:var(--gold-soft);background:linear-gradient(90deg,rgba(201,162,74,.16),rgba(255,250,238,.6));box-shadow:0 0 14px rgba(201,162,74,.3);animation:giftGlow 3.2s ease-in-out infinite}\n.choice.gift-choice::before{content:\"✦ \";color:var(--gold)}\n@keyframes giftGlow{0%,100%{box-shadow:0 0 10px rgba(201,162,74,.2)}50%{box-shadow:0 0 18px rgba(201,162,74,.45)}}\n@media (prefers-reduced-motion:reduce){.choice.gift-choice{animation:none}}\n.choice.gift-own{font-family:var(--display);font-style:italic;color:var(--ink);border:1px dashed var(--gold-soft)}\n.choice.gift-own::before{content:\"✧ \";color:var(--gold)}\n\n/* ---------- The book ---------- */\n:root, :root[data-theme=\"dark\"], :root:not([data-theme=\"light\"]){\n --bg:#1b1320; --surface:#f1e6cf; --raised:#e6d7b8; --ink:#2b1f2a; --muted:#6b5a55;\n --gold:#8a6a1c; --gold-soft:#b8923a; --violet:#5b3a82; --line:#cbb894; --danger:#8e2f4a;\n --his:\"Italianno\",\"Yuji Syuku\",cursive; --reader:\"Caveat\",\"Zen Kurenaido\",cursive;\n}\n@media (prefers-color-scheme: dark){ :root:not([data-theme=\"light\"]){ --surface:#ddd0b2; --raised:#d0c1a0; } }\nbody{background:radial-gradient(ellipse at 50% 0%, #2a1d33 0%, #140e18 70%) fixed}\nmain{background:var(--surface);background-image:radial-gradient(ellipse at 50% 40%, rgba(255,250,235,.55), rgba(255,250,235,0) 65%),radial-gradient(circle at 20% 80%, rgba(120,90,50,.08), rgba(0,0,0,0) 40%),radial-gradient(circle at 85% 15%, rgba(120,90,50,.07), rgba(0,0,0,0) 35%);\n margin:16px auto 110px;padding:28px 26px 40px;box-shadow:0 0 0 1px rgba(90,60,30,.25), 0 18px 50px rgba(0,0,0,.55), inset 0 0 60px rgba(120,90,50,.18);border-radius:2px 6px 6px 2px;min-height:80vh;position:relative}\nmain::before{content:\"\";position:absolute;inset:0;pointer-events:none;border-left:6px solid rgba(90,60,30,.12);border-radius:2px 0 0 2px}\n.tabs{background:#e6d7b8 !important;border-top:1px solid #b8a37a !important}\n.card{background:rgba(255,250,238,.55)}\n.choice{background:rgba(255,250,238,.6)}\n/* drop cap on each page's first paragraph */\n.scene p:first-child::first-letter{float:left;font-family:var(--display);font-size:3.3em;line-height:.82;padding:.08em .08em 0 0;color:var(--gold)}\nhtml[lang=\"ja\"] .scene p:first-child::first-letter{float:none;font-size:inherit;color:inherit;padding:0}\n.folio{text-align:center;font-family:var(--display);color:var(--muted);font-size:.9rem;margin-top:1.6rem;letter-spacing:.2em}\n/* illustration plates */\n.vhs{background:#f7eedb;padding:7px 7px 0;border:1px solid #6b5436;box-shadow:0 0 0 3px #f7eedb, 0 0 0 4px #6b5436, 0 6px 16px rgba(60,40,20,.25);border-radius:1px;margin:.4rem 4px 1.3rem}\n.vhs canvas{border:1px solid #6b5436}\n.vhs figcaption{font-family:var(--display);font-style:italic;text-align:center;color:#5a4630;font-size:.92rem;padding:.35rem .4rem .45rem}\n/* margin notes */\n.note{display:block;margin:.2rem 0 1rem;max-width:88%;line-height:1.15}\n.note.gold{font-family:var(--his);color:var(--gold);font-size:1.75rem;transform:rotate(-1.6deg);margin-left:12%}\n.note.reader{font-family:var(--reader);color:var(--violet);font-size:1.3rem;transform:rotate(1.2deg);margin-left:2%}\n.note.reader .sig{display:block;font-size:1.05rem;opacity:.8;margin-left:40%}\nhtml[lang=\"ja\"] .note.gold{font-size:1.2rem} html[lang=\"ja\"] .choice.gift-choice{font-size:1.05rem} html[lang=\"ja\"] .note.reader{font-size:1.05rem}\nu.vu{text-decoration:underline wavy var(--violet);text-decoration-thickness:1.5px;text-underline-offset:4px}\n@media (min-width: 1000px){ main{max-width:900px;padding-right:270px} .note{float:right;clear:right;width:215px;margin:0 -250px .8rem 0} .note.gold{margin-left:0} .note.reader{margin-left:0} }\n/* his voice everywhere is his own hand, in tarnished gold */\n.pfx-voice{color:var(--gold) !important;font-family:var(--his) !important;font-style:normal !important;font-size:1.45em;line-height:1.1}\nhtml[lang=\"ja\"] .pfx-voice{font-size:1.05em}\n\n/* ---------- The cover ---------- */\n.cover{min-height:88vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;cursor:pointer;text-align:center;margin:-28px -26px -40px;padding:18px 12px 30px;background:#0b0712;border-radius:2px 6px 6px 2px}\n.cover-art{position:relative;width:min(100%,420px)}\n.cover-art canvas{display:block;width:100%;height:auto;aspect-ratio:2/3;border-radius:3px;box-shadow:0 0 0 1px #6b5436, 0 0 40px rgba(154,99,224,.25)}\n.cover .stars{display:none}\n.cover .stars i{position:absolute;background:#fff6dc;border-radius:50%;opacity:.2;animation:twinkle 2.8s ease-in-out infinite}\n@keyframes twinkle{0%,100%{opacity:.15;transform:scale(.8)}50%{opacity:1;transform:scale(1.4)}}\n.cover h1{color:#e9dcb8;font-size:2.4rem;text-shadow:0 2px 12px rgba(0,0,0,.8)}\n.cover-tap{color:var(--gold-soft);font-family:var(--display);font-style:italic;letter-spacing:.12em;animation:tapPulse 2.4s ease-in-out infinite;margin:0}\n@keyframes tapPulse{0%,100%{opacity:.45}50%{opacity:1}}\n@media (prefers-reduced-motion:reduce){ .cover .stars i,.cover-tap{animation:none;opacity:.7} }\n\n/* tarot plates */\n.tarot{margin:.4rem auto 1.2rem;max-width:300px}\n.tarot-card{position:relative;background:#1a1024;padding:58px 16px 64px;border-radius:10px;box-shadow:0 0 0 2px #b8923a, 0 0 0 5px #1a1024, 0 0 0 6px #b8923a, 0 10px 26px rgba(0,0,0,.45);background-image:radial-gradient(circle at 12px 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at 12px calc(100% - 12px),#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) calc(100% - 12px),#b8923a 1.5px,transparent 2px)}\n.tarot-card canvas{display:block;width:100%;height:auto;aspect-ratio:4/3;border:1px solid #b8923a;border-radius:2px}\n.tarot .arc-num,.tarot .arc-name{position:absolute;left:0;right:0;text-align:center;font-family:var(--display);color:#e0c070;letter-spacing:.14em}\n.tarot .arc-num{top:14px;font-size:1.15rem}\n.tarot .arc-num::after,.tarot .arc-name::before{content:\"✧ ✦ ✧\";display:block;font-size:.6rem;letter-spacing:.5em;color:#b8923a;margin:.35rem 0 0}\n.tarot .arc-name::before{margin:0 0 .35rem}\n.tarot .arc-name{bottom:12px;font-size:1rem;text-transform:uppercase}\n.tarot.reversed .tarot-card{transform:rotate(180deg)}\n.tarot figcaption{text-align:center;font-family:var(--display);font-style:italic;color:var(--muted);font-size:.9rem;margin-top:.5rem}\nhtml[lang=\"ja\"] .tarot .arc-name{text-transform:none;letter-spacing:.06em}\n/* his offer: a card held out to you */\n.choice.gift-choice{border-radius:9px !important;border:1px solid var(--gold-soft) !important;box-shadow:0 0 0 3px rgba(255,250,238,.6), 0 0 0 4px var(--gold-soft), 0 6px 16px rgba(120,80,20,.25) !important;transform:rotate(-1.2deg);position:relative;padding-left:2.2rem !important}\n.choice.gift-choice::before{content:\"✦\";position:absolute;left:.7rem;top:.35rem;font-size:.9rem;color:var(--gold)}\n.choice.gift-choice::after{content:\"✦\";position:absolute;right:.7rem;bottom:.3rem;font-size:.9rem;color:var(--gold);transform:rotate(180deg)}\n/* epigraphs, recap, engine */\n.epi{margin:0 0 1.5rem;padding:.1rem 0 .1rem 1rem;border-left:2px solid var(--gold-soft)}\n.epi-q{font-family:var(--display);font-style:italic;font-size:1.12rem;line-height:1.4;margin:0}\n.epi-q::before{content:\"\\201C\"} .epi-q::after{content:\"\\201D\"}\n.epi-by{margin:.35rem 0 0;color:var(--muted);font-size:.88rem;letter-spacing:.03em}\n.card .epi{margin:1rem 0}\n.recap{border:1px dashed var(--line);border-radius:3px;padding:.6rem .9rem;margin:0 0 1.2rem;color:var(--muted);font-size:.95rem}\n.recap summary{cursor:pointer;font-family:var(--display);color:var(--violet)}\n.recap p{margin:.4rem 0 0}\ninput[type=password]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem}\n/* scene header, choice icons and portraits */\n.story-head{display:flex;justify-content:space-between;align-items:center;gap:.8rem;margin-bottom:.4rem}\n.story-head .step-kicker{margin:0}\n.status-strip{display:flex;gap:2px;flex:none}\n.status-strip .st{width:28px;height:28px;display:block}\n.cicon{width:1.15em;height:1.15em;vertical-align:-.2em;margin-right:.55rem;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;opacity:.75}\n.choice.gift-choice .cicon{opacity:1}\n.cast{display:flex;flex-wrap:wrap;gap:.9rem;margin:.2rem 0 1rem}\n.por{margin:0;text-align:center;width:64px}\n.por canvas{display:block;width:56px;height:56px;margin:0 auto;border-radius:50%;box-shadow:0 0 0 1.5px #6b5436}\n.por.sovereign canvas{box-shadow:0 0 0 1.5px var(--gold-soft),0 0 10px rgba(201,162,74,.45)}\n.por.servant canvas{box-shadow:0 0 0 1.5px var(--violet)}\n.por figcaption{font-family:var(--display);font-size:.82rem;color:var(--muted);margin-top:.25rem;line-height:1.2;overflow-wrap:anywhere}\n" + "</style>");
3
+ document.head.insertAdjacentHTML("beforeend", "<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=IM+Fell+English:ital@0;1&family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=Zen+Antique&family=Italianno&family=Caveat:wght@500&family=Yuji+Syuku&family=Zen+Kurenaido&family=VT323&family=Shippori+Mincho:wght@400;600&display=swap\" rel=\"stylesheet\">" + "<style>" + "\n/* paranoia: whisperling margin notes */\n.note.whisperling{opacity:.85;letter-spacing:.04em}\n.note.whisperling .g{filter:blur(1px);opacity:.8}\n.note.whisperling .clear{text-shadow:0 0 6px rgba(201,162,74,.6)}\n:root{\n --bg:#EFE8F3; --surface:#F8F4FA; --raised:#E4D9EC; --ink:#2A1B38; --muted:#6A5680;\n --gold:#83620C; --gold-soft:#C9A33A; --violet:#5B3A82; --line:#D3C4DF; --danger:#8E2F4A;\n --display:\"IM Fell English\", \"Iowan Old Style\", \"Palatino Linotype\", Georgia, serif;\n --body:\"Spectral\", \"Iowan Old Style\", Georgia, \"Times New Roman\", serif;\n box-sizing:border-box;\n padding-top:env(safe-area-inset-top,0px);\n padding-bottom:env(safe-area-inset-bottom,0px);\n}\n@media (prefers-color-scheme: dark){\n :root:not([data-theme=\"light\"]){\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n }\n}\n:root[data-theme=\"dark\"]{\n --bg:#1A1224; --surface:#241932; --raised:#302241; --ink:#EAE1F2; --muted:#A895BB;\n --gold:#DDB654; --gold-soft:#9C7E2C; --violet:#A07BD0; --line:#3B2C4E; --danger:#D9708C;\n}\n*,*::before,*::after{box-sizing:inherit}\nhtml{scroll-padding-top:env(safe-area-inset-top,0px)}\nbody{margin:0;background:var(--bg);color:var(--ink);font-family:var(--body);font-size:17px;line-height:1.62;-webkit-font-smoothing:antialiased}\nmain{max-width:640px;margin:0 auto;padding:20px 20px 120px}\nh1,h2,h3{font-family:var(--display);font-weight:400;line-height:1.15;margin:0}\nh1{font-size:2.3rem}\nh2{font-size:1.65rem;margin-bottom:.35rem}\nh3{font-size:1.2rem;color:var(--violet);margin:1.4rem 0 .5rem}\np{margin:0 0 1rem}\n.muted{color:var(--muted)}\n.small{font-size:.9rem}\nbutton{font:inherit;color:inherit;cursor:pointer}\nbutton:focus-visible,textarea:focus-visible,input:focus-visible{outline:2px solid var(--gold);outline-offset:2px}\n.btn{background:var(--violet);color:var(--surface);border:0;border-radius:3px;padding:.7rem 1.1rem;font-size:1rem}\n.btn:disabled{opacity:.5;cursor:default}\n.btn-quiet{background:transparent;border:1px solid var(--line);border-radius:3px;padding:.55rem .9rem;color:var(--ink);font-size:.95rem}\n.btn-link{background:none;border:0;padding:0;color:var(--violet);text-decoration:underline;text-underline-offset:3px;font-size:.95rem}\n.row{display:flex;gap:.6rem;flex-wrap:wrap;align-items:center}\n\n/* Title */\n.title{min-height:70vh;display:flex;flex-direction:column;justify-content:center;gap:1.2rem;padding-top:2rem}\n.sigil{width:92px;height:92px;color:var(--gold)}\n.title h1{font-size:3rem;letter-spacing:.01em}\n.title .lede{font-style:italic;color:var(--muted);max-width:30ch;font-size:1.1rem}\n.saves{margin-top:1rem;display:grid;gap:.5rem}\n.save{display:flex;justify-content:space-between;align-items:center;gap:.5rem;border-left:3px solid var(--gold-soft);background:var(--surface);padding:.7rem .9rem}\n.save button.open{background:none;border:0;text-align:left;padding:0;flex:1}\n.save .name{font-family:var(--display);font-size:1.2rem;display:block}\n\n/* Creation */\n.progress{display:flex;gap:4px;margin:0 0 1.4rem}\n.progress span{flex:1;height:3px;background:var(--line)}\n.progress span.done{background:var(--gold)}\n.step-kicker{font-style:italic;color:var(--gold);margin-bottom:.2rem}\n.picked{border-left:2px solid var(--line);padding-left:.8rem;margin:1.2rem 0;color:var(--muted);font-size:.92rem}\n.picked b{color:var(--ink);font-weight:500}\n\n/* Choices */\n.choices{display:grid;gap:.55rem;margin:1.2rem 0 .6rem}\n.choice{display:block;width:100%;text-align:left;background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.8rem 1rem;line-height:1.4}\n.choice:hover{border-color:var(--gold-soft)}\n.choice[aria-pressed=\"true\"]{border-color:var(--gold);box-shadow:inset 3px 0 0 var(--gold)}\n.choice .hint{display:block;font-size:.82rem;color:var(--muted);font-style:italic;margin-top:.2rem}\n.extra{display:grid;grid-template-columns:1fr 1fr;gap:.55rem}\n.extra button{background:transparent;border:1px dashed var(--line);border-radius:3px;padding:.65rem;color:var(--muted)}\n.custom{margin-top:.6rem;display:grid;gap:.5rem}\n[hidden]{display:none !important}\ntextarea,input[type=text]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem;resize:vertical}\n\n/* Story */\n.scene{font-size:1.08rem;line-height:1.72}\n.scene p{margin-bottom:1.05rem}\n.action{font-style:italic;color:var(--muted);border-top:1px solid var(--line);padding-top:.8rem;margin:1.4rem 0 .4rem}\n.whisper{display:none}\n.scale.main .ends{color:var(--ink);font-size:.95rem}\n.scale.main .bar{height:3px}\n.omen{font-family:var(--display);font-style:italic;color:var(--gold);margin:0 0 1rem}\n.thinking{display:flex;gap:.8rem;align-items:center;color:var(--muted);font-style:italic;margin:1.5rem 0}\n.thinking svg{width:34px;height:34px;color:var(--gold);flex:none}\n@media (prefers-reduced-motion:no-preference){ .thinking svg{animation:breathe 2.6s ease-in-out infinite} }\n@keyframes breathe{0%,100%{opacity:.35;transform:scale(.94)}50%{opacity:1;transform:scale(1)}}\n.card{background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:1rem 1.1rem;margin:1.2rem 0}\n.card.gold{border-color:var(--gold-soft);box-shadow:inset 3px 0 0 var(--gold)}\n.card.door{border-color:var(--danger);box-shadow:inset 3px 0 0 var(--danger)}\n.card h3{margin-top:0}\n.error{color:var(--danger)}\n\n/* Sheet */\n.traits{display:grid;grid-template-columns:1fr 1fr;gap:.4rem 1.2rem}\n.trait{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid var(--line);padding:.35rem 0}\n.pips{display:flex;gap:3px}\n.pips i{width:9px;height:9px;border-radius:50%;border:1px solid var(--gold);display:block}\n.pips i.on{background:var(--gold)}\n.track{display:flex;gap:5px;align-items:center}\n.track i{width:18px;height:18px;border:1.5px solid var(--muted);display:block;border-radius:2px}\n.track i.on{background:var(--danger);border-color:var(--danger)}\n.track.stab i.on{background:var(--violet);border-color:var(--violet)}\n.scale{margin:.7rem 0 1rem}\n.scale .ends{display:flex;justify-content:space-between;font-size:.85rem;color:var(--muted);gap:1rem}\n.scale .ends span:last-child{text-align:right}\n.scale .bar{position:relative;height:2px;background:var(--line);margin:.55rem 0 .2rem}\n.scale .bar::after{content:\"\";position:absolute;left:50%;top:-4px;width:1px;height:10px;background:var(--muted)}\n.scale .dot{position:absolute;top:-6px;width:14px;height:14px;border-radius:50%;background:var(--gold);transform:translateX(-50%);border:2px solid var(--surface)}\nul.plain{list-style:none;padding:0;margin:0}\nul.plain li{padding:.45rem 0;border-bottom:1px solid var(--line)}\nul.plain li:last-child{border-bottom:0}\n.empty{color:var(--muted);font-style:italic}\n\n/* Nav */\nnav.tabs{position:fixed;left:0;right:0;bottom:0;background:var(--surface);border-top:1px solid var(--line);padding-bottom:env(safe-area-inset-bottom,0px);z-index:10}\nnav.tabs .inner{max-width:640px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr)}\nnav.tabs button{background:none;border:0;padding:.75rem .2rem;font-family:var(--display);font-size:1.02rem;color:var(--muted);border-top:2px solid transparent}\nnav.tabs button[aria-current=\"page\"]{color:var(--gold);border-top-color:var(--gold)}\n.seg{display:inline-flex;border:1px solid var(--line);border-radius:3px;overflow:hidden}\n.seg button{background:none;border:0;padding:.45rem .8rem;font-size:.92rem;color:var(--muted)}\n.seg button[aria-pressed=\"true\"]{background:var(--violet);color:var(--surface)}\n\n/* Japanese: mincho faces matching the English serif look, no fake italics, Japanese line-breaking */\nhtml[lang=\"ja\"]{\n --display:\"Zen Antique\",\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",serif;\n --body:\"Shippori Mincho\",\"Hiragino Mincho ProN\",\"Yu Mincho\",\"Noto Serif JP\",serif;\n}\nhtml[lang=\"ja\"] body{line-height:1.75;line-break:strict;overflow-wrap:anywhere}\nhtml[lang=\"ja\"] *{font-style:normal !important}\nhtml[lang=\"ja\"] h1,html[lang=\"ja\"] h2,html[lang=\"ja\"] h3{line-height:1.35;letter-spacing:.04em}\nhtml[lang=\"ja\"] .title .lede{max-width:16em}\nhtml[lang=\"ja\"] .step-kicker,html[lang=\"ja\"] .omen,html[lang=\"ja\"] .whisper,html[lang=\"ja\"] .action{letter-spacing:.03em}\n\n/* Page effects: always marked by his eye, always brief, never change what you chose */\n#pfxEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity .5s ease;z-index:60}\n#pfxEye svg{width:100%;height:100%}\n#pfxEye.on{opacity:.9}\n.pfx-flip{transform:scaleX(-1);transition:transform .3s ease}\n.pfx-unflip{transition:transform .3s ease}\n.pfx-mirror{display:block;transform:scaleX(-1)}\n.pfx-glow{border-color:var(--violet) !important;box-shadow:inset 0 0 0 2px var(--violet) !important}\n.pfx-voice{color:var(--violet) !important;font-family:var(--display);font-style:italic}\n.pfx-line{color:var(--violet);font-family:var(--display);font-style:italic;opacity:0;transition:opacity .8s ease;margin:.6rem 0 0}\n.pfx-line.on{opacity:1}\n\n/* Hidden race hints: a faint watching eye and a violet tint that deepen with his Manifestation; your resolve as a small steady light */\nbody{background-image:linear-gradient(rgba(106,64,160,var(--taint,0)),rgba(106,64,160,var(--taint,0)));background-attachment:fixed}\n#watchEye{position:fixed;top:calc(env(safe-area-inset-top,0px) + 10px);right:12px;width:30px;height:30px;color:var(--gold-soft);opacity:0;pointer-events:none;transition:opacity 2s ease;z-index:55}\n#watchEye svg{width:100%;height:100%}\n.resolve-light{display:inline-block;width:.55rem;height:.55rem;border-radius:50%;background:var(--gold);vertical-align:middle;margin-left:.5rem;opacity:calc(.15 + var(--r,0)*.85);box-shadow:0 0 calc(2px + var(--r,0)*10px) var(--gold)}\n\n.faded-end{animation:fadeAway 9s ease forwards}\n@keyframes fadeAway{from{opacity:1}to{opacity:.45}}\n@media (prefers-reduced-motion:reduce){.faded-end{animation:none;opacity:.6}}\n.hint.pull{color:var(--gold);font-style:normal;margin-bottom:.2rem}\n.learned{margin-top:1rem;padding-top:.8rem;border-top:1px solid var(--line)}\n.learned li{margin:.25rem 0}\n\n/* Pictures: VHS-tape stills */\n.vhs{position:relative;margin:0 0 1rem;border-radius:6px;overflow:hidden;background:#0b0714;box-shadow:0 0 0 1px var(--line)}\n.vhs canvas{display:block;width:100%;height:auto;aspect-ratio:4/3}\n.vhs .osd{position:absolute;top:.5rem;left:.7rem;font-family:\"VT323\",ui-monospace,monospace;font-size:1.25rem;color:#f1ecf8;text-shadow:1px 1px 0 #000,-1px 0 0 rgba(255,0,80,.5);letter-spacing:.06em;font-style:normal !important}\n.vhs .osd.ts{top:auto;bottom:.4rem;left:auto;right:.7rem}\n.vhs .osd.rec{color:#e0a0ff}\n.title .vhs{max-width:26rem}\n.more-pulls summary{cursor:pointer;color:var(--violet);margin:.6rem 0}\n.choice.gift-choice{font-family:var(--his);font-size:1.45rem;line-height:1.1;color:var(--gold);border-color:var(--gold-soft);background:linear-gradient(90deg,rgba(201,162,74,.16),rgba(255,250,238,.6));box-shadow:0 0 14px rgba(201,162,74,.3);animation:giftGlow 3.2s ease-in-out infinite}\n.choice.gift-choice::before{content:\"✦ \";color:var(--gold)}\n@keyframes giftGlow{0%,100%{box-shadow:0 0 10px rgba(201,162,74,.2)}50%{box-shadow:0 0 18px rgba(201,162,74,.45)}}\n@media (prefers-reduced-motion:reduce){.choice.gift-choice{animation:none}}\n.choice.gift-own{font-family:var(--display);font-style:italic;color:var(--ink);border:1px dashed var(--gold-soft)}\n.choice.gift-own::before{content:\"✧ \";color:var(--gold)}\n\n/* ---------- The book ---------- */\n:root, :root[data-theme=\"dark\"], :root:not([data-theme=\"light\"]){\n --bg:#1b1320; --surface:#f1e6cf; --raised:#e6d7b8; --ink:#2b1f2a; --muted:#6b5a55;\n --gold:#8a6a1c; --gold-soft:#b8923a; --violet:#5b3a82; --line:#cbb894; --danger:#8e2f4a;\n --his:\"Italianno\",\"Yuji Syuku\",cursive; --reader:\"Caveat\",\"Zen Kurenaido\",cursive;\n}\n@media (prefers-color-scheme: dark){ :root:not([data-theme=\"light\"]){ --surface:#ddd0b2; --raised:#d0c1a0; } }\nbody{background:radial-gradient(ellipse at 50% 0%, #2a1d33 0%, #140e18 70%) fixed}\nmain{background:var(--surface);background-image:radial-gradient(ellipse at 50% 40%, rgba(255,250,235,.55), rgba(255,250,235,0) 65%),radial-gradient(circle at 20% 80%, rgba(120,90,50,.08), rgba(0,0,0,0) 40%),radial-gradient(circle at 85% 15%, rgba(120,90,50,.07), rgba(0,0,0,0) 35%);\n margin:16px auto 110px;padding:28px 26px 40px;box-shadow:0 0 0 1px rgba(90,60,30,.25), 0 18px 50px rgba(0,0,0,.55), inset 0 0 60px rgba(120,90,50,.18);border-radius:2px 6px 6px 2px;min-height:80vh;position:relative}\nmain::before{content:\"\";position:absolute;inset:0;pointer-events:none;border-left:6px solid rgba(90,60,30,.12);border-radius:2px 0 0 2px}\n.tabs{background:#e6d7b8 !important;border-top:1px solid #b8a37a !important}\n.card{background:rgba(255,250,238,.55)}\n.choice{background:rgba(255,250,238,.6)}\n/* drop cap on each page's first paragraph */\n.scene p:first-child::first-letter{float:left;font-family:var(--display);font-size:3.3em;line-height:.82;padding:.08em .08em 0 0;color:var(--gold)}\nhtml[lang=\"ja\"] .scene p:first-child::first-letter{float:none;font-size:inherit;color:inherit;padding:0}\n.folio{text-align:center;font-family:var(--display);color:var(--muted);font-size:.9rem;margin-top:1.6rem;letter-spacing:.2em}\n/* illustration plates */\n.vhs{background:#f7eedb;padding:7px 7px 0;border:1px solid #6b5436;box-shadow:0 0 0 3px #f7eedb, 0 0 0 4px #6b5436, 0 6px 16px rgba(60,40,20,.25);border-radius:1px;margin:.4rem 4px 1.3rem}\n.vhs canvas{border:1px solid #6b5436}\n.vhs figcaption{font-family:var(--display);font-style:italic;text-align:center;color:#5a4630;font-size:.92rem;padding:.35rem .4rem .45rem}\n/* margin notes */\n.note{display:block;margin:.2rem 0 1rem;max-width:88%;line-height:1.15}\n.note.gold{font-family:var(--his);color:var(--gold);font-size:1.75rem;transform:rotate(-1.6deg);margin-left:12%}\n.note.reader{font-family:var(--reader);color:var(--violet);font-size:1.3rem;transform:rotate(1.2deg);margin-left:2%}\n.note.reader .sig{display:block;font-size:1.05rem;opacity:.8;margin-left:40%}\nhtml[lang=\"ja\"] .note.gold{font-size:1.2rem} html[lang=\"ja\"] .choice.gift-choice{font-size:1.05rem} html[lang=\"ja\"] .note.reader{font-size:1.05rem}\nu.vu{text-decoration:underline wavy var(--violet);text-decoration-thickness:1.5px;text-underline-offset:4px}\n@media (min-width: 1000px){ main{max-width:900px;padding-right:270px} .note{float:right;clear:right;width:215px;margin:0 -250px .8rem 0} .note.gold{margin-left:0} .note.reader{margin-left:0} }\n/* his voice everywhere is his own hand, in tarnished gold */\n.pfx-voice{color:var(--gold) !important;font-family:var(--his) !important;font-style:normal !important;font-size:1.45em;line-height:1.1}\nhtml[lang=\"ja\"] .pfx-voice{font-size:1.05em}\n\n/* ---------- The cover ---------- */\n.cover{min-height:88vh;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.6rem;cursor:pointer;text-align:center;margin:-28px -26px -40px;padding:18px 12px 30px;background:#0b0712;border-radius:2px 6px 6px 2px}\n.cover-art{position:relative;width:min(100%,420px)}\n.cover-art canvas{display:block;width:100%;height:auto;aspect-ratio:2/3;border-radius:3px;box-shadow:0 0 0 1px #6b5436, 0 0 40px rgba(154,99,224,.25)}\n.cover .stars{display:none}\n.cover .stars i{position:absolute;background:#fff6dc;border-radius:50%;opacity:.2;animation:twinkle 2.8s ease-in-out infinite}\n@keyframes twinkle{0%,100%{opacity:.15;transform:scale(.8)}50%{opacity:1;transform:scale(1.4)}}\n.cover h1{color:#e9dcb8;font-size:2.4rem;text-shadow:0 2px 12px rgba(0,0,0,.8)}\n.cover-tap{color:var(--gold-soft);font-family:var(--display);font-style:italic;letter-spacing:.12em;animation:tapPulse 2.4s ease-in-out infinite;margin:0}\n@keyframes tapPulse{0%,100%{opacity:.45}50%{opacity:1}}\n@media (prefers-reduced-motion:reduce){ .cover .stars i,.cover-tap{animation:none;opacity:.7} }\n\n/* tarot plates */\n.tarot{margin:.4rem auto 1.2rem;max-width:300px}\n.tarot-card{position:relative;background:#1a1024;padding:58px 16px 64px;border-radius:10px;box-shadow:0 0 0 2px #b8923a, 0 0 0 5px #1a1024, 0 0 0 6px #b8923a, 0 10px 26px rgba(0,0,0,.45);background-image:radial-gradient(circle at 12px 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) 12px,#b8923a 1.5px,transparent 2px),radial-gradient(circle at 12px calc(100% - 12px),#b8923a 1.5px,transparent 2px),radial-gradient(circle at calc(100% - 12px) calc(100% - 12px),#b8923a 1.5px,transparent 2px)}\n.tarot-card canvas{display:block;width:100%;height:auto;aspect-ratio:4/3;border:1px solid #b8923a;border-radius:2px}\n.tarot .arc-num,.tarot .arc-name{position:absolute;left:0;right:0;text-align:center;font-family:var(--display);color:#e0c070;letter-spacing:.14em}\n.tarot .arc-num{top:14px;font-size:1.15rem}\n.tarot .arc-num::after,.tarot .arc-name::before{content:\"✧ ✦ ✧\";display:block;font-size:.6rem;letter-spacing:.5em;color:#b8923a;margin:.35rem 0 0}\n.tarot .arc-name::before{margin:0 0 .35rem}\n.tarot .arc-name{bottom:12px;font-size:1rem;text-transform:uppercase}\n.tarot.reversed .tarot-card{transform:rotate(180deg)}\n.tarot figcaption{text-align:center;font-family:var(--display);font-style:italic;color:var(--muted);font-size:.9rem;margin-top:.5rem}\nhtml[lang=\"ja\"] .tarot .arc-name{text-transform:none;letter-spacing:.06em}\n/* his offer: a card held out to you */\n.choice.gift-choice{border-radius:9px !important;border:1px solid var(--gold-soft) !important;box-shadow:0 0 0 3px rgba(255,250,238,.6), 0 0 0 4px var(--gold-soft), 0 6px 16px rgba(120,80,20,.25) !important;transform:rotate(-1.2deg);position:relative;padding-left:2.2rem !important}\n.choice.gift-choice::before{content:\"✦\";position:absolute;left:.7rem;top:.35rem;font-size:.9rem;color:var(--gold)}\n.choice.gift-choice::after{content:\"✦\";position:absolute;right:.7rem;bottom:.3rem;font-size:.9rem;color:var(--gold);transform:rotate(180deg)}\n/* epigraphs, recap, engine */\n.epi{margin:0 0 1.5rem;padding:.1rem 0 .1rem 1rem;border-left:2px solid var(--gold-soft)}\n.epi-q{font-family:var(--display);font-style:italic;font-size:1.12rem;line-height:1.4;margin:0}\n.epi-q::before{content:\"\\201C\"} .epi-q::after{content:\"\\201D\"}\n.epi-by{margin:.35rem 0 0;color:var(--muted);font-size:.88rem;letter-spacing:.03em}\n.card .epi{margin:1rem 0}\n.recap{border:1px dashed var(--line);border-radius:3px;padding:.6rem .9rem;margin:0 0 1.2rem;color:var(--muted);font-size:.95rem}\n.recap summary{cursor:pointer;font-family:var(--display);color:var(--violet)}\n.recap p{margin:.4rem 0 0}\ninput[type=password]{width:100%;font:inherit;color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:3px;padding:.7rem .8rem}\n/* scene header, choice icons and portraits */\n.story-head{display:flex;justify-content:space-between;align-items:center;gap:.8rem;margin-bottom:.4rem}\n.story-head .step-kicker{margin:0}\n.status-strip{display:flex;gap:2px;flex:none}\n.status-strip .st{width:28px;height:28px;display:block}\n.cicon{width:1.15em;height:1.15em;vertical-align:-.2em;margin-right:.55rem;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round;opacity:.75}\n.choice.gift-choice .cicon{opacity:1}\n.cast{display:flex;flex-wrap:wrap;gap:.9rem;margin:.2rem 0 1rem}\n.por{margin:0;text-align:center;width:64px}\n.por canvas{display:block;width:56px;height:56px;margin:0 auto;border-radius:50%;box-shadow:0 0 0 1.5px #6b5436}\n.por.sovereign canvas{box-shadow:0 0 0 1.5px var(--gold-soft),0 0 10px rgba(201,162,74,.45)}\n.por.servant canvas{box-shadow:0 0 0 1.5px var(--violet)}\n.por figcaption{font-family:var(--display);font-size:.82rem;color:var(--muted);margin-top:.25rem;line-height:1.2;overflow-wrap:anywhere}\n/* picture first: the plate sketches in as faint ink, then the colour washes in; a gold drop cap blooms, then the words */\n@keyframes sketchIn{0%{opacity:0;filter:grayscale(1) contrast(2.4) brightness(1.55) sepia(.35)}22%{opacity:.75;filter:grayscale(1) contrast(2.4) brightness(1.55) sepia(.35)}100%{opacity:1;filter:none}}\n.vhs.sketch-in canvas{animation:sketchIn 1.5s ease-out both}\n@keyframes dcBloom{from{opacity:0;filter:blur(3px)}to{opacity:1;filter:none}}\n.scene .dc-bloom{animation:dcBloom .5s ease-out both;min-height:3em}\n@keyframes writeIn{from{opacity:0;transform:translateY(3px)}to{opacity:1;transform:none}}\n.scene.write-in > *{animation:writeIn .6s ease-out both;animation-delay:1.5s}\n.scene.write-in > *:nth-child(2){animation-delay:1.9s} .scene.write-in > *:nth-child(3){animation-delay:2.3s} .scene.write-in > *:nth-child(n+4){animation-delay:2.7s}\n/* his signs */\n.sv-shadow-slide{position:fixed;left:0;bottom:12vh;width:60px;height:170px;pointer-events:none;z-index:70;color:#120a18;opacity:0;animation:svSlide 2.4s ease-in-out forwards}\n.sv-shadow-slide svg,.sv-shadow-still svg{width:100%;height:100%;fill:currentColor}\n@keyframes svSlide{0%{transform:translateX(-80px) skewX(-8deg);opacity:0}15%{opacity:.32}85%{opacity:.32}100%{transform:translateX(calc(100vw + 40px)) skewX(-8deg);opacity:0}}\n.sv-shadow-still{width:22px;height:62px;color:#120a18;opacity:.16;margin:-2.2rem 0 0 auto}\n.sv-rw{white-space:nowrap}\n.sv-rw s{text-decoration:none;background:linear-gradient(var(--violet),var(--violet)) no-repeat 0 55%/100% 2px}\n.sv-rw .sv-new{font-family:var(--his);color:var(--violet);font-size:1.45em;line-height:.6;margin-left:.15em;display:inline-block;transform:rotate(-3deg)}\n.sv-rw.anim s{animation:svStrike .7s ease-in 2.2s both}\n.sv-rw.anim .sv-new{animation:svInk .9s ease-out 2.9s both}\n@keyframes svStrike{from{background-size:0 2px}to{background-size:100% 2px}}\n@keyframes svInk{from{opacity:0;filter:blur(2px)}to{opacity:1;filter:none}}\n.note.sv-reply{margin-left:22%;transform:rotate(-.8deg);opacity:.9}\n.choice.sv-took{color:var(--violet);border-color:var(--violet) !important;box-shadow:inset 3px 0 0 var(--violet);transition:color .35s,border-color .35s}\n.sv-half{font-style:italic;color:var(--violet);opacity:.45;margin:.9rem 0 0;border-top:1px dashed var(--line);padding-top:.7rem}\n.sv-half.anim{animation:svHalf 1.6s ease-out 3s both}\n@keyframes svHalf{from{opacity:0}to{opacity:.45}}\n.por.sv-behind{position:relative}\n.por.sv-behind canvas{position:relative;z-index:1}\n.por.sv-behind::before{content:\"\";position:absolute;z-index:0;left:26px;top:-4px;width:40px;height:82px;opacity:.7;background:no-repeat center/contain url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 60 170'%3E%3Cpath fill='%23120a18' d='M17 4h26v26h9v5H8v-5h9z'/%3E%3Cellipse fill='%23120a18' cx='30' cy='48' rx='13' ry='14'/%3E%3Cpath fill='%23120a18' d='M14 64q16-6 32 0l9 100q-25 8-50 0z'/%3E%3C/svg%3E\")}\nmain::after{content:\"\";position:absolute;inset:0;pointer-events:none;border-radius:inherit;transition:box-shadow 2s ease;box-shadow:inset 0 0 calc(var(--creep,0) * 90px) calc(var(--creep,0) * 14px) rgba(52,18,82,calc(var(--creep,0) * .6))}\n@media (prefers-reduced-motion:reduce){ .vhs.sketch-in canvas,.scene .dc-bloom,.scene.write-in > *,.sv-rw.anim s,.sv-rw.anim .sv-new,.sv-half.anim{animation:none} }\n" + "</style>");
4
4
  var wait = document.getElementById("svLoading"); if (wait) wait.remove();
5
5
  document.body.insertAdjacentHTML("afterbegin", "<main id=\"app\" aria-live=\"polite\"></main>\n<div id=\"watchEye\" aria-hidden=\"true\"></div>\n<div id=\"pfxEye\" aria-hidden=\"true\"></div>\n<nav class=\"tabs\" id=\"tabs\" hidden><div class=\"inner\">\n <button data-tab=\"story\">Story</button>\n <button data-tab=\"sheet\">Sheet</button>\n <button data-tab=\"journal\">Journal</button>\n <button data-tab=\"world\">World</button>\n</div></nav>");
6
6
  })();
7
- window.SOVEREIGN_VERSION = "1.0.3";
7
+ window.SOVEREIGN_VERSION = "1.0.4";
8
8
 
9
9
  /* ---------- Sigil (original mark) ---------- */
10
10
  const SIGIL = `<svg class="sigil" viewBox="0 0 100 100" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true">
@@ -444,6 +444,63 @@ async function pfxEcho(chosenIdx){
444
444
  const bs=[...app.querySelectorAll(".choices [data-choice]")].filter(b=>+b.dataset.choice!==chosenIdx); if(!bs.length) return;
445
445
  const b=pick(bs); pfxBusy = true; b.classList.add("pfx-glow"); pfxEye(900); await pfxWait(650); b.classList.remove("pfx-glow"); pfxBusy = false;
446
446
  }
447
+ /* ---------- The Sovereign shows himself: "one is deniable, two is weird" ----------
448
+ Chapter I: a top-hatted shadow slides across a page turn (deniable), then a few pages later a violet stroke
449
+ rewrites a word (unmistakable). Later acts escalate: a second hand answers his margin notes, choices turn violet
450
+ when tapped, ink creeps in from the page edges with his Manifestation, his shadow stands behind a portrait,
451
+ and the next page is half written before the player chooses. Each sign is decided once per page and saved,
452
+ so re-rendering never replays it. With page effects off or reduced motion, signs appear still: nothing moves. */
453
+ const svMotion = () => pfxOn && !pfxReduced();
454
+ const svPlayed = new Set(); // signs already animated this session (per page), so switching tabs never replays them
455
+ const SV_SHADOW = `<svg viewBox="0 0 60 170" aria-hidden="true"><path d="M17 4h26v26h9v5H8v-5h9z"/><ellipse cx="30" cy="48" rx="13" ry="14"/><path d="M14 64q16-6 32 0l9 100q-25 8-50 0z"/></svg>`;
456
+ const SV_REPLIES = {
457
+ en:["don't answer him","he wrote that to me too","who are you talking to?","stop reading. please.","he's lying. he's not.","I can see you reading this","close the book"],
458
+ ja:["返事をしないで","私にも同じことを書いた","誰と話してるの?","読むのをやめて。お願い。","嘘よ。嘘じゃない。","読んでいるのが見える","本を閉じて"]
459
+ };
460
+ function svRewriteFor(text){ // the first word he can claim, preferring a middle paragraph: your→our, You→We, I→we
461
+ const ps = String(text||"").trim().split(/\n\s*\n|\n/).filter(Boolean);
462
+ const order = ps.map((_,i)=>i).sort((a,b)=>(a===0)-(b===0));
463
+ for (const re of [[/\b(your)\b/,"our"],[/\b(Your)\b/,"Our"],[/(?:^|>|[.!?]\s+)(You)\b/,"We"],[/\b(I)\b/,"we"]])
464
+ for (const i of order){ if (re[0].test(ps[i])) return {p:i, re:re[0].source, to:re[1]}; }
465
+ return null;
466
+ }
467
+ function svDecide(cur){
468
+ if (cur.sg) return cur.sg;
469
+ if (G.ended || G.intermission || cur.priceScene || (cur.reflection && !cur.reflectionDone) || (G.state.deathsDoor && !cur.priceScene)) return {};
470
+ const sg = cur.sg = {}, S = G.signs = G.signs || {};
471
+ const n = G.sceneCount, cs = n - (G.chapterStart||0), act = actOf(), h = G.state.hold||0, m = G.state.manifest ?? 2;
472
+ const later = act >= 2 || h >= 5, deep = act >= 3 || h >= 7;
473
+ if (S.slide == null ? cs >= 3 : (later && n - S.slide >= 6 && Math.random() < 0.12)){ sg.slide = true; S.slide = n; }
474
+ else if (S.slide != null && (S.rewrite == null ? n - S.slide >= 3 : (later && n - S.rewrite >= 5 && Math.random() < 0.15))){
475
+ const rw = svRewriteFor(cur.text); if (rw){ sg.rewrite = rw; S.rewrite = n; } }
476
+ const last = G.history[G.history.length-1];
477
+ if (later && last && last.whisper && Math.random() < 0.5) sg.reply = Math.floor(Math.random()*SV_REPLIES.en.length);
478
+ if (later) sg.violet = true;
479
+ if (deep && (cur.speakers||[]).some(x=>x.kind!=="sovereign")) sg.behind = cur.speakers.findIndex(x=>x.kind!=="sovereign");
480
+ if (deep && cur.choices && cur.choices.length > 1 && n - (S.half||0) >= 4 && Math.random() < 0.5){
481
+ const c = cur.choices.find(x=>x.gift || x.price) || pick(cur.choices);
482
+ sg.half = c.text; S.half = n; }
483
+ return sg;
484
+ }
485
+ function svRewriteHtml(paraHtml, rw, anim){
486
+ const re = new RegExp(rw.re);
487
+ return paraHtml.replace(re, (all, word) => all.replace(word, `<span class="sv-rw${anim?" anim":""}"><s>${word}</s><span class="sv-new">${esc(rw.to)}</span></span>`));
488
+ }
489
+ function svHalfText(t){
490
+ t = String(t||"").trim(); if (!t) return "";
491
+ const line = lang === "ja" ? t : "You " + t.charAt(0).toLowerCase() + t.slice(1).replace(/[.!?]$/,"");
492
+ const sp = line.lastIndexOf(" "), word = line.length - sp - 1;
493
+ return word >= 3 ? line.slice(0, sp + 1 + Math.ceil(word / 2)) : line.slice(0, -1); // stops mid-word, as if the pen was lifted
494
+ }
495
+ function svAfterRender(cur){ // play this page's moving signs once
496
+ const sg = cur.sg || {}, key = n => G.id + ":" + G.sceneCount + ":" + n, motion = svMotion();
497
+ if (sg.slide && motion && !svPlayed.has(key("slide"))){
498
+ svPlayed.add(key("slide"));
499
+ const d = document.createElement("div"); d.className = "sv-shadow-slide"; d.innerHTML = SV_SHADOW; document.body.appendChild(d);
500
+ setTimeout(()=>d.remove(), 2600);
501
+ }
502
+ if (sg.rewrite && motion && !svPlayed.has(key("rw"))){ svPlayed.add(key("rw")); setTimeout(()=>pfxEye(1600), 2600); }
503
+ }
447
504
  function thinking(msg){ return `<div class="thinking">${SIGIL.replace('class="sigil"','')}<span>${esc(msg)}</span></div>`; }
448
505
  function progressBar(done, total){ let s='<div class="progress" aria-hidden="true">'; for(let i=0;i<total;i++) s+=`<span class="${i<done?'done':''}"></span>`; return s+'</div>'; }
449
506
 
@@ -1083,7 +1140,7 @@ function scenePromptRaw(action){
1083
1140
  const refl = (reachOn ? G.reflections : []).filter(r=>r.a).slice(-4).map(r=> r.intruded ? `(An entry the player did NOT write; the Sovereign put it in their journal: ${r.a})` : `Q: ${r.q} A: ${r.a}`).join("\n");
1084
1141
  const edits = G.edits.map(e=>"- "+e.canon).join("\n");
1085
1142
  const rollLine = action.roll ? `HIDDEN ROLL (the player never sees numbers): they attempted this with ${action.roll.trait} at ${action.roll.risk} risk. Outcome: ${action.roll.outcome.toUpperCase()}. Narrate that outcome faithfully.` : `No roll was needed for this action.`;
1086
- return `${SETTING}\n\n${themesFor(tset(), G && G.personal)}\n\nYou are the narrator of a solo roleplay.\n${pacing}\n${door}\n\nCURRENT STATE: ${stateBrief()}\n\nSTORY SO FAR:\n${(G.chronicle.length > 20 ? [...G.chronicle.slice(0,4), "(…)", ...G.chronicle.slice(-16)] : G.chronicle).join(" ") || "(nothing yet: this is the opening scene. Start in the player's ordinary day, and let the Sovereign's first real intrusion arrive by the end.)"}\n\n${recent ? "MOST RECENT SCENES:\n"+recent+"\n\n" : ""}WHAT THE CHARACTER KNOWS ABOUT THE HIDDEN WORLD: ${(G.known||[]).join("; ") || "nothing yet"}\nCONTINUITY: The character only knows what is listed above or has plainly happened on-page. Choices must never assume knowledge they lack (for example, no choice to enter the mirror world until they have discovered it exists). Never refer back to a past detail (\"this time\", \"again\", \"as before\") unless that detail actually appears in the story so far or the recent scenes.\n\n${edits ? "WORLD FACTS THE PLAYER HAS SET (always honor these):\n"+edits+"\n\n" : ""}${refl ? "THE PLAYER'S OWN REFLECTIONS (let these subtly inform the story; never quote them back):\n"+refl+"\n\n" : ""}PLAYER'S ACTION: ${action.text}\n${rollLine}\n\nWRITE: second person, present tense, 80 to 140 words in 2 to 4 short paragraphs. Short, clear sentences and common words, so readers whose first language is not English can follow easily; keep the voice, lose the padding. Let the picture carry the setting. End on a moment that asks for a decision. Plain paragraphs, no headings, no markdown. Avoid the obvious next beat; at least once a chapter, subvert a genre expectation.\nThen write a line containing only @@@ and then ONLY this JSON:\n{"speakers":[{"name":"anyone who speaks or matters in this scene, max 3","kind":"sovereign|friend|stranger|servant"}],\n"choices":[{"icon":"talk|look|go|run|hide|help|fight|make|wait|take|refuse|call","text":"under 10 words","trait":"Nerve|Wits|Grit|Charm","risk":"none|low|medium|high"${action.kind==="face"?',"price":"short phrase"':(offerAllowed()?',"price":"only on the one choice that accepts an offer; omit otherwise"':'')}${giftOfferNow()?',"gift":"${giftOfferKind()} (only on the one choice that uses his power)"':''}${G.nextProbe?(G.nextProbe.regret?',"regret":"no, yes or unsure (see THE QUIET MOMENT)"':',"probe":"2, 1 or 0 (see THE QUIET MOMENT)"'):''}}],\n"changes":{"wounds":0,"stability":0,"hold":0,"resolve":0,"manifest":0,"fade":0,"tensions":{${tset().map(t=>`"${t.key}":0`).join(",")}},"daysPassed":0,"addItems":[],"removeItems":[],"addBonds":[{"name":"","note":""}],"removeBonds":[],"addGifts":[{"name":"","effect":"","cost":""}],"addScars":[]},\n"threads":["up to 6 short open threads, replacing the old list"],\n"npcs":[{"name":"","note":"up to 8 people who matter, replacing the old list"}],\n"aims":["the player's current aims, 2 to 4, replacing the old list; keep them unless the story changes them"],
1143
+ return `${SETTING}\n\n${themesFor(tset(), G && G.personal)}\n\nYou are the narrator of a solo roleplay.\n${pacing}\n${door}\n\nCURRENT STATE: ${stateBrief()}\n\nSTORY SO FAR:\n${(G.chronicle.length > 20 ? [...G.chronicle.slice(0,4), "(…)", ...G.chronicle.slice(-16)] : G.chronicle).join(" ") || "(nothing yet: this is the opening scene. Start in the player's ordinary day, and let the Sovereign's first real intrusion arrive by the end.)"}\n\n${recent ? "MOST RECENT SCENES:\n"+recent+"\n\n" : ""}WHAT THE CHARACTER KNOWS ABOUT THE HIDDEN WORLD: ${(G.known||[]).join("; ") || "nothing yet"}\nCONTINUITY: The character only knows what is listed above or has plainly happened on-page. Choices must never assume knowledge they lack (for example, no choice to enter the mirror world until they have discovered it exists). Never refer back to a past detail (\"this time\", \"again\", \"as before\") unless that detail actually appears in the story so far or the recent scenes.\n\n${edits ? "WORLD FACTS THE PLAYER HAS SET (always honor these):\n"+edits+"\n\n" : ""}${refl ? "THE PLAYER'S OWN REFLECTIONS (let these subtly inform the story; never quote them back):\n"+refl+"\n\n" : ""}PLAYER'S ACTION: ${action.text}\n${rollLine}\n\nFIRST LINE: begin your reply with one line in exactly this form, naming this scene's illustration: @pic SETTING TIME WEATHER, where SETTING is one of street, alley, room, station, train, cafe, office, rooftop, park, shop, stage, mirror; TIME is dawn, day, dusk or night; WEATHER is clear, rain, fog or snow. For example: @pic cafe night rain. Then a new line, then the story.\nWRITE: second person, present tense, 80 to 140 words in 2 to 4 short paragraphs. Short, clear sentences and common words, so readers whose first language is not English can follow easily; keep the voice, lose the padding. Let the picture carry the setting. End on a moment that asks for a decision. Plain paragraphs, no headings, no markdown. Avoid the obvious next beat; at least once a chapter, subvert a genre expectation.\nThen write a line containing only @@@ and then ONLY this JSON:\n{"speakers":[{"name":"anyone who speaks or matters in this scene, max 3","kind":"sovereign|friend|stranger|servant"}],\n"choices":[{"icon":"talk|look|go|run|hide|help|fight|make|wait|take|refuse|call","text":"under 10 words","trait":"Nerve|Wits|Grit|Charm","risk":"none|low|medium|high"${action.kind==="face"?',"price":"short phrase"':(offerAllowed()?',"price":"only on the one choice that accepts an offer; omit otherwise"':'')}${giftOfferNow()?',"gift":"${giftOfferKind()} (only on the one choice that uses his power)"':''}${G.nextProbe?(G.nextProbe.regret?',"regret":"no, yes or unsure (see THE QUIET MOMENT)"':',"probe":"2, 1 or 0 (see THE QUIET MOMENT)"'):''}}],\n"changes":{"wounds":0,"stability":0,"hold":0,"resolve":0,"manifest":0,"fade":0,"tensions":{${tset().map(t=>`"${t.key}":0`).join(",")}},"daysPassed":0,"addItems":[],"removeItems":[],"addBonds":[{"name":"","note":""}],"removeBonds":[],"addGifts":[{"name":"","effect":"","cost":""}],"addScars":[]},\n"threads":["up to 6 short open threads, replacing the old list"],\n"npcs":[{"name":"","note":"up to 8 people who matter, replacing the old list"}],\n"aims":["the player's current aims, 2 to 4, replacing the old list; keep them unless the story changes them"],
1087
1144
  "picture":{"setting":"street|alley|room|station|train|cafe|office|rooftop|park|shop|stage|mirror (closest match to where this scene happens)","time":"dawn|day|dusk|night","weather":"clear|rain|fog|snow","figures":"0 to 4 other people visible","caption":"a short, evocative title for this illustration, like a plate in an old storybook"},
1088
1145
  "whisper":"optional: one line the Sovereign writes in the margin in his own hand from their shadow, in character with his current mask, or null",
1089
1146
  "chronicle":"one sentence summarizing what happened this scene",\n"learned":["short facts about the hidden world the character newly and clearly learned this scene, e.g. the mirror world exists; usually empty"],\n"reflection":null,\n"fourthWall":false,\n"offer":false,\n"knew":false,\n"echo":false,\n"haunt":false,\n"readerNote":null,\n"readerUnderline":null,\n"giftUsed":false,\n"giftActed":false,\n"observed":null,\n"intrusion":null,\n"goal":null,\n"work":null,\n"lasting":null,\n"ending":false}\nRULES FOR THE JSON: exactly 4 choices unless ending. Make choices distinct and concrete; where natural, let them pull toward different sides of the tensions. Risk "none" for safe actions. wounds change 0 to +2 for real harm (negative only for healing); stability change 0 to -2 for horror or loss (positive only for real rest or connection). Tension changes -2 to +2 only for tensions this scene's action truly touched, mostly 0. Gifts are rare, always cost something, and are strongest in the mirror world. "daysPassed": 0 within a day, 1 or more when time moves on. ${G.history.length && G.history[G.history.length-1].whisper ? "\"whisper\" must be null this scene (he spoke last scene)." : "Include a \"whisper\" in roughly one scene in three or four, never two in a row, and never lecturing or giving advice."} Empty arrays when nothing changes. ${reflRule()}\n${fwRule()}\n${offerRule(action)}\n${hauntRules(action)}`;
@@ -1102,13 +1159,10 @@ async function takeTurn(action, isRetry){
1102
1159
  ctl = new AbortController();
1103
1160
  const res = await sample(scenePrompt(action), {
1104
1161
  modelTier: tier, cache:false, signal: ctl.signal,
1105
- onText: ({text})=>{
1106
- const narr = text.split("@@@")[0];
1107
- const live = document.getElementById("liveScene");
1108
- if (live) { const th = document.getElementById("liveThinking"); if (th) th.remove(); live.innerHTML = paras(narr); }
1109
- }
1162
+ onText: ({text})=>{ try { liveReveal(text); } catch(e){} }
1110
1163
  });
1111
- const parts = res.text.split("@@@");
1164
+ const sp = splitPic(res.text);
1165
+ const parts = sp.rest.split("@@@");
1112
1166
  const narrative = parts[0].trim();
1113
1167
  let data = null;
1114
1168
  if (parts[1]) data = parseLoose(parts[1]);
@@ -1117,6 +1171,7 @@ async function takeTurn(action, isRetry){
1117
1171
  data = { choices: [] }; // keep the scene; stats just don't change this turn
1118
1172
  }
1119
1173
  if (!Array.isArray(data.choices) || !data.choices.length) data.choices = [{text:"Continue", trait:"Wits", risk:"none"}];
1174
+ if (sp.pic) data.picture = {...((data.picture && typeof data.picture === "object") ? data.picture : {}), ...sp.pic}; // keep the picture that already sketched in
1120
1175
  applyScene(action, narrative, data);
1121
1176
  delete G.pending;
1122
1177
  await Store.put(G);
@@ -1295,10 +1350,11 @@ function endChapter(type, lasting){
1295
1350
  function raceHints(){
1296
1351
  const m = G && G.state ? (G.state.manifest ?? 2) : 0;
1297
1352
  document.documentElement.style.setProperty("--taint", String((m*0.013).toFixed(3)));
1353
+ document.documentElement.style.setProperty("--creep", String(Math.max(0, Math.min(1, (m - 3) / 7)).toFixed(2))); // ink creeps in from the page edges
1298
1354
  const w = document.getElementById("watchEye");
1299
1355
  if (w){ if (!w.innerHTML) w.innerHTML = SIGIL; w.style.opacity = G ? String(0.05 + m*0.035) : "0"; }
1300
1356
  }
1301
- function clearHints(){ document.documentElement.style.setProperty("--taint","0"); const w=document.getElementById("watchEye"); if (w) w.style.opacity="0"; }
1357
+ function clearHints(){ document.documentElement.style.setProperty("--taint","0"); document.documentElement.style.setProperty("--creep","0"); const w=document.getElementById("watchEye"); if (w) w.style.opacity="0"; }
1302
1358
 
1303
1359
  const ENDINGS = {
1304
1360
  free: "You resisted his call and sealed him away, perhaps with a touch of his own power. But an idea is never truly gone. You'll always be glancing over your shoulder: at a flash of violet or tarnished gold, a shadow that moves a beat too late, a laugh that sounds almost like his.",
@@ -1645,6 +1701,51 @@ function cleanPicture(p, seed){
1645
1701
  weather: ["clear","rain","fog","snow"].includes(p.weather) ? p.weather : "clear",
1646
1702
  figures: Math.max(0, Math.min(4, parseInt(p.figures)||0)), seed, caption: typeof p.caption === "string" ? p.caption.trim().slice(0,70) : "" };
1647
1703
  }
1704
+ function splitPic(text){ // reads the "@pic setting time weather" first line; while it is still arriving, nothing is shown
1705
+ const t = String(text||""), lead = t.replace(/^\s+/, "");
1706
+ const nl = lead.indexOf("\n"), first = nl < 0 ? lead : lead.slice(0, nl);
1707
+ if (nl < 0 && /^@(p(i(c)?)?)?$/i.test(first)) return {pic:null, rest:"", waiting:true}; // the picture line is still arriving
1708
+ if (!/^@pic\b/i.test(first)) return {pic:null, rest:t};
1709
+ if (nl < 0) return {pic:null, rest:"", waiting:true};
1710
+ const pic = {};
1711
+ lead.slice(0, nl).replace(/^@pic\b/i, "").toLowerCase().split(/[\s|,]+/).forEach(w=>{
1712
+ if (PIC_SETTINGS.includes(w)) pic.setting = w;
1713
+ else if (["dawn","day","dusk","night"].includes(w)) pic.time = w;
1714
+ else if (["clear","rain","fog","snow"].includes(w)) pic.weather = w; });
1715
+ return {pic: pic.setting ? pic : null, rest: lead.slice(nl + 1)};
1716
+ }
1717
+ let revealedScene = null, shownTextScene = null; // scenes whose picture already sketched in / whose text already appeared live
1718
+ const REVEAL_MS = 1500, DROPCAP_MS = 550;
1719
+ function liveReveal(text){ // painting first, then the gold drop cap, then the words as they arrive
1720
+ const L = liveReveal.s || (liveReveal.s = {}), sp = splitPic(text);
1721
+ if (L.scene !== G.sceneCount + 1){ Object.assign(L, {scene:G.sceneCount + 1, pic:false, t0:0, cap:0, timer:null}); }
1722
+ const live = document.getElementById("liveScene"), box = document.getElementById("livePic");
1723
+ if (!live) return;
1724
+ if (sp.waiting) return;
1725
+ if (sp.pic && !L.pic && box){
1726
+ L.pic = true; L.t0 = Date.now(); revealedScene = G.sceneCount + 1;
1727
+ const th = document.getElementById("liveThinking"); if (th) th.remove();
1728
+ box.innerHTML = picHtml("livePicC", G.sceneCount + 1, "");
1729
+ box.firstElementChild.classList.add("sketch-in");
1730
+ paintPicture("livePicC", cleanPicture(sp.pic, (G.sceneCount + 1)*97 + (G.chapter||1)*13));
1731
+ }
1732
+ const narr = sp.rest.split("@@@")[0];
1733
+ L.text = narr;
1734
+ const wait = L.pic && !pfxReduced() ? L.t0 + REVEAL_MS - Date.now() : 0;
1735
+ const show = () => {
1736
+ const th = document.getElementById("liveThinking"); if (th && L.text.trim()) th.remove();
1737
+ const el = document.getElementById("liveScene"); if (!el || !L.text.trim()) return;
1738
+ if (!L.cap){ // the drop cap blooms alone before the words follow
1739
+ L.cap = Date.now(); el.innerHTML = `<p class="dc-bloom">${esc(L.text.trim().charAt(0))}</p>`;
1740
+ if (!pfxReduced()) { clearTimeout(L.timer); L.timer = setTimeout(show, DROPCAP_MS); return; }
1741
+ }
1742
+ const left = L.cap + DROPCAP_MS - Date.now();
1743
+ if (left > 0 && !pfxReduced()) { clearTimeout(L.timer); L.timer = setTimeout(show, left); return; }
1744
+ el.innerHTML = paras(L.text); shownTextScene = L.scene;
1745
+ };
1746
+ clearTimeout(L.timer);
1747
+ if (wait > 0) L.timer = setTimeout(show, wait); else show();
1748
+ }
1648
1749
  function tapeClock(n){ const t = 14*3600 + (n||0)*97; const h = Math.floor(t/3600)%24, m = Math.floor(t/60)%60, sec = t%60; return `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(sec).padStart(2,"0")}`; }
1649
1750
  function picHtml(id, n, caption){
1650
1751
  return `<figure class="vhs" aria-hidden="true"><canvas id="${id}" width="320" height="240"></canvas>${caption ? `<figcaption>${esc(caption)}</figcaption>` : ""}</figure>`;
@@ -1747,9 +1848,11 @@ function renderStoryShell(action, inner){
1747
1848
  setTimeout(pfxLoading, 0);
1748
1849
  tabs.hidden = false; setTabs();
1749
1850
  const last = G.current;
1750
- app.innerHTML = `<p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?", one-shot":""}</p>
1751
- ${action && action.kind!=="start" ? `<p class="action">${esc(action.label||action.text)}</p>` : ""}
1752
- <div id="storyBody"><div class="scene" id="liveScene"></div><div id="liveThinking">${inner}</div><div class="row"><button class="btn-link" id="stopBtn">Stop</button></div></div>`;
1851
+ const opening = action && (action.kind==="start" || action.kind==="chapter") && G.mode !== "oneshot"; // same header and chapter quote as the finished page, so nothing jumps
1852
+ app.innerHTML = `<div class="story-head"><p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?", one-shot":`, chapter ${ROMAN[G.chapter||1]||G.chapter}`}</p>${G.state ? statusStrip() : ""}</div>
1853
+ ${opening ? epigraphHtml(quoteFor("open", G, G.chapter||1)) : ""}
1854
+ ${action && action.kind!=="start" && action.kind!=="chapter" ? `<p class="action">${esc(action.label||action.text)}</p>` : ""}
1855
+ <div id="storyBody"><div id="livePic"></div><div class="scene" id="liveScene"></div><div id="liveThinking">${inner}</div><div class="row"><button class="btn-link" id="stopBtn">Stop</button></div></div>`;
1753
1856
  const sb = document.getElementById("stopBtn"); if (sb) sb.onclick=()=>ctl?.abort();
1754
1857
  }
1755
1858
  function setTabs(){ tabs.querySelectorAll("[data-tab]").forEach(b=>{ if(b.dataset.tab===view.tab) b.setAttribute("aria-current","page"); else b.removeAttribute("aria-current"); }); }
@@ -1773,7 +1876,10 @@ function renderStory(){
1773
1876
  if (G.mode !== "oneshot" && G.sceneCount - (G.chapterStart||0) === 1 && !G.ended && !G.intermission) html += epigraphHtml(quoteFor("open", G, G.chapter||1));
1774
1877
  if (last && last.action!=="Began the story." && last.action!=="A new chapter.") html += `<p class="action">${esc(last.action)}</p>`;
1775
1878
  if (last && last.omen) html += `<p class="omen">${esc(last.omen)}</p>`;
1776
- if (cur.picture) html += picHtml("scenePic", G.sceneCount, cur.picture.caption ? `Plate ${G.sceneCount}. ${cur.picture.caption}` : "");
1879
+ const fresh = revealedScene !== G.sceneCount && !G.ended && !G.intermission, freshText = fresh && shownTextScene !== G.sceneCount;
1880
+ revealedScene = shownTextScene = G.sceneCount;
1881
+ if (cur.picture) html += picHtml("scenePic", G.sceneCount, cur.picture.caption ? `Plate ${G.sceneCount}. ${cur.picture.caption}` : "").replace('<figure class="vhs"', `<figure class="vhs${fresh?" sketch-in":""}"`);
1882
+ const sg = svDecide(cur), svAnim = n => svMotion() && !svPlayed.has(G.id + ":" + G.sceneCount + ":" + n);
1777
1883
  let sceneHtml = paras(cur.text);
1778
1884
  if (cur.underline){ const u = esc(cur.underline); const at = sceneHtml.indexOf(u); if (u.length > 3 && at >= 0) sceneHtml = sceneHtml.slice(0,at) + `<u class="vu">${u}</u>` + sceneHtml.slice(at+u.length); }
1779
1885
  let noteGold = (last && last.whisper) ? `<aside class="note gold">${esc(last.whisper)}</aside>` : "";
@@ -1784,12 +1890,15 @@ function renderStory(){
1784
1890
  noteGold = `<aside class="note gold whisperling" aria-hidden="true">${words.join(" ")}</aside>`; } }
1785
1891
  const noteReader = cur.readerNote ? `<aside class="note reader">${esc(cur.readerNote)}${cur.readerBy ? `<span class="sig">— ${esc(cur.readerBy)}</span>` : ""}</aside>` : "";
1786
1892
  const ps = sceneHtml.split("</p>").filter(x=>x.trim()).map(x=>x+"</p>");
1893
+ if (sg.rewrite && ps[sg.rewrite.p]) ps[sg.rewrite.p] = svRewriteHtml(ps[sg.rewrite.p], sg.rewrite, svAnim("rw"));
1894
+ if (noteGold && sg.reply != null) noteGold += `<aside class="note reader sv-reply" aria-hidden="true">${esc((SV_REPLIES[lang] || SV_REPLIES.en)[sg.reply])}</aside>`;
1787
1895
  if (noteGold) ps.splice(Math.min(1, ps.length), 0, noteGold);
1788
1896
  if (noteReader) ps.splice(Math.min(ps.length, noteGold ? 3 : 2), 0, noteReader);
1789
1897
  sceneHtml = ps.join("");
1790
- if (cur.speakers && cur.speakers.length) html += `<div class="cast">${cur.speakers.map((sp,i)=>`<figure class="por ${sp.kind}"><canvas id="por${i}"></canvas><figcaption>${esc(sp.kind==="sovereign"?"???":sp.name)}</figcaption></figure>`).join("")}</div>`;
1791
- html += `<div class="scene">${sceneHtml}</div>`;
1898
+ if (cur.speakers && cur.speakers.length) html += `<div class="cast">${cur.speakers.map((sp,i)=>`<figure class="por ${sp.kind}${sg.behind === i ? " sv-behind" : ""}"><canvas id="por${i}"></canvas><figcaption>${esc(sp.kind==="sovereign"?"???":sp.name)}</figcaption></figure>`).join("")}</div>`;
1899
+ html += `<div class="scene${freshText ? " write-in" : ""}">${sceneHtml}</div>`;
1792
1900
  html += `<p class="folio">— ${G.sceneCount} —</p>`;
1901
+ if (sg.slide && !svMotion()) html += `<div class="sv-shadow-still" aria-hidden="true">${SV_SHADOW}</div>`; // gentle version: printed on the page, not moving
1793
1902
 
1794
1903
  if (cur.reflection && !cur.reflectionDone){
1795
1904
  html += `<div class="card gold"><h3>A quiet moment</h3><p>${esc(cur.reflection)}</p>
@@ -1845,7 +1954,10 @@ function renderStory(){
1845
1954
  const isPrice = cur.priceScene && G.state.deathsDoor;
1846
1955
  const shown = isPrice ? opts : opts.map(o=>o.price ? {...o, hint:"Price: "+o.price} : o);
1847
1956
  html += choicePanel(shown, {hints:true, customLabel: isPrice?"Name your own price":"Write your own", placeholder: isPrice?"What will you give up?":"What do you do? (Start with /edit to change the world instead)"});
1957
+ if (sg.half) html += `<p class="sv-half${svAnim("half") ? " anim" : ""}" aria-hidden="true">${esc(svHalfText(sg.half))}</p>`;
1848
1958
  app.innerHTML = html;
1959
+ if (sg.half) svPlayed.add(G.id + ":" + G.sceneCount + ":half");
1960
+ svAfterRender(cur);
1849
1961
  paintPicture("scenePic", cur.picture);
1850
1962
  (cur.speakers||[]).forEach((sp,i)=>{ const cv = document.getElementById("por"+i); if (cv) try { portraitFor(cv, sp.name, sp.kind); } catch(e){} });
1851
1963
  raceHints();
@@ -1871,6 +1983,7 @@ function renderStory(){
1871
1983
  return takeTurn({text:o.text, kind:"custom", gift: o.giftKind || false, regretOf: (pr && pr.regret) ? pr : null, regret: (pr && pr.regret) ? "instead" : null,
1872
1984
  probeOf: (pr && !pr.regret) ? pr : null, probe: (pr && !pr.regret && o.probeRead != null) ? o.probeRead : null, quietOwn: pr ? o.text : null, roll: secretRoll(trait, risk)});
1873
1985
  }
1986
+ if (sg.violet && how === "choice"){ const b = app.querySelector(`[data-choice="${shown.indexOf(o)}"]`); if (b){ b.classList.add("sv-took"); if (svMotion()) await pfxWait(480); } }
1874
1987
  if (how === "choice") await pfxEcho(shown.indexOf(o));
1875
1988
  const pp = G.pendingProbe;
1876
1989
  const probeOf = (how === "choice" && o.probe != null && pp && !pp.regret) ? pp : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sovereign-in-violet",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "The Sovereign in Violet: a solo, AI-narrated horror storybook game. Loaded by a small page published as a claude.ai artifact.",
5
5
  "license": "UNLICENSED",
6
6
  "files": [