/* React globals — available via CDN in browser, injected in Claude artifact context */
const { useState, useReducer, useEffect, useRef } = React;

/* ══════════════════════════════════════════════════════════════
   VERSION + UPDATE NOTES — edit here to update the title screen
   ══════════════════════════════════════════════════════════════ */
const GAME_VERSION = "0.3.4";
const UPDATE_NOTES = `
v0.3.4 — Menu Polish
• Main menu: Create a Lobby is now green, How to Play is now red
• Removed "2-6 players" subtitle from Create a Lobby — text centered and larger
• Brightened low-contrast text across home screen and multiplayer lobby (tagline, "or join existing", QR code description, "Connecting…", etc.)

v0.3.3 — Update Notification
• Home screen now shows a dismissable banner whenever a new version is deployed — links to What's New and nudges toward Feedback. Stored in localStorage so it only appears once per version.

v0.3.2 — Polish
• Tutorial mobile UI slide redrawn to match the actual screen layout (hand at bottom, deck/shop tiles on main tab, stats bar, etc.)
• Card play animation no longer floats during hold — just bounces in and stays
• Blind Buy box stays visible after a purchase but dims and locks, avoiding layout shift

v0.3.1 — Animations
• Card play: flies up from bottom with overshoot, floats gently, then exits upward
• Mobile shop: brief "PURCHASED" overlay with fade-out when buying an asset
• Reaction window: bouncy spring entrance; amber border flash + soft glow when it's your turn to react

v0.3.0 — Bug Fixes
• Close of Business can no longer be used during your own turn
• Clicking the triggered card name in the reaction window now opens its detail view
• Reaction window now shows the correct verb and context for all trigger types (start of turn, activation, reaction)
• Risk vs Reward now correctly fires when a player loses money through Kickstarter

v0.2.9 — Shop Visual Update
• Mobile shop panel now uses an amber/gold color scheme to visually distinguish it from the asset list

v0.2.8 — Reaction Window Redesign
• Redesigned reaction window with red color scheme, centered header, and cleaner card selection
• Cards now show type label and cost; SELECT/SELECTED buttons stay aligned regardless of card content
• Added context sentence (targeted player, loss amount, or card description) below the triggered card name
• Added info button explaining what a reaction window is
• React and Pass buttons replace the old inline Play buttons; React requires selecting a card first

v0.2.7 — Bug Fixes
• Fixed app failing to load (Babel 8 JSX compatibility — bare > in text is no longer allowed)
• Business Partners now correctly resolves the original card after copying it — Total Loss and similar effects no longer get stuck
• Downsizing now correctly re-enables the disabled asset at the end of the targeted player's current turn (not next turn)

v0.2.6 — UI Refinements
• Toast notifications now appear above the hand and action bar instead of covering them
• Emojis removed from toast notifications (desktop and mobile)
• Lobby and waiting room now show the brick background aesthetic matching the rest of the app
• Mode select screen cleaned up: removed divider line, simplified Create a Lobby button

v0.2.5 — Visual Polish & Cleanup
• Brick wall texture now appears throughout the UI background — dark and on-brand, replaces the solid color
• Title/mode-select screen modernized with larger logo, cleaner layout, and updated styling
• Gain/loss animations now move only a short distance from your balance instead of floating far across the screen
• Emojis removed from the game log, activity feed, and UI elements throughout

v0.2.4 — Bug Fixes & UI Polish
• Business Partners now correctly waits for the original card or asset to fully resolve before copying its effect — no more interruption
• Notification popups now have a dark background with a colored left accent stripe instead of a tinted colored background, making them easier to read
• Money gain/loss animations return to floating just above (gains) or below (losses) your balance total
• Action notifications in multiplayer mobile now appear at the bottom of the screen above the action bar (matching single-device behavior)

v0.2.3 — Mobile Refinements
• Money gain/loss now floats in the center of the screen instead of overlapping your balance
• Your balance animates (counts up or down) to the new total when it changes by more than $1k
• Opponent cards now flash a full-card overlay (green for gains, red for losses) matching the desktop style
• Notifications moved to the bottom of the screen just above the action bar, so they no longer crowd the top
• Reaction window is now a centered popup with space on all sides (was a bottom sheet)
• In card-selection popups (e.g. Client Theft, discard prompts) tapping a card now shows its detail view; only the button below it performs the action

v0.2.2 — UX Polish & Quality of Life
• Loading screen: animated spinner now shows while the game loads instead of a blank screen
• Action bar now shows whose turn it is (with a pulsing color dot) when it's not your turn
• Hand card count badge (e.g. "3/5") shown in the corner of the hand area; turns red when at the limit
• Your balance now animates gains and losses — gains float upward, losses float downward from the money display
• Opponent money changes are shown with an animated overlay on their card
• Overdraft fee tracker color-codes by urgency: neutral at $1k, amber at $2k, red at $3k
• Bankruptcy warning: a pulsing ⚠ appears next to your balance below -$6k; tap it for a reminder of what happens at -$10k
• Action point pips (dots) now shown under the action count so remaining actions are easier to read at a glance
• Player names are remembered between sessions — name fields pre-fill with whatever you typed last time
• "?" button added to the mobile header to open the How to Play guide at any point during a game
• Reaction window on mobile is now a centered modal with larger touch targets and a thicker timer bar

v0.2.1 — Mobile UI Overhaul & Fixes
• Hand cards: bigger and easier to tap, no longer clip when selected, and now show a faint tint of their type color (blue/red/green) even when not selected
• Hand cards now display a small ⊘ marker (and a dimmed look) when they can't currently be played - e.g. blocked by Monopoly or Part Time Work - and the card detail panel explains why
• End Turn moved into the main action bar; tapping it opens a centered confirmation popup, and it now pulses gently when you have no actions left
• Single-device mode: added a "Viewing" switcher so you can change which player's hand/board you're looking at, even after a card effect changes the active seat
• Opponent info popup now slides down from the top of the screen instead of sliding up from the bottom (clears the notch/status bar area)
• "S" and "B" in the top-left logo are now colored green and red
• Shop tile on the Main tab now shows a "TAP TO VIEW" hint
• Fixed: Cover the Costs wasn't appearing as an eligible reaction when paying out money during Negative Reaction's resolution
• Tutorial: slides now scroll back to the top when you move between them
• Tutorial: card names in the lesson text are now bold and clickable, opening that card's detail view
• Tutorial: corrected the Bankruptcy explanation - you reset to $0k, discard your hand (gaining $1k per card discarded), and lose only your single most valuable asset
• Tutorial: fixed the Next/Finish button sometimes getting clipped by the screen edge on mobile

v0.2.0 — Tutorial Overhaul
• How To Play updated throughout to reflect Free action mode as the default
• Action cards slide: callout now correctly explains Free vs Limited mode
• Selling slide: no longer says "exactly one per turn" (that's Limited mode only)
• Full-turn walkthrough: planning phase now explains Free mode action points with a note about Limited mode
• Assets & shop free actions: clarified that buying assets and activating DURING assets cost no action points
• Welcome slide: game length (default 15 turns) now shown alongside the scoring rules
• Cheat sheet: updated all tips; added explicit "Assets & buys are free" and "Hand limit is 5" entries

v0.1.9 — Bug Fixes
• Discount asset now correctly gains a token when any player uses the blind buy (deck) option, not only shop buys
• Full Refund on Client Theft and Help Wanted (targeted cards) now asks you to choose an opponent first, instead of incorrectly targeting yourself
• How To Play tutorial: slides 6, 7, and the full-turn walkthrough were blank due to "Budget Cut" being looked up instead of "Budget Cuts" — all three slides now load correctly

v0.1.8 — Reaction Card Fixes
• Overtime no longer resets the action counter to full in Free mode; now visibly costs 1 action point
• Refusal to Work now correctly skips the player from all targeting effects (Kickstarter, Negative Reaction, Shrinkage, Let Go, Pocket Change) instead of still applying them after the refusal
• Refusal to Work now gains the correct card value (the card that targeted you) when reacting to Let Go
• Repeat Customers now actually resets the activated asset to READY (the reset was silently dropped before)
• Early Lead and Startup Costs now fire Retainer (e.g. Recession stored) and Accountant correctly
• Recession (via Retainer) now triggers when players lose money to Early Lead or Startup Costs

v0.1.7 — Trigger Audit & Bug Fixes
• Plus Interest now correctly fires on: Toll, Workman's Comp, Water Damage, Let Go, Client Theft, Early Lead, Last Minute Bid, Inflation, Startup Costs
• Accountant retaliation now correctly fires on: Water Damage, Let Go, Client Theft, Last Minute Bid, Inflation, Startup Costs
• Kickstarter now correctly applies Loss Prevention, Plus Interest, Accountant, and Side Hustle per payer
• Cover the Costs / Risky Investment can now react to losses from Last Minute Bid and Inflation
• Scammed now steals the correct gain amount instead of always stealing $4k
• Removed two orphaned dead-code hooks (Hostile Bid, Class Action) that were never reachable

v0.1.6 — Balance Pass & Settings Improvements
• Three of a Kind reworked: now requires discarding 3 cards worth exactly $2k (was: 3 of same value)
• Three of a Kind: Product Update can now adjust both the card count and required card value
• Loss Prevention reworked: flat $1k reduction on all losses (not purchases); blocks $1k passives like Toll and Workman's Comp; hard floor at $0
• Out of Office moved to $3k, 1 copy (was $2k, 2 copies)
• Client Theft moved to $1k (was $2k)
• Fresh Supplies moved to $1k (was $2k)
• Buying Supplies reduced to 1 copy (was 2)
• A Minor Loss increased to 2 copies (was 1)
• Free action mode is now the default; Limited mode still available in settings
• R&D Budget automatically disabled in Free action mode and re-enabled when switching to Limited

v0.1.5 — Bug Fixes & Balance Pass
• Recession no longer double-charges the losing player
• Refusal to Work only reacts to action/reaction cards (not assets)
• Worth the Price now shows the correct card value to beat; successfully cancels the reacted card
• Closed for Remodeling cooldown now properly blocks the same asset for one full cycle
• Cover the Costs now triggers on Water Damage and other direct losses
• Back to Basics can only target the player whose action you are reacting to
• Extra Shift correctly grants +1 action (not +2)
• Expense Report now properly triggers Loss Prevention, Plus Interest, and Accountant
• Taxes badge no longer shows "DISABLED" alongside "TAXED"
• CFR modal now appears correctly on all device/screen configurations
• Bribery description updated with full activation requirements
• QR code added to multi-device lobby for easy link sharing

v0.1.4 — Initial Playtest Build
• All core cards implemented
• Single-device and multi-device modes
• Mobile and desktop views
• Added 'Feedback' button to submit bugs
`;
/* ══════════════════════════════════════════════════════════════ */


/* -
   STRICTLY BUSINESS - v1
   Card submission template:
   NAME: Card Name
   TYPE: ACTION | REACTION
   VALUE: 1-5
   COUNT: 2
   TARGET: none | self | opponent
   DESCRIPTION: Effect text.
   HOOK: effect_hook_name
   TRIGGERS: ACTION_PLAYED -> TARGET_PLAYER -> DISCARD_CARD -> GAIN_MONEY(value)
   LISTENERS: (trigger types this card reacts to, if reaction)
   NOTES: Any special rules.
- */

const CSS = `
  @import url('https://fonts.googleapis.com/css2?family=Rubik:wght@600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;600&display=swap');
  *{box-sizing:border-box;margin:0;padding:0}
  body,#root{
    background-color:#14100d;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='36'%3E%3Crect width='80' height='36' fill='%2314100d'/%3E%3Crect x='1' y='1' width='37' height='15' fill='%231e1913' rx='1'/%3E%3Crect x='42' y='1' width='37' height='15' fill='%231b1610' rx='1'/%3E%3Crect x='1' y='20' width='18' height='15' fill='%231b1610' rx='1'/%3E%3Crect x='23' y='20' width='35' height='15' fill='%231e1913' rx='1'/%3E%3Crect x='62' y='20' width='17' height='15' fill='%231b1610' rx='1'/%3E%3C/svg%3E");
    background-repeat:repeat;
    background-size:80px 36px;
  }
  ::-webkit-scrollbar{width:5px;height:5px}
  ::-webkit-scrollbar-track{background:#231e19}
  ::-webkit-scrollbar-thumb{background:#6b5a50;border-radius:3px}
  @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
  @keyframes popIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
  @keyframes slideUp{from{opacity:0;transform:translateY(32px) scale(.97)}to{opacity:1;transform:none}}
  @keyframes slideDown{from{opacity:0;transform:translateY(-32px) scale(.97)}to{opacity:1;transform:none}}
  @keyframes fadeBackdrop{from{opacity:0}to{opacity:1}}
  @keyframes glow{0%,100%{box-shadow:0 0 6px #d9770644}50%{box-shadow:0 0 16px #d9770688}}
  @keyframes moneyOverlayIn{from{opacity:0}to{opacity:1}}
  @keyframes moneyOverlayOut{from{opacity:1;transform:none}to{opacity:0;transform:translateY(-6px)}}
  @keyframes moneyLineIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
  @keyframes toastIn{from{opacity:0;transform:translateY(20px) scale(.95)}to{opacity:1;transform:none}}
  @keyframes notifDropIn{from{opacity:0;transform:translateX(-50%) translateY(-12px)}to{opacity:1;transform:translateX(-50%) translateY(0)}}
  @keyframes notifPulse{0%,100%{box-shadow:0 4px 24px rgba(217,119,6,0.15)}50%{box-shadow:0 4px 32px rgba(245,158,11,0.35)}}
  @keyframes toastOut{from{opacity:1;transform:none}to{opacity:0;transform:translateY(-14px) scale(.97)}}
  @keyframes floatUp{0%{opacity:0;transform:translateX(-50%) translateY(0)}15%{opacity:1}80%{opacity:1;transform:translateX(-50%) translateY(-22px)}100%{opacity:0;transform:translateX(-50%) translateY(-28px)}}
  @keyframes floatDown{0%{opacity:0;transform:translateX(-50%) translateY(0)}15%{opacity:1}80%{opacity:1;transform:translateX(-50%) translateY(20px)}100%{opacity:0;transform:translateX(-50%) translateY(26px)}}
  @keyframes floatUpSmall{0%{opacity:0;transform:translateX(-50%) translateY(4px)}15%{opacity:1;transform:translateX(-50%) translateY(0)}80%{opacity:1;transform:translateX(-50%) translateY(-7px)}100%{opacity:0;transform:translateX(-50%) translateY(-10px)}}
  @keyframes floatDownSmall{0%{opacity:0;transform:translateX(-50%) translateY(-4px)}15%{opacity:1;transform:translateX(-50%) translateY(0)}80%{opacity:1;transform:translateX(-50%) translateY(7px)}100%{opacity:0;transform:translateX(-50%) translateY(10px)}}
  @keyframes pressurePulse { 0%,100%{box-shadow:0 0 4px #ef444466;} 50%{box-shadow:0 0 12px #ef4444cc, 0 0 4px #ef4444;} }
  @keyframes endPulse { 0%,100%{box-shadow:0 0 0 rgba(248,113,113,0); border-color:#7f1d1d;} 50%{box-shadow:0 0 13px rgba(248,113,113,0.7); border-color:#f87171;} }
  @keyframes reactionPulse{0%,100%{box-shadow:0 0 40px rgba(255,255,255,0.08),0 24px 60px #000000cc}50%{box-shadow:0 0 70px rgba(255,255,255,0.22),0 0 30px rgba(255,255,255,0.12),0 24px 60px #000000cc}}
  @keyframes cardFlyUp{0%{opacity:0;transform:translateY(130px) scale(1.5)}60%{opacity:1;transform:translateY(-12px) scale(2.45)}80%{transform:translateY(5px) scale(2.37)}100%{opacity:1;transform:translateY(0) scale(2.4)}}
  @keyframes cardFloat{0%,100%{transform:translateY(0) scale(2.4)}50%{transform:translateY(-10px) scale(2.4)}}
  @keyframes cardFlyOut{0%{opacity:1;transform:translateY(0) scale(2.4)}100%{opacity:0;transform:translateY(-44px) scale(2.15)}}
  @keyframes overlayFadeIn{from{opacity:0}to{opacity:1}}
  @keyframes overlayFadeOut{from{opacity:1}to{opacity:0}}
  @keyframes reactionWindowEnter{0%{opacity:0;transform:translateY(64px) scale(0.87)}55%{opacity:1;transform:translateY(-5px) scale(1.016)}78%{transform:translateY(2px) scale(0.998)}100%{opacity:1;transform:translateY(0) scale(1)}}
  @keyframes reactionBorderFlash{0%{border-color:#3d1808}40%{border-color:#f97316;box-shadow:0 0 28px #f9731633}100%{border-color:#8b3d18;box-shadow:none}}
  @keyframes reactionActiveGlow{0%,100%{box-shadow:0 0 20px rgba(249,115,22,0.08)}50%{box-shadow:0 0 40px rgba(249,115,22,0.22)}}
  @keyframes shopPurchased{0%{opacity:1;transform:scale(1)}40%{transform:scale(1.03)}100%{opacity:0;transform:scale(0.93) translateY(-10px)}}
  @keyframes shopPurchasedText{0%{opacity:0;transform:scale(0.7) translateY(6px)}30%{opacity:1;transform:scale(1.08)}60%{transform:scale(1)}85%{opacity:1}100%{opacity:0}}
  @keyframes borderGlow{0%,100%{border-color:var(--rw-col,#f97316)}50%{border-color:var(--rw-col,#f97316);filter:brightness(1.5)}}
  @keyframes brickPulse{0%,100%{box-shadow:inset 0 0 0 2px #4a3f3522}50%{box-shadow:inset 0 0 0 2px #4a3f3566}}
  button{transition:transform .1s ease,filter .1s ease!important}
  button:not([disabled]):hover{transform:scale(1.05)!important;filter:brightness(1.18)!important}
  button:not([disabled]):active{transform:scale(0.96)!important;filter:brightness(0.9)!important}
  .brick-panel,.sb-bg-warm{
    background-color:#14100d;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='36'%3E%3Crect width='80' height='36' fill='%2314100d'/%3E%3Crect x='1' y='1' width='37' height='15' fill='%231e1913' rx='1'/%3E%3Crect x='42' y='1' width='37' height='15' fill='%231b1610' rx='1'/%3E%3Crect x='1' y='20' width='18' height='15' fill='%231b1610' rx='1'/%3E%3Crect x='23' y='20' width='35' height='15' fill='%231e1913' rx='1'/%3E%3Crect x='62' y='20' width='17' height='15' fill='%231b1610' rx='1'/%3E%3C/svg%3E");
    background-repeat:repeat;
    background-size:80px 36px;
  }
  .sb-bg-blue{
    background-color:#060912;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='36'%3E%3Crect width='80' height='36' fill='%23060912'/%3E%3Crect x='1' y='1' width='37' height='15' fill='%230d1525' rx='1'/%3E%3Crect x='42' y='1' width='37' height='15' fill='%230b1220' rx='1'/%3E%3Crect x='1' y='20' width='18' height='15' fill='%230b1220' rx='1'/%3E%3Crect x='23' y='20' width='35' height='15' fill='%230d1525' rx='1'/%3E%3Crect x='62' y='20' width='17' height='15' fill='%230b1220' rx='1'/%3E%3C/svg%3E");
    background-repeat:repeat;
    background-size:80px 36px;
  }
`
;

/* - Constants - */
const MIN_PLAYERS = 3;
const MAX_PLAYERS = 6;
const START_MONEY = 10;
const START_FEE   = 1;
const HAND_LIMIT  = 5;
const BLIND_BUY   = 7;
const MIN_BUY     = 1;
const BANKRUPT_AT = -10;
const DEF_ACTIONS = 2;
const INIT_SHOP   = 2;

// - Card play animation config - easy to tune -
const ANIM_FADE_IN_MS  = 300;   // fade-in duration
const ANIM_HOLD_MS     = 1400;  // how long card is fully visible
const ANIM_FADE_OUT_MS = 400;   // fade-out duration
const ANIM_CARD_SCALE  = 2.4;   // card size multiplier (relative to normal)


const PH  = { SOT:"SOT", BP:"BP" };
const CT  = { ACTION:"ACTION", REACTION:"REACTION", ASSET:"ASSET" };
const AS  = { PASSIVE:"PASSIVE", DURING:"DURING", ONCE:"ONCE", ANYTIME:"ANYTIME" };
const ST  = { READY:"READY", USED:"USED" };
const ACT = { DRAW:"DRAW", ACTION:"ACTION", SELL:"SELL" };

// Trigger types - for display + reaction window filtering
const T = {
  ACTION_PLAYED:"ACTION_PLAYED", REACTION_PLAYED:"REACTION_PLAYED",
  TARGET_PLAYER:"TARGET_PLAYER", GAIN_MONEY:"GAIN_MONEY", LOSE_MONEY:"LOSE_MONEY",
  DRAW_CARD:"DRAW_CARD",         DISCARD_CARD:"DISCARD_CARD",
  SELL_CARD:"SELL_CARD",         BUY_ASSET:"BUY_ASSET",
  ACTIVATE_ASSET:"ACTIVATE_ASSET", START_TURN:"START_TURN", END_TURN:"END_TURN",
  PAY_FEES:"PAY_FEES",           PAY_MONEY:"PAY_MONEY",   BANKRUPTCY:"BANKRUPTCY",
  SHOP_REFRESH:"SHOP_REFRESH",   END_GAME:"END_GAME",
  STEAL_ASSET:"STEAL_ASSET",
  SKIP_TURN:"SKIP_TURN",
  CANCEL_ASSET:"CANCEL_ASSET",
};
const TCOLOR = {
  [T.ACTION_PLAYED]:"#60a5fa",   [T.REACTION_PLAYED]:"#f87171",
  [T.TARGET_PLAYER]:"#fb923c",   [T.GAIN_MONEY]:"#4ade80",
  [T.LOSE_MONEY]:"#ef4444",      [T.DRAW_CARD]:"#c084fc",
  [T.DISCARD_CARD]:"#94a3b8",    [T.SELL_CARD]:"#fbbf24",
  [T.BUY_ASSET]:"#34d399",       [T.ACTIVATE_ASSET]:"#67e8f9",
  [T.START_TURN]:"#6b7280",      [T.END_TURN]:"#6b7280",
  [T.PAY_FEES]:"#f97316",        [T.BANKRUPTCY]:"#ef4444",
  [T.SHOP_REFRESH]:"#38bdf8",    [T.END_GAME]:"#f59e0b",
  [T.STEAL_ASSET]:"#a78bfa",
  [T.SKIP_TURN]:"#f59e0b",
  [T.CANCEL_ASSET]:"#94a3b8",
};
const tc = t => TCOLOR[t] || "#94a3b8";

/* - Palette - */
const C = {
  bg:"#1a1410", panel:"#231e19", border:"#4a3f35", hi:"#2e2620",
  text:"#e8ddd4", sub:"#a89080", muted:"#6b5a50",
  gold:"#d97706", green:"#22c55e", red:"#ef4444", blue:"#60a5fa",
};
const PCOLORS = ["#f59e0b","#3b82f6","#ef4444","#22c55e","#a855f7","#ec4899"];

/* - Card type styles - */
const CUI = {
  [CT.ACTION]:   {bg:"#0e1520",bd:"#2563eb",ac:"#93c5fd",lbl:"ACTION"},
  [CT.REACTION]: {bg:"#1e0d0d",bd:"#dc2626",ac:"#fca5a5",lbl:"REACTION"},
  [CT.ASSET]:    {bg:"#0d1a0d",bd:"#16a34a",ac:"#4ade80",lbl:"ASSET"},
  PASSIVE_ASSET: {bg:"#0f1e10",bd:"#22c55e",ac:"#86efac",lbl:"ASSET"},
};
const SUB_UI = {
  [AS.PASSIVE]:  {col:"#d9f99d",bg:"#1a2e05",lbl:"PASSIVE"},
  [AS.DURING]:   {col:"#fde68a",bg:"#1c1003",lbl:"DURING YOUR TURN"},
  [AS.ONCE]:     {col:"#fdba74",bg:"#1c0e03",lbl:"ONE TIME USE"},
  [AS.ANYTIME]:  {col:"#67e8f9",bg:"#031c1f",lbl:"ANYTIME"},
};

/* - Utils - */
var _rngState = Date.now() & 0xFFFFFFFF;
function _rand() {
  _rngState += 0x6D2B79F5;
  var t = _rngState;
  t = Math.imul(t ^ t>>>15, t | 1);
  t ^= t + Math.imul(t ^ t>>>7, t | 61);
  return ((t ^ t>>>14) >>> 0) / 4294967296;
}
const uid  = () => _rand().toString(36).substr(2,9);
const shuf = a => { const b=[...a]; for(let i=b.length-1;i>0;i--){const j=Math.floor(_rand()*(i+1));[b[i],b[j]]=[b[j],b[i]];} return b; };
const $    = n => n>=0 ? `$${n}k` : `-$${Math.abs(n)}k`;
const clmp = (v,lo,hi) => Math.min(Math.max(v,lo),hi);
const pname = (st,id) => st.players.find(p=>p.id===id)?.name || "?";
const setPl  = (st, pid, fn) => ({ ...st, players: st.players.map(p => p.id===pid ? fn(p) : p) });
const curPl  = st => st.players[st.curIdx];

/* - Card templates - */
const ACTIONS_T = [
  {name:"Client Theft",     val:1, hook:"client_theft",     target:"opponent", desc:"Target opponent discards a card and loses its value.", count:2},
  {name:"Fresh Supplies",   val:1, hook:"fresh_supplies",   target:"none",     desc:"Discard your entire hand. Draw 3 new cards.", count:2},
  {name:"Cost of Business", val:2, hook:"cost_of_business", target:"none",     count:2, desc:"Gain $5k, then discard 1 card from your hand."},
  {name:"Full Price",       val:2, hook:"full_price",       target:"none",     count:1, desc:"Gain twice this card's current value."},
  {name:"Refresh",          val:2, hook:"refresh_asset",    target:"none",     count:1, desc:"Reset one of your used assets.",
   descFn:function(a){ return "Reset one of your used assets back to the READY position. This does not apply to disabled assets."; }},
  {name:"Give Back",        val:3, hook:"give_back",        target:"none",     count:1, desc:"The richest opponent pays you $4k. If tied, you choose.",
   descFn:function(a){ return "The player with the most money pays you $4k. If tied, you choose between them. Cannot be played if you have the most money."; }},
  {name:"Negative Reaction",val:3, hook:"negative_reaction",target:"none",     count:1, desc:"All opponents with one or more reaction cards in hand discard one and pay you $2k."},
  {name:"Outside Hire",     val:3, hook:"outside_hire",      target:"none",     count:1, desc:"Steal an opponent\'s asset but lose money equal to its value.",
   descFn:function(a){ return "Steal an asset from an opponent who has 2+ assets. You lose money equal to that asset\'s value. Requires $1k minimum to play."; }},
  {name:"Liquidation",      val:3, hook:"liquidation",        target:"none",     count:1, desc:"Gain $1k per asset in the shop, then refresh it.",
   descFn:function(a){ return "Gain $1k for every asset currently in the shop, then refresh the shop (assets reshuffled + 1 slot added)."; }},
  {name:"Out of Office",    val:3, hook:"out_of_office",       target:"opponent", count:1, desc:"Target opponent skips their next turn."},
  {name:"Kickstarter",       val:5, hook:"kickstarter",     target:"none",     count:1,
   desc:"Opponents pay you at least $2k. Paying more unlocks bonuses.",
   descFn:function(a){ return "All opponents must pay you at least $2k. Paying $3k draws them a card; $4k draws 2 cards; $5k gains them a free blind-buy asset."; }},
  {name:"Pocket Change",    val:3, hook:"pocket_change",    target:"none",     count:1,
   desc:"All opponents show you a card. Gain their combined value.",
   descFn:function(a){ return "All opponents select and show you one card from their hand. You gain money equal to the combined value of all cards shown."; }},
  {name:"Water Damage",     val:2, hook:"water_damage",      target:"none",     count:2,
   desc:"Choose a card from your hand to discard. Each opponent loses money equal to that card's current value."},
  {name:"Used Goods",       val:1, hook:"used_goods",        target:"none",     count:2,
   desc:"Take the top card of the discard pile into your hand."},
  {name:"Pay Day",          val:1, hook:"pay_day",          target:"none", count:2,
   desc:"Gain $1k and draw a card."},
  {name:"Expense Report",   val:1, hook:"expense_report",   target:"none", count:1,
   desc:"All opponents lose $1k. Draw a card.",
   descFn:function(a){ return "Every other player loses $1k. You draw a card from the deck."; }},
  {name:"Taxes",             val:1, hook:"taxes",             target:"none", count:2,
   desc:"Disable an opponent's asset until the end of their next turn."},
  {name:"Dumpster Diving",  val:1, hook:"dumpster_diving",   target:"none",     desc:"Look at the top card of the Main Discard pile. Gain money equal to its value.", count:2},
  {name:"A Quick Buck",     val:1, hook:"quick_buck",        target:"none",     count:2,
   desc:"Discard the top card of the Main Deck, then gain money equal to its value."},
  {name:"Help Wanted",      val:1, hook:"help_wanted",       target:"opponent", count:1,
   desc:"Target opponent gives you 1 card from their hand.",
   descFn:function(a){ return "Target opponent selects a card from their hand to give to you."; }},
  {name:"Delayed Payment",  val:5, hook:"delayed_payment",   target:"none",     count:1,
   desc:"Shuffle face-up into the Main Deck. Gain $10k when drawn.",
   descFn:function(a){ return "Shuffle this card face-up into the Main Deck. You gain $10k when any player draws it. That player discards it and draws a replacement."; }},
  {name:"Interviews",       val:2, hook:"interviews",        target:"none",     count:1,
   desc:"Draw 3 cards, keep 1, put the others back.",
   descFn:function(a){ return "Draw 3 cards from the main deck, choose 1 to keep, and place the other 2 at the bottom of the deck."; }},
  {name:"Part Time Work",   val:2, hook:"part_time_work",    target:"opponent", count:2,
   desc:"Target opponent cannot play Action Cards on their next turn."},
  {name:"Buying Supplies",  val:2, hook:"buying_supplies",   target:"none",     count:1,
   desc:"Draw cards until you have 5 in hand."},
  {name:"Restocked",        val:2, hook:"restocked",         target:"none",     count:1,
   desc:"Refresh the shop and gain $2k.",
   descFn:function(a){ return "Refresh the shop (reshuffles all assets and adds 1 slot) and gain $2k."; }},
  {name:"Holiday Bonus",    val:2, hook:"holiday_bonus",     target:"none",     count:1,
   desc:"Gain $1k for every asset you own.",
   descFn:function(a){ return "Gain $1k for every asset you currently have in your collection."; }},
  {name:"Let Go",           val:2, hook:"let_go",            target:"none",     count:1,
   desc:"All opponents discard a card and lose its value.",
   descFn:function(a){ return "Every opponent who has cards in hand must discard one card of their choice and lose money equal to its value."; }},
  {name:"Take Inventory",   val:2, hook:"take_inventory",    target:"none",     count:1,
   desc:"Gain $1k for every card in your hand including this one."},
  {name:"Robbed",           val:3, hook:"robbed",             target:"opponent", count:1,
   desc:"Steal 2 random cards from another player\'s hand."},
  {name:"Out of Order",     val:3, hook:"out_of_order",      target:"opponent", count:1,
   desc:"Disable an opponent\'s asset. They pay you $5k to re-enable it.",
   descFn:function(a){ return "Disable an opponent\'s asset. It stays disabled until they choose to pay you $5k to re-enable it (can be done at any time)."; }},
  {name:"Overtime",         val:3, hook:"overtime",           target:"none",     count:1,
   desc:"Gain $3k and gain an extra action this turn."},
  {name:"Upgrade",          val:3, hook:"upgrade",            target:"none",     count:1,
   desc:"Destroy one of your assets and take one from the shop for free.",
   descFn:function(a){ return "Discard one of your non-one-time assets, then take any asset from the shop for free. Cannot target disabled assets."; }},
  {name:"Total Loss",        val:4, hook:"total_loss",          target:"none",     count:1,
   desc:"Choose a card value 1-5. Opponents discard all matching cards and pay you $1k each.",
   descFn:function(a){ return "Choose a card value from 1k to 5k. All opponents discard every card in their hand matching that value, then pay you $1k per card discarded."; }},
  {name:"Quick Exchange",    val:4, hook:"quick_exchange",      target:"none",     count:1,
   desc:"Discard 2 cards of equal value from your hand. Draw the top asset from the Asset Deck."},
  {name:"Double Shift",       val:4, hook:"a_double_shift",       target:"none",     count:1,
   desc:"Until the end of your turn, your passive assets\' monetary effects are doubled."},
  {name:"Market Research",   val:4, hook:"market_research",     target:"none",     count:1,
   desc:"Show opponents a card. Those who can\'t match its value pay you $2k.",
   descFn:function(a){ return "Reveal a card from your hand to all opponents. Every opponent who doesn\'t have a card of equal value in their hand pays you $2k."; }},
  {name:"Valuation",         val:5, hook:"valuation",           target:"none",     count:1,
   desc:"Discard a shop asset and gain its value.",
   descFn:function(a){ return "Choose any asset currently in the shop to discard. Gain money equal to its value. The shop slot refills immediately."; }},
  {name:"A Small Fee",       val:5, hook:"small_fee",           target:"none",     count:1,
   desc:"Steal an asset from an opponent who has 2 or more assets."},
  {name:"Budget Cuts",       val:4, hook:"budget_cuts",         target:"opponent", count:1,
   desc:"Target opponent pays you $1k per asset they own.",
   descFn:function(a){ return "Target opponent pays you $1k for each asset in their collection."; }},
  {name:"Shut Down",         val:4, hook:"shut_down",           target:"none",     count:1,
   desc:"All opponents\' assets are disabled until your next turn.",
   descFn:function(a){ return "All assets owned by all opponents are disabled until the start of your next turn."; }},
];
const REACTIONS_T = [
  // listenTo: which triggers open the reaction window for this card
  // Actual eligibility is filtered inside reactorsFor() per-card
  {name:"Ignored",       val:1, hook:"cancel_whole_action", count:3,
   desc:"Cancel an action card as it is played."},
  {name:"Fired",         val:1, hook:"cancel_reaction",    count:3,
   desc:"Cancel a reaction card as it is played."},
  {name:"Defective Unit",  val:1, hook:"defective_unit",                                          count:2, desc:"Cancel the effect of an asset an opponent just activated."},
  {name:"Shrinkage",     val:1, hook:"shrinkage",         count:2,  desc:"When an opponent plays or sells a card, they must also discard one.",
   descFn:function(a){ return "React to an opponent playing an action card or selling a card. They must also discard one card from their hand."; }},

  {name:"Last Minute Bid", val:2, hook:"last_minute_bid",  listenTo:[T.SELL_CARD],  count:1, desc:"React to an opponent\'s sale. They lose money equal to the card\'s value instead of gaining it.",
   descFn:function(a){ return "React when an opponent sells a card. Instead of gaining its value, the seller loses that amount. The card is discarded."; }},
  {name:"Not A Chance",    val:2, hook:"not_a_chance",     listenTo:[T.TARGET_PLAYER], count:2, desc:"React when you are targeted. Cancel the targeting effect. The card may still affect others."},
  {name:"A Minor Loss",    val:2, hook:"minor_loss",         count:2, desc:"React when you are in the negatives, discard a card from your hand and reset your balance to $1k."},
  {name:"Equal Pay",        val:2, hook:"equal_pay",          listenTo:[T.SELL_CARD], count:1, desc:"When an opponent sells a card, gain the same amount they do.",
   descFn:function(a){ return "React when an opponent sells a card. You gain the same amount of money they receive from the sale."; }},
  {name:"Cover the Costs",  val:2, hook:"cover_the_costs",   listenTo:[T.LOSE_MONEY, T.PAY_MONEY], count:1, desc:"When you lose money, redirect the loss to another player.",
   descFn:function(a){ return "React when you are about to lose money. Redirect that loss to another player of your choice."; }},
  {name:"Risky Investment", val:3, hook:"risky_investment",  listenTo:[T.LOSE_MONEY, T.PAY_MONEY], count:1, desc:"React when you are about to lose money (including buying assets). Gain that amount instead of losing it."},
  {name:"Refusal to Work",  val:3, hook:"refusal_to_work",   listenTo:[T.TARGET_PLAYER], count:2, desc:"Ignore being targeted and gain money equal to the card\'s value.",
   descFn:function(a){ return "React when you are targeted by an action or reaction card. The effect is ignored and you gain money equal to that card\'s value."; }},
  {name:"Repeat Customers", val:3, hook:"repeat_customers",  listenTo:[T.ACTIVATE_ASSET], count:1, desc:"After activating one of your assets, reset it to READY.",
   descFn:function(a){ return "React immediately after activating one of your own assets to reset it back to READY, allowing it to be used again this turn."; }},
  {name:"Business Partners", val:5, hook:"business_partners", listenTo:[T.ACTION_PLAYED, T.BUY_ASSET, T.ACTIVATE_ASSET], count:1, desc:"Copy the last action or asset effect used by an opponent.",
   descFn:function(a){ return "Copy a non-blacklisted action card or asset activation just used by an opponent. Both you and the original player resolve the effect."; }},
  {name:"Downsizing",       val:2, hook:"downsizing",        listenTo:[T.ACTIVATE_ASSET], count:1, desc:"Cancel an opponent\'s asset activation and disable it.",
   descFn:function(a){ return "React when an opponent activates an asset. Cancel the effect and disable that asset until the end of their current turn."; }},
  {name:"Recession",        val:3, hook:"recession",         listenTo:[T.LOSE_MONEY, T.PAY_MONEY], count:1, desc:"React when any other player loses money. They pay you that amount instead."},
  {name:"Property Theft",   val:3, hook:"property_theft",    listenTo:[T.ACTION_PLAYED], count:1, desc:"Steal the action card an opponent just played and use it yourself.",
   descFn:function(a){ return "React to an opponent playing an action card. Steal it and activate its effect as if you had played it."; }},
  {name:"Scammed",          val:4, hook:"scammed",           listenTo:[T.GAIN_MONEY], count:1, desc:"React when an opponent gains money. You gain that money instead."},
  {name:"Competitor",       val:4, hook:"competitor",        listenTo:[T.BUY_ASSET], count:1, desc:"When an opponent buys an asset, gain what they paid.",
   descFn:function(a){ return "React when an opponent buys an asset from the shop. Gain money equal to the price they paid."; }},
  {name:"Inflation",        val:5, hook:"inflation",         listenTo:[T.BUY_ASSET], count:1, desc:"Add $6k to the cost of an asset an opponent is trying to buy.",
   descFn:function(a){ return "React when an opponent attempts to buy a shop asset. Add $6k to its purchase price for them."; }},
  {name:"Equity",           val:5, hook:"equity",            listenTo:[T.BUY_ASSET], count:1, desc:"When an opponent buys an asset, draw a free asset from the deck.",
   descFn:function(a){ return "React when an opponent buys an asset. Draw the top card from the Asset Deck and add it to your collection for free."; }},
  {name:"Back to Basics",   val:5, hook:"back_to_basics",    listenTo:[T.ACTION_PLAYED, T.BUY_ASSET, T.ACTIVATE_ASSET], count:1, desc:"React and force an opponent with 3 or more assets to discard one of them (their choice)."},
  
  {name:"Close of Business", val:4, hook:"close_of_business",
   listenTo:[T.START_TURN, T.ACTION_PLAYED, T.REACTION_PLAYED, T.ACTIVATE_ASSET], count:1,
   desc:"During another player\'s turn, immediately ends that player\'s turn."},
];

const PU_FIELDS = {
  "p_leave_tip":        [{ key:"tipAmount",           label:"Tip amount",         floor:0 }],
  "p_high_valued":      [{ key:"highValuedAmount",     label:"Value boost",        floor:0 }],
  "p_price_adj":        [{ key:"priceAdjAmount",       label:"Price surcharge",    floor:0 }],
  "p_tax":              [{ key:"taxValue", label:"Trigger value", floor:1 }, { key:"taxAmount", label:"Gain amount", floor:0 }],
  "p_couple_bucks":     [{ key:"coupleBucksValue", label:"Trigger value", floor:1 }, { key:"coupleBucksAmount", label:"Gain amount", floor:0 }],
  "p_accountant":       [{ key:"accountantThreshold", label:"Loss threshold", floor:1 }, { key:"accountantAmount", label:"Retaliation", floor:0 }],
  "p_plus_interest":    [{ key:"plusInterestAmount",   label:"Extra loss",         floor:0 }],
  "p_loss_prevention":  [{ key:"preventionAmount", label:"Reduction amount", floor:0 }],
  "p_repetitive_work":  [{ key:"repetitiveWorkAmount", label:"Trigger value",      floor:1 }],
  "p_toll":             [{ key:"tollAmount",           label:"Toll per action",    floor:0 }],
  "p_startup_costs":    [{ key:"startupCostsAmount",   label:"Turn-start cost",    floor:0 }],
  "p_early_lead":       [{ key:"earlyLeadAmount",      label:"Opponents lose",     floor:0 }],
  "p_rule_of_thirds":   [{ key:"ruleOfThirdsValue", label:"Trigger value", floor:1 }, { key:"ruleOfThirdsAmount", label:"Payment", floor:0 }],
  "p_restock":          [{ key:"restockValue",  label:"Trigger value", floor:1 }, { key:"restockAmount", label:"Gain amount", floor:0 }],
  "p_empty_handed":     [{ key:"emptyHandedAmount", label:"Cards drawn", floor:0 }],
  "p_free_replacement": [{ key:"freeReplacementAmount",label:"Cards drawn",        floor:0 }],
  "p_price_check":      [{ key:"priceCheckValue",      label:"Trigger value",      floor:1 }],
  "p_full_refund":      [{ key:"fullRefundThreshold",  label:"Max sell value",     floor:1 }],
  "p_finders_fee":      [{ key:"findersFeeAmount",     label:"Fee per sale",       floor:0 }],
  "p_discount":         [{ key:"discountAmount", label:"Discount per token", floor:0 }, { key:"tmax", label:"Max tokens", floor:1 }],
  "p_lca":              [{ key:"lcaAmount",            label:"Gain per action",    floor:0 }],
  "p_pressure":         [{ key:"pressureThreshold", label:"Token threshold", floor:1 }, { key:"pressurePenalty", label:"Penalty", floor:0 }],
  "p_portfolio":        [{ key:"portfolioAmount", label:"Payout per passive", floor:0 }, { key:"tmax", label:"Token threshold", floor:1 }],
  "p_swear_jar":        [{ key:"swearJarAmount",       label:"Charge per reaction",floor:0 }],
  "p_side_hustle":      [{ key:"sideHustleAmount",     label:"Extra payment",      floor:0 }],
  "p_targeted_ad":      [{ key:"targetedAdAmount",     label:"Payment on cancel",  floor:0 }],
  "p_interest":         [{ key:"bankBranchAmount", label:"Interest per unit", floor:0 }, { key:"bankBranchRate", label:"Rate unit ($k)", floor:1 }, { key:"bankBranchCap", label:"Interest cap ($k)", floor:0 }],
  "p_sell_p1":          [{ key:"resaleAmount",         label:"Sell bonus",         floor:0 }],
  "p_check_inv":        [{ key:"checkInvAmount",       label:"Gain per draw",      floor:0 }],
  "p_revenue_stream":   [{ key:"revenueAmount", label:"Gain per token", floor:0 }, { key:"revenueMaxPayout", label:"Max payout bonus", floor:0 }, { key:"tmax", label:"Token threshold", floor:1 }],
  "p_workmans_comp":    [{ key:"workmanAmount",        label:"Opponents lose/sell",floor:0 }],
  "p_consolation":      [{ key:"consolationGain",      label:"Assets gained",      floor:0 }],
  "a_extra_income":     [{ key:"extraIncomeAmount",    label:"Gain amount",        floor:0 }],
  "a_risk_reward":      [{ key:"riskRewardAmount",     label:"Gain amount",        floor:0 }],
  "a_lets_deal":        [{ key:"letsDealAmount",       label:"Gain amount",        floor:0 }],
  "a_retainer":         [{ key:"retainerAmount",       label:"Retainer payout",    floor:0 }],
  "a_severance_pay":    [{ key:"tmax",                 label:"Token requirement",  floor:1 }],
  "a_rebranding":       [{ key:"rebrandBonus",         label:"Bonus assets",       floor:0 }],
  "a_bribery":          [{ key:"bribeTokenCost",       label:"Cost per token",     floor:0 }],
  "a_mark_up":          [{ key:"markUpAmount",         label:"Adjustment amount",  floor:0 }],
  "a_paid_work":        [{ key:"paidWorkAmount",       label:"Adjustment amount",  floor:0 }],
  "a_pharma":           [{ key:"tmax",                 label:"Token requirement",  floor:1 }],
  "a_three_of_kind":    [{ key:"tripleCount", label:"Cards required", floor:1 }, { key:"tripleValue", label:"Required card value", floor:1 }],
  "a_rnd_budget":       [{ key:"rndDrawCount", label:"Cards drawn", floor:0 }, { key:"tmax", label:"Token requirement", floor:1 }],
  "a_credit_line":      [{ key:"_creditLineAll",       label:"All prices (±1 each)",floor:0 }],
  "a_trade_in":         [{ key:"tradeInAmount",        label:"Value received",     floor:0 }],
};
const ASSETS_T = [
  {name:"Bank Branch",     val:7, sub:AS.PASSIVE,  hook:"p_interest",   tmax:0, bankBranchAmount:1, bankBranchRate:5, bankBranchCap:3, count:1,
   desc:"Gain $1k interest per $5k held at start of your turn. Capped at $3k per turn.",
   descFn:function(a){ var amt=a.bankBranchAmount||1,rate=a.bankBranchRate||5,cap=(a.bankBranchCap!==undefined&&a.bankBranchCap!==null)?a.bankBranchCap:3; return "Gain $"+amt+"k interest per $"+rate+"k held at the start of your turn. Interest is capped at $"+cap+"k per turn."; }},
  {name:"4 Day Work Week",  val:10,sub:AS.DURING,   hook:"a_pharma",     tmax:4, count:1,
   desc:"Activate to add a work token. When you activate with a full 4 tokens, gain $10k and reset.",
   descFn:function(a){ var n=(a.tokens||[]).length,t=a.tmax||4; return n>=t?"✅ READY: Activate for $10k and reset ("+n+"/"+t+")":"Tokens: "+n+"/"+t+". Activate to add a token. Activate when full for $10k."; }},
  {name:"Let's Make a Deal",val:9, sub:AS.DURING,   hook:"a_lets_deal",   tmax:0, letsDealAmount:1, letsDealTarget:null, count:1,
   desc:"Activate: Target an opponent. Gain $1k whenever they activate a non-passive asset.",
   descFn:function(a){ var t=a.letsDealTarget; var tName=a.letsDealTargetName; return (tName ? "📍 Monitoring: "+tName+"." : "No target set — activate to choose.") + " Gain $"+(a.letsDealAmount||1)+"k when target activates a non-passive asset."; }},
  {name:"Extra Income",    val:9, sub:AS.DURING,   hook:"a_extra_income",tmax:0, extraIncomeAmount:1, extraIncomeTarget:null, count:1,
   desc:"Activate: Target an opponent. Gain $1k whenever they gain money.",
   descFn:function(a){ return "Activate: Target an opponent. Gain $"+a.extraIncomeAmount+"k whenever they gain money."; }},
  {name:"Risk vs Reward",  val:8, sub:AS.DURING,   hook:"a_risk_reward", tmax:0, riskRewardAmount:1, riskRewardTarget:null, count:1,
   desc:"Activate: Target an opponent. Gain $1k whenever they lose money.",
   descFn:function(a){ return "Activate: Target an opponent. Gain $"+a.riskRewardAmount+"k whenever they lose money."+(a.riskRewardTarget?" [Targeting: "+(a.riskRewardTargetName||a.riskRewardTarget)+"]":"")+""; }},
  {name:"Leave A Tip",     val:10,sub:AS.PASSIVE,  hook:"p_leave_tip",  tmax:0, tipAmount:1, count:1,
   desc:"Gain a bonus $1k on top of any money you gain.",
   descFn:function(a){ return "Gain an extra $"+a.tipAmount+"k every time you gain money."; }},
  {name:"High Valued Goods",val:9,sub:AS.PASSIVE,  hook:"p_high_valued", tmax:0, highValuedAmount:1, count:1,
   desc:"All your Action & Reaction cards gain +$1k value."},
  {name:"Price Adjustment", val:10,sub:AS.PASSIVE,  hook:"p_price_adj",  tmax:0, priceAdjAmount:2, count:1,
   desc:"Assets cost $2k more for other players.",
   descFn:function(a){ return "Assets cost $"+a.priceAdjAmount+"k more for other players (shop and blind buy)."; }},
  {name:"Tax",              val:8, sub:AS.PASSIVE,  hook:"p_tax",         tmax:0, taxValue:1, taxAmount:1, count:1,
   desc:"Gain $1k when an opponent plays a card worth $1k.",
   descFn:function(a){ return "Gain $"+a.taxAmount+"k when an opponent plays a card worth $"+a.taxValue+"k."; }},
  {name:"A Couple Bucks",  val:8, sub:AS.PASSIVE,  hook:"p_couple_bucks",tmax:0, coupleBucksValue:2, coupleBucksAmount:1, count:1,
   desc:"Gain $1k when an opponent plays a card worth $2k.",
   descFn:function(a){ return "Gain $"+a.coupleBucksAmount+"k when an opponent plays a card worth $"+a.coupleBucksValue+"k."; }},
  {name:"Accountant",      val:6, sub:AS.PASSIVE,  hook:"p_accountant",  tmax:0, accountantThreshold:1, accountantAmount:1, count:1,
   desc:"When an opponent causes you to lose $2k+, they also lose $1k.",
   descFn:function(a){ return "When an opponent causes you to lose more than $"+(a.accountantThreshold||1)+"k, they lose $"+(a.accountantAmount||1)+"k too."; }},
  {name:"Plus Interest",   val:8, sub:AS.PASSIVE,  hook:"p_plus_interest",tmax:0, plusInterestAmount:1, count:1,
   desc:"When you make someone else lose money, they lose an extra $1k.",
   descFn:function(a){ return "When you cause an opponent to lose money, they lose an extra $"+(a.plusInterestAmount||1)+"k."; }},
  {name:"Loss Prevention", val:7, sub:AS.PASSIVE,  hook:"p_loss_prevention",tmax:0, preventionAmount:1, count:1,
   desc:"Reduce all money lost by $1k (minimum loss: $0). Does not apply to asset purchases.",
   descFn:function(a){ var r=(a.preventionAmount||1)*(a._franchised?2:1); return "Any time you lose money, reduce that loss by $"+r+"k (minimum $0k). Does not apply when buying assets."+(a._franchised?" (Doubled by Franchise)":""); }},
  {name:"Repetitive Work", val:7, sub:AS.PASSIVE,  hook:"p_repetitive_work",tmax:0, repetitiveWorkAmount:1, count:1,
   desc:"Gain +1 action whenever you play a $1k card.",
   descFn:function(a){ return "Gain +1 action whenever you play a $"+(a.repetitiveWorkAmount||1)+"k card."; }},
  {name:"Toll",             val:8, sub:AS.PASSIVE,  hook:"p_toll",         tmax:0, tollAmount:1, count:1,
   desc:"When you play an action card, all other players lose $1k.",
   descFn:function(a){ return "When you play an action card, all other players lose $"+(a.tollAmount||1)+"k."; }},
  {name:"Startup Costs",   val:7, sub:AS.PASSIVE,  hook:"p_startup_costs",tmax:0, startupCostsAmount:1, count:1,
   desc:"All opponents lose $1k at the start of their turn.",
   descFn:function(a){ return "At the start of each opponent's turn, they lose $"+(a.startupCostsAmount||1)+"k."; }},
  {name:"Early Lead",      val:7, sub:AS.PASSIVE,  hook:"p_early_lead",   tmax:0, earlyLeadAmount:1, count:1,
   desc:"At the start of your turn, all opponents lose $1k.",
   descFn:function(a){ return "At the start of your turn, all opponents lose $"+(a.earlyLeadAmount||1)+"k."; }},
  {name:"Rule of Thirds",  val:9, sub:AS.PASSIVE,  hook:"p_rule_of_thirds",tmax:0, ruleOfThirdsAmount:1, count:1,
   ruleOfThirdsValue:3, ruleOfThirdsAmount:1,
   desc:"When an opponent plays a $3k card, they pay you $1k.",
   descFn:function(a){ return "When an opponent plays a $"+(a.ruleOfThirdsValue||3)+"k card, they pay you $"+(a.ruleOfThirdsAmount||1)+"k."; }},
  {name:"Resale Value",    val:7, sub:AS.PASSIVE,  hook:"p_sell_p1",     tmax:0, resaleAmount:1, count:1,
   desc:"Your sold cards are worth $1k more."},
  {name:"Free Replacement",val:7, sub:AS.PASSIVE,  hook:"p_free_replacement",tmax:0, freeReplacementAmount:1, count:1,
   desc:"Whenever you sell a card, draw a card from the deck.",
   descFn:function(a){ return "Whenever you sell a card, draw a card from the main deck."; }},
  {name:"Price Check",     val:6, sub:AS.PASSIVE,  hook:"p_price_check",  tmax:0, priceCheckValue:2, count:1,
   desc:"When you play a $2k card, draw a card. If it is an action card, you may play it for free."},
  {name:"Full Refund",     val:5, sub:AS.PASSIVE,  hook:"p_full_refund",  tmax:0, fullRefundThreshold:1, count:1,
   desc:"When you sell an action card worth $1k, you may choose to also activate its effect."},
  {name:"Finder's Fee",    val:6, sub:AS.PASSIVE,  hook:"p_finders_fee",  tmax:0, findersFeeAmount:1, count:1,
   desc:"Gain $1k whenever another player sells a card.",
   descFn:function(a){ return "Gain $"+(a.findersFeeAmount||1)+"k whenever another player sells a card."; }},
  {name:"Recycle",         val:6, sub:AS.PASSIVE,  hook:"p_recycle",      tmax:0, count:1, _franchiseExempt:true,
   desc:"At the start of your turn, you can choose to draw from the top of the discard pile instead of the deck."},
  {name:"Revenue Stream",  val:9, sub:AS.PASSIVE,  hook:"p_revenue_stream",tmax:3, revenueAmount:1, revenueMaxPayout:5, count:1,
   desc:"Tokens: 0/3. Place a token whenever you gain $6k or more at once. Each turn, gain $1k per token on this card. At 3 tokens gain $5k and reset.",
   descFn:function(a){ var n=(a.tokens||[]).length,tmax=a.tmax||3,amt=a.revenueAmount||1,mp=(a.revenueMaxPayout||5)*(a._franchised?2:1); return "Tokens: "+n+"/"+tmax+". Place a token whenever you gain $6k or more at once. Each turn, gain $"+amt+"k per token. At "+tmax+" tokens: gain $"+mp+"k and reset."; }},
  {name:"Action Plan",     val:7, sub:AS.DURING,   hook:"a_action_plan",  tmax:5, count:1,
   desc:"Activate: place an action card here or add a token. Spend tokens to fire that card's effect.",
   descFn:function(a){ var n=a.tokens?a.tokens.length:0; var lc=a.lockedCard; return lc?"Stored: \""+lc.name+"\" ($"+lc.value+"k). Tokens: "+n+"/5. Activate to add a token or fire that card\'s effect.":"Activate: place an action card here or add a token. Spend tokens to fire that card\'s effect."; }},
  {name:"Severance Pay",   val:8, sub:AS.DURING,   hook:"a_severance_pay",tmax:10, count:1,
   desc:"Tokens: 0/10. Place tokens equal to the value of each action card you play. Activate at max token count to take an extra turn.",
   descFn:function(a){ var n=a.tokens?a.tokens.length:0; return n>=(a.tmax||10)?"✅ READY — Activate for an extra turn!":"Tokens: "+n+"/"+(a.tmax||10)+". Place tokens equal to the value of each action card you play. Activate at max tokens for an extra turn."; }},
  {name:"Exchange",        val:6, sub:AS.ONCE,     hook:"a_exchange",     tmax:0, count:1,
   desc:"Activate to choose any asset from the shop and take it for free. This asset is discarded."},
  {name:"An Eye For An Eye",val:5,sub:AS.ONCE,     hook:"a_eye_for_eye",  tmax:0, count:1,
   desc:"Discard one of an opponent's assets (they need 2+). Draw a card. This asset is discarded."},
  {name:"BOGO",             val:10,sub:AS.ONCE,    hook:"a_bogo",         tmax:0, count:1,
   desc:"Activate: copy another asset you own. This card becomes that asset permanently."},
  {name:"Monopoly",        val:8, sub:AS.DURING,   hook:"a_monopoly",     tmax:0, count:1,
   desc:"Activate: place an action card on this asset. Others can't play cards of that value.",
   descFn:function(a){ var lc=a.lockedCard; return lc?"LOCKED: No one can play $"+lc.value+"k cards (\""+lc.name+"\"). Click to activate/swap.":"Activate: place a card here. Opponents cannot play matching-value cards."; }},
  {name:"Franchise",       val:10,sub:AS.ONCE,     hook:"a_franchise",    tmax:0, count:1,
   desc:"Activate: choose one of your passive assets to double its effect permanently."},
  {name:"Targeted Ad",    val:7, sub:AS.PASSIVE,  hook:"p_targeted_ad",  tmax:0, targetedAdAmount:2, count:1,
   desc:"Gain $2k whenever you cancel or negate an opponent\'s card or asset.",
   descFn:function(a){ return "Whenever you cancel/negate an opponent\'s card or asset, they pay you $"+(a.targetedAdAmount||2)+"k."; }},
  {name:"Paid Work",         val:8, sub:AS.ANYTIME,  hook:"a_paid_work",    tmax:0, paidWorkAmount:2, count:1,
   desc:"When a player gains or loses money (including you), activate to adjust the money gain/loss by $2k."},
  {name:"Checking Inventory",val:7,sub:AS.PASSIVE,  hook:"p_check_inv",    tmax:0, checkInvAmount:1, count:1,
   desc:"Gain $1k whenever an effect causes you to draw a card.",
   descFn:function(a){ return "Gain $"+(a.checkInvAmount||1)+"k per triggered card draw (not turn-start or Draw action)."; }},
  {name:"Discount",        val:8, sub:AS.PASSIVE,  hook:"p_discount",     tmax:5, discountAmount:1, count:1,
   desc:"Gain a token when an opponent buys an asset (max 5). Asset prices are reduced by $1k per token. Tokens reset when you buy an asset.",
   descFn:function(a){ var n=a.tokens?a.tokens.length:0, amt=a.discountAmount||1; return "Tokens: "+n+"/"+(a.tmax||5)+". Gain a token when an opponent buys an asset (max "+(a.tmax||5)+"). Asset prices are reduced by $"+amt+"k per token ($"+(n*amt)+"k discount). Tokens reset when you buy an asset."; }},
  {name:"Worth The Price", val:6, sub:AS.ANYTIME,  hook:"a_worth_the_price",tmax:0, count:1,
   desc:"Discard cards totaling more than a played card\'s value to cancel its effect."},
  {name:"Coming Soon",     val:5, sub:AS.DURING,   hook:"a_coming_soon",  tmax:0, count:1,
   desc:"Activate: View the top card of the Asset Deck."},
  {name:"Lights, Camera, Action!", val:8, sub:AS.PASSIVE, hook:"p_lca",   tmax:0, lcaAmount:1, count:1,
   desc:"Gain $1k every time you play an action card.",
   descFn:function(a){ return "Gain $"+(a.lcaAmount||1)+"k every time you successfully play an action card."; }},
  {name:"Credit Line",     val:8, sub:AS.DURING,   hook:"a_credit_line",  tmax:3, clValue:2, clPrice1:1, clPrice2:3, clPrice3:6, count:1,
   desc:"Tokens: 0/3. Tokens reset to max at the start of your turn. Remove tokens to gain $2k each. Refill costs: 1 token=$1k, 2=$3k, 3=$6k (auto at turn start).",
   descFn:function(a){ var n=a.tokens?a.tokens.length:0; return "Tokens: "+n+"/3. Tokens reset to max at the start of your turn. Remove tokens to gain $"+(a.clValue||2)+"k each. Refill costs: 1=$"+(a.clPrice1||1)+"k, 2=$"+(a.clPrice2||3)+"k, 3=$"+(a.clPrice3||6)+"k (auto at turn start)."; }},
  {name:"R&D Budget",      val:8, sub:AS.DURING,   hook:"a_rnd_budget",   tmax:3, rndDrawCount:2, count:1,
   desc:"Skip actions to gain tokens. Activate when full: draw 2 cards, free actions this turn.",
   descFn:function(a){ var n=a.tokens?a.tokens.length:0; return n>=(a.tmax||3)?"READY: Activate to draw "+(a.rndDrawCount||3)+" cards & play actions free this turn. (Hand discards at turn end.)":"Tokens: "+n+"/"+(a.tmax||3)+". Gain a token each turn you don't play an action card."; }},
  {name:"Pressure Campaign",val:7,sub:AS.PASSIVE,  hook:"p_pressure",    tmax:0, pressureThreshold:3, pressurePenalty:4, count:1,
   desc:"Each of your turns, all opponents gain a pressure token. If a player ends their turn with 3+ tokens, they pay you $4k and their tokens reset to 0."},
  {name:"Portfolio",       val:6, sub:AS.PASSIVE,  hook:"p_portfolio",    tmax:3, portfolioAmount:1, count:1,
   desc:"Gain a token each turn. At max tokens, gain $1k per passive asset you own.",
   descFn:function(a){ var n=a.tokens?a.tokens.length:0; return "Tokens: "+n+"/3. Gain a token each turn start. At 3, gain $"+(a.portfolioAmount||1)+"k x passive assets and reset."; }},
  {name:"Blueprint",       val:7, sub:AS.DURING,   hook:"a_blueprint",    tmax:0, count:1,
   desc:"Store an action card. Matching-value actions can be played for free.",
   descFn:function(a){ var lc=a.lockedCard; return lc?"Stored: \""+lc.name+"\" ($"+lc.value+"k). $"+lc.value+"k action cards play free. Works while USED.":"Activate to store an action card. Matching-value actions are then free."; }},
  {name:"Retainer",        val:10,sub:AS.DURING,   hook:"a_retainer",     tmax:0, retainerAmount:1, count:1,
   desc:"Activate to store a reaction card. Gain $1k each time that reaction card could fire. The amount of times it can fire per turn is equal to that card's value.",
   descFn:function(a){ var lc=a.lockedCard, n=a.tokens?a.tokens.length:0, amt=a.retainerAmount||1; return lc?"Stored: \""+lc.name+"\" ("+n+" use"+(n!==1?"s":"")+" left). Gain $"+amt+"k each time that reaction card could fire. Activate to replace.":"Activate to store a reaction card. Gain $"+amt+"k each time that reaction card could fire. Uses per turn = card value."; }},
  {name:"Prospects",       val:5, sub:AS.PASSIVE,     hook:"a_prospects",    tmax:0, count:1,
   desc:"Tokens: 3/3. Starts with 3 tokens. Remove 1 token at the start of your turn. At 0 tokens, transforms into a copy of the highest-valued shop asset.",
   descFn:function(a){ var n=(a.tokens||[]).length; return "Tokens: "+n+"/3. Remove 1 token at the start of your turn. At 0 tokens, transforms into a copy of the highest-valued shop asset."; }},
  {name:"Swear Jar",       val:8, sub:AS.PASSIVE,  hook:"p_swear_jar",    tmax:0, swearJarAmount:2, count:1,
   desc:"When an opponent plays a reaction card during your turn, they pay you $2k.",
   descFn:function(a){ return "When an opponent plays a reaction card during your turn, they must pay you $"+(a.swearJarAmount||2)+"k."; }},
  {name:"Mark-Up",          val:7, sub:AS.DURING,   hook:"a_mark_up",      tmax:0, markUpAmount:1, count:1,
   desc:"Activate: Increase or decrease the value of a card in your hand by $1k until end of turn."},
  {name:"Closed for Remodeling",val:7,sub:AS.DURING, hook:"a_cfr",         tmax:0, count:1,
   desc:"Activate: Disable an opponent\'s asset until your next turn. You cannot disable the same asset two turns in a row."},
  
  {name:"Product Update",   val:10,sub:AS.DURING,  hook:"a_product_update",tmax:0, puAmount:1, count:1,
   desc:"Upgrade an owned asset: adjust one of its values permanently.",
   descFn:function(a){ return "One-time: select an owned asset and adjust one of its values by ±"+(a.puAmount||1)+". Reverts if this card leaves your possession."; }},
  {name:"Rebranding",      val:5, sub:AS.DURING,   hook:"a_rebranding",   tmax:0, rebrandBonus:1, count:1,
   desc:"Discard this asset and choose any number of assets you own to shuffle back into the asset deck. Then draw the same number of assets from the top of the asset deck + 1.",
   descFn:function(a){ var b=a.rebrandBonus||1; return "Discard this asset and choose any number of assets you own to shuffle back into the asset deck. Then draw the same number of assets from the top of the asset deck + "+b+"."; }},
  {name:"Multi-Tool",      val:9, sub:AS.DURING,   hook:"a_multi_tool",   tmax:0, count:1,
   desc:"Activate: reset one of your used assets to READY.",
   descFn:function(a){ return "Activate to reset one of your USED assets to READY. Cannot target another Multi-Tool."; }},
  {name:"Workman's Comp",  val:8, sub:AS.PASSIVE,  hook:"p_workmans_comp",tmax:0, workmanAmount:1, count:1,
   desc:"Whenever you sell a card, all opponents lose $1k.",
   descFn:function(a){ return "When you sell a card, all opponents lose $"+(a.workmanAmount||1)+"k."; }},
  {name:"Bribery",           val:10,sub:AS.DURING,   hook:"a_bribery",      tmax:0, bribeTokenCost:1, count:1,
   _noTokenCircles:true,
   desc:"Discard cards to steal an opponent\'s asset.",
   descFn:function(a){
     var t=(a.tokens||[]).length, c=a.bribeTokenCost||1;
     return "Choose an opponent's asset. Discard cards with a total value greater than the value of that asset + $"+c+"k per token on this card in order to steal that asset. Tokens currently on this card: "+t+". A player must own at least 2 assets to be targeted. You must have a valid target to activate.";
   }},
  {name:"Consolation Prize",val:6, sub:AS.PASSIVE,  hook:"p_consolation", tmax:0, consolationGain:1, count:1,
   desc:"On bankruptcy, gain a free asset instead of losing your best one.",
   descFn:function(a){ return "Bankruptcy shield: gain "+(a.consolationGain||1)+" asset(s) + this is discarded instead of your best. OR game-end bonus: gain "+(a.consolationGain||1)+" asset(s) if never bankrupt."; }},
  {name:"Restock",         val:8, sub:AS.PASSIVE,   hook:"p_restock",     tmax:0, restockValue:3, restockAmount:1, count:1,
   desc:"When you play a $3k card, gain $1k and draw a card.",
   descFn:function(a){ var v=a.restockValue||3,m=a.restockAmount||1,f=a._franchised; return "When you play a $"+v+"k card, gain $"+(f?m*2:m)+"k and draw "+(f?2:1)+" card"+(f?"s":"")+" ("+v+"k trigger, "+m+"k gain"+( f?" — doubled by Franchise":"")+")."; }},
{name:"Empty Handed",    val:8, sub:AS.PASSIVE,   hook:"p_empty_handed", tmax:0, emptyHandedAmount:2, count:1,
   desc:"When you end your turn with no cards in hand, draw 2 cards.",
   descFn:function(a){ var n=(a.emptyHandedAmount||2)*(a._franchised?2:1); return "When you end your turn with an empty hand, draw "+n+" card"+(n!==1?"s":"")+". "+(a._franchised?"(Doubled by Franchise)":""); }},
{name:"Three of a Kind",val:10,sub:AS.DURING,   hook:"a_three_of_kind",tmax:0, tripleCount:3, tripleValue:2, count:1,
   desc:"Activate: Discard 3 cards worth $2k to gain the top asset deck card for free.",
   descFn:function(a){ var n=a.tripleCount||3; var v=a.tripleValue||2; return "Activate: Discard "+n+" cards each worth $"+v+"k to gain the top asset deck card for free."+(a._franchised?" (Doubled by Franchise)":""); }},
  {name:"Sneak Peak",     val:5, sub:AS.DURING,   hook:"a_sneak_peak",  tmax:0, count:1,
   desc:"Activate: View the top 2 cards of the main deck and optionally reorder them."},
  {name:"Extra Shift",      val:9, sub:AS.DURING,   hook:"a_extra_shift",  tmax:0, count:1,
   desc:"Activate: gain 1 extra action this turn."},
  {name:"Side Hustle",     val:6, sub:AS.PASSIVE,  hook:"p_side_hustle",  tmax:0, sideHustleAmount:1, count:1,
   desc:"Earn $1k extra whenever an opponent pays you.",
   descFn:function(a){ return "When another player pays you money, they pay an additional $"+(a.sideHustleAmount||1)+"k on top."; }},
  {name:"Trade-In Value",  val:9, sub:AS.DURING,   hook:"a_trade_in",     tmax:0, tradeInAmount:6, count:1,
   desc:"Discard 1 action and 1 reaction card to gain $6k.",
   descFn:function(a){ return "Activate: discard 1 action card and 1 reaction card to gain $"+(a.tradeInAmount||6)+"k."; }},
];

function mkCards(tpls, type, def=3, disabledNames, countOverrides) {
  disabledNames = disabledNames || new Set();
  countOverrides = countOverrides || {};
  return shuf(tpls.filter(function(t) { return !disabledNames.has(t.name); }).flatMap(function(t) {
    var count = (countOverrides[t.name] !== undefined) ? countOverrides[t.name] : (t.count || def);
    return Array.from({length:count}, function() {
      var mk_tok = (t.hook==="a_prospects") ? [{id:uid()},{id:uid()},{id:uid()}] : [];
      return { ...t, id:uid(), type, value:t.val, origVal:t.val, tokens:mk_tok, status:ST.READY, disabled:false };
    });
  }));
}


/* - Dynamic description resolver - */
// Cards with modifiable values store a descFn(card) instead of a static desc.
// All rendering should call getDesc(card) rather than card.desc directly.
function getDesc(card) {
  if (!card) return "";
  if (typeof card.descFn === "function") return card.descFn(card);
  return card.desc || "";
}

/* - Reaction eligibility - */
// Determines whether a card/asset is eligible to react to a specific trigger.
// trigger.tgtPlayer = who is being targeted; owner = player holding the card.
const addLog = (st, msg, type="info", cards=[], toast=false) => {
  var newLog = [...st.log, { id:uid(), msg, type, turn:st.turnNum, cards }];
  if (newLog.length > 30) newLog = newLog.slice(newLog.length - 30);
  var next = { ...st, log: newLog };
  if (toast) {
    var toastObj = { id:uid(), msg, type };
    next = { ...next, toasts:[...(next.toasts||[]), toastObj] };
  }
  return next;
};

function isEligibleReaction(card, trigger, owner, players) {
  const iAmTarget = trigger.tgtPlayer === owner.id;
  const notMine   = trigger.srcPlayer !== owner.id;
  switch (card.hook) {
    case "cancel_whole_action": // Ignored - ACTION_PLAYED by opponent
      return trigger.type === T.ACTION_PLAYED && notMine;
    case "cancel_reaction":     // Fired - REACTION_PLAYED by opponent
      return trigger.type === T.REACTION_PLAYED && notMine;
    case "prevent_loss":     // Insurance - LOSE_MONEY, PAY_FEES, or PAY_MONEY aimed at me
      return (trigger.type === T.LOSE_MONEY || trigger.type === T.PAY_FEES || trigger.type === T.PAY_MONEY) && iAmTarget;


    case "shrinkage":           // Shrinkage - opponent plays OR sells a card
      return (trigger.type === T.ACTION_PLAYED || trigger.type === T.REACTION_PLAYED || trigger.type === T.SELL_CARD) && notMine;
    case "defective_unit":      // Defective Unit - opponent activates an asset
      return trigger.type === T.ACTIVATE_ASSET && notMine;
    case "last_minute_bid":     // Last Minute Bid - opponent sells a card
      return trigger.type === T.SELL_CARD && notMine;
    case "not_a_chance":        return trigger.type === T.TARGET_PLAYER && iAmTarget && !trigger._automatic;
    case "property_theft":      return trigger.type === T.ACTION_PLAYED && notMine;
    case "worth_the_price": {
      if (trigger.type !== T.ACTION_PLAYED) return false;
      if (!notMine) return false;
      var wtp_ht = owner.hand.filter(function(c){ return c.type !== "ASSET"; })
                             .reduce(function(s,c){ return s+(c.value||0); }, 0);
      return wtp_ht > (trigger.value || 0);
    }
    case "minor_loss": { if (!iAmTarget) return false; if (trigger.type !== T.LOSE_MONEY && trigger.type !== T.PAY_MONEY) return false; return owner.money < 0 && owner.hand.length >= 2; }
    case "downsizing":          return trigger.type === T.ACTIVATE_ASSET && notMine;
    case "close_of_business": {
      if (trigger.curPlayerId && trigger.curPlayerId === owner.id) return false;
      return notMine && (trigger.type === T.START_TURN || trigger.type === T.ACTION_PLAYED ||
        trigger.type === T.REACTION_PLAYED || trigger.type === T.ACTIVATE_ASSET);
    }
    case "back_to_basics": {
      if (!notMine) return false;
      if (trigger.type !== T.ACTION_PLAYED && trigger.type !== T.BUY_ASSET && trigger.type !== T.ACTIVATE_ASSET) return false;
      // Only eligible if the specific player you're reacting to (the trigger source) has 3+ assets
      var btb_trigSrc = players.find(function(p){ return p.id === trigger.srcPlayer; });
      return !!(btb_trigSrc && (btb_trigSrc.assets||[]).length >= 3);
    }
    case "equity":              return trigger.type === T.BUY_ASSET && notMine;
    case "inflation":           return trigger.type === T.BUY_ASSET && notMine;
    case "competitor":          return trigger.type === T.BUY_ASSET && notMine && (trigger.value || 0) > 0;
    case "scammed":             return trigger.type === T.GAIN_MONEY && notMine;
    case "recession":           return (trigger.type === T.LOSE_MONEY || trigger.type === T.PAY_MONEY) && notMine && trigger.tgtPlayer !== owner.id && trigger.srcPlayer !== owner.id;
    case "risky_investment":    return (trigger.type === T.LOSE_MONEY || trigger.type === T.PAY_MONEY) && trigger.srcPlayer === owner.id;
    case "refusal_to_work":     return trigger.type === T.TARGET_PLAYER && iAmTarget && !trigger._automatic && !trigger._fromAsset;
    case "repeat_customers":    return trigger.type === T.ACTIVATE_ASSET && !notMine && owner.assets.some(function(a){ return a.status === ST.USED && a.sub !== AS.ONCE && a.sub !== AS.PASSIVE; });
    case "cover_the_costs":     return (trigger.type === T.LOSE_MONEY || trigger.type === T.PAY_MONEY || trigger.type === T.PAY_FEES) && trigger.srcPlayer === owner.id;
    case "equal_pay":           return trigger.type === T.SELL_CARD && notMine;
    case "business_partners":   return notMine && (trigger.type === T.ACTION_PLAYED || trigger.type === T.ACTIVATE_ASSET);
    default: return false;
  }
}
function isEligibleAsset(asset, trigger, owner) {
  if (asset.hook === "a_worth_the_price") {
    if (trigger.type !== T.ACTION_PLAYED && trigger.type !== T.REACTION_PLAYED) return false;
    if (trigger.srcPlayer === owner.id) return false;
    var trigVal = trigger.value || 0;
    var handTotal = owner.hand.reduce(function(s,c){ return s + (c.value||0); }, 0);
    return handTotal > trigVal;
  }
  return false;
}

function reactorsFor(players, trigger) {
  const si = players.findIndex(p => p.id === trigger.srcPlayer);
  const order = players.map((_,i) => players[(si+1+i) % players.length]);
  return order.map(pl => {
    const cards  = pl.hand.filter(c  => c.type === CT.REACTION && isEligibleReaction(c, trigger, pl, players));
    const assets = pl.assets.filter(a => a.sub === AS.ANYTIME && !a.disabled && a.status === ST.READY && isEligibleAsset(a, trigger, pl));
    return { pid:pl.id, cards, assets, canReact: cards.length>0||assets.length>0, decision:"PENDING" };
  });
}

/* - Reaction-window resume helpers - */

// Resume an action card effect after the ACTION_PLAYED window resolves.
// pe: { srcPlayer, tgtPlayer (=targetId), value, srcCard }
function continueAfterActionPlayed(st, pe) {
  var card = pe.srcCard;
  var targetId = pe.tgtPlayer;
  if (targetId) {
    var ns = pushTrigger(st, { type:T.TARGET_PLAYER, srcPlayer:pe.srcPlayer, tgtPlayer:targetId, label:card.name });
    var trig = ns.triggerStack[ns.triggerStack.length-1];
    var reactors = reactorsFor(ns.players, trig);
    // Fire action-play passives for targeted cards (Toll, LCA, Restock, RW, SP) — mirrors non-targeted path
    var caap_pl_tgt = ns.players.find(function(p){ return p.id===pe.srcPlayer; });
    if (caap_pl_tgt) {
      caap_pl_tgt.assets.forEach(function(a) {
        if (a.hook!=="p_toll"||a.disabled) return;
        var toll_amt=(a.tollAmount||1)*(a._franchised?2:1)*(caap_pl_tgt._doublePassive?2:1);
        ns.players.forEach(function(opp){ if (opp.id===pe.srcPlayer) return; ns=addMoney(ns,opp.id,-toll_amt,"Toll ("+pname(ns,pe.srcPlayer)+")"); ns=applyPlusInterest(ns,pe.srcPlayer,opp.id); });
        ns=addLog(ns,"🛤 "+pname(ns,pe.srcPlayer)+"'s Toll fires - all opponents lose $"+toll_amt+"k!","asset",[],true);
      });
      caap_pl_tgt.assets.forEach(function(a) {
        if (a.hook!=="p_lca"||a.disabled) return;
        var lca_amt=(a.lcaAmount||1)*(a._franchised?2:1)*(caap_pl_tgt._doublePassive?2:1);
        ns=addLog(ns,"🎬 "+pname(ns,pe.srcPlayer)+"'s Lights, Camera, Action! fires - +$"+lca_amt+"k!","asset",[],true);
        ns=addMoney(ns,pe.srcPlayer,lca_amt,"Lights, Camera, Action!");
      });
      caap_pl_tgt.assets.forEach(function(a) {
        if (a.hook!=="p_restock"||a.disabled||card.origVal!==(a.restockValue||3)) return;
        var rs_amt=(a.restockAmount||1)*(a._franchised?2:1)*(caap_pl_tgt._doublePassive?2:1);
        var rs_draws=a._franchised?2:1;
        ns=addLog(ns,"🔄 "+pname(ns,pe.srcPlayer)+"'s Restock fires - +$"+rs_amt+"k and draw "+rs_draws+" card"+(rs_draws>1?"s":"")+"!","asset",[],true);
        ns=addMoney(ns,pe.srcPlayer,rs_amt,"Restock"); ns=drawN(ns,pe.srcPlayer,rs_draws,0,true);
      });
      caap_pl_tgt.assets.forEach(function(a) {
        if (a.hook!=="p_repetitive_work"||a.disabled||card.value!==(a.repetitiveWorkAmount||1)) return;
        var rw_gain=(a.repetitiveWorkAmount||1)*(a._franchised?2:1);
        ns={...ns,actionsLeft:(ns.actionsLeft||0)+rw_gain,rwActionsBonus:(ns.rwActionsBonus||0)+rw_gain};
        ns=addLog(ns,"⏩ "+pname(ns,pe.srcPlayer)+"'s Repetitive Work fires - +"+rw_gain+" action"+(rw_gain!==1?"s":"")+"!","asset",[],true);
      });
      caap_pl_tgt.assets.forEach(function(a) {
        if (a.hook!=="a_severance_pay"||a.disabled) return;
        var sp_cur=(a.tokens||[]).length,sp_max=a.tmax||10,sp_add=Math.min(card.origVal||card.value||0,sp_max-sp_cur);
        if (sp_add>0) {
          ns=setPl(ns,pe.srcPlayer,function(p){return {...p,assets:p.assets.map(function(b){return b.id===a.id?{...b,tokens:[...(b.tokens||[]),...Array.from({length:sp_add},function(){return {id:uid()};})]}:b;})};});
          ns=addLog(ns,"📋 "+pname(ns,pe.srcPlayer)+"'s Severance Pay: +"+sp_add+" token"+(sp_add!==1?"s":"")+" ("+(sp_cur+sp_add)+"/"+sp_max+").","asset",[],true);
        }
      });
    }
    ns = { ...ns, pendingEffect:{ hook:card.hook, srcPlayer:pe.srcPlayer, tgtPlayer:targetId, value:pe.value, srcCard:card } };
    if (reactors.some(function(r){ return r.canReact; })) {
      return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, pe.srcPlayer, targetId, reactors) };
    }
    ns = { ...ns, pendingEffect:null };
    ns = applyHook(ns, card.hook, { srcPlayer:pe.srcPlayer, tgtPlayer:targetId, value:pe.value, srcCard:card });
    // Price Check: also fires for targeted cards
    var caap_pl_t = ns.players.find(function(p){ return p.id===pe.srcPlayer; });
    if (caap_pl_t) caap_pl_t.assets.forEach(function(a) {
      if (a.hook!=="p_price_check"||a.disabled) return;
      if (card.origVal===(a.priceCheckValue||2)) {
        var pc_draws_t = a._franchised ? 2 : 1;
        var pc_actionDrawn_t = null;
        for (var pcti=0; pcti<pc_draws_t; pcti++) {
          var pc_res_t=tryReshuffle(ns.mainDeck,ns.mainDiscard);
          if (pc_res_t.empty) break;
          var pc_drawn_t=pc_res_t.deck[0];
          if (pc_res_t.reshuffled) ns={...ns,mainDeck:pc_res_t.deck,mainDiscard:[]};
          else ns={...ns,mainDeck:pc_res_t.deck.slice(1)};
          ns=setPl(ns,pe.srcPlayer,function(p){return {...p,hand:[...p.hand,pc_drawn_t]};});
          ns=addLog(ns,"🎯 "+pname(ns,pe.srcPlayer)+"'s Price Check fires - draws \""+pc_drawn_t.name+"\"!","asset",[pc_drawn_t],true);
          ns=checkInventoryDraw(ns,pe.srcPlayer);
          if (pc_drawn_t.type===CT.ACTION && !pc_actionDrawn_t) pc_actionDrawn_t=pc_drawn_t;
        }
        if (pc_actionDrawn_t) {
          ns={...ns,pendingChoice:{type:"PRICE_CHECK_PROMPT",srcPlayer:pe.srcPlayer,drawnCard:pc_actionDrawn_t,timerEnd:Date.now()+10000}};
          return;
        }
      }
    });
    return maybeFlush(ns);
  }
  var caap_ns = applyHook(st, card.hook, { srcPlayer:pe.srcPlayer, tgtPlayer:null, value:pe.value, srcCard:card });
  // Toll: all opponents lose tollAmount after action played
  var caap_pl = caap_ns.players.find(function(p){ return p.id===pe.srcPlayer; });
  if (caap_pl) caap_pl.assets.forEach(function(a) {
    if (a.hook!=="p_toll"||a.disabled) return;
    var toll_amt=(a.tollAmount||1)*(a._franchised?2:1)*(caap_pl._doublePassive?2:1);
    caap_ns.players.forEach(function(opp){
      if (opp.id===pe.srcPlayer) return;
      caap_ns=addMoney(caap_ns,opp.id,-toll_amt,"Toll ("+pname(caap_ns,pe.srcPlayer)+")");
      caap_ns=applyPlusInterest(caap_ns,pe.srcPlayer,opp.id);
    });
    caap_ns=addLog(caap_ns,"🛤 "+pname(caap_ns,pe.srcPlayer)+"'s Toll fires - all opponents lose $"+toll_amt+"k!","asset",[],true);
  });
  // Lights Camera Action: gain lcaAmount per action played
  if (caap_pl) caap_pl.assets.forEach(function(a) {
    if (a.hook!=="p_lca"||a.disabled) return;
    var lca_amt=(a.lcaAmount||1)*(a._franchised?2:1)*(caap_pl._doublePassive?2:1);
    caap_ns=addLog(caap_ns,"🎬 "+pname(caap_ns,pe.srcPlayer)+"'s Lights, Camera, Action! fires - +$"+lca_amt+"k!","asset",[],true);
    caap_ns=addMoney(caap_ns,pe.srcPlayer,lca_amt,"Lights, Camera, Action!");
  });
  // Restock: gain money + draw a card when owner plays matching value card
  if (caap_pl) caap_pl.assets.forEach(function(a) {
    if (a.hook!=="p_restock"||a.disabled) return;
    if (card.origVal !== (a.restockValue||3)) return;
    var rs_amt = (a.restockAmount||1) * (a._franchised ? 2 : 1) * (caap_pl._doublePassive ? 2 : 1);
    var rs_draws = a._franchised ? 2 : 1;
    caap_ns = addLog(caap_ns, "🔄 "+pname(caap_ns,pe.srcPlayer)+"'s Restock fires - +$"+rs_amt+"k and draw "+rs_draws+" card"+(rs_draws>1?"s":"")+"!", "asset", [], true);
    caap_ns = addMoney(caap_ns, pe.srcPlayer, rs_amt, "Restock");
    caap_ns = drawN(caap_ns, pe.srcPlayer, rs_draws, 0, true);
  });
  // Repetitive Work: gain extra action if card value matches
  if (caap_pl) caap_pl.assets.forEach(function(a) {
    if (a.hook!=="p_repetitive_work"||a.disabled) return;
    if (card.value===(a.repetitiveWorkAmount||1)) {
      var rw_gain = (a.repetitiveWorkAmount || 1) * (a._franchised ? 2 : 1);
      caap_ns={...caap_ns,actionsLeft:(caap_ns.actionsLeft||0)+rw_gain,rwActionsBonus:(caap_ns.rwActionsBonus||0)+rw_gain};
      caap_ns=addLog(caap_ns,"⏩ "+pname(caap_ns,pe.srcPlayer)+"'s Repetitive Work fires - +"+rw_gain+" action"+(rw_gain!==1?"s":"")+"!","asset",[],true);
    }
  });
  // Severance Pay: accumulate tokens equal to card value
  if (caap_pl) caap_pl.assets.forEach(function(a) {
    if (a.hook!=="a_severance_pay"||a.disabled) return;
    var sp_cur=(a.tokens||[]).length, sp_max=a.tmax||10, sp_add=Math.min(card.origVal||card.value||0,sp_max-sp_cur);
    if (sp_add>0) {
      caap_ns=setPl(caap_ns,pe.srcPlayer,function(p){return {...p,assets:p.assets.map(function(b){
        return b.id===a.id?{...b,tokens:[...(b.tokens||[]),...Array.from({length:sp_add},function(){return {id:uid()};})]}:b;
      })};});
      caap_ns=addLog(caap_ns,"📋 "+pname(caap_ns,pe.srcPlayer)+"'s Severance Pay: +"+(sp_add)+" token"+(sp_add!==1?"s":"")+" ("+(sp_cur+sp_add)+"/"+sp_max+").","asset",[],true);
    }
  });
  // Price Check: draw on matching value play
  if (caap_pl) caap_pl.assets.forEach(function(a) {
        if (a.hook!=="p_price_check"||a.disabled) return;
    if (card.origVal===(a.priceCheckValue||2)) {
      var pc_draws = a._franchised ? 2 : 1;
      var pc_actionDrawn = null;
      for (var pci=0; pci<pc_draws; pci++) {
        var pc_res=tryReshuffle(caap_ns.mainDeck,caap_ns.mainDiscard);
        if (pc_res.empty) break;
        var pc_drawn=pc_res.deck[0];
        if (pc_res.reshuffled) caap_ns={...caap_ns,mainDeck:pc_res.deck,mainDiscard:[]};
        else caap_ns={...caap_ns,mainDeck:pc_res.deck.slice(1)};
        caap_ns=setPl(caap_ns,pe.srcPlayer,function(p){return {...p,hand:[...p.hand,pc_drawn]};});
        caap_ns=addLog(caap_ns,"🎯 "+pname(caap_ns,pe.srcPlayer)+"'s Price Check fires - draws \""+pc_drawn.name+"\"!","asset",[pc_drawn],true);
        if (pc_drawn.type===CT.ACTION && !pc_actionDrawn) pc_actionDrawn=pc_drawn;
      }
      // Checking Inventory: fires once per Price Check event
      caap_ns = checkInventoryDraw(caap_ns, pe.srcPlayer);
      if (pc_actionDrawn) {
        // If the card also opened a pending state (let_go, pocket_change, etc.),
        // defer PRICE_CHECK_PROMPT until after that state resolves via maybeFlush
        var pc_hasPending = caap_ns.letGoState || caap_ns.pocketChangeState ||
          caap_ns.negReactionState || caap_ns.kickstarterState || caap_ns.shutDownState;
        if (pc_hasPending) {
          caap_ns = { ...caap_ns, _pendingPriceCheck:{ srcPlayer:pe.srcPlayer, drawnCard:pc_actionDrawn } };
        } else {
          caap_ns={...caap_ns,pendingChoice:{type:"PRICE_CHECK_PROMPT",srcPlayer:pe.srcPlayer,drawnCard:pc_actionDrawn,timerEnd:Date.now()+10000}};
          return;
        }
      }
    }
  });
  return maybeFlush(caap_ns);
}

// Apply the pending reaction card effect then restore any saved outer reaction window.
function checkSwearJar(st, reactorId) {
  var cp = curPl(st);
  if (reactorId === cp.id) return st;
  var ns = st;
  cp.assets.forEach(function(a) {
    if (a.hook !== "p_swear_jar" || a.disabled) return;
    var amt = (a.swearJarAmount || 2) * (a._franchised ? 2 : 1) * (cp._doublePassive ? 2 : 1);
    var cur = ns.swearJarQueue || [];
    ns = { ...ns, swearJarQueue:[...cur, { ownerId:cp.id, reactorId:reactorId, amount:amt }] };
  });
  return ns;
}

function applyAndClearPendingReaction(st) {
  var prc = st.pendingReactionCard;
  if (!prc) return maybeFlush(st);
  // Swear Jar is charged at play time (resumeReactionPlayed / ENG_REACT),
  // NOT here, to avoid double-charging.
  var savedWin = st.savedReactionWindow;
  var ns = { ...st, pendingReactionCard:null, savedReactionWindow:null };
  // Cancel hooks need special treatment - applyHook is a no-op for them
  if (prc.hook === "cancel_whole_action") {
    // Ignored resolved without being Fired - cancel the pending action entirely
    ns = addLog(ns, "🚫 " + pname(ns, prc.srcPlayer) + "'s Ignored cancels the action!", "reaction");
    ns = { ...ns, pendingEffect:null };
    ns = checkTargetedAd(ns, prc.srcPlayer, prc.tgtPlayer);
    return maybeFlush(ns);
  }
 
  if (prc.hook === "cancel_reaction") {
    // Fired resolved at the sub-window level - cancels the outer pending reaction
    ns = addLog(ns, "🚫 Fired! Reaction cancelled.", "reaction");
    // Only clear pendingEffect if it's NOT the underlying action's __resume_action__
    // (we don't want to cancel the original card that was being reacted to)
    if (ns.pendingEffect && ns.pendingEffect.hook !== "__resume_action__") {
      ns = { ...ns, pendingEffect:null };
    }
    // pendingReactionCard was already cleared; restore outer window if pending decisions remain
    ns = checkTargetedAd(ns, prc.srcPlayer, prc.tgtPlayer);
    if (savedWin && savedWin.reactors && savedWin.reactors.some(function(r){ return r.decision === "PENDING"; })) {
      return { ...ns, reactionWindow:savedWin };
    }
    return maybeFlush(ns);
  }
    if (prc.hook === "property_theft") {
    ns = applyHook(ns, "property_theft", { srcPlayer:prc.srcPlayer, tgtPlayer:prc.tgtPlayer, srcCard:prc.srcCard });
    return maybeFlush(ns);
  }

  if (prc.hook === "not_a_chance") {
    var nac_name = pname(ns, prc.srcPlayer);
    ns = addLog(ns, "🛑 Not A Chance! " + nac_name + " ignores the targeting.", "reaction");
    var nac_victim = ns.pendingEffect ? ns.pendingEffect.srcPlayer : null;
    ns = { ...ns, pendingEffect:null };
    if (nac_victim) ns = checkTargetedAd(ns, prc.srcPlayer, nac_victim);
    // For multi-target states: advance WITHOUT confirming this target
    if (ns.letGoState && ns.letGoState.phase === "targeting") {
      return advanceLetGoTargeting({ ...ns, letGoState:{ ...ns.letGoState, currentTarget:null } });
    }
    if (ns.pocketChangeState && ns.pocketChangeState.phase === "targeting") {
      return advancePocketChangeTargeting({ ...ns, pocketChangeState:{ ...ns.pocketChangeState, currentTarget:null } });
    }
    if (ns.negReactionState && ns.negReactionState.phase === "targeting") {
      return advanceNegReactionTargeting({ ...ns, negReactionState:{ ...ns.negReactionState, currentTarget:null } });
    }
    if (ns.kickstarterState && ns.kickstarterState.phase === "targeting") {
      return advanceKickstarterTargeting({ ...ns, kickstarterState:{ ...ns.kickstarterState, currentTarget:null } });
    }
    // Single target - pendingEffect already cleared, action fully cancelled for this target
    return maybeFlush(ns);
  }
  ns = applyHook(ns, prc.hook, { srcPlayer:prc.srcPlayer, tgtPlayer:prc.tgtPlayer, value:prc.value, srcCard:prc.srcCard });
  // If hook opened its own reaction window (additive hooks like Shrinkage):
  if (ns.reactionWindow && savedWin) {
    // For ACTION_PLAYED outer windows: defer so action still runs after hook chain
    if (savedWin.ttype === T.ACTION_PLAYED) {
      return { ...ns, deferredWindow: savedWin };
    }
    // For other outer windows (SELL_CARD, etc.): event already resolved, just continue
    return ns;
  }
  // Business Partners (and similar) store the original action in pendingEffect as
  // __resume_action__. Run it now once all reactions on the BP copy have settled.
  if (ns.pendingEffect && ns.pendingEffect.hook === "__resume_action__") {
    var bpRpe = ns.pendingEffect;
    // If the outer ACTION_PLAYED window still has pending reactors, restore it first
    if (savedWin && savedWin.ttype === T.ACTION_PLAYED &&
        savedWin.reactors && savedWin.reactors.some(function(r){ return r.decision === "PENDING"; })) {
      return { ...ns, reactionWindow:savedWin };
    }
    ns = { ...ns, pendingEffect:null, deferredWindow:null };
    return continueAfterActionPlayed(ns, bpRpe);
  }
  // If an outer window was saved AND it still has pending decisions AND
  // it's not a resolved buy/action/sell event, restore the window
  if (savedWin && savedWin.reactors &&
      savedWin.reactors.some(function(r){ return r.decision === "PENDING"; }) &&
      ns.pendingEffect &&
      ns.pendingEffect.hook !== "__sold_card__" && ns.pendingEffect.hook !== "__buy_asset__") {
    return { ...ns, reactionWindow:savedWin };
  }
  if (ns.pendingEffect && ns.pendingEffect.hook === "__sold_card__") {
    return resolveSell(ns);
  }
  return maybeFlush(ns);
}

/* - Pending-discard helpers - */

/* - High Valued Goods helpers - */
function computeHandBoost(st, pid) {
  var pl = st.players.find(function(p){ return p.id === pid; });
  if (!pl) return 0;
  return pl.assets.filter(function(a){ return a.hook === "p_high_valued" && !a.disabled; })
    .reduce(function(s, a){ return s + (a.highValuedAmount || 1) * (a._franchised ? 2 : 1) * (pl._doublePassive ? 2 : 1); }, 0);
}

function applyHandBoosts(st, pid) {
  var boost = computeHandBoost(st, pid);
  return setPl(st, pid, function(p) {
    return { ...p, hand: p.hand.map(function(c) {
      if (c.type === CT.ASSET) return c;
      var delta = c._markupDelta || 0;
      var newVal = c.origVal + boost + delta;
      var modBy = [];
      if (boost > 0) modBy.push("High Valued Goods (+" + boost + "k)");
      if (delta > 0) modBy.push("Mark-Up (+" + delta + "k)");
      if (delta < 0) modBy.push("Mark-Up (" + delta + "k)");
      return { ...c, value: newVal, modifiedBy: modBy };
    })};
  });
}

function applyAllHandBoosts(st) {
  var ns = st;
  st.players.forEach(function(p) { ns = applyHandBoosts(ns, p.id); });
  return ns;
}

function flushPendingDiscard(st) {
  var pd = st.pendingDiscard;
  if (!pd || !pd.length) return st;
  // Reset card values to origVal before they land in the discard pile
  var resetPd = pd.map(function(c){ return { ...c, value:c.origVal, modifiedBy:[] }; });
  return { ...st, mainDiscard:[...st.mainDiscard, ...resetPd], pendingDiscard:[] };
}
// Flush only when the action is fully settled (no open windows or pending choices)
function maybeFlush(st) {
  // Fire deferred Price Check prompt (set when card resolved alongside a targeting state)
  if (st._pendingPriceCheck && !st.letGoState && !st.pocketChangeState &&
      !st.negReactionState && !st.kickstarterState && !st.shutDownState &&
      !st.pendingChoice && !st.reactionWindow) {
    var ppc = st._pendingPriceCheck;
    return { ...st, _pendingPriceCheck:null,
      pendingChoice:{ type:"PRICE_CHECK_PROMPT", srcPlayer:ppc.srcPlayer, drawnCard:ppc.drawnCard, timerEnd:Date.now()+10000 } };
  }
  // Don't flush while any reaction window, choice, pending effect, or PW prompt is open
  if (st.pendingPWChoice) return st;
  if (!st.pendingChoice && !st.reactionWindow && !st.pendingEffect && !st.pendingReactionCard) {
    // Fire Business Partners copies AFTER the original action/asset has fully resolved
    if (st._bpPendingCopies && st._bpPendingCopies.length) {
      var bpc = st._bpPendingCopies[0];
      var bpc_rest = st._bpPendingCopies.slice(1);
      var bpc_ns = { ...st, _bpPendingCopies: bpc_rest.length ? bpc_rest : null };
      if (bpc.card.target === "opponent") {
        var bpc_opps = bpc_ns.players.filter(function(p){ return p.id !== bpc.srcPlayer; })
                                     .map(function(p){ return { id:p.id, name:p.name }; });
        return { ...bpc_ns, pendingChoice:{ type:"BP_TARGET_SELECT", srcPlayer:bpc.srcPlayer, card:bpc.card, timerEnd:Date.now()+15000, options:bpc_opps } };
      }
      return maybeFlush(applyHook(bpc_ns, bpc.card.hook, { srcPlayer:bpc.srcPlayer, tgtPlayer:null, value:bpc.card.value, srcCard:bpc.card }));
    }
    if (st.retainerQueue && st.retainerQueue.length) {
      var rq_entry = st.retainerQueue[0];
      var rq_ns = { ...st, retainerQueue:st.retainerQueue.slice(1) };
      rq_ns = addMoney(rq_ns, rq_entry.ownerId, rq_entry.amt, "Retainer (\""+rq_entry.storedName+"\" triggered)");
      rq_ns = addLog(rq_ns, "🤝 "+pname(rq_ns,rq_entry.ownerId)+"'s Retainer fires - +$"+rq_entry.amt+"k! ("+rq_entry.usesLeft+" use"+(rq_entry.usesLeft!==1?"s":"")+" left)", "asset", [], true);
      return maybeFlush(rq_ns);
    }
    if (st.swearJarQueue && st.swearJarQueue.length) {
      var sq_entry = st.swearJarQueue[0];
      var sq_ns = { ...st, swearJarQueue:st.swearJarQueue.slice(1) };
      sq_ns = pushTrigger(sq_ns, { type:T.TARGET_PLAYER, srcPlayer:sq_entry.ownerId, tgtPlayer:sq_entry.reactorId, label:"Swear Jar", _automatic:true });
      var sq_trig = sq_ns.triggerStack[sq_ns.triggerStack.length-1];
      var sq_reactors = reactorsFor(sq_ns.players, sq_trig);
      var sq_eff = { hook:"swear_jar_pay", srcPlayer:sq_entry.ownerId, tgtPlayer:sq_entry.reactorId, value:sq_entry.amount, srcCard:{ name:"Swear Jar" } };
      sq_ns = { ...sq_ns, pendingEffect:sq_eff };
      if (sq_reactors.some(function(r){ return r.canReact; })) {
        return { ...sq_ns, reactionWindow:buildWindow(uid(), sq_trig.tid, T.TARGET_PLAYER, sq_entry.ownerId, sq_entry.reactorId, sq_reactors) };
      }
      sq_ns = { ...sq_ns, pendingEffect:null };
      return maybeFlush(applyHook(sq_ns, "swear_jar_pay", sq_eff));
    }
    if (st.wdQueue && st.wdQueue.length) {
      var wdq_entry = st.wdQueue[0];
      var wdq_ns = { ...st, wdQueue:st.wdQueue.slice(1) };
      wdq_ns = pushTrigger(wdq_ns, { type:T.TARGET_PLAYER, srcPlayer:wdq_entry.srcPlayer, tgtPlayer:wdq_entry.tgtPlayer, label:"Water Damage" });
      var wdq_trig = wdq_ns.triggerStack[wdq_ns.triggerStack.length-1];
      var wdq_reactors = reactorsFor(wdq_ns.players, wdq_trig);
      // Include value in srcCard so Refusal to Work can read the hit amount from srcCard.value
      var wdq_eff = { hook:"water_damage_hit", srcPlayer:wdq_entry.srcPlayer, tgtPlayer:wdq_entry.tgtPlayer, value:wdq_entry.amount, srcCard:{ name:"Water Damage", hook:"water_damage", value:wdq_entry.amount } };
      wdq_ns = { ...wdq_ns, pendingEffect:wdq_eff };
      if (wdq_reactors.some(function(r){ return r.canReact; })) {
        return { ...wdq_ns, reactionWindow:buildWindow(uid(), wdq_trig.tid, T.TARGET_PLAYER, wdq_entry.srcPlayer, wdq_entry.tgtPlayer, wdq_reactors) };
      }
      wdq_ns = { ...wdq_ns, pendingEffect:null };
      return maybeFlush(applyHook(wdq_ns, "water_damage_hit", wdq_eff));
    }
    if (st.negReactionPayQueue && st.negReactionPayQueue.length) {
      var nrpq_entry = st.negReactionPayQueue[0];
      var nrpq_ns = { ...st, negReactionPayQueue: st.negReactionPayQueue.slice(1), _skipGainWindow: true };
      nrpq_ns = payMoney(nrpq_ns, nrpq_entry.payerId, nrpq_entry.ownerId, 2, "Negative Reaction");
      nrpq_ns = { ...nrpq_ns, _skipGainWindow: false };
      return maybeFlush(nrpq_ns); // pauses here if a LOSE_MONEY window opened
    }
    return applyAllHandBoosts(flushPendingDiscard(st));
  }
  return st;
}


/* - Pocket Change helpers - */


/* -- Discard Queue -- sequential hand-discard with DISCARD_CARD reaction windows -- */
// afterHook values: "let_go_resolve" | "neg_reaction_resolve"
function startDiscardQueue(st, items, afterHook, afterContext) {
  var validItems = items.filter(function(i) { return i && i.card && i.pid; });
  if (!validItems.length) return resolveDiscardQueue({ ...st, discardQueue:null }, afterHook, afterContext, []);
  var ns = { ...st, discardQueue:{
    queue: validItems, currentDiscard: null, discarded: [],
    afterHook: afterHook, afterContext: afterContext
  }};
  return advanceDiscardQueue(ns);
}

function advanceDiscardQueue(st) {
  var dq = st.discardQueue;
  if (!dq) return maybeFlush(st);
  if (dq.queue.length === 0) {
    var ns = { ...st, discardQueue:null };
    return resolveDiscardQueue(ns, dq.afterHook, dq.afterContext, dq.discarded);
  }
  var item = dq.queue[0];
  var rest = dq.queue.slice(1);
  var ns = { ...st, discardQueue:{ ...dq, queue:rest, currentDiscard:item } };
  ns = pushTrigger(ns, { type:T.DISCARD_CARD, srcPlayer:item.pid, tgtPlayer:item.pid });
  var trig = ns.triggerStack[ns.triggerStack.length - 1];
  var reactors = reactorsFor(ns.players, trig);
  if (reactors.some(function(r) { return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.DISCARD_CARD, item.pid, item.pid, reactors) };
  }
  return executeDiscard(ns, item);
}

function executeDiscard(st, item) {
  var dq = st.discardQueue;
  var ns = setPl(st, item.pid, function(p) {
    return { ...p, hand:p.hand.filter(function(c) { return c.id !== item.card.id; }) };
  });
  ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...item.card, value:item.card.origVal, modifiedBy:[] }] };
  ns = addLog(ns, "\u{1F5D1} " + pname(ns, item.pid) + " discards \"" + item.card.name + "\".", "discard", [item.card]);
  if (dq) {
    ns = { ...ns, discardQueue:{ ...dq, discarded:[...dq.discarded, item], currentDiscard:null } };
  }
  return advanceDiscardQueue(ns);
}

function resolveDiscardQueue(st, afterHook, afterContext, discarded) {
  var ns = st;
  if (afterHook === "let_go_resolve") {
    discarded.forEach(function(item) {
      ns = addMoney(ns, item.pid, -item.card.value, "Let Go (" + item.card.name + ")");
      ns = applyPlusInterest(ns, afterContext.srcPlayer, item.pid);
      ns = applyAccountant(ns, item.pid, afterContext.srcPlayer, item.card.value);
    });
  } else if (afterHook === "neg_reaction_resolve") {
    if (discarded.length > 0) {
      ns = addLog(ns, "\uD83D\uDCB0 " + pname(ns, afterContext.srcPlayer) + " collects $2k from each opponent (" + discarded.length + " card" + (discarded.length > 1 ? "s" : "") + " discarded).", "money");
      // Process payments sequentially via queue so each payer can react (Cover the Costs, etc.)
      // _skipGainWindow suppresses Scammed for the collector; _skipLoseMoneyWindow is NOT set
      // so the payer's LOSE_MONEY window can open normally.
      var nrPayers = discarded.map(function(item) { return { payerId: item.pid, ownerId: afterContext.srcPlayer }; });
      ns = { ...ns, negReactionPayQueue: nrPayers.slice(1), _skipGainWindow: true };
      ns = payMoney(ns, nrPayers[0].payerId, nrPayers[0].ownerId, 2, "Negative Reaction");
      ns = { ...ns, _skipGainWindow: false };
      if (ns.reactionWindow) return ns; // window opened for first payer \u2014 resume via maybeFlush
    } else {
      ns = addLog(ns, "Negative Reaction: No cards discarded.", "info");
    }
  }
  // If this effect was copied by Business Partners (or similar), the original action's
  // __resume_action__ pendingEffect is still in state — resume it now.
  if (ns.pendingEffect && ns.pendingEffect.hook === "__resume_action__") {
    var bpRpe = ns.pendingEffect;
    var bpDw = ns.deferredWindow;
    ns = { ...ns, pendingEffect:null, deferredWindow:null };
    // If the outer ACTION_PLAYED window still has pending decisions (e.g., 3+ players), restore it
    if (bpDw && bpDw.ttype === T.ACTION_PLAYED && bpDw.reactors && bpDw.reactors.some(function(r){ return r.decision === "PENDING"; })) {
      return { ...ns, reactionWindow:bpDw, pendingEffect:bpRpe };
    }
    return continueAfterActionPlayed(ns, bpRpe);
  }
  return maybeFlush(ns);
}

/* - Shrinkage helpers - */
function executeShrinkageDiscard(st) {
  var ss = st.shrinkageState;
  if (!ss) return maybeFlush(st);
  var tgtPl = st.players.find(function(p){ return p.id === ss.tgtPlayer; });
  if (!tgtPl || !tgtPl.hand.length) {
    var ns_fizzle = addLog(st, pname(st, ss.tgtPlayer) + " has no cards to discard (Shrinkage fizzled).", "info");
    ns_fizzle = { ...ns_fizzle, shrinkageState:null };
    var dw_fizzle = ns_fizzle.deferredWindow;
    ns_fizzle = { ...ns_fizzle, deferredWindow:null };
    if (dw_fizzle) return { ...ns_fizzle, reactionWindow:dw_fizzle };
    return maybeFlush(ns_fizzle);
  }
  return { ...st, pendingChoice:{
    type:"SHRINKAGE_SELECT", srcPlayer:ss.srcPlayer, tgtPlayer:ss.tgtPlayer,
    timerEnd:Date.now()+20000,
    prompt:pname(st, ss.tgtPlayer) + " must discard a card (Shrinkage).",
    options:tgtPl.hand,
    shrinkageState:ss,
    _deferredWindow:st.deferredWindow||null,
  }};
}

/* - Asset transfer helpers - */
// Moves an asset from one player to another, recalculating value for the new owner.
// NOTE (future): When "attached cards" are implemented (cards placed on top of assets),
// those cards must be returned to the previous owner's hand here before transfer.
function transferAsset(st, fromPid, toPid, assetId) {
  var fromPl = st.players.find(function(p) { return p.id === fromPid; });
  if (!fromPl) return st;
  var asset = fromPl.assets.find(function(a) { return a.id === assetId; });
  if (!asset) return st;
  // TODO (attached cards): before removing the asset, find any cards attached to it
  // and push them back into fromPl's hand. e.g.:
  // if (asset.attachedCards) {
  //   ns = setPl(ns, fromPid, p => ({ ...p, hand:[...p.hand, ...asset.attachedCards] }));
  // }
  var ns = st;
  // If the transferring asset is a Product Update with an applied effect, revert it
  if (asset.hook === "a_product_update" && asset._appliedTo) {
    ns = revertPUEffect(ns, asset);
  }
  // If the transferring asset was modified by a Product Update, notify the PU owner
  if (asset._puSourceId) {
    ns.players.forEach(function(p) {
      var pu_a = p.assets.find(function(a){ return a.id===asset._puSourceId; });
      if (pu_a) {
        ns = setPl(ns, p.id, function(pl){ return {...pl, assets:pl.assets.map(function(a){
          return a.id===pu_a.id ? {...a,_appliedTo:null} : a;
        })}; });
      }
    });
  }
  ns = setPl(ns, fromPid, function(p) {
    return { ...p, assets:p.assets.filter(function(a) { return a.id !== assetId; }) };
  });
  // Recalculate value for new owner (strips previous owner's modifiers, applies new owner's)
  var transferred = computeAssetValueForOwner(asset, toPid, ns);
  // Status, tokens transfer as-is (per spec - state does not change on ownership transfer)
  ns = setPl(ns, toPid, function(p) {
    return { ...p, assets:[...p.assets, transferred] };
  });
  // If asset had an Out of Order effect, remove it (ownership change resets OOO per spec)
  ns = { ...ns, outOfOrderEffects:(ns.outOfOrderEffects||[]).filter(function(e){ return e.assetId !== assetId; }) };
  // Re-enable the asset if it was OOO-disabled (clear the flag after stripping the effect)
  ns = setPl(ns, toPid, function(p){
    return { ...p, assets:p.assets.map(function(a){
      if (a.id !== assetId) return a;
      var cleaned = { ...a };
      delete cleaned.outOfOrderBy; delete cleaned.outOfOrderPayAmt;
      return { ...cleaned, disabled:false };
    })};
  });
  // Reapply hand boosts (HVG) for both affected players
  ns = applyHandBoosts(ns, fromPid);
  ns = applyHandBoosts(ns, toPid);
  return ns;
}

// Placeholder for future asset value modifiers tied to ownership.
// Currently resets to origVal. Future: check new owner's passive assets/cards
// that modify asset values (e.g. an "Asset Appreciation" card) and apply them here.
function computeAssetValueForOwner(asset, ownerId, st) {
  // Strip any previous-owner modifiers and reset to base value
  // Future implementation: query new owner's relevant passives and adjust
  return { ...asset, value:asset.origVal, modifiedBy:[] };
}

/* - Negative Reaction helpers - */
function startNegReactionTargeting(st, srcPlayer) {
  var opponents = st.players.filter(function(p) {
    return p.id !== srcPlayer && p.hand.some(function(c) { return c.type === CT.REACTION; });
  });
  if (!opponents.length) {
    return maybeFlush(addLog(st, pname(st, srcPlayer) + ": No opponents with reaction cards - Negative Reaction fizzled.", "warn"));
  }
  var queue = opponents.map(function(p) { return p.id; });
  var firstTarget = queue[0];
  var ns = { ...st, negReactionState: {
    srcPlayer: srcPlayer, queue: queue.slice(1), currentTarget: firstTarget,
    confirmedTargets: [], collectQueue: [], selections: [], phase: "targeting"
  }};
  ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:srcPlayer, tgtPlayer:firstTarget, label:"Negative Reaction" });
  var trig = ns.triggerStack[ns.triggerStack.length - 1];
  var reactors = reactorsFor(ns.players, trig);
  if (reactors.some(function(r) { return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, srcPlayer, firstTarget, reactors) };
  }
  var ns2 = { ...ns, negReactionState: { ...ns.negReactionState, currentTarget:null, confirmedTargets:[firstTarget] } };
  return advanceNegReactionTargeting(ns2);
}

function advanceNegReactionTargeting(st) {
  var nrs = st.negReactionState;
  if (!nrs || nrs.phase !== "targeting") return maybeFlush(st);

  if (nrs.queue.length > 0) {
    var nextPid = nrs.queue[0];
    var ns = { ...st, negReactionState: { ...nrs, queue:nrs.queue.slice(1), currentTarget:nextPid } };
    ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:nrs.srcPlayer, tgtPlayer:nextPid, label:"Negative Reaction" });
    var trig = ns.triggerStack[ns.triggerStack.length - 1];
    var reactors = reactorsFor(ns.players, trig);
    if (reactors.some(function(r) { return r.canReact; })) {
      return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, nrs.srcPlayer, nextPid, reactors) };
    }
    var confirmed = [...nrs.confirmedTargets, nextPid];
    return advanceNegReactionTargeting({ ...ns, negReactionState:{ ...ns.negReactionState, currentTarget:null, confirmedTargets:confirmed } });
  }

  // Queue empty - move to collecting
  if (nrs.confirmedTargets.length === 0) {
    return maybeFlush(addLog({ ...st, negReactionState:null }, "Negative Reaction: All targets blocked.", "info"));
  }
  var firstTgt = nrs.confirmedTargets[0];
  var collectQ = nrs.confirmedTargets.slice(1);
  var ns2 = { ...st, negReactionState:{ ...nrs, phase:"collecting", collectQueue:collectQ, selections:[] } };
  var tpFirst = ns2.players.find(function(p) { return p.id === firstTgt; });
  var reactionCards = tpFirst ? tpFirst.hand.filter(function(c) { return c.type === CT.REACTION; }) : [];
  return { ...ns2, pendingChoice:{ type:"NEG_REACTION_SELECT", srcPlayer:nrs.srcPlayer, tgtPlayer:firstTgt,
    timerEnd:Date.now()+20000,
    prompt: pname(ns2, nrs.srcPlayer) + " played Negative Reaction - choose a reaction card to discard.",
    options: reactionCards }};
}

/* - Let Go helpers - */
function startLetGoTargeting(st, srcPlayer, srcCard) {
  var opponents = st.players.filter(function(p) { return p.id !== srcPlayer && p.hand.length > 0; });
  if (!opponents.length) {
    return addLog(st, pname(st, srcPlayer) + ": No opponents with cards - Let Go fizzled.", "warn");
  }
  var queue = opponents.map(function(p) { return p.id; });
  var firstTarget = queue[0];
  var ns = { ...st, letGoState: {
    srcPlayer: srcPlayer, queue: queue.slice(1), currentTarget: firstTarget,
    confirmedTargets: [], collectQueue: [], selections: [], phase: "targeting",
    srcCard: srcCard || null   // stored so Refusal to Work can read the card's value
  }};
  ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:srcPlayer, tgtPlayer:firstTarget, label:"Let Go" });
  var trig = ns.triggerStack[ns.triggerStack.length - 1];
  var reactors = reactorsFor(ns.players, trig);
  if (reactors.some(function(r) { return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, srcPlayer, firstTarget, reactors) };
  }
  var ns2 = { ...ns, letGoState: { ...ns.letGoState, currentTarget:null, confirmedTargets:[firstTarget] } };
  return advanceLetGoTargeting(ns2);
}

function advanceLetGoTargeting(st) {
  var lgs = st.letGoState;
  if (!lgs || lgs.phase !== "targeting") return maybeFlush(st);

  if (lgs.queue.length > 0) {
    var nextPid = lgs.queue[0];
    var ns = { ...st, letGoState: { ...lgs, queue:lgs.queue.slice(1), currentTarget:nextPid } };
    ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:lgs.srcPlayer, tgtPlayer:nextPid, label:"Let Go" });
    var trig = ns.triggerStack[ns.triggerStack.length - 1];
    var reactors = reactorsFor(ns.players, trig);
    if (reactors.some(function(r) { return r.canReact; })) {
      return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, lgs.srcPlayer, nextPid, reactors) };
    }
    var confirmed = [...lgs.confirmedTargets, nextPid];
    return advanceLetGoTargeting({ ...ns, letGoState:{ ...ns.letGoState, currentTarget:null, confirmedTargets:confirmed } });
  }

  // Queue empty - move to collecting
  if (lgs.confirmedTargets.length === 0) {
    return maybeFlush(addLog({ ...st, letGoState:null }, "Let Go: All targets blocked.", "info"));
  }
  var firstTgt = lgs.confirmedTargets[0];
  var collectQ = lgs.confirmedTargets.slice(1);
  var ns2 = { ...st, letGoState:{ ...lgs, phase:"collecting", collectQueue:collectQ, selections:[] } };
  var tpFirst = ns2.players.find(function(p) { return p.id === firstTgt; });
  var firstTgtIdx = ns2.players.findIndex(function(p){ return p.id===firstTgt; });
  return { ...ns2, viewIdx:firstTgtIdx >= 0 ? firstTgtIdx : ns2.viewIdx,
    pendingChoice:{ type:"LET_GO_SELECT", srcPlayer:lgs.srcPlayer, tgtPlayer:firstTgt,
    timerEnd:Date.now()+20000,
    prompt: pname(ns2, lgs.srcPlayer) + " played Let Go - choose a card to discard. You will lose its value.",
    options: tpFirst ? tpFirst.hand : [] }};
}

function startPocketChangeTargeting(st, srcPlayer) {
  var opponents = st.players.filter(function(p) { return p.id !== srcPlayer && p.hand.length > 0; });
  if (!opponents.length) {
    return addLog(st, pname(st, srcPlayer) + ": No opponents with cards - Pocket Change fizzled.", "warn");
  }
  var queue = opponents.map(function(p) { return p.id; });
  var firstTarget = queue[0];
  var ns = { ...st, pocketChangeState: {
    srcPlayer: srcPlayer, queue: queue.slice(1), currentTarget: firstTarget,
    confirmedTargets: [], collectQueue: [], selections: [], phase: "targeting"
  }};
  ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:srcPlayer, tgtPlayer:firstTarget, label:"Pocket Change" });
  var trig = ns.triggerStack[ns.triggerStack.length - 1];
  var reactors = reactorsFor(ns.players, trig);
  if (reactors.some(function(r) { return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, srcPlayer, firstTarget, reactors) };
  }
  // No reactors - auto-confirm first, advance immediately
  var ns2 = { ...ns, pocketChangeState: { ...ns.pocketChangeState, currentTarget:null, confirmedTargets:[firstTarget] } };
  return advancePocketChangeTargeting(ns2);
}

function advancePocketChangeTargeting(st) {
  var pcs = st.pocketChangeState;
  if (!pcs || pcs.phase !== "targeting") return maybeFlush(st);

  if (pcs.queue.length > 0) {
    var nextPid = pcs.queue[0];
    var ns = { ...st, pocketChangeState: { ...pcs, queue:pcs.queue.slice(1), currentTarget:nextPid } };
    ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pcs.srcPlayer, tgtPlayer:nextPid, label:"Pocket Change" });
    var trig = ns.triggerStack[ns.triggerStack.length - 1];
    var reactors = reactorsFor(ns.players, trig);
    if (reactors.some(function(r) { return r.canReact; })) {
      return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, pcs.srcPlayer, nextPid, reactors) };
    }
    // Auto-confirm, recurse
    var autoConfirmed = [...pcs.confirmedTargets, nextPid];
    return advancePocketChangeTargeting({ ...ns, pocketChangeState:{ ...ns.pocketChangeState, currentTarget:null, confirmedTargets:autoConfirmed } });
  }

  // Queue empty - move to collecting phase
  if (pcs.confirmedTargets.length === 0) {
    return maybeFlush(addLog({ ...st, pocketChangeState:null }, "💸 Pocket Change: All targets blocked.", "info"));
  }
  var firstTgt = pcs.confirmedTargets[0];
  var collectQ  = pcs.confirmedTargets.slice(1);
  var ns2 = { ...st, pocketChangeState:{ ...pcs, phase:"collecting", collectQueue:collectQ, selections:[] } };
  var tpFirst = ns2.players.find(function(p) { return p.id === firstTgt; });
  return { ...ns2, pendingChoice:{ type:"POCKET_CHANGE_SELECT", srcPlayer:pcs.srcPlayer, tgtPlayer:firstTgt,
    timerEnd:Date.now()+20000, needed:1,
    prompt:pname(ns2,pcs.srcPlayer)+" will gain the value of the card you select.",
    options:tpFirst ? tpFirst.hand : [] }};
}


/* - Kickstarter helpers - */
function turnRelativeOrder(players, srcPlayerId) {
  var si = players.findIndex(function(p) { return p.id === srcPlayerId; });
  var result = [];
  for (var i = 1; i < players.length; i++) {
    result.push(players[(si + i) % players.length].id);
  }
  return result;
}

function startKickstarter(st, srcPlayer) {
  var queue = turnRelativeOrder(st.players, srcPlayer);
  if (!queue.length) return addLog(st, pname(st, srcPlayer) + ": No opponents for Kickstarter.", "warn");
  var firstTarget = queue[0];
  var ns = { ...st, kickstarterState: {
    srcPlayer: srcPlayer,
    phase: "targeting",
    targetQueue: queue.slice(1),
    currentTarget: firstTarget,
    confirmedTargets: [],
    choices: {},
  }};
  ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:srcPlayer, tgtPlayer:firstTarget, label:"Kickstarter" });
  var trig = ns.triggerStack[ns.triggerStack.length - 1];
  var reactors = reactorsFor(ns.players, trig);
  if (reactors.some(function(r) { return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, srcPlayer, firstTarget, reactors) };
  }
  var ks2 = { ...ns.kickstarterState, currentTarget:null, confirmedTargets:[firstTarget] };
  return advanceKickstarterTargeting({ ...ns, kickstarterState:ks2 });
}

function advanceKickstarterTargeting(st) {
  var ks = st.kickstarterState;
  if (!ks || ks.phase !== "targeting") return maybeFlush(st);
  if (ks.targetQueue.length > 0) {
    var nextPid = ks.targetQueue[0];
    var ns = { ...st, kickstarterState:{ ...ks, targetQueue:ks.targetQueue.slice(1), currentTarget:nextPid } };
    ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:ks.srcPlayer, tgtPlayer:nextPid, label:"Kickstarter" });
    var trig = ns.triggerStack[ns.triggerStack.length - 1];
    var reactors = reactorsFor(ns.players, trig);
    if (reactors.some(function(r) { return r.canReact; })) {
      return { ...ns, reactionWindow:buildWindow(uid(), trig.tid, T.TARGET_PLAYER, ks.srcPlayer, nextPid, reactors) };
    }
    var confirmed = [...ks.confirmedTargets, nextPid];
    return advanceKickstarterTargeting({ ...ns, kickstarterState:{ ...ns.kickstarterState, currentTarget:null, confirmedTargets:confirmed } });
  }
  // Targeting done - start choosing phase
  if (!ks.confirmedTargets.length) {
    return maybeFlush(addLog({ ...st, kickstarterState:null }, "💸 Kickstarter: All targets blocked.", "info"));
  }
  return startKickstarterChoosing(st);
}

function startKickstarterChoosing(st) {
  var ks = st.kickstarterState;
  var choiceQueue = [...ks.confirmedTargets];
  var firstChooser = choiceQueue[0];
  var ns = flushPendingDiscard({ ...st, kickstarterState:{ ...ks, phase:"choosing", choiceQueue:choiceQueue.slice(1) } });
  var tgtPl = ns.players.find(function(p) { return p.id === firstChooser; });
  return { ...ns, pendingChoice:{ type:"KICKSTARTER_CHOICE", srcPlayer:ks.srcPlayer, tgtPlayer:firstChooser,
    timerEnd:Date.now()+20000,
    assetDeckEmpty: ns.assetDeck.length === 0,
    prompt:pname(ns, ks.srcPlayer) + " played Kickstarter. Choose your contribution:" }};
}

function applyKickstarterResolution(st) {
  var ks = st.kickstarterState;
  if (!ks) return maybeFlush(st);
  var ns = { ...st, kickstarterState:null };
  // Step A: debit each payer individually (LOSE_MONEY triggers fire per-player),
  //         but defer crediting the owner - they receive one lump sum at the end.
  var totalPaid = 0;
  var assetQueue = [];
  var drawQueue  = [];
  ks.confirmedTargets.forEach(function(pid) {
    var choice = ks.choices[pid] || { amount:2, draw:0, gainAsset:false };
    totalPaid += choice.amount;
    // Debit the payer — apply Loss Prevention, Plus Interest, Accountant, and Side Hustle
    ns = pushTrigger(ns, { type:T.PAY_MONEY, srcPlayer:pid, tgtPlayer:ks.srcPlayer, value:choice.amount });
    var ks_lp = calcLossAfterPrevention(ns, pid, choice.amount);
    var ks_loss = ks_lp.reducedAmount;
    if (ks_lp.saved > 0) ns = addLog(ns, "🛡 "+pname(ns,pid)+"'s Loss Prevention saves $"+ks_lp.saved+"k!", "asset", [], true);
    ns = setPl(ns, pid, function(p) { return { ...p, money:p.money - ks_loss, totalLost:(p.totalLost||0)+ks_loss }; });
    var payerBal = (ns.players.find(function(p){ return p.id === pid; })||{}).money||0;
    ns = addLog(ns, "📤 " + pname(ns,pid) + " pays $" + choice.amount + "k (Kickstarter) -> " + $(payerBal), "loss");
    ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:pid, value:ks_loss });
    // Risk vs Reward: fire for each payer since Kickstarter bypasses addMoney for debtors
    (function(loser_pid) {
      ns.players.forEach(function(rvrOwner) {
        if (rvrOwner.id === loser_pid) return;
        rvrOwner.assets.forEach(function(a) {
          if (a.hook !== "a_risk_reward" || a.disabled || a.riskRewardTarget !== loser_pid) return;
          var rvrAmt = (a.riskRewardAmount || 1) * (a._franchised ? 2 : 1);
          ns = addMoney(ns, rvrOwner.id, rvrAmt, "Risk vs Reward (-> "+pname(ns,loser_pid)+" paid Kickstarter)", false, true);
          ns = addLog(ns, "⚖️ "+pname(ns,rvrOwner.id)+"'s Risk vs Reward fires! +$"+rvrAmt+"k", "asset", [], true);
        });
      });
    })(pid);
    ns = applyPlusInterest(ns, ks.srcPlayer, pid);
    ns = applyAccountant(ns, pid, ks.srcPlayer, ks_loss);
    // Side Hustle: owner earns extra per payer (mirrors payMoney logic)
    var ks_sh_owner = ns.players.find(function(p){ return p.id===ks.srcPlayer; });
    if (ks_sh_owner) {
      var ks_sh_saved = ns._skipLoseMoneyWindow;
      ns = { ...ns, _skipLoseMoneyWindow:true };
      ks_sh_owner.assets.forEach(function(a) {
        if (a.hook !== "p_side_hustle" || a.disabled) return;
        var sh_amt = (a.sideHustleAmount||1)*(a._franchised?2:1)*(ks_sh_owner._doublePassive?2:1);
        ns = addLog(ns, "💼 "+pname(ns,pid)+" pays $"+sh_amt+"k extra (Side Hustle -> Kickstarter)!", "asset", [], true);
        ns = payMoney(ns, pid, ks.srcPlayer, sh_amt, "Side Hustle", true);
      });
      ns = { ...ns, _skipLoseMoneyWindow:ks_sh_saved };
    }
    ns = checkBankruptcy(ns, pid);
    if (choice.gainAsset) assetQueue.push(pid);
    if (choice.draw > 0)  drawQueue.push({ pid:pid, count:choice.draw });
  });
  // Credit the Kickstarter owner in one lump sum (fires GAIN_MONEY / Leave A Tip / EI only once)
  if (totalPaid > 0) ns = addMoney(ns, ks.srcPlayer, totalPaid, "Kickstarter (total collected)");
  // Step B: asset gains in order
  assetQueue.forEach(function(pid) {
    var res = tryReshuffle(ns.assetDeck, ns.assetDiscard);
    if (res.empty) {
      ns = addLog(ns, pname(ns, pid) + ": Asset deck empty - no asset gained.", "warn");
      return;
    }
    if (res.reshuffled) ns = addLog(ns, "🔀 Asset deck reshuffled.", "sys");
    var gained = (res.reshuffled ? res.deck : ns.assetDeck)[0];
    ns = setPl(ns, pid, function(p) { return { ...p, assets:[...p.assets, {...gained, status:ST.READY, tokens:[]}] }; });
    ns = { ...ns, assetDeck:(res.reshuffled ? res.deck : ns.assetDeck).slice(1), assetDiscard:res.reshuffled ? [] : ns.assetDiscard };
    ns = addLog(ns, "🏢 " + pname(ns, pid) + " gains \"" + gained.name + "\" from Kickstarter bonus.", "asset", [gained]);
    ns = pushTrigger(ns, { type:T.BUY_ASSET, srcPlayer:pid, value:0 });
  });
  // Step C: draws in order
  drawQueue.forEach(function(entry) {
    ns = drawN(ns, entry.pid, entry.count, 0, true);
  });
  return maybeFlush(ns);
}

/* - Deck helpers - */
function tryReshuffle(deck, disc) {
  if (deck.length > 0) return { deck, disc, reshuffled:false, empty:false };
  if (!disc.length)    return { deck:[], disc:[], reshuffled:false, empty:true };
  return { deck:shuf([...disc]), disc:[], reshuffled:true, empty:false };
}

/* - Money helpers - */
function addMoney(st, pid, delta, reason, _skipTip, _skipEI, _skipPW, _skipLP) {
  reason = reason || "";
  _skipTip = _skipTip || false;
  // Loss Prevention: flat reduction on any direct loss (not purchases — callers pass _skipLP:true there)
  if (delta < 0 && !_skipLP) {
    var lp_r = calcLossAfterPrevention(st, pid, Math.abs(delta));
    if (lp_r.saved > 0) {
      st = addLog(st, "🛡 "+pname(st,pid)+"'s Loss Prevention saves $"+lp_r.saved+"k!", "asset", [], true);
      delta = -lp_r.reducedAmount;
      if (delta === 0) return st; // loss fully absorbed — nothing more to do
    }
  }
  var actualDelta = delta;
  var tipLog = null;
  // Leave A Tip passive: bonus on positive gains (guard _skipTip to prevent self-triggering)
  if (delta > 0 && !_skipTip) {
    var pl0 = st.players.find(function(p) { return p.id === pid; });
    var tipAsset = pl0 && pl0.assets.find(function(a) { return a.hook === "p_leave_tip" && !a.disabled; });
    if (tipAsset) {
      var tipAmt = (tipAsset.tipAmount || 1) * (tipAsset._franchised ? 2 : 1) * (pl0._doublePassive ? 2 : 1);
      actualDelta = delta + tipAmt;
      tipLog = pname(st, pid) + "'s Leave A Tip adds $" + tipAmt + "k bonus.";
    }
  }
  var ns = setPl(st, pid, function(p) {
    return {
      ...p,
      money: p.money + actualDelta,
      totalGained: (p.totalGained||0) + (actualDelta > 0 ? actualDelta : 0),
      totalLost:   (p.totalLost  ||0) + (actualDelta < 0 ? Math.abs(actualDelta) : 0),
    };
  });
  var bal = (ns.players.find(function(p) { return p.id === pid; }) || {}).money || 0;
  if (reason || actualDelta !== 0) ns = addLog(ns, (actualDelta>=0?"💵":"📤") + " " + pname(ns,pid) + " " + (actualDelta>=0?"gains":"loses") + " " + $(Math.abs(actualDelta)) + (reason?" ("+reason+")":"") + " -> " + $(bal), actualDelta>=0?"money":"loss");
  if (tipLog) { ns = addLog(ns, "  -> " + tipLog, "hook"); ns = addLog(ns, "🪙 " + tipLog, "asset", [], true); }
  // Emit money event for popup animation
  if (delta !== 0) {
    var evLines = [];
    if (delta !== 0) evLines.push({ amount: delta, reason: reason || "transaction" });
    if (actualDelta - delta > 0) evLines.push({ amount: actualDelta - delta, reason: "Leave A Tip" });
    var _evName = pname(ns, pid);
  ns = { ...ns, moneyEvents: [...(ns.moneyEvents||[]), { id:uid(), pid, playerName:_evName, lines:evLines, total:actualDelta }] };
  }
  // Push GAIN_MONEY trigger for positive amounts (non-blocking, for future reaction extensibility)
  if (actualDelta > 0) {
    ns = pushTrigger(ns, { type:T.GAIN_MONEY, srcPlayer:pid, value:actualDelta });
    // Paid Work: 2s prompt before full reaction window
    if (!ns.pendingPWChoice && !ns._skipPWCheck) {
      ns = checkPaidWork(ns, pid, actualDelta, true, reason);
    }
  // Open reaction window for Scammed and similar GAIN_MONEY reactions
  if (!ns._skipGainWindow) {
    var gm_trig = ns.triggerStack[ns.triggerStack.length-1];
    var gm_reactors = reactorsFor(ns.players, gm_trig);
    if (gm_reactors.some(function(r){ return r.canReact; })) {
      // Store actual gain amount so Scammed hook can steal the correct value
      ns = { ...ns,
        pendingEffect:{ hook:"__scammed__", tgtPlayer:pid, value:actualDelta },
        reactionWindow:buildWindow(uid(), gm_trig.tid, T.GAIN_MONEY, pid, null, gm_reactors) };
    }
  }
    // Revenue Stream: add token on gains >= 6k
    if (actualDelta >= 6) {
      var rs_pl2 = ns.players.find(function(p){ return p.id===pid; });
      if (rs_pl2) rs_pl2.assets.forEach(function(a) {
        if (a.hook==="p_revenue_stream" && !a.disabled && (a.tokens||[]).length < (a.tmax||3)) {
          ns = setPl(ns, pid, function(p){ return { ...p, assets:p.assets.map(function(b){
            return b.id===a.id ? { ...b, tokens:[...(b.tokens||[]),{id:uid()}] } : b;
          }) }; });
          ns = addLog(ns, "💰 "+pname(ns,pid)+"'s Revenue Stream gains a token ("+(( a.tokens||[]).length+1)+"/"+(a.tmax||3)+").", "asset", [], true);
        }
      });
    }
  }
  // Paid Work: show 2s popup if any player has PW ready (skip for PW-triggered adjustments)
  // (Paid Work is handled via checkPaidWork → pendingPWChoice in the gain/loss blocks)
  // Paid Work for direct losses (addMoney with negative delta)
  if (actualDelta < 0 && !ns.pendingPWChoice && !ns._skipPWCheck) {
    ns = checkPaidWork(ns, pid, Math.abs(actualDelta), false, reason);
  }
  // Extra Income passive: any player with EI targeting this pid gains their bonus
  if (actualDelta > 0 && !_skipEI) {
    ns.players.forEach(function(eiOwner) {
      if (eiOwner.id === pid) return;
      eiOwner.assets.forEach(function(a) {
        if (a.hook === "a_extra_income" && !a.disabled && a.extraIncomeTarget === pid) {
          var eiAmt = a.extraIncomeAmount || 1;
          ns = addMoney(ns, eiOwner.id, eiAmt, "Extra Income (-> " + pname(ns, pid) + " gained)", false, true); ns = addLog(ns, "📈 " + pname(ns, eiOwner.id) + "'s Extra Income fires! +$" + eiAmt + "k", "asset", [], true);
        }
      });
    });
  }
  // Risk vs Reward passive: any player with RVR targeting this pid gains on their loss
  if (actualDelta < 0) {
    ns.players.forEach(function(rvrOwner) {
      if (rvrOwner.id === pid) return;
      rvrOwner.assets.forEach(function(a) {
        if (a.hook === "a_risk_reward" && !a.disabled && a.riskRewardTarget === pid) {
          var rvrAmt = (a.riskRewardAmount || 1) * (a._franchised ? 2 : 1);
          ns = addMoney(ns, rvrOwner.id, rvrAmt, "Risk vs Reward (-> " + pname(ns, pid) + " lost money)", false, true); ns = addLog(ns, "⚖️ " + pname(ns, rvrOwner.id) + "'s Risk vs Reward fires! +$" + rvrAmt + "k", "asset", [], true);
        }
      });
    });
  }
  return checkBankruptcy(ns, pid);
}


/* - PAY_MONEY helper -
   Owner receives grossAmount via addMoney (triggers GAIN_MONEY, Leave A Tip, EI etc.)
   Payer is debited directly then a LOSE_MONEY trigger is pushed so reducers
   (Fine Print, Insurance) can cut the payer's actual loss without affecting
   what the owner already received.
- */
function calcLossAfterPrevention(st, pid, amount) {
  var saved = 0;
  var victim = st.players.find(function(p){ return p.id === pid; });
  if (!victim) return { reducedAmount:amount, saved:0 };
  victim.assets.forEach(function(a) {
    if (a.hook === "p_loss_prevention" && !a.disabled) {
      // Flat reduction on any loss — no threshold. Franchise and Double Passive both stack.
      var reduction = (a.preventionAmount || 1) * (a._franchised ? 2 : 1) * (victim._doublePassive ? 2 : 1);
      saved += reduction;
    }
  });
  // Cap: cannot save more than the actual loss (no negative losses)
  saved = Math.min(saved, amount);
  return { reducedAmount:Math.max(0, amount - saved), saved };
}

function applyPlusInterest(st, causedById, victimId) {
  if (!causedById || !victimId || causedById === victimId) return st;
  var ns = st;
  var causer = ns.players.find(function(p){ return p.id === causedById; });
  if (!causer) return ns;
  causer.assets.forEach(function(a) {
    if (a.hook === "p_plus_interest" && !a.disabled) {
      var pi_amt = (a.plusInterestAmount || 1) * (a._franchised ? 2 : 1);
      ns = setPl(ns, victimId, function(p){ return { ...p, money:p.money-pi_amt, totalLost:(p.totalLost||0)+pi_amt }; });
      var pi_bal = (ns.players.find(function(p){ return p.id===victimId; })||{}).money||0;
      ns = addLog(ns, "📈 "+pname(ns,victimId)+" loses $"+pi_amt+"k extra (Plus Interest) -> "+$(pi_bal), "loss");
      ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:causedById, tgtPlayer:victimId, value:pi_amt });
      ns = checkBankruptcy(ns, victimId);
    }
  });
  return ns;
}

function applyAccountant(st, victimId, causedById, amount) {
  if (!victimId || !causedById || victimId === causedById) return st;
  var ns = st;
  var victim = ns.players.find(function(p){ return p.id === victimId; });
  if (!victim) return ns;
  victim.assets.forEach(function(a) {
    if (a.hook === "p_accountant" && !a.disabled) {
      var threshold = a.accountantThreshold || 1;
      var penalty = (a.accountantAmount || 1) * (a._franchised ? 2 : 1);
      if (amount > threshold) {
        ns = setPl(ns, causedById, function(p){ return { ...p, money:p.money-penalty, totalLost:(p.totalLost||0)+penalty }; });
        var acc_bal = (ns.players.find(function(p){ return p.id===causedById; })||{}).money||0;
        ns = addLog(ns, "🧾 "+pname(ns,causedById)+" loses $"+penalty+"k (Accountant) -> "+$(acc_bal), "loss");
        ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:victimId, tgtPlayer:causedById, value:penalty });
        ns = checkBankruptcy(ns, causedById);
      }
    }
  });
  return ns;
}

function payMoney(st, payerId, ownerId, grossAmount, reason, _skipSideHustle) {
  reason = reason || "payment";
  if (grossAmount <= 0) return st;
  var ns = st;
  ns = pushTrigger(ns, { type:T.PAY_MONEY, srcPlayer:payerId, tgtPlayer:ownerId, value:grossAmount });
  ns = addMoney(ns, ownerId, grossAmount, reason + " from " + pname(ns, payerId));
  var lp_pay = calcLossAfterPrevention(ns, payerId, grossAmount);
  var payerActualLoss = lp_pay.reducedAmount;
  if (lp_pay.saved > 0) ns = addLog(ns, "🛡 "+pname(ns,payerId)+"'s Loss Prevention saves $"+lp_pay.saved+"k!", "asset", [], true);
  ns = setPl(ns, payerId, function(p){ return { ...p, money:p.money-payerActualLoss, totalLost:(p.totalLost||0)+payerActualLoss }; });
  var payerBal = (ns.players.find(function(p){ return p.id === payerId; }) || {}).money || 0;
  ns = addLog(ns, "📤 " + pname(ns, payerId) + " pays " + $(grossAmount) + " (" + reason + ") -> " + $(payerBal), "loss");
  // Risk vs Reward: payer losing money triggers any RvR monitoring them
  ns.players.forEach(function(rvrOwner) {
    if (rvrOwner.id === payerId) return;
    rvrOwner.assets.forEach(function(a) {
      if (a.hook === "a_risk_reward" && !a.disabled && a.riskRewardTarget === payerId) {
        var rvrAmt = (a.riskRewardAmount || 1) * (a._franchised ? 2 : 1);
        ns = addMoney(ns, rvrOwner.id, rvrAmt, "Risk vs Reward (-> " + pname(ns, payerId) + " paid)", false, true);
        ns = addLog(ns, "⚖️ " + pname(ns, rvrOwner.id) + "'s Risk vs Reward fires! +$" + rvrAmt + "k", "asset", [], true);
      }
    });
  });
  ns = { ...ns, moneyEvents: [...(ns.moneyEvents||[]), { id:uid(), pid:payerId, lines:[{ amount:-grossAmount, reason }], total:-grossAmount }] };
  // 4. Push LOSE_MONEY so Fine Print / Insurance can reduce payer's actual loss
  ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:payerId, value:payerActualLoss });
  // Paid Work: 2s prompt for the payer's loss
  if (!ns.pendingPWChoice && !ns._skipPWCheck) {
    ns = checkPaidWork(ns, payerId, payerActualLoss, false, reason);
  }
  ns = applyPlusInterest(ns, ownerId, payerId);
  ns = applyAccountant(ns, payerId, ownerId, payerActualLoss);
  // Side Hustle: owner earns extra when an opponent pays them.
  // Must run BEFORE the LOSE_MONEY window so it isn't blocked by an early return.
  // Suppress nested LOSE_MONEY windows for the Side Hustle sub-payment to avoid
  // stacking windows; the main payment's window opens right after.
  if (!_skipSideHustle) {
    var sh_owner2 = ns.players.find(function(p){ return p.id===ownerId; });
    if (sh_owner2) {
      var sh_savedSkip = ns._skipLoseMoneyWindow;
      ns = { ...ns, _skipLoseMoneyWindow:true };
      sh_owner2.assets.forEach(function(a) {
        if (a.hook !== "p_side_hustle" || a.disabled) return;
        var sh_amt2 = (a.sideHustleAmount || 1) * (a._franchised ? 2 : 1) * (sh_owner2._doublePassive ? 2 : 1);
        ns = addLog(ns, "💼 "+pname(ns,payerId)+" pays $"+sh_amt2+"k extra (Side Hustle)!", "asset", [], true);
        ns = payMoney(ns, payerId, ownerId, sh_amt2, "Side Hustle", true);
      });
      ns = { ...ns, _skipLoseMoneyWindow: sh_savedSkip || false };
    }
  }
  // Open a LOSE_MONEY reaction window for Cover the Costs / Risky Investment etc.
  if (!ns.reactionWindow && !ns._skipLoseMoneyWindow) {
    var lm_trig_pm = ns.triggerStack[ns.triggerStack.length-1];
    var lm_reactors_pm = reactorsFor(ns.players, lm_trig_pm);
    if (lm_reactors_pm.some(function(r){ return r.canReact; })) {
      ns = { ...ns, pendingEffect:{ hook:"__lose_money__", srcPlayer:payerId, tgtPlayer:ownerId, value:payerActualLoss, reason:reason } };
      return { ...ns, reactionWindow:buildWindow(uid(), lm_trig_pm.tid, T.LOSE_MONEY, payerId, payerId, lm_reactors_pm) };
    }
  }
  return checkBankruptcy(ns, payerId);
}

function calcOverdraft(player) {
  // Fee is purely tracker-based — not tied to how deep in the negatives the player is.
  // Starts at START_FEE ($1k), increases by $1k each turn starting negative (max $3k),
  // decreases by $1k each turn starting positive (minimum START_FEE).
  if (player.money >= 0) return 0;
  return Math.min(Math.max(player.feeToken, 1), 3);
}

/* - Bankruptcy - */
function doBankruptcy(st, pid) {
  const pl = st.players.find(p => p.id===pid);
  if (!pl || pl.money > BANKRUPT_AT) return st;
  const cardCount = pl.hand.length;
  const bonus = cardCount;
  let ns = st;

  // --- Consolation Prize: fires FIRST before asset discard ---
  const cp_asset = pl.assets.find(function(a){ return a.hook==="p_consolation" && !a.disabled; });
  if (cp_asset) {
    var cp_gain = (cp_asset.consolationGain||1) * (cp_asset._franchised ? 2 : 1);
    for (var cpi=0; cpi<cp_gain; cpi++) {
      var cp_res = tryReshuffle(ns.assetDeck, ns.assetDiscard);
      if (cp_res.empty) { ns=addLog(ns,"Consolation Prize: asset deck empty, no asset gained.","warn"); break; }
      if (cp_res.reshuffled) ns={...ns,assetDeck:cp_res.deck,assetDiscard:[]};
      var cp_new = cp_res.deck[0];
      var cp_toks = cp_new.hook==="a_credit_line"?Array.from({length:cp_new.tmax||3},function(){return{id:uid()};}) : cp_new.hook==="a_prospects"?[{id:uid()},{id:uid()},{id:uid()}]:[];
      ns = setPl(ns, pid, function(p){ return {...p,assets:[...p.assets,{...cp_new,status:ST.READY,tokens:cp_toks,disabled:false}]}; });
      ns = {...ns, assetDeck:cp_res.deck.slice(1)};
      ns = addLog(ns,"🎁 "+pname(ns,pid)+"'s Consolation Prize gains \""+cp_new.name+"\" before bankruptcy!","asset",[cp_new]);
    }
    // Mark this player as having filed for bankruptcy (for end-game bonus check)
    ns = setPl(ns, pid, function(p){ return {...p, _filedBankruptcy:true}; });
  }

  // Re-read updated player after asset gains above
  const pl2 = ns.players.find(p => p.id===pid);
  const sorted = [...pl2.assets].sort((a,b) => b.origVal - a.origVal);
  // Consolation Prize itself is discarded instead of most valuable (if it exists and multiple assets)
  const loseAsset = sorted.length > 1;
  var discAsset = null;
  var kept = sorted;
  if (loseAsset) {
    if (cp_asset) {
      // Discard Consolation Prize instead of the most valuable asset
      discAsset = sorted.find(function(a){ return a.id===cp_asset.id; }) || sorted[0];
      kept = sorted.filter(function(a){ return a.id!==discAsset.id; });
    } else {
      discAsset = sorted[0];
      kept = sorted.slice(1);
    }
  }

  // Reset targeting state on kept assets
  const keptReset = kept.map(function(a) {
    var reset = a.hook === "a_extra_income" ? { ...a, extraIncomeTarget:null, status:ST.USED }
              : a.hook === "a_risk_reward"  ? { ...a, riskRewardTarget:null,  status:ST.USED } :
              a.hook === "a_lets_deal"    ? { ...a, letsDealTarget:null,   letsDealTargetName:null, status:ST.USED } : a;
    return reset.taxedUntilEndOf ? { ...reset, disabled:false, taxedUntilEndOf:null } : reset;
  });
  ns = setPl(ns, pid, p => ({ ...p, money:bonus, feeToken:START_FEE, hand:[], assets:keptReset }));
  if (discAsset) {
    if (discAsset.hook === "a_product_update" && discAsset._appliedTo) ns = revertPUEffect(ns, discAsset);
    if (discAsset._puSourceId) {
      ns.players.forEach(function(p) {
        var pu2 = p.assets.find(function(a){ return a.id===discAsset._puSourceId; });
        if (pu2) ns = setPl(ns, p.id, function(pl){ return {...pl, assets:pl.assets.map(function(a){ return a.id===pu2.id ? {...a,_appliedTo:null} : a; })}; });
      });
    }
    ns = { ...ns, assetDiscard:[...ns.assetDiscard, discAsset] };
  }
  const assetMsg = discAsset
    ? `"${discAsset.name}" discarded${cp_asset && discAsset.id===cp_asset.id ? " (Consolation Prize shields best asset)" : ""}`
    : `kept sole asset (min 1)`;
  ns = addLog(ns, `💥 BANKRUPTCY: ${pl.name} - ${cardCount} cards cleared (+${$(bonus)}), ${assetMsg}. Balance: ${$(bonus)}.`, "danger");
  // Bankruptcy immediately ends the bankrupt player's turn if it's their turn
  if (ns.curIdx !== undefined && ns.players[ns.curIdx]?.id === pid && ns.phase === PH.BP) {
    return finishEndTurn(ns);
  }
  return ns;
}
function checkBankruptcy(st, pid) {
  const p = st.players.find(pl => pl.id===pid);
  return (p && p.money <= BANKRUPT_AT) ? doBankruptcy(st, pid) : st;
}

/* - Draw cards - */
function drawN(st, pid, n, _depth, _triggered) {
  _depth = _depth || 0;
  let ns = { ...st };
  for (let i = 0; i < n; i++) {
    const { deck, disc, reshuffled, empty } = tryReshuffle(ns.mainDeck, ns.mainDiscard);
    if (empty) { ns = addLog(ns, "⚠ Main deck and discard both empty.", "warn"); break; }
    if (reshuffled) {
      ns = addLog(ns, "🔀 Main deck reshuffled - shop refresh!", "sys");
      ns = doShopRefresh(ns);
      ns = { ...ns, mainDeck:deck, mainDiscard:[] };
    }
    const dk = reshuffled ? deck : ns.mainDeck;
    if (!dk.length) break;
    const drawn = dk[0];
    // Delayed Payment interception: face-up card goes to discard, owner gets paid, drawer gets a replacement
    if (drawn.faceUp) {
      var dpOwner = drawn.faceUpOwner;
      ns = { ...ns, mainDeck:dk.slice(1), mainDiscard:[...(reshuffled ? [] : ns.mainDiscard), drawn] };
      ns = addLog(ns, "💰 " + pname(ns,pid) + " draws Delayed Payment - " + pname(ns,dpOwner) + " collects $10k!", "money", [drawn]);
      if (dpOwner) ns = addMoney(ns, dpOwner, 10, "Delayed Payment");
      // Draw a replacement card for the drawer
      ns = drawN(ns, pid, 1, (_depth||0)+1, _triggered);
      continue;
    }
    ns = setPl(ns, pid, p => ({ ...p, hand:[...p.hand, drawn] }));
    ns = { ...ns, mainDeck:dk.slice(1), mainDiscard:reshuffled ? [] : ns.mainDiscard };
    ns = addLog(ns, `🃏 ${pname(ns,pid)} draws "${drawn.name}".`, "draw", [drawn]);
    ns = pushTrigger(ns, { type:T.DRAW_CARD, srcPlayer:pid });
  }
  // Checking Inventory: fire once per triggered draw event (regardless of card count)
  if (_triggered && _depth === 0) ns = checkInventoryDraw(ns, pid);
  // Apply High Valued Goods boost to newly drawn cards
  ns = applyHandBoosts(ns, pid);
  return ns;
}

/* - Shop refresh - */
function doShopRefresh(st) {
  const all = shuf([...st.shop, ...st.assetDeck, ...st.assetDiscard]);
  const newSize = st.shopSize + 1;
  return { ...st, shop:all.slice(0,newSize), assetDeck:all.slice(newSize), assetDiscard:[], shopSize:newSize, shopRefreshAnim:true };
}

/* - Apply passive start-of-turn effects - */
function applyPassives(st, pid) {
  let ns = st;
  const pl = st.players.find(p => p.id===pid);
  if (!pl) return ns;
  for (const a of pl.assets) {
    if (a.sub !== AS.PASSIVE || a.disabled) continue;
    switch (a.hook) {
      case "p_interest": {
        const p2 = ns.players.find(p=>p.id===pid);
        var bb_rate = a.bankBranchAmount || 1;
        var bb_cap = (a.bankBranchCap !== undefined && a.bankBranchCap !== null) ? a.bankBranchCap : 3;
        const interest = Math.min(Math.floor(Math.max(0, p2.money) / (a.bankBranchRate||5)) * bb_rate * (a._franchised ? 2 : 1), bb_cap);
        if (interest > 0) { ns = addMoney(ns, pid, interest, "Bank Branch interest"); ns = addLog(ns, "🏦 " + pname(ns,pid) + "'s Bank Branch: +$" + interest + "k interest (cap: $"+bb_cap+"k)", "asset", [], true); }
        break;
      }
      case "p_sell_p1":    /* handled in sell handler */ break;
      case "p_early_lead": {
        var el_amt = (a.earlyLeadAmount || 1) * (a._franchised ? 2 : 1);
        ns.players.forEach(function(opp) {
          if (opp.id === pid) return;
          ns = addMoney(ns, opp.id, -el_amt, "Early Lead ("+pname(ns,pid)+")");
          // Push LOSE_MONEY so Retainer (e.g. Recession stored) can fire passively
          ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:opp.id, value:el_amt });
          ns = applyPlusInterest(ns, pid, opp.id);
          ns = applyAccountant(ns, opp.id, pid, el_amt);
        });
        ns = addLog(ns, "🏁 "+pname(ns,pid)+"'s Early Lead: all opponents lose $"+el_amt+"k!", "asset", [], true);
        break;
      }
      default: break;
    }
  }
  return ns;
}

/* -
   TRIGGER STACK - pure display + reaction window
   Effects are applied directly by handlers,
   not inside trigger resolution.
- */
function applyRetainerPassive(st, trigger) {
  var ns = st;
  ns.players.forEach(function(owner) {
    owner.assets.forEach(function(a) {
      if (a.hook !== "a_retainer" || a.disabled || !a.lockedCard) return;
      var stored = a.lockedCard;
      if (["business_partners","back_to_basics","minor_loss"].indexOf(stored.hook) >= 0) return;
      if (trigger._preReaction) return;
      if (!isEligibleReaction(stored, trigger, owner, ns.players)) return;
      var tokensLeft = a.tokens ? a.tokens.length : 0;
      if (tokensLeft <= 0) return;
      var amt = a.retainerAmount || 1;
      var newTokens = (a.tokens||[]).slice(1);
      var cardReturns = newTokens.length === 0;
      ns = setPl(ns, owner.id, function(p) {
        return { ...p, assets:p.assets.map(function(b) {
          if (b.id !== a.id) return b;
          return { ...b, tokens:newTokens, lockedCard:cardReturns ? null : b.lockedCard };
        }) };
      });
      if (cardReturns) {
        ns = setPl(ns, owner.id, function(p){ return { ...p, hand:[...p.hand, stored] }; });
        ns = addLog(ns, "💼 "+pname(ns,owner.id)+"'s Retainer: \""+stored.name+"\" returned to hand (uses exhausted).", "asset", [], true);
      }
      var q = ns.retainerQueue || [];
      ns = { ...ns, retainerQueue:[...q, { ownerId:owner.id, amt:amt, storedName:stored.name, usesLeft:cardReturns?0:newTokens.length }] };
    });
  });
  return ns;
}

function pushTrigger(st, data) {
  var curPlId = (st.players && st.curIdx != null) ? (st.players[st.curIdx] ? st.players[st.curIdx].id : null) : null;
  const t = {
    ...data,
    tid: uid(),
    srcPlayer:data.srcPlayer||null, tgtPlayer:data.tgtPlayer||null,
    value:data.value||null, label:data.label||null,
    curPlayerId: curPlId,
    status:"ACTIVE",
  };
  var ns = { ...st, triggerStack:[...st.triggerStack, t] };
  ns = applyRetainerPassive(ns, t);
  return ns;
}
function completeTrigger(st, tid) {
  return { ...st, triggerStack:st.triggerStack.map(t => t.tid===tid ? {...t,status:"DONE"} : t) };
}
function cancelTrigger(st, tid) {
  return { ...st, triggerStack:st.triggerStack.map(t => t.tid===tid ? {...t,status:"CANCELLED"} : t) };
}

// Open a reaction window for the latest active trigger on the stack
function openReactionWindow(st, triggerTid) {
  const trigger = st.triggerStack.find(t => t.tid === triggerTid);
  if (!trigger) return st;
  const reactors = reactorsFor(st.players, trigger);
  if (!reactors.some(r => r.canReact)) return st;
  return {
    ...st,
    reactionWindow: {
      wid:uid(), triggerTid, ttype:trigger.type,
      srcPlayer:trigger.srcPlayer,
      reactors,
      status:"OPEN",
    },
  };
}

// Auto-mark non-reactive players as PASSED; return null if no one left pending
function autoPassNonReactors(reactors) {
  return reactors.map(function(r) {
    return (!r.canReact && r.decision === "PENDING") ? { ...r, decision:"PASSED" } : r;
  });
}
function buildWindow(wid, triggerTid, ttype, srcPlayer, tgtPlayer, reactors) {
  var autoReactors = autoPassNonReactors(reactors);
  return { wid, triggerTid, ttype, srcPlayer, tgtPlayer:tgtPlayer||null, reactors:autoReactors, status:"OPEN" };
}

/* EFFECT APPLICATION */
function applyHook(st, hook, ctx) {
  const { srcPlayer, tgtPlayer, value, srcCard } = ctx;
  const sn = pname(st, srcPlayer);
  const tn = tgtPlayer ? pname(st, tgtPlayer) : null;

  switch (hook) {
    case "gain_2":         return addMoney(st, srcPlayer, 2, srcCard?.name);
    case "gain_3":         return addMoney(st, srcPlayer, 3, srcCard?.name);
    case "draw_1":         return drawN(st, srcPlayer, 1, 0, true);
    case "draw_2":         return drawN(st, srcPlayer, 2, 0, true);
    case "let_go":         return startLetGoTargeting(st, srcPlayer, srcCard);
    case "take_inventory": {
      var ti_pl = st.players.find(function(p){ return p.id === srcPlayer; });
      var ti_count = (ti_pl ? ti_pl.hand.length : 0) + 1; // +1 for this card (in pendingDiscard)
      return addMoney(st, srcPlayer, ti_count, "Take Inventory (" + ti_count + " card" + (ti_count!==1?"s":"") + ")");
    }
    case "overtime": {
      // Gain $3k then grant extra action. Money gain may be intercepted by reactions,
      // but action gain always follows if the card itself wasn't cancelled.
      // rwActionsBonus gates the extra action play in Limited mode.
      // In Free mode we do NOT refund actionsLeft (doing so nets zero cost, making
      // Overtime appear to "reset" the counter without any visible effect).
      var ot_ns = addMoney(st, srcPlayer, 3, "Overtime");
      ot_ns = { ...ot_ns, rwActionsBonus:(ot_ns.rwActionsBonus||0)+1 };
      if (!st.freeActions) {
        // Limited mode: also bump actionsLeft so the bonus slot is immediately available
        ot_ns = { ...ot_ns, actionsLeft:(ot_ns.actionsLeft||0)+1 };
      }
      return addLog(ot_ns, "⏰ " + sn + " earns $3k and gains 1 extra action (Overtime).", "action");
    }
    case "upgrade": {
      // Must own at least one non-ONCE asset
      var up_pl = st.players.find(function(p){ return p.id === srcPlayer; });
      var up_opts = up_pl ? up_pl.assets.filter(function(a){ return a.sub !== AS.ONCE; }) : [];
      if (!up_opts.length) return addLog(st, sn + " has no eligible assets to destroy (Upgrade requires at least one non-one-time asset).", "warn");
      if (!st.shop.length) return addLog(st, sn + ": Shop is empty - Upgrade has nothing to take.", "warn");
      return { ...st, pendingChoice:{ type:"UPGRADE_DISCARD", srcPlayer:srcPlayer,
        timerEnd:Date.now()+20000,
        prompt:sn+": choose one of your assets to destroy.",
        options:up_opts } };
    }
    case "holiday_bonus": {
      var hb_pl = st.players.find(function(p){ return p.id === srcPlayer; });
      var hb_count = hb_pl ? hb_pl.assets.length : 0;
      if (hb_count === 0) return addLog(st, sn + " has no assets - Holiday Bonus pays nothing.", "info");
      return addMoney(st, srcPlayer, hb_count, "Holiday Bonus (" + hb_count + " asset" + (hb_count!==1?"s":"") + ")");
    }
    case "restocked": {
      var ns_rs = doShopRefresh(st);
      ns_rs = pushTrigger(ns_rs, { type:T.SHOP_REFRESH, srcPlayer:srcPlayer });
      ns_rs = addLog(ns_rs, "🔄 " + sn + " plays Restocked - shop refreshed!", "sys");
      ns_rs = addMoney(ns_rs, srcPlayer, 2, "Restocked");
      return ns_rs;
    }
    case "buying_supplies": {
      // srcCard is in pendingDiscard - hand count is BEFORE the card was removed
      // so current hand size is already accurate (card already stripped from hand)
      var bs_pl = st.players.find(function(p){ return p.id === srcPlayer; });
      var bs_current = bs_pl ? bs_pl.hand.length : 0; // card already removed
      var bs_need = Math.max(0, 5 - bs_current);
      if (bs_need === 0) return addLog(st, sn + " already has 5+ cards - Buying Supplies fizzled.", "warn");
      var ns_bs = pushTrigger(st, { type:T.DRAW_CARD, srcPlayer:srcPlayer });
      ns_bs = drawN(ns_bs, srcPlayer, bs_need, 0, true);
      return addLog(ns_bs, "🛒 " + sn + " draws " + bs_need + " card" + (bs_need!==1?"s":"") + " (Buying Supplies - up to 5).", "draw");
    }
    case "action_gain_1": {
      let ns = { ...st, actionsLeft: st.actionsLeft + 1 };
      return addLog(ns, `🎯 ${sn} gains 1 extra action this turn.`, "action");
    }
    case "reduce_fee_2": {
      let ns = setPl(st, srcPlayer, p => ({ ...p, feeToken:Math.max(0, p.feeToken - 2) }));
      return addLog(ns, `📋 ${sn}'s overdraft fee reduced by $2k.`, "info");
    }
    case "ipo": {
      let ns = addMoney(st, srcPlayer, 4, "IPO Launch");
      ns.players.filter(p => p.id !== srcPlayer).forEach(p => { ns = addMoney(ns, p.id, 1, "IPO bonus"); });
      return ns;
    }
    case "negative_reaction": return startNegReactionTargeting(st, srcPlayer);
    case "defective_unit": {
      // st.pendingEffect holds the saved asset hook from handleActivateAsset
      var du_pe = st.pendingEffect;
      if (!du_pe || !du_pe.srcCard) return addLog(st, sn + "'s Defective Unit fires - no asset found.", "warn");
      var du_asset   = du_pe.srcCard;
      var du_ownerId = du_pe.srcPlayer;
      // Mark ONCE assets with defectivelyBlocked so they reset next turn
      // (non-ONCE are already USED from handleActivateAsset; they reset normally at start of turn)
      var ns_du = setPl(st, du_ownerId, function(p) {
        return { ...p, assets:p.assets.map(function(a) {
          if (a.id !== du_asset.id) return a;
          return a.sub === AS.ONCE ? { ...a, defectivelyBlocked:true } : a;
        })};
      });
      ns_du = { ...ns_du, pendingEffect:null }; // Cancel the asset's effect
      ns_du = addLog(ns_du, "❌ " + sn + "'s Defective Unit cancels \"" + du_asset.name + "\"!", "reaction");
      ns_du = checkTargetedAd(ns_du, srcPlayer, du_ownerId);
      ns_du = pushTrigger(ns_du, { type:T.CANCEL_ASSET, srcPlayer:srcPlayer, tgtPlayer:du_ownerId, label:du_asset.name });
      return ns_du;
    }
    case "shrinkage": {      // Additive reaction: tgtPlayer = person who played/sold the card we reacted to
      if (!tgtPlayer) return addLog(st, sn + "'s Shrinkage: no target found.", "warn");
      var sh_tgtPl = st.players.find(function(p){ return p.id === tgtPlayer; });
      if (!sh_tgtPl || !sh_tgtPl.hand.length) {
        return addLog(st, sn + "'s Shrinkage fires - " + pname(st, tgtPlayer) + " has no cards to discard.", "info");
      }
      // Push TARGET_PLAYER trigger - Counteroffer can protect the target
      var ns_sh = pushTrigger(st, { type:T.TARGET_PLAYER, srcPlayer:srcPlayer, tgtPlayer:tgtPlayer, label:"Shrinkage" });
      var sh_trig = ns_sh.triggerStack[ns_sh.triggerStack.length - 1];
      var sh_reactors = reactorsFor(ns_sh.players, sh_trig);
      var sh_rpe = st.pendingEffect && st.pendingEffect.hook==="__resume_action__" ? st.pendingEffect : null;
      var sh_sdw = st.deferredWindow || null;
      ns_sh = { ...ns_sh, shrinkageState:{ phase:"targeting", srcPlayer:srcPlayer, tgtPlayer:tgtPlayer, resumePE:sh_rpe, outerDeferredWindow:sh_sdw } };
      if (sh_reactors.some(function(r){ return r.canReact; })) {
        return { ...ns_sh, reactionWindow:buildWindow(uid(), sh_trig.tid, T.TARGET_PLAYER, srcPlayer, tgtPlayer, sh_reactors) };
      }
      // No reactors - go straight to discard
      return executeShrinkageDiscard(ns_sh);
    }
    case "out_of_office": {      if (!tgtPlayer) return st;
      var ooo_newVal = (st.players.find(function(p){ return p.id === tgtPlayer; })?.turnsToSkip || 0) + 1;
      var ns_ooo = setPl(st, tgtPlayer, function(p) { return { ...p, turnsToSkip:(p.turnsToSkip||0) + 1 }; });
      ns_ooo = addLog(ns_ooo, "⏭ " + pname(ns_ooo, tgtPlayer) + " will skip " + ooo_newVal + " turn" + (ooo_newVal > 1 ? "s" : "") + " (Out of Office).", "action");
      return ns_ooo;
    }
    case "liquidation": {      var liq_count = st.shop.length;
      var liq_ns = liq_count > 0
        ? addMoney(st, srcPlayer, liq_count, "Liquidation (" + liq_count + " shop asset" + (liq_count > 1 ? "s" : "") + " x $1k)")
        : addLog(st, sn + ": Shop is empty - no money gained.", "info");
      liq_ns = doShopRefresh(liq_ns);
      return liq_ns;
    }
    case "outside_hire": {      // Build options: only assets from players with 2+ assets
      var oh_opts = [];
      st.players.forEach(function(p) {
        if (p.id === srcPlayer || p.assets.length < 2) return;
        p.assets.forEach(function(a) { oh_opts.push({ ...a, ownerId:p.id, ownerName:p.name }); });
      });
      if (!oh_opts.length) return addLog(st, sn + ": No eligible assets - Outside Hire fizzled.", "warn");
      return { ...st, pendingChoice:{ type:"OUTSIDE_HIRE_SELECT", srcPlayer,
        timerEnd:Date.now()+20000,
        prompt:sn + ": choose an asset to steal.",
        options:oh_opts } };
    }
    case "outside_hire_after_target": {
      // TARGET_PLAYER resolved for the asset owner - now open LOSE_MONEY window for srcPlayer
      // value = assetValue, srcCard.id = assetId, tgtPlayer = asset owner
      var ohLM_val = value || 0;
      var ohLM_ns = pushTrigger(st, { type:T.LOSE_MONEY, srcPlayer:srcPlayer, tgtPlayer:srcPlayer, value:ohLM_val });
      var ohLM_trig = ohLM_ns.triggerStack[ohLM_ns.triggerStack.length - 1];
      var ohLM_reactors = reactorsFor(ohLM_ns.players, ohLM_trig);
      // Store context for after LOSE_MONEY window resolves
      ohLM_ns = { ...ohLM_ns, pendingEffect:{ hook:"outside_hire_steal", srcPlayer, tgtPlayer, value:ohLM_val, srcCard } };
      if (ohLM_reactors.some(function(r) { return r.canReact; })) {
        return { ...ohLM_ns, reactionWindow:buildWindow(uid(), ohLM_trig.tid, T.LOSE_MONEY, srcPlayer, srcPlayer, ohLM_reactors) };
      }
      // No reactors - apply loss and steal immediately
      ohLM_ns = { ...ohLM_ns, pendingEffect:null };
      ohLM_ns = addMoney(ohLM_ns, srcPlayer, -ohLM_val, "Outside Hire (asset cost)");
      return maybeFlush(applyHook(ohLM_ns, "outside_hire_steal", { srcPlayer, tgtPlayer, value:ohLM_val, srcCard }));
    }
    case "outside_hire_steal": {
      // tgtPlayer = previous asset owner, srcCard.id = assetId, value = assetValue
      if (!srcCard || !tgtPlayer) return st;
      var ohSteal_asset = null;
      st.players.forEach(function(p) {
        var found = p.assets.find(function(a) { return a.id === srcCard.id; });
        if (found) ohSteal_asset = found;
      });
      if (!ohSteal_asset) return addLog(st, sn + ": Asset not found - may have already moved.", "warn");
      var ns_oh = transferAsset(st, tgtPlayer, srcPlayer, ohSteal_asset.id);
      ns_oh = addLog(ns_oh, "🏢 " + sn + " steals \"" + ohSteal_asset.name + "\" from " + pname(st, tgtPlayer) + "!", "asset", [ohSteal_asset]);
      ns_oh = pushTrigger(ns_oh, { type:T.STEAL_ASSET, srcPlayer, tgtPlayer, value:ohSteal_asset.origVal });
      return ns_oh;
    }
    case "give_back": {
      var gb_maxMoney = Math.max.apply(null, st.players.map(function(p){ return p.money; }));
      var gb_richest = st.players.filter(function(p){ return p.id !== srcPlayer && p.money === gb_maxMoney; });
      if (!gb_richest.length) return addLog(st, sn + ": No valid target for Give Back.", "warn");
      if (gb_richest.length === 1) {
        // Single richest - auto-target, no popup needed
        var gb_auto = gb_richest[0];
        var gb_ns = pushTrigger(st, { type:T.TARGET_PLAYER, srcPlayer:srcPlayer, tgtPlayer:gb_auto.id, label:"Give Back" });
        var gb_trig = gb_ns.triggerStack[gb_ns.triggerStack.length-1];
        var gb_reactors = reactorsFor(gb_ns.players, gb_trig);
        gb_ns = { ...gb_ns, pendingEffect:{ hook:"give_back_pay", srcPlayer:srcPlayer, tgtPlayer:gb_auto.id, value:4, srcCard:srcCard } };
        if (gb_reactors.some(function(r){ return r.canReact; })) {
          return { ...gb_ns, reactionWindow:buildWindow(uid(), gb_trig.tid, T.TARGET_PLAYER, srcPlayer, gb_auto.id, gb_reactors) };
        }
        gb_ns = { ...gb_ns, pendingEffect:null };
        return maybeFlush(applyHook(gb_ns, "give_back_pay", { srcPlayer:srcPlayer, tgtPlayer:gb_auto.id, value:4, srcCard:srcCard }));
      }
      // Tie - show selection popup
      return { ...st, pendingChoice:{ type:"GIVE_BACK_TIE", srcPlayer:srcPlayer, srcCard:srcCard,
        timerEnd:Date.now()+20000,
        prompt:sn+": tie for richest! Choose which player pays you $4k.",
        options:gb_richest } };
    }
    case "give_back_pay": {
      // tgtPlayer pays srcPlayer $4k
      if (!tgtPlayer) return st;
      var ns_gb = payMoney(st, tgtPlayer, srcPlayer, 4, "Give Back");
      return addLog(ns_gb, "💸 " + pname(ns_gb,tgtPlayer) + " pays " + sn + " $4k (Give Back).", "money");
    }
    case "refresh_asset": {
      var rf_pl = st.players.find(function(p){ return p.id === srcPlayer; });
      var rf_opts = rf_pl ? rf_pl.assets.filter(function(a){ return a.status===ST.USED && a.sub!==AS.ONCE && !a.disabled; }) : [];
      if (!rf_opts.length) return addLog(st, sn + " has no used assets to refresh.", "warn");
      return { ...st, pendingChoice:{ type:"REFRESH_ASSET", srcPlayer:srcPlayer,
        timerEnd:Date.now()+20000,
        prompt:sn+": choose a used asset to reset to READY.",
        options:rf_opts } };
    }
    case "full_price": {
      // srcCard.value is the current (possibly boosted) value at time of play
      var fp_val = srcCard ? srcCard.value * 2 : 4;
      return addMoney(st, srcPlayer, fp_val, "Full Price (" + fp_val + "k = " + (srcCard?srcCard.value:2) + "k x2)");
    }
    case "cost_of_business": {
      let ns = addMoney(st, srcPlayer, 5, "Cost of Business");
      const pl = ns.players.find(p => p.id===srcPlayer);
      if (pl.hand.length > 0) {
        ns = { ...ns, pendingChoice:{
          type:"DISCARD_SELF", srcPlayer, prompt:`${sn}: choose a card to discard.`,
          options: pl.hand,
        }};
      }
      return ns;
    }
    case "target_discard_1": {
      if (!tgtPlayer) return st;
      const tp = st.players.find(p => p.id===tgtPlayer);
      if (!tp || !tp.hand.length) return addLog(st, `${tn} has no cards.`, "info");
      return { ...st, pendingChoice:{
        type:"OPPONENT_DISCARD", srcPlayer, tgtPlayer,
        prompt:`${tn}: choose a card to discard.`, options:tp.hand,
      }};
    }
    case "client_theft": {
      if (!tgtPlayer) return st;
      const tp = st.players.find(p => p.id===tgtPlayer);
      if (!tp || !tp.hand.length) return addLog(st, `${tn} has no cards.`, "info");
      return { ...st, pendingChoice:{
        type:"CLIENT_THEFT", srcPlayer, tgtPlayer,
        prompt:`${tn}: choose a card to discard. You lose its value.`,
        options: tp.hand,
      }};
    }
    case "robbed": {
      if (!tgtPlayer) return st;
      var rb_tp = st.players.find(function(p){ return p.id === tgtPlayer; });
      if (!rb_tp || !rb_tp.hand.length) return addLog(st, tn + " has no cards to steal.", "info");
      // Take up to 2 random cards
      var rb_shuffled = shuf([...rb_tp.hand]);
      var rb_taken = rb_shuffled.slice(0, Math.min(2, rb_shuffled.length));
      var rb_remaining = rb_shuffled.slice(rb_taken.length);
      var rb_ns = setPl(st, tgtPlayer, function(p){ return { ...p, hand:rb_remaining }; });
      rb_ns = setPl(rb_ns, srcPlayer, function(p){ return { ...p, hand:[...p.hand, ...rb_taken] }; });
      rb_ns = pushTrigger(rb_ns, { type:T.STEAL_CARD, srcPlayer:srcPlayer, tgtPlayer:tgtPlayer });
      var rb_names = rb_taken.map(function(c){ return "\""+c.name+"\""; }).join(" and ");
      return addLog(rb_ns, "🦹 " + sn + " steals " + rb_taken.length + " card" + (rb_taken.length!==1?"s":"") + " from " + tn + " (" + rb_names + ").", "action", rb_taken);
    }
    case "out_of_order": {
      // Collect all non-disabled assets from opponents
      var ooo_opts = [];
      st.players.forEach(function(p) {
        if (p.id === srcPlayer) return;
        p.assets.forEach(function(a) {
          if (!a.disabled) ooo_opts.push({ ...a, _ownerId:p.id, _ownerName:p.name });
        });
      });
      if (!ooo_opts.length) return addLog(st, sn + ": No opponent assets available to disable.", "warn");
      // Two-phase modal - start on opponent selection
      var ooo_opponents = st.players.filter(function(p) {
        return p.id !== srcPlayer && p.assets.some(function(a) { return !a.disabled; });
      });
      return { ...st, pendingChoice:{
        type:"OUT_OF_ORDER_SELECT", srcPlayer,
        timerEnd:Date.now()+25000,
        phase:"opponent",
        opponents:ooo_opponents.map(function(p){ return { id:p.id, name:p.name }; }),
        assets:[],
        selectedOpponent:null,
      }};
    }
    case "apply_out_of_order": {
      // value = assetId, tgtPlayer = asset owner, srcPlayer = card owner (payee)
      if (!value || !tgtPlayer) return st;
      var ooo_assetId = value;
      var ooo_payAmt  = 5; // base $5k - future modifiers hook here via PAY_MONEY trigger
      var ooo_ns = setPl(st, tgtPlayer, function(p){
        return { ...p, assets:p.assets.map(function(a){
          return a.id === ooo_assetId
            ? { ...a, disabled:true, outOfOrderBy:srcPlayer, outOfOrderPayAmt:ooo_payAmt }
            : a;
        })};
      });
      var ooo_asset = st.players.find(function(p){return p.id===tgtPlayer;})
                        ?.assets.find(function(a){return a.id===ooo_assetId;});
      // Track the effect globally so it survives card discard
      var ooo_effect = {
        id:uid(), assetId:ooo_assetId, assetName:ooo_asset?ooo_asset.name:"?",
        ownerId:tgtPlayer, ownerName:pname(st,tgtPlayer),
        payeeId:srcPlayer, payeeName:pname(st,srcPlayer),
        payAmt:ooo_payAmt,
      };
      ooo_ns = { ...ooo_ns, outOfOrderEffects:[...(ooo_ns.outOfOrderEffects||[]), ooo_effect] };
      ooo_ns = pushTrigger(ooo_ns, { type:T.DISABLE_ASSET, srcPlayer, tgtPlayer, label:ooo_asset?ooo_asset.name:"?" });
      ooo_ns = addLog(ooo_ns, "Out of Order: " + sn + " disables " + pname(ooo_ns,tgtPlayer) + " asset " + (ooo_asset?ooo_asset.name:"?") + " - pay $"+(ooo_payAmt||5)+"k to re-enable it.", "action");

      return ooo_ns;
    }
    case "steal_card": {
      if (!tgtPlayer) return st;
      const tp = st.players.find(p => p.id===tgtPlayer);
      if (!tp || !tp.hand.length) return addLog(st, `${tn} has no cards to steal.`, "info");
      return { ...st, pendingChoice:{
        type:"STEAL_CARD", srcPlayer, tgtPlayer,
        prompt:`${sn}: choose a card to steal from ${tn}.`, options:tp.hand,
      }};
    }
    case "help_wanted": {
      if (!tgtPlayer) return st;
      var hwTp = st.players.find(function(p){ return p.id===tgtPlayer; });
      if (!hwTp || !hwTp.hand.length) return addLog(st, tn+" has no cards to give.", "info");
      var hw_tgtIdx = st.players.findIndex(function(p){ return p.id===tgtPlayer; });
      return { ...st, viewIdx:hw_tgtIdx >= 0 ? hw_tgtIdx : st.viewIdx, pendingChoice:{
        type:"HELP_WANTED_GIVE", srcPlayer:srcPlayer, tgtPlayer:tgtPlayer,
        timerEnd:Date.now()+20000,
        prompt:tn+" must choose a card to hand over to "+sn+".",
        options:hwTp.hand,
      }};
    }
    case "part_time_work": {
      if (!tgtPlayer) return st;
      var ns_ptw = setPl(st, tgtPlayer, function(p) { return { ...p, actionLocked:true }; });
      ns_ptw = pushTrigger(ns_ptw, { type:"DISABLE_ACTION", srcPlayer:srcPlayer, tgtPlayer:tgtPlayer });
      return addLog(ns_ptw, "🚫 " + tn + " cannot play Action Cards on their next turn (Part Time Work).", "action");
    }
    case "inspect_hand": {
      if (!tgtPlayer) return st;
      const tp = st.players.find(p => p.id===tgtPlayer);
      return { ...st, pendingChoice:{
        type:"INSPECT", srcPlayer, tgtPlayer, readOnly:true,
        prompt:`${sn} is viewing ${tn}'s hand.`, options:tp?.hand||[],
      }};
    }
    case "half_price_buy":
      return { ...st, pendingChoice:{
        type:"HALF_PRICE_BUY", srcPlayer,
        prompt:`${sn}: choose a Shop asset to buy at half price.`, options:st.shop,
      }};
    case "double_sell": {
      const pl = st.players.find(p => p.id===srcPlayer);
      const opts = pl.hand.filter(c => c.type !== CT.ASSET);
      if (!opts.length) return addLog(st, `${sn} has no cards to sell.`, "warn");
      return { ...st, pendingChoice:{ type:"DOUBLE_SELL", srcPlayer, prompt:`${sn}: choose a card to sell at double value.`, options:opts }};
    }
    case "fresh_supplies": {
      const pl = st.players.find(p => p.id===srcPlayer);
      const disc = [...pl.hand];
      let ns = setPl(st, srcPlayer, p => ({ ...p, hand:[] }));
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, ...disc] };
      ns = addLog(ns, `🗑 ${sn} discards entire hand (${disc.length} cards).`, "discard", disc);
      return drawN(ns, srcPlayer, 3, 0, true);
    }
    case "cancel_whole_action": // Ignored - actual cancel handled in ENG_REACT; hook is a no-op
      return addLog(st, "🚫 " + sn + " ignores the action - cancelled!", "reaction");
    case "cancel_reaction":     // Fired - actual cancel handled in ENG_REACT; hook is a no-op
      return addLog(st, "🚫 " + sn + " fires back - reaction cancelled!", "reaction");
    case "__removed_discard_reaction__": {
      // Whistleblower: force reactor to discard an extra reaction card from hand
      var wbSrc = st.pendingReactionCard ? st.pendingReactionCard.srcPlayer : tgtPlayer;
      var wbPl = st.players.find(function(p){ return p.id === wbSrc; });
      var wbReactions = wbPl ? wbPl.hand.filter(function(c){ return c.type === CT.REACTION; }) : [];
      if (!wbReactions.length) return addLog(st, sn + "'s Whistleblower fires - " + pname(st,wbSrc) + " has no reactions to discard.", "info");
      var wbCard = shuf([...wbReactions])[0];
      var ns_wb = setPl(st, wbSrc, function(p){ return { ...p, hand:p.hand.filter(function(c){ return c.id !== wbCard.id; }) }; });
      ns_wb = { ...ns_wb, mainDiscard:[...ns_wb.mainDiscard, wbCard] };
      return addLog(ns_wb, "🔔 " + sn + "'s Whistleblower forces " + pname(ns_wb,wbSrc) + " to discard \"" + wbCard.name + "\".", "reaction", [wbCard]);
    }
    case "prevent_loss": {
      // Applied when reacting to a LOSE_MONEY trigger - cancel the trigger
      return addLog(st, `🛡 ${sn} negates money loss.`, "reaction");
    }
    // Asset hooks
          return { ...st, pendingChoice:{ type:"DISCOUNT_BUY", srcPlayer, prompt:`${sn}: choose a Shop asset to buy at $1k discount.`, options:st.shop }};
        case "a_pharma": {
      var ph_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var ph_a = ph_pl && ph_pl.assets.find(function(a){ return a.hook==="a_pharma"; });
      if (!ph_a) return st;
      var ph_tmax = ph_a.tmax || 4;
      // If already at tmax tokens: cash out and reset
      if ((ph_a.tokens||[]).length >= ph_tmax) {
        var ph_ns = setPl(st, srcPlayer, function(p){ return {...p, assets:p.assets.map(function(a){
          return a.hook==="a_pharma" ? {...a, tokens:[]} : a;
        })}; });
        ph_ns = addLog(ph_ns, "💰 "+sn+" activates 4 Day Work Week - gains $10k!", "asset");
        return addMoney(ph_ns, srcPlayer, 10, "4 Day Work Week");
      }
      // Otherwise add a token
      var ph_newToks = [...(ph_a.tokens||[]), {id:uid()}];
      var ph_ns2 = setPl(st, srcPlayer, function(p){ return {...p, assets:p.assets.map(function(a){
        return a.hook==="a_pharma" ? {...a, tokens:ph_newToks} : a;
      })}; });
      return addLog(ph_ns2, "🏢 "+sn+" adds work token ("+ph_newToks.length+"/"+ph_tmax+").", "asset");
    }
        case "__removed_crypto__": {
      const pl = st.players.find(p => p.id===srcPlayer);
      return addMoney(st, srcPlayer, pl.hand.length * 2, "Crypto Exchange");
    }
    case "__removed_peek__": {
      const top3 = st.mainDeck.slice(0, 3);
      return { ...st, pendingChoice:{ type:"PEEK", srcPlayer, readOnly:true, prompt:`${sn} peeks at the top 3 Main Deck cards.`, options:top3 }};
    }
    case "interviews": {
      // Pull up to 3 from the top, handling reshuffle
      var { deck:ivDeck, reshuffled:ivReshuffle, empty:ivEmpty } = tryReshuffle(st.mainDeck, st.mainDiscard);
      var ns_iv = st;
      if (ivEmpty) return addLog(st, sn + ": deck empty - Interviews fizzled.", "warn");
      if (ivReshuffle) {
        ns_iv = addLog(ns_iv, "🔀 Main deck reshuffled - shop refresh!", "sys");
        ns_iv = doShopRefresh(ns_iv);
        ns_iv = { ...ns_iv, mainDeck:ivDeck, mainDiscard:[] };
      }
      var ivTop3 = (ivReshuffle ? ivDeck : ns_iv.mainDeck).slice(0, 3);
      if (!ivTop3.length) return addLog(ns_iv, sn + ": deck empty - Interviews fizzled.", "warn");
      // Remove the top 3 from the deck temporarily - placed back in resolveChoice
      ns_iv = { ...ns_iv, mainDeck:(ivReshuffle ? ivDeck : ns_iv.mainDeck).slice(ivTop3.length) };
      ns_iv = pushTrigger(ns_iv, { type:T.DRAW_CARD, srcPlayer:srcPlayer });
      ns_iv = addLog(ns_iv, "🎤 " + sn + " uses Interviews - pick 1 of " + ivTop3.length + " cards.", "draw");
      return { ...ns_iv, pendingChoice:{ type:"INTERVIEWS_PICK", srcPlayer,
        prompt: sn + ": pick 1 card to keep. The others go to the bottom of the deck.",
        options: ivTop3 } };
    }
    case "dumpster_diving": {
      // Look at top of mainDiscard (played card is still in pendingDiscard at this point)
      if (!st.mainDiscard.length) return addLog(st, sn + " can't dive - discard pile is empty.", "warn");
      var topCard = st.mainDiscard[st.mainDiscard.length - 1];
      return addMoney(st, srcPlayer, topCard.origVal, "Dumpster Diving (" + topCard.name + ")");
    }
    case "quick_buck": {
      // Take top card of mainDeck, discard it, gain its origVal
      var { deck:qbDeck, disc:qbDisc, reshuffled:qbReshuffle, empty:qbEmpty } = tryReshuffle(st.mainDeck, st.mainDiscard);
      if (qbEmpty) return addLog(st, sn + ": Main Deck is empty - A Quick Buck fizzled.", "warn");
      if (qbReshuffle) {
        st = addLog(st, "🔀 Main deck reshuffled - shop refresh!", "sys");
        st = doShopRefresh(st);
        st = { ...st, mainDeck:qbDeck, mainDiscard:[] };
      }
      var qbTop = (qbReshuffle ? qbDeck : st.mainDeck)[0];
      var ns = { ...st, mainDeck:(qbReshuffle ? qbDeck : st.mainDeck).slice(1), mainDiscard:[...(qbReshuffle ? [] : st.mainDiscard), qbTop] };
      ns = addLog(ns, "💸 " + sn + " discards \"" + qbTop.name + "\" from the deck (worth $" + qbTop.origVal + "k).", "discard", [qbTop]);
      ns = pushTrigger(ns, { type:T.DISCARD_CARD, srcPlayer:srcPlayer });
      ns = addMoney(ns, srcPlayer, qbTop.origVal, "A Quick Buck (" + qbTop.name + ")");
      // Show the discarded card briefly - reuse animation system with a no-op resumeAction
      return { ...ns, playAnimation:{ card:{ ...qbTop, _displayOnly:true }, resumeAction:{ type:"ANIMATION_DONE_NOOP" } } };
    }
    case "last_minute_bid": {
      // Card is still in pendingEffect (trigger fired before money/discard)
      var lmb_pe = st.pendingEffect;
      if (!lmb_pe || !lmb_pe.soldCard) return addLog(st, sn + "'s Last Minute Bid: no card in limbo (fizzled).", "warn");
      var lmb_card   = lmb_pe.soldCard;
      var lmb_seller = lmb_pe.srcPlayer;
      var lmb_val    = lmb_pe.saleVal || lmb_card.value;
      // Card goes to discard (not to reactor's hand); seller loses the card's value
      var ns_lmb = { ...st, mainDiscard:[...st.mainDiscard, { ...lmb_card, value:lmb_card.origVal||lmb_card.value, modifiedBy:[] }] };
      ns_lmb = addLog(ns_lmb, '📉 ' + sn + ' plays Last Minute Bid! ' + pname(ns_lmb, lmb_seller) + ' loses $' + lmb_val + 'k instead of gaining it.', 'reaction', [lmb_card]);
      ns_lmb = addMoney(ns_lmb, lmb_seller, -lmb_val, "Last Minute Bid");
      ns_lmb = applyPlusInterest(ns_lmb, srcPlayer, lmb_seller);
      ns_lmb = applyAccountant(ns_lmb, lmb_seller, srcPlayer, lmb_val);
      // Push LOSE_MONEY so Cover the Costs / Risky Investment can react
      ns_lmb = pushTrigger(ns_lmb, { type:T.LOSE_MONEY, srcPlayer:lmb_seller, value:lmb_val });
      ns_lmb = { ...ns_lmb, pendingEffect:null };
      if (!ns_lmb.reactionWindow && !ns_lmb._skipLoseMoneyWindow) {
        var lmb_trig = ns_lmb.triggerStack[ns_lmb.triggerStack.length-1];
        var lmb_reactors = reactorsFor(ns_lmb.players, lmb_trig);
        if (lmb_reactors.some(function(r){ return r.canReact; })) {
          ns_lmb = { ...ns_lmb, pendingEffect:{ hook:"__lose_money__", srcPlayer:lmb_seller, tgtPlayer:null, value:lmb_val, reason:"Last Minute Bid" } };
          return { ...ns_lmb, reactionWindow:buildWindow(uid(), lmb_trig.tid, T.LOSE_MONEY, lmb_seller, lmb_seller, lmb_reactors) };
        }
      }
      return ns_lmb;
    }
    case "not_a_chance":   return st; // handled entirely in applyAndClearPendingReaction
    case "property_theft": {
      // Handles both the direct path (resumeReactionPlayed no sub-reactors)
      // and falls through from ACPR (which also calls applyHook via this path)
      var pt2_pe = st.pendingEffect;
      var pt2_name = pname(st, srcPlayer);
      if (!pt2_pe || pt2_pe.hook !== "__resume_action__") return st;
      var pt2_opps = st.players.filter(function(p){ return p.id !== srcPlayer; });
      var pt2_newPe = { ...pt2_pe, srcPlayer:srcPlayer,
        tgtPlayer: pt2_pe.srcCard && pt2_pe.srcCard.target === "opponent" && pt2_opps.length
          ? pt2_opps[Math.floor(_rand()*pt2_opps.length)].id : null
      };
      var pt2_ns = { ...st, pendingEffect:null };
      pt2_ns = checkTargetedAd(pt2_ns, srcPlayer, tgtPlayer);
      pt2_ns = addLog(pt2_ns, "🔀 "+pt2_name+"'s Property Theft steals the action!", "reaction");
      pt2_ns = addLog(pt2_ns, "▶ "+pt2_name+" activates \""+pt2_pe.srcCard.name+"\"!", "action", [pt2_pe.srcCard]);
      return continueAfterActionPlayed(pt2_ns, pt2_newPe);
    }
    case "p_leave_tip":    return st; // handled passively inside addMoney
    case "p_tax":           return st; // handled passively in applyValueTaxPassives
    case "p_couple_bucks":  return st; // handled passively in applyValueTaxPassives
    case "p_restock":       return st; // handled passively in CAAP
    case "p_empty_handed":  return st; // handled in finishEndTurn
    case "p_price_adj":     return st; // purely display-time cost inflation in calcShopCost
    case "p_high_valued":   return st; // purely computed via applyHandBoosts at flush time
    case "pay_day": {
      var ns_pd = addMoney(st, srcPlayer, 1, "Pay Day");
      return drawN(ns_pd, srcPlayer, 1, 0, true);
    }
    case "expense_report": {
      var er_ns = st;
      st.players.forEach(function(p) {
        if (p.id === srcPlayer) return;
        // Respect Loss Prevention — reduces actual loss if victim has the asset
        var er_lp = calcLossAfterPrevention(er_ns, p.id, 1);
        var er_loss = er_lp.reducedAmount;
        if (er_lp.saved > 0) {
          er_ns = addLog(er_ns, "🛡 "+pname(er_ns,p.id)+"'s Loss Prevention reduces Expense Report — saves $"+er_lp.saved+"k.", "asset", [], true);
        }
        if (er_loss > 0) {
          er_ns = addMoney(er_ns, p.id, -er_loss, "Expense Report", false, false, false, true);
          // Record the loss event so passive listeners (Scammer, future hooks) fire correctly
          er_ns = pushTrigger(er_ns, { type:T.LOSE_MONEY, srcPlayer:srcPlayer, tgtPlayer:p.id, value:er_loss });
          // Plus Interest: if ER player owns it, each victim loses an extra $1k
          er_ns = applyPlusInterest(er_ns, srcPlayer, p.id);
        }
        // Accountant retaliation: if victim has Accountant, ER player takes a penalty
        er_ns = applyAccountant(er_ns, p.id, srcPlayer, 1);
      });
      return drawN(er_ns, srcPlayer, 1, 0, true);
    }
    case "delayed_payment": {
      var dpCard = { ...(srcCard || {}), faceUp:true, faceUpOwner:srcPlayer };
      var deck_dp = [...st.mainDeck];
      var insertAt = Math.floor(_rand() * (deck_dp.length + 1));
      deck_dp.splice(insertAt, 0, dpCard);
      // Remove from pendingDiscard - card goes straight into the deck, not the discard pile
      var pd_dp = (st.pendingDiscard || []).filter(function(c) { return c.id !== (srcCard && srcCard.id); });
      var ns_dp = { ...st, mainDeck: deck_dp, pendingDiscard: pd_dp };
      return addLog(ns_dp, "🂠 " + sn + " shuffles Delayed Payment into the deck (position " + (insertAt+1) + " of " + deck_dp.length + ").", "action");
    }
    case "water_damage": {
      var wd_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      if (!wd_pl || !wd_pl.hand.length)
        return addLog(st, sn + " has no cards to discard (Water Damage fizzled).", "warn");
      return { ...st, pendingChoice:{
        type:"WATER_DAMAGE_DISCARD", srcPlayer,
        timerEnd:Date.now()+20000,
        prompt:sn+": choose a card to discard — all opponents lose its value.",
        options:wd_pl.hand,
      }};
    }
    case "water_damage_hit": {
      if (!tgtPlayer) return st;
      var wdh_ns = addMoney(st, tgtPlayer, -(value||0), "Water Damage");
      wdh_ns = applyPlusInterest(wdh_ns, srcPlayer, tgtPlayer);
      wdh_ns = applyAccountant(wdh_ns, tgtPlayer, srcPlayer, value||0);
      // Push LOSE_MONEY so Cover the Costs / Risky Investment can react
      wdh_ns = pushTrigger(wdh_ns, { type:T.LOSE_MONEY, srcPlayer:tgtPlayer, value:(value||0) });
      if (!wdh_ns.reactionWindow && !wdh_ns._skipLoseMoneyWindow) {
        var wdh_trig = wdh_ns.triggerStack[wdh_ns.triggerStack.length-1];
        var wdh_reactors = reactorsFor(wdh_ns.players, wdh_trig);
        if (wdh_reactors.some(function(r){ return r.canReact; })) {
          wdh_ns = { ...wdh_ns, pendingEffect:{ hook:"__lose_money__", srcPlayer:tgtPlayer, tgtPlayer:null, value:(value||0), reason:"Water Damage" } };
          return { ...wdh_ns, reactionWindow:buildWindow(uid(), wdh_trig.tid, T.LOSE_MONEY, tgtPlayer, tgtPlayer, wdh_reactors) };
        }
      }
      return wdh_ns;
    }
    case "used_goods": {
      // mainDiscard top is safe: Used Goods itself is in pendingDiscard, not mainDiscard
      if (!st.mainDiscard.length) return addLog(st, sn + ": Discard pile is empty - Used Goods fizzled.", "warn");
      var ugCard = st.mainDiscard[st.mainDiscard.length - 1];
      var ns_ug = { ...st, mainDiscard: st.mainDiscard.slice(0, -1) };
      ns_ug = setPl(ns_ug, srcPlayer, function(p) { return { ...p, hand:[...p.hand, ugCard] }; });
      return addLog(ns_ug, "🛍 " + sn + " picks up \"" + ugCard.name + "\" from the discard pile.", "draw", [ugCard]);
    }
    case "taxes": {
      // Collect all assets owned by opponents
      var taxOpts = [];
      st.players.forEach(function(p) {
        if (p.id === srcPlayer) return;
        p.assets.forEach(function(a) {
          if (!a.disabled) taxOpts.push({ ...a, _ownerId: p.id, _ownerName: p.name });
        });
      });
      if (!taxOpts.length) return addLog(st, sn + ": No opponent assets to tax.", "warn");
      return { ...st, pendingChoice:{ type:"TAXES_SELECT_ASSET", srcPlayer,
        timerEnd: Date.now() + 20000,
        prompt: sn + ": choose an opponent's asset to disable.",
        options: taxOpts }};
    }
    case "apply_bribery": {
      // value = target assetId, tgtPlayer = asset owner
      var apb_assetId = value;
      var apb_tgtPl = st.players.find(function(p){ return p.id===tgtPlayer; });
      var apb_asset = apb_tgtPl && apb_tgtPl.assets.find(function(a){ return a.id===apb_assetId; });
      if (!apb_tgtPl || !apb_asset) return addLog(st, sn+": Bribery target lost.", "warn");
      var apb_cardIds = srcCard.bribeCardIds || [];
      var apb_assetOwnerId = srcCard.assetId; // the Bribery asset itself
      // Discard the selected cards
      var apb_ns = st;
      var apb_srcPl = apb_ns.players.find(function(p){ return p.id===srcPlayer; });
      var apb_cards = apb_srcPl ? apb_cardIds.map(function(id){ return apb_srcPl.hand.find(function(c){ return c.id===id; }); }).filter(Boolean) : [];
      if (apb_cards.length) {
        var apb_idSet = new Set(apb_cardIds);
        apb_ns = setPl(apb_ns, srcPlayer, function(p){ return {...p, hand:p.hand.filter(function(c){ return !apb_idSet.has(c.id); })}; });
        apb_cards.forEach(function(c){ apb_ns={...apb_ns,mainDiscard:[...apb_ns.mainDiscard,{...c,value:c.origVal,modifiedBy:[]}]}; });
        var apb_total = apb_cards.reduce(function(s,c){ return s+(c.value||0); }, 0);
        apb_ns = addLog(apb_ns, "💸 "+sn+" discards $"+apb_total+"k in cards for Bribery.", "discard");
      }
      // Steal the asset
      apb_ns = transferAsset(apb_ns, tgtPlayer, srcPlayer, apb_assetId);
      apb_ns = addLog(apb_ns, "🤑 "+sn+" bribes \""+apb_asset.name+"\" away from "+pname(apb_ns,tgtPlayer)+"!", "asset", [apb_asset]);
      apb_ns = pushTrigger(apb_ns, { type:T.STEAL_ASSET, srcPlayer, tgtPlayer, value:apb_asset.origVal });
      // Add a token to the Bribery asset
      if (apb_assetOwnerId) {
        apb_ns = setPl(apb_ns, srcPlayer, function(p){ return {...p, assets:p.assets.map(function(a){
          return a.id===apb_assetOwnerId ? {...a, tokens:[...(a.tokens||[]),{id:uid()}]} : a;
        })}; });
      }
      return apb_ns;
    }
    case "apply_cfr": {
      // value = assetId, tgtPlayer = owner, srcPlayer = CFR owner
      var cfr_aid = value;
      var cfr_tgt = st.players.find(function(p){ return p.id===tgtPlayer; });
      var cfr_a = cfr_tgt && cfr_tgt.assets.find(function(a){ return a.id===cfr_aid; });
      if (!cfr_tgt || !cfr_a) return addLog(st, sn+": CFR target not found.", "warn");
      var cfr_ns = setPl(st, tgtPlayer, function(p){ return { ...p, assets:p.assets.map(function(a){
        return a.id===cfr_aid ? {...a, disabled:true, _cfrBy:pname(st,srcPlayer), _cfrUntilTurnOf:srcPlayer } : a;
      }) }; });
      // Record in cfrHistory: block this asset for the CFR owner's very next turn.
      // nextOwnerTurnNum = current global turnNum + number of players (one full cycle).
      var cfr_hist = [...(cfr_ns.cfrHistory||[]), { assetId:cfr_aid, cfrOwnerId:srcPlayer, turnNum:cfr_ns.turnNum, nextOwnerTurnNum:cfr_ns.turnNum+cfr_ns.players.length }];
      cfr_ns = {...cfr_ns, cfrHistory:cfr_hist};
      cfr_ns = applyAllHandBoosts(cfr_ns);
      return addLog(cfr_ns, "🔒 "+sn+"'s Closed for Remodeling: \""+cfr_a.name+"\" deactivated until their next turn!", "action");
    }
    case "apply_tax": {
      // value = assetId to disable, tgtPlayer = owner
      var taxAid = value;
      var ns_tx = setPl(st, tgtPlayer, function(p) {
        return { ...p, assets: p.assets.map(function(a) {
          return a.id === taxAid ? { ...a, disabled:true, taxedUntilEndOf:tgtPlayer, _taxedBy:pname(st,srcPlayer) } : a;
        })};
      });
      var taxedAsset = st.players.find(function(p){return p.id===tgtPlayer;})?.assets.find(function(a){return a.id===taxAid;});
      ns_tx = applyAllHandBoosts(ns_tx);
      return addLog(ns_tx, "🚫 " + pname(ns_tx,srcPlayer) + " taxes " + pname(ns_tx,tgtPlayer)
        + "'s \"" + (taxedAsset?taxedAsset.name:"asset") + "\" (disabled until end of their next turn).", "action");
    }
    case "kickstarter":   return startKickstarter(st, srcPlayer);
    case "pocket_change": return startPocketChangeTargeting(st, srcPlayer);
    case "a_risk_reward": {
      var rvrOpts = st.players.filter(function(p) { return p.id !== srcPlayer; });
      if (!rvrOpts.length) return addLog(st, sn + ": No opponents to target.", "warn");
      var curRVR = null;
      var plRVR = st.players.find(function(p) { return p.id === srcPlayer; });
      if (plRVR) { var aRVR = plRVR.assets.find(function(a) { return a.id === (srcCard && srcCard.id); }); if (aRVR) curRVR = aRVR.riskRewardTarget; }
      var curRVRName = curRVR ? (" Current: " + pname(st, curRVR) + ".") : "";
      return { ...st, pendingChoice:{ type:"RISK_REWARD_SET_TARGET", srcPlayer:srcPlayer, assetId:(srcCard && srcCard.id),
        prompt:sn + ": choose an opponent to monitor with Risk vs Reward." + curRVRName,
        options:rvrOpts }};
    }
    case "rvr_set_target": {
      var rvrAid = value;
      var rvrTgtName = pname(st, tgtPlayer);
      var nsRVR = setPl(st, srcPlayer, function(p) {
        return { ...p, assets:p.assets.map(function(a) { return a.id === rvrAid ? { ...a, riskRewardTarget:tgtPlayer, riskRewardTargetName:rvrTgtName } : a; }) };
      });
      return addLog(nsRVR, "🎯 " + sn + "'s Risk vs Reward now monitoring " + rvrTgtName + ".", "asset");
    }
    case "a_lets_deal": {
      var ldOpts = st.players.filter(function(p){ return p.id !== srcPlayer; });
      if (!ldOpts.length) return addLog(st, sn+": No opponents to target.", "warn");
      var plLD = st.players.find(function(p){ return p.id===srcPlayer; });
      var curLD = plLD && plLD.assets.find(function(a){ return a.id===(srcCard&&srcCard.id); });
      var curLDName = curLD && curLD.letsDealTarget ? (" Current: "+pname(st,curLD.letsDealTarget)+".") : "";
      return { ...st, pendingChoice:{ type:"LETS_DEAL_SET_TARGET", srcPlayer, assetId:(srcCard&&srcCard.id),
        prompt:sn+": choose an opponent to monitor with Let's Make a Deal."+curLDName, options:ldOpts }};
    }
    case "a_extra_income": {
      var eiOpts = st.players.filter(function(p) { return p.id !== srcPlayer; });
      if (!eiOpts.length) return addLog(st, sn + ": No opponents to target.", "warn");
      var curEI = null;
      var plEI = st.players.find(function(p) { return p.id === srcPlayer; });
      if (plEI) { var aEI = plEI.assets.find(function(a) { return a.id === (srcCard && srcCard.id); }); if (aEI) curEI = aEI.extraIncomeTarget; }
      var curEIName = curEI ? (" Current: " + pname(st, curEI) + ".") : "";
      return { ...st, pendingChoice:{ type:"EXTRA_INCOME_SET_TARGET", srcPlayer:srcPlayer, assetId:(srcCard && srcCard.id),
        prompt:sn + ": choose an opponent to monitor with Extra Income." + curEIName,
        options:eiOpts }};
    }
    case "ld_set_target": {
      var ldAid = value;
      var ldTgtName = pname(st, tgtPlayer);
      var nsLD = setPl(st, srcPlayer, function(p){
        return { ...p, assets:p.assets.map(function(a){ return a.id===ldAid ? { ...a, letsDealTarget:tgtPlayer, letsDealTargetName:ldTgtName } : a; }) };
      });
      return addLog(nsLD, "🤝 "+sn+"'s Let's Make a Deal now monitoring "+ldTgtName+".", "asset");
    }
    case "ei_set_target": {
      var eiAid = value;
      var nsEI = setPl(st, srcPlayer, function(p) {
        return { ...p, assets:p.assets.map(function(a) { return a.id === eiAid ? { ...a, extraIncomeTarget:tgtPlayer } : a; }) };
      });
      return addLog(nsEI, "🎯 " + sn + "'s Extra Income now monitoring " + pname(nsEI, tgtPlayer) + ".", "asset");
    }    // - Asset hooks -
    case "p_restock":       return st; // handled passively in CAAP
    case "a_product_update": {
      var pu_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var pu_opts = pu_pl ? pu_pl.assets.filter(function(a){
        return a.id !== (srcCard&&srcCard.id) && PU_FIELDS[a.hook] && PU_FIELDS[a.hook].length > 0;
      }) : [];
      if (!pu_opts.length) return addLog(st, sn+": No adjustable assets.", "warn");
      var pu_amount = (srcCard && srcCard.puAmount) || 1;
      return { ...st, pendingChoice:{ type:"PU_ASSET_SELECT", srcPlayer,
        puAssetId:srcCard ? srcCard.id : null, puAmount:pu_amount,
        phase:"asset", timerEnd:Date.now()+30000, options:pu_opts }};
    }
    case "a_rebranding": {
      var rb_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      // Other assets the owner has (excluding Rebranding itself)
      var rb_opts = rb_pl ? rb_pl.assets.filter(function(a){ return a.id !== (srcCard&&srcCard.id); }) : [];
      var rb_bonus = (srcCard && srcCard.rebrandBonus) || 1;
      return { ...st, pendingChoice:{ type:"REBRANDING_SELECT", srcPlayer,
        rebrandAssetId:srcCard ? srcCard.id : null, rebrandBonus:rb_bonus,
        timerEnd:Date.now()+20000, options:rb_opts, selected:[] }};
    }
    case "a_mark_up": {
      var mu_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var mu_hand = mu_pl ? mu_pl.hand.filter(function(c){ return c.type !== CT.ASSET; }) : [];
      if (!mu_hand.length) return addLog(st, sn+": No cards in hand to modify.", "warn");
      var mu_amt = (srcCard && srcCard.markUpAmount) || 1;
      return { ...st, pendingChoice:{ type:"MARKUP_DIRECTION", srcPlayer, assetId:srcCard?srcCard.id:null,
        markUpAmount:mu_amt, timerEnd:Date.now()+5000, options:[] }};
    }
    case "a_bribery": {
      var brb_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var brb_hand = brb_pl ? brb_pl.hand.filter(function(c){ return c.type!==CT.ASSET; }) : [];
      var brb_myMax = brb_hand.reduce(function(s,c){ return s+(c.value||0); }, 0);
      var brb_asset_ref = srcCard;
      var brb_tokens = brb_asset_ref ? (brb_asset_ref.tokens||[]).length : 0;
      var brb_cost = (brb_asset_ref && brb_asset_ref.bribeTokenCost) || 1;
      // Build list of stealable assets: opponents with 2+ assets, and hand total can exceed threshold
      var brb_opts = [];
      st.players.forEach(function(opp){
        if (opp.id===srcPlayer || opp.assets.length<2) return;
        opp.assets.forEach(function(a){
          var threshold = a.origVal + brb_tokens * brb_cost;
          if (brb_myMax > threshold) {
            brb_opts.push({ ...a, _ownerId:opp.id, _ownerName:opp.name, _threshold:threshold });
          }
        });
      });
      if (!brb_opts.length) return addLog(st, sn+": No eligible assets.", "warn");
      return { ...st, pendingChoice:{ type:"BRIBERY_ASSET_SELECT", srcPlayer,
        assetId:brb_asset_ref ? brb_asset_ref.id : null,
        bribeTokenCost:brb_cost, currentTokens:brb_tokens,
        timerEnd:Date.now()+20000, options:brb_opts }};
    }
    case "a_cfr": {
      var cfr_opps = st.players.filter(function(p){
        return p.id !== srcPlayer && p.assets.some(function(a){ return !a.disabled; });
      });
      if (!cfr_opps.length) return addLog(st, sn+": No valid targets.", "warn");
      return { ...st, pendingChoice:{ type:"CFR_SELECT", srcPlayer, assetId:srcCard?srcCard.id:null,
        phase:"opponent", timerEnd:Date.now()+20000,
        opponents:cfr_opps.map(function(p){ return {id:p.id,name:p.name}; }),
        options:cfr_opps, assets:[], selectedOpponent:null }};
    }
    case "a_multi_tool": {
      var mt_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var mt_opts = mt_pl ? mt_pl.assets.filter(function(a){
        return a.id !== (srcCard && srcCard.id) && a.status === ST.USED && !a.disabled && a.hook !== "a_multi_tool";
      }) : [];
      if (!mt_opts.length) return addLog(st, sn+": No used assets to reset.", "warn");
      return { ...st, pendingChoice:{ type:"MULTI_TOOL_SELECT", srcPlayer,
        timerEnd:Date.now()+20000,
        prompt:sn+": choose a used asset to reset to READY.",
        options:mt_opts }};
    }
    case "a_three_of_kind": {
      var tok_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var tok_need2 = (srcCard && srcCard.tripleCount) || 3;
      var tok_val_req2 = (srcCard && srcCard.tripleValue) || 2;
      var tok_hand2 = tok_pl ? tok_pl.hand : [];
      var tok_matching2 = tok_hand2.filter(function(c){ return c.value === tok_val_req2; });
      if (tok_matching2.length < tok_need2) return addLog(st, sn+": Need "+tok_need2+" cards worth $"+tok_val_req2+"k in hand.", "warn");
      return { ...st, pendingChoice:{ type:"THREE_OF_KIND_SELECT", srcPlayer, assetId:srcCard?srcCard.id:null,
        timerEnd:Date.now()+20000, tripleCount:tok_need2, tripleValue:tok_val_req2,
        prompt:sn+": discard "+tok_need2+" cards worth $"+tok_val_req2+"k to gain a free asset.", options:tok_hand2 }};
    }
    case "a_sneak_peak": {
      var sp_deck = st.mainDeck || [];
      if (sp_deck.length === 0) return addLog(st, sn+": Main deck is empty.", "warn");
      var sp_top = sp_deck.slice(0, 2); // top 1 or 2 cards
      return { ...st, pendingChoice:{ type:"SNEAK_PEAK_SELECT", srcPlayer, timerEnd:Date.now()+20000, cards:sp_top }};
    }
    case "a_extra_shift": {
      var es_ns = { ...st, actionsLeft:(st.actionsLeft||1)+1 };
      return addLog(es_ns, "⏩ "+sn+" gains 1 extra action (Extra Shift).", "action");
    }
    case "a_revenue_stream": {
      var rs_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var rs_asset = rs_pl && srcCard && rs_pl.assets.find(function(a){ return a.id===srcCard.id; });
      if (!rs_asset) return addLog(st, sn+": Revenue Stream not found.", "warn");
      if ((rs_asset.tokens||[]).length < (rs_asset.tmax||3)) return addLog(st, "❌ Revenue Stream needs "+(rs_asset.tmax||3)+" tokens to activate.", "warn");
      var rs_gain = rs_asset.revenueAmount || 5;
      var rs_ns = setPl(st, srcPlayer, function(p){ return { ...p, assets:p.assets.map(function(a){ return a.id===srcCard.id ? { ...a, tokens:[] } : a; }) }; });
      return addMoney(rs_ns, srcPlayer, rs_gain, "Revenue Stream (cashed out)");
    }
    case "a_severance_pay": {
      var sp_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var sp_asset = sp_pl && srcCard && sp_pl.assets.find(function(a){ return a.id===srcCard.id; });
      if (!sp_asset) return addLog(st, sn+": Severance Pay not found.", "warn");
      if ((sp_asset.tokens||[]).length < (sp_asset.tmax||10))
        return addLog(st, "❌ Severance Pay needs "+(sp_asset.tmax||10)+" tokens to activate (have "+((sp_asset.tokens||[]).length)+").", "warn");
      var sp_ns = setPl(st, srcPlayer, function(p){ return { ...p, assets:p.assets.map(function(a){
        return a.id===sp_asset.id ? { ...a, tokens:[], status:"READY" } : a;
      }) }; });
      sp_ns = addLog(sp_ns, "⏰ "+sn+" activates Severance Pay - earns a free extra turn!", "asset");
      return { ...sp_ns, _extraTurn:true, phase:PH.SOT, startTurnPending:true, actionsLeft:2, actionsTaken:[], hasBoughtAsset:false };
    }
    case "a_blueprint": {
      var bph_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var bph_actions = bph_pl ? bph_pl.hand.filter(function(c){ return c.type==="ACTION"; }) : [];
      if (!bph_actions.length) return addLog(st, sn+": No action cards in hand to store on Blueprint.", "warn");
      return { ...st, pendingChoice:{ type:"BLUEPRINT_PLACE", srcPlayer, assetId:srcCard?srcCard.id:null, timerEnd:Date.now()+20000, options:bph_actions }};
    }
    case "a_retainer": {
      var ret_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var ret_eligible = ret_pl ? ret_pl.hand.filter(function(c){ return c.type==="REACTION" && ["business_partners","back_to_basics","minor_loss"].indexOf(c.hook)<0; }) : [];
      if (!ret_eligible.length) return addLog(st, sn+": No eligible reaction cards to store.", "warn");
      return { ...st, pendingChoice:{ type:"RETAINER_PLACE", srcPlayer, assetId:srcCard?srcCard.id:null, timerEnd:Date.now()+20000, options:ret_eligible }};
    }
    case "a_prospects": return addLog(st, sn+"'s Prospects is counting down - tokens removed at turn start.", "asset");
    case "a_trade_in": {
      var ti_owner = st.players.find(function(p){ return p.id===srcPlayer; });
      var ti_actions = ti_owner ? ti_owner.hand.filter(function(c){ return c.type===CT.ACTION; }) : [];
      var ti_reactions = ti_owner ? ti_owner.hand.filter(function(c){ return c.type===CT.REACTION; }) : [];
      if (!ti_actions.length || !ti_reactions.length) return addLog(st, sn+": Trade-In Value needs at least 1 action and 1 reaction card.", "warn");
      var ti_asset = ti_owner && srcCard && ti_owner.assets.find(function(a){ return a.id===srcCard.id; });
      return { ...st, pendingChoice:{ type:"TRADE_IN_SELECT", srcPlayer, assetId:srcCard?srcCard.id:null, tradeInAmount:ti_asset?(ti_asset.tradeInAmount||6):6, timerEnd:Date.now()+20000, actionOptions:ti_actions, reactionOptions:ti_reactions }};
    }
    case "a_credit_line": {
      var cl_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var cl_asset = cl_pl && srcCard && cl_pl.assets.find(function(a){ return a.id===srcCard.id; });
      if (!cl_asset) return addLog(st, sn+": Credit Line not found.", "warn");
      var cl_cur = cl_asset.tokens ? cl_asset.tokens.length : 0;
      if (cl_cur===0) return addLog(st, "❌ Credit Line has no tokens - wait until your next turn.", "warn");
      return { ...st, pendingChoice:{ type:"CREDIT_LINE_CHOICE", srcPlayer, assetId:srcCard.id, timerEnd:Date.now()+10000, currentTokens:cl_cur, clValue:cl_asset.clValue||2, clPrice1:cl_asset.clPrice1||1, clPrice2:cl_asset.clPrice2||3, clPrice3:cl_asset.clPrice3||6 }};
    }
    case "a_rnd_budget": {
      var rnd_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var rnd_asset = rnd_pl && srcCard && rnd_pl.assets.find(function(a){ return a.id===srcCard.id; });
      if (!rnd_asset) return addLog(st, sn+": R&D Budget not found.", "warn");
      if ((rnd_asset.tokens||[]).length < (rnd_asset.tmax||3)) return addLog(st, "❌ R&D Budget needs "+(rnd_asset.tmax||3)+" tokens.", "warn");
      return { ...st, pendingChoice:{ type:"RND_ACTIVATE", srcPlayer, assetId:srcCard.id, timerEnd:Date.now()+10000, drawCount:rnd_asset.rndDrawCount||2 }};
    }
    case "a_monopoly": {
      var mono_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      if (!mono_pl || !mono_pl.hand.length) return addLog(st, sn+": No cards in hand.", "warn");
      return { ...st, pendingChoice:{ type:"MONOPOLY_SELECT", srcPlayer, assetId:srcCard?srcCard.id:null, timerEnd:Date.now()+20000, options:mono_pl.hand }};
    }
    case "a_bogo": {
      var bogo_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var bogo_others = bogo_pl ? bogo_pl.assets.filter(function(a){ return a.id!==(srcCard&&srcCard.id); }) : [];
      if (!bogo_others.length) return addLog(st, sn+": No other assets to copy.", "warn");
      return { ...st, pendingChoice:{ type:"BOGO_SELECT", srcPlayer, bogoAssetId:srcCard?srcCard.id:null, timerEnd:Date.now()+20000, options:bogo_others }};
    }
    case "a_franchise": {
      var fran_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var fran_opts = fran_pl ? fran_pl.assets.filter(function(a){ return a.sub===AS.PASSIVE && a.id!==(srcCard&&srcCard.id) && !a._franchised && !a._franchiseExempt; }) : [];
      if (!fran_opts.length) return addLog(st, sn+": No un-franchised passive assets.", "warn");
      return { ...st, pendingChoice:{ type:"FRANCHISE_SELECT", srcPlayer, franAssetId:srcCard?srcCard.id:null, timerEnd:Date.now()+20000, options:fran_opts }};
    }
    case "a_exchange": {
      if (!st.shop.length) return addLog(st, sn+": Shop is empty.", "warn");
      return { ...st, pendingChoice:{ type:"EXCHANGE_SELECT", srcPlayer, thisAssetId:srcCard?srcCard.id:null, timerEnd:Date.now()+20000, options:st.shop }};
    }
    case "a_eye_for_eye": {
      var eye_self = st.players.find(function(p){ return p.id===srcPlayer; });
      if (!eye_self || eye_self.assets.length <= 1) return addLog(st, sn+": Eye for an Eye requires you to own more than 1 asset.", "warn");
      var eye_opps = st.players.filter(function(p){ return p.id!==srcPlayer && p.assets.length>1; });
      if (!eye_opps.length) return addLog(st, sn+": No eligible targets.", "warn");
      return { ...st, pendingChoice:{ type:"EYE_SELECT", srcPlayer, phase:"opponent", opponents:eye_opps.map(function(p){ return {id:p.id,name:p.name,assetCount:p.assets.length}; }), assets:[], selectedOpponent:null, thisAssetId:srcCard?srcCard.id:null, timerEnd:Date.now()+25000 }};
    }
    case "apply_eye_for_eye": {
      if (!tgtPlayer||!value) return addLog(st, sn+": Eye For An Eye - missing target.", "warn");
      var eye_ownerPl = st.players.find(function(p){ return p.id===tgtPlayer; });
      var eye_asset = eye_ownerPl && eye_ownerPl.assets.find(function(a){ return a.id===value; });
      if (!eye_asset) return addLog(st, sn+": Asset not found.", "warn");
      var ns_eye = setPl(st, tgtPlayer, function(p){ return { ...p, assets:p.assets.filter(function(a){ return a.id!==value; }) }; });
      ns_eye = { ...ns_eye, assetDiscard:[...ns_eye.assetDiscard, { ...eye_asset, status:ST.READY, tokens:[], disabled:false }] };
      ns_eye = addLog(ns_eye, "🗑 "+sn+" discards "+pname(ns_eye,tgtPlayer)+"'s \""+eye_asset.name+"\". (Eye For An Eye)", "reaction", [eye_asset]);
      ns_eye = drawN(ns_eye, srcPlayer, 1, 0, true);
      if (srcCard) {
        ns_eye = setPl(ns_eye, srcPlayer, function(p){ return { ...p, assets:p.assets.filter(function(a){ return a.id!==srcCard.id; }) }; });
        ns_eye = { ...ns_eye, assetDiscard:[...ns_eye.assetDiscard, { ...srcCard, status:ST.READY, tokens:[], disabled:false }] };
      }
      return applyAllHandBoosts(ns_eye);
    }
    case "a_action_plan": {
      var ap_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var ap_asset = ap_pl && srcCard && ap_pl.assets.find(function(a){ return a.id===srcCard.id; });
      if (!ap_asset) return addLog(st, sn+": Action Plan not found.", "warn");
      var ap_hand_opts = ap_pl ? ap_pl.hand.filter(function(c){ return c.type === "ACTION"; }) : [];
      return { ...st, pendingChoice:{ type:"AP_ACTIVATE_SELECT", srcPlayer, assetId:srcCard.id,
        timerEnd:Date.now()+30000,
        currentTokens:(ap_asset.tokens||[]).length, maxTokens:ap_asset.tmax||5,
        lockedCard:ap_asset.lockedCard||null,
        canAdd:(ap_asset.tokens||[]).length < (ap_asset.tmax||5),
        canFire:!!(ap_asset.lockedCard && (ap_asset.tokens||[]).length >= (ap_asset.lockedCard.value||0)),
        canPlace:ap_hand_opts.length > 0,
        handOptions:ap_hand_opts } };    }
    case "a_coming_soon": {
      var cs_res = tryReshuffle(st.assetDeck, st.assetDiscard);
      if (cs_res.empty) return addLog(st, sn+": Asset deck empty.", "warn");
      return { ...st, pendingChoice:{ type:"COMING_SOON_PEEK", srcPlayer, card:cs_res.deck[0], timerEnd:Date.now()+10000 }};
    }
    case "a_worth_the_price": {
      var wtp_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var wtp_rw = st.reactionWindow;
      var wtp_trig = wtp_rw && st.triggerStack.find(function(t){ return t.tid===wtp_rw.triggerTid; });
      // Card value: prefer trigger.value (now set on ACTION_PLAYED/REACTION_PLAYED triggers),
      // then pendingReactionCard (reaction card being countered), then pendingEffect.
      var wtp_pe = st.pendingEffect;
      var wtp_prc = st.pendingReactionCard;
      var wtp_cardVal = (wtp_trig && wtp_trig.value) ||
                        (wtp_prc && (wtp_prc.value || (wtp_prc.srcCard && wtp_prc.srcCard.value))) ||
                        (wtp_pe && wtp_pe.hook !== "__resume_action__" && (wtp_pe.value || (wtp_pe.srcCard && wtp_pe.srcCard.value))) ||
                        (wtp_pe && wtp_pe.srcCard && wtp_pe.srcCard.value) || 0;
      return { ...st, reactionWindow:null, pendingChoice:{ type:"WTP_SELECT", srcPlayer, assetId:srcCard?srcCard.id:null, timerEnd:Date.now()+20000, trigVal:wtp_cardVal, ttype:wtp_rw?wtp_rw.ttype:null, options:wtp_pl?wtp_pl.hand:[], selected:[], savedWindow:wtp_rw }};
    }
    case "a_risk_reward": {
      var rvrOpts = st.players.filter(function(p){ return p.id!==srcPlayer; });
      if (!rvrOpts.length) return addLog(st, sn+": No opponents.", "warn");
      return { ...st, pendingChoice:{ type:"RISK_REWARD_SET_TARGET", srcPlayer, assetId:srcCard&&srcCard.id, prompt:sn+": choose an opponent for Risk vs Reward.", options:rvrOpts }};
    }
    case "rvr_set_target": {
      var nsRVR = setPl(st, srcPlayer, function(p){ return { ...p, assets:p.assets.map(function(a){ return a.id===value?{...a,riskRewardTarget:tgtPlayer,riskRewardTargetName:pname(st,tgtPlayer)}:a; }) }; });
      return addLog(nsRVR, "🎯 "+sn+"'s Risk vs Reward now monitoring "+pname(nsRVR,tgtPlayer)+".", "asset");
    }
    case "a_extra_income": {
      var eiOpts = st.players.filter(function(p){ return p.id!==srcPlayer; });
      if (!eiOpts.length) return addLog(st, sn+": No opponents.", "warn");
      return { ...st, pendingChoice:{ type:"EXTRA_INCOME_SET_TARGET", srcPlayer, assetId:srcCard&&srcCard.id, prompt:sn+": choose an opponent for Extra Income.", options:eiOpts }};
    }
    case "ei_set_target": {
      var nsEI = setPl(st, srcPlayer, function(p){ return { ...p, assets:p.assets.map(function(a){ return a.id===value?{...a,extraIncomeTarget:tgtPlayer}:a; }) }; });
      return addLog(nsEI, "🎯 "+sn+"'s Extra Income now monitoring "+pname(nsEI,tgtPlayer)+".", "asset");
    }
    case "swear_jar_pay": {
      if (!tgtPlayer||!value) return st;
      return payMoney(st, tgtPlayer, srcPlayer, value, "Swear Jar");
    }
    case "total_loss": {
      return { ...st, pendingChoice:{ type:"TOTAL_LOSS_PICK_VALUE", srcPlayer, timerEnd:Date.now()+20000, prompt:sn+": choose a value 1-5." }};
    }
    case "quick_exchange": {
      var qe_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      var qe_others = qe_pl ? qe_pl.hand.filter(function(c){ return c.id!==srcCard.id; }) : [];
      var qe_vMap = {}; qe_others.forEach(function(c){ qe_vMap[c.value]=(qe_vMap[c.value]||[]).concat(c.id); });
      var qe_validVals = Object.keys(qe_vMap).filter(function(v){ return qe_vMap[v].length>=2; }).map(Number);
      if (!qe_validVals.length) return addLog(st, sn+": No matching-value pairs.", "warn");
      return { ...st, pendingChoice:{ type:"QUICK_EXCHANGE_DISCARD", srcPlayer, timerEnd:Date.now()+20000, prompt:sn+": choose 2 cards of equal value to discard.", options:qe_others, validValues:qe_validVals }};
    }
    case "a_double_shift": {
      var ns_ds = setPl(st, srcPlayer, function(p){ return { ...p, _doublePassive:true }; });
      return addLog(ns_ds, "⚡ "+sn+"'s passive assets are doubled for the rest of this turn!", "action");
    }
    case "market_research": {
      var mr_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      return { ...st, pendingChoice:{ type:"MR_SHOW_CARD", srcPlayer, timerEnd:Date.now()+20000, prompt:sn+": choose a card to show.", options:mr_pl?mr_pl.hand:[] }};
    }
    case "valuation": {
      if (!st.shop.length) return addLog(st, sn+": Shop is empty.", "warn");
      return { ...st, pendingChoice:{ type:"VALUATION_PICK", srcPlayer, timerEnd:Date.now()+20000, prompt:sn+": choose a shop asset to discard and gain its value.", options:st.shop }};
    }
    case "small_fee": {
      var sf_opps = st.players.filter(function(p){ return p.id!==srcPlayer && p.assets.length>1; });
      if (!sf_opps.length) return addLog(st, sn+": No eligible targets.", "warn");
      return { ...st, pendingChoice:{ type:"SF_SELECT", srcPlayer, phase:"opponent", opponents:sf_opps.map(function(p){ return {id:p.id,name:p.name}; }), assets:[], selectedOpponent:null, timerEnd:Date.now()+25000 }};
    }
    case "apply_small_fee": {
      if (!tgtPlayer||!value) return st;
      var sf_ownerPl = st.players.find(function(p){ return p.id===tgtPlayer; });
      var sf_asset = sf_ownerPl && sf_ownerPl.assets.find(function(a){ return a.id===value; });
      if (!sf_asset) return addLog(st, sn+": Asset not found.", "warn");
      var ns_sf = setPl(st, tgtPlayer, function(p){ return { ...p, assets:p.assets.filter(function(a){ return a.id!==value; }) }; });
      ns_sf = setPl(ns_sf, srcPlayer, function(p){ return { ...p, assets:[...p.assets, {...sf_asset, status:ST.READY, tokens:[], disabled:false}] }; });
      return applyAllHandBoosts(addLog(ns_sf, "🏦 "+sn+" steals \""+sf_asset.name+"\" from "+pname(ns_sf,tgtPlayer)+"! (A Small Fee)", "asset", [sf_asset]));
    }
    case "budget_cuts": {
      if (!tgtPlayer) return st;
      var bc_pl = st.players.find(function(p){ return p.id===tgtPlayer; });
      var bc_count = bc_pl ? bc_pl.assets.length : 0;
      if (bc_count===0) return addLog(st, pname(st,tgtPlayer)+" owns no assets.", "info");
      return payMoney(st, tgtPlayer, srcPlayer, bc_count, "Budget Cuts ("+bc_count+" asset"+(bc_count!==1?"s":"")+" x $1k)");
    }
    case "shut_down": {
      var sd_opps = st.players.filter(function(p){ return p.id!==srcPlayer; });
      var ns_sd = { ...st };
      sd_opps.forEach(function(opp) {
        ns_sd = setPl(ns_sd, opp.id, function(p){ return { ...p, assets:p.assets.map(function(a){ return { ...a, disabled:true, shutDownBy:srcPlayer }; }) }; });
      });
      var names = sd_opps.map(function(p){ return pname(ns_sd,p.id); }).join(", ");
      return addLog(ns_sd, "🔴 Shut Down! "+names+" - all assets disabled until "+sn+"'s next turn.", "action");
    }
    case "downsizing": {
      var dsz_pe = st.pendingEffect;
      if (!dsz_pe||!dsz_pe.srcCard) return addLog(st, sn+"'s Downsizing fires - no asset found.", "warn");
      var ns_dsz = setPl(st, dsz_pe.srcPlayer, function(p){ return { ...p, assets:p.assets.map(function(a){ return a.id===dsz_pe.srcCard.id?{...a,disabled:true,downsizingBy:srcPlayer,downsizedUntilEndOf:dsz_pe.srcPlayer}:a; }) }; });
      ns_dsz = { ...ns_dsz, pendingEffect:null };
      ns_dsz = addLog(ns_dsz, "⛔ "+sn+"'s Downsizing cancels \""+dsz_pe.srcCard.name+"\"!", "reaction");
      ns_dsz = checkTargetedAd(ns_dsz, srcPlayer, dsz_pe.srcPlayer);
      return ns_dsz;
    }
    case "close_of_business": {
      // Cancel current effect, flush pending discard, then run end-of-turn for the target player
      var cob_pid = tgtPlayer || curPl(st).id;
      var cob_ns = { ...st,
        reactionWindow:null, savedReactionWindow:null, pendingEffect:null,
        pendingReactionCard:null, shrinkageState:null, deferredWindow:null,
        letGoState:null, pocketChangeState:null, negReactionState:null,
        kickstarterState:null, pendingChoice:null, phase:PH.BP,
      };
      cob_ns = flushPendingDiscard(cob_ns);
      cob_ns = addLog(cob_ns, "🚪 "+sn+"'s Close of Business: "+pname(cob_ns,cob_pid)+"'s turn ends immediately!", "reaction");
      return handleEndTurn(cob_ns);
    }
    case "back_to_basics": {
      // tgtPlayer = the player who triggered the reaction (set by resumeReactionPlayed via reactionWindow.srcPlayer)
      var btb_target = st.players.find(function(p){ return p.id===tgtPlayer && p.assets.length>=3; });
      if (!btb_target) return addLog(st, sn+": Back to Basics - target doesn't have 3+ assets.", "warn");
      var btb_rw = st.reactionWindow;
      // Skip the opponent-selection phase — target is fixed to whoever triggered the reaction
      return { ...st, pendingChoice:{ type:"BTB_SELECT", srcPlayer, phase:"asset", selectedOpponent:tgtPlayer,
        opponents:[], assets:btb_target.assets, timerEnd:Date.now()+25000,
        _outerRwType:btb_rw ? btb_rw.ttype : null, _letGoState:st.letGoState||null,
        _pcState:st.pocketChangeState||null, _ksState:st.kickstarterState||null, _negState:st.negReactionState||null
      }};
    }
    case "apply_btb": {
      if (!tgtPlayer||!value) return addLog(st, sn+": Back to Basics - missing target.", "warn");
      var abtb_ownerPl = st.players.find(function(p){ return p.id===tgtPlayer; });
      var abtb_asset = abtb_ownerPl && abtb_ownerPl.assets.find(function(a){ return a.id===value; });
      if (!abtb_asset) return addLog(st, sn+": Asset not found.", "warn");
      var ns_abtb = setPl(st, tgtPlayer, function(p){ return { ...p, assets:p.assets.filter(function(a){ return a.id!==value; }) }; });
      ns_abtb = { ...ns_abtb, assetDiscard:[...ns_abtb.assetDiscard, { ...abtb_asset, status:ST.READY, tokens:[], disabled:false }] };
      return applyAllHandBoosts(addLog(ns_abtb, "🗑 "+sn+" discards "+pname(ns_abtb,tgtPlayer)+"'s \""+abtb_asset.name+"\" (Back to Basics)!", "reaction", [abtb_asset]));
    }
    case "equity": {
      // Draw the NEXT card from deck (buyer already got the top card via handleBuyDeck/doBuyShop)
      var eq_res = tryReshuffle(st.assetDeck, st.assetDiscard);
      if (eq_res.empty) return addLog(st, sn+": Asset deck empty (Equity).", "warn");
      if (eq_res.reshuffled) { var ns_eq_r = addLog(st, "🔀 Asset deck reshuffled.", "sys"); }
      var eq_asset = eq_res.deck[0];
      var ns_eq = { ...(eq_res.reshuffled ? ns_eq_r : st), assetDeck:eq_res.deck.slice(1), assetDiscard:eq_res.reshuffled?[]:eq_res.disc };
      ns_eq = setPl(ns_eq, srcPlayer, function(p){ return { ...p, assets:[...p.assets, {...eq_asset, status:ST.READY,
        tokens:eq_asset.hook==="a_credit_line"?Array.from({length:eq_asset.tmax||3},function(){return{id:uid()};}):[],
        disabled:false}] }; });
      return applyAllHandBoosts(addLog(ns_eq, "💰 "+sn+" draws \""+eq_asset.name+"\" (Equity)!", "asset", [eq_asset]));
    }
    case "inflation": {
      var inf_pe = st.pendingEffect;
      if (inf_pe && inf_pe.hook === "__pending_buy__") {
        // Used during a shop buy (legacy path)
        return { ...st, pendingEffect:{ ...inf_pe, cost:inf_pe.cost+6 } };
      }
      // Used in a BUY_ASSET reaction window - apply surcharge to the buyer's money directly
      var inf_rw = st.reactionWindow;
      var inf_buyerId = inf_rw ? inf_rw.srcPlayer : tgtPlayer;
      if (!inf_buyerId) return addLog(st, sn+": Inflation - no valid target.", "warn");
      var inf_surcharge = 6;
      var ns_inf = addMoney(st, inf_buyerId, -inf_surcharge, "Inflation ("+sn+")");
      ns_inf = addLog(ns_inf, "💸 "+sn+"'s Inflation adds $"+inf_surcharge+"k cost to "+pname(ns_inf,inf_buyerId)+"'s purchase!", "loss");
      ns_inf = applyPlusInterest(ns_inf, srcPlayer, inf_buyerId);
      ns_inf = applyAccountant(ns_inf, inf_buyerId, srcPlayer, inf_surcharge);
      // Push LOSE_MONEY so Cover the Costs / Risky Investment can react
      ns_inf = pushTrigger(ns_inf, { type:T.LOSE_MONEY, srcPlayer:inf_buyerId, value:inf_surcharge });
      if (!ns_inf.reactionWindow && !ns_inf._skipLoseMoneyWindow) {
        var inf_trig = ns_inf.triggerStack[ns_inf.triggerStack.length-1];
        var inf_reactors = reactorsFor(ns_inf.players, inf_trig);
        if (inf_reactors.some(function(r){ return r.canReact; })) {
          ns_inf = { ...ns_inf, pendingEffect:{ hook:"__lose_money__", srcPlayer:inf_buyerId, tgtPlayer:null, value:inf_surcharge, reason:"Inflation" } };
          return { ...ns_inf, reactionWindow:buildWindow(uid(), inf_trig.tid, T.LOSE_MONEY, inf_buyerId, inf_buyerId, inf_reactors) };
        }
      }
      return ns_inf;
    }
    case "competitor": {
      var comp_cost = st.pendingEffect && (st.pendingEffect.hook==="__pending_buy__"||st.pendingEffect.hook==="__buy_asset__")
        ? st.pendingEffect.cost : (value||0);
      if (!comp_cost) return addLog(st, sn+": Competitor - no purchase amount.", "warn");
      return addMoney(st, srcPlayer, comp_cost, "Competitor (opponent spent $"+comp_cost+"k)");
    }
    case "scammed": {
      var sc_pe = st.pendingEffect;
      var sc_amount = sc_pe&&sc_pe.hook==="__scammed__" ? sc_pe.value : (value||0);
      var sc_fromPid = sc_pe&&sc_pe.hook==="__scammed__" ? sc_pe.tgtPlayer : tgtPlayer;
      if (!sc_amount||!sc_fromPid) return addLog(st, sn+": Scammed - no gain found.", "warn");
      var sc_ns = { ...st, pendingEffect:null };
      sc_ns = setPl(sc_ns, sc_fromPid, function(p){ return { ...p, money:p.money-sc_amount }; });
      sc_ns = addLog(sc_ns, pname(sc_ns,sc_fromPid)+" loses $"+sc_amount+"k (Scammed by "+sn+").", "loss");
      return addMoney(sc_ns, srcPlayer, sc_amount, "Scammed", false, false);
    }
    case "recession": {
      var rec_pe = st.pendingEffect;
      var rec_amount = rec_pe&&rec_pe.value ? rec_pe.value : (value||0);
      if (rec_amount<=0) return addLog(st, sn+": Recession - no loss amount.", "warn");
      var rec_loser = st.reactionWindow ? (st.reactionWindow.tgtPlayer||st.reactionWindow.srcPlayer) : tgtPlayer;
      if (!rec_loser||rec_loser===srcPlayer) return addLog(st, sn+": Recession - invalid target.", "warn");
      var rec_ns = { ...st, pendingEffect:null };
      // The loser's money was already deducted when the LOSE_MONEY reaction window opened.
      // Undo that deduction (raw restore — bypass gain-money side-effects) so the net result
      // stays at -rec_amount once payMoney re-charges them below.
      rec_ns = setPl(rec_ns, rec_loser, function(p){
        return { ...p, money:p.money+rec_amount, totalLost:Math.max(0,(p.totalLost||0)-rec_amount) };
      });
      // Now treat it as a payment from loser to Recession owner so pay-related passives
      // (Side Hustle, Plus Interest, Accountant, etc.) all fire correctly.
      // Suppress a new LOSE_MONEY reaction window to avoid cascading re-reactions.
      rec_ns = { ...rec_ns, _skipLoseMoneyWindow:true };
      rec_ns = payMoney(rec_ns, rec_loser, srcPlayer, rec_amount, "Recession");
      return { ...rec_ns, _skipLoseMoneyWindow:false };
    }
    case "risky_investment": {
      var ri_pe = st.pendingEffect;
      var ri_amount = ri_pe&&ri_pe.value ? ri_pe.value : (value||0);
      if (ri_amount<=0) return addLog(st, sn+": Risky Investment - no loss amount.", "warn");
      var ri_ns = { ...st, pendingEffect:null };
      return addMoney(ri_ns, srcPlayer, ri_amount, "Risky Investment (loss -> gain)");
    }
    case "refusal_to_work": {
      var rtw_pe = st.pendingEffect;
      // srcCard.value may be undefined if the card only has origVal/val — fall through each
      var rtw_val = rtw_pe
        ? ((rtw_pe.srcCard ? (rtw_pe.srcCard.value || rtw_pe.srcCard.origVal || rtw_pe.srcCard.val || 0) : 0) || rtw_pe.value || 0)
        : (value||0);
      // If fired in a Let Go targeting context, use the Let Go card's value (pendingEffect is null during targeting)
      if (!rtw_pe && st.letGoState && st.letGoState.srcCard) {
        rtw_val = st.letGoState.srcCard.value || st.letGoState.srcCard.origVal || rtw_val;
      }
      var rtw_ns = { ...st, pendingEffect:null };
      rtw_ns = addLog(rtw_ns, "🚫 "+sn+" refuses the effect!"+(rtw_val>0?" Gains $"+rtw_val+"k.":""), "reaction");
      if (rtw_val>0) rtw_ns = addMoney(rtw_ns, srcPlayer, rtw_val, "Refusal to Work");
      var rtw_victim = rtw_pe ? rtw_pe.srcPlayer : null;
      if (rtw_victim) rtw_ns = checkTargetedAd(rtw_ns, srcPlayer, rtw_victim);
      // If pocket change was targeting this player, skip them and advance
      if (rtw_ns.pocketChangeState && rtw_ns.pocketChangeState.phase === "targeting" &&
          rtw_ns.pocketChangeState.currentTarget === srcPlayer) {
        rtw_ns = { ...rtw_ns, pocketChangeState:{ ...rtw_ns.pocketChangeState, currentTarget:null } };
        return advancePocketChangeTargeting(rtw_ns);
      }
      // If Let Go was targeting this player, skip them and advance
      if (rtw_ns.letGoState && rtw_ns.letGoState.phase === "targeting" &&
          rtw_ns.letGoState.currentTarget === srcPlayer) {
        rtw_ns = { ...rtw_ns, letGoState:{ ...rtw_ns.letGoState, currentTarget:null } };
        return advanceLetGoTargeting(rtw_ns);
      }
      // If Kickstarter was targeting this player, skip them and advance
      if (rtw_ns.kickstarterState && rtw_ns.kickstarterState.phase === "targeting" &&
          rtw_ns.kickstarterState.currentTarget === srcPlayer) {
        rtw_ns = { ...rtw_ns, kickstarterState:{ ...rtw_ns.kickstarterState, currentTarget:null } };
        return advanceKickstarterTargeting(rtw_ns);
      }
      // If Negative Reaction was targeting this player, skip them and advance
      if (rtw_ns.negReactionState && rtw_ns.negReactionState.phase === "targeting" &&
          rtw_ns.negReactionState.currentTarget === srcPlayer) {
        rtw_ns = { ...rtw_ns, negReactionState:{ ...rtw_ns.negReactionState, currentTarget:null } };
        return advanceNegReactionTargeting(rtw_ns);
      }
      // If Shrinkage was targeting this player, clear the discard and restore any deferred window
      if (rtw_ns.shrinkageState && rtw_ns.shrinkageState.phase === "targeting" &&
          rtw_ns.shrinkageState.tgtPlayer === srcPlayer) {
        var rtw_sh_dw = rtw_ns.deferredWindow;
        rtw_ns = { ...rtw_ns, shrinkageState:null, deferredWindow:null };
        if (rtw_sh_dw) return { ...rtw_ns, reactionWindow:rtw_sh_dw };
        return maybeFlush(flushPendingDiscard(rtw_ns));
      }
      return rtw_ns;
    }
    case "repeat_customers": {
      var rc_pe = st.pendingEffect;
      var rc_assetId = rc_pe && rc_pe.srcCard ? rc_pe.srcCard.id : null;
      var rc_ownerId = rc_pe ? rc_pe.srcPlayer : srcPlayer;
      var rc_assetName = rc_pe && rc_pe.srcCard ? rc_pe.srcCard.name : "asset";
      if (!rc_assetId) return addLog(st, sn+": Repeat Customers - asset not found.", "warn");
      var rc_ns = setPl(st, rc_ownerId, function(p) {
        return { ...p, assets:p.assets.map(function(a) {
          return a.id === rc_assetId ? { ...a, status:ST.READY } : a;
        }) };
      });
      return addLog(rc_ns, "🔄 "+sn+"'s Repeat Customers resets \""+rc_assetName+"\" to READY.", "asset");
    }
    case "cover_the_costs": {
      var ctc_pe = st.pendingEffect;
      var ctc_amount = (ctc_pe && ctc_pe.value) || value || 0;
      if (ctc_amount<=0) return addLog(st, sn+": Cover the Costs - no amount.", "warn");
      var ctc_opps = st.players.filter(function(p){ return p.id!==srcPlayer; });
      if (!ctc_opps.length) return addLog(st, sn+": No opponents.", "warn");
      var ns_ctc = { ...st, coverCostsState:{ amount:ctc_amount, ownerId:srcPlayer } };
      return { ...ns_ctc, pendingChoice:{ type:"COVER_COSTS_TARGET", srcPlayer, timerEnd:Date.now()+20000, amount:ctc_amount, prompt:sn+": choose who takes the loss.", options:ctc_opps.map(function(p){ return {id:p.id,name:p.name}; }) }};
    }
    case "cover_costs_redirect": {
      if (!tgtPlayer||!value) return st;
      var ccr_ns = { ...st, coverCostsState:null };
      ccr_ns = addMoney(ccr_ns, srcPlayer, value, "Cover the Costs (refunded)");
      ccr_ns = addMoney(ccr_ns, tgtPlayer, -value, "Cover the Costs (redirected)");
      return ccr_ns;
    }
    case "equal_pay": {
      var ep_pe = st.pendingEffect;
      var ep_val = ep_pe&&ep_pe.hook==="__sold_card__" ? ep_pe.saleVal : value;
      if (!ep_val) return addLog(st, sn+": Equal Pay - no sale value.", "warn");
      // _skipGainWindow: prevents a nested GAIN_MONEY reaction window from
      // swallowing the SELL_CARD context, which would cause resolveSell to
      // never fire and the original seller to not receive their money.
      return addMoney({ ...st, _skipGainWindow:true }, srcPlayer, ep_val, "Equal Pay");
    }
    case "minor_loss": {
      var ml_pl = st.players.find(function(p){ return p.id===srcPlayer; });
      if (!ml_pl||ml_pl.money>=0) return addLog(st, sn+": Not in the negatives.", "warn");
      if (!ml_pl.hand.length) return addLog(st, sn+": No cards to discard.", "warn");
      return { ...st, pendingChoice:{ type:"MINOR_LOSS_DISCARD", srcPlayer, timerEnd:Date.now()+20000, prompt:sn+": choose a card to discard (A Minor Loss).", options:ml_pl.hand }};
    }
    case "p_leave_tip":    return st;
    case "p_tax":          return st;
    case "p_couple_bucks": return st;
    case "p_price_adj":    return st;
    case "p_high_valued":  return st;
    case "p_targeted_ad":  return st;
    case "p_consolation":   return st;
    case "a_paid_work":    return st; // handled via pendingPaidWork
    case "p_workmans_comp": return st;
    case "p_check_inv":    return st;
    case "a_paid_work":    return st; // effect applied directly via pendingPWChoice
    case "p_targeted_ad":  return st;
    case "p_discount":     return st;
    case "p_portfolio":    return st;
    case "p_pressure":     return st;
    case "p_swear_jar":    return st;
    case "p_side_hustle":  return st;
    case "p_lca":          return st;
    case "p_toll":         return st;
    case "p_startup_costs":return st;
    case "p_early_lead":   return st; // handled in applyPassives at turn start
    case "p_repetitive_work": return st;
    case "p_loss_prevention": return st;
    case "p_plus_interest": return st;
    case "p_accountant":   return st;
    case "p_recycle":      return st;
    case "p_full_refund":  return st;
    case "p_finders_fee":  return st;
    case "p_free_replacement": return st;
    case "p_price_check":  return st;
    case "p_sell_p1":      return st;
    case "p_interest":     return st;
    case "p_rule_of_thirds": return st;
    case "p_revenue_stream": return st; // tokens gained passively in addMoney, payout in finishEndTurn

    case "business_partners": {
      var bp_pe = st.pendingEffect;
      var bp_srcCard = bp_pe && bp_pe.srcCard ? bp_pe.srcCard : null;
      if (!bp_srcCard) return addLog(st, sn + "'s Business Partners: no card to copy.", "warn");
      var bp_blacklist = ["not_a_chance","fired","cancel_whole_action","cancel_reaction",
        "shrinkage","defective_unit","downsizing","last_minute_bid","risky_investment",
        "refusal_to_work","equal_pay","cover_the_costs","recession","competitor","inflation",
        "scammed","equity","business_partners","repeat_customers","back_to_basics","minor_loss",
        "worth_the_price","property_theft"];
      if (bp_blacklist.indexOf(bp_srcCard.hook) >= 0)
        return addLog(st, sn + "'s Business Partners: can't copy \"" + bp_srcCard.name + "\".", "info");
      var ns_bp = addLog(st, "🤝 " + sn + " will copy \"" + bp_srcCard.name + "\" after it resolves!", "reaction", [bp_srcCard]);
      // Queue the copy to fire AFTER the original action/asset fully resolves.
      // This ensures the original effect always goes off first, then BP mirrors it.
      var bp_existing = ns_bp._bpPendingCopies || [];
      return { ...ns_bp, _bpPendingCopies: [...bp_existing, { srcPlayer: srcPlayer, card: bp_srcCard }] };
    }

    default: return st;
  }
}

/* - Resolve a pending choice - */
function resolveChoice(st, choiceId) {
  const pc = st.pendingChoice;
  if (!pc) return st;
  let ns = { ...st, pendingChoice:null };
  const sn = pname(ns, pc.srcPlayer);
  const tn = pc.tgtPlayer ? pname(ns, pc.tgtPlayer) : null;

  switch (pc.type) {
    case "DISCARD_SELF": {
      const pl = ns.players.find(p => p.id===pc.srcPlayer);
      const card = pl?.hand.find(c => c.id===choiceId);
      if (!card) break;
      ns = setPl(ns, pc.srcPlayer, p => ({ ...p, hand:p.hand.filter(c=>c.id!==choiceId) }));
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...card, value:card.origVal, modifiedBy:[] }] };
      ns = addLog(ns, `🗑 ${sn} discards "${card.name}".`, "discard", [card]);
      break;
    }
    case "OPPONENT_DISCARD": {
      const pl = ns.players.find(p => p.id===pc.tgtPlayer);
      const card = pl?.hand.find(c => c.id===choiceId);
      if (!card) break;
      ns = setPl(ns, pc.tgtPlayer, p => ({ ...p, hand:p.hand.filter(c=>c.id!==choiceId) }));
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...card, value:card.origVal, modifiedBy:[] }] };
      ns = addLog(ns, `🗑 ${tn} discards "${card.name}".`, "discard", [card]);
      break;
    }
    case "CLIENT_THEFT": {
      ns = applyHandBoosts(ns, pc.tgtPlayer);
      const pl = ns.players.find(p => p.id===pc.tgtPlayer);
      const card = pl?.hand.find(c => c.id===choiceId);
      if (!card) break;
      ns = setPl(ns, pc.tgtPlayer, p => ({ ...p, hand:p.hand.filter(c=>c.id!==choiceId) }));
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...card, value:card.origVal, modifiedBy:[] }] };
      ns = addLog(ns, `🗑 ${tn} discards "${card.name}".`, "discard", [card]);
      ns = addMoney(ns, pc.tgtPlayer, -card.value, `Client Theft (${card.name})`);
      ns = applyPlusInterest(ns, pc.srcPlayer, pc.tgtPlayer);
      ns = applyAccountant(ns, pc.tgtPlayer, pc.srcPlayer, card.value);
      // Push a LOSE_MONEY trigger so Cover the Costs and Risky Investment can react
      ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:pc.tgtPlayer, value:card.value });
      if (!ns.reactionWindow && !ns._skipLoseMoneyWindow) {
        var ct_trig = ns.triggerStack[ns.triggerStack.length-1];
        var ct_reactors = reactorsFor(ns.players, ct_trig);
        if (ct_reactors.some(function(r){ return r.canReact; })) {
          ns = { ...ns, pendingEffect:{ hook:"__lose_money__", srcPlayer:pc.tgtPlayer, tgtPlayer:null, value:card.value, reason:`Client Theft (${card.name})` } };
          return { ...ns, reactionWindow:buildWindow(uid(), ct_trig.tid, T.LOSE_MONEY, pc.tgtPlayer, pc.tgtPlayer, ct_reactors) };
        }
      }
      break;
    }
    case "STEAL_CARD": {
      const pl = ns.players.find(p => p.id===pc.tgtPlayer);
      const card = pl?.hand.find(c => c.id===choiceId);
      if (!card) break;
      ns = setPl(ns, pc.tgtPlayer, p => ({ ...p, hand:p.hand.filter(c=>c.id!==choiceId) }));
      ns = setPl(ns, pc.srcPlayer, p => ({ ...p, hand:[...p.hand, card] }));
      ns = addLog(ns, `🤝 ${sn} steals "${card.name}" from ${tn}.`, "action", [card]);
      break;
    }
    case "NEG_REACTION_SELECT": {
      var nrs_r = ns.negReactionState;
      var tgtPid_nr = pc.tgtPlayer;
      var tpNR = ns.players.find(function(p) { return p.id === tgtPid_nr; });
      var rxCards = tpNR ? tpNR.hand.filter(function(c) { return c.type === CT.REACTION; }) : [];
      var selCard_nr;
      if (choiceId === "auto" || !choiceId) {
        selCard_nr = rxCards.length ? shuf([...rxCards])[0] : null;
        if (selCard_nr) ns = addLog(ns, "⏱ " + pname(ns, tgtPid_nr) + " ran out of time - card chosen randomly.", "warn");
      } else {
        selCard_nr = rxCards.find(function(c) { return c.id === choiceId; });
      }
      var newSels_nr = selCard_nr
        ? [...(nrs_r.selections || []), { pid:tgtPid_nr, card:selCard_nr }]
        : [...(nrs_r.selections || [])];

      if (nrs_r.collectQueue.length > 0) {
        var nextTgt_nr = nrs_r.collectQueue[0];
        var remQ_nr = nrs_r.collectQueue.slice(1);
        ns = { ...ns, negReactionState:{ ...nrs_r, collectQueue:remQ_nr, selections:newSels_nr } };
        var tpNext_nr = ns.players.find(function(p) { return p.id === nextTgt_nr; });
        var rxNext = tpNext_nr ? tpNext_nr.hand.filter(function(c) { return c.type === CT.REACTION; }) : [];
        return { ...ns, pendingChoice:{ type:"NEG_REACTION_SELECT", srcPlayer:nrs_r.srcPlayer, tgtPlayer:nextTgt_nr,
          timerEnd:Date.now()+20000,
          prompt: pname(ns, nrs_r.srcPlayer) + " played Negative Reaction - choose a reaction card to discard.",
          options: rxNext }};
      }

      // All selections done - run through discard queue (DISCARD_CARD windows fire per card)
      ns = { ...ns, negReactionState:null };
      var finalSels_nr = newSels_nr.filter(function(s) { return s.card; });
      return startDiscardQueue(ns, finalSels_nr, "neg_reaction_resolve", { srcPlayer:pc.srcPlayer });
    }
    case "SHRINKAGE_SELECT": {
      var ss_pl = ns.players.find(function(p){ return p.id === pc.tgtPlayer; });
      var ss_card = (choiceId === "auto" || !choiceId)
        ? (ss_pl && ss_pl.hand.length ? shuf([...ss_pl.hand])[0] : null)
        : (ss_pl ? ss_pl.hand.find(function(c){ return c.id === choiceId; }) : null);
      if (!ss_card) { ns = { ...ns, shrinkageState:null }; break; }
      ns = setPl(ns, pc.tgtPlayer, function(p){ return { ...p, hand:p.hand.filter(function(c){ return c.id !== ss_card.id; }) }; });
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...ss_card, value:ss_card.origVal, modifiedBy:[] }] };
      ns = addLog(ns, "🗑 " + pname(ns, pc.tgtPlayer) + " discards \"" + ss_card.name + "\" (Shrinkage).", "discard", [ss_card]);
      ns = pushTrigger(ns, { type:T.DISCARD_CARD, srcPlayer:pc.srcPlayer, tgtPlayer:pc.tgtPlayer });
      ns = { ...ns, shrinkageState:null };
      var ss_dw = ns.deferredWindow || pc._deferredWindow || null;
      ns = { ...ns, deferredWindow:null };
      // Resolve the original action card — derive resumePE independently of ss_dw
      // so it works even when Shrinkage fired via applyAndClearPendingReaction (no deferredWindow set).
      var ss_rpe = (ns.pendingEffect && ns.pendingEffect.hook === "__resume_action__") ? ns.pendingEffect
                 : (pc.shrinkageState && pc.shrinkageState.resumePE) ? pc.shrinkageState.resumePE
                 : null;
      // If the deferred window was ACTION_PLAYED, it was already fully resolved -
      // don't re-show it; just continue the original action directly.
      if ((ss_dw && ss_dw.ttype === T.ACTION_PLAYED) || (!ss_dw && ss_rpe)) {
        if (ss_rpe) {
          ns = { ...ns, pendingEffect:null };
          return continueAfterActionPlayed(ns, ss_rpe);
        }
        return maybeFlush(ns);
      }
      if (ss_dw && ss_dw.ttype === T.SELL_CARD) {
        return resolveSell(ns);
      }
      if (ss_dw) ns = { ...ns, reactionWindow:ss_dw };
      break;
    }
    case "OUTSIDE_HIRE_SELECT": {      var oh_opts2 = pc.options || [];
      var oh_chosen = (choiceId === "auto" || !choiceId)
        ? (oh_opts2.length ? oh_opts2[Math.floor(_rand()*oh_opts2.length)] : null)
        : oh_opts2.find(function(a) { return a.id === choiceId; });
      if (!oh_chosen) break;
      var oh_ownerPid = oh_chosen.ownerId;
      var oh_assetVal = oh_chosen.value;
      // Push TARGET_PLAYER for asset owner, then proceed to outside_hire_after_target
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:oh_ownerPid, label:"Outside Hire" });
      var oh_trig = ns.triggerStack[ns.triggerStack.length - 1];
      var oh_reactors = reactorsFor(ns.players, oh_trig);
      // Store context: assetId in srcCard.id, assetValue in value, asset owner in tgtPlayer
      var oh_ctx = { hook:"outside_hire_after_target", srcPlayer:pc.srcPlayer, tgtPlayer:oh_ownerPid,
                     value:oh_assetVal, srcCard:{ id:oh_chosen.id, name:oh_chosen.name } };
      ns = { ...ns, pendingEffect:oh_ctx };
      if (oh_reactors.some(function(r) { return r.canReact; })) {
        return { ...ns, reactionWindow:buildWindow(uid(), oh_trig.tid, T.TARGET_PLAYER, pc.srcPlayer, oh_ownerPid, oh_reactors) };
      }
      // No reactors on TARGET_PLAYER - proceed directly
      ns = { ...ns, pendingEffect:null };
      return maybeFlush(applyHook(ns, "outside_hire_after_target", oh_ctx));
    }
    case "GIVE_BACK_TIE": {
      var gb_chosen = (choiceId === "auto" || !choiceId)
        ? pc.options[Math.floor(_rand()*pc.options.length)]
        : pc.options.find(function(p){ return p.id === choiceId; });
      if (!gb_chosen) break;
      // Now run TARGET_PLAYER trigger then pay
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:gb_chosen.id, label:"Give Back" });
      var gb_trig2 = ns.triggerStack[ns.triggerStack.length-1];
      var gb_react2 = reactorsFor(ns.players, gb_trig2);
      ns = { ...ns, pendingEffect:{ hook:"give_back_pay", srcPlayer:pc.srcPlayer, tgtPlayer:gb_chosen.id, value:4, srcCard:pc.srcCard } };
      if (gb_react2.some(function(r){ return r.canReact; })) {
        return { ...ns, reactionWindow:buildWindow(uid(), gb_trig2.tid, T.TARGET_PLAYER, pc.srcPlayer, gb_chosen.id, gb_react2) };
      }
      ns = { ...ns, pendingEffect:null };
      return maybeFlush(applyHook(ns, "give_back_pay", { srcPlayer:pc.srcPlayer, tgtPlayer:gb_chosen.id, value:4, srcCard:pc.srcCard }));
    }
    case "REFRESH_ASSET": {
      var rf_opts2 = pc.options || [];
      var rf_chosen = (choiceId === "auto" || !choiceId)
        ? (rf_opts2.length ? rf_opts2[Math.floor(_rand()*rf_opts2.length)] : null)
        : rf_opts2.find(function(a){ return a.id === choiceId; });
      if (!rf_chosen) break;
      var autoTag = (choiceId === "auto" || !choiceId) ? " (auto)" : "";
      ns = setPl(ns, pc.srcPlayer, function(p) {
        return { ...p, assets:p.assets.map(function(a){ return a.id===rf_chosen.id ? { ...a, status:ST.READY } : a; }) };
      });
      ns = addLog(ns, "🔄 " + sn + " refreshes \"" + rf_chosen.name + "\" -> READY" + autoTag + ".", "asset", [rf_chosen]);
      break;
    }
    case "OUT_OF_ORDER_SELECT": {
      // choiceId is either an opponentId (phase=opponent) or an assetId (phase=asset) or "auto"
      var ooos_pc  = pc;
      var ooos_src = pc.srcPlayer;
      if (choiceId === "auto") {
        // Auto-pick: random opponent -> random eligible asset
        var ooos_validOpp = ns.players.filter(function(p){
          return p.id !== ooos_src && p.assets.some(function(a){ return !a.disabled; });
        });
        if (!ooos_validOpp.length) { ns = addLog(ns, "Out of Order: No eligible targets.", "warn"); break; }
        var ooos_opp = shuf(ooos_validOpp)[0];
        var ooos_eligibles = ooos_opp.assets.filter(function(a){ return !a.disabled; });
        var ooos_asset = shuf(ooos_eligibles)[0];
        // Emit TARGET_PLAYER -> pendingEffect -> apply
        ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:ooos_src, tgtPlayer:ooos_opp.id, label:"Out of Order" });
        var ooos_trig = ns.triggerStack[ns.triggerStack.length-1];
        var ooos_reactors = reactorsFor(ns.players, ooos_trig);
        var ooos_effect = { hook:"apply_out_of_order", srcPlayer:ooos_src, tgtPlayer:ooos_opp.id, value:ooos_asset.id, srcCard:{ name:"Out of Order" } };
        ns = { ...ns, pendingEffect:ooos_effect };
        if (ooos_reactors.some(function(r){ return r.canReact; })) {
          return { ...ns, reactionWindow:buildWindow(uid(), ooos_trig.tid, T.TARGET_PLAYER, ooos_src, ooos_opp.id, ooos_reactors) };
        }
        ns = { ...ns, pendingEffect:null };
        return maybeFlush(applyHook(ns, "apply_out_of_order", ooos_effect));
      }
      if (ooos_pc.phase === "opponent") {
        // Player picked an opponent - move to asset selection phase
        var ooos_chosenOpp = ns.players.find(function(p){ return p.id === choiceId; });
        if (!ooos_chosenOpp) break;
        var ooos_oppAssets = ooos_chosenOpp.assets.filter(function(a){ return !a.disabled; });
        ns = { ...ns, pendingChoice:{ ...ooos_pc,
          phase:"asset",
          selectedOpponent:choiceId,
          selectedOpponentName:ooos_chosenOpp.name,
          assets:ooos_oppAssets,
          // timerEnd stays the same - single countdown for the whole selection
        }};
        return ns;
      }
      if (ooos_pc.phase === "asset") {
        // Player picked an asset
        var ooos_ownerId   = ooos_pc.selectedOpponent;
        var ooos_chosenAsset = ns.players.find(function(p){ return p.id === ooos_ownerId; })
                                 ?.assets.find(function(a){ return a.id === choiceId; });
        if (!ooos_chosenAsset || ooos_chosenAsset.disabled) break;
        // Emit TARGET_PLAYER -> pendingEffect -> window or direct apply
        ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:ooos_src, tgtPlayer:ooos_ownerId, label:"Out of Order" });
        var ooos_trig2 = ns.triggerStack[ns.triggerStack.length-1];
        var ooos_reactors2 = reactorsFor(ns.players, ooos_trig2);
        var ooos_eff2 = { hook:"apply_out_of_order", srcPlayer:ooos_src, tgtPlayer:ooos_ownerId, value:choiceId, srcCard:{ name:"Out of Order" } };
        ns = { ...ns, pendingEffect:ooos_eff2 };
        if (ooos_reactors2.some(function(r){ return r.canReact; })) {
          return { ...ns, reactionWindow:buildWindow(uid(), ooos_trig2.tid, T.TARGET_PLAYER, ooos_src, ooos_ownerId, ooos_reactors2) };
        }
        ns = { ...ns, pendingEffect:null };
        return maybeFlush(applyHook(ns, "apply_out_of_order", ooos_eff2));
      }
      break;
    }
    case "UPGRADE_DISCARD": {
      // Step 2: player chose which asset to destroy
      var ud_opts = pc.options || [];
      var ud_chosen = (choiceId === "auto" || !choiceId)
        ? (ud_opts.length ? ud_opts[Math.floor(_rand()*ud_opts.length)] : null)
        : ud_opts.find(function(a){ return a.id === choiceId; });
      if (!ud_chosen) break;
      var ud_auto = (choiceId === "auto" || !choiceId) ? " (auto)" : "";
      // Remove the asset from player - purge any passive effects it carried
      ns = setPl(ns, pc.srcPlayer, function(p){
        return { ...p, assets:p.assets.filter(function(a){ return a.id !== ud_chosen.id; }) };
      });
      ns = addLog(ns, "💥 " + sn + " destroys \"" + ud_chosen.name + "\"" + ud_auto + " (Upgrade).", "asset", [ud_chosen]);
      // Now open shop selection if shop has cards
      if (!ns.shop.length) {
        ns = addLog(ns, sn + ": Shop is empty - Upgrade gains no replacement.", "info");
        break;
      }
      ns = { ...ns, pendingChoice:{ type:"UPGRADE_GAIN", srcPlayer:pc.srcPlayer,
        timerEnd:Date.now()+20000,
        prompt:sn+": choose a shop asset to take for free.",
        options:ns.shop.filter(function(a){ return !ns.players.find(function(p){ return p.assets.some(function(oa){ return oa.id===a.id; }); }); }) } };
      return ns;
    }
    case "UPGRADE_GAIN": {
      // Step 3: player chose which shop asset to acquire
      var ug_opts = pc.options || [];
      var ug_chosen = (choiceId === "auto" || !choiceId)
        ? (ug_opts.length ? ug_opts[Math.floor(_rand()*ug_opts.length)] : null)
        : ns.shop.find(function(a){ return a.id === choiceId; });
      if (!ug_chosen) break;
      var ug_auto = (choiceId === "auto" || !choiceId) ? " (auto)" : "";
      // Remove from shop
      var ug_newShop = ns.shop.filter(function(a){ return a.id !== ug_chosen.id; });
      // Refill shop slot from asset deck
      var ug_deck = ns.assetDeck || [];
      var ug_disc = ns.assetDiscard || [];
      if (ug_deck.length === 0 && ug_disc.length > 0) {
        ug_deck = shuf([...ug_disc]);
        ug_disc = [];
        ns = addLog(ns, "🔀 Asset deck reshuffled.", "info");
      }
      if (ug_deck.length > 0) {
        ug_newShop = [...ug_newShop, ug_deck[0]];
        ug_deck = ug_deck.slice(1);
      }
      ns = { ...ns, shop:ug_newShop, assetDeck:ug_deck, assetDiscard:ug_disc };
      // Give asset to player (reset to READY, clear tokens)
      var ug_asset = { ...ug_chosen, id:uid(), status:"READY", tokens:[], disabled:false };
      ns = setPl(ns, pc.srcPlayer, function(p){ return { ...p, assets:[...p.assets, ug_asset] }; });
      ns = addLog(ns, "🏢 " + sn + " acquires \"" + ug_chosen.name + "\"" + ug_auto + " for free (Upgrade).", "asset", [ug_chosen]);
      ns = pushTrigger(ns, { type:T.BUY_ASSET, srcPlayer:pc.srcPlayer, value:0 });
      break;
    }
    case "HELP_WANTED_GIVE": {
      // Target chose which card to give; if choiceId is "auto" pick random
      const hw_tgt = ns.players.find(p => p.id===pc.tgtPlayer);
      if (!hw_tgt || !hw_tgt.hand.length) break;
      const hw_card = choiceId === "auto"
        ? hw_tgt.hand[Math.floor(_rand()*hw_tgt.hand.length)]
        : hw_tgt.hand.find(c => c.id===choiceId);
      if (!hw_card) break;
      ns = setPl(ns, pc.tgtPlayer, p => ({ ...p, hand:p.hand.filter(c=>c.id!==hw_card.id) }));
      ns = setPl(ns, pc.srcPlayer, p => ({ ...p, hand:[...p.hand, hw_card] }));
      const autoStr = choiceId === "auto" ? " (auto-selected)" : "";
      ns = addLog(ns, `📋 ${tn} gives "${hw_card.name}" to ${sn}${autoStr}.`, "action", [hw_card]);
      break;
    }
    case "INSPECT": break; // read-only, no action needed
    case "PEEK":    break;
    case "INTERVIEWS_PICK": {
      var iv_opts = pc.options || [];
      // choiceId "auto" -> random pick
      var iv_kept = choiceId === "auto"
        ? iv_opts[Math.floor(_rand()*iv_opts.length)]
        : iv_opts.find(c => c.id === choiceId);
      if (!iv_kept) break;
      var iv_rest = shuf(iv_opts.filter(c => c.id !== iv_kept.id));
      // Add kept card to hand
      ns = setPl(ns, pc.srcPlayer, p => ({ ...p, hand:[...p.hand, iv_kept] }));
      ns = addLog(ns, "🤝 " + sn + " keeps \"" + iv_kept.name + "\"" + (choiceId==="auto"?" (auto)":"") + ".", "draw", [iv_kept]);
      // Checking Inventory: Interviews counts as a triggered draw event
      ns = checkInventoryDraw(ns, pc.srcPlayer);
      // Place rest at bottom of deck in random order
      if (iv_rest.length) {
        ns = { ...ns, mainDeck:[...ns.mainDeck, ...iv_rest] };
        ns = addLog(ns, "⬇ " + iv_rest.map(c=>"\""+c.name+"\"").join(" & ") + " placed at bottom of deck.", "info");
      }
      break;
    }
    case "HALF_PRICE_BUY": {
      const asset = ns.shop.find(a => a.id===choiceId);
      if (!asset) break;
      ns = doBuyShop(ns, pc.srcPlayer, choiceId, Math.floor(asset.origVal / 2));
      break;
    }
    case "DISCOUNT_BUY": {
      ns = doBuyShop(ns, pc.srcPlayer, choiceId, 1);
      break;
    }
    case "DOUBLE_SELL": {
      const pl = ns.players.find(p => p.id===pc.srcPlayer);
      const card = pl?.hand.find(c => c.id===choiceId);
      if (!card) break;
      const val = card.value * 2;
      ns = setPl(ns, pc.srcPlayer, p => ({ ...p, hand:p.hand.filter(c=>c.id!==choiceId), money:p.money+val }));
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, card] };
      ns = addLog(ns, `💰 ${sn} double-sells "${card.name}" for ${$(val)}.`, "money", [card]);
      break;
    }
    case "DISCARD_TO_LIMIT": {
      var pl2 = ns.players.find(function(p) { return p.id===pc.srcPlayer; });
      var toDiscard;
      if (choiceId === "auto") {
        toDiscard = shuf([...pl2.hand]).slice(0, pc.needed);
        ns = addLog(ns, "⏱ " + sn + " ran out of time - " + pc.needed + " card(s) discarded randomly.", "warn", toDiscard);
      } else {
        var ids = Array.isArray(choiceId) ? choiceId : [choiceId];
        toDiscard = pl2.hand.filter(function(c) { return ids.includes(c.id); });
        ns = addLog(ns, "🗑 " + sn + " discards " + toDiscard.length + " card(s) to hand limit.", "discard", toDiscard);
      }
      ns = setPl(ns, pc.srcPlayer, function(p) { return { ...p, hand:p.hand.filter(function(c) { return !toDiscard.some(function(d) { return d.id===c.id; }); }) }; });
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, ...toDiscard] };
      return finishEndTurn(ns);
    }
    case "POCKET_CHANGE_SELECT": {
      var pcs_r = ns.pocketChangeState;
      var tgtPid_r = pc.tgtPlayer;
      var tpR = ns.players.find(function(p) { return p.id === tgtPid_r; });
      var selCard;
      if (choiceId === "auto") {
        selCard = tpR && shuf([...tpR.hand])[0];
        ns = addLog(ns, "⏱ " + pname(ns, tgtPid_r) + " ran out of time - card chosen randomly.", "warn");
      } else {
        selCard = tpR && tpR.hand.find(function(c) { return c.id === choiceId; });
      }
      if (!selCard) { return maybeFlush(ns); }
      ns = addLog(ns, "🎴 " + pname(ns, tgtPid_r) + " reveals \"" + selCard.name + "\" ($" + selCard.value + "k).", "info", [selCard]);
      var newSels = [...(pcs_r.selections || []), { pid:tgtPid_r, card:selCard }];
      if (pcs_r.collectQueue.length > 0) {
        var nextTgt_r = pcs_r.collectQueue[0];
        var remQ_r = pcs_r.collectQueue.slice(1);
        ns = { ...ns, pocketChangeState:{ ...pcs_r, collectQueue:remQ_r, selections:newSels } };
        var tpNext_r = ns.players.find(function(p) { return p.id === nextTgt_r; });
        return { ...ns, pendingChoice:{ type:"POCKET_CHANGE_SELECT", srcPlayer:pcs_r.srcPlayer, tgtPlayer:nextTgt_r,
          timerEnd:Date.now()+20000, needed:1,
          prompt:pname(ns,pcs_r.srcPlayer)+" will gain the value of the card you select.",
          options:tpNext_r ? tpNext_r.hand : [] }};
      } else {
        // All collected - reveal phase
        ns = { ...ns, pocketChangeState:{ ...pcs_r, selections:newSels } };
        var revCards = newSels.map(function(s) { return s.card; });
        return { ...ns, pendingChoice:{ type:"POCKET_CHANGE_REVEAL", srcPlayer:pcs_r.srcPlayer,
          cards:revCards, timerEnd:Date.now()+5000,
          prompt:pname(ns,pcs_r.srcPlayer)+" gains the combined value of all shown cards." }};
      }
    }

    case "LET_GO_SELECT": {
      var lgs_r = ns.letGoState;
      var tgtPid_lg = pc.tgtPlayer;
      var tpLG = ns.players.find(function(p) { return p.id === tgtPid_lg; });
      var selCard_lg;
      if (choiceId === "auto" || !choiceId) {
        selCard_lg = tpLG && tpLG.hand.length ? shuf([...tpLG.hand])[0] : null;
        if (tpLG) ns = addLog(ns, "⏱ " + pname(ns, tgtPid_lg) + " ran out of time - card chosen randomly.", "warn");
      } else {
        selCard_lg = tpLG && tpLG.hand.find(function(c) { return c.id === choiceId; });
      }
      var newSels_lg = selCard_lg
        ? [...(lgs_r.selections || []), { pid:tgtPid_lg, card:selCard_lg }]
        : [...(lgs_r.selections || [])];

      if (lgs_r.collectQueue.length > 0) {
        // More players to collect from
        var nextTgt_lg = lgs_r.collectQueue[0];
        var remQ_lg = lgs_r.collectQueue.slice(1);
        ns = { ...ns, letGoState:{ ...lgs_r, collectQueue:remQ_lg, selections:newSels_lg } };
        var tpNext_lg = ns.players.find(function(p) { return p.id === nextTgt_lg; });
        var nextTgt_lg_idx = ns.players.findIndex(function(p){ return p.id===nextTgt_lg; });
        ns = { ...ns, viewIdx:nextTgt_lg_idx >= 0 ? nextTgt_lg_idx : ns.viewIdx };
        return { ...ns, pendingChoice:{ type:"LET_GO_SELECT", srcPlayer:lgs_r.srcPlayer, tgtPlayer:nextTgt_lg,
          timerEnd:Date.now()+20000,
          prompt: pname(ns, lgs_r.srcPlayer) + " played Let Go - choose a card to discard. You will lose its value.",
          options: tpNext_lg ? tpNext_lg.hand : [] }};
      }

      // All selections done - run through discard queue (DISCARD_CARD windows fire per card)
      ns = { ...ns, letGoState:null };
      var finalSels = newSels_lg.filter(function(s) { return s.card; });
      return startDiscardQueue(ns, finalSels, "let_go_resolve", { srcPlayer:lgs_r.srcPlayer });
    }

    case "POCKET_CHANGE_REVEAL": {
      var pcs_rv = ns.pocketChangeState;
      var total_rv = pcs_rv ? (pcs_rv.selections||[]).reduce(function(s,sel){ return s+(sel.card?sel.card.value:0); },0) : 0;
      ns = { ...ns, pocketChangeState:null };
      if (total_rv > 0) ns = addMoney(ns, pc.srcPlayer, total_rv, "Pocket Change");
      return maybeFlush(ns);
    }
    case "RISK_REWARD_SET_TARGET": {
      var rvrChoiceTgt = ns.players.find(function(p) { return p.id === choiceId; });
      if (!rvrChoiceTgt) break;
      // Clear existing target immediately - if new targeting is cancelled, card is left with no target
      ns = setPl(ns, pc.srcPlayer, function(p) {
        return { ...p, assets:p.assets.map(function(a) { return a.id === pc.assetId ? { ...a, riskRewardTarget:null, riskRewardTargetName:null } : a; }) };
      });
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, label:"Risk vs Reward", _fromAsset:true });
      var rvrTrig = ns.triggerStack[ns.triggerStack.length - 1];
      var rvrReactors = reactorsFor(ns.players, rvrTrig);
      var rvrSrcCard = { id:pc.assetId, hook:"rvr_set_target", name:"Risk vs Reward" };
      ns = { ...ns, pendingEffect:{ hook:"rvr_set_target", srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:pc.assetId, srcCard:rvrSrcCard } };
      if (rvrReactors.some(function(r) { return r.canReact; })) {
        return { ...ns, reactionWindow:buildWindow(uid(), rvrTrig.tid, T.TARGET_PLAYER, pc.srcPlayer, choiceId, rvrReactors) };
      }
      ns = { ...ns, pendingEffect:null };
      return maybeFlush(applyHook(ns, "rvr_set_target", { srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:pc.assetId, srcCard:rvrSrcCard }));
    }
    case "LETS_DEAL_SET_TARGET": {
      var ldChoiceTgt = ns.players.find(function(p){ return p.id===choiceId; });
      if (!ldChoiceTgt) break;
      ns = setPl(ns, pc.srcPlayer, function(p){
        return { ...p, assets:p.assets.map(function(a){ return a.id===pc.assetId?{...a,letsDealTarget:null,letsDealTargetName:null}:a; }) };
      });
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, label:"Let's Make a Deal", _fromAsset:true });
      var ldTrig = ns.triggerStack[ns.triggerStack.length-1];
      var ldReactors = reactorsFor(ns.players, ldTrig);
      var ldSrcCard = { id:pc.assetId, hook:"ld_set_target", name:"Let's Make a Deal" };
      ns = { ...ns, pendingEffect:{ hook:"ld_set_target", srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:pc.assetId, srcCard:ldSrcCard } };
      if (ldReactors.some(function(r){ return r.canReact; })) {
        return { ...ns, reactionWindow:buildWindow(uid(), ldTrig.tid, T.TARGET_PLAYER, pc.srcPlayer, choiceId, ldReactors) };
      }
      ns = { ...ns, pendingEffect:null };
      return maybeFlush(applyHook(ns, "ld_set_target", { srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:pc.assetId, srcCard:ldSrcCard }));
    }
    case "EXTRA_INCOME_SET_TARGET": {
      var eiTgt = ns.players.find(function(p) { return p.id === choiceId; });
      if (!eiTgt) break;
      // Clear existing target immediately - if new targeting is cancelled, card is left with no target
      ns = setPl(ns, pc.srcPlayer, function(p) {
        return { ...p, assets:p.assets.map(function(a) { return a.id === pc.assetId ? { ...a, extraIncomeTarget:null } : a; }) };
      });
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, label:"Extra Income", _fromAsset:true });
      var eiTrig = ns.triggerStack[ns.triggerStack.length - 1];
      var eiReactors = reactorsFor(ns.players, eiTrig);
      var eiSrcCard = { id:pc.assetId, hook:"ei_set_target", name:"Extra Income" };
      ns = { ...ns, pendingEffect:{ hook:"ei_set_target", srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:pc.assetId, srcCard:eiSrcCard } };
      if (eiReactors.some(function(r) { return r.canReact; })) {
        return { ...ns, reactionWindow:buildWindow(uid(), eiTrig.tid, T.TARGET_PLAYER, pc.srcPlayer, choiceId, eiReactors) };
      }
      ns = { ...ns, pendingEffect:null };
      return maybeFlush(applyHook(ns, "ei_set_target", { srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:pc.assetId, srcCard:eiSrcCard }));
    }
    case "KICKSTARTER_CHOICE": {
      var ksChoice = choiceId; // "2","3","4","6" or "auto"
      if (ksChoice === "auto") ksChoice = "2";
      var ksMap = { "2":{amount:2,draw:0,gainAsset:false}, "3":{amount:3,draw:1,gainAsset:false},
                    "4":{amount:4,draw:2,gainAsset:false}, "6":{amount:6,draw:1,gainAsset:true} };
      var ksCh = ksMap[ksChoice] || ksMap["2"];
      var ksKs = ns.kickstarterState;
      var ksLabels = {"2":"Pay $2k","3":"Pay $3k + draw 1","4":"Pay $4k + draw 2","6":"Pay $6k + asset + draw 1"};
      ns = addLog(ns, "💼 " + pname(ns, pc.tgtPlayer) + " chose: " + ksLabels[ksChoice] + ".", "info");
      var newChoices = { ...ksKs.choices, [pc.tgtPlayer]: ksCh };
      ns = { ...ns, kickstarterState:{ ...ksKs, choices:newChoices } };
      if (ksKs.choiceQueue.length > 0) {
        var nextChooser = ksKs.choiceQueue[0];
        ns = { ...ns, kickstarterState:{ ...ns.kickstarterState, choiceQueue:ksKs.choiceQueue.slice(1) } };
        return { ...ns, pendingChoice:{ type:"KICKSTARTER_CHOICE", srcPlayer:ksKs.srcPlayer,
          tgtPlayer:nextChooser, timerEnd:Date.now()+20000,
          assetDeckEmpty: ns.assetDeck.length === 0,
          prompt:pname(ns, ksKs.srcPlayer) + " played Kickstarter. Choose your contribution:" }};
      }
      // All choices made - resolve
      return applyKickstarterResolution(ns);
    }
    case "TAXES_SELECT_ASSET": {
      var taxAssetId = choiceId;
      // Find which player owns this asset
      var taxOwner = null, taxAsset = null;
      if (taxAssetId === "auto") {
        // Pick a random asset from options
        var allTaxOpts = [];
        ns.players.forEach(function(p) {
          if (p.id === pc.srcPlayer) return;
          p.assets.forEach(function(a) { if (!a.disabled) allTaxOpts.push({ pid:p.id, asset:a }); });
        });
        if (!allTaxOpts.length) { ns = addLog(ns, "No valid assets to tax.", "warn"); break; }
        var pick = shuf(allTaxOpts)[0];
        taxOwner = ns.players.find(function(p) { return p.id === pick.pid; });
        taxAsset = pick.asset;
      } else {
        ns.players.forEach(function(p) {
          if (p.id === pc.srcPlayer) return;
          var found = p.assets.find(function(a) { return a.id === taxAssetId; });
          if (found) { taxOwner = p; taxAsset = found; }
        });
      }
      if (!taxOwner || !taxAsset) break;
      // Emit TARGET_PLAYER on the asset owner, store pendingEffect for the disable
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:taxOwner.id, label:"Taxes" });
      var taxTrig = ns.triggerStack[ns.triggerStack.length - 1];
      var taxReactors = reactorsFor(ns.players, taxTrig);
      var taxEffect = { hook:"apply_tax", srcPlayer:pc.srcPlayer, tgtPlayer:taxOwner.id, value:taxAsset.id, srcCard:{ name:"Taxes" } };
      ns = { ...ns, pendingEffect: taxEffect };
      if (taxReactors.some(function(r) { return r.canReact; })) {
        return { ...ns, reactionWindow:buildWindow(uid(), taxTrig.tid, T.TARGET_PLAYER, pc.srcPlayer, taxOwner.id, taxReactors) };
      }
      ns = { ...ns, pendingEffect:null };
      return maybeFlush(applyHook(ns, "apply_tax", taxEffect));
    }
    case "BLUEPRINT_PLACE": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Blueprint placement cancelled.","info"); break; }
      var bpp_card = ns.players.find(function(p){return p.id===pc.srcPlayer;})?.hand.find(function(c){return c.id===choiceId;});
      if (!bpp_card) break;
      var bpp_asset = ns.players.find(function(p){return p.id===pc.srcPlayer;})?.assets.find(function(a){return a.id===pc.assetId;});
      var bpp_oldCard = bpp_asset ? bpp_asset.lockedCard : null;
      ns = setPl(ns, pc.srcPlayer, function(p){
        var newHand = p.hand.filter(function(c){return c.id!==choiceId;});
        if (bpp_oldCard) newHand = [...newHand, bpp_oldCard];
        return { ...p, hand:newHand, assets:p.assets.map(function(a){
          return a.id===pc.assetId ? { ...a, lockedCard:bpp_card } : a;
        }) };
      });
      if (bpp_oldCard) ns = addLog(ns, '🔄 "'+bpp_oldCard.name+'" returned to hand (replaced on Blueprint).', "asset");
      ns = addLog(ns, "📋 "+pname(ns,pc.srcPlayer)+' stores "'+bpp_card.name+'" on Blueprint. $'+bpp_card.value+'k actions now free.', "asset");
      break;
    }
    case "RETAINER_PLACE": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Retainer placement cancelled.","info"); break; }
      var rtp_card = ns.players.find(function(p){return p.id===pc.srcPlayer;})?.hand.find(function(c){return c.id===choiceId;});
      if (!rtp_card) break;
      var rtp_asset = ns.players.find(function(p){return p.id===pc.srcPlayer;})?.assets.find(function(a){return a.id===pc.assetId;});
      var rtp_oldCard = rtp_asset ? rtp_asset.lockedCard : null;
      var rtp_uses = rtp_card.origVal||1;
      var rtp_toks = Array.from({length:rtp_uses},function(){return {id:uid()};});
      ns = setPl(ns, pc.srcPlayer, function(p) {
        var newHand = p.hand.filter(function(c){return c.id!==choiceId;});
        if (rtp_oldCard) newHand = [...newHand, rtp_oldCard];
        return { ...p, hand:newHand, assets:p.assets.map(function(a){
          return a.id===pc.assetId ? { ...a, lockedCard:rtp_card, tokens:rtp_toks } : a;
        }) };
      });
      if (rtp_oldCard) ns = addLog(ns, '🔄 "'+rtp_oldCard.name+'" returned to hand (replaced on Retainer).', 'asset');
      ns = addLog(ns, "🤝 "+pname(ns,pc.srcPlayer)+' stores "'+rtp_card.name+'" on Retainer ('+rtp_uses+' uses).', "asset");
      break;
    }
    case "TRADE_IN_SELECT": {
      var ti_src = pc.srcPlayer, ti_gain = pc.tradeInAmount||6;
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") {
        if (choiceId==="auto") ns=addLog(ns,"⏱ Trade-In timed out - paying out without discard.","warn");
        ns = addMoney(ns, ti_src, ti_gain, "Trade-In Value"); return maybeFlush(ns);
      }
      var ti_parts = choiceId.split(":"); var ti_actId=ti_parts[0], ti_rxId=ti_parts[1];
      var ti_pl2 = ns.players.find(function(p){return p.id===ti_src;});
      var ti_ac = ti_pl2&&ti_pl2.hand.find(function(c){return c.id===ti_actId;});
      var ti_rx = ti_pl2&&ti_pl2.hand.find(function(c){return c.id===ti_rxId;});
      if (!ti_ac||!ti_rx) { ns=addMoney(ns,ti_src,ti_gain,"Trade-In Value"); return maybeFlush(ns); }
      ns = setPl(ns,ti_src,function(p){return {...p,hand:p.hand.filter(function(c){return c.id!==ti_actId&&c.id!==ti_rxId;})};});
      ns = {...ns,mainDiscard:[...ns.mainDiscard,{...ti_ac,value:ti_ac.origVal,modifiedBy:[]},{...ti_rx,value:ti_rx.origVal,modifiedBy:[]}]};
      ns = addLog(ns,"🗑 "+pname(ns,ti_src)+' discards "'+ti_ac.name+'" and "'+ti_rx.name+'" (Trade-In).', "discard");
      ns = addMoney(ns, ti_src, ti_gain, "Trade-In Value");
      return maybeFlush(ns);
    }
    case "CREDIT_LINE_CHOICE": {
      var cl_src=pc.srcPlayer;
      if (!choiceId||choiceId==="auto") { ns=addLog(ns,pname(ns,cl_src)+"'s Credit Line timed out.","warn"); break; }
      var cl_remove=Math.min(parseInt(choiceId,10),pc.currentTokens||0);
      if (isNaN(cl_remove)||cl_remove<1) { ns=addLog(ns,"Credit Line: invalid choice.","warn"); break; }
      var cl_gain=cl_remove*(pc.clValue||2), cl_remaining=(pc.currentTokens||0)-cl_remove;
      ns = setPl(ns,cl_src,function(p){return {...p,assets:p.assets.map(function(a){
        if(a.id!==pc.assetId)return a;
        var newToks=(a.tokens||[]).slice(cl_remove);
        return {...a,tokens:newToks,status:newToks.length>0?"READY":a.status};
      })};});
      ns = addMoney(ns,cl_src,cl_gain,"Credit Line ("+cl_remove+" token"+(cl_remove!==1?"s":"")+")");
      ns = addLog(ns,"💳 "+pname(ns,cl_src)+" removes "+cl_remove+" token"+(cl_remove!==1?"s":"")+" - gains $"+cl_gain+"k!"+(cl_remaining>0?" ("+cl_remaining+" left)":""),"asset");
      break;
    }
    case "RND_ACTIVATE": {
      var rnd_src=pc.srcPlayer, rnd_draw=pc.drawCount||2;
      ns = setPl(ns,rnd_src,function(p){return {...p,rndActive:true,_rndActivatedThisTurn:true,assets:p.assets.map(function(a){return a.id===pc.assetId?{...a,tokens:[],status:ST.USED}:a;})};});
      ns = drawN(ns,rnd_src,rnd_draw,0,true);
      ns = addLog(ns,"🔬 "+pname(ns,rnd_src)+" activates R&D Budget - draws "+rnd_draw+" cards! Actions free this turn (hand discards at turn end).","asset");
      break;
    }
    case "MONOPOLY_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Monopoly: no card placed.","info"); break; }
      var mono_card = ns.players.find(function(p){return p.id===pc.srcPlayer;})?.hand.find(function(c){return c.id===choiceId;});
      if (!mono_card) break;
      ns = setPl(ns,pc.srcPlayer,function(p){return {...p,hand:p.hand.filter(function(c){return c.id!==choiceId;}),assets:p.assets.map(function(a){
        return a.id===pc.assetId?{...a,lockedCard:mono_card}:a;
      })};});
      ns = {...ns,mainDiscard:[...ns.mainDiscard,{...mono_card,value:mono_card.origVal,modifiedBy:[]}]};
      ns = addLog(ns,"🚫 "+pname(ns,pc.srcPlayer)+' places "'+mono_card.name+'" on Monopoly - $'+mono_card.value+'k cards now blocked!', "asset");
      break;
    }
    case "SNEAK_PEAK_SELECT": {
      var sp_cards = pc.cards || [];
      if (sp_cards.length < 2 || !choiceId || choiceId === "auto" || choiceId === "cancel") {
        // No choice or only 1 card — keep original order
        ns = addLog(ns, pname(ns,pc.srcPlayer)+" returns the cards in their original order (Sneak Peak).", "info");
        break;
      }
      // choiceId = id of the card to place ON TOP
      var sp_first = sp_cards.find(function(c){ return c.id===choiceId; });
      var sp_second = sp_cards.find(function(c){ return c.id!==choiceId; });
      if (!sp_first) break;
      var sp_rest = ns.mainDeck.slice(sp_cards.length);
      var sp_order = sp_second ? [sp_first, sp_second] : [sp_first];
      ns = { ...ns, mainDeck:[...sp_order, ...sp_rest] };
      ns = addLog(ns, "🔍 "+pname(ns,pc.srcPlayer)+" reorders the top of the deck (Sneak Peak).", "asset");
      break;
    }
    case "BOGO_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"BOGO: cancelled.","info"); break; }
      var bogo_tgt = ns.players.find(function(p){return p.id===pc.srcPlayer;})?.assets.find(function(a){return a.id===choiceId;});
      if (!bogo_tgt) break;
      var bogo_copy = {...bogo_tgt,id:pc.bogoAssetId,status:ST.READY,tokens:[],disabled:false,_isBogo:true};
      ns = setPl(ns,pc.srcPlayer,function(p){return {...p,assets:p.assets.map(function(a){return a.id===pc.bogoAssetId?bogo_copy:a;})};});
      ns = addLog(ns,"🎁 "+pname(ns,pc.srcPlayer)+" copies "+bogo_tgt.name+' with BOGO!', "asset");
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "FRANCHISE_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Franchise: cancelled.","info"); break; }
      ns = setPl(ns,pc.srcPlayer,function(p){return {...p,assets:p.assets.map(function(a){
        if(a.id===choiceId)return {...a,_franchised:true};
        if(a.id===pc.franAssetId)return {...a,disabled:true,status:ST.USED};
        return a;
      })};});
      var fran_asset = ns.players.find(function(p){return p.id===pc.srcPlayer;})?.assets.find(function(a){return a.id===choiceId;});
      ns = addLog(ns,"⚡ "+pname(ns,pc.srcPlayer)+" franchises "+( fran_asset?'"'+fran_asset.name+'"':"an asset")+"! Effect doubled permanently.","asset");
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "EXCHANGE_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Exchange: cancelled.","info"); break; }
      var ex_asset = ns.shop.find(function(a){return a.id===choiceId;});
      if (!ex_asset) break;
      var ex_newShop = ns.shop.filter(function(a){return a.id!==choiceId;});
      ns = setPl(ns,pc.srcPlayer,function(p){return {...p,assets:p.assets.filter(function(a){return a.id!==pc.thisAssetId;}).concat([{...ex_asset,status:ST.READY,tokens:[],disabled:false}])};});
      // Discard the old asset if one was exchanged
      if(pc.thisAssetId){
        var ex_old=ns.players.find(function(p){return p.id===pc.srcPlayer;});
        ex_old = ex_old && ex_old.assets.find(function(a){return a.id===pc.thisAssetId;});
        if(ex_old) ns={...ns,assetDiscard:[...ns.assetDiscard,{...ex_old,status:ST.READY,tokens:[],disabled:false}]};
      }
      // Refill shop from asset deck
      var ex_reshuffle = tryReshuffle(ns.assetDeck, ns.assetDiscard);
      if (!ex_reshuffle.empty) {
        if (ex_reshuffle.reshuffled) ns = addLog(ns, "🔀 Asset deck reshuffled.", "sys");
        ex_newShop = [...ex_newShop, ex_reshuffle.deck[0]];
        ns = { ...ns, assetDeck:ex_reshuffle.deck.slice(1), assetDiscard:ex_reshuffle.reshuffled?[]:ex_reshuffle.disc };
      }
      ns = {...ns, shop:ex_newShop};
      ns = addLog(ns,"🔄 "+pname(ns,pc.srcPlayer)+' takes "'+ex_asset.name+'" for free (Exchange).', "asset");
      ns = checkAssetDeckExhausted(ns);
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "EYE_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Eye For An Eye: cancelled.","info"); break; }
      if (choiceId==="back") {
        return { ...ns, pendingChoice:{ ...pc, phase:"opponent", assets:[], selectedOpponent:null, timerEnd:Date.now()+20000 } };
      }
      if (pc.phase==="opponent") {
        var eye_opp = ns.players.find(function(p){return p.id===choiceId;});
        if (!eye_opp) break;
        return {...ns, pendingChoice:{...pc,phase:"asset",selectedOpponent:choiceId,assets:eye_opp.assets.filter(function(a){return !a.disabled;}),timerEnd:Date.now()+20000}};
      }
      return maybeFlush(applyHook(ns,"apply_eye_for_eye",{srcPlayer:pc.srcPlayer,tgtPlayer:pc.selectedOpponent,value:choiceId,srcCard:pc.thisAssetId?{id:pc.thisAssetId}:null}));
    }
    case "BTB_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Back to Basics: cancelled.","info"); break; }
      if (pc.phase==="opponent") {
        var btb_opp = ns.players.find(function(p){return p.id===choiceId;});
        if (!btb_opp||btb_opp.assets.length<3) break;
        return {...ns, pendingChoice:{...pc,phase:"asset",selectedOpponent:choiceId,assets:btb_opp.assets,timerEnd:Date.now()+20000}};
      }
      var btb_ns = maybeFlush(applyHook(ns,"apply_btb",{srcPlayer:pc.srcPlayer,tgtPlayer:pc.selectedOpponent,value:choiceId}));
      // Resume any outer targeting state that was paused while BTB resolved
      if (pc._letGoState && pc._letGoState.phase === "targeting") {
        var btb_lgs = btb_ns.letGoState || pc._letGoState;
        var btb_confirmed = [...(btb_lgs.confirmedTargets||[]), btb_lgs.currentTarget].filter(Boolean);
        btb_ns = { ...btb_ns, letGoState:{ ...btb_lgs, currentTarget:null, confirmedTargets:btb_confirmed } };
        return advanceLetGoTargeting(btb_ns);
      }
      if (pc._pcState && pc._pcState.phase === "targeting") {
        var btb_pcs = btb_ns.pocketChangeState || pc._pcState;
        btb_ns = { ...btb_ns, pocketChangeState:{ ...btb_pcs, currentTarget:null, confirmedTargets:[...(btb_pcs.confirmedTargets||[]),btb_pcs.currentTarget].filter(Boolean) } };
        return advancePocketChangeTargeting(btb_ns);
      }
      if (pc._negState && pc._negState.phase === "targeting") {
        var btb_nrs = btb_ns.negReactionState || pc._negState;
        btb_ns = { ...btb_ns, negReactionState:{ ...btb_nrs, currentTarget:null, confirmedTargets:[...(btb_nrs.confirmedTargets||[]),btb_nrs.currentTarget].filter(Boolean) } };
        return advanceNegReactionTargeting(btb_ns);
      }
      return btb_ns;
    }
    case "SF_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"A Small Fee: cancelled.","info"); break; }
      if (pc.phase==="opponent") {
        var sf_opp = ns.players.find(function(p){return p.id===choiceId;});
        if (!sf_opp||sf_opp.assets.length<2) break;
        return {...ns, pendingChoice:{...pc,phase:"asset",selectedOpponent:choiceId,assets:sf_opp.assets,timerEnd:Date.now()+20000}};
      }
      // Push TARGET_PLAYER so target can react (e.g. Not A Chance)
      var sf_tgt2 = pc.selectedOpponent;
      var sf_ns2 = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:sf_tgt2, label:"A Small Fee" });
      var sf_trig2 = sf_ns2.triggerStack[sf_ns2.triggerStack.length-1];
      var sf_react2 = reactorsFor(sf_ns2.players, sf_trig2);
      var sf_pe2 = { hook:"apply_small_fee", srcPlayer:pc.srcPlayer, tgtPlayer:sf_tgt2, value:choiceId };
      sf_ns2 = { ...sf_ns2, pendingEffect:sf_pe2 };
      if (sf_react2.some(function(r){ return r.canReact; })) {
        return { ...sf_ns2, reactionWindow:buildWindow(uid(), sf_trig2.tid, T.TARGET_PLAYER, pc.srcPlayer, sf_tgt2, sf_react2) };
      }
      sf_ns2 = { ...sf_ns2, pendingEffect:null };
      return maybeFlush(applyHook(sf_ns2, "apply_small_fee", sf_pe2));
    }
    case "AP_ACTIVATE_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Action Plan: cancelled.","info"); break; }
      if (choiceId==="add_token") {
        ns=setPl(ns,pc.srcPlayer,function(p){return {...p,assets:p.assets.map(function(a){return a.id===pc.assetId?{...a,tokens:[...(a.tokens||[]),{id:uid()}]}:a;})};});
        ns=addLog(ns,"📋 "+pname(ns,pc.srcPlayer)+"'s Action Plan: token added ("+(pc.currentTokens+1)+"/"+(pc.maxTokens||5)+").","asset");
        break;
      }
      if (typeof choiceId === "string" && choiceId.startsWith("place:")) {
        var ap_placeId = choiceId.slice(6);
        var ap_placer  = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
        var ap_placing = ap_placer && ap_placer.hand.find(function(c){ return c.id===ap_placeId; });
        if (!ap_placing) { ns=addLog(ns,"Action Plan: card not found.","warn"); break; }
        ns = setPl(ns, pc.srcPlayer, function(p){ return {
          ...p,
          hand:p.hand.filter(function(c){ return c.id!==ap_placeId; }),
          assets:p.assets.map(function(a){
            // Placing a card is free: restore READY so the asset can still be activated this turn
            return a.id===pc.assetId ? {...a, lockedCard:ap_placing, status:ST.READY} : a;
          })
        }; });
        ns = addLog(ns, "📋 "+pname(ns,pc.srcPlayer)+" stores \""+ap_placing.name+"\" on Action Plan (free).", "asset", [ap_placing]);
        break;
      }
      if (choiceId==="fire") {
        var ap_asset2=ns.players.find(function(p){return p.id===pc.srcPlayer;})?.assets.find(function(a){return a.id===pc.assetId;});
        if (!ap_asset2||!ap_asset2.lockedCard) { ns=addLog(ns,"Action Plan: no stored card.","warn"); break; }
        var ap_cost=ap_asset2.lockedCard.value||0;
        var ap_have=(ap_asset2.tokens||[]).length;
        if (ap_have<ap_cost) { ns=addLog(ns,"Action Plan: need "+ap_cost+" tokens, have "+ap_have+".","warn"); break; }
        var ap_remaining=ap_have-ap_cost;
        ns=setPl(ns,pc.srcPlayer,function(p){return {...p,assets:p.assets.map(function(a){
          return a.id===pc.assetId?{...a,tokens:(a.tokens||[]).slice(ap_cost)}:a;
        })};});
        var ap_card=ap_asset2.lockedCard;
        ns=addLog(ns,"📋 "+pname(ns,pc.srcPlayer)+' fires "'+ap_card.name+'" from Action Plan! ('+ap_remaining+' tokens left)',"asset",[ap_card]);
        ns=applyHook(ns,ap_card.hook,{srcPlayer:pc.srcPlayer,srcCard:ap_card});
        return maybeFlush(ns);
      }
      break;
    }
    case "BP_TARGET_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") {
        ns = addLog(ns, pname(ns,pc.srcPlayer)+"'s Business Partners: no target, effect fizzles.", "info"); break;
      }
      var bpts_card = pc.card;
      if (!bpts_card) break;
      ns = applyHook(ns, bpts_card.hook, { srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:bpts_card.value, srcCard:bpts_card });
      break; // resolveChoice's final maybeFlush handles continuation
    }
    case "FULL_REFUND_PROMPT": {
      if (!choiceId || choiceId === "skip") {
        ns = addLog(ns, pname(ns,pc.srcPlayer)+" skips Full Refund.", "info");
        // Discard the sold card now (was deferred from resolveSell)
        if (pc.card) ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...pc.card, value:pc.card.origVal||pc.card.value, modifiedBy:[] }] };
        break;
      }
      if (choiceId === "activate") {
        var frp_card = pc.card;
        if (!frp_card) break;
        // Targeted cards need a player selection before the hook can fire
        if (frp_card.target === "opponent") {
          var frp_opps = ns.players.filter(function(p){
            return p.id !== pc.srcPlayer && (frp_card.hook === "help_wanted" || frp_card.hook === "client_theft" ? p.hand.length > 0 : true);
          });
          if (!frp_opps.length) {
            ns = addLog(ns, pname(ns,pc.srcPlayer)+": No valid targets for \""+frp_card.name+"\" (Full Refund).", "warn");
            ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...frp_card, value:frp_card.origVal||frp_card.value, modifiedBy:[] }] };
            break;
          }
          return { ...ns, pendingChoice:{ type:"FULL_REFUND_TARGET", srcPlayer:pc.srcPlayer,
            card:frp_card, activationsLeft:(pc.activationsLeft||1),
            timerEnd:Date.now()+20000,
            prompt:pname(ns,pc.srcPlayer)+": choose a target for \""+frp_card.name+"\" (Full Refund).",
            options:frp_opps.map(function(p){ return { id:p.id, name:p.name }; }) }};
        }
        ns = addLog(ns, "✅ "+pname(ns,pc.srcPlayer)+" activates \""+frp_card.name+"\" via Full Refund!", "action", [frp_card]);
        // Apply the card's effect BEFORE discarding so cards like Dumpster Diving/Used Goods
        // don't see the sold card already sitting in the discard pile
        ns = applyHook(ns, frp_card.hook, { srcPlayer:pc.srcPlayer, tgtPlayer:null, value:frp_card.value, srcCard:frp_card });
        // Now discard the card (after effect has fired)
        ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...frp_card, value:frp_card.origVal||frp_card.value, modifiedBy:[] }] };
        var frp_left = (pc.activationsLeft||1) - 1;
        if (frp_left > 0) return { ...ns, pendingChoice:{ ...pc, activationsLeft:frp_left, timerEnd:Date.now()+15000 } };
        return maybeFlush(ns);
      }
      break;
    }
    case "FULL_REFUND_TARGET": {
      // Player has chosen a target for a targeted card activated via Full Refund
      if (!choiceId || choiceId === "cancel" || choiceId === "auto") {
        ns = addLog(ns, pname(ns,pc.srcPlayer)+" cancels Full Refund target selection.", "info");
        if (pc.card) ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...pc.card, value:pc.card.origVal||pc.card.value, modifiedBy:[] }] };
        break;
      }
      var frt_card = pc.card;
      if (!frt_card) break;
      ns = addLog(ns, "✅ "+pname(ns,pc.srcPlayer)+" activates \""+frt_card.name+"\" -> "+pname(ns,choiceId)+" (Full Refund)!", "action", [frt_card]);
      ns = applyHook(ns, frt_card.hook, { srcPlayer:pc.srcPlayer, tgtPlayer:choiceId, value:frt_card.value, srcCard:frt_card });
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...frt_card, value:frt_card.origVal||frt_card.value, modifiedBy:[] }] };
      return maybeFlush(ns);
    }
    case "COMING_SOON_PEEK": {
      ns=addLog(ns,"🔍 "+pname(ns,pc.srcPlayer)+" peeked at the top asset deck card.","asset");
      break;
    }
    case "WTP_SELECT": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") {
        ns=addLog(ns,"Worth The Price: cancelled.","info");
        if (pc.savedWindow) { ns={...ns,reactionWindow:pc.savedWindow}; return reducer(ns,{type:"ENG_PASS",pid:pc.srcPlayer}); }
        break;
      }
      var wtp_ids=choiceId.split(",");
      var wtp_pl2=ns.players.find(function(p){return p.id===pc.srcPlayer;});
      var wtp_cards=wtp_pl2?wtp_pl2.hand.filter(function(c){return wtp_ids.indexOf(c.id)>=0;}):[];
      var wtp_total=wtp_cards.reduce(function(s,c){return s+(c.value||0);},0);
      if (wtp_total<=(pc.trigVal||0)) { ns=addLog(ns,"Worth The Price: total "+wtp_total+" not > "+pc.trigVal+".","warn"); break; }
      wtp_cards.forEach(function(c){
        ns=setPl(ns,pc.srcPlayer,function(p){return {...p,hand:p.hand.filter(function(h){return h.id!==c.id;})};});
        ns={...ns,mainDiscard:[...ns.mainDiscard,{...c,value:c.origVal,modifiedBy:[]}]};
      });
      ns=addLog(ns,"💰 "+pname(ns,pc.srcPlayer)+" discards $"+wtp_total+"k worth of cards - cancels the effect! (Worth The Price)","reaction");
      // Cancel the triggering card's effect:
      // - pendingReactionCard: the reaction card WTP is countering (e.g. Competitor)
      // - pendingEffect: non-__resume_action__ effects (targeted action cards)
      var wtp_victim = (ns.pendingReactionCard && ns.pendingReactionCard.tgtPlayer) ||
                       (ns.pendingEffect && ns.pendingEffect.srcPlayer) ||
                       (ns.savedReactionWindow && ns.savedReactionWindow.srcPlayer) || null;
      ns = { ...ns, pendingReactionCard:null };
      if (ns.pendingEffect && ns.pendingEffect.hook !== "__resume_action__") ns={...ns,pendingEffect:null};
      if (wtp_victim && wtp_victim !== pc.srcPlayer) ns = checkTargetedAd(ns, pc.srcPlayer, wtp_victim);
      // Clear current (sub-)window but restore outer window if WTP fired during a sub-window
      ns={...ns,reactionWindow:null};
      if (ns.savedReactionWindow) {
        var wtp_outer=ns.savedReactionWindow;
        ns={...ns,savedReactionWindow:null,reactionWindow:wtp_outer};
      }
      break;
    }
    case "TOTAL_LOSS_PICK_VALUE": {
      if (!choiceId||choiceId==="auto") { ns=addLog(ns,"Total Loss: timed out.","warn"); break; }
      var tlv=parseInt(choiceId,10);
      if (isNaN(tlv)||tlv<1||tlv>5) { ns=addLog(ns,"Total Loss: invalid value.","warn"); break; }
      var tl_opps=ns.players.filter(function(p){return p.id!==pc.srcPlayer;});
      var tl_targets=tl_opps.filter(function(p){return p.hand.some(function(c){return c.origVal===tlv;});});
      if (!tl_targets.length) { ns=addLog(ns,"Total Loss: no opponents have $"+tlv+"k cards.","info"); break; }
      tl_targets.forEach(function(p){
        var tl_cards=p.hand.filter(function(c){return c.origVal===tlv;});
        ns=setPl(ns,p.id,function(pl){return {...pl,hand:pl.hand.filter(function(c){return c.origVal!==tlv;})};});
        tl_cards.forEach(function(c){
          ns={...ns,mainDiscard:[...ns.mainDiscard,{...c,value:c.origVal,modifiedBy:[]}]};
          ns=addLog(ns,"🗑 "+pname(ns,p.id)+' discards "'+c.name+'" (Total Loss).', "discard",[c]);
          ns=pushTrigger(ns,{type:T.DISCARD_CARD,srcPlayer:pc.srcPlayer,tgtPlayer:p.id});
        });
        ns=payMoney(ns,p.id,pc.srcPlayer,tl_cards.length,"Total Loss ("+tl_cards.length+" x $1k)");
      });
      break;
    }
    case "PAID_WORK_PROMPT": {
      // choiceId: "up" | "down" | "auto"/"cancel" (skip)
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        // Skip - PW stays READY, nothing changes
        break;
      }
      var pw_dir = choiceId === "up" ? 1 : -1;
      var pw_owner_pl = ns.players.find(function(p){ return p.id===pc.ownerId; });
      var pw_a = pw_owner_pl && pw_owner_pl.assets.find(function(a){ return a.id===pc.assetId; });
      if (!pw_a) break;
      var pw_adj = (pc.pwAmount||2) * pw_dir;
      var pw_newAmt;
      if (pc.isGain) {
        // Adjusting a gain: can't go below 0
        pw_newAmt = Math.max(0, pc.amount + pw_adj);
      } else {
        // Adjusting a loss: can't go below 0 (can't turn a loss into a gain)
        pw_newAmt = Math.max(0, pc.amount + pw_adj);
      }
      var pw_diff = pw_newAmt - pc.amount; // actual change (may be < pw_adj if clamped)
      // Mark Paid Work as USED
      ns = setPl(ns, pc.ownerId, function(p){ return {...p, assets:p.assets.map(function(a){
        return a.id===pc.assetId ? {...a, status:ST.USED} : a;
      })}; });
      var pw_dir_str = pw_dir > 0 ? "+" : "";
      ns = addLog(ns, "💼 "+pname(ns,pc.ownerId)+"'s Paid Work: "+(pc.isGain?"gain":"loss")+" adjusted "+pw_dir_str+pw_diff+"k ("+$(pc.amount)+" → "+$(pw_newAmt)+").", "asset");
      // Apply the difference: adjust the affected player's money directly
      if (pw_diff !== 0) {
        // For gains: give/take directly; for losses: reduce/increase the loss
        var pw_delta = pc.isGain ? pw_diff : -pw_diff; // positive diff on gain = more money; positive diff on loss = less money lost
        ns = setPl(ns, pc.affectedPid, function(p){ return {...p,
          money:p.money + pw_delta,
          totalGained:(p.totalGained||0) + (pw_delta>0?pw_delta:0),
          totalLost:(p.totalLost||0) + (pw_delta<0?Math.abs(pw_delta):0),
        }; });
        ns = { ...ns, moneyEvents:[...(ns.moneyEvents||[]), { id:uid(), pid:pc.affectedPid,
          lines:[{amount:pw_delta, reason:"Paid Work adjustment"}], total:pw_delta }] };
      }
      return maybeFlush(ns);
    }
    case "PU_ASSET_SELECT": {
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        ns=addLog(ns,"Product Update: no asset selected - marked used.","info"); break;
      }
      // Find the chosen asset
      var pu_src_pl = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      var pu_tgt_a = pu_src_pl && pu_src_pl.assets.find(function(a){ return a.id===choiceId; });
      if (!pu_tgt_a || !PU_FIELDS[pu_tgt_a.hook]) { ns=addLog(ns,"Product Update: invalid asset.","warn"); break; }
      var pu_fields = PU_FIELDS[pu_tgt_a.hook];
      if (pu_fields.length === 1) {
        // Only one field — skip field selection, go to direction
        return {...ns, pendingChoice:{ type:"PU_DIRECTION_SELECT", srcPlayer:pc.srcPlayer,
          puAssetId:pc.puAssetId, puAmount:pc.puAmount, timerEnd:Date.now()+5000,
          targetAssetId:pu_tgt_a.id, targetField:pu_fields[0].key, targetFieldLabel:pu_fields[0].label, fieldFloor:pu_fields[0].floor,
          targetAssetName:pu_tgt_a.name }};
      }
      // Multiple fields — show picker
      return {...ns, pendingChoice:{ type:"PU_FIELD_SELECT", srcPlayer:pc.srcPlayer,
        puAssetId:pc.puAssetId, puAmount:pc.puAmount, timerEnd:Date.now()+10000,
        targetAssetId:pu_tgt_a.id, targetAssetName:pu_tgt_a.name,
        fields:pu_fields, options:pu_fields }};
    }
    case "PU_FIELD_SELECT": {
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        ns=addLog(ns,"Product Update: no field selected - marked used.","info"); break;
      }
      var pu_field_entry = (pc.fields||[]).find(function(f){ return f.key===choiceId; });
      if (!pu_field_entry) { ns=addLog(ns,"Product Update: invalid field.","warn"); break; }
      return {...ns, pendingChoice:{ type:"PU_DIRECTION_SELECT", srcPlayer:pc.srcPlayer,
        puAssetId:pc.puAssetId, puAmount:pc.puAmount, timerEnd:Date.now()+10000,
        targetAssetId:pc.targetAssetId, targetField:pu_field_entry.key,
        targetFieldLabel:pu_field_entry.label, fieldFloor:pu_field_entry.floor,
        targetAssetName:pc.targetAssetName }};
    }
    case "PU_DIRECTION_SELECT": {
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        ns=addLog(ns,"Product Update: no direction selected - marked used.","info"); break;
      }
      var pu_dir2 = choiceId==="up" ? 1 : -1;
      var pu_pl3 = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      var pu_a3 = pu_pl3 && pu_pl3.assets.find(function(a){ return a.id===pc.puAssetId; });
      var pu_tgt3 = pu_pl3 && pu_pl3.assets.find(function(a){ return a.id===pc.targetAssetId; });
      if (!pu_a3 || !pu_tgt3) { ns=addLog(ns,"Product Update: assets not found.","warn"); break; }
      var pu_delta3 = (pc.puAmount||1) * pu_dir2;
      var pu_floor3 = pc.fieldFloor !== null && pc.fieldFloor !== undefined ? pc.fieldFloor : 0;
      var pu_field3 = pc.targetField;
      // Apply the change
      ns = setPl(ns, pc.srcPlayer, function(p){ return {...p, assets:p.assets.map(function(a){
        if (a.id !== pc.targetAssetId) return a;
        var updated = {...a, _puSourceId:pc.puAssetId};
        if (pu_field3 === "_creditLineAll") {
          updated = {...updated,
            clValue:Math.max(pu_floor3, (a.clValue||2)+pu_delta3),
            clPrice1:Math.max(pu_floor3, (a.clPrice1||1)+pu_delta3),
            clPrice2:Math.max(pu_floor3, (a.clPrice2||3)+pu_delta3),
            clPrice3:Math.max(pu_floor3, (a.clPrice3||6)+pu_delta3)
          };
        } else {
          var pu_cur3 = a[pu_field3] !== undefined ? a[pu_field3] : 0;
          updated = {...updated, [pu_field3]: Math.max(pu_floor3, pu_cur3+pu_delta3)};
        }
        return updated;
      }) }; });
      // Mark PU as disabled + store _appliedTo
      var pu_sign3 = pu_delta3 >= 0 ? "+" : "";
      ns = setPl(ns, pc.srcPlayer, function(p){ return {...p, assets:p.assets.map(function(a){
        return a.id===pc.puAssetId ? {...a, status:ST.USED, disabled:true, _appliedTo:{
          assetId:pc.targetAssetId, playerId:pc.srcPlayer, field:pu_field3, delta:pu_delta3
        }} : a;
      }) }; });
      ns = addLog(ns,"🔧 "+pname(ns,pc.srcPlayer)+" uses Product Update: \""+pc.targetAssetName+"\" "+pc.targetFieldLabel+" "+pu_sign3+pu_delta3+"!","asset");
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "REBRANDING_SELECT": {
      // choiceId = comma-separated asset ids, or "none"/"auto"/"cancel" for no selection
      var rb_ids = (choiceId && choiceId !== "auto" && choiceId !== "cancel" && choiceId !== "none")
        ? choiceId.split(",").filter(Boolean) : [];
      var rb_src_pl = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      // Validate selected assets (must be owned, not Rebranding itself)
      var rb_sel = rb_src_pl ? rb_ids.map(function(id){
        return rb_src_pl.assets.find(function(a){ return a.id===id && a.id!==pc.rebrandAssetId; });
      }).filter(Boolean) : [];
      var rb_count = rb_sel.length + (pc.rebrandBonus||1); // total assets to receive
      // 1. Remove selected assets from owner's hand + Rebranding itself
      var rb_all_remove = new Set([...rb_sel.map(function(a){return a.id;}), pc.rebrandAssetId].filter(Boolean));
      ns = setPl(ns, pc.srcPlayer, function(p){ return {...p, assets:p.assets.filter(function(a){ return !rb_all_remove.has(a.id); })}; });
      // 2. Shuffle selected assets + Rebranding back into asset deck
      var rb_rb_asset = rb_src_pl && pc.rebrandAssetId ? rb_src_pl.assets.find(function(a){ return a.id===pc.rebrandAssetId; }) : null;
      var rb_to_shuffle = [...rb_sel];
      if (rb_rb_asset) rb_to_shuffle.push(rb_rb_asset);
      // Revert any PU effects among returned assets
      rb_to_shuffle.forEach(function(a) {
        if (a.hook==="a_product_update" && a._appliedTo) ns = revertPUEffect(ns, a);
        if (a._puSourceId) {
          ns.players.forEach(function(p) {
            var pu2 = p.assets.find(function(x){ return x.id===a._puSourceId; });
            if (pu2) ns = setPl(ns, p.id, function(pl){ return {...pl, assets:pl.assets.map(function(x){ return x.id===pu2.id ? {...x,_appliedTo:null} : x; })}; });
          });
        }
      });
      // Strip to base template (remove runtime flags)
      var rb_clean = rb_to_shuffle.map(function(a){ return {...a,status:ST.READY,tokens:[],disabled:false,_franchised:false,_isBogo:false,extraIncomeTarget:null,riskRewardTarget:null,letsDealTarget:null}; });
      ns = { ...ns, assetDeck:shuf([...ns.assetDeck, ...rb_clean]) };
      var rb_names = rb_to_shuffle.map(function(a){ return '"'+a.name+'"'; }).join(", ");
      ns = addLog(ns, "🔄 "+pname(ns,pc.srcPlayer)+" rebrands: "+rb_names+" shuffled back into deck. Drawing "+rb_count+" free assets!", "asset");
      // 3. Draw rb_count assets from top of deck
      var rb_gained = [];
      for (var rbi=0; rbi<rb_count; rbi++) {
        var rb_res = tryReshuffle(ns.assetDeck, ns.assetDiscard);
        if (rb_res.empty) { ns=addLog(ns,"Rebranding: asset deck empty, can't draw more.","warn"); break; }
        if (rb_res.reshuffled) ns={...ns,assetDeck:rb_res.deck,assetDiscard:[]};
        var rb_new = rb_res.deck[0];
        var rb_toks = rb_new.hook==="a_prospects"?[{id:uid()},{id:uid()},{id:uid()}]:[];
        var rb_cl_toks = rb_new.hook==="a_credit_line"?Array.from({length:rb_new.tmax||3},function(){return{id:uid()};}):rb_toks;
      ns = setPl(ns,pc.srcPlayer,function(p){ return {...p,assets:[...p.assets,{...rb_new,status:ST.READY,tokens:rb_cl_toks,disabled:false}]}; });
        ns = {...ns, assetDeck:rb_res.deck.slice(1)};
        rb_gained.push(rb_new);
        ns = addLog(ns,"🎁 "+pname(ns,pc.srcPlayer)+' gains "'+rb_new.name+'" ($'+rb_new.origVal+'k) from Rebranding!',"asset",[rb_new]);
      }
      ns = checkAssetDeckExhausted(ns);
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "MARKUP_DIRECTION": {
      // choiceId = "increase" | "decrease" | "auto" (auto = increase)
      var mu_dir = (choiceId === "decrease") ? -1 : 1;
      var mu_pl2 = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      var mu_hand2 = mu_pl2 ? mu_pl2.hand.filter(function(c){ return c.type !== CT.ASSET; }) : [];
      if (!mu_hand2.length) { ns=addLog(ns,"Mark-Up: no cards to modify.","warn"); break; }
      return { ...ns, pendingChoice:{
        type:"MARKUP_CARD", srcPlayer:pc.srcPlayer, markUpAmount:pc.markUpAmount||1, direction:mu_dir,
        timerEnd:Date.now()+20000, options:mu_hand2,
        prompt:pname(ns,pc.srcPlayer)+": pick a card to "+(mu_dir>0?"increase":"decrease")+" by $"+(pc.markUpAmount||1)+"k."
      }};
    }
    case "MARKUP_CARD": {
      var mu_opts = pc.options || [];
      var mu_chosen = (choiceId==="auto"||!choiceId)
        ? (mu_opts.length ? mu_opts[Math.floor(_rand()*mu_opts.length)] : null)
        : mu_opts.find(function(c){ return c.id===choiceId; });
      if (!mu_chosen) { ns=addLog(ns,"Mark-Up: no card selected.","info"); break; }
      var mu_delta = (pc.markUpAmount||1) * (pc.direction||1);
      var mu_auto = (choiceId==="auto"||!choiceId) ? " (auto)" : "";
      ns = setPl(ns, pc.srcPlayer, function(p){ return {...p, hand:p.hand.map(function(c){
        return c.id===mu_chosen.id ? {...c, _markupDelta:(c._markupDelta||0)+mu_delta} : c;
      })}; });
      ns = applyAllHandBoosts(ns);
      var mu_sign = mu_delta >= 0 ? "+" : "";
      ns = addLog(ns, "📊 "+pname(ns,pc.srcPlayer)+" Mark-Up: \""+mu_chosen.name+"\" "+mu_sign+mu_delta+"k until turn end"+mu_auto+".", "action");
      return maybeFlush(ns);
    }
    case "BRIBERY_ASSET_SELECT": {
      // Step 1: owner selects which asset to steal
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        ns=addLog(ns,"Bribery: no asset selected - marked used.","info"); break;
      }
      var brb_tgt_asset = null, brb_tgt_owner_id = null;
      ns.players.forEach(function(p){
        var found = p.assets.find(function(a){ return a.id===choiceId; });
        if (found && p.id !== pc.srcPlayer) { brb_tgt_asset=found; brb_tgt_owner_id=p.id; }
      });
      if (!brb_tgt_asset || !brb_tgt_owner_id) { ns=addLog(ns,"Bribery: target not found.","warn"); break; }
      var brb_threshold = brb_tgt_asset.origVal + (pc.currentTokens||0)*(pc.bribeTokenCost||1);
      // Step 2: show hand cards for discard selection
      var brb_src_pl = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      var brb_hand_opts = brb_src_pl ? brb_src_pl.hand.filter(function(c){ return c.type!==CT.ASSET; }) : [];
      return { ...ns, pendingChoice:{ type:"BRIBERY_CARD_SELECT", srcPlayer:pc.srcPlayer,
        assetId:pc.assetId, bribeTokenCost:pc.bribeTokenCost, currentTokens:pc.currentTokens,
        targetAssetId:brb_tgt_asset.id, targetAssetName:brb_tgt_asset.name,
        targetAssetVal:brb_tgt_asset.origVal, targetOwnerId:brb_tgt_owner_id,
        threshold:brb_threshold, timerEnd:Date.now()+20000, options:brb_hand_opts }};
    }
    case "BRIBERY_CARD_SELECT": {
      // Step 2: owner selects cards to discard (must total > threshold)
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        ns=addLog(ns,"Bribery: no cards selected - marked used.","info"); break;
      }
      var brb_card_ids = choiceId.split(",");
      var brb_src_pl2 = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      var brb_sel_cards = brb_src_pl2 ? brb_card_ids.map(function(id){
        return brb_src_pl2.hand.find(function(c){ return c.id===id; });
      }).filter(Boolean) : [];
      var brb_total = brb_sel_cards.reduce(function(s,c){ return s+(c.value||0); }, 0);
      if (brb_total <= (pc.threshold||0)) {
        ns=addLog(ns,"Bribery: selected cards ($"+brb_total+"k) don't exceed required $"+pc.threshold+"k - cancelled.","warn"); break;
      }
      // Cards selected but NOT yet discarded - push TARGET_PLAYER first
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:pc.targetOwnerId, label:"Bribery", _fromAsset:true });
      var brb_trig = ns.triggerStack[ns.triggerStack.length-1];
      var brb_reactors = reactorsFor(ns.players, brb_trig);
      var brb_effect = { hook:"apply_bribery", srcPlayer:pc.srcPlayer, tgtPlayer:pc.targetOwnerId,
        value:pc.targetAssetId, srcCard:{ name:"Bribery" },
        bribeCardIds:brb_card_ids, assetId:pc.assetId,
        bribeTokenCost:pc.bribeTokenCost, currentTokens:pc.currentTokens };
      ns = {...ns, pendingEffect:brb_effect};
      if (brb_reactors.some(function(r){ return r.canReact; })) {
        return {...ns, reactionWindow:buildWindow(uid(),brb_trig.tid,T.TARGET_PLAYER,pc.srcPlayer,pc.targetOwnerId,brb_reactors)};
      }
      ns = {...ns, pendingEffect:null};
      return maybeFlush(applyHook(ns, "apply_bribery", brb_effect));
    }
    case "CFR_SELECT": {
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        ns=addLog(ns,"Closed for Remodeling: no selection - asset marked used.","info");
        break;
      }
      if (choiceId === "back") {
        // Back to opponent selection
        return {...ns, pendingChoice:{...pc, phase:"opponent", selectedOpponent:null, assets:[], timerEnd:Date.now()+20000}};
      }
      if (pc.phase==="opponent") {
        var cfr_opp = ns.players.find(function(p){ return p.id===choiceId; });
        if (!cfr_opp) break;
        var cfr_history = ns.cfrHistory||[];
        var cfr_eligible = cfr_opp.assets.filter(function(a){
          // Exclude disabled assets and assets this specific player used CFR on last turn
          return !a.disabled && !cfr_history.some(function(h){ return h.assetId===a.id && h.cfrOwnerId===pc.srcPlayer; });
        });
        if (!cfr_eligible.length) {
          ns=addLog(ns,"All of "+cfr_opp.name+"'s assets are either disabled or on cooldown.","warn");
          return {...ns,pendingChoice:{...pc,phase:"opponent",timerEnd:Date.now()+20000}};
        }
        return {...ns, pendingChoice:{...pc, phase:"asset", selectedOpponent:choiceId,
          assets:cfr_eligible, options:cfr_eligible, timerEnd:Date.now()+20000 }};
      }
      // Phase: asset
      var cfr_tgt_pl = ns.players.find(function(p){ return p.id===pc.selectedOpponent; });
      var cfr_tgt_asset = cfr_tgt_pl && (pc.assets||[]).find(function(a){ return a.id===choiceId; });
      if (!cfr_tgt_pl || !cfr_tgt_asset) { ns=addLog(ns,"CFR: invalid asset selection.","warn"); break; }
      // Push TARGET_PLAYER - Counteroffer can block
      ns = pushTrigger(ns, { type:T.TARGET_PLAYER, srcPlayer:pc.srcPlayer, tgtPlayer:cfr_tgt_pl.id, label:"Closed for Remodeling", _fromAsset:true });
      var cfr_trig = ns.triggerStack[ns.triggerStack.length-1];
      var cfr_reactors = reactorsFor(ns.players, cfr_trig);
      var cfr_effect = { hook:"apply_cfr", srcPlayer:pc.srcPlayer, tgtPlayer:cfr_tgt_pl.id,
        value:cfr_tgt_asset.id, srcCard:{ name:"Closed for Remodeling" } };
      ns = {...ns, pendingEffect:cfr_effect};
      if (cfr_reactors.some(function(r){ return r.canReact; })) {
        return {...ns, reactionWindow:buildWindow(uid(),cfr_trig.tid,T.TARGET_PLAYER,pc.srcPlayer,cfr_tgt_pl.id,cfr_reactors)};
      }
      ns = {...ns, pendingEffect:null};
      return maybeFlush(applyHook(ns,"apply_cfr",cfr_effect));
    }
    case "MULTI_TOOL_SELECT": {
      var mt_opts2 = pc.options || [];
      var mt_chosen = (choiceId === "auto" || !choiceId)
        ? (mt_opts2.length ? mt_opts2[Math.floor(_rand()*mt_opts2.length)] : null)
        : mt_opts2.find(function(a){ return a.id===choiceId; });
      if (!mt_chosen) { ns=addLog(ns,"Multi-Tool: no valid asset selected.","info"); break; }
      var mt_auto = (choiceId==="auto"||!choiceId) ? " (auto)" : "";
      ns = setPl(ns, pc.srcPlayer, function(p){
        return { ...p, assets:p.assets.map(function(a){ return a.id===mt_chosen.id ? {...a,status:ST.READY} : a; }) };
      });
      ns = addLog(ns, "🔧 "+sn+" uses Multi-Tool: \""+mt_chosen.name+"\" reset to READY"+mt_auto+".", "asset", [mt_chosen]);
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "THREE_OF_KIND_SELECT": {
      var tok_need3 = pc.tripleCount || 3;
      var tok_req_val = pc.tripleValue || 2;
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        // Timeout/cancel - mark asset USED, nothing discarded
        ns = addLog(ns, pname(ns,pc.srcPlayer)+" timed out - Three of a Kind cancelled (asset stays used).", "info");
        break;
      }
      var tok_ids = choiceId.split(",");
      if (tok_ids.length !== tok_need3) { ns=addLog(ns,"Three of a Kind: need exactly "+tok_need3+" cards.","warn"); break; }
      var tok_pl3 = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      var tok_cards = tok_pl3 ? tok_ids.map(function(id){ return tok_pl3.hand.find(function(c){ return c.id===id; }); }).filter(Boolean) : [];
      if (tok_cards.length !== tok_need3) { ns=addLog(ns,"Three of a Kind: invalid selection.","warn"); break; }
      var tok_val = tok_req_val;
      if (!tok_cards.every(function(c){ return c.value===tok_val; })) { ns=addLog(ns,"Three of a Kind: all selected cards must be worth $"+tok_val+"k.","warn"); break; }
      // Discard the selected cards
      var tok_id_set = new Set(tok_ids);
      ns = setPl(ns, pc.srcPlayer, function(p){ return { ...p, hand:p.hand.filter(function(c){ return !tok_id_set.has(c.id); }) }; });
      tok_cards.forEach(function(c){ ns={...ns,mainDiscard:[...ns.mainDiscard,{...c,value:c.origVal,modifiedBy:[]}]}; });
      ns = addLog(ns,"🗑 "+pname(ns,pc.srcPlayer)+" discards "+tok_need3+" $"+tok_val+"k cards (Three of a Kind).","discard");
      // Gain top asset from asset deck for free
      var tok_res = tryReshuffle(ns.assetDeck, ns.assetDiscard);
      if (tok_res.empty) {
        ns=addLog(ns,"Asset deck empty - Three of a Kind pays $5k instead.","warn");
        ns=addMoney(ns,pc.srcPlayer,5,"Three of a Kind (no asset)");
        return applyAllHandBoosts(maybeFlush(ns));
      }
      if (tok_res.reshuffled) ns={...ns,assetDeck:tok_res.deck,assetDiscard:[]};
      var tok_asset = tok_res.deck[0];
      var tok_toks2 = tok_asset.hook==="a_credit_line"?Array.from({length:tok_asset.tmax||3},function(){return{id:uid()};}) : tok_asset.hook==="a_prospects"?[{id:uid()},{id:uid()},{id:uid()}]:[];
      ns = setPl(ns,pc.srcPlayer,function(p){ return {...p,assets:[...p.assets,{...tok_asset,status:ST.READY,tokens:tok_toks2,disabled:false}]}; });
      ns = {...ns,assetDeck:tok_res.deck.slice(1)};
      ns = addLog(ns,"🎁 "+pname(ns,pc.srcPlayer)+' gains "'+tok_asset.name+'" from deck free! (Three of a Kind)',"asset",[tok_asset]);
      ns = checkAssetDeckExhausted(ns);
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "QUICK_EXCHANGE_DISCARD": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Quick Exchange: cancelled.","info"); break; }
      var qe_ids=choiceId.split(",");
      if (qe_ids.length!==2) { ns=addLog(ns,"Quick Exchange: need exactly 2 cards.","warn"); break; }
      var qe_pl2=ns.players.find(function(p){return p.id===pc.srcPlayer;});
      var qe_c1=qe_pl2&&qe_pl2.hand.find(function(c){return c.id===qe_ids[0];});
      var qe_c2=qe_pl2&&qe_pl2.hand.find(function(c){return c.id===qe_ids[1];});
      if (!qe_c1||!qe_c2||qe_c1.value!==qe_c2.value) { ns=addLog(ns,"Quick Exchange: invalid selection.","warn"); break; }
      ns=setPl(ns,pc.srcPlayer,function(p){return {...p,hand:p.hand.filter(function(c){return c.id!==qe_ids[0]&&c.id!==qe_ids[1];})};});
      ns={...ns,mainDiscard:[...ns.mainDiscard,{...qe_c1,value:qe_c1.origVal,modifiedBy:[]},{...qe_c2,value:qe_c2.origVal,modifiedBy:[]}]};
      ns=addLog(ns,"🗑 "+pname(ns,pc.srcPlayer)+' discards "'+qe_c1.name+'" and "'+qe_c2.name+'" (Quick Exchange).', "discard");
      var qe_res=tryReshuffle(ns.assetDeck,ns.assetDiscard);
      if (qe_res.empty) { ns=addLog(ns,"Asset deck empty - Quick Exchange pays $3k instead.","warn"); ns=addMoney(ns,pc.srcPlayer,3,"Quick Exchange (no asset)"); break; }
      var qe_asset=qe_res.deck[0];
      var qe_toks2 = qe_asset.hook==="a_credit_line"?Array.from({length:qe_asset.tmax||3},function(){return{id:uid()};}) : qe_asset.hook==="a_prospects"?[{id:uid()},{id:uid()},{id:uid()}]:[];
      ns=setPl(ns,pc.srcPlayer,function(p){return {...p,assets:[...p.assets,{...qe_asset,status:ST.READY,tokens:qe_toks2,disabled:false}]};});
      ns={...ns,assetDeck:qe_res.deck.slice(1)};
      ns=addLog(ns,"🎁 "+pname(ns,pc.srcPlayer)+' gets "'+qe_asset.name+'" from deck! (Quick Exchange).',"asset",[qe_asset]);
      ns=checkAssetDeckExhausted(ns);
      return applyAllHandBoosts(maybeFlush(ns));
    }
    case "MR_SHOW_CARD": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Market Research: cancelled.","info"); break; }
      var mr_card=ns.players.find(function(p){return p.id===pc.srcPlayer;})?.hand.find(function(c){return c.id===choiceId;});
      if (!mr_card) break;
      var mr_opps=ns.players.filter(function(p){return p.id!==pc.srcPlayer;});
      var mr_pay_opps=mr_opps.filter(function(p){return !p.hand.some(function(c){return c.value===mr_card.value;});});
      if (!mr_pay_opps.length) { ns=addLog(ns,pname(ns,pc.srcPlayer)+' shows "'+mr_card.name+'" - all opponents matched!', "asset",[mr_card]); break; }
      mr_pay_opps.forEach(function(p){
        ns=payMoney(ns,p.id,pc.srcPlayer,2,"Market Research (couldn't match $"+mr_card.value+"k)");
      });
      ns=addLog(ns,pname(ns,pc.srcPlayer)+' shows "'+mr_card.name+'" ($'+mr_card.value+'k) - '+mr_pay_opps.length+' opponent'+(mr_pay_opps.length!==1?"s":"")+" pay $2k each.", "asset",[mr_card]);
      break;
    }
    case "VALUATION_PICK": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Valuation: cancelled.","info"); break; }
      var val_asset=ns.shop.find(function(a){return a.id===choiceId;});
      if (!val_asset) break;
      var val_v=val_asset.origVal||val_asset.val||0;
      ns={...ns,shop:ns.shop.filter(function(a){return a.id!==choiceId;}),assetDiscard:[...ns.assetDiscard,{...val_asset,status:ST.READY,tokens:[],disabled:false}]};
      var val_res=tryReshuffle(ns.assetDeck,ns.assetDiscard);
      if (!val_res.empty) {
        var val_new=val_res.deck[0];
        ns={...ns,shop:[...ns.shop,{...val_new,status:ST.READY,tokens:[],disabled:false}],assetDeck:val_res.deck.slice(1)};
      }
      ns=addMoney(ns,pc.srcPlayer,val_v,"Valuation");
      ns=addLog(ns,pname(ns,pc.srcPlayer)+' discards "'+val_asset.name+'" for $'+val_v+'k. (Valuation)',"asset");
      ns=checkAssetDeckExhausted(ns);
      break;
    }
    case "COVER_COSTS_TARGET": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"Cover the Costs: timed out - cost not redirected.","warn"); break; }
      var cct_amount=pc.amount||0;
      ns=addMoney(ns,pc.srcPlayer,cct_amount,"Cover the Costs (refunded)");
      ns=addMoney(ns,choiceId,-cct_amount,"Cover the Costs (redirected)");
      ns=addLog(ns,pname(ns,pc.srcPlayer)+" redirects $"+cct_amount+"k loss to "+pname(ns,choiceId)+"! (Cover the Costs)","reaction");
      break;
    }
    case "MINOR_LOSS_DISCARD": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") { ns=addLog(ns,"A Minor Loss: timed out.","warn"); break; }
      var ml_card=ns.players.find(function(p){return p.id===pc.srcPlayer;})?.hand.find(function(c){return c.id===choiceId;});
      if (!ml_card) break;
      ns=setPl(ns,pc.srcPlayer,function(p){return {...p,money:1,hand:p.hand.filter(function(c){return c.id!==choiceId;})};});
      ns={...ns,mainDiscard:[...ns.mainDiscard,{...ml_card,value:ml_card.origVal,modifiedBy:[]}]};
      ns=addLog(ns,"💸 "+pname(ns,pc.srcPlayer)+' discards "'+ml_card.name+'" and resets to $1k! (A Minor Loss)',"reaction",[ml_card]);
      if (ns.pendingEffect) ns={...ns,pendingEffect:null};
      break;
    }
    case "DISCOUNT_CHOICE": {
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") break;
      return applyAllHandBoosts(doBuyShop(ns,pc.srcPlayer,choiceId,pc.discount||0));
    }
    case "RECYCLE_CHOICE": {
      var rc_remaining = (pc.recycleCount||1) - 1;
      if (!choiceId||choiceId==="auto"||choiceId==="cancel") {
        ns=addLog(ns,"Recycle: skipped - keeping normal draw.","info");
        if (rc_remaining > 0 && ns.mainDiscard.length) {
          var rc_top2 = ns.mainDiscard[ns.mainDiscard.length-1];
          return {...ns, pendingChoice:{...pc, recycleCount:rc_remaining, timerEnd:Date.now()+15000, topCard:rc_top2, options:[rc_top2]}};
        }
        break;
      }
      var rec_card=ns.mainDiscard.find(function(c){return c.id===choiceId;});
      if (!rec_card) break;
      ns={...ns,mainDiscard:ns.mainDiscard.filter(function(c){return c.id!==choiceId;})};
      ns=setPl(ns,pc.srcPlayer,function(p){return {...p,hand:[...p.hand,rec_card]};});
      ns=addLog(ns,"♻ "+pname(ns,pc.srcPlayer)+' draws "'+rec_card.name+'" from discard (Recycle).', "draw",[rec_card]);
      if (rc_remaining > 0 && ns.mainDiscard.length) {
        var rc_top3 = ns.mainDiscard[ns.mainDiscard.length-1];
        return {...ns, pendingChoice:{...pc, recycleCount:rc_remaining, timerEnd:Date.now()+15000, topCard:rc_top3, options:[rc_top3]}};
      }
      break;
    }

    case "PRICE_CHECK_PROMPT": {
      if (!choiceId || choiceId === "auto" || choiceId === "cancel") {
        ns = addLog(ns, pname(ns,pc.srcPlayer)+" skipped Price Check free play.", "info", [], true);
        break;
      }
      var frp_target = pc.drawnCard;
      if (!frp_target && choiceId !== "auto" && choiceId !== "cancel") {
        // Try to find from hand by id
        var frp_pl = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
        frp_target = frp_pl && frp_pl.hand.find(function(c){ return c.id===choiceId; });
      }
      if (frp_target) {
        // Remove the card from hand first (treat as playing from hand)
        ns = setPl(ns, pc.srcPlayer, function(p){ return {...p,
          hand:p.hand.filter(function(c){ return c.id!==frp_target.id; }),
          pendingDiscard:[...(p.pendingDiscard||[])]
        }; });
        ns = {...ns, pendingDiscard:[...(ns.pendingDiscard||[]), frp_target]};
        return maybeFlush(applyHook(ns, frp_target.hook, { srcPlayer:pc.srcPlayer, srcCard:frp_target }));
      }
      break;
    }
    case "WATER_DAMAGE_DISCARD": {
      var wdd_pl = ns.players.find(function(p){ return p.id===pc.srcPlayer; });
      var wdd_card;
      if (!choiceId || choiceId==="auto" || choiceId==="cancel") {
        wdd_card = wdd_pl && wdd_pl.hand.length ? wdd_pl.hand[Math.floor(_rand()*wdd_pl.hand.length)] : null;
      } else {
        wdd_card = wdd_pl && wdd_pl.hand.find(function(c){ return c.id===choiceId; });
      }
      if (!wdd_card) { ns=addLog(ns,"Water Damage: no card to discard.","warn"); break; }
      ns = setPl(ns, pc.srcPlayer, function(p){ return { ...p, hand:p.hand.filter(function(c){ return c.id!==wdd_card.id; }) }; });
      ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...wdd_card, value:wdd_card.origVal||wdd_card.value, modifiedBy:[] }] };
      var wdd_val = wdd_card.value;
      ns = addLog(ns,"🌊 "+sn+" discards \""+wdd_card.name+"\" ($"+wdd_val+"k) - all opponents lose $"+wdd_val+"k!","action",[wdd_card]);
      var wdd_targets = ns.players.filter(function(p){ return p.id!==pc.srcPlayer; });
      ns = { ...ns, wdQueue:wdd_targets.map(function(p){ return { srcPlayer:pc.srcPlayer, tgtPlayer:p.id, amount:wdd_val }; }) };
      break;
    }
    default: break;
  }
  return maybeFlush(ns);
}

/* - Buy Shop (internal) - */
function clearPressureTokens(st, pid, reason) {
  // Just clears pressure tokens (no money charge - penalty is applied in finishEndTurn)
  var pl = st.players.find(function(p){ return p.id===pid; });
  if (!pl || !pl.pressureTokens || !Object.keys(pl.pressureTokens).length) return st;
  if (reason) {
    var ns_log = addLog(st, "🟢 "+pname(st,pid)+"'s pressure tokens cleared ("+reason+").", "info", [], true);
    return setPl(ns_log, pid, function(p){ return { ...p, pressureTokens:{} }; });
  }
  return setPl(st, pid, function(p){ return { ...p, pressureTokens:{} }; });
}

function doBuyShop(st, pid, assetId, discount=0) {
  const asset = st.shop.find(a => a.id===assetId);
  if (!asset) return st;
  const { cost } = calcShopCost(st, asset, pid, discount);
  let ns = setPl(st, pid, p => ({ ...p, money:p.money-cost, assets:[...p.assets, {...asset, status:ST.READY,
          tokens:asset.hook==="a_credit_line"
            ? Array.from({length:asset.tmax||3}, function(){ return {id:uid()}; })
            : asset.hook==="a_prospects"
              ? Array.from({length:3}, function(){ return {id:uid()}; })
              : []}] }));
  const { deck, disc, reshuffled, empty } = tryReshuffle(ns.assetDeck, ns.assetDiscard);
  let newShop = ns.shop.filter(a => a.id!==assetId);
  if (!empty) {
    if (reshuffled) ns = addLog(ns, "🔀 Asset deck reshuffled.", "sys");
    newShop = [...newShop, deck[0]];
    ns = { ...ns, assetDeck:deck.slice(1), assetDiscard:reshuffled ? [] : disc };
  }
  ns = { ...ns, shop:newShop, hasBoughtAsset:true };
  ns = checkAssetDeckExhausted(ns);
  // Discount: give all opponents (not buyer) a token; reset buyer's discount tokens
  ns.players.forEach(function(opp) {
    if (opp.id === pid) return;
    opp.assets.forEach(function(a) {
      if (a.hook === "p_discount" && !a.disabled && (a.tokens||[]).length < (a.tmax||5)) {
        var dc_gain = a._franchised ? 2 : 1;
        ns = setPl(ns, opp.id, function(p){ return { ...p, assets:p.assets.map(function(b){
          if (b.id !== a.id) return b;
          var dc_new=[...( b.tokens||[])]; for(var dci=0;dci<dc_gain&&dc_new.length<(b.tmax||5);dci++) dc_new.push({id:uid()});
          return { ...b, tokens:dc_new };
        }) }; });
      }
    });
  });
  ns = setPl(ns, pid, function(p){ return { ...p, assets:p.assets.map(function(a){
    return a.hook==="p_discount" ? { ...a, tokens:[] } : a;
  }) }; });
  ns = clearPressureTokens(ns, pid, "bought an asset");
  ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:pid, value:cost });
  ns = addLog(ns, `🏢 ${pname(ns,pid)} buys "${asset.name}" for ${$(cost)}.`, "asset", [asset]);
  ns = pushTrigger(ns, { type:T.BUY_ASSET, srcPlayer:pid, value:cost });
  var dbs_rt = ns.triggerStack[ns.triggerStack.length-1];
  var dbs_rx = reactorsFor(ns.players, dbs_rt);
  if (dbs_rx.some(function(r){ return r.canReact; })) {
    return { ...ns, pendingEffect:{ hook:"__buy_asset__", cost:cost, srcPlayer:pid },
      reactionWindow:buildWindow(uid(), dbs_rt.tid, T.BUY_ASSET, pid, null, dbs_rx) };
  }
  return checkBankruptcy(ns, pid);
}

/* - Shop cost calculators - */
// Returns { cost, surcharge, sources } for a player buying a specific shop asset.
// surcharge > 0 means opponents' Price Adjustment(s) are inflating the price.
function calcShopCost(st, asset, pid, discount) {
  discount = discount || 0;
  // Add buyer's Discount asset tokens as additional discount
  var discountTokens = 0;
  var discountSources = [];
  var buyer = st.players.find(function(p){ return p.id === pid; });
  if (buyer) buyer.assets.forEach(function(a) {
    if (a.hook !== "p_discount" || a.disabled) return;
    var cnt = (a.tokens || []).length;
    if (cnt > 0) {
      var amt = cnt * (a.discountAmount || 1);
      discountTokens += amt;
      discountSources.push(cnt + " Discount token" + (cnt!==1?"s":"") + " (-$" + amt + "k)");
    }
  });
  discount = discount + discountTokens;
  var base = Math.max(0, asset.origVal + st.shopMod - discount);
  var surcharge = 0;
  var sources = discountSources.slice();
  st.players.forEach(function(p) {
    if (p.id === pid) return;
    p.assets.forEach(function(a) {
      if (a.hook === "p_price_adj" && !a.disabled) {
        surcharge += (a.priceAdjAmount || 2) * (a._franchised ? 2 : 1);
        sources.push(p.name + "'s " + a.name);
      }
    });
  });
  return { cost: base + surcharge, surcharge: surcharge, discount: discountTokens, sources: sources };
}

function calcBlindCost(st, pid) {
  var base = BLIND_BUY + st.shopMod;
  var surcharge = 0;
  var sources = [];
  st.players.forEach(function(p) {
    if (p.id === pid) return;
    p.assets.forEach(function(a) {
      if (a.hook === "p_price_adj" && !a.disabled) {
        surcharge += a.priceAdjAmount || 2;
        sources.push(p.name + "'s " + a.name);
      }
    });
  });
  return { cost: base + surcharge, surcharge: surcharge, sources: sources };
}

/* GAME HANDLERS */
function mkPlayer(idx, name, startAsset) {
  return {
    id:`p${idx}`, name, color:PCOLORS[idx%6],
    money: START_MONEY - (startAsset?.origVal||0),
    feeToken:START_FEE, handLimit:HAND_LIMIT,
    hand:[], assets:startAsset ? [{...startAsset, status:ST.READY,
      tokens:startAsset.hook==="a_credit_line"
        ? Array.from({length:startAsset.tmax||3}, function(){ return {id:uid()}; })
        : startAsset.hook==="a_prospects"
          ? Array.from({length:3}, function(){ return {id:uid()}; })
          : (startAsset.tokens||[])}] : [],
    actionCountModifier:0, totalGained:0, totalLost:0, actionLocked:false, turnsToSkip:0, _doublePassive:false,
  };
}

function initGame(names, totalTurns, disabledNames, countOverrides, freeActions) {
  disabledNames = disabledNames || new Set();
  countOverrides = countOverrides || {};
  freeActions = !!freeActions;
  const mainDeck  = mkCards(ACTIONS_T, CT.ACTION, 3, disabledNames, countOverrides).concat(mkCards(REACTIONS_T, CT.REACTION, 3, disabledNames, countOverrides));
  const assetDeck = shuf(mkCards(ASSETS_T, CT.ASSET, 1, disabledNames, countOverrides));
  const startAssets = assetDeck.slice(0, names.length);
  const remaining   = assetDeck.slice(names.length);
  // Turn order: lowest starting asset value goes first
  const order = startAssets.map((a,i) => ({i,v:a.origVal})).sort((a,b) => a.v-b.v).map(x=>x.i);
  const players = order.map((oi, ti) => mkPlayer(ti, names[oi], startAssets[oi]));
  let deck = shuf(mainDeck);
  players.forEach(p => { for(let i=0;i<4;i++) if(deck.length) p.hand.push(deck.shift()); });
  const shop = remaining.splice(0, INIT_SHOP);
  return {
    players, curIdx:0, viewIdx:0,
    phase:PH.SOT, startTurnPending:true,
    actionsLeft:DEF_ACTIONS, actionsTaken:[], hasBoughtAsset:false,
    mainDeck:deck, mainDiscard:[],
    assetDeck:remaining, assetDiscard:[],
    shop, shopSize:INIT_SHOP, shopMod:0,
    triggerStack:[], reactionWindow:null, pendingChoice:null, pendingEffect:null, pendingPaidWork:null, pendingDiscard:[], pocketChangeState:null, kickstarterState:null, letGoState:null, negReactionState:null, totalLossState:null, shutDownState:null, discardQueue:null, globalShopSurcharges:[], cfrHistory:[], pendingPWChoice:null, playAnimation:null, shopRefreshAnim:false, borrowState:null, outOfOrderEffects:[], shutDownEffects:[], mrState:null, coverCostsState:null, bpIntent:null, rcIntent:null, equityIntent:null, rwActionsBonus:0, pcIntent:null, retainerQueue:[], swearJarQueue:[], wdQueue:[], negReactionPayQueue:[], _finalRound:false,
    savedReactionWindow:null, pendingReactionCard:null, detailShopInfo:null, deferredWindow:null, shrinkageState:null, toasts:[], moneyEvents:[], detailShopInfo:null, deferredWindow:null, shrinkageState:null, toasts:[], moneyEvents:[], outOfOrderEffects:[],
    log:[
      {id:uid(),msg:"🎮 Game started! Turn order: lowest starting asset goes first.",type:"sys",turn:1,cards:[]},
      ...players.map(p=>({id:uid(),msg:`  . ${p.name} starts with "${p.assets[0]?.name||"no asset"}" -> ${$(p.money)}`,type:"info",turn:1,cards:p.assets.slice(0,1)})),
    ],
    turnNum:1, roundNum:1, turnsLeft:totalTurns, totalTurns, _finalRound:false,
    selectedCardId:null, awaitingTarget:null,
    detailCard:null, gameOver:false, scores:[],
    freeActions: freeActions,
  };
}

function handleStartTurn(st) {
  let ns = { ...st, startTurnPending:false, _extraTurn:false };
  let p  = curPl(ns);

  if ((p.turnsToSkip || 0) > 0) {
    ns = pushTrigger(ns, { type:T.SKIP_TURN, srcPlayer:p.id });
    ns = addLog(ns, `⏭ ${p.name}'s turn is skipped (${p.turnsToSkip} remaining).`, "warn");
    ns = setPl(ns, p.id, pl => ({ ...pl, turnsToSkip:Math.max(0, (pl.turnsToSkip||0) - 1) }));
    return advanceTurn(ns);
  }

  // 1. Pay overdraft fee
  // 1. Pay overdraft fee — order: pay current fee FIRST, then increment tracker for next turn.
  // Turn 1 negative: feeToken=1 → pay $1k → tracker becomes 2.
  // Turn 2 negative: feeToken=2 → pay $2k → tracker becomes 3 (max).
  // Turn positive: no fee, tracker decreases back toward START_FEE.
  if (p.money < 0) {
    const fee = calcOverdraft(p);
    // Pay first
    ns = setPl(ns, p.id, pl => ({ ...pl, money:pl.money-fee }));
    p = ns.players.find(pl => pl.id===p.id);
    ns = addLog(ns, `📉 ${p.name} pays overdraft fee ${$(fee)}. Balance: ${$(p.money)}.`, "loss");
    ns = pushTrigger(ns, { type:T.PAY_FEES, srcPlayer:p.id, value:fee });
    var preCheckMoney = p.money;
    ns = checkBankruptcy(ns, p.id);
    p = ns.players.find(pl => pl.id===p.id);
    if (preCheckMoney <= BANKRUPT_AT) return advanceTurn(ns);
    // Then increment tracker (capped at 3)
    ns = setPl(ns, p.id, pl => ({ ...pl, feeToken:Math.min(pl.feeToken+1, 3) }));
  } else {
    // Positive balance: decrease tracker by 1 toward minimum (START_FEE)
    ns = setPl(ns, p.id, pl => ({ ...pl, feeToken:Math.max(pl.feeToken-1, START_FEE) }));
  }

  // 2. Reset assets to READY
  ns = setPl(ns, p.id, pl => ({
    ...pl,
    assets:pl.assets.map(a => (a.sub===AS.ONCE && !a.defectivelyBlocked) ? a : {...a, status:ST.READY, defectivelyBlocked:false}),
    actionCountModifier:0,
  }));

  // 2b. Portfolio: add token OR pay out at max (passive assets only)
  p = ns.players.find(function(pl){ return pl.id===p.id; });
  if (p) p.assets.forEach(function(a) {
    if (a.hook !== "p_portfolio" || a.disabled) return;
    var pf_cur = a.tokens ? a.tokens.length : 0;
    if (pf_cur < (a.tmax||3)) {
      // Add the token first
      var pf_new = pf_cur + 1;
      ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
        return b.id===a.id ? { ...b, tokens:[...(b.tokens||[]),{id:uid()}] } : b;
      }) }; });
      ns = addLog(ns, "📈 "+pname(ns,p.id)+"'s Portfolio gains a token ("+pf_new+"/"+(a.tmax||3)+").", "asset", [], true);
      // Check: if we just hit the threshold, pay out immediately and reset
      if (pf_new >= (a.tmax||3)) {
      var pf_owner = ns.players.find(function(pl){ return pl.id===p.id; });
      var pf_count = pf_owner ? pf_owner.assets.filter(function(a2){ return a2.sub===AS.PASSIVE; }).length : 0;
      var pf_amt = (a.portfolioAmount||1) * (a._franchised ? 2 : 1) * pf_count;
      ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
        return b.id===a.id ? { ...b, tokens:[] } : b;
      }) }; });
      ns = addMoney(ns, p.id, pf_amt, "Portfolio ("+pf_count+" passive assets x $"+(a.portfolioAmount||1)+"k)");
      ns = addLog(ns, "💰 "+pname(ns,p.id)+"'s Portfolio pays out $"+pf_amt+"k! Tokens reset.", "asset", [], true);
      } // end if pf_new >= tmax
    } // end if pf_cur < tmax
  });
  // 2c. Prospects: remove a token; on last token, copy highest shop asset
  p = ns.players.find(function(pl){ return pl.id===p.id; });
  if (p) p.assets.forEach(function(a) {
    if (a.hook !== "a_prospects" || a.disabled) return;
    if (a._fromProspects) return; // already transformed
    var pros_cur = a.tokens ? a.tokens.length : 0;
    // If already at 0 tokens but not yet transformed, transform now
    if (pros_cur <= 0) {
      var pros_shop0 = ns.shop || [];
      if (!pros_shop0.length) {
        ns = addMoney(ns, p.id, 5, "Prospects (shop empty - refund)");
        ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
          return b.id===a.id ? { ...b, disabled:true } : b;
        }) }; });
        ns = addLog(ns, "🔭 "+pname(ns,p.id)+"'s Prospects: shop empty - $5k refunded, disabled.", "asset");
      } else {
        var pros_maxVal0 = pros_shop0.reduce(function(m,cur){ return Math.max(m,cur.origVal||0); }, 0);
        var pros_tied0 = pros_shop0.filter(function(a2){ return (a2.origVal||0)===pros_maxVal0; });
        var pros_best0 = pros_tied0[Math.floor(_rand()*pros_tied0.length)];
        var pros_copy0 = { ...pros_best0, id:a.id, status:ST.READY, tokens:[], disabled:false, _fromProspects:true, _prospectsSource:pros_best0.name };
        ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
          return b.id===a.id ? pros_copy0 : b;
        }) }; });
        ns = addLog(ns, "🔭 "+pname(ns,p.id)+"'s Prospects transforms into pros_best0.name ($"+pros_best0.origVal+"k)!", "asset");
      }
      return;
    }
    var pros_new = pros_cur - 1;
    ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
      return b.id===a.id ? { ...b, tokens:(a.tokens||[]).slice(1) } : b;
    }) }; });
    ns = addLog(ns, "🔭 "+pname(ns,p.id)+"'s Prospects: "+pros_new+" token"+(pros_new!==1?"s":"")+" remaining.", "asset", [], true);
    if (pros_new === 0) {
      var pros_shop = ns.shop || [];
      if (!pros_shop.length) {
        ns = addMoney(ns, p.id, 5, "Prospects (shop empty - refund)");
        ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
          return b.id===a.id ? { ...b, disabled:true } : b;
        }) }; });
        ns = addLog(ns, "🔭 "+pname(ns,p.id)+"'s Prospects: shop empty - $5k refunded, disabled.", "asset");
      } else {
        var pros_maxVal = pros_shop.reduce(function(m,cur){ return Math.max(m,cur.origVal||0); }, 0);
        var pros_tied = pros_shop.filter(function(a2){ return (a2.origVal||0)===pros_maxVal; });
        var pros_best = pros_tied[Math.floor(_rand()*pros_tied.length)];
        var pros_copy = { ...pros_best, id:a.id, status:ST.READY, tokens:[], disabled:false, _fromProspects:true, _prospectsSource:pros_best.name };
        if (a._franchised) {
          // Franchised Prospects: produce 2 copies
          var pros_copy2 = { ...pros_best, id:uid(), status:ST.READY, tokens:[], disabled:false, _fromProspects:true, _prospectsSource:pros_best.name };
          ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:[...pl.assets.map(function(b){
            return b.id===a.id ? pros_copy : b;
          }), pros_copy2] }; });
          ns = addLog(ns, "🔭🔭 "+pname(ns,p.id)+"'s Franchised Prospects transforms into 2x pros_best.name ($"+pros_best.origVal+"k each)!", "asset");
        } else {
          ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
            return b.id===a.id ? pros_copy : b;
          }) }; });
          ns = addLog(ns, "🔭 "+pname(ns,p.id)+"'s Prospects transforms into \""+pros_best.name+"\" ($"+pros_best.origVal+"k)!", "asset");
        }
      }
    }
  });
  // 2d. Pressure Campaign: add 1 pressure token to each opponent (1 per owner regardless of copies)
  p = ns.players.find(function(pl){ return pl.id===p.id; });
  if (p && p.assets.some(function(a){ return a.hook==="p_pressure" && !a.disabled; })) {
    ns.players.forEach(function(opp) {
      if (opp.id === p.id) return;
      var cur = (opp.pressureTokens && opp.pressureTokens[p.id]) || 0;
      ns = setPl(ns, opp.id, function(pl){ return { ...pl, pressureTokens:{ ...(pl.pressureTokens||{}), [p.id]:cur+1 } }; });
      ns = addLog(ns, "🔴 "+pname(ns,opp.id)+" gains a pressure token ("+(cur+1)+") from "+pname(ns,p.id)+"'s Pressure Campaign.", "asset", [], true);
    });
  }
  // 2e. R&D Budget: token gained at END of turn (see finishEndTurn)
  // 2f. Credit Line: refill tokens at start of turn and charge cost
  p = ns.players.find(function(pl){ return pl.id===p.id; });
  if (p) p.assets.forEach(function(a) {
    if (a.hook !== "a_credit_line" || a.disabled) return;
    var cl_cur = a.tokens ? a.tokens.length : 0;
    var cl_max = a.tmax || 3;
    var cl_need = cl_max - cl_cur;
    if (cl_need <= 0) return;
    var prices = [0, a.clPrice1||1, a.clPrice2||3, a.clPrice3||6];
    var cl_cost = prices[cl_need] || 0;
    if (cl_cost > 0) {
      ns = addMoney(ns, p.id, -cl_cost, "Credit Line refill ("+cl_need+" token"+(cl_need!==1?"s":"")+")" );
      ns = addLog(ns, "💳 "+pname(ns,p.id)+"'s Credit Line refills "+cl_need+" token"+(cl_need!==1?"s":"")+" - costs $"+cl_cost+"k.", "asset", [], true);
    }
    ns = setPl(ns, p.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
      return b.id===a.id ? { ...b, tokens:Array.from({length:cl_max},function(){ return {id:uid()}; }) } : b;
    }) }; });
    ns = checkBankruptcy(ns, p.id);
    p = ns.players.find(function(pl){ return pl.id===p.id; });
  });
  // 2g. Un-Shut-Down assets (if we are the srcPlayer who shut them down)
  p = ns.players.find(function(pl){ return pl.id===p.id; });
  ns.players.forEach(function(opp) {
    if (!opp.assets.some(function(a){ return a.shutDownBy===p.id; })) return;
    ns = setPl(ns, opp.id, function(pl){ return { ...pl, assets:pl.assets.map(function(a){
      return a.shutDownBy===p.id ? { ...a, disabled:false, shutDownBy:null } : a;
    }) }; });
  });
  // 2g2. Re-enable CFR-deactivated assets when it's the CFR owner's turn start
  var cfr_reEnabled = [];
  ns.players.forEach(function(opp) {
    ns = setPl(ns, opp.id, function(pl){ return { ...pl, assets:pl.assets.map(function(a){
      if (a._cfrUntilTurnOf !== p.id) return a;
      cfr_reEnabled.push(opp.name+"'s \""+a.name+"\"");
      return { ...a, disabled:false, _cfrBy:null, _cfrUntilTurnOf:null };
    }) }; });
  });
  if (cfr_reEnabled.length) {
    ns = addLog(ns, "✅ CFR expired: "+cfr_reEnabled.join(", ")+" re-enabled.", "info");
    ns = applyAllHandBoosts(ns);
  }
  // Clean up cfrHistory: remove entries whose cooldown window has fully passed.
  // nextOwnerTurnNum is the global turnNum of the CFR owner's NEXT turn — keep the entry until AFTER that turn.
  // Fall back to legacy turnNum+players.length for old entries that predate this field.
  var cfr_curTurn = ns.turnNum||1;
  ns = {...ns, cfrHistory:(ns.cfrHistory||[]).filter(function(h){
    var expiry = (h.nextOwnerTurnNum !== undefined) ? h.nextOwnerTurnNum : (h.turnNum + ns.players.length);
    return cfr_curTurn <= expiry;
  })};
  // 2i. Startup Costs: if OTHER players have this asset, current player (p) loses money at start of THEIR turn
  ns.players.forEach(function(owner) {
    if (owner.id === p.id) return;
    owner.assets.forEach(function(a) {
      if (a.hook !== "p_startup_costs" || a.disabled) return;
      var sc_amt = (a.startupCostsAmount || 1) * (a._franchised ? 2 : 1);
      ns = addMoney(ns, p.id, -sc_amt, "Startup Costs ("+pname(ns,owner.id)+")");
      ns = addLog(ns, "💸 "+pname(ns,p.id)+" pays $"+sc_amt+"k startup costs to "+pname(ns,owner.id)+".", "loss", [], true);
      // Push LOSE_MONEY so Retainer (e.g. Recession stored) can fire passively
      ns = pushTrigger(ns, { type:T.LOSE_MONEY, srcPlayer:p.id, value:sc_amt });
      ns = applyPlusInterest(ns, owner.id, p.id);
      ns = applyAccountant(ns, p.id, owner.id, sc_amt);
    });
  });
  // 2j. Recycle: handled at end of turn start (after setup)
  // 3. Apply passive effects
  ns = applyPassives(ns, p.id);
  p = ns.players.find(pl => pl.id===p.id);

  // 4. Push START_TURN trigger
  ns = pushTrigger(ns, { type:T.START_TURN, srcPlayer:p.id });
  var sot_trig = ns.triggerStack[ns.triggerStack.length-1];
  var sot_reactors = reactorsFor(ns.players, sot_trig);

  // 5. Draw 1 card
  ns = drawN(ns, p.id, 1);
  p = ns.players.find(pl => pl.id===p.id);

  ns = addLog(ns, `- ${p.name}'s turn . Round ${ns.roundNum} . Turns left: ${ns.turnsLeft} -`, "turn");
  ns = addLog(ns, `⏱ ${p.name}'s turn begins.`, "turn");
  const acts = DEF_ACTIONS + (p.actionCountModifier||0);
  ns = applyAllHandBoosts({
    ...ns,
    phase:PH.BP, actionsLeft:acts, actionsTaken:[], hasBoughtAsset:false,
    selectedCardId:null, awaitingTarget:null, viewIdx:ns.curIdx,
  });
  // 2j. Recycle + START_TURN reaction window AFTER full turn setup
  p = ns.players.find(function(pl){ return pl.id===p.id; });
  // Open START_TURN reaction window if any player can react (e.g. Close of Business)
  if (sot_reactors && sot_reactors.some(function(r){ return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), sot_trig.tid, T.START_TURN, p.id, null, sot_reactors) };
  }
  if (p && p.assets.some(function(a){ return a.hook==="p_recycle"&&!a.disabled; }) && ns.mainDiscard && ns.mainDiscard.length) {
    var rc_top = ns.mainDiscard[ns.mainDiscard.length - 1];
    var rc_franchised = p.assets.some(function(ax){ return ax.hook==="p_recycle"&&!ax.disabled&&ax._franchised; });
    return { ...ns, pendingChoice:{ type:"RECYCLE_CHOICE", srcPlayer:p.id,
      recycleCount:rc_franchised ? 2 : 1, timerEnd:Date.now()+15000, topCard:rc_top, options:[rc_top] } };
  }
  return ns;
}

function handleDrawExtra(st) {
  if (st.actionsLeft <= 0) return addLog(st, "❌ No actions remaining.", "warn");
  if (!st.freeActions && st.actionsTaken.includes(ACT.DRAW)) return addLog(st, "❌ Already drew this turn.", "warn");
  const pid = curPl(st).id;
  let ns = drawN(st, pid, 1);
  return { ...ns, actionsLeft:ns.actionsLeft-1, actionsTaken:[...ns.actionsTaken, ACT.DRAW] };
}

/* fires non-blocking passive gain for Tax / A Couple Bucks when an opponent plays a card */
function applyValueTaxPassives(st, playerId, cardValue) {
  var ns = st;
  ns.players.forEach(function(owner) {
    if (owner.id === playerId) return; // must be an opponent asset
    owner.assets.forEach(function(a) {
      if (a.disabled) return;
      if (a.hook === "p_tax" && a.taxValue === cardValue) {
        var amt = (a.taxAmount || 1) * (a._franchised ? 2 : 1) * (owner._doublePassive ? 2 : 1);
        ns = addLog(ns, "💰 " + owner.name + "'s Tax fires! (opponent played $" + cardValue + "k card)", "asset");
        ns = addMoney(ns, owner.id, amt, "Tax");
      }
      if (a.hook === "p_couple_bucks" && a.coupleBucksValue === cardValue) {
        var amt2 = (a.coupleBucksAmount || 1) * (a._franchised ? 2 : 1) * (owner._doublePassive ? 2 : 1);
        ns = addMoney(ns, owner.id, amt2, "A Couple Bucks"); ns = addLog(ns, "💰 " + owner.name + "'s A Couple Bucks fires! +$" + amt2 + "k", "asset", [], true);
      }
      if (a.hook === "p_rule_of_thirds" && (a.ruleOfThirdsValue||3) === cardValue) {
        var rot_amt = (a.ruleOfThirdsAmount||1) * (a._franchised?2:1) * (owner._doublePassive?2:1);
        ns = payMoney(ns, playerId, owner.id, rot_amt, "Rule of Thirds");
        ns = addLog(ns, "📐 " + owner.name + "'s Rule of Thirds fires - " + pname(ns,playerId) + " pays $"+rot_amt+"k!", "asset", [], true);
      }
    });
  });
  return ns;
}

function handlePlayAction(st, { cardId, targetId, _skipAnimation }) {
  var bp_card_chk = curPl(st).hand.find(function(c){ return c.id===cardId; });
  var bp_free = !!(bp_card_chk && curPl(st).assets.some(function(a){
    return a.hook==="a_blueprint" && !a.disabled && a.lockedCard && a.lockedCard.value===bp_card_chk.value;
  }));
  var rnd_free = curPl(st).rndActive || false;
  if (!rnd_free && !bp_free && st.actionsLeft <= 0) return addLog(st, "❌ No actions remaining.", "warn");
  if (!rnd_free && !bp_free && !st.freeActions && st.actionsTaken.includes(ACT.ACTION) && !(st.rwActionsBonus > 0)) return addLog(st, "❌ Already played an Action this turn.", "warn");
  if (curPl(st).actionLocked)                       return addLog(st, "❌ Part Time Work - cannot play Action Cards this turn.", "warn");
  const cp   = curPl(st);
  const card = cp.hand.find(c => c.id===cardId);
  if (!card || card.type !== CT.ACTION)              return addLog(st, "❌ Invalid card.", "warn");
  // Monopoly: opponent's asset blocks cards of a specific value
  var mono_blocked = monopolyBlockedValues(st, cp.id);
  if (mono_blocked.size > 0 && card && mono_blocked.has(card.value)) {
    return addLog(st, "❌ Monopoly — $" + card.value + "k cards are blocked this turn.", "warn");
  }
  // Buying Supplies: fizzle if hand already has ≥5 cards (card itself excluded - already in hand at check time)
  if (card.hook === "buying_supplies" && cp.hand.filter(c=>c.id!==cardId).length >= 5)
    return addLog(st, "❌ Buying Supplies requires fewer than 5 cards in hand.", "warn");
  if (card.hook === "refresh_asset") {
    var rf_usable = cp.assets.filter(function(a){ return a.status===ST.USED && a.sub!==AS.ONCE && !a.disabled; });
    if (!rf_usable.length) return addLog(st, "❌ Refresh requires at least one used (non-one-time) asset.", "warn");
  }
  if (card.hook === "give_back") {
    var gb_maxMoney = Math.max.apply(null, st.players.map(function(p){ return p.money; }));
    if (cp.money >= gb_maxMoney) return addLog(st, "❌ Give Back cannot be played when you have the highest (or tied highest) amount of money.", "warn");
  }
  if (card.hook === "outside_hire") {
    if (cp.money < 0) return addLog(st, "❌ Outside Hire cannot be played while you are in the negatives.", "warn");
    var oh_eligible = st.players.filter(function(p) { return p.id !== cp.id && p.assets.length >= 2; });
    if (!oh_eligible.length) return addLog(st, "❌ Outside Hire requires at least one opponent with 2 or more assets.", "warn");
  }

  // Remove from hand, consume action - card goes to pendingDiscard until effect resolves
  let ns = setPl(st, cp.id, p => ({ ...p, hand:p.hand.filter(c=>c.id!==cardId) }));
  var rw_usingBonus = !st.freeActions && ns.actionsTaken.includes(ACT.ACTION);
  ns = { ...ns, pendingDiscard:[...(ns.pendingDiscard||[]), card],
         actionsLeft:(rnd_free||bp_free) ? ns.actionsLeft : ns.actionsLeft-1,
         actionsTaken:(rnd_free||bp_free||rw_usingBonus) ? ns.actionsTaken : [...ns.actionsTaken, ACT.ACTION],
         rwActionsBonus:rw_usingBonus ? Math.max(0,(ns.rwActionsBonus||0)-1) : (ns.rwActionsBonus||0),
         selectedCardId:null, awaitingTarget:null };
  const tn = targetId ? pname(ns, targetId) : "";
  ns = addLog(ns, `⚡ ${cp.name} plays "${card.name}"${tn ? " -> " + tn : ""}`, "action", [card]);
  ns = addLog(ns, `  ↳ ${getDesc(card)}`, "hook");

  // Tax / A Couple Bucks passives: opponents gain money if value matches
  // If we came via ANIMATION_DONE (_skipAnimation), the card was already shown — go straight
  // to resumeActionPlayed. Otherwise show the animation first.
  if (_skipAnimation) {
    return resumeActionPlayed(ns, { cp_id:cp.id, card, targetId });
  }
  return { ...ns, playAnimation:{ card, resumeAction:{ type:"RESUME_ACTION_PLAYED",
    p:{ cp_id:cp.id, card, targetId } } } };
}

function resumeActionPlayed(st, { cp_id, card, targetId }) {
  let ns = st;
  ns = applyValueTaxPassives(ns, cp_id, card.value);

  ns = pushTrigger(ns, { type:T.ACTION_PLAYED, srcPlayer:cp_id, label:card.name, value:card.value, srcCard:card });
  var actionTrig = ns.triggerStack[ns.triggerStack.length-1];
  var actionReactors = reactorsFor(ns.players, actionTrig);

  // Store resume context so ACTION_PLAYED window can continue after reactions
  var resumePE = { hook:"__resume_action__", srcPlayer:cp_id, tgtPlayer:targetId, value:card.value, srcCard:card };
  ns = { ...ns, pendingEffect:resumePE };

  if (actionReactors.some(function(r){ return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), actionTrig.tid, T.ACTION_PLAYED, cp_id, null, actionReactors) };
  }
  // No ACTION_PLAYED reactors - resume immediately
  ns = { ...ns, pendingEffect:null };
  return continueAfterActionPlayed(ns, resumePE);
}

function handlePlayReaction(st, { cardId, ownerId, _skipAnimation, _savedWindow }) {
  const owner = st.players.find(p => p.id===ownerId);
  const card  = owner?.hand.find(c => c.id===cardId);
  if (!card || card.type !== CT.REACTION) return addLog(st, "❌ Not a reaction card.", "warn");
  // All reaction cards except minor_loss only fire inside a reaction window.
  // Block any attempt to play them outside one (e.g. accidental free-play dispatch).
  if (!st.reactionWindow && !_savedWindow && card.hook !== "minor_loss") {
    return addLog(st, `❌ ${card.name} can only be played during a reaction window.`, "warn");
  }
  let ns = setPl(st, ownerId, p => ({ ...p, hand:p.hand.filter(c=>c.id!==cardId) }));
  ns = { ...ns, pendingDiscard:[...(ns.pendingDiscard||[]), card], selectedCardId:null };
  ns = addLog(ns, `🛡 ${owner.name} plays "${card.name}" (free action).`, "reaction", [card]);
  // Restore the reaction window saved before the animation started
  if (_savedWindow) ns = { ...ns, reactionWindow:_savedWindow };
  if (_skipAnimation) {
    return resumeReactionPlayed(ns, { cardId, ownerId });
  }
  return { ...ns, playAnimation:{ card, resumeAction:{ type:"RESUME_REACTION_PLAYED",
    p:{ cardId, ownerId } } } };
}

function resumeReactionPlayed(st, { cardId, ownerId }) {
  const owner2 = st.players.find(p => p.id===ownerId);
  const card2 = st.pendingDiscard.find(c => c.id===cardId);
  if (!card2) return maybeFlush(st);
  let ns = st;
  // Swear Jar: charge the reactor once at play time (mirrors ENG_REACT path).
  ns = checkSwearJar(ns, ownerId);
  ns = applyValueTaxPassives(ns, ownerId, card2.value);
  ns = pushTrigger(ns, { type:T.REACTION_PLAYED, srcPlayer:ownerId, label:card2.name, value:card2.value, srcCard:card2 });
  var reactTrig = ns.triggerStack[ns.triggerStack.length-1];
  var reactReactors = reactorsFor(ns.players, reactTrig);
  // Capture the trigger source (who played/sold the card we're reacting to)
  var reactionTgtPlayer = st.reactionWindow ? st.reactionWindow.srcPlayer : null;
  // Store pending reaction so REACTION_PLAYED window can cancel or confirm it
  var prc = { hook:card2.hook, srcPlayer:ownerId, tgtPlayer:reactionTgtPlayer, value:card2.value, srcCard:card2 };
  ns = { ...ns, pendingReactionCard:prc };
  if (reactReactors.some(function(r){ return r.canReact; })) {
    return { ...ns,
      savedReactionWindow: ns.reactionWindow || null,
      reactionWindow:buildWindow(uid(), reactTrig.tid, T.REACTION_PLAYED, ownerId, null, reactReactors) };
  }
  // No reactors on REACTION_PLAYED - apply immediately
  ns = { ...ns, pendingReactionCard:null };
  // For additive hooks (shrinkage) that open their own window, save outer window before applying
  if (card2.hook === "shrinkage" && ns.reactionWindow && ns.reactionWindow.ttype === T.ACTION_PLAYED) {
    ns = { ...ns, deferredWindow:ns.reactionWindow, reactionWindow:null };
  }
  ns = applyHook(ns, card2.hook, { srcPlayer:ownerId, tgtPlayer:reactionTgtPlayer, value:card2.value, srcCard:card2 });
  return maybeFlush(ns);
}

function handleSellCard(st, { cardId }) {
  if (st.actionsLeft <= 0)               return addLog(st, "❌ No actions remaining.", "warn");
  if (!st.freeActions && st.actionsTaken.includes(ACT.SELL)) return addLog(st, "❌ Already sold a card this turn.", "warn");
  const cp   = curPl(st);
  const card = cp.hand.find(c => c.id===cardId);
  if (!card || card.type === CT.ASSET)   return addLog(st, "❌ Cannot sell this.", "warn");

  const saleVal   = card.value;  // bonus from p_sell_p1 added in resolveSell to avoid double-count
  // Remove card from hand + consume sell action - card stays in limbo (pendingEffect) until resolved
  let ns = setPl(st, cp.id, p => ({ ...p, hand:p.hand.filter(c=>c.id!==cardId) }));
  ns = { ...ns, actionsLeft:ns.actionsLeft-1, actionsTaken:[...ns.actionsTaken, ACT.SELL], selectedCardId:null };
  // Push SELL_CARD trigger - reactions fire before money is paid or card is discarded
  ns = pushTrigger(ns, { type:T.SELL_CARD, srcPlayer:cp.id, value:saleVal, _preReaction:true });
  var sellTrig = ns.triggerStack[ns.triggerStack.length - 1];
  var sellReactors = reactorsFor(ns.players, sellTrig);
  // Always store sold card in pendingEffect - resolved in ENG_PASS/ENG_PASS_ALL or Last Minute Bid
  ns = { ...ns, pendingEffect:{ hook:"__sold_card__", srcPlayer:cp.id, soldCard:card, saleVal } };
  if (sellReactors.some(function(r){ return r.canReact; })) {
    ns = addLog(ns, '🛒 ' + cp.name + ' is selling "' + card.name + '" ($' + saleVal + 'k) — waiting for reactions...', "action", [card]);
    return { ...ns, reactionWindow:buildWindow(uid(), sellTrig.tid, T.SELL_CARD, cp.id, null, sellReactors) };
  }
  // No reactors - resolve immediately
  return resolveSell(ns);
}

// Apply sale: give money to seller, move card to discard. Called after window resolves with no steal.
function resolveSell(st) {
  var pe = st.pendingEffect;
  if (!pe || pe.hook !== "__sold_card__") return maybeFlush(st);
  var ns = { ...st, pendingEffect:null };
  // Import Co / Resale Value: +$1k to sell price
  var ns_sell_bonus = 0;
  var ns_seller = ns.players.find(function(p){ return p.id===pe.srcPlayer; });
  if (ns_seller) ns_seller.assets.forEach(function(a){ if (a.hook==="p_sell_p1"&&!a.disabled) ns_sell_bonus+=((a.resaleAmount||1))*(a._franchised?2:1)*(ns_seller._doublePassive?2:1); });
  var ns_sell_val = (pe.saleVal||0) + ns_sell_bonus;
  // NOTE: soldCard is NOT pushed to mainDiscard here.
  // Full Refund may activate the card's effect before it's discarded.
  // The discard happens below (non-FR path) or inside FULL_REFUND_PROMPT resolution.
  var ns_total_sale = (pe.saleVal||0) + ns_sell_bonus;
  ns = addLog(ns, '💵 ' + pname(ns, pe.srcPlayer) + ' sold "' + pe.soldCard.name + '"' + (ns_sell_bonus>0?' (+$'+ns_sell_bonus+'k Resale)':'') + ' — gains ' + $(ns_total_sale) + '.', "money", [pe.soldCard]);
  ns = addMoney(ns, pe.srcPlayer, ns_total_sale, '');
  ns = pushTrigger(ns, { type:T.SELL_CARD, srcPlayer:pe.srcPlayer, value:ns_sell_val, label:pe.soldCard.name });
  // Finder's Fee: opponents with this asset gain findersFeeAmount
  ns.players.forEach(function(ff_pl) {
    if (ff_pl.id === pe.srcPlayer) return;
    ff_pl.assets.forEach(function(a) {
      if (a.hook !== "p_finders_fee" || a.disabled) return;
      var ff_amt = (a.findersFeeAmount||1) * (a._franchised?2:1) * (ff_pl._doublePassive?2:1);
      ns = addMoney(ns, ff_pl.id, ff_amt, "Finder's Fee");
      ns = addLog(ns, "💰 "+pname(ns,ff_pl.id)+"'s Finder's Fee fires - +$"+ff_amt+"k!", "asset", [], true);
    });
  });
  // Free Replacement: seller draws a card
  var fr_seller_pl = ns.players.find(function(p){ return p.id===pe.srcPlayer; });
  if (fr_seller_pl) fr_seller_pl.assets.forEach(function(a) {
    if (a.hook !== "p_free_replacement" || a.disabled) return;
    var fr_draws = (a.freeReplacementAmount || 1) * (a._franchised ? 2 : 1);
    ns = drawN(ns, pe.srcPlayer, fr_draws, 0, true);
    ns = addLog(ns, "🔄 "+pname(ns,pe.srcPlayer)+"'s Free Replacement: draws "+fr_draws+" card"+(fr_draws!==1?"s":"")+"!", "asset", [], true);
  });
  // Workman's Comp: all opponents lose workmanAmount when seller sells a card
  var wc_seller = ns.players.find(function(p){ return p.id===pe.srcPlayer; });
  if (wc_seller) wc_seller.assets.forEach(function(a) {
    if (a.hook !== "p_workmans_comp" || a.disabled) return;
    var wc_amt = (a.workmanAmount || 1) * (a._franchised ? 2 : 1) * (wc_seller._doublePassive ? 2 : 1);
    ns.players.forEach(function(opp) {
      if (opp.id === pe.srcPlayer) return;
      ns = addMoney(ns, opp.id, -wc_amt, "Workman's Comp ("+pname(ns,pe.srcPlayer)+")");
      ns = applyPlusInterest(ns, pe.srcPlayer, opp.id);
    });
    ns = addLog(ns, "🔨 "+pname(ns,pe.srcPlayer)+"'s Workman's Comp fires - all opponents lose $"+wc_amt+"k!", "asset", [], true);
  });
  // Full Refund: when selling a $1k action card, offer to also activate its effect
  var fr2_seller = ns.players.find(function(p){ return p.id===pe.srcPlayer; });
  if (fr2_seller && pe.soldCard && pe.soldCard.type === "ACTION" && (pe.soldCard.origVal||pe.soldCard.value) <= ((fr2_seller.assets.find(function(a2){return a2.hook==="p_full_refund"&&!a2.disabled;})||{}).fullRefundThreshold||1)) {
    var fr2_has = fr2_seller.assets.some(function(a){ return a.hook==="p_full_refund" && !a.disabled; });
    if (fr2_has) {
      ns = addLog(ns, "💡 "+pname(ns,pe.srcPlayer)+"'s Full Refund: sold a $1k card - option to activate it!", "asset", [], true);
      var fr2_asset2 = fr2_seller.assets.find(function(a){ return a.hook==="p_full_refund"&&!a.disabled; });
      // soldCard is stored in pc.card; discard happens inside FULL_REFUND_PROMPT resolution
      return { ...ns, pendingChoice:{ type:"FULL_REFUND_PROMPT", srcPlayer:pe.srcPlayer, card:pe.soldCard, timerEnd:Date.now()+15000, activationsLeft:fr2_asset2&&fr2_asset2._franchised?2:1 } };
    }
  }
  // No Full Refund — discard the card now
  ns = { ...ns, mainDiscard:[...ns.mainDiscard, { ...pe.soldCard, value:pe.soldCard.origVal||pe.soldCard.value, modifiedBy:[] }] };
  return maybeFlush(ns);
}

function handleBuyShop(st, { assetId, discount=0 }) {
  const cp = curPl(st);
  if (st.hasBoughtAsset)       return addLog(st, "❌ Already bought an asset this turn.", "warn");
  if (cp.money < MIN_BUY)      return addLog(st, `❌ Need at least ${$(MIN_BUY)} to buy.`, "warn");
  if (!st.shop.find(a=>a.id===assetId)) return st;
  return applyAllHandBoosts(doBuyShop(st, cp.id, assetId, discount));
}

function handleBuyDeck(st) {
  const cp = curPl(st);
  if (st.hasBoughtAsset) return addLog(st, "❌ Already bought an asset this turn.", "warn");
  if (cp.money < MIN_BUY) return addLog(st, "❌ Need at least $1k to buy.", "warn");
  const { deck, empty } = tryReshuffle(st.assetDeck, st.assetDiscard);
  if (empty) return addLog(st, "❌ No assets available.", "warn");
  const { cost } = calcBlindCost(st, cp.id);
  const asset = deck[0];
  let ns = setPl(st, cp.id, p => ({ ...p, money:p.money-cost, assets:[...p.assets, {...asset, status:ST.READY,
          tokens:asset.hook==="a_credit_line"
            ? Array.from({length:asset.tmax||3}, function(){ return {id:uid()}; })
            : asset.hook==="a_prospects"
              ? Array.from({length:3}, function(){ return {id:uid()}; })
              : []}] }));
  ns = { ...ns, assetDeck:deck.slice(1), hasBoughtAsset:true };
  // Discount: give all opponents a token when any player buys an asset (mirrors doBuyShop)
  ns.players.forEach(function(opp) {
    if (opp.id === cp.id) return;
    opp.assets.forEach(function(a) {
      if (a.hook === "p_discount" && !a.disabled && (a.tokens||[]).length < (a.tmax||5)) {
        var dc_gain = a._franchised ? 2 : 1;
        ns = setPl(ns, opp.id, function(p){ return { ...p, assets:p.assets.map(function(b){
          if (b.id !== a.id) return b;
          var dc_new=[...(b.tokens||[])]; for(var dci=0;dci<dc_gain&&dc_new.length<(b.tmax||5);dci++) dc_new.push({id:uid()});
          return { ...b, tokens:dc_new };
        }) }; });
      }
    });
  });
  // Reset buyer's Discount tokens (same as doBuyShop)
  ns = setPl(ns, cp.id, function(p){ return { ...p, assets:p.assets.map(function(a){
    return a.hook==="p_discount" ? { ...a, tokens:[] } : a;
  }) }; });
  ns = clearPressureTokens(ns, cp.id, "bought an asset");
  ns = addLog(ns, `🎲 ${cp.name} blind-buys "${asset.name}" for ${$(cost)}.`, "asset", [asset]);
  ns = pushTrigger(ns, { type:T.BUY_ASSET, srcPlayer:cp.id, value:cost });
  var hbd_rt = ns.triggerStack[ns.triggerStack.length-1];
  var hbd_rx = reactorsFor(ns.players, hbd_rt);
  if (hbd_rx.some(function(r){ return r.canReact; })) {
    return applyAllHandBoosts({ ...ns, pendingEffect:{ hook:"__buy_asset__", cost:cost, srcPlayer:cp.id },
      reactionWindow:buildWindow(uid(), hbd_rt.tid, T.BUY_ASSET, cp.id, null, hbd_rx) });
  }
  return applyAllHandBoosts(checkBankruptcy(ns, cp.id));
}

function handleActivateAsset(st, { assetId, ownerId, _skipAnimation }) {
  const owner = st.players.find(p => p.id===ownerId);
  const asset = owner?.assets.find(a => a.id===assetId);
  if (!asset) return st;
  if (asset.sub === AS.PASSIVE)    return addLog(st, "❌ Passive assets activate automatically.", "warn");
  if (asset.disabled || asset.status===ST.USED) return addLog(st, `❌ ${asset.name} unavailable.`, "warn");
  if (asset.hook === "a_paid_work")
    return addLog(st, "❌ Paid Work activates automatically on money events.", "warn");
  if (asset.hook === "a_product_update") {
    var pu_eligible = owner.assets.filter(function(a){
      return a.id !== asset.id && PU_FIELDS[a.hook] && PU_FIELDS[a.hook].length > 0;
    });
    if (!pu_eligible.length) return addLog(st, "❌ Product Update: no adjustable assets owned.", "warn");
  }
  if (asset.hook === "a_bribery") {
    var brib_myHand = owner.hand.filter(function(c){ return c.type !== CT.ASSET; });
    var brib_myMax = brib_myHand.reduce(function(s,c){ return s+(c.value||0); }, 0);
    var brib_tokens = (asset.tokens||[]).length;
    var brib_cost = asset.bribeTokenCost || 1;
    var brib_eligible = st.players.some(function(opp){
      if (opp.id === ownerId || opp.assets.length < 2) return false;
      return opp.assets.some(function(a){
        return brib_myMax > (a.origVal + brib_tokens * brib_cost);
      });
    });
    if (!brib_eligible) return addLog(st, "❌ Bribery: no eligible assets to steal (check hand value and opponent asset counts).", "warn");
  }
  if (asset.hook === "a_cfr") {
    var cfr_hasTarget = st.players.some(function(opp){
      return opp.id !== ownerId && opp.assets.some(function(a){ return !a.disabled; });
    });
    if (!cfr_hasTarget) return addLog(st, "❌ Closed for Remodeling: no opponent assets to deactivate.", "warn");
  }
  if (asset.hook === "a_multi_tool") {
    var mt_usable = owner.assets.filter(function(a){
      return a.id !== asset.id && a.status === ST.USED && !a.disabled && a.hook !== "a_multi_tool";
    });
    if (!mt_usable.length) return addLog(st, "❌ Multi-Tool: no used assets to reset.", "warn");
  }
  if (asset.hook === "a_three_of_kind") {
    var tok_need = asset.tripleCount || 3;
    var tok_val_req = asset.tripleValue || 2;
    var tok_hand = owner.hand;
    var tok_matching = tok_hand.filter(function(c){ return c.value === tok_val_req; }).length;
    if (tok_matching < tok_need) return addLog(st, "❌ Three of a Kind: need "+tok_need+" cards worth $"+tok_val_req+"k in hand (have "+tok_matching+").", "warn");
  }
  if (asset.hook === "a_severance_pay" && (asset.tokens||[]).length < (asset.tmax||10))
    return addLog(st, `❌ Severance Pay: ${(asset.tokens||[]).length}/${asset.tmax||10} tokens — not ready yet.`, "warn");
  if (asset.sub===AS.DURING && owner.id!==curPl(st).id) return addLog(st, "❌ During-Your-Turn assets only usable on your turn.", "warn");
  // Mark USED immediately (Defective Unit keeps it USED even if effect is cancelled)
  let ns = setPl(st, ownerId, p => ({ ...p, assets:p.assets.map(a => a.id===assetId ? {...a,status:ST.USED} : a) }));
  ns = addLog(ns, `⚙ ${owner.name} activates "${asset.name}".`, "asset", [asset]);
  if (_skipAnimation) {
    return resumeActivateAsset(ns, { assetId, ownerId });
  }
  return { ...ns, playAnimation:{ card:{ ...asset, type:CT.ASSET }, resumeAction:{
    type:"RESUME_ACTIVATE_ASSET", p:{ assetId, ownerId } } } };
}

function checkTargetedAd(st, cancellerPid, victimPid) {
  if (!cancellerPid || !victimPid || cancellerPid === victimPid) return st;
  var ns = st;
  var canceller = ns.players.find(function(p){ return p.id===cancellerPid; });
  if (!canceller) return ns;
  canceller.assets.forEach(function(a) {
    if (a.hook !== "p_targeted_ad" || a.disabled) return;
    var amt = (a.targetedAdAmount || 2) * (a._franchised ? 2 : 1);
    ns = payMoney(ns, victimPid, cancellerPid, amt, "Targeted Ad");
    ns = addLog(ns, "🎯 "+pname(ns,cancellerPid)+"'s Targeted Ad fires - "+pname(ns,victimPid)+" pays $"+amt+"k!", "asset", [], true);
  });
  return ns;
}

function revertPUEffect(st, puAsset) {
  if (!puAsset || !puAsset._appliedTo) return st;
  var app = puAsset._appliedTo;
  var ns = st;
  // Find the target asset on the target player
  var tgt_pl = ns.players.find(function(p){ return p.id===app.playerId; });
  if (!tgt_pl) return ns; // already gone
  var tgt_a = tgt_pl.assets.find(function(a){ return a.id===app.assetId; });
  if (!tgt_a) return ns;
  // Revert the field(s)
  ns = setPl(ns, app.playerId, function(p){ return { ...p, assets:p.assets.map(function(a){
    if (a.id !== app.assetId) return a;
    var reverted = { ...a, _puSourceId:null };
    if (app.field === "_creditLineAll") {
      reverted = { ...reverted, clValue:Math.max(0,(a.clValue||2)-app.delta),
        clPrice1:Math.max(0,(a.clPrice1||1)-app.delta), clPrice2:Math.max(0,(a.clPrice2||3)-app.delta),
        clPrice3:Math.max(0,(a.clPrice3||6)-app.delta) };
    } else {
      var cur = a[app.field] !== undefined ? a[app.field] : 0;
      reverted = { ...reverted, [app.field]: cur - app.delta };
    }
    return reverted;
  }) }; });
  ns = addLog(ns, "↩ Product Update effect on \""+tgt_a.name+"\" reverted ("+app.field+" "+app.delta+").", "info");
  return ns;
}

function checkPaidWork(st, affectedPid, amount, isGain, reason) {
  // Only fire if not already in a PW prompt and amount > 0
  if (st.pendingPWChoice || st._skipPWCheck || amount <= 0) return st;
  // Find a READY Paid Work owned by any player (owner can react to their own events too)
  var pw_owner = null, pw_asset = null;
  st.players.forEach(function(p) {
    if (pw_owner) return;
    p.assets.forEach(function(a) {
      if (pw_owner) return;
      if (a.hook === "a_paid_work" && !a.disabled && a.status === ST.READY) {
        pw_owner = p; pw_asset = a;
      }
    });
  });
  if (!pw_owner || !pw_asset) return st;
  var pw_amt = pw_asset.paidWorkAmount || 2;
  var pw_duration = 3500;
  return { ...st, pendingPWChoice:{
    type:"PAID_WORK_PROMPT", ownerId:pw_owner.id, assetId:pw_asset.id,
    affectedPid, amount, isGain, reason:reason||"",
    pwAmount:pw_amt, timerEnd:Date.now()+pw_duration, timerTotal:pw_duration
  }};
}

function checkInventoryDraw(st, drawerPid) {
  var ns = st;
  var drawer = ns.players.find(function(p){ return p.id===drawerPid; });
  if (!drawer) return ns;
  drawer.assets.forEach(function(a) {
    if (a.hook !== "p_check_inv" || a.disabled) return;
    var amt = (a.checkInvAmount || 1) * (a._franchised ? 2 : 1) * (drawer._doublePassive ? 2 : 1);
    ns = addMoney(ns, drawerPid, amt, "Checking Inventory (draw)");
    ns = addLog(ns, "📦 "+pname(ns,drawerPid)+"'s Checking Inventory fires - +$"+amt+"k!", "asset", [], true);
  });
  return ns;
}

function checkLetsDeal(st, activatorId, activatedAsset) {
  // Fire Let's Make a Deal for any player monitoring the activator of a non-passive asset
  if (!activatedAsset || activatedAsset.sub === AS.PASSIVE) return st;
  var ns = st;
  ns.players.forEach(function(owner) {
    if (owner.id === activatorId) return;
    owner.assets.forEach(function(a) {
      if (a.hook !== "a_lets_deal" || a.disabled || a.letsDealTarget !== activatorId) return;
      var ldAmt = (a.letsDealAmount || 1);
      ns = addMoney(ns, owner.id, ldAmt, "Let's Make a Deal ("+pname(ns,activatorId)+" activated)");
      ns = addLog(ns, "🤝 "+pname(ns,owner.id)+"'s Let's Make a Deal fires - +$"+ldAmt+"k!", "asset", [], true);
    });
  });
  return ns;
}

function resumeActivateAsset(st, { assetId, ownerId }) {
  const owner2 = st.players.find(p => p.id===ownerId);
  const asset2 = owner2?.assets.find(a => a.id===assetId);
  if (!asset2) return st;
  let ns = st;
  ns = pushTrigger(ns, { type:T.ACTIVATE_ASSET, srcPlayer:ownerId, label:asset2.name, srcCard:asset2 });
  var actTrig = ns.triggerStack[ns.triggerStack.length - 1];
  var actReactors = reactorsFor(ns.players, actTrig);
  // Store the asset hook so it can be applied after the reaction window resolves
  ns = { ...ns, pendingEffect:{ hook:asset2.hook, srcPlayer:ownerId, tgtPlayer:null, value:null, srcCard:{ ...asset2 } } };
  if (actReactors.some(function(r){ return r.canReact; })) {
    return { ...ns, reactionWindow:buildWindow(uid(), actTrig.tid, T.ACTIVATE_ASSET, ownerId, null, actReactors) };
  }
  // No reactors - apply hook directly
  ns = { ...ns, pendingEffect:null };
  ns = applyHook(ns, asset2.hook, { srcPlayer:ownerId, tgtPlayer:null, value:null, srcCard:asset2 });
  ns = checkLetsDeal(ns, ownerId, asset2);
  return maybeFlush(ns);
}


function finishEndTurn(st) {
  var ns2 = flushPendingDiscard({ ...st }); // clear any cards still in limbo
  // Clear Mark-Up adjustments from current player's hand
  var mu_cp0 = ns2.players[ns2.curIdx];
  if (mu_cp0 && mu_cp0.hand.some(function(c){ return c._markupDelta; })) {
    ns2 = setPl(ns2, mu_cp0.id, function(p){ return {...p, hand:p.hand.map(function(c){ return c._markupDelta ? {...c,_markupDelta:0} : c; })}; });
    ns2 = applyAllHandBoosts(ns2);
  }
  var cp2 = curPl(ns2);
  // Clear Part Time Work lock and Double Shift flag for the player whose turn is ending
  if (cp2.actionLocked) {
    ns2 = setPl(ns2, cp2.id, function(p) { return { ...p, actionLocked:false }; });
    ns2 = addLog(ns2, "✅ " + cp2.name + "'s Part Time Work restriction lifted.", "info");
  }
  if (cp2._doublePassive) {
    ns2 = setPl(ns2, cp2.id, function(p) { return { ...p, _doublePassive:false }; });
  }
  // Re-enable any assets taxed "until end of cp2's turn"
  var reEnabled = [];
  ns2 = { ...ns2, players: ns2.players.map(function(p) {
    var changed = false;
    var newAssets = p.assets.map(function(a) {
      if (a.taxedUntilEndOf === cp2.id) {
        changed = true;
        reEnabled.push(p.name + "'s \"" + a.name + "\"");
        return { ...a, disabled:false, taxedUntilEndOf:null, _taxedBy:null };
      }
      return a;
    });
    return changed ? { ...p, assets:newAssets } : p;
  })};
  if (reEnabled.length) {
    ns2 = addLog(ns2, "✅ Tax expired: " + reEnabled.join(", ") + " re-enabled.", "info");
    ns2 = applyAllHandBoosts(ns2);
  }
  // Re-enable assets disabled by Downsizing when the targeted player ends their current turn
  var downsizeReEnabled = [];
  ns2 = { ...ns2, players: ns2.players.map(function(p) {
    var changed = false;
    var newAssets = p.assets.map(function(a) {
      if (a.downsizedUntilEndOf === cp2.id) {
        changed = true;
        downsizeReEnabled.push(p.name + "'s \"" + a.name + "\"");
        return { ...a, disabled:false, downsizingBy:null, downsizedUntilEndOf:null };
      }
      return a;
    });
    return changed ? { ...p, assets:newAssets } : p;
  })};
  if (downsizeReEnabled.length) {
    ns2 = addLog(ns2, "✅ Downsizing expired: " + downsizeReEnabled.join(", ") + " re-enabled.", "info");
  }
  ns2 = addLog(ns2, "✅ " + cp2.name + " ends their turn.", "turn");
  ns2 = pushTrigger(ns2, { type:T.END_TURN, srcPlayer:cp2.id });
  // R&D Budget: discard hand if rndActive
  var cp2b = ns2.players[ns2.curIdx];
  if (cp2b && cp2b.rndActive) {
    var rnd_hand = cp2b.hand || [];
    if (rnd_hand.length) {
      ns2 = { ...ns2, mainDiscard:[...ns2.mainDiscard, ...rnd_hand.map(function(c){ return {...c,value:c.origVal,modifiedBy:[]}; })] };
      ns2 = addLog(ns2, "🔬 R&D Budget: "+cp2b.name+" discards their hand ("+rnd_hand.length+" card"+(rnd_hand.length!==1?"s":"")+").", "asset");
    }
    ns2 = setPl(ns2, cp2b.id, function(p){ return { ...p, hand:[], rndActive:false }; });
  }
  // Pressure Campaign: penalise if >= threshold pressure tokens; only clear penalised entries
  var cp2c = ns2.players[ns2.curIdx];
  if (cp2c && cp2c.pressureTokens) {
    var pt_cleared = {};
    Object.keys(cp2c.pressureTokens).forEach(function(ownerPid) {
      var count = cp2c.pressureTokens[ownerPid];
      if (!count) return;
      var pc_owner = ns2.players.find(function(p){ return p.id===ownerPid; });
      if (!pc_owner) return;
      pc_owner.assets.forEach(function(a) {
        if (a.hook !== "p_pressure" || a.disabled) return;
        var threshold = a.pressureThreshold || 3;
        var penalty = (a.pressurePenalty || 4) * (a._franchised ? 2 : 1);
        if (count >= threshold) {
          ns2 = addLog(ns2, "🔴 "+cp2c.name+" pays $"+penalty+"k to "+pname(ns2,ownerPid)+" (Pressure Campaign)!", "loss");
          ns2 = payMoney(ns2, cp2c.id, ownerPid, penalty, "Pressure Campaign");
          pt_cleared[ownerPid] = true;
        }
      });
    });
    // Only clear tokens that triggered the penalty; accumulating ones stay
    if (Object.keys(pt_cleared).length) {
      var pt_remaining = {};
      Object.keys(cp2c.pressureTokens).forEach(function(k){
        if (!pt_cleared[k]) pt_remaining[k] = cp2c.pressureTokens[k];
      });
      ns2 = setPl(ns2, cp2c.id, function(p){ return { ...p, pressureTokens:pt_remaining }; });
    }
  }
  ns2 = { ...ns2, triggerStack:[] };
  // Revenue Stream: gain per token at end of turn; reset at tmax
  var rs_cp = curPl(ns2);
  if (rs_cp) rs_cp.assets.forEach(function(a) {
    if (a.hook !== "p_revenue_stream" || a.disabled) return;
    var rs_toks = (a.tokens||[]).length;
    if (rs_toks === 0) return;
    var rs_is_max = rs_toks >= (a.tmax||3);
    // At max tokens: flat bonus payout only (no per-token payment)
    var rs_amt = rs_is_max
      ? (a.revenueMaxPayout||5) * (a._franchised?2:1)
      : (a.revenueAmount||1) * rs_toks;
    ns2 = addMoney(ns2, rs_cp.id, rs_amt, "Revenue Stream");
    ns2 = addLog(ns2, "💰 "+pname(ns2,rs_cp.id)+"'s Revenue Stream "
      +(rs_is_max ? "max payout: $"+rs_amt+"k!" : "pays $"+rs_amt+"k ("+rs_toks+" token"+(rs_toks!==1?"s":"")+")!"),
      "asset", [], true);
    if (rs_is_max) {
      ns2 = setPl(ns2, rs_cp.id, function(p){ return {...p, assets:p.assets.map(function(b){ return b.id===a.id ? {...b,tokens:[]} : b; })}; });
      ns2 = addLog(ns2, "🔄 Revenue Stream tokens reset!", "asset", [], true);
    }
  });
  // R&D Budget: gain token at end of turn if no action was played this turn
  var rnd_cp_end = curPl(ns2);
  var rnd_was_activated = ns2.players[ns2.curIdx] && ns2.players[ns2.curIdx]._rndActivatedThisTurn;
  // Clear the flag
  if (rnd_was_activated) ns2 = setPl(ns2, ns2.players[ns2.curIdx].id, function(p){ return {...p, _rndActivatedThisTurn:false}; });
  if (rnd_cp_end && !ns2.actionsTaken.includes(ACT.ACTION) && !rnd_was_activated) {
    rnd_cp_end.assets.forEach(function(a) {
      if (a.hook !== "a_rnd_budget" || a.disabled) return;
      var rnd_cur2 = a.tokens ? a.tokens.length : 0;
      if (rnd_cur2 < (a.tmax||3)) {
        ns2 = setPl(ns2, rnd_cp_end.id, function(pl){ return { ...pl, assets:pl.assets.map(function(b){
          return b.id===a.id ? { ...b, tokens:[...(b.tokens||[]),{id:uid()}] } : b;
        }) }; });
        ns2 = addLog(ns2, "🔬 "+pname(ns2,rnd_cp_end.id)+"'s R&D Budget: token gained for skipping an action ("+(rnd_cur2+1)+"/"+(a.tmax||3)+").", "asset", [], true);
      }
    });
  }
  // Empty Handed: draw cards if ending turn with empty hand
  var eh_cp = curPl(ns2);
  if (eh_cp && (eh_cp.hand||[]).length === 0) {
    eh_cp.assets.forEach(function(a) {
      if (a.hook !== "p_empty_handed" || a.disabled) return;
      var eh_n = (a.emptyHandedAmount||2) * (a._franchised ? 2 : 1);
      ns2 = addLog(ns2, "🃏 "+pname(ns2,eh_cp.id)+"'s Empty Handed fires - drawing "+eh_n+" card"+(eh_n!==1?"s":"")+"!", "asset", [], true);
      ns2 = drawN(ns2, eh_cp.id, eh_n, 0, true);
    });
  }
  return advanceTurn(ns2);
}

function handleEndTurn(st) {
  var cp = curPl(st);
  var limit  = cp.handLimit;
  var needed = cp.hand.length - limit;
  if (needed > 0) {
    return {
      ...st,
      pendingChoice: {
        type: "DISCARD_TO_LIMIT",
        srcPlayer: cp.id,
        needed: needed,
        limit: limit,
        timerEnd: Date.now() + 20000,
        prompt: "Discard " + needed + " card" + (needed > 1 ? "s" : "") + " to return to the hand limit of " + limit + ".",
      }
    };
  }
  return finishEndTurn(st);
}

function checkAssetDeckExhausted(st) {
  if (st._finalRound) return st;
  if ((st.assetDeck||[]).length === 0 && (st.assetDiscard||[]).length === 0) {
    return addLog({ ...st, _finalRound:true }, "⚠ Asset deck exhausted - this is the final round!", "danger");
  }
  return st;
}

function advanceTurn(st) {
  const ni = (st.curIdx + 1) % st.players.length;
  const isNewRound = ni === 0;
  const tl = isNewRound ? st.turnsLeft - 1 : st.turnsLeft;
  if (st._finalRound && isNewRound) {
    let ns = addLog(st, "🏁 Final round complete - Game Over!", "danger");
    return calcWinner({ ...ns, gameOver:true, _finalRound:false });
  }
  if (!st._finalRound && tl <= 0) {
    let ns = addLog(st, "🏁 Turns left = 0 - Game Over!", "danger");
    return calcWinner({ ...ns, gameOver:true });
  }
  return {
    ...st, curIdx:ni, viewIdx:ni, phase:PH.SOT, startTurnPending:true,
    actionsLeft:DEF_ACTIONS, actionsTaken:[], hasBoughtAsset:false, rwActionsBonus:0,
    turnNum:st.turnNum+1, roundNum:isNewRound ? st.roundNum+1 : st.roundNum, turnsLeft:tl,
    selectedCardId:null, awaitingTarget:null, reactionWindow:null, pendingDiscard:[], pocketChangeState:null, kickstarterState:null, letGoState:null, negReactionState:null, totalLossState:null, shutDownState:null, discardQueue:null, playAnimation:null, retainerQueue:[], swearJarQueue:[], wdQueue:[], negReactionPayQueue:[], bpIntent:null, rcIntent:null, equityIntent:null, rwActionsBonus:0, pcIntent:null, borrowState:null, mrState:null, coverCostsState:null,
    savedReactionWindow:null, pendingReactionCard:null, deferredWindow:null, shrinkageState:null, pendingPaidWork:null, toasts:st.toasts||[], moneyEvents:st.moneyEvents||[], cfrHistory:st.cfrHistory||[], pendingPWChoice:null,
  };
}

function calcWinner(st) {
  // Consolation Prize: players who still own it and never filed bankruptcy gain free assets
  var ns_cw = st;
  st.players.forEach(function(p) {
    if (p._filedBankruptcy) return; // only fires if never bankrupt
    var cp_a = p.assets.find(function(a){ return a.hook==="p_consolation" && !a.disabled; });
    if (!cp_a) return;
    var cp_gain_cw = (cp_a.consolationGain||1) * (cp_a._franchised ? 2 : 1);
    ns_cw = addLog(ns_cw, "🎁 "+pname(ns_cw,p.id)+"'s Consolation Prize: never went bankrupt - gains "+cp_gain_cw+" asset(s) before scoring!", "asset");
    for (var i=0; i<cp_gain_cw; i++) {
      var cp_res_cw = tryReshuffle(ns_cw.assetDeck, ns_cw.assetDiscard);
      if (cp_res_cw.empty) { ns_cw=addLog(ns_cw,"Consolation Prize: deck empty.","warn"); break; }
      if (cp_res_cw.reshuffled) ns_cw={...ns_cw,assetDeck:cp_res_cw.deck,assetDiscard:[]};
      var cp_new_cw = cp_res_cw.deck[0];
      var cp_cw_toks = cp_new_cw.hook==="a_credit_line"?Array.from({length:cp_new_cw.tmax||3},function(){return{id:uid()};}):[]; 
      ns_cw = setPl(ns_cw, p.id, function(pl){ return {...pl,assets:[...pl.assets,{...cp_new_cw,status:ST.READY,tokens:cp_cw_toks,disabled:false}]}; });
      ns_cw = {...ns_cw, assetDeck:cp_res_cw.deck.slice(1)};
      ns_cw = addLog(ns_cw, "  → "+pname(ns_cw,p.id)+' gains "'+cp_new_cw.name+'" ('+$(cp_new_cw.origVal)+'k).', "asset", [cp_new_cw]);
    }
  });
  const scores = ns_cw.players.map(p => {
    const assetVal = p.assets.reduce((s,a) => s+a.origVal, 0);
    const netWorth = p.money + assetVal;
    const points = Math.floor(netWorth / 10) + p.assets.length;
    return { ...p, assetVal, netWorth, points };
  }).sort((a,b) => {
    if (b.points !== a.points) return b.points - a.points;
    if (b.netWorth !== a.netWorth) return b.netWorth - a.netWorth;
    return b.assetVal - a.assetVal;
  });
  return { ...ns_cw, scores, winner:scores[0] };
}

/* REDUCER */
function reducer(state, action) {
  const st = state;
  if (!st && action.type !== "INIT") return state;
  switch (action.type) {
    case "INIT":           return initGame(action.names, action.turns, action.disabledNames, action.countOverrides, action.freeActions);
    case "RESTORE_STATE":  return action.state;
    case "START_TURN":     return handleStartTurn(st);
    case "RESUME_ACTION_PLAYED": return resumeActionPlayed(st, action.p);
    // - Block gameplay actions while a reaction window is open -
    case "DRAW":
    case "PLAY_ACTION":
    case "SELL":
    case "BUY_SHOP":
    case "BUY_DECK":
    case "ACTIVATE":
    case "END_TURN":
    case "SET_TARGET": {
      if (st.reactionWindow) return addLog(st, "⏳ Wait for reactions to resolve first.", "warn");
      // Fall through to specific handlers below
      if (action.type === "DRAW")       return handleDrawExtra(st);
      if (action.type === "SELL")       return handleSellCard(st, action.p);
      if (action.type === "BUY_SHOP")   return handleBuyShop(st, action.p);
      if (action.type === "BUY_DECK")   return handleBuyDeck(st);
      if (action.type === "END_TURN")   return handleEndTurn(st);
      if (action.type === "SET_TARGET") return handlePlayAction(st, { cardId:action.cardId, targetId:action.targetId });
      if (action.type === "PLAY_ACTION") {
        if (action.p._skipAnimation) return handlePlayAction(st, action.p);
        var cpA = curPl(st);
        var cardA = cpA.hand.find(function(c){ return c.id === action.p.cardId; });
        if (!cardA) return handlePlayAction(st, action.p);
        return { ...st, playAnimation:{ card:cardA, resumeAction:{ type:"PLAY_ACTION", p:{ ...action.p, _skipAnimation:true } } } };
      }
      if (action.type === "ACTIVATE") {
        if (action.p._skipAnimation) return handleActivateAsset(st, action.p);
        var ownerAct = st.players.find(function(p){ return p.id === action.p.ownerId; });
        var assetAct = ownerAct && ownerAct.assets.find(function(a){ return a.id === action.p.assetId; });
        if (!assetAct) return handleActivateAsset(st, action.p);
        return { ...st, playAnimation:{ card:assetAct, resumeAction:{ type:"ACTIVATE", p:{ ...action.p, _skipAnimation:true } } } };
      }
      return st;
    }
    case "_DRAW":           return handleDrawExtra(st);  // internal unused, kept for safety
    case "PLAY_REACTION": {
      if (action.p._skipAnimation) return handlePlayReaction(st, action.p);
      var ownerR = st.players.find(function(p){ return p.id === action.p.ownerId; });
      var cardR = ownerR && ownerR.hand.find(function(c){ return c.id === action.p.cardId; });
      if (!cardR) return handlePlayReaction(st, action.p);
      // Immediately mark this reactor as PLAYING in the window so UI locks in
      var updatedWindow = st.reactionWindow ? {
        ...st.reactionWindow,
        reactors:st.reactionWindow.reactors.map(function(r){
          return r.pid === action.p.ownerId ? {...r, decision:"PLAYING"} : r;
        })
      } : st.reactionWindow;
      return { ...st, reactionWindow:null, playAnimation:{ card:cardR, resumeAction:{ type:"PLAY_REACTION", p:{ ...action.p, _skipAnimation:true, _savedWindow:updatedWindow } } } };
    }
    case "RESOLVE_PW_CHOICE": {
      if (!st.pendingPWChoice) return st;
      const pw_pc = st.pendingPWChoice;
      const ns_pw = { ...st, pendingPWChoice:null };
      // Re-use resolveChoice logic with the PAID_WORK_PROMPT type
      return resolveChoice({ ...ns_pw, pendingChoice:pw_pc }, action.id);
    }
    case "RESOLVE_CHOICE": return resolveChoice(st, action.id);
    case "DISMISS_CHOICE": return { ...st, pendingChoice:null };
    case "RESOLVE_CHOICE_PHASE_BACK": {
      if (!st.pendingChoice || st.pendingChoice.type !== "OUT_OF_ORDER_SELECT") return st;
      var ooo_opponents = st.players.filter(function(p){
        return p.id !== st.pendingChoice.srcPlayer && p.assets.some(function(a){ return !a.disabled; });
      });
      return { ...st, pendingChoice:{ ...st.pendingChoice,
        phase:"opponent",
        selectedOpponent:null,
        selectedOpponentName:null,
        assets:[],
        opponents:ooo_opponents.map(function(p){ return { id:p.id, name:p.name }; }),
      }};
    }
    case "PAY_OUT_OF_ORDER": {
      // action.effectId = the outOfOrderEffect id to resolve
      var poo_effect = (st.outOfOrderEffects||[]).find(function(e){ return e.id === action.effectId; });
      if (!poo_effect) return st;
      var poo_pid    = poo_effect.ownerId;
      var poo_payee  = poo_effect.payeeId;
      var poo_amt    = poo_effect.payAmt;
      if (st.players.find(function(p){return p.id===poo_pid;}).money < 1) {
        return addLog(st, "❌ Not enough money to pay Out of Order fee.", "warn");
      }
      // Pay immediately - per spec note 6, asset re-enables regardless of reaction result
      var poo_ns = payMoney(st, poo_pid, poo_payee, poo_amt, "Out of Order fee");
      // Re-enable the asset and strip OOO fields
      poo_ns = setPl(poo_ns, poo_pid, function(p){
        return { ...p, assets:p.assets.map(function(a){
          if (a.id !== poo_effect.assetId) return a;
          var clean = { ...a, disabled:false };
          delete clean.outOfOrderBy;
          delete clean.outOfOrderPayAmt;
          return clean;
        })};
      });
      // Remove from global effects list
      poo_ns = { ...poo_ns, outOfOrderEffects:(poo_ns.outOfOrderEffects||[]).filter(function(e){ return e.id !== poo_effect.id; }) };
      poo_ns = addLog(poo_ns, "✅ " + poo_effect.ownerName + " pays " + $(poo_amt) + " - " + poo_effect.assetName + " is back in service!", "asset");

      return maybeFlush(poo_ns);
    }
    case "ENG_REACT": {
      const { pid, cardId, assetId } = action;
      const owner = st.players.find(p => p.id===pid);
      if (!owner) return st;
      var rwType = st.reactionWindow ? st.reactionWindow.ttype : null;

      // - Helper: handle cancel reaction window -
      function doCancel(ns, isReactionWindow) {
        if (isReactionWindow) {
          // Fired cancels the pending reaction card; restore outer window
          ns = addLog(ns, "🚫 Reaction cancelled!", "reaction");
          var savedWin = ns.savedReactionWindow;
          ns = { ...ns, reactionWindow:null, pendingReactionCard:null, savedReactionWindow:null };
          if (savedWin && savedWin.reactors && savedWin.reactors.some(function(r){ return r.decision === "PENDING"; })) {
            ns = { ...ns, reactionWindow:savedWin };
            return ns;
          }
          // If a sell was pending (e.g. LMB cancelled), complete it
          if (ns.pendingEffect && ns.pendingEffect.hook === "__sold_card__") {
            return resolveSell(ns);
          }
          return maybeFlush(ns);
        }
        // - Shrinkage TARGET_PLAYER cancelled (Counteroffer) -
        // pendingEffect is NOT cleared here so __resume_action__ survives
        if (!isReactionWindow && ns.shrinkageState && ns.shrinkageState.phase === "targeting") {
          ns = addLog(ns, "🚫 Shrinkage blocked - no discard forced.", "reaction");
          ns = { ...ns, reactionWindow:null, shrinkageState:null };
          var sh_dw_c = ns.deferredWindow;
          ns = { ...ns, deferredWindow:null };
          if (sh_dw_c) return { ...ns, reactionWindow:sh_dw_c };
          return maybeFlush(flushPendingDiscard(ns));
        }
        // - LOSE_MONEY window cancelled (Fine Print / Insurance / Bailout) -        // Loss is negated, but any steal effect tied to it still proceeds (per design - Note 3)
        if (rwType === T.LOSE_MONEY) {
          ns = addLog(ns, "🛡 Money loss negated!", "reaction");
          ns = { ...ns, reactionWindow:null };
          var lm_cancel_pe = ns.pendingEffect;
          if (lm_cancel_pe && lm_cancel_pe.hook === "outside_hire_steal") {
            ns = { ...ns, pendingEffect:null };
            return maybeFlush(applyHook(ns, "outside_hire_steal", lm_cancel_pe));
          }
          ns = { ...ns, pendingEffect:null };
          return maybeFlush(ns);
        }
        // Ignored or Counteroffer: cancel the action/targeting
        ns = addLog(ns, "🚫 Effect cancelled!", "reaction");
        // Bribery cancelled: return selected cards to owner's hand
        if (ns.pendingEffect && ns.pendingEffect.hook === "apply_bribery" && ns.pendingEffect.bribeCardIds) {
          // Cards not yet discarded at this point (discard happens in apply_bribery after TARGET_PLAYER resolves)
          // So nothing to restore - cards are still in hand.
        }
        ns = { ...ns, reactionWindow:null, pendingEffect:null };        if (ns.pocketChangeState && ns.pocketChangeState.phase === "targeting") {
          var pcsC2 = ns.pocketChangeState;
          ns = { ...ns, pocketChangeState:{ ...pcsC2, currentTarget:null } };
          return advancePocketChangeTargeting(flushPendingDiscard(ns));
        }
        if (ns.kickstarterState && ns.kickstarterState.phase === "targeting") {
          var ksC2 = ns.kickstarterState;
          ns = { ...ns, kickstarterState:{ ...ksC2, currentTarget:null } };
          return advanceKickstarterTargeting(flushPendingDiscard(ns));
        }
        if (ns.letGoState && ns.letGoState.phase === "targeting") {
          var lgC2 = ns.letGoState;
          ns = { ...ns, letGoState:{ ...lgC2, currentTarget:null } };
          return advanceLetGoTargeting(flushPendingDiscard(ns));
        }
        if (ns.negReactionState && ns.negReactionState.phase === "targeting") {
          var nrC2 = ns.negReactionState;
          ns = { ...ns, negReactionState:{ ...nrC2, currentTarget:null } };
          return advanceNegReactionTargeting(flushPendingDiscard(ns));
        }
        if (ns.discardQueue && ns.discardQueue.currentDiscard) {
          // Discard was blocked - skip item, advance queue without adding to discarded
          var dqBlocked = ns.discardQueue;
          ns = addLog(ns, "🛡 " + pname(ns, dqBlocked.currentDiscard.pid) + "'s discard of \"" + dqBlocked.currentDiscard.card.name + "\" was blocked!", "reaction");
          ns = { ...ns, discardQueue:{ ...dqBlocked, currentDiscard:null } };
          return advanceDiscardQueue(flushPendingDiscard(ns));
        }
        return flushPendingDiscard(ns);
      }

      // - Helper: resume after non-cancel reaction -
      function doResume(ns) {
        // If applyHook opened a reaction window, pending choice (e.g. SHRINKAGE_SELECT),
        // or shrinkageState - wait for it to resolve before resuming the outer action.
        if (ns.reactionWindow) return ns;
        if (ns.pendingChoice) return ns;
        if (ns.shrinkageState) return ns;
        ns = { ...ns, reactionWindow:null };
        if (rwType === T.REACTION_PLAYED) {
          return applyAndClearPendingReaction(ns);
        }
        if (rwType === T.ACTION_PLAYED) {
          var pe2 = ns.pendingEffect;
          if (pe2 && pe2.hook === "__resume_action__") {
            ns = { ...ns, pendingEffect:null };
            return continueAfterActionPlayed(ns, pe2);
          }
          return maybeFlush(ns);
        }
        // TARGET_PLAYER or other: existing PC/KS/pendingEffect paths
        if (ns.pocketChangeState && ns.pocketChangeState.phase === "targeting") {
          var pcsNC2 = ns.pocketChangeState;
          var cNC2 = [...pcsNC2.confirmedTargets, pcsNC2.currentTarget];
          ns = { ...ns, pendingEffect:null, pocketChangeState:{ ...pcsNC2, currentTarget:null, confirmedTargets:cNC2 } };
          return advancePocketChangeTargeting(ns);
        }
        if (ns.kickstarterState && ns.kickstarterState.phase === "targeting") {
          var ksNC2 = ns.kickstarterState;
          var cKsNC2 = [...ksNC2.confirmedTargets, ksNC2.currentTarget];
          ns = { ...ns, pendingEffect:null, kickstarterState:{ ...ksNC2, currentTarget:null, confirmedTargets:cKsNC2 } };
          return advanceKickstarterTargeting(ns);
        }
        if (ns.letGoState && ns.letGoState.phase === "targeting") {
          var lgNC2 = ns.letGoState;
          var cLgNC2 = [...lgNC2.confirmedTargets, lgNC2.currentTarget];
          ns = { ...ns, pendingEffect:null, letGoState:{ ...lgNC2, currentTarget:null, confirmedTargets:cLgNC2 } };
          return advanceLetGoTargeting(ns);
        }
        if (ns.negReactionState && ns.negReactionState.phase === "targeting") {
          var nrNC2 = ns.negReactionState;
          var cNrNC2 = [...nrNC2.confirmedTargets, nrNC2.currentTarget];
          ns = { ...ns, pendingEffect:null, negReactionState:{ ...nrNC2, currentTarget:null, confirmedTargets:cNrNC2 } };
          return advanceNegReactionTargeting(ns);
        }
        if (ns.discardQueue && ns.discardQueue.currentDiscard) {
          // Discard confirmed - execute it
          return executeDiscard(ns, ns.discardQueue.currentDiscard);
        }
        // - Shrinkage TARGET_PLAYER resolved - proceed to forced discard -
        if (ns.shrinkageState && ns.shrinkageState.phase === "targeting") {
          return executeShrinkageDiscard(ns);
        }
        // - BUY_ASSET window resolved - continue after buy -
        if (rwType === T.BUY_ASSET) {
          if (ns.pendingReactionCard) return applyAndClearPendingReaction(ns);
          return maybeFlush(checkBankruptcy(ns, ns.reactionWindow ? ns.reactionWindow.srcPlayer : curPl(ns).id));
        }
        // - SELL_CARD window resolved - complete the sale -
        if (rwType === T.SELL_CARD) {
          return resolveSell(ns);
        }
        // - ACTIVATE_ASSET window resolved - apply the asset hook -
        if (rwType === T.ACTIVATE_ASSET) {
          var pe_aa = ns.pendingEffect;
          if (pe_aa) {
            ns = { ...ns, pendingEffect:null };
            ns = applyHook(ns, pe_aa.hook, { srcPlayer:pe_aa.srcPlayer, tgtPlayer:pe_aa.tgtPlayer, value:pe_aa.value, srcCard:pe_aa.srcCard });
          }
          return maybeFlush(ns);
        }
        // - LOSE_MONEY window confirmed (no reaction played) -
        if (rwType === T.LOSE_MONEY) {
          var lm_resume_pe = ns.pendingEffect;
          if (lm_resume_pe && lm_resume_pe.hook === "outside_hire_steal") {
            ns = { ...ns, pendingEffect:null };
            ns = addMoney(ns, lm_resume_pe.srcPlayer, -lm_resume_pe.value, "Outside Hire (asset cost)");
            return maybeFlush(applyHook(ns, "outside_hire_steal", lm_resume_pe));
          }
          ns = { ...ns, pendingEffect:null };
          return maybeFlush(ns);
        }
        if (ns.pendingEffect) {
          var pe3 = ns.pendingEffect;
          ns = { ...ns, pendingEffect:null };
          ns = applyHook(ns, pe3.hook, { srcPlayer:pe3.srcPlayer, tgtPlayer:pe3.tgtPlayer, value:pe3.value, srcCard:pe3.srcCard });
        }
        return maybeFlush(ns);
      }

      var isCancelHook = function(h) {
        return h === "cancel_whole_action" || h === "cancel_reaction" || h === "not_a_chance";
      };
      var isReactionWindowType = rwType === T.REACTION_PLAYED;

      if (cardId) {
        const card = owner.hand.find(c => c.id===cardId);
        if (!card) return st;
        if (!action._skipAnimation) {
          return { ...st, playAnimation:{ card, resumeAction:{ type:"ENG_REACT", pid, cardId, _skipAnimation:true } } };
        }
        let ns = setPl(st, pid, p => ({ ...p, hand:p.hand.filter(c=>c.id!==cardId) }));
        ns = { ...ns, pendingDiscard:[...(ns.pendingDiscard||[]), card] };
        ns = addLog(ns, "🛡 " + owner.name + " reacts with \"" + card.name + "\".", "reaction", [card]);
        ns = checkSwearJar(ns, pid);  // Swear Jar: charge reactor once
        ns = applyValueTaxPassives(ns, pid, card.value);
        // Push REACTION_PLAYED so Fired/Whistleblower can respond - but only one level deep.
        // If savedReactionWindow is already set we are in a sub-window; apply directly.
        var alreadyInSubWindow = !!ns.savedReactionWindow;
        // When reacting to a sub-window, auto-PASS this reactor in the outer window
        // so the outer window doesn't reopen just waiting for this player to decide.
        if (alreadyInSubWindow && ns.savedReactionWindow) {
          ns = { ...ns, savedReactionWindow: { ...ns.savedReactionWindow,
            reactors: ns.savedReactionWindow.reactors.map(function(r){
              return r.pid === pid ? { ...r, decision:"PASSED" } : r;
            })
          }};
        }
        if (!alreadyInSubWindow) {
          ns = pushTrigger(ns, { type:T.REACTION_PLAYED, srcPlayer:pid, label:card.name, value:card.value });
          var erTrig = ns.triggerStack[ns.triggerStack.length-1];
          var erReactors = reactorsFor(ns.players, erTrig);
          // Store pendingReactionCard so applyAndClearPendingReaction knows what to do
          var erTrigSrc = st.reactionWindow ? st.reactionWindow.srcPlayer : pid;
          var erPRC = { hook:card.hook, srcPlayer:pid, tgtPlayer:erTrigSrc, value:card.value, srcCard:card };
          ns = { ...ns, pendingReactionCard:erPRC };
          if (erReactors.some(function(r){ return r.canReact; })) {
            // Mark the reacting player as REACTED in the outer window before saving it
            var erSaveWin = ns.reactionWindow ? {
              ...ns.reactionWindow,
              reactors: ns.reactionWindow.reactors.map(function(r){
                return r.pid===pid ? {...r, decision:"REACTED"} : r;
              })
            } : null;
            return { ...ns,
              savedReactionWindow: erSaveWin,
              reactionWindow: buildWindow(uid(), erTrig.tid, T.REACTION_PLAYED, pid, null, erReactors) };
          }
          // No reactors - apply immediately (clear PRC first)
          ns = { ...ns, pendingReactionCard:null };
          // For shrinkage: defer the ACTION_PLAYED or SELL_CARD window BEFORE erAllDone closes it
          if (card.hook === "shrinkage" && ns.reactionWindow && !ns.savedReactionWindow &&
              (ns.reactionWindow.ttype === T.ACTION_PLAYED || ns.reactionWindow.ttype === T.SELL_CARD)) {
            ns = { ...ns, deferredWindow:ns.reactionWindow };
          }
          // Mark reactor REACTED; close window if all decisions are in
          if (ns.reactionWindow && !ns.savedReactionWindow) {
            var erUpdatedW = { ...ns.reactionWindow,
              reactors:ns.reactionWindow.reactors.map(function(r){
                return r.pid===pid ? {...r,decision:"REACTED"} : r;
              })
            };
            var erAllDone = erUpdatedW.reactors.every(function(r){ return r.decision!=="PENDING"; });
            ns = { ...ns, reactionWindow:erAllDone ? null : erUpdatedW };
          }
        }
        if (isCancelHook(card.hook)) {
          // Targeted Ad: fires when cancel_whole_action (Ignored) cancels an action
          if (card.hook === "cancel_whole_action") {
            var cwa_victim = st.reactionWindow ? st.reactionWindow.srcPlayer : null;
            if (cwa_victim) ns = checkTargetedAd(ns, pid, cwa_victim);
          }
          // Targeted Ad: fires when cancel_reaction (Fired) cancels an opponent's reaction
          if (card.hook === "cancel_reaction") {
            var cr_prc = ns.pendingReactionCard;
            if (cr_prc && cr_prc.srcPlayer && cr_prc.srcPlayer !== pid) {
              ns = checkTargetedAd(ns, pid, cr_prc.srcPlayer);
            }
          }
          return doCancel(ns, isReactionWindowType);
        }
        var erDirectTgt = st.reactionWindow ? st.reactionWindow.srcPlayer : pid;
        // Mark this reactor REACTED in the current sub-window and close it if all done
        if (ns.reactionWindow && !ns.savedReactionWindow) {
          var erDirect_updated = { ...ns.reactionWindow,
            reactors: ns.reactionWindow.reactors.map(function(r){
              return r.pid === pid ? {...r, decision:"REACTED"} : r;
            })
          };
          var erDirect_allDone = erDirect_updated.reactors.every(function(r){ return r.decision !== "PENDING"; });
          ns = { ...ns, reactionWindow: erDirect_allDone ? null : erDirect_updated };
        } else if (ns.reactionWindow && ns.savedReactionWindow) {
          // We are in the alreadyInSubWindow path — close current sub-window
          var erDirect_sub = { ...ns.reactionWindow,
            reactors: ns.reactionWindow.reactors.map(function(r){
              return r.pid === pid ? {...r, decision:"REACTED"} : r;
            })
          };
          var erDirect_subDone = erDirect_sub.reactors.every(function(r){ return r.decision !== "PENDING"; });
          ns = { ...ns, reactionWindow: erDirect_subDone ? null : erDirect_sub };
        }
        ns = applyHook(ns, card.hook, { srcPlayer:pid, tgtPlayer:erDirectTgt, value:card.value, srcCard:card });
        return doResume(ns);
      }
      if (assetId) {
        const asset = owner.assets.find(a => a.id===assetId);
        if (!asset) return st;
        let ns = setPl(st, pid, p => ({ ...p, assets:p.assets.map(a=>a.id===assetId?{...a,status:ST.USED}:a) }));
        ns = addLog(ns, "⚙ " + owner.name + " uses \"" + asset.name + "\" as reaction.", "reaction", [asset]);
        if (isCancelHook(asset.hook)) return doCancel(ns, isReactionWindowType);
        ns = applyHook(ns, asset.hook, { srcPlayer:pid, tgtPlayer:pid, value:null, srcCard:asset });
        return doResume(ns);
      }
      return st;
    }
    case "ENG_PASS": {
      if (!st.reactionWindow) return st;
      const w = { ...st.reactionWindow, reactors:st.reactionWindow.reactors.map(r=>r.pid===action.pid?{...r,decision:"PASSED"}:r) };
      if (w.reactors.every(r=>r.decision!=="PENDING")) {
        var rwTypeP = st.reactionWindow.ttype;
        let ns = { ...st, reactionWindow:null };
        if (rwTypeP === T.REACTION_PLAYED) {
          return applyAndClearPendingReaction(ns);
        }
        if (rwTypeP === T.START_TURN) {
          // No one played Close of Business - continue turn normally; check recycle
          var sot_cp = curPl(ns);
          if (sot_cp && sot_cp.assets.some(function(a){ return a.hook==="p_recycle"&&!a.disabled; }) && ns.mainDiscard && ns.mainDiscard.length) {
            var sot_rc_top = ns.mainDiscard[ns.mainDiscard.length-1];
            var sot_rc_fran = sot_cp.assets.some(function(ax){ return ax.hook==="p_recycle"&&!ax.disabled&&ax._franchised; });
            return { ...ns, pendingChoice:{ type:"RECYCLE_CHOICE", srcPlayer:sot_cp.id, recycleCount:sot_rc_fran?2:1, timerEnd:Date.now()+15000, topCard:sot_rc_top, options:[sot_rc_top] } };
          }
          return maybeFlush(ns);
        }
        if (rwTypeP === T.ACTION_PLAYED) {
          var peP = ns.pendingEffect;
          if (peP && peP.hook === "__resume_action__") {
            ns = { ...ns, pendingEffect:null };
            return continueAfterActionPlayed(ns, peP);
          }
          return maybeFlush(ns);
        }
        if (rwTypeP === T.BUY_ASSET) {
          if (ns.pendingReactionCard) return applyAndClearPendingReaction(ns);
          var ba_pid = ns.reactionWindow ? ns.reactionWindow.srcPlayer : curPl(ns).id;
          ns = { ...ns, pendingEffect:null }; // clear __buy_asset__ cost
          return maybeFlush(checkBankruptcy(ns, ba_pid));
        }
        if (rwTypeP === T.SELL_CARD) {
          if (ns.pendingReactionCard) return applyAndClearPendingReaction(ns);
          return resolveSell(ns);
        }
        if (rwTypeP === T.ACTIVATE_ASSET) {
          if (ns.pendingReactionCard) return applyAndClearPendingReaction(ns);
          var pe_aa_p = ns.pendingEffect;
          if (pe_aa_p) {
            ns = { ...ns, pendingEffect:null };
            ns = applyHook(ns, pe_aa_p.hook, { srcPlayer:pe_aa_p.srcPlayer, tgtPlayer:pe_aa_p.tgtPlayer, value:pe_aa_p.value, srcCard:pe_aa_p.srcCard });
            ns = checkLetsDeal(ns, pe_aa_p.srcPlayer, pe_aa_p.srcCard);
          }
          return maybeFlush(ns);
        }
        // TARGET_PLAYER / LOSE_MONEY / PAY_MONEY - original paths
        if (ns.shrinkageState && ns.shrinkageState.phase === "targeting") {
          return executeShrinkageDiscard(ns);
        }
        if (ns.letGoState && ns.letGoState.phase === "targeting") {
          var lgP2 = ns.letGoState;
          var cLgP2 = [...lgP2.confirmedTargets, lgP2.currentTarget];
          ns = { ...ns, letGoState:{ ...lgP2, currentTarget:null, confirmedTargets:cLgP2 } };
          return advanceLetGoTargeting(ns);
        }
        if (ns.negReactionState && ns.negReactionState.phase === "targeting") {
          var nrP2 = ns.negReactionState;
          var cNrP2 = [...nrP2.confirmedTargets, nrP2.currentTarget];
          ns = { ...ns, negReactionState:{ ...nrP2, currentTarget:null, confirmedTargets:cNrP2 } };
          return advanceNegReactionTargeting(ns);
        }
        if (ns.pocketChangeState && ns.pocketChangeState.phase === "targeting") {
          var pcsP2 = ns.pocketChangeState;
          var cP2 = [...pcsP2.confirmedTargets, pcsP2.currentTarget];
          ns = { ...ns, pocketChangeState:{ ...pcsP2, currentTarget:null, confirmedTargets:cP2 } };
          return advancePocketChangeTargeting(ns);
        }
        if (ns.kickstarterState && ns.kickstarterState.phase === "targeting") {
          var ksP2 = ns.kickstarterState;
          var cKsP2 = [...ksP2.confirmedTargets, ksP2.currentTarget];
          ns = { ...ns, kickstarterState:{ ...ksP2, currentTarget:null, confirmedTargets:cKsP2 } };
          return advanceKickstarterTargeting(ns);
        }
        if (ns.pendingEffect && ns.pendingEffect.hook !== "__resume_action__") {
          const pe = ns.pendingEffect;
          ns = { ...ns, pendingEffect:null };
          ns = applyHook(ns, pe.hook, { srcPlayer:pe.srcPlayer, tgtPlayer:pe.tgtPlayer, value:pe.value, srcCard:pe.srcCard });
        }
        return maybeFlush(ns);
      }
      return { ...st, reactionWindow:w };
    }
    case "ENG_PASS_ALL": {
      var rwTypePA = st.reactionWindow ? st.reactionWindow.ttype : null;
      let ns = { ...st, reactionWindow:null };
      if (rwTypePA === T.REACTION_PLAYED) {
        return applyAndClearPendingReaction(ns);
      }
      if (rwTypePA === T.START_TURN) {
        var sot_cp_pa = curPl(ns);
        if (sot_cp_pa && sot_cp_pa.assets.some(function(a){ return a.hook==="p_recycle"&&!a.disabled; }) && ns.mainDiscard && ns.mainDiscard.length) {
          var sot_rc_top_pa = ns.mainDiscard[ns.mainDiscard.length-1];
          var sot_rc_fran_pa = sot_cp_pa.assets.some(function(ax){ return ax.hook==="p_recycle"&&!ax.disabled&&ax._franchised; });
          return { ...ns, pendingChoice:{ type:"RECYCLE_CHOICE", srcPlayer:sot_cp_pa.id, recycleCount:sot_rc_fran_pa?2:1, timerEnd:Date.now()+15000, topCard:sot_rc_top_pa, options:[sot_rc_top_pa] } };
        }
        return maybeFlush(ns);
      }
      if (rwTypePA === T.ACTION_PLAYED) {
        var pePA = ns.pendingEffect;
        if (pePA && pePA.hook === "__resume_action__") {
          ns = { ...ns, pendingEffect:null };
          return continueAfterActionPlayed(ns, pePA);
        }
        return maybeFlush(ns);
      }
      if (rwTypePA === T.BUY_ASSET) {
        if (ns.pendingReactionCard) return applyAndClearPendingReaction(ns);
        var ba_pid_pa = ns.reactionWindow ? ns.reactionWindow.srcPlayer : curPl(ns).id;
        ns = { ...ns, pendingEffect:null };
        return maybeFlush(checkBankruptcy(ns, ba_pid_pa));
      }
      if (rwTypePA === T.SELL_CARD) {
        if (ns.pendingReactionCard) return applyAndClearPendingReaction(ns);
        return resolveSell(ns);
      }
      if (rwTypePA === T.ACTIVATE_ASSET) {
        if (ns.pendingReactionCard) return applyAndClearPendingReaction(ns);
        var pe_aa_pa = ns.pendingEffect;
        if (pe_aa_pa) {
          ns = { ...ns, pendingEffect:null };
          ns = applyHook(ns, pe_aa_pa.hook, { srcPlayer:pe_aa_pa.srcPlayer, tgtPlayer:pe_aa_pa.tgtPlayer, value:pe_aa_pa.value, srcCard:pe_aa_pa.srcCard });
          ns = checkLetsDeal(ns, pe_aa_pa.srcPlayer, pe_aa_pa.srcCard);
        }
        return maybeFlush(ns);
      }
      if (ns.shrinkageState && ns.shrinkageState.phase === "targeting") {
        return executeShrinkageDiscard(ns);
      }
      if (ns.letGoState && ns.letGoState.phase === "targeting") {
        var lgPA = ns.letGoState;
        var cLgPA = [...lgPA.confirmedTargets, lgPA.currentTarget];
        ns = { ...ns, letGoState:{ ...lgPA, currentTarget:null, confirmedTargets:cLgPA } };
        return advanceLetGoTargeting(ns);
      }
      if (ns.negReactionState && ns.negReactionState.phase === "targeting") {
        var nrPA = ns.negReactionState;
        var cNrPA = [...nrPA.confirmedTargets, nrPA.currentTarget];
        ns = { ...ns, negReactionState:{ ...nrPA, currentTarget:null, confirmedTargets:cNrPA } };
        return advanceNegReactionTargeting(ns);
      }
      if (ns.pocketChangeState && ns.pocketChangeState.phase === "targeting") {
        var pcsPA = ns.pocketChangeState;
        var cPA = [...pcsPA.confirmedTargets, pcsPA.currentTarget];
        ns = { ...ns, pocketChangeState:{ ...pcsPA, currentTarget:null, confirmedTargets:cPA } };
        return advancePocketChangeTargeting(ns);
      }
      if (ns.kickstarterState && ns.kickstarterState.phase === "targeting") {
        var ksPA = ns.kickstarterState;
        var cKsPA = [...ksPA.confirmedTargets, ksPA.currentTarget];
        ns = { ...ns, kickstarterState:{ ...ksPA, currentTarget:null, confirmedTargets:cKsPA } };
        return advanceKickstarterTargeting(ns);
      }
      if (ns.pendingEffect && ns.pendingEffect.hook !== "__resume_action__") {
        const pe = ns.pendingEffect;
        ns = { ...ns, pendingEffect:null };
        ns = applyHook(ns, pe.hook, { srcPlayer:pe.srcPlayer, tgtPlayer:pe.tgtPlayer, value:pe.value, srcCard:pe.srcCard });
      }
      return maybeFlush(ns);
    }
    case "OPEN_WINDOW": {
      const trig = st.triggerStack.find(t=>t.tid===action.tid);
      if (!trig) return st;
      return openReactionWindow(st, action.tid);
    }
    case "DISMISS_TOAST":        return { ...st, toasts:(st.toasts||[]).filter(t=>t.id!==action.id) };
    case "DISMISS_MONEY_EVENT": return { ...st, moneyEvents:(st.moneyEvents||[]).filter(e=>e.id!==action.id) };
    case "SET_VIEW":       return { ...st, viewIdx:action.i };
    case "SELECT_CARD":    return { ...st, selectedCardId:action.id, awaitingTarget:null };
    case "AWAIT_TARGET":   return { ...st, awaitingTarget:action.cardId, selectedCardId:null };
    case "OPEN_DETAIL":    return { ...st, detailCard:action.card, detailShopInfo:action.shopInfo||null };
    case "CLOSE_DETAIL":   return { ...st, detailCard:null, detailShopInfo:null };
    case "ADJ_MONEY": {
      let ns = setPl(st, action.pid, p => ({ ...p, money:p.money+action.delta }));
      return checkBankruptcy(ns, action.pid);
    }
    case "ANIMATION_DONE_NOOP": return { ...st, playAnimation:null };
    case "ANIMATION_DONE": {
      var resume = st.playAnimation && st.playAnimation.resumeAction;
      var ns_ad = { ...st, playAnimation:null };
      if (!resume) return ns_ad;
      if (resume.type === "RESUME_ACTION_PLAYED") return resumeActionPlayed(ns_ad, resume.p);
      if (resume.type === "RESUME_ACTIVATE_ASSET") return resumeActivateAsset(ns_ad, resume.p);
      if (resume.type === "RESUME_REACTION_PLAYED") return resumeReactionPlayed(ns_ad, resume.p);
      return reducer(ns_ad, resume);
    }
    case "RESOLVE_PW": {
      var pw = st.pendingPaidWork;
      if (!pw) return st;
      var pw_ns = { ...st, pendingPaidWork:null };
      var pw_choice = action.choice; // "increase" | "decrease" | "skip" | "auto"
      if (!pw_choice || pw_choice === "skip" || pw_choice === "auto") return maybeFlush(pw_ns);
      // Find the PW asset
      var pw_owner_pl = pw_ns.players.find(function(p){ return p.id===pw.ownerId; });
      var pw_asset = pw_owner_pl && pw_owner_pl.assets.find(function(a){ return a.hook==="a_paid_work"&&!a.disabled&&a.status===ST.READY; });
      if (!pw_asset) return maybeFlush(pw_ns);
      // Mark USED
      pw_ns = setPl(pw_ns, pw.ownerId, function(p){ return {...p, assets:p.assets.map(function(a){ return a.id===pw_asset.id?{...a,status:ST.USED}:a; })}; });
      var pw_adj = pw_asset.payWorkAmount || 2;
      var pw_change = 0;
      if (pw_choice === "increase") {
        // Increase absolute amount: gain more or lose more
        pw_change = pw.delta > 0 ? pw_adj : -pw_adj;
      } else { // decrease
        // Decrease absolute amount, floor at 0
        if (pw.delta > 0) pw_change = -Math.min(pw_adj, pw.delta);
        else pw_change = Math.min(pw_adj, Math.abs(pw.delta));
      }
      if (pw_change !== 0) {
        pw_ns = addMoney(pw_ns, pw.pid, pw_change, "Paid Work ("+(pw_change>0?"+":"")+pw_change+"k adjustment)", true, true, true);
        pw_ns = addLog(pw_ns, "💼 "+pname(pw_ns,pw.ownerId)+"'s Paid Work adjusts "+pname(pw_ns,pw.pid)+"'s "+(pw.delta>0?"gain":"loss")+" by "+(pw_change>0?"+":"")+pw_change+"k!", "asset", [], true);
      }
      return maybeFlush(pw_ns);
    }
    case "FORCE_END_GAME": return calcWinner({ ...st, gameOver:true, turnsLeft:0 });
    case "RESET": return null;
    case "CLEAR_SHOP_ANIM": return { ...st, shopRefreshAnim:false };
    default: return st;
  }
}

/* UI COMPONENTS */
function monopolyBlockedValues(st, pid) {
  var s = new Set();
  var pl = st.players.find(function(p){return p.id===pid;});
  if (!pl) return s;
  st.players.forEach(function(opp){
    if (opp.id===pid) return;
    opp.assets.forEach(function(a){
      if (a.hook==="a_monopoly"&&!a.disabled&&a.lockedCard) s.add(a.lockedCard.value);
    });
  });
  return s;
}

const btn = (bg, bd, tx, dis=false) => ({
  background:bg, border:`1px solid ${bd}`, color:tx, borderRadius:6,
  padding:"6px 14px", cursor:dis?"not-allowed":"pointer",
  fontFamily:"'Inter',sans-serif", fontSize:12, fontWeight:600,
  opacity:dis?0.4:1, transition:"all .15s", lineHeight:1.4,
});

function Overlay({ children, onClose }) {
  return (
    <div onClick={onClose}
      style={{ position:"fixed",inset:0,background:"#000000cc",zIndex:1000,
               display:"flex",alignItems:"center",justifyContent:"center",animation:"popIn .15s ease" }}>
      <div onClick={e=>e.stopPropagation()}
        style={{ background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:24,
                 maxWidth:600,width:"92%",maxHeight:"85vh",overflowY:"auto",
                 animation:"popIn .2s ease",boxShadow:"0 25px 50px #00000099" }}>
        {children}
      </div>
    </div>
  );
}

function MoneyBar({ money }) {
  const mn=-9, mx=20, pct=clmp((money-mn)/(mx-mn)*100,0,100), neg=money<0;
  return (
    <div>
      <div style={{ display:"flex",justifyContent:"space-between",fontSize:9,color:C.muted,marginBottom:3 }}>
        <span>-$9k</span><span>$0</span><span>+$20k</span>
      </div>
      <div style={{ height:8,background:"#0f172a",borderRadius:4,position:"relative",border:`1px solid ${C.border}` }}>
        <div style={{ position:"absolute",left:`${clmp((0-mn)/(mx-mn)*100,0,100)}%`,top:-1,bottom:-1,width:1,background:C.border }} />
        <div style={{ height:"100%",width:`${pct}%`,borderRadius:4,background:neg?"#7f1d1d":"#14532d",transition:"width .4s" }} />
        <div style={{ position:"absolute",top:-3,left:`${pct}%`,transform:"translateX(-50%)",
                      width:14,height:14,borderRadius:"50%",transition:"left .4s",
                      background:neg?C.red:C.green,border:"2px solid #e2e8f0",
                      boxShadow:`0 0 8px ${neg?C.red+"66":C.green+"66"}` }} />
      </div>
    </div>
  );
}

function CardSvg({ card, w=88, h=54 }) {
  const isP = card.sub===AS.PASSIVE;
  const c = isP ? CUI.PASSIVE_ASSET : CUI[card.type] || CUI[CT.ACTION];
  const init = card.name.split(" ").map(w=>w[0]).join("").slice(0,3).toUpperCase();
  const gid = "g" + card.id.slice(0,6);
  return (
    <svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ borderRadius:4,display:"block",flexShrink:0 }}>
      <defs>
        <linearGradient id={gid} x1="0%" y1="0%" x2="100%" y2="100%">
          <stop offset="0%" stopColor={c.bg} />
          <stop offset="100%" stopColor={c.bd+"55"} />
        </linearGradient>
      </defs>
      <rect width={w} height={h} fill={`url(#${gid})`} rx={4} />
      <rect x={1} y={1} width={w-2} height={h-2} fill="none" stroke={c.bd} strokeWidth={.8} rx={3.5} strokeOpacity={.6} />
      <text x={w/2} y={h/2+1} textAnchor="middle" dominantBaseline="middle"
            fontSize={h*.3} fontFamily="Rubik,sans-serif" fill={c.ac} opacity={.9}>{init}</text>
    </svg>
  );
}

function CardComp({ card, selected, onClick, onDetail, small, isUsed, noImg, costColor, isPlayable, actionLocked, isBpFree, monopolyBlocked, reactionOnly }) {
  const isP = card.sub===AS.PASSIVE;
  const c   = isP ? {...CUI.PASSIVE_ASSET, lbl:"ASSET"} : CUI[card.type] || CUI[CT.ACTION];
  const sub = card.sub ? SUB_UI[card.sub] : null;
  const bd  = selected ? C.gold : isUsed ? "#374151" : c.bd;
  const W   = small ? 76 : noImg ? 100 : 112;
  const H   = small ? 106 : noImg ? 76 : 152;
  const [hovered, setHovered] = useState(false);
  const canInteract = !isUsed && !!onClick;
  const dimmed = isPlayable === false;
  const showMonoOverlay = monopolyBlocked && !small;
  const isBogo = !!(card && card._isBogo);
  const isFranchised = !!(card && card._franchised);
  const scale = selected ? "translateY(-4px) scale(1.06)"
              : isUsed   ? "rotate(5deg)"
              : hovered && canInteract ? "scale(1.05)"
              : "none";
  return (
    <div onClick={isUsed ? undefined : onClick}
      onMouseEnter={canInteract ? ()=>setHovered(true)  : undefined}
      onMouseLeave={canInteract ? ()=>setHovered(false) : undefined}
      onContextMenu={e=>{e.preventDefault();onDetail?.();}}
      title="Right-click for details"
      style={{ width:W, minHeight:H, border:`2px solid ${bd}`, borderRadius:8,
               background:isUsed?"#1e293b":c.bg, padding:6,
               display:"flex",flexDirection:"column",gap:3,
               position:"relative",flexShrink:0,cursor:isUsed?"default":onClick?"pointer":"default",
               boxShadow:selected?`0 0 12px ${C.gold}66`:hovered&&canInteract?`0 4px 14px #00000088`:"0 2px 6px #00000055",
               opacity:isUsed?0.4:dimmed?0.6:1,
               filter:dimmed?"saturate(0.35) brightness(0.72)":"none",
               transition:"transform .12s ease, box-shadow .12s ease, filter .12s ease",
               transform:scale }}>
      <div style={{ display:"flex",justifyContent:"space-between",gap:2 }}>
        <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:small?8:9,color:"#f1f5f9",fontWeight:700,lineHeight:1.2,flex:1 }}>{card.name}</div>
        <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:small?9:11,color:costColor||C.gold,fontWeight:700,whiteSpace:"nowrap" }}>
          ${card.value}k
          {card.value!==card.origVal && card.origVal!==undefined && <span style={{ fontSize:small?6:8,color:C.muted,textDecoration:"line-through",marginLeft:3 }}>{card.origVal}</span>}
        </div>
      </div>
      {!small && !noImg && <CardSvg card={card} w={W-12} h={Math.round((W-12)*.56)} />}
      {!small && <div style={{ fontSize:8,color:C.sub,lineHeight:1.45,flex:1,overflow:"hidden" }}>{getDesc(card)}</div>}
      {/* Token pips */}
      {(card.tmax > 0) && (
        <div style={{ display:"flex",flexWrap:"wrap",gap:2,marginTop:1 }}>
          {Array.from({length:card.tmax},(_,i) => (
            <div key={i} style={{ width:8,height:8,borderRadius:"50%",
                                  background:i<(card.tokens?.length||0)?C.gold:"#334155",
                                  border:`1px solid ${i<(card.tokens?.length||0)?"#d97706":"#475569"}` }} />
          ))}
          <span style={{ fontSize:7,color:C.muted,alignSelf:"center" }}>{card.tokens?.length||0}/{card.tmax}</span>
        </div>
      )}
      <div style={{ fontSize:7,fontWeight:600,marginTop:"auto",color:sub?sub.col:c.ac }}>{sub?sub.lbl:c.lbl}</div>
      {card && card._puSourceId && !small && (
        <div title="Modified by Product Update" style={{ position:"absolute", bottom:4, left:4, zIndex:5,
                      background:"#5b21b6", borderRadius:3, padding:"1px 4px",
                      fontSize:7, fontWeight:800, color:"#ddd6fe", letterSpacing:1 }}>
          PU
        </div>
      )}
      {isFranchised && !small && (
        <div style={{ position:"absolute", top:4, right:4, zIndex:5,
                      background:"#b45309", borderRadius:3, padding:"1px 4px",
                      fontSize:7, fontWeight:800, color:"#fde68a", letterSpacing:1 }}>
          ×2
        </div>
      )}
      {isBogo && !small && (
        <div style={{ position:"absolute", bottom:4, right:4, zIndex:5,
                      background:"#7c3aed", borderRadius:3, padding:"1px 4px",
                      fontSize:7, fontWeight:800, color:"#fff", letterSpacing:1 }}>
          BOGO
        </div>
      )}
      {reactionOnly && !small && (
        <div title="Auto-plays in reaction windows" style={{ position:"absolute", bottom:4, right:4, zIndex:5,
                      background:"rgba(220,38,38,0.85)", borderRadius:3, padding:"1px 5px",
                      fontSize:7, fontWeight:800, color:"#fecaca", letterSpacing:0.5,
                      display:"flex", alignItems:"center", gap:2 }}>
          🛡 AUTO
        </div>
      )}
      {showMonoOverlay && (
        <div style={{ position:"absolute", inset:0, borderRadius:"inherit",
                      background:"rgba(220,38,38,0.18)", display:"flex",
                      alignItems:"center", justifyContent:"center",
                      border:"2px solid #ef4444", zIndex:4 }}>
          <div style={{ fontSize:8, fontWeight:800, color:"#ef4444",
                        letterSpacing:2, textAlign:"center", lineHeight:1.3,
                        textShadow:"0 1px 4px #000" }}>MONOPOLY</div>
        </div>
      )}
      {card && card.disabled && card._cfrBy && !small && (
        <div style={{ position:"absolute", inset:0, borderRadius:"inherit",
                      background:"rgba(15,23,42,0.5)", display:"flex",
                      alignItems:"center", justifyContent:"center",
                      border:"2px solid #f97316", zIndex:4 }}>
          <div style={{ fontSize:7, fontWeight:800, color:"#f97316",
                        letterSpacing:2, textAlign:"center", lineHeight:1.4,
                        padding:"2px 4px", background:"rgba(0,0,0,0.6)", borderRadius:2 }}>
            CLOSED<br/><span style={{fontSize:6,fontWeight:400}}>by {card._cfrBy}</span>
          </div>
        </div>
      )}
      {card && card.disabled && card._taxedBy && !card.shutDownBy && !small && (
        <div style={{ position:"absolute", inset:0, borderRadius:"inherit",
                      background:"rgba(100,116,139,0.35)", display:"flex",
                      alignItems:"center", justifyContent:"center",
                      border:"2px solid #64748b", zIndex:3 }}>
          <div style={{ fontSize:7, fontWeight:800, color:"#94a3b8",
                        letterSpacing:2, textAlign:"center", lineHeight:1.4,
                        padding:"2px 4px", background:"rgba(0,0,0,0.5)", borderRadius:2 }}>
            TAXED<br/><span style={{fontSize:6,fontWeight:400}}>by {card._taxedBy}</span>
          </div>
        </div>
      )}
      {isUsed && (
        <div style={{ position:"absolute",top:"50%",left:"50%",
                      transform:"translate(-50%,-50%) rotate(-15deg)",
                      fontSize:10,fontFamily:"'Rubik',sans-serif",fontWeight:700,
                      color:"#ef4444",border:"2px solid #ef4444",
                      padding:"2px 6px",borderRadius:3,opacity:.85 }}>USED</div>
      )}
      {card.disabled && card.shutDownBy && !isUsed && !card._cfrBy && !card._taxedBy && !card.outOfOrderBy && (
        <div style={{ position:"absolute",top:"50%",left:"50%",
                      transform:"translate(-50%,-50%) rotate(-12deg)",
                      fontSize:8,fontFamily:"'Rubik',sans-serif",fontWeight:700,
                      color:"#ef4444",border:"2px solid #ef4444",
                      padding:"2px 5px",borderRadius:3,opacity:.9,whiteSpace:"nowrap" }}>SHUT DOWN</div>
      )}
      {card.disabled && !isUsed && !card.outOfOrderBy && !card.shutDownBy && !card._cfrBy && !card._taxedBy && (
        <div style={{ position:"absolute",top:"50%",left:"50%",
                      transform:"translate(-50%,-50%) rotate(-12deg)",
                      fontSize:9,fontFamily:"'Rubik',sans-serif",fontWeight:700,
                      color:"#f97316",border:"2px solid #f97316",
                      padding:"2px 6px",borderRadius:3,opacity:.9 }}>DISABLED</div>
      )}
      {card.disabled && card.outOfOrderBy && !isUsed && (
        <div style={{ position:"absolute",top:0,left:0,right:0,bottom:0,
                      display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",
                      borderRadius:6,background:"rgba(0,0,0,0.55)",gap:3,cursor:"pointer" }}>
          <div style={{ transform:"rotate(-12deg)",fontSize:9,fontFamily:"'Rubik',sans-serif",fontWeight:700,
                        color:"#fbbf24",border:"2px solid #fbbf24",
                        padding:"2px 6px",borderRadius:3,opacity:.95,whiteSpace:"nowrap" }}>OUT OF ORDER</div>
          <div style={{ fontSize:7,color:"#94a3b8",fontWeight:600,letterSpacing:.5,textAlign:"center",
                        padding:"0 4px",lineHeight:1.4 }}>tap for details</div>
        </div>
      )}
      {actionLocked && card.type===CT.ACTION && !isUsed && (
        <div style={{ position:"absolute",top:"50%",left:"50%",
                      transform:"translate(-50%,-50%) rotate(-10deg)",
                      fontSize:8,fontFamily:"'Rubik',sans-serif",fontWeight:700,
                      color:"#a78bfa",border:"2px solid #a78bfa",
                      padding:"2px 5px",borderRadius:3,opacity:.92,whiteSpace:"nowrap" }}>NO PLAY</div>
      )}
    </div>
  );
}

/* - Money Popup System - */
function TickingMoney({ target, color }) {
  const [display, setDisplay] = useState(0);
  const prevRef = useRef(0);
  useEffect(() => {
    const from = prevRef.current;
    prevRef.current = target;
    if (from === target) { setDisplay(target); return; }
    const diff = target - from;
    const steps = Math.min(Math.abs(diff) * 3, 18);
    let cur = from, i = 0;
    const id = setInterval(() => {
      i++;
      cur = i >= steps ? target : cur + diff / steps;
      setDisplay(Math.round(cur));
      if (i >= steps) clearInterval(id);
    }, 28);
    return () => clearInterval(id);
  }, [target]);
  const abs = Math.abs(display);
  const sign = display > 0 ? "+" : display < 0 ? "-" : "+";
  return (
    <div style={{
      fontSize:32, fontFamily:"'JetBrains Mono',monospace", fontWeight:800,
      color, lineHeight:1, letterSpacing:-1,
      textShadow:`0 0 24px ${color}55`,
      animation:"moneyLineIn .3s cubic-bezier(.22,.8,.36,1)",
    }}>
      {sign}{$(abs)}
    </div>
  );
}

function MoneyPopupOverlay({ events, dispatch, enabled, mobile, myPlayerId }) {
  const [activeEv, setActiveEv] = useState(null);
  const [phase, setPhase]       = useState(0);
  const [dispTotal, setDispTotal] = useState(0);
  const [leaving, setLeaving]   = useState(false);
  const busyRef   = useRef(false);
  const timersRef = useRef([]);

  var clearTimers = function() { timersRef.current.forEach(clearTimeout); timersRef.current = []; }

  // Drain queue silently when feature is disabled
  useEffect(() => {
    if (!enabled && events.length > 0) {
      events.forEach(ev => dispatch({ type:"DISMISS_MONEY_EVENT", id:ev.id }));
    }
  }, [enabled, events.length]);

  // Process next event in queue
  useEffect(() => {
    if (!enabled || busyRef.current || events.length === 0) return;
    const ev = events[0];
    busyRef.current = true;
    clearTimers();
    setActiveEv(ev);
    setPhase(0);
    setDispTotal(ev.lines[0]?.amount || 0);
    setLeaving(false);

    const lines = ev.lines || [];
    const PER_LINE = 800;   // ms between line transitions
    const HOLD     = 950;   // hold after last line
    const FADE     = 480;   // fade-out duration

    for (let i = 1; i < lines.length; i++) {
      const idx = i;
      timersRef.current.push(setTimeout(() => {
        setPhase(idx);
        setDispTotal(prev => prev + lines[idx].amount);
      }, idx * PER_LINE));
    }
    const lastDelay = (lines.length - 1) * PER_LINE;
    timersRef.current.push(setTimeout(() => setLeaving(true), lastDelay + HOLD));
    timersRef.current.push(setTimeout(() => {
      dispatch({ type:"DISMISS_MONEY_EVENT", id:ev.id });
      setActiveEv(null);
      setLeaving(false);
      busyRef.current = false;
    }, lastDelay + HOLD + FADE));

    return function() { clearTimers(); busyRef.current = false; };
  }, [events.length, enabled]);

  if (!activeEv) return null;

  const lines    = activeEv.lines || [];
  const curLine  = lines[Math.min(phase, lines.length - 1)] || { amount:0, reason:"" };
  const isPos    = activeEv.total >= 0;
  const amtColor = isPos ? "#4ade80" : "#f87171";

  return (
    <div style={{
      position: mobile ? "fixed" : "absolute",
      zIndex: mobile ? 500 : 200,
      // Mobile layout: own events top, other-player events bottom — both framed
      ...(mobile ? (
        (myPlayerId && activeEv && activeEv.pid && activeEv.pid !== myPlayerId) ? {
          // Other player's money: bottom third
          bottom: 110, left: 0, right: 0, top: "auto",
          display:"flex", alignItems:"center", justifyContent:"center",
          background: "none", backdropFilter: "none",
        } : {
          // My own money event: top third
          top: 80, left: 0, right: 0, bottom: "auto",
          display:"flex", alignItems:"center", justifyContent:"center",
          background: "none", backdropFilter: "none",
        }
      ) : {
        inset: 0,
        display:"flex", alignItems:"center", justifyContent:"center",
        background: "rgba(8,6,4,0.86)",
        backdropFilter: "blur(3px)",
      }),
      pointerEvents:"none",
      animation: leaving ? "moneyOverlayOut .48s ease forwards" : "moneyOverlayIn .15s ease",
    }}>
      <div style={mobile ? {
        background:"rgba(13,21,32,0.92)", border:"2px solid #334155",
        borderRadius:12, padding:"10px 18px", textAlign:"center",
        boxShadow:"0 4px 20px rgba(0,0,0,0.7)"
      } : { textAlign:"center", padding:"6px 10px" }}>
        {/* Big ticking total - key=activeEv.id keeps it mounted through phases */}
        {/* On mobile, show whose money this is when it's not yours */}
        {mobile && activeEv.playerName && (
          <div style={{ fontSize:13, fontWeight:700, marginBottom:3,
                        fontFamily:"'Rubik',sans-serif", textAlign:"center",
                        color: myPlayerId && activeEv.pid !== myPlayerId ? "#94a3b8" : (activeEv.total > 0 ? "#4ade80" : "#f87171") }}>
            {myPlayerId && activeEv.pid !== myPlayerId
              ? activeEv.playerName + " is " + (activeEv.total > 0 ? "gaining" : "losing")
              : "You are " + (activeEv.total > 0 ? "gaining" : "losing")}
          </div>
        )}
        <TickingMoney key={activeEv.id} target={dispTotal} color={amtColor} />
        {/* Source label - key changes per phase -> remounts -> animation replays */}
        <div key={`${activeEv.id}-${phase}`} style={{
          marginTop:6, fontSize:10, fontWeight:600,
          fontFamily:"'JetBrains Mono',monospace",
          color:"#94a3b8", letterSpacing:.3,
          animation:"moneyLineIn .22s ease",
          maxWidth:130, margin:"6px auto 0",
          whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis",
        }}>
          {curLine.amount >= 0 ? "+" : ""}{$(Math.abs(curLine.amount))} . {curLine.reason}
        </div>
      </div>
    </div>
  );
}

/* - Toast Layer - */
function ToastLayer({ toasts, dispatch }) {
  return (
    <div style={{ position:"fixed",bottom:24,right:18,zIndex:1200,
                  display:"flex",flexDirection:"column-reverse",gap:8,pointerEvents:"none" }}>
      {(toasts||[]).slice(-6).map(t => (
        <Toast key={t.id} toast={t} dispatch={dispatch} />
      ))}
    </div>
  );
}
function Toast({ toast, dispatch }) {
  const [leaving, setLeaving] = useState(false);
  const typeCol = toast.type==="money"||toast.type==="asset" ? C.gold
                : toast.type==="loss" ? "#f87171" : "#34d399";
  useEffect(() => {
    const fadeT = setTimeout(() => setLeaving(true), 5000);
    const removeT = setTimeout(() => dispatch({ type:"DISMISS_TOAST", id:toast.id }), 5500);
    return () => { clearTimeout(fadeT); clearTimeout(removeT); };
  }, []);
  return (
    <div style={{
      background:"#231e19", border:`1px solid ${typeCol}66`,
      borderLeft:`3px solid ${typeCol}`,
      borderRadius:8, padding:"8px 14px",
      fontSize:12, color:"#f1f5f9", fontWeight:600,
      boxShadow:`0 4px 20px #000000aa, 0 0 10px ${typeCol}22`,
      maxWidth:280, pointerEvents:"auto",
      animation: leaving ? "toastOut .5s ease forwards" : "toastIn .25s cubic-bezier(.22,.8,.36,1)",
      cursor:"pointer",
    }} onClick={() => dispatch({ type:"DISMISS_TOAST", id:toast.id })}>
      {(toast.msg||'').replace(/^[^a-zA-Z$"(\[]+/, '')}
    </div>
  );
}

/* - Animated Money - */
function AnimatedMoney({ value, style }) {
  const [display, setDisplay] = useState(value);
  const [delta, setDelta] = useState(null);
  const prevRef = useRef(value);
  useEffect(() => {
    const prev = prevRef.current;
    if (prev === value) return;
    const diff = value - prev;
    prevRef.current = value;
    setDelta(diff);
    // Tick toward new value
    const steps = Math.min(Math.abs(diff), 12);
    const step  = diff / steps;
    let cur = prev;
    let i = 0;
    const tick = setInterval(() => {
      i++;
      cur = i === steps ? value : Math.round(cur + step);
      setDisplay(cur);
      if (i >= steps) {
        clearInterval(tick);
        setTimeout(() => setDelta(null), 800);
      }
    }, 45);
    return () => clearInterval(tick);
  }, [value]);
  const deltaCol = delta === null ? null : delta > 0 ? "#4ade80" : "#f87171";
  return (
    <div style={{ position:"relative", display:"inline-block" }}>
      <span style={style}>{$(display)}</span>
      {delta !== null && (
        <span style={{
          position:"absolute", top:-14, left:"50%", transform:"translateX(-50%)",
          fontSize:10, fontWeight:800, color:deltaCol, whiteSpace:"nowrap",
          animation:"toastIn .2s ease, toastOut .5s ease 0.7s forwards",
          pointerEvents:"none", letterSpacing:.5,
        }}>
          {delta > 0 ? "+" : ""}{$(delta)}
        </span>
      )}
    </div>
  );
}

/* - PlayerMat - */
function PlayerMat({ player, isActive, isViewing, onClick, compact, onPressureClick }) {
  const av = player.assets.reduce((s,a) => s+a.origVal, 0);
  const borderCol = isActive ? player.color : isViewing ? C.muted : C.border;
  return (
    <div onClick={onClick}
      style={{ border:`2px solid ${borderCol}`,
               borderRadius:10, overflow:"hidden", background:isActive?"#172033":C.panel,
               cursor:onClick?"pointer":"default", minWidth:compact?120:155, flexShrink:0,
               boxShadow:isActive?`0 0 16px ${player.color}44`:"none", transition:"all .2s" }}>
      {/* Status stripe - sits flush inside the top border */}
      {(isActive || isViewing) && (
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",
                      background:isActive?player.color:C.border,
                      padding:"2px 8px" }}>
          {isActive && <span style={{ fontSize:8,fontWeight:800,letterSpacing:2,color:"#0f172a",fontFamily:"'Rubik',sans-serif" }}>● ACTIVE</span>}
          {isViewing && <span style={{ fontSize:8,letterSpacing:1,color:isActive?"#0f172a44":C.sub,marginLeft:"auto" }}>VIEWING</span>}
        </div>
      )}
      <div style={{ padding:compact?8:12 }}>
      <div style={{ display:"flex",alignItems:"center",gap:6,marginBottom:5 }}>
        <div style={{ width:8,height:8,borderRadius:"50%",background:player.color,boxShadow:`0 0 4px ${player.color}` }} />
        <span style={{ fontFamily:"'Rubik',sans-serif",fontSize:compact?11:13,color:"#f1f5f9",fontWeight:700 }}>{player.name}</span>
      </div>
      <AnimatedMoney value={player.money} style={{
        fontFamily:"'JetBrains Mono',monospace",
        fontSize:compact?20:26,
        color:player.money<0?C.red:C.green,
        fontWeight:700, lineHeight:1, display:"block", marginBottom:4
      }} />
      <MoneyBar money={player.money} />
      <div style={{ display:"flex",justifyContent:"space-between",marginTop:5,fontSize:10,color:C.sub }}>
        <span>Fee: <b style={{color:C.gold}}>{$(player.feeToken)}</b></span>
        <span>NW: <b style={{color:"#a3e635"}}>{$(player.money+av)}</b></span>
      </div>
      {!compact && (
        <div style={{ marginTop:5,display:"flex",flexWrap:"wrap",gap:2 }}>
          {player.assets.map(a => (
            <div key={a.id} style={{ fontSize:8,padding:"2px 5px",borderRadius:3,
                                     background:a.sub===AS.PASSIVE?"#052e16":"#0a1f0a",
                                     color:a.status===ST.USED?C.muted:a.sub===AS.PASSIVE?"#86efac":"#4ade80",
                                     border:`1px solid ${a.status===ST.USED?C.border:a.sub===AS.PASSIVE?"#166534":"#15803d"}`,
                                     maxWidth:66,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap" }}
              title={a.name}>{a.name}</div>
          ))}
        </div>
      )}
      <div style={{ fontSize:9,color:C.muted,marginTop:4 }}>Hand: {player.hand.length} . Assets: {player.assets.length}</div>
      {/* Pressure tokens badge */}
      {(function(){
        var totalPressure = Object.values(player.pressureTokens||{}).reduce(function(s,v){return s+v;},0);
        if (!totalPressure) return null;
        return (<div style={{ display:"flex",justifyContent:"flex-end",marginTop:4 }}>
          <div onClick={function(e){ e.stopPropagation(); if(onPressureClick) onPressureClick(player); }}
            style={{ display:"flex",alignItems:"center",gap:3,padding:"2px 6px",borderRadius:4,
                     background:"#1c0a0a",border:"1px solid #ef4444",cursor:"pointer",
                     title:"Pressure tokens" }}>
            <span style={{ width:6,height:6,borderRadius:"50%",background:"#ef4444",display:"inline-block",flexShrink:0 }}/>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#fca5a5",fontWeight:700 }}>{totalPressure}</span>
          </div>
        </div>);
      })()}
      {((player.turnsToSkip||0) > 0 || player.actionLocked) && (
        <div style={{ display:"flex",gap:4,flexWrap:"wrap",marginTop:5 }}>
          {(player.turnsToSkip||0) > 0 && (
            <div style={{ fontSize:8,fontWeight:700,letterSpacing:1,padding:"2px 6px",borderRadius:3,
                          background:"#1c0a00",border:"1px solid #f59e0b",color:"#fcd34d" }}>
              ⏭ SKIP x{player.turnsToSkip}
            </div>
          )}
          {player.actionLocked && (
            <div style={{ fontSize:8,fontWeight:700,letterSpacing:1,padding:"2px 6px",borderRadius:3,
                          background:"#120a1e",border:"1px solid #a78bfa",color:"#c4b5fd" }}>
              NO ACTION
            </div>
          )}
        </div>
      )}
      {/* View assets hint */}
      <div style={{ fontSize:8,color:"#334155",textAlign:"center",marginTop:4,
                    letterSpacing:.5,cursor:"pointer",padding:"2px 0" }}>
        tap to view assets
      </div>
      </div>{/* end inner padding */}
    </div>
  );
}

function TargetModal({ st, cardId, dispatch }) {
  const cp   = curPl(st);
  const card = cp.hand.find(c => c.id===cardId);
  if (!card) return null;
  const needsHand = ["client_theft","robbed"].includes(card.hook);
  const targets = st.players.filter(p => p.id!==cp.id && (!needsHand || p.hand.length>0));
  return (
    <Overlay onClose={() => dispatch({type:"SELECT_CARD",id:null})}>
      <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:15,color:"#f1f5f9",marginBottom:4 }}>{card.name}</div>
      <div style={{ fontSize:12,color:C.sub,marginBottom:16 }}>{getDesc(card)}</div>
      <div style={{ fontSize:11,color:C.muted,marginBottom:10,letterSpacing:1 }}>SELECT A TARGET:</div>
      {!targets.length && <div style={{ color:C.red,fontSize:12 }}>No valid targets.</div>}
      <div style={{ display:"flex",flexDirection:"column",gap:8 }}>
        {targets.map(p => (
          <button key={p.id}
            onClick={() => dispatch({type:"SET_TARGET", cardId, targetId:p.id})}
            style={{ ...btn("#172033",p.color,p.color), textAlign:"left", display:"flex", alignItems:"center", gap:8 }}>
            <div style={{ width:10,height:10,borderRadius:"50%",background:p.color,flexShrink:0 }} />
            <span>
              <b style={{color:"#f1f5f9"}}>{p.name}</b>
              <span style={{color:C.sub,marginLeft:8}}>Balance: {$(p.money)} . Hand: {p.hand.length} . Assets: {p.assets.length}</span>
            </span>
          </button>
        ))}
      </div>
    </Overlay>
  );
}

/* - Discard-to-Limit Modal (has its own timer hooks) - */
function DiscardLimitModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "DISCARD_TO_LIMIT") return null;
  const [sel, setSel] = useState([]);
  const [timeLeft, setTimeLeft] = useState(20);

  useEffect(function() { setSel([]); }, [pc && pc.timerEnd]);

  useEffect(function() {
    if (!pc || pc.type !== "DISCARD_TO_LIMIT") return;
    function tick() {
      var left = Math.max(0, Math.ceil((pc.timerEnd - Date.now()) / 1000));
      setTimeLeft(left);
      if (left <= 0) {
        dispatch({ type:"RESOLVE_CHOICE", id:"auto" });
      }
    }
    tick();
    var iv = setInterval(tick, 250);
    return function() { clearInterval(iv); };
  }, [pc && pc.timerEnd]);


  var pl = st.players.find(function(p) { return p.id === pc.srcPlayer; });
  if (!pl) return null;

  var needed = pc.needed;
  var timerPct = Math.max(0, (pc.timerEnd - Date.now()) / 20000 * 100);
  var timerColor = timerPct > 50 ? "#22c55e" : timerPct > 25 ? "#f59e0b" : "#ef4444";
  var canConfirm = sel.length === needed;

  var toggleCard = function(id) {
    setSel(function(prev) {
      if (prev.includes(id)) return prev.filter(function(x) { return x !== id; });
      if (prev.length >= needed) return prev;
      return [...prev, id];
    });
  }

  var confirmDiscard = function() {
    dispatch({ type:"RESOLVE_CHOICE", id: sel });
  }

  var randomDiscard = function() {
    dispatch({ type:"RESOLVE_CHOICE", id:"auto" });
  }

  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.75)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:300 }}>
      <div style={{ background:"#0f172a",border:"2px solid #ef4444",borderRadius:14,padding:20,width:540,maxWidth:"95vw",boxShadow:"0 0 40px #ef444433" }}>
        {/* Header */}
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:10 }}>
          <div>
            <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:16,color:"#f87171",fontWeight:700,letterSpacing:1 }}>Discard to Hand Limit</div>
            <div style={{ fontSize:12,color:"#94a3b8",marginTop:3 }}>{pc.prompt}</div>
          </div>
          <div style={{ textAlign:"right" }}>
            <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:28,fontWeight:700,
                          color: timeLeft <= 5 ? "#ef4444" : timeLeft <= 10 ? "#f59e0b" : "#f1f5f9",
                          animation: timeLeft <= 5 ? "pulse 0.6s infinite" : "none" }}>
              {timeLeft}s
            </div>
            <div style={{ fontSize:9,color:"#64748b",letterSpacing:1 }}>SECONDS LEFT</div>
          </div>
        </div>
        {/* Timer bar */}
        <div style={{ height:5,background:"#1e293b",borderRadius:3,marginBottom:14,overflow:"hidden" }}>
          <div style={{ height:"100%",width:timerPct+"%",background:timerColor,borderRadius:3,transition:"width 0.25s linear,background 0.5s" }} />
        </div>
        {/* Selection status */}
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:10 }}>
          <div style={{ fontSize:11,color:"#94a3b8" }}>
            Selected: <span style={{ color: canConfirm ? "#4ade80" : "#f1f5f9", fontWeight:700 }}>{sel.length}</span> / {needed}
          </div>
          <button onClick={randomDiscard}
            style={{ background:"#7c3aed",border:"1px solid #a855f7",color:"#e9d5ff",borderRadius:6,padding:"4px 12px",cursor:"pointer",fontSize:11,fontWeight:600 }}>
            ↻ Random
          </button>
        </div>
        {/* Hand cards — click for info, separate discard button per row */}
        <div style={{ display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto",marginBottom:14 }}>
          {pl.hand.map(function(card) {
            var isSelected = sel.includes(card.id);
            var canSelect  = isSelected || sel.length < needed;
            var typeUI = { ACTION:{col:"#93c5fd",bg:"#0a1220"}, REACTION:{col:"#f87171",bg:"#1f0a0a"}, ASSET:{col:"#4ade80",bg:"#0a1f0a"} };
            var ui = typeUI[card.type] || {col:"#94a3b8",bg:"#1e293b"};
            return (
              <div key={card.id} style={{ display:"flex",alignItems:"center",gap:8,
                background: isSelected ? "#3b0000" : ui.bg,
                border: isSelected ? "2px solid #ef4444" : "1px solid "+(canSelect?ui.col+"66":"#334155"),
                borderRadius:8, padding:"8px 10px", opacity:!canSelect?0.4:1, transition:"all .12s" }}>
                <div style={{ flex:1, cursor:"pointer", minWidth:0 }}
                  onClick={function(){ dispatch({type:"OPEN_DETAIL",card:card}); }}>
                  <div style={{ display:"flex",alignItems:"center",gap:8 }}>
                    <div style={{ minWidth:0 }}>
                      <div style={{ fontSize:9,color:ui.col,fontWeight:700,letterSpacing:0.5 }}>{card.type}</div>
                      <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:13,color:"#f1f5f9",fontWeight:600,lineHeight:1.2 }}>{card.name}</div>
                      <div style={{ fontSize:9,color:"#64748b",lineHeight:1.3,marginTop:2,
                                    whiteSpace:"nowrap",overflow:"hidden",textOverflow:"ellipsis",maxWidth:"100%" }}>{getDesc(card)}</div>
                    </div>
                    <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:15,color:"#f59e0b",fontWeight:700,flexShrink:0,marginLeft:"auto" }}>${card.value}k</div>
                  </div>
                </div>
                <button onClick={function(){ if(canSelect) toggleCard(card.id); }}
                  disabled={!canSelect}
                  style={{ padding:"5px 10px",fontSize:11,fontWeight:700,borderRadius:6,cursor:canSelect?"pointer":"not-allowed",flexShrink:0,
                    background: isSelected ? "#7f1d1d" : "#1e293b",
                    border: "1px solid " + (isSelected ? "#ef4444" : "#475569"),
                    color: isSelected ? "#fca5a5" : "#94a3b8" }}>
                  {isSelected ? "✕ Remove" : "Discard"}
                </button>
              </div>
            );
          })}
        </div>
        {/* Confirm button */}
        <button onClick={confirmDiscard} disabled={!canConfirm}
          style={{ width:"100%",padding:"11px 0",fontSize:14,fontWeight:700,letterSpacing:1,cursor:canConfirm?"pointer":"not-allowed",
                   background: canConfirm ? "#7f1d1d" : "#1e293b",
                   border: "2px solid " + (canConfirm ? "#ef4444" : "#334155"),
                   color: canConfirm ? "#fca5a5" : "#475569",
                   borderRadius:8, transition:"all .2s" }}>
          {canConfirm ? "Confirm Discard (" + sel.length + "/" + needed + ")" : "Select " + needed + " card" + (needed !== 1 ? "s" : "") + " to discard"}
        </button>
      </div>
    </div>
  );
}

function ChoiceModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc) return null;
  const LABELS = {
    DISCARD_SELF:"Choose a Card to Discard", OPPONENT_DISCARD:"Choose a Card to Discard",
    CLIENT_THEFT:"Client Theft - Choose Card", STEAL_CARD:"Choose a Card to Steal",
    INSPECT:"Viewing Hand", PEEK:"Top 3 Cards",
    HALF_PRICE_BUY:"Half-Price Buy", DISCOUNT_BUY:"Discounted Buy",
    DOUBLE_SELL:"Double Sell",
    LETS_DEAL_SET_TARGET:"Let's Make a Deal — Choose Target", EXTRA_INCOME_SET_TARGET:"Extra Income: Choose Target", RISK_REWARD_SET_TARGET:"Risk vs Reward: Choose Target",
    INTERVIEWS_PICK:"Interviews - Pick a Card",
    HELP_WANTED_GIVE:"Help Wanted - Choose a Card to Hand Over",
  };
  const readOnly = ["INSPECT","PEEK"].includes(pc.type);
  const isPlayerPick = ["EXTRA_INCOME_SET_TARGET","LETS_DEAL_SET_TARGET","RISK_REWARD_SET_TARGET"].includes(pc.type);
  return (
    <Overlay onClose={readOnly ? ()=>dispatch({type:"RESOLVE_CHOICE",id:null}) : undefined}>
      <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:14,color:C.gold,marginBottom:4 }}>{LABELS[pc.type]||pc.type}</div>
      <div style={{ fontSize:12,color:C.sub,marginBottom:14 }}>{pc.prompt}</div>
      {!pc.options?.length && <div style={{ color:C.muted,fontSize:11 }}>No options available.</div>}
      {isPlayerPick ? (
        <div style={{ display:"flex",flexDirection:"column",gap:8 }}>
          {pc.options.map(player => (
            <button key={player.id} onClick={()=>dispatch({type:"RESOLVE_CHOICE",id:player.id})}
              style={{ ...btn("#172033",player.color,player.color),textAlign:"left",display:"flex",alignItems:"center",gap:10 }}>
              <div style={{ width:10,height:10,borderRadius:"50%",background:player.color,flexShrink:0 }} />
              <span><b style={{color:"#f1f5f9"}}>{player.name}</b>
                <span style={{color:C.sub,marginLeft:8}}>{player.money >= 0 ? "$"+player.money+"k" : "-$"+Math.abs(player.money)+"k"} . {player.hand.length} cards</span>
              </span>
            </button>
          ))}
        </div>
      ) : (
        <div style={{ display:"flex",gap:8,flexWrap:"wrap",maxHeight:320,overflowY:"auto" }}>
          {pc.options?.map(card => (
            <div key={card.id} style={{ display:"flex",flexDirection:"column",gap:4,alignItems:"center" }}>
              <CardComp card={card} onClick={()=>dispatch({type:"OPEN_DETAIL",card:card})} />
              {!readOnly && (
                <button onClick={()=>dispatch({type:"RESOLVE_CHOICE",id:card.id})}
                  style={{ ...btn(C.hi,C.blue,"#93c5fd"),fontSize:10,padding:"3px 10px" }}>Select</button>
              )}
            </div>
          ))}
        </div>
      )}
      {readOnly && (
        <button onClick={()=>dispatch({type:"RESOLVE_CHOICE",id:null})}
          style={{ ...btn(C.panel,C.border,C.sub),marginTop:14 }}>Close</button>
      )}
    </Overlay>
  );
}

function DetailModal({ card, st, dispatch, onClose }) {
  if (!card) return null;
  const isP = card.sub===AS.PASSIVE;
  const c   = isP ? {...CUI.PASSIVE_ASSET,lbl:"ASSET"} : CUI[card.type] || CUI[CT.ACTION];
  const sub = card.sub ? SUB_UI[card.sub] : null;
  const vp  = st.players[st.viewIdx];
  const cp  = curPl(st);
  const inShop  = st.shop.some(a => a.id===card.id);
  const owned   = vp.assets.some(a => a.id===card.id);
  const canBuy  = inShop && vp.id===cp.id && !st.hasBoughtAsset && cp.money>=MIN_BUY;
  const canAct  = owned && card.sub!==AS.PASSIVE && !card.disabled && card.status!==ST.USED && (vp.id===cp.id||card.sub===AS.ANYTIME);
  return (
    <Overlay onClose={onClose}>
      <div style={{ display:"flex",justifyContent:"space-between",marginBottom:10 }}>
        <div>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:18,color:"#f1f5f9",fontWeight:700 }}>{card.name}</div>
          {sub && <div style={{ fontSize:10,color:sub.col,fontWeight:700,letterSpacing:1,marginTop:2 }}>{sub.lbl}</div>}
        </div>
        <div style={{ textAlign:"right" }}>
          <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:22,
                        color:(st.detailShopInfo&&st.detailShopInfo.surcharge>0)||card.value!==card.origVal?"#f97316":C.gold,
                        fontWeight:700 }}>${card.value}k</div>
          {card.origVal!==undefined && card.origVal!==card.value && <div style={{ fontSize:10,color:C.muted,textDecoration:"line-through" }}>${card.origVal}k base</div>}
        </div>
      </div>
      <CardSvg card={card} w={300} h={160} />
      <div style={{ marginTop:12,padding:12,background:C.bg,borderRadius:6,border:`1px solid ${c.bd}44` }}>
        <div style={{ fontSize:9,color:c.ac,letterSpacing:1,marginBottom:5,fontWeight:600 }}>EFFECT</div>
        <div style={{ fontSize:13,color:"#cbd5e1",lineHeight:1.6 }}>{getDesc(card)}</div>
      </div>
      {st.detailShopInfo && st.detailShopInfo.surcharge > 0 && (
        <div style={{ marginTop:8,padding:"8px 12px",background:"#1c0a00",borderRadius:6,border:"1px solid #f9731644" }}>
          <div style={{ fontSize:9,color:"#f97316",letterSpacing:1,marginBottom:3,fontWeight:700 }}>PRICE MODIFIED</div>
          <div style={{ fontSize:11,color:"#fed7aa" }}>Cost increased by {$(st.detailShopInfo.surcharge)} by: {st.detailShopInfo.sources.join(", ")}</div>
        </div>
      )}
      {card.modifiedBy && card.modifiedBy.length > 0 && (
        <div style={{ marginTop:8,padding:"8px 12px",background:"#0a0a1c",borderRadius:6,border:"1px solid #f9731644" }}>
          <div style={{ fontSize:9,color:"#f97316",letterSpacing:1,marginBottom:3,fontWeight:700 }}>VALUE MODIFIED</div>
          {card.modifiedBy.map(function(m,i) { return (
            <div key={i} style={{ fontSize:11,color:"#fed7aa" }}>{m}</div>
          ); })}
        </div>
      )}
      {/* - Out of Order info + pay button - */}
      {card.outOfOrderBy && (()=>{
        const oooEffect = (st.outOfOrderEffects||[]).find(function(e){ return e.assetId===card.id; });
        const oooPayee  = oooEffect ? oooEffect.payeeName : "?";
        const oooAmt    = oooEffect ? oooEffect.payAmt : 5;
        const oooEffId  = oooEffect ? oooEffect.id : null;
        const isMyAsset = vp.assets.some(function(a){ return a.id===card.id; });
        const isMyTurn  = vp.id === cp.id;
        const myMoney   = vp.money;
        const canPay    = isMyAsset && isMyTurn && oooEffId && myMoney >= 1;
        return (
          <div style={{ marginTop:10,padding:"10px 12px",background:"#1a0e00",borderRadius:8,
                        border:"2px solid #fbbf2466" }}>
            <div style={{ fontSize:9,color:"#fbbf24",letterSpacing:1.5,fontWeight:700,marginBottom:6 }}>
              🚧 OUT OF ORDER
            </div>
            <div style={{ fontSize:11,color:"#fde68a",lineHeight:1.6,marginBottom:8 }}>
              This asset was disabled by <strong style={{color:"#fbbf24"}}>{oooPayee}</strong>.
              Pay <strong style={{color:"#fbbf24"}}>${oooAmt}k</strong> to re-enable it.
              {!isMyTurn && <span style={{color:"#94a3b8"}}> (Can only pay on your turn.)</span>}
              {isMyTurn && myMoney < 1 && <span style={{color:"#f87171"}}> (Not enough money.)</span>}
            </div>
            {isMyAsset && (
              <button
                disabled={!canPay}
                onClick={function(){
                  if (canPay) { dispatch({type:"PAY_OUT_OF_ORDER",effectId:oooEffId}); onClose(); }
                }}
                style={{ width:"100%",padding:"9px 0",fontSize:12,fontWeight:700,letterSpacing:1,
                         cursor:canPay?"pointer":"not-allowed",borderRadius:7,
                         background:canPay?"#451a03":"#1e293b",
                         border:"2px solid "+(canPay?"#fbbf24":"#334155"),
                         color:canPay?"#fde68a":"#475569",transition:"all .15s" }}>
                {canPay ? "Pay $"+oooAmt+"k to Re-enable" : "Cannot Pay Right Now"}
              </button>
            )}
          </div>
        );
      })()}
      {(card.tmax>0) && (
        <div style={{ marginTop:10 }}>
          <div style={{ fontSize:10,color:C.sub,marginBottom:5 }}>Tokens: {card.tokens?.length||0} / {card.tmax}</div>
          <div style={{ display:"flex",gap:4 }}>
            {Array.from({length:card.tmax},(_,i) => (
              <div key={i} style={{ width:12,height:12,borderRadius:"50%",
                                    background:i<(card.tokens?.length||0)?C.gold:"#334155",
                                    border:`1px solid ${i<(card.tokens?.length||0)?"#d97706":"#475569"}` }} />
            ))}
          </div>
        </div>
      )}
      {card.status && <div style={{ marginTop:6,fontSize:10,color:card.status===ST.READY?C.green:C.red }}>Status: {card.status}</div>}
      <div style={{ display:"flex",gap:8,marginTop:14 }}>
        {canBuy && <button onClick={()=>{dispatch({type:"BUY_SHOP",p:{assetId:card.id}});onClose();}} style={{ ...btn("#052e16","#16a34a","#4ade80"),flex:1 }}>Buy {$(calcShopCost(st,card,cp.id).cost)}</button>}
        {canAct && <button onClick={()=>{dispatch({type:"ACTIVATE",p:{assetId:card.id,ownerId:vp.id}});onClose();}} style={{ ...btn("#052e16","#16a34a","#4ade80"),flex:1 }}>Activate</button>}
        <button onClick={onClose} style={{ ...btn(C.panel,C.border,C.sub) }}>Close</button>
      </div>
      {/* Product Update info */}
      {card._puSourceId && (() => {
        var pu_info = null;
        st.players.forEach(function(p){ p.assets.forEach(function(a){ if(a.id===card._puSourceId) pu_info=a; }); });
        return pu_info ? (
          <div style={{ marginTop:8,background:"#1a0d3a",border:"1px solid #7c3aed",borderRadius:6,padding:"6px 10px" }}>
            <div style={{ fontSize:8,color:"#a78bfa",fontWeight:700,marginBottom:2 }}>🔧 PRODUCT UPDATE ACTIVE</div>
            <div style={{ fontSize:9,color:"#c4b5fd" }}>Modified by "{pu_info.name}". Values on this card have been adjusted.</div>
          </div>
        ) : (
          <div style={{ marginTop:8,background:"#1a0d3a",border:"1px solid #7c3aed",borderRadius:6,padding:"6px 10px" }}>
            <div style={{ fontSize:8,color:"#a78bfa",fontWeight:700 }}>🔧 PRODUCT UPDATE APPLIED</div>
          </div>
        );
      })()}
      {card.hook === "a_product_update" && card._appliedTo && (() => {
        var tgt_info = null;
        st.players.forEach(function(p){ p.assets.forEach(function(a){ if(a.id===card._appliedTo.assetId) tgt_info=a; }); });
        return (
          <div style={{ marginTop:8,background:"#0a2010",border:"1px solid #16a34a",borderRadius:6,padding:"6px 10px" }}>
            <div style={{ fontSize:8,color:"#4ade80",fontWeight:700,marginBottom:2 }}>🔧 APPLIED TO: {tgt_info?tgt_info.name:card._appliedTo.assetId}</div>
            <div style={{ fontSize:9,color:"#86efac" }}>Field: {card._appliedTo.field} · Delta: {card._appliedTo.delta>0?"+":""}{card._appliedTo.delta}</div>
          </div>
        );
      })()}
    </Overlay>
  );
}

/* - Trigger Stack display - */
/* - MultiActivity Feed (replaces TriggerStack in multiplayer desktop) - */
function MultiActivityFeed({ st, myPlayerId }) {
  var log = st.log || [];
  var relevant = log.slice().reverse().filter(function(e) {
    return ['money','loss','asset','reaction','draw','discard','turn'].indexOf(e.type) >= 0;
  }).slice(0, 8);
  var typeColors = { money:"#4ade80", loss:"#f87171", asset:"#f59e0b",
    reaction:"#a78bfa", draw:"#60a5fa", discard:"#94a3b8", turn:"#334155" };
  var typeIcons  = { money:"+", loss:"-", asset:"◆", reaction:"↩",
    draw:"↓", discard:"×", turn:"▷" };
  return (
    <div style={{ background:C.panel, border:"1px solid "+C.border, borderRadius:10, padding:12,
                  display:"flex", flexDirection:"column", gap:3 }}>
      <div style={{ fontSize:9,color:C.muted,letterSpacing:1,fontWeight:600,marginBottom:4 }}>ACTIVITY</div>
      {relevant.length === 0 && <div style={{ fontSize:10,color:C.muted,fontStyle:"italic" }}>No activity yet.</div>}
      {relevant.map(function(e) {
        var col = typeColors[e.type] || C.muted;
        var icon = typeIcons[e.type] || "·";
        return (
          <div key={e.id} style={{ display:"flex",gap:6,alignItems:"flex-start",
                                    padding:"3px 0",borderBottom:"1px solid #1e293b" }}>
            <span style={{ fontSize:10,flexShrink:0,color:col,fontWeight:700,lineHeight:1 }}>{icon}</span>
            <span style={{ fontSize:9,color:col,lineHeight:1.4 }}>{(e.msg||'').replace(/^[^a-zA-Z$"(\[]+/,'')}</span>
          </div>
        );
      })}
    </div>
  );
}

function TriggerStack({ st }) {
  const active   = st.triggerStack.filter(t => t.status==="ACTIVE");
  const recent   = st.triggerStack.filter(t => t.status!=="ACTIVE").slice(-5).reverse();
  const top      = active.length ? active[active.length-1] : null;
  return (
    <div style={{ background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:12,maxHeight:320,overflowY:"auto" }}>
      <div style={{ fontSize:10,color:C.muted,letterSpacing:1,fontWeight:600,marginBottom:6 }}>TRIGGER STACK</div>
      {!top && !active.length && <div style={{ fontSize:10,color:C.muted,fontStyle:"italic" }}>Stack idle.</div>}
      {top && (
        <div style={{ border:`1px solid ${tc(top.type)}`,borderRadius:6,padding:"6px 10px",marginBottom:4,
                      background:C.bg,boxShadow:`0 0 8px ${tc(top.type)}33` }}>
          <div style={{ display:"flex",alignItems:"center",gap:6 }}>
            <span style={{ fontSize:10,color:C.gold,fontWeight:700,letterSpacing:1 }}>TOP ↑</span>
            <span style={{ fontSize:10,color:tc(top.type),fontFamily:"'JetBrains Mono',monospace",fontWeight:600 }}>{top.type}</span>
            {top.label && <span style={{ fontSize:9,color:C.sub }}> - "{top.label}"</span>}
            {top.value != null && <span style={{ fontSize:10,color:C.gold }}>{$(top.value)}</span>}
          </div>
        </div>
      )}
      {active.slice(0,-1).reverse().map(t => (
        <div key={t.tid} style={{ marginBottom:3,padding:"4px 8px",borderRadius:4,background:C.bg,
                                   border:`1px solid ${tc(t.type)}44` }}>
          <span style={{ fontSize:9,color:`${tc(t.type)}99`,fontFamily:"'JetBrains Mono',monospace" }}>{t.type}</span>
        </div>
      ))}
      {recent.length > 0 && (
        <div style={{ marginTop:8,paddingTop:8,borderTop:`1px solid ${C.border}` }}>
          <div style={{ fontSize:8,color:C.muted,marginBottom:3 }}>RECENT</div>
          {recent.map(t => (
            <div key={t.tid} style={{ fontSize:9,color:t.status==="CANCELLED"?"#7f1d1d":C.border,
                                       fontFamily:"'JetBrains Mono',monospace",lineHeight:1.6,
                                       textDecoration:t.status==="CANCELLED"?"line-through":"none" }}>
              {t.status==="CANCELLED"?"✕":"✓"} {t.type}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

/* - Reaction Window - */
function ReactionWindow({ st, dispatch, isMultiplayer, mobile }) {
  const w = st.reactionWindow;
  const [timeLeft, setTimeLeft] = useState(20);
  const [selectedItem, setSelectedItem]     = useState(null);   // card/asset + _isAsset flag
  const [myLastDecision, setMyLastDecision] = useState(null);   // {type:'react'|'pass', card?}
  const [showInfo, setShowInfo]             = useState(false);

  useEffect(function() {
    setTimeLeft(20);
    setSelectedItem(null);
    setMyLastDecision(null);
    setShowInfo(false);
  }, [w && w.wid]);

  useEffect(function() {
    if (!w) return;
    if (timeLeft <= 0) { dispatch({ type:"ENG_PASS_ALL" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0,n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft, w && w.wid]);

  if (!w || w.status !== "OPEN") return null;

  const trig      = st.triggerStack.find(function(t){ return t.tid===w.triggerTid; });
  const vp        = st.players[st.viewIdx];
  const vpReactor = vp ? w.reactors.find(function(r){ return r.pid===vp.id && r.decision==="PENDING" && r.canReact; }) : null;
  const vpEntry   = vp ? w.reactors.find(function(r){ return r.pid===vp.id; }) : null;

  // Source player + verb
  const srcPlayer = st.players.find(function(p){ return p.id===w.srcPlayer; });
  const srcName   = srcPlayer ? srcPlayer.name : "A player";
  const verb      = w.ttype===T.ACTIVATE_ASSET  ? "is activating"
                  : w.ttype===T.BUY_ASSET        ? "is buying"
                  : w.ttype===T.REACTION_PLAYED  ? "is reacting with"
                  : w.ttype===T.START_TURN        ? "is starting their turn"
                  : "is playing";

  // Triggered card name + colour (use trig.srcCard now that pushTrigger preserves extra fields)
  const trigSrcCard   = trig && trig.srcCard ? trig.srcCard : null;
  const trigCardName  = (trig && trig.label) ? trig.label : "";
  const trigCardColor = trigSrcCard && trigSrcCard.type===CT.ASSET    ? "#22c55e"
                      : trigSrcCard && trigSrcCard.type===CT.REACTION ? "#fca5a5"
                      : "#60a5fa";

  // Context sentence
  var contextSentence = null;
  if (w.ttype===T.LOSE_MONEY && w.value) {
    var lossTarget = st.players.find(function(p){ return p.id===(w.tgtPlayer||w.srcPlayer); });
    if (lossTarget) contextSentence = (vp && lossTarget.id===vp.id)
      ? "You will lose $"+w.value+"k"
      : lossTarget.name+" will lose $"+w.value+"k";
  } else if (w.tgtPlayer && w.tgtPlayer!==w.srcPlayer) {
    var tgtP = st.players.find(function(p){ return p.id===w.tgtPlayer; });
    if (tgtP) contextSentence = (vp && tgtP.id===vp.id)
      ? "You have been targeted"
      : tgtP.name+" has been targeted";
  } else if (trigSrcCard) {
    contextSentence = typeof trigSrcCard.descFn==="function" ? trigSrcCard.descFn(trigSrcCard) : (trigSrcCard.desc||null);
  } else if (w.ttype===T.START_TURN) {
    contextSentence = "Their turn is beginning. You may react before they take any actions.";
  }

  // Reaction items (cards + assets combined)
  const reactionItems = vpReactor ? [
    ...(vpReactor.cards  ||[]).map(function(c){ return {...c,_isAsset:false}; }),
    ...(vpReactor.assets ||[]).map(function(a){ return {...a,_isAsset:true}; })
  ] : [];

  function handleReact() {
    if (!selectedItem || !vpReactor) return;
    var item = selectedItem;
    setMyLastDecision({ type:"react", card:item });
    setSelectedItem(null);
    dispatch(item._isAsset
      ? { type:"ENG_REACT", pid:vpReactor.pid, assetId:item.id }
      : { type:"ENG_REACT", pid:vpReactor.pid, cardId:item.id });
  }

  function handlePass() {
    if (!vpReactor) return;
    setMyLastDecision({ type:"pass" });
    dispatch({ type:"ENG_PASS", pid:vpReactor.pid });
  }

  // State 2 message
  var state2Msg = null;
  if (myLastDecision && myLastDecision.type==="react") {
    state2Msg = { label:"You are reacting with", card:myLastDecision.card };
  } else if (myLastDecision && myLastDecision.type==="pass") {
    state2Msg = { label:"You chose not to react." };
  } else if (!vpReactor) {
    if (!vpEntry || !vpEntry.canReact)       state2Msg = { label:"You have no eligible reactions." };
    else if (vpEntry.decision==="PASSED")    state2Msg = { label:"You chose not to react." };
    else if (vpEntry.decision==="REACTED")   state2Msg = { label:"You reacted." };
  }

  const isState1 = !!vpReactor;

  return (
    <div style={{
      position:"fixed", inset:0, zIndex:900,
      display:"flex", alignItems:"center", justifyContent:"center",
      background:"rgba(0,0,0,0.72)", backdropFilter:"blur(3px)",
      animation:"fadeBackdrop .18s ease"
    }}>
      <div style={{
        background:"#130806", border:"1px solid #8b3d18", borderRadius:12,
        width:mobile?"92%":"90%", maxWidth:mobile?460:480,
        overflow:"hidden", position:"relative",
        fontFamily:"'Rubik',sans-serif",
        animation: isState1
          ? "reactionWindowEnter 0.42s cubic-bezier(0.22, 0.8, 0.36, 1) forwards, reactionBorderFlash 0.7s ease 0.35s forwards, reactionActiveGlow 2.2s ease-in-out 1s infinite"
          : "reactionWindowEnter 0.42s cubic-bezier(0.22, 0.8, 0.36, 1) forwards"
      }}>

        {/* Info overlay */}
        {showInfo && (
          <div onClick={function(){setShowInfo(false);}}
            style={{position:"absolute",inset:0,background:"rgba(6,2,1,0.93)",zIndex:10,padding:14,
                    display:"flex",alignItems:"flex-start",justifyContent:"center",cursor:"default"}}>
            <div onClick={function(e){e.stopPropagation();}}
              style={{background:"#1c0e08",border:"1px solid #8b3d18",borderRadius:10,padding:"16px 18px",width:"100%",cursor:"auto"}}>
              <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:11}}>
                <div style={{fontSize:12,fontWeight:900,color:"#f1f5f9",letterSpacing:.3}}>What is a Reaction Window?</div>
                <button onClick={function(){setShowInfo(false);}}
                  style={{background:"transparent",border:"none",color:"#64748b",fontSize:14,fontWeight:700,
                          cursor:"pointer",padding:"0 2px",lineHeight:1,fontFamily:"'Rubik',sans-serif"}}>X</button>
              </div>
              <p style={{fontSize:11,color:"#94a3b8",lineHeight:1.65,margin:"0 0 9px"}}>A reaction window opens whenever a player plays an action card, activates an asset, or makes a significant game move. It gives all other players a brief period of time to respond before the effect takes place.</p>
              <p style={{fontSize:11,color:"#94a3b8",lineHeight:1.65,margin:"0 0 9px"}}>During this window you can play a reaction card from your hand to cancel the effect, reduce its impact, or trigger a counter-effect of your own. If you have nothing to play, or choose not to react, select Pass.</p>
              <p style={{fontSize:11,color:"#94a3b8",lineHeight:1.65,margin:0}}>Once all players have made their choice (or the timer runs out), the original effect resolves and the game continues.</p>
            </div>
          </div>
        )}

        {/* Header */}
        <div style={{padding:"14px 16px 10px",borderBottom:"1px solid #4d2010"}}>
          <div style={{display:"flex",alignItems:"flex-start",marginBottom:contextSentence?9:0}}>
            <div style={{width:52,flexShrink:0}}></div>
            <div style={{flex:1,textAlign:"center"}}>
              <div style={{display:"inline-flex",alignItems:"center",gap:5,marginBottom:5}}>
                <div style={{fontSize:8,color:"#c2610f",fontWeight:700,letterSpacing:2}}>REACTION WINDOW</div>
                <button onClick={function(){setShowInfo(true);}}
                  style={{width:14,height:14,minWidth:14,minHeight:14,borderRadius:"50%",
                          border:"1px solid #7a3515",color:"#94a3b8",fontSize:8,fontWeight:700,
                          background:"transparent",cursor:"pointer",padding:0,
                          display:"flex",alignItems:"center",justifyContent:"center",
                          fontFamily:"'Rubik',sans-serif",lineHeight:1,boxSizing:"border-box"}}>?</button>
              </div>
              <div style={{fontSize:12,color:"#94a3b8",marginBottom:3}}>{srcName} {verb}</div>
              {trigCardName && (
                <div onClick={trigSrcCard?function(){dispatch({type:"OPEN_DETAIL",card:trigSrcCard});}:undefined}
                  style={{fontSize:18,fontWeight:900,color:trigCardColor,lineHeight:1.1,
                          textDecoration:trigSrcCard?"underline":"none",textUnderlineOffset:3,
                          textDecorationColor:trigCardColor+"44",cursor:trigSrcCard?"pointer":"default"}}>
                  {trigCardName}
                </div>
              )}
            </div>
            <div style={{width:52,flexShrink:0,background:"#0d0605",border:"1px solid #4d2010",
                         borderRadius:8,padding:"5px 8px",textAlign:"center"}}>
              <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:20,fontWeight:700,lineHeight:1.1,
                           color:timeLeft<=5?"#f87171":timeLeft<=10?"#f59e0b":"#f1f5f9"}}>{timeLeft}</div>
              <div style={{fontSize:8,color:"#475569",letterSpacing:1,marginTop:1}}>SEC</div>
            </div>
          </div>
          {contextSentence && (
            <div style={{fontSize:11,color:"#94a3b8",lineHeight:1.5,background:"#0d0605",
                         borderLeft:"2px solid #8b3d18",borderRadius:6,padding:"7px 10px"}}>
              {contextSentence}
            </div>
          )}
        </div>

        {/* State 1: selecting a reaction */}
        {isState1 && (
          <div>
            <div style={{padding:"12px 16px 0"}}>
              <div style={{fontSize:8,color:"#a05a30",fontWeight:700,letterSpacing:2,marginBottom:8}}>YOUR REACTIONS</div>
              <div style={{display:"flex",gap:8,overflowX:"auto",paddingBottom:4,alignItems:"stretch"}}>
                {reactionItems.map(function(item) {
                  var isSel    = selectedItem && selectedItem.id===item.id;
                  var itemDesc = typeof item.descFn==="function" ? item.descFn(item) : (item.desc||"");
                  var itemCost = item.value||item.origVal||item.val;
                  return (
                    <div key={item.id} onClick={function(){setSelectedItem(isSel?null:item);}}
                      style={{flexShrink:0,width:130,display:"flex",flexDirection:"column",
                              background:isSel?"#1c0d04":"#0e0706",
                              border:"2px solid "+(isSel?"#f97316":"#5a2412"),
                              borderRadius:9,padding:9,cursor:"pointer",
                              transition:"background .15s,border-color .15s"}}>
                      <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:4}}>
                        <div style={{fontSize:8,fontWeight:700,color:"#c2610f",letterSpacing:1}}>{item.type||"REACTION"}</div>
                        {itemCost!=null && <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#f59e0b",fontWeight:700}}>${itemCost}k</div>}
                      </div>
                      <div onClick={function(e){e.stopPropagation();dispatch({type:"OPEN_DETAIL",card:item});}}
                        style={{fontSize:12,fontWeight:900,color:isSel?"#f97316":"#e2e8f0",lineHeight:1.2,marginBottom:5,
                                cursor:"pointer",textDecoration:"underline",textUnderlineOffset:2,textDecorationColor:"#ffffff22"}}>
                        {item.name}
                      </div>
                      <div style={{fontSize:9,color:"#64748b",lineHeight:1.4,flex:1}}>{itemDesc}</div>
                      <button onClick={function(e){e.stopPropagation();setSelectedItem(isSel?null:item);}}
                        style={{width:"100%",background:isSel?"#2a1200":"transparent",
                                border:"1px solid "+(isSel?"#f97316":"#7a3515"),
                                borderRadius:5,color:isSel?"#f97316":"#64748b",
                                fontSize:9,fontWeight:700,padding:"5px 0",cursor:"pointer",
                                letterSpacing:1,fontFamily:"'Rubik',sans-serif",marginTop:8,transition:"all .15s"}}>
                        {isSel?"SELECTED":"SELECT"}
                      </button>
                    </div>
                  );
                })}
                {reactionItems.length===0 && (
                  <div style={{fontSize:11,color:"#475569",padding:"8px 0",flex:1}}>No reaction cards in hand.</div>
                )}
              </div>
            </div>
            <div style={{padding:"10px 16px 12px",borderTop:"1px solid #4d2010",display:"flex",gap:8,marginTop:12}}>
              <button onClick={handleReact} disabled={!selectedItem}
                style={{flex:2,background:selectedItem?"#052e16":"#100806",
                        border:"1px solid "+(selectedItem?"#16a34a":"#4d2010"),
                        borderRadius:7,color:selectedItem?"#4ade80":"#3a1808",
                        fontSize:13,fontWeight:700,padding:"11px 0",
                        cursor:selectedItem?"pointer":"not-allowed",
                        fontFamily:"'Rubik',sans-serif",transition:"all .15s"}}>React</button>
              <button onClick={handlePass}
                style={{flex:1,background:"transparent",border:"1px solid #7a4025",borderRadius:7,
                        color:"#94a3b8",fontSize:13,fontWeight:700,padding:"11px 0",
                        cursor:"pointer",fontFamily:"'Rubik',sans-serif"}}>Pass</button>
            </div>
          </div>
        )}

        {/* State 2: waiting */}
        {!isState1 && (
          <div style={{padding:"22px 16px 14px",textAlign:"center"}}>
            {state2Msg ? (
              <React.Fragment>
                <div style={{fontSize:15,color:"#f1f5f9",fontWeight:700,lineHeight:1.3,
                             marginBottom:state2Msg.card?4:0}}>
                  {state2Msg.label}
                </div>
                {state2Msg.card && (
                  <div onClick={function(){dispatch({type:"OPEN_DETAIL",card:state2Msg.card});}}
                    style={{fontSize:17,fontWeight:900,cursor:"pointer",
                            textDecoration:"underline",textUnderlineOffset:3,textDecorationStyle:"dotted",
                            color:"#f97316",textDecorationColor:"#f9731655",marginTop:4}}>
                    {state2Msg.card.name}
                  </div>
                )}
              </React.Fragment>
            ) : (
              <div style={{fontSize:15,color:"#94a3b8",fontWeight:700}}>Waiting...</div>
            )}
          </div>
        )}

        {/* Other pending reactors — lets you switch view to react for them in single-player */}
        {(function(){
          var otherPending = w.reactors.filter(function(r){
            return r.decision==="PENDING" && r.canReact && r.pid!==(vp&&vp.id);
          });
          if (!otherPending.length) return null;
          return (
            <div style={{padding:"0 16px 10px",borderTop:"1px solid #4d2010"}}>
              <div style={{fontSize:8,color:"#a05a30",fontWeight:700,letterSpacing:2,marginBottom:7,marginTop:10}}>
                STILL DECIDING
              </div>
              {otherPending.map(function(r){
                var pl = st.players.find(function(p){ return p.id===r.pid; });
                if (!pl) return null;
                var plIdx = st.players.findIndex(function(p){ return p.id===r.pid; });
                return (
                  <div key={r.pid} style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:5}}>
                    <div style={{display:"flex",alignItems:"center",gap:7}}>
                      <div style={{width:7,height:7,borderRadius:"50%",background:pl.color,flexShrink:0}}></div>
                      <span style={{fontSize:11,color:pl.color,fontWeight:700,fontFamily:"'Rubik',sans-serif"}}>{pl.name}</span>
                    </div>
                    <button onClick={function(){dispatch({type:"SET_VIEW",i:plIdx});}}
                      style={{background:"transparent",border:"1px solid #5a2412",borderRadius:5,
                              color:"#94a3b8",fontSize:9,fontWeight:700,padding:"3px 10px",
                              cursor:"pointer",fontFamily:"'Rubik',sans-serif",letterSpacing:.5}}>
                      React for them
                    </button>
                  </div>
                );
              })}
            </div>
          );
        })()}

        {/* Skip All — single-player dev helper */}
        {!isMultiplayer && (
          <div style={{padding:"0 16px 8px"}}>
            <button onClick={function(){dispatch({type:"ENG_PASS_ALL"});}}
              style={{width:"100%",background:"transparent",border:"1px solid #4d2010",
                      borderRadius:6,color:"#475569",fontSize:9,fontWeight:700,
                      padding:"4px 0",cursor:"pointer",fontFamily:"'Rubik',sans-serif",letterSpacing:1}}>
              SKIP ALL
            </button>
          </div>
        )}

        {/* Footer */}
        <div style={{background:"#0d0605",borderTop:"1px solid #4d2010",padding:"8px 16px 12px"}}>
          <div style={{fontSize:9,color:"#64748b",fontWeight:700,letterSpacing:1.5,textAlign:"center",marginBottom:6}}>
            WAITING ON OTHER PLAYERS
          </div>
          <div style={{height:3,background:"#2a1008",borderRadius:2,overflow:"hidden"}}>
            <div style={{height:"100%",width:(timeLeft/20*100)+"%",background:"#f59e0b",borderRadius:2,transition:"width 1s linear"}} />
          </div>
        </div>

      </div>
    </div>
  );
}

/* - Discard pile - */
function DiscardPile({ mainDiscard, dispatch }) {
  const top = mainDiscard.length ? mainDiscard[mainDiscard.length-1] : null;
  return (
    <div style={{ display:"flex",flexDirection:"column",gap:4,alignItems:"center",flexShrink:0 }}>
      <div style={{ fontSize:9,color:C.muted,letterSpacing:1,fontWeight:600 }}>DISCARD</div>
      {top ? (
        <div style={{ cursor:"pointer" }} onClick={()=>dispatch({type:"OPEN_DETAIL",card:top})}>
          <CardComp card={top} noImg onClick={()=>dispatch({type:"OPEN_DETAIL",card:top})} onDetail={()=>dispatch({type:"OPEN_DETAIL",card:top})} />
          <div style={{ fontSize:9,color:C.muted,textAlign:"center",marginTop:2 }}>{mainDiscard.length} cards</div>
        </div>
      ) : (
        <div style={{ width:100,height:76,border:`2px dashed ${C.border}`,borderRadius:8,display:"flex",alignItems:"center",justifyContent:"center" }}>
          <span style={{ fontSize:10,color:C.muted }}>Empty</span>
        </div>
      )}
    </div>
  );
}

/* - Game Log - */
const LC = {
  sys:"#94a3b8", info:"#64748b", turn:"#f59e0b", action:"#60a5fa",
  reaction:"#f87171", asset:"#4ade80", money:"#22c55e", loss:"#ef4444",
  warn:"#fb923c", draw:"#c084fc", discard:"#6b7280", hook:"#374151",
  danger:"#ef4444", token:"#fbbf24",
};

function GameLog({ log, onCardClick }) {
  const ref = useRef(null);
  useEffect(() => { if(ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [log]);

  var renderLine = function(entry) {
    const { msg, cards=[] } = entry;
    var clean = msg.replace(/^[^a-zA-Z$"(\[]+/, '');
    if (!cards.length) return clean;
    let result = [];
    let remaining = clean;
    for (const card of cards) {
      const needle = `"${card.name}"`;
      const idx = remaining.indexOf(needle);
      if (idx >= 0) {
        if (idx > 0) result.push(remaining.slice(0, idx+1));
        result.push(
          <span key={card.id}
            onClick={() => onCardClick(card)}
            style={{ color: card.type===CT.ACTION?"#93c5fd":card.type===CT.REACTION?"#fca5a5":"#86efac",
                     cursor:"pointer", textDecoration:"underline dotted" }}>
            {card.name}
          </span>
        );
        result.push('"');
        remaining = remaining.slice(idx + needle.length);
      }
    }
    if (remaining) result.push(remaining);
    return result;
  }

  return (
    <div ref={ref} style={{ flex:1,overflowY:"auto",display:"flex",flexDirection:"column",gap:1 }}>
      {log.slice(-100).map((e,i) => (
        <div key={e.id||i} style={{ fontSize:10,color:LC[e.type]||C.muted,lineHeight:1.5,padding:"1px 0" }}>
          <span style={{ color:"#1e293b",marginRight:4,fontFamily:"'JetBrains Mono',monospace",fontSize:8 }}>[T{e.turn}]</span>
          {renderLine(e)}
        </div>
      ))}
    </div>
  );
}

/* - Shop - */
function ShopPanel({ st, dispatch }) {
  const cp=curPl(st), vp=st.players[st.viewIdx];
  const isCP=vp.id===cp.id;
  const canBuy=isCP&&!st.hasBoughtAsset&&cp.money>=MIN_BUY;
  const blindInfo = calcBlindCost(st, vp.id);
  const [showRefresh, setShowRefresh] = useState(false);
  useEffect(function() {
    if (st.shopRefreshAnim) {
      setShowRefresh(true);
      var t = setTimeout(function() {
        setShowRefresh(false);
        dispatch({ type:"CLEAR_SHOP_ANIM" });
      }, 2200);
      return function() { clearTimeout(t); };
    }
  }, [st.shopRefreshAnim]);
  return (
    <div className="brick-panel" style={{ background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:12,position:"relative",overflow:"hidden" }}>
      {showRefresh && (
        <div style={{ position:"absolute",inset:0,zIndex:10,display:"flex",flexDirection:"column",
                      alignItems:"center",justifyContent:"center",pointerEvents:"none",
                      background:"rgba(14,20,30,0.82)",animation:"fadeBackdrop .15s ease" }}>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:15,fontWeight:700,color:"#38bdf8",
                        letterSpacing:3,animation:"popIn .2s ease",marginBottom:6 }}>MARKET REFRESH</div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4,animation:"popIn .25s ease" }}>New assets available . +1 slot</div>
        </div>
      )}
      <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8 }}>
        <div style={{ fontSize:11,color:C.gold,fontFamily:"'Rubik',sans-serif",letterSpacing:2,fontWeight:700 }}>THE MARKET</div>
        <div style={{ textAlign:"right" }}>
          <div style={{ fontSize:9,color:C.muted }}>Deck: {st.assetDeck.length} . Size: {st.shopSize}</div>
        </div>
      </div>
      {(function() {
        var topMain = st.mainDeck && st.mainDeck[0];
        if (!topMain || !topMain.faceUp) return null;
        var dpOwnerName = (st.players.find(function(p){ return p.id === topMain.faceUpOwner; }) || {}).name || "?";
        return (
          <div style={{ display:"flex",alignItems:"center",gap:6,marginBottom:8,padding:"5px 8px",
                        background:"#1c1400",border:"1px solid #d97706",borderRadius:6 }}>
            <span style={{ fontSize:13 }}>🂠</span>
            <div>
              <span style={{ fontSize:10,color:"#fbbf24",fontWeight:700 }}>Delayed Payment</span>
              <span style={{ fontSize:9,color:"#92400e",marginLeft:4 }}>is on top of the main deck</span>
              <div style={{ fontSize:8,color:"#78350f" }}>Owner: {dpOwnerName} . Will pay $10k when drawn</div>
            </div>
          </div>
        );
      })()}
      <div style={{ display:"flex",gap:8,flexWrap:"wrap" }}>
        {st.shop.map(function(asset) {
          var ci = calcShopCost(st, asset, vp.id);  // always use viewing player's cost
          var costColor = ci.surcharge > 0 ? "#f97316" : C.gold;
          var displayCard = { ...asset, value:ci.cost };
          return (
            <div key={asset.id} style={{ display:"flex",flexDirection:"column",gap:4 }}>
              <CardComp card={displayCard} costColor={costColor}
                onClick={function(){dispatch({type:"OPEN_DETAIL",card:displayCard,shopInfo:ci});}}
                onDetail={function(){dispatch({type:"OPEN_DETAIL",card:displayCard,shopInfo:ci});}} />
              {(ci.surcharge > 0 || ci.discount > 0) && (
                <div style={{ fontSize:8, textAlign:"center", lineHeight:1.4 }}>
                  {ci.discount > 0 && (
                    <div style={{ color:"#4ade80" }}>-{$(ci.discount)} discount</div>
                  )}
                  {ci.surcharge > 0 && (
                    <div style={{ color:"#f97316" }}>+{$(ci.surcharge)} surcharge</div>
                  )}
                  <div style={{ color:"#64748b", fontSize:7 }}>{ci.sources.join(", ")}</div>
                </div>
              )}
              <div style={{ display:"flex",gap:3 }}>
                {canBuy && <button onClick={function(){dispatch({type:"BUY_SHOP",p:{assetId:asset.id}});}} style={{ ...btn("#052e16","#16a34a","#4ade80"),fontSize:10,padding:"3px 8px",flex:1 }}>Buy {$(ci.cost)}</button>}
                <button onClick={function(){dispatch({type:"OPEN_DETAIL",card:displayCard,shopInfo:ci});}} style={{ ...btn(C.panel,C.border,C.sub),fontSize:10,padding:"3px 7px" }}>ℹ</button>
              </div>
            </div>
          );
        })}
        {!st.shop.length && <div style={{ color:C.muted,fontSize:10 }}>Market empty.</div>}
      </div>
      {isCP && !st.hasBoughtAsset && st.assetDeck.length>0 && blindInfo && (
        <div style={{ marginTop:8 }}>
          <button onClick={function(){dispatch({type:"BUY_DECK"});}} style={{ ...btn(C.bg,C.border,C.sub),fontSize:10,width:"100%" }}>
            Blind Buy (top of deck) - <span style={{ color:blindInfo.surcharge>0?"#f97316":C.sub }}>{$(blindInfo.cost)}</span>
          </button>
          {blindInfo.surcharge > 0 && (
            <div style={{ fontSize:8,color:"#f97316",marginTop:2,textAlign:"center" }}>+{$(blindInfo.surcharge)} ({blindInfo.sources.join(", ")})</div>
          )}
        </div>
      )}
      {st.hasBoughtAsset && <div style={{ marginTop:6,fontSize:9,color:C.muted }}>✓ Asset purchased this turn.</div>}
    </div>
  );
}

/* - Asset Slot with hover tooltip - */
function AssetSlot({ asset, isUsed, canAct, dispatch, ownerId }) {
  const [hovered, setHovered] = useState(false);
  const subUI = { PASSIVE:{col:"#a78bfa",bg:"#130c1e"}, DURING:{col:"#60a5fa",bg:"#0a1220"},
                  ANYTIME:{col:"#34d399",bg:"#0a1f14"}, ONCE:{col:"#fdba74",bg:"#1c0e03"} };
  const ui = subUI[asset.sub] || {col:"#94a3b8",bg:"#1e293b"};
  const desc = typeof asset.descFn === "function" ? asset.descFn(asset) : (asset.desc||"");
  const statusLbl = isUsed ? "USED" : asset.disabled ? "DISABLED" : "READY";
  const statusCol = isUsed ? "#ef4444" : asset.disabled ? "#f97316" : "#4ade80";
  return (
    <div style={{ display:"flex",flexDirection:"column",gap:3,position:"relative" }}
      onMouseEnter={()=>setHovered(true)} onMouseLeave={()=>setHovered(false)}>
      <CardComp card={asset} isUsed={isUsed}
        onClick={()=>dispatch({type:"OPEN_DETAIL",card:asset})}
        onDetail={()=>dispatch({type:"OPEN_DETAIL",card:asset})} />
      <div style={{ display:"flex",gap:3 }}>
        {canAct && <button onClick={()=>dispatch({type:"ACTIVATE",p:{assetId:asset.id,ownerId}})} style={{ ...btn("#052e16","#16a34a","#4ade80"),fontSize:9,padding:"2px 7px" }}>Use</button>}
        <button onClick={()=>dispatch({type:"OPEN_DETAIL",card:asset})} style={{ ...btn(C.panel,C.border,C.muted),fontSize:9,padding:"2px 6px" }}>ℹ</button>
      </div>
      {hovered && (
        <div style={{
          position:"absolute", bottom:"calc(100% + 8px)", left:"50%", transform:"translateX(-50%)",
          background:"#0f0d0b", border:`1px solid ${ui.col}88`,
          borderRadius:9, padding:"10px 12px", width:190, zIndex:500,
          boxShadow:`0 8px 30px #000000cc, 0 0 14px ${ui.col}22`,
          animation:"popIn .12s ease",
          pointerEvents:"none",
        }}>
          <div style={{ fontSize:8,color:ui.col,fontWeight:700,letterSpacing:1.5,marginBottom:4 }}>{asset.sub} ASSET</div>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:12,color:"#f1f5f9",fontWeight:700,marginBottom:4 }}>{asset.name}</div>
          <div style={{ fontSize:10,color:"#94a3b8",lineHeight:1.5,marginBottom:6 }}>{desc}</div>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:9,color:C.muted,borderTop:`1px solid ${C.border}`,paddingTop:5 }}>
            <span>Value: <b style={{color:C.gold}}>${asset.origVal}k</b></span>
            <span style={{color:statusCol,fontWeight:700}}>{statusLbl}</span>
          </div>
          {asset.tokens && asset.tokens.length > 0 && (
            <div style={{ fontSize:9,color:"#94a3b8",marginTop:4 }}>
              {asset.tokens.length} token{asset.tokens.length>1?"s":""} attached
            </div>
          )}
          {/* Tooltip caret */}
          <div style={{ position:"absolute",bottom:-6,left:"50%",transform:"translateX(-50%) rotate(45deg)",
                        width:10,height:10,background:"#0f0d0b",border:`1px solid ${ui.col}88`,
                        borderTop:"none",borderLeft:"none" }} />
        </div>
      )}
    </div>
  );
}

/* - Assets - */
function AssetsPanel({ st, dispatch }) {
  const vp=st.players[st.viewIdx], cp=curPl(st);
  const isCP=vp.id===cp.id;
  return (
    <div style={{ background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:12 }}>
      <div style={{ fontSize:11,color:C.gold,fontFamily:"'Rubik',sans-serif",letterSpacing:2,fontWeight:700,marginBottom:8 }}>{vp.name}'s ASSETS</div>
      {!vp.assets.length && <div style={{ color:C.muted,fontSize:10 }}>No assets.</div>}
      <div style={{ display:"flex",gap:8,flexWrap:"wrap" }}>
        {vp.assets.map(asset => {
          const isUsed=asset.status===ST.USED;
          const canAct=!isUsed&&asset.sub!==AS.PASSIVE&&!asset.disabled&&(isCP||asset.sub===AS.ANYTIME);
          return (
            <AssetSlot key={asset.id} asset={asset} isUsed={isUsed} canAct={canAct}
              dispatch={dispatch} ownerId={vp.id} />
          );
        })}
      </div>
    </div>
  );
}

/* - Hand Panel - */

function HandPanel({ st, dispatch }) {
  const cp=curPl(st), vp=st.players[st.viewIdx];
  const isCP=vp.id===cp.id, isPlanning=st.phase===PH.BP;
  const { selectedCardId, awaitingTarget, actionsTaken, actionsLeft } = st;
  const sel = vp.hand.find(c => c.id===selectedCardId);
  // Track cards entering the hand for fade-in animation
  const prevHandIdsRef = useRef([]);
  const [enteringIds, setEnteringIds] = useState(new Set());
  useEffect(function() {
    var prev = prevHandIdsRef.current;
    var cur = vp.hand.map(function(c){ return c.id; });
    var newIds = cur.filter(function(id){ return prev.indexOf(id) < 0; });
    if (newIds.length > 0) {
      setEnteringIds(new Set(newIds));
      var t = setTimeout(function(){ setEnteringIds(new Set()); }, 450);
      return function(){ clearTimeout(t); };
    }
    prevHandIdsRef.current = cur;
  }, [vp.hand.length, vp.id]);
  useEffect(function(){ prevHandIdsRef.current = vp.hand.map(function(c){ return c.id; }); }, [vp.id]);
  const bpFreeValues=new Set(isCP?vp.assets.filter(function(a){return a.hook==="a_blueprint"&&!a.disabled&&a.lockedCard;}).map(function(a){return a.lockedCard.value;}):[]); const canAction=isCP&&isPlanning&&actionsLeft>0&&(!actionsTaken.includes(ACT.ACTION)||st.freeActions||(st.rwActionsBonus||0)>0)&&!vp.actionLocked;
  const canSell  =isCP&&isPlanning&&actionsLeft>0&&(!actionsTaken.includes(ACT.SELL)||st.freeActions);
  const canActionOrFree = canAction || !!(sel && isCP && sel.type===CT.ACTION && bpFreeValues.has(sel.value));
  return (
    <div style={{ background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:12 }}>
      <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8 }}>
        <div style={{ fontSize:11,color:C.gold,fontFamily:"'Rubik',sans-serif",letterSpacing:2,fontWeight:700 }}>
          {vp.name}'s HAND
          {awaitingTarget && <span style={{ color:C.red,marginLeft:10,fontSize:10,animation:"pulse 1s infinite" }}>← CLICK A PLAYER MAT TO TARGET</span>}
        </div>
        <div style={{ fontSize:10,color:C.muted,fontFamily:"'JetBrains Mono',monospace" }}>{vp.hand.length}/{vp.handLimit}</div>
      </div>
      <div style={{ display:"flex",gap:8,flexWrap:"wrap",minHeight:70 }}>
        {vp.hand.map(card => {
          const clickable = isCP || card.type===CT.REACTION;
          // Dim only when the card has NO available interaction for this player:
          //  - Reaction: always bright (free anytime)
          //  - Own hand: bright if can play OR can sell
          //  - Viewing someone else's hand: always dim
          const isBpFree = isCP && card.type===CT.ACTION && bpFreeValues.has(card.value);
          const canActionOrFree = canAction || isBpFree;
          const monoBlockedSet = monopolyBlockedValues(st, vp.id);
          const monoBlocked = isCP && monoBlockedSet.has(card.value);
          const cardPlayable = card.type===CT.REACTION ? true
                             : !isCP ? false
                             : monoBlocked ? false
                             : (canActionOrFree && card.type===CT.ACTION) || canSell;
          return (
            <div key={card.id} style={{ animation: enteringIds.has(card.id) ? "cardEnter 0.35s ease" : "none", display:"inline-block" }}>
            <CardComp card={card} selected={selectedCardId===card.id}
              isPlayable={cardPlayable}
              monopolyBlocked={monoBlocked}
              reactionOnly={card.type===CT.REACTION && card.hook!=="minor_loss"}
              actionLocked={isCP && !!(vp.actionLocked)}
              onClick={clickable ? ()=>dispatch({type:"SELECT_CARD",id:selectedCardId===card.id?null:card.id}) : undefined}
              onDetail={()=>dispatch({type:"OPEN_DETAIL",card})} />
            </div>
          );
        })}
        {!vp.hand.length && <div style={{ color:C.muted,fontSize:11,alignSelf:"center",fontStyle:"italic" }}>Empty hand.</div>}
      </div>
      {sel && (
        <div style={{ marginTop:10,padding:"10px 12px",background:C.bg,borderRadius:6,border:`1px solid ${C.border}` }}>
          <div style={{ fontSize:10,color:C.sub,marginBottom:7 }}>
            <b style={{color:"#f1f5f9"}}>{sel.name}</b> - {sel.desc}
          </div>
          <div style={{ display:"flex",gap:7,flexWrap:"wrap",alignItems:"center" }}>
            {sel.type===CT.ACTION && isCP && (
              sel.target==="opponent" ? (
                <button disabled={!canActionOrFree}
                  onClick={()=>canActionOrFree && dispatch({type:"AWAIT_TARGET",cardId:sel.id})}
                  style={{ ...btn("#0c1833","#1d4ed8","#60a5fa",!canActionOrFree) }}>
                  {awaitingTarget===sel.id ? "Awaiting Target..." : `Play -> Select Target [${actionsLeft} left]`}
                </button>
              ) : (
                <button disabled={!canActionOrFree}
                  onClick={()=>canActionOrFree && dispatch({type:"PLAY_ACTION",p:{cardId:sel.id}})}
                  style={{ ...btn("#0c1833","#1d4ed8","#60a5fa",!canActionOrFree) }}>
                  Play Action [{actionsLeft} left]
                </button>
              )
            )}
            {sel.type===CT.REACTION && sel.hook==="minor_loss" && (
              <button onClick={()=>dispatch({type:"PLAY_REACTION",p:{cardId:sel.id,ownerId:vp.id}})}
                style={{ ...btn("#1f0a0a","#dc2626","#f87171") }}>Play (Free)</button>
            )}
            {sel.type===CT.REACTION && sel.hook!=="minor_loss" && (
              <span style={{ fontSize:10, color:"#64748b", fontStyle:"italic", alignSelf:"center" }}>
                Plays automatically in reaction windows
              </span>
            )}
            {sel.type!==CT.ASSET && isCP && (
              <button disabled={!canSell}
                onClick={()=>dispatch({type:"SELL",p:{cardId:sel.id}})}
                style={{ ...btn("#1c1004","#d97706",C.gold,!canSell) }}>Sell {$(sel.value)}</button>
            )}
            <button onClick={()=>dispatch({type:"OPEN_DETAIL",card:sel})} style={{ ...btn(C.panel,C.border,C.sub) }}>Details</button>
            <button onClick={()=>dispatch({type:"SELECT_CARD",id:null})} style={{ ...btn(C.panel,C.border,C.muted) }}>✕</button>
          </div>
          <div style={{ marginTop:7 }}>
            <div style={{ fontSize:8,color:C.muted,marginBottom:4 }}>
              Your 3 possible actions: <b style={{color:"#94a3b8"}}>Draw</b>, <b style={{color:"#94a3b8"}}>Play Action</b>, <b style={{color:"#94a3b8"}}>Sell</b>. You can do 2 per turn.
            </div>
            <div style={{ display:"flex",gap:5 }}>
              {[ACT.DRAW,ACT.ACTION,ACT.SELL].map(a => (
                <div key={a} style={{ fontSize:8,padding:"2px 7px",borderRadius:3,
                                      background:actionsTaken.includes(a)?"#450a0a":"#052e16",
                                      color:actionsTaken.includes(a)?"#f87171":"#4ade80",
                                      border:`1px solid ${actionsTaken.includes(a)?"#7f1d1d":"#166534"}` }}>
                  {a.replace("_"," ")} {st.freeActions ? (actionsTaken.filter(function(x){return x===a;}).length > 0 ? "[×"+actionsTaken.filter(function(x){return x===a;}).length+"]" : "[ ]") : (actionsTaken.includes(a)?"[done]":"[ ]")}
                </div>
              ))}
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

/* - Turn Controls - */
function TurnControls({ st, dispatch, isMyTurn }) {
  const cp=curPl(st);
  const { phase, actionsLeft, turnsLeft } = st;
  const isPlanning = phase===PH.BP;
  const [confirmEnd, setConfirmEnd] = useState(false);
  return (
    <div style={{ display:"flex",alignItems:"center",gap:10,flexWrap:"wrap" }}>
      <div style={{ display:"flex",flexDirection:"column",alignItems:"center",minWidth:44 }}>
        <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:30,fontWeight:700,lineHeight:1,
                      color:turnsLeft<=3?C.red:turnsLeft<=6?"#fb923c":C.gold,
                      animation:turnsLeft<=3?"pulse 1.5s infinite":"none" }}>{turnsLeft}</div>
        <div style={{ fontSize:8,color:C.muted,letterSpacing:1 }}>TURNS</div>
      </div>
      <div style={{ width:1,height:36,background:C.border }} />
      <div>
        <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:14,color:cp.color,fontWeight:700 }}>{cp.name}</div>
        <div style={{ fontSize:9,color:C.muted }}>Round {st.roundNum} . Turn {st.turnNum}</div>
      </div>
      <div style={{ width:1,height:36,background:C.border }} />
      <div style={{ fontSize:10,color:C.sub }}>Phase: <b style={{color:C.green}}>{phase==="SOT"?"START OF TURN":"BUSINESS PLANNING"}</b></div>
      <div style={{ width:1,height:36,background:C.border }} />
      <div>
        <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:11,color:C.gold }}>{actionsLeft}/{DEF_ACTIONS} Actions</div>
        <div style={{ fontSize:8,color:C.muted,letterSpacing:.5 }}>Draw · Play Action · Sell</div>
      </div>
      <div style={{ width:1,height:36,background:C.border }} />
      {isPlanning && (
        <div>
          <button onClick={()=>dispatch({type:"DRAW"})}
            disabled={actionsLeft<=0||(st.actionsTaken.includes(ACT.DRAW)&&!st.freeActions)||isMyTurn===false}
            style={{ ...btn(C.bg,C.border,"#c084fc",actionsLeft<=0||(st.actionsTaken.includes(ACT.DRAW)&&!st.freeActions)||isMyTurn===false),fontSize:11 }}>
            Draw Card
          </button>
          <button onClick={()=>dispatch({type:"END_TURN"})}
            disabled={isMyTurn===false}
            style={{ ...btn("#1f0a0a","#dc2626","#f87171",isMyTurn===false),fontSize:11 }}>End Turn →</button>
          {!confirmEnd
            ? <button onClick={()=>setConfirmEnd(true)} style={{ ...btn(C.bg,"#334155",C.muted),fontSize:10,padding:"4px 10px" }}>✕ End Game</button>
            : <div style={{ display:"flex",alignItems:"center",gap:5,background:"#1c0a0a",border:"1px solid #7f1d1d",borderRadius:6,padding:"4px 8px" }}>
                <span style={{ fontSize:10,color:"#fca5a5" }}>End game?</span>
                <button onClick={()=>{ dispatch({type:"FORCE_END_GAME"}); setConfirmEnd(false); }}
                  style={{ ...btn("#7f1d1d","#ef4444","#fca5a5"),fontSize:10,padding:"3px 8px" }}>Yes</button>
                <button onClick={()=>setConfirmEnd(false)}
                  style={{ ...btn(C.bg,"#334155",C.muted),fontSize:10,padding:"3px 8px" }}>No</button>
              </div>
          }
        </div>
      )}
      {phase===PH.SOT && <div style={{ fontSize:10,color:C.muted,fontStyle:"italic" }}>Starting turn...</div>}
    </div>
  );
}

/* - Solo Test Bar - */
function SoloBar({ st, dispatch }) {
  const [amt, setAmt] = useState(2);
  const [pid, setPid] = useState(() => st.players[0]?.id || "");
  return (
    <div style={{ background:C.bg,border:`1px solid ${C.panel}`,borderRadius:8,padding:"7px 12px" }}>
      <div style={{ fontSize:8,color:C.panel,letterSpacing:2,marginBottom:5,fontWeight:600 }}>SOLO TEST MODE</div>
      <div style={{ display:"flex",gap:12,flexWrap:"wrap",alignItems:"center" }}>
        <div style={{ display:"flex",gap:4,alignItems:"center" }}>
          <span style={{ fontSize:10,color:C.muted }}>View as:</span>
          {st.players.map((p,i) => (
            <button key={p.id} onClick={()=>dispatch({type:"SET_VIEW",i})}
              style={{ ...btn(st.viewIdx===i?"#172033":C.bg,st.viewIdx===i?p.color:C.border,st.viewIdx===i?p.color:C.muted),fontSize:10,padding:"3px 8px" }}>
              {p.name}
            </button>
          ))}
        </div>
        <div style={{ width:1,height:18,background:C.panel }} />
        <div style={{ display:"flex",gap:4,alignItems:"center" }}>
          <span style={{ fontSize:10,color:C.muted }}>Debug money:</span>
          <select value={pid} onChange={e=>setPid(e.target.value)}
            style={{ background:C.panel,color:"#f1f5f9",border:`1px solid ${C.border}`,borderRadius:4,padding:"2px 5px",fontSize:10 }}>
            {st.players.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
          </select>
          <input type="number" value={amt} min={1} max={20} onChange={e=>setAmt(Number(e.target.value)||1)}
            style={{ width:44,background:C.panel,color:C.gold,border:`1px solid ${C.border}`,borderRadius:4,padding:"2px",fontSize:10,textAlign:"center" }} />
          <button onClick={()=>dispatch({type:"ADJ_MONEY",pid,delta:amt})}  style={{ ...btn("#052e16","#16a34a","#4ade80"),fontSize:10,padding:"3px 7px" }}>+</button>
          <button onClick={()=>dispatch({type:"ADJ_MONEY",pid,delta:-amt})} style={{ ...btn("#450a0a","#7f1d1d","#ef4444"),fontSize:10,padding:"3px 7px" }}>−</button>
        </div>
        <div style={{ fontSize:9,color:"#1e293b" }}>Main: {st.mainDeck.length} . Disc: {st.mainDiscard.length} . Asset: {st.assetDeck.length}</div>
      </div>
    </div>
  );
}

/* - Score Screen - */
function ScoreScreen({ st, onMenu, onRematch }) {
  const [showStats, setShowStats] = useState(false);
  const scores = st.scores || [];
  const statsRows = [...scores].sort((a,b) => (b.totalGained||0) - (a.totalGained||0));
  const isMob = typeof window !== "undefined" && window.innerWidth < 600;

  return (
    <div className="sb-bg-warm" style={{ minHeight:"100vh", overflowY:"auto", padding:"16px 12px" }}>
      <style>{CSS}</style>
      <div style={{ maxWidth:560, margin:"0 auto", animation:"popIn .3s ease" }}>

        {/* Header */}
        <div style={{ textAlign:"center", marginBottom:20 }}>
          <div style={{ fontSize:9, letterSpacing:5, color:C.muted, fontFamily:"'Rubik',sans-serif", marginBottom:6 }}>GAME OVER</div>
          <div style={{ fontSize:isMob?24:34, fontFamily:"'Rubik',sans-serif", fontWeight:700, color:C.gold }}>
            {st.winner?.name} Wins
          </div>
          <div style={{ fontSize:11, color:C.muted, marginTop:4 }}>After {st.totalTurns} rounds</div>
          <div style={{ display:"flex", gap:10, justifyContent:"center", marginTop:14, flexWrap:"wrap" }}>
            <button onClick={onMenu} style={{ ...btn("#0f172a","#d97706",C.gold), fontSize:12, padding:"8px 20px", letterSpacing:1 }}>
              ↩ Back to Menu
            </button>
            {onRematch && (
              <button onClick={onRematch}
                style={{ ...btn("#052e16","#16a34a","#4ade80"), fontSize:12, padding:"8px 20px", letterSpacing:1, fontWeight:700 }}>
                Rematch
              </button>
            )}
          </div>
        </div>

        {/* Final standings — stacked cards on mobile, grid on desktop */}
        {isMob ? (
          <div style={{ display:"flex", flexDirection:"column", gap:8, marginBottom:12 }}>
            {scores.map(function(p, i) {
              return (
                <div key={p.id} style={{ background: i===0 ? "#0c2a0f" : C.panel,
                                          border:"1px solid "+(i===0?"#22c55e":C.border),
                                          borderRadius:10, padding:"12px 14px" }}>
                  <div style={{ display:"flex", alignItems:"center", gap:8, marginBottom:8 }}>
                    <span style={{ fontSize:11,fontWeight:800,color:["#fde68a","#94a3b8","#cd7f32"][i]||"transparent",fontFamily:"'JetBrains Mono',monospace",minWidth:14 }}>{["1","2","3"][i]||""}</span>
                    <div style={{ width:8,height:8,borderRadius:"50%",background:p.color }} />
                    <span style={{ fontFamily:"'Rubik',sans-serif", fontSize:14, fontWeight:700, color:"#f1f5f9", flex:1 }}>{p.name}</span>
                    <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:18, fontWeight:700,
                                  color: i===0?"#fde68a":"#f59e0b" }}>{p.points||0} pts</div>
                  </div>
                  <div style={{ display:"flex", gap:12, fontSize:11, color:C.muted }}>
                    <span>Cash <span style={{ color:p.money<0?C.red:C.green, fontWeight:700 }}>{$(p.money)}</span></span>
                    <span>Assets <span style={{ color:"#4ade80", fontWeight:700 }}>{$(p.assetVal)}</span></span>
                    <span>= <span style={{ color: i===0?C.gold:C.sub, fontWeight:700 }}>{$(p.netWorth)}</span></span>
                  </div>
                </div>
              );
            })}
          </div>
        ) : (
          <div style={{ background:C.panel, border:"1px solid "+C.border, borderRadius:12, overflow:"hidden", marginBottom:12 }}>
            <div style={{ display:"grid", gridTemplateColumns:"1fr 60px 60px 72px 80px",
                          padding:"8px 14px", background:C.bg, borderBottom:"1px solid "+C.border }}>
              {["PLAYER","CASH","ASSETS","NET","PTS"].map(function(h) {
                return <div key={h} style={{ fontSize:9,color:C.muted,letterSpacing:1,textAlign:h==="PLAYER"?"left":"center",fontWeight:600 }}>{h}</div>;
              })}
            </div>
            {scores.map(function(p, i) {
              return (
                <div key={p.id} style={{ display:"grid", gridTemplateColumns:"1fr 60px 60px 72px 80px",
                                          padding:"12px 14px", alignItems:"center",
                                          borderBottom:"1px solid "+C.bg, background:i===0?"#0c2a0f":C.panel }}>
                  <div style={{ display:"flex", alignItems:"center", gap:6 }}>
                    <span style={{ fontSize:11,fontWeight:800,color:["#fde68a","#94a3b8","#cd7f32"][i]||"transparent",fontFamily:"'JetBrains Mono',monospace" }}>{["1","2","3"][i]||""}</span>
                    <div style={{ width:7,height:7,borderRadius:"50%",background:p.color }} />
                    <span style={{ fontFamily:"'Rubik',sans-serif", fontSize:12, color:"#f1f5f9" }}>{p.name}</span>
                  </div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:11,color:p.money<0?C.red:C.green,textAlign:"center" }}>{$(p.money)}</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:11,color:"#4ade80",textAlign:"center" }}>{$(p.assetVal)}</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:i===0?C.gold:C.sub,fontWeight:700,textAlign:"center" }}>{$(p.netWorth)}</div>
                  <div style={{ textAlign:"center" }}>
                    <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:14,color:i===0?"#fde68a":"#f59e0b",fontWeight:700 }}>{p.points||0}</div>
                    <div style={{ fontSize:7,color:"#64748b" }}>{Math.floor((p.netWorth||0)/10)}+{p.assets?p.assets.length:0}</div>
                  </div>
                </div>
              );
            })}
          </div>
        )}

        {/* Balancing stats toggle */}
        <div style={{ textAlign:"center", marginBottom:8 }}>
          <button onClick={function(){ setShowStats(function(s){ return !s; }); }}
            style={{ ...btn(C.bg,"#334155","#94a3b8"), fontSize:10, padding:"5px 16px" }}>
            {showStats ? "▲ Hide Balancing Stats" : "▼ Show Balancing Stats"}
          </button>
        </div>
        {showStats && (
          <div style={{ background:C.panel, border:"1px solid #334155", borderRadius:12, overflow:"hidden", marginBottom:12 }}>
            <div style={{ padding:"10px 14px", background:C.bg, borderBottom:"1px solid #334155" }}>
              <div style={{ fontSize:10,color:C.gold,fontFamily:"'Rubik',sans-serif",letterSpacing:2,fontWeight:700 }}>MONEY FLOW</div>
            </div>
            <div style={{ display:"grid", gridTemplateColumns:"1fr 70px 70px 70px 60px",
                          padding:"6px 14px", background:"#0f172a", borderBottom:"1px solid #1e293b" }}>
              {["PLAYER","GAINED","LOST","NET","RATIO"].map(function(h) {
                return <div key={h} style={{ fontSize:8,color:C.muted,letterSpacing:1,textAlign:h==="PLAYER"?"left":"center",fontWeight:600 }}>{h}</div>;
              })}
            </div>
            {statsRows.map(function(p) {
              var gained=p.totalGained||0, lost=p.totalLost||0, net=gained-lost;
              var ratioNum=lost>0?gained/lost:99, ratio=lost>0?(gained/lost).toFixed(2):"∞";
              var ratioColor=ratioNum>=1.5?"#4ade80":ratioNum>=1.0?"#facc15":"#f87171";
              return (
                <div key={p.id} style={{ display:"grid", gridTemplateColumns:"1fr 70px 70px 70px 60px",
                                          padding:"9px 14px", borderBottom:"1px solid #1e293b" }}>
                  <div style={{ display:"flex",alignItems:"center",gap:6 }}>
                    <div style={{ width:6,height:6,borderRadius:"50%",background:p.color }} />
                    <span style={{ fontSize:11,color:"#f1f5f9" }}>{p.name}</span>
                  </div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#4ade80",textAlign:"center" }}>+{$(gained)}</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#f87171",textAlign:"center" }}>{"-"+$(lost)}</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:net>=0?"#4ade80":"#f87171",textAlign:"center",fontWeight:700 }}>{net>=0?"+":""}{$(net)}</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:ratioColor,textAlign:"center",fontWeight:700 }}>{ratio}x</div>
                </div>
              );
            })}
            {(function(){
              var tg=statsRows.reduce(function(s,p){return s+(p.totalGained||0);},0);
              var tl=statsRows.reduce(function(s,p){return s+(p.totalLost||0);},0);
              return (
                <div style={{ display:"grid",gridTemplateColumns:"1fr 70px 70px 70px 60px",
                              padding:"8px 14px",background:"#0f172a",borderTop:"1px solid #334155" }}>
                  <div style={{ fontSize:9,color:C.muted,fontWeight:700 }}>ALL</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#4ade80",textAlign:"center" }}>+{$(tg)}</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#f87171",textAlign:"center" }}>{"-"+$(tl)}</div>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:C.sub,textAlign:"center" }}>{$(tg-tl)}</div>
                  <div style={{ fontSize:9,color:C.muted,textAlign:"center" }}>-</div>
                </div>
              );
            })()}
          </div>
        )}
      </div>
    </div>
  );
}


function SetupScreen({ onStart, settings, setSettings, onBack, initialCardSettings, onHowToPlay }) {
  const [count, setCount] = useState(3);
  const [names, setNames] = useState(function(){
    try { var s=JSON.parse(localStorage.getItem("sb_player_names")||"null");
          if (Array.isArray(s) && s.length >= 5) return s; } catch(e){}
    return ["Alex","Blake","Casey","Drew","Evie"];
  });
  const init = initialCardSettings || {};
  const [turns, setTurns] = useState(init.turns !== undefined ? init.turns : 15);
  const [tab, setTab] = useState("setup"); // "setup" | "settings" | "cards"
  const [freeActions, setFreeActions] = useState(init.freeActions !== undefined ? init.freeActions : true);
  const [disabled, setDisabled] = useState(function(){
    if (init.disabled) return new Set(init.disabled);
    // R&D Budget is off by default in Free mode
    return (init.freeActions === false) ? new Set() : new Set(["R&D Budget"]);
  });
  const [cardCounts, setCardCounts] = useState(init.cardCounts || {});

  const allCardDefs = [
    ...ACTIONS_T.map(function(t){ return {...t, deckType:"ACTION"}; }),
    ...REACTIONS_T.map(function(t){ return {...t, deckType:"REACTION"}; }),
    ...ASSETS_T.map(function(t){ return {...t, deckType:"ASSET"}; }),
  ];

  var toggleCard = function(name) {
    setDisabled(function(prev) {
      var next = new Set(prev);
      if (next.has(name)) next.delete(name); else next.add(name);
      return next;
    });
  }

  var adjustCount = function(name, delta, defCount) {
    setCardCounts(function(prev) {
      var cur = (prev[name] !== undefined) ? prev[name] : defCount;
      var next = Math.max(0, cur + delta);
      var out = {...prev};
      if (next === defCount) delete out[name]; else out[name] = next;
      return out;
    });
  }

  const ts = {
    ACTION:   {col:"#93c5fd", bg:"#0a1220", border:"#2563eb"},
    REACTION: {col:"#f87171", bg:"#1f0a0a", border:"#dc2626"},
    ASSET:    {col:"#4ade80", bg:"#0a1f0a", border:"#16a34a"},
  };

  const sections = [
    {label:"ACTION CARDS",   key:"ACTION"},
    {label:"REACTION CARDS", key:"REACTION"},
    {label:"ASSET CARDS",    key:"ASSET"},
  ];

  const tabBtn = function(id, label) {
    var active = tab === id;
    return (
      <button key={id} onClick={function(){ setTab(id); }}
        style={{ flex:1, padding:"8px 0", fontSize:11, fontWeight:700,
                 letterSpacing:1, cursor:"pointer", borderRadius:8,
                 background:active?"#1c1004":"#1e293b",
                 border:"1px solid "+(active?"#d97706":"#334155"),
                 color:active?C.gold:"#64748b", fontFamily:"'Rubik',sans-serif" }}>
        {label}
      </button>
    );
  };

  return (
    <div className="sb-bg-warm" style={{ minHeight:"100vh", display:"flex",
                  alignItems:"center", justifyContent:"center", padding:24, position:"relative" }}>
      <style>{CSS}</style>
      <div style={{ maxWidth:480, width:"100%", animation:"popIn .3s ease" }}>

        {/* Title */}
        <div style={{ textAlign:"center", marginBottom:24 }}>
          {onBack && (
        <button onClick={onBack}
          style={{ position:"absolute",top:14,left:14,background:"none",border:"none",
                   color:"#64748b",cursor:"pointer",fontSize:12,padding:"4px 8px",
                   borderRadius:6,zIndex:10 }}>
          ← Mode Select
        </button>
      )}
      <div style={{ fontSize:9, letterSpacing:6, color:"#334155",
                        fontFamily:"'Rubik',sans-serif", marginBottom:8 }}>WELCOME TO</div>
          <div style={{ fontSize:48, fontFamily:"'Rubik',sans-serif",
                        fontWeight:700, color:"#ef4444", lineHeight:.85 }}>STRICTLY</div>
          <div style={{ fontSize:48, fontFamily:"'Rubik',sans-serif",
                        fontWeight:700, color:"#22c55e", lineHeight:.85 }}>BUSINESS</div>
          <div style={{ fontSize:9, color:"#64748b", letterSpacing:4, marginTop:8 }}>
            THE ENTREPRENEUR CARD GAME
          </div>
        </div>

        {/* START GAME button */}
        <button onClick={function(){
            onStart(names.slice(0,count).map(function(n,i){ return n||"Player "+(i+1); }),
                    turns, disabled, cardCounts,
                    { turns, disabled: Array.from(disabled), cardCounts, freeActions },
                    freeActions);
          }}
          style={{ width:"100%", padding:"14px 0", fontSize:15, fontWeight:700,
                   letterSpacing:2, cursor:"pointer", borderRadius:10,
                   background:"#1c1004", border:"2px solid #d97706", color:C.gold,
                   fontFamily:"'Rubik',sans-serif", marginBottom:10 }}>
          START GAME ▶
        </button>

        {/* Tab row: Settings | How To Play */}
        <div style={{ display:"flex", gap:8, marginBottom:16 }}>
          {tabBtn("setup",    "SETUP")}
          {tabBtn("settings", "SETTINGS")}
          {tabBtn("cards",    "CARDS")}
          <button onClick={onHowToPlay} disabled={!onHowToPlay}
            style={{ flex:1, padding:"8px 0", fontSize:11,
                     fontWeight:700, letterSpacing:1,
                     cursor: onHowToPlay ? "pointer" : "not-allowed",
                     borderRadius:8,
                     background: onHowToPlay ? "#1e293b" : "#1e293b",
                     border:"1px solid " + (onHowToPlay ? "#475569" : "#334155"),
                     color: onHowToPlay ? "#94a3b8" : "#334155",
                     fontFamily:"'Rubik',sans-serif" }}>
            ? HOW TO PLAY
          </button>
        </div>

        {/* - SETUP tab - */}
        {tab === "setup" && (
          <div style={{ background:C.panel, border:"1px solid "+C.border,
                        borderRadius:12, padding:18 }}>
            {/* Turns */}
            <div style={{ marginBottom:20 }}>
              <div style={{ fontSize:8, color:C.muted, letterSpacing:3,
                            fontFamily:"'Rubik',sans-serif", fontWeight:700,
                            marginBottom:10, textTransform:"uppercase" }}>Game Length</div>
              <div style={{ display:"flex", gap:8 }}>
                {[10,15,20].map(function(n){
                  return (
                    <button key={n} onClick={function(){ setTurns(n); }}
                      style={{ flex:1, padding:"12px 0", fontSize:22, fontWeight:700,
                               cursor:"pointer", borderRadius:6,
                               fontFamily:"'JetBrains Mono',monospace",
                               background:turns===n?"#1c1004":C.bg,
                               border:"2px solid "+(turns===n?C.gold:C.border),
                               color:turns===n?C.gold:C.muted }}>
                      {n}
                    </button>
                  );
                })}
              </div>
              <div style={{ fontSize:9,color:"#4b5563",marginTop:6,letterSpacing:.5 }}>turns per game</div>
            </div>
            {/* Player count */}
            <div style={{ marginBottom:20 }}>
              <div style={{ fontSize:8, color:C.muted, letterSpacing:3,
                            fontFamily:"'Rubik',sans-serif", fontWeight:700,
                            marginBottom:10, textTransform:"uppercase" }}>Players</div>
              <div style={{ display:"flex", gap:8 }}>
                {[3,4,5].map(function(n){
                  return (
                    <button key={n} onClick={function(){ setCount(n); }}
                      style={{ flex:1, padding:"8px 0", fontSize:18, fontWeight:700,
                               cursor:"pointer", borderRadius:6,
                               fontFamily:"'JetBrains Mono',monospace",
                               background:count===n?"#052e16":C.bg,
                               border:"2px solid "+(count===n?"#16a34a":C.border),
                               color:count===n?"#4ade80":C.muted }}>
                      {n}
                    </button>
                  );
                })}
              </div>
            </div>
            {/* Player names */}
            {Array.from({length:count}, function(_,i){
              return (
                <div key={i} style={{ marginBottom:8 }}>
                  <div style={{ fontSize:9, color:C.muted, letterSpacing:1,
                                marginBottom:3, fontWeight:600,
                                display:"flex", alignItems:"center", gap:6 }}>
                    PLAYER {i+1}
                    <span style={{ display:"inline-block", width:8, height:8,
                                   borderRadius:"50%", background:PCOLORS[i] }} />
                  </div>
                  <input value={names[i]||""} onChange={function(e){
                      setNames(function(ns){ var n=[...ns]; n[i]=e.target.value;
                        try { localStorage.setItem("sb_player_names", JSON.stringify(n)); } catch(x){}
                        return n; });
                    }}
                    style={{ width:"100%", background:C.bg, color:"#f1f5f9",
                             border:"1px solid "+C.border, borderRadius:6,
                             padding:"9px 12px", fontSize:13,
                             boxSizing:"border-box" }} />
                </div>
              );
            })}
          </div>
        )}

        {/* - SETTINGS tab - */}
        {tab === "settings" && (
          <div style={{ background:C.panel, border:"1px solid "+C.border,
                        borderRadius:12, padding:18 }}>
            <div style={{ fontFamily:"'Rubik',sans-serif", fontSize:12, color:C.gold,
                          fontWeight:700, letterSpacing:1, marginBottom:14,
                          borderBottom:"1px solid "+C.border, paddingBottom:8 }}>
              GAME SETTINGS
            </div>
            {/* Money Popups */}
            <div style={{ display:"flex", justifyContent:"space-between",
                          alignItems:"flex-start", marginBottom:14,
                          paddingBottom:14, borderBottom:"1px solid #1e293b" }}>
              <div>
                <div style={{ fontSize:12, color:"#f1f5f9", fontWeight:700, marginBottom:4 }}>
                  Money Popups
                </div>
                <div style={{ fontSize:10, color:C.muted, lineHeight:1.5, maxWidth:300 }}>
                  Show animated +/- overlays on player cards when money changes.
                  Helpful for tracking gains and losses at a glance.
                </div>
              </div>
              <button onClick={function(){
                  setSettings(function(s){ return {...s, moneyPopups:!s.moneyPopups}; });
                }}
                style={{ flexShrink:0, marginLeft:12, padding:"6px 16px", fontSize:11,
                         fontWeight:700, borderRadius:6, cursor:"pointer",
                         background:settings.moneyPopups?"#052e16":"#3b0000",
                         border:"1px solid "+(settings.moneyPopups?"#16a34a":"#ef4444"),
                         color:settings.moneyPopups?"#4ade80":"#f87171" }}>
                {settings.moneyPopups ? "ON" : "OFF"}
              </button>
            </div>
            {/* Action Rules */}
            <div style={{ display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:14,paddingBottom:14,borderBottom:"1px solid #1e293b" }}>
              <div style={{ flex:1,marginRight:12 }}>
                <div style={{ fontSize:12,color:"#f1f5f9",fontWeight:700,marginBottom:4 }}>Action Rules</div>
                <div style={{ fontSize:10,color:"#94a3b8",lineHeight:1.5,maxWidth:300 }}>
                  {freeActions ? "Free: use both actions on any combination (play 2 cards, draw twice, etc.)" : "Limited: each action type (play/sell/draw) can only be used once per turn"}
                </div>
              </div>
              <div style={{ display:"flex",gap:6,flexShrink:0 }}>
                <button onClick={function(){
                    setFreeActions(false);
                    setDisabled(function(prev){ var n=new Set(prev); n.delete("R&D Budget"); return n; });
                  }}
                  style={{ padding:"6px 12px",fontSize:11,fontWeight:700,borderRadius:6,cursor:"pointer",
                           background:!freeActions?"#0c1833":"#1e293b",border:"1px solid "+(!freeActions?"#3b82f6":"#334155"),
                           color:!freeActions?"#93c5fd":"#64748b" }}>Limited</button>
                <button onClick={function(){
                    setFreeActions(true);
                    setDisabled(function(prev){ var n=new Set(prev); n.add("R&D Budget"); return n; });
                  }}
                  style={{ padding:"6px 12px",fontSize:11,fontWeight:700,borderRadius:6,cursor:"pointer",
                           background:freeActions?"#052e16":"#1e293b",border:"1px solid "+(freeActions?"#16a34a":"#334155"),
                           color:freeActions?"#4ade80":"#64748b" }}>Free</button>
              </div>
            </div>
            {/* Mobile View */}
            <div style={{ display:"flex", justifyContent:"space-between",
                          alignItems:"flex-start" }}>
              <div>
                <div style={{ fontSize:12, color:"#f1f5f9", fontWeight:700, marginBottom:4 }}>
                  Mobile Layout
                </div>
                <div style={{ fontSize:10, color:C.muted, lineHeight:1.5, maxWidth:300 }}>
                  Switch to a compact mobile-friendly layout optimised for smaller screens.
                </div>
              </div>
              <button onClick={function(){
                  setSettings(function(s){ return {...s, mobileView:!s.mobileView}; });
                }}
                style={{ flexShrink:0, marginLeft:12, padding:"6px 16px", fontSize:11,
                         fontWeight:700, borderRadius:6, cursor:"pointer",
                         background:(settings.mobileView)?"#052e16":"#3b0000",
                         border:"1px solid "+((settings.mobileView)?"#16a34a":"#ef4444"),
                         color:(settings.mobileView)?"#4ade80":"#f87171" }}>
                {settings.mobileView ? "ON" : "OFF"}
              </button>
            </div>
          </div>
        )}

        {/* - CARDS tab - */}
        {tab === "cards" && (
          <div style={{ background:C.panel, border:"1px solid "+C.border,
                        borderRadius:12, padding:18 }}>
            <div style={{ display:"flex", justifyContent:"space-between",
                          alignItems:"center", marginBottom:14,
                          borderBottom:"1px solid "+C.border, paddingBottom:8 }}>
              <div style={{ fontFamily:"'Rubik',sans-serif", fontSize:12, color:C.gold,
                            fontWeight:700, letterSpacing:1 }}>
                CARD CONFIGURATION
              </div>
              <button onClick={function(){ setDisabled(new Set()); setCardCounts({}); }}
                style={{ fontSize:9, padding:"4px 10px", background:"#052e16",
                         border:"1px solid #16a34a", color:"#4ade80",
                         borderRadius:5, cursor:"pointer", fontWeight:700 }}>
                Reset to Default
              </button>
            </div>
            <div style={{ maxHeight:"52vh",overflowY:"auto",paddingRight:4,marginTop:4 }}>
            {sections.map(function(sec){
              var cards = allCardDefs
                .filter(function(t){ return t.deckType === sec.key; })
                .slice().sort(function(a,b){ return a.name.localeCompare(b.name); });
              var t = ts[sec.key];
              var enabledCount = cards.filter(function(c){ return !disabled.has(c.name); }).length;
              return (
                <div key={sec.key} style={{ marginBottom:20 }}>
                  <div style={{ fontSize:9, letterSpacing:2, color:t.col,
                                fontWeight:700, marginBottom:8,
                                borderBottom:"1px solid "+t.border+"66", paddingBottom:4,
                                display:"flex", justifyContent:"space-between" }}>
                    <span>{sec.label}</span>
                    <span style={{ color:"#475569", fontWeight:400 }}>
                      {enabledCount}/{cards.length} enabled
                    </span>
                  </div>
                  {cards.map(function(card){
                    var isOff = disabled.has(card.name);
                    var defCount = card.count !== undefined ? card.count : 1;
                    var curCount = (cardCounts[card.name] !== undefined)
                                   ? cardCounts[card.name] : defCount;
                    var isRndLocked = card.name === "R&D Budget" && freeActions;
                    return (
                      <div key={card.name} style={{ borderBottom:"1px solid #1a2030",
                                                     padding:"10px 0", opacity:isRndLocked?0.45:1 }}>
                        <div style={{ display:"flex", alignItems:"center", gap:8 }}>
                          {/* Toggle */}
                          <button onClick={function(){ if(!isRndLocked) toggleCard(card.name); }}
                            style={{ width:38, height:22, borderRadius:11, border:"none",
                                     cursor:isRndLocked?"not-allowed":"pointer", flexShrink:0, position:"relative",
                                     transition:"background .2s",
                                     background:isOff?"#1e293b":"#15803d" }}>
                            <span style={{ position:"absolute", top:3,
                                           left:isOff?"3px":"19px",
                                           width:16, height:16, borderRadius:8,
                                           background:"#f1f5f9",
                                           transition:"left .2s",
                                           display:"block" }} />
                          </button>
                          {/* Name */}
                          <div style={{ flex:1, fontSize:12,
                                        color:isOff?"#475569":"#f1f5f9",
                                        fontWeight:600 }}>
                            {card.name}
                            {isRndLocked && <span style={{ fontSize:9,color:"#64748b",marginLeft:6,fontWeight:400 }}>disabled in Free mode</span>}
                          </div>
                          {/* Count +/- */}
                          <div style={{ display:"flex", alignItems:"center", gap:4 }}>
                            <button onClick={function(){ adjustCount(card.name, -1, defCount); }}
                              disabled={curCount <= 0}
                              style={{ width:22, height:22, borderRadius:4, border:"none",
                                       cursor:curCount>0?"pointer":"not-allowed",
                                       background:"#1e293b", color:curCount>0?"#f1f5f9":"#334155",
                                       fontSize:14, fontWeight:700,
                                       display:"flex", alignItems:"center", justifyContent:"center" }}>
                              −
                            </button>
                            <span style={{ fontFamily:"'JetBrains Mono',monospace",
                                           fontSize:11, color:t.col,
                                           minWidth:16, textAlign:"center" }}>
                              {curCount}
                            </span>
                            <button onClick={function(){ adjustCount(card.name, 1, defCount); }}
                              style={{ width:22, height:22, borderRadius:4, border:"none",
                                       cursor:"pointer", background:"#1e293b",
                                       color:"#f1f5f9", fontSize:14, fontWeight:700,
                                       display:"flex", alignItems:"center", justifyContent:"center" }}>
                              +
                            </button>
                          </div>
                        </div>
                        {/* Description */}
                        <div style={{ fontSize:9, color:"#475569", lineHeight:1.5,
                                      marginTop:4, paddingLeft:46 }}>
                          {card.desc || ""}
                        </div>
                      </div>
                    );
                  })}
                </div>
              );
            })}
            </div>
          </div>
        )}

      </div>
    </div>
  );
}


function TaxesSelectModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "TAXES_SELECT_ASSET") return null;
  const [sel, setSel] = useState(null);
  const [timeLeft, setTimeLeft] = useState(20);

  useEffect(function() { setSel(null); }, [pc && pc.timerEnd]);

  useEffect(function() {
    if (!pc || pc.type !== "TAXES_SELECT_ASSET") return;
    function tick() {
      var left = Math.max(0, Math.ceil((pc.timerEnd - Date.now()) / 1000));
      setTimeLeft(left);
      if (left <= 0) dispatch({ type:"RESOLVE_CHOICE", id:"auto" });
    }
    tick();
    var iv = setInterval(tick, 250);
    return function() { clearInterval(iv); };
  }, [pc && pc.timerEnd]);


  var owner = st.players.find(function(p) { return p.id === pc.srcPlayer; });
  var timerPct = Math.max(0, (pc.timerEnd - Date.now()) / 20000 * 100);
  var timerColor = timerPct > 50 ? "#22c55e" : timerPct > 25 ? "#f59e0b" : "#ef4444";

  // Group options by owner
  var byOwner = {};
  st.players.forEach(function(p) {
    if (p.id === pc.srcPlayer) return;
    p.assets.forEach(function(a) {
      if (!a.disabled) {
        if (!byOwner[p.id]) byOwner[p.id] = { name:p.name, color:p.color, assets:[] };
        byOwner[p.id].assets.push(a);
      }
    });
  });

  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.8)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:300 }}>
      <div style={{ background:"#0f172a",border:"2px solid #ef4444",borderRadius:14,padding:20,width:560,maxWidth:"95vw",boxShadow:"0 0 40px #ef444433" }}>
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:8 }}>
          <div>
            <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:15,color:"#f87171",fontWeight:700,letterSpacing:1 }}>TAXES</div>
            <div style={{ fontSize:12,color:"#94a3b8",marginTop:3 }}>{pc.prompt}</div>
          </div>
          <div style={{ textAlign:"right" }}>
            <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:26,fontWeight:700,
                          color:timeLeft<=5?"#ef4444":timeLeft<=10?"#f59e0b":"#f1f5f9",
                          animation:timeLeft<=5?"pulse 0.6s infinite":"none" }}>{timeLeft}s</div>
            <div style={{ fontSize:9,color:"#64748b",letterSpacing:1 }}>SECONDS LEFT</div>
          </div>
        </div>
        <div style={{ height:5,background:"#1e293b",borderRadius:3,marginBottom:14,overflow:"hidden" }}>
          <div style={{ height:"100%",width:timerPct+"%",background:timerColor,borderRadius:3,transition:"width 0.25s linear,background 0.5s" }} />
        </div>
        <div style={{ maxHeight:320,overflowY:"auto",display:"flex",flexDirection:"column",gap:12,marginBottom:14 }}>
          {Object.values(byOwner).map(function(ownerGroup) {
            return (
              <div key={ownerGroup.name}>
                <div style={{ fontSize:9,color:ownerGroup.color,fontWeight:700,letterSpacing:1,marginBottom:6 }}>{ownerGroup.name.toUpperCase()}</div>
                <div style={{ display:"flex",flexWrap:"wrap",gap:8 }}>
                  {ownerGroup.assets.map(function(asset) {
                    var isSelected = sel === asset.id;
                    return (
                      <div key={asset.id} onClick={function() { setSel(asset.id); }}
                        style={{ width:108,border:isSelected?"2px solid #ef4444":"1px solid #4ade8066",
                                 borderRadius:8,background:isSelected?"#3b0000":"#0a1f0a",
                                 padding:"8px 6px",cursor:"pointer",textAlign:"center",
                                 boxShadow:isSelected?"0 0 14px #ef444455":"none",
                                 transition:"all .12s",userSelect:"none" }}>
                        {isSelected && <div style={{ fontSize:8,marginBottom:2,letterSpacing:1,color:"#f87171",fontWeight:800 }}>SELECTED</div>}
                        <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:10,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{asset.name}</div>
                        <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:C.gold,fontWeight:700,marginTop:3 }}>${asset.value}k</div>
                        <div style={{ fontSize:8,color:"#64748b",marginTop:3,lineHeight:1.3 }}>{getDesc(asset)}</div>
                      </div>
                    );
                  })}
                </div>
              </div>
            );
          })}
        </div>
        <div style={{ display:"flex",gap:8 }}>
          <button onClick={function() { dispatch({type:"RESOLVE_CHOICE",id:"auto"}); }}
            style={{ flex:"0 0 auto",background:"#1e293b",border:"1px solid #475569",color:"#94a3b8",borderRadius:6,padding:"8px 14px",cursor:"pointer",fontSize:11,fontWeight:600 }}>
            ↻ Random
          </button>
          <button onClick={function() { if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
            disabled={!sel}
            style={{ flex:1,padding:"10px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                     cursor:sel?"pointer":"not-allowed",borderRadius:8,
                     background:sel?"#7f1d1d":"#1e293b",
                     border:"2px solid "+(sel?"#ef4444":"#334155"),
                     color:sel?"#f87171":"#475569",transition:"all .2s" }}>
            {sel ? "Tax This Asset" : "Select an Asset"}
          </button>
        </div>
      </div>
    </div>
  );
}

/* - Kickstarter Choice Modal - */
function KickstarterChoiceModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "KICKSTARTER_CHOICE") return null;
  const [timeLeft, setTimeLeft] = useState(20);

  useEffect(function() {
    if (!pc || pc.type !== "KICKSTARTER_CHOICE") return;
    function tick() {
      var left = Math.max(0, Math.ceil((pc.timerEnd - Date.now()) / 1000));
      setTimeLeft(left);
      if (left <= 0) dispatch({ type:"RESOLVE_CHOICE", id:"auto" });
    }
    tick();
    var iv = setInterval(tick, 250);
    return function() { clearInterval(iv); };
  }, [pc && pc.timerEnd]);


  var tgtPlayer = st.players.find(function(p) { return p.id === pc.tgtPlayer; });
  var owner     = st.players.find(function(p) { return p.id === pc.srcPlayer; });
  if (!tgtPlayer || !owner) return null;

  var timerPct   = Math.max(0, (pc.timerEnd - Date.now()) / 20000 * 100);
  var timerColor = timerPct > 50 ? "#22c55e" : timerPct > 25 ? "#f59e0b" : "#ef4444";
  var deckEmpty  = pc.assetDeckEmpty;

  var options = [
    { id:"2", label:"Pay $2k",              sub:"Minimum contribution",           cost:2, extra:null,              color:"#475569", border:"#475569", available:true },
    { id:"3", label:"Pay $3k -> Draw 1",     sub:"Draw one card from the deck",    cost:3, extra:"Draw 1 card",     color:"#60a5fa", border:"#3b82f6", available:true },
    { id:"4", label:"Pay $4k -> Draw 2",     sub:"Draw two cards from the deck",   cost:4, extra:"Draw 2 cards",    color:"#a78bfa", border:"#7c3aed", available:true },
    { id:"6", label:"Pay $6k -> Asset + Draw 1", sub: deckEmpty ? "Asset deck empty" : "Gain an asset + draw a card",
      cost:6, extra:"Asset + Draw 1",
      color: deckEmpty ? "#374151" : "#4ade80",
      border: deckEmpty ? "#374151" : "#16a34a",
      available: !deckEmpty },
  ];

  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:300 }}>
      <div style={{ background:"#0f172a",border:"2px solid "+C.gold,borderRadius:14,padding:22,width:520,maxWidth:"95vw",boxShadow:"0 0 40px #d9770644" }}>
        {/* Header */}
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:8 }}>
          <div>
            <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:15,color:C.gold,fontWeight:700,letterSpacing:1 }}>🚀 Kickstarter</div>
            <div style={{ fontSize:12,color:"#94a3b8",marginTop:3 }}>
              <span style={{ color:tgtPlayer.color,fontWeight:700 }}>{tgtPlayer.name}</span>: {pc.prompt}
            </div>
            <div style={{ fontSize:11,color:"#64748b",marginTop:2 }}>
              Paying: <span style={{ color:owner.color,fontWeight:600 }}>{owner.name}</span>
            </div>
            <div style={{ fontSize:12,color:"#94a3b8",marginTop:4 }}>
              Your balance: <span style={{ fontFamily:"'JetBrains Mono',monospace",
                fontWeight:700, color: tgtPlayer.money < 6 ? "#f87171" : "#4ade80" }}>
                ${tgtPlayer.money}k
              </span>
            </div>
          </div>
          <div style={{ textAlign:"right" }}>
            <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:26,fontWeight:700,
                          color:timeLeft<=5?"#ef4444":timeLeft<=10?"#f59e0b":"#f1f5f9",
                          animation:timeLeft<=5?"pulse 0.6s infinite":"none" }}>{timeLeft}s</div>
            <div style={{ fontSize:9,color:"#64748b",letterSpacing:1 }}>SECONDS LEFT</div>
            <div style={{ fontSize:8,color:"#334155",marginTop:2 }}>Defaults to $2k</div>
          </div>
        </div>
        {/* Timer bar */}
        <div style={{ height:4,background:"#1e293b",borderRadius:3,marginBottom:16,overflow:"hidden" }}>
          <div style={{ height:"100%",width:timerPct+"%",background:timerColor,borderRadius:3,transition:"width 0.25s linear,background 0.5s" }} />
        </div>
        {/* Options */}
        <div style={{ display:"flex",flexDirection:"column",gap:8 }}>
          {options.map(function(opt) {
            return (
              <button key={opt.id}
                disabled={!opt.available}
                onClick={function() { if (opt.available) dispatch({type:"RESOLVE_CHOICE",id:opt.id}); }}
                style={{ display:"flex",justifyContent:"space-between",alignItems:"center",
                         padding:"12px 16px",borderRadius:8,cursor:opt.available?"pointer":"not-allowed",
                         background:opt.available?"#0f172a":"#0a0a0a",
                         border:"2px solid "+opt.border,
                         opacity:opt.available?1:0.35,
                         transition:"all .15s" }}>
                <div style={{ textAlign:"left" }}>
                  <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:13,color:opt.available?opt.color:"#374151",fontWeight:700 }}>{opt.label}</div>
                  <div style={{ fontSize:10,color:"#64748b",marginTop:2 }}>{opt.sub}</div>
                </div>
                <div style={{ textAlign:"right" }}>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:18,color:opt.available?opt.color:"#374151",fontWeight:700 }}>${opt.cost}k</div>
                  {opt.extra && <div style={{ fontSize:9,color:"#64748b",marginTop:1 }}>{opt.extra}</div>}
                </div>
              </button>
            );
          })}
        </div>
      </div>
    </div>
  );
}

/* - Card Play Animation Overlay - */
function CardPlayOverlay({ st, dispatch }) {
  const anim = st.playAnimation;
  const [phase, setPhase] = useState("idle");
  const [latchedAnim, setLatchedAnim] = useState(null); // holds anim through full duration

  var animCardId = anim && anim.card && anim.card.id;
  useEffect(function() {
    if (!animCardId) return;
    var holdMs2 = (anim.card._displayOnly) ? 500 : ANIM_HOLD_MS;
    setLatchedAnim(anim);
    setPhase("in");
    var t1 = setTimeout(function() { setPhase("hold"); }, ANIM_FADE_IN_MS);
    var t2 = setTimeout(function() { setPhase("out"); },  ANIM_FADE_IN_MS + holdMs2);
    var t3 = setTimeout(function() {
      setLatchedAnim(null); setPhase("idle");
      dispatch({ type:"ANIMATION_DONE" });
    }, ANIM_FADE_IN_MS + holdMs2 + ANIM_FADE_OUT_MS);
    // Safety: if ANIMATION_DONE was never processed (e.g. network drop on non-host),
    // force-clear the overlay after 2x the expected duration.
    var safetyMs = (ANIM_FADE_IN_MS + holdMs2 + ANIM_FADE_OUT_MS) * 2;
    var t4 = setTimeout(function() {
      setLatchedAnim(null); setPhase("idle");
    }, safetyMs);
    return function() { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); clearTimeout(t4); };
  // Dep is `anim` (object reference), NOT animCardId (string).
  // The same card can appear in two consecutive playAnimation phases (e.g. PLAY_ACTION then
  // RESUME_ACTION_PLAYED both use the same card id), so animCardId wouldn't change between
  // them and the effect would never re-fire. Each reducer update that sets playAnimation
  // creates a fresh object, so the reference always changes and the effect always fires.
  }, [anim]);

  const displayAnim = latchedAnim || anim;
  if (!displayAnim || phase === "idle") return null;

  var card = displayAnim.card;
  var isAsset = card.type === "ASSET";
  var isDisplayOnly = !!(card && card._displayOnly);

  var playerName = (function() {
    for (var i = 0; i < st.players.length; i++) {
      var p = st.players[i];
      if (p.hand.some(function(c){ return c.id === card.id; })) return p.name;
      if (p.assets.some(function(a){ return a.id === card.id; })) return p.name;
    }
    return curPl(st).name;
  })();

  var accentColor = isAsset ? C.gold : card.type === "REACTION" ? "#a78bfa" : "#60a5fa";

  // Display-only cards (e.g. revealed cards) keep a simple fade
  if (isDisplayOnly) {
    var dpOpacity = phase === "hold" ? 1 : 0;
    var dpTrans = "opacity " + (phase === "in" ? ANIM_FADE_IN_MS : ANIM_FADE_OUT_MS) + "ms ease";
    return (
      <div style={{ position:"fixed", inset:0, zIndex:950, display:"flex", alignItems:"center", justifyContent:"center", pointerEvents:"none" }}>
        <div style={{ position:"absolute", inset:0, background:"rgba(0,0,0,0.18)", opacity:dpOpacity, transition:dpTrans }} />
        <div style={{ position:"relative", display:"flex", flexDirection:"column", alignItems:"center", gap:14, opacity:dpOpacity, transition:dpTrans, transform:"scale(1.1)" }}>
          <CardComp card={card} noImg={false} />
          <div style={{ position:"absolute", bottom:-28, whiteSpace:"nowrap", fontFamily:"'Rubik',sans-serif", fontSize:5, fontWeight:700, color:accentColor, letterSpacing:1 }}>
            Revealed: {card.name}
          </div>
        </div>
      </div>
    );
  }

  // Main play animation — fly up from bottom, float, then fly out
  var cardAnim = phase === "in"  ? "cardFlyUp 380ms cubic-bezier(0.22, 0.8, 0.4, 1) forwards"
               : phase === "hold" ? undefined
               :                    "cardFlyOut 420ms ease forwards";
  var backdropAnim = phase === "in"  ? "overlayFadeIn 300ms ease forwards"
                   : phase === "hold" ? undefined
                   :                    "overlayFadeOut 420ms ease forwards";

  return (
    <div style={{
      position:"fixed", inset:0, zIndex:950,
      display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center",
      pointerEvents:"none",
    }}>
      <div style={{
        position:"absolute", inset:0,
        background:"rgba(0,0,0,0.58)",
        opacity: phase === "hold" ? 1 : undefined,
        animation: backdropAnim,
      }} />
      <div style={{
        position:"relative", display:"flex", flexDirection:"column", alignItems:"center", gap:14,
        animation: cardAnim,
        ...(phase === "hold" ? { transform:"translateY(0) scale(2.4)" } : {}),
      }}>
        <CardComp card={card} noImg={false} />
        <div style={{
          position:"absolute", bottom:-28,
          whiteSpace:"nowrap",
          fontFamily:"'Rubik',sans-serif", fontSize:5, fontWeight:700,
          color:accentColor, letterSpacing:1,
          textShadow:"0 0 14px "+accentColor+"99",
          animation: phase === "in" ? "overlayFadeIn 380ms ease forwards" : undefined,
          opacity: phase === "hold" ? 1 : phase === "out" ? 0 : undefined,
        }}>
          {playerName} plays {card.name}
        </div>
      </div>
    </div>
  );
}







/* - Negative Reaction Select Modal - */
function NegReactionSelectModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "NEG_REACTION_SELECT") return null;
  const [sel, setSel] = useState(null);
  const timerEnd = pc.timerEnd || (Date.now() + 20000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const srcPl = st.players.find(function(p){ return p.id === pc.srcPlayer; });
  const tgtPl = st.players.find(function(p){ return p.id === pc.tgtPlayer; });
  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";
  const nrRemaining = (st.negReactionState ? st.negReactionState.collectQueue.length : 0) + 1;


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #f87171",borderRadius:14,
                    padding:24,maxWidth:520,width:"90%",
                    boxShadow:"0 0 40px #f8717144, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        <div style={{ marginBottom:16 }}>
          <div style={{ display:"flex",justifyContent:"space-between",alignItems:"flex-start" }}>
            <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#f87171",fontFamily:"'Rubik',sans-serif" }}>NEGATIVE REACTION</div>
            {nrRemaining > 1 && <div style={{ fontSize:9,color:"#94a3b8",background:"#1e293b",padding:"2px 7px",borderRadius:10 }}>{nrRemaining} remaining</div>}
          </div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            {tgtPl?.name} - discard a reaction card
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>
            {srcPl?.name} gains $2k for each card discarded.
          </div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time to decide</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>
        <div style={{ height:1,background:"#f8717122",marginBottom:14 }} />
        <div style={{ display:"flex",flexWrap:"wrap",gap:8,justifyContent:"center",marginBottom:16 }}>
          {pc.options.map(function(card) {
            var isSelected = sel === card.id;
            return (
              <div key={card.id} onClick={function(){ setSel(card.id); }}
                style={{ width:100,border:isSelected?"2px solid #f87171":"1px solid #f8717155",
                         borderRadius:8,background:isSelected?"#1f0707":"#160707",
                         padding:"8px 6px",cursor:"pointer",textAlign:"center",
                         boxShadow:isSelected?"0 0 12px #f8717155":"none",
                         transition:"all .12s",userSelect:"none",position:"relative" }}>
                {isSelected && <div style={{ position:"absolute",top:6,right:8,fontSize:12,color:"#f87171",fontWeight:700 }}>✓</div>}
                <div style={{ fontSize:8,color:"#f87171",fontWeight:700,letterSpacing:1,marginBottom:3 }}>REACTION</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{card.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:C.gold,fontWeight:700,marginTop:4 }}>${card.value}k</div>
                <div style={{ fontSize:8,color:"#94a3b8",marginTop:3,lineHeight:1.3 }}>{card.desc}</div>
              </div>
            );
          })}
        </div>
        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#1f0707":"#1e293b",
                   border:"2px solid "+(sel?"#f87171":"#334155"),
                   color:sel?"#fca5a5":"#475569",transition:"all .2s" }}>
          {sel ? "Discard Selected Card" : "Select a Reaction Card..."}
        </button>
      </div>
    </div>
  );
}

/* - Shrinkage Select Modal - */
function ShrinkageSelectModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "SHRINKAGE_SELECT") return null;
  const [sel, setSel] = useState(null);
  const timerEnd = pc.timerEnd || (Date.now() + 20000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const tgtPl = st.players.find(function(p){ return p.id === pc.tgtPlayer; });
  const srcPl = st.players.find(function(p){ return p.id === pc.srcPlayer; });
  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";
  const selCard = sel ? pc.options.find(function(c){ return c.id === sel; }) : null;


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #ef4444",borderRadius:14,
                    padding:24,maxWidth:520,width:"90%",maxHeight:"85vh",overflowY:"auto",
                    boxShadow:"0 0 40px #ef444444, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        <div style={{ marginBottom:16 }}>
          <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#ef4444",fontFamily:"'Rubik',sans-serif" }}>SHRINKAGE</div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            {tgtPl?.name} - choose a card to discard
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>
            Forced by {srcPl?.name}'s Shrinkage.
          </div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time remaining</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>
        <div style={{ height:1,background:"#ef444422",marginBottom:14 }} />
        <div style={{ display:"flex",flexWrap:"wrap",gap:8,marginBottom:14 }}>
          {pc.options.map(function(card) {
            var isSelected = sel === card.id;
            var typeCol = card.type === CT.ACTION ? "#60a5fa" : card.type === CT.REACTION ? "#f87171" : "#4ade80";
            return (
              <div key={card.id} onClick={function(){ setSel(card.id); }}
                style={{ width:110,border:isSelected?"2px solid #ef4444":"1px solid "+typeCol+"55",
                         borderRadius:8,background:isSelected?"#1f0707":"#1e293b",
                         padding:"10px 8px",cursor:"pointer",textAlign:"center",
                         boxShadow:isSelected?"0 0 14px #ef444455":"none",
                         transition:"all .12s",userSelect:"none",position:"relative" }}>
                {isSelected && <div style={{ position:"absolute",top:5,right:7,fontSize:11,color:"#ef4444",fontWeight:700 }}>✓</div>}
                <div style={{ fontSize:8,color:typeCol,fontWeight:700,letterSpacing:1,marginBottom:3 }}>{card.type}</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{card.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:C.gold,fontWeight:700,marginTop:4 }}>${card.value}k</div>
              </div>
            );
          })}
        </div>
        {selCard && (
          <div style={{ marginBottom:12,padding:"8px 12px",background:"#1f0707",borderRadius:6,border:"1px solid #ef444444",fontSize:11,color:"#fca5a5" }}>
            Discarding <strong>{selCard.name}</strong>
          </div>
        )}
        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#1f0707":"#1e293b",
                   border:"2px solid "+(sel?"#ef4444":"#334155"),
                   color:sel?"#fca5a5":"#475569",transition:"all .2s" }}>
          {sel ? "Discard - " + selCard?.name : "Select a Card to Discard..."}
        </button>
      </div>
    </div>
  );
}

/* - Outside Hire Select Modal - */
function OutsideHireSelectModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "OUTSIDE_HIRE_SELECT") return null;
  const [sel, setSel] = useState(null);
  const timerEnd = pc.timerEnd || (Date.now() + 20000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const srcPl = st.players.find(function(p){ return p.id === pc.srcPlayer; });
  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";

  const byOwner = {};
  pc.options.forEach(function(a) {
    if (!byOwner[a.ownerId]) byOwner[a.ownerId] = { name:a.ownerName, assets:[] };
    byOwner[a.ownerId].assets.push(a);
  });
  const selAsset = sel ? pc.options.find(function(a){ return a.id === sel; }) : null;


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #a78bfa",borderRadius:14,
                    padding:24,maxWidth:580,width:"90%",maxHeight:"85vh",overflowY:"auto",
                    boxShadow:"0 0 40px #a78bfa44, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        <div style={{ marginBottom:16 }}>
          <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#a78bfa",fontFamily:"'Rubik',sans-serif" }}>OUTSIDE HIRE</div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            {srcPl?.name} - choose an asset to steal
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>You will pay the asset's current value. Only players with 2+ assets are shown.</div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time to decide</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>
        <div style={{ height:1,background:"#a78bfa22",marginBottom:14 }} />
        {Object.entries(byOwner).map(function([pid, group]) {
          return (
            <div key={pid} style={{ marginBottom:16 }}>
              <div style={{ fontSize:10,color:"#a78bfa",fontWeight:700,letterSpacing:1,marginBottom:8 }}>{group.name}</div>
              <div style={{ display:"flex",flexWrap:"wrap",gap:8 }}>
                {group.assets.map(function(asset) {
                  var isSelected = sel === asset.id;
                  var subUI = { PASSIVE:{col:"#86efac",bg:"#052e16"}, DURING:{col:"#4ade80",bg:"#0a1a0a"},
                                ANYTIME:{col:"#34d399",bg:"#0a1f14"}, ONCE:{col:"#a3e635",bg:"#0d1a05"} };
                  var ui = subUI[asset.sub] || {col:"#4ade80",bg:"#0a1a0a"};
                  var statusCol = asset.status === "USED" ? "#ef4444" : asset.disabled ? "#f97316" : "#4ade80";
                  var statusLbl = asset.status === "USED" ? "USED" : asset.disabled ? "DISABLED" : "READY";
                  return (
                    <div key={asset.id} onClick={function(){ setSel(asset.id); }}
                      style={{ width:116,border:isSelected?"2px solid #a78bfa":"1px solid "+ui.col+"55",
                               borderRadius:8,background:isSelected?"#150a2a":ui.bg,
                               padding:"10px 8px",cursor:"pointer",textAlign:"center",
                               boxShadow:isSelected?"0 0 14px #a78bfa55":"none",
                               transition:"all .12s",userSelect:"none",position:"relative" }}>
                      {isSelected && <div style={{ position:"absolute",top:5,right:7,fontSize:11,color:"#a78bfa",fontWeight:700 }}>✓</div>}
                      <div style={{ fontSize:8,color:ui.col,fontWeight:700,letterSpacing:1,marginBottom:3 }}>{asset.sub||"ASSET"}</div>
                      <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{asset.name}</div>
                      <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:C.gold,fontWeight:700,marginTop:4 }}>${asset.value}k</div>
                      <div style={{ fontSize:8,color:statusCol,fontWeight:700,marginTop:3,letterSpacing:1 }}>{statusLbl}</div>
                      {asset.tokens && asset.tokens.length > 0 && (
                        <div style={{ fontSize:8,color:"#94a3b8",marginTop:2 }}>{asset.tokens.length} token{asset.tokens.length>1?"s":""}</div>
                      )}
                      <div style={{ fontSize:8,color:"#64748b",marginTop:4,lineHeight:1.4,textAlign:"left" }}>{getDesc(asset)}</div>
                      {isSelected && <div style={{ fontSize:9,color:"#f87171",fontWeight:700,marginTop:3 }}>cost: -${asset.value}k</div>}
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })}
        <div style={{ height:1,background:"#a78bfa22",marginBottom:14 }} />
        {selAsset && (
          <div style={{ marginBottom:12,padding:"8px 12px",background:"#150a2a",borderRadius:6,border:"1px solid #a78bfa44",fontSize:11,color:"#c4b5fd" }}>
            Stealing <strong>{selAsset.name}</strong> from {selAsset.ownerName} - you will pay <span style={{ color:"#f87171",fontWeight:700 }}>${selAsset.value}k</span>
          </div>
        )}
        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#150a2a":"#1e293b",
                   border:"2px solid "+(sel?"#a78bfa":"#334155"),
                   color:sel?"#c4b5fd":"#475569",transition:"all .2s" }}>
          {sel ? "Confirm Hire - Pay $" + (selAsset?.value||"?") + "k" : "Select an Asset First..."}
        </button>
      </div>
    </div>
  );
}

/* - Give Back Tie Modal - */
function GiveBackTieModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "GIVE_BACK_TIE") return null;
  const [sel, setSel] = useState(null);
  const timerEnd = pc.timerEnd || (Date.now() + 20000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const srcPl = st.players.find(function(p){ return p.id === pc.srcPlayer; });
  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #fbbf24",borderRadius:14,
                    padding:24,maxWidth:480,width:"90%",
                    boxShadow:"0 0 40px #fbbf2444, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        <div style={{ marginBottom:16 }}>
          <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#fbbf24",fontFamily:"'Rubik',sans-serif" }}>💸 GIVE BACK - TIE</div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            {srcPl?.name} - choose who pays you $4k
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>These players are tied for the highest amount of money.</div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time to decide</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>
        <div style={{ height:1,background:"#fbbf2422",marginBottom:14 }} />
        <div style={{ display:"flex",flexDirection:"column",gap:8,marginBottom:16 }}>
          {pc.options.map(function(player) {
            var isSelected = sel === player.id;
            return (
              <div key={player.id} onClick={function(){ setSel(player.id); }}
                style={{ display:"flex",alignItems:"center",gap:12,padding:"10px 14px",
                         border:isSelected?"2px solid #fbbf24":"1px solid #334155",
                         borderRadius:8,background:isSelected?"#1c1200":"#161616",
                         cursor:"pointer",transition:"all .12s",userSelect:"none" }}>
                <div style={{ width:12,height:12,borderRadius:"50%",background:player.color,flexShrink:0 }} />
                <div style={{ flex:1 }}>
                  <span style={{ fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",fontSize:13 }}>{player.name}</span>
                  <span style={{ fontFamily:"'JetBrains Mono',monospace",color:C.gold,fontSize:12,marginLeft:10 }}>${player.money}k</span>
                </div>
                {isSelected && <div style={{ fontSize:12,color:"#fbbf24",fontWeight:700 }}>✓ -$4k</div>}
              </div>
            );
          })}
        </div>
        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#1c1200":"#1e293b",
                   border:"2px solid "+(sel?"#fbbf24":"#334155"),
                   color:sel?"#fde68a":"#475569",transition:"all .2s" }}>
          {sel ? "Collect $4k" : "Select a Player First..."}
        </button>
      </div>
    </div>
  );
}


/* - Upgrade Discard Modal - */
function UpgradeDiscardModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "UPGRADE_DISCARD") return null;
  const { useState, useEffect } = window.React || React;
  const [sel, setSel] = useState(null);
  const timerEnd = pc.timerEnd || (Date.now() + 20000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";
  const subUI = { PASSIVE:{col:"#a78bfa",bg:"#130c1e"}, DURING:{col:"#60a5fa",bg:"#0a1220"},
                  ANYTIME:{col:"#34d399",bg:"#0a1f14"}, ONCE:{col:"#fdba74",bg:"#1c0e03"} };


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #f87171",borderRadius:14,
                    padding:24,maxWidth:540,width:"90%",
                    boxShadow:"0 0 40px #f8717144, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        <div style={{ marginBottom:16 }}>
          <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#f87171",fontFamily:"'Rubik',sans-serif" }}>UPGRADE — DESTROY ASSET</div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            Choose an asset to destroy
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>The selected asset will be permanently removed. You'll then pick a free replacement from the shop.</div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time to decide</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>
        <div style={{ height:1,background:"#f8717122",marginBottom:14 }} />
        <div style={{ display:"flex",flexWrap:"wrap",gap:10,justifyContent:"center",marginBottom:16 }}>
          {(pc.options||[]).map(function(asset) {
            var isSelected = sel === asset.id;
            var ui = subUI[asset.sub] || {col:"#94a3b8",bg:"#1e293b"};
            return (
              <div key={asset.id} onClick={function(){ setSel(asset.id); }}
                style={{ width:112,border:isSelected?"2px solid #f87171":"1px solid "+ui.col+"55",
                         borderRadius:8,background:isSelected?"#2a0a0a":ui.bg,
                         padding:"10px 8px",cursor:"pointer",
                         boxShadow:isSelected?"0 0 16px #f8717155":"none",
                         transition:"all .12s",textAlign:"center",userSelect:"none",position:"relative" }}>
                {isSelected && <div style={{ position:"absolute",top:6,right:8,fontSize:12,color:"#f87171",fontWeight:700 }}>✕</div>}
                <div style={{ fontSize:9,color:ui.col,fontWeight:700,letterSpacing:1,marginBottom:3 }}>{asset.sub||"ASSET"}</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:12,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{asset.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:"#f59e0b",fontWeight:700,marginTop:5 }}>${asset.value}k</div>
                {isSelected && <div style={{ fontSize:9,color:"#f87171",fontWeight:700,marginTop:3 }}>→ DESTROYED</div>}
              </div>
            );
          })}
        </div>
        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#2a0a0a":"#1e293b",
                   border:"2px solid "+(sel?"#f87171":"#334155"),
                   color:sel?"#fca5a5":"#475569",transition:"all .2s" }}>
          {sel ? "Destroy Asset" : "Select an Asset First..."}
        </button>
      </div>
    </div>
  );
}

/* - Upgrade Gain Modal - */
function UpgradeGainModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "UPGRADE_GAIN") return null;
  const { useState, useEffect } = window.React || React;
  const [sel, setSel] = useState(null);
  const timerEnd = pc.timerEnd || (Date.now() + 20000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";
  const subUI = { PASSIVE:{col:"#a78bfa",bg:"#130c1e"}, DURING:{col:"#60a5fa",bg:"#0a1220"},
                  ANYTIME:{col:"#34d399",bg:"#0a1f14"}, ONCE:{col:"#fdba74",bg:"#1c0e03"} };


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #4ade80",borderRadius:14,
                    padding:24,maxWidth:600,width:"92%",
                    boxShadow:"0 0 40px #4ade8044, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        <div style={{ marginBottom:16 }}>
          <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#4ade80",fontFamily:"'Rubik',sans-serif" }}>🆙 UPGRADE - CLAIM REPLACEMENT</div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            Choose a shop asset to take for free
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>Select any asset currently in the shop. It's yours at no cost.</div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time to decide</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>
        <div style={{ height:1,background:"#4ade8022",marginBottom:14 }} />
        <div style={{ display:"flex",flexWrap:"wrap",gap:10,justifyContent:"center",marginBottom:16 }}>
          {(pc.options||[]).map(function(asset) {
            var isSelected = sel === asset.id;
            var ui = subUI[asset.sub] || {col:"#94a3b8",bg:"#1e293b"};
            return (
              <div key={asset.id} onClick={function(){ setSel(asset.id); }}
                style={{ width:118,border:isSelected?"2px solid #4ade80":"1px solid "+ui.col+"55",
                         borderRadius:8,background:isSelected?"#0a2015":ui.bg,
                         padding:"10px 8px",cursor:"pointer",
                         boxShadow:isSelected?"0 0 16px #4ade8055":"none",
                         transition:"all .12s",textAlign:"center",userSelect:"none",position:"relative" }}>
                {isSelected && <div style={{ position:"absolute",top:6,right:8,fontSize:12,color:"#4ade80",fontWeight:700 }}>✓</div>}
                <div style={{ fontSize:9,color:ui.col,fontWeight:700,letterSpacing:1,marginBottom:3 }}>{asset.sub||"ASSET"}</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:12,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{asset.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:"#f59e0b",fontWeight:700,marginTop:5 }}>${asset.value}k</div>
                <div style={{ fontSize:9,color:"#94a3b8",marginTop:3 }}>FREE</div>
                {isSelected && <div style={{ fontSize:9,color:"#4ade80",fontWeight:700,marginTop:2 }}>→ YOURS</div>}
              </div>
            );
          })}
        </div>
        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#0a2015":"#1e293b",
                   border:"2px solid "+(sel?"#4ade80":"#334155"),
                   color:sel?"#6ee7b7":"#475569",transition:"all .2s" }}>
          {sel ? "Claim Asset for Free" : "Select an Asset First..."}
        </button>
      </div>
    </div>
  );
}


/* - Out of Order Select Modal - */
function OutOfOrderSelectModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "OUT_OF_ORDER_SELECT") return null;
  const timerEnd = pc.timerEnd || (Date.now() + 25000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));
  const [selOpp, setSelOpp] = useState(pc.selectedOpponent || null);

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const timerPct = (timeLeft / 25) * 100;
  const timerCol = timeLeft > 12 ? "#4ade80" : timeLeft > 6 ? "#facc15" : "#f87171";
  const subUI = { PASSIVE:{col:"#a78bfa",bg:"#130c1e"}, DURING:{col:"#60a5fa",bg:"#0a1220"},
                  ANYTIME:{col:"#34d399",bg:"#0a1f14"}, ONCE:{col:"#fdba74",bg:"#1c0e03"} };

  const isAssetPhase = pc.phase === "asset";

  // Timer bar
  const timerBar = (
    <div style={{ marginBottom:14 }}>
      <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
        <span>Time to decide</span>
        <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
      </div>
      <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
        <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
      </div>
    </div>
  );


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #fbbf24",borderRadius:14,
                    padding:24,maxWidth:560,width:"92%",
                    boxShadow:"0 0 40px #fbbf2444, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>

        {/* Header */}
        <div style={{ marginBottom:16 }}>
          <div style={{ display:"flex",alignItems:"center",gap:8,marginBottom:4 }}>
            {isAssetPhase && (
              <button onClick={function(){
                dispatch({ type:"RESOLVE_CHOICE_PHASE_BACK" });
              }} style={{ background:"#2a1f0a",border:"1px solid #fbbf2466",borderRadius:6,
                          color:"#fbbf24",fontSize:11,padding:"3px 8px",cursor:"pointer",fontWeight:700 }}>
                ← Back
              </button>
            )}
            <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#fbbf24",fontFamily:"'Rubik',sans-serif" }}>
              🚧 OUT OF ORDER - {isAssetPhase ? "SELECT ASSET" : "SELECT OPPONENT"}
            </div>
          </div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            {isAssetPhase
              ? "Choose an asset to disable (" + pc.selectedOpponentName + ")"
              : "Choose which opponent to target"}
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>
            {isAssetPhase
              ? "The chosen asset will be disabled until its owner pays you $5k."
              : "Only opponents with at least one available asset are shown."}
          </div>
        </div>

        {timerBar}
        <div style={{ height:1,background:"#fbbf2422",marginBottom:14 }} />

        {/* Opponent selection phase */}
        {!isAssetPhase && (
          <div style={{ display:"flex",flexWrap:"wrap",gap:10,justifyContent:"center",marginBottom:16 }}>
            {(pc.opponents||[]).map(function(opp) {
              var isSelected = selOpp === opp.id;
              return (
                <div key={opp.id}
                  onClick={function(){ setSelOpp(opp.id); }}
                  style={{ minWidth:120,border:isSelected?"2px solid #fbbf24":"1px solid #fbbf2444",
                           borderRadius:8,background:isSelected?"#2a1f0a":"#1e1a14",
                           padding:"12px 16px",cursor:"pointer",textAlign:"center",
                           boxShadow:isSelected?"0 0 16px #fbbf2455":"none",
                           transition:"all .12s",userSelect:"none",position:"relative" }}>
                  {isSelected && <div style={{ position:"absolute",top:6,right:8,fontSize:12,color:"#fbbf24",fontWeight:700 }}>✓</div>}
                  <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:14,color:"#f1f5f9",fontWeight:700 }}>{opp.name}</div>
                </div>
              );
            })}
          </div>
        )}
        {!isAssetPhase && (
          <button onClick={function(){ if (selOpp) dispatch({type:"RESOLVE_CHOICE",id:selOpp}); }}
            disabled={!selOpp}
            style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                     cursor:selOpp?"pointer":"not-allowed",borderRadius:8,
                     background:selOpp?"#2a1f0a":"#1e293b",
                     border:"2px solid "+(selOpp?"#fbbf24":"#334155"),
                     color:selOpp?"#fde68a":"#475569",transition:"all .2s" }}>
            {selOpp ? "Select Opponent ->" : "Choose an Opponent First..."}
          </button>
        )}

        {/* Asset selection phase */}
        {isAssetPhase && (
          <div style={{ display:"flex",flexWrap:"wrap",gap:10,justifyContent:"center",marginBottom:16 }}>
            {(pc.assets||[]).map(function(asset) {
              var ui = subUI[asset.sub] || {col:"#94a3b8",bg:"#1e293b"};
              return (
                <AssetPickCard key={asset.id} asset={asset} ui={ui}
                  onSelect={function(id){ dispatch({type:"RESOLVE_CHOICE",id:id}); }} />
              );
            })}
          </div>
        )}
      </div>
    </div>
  );
}

// Small stateful helper so each asset card can track its own hover
function AssetPickCard({ asset, ui, onSelect }) {
  const desc = typeof asset.descFn === "function" ? asset.descFn(asset) : (asset.desc||"");
  return (
    <div onClick={function(){ onSelect(asset.id); }}
      style={{ width:118,border:"1px solid "+ui.col+"55",borderRadius:8,background:ui.bg,
               padding:"10px 8px",cursor:"pointer",textAlign:"center",
               transition:"all .12s",userSelect:"none",position:"relative" }}>
      <div style={{ fontSize:9,color:ui.col,fontWeight:700,letterSpacing:1,marginBottom:3 }}>{asset.sub||"ASSET"}</div>
      <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:12,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{asset.name}</div>
      <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:"#f59e0b",fontWeight:700,marginTop:5 }}>${asset.value}k</div>
      <div style={{ fontSize:8,color:"#94a3b8",marginTop:4,lineHeight:1.4 }}>{desc.slice(0,50)}{desc.length>50?"...":""}</div>
    </div>
  );
}

/* - Refresh Asset Modal - */
function RefreshAssetModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "REFRESH_ASSET") return null;
  const [sel, setSel] = useState(null);
  const timerEnd = pc.timerEnd || (Date.now() + 20000);
  const [timeLeft, setTimeLeft] = useState(Math.max(0, Math.ceil((timerEnd - Date.now()) / 1000)));

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0, n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #34d399",borderRadius:14,
                    padding:24,maxWidth:520,width:"90%",
                    boxShadow:"0 0 40px #34d39944, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        <div style={{ marginBottom:16 }}>
          <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#34d399",fontFamily:"'Rubik',sans-serif" }}>REFRESH</div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            Choose a used asset to reset
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>The selected asset will return to READY and can be used again.</div>
        </div>
        <div style={{ marginBottom:14 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time to decide</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>
        <div style={{ height:1,background:"#34d39922",marginBottom:14 }} />
        <div style={{ display:"flex",flexWrap:"wrap",gap:10,justifyContent:"center",marginBottom:16 }}>
          {pc.options.map(function(asset) {
            var isSelected = sel === asset.id;
            var subUI = { PASSIVE:{col:"#a78bfa",bg:"#130c1e"}, DURING:{col:"#60a5fa",bg:"#0a1220"},
                          ANYTIME:{col:"#34d399",bg:"#0a1f14"}, ONCE:{col:"#fdba74",bg:"#1c0e03"} };
            var ui = subUI[asset.sub] || {col:"#94a3b8",bg:"#1e293b"};
            return (
              <div key={asset.id} onClick={function(){ setSel(asset.id); }}
                style={{ width:112,border:isSelected?"2px solid #34d399":"1px solid "+ui.col+"55",
                         borderRadius:8,background:isSelected?"#0a2015":ui.bg,
                         padding:"10px 8px",cursor:"pointer",
                         boxShadow:isSelected?"0 0 16px #34d39955":"none",
                         transition:"all .12s",textAlign:"center",userSelect:"none",position:"relative" }}>
                {isSelected && <div style={{ position:"absolute",top:6,right:8,fontSize:12,color:"#34d399",fontWeight:700 }}>✓</div>}
                <div style={{ fontSize:9,color:ui.col,fontWeight:700,letterSpacing:1,marginBottom:3 }}>{asset.sub||"ASSET"}</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:12,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{asset.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:"#f59e0b",fontWeight:700,marginTop:5 }}>${asset.value}k</div>
                <div style={{ fontSize:9,color:"#f87171",fontWeight:700,marginTop:4,letterSpacing:1 }}>USED</div>
                {isSelected && <div style={{ fontSize:9,color:"#34d399",fontWeight:700,marginTop:3 }}>→ READY</div>}
              </div>
            );
          })}
        </div>
        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#0a2015":"#1e293b",
                   border:"2px solid "+(sel?"#34d399":"#334155"),
                   color:sel?"#6ee7b7":"#475569",transition:"all .2s" }}>
          {sel ? "Reset Asset to READY" : "Select an Asset First..."}
        </button>
      </div>
    </div>
  );
}

/* - Let Go Select Modal - */
/* - Interviews Modal - */
function InterviewsModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "INTERVIEWS_PICK") return null;
  const [sel, setSel] = useState(null);
  const [timeLeft, setTimeLeft] = useState(20);

  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return n-1; }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);

  const srcPl = st.players.find(p => p.id===pc.srcPlayer);
  const timerPct = (timeLeft / 20) * 100;
  const timerCol = timeLeft > 10 ? "#4ade80" : timeLeft > 5 ? "#facc15" : "#f87171";


  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:800,
                  backdropFilter:"blur(3px)",animation:"fadeBackdrop .18s ease" }}>
      <div style={{ background:"#1a1410",border:"2px solid #60a5fa",borderRadius:14,
                    padding:24,maxWidth:540,width:"90%",
                    boxShadow:"0 0 40px #60a5fa44, 0 24px 60px #000000cc",
                    animation:"slideUp .22s cubic-bezier(.22,.8,.36,1)" }}>
        {/* Header */}
        <div style={{ marginBottom:16 }}>
          <div style={{ fontSize:10,letterSpacing:2,fontWeight:700,color:"#60a5fa",fontFamily:"'Rubik',sans-serif" }}>
            🎤 INTERVIEWS
          </div>
          <div style={{ fontSize:17,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginTop:4 }}>
            {srcPl?.name} - keep 1 card
          </div>
          <div style={{ fontSize:11,color:"#94a3b8",marginTop:4 }}>
            The other {pc.options.length > 1 ? pc.options.length - 1 : 0} card{pc.options.length > 2 ? "s go" : " goes"} to the bottom of the deck in a random order.
          </div>
        </div>

        {/* Timer */}
        <div style={{ marginBottom:16 }}>
          <div style={{ display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4 }}>
            <span>Time to decide</span>
            <span style={{ fontFamily:"'JetBrains Mono',monospace",fontWeight:700 }}>{timeLeft}s</span>
          </div>
          <div style={{ height:4,background:"#1e293b",borderRadius:2,overflow:"hidden" }}>
            <div style={{ height:"100%",width:timerPct+"%",background:timerCol,borderRadius:2,transition:"width 1s linear,background .5s" }} />
          </div>
        </div>

        <div style={{ height:1,background:"#60a5fa22",marginBottom:14 }} />

        {/* Card choices */}
        <div style={{ display:"flex",gap:10,justifyContent:"center",flexWrap:"wrap",marginBottom:16 }}>
          {pc.options.map(function(card) {
            var isSelected = sel === card.id;
            var typeUI = { ACTION:{col:"#93c5fd",bg:"#0a1220"}, REACTION:{col:"#f87171",bg:"#1f0a0a"}, ASSET:{col:"#4ade80",bg:"#0a1f0a"} };
            var ui = typeUI[card.type] || {col:"#94a3b8",bg:"#1e293b"};
            return (
              <div key={card.id} onClick={function(){ setSel(card.id); }}
                style={{ width:108,border:isSelected?"2px solid #60a5fa":"1px solid "+ui.col+"55",
                         borderRadius:8,background:isSelected?"#0c1e3b":ui.bg,
                         padding:"10px 8px",cursor:"pointer",
                         boxShadow:isSelected?"0 0 16px #60a5fa55":"none",
                         transition:"all .12s",textAlign:"center",userSelect:"none",position:"relative" }}>
                {isSelected && (
                  <div style={{ position:"absolute",top:6,right:8,fontSize:13,color:"#60a5fa",fontWeight:700 }}>✓</div>
                )}
                <div style={{ fontSize:9,color:ui.col,fontWeight:700,marginBottom:3,letterSpacing:1 }}>{card.type}</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:12,color:"#f1f5f9",fontWeight:700,lineHeight:1.3 }}>{card.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:14,color:"#f59e0b",fontWeight:700,marginTop:5 }}>${card.value}k</div>
                <div style={{ fontSize:8,color:"#64748b",marginTop:4,lineHeight:1.35 }}>{getDesc(card)}</div>
              </div>
            );
          })}
        </div>

        <button onClick={function(){ if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
          disabled={!sel}
          style={{ width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                   cursor:sel?"pointer":"not-allowed",borderRadius:8,
                   background:sel?"#0c1e3b":"#1e293b",
                   border:"2px solid "+(sel?"#60a5fa":"#334155"),
                   color:sel?"#93c5fd":"#475569",transition:"all .2s" }}>
          {sel ? "Keep This Card" : "Select a Card First..."}
        </button>
      </div>
    </div>
  );
}

/* - Help Wanted Modal - */

/* - Pocket Change Select Modal - */
function PocketChangeSelectModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "POCKET_CHANGE_SELECT") return null;
  const [sel, setSel] = useState(null);
  const [timeLeft, setTimeLeft] = useState(20);

  useEffect(function() { setSel(null); }, [pc && pc.timerEnd]);

  useEffect(function() {
    if (!pc || pc.type !== "POCKET_CHANGE_SELECT") return;
    function tick() {
      var left = Math.max(0, Math.ceil((pc.timerEnd - Date.now()) / 1000));
      setTimeLeft(left);
      if (left <= 0) dispatch({ type:"RESOLVE_CHOICE", id:"auto" });
    }
    tick();
    var iv = setInterval(tick, 250);
    return function() { clearInterval(iv); };
  }, [pc && pc.timerEnd]);


  var tgtPlayer = st.players.find(function(p) { return p.id === pc.tgtPlayer; });
  var owner = st.players.find(function(p) { return p.id === pc.srcPlayer; });
  if (!tgtPlayer) return null;

  var timerPct  = Math.max(0, (pc.timerEnd - Date.now()) / 20000 * 100);
  var timerColor = timerPct > 50 ? "#22c55e" : timerPct > 25 ? "#f59e0b" : "#ef4444";

  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.8)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:300 }}>
      <div style={{ background:"#0f172a",border:"2px solid #7c3aed",borderRadius:14,padding:20,width:520,maxWidth:"95vw",boxShadow:"0 0 40px #7c3aed33" }}>
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:8 }}>
          <div>
            <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:15,color:"#c4b5fd",fontWeight:700,letterSpacing:1 }}>💸 Pocket Change</div>
            <div style={{ fontSize:12,color:"#94a3b8",marginTop:3 }}>
              <span style={{ color:tgtPlayer.color||"#c4b5fd",fontWeight:600 }}>{tgtPlayer.name}</span>: {pc.prompt}
            </div>
          </div>
          <div style={{ textAlign:"right" }}>
            <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:26,fontWeight:700,
                          color:timeLeft<=5?"#ef4444":timeLeft<=10?"#f59e0b":"#f1f5f9",
                          animation:timeLeft<=5?"pulse 0.6s infinite":"none" }}>{timeLeft}s</div>
            <div style={{ fontSize:9,color:"#64748b",letterSpacing:1 }}>SECONDS LEFT</div>
          </div>
        </div>
        <div style={{ height:5,background:"#1e293b",borderRadius:3,marginBottom:14,overflow:"hidden" }}>
          <div style={{ height:"100%",width:timerPct+"%",background:timerColor,borderRadius:3,transition:"width 0.25s linear,background 0.5s" }} />
        </div>
        <div style={{ display:"flex",flexWrap:"wrap",gap:8,maxHeight:280,overflowY:"auto",justifyContent:"center",marginBottom:14 }}>
          {tgtPlayer.hand.map(function(card) {
            var isSelected = sel === card.id;
            var typeUI = { ACTION:{col:"#93c5fd",bg:"#0a1220"}, REACTION:{col:"#f87171",bg:"#1f0a0a"}, ASSET:{col:"#4ade80",bg:"#0a1f0a"} };
            var ui = typeUI[card.type] || {col:"#94a3b8",bg:"#1e293b"};
            return (
              <div key={card.id} onClick={function() { setSel(card.id); }}
                style={{ width:100,border:isSelected?"2px solid #7c3aed":"1px solid "+ui.col+"66",borderRadius:8,
                         background:isSelected?"#2e1065":ui.bg,padding:"8px 6px",cursor:"pointer",
                         boxShadow:isSelected?"0 0 14px #7c3aed55":"none",transition:"all .12s",textAlign:"center",userSelect:"none" }}>
                {isSelected && <div style={{ fontSize:16,marginBottom:2 }}>✓</div>}
                <div style={{ fontSize:10,color:ui.col,fontWeight:700 }}>{card.type}</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:600,marginTop:3,lineHeight:1.3 }}>{card.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:"#f59e0b",fontWeight:700,marginTop:4 }}>${card.value}k</div>
                <div style={{ fontSize:8,color:"#64748b",marginTop:3,lineHeight:1.3,overflow:"hidden",textOverflow:"ellipsis" }}>{getDesc(card)}</div>
              </div>
            );
          })}
        </div>
        <div style={{ display:"flex",gap:8 }}>
          <button onClick={function() { dispatch({type:"RESOLVE_CHOICE",id:"auto"}); }}
            style={{ flex:"0 0 auto",background:"#1e293b",border:"1px solid #475569",color:"#94a3b8",borderRadius:6,padding:"8px 14px",cursor:"pointer",fontSize:11,fontWeight:600 }}>
            ↻ Random
          </button>
          <button onClick={function() { if (sel) dispatch({type:"RESOLVE_CHOICE",id:sel}); }}
            disabled={!sel}
            style={{ flex:1,padding:"10px 0",fontSize:13,fontWeight:700,letterSpacing:1,
                     cursor:sel?"pointer":"not-allowed",borderRadius:8,
                     background:sel?"#2e1065":"#1e293b",
                     border:"2px solid "+(sel?"#7c3aed":"#334155"),
                     color:sel?"#c4b5fd":"#475569",transition:"all .2s" }}>
            {sel ? "Confirm Selection" : "Select a Card"}
          </button>
        </div>
      </div>
    </div>
  );
}

/* - Pocket Change Reveal Modal - */
function PocketChangeRevealModal({ st, dispatch }) {
  const pc = st.pendingChoice;
  if (!pc || pc.type !== "POCKET_CHANGE_REVEAL") return null;
  const [timeLeft, setTimeLeft] = useState(5);

  useEffect(function() {
    if (!pc || pc.type !== "POCKET_CHANGE_REVEAL") return;
    function tick() {
      var left = Math.max(0, Math.ceil((pc.timerEnd - Date.now()) / 1000));
      setTimeLeft(left);
      if (left <= 0) dispatch({ type:"RESOLVE_CHOICE", id:"auto" });
    }
    tick();
    var iv = setInterval(tick, 250);
    return function() { clearInterval(iv); };
  }, [pc && pc.timerEnd]);


  var pcs = st.pocketChangeState;
  var cards = pc.cards || [];
  var total = cards.reduce(function(s, c) { return s + (c ? c.value : 0); }, 0);
  var owner = st.players.find(function(p) { return p.id === pc.srcPlayer; });

  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.8)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:300 }}>
      <div style={{ background:"#0f172a",border:"2px solid #f59e0b",borderRadius:14,padding:20,width:500,maxWidth:"95vw",boxShadow:"0 0 40px #f59e0b44",textAlign:"center" }}>
        <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:15,color:C.gold,fontWeight:700,marginBottom:4 }}>💸 Cards Revealed</div>
        <div style={{ fontSize:12,color:"#94a3b8",marginBottom:12 }}>{pc.prompt}</div>
        <div style={{ display:"flex",flexWrap:"wrap",gap:8,justifyContent:"center",marginBottom:14 }}>
          {(pcs && pcs.selections || []).map(function(sel, idx) {
            if (!sel || !sel.card) return null;
            var tgtPl = st.players.find(function(p) { return p.id === sel.pid; });
            var typeUI = { ACTION:{col:"#93c5fd",bg:"#0a1220"}, REACTION:{col:"#f87171",bg:"#1f0a0a"}, ASSET:{col:"#4ade80",bg:"#0a1f0a"} };
            var ui = typeUI[sel.card.type] || {col:"#94a3b8",bg:"#1e293b"};
            return (
              <div key={idx} style={{ width:110,borderRadius:8,border:"1px solid "+ui.col+"88",background:ui.bg,padding:"8px 6px",textAlign:"center" }}>
                <div style={{ fontSize:9,color:"#94a3b8",marginBottom:2 }}>{tgtPl && tgtPl.name}</div>
                <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:600 }}>{sel.card.name}</div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:14,color:C.gold,fontWeight:700,marginTop:4 }}>${sel.card.value}k</div>
              </div>
            );
          })}
        </div>
        <div style={{ fontSize:18,fontWeight:700,color:"#4ade80",marginBottom:8 }}>
          Total: <span style={{ fontFamily:"'JetBrains Mono',monospace" }}>${total}k</span>
          {owner && <span style={{ fontSize:13,color:"#94a3b8",fontWeight:400 }}> → {owner.name}</span>}
        </div>
        <div style={{ fontSize:11,color:"#64748b" }}>Auto-closing in {timeLeft}s...</div>
      </div>
    </div>
  );
}







/* - Negative Reaction Select Modal - */

/* - Generic timed-choice modal factory - */
function mkTimedModal(type, title, color, renderContent) {
  return function({ st, dispatch }) {
    var pc = st.pendingChoice;
    if (!pc||pc.type!==type) return null;
    var [timeLeft, setTimeLeft] = useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
    useEffect(function(){ setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000))); }, [pc.timerEnd]);
    useEffect(function(){
      if (timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}
      var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);
      return function(){clearTimeout(t);};
    },[timeLeft]);
    var pct=(timeLeft/20)*100, col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

    return (
      <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
        <div style={{background:"#1a1410",border:"2px solid "+color,borderRadius:14,padding:24,maxWidth:540,width:"92%",boxShadow:"0 0 40px "+color+"33"}}>
          <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:color,fontFamily:"'Rubik',sans-serif",marginBottom:6}}>{title}</div>
          <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:col,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
          <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:pct+"%",background:col,borderRadius:2,transition:"width 1s linear"}}/></div>
          {renderContent(pc, dispatch, st)}
        </div>
      </div>
    );
  };
}

function CardPickRow({cards, label, color, sel, onSel}) {
  return (
    <div style={{marginBottom:12}}>
      <div style={{fontSize:9,color:color,fontWeight:700,letterSpacing:1,marginBottom:6}}>{label}</div>
      <div style={{display:"flex",flexWrap:"wrap",gap:6}}>
        {cards.map(function(c){
          var isSel=sel===c.id;
          return (<div key={c.id} onClick={function(){onSel(isSel?null:c.id);}} style={{width:100,border:isSel?"2px solid "+color:"1px solid "+color+"44",borderRadius:8,background:isSel?"#0c1a2e":"#1e1a14",padding:"8px 10px",cursor:"pointer",textAlign:"center",transition:"all .12s",position:"relative"}}>
            {isSel&&<div style={{position:"absolute",top:4,right:6,fontSize:10,color:color,fontWeight:700}}>✓</div>}
            <div style={{fontSize:7,color:color,fontWeight:700,letterSpacing:.5,marginBottom:2}}>{c.type}</div>
            <div style={{fontFamily:"'Rubik',sans-serif",fontSize:10,color:"#f1f5f9",fontWeight:700,lineHeight:1.3}}>{c.name}</div>
            <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:3}}>${c.value}k</div>
          </div>);
        })}
      </div>
    </div>
  );
}

function TradeInSelectModal({st, dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "TRADE_IN_SELECT") return null;
  var [selA,setSelA]=useState(null); var [selR,setSelR]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSelA(null);setSelR(null);setTimeLeft(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var owner=st.players.find(function(p){return p.id===pc.srcPlayer;}); var hand=owner?owner.hand:[];
  var acts=hand.filter(function(c){return c.type==="ACTION";}); var rxs=hand.filter(function(c){return c.type==="REACTION";});
  var ok=!!(selA&&selR); var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
    <div style={{background:"#1a1410",border:"2px solid #f59e0b",borderRadius:14,padding:24,maxWidth:560,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>TRADE-IN VALUE</div>
      <div style={{fontSize:13,color:"#94a3b8",marginBottom:8}}>Select 1 action + 1 reaction to discard for ${pc.tradeInAmount||6}k</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:col,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:pct+"%",background:col,borderRadius:2,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",gap:12}}>
        <div style={{flex:1}}><CardPickRow cards={acts} label="ACTION CARDS" color="#93c5fd" sel={selA} onSel={setSelA}/></div>
        <div style={{flex:1}}><CardPickRow cards={rxs} label="REACTION CARDS" color="#f87171" sel={selR} onSel={setSelR}/></div>
      </div>
      <div style={{display:"flex",gap:8,marginTop:12}}>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}} style={{flex:1,padding:"9px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"2px solid #334155",color:"#94a3b8"}}>Skip (gain ${pc.tradeInAmount||6}k)</button>
        <button onClick={function(){if(ok)dispatch({type:"RESOLVE_CHOICE",id:selA+":"+selR});}} disabled={!ok} style={{flex:2,padding:"9px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:ok?"pointer":"not-allowed",background:ok?"#1c1004":"#1e293b",border:"2px solid "+(ok?"#f59e0b":"#334155"),color:ok?"#fcd34d":"#475569"}}>{ok?"Discard Both -> Gain $"+(pc.tradeInAmount||6)+"k":"Select 1 action + 1 reaction"}</button>
      </div>
    </div>
  </div>);
}

function SimpleCardListModal({type, title, color, dispatch, st}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== type) return null;
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSel(null);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";
  var opts=pc.options||[];

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
    <div style={{background:"#1a1410",border:"2px solid "+color,borderRadius:14,padding:24,maxWidth:540,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:color,fontFamily:"'Rubik',sans-serif",marginBottom:4}}>{title}</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:tCol,borderRadius:2,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14,maxHeight:260,overflowY:"auto"}}>
        {opts.map(function(c){var isSel=sel===c.id; return(<div key={c.id} onClick={function(){setSel(isSel?null:c.id);}} style={{width:104,border:isSel?"2px solid "+color:"1px solid "+color+"44",borderRadius:8,background:isSel?"#0c1a2e":"#1e1a14",padding:"8px 10px",cursor:"pointer",textAlign:"center",transition:"all .12s",position:"relative"}}>
          {isSel&&<div style={{position:"absolute",top:4,right:6,fontSize:10,color,fontWeight:700}}>✓</div>}
          <div style={{fontSize:7,color,fontWeight:700,letterSpacing:.5,marginBottom:2}}>{c.type||"ASSET"}</div>
          <div style={{fontFamily:"'Rubik',sans-serif",fontSize:10,color:"#f1f5f9",fontWeight:700,lineHeight:1.3}}>{c.name}</div>
          <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:3}}>${c.value||c.origVal||c.val}k</div>
        </div>);})}
      </div>
      <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#0c1a2e":"#1e293b",border:"2px solid "+(sel?color:"#334155"),color:sel?color:"#475569"}}>{sel?"Confirm Selection":"Select a Card..."}</button>
    </div>
  </div>);
}

function BlueprintPlaceModal({st,dispatch}){return <SimpleCardListModal type="BLUEPRINT_PLACE" title="BLUEPRINT - STORE ACTION CARD" color="#60a5fa" st={st} dispatch={dispatch}/>;}

function RetainerPlaceModal({st,dispatch}){return <SimpleCardListModal type="RETAINER_PLACE" title="RETAINER - STORE REACTION CARD" color="#a78bfa" st={st} dispatch={dispatch}/>;}

function MonopolySelectModal({st,dispatch}){return <SimpleCardListModal type="MONOPOLY_SELECT" title="MONOPOLY - PLACE CARD" color="#a78bfa" st={st} dispatch={dispatch}/>;}

function MRShowCardModal({st,dispatch}){return <SimpleCardListModal type="MR_SHOW_CARD" title="MARKET RESEARCH - SHOW A CARD" color="#60a5fa" st={st} dispatch={dispatch}/>;}

function MinorLossDiscardModal({st,dispatch}){return <SimpleCardListModal type="MINOR_LOSS_DISCARD" title="A MINOR LOSS - DISCARD A CARD" color="#f87171" st={st} dispatch={dispatch}/>;};

function TwoPhaseModal({type, title, color, dispatch, st, phase1Label, phase2Label}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== type) return null;
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+25000)-Date.now())/1000)));
  useEffect(function(){setSel(null);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+25000)-Date.now())/1000)));}, [pc.timerEnd, pc.phase]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/25)*100; var tCol=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";
  var opts=pc.phase==="opponent" ? (pc.opponents||[]) : (pc.assets||[]);
  var isP1 = pc.phase==="opponent";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
    <div style={{background:"#1a1410",border:"2px solid "+color,borderRadius:14,padding:24,maxWidth:540,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color,fontFamily:"'Rubik',sans-serif",marginBottom:4}}>{title}</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:8}}>{isP1?phase1Label:phase2Label}</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:tCol,borderRadius:2,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14,maxHeight:240,overflowY:"auto"}}>
        {opts.map(function(o){var id=o.id||null; var isSel=sel===id; var lbl=o.name||(o.assetCount?" ("+o.assetCount+" assets)":""); var val=o.value||o.origVal||o.val;
          return (<div key={id} onClick={function(){setSel(isSel?null:id);}} style={{minWidth:100,border:isSel?"2px solid "+color:"1px solid "+color+"44",borderRadius:8,background:isSel?"#0c1a2e":"#1e1a14",padding:"8px 10px",cursor:"pointer",textAlign:"center",transition:"all .12s",position:"relative"}}>
            {isSel&&<div style={{position:"absolute",top:4,right:6,fontSize:10,color,fontWeight:700}}>✓</div>}
            <div style={{fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700}}>{lbl}</div>
            {val&&<div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:2}}>${val}k</div>}
          </div>);
        })}
      </div>
      <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#0c1a2e":"#1e293b",border:"2px solid "+(sel?color:"#334155"),color:sel?color:"#475569"}}>{sel?"Confirm":"Select..."}</button>
    </div>
  </div>);
}

function EyeSelectModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "EYE_SELECT") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd, pc.phase]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var [sel,setSel]=useState(null);
  var [detailAsset,setDetailAsset]=useState(null);
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";
  var isP1=pc.phase==="opponent";
  var opts=isP1?(pc.opponents||[]):(pc.assets||[]);
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
    {detailAsset&&<div style={{position:"absolute",inset:0,zIndex:10,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center"}} onClick={function(){setDetailAsset(null);}}>
      <div style={{background:"#1a1410",border:"2px solid #f87171",borderRadius:14,padding:24,maxWidth:320,width:"92%"}} onClick={function(e){e.stopPropagation();}}>
        <div style={{fontFamily:"'Rubik',sans-serif",fontSize:16,color:"#f1f5f9",fontWeight:700,marginBottom:4}}>{detailAsset.name}</div>
        <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:14,color:"#f59e0b",fontWeight:700,marginBottom:10}}>${detailAsset.origVal||detailAsset.value||detailAsset.val}k</div>
        <div style={{fontSize:10,color:"#94a3b8",lineHeight:1.5}}>{getDesc(detailAsset)}</div>
        <button onClick={function(){setDetailAsset(null);}} style={{marginTop:14,width:"100%",padding:"8px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Close</button>
      </div>
    </div>}
    <div style={{background:"#1a1410",border:"2px solid #f87171",borderRadius:14,padding:24,maxWidth:520,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f87171",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>⚖ EYE FOR AN EYE</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:8}}>{isP1?"Choose an opponent to target":"Choose one of their assets to discard"}</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:tCol,borderRadius:2,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14,maxHeight:220,overflowY:"auto"}}>
        {opts.map(function(o){
          var id=o.id||null; var isSel=sel===id;
          var lbl=o.name||(o.assetCount?" ("+o.assetCount+" assets)":"");
          var val=o.value||o.origVal||o.val;
          return (<div key={id} style={{position:"relative",minWidth:100,border:isSel?"2px solid #f87171":"1px solid #f8717144",borderRadius:8,background:isSel?"#2a0a0a":"#1e1a14",padding:"8px 10px",cursor:"pointer",textAlign:"center",transition:"all .12s"}}
            onClick={function(){setSel(isSel?null:id);}}>
            {isSel&&<div style={{position:"absolute",top:4,right:6,fontSize:10,color:"#f87171",fontWeight:700}}>✓</div>}
            <div style={{fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700}}>{lbl}</div>
            {val&&<div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:2}}>${val}k</div>}
            {!isP1&&<button onClick={function(e){e.stopPropagation();setDetailAsset(o);}} style={{marginTop:4,fontSize:8,padding:"1px 6px",borderRadius:3,background:"#0a1220",border:"1px solid #3b82f644",color:"#60a5fa",cursor:"pointer"}}>ℹ Info</button>}
          </div>);
        })}
      </div>
      <div style={{display:"flex",gap:8}}>
        {!isP1&&<button onClick={function(){setSel(null);dispatch({type:"RESOLVE_CHOICE",id:"back"});}} style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>← Back</button>}
        <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel} style={{flex:3,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#2a0a0a":"#1e293b",border:"2px solid "+(sel?"#f87171":"#334155"),color:sel?"#fca5a5":"#475569",transition:"all .2s"}}>
          {sel?(isP1?"Target →":"Confirm Discard"):"Select..."}
        </button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"auto"});}} style={{flex:1,padding:"10px 0",fontSize:10,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#64748b"}}>Cancel</button>
      </div>
    </div>
  </div>);
}

function BTBSelectModal({st,dispatch}){return <TwoPhaseModal type="BTB_SELECT" title="BACK TO BASICS" color="#f87171" st={st} dispatch={dispatch} phase1Label="Choose an opponent (3+ assets)" phase2Label="Choose an asset to discard"/>;}

function SFSelectModal({st,dispatch}){return <TwoPhaseModal type="SF_SELECT" title="A SMALL FEE" color="#f59e0b" st={st} dispatch={dispatch} phase1Label="Choose an opponent (2+ assets)" phase2Label="Choose an asset to steal"/>;}


function BOGOSelectModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "BOGO_SELECT") return null;
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[]; var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #f59e0b",borderRadius:14,padding:24,maxWidth:500,width:"92%"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:8}}>BOGO - COPY AN ASSET</div>
    <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:col,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
    <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
    <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14}}>{opts.map(function(a){var isSel=sel===a.id;return(<div key={a.id} onClick={function(){setSel(isSel?null:a.id);}} style={{width:110,border:isSel?"2px solid #f59e0b":"1px solid #f59e0b44",borderRadius:8,background:isSel?"#1c1004":"#1e1a14",padding:"8px 10px",cursor:"pointer",textAlign:"center"}}>
      <div style={{fontSize:9,color:isSel?"#f59e0b":"#94a3b8",letterSpacing:.5}}>{a.sub}</div>
      <div style={{fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700}}>{a.name} {a.lockedCard && <span style={{fontSize:8,color:"#60a5fa",marginLeft:4,background:"#0c1a2e",borderRadius:3,padding:"1px 4px"}}>+ {a.lockedCard.name}</span>}</div>
      <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:2}}>${a.origVal||a.val||a.value}k</div>
    </div>);})}</div>
    <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#1c1004":"#1e293b",border:"2px solid "+(sel?"#f59e0b":"#334155"),color:sel?"#fcd34d":"#475569"}}>{sel?"Copy This Asset":"Select an Asset to Copy"}</button>
  </div></div>);
}

function FranchiseSelectModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "FRANCHISE_SELECT") return null;
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[]; var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #a78bfa",borderRadius:14,padding:24,maxWidth:500,width:"92%"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#a78bfa",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>FRANCHISE - DOUBLE A PASSIVE ASSET</div>
    <div style={{fontSize:11,color:"#94a3b8",marginBottom:8}}>Choose one of your passive assets to permanently double its effect.</div>
    <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
    <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14}}>{opts.map(function(a){var isSel=sel===a.id;return(<div key={a.id} onClick={function(){setSel(isSel?null:a.id);}} style={{width:110,border:isSel?"2px solid #a78bfa":"1px solid #a78bfa44",borderRadius:8,background:isSel?"#12062e":"#1e1a14",padding:"8px 10px",cursor:"pointer",textAlign:"center"}}>
      <div style={{fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700}}>{a.name} {a.lockedCard && <span style={{fontSize:8,color:"#60a5fa",marginLeft:4,background:"#0c1a2e",borderRadius:3,padding:"1px 4px"}}>+ {a.lockedCard.name}</span>}</div>
      <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:2}}>${a.origVal||a.val||a.value}k</div>
    </div>);})}</div>
    <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#12062e":"#1e293b",border:"2px solid "+(sel?"#a78bfa":"#334155"),color:sel?"#c4b5fd":"#475569"}}>{sel?"Franchise This Asset":"Select a Passive Asset"}</button>
  </div></div>);
}

function ExchangeSelectModal({st,dispatch}){return <SimpleCardListModal type="EXCHANGE_SELECT" title="EXCHANGE - TAKE FROM SHOP" color="#4ade80" st={st} dispatch={dispatch}/>;}

function ValuationPickModal({st,dispatch}){return <SimpleCardListModal type="VALUATION_PICK" title="VALUATION - DISCARD FOR VALUE" color="#f59e0b" st={st} dispatch={dispatch}/>;};

function CreditLineChoiceModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "CREDIT_LINE_CHOICE") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+10000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/10)*100; var col=timeLeft>5?"#4ade80":timeLeft>2?"#facc15":"#f87171";
  var cur=pc.currentTokens||0; var prices=[0,pc.clPrice1||1,pc.clPrice2||3,pc.clPrice3||6];

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #60a5fa",borderRadius:14,padding:24,maxWidth:400,width:"92%"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#60a5fa",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>CREDIT LINE - REMOVE TOKENS</div>
    <div style={{fontSize:11,color:"#94a3b8",marginBottom:8}}>Remove tokens to gain ${pc.clValue||2}k each. Current tokens: {cur}</div>
    <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
    <div style={{display:"flex",gap:8}}>
      {[1,2,3].filter(function(n){return n<=cur;}).map(function(n){return(
        <button key={n} onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:String(n)});}} style={{flex:1,padding:"12px 0",fontSize:13,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#0c1833",border:"2px solid #2563eb",color:"#93c5fd"}}>
          {n} token{n!==1?"s":""}<br/><span style={{fontSize:11,color:"#60a5fa"}}>+${n*(pc.clValue||2)}k</span>
        </button>
      );})}
    </div>
  </div></div>);
}

function RndActivateModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "RND_ACTIVATE") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+10000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #22c55e",borderRadius:14,padding:24,maxWidth:380,width:"92%",textAlign:"center"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#22c55e",fontFamily:"'Rubik',sans-serif",marginBottom:8}}>R&D BUDGET - READY!</div>
    <div style={{fontSize:13,color:"#f1f5f9",marginBottom:4}}>Draw {pc.drawCount||3} cards and play actions for free this turn.</div>
    <div style={{fontSize:11,color:"#94a3b8",marginBottom:14}}>Warning: your hand will be discarded at end of turn.</div>
    <div style={{display:"flex",gap:8}}>
      <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"confirm"});}} style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#0d2010",border:"2px solid #22c55e",color:"#4ade80"}}>Activate R&D Budget</button>
      <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"auto"});}} style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"2px solid #334155",color:"#94a3b8"}}>Cancel</button>
    </div>
  </div></div>);
}

function APActivateSelectModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "AP_ACTIVATE_SELECT") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+10000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var lc=pc.lockedCard; var canAdd=pc.canAdd; var canFire=pc.canFire;
  var [showPlace, setShowPlace] = useState(false);
  var handOpts = pc.handOptions || [];

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #fbbf24",borderRadius:14,padding:24,maxWidth:420,width:"92%"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#fbbf24",fontFamily:"'Rubik',sans-serif",marginBottom:6}}>ACTION PLAN</div>
    <div style={{fontSize:12,color:"#94a3b8",marginBottom:6}}>Tokens: {pc.currentTokens||0}/{pc.maxTokens||5}{lc?' | Stored: "'+lc.name+'" ($'+lc.value+'k)':' | No card stored'}</div>
    {lc&&<div style={{fontSize:9,color:"#475569",marginBottom:10,padding:"6px 8px",background:"#0a1220",borderRadius:6,border:"1px solid #1e293b",lineHeight:1.4}}>{lc.desc||""}</div>}
    {!showPlace ? (
      <div style={{display:"flex",flexDirection:"column",gap:8}}>
        <div style={{display:"flex",gap:8}}>
          {canAdd&&<button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"add_token"});}} style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1c1003",border:"2px solid #fbbf24",color:"#fcd34d"}}>+ Add Token</button>}
          {canFire&&<button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"fire"});}} style={{flex:2,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1c0803",border:"2px solid #f97316",color:"#fb923c"}}>Fire "{lc&&lc.name}" ({lc&&lc.value}k tokens)</button>}
        </div>
        {pc.canPlace&&<button onClick={function(){setShowPlace(true);}} style={{width:"100%",padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#0c1833",border:"2px solid #3b82f6",color:"#93c5fd"}}>{lc?"Replace Stored Card":"Store an Action Card"}</button>}
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"auto"});}} style={{width:"100%",padding:"8px 0",fontSize:10,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Cancel</button>
      </div>
    ) : (
      <div>
        <div style={{fontSize:10,color:"#93c5fd",marginBottom:8,fontWeight:700}}>SELECT AN ACTION CARD TO STORE:</div>
        <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:12}}>
          {handOpts.map(function(card){ return (
            <div key={card.id} onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"place:"+card.id});}}
              style={{width:100,border:"1px solid #3b82f644",borderRadius:8,background:"#0a1220",padding:"8px",cursor:"pointer",textAlign:"center"}}>
              <div style={{fontSize:7,color:"#93c5fd",fontWeight:700,marginBottom:2}}>{card.type}</div>
              <div style={{fontFamily:"'Rubik',sans-serif",fontSize:10,color:"#f1f5f9",fontWeight:700,lineHeight:1.3}}>{card.name}</div>
              <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:11,color:"#f59e0b",fontWeight:700,marginTop:2}}>${card.value}k</div>
            </div>
          ); })}
        </div>
        <button onClick={function(){setShowPlace(false);}} style={{width:"100%",padding:"8px 0",fontSize:10,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Back</button>
      </div>
    )}
  </div></div>);
}

function PriceCheckPromptModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "PRICE_CHECK_PROMPT") return null;
  var card=pc.drawnCard;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+10000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}>
    <div style={{background:"#1a1410",border:"2px solid #67e8f9",borderRadius:14,padding:24,maxWidth:340,width:"92%",textAlign:"center"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#67e8f9",fontFamily:"'Rubik',sans-serif",marginBottom:8}}>PRICE CHECK</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:12}}>You drew <b style={{color:"#f1f5f9"}}>{card&&card.name}</b> — play it for free?</div>
      {card&&<div style={{background:"#071a1f",border:"1px solid #0e4a5a",borderRadius:8,padding:10,marginBottom:12,textAlign:"left"}}>
        <div style={{fontSize:9,color:"#67e8f9",fontWeight:700,marginBottom:4}}>{card.type} · ${card.value||card.origVal}k</div>
        <div style={{fontSize:9,color:"#64748b",lineHeight:1.4}}>{getDesc(card)||card.desc||""}</div>
      </div>}
      <div style={{display:"flex",gap:8}}>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:card&&card.id});}} style={{flex:2,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#071a1f",border:"2px solid #67e8f9",color:"#67e8f9"}}>Play Free ({timeLeft}s)</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}} style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"2px solid #334155",color:"#94a3b8"}}>Skip</button>
      </div>
    </div>
  </div>);
}

function FullRefundModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "FULL_REFUND_PROMPT") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+15000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"skip"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var card=pc.card;
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}>
    <div style={{background:"#1a1410",border:"2px solid #f59e0b",borderRadius:14,padding:24,maxWidth:340,width:"92%",textAlign:"center"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:8}}>FULL REFUND</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:12}}>You sold <b style={{color:"#f1f5f9"}}>{card&&card.name}</b> for $1k. Activate its effect for free?</div>
      {card&&<div style={{fontSize:9,color:"#64748b",marginBottom:14,lineHeight:1.5}}>{getDesc(card)||card.desc||""}</div>}
      <div style={{display:"flex",gap:8}}>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"activate"});}} style={{flex:2,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1c1003",border:"2px solid #f59e0b",color:"#fcd34d"}}>Activate! ({timeLeft}s)</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"skip"});}} style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"2px solid #334155",color:"#94a3b8"}}>Skip</button>
      </div>
    </div>
  </div>);
}

function ComingSoonPeekModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "COMING_SOON_PEEK") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+10000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"done"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var card=pc.card;

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #34d399",borderRadius:14,padding:24,maxWidth:360,width:"92%",textAlign:"center"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#34d399",fontFamily:"'Rubik',sans-serif",marginBottom:8}}>COMING SOON - TOP ASSET</div>
    {card&&<div style={{background:"#0d1a0d",border:"1px solid #16a34a",borderRadius:10,padding:14,marginBottom:14}}>
      <div style={{fontFamily:"'Rubik',sans-serif",fontSize:16,color:"#f1f5f9",fontWeight:700}}>{card.name}</div>
      <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:18,color:"#f59e0b",fontWeight:700,marginTop:4,marginBottom:8}}>${card.origVal||card.val}k</div>
      <div style={{fontSize:9,color:"#94a3b8",lineHeight:1.5,textAlign:"left"}}>{getDesc(card)}</div>
    </div>}
    <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"done"});}} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#0d2010",border:"2px solid #34d399",color:"#4ade80"}}>OK ({timeLeft}s)</button>
  </div></div>);
}

function WTPSelectModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "WTP_SELECT") return null;
  var [sels,setSels]=useState([]);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSels([]);setTimeLeft(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[]; var trigVal=pc.trigVal||0;
  var total=sels.reduce(function(s,id){var c=opts.find(function(o){return o.id===id;});return s+(c?c.value:0);},0);
  var ok=total>trigVal;
  var thresholdMet=total>trigVal;
  var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1000}}><div style={{background:"#1a1410",border:"2px solid #67e8f9",borderRadius:14,padding:24,maxWidth:540,width:"92%"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#67e8f9",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>WORTH THE PRICE</div>
    <div style={{fontSize:11,color:"#94a3b8",marginBottom:8}}>Discard cards totaling more than ${trigVal}k to cancel the effect. Selected: ${total}k</div>
    <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
    <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14}}>{opts.map(function(c){var isSel=sels.indexOf(c.id)>=0;var canAdd = isSel || !thresholdMet;
      return(<div key={c.id} onClick={function(){ if(canAdd) setSels(function(s){return isSel?s.filter(function(x){return x!==c.id;}):[...s,c.id];}); }} style={{width:104,border:isSel?"2px solid #67e8f9":canAdd?"1px solid #67e8f944":"1px solid #1e293b",borderRadius:8,background:isSel?"#031c1f":canAdd?"#1e1a14":"#0d1117",padding:"8px 10px",cursor:canAdd?"pointer":"not-allowed",opacity:canAdd?1:0.4,textAlign:"center"}}>
      <div style={{fontSize:7,color:"#67e8f9",fontWeight:700,marginBottom:2}}>{c.type}</div>
      <div style={{fontFamily:"'Rubik',sans-serif",fontSize:10,color:"#f1f5f9",fontWeight:700}}>{c.name}</div>
      <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:2}}>${c.value}k</div>
    </div>);})}</div>
    <div style={{display:"flex",gap:8}}>
      <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}} style={{flex:1,padding:"9px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"2px solid #334155",color:"#94a3b8"}}>Pass</button>
      <button onClick={function(){if(ok)dispatch({type:"RESOLVE_CHOICE",id:sels.join(",")});}} disabled={!ok} style={{flex:2,padding:"9px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:ok?"pointer":"not-allowed",background:ok?"#031c1f":"#1e293b",border:"2px solid "+(ok?"#67e8f9":"#334155"),color:ok?"#67e8f9":"#475569"}}>{ok?"Discard $"+total+"k -> Cancel Effect":"Need >"+ trigVal+"k total (have $"+total+"k)"}</button>
    </div>
  </div></div>);
}

function TotalLossPickModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "TOTAL_LOSS_PICK_VALUE") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #ef4444",borderRadius:14,padding:24,maxWidth:400,width:"92%",textAlign:"center"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#ef4444",fontFamily:"'Rubik',sans-serif",marginBottom:8}}>TOTAL LOSS - CHOOSE A VALUE</div>
    <div style={{fontSize:12,color:"#94a3b8",marginBottom:12}}>Opponents discard all cards of chosen value. ({timeLeft}s)</div>
    <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
    <div style={{display:"flex",gap:8,justifyContent:"center"}}>
      {[1,2,3,4,5].map(function(v){return(<button key={v} onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:String(v)});}} style={{width:56,height:56,fontSize:18,fontWeight:700,borderRadius:10,cursor:"pointer",background:"#1f0a0a",border:"2px solid #ef4444",color:"#fca5a5"}}>${v}k</button>);})}
    </div>
  </div></div>);
}

function QuickExchangeModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "QUICK_EXCHANGE_DISCARD") return null;
  var [sels,setSels]=useState([]);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSels([]);setTimeLeft(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[]; var valid=pc.validValues||[];
  var toggle = function(id) {var c=opts.find(function(o){return o.id===id;}); if(!c) return; var cur=sels.indexOf(id)>=0?sels.filter(function(x){return x!==id;}):[...sels,id]; if(cur.length>2) return; var cs=cur.map(function(x){return opts.find(function(o){return o.id===x;});}); if(cs.some(function(x){return !x;})) return; if(cur.length===2&&cs[0].value!==cs[1].value) return; setSels(cur); }
  var ok=sels.length===2; var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #34d399",borderRadius:14,padding:24,maxWidth:520,width:"92%"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#34d399",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>QUICK EXCHANGE - DISCARD 2 MATCHING</div>
    <div style={{fontSize:11,color:"#94a3b8",marginBottom:8}}>Select 2 cards of the same value to discard. Valid values: {valid.map(function(v){return "$"+v+"k";}).join(", ")}</div>
    <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
    <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14}}>{opts.map(function(c){var isSel=sels.indexOf(c.id)>=0;var isValid=valid.indexOf(c.value)>=0;return(<div key={c.id} onClick={function(){toggle(c.id);}} style={{width:104,border:isSel?"2px solid #34d399":"1px solid "+(isValid?"#34d39944":"#33334444"),borderRadius:8,background:isSel?"#0d2010":"#1e1a14",padding:"8px 10px",cursor:isValid?"pointer":"not-allowed",textAlign:"center",opacity:isValid?1:0.4}}>
      <div style={{fontSize:7,color:"#34d399",fontWeight:700,marginBottom:2}}>{c.type}</div>
      <div style={{fontFamily:"'Rubik',sans-serif",fontSize:10,color:"#f1f5f9",fontWeight:700}}>{c.name}</div>
      <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:12,color:"#f59e0b",fontWeight:700,marginTop:2}}>${c.value}k</div>
    </div>);})}</div>
    <button onClick={function(){if(ok)dispatch({type:"RESOLVE_CHOICE",id:sels.join(",")});}} disabled={!ok} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:ok?"pointer":"not-allowed",background:ok?"#0d2010":"#1e293b",border:"2px solid "+(ok?"#34d399":"#334155"),color:ok?"#4ade80":"#475569"}}>{ok?"Discard Both -> Get Asset":"Select 2 matching-value cards"}</button>
  </div></div>);
}

function FullRefundTargetModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "FULL_REFUND_TARGET") return null;
  var [sel,setSel]=React.useState(null);
  var [timeLeft,setTimeLeft]=React.useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  React.useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[]; var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}>
    <div style={{background:"#1a1410",border:"2px solid #f59e0b",borderRadius:14,padding:24,maxWidth:380,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>FULL REFUND — SELECT TARGET</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:8}}>Choose a target for <b style={{color:"#f1f5f9"}}>{pc.card&&pc.card.name}</b>.</div>
      <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",gap:8,flexWrap:"wrap",marginBottom:14}}>{opts.map(function(o){var isSel=sel===o.id;return(<button key={o.id} onClick={function(){setSel(isSel?null:o.id);}} style={{flex:1,padding:"12px 8px",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",background:isSel?"#1c1003":"#1e293b",border:"2px solid "+(isSel?"#f59e0b":"#334155"),color:isSel?"#fcd34d":"#94a3b8"}}>{o.name}</button>);})}</div>
      <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#1c1003":"#1e293b",border:"2px solid "+(sel?"#f59e0b":"#334155"),color:sel?"#fcd34d":"#475569"}}>{sel?"Target "+((opts.find(function(o){return o.id===sel;})||{}).name||"?"):"Select a Target"}</button>
    </div>
  </div>);
}

function CoverCostsTargetModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "COVER_COSTS_TARGET") return null;
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil((((pc&&pc.timerEnd)||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[]; var pct=(timeLeft/20)*100; var col=timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";

  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800}}><div style={{background:"#1a1410",border:"2px solid #f87171",borderRadius:14,padding:24,maxWidth:400,width:"92%"}}>
    <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f87171",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>COVER THE COSTS</div>
    <div style={{fontSize:12,color:"#94a3b8",marginBottom:8}}>Redirect ${pc.amount||0}k loss to an opponent.</div>
    <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:col,transition:"width 1s linear"}}/></div>
    <div style={{display:"flex",gap:8,flexWrap:"wrap",marginBottom:14}}>{opts.map(function(o){var isSel=sel===o.id;return(<button key={o.id} onClick={function(){setSel(isSel?null:o.id);}} style={{flex:1,padding:"12px 8px",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",background:isSel?"#1f0a0a":"#1e293b",border:"2px solid "+(isSel?"#f87171":"#334155"),color:isSel?"#fca5a5":"#94a3b8"}}>{o.name}</button>);})}</div>
    <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel} style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#1f0a0a":"#1e293b",border:"2px solid "+(sel?"#f87171":"#334155"),color:sel?"#fca5a5":"#475569"}}>{sel?"Redirect Loss to "+((opts.find(function(o){return o.id===sel;})||{}).name||"?"):"Select a Target"}</button>
  </div></div>);
}

function DiscountChoiceModal({st,dispatch}) {
  return <SimpleCardListModal type="DISCOUNT_CHOICE" title="DISCOUNT - BUY FROM SHOP" color="#4ade80" st={st} dispatch={dispatch}/>;
}
function ProductUpdateModal({st,dispatch}) {
  var pc=st.pendingChoice;
  var types=["PU_ASSET_SELECT","PU_FIELD_SELECT","PU_DIRECTION_SELECT"];
  if (!pc || !types.includes(pc.type)) return null;
  var isAsset=pc.type==="PU_ASSET_SELECT";
  var isField=pc.type==="PU_FIELD_SELECT";
  var isDir=pc.type==="PU_DIRECTION_SELECT";
  var maxT=isAsset?20:5;
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+maxT*1000)-Date.now())/1000)));
  useEffect(function(){setSel(null);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+maxT*1000)-Date.now())/1000)));}, [pc.timerEnd,pc.type]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/maxT)*100; var tCol=timeLeft>10?"#a78bfa":timeLeft>3?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #a78bfa",borderRadius:16,padding:24,maxWidth:460,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#a78bfa",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>🔧 PRODUCT UPDATE</div>
      {isAsset && <div>
        <div style={{fontSize:11,color:"#94a3b8",marginBottom:8}}>Select an asset to upgrade (20s):</div>
        <div style={{display:"flex",flexDirection:"column",gap:6,maxHeight:240,overflowY:"auto",marginBottom:12}}>
          {(pc.options||[]).map(function(a){
            var isSel=sel===a.id; var flds=PU_FIELDS[a.hook]||[];
            return (<div key={a.id} onClick={function(){setSel(isSel?null:a.id);}}
              style={{background:isSel?"#1a0d3a":"#111827",border:"2px solid "+(isSel?"#a78bfa":"#1e293b"),
                      borderRadius:8,padding:"8px 12px",cursor:"pointer",transition:"all .12s"}}>
              <div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
                <span style={{fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",fontSize:12}}>{a.name}</span>
                <div style={{textAlign:"right"}}>
                  <div style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:10}}>${a.origVal}k</div>
                  <div style={{fontSize:8,color:"#64748b"}}>{flds.map(function(f){return f.label;}).join(", ")}</div>
                </div>
              </div>
            </div>);
          })}
        </div>
      </div>}
      {isField && <div>
        <div style={{fontSize:11,color:"#94a3b8",marginBottom:8}}>Select a value to adjust on <b style={{color:"#f1f5f9"}}>{pc.targetAssetName}</b> (5s):</div>
        <div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:12}}>
          {(pc.fields||[]).map(function(f){
            var isSel=sel===f.key;
            var curSrc=st.players.flatMap(function(p){return p.assets;}).find(function(a){return a.id===pc.targetAssetId;});
            var curVal=curSrc&&curSrc[f.key]!==undefined?curSrc[f.key]:"?";
            return (<div key={f.key} onClick={function(){setSel(isSel?null:f.key);}}
              style={{background:isSel?"#1a0d3a":"#111827",border:"2px solid "+(isSel?"#a78bfa":"#1e293b"),
                      borderRadius:8,padding:"10px 14px",cursor:"pointer",transition:"all .12s",
                      display:"flex",justifyContent:"space-between",alignItems:"center"}}>
              <span style={{color:isSel?"#a78bfa":"#f1f5f9",fontWeight:600,fontSize:12}}>{f.label}</span>
              <span style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:12,fontWeight:700}}>
                {f.key==="_creditLineAll"?"all prices":(""+curVal)}
              </span>
            </div>);
          })}
        </div>
      </div>}
      {isDir && <div>
        <div style={{fontSize:11,color:"#94a3b8",marginBottom:6}}>Adjust <b style={{color:"#f1f5f9"}}>{pc.targetAssetName}</b> — <b style={{color:"#a78bfa"}}>{pc.targetFieldLabel}</b></div>
        <div style={{fontSize:10,color:"#64748b",marginBottom:12}}>±{pc.puAmount||1} · Floor: {pc.fieldFloor!==null?pc.fieldFloor:"none"}</div>
        <div style={{display:"flex",gap:10,marginBottom:12}}>
          <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"up"});}}
            style={{flex:1,padding:"14px 0",fontSize:13,fontWeight:700,borderRadius:8,cursor:"pointer",
                    background:"#0a2010",border:"2px solid #4ade80",color:"#4ade80"}}>▲ Increase</button>
          <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"down"});}}
            style={{flex:1,padding:"14px 0",fontSize:13,fontWeight:700,borderRadius:8,cursor:"pointer",
                    background:"#200a0a",border:"2px solid #f87171",color:"#f87171"}}>▼ Decrease</button>
        </div>
      </div>}
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:isDir?0:10}}><div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/></div>
      {!isDir && <div style={{display:"flex",gap:8,marginTop:10}}>
        <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}}
          style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",
                  background:sel?"#150d2e":"#1e293b",border:"2px solid "+(sel?"#a78bfa":"#334155"),color:sel?"#a78bfa":"#475569"}}>
          {isAsset?"Select Asset":"Select Field"}</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}}
          style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Skip</button>
      </div>}
    </div>
  </div>);
}

function PaidWorkModal({st,dispatch}) {
  var pc=st.pendingPWChoice;
  if (!pc || pc.type !== "PAID_WORK_PROMPT") return null;
  var totalMs = pc.timerTotal || 3500;
  var totalSec = Math.ceil(totalMs / 1000);
  // timeLeft: seconds remaining, clamped to [0, totalSec]
  var [timeLeft,setTimeLeft]=useState(function(){ return Math.min(totalSec, Math.max(0, Math.ceil(((pc.timerEnd||Date.now()+totalMs)-Date.now())/1000))); });
  useEffect(function(){ setTimeLeft(Math.min(totalSec, Math.max(0, Math.ceil(((pc.timerEnd||Date.now()+totalMs)-Date.now())/1000)))); }, [pc.timerEnd]);
  // Count down every second; when hitting 0, delay dispatch by 1.1s to let bar animation finish
  useEffect(function(){
    if(timeLeft<=0){
      var t=setTimeout(function(){ dispatch({type:"RESOLVE_PW_CHOICE",id:"auto"}); }, 1100);
      return function(){ clearTimeout(t); };
    }
    var t=setTimeout(function(){ setTimeLeft(function(n){ return Math.max(0,n-1); }); },1000);
    return function(){ clearTimeout(t); };
  }, [timeLeft]);
  var affName = pc.affectedPid ? (st.players.find(function(p){return p.id===pc.affectedPid;})||{}).name||"?" : "?";
  var pwAmt = pc.pwAmount||2;
  var upOk = pc.isGain ? true : (pc.amount - pwAmt >= 0);
  var downOk = pc.isGain ? (pc.amount - pwAmt >= 0) : true;
  // Bar shrinks from 100% → 0% over totalSec seconds
  var pct = (timeLeft / totalSec) * 100;
  var barCol = pct > 50 ? "#38bdf8" : pct > 20 ? "#facc15" : "#f87171";
  return (
    <div style={{position:"fixed",inset:0,display:"flex",alignItems:"center",justifyContent:"center",
                 zIndex:1200,pointerEvents:"none"}}>
      <div style={{background:"#0d1a2d",border:"2px solid #38bdf8",borderRadius:16,
                   padding:"18px 22px",boxShadow:"0 8px 40px rgba(0,0,0,0.8)",
                   minWidth:320,maxWidth:400,width:"90%",pointerEvents:"auto",
                   animation:"popIn .2s ease"}}>
        {/* Header */}
        <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:10}}>
          <div style={{fontSize:18}}>💼</div>
          <div style={{flex:1}}>
            <div style={{fontSize:10,color:"#38bdf8",fontWeight:700,letterSpacing:1.5}}>PAID WORK</div>
            <div style={{fontSize:13,color:"#f1f5f9",marginTop:1}}>
              <b style={{color:"#f59e0b"}}>{affName}</b> is {pc.isGain?"gaining":"losing"} <b style={{color:"#fde68a"}}>${pc.amount}k</b>
            </div>
          </div>
          <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:13,fontWeight:700,
                       color:barCol,textAlign:"right",minWidth:28}}>{timeLeft}s</div>
        </div>
        {/* Timer bar */}
        <div style={{height:4,background:"#1e3a5f",borderRadius:2,overflow:"hidden",marginBottom:14}}>
          <div style={{height:"100%",width:pct+"%",background:barCol,
                       transition:"width 1s linear, background 0.3s"}}/>
        </div>
        {/* Buttons */}
        <div style={{display:"flex",gap:8}}>
          {upOk && <button onClick={function(){dispatch({type:"RESOLVE_PW_CHOICE",id:"up"});}}
            style={{flex:1,padding:"10px 0",fontSize:13,fontWeight:700,borderRadius:8,cursor:"pointer",
                    background:"#0a2010",border:"2px solid #4ade80",color:"#4ade80"}}>
            ▲ +{pwAmt}k</button>}
          {downOk && <button onClick={function(){dispatch({type:"RESOLVE_PW_CHOICE",id:"down"});}}
            style={{flex:1,padding:"10px 0",fontSize:13,fontWeight:700,borderRadius:8,cursor:"pointer",
                    background:"#200a0a",border:"2px solid #f87171",color:"#f87171"}}>
            ▼ -{pwAmt}k</button>}
          <button onClick={function(){dispatch({type:"RESOLVE_PW_CHOICE",id:"cancel"});}}
            style={{padding:"10px 14px",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                    background:"#1e293b",border:"1px solid #334155",color:"#64748b"}}>
            Skip</button>
        </div>
      </div>
    </div>
  );
}

function RebrandingModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "REBRANDING_SELECT") return null;
  var opts=pc.options||[];
  var bonus=pc.rebrandBonus||1;
  var [sels,setSels]=useState([]);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSels([]);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"none"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  function toggle(id){ setSels(function(prev){ return prev.includes(id)?prev.filter(function(x){return x!==id;}): [...prev,id]; }); }
  var totalGain = sels.length + bonus;
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#34d399":timeLeft>5?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #34d399",borderRadius:16,padding:24,maxWidth:480,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#34d399",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>REBRANDING</div>
      <div style={{fontSize:11,color:"#94a3b8",marginBottom:6}}>
        Select assets to shuffle back into the deck. You will receive <b style={{color:"#34d399"}}>{totalGain}</b> free asset{totalGain!==1?"s":""} in return.
        {opts.length===0&&<span style={{color:"#64748b"}}> (No other assets to trade — just +{bonus} free asset{bonus!==1?"s":""}.)</span>}
      </div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Auto: keep all</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/></div>
      {opts.length>0&&<div style={{display:"flex",flexDirection:"column",gap:6,maxHeight:220,overflowY:"auto",marginBottom:14}}>
        {opts.map(function(a){
          var isSel=sels.includes(a.id);
          return (<div key={a.id} onClick={function(){toggle(a.id);}}
            style={{background:isSel?"#0a2010":"#111827",border:"2px solid "+(isSel?"#34d399":"#1e293b"),
                    borderRadius:8,padding:"8px 12px",cursor:"pointer",transition:"all .12s"}}>
            <div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
              <span style={{fontFamily:"'Rubik',sans-serif",fontWeight:700,color:isSel?"#4ade80":"#f1f5f9",fontSize:12}}>{a.name}</span>
              <div style={{textAlign:"right"}}>
                <div style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:11}}>${a.origVal||a.value}k</div>
                {isSel&&<div style={{fontSize:8,color:"#34d399",fontWeight:700}}>↩ returning</div>}
              </div>
            </div>
          </div>);
        })}
      </div>}
      <div style={{background:"#071a10",border:"1px solid #166534",borderRadius:8,padding:"8px 12px",marginBottom:14,fontSize:10,color:"#4ade80"}}>
        Rebranding + {sels.length} asset{sels.length!==1?"s":""} returned → <b>+{totalGain}</b> free asset{totalGain!==1?"s":""}
      </div>
      <div style={{display:"flex",gap:8}}>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:sels.length>0?sels.join(","):"none"});}}
          style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#071a10",border:"2px solid #34d399",color:"#34d399"}}>
          {sels.length>0?"Confirm ("+sels.length+" returned, +"+totalGain+" gained)":"Proceed (keep all, +"+bonus+" gained)"}</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"none"});}}
          style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Skip</button>
      </div>
    </div>
  </div>);
}

function CFRSelectModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "CFR_SELECT") return null;
  var isOpp = pc.phase==="opponent";
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSel(null);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd,pc.phase]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#f97316":timeLeft>5?"#facc15":"#f87171";
  // Opponent list or asset list
  var opponents = isOpp ? st.players.filter(function(p){ return p.id!==pc.srcPlayer && p.assets.some(function(a){return !a.disabled;}); }) : [];
  var assets = isOpp ? [] : (pc.assets||[]);
  var selPlayer = !isOpp && pc.selectedOpponent ? st.players.find(function(p){return p.id===pc.selectedOpponent;}) : null;
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #f97316",borderRadius:16,padding:24,maxWidth:420,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f97316",fontFamily:"'Rubik',sans-serif",marginBottom:6}}>CLOSED FOR REMODELING</div>
      {isOpp ? (
        <div style={{fontSize:11,color:"#94a3b8",marginBottom:10}}>Select an opponent to target:</div>
      ) : (
        <div style={{fontSize:11,color:"#94a3b8",marginBottom:10}}>
          Select an asset of <b style={{color:"#f1f5f9"}}>{selPlayer?selPlayer.name:"?"}</b> to deactivate until your next turn:
        </div>
      )}
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}>
        <span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span>
      </div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}>
        <div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/>
      </div>
      <div style={{display:"flex",flexDirection:"column",gap:6,maxHeight:220,overflowY:"auto",marginBottom:14}}>
        {isOpp && opponents.map(function(p){
          var isSel=sel===p.id;
          return (<div key={p.id} onClick={function(){setSel(isSel?null:p.id);}}
            style={{background:isSel?"#1c1003":"#111827",border:"2px solid "+(isSel?"#f97316":"#1e293b"),
                    borderRadius:8,padding:"10px 14px",cursor:"pointer",transition:"all .12s",
                    display:"flex",justifyContent:"space-between",alignItems:"center"}}>
            <div style={{display:"flex",alignItems:"center",gap:8}}>
              <div style={{width:8,height:8,borderRadius:"50%",background:p.color||"#94a3b8"}}/>
              <span style={{fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",fontSize:13}}>{p.name}</span>
            </div>
            <div style={{fontSize:9,color:"#64748b"}}>{p.assets.length} asset{p.assets.length!==1?"s":""}</div>
          </div>);
        })}
        {!isOpp && assets.map(function(a){
          var isSel=sel===a.id;
          var sub=SUB_UI[a.sub]||SUB_UI[AS.DURING];
          return (<div key={a.id} onClick={function(){setSel(isSel?null:a.id);}}
            style={{background:isSel?"#1c1003":"#111827",border:"2px solid "+(isSel?"#f97316":"#1e293b"),
                    borderRadius:8,padding:"10px 14px",cursor:"pointer",transition:"all .12s"}}>
            <div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
              <div>
                <div style={{fontSize:7,color:sub.col,fontWeight:700,background:sub.bg,borderRadius:3,padding:"1px 4px",display:"inline-block",marginBottom:2}}>{sub.lbl}</div>
                <div style={{fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",fontSize:12}}>{a.name}</div>
              </div>
              <div style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:12,fontWeight:700}}>${a.origVal}k</div>
            </div>
          </div>);
        })}
      </div>
      <div style={{display:"flex",gap:8}}>
        {!isOpp && <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"back"});}}
          style={{padding:"10px 14px",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>← Back</button>}
        <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}}
          style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",
                  background:sel?"#1c1003":"#1e293b",border:"2px solid "+(sel?"#f97316":"#334155"),color:sel?"#f97316":"#475569"}}>
          {isOpp?"Select Player":"Deactivate Asset"}</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}}
          style={{padding:"10px 12px",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Skip</button>
      </div>
    </div>
  </div>);
}

function BriberyAssetModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "BRIBERY_ASSET_SELECT") return null;
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSel(null);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[];
  var tokens=pc.currentTokens||0;
  var cost=pc.bribeTokenCost||1;
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#f59e0b":timeLeft>5?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #f59e0b",borderRadius:16,padding:24,maxWidth:480,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>🤑 BRIBERY — SELECT ASSET</div>
      <div style={{fontSize:11,color:"#94a3b8",marginBottom:4}}>Choose an asset to steal. You must discard cards exceeding that asset's value + ${cost}k per token ({tokens} tokens = +${tokens*cost}k).</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",flexDirection:"column",gap:6,maxHeight:260,overflowY:"auto",marginBottom:14}}>
        {opts.map(function(a){
          var threshold=a._threshold||0;
          var isSel=sel===a.id;
          return (<div key={a.id} onClick={function(){setSel(isSel?null:a.id);}}
            style={{background:isSel?"#1c1003":"#111827",border:"2px solid "+(isSel?"#f59e0b":"#1e293b"),
                    borderRadius:8,padding:"8px 12px",cursor:"pointer",transition:"all .12s"}}>
            <div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
              <div>
                <span style={{fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",fontSize:12}}>{a.name}</span>
                <span style={{fontSize:9,color:"#64748b",marginLeft:6}}>owned by {a._ownerName}</span>
              </div>
              <div style={{textAlign:"right"}}>
                <div style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:11,fontWeight:700}}>${a.origVal||a.value}k</div>
                <div style={{fontSize:8,color:"#ef4444"}}>Need: &gt;${threshold}k</div>
              </div>
            </div>
          </div>);
        })}
      </div>
      <div style={{display:"flex",gap:8}}>
        <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}}
          style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",
                  background:sel?"#1c1003":"#1e293b",border:"2px solid "+(sel?"#f59e0b":"#334155"),color:sel?"#f59e0b":"#475569"}}>
          Select Asset</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}}
          style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Cancel</button>
      </div>
    </div>
  </div>);
}

function BriberyCardModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "BRIBERY_CARD_SELECT") return null;
  var [sels,setSels]=useState([]);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSels([]);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var opts=pc.options||[];
  var threshold=pc.threshold||0;
  var total=sels.reduce(function(s,id){ var c=opts.find(function(x){return x.id===id;}); return s+(c?c.value||0:0); },0);
  var canConfirm=total>threshold;
  function toggle(id){ setSels(function(prev){ return prev.includes(id)?prev.filter(function(x){return x!==id;}): [...prev,id]; }); }
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#f59e0b":timeLeft>5?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #f59e0b",borderRadius:16,padding:24,maxWidth:480,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>🤑 BRIBERY — DISCARD CARDS</div>
      <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:8}}>
        <div style={{fontSize:11,color:"#94a3b8"}}>Stealing: <b style={{color:"#f1f5f9"}}>{pc.targetAssetName}</b> (${pc.targetAssetVal}k)</div>
        <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:11,color:canConfirm?"#4ade80":"#f87171",fontWeight:700}}>
          ${total}k / &gt;${threshold}k
        </div>
      </div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",flexWrap:"wrap",gap:6,maxHeight:220,overflowY:"auto",marginBottom:14}}>
        {opts.map(function(c){
          var isSel=sels.includes(c.id);
          return (<div key={c.id} onClick={function(){toggle(c.id);}}
            style={{background:isSel?"#1c1003":"#111827",border:"2px solid "+(isSel?"#f59e0b":"#1e293b"),
                    borderRadius:8,padding:"6px 10px",cursor:"pointer",minWidth:90,transition:"all .12s"}}>
            <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",gap:8}}>
              <span style={{fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:600}}>{c.name}</span>
              <span style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:11,fontWeight:700}}>${c.value||c.origVal}k</span>
            </div>
            {isSel&&<div style={{fontSize:8,color:"#f59e0b",fontWeight:700,marginTop:2}}>✓ discarding</div>}
          </div>);
        })}
      </div>
      <div style={{display:"flex",gap:8}}>
        <button onClick={function(){if(canConfirm)dispatch({type:"RESOLVE_CHOICE",id:sels.join(",")});}}
          style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:canConfirm?"pointer":"not-allowed",
                  background:canConfirm?"#1c1003":"#1e293b",border:"2px solid "+(canConfirm?"#4ade80":"#334155"),color:canConfirm?"#4ade80":"#475569"}}>
          {canConfirm?"Confirm Discard":"Need >"+(threshold)+"k total"}</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}}
          style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Cancel</button>
      </div>
    </div>
  </div>);
}

function MarkupDirectionModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "MARKUP_DIRECTION") return null;
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+5000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/5)*100; var tCol=timeLeft>3?"#f59e0b":timeLeft>1?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #f59e0b",borderRadius:16,padding:24,maxWidth:340,width:"92%",textAlign:"center"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:8}}>MARK-UP</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:10}}>Increase or decrease a card's value by ${pc.markUpAmount||1}k?</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Auto-selects: Increase</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:16}}><div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",gap:10}}>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"increase"});}}
          style={{flex:1,padding:"12px 0",fontSize:13,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#0a2010",border:"2px solid #4ade80",color:"#4ade80"}}>
          ▲ Increase</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"decrease"});}}
          style={{flex:1,padding:"12px 0",fontSize:13,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#200a0a",border:"2px solid #f87171",color:"#f87171"}}>
          ▼ Decrease</button>
      </div>
    </div>
  </div>);
}

function ThreeOfAKindModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "THREE_OF_KIND_SELECT") return null;
  var need = pc.tripleCount || 3;
  var reqVal = pc.tripleValue || 2;
  var cards = pc.options || [];
  var [sels,setSels]=useState([]);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){setSels([]);setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var selCount=sels.length;
  function toggle(cid) {
    setSels(function(prev){
      if (prev.includes(cid)) return prev.filter(function(id){return id!==cid;});
      if (prev.length>=need) return prev;
      return [...prev,cid];
    });
  }
  var canConfirm = selCount===need;
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#a78bfa":timeLeft>5?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #a78bfa",borderRadius:16,padding:24,maxWidth:480,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#a78bfa",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>THREE OF A KIND</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:10}}>Select {need} cards worth <span style={{color:"#f59e0b",fontWeight:700}}>${reqVal}k</span> each to discard. Gain the top asset for free.{selCount>0?" ("+selCount+"/"+need+" selected)":""}</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/></div>
      <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:16,maxHeight:240,overflowY:"auto"}}>
        {cards.map(function(c){
          var isSel=sels.includes(c.id);
          var isValid=c.value===reqVal;
          var canSelect=isValid&&(isSel||selCount<need);
          var dim=!isValid;
          return (<div key={c.id} onClick={canSelect||isSel?function(){toggle(c.id);}:undefined}
            style={{background:isSel?"#1a0d3a":"#111827",border:"2px solid "+(isSel?"#a78bfa":isValid?"#4b5563":"#1e293b"),
                    borderRadius:8,padding:"8px 10px",cursor:canSelect||isSel?"pointer":"default",
                    opacity:dim?0.35:1,minWidth:90,transition:"all .12s"}}>
            <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:2}}>
              <span style={{fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",fontSize:11}}>{c.name}</span>
              <span style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:11,fontWeight:700}}>${c.value||c.origVal}k</span>
            </div>
            {isSel&&<div style={{fontSize:8,color:"#a78bfa",fontWeight:700}}>✓ Selected</div>}
          </div>);
        })}
      </div>
      <div style={{display:"flex",gap:8}}>
        <button onClick={function(){if(canConfirm)dispatch({type:"RESOLVE_CHOICE",id:sels.join(",")});}}
          style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:canConfirm?"pointer":"not-allowed",
                  background:canConfirm?"#150d2e":"#1e293b",border:"2px solid "+(canConfirm?"#a78bfa":"#334155"),color:canConfirm?"#a78bfa":"#475569"}}>
          Discard {need} & Claim Asset</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}}
          style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Cancel</button>
      </div>
    </div>
  </div>);
}

function SneakPeakModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "SNEAK_PEAK_SELECT") return null;
  var cards=pc.cards||[];
  var [sel,setSel]=useState(null);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+20000)-Date.now())/1000)));
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/20)*100; var tCol=timeLeft>10?"#67e8f9":timeLeft>5?"#facc15":"#f87171";
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.85)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:900,backdropFilter:"blur(4px)"}}>
    <div style={{background:"#0d1520",border:"2px solid #67e8f9",borderRadius:16,padding:24,maxWidth:460,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#67e8f9",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>🔍 SNEAK PEAK</div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:10}}>Choose which card to place on top of the deck. The other goes 2nd.</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:pct+"%",background:tCol,transition:"width 1s linear"}}/></div>
      {cards.length===0&&<div style={{color:"#475569",textAlign:"center",marginBottom:14}}>Deck is empty.</div>}
      <div style={{display:"flex",flexDirection:"column",gap:8,marginBottom:16}}>
        {cards.map(function(c,i){
          var isSel=sel===c.id;
          return (<div key={c.id} onClick={function(){setSel(isSel?null:c.id);}}
            style={{background:isSel?"#0a2235":"#111827",border:"2px solid "+(isSel?"#67e8f9":"#1e293b"),
                    borderRadius:10,padding:"10px 14px",cursor:"pointer",transition:"all .15s"}}>
            <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:4}}>
              <span style={{fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",fontSize:13}}>{c.name}</span>
              <span style={{fontFamily:"'JetBrains Mono',monospace",color:"#f59e0b",fontSize:13,fontWeight:700}}>${c.value||c.origVal}k</span>
            </div>
            <div style={{fontSize:9,color:"#64748b",lineHeight:1.4}}>{getDesc(c)||c.desc||""}</div>
            {isSel&&<div style={{marginTop:6,fontSize:9,color:"#67e8f9",fontWeight:700}}>▲ Place on TOP</div>}
          </div>);
        })}
      </div>
      <div style={{display:"flex",gap:8}}>
        <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}}
          style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",
                  background:sel?"#0a2235":"#1e293b",border:"2px solid "+(sel?"#67e8f9":"#334155"),color:sel?"#67e8f9":"#475569"}}>
          Confirm</button>
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}}
          style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",
                  background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Keep Order</button>
      </div>
    </div>
  </div>);
}

function RecycleChoiceModal({st,dispatch}) {
  var pc=st.pendingChoice;
  if (!pc || pc.type !== "RECYCLE_CHOICE") return null;
  var card = pc.topCard || (pc.options && pc.options[0]);
  var [timeLeft,setTimeLeft]=useState(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+15000)-Date.now())/1000)));
  useEffect(function(){setTimeLeft(Math.max(1,Math.ceil(((pc.timerEnd||Date.now()+15000)-Date.now())/1000)));}, [pc.timerEnd]);
  useEffect(function(){if(timeLeft<=0){dispatch({type:"RESOLVE_CHOICE",id:"auto"});return;}var t=setTimeout(function(){setTimeLeft(function(n){return Math.max(0,n-1);});},1000);return function(){clearTimeout(t);};}, [timeLeft]);
  var pct=(timeLeft/15)*100; var tCol=timeLeft>8?"#34d399":timeLeft>4?"#facc15":"#f87171";
  var rcLeft = pc.recycleCount||1;
  return (<div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
    <div style={{background:"#1a1410",border:"2px solid #34d399",borderRadius:14,padding:24,maxWidth:340,width:"92%"}}>
      <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#34d399",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>
        ♻ RECYCLE{rcLeft>1?" ("+rcLeft+" draws)":""}
      </div>
      <div style={{fontSize:12,color:"#94a3b8",marginBottom:10}}>Draw the top discard card instead of from the deck?</div>
      <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:tCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
      <div style={{height:3,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:12}}><div style={{height:"100%",width:pct+"%",background:tCol,borderRadius:2,transition:"width 1s linear"}}/></div>
      {card&&<div style={{background:"#0d1a0d",border:"1px solid #16a34a",borderRadius:10,padding:12,marginBottom:14}}>
        <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:6}}>
          <div style={{fontFamily:"'Rubik',sans-serif",fontSize:14,color:"#f1f5f9",fontWeight:700}}>{card.name}</div>
          <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:14,color:"#f59e0b",fontWeight:700}}>${card.value||card.origVal}k</div>
        </div>
        <div style={{fontSize:8,color:"#64748b",lineHeight:1.4}}>{card.desc||getDesc(card)||""}</div>
      </div>}
      {!card&&<div style={{color:"#475569",fontSize:11,marginBottom:14,textAlign:"center"}}>Discard pile is empty.</div>}
      <div style={{display:"flex",gap:8}}>
        {card&&<button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:card.id});}} style={{flex:2,padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#0d2010",border:"2px solid #34d399",color:"#4ade80"}}>Take It</button>}
        <button onClick={function(){dispatch({type:"RESOLVE_CHOICE",id:"cancel"});}} style={{flex:1,padding:"10px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Skip</button>
      </div>
    </div>
  </div>);
}


function HelpWantedModal({ st, dispatch }) {
  var pc = st.pendingChoice;
  if (!pc || pc.type !== "HELP_WANTED_GIVE") return null;
  var [sel, setSel] = useState(null);
  var [timeLeft, setTimeLeft] = useState(Math.max(1, Math.ceil(((pc.timerEnd||Date.now()+20000) - Date.now()) / 1000)));
  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0,n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);
  var tgtPl = st.players.find(function(p){ return p.id === pc.tgtPlayer; });
  var viewPl = st.players[st.viewIdx];
  var isMe = viewPl && viewPl.id === pc.tgtPlayer;
  var hand = tgtPl ? tgtPl.hand : [];
  var timerPct = (timeLeft/20)*100;
  var timerCol = timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";
  return (
    <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
      <div style={{background:"#1a1410",border:"2px solid #f59e0b",borderRadius:14,padding:24,maxWidth:520,width:"92%"}}>
        <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f59e0b",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>HELP WANTED</div>
        <div style={{fontSize:14,color:"#f1f5f9",fontWeight:700,marginBottom:6}}>{isMe ? "Choose a card to give" : (tgtPl ? tgtPl.name+" is choosing..." : "Waiting...")}</div>
        <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4}}><span>Time</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
        <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:timerPct+"%",background:timerCol,transition:"width 1s linear"}}/></div>
        {isMe ? (
          <div>
            <div style={{display:"flex",flexWrap:"wrap",gap:8,marginBottom:14}}>
              {hand.map(function(card){
                var isSel = sel===card.id;
                var col = card.type==="ACTION"?"#93c5fd":card.type==="REACTION"?"#f87171":"#4ade80";
                return (<div key={card.id} onClick={function(){setSel(isSel?null:card.id);}} style={{width:110,border:isSel?"2px solid "+col:"1px solid "+col+"44",borderRadius:8,background:isSel?"#0c1a2e":"#1e1a14",padding:"8px 10px",cursor:"pointer",textAlign:"center",position:"relative"}}>
                  {isSel&&<div style={{position:"absolute",top:4,right:6,fontSize:10,color:col,fontWeight:700}}>+</div>}
                  <div style={{fontSize:7,color:col,fontWeight:700,letterSpacing:.5,marginBottom:2}}>{card.type}</div>
                  <div style={{fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700}}>{card.name}</div>
                  <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:"#f59e0b",fontWeight:700,marginTop:3}}>${card.value}k</div>
                </div>);
              })}
            </div>
            <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel}
              style={{width:"100%",padding:"10px 0",fontSize:12,fontWeight:700,borderRadius:8,cursor:sel?"pointer":"not-allowed",background:sel?"#1c1003":"#1e293b",border:"2px solid "+(sel?"#f59e0b":"#334155"),color:sel?"#fcd34d":"#475569"}}>
              {sel?"Give This Card":"Select a Card to Give"}
            </button>
          </div>
        ) : (
          <div style={{textAlign:"center",padding:"20px 0",color:"#64748b",fontSize:13}}>Waiting for {tgtPl?tgtPl.name:"player"} to choose...</div>
        )}
      </div>
    </div>
  );
}

function LetGoSelectModal({ st, dispatch }) {
  var pc = st.pendingChoice;
  if (!pc || pc.type !== "LET_GO_SELECT") return null;
  var [sel, setSel] = useState(null);
  var timerEnd = pc.timerEnd || (Date.now()+20000);
  var [timeLeft, setTimeLeft] = useState(Math.max(1, Math.ceil((timerEnd - Date.now()) / 1000)));
  useEffect(function(){ setSel(null); setTimeLeft(Math.max(1,Math.ceil((timerEnd-Date.now())/1000))); }, [pc.tgtPlayer, pc.timerEnd]);
  useEffect(function() {
    if (timeLeft <= 0) { dispatch({ type:"RESOLVE_CHOICE", id:"auto" }); return; }
    var t = setTimeout(function() { setTimeLeft(function(n){ return Math.max(0,n-1); }); }, 1000);
    return function() { clearTimeout(t); };
  }, [timeLeft]);
  var tgtPl = st.players.find(function(p){ return p.id === pc.tgtPlayer; });
  var srcPl = st.players.find(function(p){ return p.id === pc.srcPlayer; });
  var viewPl = st.players[st.viewIdx];
  var isMe = viewPl && viewPl.id === pc.tgtPlayer;
  var liveHand = tgtPl ? tgtPl.hand : (pc.options||[]);
  var timerPct = (timeLeft/20)*100;
  var timerCol = timeLeft>10?"#4ade80":timeLeft>5?"#facc15":"#f87171";
  return (
    <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:800,backdropFilter:"blur(3px)"}}>
      <div style={{background:"#1a1410",border:"2px solid #f87171",borderRadius:14,padding:24,maxWidth:520,width:"92%",boxShadow:"0 0 40px #f8717144"}}>
        <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#f87171",fontFamily:"'Rubik',sans-serif",marginBottom:4}}>LET GO</div>
        <div style={{fontSize:16,fontFamily:"'Rubik',sans-serif",fontWeight:700,color:"#f1f5f9",marginBottom:4}}>{isMe?"Choose a card to discard":(tgtPl?tgtPl.name+" is choosing...":"Waiting...")}</div>
        <div style={{fontSize:11,color:"#94a3b8",marginBottom:8}}>{srcPl?srcPl.name+" played Let Go. ":""}{isMe?"You must discard a card and lose its value.":"Waiting for "+(tgtPl?tgtPl.name:"player")+" to decide."}</div>
        <div style={{display:"flex",justifyContent:"space-between",fontSize:10,color:timerCol,marginBottom:4}}><span>Time remaining</span><span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700}}>{timeLeft}s</span></div>
        <div style={{height:4,background:"#1e293b",borderRadius:2,overflow:"hidden",marginBottom:14}}><div style={{height:"100%",width:timerPct+"%",background:timerCol,transition:"width 1s linear"}}/></div>
        {isMe ? (
          <div>
            <div style={{display:"flex",flexWrap:"wrap",gap:8,justifyContent:"center",marginBottom:16}}>
              {liveHand.map(function(card){
                var isSel = sel===card.id;
                var col = card.type==="ACTION"?"#93c5fd":card.type==="REACTION"?"#f87171":"#4ade80";
                return (<div key={card.id} onClick={function(){setSel(card.id);}} style={{width:112,border:isSel?"2px solid #f87171":"1px solid "+col+"44",borderRadius:8,background:isSel?"#2a0a0a":"#1e1a14",padding:"8px 10px",cursor:"pointer",boxShadow:isSel?"0 0 14px #f8717144":"none",transition:"all .12s",textAlign:"center",position:"relative"}}>
                  {isSel&&<div style={{position:"absolute",top:5,right:7,fontSize:11,color:"#f87171",fontWeight:700}}>+</div>}
                  <div style={{fontSize:8,color:col,fontWeight:700,letterSpacing:1,marginBottom:3}}>{card.type}</div>
                  <div style={{fontFamily:"'Rubik',sans-serif",fontSize:11,color:"#f1f5f9",fontWeight:700,lineHeight:1.3}}>{card.name}</div>
                  <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:"#f59e0b",fontWeight:700,marginTop:4}}>${card.value}k</div>
                  {isSel&&<div style={{fontSize:9,color:"#f87171",marginTop:3}}>will lose ${card.value}k</div>}
                </div>);
              })}
            </div>
            <button onClick={function(){if(sel)dispatch({type:"RESOLVE_CHOICE",id:sel});}} disabled={!sel}
              style={{width:"100%",padding:"11px 0",fontSize:13,fontWeight:700,letterSpacing:1,cursor:sel?"pointer":"not-allowed",borderRadius:8,background:sel?"#2a0a0a":"#1e293b",border:"2px solid "+(sel?"#f87171":"#334155"),color:sel?"#fca5a5":"#475569"}}>
              {sel?"Discard This Card":"Select a Card to Discard..."}
            </button>
          </div>
        ) : (<div style={{textAlign:"center",padding:"20px 0",color:"#64748b",fontSize:13}}>Waiting for {tgtPl?tgtPl.name:"player"} to choose...</div>)}
      </div>
    </div>
  );
}


/* MOBILE GAME SCREEN */
function MobileGameScreen({ st, dispatch, setSettings, isMultiplayer }) {
  var cp = curPl(st);
  var vp = st.players[st.viewIdx];
  var TABS = ["main","assets","shop","log"];
  var [tab, setTab] = useState("main");
  var [selId, setSelId] = useState(null);
  var [endConfirm, setEndConfirm] = useState(false);
  var [confirmEndGame, setConfirmEndGame] = useState(false);
  var [oppPopup, setOppPopup] = useState(null);
  var [dragX, setDragX] = useState(0);
  var [isDragging, setIsDragging] = useState(false);
  var [showInGameTutorial, setShowInGameTutorial] = useState(false);
  var [showBkPopup, setShowBkPopup] = useState(false);
  var [moneyDeltas, setMoneyDeltas] = useState([]);
  var touchStartX = useRef(null);
  var touchStartY = useRef(null);
  var dragLockedRef = useRef(false);
  var prevMoneyRef = useRef(null);
  var prevOppMoneyRef = useRef({});
  var [displayMoney, setDisplayMoney] = useState(null); // null = uninitialized
  var displayMoneyValRef = useRef(null);
  var displayMoneyAnimRef = useRef(null);
  var tabContentRef = useRef(null);
  var tabIdx = TABS.indexOf(tab);

  useEffect(function() {
    if (st.startTurnPending && st.phase === PH.SOT) {
      var t = setTimeout(function() { dispatch({ type:"START_TURN" }); }, 400);
      return function() { clearTimeout(t); };
    }
  }, [st.curIdx, st.startTurnPending]);

  // Deselect card when the active player changes
  useEffect(function() { setSelId(null); setEndConfirm(false); }, [cp.id]);

  // Reset money delta tracking when viewed player switches
  useEffect(function() {
    prevMoneyRef.current = null;
    if (displayMoneyAnimRef.current) cancelAnimationFrame(displayMoneyAnimRef.current);
    setDisplayMoney(vp.money);
    displayMoneyValRef.current = vp.money;
  }, [vp.id]);

  // Counting animation for own balance
  useEffect(function() {
    if (displayMoneyValRef.current === null) {
      setDisplayMoney(vp.money);
      displayMoneyValRef.current = vp.money;
      return;
    }
    var target = vp.money;
    var start = displayMoneyValRef.current;
    var diff = target - start;
    if (Math.abs(diff) <= 1) {
      setDisplayMoney(target);
      displayMoneyValRef.current = target;
      return;
    }
    var duration = Math.min(650, 100 + Math.abs(diff) * 55);
    var startTime = Date.now();
    function tick() {
      var elapsed = Date.now() - startTime;
      var progress = Math.min(1, elapsed / duration);
      var eased = 1 - Math.pow(1 - progress, 2);
      var current = Math.round(start + diff * eased);
      setDisplayMoney(current);
      displayMoneyValRef.current = current;
      if (progress < 1) {
        displayMoneyAnimRef.current = requestAnimationFrame(tick);
      } else {
        setDisplayMoney(target);
        displayMoneyValRef.current = target;
      }
    }
    if (displayMoneyAnimRef.current) cancelAnimationFrame(displayMoneyAnimRef.current);
    displayMoneyAnimRef.current = requestAnimationFrame(tick);
    return function() { if (displayMoneyAnimRef.current) cancelAnimationFrame(displayMoneyAnimRef.current); };
  }, [vp.money]);

  // Animated money delta (own balance + opponents)
  useEffect(function() {
    if (prevMoneyRef.current === null) {
      prevMoneyRef.current = vp.money;
      var init = {};
      (st.players||[]).forEach(function(p){ init[p.id] = p.money; });
      prevOppMoneyRef.current = init;
      return;
    }
    var now = Date.now();
    var newDeltas = [];
    var myDelta = vp.money - prevMoneyRef.current;
    if (myDelta !== 0) newDeltas.push({ id:now+Math.random(), pid:vp.id, delta:myDelta });
    prevMoneyRef.current = vp.money;
    (st.players||[]).forEach(function(p) {
      if (p.id === vp.id) return;
      var prev = prevOppMoneyRef.current[p.id];
      if (prev !== undefined && p.money !== prev) {
        newDeltas.push({ id:now+Math.random(), pid:p.id, delta:p.money - prev });
      }
      prevOppMoneyRef.current[p.id] = p.money;
    });
    if (!newDeltas.length) return;
    setMoneyDeltas(function(d){ return [...d, ...newDeltas]; });
    var ids = newDeltas.map(function(d){ return d.id; });
    var t = setTimeout(function(){
      setMoneyDeltas(function(d){ return d.filter(function(x){ return ids.indexOf(x.id)<0; }); });
    }, 2000);
    return function(){ clearTimeout(t); };
  }, [vp.money, st.players.map(function(p){ return p.id+":"+p.money; }).join(",")]);

  var isCP = vp.id === cp.id;
  var isPlanning = st.phase === PH.BP;
  var at = st.actionsLeft;
  var taken = st.actionsTaken || [];
  var canDraw   = isCP && isPlanning && at > 0 && (!taken.includes(ACT.DRAW)   || st.freeActions);
  var canSell   = isCP && isPlanning && at > 0 && (!taken.includes(ACT.SELL)   || st.freeActions);
  var canAction = isCP && isPlanning && at > 0 && (!taken.includes(ACT.ACTION) || st.freeActions || (st.rwActionsBonus||0) > 0) && !vp.actionLocked;
  var hand    = vp.hand || [];
  var assets  = vp.assets || [];
  var shop    = st.shop || [];
  var selCard = selId ? hand.find(function(c){ return c.id === selId; }) : null;
  var topDiscard = st.mainDiscard.length > 0 ? st.mainDiscard[st.mainDiscard.length-1] : null;
  var overdraft  = calcOverdraft(vp);
  var opponents  = st.players.filter(function(p){ return p.id !== vp.id; });

  // Action availability for selected card
  var canPlay  = !!(selCard && canAction && selCard.type === CT.ACTION);
  var canPlayR = !!(selCard && selCard.type === CT.REACTION && selCard.hook === "minor_loss");
  var canSellC = !!(selCard && canSell && selCard.type !== CT.ASSET);

  // Why a hand card can't currently be played (Monopoly lock, Part Time Work, etc.)
  var monoBlockedSet = monopolyBlockedValues(st, vp.id);
  function unplayableReason(c) {
    if (!(isCP && isPlanning)) return null;
    if (c.type === CT.ACTION) {
      if (vp.actionLocked) return "🔒 Part Time Work is preventing you from playing Action cards this turn.";
      if (monoBlockedSet.size > 0 && monoBlockedSet.has(c.value)) return "🚫 Monopoly is blocking $"+c.value+"k cards from being played this turn.";
    }
    return null;
  }

  // Pressure token helpers
  function totalPressure(p) {
    return Object.values(p.pressureTokens||{}).reduce(function(s,v){return s+v;},0);
  }
  var myPressure = totalPressure(vp);

  // Touch swipe handlers (touch-action:pan-y lets browser handle vertical natively)
  function onTouchStart(e) {
    touchStartX.current = e.touches[0].clientX;
    touchStartY.current = e.touches[0].clientY;
    dragLockedRef.current = false;
    setIsDragging(true);
  }
  function onTouchMove(e) {
    if (dragLockedRef.current || touchStartX.current === null) return;
    var dx = e.touches[0].clientX - touchStartX.current;
    var dy = e.touches[0].clientY - touchStartY.current;
    if (Math.abs(dy) > Math.abs(dx) * 1.1 && Math.abs(dy) > 5) {
      dragLockedRef.current = true;
      setDragX(0);
      return;
    }
    // Rubber-band at edges
    var raw = dx;
    if (tabIdx === 0 && raw > 0) raw = Math.min(raw * 0.25, 40);
    if (tabIdx === TABS.length-1 && raw < 0) raw = Math.max(raw * 0.25, -40);
    setDragX(raw);
  }
  function onTouchEnd() {
    setIsDragging(false);
    if (!dragLockedRef.current) {
      if (dragX < -55 && tabIdx < TABS.length-1) setTab(TABS[tabIdx+1]);
      else if (dragX > 55 && tabIdx > 0) setTab(TABS[tabIdx-1]);
    }
    setDragX(0);
    touchStartX.current = null;
    dragLockedRef.current = false;
  }

  // Action handlers
  function handleDraw() { dispatch({ type:"DRAW" }); }
  function handlePlay() {
    if (!selCard) return;
    if (selCard.target === "opponent") { dispatch({ type:"AWAIT_TARGET", cardId:selCard.id }); setSelId(null); }
    else { dispatch({ type:"PLAY_ACTION", p:{ cardId:selCard.id } }); setSelId(null); }
  }
  function handlePlayReaction() { if (selCard) { dispatch({ type:"PLAY_REACTION", p:{ cardId:selCard.id, ownerId:vp.id } }); setSelId(null); } }
  function handleSell() { if (selCard) { dispatch({ type:"SELL", p:{ cardId:selCard.id } }); setSelId(null); } }
  function handleActivate(assetId) { dispatch({ type:"ACTIVATE", p:{ assetId:assetId, ownerId:vp.id } }); }
  function handleBlindBuy() { dispatch({ type:"BUY_DECK" }); }
  const [exitingShopItems, setExitingShopItems] = useState({});
  function handleBuyShop(assetId) {
    var asset = shop ? shop.find(function(a){ return a.id===assetId; }) : null;
    if (asset) {
      setExitingShopItems(function(prev){ return { ...prev, [assetId]:asset }; });
      setTimeout(function(){
        setExitingShopItems(function(prev){ var n={...prev}; delete n[assetId]; return n; });
      }, 950);
    }
    dispatch({ type:"BUY_SHOP", p:{ assetId:assetId } });
  }
  function handleEndTurn() { dispatch({ type:"END_TURN" }); setEndConfirm(false); }
  function handleCloseDetail() { dispatch({ type:"CLOSE_DETAIL" }); }
  function handleDetailCard(card) { dispatch({ type:"OPEN_DETAIL", card:card }); }

  // In-game tutorial shortcut
  var TutorialComp = (typeof window!=="undefined" && window.SB_EXPORTS && window.SB_EXPORTS.Tutorial) || null;
  if (showInGameTutorial && TutorialComp) {
    return <TutorialComp onExit={function(){ setShowInGameTutorial(false); }} />;
  }

  // Card type colors helper
  var TC = {
    ACTION:   { bg:"#0c1833", border:"#2563eb", text:"#60a5fa" },
    REACTION: { bg:"#1f0a0a", border:"#dc2626", text:"#f87171" },
    ASSET:    { bg:"#0d1a0d", border:"#16a34a", text:"#4ade80" },
  };

  return (
    <div className="sb-bg-blue" style={{ position:"fixed", inset:0, color:C.text,
                  display:"flex", flexDirection:"column", maxWidth:480, margin:"0 auto",
                  fontFamily:"system-ui,sans-serif", overflow:"hidden" }}>
      <style>{CSS}</style>

      {/* ── 1. HEADER ── */}
      <div style={{ flexShrink:0, height:52, background:"#0d1520",
                    borderBottom:"1px solid "+C.border,
                    display:"flex", alignItems:"center", padding:"0 12px", gap:8 }}>
        <div style={{ fontFamily:"'Rubik',sans-serif", fontSize:18, fontWeight:900,
                      letterSpacing:1, flexShrink:0 }}>
          <span style={{ color:"#4ade80" }}>S</span><span style={{ color:"#f87171" }}>B</span>
        </div>
        <div style={{ flex:1, textAlign:"center" }}>
          <div style={{ fontFamily:"'Rubik',sans-serif", fontSize:13, fontWeight:700, color:cp.color, letterSpacing:.5 }}>
            {cp.name.toUpperCase()}'S TURN
          </div>
          <div style={{ fontSize:9, color:C.muted, letterSpacing:.3 }}>
            {st.turnsLeft} turn{st.turnsLeft!==1?"s":""} remaining · round {st.roundNum}
          </div>
        </div>
        <div style={{ display:"flex", gap:5, flexShrink:0 }}>
          {TutorialComp && (
            <button onClick={function(){ setShowInGameTutorial(true); }}
              style={{ padding:"5px 9px", fontSize:11, fontWeight:900, borderRadius:5, cursor:"pointer",
                       background:"#0d1520", border:"1px solid #334155", color:"#64748b",
                       lineHeight:1, display:"flex", alignItems:"center", justifyContent:"center",
                       width:28, height:28 }}>
              ?
            </button>
          )}
          <button onClick={function(){ if(setSettings) setSettings(function(s){ return {...s,mobileView:false}; }); }}
            style={{ padding:"5px 9px", fontSize:9, fontWeight:700, borderRadius:5, cursor:"pointer",
                     background:"#1e293b", border:"1px solid #334155", color:"#64748b", letterSpacing:.5 }}>
            DESKTOP
          </button>
          {!confirmEndGame
            ? <button onClick={function(){ setConfirmEndGame(true); }}
                style={{ padding:"5px 9px", fontSize:9, fontWeight:700, borderRadius:5, cursor:"pointer",
                         background:"#1c0a0a", border:"1px solid #7f1d1d", color:"#f87171", letterSpacing:.5 }}>
                END GAME
              </button>
            : <div style={{ display:"flex", gap:3, alignItems:"center", background:"#1c0a0a",
                            border:"1px solid #7f1d1d", borderRadius:5, padding:"4px 7px" }}>
                <span style={{ fontSize:8, color:"#fca5a5" }}>End?</span>
                <button onClick={function(){ dispatch({type:"FORCE_END_GAME"}); setConfirmEndGame(false); }}
                  style={{ padding:"2px 7px", fontSize:9, fontWeight:700, borderRadius:3, cursor:"pointer",
                           background:"#7f1d1d", border:"1px solid #ef4444", color:"#fca5a5" }}>Yes</button>
                <button onClick={function(){ setConfirmEndGame(false); }}
                  style={{ padding:"2px 6px", fontSize:9, fontWeight:700, borderRadius:3, cursor:"pointer",
                           background:"#1e293b", border:"1px solid #334155", color:"#64748b" }}>No</button>
              </div>
          }
        </div>
      </div>

      {/* ── 1b. SINGLE-DEVICE PLAYER SWITCHER ── */}
      {!isMultiplayer && (
        <div style={{ flexShrink:0, background:"#080d18", borderBottom:"1px solid "+C.border,
                      padding:"5px 10px", display:"flex", alignItems:"center", gap:6,
                      overflowX:"auto", scrollbarWidth:"none" }}>
          <div style={{ fontSize:8, color:"#475569", letterSpacing:.5, flexShrink:0, fontWeight:700 }}>
            VIEWING
          </div>
          {st.players.map(function(p, i) {
            var isViewing = vp.id === p.id;
            var isActive  = cp.id === p.id;
            return (
              <button key={p.id}
                onClick={function(){ dispatch({ type:"SET_VIEW", i:i }); }}
                style={{ flexShrink:0, padding:"3px 10px", borderRadius:12,
                         fontSize:10, fontWeight:700, cursor:"pointer",
                         fontFamily:"'Rubik',sans-serif", letterSpacing:.3,
                         background: isViewing ? (isActive?"#1c2a14":"#0d1520") : "#0a0f1a",
                         border:"1.5px solid "+(isViewing?(isActive?"#4ade80":"#38bdf8"):"#1e2d45"),
                         color: isViewing?(isActive?"#4ade80":"#7dd3fc"):(isActive?"#d97706":"#475569"),
                         transition:"all .1s" }}>
                {p.name}
                {isActive && <span style={{ fontSize:7, marginLeft:4, opacity:.8 }}>●</span>}
              </button>
            );
          })}
        </div>
      )}

      {/* ── 2. OPPONENT STRIP ── */}
      <div style={{ flexShrink:0, height:96, borderBottom:"1px solid "+C.border,
                    overflowX:"auto", overflowY:"hidden", scrollbarWidth:"none",
                    display:"flex", alignItems:"center", padding:"8px 10px", gap:8 }}>
        {opponents.length === 0 && (
          <div style={{ flex:1, textAlign:"center", fontSize:10, color:C.muted }}>
            No opponents yet
          </div>
        )}
        {opponents.map(function(p) {
          var mCol = p.money<0?"#ef4444":p.money<5?"#f59e0b":"#4ade80";
          var isActive = p.id === cp.id;
          var pPressure = totalPressure(p);
          var oppDeltas = moneyDeltas.filter(function(d){ return d.pid === p.id; });
          var latestDelta = oppDeltas.length ? oppDeltas[oppDeltas.length-1] : null;
          return (
            <div key={p.id} onClick={function(){ setOppPopup(p.id); }}
              style={{ flexShrink:0, width:90, height:80, position:"relative", overflow:"hidden",
                       background:isActive?"#1a1004":"#131b2e",
                       border:"1px solid "+(isActive?p.color:C.border),
                       borderTop:"2px solid "+p.color,
                       borderRadius:8, padding:"6px 8px", cursor:"pointer",
                       display:"flex", flexDirection:"column", justifyContent:"space-between" }}>
              {latestDelta&&(
                <div style={{ position:"absolute",inset:0,borderRadius:8,zIndex:10,pointerEvents:"none",
                               background:latestDelta.delta>0?"rgba(20,83,45,0.87)":"rgba(127,29,29,0.87)",
                               display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",gap:2,
                               animation:"moneyOverlayIn .12s ease, moneyOverlayOut .45s ease 1.1s forwards" }}>
                  <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:16,fontWeight:900,lineHeight:1,
                                 color:latestDelta.delta>0?"#4ade80":"#f87171",
                                 textShadow:"0 0 12px "+(latestDelta.delta>0?"rgba(74,222,128,0.7)":"rgba(248,113,113,0.7)") }}>
                    {latestDelta.delta>0?"+":""}{latestDelta.delta}k
                  </div>
                  <div style={{ fontSize:8,fontWeight:700,color:latestDelta.delta>0?"#86efac":"#fca5a5",
                                 letterSpacing:.5,opacity:.8 }}>
                    {p.name}
                  </div>
                </div>
              )}
              <div style={{ fontSize:11, fontWeight:700, color:"#f1f5f9",
                            whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>
                {p.name}
              </div>
              <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:14, fontWeight:700, color:mCol }}>
                {$(p.money)}
              </div>
              <div style={{ display:"flex", gap:3, flexWrap:"wrap", minHeight:14 }}>
                {isActive && <div style={{ fontSize:7,fontWeight:800,color:p.color,background:p.color+"22",borderRadius:3,padding:"1px 4px",letterSpacing:.5 }}>NOW</div>}
                {pPressure>0 && <div style={{ fontSize:7,fontWeight:700,color:"#fca5a5",background:"#3b0000",borderRadius:3,padding:"1px 4px" }}>{pPressure}P</div>}
                {(p.turnsToSkip||0)>0 && <div style={{ fontSize:7,fontWeight:700,color:"#fbbf24",background:"#2d1f00",borderRadius:3,padding:"1px 4px" }}>SKIP</div>}
                {p.actionLocked && <div style={{ fontSize:7,fontWeight:700,color:"#c4b5fd",background:"#1a0a3e",borderRadius:3,padding:"1px 4px" }}>LOCKED</div>}
              </div>
            </div>
          );
        })}
      </div>

      {/* ── 3. YOUR INFO BAR ── */}
      {(function(){
        var feeColor = vp.feeToken >= 3 ? "#ef4444" : vp.feeToken >= 2 ? "#f59e0b" : "#475569";
        var ownDeltas = moneyDeltas.filter(function(d){ return d.pid === vp.id; });
        var nearBankruptcy = vp.money <= -6;
        var shownMoney = displayMoney !== null ? displayMoney : vp.money;
        var moneyColor = shownMoney<0?"#ef4444":shownMoney<5?"#f59e0b":"#4ade80";
        return (
        <div style={{ flexShrink:0, height:70, background:"#0d1520",
                      borderBottom:"1px solid "+C.border,
                      display:"flex", alignItems:"stretch", padding:"0 10px", gap:6 }}>
          {/* Left: actions + pips + pressure */}
          <div style={{ width:58, display:"flex", flexDirection:"column",
                        alignItems:"center", justifyContent:"center", gap:2 }}>
            <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:24, fontWeight:800, lineHeight:1,
                          color:isCP&&isPlanning?"#a78bfa":C.muted }}>
              {isCP&&isPlanning?at:"—"}
            </div>
            <div style={{ fontSize:7, fontWeight:700, letterSpacing:.5,
                          color:isCP&&isPlanning?"#7c3aed":"#334155" }}>
              {isCP&&isPlanning?"ACTIONS":""}
            </div>
            {isCP&&isPlanning&&at>0&&(
              <div style={{ display:"flex", gap:2, flexWrap:"wrap", justifyContent:"center", marginTop:1 }}>
                {Array.from({length:Math.min(at,6)},function(_,i){
                  return <div key={i} style={{ width:5,height:5,borderRadius:"50%",background:"#7c3aed",
                                               boxShadow:"0 0 3px #7c3aed88" }} />;
                })}
              </div>
            )}
            {myPressure>0 && (
              <div style={{ fontSize:7,fontWeight:700,color:"#fca5a5",background:"#3b0000",
                            borderRadius:3,padding:"1px 5px",letterSpacing:.3 }}>
                {myPressure}P
              </div>
            )}
          </div>
          {/* Center: money + bankruptcy warning + own delta animations */}
          <div style={{ flex:1, display:"flex", flexDirection:"column",
                        alignItems:"center", justifyContent:"center", position:"relative" }}>
            {/* Own money delta — floats just above (gain) or below (loss) the balance */}
            {ownDeltas.map(function(d){
              return (
                <div key={d.id}
                  style={{ position:"absolute",
                           left:"50%",
                           ...(d.delta>0 ? { bottom:"100%" } : { top:"100%" }),
                           pointerEvents:"none",
                           fontFamily:"'JetBrains Mono',monospace", fontSize:18, fontWeight:900,
                           color: d.delta>0?"#4ade80":"#f87171", whiteSpace:"nowrap",
                           textShadow: "0 0 14px "+(d.delta>0?"rgba(74,222,128,0.6)":"rgba(248,113,113,0.6)"),
                           zIndex:600,
                           animation:(d.delta>0?"floatUpSmall":"floatDownSmall")+" 1.8s ease forwards" }}>
                  {d.delta>0?"+":""}{d.delta}k
                </div>
              );
            })}
            <div style={{ display:"flex", alignItems:"center", gap:4 }}>
              <div style={{ fontFamily:"'JetBrains Mono',monospace",
                            fontSize:shownMoney>=-9&&shownMoney<100?28:22, fontWeight:800, lineHeight:1,
                            color:moneyColor, transition:"color .2s" }}>
                {$(shownMoney)}
              </div>
              {nearBankruptcy&&(
                <button onClick={function(){ setShowBkPopup(true); }}
                  style={{ background:"none", border:"none", cursor:"pointer", padding:0, lineHeight:1,
                           fontSize:14, animation:"pulse .9s ease-in-out infinite", color:"#fbbf24" }}>
                  ⚠
                </button>
              )}
            </div>
            <div style={{ fontSize:8, color:C.muted, marginTop:3, letterSpacing:.5 }}>{vp.name}</div>
          </div>
          {/* Right: overdraft fee (color-coded) + status chips */}
          <div style={{ width:62, display:"flex", flexDirection:"column",
                        alignItems:"center", justifyContent:"center", gap:2 }}>
            <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:13, fontWeight:700, lineHeight:1,
                          color:feeColor, transition:"color .4s" }}>
              {"-$"+overdraft+"k"}
            </div>
            <div style={{ fontSize:7,fontWeight:700,color:feeColor,letterSpacing:.5,transition:"color .4s" }}>
              OVERDRAFT
            </div>
            {(vp.turnsToSkip||0)>0 && (
              <div style={{ fontSize:7,fontWeight:700,color:"#fbbf24",background:"#2d1f00",
                            borderRadius:3,padding:"1px 5px",letterSpacing:.3 }}>
                SKIP×{vp.turnsToSkip}
              </div>
            )}
            {vp.actionLocked && (
              <div style={{ fontSize:7,fontWeight:700,color:"#c4b5fd",background:"#1a0a3e",
                            borderRadius:3,padding:"1px 5px",letterSpacing:.3 }}>
                LOCKED
              </div>
            )}
          </div>
        </div>
        );
      })()}

      {/* ── BANKRUPTCY WARNING POPUP ── */}
      {showBkPopup&&(
        <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.75)",
                     display:"flex",alignItems:"center",justifyContent:"center",
                     zIndex:750,animation:"fadeBackdrop .15s ease"}}
          onClick={function(){setShowBkPopup(false);}}>
          <div style={{background:"#131b2e",border:"1px solid #f59e0b",borderRadius:14,
                       padding:"22px 24px",maxWidth:280,width:"88%",
                       animation:"popIn .18s ease"}}
            onClick={function(e){e.stopPropagation();}}>
            <div style={{fontFamily:"'Rubik',sans-serif",fontSize:15,fontWeight:700,
                         color:"#fbbf24",marginBottom:8}}>⚠ Approaching Bankruptcy</div>
            <div style={{fontSize:11,color:"#94a3b8",lineHeight:1.6,marginBottom:16}}>
              Your balance is <span style={{color:"#ef4444",fontWeight:700,fontFamily:"'JetBrains Mono',monospace"}}>{$(vp.money)}</span>.
              Bankruptcy hits at <span style={{fontFamily:"'JetBrains Mono',monospace",fontWeight:700,color:"#f87171"}}>-$10k</span>.<br/><br/>
              On bankruptcy: your hand is discarded (you gain <b style={{color:"#4ade80"}}>$1k per card</b> discarded), and you lose your <b style={{color:"#f87171"}}>most valuable asset</b>. All other assets are kept.
            </div>
            <button onClick={function(){setShowBkPopup(false);}}
              style={{width:"100%",padding:"9px 0",fontSize:12,fontWeight:700,borderRadius:8,
                      cursor:"pointer",background:"#1c1003",border:"1px solid #d97706",
                      color:"#fbbf24",fontFamily:"'Rubik',sans-serif"}}>
              Got it
            </button>
          </div>
        </div>
      )}

      {/* ── 4. TAB HEADERS ── */}
      <div style={{ flexShrink:0, height:38, background:"#080d18",
                    borderBottom:"1px solid "+C.border,
                    display:"flex", alignItems:"stretch" }}>
        {TABS.map(function(t) {
          var active = tab === t;
          var badge = {main:"",assets:assets.length||"",shop:shop.length||"",log:""}[t];
          return (
            <button key={t} onClick={function(){ setTab(t); }}
              style={{ flex:1, background:"none", border:"none", cursor:"pointer",
                       borderBottom:active?"2px solid "+C.gold:"2px solid transparent",
                       display:"flex", flexDirection:"column", alignItems:"center",
                       justifyContent:"center", gap:1, paddingBottom:1 }}>
              <div style={{ fontSize:9, fontWeight:700, letterSpacing:1,
                            fontFamily:"'Rubik',sans-serif",
                            color:active?C.gold:C.muted }}>
                {t.toUpperCase()}
              </div>
              {badge ? (
                <div style={{ fontSize:7, fontWeight:800, lineHeight:1.6,
                              background:active?C.gold:"#1e2d45",
                              color:active?"#0a0f1a":"#64748b",
                              borderRadius:8, padding:"0 5px" }}>
                  {badge}
                </div>
              ) : null}
            </button>
          );
        })}
      </div>

      {/* ── 5. TAB CONTENT (swipeable) ── */}
      <div ref={tabContentRef}
        style={{ flex:1, overflow:"hidden", position:"relative", touchAction:"pan-y" }}
        onTouchStart={onTouchStart}
        onTouchMove={onTouchMove}
        onTouchEnd={onTouchEnd}>
        <div style={{
          display:"flex", width:"400%", height:"100%",
          transform:"translateX(calc("+(-(tabIdx*25))+"% + "+dragX+"px))",
          transition:isDragging?"none":"transform .28s cubic-bezier(.22,.8,.36,1)",
          willChange:"transform"
        }}>

          {/* ── MAIN TAB ── */}
          <div style={{ width:"25%", height:"100%", overflowY:"auto", padding:"10px 12px", boxSizing:"border-box" }}>
            {/* Deck / Discard / Shop row */}
            <div style={{ display:"flex", gap:8, marginBottom:12, height:88 }}>
              {/* Deck */}
              <div style={{ flex:1, background:C.panel, border:"1px solid "+C.border, borderRadius:10,
                            display:"flex", flexDirection:"column", alignItems:"center", justifyContent:"center", gap:2 }}>
                <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:28, fontWeight:700, lineHeight:1 }}>
                  {st.mainDeck.length}
                </div>
                <div style={{ fontSize:8, color:C.muted, letterSpacing:1 }}>DECK</div>
              </div>
              {/* Top discard */}
              <div style={{ flex:1.5, background:topDiscard?"#0e1a12":C.panel,
                            border:"1px solid "+(topDiscard?"#166534":C.border), borderRadius:10,
                            padding:"8px 10px", cursor:topDiscard?"pointer":"default",
                            display:"flex", flexDirection:"column", justifyContent:"center" }}
                onClick={function(){ if(topDiscard) handleDetailCard(topDiscard); }}>
                <div style={{ fontSize:7, color:"#4ade80", fontWeight:700, marginBottom:3, letterSpacing:1 }}>DISCARD</div>
                {topDiscard ? (
                  <div>
                    <div style={{ fontFamily:"'Rubik',sans-serif", fontSize:11, fontWeight:700, color:"#f1f5f9",
                                  whiteSpace:"nowrap", overflow:"hidden", textOverflow:"ellipsis" }}>
                      {topDiscard.name}
                    </div>
                    <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:14, color:C.gold, fontWeight:700, marginTop:2 }}>
                      ${topDiscard.origVal||topDiscard.value}k
                    </div>
                  </div>
                ) : (
                  <div style={{ fontSize:10, color:"#334155" }}>Empty</div>
                )}
              </div>
              {/* Shop shortcut */}
              <div style={{ flex:1, background:"#0d1a0d", border:"1px solid #16a34a", borderRadius:10,
                            display:"flex", flexDirection:"column", alignItems:"center",
                            justifyContent:"center", gap:3, cursor:"pointer" }}
                onClick={function(){ setTab("shop"); }}>
                <div style={{ fontFamily:"'JetBrains Mono',monospace", fontSize:26, fontWeight:700, color:"#4ade80", lineHeight:1 }}>
                  {shop.length}
                </div>
                <div style={{ fontSize:8, color:"#4ade80", letterSpacing:1, fontWeight:700 }}>SHOP</div>
                <div style={{ fontSize:7, color:"#34d399", letterSpacing:.5 }}>TAP TO VIEW</div>
              </div>
            </div>
            {/* Recent activity */}
            {(function(){
              var typeColors2={money:"#4ade80",loss:"#f87171",asset:"#f59e0b",reaction:"#a78bfa",draw:"#60a5fa",discard:"#94a3b8",turn:"#64748b"};
              var recentLog=(st.log||[]).slice().reverse().filter(function(e){
                return e&&['money','loss','asset','reaction','draw','discard','turn'].indexOf(e.type)>=0
                  &&e.msg&&e.msg.indexOf('ends their turn')<0;
              }).slice(0,4);
              return (
                <div style={{ background:"#0a1220", border:"1px solid "+C.border, borderRadius:8, padding:"8px 12px" }}>
                  <div style={{ fontSize:8, color:"#334155", letterSpacing:1, fontWeight:700, marginBottom:6 }}>RECENT ACTIVITY</div>
                  {recentLog.length===0&&<div style={{fontSize:10,color:"#334155"}}>No activity yet</div>}
                  {recentLog.map(function(e){
                    var col=typeColors2[e.type]||"#64748b";
                    var parts=(function(){
                      var msg=(e.msg||'').replace(/^[^a-zA-Z0-9]+/,'');
                      var cards=e.cards||[];
                      if(!cards.length) return [msg];
                      var result=[],rem=msg;
                      cards.forEach(function(card){ var needle='"'+card.name+'"',idx2=rem.indexOf(needle);
                        if(idx2>=0){if(idx2>0)result.push(rem.slice(0,idx2+1));result.push({card:card});result.push('"');rem=rem.slice(idx2+needle.length);}
                      });
                      if(rem) result.push(rem);
                      return result;
                    })();
                    return (
                      <div key={e.id} style={{padding:"4px 0",borderBottom:"1px solid #0f1828",fontSize:11,lineHeight:1.4}}>
                        {parts.map(function(part,pi){
                          if(typeof part==='string') return <span key={pi} style={{color:col}}>{part}</span>;
                          return <span key={pi} onClick={function(){handleDetailCard(part.card);}}
                            style={{color:col,textDecoration:"underline dotted",cursor:"pointer"}}>{part.card.name}</span>;
                        })}
                      </div>
                    );
                  })}
                </div>
              );
            })()}
          </div>

          {/* ── ASSETS TAB ── */}
          <div style={{ width:"25%", height:"100%", overflowY:"auto", padding:"10px 12px", boxSizing:"border-box" }}>
            <div style={{ fontSize:9,color:C.muted,marginBottom:8,letterSpacing:1,fontWeight:700 }}>
              {vp.name.toUpperCase()}{"'S ASSETS ("+assets.length+")"}
            </div>
            {assets.length===0&&<div style={{textAlign:"center",color:C.muted,fontSize:12,padding:20}}>No assets</div>}
            {assets.map(function(a){
              var sub=SUB_UI[a.sub]||SUB_UI[AS.DURING];
              var canAct=isCP&&!a.disabled&&a.status!==ST.USED&&a.sub!==AS.PASSIVE&&a.sub!==AS.ONCE;
              var isOnce=a.sub===AS.ONCE&&!a.disabled&&a.status!==ST.USED;
              return (
                <div key={a.id} style={{background:"#0d1a0d",border:"1px solid #16a34a",borderRadius:10,padding:10,marginBottom:8}}>
                  <div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}>
                    <div>
                      <div style={{fontSize:8,color:sub.col,fontWeight:700,background:sub.bg,borderRadius:3,padding:"1px 5px",display:"inline-block",marginBottom:3}}>{sub.lbl}</div>
                      <div style={{fontFamily:"'Rubik',sans-serif",fontSize:14,color:"#f1f5f9",fontWeight:700}}>
                        {a.name}
                        {a._franchised&&<span style={{fontSize:8,color:"#fde68a",marginLeft:4,fontWeight:800}}>×2</span>}
                        {a._puSourceId&&<span style={{fontSize:7,color:"#ddd6fe",marginLeft:4,fontWeight:800}}>PU</span>}
                        {a.lockedCard&&<span style={{fontSize:8,color:"#60a5fa",marginLeft:4,fontWeight:700,background:"#0c1a2e",borderRadius:3,padding:"1px 4px"}}>+ {a.lockedCard.name}</span>}
                      </div>
                    </div>
                    <div style={{textAlign:"right"}}>
                      <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:15,color:C.gold,fontWeight:700}}>${a.origVal}k</div>
                      {a.status===ST.USED&&<div style={{fontSize:8,color:"#64748b"}}>USED</div>}
                    </div>
                  </div>
                  <div style={{fontSize:9,color:"#94a3b8",lineHeight:1.5,marginBottom:4}}>{getDesc(a)}</div>
                  {(a.tokens||[]).length>0&&!a._noTokenCircles&&(
                    <div style={{display:"flex",gap:3,marginBottom:6,flexWrap:"wrap"}}>
                      {(a.tokens||[]).map(function(_,i){return <div key={i} style={{width:8,height:8,borderRadius:"50%",background:"#f59e0b"}}/>;})}
                    </div>
                  )}
                  {a._noTokenCircles&&(a.tokens||[]).length>0&&<div style={{fontSize:9,color:"#f59e0b",marginBottom:4}}>Tokens: {(a.tokens||[]).length}</div>}
                  {a.disabled&&a.shutDownBy&&!a._cfrBy&&<div style={{fontSize:9,color:"#ef4444",marginBottom:4}}>SHUT DOWN</div>}
                  {a.disabled&&!a._cfrBy&&!a.shutDownBy&&!a._taxedBy&&<div style={{fontSize:9,color:"#ef4444",marginBottom:4}}>DISABLED</div>}
                  {a._cfrBy&&<div style={{fontSize:9,color:"#f97316",marginBottom:4,letterSpacing:.5,fontWeight:700}}>CLOSED by {a._cfrBy}</div>}
                  {a._taxedBy&&<div style={{fontSize:9,color:"#94a3b8",marginBottom:4,letterSpacing:.5,fontWeight:700}}>TAXED by {a._taxedBy}</div>}
                  <div style={{display:"flex",gap:6}}>
                    {(canAct||isOnce)&&(
                      <button onClick={function(){handleActivate(a.id);}}
                        style={{flex:1,padding:"7px 0",fontSize:11,fontWeight:700,borderRadius:7,cursor:"pointer",background:"#0d2010",border:"1px solid #22c55e",color:"#4ade80"}}>
                        Activate
                      </button>
                    )}
                    <button onClick={function(){handleDetailCard(a);}}
                      style={{padding:"7px 12px",fontSize:11,fontWeight:700,borderRadius:7,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>
                      ?
                    </button>
                  </div>
                </div>
              );
            })}
          </div>

          {/* ── SHOP TAB ── */}
          <div style={{ width:"25%", height:"100%", overflowY:"auto", padding:"10px 12px", boxSizing:"border-box", background:"#0c0a00" }}>
            {isCP&&isPlanning&&(function(){
              var bci=calcBlindCost(st,cp.id);
              var used=st.hasBoughtAsset;
              var canBlind=!used&&cp.money>=MIN_BUY;
              return (
                <div style={{background:"#1a1200",border:"1px solid #92400e",borderRadius:10,padding:10,marginBottom:8,opacity:used?0.4:1,transition:"opacity 0.25s ease",pointerEvents:used?"none":"auto"}}>
                  <div style={{fontSize:10,color:"#fbbf24",fontWeight:700,marginBottom:2,letterSpacing:1}}>BLIND BUY</div>
                  <div style={{fontSize:9,color:"#94a3b8",marginBottom:6}}>Buy a random asset from the deck.</div>
                  <button disabled={!canBlind} onClick={handleBlindBuy}
                    style={{width:"100%",padding:"7px 0",fontSize:11,fontWeight:700,borderRadius:7,cursor:canBlind?"pointer":"not-allowed",opacity:canBlind?1:0.5,background:"#1c1000",border:"1px solid #92400e",color:"#fbbf24"}}>
                    Blind Buy ${bci.cost}k
                  </button>
                </div>
              );
            })()}
            <div style={{fontSize:9,color:"#92400e",letterSpacing:1,marginBottom:8,fontWeight:700}}>SHOP ({shop.length})</div>
            {shop.length===0&&Object.keys(exitingShopItems).length===0&&<div style={{textAlign:"center",color:C.muted,fontSize:12,padding:20}}>Shop empty</div>}
            {shop.map(function(a){
              var ci=calcShopCost(st,a,cp.id,0); var sub=SUB_UI[a.sub]||SUB_UI[AS.DURING];
              var canBuy=isCP&&isPlanning&&!st.hasBoughtAsset&&cp.money>=MIN_BUY;
              var cc=ci.cost>cp.money?"#ef4444":"#fbbf24";
              return (
                <div key={a.id} style={{background:"#140f00",border:"1px solid #92400e",borderRadius:10,padding:10,marginBottom:8}}>
                  <div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}>
                    <div>
                      <div style={{fontSize:8,color:sub.col,fontWeight:700,background:sub.bg,borderRadius:3,padding:"1px 5px",display:"inline-block",marginBottom:3}}>{sub.lbl}</div>
                      <div style={{fontFamily:"'Rubik',sans-serif",fontSize:14,color:"#f1f5f9",fontWeight:700}}>{a.name}</div>
                    </div>
                    <div style={{textAlign:"right"}}>
                      <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:15,color:cc,fontWeight:700}}>${ci.cost}k</div>
                      <div style={{fontSize:8,color:"#78716c"}}>val ${a.origVal}k</div>
                    </div>
                  </div>
                  <div style={{fontSize:9,color:"#94a3b8",lineHeight:1.5,marginBottom:6}}>{getDesc(a)}</div>
                  {canBuy&&(
                    <button onClick={function(){handleBuyShop(a.id);}}
                      style={{width:"100%",padding:"7px 0",fontSize:11,fontWeight:700,borderRadius:7,cursor:"pointer",background:"#1c1000",border:"1px solid #d97706",color:"#fbbf24"}}>
                      Buy ${ci.cost}k
                    </button>
                  )}
                </div>
              );
            })}
            {Object.entries(exitingShopItems).map(function(entry){
              var eid=entry[0], ea=entry[1];
              if (shop.some(function(s){ return s.id===eid; })) return null;
              return (
                <div key={"exit_"+eid} style={{background:"#140f00",border:"1px solid #92400e",borderRadius:10,padding:10,marginBottom:8,position:"relative",overflow:"hidden",animation:"shopPurchased 0.95s ease forwards"}}>
                  <div style={{position:"absolute",inset:0,display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",background:"rgba(12,8,0,0.88)",borderRadius:10,zIndex:2}}>
                    <div style={{fontSize:13,fontWeight:900,color:"#fbbf24",letterSpacing:2,animation:"shopPurchasedText 0.95s ease forwards"}}>PURCHASED</div>
                    <div style={{fontSize:9,color:"#92400e",marginTop:3,fontFamily:"'Rubik',sans-serif"}}>{ea.name}</div>
                  </div>
                  <div style={{opacity:0.25}}>
                    <div style={{fontFamily:"'Rubik',sans-serif",fontSize:14,color:"#f1f5f9",fontWeight:700,marginBottom:4}}>{ea.name}</div>
                    <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:15,color:"#fbbf24",fontWeight:700}}>${ea.origVal||ea.value}k</div>
                  </div>
                </div>
              );
            })}
          </div>

          {/* ── LOG TAB ── */}
          <div style={{ width:"25%", height:"100%", overflowY:"auto", padding:"10px 12px", boxSizing:"border-box" }}>
            <div style={{fontSize:9,color:C.muted,marginBottom:8,letterSpacing:1,fontWeight:700}}>GAME LOG</div>
            <GameLog log={st.log||[]} onCardClick={function(card){handleDetailCard(card);}} />
          </div>

        </div>{/* end sliding container */}
      </div>{/* end tab content */}

      {/* ── 6. CARD DETAIL PANEL (slides in above action bar) ── */}
      {selCard && (
        <div style={{ flexShrink:0,
                      background:CUI[selCard.type]?CUI[selCard.type].bg:"#0e1520",
                      borderTop:"2px solid "+(CUI[selCard.type]?CUI[selCard.type].ac:"#334155"),
                      padding:"10px 14px",
                      animation:"slideUp .18s ease" }}>
          <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:5}}>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontSize:8,color:CUI[selCard.type]?CUI[selCard.type].ac:"#93c5fd",
                           fontWeight:700,letterSpacing:1,marginBottom:2}}>{selCard.type}</div>
              <div style={{fontFamily:"'Rubik',sans-serif",fontSize:15,color:"#f1f5f9",fontWeight:700,lineHeight:1.2}}>
                {selCard.name}
              </div>
            </div>
            <div style={{display:"flex",alignItems:"center",gap:8,flexShrink:0,marginLeft:8}}>
              <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:19,color:C.gold,fontWeight:700}}>
                ${selCard.value||selCard.origVal}k
              </div>
              <button onClick={function(){setSelId(null);}}
                style={{width:26,height:26,borderRadius:5,background:"#1e293b",
                        border:"1px solid #334155",color:"#94a3b8",cursor:"pointer",
                        fontSize:13,fontWeight:700,display:"flex",alignItems:"center",justifyContent:"center"}}>
                ✕
              </button>
            </div>
          </div>
          <div style={{fontSize:10,color:"#94a3b8",lineHeight:1.5}}>
            {getDesc(selCard)}
          </div>
          {unplayableReason(selCard) && (
            <div style={{marginTop:8,padding:"6px 9px",borderRadius:6,
                         background:"rgba(220,38,38,0.12)",border:"1px solid rgba(220,38,38,0.4)",
                         fontSize:9,color:"#fca5a5",fontWeight:600,lineHeight:1.4}}>
              {unplayableReason(selCard)}
            </div>
          )}
        </div>
      )}

      {/* ── 7. ACTION BAR ── */}
      <div style={{ flexShrink:0, background:"#0d1520",
                    borderTop:"1px solid "+C.border,
                    display:"flex", alignItems:"stretch",
                    padding:"6px 10px",
                    gap:7, minHeight:48 }}>
        {(!isCP || !isPlanning) ? (
          <div style={{ flex:1,display:"flex",alignItems:"center",justifyContent:"center",gap:7 }}>
            <div style={{ width:6,height:6,borderRadius:"50%",flexShrink:0,
                          background:cp.color,boxShadow:"0 0 7px "+cp.color,
                          animation:"pulse .9s ease-in-out infinite" }} />
            <span style={{ fontSize:11,fontWeight:700,color:"#475569",
                           fontFamily:"'Rubik',sans-serif",letterSpacing:.5 }}>
              {cp.name}'s turn
            </span>
          </div>
        ) : (
          <>
            <button disabled={!canDraw} onClick={handleDraw}
              style={{ flex:1,borderRadius:7,fontSize:11,fontWeight:800,letterSpacing:1,
                       fontFamily:"'Rubik',sans-serif",cursor:canDraw?"pointer":"not-allowed",
                       background:canDraw?"#0c1833":"#0a0f1a",
                       border:"1.5px solid "+(canDraw?"#2563eb":"#1e2d45"),
                       color:canDraw?"#60a5fa":"#334155",transition:"all .12s" }}>
              DRAW
            </button>
            <button disabled={!(canPlay||canPlayR)} onClick={canPlayR?handlePlayReaction:handlePlay}
              style={{ flex:1.5,borderRadius:7,fontSize:11,fontWeight:800,letterSpacing:1,
                       fontFamily:"'Rubik',sans-serif",cursor:(canPlay||canPlayR)?"pointer":"not-allowed",
                       background:canPlay?"#0c1833":canPlayR?"#1f0a0a":"#0a0f1a",
                       border:"1.5px solid "+(canPlay?"#2563eb":canPlayR?"#dc2626":"#1e2d45"),
                       color:canPlay?"#60a5fa":canPlayR?"#f87171":"#334155",transition:"all .12s" }}>
              PLAY
            </button>
            <button disabled={!canSellC} onClick={handleSell}
              style={{ flex:1,borderRadius:7,fontSize:11,fontWeight:800,letterSpacing:1,
                       fontFamily:"'Rubik',sans-serif",cursor:canSellC?"pointer":"not-allowed",
                       background:canSellC?"#1c1003":"#0a0f1a",
                       border:"1.5px solid "+(canSellC?"#d97706":"#1e2d45"),
                       color:canSellC?"#fbbf24":"#334155",transition:"all .12s" }}>
              SELL
            </button>
            <button onClick={function(){setEndConfirm(true);}}
              style={{ flex:.75,borderRadius:7,fontSize:11,fontWeight:800,letterSpacing:1,
                       fontFamily:"'Rubik',sans-serif",cursor:"pointer",
                       background:"#1c0a0a",
                       border:"1.5px solid #7f1d1d",
                       color:"#f87171",transition:"all .12s",
                       animation: at===0 ? "endPulse 1.6s ease-in-out infinite" : "none" }}>
              END
            </button>
          </>
        )}
      </div>

      {/* ── END TURN CONFIRM MODAL ── */}
      {endConfirm&&(
        <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.72)",
                     display:"flex",alignItems:"center",justifyContent:"center",
                     zIndex:700,animation:"fadeBackdrop .15s ease"}}
          onClick={function(){setEndConfirm(false);}}>
          <div style={{background:"#131b2e",border:"1px solid #7f1d1d",borderRadius:14,
                       padding:"24px 28px",textAlign:"center",minWidth:200,
                       animation:"popIn .18s ease"}}
            onClick={function(e){e.stopPropagation();}}>
            <div style={{fontFamily:"'Rubik',sans-serif",fontSize:15,fontWeight:700,
                         color:"#fca5a5",marginBottom:6}}>End your turn?</div>
            <div style={{fontSize:11,color:"#64748b",marginBottom:18}}>
              {at} action{at!==1?"s":""} remaining
            </div>
            <div style={{display:"flex",gap:10,justifyContent:"center"}}>
              <button onClick={handleEndTurn}
                style={{padding:"9px 22px",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",
                        background:"#7f1d1d",border:"1px solid #ef4444",color:"#fca5a5",
                        fontFamily:"'Rubik',sans-serif"}}>
                End Turn
              </button>
              <button onClick={function(){setEndConfirm(false);}}
                style={{padding:"9px 16px",fontSize:12,fontWeight:700,borderRadius:8,cursor:"pointer",
                        background:"#1e293b",border:"1px solid #334155",color:"#94a3b8",
                        fontFamily:"'Rubik',sans-serif"}}>
                Cancel
              </button>
            </div>
          </div>
        </div>
      )}

      {/* ── 8. HAND + END TURN ── */}
      <div style={{ flexShrink:0, background:"#080d18", borderTop:"1px solid "+C.border,
                    padding:"8px 10px", paddingBottom:"calc(28px + env(safe-area-inset-bottom,0px))",
                    display:"flex", alignItems:"center", gap:8,
                    position:"relative" }}>
        {/* Hand count badge */}
        <div style={{ position:"absolute", top:4, right:12, fontSize:8, fontWeight:700,
                      color: hand.length>=(vp.handLimit||HAND_LIMIT)?"#f87171":"#334155",
                      fontFamily:"'JetBrains Mono',monospace", letterSpacing:.3, pointerEvents:"none" }}>
          {hand.length}/{vp.handLimit||HAND_LIMIT}
        </div>
        {/* Scrollable hand */}
        <div style={{ flex:1, overflowX:"auto", display:"flex", gap:6,
                      scrollbarWidth:"none", alignItems:"flex-end", paddingBottom:2 }}>
          {hand.length===0 && (
            <div style={{display:"flex",alignItems:"center",color:C.muted,fontSize:11,whiteSpace:"nowrap",paddingLeft:2}}>
              Empty hand
            </div>
          )}
          {hand.map(function(c){
            var isSel=c.id===selId;
            var tc2=TC[c.type]||{bg:"#0d1520",border:"#334155",text:"#94a3b8"};
            var tcDim={ ACTION:{bg:"rgba(37,99,235,0.10)",border:"rgba(37,99,235,0.32)",text:"#5b84b8"},
                        REACTION:{bg:"rgba(220,38,38,0.10)",border:"rgba(220,38,38,0.32)",text:"#b06c6c"},
                        ASSET:{bg:"rgba(22,163,74,0.10)",border:"rgba(22,163,74,0.32)",text:"#5fa37a"} };
            var dim=tcDim[c.type];
            var unplayReason=unplayableReason(c);
            var unplayable=!!unplayReason;
            return (
              <div key={c.id} onClick={function(){setSelId(isSel?null:c.id);}}
                style={{ flexShrink:0, width:62, height:90, position:"relative",
                         background:isSel?tc2.bg:(dim?dim.bg:"#0d1520"),
                         border:"2px solid "+(isSel?tc2.border:(dim?dim.border:"#1e2d45")),
                         borderRadius:7, padding:"6px 5px 5px", cursor:"pointer",
                         transition:"border-color .12s",
                         display:"flex", flexDirection:"column", justifyContent:"space-between",
                         opacity:unplayable?0.55:1,
                         boxShadow:isSel?"0 4px 16px rgba(0,0,0,0.6)":"none" }}>
                {unplayable&&(
                  <div style={{position:"absolute",top:3,right:3,width:14,height:14,borderRadius:"50%",
                               background:"rgba(15,23,42,0.85)",border:"1px solid #f87171",
                               display:"flex",alignItems:"center",justifyContent:"center",
                               fontSize:8,fontWeight:900,color:"#f87171",lineHeight:1,zIndex:1}}>
                    ⊘
                  </div>
                )}
                <div style={{fontSize:7,fontWeight:700,color:isSel?tc2.text:(dim?dim.text:"#475569"),
                             overflow:"hidden",whiteSpace:"nowrap",textOverflow:"ellipsis",lineHeight:1.3}}>
                  {c.name}
                </div>
                <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:isSel?13:11,
                             color:isSel?C.gold:"#64748b",fontWeight:700,textAlign:"center"}}>
                  ${c.value||c.origVal}k
                </div>
                <div style={{fontSize:6,color:isSel?tc2.text:(dim?dim.text:"#334155"),fontWeight:700,textAlign:"center",letterSpacing:.5}}>
                  {c.type}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* ── OPPONENT ASSET POPUP ── */}
      {oppPopup&&(function(){
        var op=st.players.find(function(p){return p.id===oppPopup;});
        if(!op) return null;
        return (
          <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.75)",
                       display:"flex",alignItems:"flex-start",justifyContent:"center",
                       zIndex:800,animation:"fadeBackdrop .15s ease"}}
            onClick={function(){setOppPopup(null);}}>
            <div style={{background:"#131b2e",border:"1px solid "+C.border,
                         borderRadius:"0 0 14px 14px",width:"100%",maxWidth:480,
                         maxHeight:"72vh",overflowY:"auto",
                         paddingTop:"max(16px, env(safe-area-inset-top, 16px))",
                         padding:"max(16px, env(safe-area-inset-top, 16px)) 16px 20px",
                         animation:"slideDown .2s ease"}}
              onClick={function(e){e.stopPropagation();}}>
              <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:12}}>
                <div>
                  <div style={{fontFamily:"'Rubik',sans-serif",fontSize:15,fontWeight:700,color:op.color}}>{op.name}</div>
                  <div style={{fontSize:10,color:C.muted}}>
                    {op.assets.length} asset{op.assets.length!==1?"s":""} · {$(op.money)}
                  </div>
                </div>
                <button onClick={function(){setOppPopup(null);}}
                  style={{width:30,height:30,borderRadius:6,background:"#1e293b",border:"1px solid #334155",
                          color:"#94a3b8",cursor:"pointer",fontSize:14,fontWeight:700}}>✕</button>
              </div>
              {op.assets.length===0&&<div style={{textAlign:"center",color:C.muted,fontSize:12,padding:"20px 0"}}>No assets</div>}
              {op.assets.map(function(a){
                var sub=SUB_UI[a.sub]||SUB_UI[AS.DURING];
                return (
                  <div key={a.id} style={{background:"#0d1a0d",border:"1px solid #16a34a",borderRadius:10,padding:10,marginBottom:8}}>
                    <div style={{display:"flex",justifyContent:"space-between",marginBottom:4}}>
                      <div>
                        <div style={{fontSize:8,color:sub.col,fontWeight:700,background:sub.bg,borderRadius:3,padding:"1px 5px",display:"inline-block",marginBottom:3}}>{sub.lbl}</div>
                        <div style={{fontFamily:"'Rubik',sans-serif",fontSize:13,color:"#f1f5f9",fontWeight:700}}>
                          {a.name}{a._franchised&&<span style={{fontSize:8,color:"#fde68a",marginLeft:4}}>×2</span>}
                        </div>
                      </div>
                      <div style={{textAlign:"right"}}>
                        <div style={{fontFamily:"'JetBrains Mono',monospace",fontSize:13,color:C.gold,fontWeight:700}}>${a.origVal}k</div>
                        {a.status===ST.USED&&<div style={{fontSize:8,color:"#64748b"}}>USED</div>}
                      </div>
                    </div>
                    <div style={{fontSize:9,color:"#94a3b8",lineHeight:1.5}}>{getDesc(a)}</div>
                    {(a.tokens||[]).length>0&&!a._noTokenCircles&&(
                      <div style={{display:"flex",gap:3,marginTop:4,flexWrap:"wrap"}}>
                        {(a.tokens||[]).map(function(_,i){return <div key={i} style={{width:7,height:7,borderRadius:"50%",background:"#f59e0b"}}/>;})}
                      </div>
                    )}
                  </div>
                );
              })}
            </div>
          </div>
        );
      })()}

      {/* ── ALL OVERLAYS AND MODALS ── */}
      <CardPlayOverlay st={st} dispatch={dispatch} />
      {st.reactionWindow && <ReactionWindow st={st} dispatch={dispatch} isMultiplayer={isMultiplayer||false} mobile={true} />}
      {st.detailCard && <DetailModal card={st.detailCard} st={st} dispatch={dispatch} onClose={handleCloseDetail} />}
      {st.awaitingTarget && <TargetModal st={st} cardId={st.awaitingTarget} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "DISCARD_TO_LIMIT"     && <DiscardLimitModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "TAXES_SELECT_ASSET"   && <TaxesSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "KICKSTARTER_CHOICE"   && <KickstarterChoiceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "POCKET_CHANGE_SELECT" && <PocketChangeSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "POCKET_CHANGE_REVEAL" && <PocketChangeRevealModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "HELP_WANTED_GIVE"     && <HelpWantedModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "INTERVIEWS_PICK"      && <InterviewsModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "LET_GO_SELECT"        && <LetGoSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "REFRESH_ASSET"        && <RefreshAssetModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "GIVE_BACK_TIE"        && <GiveBackTieModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "NEG_REACTION_SELECT"  && <NegReactionSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "OUTSIDE_HIRE_SELECT"  && <OutsideHireSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "SHRINKAGE_SELECT"     && <ShrinkageSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "OUT_OF_ORDER_SELECT"  && <OutOfOrderSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "UPGRADE_DISCARD"      && <UpgradeDiscardModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "UPGRADE_GAIN"         && <UpgradeGainModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "BLUEPRINT_PLACE"      && <BlueprintPlaceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "RETAINER_PLACE"       && <RetainerPlaceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "TRADE_IN_SELECT"      && <TradeInSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "CREDIT_LINE_CHOICE"   && <CreditLineChoiceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "RND_ACTIVATE"         && <RndActivateModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "MONOPOLY_SELECT"      && <MonopolySelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "BOGO_SELECT"          && <BOGOSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "FRANCHISE_SELECT"     && <FranchiseSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "EXCHANGE_SELECT"      && <ExchangeSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "EYE_SELECT"           && <EyeSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "BTB_SELECT"           && <BTBSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "SF_SELECT"            && <SFSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "AP_ACTIVATE_SELECT"   && <APActivateSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "FULL_REFUND_PROMPT"   && <FullRefundModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "FULL_REFUND_TARGET"   && <FullRefundTargetModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "PRICE_CHECK_PROMPT"   && <PriceCheckPromptModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "COMING_SOON_PEEK"     && <ComingSoonPeekModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "WTP_SELECT"           && <WTPSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "TOTAL_LOSS_PICK_VALUE"&& <TotalLossPickModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "QUICK_EXCHANGE_DISCARD"&&<QuickExchangeModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "MR_SHOW_CARD"         && <MRShowCardModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "VALUATION_PICK"       && <ValuationPickModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "COVER_COSTS_TARGET"   && <CoverCostsTargetModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "MINOR_LOSS_DISCARD"   && <MinorLossDiscardModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "DISCOUNT_CHOICE"      && <DiscountChoiceModal st={st} dispatch={dispatch} />}
      {st.pendingPWChoice && <PaidWorkModal st={st} dispatch={dispatch} />}
      {(st.pendingChoice && ["PU_ASSET_SELECT","PU_FIELD_SELECT","PU_DIRECTION_SELECT"].includes(st.pendingChoice.type)) && <ProductUpdateModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "REBRANDING_SELECT"    && <RebrandingModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "CFR_SELECT"            && <CFRSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "BRIBERY_ASSET_SELECT" && <BriberyAssetModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "BRIBERY_CARD_SELECT"  && <BriberyCardModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "MARKUP_DIRECTION"     && <MarkupDirectionModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "MARKUP_CARD"          && <SimpleCardListModal type="MARKUP_CARD" title="MARK-UP — CHOOSE A CARD" color="#f59e0b" st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "MULTI_TOOL_SELECT"    && <SimpleCardListModal type="MULTI_TOOL_SELECT" title="MULTI-TOOL — RESET AN ASSET" color="#67e8f9" st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "THREE_OF_KIND_SELECT" && <ThreeOfAKindModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "SNEAK_PEAK_SELECT"    && <SneakPeakModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "RECYCLE_CHOICE"       && <RecycleChoiceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && !["DISCARD_TO_LIMIT","TAXES_SELECT_ASSET","KICKSTARTER_CHOICE","POCKET_CHANGE_SELECT","POCKET_CHANGE_REVEAL","HELP_WANTED_GIVE","INTERVIEWS_PICK","LET_GO_SELECT","REFRESH_ASSET","GIVE_BACK_TIE","NEG_REACTION_SELECT","OUTSIDE_HIRE_SELECT","SHRINKAGE_SELECT","OUT_OF_ORDER_SELECT","UPGRADE_DISCARD","UPGRADE_GAIN","BLUEPRINT_PLACE","RETAINER_PLACE","TRADE_IN_SELECT","CREDIT_LINE_CHOICE","RND_ACTIVATE","MONOPOLY_SELECT","BOGO_SELECT","FRANCHISE_SELECT","EXCHANGE_SELECT","EYE_SELECT","BTB_SELECT","SF_SELECT","AP_ACTIVATE_SELECT","FULL_REFUND_PROMPT","FULL_REFUND_TARGET","PRICE_CHECK_PROMPT","COMING_SOON_PEEK","WTP_SELECT","TOTAL_LOSS_PICK_VALUE","QUICK_EXCHANGE_DISCARD","MR_SHOW_CARD","VALUATION_PICK","COVER_COSTS_TARGET","MINOR_LOSS_DISCARD","DISCOUNT_CHOICE","REBRANDING_SELECT","BRIBERY_ASSET_SELECT","BRIBERY_CARD_SELECT","MARKUP_DIRECTION","MARKUP_CARD","MULTI_TOOL_SELECT","THREE_OF_KIND_SELECT","SNEAK_PEAK_SELECT","RECYCLE_CHOICE","PU_ASSET_SELECT","PU_FIELD_SELECT","PU_DIRECTION_SELECT","CFR_SELECT"].includes(st.pendingChoice.type) && <ChoiceModal st={st} dispatch={dispatch} />}
      <MobileToastLayer toasts={st.toasts} dispatch={dispatch} hasCardDetail={!!selCard} />
    </div>
  );
}


function MobileToastLayer({ toasts, dispatch, hasCardDetail }) {
  if (!toasts || !toasts.length) return null;
  var toastBottom = hasCardDetail ? "calc(310px + env(safe-area-inset-bottom,0px))"
                                  : "calc(178px + env(safe-area-inset-bottom,0px))";
  return (
    <div style={{ position:"fixed",bottom:toastBottom,left:0,right:0,maxWidth:480,margin:"0 auto",
                  display:"flex",flexDirection:"column-reverse",gap:6,padding:"0 14px",zIndex:500,
                  pointerEvents:"none" }}>
      {toasts.slice(-3).map(function(t) {
        return <MobileToast key={t.id} toast={t} dispatch={dispatch} />;
      })}
    </div>
  );
}
function MobileToast({ toast, dispatch }) {
  var [leaving, setLeaving] = useState(false);
  var col = toast.type==="money"||toast.type==="asset" ? "#f59e0b"
           : toast.type==="loss" ? "#f87171" : "#34d399";
  useEffect(function() {
    var fadeT  = setTimeout(function() { setLeaving(true); }, 4500);
    var removeT = setTimeout(function() { dispatch({ type:"DISMISS_TOAST", id:toast.id }); }, 5000);
    return function() { clearTimeout(fadeT); clearTimeout(removeT); };
  }, []);
  return (
    <div onClick={function() { dispatch({ type:"DISMISS_TOAST", id:toast.id }); }}
      style={{ background:"rgba(13,18,28,0.97)",border:"1px solid #1e293b",
               borderLeft:"3px solid "+col,
               borderRadius:8,padding:"9px 14px",color:"#e2e8f0",
               fontFamily:"'Rubik',sans-serif",fontSize:13,fontWeight:600,
               animation: leaving ? "toastOut .5s ease forwards" : "slideUp .25s ease",
               boxShadow:"0 4px 16px rgba(0,0,0,0.6)",
               pointerEvents:"auto",cursor:"pointer" }}>
      {(toast.msg||'').replace(/^[^a-zA-Z$"(\[]+/, '')}
    </div>
  );
}


function PressureInfoModal({ player, allPlayers, dispatch, onClose }) {
  if (!player) return null;
  var totalPressure = Object.values(player.pressureTokens||{}).reduce(function(s,v){return s+v;},0);
  var entries = Object.entries(player.pressureTokens||{}).filter(function(e){ return e[1] > 0; });
  return (
    <div style={{position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",alignItems:"center",justifyContent:"center",zIndex:1000}}
      onClick={onClose}>
      <div style={{background:"#1a0a0a",border:"2px solid #ef4444",borderRadius:14,padding:24,maxWidth:340,width:"92%"}}
        onClick={function(e){e.stopPropagation();}}>
        <div style={{fontSize:10,letterSpacing:2,fontWeight:700,color:"#ef4444",fontFamily:"'Rubik',sans-serif",marginBottom:6}}>PRESSURE TOKENS</div>
        <div style={{fontSize:13,color:"#f1f5f9",fontWeight:700,marginBottom:12}}>{player.name}: {totalPressure} token{totalPressure!==1?"s":""}</div>
        {entries.map(function(e){
          var ownerPl = allPlayers.find(function(p){ return p.id===e[0]; });
          var asset = ownerPl && ownerPl.assets.find(function(a){ return a.hook==="p_pressure"; });
          var threshold = asset ? (asset.pressureThreshold||3) : 3;
          var penalty = asset ? (asset.pressurePenalty||5) : 5;
          return (<div key={e[0]} style={{marginBottom:10,padding:"8px 10px",background:"#2a0a0a",borderRadius:6,border:"1px solid #ef444433"}}>
            <div style={{fontSize:11,color:"#fca5a5",fontWeight:700}}>{ownerPl?ownerPl.name:"?"}'s Pressure Campaign</div>
            <div style={{fontSize:10,color:"#94a3b8",marginTop:3}}>Tokens: <b style={{color:"#f87171"}}>{e[1]}</b> / threshold: {threshold}</div>
            {e[1]>=threshold && <div style={{fontSize:10,color:"#f87171",marginTop:2,fontWeight:700}}>End your turn and lose ${penalty}k!</div>}
            {e[1]<threshold && <div style={{fontSize:9,color:"#64748b",marginTop:2}}>Reach {threshold} tokens → lose ${penalty}k at turn end</div>}
          </div>);
        })}
        <div style={{fontSize:9,color:"#64748b",marginTop:8,lineHeight:1.5}}>Buy an asset to clear all pressure tokens. If you end your turn with {entries[0]?((allPlayers.find(function(p){return p.id===entries[0][0];})?.assets.find(function(a){return a.hook==="p_pressure";})||{}).pressureThreshold||3):3}+ tokens from any owner, you lose the penalty amount.</div>
        <button onClick={onClose} style={{width:"100%",marginTop:12,padding:"8px 0",fontSize:11,fontWeight:700,borderRadius:8,cursor:"pointer",background:"#1e293b",border:"1px solid #334155",color:"#94a3b8"}}>Close</button>
      </div>
    </div>
  );
}

function GameScreen({ st, dispatch, settings, setSettings }) {
  const cp = curPl(st);
  const vp = st.players[st.viewIdx];
  const isMobile = settings.mobileView || false;
  const [pressurePlayer, setPressurePlayer] = useState(null);

  // Auto-start turn
  useEffect(() => {
    if (st.startTurnPending && st.phase===PH.SOT) {
      const t = setTimeout(() => dispatch({type:"START_TURN"}), 400);
      return () => clearTimeout(t);
    }
  }, [st.curIdx, st.startTurnPending]);

  var onMatClick = function(i) {
    const clickedPlayer = st.players[i];
    if (st.awaitingTarget && clickedPlayer.id !== cp.id) {
      dispatch({type:"SET_TARGET", cardId:st.awaitingTarget, targetId:clickedPlayer.id});
    } else {
      dispatch({type:"SET_VIEW", i});
    }
  }

  return (
    <div className="sb-bg-warm" style={{ minHeight:"100vh",color:C.text,display:"flex",flexDirection:"column",padding:"10px 14px",gap:9 }}>
      <style>{CSS}</style>
      {/* Header */}
      <div className="brick-panel" style={{ display:"flex",justifyContent:"space-between",alignItems:"center",background:C.panel,border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 16px",flexWrap:"wrap",gap:8 }}>
        <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:16,color:C.gold,fontWeight:700,letterSpacing:2 }}>STRICTLY BUSINESS</div>
        <div style={{ display:"flex",gap:8,alignItems:"center" }}>
          <button
            onClick={()=>setSettings(s=>({...s,moneyPopups:!s.moneyPopups}))}
            title="Toggle money popup animations"
            style={{ ...btn(settings.moneyPopups?"#052e16":C.bg, settings.moneyPopups?"#16a34a":C.border, settings.moneyPopups?"#4ade80":C.muted),
                     fontSize:10, padding:"4px 10px", letterSpacing:.5 }}>
            {settings.moneyPopups ? "POPUPS ON" : "POPUPS OFF"}
          </button>
          {(() => { var tc_curPl = st.players[st.curIdx]; var tc_myId = settings.multiplayMyId; var tc_isMyTurn = settings.multiplayMode ? (tc_curPl && tc_myId ? tc_curPl.id === tc_myId : true) : true; return <TurnControls st={st} dispatch={dispatch} isMyTurn={tc_isMyTurn} />; })()}
          <button onClick={()=>setSettings(s=>({...s,mobileView:!s.mobileView}))}
            style={{ padding:"5px 10px",fontSize:10,fontWeight:700,borderRadius:6,cursor:"pointer",
                     background:isMobile?"#1c1003":"#1e293b",border:"1px solid "+(isMobile?"#d97706":"#334155"),
                     color:isMobile?"#d97706":"#64748b",letterSpacing:1,fontFamily:"'Rubik',sans-serif" }}>
            {isMobile?"DESKTOP VIEW":"MOBILE VIEW"}
          </button>
        </div>
      </div>
      {/* Player mats */}
      <div style={{ display:"flex",gap:7,overflowX:"auto",paddingBottom:2 }}>
        {st.players.map((p,i) => (
          <div key={p.id} style={{ position:"relative", flexShrink:0 }}>
            <PlayerMat player={p} isActive={i===st.curIdx} isViewing={i===st.viewIdx}
              onClick={()=>onMatClick(i)} compact={st.players.length>3}
              onPressureClick={function(pl){ setPressurePlayer(pl); }} />
            <MoneyPopupOverlay
              events={(st.moneyEvents||[]).filter(e=>e.pid===p.id)}
              dispatch={dispatch}
              enabled={settings.moneyPopups}
            />
          </div>
        ))}
      </div>
      {/* Middle: Shop + Discard + Stack + Reaction + Log */}
      <div style={{ display:"flex",gap:9,alignItems:"flex-start" }}>
        <div style={{ flex:"0 0 auto",maxWidth:"38%" }}><ShopPanel st={st} dispatch={dispatch} /></div>
        <DiscardPile mainDiscard={st.mainDiscard} dispatch={dispatch} />
        <div style={{ flex:1,display:"flex",flexDirection:"column",gap:8 }}>
          {settings.multiplayMode
            ? <MultiActivityFeed st={st} myPlayerId={settings.multiplayMyId} />
            : <TriggerStack st={st} />}
      <CardPlayOverlay st={st} dispatch={dispatch} />
        </div>
        <div style={{ width:230,background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:10,display:"flex",flexDirection:"column",gap:4,maxHeight:260 }}>
          <div style={{ fontSize:9,color:C.muted,letterSpacing:1,fontWeight:600 }}>GAME LOG</div>
          <GameLog log={st.log} onCardClick={c=>dispatch({type:"OPEN_DETAIL",card:c})} />
        </div>
      </div>
      {/* Bottom: Assets + Hand */}
      <div style={{ display:"flex",gap:9 }}>
        <div style={{ flex:"0 0 auto",maxWidth:"40%" }}><AssetsPanel st={st} dispatch={dispatch} /></div>
        <div style={{ flex:1 }}><HandPanel st={st} dispatch={dispatch} /></div>
      </div>
      {!settings.multiplayMode && <SoloBar st={st} dispatch={dispatch} />}
      {/* Modals */}
      {st.awaitingTarget && <TargetModal st={st} cardId={st.awaitingTarget} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "DISCARD_TO_LIMIT"    && <DiscardLimitModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "TAXES_SELECT_ASSET"    && <TaxesSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "KICKSTARTER_CHOICE"    && <KickstarterChoiceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "POCKET_CHANGE_SELECT" && <PocketChangeSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "POCKET_CHANGE_REVEAL" && <PocketChangeRevealModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "HELP_WANTED_GIVE"     && <HelpWantedModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "INTERVIEWS_PICK"      && <InterviewsModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "LET_GO_SELECT"        && <LetGoSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "REFRESH_ASSET"        && <RefreshAssetModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "GIVE_BACK_TIE"        && <GiveBackTieModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "NEG_REACTION_SELECT"  && <NegReactionSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "OUTSIDE_HIRE_SELECT"  && <OutsideHireSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "SHRINKAGE_SELECT"      && <ShrinkageSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "OUT_OF_ORDER_SELECT"    && <OutOfOrderSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "UPGRADE_DISCARD"        && <UpgradeDiscardModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type === "UPGRADE_GAIN"           && <UpgradeGainModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="BLUEPRINT_PLACE"     && <BlueprintPlaceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="RETAINER_PLACE"      && <RetainerPlaceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="TRADE_IN_SELECT"     && <TradeInSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="CREDIT_LINE_CHOICE"  && <CreditLineChoiceModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="RND_ACTIVATE"        && <RndActivateModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="MONOPOLY_SELECT"     && <MonopolySelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="BOGO_SELECT"         && <BOGOSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="FRANCHISE_SELECT"    && <FranchiseSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="EXCHANGE_SELECT"     && <ExchangeSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="EYE_SELECT"          && <EyeSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="BTB_SELECT"          && <BTBSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="SF_SELECT"           && <SFSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="AP_ACTIVATE_SELECT"  && <APActivateSelectModal st={st} dispatch={dispatch} />}
      {pressurePlayer && <PressureInfoModal player={pressurePlayer} allPlayers={st.players} dispatch={dispatch} onClose={function(){ setPressurePlayer(null); }} />}
      {st.pendingChoice && st.pendingChoice.type==="FULL_REFUND_PROMPT"  && <FullRefundModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="FULL_REFUND_TARGET"  && <FullRefundTargetModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="PRICE_CHECK_PROMPT" && <PriceCheckPromptModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="COMING_SOON_PEEK"    && <ComingSoonPeekModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="WTP_SELECT"          && <WTPSelectModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="TOTAL_LOSS_PICK_VALUE"&& <TotalLossPickModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="QUICK_EXCHANGE_DISCARD"&&<QuickExchangeModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="MR_SHOW_CARD"        && <MRShowCardModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="VALUATION_PICK"      && <ValuationPickModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="COVER_COSTS_TARGET"  && <CoverCostsTargetModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="MINOR_LOSS_DISCARD"  && <MinorLossDiscardModal st={st} dispatch={dispatch} />}
      {st.pendingChoice && st.pendingChoice.type==="DISCOUNT_CHOICE"     && <DiscountChoiceModal st={st} dispatch={dispatch} />}
      {st.pendingPWChoice && <PaidWorkModal st={st} dispatch={dispatch} />}
            {(st.pendingChoice && ["PU_ASSET_SELECT","PU_FIELD_SELECT","PU_DIRECTION_SELECT"].includes(st.pendingChoice.type)) && <ProductUpdateModal st={st} dispatch={dispatch} />}
            {st.pendingPWChoice && <PaidWorkModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="REBRANDING_SELECT"       && <RebrandingModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="CFR_SELECT"             && <CFRSelectModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="BRIBERY_ASSET_SELECT" && <BriberyAssetModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="BRIBERY_CARD_SELECT"  && <BriberyCardModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="MARKUP_DIRECTION"      && <MarkupDirectionModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="THREE_OF_KIND_SELECT" && <ThreeOfAKindModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="MARKUP_CARD"           && <SimpleCardListModal type="MARKUP_CARD" title="MARK-UP — CHOOSE A CARD" color="#f59e0b" st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="MULTI_TOOL_SELECT"    && <SimpleCardListModal type="MULTI_TOOL_SELECT" title="MULTI-TOOL — RESET AN ASSET" color="#67e8f9" st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="SNEAK_PEAK_SELECT"   && <SneakPeakModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && st.pendingChoice.type==="RECYCLE_CHOICE"      && <RecycleChoiceModal st={st} dispatch={dispatch} />}
            {st.pendingChoice && !["DISCARD_TO_LIMIT","TAXES_SELECT_ASSET","KICKSTARTER_CHOICE","POCKET_CHANGE_SELECT","POCKET_CHANGE_REVEAL","HELP_WANTED_GIVE","INTERVIEWS_PICK","LET_GO_SELECT","REFRESH_ASSET","GIVE_BACK_TIE","NEG_REACTION_SELECT","OUTSIDE_HIRE_SELECT","SHRINKAGE_SELECT","UPGRADE_DISCARD","UPGRADE_GAIN","OUT_OF_ORDER_SELECT","BLUEPRINT_PLACE","RETAINER_PLACE","TRADE_IN_SELECT","CREDIT_LINE_CHOICE","RND_ACTIVATE","MONOPOLY_SELECT","BOGO_SELECT","FRANCHISE_SELECT","EXCHANGE_SELECT","EYE_SELECT","BTB_SELECT","SF_SELECT","AP_ACTIVATE_SELECT","COMING_SOON_PEEK","WTP_SELECT","TOTAL_LOSS_PICK_VALUE","QUICK_EXCHANGE_DISCARD","MR_SHOW_CARD","VALUATION_PICK","COVER_COSTS_TARGET","MINOR_LOSS_DISCARD","DISCOUNT_CHOICE","RECYCLE_CHOICE","FULL_REFUND_PROMPT","FULL_REFUND_TARGET","PRICE_CHECK_PROMPT","SNEAK_PEAK_SELECT","THREE_OF_KIND_SELECT","MULTI_TOOL_SELECT","CFR_SELECT","MARKUP_DIRECTION","MARKUP_CARD","BRIBERY_ASSET_SELECT","BRIBERY_CARD_SELECT","REBRANDING_SELECT","PU_ASSET_SELECT","PU_FIELD_SELECT","PU_DIRECTION_SELECT"].includes(st.pendingChoice.type) && <ChoiceModal st={st} dispatch={dispatch} />}
      {st.reactionWindow  && <ReactionWindow st={st} dispatch={dispatch} isMultiplayer={settings.multiplayMode||false} />}
      {st.detailCard      && <DetailModal card={st.detailCard} st={st} dispatch={dispatch} onClose={()=>dispatch({type:"CLOSE_DETAIL"})} />}
      <ToastLayer toasts={st.toasts} dispatch={dispatch} />
    </div>
  );
}

/* ══════════════════════════════════════════════════════════
   MULTIPLAYER LAYER
   ══════════════════════════════════════════════════════════ */

/* - State filtering: hide other players' hands + set correct viewIdx - */
function filterStateForPlayer(st, gamePlayerId) {
  if (!st) return null;
  var myIdx = st.players.findIndex(function(p){ return p.id === gamePlayerId; });

  // Determine who makes decisions for each pendingChoice type.
  // Most choices: srcPlayer. Some: tgtPlayer (the opponent must respond).
  var tgtPlayerDecides = [
    "SHRINKAGE_SELECT","LET_GO_SELECT","HELP_WANTED_GIVE",
    "OPPONENT_DISCARD","CLIENT_THEFT","NEG_REACTION_SELECT","KICKSTARTER_CHOICE","POCKET_CHANGE_SELECT",
  ];
  var pc = st.pendingChoice;
  var pcDecider = null;
  if (pc) {
    pcDecider = tgtPlayerDecides.indexOf(pc.type) >= 0
      ? (pc.tgtPlayer || pc.srcPlayer)
      : pc.srcPlayer;
  }

  // Who is the current active player (their turn)
  var curGamePlayer = st.players && st.players[st.curIdx];
  var curId = curGamePlayer ? curGamePlayer.id : null;

  // Filter log: hide specific card names on draw entries for non-drawing players
  var filteredLog = st.log ? st.log.map(function(entry) {
    if (!entry || !entry.msg) return entry;
    // Pattern: draws "CardName" — strip the quoted name for other players
    if (entry.type === "draw" && entry.msg.indexOf(gamePlayerId) < 0) {
      var drawMatch = entry.msg.match(/^(.*?) draws "(.+?)"\.$/);
      if (drawMatch) {
        return { ...entry, msg: drawMatch[1] + " draws a card.", cards: [] };
      }
    }
    return entry;
  }) : st.log;

  return {
    ...st,
    viewIdx: myIdx >= 0 ? myIdx : st.viewIdx,
    log: filteredLog,
    // Hide detailCard — each client manages this locally
    detailCard: null,
    detailShopInfo: null,
    // Only show pendingChoice to the decision-maker
    pendingChoice: (pc && pcDecider === gamePlayerId) ? pc : null,
    // Only show PW prompt to its owner
    pendingPWChoice: (st.pendingPWChoice && st.pendingPWChoice.ownerId === gamePlayerId)
      ? st.pendingPWChoice : null,
    // Only show awaitingTarget to the current player
    awaitingTarget: (st.awaitingTarget && curId === gamePlayerId) ? st.awaitingTarget : null,
    // Only send this player's money events — prevents foreign events from
    // filling the queue and causing animation/dismiss mismatches on mobile.
    moneyEvents: (st.moneyEvents||[]).filter(function(e){ return e.pid === gamePlayerId; }),
    // Hide other players' hands
    players: st.players.map(function(p) {
      if (p.id === gamePlayerId) return p;
      return { ...p, hand: p.hand.map(function(c) {
        return { hidden:true, id:"h_"+c.id, type:"HIDDEN", name:"?", value:0, origVal:0 };
      })};
    }),
  };
}

/* - WebSocket hook - */
function useMultiplayerSocket({ onMessage, reconnectInfo }) {
  var wsRef = useRef(null);
  var [connected, setConnected] = useState(false);
  var [reconnecting, setReconnecting] = useState(false);
  var [error, setError] = useState(null);
  var onMsgRef = useRef(onMessage);
  var reconnInfoRef = useRef(reconnectInfo);
  var retryCountRef = useRef(0);
  var retryTimerRef = useRef(null);
  useEffect(function(){ onMsgRef.current = onMessage; }, [onMessage]);
  useEffect(function(){ reconnInfoRef.current = reconnectInfo; }, [reconnectInfo]);

  function connect() {
    var ws = new WebSocket(window.WS_URL);
    wsRef.current = ws;
    ws.onopen = function() {
      setConnected(true); setReconnecting(false); setError(null);
      retryCountRef.current = 0;
      // If we have reconnect info, auto-send RECONNECT on re-open
      var info = reconnInfoRef.current;
      if (info && info.roomCode && info.playerId) {
        ws.send(JSON.stringify({ type:"RECONNECT", roomCode:info.roomCode,
          playerId:info.playerId, name:info.name }));
      }
    };
    ws.onclose = function() {
      setConnected(false);
      // Auto-retry up to 8 times with increasing delays
      if (retryCountRef.current < 8) {
        var delay = Math.min(1000 * Math.pow(1.5, retryCountRef.current), 12000);
        retryCountRef.current++;
        setReconnecting(true);
        retryTimerRef.current = setTimeout(connect, delay);
      } else {
        setReconnecting(false);
        setError("Lost connection. Tap to reconnect.");
      }
    };
    ws.onerror = function() {};
    ws.onmessage = function(e) {
      try { onMsgRef.current(JSON.parse(e.data)); } catch(err) { console.error("WS parse error", err); }
    };
  }

  useEffect(function() {
    connect();
    // Re-connect when page becomes visible (iOS lock screen)
    function onVisibility() {
      if (document.visibilityState === "visible") {
        var ws = wsRef.current;
        if (!ws || ws.readyState === 3 || ws.readyState === 2) {
          retryCountRef.current = 0;
          connect();
        }
      }
    }
    document.addEventListener("visibilitychange", onVisibility);
    return function() {
      document.removeEventListener("visibilitychange", onVisibility);
      clearTimeout(retryTimerRef.current);
      if (wsRef.current) wsRef.current.close();
    };
  }, []);

  function sendMsg(payload) {
    if (wsRef.current && wsRef.current.readyState === 1) {
      wsRef.current.send(JSON.stringify(payload));
    }
  }
  function manualReconnect() {
    retryCountRef.current = 0;
    setError(null);
    connect();
  }
  return { sendMsg, connected, reconnecting, error, manualReconnect };
}

/* - Mode Selection Screen - */
/* - Feedback Modal -
   Uses Formspree for submission. To activate:
   1. Go to https://formspree.io and create a free account
   2. Create a new form, copy the endpoint ID (looks like "xxxxxabc")
   3. Replace FORMSPREE_ENDPOINT below with your endpoint ID
   ----------------------------------------------------------------- */
const FORMSPREE_ENDPOINT = "mzdknzrk"; // ← paste your Formspree ID here

function FeedbackModal({ onClose }) {
  var [name, setName] = useState("");
  var [type, setType] = useState("bug");
  var [msg, setMsg] = useState("");
  var [status, setStatus] = useState("idle"); // idle | sending | sent | error

  var typeLabels = [
    { value:"bug",      label:"Bug Report",       color:"#f87171" },
    { value:"balance",  label:"Balance Issue",    color:"#fbbf24" },
    { value:"ux",       label:"UX / Clarity",     color:"#60a5fa" },
    { value:"idea",     label:"Idea / Suggestion",color:"#4ade80" },
    { value:"other",    label:"Other",            color:"#94a3b8" },
  ];

  function handleSubmit() {
    if (!msg.trim()) return;
    if (FORMSPREE_ENDPOINT === "YOUR_ENDPOINT_ID") {
      alert("Feedback form not configured yet. Ask the developer to set up Formspree!");
      return;
    }
    setStatus("sending");
    fetch("https://formspree.io/f/" + FORMSPREE_ENDPOINT, {
      method: "POST",
      headers: { "Content-Type": "application/json", Accept: "application/json" },
      body: JSON.stringify({
        name: name || "Anonymous",
        type: typeLabels.find(function(t){ return t.value===type; })?.label || type,
        feedback: msg,
        version: GAME_VERSION,
        submitted: new Date().toISOString(),
      }),
    })
      .then(function(r) { return r.json(); })
      .then(function(d) {
        if (d.ok) setStatus("sent");
        else setStatus("error");
      })
      .catch(function() { setStatus("error"); });
  }

  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:1000,padding:20 }}
         onClick={onClose}>
      <div style={{ background:"#0d1520",border:"1px solid #1e3a5f",borderRadius:14,
                    padding:24,maxWidth:400,width:"100%" }}
           onClick={function(e){ e.stopPropagation(); }}>
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16 }}>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:16,fontWeight:700,color:"#f1f5f9" }}>Send Feedback</div>
          <button onClick={onClose} style={{ background:"none",border:"none",color:"#64748b",cursor:"pointer",fontSize:18 }}>✕</button>
        </div>

        {status === "sent" ? (
          <div style={{ textAlign:"center",padding:"24px 0" }}>
            <div style={{ fontSize:15,fontWeight:700,color:"#4ade80",marginBottom:12,letterSpacing:2,fontFamily:"'Rubik',sans-serif" }}>SENT</div>
            <div style={{ color:"#4ade80",fontSize:15,fontWeight:700,marginBottom:6 }}>Thanks for the feedback!</div>
            <div style={{ color:"#64748b",fontSize:12 }}>It's been recorded.</div>
            <button onClick={onClose}
              style={{ marginTop:20,padding:"8px 24px",borderRadius:8,cursor:"pointer",
                       background:"#052e16",border:"1px solid #16a34a",color:"#4ade80",fontSize:13 }}>
              Close
            </button>
          </div>
        ) : (
          <>
            <div style={{ marginBottom:12 }}>
              <div style={{ fontSize:11,color:"#64748b",marginBottom:6,letterSpacing:1 }}>YOUR NAME (optional)</div>
              <input value={name} onChange={function(e){ setName(e.target.value.slice(0,40)); }}
                placeholder="Anonymous"
                style={{ width:"100%",padding:"9px 12px",background:"#0a1220",border:"1px solid #1e293b",
                         borderRadius:8,color:"#f1f5f9",fontSize:13,outline:"none",boxSizing:"border-box" }} />
            </div>
            <div style={{ marginBottom:12 }}>
              <div style={{ fontSize:11,color:"#64748b",marginBottom:6,letterSpacing:1 }}>TYPE</div>
              <div style={{ display:"flex",gap:6,flexWrap:"wrap" }}>
                {typeLabels.map(function(t) {
                  return (
                    <button key={t.value} onClick={function(){ setType(t.value); }}
                      style={{ padding:"5px 10px",borderRadius:6,cursor:"pointer",fontSize:11,fontWeight:600,
                               background: type===t.value ? "#0f172a" : "#1e293b",
                               border: "1px solid " + (type===t.value ? t.color : "#334155"),
                               color: type===t.value ? t.color : "#64748b" }}>
                      {t.label}
                    </button>
                  );
                })}
              </div>
            </div>
            <div style={{ marginBottom:16 }}>
              <div style={{ fontSize:11,color:"#64748b",marginBottom:6,letterSpacing:1 }}>DETAILS</div>
              <textarea value={msg} onChange={function(e){ setMsg(e.target.value.slice(0,1000)); }}
                placeholder="Describe the bug, balance issue, or idea..."
                rows={4}
                style={{ width:"100%",padding:"9px 12px",background:"#0a1220",border:"1px solid #1e293b",
                         borderRadius:8,color:"#f1f5f9",fontSize:13,outline:"none",resize:"vertical",
                         fontFamily:"'Inter',sans-serif",boxSizing:"border-box" }} />
              <div style={{ fontSize:10,color:"#334155",textAlign:"right",marginTop:2 }}>{msg.length}/1000</div>
            </div>
            {status === "error" && (
              <div style={{ fontSize:12,color:"#f87171",marginBottom:10 }}>
                Something went wrong. Try again or contact the developer directly.
              </div>
            )}
            <button onClick={handleSubmit} disabled={!msg.trim() || status==="sending"}
              style={{ width:"100%",padding:"11px 0",borderRadius:8,fontWeight:700,fontSize:14,
                       cursor: msg.trim() && status!=="sending" ? "pointer" : "not-allowed",
                       opacity: msg.trim() && status!=="sending" ? 1 : 0.4,
                       background:"#052e16",border:"2px solid #16a34a",color:"#4ade80" }}>
              {status === "sending" ? "Sending…" : "Submit Feedback"}
            </button>
          </>
        )}
      </div>
    </div>
  );
}

function ModeSelectScreen({ onSelect, onHowToPlay }) {
  var [showNotes, setShowNotes] = useState(false);
  var [showFeedback, setShowFeedback] = useState(false);
  var [showBanner, setShowBanner] = useState(function() {
    try { return localStorage.getItem("sb_notif_seen") !== GAME_VERSION; }
    catch(e) { return false; }
  });
  function dismissBanner() {
    try { localStorage.setItem("sb_notif_seen", GAME_VERSION); } catch(e) {}
    setShowBanner(false);
  }
  return (
    <div className="sb-bg-blue" style={{ position:"fixed",inset:0,display:"flex",flexDirection:"column",
                  alignItems:"center",justifyContent:"center",overflow:"hidden" }}>
      <style>{CSS}</style>

      {/* Version + What's New — top right */}
      <div style={{ position:"absolute",top:16,right:16,display:"flex",alignItems:"center",gap:8 }}>
        <span style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#475569",letterSpacing:1 }}>
          v{GAME_VERSION}
        </span>
        <button onClick={function(){ setShowNotes(true); }}
          style={{ fontSize:10,fontFamily:"'Rubik',sans-serif",fontWeight:700,
                   color:"#94a3b8",background:"#0d1525",border:"1px solid #1e3a5f",
                   borderRadius:5,padding:"3px 10px",cursor:"pointer" }}>
          What's new
        </button>
      </div>

      {/* Solo testing — bottom left */}
      <div style={{ position:"absolute",bottom:16,left:16 }}>
        <button onClick={function(){ onSelect("single"); }}
          style={{ fontSize:10,fontFamily:"'Rubik',sans-serif",fontWeight:600,
                   color:"#475569",background:"none",border:"none",
                   padding:"4px 0",cursor:"pointer",letterSpacing:.5 }}>
          Solo Testing →
        </button>
      </div>

      {/* Feedback — bottom right */}
      <div style={{ position:"absolute",bottom:16,right:16 }}>
        <button onClick={function(){ setShowFeedback(true); }}
          style={{ fontSize:10,fontFamily:"'Rubik',sans-serif",fontWeight:700,
                   color:"#94a3b8",background:"#0d1525",border:"1px solid #1e3a5f",
                   borderRadius:5,padding:"3px 10px",cursor:"pointer" }}>
          Feedback
        </button>
      </div>

      {/* Update notification banner */}
      {showBanner && (
        <div style={{
          position:"absolute", top:52, left:"50%", transform:"translateX(-50%)",
          width:"calc(100% - 32px)", maxWidth:340, zIndex:100,
          background:"#1a1200", border:"1px solid #d97706",
          borderRadius:10, padding:"12px 14px",
          animation:"notifDropIn .32s ease, notifPulse 2.4s ease-in-out 0.5s 3",
          fontFamily:"'Rubik',sans-serif",
        }}>
          <div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",gap:10}}>
            <div style={{flex:1}}>
              <div style={{display:"flex",alignItems:"center",gap:6,marginBottom:5}}>
                <div style={{width:6,height:6,borderRadius:"50%",background:"#f59e0b",flexShrink:0}} />
                <span style={{fontFamily:"'JetBrains Mono',monospace",fontSize:10,color:"#f59e0b",fontWeight:700,letterSpacing:1}}>
                  v{GAME_VERSION} — just updated
                </span>
              </div>
              <div style={{fontSize:12,color:"#94a3b8",lineHeight:1.55,marginBottom:10}}>
                There are new fixes and changes. Check What's New for the full list. If you run into anything or have thoughts, the Feedback button is in the bottom right corner.
              </div>
              <div style={{display:"flex",gap:6}}>
                <button onClick={function(){setShowNotes(true);dismissBanner();}}
                  style={{flex:1,background:"#2a1800",border:"1px solid #d97706",borderRadius:7,
                          color:"#f59e0b",fontSize:11,fontWeight:700,padding:"8px 0",cursor:"pointer",
                          fontFamily:"'Rubik',sans-serif",letterSpacing:.3}}>
                  What's new →
                </button>
                <button onClick={dismissBanner}
                  style={{flex:1,background:"transparent",border:"1px solid #334155",borderRadius:7,
                          color:"#64748b",fontSize:11,fontWeight:700,padding:"8px 0",cursor:"pointer",
                          fontFamily:"'Rubik',sans-serif"}}>
                  Got it
                </button>
              </div>
            </div>
            <button onClick={dismissBanner}
              style={{background:"none",border:"none",color:"#475569",cursor:"pointer",
                      fontSize:16,lineHeight:1,padding:"0 2px",flexShrink:0}}>✕</button>
          </div>
        </div>
      )}

      {/* Main content */}
      <div style={{ width:"100%",maxWidth:340,padding:"0 28px",display:"flex",
                    flexDirection:"column",alignItems:"center" }}>

        {/* Logo block */}
        <div style={{ textAlign:"center",marginBottom:32,width:"100%" }}>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:10,letterSpacing:5,
                        color:"#94a3b8",marginBottom:10,fontWeight:700 }}>
            WELCOME TO
          </div>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontWeight:900,lineHeight:.88,marginBottom:14 }}>
            <div style={{ fontSize:56,color:"#ef4444",letterSpacing:-1 }}>STRICTLY</div>
            <div style={{ fontSize:56,color:"#22c55e",letterSpacing:-1 }}>BUSINESS</div>
          </div>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:9,color:"#94a3b8",
                        letterSpacing:4,marginBottom:18,fontWeight:600 }}>
            THE ENTREPRENEUR CARD GAME
          </div>
        </div>

        {/* Primary action: Create a Lobby */}
        <button onClick={function(){ onSelect("multi"); }}
          style={{ width:"100%",marginBottom:12,padding:"18px 24px",
                   borderRadius:10,cursor:"pointer",textAlign:"center",
                   background:"#052e16",border:"1px solid #16a34a",
                   boxShadow:"0 4px 20px rgba(0,0,0,0.6)" }}>
          <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:18,fontWeight:900,
                        color:"#4ade80",letterSpacing:.3 }}>Create a Lobby</div>
        </button>

        {/* How to Play */}
        {onHowToPlay && (
          <button onClick={onHowToPlay}
            style={{ width:"100%",padding:"14px 24px",borderRadius:10,cursor:"pointer",
                     background:"#1a0808",border:"1px solid #991b1b",
                     fontFamily:"'Rubik',sans-serif",fontSize:13,fontWeight:700,
                     color:"#f87171",letterSpacing:1,
                     boxShadow:"0 2px 10px rgba(0,0,0,0.4)" }}>
            HOW TO PLAY
          </button>
        )}
      </div>

      {/* Update Notes Modal */}
      {showNotes && (
        <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                      alignItems:"center",justifyContent:"center",zIndex:1000,padding:20 }}
             onClick={function(){ setShowNotes(false); }}>
          <div style={{ background:"#0d1520",border:"1px solid #1e3a5f",borderRadius:14,
                        padding:0,maxWidth:420,width:"100%",maxHeight:"82vh",
                        display:"flex",flexDirection:"column",overflow:"hidden" }}
               onClick={function(e){ e.stopPropagation(); }}>
            {/* Header */}
            <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",
                          padding:"18px 20px 14px",borderBottom:"1px solid #1e293b",flexShrink:0 }}>
              <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:15,fontWeight:800,
                            color:"#f1f5f9",letterSpacing:.5 }}>
                What's New
              </div>
              <div style={{ display:"flex",alignItems:"center",gap:10 }}>
                <span style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:11,
                               color:"#f59e0b",background:"#1c1003",border:"1px solid #d97706",
                               borderRadius:5,padding:"2px 9px",fontWeight:700 }}>v{GAME_VERSION}</span>
                <button onClick={function(){ setShowNotes(false); }}
                  style={{ background:"none",border:"none",color:"#475569",cursor:"pointer",
                           fontSize:18,lineHeight:1,padding:2 }}>✕</button>
              </div>
            </div>
            {/* Rendered notes */}
            <div style={{ overflowY:"auto",padding:"16px 20px 20px",flex:1 }}>
              {(function(){
                var sections = [];
                var cur = null;
                UPDATE_NOTES.trim().split("\n").forEach(function(line){
                  line = line.trim();
                  if (!line) return;
                  var hdr = line.match(/^(v[\d.]+)\s*[—-]\s*(.+)$/);
                  if (hdr) {
                    cur = { ver:hdr[1], title:hdr[2], bullets:[] };
                    sections.push(cur);
                  } else if (cur && line.startsWith("•")) {
                    cur.bullets.push(line.slice(1).trim());
                  }
                });
                return sections.map(function(s, si){
                  var isCurrent = si === 0;
                  return (
                    <div key={s.ver} style={{ marginBottom:si<sections.length-1?20:0 }}>
                      {/* Version header */}
                      <div style={{ display:"flex",alignItems:"center",gap:8,marginBottom:10 }}>
                        <span style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:10,fontWeight:800,
                                       padding:"2px 8px",borderRadius:4,
                                       background:isCurrent?"#1c1003":"#131b2e",
                                       border:"1px solid "+(isCurrent?"#d97706":"#1e293b"),
                                       color:isCurrent?"#f59e0b":"#475569" }}>
                          {s.ver}
                        </span>
                        <span style={{ fontFamily:"'Rubik',sans-serif",fontSize:isCurrent?13:12,
                                       fontWeight:isCurrent?700:600,
                                       color:isCurrent?"#f1f5f9":"#64748b",letterSpacing:.2 }}>
                          {s.title}
                        </span>
                      </div>
                      {/* Bullets */}
                      <div style={{ display:"flex",flexDirection:"column",gap:isCurrent?7:5,
                                    paddingLeft:4 }}>
                        {s.bullets.map(function(b,bi){
                          return (
                            <div key={bi} style={{ display:"flex",gap:8,alignItems:"flex-start" }}>
                              <div style={{ width:5,height:5,borderRadius:"50%",flexShrink:0,
                                            marginTop:5,
                                            background:isCurrent?"#4ade80":"#334155" }} />
                              <span style={{ fontSize:isCurrent?12:11,
                                             color:isCurrent?"#cbd5e1":"#475569",
                                             lineHeight:1.55,fontFamily:"'Inter',system-ui,sans-serif" }}>
                                {b}
                              </span>
                            </div>
                          );
                        })}
                      </div>
                      {si < sections.length-1 && (
                        <div style={{ height:1,background:"#0f172a",marginTop:18 }} />
                      )}
                    </div>
                  );
                });
              })()}
            </div>
          </div>
        </div>
      )}

      {/* Feedback Modal */}
      {showFeedback && <FeedbackModal onClose={function(){ setShowFeedback(false); }} />}
    </div>
  );
}

/* - LobbyCardConfig: card toggles + count adjustments for waiting room - */
function LobbyCardConfig({ lobbySettings, updateSettings }) {
  var [expanded, setExpanded] = useState(false);
  var allCardDefs2 = [
    ...ACTIONS_T.map(function(t){ return {...t, deckType:"ACTION"}; }),
    ...REACTIONS_T.map(function(t){ return {...t, deckType:"REACTION"}; }),
    ...ASSETS_T.map(function(t){ return {...t, deckType:"ASSET"}; }),
  ];
  var sections2 = [
    {label:"ACTIONS",   key:"ACTION",   col:"#93c5fd"},
    {label:"REACTIONS", key:"REACTION", col:"#f87171"},
    {label:"ASSETS",    key:"ASSET",    col:"#4ade80"},
  ];
  var disabled2 = new Set(lobbySettings.disabledNames||[]);
  var cardCounts2 = lobbySettings.cardCounts||{};

  function toggleCard(name) {
    var next = new Set(disabled2);
    if (next.has(name)) next.delete(name); else next.add(name);
    updateSettings({ disabledNames: Array.from(next), cardCounts: cardCounts2 });
  }
  function setCount(name, defCount, delta) {
    var cur = cardCounts2[name] !== undefined ? cardCounts2[name] : defCount;
    var next = Math.max(0, cur + delta);
    var nextCounts = Object.assign({}, cardCounts2, {[name]: next});
    updateSettings({ disabledNames: Array.from(disabled2), cardCounts: nextCounts });
  }

  return (
    <div>
      <button onClick={function(){ setExpanded(function(e){ return !e; }); }}
        style={{ width:"100%",padding:"7px 0",borderRadius:7,fontSize:11,fontWeight:700,
                 cursor:"pointer",background:"#0c1833",border:"1px solid #1d4ed8",color:"#60a5fa",
                 display:"flex",alignItems:"center",justifyContent:"center",gap:6 }}>
        {expanded ? "▲ Hide" : "▼ Show"} Card Settings
        <span style={{ fontSize:9,color:"#475569" }}>
          ({allCardDefs2.length - disabled2.size}/{allCardDefs2.length} enabled)
        </span>
      </button>
      {expanded && (
        <div style={{ marginTop:10,maxHeight:320,overflowY:"auto" }}>
          <div style={{ display:"flex",justifyContent:"flex-end",marginBottom:8 }}>
            <button onClick={function(){ updateSettings({ disabledNames:[], cardCounts:{} }); }}
              style={{ fontSize:9,padding:"3px 10px",background:"#052e16",border:"1px solid #16a34a",
                       color:"#4ade80",borderRadius:4,cursor:"pointer",fontWeight:700 }}>
              Reset All
            </button>
          </div>
          {sections2.map(function(sec) {
            var cards = allCardDefs2.filter(function(t){ return t.deckType===sec.key; })
              .slice().sort(function(a,b){ return a.name.localeCompare(b.name); });
            return (
              <div key={sec.key} style={{ marginBottom:12 }}>
                <div style={{ fontSize:9,letterSpacing:2,color:sec.col,fontWeight:700,
                              marginBottom:6,paddingBottom:3,borderBottom:"1px solid #1e293b" }}>
                  {sec.label}
                </div>
                {cards.map(function(card) {
                  var isOff = disabled2.has(card.name);
                  var defCount = card.count !== undefined ? card.count : 1;
                  var curCount = cardCounts2[card.name] !== undefined ? cardCounts2[card.name] : defCount;
                  var isRndLocked2 = card.name === "R&D Budget" && lobbySettings.freeActions;
                  return (
                    <div key={card.name} style={{ display:"flex",alignItems:"center",gap:8,
                                                   padding:"5px 0",borderBottom:"1px solid #0f1828",
                                                   opacity:isRndLocked2?0.4:1 }}>
                      <button onClick={function(){ if(!isRndLocked2) toggleCard(card.name); }}
                        style={{ width:28,height:16,borderRadius:8,cursor:isRndLocked2?"not-allowed":"pointer",border:"none",flexShrink:0,
                                 background:isOff?"#1e293b":"#16a34a",position:"relative",transition:"background .15s" }}>
                        <div style={{ width:12,height:12,borderRadius:"50%",background:"#f1f5f9",
                                      position:"absolute",top:2,
                                      left:isOff?2:14,transition:"left .15s" }}/>
                      </button>
                      <span style={{ flex:1,fontSize:10,color:isOff?"#475569":"#f1f5f9",
                                     textDecoration:isOff?"line-through":"none" }}>
                        {card.name}
                        {isRndLocked2 && <span style={{ fontSize:8,color:"#64748b",marginLeft:4 }}>disabled in Free mode</span>}
                        {!isOff && !isRndLocked2 && card.desc && <div style={{ fontSize:8,color:"#475569",lineHeight:1.3,marginTop:1 }}>{card.desc}</div>}
                      </span>
                      {!isOff && (
                        <div style={{ display:"flex",alignItems:"center",gap:4,flexShrink:0 }}>
                          <button onClick={function(){ setCount(card.name, defCount, -1); }}
                            style={{ width:18,height:18,borderRadius:3,cursor:"pointer",border:"1px solid #334155",
                                     background:"#1e293b",color:"#94a3b8",fontSize:12,lineHeight:1,fontWeight:700 }}>−</button>
                          <span style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:11,
                                         color:curCount!==defCount?"#f59e0b":"#94a3b8",minWidth:14,textAlign:"center" }}>
                            {curCount}
                          </span>
                          <button onClick={function(){ setCount(card.name, defCount, 1); }}
                            style={{ width:18,height:18,borderRadius:3,cursor:"pointer",border:"1px solid #334155",
                                     background:"#1e293b",color:"#94a3b8",fontSize:12,lineHeight:1,fontWeight:700 }}>+</button>
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

/* - MPEventToast: large centered notification for key game events - */
function MPEventToast({ toasts }) {
  if (!toasts || !toasts.length) return null;
  var t = toasts[0];
  var colorMap = {
    turn_self:  { border:"#22c55e", color:"#4ade80" },
    turn_other: { border:"#475569", color:"#e2e8f0" },
    draw:       { border:"#3b82f6", color:"#93c5fd" },
    sell:       { border:"#d97706", color:"#fbbf24" },
    buy:        { border:"#16a34a", color:"#4ade80" },
    reaction:   { border:"#7c3aed", color:"#a78bfa" },
  };
  var s = colorMap[t.kind] || colorMap.turn_other;
  return (
    <div style={{ position:"fixed",inset:0,display:"flex",alignItems:"center",
                  justifyContent:"center",zIndex:1500,pointerEvents:"none" }}>
      <div style={{ background:"rgba(10,15,26,0.96)",border:"2px solid "+s.border,
                    borderRadius:18,padding:"20px 48px",textAlign:"center",
                    animation:"toastPopInOut 3s ease forwards",
                    boxShadow:"0 8px 40px rgba(0,0,0,0.7)" }}>
        <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:22,fontWeight:900,
                      color:s.color,letterSpacing:2,lineHeight:1.2,textTransform:"uppercase" }}>
          {t.line1}
        </div>
        {t.line2 && (
          <div style={{ fontSize:13,color:s.color+"cc",marginTop:4,fontWeight:600 }}>
            {t.line2}
          </div>
        )}
      </div>
    </div>
  );
}

/* - Multiplayer overlay components (module-level for stable React identity) - */

function MPAssetViewPopup({ viewingAssetsOf, gameSt, onClose }) {
  if (!viewingAssetsOf || !gameSt) return null;
  var pl = gameSt.players.find(function(p){ return p.id === viewingAssetsOf; });
  if (!pl) return null;
  return (
    <div style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.82)",display:"flex",
                  alignItems:"center",justifyContent:"center",zIndex:1200,backdropFilter:"blur(4px)" }}
         onClick={function(e){ if(e.target===e.currentTarget) onClose(); }}>
      <div style={{ background:"#0d1520",border:"2px solid #16a34a",borderRadius:14,
                    padding:20,maxWidth:400,width:"92%",maxHeight:"80vh",overflowY:"auto" }}>
        <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:14 }}>
          <div>
            <div style={{ fontSize:10,color:"#4ade80",letterSpacing:1,marginBottom:2 }}>ASSETS</div>
            <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:16,fontWeight:700,color:"#f1f5f9" }}>{pl.name}</div>
          </div>
          <button onClick={onClose} style={{ background:"none",border:"none",color:"#64748b",cursor:"pointer",fontSize:20 }}>✕</button>
        </div>
        {(pl.assets||[]).length === 0 && (
          <div style={{ color:"#64748b",fontSize:12,textAlign:"center",padding:16 }}>No assets</div>
        )}
        {(pl.assets||[]).map(function(a) {
          var sub = SUB_UI[a.sub]||SUB_UI[AS.DURING]||{};
          return (
            <div key={a.id} style={{ background:"#0a1a0a",border:"1px solid #16a34a",borderRadius:8,padding:10,marginBottom:8 }}>
              <div style={{ display:"flex",justifyContent:"space-between",marginBottom:4 }}>
                <div>
                  {sub.lbl && <div style={{ fontSize:7,color:sub.col||"#4ade80",fontWeight:700,background:sub.bg||"#0d1a0d",borderRadius:3,padding:"1px 5px",display:"inline-block",marginBottom:2 }}>{sub.lbl}</div>}
                  <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:13,fontWeight:700,color:"#f1f5f9" }}>
                    {a.name}{a._franchised&&<span style={{fontSize:8,color:"#fde68a",marginLeft:4}}>×2</span>}{a.lockedCard&&<span style={{fontSize:8,color:"#60a5fa",marginLeft:4,background:"#0c1a2e",borderRadius:3,padding:"1px 4px"}}>+ {a.lockedCard.name}</span>}
                  </div>
                </div>
                <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:14,color:"#f59e0b",fontWeight:700 }}>${a.origVal}k</div>
              </div>
              <div style={{ fontSize:9,color:"#94a3b8",lineHeight:1.5 }}>{typeof getDesc==="function"?getDesc(a):a.desc}</div>
              {(a.tokens||[]).length>0&&<div style={{ display:"flex",gap:3,marginTop:4,flexWrap:"wrap" }}>{(a.tokens||[]).map(function(_,i){return <div key={i} style={{width:7,height:7,borderRadius:"50%",background:"#f59e0b"}}/>;})} </div>}
              {a.disabled&&<div style={{fontSize:9,color:"#ef4444",marginTop:4}}>DISABLED</div>}
              {a.status==="USED"&&<div style={{fontSize:9,color:"#64748b",marginTop:4}}>USED</div>}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function MPTurnFlash({ turnFlash }) {
  if (!turnFlash) return null;
  return (
    <div style={{ position:"fixed",inset:0,zIndex:1300,pointerEvents:"none",
                  display:"flex",alignItems:"center",justifyContent:"center" }}>
      <div style={{ background:"rgba(34,197,94,0.15)",border:"2px solid #22c55e",
                    borderRadius:16,padding:"18px 40px",
                    animation:"turnFlashAnim 2.2s ease forwards" }}>
        <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:22,fontWeight:900,
                      color:"#4ade80",textAlign:"center",letterSpacing:2 }}>YOUR TURN</div>
      </div>
    </div>
  );
}

function MPActionNotifications({ notifications, mobile }) {
  if (!notifications || !notifications.length) return null;
  var typeIcons = { money:"+", loss:"-", asset:"◆", reaction:"↩", draw:"↓", discard:"×", turn:"▷" };
  var typeColors = { money:"#4ade80", loss:"#f87171", asset:"#f59e0b", reaction:"#a78bfa", draw:"#60a5fa", discard:"#94a3b8", turn:"#64748b" };
  var containerStyle = mobile
    ? { position:"fixed",bottom:"calc(178px + env(safe-area-inset-bottom,0px))",
        left:0,right:0,maxWidth:480,margin:"0 auto",
        display:"flex",flexDirection:"column-reverse",gap:6,padding:"0 14px",
        zIndex:1200,pointerEvents:"none" }
    : { position:"fixed",top:12,right:12,zIndex:1200,display:"flex",
        flexDirection:"column",gap:6,maxWidth:260,pointerEvents:"none" };
  return (
    <div style={containerStyle}>
      {notifications.slice(-3).map(function(n) {
        var col = typeColors[n.type]||"#94a3b8";
        var icon = typeIcons[n.type]||"·";
        return (
          <div key={n.id} style={{ background:"rgba(13,21,32,0.96)",border:"1px solid #1e293b",
                                    borderLeft:"3px solid "+col,
                                    borderRadius:8,padding:"8px 12px",
                                    display:"flex",alignItems:"flex-start",gap:8,
                                    animation:"slideUp .25s ease",
                                    boxShadow:"0 4px 12px rgba(0,0,0,0.5)" }}>
            <span style={{ fontSize:11,flexShrink:0,color:col,fontWeight:700,lineHeight:1 }}>{icon}</span>
            <div style={{ fontSize:11,color:"#e2e8f0",lineHeight:1.4 }}>{(n.msg||'').replace(/^[^a-zA-Z$"(\[]+/,'')}</div>
          </div>
        );
      })}
    </div>
  );
}

function MPWaitingBanner({ gameSt }) {
  if (!gameSt) return null;
  var rw = gameSt.reactionWindow;
  if (!rw) return null;
  var myPlayer = gameSt.players[gameSt.viewIdx];
  if (!myPlayer) return null;
  var myReactor = rw.reactors && rw.reactors.find(function(r){ return r.pid === myPlayer.id; });
  if (myReactor && myReactor.decision === "PENDING" && myReactor.canReact) return null;
  var pendingNames = (rw.reactors||[])
    .filter(function(r){ return r.decision === "PENDING"; })
    .map(function(r){ var pl = gameSt.players.find(function(p){ return p.id===r.pid; }); return pl?pl.name:"?"; });
  if (!pendingNames.length) return null;
  return (
    <div style={{ position:"fixed",bottom:70,left:"50%",transform:"translateX(-50%)",
                  background:"#0d1520",border:"1px solid #334155",borderRadius:10,
                  padding:"8px 16px",zIndex:800,display:"flex",alignItems:"center",gap:8,
                  whiteSpace:"nowrap",fontSize:11,color:"#94a3b8" }}>
      <span style={{ color:"#f59e0b" }}>⏳</span>
      Waiting for {pendingNames.join(", ")}…
    </div>
  );
}

function MPDisconnectBanner({ connected, reconnecting, manualReconnect }) {
  if (connected) return null;
  return (
    <div style={{ position:"fixed",top:0,left:0,right:0,zIndex:2000,
                  background:reconnecting?"#1c1003":"#1c0a0a",
                  borderBottom:"2px solid "+(reconnecting?"#d97706":"#ef4444"),
                  padding:"8px 16px",display:"flex",alignItems:"center",gap:10 }}>
      <div style={{ flex:1,fontSize:12,color:reconnecting?"#fbbf24":"#fca5a5",fontWeight:600 }}>
        {reconnecting ? "⏳ Reconnecting…" : "📵 Disconnected from server"}
      </div>
      {!reconnecting && (
        <button onClick={manualReconnect}
          style={{ padding:"5px 14px",fontSize:11,fontWeight:700,borderRadius:6,cursor:"pointer",
                   background:"#7f1d1d",border:"1px solid #ef4444",color:"#fca5a5" }}>
          Reconnect
        </button>
      )}
    </div>
  );
}


/* - Multi Player App - */
function MultiPlayerApp({ onBack }) {
  var [phase, setPhase] = useState("lobby");
  var [nameInput, setNameInput] = useState(function(){
    try { return localStorage.getItem("sb_mp_name")||""; } catch(e){ return ""; }
  });
  var [codeInput, setCodeInput] = useState("");
  var [myPlayerId, setMyPlayerId] = useState(null);  // room-level ID
  var [roomCode, setRoomCode] = useState(null);
  var [isHost, setIsHost] = useState(false);
  var [players, setPlayers] = useState([]);           // room players [{id,name,isHost,connected}]
  var [playerIdMap, setPlayerIdMap] = useState([]);   // room playerIds in game order
  var [roomNameToId, setRoomNameToId] = useState({});  // name -> room playerId (for remapping after shuffle)
  var [lobbyError, setLobbyError] = useState(null);
  var [qrExpanded, setQrExpanded] = useState(false);
  // Persist connection info for reconnect (survives iOS lock screen drops)
  var [reconnectInfo, setReconnectInfo] = useState(function() {
    try { return JSON.parse(localStorage.getItem("sb_reconnect")||"null"); } catch(e) { return null; }
  });
  function saveReconnectInfo(info) {
    setReconnectInfo(info);
    try { localStorage.setItem("sb_reconnect", JSON.stringify(info)); } catch(e) {}
  }
  function clearReconnectInfo() {
    setReconnectInfo(null);
    try { localStorage.removeItem("sb_reconnect"); } catch(e) {}
  }
  var [lobbySettings, setLobbySettings] = useState({ turns:15, disabledNames:["R&D Budget"], cardCounts:{}, freeActions:true });
  var [settings, setSettings] = useState({ moneyPopups:true, mobileView: typeof window!=="undefined" && window.innerWidth<640 });
  var [localDetailCard, setLocalDetailCard] = useState(null);
  var [notifications, setNotifications] = useState([]);
  var [mpEventToasts, setMpEventToasts] = useState([]);
  var prevLogLenRef = useRef(-1);  // -1 = not yet initialized; set on first render // detail card is local, not shared
  var [viewingAssetsOf, setViewingAssetsOf] = useState(null); // player whose assets we're viewing
  var [turnFlash, setTurnFlash] = useState(false);
  var [prevCurIdx, setPrevCurIdx] = useState(null); // null = not yet set

  // Host runs the game reducer locally
  var [localSt, localDispatchFn] = useReducer(reducer, null);
  // Non-host receives filtered state via WebSocket
  var [remoteSt, setRemoteSt] = useState(null);

  var { sendMsg, connected, reconnecting, error: wsError, manualReconnect } = useMultiplayerSocket({ onMessage: handleMessage, reconnectInfo });

  function handleMessage(msg) {
    switch (msg.type) {
      case "ROOM_CREATED":
        setRoomCode(msg.code); setMyPlayerId(msg.playerId);
        setIsHost(true); setPlayers(msg.players);
        saveReconnectInfo({ roomCode:msg.code, playerId:msg.playerId, name:nameInput||"", isHost:true });
        setPhase("waiting");
        break;
      case "ROOM_JOINED":
        setRoomCode(msg.code); setMyPlayerId(msg.playerId);
        setIsHost(false); setPlayers(msg.players);
        if (msg.settings) setLobbySettings(msg.settings);
        saveReconnectInfo({ roomCode:msg.code, playerId:msg.playerId, name:msg.name||"", isHost:false });
        setPhase("waiting");
        break;
      case "RECONNECTED":
        setRoomCode(msg.code); setMyPlayerId(msg.playerId);
        setIsHost(msg.isHost||false); setPlayers(msg.players||[]);
        if (msg.settings) setLobbySettings(msg.settings);
        saveReconnectInfo({ roomCode:msg.code, playerId:msg.playerId, name:"", isHost:msg.isHost||false });
        setPhase(msg.gameStarted ? "ingame" : "waiting");
        break;
      case "HOST_STATE_RESTORE":
        // Host reconnected with lost React state — restore from server's cached full state
        localDispatchFn({ type:"RESTORE_STATE", state:msg.state });
        break;
      case "RECONNECT_REQUEST":
        // A player reconnected — re-broadcast current state immediately
        if (isHost && localSt && playerIdMap.length > 0) {
          var rr_ps = {};
          localSt.players.forEach(function(gp) {
            var rid = roomNameToId[gp.name];
            if (rid) rr_ps[rid] = filterStateForPlayer(localSt, gp.id);
          });
          sendMsg({ type:"STATE_UPDATE", playerStates:rr_ps, fullState:localSt });
        }
        break;
      case "PLAYER_REJOINED":
        setPlayers(msg.players); break;
      case "PLAYER_JOINED": case "PLAYERS_UPDATED": case "PLAYER_LEFT":
        setPlayers(msg.players); break;
      case "HOST_CHANGED":
        setPlayers(msg.players);
        if (msg.newHostId === myPlayerId) setIsHost(true);
        break;
      case "SETTINGS_UPDATED":
        if (!isHost) setLobbySettings(msg.settings); break;
      case "GAME_STARTING":
        // Build name->roomId map so we can remap playerIdMap after engine shuffles turn order
        var gs_nameToId = {};
        (msg.playerNames||[]).forEach(function(n,i){ gs_nameToId[n] = (msg.playerIds||[])[i]; });
        setRoomNameToId(gs_nameToId);
        setPlayerIdMap(msg.playerIds || []);
        setPlayers(msg.players);
        setPhase("ingame");
        if (isHost) {
          localDispatchFn({ type:"INIT", names:msg.playerNames, turns:lobbySettings.turns,
            disabledNames:new Set(lobbySettings.disabledNames||[]), countOverrides:lobbySettings.cardCounts||{}, freeActions:!!(lobbySettings.freeActions) });
        }
        break;
      case "GAME_ACTION":
        // Host only: action relayed from a non-host player
        if (isHost) localDispatchFn(msg.action);
        break;
      case "STATE_UPDATE":
        if (!isHost) setRemoteSt(msg.state);
        break;
      case "RETURN_TO_LOBBY":
        // Host broadcasted rematch — all clients return to waiting room
        setRemoteSt(null);
        setPhase("waiting");
        prevLogLenRef.current = -1;
        break;
      case "ERROR":
        setLobbyError(msg.message); break;
    }
  }

  // Compute log length early so useEffect dep arrays work correctly
  // (gameSt is assigned late in the function body, so referencing it in deps evaluates to undefined)
  var mpLogLen = isHost
    ? (localSt && localSt.log ? localSt.log.length : 0)
    : (remoteSt && remoteSt.log ? remoteSt.log.length : 0);
  var mpCurIdx = isHost
    ? (localSt ? localSt.curIdx : -1)
    : (remoteSt ? remoteSt.curIdx : -1);

  // Host: broadcast filtered state to every player whenever state changes
  useEffect(function() {
    if (!isHost || !localSt || phase !== "ingame" || playerIdMap.length === 0) return;
    var playerStates = {};
    // Remap: use player NAME to find the correct room-level ID
    // (engine may have shuffled turn order, so gameIdx != join order)
    localSt.players.forEach(function(gamePlayer, gameIdx) {
      var roomId = roomNameToId[gamePlayer.name];
      if (!roomId) return; // player name not in room (shouldn't happen)
      playerStates[roomId] = filterStateForPlayer(localSt, gamePlayer.id);
    });
    sendMsg({ type:"STATE_UPDATE", playerStates, fullState:localSt });
  }, [localSt, isHost, phase, playerIdMap]);

  // Dispatch: host runs reducer; others relay via WebSocket
  function dispatch(action) {
    // Detail card is pure local UI — never send to engine or other players
    if (action.type === "OPEN_DETAIL") { setLocalDetailCard(action.card); return; }
    if (action.type === "CLOSE_DETAIL") { setLocalDetailCard(null); return; }
    // Non-host: certain automatic engine actions should be ignored
    // START_TURN fires from useEffect on ALL clients, but only host should process it
    // ANIMATION_DONE is also local — don't relay
    if (!isHost) {
      // Only block START_TURN (auto-fires from useEffect on all clients)
      // ANIMATION_DONE and DISMISS_MONEY_EVENT must be relayed so the host
      // can process resumeActions and clear money events
      if (action.type === "START_TURN") return;
      sendMsg({ type:"GAME_ACTION", action });
      return;
    }
    localDispatchFn(action);
  }

  // Watch game log for new entries → show action notifications for observers
  useEffect(function() {
    if (!gameSt || !gameSt.log) return;
    var myPlayer = gameSt.players[gameSt.viewIdx];
    var myId = myPlayer ? myPlayer.id : null;
    var log = gameSt.log;
    // First render: just record length, show nothing historical
    if (prevLogLenRef.current === -1) {
      prevLogLenRef.current = log.length;
      return;
    }
    var prev = prevLogLenRef.current;
    if (log.length > prev) {
      // New entries since last render
      var newEntries = log.slice(prev);
      var notifTypes = { money:true, loss:true, asset:true, reaction:true, draw:true, discard:true, turn:true, info:false, warn:false, sys:false, hook:false };
      newEntries.forEach(function(entry) {
        if (!entry || !notifTypes[entry.type]) return;
        // Skip "ends their turn" and ✅ system noise
        if (entry.type === "turn" && entry.msg && (entry.msg.indexOf("ends their turn") >= 0 || entry.msg.indexOf("✅") === 0)) return;
        // Skip if this entry is about myself
        var myName = myPlayer ? myPlayer.name : null;
        // Skip entries that are about the viewing player themselves
        if (myName) {
          var msgNoEmoji = entry.msg ? entry.msg.replace(/^[^ a-zA-Z0-9]+\s*/, '') : '';
          if (msgNoEmoji.indexOf(myName) === 0) return;
        }
        var displayMsg = entry.msg;
        var notif = { id: entry.id+"_n", msg: displayMsg, type: entry.type, ts: Date.now() };
        setNotifications(function(prev2) {
          if (prev2.some(function(n){ return n.id === notif.id; })) return prev2;
          return [...prev2.slice(-4), notif];
        });
        setTimeout(function() {
          setNotifications(function(p) { return p.filter(function(n){ return n.id !== notif.id; }); });
        }, 3500);
        // (large toasts handled by dedicated effect below)
      });
    }
    prevLogLenRef.current = log.length;
  }, [mpLogLen]);

  // Dedicated large toast for draw / sell / buy events
  var prevToastLogLenRef = React.useRef(-1);
  useEffect(function() {
    if (!gameSt || !gameSt.log) return;
    var log = gameSt.log;
    if (prevToastLogLenRef.current === -1) { prevToastLogLenRef.current = log.length; return; }
    var prev = prevToastLogLenRef.current;
    prevToastLogLenRef.current = log.length;
    if (log.length <= prev) return;
    var myPlayer2 = gameSt.players && gameSt.players[gameSt.viewIdx];
    var myName2 = myPlayer2 ? myPlayer2.name : null;
    // Process new entries for event toasts
    var toasts2 = [];
    var batchEntries = log.slice(prev);
    var hasTurnStart = batchEntries.some(function(e){ return e && e.type==='turn' && e.msg && e.msg.indexOf('begins')>=0; });
    batchEntries.forEach(function(entry) {
      if (!entry || !entry.msg) return;
      var kind2 = null, l1 = null, l2 = null;
      var cleanMsg = entry.msg.replace(/^[^a-zA-Z0-9]+/, '');
      // Skip if about myself
      if (myName2 && cleanMsg.indexOf(myName2) === 0) return;
      // Skip auto-draw at turn start (the turn toast already covers this)
      if (entry.type === 'draw' && hasTurnStart) return;
      if (entry.type === 'draw') {
        var dm2 = cleanMsg.match(/^(.+?) draws (\d+|a) cards?/);
        if (dm2) { kind2='draw'; l1=dm2[1]; l2='drew '+(dm2[2]==='a'?'a card':dm2[2]+' cards'); }
      } else if (entry.type === 'money' && cleanMsg.indexOf('sold') >= 0) {
        var sm2 = cleanMsg.match(/^(.+?) sold "(.+?)"/);
        if (sm2) { kind2='sell'; l1=sm2[1]; l2='sold "'+sm2[2]+'"'; }
      } else if (entry.type === 'asset' && cleanMsg.indexOf('buys') >= 0) {
        var bm2 = cleanMsg.match(/^(.+?) buys "(.+?)"/);
        if (bm2) { kind2='buy'; l1=bm2[1]; l2='bought "'+bm2[2]+'"'; }
      }
      if (kind2 && l1) toasts2.push({ id:entry.id+'_t', kind:kind2, line1:l1, line2:l2 });
    });
    if (toasts2.length > 0) {
      setMpEventToasts(function() { return [toasts2[0]]; }); // show first one
      setTimeout(function() { setMpEventToasts(function() { return []; }); }, 3000);
    }
  }, [mpLogLen]);

  // Detect turn change → show "your turn" flash
  var myRoomIdxForFlash = playerIdMap.indexOf(myPlayerId);
  var myGameIdxForFlash = isHost && localSt ? myRoomIdxForFlash : -1;
  useEffect(function() {
    if (!gameSt || !gameSt.players) return;
    var curIdx2 = gameSt.curIdx;
    var myIdx2 = gameSt.viewIdx; // viewIdx is always set to our player by filterStateForPlayer
    if (curIdx2 !== prevCurIdx && prevCurIdx !== null) {
      // Always show whose turn it is
      var curPlayer = gameSt.players[curIdx2];
      var isMyTurn2 = curIdx2 === myIdx2;
      if (isMyTurn2) {
        var turnNotif = { id:"turn_"+Date.now(), msg:"It's your turn!", type:"turn", ts:Date.now() };
        setNotifications(function(p) { return [...p.slice(-4), turnNotif]; });
        setTimeout(function(){ setNotifications(function(p){ return p.filter(function(n){ return n.id !== turnNotif.id; }); }); }, 4000);
        setMpEventToasts(function() { return [{ id:'myturn_'+Date.now(), kind:'turn_self', line1:'YOUR TURN', line2:null }]; });
        setTimeout(function(){ setMpEventToasts(function(){ return []; }); }, 2500);
        } else if (curPlayer) {
        // Show other player's turn
        var otherTurnToast = { id:'otherturn_'+Date.now(), kind:'turn_other', line1:curPlayer.name+"'s Turn", line2:null };
        setMpEventToasts(function() { return [otherTurnToast]; });
        setTimeout(function(){ setMpEventToasts(function(){ return []; }); }, 3000);
      }
    }
    if (prevCurIdx !== curIdx2) setPrevCurIdx(curIdx2);
  }, [mpCurIdx]);

  // Derive the state this client should display (always filtered to own player)
  function getDisplayState() {
    if (!isHost) return remoteSt;
    if (!localSt) return null;
    // Find the game player whose name maps to myPlayerId in the room
    var myGamePlayer = (function() {
      var myName = Object.keys(roomNameToId).find(function(n){ return roomNameToId[n] === myPlayerId; });
      if (myName) return localSt.players && localSt.players.find(function(p){ return p.name === myName; });
      // Fallback: original join-order index
      var fallbackIdx = playerIdMap.indexOf(myPlayerId);
      return localSt.players && localSt.players[fallbackIdx >= 0 ? fallbackIdx : 0];
    })();
    if (!myGamePlayer) return localSt;
    return filterStateForPlayer(localSt, myGamePlayer.id);
  }

  var gameSt = getDisplayState();

  // ── Overlay helpers (defined at module level for stable React identity) ──
  // MPAssetViewPopup, MPTurnFlash, MPActionNotifications, MPWaitingBanner, MPDisconnectBanner

  // ── Lobby ────────────────────────────────────  // ── Lobby ──────────────────────────────────────────────────────────────
  if (phase === "lobby") {
    return (
      <div className="sb-bg-blue" style={{ position:"fixed",inset:0,display:"flex",flexDirection:"column",
                    alignItems:"center",justifyContent:"center",padding:20 }}>
        <style>{CSS}</style>
        <div style={{ width:"100%",maxWidth:380 }}>
          <button onClick={onBack}
            style={{ background:"#0d1520",border:"1px solid #1e293b",borderRadius:8,
                     color:"#94a3b8",cursor:"pointer",fontSize:12,fontWeight:600,
                     marginBottom:24,padding:"7px 16px",fontFamily:"'Rubik',sans-serif",
                     letterSpacing:.5 }}>← Back</button>
          <div style={{ textAlign:"center",marginBottom:20 }}>
            <div style={{ fontFamily:"'Rubik',sans-serif",fontWeight:900,lineHeight:.9,marginBottom:8 }}>
              <div style={{ fontSize:38,color:"#ef4444",letterSpacing:-1 }}>STRICTLY</div>
              <div style={{ fontSize:38,color:"#22c55e",letterSpacing:-1 }}>BUSINESS</div>
            </div>
            <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:9,color:"#94a3b8",
                          letterSpacing:4,fontWeight:600 }}>THE ENTREPRENEUR CARD GAME</div>
          </div>

          {/* Static QR code — links to this page so others can scan to join */}
          {qrExpanded && (
            <div onClick={function(){ setQrExpanded(false); }}
              style={{ position:"fixed",inset:0,background:"rgba(0,0,0,0.92)",zIndex:999,
                       display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",
                       gap:16,cursor:"pointer" }}>
              <div style={{ fontSize:10,letterSpacing:2,color:"#64748b",fontWeight:700 }}>TAP TO CLOSE</div>
              <img src={"https://api.qrserver.com/v1/create-qr-code/?data="+encodeURIComponent(window.location.origin)+"&size=280x280&format=svg&bgcolor=13-21-32&color=245-158-11&margin=6"}
                   alt="Scan to join" width={280} height={280}
                   style={{ borderRadius:12,display:"block" }} />
              <div style={{ fontSize:13,fontWeight:700,color:"#f59e0b" }}>Scan to open the game</div>
              <div style={{ fontSize:11,color:"#94a3b8" }}>Then enter the room code to connect</div>
            </div>
          )}
          <div onClick={function(){ setQrExpanded(true); }}
            style={{ display:"flex",alignItems:"center",gap:12,marginBottom:20,padding:"10px 14px",
                      background:"#0d1520",border:"1px solid #1e3a5f",borderRadius:10,cursor:"pointer" }}>
            <img src={"https://api.qrserver.com/v1/create-qr-code/?data="+encodeURIComponent(window.location.origin)+"&size=72x72&format=svg&bgcolor=13-21-32&color=245-158-11&margin=3"}
                 alt="Scan to join" width={72} height={72}
                 style={{ borderRadius:6,flexShrink:0 }} />
            <div>
              <div style={{ fontSize:11,fontWeight:700,color:"#f59e0b",marginBottom:3 }}>Scan to join</div>
              <div style={{ fontSize:10,color:"#94a3b8",lineHeight:1.5 }}>
                Opens the game on your device. Enter the room code to connect.
              </div>
              <div style={{ fontSize:9,color:"#64748b",marginTop:3 }}>Tap to enlarge</div>
            </div>
          </div>

          <MPDisconnectBanner connected={connected} reconnecting={reconnecting} manualReconnect={manualReconnect} />
          {(wsError || lobbyError) && (
            <div style={{ background:"#1c0a0a",border:"1px solid #7f1d1d",borderRadius:8,
                          padding:"10px 14px",color:"#fca5a5",fontSize:12,marginBottom:16,
                          display:"flex",justifyContent:"space-between",alignItems:"center" }}>
              {wsError || lobbyError}
              {lobbyError && <button onClick={function(){ setLobbyError(null); }} style={{ color:"#f87171",background:"none",border:"none",cursor:"pointer",fontSize:14 }}>✕</button>}
            </div>
          )}

          <div style={{ marginBottom:14 }}>
            <div style={{ fontSize:11,color:"#94a3b8",marginBottom:6,letterSpacing:1 }}>YOUR NAME</div>
            <input value={nameInput} onChange={function(e){ var v=e.target.value.slice(0,20); setNameInput(v); try{localStorage.setItem("sb_mp_name",v);}catch(x){} }}
              placeholder="Enter your name" onKeyDown={function(e){ if(e.key==="Enter"&&nameInput.trim()&&connected) { setIsHost(true); sendMsg({ type:"CREATE_ROOM", name:nameInput.trim() }); } }}
              style={{ width:"100%",padding:"10px 12px",background:"#0d1520",border:"1px solid #1e3a5f",
                       borderRadius:8,color:"#f1f5f9",fontSize:14,outline:"none" }} />
          </div>

          <button disabled={!nameInput.trim()||!connected}
            onClick={function(){ sendMsg({ type:"CREATE_ROOM", name:nameInput.trim() }); }}
            style={{ width:"100%",padding:"12px 0",borderRadius:8,fontSize:14,fontWeight:700,
                     cursor:nameInput.trim()&&connected?"pointer":"not-allowed",
                     opacity:nameInput.trim()&&connected?1:0.4,marginBottom:10,
                     background:"#052e16",border:"2px solid #16a34a",color:"#4ade80" }}>
            Create Room
          </button>

          <div style={{ display:"flex",alignItems:"center",gap:8,marginBottom:10 }}>
            <div style={{ flex:1,height:1,background:"#1e293b" }}/><div style={{ fontSize:11,color:"#94a3b8" }}>or join existing</div><div style={{ flex:1,height:1,background:"#1e293b" }}/>
          </div>

          <div style={{ display:"flex",gap:8 }}>
            <input value={codeInput} onChange={function(e){ setCodeInput(e.target.value.toUpperCase().slice(0,6)); }}
              placeholder="ROOM CODE"
              style={{ flex:1,padding:"10px 12px",background:"#0d1520",border:"1px solid #1e3a5f",
                       borderRadius:8,color:"#f1f5f9",fontSize:14,fontFamily:"'JetBrains Mono',monospace",
                       outline:"none",letterSpacing:3 }} />
            <button disabled={!nameInput.trim()||!codeInput.trim()||!connected}
              onClick={function(){ sendMsg({ type:"JOIN_ROOM", name:nameInput.trim(), code:codeInput.trim() }); }}
              style={{ padding:"10px 18px",borderRadius:8,fontSize:13,fontWeight:700,
                       cursor:nameInput.trim()&&codeInput.trim()&&connected?"pointer":"not-allowed",
                       opacity:nameInput.trim()&&codeInput.trim()&&connected?1:0.4,
                       background:"#0c1833",border:"2px solid #1d4ed8",color:"#60a5fa" }}>
              Join
            </button>
          </div>
          {!connected && <div style={{ fontSize:11,color:"#94a3b8",textAlign:"center",marginTop:12 }}>Connecting…</div>}
        </div>
      </div>
    );
  }

  // ── Waiting room ──────────────────────────────────────────────────────
  if (phase === "waiting") {
    var connPlayers = players.filter(function(p){ return p.connected; });
    var canStart = isHost && connPlayers.length >= 2;
    var isTwoPlayer = connPlayers.length === 2;
    var isSixPlayer = connPlayers.length === 6;

    function updateSettings(patch) {
      var next = { ...lobbySettings, ...patch };
      setLobbySettings(next);
      sendMsg({ type:"UPDATE_SETTINGS", settings:next });
    }

    return (
      <div className="sb-bg-blue" style={{ position:"fixed",inset:0,overflowY:"auto",padding:"24px 16px" }}>
        <style>{CSS}</style>
        <div style={{ maxWidth:440,margin:"0 auto" }}>
          <div style={{ display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:20 }}>
            <div>
              <div style={{ fontFamily:"'Rubik',sans-serif",fontSize:18,fontWeight:900 }}>
                <span style={{ color:"#ef4444" }}>STRICTLY </span><span style={{ color:"#22c55e" }}>BUSINESS</span>
              </div>
              <div style={{ fontSize:10,color:"#64748b",letterSpacing:1 }}>WAITING ROOM</div>
            </div>
            <div style={{ textAlign:"right" }}>
              <div style={{ fontSize:9,color:"#64748b",marginBottom:2 }}>ROOM CODE</div>
              <div style={{ fontFamily:"'JetBrains Mono',monospace",fontSize:24,fontWeight:700,
                            color:"#f59e0b",letterSpacing:4 }}>{roomCode}</div>
            </div>
          </div>

          {/* 2-player advisory */}
          {isTwoPlayer && isHost && (
            <div style={{ background:"#1c1003",border:"1px solid #d97706",borderLeft:"3px solid #f59e0b",
                          borderRadius:6,padding:"10px 14px",marginBottom:14,fontSize:11,
                          color:"#fbbf24",lineHeight:1.6,fontFamily:"'Rubik',sans-serif" }}>
              <strong style={{ letterSpacing:.5 }}>2 PLAYERS:</strong> The game works but is best with 3–5. You can still start if you're testing or just want to play.
            </div>
          )}
          {/* 6-player advisory */}
          {isSixPlayer && isHost && (
            <div style={{ background:"#1c1003",border:"1px solid #d97706",borderLeft:"3px solid #f59e0b",
                          borderRadius:6,padding:"10px 14px",marginBottom:14,fontSize:11,
                          color:"#fbbf24",lineHeight:1.6,fontFamily:"'Rubik',sans-serif" }}>
              <strong style={{ letterSpacing:.5 }}>6 PLAYERS:</strong> The game is recommended for 3–5. It will work with 6, but expect longer turns and a tighter economy.
            </div>
          )}

          {/* Player list */}
          <div style={{ background:"#0d1520",border:"1px solid #1e3a5f",borderRadius:12,padding:14,marginBottom:14 }}>
            <div style={{ fontSize:10,color:"#64748b",letterSpacing:1,marginBottom:10 }}>
              PLAYERS ({connPlayers.length}/6) — best with 3–5
            </div>
            {players.map(function(p) {
              var isMe = p.id === myPlayerId;
              return (
                <div key={p.id} style={{ display:"flex",alignItems:"center",gap:8,padding:"8px 0",borderBottom:"1px solid #1e293b" }}>
                  <div style={{ width:8,height:8,borderRadius:"50%",background:p.connected?"#22c55e":"#475569",flexShrink:0 }}/>
                  <div style={{ flex:1,fontSize:13,fontWeight:700,color:isMe?"#f59e0b":"#f1f5f9" }}>
                    {p.name}{isMe?" (you)":""}
                  </div>
                  {p.isHost && <div style={{ fontSize:9,color:"#f59e0b",background:"#1c1003",border:"1px solid #d97706",borderRadius:4,padding:"1px 6px",letterSpacing:1 }}>HOST</div>}
                  {!p.connected && <div style={{ fontSize:9,color:"#94a3b8" }}>disconnected</div>}
                </div>
              );
            })}
            {connPlayers.length < 6 && (
              <div style={{ fontSize:11,color:"#475569",marginTop:10,textAlign:"center" }}>
                Share code <span style={{ color:"#f59e0b",fontFamily:"'JetBrains Mono',monospace",letterSpacing:2 }}>{roomCode}</span> to invite more players
              </div>
            )}
          </div>

          {/* Settings (host only) */}
          {isHost && (
            <div style={{ background:"#0d1520",border:"1px solid #1e3a5f",borderRadius:12,padding:14,marginBottom:14 }}>
              <div style={{ fontSize:10,color:"#64748b",letterSpacing:1,marginBottom:10 }}>GAME LENGTH</div>
              <div style={{ display:"flex",gap:8,flexWrap:"wrap",marginBottom:14 }}>
                {[10,15,20,25].map(function(t) {
                  return (
                    <button key={t} onClick={function(){ updateSettings({turns:t}); }}
                      style={{ padding:"6px 14px",borderRadius:6,fontSize:12,fontWeight:700,cursor:"pointer",
                               background:lobbySettings.turns===t?"#1d4ed8":"#1e293b",
                               border:"1px solid "+(lobbySettings.turns===t?"#3b82f6":"#334155"),
                               color:lobbySettings.turns===t?"#93c5fd":"#94a3b8" }}>
                      {t} turns
                    </button>
                  );
                })}
              </div>
              <div style={{ marginTop:10 }}>
                <div style={{ fontSize:10,color:"#64748b",letterSpacing:1,marginBottom:8 }}>ACTION RULES</div>
                <div style={{ display:"flex",gap:8,marginBottom:8 }}>
                  <button onClick={function(){
                      var dn = new Set(lobbySettings.disabledNames||[]);
                      dn.delete("R&D Budget");
                      updateSettings({freeActions:false, disabledNames:Array.from(dn)});
                    }}
                    style={{ flex:1,padding:"7px 0",borderRadius:6,fontSize:11,fontWeight:700,cursor:"pointer",
                             background:!lobbySettings.freeActions?"#0c1833":"#1e293b",
                             border:"1px solid "+(!lobbySettings.freeActions?"#3b82f6":"#334155"),
                             color:!lobbySettings.freeActions?"#93c5fd":"#94a3b8" }}>
                    Limited
                  </button>
                  <button onClick={function(){
                      var dn = new Set(lobbySettings.disabledNames||[]);
                      dn.add("R&D Budget");
                      updateSettings({freeActions:true, disabledNames:Array.from(dn)});
                    }}
                    style={{ flex:1,padding:"7px 0",borderRadius:6,fontSize:11,fontWeight:700,cursor:"pointer",
                             background:lobbySettings.freeActions?"#052e16":"#1e293b",
                             border:"1px solid "+(lobbySettings.freeActions?"#16a34a":"#334155"),
                             color:lobbySettings.freeActions?"#4ade80":"#94a3b8" }}>
                    Free
                  </button>
                </div>
                <div style={{ fontSize:9,color:"#475569",lineHeight:1.5 }}>
                  {lobbySettings.freeActions ? "Free: use both actions on any combination (play 2 cards, draw twice, etc.)" : "Limited: each action type (play/sell/draw) can only be used once per turn"}
                </div>
              </div>
              <LobbyCardConfig lobbySettings={lobbySettings} updateSettings={updateSettings} />
            </div>
          )}
          {!isHost && (
            <div style={{ background:"#0d1520",border:"1px solid #1e293b",borderRadius:10,padding:12,marginBottom:14,fontSize:12,color:"#64748b" }}>
              Game length: <span style={{ color:"#f1f5f9",fontWeight:700 }}>{lobbySettings.turns} turns</span> <span style={{ fontSize:10 }}>(set by host)</span>
              <span style={{ marginLeft:12 }}>Actions: <span style={{ color:"#f1f5f9",fontWeight:700 }}>{lobbySettings.freeActions ? "Free" : "Limited"}</span></span>
            </div>
          )}

          {/* Start / waiting */}
          {isHost ? (
            <button disabled={!canStart} onClick={function(){ sendMsg({ type:"START_GAME" }); }}
              style={{ width:"100%",padding:"14px 0",borderRadius:10,fontSize:15,fontWeight:700,
                       cursor:canStart?"pointer":"not-allowed",opacity:canStart?1:0.4,
                       background:"#052e16",border:"2px solid #16a34a",color:"#4ade80",marginBottom:10 }}>
              Start Game{isTwoPlayer?" (2 players — testing)":isSixPlayer?" (6 players — not recommended)":""}
            </button>
          ) : (
            <div style={{ textAlign:"center",color:"#64748b",fontSize:12,padding:16,
                          background:"#0d1520",border:"1px solid #1e293b",borderRadius:10,marginBottom:10 }}>
              ⏳ Waiting for host to start…
            </div>
          )}

          <button onClick={function(){ setPhase("lobby"); setRoomCode(null); setPlayers([]); setIsHost(false); setMyPlayerId(null); setPlayerIdMap([]); }}
            style={{ width:"100%",padding:"8px 0",borderRadius:8,fontSize:11,cursor:"pointer",
                     background:"none",border:"1px solid #1e293b",color:"#475569" }}>
            Leave Room
          </button>
        </div>
      </div>
    );
  }

  // ── In-game ────────────────────────────────────────────────────────────
  if (phase === "ingame") {
    if (!gameSt) {
      return (
        <div style={{ position:"fixed",inset:0,background:"#0a0f1a",display:"flex",alignItems:"center",
                      justifyContent:"center",flexDirection:"column",gap:12 }}>
          <div style={{ fontSize:13,color:"#64748b" }}>Waiting for game state…</div>
          <div style={{ fontSize:11,color:"#475569" }}>{isHost ? "Initialising game…" : "Waiting for host to sync…"}</div>
        </div>
      );
    }

    if (gameSt.gameOver) {
      function handleRematch() {
        // Reset game state and return to waiting room
        if (isHost) {
          localDispatchFn({ type:"RESET" });
          sendMsg({ type:"RETURN_TO_LOBBY" });
        }
        setRemoteSt(null);
        setPhase("waiting");
        prevLogLenRef.current = -1;
      }
      return <ScoreScreen st={gameSt}
        onMenu={function(){ setPhase("lobby"); setRemoteSt(null); if(isHost) localDispatchFn({type:"RESET"}); }}
        onRematch={handleRematch} />;
    }

    // Inject multiplayer flag into settings so GameScreen knows to use ActivityFeed
    var mpSettings = Object.assign({}, settings, {
      multiplayMode: true,
      multiplayMyId: (function(){ var mp2 = gameSt && gameSt.players && gameSt.players[gameSt.viewIdx]; return mp2 ? mp2.id : null; })()
    });

    var detailModal = localDetailCard
      ? <DetailModal card={localDetailCard} st={gameSt} dispatch={dispatch}
                     onClose={function(){ setLocalDetailCard(null); }} />
      : null;

    // Player list click handler: open asset view popup
    function handlePlayerClick(playerId) {
      var myPlayer = gameSt && gameSt.players[gameSt.viewIdx];
      if (!myPlayer || playerId === myPlayer.id) return; // clicking yourself: no-op
      setViewingAssetsOf(playerId);
    }

    // Augment dispatch to inject player-click handler into the context
    function mpDispatch(action) {
      if (action.type === "SET_VIEW") {
        handlePlayerClick(gameSt.players[action.i] && gameSt.players[action.i].id);
        return;
      }
      dispatch(action);
    }

    return (
      <div style={{ height:"100%", position:"relative" }}>
        {mpSettings.mobileView
          ? <MobileGameScreen st={gameSt} dispatch={mpDispatch} setSettings={setSettings} isMultiplayer={true} />
          : <GameScreen st={gameSt} dispatch={mpDispatch} settings={mpSettings} setSettings={setSettings} />
        }
        {detailModal}
        <MPWaitingBanner gameSt={gameSt} />
        <MPAssetViewPopup viewingAssetsOf={viewingAssetsOf} gameSt={gameSt} onClose={function(){ setViewingAssetsOf(null); }} />
        <MPActionNotifications notifications={notifications} mobile={mpSettings.mobileView} />
        <MPEventToast toasts={mpEventToasts} />
        <MPDisconnectBanner connected={connected} reconnecting={reconnecting} manualReconnect={manualReconnect} />
      </div>
    );
  }

  return null;
}


/* ══════════════════════════════════════════════════════════
   ROOT APP  (replaces the old export default App)
   ══════════════════════════════════════════════════════════ */
function App() {
  var [mode, setMode] = useState(null); // null | "single" | "multi"
  var [showTutorial, setShowTutorial] = useState(false);

  // Single-device path — identical to old behaviour
  var [st, dispatch] = useReducer(reducer, null);
  var [settings, setSettings] = useState({ moneyPopups:true, mobileView: typeof window!=="undefined" && window.innerWidth<640 });
  // Persist card settings across games so players don't re-configure every session
  var [savedCardSettings, setSavedCardSettings] = useState(null);

  // Tutorial — loaded from tutorial.jsx via window.SB_EXPORTS.Tutorial
  var TutorialComp = (typeof window !== "undefined" && window.SB_EXPORTS && window.SB_EXPORTS.Tutorial) || null;

  // If showTutorial is true but the tutorial component isn't available yet,
  // clear the flag via useEffect (never call setState during render).
  useEffect(function() {
    if (showTutorial && !TutorialComp) {
      setShowTutorial(false);
    }
  }, [showTutorial, TutorialComp]);

  if (showTutorial && TutorialComp) {
    return <TutorialComp onExit={function(){ setShowTutorial(false); }} />;
  }

  if (!mode) return <ModeSelectScreen onSelect={setMode} onHowToPlay={function(){ setShowTutorial(true); }} />;

  if (mode === "multi") {
    return <MultiPlayerApp onBack={function(){ setMode(null); }} />;
  }

  // mode === "single"
  if (!st) return <SetupScreen
    onStart={function(names, turns, disabledNames, countOverrides, cardSettingsSnapshot, freeActions) {
      if (cardSettingsSnapshot) setSavedCardSettings(cardSettingsSnapshot);
      dispatch({ type:"INIT", names, turns, disabledNames, countOverrides, freeActions:!!freeActions });
    }}
    settings={settings} setSettings={setSettings}
    onBack={function(){ setMode(null); }}
    initialCardSettings={savedCardSettings}
    onHowToPlay={function(){ setShowTutorial(true); }}
  />;
  if (st.gameOver) return <ScoreScreen st={st} onMenu={function(){ dispatch({type:"RESET"}); }} />;
  if (settings.mobileView) return <MobileGameScreen st={st} dispatch={dispatch} setSettings={setSettings} />;
  return <GameScreen st={st} dispatch={dispatch} settings={settings} setSettings={setSettings} />;
}

/* ══════════════════════════════════════════════════════════
   TUTORIAL HANDOFF — exports for tutorial.jsx
   tutorial.jsx reads from window.SB_EXPORTS and attaches
   its Tutorial component back onto the same object.
   Keep this list stable; changes here are a contract.
   ══════════════════════════════════════════════════════════ */
if (typeof window !== "undefined") {
  window.SB_EXPORTS = Object.assign(window.SB_EXPORTS || {}, {
    // Card templates (canonical source of truth)
    ACTIONS_T: ACTIONS_T,
    REACTIONS_T: REACTIONS_T,
    ASSETS_T: ASSETS_T,
    // Components
    CardComp: CardComp,
    DetailModal: DetailModal,
    // Design tokens & enums
    C: C,
    CT: CT,
    AS: AS,
    ST: ST,
    CUI: CUI,
    SUB_UI: SUB_UI,
    CSS: CSS,
    // Helpers
    getDesc: getDesc,
    dollars: $,
    uid: uid,
    mkCards: mkCards,
    // React hooks (tutorial.jsx is a separate script and can't import these)
    React: React,
    useState: useState,
    useEffect: useEffect,
    useRef: useRef,
  });
}

/* - Mount - */
ReactDOM.createRoot(document.getElementById("root")).render(React.createElement(App));

/* ── CLAUDE TESTING ────────────────────────────────────────────────────────
   To run the game inside Claude.ai as an artifact:
   1. Comment out the ReactDOM.createRoot line above
   2. Uncomment the line below
   3. Remember to revert both before pushing to GitHub/Railway
   export default App;
   ─────────────────────────────────────────────────────────────────────── */
