sovereign-in-violet 1.0.2 → 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 +174 -21
  2. package/package.json +1 -1
package/dist/game.js CHANGED
@@ -1,10 +1,10 @@
1
- /* sovereign-in-violet 1.0.2, 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" + "</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.2";
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,11 +444,98 @@ 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
 
507
+ const ICONS = {
508
+ talk:'<path d="M4 6h16v9H10l-4 4v-4H4z"/>', look:'<ellipse cx="12" cy="12" rx="9" ry="5.5"/><circle cx="12" cy="12" r="2.4"/>',
509
+ go:'<path d="M5 12h12M13 7l5 5-5 5"/>', run:'<circle cx="14" cy="5" r="2"/><path d="M8 21l3-6 3 2v4M6 12l4-3 4 2 3 3"/>',
510
+ hide:'<path d="M3 12s3-5 9-5 9 5 9 5M4 20L20 4"/>', help:'<path d="M12 20s-7-4.5-7-10a4 4 0 0 1 7-2 4 4 0 0 1 7 2c0 5.5-7 10-7 10z"/>',
511
+ fight:'<path d="M5 19l9-9M14 4l6 6-3 1-4-4zM4 15l5 5"/>', make:'<path d="M4 20l4-1 11-11-3-3L5 16zM14 6l3 3"/>',
512
+ wait:'<circle cx="12" cy="12" r="8"/><path d="M12 8v4l3 2"/>', take:'<path d="M6 12V7a2 2 0 0 1 4 0v4M10 11V5a2 2 0 0 1 4 0v6M14 11V7a2 2 0 0 1 4 0v7a6 6 0 0 1-12 0v-2"/>',
513
+ refuse:'<circle cx="12" cy="12" r="8"/><path d="M6.5 6.5l11 11"/>', call:'<path d="M5 4h4l2 5-3 2a11 11 0 0 0 5 5l2-3 5 2v4a2 2 0 0 1-2 2A17 17 0 0 1 3 6a2 2 0 0 1 2-2z"/>'
514
+ };
515
+ const iconSvg = k => ICONS[k] ? `<svg class="cicon" viewBox="0 0 24 24" aria-hidden="true">${ICONS[k]}</svg>` : "";
516
+ function portraitFor(canvas, name, kind){
517
+ canvas.width = 96; canvas.height = 96; const c = canvas.getContext("2d"); let h = 0; for (const ch of name) h = (h*31 + ch.charCodeAt(0)) >>> 0; const r = inkRng(h || 1);
518
+ const bg = c.createRadialGradient(48,40,6,48,48,60); bg.addColorStop(0, kind==="sovereign" ? "#3a2050" : "#efe3cc"); bg.addColorStop(1, kind==="sovereign" ? "#0c0614" : "#c9b48e"); c.fillStyle = bg; c.fillRect(0,0,96,96);
519
+ if (kind === "sovereign"){ c.fillStyle="#07030e"; c.beginPath(); c.ellipse(48,62,30,38,0,0,7); c.fill(); c.fillStyle="#f5d98a"; [36,60].forEach(x=>{ c.beginPath(); c.moveTo(x-8,52); c.quadraticCurveTo(x,44,x+8,52); c.quadraticCurveTo(x,49,x-8,52); c.fill(); }); c.strokeStyle="#f3ead8"; c.lineWidth=2; c.beginPath(); c.moveTo(32,66); c.quadraticCurveTo(48,80,64,66); c.stroke(); return; }
520
+ const skins = ["#f3dcc4","#e6c2a0","#c99a74","#9a6a4a","#f6e2d0"], hairs = ["#2a1a14","#5a3a22","#8a5a2a","#c89a4a","#3a3a44","#7a2a2a"], coats = ["#6a5a8a","#4a6a72","#8a5a6a","#5a6a4a","#7a6050"];
521
+ c.fillStyle = coats[Math.floor(r()*coats.length)]; c.beginPath(); c.ellipse(48,100,32,26,0,0,7); c.fill();
522
+ c.fillStyle = skins[Math.floor(r()*skins.length)]; c.beginPath(); c.arc(48,48,24,0,7); c.fill();
523
+ c.fillStyle = hairs[Math.floor(r()*hairs.length)]; c.beginPath(); c.arc(48,42,25, Math.PI*1.05, Math.PI*1.95 + r()*0.3); c.fill();
524
+ const marked = kind === "servant"; c.fillStyle = marked ? "#9a63e0" : "#2a1f2a"; [40,56].forEach(x=>{ c.beginPath(); c.arc(x,50,2.6,0,7); c.fill(); });
525
+ c.fillStyle="rgba(230,140,140,.4)"; [34,62].forEach(x=>{ c.beginPath(); c.arc(x,57,4,0,7); c.fill(); });
526
+ c.strokeStyle="#2a1f2a"; c.lineWidth=1.6; c.beginPath(); c.arc(48,58,5, .2, Math.PI-.2); c.stroke();
527
+ c.strokeStyle="rgba(60,40,50,.6)"; c.lineWidth=1.4; c.beginPath(); c.arc(48,48,24,0,7); c.stroke();
528
+ }
529
+ function statusStrip(){ // the hidden race, shown only as pictures: his moon, your candle, your shadow
530
+ const s = G.state || {}, m = s.manifest ?? 2, r = s.resolve || 0, h = s.hold || 0, f = s.fade || 0;
531
+ const bite = 10 - Math.min(10, m) * 0.9; // the more he manifests, the fuller (and more golden) his moon
532
+ const moon = `<svg viewBox="0 0 40 40" class="st"><circle cx="20" cy="20" r="13" fill="${m>=6?"#e8c060":"#e9dcb8"}"/><circle cx="${20+bite*1.6}" cy="16" r="12" fill="var(--surface)"/></svg>`;
533
+ const candle = `<svg viewBox="0 0 40 40" class="st"><rect x="16" y="20" width="8" height="15" rx="1" fill="#f1e6cf" stroke="#6b5436"/><path d="M20 ${18-r*0.9}c-${1+r*0.3} 3-${1+r*0.3} 6 0 6s${1+r*0.3}-3 0-6z" fill="#f2b640" opacity="${0.35+r*0.065}"/></svg>`;
534
+ const shadow = `<svg viewBox="0 0 40 40" class="st" style="opacity:${(1-f*0.06).toFixed(2)}"><circle cx="12" cy="12" r="4" fill="#4a3a5a"/><path d="M9 16h6l1 12h-8z" fill="#4a3a5a"/><path d="M8 29 L${14+h*2.2} ${31+h*0.5} L${16+h*2.2} ${33+h*0.5} L10 30z" fill="${h>=7?"#5a2a8a":"#1a1224"}" opacity=".7"/></svg>`;
535
+ return `<div class="status-strip" aria-hidden="true">${moon}${candle}${shadow}</div>`;
536
+ }
450
537
  function choicePanel(opts, {hints=false, customLabel="Write your own", placeholder="Describe your own choice"}={}){
451
- return `<div class="choices">${opts.map((o,i)=>`<button class="choice${o.gift==="own"?" gift-own":o.gift?" gift-choice":""}" data-choice="${i}">${esc(o.text)}${hints&&o.hint?`<span class="hint">${esc(o.hint)}</span>`:""}</button>`).join("")}</div>
538
+ return `<div class="choices">${opts.map((o,i)=>`<button class="choice${o.gift==="own"?" gift-own":o.gift?" gift-choice":""}${o.icon?" has-icon":""}" data-choice="${i}">${iconSvg(o.icon)}${esc(o.text)}${hints&&o.hint?`<span class="hint">${esc(o.hint)}</span>`:""}</button>`).join("")}</div>
452
539
  <div class="extra"><button data-random>Random</button><button data-custom-toggle>${esc(customLabel)}</button></div>
453
540
  <div class="custom" id="customBox" hidden><textarea id="customText" rows="3" placeholder="${esc(placeholder)}"></textarea><div class="row"><button class="btn" data-custom-go>Choose this</button></div></div>`;
454
541
  }
@@ -523,7 +610,7 @@ function renderCreation(){
523
610
  document.getElementById("backBtn").onclick=renderTitle;
524
611
  inp.focus(); return;
525
612
  }
526
- if (s === 1){ renderTensionPick(back); return; }
613
+ if (s === 1){ if (C.skipPick){ C.step = 0; return renderCreation(); } renderTensionPick(back); return; }
527
614
  if (s <= PILLARS.length + 1){
528
615
  const p = PILLARS[s-2];
529
616
  if (!C.options[p.key]){
@@ -650,6 +737,13 @@ const STRENGTH_HINT = v => v>=4 ? "Pulls hard both ways" : v>=3 ? "Pulls both wa
650
737
  function renderTensionPick(back){
651
738
  const hasSaved = C.profile && Array.isArray(C.profile.tset) && C.profile.tset.length;
652
739
  if (!C.quizDone && (!hasSaved || C.retake)) return renderPullQuiz(back);
740
+ // The game decides the pulls from the answers; players no longer pick them. Quiet moments keep refining them later.
741
+ if (!C.skipPick){ C.skipPick = true;
742
+ if (!C.quizDone && hasSaved) C.tset = C.profile.tset.filter(k=>TENSIONS.some(t=>t.key===k));
743
+ C.tensionSet = TENSIONS.filter(t=>C.tset.includes(t.key) || t.key==="devotion");
744
+ C.options = {}; C.step = 2;
745
+ Store.putProfile({...(C.profile||{}), tset:C.tset, own:C.own, personal:C.personal});
746
+ return renderCreation(); }
653
747
  const opts = TENSIONS.filter(t=>t.key!=="devotion");
654
748
  const draw = () => {
655
749
  const firstKeys = C.quizDone ? (C.quizKeys||[]) : C.tset;
@@ -1046,7 +1140,7 @@ function scenePromptRaw(action){
1046
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");
1047
1141
  const edits = G.edits.map(e=>"- "+e.canon).join("\n");
1048
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.`;
1049
- 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, 130 to 230 words, vivid and grounded, ending 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{"choices":[{"text":"under 14 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"],
1050
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"},
1051
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",
1052
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)}`;
@@ -1065,13 +1159,10 @@ async function takeTurn(action, isRetry){
1065
1159
  ctl = new AbortController();
1066
1160
  const res = await sample(scenePrompt(action), {
1067
1161
  modelTier: tier, cache:false, signal: ctl.signal,
1068
- onText: ({text})=>{
1069
- const narr = text.split("@@@")[0];
1070
- const live = document.getElementById("liveScene");
1071
- if (live) { const th = document.getElementById("liveThinking"); if (th) th.remove(); live.innerHTML = paras(narr); }
1072
- }
1162
+ onText: ({text})=>{ try { liveReveal(text); } catch(e){} }
1073
1163
  });
1074
- const parts = res.text.split("@@@");
1164
+ const sp = splitPic(res.text);
1165
+ const parts = sp.rest.split("@@@");
1075
1166
  const narrative = parts[0].trim();
1076
1167
  let data = null;
1077
1168
  if (parts[1]) data = parseLoose(parts[1]);
@@ -1080,6 +1171,7 @@ async function takeTurn(action, isRetry){
1080
1171
  data = { choices: [] }; // keep the scene; stats just don't change this turn
1081
1172
  }
1082
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
1083
1175
  applyScene(action, narrative, data);
1084
1176
  delete G.pending;
1085
1177
  await Store.put(G);
@@ -1144,13 +1236,14 @@ function applyScene(action, narrative, d){
1144
1236
  if (G.history.length > 8) G.history = G.history.slice(-8);
1145
1237
  let refl = null;
1146
1238
  if (reflAllowed() && d.reflection && typeof d.reflection === "string" && d.reflection.trim()){ refl = d.reflection.trim(); G.lastReflection = G.sceneCount; }
1147
- const choices = (Array.isArray(d.choices)?d.choices:[]).slice(0,4).map(c=>({text:String(c.text||""), trait:TRAITS.includes(c.trait)?c.trait:"Wits", risk:["none","low","medium","high"].includes(c.risk)?c.risk:"none", price:c.price?String(c.price):null, probe:[0,1,2].includes(parseInt(c.probe)) ? parseInt(c.probe) : null, regret:["no","yes","unsure"].includes(c.regret) ? c.regret : null, gift: c.gift === "own" ? "own" : (c.gift === true || c.gift === "true" || c.gift === "shortcut") ? "shortcut" : false}));
1239
+ const choices = (Array.isArray(d.choices)?d.choices:[]).slice(0,4).map(c=>({text:String(c.text||""), trait:TRAITS.includes(c.trait)?c.trait:"Wits", risk:["none","low","medium","high"].includes(c.risk)?c.risk:"none", price:c.price?String(c.price):null, probe:[0,1,2].includes(parseInt(c.probe)) ? parseInt(c.probe) : null, regret:["no","yes","unsure"].includes(c.regret) ? c.regret : null, icon: ICONS[c.icon] ? c.icon : null, gift: c.gift === "own" ? "own" : (c.gift === true || c.gift === "true" || c.gift === "shortcut") ? "shortcut" : false}));
1148
1240
  if (choices.filter(c=>c.gift).length > 1) choices.forEach((c,i)=>{ if (c.gift && i !== choices.findIndex(x=>x.gift)) c.gift = false; });
1149
1241
  if (choices.some(c=>c.gift)) G.lastGiftOffer = G.sceneCount;
1150
1242
  const rn = (typeof d.readerNote === "string" && d.readerNote.trim()) ? d.readerNote.trim().slice(0,140) : null;
1151
1243
  if (rn) G.lastReaderNote = G.sceneCount;
1152
1244
  const rAuthor = rn ? readerAuthor() : null;
1153
- G.current = {readerNote: rn, readerBy: rAuthor ? rAuthor.name : "", underline: (rn && typeof d.readerUnderline === "string") ? d.readerUnderline.trim().slice(0,80) : "", text:narrative, choices, reflection:refl, reflectionDone:false, priceScene: action.kind==="face", picture: cleanPicture(d.picture, G.sceneCount*97 + (G.chapter||1)*13)};
1245
+ const speakers = (Array.isArray(d.speakers)?d.speakers:[]).filter(x=>x && x.name).slice(0,3).map(x=>({name:String(x.name).slice(0,30), kind:["sovereign","friend","stranger","servant"].includes(x.kind)?x.kind:"stranger"}));
1246
+ G.current = {speakers, readerNote: rn, readerBy: rAuthor ? rAuthor.name : "", underline: (rn && typeof d.readerUnderline === "string") ? d.readerUnderline.trim().slice(0,80) : "", text:narrative, choices, reflection:refl, reflectionDone:false, priceScene: action.kind==="face", picture: cleanPicture(d.picture, G.sceneCount*97 + (G.chapter||1)*13)};
1154
1247
  if (d.offer === true) G.lastOffer = G.sceneCount;
1155
1248
  if (d.knew === true) G.lastKnew = G.sceneCount;
1156
1249
  if (d.echo === true) G.lastEcho = G.sceneCount;
@@ -1257,10 +1350,11 @@ function endChapter(type, lasting){
1257
1350
  function raceHints(){
1258
1351
  const m = G && G.state ? (G.state.manifest ?? 2) : 0;
1259
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
1260
1354
  const w = document.getElementById("watchEye");
1261
1355
  if (w){ if (!w.innerHTML) w.innerHTML = SIGIL; w.style.opacity = G ? String(0.05 + m*0.035) : "0"; }
1262
1356
  }
1263
- 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"; }
1264
1358
 
1265
1359
  const ENDINGS = {
1266
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.",
@@ -1607,6 +1701,51 @@ function cleanPicture(p, seed){
1607
1701
  weather: ["clear","rain","fog","snow"].includes(p.weather) ? p.weather : "clear",
1608
1702
  figures: Math.max(0, Math.min(4, parseInt(p.figures)||0)), seed, caption: typeof p.caption === "string" ? p.caption.trim().slice(0,70) : "" };
1609
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
+ }
1610
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")}`; }
1611
1750
  function picHtml(id, n, caption){
1612
1751
  return `<figure class="vhs" aria-hidden="true"><canvas id="${id}" width="320" height="240"></canvas>${caption ? `<figcaption>${esc(caption)}</figcaption>` : ""}</figure>`;
@@ -1709,9 +1848,11 @@ function renderStoryShell(action, inner){
1709
1848
  setTimeout(pfxLoading, 0);
1710
1849
  tabs.hidden = false; setTabs();
1711
1850
  const last = G.current;
1712
- app.innerHTML = `<p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?", one-shot":""}</p>
1713
- ${action && action.kind!=="start" ? `<p class="action">${esc(action.label||action.text)}</p>` : ""}
1714
- <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>`;
1715
1856
  const sb = document.getElementById("stopBtn"); if (sb) sb.onclick=()=>ctl?.abort();
1716
1857
  }
1717
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"); }); }
@@ -1730,12 +1871,15 @@ function renderStory(){
1730
1871
  const cur = G.current;
1731
1872
  if (!cur){ if (G.pending) return takeTurn(G.pending); return takeTurn({text:"Begin the story.", kind:"start"}); }
1732
1873
  const last = G.history[G.history.length-1];
1733
- let html = `<p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?`, one-shot, scene ${G.sceneCount}`:`, chapter ${ROMAN[G.chapter||1]||G.chapter}`}</p>`;
1874
+ let html = `<div class="story-head"><p class="step-kicker">${esc(G.char.name)}${G.mode==="oneshot"?`, one-shot, scene ${G.sceneCount}`:`, chapter ${ROMAN[G.chapter||1]||G.chapter}`}</p>${statusStrip()}</div>`;
1734
1875
  if (resumeRecap){ resumeRecap = false; const rc = (G.chronicle||[]).slice(-3); if (rc.length && G.sceneCount > 1) html += `<details class="recap" open><summary>Previously</summary>${rc.map(x=>`<p>${esc(x)}</p>`).join("")}</details>`; }
1735
1876
  if (G.mode !== "oneshot" && G.sceneCount - (G.chapterStart||0) === 1 && !G.ended && !G.intermission) html += epigraphHtml(quoteFor("open", G, G.chapter||1));
1736
1877
  if (last && last.action!=="Began the story." && last.action!=="A new chapter.") html += `<p class="action">${esc(last.action)}</p>`;
1737
1878
  if (last && last.omen) html += `<p class="omen">${esc(last.omen)}</p>`;
1738
- 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);
1739
1883
  let sceneHtml = paras(cur.text);
1740
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); }
1741
1885
  let noteGold = (last && last.whisper) ? `<aside class="note gold">${esc(last.whisper)}</aside>` : "";
@@ -1746,11 +1890,15 @@ function renderStory(){
1746
1890
  noteGold = `<aside class="note gold whisperling" aria-hidden="true">${words.join(" ")}</aside>`; } }
1747
1891
  const noteReader = cur.readerNote ? `<aside class="note reader">${esc(cur.readerNote)}${cur.readerBy ? `<span class="sig">— ${esc(cur.readerBy)}</span>` : ""}</aside>` : "";
1748
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>`;
1749
1895
  if (noteGold) ps.splice(Math.min(1, ps.length), 0, noteGold);
1750
1896
  if (noteReader) ps.splice(Math.min(ps.length, noteGold ? 3 : 2), 0, noteReader);
1751
1897
  sceneHtml = ps.join("");
1752
- 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>`;
1753
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
1754
1902
 
1755
1903
  if (cur.reflection && !cur.reflectionDone){
1756
1904
  html += `<div class="card gold"><h3>A quiet moment</h3><p>${esc(cur.reflection)}</p>
@@ -1806,8 +1954,12 @@ function renderStory(){
1806
1954
  const isPrice = cur.priceScene && G.state.deathsDoor;
1807
1955
  const shown = isPrice ? opts : opts.map(o=>o.price ? {...o, hint:"Price: "+o.price} : o);
1808
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>`;
1809
1958
  app.innerHTML = html;
1959
+ if (sg.half) svPlayed.add(G.id + ":" + G.sceneCount + ":half");
1960
+ svAfterRender(cur);
1810
1961
  paintPicture("scenePic", cur.picture);
1962
+ (cur.speakers||[]).forEach((sp,i)=>{ const cv = document.getElementById("por"+i); if (cv) try { portraitFor(cv, sp.name, sp.kind); } catch(e){} });
1811
1963
  raceHints();
1812
1964
  pfxStory();
1813
1965
  wireChoices(shown, async (o, how)=>{
@@ -1831,6 +1983,7 @@ function renderStory(){
1831
1983
  return takeTurn({text:o.text, kind:"custom", gift: o.giftKind || false, regretOf: (pr && pr.regret) ? pr : null, regret: (pr && pr.regret) ? "instead" : null,
1832
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)});
1833
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); } }
1834
1987
  if (how === "choice") await pfxEcho(shown.indexOf(o));
1835
1988
  const pp = G.pendingProbe;
1836
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.2",
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": [