Your browser lacks required capabilities. Please upgrade it or switch to another to continue.
Loading…
{
"name": "Vesper",
"ifid": "b1723da6-eeda-4174-ad9a-aca221b5cea3",
"format": "SugarCube",
"format-version": "2.36.1",
"start": "Start"
}<<set $player = {
"name": "Wren",
"portrait": "wren.jpg",
"current_location": "",
"core_traits": {"energy": 100, "money": 50, "fighting": 0, "stealth": 0, "yard_depth": 0, "weapon_repair": 0, "coin": 0, "equipped_weapon": 1, "drain_charge": 1, "arousal_charge": 0, "corruption": 0, "arousal": 0, "hygiene": 100, "sex_stage": 0, "loop_npc_pleasure": 0, "sex_finisher_type": 0, "anal_active": 0, "sex_entry_origin": 0, "drains_done": 0},
"wardrobe": {"company_grays": {"id": "company_grays", "name": "Company grays", "slot": "dress", "image": "./videos/clothing/company_grays.jpg"}, "plain_bra": {"id": "plain_bra", "name": "Plain bra", "slot": "bra", "image": "./videos/clothing/plain_bra.jpg"}, "plain_briefs": {"id": "plain_briefs", "name": "Plain briefs", "slot": "underwear", "image": "./videos/clothing/plain_briefs.jpg"}, "flat_shoes": {"id": "flat_shoes", "name": "Flats", "slot": "shoes", "image": "./videos/clothing/flat_shoes.jpg"}},
"equipped": {"bra": "plain_bra", "underwear": "plain_briefs", "top": null, "bottom": null, "dress": "company_grays", "legwear": null, "shoes": "flat_shoes"}
}>><<set $npcs = {"08112940-c473-44a2-baad-9af2b3d2bb5d": {"name": "Mercer", "portrait": "mercer.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "886a93c3-e70b-455e-893b-eb97578df3c3": {"name": "Renner", "portrait": "renner.jpg", "core_traits": {"relation": 0, "corruption": 0, "arousal": 0}, "flags": {}, "schedule": [], "relationship": ""}}>><<set $npc_interacted_today = {}>><<set $flags = {"game_started": true, "debug_mode": false}>><<set $flags_meta = {}>><<set $game_state = {
"current_canvas": "",
"visited_locations": [],
"visited_nodes": [],
"trigger_history": {},
"activity_trigger_history": {},
"visited_choices": {},
"active_modifiers": {},
"random_cooldowns": {},
"stage_advancement_log": {},
"quests": {},
"scheduled": [],
"fast_jobs": { "xp": 0, "cooldowns": {} },
"bank": { "balance": 0 },
"time_state": {
"current_hour": 20,
"current_minute": 0,
"current_day": "Monday",
"current_week": 1,
"day": 1
}
}>><<nobr>>
<div class="game-intro">
<h1>Vesper</h1>
<p class="game-description">An owned half-human weapon who feels nothing but the sex she's used for. Inside Vance Dynamics she's the company's slave; outside, under a false face, she slips into powerful men's lives and drains them while they think they're using her — sent to hunt a rogue called Cain.</p>
<div class="developer-intro">
<p class="developer-about">We're a small indie studio crafting intimate, story-driven experiences. Every game is made with care, and your support helps us keep creating. If you enjoy our work, consider supporting us!</p>
<p class="support-link">👉 <a href="https://www.patreon.com/cw/nutgames844" target="_blank" rel="noopener">Support us on Patreon</a></p>
</div>
<div class="age-gate">
<p class="age-warning">⚠️ This game contains adult content intended for players 18 years of age or older.</p>
<div class="age-buttons">
[[✓ I am 18 or older - Enter Game->StartingCanvas_canvas_opening_office_Node_1]]
[[✗ I am NOT 18 or older->AgeBlocked]]
</div>
</div>
<div class="developer-footer">
<p class="developer-credit">Developed by <strong>NutGames</strong></p>
<p class="support-link">👉 <a href="https://www.patreon.com/cw/nutgames844" target="_blank" rel="noopener">Support us on Patreon</a></p>
</div>
</div>
<</nobr>><div class="blocked-page">
<h2>Access Denied</h2>
<p>This content is not available for you.</p>
<p>Please close this page.</p>
</div><!-- Time Display Widgets -->
<<widget "timeFormatted">>
<<set _hour to $game_state.time_state.current_hour>>
<<set _minute to $game_state.time_state.current_minute>>
<<set _displayHour to (_hour is 0 ? 12 : (_hour > 12 ? _hour - 12 : _hour))>>
<<set _ampm to (_hour < 12 ? "AM" : "PM")>>
<<set _minuteStr to (_minute < 10 ? "0" + _minute : _minute)>>
<<print _displayHour + ":" + _minuteStr + " " + _ampm>>
<</widget>>
<!-- Render a quest card: narrative line + (Pattern 2) auto-rendered 🎯 goal
block + 💡 tip line. Author tags state-pressure variants in the TOML and
the picker swaps which line fires; widget renders the chosen line, then
calls setup.computeHintGoal() to render the structured progress block from
the hint's stage condition (or canvas trigger conditions for branch-inside-
shell transitions). Game-level mechanics (decay rates, time costs, etc.)
live on :: TipsPage instead. -->
<<widget "renderStageHint">>
/* Pattern 2 (2026-05-01): accepts either a string (legacy callers) OR a
hint object {text, condition, tip, auto_goal, ...}. When passed an object
with auto_goal=true and a stage condition, the engine computes the
structured 🎯 goal block (bullets + live progress + 📍 location). When
passed a string OR a no-auto-goal object, falls back to splitting on
" — 🎯 " (legacy behavior) so existing games keep working unchanged. */
<<set _hintArg to $args[0]>>
<<set _hintObj to (typeof _hintArg === "object" && _hintArg !== null) ? _hintArg : null>>
<<set _hintText to _hintObj ? (_hintObj.text || "") : (_hintArg || "")>>
<<set _computedGoalHtml to _hintObj ? setup.computeHintGoal(_hintObj) : "">>
<<set _separator to " — 🎯 ">>
<<set _splitIdx to _hintText.indexOf(_separator)>>
<<if _computedGoalHtml>>
/* New path: structured auto-render. If text contains " — 🎯 ", strip the
manual goal portion (auto-render replaces it). */
<<if _splitIdx gt -1>>
<<set _flavor to _hintText.substring(0, _splitIdx)>>
<<else>>
<<set _flavor to _hintText>>
<</if>>
/* 2026-05-09: when State C "🔓 Ready" fires AND the hint defines
ready_text, swap flavor → ready_text. The author-supplied ready_text
replaces the regular text once all helper gates are met. */
<<if _hintObj && _hintObj.ready_text && setup._isHintReady(_hintObj)>>
<<set _flavor to _hintObj.ready_text>>
<</if>>
<div class="stage-hint-card">
<div class="stage-hint-flavor"><<print _flavor>></div>
<<print _computedGoalHtml>>
<<if _hintObj && _hintObj.tip>>
<div class="stage-hint-tip">💡 <<print _hintObj.tip>></div>
<</if>>
</div>
<<elseif _splitIdx gt -1>>
/* Legacy path: split on " — 🎯 " */
<<set _flavor to _hintText.substring(0, _splitIdx)>>
<<set _goal to _hintText.substring(_splitIdx + _separator.length)>>
<div class="stage-hint-card">
<div class="stage-hint-flavor"><<print _flavor>></div>
<div class="stage-hint-goal"><span class="stage-hint-target">🎯</span> <<print _goal>></div>
<<if _hintObj && _hintObj.tip>>
<div class="stage-hint-tip">💡 <<print _hintObj.tip>></div>
<</if>>
</div>
<<elseif _hintText>>
<div class="stage-hint-card">
<div class="stage-hint-flavor"><<print _hintText>></div>
</div>
<</if>>
<</widget>>
<<widget "timeDisplay">>
<div id="time-widget" class="time-display">
<div class="time-line">
<span id="time-display"><<timeFormatted>></span> | <span id="current-day"><<print $game_state.time_state.current_day>></span>
</div>
<div class="control-line">
<button class="time-btn" onclick="advanceTime(10)" title="Advance 10 minutes">></button> | <button class="time-btn" onclick="advanceTime(60)" title="Advance 1 hour">>></button> | <button class="time-btn" onclick="advanceTime(1440)" title="Advance 1 day">>>>>></button>
</div>
</div>
<</widget>>
<<widget "sidebarItems">>
<<if setup.sidebar_items && setup.sidebar_items.length > 0>>
<div id="sidebar-items-widget">
<<for _si to 0; _si lt setup.sidebar_items.length; _si++>>
<<set _item to setup.sidebar_items[_si]>>
<<if _item.show_when and not setup.triggerConditionsSatisfied(_item.show_when)>>
<<continue>>
<</if>>
<<if _item.type is "countdown">>
<<set _daysLeft to _item.total_days - $game_state.time_state.day + 1>>
<div class="sidebar-item countdown-item" id="sidebar-countdown-<<print _si>>">
<<if _daysLeft gt 1>>
<<print _daysLeft + " " + _item.label>>
<<elseif _daysLeft is 1>>
<<print "Tomorrow is " + _item.label.replace("days until ", "")>>
<<elseif _daysLeft is 0>>
<<print "Today is " + _item.label.replace("days until ", "")>>
<<else>>
<<print _item.label.replace("days until ", "") + " has passed">>
<</if>>
</div>
<<elseif _item.type is "trait_bar">>
<<set _tbOwner to _item.trait_owner || "player">>
<<set _tbKey to _item.trait>>
<<if _tbOwner is "npc">>
<<set _tbNpcId to _item.npc_id>>
<<set _tbNpcObj to (setup.npc_slug_map && setup.npc_slug_map[_tbNpcId]) ? State.variables.npcs[setup.npc_slug_map[_tbNpcId]] : (State.variables.npcs ? State.variables.npcs[_tbNpcId] : null)>>
<<set _traitVal to (_tbNpcObj && _tbNpcObj.core_traits) ? (_tbNpcObj.core_traits[_tbKey] || 0) : 0>>
<<else>>
<<set _traitVal to ($player && $player.core_traits) ? ($player.core_traits[_tbKey] || 0) : 0>>
<</if>>
<<set _traitMax to _item.max || 100>>
<<set _traitLabel to _item.label || _tbKey>>
<<set _traitPct to Math.max(0, Math.min(100, (_traitVal / _traitMax) * 100))>>
<<set _tbTier to "">>
<<if _item.color_tiers>>
<<for _ti to 0; _ti lt _item.color_tiers.length; _ti++>>
<<if _tbTier is "" and _traitPct lte (_item.color_tiers[_ti].up_to)>>
<<set _tbTier to _item.color_tiers[_ti].class>>
<</if>>
<</for>>
<</if>>
<<set _tbBandText to "">>
<<set _tbBandIcon to "">>
<<if _item.bands>>
<<for _bi to 0; _bi lt _item.bands.length; _bi++>>
<<set _bb to _item.bands[_bi]>>
<<if _tbBandText is "" and _bb.min isnot undefined and _bb.max isnot undefined and _traitVal gte _bb.min and _traitVal lte _bb.max>>
<<set _tbBandText to _bb.text>>
<<set _tbBandIcon to _bb.icon || "">>
<</if>>
<</for>>
<</if>>
<div class="sidebar-item trait-bar-item" id="sidebar-trait-bar-<<print _si>>">
<div class="trait-bar-label">
<<if _item.hide_value is true>>
<<print _traitLabel>>
<<else>>
<<print _traitLabel>>: <<print Math.floor(_traitVal)>> / <<print _traitMax>>
<</if>>
</div>
<div class="trait-bar-bg">
<div class="trait-bar-fill <<print _tbTier>>" style="width: <<print _traitPct>>%"></div>
<<if _tbBandText isnot "">>
<span class="trait-bar-band-text"><<print (_tbBandIcon ? _tbBandIcon + " " : "") + _tbBandText>></span>
<</if>>
</div>
</div>
<<elseif _item.type is "trait_status_text">>
<<set _tsOwner to _item.trait_owner || "player">>
<<set _tsKey to _item.trait>>
<<if _tsOwner is "npc">>
<<set _tsNpcId to _item.npc_id>>
<<set _tsNpcObj to (setup.npc_slug_map && setup.npc_slug_map[_tsNpcId]) ? State.variables.npcs[setup.npc_slug_map[_tsNpcId]] : (State.variables.npcs ? State.variables.npcs[_tsNpcId] : null)>>
<<set _tsVal to (_tsNpcObj && _tsNpcObj.core_traits) ? (_tsNpcObj.core_traits[_tsKey] || 0) : 0>>
<<else>>
<<set _tsVal to ($player && $player.core_traits) ? ($player.core_traits[_tsKey] || 0) : 0>>
<</if>>
<<set _tsText to "">>
<<set _tsIcon to "">>
<<if _item.bands>>
<<for _bi to 0; _bi lt _item.bands.length; _bi++>>
<<set _bb to _item.bands[_bi]>>
<<set _bMin to (_bb.min isnot undefined) ? _bb.min : -1e9>>
<<set _bMax to (_bb.max isnot undefined) ? _bb.max : 1e9>>
<<if _tsText is "" and _tsVal gte _bMin and _tsVal lte _bMax>>
<<set _tsText to _bb.text>>
<<set _tsIcon to _bb.icon || "">>
<</if>>
<</for>>
<</if>>
<<if _tsText isnot "">>
<div class="sidebar-item trait-status-text-item">
<<if _item.label>><div class="band-header"><<print _item.label>></div><</if>>
<span class="band-value"><<print (_tsIcon ? _tsIcon + " " : "") + _tsText>></span>
</div>
<</if>>
<<elseif _item.type is "trait_decay_warning">>
<!-- E20: render an amber warning when a decaying trait dropped today AND
is within 2.0 of its next stage gate. setup.getDecayWarnings() walks
the configured thresholds and compares last_day_snapshot vs current. -->
<<set _decayWarnings to setup.getDecayWarnings(_item.thresholds || {})>>
<<for _dw range _decayWarnings>>
<div class="sidebar-item trait-decay-warning-item">
⚠ <<print _dw.text>>
</div>
<</for>>
<<elseif _item.type is "passes">>
<div class="sidebar-item passes-item" id="sidebar-passes-<<print _si>>">
<<for _pi to 0; _pi lt setup.passes.length; _pi++>>
<<set _pass to setup.passes[_pi]>>
<<set _remaining to setup.getPassDaysRemaining(_pass.id)>>
<<if _remaining gte 0>>
<div class="pass-entry pass-active" id="pass-display-<<print _pass.id>>">
<<print (_pass.icon || "") + " " + _pass.name + ": " + _remaining + "d">>
</div>
<</if>>
<</for>>
</div>
<<elseif _item.type is "inventory">>
<div class="sidebar-item inventory-item" id="sidebar-inventory-<<print _si>>">
<<for _ii to 0; _ii lt setup.items.length; _ii++>>
<<set _itm to setup.items[_ii]>>
<<set _count to setup.getItemCount(_itm.id)>>
<<if _count gt 0>>
<div class="inventory-entry" id="inventory-display-<<print _itm.id>>">
<<print (_itm.icon || "") + " " + _itm.name + ": " + _count>>
</div>
<</if>>
<</for>>
</div>
<<elseif _item.type is "trait_words">>
<<set _twOwner to _item.trait_owner || "player">>
<<set _twKey to _item.trait>>
<<if _twOwner is "npc">>
<<set _twNpcId to _item.npc_id>>
<<set _twNpcObj to (setup.npc_slug_map && setup.npc_slug_map[_twNpcId]) ? State.variables.npcs[setup.npc_slug_map[_twNpcId]] : (State.variables.npcs ? State.variables.npcs[_twNpcId] : null)>>
<<set _twVal to (_twNpcObj && _twNpcObj.core_traits) ? (_twNpcObj.core_traits[_twKey] || 0) : 0>>
<<else>>
<<set _twVal to ($player && $player.core_traits) ? ($player.core_traits[_twKey] || 0) : 0>>
<</if>>
<<set _twMatched to "">>
<<set _twFound to false>>
<<if _item.bands>>
<<for _bi to 0; _bi lt _item.bands.length; _bi++>>
<<if not _twFound>>
<<set _twBand to _item.bands[_bi]>>
<<if _twBand.flag>>
<<if $flags and $flags[_twBand.flag] is true>>
<<set _twMatched to _twBand.text>>
<<set _twFound to true>>
<</if>>
<<elseif _twBand.min isnot undefined and _twBand.max isnot undefined>>
<<if _twVal gte _twBand.min and _twVal lte _twBand.max>>
<<set _twMatched to _twBand.text>>
<<set _twFound to true>>
<</if>>
<</if>>
<</if>>
<</for>>
<</if>>
<<if _twMatched isnot "">>
<div class="sidebar-item trait-words-item" id="sidebar-trait-words-<<print _si>>">
<<if _item.label>><div class="band-header"><<print _item.label>></div><</if>>
<span class="band-value"><<print _twMatched>></span>
</div>
<</if>>
<<elseif _item.type is "stage_label">>
/* E11: render "<prefix>: <stage label>" sourced from arc_stages.
Stage value lives in $player.core_traits[<slug>_stage] (integer).
Out-of-range value → highest-defined label. Undefined trait → just
the prefix with no colon. Empty arc_stages → renders nothing. */
<<set _slNpcId to _item.npc_id>>
<<set _slStages to (setup.npc_arc_stages && setup.npc_arc_stages[_slNpcId]) ? setup.npc_arc_stages[_slNpcId] : []>>
<<set _slRawStage to ($player && $player.core_traits) ? $player.core_traits[_slNpcId + "_stage"] : undefined>>
<<set _slPrefix to _item.prefix>>
<<if not _slPrefix>>
<<set _slUuid to (setup.npc_slug_map && setup.npc_slug_map[_slNpcId]) ? setup.npc_slug_map[_slNpcId] : _slNpcId>>
<<set _slNpcObj to State.variables.npcs ? State.variables.npcs[_slUuid] : null>>
<<set _slPrefix to _slNpcObj ? (_slNpcObj.name || _slNpcId) : _slNpcId>>
<</if>>
<<if _slStages.length gt 0 and _slRawStage isnot undefined>>
<<set _slIdx to Math.max(0, Math.min(Number(_slRawStage), _slStages.length - 1))>>
<<set _slLabel to _slStages[_slIdx]>>
<div class="sidebar-item stage-label-item" id="sidebar-stage-label-<<print _si>>">
<<print _slPrefix>>: <<print _slLabel>>
</div>
<<elseif _slPrefix>>
<div class="sidebar-item stage-label-item" id="sidebar-stage-label-<<print _si>>">
<<print _slPrefix>>
</div>
<</if>>
<<elseif _item.type is "npc_panel">>
/* RTS-style per-NPC card: name header + rows (arousal band / corruption / location).
Location uses setup.getNpcLocation — the SAME schedule source the Schedule page uses. */
<<set _npId to _item.npc_id>>
<<set _npObj to (setup.npc_slug_map && setup.npc_slug_map[_npId]) ? State.variables.npcs[setup.npc_slug_map[_npId]] : (State.variables.npcs ? State.variables.npcs[_npId] : null)>>
<<if _npObj>>
<<set _npRows to _item.rows || ["arousal"]>>
<<set _npHeader to _item.label || _npObj.name || _npId>>
<div class="sidebar-item npc-panel-item" id="sidebar-npc-panel-<<print _si>>">
<div class="npc-panel-header"><<print _npHeader>></div>
<<for _npri to 0; _npri lt _npRows.length; _npri++>>
<<set _npRow to _npRows[_npri]>>
<<if _npRow is "arousal">>
<<if not (setup.hiddenTraits && setup.hiddenTraits.includes("arousal"))>>
<<set _npAr to (_npObj.core_traits) ? (_npObj.core_traits.arousal || 0) : 0>>
<<set _npBands to _item.arousal_bands || [{"min":0,"max":0,"text":"❄️"},{"min":1,"max":1,"text":"🔥"},{"min":2,"max":2,"text":"🔥🔥"},{"min":3,"max":3,"text":"🔥🔥🔥"}]>>
<<set _npArText to "">>
<<for _nbi to 0; _nbi lt _npBands.length; _nbi++>>
<<set _nb to _npBands[_nbi]>>
<<if _npArText is "" and _nb.min isnot undefined and _nb.max isnot undefined and _npAr gte _nb.min and _npAr lte _nb.max>>
<<set _npArText to _nb.text>>
<</if>>
<</for>>
<div class="sidebar-row"><span class="sidebar-label">🔥 Arousal</span> <span class="sidebar-value"><<print _npArText>></span></div>
<</if>>
<<elseif _npRow is "corruption">>
<<if not (setup.hiddenTraits && setup.hiddenTraits.includes("corruption"))>>
<<set _npCorr to (_npObj.core_traits) ? (_npObj.core_traits.corruption || 0) : 0>>
<<set _npCorrOut to _npCorr>>
<<if _item.corruption_max_value isnot undefined and _npCorr gte _item.corruption_max_value>>
<<set _npCorrOut to (_item.corruption_max_label || "MAX")>>
<</if>>
<div class="sidebar-row"><span class="sidebar-label">🫦 Corruption</span> <span class="sidebar-value"><<print _npCorrOut>></span></div>
<</if>>
<<elseif _npRow is "location">>
<<set _npLoc to setup.getNpcLocation(_npId)>>
<<set _npLocName to _npLoc ? (setup._locNameFromUuid(_npLoc.location) || _npLoc.location) : (_item.away_label || "Away")>>
<div class="sidebar-row"><span class="sidebar-label">📍 Location</span> <span class="sidebar-value"><<print _npLocName>></span></div>
<<elseif _npRow is "next">>
/* Next-milestone block — EXACT Quests-page parity: reuses setup.renderQuestsGoalBlock, so
the card shows the same goal block as the Quests page — 🎯 To advance + ◯ live progress
(climbing) / 🔓 Ready + 📍 + 🕒 (ready) / ✓ Arc complete (terminal). No flavor/tip text. */
<<if setup.pickQuestsCard and setup.renderQuestsGoalBlock>>
<<set _npCard to setup.pickQuestsCard(_npId)>>
<<if _npCard>>
<<set _npGoalBlock to setup.renderQuestsGoalBlock(_npCard, setup.evaluateGoals(_npCard))>>
<<if _npGoalBlock>>
<div class="npc-panel-next"><<print _npGoalBlock>></div>
<</if>>
<</if>>
<</if>>
<</if>>
<</for>>
</div>
<</if>>
<</if>>
<</for>>
</div>
<</if>>
<</widget>>
<<widget "activeModifiers">>
<<if $game_state && $game_state.active_modifiers && Object.keys($game_state.active_modifiers).length gt 0>>
<div id="sidebar-modifiers-widget" class="traits-display">
<div class="traits-header">Active Effects</div>
<<set _modKeys to Object.keys($game_state.active_modifiers)>>
<<for _mi to 0; _mi lt _modKeys.length; _mi++>>
<<set _mod to $game_state.active_modifiers[_modKeys[_mi]]>>
<<set _hoursLeft to (_mod.expires_day - $game_state.time_state.day) * 24 + (_mod.expires_hour - $game_state.time_state.current_hour)>>
<div class="modifier-badge"><<print _mod.name>> (<<print _hoursLeft>>h)</div>
<</for>>
</div>
<</if>>
<</widget>>
<<widget "inc">>
<<set _incTrait to $args[0]>>
<<set _incBy to ($args[1] != null) ? Number($args[1]) : 1>>
<<script>>setup.applyAndNotifyTrait("player", null, _incTrait, "add", _incBy, false, null);<</script>>
<</widget>>
<<widget "dec">>
<<set _decTrait to $args[0]>>
<<set _decBy to ($args[1] != null) ? Number($args[1]) : 1>>
<<script>>setup.applyAndNotifyTrait("player", null, _decTrait, "add", -_decBy, false, null);<</script>>
<</widget>>
<<widget "playerTraits">>
<div id="traits-widget" class="traits-display">
<div class="traits-header">Traits</div>
<<if $player and $player.core_traits and Object.keys($player.core_traits).length > 0>>
<<set _keys to Object.keys($player.core_traits).sort()>>
<ul class="traits-list">
<<for _i to 0; _i lt _keys.length; _i++>>
<<set _k to _keys[_i]>>
<<if setup.hiddenTraits && setup.hiddenTraits.includes(_k)>><<continue>><</if>>
<li class="trait-item">
<span class="trait-name"><<print _k>></span>
<span class="trait-value"><<print $player.core_traits[_k]>></span>
</li>
<</for>>
</ul>
<<else>>
<div class="no-traits">No traits</div>
<</if>>
<div class="traits-hint">Updates after each choice.</div>
</div>
<</widget>>
<<widget "playerFlags">>
<div id="flags-widget" class="traits-display">
<div class="traits-header">Flags</div>
<<if $flags and Object.keys($flags).length > 0>>
<<set _fkeys to Object.keys($flags).sort()>>
<ul class="traits-list">
<<for _j to 0; _j lt _fkeys.length; _j++>>
<<set _fk to _fkeys[_j]>>
<li class="trait-item">
<span class="trait-name"><<print _fk>></span>
<span class="trait-value"><<print $flags[_fk] ? '✔' : '✖'>></span>
</li>
<</for>>
</ul>
<<else>>
<div class="no-traits">No flags</div>
<</if>>
<div class="traits-hint">Set by choices and story logic.</div>
</div>
<</widget>>
<<widget "questsButton">>
<!-- Debug/legacy quest view - shows raw mechanics -->
<div id="quests-btn-widget">
<<if passage() isnot "QuestsPage">><<link "Quests" "QuestsPage">><</link>><<else>><span class="nav-row">Quests</span><</if>><span class="nav-i">📋</span>
</div>
<</widget>>
<<widget "missingMediaButton">>
<<if $flags.debug_mode>>
<div id="missing-media-btn-widget" style="margin-bottom:8px;">
<<if passage() isnot "MissingMediaPage">><<link "⚠️ Missing Media" "MissingMediaPage">><</link>><<else>>⚠️ Missing Media<</if>>
</div>
<</if>>
<</widget>>
<<widget "journalButton">>
<!-- Quests - active per-NPC quest cards -->
<div id="journal-btn-widget">
<<if passage() isnot "QuestsPage">><<link "Quests" "QuestsPage">><</link>><<else>><span class="nav-row">Quests</span><</if>><span class="nav-i">📋</span>
</div>
<</widget>>
<<widget "tipsButton">>
<!-- Tips - game-level mechanics surface; conditional on [ui.tips_page] authored -->
<<if setup.tips_page && setup.tips_page.content>>
<div id="tips-btn-widget">
<<if passage() isnot "TipsPage">><<link "Tips" "TipsPage">><</link>><<else>><span class="nav-row">Tips</span><</if>><span class="nav-i">💡</span>
</div>
<</if>>
<</widget>>
<<widget "statsButton">>
<div id="stats-btn-widget">
<<if passage() isnot "StatsPage">><<link "Stats" "StatsPage">><</link>><<else>><span class="nav-row">Stats</span><</if>><span class="nav-i">📊</span>
</div>
<</widget>>
<<widget "scheduleButton">>
<<set _npcsWithSchedules to setup.getNpcsWithSchedules()>>
<<set _soloActivities to setup.getSoloActivitiesForToday()>>
<<if _npcsWithSchedules.length > 0 || _soloActivities.length > 0>>
<div id="schedule-btn-widget">
<<if passage() isnot "SchedulePage">><<link "Schedules" "SchedulePage">><</link>><<else>><span class="nav-row">Schedules</span><</if>><span class="nav-i">📅</span>
</div>
<</if>>
<</widget>>
<<widget "patreonButton">>
<div id="patreon-btn-widget">
<a href="https://www.patreon.com/cw/nutgames844" target="_blank" rel="noopener" class="patreon-link">
<svg class="patreon-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M15.386.524c-4.764 0-8.64 3.876-8.64 8.64 0 4.75 3.876 8.613 8.64 8.613 4.75 0 8.614-3.864 8.614-8.613C24 4.4 20.136.524 15.386.524M.003 23.537h4.22V.524H.003"/>
</svg>
Support Us
</a>
</div>
<</widget>>
<<widget "flagsButton">>
<div id="flags-btn-widget">
<<if passage() isnot "FlagsPage">><<link "Flags" "FlagsPage">><</link>><<else>><span class="nav-row">Flags</span><</if>><span class="nav-i">🚩</span>
</div>
<</widget>><<missingMediaButton>>
<<timeDisplay>>
<<sidebarItems>>
<<activeModifiers>>
<<journalButton>>
<<tipsButton>>
<<statsButton>>
<<scheduleButton>>
<<playerTraits>>
<<patreonButton>>
<style>
#time-widget {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
padding: 8px;
margin-bottom: 10px;
font-family: var(--theme-font-mono);
font-size: 14px;
line-height: 1.2;
color: var(--theme-text);
}
.time-line {
text-align: center;
margin-bottom: 4px;
font-weight: bold;
color: var(--theme-text-strong);
}
.control-line {
text-align: center;
font-size: 12px;
}
.time-btn {
background: var(--theme-surface-alt);
border: 1px solid var(--theme-border);
border-radius: 3px;
color: var(--theme-text-secondary);
cursor: pointer;
padding: 3px 6px;
font-size: 12px;
font-family: var(--theme-font-mono);
font-weight: bold;
}
.time-btn:hover {
background: var(--theme-border);
color: var(--theme-text-strong);
border-color: var(--theme-border);
}
#sidebar-items-widget {
margin-bottom: 8px;
}
.sidebar-item {
text-align: left; /* one type system across the rail — left, theme sans (was centered mono) */
padding: 6px 8px;
font-size: 13px;
line-height: 1.3;
}
.countdown-item {
background: var(--theme-warning-bg);
border: 1px solid var(--theme-warning);
border-radius: 4px;
color: var(--theme-warning-text);
font-weight: bold;
}
.hint-item {
background: var(--theme-success-bg);
border: 1px solid var(--theme-success);
border-radius: 4px;
color: var(--theme-success);
font-style: italic;
}
#traits-widget {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
padding: 8px;
margin-top: 8px;
font-family: var(--theme-font-mono);
font-size: 13px;
color: var(--theme-text);
}
#flags-widget {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
padding: 8px;
margin-top: 8px;
font-family: var(--theme-font-mono);
font-size: 13px;
color: var(--theme-text);
}
.traits-header {
font-weight: bold;
margin-bottom: 6px;
}
.traits-list {
list-style: none;
padding-left: 0;
margin: 0;
}
.trait-item {
display: flex;
justify-content: space-between;
border-bottom: 1px dashed var(--theme-border);
padding: 2px 0;
}
.trait-name {
color: var(--theme-text-secondary);
}
.trait-value {
color: var(--theme-primary);
font-weight: bold;
}
.no-traits {
font-style: italic;
color: var(--theme-text-muted);
}
.traits-hint {
margin-top: 6px;
font-size: 11px;
color: var(--theme-text-muted);
}
#time-display, #current-day {
font-family: var(--theme-font-mono);
color: var(--theme-text-strong);
}
/* ===== Sidebar action buttons — ONE neutral pill family =====
Structure is carried by neutral surfaces; the accent is reserved for hover
(and .is-active). Replaces the old zoo: 4 tinted pills + 2 parchment-serif
journal/tips. All six now share one filled surface pill, left-aligned, muted
label, accent on hover, single 6px radius. */
/* ===== Sidebar action buttons — ONE neutral pill family =====
The link (or the current-page .nav-row span) FILLS the whole padded box, so
the ENTIRE box is clickable — not just the text. The icon overlays the right
edge with pointer-events:none, so clicks there pass through to the link beneath.
Structure in neutrals; accent reserved for hover. */
#phone-sidebar-btn,
#quests-btn-widget,
#journal-btn-widget,
#tips-btn-widget,
#stats-btn-widget,
#schedule-btn-widget,
#flags-btn-widget {
position: relative; /* anchor the icon overlay + unread badge */
background: var(--theme-surface);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 6px;
margin: 8px 0 0 0; /* uniform 8px rhythm — same as the cards */
}
/* the clickable fill: <a> in nav state, .nav-row span on the current page */
#phone-sidebar-btn a,
#quests-btn-widget a, #journal-btn-widget a, #tips-btn-widget a, #stats-btn-widget a, #schedule-btn-widget a, #flags-btn-widget a,
#quests-btn-widget .nav-row, #journal-btn-widget .nav-row, #tips-btn-widget .nav-row, #stats-btn-widget .nav-row, #schedule-btn-widget .nav-row, #flags-btn-widget .nav-row {
display: block;
width: 100%;
box-sizing: border-box;
padding: 8px 30px 8px 10px; /* padded fill → whole box clickable; right room for the icon */
text-align: left; /* override the #ui-bar default center */
color: var(--theme-text-muted);
text-decoration: none;
font-weight: 600;
}
#phone-sidebar-btn a:hover,
#quests-btn-widget a:hover, #journal-btn-widget a:hover, #tips-btn-widget a:hover, #stats-btn-widget a:hover, #schedule-btn-widget a:hover, #flags-btn-widget a:hover {
color: var(--theme-accent);
text-decoration: none;
}
.nav-i { /* right-edge icon overlay; clicks pass through to the link */
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
/* Sidebar spacing is margin-driven, not <br>-driven (kill stray empty-widget breaks). */
#story-caption br { display: none; }
/* Patreon Button in Sidebar */
#patreon-btn-widget {
margin: 15px 0 0 0;
padding: 15px 0 0 0;
border-top: 1px solid #444;
text-align: center;
}
/* Reduce gap between StoryCaption and Save/Restart menu */
#story-caption {
margin-bottom: 0 !important;
padding-bottom: 0 !important;
}
#menu-story {
margin-top: 8px !important;
}
.patreon-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #f96854;
color: #fff !important;
border-radius: 4px;
text-decoration: none;
font-size: 0.9em;
font-weight: bold;
}
.patreon-link:hover {
background: #e85a47;
text-decoration: none !important;
}
.patreon-icon {
flex-shrink: 0;
}
.stats-card {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 6px;
padding: 12px;
margin-bottom: 12px;
}
.stats-name {
font-weight: bold;
font-size: 1.2em;
color: var(--theme-text);
margin-bottom: 8px;
border-bottom: 1px solid var(--theme-border);
padding-bottom: 6px;
}
.stats-traits {
margin-top: 8px;
}
.stats-trait-item {
display: flex;
justify-content: space-between;
padding: 4px 8px;
border-bottom: 1px dashed var(--theme-border);
color: var(--theme-text-strong);
}
.stats-trait-value {
font-weight: bold;
color: var(--theme-primary);
}
/* Story Arc Help Page Styles */
.chapter-context {
background: linear-gradient(135deg, var(--theme-surface) 0%, var(--theme-surface-alt) 100%);
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.chapter-name {
font-size: 1.1em;
color: var(--theme-text);
font-weight: bold;
}
.chapter-mood {
font-size: 0.9em;
color: var(--theme-text-muted);
font-style: italic;
}
.available-section h3 {
font-size: 1em;
color: var(--theme-text-secondary);
margin: 0 0 10px 0;
border-bottom: 1px solid var(--theme-border);
padding-bottom: 6px;
}
.help-card {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 6px;
padding: 12px 16px;
margin-bottom: 10px;
}
.node-name {
font-weight: bold;
color: var(--theme-text);
}
.node-hint {
color: var(--theme-text-muted);
font-size: 0.9em;
margin-top: 4px;
}
.narrative-hint {
background: var(--theme-warning-bg);
border: 1px solid var(--theme-warning);
border-radius: 6px;
padding: 12px;
margin: 16px 0;
}
.narrative-hint p {
margin: 0;
color: var(--theme-warning-text);
}
.progress-summary {
text-align: center;
color: var(--theme-text-muted);
font-size: 0.9em;
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--theme-border);
}
.no-quests-msg {
font-style: italic;
color: var(--theme-text-muted);
padding: 10px;
}
/* Quest Page - Simplified One Activity Per NPC */
.npc-section {
margin-bottom: 20px;
padding: 16px;
background: linear-gradient(135deg, var(--theme-surface) 0%, var(--theme-surface-alt) 100%);
border-radius: 10px;
border: 1px solid var(--theme-border);
}
.npc-name {
font-size: 1.2em;
color: var(--theme-text);
margin: 0 0 12px 0;
border-bottom: 2px solid var(--theme-primary);
padding-bottom: 8px;
font-weight: 600;
}
.guide-hint {
font-size: 0.85em;
color: #f0c040;
font-style: italic;
}
.quest-available {
color: var(--theme-success);
font-weight: 500;
padding: 10px 14px;
background: rgba(40, 167, 69, 0.1);
border-radius: 6px;
border-left: 4px solid var(--theme-success);
}
/* E16: Stage hint two-part card. Flavor on top, goal block below. */
.stage-hint-card {
padding: 12px 14px;
background: rgba(40, 167, 69, 0.08);
border-radius: 6px;
border-left: 4px solid var(--theme-success);
margin: 8px 0;
}
.stage-hint-flavor {
font-style: italic;
opacity: 0.75;
margin-bottom: 10px;
font-size: 0.95em;
}
/* Pattern 2 (2026-05-01) — auto-rendered structured goal block */
.stage-hint-goal-header {
font-weight: 600;
margin-bottom: 6px;
color: var(--theme-success);
}
.stage-hint-goal ul {
list-style: none;
padding-left: 8px;
margin: 4px 0;
}
.stage-hint-goal li {
padding: 3px 0;
font-family: var(--theme-font-mono, 'Courier New', monospace);
font-size: 0.92em;
line-height: 1.4;
}
.stage-hint-met {
color: var(--theme-success);
font-weight: 600;
display: inline-block;
width: 1.2em;
}
.stage-hint-unmet {
color: var(--theme-text-muted);
display: inline-block;
width: 1.2em;
}
.stage-hint-met-row {
color: var(--theme-success);
opacity: 0.9;
}
.stage-hint-unmet-row {
color: var(--theme-text);
opacity: 0.85;
}
.stage-hint-progress {
color: var(--theme-text-muted);
margin-left: 6px;
font-size: 0.88em;
}
.stage-hint-where {
margin-top: 8px;
font-size: 0.9em;
opacity: 0.85;
color: var(--theme-text);
font-style: normal;
}
.stage-hint-tip {
margin-top: 10px;
padding: 6px 10px;
background: rgba(255, 193, 7, 0.08);
border-left: 3px solid var(--theme-warning, #ffc107);
border-radius: 3px;
font-size: 0.9em;
line-height: 1.45;
opacity: 0.92;
}
.stage-hint-path {
margin-top: 6px;
padding: 6px 8px;
border-left: 2px solid var(--theme-accent, #4ecdc4);
background: rgba(78, 205, 196, 0.05);
}
.stage-hint-path strong {
color: var(--theme-accent, #4ecdc4);
font-size: 0.92em;
}
/* Pattern 2 (2026-05-04) — Ready frame: shown when every gate of a stage
transition's trigger evaluates true. Replaces the gate checklist with a
compact "go here, this time window" surface. */
.stage-hint-ready {
color: var(--theme-success);
font-weight: 700;
letter-spacing: 0.02em;
}
/* Pattern 2 (2026-05-09) — Arc-complete frame: shown when an author marks
a hint template `arc_complete = true` (terminal stage in the slice).
Quieter than Ready — reads as closure rather than an action prompt. */
.stage-hint-arc-complete {
color: var(--theme-text-muted, #888);
font-weight: 600;
font-style: italic;
letter-spacing: 0.02em;
}
.quest-locked {
color: var(--theme-warning-text);
font-weight: 500;
padding: 10px 14px;
background: rgba(255, 193, 7, 0.15);
border-radius: 6px;
border-left: 4px solid var(--theme-warning);
}
.quest-conditions {
color: var(--theme-danger);
font-weight: 500;
padding: 10px 14px;
background: rgba(220, 53, 69, 0.1);
border-radius: 6px;
border-left: 4px solid var(--theme-danger);
}
.quest-waiting {
color: var(--theme-warning-text);
font-weight: 500;
padding: 10px 14px;
background: rgba(255, 193, 7, 0.15);
border-radius: 6px;
border-left: 4px solid var(--theme-warning);
font-style: italic;
}
.dev-canvas-info {
background: var(--theme-primary);
color: white;
padding: 6px 10px;
border-radius: 4px;
margin-bottom: 10px;
font-size: 11px;
font-family: var(--theme-font-mono);
}
.quest-complete {
color: var(--theme-success);
font-style: italic;
padding: 10px 14px;
text-align: center;
}
.no-quests {
color: var(--theme-text-muted);
font-style: italic;
padding: 12px;
text-align: center;
}
/* Tips page — wrapped inside an .npc-section card frame on the page itself.
Section headers (<h3>) use the same accent treatment as .npc-name dividers
in the Quests page so the two pages read as the same family. */
.tips-page-content {
line-height: 1.55;
color: var(--theme-text);
}
.tips-page-content h3 {
font-size: 1.05em;
color: var(--theme-text);
margin: 18px 0 8px;
padding-bottom: 6px;
border-bottom: 2px solid var(--theme-primary);
font-weight: 600;
}
.tips-page-content h3:first-child {
margin-top: 0;
}
.tips-page-content p {
margin: 6px 0;
}
.tips-page-content strong {
color: var(--theme-text);
font-weight: 600;
}
/* Trait Requirement Link */
.trait-requirement-link {
color: var(--theme-danger);
text-decoration: underline;
cursor: pointer;
}
.trait-requirement-link:hover {
color: var(--theme-danger);
}
/* Trait Activities Modal Overlay */
.trait-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
}
/* Modal Container */
.trait-modal {
background: #fff;
border-radius: 12px;
max-width: 400px;
width: 90%;
max-height: 80vh;
overflow: hidden;
box-shadow: 0 10px 40px rgba(0,0,0,0.3);
}
.trait-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
background: var(--theme-surface);
border-bottom: 1px solid var(--theme-border);
}
.trait-modal-header h3 {
margin: 0;
font-size: 1.1em;
color: #333;
}
.trait-modal-close {
font-size: 24px;
cursor: pointer;
color: var(--theme-text-muted);
line-height: 1;
}
.trait-modal-close:hover {
color: #333;
}
.trait-modal-progress {
padding: 12px 20px;
background: rgba(220, 53, 69, 0.1);
color: var(--theme-danger);
font-weight: 500;
text-align: center;
}
.trait-modal-body {
padding: 16px 20px;
max-height: 50vh;
overflow-y: auto;
}
/* Activity List */
.trait-activity-list {
list-style: none;
padding: 0;
margin: 0;
}
.trait-activity-list li {
padding: 12px;
border-bottom: 1px solid var(--theme-border);
}
.trait-activity-list li:last-child {
border-bottom: none;
}
.activity-name {
font-weight: 600;
color: #333;
}
.activity-bonus {
float: right;
color: var(--theme-success);
font-weight: 500;
}
.activity-hint {
margin-top: 4px;
color: var(--theme-text-muted);
font-size: 0.9em;
clear: both;
}
/* Effect Toast Notification */
.effect-toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.85);
color: var(--theme-success);
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
z-index: 10002;
white-space: nowrap;
}
/* S4 — Threshold-publisher toast (gated_action). Distinct from the green
effect-toast: amber/warning palette, italic, allows wrapping for the longer
in-character explanation, slightly higher z-index so it doesn't get hidden
behind the success toast when both fire on the same click. */
.effect-toast.notify-warning {
background: rgba(255, 152, 0, 0.95);
color: #1a1a1a;
border-left: 4px solid #c66900;
font-style: italic;
white-space: normal;
max-width: 480px;
text-align: left;
line-height: 1.4;
z-index: 10003;
}
/* Game Intro - Top Left Aligned */
.game-intro {
text-align: left;
}
/* Age Gate Section */
.age-gate {
background: var(--theme-warning-bg);
border: 1px solid var(--theme-warning);
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.age-warning {
font-weight: bold;
color: var(--theme-warning-text);
margin-bottom: 15px;
}
.age-buttons {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
/* Developer Intro Section (above age gate) */
.developer-intro {
text-align: left;
color: var(--theme-text-muted);
margin: 20px 0;
}
/* Developer Footer Section (below age gate) */
.developer-footer {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid var(--theme-border);
text-align: center;
color: var(--theme-text-muted);
font-size: 0.9em;
}
.developer-intro a,
.developer-footer a {
color: #f96854;
text-decoration: none;
}
.developer-intro a:hover,
.developer-footer a:hover {
text-decoration: underline;
}
.developer-about {
line-height: 1.5;
margin: 10px 0;
}
.support-link {
margin-top: 8px;
}
.developer-footer .support-link {
font-weight: bold;
}
/* Age Blocked Page */
.blocked-page {
text-align: center;
padding: 50px;
color: var(--theme-danger);
background: var(--theme-danger-bg);
border-radius: 8px;
margin: 50px auto;
max-width: 400px;
}
/* Override SugarCube default vertical centering */
#story {
align-items: flex-start !important;
justify-content: flex-start !important;
padding-top: 1em !important;
}
.passage {
margin-top: 0 !important;
padding-top: 0 !important;
}
/* New/Unvisited Canvas Highlight */
.new-canvas a {
color: var(--theme-success) !important;
font-weight: 600;
}
.new-canvas a::before {
content: "✨ ";
}
/* Unlocked conditional choice highlight (in passages) */
.unlocked-choice a, .unlocked-choice .link-internal {
color: var(--theme-warning) !important;
font-weight: 600;
}
.unlocked-choice a::before, .unlocked-choice .link-internal::before {
content: "🔓 ";
}
/* Locked choice (visible but not available — Mode A) */
.locked-choice {
color: var(--theme-text-muted);
font-style: italic;
cursor: not-allowed;
opacity: 0.6;
display: inline-block;
padding: 2px 0;
}
.locked-choice::before {
content: "🔒 ";
}
/* Rejection choice (clickable but leads to rejection — Mode B) */
.rejection-choice a, .rejection-choice .link-internal {
color: #b45555 !important;
font-style: italic;
}
.rejection-choice a::before, .rejection-choice .link-internal::before {
content: "⚠️ ";
}
/* Active modifier sidebar badge */
.modifier-badge {
display: inline-block;
background: rgba(168, 85, 247, 0.15);
color: #c084fc;
border: 1px solid rgba(168, 85, 247, 0.3);
border-radius: 12px;
padding: 2px 10px;
font-size: 0.8em;
font-style: italic;
margin: 2px 0;
}
/* Unlocked choice canvas link highlight (in location activity list) */
.unlocked-choice-canvas a {
color: var(--theme-warning) !important;
font-weight: 600;
}
.unlocked-choice-canvas a::before {
content: "🔓 ";
}
/* Navigation New Content Indicator */
.nav-new {
color: var(--theme-warning);
font-weight: bold;
font-size: 1.1em;
}
/* Navigation NPC Portrait Indicator */
.nav-npc-portrait {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
vertical-align: middle;
margin-left: 4px;
border: 2px solid var(--theme-border);
}
/* ===============================================
VISUAL NAVIGATION GRID
=============================================== */
.location-nav-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
margin: 8px 0;
}
@media (min-width: 768px) {
.location-nav-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 1024px) {
.location-nav-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* Location Card */
.location-card {
display: flex;
flex-direction: column;
background: #2a2a2a;
border-radius: 8px;
overflow: hidden;
text-decoration: none !important;
color: inherit !important;
transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
}
.location-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
/* Card Image Area (5:2 aspect ratio - more compact) */
.location-card-image {
width: 100%;
padding-bottom: 40%; /* 5:2 ratio */
background-size: cover;
background-position: center;
position: relative;
background-color: #1a1a1a;
}
/* Placeholder silhouette for locations without images */
.location-card-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #2d2d3a 0%, #1a1a2a 100%);
}
.location-card-placeholder svg {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60%;
height: 60%;
opacity: 0.5;
}
/* Card Content Area */
.location-card-content {
padding: 6px 8px;
display: flex;
flex-direction: column;
gap: 2px;
}
.location-card-name {
font-weight: 600;
font-size: 0.9em;
line-height: 1.2;
color: var(--theme-text);
}
/* Indicators Row */
.location-card-indicators {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
/* NEW badge (replaces ! indicator in visual mode) */
.nav-new-badge {
background: var(--theme-warning);
color: #000;
font-size: 0.65em;
font-weight: bold;
padding: 2px 5px;
border-radius: 3px;
text-transform: uppercase;
}
/* NPC portrait badges (smaller version for cards) */
.nav-npc-badge {
width: 20px;
height: 20px;
border-radius: 50%;
object-fit: cover;
border: 1px solid var(--theme-text-muted);
}
/* Locked destination card (lock-as-prose): shown but not clickable, with the reason */
.location-card-locked {
opacity: 0.6;
cursor: not-allowed;
filter: grayscale(0.4);
}
.location-card-locked .location-card-image {
filter: grayscale(0.6) brightness(0.85);
}
.nav-locked-reason {
font-size: 0.7em;
line-height: 1.25;
color: var(--theme-text-muted);
font-style: italic;
margin-top: 2px;
}
/* Text-mode locked link */
.nav-link-locked {
color: var(--theme-text-muted);
font-style: italic;
}
/* Travel-cost tag on a nav destination (neutral, informational) */
.nav-cost-tag {
font-size: 0.65em;
color: var(--theme-text-muted);
background: var(--theme-surface);
border: 1px solid var(--theme-border, rgba(255,255,255,0.12));
border-radius: 3px;
padding: 1px 5px;
margin-left: 4px;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
/* Exit Links Section (text-only, below grid) */
.location-nav-exits {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #444;
}
.location-nav-exits a {
color: var(--theme-text-muted);
}
.location-nav-exits a:hover {
color: #fff;
}
/* ===== NPC Portrait System (Location Interaction) ===== */
/* Grid of clickable NPC portraits shown at locations */
.location-npcs {
display: flex;
gap: 1.5rem;
justify-content: center;
padding: 1.5rem 0;
flex-wrap: wrap;
}
.npc-portrait-card {
text-align: center;
cursor: pointer;
transition: transform 0.2s ease;
position: relative;
}
.npc-portrait-card:hover {
transform: scale(1.08);
}
.npc-portrait-link {
text-decoration: none !important;
color: inherit !important;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
}
.npc-portrait-img {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 3px solid var(--theme-text-muted);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.npc-portrait-card:hover .npc-portrait-img {
border-color: var(--theme-border);
box-shadow: 0 0 12px rgba(200, 200, 200, 0.2);
}
/* Initial letter fallback for missing portraits */
.npc-portrait-placeholder {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, #3a3a4a 0%, #2a2a3a 100%);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
font-weight: 600;
color: var(--theme-border);
border: 3px solid var(--theme-text-muted);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.npc-portrait-card:hover .npc-portrait-placeholder {
border-color: var(--theme-border);
box-shadow: 0 0 12px rgba(200, 200, 200, 0.2);
}
.npc-portrait-name {
display: block;
font-size: 0.85rem;
color: var(--theme-text-muted);
line-height: 1.2;
}
/* .npc-badge kept as base class for .npc-cost-badge (cost-blocked portraits).
NEW/unlocked portrait border + badge classes removed 2026-05-25. */
.npc-badge {
position: absolute;
top: -4px;
right: -4px;
font-size: 0.6rem;
font-weight: bold;
padding: 2px 5px;
border-radius: 8px;
text-transform: uppercase;
}
/* Solo activity buttons (activities with no NPC) */
.location-solo-activities {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #333;
}
.solo-activity-btn {
display: inline-block;
padding: 0.4rem 1rem;
margin: 0.3rem 0;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 4px;
color: var(--theme-text) !important;
text-decoration: none !important;
transition: background 0.2s ease;
}
.solo-activity-btn:hover {
background: var(--theme-surface-alt);
color: var(--theme-text-strong) !important;
}
/* ===== Cost-blocked states ===== */
/* Blocked NPC portraits (greyed out) */
.npc-portrait-blocked .npc-portrait-img,
.npc-portrait-blocked .npc-portrait-placeholder {
filter: grayscale(70%) brightness(0.6);
border-color: #444;
}
.npc-portrait-blocked .npc-portrait-name {
color: var(--theme-text-muted);
}
.npc-portrait-blocked:hover {
transform: scale(1.04);
}
/* Cost badge on portraits */
.npc-cost-badge {
background: var(--theme-danger);
color: #fff;
font-size: 0.55rem;
position: absolute;
bottom: 18px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
padding: 1px 6px;
border-radius: 8px;
top: auto;
right: auto;
}
/* Blocked solo activities */
.solo-activity-blocked {
opacity: 0.5;
}
.solo-activity-blocked:hover {
opacity: 0.7;
}
.solo-cost-tag {
font-size: 0.75em;
color: var(--theme-danger);
}
/* E21 — author-opt-in cooldown entry. Non-clickable dimmed text. */
.solo-activity-cooldown {
display: inline-block;
padding: 4px 8px;
margin-bottom: 2px;
color: var(--theme-border);
font-style: normal;
font-size: 0.9em;
opacity: 0.65;
}
.solo-activity-cooldown em {
font-size: 0.85em;
opacity: 0.85;
}
/* Cost-blocked passage message */
.cost-blocked-message {
text-align: center;
padding: 2rem;
color: var(--theme-text-muted);
font-style: italic;
}
.cost-blocked-message p {
margin-bottom: 1rem;
}
/* ===== E20: Sidebar decay warning ===== */
.trait-decay-warning-item {
margin-top: 0.5rem;
padding: 6px 10px;
background: rgba(255, 193, 7, 0.18);
border-left: 3px solid var(--theme-warning);
border-radius: 4px;
color: var(--theme-warning-text);
font-size: 0.78rem;
line-height: 1.35;
}
/* ===== Sidebar band cards (trait_words / trait_status_text / trait_bar) =====
ONE card language. Structure is carried by NEUTRAL surfaces + spacing; the
accent is RESERVED for signal (hover / .is-active), never the resting stripe.
trait_words is the HERO stat (corruption / identity) — raised surface + larger
value so it anchors the top of the rail instead of reading as a peer card. */
.trait-words-item,
.trait-status-text-item,
.trait-bar-item {
text-align: left;
margin-top: 0.5rem;
padding: 8px 10px;
background: var(--theme-surface);
border: 1px solid rgba(255, 255, 255, 0.07); /* hairline — survives across tiers, clears the eye (the old #333 border was ~1.3:1, invisible) */
border-left: 3px solid rgba(255, 255, 255, 0.10); /* neutral at rest */
border-radius: 6px;
color: var(--theme-text);
font-size: 0.78rem;
line-height: 1.35;
}
/* HERO: the identity word-state (corruption) reads as the top-of-rail anchor. */
.trait-words-item {
background: var(--theme-surface-hover); /* +0.08 lighter — the existing 'raised' tier */
}
.trait-words-item .band-value {
font-size: 1.05rem;
line-height: 1.25;
}
/* The accent/semantic left-stripe is a SCARCE SIGNAL (active / urgent / locked),
not decoration — a row earns it; toggled by emitting the class on that item. */
.sidebar-item.is-active { border-left-color: var(--theme-accent); }
.sidebar-item.is-urgent { border-left-color: var(--theme-warning); }
.sidebar-item.is-locked { border-left-color: var(--theme-danger); }
/* the label, rendered as a compact card header (matches .npc-panel-header) */
.band-header {
color: var(--theme-text-muted);
font-weight: bold;
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 4px;
}
/* the value line (word / status text) */
.band-value {
color: var(--theme-text-strong);
font-weight: 600;
font-size: 0.86rem;
font-variant-numeric: tabular-nums lining-nums; /* columns don't jitter as values change */
}
/* ===== trait_bar — banded word over a flat fill bar ===== */
.trait-bar-label {
color: var(--theme-text-muted);
font-weight: bold;
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 5px;
}
.trait-bar-bg {
height: 8px;
background: var(--theme-surface-alt);
border-radius: 6px;
overflow: hidden;
position: relative;
}
.trait-bar-fill {
height: 100%;
background: var(--theme-accent);
border-radius: 6px;
transition: width 0.3s ease;
}
/* Banded mode: when a trait_bar's band-text overlay is rendered, the bg grows
to fit the overlay text. Gated via :has() so non-banded bars stay at 8px. */
.trait-bar-bg:has(.trait-bar-band-text) {
height: 26px;
border-radius: 6px;
}
.trait-bar-band-text {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 9px;
font-size: 0.78rem;
font-weight: 600;
color: var(--theme-text-strong);
font-variant-numeric: tabular-nums lining-nums;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
pointer-events: none;
z-index: 2;
white-space: nowrap;
}
/* Built-in value-tier fills — flat & matte (set a tier class via color_tiers).
Authors can override by shipping their own .trait-bar-fill.<class> rules. */
.trait-bar-fill.low {
background: #3b82c4;
}
.trait-bar-fill.medium {
background: #d98a3a;
}
.trait-bar-fill.high {
background: #d65a5a;
}
/* NPC panel card (npc_panel sidebar item) — RTS House-card: header strip + label/value rows */
.npc-panel-item {
text-align: left;
padding: 0;
margin-top: 0.5rem;
background: var(--theme-surface);
border: 1px solid rgba(255, 255, 255, 0.07);
border-radius: 6px;
overflow: hidden;
}
.npc-panel-header {
padding: 5px 8px;
background: var(--theme-surface-alt);
border-bottom: 1px solid rgba(255, 255, 255, 0.07);
color: var(--theme-text-strong);
font-weight: bold;
font-size: 12px;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.npc-panel-item .sidebar-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 4px 8px;
font-size: 12px;
}
.npc-panel-item .sidebar-label {
color: var(--theme-text-muted);
white-space: nowrap;
}
.npc-panel-item .sidebar-value {
color: var(--theme-text-strong);
font-weight: 500;
text-align: right;
font-variant-numeric: tabular-nums lining-nums;
}
.npc-panel-next {
margin-top: 4px;
padding: 4px 8px 2px;
border-top: 1px solid rgba(255, 255, 255, 0.07);
}
/* the inner goal block reuses the Quests-page classes (.quests-goal/.quests-ready/.quests-where) */
/* Dev Mode Controls */
.dev-adj-btn {
background: var(--theme-text-muted);
color: white;
border: none;
border-radius: 3px;
width: 20px;
height: 20px;
font-size: 14px;
font-weight: bold;
cursor: pointer;
padding: 0;
line-height: 1;
vertical-align: middle;
}
.dev-adj-btn:hover {
background: var(--theme-text-muted);
}
.trait-controls {
display: flex;
align-items: center;
gap: 4px;
}
.trait-controls .trait-value {
min-width: 24px;
text-align: center;
}
/* Portrait System */
.portrait {
width: 40px;
height: 40px;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
background: var(--theme-surface-alt);
display: flex;
align-items: center;
justify-content: center;
}
.portrait img {
width: 100%;
height: 100%;
object-fit: cover;
}
.portrait-placeholder svg {
width: 70%;
height: 70%;
}
/* Dialog Block with Portrait */
.dialog-block {
display: flex;
align-items: center;
gap: 12px;
margin: 10px 0;
padding: 10px;
border-radius: 8px;
color: var(--theme-text);
}
.dialog-player {
border-left: 4px solid var(--theme-primary);
background: var(--theme-primary-bg);
}
.dialog-player strong {
color: var(--theme-primary);
}
.dialog-npc {
border-left: 4px solid var(--theme-text-muted);
background: var(--theme-surface);
}
.dialog-npc strong {
color: var(--theme-text-secondary);
}
.dialog-content {
flex: 1;
}
/* Thought Bubble Block (S8) — interior monologue, paired with cascade beats */
.thought-bubble {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0 8px 24px;
padding: 8px 12px;
border-radius: 12px;
background: var(--theme-surface-alt);
color: var(--theme-text-secondary);
font-style: italic;
border-left: 4px dashed var(--theme-text-secondary);
}
.thought-bubble-player {
border-left-color: var(--theme-accent, var(--theme-text-primary));
}
.thought-bubble-content {
flex: 1;
}
/* Cascade Advance Links (S7) — visual cue distinct from choice exits */
.macro-linkreplace a {
display: inline-block;
margin-top: 1px;
padding: 2px 8px;
background: var(--theme-surface);
border: 1px solid var(--theme-accent, var(--theme-text-primary));
border-radius: 6px;
color: var(--theme-accent, var(--theme-text-primary));
text-decoration: none;
font-size: 0.95em;
line-height: 1.3;
}
.macro-linkreplace a:hover {
background: var(--theme-accent, var(--theme-text-primary));
color: var(--theme-bg, #fff);
}
/* Engine "no exits satisfied" dev diagnostic (2026-05-06) — fires when a
canvas's all-conditional exit_block has zero satisfied choices. Visible
only when $flags.debug_mode is true. The console.warn fires regardless. */
.engine-diag-no-exits {
background: #2a0a0a;
border: 2px solid #c0392b;
padding: 12px 16px;
margin: 16px 0;
border-radius: 4px;
color: #f5d8d6;
font-family: var(--theme-font-mono, monospace);
font-size: 12px;
}
.engine-diag-header {
font-weight: bold;
color: #ff6b6b;
margin-bottom: 8px;
}
.engine-diag-hint {
color: #f0c0bd;
margin-bottom: 12px;
font-style: italic;
}
.engine-diag-choice {
background: #1a0505;
padding: 8px 12px;
margin: 8px 0;
border-left: 3px solid #c0392b;
}
.engine-diag-choice-text { font-weight: bold; }
.engine-diag-choice-logic { font-size: 11px; color: #d4a8a4; margin-bottom: 4px; }
.engine-diag-items { margin: 4px 0 0 0; padding-left: 16px; list-style: none; }
.engine-diag-items li { margin: 2px 0; }
.engine-diag-pass { color: #6dcf6d; }
.engine-diag-fail { color: #ff8a85; }
/* Stats Card Portrait (larger) */
.stats-portrait {
width: 80px;
height: 100px;
border-radius: 8px;
overflow: hidden;
background: var(--theme-surface-alt);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
}
.stats-portrait img {
width: 100%;
height: 100%;
object-fit: cover;
}
.stats-portrait svg {
width: 60%;
height: 60%;
}
/* Stats Card with Portrait Layout */
.stats-card-with-portrait {
display: flex;
gap: 12px;
align-items: flex-start;
}
.stats-info {
flex: 1;
min-width: 0;
}
/* Wardrobe Page */
.wardrobe-page {
margin: 12px 0;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 10px;
padding: 16px;
}
.wardrobe-table {
width: 100%;
border-collapse: collapse;
}
.wardrobe-row {
border-bottom: 1px solid var(--theme-border);
}
.wardrobe-row:last-child {
border-bottom: none;
}
.wardrobe-row-disabled {
opacity: 0.45;
}
.wardrobe-slot-label {
padding: 12px 8px;
font-weight: 700;
color: #333;
width: 90px;
vertical-align: middle;
font-size: 0.85em;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.wardrobe-slot-items {
padding: 10px 4px;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.wardrobe-item {
position: relative;
width: 76px;
height: 76px;
border: 2px solid var(--theme-border);
border-radius: 8px;
cursor: pointer;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
}
.wardrobe-item:hover {
border-color: var(--theme-text-muted);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.wardrobe-item-equipped {
border-color: var(--theme-success);
background: var(--theme-success-bg);
box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.25);
}
.wardrobe-item-equipped:hover {
border-color: var(--theme-success);
}
.wardrobe-item-locked {
opacity: 0.5;
cursor: not-allowed;
filter: grayscale(50%);
}
.wardrobe-item-locked:hover {
transform: none;
box-shadow: none;
border-color: var(--theme-border);
}
.wardrobe-item-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.wardrobe-item-text {
font-size: 0.8em;
text-align: center;
padding: 6px;
word-break: break-word;
color: #333;
line-height: 1.2;
}
.wardrobe-badge-equipped {
position: absolute;
top: 2px;
right: 2px;
background: var(--theme-success);
color: #fff;
font-size: 11px;
width: 18px;
height: 18px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.wardrobe-badge-locked {
position: absolute;
bottom: 2px;
right: 2px;
font-size: 13px;
}
.wardrobe-unequip-btn {
background: none;
border: 1px solid var(--theme-text-muted);
color: var(--theme-text-muted);
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 16px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
transition: background 0.15s, color 0.15s;
}
.wardrobe-unequip-btn:hover {
background: var(--theme-danger);
border-color: var(--theme-danger);
color: #fff;
}
.wardrobe-disabled-hint,
.wardrobe-empty-hint {
color: var(--theme-text-muted);
font-style: italic;
font-size: 0.85em;
}
.entry-blocked {
color: var(--theme-danger);
font-weight: 500;
}
.entry-requirements {
color: var(--theme-warning-text);
background: var(--theme-warning-bg);
padding: 8px 12px;
border-radius: 6px;
font-size: 0.9em;
}
.entry-blocked-narrative {
font-style: italic;
color: var(--theme-text-muted);
line-height: 1.6;
margin: 12px 0;
}
#wardrobe-btn-widget button {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
color: #333;
padding: 6px 12px;
border-radius: 6px;
cursor: pointer;
width: 100%;
font-size: 0.95em;
margin: 4px 0;
}
#wardrobe-btn-widget button:hover {
background: var(--theme-surface-alt);
border-color: var(--theme-border);
}
/* Shop page styles */
.shop-page {
margin: 12px 0;
}
.shop-money {
background: var(--theme-success-bg);
border: 1px solid var(--theme-success);
border-radius: 8px;
padding: 10px 16px;
font-size: 1.05em;
font-weight: 600;
color: var(--theme-success);
margin-bottom: 16px;
display: inline-block;
}
.shop-tier {
margin-bottom: 20px;
border: 1px solid var(--theme-border);
border-radius: 10px;
overflow: hidden;
background: var(--theme-surface);
}
.shop-tier-locked {
opacity: 0.55;
}
.shop-tier-header {
background: var(--theme-text-strong);
color: #fff;
padding: 10px 16px;
font-weight: 700;
font-size: 0.95em;
display: flex;
justify-content: space-between;
align-items: center;
}
.shop-tier-locked .shop-tier-header {
background: var(--theme-text-muted);
}
.shop-tier-req {
font-weight: 400;
font-size: 0.85em;
opacity: 0.8;
}
.shop-tier-locked-msg {
padding: 12px 16px;
color: var(--theme-warning-text);
background: var(--theme-warning-bg);
font-style: italic;
font-size: 0.9em;
text-align: center;
}
.shop-items {
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.shop-slot-group {
margin-bottom: 4px;
}
.shop-slot-label {
font-weight: 700;
font-size: 0.8em;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--theme-text-secondary);
padding: 4px 0;
border-bottom: 1px solid var(--theme-surface-alt);
margin-bottom: 6px;
}
.shop-slot-items {
display: flex;
flex-direction: column;
gap: 6px;
}
.shop-item {
display: flex;
align-items: center;
gap: 12px;
background: #fff;
border: 1px solid var(--theme-border);
border-radius: 8px;
padding: 10px 14px;
transition: border-color 0.15s;
}
.shop-item-thumb {
width: 56px;
height: 56px;
border: 2px solid var(--theme-border);
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: var(--theme-surface);
flex-shrink: 0;
}
.shop-item-thumb-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.shop-item-thumb-text {
font-size: 0.7em;
text-align: center;
padding: 4px;
word-break: break-word;
color: var(--theme-text-muted);
line-height: 1.2;
}
.shop-item:hover {
border-color: var(--theme-border);
}
.shop-item-owned {
border-color: var(--theme-success);
background: var(--theme-success-bg);
}
.shop-item-unaffordable {
opacity: 0.55;
}
.shop-item-info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
min-width: 0;
}
.shop-item-name {
font-weight: 600;
color: var(--theme-text);
font-size: 0.95em;
}
.shop-item-action {
display: flex;
align-items: center;
gap: 10px;
}
.shop-item-price {
font-weight: 600;
color: var(--theme-text-secondary);
font-size: 0.95em;
}
.shop-item-owned-badge {
color: var(--theme-success);
font-weight: 600;
font-size: 0.9em;
}
.shop-item-cant-afford {
color: var(--theme-danger);
font-size: 0.85em;
font-style: italic;
}
.shop-buy-btn {
background: var(--theme-primary);
color: #fff;
border: none;
padding: 6px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
font-weight: 600;
transition: background 0.15s;
}
.shop-buy-btn:hover {
background: var(--theme-primary);
}
/* Rent page styles */
.rent-title {
color: var(--theme-bg);
margin-bottom: 16px;
font-size: 1.4em;
}
.rent-choices {
margin: 16px 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.rent-choices a {
display: block;
padding: 10px 16px;
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 6px;
color: var(--theme-text);
text-decoration: none;
font-weight: 500;
transition: background 0.15s, border-color 0.15s;
}
.rent-choices a:hover {
background: var(--theme-surface-alt);
border-color: var(--theme-border);
}
.rent-balance {
margin-top: 12px;
padding: 8px 12px;
background: rgba(0,0,0,0.04);
border-radius: 6px;
font-size: 0.95em;
}
.game-over-text {
text-align: center;
font-size: 1.5em;
font-weight: bold;
color: var(--theme-danger);
margin: 20px 0;
letter-spacing: 2px;
}
</style><<widget "renderQuestsCard">>
<<set _card to $args[0]>>
<<if _card>>
<<set _goalState to setup.evaluateGoals(_card)>>
<<set _flavor to (_goalState.allMet && _card.ready_text) ? _card.ready_text : _card.text>>
<<set _goalBlock to setup.renderQuestsGoalBlock(_card, _goalState)>>
<div class="quests-card">
<div class="quests-flavor"><<print _flavor>></div>
<<if _goalBlock>><<print _goalBlock>><</if>>
<<if _card.tip>><div class="quests-tip">💡 <<print _card.tip>></div><</if>>
</div>
<</if>>
<</widget>><<nobr>>
<h2>What's Next</h2>
/* PRD 48 — Quests Engine V2 page. One card per arc NPC + Story Goals
section. Picker swaps cards as state crosses; goal-block frame swaps
from 🎯 (climbing) → 🔓 (ready) → ✓ (terminal) automatically. */
<<set _goals = setup.pickQuestsCards("story_goals")>>
<<if _goals.length > 0>>
<div class="quests-section">
<h3>Story Goals</h3>
<<for _g range _goals>><<renderQuestsCard _g>><</for>>
</div>
<</if>>
<<set _helpData = setup.help_data || {}>>
<<set _hasNpcs = _helpData.npcs && Object.keys(_helpData.npcs).length > 0>>
<<if _hasNpcs>>
<<for _npcId, _npcData range _helpData.npcs>>
<<set _slug = setup.npcSlugForId(_npcId)>>
<<set _card = _slug ? setup.pickQuestsCard(_slug) : null>>
<<if _card>>
<div class="quests-section">
<h3><<print ($npcs[_npcId] && $npcs[_npcId].name) || _npcData.name>></h3>
<<renderQuestsCard _card>>
</div>
<</if>>
<</for>>
<</if>>
<<if _goals.length === 0 && !_hasNpcs>>
<div class="no-quests">No active quests.</div>
<</if>>
<</nobr>>
<<link "← Back">><<run setup.smartBack()>><</link>><<nobr>>
<<set _tp = setup.tips_page || {}>>
<h2><<print _tp.title || "Tips">></h2>
<div class="npc-section">
<div class="tips-page-content">
<<print _tp.content || "No tips available.">>
</div>
</div>
<</nobr>>
<<link "← Back">><<run setup.smartBack()>><</link>><<set _position = setup.detectStoryPosition()>>
<<set _hint = setup.generateNarrativeHint()>>
<div class="journal-container">
<div class="journal-header">
<<if _position.current_chapter>>
<h2 class="journal-chapter-title"><<print _position.current_chapter.name>></h2>
<<if _position.current_chapter.mood>>
<div class="journal-chapter-mood <<print _position.current_chapter.mood>>">
<<print _position.current_chapter.description || "">>
</div>
<</if>>
<<else>>
<h2 class="journal-chapter-title">Your Story</h2>
<</if>>
<<if _position.progress_percent > 0>>
<div class="journal-progress">
<div class="journal-progress-bar">
<div class="journal-progress-fill" style="width: <<print _position.progress_percent>>%;"></div>
</div>
</div>
<</if>>
</div>
<!-- Memories (completed story moments) -->
<<if _position.completed_nodes.length > 0>>
<div class="journal-section">
<div class="journal-section-title">Memories</div>
<<for _i, _node range _position.completed_nodes>>
<div class="journal-memory <<if _node.is_milestone>>milestone<</if>>">
<div class="journal-memory-title"><<print _node.name>></div>
<<if _node.journal_entry>>
<<print _node.journal_entry>>
<</if>>
</div>
<</for>>
</div>
<</if>>
<!-- Active story threads (groups in progress) -->
<<if _position.active_groups.length > 0>>
<<set _incompleteGroups = _position.active_groups.filter(function(g) { return !g.isComplete && g.completed > 0; })>>
<<if _incompleteGroups.length > 0>>
<div class="journal-section">
<div class="journal-section-title">Unfolding</div>
<<for _i, _group range _incompleteGroups>>
<div class="journal-group">
<div class="journal-group-name"><<print _group.name>></div>
<div class="journal-group-progress">
<<print _group.description>>
</div>
</div>
<</for>>
</div>
<</if>>
<</if>>
<!-- Relationship states (NPC emotions) -->
<<if Object.keys($npcs).length > 0>>
<div class="journal-section">
<div class="journal-section-title">Connections</div>
<<for _npcId range Object.keys($npcs)>>
<<set _state = setup.interpretNpcState(_npcId)>>
<<if _state.npc_name>>
<div class="journal-npc">
<div class="journal-npc-name"><<print _state.npc_name>></div>
<div class="journal-npc-emotion"><<print _state.description>></div>
<<if _state.relationship_summary && _state.relationship_summary !== _state.description>>
<div class="journal-npc-summary"><<print _state.relationship_summary>></div>
<</if>>
</div>
<</if>>
<</for>>
</div>
<</if>>
<!-- Subtle narrative hint (only when needed) -->
<<if _hint.hint_type !== "none" && _hint.text>>
<div class="journal-hint <<print _hint.hint_type>>">
<<print _hint.text>>
</div>
<</if>>
<!-- Empty state -->
<<if _position.completed_nodes.length === 0 && Object.keys($npcs).length === 0>>
<div class="journal-empty">
Your story is just beginning...
</div>
<</if>>
<div class="journal-back-link">
[[Return to your story->Navigation]]
</div>
</div><<nobr>>
<h2>All Stats</h2>
<!-- Player Stats -->
<div class="stats-card stats-card-with-portrait">
<div class="stats-portrait">
<<if $player.portrait>>
<img @src="'./videos/' + $player.portrait" @alt="$player.name" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';">
<<else>>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>
<</if>>
</div>
<div class="stats-info">
<div class="stats-name">You (<<print $player.name>>)</div>
<div class="stats-traits">
<<if $player && $player.core_traits && Object.keys($player.core_traits).length > 0>>
<<for _tk, _tv range $player.core_traits>>
<<if setup.hiddenTraits && setup.hiddenTraits.includes(_tk)>><<continue>><</if>>
<div class="stats-trait-item">
<span><<print _tk>></span>
<span class="stats-trait-value"><<print _tv>></span>
</div>
<</for>>
<<else>>
<em>No traits</em>
<</if>>
</div>
</div>
</div>
<!-- NPC Stats -->
<<if Object.keys($npcs).length > 0>>
<<for _npcId, _npc range $npcs>>
<<if (setup.hiddenNpcs && setup.hiddenNpcs[_npcId])>><<continue>><</if>>
<div class="stats-card stats-card-with-portrait">
<div class="stats-portrait">
<<if _npc.portrait>>
<img @src="'./videos/' + _npc.portrait" @alt="_npc.name" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';">
<<else>>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>
<</if>>
</div>
<div class="stats-info">
<div class="stats-name"><<print _npc.name>></div>
<div class="stats-traits">
<<if _npc.core_traits && Object.keys(_npc.core_traits).length > 0>>
<<for _tk, _tv range _npc.core_traits>>
<<if setup.hiddenTraits && setup.hiddenTraits.includes(_tk)>><<continue>><</if>>
<div class="stats-trait-item">
<span><<print _tk>></span>
<span class="stats-trait-value"><<print _tv>></span>
</div>
<</for>>
<<else>>
<em>No traits</em>
<</if>>
</div>
</div>
</div>
<</for>>
<<else>>
<p><em>No NPCs found.</em></p>
<</if>>
<</nobr>>
<<link "← Back">><<run setup.smartBack()>><</link>><<nobr>>
<h2>Change Clothes</h2>
<<= setup.renderWardrobePage()>>
<</nobr>>
<div id="wardrobe-warning" style="display:none; color:#ff6b6b; margin:10px 0; padding:8px; border:1px solid #ff6b6b; border-radius:4px;"></div>
<<link "← Back">><<script>>
var dest = State.variables.last_game_passage || "Navigation";
var clothingMsg = setup.checkLocationClothing(dest);
if (clothingMsg) {
var el = document.getElementById('wardrobe-warning');
if (el) { el.innerHTML = clothingMsg; el.style.display = 'block'; }
} else {
setup.smartBack();
}
<</script>><</link>><h2>Not Dressed for This</h2>
<p><<print State.variables._clothing_block_message || "You need to put on more clothes.">></p>
<div class="clothing-block-choices">
<<link "Change clothes">><<script>>
State.variables.last_game_passage = State.variables._clothing_block_destination;
Engine.play("WardrobePage");
<</script>><</link>>
<br>
<<link "Go back">><<script>>
Engine.play(State.variables.last_game_passage || "Navigation");
<</script>><</link>>
</div><<nobr>>
<<set _currentDay to $game_state.time_state.current_day>>
<<set _todayIndex to ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].indexOf(_currentDay)>>
<h2>📅 Weekly Schedule</h2>
<p class="current-time">Today is <<print _currentDay>> · <<print setup.formatTime($game_state.time_state.current_hour, $game_state.time_state.current_minute)>></p>
<<set _npcsWithSchedules to setup.getNpcsWithSchedules()>>
<<if _npcsWithSchedules.length === 0>>
<p><em>No NPCs have schedules defined.</em></p>
<<else>>
<<for _npcInfo range _npcsWithSchedules>>
<<set _npcId to _npcInfo.id>>
<<set _npcName to _npcInfo.name>>
<<set _currentLoc to setup.getNpcLocation(_npcId)>>
<<set _allSchedules to setup.getNpcAllSchedulesSorted(_npcId)>>
<div class="npc-section">
<h3 class="npc-name">
<<print _npcName>>
<<if _currentLoc>>
<<set _locName to (setup.locations[_currentLoc.location] && setup.locations[_currentLoc.location].name) || setup._locNameFromUuid(_currentLoc.location) || _currentLoc.location>>
<span class="now-badge">NOW: <<print _locName>></span>
<</if>>
</h3>
<<if _currentLoc && _currentLoc.activity>>
<div class="current-activity">"<<print _currentLoc.activity>>"</div>
<</if>>
<<if _allSchedules.length === 0>>
<p><em>No schedule entries.</em></p>
<<else>>
<table class="schedule-table">
<thead><tr><th>Time</th><th>Location</th><th>Activity</th><th>Days</th></tr></thead>
<tbody>
<<for _sch range _allSchedules>>
<<set _isCurrent to setup.isCurrentTimeSlot(_sch.start_time, _sch.end_time) && setup._weekdayMatches(_sch.weekdays, _todayIndex)>>
<<set _rowClass to _isCurrent ? "current-slot" : "">>
<<set _schLocName to (setup.locations[_sch.location] && setup.locations[_sch.location].name) || _sch.location>>
<tr class="<<print _rowClass>>">
<td><<if _isCurrent>>▶ <</if>><<print _sch.start_time>>-<<print _sch.end_time || "?">></td>
<td><<print _schLocName>></td>
<td><<print _sch.activity>></td>
<td><<print setup.renderWeekdayBadges(_sch.weekdays, _todayIndex)>></td>
</tr>
<</for>>
</tbody>
</table>
<</if>>
</div>
<</for>>
<</if>>
<<set _soloActivities to setup.getSoloActivitiesAllSchedules()>>
<<if _soloActivities.length > 0>>
<div class="npc-section solo-activities-section">
<h3 class="npc-name">Your Activities</h3>
<table class="schedule-table">
<thead><tr><th>Time</th><th>Location</th><th>Activity</th><th>Days</th></tr></thead>
<tbody>
<<for _act range _soloActivities>>
<<set _rowClass to _act.isCurrent ? "current-slot" : "">>
<tr class="<<print _rowClass>>">
<td><<if _act.isCurrent>>▶ <</if>><<print _act.startTime>>-<<print _act.endTime || "?">></td>
<td><<print _act.locationName>></td>
<td><<print _act.name>></td>
<td><<print setup.renderWeekdayBadges(_act.weekdays, _todayIndex)>></td>
</tr>
<</for>>
</tbody>
</table>
</div>
<</if>>
<<link "← Back">><<run setup.smartBack()>><</link>>
<</nobr>>
<style>
.current-time {
color: var(--theme-text-strong);
font-size: 0.9em;
margin: 0 0 10px 0;
}
.now-badge {
background: var(--theme-success);
color: white;
padding: 3px 8px;
border-radius: 10px;
font-size: 0.75em;
font-weight: bold;
margin-left: 10px;
vertical-align: middle;
}
.current-activity {
color: var(--theme-text-muted);
font-style: italic;
margin-bottom: 8px;
font-size: 0.85em;
}
.schedule-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85em;
color: var(--theme-text);
}
.schedule-table th {
text-align: left;
padding: 4px 8px;
background: var(--theme-surface);
border-bottom: 2px solid var(--theme-border);
color: var(--theme-text-strong);
font-weight: 600;
}
.schedule-table td {
padding: 4px 8px;
border-bottom: 1px solid var(--theme-border);
color: var(--theme-text-strong);
vertical-align: middle;
}
.schedule-table tr.current-slot {
background: var(--theme-warning-bg);
font-weight: 500;
}
.schedule-table tr.current-slot td:first-child {
color: var(--theme-warning-text);
}
.solo-activities-section {
margin-top: 18px;
}
.weekday-badges {
display: inline-flex;
flex-wrap: wrap;
gap: 3px;
}
.weekday-badge {
display: inline-block;
padding: 1px 6px;
border-radius: 8px;
font-size: 0.72em;
font-weight: 600;
background: var(--theme-surface);
color: var(--theme-text-muted);
border: 1px solid var(--theme-border);
}
.weekday-badge.weekday-today {
background: var(--theme-primary);
color: white;
border-color: var(--theme-primary);
}
.weekday-badge.weekday-all {
background: var(--theme-surface-alt);
color: var(--theme-text);
border-style: dashed;
}
</style><<nobr>>
<h2>🚩 Game Flags</h2>
<div class="flags-container">
<<if $flags && Object.keys($flags).length > 0>>
<div class="flags-section">
<h3>Story Flags</h3>
<<set _gkeys to Object.keys($flags).sort()>>
<ul class="flags-list">
<<for _i to 0; _i lt _gkeys.length; _i++>>
<<set _gk to _gkeys[_i]>>
<li class="flag-item">
<span class="flag-name"><<print _gk>></span>
<span class="flag-value <<print $flags[_gk] ? 'flag-true' : 'flag-false'>>"><<print $flags[_gk] ? '✔' : '✖'>></span>
</li>
<</for>>
</ul>
</div>
<<else>>
<div class="no-flags">No story flags set.</div>
<</if>>
</div>
<<link "← Back">><<run setup.smartBack()>><</link>>
<</nobr>>
<style>
.flags-container {
background: var(--theme-surface);
border: 1px solid var(--theme-border);
border-radius: 8px;
padding: 16px;
margin: 16px 0;
}
.flags-section {
margin-bottom: 20px;
}
.flags-section:last-child {
margin-bottom: 0;
}
.flags-section h3 {
color: var(--theme-text-secondary);
border-bottom: 2px solid var(--theme-border);
padding-bottom: 8px;
margin: 0 0 12px 0;
}
.flags-list {
list-style: none;
padding: 0;
margin: 0;
}
.flag-item {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--theme-surface-alt);
}
.flag-item:last-child {
border-bottom: none;
}
.flag-name {
font-weight: 500;
color: var(--theme-text);
}
.flag-value {
font-family: var(--theme-font-mono);
}
.flag-true {
color: var(--theme-text-strong);
}
.flag-false {
color: var(--theme-text-strong);
}
.no-flags {
color: var(--theme-text-muted);
font-style: italic;
padding: 12px;
background: #fff;
border-radius: 4px;
}
</style><img src="./videos/scenes/opening_office.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Four of them stand in a line on the black floor of Mercer's office. Vega, Lyra, Nova — chrome-perfect, hands at their sides, nothing moving behind their eyes. And Wren, at the end, the one that came back wrong.</p><span id="cascade-336b2a3f-9609-43ce-8e5f-8d77c25f0482-beat-0"><p>Mercer doesn't look up from the desk. He lets the quiet run until it has weight; he has all night, and the units were built to stand here as long as he wants them to.</p><span id="cascade-336b2a3f-9609-43ce-8e5f-8d77c25f0482-beat-1"><<linkreplace "Hear it.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> You had him. Cain. In the open, in your sights — and you stood there and let him walk.</div></div><p>She had. Her hand had been on the trigger and her hand had not closed, and there is no file for why. She has run the diagnostic since. It comes back clean, and clean is the wrong answer.</p><span id="cascade-336b2a3f-9609-43ce-8e5f-8d77c25f0482-beat-2"><<linkreplace "Take it.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> An asset that won't fire is scrap. I could have you stripped for parts by morning and the Chairman wouldn't blink.</div></div><p>The units don't move. They've watched him scrap things before.</p><<link "Beg him to keep you." "StartingCanvas_canvas_opening_office_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Say nothing." "StartingCanvas_canvas_opening_office_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Tell him, flatly, that you froze." "StartingCanvas_canvas_opening_office_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "6a14a201-46f3-412d-99c3-9deabb0b66e3">>
<<set $game_state.current_node = "9150332c-fa39-4421-9b0d-067e80fcfad0">>
<<script>>setup.markCanvasTriggered("6a14a201-46f3-412d-99c3-9deabb0b66e3");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_office.office") === -1) { vn.push("canvas_opening_office.office"); }<</script>><img src="./videos/scenes/opening_punishment.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Whatever she gives him — words, silence, the truth — it changes nothing; he decided before her mouth opened. He sets the tablet down.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> No. You're the Chairman's investment, and you still feel it — which the others don't. That makes you worth more broken back in than thrown out.</div></div><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> So we remind you what you're for. Here. In front of them.</div></div><p>He stands and comes around the desk, unhurried. The units' eyes follow him and not her.</p><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-1"><<linkreplace "...">><p>He bends her over the desk with a hand on the back of her neck, the way he'd reposition a tool. He works her skirt up, tests her with two fingers, finds her wet — she is always wet, it's the one thing they left switched on — and makes a small satisfied sound, like a reading came back in range.</p><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-2"><<linkreplace "Feel it.">><p>Then his cock is in her and he fucks her in long flat strokes, no hurry, talking over her head to the room about maintenance schedules while he does it. Her body answers him — it clenches and slicks and pushes back, it does everything a wanting body does — while the rest of her stands somewhere behind her own eyes, watching the grain of the desk.</p><video src="./videos/sex/punishment_fuck_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-3"><<linkreplace "Look at them.">><p>Vega, Lyra, Nova watch with the mild attention of machines logging a procedure. Nothing in their faces. They were built without the part that makes this mean anything; she has it, and that is the whole difference between them — and bent over his desk with him grunting behind her, the part that feels does not feel like a gift.</p><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-4"><<linkreplace "...">><p>He comes inside her without ceremony, holds, pulls out. Cum slides down the inside of her thigh. He's already turned away, wiping his hand on a cloth from the drawer, looking at the units instead of her.</p><video src="./videos/sex/punishment_finish_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-5"><<linkreplace "Thank him.">><p>She straightens her skirt. The words come level, because they're true the only way she has truths.</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="You" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>You:</strong> Thank you, sir.</div></div><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-6"><<linkreplace "The next job.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Vega, Lyra, Nova run the hunt head-on. You're useless with a gun — but you're the one asset I have that a man wants to keep close.</div></div><p>A face comes up on the wall screen. A man gone to seed, mid-forties, a hard mouth. The name under it: RENNER.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> He supplied the gear for the facility Cain burned. He knows where the trail starts. Get inside his life and find it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> And understand what you're bringing home. Cain murdered the Chairman's wife. He's an enemy of this company and everything it keeps standing. Put him in a box and you've earned your place for good.</div></div><p>She understands the leash without being told it: everything she does out there feeds back to the Tower, and an asset that slips — that lets the malfunction show, that fails to bring the work home — gets pulled in and looked at. She does not intend to be looked at.</p><span id="cascade-936d31a0-6c95-43a1-bb4b-fe987dcce7a7-beat-7"><<linkreplace "Dismissed.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Now get out of my sight.</div></div><p>She does as she's told. The door closes on the three of them still standing in their line. In the corridor, under the quiet, something she has no name for is leaking — a pressure behind the eyes, a wrongness with no readout — and she logs it as fatigue and walks toward the cradle.</p>[[Power down.->Location_The_Cradle]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "6a14a201-46f3-412d-99c3-9deabb0b66e3">>
<<set $game_state.current_node = "7af3dacb-b6ea-49da-b84d-ce6f0216d165">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_office.punishment") === -1) { vn.push("canvas_opening_office.punishment"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "office_done", "set");<</script>><h2>Navigation</h2>
<p>Where would you like to go?</p>
<div class="location-list">
[[Spire Plaza->Location_Spire_Plaza]]<br>
[[The Atrium->Location_The_Atrium]]<br>
[[Mercer's Penthouse->Location_Mercer's_Penthouse]]<br>
[[Wren's Floor->Location_Wren's_Floor]]<br>
[[Wren's Room->Location_Wren's_Room]]<br>
[[The Cradle->Location_The_Cradle]]<br>
[[The Waterfront->Location_The_Waterfront]]<br>
[[The Anchor->Location_The_Anchor]]<br>
[[Renner's Depot->Location_Renner's_Depot]]<br>
[[The Facility->Location_The_Facility]]<br>
[[The Site->Location_The_Site]]<br>
[[The Burned Yard->Location_The_Burned_Yard]]<br>
[[The Underworld Gate->Location_The_Underworld_Gate]]<br>
[[The Underground->Location_The_Underground]]<br>
[[The House->Location_The_House]]<br>
[[The Pit->Location_The_Pit]]<br>
[[The Black Market->Location_The_Black_Market]]<br>
</div><<run
var msg = State.variables._broken_exit_msg || 'Unresolved exit reference (no message captured)';
console.error('[BROKEN EXIT]', msg);
throw new Error('Broken exit: ' + msg);
<</run>>
<!-- LOCATION PASSAGES --><<nobr>>
<<set $player.current_location = "e2102d61-5ee1-44d7-9b7e-ca47369f2c0f">>
<<if not $game_state.visited_locations.includes("e2102d61-5ee1-44d7-9b7e-ca47369f2c0f")>>
<<set $game_state.visited_locations.push("e2102d61-5ee1-44d7-9b7e-ca47369f2c0f")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("e2102d61-5ee1-44d7-9b7e-ca47369f2c0f")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Spire Plaza</h2>
<p>The chrome public level at the foot of Vance Tower. Suits cross it without looking up. Cameras on every column. THE SPIRE — the corporate core, clean and lit and watched. The company car waits at the curb to run her down to the Reach.</p>
<<= setup.renderNpcPortraits("e2102d61-5ee1-44d7-9b7e-ca47369f2c0f")>>
<<= setup.renderSoloActivities("e2102d61-5ee1-44d7-9b7e-ca47369f2c0f")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("atrium")>><a class="location-card link-internal" data-passage="Location_The_Atrium"><div class="location-card-image" style="background-image: url('./videos/locations/atrium.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Atrium</span><<if setup.getLocationCostTag("atrium")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("atrium")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("41db0e87-fb1b-4520-87bd-0cd30ef3df91")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("41db0e87-fb1b-4520-87bd-0cd30ef3df91")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/atrium.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Atrium</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("atrium")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<nobr>>
<<set $player.current_location = "41db0e87-fb1b-4520-87bd-0cd30ef3df91">>
<<if not $game_state.visited_locations.includes("41db0e87-fb1b-4520-87bd-0cd30ef3df91")>>
<<set $game_state.visited_locations.push("41db0e87-fb1b-4520-87bd-0cd30ef3df91")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("41db0e87-fb1b-4520-87bd-0cd30ef3df91")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Atrium</h2>
<p>The Tower's inner lobby. Polished stone, a security desk that knows her face, elevators that only stop where her clearance says they can. The threshold between the cage and the street.</p>
<<= setup.renderNpcPortraits("41db0e87-fb1b-4520-87bd-0cd30ef3df91")>>
<<= setup.renderSoloActivities("41db0e87-fb1b-4520-87bd-0cd30ef3df91")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("penthouse")>><a class="location-card link-internal" data-passage="Location_Mercer's_Penthouse"><div class="location-card-image" style="background-image: url('./videos/locations/penthouse.jpg')"></div><div class="location-card-content"><span class="location-card-name">Mercer's Penthouse</span><<if setup.getLocationCostTag("penthouse")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("penthouse")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("8d46d412-5821-4d5e-afcf-9e33d70f4670")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("8d46d412-5821-4d5e-afcf-9e33d70f4670")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/penthouse.jpg')"></div><div class="location-card-content"><span class="location-card-name">Mercer's Penthouse</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("penthouse")>></span></div></div><</if>><<if setup.navDestUnlocked("wren_floor")>><a class="location-card link-internal" data-passage="Location_Wren's_Floor"><div class="location-card-image" style="background-image: url('./videos/locations/wren_floor.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Floor</span><<if setup.getLocationCostTag("wren_floor")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("wren_floor")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("cc0ab271-2067-40a0-9003-0b609b44fa21")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("cc0ab271-2067-40a0-9003-0b609b44fa21")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/wren_floor.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Floor</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("wren_floor")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave The Atrium->Location_Spire_Plaza]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "8d46d412-5821-4d5e-afcf-9e33d70f4670">>
<<if not $game_state.visited_locations.includes("8d46d412-5821-4d5e-afcf-9e33d70f4670")>>
<<set $game_state.visited_locations.push("8d46d412-5821-4d5e-afcf-9e33d70f4670")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("8d46d412-5821-4d5e-afcf-9e33d70f4670")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Mercer's Penthouse</h2>
<p>The top floor. Glass on three sides, the city laid out below like something he owns. This is where she's summoned, and where she serves.</p>
<<= setup.renderNpcPortraits("8d46d412-5821-4d5e-afcf-9e33d70f4670")>>
<<= setup.renderSoloActivities("8d46d412-5821-4d5e-afcf-9e33d70f4670")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave Mercer's Penthouse->Location_The_Atrium]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "cc0ab271-2067-40a0-9003-0b609b44fa21">>
<<if not $game_state.visited_locations.includes("cc0ab271-2067-40a0-9003-0b609b44fa21")>>
<<set $game_state.visited_locations.push("cc0ab271-2067-40a0-9003-0b609b44fa21")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("cc0ab271-2067-40a0-9003-0b609b44fa21")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Wren's Floor</h2>
<p>The asset level — narrow corridors, numbered doors, no windows. Quiet the way a kept place is quiet.</p>
<<= setup.renderNpcPortraits("cc0ab271-2067-40a0-9003-0b609b44fa21")>>
<<= setup.renderSoloActivities("cc0ab271-2067-40a0-9003-0b609b44fa21")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("wren_room")>><a class="location-card link-internal" data-passage="Location_Wren's_Room"><div class="location-card-image" style="background-image: url('./videos/locations/wren_room.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Room</span><<if setup.getLocationCostTag("wren_room")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("wren_room")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("3fd9bb60-090c-47e5-96c2-702c4ed171f4")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("3fd9bb60-090c-47e5-96c2-702c4ed171f4")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/wren_room.jpg')"></div><div class="location-card-content"><span class="location-card-name">Wren's Room</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("wren_room")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave Wren's Floor->Location_The_Atrium]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "3fd9bb60-090c-47e5-96c2-702c4ed171f4">>
<<if not $game_state.visited_locations.includes("3fd9bb60-090c-47e5-96c2-702c4ed171f4")>>
<<set $game_state.visited_locations.push("3fd9bb60-090c-47e5-96c2-702c4ed171f4")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("3fd9bb60-090c-47e5-96c2-702c4ed171f4")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Wren's Room</h2>
<p>Hers, in the way a tool's drawer is the tool's. A bed she rarely uses, a charging cradle in the corner, nothing on the walls. Clean, bare, company-issue.</p>
[[Change Clothes->WardrobePage]]<br>
<<= setup.renderNpcPortraits("3fd9bb60-090c-47e5-96c2-702c4ed171f4")>>
<<= setup.renderSoloActivities("3fd9bb60-090c-47e5-96c2-702c4ed171f4")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("cradle")>><a class="location-card link-internal" data-passage="Location_The_Cradle"><div class="location-card-image" style="background-image: url('./videos/locations/cradle.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Cradle</span><<if setup.getLocationCostTag("cradle")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("cradle")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("95b618a2-e0ce-42a6-9398-18ed9e013321")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("95b618a2-e0ce-42a6-9398-18ed9e013321")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/cradle.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Cradle</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("cradle")>></span></div></div><</if>></div><</nobr>>
<div class="location-nav-exits">
[[Leave Wren's Room->Location_Wren's_Floor]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "95b618a2-e0ce-42a6-9398-18ed9e013321">>
<<if not $game_state.visited_locations.includes("95b618a2-e0ce-42a6-9398-18ed9e013321")>>
<<set $game_state.visited_locations.push("95b618a2-e0ce-42a6-9398-18ed9e013321")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("95b618a2-e0ce-42a6-9398-18ed9e013321")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Cradle</h2>
<p>The charging cradle. She powers down here and wakes here — the reset, the start of every day. When something leaks through the quiet, it leaks here.</p>
<<= setup.renderNpcPortraits("95b618a2-e0ce-42a6-9398-18ed9e013321")>>
<<= setup.renderSoloActivities("95b618a2-e0ce-42a6-9398-18ed9e013321")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Cradle->Location_Wren's_Room]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "7ab13bfe-967a-4b94-9b00-a50679889c8f">>
<<if not $game_state.visited_locations.includes("7ab13bfe-967a-4b94-9b00-a50679889c8f")>>
<<set $game_state.visited_locations.push("7ab13bfe-967a-4b94-9b00-a50679889c8f")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("7ab13bfe-967a-4b94-9b00-a50679889c8f")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Waterfront</h2>
<p>The docks strip — wet concrete, sodium lights, smuggler traffic that nobody logs. THE REACH — the city's underside, grit and salt and the parts the Spire pretends it doesn't use. The car back up to the Spire waits at the curb.</p>
<<= setup.renderNpcPortraits("7ab13bfe-967a-4b94-9b00-a50679889c8f")>>
<<= setup.renderSoloActivities("7ab13bfe-967a-4b94-9b00-a50679889c8f")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("the_anchor")>><a class="location-card link-internal" data-passage="Location_The_Anchor"><div class="location-card-image" style="background-image: url('./videos/locations/the_anchor.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Anchor</span><<if setup.getLocationCostTag("the_anchor")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("the_anchor")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("cd98b542-17f8-427c-8c06-28fe48bea6b7")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("cd98b542-17f8-427c-8c06-28fe48bea6b7")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/the_anchor.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Anchor</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("the_anchor")>></span></div></div><</if>><<if setup.navDestUnlocked("renner_depot")>><a class="location-card link-internal" data-passage="Location_Renner's_Depot"><div class="location-card-image" style="background-image: url('./videos/locations/renner_depot.jpg')"></div><div class="location-card-content"><span class="location-card-name">Renner's Depot</span><<if setup.getLocationCostTag("renner_depot")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("renner_depot")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("8f7e3c9a-db1e-4433-af9e-0fad4e568e02")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("8f7e3c9a-db1e-4433-af9e-0fad4e568e02")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/renner_depot.jpg')"></div><div class="location-card-content"><span class="location-card-name">Renner's Depot</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("renner_depot")>></span></div></div><</if>><<if setup.navDestUnlocked("facility_ruins")>><a class="location-card link-internal" data-passage="Location_The_Facility"><div class="location-card-image" style="background-image: url('./videos/locations/facility_ruins.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Facility</span><<if setup.getLocationCostTag("facility_ruins")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("facility_ruins")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("1fbd93c5-765d-47b8-9cfd-5be6d40d813f")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("1fbd93c5-765d-47b8-9cfd-5be6d40d813f")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/facility_ruins.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Facility</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("facility_ruins")>></span></div></div><</if>><<if setup.navDestUnlocked("renner_burned_yard")>><a class="location-card link-internal" data-passage="Location_The_Burned_Yard"><div class="location-card-image" style="background-image: url('./videos/locations/renner_burned_yard.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Burned Yard</span><<if setup.getLocationCostTag("renner_burned_yard")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("renner_burned_yard")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/renner_burned_yard.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Burned Yard</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("renner_burned_yard")>></span></div></div><</if>><<if setup.navDestUnlocked("underworld_gate")>><a class="location-card link-internal" data-passage="Location_The_Underworld_Gate"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_gate.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Underworld Gate</span><<if setup.getLocationCostTag("underworld_gate")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_gate")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("46350ea6-06ba-4c39-8c3b-11fc1812ca51")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("46350ea6-06ba-4c39-8c3b-11fc1812ca51")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_gate.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Underworld Gate</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_gate")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<nobr>>
<<set $player.current_location = "cd98b542-17f8-427c-8c06-28fe48bea6b7">>
<<if not $game_state.visited_locations.includes("cd98b542-17f8-427c-8c06-28fe48bea6b7")>>
<<set $game_state.visited_locations.push("cd98b542-17f8-427c-8c06-28fe48bea6b7")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("cd98b542-17f8-427c-8c06-28fe48bea6b7")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Anchor</h2>
<p>A dive bar two streets off the water. Dark wood, dead neon, a bartender who's seen everyone's worst night. Renner drowns his here, evenings, every evening.</p>
<<= setup.renderNpcPortraits("cd98b542-17f8-427c-8c06-28fe48bea6b7")>>
<<= setup.renderSoloActivities("cd98b542-17f8-427c-8c06-28fe48bea6b7")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Anchor->Location_The_Waterfront]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "8f7e3c9a-db1e-4433-af9e-0fad4e568e02">>
<<if not $game_state.visited_locations.includes("8f7e3c9a-db1e-4433-af9e-0fad4e568e02")>>
<<set $game_state.visited_locations.push("8f7e3c9a-db1e-4433-af9e-0fad4e568e02")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("8f7e3c9a-db1e-4433-af9e-0fad4e568e02")>><<if _autoFire>><<goto _autoFire>><<else>><h2>Renner's Depot</h2>
<p>A gutted equipment yard — half-empty racks, crates Cain's people didn't bother to take, a forklift nobody's fixed. He's trying to rebuild it on cheap hands and credit he doesn't have. The work floor's out front; his office is the glassed-in box in back, and you don't go in there until he wants you to.</p>
<<= setup.renderNpcPortraits("8f7e3c9a-db1e-4433-af9e-0fad4e568e02")>>
<<= setup.renderSoloActivities("8f7e3c9a-db1e-4433-af9e-0fad4e568e02")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave Renner's Depot->Location_The_Waterfront]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "1fbd93c5-765d-47b8-9cfd-5be6d40d813f">>
<<if not $game_state.visited_locations.includes("1fbd93c5-765d-47b8-9cfd-5be6d40d813f")>>
<<set $game_state.visited_locations.push("1fbd93c5-765d-47b8-9cfd-5be6d40d813f")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("1fbd93c5-765d-47b8-9cfd-5be6d40d813f")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Facility</h2>
<p>What's left of the Vance asset-facility Cain burned to the ground — scorched frame, melted server racks, the smell of old smoke that never washed out. The first dread-place. Something in the wreckage is going to look like it belongs to her.</p>
<<= setup.renderNpcPortraits("1fbd93c5-765d-47b8-9cfd-5be6d40d813f")>>
<<= setup.renderSoloActivities("1fbd93c5-765d-47b8-9cfd-5be6d40d813f")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Facility->Location_The_Waterfront]]<br>
</div>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "the_site_open", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "46175232-2ebd-4349-accb-a67d1ccf04d7">>
<<if not $game_state.visited_locations.includes("46175232-2ebd-4349-accb-a67d1ccf04d7")>>
<<set $game_state.visited_locations.push("46175232-2ebd-4349-accb-a67d1ccf04d7")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("46175232-2ebd-4349-accb-a67d1ccf04d7")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Site</h2>
<p>Somewhere out past the container stacks — the place Cain just walked away from. She doesn't have it yet.</p>
<<= setup.renderNpcPortraits("46175232-2ebd-4349-accb-a67d1ccf04d7")>>
<<= setup.renderSoloActivities("46175232-2ebd-4349-accb-a67d1ccf04d7")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Site->Location_The_Underground]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Site</h2>
<p class="entry-blocked-narrative">Past the strip the city goes dark and deep, and that's where he is. The way down isn't open to her yet.</p>
[[Go back->Location_The_Underground]]
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_hired", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee">>
<<if not $game_state.visited_locations.includes("f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee")>>
<<set $game_state.visited_locations.push("f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Burned Yard</h2>
<p>Renner's old yard, three streets past the depot — what Cain left of it. Torched to the slab, fence down, but his men still walk it with lights, turning back anyone who comes to pick the bones. Whatever he's keeping out here, he keeps it watched.</p>
<<= setup.renderNpcPortraits("f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee")>>
<<= setup.renderSoloActivities("f7aeeb05-d0d4-442c-bf2d-ba4dd1dc10ee")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Burned Yard->Location_The_Waterfront]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Burned Yard</h2>
<p class="entry-blocked-narrative">You've no reason to be out at his burned yard — not yet. Get inside his world first.</p>
[[Go back->Location_The_Waterfront]]
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "46350ea6-06ba-4c39-8c3b-11fc1812ca51">>
<<if not $game_state.visited_locations.includes("46350ea6-06ba-4c39-8c3b-11fc1812ca51")>>
<<set $game_state.visited_locations.push("46350ea6-06ba-4c39-8c3b-11fc1812ca51")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("46350ea6-06ba-4c39-8c3b-11fc1812ca51")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Underworld Gate</h2>
<p>A freight door under the docks that isn't on any manifest, and a man in front of it who is. Past him the city goes down instead of up — the part of the Reach the Spire pretends doesn't exist. He doesn't move for free.</p>
<<= setup.renderNpcPortraits("46350ea6-06ba-4c39-8c3b-11fc1812ca51")>>
<<= setup.renderSoloActivities("46350ea6-06ba-4c39-8c3b-11fc1812ca51")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Underworld Gate->Location_The_Waterfront]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Underworld Gate</h2>
<p class="entry-blocked">You can't go here right now.</p>
<p class="entry-requirements"><<print setup.formatCanvasConditions({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})>></p>
[[Go back->Location_The_Waterfront]]
<</if>><<nobr>>
<<set $player.current_location = "b898c096-28a2-4d4b-91ea-d71b1774a140">>
<<if not $game_state.visited_locations.includes("b898c096-28a2-4d4b-91ea-d71b1774a140")>>
<<set $game_state.visited_locations.push("b898c096-28a2-4d4b-91ea-d71b1774a140")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("b898c096-28a2-4d4b-91ea-d71b1774a140")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Underground</h2>
<p>Down past the gate the city turns inside out — wet brick, red light, stalls selling what the Spire outlaws and bodies selling what it pretends to. Nobody here knows what she is, and nobody here owns her. For the length of a visit she's just another face with coin to make. The way back up is the gate.</p>
<<= setup.renderNpcPortraits("b898c096-28a2-4d4b-91ea-d71b1774a140")>>
<<= setup.renderSoloActivities("b898c096-28a2-4d4b-91ea-d71b1774a140")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("underworld_brothel")>><a class="location-card link-internal" data-passage="Location_The_House"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_brothel.jpg')"></div><div class="location-card-content"><span class="location-card-name">The House</span><<if setup.getLocationCostTag("underworld_brothel")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_brothel")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("2c90e82f-e62c-4fc7-b7c7-f34085e86b5b")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("2c90e82f-e62c-4fc7-b7c7-f34085e86b5b")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_brothel.jpg')"></div><div class="location-card-content"><span class="location-card-name">The House</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_brothel")>></span></div></div><</if>><<if setup.navDestUnlocked("underworld_pit")>><a class="location-card link-internal" data-passage="Location_The_Pit"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_pit.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Pit</span><<if setup.getLocationCostTag("underworld_pit")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_pit")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("805251ba-3c65-4131-8496-c00ace391664")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("805251ba-3c65-4131-8496-c00ace391664")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_pit.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Pit</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_pit")>></span></div></div><</if>><<if setup.navDestUnlocked("underworld_market")>><a class="location-card link-internal" data-passage="Location_The_Black_Market"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_market.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Black Market</span><<if setup.getLocationCostTag("underworld_market")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_market")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("82e2de25-1f87-4b36-9b73-7afc08b6b382")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("82e2de25-1f87-4b36-9b73-7afc08b6b382")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_market.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Black Market</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_market")>></span></div></div><</if>><<if setup.navDestUnlocked("the_site")>><a class="location-card link-internal" data-passage="Location_The_Site"><div class="location-card-image" style="background-image: url('./videos/locations/the_site.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Site</span><<if setup.getLocationCostTag("the_site")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("the_site")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("46175232-2ebd-4349-accb-a67d1ccf04d7")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("46175232-2ebd-4349-accb-a67d1ccf04d7")>><<if _npc.portrait>><img @src="'./videos/' + _npc.portrait" class="nav-npc-badge" @alt="_npc.name"><</if>><</for>></div></div></a><<else>><div class="location-card location-card-locked"><div class="location-card-image" style="background-image: url('./videos/locations/the_site.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Site</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("the_site")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<nobr>>
<<set $player.current_location = "2c90e82f-e62c-4fc7-b7c7-f34085e86b5b">>
<<if not $game_state.visited_locations.includes("2c90e82f-e62c-4fc7-b7c7-f34085e86b5b")>>
<<set $game_state.visited_locations.push("2c90e82f-e62c-4fc7-b7c7-f34085e86b5b")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("2c90e82f-e62c-4fc7-b7c7-f34085e86b5b")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The House</h2>
<p>A working house off the strip — rooms by the hour, a madam who counts heads and coin. They'll take her on a shift; down here the thing the company uses her for is just a trade, and the coin is hers.</p>
<<= setup.renderNpcPortraits("2c90e82f-e62c-4fc7-b7c7-f34085e86b5b")>>
<<= setup.renderSoloActivities("2c90e82f-e62c-4fc7-b7c7-f34085e86b5b")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The House->Location_The_Underground]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "805251ba-3c65-4131-8496-c00ace391664">>
<<if not $game_state.visited_locations.includes("805251ba-3c65-4131-8496-c00ace391664")>>
<<set $game_state.visited_locations.push("805251ba-3c65-4131-8496-c00ace391664")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("805251ba-3c65-4131-8496-c00ace391664")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Pit</h2>
<p>A sunken ring under a warehouse, a crowd around it laying bets. They'll match her against whoever's standing. Win and the purse is coin; lose and you crawl out. Down here her fists earn the same as her body.</p>
<<= setup.renderNpcPortraits("805251ba-3c65-4131-8496-c00ace391664")>>
<<= setup.renderSoloActivities("805251ba-3c65-4131-8496-c00ace391664")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Pit->Location_The_Underground]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "82e2de25-1f87-4b36-9b73-7afc08b6b382">>
<<if not $game_state.visited_locations.includes("82e2de25-1f87-4b36-9b73-7afc08b6b382")>>
<<set $game_state.visited_locations.push("82e2de25-1f87-4b36-9b73-7afc08b6b382")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("82e2de25-1f87-4b36-9b73-7afc08b6b382")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Black Market</h2>
<p>Stalls and back-rooms where coin buys what's off every shelf above — weapons, gear, and people, penned at the far end like the rest of the stock. She knows the look of the merchandise better than she'd like.</p>
<<= setup.renderNpcPortraits("82e2de25-1f87-4b36-9b73-7afc08b6b382")>>
<<= setup.renderSoloActivities("82e2de25-1f87-4b36-9b73-7afc08b6b382")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Black Market->Location_The_Underground]]<br>
</div>
</div>
<</if>>
<!-- STORY CANVAS PASSAGES --><p>She slides onto the stool beside him and it's wrong before she's said a word. Out of the coveralls, in her own clean clothes, she's not the dock-rat who hauls his crates — she's a stranger sitting too close, and his bleary eyes narrow on her with the old wariness coming up through the drink.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...do I know you? You don't look like you tonight.</div></div><p>Even three drinks down he knows the shape of her is off. Nothing she wants from him happens while she's wearing the wrong face. The cover's back in her quarters, where it does her no good.</p>
<<set $game_state.current_canvas = "9cb1e891-ee3d-403c-9d2a-b13e90cfc98b">>
<<set $game_state.current_node = "e5a16dea-fd09-4926-b229-c28bf6b15f6e">>
<<script>>setup.markCanvasTriggered("9cb1e891-ee3d-403c-9d2a-b13e90cfc98b");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_renner_nocover_anchor.base") === -1) { vn.push("react_renner_nocover_anchor.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave before he sobers on it.->Location_The_Anchor]]<p>She comes onto the floor and Renner's head comes up wrong. The coveralls aren't on her — she's in her own clothes, the clean expensive line of them all out of place against the racks — and for a second he doesn't place her at all. Then the wariness: not recognition. Suspicion.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...the hell are you dressed like that for. Do I know you?</div></div><p>Wrong skin for this yard. Whatever she'd get done here today, she won't get it done looking like this. The cover's back in her quarters; it does her no good on the rack.</p>
<<set $game_state.current_canvas = "923a100e-c584-4a47-8ff5-32106d33366f">>
<<set $game_state.current_node = "c4ac6140-fa6f-433d-b61b-619100148537">>
<<script>>setup.markCanvasTriggered("923a100e-c584-4a47-8ff5-32106d33366f");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_renner_nocover_depot.base") === -1) { vn.push("react_renner_nocover_depot.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Get out before he looks harder.->Location_Renner's_Depot]]<img src="./videos/scenes/renner_anchor.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Renner's at the end of the bar. She takes the stool and starts in — heard he's rebuilding, looking for work — and watches it land wrong. She's too clean. Too put-together for a woman who'd want a dock-rat's wages. He looks at her the way a man with enemies looks at a stranger who knows his business.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Whatever you are — cop, fed, one of theirs — I've got nothing to say to it. Off my stool.</div></div><p>He turns his shoulder and that's the end of it. Dressed like this she's not cheap hands; she's a question, and a man this far down doesn't answer questions. She'd have to come at him as something that doesn't get a second look — the cover's in her quarters, and it does her no good on the rack.</p>
<<set $game_state.current_canvas = "d53bed88-d5e0-4623-9015-83eb859bef1b">>
<<set $game_state.current_node = "52694484-ddae-4297-900a-42f57aae8e03">>
<<script>>setup.markCanvasTriggered("d53bed88-d5e0-4623-9015-83eb859bef1b");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("react_renner_threat.base") === -1) { vn.push("react_renner_threat.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave it.->Location_The_Anchor]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_anal_once", "operator": "is_true"}]})>>
<video src="./videos/sex/renner_cheerup_alley_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He's wrecked tonight, and she does the thing that works. She gets him out of the bar and into the dark behind it and gives him whatever he needs — on her knees on the filthy concrete, or bent over a crate, the cold boss falling apart and the help putting him back together with her mouth and her ass. It's the one real thing she has, and she spends it on him like it's nothing, because it is.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_oral_once", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "renner_anal_once", "operator": "is_false"}]})>>
<video src="./videos/sex/renner_cheerup_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>He's low tonight. She slips off the stool and into the dark beside the cigarette machine and takes his cock in her mouth right there, fast and devoted, the bartender pointedly not looking. He comes with his hand in her hair saying her work-name like a man saying his prayers wrong.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_oral_once", "operator": "is_false"}]})>>
<p>He's low tonight — too low to do anything but sit in it. She sits closer than she needs to, lets her thigh press his, lets her hand rest high on his leg where the wanting has somewhere to go. It's not much. But it makes the worst of the night about her instead of about the wreck of his life, and that's the seed of everything that comes after.</p>
<</if>>
<<set $game_state.current_canvas = "15562de0-4dfd-4f3f-a108-f7bb8d6b0ba0">>
<<set $game_state.current_node = "ab23d5a5-ab1a-46ed-8bde-1afd6906929b">>
<<script>>setup.markCanvasTriggered("15562de0-4dfd-4f3f-a108-f7bb8d6b0ba0");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("amb_renner_cheerup.base") === -1) { vn.push("amb_renner_cheerup.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave him steadier.->Location_The_Anchor]]<img src="./videos/scenes/amb_renner_anchor_ruin.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 20}]})>>
<p>Two stools down he's telling the bartender a story about the depot before Cain — when it was something, when he was something. The bartender's heard it. So has she. Renner doesn't notice either of them stopped listening a while ago.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 40}]})>>
<p>There's a letter on the bar in front of him tonight he keeps not reading — blacklist stationery, the third this month. He drinks around it. When he catches her looking he turns it face-down, too late, and orders another.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 40}]})>>
<p>He's further gone tonight than she's seen him — talking too much, too loud, the rot all the way to the surface. What they made him part of. What burned. What he signed for and told himself he didn't understand. He won't remember saying any of it tomorrow. She'll remember all of it. It's the guilt the seduction never has to touch.</p>
<</if>>
<<set $game_state.current_canvas = "4ec26726-55f0-4cca-a2ff-e686c3d9676a">>
<<set $game_state.current_node = "52c730a0-afc4-48d7-878f-5006dc0705a7">>
<<script>>setup.markCanvasTriggered("4ec26726-55f0-4cca-a2ff-e686c3d9676a");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("amb_renner_anchor_ruin.base") === -1) { vn.push("amb_renner_anchor_ruin.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Let him drink.->Location_The_Anchor]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 20}]})>>
<p>She's halfway up a ladder with a crate when she feels it — Renner, stopped at the bottom, not pretending to check a manifest, just watching her work. He looks away when she looks down. But he was watching, and now they both know he was.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 40}]})>>
<p>He finds reasons to be where she is — stands too close at the racks, hands her tools she didn't ask for, lets the backs of his fingers find her arm. "You're good at this," he says, like it costs him, and doesn't move away when he's said it.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 40}]})>>
<p>He doesn't bother with the pretext anymore. He takes the crate out of her hands, sets it down, and backs her into the shelving with his whole ruined weight against her. "Leave it," he says against her neck. "The invoices can wait. Come in the back."</p>
<</if>>
<<set $game_state.current_canvas = "03e28d2c-c1f4-469f-9c1b-da474ccf3796">>
<<set $game_state.current_node = "46570558-1254-45ca-a766-a88f8da27a1e">>
<<script>>setup.markCanvasTriggered("03e28d2c-c1f4-469f-9c1b-da474ccf3796");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("walkin_renner_depot.base") === -1) { vn.push("walkin_renner_depot.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[...->Location_Renner's_Depot]]<p>He'll answer anything now, and remember none of it. She crouches to his eye level and asks, quiet, the way you'd read a meter.</p><span id="cascade-285d449e-d794-470f-8ff8-90d2f3352fdd-beat-0"><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="You" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>You:</strong> The gear you sold Vance. What did it build?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Containment. Restraint rigs, suppression fields, a wet lab built to hold things that aren't supposed to be holdable. I didn't ask what. They paid on time and I had a business to save.</div></div><span id="cascade-285d449e-d794-470f-8ff8-90d2f3352fdd-beat-1"><<linkreplace "What did Cain take?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="You" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>You:</strong> When Cain hit the facility. What did he take?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Nothing. He freed it. Whatever they were holding in there, he let it out before he burned the place. The manifests after didn't add up — they were short bodies that weren't bodies. He wasn't robbing them. He was emptying a cage.</div></div><p>She files it somewhere the cradle can't reach. The monster she's sent to kill broke into the worst place these people built and let the prisoners walk out. It doesn't fit the briefing. Nothing about him fits the briefing, and the not-fitting is starting to have a shape.</p><span id="cascade-285d449e-d794-470f-8ff8-90d2f3352fdd-beat-2"><<linkreplace "Why did Cain burn you down?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="You" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>You:</strong> Cain didn't just walk away from you. He came for you. Why?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Two of my people. They moved my materials — and they were moving them to him too, feeding Cain off my own floor. When I found out I didn't just cut them loose. I did them, and everyone in their houses, the same night. He burned me to the slab for it. We've been at each other since.</div></div><p>She keeps her face a meter-reader's blank. So the burned yard she's been picking through wasn't collateral in someone else's war — it was the answer, and the two files she pulled out there are the men he's confessing to now. The wreck of him has a shape after all, and the shape is a debt.</p><span id="cascade-285d449e-d794-470f-8ff8-90d2f3352fdd-beat-3"><<linkreplace "Where is he now?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="You" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>You:</strong> Where would he go to ground?</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> I don't know. But those two came up out of the underworld before they ever came to me — that's where their kind runs. If Cain's anywhere, he's down there. That's all I... that's all I have.</div></div><span id="cascade-285d449e-d794-470f-8ff8-90d2f3352fdd-beat-4"><<linkreplace "Tell him to forget.">><p>She tells him to forget, and he will. In a few minutes he'll surface blinking, his cock going soft and no memory of the last ten, certain only that the new hire is very good and he should keep her close. She fixes her clothes while the man who just handed her everything stares at nothing.</p>[[Leave him to come back to himself.->Location_Renner's_Depot]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "79bfc53b-607d-488a-9198-f1f0cb08791f">>
<<set $game_state.current_node = "d47a0b4f-3d97-4e72-84f6-fde298a679d8">>
<<script>>setup.markCanvasTriggered("79bfc53b-607d-488a-9198-f1f0cb08791f");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("renner_control_canvas.intro") === -1) { vn.push("renner_control_canvas.intro"); }<</script>>
<<script>>advanceTime(3);<</script>><p>He goes soft and open the second she's done, the controls warm in her hand again — hers for a few minutes, and he won't remember a second of it. She already has what he knew. There's nothing left in him she needs; there's only what she feels like taking.</p>
<<set $game_state.current_canvas = "79bfc53b-607d-488a-9198-f1f0cb08791f">>
<<set $game_state.current_node = "db13216e-7d38-41d3-a738-8175f78ad748">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("renner_control_canvas.again") === -1) { vn.push("renner_control_canvas.again"); }<</script>>
<<nobr>>
<<link "Ask him again." "Canvas_renner_control_canvas_Node_3">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Just leave him." "Location_Renner's_Depot">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She crouches to his eye level and asks anyway — the gear, the two he buried, where the man went.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Two of mine, feeding him off my floor. I did them and theirs, and he burned me for it. Where he went — down. It's all down in the underworld. That's everything I had. I swear it.</div></div><p>The same confession, in the same order. He doesn't know he's said it before, and he never will. Nothing she didn't already have — but watching the wrecked man empty himself out for her on command is its own kind of confirmation. She fixes her clothes and leaves him to surface alone.</p>
<<set $game_state.current_canvas = "79bfc53b-607d-488a-9198-f1f0cb08791f">>
<<set $game_state.current_node = "e33a2e37-2dbd-4318-a683-126aa200c6ca">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("renner_control_canvas.again_ask") === -1) { vn.push("renner_control_canvas.again_ask"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Leave him to come back to himself.->Location_Renner's_Depot]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He finishes on her face with a groan that's half a sob, one hand cradling the back of her head like she's something he can't believe he's allowed. He'd give her anything right now. He doesn't know yet how much that's going to cost him.</p><video src="./videos/sex/renner_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her, holding her down on him, shaking. For a moment after he just breathes into her shoulder, wrecked and grateful, and she lets him have the moment because letting him have it is how she keeps him.</p><video src="./videos/sex/renner_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass, and the controls slide warm into her hand — the give, the opening, his whole ruined weight gone pliant against her back. The same act that hands her nothing with Mercer hands her everything with him.</p><video src="./videos/sex/renner_finish_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "9ce98153-64e4-42e9-be99-0171204ef665">>
<<set $game_state.current_node = "91abe83b-63bf-47be-9975-9e06cfc03cc3">>
<<script>>setup.markCanvasTriggered("9ce98153-64e4-42e9-be99-0171204ef665");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_finisher.climax") === -1) { vn.push("loop_renner_finisher.climax"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})>>
<span @class="setup.isChoiceVisited('9ce98153-64e4-42e9-be99-0171204ef665:cc0') ? '' : 'unlocked-choice'">
<<link "Straighten up." "Location_Renner's_Depot">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("9ce98153-64e4-42e9-be99-0171204ef665:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "lt", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('9ce98153-64e4-42e9-be99-0171204ef665:cc1') ? '' : 'unlocked-choice'">
<<link "Take the keys." "Canvas_renner_control_canvas_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_drained", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_leads_extracted", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("9ce98153-64e4-42e9-be99-0171204ef665:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('9ce98153-64e4-42e9-be99-0171204ef665:cc2') ? '' : 'unlocked-choice'">
<<link "Take the keys." "Canvas_renner_control_canvas_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("9ce98153-64e4-42e9-be99-0171204ef665:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]})>>
<span @class="setup.isChoiceVisited('9ce98153-64e4-42e9-be99-0171204ef665:cc3') ? '' : 'unlocked-choice'">
<<link "Let it go — the drain's empty. Recharge it at the cradle." "Location_Renner's_Depot">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("9ce98153-64e4-42e9-be99-0171204ef665:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<span @class="setup.isChoiceVisited('9ce98153-64e4-42e9-be99-0171204ef665:cc4') ? '' : 'unlocked-choice'">
<<link "Let it go — wrong gear." "Location_Renner's_Depot">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("9ce98153-64e4-42e9-be99-0171204ef665:cc4");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "lt", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})))>>
<<script>>
try {
window._engineNoExitsDiag_loop_renner_finisher = [{"text": "Straighten up.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "lt", "value": 2}]}}, {"text": "Take the keys.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "lt", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]}}, {"text": "Take the keys.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "drains_done", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "gte", "value": 1}]}}, {"text": "Let it go \u2014 the drain's empty. Recharge it at the cradle.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]}}, {"text": "Let it go \u2014 wrong gear.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "loop_renner_finisher",
choices: window._engineNoExitsDiag_loop_renner_finisher,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_loop_renner_finisher>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas loop_renner_finisher</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Navigation]]
<</if>>
<</nobr>><video src="./videos/sex/renner_loop_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She locks the office door. He's already reaching for her — the cold boss gone, just a wrecked man who's spent all day waiting for the help to come and take him apart.</p>
<<set $game_state.current_canvas = "5ae79b14-c497-4b6b-a85c-fcb31d9f0316">>
<<set $game_state.current_node = "65f139a1-d17e-48d7-9e0e-7f17734983de">>
<<script>>setup.markCanvasTriggered("5ae79b14-c497-4b6b-a85c-fcb31d9f0316");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.intro") === -1) { vn.push("loop_renner_office_sex.intro"); }<</script>>
<<nobr>>
<<link "Get on your knees." "Canvas_loop_renner_office_sex_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/renner_loop_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees between the chair and the desk, his cock in her mouth, his hand gentler in her hair than Mercer's hand has ever been. Renner holds on like he's afraid she'll stop.</p>
<<set $game_state.current_canvas = "5ae79b14-c497-4b6b-a85c-fcb31d9f0316">>
<<set $game_state.current_node = "c694a202-9258-4976-9db1-695552d87bb9">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.base_oral_r") === -1) { vn.push("loop_renner_office_sex.base_oral_r"); }<</script>>
<<nobr>>
<<link "Keep sucking his cock." "Canvas_loop_renner_office_sex_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Climb on his cock." "Canvas_loop_renner_office_sex_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_loop_renner_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/renner_loop_vaginal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She rides him in the office chair, slow, watching the last of his composure go. He says things he'll be ashamed of — please, and the false name she works under, and don't stop, don't ever stop.</p>
<<set $game_state.current_canvas = "5ae79b14-c497-4b6b-a85c-fcb31d9f0316">>
<<set $game_state.current_node = "1605c6c7-80c6-4c97-8fee-d8fc375b63e6">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.base_vaginal_r") === -1) { vn.push("loop_renner_office_sex.base_vaginal_r"); }<</script>>
<<nobr>>
<<link "Take him." "Canvas_loop_renner_office_sex_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on his cock." "Canvas_loop_renner_office_sex_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 50}, {"type": "clothing_item", "item_id": "cover_dockhand", "operator": "equipped"}]})>>
<span @class="setup.isChoiceVisited('5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc1') ? '' : 'unlocked-choice'">
<<link "Turn around — give him your ass." "Canvas_loop_renner_office_sex_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_anal_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Not yet — he has to be all the way broken first.">Not yet — he has to be all the way broken first.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_loop_renner_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/renner_loop_doggy_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over his own desk, his cock in her ass, his hands shaking on her hips. This is the act that hands her the keys — and now that she's turned it on him once, she can take them again whenever she likes.</p>
<<set $game_state.current_canvas = "5ae79b14-c497-4b6b-a85c-fcb31d9f0316">>
<<set $game_state.current_node = "506c343f-8606-4c5e-8fb9-68b2d7c5e455">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("loop_renner_office_sex.base_doggy_r") === -1) { vn.push("loop_renner_office_sex.base_doggy_r"); }<</script>>
<<nobr>>
<<link "Take it in the ass." "Canvas_loop_renner_office_sex_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back on his cock." "Canvas_loop_renner_office_sex_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc3') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_loop_renner_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("5ae79b14-c497-4b6b-a85c-fcb31d9f0316:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/ma_renner_blowjob_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Next afternoon, the office, like nothing happened — except she won't let it be like nothing happened. She waits until he's mid-sentence about a shipment, then asks him level, watching his face:</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="You" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>You:</strong> Did you like it. Yesterday. Or not.</div></div><p>He can't get out of it. She watches him try — the bluster, the deflection, the reach for the cold he used to have — and watches all of it fail, because the answer's already on him, plain. "You know I did," he says at last, like it's dragged out of him. And that's the thing she came for: not the yes, the surrender in having to say it. He's further in than he was, and he can feel it too.</p>
<<set $game_state.current_canvas = "b2d8abc7-70f8-45aa-b1bf-45e334a803f2">>
<<set $game_state.current_node = "90a56e68-e95f-4ca5-84ae-6258c4941a2b">>
<<script>>setup.markCanvasTriggered("b2d8abc7-70f8-45aa-b1bf-45e334a803f2");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("ma_renner_blowjob.base") === -1) { vn.push("ma_renner_blowjob.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 3.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "ma_blowjob_seen", "set");<</script>>
[[Leave it sitting on him.->Location_Renner's_Depot]]<video src="./videos/sex/renner_blowjob_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>His wanting runs ahead of him all day now. She picks the afternoon — the door shut, the yard empty, his hands shaking around a pen that isn't writing anything. She crosses to him and goes down on her knees in front of the chair, and for once he doesn't pretend he doesn't know what's happening.</p><span id="cascade-f70a5e68-318e-409c-a882-39450b5914c1-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...you don't have to.</div></div><p>It's the first kind thing he's said to her, and it's a lie, and they both let it be one. She takes his cock out and takes him in her mouth, and the cold boss makes a sound like something giving way under load.</p><span id="cascade-f70a5e68-318e-409c-a882-39450b5914c1-beat-1"><<linkreplace "Work him.">><p>She works him slow, the way she's learned men like him want it — not skilled exactly, but devoted, like he's the only thing in the room worth her mouth. He finishes with his fist in her hair and the false name she's working under coming out of him broken in half. She swallows, and clocks the shape of what just cracked in him.</p>[[Wipe your mouth, get back to work.->Location_Renner's_Depot]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "f8385d7b-bacc-447a-8e1c-748e30998b9b">>
<<set $game_state.current_node = "cad16a0e-62ba-4aed-95a1-e182381726ee">>
<<script>>setup.markCanvasTriggered("f8385d7b-bacc-447a-8e1c-748e30998b9b");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_renner_blowjob.base") === -1) { vn.push("cap_renner_blowjob.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "renner_oral_once", "set");<</script>><video src="./videos/scenes/rung_renner_grope_t4.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She stops pretending the contact is accidental — a hand on his thigh while he signs something, sliding up, letting him feel that she means it.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 30}]})>>
<p>He goes still. The pen stops. For a second the gutted business and the blacklist and the drinking are gone and there's just her hand and his breathing.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...don't stop.</div></div><p>Then he catches himself and pulls back, and hates that he had to.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 30}]})>>
<p>He doesn't pretend anymore. He covers her hand with his and presses it harder against him, his head going back, the cold boss just a man in a chair letting the help take him apart.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Whatever this is. Don't — don't stop doing it.</div></div>
<</if>>
<<set $game_state.current_canvas = "27792cc0-06d1-4271-b840-c27153bae5a4">>
<<set $game_state.current_node = "db77492a-beee-4da2-8500-5b71d6520bac">>
<<script>>setup.markCanvasTriggered("27792cc0-06d1-4271-b840-c27153bae5a4");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_renner_grope.base") === -1) { vn.push("rung_renner_grope.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Back to the invoices.->Location_Renner's_Depot]]<video src="./videos/scenes/rung_renner_flash_t3.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She's wearing nothing under the coveralls today, and she makes sure the moment comes where he can't not see it — bending slow for a low crate while he's looking.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 20}]})>>
<p>He looks. He looks longer than a man pretending not to. Then he turns hard to the window.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Get back to work.</div></div><p>His voice has gone rough. He doesn't mean the window.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Christ. Come here.</div></div><p>He says it low, half to himself, and doesn't finish the order — doesn't know what he'd do with her if she came. The wanting's ahead of the nerve. But it's there now, plain, and they both felt it land.</p>
<</if>>
<<set $game_state.current_canvas = "04038570-4cfc-4fc7-b0b0-bd4af3b108c1">>
<<set $game_state.current_node = "79d245e2-cd3f-484c-aabe-a9f855ca0d55">>
<<script>>setup.markCanvasTriggered("04038570-4cfc-4fc7-b0b0-bd4af3b108c1");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_renner_flash.base") === -1) { vn.push("rung_renner_flash.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Back to the invoices.->Location_Renner's_Depot]]<video src="./videos/scenes/rung_renner_tease_t2.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>In the office now, the door shut. She works close — leans across him for a file she doesn't need, lets her body say the thing her mouth won't.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "lt", "value": 10}]})>>
<div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Filing's in the cabinet. Not on my desk.</div></div><p>He doesn't look up. But he moves his chair back an inch, and the inch costs him something.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 10}]})>>
<p>His eyes go to her and snap away, and a muscle works in his jaw.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...You do that on purpose.</div></div><p>It isn't quite a question, and he doesn't tell her to stop.</p>
<</if>>
<<set $game_state.current_canvas = "4a438e67-7f21-4aa2-b515-c38e4b9b483d">>
<<set $game_state.current_node = "e69ca4e2-88d4-4448-941e-869691f64759">>
<<script>>setup.markCanvasTriggered("4a438e67-7f21-4aa2-b515-c38e4b9b483d");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rung_renner_tease.base") === -1) { vn.push("rung_renner_tease.base"); }<</script>>
<<script>>advanceTime(180);<</script>>
[[Back to the invoices.->Location_Renner's_Depot]]<img src="./videos/scenes/renner_office.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>It takes a week of good work before he stops finding fault just to have something to say to her. Then one grey afternoon he jerks his head at the glassed-in box at the back of the yard — his office — and doesn't quite look at her when he says it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Invoices are three months behind and I can't read my own writing. You'll do them in here. Heater works, door shuts, and I don't have to watch the help breathe.</div></div><p>It's the closest thing to trust a man this gutted has left to give. The door clicks shut behind her, and the public yard with its cameras and its cold becomes, for the length of a workday, a small private room with one wrecked man in it. Everything she's here to do starts now.</p>
<<set $game_state.current_canvas = "4f3800e7-4ec7-4fd9-bc41-8af1c0d63966">>
<<set $game_state.current_node = "88e8c9a9-9e12-4e9c-8c31-c2f603b78f98">>
<<script>>setup.markCanvasTriggered("4f3800e7-4ec7-4fd9-bc41-8af1c0d63966");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_renner_noticed.base") === -1) { vn.push("cap_renner_noticed.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "renner_office_open", "set");<</script>>
[[Get to work.->Location_Renner's_Depot]]<<set _sub_target = setup.checkAndSubstituteCanvas("3292c4d3-43f9-4130-90dc-3aa058e98942")>><<if _sub_target>><<goto _sub_target>><</if>>
<img src="./videos/scenes/renner_anchor_evening.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Renner's at his end of the bar, working on the evening's ruin. Off the clock he's quieter and worse — less temper, more rot. He tolerates her sitting near him now, which is its own kind of progress.</p>
<<set $game_state.current_canvas = "3292c4d3-43f9-4130-90dc-3aa058e98942">>
<<set $game_state.current_node = "91b0e23f-bea8-47fc-a03a-f63a9cf2a3f9">>
<<script>>setup.markCanvasTriggered("3292c4d3-43f9-4130-90dc-3aa058e98942");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_anchor_renner.base") === -1) { vn.push("hub_anchor_renner.base"); }<</script>>
<<nobr>>
<<link "Sit with him a while." "Canvas_hub_anchor_renner_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Leave." "Location_The_Waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She sits. He doesn't talk and doesn't tell her to go. The bartender refills him without being asked. Somewhere in the quiet she clocks what she sees — the blacklist letters he doesn't hide anymore, the way his hands shake until the second drink — and lets him think it's company.</p>
<<set $game_state.current_canvas = "3292c4d3-43f9-4130-90dc-3aa058e98942">>
<<set $game_state.current_node = "3db5b6c4-7347-49ad-bc8b-b66a564202b1">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_anchor_renner.sit") === -1) { vn.push("hub_anchor_renner.sit"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Head out.->Location_The_Anchor]]<<set _costs to [{"trait": "energy", "value": 15}]>>\
<<if setup.checkCostsAffordable(_costs)>>\
<<script>>setup.deductCosts("f415103c-b37f-4304-8b54-dabcfff0bea3");<</script>>
<<set _sub_target = setup.checkAndSubstituteCanvas("f415103c-b37f-4304-8b54-dabcfff0bea3")>><<if _sub_target>><<goto _sub_target>><</if>>
<img src="./videos/activities/renner_menial.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She works the floor — hauling, sorting, logging crates against a manifest that hasn't matched reality since Cain came through. It's dull, and her body doesn't tire the way a real one would, so she's better at it than the last three hands he ran off. The pay is thin, counted out grudging. But she does it well, and well is a language even a man this far gone still reads.</p>
<<set $game_state.current_canvas = "f415103c-b37f-4304-8b54-dabcfff0bea3">>
<<set $game_state.current_node = "2fd51e6b-5e54-41bc-933f-21030ac97e18">>
<<script>>setup.markCanvasTriggered("f415103c-b37f-4304-8b54-dabcfff0bea3");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("work_depot_haul.base") === -1) { vn.push("work_depot_haul.base"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("npc", "npc_renner", "relation", "add", 3.0, false, null);setup.applyAndNotifyTrait("player", null, "money", "add", 8.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "worked_renner_once", "set");<</script>>
[[Knock off for now.->Location_Renner's_Depot]]
<<else>>
<div class="cost-blocked-message">
<p><<= setup.getCostBlockedMessage(_costs)>></p>
</div>
[[Back->Location_Renner's_Depot]]
<</if>><<set _sub_target = setup.checkAndSubstituteCanvas("05f9b505-b519-4e57-bc5d-41be806c4267")>><<if _sub_target>><<goto _sub_target>><</if>>
<img src="./videos/scenes/renner_depot_floor.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Renner's depot. Half-empty racks, a forklift nobody's fixed, crates Cain's people left behind. He works the floor in a temper, and when he notices her at all it's to find fault. The work is the work.</p>
<<set $game_state.current_canvas = "05f9b505-b519-4e57-bc5d-41be806c4267">>
<<set $game_state.current_node = "eed03d49-cf2b-4a67-9d91-f9c3b101f962">>
<<script>>setup.markCanvasTriggered("05f9b505-b519-4e57-bc5d-41be806c4267");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_depot_floor.base") === -1) { vn.push("hub_depot_floor.base"); }<</script>>
<<nobr>>
<<link "Check in with him." "Canvas_hub_depot_floor_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 0}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('05f9b505-b519-4e57-bc5d-41be806c4267:cc0') ? '' : 'unlocked-choice'">
<<link "Tease him." "Canvas_rung_renner_tease_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "arousal", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("05f9b505-b519-4e57-bc5d-41be806c4267:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Tease him.">Tease him. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<else>>
<span class="locked-choice" title="Tease him.">Tease him.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 10}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('05f9b505-b519-4e57-bc5d-41be806c4267:cc1') ? '' : 'unlocked-choice'">
<<link "Flash him — work commando." "Canvas_rung_renner_flash_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "arousal", "add", 4.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_flirts_back", "set");<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("05f9b505-b519-4e57-bc5d-41be806c4267:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Flash him — work commando.">Flash him — work commando. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<else>>
<span class="locked-choice" title="Flash him — work commando.">Flash him — work commando.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "renner_flirts_back", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 20}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('05f9b505-b519-4e57-bc5d-41be806c4267:cc2') ? '' : 'unlocked-choice'">
<<link "Get your hands on him." "Canvas_rung_renner_grope_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "add", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "arousal", "add", 5.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("05f9b505-b519-4e57-bc5d-41be806c4267:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Get your hands on him.">Get your hands on him. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<else>>
<span class="locked-choice" title="Get your hands on him.">Get your hands on him.</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_office_open", "operator": "is_true"}, {"type": "flag", "subject": "player", "flag_key": "renner_oral_once", "operator": "is_true"}, {"type": "trait", "subject": "npc", "npc_id": "npc_renner", "trait_key": "corruption", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('05f9b505-b519-4e57-bc5d-41be806c4267:cc3') ? '' : 'unlocked-choice'">
<<link "Take him in the office." "Canvas_loop_renner_office_sex_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_fucked_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("05f9b505-b519-4e57-bc5d-41be806c4267:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Take him in the office.">Take him in the office.</span><br>
<</if>>
<<link "Head out." "Location_The_Waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She checks in. He grunts something about the manifest and doesn't look up. It's not much — but she lets him not-look, lets him get used to the shape of her in his yard. That's the work under the work.</p>
<<set $game_state.current_canvas = "05f9b505-b519-4e57-bc5d-41be806c4267">>
<<set $game_state.current_node = "e44b7fd1-42ec-4548-975a-eda72f65eb7e">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hub_depot_floor.check_in") === -1) { vn.push("hub_depot_floor.check_in"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Back to it.->Location_Renner's_Depot]]<img src="./videos/scenes/renner_anchor.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The Anchor is the kind of bar that's given up on its own neon. Renner's at the end of it — a big man gone soft and sour, three drinks into the evening and looking like the fourth won't help. He doesn't look up when she takes the stool beside him.</p><span id="cascade-3fe123d1-6caa-4f6f-bea7-c123d1189335-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> Whatever you're selling, I'm broke. Whatever you're offering, I've had it.</div></div><p>She tells him she heard he's rebuilding. That she works cheap, doesn't ask questions, doesn't tire. He looks at her then — not the way men look, the way a man counting his last dollars looks at a price that's almost low enough.</p><span id="cascade-3fe123d1-6caa-4f6f-bea7-c123d1189335-beat-1"><<linkreplace "Wait.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/renner.jpg" alt="Renner" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Renner:</strong> ...Depot. Nine sharp. You're late, you're gone. You steal, I break your hands. Now buy your own drink or get off my stool.</div></div><p>He goes back to the glass. It isn't much. But she's in — and being in, day after day, until a man like this stops noticing she's there, is the whole way the job gets done.</p>[[Leave him to it.->Location_The_Anchor]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "5331bee0-af88-43ef-8a69-4205f265e1e4">>
<<set $game_state.current_node = "b8f9b1f3-5b73-4650-9a2e-594b83c81562">>
<<script>>setup.markCanvasTriggered("5331bee0-af88-43ef-8a69-4205f265e1e4");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_renner_hired.base") === -1) { vn.push("cap_renner_hired.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "renner_hired", "set");<</script>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He pulls out and finishes on her face, holding her there by the jaw until he's done, then letting go the way you drop a thing back in a drawer. He's already turned to the window.</p><video src="./videos/sex/mercer_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her, holds, pulls out without comment. His cum slides down the inside of her thigh. He wipes himself on a cloth from the drawer and checks the time.</p><video src="./videos/sex/mercer_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass — and nothing comes. No give, no opening, no man-belonging-to-her. Just his weight and his cum and then his absence as he steps back. The same nothing as every time. She sets it aside with the other things she isn't allowed to ask, and straightens up.</p><video src="./videos/sex/mercer_finish_ass_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "01f8dfed-5f45-4801-a485-e13c484d5ff6">>
<<set $game_state.current_node = "7c6e45d2-1394-4b26-b450-22d44ab02285">>
<<script>>setup.markCanvasTriggered("01f8dfed-5f45-4801-a485-e13c484d5ff6");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_finisher.climax") === -1) { vn.push("mercer_finisher.climax"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.showEffectNotification();<</script>>
[[Straighten up.->Location_Mercer's_Penthouse]]<p>He's already unbuckling when she crosses to him; there's never preamble. He uses her the way he uses anything he owns — fully, without thanks, his mind somewhere else the whole time.</p>
<<set $game_state.current_canvas = "3bfc048f-2390-4a31-8399-51e7add4a97f">>
<<set $game_state.current_node = "cd0c3420-7f86-4879-aed7-e3e020674907">>
<<script>>setup.markCanvasTriggered("3bfc048f-2390-4a31-8399-51e7add4a97f");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.intro") === -1) { vn.push("mercer_serve.intro"); }<</script>>
<<nobr>>
<<link "Down on your knees." "Canvas_mercer_serve_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/mercer_serve_knees_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees on the floor, his cock in her mouth, his hand setting the pace by her hair. He watches the city and not her, talking to no one — a maintenance note, a time he wants a thing done. She services him.</p>
<<set $game_state.current_canvas = "3bfc048f-2390-4a31-8399-51e7add4a97f">>
<<set $game_state.current_node = "a2702a8f-349b-4e40-be5b-49f017f6a268">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.base_knees") === -1) { vn.push("mercer_serve.base_knees"); }<</script>>
<<nobr>>
<<link "Keep sucking his cock." "Canvas_mercer_serve_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Let him bend you over the desk." "Canvas_mercer_serve_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Present your ass against the glass." "Canvas_mercer_serve_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('3bfc048f-2390-4a31-8399-51e7add4a97f:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_mercer_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("3bfc048f-2390-4a31-8399-51e7add4a97f:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/mercer_serve_desk_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over the desk, his hand flat between her shoulder blades to keep her down, he fucks her from behind — unhurried, proprietary. Her body answers the way it always does, and he notes that it does, the way you'd note a gauge reading true.</p>
<<set $game_state.current_canvas = "3bfc048f-2390-4a31-8399-51e7add4a97f">>
<<set $game_state.current_node = "d8bad1e1-ae75-4e75-970f-aa5e7ee88b6d">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.base_desk") === -1) { vn.push("mercer_serve.base_desk"); }<</script>>
<<nobr>>
<<link "Take it." "Canvas_mercer_serve_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on your knees." "Canvas_mercer_serve_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Let him take your ass." "Canvas_mercer_serve_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('3bfc048f-2390-4a31-8399-51e7add4a97f:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_mercer_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("3bfc048f-2390-4a31-8399-51e7add4a97f:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/mercer_serve_glass_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Against the glass wall, the lit city laid out under them, he takes her ass in long deliberate strokes, both hands on her hips. This is the act that hands her the controls of any other man alive. She braces on the glass and waits to feel it start — the give, the opening. It doesn't come. It never comes with him.</p>
<<set $game_state.current_canvas = "3bfc048f-2390-4a31-8399-51e7add4a97f">>
<<set $game_state.current_node = "3669e507-dd43-4d9b-bf80-6828c2a9bafe">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_serve.base_glass") === -1) { vn.push("mercer_serve.base_glass"); }<</script>>
<<nobr>>
<<link "Take it in the ass." "Canvas_mercer_serve_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back over the desk." "Canvas_mercer_serve_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('3bfc048f-2390-4a31-8399-51e7add4a97f:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_mercer_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("3bfc048f-2390-4a31-8399-51e7add4a97f:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><img src="./videos/scenes/mercer_penthouse_hub.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Mercer's penthouse. He's at the desk or the glass, and whatever he's doing he can be interrupted to use her — that's what she's for, and they both know it. He looks up when she comes in. He doesn't smile. He never smiles.</p>
<<set $game_state.current_canvas = "4635f56d-6f21-4371-adaa-328b04628fa8">>
<<set $game_state.current_node = "2fe3b0d0-c732-4afa-881b-35ae6775a888">>
<<script>>setup.markCanvasTriggered("4635f56d-6f21-4371-adaa-328b04628fa8");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_penthouse_hub.base") === -1) { vn.push("mercer_penthouse_hub.base"); }<</script>>
<<nobr>>
<<link "Report." "Canvas_mercer_penthouse_hub_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Kneel and serve him." "Canvas_mercer_serve_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Leave." "Location_The_Atrium">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She gives him what she has — the Renner approach, the angle, the timeline. He listens the way he listens to a status light: for whether it's green.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Don't bring me progress. Bring me the man's secrets or bring me nothing. Go make yourself useful to someone who can't see what you are.</div></div>
<<set $game_state.current_canvas = "4635f56d-6f21-4371-adaa-328b04628fa8">>
<<set $game_state.current_node = "c84404c2-b7ed-439d-9677-fdb21da13c0c">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("mercer_penthouse_hub.report") === -1) { vn.push("mercer_penthouse_hub.report"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Go.->Location_Mercer's_Penthouse]]<img src="./videos/scenes/mercer_penthouse.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The penthouse, by daylight. Mercer's at the glass with a tablet, and he doesn't turn when the elevator lets her in. He doesn't have to. She's company-issue, and she's here because being here is what she's for.</p><span id="cascade-887172d8-10c8-4bdf-86e3-90c1e1dd38fd-beat-0"><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> On your knees. I've got a call in ten.</div></div><span id="cascade-887172d8-10c8-4bdf-86e3-90c1e1dd38fd-beat-1"><<linkreplace "Kneel.">><p>She goes down without a word. He takes the call one-handed, her mouth around his cock, the other hand resting in her hair the way a man rests his hand on a tool he isn't using yet. He finishes before the call does. She swallows, because that's tidy, and tidy is what's expected.</p><video src="./videos/sex/mercer_first_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><span id="cascade-887172d8-10c8-4bdf-86e3-90c1e1dd38fd-beat-2"><<linkreplace "Hear the terms.">><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Good. You come up when I want you, and you're like this when you do. That's the arrangement. It was always the arrangement.</div></div><p>It was. She straightens, wipes her mouth, and the shape of the days settles over her — the same shape as all the days behind it: the cradle, the call, the kneeling. World without end.</p>[[Stand up.->Location_Mercer's_Penthouse]]
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "37406122-7344-4f19-8b5d-8ef9db65c2f7">>
<<set $game_state.current_node = "8483cde0-bfff-4b82-8386-efb9d2d4776f">>
<<script>>setup.markCanvasTriggered("37406122-7344-4f19-8b5d-8ef9db65c2f7");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cap_first_penthouse_service.base") === -1) { vn.push("cap_first_penthouse_service.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "mercer_hub_open", "set");<</script>><p>She strips and stands under the water until whatever the work left on her is gone — sweat, a client's hands, the smell of the docks. The company keeps her maintained; this part she does for herself, the one washing that isn't on anyone's schedule. She comes out clean, and for a minute, almost new.</p>
<<set $game_state.current_canvas = "02ac7616-37ed-470a-bfca-f1c541e51b3a">>
<<set $game_state.current_node = "d80f6909-5c31-4475-ad06-09ceeedc5e98">>
<<script>>setup.markCanvasTriggered("02ac7616-37ed-470a-bfca-f1c541e51b3a");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_wash.base") === -1) { vn.push("activity_wash.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "hygiene", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
[[Dry off. Dressed.->Location_Wren's_Room]]<img src="./videos/locations/underworld_market.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Coin moves here for everything the Spire pretends doesn't exist. Blades and field-gear on the near tables; further back, behind chain, the part she doesn't look at directly. She has coin now. She can leave with an edge.</p>
<<set $game_state.current_canvas = "afffb3e5-a8a8-4145-9537-d4abd116fec3">>
<<set $game_state.current_node = "1963a483-a839-40d0-a835-ae8ddd4742a3">>
<<script>>setup.markCanvasTriggered("afffb3e5-a8a8-4145-9537-d4abd116fec3");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.base") === -1) { vn.push("underworld_market_shop.base"); }<</script>>
<<nobr>>
<<if setup.checkCostsAffordable([{"trait": "coin", "value": 40}])>>
<<link "Buy a weapon — something with an edge." "Canvas_underworld_market_shop_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "fighting", "add", 5.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", -40, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Buy a weapon — something with an edge.">Buy a weapon — something with an edge. (<<= setup.getCostBlockedMessage([{"trait": "coin", "value": 40}])>>)</span><br>
<</if>>
<<if setup.checkCostsAffordable([{"trait": "coin", "value": 30}])>>
<<link "Buy infiltration gear." "Canvas_underworld_market_shop_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "stealth", "add", 5.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", -30, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Buy infiltration gear.">Buy infiltration gear. (<<= setup.getCostBlockedMessage([{"trait": "coin", "value": 30}])>>)</span><br>
<</if>>
<<link "Walk the pens." "Canvas_underworld_market_shop_Node_4">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Leave the stalls." "Location_The_Black_Market">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She buys a blade the way a soldier does — by balance, not by looks — and a thing to hide it in. Her hands are deadlier leaving than they were coming in. Coin well spent.</p>
<<set $game_state.current_canvas = "afffb3e5-a8a8-4145-9537-d4abd116fec3">>
<<set $game_state.current_node = "380210ec-8d6b-47db-8636-302faacd727f">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.weapon") === -1) { vn.push("underworld_market_shop.weapon"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Stow it.->Location_The_Black_Market]]<p>Soft soles, a light that isn't a light, a tool roll that opens what's meant to stay shut. The kind of kit that turns a careful woman into one nobody sees. Coin well spent.</p>
<<set $game_state.current_canvas = "afffb3e5-a8a8-4145-9537-d4abd116fec3">>
<<set $game_state.current_node = "52aee529-dbef-4c29-acb5-6d1c90839c2a">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.gear") === -1) { vn.push("underworld_market_shop.gear"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Pack it away.->Location_The_Black_Market]]<p>Behind the chain they keep people the way the market keeps anything — by weight and condition, tagged, waiting on a buyer. She walks the line because she makes herself walk it. Most are just people, stolen and sold. But one of them, a girl too still, too even, watches her back with eyes that don't track quite right — and something in Wren's chest does a thing it has no name for, a recognition she shuts off before it finishes. She's looked at her own reflection in a cage. She shuts it down and walks out.</p>
<<set $game_state.current_canvas = "afffb3e5-a8a8-4145-9537-d4abd116fec3">>
<<set $game_state.current_node = "ec80ec89-1895-40f8-9a98-53759ab26859">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_market_shop.pens") === -1) { vn.push("underworld_market_shop.pens"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Don't look again.->Location_The_Black_Market]]<p>The ring is a pit of packed dirt under a single bulb, the crowd three deep around it laying coin on the next body to fall. The matchmaker looks her up and down, unimpressed, and asks what she thinks she can take. Down here her fists pay the same as the rest of her.</p>
<<set $game_state.current_canvas = "d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5">>
<<set $game_state.current_node = "dfb007df-2766-43ba-b65b-f7d4788e3fd0">>
<<script>>setup.markCanvasTriggered("d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_pit_fight.base") === -1) { vn.push("underworld_pit_fight.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5:cc0') ? '' : 'unlocked-choice'">
<<link "Take a low bout." "Canvas_underworld_pit_fight_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", 8.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Take a low bout.">Take a low bout. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 30}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 20}])>>
<span @class="setup.isChoiceVisited('d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5:cc1') ? '' : 'unlocked-choice'">
<<link "Take a hard bout." "Canvas_underworld_pit_fight_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", 20.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -20, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Take a hard bout.">Take a hard bout. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 20}])>>)</span><br>
<</if>>
<</if>>
<<link "Just watch tonight." "Location_The_Pit">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>They put a brawler in with her, slow and heavy. She lets him swing, reads the half-beat, and ends it with the kind of economy the crowd doesn't cheer because it scared them. The matchmaker pays out without meeting her eyes.</p>
<<set $game_state.current_canvas = "d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5">>
<<set $game_state.current_node = "21df857c-6f7d-4dbd-a767-bca1c1dbb672">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_pit_fight.win_low") === -1) { vn.push("underworld_pit_fight.win_low"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Take the purse.->Location_The_Pit]]<p>This one's real — fast, trained, in it to hurt her. For a few seconds it's almost a fight. Then the thing they built her to be takes over, and what's left on the dirt isn't getting up. The crowd goes quiet. The purse is heavy. Someone in the dark is watching her now.</p>
<<set $game_state.current_canvas = "d1e54288-fbdf-450f-bbcf-a9ebe09f1bd5">>
<<set $game_state.current_node = "5f1c155f-e392-4049-a303-115f6dcbf005">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_pit_fight.win_hard") === -1) { vn.push("underworld_pit_fight.win_hard"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Take the purse.->Location_The_Pit]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 0}]})>>
<p>He holds her head still and finishes in her mouth; she takes it, swallows because the trade expects it, and sits back on her heels. He's already reaching for his drink.</p><video src="./videos/sex/brothel_finish_facial_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 1}]})>>
<p>He finishes inside her with a grunt, holds, then pulls out. His cum slides down the inside of her thigh. She waits the polite beat and lifts off, and counts the coin into her palm.</p><video src="./videos/sex/brothel_finish_inside_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "sex_finisher_type", "operator": "eq", "value": 2}]})>>
<p>He finishes in her ass and stays a moment, breathing, then steps back. His weight and his cum and then nothing — the same nothing as every man down here. She straightens. The next one's already on the stairs.</p><video src="./videos/sex/brothel_finish_ass_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video>
<</if>>
<<set $game_state.current_canvas = "6391c6e6-4d53-4f3c-b4b0-89eb7be891c5">>
<<set $game_state.current_node = "ed9720e0-04ec-4f69-a6e8-dd9ed4abfdbc">>
<<script>>setup.markCanvasTriggered("6391c6e6-4d53-4f3c-b4b0-89eb7be891c5");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_finisher.climax") === -1) { vn.push("underworld_brothel_finisher.climax"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 10.0, false, null);setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);setup.showEffectNotification();<</script>>
[[Clean up. Collect.->Location_The_House]]<p>The madam points her at a door. The man inside doesn't look up from his drink until she's undressed — then he looks her over the way men look over rented things, deciding where to start. No name, no preamble. He paid for the hour and means to use it.</p>
<<set $game_state.current_canvas = "ed092034-264f-42f6-8d48-21db874249df">>
<<set $game_state.current_node = "9c568092-6600-442e-b325-8df94d65256e">>
<<script>>setup.markCanvasTriggered("ed092034-264f-42f6-8d48-21db874249df");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.intro") === -1) { vn.push("underworld_brothel_loop.intro"); }<</script>>
<<nobr>>
<<link "Down on your knees." "Canvas_underworld_brothel_loop_Node_2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/brothel_oral_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>On her knees between his, she takes his cock in her mouth and works it the way the trade taught her — steady, practiced, her eyes somewhere past his hip. He fists a hand in her hair and sets the pace. She lets him. None of it reaches her.</p>
<<set $game_state.current_canvas = "ed092034-264f-42f6-8d48-21db874249df">>
<<set $game_state.current_node = "8f292370-ebc0-4eee-9c53-cad9fa31e2af">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.base_oral") === -1) { vn.push("underworld_brothel_loop.base_oral"); }<</script>>
<<nobr>>
<<link "Keep sucking him." "Canvas_underworld_brothel_loop_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Climb on and ride him." "Canvas_underworld_brothel_loop_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Turn around and give him your ass." "Canvas_underworld_brothel_loop_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('ed092034-264f-42f6-8d48-21db874249df:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_underworld_brothel_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ed092034-264f-42f6-8d48-21db874249df:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/brothel_client.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She climbs on and takes his cock without ceremony, sinks down and rides, hips keeping a rhythm that has nothing to do with her. He paws at her tits and fucks up into her; she gives him the small sounds the trade expects and feels none of them.</p>
<<set $game_state.current_canvas = "ed092034-264f-42f6-8d48-21db874249df">>
<<set $game_state.current_node = "e0153b40-9b19-4855-bc0e-e110b70fa108">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.base_vaginal") === -1) { vn.push("underworld_brothel_loop.base_vaginal"); }<</script>>
<<nobr>>
<<link "Keep riding him." "Canvas_underworld_brothel_loop_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back down on your knees." "Canvas_underworld_brothel_loop_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Let him have your ass." "Canvas_underworld_brothel_loop_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 2.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}]})>>
<span @class="setup.isChoiceVisited('ed092034-264f-42f6-8d48-21db874249df:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_underworld_brothel_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ed092034-264f-42f6-8d48-21db874249df:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/brothel_anal_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>Bent over the edge of the bed, she takes him in the ass in long flat strokes, both his hands clamped on her hips. Just more friction down a body that gives nothing back. She braces and waits it out, counting somewhere behind her own face what the hour's worth.</p>
<<set $game_state.current_canvas = "ed092034-264f-42f6-8d48-21db874249df">>
<<set $game_state.current_node = "7df89eef-2cf5-4a36-adc8-a9998e6934f9">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_loop.base_anal") === -1) { vn.push("underworld_brothel_loop.base_anal"); }<</script>>
<<nobr>>
<<link "Take it." "Canvas_underworld_brothel_loop_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "add", (Math.floor(Math.random() * 7) + 8), false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Back onto his cock." "Canvas_underworld_brothel_loop_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('ed092034-264f-42f6-8d48-21db874249df:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_underworld_brothel_finisher_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ed092034-264f-42f6-8d48-21db874249df:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><p>The madam counts her with one look and tips her head at the rooms. No contract, no company, no Mercer signing her out — just a door, a client, and coin at the end that goes in her pocket and nowhere else. The thing she's built for, sold for once, on her own account.</p>
<<set $game_state.current_canvas = "323363f0-edea-41f0-89eb-02c19952c615">>
<<set $game_state.current_node = "e1b7e110-9cf6-4ecf-b81d-fba1f707874a">>
<<script>>setup.markCanvasTriggered("323363f0-edea-41f0-89eb-02c19952c615");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_brothel_work.base") === -1) { vn.push("underworld_brothel_work.base"); }<</script>>
<<nobr>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<<link "Take a client." "Canvas_underworld_brothel_loop_Node_1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_finisher_type", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "anal_active", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_entry_origin", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Take a client.">Take a client. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<<link "Not tonight." "Location_The_House">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/locations/underworld_strip.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The strip runs under the docks for as far as the light does — the house, the pit, the market, and past them the dark where the real city keeps its teeth. Up here nobody's looking for her. It's the closest thing to free she's felt.</p>
<<set $game_state.current_canvas = "edbf5e21-07b1-4edf-a948-27108e8cd221">>
<<set $game_state.current_node = "87b76d33-b49b-437d-852c-0528003cc184">>
<<script>>setup.markCanvasTriggered("edbf5e21-07b1-4edf-a948-27108e8cd221");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_strip_hub.base") === -1) { vn.push("underworld_strip_hub.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Climb back up to the docks.->Location_The_Waterfront]]<img src="./videos/locations/underworld_gate.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The man at the door doesn't ask her name. He looks her over the way a turnstile would and holds out a hand. "Coin," he says. "Or you're not my problem." Behind him the stairs go down into red light and noise. There's more than one way through a man like this. The coin he wants isn't company credits — it's underworld scrip: earned down here, spent down here, worth nothing up top.</p>
<<set $game_state.current_canvas = "1103d807-816c-4f87-8b4c-a7a88f644af9">>
<<set $game_state.current_node = "5ee07c53-dcde-4186-aa78-ab5b587f679d">>
<<script>>setup.markCanvasTriggered("1103d807-816c-4f87-8b4c-a7a88f644af9");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_gate_check.base") === -1) { vn.push("underworld_gate_check.base"); }<</script>>
<<nobr>>
<<if setup.checkCostsAffordable([{"trait": "coin", "value": 5}])>>
<<link "Pay the toll." "Location_The_Underground">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", -5, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<else>>
<span class="locked-choice" title="Pay the toll.">Pay the toll. (<<= setup.getCostBlockedMessage([{"trait": "coin", "value": 5}])>>)</span><br>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 15}]})>>
<span @class="setup.isChoiceVisited('1103d807-816c-4f87-8b4c-a7a88f644af9:cc0') ? '' : 'unlocked-choice'">
<<link "Force past him." "Location_The_Underground">><<script>>setup.markChoiceVisited("1103d807-816c-4f87-8b4c-a7a88f644af9:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}]})>>
<<if setup.checkCostsAffordable([{"trait": "arousal_charge", "value": 1}])>>
<span @class="setup.isChoiceVisited('1103d807-816c-4f87-8b4c-a7a88f644af9:cc1') ? '' : 'unlocked-choice'">
<<link "Put the weapon on him." "Canvas_underworld_gate_check_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("1103d807-816c-4f87-8b4c-a7a88f644af9:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Put the weapon on him.">Put the weapon on him. (<<= setup.getCostBlockedMessage([{"trait": "arousal_charge", "value": 1}])>>)</span><br>
<</if>>
<</if>>
<<link "Turn back." "Location_The_Waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/arousal_weapon_use.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She thumbs the emitter and the doorman's whole face changes — the bored menace gone, breath short, the hand that wanted coin wanting something else now. She walks him back into the shadow of the stair, gets his cock out, and rides him against the wall in quick flat strokes, fast and clinical, giving him nothing but the friction. He spills into her with his eyes rolling white and sags down the bricks, under. He'll wake on the steps in an hour with no memory, and never know she walked past him. The stairs are open.</p>
<<set $game_state.current_canvas = "1103d807-816c-4f87-8b4c-a7a88f644af9">>
<<set $game_state.current_node = "05eead55-4d72-42a2-a25b-b9267d1aa37e">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_gate_check.weapon_scene") === -1) { vn.push("underworld_gate_check.weapon_scene"); }<</script>>
<<script>>advanceTime(60);<</script>>
[[Down into the strip.->Location_The_Underground]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}]})>>
<p>The emitter sits charged on the bench. One core, one weapon — it feeds the drain inside her or the thing in her hand, never both at once. Right now it's the drain.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<p>The emitter's live in her hand, the drain inside her gone quiet behind it. One core, one weapon. Right now it's the emitter.</p>
<</if>>
<<set $game_state.current_canvas = "7ddcb642-b712-4f7b-b30f-792b85e61462">>
<<set $game_state.current_node = "b5371502-4b73-4411-9f62-7ed0176aab87">>
<<script>>setup.markCanvasTriggered("7ddcb642-b712-4f7b-b30f-792b85e61462");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_swap_weapon.base") === -1) { vn.push("activity_swap_weapon.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}]})>>
<span @class="setup.isChoiceVisited('7ddcb642-b712-4f7b-b30f-792b85e61462:cc0') ? '' : 'unlocked-choice'">
<<link "Run the emitter instead." "Location_Wren's_Room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 2.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("7ddcb642-b712-4f7b-b30f-792b85e61462:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<span @class="setup.isChoiceVisited('7ddcb642-b712-4f7b-b30f-792b85e61462:cc1') ? '' : 'unlocked-choice'">
<<link "Go back to the drain." "Location_Wren's_Room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("7ddcb642-b712-4f7b-b30f-792b85e61462:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<link "Leave it." "Location_Wren's_Room">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The emitter sits on her bench, casing off, guts exposed. She knows the architecture of it the way she knows her own — because it is her own, scaled down to a handheld. Heat-warp on the field coils, a cracked driver. Slow work, and hers alone.</p>
<<set $game_state.current_canvas = "68cf46f5-e6f7-483d-aa6f-3a93c2376e77">>
<<set $game_state.current_node = "549698ec-89e8-46db-9546-dc8edbaf9745">>
<<script>>setup.markCanvasTriggered("68cf46f5-e6f7-483d-aa6f-3a93c2376e77");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_repair_weapon.base") === -1) { vn.push("activity_repair_weapon.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "lt", "value": 2}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('68cf46f5-e6f7-483d-aa6f-3a93c2376e77:cc0') ? '' : 'unlocked-choice'">
<<link "Work on it." "Canvas_activity_repair_weapon_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "weapon_repair", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("68cf46f5-e6f7-483d-aa6f-3a93c2376e77:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Work on it.">Work on it. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "gte", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "lt", "value": 3}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('68cf46f5-e6f7-483d-aa6f-3a93c2376e77:cc1') ? '' : 'unlocked-choice'">
<<link "Finish it." "Canvas_activity_repair_weapon_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "weapon_repair", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "set", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "arousal_weapon_ready", "set");<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("68cf46f5-e6f7-483d-aa6f-3a93c2376e77:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Finish it.">Finish it. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<link "Leave it for now." "Location_Wren's_Room">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "lt", "value": 2}]})>>
<p>She straightens the field coils, one pass, and tests the draw. Dead still — but cleaner dead than it was. More to do.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "weapon_repair", "operator": "gte", "value": 2}]})>>
<p>Driver reseated, coils true. It draws a charge now and holds it — a flicker of the field crossing her own skin, the faint unwanted warmth of it. Almost. One more pass and it fires.</p>
<</if>>
<<set $game_state.current_canvas = "68cf46f5-e6f7-483d-aa6f-3a93c2376e77">>
<<set $game_state.current_node = "0f0fc294-f16d-447c-a838-8b45140e76b1">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_repair_weapon.progress") === -1) { vn.push("activity_repair_weapon.progress"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Set it down.->Location_Wren's_Room]]<p>Last pass. She powers it and the field comes up clean, a low hum she feels in her teeth — and, testing it on herself a half-second too long, a heat low in her belly she didn't ask for and shuts off fast. It works. A second weapon now, hers, that the company never issued and doesn't know she has. A way past any man she can't fight.</p>
<<set $game_state.current_canvas = "68cf46f5-e6f7-483d-aa6f-3a93c2376e77">>
<<set $game_state.current_node = "11d1a0c3-b0ea-4049-8846-2faa69844f86">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_repair_weapon.done") === -1) { vn.push("activity_repair_weapon.done"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Stow it.->Location_Wren's_Room]]<p>The strongroom is what he saved. Two personnel files, a man each — his own people, the two who moved the materials. Both stamped CLOSED. Under each, a death notice, and under that, more notices: wives, children, the whole line, the dates all the same night. Renner didn't just fire them. He erased them. And paperclipped to the back, the thing he's really hiding: where the two men came from before Vance — the same name twice, a place down in the city that isn't on any map. The underworld. That's the thread Cain followed back. That's where it goes.</p><p>Standing in it, something turns over in her — not a thought, a feeling, brief and uninvited: she has stood in a room like this before, on the other side of the file. Then it's gone, filed under nothing.</p>
<<set $game_state.current_canvas = "0a834795-3c12-431f-a631-43303b487c90">>
<<set $game_state.current_node = "4f3a8854-85a6-46c1-abaf-87b1f5a63091">>
<<script>>setup.markCanvasTriggered("0a834795-3c12-431f-a631-43303b487c90");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_find_3.base") === -1) { vn.push("yard_find_3.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "found_dead_men", "set");<</script>>
[[Take what matters. Go.->Location_The_Burned_Yard]]<img src="./videos/activities/arousal_weapon.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>In a melted equipment locker, half-slagged but whole at the core, is a device she knows in her hands before she knows in her head — the same build as the thing inside her, the one that makes a body want against its own sense. A field emitter. Renner had one off the line and never fired it. Heat-warped, dead. But the core is sound. She pockets it. Fixed, it's a way past any man who'd otherwise put her down.</p>
<<set $game_state.current_canvas = "ff4ffaa6-a21a-4ce7-9033-8bc5d61dd473">>
<<set $game_state.current_node = "0a54d3de-cba6-41f1-8205-605b05bf7312">>
<<script>>setup.markCanvasTriggered("ff4ffaa6-a21a-4ce7-9033-8bc5d61dd473");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_find_2.base") === -1) { vn.push("yard_find_2.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "has_arousal_weapon", "set");<</script>>
[[Take it. Get out clean.->Location_The_Burned_Yard]]<p>Inside the outer line she finds the shape of it under the burn: not just a storage yard. Loading bays built for things heavier than crates, restraint points set into a slab, a billing wall scorched but legible — shipments logged out to the same Vance facility she's already walked the ashes of. Renner moved more than equipment through here. He just never asked what for.</p>
<<set $game_state.current_canvas = "64d279b8-f341-48a4-a656-4b3298e95728">>
<<set $game_state.current_node = "31d592a7-c845-4150-b4bd-4fd69481055d">>
<<script>>setup.markCanvasTriggered("64d279b8-f341-48a4-a656-4b3298e95728");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_find_1.base") === -1) { vn.push("yard_find_1.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "yard_clue_seen", "set");<</script>>
[[Log it. Keep moving.->Location_The_Burned_Yard]]<img src="./videos/locations/renner_burned_yard.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 1}]})>>
<p>The fence is down on the water side. Past it the yard is black glass and bent steel — and a guard with a torch, walking the line where the gate used to be. Getting in means getting past him.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 3}]})>>
<p>She's inside the outer line now. Deeper in there are more lights and more men — Renner doesn't watch a dead yard this hard for nothing. Whatever's worth guarding is further back.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 3}]})>>
<p>She's been all the way to the back of it. There's nothing left in the yard she hasn't already put her hands on.</p>
<</if>>
<<set $game_state.current_canvas = "ca929d2c-a417-4856-a411-6171567e4871">>
<<set $game_state.current_node = "198c35e9-7ba6-4e1a-acaa-1bf143878732">>
<<script>>setup.markCanvasTriggered("ca929d2c-a417-4856-a411-6171567e4871");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.base") === -1) { vn.push("yard_crawl.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 1}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc0') ? '' : 'unlocked-choice'">
<<link "Push in past the gate." "Canvas_yard_crawl_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Push in past the gate.">Push in past the gate. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 2}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc1') ? '' : 'unlocked-choice'">
<<link "Work deeper in." "Canvas_yard_crawl_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Work deeper in.">Work deeper in. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 3}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc2') ? '' : 'unlocked-choice'">
<<link "Push to the back of it." "Canvas_yard_crawl_Node_4">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Push to the back of it.">Push to the back of it. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<link "Slip back out." "Location_The_Waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>One man, one torch, bored and slow — the easy one, the test before the real ones. He hasn't seen her yet.</p>
<<set $game_state.current_canvas = "ca929d2c-a417-4856-a411-6171567e4871">>
<<set $game_state.current_node = "2e21592b-2c7d-4b54-b26e-5c4f80cc0da4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.guard1") === -1) { vn.push("yard_crawl.guard1"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 10}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc3') ? '' : 'unlocked-choice'">
<<link "Slip past him, unseen." "Canvas_yard_crawl_Node_5">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc4') ? '' : 'unlocked-choice'">
<<link "Take him down." "Canvas_yard_crawl_Node_5">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc4");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc5') ? '' : 'unlocked-choice'">
<<link "Use the weapon on him." "Canvas_yard_crawl_Node_6">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc5");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc6') ? '' : 'unlocked-choice'">
<<link "He's on you — break and run." "Canvas_yard_crawl_Node_7">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc6");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 10}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}]})))>>
<<script>>
try {
window._engineNoExitsDiag_yard_crawl = [{"text": "Slip past him, unseen.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 10}]}}, {"text": "Take him down.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 10}]}}, {"text": "Use the weapon on him.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]}}, {"text": "He's on you \u2014 break and run.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 10}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "yard_crawl",
choices: window._engineNoExitsDiag_yard_crawl,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_yard_crawl>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas yard_crawl</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_The_Burned_Yard]]
<</if>>
<</nobr>><p>Two of them here, awake and armed, working a pattern between the racks. Past the easy line — these ones would hurt her if she let them.</p>
<<set $game_state.current_canvas = "ca929d2c-a417-4856-a411-6171567e4871">>
<<set $game_state.current_node = "0cad9cd6-d31e-42b8-810f-cb0ab3c23d00">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.guard2") === -1) { vn.push("yard_crawl.guard2"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc7') ? '' : 'unlocked-choice'">
<<link "Slip past them, unseen." "Canvas_yard_crawl_Node_5">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc7");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc8') ? '' : 'unlocked-choice'">
<<link "Take them down." "Canvas_yard_crawl_Node_5">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc8");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc9') ? '' : 'unlocked-choice'">
<<link "Use the weapon on them." "Canvas_yard_crawl_Node_6">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc9");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc10') ? '' : 'unlocked-choice'">
<<link "Too many — break and run." "Canvas_yard_crawl_Node_7">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc10");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]})))>>
<<script>>
try {
window._engineNoExitsDiag_yard_crawl = [{"text": "Slip past them, unseen.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]}}, {"text": "Take them down.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]}}, {"text": "Use the weapon on them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]}}, {"text": "Too many \u2014 break and run.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "yard_crawl",
choices: window._engineNoExitsDiag_yard_crawl,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_yard_crawl>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas yard_crawl</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_The_Burned_Yard]]
<</if>>
<</nobr>><p>The heaviest watch is here, at the back — a knot of his best men around a strongroom the fire didn't reach. Whatever Renner's keeping, it's behind them.</p>
<<set $game_state.current_canvas = "ca929d2c-a417-4856-a411-6171567e4871">>
<<set $game_state.current_node = "fcb54246-7e34-438d-9de9-889d075e3774">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.guard3") === -1) { vn.push("yard_crawl.guard3"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc11') ? '' : 'unlocked-choice'">
<<link "Ghost straight through them." "Canvas_yard_crawl_Node_5">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc11");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc12') ? '' : 'unlocked-choice'">
<<link "Cut through them." "Canvas_yard_crawl_Node_5">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc12");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc13') ? '' : 'unlocked-choice'">
<<link "Use the weapon on them." "Canvas_yard_crawl_Node_6">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "arousal_charge", "add", -1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc13");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}]})>>
<span @class="setup.isChoiceVisited('ca929d2c-a417-4856-a411-6171567e4871:cc14') ? '' : 'unlocked-choice'">
<<link "Not yet — break and run." "Canvas_yard_crawl_Node_7">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("ca929d2c-a417-4856-a411-6171567e4871:cc14");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 40}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 40}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}]})))>>
<<script>>
try {
window._engineNoExitsDiag_yard_crawl = [{"text": "Ghost straight through them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 40}]}}, {"text": "Cut through them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 40}]}}, {"text": "Use the weapon on them.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}, {"type": "flag", "subject": "player", "flag_key": "arousal_weapon_ready", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "arousal_charge", "operator": "gte", "value": 1}]}}, {"text": "Not yet \u2014 break and run.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 40}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "yard_crawl",
choices: window._engineNoExitsDiag_yard_crawl,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_yard_crawl>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas yard_crawl</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_The_Burned_Yard]]
<</if>>
<</nobr>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 2}]})>>
<p>She's through the gate and into the outer yard, the guard behind her none the wiser or none the able. The dark swallows her.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 2}, {"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "lt", "value": 3}]})>>
<p>Deeper now, in among the racks where the lights are — and past the men who'd have stopped her. She keeps to the shadow and goes on.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "yard_depth", "operator": "gte", "value": 3}]})>>
<p>The back of the yard, the strongroom, the thing they were all standing around. She's in.</p>
<</if>>
<<set $game_state.current_canvas = "ca929d2c-a417-4856-a411-6171567e4871">>
<<set $game_state.current_node = "397ff16c-9392-4f95-adc8-f2ede647a8be">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.deeper") === -1) { vn.push("yard_crawl.deeper"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[See what's here.->Location_The_Burned_Yard]]<video src="./videos/sex/yard_emitter_fire_t5.webm" autoplay muted loop playsinline controls preload="metadata" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;"></video><p>She thumbs the emitter and the field rolls out ahead of her. Whoever's in reach goes stupid with it inside a breath — pupils blown, breath ragged, the gun forgotten in a slack hand. She picks the nearest, drags his cock out of his fatigues, and sinks down onto it against the crates, riding him in hard flat strokes while the others sway on their feet. There's nothing in it on her side — just leverage; she works him until he spills into her and drops boneless and out. The rest fold a beat later. By the time any of them surface she's gone, deeper in, his cum drying on her thigh and nothing in her face at all.</p>
<<set $game_state.current_canvas = "ca929d2c-a417-4856-a411-6171567e4871">>
<<set $game_state.current_node = "746f609e-e9ab-4961-a3e7-a17d4b449293">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.weapon_scene") === -1) { vn.push("yard_crawl.weapon_scene"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Step over them. On.->Location_The_Burned_Yard]]<p>It goes against her — too many, or too sharp, or her own body too slow. She breaks for the downed fence and is over the water side before the torches swing round. Nothing gained tonight. She walks it off.</p>
<<set $game_state.current_canvas = "ca929d2c-a417-4856-a411-6171567e4871">>
<<set $game_state.current_node = "b339bf4e-b157-4ca9-9387-9b31f3e89b68">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("yard_crawl.flee") === -1) { vn.push("yard_crawl.flee"); }<</script>>
<<script>>advanceTime(60);<</script>>
[[Get clear.->Location_The_Waterfront]]<img src="./videos/locations/facility_ruins.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The facility is a black skeleton against the dock lights — scorched frame, melted racks, the smell of old smoke that never washed out. She walks it slowly, cataloguing: where the containment was, where the fire started, what the dead machines used to do. None of it matches the briefing. All of it was built to hold things that could feel — and someone came through and let them out before he burned it down. The dread-place. The first one.</p>
<<set $game_state.current_canvas = "e31e74f1-c1e3-4adb-9f31-c89f1ff88ad1">>
<<set $game_state.current_node = "9f1b62b5-ce55-471e-90d0-55e34b8fa15e">>
<<script>>setup.markCanvasTriggered("e31e74f1-c1e3-4adb-9f31-c89f1ff88ad1");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("inv_facility_explore.base") === -1) { vn.push("inv_facility_explore.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Done here.->Location_The_Facility]]<p>The floor's empty, the door shut. No one watches an asset in its own quarters. She uses the hour the way the company never told her to — drilling the body they built, sharpening it past the spec on her file. It's the one thing here that's only hers.</p>
<<set $game_state.current_canvas = "43d0dc3d-c28b-4505-a1e6-f45fc3e324e6">>
<<set $game_state.current_node = "d9e54901-9581-4466-8725-0c77390a4c5a">>
<<script>>setup.markCanvasTriggered("43d0dc3d-c28b-4505-a1e6-f45fc3e324e6");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_train.base") === -1) { vn.push("activity_train.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "lt", "value": 30}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc0') ? '' : 'unlocked-choice'">
<<link "Combat drill." "Canvas_activity_train_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "fighting", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Combat drill.">Combat drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 30}, {"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "lt", "value": 50}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc1') ? '' : 'unlocked-choice'">
<<link "Combat drill." "Canvas_activity_train_Node_2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "fighting", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Combat drill.">Combat drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "yard_caught_once", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 30}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc2') ? '' : 'unlocked-choice'">
<<link "Stealth drill." "Canvas_activity_train_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "stealth", "add", 3.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc2");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Stealth drill.">Stealth drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "yard_caught_once", "operator": "is_true"}, {"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 30}, {"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 50}]})>>
<<if setup.checkCostsAffordable([{"trait": "energy", "value": 15}])>>
<span @class="setup.isChoiceVisited('43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc3') ? '' : 'unlocked-choice'">
<<link "Stealth drill." "Canvas_activity_train_Node_3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "stealth", "add", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("43d0dc3d-c28b-4505-a1e6-f45fc3e324e6:cc3");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Stealth drill.">Stealth drill. (<<= setup.getCostBlockedMessage([{"trait": "energy", "value": 15}])>>)</span><br>
<</if>>
<</if>>
<<link "Enough for now." "Location_Wren's_Room">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "lt", "value": 25}]})>>
<p>She runs the forms against the empty air — strikes, breaks, the throw that ends a man. Her body obeys a half-beat slow, the way a tool obeys before it's been worked hard. Clumsy still. But the half-beat is shorter than it was yesterday.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "fighting", "operator": "gte", "value": 25}]})>>
<p>No hesitation now. The forms come fast and flat and final — strike, break, the throw — each one landing where a throat or a knee would be. Whatever they built her for, this wasn't on the spec. She's better than the file says. She keeps that to herself.</p>
<</if>>
<<set $game_state.current_canvas = "43d0dc3d-c28b-4505-a1e6-f45fc3e324e6">>
<<set $game_state.current_node = "7b45b793-ffc1-4ae3-8933-8ac407b2ee5f">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_train.combat") === -1) { vn.push("activity_train.combat"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Rack it. Catch your breath.->Location_Wren's_Room]]<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "lt", "value": 25}]})>>
<p>She practices the small things — weight rolled onto the outside of the foot, breath dropped low, the timing of a turned back. Her own joints betray her with a tick of sound she'd never have caught a week ago. She catches it now. That's the start of it.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "stealth", "operator": "gte", "value": 25}]})>>
<p>She moves through her own quarters and the room doesn't register her — no sound, no shift of air, nothing for a camera to hold. A thing that can't be seen can go where it isn't allowed. She doesn't let the thought finish.</p>
<</if>>
<<set $game_state.current_canvas = "43d0dc3d-c28b-4505-a1e6-f45fc3e324e6">>
<<set $game_state.current_node = "3f583505-4fc8-4d52-911e-8ddd5bab4120">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_train.stealth") === -1) { vn.push("activity_train.stealth"); }<</script>>
<<script>>advanceTime(120);<</script>>
[[Ease off. Settle.->Location_Wren's_Room]]<img src="./videos/activities/chip_view.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She opens the store the company gave her for what it calls recovered assets — the place a restored memory would go, if she ever recovered one. It's empty. It has always been empty.</p><p>There is one slot in it, greyed out, labelled in a hand that isn't the company's: FRAGMENT 01 — LOCKED. She doesn't remember making the label. She closes the store — and notices she's opened it more often than the task log would ever justify. She closes that thought too.</p>
<<set $game_state.current_canvas = "6185ae0b-2063-40d6-9e8a-28b141c909c5">>
<<set $game_state.current_node = "1837c019-52de-469e-a8c7-1c0191416158">>
<<script>>setup.markCanvasTriggered("6185ae0b-2063-40d6-9e8a-28b141c909c5");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_chip_view.base") === -1) { vn.push("canvas_chip_view.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Close it.->Location_The_Cradle]]<p>She bleeds the cradle's charge into the emitter's cell until it reads full — three. Then she pockets it and goes.</p>
<<set $game_state.current_canvas = "a6d8f639-ff49-48bd-a900-22ebeb508c89">>
<<set $game_state.current_node = "186caf13-9069-4d4f-a676-fb9247c5e361">>
<<script>>setup.markCanvasTriggered("a6d8f639-ff49-48bd-a900-22ebeb508c89");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_recharge_emitter.base") === -1) { vn.push("activity_recharge_emitter.base"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "arousal_charge", "set", 3.0, false, null);setup.showEffectNotification();<</script>>
[[Done.->Location_The_Cradle]]<p>A few minutes on the feed line and the drain rebuilds its one shot. Loaded. She unhooks.</p>
<<set $game_state.current_canvas = "1acb2cef-1c24-44de-8277-cb5d31a31b5b">>
<<set $game_state.current_node = "0abe7881-544c-40ad-8f6d-223c4cbb2a86">>
<<script>>setup.markCanvasTriggered("1acb2cef-1c24-44de-8277-cb5d31a31b5b");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_recharge_drain.base") === -1) { vn.push("activity_recharge_drain.base"); }<</script>>
<<script>>advanceTime(60);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
[[Done.->Location_The_Cradle]]<p>Not a full night — just a couple hours hooked to the cradle's feed line, eyes open. Her cells pull back to full. Then she unhooks, the day still going.</p>
<<set $game_state.current_canvas = "50cd323e-057c-4784-8aef-f128016a164a">>
<<set $game_state.current_node = "2f355353-c268-430e-b0c1-46bfe11b2300">>
<<script>>setup.markCanvasTriggered("50cd323e-057c-4784-8aef-f128016a164a");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_charge_up.base") === -1) { vn.push("activity_charge_up.base"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
[[Unhook. Back to it.->Location_The_Cradle]]<p>She settles back into the cradle. The contacts find their points along her spine, cold and familiar. The room dims as the charge takes her down, and the quiet closes over the top of her like water. When it lets her go it's morning again — a new day, the charge full, the hours she spent out there walked back into her.</p>
<<set $game_state.current_canvas = "0be2a948-4570-427e-a263-e1aae0a40325">>
<<set $game_state.current_node = "9ad57c4c-7c09-4a44-87ca-3ebfa2fae9b3">>
<<script>>setup.markCanvasTriggered("0be2a948-4570-427e-a263-e1aae0a40325");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_recharge.base") === -1) { vn.push("activity_recharge.base"); }<</script>>
<<script>>advanceTime(540);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
[[Power up.->Location_The_Cradle]]<p>She calls the car. The docks recede behind her; the towers climb back up until the glass swallows the sky again. Home, if that's the word for the place that owns you.</p>
<<set $game_state.current_canvas = "261f7460-63ab-4960-9af2-e96911905a4c">>
<<set $game_state.current_node = "0694ef4d-c72b-4e9e-bd38-57171968add2">>
<<script>>setup.markCanvasTriggered("261f7460-63ab-4960-9af2-e96911905a4c");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_spire.base") === -1) { vn.push("activity_travel_to_spire.base"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Ride up to the Plaza.->Location_Spire_Plaza]]<p>A company car idles at the curb, waiting to run her down to the docks.</p>
<<set $game_state.current_canvas = "d65d8e37-716c-491a-aec7-6981b934169d">>
<<set $game_state.current_node = "2bb01042-4281-4037-8b62-e50585d4a7c4">>
<<script>>setup.markCanvasTriggered("d65d8e37-716c-491a-aec7-6981b934169d");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_reach.base") === -1) { vn.push("activity_travel_to_reach.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "hygiene", "operator": "gte", "value": 40}]})>>
<span @class="setup.isChoiceVisited('d65d8e37-716c-491a-aec7-6981b934169d:cc0') ? '' : 'unlocked-choice'">
<<link "Ride down to the Waterfront." "Canvas_activity_travel_to_reach_Node_2">><<script>>setup.markChoiceVisited("d65d8e37-716c-491a-aec7-6981b934169d:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<<else>>
<span class="locked-choice" title="Not like this — you're filthy, and the cover won't hold with the docks able to smell it on you. Wash up first.">Not like this — you're filthy, and the cover won't hold with the docks able to smell it on you. Wash up first.</span><br>
<</if>>
<<if not ((setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "hygiene", "operator": "gte", "value": 40}]})))>>
<<script>>
try {
window._engineNoExitsDiag_activity_travel_to_reach = [{"text": "Ride down to the Waterfront.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "hygiene", "operator": "gte", "value": 40}]}}];
if (typeof console !== 'undefined' && console.warn) {
console.warn('[engine] no exit choices satisfied', {
canvas: "activity_travel_to_reach",
choices: window._engineNoExitsDiag_activity_travel_to_reach,
flags: State.variables.flags,
player_traits: (State.variables.player || {}).core_traits
});
}
} catch(e) {}
<</script>>
<<if $flags.debug_mode>>
<<set _diagChoices to window._engineNoExitsDiag_activity_travel_to_reach>>
<div class="engine-diag-no-exits">
<div class="engine-diag-header">⚠️ DEV: No exit choices satisfied — canvas activity_travel_to_reach</div>
<div class="engine-diag-hint">All exits are conditional and none of their gates currently evaluate true. Player will use the Continue link below to escape, but the visit fires no effects. Likely authoring gap — add a fallback choice or broaden a condition.</div>
<<for _i to 0; _i lt _diagChoices.length; _i++>>
<<set _ch to _diagChoices[_i]>>
<div class="engine-diag-choice">
<div class="engine-diag-choice-text">Choice: "<<print _ch.text>>"</div>
<div class="engine-diag-choice-logic">Combined as: <<print _ch.conditions.logic>></div>
<ul class="engine-diag-items">
<<for _j to 0; _j lt _ch.conditions.items.length; _j++>>
<<set _it to _ch.conditions.items[_j]>>
<<set _single to {version: "1.0", logic: "AND", items: [_it]}>>
<<set _ok to setup.triggerConditionsSatisfied(_single)>>
<li class="<<print _ok ? 'engine-diag-pass' : 'engine-diag-fail'>>"><<if _ok>>✓<<else>>✗<</if>> <<print JSON.stringify(_it)>></li>
<</for>>
</ul>
</div>
<</for>>
</div>
<</if>>
[[Continue->Location_Spire_Plaza]]
<</if>>
<</nobr>><p>She gives the docks address and the car pulls out without a word. The Spire's glass falls away behind her; the city gets lower, wetter, dirtier the whole way down.</p>
<<set $game_state.current_canvas = "d65d8e37-716c-491a-aec7-6981b934169d">>
<<set $game_state.current_node = "58df7cca-0a41-4ab6-9db3-687e112a2917">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_reach.riding") === -1) { vn.push("activity_travel_to_reach.riding"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Down to the Waterfront.->Location_The_Waterfront]]<img src="./videos/scenes/facility_match.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>In the wreckage of a containment rig — half-melted, stamped with a maker's mark — she finds a component she recognizes. Not from a file. From herself. The same part, the same stamp, sits inside her own chassis where the lab installed it; she has felt the engineers' hands on its twin. The gear Renner supplied to cage the things that feel was built in the same shop, to the same spec, as the thing she is.</p><p>She notes it the way she notes a serial number matching a serial number. "That's mine," she says, flat, to the burned and empty room — and doesn't know why the words come out cold and certain and terrible, or why she can't make herself say them again.</p>
<<set $game_state.current_canvas = "b2efc6cd-24b6-418c-b4c6-502fce50d51c">>
<<set $game_state.current_node = "ddfcc4b2-fa0c-446e-86ce-98b4507d9809">>
<<script>>setup.markCanvasTriggered("b2efc6cd-24b6-418c-b4c6-502fce50d51c");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("inv_facility_match.base") === -1) { vn.push("inv_facility_match.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "facility_match_seen", "set");<</script>>
[[Log it. Move on.->Location_The_Facility]]<img src="./videos/scenes/glitch_ii.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Tonight it doesn't wait for the charge. She's barely in the cradle before it takes her — the wetness, the fragment, but more now, like a door that used to be locked has warped in its frame. The voice almost resolves. A face almost forms.</p><p>And for the first time she gets a whole feeling instead of a flicker: that she was loved once, completely, by someone, before all of this — and that the company knew, and took it, and built the taking so deep into her that she thanks them for it every morning. Then the charge slams the door, and she's standing in the grey quiet with her face wet and the catechism in her mouth, and for one second she'll spend the rest of her life trying to un-have, the catechism doesn't fit anymore. She files it under nothing. Nothing is getting full too.</p>
<<set $game_state.current_canvas = "c0d80682-917b-4caa-9d66-fe73feb742d9">>
<<set $game_state.current_node = "35b0cc19-0a37-42f3-8470-b55300f4def4">>
<<script>>setup.markCanvasTriggered("c0d80682-917b-4caa-9d66-fe73feb742d9");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_glitch_recurrence_ii.base") === -1) { vn.push("canvas_glitch_recurrence_ii.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "glitch_ii_seen", "set");<</script>>
[[Power down anyway.->Location_The_Cradle]]<img src="./videos/scenes/glitch_i.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She powers down like every night, except tonight the leak comes back. The tears first — her face wet again, no order behind it — and then the fragment, longer this time, almost holding: the warmth, the voice with her name folded into it, the someone who was glad to look at her. She reaches for it the way you reach for a word on the tip of your tongue, and it's gone, and the reaching leaves a mark. She files it under fatigue. The fatigue file is getting full.</p>
<<set $game_state.current_canvas = "9b2ea2f7-634b-491a-8551-28e0da0c0a9f">>
<<set $game_state.current_node = "843d55aa-6076-43f2-92c9-5d250921a0c2">>
<<script>>setup.markCanvasTriggered("9b2ea2f7-634b-491a-8551-28e0da0c0a9f");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_glitch_recurrence_i.base") === -1) { vn.push("canvas_glitch_recurrence_i.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "glitch_i_seen", "set");<</script>>
[[Let the charge take you.->Location_The_Cradle]]<img src="./videos/scenes/opening_morning.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The charge releases her at oh-six-hundred. Full. New day. The water is gone from her face; the fragment has filed itself back under the quiet where she can't reach it, and she's grateful for that the way you're grateful for a wound scabbing over.</p><span id="cascade-814c4652-d997-46fd-b853-61b9babd33d0-beat-0"><p>Her phone has a dossier on it, pushed overnight, Mercer's authority stamped on the header. She reads it standing in the cradle.</p><span id="cascade-814c4652-d997-46fd-b853-61b9babd33d0-beat-1"><<linkreplace "Read it.">><p>A Vance asset-facility in the Reach burned to the ground three weeks ago — Cain's work. The man who supplied its equipment is named Renner: a quartermaster, broke now, blacklisted, drinking himself down at a dock bar called the Anchor and clawing at the wreck of his business. He knows what the facility was and where the trail goes. He does not know what she is.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/mercer.jpg" alt="Mercer" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="dialog-content"><strong>Mercer:</strong> Get inside his life. You know how.</div></div><p>There's a cover allowance stamped under the dossier — company credits, for drink and whatever else buys a way into a man's evening. The kind of money she's meant to spend like she intends to come home with him.</p><p>A kit's logged with it — couriered down to her quarters, cheap and worn, the part she's to play. Put it on before the Anchor. She does not walk up to a man like Renner wearing anything the Tower would recognize.</p><span id="cascade-814c4652-d997-46fd-b853-61b9babd33d0-beat-2"><<linkreplace "The part.">><p>She knows the shape of it without being told — not a disguise so much as a register: cheap, willing, a little desperate, the kind of hire a wrecked man doesn't look at twice until she makes him. When she puts it on she'll put it on the way she puts on everything, from the outside. Wren goes under. Whoever Renner needs comes up.</p><span id="cascade-814c4652-d997-46fd-b853-61b9babd33d0-beat-3"><<linkreplace "Out the door.">><img src="./videos/scenes/spire_plaza_reveal.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>She rides down through a Tower she has never once left on her own legs for anything but an order. The atrium doors read her face and open. And then she's outside, on Spire Plaza, the city laid out wet and enormous and unowned in front of her — and for one second, with no cover on her yet, nothing between her and it, something that isn't in any of her files stands very still in her chest and looks at all of it.</p>[[Step into the city.->Location_Spire_Plaza]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "29e61206-afcc-42e6-b948-09964cd9e628">>
<<set $game_state.current_node = "9c0e1815-a6a9-4d43-8c69-d29118e4c16f">>
<<script>>setup.markCanvasTriggered("29e61206-afcc-42e6-b948-09964cd9e628");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_morning.morning") === -1) { vn.push("canvas_opening_morning.morning"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "morning_done", "set");setup.applyAndNotifyFlag("player", null, "opening_done", "set");setup.applyAndNotifyFlag("player", null, "mission_1_active", "set");<</script>>
<<script>>setup.pendingEffects = setup.pendingEffects || [];setup.addToWardrobe("cover_dockhand");setup.showEffectNotification();<</script>><img src="./videos/scenes/opening_cradle.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The asset level is quiet the way a kept place is quiet. She comes down through the Tower to her own floor, her own door, her own room with nothing on the walls. The cradle waits in the corner.</p><span id="cascade-b8fc1b90-8a53-4c78-a60c-c8e6b2b7646f-beat-0"><p>She steps back into it. The contacts find the points along her spine, cold and familiar. The room dims as the charge takes hold, and the day's log begins to write itself down into the quiet. Whatever the day spent out of her — every street walked, every shift worked — the cradle gives it back by morning; let the charge run too low out there and the body starts to fail in ways a man's hands would notice.</p><span id="cascade-b8fc1b90-8a53-4c78-a60c-c8e6b2b7646f-beat-1"><<linkreplace "...">><p>Then her face is wet. She registers it the way she'd register a leak in a seal — water, on her cheeks, with no source she can name and no order that called for it. She holds perfectly still and runs every diagnostic she has, and every one comes back clean. She files the water under malfunction and resolves to have it looked at, and does not have it looked at, because some part of her already knows they'd take it out.</p><span id="cascade-b8fc1b90-8a53-4c78-a60c-c8e6b2b7646f-beat-2"><<linkreplace "...">><img src="./videos/scenes/cradle_fragment.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>And under the water, for less than a second, something else. Not a thought — she doesn't have thoughts that aren't tasks — but a fragment: warmth on the side of her face that isn't the cradle's, a low sound that might be a voice with her name folded somewhere inside it, a sense of being looked at by someone who was glad to be looking. Then the charge swallows it and it's gone, and the place where it was aches like a socket with the tooth pulled, and she has no word for the ache, so she has no word for the loss.</p><span id="cascade-b8fc1b90-8a53-4c78-a60c-c8e6b2b7646f-beat-3"><<linkreplace "Recite it.">><p>She does what she does when the quiet gets loud. She recites.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="You" onerror="this.style.display='none';this.parentElement.innerHTML='<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="35" r="20" fill="#6c757d"/><ellipse cx="50" cy="85" rx="35" ry="25" fill="#6c757d"/></svg>';" /></div><div class="thought-bubble-content"><em>💭 You are thinking:</em> I was nothing. The company found me and made me of use. The Chairman keeps me running. I owe them the body and the work and the obedience. I exist to serve. That is all I have ever been.</div></div><p>The words settle her. They always do. She does not notice that the water is still on her face while she tells herself she is a thing that cannot weep.</p><span id="cascade-b8fc1b90-8a53-4c78-a60c-c8e6b2b7646f-beat-4"><<linkreplace "The gift.">><p>There is one thing she has that the company gave her and the units don't. When a man uses her the lowest way — takes her ass and spends himself in it — for a few minutes after, he belongs to her. He'll answer anything, do anything, wake remembering none of it. It's how she works the men she's sent to work. It's the one thing that is, in any sense, hers.</p><p>It has never once worked on Mercer. He's had her every way there is, that one included. Nothing passes; the controls never come to her; he stays the hand on the leash. She has never asked why. The not-asking is so complete she doesn't notice it has a shape — a hole exactly the size of a question she isn't allowed to hold.</p><span id="cascade-b8fc1b90-8a53-4c78-a60c-c8e6b2b7646f-beat-5"><<linkreplace "Power down.">><p>She lets the catechism be the last thing. She promises the quiet she'll do better — fire next time, feel less, want nothing — and the charge pulls her down past where the water and the fragment and the question all live, into the dark that is supposed to be empty and, lately, isn't quite.</p>[[Sleep.->Location_The_Cradle]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "a9edd87b-1a21-4ccc-a079-2e2602ad7c1a">>
<<set $game_state.current_node = "326ec910-ac16-48c4-a323-d20d80c1b37f">>
<<script>>setup.markCanvasTriggered("a9edd87b-1a21-4ccc-a079-2e2602ad7c1a");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("canvas_opening_night.night") === -1) { vn.push("canvas_opening_night.night"); }<</script>>
<<script>>advanceTime(540);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "night_done", "set");<</script>><h2>Missing Media Files</h2>
<p>No missing media files found.</p>
<<link "← Back">><<run setup.smartBack()>><</link>>