Your browser lacks required capabilities. Please upgrade it or switch to another to continue.
Loading…
{
"name": "Vesper",
"ifid": "7032a282-b49c-5de1-9ef0-8553b88d1f63",
"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, "names_known": 0, "core_strain": 0, "cell_door": 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 = {"npc_mercer": {"name": "Mercer", "portrait": "mercer.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_renner": {"name": "Renner", "portrait": "renner.jpg", "core_traits": {"relation": 0, "corruption": 0, "arousal": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_sol": {"name": "Sol", "portrait": "sol.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_bastien": {"name": "Bastien", "portrait": "bastien.jpg", "core_traits": {"relation": 0, "corruption": 0, "arousal": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_rue": {"name": "Rue", "portrait": "rue.jpg", "core_traits": {"relation": 0}, "flags": {}, "schedule": [], "relationship": ""}, "npc_marsh": {"name": "Marsh", "portrait": "marsh.jpg", "core_traits": {"relation": 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_office]]
[[✗ 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="waitTime(10)" title="Advance 10 minutes">></button> | <button class="time-btn" onclick="waitTime(60)" title="Advance 1 hour">>></button> | <button class="time-btn" onclick="waitTime(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 "playerPortrait">>
<<if setup.player_portrait_enabled and setup.player_portrait>>
<<set _pimg to setup.getPlayerPortrait()>>
<<if _pimg>>
<div class="sidebar-player-portrait"><img @src="_pimg" @alt="$player.name" onerror="this.style.display='none';"></div>
<</if>>
<</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>>
<<playerPortrait>>
<<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; }
/* State-reactive player portrait (opt-in) — the TOP-MOST sidebar image. The theme's
global img rule does not reach sidebar imgs, so without this the raw image renders at
natural size and overflows the narrow sidebar (you'd see a background edge, not the
face). Constrain to the sidebar width and frame it as a 3:4 portrait crop centred a bit
high, so a square or tall source shows the face/torso rather than the legs/background. */
.sidebar-player-portrait {
margin: 0 0 12px 0;
text-align: center;
}
.sidebar-player-portrait img {
display: block;
width: 100%;
max-width: 100%;
height: auto;
aspect-ratio: 3 / 4;
object-fit: cover;
object-position: 50% 18%;
border-radius: 6px;
}
/* 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-b2-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-b2-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-b2-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_punishment">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Say nothing." "StartingCanvas_canvas_opening_office_Node_punishment">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Tell him, flatly, that you froze." "StartingCanvas_canvas_opening_office_Node_punishment">><<script>>advanceTime(3);<</script>><</link>><br>
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "canvas_opening_office">>
<<set $game_state.current_node = "canvas_opening_office.office">>
<<script>>setup.markCanvasTriggered("canvas_opening_office");<</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-b3-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-b3-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-b3-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-b3-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-b3-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-b3-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="Wren" 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><<print $player.name>>:</strong> Thank you, sir.</div></div><span id="cascade-b3-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-b3-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_cradle]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "canvas_opening_office">>
<<set $game_state.current_node = "canvas_opening_office.punishment">>
<<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_atrium]]<br>
[[Mercer's Penthouse->Location_penthouse]]<br>
[[Wren's Floor->Location_wren_floor]]<br>
[[Wren's Room->Location_wren_room]]<br>
[[The Cradle->Location_cradle]]<br>
[[The Waterfront->Location_the_waterfront]]<br>
[[The Anchor->Location_the_anchor]]<br>
[[Renner's Depot->Location_renner_depot]]<br>
[[The Facility->Location_facility_ruins]]<br>
[[The Site->Location_the_site]]<br>
[[The Burned Yard->Location_renner_burned_yard]]<br>
[[The Underworld Gate->Location_underworld_gate]]<br>
[[The Underground->Location_underworld_strip]]<br>
[[The House->Location_underworld_brothel]]<br>
[[The Pit->Location_underworld_pit]]<br>
[[The Black Market->Location_underworld_market]]<br>
[[The Undertow->Location_underworld_bar]]<br>
[[The Crew's Rooms->Location_crew_den]]<br>
[[The Room->Location_captive_room]]<br>
[[The Door->Location_captive_door]]<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 = "spire_plaza">>
<<if not $game_state.visited_locations.includes("spire_plaza")>>
<<set $game_state.visited_locations.push("spire_plaza")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("spire_plaza")>><<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("spire_plaza")>>
<<= setup.renderSoloActivities("spire_plaza")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("atrium")>><a class="location-card link-internal" data-passage="Location_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("atrium")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("atrium")>><<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 = "atrium">>
<<if not $game_state.visited_locations.includes("atrium")>>
<<set $game_state.visited_locations.push("atrium")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("atrium")>><<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("atrium")>>
<<= setup.renderSoloActivities("atrium")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("penthouse")>><a class="location-card link-internal" data-passage="Location_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("penthouse")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("penthouse")>><<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_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("wren_floor")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("wren_floor")>><<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 = "penthouse">>
<<if not $game_state.visited_locations.includes("penthouse")>>
<<set $game_state.visited_locations.push("penthouse")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("penthouse")>><<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("penthouse")>>
<<= setup.renderSoloActivities("penthouse")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave Mercer's Penthouse->Location_atrium]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "wren_floor">>
<<if not $game_state.visited_locations.includes("wren_floor")>>
<<set $game_state.visited_locations.push("wren_floor")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("wren_floor")>><<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("wren_floor")>>
<<= setup.renderSoloActivities("wren_floor")>>
<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_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("wren_room")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("wren_room")>><<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_atrium]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "wren_room">>
<<if not $game_state.visited_locations.includes("wren_room")>>
<<set $game_state.visited_locations.push("wren_room")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("wren_room")>><<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("wren_room")>>
<<= setup.renderSoloActivities("wren_room")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("cradle")>><a class="location-card link-internal" data-passage="Location_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("cradle")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("cradle")>><<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_floor]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "cradle">>
<<if not $game_state.visited_locations.includes("cradle")>>
<<set $game_state.visited_locations.push("cradle")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("cradle")>><<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("cradle")>>
<<= setup.renderSoloActivities("cradle")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Cradle->Location_wren_room]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "the_waterfront">>
<<if not $game_state.visited_locations.includes("the_waterfront")>>
<<set $game_state.visited_locations.push("the_waterfront")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("the_waterfront")>><<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("the_waterfront")>>
<<= setup.renderSoloActivities("the_waterfront")>>
<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("the_anchor")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("the_anchor")>><<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_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("renner_depot")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("renner_depot")>><<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_facility_ruins"><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("facility_ruins")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("facility_ruins")>><<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_renner_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("renner_burned_yard")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("renner_burned_yard")>><<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>></div><</nobr>>
</div>
<</if>><<nobr>>
<<set $player.current_location = "the_anchor">>
<<if not $game_state.visited_locations.includes("the_anchor")>>
<<set $game_state.visited_locations.push("the_anchor")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("the_anchor")>><<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("the_anchor")>>
<<= setup.renderSoloActivities("the_anchor")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Anchor->Location_the_waterfront]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "renner_depot">>
<<if not $game_state.visited_locations.includes("renner_depot")>>
<<set $game_state.visited_locations.push("renner_depot")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("renner_depot")>><<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("renner_depot")>>
<<= setup.renderSoloActivities("renner_depot")>>
<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 = "facility_ruins">>
<<if not $game_state.visited_locations.includes("facility_ruins")>>
<<set $game_state.visited_locations.push("facility_ruins")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("facility_ruins")>><<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("facility_ruins")>>
<<= setup.renderSoloActivities("facility_ruins")>>
<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 = "the_site">>
<<if not $game_state.visited_locations.includes("the_site")>>
<<set $game_state.visited_locations.push("the_site")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("the_site")>><<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("the_site")>>
<<= setup.renderSoloActivities("the_site")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Site->Location_underworld_strip]]<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_underworld_strip]]
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "renner_hired", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "renner_burned_yard">>
<<if not $game_state.visited_locations.includes("renner_burned_yard")>>
<<set $game_state.visited_locations.push("renner_burned_yard")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("renner_burned_yard")>><<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("renner_burned_yard")>>
<<= setup.renderSoloActivities("renner_burned_yard")>>
<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>><<nobr>>
<<set $player.current_location = "underworld_gate">>
<<if not $game_state.visited_locations.includes("underworld_gate")>>
<<set $game_state.visited_locations.push("underworld_gate")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_gate")>><<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("underworld_gate")>>
<<= setup.renderSoloActivities("underworld_gate")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Underworld Gate->Location_the_waterfront]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_strip">>
<<if not $game_state.visited_locations.includes("underworld_strip")>>
<<set $game_state.visited_locations.push("underworld_strip")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_strip")>><<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("underworld_strip")>>
<<= setup.renderSoloActivities("underworld_strip")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("underworld_bar")>><a class="location-card link-internal" data-passage="Location_underworld_bar"><div class="location-card-image" style="background-image: url('./videos/locations/underworld_bar.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Undertow</span><<if setup.getLocationCostTag("underworld_bar")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("underworld_bar")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("underworld_bar")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_bar")>><<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_bar.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Undertow</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("underworld_bar")>></span></div></div><</if>><<if setup.navDestUnlocked("underworld_brothel")>><a class="location-card link-internal" data-passage="Location_underworld_brothel"><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("underworld_brothel")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_brothel")>><<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_underworld_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("underworld_pit")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_pit")>><<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_underworld_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("underworld_market")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("underworld_market")>><<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("crew_den")>><a class="location-card link-internal" data-passage="Location_crew_den"><div class="location-card-image" style="background-image: url('./videos/locations/crew_den.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Crew's Rooms</span><<if setup.getLocationCostTag("crew_den")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("crew_den")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("crew_den")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("crew_den")>><<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/crew_den.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Crew's Rooms</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("crew_den")>></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("the_site")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("the_site")>><<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 = "underworld_brothel">>
<<if not $game_state.visited_locations.includes("underworld_brothel")>>
<<set $game_state.visited_locations.push("underworld_brothel")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_brothel")>><<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("underworld_brothel")>>
<<= setup.renderSoloActivities("underworld_brothel")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The House->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_pit">>
<<if not $game_state.visited_locations.includes("underworld_pit")>>
<<set $game_state.visited_locations.push("underworld_pit")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_pit")>><<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("underworld_pit")>>
<<= setup.renderSoloActivities("underworld_pit")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Pit->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_market">>
<<if not $game_state.visited_locations.includes("underworld_market")>>
<<set $game_state.visited_locations.push("underworld_market")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_market")>><<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("underworld_market")>>
<<= setup.renderSoloActivities("underworld_market")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Black Market->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<nobr>>
<<set $player.current_location = "underworld_bar">>
<<if not $game_state.visited_locations.includes("underworld_bar")>>
<<set $game_state.visited_locations.push("underworld_bar")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("underworld_bar")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Undertow</h2>
<p>A bar off the strip, quieter than the rest of the underground — low light, older money, men who talk in the corners and go still when you pass. It runs cleaner than a place this deep has any right to, and it plainly belongs to someone; nobody says who. Sol works the taps and hears all of it.</p>
<<= setup.renderNpcPortraits("underworld_bar")>>
<<= setup.renderSoloActivities("underworld_bar")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Undertow->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_true"}]})>><<nobr>>
<<set $player.current_location = "crew_den">>
<<if not $game_state.visited_locations.includes("crew_den")>>
<<set $game_state.visited_locations.push("crew_den")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("crew_den")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Crew's Rooms</h2>
<p>A dead man's rooms down past the market, off a stair nobody's cleared — where Doss and Rourke and the rest holed up before Renner put them in the ground. The door still answers to a key under the third stair, and nobody's been back. Nobody but her.</p>
<<= setup.renderNpcPortraits("crew_den")>>
<<= setup.renderSoloActivities("crew_den")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Crew's Rooms->Location_underworld_strip]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Crew's Rooms</h2>
<p class="entry-blocked-narrative">There's a crew that holed up down past the market — but she doesn't have the address yet. Marsh has it.</p>
[[Go back->Location_underworld_strip]]
<</if>><<nobr>>
<<set $player.current_location = "captive_room">>
<<if not $game_state.visited_locations.includes("captive_room")>>
<<set $game_state.visited_locations.push("captive_room")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("captive_room")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Room</h2>
<p>Concrete, painted once, a long time ago. A bed bolted to the floor and a chair that isn't. A table with nothing on it. A drain in the middle of the floor, because whoever built this expected to hose it out. The light stays on. There is a door.</p>
<<= setup.renderNpcPortraits("captive_room")>>
<<= setup.renderSoloActivities("captive_room")>>
<div class="location-navigation">
<<nobr>><div class="location-nav-grid"><<if setup.navDestUnlocked("captive_door")>><a class="location-card link-internal" data-passage="Location_captive_door"><div class="location-card-image" style="background-image: url('./videos/locations/captive_door.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Door</span><<if setup.getLocationCostTag("captive_door")>><span class="nav-cost-tag"><<= setup.getLocationCostTag("captive_door")>></span><</if>><div class="location-card-indicators"><<if setup.locationHasNewCanvases("captive_door")>><span class="nav-new-badge">NEW</span><</if>><<for _npc range setup.getNpcsPresentAtLocation("captive_door")>><<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/captive_door.jpg')"></div><div class="location-card-content"><span class="location-card-name">The Door</span><span class="nav-locked-reason"><<= setup.navDestBlockedReason("captive_door")>></span></div></div><</if>></div><</nobr>>
</div>
<</if>><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "trait", "subject": "player", "trait_key": "cell_door", "operator": "gte", "value": 1}]})>><<nobr>>
<<set $player.current_location = "captive_door">>
<<if not $game_state.visited_locations.includes("captive_door")>>
<<set $game_state.visited_locations.push("captive_door")>>
<</if>>
<</nobr>><<set _autoFire = setup.getStoryCanvasRedirect("captive_door")>><<if _autoFire>><<goto _autoFire>><<else>><h2>The Door</h2>
<p>Steel, and newer than the room. It opens when they want it to.</p>
<<= setup.renderNpcPortraits("captive_door")>>
<<= setup.renderSoloActivities("captive_door")>>
<div class="location-navigation">
<div class="location-nav-exits">
[[Leave The Door->Location_captive_room]]<br>
</div>
</div>
<</if>><<else>>
<h2>The Door</h2>
<p class="entry-blocked-narrative">The door does not open for her.</p>
[[Go back->Location_captive_room]]
<</if>>
<!-- STORY CANVAS PASSAGES --><p>Dev jump: seeding a near-broken captivity state and dropping into the cell. Lie down once to tip the break.</p>
<<set $game_state.current_canvas = "dev_jump_captivity_break">>
<<set $game_state.current_node = "dev_jump_captivity_break.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_captivity_break");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_captivity_break.seed") === -1) { vn.push("dev_jump_captivity_break.seed"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "core_strain", "set", 95.0, false, null);setup.applyAndNotifyTrait("player", null, "energy", "set", 30.0, false, null);setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);setup.showEffectNotification();<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");setup.applyAndNotifyFlag("player", null, "renner_drained", "set");setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");setup.applyAndNotifyFlag("player", null, "crew_known", "set");setup.applyAndNotifyFlag("player", null, "bastien_found", "set");setup.applyAndNotifyFlag("player", null, "bastien_revealed", "set");setup.applyAndNotifyFlag("player", null, "captivity_entered", "set");<</script>>
[[Into the cell.->Location_captive_room]]<p>Dev jump: seeding the hunt chain and dropping into the grab.</p>
<<set $game_state.current_canvas = "dev_jump_bastien_grab">>
<<set $game_state.current_node = "dev_jump_bastien_grab.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_bastien_grab");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_bastien_grab.seed") === -1) { vn.push("dev_jump_bastien_grab.seed"); }<</script>>
<<nobr>>
<<link "Jump to the grab" "Canvas_hunt_the_grab_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "set", 60.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_fucked_once", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_anal_once", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_drained", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_leads_extracted", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "brothel_hired", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "crew_known", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "bastien_found", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>Dev jump: seeding the drain state and dropping into the payload.</p>
<<set $game_state.current_canvas = "dev_jump_renner_drain">>
<<set $game_state.current_node = "dev_jump_renner_drain.seed">>
<<script>>setup.markCanvasTriggered("dev_jump_renner_drain");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("dev_jump_renner_drain.seed") === -1) { vn.push("dev_jump_renner_drain.seed"); }<</script>>
<<nobr>>
<<link "Jump to the drain" "Canvas_renner_control_canvas_Node_intro">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drains_done", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("npc", "npc_renner", "corruption", "set", 60.0, false, null);<</script>><<script>>setup.applyAndNotifyFlag("player", null, "opening_done", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_fucked_once", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "renner_anal_once", "set");<</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>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She comes back a little at a time, into a room that has changed. The men are gone. The near door stands open on an empty corridor. The frame she was strapped to is on its side where somebody knocked it going out fast. She is on the floor by the grate they meant to hose her over, and her left leg will not answer, and the light under her skin is out — properly out, not flickering, dark the way a cooled coal is dark.</p><p>Through the wall, two men are arguing.</p><p>One of them is Bastien. She would know that voice anywhere now, and it has not changed — flat, unhurried, reasonable, a man explaining a position he sees no reason to raise his voice about. The other voice she has never heard in her life, and it goes through her wrong, because it is furious, and the fury has something under it she has no file for. It takes her a long muddled moment to find the word for the thing under it, and the word she finds is grief, and that makes no sense at all, because nobody has ever once been angry on her account, let alone torn up about it.</p><p>She is too far gone to hold any of it. The words come apart before they reach her. Four get through the whole of it and no more. One is Bastien's, and it is a word she knows the shape of because it is a word men use about her: an ownership word, said the way you'd say the price of a thing. And three come across in the other voice, and one of those three is a name — she is nearly certain it is a name — and it goes into her somewhere below the amputation and fits, the way a key fits the one lock in a house that was left, a lock she did not know she had, and she cannot for her life say what the name was or why a stranger saying it should make the empty places in her ache like frostbite coming warm. And the last word, in that same broken voice, is one that has no business anywhere near a room like this, a word you say over a grave and not over a thing you are haggling for. She reaches for all four of them and they slide out of her hands.</p><p>The arguing stops. Not trails off — stops, the way it stops when one man has said the thing there is no answer to.</p><p>Then the door — the far one, off its hinges already — and he comes in, and she cannot make him hold still. He is a shape against the bad light and a smell of old smoke, and he crosses the room fast and goes down on one knee beside her, and she flinches, because everyone who has knelt beside her in this room has done it to use her. He doesn't. He turns her, careful of the dead leg, the way you'd turn something you already knew the weight of, and he finds the seam at the base of her spine without looking for it — without searching, his hand going straight to it in the dark like a hand going to its own pocket — and he puts the weight back.</p><p>The drain seats home. She feels it take, feels the terrible backed-up pressure behind her sternum find, at last, the door it has been beating on, and begin — slowly, agonisingly — to bleed off down a channel that is hers again. It is not relief. It is too late for relief. But it is the difference between a thing that is failing and a thing that is going to fail, and her leg, somewhere very far away, twitches once and remembers most of itself.</p><p>He is already up. Whatever he came to do, it was not to talk to her; or he cannot; or there is no time. He puts two fingers against the side of her face for exactly as long as it takes to be sure she has come back, and even that is not a stranger's touch, and then he is a shape in the doorway and then he is gone, and she is alone in a broken room with her weapon back in her spine and no idea on God's earth why any of it just happened.</p><p>She wakes on the waterfront at dawn. Sore, and not whole — the fault sits in her core where it stopped and does not move, and she already knows, the way you know a scar before you've seen it, that it is not going to move again. But the drain is in its socket. Her leg carries her. The city is exactly where she left it, indifferent, the gulls going and the tide out, Mercer's work waiting and Renner's depot waiting and Sunday coming around again the way Sunday does.</p><p>And four words she cannot hold, and cannot stop reaching for, and one of them a name that fit a lock she didn't know was hers.</p>
<<set $game_state.current_canvas = "captivity_cain">>
<<set $game_state.current_node = "captivity_cain.base">>
<<script>>setup.markCanvasTriggered("captivity_cain");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_cain.base") === -1) { vn.push("captivity_cain.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 1.0, false, null);setup.applyAndNotifyTrait("player", null, "sex_stage", "set", 0.0, false, null);setup.applyAndNotifyTrait("player", null, "loop_npc_pleasure", "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>>
<<script>>setup.applyAndNotifyFlag("player", null, "captivity_done", "set");<</script>>
[[Get up off the dock.->Location_the_waterfront]]<p>It doesn't happen the way she has been bracing for. There is no tearing, nothing gives, no part of her comes off in anyone's hand. What happens is quieter and much worse: the charge fills the last of the room it has, and then it keeps coming, and there is simply nowhere left for it to be.</p><p>The man in her feels it before she does — feels the whole housing of her go rigid under him, feels the hum climb past anything a body makes — and he is already scrambling back off the frame when the light goes. Not out. Up. Every seam of her, every port and plate and the long empty line of the socket Bastien stripped, all of it comes up white at once through the skin, so that for a moment she is lit from the inside like a room with the lamp knocked over, and the men are on their feet and against the walls and one of them is saying oh Christ oh Christ and none of it reaches her.</p><p>Because inside, where she has spent her whole life being quiet, it has finally gone loud. Every door she was built with is open at once. Everything the company filed away where she couldn't reach it is standing in the light — not memories, she has no memories, only the shape of the places they were cut out of, a hundred hollows all shrieking the same note. She has been a thing that feels nothing but the sex it's used for. She feels this. She feels all of it, for one white unsurvivable second, the entire amputated size of herself, and there is no word in her for what it is except that it is hers and they took it.</p><p>Then the safeties she doesn't own throw themselves, the way a station drops its own breakers before the fire, and it all goes black from the top down. The light under her skin dies. The hum falls out of the air. Her held-open eyes lose the ceiling.</p><p>The last thing that reaches her, from very far away, is the sound of the door — not the men's door, the far one, the one that has never once opened while she was awake — coming off its hinges.</p>
<<set $game_state.current_canvas = "captivity_break">>
<<set $game_state.current_node = "captivity_break.base">>
<<script>>setup.markCanvasTriggered("captivity_break");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_break.base") === -1) { vn.push("captivity_break.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "captivity_broken", "set");<</script>>
[[Go dark.->Location_captive_room]]<video src="./videos/sex/cell_he_does_not_stop.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>There's a line of them and it hasn't thinned. Someone's got her by the hips on the concrete and is working into her cunt at a flat, bored pace, and the man before him left her open and slick enough that it goes easy. That isn't the part they've stopped watching for. Under her sternum the plate has started to stutter — light coming up between her ribs in pulses, off-time with the fucking, a dull red that wasn't there when they started and won't settle back down. Every load one of them put in her ticked it higher. It has nowhere to go. She's lit from the inside like something left switched on.</p><p>The socket at the base of her spine sits empty — a dry seam in the metal where the drain used to seat. That was the part that took. Stripped out, there's only the filling: cock after cock stacking charge behind the plate with no line out, and the read-out on her chest climbs while the man fucking her doesn't notice, chasing his own thing, watching her tits move with it. The glow throbs up out of her skin and nobody backs off. They just wait their turn and fill her hotter.</p><p>'The fuck is she doing, she's lighting up.'</p><p>'Who cares, she's still tight — go, go, you're up.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.base">>
<<script>>setup.markCanvasTriggered("cell_use_he_does_not_stop");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.base") === -1) { vn.push("cell_use_he_does_not_stop.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_he_does_not_stop_a_crew_man_balks.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>The one in her cunt pulls out mid-stroke. Not finished — he just steps back off her, cock still hard and shining, staring at the light going through her ribs like it's finally reached him what he's been putting himself into. She keeps twitching where he left her, hips still tipped up because nobody told them to come down. The glow pulses again, brighter, and something under the plate makes a sound — a thin electric whine climbing pitch — and it's the machine, not her, because her face hasn't changed at all. Her face stopped changing a while back.</p><p>He backs another step and won't go near her again, cock going soft in his fist, looking around at the others like one of them's going to explain it. None of them do. The whine keeps rising out of her chest and the red bar under her sternum flickers, gutters, catches — a light with a short in it — and still she lies open on the floor the exact way the last man left her, ass up, thighs slack, waiting to be used by whoever's next.</p><p>'Boss. Boss, that's — that's not right.'</p><p>'Whatever the fuck she is, that ain't it working. That's it breaking.'</p><p>'She's glowing, man, look at her chest, I'm not putting my dick back in that.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.n2") === -1) { vn.push("cell_use_he_does_not_stop.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>Bastien crosses the floor unhurried and crouches at her chest to look — not at her face, at the read-out, at the number climbing behind the flicker. He watches it the way he watches everything about her: like a figure going into a ledger. He lays two fingers flat on the plate, feels the heat coming off it, the whine buzzing up through the metal into his hand, and he doesn't pull them away. He turns his head and looks at the man who stepped back, then at the rest of them standing there with their cocks out, gone uncertain.</p><p>Then he takes his fingers off the plate, wipes the heat off on her tit, and stands, and tells them to get back in her. He's not curious out loud. He's curious in the eyes — but his mouth is only cruel, and the two don't argue with each other in him.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> 'It's not breaking. It's filling. There's a difference and I've been watching it fill for a while now.'</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> 'You fuck her, it charges her — and somebody pulled the one part off her that let her spend it. So now it just goes in. Stacks up. Nowhere out. That's what the light is.'</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> 'Which means if she comes apart, I finally get to see what they built her out of. And there's exactly one way to reach that number. So finish. All of you. Back in her.'</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> 'She doesn't get to keep it, and she doesn't get to hold anything back either. Fill the filthy machine till it can't take another drop.'</div></div><p>'You heard him. Turn her over.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.n3") === -1) { vn.push("cell_use_he_does_not_stop.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_he_does_not_stop_used_past_failure.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>They turn her over. Someone hooks her by the ankle and folds her onto her front, and she goes where she's put — arms loose under her, cheek flat to the concrete — and then there's weight across her back and someone's in her ass now, driving hard the second the boss said go. Her legs stay where they set them, knees shoved apart, ass tipped up, because she's stopped bracing, stopped everything that isn't the plate. And it's her ass — the socket right there above the crack, dead and empty, the drain that used to take pulled out of it. Nothing takes now. Every thrust into her only feeds the thing that fills. The red bar between her ribs has stopped pulsing and gone solid, a level rising, and each grunt and each load somebody empties into her walks it up toward the top.</p><p>One of them reaches under her and works her clit rough while the man behind fucks her, because the boss said the fast way is to make her come — and when it wrenches through her the read-out jumps three marks at once and something under the plate flares white. It isn't pleasure on her at that point. It's overflow with no drain, and it burns; the charge climbing through her with nowhere to be, cooking her from the plate out. She makes a sound she hasn't made — thin, mechanical, wrong — and they laugh and hold her down and drag it out of her again.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> 'Make her come. That's what moves it. Anyone can just fuck her — I want her spiking.'</div></div><p>'There it goes — you see it jump? Do it again, rub her out.'</p><p>'Hold her, hold her, she's going, she's fucking going—'</p><p>'Look at that light, it don't stop where the last one did.'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.n4") === -1) { vn.push("cell_use_he_does_not_stop.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_he_does_not_stop_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/scenes/cell_he_does_not_stop_the_read_out_the_break.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>The read-out reaches the top of its climb and stops — not because they've stopped, but because there is no higher left for it to go. The bar under her sternum is full end to end, a hard flat line of light burning through the skin between her ribs, too bright now to look at straight. The whine has gone past sound into a pressure you feel in your teeth. Everywhere else she's dark: face slack, eyes open on nothing, body still moving only because the man behind her is still moving it, hips still working her onto him out of habit and weight. Every small resistance the machine ever had — gone at once, given up together.</p><p>Bastien stands over the top of her head watching the plate, not her, the way he has the whole time. Charged to the top. No socket. No out. Something in her has to give with all that light and nowhere for it, and under the flat glare of the full read-out — while a man is still emptying into her ass and doesn't even notice — it does.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> 'There. Top of the gauge. Now we find out what they were hiding in you.'</div></div><p>'Boss — boss, it's not moving anymore, it's just—'</p>
<<set $game_state.current_canvas = "cell_use_he_does_not_stop">>
<<set $game_state.current_node = "cell_use_he_does_not_stop.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_he_does_not_stop.readout") === -1) { vn.push("cell_use_he_does_not_stop.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -14.0, true, null);setup.showEffectNotification();<</script>>
[[Take the last of it.->Location_captive_room]]<video src="./videos/sex/cell_stutter.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>The mattress lost its sheet a long stretch ago. She's face-down on the bare ticking, hips hauled up, and the man behind her has both hands clamped on her waist, fucking into her cunt at a flat bored pace — the pace of a thing he's used long enough to stop looking at. He isn't working for it. He's just emptying himself into a hole that's been left open for him.</p><p>Under her sternum the plate that used to burn clean white sits dim now — a dull ember guttering through her skin with every shove, the charge stacked so high behind it that it's stopped glowing right. Each time his cock bottoms out, the read-out over the plate strobes and climbs another notch. Nobody in the room is bothering to read it yet. They will.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Look at it take that. Filthy machine doesn't even need telling anymore — just lies there and lets itself get bred.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Barely lit. Whole thing's running on fumes. Good.</div></div>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.base">>
<<script>>setup.markCanvasTriggered("cell_use_stutter");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.base") === -1) { vn.push("cell_use_stutter.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_stutter_her_leg_dies.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>It goes between one thrust and the next. Her left leg — braced, taking her weight on the knee — simply stops. The muscle lets go all at once, the joint unlocks, and the leg slides out flat across the ticking, dead weight, slack from hip to heel. No twitch. No shudder back to life. It just quits.</p><p>Her hip drops crooked with nothing under it holding the line, and the man's next stroke shoves her sideways across the mattress because half of what used to brace her isn't answering anymore. Somewhere under the skin a thing that was supposed to keep working has gone dark and stayed dark, and the rest of her body keeps getting fucked around the failure like it never happened.</p>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.n2") === -1) { vn.push("cell_use_stutter.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_stutter_he_keeps_going.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 doesn't stop. He barely slows — just reaches down, catches the dead leg under the knee, and hooks it up over his forearm to get the crooked hip back where he wants it. Then he fucks her exactly as before, harder if anything, the slack limb swinging loose off his arm with every drive of his cock like a thing that's already been thrown out.</p><p>The rest of the room watches a machine's leg give out and does the only arithmetic that matters to them: it's still a hole, and the hole still works. Nobody moves to stop it. Nobody would. They lean in closer instead, to see.</p><p>"Its fuckin' leg just died — keep going, keep going, don't you dare stop!"</p><p>"Leg's dead, hole's not. Who cares."</p><p>"Ride it till the next bit falls off."</p>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.n3") === -1) { vn.push("cell_use_stutter.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/scenes/cell_stutter_bastien_leans_in.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>Bastien comes off the wall and crouches at the edge of the mattress, close, elbows on his knees, watching the dead leg swing where it hangs off the man's arm. Not her face — her face has never been the thing. He watches the failure. He reaches out and pushes two fingers deep into the slack meat of her thigh, testing, feeling for a tone that isn't there anymore, and something moves behind his eyes that has nothing to do with mercy.</p><p>He's interested. He's always most interested when a part of her stops. He keeps his fingers buried in the dead muscle a moment longer than he needs to, thumb pressing, reading her by hand while the man's cock keeps working her hole two inches away.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There it goes. Left side's dropping out — right on the number I said it would.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Don't you fucking stop. I want to see what quits next.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Look at that. Bought a machine, and it's coming apart while it gets used. Beautiful.</div></div>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.n4") === -1) { vn.push("cell_use_stutter.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_stutter_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/scenes/cell_stutter_the_read_out.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 the sternum plate the read-out has climbed again — one more increment, logged the second the man's pace picked back up. Every stroke into her stacks a little more charge behind a plate that has nowhere left to send it. The socket at the base of her spine, where the drain used to bleed it off — where anal used to take something out of a man instead of pour it into her — sits empty and cold, a dead port in the small of her back that nothing plugs anymore.</p><p>The number does not fall. It has never once fallen. Bastien's eyes go straight to it and stay there — not on the swinging leg, not on the crew, not on her — on the count. He takes out the little book and writes the new figure under all the others, unhurried, while behind the number the plate flickers like it's finally getting tired.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Higher. Every time it fails, the number still climbs. That's the part they never believe.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Keep filling it. It's got nowhere to put any of it.</div></div>
<<set $game_state.current_canvas = "cell_use_stutter">>
<<set $game_state.current_node = "cell_use_stutter.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_stutter.readout") === -1) { vn.push("cell_use_stutter.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -14.0, true, null);setup.showEffectNotification();<</script>>
[[Stay where she's dropped.->Location_captive_room]]<video src="./videos/sex/cell_overflow_legs_held_open.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>The crew are already in the room when it starts. No unwrapping, no chair pulled up slow — that part's over, she's been stripped since before they came in. Two of them take a leg each and fold her open on the mattress, a knee pinned to the concrete on either side, a forearm braced across each thigh, a hand clamped on each ankle. She's spread and she's held and nothing's inside her yet.</p><p>That's the point of holding her first. What they're about to do works better if she can't close on it, so they get her open and locked before a single cock touches her. The plate under her sternum reads high enough today to be worth the trouble, and every man in the room knows what a high reading means: she's easy to push over now, and pushing her over is the whole job.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Hold her open. I want the reading clean when she goes.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Look at it — a machine that fills up like a cheap whore and can't even spill what's in it. Bought and paid for, and it doesn't even work.</div></div><p>"Get the legs — spread the filthy thing wide."</p><p>"It can't even close, look at it, it's just held there."</p><p>"Stay still, hole. You're gonna want to be real still for this."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.base">>
<<script>>setup.markCanvasTriggered("cell_use_overflow");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.base") === -1) { vn.push("cell_use_overflow.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_overflow.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>A man kneels into the gap they've made her hold and pushes into her cunt, and he doesn't fuck her so much as work her — a thumb set on her clit, a rhythm picked cold, not to get himself off but to get her off, because that's the order he was given. They're not here to come in her. They're here to make her come. A forced orgasm spikes the read-out harder than anything else they can do to a body like hers, and every man in the room has learned that now, and they're grinning at it.</p><p>She holds against it because there is nothing else left to do with her hands full and her legs locked, and it doesn't matter. The machine comes when it's made to. He works her over the edge with the flat patience of a man doing paid work, and behind her sternum something starts to climb toward the wall it always finds now.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Make her spill. Doesn't matter what the thing wants — a machine comes when it's made to, and the number jumps every time it does.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There. Force it out of her. That reading's worth twice what just fucking her is — so make the filthy thing come, and mind you get it all the way.</div></div><p>"Come on, machine, give it up."</p><p>"Look at it fight — it doesn't want to, that's the best part."</p><p>"Doesn't matter what it wants, does it. Push it over."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.n2") === -1) { vn.push("cell_use_overflow.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_overflow_it_hurts_the_overflow.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>And she comes, and it's wrong. The charge goes off behind her sternum and slams into a wall that a body built right doesn't have — the heat that used to run out through the drain at the base of her spine has nowhere to run, so it turns and runs back up into her chest and stacks on top of the last load, and the coming stops being pleasure halfway through and becomes a fault firing down a line. She screams. Not the noise a whore makes on cue — a real one, torn out of her, the sound of a thing overloading with the vents shut.</p><p>The crew think it's the funniest thing they've seen all week. She screamed coming, and it clearly hurt her, and that just makes them want another one out of her. Someone's laughing hard enough to lean on the wall.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> That's the overflow. No channel — it's got nowhere to put itself, so it puts it in her. Scream all you want, filthy thing. The number doesn't care, and I paid for the number.</div></div><p>"Ha — hear that? It hurts it! It hurts the machine to come!"</p><p>"It's crying, look — do it again, make it scream again."</p><p>"Whores don't scream when they come, sweetheart. You're broke."</p><p>"Doesn't even work right and we're still using it."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.n3") === -1) { vn.push("cell_use_overflow.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_overflow_they_keep_going.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>It changes nothing. They take turns on her. One pulls out and the next kneels straight in, her cunt then her ass by turns, and every load that goes into her adds a step to the thing stacked behind her sternum and takes nothing back out. The socket at the base of her spine sits empty through all of it — the one thing that ever drew any of this back out of her, stripped clean and gone — so her ass isn't a drain any more, it's just another hole that fills her. It only ever adds now. Nothing leaves.</p><p>Bastien has dragged his chair up close. He is not watching her face, or her cunt, or the man buried in her. He is watching the plate under her sternum, where the light has stopped sitting steady and started to climb and hold, and he is writing the number down in a book propped on his knee, checking it against her chest between loads the way a man reads a gauge that's running hot.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Keep her going. It stacks — nothing comes back out of a machine with its drain pulled, so pour it all in.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Filthy overfilled thing. You'll take every one of them and hold every drop, and the reading goes up each time. That's all you're for now.</div></div><p>"Next — get in the hole, it can't spill."</p><p>"Fill it right up, it's got nowhere to put it."</p><p>"Passed round like it's nothing, this one — and it takes it every time."</p>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.n4") === -1) { vn.push("cell_use_overflow.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_overflow_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>When they finally leave her spread and slack on the mattress, the plate under her sternum is not doing what it did before they started. The light in it has stopped being steady and gone to a pulse, higher than she has ever seen it read, and it does not come down. It sits there, lit, climbing on nothing, with no drain to bleed it off.</p><p>Bastien crouches over her and looks at it — only at it. He reads the number, writes the number, and the number is the whole reason he came. It went up. It does not go down. Out of everything that was done to her in this room, that is the one thing that changed, and it is the only thing in here that he counts as changed at all.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There's the reading. Higher than the last. You're closer now, machine — and every hole they put in you drives it up another notch and it never once falls.</div></div>
<<set $game_state.current_canvas = "cell_use_overflow">>
<<set $game_state.current_node = "cell_use_overflow.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_overflow.readout") === -1) { vn.push("cell_use_overflow.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -14.0, true, null);setup.showEffectNotification();<</script>>
[[Come down off it.->Location_captive_room]]<video src="./videos/sex/cell_frame_strapped_into_the_rig.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>The frame is bolted to the floor before she's brought to it — a waist-high steel jig with a padded bar and cuffs at the four corners, built to hold something down while it gets worked on. They don't wrestle her into it. They set her. Fold her over the bar, feed her wrists into the cuffs, wrench the strap across the small of her back and crank it until she's fixed face-down at the exact angle they want, ass up, knees spread by the frame's own geometry, the empty socket at the base of her spine sitting open to the room.</p><p>Not held down. Positioned. Locked to spec, like a part dropped into a machine that's about to run — no hands needed once the straps bite, because a fixture doesn't get to move. Someone tests it with a shove and she doesn't shift an inch. The frame takes it for her.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Bolt it down. I don't want the machine wandering off while it does its job.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Ass up, hole open, spine to the ceiling. That's the only shape it's good for now — so lock it there and leave it.</div></div>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.base">>
<<script>>setup.markCanvasTriggered("cell_use_frame");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.base") === -1) { vn.push("cell_use_frame.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_frame_positioned_tuned.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>A crank on the side of the frame lifts the bar an inch; a boot hooks her knee and drags it wider until the cunt and the empty ass sit at the height a standing man wants. A fist takes her hair and tips her head off the frame to clear the throat, lets it drop, tips it again — checking the line. They talk over her the whole time, the way men talk over a bench they're setting up. Not to her. About her.</p><p>Someone works two fingers into her ass and spreads, eyes the angle, wipes the hand off on her thigh. Someone else thumbs her cunt open, lets it close, satisfied. Nobody asks if she's ready because ready isn't a setting the rig has. Tuned. Zeroed. Set to spec and left waiting at the height of the line.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Zero the readout before anyone touches it. I want a clean count off this run — every load logged, none of it wasted.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Height's good. Now it's just a hole at the right elevation. Start the line.</div></div><p>Bring her up an inch — I'm not squatting for a machine.</p><p>There. That's the height. Lock it.</p><p>Spread's good. Both holes on the same level, take your pick.</p><p>Wipe your hand on it, it's not gonna complain.</p>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.n2") === -1) { vn.push("cell_use_frame.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_frame.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>Then they run her. One after another down the line — cunt, then ass, then whichever's nearest, no order to it, the frame absorbing every shove so she never moves off her mark. Each one fucks until he's finished and then stays, buried to the root, leaning his weight down on the small of her back to catch his breath, using her like a place to rest before he pulls out and the next steps up into the gap. Nobody talks to her. She's the bench they're all taking a turn on.</p><p>And every load that goes in lands with nowhere to go. The socket at the base of her spine sits empty where the drain used to bolt in — the ass that used to TAKE something out of a man now just filling, over and over, the drain inverted. Each one that gets held in her feeds the plate under her sternum. The charge stacks behind it and climbs, and the read-out ticks and does not drop.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> That's it — hold it in when you're done. Every one you leave sitting in there winds the number tighter than the fuck did.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Don't rush the line. It's not going anywhere. It's a fitting.</div></div><p>Furniture with a hole. Best kind.</p><p>Move — my turn. And don't pull out slow, leave it in there for the meter.</p><p>Still tight after all that? Machine earns its keep.</p>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.n3") === -1) { vn.push("cell_use_frame.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_frame_held_inside_marks.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>One of them doesn't pull out at all. He drives in to the root and stops, grinds her down flat onto the bar and just holds there — a full beat, two, hips flush to her ass — while his hands go to work on everything else. A fistful of hair cranks her head back off the frame. The other hand wraps her throat from behind and squeezes until the read-out jumps, then lets go and leaves the print of every finger behind on her skin.</p><p>He slaps the ass he's still buried in, hard, twice, watches the handprint bloom red and stay. Digs his thumbs into her hips until they bruise, marking his grip where the next man can see it. By the time he finally pulls out she's covered — handprints across both cheeks, choke-marks coming up at her throat, drool dried on her chin, thumb-bruises on her hips. A machine handled rough and left marked all over, so anyone in the room can read exactly where it's been.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Choke it while you're seated in. The readout spikes when it can't breathe — fastest way to run the number up is to make the thing come while it's strangling.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Mark it up. I want to be able to see everywhere it's been, from across the room.</div></div><p>Look at that print come up. Take a picture.</p><p>Leave your grip on it — save the next guy a handhold.</p>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.n4") === -1) { vn.push("cell_use_frame.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_frame_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>When they're finally done and the frame's cranked back and she's left bent over it — dripping, marked, still bolted to spec — the only thing in the room still doing work is the plate under her sternum. The read-out glows in the dim, and the number on it is higher than it was. A lot higher. It climbed with every load that got held in her and it did not come back down, because there is nowhere for it to go.</p><p>Bastien crouches at the front of the frame to read it. Not her face — the number. He watches it settle, holds a light to it, writes it down.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Up again. Big jump off this run. It takes it in and it can't give any of it back — that's the whole trick of the thing.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Nowhere near topped out. Log it, reset the frame. We're not done running numbers off it.</div></div>
<<set $game_state.current_canvas = "cell_use_frame">>
<<set $game_state.current_node = "cell_use_frame.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_frame.readout") === -1) { vn.push("cell_use_frame.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -12.0, true, null);setup.showEffectNotification();<</script>>
[[Let them unstrap her.->Location_captive_room]]<video src="./videos/scenes/cell_turns_the_queue.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>By the time she's down on her knees in the middle of the concrete, the crew's already strung out along the wall in a loose line, waiting their turn like it's a thing they've done before. Bastien doesn't get in the line. He hauls the chair round so it faces the whole row of them, sits, and clicks a timer open in his palm. His cock's already out. His eyes are on the plate under her sternum, not on her.</p><p>'Thirty a man to start,' he says to the room. 'Move her down the line, don't crowd. I want to see how fast this thing climbs when you keep it fed.' The first man's already got a fist in her hair, waiting on the word. Bastien thumbs the timer. 'Go.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Thirty a man. Don't crowd her — I want it steady, not a pile-on. Not yet.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Look at it. Bought and paid for, and it takes a line better than a turnstile.</div></div><p>'Fuckin' finally. Been standing here with it out.'</p><p>'Save some, she's not going anywhere.'</p><p>'It's got more holes than the three of us need. Line moves.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.base">>
<<script>>setup.markCanvasTriggered("cell_use_turns");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.base") === -1) { vn.push("cell_use_turns.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_turns_passed_down_the_line.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>The first one fucks her mouth till he's done and pulls out to finish across her cheek, and before she's coughed it clear the next man's already stepping up and turning her by the jaw. Mouth, then cunt, then mouth again — whoever's up takes whichever hole he wants and the crew calls it out while they wait. One holds her hips and fucks her through it kneeling; the next just wants her face and uses it. Nobody waits for her to be ready. Ready was never on the list.</p><p>It stacks on her and in her as it goes. Cum on her cheek, roped off her chin, a load left in her cunt that the next man just shoves back up into her with his own. By the fourth turn she's slick and dripping and the line hasn't even thinned. Bastien clicks the timer over at every swap and reads the plate, not her face, calling the number out loud like a man calling races.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Next. Don't be gentle — it doesn't get you a better number.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There it goes. Half a point a load. Keep it moving, it's finally earning its keep.</div></div><p>'Turn it round, I want the cunt.'</p><p>'Swallowed that one — good little hole.'</p><p>'Hurry up, I'm not done looking at what I'm getting.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.n2") === -1) { vn.push("cell_use_turns.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_turns.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>Someone gets tired of the line and bends her over a knee, spreads her ass open with both thumbs, and works into the one hole that used to do the taking. He fucks it rough and finishes deep and doesn't pull out till he's soft. And that's the joke of her nobody in the room knows: the drain lived here once, at the base of her spine, and the ass was the thing that TOOK something out of a man. The socket's stripped now. Empty. So it only fills her — the drain run backwards, a load in the ass that goes nowhere but up.</p><p>Someone else reaches under and works her clit while the first man's still buried in her, mean and deliberate, forcing her over whether she wants it or not — and when she clenches and comes the plate jumps, a hard tick, faster than any load did. Bastien sees it. 'There,' he says. 'That's the one that moves it.' He writes the number down. The charge climbs with nowhere left to go, and the stripped socket at the base of her spine does nothing at all.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> In the ass — that's where the good reading is. Trust me on that one.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There. Forced, it spikes. Make the machine come, don't just fuck it — climbs twice as fast.</div></div><p>'Rip that little hole open, watch it just take it.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.n3") === -1) { vn.push("cell_use_turns.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_turns_used_and_pissed_on.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>By the time the line's thinned she's a mess on the floor, leaking out of both holes, cum drying tight on her tits and her face. One of them steps up like it's his turn again, but he doesn't get hard — he just plants his feet over her, takes his cock in his hand, and pisses on her instead. Down her chest, into her open mouth, across the plate under her sternum, hosing off what the others left like she's a thing you rinse and not a person.</p><p>The crew loses it. Someone tips her chin up by the hair so it runs across her face and she has to hold it. Nobody stops him. When he's done he shakes it off on her and steps back into what's left of the line, and she kneels there in the wet of it, dripping, used up, a thing to empty into and onto and nothing more.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Christ. Fine — it rinses off same as anything else. Doesn't move the number, though. Do that on your own time.</div></div><p>'Open up, machine — waste not.'</p><p>'Look at it just take that too.'</p><p>'Rinse and reuse, boys.'</p>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.n4") === -1) { vn.push("cell_use_turns.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_turns_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/cell_turns_the_read_out.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>When the last of them steps back the room goes quiet except for her breathing. Bastien crouches in front of her — the only time he gets close — and wipes the plate under her sternum clean with his thumb so he can read it. The number's higher than when they started. A lot higher. It went up with every load and it jumped with the ones they forced, and it hasn't shed a single point, because there's nothing left in her that lets it.</p><p>He looks at it a long moment. Not at her — through her, at the reading, the way you'd check a gauge creeping toward a line you drew yourself. He clicks the pen and writes it down. He caps it. He doesn't look at her face once.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Higher than the last count, and it didn't drop a point. Nothing drains out of you anymore, does it. Good.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Faster than I thought. We'll do this again before it's had time to sit.</div></div>
<<set $game_state.current_canvas = "cell_use_turns">>
<<set $game_state.current_node = "cell_use_turns.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_turns.readout") === -1) { vn.push("cell_use_turns.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -12.0, true, null);setup.showEffectNotification();<</script>>
[[Wait for the next one.->Location_captive_room]]<p>The bolt drags back and for once it isn't just him in the gap. He holds the door open with one hand and lets three of them file past his shoulder into the cell, then crosses to the chair against the wall and sits, one ankle up on his knee, the notebook already open on his thigh. The three spread out around her the way men spread out around something cornered — looking, deciding what she is before a single hand's on her.</p><p>That's where she stops being a person. Three sets of eyes, three sets of hands, and the one in the chair who only wants the number after.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Go on. She's bought and paid for — there's nothing in there you can wear out that I'm precious about.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Mind the plate on her chest, though. I want to read it when you're done with her.</div></div><p>'This the one? Christ.'</p><p>'They really do build 'em with a hole for everything.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.base">>
<<script>>setup.markCanvasTriggered("cell_use_first_three");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.base") === -1) { vn.push("cell_use_first_three.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three_manhandled.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>They don't wait for him to finish talking. A fist knots in her hair and turns her by it; another catches her wrists and wrenches them back; a third checks her over rougher than Bastien ever bothers — grabs her jaw, thumbs her mouth open, slaps her tits just to watch them move. Spit lands on her cheek and nobody wipes it off. A hand closes on her throat and squeezes, not to choke, just to feel her take it and hold there.</p><p>Bruises come up under the grips — handprints going red on her hips, on her arms, a mark on her jaw where a thumb dug in. First time it's more than one pair of hands. They pass her between them like they're testing she'll hold together.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Marks are fine. Nothing that opens the casing — she stays sealed.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Filthy machine's been handled worse than you three. Watch — she won't even flinch.</div></div><p>'Hold still, machine.'</p><p>'She don't even blink — spit on her, watch.'</p><p>'Grab-marks come up lovely on this one.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n2") === -1) { vn.push("cell_use_first_three.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three.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>They put her on her knees and open her at both ends at once. One fists her hair and feeds his cock down her throat till she gags around it, holds her there while drool ropes off her chin onto her tits; the other's behind her with his cock buried to the root in her cunt, driving her back onto the first with every thrust so she can't pull off either. She's a length of holes stretched between two men and worked from both directions.</p><p>Every stroke into her charges the core. The read-out under her sternum ticks up with it — and it does not tick back down. Bastien watches from the chair, writes one line, watches again.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> She was built to be used at both ends, weren't you. Filthy fucking thing.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Charge is climbing on penetration alone — note that. Nobody's even made her come yet.</div></div><p>'Look at her take it both ends.'</p><p>'Push her — there, all the way down, that's a good hole.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n3") === -1) { vn.push("cell_use_first_three.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three_dp.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>The third doesn't wait his turn. They stack her — one under her with his cock in her cunt, one still fucking her throat, and the third lines up behind and shoves into her ass. The stripped socket sits empty at the base of her spine, right above where he's splitting her open. The ass used to be the drain — the part that TOOK something out of a man. Now it only fills her, the whole thing running backwards, and every cock in her stacks the charge higher.</p><p>Then one reaches under and works her clit while she's plugged at all three, forces the orgasm up out of her whether she'd give it or not — and that's when the plate jumps. A come wrung out of her spikes it worse than any thrust. At this load it isn't pleasure landing anymore; it's charge with nowhere to go, the overflow gone hot and wrong under her sternum, and it climbs and climbs.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There it is. Every hole loaded and the number won't stop.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Make her come — that's the one. Forced spikes it hardest. Confirmed.</div></div><p>'All three holes — she's fuckin' full.'</p><p>'Feel that? Something's buzzing inside her.'</p><p>'Machine's overheating — don't stop, wring it out of her.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n4") === -1) { vn.push("cell_use_first_three.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_n5">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_first_three_they_finish_on_in_her.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>They finish where they like. One pulls out of her throat and empties across her face — cum in her lashes, on her lips, dripping off her chin. One comes deep in her cunt and one across her back, ropes of it laid over the handprints they left there. They wipe their cocks clean on her tits and her hair when they're done with her.</p><p>She kneels there striped and dripping and does not move. Bastien counts the loads off the plate — every one of them registered, every one banked, none of it going anywhere.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Look at the state of you. Passed round and filled up like the thing you are.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Every load logged on the plate. She banks all of it — nothing drains.</div></div><p>'Open your mouth, machine — there, hold it.'</p><p>'All over her back, look at that.'</p><p>'Leave it on her. Let him see what he paid for.'</p>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.n5">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.n5") === -1) { vn.push("cell_use_first_three.n5"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_first_three_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>When they've filed out he crosses the cell, crouches in front of her, and wipes a smear of cum off the plate under her sternum with his thumb to read it clean. The read-out has climbed hard — further in one session than in a week of just him alone. He doesn't look at her face. He looks at the number, writes it down, and underlines it.</p><p>The strain's up with it. The charge stacks behind the plate with nowhere to go, hotter each load, and the meter only ever runs one direction.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Three at once and forced to come — fastest climb yet. Good.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Charge has nowhere to go and it's stacking. Note the strain.</div></div>
<<set $game_state.current_canvas = "cell_use_first_three">>
<<set $game_state.current_node = "cell_use_first_three.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_first_three.readout") === -1) { vn.push("cell_use_first_three.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -12.0, true, null);setup.showEffectNotification();<</script>>
[[Stay on the floor.->Location_captive_room]]<p>The notebook comes in before he does — a cheap spiral thing he sets on the crate, squares up with the edge, uncaps a pen. Then he drags the chair round to face her and sits, not close, the way a man settles in to watch a gauge rather than fuck a woman. 'Not fucking you today,' he says, almost bored. He taps his own sternum, where hers carries the plate. 'Today I want to see the needle move. You come, that thing counts it. So we're going to find out how many I can get out of you before it complains.'</p><p>He leans in and reads the read-out under her sternum through the strip of grime, and he writes that number down before he's laid a hand on her — a baseline, neat, underlined twice. Cold about it. Curious. 'Filthy little machine,' he says, unhurried, more to the page than to her. 'Let's see what you do.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Not fucking you today. Today I want to see the needle move.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> You come, that thing counts it. We're going to find out how many I can get out of you before it complains.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Filthy little machine. Let's see what you do.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.base">>
<<script>>setup.markCanvasTriggered("cell_use_charge_test");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.base") === -1) { vn.push("cell_use_charge_test.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test.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 works two fingers into her cunt without ceremony and starts — not for her, never for her, his eyes stay up on the plate the whole time. Slow, patient, the heel of his hand grinding a circle while his thumb finds the spot and stays on it. He isn't trying to please her. He's trying to trip a switch. 'There it is,' he murmurs, feeling her clench around him, and he doesn't sound pleased so much as confirmed. 'Wet already. Built to. You can strip everything out of a thing and it still slicks up on command like a good little hole.'</p><p>When the fingers stall he puts his mouth on her too — tongue flat and mean and efficient, spit and friction, throttling her up the way you'd rev an engine — and the whole time one eye stays on the read-out under her sternum, waiting for the twitch. 'That's it. Right up to it.' He pulls back a fraction and keeps her hanging there, deliberate. 'I want the number, not you. Don't matter what you want.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There it is. Wet already. Built to. You can strip everything out of a thing and it still slicks up on command like a good little hole.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> That's it. Right up to it.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> I want the number, not you. Don't matter what you want.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.n2") === -1) { vn.push("cell_use_charge_test.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test_she_comes_degraded_for_it.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>It goes off without her say-so — her cunt clamps down on his fingers and she comes, hard, hips jerking up into a hand that owns her, a sound tearing out of her that isn't a choice. He doesn't ease off through it; he holds her on the edge and wrings it out, watching her body betray her from the inside. 'There she is,' he breathes. 'Look at you. Filthy little machine, creaming all over my hand, gagging for it — didn't even ask you nice.'</p><p>Under her sternum the read-out jumps — not a crawl, a jump, the forced one spiking it the way the slow ones never do — and the charge it books has nowhere to go: the socket at the base of her spine sits empty where the drain used to seat, so it just stacks, hot, behind the plate. He watches the number leap and grins and writes it down. Not her face. The number. He wipes his fingers off on her thigh and reads the new digits out loud like a score.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There she is. Look at you. Filthy little machine, creaming all over my hand, gagging for it — didn't even ask you nice.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.n3") === -1) { vn.push("cell_use_charge_test.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test_again_overstim_slaps.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 goes straight back down on her before the first has finished ringing out, and this time it's too much and that's the point — fingers hard on a clit still fizzing, no mercy in it, dragging her up the raw wall of it while she's still shaking. When she flinches away he slaps her for it, open-handed, across the cunt — a wet crack — then again across the inside of her thigh, watching the red bloom up under his palm. 'Hold still. You'll move the number or you'll wear my hand, machine. Pick one.'</p><p>The second one hurts coming out — the overflow's got nowhere to sit and it turns the pleasure to a hot ache behind her ribs — but he forces it anyway, that's the whole trick, the forced ones move it fastest. She comes with a bruise already coming up on her thigh and a handprint on her cunt, jerking against a grip that doesn't care, and the read-out climbs again. He checks the plate before he checks anything else. 'Good. Again.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Hold still. You'll move the number or you'll wear my hand, machine. Pick one.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Good. Again.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.n4") === -1) { vn.push("cell_use_charge_test.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_charge_test_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_charge_test_again_read_out.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 sits back on his heels, flexes his slick hand, and writes the new figure under the last — a little column building down the page now, each line higher than the one above it. He tips the plate toward the light with two fingers under her chin, not to see her, to read her, comparing the digits against the number he set down at the start. Then he turns the page. 'Climbed,' he says, satisfied, tapping the pen. 'You went up three just now. Nice steep little curve you've got.'</p><p>It doesn't drop back. That's the thing he's writing down and she isn't saying: the read-out ticks up and it stays up, every one he forces out of her stacked behind the sternum plate with the socket at her spine sitting empty and useless to bleed any of it off. He caps the pen, uncaps it again, gets comfortable on the crate. 'We've got all the time in the world,' he says, and puts his hand back between her legs. 'Again.'</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Climbed. You went up three just now. Nice steep little curve you've got.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> We've got all the time in the world. Again.</div></div>
<<set $game_state.current_canvas = "cell_use_charge_test">>
<<set $game_state.current_node = "cell_use_charge_test.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_charge_test.readout") === -1) { vn.push("cell_use_charge_test.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Let him finish writing.->Location_captive_room]]<img src="./videos/scenes/cell_inventory_the_order.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The door lets him in and no one else. He has a folding chair under one arm; he snaps it open a body-length from her and drops into it like a man come to look over stock he already owns. No hurry in him. He sits with his knees apart and looks her up and down the way you'd look at a crate you paid too much for and mean to get your money's worth out of, and lets the quiet sit until it's clear the quiet is his too.</p><p>She stands where the room keeps her. He takes his time getting to the point, because taking his time is part of it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Take it off. All of it. Let's see what I paid for.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Slow. I've got a whole drawer of notes on you upstairs and not one of them tells me what a thing like you is worth. So I'll price you myself.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.base">>
<<script>>setup.markCanvasTriggered("cell_use_inventory");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.base") === -1) { vn.push("cell_use_inventory.base"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n2">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_strip_on_command.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>Her hands don't go to the collar fast enough — a second's hesitation, a beat too long deciding whether to obey — and that's the whole of the excuse he wants. He comes up out of the chair and he's on her before the pause is done, gets a fist in the cloth at her shoulder and tears it down off her in one hard pull, seam giving with a rip, already grabbing the next piece before the first has hit the floor. He strips her the way you'd tear the wrap off stock you paid too much for and can't be bothered to unpick — rough, fast, cloth going in his hands — until she's bare and the torn scraps are around her feet.</p><p>He doesn't do it because he has to. He does it because making her do it herself is slower, and because tearing it off her tells her what she is here quicker than any order could — a thing to be unwrapped, not a thing to be asked. When she's bare he steps back to the chair, cock already out, and looks her over like he's checking he got what he paid for.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Too slow. When I say off, it comes off. I'll not ask a machine twice.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There. Bought and paid for, and not even worth the wrapping. Let's see what's under it.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n2">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n2") === -1) { vn.push("cell_use_inventory.n2"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n3">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_the_inspection.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 gets up only when she's bare. He checks her over like stock — turns her by the hip, spreads her open with both thumbs, works two fingers up into her cunt and a third into her ass in the same breath, no interest at all in whether she's ready for any of it.</p><p>She doesn't answer him, so he spits in her face, unhurried, right across the cheek, and wipes his wet hand off on her tits. Then his thumb finds the empty socket at the base of her spine, and he stops, and works the tip of it into the hole, and laughs.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Nice and tight for a thing that's been passed round half the docks. They tell me you were a fixture in a whorehouse. Doesn't feel used up. Yet.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There it is. That's where the party trick lived. Empty as a robbed till. Somebody's had it out of you — so now you're just holes and a bill.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n3">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n3") === -1) { vn.push("cell_use_inventory.n3"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n4">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_he_fucks_her_throat.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 doesn't ask her to suck it. He fists a handful of her hair, drags her down onto his cock, and fucks it straight down her throat — holds her there past the gag, past the point her eyes run and drool ropes off her chin onto her tits, then hauls her up for one wet breath and shoves her back down onto it.</p><p>He uses her face like it owes him money, both hands in her hair now, driving her throat down the length of him at his own pace and holding her there each time until her whole body kicks. She takes all of it, because taking it is the only thing left that she does.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There's a good hole.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> That's it. Choke on it. That part of you still works, at least.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n4">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n4") === -1) { vn.push("cell_use_inventory.n4"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n5">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory.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 pulls her up off his cock, turns her, and folds her over the table with one hand flat between her shoulder blades, holding her down against nothing because she isn't fighting. Then he pushes into her cunt and fucks her the way a man saws a board he means to use — unhurried, all the way through, hand pressing her cheek to the wood.</p><p>When he spends himself in her he doesn't make a sound. The charge comes off him and goes into her and stops. It climbs up behind the plate under her sternum and stacks there against the wall of the stripped socket, turning over, looking for the way out, and not finding one.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> That's one in the tank. Now let's see if any of it goes anywhere.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n5">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n5") === -1) { vn.push("cell_use_inventory.n5"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_n6">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_he_takes_her_ass.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 pulls out of her cunt still hard, works the head against her ass, and pushes in slow. It takes him longer here, and it's tighter, and he doesn't care. He holds her by the hips and drives up into her ass an inch from the empty socket at the base of her spine — the hole that used to be the drain, the thing that TOOK, that reached down a man's spine and pulled something loose.</p><p>Now it only fills her. Every stroke stacks the charge higher with nowhere for it to run, and when he empties himself into her ass she gets all of it and there is nothing on the far end of the take — no man gone soft and honest and hers — just his cum in her and the meter behind her sternum jumping.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> That used to be the good one, didn't it. The one that emptied a man out and left him yours. Look at it now — just another hole I get to use.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.n6">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.n6") === -1) { vn.push("cell_use_inventory.n6"); }<</script>>
<<nobr>>
<<link "Continue" "Canvas_cell_use_inventory_Node_readout">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/cell_inventory_he_finishes_the_read_out.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 stays buried in her ass a beat after he finishes, breathing, then pulls out and lets it run. He wipes his cock off on the curve of her ass and steps back to look at her — bent over his table, cheek on the wood, holding what he put in her.</p><p>The read-out on the plate under her sternum ticks up one, and does not tick back down. It never ticks back down now. He watches the number, not her; takes the notebook out of his coat, and writes it in the column beside the others.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There. Up, and holding. Whatever's meant to bleed that off isn't working — because I've got the part that does it sitting in a drawer upstairs. You just fill.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Good. We'll run it again once you've settled.</div></div>
<<set $game_state.current_canvas = "cell_use_inventory">>
<<set $game_state.current_node = "cell_use_inventory.readout">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_use_inventory.readout") === -1) { vn.push("cell_use_inventory.readout"); }<</script>>
<<script>>advanceTime(3);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Get up off the table.->Location_captive_room]]<p>She comes up out of it slowly, which is new. There is a ceiling, painted a long time ago, and a light in a cage that does not turn off. A bed bolted to the floor. A chair that isn't. A table with nothing on it. In the middle of the floor there is a drain, because whoever built this room expected to hose it out.</p><p>She reaches back and finds the seam at the base of her spine open and empty. It doesn't hurt. That's the part she keeps checking — it doesn't hurt, the way a missing tooth doesn't hurt, and her hand goes back to it three times before she makes it stop.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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>💭 <<print $player.name>> is thinking:</em> Charge comes in. Charge has to go somewhere.</div></div><p>There is a door. It is steel and newer than the room, and it has no handle on this side. She sits on the edge of the bed with her hands in her lap and listens to men moving on the other side of it, and she works out, without any particular feeling about it, that she can lie down or she can pay attention, and that those are the two things there are.</p>
<<set $game_state.current_canvas = "captivity_intro">>
<<set $game_state.current_node = "captivity_intro.base">>
<<script>>setup.markCanvasTriggered("captivity_intro");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_intro.base") === -1) { vn.push("captivity_intro.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Sit up.->Location_captive_room]]<img src="./videos/scenes/captivity_taken.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>It's the newspaper she notices, half a street back — folded the same way, held by the same patient man who stepped aside to let them carry her past. He doesn't hurry. He doesn't need to. The van is already at the kerb behind her and the dock is long and there is nowhere on it to go that isn't further out.</p><p>This time nobody bothers with the bag until she's inside. A hand finds the seam at the base of her spine and works it, patient, the way a man works a jar lid, until it gives. The weight of the drain comes off her. Somebody says something about the boss wanting a proper look this time, and somebody else laughs, and then the bag.</p>
<<set $game_state.current_canvas = "captivity_recovery">>
<<set $game_state.current_node = "captivity_recovery.base">>
<<script>>setup.markCanvasTriggered("captivity_recovery");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("captivity_recovery.base") === -1) { vn.push("captivity_recovery.base"); }<</script>>
<<nobr>>
<<link "Go under." "Location_captive_room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "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_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, "captivity_entered", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She doesn't hear the door. One second she's got the ledger under her arm and the stair in front of her; the next there's a bag over her head, a knee in her back, and hands that know exactly how much pressure a body takes before it stops fighting. Nobody says a word — professionals. The last thing she registers before the dark is a man at the foot of the stair folding his newspaper, the same patient watcher from The House, stepping aside to let them carry her past.</p><p>She comes to upright in a chair that isn't restraining her, which is its own kind of message. A back room, warm light, a door that's shut and not locked. Across a plain table a man watches her with the patience of someone who owns the building and everything in it — unhurried, certain, a voice that will never have to be raised because the room is already his.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Wren. That's the file Mercer keeps you under, so it's the one I'll use. You've been busy. Doss. Rourke. My man Marsh, who woke up Monday poorer and stupider and can't tell me why. A great deal of digging, for one of Mercer's assets — into people who are mine.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Don't reach for whatever cover you had ready. I knew you were Mercer's before you finished your first drink down here; I hear things before they're said. That isn't what interests me.</div></div><p>He leans in, unhurried, and doesn't threaten her — doesn't reach for anything. He looks at her the way you'd look at the first genuinely interesting thing to cross your desk in years.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> What interests me is that when my man watched you leave The House, he swore the girl who walked out wasn't quite a girl. And my people are very rarely wrong about the things that unsettle them. So before I decide what you're worth — to me, and to the people I answer to, who'd pay a great deal for the honest answer — I'll ask you plainly. What are you, really?</div></div><p>The question hangs. She has covers for men who want her body and men who want her obedience, and none at all for a man who wants the one thing under the chip she can't reach herself. Somewhere below the amputation, something turns over at being seen. She doesn't answer. He reads the not-answering, nods as though it told him plenty, and lifts two fingers — and the bag comes down again.</p><p>Hands under her arms. A stair, and then further down than a stair should go. The salt smell of the docks thins out and stops. Somewhere in the dark someone finds the seam at the base of her spine, working it the way a man works a jar lid, patient, until it gives — and the weight of the drain comes off her and does not come back. She goes under before she can count the turns.</p>
<<set $game_state.current_canvas = "hunt_the_grab">>
<<set $game_state.current_node = "hunt_the_grab.base">>
<<script>>setup.markCanvasTriggered("hunt_the_grab");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_the_grab.base") === -1) { vn.push("hunt_the_grab.base"); }<</script>>
<<nobr>>
<<link "Go under." "Location_captive_room">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "equipped_weapon", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "sex_stage", "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_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, "bastien_revealed", "set");<</script>><<script>>setup.applyAndNotifyFlag("player", null, "captivity_entered", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/crew_den_interior.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The key's under the third stair, like Marsh said. Inside, the rooms have the stillness of a place people left fast and never came back to — a card game abandoned mid-hand, a coat that fits no one now, a fridge that gave out weeks ago and was never opened since. Doss and Rourke lived here, and died somewhere else. She takes the place apart the way she takes everything apart: cold, thorough, patient.</p>
<<set $game_state.current_canvas = "hunt_crew_den_search">>
<<set $game_state.current_node = "hunt_crew_den_search.base">>
<<script>>setup.markCanvasTriggered("hunt_crew_den_search");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_crew_den_search.base") === -1) { vn.push("hunt_crew_den_search.base"); }<</script>>
<<nobr>>
<<link "Take the place apart." "Canvas_hunt_crew_den_search_Node_find">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Not yet." "Location_underworld_strip">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>It's in a strongbox behind the water heater — the one thing in the place worth locking. A run of ledgers, tallies of cargo that never touched a legal dock, and a name at the top of every page: the man the crew answered to, whose coin moved through Doss's hands and out into the company's blind spots. Not Cain. Someone she's never heard of. Bastien.</p><p>She has it now — a person at the end of the trail, a place to put the hunt. She pockets the ledger and turns for the stair.</p>
<<set $game_state.current_canvas = "hunt_crew_den_search">>
<<set $game_state.current_node = "hunt_crew_den_search.find">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_crew_den_search.find") === -1) { vn.push("hunt_crew_den_search.find"); }<</script>>
<<nobr>>
<<link "Bastien. Take the ledger and go." "Canvas_hunt_the_grab_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "bastien_found", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/marsh_booking.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Marsh comes in on the hour, exact. A small neat man who checks the room's exits before he checks the girl, and takes the one the madam pointed him to without a word — her, tonight, in Rue's place. He doesn't notice the swap, or doesn't care; he wants the hour he paid for. Whatever a careful man keeps behind his face, she'll have to work him loose for it — and she knows the one act that opens him.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 1}, {"type": "trait", "subject": "player", "trait_key": "drain_charge", "operator": "lt", "value": 1}]})>>
<p>The drain sits cold against her hip — spent on Renner, never recharged. She can take Marsh's hour for the coin, but the truth stays locked behind his face until the weapon's live. Recharge it at the cradle and take his next Sunday.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<p>She's carrying the emitter, not the drain — wrong weapon for this work. She can take Marsh's hour for the coin, but she'll get nothing out of him. Swap to the drain at her bench and take his next Sunday.</p>
<</if>>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.base">>
<<script>>setup.markCanvasTriggered("hunt_marsh_sunday");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.base") === -1) { vn.push("hunt_marsh_sunday.base"); }<</script>>
<<nobr>>
<<link "Take him — earn the hour." "Canvas_hunt_marsh_sunday_Node_oral">><<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 "Not tonight." "Location_underworld_strip">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><video src="./videos/sex/marsh_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, she takes him in her mouth and works him the way the trade taught her, but slower, watching — reading where the careful in him loosens and where it doesn't. He rests a hand in her hair, polite even here, and lets his head go back. Getting there.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.oral">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.oral") === -1) { vn.push("hunt_marsh_sunday.oral"); }<</script>>
<<nobr>>
<<link "Keep working him." "Canvas_hunt_marsh_sunday_Node_oral">><<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_hunt_marsh_sunday_Node_vaginal">><<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 — give him your ass." "Canvas_hunt_marsh_sunday_Node_anal">><<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('hunt_marsh_sunday:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_hunt_marsh_sunday_Node_finish_soft">><<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("hunt_marsh_sunday:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/marsh_ride_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 climbs on and rides him slow, keeping the pace hers, watching the careful man forget to be careful. He grips her hips and lets go of something — not much, but she's not after much. She's after the one thing men give up when she takes them the last way.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.vaginal">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.vaginal") === -1) { vn.push("hunt_marsh_sunday.vaginal"); }<</script>>
<<nobr>>
<<link "Keep riding him." "Canvas_hunt_marsh_sunday_Node_vaginal">><<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_hunt_marsh_sunday_Node_oral">><<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 "Turn around — give him your ass." "Canvas_hunt_marsh_sunday_Node_anal">><<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('hunt_marsh_sunday:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_hunt_marsh_sunday_Node_finish_soft">><<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("hunt_marsh_sunday:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><video src="./videos/sex/marsh_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>She turns and gives him her ass, and he takes it the way careful men take the thing they'd never ask for out loud — slow, greedy, undone by it. This is the act that opens him. Bent over the bed with his hands clamped on her hips, she waits for the moment his whole guarded weight goes soft, and reaches, if she's carrying it, for the drain.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.anal">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.anal") === -1) { vn.push("hunt_marsh_sunday.anal"); }<</script>>
<<nobr>>
<<link "Take it — work him to the edge." "Canvas_hunt_marsh_sunday_Node_anal">><<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_hunt_marsh_sunday_Node_vaginal">><<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}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"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('hunt_marsh_sunday:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass — take the drain." "Canvas_hunt_marsh_sunday_Node_drain">><<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("hunt_marsh_sunday: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": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc3') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_hunt_marsh_sunday_Node_finish_ass_paid">><<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("hunt_marsh_sunday: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": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"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('hunt_marsh_sunday:cc4') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass — the drain's spent." "Canvas_hunt_marsh_sunday_Node_finish_ass_nodrain">><<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("hunt_marsh_sunday: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": "loop_npc_pleasure", "operator": "gte", "value": 50}, {"type": "trait", "subject": "player", "trait_key": "anal_active", "operator": "gte", "value": 1}, {"type": "flag", "subject": "player", "flag_key": "crew_known", "operator": "is_false"}, {"type": "trait", "subject": "player", "trait_key": "equipped_weapon", "operator": "eq", "value": 2}]})>>
<span @class="setup.isChoiceVisited('hunt_marsh_sunday:cc5') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass — you're carrying the wrong gear." "Canvas_hunt_marsh_sunday_Node_finish_ass_nodrain">><<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("hunt_marsh_sunday:cc5");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><p>He finishes in her ass, and the drain wakes with him — the same act that hands her nothing with a paying man hands her everything with this one. The careful goes out of him all at once, loose and honest, his guarded weight gone soft against her back. She asks the only thing she came for: where the crew lived — Doss, Rourke, the rest. He tells her. A dead man's rooms down past the market, nobody's cleared them, the door still answers to a key under the third stair. In a few minutes he'll surface remembering a good hour and nothing else.</p><p>On her way out a man at the bar folds his paper and watches her go — not the madam's muscle, someone else's, patient about it. She clocks him clocking her and files it. Careful men keep careful company; someone just paid attention to the wrong girl asking the wrong questions.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.drain">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.drain") === -1) { vn.push("hunt_marsh_sunday.drain"); }<</script>>
<<nobr>>
<<link "Take the address and go." "Location_underworld_strip">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "coin", "add", 15.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "drain_charge", "set", 0.0, false, null);<</script>><<script>>setup.applyAndNotifyTrait("player", null, "hygiene", "add", -30.0, true, 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, "crew_known", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>He finishes the way he came in — quiet, contained, already reaching for his coat before his breath's back. A careful man keeps his softness behind him when he doesn't lose the last of himself, and the mouth and the cunt don't cost him that. She takes the coin he leaves and lets him go. Whatever he knows is still behind his face. It's the ass that opens him — she'll need him again, and the drain worn and charged.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.finish_soft">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.finish_soft") === -1) { vn.push("hunt_marsh_sunday.finish_soft"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 15.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_underworld_brothel]]<p>He finishes in her ass and goes slack against her, spent and honest and empty of anything she still needs — she took what was behind his face already, and a man only has the one secret worth the drain. Now he's just a careful regular with a standing Sunday, tipping fair, none the wiser. She straightens, takes the coin, and lets him go.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.finish_ass_paid">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.finish_ass_paid") === -1) { vn.push("hunt_marsh_sunday.finish_ass_paid"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 15.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_underworld_brothel]]<p>He finishes in her ass and the careful goes out of him — loose, open, everything he knows a touch away — and she's got nothing on her to take it with. Without the drain worn and charged he's just a spent john with a soft face and a secret still locked behind it. She takes his coin and files the miss. Recharge the drain at the cradle, carry it, take his Sunday again — the truth's a touch away once the weapon's on her.</p>
<<set $game_state.current_canvas = "hunt_marsh_sunday">>
<<set $game_state.current_node = "hunt_marsh_sunday.finish_ass_nodrain">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_sunday.finish_ass_nodrain") === -1) { vn.push("hunt_marsh_sunday.finish_ass_nodrain"); }<</script>>
<<script>>advanceTime(120);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "coin", "add", 15.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_underworld_brothel]]<p>Sunday, and the House keeps its one clock. Rue's at her chair with the ledger open to tonight's page, the far room already blocked out in her own hand. A few hours yet before the careful little man walks in expecting her.</p><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</strong> Your Sunday standing. The careful little man, same hour every week. I want tonight.</div></div><p>A slow look up from the ledger, reassessing. She knows exactly who the girl means, and she doesn't ask why.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" 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>Rue:</strong> Marsh. Quiet money, tips fair, never any trouble — the kind of regular you don't hand off. But I could use a night that's mine, and you've got the look of someone who wants him for more than the coin.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" 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>Rue:</strong> So take tonight. Tonight, mind — not my Sundays. He's on the hour, the far room. Whatever your business is, keep it off my floor, and he never hears from me that the girl got swapped. You want him again, you come ask me again.</div></div>
<<set $game_state.current_canvas = "hunt_marsh_scheme">>
<<set $game_state.current_node = "hunt_marsh_scheme.base">>
<<script>>setup.markCanvasTriggered("hunt_marsh_scheme");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_marsh_scheme.base") === -1) { vn.push("hunt_marsh_scheme.base"); }<</script>>
<<nobr>>
<<link "Tonight, then. The far room." "Location_underworld_brothel">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "rue_met", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Not this week." "Location_underworld_brothel">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>The floor's quiet this hour — a girl asleep in a chair, a lamp left burning low, the rooms all shut. The madam runs the House by day; nothing moves through here until she's back at her post. Come back when she's in, if it's work you're after.</p>
<<set $game_state.current_canvas = "brothel_offhours">>
<<set $game_state.current_node = "brothel_offhours.base">>
<<script>>setup.markCanvasTriggered("brothel_offhours");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("brothel_offhours.base") === -1) { vn.push("brothel_offhours.base"); }<</script>>
<<script>>advanceTime(3);<</script>>
[[Head back up.->Location_underworld_strip]]<p>Rue keeps her chair by the rooms, ledger open, watching the floor the way a dealer watches a table. She nods when she sees her — not warmth, just the level look of one working woman clocking another.</p>
<<set $game_state.current_canvas = "rue_hub">>
<<set $game_state.current_node = "rue_hub.base">>
<<script>>setup.markCanvasTriggered("rue_hub");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_hub.base") === -1) { vn.push("rue_hub.base"); }<</script>>
<<nobr>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "brothel_hired", "operator": "is_false"}]})>>
<span @class="setup.isChoiceVisited('rue_hub:cc0') ? '' : 'unlocked-choice'">
<<link "Ask her for work." "Canvas_rue_hub_Node_work">><<script>>setup.markChoiceVisited("rue_hub:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "brothel_hired", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('rue_hub:cc1') ? '' : 'unlocked-choice'">
<<link "Trade a word." "Canvas_rue_hub_Node_talk">><<script>>setup.markChoiceVisited("rue_hub:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<link "Leave her to it." "Location_underworld_brothel">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</strong> I want the work. The rooms.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" 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>Rue:</strong> Course you do. They all do, once they've been down here long enough to stop pretending otherwise.</div></div><p>She writes something in the ledger without looking up — a name, or a mark against one — and tips her head at the rooms. That's the whole hiring. No contract, no questions.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" 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>Rue:</strong> Rooms are yours when you want them. Coin's yours after. Don't make me regret the chair I didn't get up from.</div></div>
<<set $game_state.current_canvas = "rue_hub">>
<<set $game_state.current_node = "rue_hub.work">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_hub.work") === -1) { vn.push("rue_hub.work"); }<</script>>
<<nobr>>
<<link "You won't." "Canvas_rue_hub_Node_base">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "brothel_hired", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She trades what she hears, the small weather of the underworld — which men are talking, whose protection got thinner this week, who's asking after who. "You didn't get it off me," she says every time, and tells her anyway. It isn't much. But it's a face down here that isn't a mark and isn't an owner, and there aren't many of those.</p>
<<set $game_state.current_canvas = "rue_hub">>
<<set $game_state.current_node = "rue_hub.talk">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_hub.talk") === -1) { vn.push("rue_hub.talk"); }<</script>>
<<nobr>>
<<link "Good talking." "Location_underworld_brothel">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/rue_madam.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>The woman by the rooms takes her measure before she's three steps in — the flat, unhurried read of someone who's priced a thousand bodies and is rarely wrong by much. She doesn't get up.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" 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>Rue:</strong> Rue. I run this house. Not the muscle, not the owner — the floor. Which means I decide who works my rooms and who serves the few names worth keeping. You've got the look of someone who's been used for it before and stopped feeling it. Down here that's not a mark against you. That's the job.</div></div><p>She tips her head at the rooms behind her — the offer plain, no contract, no company, nobody signing the girl out at the end of it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/rue.jpg" alt="Rue" 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>Rue:</strong> Coin's yours when you're done, and nobody up top hears a word of it from me. You want in, you ask me. Simple as that.</div></div>
<<set $game_state.current_canvas = "rue_intro">>
<<set $game_state.current_node = "rue_intro.base">>
<<script>>setup.markCanvasTriggered("rue_intro");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("rue_intro.base") === -1) { vn.push("rue_intro.base"); }<</script>>
<<nobr>>
<<link "Take her measure back." "Location_underworld_brothel">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "rue_introduced", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><img src="./videos/scenes/underworld_bar_bartender.jpg" alt="" loading="lazy" decoding="async" style="max-width:100%;max-height:70vh;object-fit:contain;height:auto;border-radius:8px;" /><p>Sol works the taps at the dim end of the bar, wiping a glass that's already clean. He's outlasted every owner this place has had — and it's had a few, though the one it answers to now never shows his face. He looks at her the way he looks at regulars, not an owner, not a mark. He hears everything said over his counter down here and repeats none of it, unless you're the kind he's decided he likes.</p>
<<set $game_state.current_canvas = "hunt_sol_lead">>
<<set $game_state.current_node = "hunt_sol_lead.base">>
<<script>>setup.markCanvasTriggered("hunt_sol_lead");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_sol_lead.base") === -1) { vn.push("hunt_sol_lead.base"); }<</script>>
<<nobr>>
<<link "Ask him about Doss and Rourke." "Canvas_hunt_sol_lead_Node_ask">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Not tonight." "Location_underworld_bar">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</strong> Two men. Doss and Rourke. They came up out of the underworld before they ever ran for Renner. You'd have known them.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" 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>Sol:</strong> Doss I knew. Ran cargo for the company back when it still pretended it didn't. Renner made an example of him — him and Rourke both, and it took. But Doss ran with a third. A fixer, name of Marsh. Careful little man, and still breathing.</div></div><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</strong> Where.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/sol.jpg" alt="Sol" 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>Sol:</strong> He keeps one habit. The House, every Sunday — same girl, like clockwork. That's the only place Marsh surfaces. A man with one habit's got one weakness.</div></div><p>He sets the clean glass down. A name, a day, a door — the whole of it. Then, not looking up: "Whatever you carry that does your quiet work — carry it Sunday. Marsh doesn't talk twice."</p>
<<set $game_state.current_canvas = "hunt_sol_lead">>
<<set $game_state.current_node = "hunt_sol_lead.ask">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("hunt_sol_lead.ask") === -1) { vn.push("hunt_sol_lead.ask"); }<</script>>
<<nobr>>
<<link "Sundays, then. Carry the drain." "Location_underworld_bar">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "hunt_lead", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><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 = "react_renner_nocover_anchor">>
<<set $game_state.current_node = "react_renner_nocover_anchor.base">>
<<script>>setup.markCanvasTriggered("react_renner_nocover_anchor");<</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 = "react_renner_nocover_depot">>
<<set $game_state.current_node = "react_renner_nocover_depot.base">>
<<script>>setup.markCanvasTriggered("react_renner_nocover_depot");<</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_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 = "react_renner_threat">>
<<set $game_state.current_node = "react_renner_threat.base">>
<<script>>setup.markCanvasTriggered("react_renner_threat");<</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 = "amb_renner_cheerup">>
<<set $game_state.current_node = "amb_renner_cheerup.base">>
<<script>>setup.markCanvasTriggered("amb_renner_cheerup");<</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 = "amb_renner_anchor_ruin">>
<<set $game_state.current_node = "amb_renner_anchor_ruin.base">>
<<script>>setup.markCanvasTriggered("amb_renner_anchor_ruin");<</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 = "walkin_renner_depot">>
<<set $game_state.current_node = "walkin_renner_depot.base">>
<<script>>setup.markCanvasTriggered("walkin_renner_depot");<</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_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-b1-beat-0"><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</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-b1-beat-1"><<linkreplace "What did Cain take?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</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-b1-beat-2"><<linkreplace "Why did Cain burn you down?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</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. Doss and Rourke. 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-b1-beat-3"><<linkreplace "Where is he now?">><div class="dialog-block dialog-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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><<print $player.name>>:</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-b1-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_depot]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "renner_control_canvas">>
<<set $game_state.current_node = "renner_control_canvas.intro">>
<<script>>setup.markCanvasTriggered("renner_control_canvas");<</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 = "renner_control_canvas">>
<<set $game_state.current_node = "renner_control_canvas.again">>
<<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_again_ask">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Just leave him." "Location_renner_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 — Doss and Rourke — 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 — but this time she keeps the two names. He doesn't know he's said it before, and he never will. Whatever she already had, 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 = "renner_control_canvas">>
<<set $game_state.current_node = "renner_control_canvas.again_ask">>
<<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_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 = "loop_renner_finisher">>
<<set $game_state.current_node = "loop_renner_finisher.climax">>
<<script>>setup.markCanvasTriggered("loop_renner_finisher");<</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('loop_renner_finisher:cc0') ? '' : 'unlocked-choice'">
<<link "Straighten up." "Location_renner_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("loop_renner_finisher: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('loop_renner_finisher:cc1') ? '' : 'unlocked-choice'">
<<link "Take the keys." "Canvas_renner_control_canvas_Node_intro">><<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.applyAndNotifyTrait("player", null, "names_known", "set", 1.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("loop_renner_finisher: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('loop_renner_finisher:cc2') ? '' : 'unlocked-choice'">
<<link "Take the keys." "Canvas_renner_control_canvas_Node_again">><<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.applyAndNotifyTrait("player", null, "names_known", "set", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("loop_renner_finisher: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('loop_renner_finisher:cc3') ? '' : 'unlocked-choice'">
<<link "Let it go — the drain's empty. Recharge it at the cradle." "Location_renner_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("loop_renner_finisher: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('loop_renner_finisher:cc4') ? '' : 'unlocked-choice'">
<<link "Let it go — wrong gear." "Location_renner_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("loop_renner_finisher: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 = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.intro">>
<<script>>setup.markCanvasTriggered("loop_renner_office_sex");<</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_base_oral_r">><<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 = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.base_oral_r">>
<<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_base_oral_r">><<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_base_vaginal_r">><<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('loop_renner_office_sex:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_loop_renner_finisher_Node_climax">><<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("loop_renner_office_sex: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 = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.base_vaginal_r">>
<<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_base_vaginal_r">><<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_base_oral_r">><<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('loop_renner_office_sex:cc1') ? '' : 'unlocked-choice'">
<<link "Turn around — give him your ass." "Canvas_loop_renner_office_sex_Node_base_doggy_r">><<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("loop_renner_office_sex: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('loop_renner_office_sex:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_loop_renner_finisher_Node_climax">><<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("loop_renner_office_sex: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 = "loop_renner_office_sex">>
<<set $game_state.current_node = "loop_renner_office_sex.base_doggy_r">>
<<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_base_doggy_r">><<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_base_vaginal_r">><<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('loop_renner_office_sex:cc3') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_loop_renner_finisher_Node_climax">><<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("loop_renner_office_sex: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="Wren" 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><<print $player.name>>:</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 = "ma_renner_blowjob">>
<<set $game_state.current_node = "ma_renner_blowjob.base">>
<<script>>setup.markCanvasTriggered("ma_renner_blowjob");<</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_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-b2-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-b2-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_depot]]
<</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_renner_blowjob">>
<<set $game_state.current_node = "cap_renner_blowjob.base">>
<<script>>setup.markCanvasTriggered("cap_renner_blowjob");<</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 = "rung_renner_grope">>
<<set $game_state.current_node = "rung_renner_grope.base">>
<<script>>setup.markCanvasTriggered("rung_renner_grope");<</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_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 = "rung_renner_flash">>
<<set $game_state.current_node = "rung_renner_flash.base">>
<<script>>setup.markCanvasTriggered("rung_renner_flash");<</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_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 = "rung_renner_tease">>
<<set $game_state.current_node = "rung_renner_tease.base">>
<<script>>setup.markCanvasTriggered("rung_renner_tease");<</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_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 = "cap_renner_noticed">>
<<set $game_state.current_node = "cap_renner_noticed.base">>
<<script>>setup.markCanvasTriggered("cap_renner_noticed");<</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_depot]]<<set _sub_target = setup.checkAndSubstituteCanvas("hub_anchor_renner")>><<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 = "hub_anchor_renner">>
<<set $game_state.current_node = "hub_anchor_renner.base">>
<<script>>setup.markCanvasTriggered("hub_anchor_renner");<</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_sit">><<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 = "hub_anchor_renner">>
<<set $game_state.current_node = "hub_anchor_renner.sit">>
<<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("work_depot_haul");<</script>>
<<set _sub_target = setup.checkAndSubstituteCanvas("work_depot_haul")>><<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 = "work_depot_haul">>
<<set $game_state.current_node = "work_depot_haul.base">>
<<script>>setup.markCanvasTriggered("work_depot_haul");<</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_depot]]
<<else>>
<div class="cost-blocked-message">
<p><<= setup.getCostBlockedMessage(_costs)>></p>
</div>
[[Back->Location_renner_depot]]
<</if>><<set _sub_target = setup.checkAndSubstituteCanvas("hub_depot_floor")>><<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 = "hub_depot_floor">>
<<set $game_state.current_node = "hub_depot_floor.base">>
<<script>>setup.markCanvasTriggered("hub_depot_floor");<</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_check_in">><<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('hub_depot_floor:cc0') ? '' : 'unlocked-choice'">
<<link "Tease him." "Canvas_rung_renner_tease_Node_base">><<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("hub_depot_floor: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('hub_depot_floor:cc1') ? '' : 'unlocked-choice'">
<<link "Flash him — work commando." "Canvas_rung_renner_flash_Node_base">><<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("hub_depot_floor: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('hub_depot_floor:cc2') ? '' : 'unlocked-choice'">
<<link "Get your hands on him." "Canvas_rung_renner_grope_Node_base">><<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("hub_depot_floor: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('hub_depot_floor:cc3') ? '' : 'unlocked-choice'">
<<link "Take him in the office." "Canvas_loop_renner_office_sex_Node_intro">><<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("hub_depot_floor: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 = "hub_depot_floor">>
<<set $game_state.current_node = "hub_depot_floor.check_in">>
<<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_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-b2-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-b2-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 = "cap_renner_hired">>
<<set $game_state.current_node = "cap_renner_hired.base">>
<<script>>setup.markCanvasTriggered("cap_renner_hired");<</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 = "mercer_finisher">>
<<set $game_state.current_node = "mercer_finisher.climax">>
<<script>>setup.markCanvasTriggered("mercer_finisher");<</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_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 = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.intro">>
<<script>>setup.markCanvasTriggered("mercer_serve");<</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_base_knees">><<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 = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.base_knees">>
<<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_base_knees">><<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_base_desk">><<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_base_glass">><<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('mercer_serve:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_mercer_finisher_Node_climax">><<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("mercer_serve: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 = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.base_desk">>
<<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_base_desk">><<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_base_knees">><<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_base_glass">><<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('mercer_serve:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_mercer_finisher_Node_climax">><<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("mercer_serve: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 = "mercer_serve">>
<<set $game_state.current_node = "mercer_serve.base_glass">>
<<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_base_glass">><<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_base_desk">><<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('mercer_serve:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_mercer_finisher_Node_climax">><<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("mercer_serve: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 = "mercer_penthouse_hub">>
<<set $game_state.current_node = "mercer_penthouse_hub.base">>
<<script>>setup.markCanvasTriggered("mercer_penthouse_hub");<</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_report">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Kneel and serve him." "Canvas_mercer_serve_Node_intro">><<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_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 = "mercer_penthouse_hub">>
<<set $game_state.current_node = "mercer_penthouse_hub.report">>
<<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_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-b2-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-b2-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-b2-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_penthouse]]
<</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "cap_first_penthouse_service">>
<<set $game_state.current_node = "cap_first_penthouse_service.base">>
<<script>>setup.markCanvasTriggered("cap_first_penthouse_service");<</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>There is nothing in the room to do. There are three things in it to notice.</p>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.base">>
<<script>>setup.markCanvasTriggered("cell_attend");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.base") === -1) { vn.push("cell_attend.base"); }<</script>>
<<nobr>>
<<link "Listen at the door." "Canvas_cell_attend_Node_listen">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Watch the man on the chair." "Canvas_cell_attend_Node_watch">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Hold still and feel the fault." "Canvas_cell_attend_Node_hold_still">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>Steel carries. She sits with her shoulder against it and counts: five voices, six, one that comes and goes and never finishes a sentence because the others stop talking when it does. Cards, some nights. A radio. A long argument about a boat that she thinks is not about a boat.</p><p>Nothing about her. That's the part she keeps turning over. Whatever she is to them, she isn't a thing they discuss.</p>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.listen">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.listen") === -1) { vn.push("cell_attend.listen"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Sit back.->Location_captive_room]]<p>There's always one, and it's always the chair. He doesn't talk to her. He reads, or he doesn't, and he watches the way a man watches a pot. When she looks back at him he holds it, bored, and eventually she is the one who looks away, and she notes that too, because a month ago she would not have.</p><p>Sometimes it's Bastien in the chair. He never reads. He sits with his hands still and studies her the way he studied her across the table, patient, waiting for the answer to a question he has not asked again.</p>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.watch">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.watch") === -1) { vn.push("cell_attend.watch"); }<</script>>
<<script>>advanceTime(30);<</script>>
[[Look away.->Location_captive_room]]<p>She lies on her back and stops doing anything at all, and listens to herself the way she listened to the door. The charge sits under her sternum with nowhere to be. It has a sound. It has, she realises, started to have a smell — hot dust, the inside of a machine that has been run too long.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "glitch_iii_seen", "operator": "is_false"}]})>>
<p>And then the fault opens, the way a badly-set bone opens, and something comes up through it that was never filed anywhere she can reach.</p><p>A hand at the back of her neck. Not holding her — resting there, the thumb moving once, slowly, along the seam. Somebody standing behind her while she looks at something she cannot see, and in the whole of her body, in the whole of the record, no fear at all. Not permitted absence of fear. The other kind. The kind that is simply not there because there is nothing in the room that could hurt her.</p><div class="thought-bubble thought-bubble-player"><div class="portrait"><img src="./videos/wren.jpg" alt="Wren" 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>💭 <<print $player.name>> is thinking:</em> Whose hand.</div></div><p>It closes. It goes back down wherever it lives. She lies in a room she cannot leave with men on the other side of the door and she finds that she is holding the back of her own neck, and that her eyes have gone wet, and that she has no idea, none, whose hand that was.</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "flag", "subject": "player", "flag_key": "glitch_iii_seen", "operator": "is_true"}]})>>
<p>She reaches for it again — the hand, the seam, the quiet — and it isn't there. The fault doesn't open twice for the same thing. What's left is the pressure, and the hot-dust smell, and the certainty that it is worse than it was.</p>
<</if>>
<<set $game_state.current_canvas = "cell_attend">>
<<set $game_state.current_node = "cell_attend.hold_still">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_attend.hold_still") === -1) { vn.push("cell_attend.hold_still"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.applyAndNotifyFlag("player", null, "glitch_iii_seen", "set");<</script>>
[[Let it go.->Location_captive_room]]<p>The light doesn't go off, so she turns her face to the wall and powers down anyway. The bed is bolted down and the mattress smells of other people. She goes under fast, the way she always does, and it doesn't matter at all, because the door opens in the night. It always opens in the night.</p><<if setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "lt", "value": 24}]})>>
<video src="./videos/sex/cell_sleep_nominal.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 down on the mat with her back to the wall, trying to drop under, when the weight settles in behind her and Bastien starts using her before she's all the way awake — cock worked into her cunt from behind, unhurried, one hand laid flat on the sternum plate so he can feel it tick while he does it. The charge climbs with every slow shove and runs down to the empty socket at the base of her spine and stops there, nowhere to go, stacking behind the plate in the dark.</p><p>He finishes inside her and the read-out clicks up one notch and holds — it never drops — and he wipes off on the small of her back and leaves her where she lies. <em>Logged as rest. It isn't.</em></p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Don't mind me. Go back under, filthy thing — I'm just reading you.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> There it goes. One more on the count and nowhere for it to run. Bought that too.</div></div>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "gte", "value": 24}, {"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "lt", "value": 48}]})>>
<video src="./videos/sex/cell_sleep_hot.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 surfaces because there are hands on her that aren't one man's — the crew have let themselves in in the dark, and she wakes already being used, a cock working her cunt while another feeds itself into her mouth and a third pair of hands just paws her tits like they're checking she's still warm. She can't tell how many; they pass her between them in the black, laughing low, and every load that goes in stacks up behind the plate with the drain gone and the socket empty, filling and filling with nowhere to send it.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Left the door open for the lads. She sleeps better with a hole full — don't you, slut.</div></div><p>"She's awake — watch, the little machine's awake."</p><p>"Doesn't matter if she is. Open, bought-and-paid-for."</p><p>"Pass her down, I've not had a turn."</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "gte", "value": 48}, {"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "lt", "value": 72}]})>>
<video src="./videos/sex/cell_sleep_faulting.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>They don't let her stay under at all now — every time she starts to slip they haul her back onto her knees and use her hard, one fisting her hair down onto his cock till she gags and drools, another driving into her from behind, handprints coming up on her ass and a set of fingermarks banding her throat. Then they flip her and take her in the ass — the drain inverted, the thing that used to TAKE from a man now only pouring INTO her — and Bastien holds her jaw and makes her come on it because forcing it spikes the read-out fastest, and the number jumps two, three, climbing in the dark while she can't tell sleep from being fucked.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Make her come — that's what moves the count, not the fucking. Force it. Watch the plate, not her.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Up it goes. Look at that. Passed round all night and still filling.</div></div><p>"She's shaking — do it again, it likes it."</p><p>"Hold her mouth, she's trying to check out."</p><p>"Mark her up, no one's counting bruises."</p>
<<elseif setup.triggerConditionsSatisfied({"version": "1.0", "items": [{"type": "trait", "subject": "player", "trait_key": "core_strain", "operator": "gte", "value": 72}]})>>
<video src="./videos/sex/cell_sleep_failing.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>By the small hours her body's half-quitting under them — legs gone loose, breath sawing, the sternum plate flickering its read-out where it should hold steady — and none of them slow down for it. The charge has stacked up so high with nowhere to drain that every load now lands as a wet flare of pain instead of anything else, and they force her over the edge anyway, again, the pleasure coming out the wrong way and hurting, the plate stuttering the number up past where the light stays on. She's limp between them, used front and back, drooled-on and slack, and Bastien just leans in close to read the flicker off her chest and not her face at all.</p><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> It's browning out. Good — keep going. I want to see how high it climbs before the light won't hold.</div></div><div class="dialog-block dialog-npc"><div class="portrait"><img src="./videos/bastien.jpg" alt="Bastien" 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>Bastien:</strong> Not her, idiot — the plate. Watch the plate. She's just the socket.</div></div><p>"Something's wrong with it — look, it's flickering."</p><p>"Boss says keep going, so keep going."</p><p>"Still warm, still open. Who cares if it's breaking."</p>
<</if>>
<<set $game_state.current_canvas = "cell_sleep">>
<<set $game_state.current_node = "cell_sleep.base">>
<<script>>setup.markCanvasTriggered("cell_sleep");<</script>>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("cell_sleep.base") === -1) { vn.push("cell_sleep.base"); }<</script>>
<<script>>advanceTime(540);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", 100.0, true, null);setup.applyAndNotifyTrait("player", null, "core_strain", "add", 12.0, true, null);setup.showEffectNotification();<</script>>
[[Wake up.->Location_captive_room]]<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 = "activity_wash">>
<<set $game_state.current_node = "activity_wash.base">>
<<script>>setup.markCanvasTriggered("activity_wash");<</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_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 = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.base">>
<<script>>setup.markCanvasTriggered("underworld_market_shop");<</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_weapon">><<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_gear">><<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_pens">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Leave the stalls." "Location_underworld_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 = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.weapon">>
<<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_underworld_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 = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.gear">>
<<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_underworld_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 = "underworld_market_shop">>
<<set $game_state.current_node = "underworld_market_shop.pens">>
<<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_underworld_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 = "underworld_pit_fight">>
<<set $game_state.current_node = "underworld_pit_fight.base">>
<<script>>setup.markCanvasTriggered("underworld_pit_fight");<</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('underworld_pit_fight:cc0') ? '' : 'unlocked-choice'">
<<link "Take a low bout." "Canvas_underworld_pit_fight_Node_win_low">><<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("underworld_pit_fight: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('underworld_pit_fight:cc1') ? '' : 'unlocked-choice'">
<<link "Take a hard bout." "Canvas_underworld_pit_fight_Node_win_hard">><<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("underworld_pit_fight: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_underworld_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 = "underworld_pit_fight">>
<<set $game_state.current_node = "underworld_pit_fight.win_low">>
<<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_underworld_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 = "underworld_pit_fight">>
<<set $game_state.current_node = "underworld_pit_fight.win_hard">>
<<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_underworld_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 = "underworld_brothel_finisher">>
<<set $game_state.current_node = "underworld_brothel_finisher.climax">>
<<script>>setup.markCanvasTriggered("underworld_brothel_finisher");<</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_underworld_brothel]]<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 = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.intro">>
<<script>>setup.markCanvasTriggered("underworld_brothel_loop");<</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_base_oral">><<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 = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.base_oral">>
<<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_base_oral">><<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_base_vaginal">><<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_base_anal">><<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('underworld_brothel_loop:cc0') ? '' : 'unlocked-choice'">
<<link "Let him finish in your mouth." "Canvas_underworld_brothel_finisher_Node_climax">><<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("underworld_brothel_loop: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 = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.base_vaginal">>
<<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_base_vaginal">><<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_base_oral">><<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_base_anal">><<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('underworld_brothel_loop:cc1') ? '' : 'unlocked-choice'">
<<link "Let him finish inside you." "Canvas_underworld_brothel_finisher_Node_climax">><<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("underworld_brothel_loop: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 = "underworld_brothel_loop">>
<<set $game_state.current_node = "underworld_brothel_loop.base_anal">>
<<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_base_anal">><<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_base_vaginal">><<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('underworld_brothel_loop:cc2') ? '' : 'unlocked-choice'">
<<link "Let him finish in your ass." "Canvas_underworld_brothel_finisher_Node_climax">><<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("underworld_brothel_loop: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 = "underworld_brothel_work">>
<<set $game_state.current_node = "underworld_brothel_work.base">>
<<script>>setup.markCanvasTriggered("underworld_brothel_work");<</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_intro">><<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_underworld_brothel">><<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 = "underworld_strip_hub">>
<<set $game_state.current_node = "underworld_strip_hub.base">>
<<script>>setup.markCanvasTriggered("underworld_strip_hub");<</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>>
<<nobr>>
<<link "Climb back up to the docks." "Location_the_waterfront">><<script>>advanceTime(3);<</script>><</link>><br>
<<link "Call the car up to the Spire." "Canvas_underworld_strip_hub_Node_to_spire">><<script>>advanceTime(3);<</script>><</link>><br>
<</nobr>><p>She climbs up out of the red light and the noise, back to the docks, and calls the car. The underworld closes over behind her; the towers rise up ahead until the glass takes the sky again.</p>
<<set $game_state.current_canvas = "underworld_strip_hub">>
<<set $game_state.current_node = "underworld_strip_hub.to_spire">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("underworld_strip_hub.to_spire") === -1) { vn.push("underworld_strip_hub.to_spire"); }<</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 Spire.->Location_spire_plaza]]<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 = "underworld_gate_check">>
<<set $game_state.current_node = "underworld_gate_check.base">>
<<script>>setup.markCanvasTriggered("underworld_gate_check");<</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_underworld_strip">><<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('underworld_gate_check:cc0') ? '' : 'unlocked-choice'">
<<link "Force past him." "Location_underworld_strip">><<script>>setup.markChoiceVisited("underworld_gate_check: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('underworld_gate_check:cc1') ? '' : 'unlocked-choice'">
<<link "Put the weapon on him." "Canvas_underworld_gate_check_Node_weapon_scene">><<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("underworld_gate_check: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 = "underworld_gate_check">>
<<set $game_state.current_node = "underworld_gate_check.weapon_scene">>
<<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_underworld_strip]]<<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 = "activity_swap_weapon">>
<<set $game_state.current_node = "activity_swap_weapon.base">>
<<script>>setup.markCanvasTriggered("activity_swap_weapon");<</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('activity_swap_weapon:cc0') ? '' : 'unlocked-choice'">
<<link "Run the emitter instead." "Location_wren_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("activity_swap_weapon: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('activity_swap_weapon:cc1') ? '' : 'unlocked-choice'">
<<link "Go back to the drain." "Location_wren_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("activity_swap_weapon:cc1");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<<link "Leave it." "Location_wren_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 = "activity_repair_weapon">>
<<set $game_state.current_node = "activity_repair_weapon.base">>
<<script>>setup.markCanvasTriggered("activity_repair_weapon");<</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('activity_repair_weapon:cc0') ? '' : 'unlocked-choice'">
<<link "Work on it." "Canvas_activity_repair_weapon_Node_progress">><<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("activity_repair_weapon: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('activity_repair_weapon:cc1') ? '' : 'unlocked-choice'">
<<link "Finish it." "Canvas_activity_repair_weapon_Node_done">><<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("activity_repair_weapon: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_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 = "activity_repair_weapon">>
<<set $game_state.current_node = "activity_repair_weapon.progress">>
<<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_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 = "activity_repair_weapon">>
<<set $game_state.current_node = "activity_repair_weapon.done">>
<<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_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 = "yard_find_3">>
<<set $game_state.current_node = "yard_find_3.base">>
<<script>>setup.markCanvasTriggered("yard_find_3");<</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_renner_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 = "yard_find_2">>
<<set $game_state.current_node = "yard_find_2.base">>
<<script>>setup.markCanvasTriggered("yard_find_2");<</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_renner_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 = "yard_find_1">>
<<set $game_state.current_node = "yard_find_1.base">>
<<script>>setup.markCanvasTriggered("yard_find_1");<</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_renner_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 = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.base">>
<<script>>setup.markCanvasTriggered("yard_crawl");<</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('yard_crawl:cc0') ? '' : 'unlocked-choice'">
<<link "Push in past the gate." "Canvas_yard_crawl_Node_guard1">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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('yard_crawl:cc1') ? '' : 'unlocked-choice'">
<<link "Work deeper in." "Canvas_yard_crawl_Node_guard2">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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('yard_crawl:cc2') ? '' : 'unlocked-choice'">
<<link "Push to the back of it." "Canvas_yard_crawl_Node_guard3">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "energy", "add", -15, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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 = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.guard1">>
<<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('yard_crawl:cc3') ? '' : 'unlocked-choice'">
<<link "Slip past him, unseen." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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('yard_crawl:cc4') ? '' : 'unlocked-choice'">
<<link "Take him down." "Canvas_yard_crawl_Node_deeper">><<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("yard_crawl: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('yard_crawl:cc5') ? '' : 'unlocked-choice'">
<<link "Use the weapon on him." "Canvas_yard_crawl_Node_weapon_scene">><<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("yard_crawl: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('yard_crawl:cc6') ? '' : 'unlocked-choice'">
<<link "He's on you — break and run." "Canvas_yard_crawl_Node_flee">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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_renner_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 = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.guard2">>
<<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('yard_crawl:cc7') ? '' : 'unlocked-choice'">
<<link "Slip past them, unseen." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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('yard_crawl:cc8') ? '' : 'unlocked-choice'">
<<link "Take them down." "Canvas_yard_crawl_Node_deeper">><<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("yard_crawl: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('yard_crawl:cc9') ? '' : 'unlocked-choice'">
<<link "Use the weapon on them." "Canvas_yard_crawl_Node_weapon_scene">><<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("yard_crawl: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('yard_crawl:cc10') ? '' : 'unlocked-choice'">
<<link "Too many — break and run." "Canvas_yard_crawl_Node_flee">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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_renner_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 = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.guard3">>
<<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('yard_crawl:cc11') ? '' : 'unlocked-choice'">
<<link "Ghost straight through them." "Canvas_yard_crawl_Node_deeper">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyTrait("player", null, "yard_depth", "add", 1.0, false, null);<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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('yard_crawl:cc12') ? '' : 'unlocked-choice'">
<<link "Cut through them." "Canvas_yard_crawl_Node_deeper">><<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("yard_crawl: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('yard_crawl:cc13') ? '' : 'unlocked-choice'">
<<link "Use the weapon on them." "Canvas_yard_crawl_Node_weapon_scene">><<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("yard_crawl: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('yard_crawl:cc14') ? '' : 'unlocked-choice'">
<<link "Not yet — break and run." "Canvas_yard_crawl_Node_flee">><<script>>setup.pendingEffects = [];<</script>><<script>>setup.applyAndNotifyFlag("player", null, "yard_caught_once", "set");<</script>><<script>>setup.showEffectNotification();<</script>><<script>>setup.markChoiceVisited("yard_crawl: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_renner_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 = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.deeper">>
<<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_renner_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 = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.weapon_scene">>
<<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_renner_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 = "yard_crawl">>
<<set $game_state.current_node = "yard_crawl.flee">>
<<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 = "inv_facility_explore">>
<<set $game_state.current_node = "inv_facility_explore.base">>
<<script>>setup.markCanvasTriggered("inv_facility_explore");<</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_facility_ruins]]<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 = "activity_train">>
<<set $game_state.current_node = "activity_train.base">>
<<script>>setup.markCanvasTriggered("activity_train");<</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('activity_train:cc0') ? '' : 'unlocked-choice'">
<<link "Combat drill." "Canvas_activity_train_Node_combat">><<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("activity_train: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('activity_train:cc1') ? '' : 'unlocked-choice'">
<<link "Combat drill." "Canvas_activity_train_Node_combat">><<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("activity_train: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('activity_train:cc2') ? '' : 'unlocked-choice'">
<<link "Stealth drill." "Canvas_activity_train_Node_stealth">><<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("activity_train: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('activity_train:cc3') ? '' : 'unlocked-choice'">
<<link "Stealth drill." "Canvas_activity_train_Node_stealth">><<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("activity_train: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_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 = "activity_train">>
<<set $game_state.current_node = "activity_train.combat">>
<<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_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 = "activity_train">>
<<set $game_state.current_node = "activity_train.stealth">>
<<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_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 = "canvas_chip_view">>
<<set $game_state.current_node = "canvas_chip_view.base">>
<<script>>setup.markCanvasTriggered("canvas_chip_view");<</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_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 = "activity_recharge_emitter">>
<<set $game_state.current_node = "activity_recharge_emitter.base">>
<<script>>setup.markCanvasTriggered("activity_recharge_emitter");<</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_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 = "activity_recharge_drain">>
<<set $game_state.current_node = "activity_recharge_drain.base">>
<<script>>setup.markCanvasTriggered("activity_recharge_drain");<</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_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 = "activity_charge_up">>
<<set $game_state.current_node = "activity_charge_up.base">>
<<script>>setup.markCanvasTriggered("activity_charge_up");<</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_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 = "activity_recharge">>
<<set $game_state.current_node = "activity_recharge.base">>
<<script>>setup.markCanvasTriggered("activity_recharge");<</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_cradle]]<p>The car idles at the curb where the docks meet the road. Up to the towers — or down under the boards, into the other half of the Reach, the part that doesn't come up in daylight.</p>
<<set $game_state.current_canvas = "activity_travel_to_spire">>
<<set $game_state.current_node = "activity_travel_to_spire.base">>
<<script>>setup.markCanvasTriggered("activity_travel_to_spire");<</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>>
<<nobr>>
<<link "Ride up to the Spire." "Canvas_activity_travel_to_spire_Node_riding_spire">><<script>>advanceTime(3);<</script>><</link>><br>
<<if setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('activity_travel_to_spire:cc0') ? '' : 'unlocked-choice'">
<<link "Down under the docks — the Underworld." "Canvas_activity_travel_to_spire_Node_riding_under">><<script>>setup.markChoiceVisited("activity_travel_to_spire:cc0");<</script>><<script>>advanceTime(3);<</script>><</link>><br>
</span>
<</if>>
<</nobr>><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 = "activity_travel_to_spire">>
<<set $game_state.current_node = "activity_travel_to_spire.riding_spire">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_spire.riding_spire") === -1) { vn.push("activity_travel_to_spire.riding_spire"); }<</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>She leaves the car and goes down on foot — off the lit strip, past the boards, to the freight door under the docks where the man already has his hand out.</p>
<<set $game_state.current_canvas = "activity_travel_to_spire">>
<<set $game_state.current_node = "activity_travel_to_spire.riding_under">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_spire.riding_under") === -1) { vn.push("activity_travel_to_spire.riding_under"); }<</script>>
<<script>>advanceTime(10);<</script>>
[[Down to the gate.->Location_underworld_gate]]<p>A company car idles at the curb. Down to the Reach and the docks, or further down still — under them, to the half of the city that never comes up in daylight.</p>
<<set $game_state.current_canvas = "activity_travel_to_reach">>
<<set $game_state.current_node = "activity_travel_to_reach.base">>
<<script>>setup.markCanvasTriggered("activity_travel_to_reach");<</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('activity_travel_to_reach:cc0') ? '' : 'unlocked-choice'">
<<link "Ride down to the Reach." "Canvas_activity_travel_to_reach_Node_riding">><<script>>setup.markChoiceVisited("activity_travel_to_reach: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 setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})>>
<span @class="setup.isChoiceVisited('activity_travel_to_reach:cc1') ? '' : 'unlocked-choice'">
<<link "Ride down to the Underworld." "Canvas_activity_travel_to_reach_Node_riding_underworld">><<script>>setup.markChoiceVisited("activity_travel_to_reach:cc1");<</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": "hygiene", "operator": "gte", "value": 40}]})) or (setup.triggerConditionsSatisfied({"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]})))>>
<<script>>
try {
window._engineNoExitsDiag_activity_travel_to_reach = [{"text": "Ride down to the Reach.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "trait", "subject": "player", "trait_key": "hygiene", "operator": "gte", "value": 40}]}}, {"text": "Ride down to the Underworld.", "conditions": {"version": "1.0", "logic": "AND", "items": [{"type": "flag", "subject": "player", "flag_key": "opening_done", "operator": "is_true"}]}}];
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 = "activity_travel_to_reach">>
<<set $game_state.current_node = "activity_travel_to_reach.riding">>
<<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]]<p>She gives an address that isn't on any manifest. The car takes her down past the docks and lets her out at a freight door under everything, where the city goes down instead of up. The driver doesn't wait to see her through it.</p>
<<set $game_state.current_canvas = "activity_travel_to_reach">>
<<set $game_state.current_node = "activity_travel_to_reach.riding_underworld">>
<<script>>var vn = State.variables.game_state.visited_nodes;if (vn.indexOf("activity_travel_to_reach.riding_underworld") === -1) { vn.push("activity_travel_to_reach.riding_underworld"); }<</script>>
<<script>>advanceTime(30);<</script>>
<<script>>setup.pendingEffects = [];setup.applyAndNotifyTrait("player", null, "energy", "add", -10.0, true, null);setup.showEffectNotification();<</script>>
[[Out at the gate.->Location_underworld_gate]]<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 = "inv_facility_match">>
<<set $game_state.current_node = "inv_facility_match.base">>
<<script>>setup.markCanvasTriggered("inv_facility_match");<</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_facility_ruins]]<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 = "canvas_glitch_recurrence_ii">>
<<set $game_state.current_node = "canvas_glitch_recurrence_ii.base">>
<<script>>setup.markCanvasTriggered("canvas_glitch_recurrence_ii");<</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_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 = "canvas_glitch_recurrence_i">>
<<set $game_state.current_node = "canvas_glitch_recurrence_i.base">>
<<script>>setup.markCanvasTriggered("canvas_glitch_recurrence_i");<</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_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-b2-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-b2-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-b2-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-b2-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 = "canvas_opening_morning">>
<<set $game_state.current_node = "canvas_opening_morning.morning">>
<<script>>setup.markCanvasTriggered("canvas_opening_morning");<</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-b2-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-b2-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-b2-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-b2-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="Wren" 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>💭 <<print $player.name>> is 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-b2-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-b2-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_cradle]]
<</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span><</linkreplace>></span></span>
<<set $game_state.current_canvas = "canvas_opening_night">>
<<set $game_state.current_node = "canvas_opening_night.night">>
<<script>>setup.markCanvasTriggered("canvas_opening_night");<</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>>