sovereign-in-violet 1.0.2 → 1.0.3

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 +49 -9
  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.3, 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" + "</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.3";
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">
@@ -447,8 +447,38 @@ async function pfxEcho(chosenIdx){
447
447
  function thinking(msg){ return `<div class="thinking">${SIGIL.replace('class="sigil"','')}<span>${esc(msg)}</span></div>`; }
448
448
  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
449
 
450
+ const ICONS = {
451
+ 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"/>',
452
+ 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"/>',
453
+ 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"/>',
454
+ 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"/>',
455
+ 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"/>',
456
+ 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"/>'
457
+ };
458
+ const iconSvg = k => ICONS[k] ? `<svg class="cicon" viewBox="0 0 24 24" aria-hidden="true">${ICONS[k]}</svg>` : "";
459
+ function portraitFor(canvas, name, kind){
460
+ 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);
461
+ 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);
462
+ 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; }
463
+ const skins = ["#f3dcc4","#e6c2a0","#c99a74","#9a6a4a","#f6e2d0"], hairs = ["#2a1a14","#5a3a22","#8a5a2a","#c89a4a","#3a3a44","#7a2a2a"], coats = ["#6a5a8a","#4a6a72","#8a5a6a","#5a6a4a","#7a6050"];
464
+ c.fillStyle = coats[Math.floor(r()*coats.length)]; c.beginPath(); c.ellipse(48,100,32,26,0,0,7); c.fill();
465
+ c.fillStyle = skins[Math.floor(r()*skins.length)]; c.beginPath(); c.arc(48,48,24,0,7); c.fill();
466
+ 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();
467
+ 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(); });
468
+ c.fillStyle="rgba(230,140,140,.4)"; [34,62].forEach(x=>{ c.beginPath(); c.arc(x,57,4,0,7); c.fill(); });
469
+ c.strokeStyle="#2a1f2a"; c.lineWidth=1.6; c.beginPath(); c.arc(48,58,5, .2, Math.PI-.2); c.stroke();
470
+ c.strokeStyle="rgba(60,40,50,.6)"; c.lineWidth=1.4; c.beginPath(); c.arc(48,48,24,0,7); c.stroke();
471
+ }
472
+ function statusStrip(){ // the hidden race, shown only as pictures: his moon, your candle, your shadow
473
+ const s = G.state || {}, m = s.manifest ?? 2, r = s.resolve || 0, h = s.hold || 0, f = s.fade || 0;
474
+ const bite = 10 - Math.min(10, m) * 0.9; // the more he manifests, the fuller (and more golden) his moon
475
+ 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>`;
476
+ 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>`;
477
+ 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>`;
478
+ return `<div class="status-strip" aria-hidden="true">${moon}${candle}${shadow}</div>`;
479
+ }
450
480
  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>
481
+ 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
482
  <div class="extra"><button data-random>Random</button><button data-custom-toggle>${esc(customLabel)}</button></div>
453
483
  <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
484
  }
@@ -523,7 +553,7 @@ function renderCreation(){
523
553
  document.getElementById("backBtn").onclick=renderTitle;
524
554
  inp.focus(); return;
525
555
  }
526
- if (s === 1){ renderTensionPick(back); return; }
556
+ if (s === 1){ if (C.skipPick){ C.step = 0; return renderCreation(); } renderTensionPick(back); return; }
527
557
  if (s <= PILLARS.length + 1){
528
558
  const p = PILLARS[s-2];
529
559
  if (!C.options[p.key]){
@@ -650,6 +680,13 @@ const STRENGTH_HINT = v => v>=4 ? "Pulls hard both ways" : v>=3 ? "Pulls both wa
650
680
  function renderTensionPick(back){
651
681
  const hasSaved = C.profile && Array.isArray(C.profile.tset) && C.profile.tset.length;
652
682
  if (!C.quizDone && (!hasSaved || C.retake)) return renderPullQuiz(back);
683
+ // The game decides the pulls from the answers; players no longer pick them. Quiet moments keep refining them later.
684
+ if (!C.skipPick){ C.skipPick = true;
685
+ if (!C.quizDone && hasSaved) C.tset = C.profile.tset.filter(k=>TENSIONS.some(t=>t.key===k));
686
+ C.tensionSet = TENSIONS.filter(t=>C.tset.includes(t.key) || t.key==="devotion");
687
+ C.options = {}; C.step = 2;
688
+ Store.putProfile({...(C.profile||{}), tset:C.tset, own:C.own, personal:C.personal});
689
+ return renderCreation(); }
653
690
  const opts = TENSIONS.filter(t=>t.key!=="devotion");
654
691
  const draw = () => {
655
692
  const firstKeys = C.quizDone ? (C.quizKeys||[]) : C.tset;
@@ -1046,7 +1083,7 @@ function scenePromptRaw(action){
1046
1083
  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
1084
  const edits = G.edits.map(e=>"- "+e.canon).join("\n");
1048
1085
  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"],
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"],
1050
1087
  "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
1088
  "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
1089
  "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)}`;
@@ -1144,13 +1181,14 @@ function applyScene(action, narrative, d){
1144
1181
  if (G.history.length > 8) G.history = G.history.slice(-8);
1145
1182
  let refl = null;
1146
1183
  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}));
1184
+ 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
1185
  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
1186
  if (choices.some(c=>c.gift)) G.lastGiftOffer = G.sceneCount;
1150
1187
  const rn = (typeof d.readerNote === "string" && d.readerNote.trim()) ? d.readerNote.trim().slice(0,140) : null;
1151
1188
  if (rn) G.lastReaderNote = G.sceneCount;
1152
1189
  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)};
1190
+ 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"}));
1191
+ 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
1192
  if (d.offer === true) G.lastOffer = G.sceneCount;
1155
1193
  if (d.knew === true) G.lastKnew = G.sceneCount;
1156
1194
  if (d.echo === true) G.lastEcho = G.sceneCount;
@@ -1730,7 +1768,7 @@ function renderStory(){
1730
1768
  const cur = G.current;
1731
1769
  if (!cur){ if (G.pending) return takeTurn(G.pending); return takeTurn({text:"Begin the story.", kind:"start"}); }
1732
1770
  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>`;
1771
+ 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
1772
  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
1773
  if (G.mode !== "oneshot" && G.sceneCount - (G.chapterStart||0) === 1 && !G.ended && !G.intermission) html += epigraphHtml(quoteFor("open", G, G.chapter||1));
1736
1774
  if (last && last.action!=="Began the story." && last.action!=="A new chapter.") html += `<p class="action">${esc(last.action)}</p>`;
@@ -1749,6 +1787,7 @@ function renderStory(){
1749
1787
  if (noteGold) ps.splice(Math.min(1, ps.length), 0, noteGold);
1750
1788
  if (noteReader) ps.splice(Math.min(ps.length, noteGold ? 3 : 2), 0, noteReader);
1751
1789
  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>`;
1752
1791
  html += `<div class="scene">${sceneHtml}</div>`;
1753
1792
  html += `<p class="folio">— ${G.sceneCount} —</p>`;
1754
1793
 
@@ -1808,6 +1847,7 @@ function renderStory(){
1808
1847
  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)"});
1809
1848
  app.innerHTML = html;
1810
1849
  paintPicture("scenePic", cur.picture);
1850
+ (cur.speakers||[]).forEach((sp,i)=>{ const cv = document.getElementById("por"+i); if (cv) try { portraitFor(cv, sp.name, sp.kind); } catch(e){} });
1811
1851
  raceHints();
1812
1852
  pfxStory();
1813
1853
  wireChoices(shown, async (o, how)=>{
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.3",
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": [