const { useState, useEffect, useCallback } = React;

function GlobalAnimations() {
  return (
    <style>{`
      @keyframes npc-sway { 0%,100%{transform:translateX(-50%) translateY(0) rotate(0deg);}50%{transform:translateX(-50%) translateY(-1.5px) rotate(0.6deg);} }
      @keyframes npc-blink { 0%,92%,100%{opacity:0;}94%,96%{opacity:1;} }
      @keyframes lens-flicker { 0%,100%{opacity:0.55;}50%{opacity:0.9;} }
      @keyframes tail-flick { 0%,100%{transform:rotate(0deg);}50%{transform:rotate(9deg);} }
      @keyframes lekku-sway-l { 0%,100%{transform:rotate(0deg);}50%{transform:rotate(-5deg);} }
      @keyframes lekku-sway-r { 0%,100%{transform:rotate(0deg);}50%{transform:rotate(5deg);} }
      @keyframes player-bob { 0%,100%{transform:translateY(0);}50%{transform:translateY(-1.5px);} }
      @keyframes ring-spin { from{transform:rotate(0deg);}to{transform:rotate(360deg);} }
      @keyframes collectible-bob { 0%,100%{transform:translateY(0);filter:drop-shadow(0 0 2px rgba(232,201,122,0.4));}50%{transform:translateY(-2px);filter:drop-shadow(0 0 5px rgba(232,201,122,0.85));} }
      @keyframes twinkle { 0%,100%{opacity:0.15;}50%{opacity:0.9;} }
      @keyframes door-pulse { 0%,100%{opacity:0.5;}50%{opacity:0.95;} }
      @keyframes steam-rise { 0%,100%{opacity:0;transform:translateY(0);}30%{opacity:0.7;}100%{transform:translateY(-10px);} }
      @keyframes traffic-drift { 0%{transform:translateX(-60px);opacity:0;}12%{opacity:0.9;}88%{opacity:0.9;}100%{transform:translateX(60px);opacity:0;} }
      @keyframes embers-drift { 0%{transform:translateY(0);opacity:0;}10%{opacity:0.9;}100%{transform:translateY(-320px);opacity:0;} }
      @keyframes mist-drift { 0%{transform:translateX(-20px);opacity:0;}20%{opacity:0.7;}100%{transform:translateX(20px);opacity:0;} }
      @keyframes world-obj-pulse { 0%,100%{opacity:0.25;box-shadow:0 0 4px #4ACDFF33;}50%{opacity:0.65;box-shadow:0 0 10px #4ACDFF88;} }
      @keyframes scanDown { from{transform:translateY(0);opacity:0.8;}to{transform:translateY(120vh);opacity:0;} }
      @keyframes drift { from{transform:translate(0,0) scale(1);}to{transform:translate(10px,-8px) scale(1.1);} }
      @keyframes rise { from{transform:translateY(0);opacity:0.5;}to{transform:translateY(-60px);opacity:0;} }
      @keyframes holo-flicker { 0%,100%{opacity:0.78;}50%{opacity:1;} }
      @keyframes prop-scan { from{transform:translateY(0);opacity:0.8;}to{transform:translateY(26px);opacity:0;} }
      @keyframes ship-blink { 0%,100%{opacity:0.15;}50%{opacity:1;} }
      @keyframes tracer-fly { 0%{transform:translateX(-120px);opacity:0;}8%{opacity:1;}85%{opacity:1;}100%{transform:translateX(860px);opacity:0;} }
      @keyframes ash-fall { 0%{transform:translate(0,-10px);opacity:0;}15%{opacity:0.8;}100%{transform:translate(-90px,380px);opacity:0;} }
      @keyframes ship-engine { 0%,100%{opacity:0.3;}50%{opacity:0.85;} }
      @keyframes ship-spark { 0%,100%{opacity:0;transform:scale(0.4);}20%{opacity:1;transform:scale(1.2);}60%{opacity:0.4;transform:scale(0.8);} }
    `}</style>
  );
}

const TILE = 32;
const VIEWPORT_COLS = 20;
const VIEWPORT_ROWS = 13;

function emptyGrid(w, h) {
  return Array.from({ length: h }, () => Array.from({ length: w }, () => ({ type: 'wall' })));
}
function carveRect(g, x1, y1, x2, y2, type = 'floor') {
  for (let y = y1; y <= y2; y++) for (let x = x1; x <= x2; x++) { if (g[y]?.[x]) g[y][x] = { type }; }
}
function pt(g, x, y, type) { if (g[y]?.[x]) g[y][x] = { type }; }
// Build a grid from ASCII rows. # wall, . floor, D door, ~ lava, w water.
function mapFromRows(rows) {
  const T = { '#': 'wall', '.': 'floor', 'D': 'door', '~': 'lava', 'w': 'water' };
  return rows.map((r) => Array.from(r, (c) => ({ type: T[c] || 'floor' })));
}

const SHIP_DEFS = {
  free_horizon:     { w: 6,  h: 5 },
  republic_shuttle: { w: 6,  h: 4 },
  valor_frame:      { w: 14, h: 4 },
  czerka_hauler:    { w: 7,  h: 5 },
  kuat_yacht:       { w: 5,  h: 4 },
  kdy_shuttle:      { w: 6,  h: 4 },
};
function carveShips(g, ships) {
  (ships || []).forEach((s) => { const d = SHIP_DEFS[s.kind]; if (d) carveRect(g, s.x, s.y, s.x + d.w - 1, s.y + d.h - 1, 'ship_hull'); });
}
function shipAtTile(zone, x, y) {
  return (zone.ships || []).find((s) => { const d = SHIP_DEFS[s.kind]; return d && x >= s.x && x < s.x + d.w && y >= s.y && y < s.y + d.h; });
}

// Engine Wastes: Vance reacts to the Black Box log (shared by several dialogue phases)
const WST_VANCE_BOX_CHOICE = { text: '[Black Box] I found a cylinder. It has your voice on it.', requiresFlag: 'black_box_found', morality: 0, loyalty: {}, result: 'Vance goes still. "I was still talking when they welded the hatch." He wipes the cylinder clean. "Then finish it. Seat the Governor."', grants: { codex: ['codex-wst-vance-truth'] } };

const PLANETS = {
  coruscant: {
    id: 'coruscant', name: 'Coruscant', travelCost: 0, startZoneId: 'spaceport',
    description: 'Endless spires and skylanes. Someone up here has been burying the truth.',
    zones: {
      spaceport: {
        id: 'spaceport', name: 'Coruscant Spaceport', subtitle: 'Subsurface Level 2 · Docking Bay 14',
        width: 32, height: 22, spawnPos: { x: 14, y: 10 }, textureId: 'coruscant',
        accent: '#8FA6FF', accentGlow: 'rgba(143,166,255,0.25)', accentDim: '#3D4A80',
        floorColor: '#242840', floorAlt: '#2E3350', wallDark: '#0D0E16', wallLight: '#181B2C',
        bg: 'radial-gradient(circle at 30% 20%, #171A2C 0%, #0B0C14 70%)', ambient: 'traffic',
        decor: ['cargo_crate', 'pipe', 'neon_sign', 'girder'],
        doors: [
          { x: 31, y: 12, targetZone: 'market', targetPos: { x: 1, y: 10 }, label: 'Market' },
          { x: 31, y: 13, targetZone: 'market', targetPos: { x: 1, y: 11 }, label: 'Market' },
        ],
        worldObjects: [
          { id: 'customs_terminal', propArt: 'console', propVariant: 'customs', x: 23, y: 9, label: 'Customs Terminal', description: 'A flickering datapad logs your entry. Transit clearance: provisional.', once: false },
          { id: 'fueling_conduit', propArt: 'fuel_hose', x: 8, y: 18, label: 'Fueling Conduit', description: 'The conduit hisses with residual pressurised fuel. Someone left this running.', once: true },
        ],
        npcs: [
          { id: 'vane', x: 24, y: 11, kind: 'republic_guard', label: 'Officer Vane',
            repeatPrompt: 'Vane gives you a curt nod. You are already cleared.',
            prompt: 'The officer scans your credentials. "Transit papers in order, but the manifest shows four crates unaccounted for. Walk me through your cargo."',
            choices: [
              { text: 'Show the correct papers. Everything is legitimate.', morality: 10, loyalty: { republic: 8 }, result: 'Vane nods. "Welcome to Coruscant. Move along."' },
              { text: 'Slip him a credit chip. No need to look too closely.', morality: -15, loyalty: { underworld: 10 }, result: 'He pockets it smoothly. "Nothing to declare. Have a pleasant stay."' },
            ],
          },
          { id: 'droid44', x: 13, y: 8, kind: 'droid', label: 'Pit Droid Unit 44', mobile: true,
            repeatPrompt: 'Unit 44 is deep in a cataloguing cycle. It does not look up.',
            prompt: '"BEEP BOOP. Cargo secured. Ramp deployed. I have also catalogued seventeen new hull scuffs. Seventeen. Do humans not see the hull?"',
            choices: [
              { text: 'Tell it the scuffs give the ship character.', morality: 5, loyalty: {}, result: 'It pauses for 2.4 seconds. "Character. Processing. Logged."' },
              { text: 'Ask it to stop cataloguing and get back to work.', morality: 0, loyalty: {}, result: '"Acknowledged. Suppressing visual distress subroutine. For now."' },
            ],
          },
          { id: 'marlo', x: 4, y: 16, kind: 'smuggler', label: '"Slick" Marlo', mobile: true,
            repeatPrompt: 'Marlo gives you a lazy two-finger salute. The offer already stands.',
            prompt: 'He leans on the cargo stack without looking at you. "Four unmarked crates in your hold. I can move them past customs clean. Thirty-percent cut."',
            choices: [
              { text: 'Decline. That kind of trouble follows you.', morality: 8, loyalty: { republic: 4 }, result: '"Your loss. Offer stands."' },
              { text: 'Shake on it. Thirty percent is fair.', morality: -18, loyalty: { underworld: 15 }, result: '"Smart. Bay seven, after dark."' },
            ],
          },
          { id: 'jon_spaceport', x: 6, y: 10, kind: 'smuggler', label: 'Jon', questNpc: true,
            hideAfterFlags: ['met_jon_spaceport'],
            repeatPrompt: 'Jon gives you a knowing look. "Head over to my place through the market. You know where I am."',
            prompt: 'Well, look who finally made it off the transport vector intact. Good to see a face that is not collecting a bounty or issuing a customs citation. I have got a partner opening and something worth your time — but not out here in the landing bay.',
            choices: [
              { text: 'Good to see you too. Lead the way.', morality: 5, loyalty: { underworld: 5 }, result: 'Jon claps you on the shoulder. "Follow me through the West Market. Keep your blaster hand loose." He gestures toward the Market District exit.', grants: { flags: ['met_jon_spaceport'] } },
              { text: 'Skip it. What is the opportunity?', morality: 0, loyalty: {}, result: 'Jon lowers his voice. "Not here — too many ears on the landing pad. My apartment. Through the market." He nods toward the exit.', grants: { flags: ['met_jon_spaceport'] } },
              { text: 'I work alone. This better be worth my time.', morality: -5, loyalty: { underworld: 3 }, result: 'Jon smirks. "It will be. My place. West Residential, through the market. You will know the door." He walks away first.', grants: { flags: ['met_jon_spaceport'] } },
            ],
          },
        ],
        collectibles: [{ id: 'fuel_cell', x: 7, y: 9, label: 'Salvaged Fuel Cell', reward: 15 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 8, 30, 20, 'floor');
          carveRect(g, 9, 1, 20, 4, 'ship_hull');
          carveRect(g, 7, 2, 9, 4, 'ship_hull');
          carveRect(g, 20, 2, 22, 4, 'ship_hull');
          carveRect(g, 11, 5, 18, 5, 'ship_hull');
          [[14,6],[15,6],[14,7],[15,7]].forEach(([x,y]) => pt(g,x,y,'ship_ramp'));
          [[2,10],[3,10],[4,10],[2,11],[3,11],[2,14],[3,14],[2,15],[3,15],[5,10],[5,14],[5,15]].forEach(([x,y]) => pt(g,x,y,'wall'));
          [[22,9],[22,10],[22,12],[22,13]].forEach(([x,y]) => pt(g,x,y,'wall'));
          pt(g,31,12,'door'); pt(g,31,13,'door');
          return g;
        },
      },
      market: {
        id: 'market', name: 'Market District', subtitle: 'Coruscant · Lower City Bazaar',
        width: 32, height: 22, spawnPos: { x: 1, y: 10 }, textureId: 'coruscant',
        accent: '#8FA6FF', accentGlow: 'rgba(143,166,255,0.18)', accentDim: '#3A4880',
        floorColor: '#1A1C2A', floorAlt: '#222438', wallDark: '#0C0D16', wallLight: '#181A28',
        bg: 'radial-gradient(circle at 50% 10%, #141628 0%, #080A14 70%)', ambient: 'traffic',
        decor: ['cargo_crate', 'pipe', 'neon_sign', 'girder'],
        doors: [
          { x: 0, y: 10, targetZone: 'spaceport', targetPos: { x: 29, y: 12 }, label: 'Spaceport' },
          { x: 0, y: 11, targetZone: 'spaceport', targetPos: { x: 29, y: 13 }, label: 'Spaceport' },
          { x: 16, y: 0, targetZone: 'apartments', targetPos: { x: 14, y: 18 }, label: 'Apartments' },
          { x: 17, y: 0, targetZone: 'apartments', targetPos: { x: 15, y: 18 }, label: 'Apartments' },
          { x: 31, y: 10, targetZone: 'plaza', targetPos: { x: 1, y: 10 }, label: 'Plaza' },
          { x: 31, y: 11, targetZone: 'plaza', targetPos: { x: 1, y: 11 }, label: 'Plaza' },
        ],
        worldObjects: [
          { id: 'wanted_holo', propArt: 'board', propVariant: 'wanted', x: 20, y: 4, label: 'Wanted Holo-Poster', description: 'A Republic bounty. The face on the poster looks vaguely familiar.', once: false },
          { id: 'scrap_bin', propArt: 'scrap_bin', x: 6, y: 17, label: 'Scrap Bin', description: 'Buried under junk you find a cracked power cell and a half-eaten protein bar. You take neither.', once: true },
        ],
        npcs: [
          { id: 'archivist', x: 8, y: 6, kind: 'jedi', label: 'Jedi Archivist Sera',
            repeatPrompt: 'Sera gives you a meaningful look. She has already said too much.',
            prompt: 'She lowers her voice. "A Senator has flagged three inquiries into shard smuggling as classified. I should not be telling you this."',
            choices: [
              { text: 'Report this to the Jedi Council.', morality: 18, loyalty: { republic: 10 }, result: 'The Council opens a quiet investigation. You feel lighter for it.' },
              { text: 'Offer to bury it deeper, for a price.', morality: -20, loyalty: { underworld: 12 }, result: 'She hesitates, then hands you a datachip.' },
            ],
          },
          { id: 'market_broker', x: 16, y: 14, kind: 'broker', label: 'Market Broker Eliss',
            repeatPrompt: 'Eliss taps her comm and waves you off. She is with another client.',
            prompt: '"Looking to trade? I have contacts across three systems. Credits talk and everything else is negotiable."',
            choices: [
              { text: 'Ask about the shard market.', morality: -5, loyalty: { underworld: 5 }, result: 'She leans in. "Careful asking about those in public."' },
              { text: 'Buy rations for the road.', morality: 3, loyalty: {}, result: '"Smart. Traveling light is traveling alive."' },
            ],
          },
          { id: 'jax', x: 24, y: 10, kind: 'broker', label: 'Scrap Trader Jax',
            repeatPrompt: 'Jax is counting credits and ignores you.',
            prompt: 'The Rodian tips his goggles. "You have the look of someone who finds things they are not supposed to find. I pay well for those kinds of finds."',
            choices: [
              { text: 'Ask what he is looking for specifically.', morality: -3, loyalty: { underworld: 6 }, result: '"Old Republic code cylinders. Jedi tech. Anything that makes the Senate nervous."' },
              { text: 'Tell him you are not that kind of person.', morality: 6, loyalty: { republic: 4 }, result: '"Sure you are not. Come back when you change your mind."' },
            ],
          },
          { id: 'sgt_kren', x: 10, y: 17, kind: 'republic_guard', label: 'Sgt. Kren',
            repeatPrompt: 'Kren is watching the crowd. He does not acknowledge you again.',
            prompt: '"Move along. Market patrols are doubled after the disturbance in the plaza. Nothing to worry about if you have nothing to hide."',
            choices: [
              { text: 'Ask what the disturbance was.', morality: 2, loyalty: { republic: 3 }, result: '"Above your clearance level. Move along."' },
              { text: 'Slip past without engaging.', morality: 0, loyalty: {}, result: 'He watches you go. You feel his eyes on the back of your neck.' },
            ],
          },
          { id: 'bith_busker', x: 26, y: 17, kind: 'bith', label: 'Bith Busker',
            repeatPrompt: 'The Bith is lost in the music. The melody pulls at something old in your memory.',
            prompt: 'The large-headed musician plays a melancholy cantina tune on a kloo horn. He pauses when he notices you. "Request? The first one is free."',
            choices: [
              { text: 'Ask for the old Jedi hymn your master used to play.', morality: 8, loyalty: {}, result: 'He plays it slowly. The notes fall through the market noise like stones in water.' },
              { text: 'Toss him a credit chip and walk on.', morality: 3, loyalty: {}, result: 'He nods, then returns to his melody.' },
            ],
          },
        ],
        collectibles: [{ id: 'datachip', x: 15, y: 5, label: 'Encrypted Datachip', reward: 30 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 30, 20, 'floor');
          [[5,4],[6,4],[7,4],[5,5],[5,8],[6,8],[7,8],[5,9],[14,4],[15,4],[16,4],[14,5],[14,8],[15,8],[16,8],[14,9]].forEach(([x,y]) => pt(g,x,y,'wall'));
          [[22,4],[23,4],[24,4],[22,5],[22,8],[23,8],[24,8],[22,9]].forEach(([x,y]) => pt(g,x,y,'wall'));
          pt(g,0,10,'door'); pt(g,0,11,'door');
          pt(g,16,0,'door'); pt(g,17,0,'door');
          pt(g,31,10,'door'); pt(g,31,11,'door');
          return g;
        },
      },
      apartments: {
        id: 'apartments', name: 'Residential Sector West', subtitle: 'Coruscant · Subsurface Level 2',
        width: 30, height: 22, spawnPos: { x: 14, y: 18 }, textureId: 'coruscant',
        accent: '#A8B8FF', accentGlow: 'rgba(168,184,255,0.2)', accentDim: '#404880',
        floorColor: '#1C2034', floorAlt: '#242840', wallDark: '#0C0D14', wallLight: '#161828',
        bg: 'radial-gradient(circle at 50% 70%, #13162A 0%, #090A12 70%)', ambient: 'traffic',
        decor: ['pipe', 'neon_sign', 'archive'],
        doors: [
          { x: 14, y: 21, targetZone: 'market', targetPos: { x: 16, y: 1 }, label: 'Market' },
          { x: 15, y: 21, targetZone: 'market', targetPos: { x: 17, y: 1 }, label: 'Market' },
          { x: 6, y: 4, targetZone: 'jons_apt_int', targetPos: { x: 9, y: 11 }, label: "Jon's Apt" },
        ],
        worldObjects: [
          { id: 'jon_apt', propArt: 'apt_door', propVariant: 'jon', x: 6, y: 7, label: "Jon's Apartment", description: 'The smell of caf and burnt wiring. The inner door is unlocked.', once: false },
          { id: 'dexter_apt', propArt: 'apt_door', propVariant: 'dexter', x: 6, y: 15, label: "Dexter's Apartment", description: 'Smells of grease and something frying. A note on the door: Back in 20. Help yourself to the caf.', once: true },
          { id: 'zillow_terminal', propArt: 'console', propVariant: 'zillow', x: 24, y: 6, label: 'Zillow Housing Terminal', description: 'Vacancy listings for Subsurface Level 2: zero. Vacancy listings for Level 5 and above: three thousand. The price makes your eyes water.', once: false },
          { id: 'speeder_shell', propArt: 'speeder_wreck', x: 22, y: 16, label: 'Abandoned Speeder Shell', description: 'The repulsor coils are stripped. Someone was living in here recently. The bedroll is still warm.', once: true },
        ],
        npcs: [
          { id: 'anxious_tenant', x: 12, y: 10, kind: 'smuggler', label: 'Anxious Tenant', mobile: true,
            repeatPrompt: 'The tenant glances at you, then back at their door. Whatever they know, they have decided not to share more of it.',
            prompt: 'The human presses against the corridor wall as you pass. "You are not with the inspection team? Good. They have been through three times this week. Something is happening. Something big."',
            choices: [
              { text: 'Ask what kind of something.', morality: 0, loyalty: {}, result: '"Heard shouting from the Dexter place. And men in grey coats who are not Republic and not Sith. Something in between."' },
              { text: 'Reassure them and keep moving.', morality: 5, loyalty: {}, result: '"Easy for you to say." They disappear behind a locked door.' },
            ],
          },
          { id: 'jn7', x: 20, y: 12, kind: 'droid', label: 'Maintenance Droid JN-7', mobile: true,
            repeatPrompt: 'JN-7 is patching the same wall panel it has been patching for three weeks. Progress: 12 percent.',
            prompt: '"MAINTENANCE CYCLE: ongoing. Current fault list: 847 items. Estimated completion time: 14 years. Requesting additional allocation of repair foam." It looks at you hopefully.',
            choices: [
              { text: 'Tell it that you do not have any repair foam.', morality: 0, loyalty: {}, result: '"Logged as anticipated. Fault 848: insufficient foam allocation." It turns back to the wall.' },
              { text: 'Ask what the worst fault on the list is.', morality: 2, loyalty: {}, result: '"Fault 1: the building is slowly rotating. 0.04 degrees per standard year. In 9,000 years it will face the wrong way entirely."' },
            ],
          },
        ],
        collectibles: [{ id: 'apt_key', x: 18, y: 9, label: 'Unclaimed Apt Key', reward: 20 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 28, 20, 'floor');
          carveRect(g, 3, 3, 10, 11, 'wall');
          carveRect(g, 4, 4, 9, 10, 'floor');
          carveRect(g, 3, 13, 10, 19, 'wall');
          carveRect(g, 4, 14, 9, 18, 'floor');
          carveRect(g, 19, 3, 27, 9, 'wall');
          carveRect(g, 20, 4, 26, 8, 'floor');
          [[6,11],[6,12],[6,13]].forEach(([x,y]) => pt(g,x,y,'floor'));
          pt(g,14,21,'door'); pt(g,15,21,'door'); pt(g,6,4,'door');
          pt(g, 19, 6, 'floor'); pt(g, 19, 7, 'floor'); // Zillow office doorway
          return g;
        },
      },
      plaza: {
        id: 'plaza', name: 'Commemorative Plaza', subtitle: 'Coruscant · Subsurface Level 2',
        width: 32, height: 22, spawnPos: { x: 1, y: 10 }, textureId: 'coruscant',
        accent: '#7AC8FF', accentGlow: 'rgba(122,200,255,0.22)', accentDim: '#2A5A80',
        floorColor: '#192030', floorAlt: '#20293A', wallDark: '#0B0F16', wallLight: '#141C26',
        bg: 'radial-gradient(circle at 50% 50%, #121C2A 0%, #08100A 70%)', ambient: 'traffic',
        decor: ['pillar', 'brazier', 'neon_sign'],
        doors: [
          { x: 0, y: 10, targetZone: 'market', targetPos: { x: 29, y: 10 }, label: 'Market' },
          { x: 0, y: 11, targetZone: 'market', targetPos: { x: 29, y: 11 }, label: 'Market' },
          { x: 31, y: 10, targetZone: 'commercial', targetPos: { x: 1, y: 10 }, label: 'Commercial' },
          { x: 31, y: 11, targetZone: 'commercial', targetPos: { x: 1, y: 11 }, label: 'Commercial' },
        ],
        worldObjects: [
          { id: 'memorial_fountain', propArt: 'fountain', x: 15, y: 10, label: 'Memorial Fountain', description: 'The inscription reads: In memory of the Fallen of Malachor. The water runs blue-white, fed from far above.', once: false },
          { id: 'public_datapad', propArt: 'console', propVariant: 'newsfeed', x: 8, y: 5, label: 'Public Datapad', description: 'The newsfeed headline: SENATE VOTES TO EXTEND EMERGENCY POWERS. Below it, someone has scratched two words in Basic: they know.', once: false },
          { id: 'graffiti_tag', propArt: 'wall_marks', propVariant: 'free', x: 24, y: 17, label: 'Graffiti Tag', description: 'Spray-etched into the durasteel wall: a stylised flame over a broken chain. The symbol of the Free Coruscant movement.', once: true },
        ],
        npcs: [
          { id: 'ph_merchant_orrel', x: 20, y: 11, kind: 'medic', label: 'Ysmin Orrel, Calm and Growth',
            isVendor: true, vendorFaction: 'underworld', repeatable: true, requiresFlag: 'inheritance_active',
            vendorStock: ['ph_pkg_spa', 'ph_pkg_garden'],
            vendorGreeting: "\"Calm is a resource. Hot water, green leaves, quiet. You will think better and sleep better, and your people will thank you.\"",
            repeatPrompt: "Orrel waters a small orchid and does not look up.",
            prompt: "\"Calm is a resource. Hot water, green leaves, quiet. You will think better and sleep better, and your people will thank you.\"",
            choices: [] },
          { id: 'calla_ren', x: 10, y: 7, kind: 'jedi', label: "Senator's Aide Calla Ren",
            repeatPrompt: 'Calla notices you again and smiles thinly. She has nothing more to share in public.',
            prompt: 'She speaks without looking at you, watching the plaza. "The Senator I work for has received three death threats this week. All three were traced back to a single Level 1 address. All three were dismissed as crank messages."',
            choices: [
              { text: 'Offer to look into the address.', morality: 5, loyalty: { republic: 8 }, result: 'She slips you a datachip without changing expression. "I did not give you that."' },
              { text: 'Tell her the Senate should handle its own security.', morality: -2, loyalty: {}, result: '"The Senate is handling it. That is precisely the problem."' },
            ],
          },
          { id: 'swoop_informant', x: 22, y: 15, kind: 'swoop_gang', label: 'Swoop Gang Informant',
            repeatPrompt: 'He tilts his head toward a corner. Still watching. Still waiting for something from you.',
            prompt: 'He is leaning against the memorial base like he owns it. "You want information? Everything costs. But I will tell you this for free: the men in grey coats meet here at third-bell. Every. Night."',
            choices: [
              { text: 'Ask what they are meeting about.', morality: -5, loyalty: { underworld: 8 }, result: '"That will cost you. Five hundred credits. Then we talk."' },
              { text: 'Report this to Sgt. Kren in the market.', morality: 10, loyalty: { republic: 6 }, result: 'He sees the intention in your eyes and melts back into the crowd.' },
            ],
          },
        ],
        collectibles: [{ id: 'plaza_cred', x: 26, y: 6, label: 'Dropped Credit Chip', reward: 25 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 30, 20, 'floor');
          carveRect(g, 12, 8, 18, 13, 'water');
          pt(g, 15, 10, 'floor'); pt(g, 15, 11, 'floor');
          [[4,4],[5,4],[4,5],[25,4],[26,4],[26,5],[4,16],[5,16],[4,17],[25,16],[26,16],[26,17]].forEach(([x,y]) => pt(g,x,y,'wall'));
          pt(g,0,10,'door'); pt(g,0,11,'door');
          pt(g,31,10,'door'); pt(g,31,11,'door');
          pt(g, 15, 8, 'floor'); pt(g, 15, 9, 'floor'); // causeway to the memorial island
          return g;
        },
      },
      commercial: {
        id: 'commercial', name: 'Commercial Sector', subtitle: 'Coruscant · Entertainment District',
        width: 30, height: 22, spawnPos: { x: 1, y: 10 }, textureId: 'coruscant',
        accent: '#FF9ADE', accentGlow: 'rgba(255,154,222,0.2)', accentDim: '#802060',
        floorColor: '#201828', floorAlt: '#2A2034', wallDark: '#100C18', wallLight: '#1A1424',
        bg: 'radial-gradient(circle at 40% 30%, #1A1028 0%, #0A0810 70%)', ambient: 'traffic',
        decor: ['neon_sign', 'pillar', 'brazier'],
        doors: [
          { x: 0, y: 10, targetZone: 'plaza', targetPos: { x: 29, y: 10 }, label: 'Plaza' },
          { x: 0, y: 11, targetZone: 'plaza', targetPos: { x: 29, y: 11 }, label: 'Plaza' },
          { x: 14, y: 21, targetZone: 'speeder1', targetPos: { x: 13, y: 1 }, label: 'Speeder Bay 1' },
          { x: 15, y: 21, targetZone: 'speeder1', targetPos: { x: 14, y: 1 }, label: 'Speeder Bay 1' },
        ],
        worldObjects: [
          { id: 'sallys_cantina', propArt: 'shopfront', propVariant: 'sallys', x: 10, y: 8, label: "Sally's Cantina", description: 'The neon sign buzzes: SALLYS. No apostrophe. Inside you hear laughter and the clink of glasses. The door is open.', once: false },
          { id: 'goods_store', propArt: 'shopfront', propVariant: 'goods', x: 24, y: 6, label: 'Goods, Trades and Treasure', description: 'A cluttered shop front. The owner has priced everything at exactly twice what it is worth. Standard practice.', once: false },
          { id: 'trex_keypad', propArt: 'apt_door', propVariant: 'trex', x: 20, y: 15, label: "Trex's Apt Keypad", description: 'A reinforced door with a seven-digit keypad. Three of the digits are worn smooth from repeated use.', once: true },
        ],
        npcs: [
          { id: 'ph_merchant_tark', x: 16, y: 11, kind: 'czerka_liaison', label: 'Boolo Tark, Home Entertainment',
            isVendor: true, vendorFaction: 'underworld', repeatable: true, requiresFlag: 'inheritance_active',
            vendorStock: ['ph_pkg_theater', 'ph_pkg_lounge'],
            vendorGreeting: "\"Entertainment is the foundation of a stable household. A theater wall, a lounge, a bar. You will thank me when the heat is on.\"",
            repeatPrompt: "Tark taps a holo remote against his palm like a man who has sold many remotes.",
            prompt: "\"Entertainment is the foundation of a stable household. A theater wall, a lounge, a bar. You will thank me when the heat is on.\"",
            choices: [] },
          { id: 'sally', x: 8, y: 12, kind: 'cantina_owner', label: 'Sally',
            repeatPrompt: 'Sally slides a drink down the bar without looking at you. She remembers what you ordered.',
            prompt: '"Sit. Drink. Whatever you are about to ask me, the answer is: I did not see anything, I do not know anything, and my establishment has nothing to do with it. That said." She leans in. "You look like you need to know things."',
            choices: [
              { text: 'Ask about the men in grey coats.', morality: 0, loyalty: { underworld: 5 }, result: '"Heard of them. They call themselves the Regulators. Private security. Very private. Very well-paid."' },
              { text: 'Just order the caf and say nothing.', morality: 3, loyalty: {}, result: 'She nods approvingly. "The smart ones always order caf first."' },
            ],
          },
          { id: 'trex', x: 22, y: 12, kind: 'crime_boss', label: 'Trex',
            repeatPrompt: 'Trex watches you from across the room. His yellow eyes do not blink.',
            prompt: 'The Trandoshan crime lord crosses his thick arms. "I know who you are. I know what ship you came in on. And I know you have been asking questions that other people have stopped asking." He smiles. It shows a great many teeth.',
            choices: [
              { text: 'Hold his gaze. You are not intimidated.', morality: 0, loyalty: { underworld: 10 }, result: 'He laughs, a low rumbling sound. "Good. Sit. We have business to discuss."' },
              { text: 'Back away slowly. This is above your weight.', morality: 5, loyalty: {}, result: '"Wise." He does not follow. That is almost worse.' },
            ],
          },
        ],
        collectibles: [{ id: 'cantina_token', x: 6, y: 5, label: 'Casino Token', reward: 35 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 28, 20, 'floor');
          carveRect(g, 4, 3, 14, 14, 'wall');
          carveRect(g, 5, 4, 13, 13, 'floor');
          carveRect(g, 18, 3, 27, 10, 'wall');
          carveRect(g, 19, 4, 26, 9, 'floor');
          [[9,14],[10,14],[11,14],[12,14]].forEach(([x,y]) => pt(g,x,y,'floor'));
          pt(g,0,10,'door'); pt(g,0,11,'door');
          pt(g,14,21,'door'); pt(g,15,21,'door');
          pt(g, 18, 6, 'floor'); pt(g, 18, 7, 'floor'); // shop front doorway
          return g;
        },
      },
      speeder1: {
        id: 'speeder1', name: 'Speeder Docking Bay 1', subtitle: 'Coruscant · Subsurface Level 2',
        width: 28, height: 20, spawnPos: { x: 13, y: 2 }, textureId: 'coruscant',
        accent: '#6AFFCC', accentGlow: 'rgba(106,255,204,0.2)', accentDim: '#1A6048',
        floorColor: '#162028', floorAlt: '#1E2A34', wallDark: '#0A1018', wallLight: '#121C26',
        bg: 'radial-gradient(circle at 60% 80%, #101820 0%, #080C10 70%)', ambient: 'traffic',
        decor: ['pipe', 'girder', 'cargo_crate'],
        doors: [
          { x: 13, y: 0, targetZone: 'commercial', targetPos: { x: 14, y: 19 }, label: 'Commercial' },
          { x: 14, y: 0, targetZone: 'commercial', targetPos: { x: 15, y: 19 }, label: 'Commercial' },
          { x: 27, y: 9, targetZone: 'speeder2', targetPos: { x: 1, y: 9 }, label: 'Bay 2' },
          { x: 27, y: 10, targetZone: 'speeder2', targetPos: { x: 1, y: 10 }, label: 'Bay 2' },
        ],
        worldObjects: [
          { id: 'airtaxi_terminal', propArt: 'airtaxi', propVariant: 'worn', x: 11, y: 5, label: 'AirTaxi Terminal', description: 'The schedule board lists forty-seven routes. Twenty-nine are marked SUSPENDED. You wonder what happened on the other twenty-nine.', once: false },
          { id: 'refuel_kiosk', propArt: 'kiosk', propVariant: 'refuel', x: 20, y: 5, label: 'Refueling Kiosk', description: 'Out of order. A handwritten sign reads: Use Bay 2. Bay 2 is also probably out of order.', once: true },
          { id: 'bay_log', propArt: 'board', propVariant: 'baylog', x: 6, y: 14, label: 'Speeder Bay Log', description: 'Last entry: Speeder Unit 7 departed 03:14. Destination: classified. Pilot: classified. Good luck finding that one.', once: true },
        ],
        npcs: [
          { id: 'at9', x: 16, y: 8, kind: 'droid', label: 'AirTaxi Droid AT-9', triggersOverlay: 'speeder',
            repeatPrompt: 'AT-9 chirps twice and resumes its departure countdown. It has a job to do.',
            prompt: '"AIRTAXI UNIT AT-9. DESTINATION QUERY. CURRENT WAIT TIME: 4 MINUTES. CURRENT QUEUE: 0 PASSENGERS. QUERY: ARE YOU A PASSENGER?"',
            choices: [
              { text: 'Yes. Take me to the Senate District.', morality: 0, loyalty: { republic: 3 }, result: '"BOOKING CONFIRMED. RATE: 80 CREDITS. DO YOU HAVE 80 CREDITS?" You do not answer.' },
              { text: 'No. Just browsing.', morality: 0, loyalty: {}, result: '"QUERY: HOW DOES ONE BROWSE A TAXI. LOGGED AS ANOMALOUS BEHAVIOUR."' },
            ],
          },
          { id: 'duvall', x: 24, y: 10, kind: 'mechanic', label: 'Speeder Mechanic Duvall',
            repeatPrompt: 'Duvall is back under the speeder. Only their boots are visible.',
            prompt: 'The mechanic slides out from under a battered airspeeder. "If you are here about the stolen coils, I told the guard already: someone took them between second and third bell. I sleep like a rock and I do not apologise for it."',
            choices: [
              { text: 'Ask if they saw anyone unusual near the bay.', morality: 3, loyalty: { republic: 4 }, result: '"Grey coat. No insignia. Moved like they had done it before." They slide back under the speeder. Interview over.' },
              { text: 'Offer to help track down the coils.', morality: 8, loyalty: {}, result: '"Appreciate it. Check the Abandoned Cargo in Bay 2. People dump things there."' },
            ],
          },
        ],
        collectibles: [{ id: 'speeder_part', x: 8, y: 10, label: 'Stripped Actuator', reward: 20 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 26, 18, 'floor');
          carveRect(g, 1, 1, 6, 4, 'ship_hull');
          carveRect(g, 21, 1, 26, 4, 'ship_hull');
          carveRect(g, 1, 15, 6, 18, 'ship_hull');
          carveRect(g, 21, 15, 26, 18, 'ship_hull');
          [[3,7],[4,7],[3,8],[4,8],[3,12],[4,12],[3,13],[4,13]].forEach(([x,y]) => pt(g,x,y,'wall'));
          [[23,7],[24,7],[23,8],[24,8],[23,12],[24,12],[23,13],[24,13]].forEach(([x,y]) => pt(g,x,y,'wall'));
          pt(g,13,0,'door'); pt(g,14,0,'door');
          pt(g,27,9,'door'); pt(g,27,10,'door');
          return g;
        },
      },
      speeder2: {
        id: 'speeder2', name: 'Speeder Docking Bay 2', subtitle: 'Coruscant · Subsurface Level 2',
        width: 28, height: 20, spawnPos: { x: 1, y: 9 }, textureId: 'coruscant',
        accent: '#6AFFCC', accentGlow: 'rgba(106,255,204,0.2)', accentDim: '#1A6048',
        floorColor: '#141E28', floorAlt: '#1C2830', wallDark: '#080E14', wallLight: '#101820',
        bg: 'radial-gradient(circle at 40% 70%, #0E1620 0%, #060C10 70%)', ambient: 'traffic',
        decor: ['pipe', 'girder', 'cargo_crate'],
        doors: [
          { x: 0, y: 9, targetZone: 'speeder1', targetPos: { x: 25, y: 9 }, label: 'Bay 1' },
          { x: 0, y: 10, targetZone: 'speeder1', targetPos: { x: 25, y: 10 }, label: 'Bay 1' },
        ],
        worldObjects: [
          { id: 'airtaxi_terminal2', propArt: 'airtaxi', propVariant: 'clean', x: 10, y: 5, label: 'AirTaxi Terminal 2', description: 'This one actually works. The wait time reads: 47 minutes. You decide to walk.', once: false },
          { id: 'departure_board', propArt: 'board', propVariant: 'departures', x: 18, y: 5, label: 'Departure Board', description: 'One entry is highlighted in red: FLIGHT C-7 OVERDUE. LAST CONTACT: 06:22. That was three days ago.', once: true },
          { id: 'cargo_container', propArt: 'cargo_container', x: 22, y: 14, label: 'Abandoned Cargo Container', description: 'Duvall was right. Inside you find a set of repulsor coils, a crate of unmarked credit chips, and a datapad with a single message: DO NOT OPEN THIS.', once: true },
        ],
        npcs: [
          { id: 'at11', x: 15, y: 8, kind: 'droid', label: 'AirTaxi Droid AT-11', triggersOverlay: 'speeder',
            repeatPrompt: 'AT-11 pulses its running lights at you. You have been logged as a repeat non-passenger.',
            prompt: '"AIRTAXI UNIT AT-11. NOTE: THIS UNIT IS AWARE IT IS THE LESS POPULAR UNIT. NOTE: THIS UNIT HAS FEELINGS ABOUT THAT. DESTINATION QUERY."',
            choices: [
              { text: 'Tell it you prefer AT-11 to AT-9.', morality: 5, loyalty: {}, result: '"LOGGED AS PREFERRED PASSENGER. RATE: STANDARD MINUS FIVE PERCENT. BECAUSE YOU ARE KIND."' },
              { text: 'Ask about Flight C-7.', morality: 2, loyalty: { republic: 3 }, result: '"FLIGHT C-7 IS A RESTRICTED QUERY. THIS UNIT IS ALSO VERY NERVOUS ABOUT FLIGHT C-7."' },
            ],
          },
        ],
        collectibles: [{ id: 'bay2_chip', x: 6, y: 14, label: 'Unsigned Credit Chip', reward: 45 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 26, 18, 'floor');
          carveRect(g, 1, 1, 6, 4, 'ship_hull');
          carveRect(g, 21, 1, 26, 4, 'ship_hull');
          carveRect(g, 1, 15, 6, 18, 'ship_hull');
          carveRect(g, 21, 15, 26, 18, 'ship_hull');
          [[3,7],[4,7],[3,8],[4,8],[3,12],[4,12],[3,13],[4,13]].forEach(([x,y]) => pt(g,x,y,'wall'));
          pt(g,0,9,'door'); pt(g,0,10,'door');
          return g;
        },
      },
      jons_apt_int: {
        id: 'jons_apt_int', name: "Jon's Apartment", subtitle: "Coruscant · West Residential · Level 2",
        width: 18, height: 14, spawnPos: { x: 9, y: 11 }, textureId: 'coruscant',
        accent: '#7AB8E0', accentGlow: 'rgba(122,184,224,0.18)', accentDim: '#2A4A60',
        floorColor: '#191E30', floorAlt: '#202540', wallDark: '#0A0C14', wallLight: '#141828',
        bg: 'radial-gradient(circle at 50% 50%, #141828 0%, #080A14 70%)', ambient: 'mist',
        decor: ['archive', 'pipe', 'cargo_crate'],
        doors: [
          { x: 9, y: 13, targetZone: 'apartments', targetPos: { x: 6, y: 5 }, label: 'Residential Corridor' },
        ],
        worldObjects: [
          { id: 'jon_datapad', propArt: 'datapad_table', propVariant: 'jon', x: 12, y: 3, label: 'Encrypted Datapad', description: 'Manifest fragments. Three hub codes, three timestamps, forty-eight hours apart. Someone who knew the routing schedules. The Broken Circle is written in the margin in red.', once: true },
          { id: 'slicing_bench', propArt: 'workbench', propVariant: 'slicing', x: 14, y: 8, label: 'Slicing Workbench', description: 'A tangle of stripped datachips and bypass leads. Jon apparently does his best work at 0300.', once: false },
          { id: 'bay14_analysis_board', propArt: 'board', propVariant: 'analysis', x: 8, y: 4, once: false, iconKind: 'board', label: 'Bay 14 Analysis Board', description: 'A holographic display pinned with freight logs, blast pattern analyses, and three photographs labeled UNKNOWN. Jon has been working this case longer than he let on. One note in his handwriting reads: "Transit codes -- mine. How?"' },
          { id: 'faction_tension_chart', propArt: 'board', propVariant: 'faction', x: 3, y: 6, once: false, iconKind: 'board', label: 'Coruscant Faction Chart', description: 'A layered map of Coruscant levels with colored overlays: blue for CSF jurisdiction, red for Iron Syndicate activity, amber for contested freight corridors. The Senate District is circled three times in a different color than the others. No label. Just the circle.' },
        ],
        npcs: [
          { id: 'jon_apartment', x: 5, y: 3, kind: 'smuggler', label: 'Jon', questNpc: true,
            prompt: 'Here is the situation. Someone is moving stolen Phrik alloy through the lower levels -- military-grade material that has no business being in civilian freight lanes. Docking Bay 14 is the last confirmed point of entry. I need a partner I can trust to find out who is behind it before it disappears into the infrastructure.',
            choices: [
              { text: 'I am in. Give me everything you have.', morality: 5, loyalty: { underworld: 8 }, result: 'He slides a datapad across the table. "Bay 14. Start at the dock manifest kiosk and work backward. Whatever was moved through there, someone went to great lengths to pretend it did not exist." He pauses. "Be careful. The people who make things disappear do not appreciate witnesses."', grants: { credits: 200, flags: ['chapter1_active', 'speeder_transit_unlocked'] } },
              { text: 'What is in it for me beyond the credits?', morality: 0, loyalty: { underworld: 5 }, result: '"Answers. If you want to know why the Bay 14 cargo is important enough to kill for, you need to follow the thread. The credits are upfront. The answers are earned."', grants: { credits: 200, flags: ['chapter1_active', 'speeder_transit_unlocked'] } },
              { text: 'I need more upfront. This sounds dangerous.', morality: -3, loyalty: {}, result: 'He does not blink. "Three hundred. Because you are right. And because I need someone who knows the value of their own skin." He counts out the extra hundred from a lockbox.', grants: { credits: 300, flags: ['chapter1_active', 'speeder_transit_unlocked'] } },
            ],
            repeatPrompt: 'Jon is at the terminal running cargo projections. "Bay 14. Start there."',
            phases: [
              {
                id: 'phase_mission_brief',
                requiresAllFlags: [],
                requiresNoneFlags: ['chapter1_active'],
                prompt: 'Here is the situation. Someone is moving stolen Phrik alloy through the lower levels -- military-grade material that has no business being in civilian freight lanes. Docking Bay 14 is the last confirmed point of entry. I need a partner I can trust to find out who is behind it before it disappears into the infrastructure.',
                choices: [
                  { text: 'I am in. Give me everything you have.', morality: 5, loyalty: { underworld: 8 }, result: 'He slides a datapad across the table. "Bay 14. Start at the dock manifest kiosk and work backward. Whatever was moved through there, someone went to great lengths to pretend it did not exist." He pauses. "Be careful. The people who make things disappear do not appreciate witnesses."', grants: { credits: 200, flags: ['chapter1_active', 'speeder_transit_unlocked'] } },
                  { text: 'What is in it for me beyond the credits?', morality: 0, loyalty: { underworld: 5 }, result: '"Answers. If you want to know why the Bay 14 cargo is important enough to kill for, you need to follow the thread. The credits are upfront. The answers are earned."', grants: { credits: 200, flags: ['chapter1_active', 'speeder_transit_unlocked'] } },
                  { text: 'I need more upfront. This sounds dangerous.', morality: -3, loyalty: {}, result: 'He does not blink. "Three hundred. Because you are right. And because I need someone who knows the value of their own skin." He counts out the extra hundred from a lockbox.', grants: { credits: 300, flags: ['chapter1_active', 'speeder_transit_unlocked'] } },
                ],
                repeatPrompt: 'Jon is at the terminal running cargo projections. "Bay 14. Start there."',
              },
              {
                id: 'phase_bay14_direction',
                requiresAllFlags: ['chapter1_active'],
                requiresNoneFlags: ['freight_hub_investigated'],
                prompt: '"You have the transit pass. Sector 4 Freight Hub is your first stop -- that is where Bay 14 logs in and out. Talk to the dock engineer if you can find one willing to say anything. The official record has been sanitized. The physical evidence has not."',
                choices: [
                  { text: '"Is there anything in particular I should look for?"', morality: 5, loyalty: { underworld: 5 }, result: '"Blast marks that do not match a fuel fire. Cargo crates logged as agricultural that weigh three times what they should. And anyone who looks like they were paid to be somewhere else when the loading happened."', grants: { flags: ['bay14_method_known'] } },
                  { text: '"Do you have contacts at the hub who can smooth my entry?"', morality: 0, loyalty: { underworld: 5 }, result: '"One. Corin. He runs a salvage shop on Platform 04. Tell him the hydronspan ratio is off. He will know what it means." He says it like it costs him something.', grants: { flags: ['jax_jon_vouched'] } },
                  { text: '"I will handle it. No need for contacts."', morality: -3, loyalty: {}, result: '"Your call. Just remember -- down at Level 088, independent only means you have no backup."' },
                ],
                repeatPrompt: 'Jon is cross-referencing freight routes on a holographic display. "Level 088 is not going to investigate itself."',
              },
              {
                id: 'phase_bay14_debrief',
                requiresAllFlags: ['freight_hub_investigated'],
                requiresNoneFlags: ['jon_bay14_briefed', 'marlo_sky_talked', 'vane_sky_cooperated'],
                prompt: '"You found something. I can tell. Sit down." He shuts off his terminal and gives you his full attention for the first time.',
                choices: [
                  { text: '"Two unmarked lifters. Grey coats. Republic security codes that checked out clean."', morality: 5, loyalty: { underworld: 8 }, result: '"Republic codes in civilian freight lanes. That is an inside job. Someone with Senate access signed those passes." He stands and walks to the viewport. "The alloy is going somewhere specific. Sky-Market Level 1450 is the next thread -- find out who brokered the move. There is a man called Slick Marlo at the Aurebesh Lounge, and an Officer Vane at the precinct across the promenade. Pick your approach. And do not mention my name first -- not to either of them."', grants: { flags: ['jon_bay14_briefed', 'sky_market_direction_given'], codex: ['codex-jon-backstory'] } },
                  { text: '"The dock was stripped before I arrived. But I found a forged keycard and testimony from the engineer."', morality: 8, loyalty: { republic: 5 }, result: '"Testimony is a start. The keycard is better. Someone forged Republic-grade clearance -- that narrows the suspect pool considerably." He transfers coordinates to your datapad. "Sky-Market District, Level 1450. Two contacts: Marlo at the Aurebesh Lounge and Officer Vane at the CSF precinct. Do not use my name. Let them come to you."', grants: { flags: ['jon_bay14_briefed', 'sky_market_direction_given'], codex: ['codex-jon-backstory'] } },
                ],
                repeatPrompt: '"Sky-Market is the next step. Level 1450. And do not use my name up there."',
              },
              {
                id: 'phase_sky_debrief_marlo',
                requiresAllFlags: ['marlo_sky_talked'],
                requiresNoneFlags: ['vane_sky_cooperated', 'jon_sky_market_debriefed'],
                prompt: '"Marlo. Of course it is Marlo." He rubs the back of his neck. "He is good at finding buyers. He is very good at not being the one who ends up in custody. What did he tell you about the Syndicate?"',
                choices: [
                  { text: '"The alloy is going to The Works. The buyer has Senate credentials."', morality: -3, loyalty: { underworld: 10 }, result: '"A Senate-backed buyer running Phrik through an underbelly broker. Someone is building something off the books and using the Republic\'s own infrastructure to do it. This is bigger than Bay 14." He opens a new route file. "Marlo\'s connections are useful, but if the Republic traces the alloy, your name is near his. Stay aware of that."', grants: { flags: ['jon_sky_market_debriefed', 'iron_syndicate_senate_link_known'] } },
                  { text: '"He gave me the Buyer\'s ID. Iron Syndicate is the real operation."', morality: 0, loyalty: { underworld: 8 }, result: '"Iron Syndicate. I have heard that name twice in the last month. Both times from people who stopped talking shortly after." He does not say it lightly. "This goes beyond a freight dispute. Watch yourself."', grants: { flags: ['jon_sky_market_debriefed', 'iron_syndicate_senate_link_known'] } },
                ],
                repeatPrompt: '"The Syndicate is the thread. Pull it carefully."',
              },
              {
                id: 'phase_sky_debrief_vane',
                requiresAllFlags: ['vane_sky_cooperated'],
                requiresNoneFlags: ['marlo_sky_talked', 'jon_sky_market_debriefed'],
                prompt: '"You went Republic. I did not see that coming." He is not angry -- he is recalibrating. "Officer Vane is exactly who he looks like: a man who believes the system still works. He is either going to be the best ally you have ever had, or he is going to get you both killed."',
                choices: [
                  { text: '"He gave me a CSF Auxiliary Pass and sent me to the Academy. I am going in officially."', morality: 12, loyalty: { republic: 10 }, result: '"Then I am the unofficial version. You will need both." He leans back. "The CSF has access I do not have. I have contacts the CSF cannot touch. We cover more ground this way -- as long as Vane does not find out you are running two ledgers."', grants: { flags: ['jon_sky_market_debriefed', 'jon_republic_aware'] } },
                  { text: '"I wanted information and the badge was the fastest path. I am not a true believer."', morality: 0, loyalty: { underworld: 5 }, result: '"Good. Believers make bad operators." He almost smiles. "Use the CSF access. Just do not let Vane decide you are his. That conversation gets complicated."', grants: { flags: ['jon_sky_market_debriefed', 'deceiver_path_hinted'] } },
                ],
                repeatPrompt: '"You are running with the Republic now. Keep me in the loop."',
              },
              {
                id: 'phase_sky_debrief_both',
                requiresAllFlags: ['marlo_sky_talked', 'vane_sky_cooperated'],
                requiresNoneFlags: ['jon_sky_market_debriefed'],
                prompt: '"You worked both sides at the Sky-Market." He sets down his drink. "That is either very smart or very reckless. With Marlo and Vane both believing you are their contact, you have leverage -- and a very short window before they compare notes."',
                choices: [
                  { text: '"I can manage both. The Syndicate is what matters."', morality: 0, loyalty: {}, result: '"Then manage them. But have an exit plan ready. When Marlo and Vane discover each other, you want to be the one holding the manifest, not caught between their arguments."', grants: { flags: ['jon_sky_market_debriefed', 'double_contact_known'] } },
                ],
                repeatPrompt: '"Both sides think you are theirs. That clock is ticking."',
              },
              {
                id: 'phase_jon_sky_direction',
                requiresAllFlags: ['jon_bay14_briefed'],
                requiresNoneFlags: ['jon_sky_market_debriefed', 'marlo_sky_talked', 'vane_sky_cooperated'],
                prompt: '"Sky-Market, Level 1450. Marlo at the Aurebesh Lounge, or Officer Vane at the precinct. Whichever of them talks first knows what is going through Bay 14. Come back and tell me which one you chose."',
                choices: [
                  { text: '"I will start at the Sky-Market."', morality: 0, loyalty: {}, result: '"Good. Keep the Scylla manifest close."' },
                ],
                repeatPrompt: '"Sky-Market, Level 1450. Marlo or Vane. Report back."',
              },
              {
                id: 'phase_jon_awaiting',
                requiresAllFlags: ['jon_sky_market_debriefed'],
                requiresNoneFlags: ['csf_briefed', 'sector4_raid_complete'],
                prompt: '"I have what I need from the Sky-Market. Now we wait for the next move." He taps the table. "If the Republic path is open to you, take it. If it is the Works, take that. Either way, the thread ends at the Senate."',
                choices: [
                  { text: '"Understood. I will follow it."', morality: 0, loyalty: {}, result: '"Do not take longer than you have to."' },
                ],
                repeatPrompt: '"Follow the thread. Either path ends at the Senate."',
              },
              {
                id: 'phase_csf_confrontation',
                requiresAllFlags: ['csf_briefed'],
                requiresNoneFlags: ['jon_confrontation_done'],
                prompt: '"A CSF badge. Are you insane? I brought you in as a partner to run cargo, not to hand my operation over to Republic guards!" He is pacing. This is the angriest you have seen him.',
                choices: [
                  { text: '"This badge is the ultimate cover. I can scrub our manifests from inside the CSF database."', morality: -12, loyalty: { underworld: 15 }, result: 'He stops pacing. A long silence. "You are saying you can run our operation from inside the CSF." He works through it. "That is either brilliant or the most dangerous thing I have ever heard you say." He unlocks the Inside Man path.', grants: { flags: ['jon_confrontation_done', 'inside_man_path'] } },
                  { text: '"The underbelly is collapsing, Jon. Work with me legally or this operation does not survive."', morality: 15, loyalty: { republic: 15, underworld: -20 }, result: 'He looks at you for a long time. Then he locks his workbench and walks to the door. "If you genuinely believe that, then we want different things." He steps out. His shop closes temporarily.', grants: { flags: ['jon_confrontation_done', 'jon_gone_straight_warned', 'jon_shop_closed'] } },
                  { text: '"Vane is using me to find the alloy location. Once I have it, I drop the badge and we take the score."', morality: -5, loyalty: { underworld: 10 }, result: 'He studies you for a long time. "You are playing a very dangerous game with a man who is good at it." He nods slowly. "Alright. But if Vane gets close enough to see through you, the deal ends. No heroics on my behalf."', grants: { flags: ['jon_confrontation_done', 'deceiver_path'], credits: 200 } },
                ],
                repeatPrompt: 'Jon has returned. He is quieter than before. Watching you more carefully.',
              },
              {
                id: 'phase_post_confrontation_inside_man',
                requiresAllFlags: ['inside_man_path'],
                requiresNoneFlags: ['sector4_raid_complete'],
                prompt: '"I have been thinking about what you said. If you genuinely have CSF database access -- and I mean genuine write access, not just read -- then we have an opportunity here that does not come around twice in a career."',
                choices: [
                  { text: '"I can scrub manifests before Vane sees them. Which routes are the priority?"', morality: -15, loyalty: { underworld: 20 }, result: '"The Level 088 freight lines are the cleanest to sanitize. Vane never looks past the manifest header." He slides you a list. "Do not be greedy. One route at a time."', grants: { flags: ['inside_man_routes_known'] } },
                  { text: '"I want to keep this limited to the Bay 14 investigation. Not a permanent arrangement."', morality: -5, loyalty: { underworld: 8 }, result: '"Fair. Bay 14 only. When it is done, the badge goes back and we are what we were." He accepts the limit without argument.', grants: { flags: ['inside_man_limited'] } },
                ],
                repeatPrompt: '"Sector 4 is the active operation. Stay focused on the alloy."',
              },
              {
                id: 'phase_post_confrontation_republic',
                requiresAllFlags: ['jon_gone_straight_warned'],
                requiresNoneFlags: ['sector4_raid_complete'],
                prompt: 'He is back. His workbench is unlocked but his manner is different. Careful. Like someone who knows the ground is uncertain and is choosing each step deliberately.',
                choices: [
                  { text: '"I did not come back to push you. I came back because you are still my best lead on the Senate connection."', morality: 8, loyalty: { republic: 5 }, result: '"That is the only reason I opened the door." He sits. "What do you need?"', grants: { flags: ['jon_republic_truce'] } },
                  { text: '"I meant what I said. The Republic path is the only one that ends cleanly."', morality: 15, loyalty: { republic: 10 }, result: '"I know you did. I am still not sure I believe you are right." He looks out the viewport. "But I am still here. That has to count for something."', grants: { flags: ['jon_republic_truce', 'jon_softening'] } },
                ],
                repeatPrompt: '"We are still talking. That is something."',
              },
              {
                id: 'phase_post_confrontation_deceiver',
                requiresAllFlags: ['deceiver_path'],
                requiresNoneFlags: ['sector4_raid_complete'],
                prompt: '"How is Vane treating you? Playing along well?" He says it like a test.',
                choices: [
                  { text: '"He trusts me. We are close to the Senate authorization codes."', morality: -8, loyalty: { underworld: 12 }, result: '"Good. When you have the codes, we pull the alloy before the CSF even knows where to look. Have a transport on standby."', grants: { flags: ['deceiver_active'] } },
                  { text: '"I am having second thoughts about burning Vane."', morality: 5, loyalty: { republic: 5 }, result: '"Second thoughts now?" He is very still. "Do not tell Vane anything. But do not commit to the score either until you have decided." He means both halves equally.', grants: { flags: ['deceiver_wavering'] } },
                ],
                repeatPrompt: '"Vane is useful until he is not. You know what comes after."',
              },
              {
                id: 'phase_endgame_reveal',
                requiresAllFlags: ['sector4_raid_complete'],
                requiresNoneFlags: ['jon_endgame_known'],
                prompt: '"You walked out of Sector 4 in one piece. I was not certain you would." He pauses. "Sit down. There is something about the Bay 14 route I should have told you at the start."',
                choices: [
                  { text: '"Tell me."', morality: 5, loyalty: { underworld: 8, republic: 5 }, result: '"Three years ago I ran cargo on the Scylla route -- before it became a cover operation. I did not know what was in the sealed containers. When I found out, I shut the route down. Someone used my old transit codes to reopen it. Whoever is behind the Bay 14 Syndicate operation did not build from nothing. They built from what I left behind." He does not ask for forgiveness. "The Senate connection means this goes higher than either of us can reach alone. But the Level 005 route ends at a Senate transit terminal. If you can get there before the shipment departs, you can stop it."', grants: { flags: ['jon_endgame_known', 'senate_personal_stake_revealed'], codex: ['codex-jon-backstory'] } },
                  { text: '"You owe me an explanation. Start talking."', morality: -5, loyalty: {}, result: '"Yes. I do." He tells you. The same information. Without the apology. The obligation is settled. The weight stays.', grants: { flags: ['jon_endgame_known', 'senate_personal_stake_revealed'], codex: ['codex-jon-backstory'] } },
                ],
                repeatPrompt: '"The Senate transit terminal is the end of the thread. Go finish it."',
              },
              {
                id: 'phase_jon_pursue',
                requiresAllFlags: ['jon_endgame_known'],
                requiresNoneFlags: ['senate_line_secured'],
                prompt: '"Level 005, The Works. Vex runs the Syndicate end of this." Jon is at the viewport with his jacket on, as if he might leave at any moment. "The Iron Syndicate kept its command hub below the cooling ducts. Their files will say what they are really building, and where. Bring me what you find, or stop it yourself if I am too slow."',
                choices: [
                  { text: '"I will go to The Works now."', morality: 0, loyalty: { underworld: 3 }, result: '"Take the cooling ducts. The Syndicate command terminal is at the end. Be careful with what you copy."' },
                  { text: '"Is there anything you are still not telling me?"', morality: 3, loyalty: {}, result: '"Almost certainly." A small, honest smile. "But nothing that changes where you go next."' },
                ],
                repeatPrompt: '"The Works, Level 005. The command terminal at the end of the cooling ducts."',
              },
              {
                id: 'phase_finale',
                requiresAllFlags: ['senate_line_secured'],
                requiresNoneFlags: ['inheritance_active', 'jon_finale_seen'],
                prompt: '"It is done." He says it quietly. No celebration. Just acknowledgment. "Whatever path you took to get here -- Republic badge, underworld leverage, or something in between -- the alloy is gone and the Senator is exposed." He looks at the viewport. "I am going to close the apartment for a while. Let things settle." A pause. "You did good work."',
                choices: [
                  { text: '"What happens to your operation now?"', morality: 0, loyalty: {}, result: '"Smaller. Cleaner. The old routes are burned. But I still know people." He almost smiles. "I will find something."', grants: { flags: ['jon_finale_seen'] } },
                  { text: '"It did not have to go this way. But I am glad you were in it."', morality: 8, loyalty: { underworld: 5, republic: 5 }, result: '"Yeah." He picks up his jacket. "Me too." He means it.', grants: { flags: ['jon_finale_seen', 'jon_farewell_warm'] } },
                  { text: '"We should talk about what comes next. There is more work to be done."', morality: 5, loyalty: { underworld: 8 }, result: '"There is always more work." He locks the terminal. "Give me a week. Then come back." He walks to the door without turning around. The invitation is open.', grants: { flags: ['jon_finale_seen', 'jon_future_open'] } },
                ],
                repeatPrompt: '"It is done. The rest is the Republic\'s problem now."',
              },
              {
                id: 'phase_inheritance_offer',
                requiresAllFlags: ['senate_line_secured', 'jon_finale_seen'],
                requiresNoneFlags: ['inheritance_active'],
                prompt: '"The Scylla route is gone. The Senate connection is burned. The CSF knows my name now, and I cannot run freight through the official channels for at least two years." He pours two cups of caf and pushes one across the table without asking. "But the network still exists. The pit, the Alleyway, the warehouse agreements, the bribe schedule. Fifteen years of infrastructure. I need someone I can trust to stand beside me and run half of it. I am asking you." He holds your gaze. "I am not asking for an answer right now. But I am asking."',
                choices: [
                  { text: '"I am in. Show me what you have built."', morality: 0, loyalty: { underworld: 10 }, result: '"Good." He opens a drawer and pulls out a sealed cargo manifest. "Start with this. Bay 14. Shipment that was waiting when the blast sealed the bay. Get it. Then we talk about what comes next."', grants: { flags: ['inheritance_active'], codex: ['codex-jon-inheritance'] } },
                  { text: '"I need to think about it." (defer)', morality: 0, loyalty: {}, result: '"Take your time. The offer does not expire." He turns back to the viewport.' },
                ],
                repeatPrompt: '"The offer stands. Bay 14. Manifest. Come back when you have it."',
              },
              {
                id: 'phase_manifest_pending',
                requiresAllFlags: ['inheritance_active'],
                requiresNoneFlags: ['weapons_manifest_acquired', 'cargo_skimmed', 'cargo_delivered_full'],
                prompt: '"First step. The Bay 14 cache at the Freight Hub, northeast bay. Thirty-two crates marked agricultural equipment. The CSF counted boxes and never opened them." He taps the table. "Under the top lid there is a manifest. I need it in my hand before I decide what you carry and where it goes."',
                choices: [
                  { text: '"Northeast bay, Freight Hub. I am on it."', morality: 0, loyalty: { underworld: 3 }, result: '"Do not stay long. Evidence tape is not a lock, but it is a promise someone will check."' },
                  { text: '"Why do you need the manifest and not the crates?"', morality: 3, loyalty: {}, result: '"Because the crates are what everyone expects me to move. The manifest is what tells me who is expecting them."' },
                ],
                repeatPrompt: '"The manifest. Bay 14, northeast bay at the Freight Hub. Bring it here."',
              },
              {
                id: 'phase_inheritance_ongoing',
                requiresAllFlags: ['inheritance_active'],
                requiresAnyFlag: ['cargo_skimmed', 'cargo_delivered_full'],
                prompt: '"Malak, Reya, the vault." Jon counts them off without looking at you. "The network is moving whether I watch or not. Do not stop to ask my permission. If you need me, you know where the viewport is."',
                choices: [
                  { text: '"I will see it through."', morality: 0, loyalty: { underworld: 3 }, result: '"I know." He turns back to the city.' },
                ],
                repeatPrompt: '"Shadow Town. Reya. The vault. In that order, or whichever order keeps you alive."',
              },
              {
                id: 'phase_manifest_choice',
                requiresAllFlags: ['inheritance_active', 'weapons_manifest_acquired'],
                requiresNoneFlags: ['cargo_skimmed', 'cargo_delivered_full'],
                prompt: '"You got it. Good." Jon takes the manifest, scans it, and sets it down. "Level 1312 contact is waiting. Straight delivery, every crate, and you walk in as my Enforcer. Or..." He slides the manifest back toward you with one finger. "...there is a storage alcove at the south end of the arena. You could pull ten crates, lock them there, and I would never know. I would have leverage over you. But you would have leverage over me. Depending on what you want from this arrangement, that might be more useful."',
                choices: [
                  { text: '"Full delivery. I do not need leverage over you."', morality: 5, loyalty: { underworld: 5 }, result: '"Good. Go to Shadow Town, Level 1312. Malak will receive the shipment. Tell him I sent you." He marks the destination on your comlink.', grants: { flags: ['cargo_delivered_full', 'enforcer_position_granted'] } },
                  { text: '"I will take the insurance. Ten crates go in the alcove."', morality: -5, loyalty: { underworld: 8 }, result: '"Smart." He almost sounds like he means it as a compliment. "Cache key is yours. The alcove is third from the south end. No one checks it." He does not ask which ten crates.', grants: { flags: ['cargo_skimmed'], items: ['shadow_arms_cache_key'] } },
                ],
              },
              {
                id: 'phase_warrant_choice',
                requiresAllFlags: ['reya_warrant_cleared'],
                requiresNoneFlags: ['reya_loyal', 'reya_blackmailed'],
                prompt: '"The warrant is gone." Jon does not seem surprised. "Reya will come around now. One thing, did you keep a copy of the data?" He watches you carefully. "If you did, that is leverage over her. She knows it. It changes the working relationship."',
                choices: [
                  { text: '"I deleted it. Clean slate."', morality: 10, loyalty: { republic: 5 }, result: '"Soft." He shrugs. "But she will respect it. Go talk to her." He turns back to his terminal.', grants: { flags: ['reya_loyal'] } },
                  { text: '"I kept a copy. She works harder knowing I have it."', morality: -10, loyalty: { underworld: 5 }, result: '"There it is." He nods once. "She will hate you for it. But she will show up." He says it without judgment.', grants: { flags: ['reya_blackmailed'] } },
                ],
              },
            ],
          },
        ],
        collectibles: [],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 16, 12, 'floor');
          carveRect(g, 1, 1, 4, 4, 'wall');
          carveRect(g, 13, 1, 16, 4, 'wall');
          pt(g, 9, 13, 'door');
          return g;
        },
      },
      sky_market: {
        id: 'sky_market', name: 'Sky-Market District', subtitle: 'Coruscant · Upper Mid-Levels · L.1450',
        width: 38, height: 26, spawnPos: { x: 4, y: 13 }, textureId: 'coruscant',
        accent: '#E8A030', accentGlow: 'rgba(232,160,48,0.35)', accentDim: '#C09040',
        floorColor: '#2A2418', floorAlt: '#38321E', wallDark: '#1E1A0E', wallLight: '#3A3220',
        bg: 'radial-gradient(circle at 50% 0%, #3A2808 0%, #241A08 20%, #0E0A06 55%, #080604 100%)', ambient: 'sky_high', floorPattern: 'marble',
        decor: ['pillar', 'neon_sign', 'brazier', 'neon_sign', 'cargo_crate', 'pipe', 'girder'],
        doors: [
          { x: 0, y: 12, targetZone: 'sky_customs', targetPos: { x: 33, y: 12 }, label: 'Skyway Customs' },
          { x: 0, y: 13, targetZone: 'sky_customs', targetPos: { x: 33, y: 11 }, label: 'Skyway Customs' },
          { x: 15, y: 25, targetZone: 'heat_sink_slums', targetPos: { x: 15, y: 1 }, label: 'Heat Sink Slums' },
          { x: 36, y: 0, targetZone: 'penthouse', targetPos: { x: 19, y: 27 }, label: 'Penthouse Elevator' },
          { x: 5, y: 0, targetZone: 'senatorial_lounges', targetPos: { x: 19, y: 26 }, label: 'Senatorial Sky-Lounges' },
        ],
        worldObjects: [
          { id: 'jon_arrival_comlink', propArt: 'comm_relay', propVariant: 'puck', x: 5, y: 13, once: true, iconKind: 'comlink', label: 'Incoming Comlink', description: "Jon's voice crackles over the encrypted channel. \"Watch your back up there. Level 1450 looks clean, but the vultures here wear tailored suits instead of gang colors. If someone's liquidating a shipment of stolen Phrik alloy, they'll need a broker registered with the Sky-Market Exchange to clear the credit transfers. Check out the Aurebesh Lounge and find Slick Marlo — or talk to Officer Vane at the precinct if you want to play this by the book. Either way: do not mention my name first.\" The channel closes." },
          { id: 'sky_market_terminal', propArt: 'console', propVariant: 'exchange', x: 20, y: 7, once: false, label: 'Trade Exchange Terminal', description: 'Live credit-transfer rates across fourteen systems. One manifest flagged for anomalous routing: SCYLLA FREIGHT. Destination: redacted. Shipper: redacted.' },
          { id: 'csf_bulletin', propArt: 'board', propVariant: 'bulletin', x: 28, y: 14, once: false, iconKind: 'board', label: 'CSF Bulletin Board', description: 'Three active investigations listed. Two marked classified. The third — Cargo Anomaly / Bay 14 — shows status: CLOSED. Filed by: Vane, T. Closure date: two days after the incident.' },
          { id: 'lounge_bar_terminal', propArt: 'kiosk', propVariant: 'drinks', x: 5, y: 5, once: false, label: 'Lounge Drink Terminal', description: 'A rotating holographic menu. Thirty-seven varieties of exotic spirits from fourteen systems. One local special listed as "Bay 14 Blend." Whoever named it has a sense of humor or information you do not.' },
          { id: 'lounge_private_booth', propArt: 'booth', propVariant: 'private', x: 9, y: 8, once: true, iconKind: 'booth', label: 'Occupied Booth', description: 'Two figures in grey coats, backs to the room. Neither is drinking. Both are watching the exit.', grantsFlag: 'syndicate_watchers_seen', grantsCodex: 'codex-iron-syndicate',
            autoCodex: { id: 'discovery-lounge-booth-watchers', title: 'Iron Syndicate Watchers', category: 'discoveries', summary: 'Field observers in the Aurebesh Lounge.', body: ['Two figures in grey coats sit with their backs to the room. Neither is drinking. Both are watching the exit. Iron Syndicate field observers — if they recognize you, they will report your presence at the lounge to Vex.', 'Their positioning covers the main entrance and the transit lift. This is a professional surveillance configuration, not a casual meeting.'] } },
          { id: 'lounge_datapad', propArt: 'datapad_table', propVariant: 'lounge', x: 3, y: 9, once: true, label: 'Left Behind Datapad', description: 'Encrypted but partially readable. Credit transfers totaling 840,000 credits routed through three shell corporations to a Coruscant financial account. The destination account number matches one field on the Scylla manifest.', grantsFlag: 'credit_trail_found' },
          { id: 'precinct_evidence_locker', propArt: 'evidence_locker', x: 29, y: 6, once: false, iconKind: 'safe', label: 'Evidence Locker — Bay 14', description: "CASE STATUS: CLOSED. PRIMARY EVIDENCE: destroyed in dock fire. PHYSICAL SAMPLES: none recovered. WITNESS STATEMENTS: sealed under Senate Directive 1182-C. The locker is padlocked with a standard CSF code seal. Vane's name is on the closure authorization." },
          { id: 'precinct_comms_station', propArt: 'console', propVariant: 'dispatch', x: 33, y: 5, once: false, label: 'CSF Dispatch Station', description: 'Twelve active patrol frequencies. Six are handling routine traffic violations. Five are static. One — Sector 4 Channel B — is broadcasting a continuous loop: "All units stand by. Sector 4 incident classified pending Senate review."' },
          { id: 'precinct_wanted_board', propArt: 'board', propVariant: 'warrants', x: 29, y: 9, once: false, iconKind: 'board', label: 'Sector 4 Active Warrants Board', description: 'Fourteen open warrants. Nine are standard Black Sun identifiers. Four are listed as CLASSIFIED with Senate clearance required to view. The fifteenth entry — Bay 14 strike team — shows status: SUSPENDED. Effective date: two days after the incident.' },
          { id: 'airtaxi_sky_market', propArt: 'airtaxi', propVariant: 'clean', x: 35, y: 20, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal', description: 'Coruscant AirTaxi Network terminal. Departs on demand.' },
          { id: 'penthouse_elevator_panel', propArt: 'elevator_door', x: 36, y: 1, once: false, iconKind: 'panel', label: 'Scylla Tower Seven Elevator',
            requiresFlag: 'inheritance_active',
            description: 'A private elevator car with no public registration. The panel reads SCYLLA TOWER SEVEN — LEVEL 1452 — AUTHORIZED CLEARANCE ONLY. Your clearance is on file. Step in.' },
          { id: 'skyline_vista', propArt: 'skyline', propVariant: 'promenade', x: 19, y: 1, once: false, iconKind: 'vista', label: 'Promenade Skyline Overlook', description: 'The promenade edge opens onto open air and three thousand meters of vertical city. Speeders stream in tight formation lanes. Above: the Senate dome, catching the last reflected light from the planet\'s artificial sun cycle. Below: nothing visible. The lower levels begin where the light stops.',
            worldStateVariant: {
              lawful: 'The view from the law: the Republic\'s skyline, lit and ordered, everything it protects visible from one glance. The lower levels are down there somewhere, past the point where the light gives up.',
              underworld: 'The view from the other side: that Senate dome is where the authorization codes came from. Everything clean and lit up top, everything complicated below. You are already on the wrong level for clean.',
            },
          },
          { id: 'holonet_kiosk', propArt: 'kiosk', propVariant: 'hnn', x: 14, y: 14, once: false, iconKind: 'hologram', label: 'HoloNet News Kiosk', description: '[HNN — PRIORITY FEED] "The Coruscant Port Authority today confirmed that a routine fuel-line incident at Sub-Surface Docking Bay 14 has been fully resolved. Port Director Hadras stated that all cargo logs were destroyed in the secondary fire, consistent with standard emergency protocol. Senate Commerce Committee Chair confirmed there is no active investigation." The kiosk screen cycles to a weather advisory. Coruscant does not have weather.' },
          { id: 'lounge_corner_conversation', propArt: 'booth', propVariant: 'corner', x: 6, y: 10, once: true, iconKind: 'booth', label: 'Overheard Corner Table', description: 'Two men in expensive suits are speaking just below audible. One places a datacard on the table. The other shakes his head. The first slides it closer. The second looks out the window for a long moment, then pockets it. Neither man acknowledges that you passed. The datacard is gone.', grantsFlag: 'corner_deal_witnessed' },
          { id: 'promenade_patrol_log', propArt: 'board', propVariant: 'patrol', x: 24, y: 18, once: false, label: 'CSF Patrol Route Terminal', description: 'A public-facing transit safety board maintained by the CSF. Patrol schedule for Sector 4: suspended pending Senate review. Patrol schedule for Sky-Market Promenade: reduced to single-officer rotating shift. The reduction took effect two days after the Bay 14 incident. The authorization is signed: Vane, T.' },
          { id: 'overhead_traffic_lanes', propArt: 'skyline', propVariant: 'lanes', x: 24, y: 2, once: false, label: 'Open-Air Promenade Edge', description: 'The skyway opens above you. Repulsorlift traffic moves in tight formation lanes a hundred meters overhead — freight skiffs, executive speeders, municipal transports. The lanes are color-coded by altitude and clearance tier. A constant low-frequency vibration moves through the durasteel walkway beneath your feet. Somewhere below, Level 1222 looks up through the same open air.' },
          { id: 'aurebesh_neon_sign', propArt: 'sign_array', propVariant: 'vendor', x: 16, y: 11, once: false, label: 'Aurebesh Neon Sign Array', description: 'A bank of hand-lettered Aurebesh signs in electric blue and deep crimson mark the vendors along the concourse edge. EXCHANGE CERTIFIED. PHRIK-FREE MATERIALS. SENATE-APPROVED TRANSIT. BEST RATES BETWEEN THE LEVELS. One sign at the end of the row has been unplugged. It read: SCYLLA FREIGHT — REGISTERED CARRIER. The housing still glows faintly from residual charge.' },
          { id: 'vendor_stall_twi', propArt: 'vendor_stall', propVariant: 'fruit', x: 10, y: 15, once: false, iconKind: 'stall', label: "Twi'lek Fruit Stand", description: "A canopied stall made from canvas weave and a repurposed cargo-crate frame. Lelani, a green-lekku Twi'lek vendor, is arguing cheerfully in three languages with a Ishi Tib buyer over the price of something that glows orange. The canopy overhead is patched in four places with a different fabric each time. It has survived longer than the district's last three CSF precinct commanders." },
          { id: 'droid_repair_kiosk', propArt: 'vendor_stall', propVariant: 'droid_repair', x: 21, y: 17, once: false, iconKind: 'stall', label: 'Droid Repair Kiosk', description: 'A narrow stall crammed with disassembled photoreceptors, motivator housings, and at least three different droid torsos in various states of rebuild. The proprietor — a pair of heavily tattooed human hands, the rest of the body hidden behind a parts rack — is precision-soldering a restraining bolt without looking up. A handwritten sign reads: NO DROIDS REFUSED. NO QUESTIONS ASKED. PAYMENT UPFRONT.' },
          { id: 'freight_crane_alpha', propArt: 'crane_arm', x: 30, y: 20, once: false, label: 'Freight Terrace Crane Arm', description: 'A heavy industrial crane arm extends from the terrace edge over the open air, weighted with a repulsor counterbalance. Below, on a cantilevered platform you can only partially see, a cargo skiff is being unloaded by two astromech units stacking standardized Czerka containers. The crane arm bears three inspection seals, none of which match the current registration cycle.' },
          { id: 'landing_pad_beacon', propArt: 'landing_beacon', x: 14, y: 21, once: false, label: 'Landing Pad 1450 Beacon', description: 'A red collision beacon mounted to the terrace railing flashes in three-second intervals, keeping skiff pilots on approach path. The pad below is rated for two medium-class freighters simultaneously. Currently: four skiffs, two of them unmarked, parked in a configuration that leaves no room for a legitimate arrival. Nobody has filed a complaint with the port authority in six days.' },
          { id: 'repulsor_fluid_slick', propArt: 'fluid_slick', x: 22, y: 20, once: false, label: 'Repulsor Fluid Slick', description: 'A wide iridescent puddle has spread from a leaking repulsor stall fitting across the walkway surface. The fluid catches every neon sign in the concourse and renders them in elongated reflections across the durasteel: electric blue, magenta, amber. Standing in it, you can read the entire Aurebesh sign row backwards in the ground beneath your feet.' },
          { id: 'maintenance_strut_a', propArt: 'support_pillar', x: 8, y: 20, once: false, label: 'Ferro-Concrete Support Pillar', description: 'One of the ribbed ferro-concrete support pillars that holds the promenade above the freight terraces. The surface is layered with stenciled Aurebesh tags going back decades — territorial markers, love declarations, gang warnings, one very detailed accusation against a port official that has never been scrubbed. Copper conduit runs up the pillar face in bundled loops, patched at two separate points with wire that does not match.' },
          { id: 'floor_grate_vista', propArt: 'floor_grate', x: 15, y: 23, once: false, label: 'Conduit Underdeck Grate', description: 'A heavy transpariesteel floor grate gives a direct view down through the promenade substructure to the Conduit Underdeck. Exposed municipal piping runs in parallel lines below. Harsh yellow work lights illuminate narrow maintenance catwalks that run the full length of the market floor from beneath. A CSF security node blinks amber at the far end of a catwalk. Someone has been down there recently — a boot print in the condensation on the grate surface.' },
          { id: 'steam_vent_municipal', propArt: 'steam_vent', x: 8, y: 23, once: false, label: 'Municipal Steam Vent', description: 'A ventilation shaft access point built into the promenade deck. At irregular intervals it exhales a column of hot industrial steam that catches the amber light and briefly turns the lower walkway into something resembling a cloudscape. The vent connects to the underdeck thermal management system for the levels below. Maintenance logs on the panel beside it show no scheduled servicing in eleven months.' },
          { id: 'conduit_security_node', propArt: 'junction_box', propVariant: 'csf', x: 22, y: 23, once: true, label: 'Underdeck Security Node', description: 'A CSF-issue security node mounted to the underside of the promenade deck, accessible from the maintenance catwalk. It controls the patrol-droid routing across the lower freight terraces. The casing is standard Republic manufacture — but the firmware version is three cycles out of date. Someone who knew what they were doing could slice it and redirect the patrol pattern entirely.', grantsFlag: 'conduit_node_seen' },
          { id: 'maintenance_catwalk_junction', propArt: 'catwalk_junction', x: 30, y: 23, once: false, label: 'Maintenance Catwalk Junction', description: 'The narrow catwalk beneath the promenade splits here into three branches: east to the freight crane platforms, west back under the Aurebesh Lounge, and straight down via a ladder access to the Level 1222 ventilation spine. The junction is unmarked. The ladder rungs disappear into darkness forty meters below the grate. Above you, through the floor, you can hear market vendors and the faint pulse of Aurebesh neon.' },
        ],
        npcs: [
          { id: 'ph_merchant_voss', x: 19, y: 13, kind: 'broker', label: 'Ilmara Voss, Skyline Interiors',
            isVendor: true, vendorFaction: 'underworld', repeatable: true, requiresFlag: 'inheritance_active',
            vendorStock: ['ph_pkg_skyview', 'ph_pkg_tint'],
            vendorGreeting: "\"A penthouse without a view is a very expensive box. I sell the view. Nightfall for the glass, a polarization suite for privacy. Take a look.\"",
            repeatPrompt: "Voss runs a fingertip along a sample of night glass. \"Skylines are my religion. Penthouse owners are my congregation.\"",
            prompt: "\"A penthouse without a view is a very expensive box. I sell the view. Nightfall for the glass, a polarization suite for privacy. Take a look.\"",
            choices: [] },
          { id: 'marlo_sky', x: 6, y: 7, kind: 'broker', label: '"Slick" Marlo',
            prompt: 'He does not look up from his drink. "You have the look of someone who wandered three hundred levels off course. Give me one reason I should not have my guard droids show you the long way down."',
            repeatPrompt: 'Marlo is watching the exits. His glass stays full.',
            phases: [
              {
                id: 'phase_intro',
                requiresAllFlags: [],
                prompt: 'He does not look up from his drink. "You have the look of someone who wandered three hundred levels off course. Give me one reason I should not have my guard droids show you the long way down."',
                repeatPrompt: 'Marlo is watching the exits. His glass stays full.',
                choices: [
                  { text: 'Lay the Scylla manifest on the table. "Because I know what came off Bay 14."', morality: -5, loyalty: { underworld: 15 }, requires: { item: 'stolen_manifest' }, result: 'He leans forward. The bored expression drops. "That alloy is going to Level 005. Someone is building armor down there. Combat-grade. Phrik-plated." He names a buyer. You gain the Buyer\'s Encrypted ID.', grants: { items: ['buyers_id'], flags: ['marlo_sky_talked'], codex: ['codex-iron-syndicate'] } },
                  { text: '"Jon sends his regards. We have a mutual interest in the Bay 14 shipment."', morality: -3, loyalty: { underworld: 8 }, result: 'He studies you. "Jon is careful about who he vouches for. Sit down. This conversation just became interesting." He does not commit — but he does not call the droids either.', grants: { flags: ['marlo_sky_intro'] } },
                  { text: '"Step aside. I have business with the CSF precinct across the promenade."', morality: 8, loyalty: { republic: 5 }, result: 'He raises an eyebrow. "Bold choice. Enjoy the view from the precinct lockup." He turns back to his drink.' },
                ],
              },
              {
                id: 'phase_warmed',
                requiresAllFlags: ['marlo_sky_intro'],
                requiresNoneFlags: ['marlo_sky_talked'],
                prompt: '"Jon vouches. That is something. But vouching is not evidence, and I deal in evidence. You want in on this conversation, you bring me something with teeth."',
                repeatPrompt: 'Marlo has one eye on you, one on the promenade. He is always measuring something.',
                choices: [
                  { text: 'Show him the Scylla manifest. "This has teeth."', morality: -5, loyalty: { underworld: 15 }, requires: { item: 'stolen_manifest' }, result: '"Now we are talking. That alloy is earmarked for The Works, Level 005. The buyer is moving fast. You want the name — you work with me on a distribution problem first." He slides you the Buyer\'s ID. "The Iron Syndicate is the real prize here. Everything else is noise."', grants: { items: ['buyers_id'], flags: ['marlo_sky_talked'], codex: ['codex-iron-syndicate'] } },
                  { text: '"What distribution problem?"', morality: 0, loyalty: { underworld: 5 }, result: '"Black Sun controls the transit lifts on Levels 1100 to 1300. If I cannot move cargo through that corridor, my entire network stalls. Rook is the problem. You might be the solution." He refills his glass.', grants: { flags: ['marlo_rook_hinted'] } },
                  { text: '"I am not your fixer. I want the buyer\'s name first."', morality: 0, loyalty: {}, result: '"Names cost trust. Trust costs time. Come back when you have both." He is not angry. He is patient. That is somehow worse.' },
                ],
              },
              {
                id: 'phase_partner',
                requiresAllFlags: ['marlo_sky_talked'],
                prompt: '"The Buyer\'s ID points to a Senate financial sub-account. Someone very senior is very nervous. The alloy is already in transit to Level 005. If you can get to Vex before the crucible fires, you can intercept the entire shipment." He leans back. "Or you work the Senate angle. Your call."',
                repeatPrompt: 'Marlo is reading a credit ticker on his personal display. He speaks without looking up. "The timer on that crucible is not decorative."',
                choices: [
                  { text: '"Tell me everything you know about the Iron Syndicate\'s operation in The Works."', morality: -3, loyalty: { underworld: 10 }, result: '"Three levels of Syndicate security. Plasma channels that double as kill corridors. And a droid archivist that the Syndicate left running by accident — 7-N4. It knows things its masters do not realize it knows."', grants: { flags: ['works_briefed_by_marlo'], codex: ['codex-iron-syndicate'] } },
                  { text: '"What is your cut of this, Marlo?"', morality: 0, loyalty: {}, result: '"Territory. Not credits. If the Syndicate falls, the mid-level freight routes open up. I get the lanes. You get the glory. Everybody wins." He means every word.', grants: { flags: ['marlo_terms_known'] } },
                ],
              },
            ],
          },
          { id: 'vane_sky', x: 30, y: 7, kind: 'republic_guard', label: 'Officer Vane',
            prompt: 'He is reviewing a holographic flight manifest when you approach. "This precinct is not a tourist stop. State your business or clear the promenade."',
            repeatPrompt: 'Vane watches the promenade traffic. His hand stays near his weapon.',
            phases: [
              {
                id: 'phase_intro',
                requiresAllFlags: [],
                prompt: 'He is reviewing a holographic flight manifest when you approach. "This precinct is not a tourist stop. State your business or clear the promenade."',
                repeatPrompt: 'Vane watches the promenade traffic. His hand stays near his weapon.',
                choices: [
                  { text: 'Place the Scylla manifest on his holo-table. "Bay 14 was not an accident."', morality: 15, loyalty: { republic: 15 }, requires: { item: 'stolen_manifest' }, result: '"This confirms Phrik alloy logged under false Senate credentials. Inside job." His expression hardens. "Take this CSF Auxiliary Pass. Get into Sector 4 and pull names. Report back to me directly." He pauses. "Welcome aboard, Auxiliary."', grants: { items: ['csf_aux_pass'], flags: ['vane_sky_cooperated', 'republic_path_open'], codex: ['codex-csf-protocol'] } },
                  { text: '"I want to join the CSF. Formally. Whatever the fast track looks like."', morality: 12, loyalty: { republic: 12 }, result: '"You do not join the CSF by walking into a precinct on the promenade. But I can sponsor an Auxiliary Corps application. Come back when you have evidence to back it up. Then we talk."', grants: { flags: ['vane_sky_intro'] } },
                  { text: '"I am looking into the Bay 14 raid. Freelance."', morality: 0, loyalty: {}, result: '"Freelance investigators are not sanctioned by the Republic. If you find anything relevant you will turn it over to this precinct. Understood?" He returns to his manifest without waiting for an answer.', grants: { flags: ['vane_sky_intro'] } },
                  { text: 'Say nothing and leave.', morality: 0, loyalty: {}, result: 'He does not acknowledge your departure.' },
                ],
              },
              {
                id: 'phase_returned',
                requiresAllFlags: ['vane_sky_intro'],
                requiresNoneFlags: ['vane_sky_cooperated'],
                prompt: '"You came back. I noted that." He sets his manifest aside. "Most people who come to a CSF precinct on business do not return once I turn them away. That tells me something. What changed?"',
                repeatPrompt: 'He is watching you now. Not the promenade. You.',
                choices: [
                  { text: 'Present the Scylla manifest. "I found what you need."', morality: 15, loyalty: { republic: 15 }, requires: { item: 'stolen_manifest' }, result: 'He takes the manifest and reads it in silence. The promenade noise drops away. "Inside job. Senate-level clearance codes. This is not Black Sun — this is someone with real authority." He stands. "Auxiliary Corps. Provisional commission. Take this pass and get into Sector 4. Names, dates, chain of custody. Everything." A beat. "Do not make me regret this."', grants: { items: ['csf_aux_pass'], flags: ['vane_sky_cooperated', 'republic_path_open'], codex: ['codex-csf-protocol'] } },
                  { text: '"I know who owns the Bay 14 alloy. But I need a Republic guarantee first."', morality: 5, loyalty: { republic: 5 }, result: '"A guarantee of what?" His eyes narrow. "If you are negotiating immunity for a contact, I need to know the contact\'s name and their exposure before I can commit to anything. I am not in the business of blank pardons."', grants: { flags: ['vane_negotiation_started'] } },
                  { text: '"I changed my mind. I am not ready to work with the Republic."', morality: 0, loyalty: {}, result: '"Then do not take up my time." He picks up his manifest. But he does not tell you to leave. The offer is still open.' },
                ],
              },
              {
                id: 'phase_cooperating',
                requiresAllFlags: ['vane_sky_cooperated'],
                prompt: '"Sector 4 is the priority. Every hour we wait, the evidence chain degrades. I need a name attached to those Senate clearance codes before the Iron Syndicate buries this deeper."',
                repeatPrompt: '"Sector 4. Every hour matters, Auxiliary."',
                choices: [
                  { text: '"What happens when we get the name?"', morality: 5, loyalty: { republic: 8 }, result: '"We build a file. A proper one. Chain of custody, witness testimony, physical evidence. When it goes to a Senate tribunal, it has to be bulletproof. One procedural error and whoever signed those codes walks free." He taps his badge. "That is not happening on my watch."', grants: { flags: ['vane_procedure_explained'] } },
                  { text: '"If this goes wrong in Sector 4, what backup do I have?"', morality: 8, loyalty: { republic: 8 }, result: '"Officially, none. Unofficially, I have two units on standby at Level 1088 Outpost. If you light the emergency beacon on your pass, they respond within four minutes." He pauses. "Try not to need them."', grants: { flags: ['vane_backup_revealed'] } },
                ],
              },
            ],
          },
          { id: 'promenade_vendor', x: 20, y: 15, kind: 'cantina_owner', label: 'Promenade Vendor Oska',
            isVendor: true, vendorFaction: 'underworld', repeatable: true,
            vendorStock: ['item_brandy', 'field_rations', 'item_spice_vial'],
            repeatPrompt: 'Oska polishes a glass and pretends not to notice you.',
            prompt: '"Upper levels, lower prices if you know how to ask. What are you after?"',
            choices: [
              { text: 'Ask about the men in grey coats on the promenade.', morality: 0, loyalty: {}, result: '"Grey coats? I know the ones. They spend credits like they have too many of them. Never buy food. Always watching the transit lifts."', grants: { codex: ['codex-iron-syndicate'] } },
              { text: 'Buy a bottle of Corellian Reserve.', morality: 0, loyalty: { underworld: 2 }, result: 'She produces a bottle from under the cart without a word. "Sixty credits. Do not tell anyone where you got it."', grants: { items: ['item_brandy'] } },
            ],
          },
          { id: 'bith_rumor_broker', x: 8, y: 5, kind: 'bith', label: 'Korvin — Information Broker', mobile: true,
            repeatPrompt: 'Korvin taps his dome-shaped skull rhythmically. He is processing something. Or composing.',
            prompt: '"Sound carries further than people assume at this altitude. I have excellent hearing. I sell what I hear. Thirty credits per item. Or you tell me something interesting and I reciprocate."',
            choices: [
              { text: 'Pay thirty credits for local intelligence. "What do you know about Bay 14?"', morality: 0, loyalty: {}, result: '"The official story is a dock fire. My ears say otherwise. Fourteen grey coats with Republic clearance codes walked a Phrik shipment out of a sealed dock while the alarms were looped. Two dock hands disappeared that night. One resurfaced on Level 1100. Ask for Rook."', grants: { flags: ['bith_intel_purchased'], codex: ['codex-docking-bay-14'] } },
              { text: 'Offer information in trade. "The Syndicate has observers in the lounge right now."', morality: 0, loyalty: {}, result: '"I know. I told them where to sit for the best sightlines." He pauses. "I work for everyone. That is how I survive. But I will tell you this for free — the one they are watching is not you. Not yet."', grants: { flags: ['bith_exchange_done'] } },
              { text: 'Ask him about Marlo.', morality: -2, loyalty: { underworld: 3 }, result: '"Marlo? Brilliant man. Paranoid man. He once had a business rival\'s ship impounded for six months over a missing cargo seal. The cargo seal was not missing. It was Marlo. He makes patience look like aggression."' },
            ],
          },
          { id: 'lounge_patron', x: 4, y: 7, kind: 'cantina_owner', label: 'High-Society Patron Sevra', mobile: false,
            repeatPrompt: 'Sevra swirls her glass and watches the promenade through the lounge window. She has opinions.',
            prompt: 'She glances at you sideways. "You do not look like someone with a Sky-Market Exchange account. Are you lost, or are you one of those investigators the Senate keeps sending down here to pretend they care?"',
            choices: [
              { text: '"I am looking into a cargo irregularity. Discreetly."', morality: 5, loyalty: {}, result: '"Discreet. From a person who just walked into the most visible bar on Level 1450." She sets down her glass. "The irregularity you mean closed two months ago. Officially. Someone very senior wanted it closed. Everyone on this promenade knows. Nobody says it."', grants: { flags: ['sevra_hinted'] } },
              { text: '"What is the Bay 14 Blend, exactly?"', morality: 0, loyalty: {}, result: '"That is what we call dark humor around here. The barkeep invented it after the dock fire. Named it before the CSF closed the case. He knew the fire story was false. We all did." She finishes her drink. "It tastes like something burning that should not be burning."' },
            ],
          },
          { id: 'senate_aide_promenade', x: 22, y: 13, kind: 'mechanic', label: 'Senate Aide Parvus', mobile: true,
            repeatPrompt: 'Parvus is reviewing a datapad with the focused anxiety of someone who knows exactly how much trouble they are in.',
            prompt: 'He nearly walks into you and flinches back. A Senate aide pin glints on his collar. "I am not here. Officially. If anyone asks, I was at a budget subcommittee hearing all afternoon."',
            choices: [
              { text: '"Who sent you down here?"', morality: 5, loyalty: { republic: 5 }, result: '"Nobody. I came voluntarily. Which is worse, legally." He glances over his shoulder. "The sub-account that cleared the Bay 14 cargo passes through my committee. I did not sign off on it. I do not know who did. I came here to find out and I am starting to wish I had not."', grants: { flags: ['senate_aide_met'], codex: ['codex-docking-bay-14'] } },
              { text: '"If you know something about Bay 14, you should go directly to Officer Vane."', morality: 10, loyalty: { republic: 8 }, result: '"Vane?" He goes pale. "You think Vane is clean? The closure authorization on this case has his name on it. That could mean he was ordered to close it. Or it could mean something worse." He pockets his datapad. "I need to think about this more carefully."', grants: { flags: ['vane_ambiguous_hinted'] } },
              { text: '"Say nothing. Take his datapad ID and walk away."', morality: -5, loyalty: { underworld: 5 }, result: 'He watches you go with the expression of someone calculating whether to report this or add it to the list of things they are pretending not to know.', grants: { flags: ['aide_id_noted'] } },
            ],
          },
          { id: 'garrus_sky', x: 16, y: 18, kind: 'smuggler', label: 'Garrus',
            repeatPrompt: 'Garrus picks at his teeth with a data chip. "You want the shaft location or you are just here to browse?"',
            prompt: 'The Rodian looks up from a spread of geological survey chips on the table. He is wearing three different smuggler vests simultaneously, as if unable to commit to any one identity. "You look like someone who is interested in going somewhere they are definitely not supposed to go."',
            choices: [
              { text: '"What are you selling?"', morality: 0, loyalty: {}, result: '"Bay 14 maintenance shaft. Coordinates to a sealed sub-level that the Republic says does not exist. I found it three weeks ago and I have been too smart to go in alone." He fans out the survey chips. "Fifty credits for the location. You bring what you find and we split it."', grants: { flags: ['garrus_met'] } },
              { text: '"I will take the coordinates."', morality: -5, loyalty: { underworld: 8 }, requires: { credits: 50 }, result: '"Pleasure doing business." He slides over a spent power cell with coordinates scratched into the casing. "Look for the ancient containment seal. Whatever is in there has been sealed for a very long time, which is either a good sign or a very bad one."', grants: { flags: ['garrus_coordinates_sold'], items: ['sublevel_coordinates'] } },
              { text: '"How do you know the CSF has not already found it?"', morality: 5, loyalty: {}, result: '"Because I found the last CSF inspection log. Eleven years old. Signed by someone named Vane. The report says: nothing of interest. Either Vane is blind or Vane is lying." He shrugs. "In my experience, CSF officers are rarely blind."', grants: { flags: ['garrus_vane_clue_shared', 'vane_sublevel_suspected'] } },
            ],
          },
          { id: 'madame_vex_sky', x: 10, y: 6, kind: 'cantina_owner', label: 'Madame Vex',
            repeatPrompt: 'Madame Vex shuffles a Sabacc deck without looking at her hands. The cards move like they are afraid of her.',
            prompt: `The Twi'lek dealer glances up from her table with the unhurried confidence of someone who has decided the outcome before the cards are dealt. "I see a new face. New faces mean new money or new complications. Sit down and we will find out which."`,
            choices: [
              { text: '"I am here for a Sabacc game."', morality: 0, loyalty: { underworld: 5 }, result: '"Everyone is here for a Sabacc game. Sit. The buy-in is two hundred credits and your reputation. Lose the first and I keep the second."', grants: { flags: ['vex_sabacc_offered'] } },
              { text: '"High-stakes. I want the real table."', morality: -5, loyalty: { underworld: 10 }, result: '"The real table has a different buy-in." She sets down the deck. "You are going to play a standard hand in the open, win convincingly, and not gloat when you do it. Then I will take you downstairs." She returns to her shuffle. "Assuming you can win convincingly."', grants: { flags: ['vex_high_table_offered'] } },
              { text: '"What do you know about the freight hub night schedules?"', morality: 0, loyalty: { underworld: 5 }, result: 'She deals three cards face-up without being asked. "I know everything that moves through the Sky-Market and below it. Information has a price here, same as everything else. Win a hand first. Then we can discuss what you actually came here for."', grants: { flags: ['vex_intel_offered'] } },
            ],
            phases: [
              {
                id: 'phase_initial',
                requiresAllFlags: [],
                requiresNoneFlags: ['sabacc_won'],
                prompt: `The Twi'lek dealer glances up from her table with the unhurried confidence of someone who has decided the outcome before the cards are dealt. "I see a new face. New faces mean new money or new complications. Sit down and we will find out which."`,
                choices: [
                  { text: '"I am here for a Sabacc game."', morality: 0, loyalty: { underworld: 5 }, result: '"Everyone is here for a Sabacc game. Sit. The buy-in is two hundred credits and your reputation. Lose the first and I keep the second."', grants: { flags: ['vex_sabacc_offered'] } },
                  { text: '"High-stakes. I want the real table."', morality: -5, loyalty: { underworld: 10 }, result: '"The real table has a different buy-in." She sets down the deck. "You are going to play a standard hand in the open, win convincingly, and not gloat when you do it. Then I will take you downstairs." She returns to her shuffle. "Assuming you can win convincingly."', grants: { flags: ['vex_high_table_offered'] } },
                  { text: '"What do you know about the freight hub night schedules?"', morality: 0, loyalty: { underworld: 5 }, result: 'She deals three cards face-up without being asked. "I know everything that moves through the Sky-Market and below it. Information has a price here, same as everything else. Win a hand first. Then we can discuss what you actually came here for."', grants: { flags: ['vex_intel_offered'] } },
                ],
              },
              {
                id: 'phase_sabacc_ready',
                requiresAllFlags: ['vex_sabacc_offered'],
                requiresNoneFlags: ['sabacc_won', 'sabacc_played'],
                prompt: '"The table is ready. Two hundred credits. You fold when your hand goes negative, you walk with nothing. You hit Pure Sabacc, you walk with the whole pot and my card." She slides a chip to the center. "Your move."',
                choices: [
                  { text: '"Deal."', morality: 0, loyalty: { underworld: 5 }, result: 'The cards move faster than they should. Three hands. Two hours. In the end you are reading her better than she intended, and she knows it. She sets down a keycard without drama. "The Bay 14 storage corridor. Third alcove on the left. Someone was storing things there that did not want to be found." She is already shuffling for the next hand.', grants: { flags: ['sabacc_won', 'sabacc_played'], items: ['smugglers_keycard'] } },
                  { text: '"Not tonight."', morality: 5, loyalty: {}, result: '"Come back when you are ready. The table will be here."', grants: {} },
                ],
              },
              {
                id: 'phase_won',
                requiresAllFlags: ['sabacc_won'],
                prompt: '"You played well. I do not say that to everyone." She refills her glass. "The keycard opens Bay 14 corridor three. Kaelen Voss stored something there before his arrest. He asked me to keep the access secure. He did not ask me to keep it forever. Consider this a transfer of obligation."',
                choices: [
                  { text: '"What is Kaelen Voss carrying?"', morality: 5, loyalty: {}, result: '"Financial records. The kind that end careers when they reach the right hands, or fund retirement when they reach the wrong ones. He had not decided which to do with them. That indecision is what got him arrested." A beat. "Or someone made sure he could not decide."', grants: { flags: ['syndicate_ledger_context_known'] } },
                  { text: '"Thank you, Madame."', morality: 5, loyalty: { underworld: 5 }, result: '"Thank me by not making me regret this." She turns back to her cards. "Kaelen was one of the few people in Sector 4 with a functioning conscience. If that costs something, make it cost something worth the price."', grants: {} },
                ],
              },
            ],
          },
        ],
        collectibles: [{ id: 'sky_market_datachip', x: 14, y: 19, label: 'Sliced Comm Fragment', reward: 40 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 36, 24, 'floor');
          carveRect(g, 1, 3, 12, 11, 'wall');
          carveRect(g, 2, 4, 11, 10, 'floor');
          carveRect(g, 26, 3, 36, 11, 'wall');
          carveRect(g, 27, 4, 35, 10, 'floor');
          carveRect(g, 34, 18, 36, 22, 'wall');
          pt(g, 35, 19, 'floor'); pt(g, 35, 20, 'floor'); pt(g, 35, 21, 'floor'); pt(g, 34, 20, 'floor');
          pt(g, 12, 7, 'floor');
          pt(g, 26, 7, 'floor');
          carveRect(g, 3, 21, 33, 21, 'wall');
          pt(g, 14, 21, 'floor'); pt(g, 22, 21, 'floor');
          pt(g, 0, 12, 'door'); pt(g, 0, 13, 'door');
          pt(g, 15, 25, 'door');
          pt(g, 36, 0, 'door');
          pt(g, 5, 0, 'door');
          return g;
        },
      },
      sky_customs: {
        id: 'sky_customs', name: 'Skyway Customs & Concourse Dock', subtitle: 'Coruscant · L.1450 — Entry Gate',
        width: 36, height: 24, spawnPos: { x: 4, y: 12 }, textureId: 'coruscant',
        accent: '#00C4D4', accentGlow: 'rgba(0,196,212,0.25)', accentDim: '#006070',
        floorColor: '#1A1E24', floorAlt: '#222830', wallDark: '#0A0C12', wallLight: '#141C24',
        bg: 'radial-gradient(circle at 50% 30%, #101828 0%, #08090E 70%)', ambient: 'traffic',
        decor: ['pillar', 'archive', 'neon_sign'],
        doors: [
          { x: 35, y: 12, targetZone: 'sky_market', targetPos: { x: 1, y: 12 }, label: 'Sky-Market Promenade' },
          { x: 35, y: 13, targetZone: 'sky_market', targetPos: { x: 1, y: 13 }, label: 'Sky-Market Promenade' },
        ],
        worldObjects: [
          { id: 'airtaxi_sky_customs', propArt: 'airtaxi', propVariant: 'clean', x: 2, y: 12, once: false, label: 'AirTaxi Terminal', description: 'Coruscant AirTaxi Network terminal. Level 1450 Customs Concourse departure point.' },
          { id: 'customs_scanner_archway', propArt: 'scanner_arch', propVariant: 'customs', x: 12, y: 7, once: false, label: 'Cargo Scanner Archway', description: 'A Republic-standard cargo scan archway. The readout shows the last twelve items processed. Eleven of them are flagged with a yellow query. None have been followed up. The twelfth is flagged red: PHRIK ALLOY TRACE. The flag was manually cleared three months ago. The operator ID on the clearance is: REDACTED.' },
          { id: 'manifest_kiosk', propArt: 'kiosk', propVariant: 'manifest', x: 18, y: 12, once: true, label: 'Cargo Manifest Registry', description: 'A public cargo manifest terminal. Searching "Scylla Freight" returns zero results. Searching the Bay 14 berth number returns a single entry: CASE CLOSED. RECORDS PURGED. AUTHORIZATION: SENATE DIRECTIVE 1182-C. The terminal\'s own activity log shows the purge command was issued from this terminal, from this building, at the same time the dock fire was reported.', grantsFlag: 'customs_manifest_checked' },
          { id: 'caf_stand', propArt: 'vendor_stall', propVariant: 'caf', x: 6, y: 6, once: false, label: 'Caf Stand — Northwest Concourse', description: 'A battered chrome caf dispenser and a fold-out table. A hand-lettered sign reads: BEST CAF BETWEEN THE LEVELS. A smaller sign below it reads: ONLY CAF BETWEEN THE LEVELS. The proprietor is a tired-looking Duros who refills your cup without being asked and does not charge you for it. "New faces are good for business," he says. "Old faces are bad for my nerves."' },
          { id: 'detention_alcove', propArt: 'booth', propVariant: 'detention', x: 6, y: 17, once: false, label: 'CSF Detention Alcove', description: 'A small holding area with two retention rings and a broken binder lock. The cell log shows fourteen detentions in the past six months. Thirteen were released within four hours. The fourteenth — listed only as GREY COAT, NO ID — was transferred off-site per Senate Directive 1182-C. No destination logged. The transfer was authorized by the same officer who filed the Bay 14 closure.' },
          { id: 'flight_control_booth', propArt: 'booth', propVariant: 'flight', x: 28, y: 7, once: true, label: 'Flight Control Booth', description: 'The customs concourse flight control station. The arrival log for the night of the Bay 14 incident shows a twelve-minute gap in the record — all arrivals logged, then nothing, then resuming as if the gap does not exist. The station officer filed a "technical malfunction" report for those twelve minutes. The report was accepted without inquiry.', grantsFlag: 'flight_gap_found' },
          { id: 'siphon_terminal', propArt: 'console', propVariant: 'siphon', x: 22, y: 7, once: true, label: 'Cargo Feed Splice Terminal', description: 'A secondary manifest feed terminal. The uplink is live. Splicing into the customs broadcast frequency would expose every clearance override logged in the past six months. The security lock runs on a frequency-keyed cipher.', triggersMinigame: 'signal_siphon', grantsFlag: 'customs_manifest_decrypted', grantsCodex: 'codex-sector-4-freight-corridors' },
        ],
        npcs: [
          { id: 'csf_customs_officer', x: 16, y: 12, kind: 'republic_guard', label: 'CSF Customs Officer Rael',
            prompt: '"This is a controlled transit point. All cargo entering the Sky-Market District must be logged and scanned. Present your manifest or step aside."',
            repeatPrompt: 'Officer Rael watches the concourse with the patience of someone who has been watching concourses for a very long time.',
            choices: [
              { text: '"I am CSF Auxiliary. I need access to the manifest registry."', morality: 8, loyalty: { republic: 8 }, requires: { item: 'csf_aux_pass' }, result: '"Auxiliary clearance noted. The registry terminal is at the center concourse. I would warn you that certain records have been — expunged. Senate directive. I am not permitted to say more." He steps aside.', grants: { flags: ['rael_cooperated'] } },
              { text: '"What happened to the Bay 14 cargo records?"', morality: 5, loyalty: { republic: 5 }, result: '"I am not authorized to discuss active or closed Senate-directed case files. If you have a formal inquiry, file it with the Port Authority. Processing time is six to eight standard weeks." He does not make eye contact when he says it.', grants: { flags: ['rael_deflected'] } },
              { text: '"I am looking for someone who was transferred out of your detention alcove."', morality: 3, loyalty: {}, result: '"Transfers off-site are handled by a dedicated Senate liaison. I do not have access to those records, and I would advise you not to press on that topic in this building." He pauses. "I am saying that as a courtesy."', grants: { flags: ['rael_warned'] } },
            ],
          },
          { id: 'ast4_security_droid', x: 28, y: 16, kind: 'droid', label: 'AST-4 Security Droid', mobile: true,
            prompt: '"SCANNING. CLEARANCE LEVEL: insufficient for restricted concourse areas. RECOMMEND: proceed to public manifest kiosk. THREAT ASSESSMENT: pending."',
            repeatPrompt: 'AST-4 tracks your movement with its photoreceptor array. The threat assessment percentage has gone up.',
            choices: [
              { text: '"Run diagnostic. Who last accessed the flight control booth?"', morality: 0, loyalty: {}, result: '"DIAGNOSTIC REQUEST: access log for Flight Control Booth, past ninety days. RESULT: twenty-two authorized entries. ANOMALY: one entry, one hundred and twelve days ago, logged under a Senate clearance tier that does not correspond to any known customs authority level. CLASSIFICATION: above my pay grade." It resumes scanning.' },
              { text: '"Stand down. Auxiliary clearance."', morality: 3, loyalty: { republic: 5 }, requires: { item: 'csf_aux_pass' }, result: '"CLEARANCE VERIFIED: Auxiliary Corps provisional. THREAT ASSESSMENT: revised to: unlikely. RECOMMENDATION: try not to do anything that changes that assessment." It pivots away.' },
            ],
          },
        ],
        collectibles: [{ id: 'customs_chip', x: 30, y: 17, label: 'Dropped Clearance Chip', reward: 25 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 34, 22, 'floor');
          carveRect(g, 1, 3, 10, 10, 'wall');
          carveRect(g, 2, 4, 9, 9, 'floor');
          pt(g, 10, 7, 'floor');
          carveRect(g, 1, 13, 10, 20, 'wall');
          carveRect(g, 2, 14, 9, 19, 'floor');
          pt(g, 10, 16, 'floor');
          carveRect(g, 22, 3, 34, 10, 'wall');
          carveRect(g, 23, 4, 33, 9, 'floor');
          pt(g, 22, 7, 'floor');
          carveRect(g, 22, 13, 34, 20, 'wall');
          carveRect(g, 23, 14, 33, 19, 'floor');
          pt(g, 22, 16, 'floor');
          pt(g, 35, 12, 'door'); pt(g, 35, 13, 'door');
          return g;
        },
      },
      heat_sink_slums: {
        id: 'heat_sink_slums', name: 'Heat Sink Slums & Cantina Alley', subtitle: 'Coruscant · L.1450 — Residential Underbelly',
        width: 36, height: 28, spawnPos: { x: 15, y: 2 }, textureId: 'coruscant',
        accent: '#FF5520', accentGlow: 'rgba(255,85,32,0.28)', accentDim: '#802010',
        floorColor: '#1A1008', floorAlt: '#261606', wallDark: '#0A0602', wallLight: '#160C06',
        bg: 'radial-gradient(circle at 50% 90%, #2A1008 0%, #160804 30%, #080402 75%, #050202 100%)',
        ambient: 'embers', decor: ['pipe', 'neon_sign', 'brazier', 'cargo_crate'],
        doors: [
          { x: 15, y: 0, targetZone: 'sky_market', targetPos: { x: 15, y: 24 }, label: 'Sky-Market Promenade' },
          { x: 16, y: 0, targetZone: 'sky_market', targetPos: { x: 16, y: 24 }, label: 'Sky-Market Promenade' },
          { x: 20, y: 27, targetZone: 'catwalk_underdeck', targetPos: { x: 20, y: 1 }, label: 'Catwalk Underdeck' },
          { x: 21, y: 27, targetZone: 'catwalk_underdeck', targetPos: { x: 21, y: 1 }, label: 'Catwalk Underdeck' },
        ],
        worldObjects: [
          { id: 'airtaxi_heat_sink', propArt: 'airtaxi', propVariant: 'cracked', x: 16, y: 20, once: false, label: 'AirTaxi Terminal', description: 'A battered AirTaxi terminal mounted to the underdeck support strut. The casing is cracked and the screen flickers, but it works. Barely.' },
          { id: 'cantina_entrance_sign', propArt: 'sign_array', propVariant: 'exhaust', x: 8, y: 4, once: false, label: 'Cantina Sign — The Exhaust', description: 'A hand-painted sign above the cantina entrance reads THE EXHAUST in faded Aurebesh. Below, someone has added in smaller lettering: "Est. after the last time this level flooded." The door is open. The smell of grilled protein and something spiced with too much heat wafts out.' },
          { id: 'cantina_gorg_spit', propArt: 'gorg_spit', x: 5, y: 8, once: false, label: 'Gorg Spit — The Exhaust', description: 'A rotating gorg spit occupies the corner of the cantina near the bar. Whatever the gorg ate before it became the gorg, it was clearly living its best life. The fat drips and sizzles. The cantina keep claims the spit has not been turned off in three years. Nobody has contradicted this.' },
          { id: 'sabacc_table', propArt: 'sabacc_table', x: 10, y: 10, once: true, label: 'Sabacc Table — Back Corner', description: 'Four players, a mixed pile of credits and vouchers, and a fifth person watching from behind a column. The watcher is not playing. The watcher is counting cards. Nobody at the table has noticed. You have.', grantsFlag: 'sabacc_observer_seen' },
          { id: 'hab_capsule_stack', propArt: 'hab_block', x: 28, y: 8, once: false, label: 'Hab Capsule Block', description: 'Forty-eight stacked sleep capsules in a converted freight container, each one a meter and a half of foam mat and a ventilation slot. The occupancy board shows thirty-nine occupied. Rate: two credits per sleep cycle. The manager\'s station is empty — the manager lives in capsule forty-seven.' },
          { id: 'exhaust_radiator_vent', propArt: 'radiator', x: 32, y: 20, once: false, label: 'Thermal Exhaust Radiator', description: 'A massive heat-sink radiator panel mounted to the outer wall, channeling heat from the manufacturing zones below upward through the residential level. On cold cycles it is a gathering point — a dozen residents crouch near the vents for warmth. The panel is rated for industrial output. The residential zone it is venting through is not.' },
          { id: 'informant_booth', propArt: 'booth', propVariant: 'curtain', x: 20, y: 18, once: true, label: 'Curtained Booth', description: 'A makeshift privacy booth constructed from cargo curtain and wire frame. A datapad left on the table inside displays a partial credit transfer log — the same shell corporation chain visible on the lounge datapad upstairs, traced one step further to an account registered under the Senate District financial authority. Someone has been following the same thread you have.', grantsFlag: 'informant_found' },
        ],
        npcs: [
          { id: 'reelo_informant', x: 22, y: 18, kind: 'broker', label: 'Reelo — Street Broker',
            prompt: '"I do not know you. I do not do business with people I do not know. Unless—" he glances at the booth behind him, "—you are the one who has been following the credit trail. In that case, we have something to discuss."',
            repeatPrompt: 'Reelo keeps one hand under the table. He has not decided whether he trusts you yet.',
            choices: [
              { text: '"I found the same shell accounts. Bay 14. Senate financial authority."', morality: 0, loyalty: { underworld: 8 }, result: '"Then you are either very smart or very stupid for following it this far. The account connects to a sub-committee that does not appear in the public Senate directory. Three people know it exists. Two of them work for the Iron Syndicate." He pauses. "I am not one of those two."', grants: { flags: ['reelo_talked', 'senate_subcommittee_named'] } },
              { text: '"Who has been sitting in this booth?"', morality: 5, loyalty: {}, result: '"Someone who asks the right questions and leaves before they can be asked back. Like you should be doing right now." He nods at the door.' },
              { text: '"What do you know about the Catwalk Underdeck?"', morality: -3, loyalty: { underworld: 5 }, result: '"Maintenance access for the levels below. The Syndicate uses it as a route to move cargo without hitting the transit checkpoints. If you go down there, go armed and go quiet."', grants: { flags: ['underdeck_warned'] } },
            ],
          },
          { id: 'cantina_keep_mirra', x: 8, y: 7, kind: 'cantina_owner', label: 'Mirra — Cantina Keep',
            prompt: '"Food is hot. Caf is strong. The sabacc table in the back is none of my business and has never existed. What do you want?"',
            repeatPrompt: 'Mirra refills glasses without being asked and forgets faces on purpose.',
            choices: [
              { text: '"What do you know about the Iron Syndicate on this level?"', morality: -5, loyalty: { underworld: 5 }, result: '"They do not drink here. They do not eat here. They sit in the hab block and watch the transit corridors. I know this because my cantina is on the transit corridor and they are not subtle." She wipes the bar. "I stopped knowing things a month ago. It is healthier."', grants: { flags: ['mirra_hinted'] } },
              { text: '"Is there a way down to the Catwalk Underdeck from here?"', morality: 0, loyalty: {}, result: '"South corridor, past the exhaust radiator. Maintenance hatch. You did not hear it from me." She moves to the other end of the bar.', grants: { flags: ['underdeck_access_known'] } },
            ],
          },
        ],
        collectibles: [{ id: 'slum_cred_chip', x: 14, y: 24, label: 'Stashed Cred Chip', reward: 35 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 34, 26, 'floor');
          carveRect(g, 1, 3, 14, 14, 'wall');
          carveRect(g, 2, 4, 13, 13, 'floor');
          pt(g, 14, 8, 'floor');
          carveRect(g, 18, 3, 34, 14, 'wall');
          carveRect(g, 19, 4, 33, 13, 'floor');
          pt(g, 18, 8, 'floor');
          pt(g, 15, 0, 'door'); pt(g, 16, 0, 'door');
          pt(g, 20, 27, 'door'); pt(g, 21, 27, 'door');
          return g;
        },
      },
      catwalk_underdeck: {
        id: 'catwalk_underdeck', name: 'Vent Sector 14-Sub — Catwalk Underdeck', subtitle: 'Coruscant · L.1450 — Industrial Substructure',
        width: 40, height: 22, spawnPos: { x: 20, y: 1 }, textureId: 'coruscant',
        accent: '#C87800', accentGlow: 'rgba(200,120,0,0.22)', accentDim: '#604000',
        floorColor: '#0E0C08', floorAlt: '#161208', wallDark: '#060402', wallLight: '#0E0A06',
        bg: 'radial-gradient(circle at 50% 100%, #180C00 0%, #0C0800 40%, #050402 80%, #030202 100%)',
        ambient: 'embers', decor: ['pipe', 'girder', 'slag'],
        doors: [
          { x: 20, y: 0, targetZone: 'heat_sink_slums', targetPos: { x: 20, y: 26 }, label: 'Heat Sink Slums' },
          { x: 21, y: 0, targetZone: 'heat_sink_slums', targetPos: { x: 21, y: 26 }, label: 'Heat Sink Slums' },
        ],
        worldObjects: [
          { id: 'airtaxi_underdeck', propArt: 'airtaxi', propVariant: 'dark', x: 2, y: 11, once: false, label: 'AirTaxi Terminal', description: 'A stripped-down AirTaxi terminal bolted to the underdeck strut framework. The screen is dark but the interface responds. Someone installed this without filing a permit.' },
          { id: 'security_junction_box', propArt: 'junction_box', propVariant: 'csf', x: 22, y: 6, once: true, label: 'CSF Patrol Routing Junction', description: 'A Republic-standard security routing node controlling patrol droid movements across the lower freight terraces. The firmware is three cycles out of date. The patrol schedule loaded into memory routes all droids away from Corridor 14-Sub between the third and fifth hour of the sleep cycle — a twelve-minute window, recurring. The window matches the Bay 14 incident timeline exactly.', grantsFlag: 'patrol_window_found', grantsCodex: 'codex-csf-protocol' },
          { id: 'hydraulic_damper', propArt: 'damper_array', x: 6, y: 6, once: false, label: 'Hydraulic Damper Array', description: 'The underdeck is held together by a series of hydraulic dampers that absorb the structural vibration from the freight terraces above. Each damper is stamped with a maintenance date. The most recent stamp is eight months old. The recommended interval is thirty days. The noise from above — a constant low throb — makes more sense now.' },
          { id: 'conduit_tap_node', propArt: 'power_tap', x: 32, y: 14, once: true, label: 'Unauthorized Power Tap Node', description: 'A jury-rigged power tap drilled directly into the municipal conduit line and drawing a continuous bleed of current to somewhere below. The cable runs down through a floor grate and disappears. The draw is small enough to stay below automated monitoring thresholds. Someone who knew exactly how the monitoring worked installed this.', grantsFlag: 'power_tap_found' },
          { id: 'drop_shaft_view', propArt: 'drop_shaft', x: 7, y: 17, once: false, label: 'Vertical Drop Shaft', description: 'An open maintenance shaft drops straight down from the underdeck floor into the levels below. No safety railing. A flickering work light thirty meters down illuminates a narrow platform, and below that — nothing visible. The shaft connects to the Level 1222 ventilation spine. If you could get down there without falling, you could move between levels without touching a single transit checkpoint.' },
          { id: 'syndicate_marker', propArt: 'wall_marks', propVariant: 'stamp', x: 30, y: 18, once: true, label: 'Iron Syndicate Transit Marker', description: 'A small iron chain emblem, stamp-pressed into the conduit housing — the Iron Syndicate\'s territorial marker. This route belongs to them. The stamp is recent: the metal around the impression is still bright, not yet oxidized. They have been using this underdeck as a cargo transit route within the last few days.', grantsFlag: 'syndicate_route_found' },
        ],
        npcs: [
          { id: 'maintenance_droid_14sub', x: 20, y: 14, kind: 'droid', label: 'Maintenance Droid M-7', mobile: true,
            prompt: '"UNIT M-7. ASSIGNED: underdeck thermal monitoring, Vent Sector 14-Sub. CURRENT STATUS: thermal readings nominal. ANOMALY LOG: seventeen unauthorized access events in the past month. REPORTING STATUS: none filed. Reporting terminal offline for seven months."',
            repeatPrompt: 'M-7 resumes its patrol route, logging thermal readings that nobody will read.',
            choices: [
              { text: '"Who has been accessing this sector without authorization?"', morality: 0, loyalty: {}, result: '"ACCESS LOG: seventeen events. Biometric match: none. Cargo mass estimate per event: substantial. Route: north entry, south conduit access, repeat. ASSESSMENT: organized. CONCERN LEVEL: high. FILED REPORTS: zero. Reporting terminal offline."', grants: { flags: ['underdeck_traffic_logged'] } },
              { text: '"Are you connected to the CSF patrol routing system?"', morality: 5, loyalty: { republic: 5 }, result: '"CONNECTED: yes, passive monitoring only. LAST PATROL UNIT IN THIS SECTOR: forty-three days ago. PATROL SCHEDULE: modified. REASON FOR MODIFICATION: system-level override, authorization unknown. CONCERN LEVEL: very high. Filed reports: zero. Reporting terminal offline."', grants: { flags: ['patrol_gap_confirmed'] } },
            ],
          },
        ],
        collectibles: [{ id: 'catwalk_cred_chip', x: 36, y: 10, label: 'Dropped Maintenance Chit', reward: 45 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 38, 20, 'floor');
          carveRect(g, 15, 2, 25, 10, 'wall');
          carveRect(g, 16, 3, 24, 9, 'floor');
          pt(g, 15, 6, 'floor');
          pt(g, 25, 6, 'floor');
          carveRect(g, 2, 13, 12, 20, 'wall');
          carveRect(g, 3, 14, 11, 19, 'floor');
          pt(g, 7, 13, 'floor');
          carveRect(g, 28, 12, 28, 18, 'lava');
          pt(g, 20, 0, 'door'); pt(g, 21, 0, 'door');
          return g;
        },
      },
      freight_hub: {
        id: 'freight_hub', name: 'Sector 4 Freight Hub', subtitle: 'Coruscant · Industrial Mid-Levels · Sector 4',
        width: 40, height: 28, spawnPos: { x: 2, y: 14 }, textureId: 'coruscant',
        accent: '#FF8C42', accentGlow: 'rgba(255,140,66,0.18)', accentDim: '#7A3A10',
        floorColor: '#1C1A14', floorAlt: '#24221A', wallDark: '#0A0902', wallLight: '#161408',
        bg: 'radial-gradient(circle at 30% 70%, #181410 0%, #08070A 70%)', ambient: 'steam', floorPattern: 'grate',
        decor: ['cargo_crate', 'pipe', 'girder', 'warning_beacon', 'slag'],
        doors: [
          { x: 20, y: 27, targetZone: 'mag_rail_corridor', targetPos: { x: 20, y: 1 }, label: 'Mag-Rail Corridor' },
          { x: 21, y: 27, targetZone: 'mag_rail_corridor', targetPos: { x: 21, y: 1 }, label: 'Mag-Rail Corridor' },
        ],
        worldObjects: [
          { id: 'bay14_crime_scene', propArt: 'scorch_wall', x: 20, y: 6, once: true, iconKind: 'evidence', label: 'Bay 14 Blast Marks', description: 'Scorch marks on the dock wall. The pattern is from shaped charges, not a fuel fire.',
            grantsItem: 'stolen_manifest',
            autoCodex: { id: 'discovery-bay14-blast-marks', title: 'Bay 14: Blast Evidence', category: 'discoveries', summary: 'Shaped charges destroyed the loading manifest records.', body: ['The dock wall is scorched. Not from a fuel fire — the scorch pattern is from shaped charges placed against the loading manifest kiosk. Someone destroyed the primary records on the way out. Whoever did this knew exactly which terminal held the cargo authorization log. One cargo disk survives in the debris — partially melted, but the freight entries are still readable.'] } },
          { id: 'discarded_keycard', propArt: 'datapad_table', propVariant: 'card', x: 32, y: 18, once: true, label: 'Discarded Passcode', description: 'Half-melted but readable: an underworld bypass key. Grants sub-level transit without checkpoint flags. You pocket it.', grantsItem: 'scrambler_keycard' },
          { id: 'shipping_crate_b14', propArt: 'crate_stack', propVariant: 'agri', x: 10, y: 20, once: true, label: 'Unsealed Shipping Crate', description: 'Marked as "agricultural supplies." Contains Glitterstim vials and unregistered blaster power packs. Clearly staged to be found.', grantsItem: 'item_spice_vial' },
          { id: 'customs_terminal_088', propArt: 'console', propVariant: 'manifest088', x: 26, y: 4, once: true, label: 'Customs Manifest Registry', description: 'Three containers marked with Senate sub-committee routing stamps. One flags as anomalous — destination redacted, shipper redacted, authorization code valid. The code traces to a sub-committee that officially does not exist.' },
          { id: 'crane_node_088', propArt: 'console', propVariant: 'crane', x: 22, y: 4, once: true, label: 'Crane Automation Node', description: 'The bay exterior crane control system. A code input here can drop a heavy repulsor-crate onto the loading yard — opening a breach point into the warehouse without triggering external alarms.' },
          { id: 'undercity_radio_terminal', propArt: 'console', propVariant: 'radio', x: 4, y: 4, once: false, label: 'Under-Grit Radio Intercept', description: '[Signal 104.9 Sub-Grit — Unauthorized] "They are calling Docking Bay 14 a logistical delay while Black Sun heavy gunners run it like a private toll booth. CSF sent fresh academy blood into Sector 4. Place your bets at Vond\'s shop — three to one the new badge sells out before end of shift..."' },
          { id: 'airtaxi_freight_hub', propArt: 'airtaxi', propVariant: 'worn', x: 38, y: 6, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal', description: 'Transit terminal. Level access pending clearance.' },
          { id: 'bay14_storage_alcove', propArt: 'storage_alcoves', x: 14, y: 18, once: true, iconKind: 'datapad', label: 'Restricted Storage Alcove',
            description: 'Third alcove on the left, just as Vex described. The storage container is sealed under Syndicate lock. Inside: a data cylinder wrapped in lead foil. The foil is standard anti-scan practice. Whatever is on this cylinder, someone spent money keeping it undetected.',
            grantsItem: 'syndicate_ledger',
            grantsFlag: 'syndicate_ledger_found',
            grantsCodex: 'codex-syndicate-ledger',
            requires: { item: 'smugglers_keycard' } },
          { id: 'bay14_weapon_cache', propArt: 'crate_stack', propVariant: 'weapons', x: 32, y: 8, once: true, iconKind: 'crate', label: 'Weapons Shipment Cache',
            requiresFlag: 'inheritance_active',
            lockedMessage: 'The cache is sealed under CSF evidence tape. Jon has not given you the go ahead, and touching it now would be a job for the Precinct, not for you.',
            description: 'Thirty-two sealed crates marked AGRICULTURAL EQUIPMENT. The cargo manifest tucked beneath the top lid tells a different story: Merr-Sonn G-8 blasters, thermal detonators, Phrik plating strips. Jon wants this delivered to a contact at Level 1312. The CSF inventory counted boxes. They did not open them.',
            grantsItem: 'bay14_weapon_manifest',
            grantsFlag: 'weapons_manifest_acquired',
            grantsCodex: 'codex-jon-inheritance' },
          { id: 'bay14_raid_breach', propArt: 'security_gate', x: 27, y: 9, once: true, iconKind: 'door', label: 'Bay 14 Sealed Cargo Door',
            requiresFlag: 'bay14_reported',
            lockedMessage: 'The sealed door beyond the blast marks carries a CSF evidence tag. No breach is authorized until the Bay 14 findings are filed with Vane at the Academy.',
            description: "A sealed cargo door at the back of the hub, scored by the old blast and welded shut from the inside. The CSF breach team stacks up behind you, Vane's authorization glowing on your comlink. When the charge goes, the door falls inward and the Iron Syndicate's staging crew is already on its feet.",
            triggersMinigame: 'tactical_combat',
            minigameConfig: { opponentProfile: 'syndicate_thug', flavorText: 'The charge blows and the door comes down into a haze of dust and phrik shavings. A Syndicate staging crew is waiting, weapons half raised. Bay 14 ends tonight.' },
            grantsFlags: ['sector4_raid_complete'] },
        ],
        npcs: [
          { id: 'ph_merchant_quill', x: 20, y: 14, kind: 'smuggler', label: 'Dara Quill, Holdings Modeling',
            isVendor: true, vendorFaction: 'underworld', repeatable: true, requiresFlag: 'inheritance_active',
            vendorStock: ['ph_pkg_ops'],
            vendorGreeting: "\"You own more than you know. I build the model that shows you. Operations Annex: a holdings table and a console that runs the numbers. Buy it, and the money starts talking.\"",
            repeatPrompt: "Quill counts crates with her lips moving, then writes a number on her wrist.",
            prompt: "\"You own more than you know. I build the model that shows you. Operations Annex: a holdings table and a console that runs the numbers. Buy it, and the money starts talking.\"",
            choices: [] },
          { id: 'jax_freight', x: 18, y: 6, kind: 'mechanic', label: 'Dock Engineer Jax',
            repeatPrompt: 'Jax is running diagnostics on a loading claw. He does not acknowledge you.',
            prompt: '"Whatever you are here for, I did not see anything. Go find someone else."',
            choices: [
              { text: '"The CSF sent me. Here is my Auxiliary Pass."', morality: 5, loyalty: { republic: 8 }, requires: { item: 'csf_aux_pass' }, result: 'He glances at the pass and exhales. "Fine. I was in the maintenance shaft when they came through. Fourteen of them. Grey coats. Republic security codes that checked out clean. They loaded the Phrik onto two unmarked lifters and went sub-level."', grants: { flags: ['jax_talked', 'jaxxon_arrested', 'freight_hub_investigated'], codex: ['codex-docking-bay-14'] } },
              { text: 'Offer him 200 credits and ask what he saw.', morality: -8, loyalty: { underworld: 8 }, result: 'He pockets the credits without counting them. "Two lifters. Unmarked. Grey coats with Republic codes. Went down. That is all I am giving you."', grants: { flags: ['jax_bribed', 'jaxxon_deal', 'freight_hub_investigated'] } },
              { text: '"I know you were in the shaft. Tell me what you saw or I will tell them you were."', morality: -15, loyalty: { underworld: 12 }, result: 'His jaw tightens. "You are going to fit right in around here." He tells you what he saw.', grants: { flags: ['jax_intimidated', 'freight_hub_investigated'] } },
            ],
          },
          { id: 'kaelen_freight', x: 6, y: 22, kind: 'swoop_gang', label: 'Kaelen "Breaker" Voss',
            repeatPrompt: 'Kaelen is tinkering with his swoop\'s repulsor coils.',
            prompt: '"Your business does not belong down here. Mine does. Those are different things."',
            choices: [
              { text: '"You were hired as a distraction during the Bay 14 raid. I am not here to arrest you."', morality: 0, loyalty: { underworld: 6 }, result: '"You are smarter than you look. Yeah, someone paid us to race through the bay and make noise while they loaded up. Paid well. Did not ask questions."', grants: { flags: ['kaelen_talked'] } },
              { text: 'Challenge him to a race for the information.', morality: 5, loyalty: { underworld: 10 }, result: '"Ha. I like you." He kicks the speeder to life and points at the service corridor. Three minutes through maintenance shafts, around a plasma conduit junction, and past a security gate you both should not have fit through. You beat him by six meters. He catches his breath, then hands over a data chip. "Grey coats. Republic codes. They paid us to run hot through the bay and keep the dock workers watching the wrong direction. Never asked their names. That is the whole job." The information is legitimate — an eyewitness account of the distraction that covered the Phrik transfer.', grants: { flags: ['kaelen_raced', 'freight_hub_investigated'] } },
            ],
          },
          { id: 'corin_088', x: 10, y: 18, kind: 'mechanic', label: '"Fixer" Corin',
            repeatPrompt: 'Corin is calibrating a blaster power cell. He does not pause when you enter.',
            prompt: '"You wear that CSF armor like it is supposed to mean something down here. I used to wear the Navy crest. Know what it bought me? A pink slip and a tin eye."',
            choices: [
              { text: '"The Navy leaves a lot of good people behind. I am just trying to keep people safe down here."', morality: 5, loyalty: {}, result: '"Safe? In Sector 4? (He sets down his hydrospanner.) You are either brand new or completely soft. If you need your stun-output upgraded, I will not charge top credit."', grants: { flags: ['corin_friendly'] } },
              { text: '"I need access to restricted Navy frequency relays."', morality: -5, loyalty: { underworld: 8 }, result: '"Now that is dangerous. Two hundred credits and your word you did not hear it from me."', grants: { flags: ['corin_relay_unlocked'] } },
            ],
          },
          { id: 'dax_shipping', x: 10, y: 6, kind: 'mechanic', label: 'Dax — Shipping Clerk',
            repeatPrompt: 'Dax is checking cargo seals with excessive attention to detail.',
            prompt: 'He glances past you before speaking. "I have been waiting for someone to come asking. I cannot keep filing phantom manifests. If they find out I talked to you, I am dead."',
            choices: [
              { text: '"You are protected under Republic witness protocols. Talk to me."', morality: 8, loyalty: { republic: 10 }, requires: { item: 'csf_aux_badge' }, result: '"Three containers, Platform 09. Logged under Senate clearance codes that trace back to a sub-committee that officially does not exist. I kept a copy of the routing data."', grants: { flags: ['dax_talked', 'phantom_freight_resolved'], codex: ['codex-sector-4-freight-corridors'] } },
              { text: '"Give me the data and I will make sure your name stays out of the report."', morality: 0, loyalty: { underworld: 5 }, result: '"My name better not appear anywhere. Here." He transfers a file. "Now leave."', grants: { flags: ['dax_talked_unofficial'] } },
            ],
          },
          { id: 'vond_vendor', x: 8, y: 24, kind: 'mechanic', label: '"Greasy" Vond — Scrap and Salvage',
            isVendor: true, vendorFaction: 'underworld', repeatable: true,
            vendorStock: ['scrambler_keycard', 'item_blaster_parts', 'item_code_cylinder'],
            repeatPrompt: 'Vond is cataloguing a crate of questionable components.',
            prompt: '"If it fell off the back of a freighter, I have it. No warranties. Once you walk off my platform, we do not know each other."',
            choices: [],
          },
          { id: 'vane_freight', x: 34, y: 17, kind: 'republic_guard', label: 'Officer Vane',
            requiresAllFlags: ['csf_training_complete'],
            phases: [
              { id: 'phase_field_briefing',
                requiresNoneFlags: ['vane_freight_briefed'],
                prompt: 'Vane has commandeered the cargo supervisor\'s office at the far end of the hub. A portable terminal runs Bay 14 manifest cross-references on one screen and a Platform 09 camera feed on another. He does not look up immediately. "You made it." He gestures to the chair. "Sit. There is a lot to cover before you walk that floor."',
                choices: [
                  { text: '"What exactly are we looking for, Detective?"', morality: 5, loyalty: { republic: 8 }, result: '"Ghost manifests. Three containers cleared through Bay 14 carry Senate authorization codes that trace to a sub-committee that does not officially exist. The cargo weight does not match the declared goods. Jax — dock engineer, he was in the maintenance shaft during the last transfer — will not talk without a push. Try the badge first. That is what it is for."', grants: { flags: ['vane_freight_briefed', 'bay14_method_known'] } },
                  { text: '"How far does my authority actually reach here?"', morality: 3, loyalty: { republic: 5 }, result: '"Sector 4, Platform 09, the Bay 14 loading corridor — full CSF Auxiliary authority. Witnesses are required to cooperate. You can access restricted cargo logs without a warrant. What you cannot do is make arrests without contact to Precinct Command. You find a name. I make the arrest."', grants: { flags: ['vane_freight_briefed'] } },
                  { text: '"This place is run by organized crime. Are we actually going to change anything?"', morality: 0, loyalty: { underworld: 3 }, result: '"We are going to find out who moved unscanned Phrik alloy through this hub using a Senate override code. Whether that changes anything depends on who is holding the other end. Start with Jax. He was there."', grants: { flags: ['vane_freight_briefed', 'vane_sector4_hint'] } },
                ],
              },
              { id: 'phase_field_ongoing',
                requiresAllFlags: ['vane_freight_briefed'],
                requiresNoneFlags: ['freight_hub_investigated'],
                prompt: '"Talk to Jax. He was in the maintenance shaft during the transfer. He knows more than he admitted to the first CSF team. If he will not cooperate, try the shipping clerk Dax — he has been filing false manifests and he knows it. The Bay 14 blast marks are also preserved near Platform 09."',
                choices: [
                  { text: '"What if both of them refuse?"', morality: 0, loyalty: {}, result: '"Then you pull the physical evidence. Crime scene at Bay 14, customs manifest registry near Platform 09 — either one gives us documentation. I need something that holds up in front of a Senate subcommittee."' },
                  { text: '"Understood. I will start with Jax."', morality: 5, loyalty: { republic: 5 }, result: '"Good. Keep your badge visible. In Sector 4 that matters more than the weapon."' },
                ],
              },
              { id: 'phase_field_post_investigation',
                requiresAllFlags: ['freight_hub_investigated'],
                prompt: 'Vane is already packing the portable terminal when you return. "I got enough from your transmissions. Good work, Investigator." He locks the terminal under his arm. "Everything you found gets documented and sealed at Precinct Command tonight. Meet me there. I am not transmitting this through an open line."',
                choices: [
                  { text: '"How serious is what we found?"', morality: 5, loyalty: { republic: 8 }, result: '"Serious enough that I want four walls around this conversation before I answer that. Precinct Command. AirTaxi back the way you came."' },
                  { text: '"I will be right behind you."', morality: 3, loyalty: { republic: 5 }, result: '"See that you are." He walks out with the terminal under his arm and does not look back.' },
                ],
              },
            ],
          },
        ],
        collectibles: [
          { id: 'freight_hub_credit', x: 24, y: 22, label: 'Dropped Pay Chip', reward: 60 },
          { id: 'stolen_spark_rig', x: 34, y: 18, label: "Jax's Calibration Rig", reward: 0, grantsItem: 'calibrated_hydrospanner' },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 38, 26, 'floor');
          carveRect(g, 15, 2, 25, 10, 'wall');
          carveRect(g, 16, 3, 24, 9, 'floor');
          carveRect(g, 30, 14, 38, 22, 'wall');
          carveRect(g, 31, 15, 37, 21, 'floor');
          pt(g, 30, 18, 'floor');
          pt(g, 20, 10, 'floor');
          carveRect(g, 37, 4, 39, 8, 'wall');
          carveRect(g, 38, 5, 38, 7, 'floor');
          pt(g, 37, 6, 'floor');
          pt(g, 20, 27, 'door'); pt(g, 21, 27, 'door');
          return g;
        },
      },
      mag_rail_corridor: {
        id: 'mag_rail_corridor', name: 'Sector 4 Mag-Rail Station', subtitle: 'Coruscant · L.088 — Freight Transit Corridor',
        width: 42, height: 26, spawnPos: { x: 20, y: 2 }, textureId: 'coruscant',
        accent: '#6090C0', accentGlow: 'rgba(96,144,192,0.22)', accentDim: '#203060',
        floorColor: '#181614', floorAlt: '#201E1A', wallDark: '#080706', wallLight: '#141210',
        bg: 'radial-gradient(circle at 50% 50%, #101420 0%, #08090E 70%)', ambient: 'traffic',
        decor: ['pipe', 'girder', 'cargo_crate', 'neon_sign'],
        doors: [
          { x: 20, y: 0, targetZone: 'freight_hub', targetPos: { x: 20, y: 26 }, label: 'Sector 4 Freight Hub' },
          { x: 21, y: 0, targetZone: 'freight_hub', targetPos: { x: 21, y: 26 }, label: 'Sector 4 Freight Hub' },
          { x: 41, y: 13, targetZone: 'fueling_depot', targetPos: { x: 1, y: 13 }, label: 'Fueling Depot' },
        ],
        worldObjects: [
          { id: 'airtaxi_mag_rail', propArt: 'airtaxi', propVariant: 'official', x: 4, y: 13, once: false, label: 'AirTaxi Terminal', description: 'A CSF-maintained AirTaxi terminal serving the Sector 4 freight corridor. The departure board shows three routes. Two are suspended pending security review.' },
          { id: 'mag_rail_manifest_board', propArt: 'board', propVariant: 'manifest', x: 20, y: 5, once: false, label: 'Cargo Manifest Display Board', description: 'A wall-mounted manifest board listing every freight car scheduled through the Sector 4 mag-rail in the current cycle. Car 14-B is listed twice — once as agricultural supplies, once as cleared industrial equipment. The two entries have different weights. The discrepancy is flagged in amber. The flag has been open for eleven days.' },
          { id: 'platform_security_scanner', propArt: 'scanner_arch', propVariant: 'platform', x: 14, y: 13, once: true, label: 'Platform Security Scanner', description: 'A Republic-standard cargo scanner mounted at the platform gate. The scan log shows that three freight cars in the past month were cleared without completing a full scan cycle. Each clearance was manually authorized. The authorization code is the same each time — a Senate-tier override that should not exist at this checkpoint level.', grantsFlag: 'scanner_override_found' },
          { id: 'north_platform_terminal', propArt: 'console', propVariant: 'freight', x: 8, y: 7, once: false, label: 'North Platform Freight Terminal', description: 'The loading terminal for the north bay platforms. Consignment records go back fourteen months. The system shows no gap in the record sequence — but three consignment numbers have been reused. Reused consignment numbers overwrite the original entry. Whatever moved through those numbers the first time has been erased.' },
          { id: 'east_cargo_bay_locker', propArt: 'crate_stack', propVariant: 'locker', x: 34, y: 7, once: true, label: 'Sealed Cargo Bay Locker', description: 'A standard freight locker with a non-standard lock — the override code is a military-issue cipher, not a Republic transit cipher. Inside: three manifest strips printed on thermal flimsi, each listing the same destination account. The account number matches the shell corporation routing from the Scylla manifest.', grantsFlag: 'freight_chain_confirmed', grantsCodex: 'codex-sector-4-freight-corridors' },
          { id: 'south_platform_crate_stack', propArt: 'container_stack', x: 8, y: 19, once: false, label: 'Numbered Cargo Stack — Platform South', description: 'Forty-eight standardized gray containers stacked three high on the south loading platform. Each bears a Senate commerce committee seal. Each seal is from a different committee session. The committees do not overlap in their stated jurisdictions. What they all share: a signature from the same undersecretary, on the same date, for cargoes described as "legislative materials."' },
          { id: 'rail_junction_box', propArt: 'junction_box', propVariant: 'rail', x: 26, y: 19, once: true, label: 'Rail Junction Control Box', description: 'The mag-rail switching junction for the Sector 4 corridor. The switching schedule shows a recurring twelve-minute hold every fourth cycle — the rail is stopped, the platform cameras are on maintenance loop, and no personnel are scheduled. The hold began eight months ago. It runs like clockwork. Nothing in the official record explains it.', grantsFlag: 'rail_window_found' },
          { id: 'senate_transit_charge', propArt: 'transit_pod', x: 36, y: 9, once: true, iconKind: 'terminal', label: 'Senate Transit Car 12',
            requiresFlag: 'bomb_reached',
            requires: { item: 'master_senate_transit_drive' },
            lockedMessage: 'A Senate transit car idles at the platform with its service hatch sealed. It looks routine, and you do not yet know what to look for. The Syndicate command terminal in the undercity would tell you.',
            description: "Car 12 of the Senate express, parked on the siding with its service hatch sealed. The drive from the Syndicate command terminal unlocks the hatch and the schematic is exactly as the file promised: a seismic charge wired under the passenger floor, counting down to the opening of the next session. You pry the casing, close the vents and jettison the charge into the incinerator shaft before the timer reaches zero.",
            triggersMinigame: 'valve_override',
            grantsFlag: 'senate_line_secured' },
          { id: 'pursuit_start', propArt: 'speeder_bay', x: 34, y: 19, once: true, label: 'Emergency Speeder Bay', description: 'A single repulsor speeder, engine warm, tethered to a quick-release bay mount. A Syndicate courier was spotted boarding the mag-rail three minutes ago heading for The Works. This speeder can intercept if you move now. The rail corridor is active.', triggersMinigame: 'speeder_pursuit', grantsFlag: 'chase_resolved' },
        ],
        npcs: [
          { id: 'checkpoint_officer_drel', x: 20, y: 13, kind: 'republic_guard', label: 'Checkpoint Officer Drel',
            prompt: '"Transit checkpoint, Sector 4 corridor. State your business and present your transit documentation."',
            repeatPrompt: 'Officer Drel watches the cargo flow with the practiced attention of someone who has learned to see what he is allowed to see and nothing else.',
            choices: [
              { text: '"CSF Auxiliary. I need access to the cargo scan logs."', morality: 8, loyalty: { republic: 8 }, requires: { item: 'csf_aux_pass' }, result: '"Auxiliary clearance. Noted." He steps aside from the terminal. "The logs are current. The anomalies in them are not my jurisdiction. That is what I have been told. Officially." He does not look at you when he says it.', grants: { flags: ['drel_cooperated'] } },
              { text: '"What is the standard protocol when a Senate override clears cargo without a full scan?"', morality: 5, loyalty: { republic: 5 }, result: '"The standard protocol is to log the override and file a query with the Senate transit authority. Processing time on those queries is eight to twelve weeks. In eight months I have filed twenty-two queries. I have received zero responses." He picks up his datapad. "Standard protocol."', grants: { flags: ['drel_queried'] } },
              { text: '"What moves through here at the twelve-minute rail hold?"', morality: -3, loyalty: { underworld: 8 }, result: '"I do not work that shift. Neither does anyone else. That is the point." He lowers his voice. "Whatever moves in those twelve minutes has full Senate clearance and leaves no scan record. That is all I can tell you without losing this job."', grants: { flags: ['rail_hold_confirmed'] } },
            ],
          },
          { id: 'cargo_runner_essa', x: 8, y: 18, kind: 'smuggler', label: 'Essa — Cargo Runner', mobile: true,
            prompt: '"You have that look. CSF adjacent. Not quite official. Working an angle." She props a crate with her shoulder and keeps her hands visible. "I am just moving freight. Legitimate freight. Certified and sealed."',
            repeatPrompt: 'Essa moves between crates with the efficiency of someone who knows exactly where the cameras are.',
            choices: [
              { text: '"What do you know about the twelve-minute rail hold?"', morality: -5, loyalty: { underworld: 10 }, result: '"Who told you about that?" She sets down the crate. "That hold is Syndicate time. Nobody uses those twelve minutes who is not paying the Syndicate for them. I know because I used to be on that schedule. I left. Some opportunities are not worth the company."', grants: { flags: ['essa_talked', 'syndicate_rail_confirmed'] } },
              { text: '"I am looking for where the Phrik shipment went after Bay 14."', morality: 0, loyalty: {}, result: '"The Works. Level 005. That is not a secret. What is a secret is how it got there without crossing a single checkpoint. Rail hold. Sealed car. Nobody asked questions because nobody who asks questions lasts long in this corridor."', grants: { flags: ['essa_phrik_route'] } },
              { text: '"Move some of my cargo. Off the record."', morality: -10, loyalty: { underworld: 8 }, result: '"How much? What kind? Where?" She is already doing the math. "If it is anything that glows, I charge double." ', grants: { flags: ['essa_contracted'] } },
            ],
          },
        ],
        collectibles: [{ id: 'mag_rail_chip', x: 36, y: 19, label: 'Freight Transit Chip', reward: 40 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 40, 24, 'floor');
          carveRect(g, 1, 3, 14, 12, 'wall');
          carveRect(g, 2, 4, 13, 11, 'floor');
          pt(g, 14, 7, 'floor');
          carveRect(g, 1, 15, 14, 23, 'wall');
          carveRect(g, 2, 16, 13, 22, 'floor');
          pt(g, 14, 19, 'floor');
          carveRect(g, 26, 3, 40, 12, 'wall');
          carveRect(g, 27, 4, 39, 11, 'floor');
          pt(g, 26, 7, 'floor');
          pt(g, 20, 0, 'door'); pt(g, 21, 0, 'door');
          pt(g, 41, 13, 'door');
          return g;
        },
      },
      fueling_depot: {
        id: 'fueling_depot', name: 'Sector 4 Fueling Depot', subtitle: 'Coruscant · L.088 — Repulsor Fueling Station',
        width: 38, height: 26, spawnPos: { x: 2, y: 13 }, textureId: 'coruscant',
        accent: '#D06000', accentGlow: 'rgba(208,96,0,0.22)', accentDim: '#602800',
        floorColor: '#161210', floorAlt: '#1E1814', wallDark: '#060402', wallLight: '#120E08',
        bg: 'radial-gradient(circle at 30% 70%, #160C04 0%, #080604 70%)', ambient: 'embers',
        decor: ['pipe', 'cargo_crate', 'slag', 'girder'],
        doors: [
          { x: 0, y: 13, targetZone: 'mag_rail_corridor', targetPos: { x: 40, y: 13 }, label: 'Mag-Rail Corridor' },
          { x: 18, y: 25, targetZone: 'drainage_pipes', targetPos: { x: 18, y: 1 }, label: 'Lower Drainage' },
          { x: 19, y: 25, targetZone: 'drainage_pipes', targetPos: { x: 19, y: 1 }, label: 'Lower Drainage' },
        ],
        worldObjects: [
          { id: 'airtaxi_fueling_depot', propArt: 'airtaxi', propVariant: 'worn', x: 16, y: 13, once: false, label: 'AirTaxi Terminal', description: 'A weather-beaten AirTaxi terminal on the depot concourse. The routing display is covered in fuel-transfer grime but functional.' },
          { id: 'fuel_register_terminal', propArt: 'console', propVariant: 'fuel', x: 6, y: 6, once: true, label: 'Fuel Transfer Register', description: 'The bay fuel accounting terminal. Cross-referencing usage logs against departure manifests reveals a forty-liter discrepancy per cycle — consistent for six months. The fuel is not being logged as waste. It is not being logged at all. Forty liters per cycle is enough to run a mid-size repulsor platform continuously for eighteen hours.', grantsFlag: 'fuel_discrepancy_found' },
          { id: 'bay_b_work_order', propArt: 'board', propVariant: 'clipboard', x: 26, y: 6, once: true, label: 'Bay B Work Order Clipboard', description: 'A physical clipboard of maintenance work orders. The third sheet from the bottom is printed on different flimsi from the rest — thicker, higher grade, the kind used for official Senate documents. The work order it describes is routine repulsor servicing. The authorization signature at the bottom is not a depot supervisor. It is a Senate sub-committee seal. A fueling depot work order, sealed by the Senate.', grantsFlag: 'bay_b_order_found' },
          { id: 'maintenance_pit_console', propArt: 'console', propVariant: 'pit', x: 8, y: 19, once: true, label: 'Maintenance Pit Access Console', description: 'The control console for the below-deck maintenance pit. The activity log shows seventeen access events in the past two months. Each entry lists the same user ID: TEMP-TRANSIT. Temp transit IDs are issued for single-use clearance and expire in four hours. These ones were all used on the same day, at the same time, for the same access — which should be impossible for single-use IDs.', grantsFlag: 'temp_id_exploit_found' },
          { id: 'fuel_drum_stack', propArt: 'drum_array', x: 28, y: 19, once: false, label: 'Repulsor Fuel Drum Array', description: 'Fifty sealed drums of Type-4 repulsor fuel stacked in the depot\'s overflow area. Each drum is stamped with a Republic Military supply chain code — civilian depots are not authorized to hold military-grade fuel. The authorization exemption on the stack is signed by the same Senate sub-committee that cleared the Bay 14 cargo.' },
        ],
        npcs: [
          { id: 'depot_mechanic_torb', x: 6, y: 5, kind: 'mechanic', label: 'Fuel Tech Torb',
            prompt: '"Keep clear of the active bays. Republic safety code requires a three-meter buffer during pressurized transfer. Also, who are you and how did you get past the corridor checkpoint?"',
            repeatPrompt: 'Torb works with the deliberate precision of someone who has seen what fuel fires look like up close.',
            choices: [
              { text: '"CSF Auxiliary. I am investigating a fuel accounting discrepancy."', morality: 8, loyalty: { republic: 8 }, requires: { item: 'csf_aux_pass' }, result: '"The register." He exhales. "I flagged that four months ago. Submitted the form to the depot supervisor. She submitted it to the freight authority. They submitted it to the Senate transit liaison. I stopped hearing about it two days later. The form number I submitted? It no longer exists in the system."', grants: { flags: ['torb_talked', 'fuel_flag_buried'] } },
              { text: '"What is stored in Bay B?"', morality: 0, loyalty: {}, result: '"Military-grade Type-4. We are not supposed to have it. We have had it for six months. I asked about it once. I was reassigned to night shift the next day. I stopped asking."', grants: { flags: ['bay_b_military_fuel'] } },
              { text: '"Is there a way down to the lower drainage level from here?"', morality: -3, loyalty: { underworld: 5 }, result: '"The south hatch past the maintenance pit. I would not go down there. The drainage runs directly under the fuel storage array. One bad coupling and the whole sub-level goes." He pauses. "People go down anyway."', grants: { flags: ['drainage_access_known'] } },
            ],
          },
          { id: 'smuggler_contact_voss', x: 28, y: 6, kind: 'smuggler', label: 'Voss — Depot Contact',
            prompt: '"You are in the wrong bay. Unless you are looking for me. In which case: how did you know to look here, and who sent you?"',
            repeatPrompt: 'Voss watches you with professional patience. He has more information than he is showing.',
            choices: [
              { text: '"Essa from the mag-rail corridor pointed me this way."', morality: -5, loyalty: { underworld: 10 }, requires: { flag: 'essa_talked' }, result: '"Essa. She is careful about who she sends." He relaxes a fraction. "Then you already know the shape of the operation. The fuel discrepancy, the Senate work orders, the Syndicate rail window. You are putting it together. Good. Someone should."', grants: { flags: ['voss_depot_talked', '088_operation_shape_known'] } },
              { text: '"I need a route to Level 005 that does not cross a checkpoint."', morality: -10, loyalty: { underworld: 12 }, result: '"The drainage hatch. South of the maintenance pit. Follow the main channel east until you hit the ventilation junction. Left at the junction, down the service ladder, and you are on Level 005. Do not touch the walls — the drainage carries thermal runoff from the level above. Hot enough to burn through a boot."', grants: { flags: ['drainage_route_to_005'] } },
              { text: '"What does the Iron Syndicate use this depot for?"', morality: 0, loyalty: {}, result: '"Staging. The Syndicate does not store cargo here. They move it. This is a transit point — cargo in from the mag-rail, transferred to vehicles here, down through the drainage to Level 005. The fuel discrepancy covers the vehicle fuel. Everything else is on paper that is officially perfect."', grants: { flags: ['voss_syndicate_explained'], codex: ['codex-iron-syndicate'] } },
            ],
          },
        ],
        collectibles: [{ id: 'depot_cred', x: 32, y: 22, label: 'Dropped Fuel Chit', reward: 55 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 36, 24, 'floor');
          carveRect(g, 1, 2, 12, 11, 'wall');
          carveRect(g, 2, 3, 11, 10, 'floor');
          pt(g, 12, 6, 'floor');
          carveRect(g, 22, 2, 36, 11, 'wall');
          carveRect(g, 23, 3, 35, 10, 'floor');
          pt(g, 22, 6, 'floor');
          carveRect(g, 2, 15, 14, 23, 'wall');
          carveRect(g, 3, 16, 13, 22, 'floor');
          pt(g, 14, 19, 'floor');
          pt(g, 0, 13, 'door');
          pt(g, 18, 25, 'door'); pt(g, 19, 25, 'door');
          return g;
        },
      },
      drainage_pipes: {
        id: 'drainage_pipes', name: 'Sector 4 Lower Drainage', subtitle: 'Coruscant · L.088 — Sub-Level Maintenance Tunnels',
        width: 36, height: 22, spawnPos: { x: 18, y: 2 }, textureId: 'coruscant',
        accent: '#208040', accentGlow: 'rgba(32,128,64,0.20)', accentDim: '#0C3020',
        floorColor: '#0E100C', floorAlt: '#161A12', wallDark: '#040602', wallLight: '#0C0E08',
        bg: 'radial-gradient(circle at 50% 100%, #0C1408 0%, #060A04 50%, #030502 100%)',
        ambient: 'embers', decor: ['pipe', 'slag', 'rubble'],
        doors: [
          { x: 18, y: 0, targetZone: 'fueling_depot', targetPos: { x: 18, y: 24 }, label: 'Fueling Depot' },
          { x: 19, y: 0, targetZone: 'fueling_depot', targetPos: { x: 19, y: 24 }, label: 'Fueling Depot' },
        ],
        worldObjects: [
          { id: 'airtaxi_drainage', propArt: 'airtaxi', propVariant: 'ancient', x: 2, y: 11, once: false, label: 'AirTaxi Terminal', description: 'An ancient AirTaxi terminal grafted onto the drainage tunnel wall. It should not work. It does. The destination list shows zones that are no longer accessible from any other terminal.' },
          { id: 'drainage_channel_main', propArt: 'drain_channel', x: 17, y: 12, once: false, label: 'Main Drainage Channel', description: 'A wide channel cut through the durasteel floor carries thermal runoff from the fueling depot above. The liquid moves slowly, hot enough to steam in the cold tunnel air. The color is the pale amber of industrial lubricant mixed with coolant fluid. Whatever process generates this much waste runs continuously. The channel is not on any Level 088 maintenance map.' },
          { id: 'syndicate_cache_drainage', propArt: 'crate_stack', propVariant: 'false_panel', x: 6, y: 7, once: true, label: 'Maintenance Alcove Cache', description: 'Behind a false panel in the maintenance alcove wall: a sealed container marked with the iron chain emblem. Inside, three cargo relay chips — each one a transit authorization for a different Level 005 loading dock. Each authorization is blank, ready to be written with any cargo description. A full set of ready-made blank transit passes for The Works.', grantsFlag: 'transit_pass_cache_found', grantsItem: 'scrambler_keycard' },
          { id: 'drainage_graffiti_wall', propArt: 'wall_marks', propVariant: 'drain', x: 24, y: 7, once: false, label: 'Drainage Tunnel Wall', description: 'The tunnel wall is layered with markings going back decades — maintenance crew tallies, territorial tags from three different gang factions, one very detailed Aurebesh map of the drainage system drawn in conductive paint that still faintly glows. The Iron Syndicate\'s iron chain emblem appears four times, each one over a different gang\'s markings. The Syndicate has been here longer than anyone admits.' },
          { id: 'junction_box_drainage', propArt: 'junction_box', propVariant: 'vent', x: 28, y: 16, once: true, label: 'Ventilation Junction Control', description: 'The junction control box for the Level 088 ventilation spine. The routing table inside has been modified — Level 005 airflow is being vented up through this drainage system instead of out through the designated exhaust ports. The modification is deliberate. Someone is using the drainage vents to circulate air to Level 005 workers without triggering environmental monitoring on that level.', grantsFlag: 'vent_route_005_found' },
          { id: 'syndicate_marker_drainage', propArt: 'wall_marks', propVariant: 'stamp', x: 32, y: 18, once: true, label: 'Recent Syndicate Transit Mark', description: 'A fresh iron chain emblem stamp on the tunnel floor — the mark is crisp, the metal around it bright. This route was used within the last forty-eight hours. The cargo moved east along the main channel, turned south at the junction, and went down. Toward Level 005.', grantsFlag: 'fresh_syndicate_trail' },
        ],
        npcs: [
          { id: 'fugitive_mek', x: 30, y: 17, kind: 'mechanic', label: 'Mek — Level 005 Fugitive',
            prompt: 'He is pressed into a maintenance alcove, coated in drainage grime, and holding a broken hydrospanner like a weapon. "Stay back. I know what you are. Syndicate does not send two." He squints. "You are not dressed like Syndicate."',
            repeatPrompt: 'Mek keeps one eye on the tunnel behind you. He has not decided whether staying here is worse than moving.',
            choices: [
              { text: '"I am not Syndicate. I am investigating them. What do you know?"', morality: 5, loyalty: { republic: 5 }, result: '"Investigating." He laughs, one short sound. "Good luck with that. I worked their Level 005 loading operation for four months before I realized what I was loading. When I tried to stop, they sealed the level. I got out through the drainage. I have been here for six days."', grants: { flags: ['mek_met', 'mek_escaped_005'] } },
              { text: '"Tell me the layout of the Level 005 loading operation."', morality: 0, loyalty: {}, result: '"Three main bays. Two are Phrik processing — they are smelting it into armor plating. The third is assembly. They are not building weapons. They are building people. Combat exoskeletons. Phrik-plated. Something that can walk through a blaster wall and not stop." He swallows. "I helped build six of them before I understood what they were."', grants: { flags: ['mek_005_layout', 'syndicate_exoskeleton_known'] } },
              { text: '"I can get you out of the drainage. But you tell me everything first."', morality: 8, loyalty: { republic: 8 }, result: '"Everything. Fine." Over the next twenty minutes, he gives you names, delivery schedules, cargo codes, and the location of every Syndicate guard post on Level 005. "Now get me out of here before the next transit cycle comes through."', grants: { flags: ['mek_full_intel', 'works_layout_known'], codex: ['codex-the-works-forges'] } },
            ],
          },
        ],
        collectibles: [{ id: 'drainage_cred', x: 14, y: 18, label: 'Waterlogged Credit Chip', reward: 70 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 34, 20, 'floor');
          carveRect(g, 1, 3, 10, 10, 'wall');
          carveRect(g, 2, 4, 9, 9, 'floor');
          pt(g, 10, 6, 'floor');
          carveRect(g, 20, 3, 34, 10, 'wall');
          carveRect(g, 21, 4, 33, 9, 'floor');
          pt(g, 20, 6, 'floor');
          carveRect(g, 22, 13, 34, 20, 'wall');
          carveRect(g, 23, 14, 33, 19, 'floor');
          pt(g, 22, 16, 'floor');
          carveRect(g, 12, 10, 16, 14, 'water');
          pt(g, 18, 0, 'door'); pt(g, 19, 0, 'door');
          return g;
        },
      },
      the_works: {
        id: 'the_works', name: 'The Works', subtitle: 'Coruscant · Undercity · The Works L.005',
        width: 36, height: 24, spawnPos: { x: 2, y: 12 }, textureId: 'coruscant',
        accent: '#FF4444', accentGlow: 'rgba(255,68,68,0.2)', accentDim: '#660000',
        floorColor: '#14100A', floorAlt: '#1C160E', wallDark: '#060402', wallLight: '#100C06',
        bg: 'radial-gradient(circle at 50% 80%, #140800 0%, #050302 70%)', ambient: 'steam', floorPattern: 'rough',
        decor: ['pipe', 'girder', 'slag', 'rubble', 'warning_beacon', 'brazier'],
        doors: [
          { x: 20, y: 23, targetZone: 'cooling_ducts', targetPos: { x: 20, y: 1 }, label: 'Cooling Ducts' },
          { x: 21, y: 23, targetZone: 'cooling_ducts', targetPos: { x: 21, y: 1 }, label: 'Cooling Ducts' },
          { x: 34, y: 12, targetZone: 'undercity_outskirts', targetPos: { x: 2, y: 12 }, label: 'Undercity Outskirts' },
        ],
        worldObjects: [
          { id: 'syndicate_cargo_cache', propArt: 'crate_stack', propVariant: 'syndicate', x: 18, y: 10, once: true, iconKind: 'crate', label: 'Syndicate Cargo Cache', description: 'Stacked crates stamped with an iron chain. Phrik plating, combat chassis, and an empty Jedi archive canister. Wedged behind the canister, a fist-sized crystal still whispers.', grantsItem: 'encrypted_shard',
            autoCodex: { id: 'discovery-works-syndicate-cache', title: 'Iron Syndicate Cache', category: 'discoveries', summary: 'Phrik plating cut to pauldron dimensions.', body: ['Stacked crates stamped with a stylised iron chain. Inside: Phrik plating cut to pauldron dimensions, half-assembled combat chassis, and one empty Jedi archive canister. Someone opened it already.'] } },
          { id: 'plasma_conduit_005', propArt: 'leaking_pipe', propVariant: 'plasma', x: 8, y: 18, once: false, label: 'Leaking Plasma Conduit', description: 'The pipe groans under pressure. A slow leak fills the air with acrid chemical haze. This entire sub-level is one spark away from a chain event.' },
          { id: 'sub_station_terminal', propArt: 'console', propVariant: 'substation', x: 10, y: 20, once: true, iconKind: 'panel', label: 'Deep Sub-Station Controls', description: 'Power sub-station 3. Controls ambient thermal hazards in the surrounding corridor.', grantsItem: null,
            autoCodex: { id: 'discovery-sub-station-3', title: 'Sub-Station 3: Thermal Grid', category: 'discoveries', summary: 'The Works thermal hazard routing mapped.', body: ['Power sub-station 3 controls the thermal vent routing for the Level 005 lower corridor. The hazard configuration is non-standard — vents have been aimed laterally rather than exhausting upward. Someone reconfigured this intentionally. The most likely purpose: to discourage unauthorized access to the smelter corridor without triggering alarms that would appear on Republic monitoring feeds.'] } },
          { id: 'syndicate_relay_node', propArt: 'comm_relay', propVariant: 'node', x: 24, y: 6, once: true, label: 'Syndicate Relay Node', description: 'Iron Syndicate tactical communications. Slicing this intercepts live patrol data — every enemy position in The Works becomes visible on your minimap for the duration of the assault.' },
          { id: 'plasma_valve_a', propArt: 'pressure_valve', propVariant: 'a', x: 4, y: 18, once: true, iconKind: 'valve', label: 'Pressure Valve Alpha', description: 'Main coolant line junction. The pressure gauge reads critical. One override and the flow stabilizes.', triggersMinigame: 'valve_override', grantsFlag: 'valve_a_closed' },
          { id: 'plasma_valve_b', propArt: 'pressure_valve', propVariant: 'b', x: 14, y: 20, once: true, iconKind: 'valve', label: 'Pressure Valve Beta', description: 'Secondary coolant junction. Steam vents from the seal around the handle.', triggersMinigame: 'valve_override', grantsFlag: 'valve_b_closed' },
          { id: 'plasma_valve_c', propArt: 'pressure_valve', propVariant: 'c', x: 8, y: 14, once: true, iconKind: 'valve', label: 'Pressure Valve Gamma', description: 'Tertiary coolant junction. Closing this one stabilizes the entire pressure network.', triggersMinigame: 'valve_override', grantsFlag: 'valve_c_closed' },
          { id: 'holonet_official_terminal', propArt: 'console', propVariant: 'hnn_official', x: 30, y: 20, once: false, label: 'HNN Official Feed', description: '[HNN Priority Core Broadcast] "The Senate Committee on Inner-Rim Trade commended the CSF for maintaining unprecedented safety standards across the Mid-Levels. Reports of industrial smuggling near Level 088 have been dismissed as isolated logistical delays." The broadcast loops. The terminal is covered in soot.' },
          { id: 'airtaxi_the_works', propArt: 'airtaxi', propVariant: 'scorched', x: 33, y: 10, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal', description: 'A terminal barely functioning under the heat. Miracle it still works.' },
        ],
        npcs: [
          { id: 'vex', x: 22, y: 7, kind: 'crime_boss', label: 'Vex',
            repeatPrompt: 'Vex watches you from the shadows. His enforcers track your movement.',
            prompt: '"I do not know how you got this deep. I know you will not be leaving the same way." He signals two enforcers. Then pauses. "Unless you have something worth my time."',
            choices: [
              { text: 'Reveal that you know about the Phrik armor project.', morality: -10, loyalty: { underworld: 15 }, result: '"Interesting. You have done your homework. The Iron Syndicate builds to last. We are not selling. We are equipping. The question is: which side of that equation do you want to be on?"', grants: { flags: ['vex_met', 'iron_syndicate_known'], codex: ['codex-iron-syndicate'] } },
              { text: '"I am here to stop whatever you are building."', morality: 18, loyalty: { republic: 15 }, result: '"Bold." He gestures. The two enforcers advance without a word. The forge floor is hot, narrow, and full of cover. You draw them toward the plasma vents — the heat plays havoc with their sensor goggles — and break for the drainage exit on the south wall before they can regroup. Vex watches you go without calling more guards. He does not need to hurry. He has already decided you are interesting rather than dangerous.', grants: { flags: ['vex_hostile', 'iron_syndicate_known'] } },
            ],
          },
          { id: 'unit_7n4', x: 4, y: 5, kind: 'droid', label: 'Archivist Droid 7-N4',
            repeatPrompt: '7-N4 resumes archiving temperature data. Progress: 94.7 percent.',
            prompt: '"ARCHIVE ACCESS: corrupted. MEMORY CORE: partially functional. I was left behind when the warehouse was abandoned. QUERY: do you require ambient temperature data?"',
            choices: [
              { text: 'Give it the Sith Memory Prism. "Can you decrypt this?"', morality: 0, loyalty: {}, requires: { item: 'encrypted_shard' }, result: '"DECRYPTION: initiating. This is a Sith-era holocron shard. The content describes a weapon design — specifically, a melee platform armored in Phrik that resists both blaster and lightsaber damage. The Iron Syndicate intends to mass-produce this design. ARCHIVE ENTRY CREATED."', grants: { flags: ['shard_decrypted', 'syndicate_weapon_known'], codex: ['codex-iron-syndicate', 'codex-phrik-alloy'] } },
              { text: 'Ask it what was stored here before the Iron Syndicate arrived.', morality: 5, loyalty: {}, result: '"This facility last logged Republic military materiel eighteen months ago. Current occupants arrived nine months ago with Phrik alloy and Jedi archive canisters. ASSESSMENT: occupation is unauthorized. CONCERN LEVEL: high."', grants: { codex: ['codex-docking-bay-14'] } },
            ],
          },
          { id: 'sula_anvil', x: 20, y: 18, kind: 'mechanic', label: 'Sula "The Anvil"',
            repeatPrompt: 'Sula is hammering a cooling piece of alloy. She does not stop when you speak.',
            prompt: '"Phrik is not just metal, officer. It is the bones of the galaxy. The Syndicate thinks they can melt it down with cheap thermal charges to build dirty bombs? It is an insult to the craft."',
            choices: [
              { text: '"Can Phrik alloy be stabilized if it is already exposed to plasma heat?"', morality: 5, loyalty: {}, result: '"Not without cryogenic cooling. If they heat that core past three thousand degrees, it is not a shipment anymore — it is a critical melt." She pulls up a schematic. "The emergency coolant valves are in the lower sub-station. Use them before the Syndicate ignites the crucible."', grants: { flags: ['sula_informed', 'coolant_method_known'] } },
              { text: '"Join us. Help CSF shut down the Syndicate\'s crucible for good."', morality: 8, loyalty: { republic: 8 }, result: '"I do not care about your politics. But I will not let them ruin my foundry. Give me droid cover for my back and I will cut their power lines myself."', grants: { flags: ['sula_allied'] } },
            ],
          },
          { id: 'kaelen_twi', x: 6, y: 20, kind: 'smuggler', label: '"The Ghost" Kaelen',
            repeatPrompt: 'Kaelen is crouched behind a burned power cell stack, watching both exits.',
            prompt: '"Do not shoot! I am just wiping terminal logs! The Syndicate took my sister. They said if I did not override the security grid for Docking Bay 14, they would throw her into the thermal vents!"',
            choices: [
              { text: '"You aided an armed attack on a CSF perimeter. Come with me."', morality: 3, loyalty: { republic: 10 }, result: '"No! You do not understand — the Syndicate owns the precinct holding cells! I will not last an hour!" His cybernetic optic flickers in genuine fear.', grants: { flags: ['kaelen_twi_arrested'] } },
              { text: '"Tell me where your sister is. If I save her, you hand over every encryption key you have."', morality: 8, loyalty: { republic: 8 }, result: '"They are keeping her in Sub-Level 3 holding cells! Save her, and I will slice the main door to the Senate transit line for you!"', grants: { flags: ['kaelen_twi_deal', 'sub_level_extraction_available'] } },
            ],
          },
          { id: 'krell_vendor', x: 28, y: 20, kind: 'crime_boss', label: 'Krell — Black Market Arms',
            isVendor: true, vendorFaction: 'underworld', repeatable: true,
            vendorStock: ['item_spice_vial', 'tool_hydrospanner', 'emp_grenade'],
            repeatPrompt: 'Krell watches you from behind the display case.',
            prompt: '"The Republic does not come down this far. My blasters hit harder, run hotter, and do not leave serial numbers. What do you need?"',
            choices: [],
          },
          { id: 'marla_foreman', x: 8, y: 20, kind: 'cantina_owner', label: 'Marla — Sub-Level Foreman',
            repeatPrompt: 'Marla is directing workers away from the venting steam. She looks exhausted.',
            prompt: '"Keep your filters tight today. The Syndicate is running the smelters at maximum. There is a pressure valve venting into the residential catwalks — if it ruptures, we lose thirty families."',
            choices: [
              { text: '"Point me to the main pressure valves. I will shut them down."', morality: 12, loyalty: { republic: 10 }, result: '"Sub-smelting level, three valves in sequence. The maintenance droids down there have gone rogue — watch yourself."', grants: { flags: ['thermal_leak_active'] } },
              { text: '"I have got bigger problems than a pipe valve."', morality: -5, loyalty: {}, result: 'She stares at you. "The people living on those catwalks do not."' },
            ],
          },
        ],
        collectibles: [{ id: 'works_syndicate_token', x: 12, y: 20, label: 'Iron Syndicate Token', reward: 0 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 34, 22, 'floor');
          carveRect(g, 10, 6, 14, 6, 'lava');
          carveRect(g, 20, 14, 24, 14, 'lava');
          carveRect(g, 28, 4, 28, 18, 'lava');
          carveRect(g, 15, 2, 27, 12, 'wall');
          carveRect(g, 16, 3, 26, 11, 'floor');
          carveRect(g, 2, 2, 8, 8, 'wall');
          carveRect(g, 3, 3, 7, 7, 'floor');
          pt(g, 5, 8, 'floor');
          pt(g, 15, 7, 'floor');
          pt(g, 34, 12, 'door');
          pt(g, 20, 23, 'door'); pt(g, 21, 23, 'door');
          return g;
        },
      },
      cooling_ducts: {
        id: 'cooling_ducts', name: 'Cooling Ducts', subtitle: 'Coruscant · Undercity · L.005 Cooling Infrastructure',
        width: 36, height: 26, spawnPos: { x: 20, y: 2 }, textureId: 'coruscant',
        accent: '#40B8C0', accentGlow: 'rgba(64,184,192,0.2)', accentDim: '#1A5860',
        floorColor: '#0E1416', floorAlt: '#141C1E', wallDark: '#040608', wallLight: '#0C1214',
        bg: 'radial-gradient(circle at 50% 40%, #081214 0%, #040608 70%)', ambient: 'embers',
        decor: ['pipe', 'girder', 'slag'],
        doors: [
          { x: 20, y: 0, targetZone: 'the_works', targetPos: { x: 20, y: 22 }, label: 'The Works' },
          { x: 21, y: 0, targetZone: 'the_works', targetPos: { x: 21, y: 22 }, label: 'The Works' },
          { x: 35, y: 13, targetZone: 'syndicate_command', targetPos: { x: 1, y: 13 }, label: 'Syndicate Command' },
        ],
        worldObjects: [
          { id: 'airtaxi_cooling_ducts', propArt: 'airtaxi', propVariant: 'cracked', x: 2, y: 13, label: 'AirTaxi Terminal', description: 'A terminal barely functional in the thermal haze. Emergency transit only.' },
          { id: 'coolant_manifold', propArt: 'leaking_pipe', propVariant: 'coolant', x: 10, y: 13, label: 'Coolant Manifold Junction', description: 'Superchilled gas hisses through cracked seals. The flow rate is wrong — someone has been bleeding coolant toward the smelter cores.' },
          { id: 'duct_access_panel', propArt: 'access_hatch', propVariant: 'crawl', x: 21, y: 18, once: true, label: 'Duct Access Panel', description: 'A maintenance crawl route into the syndicate warehouse. Wide enough for a person. The hinges are freshly oiled.', grantsFlag: 'duct_route_found' },
          { id: 'frost_buildup_terminal', propArt: 'console', propVariant: 'frost', x: 6, y: 5, once: true, label: 'Frost-Coated Terminal', description: 'Temperature logs. The cooling system was deliberately throttled twelve days ago — precisely when the Iron Syndicate began the final stage of their armor production run.', grantsFlag: 'coolant_sabotage_confirmed', grantsCodex: 'codex-the-works-forges' },
          { id: 'watcher_post', propArt: 'sentry_post', x: 26, y: 5, once: false, label: 'Observation Post', description: 'A crude sentry position overlooking the main duct junction. Scorch marks from a blaster. Someone held this position against something — or someone — coming from the south.' },
        ],
        npcs: [
          { id: 'cooling_tech_ardis', x: 6, y: 7, kind: 'mechanic', label: 'Maintenance Tech Ardis',
            prompt: '"The cooling systems are failing and nobody topside cares. If the thermal regulation collapses, the entire sub-level vaporizes. I cannot fix this alone."',
            repeatPrompt: 'Ardis is rerouting coolant lines with improvised patches. The work is never done.',
            choices: [
              { text: '"Tell me what you know about the Iron Syndicate using this duct system."', morality: 5, loyalty: { republic: 8 }, result: '"They come through at second shift — always the same route, always twelve of them. They carry plating. Combat weight. Whoever they are building it for, it is not a sport."', grants: { flags: ['duct_syndicate_observed'] } },
              { text: '"I will help stabilize the cooling flow. Point me to the valves."', morality: 10, loyalty: { republic: 5 }, result: '"The three main junctions are at sub-level four. I have a bypass rig but I cannot hold the pressure alone. If you can reach the east manifold, I can lock the rest."', grants: { flags: ['ardis_allied'] } },
            ],
          },
          { id: 'syndicate_watcher_kael', x: 28, y: 7, kind: 'swoop_gang', label: 'Syndicate Watcher', mobile: true,
            prompt: '"You are not supposed to be down here. Nobody is supposed to be down here."',
            repeatPrompt: 'The watcher tracks your movement with cold professional attention.',
            choices: [
              { text: '"I am maintenance crew. Checking the thermal seals."', morality: -5, loyalty: { underworld: 5 }, result: '"Maintenance does not wear that look. Move along before I file a report."', grants: { flags: ['watcher_bluffed'] } },
              { text: '"Stand down. CSF Auxiliary. Step away from the post."', morality: 8, loyalty: { republic: 10 }, result: 'He backs up two steps, hand near his weapon. He does not reach for it. "You have thirty seconds to get out of here before my relief arrives."', grants: { flags: ['watcher_backed_down'] } },
            ],
          },
        ],
        collectibles: [{ id: 'cooling_chip', x: 30, y: 20, label: 'Cryo-Canister Valve Cap', reward: 50 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 34, 24, 'floor');
          carveRect(g, 1, 3, 10, 12, 'wall');
          carveRect(g, 2, 4, 9, 11, 'floor');
          pt(g, 5, 3, 'floor');
          carveRect(g, 22, 3, 34, 12, 'wall');
          carveRect(g, 23, 4, 33, 11, 'floor');
          pt(g, 28, 3, 'floor');
          carveRect(g, 12, 5, 12, 20, 'water');
          carveRect(g, 20, 8, 20, 22, 'water');
          pt(g, 20, 0, 'door'); pt(g, 21, 0, 'door');
          pt(g, 35, 13, 'door');
          return g;
        },
      },
      syndicate_command: {
        id: 'syndicate_command', name: 'Syndicate Command', subtitle: 'Coruscant · Undercity · Iron Syndicate Operations Hub',
        width: 38, height: 26, spawnPos: { x: 2, y: 13 }, textureId: 'coruscant',
        accent: '#C03030', accentGlow: 'rgba(192,48,48,0.2)', accentDim: '#601010',
        floorColor: '#120A08', floorAlt: '#1A100E', wallDark: '#060202', wallLight: '#0E0806',
        bg: 'radial-gradient(circle at 40% 60%, #160804 0%, #060202 70%)', ambient: 'embers',
        decor: ['pipe', 'girder', 'slag', 'rubble'],
        doors: [
          { x: 0, y: 13, targetZone: 'cooling_ducts', targetPos: { x: 34, y: 13 }, label: 'Cooling Ducts' },
          { x: 18, y: 25, targetZone: 'extraction_vault', targetPos: { x: 18, y: 1 }, label: 'Extraction Vault' },
          { x: 19, y: 25, targetZone: 'extraction_vault', targetPos: { x: 19, y: 1 }, label: 'Extraction Vault' },
        ],
        worldObjects: [
          { id: 'airtaxi_syndicate_command', propArt: 'airtaxi', propVariant: 'rigged', x: 2, y: 13, label: 'AirTaxi Terminal', description: 'Syndicate-rigged transit terminal. The departure codes are wrong — it still works.' },
          { id: 'command_data_terminal', propArt: 'console', propVariant: 'syndicate', x: 7, y: 5, once: true, label: 'Iron Syndicate Command Terminal',
            requiresFlag: 'jon_endgame_known',
            lockedMessage: 'Active and unencrypted, a wall of Syndicate files with no way to know which one matters. Debrief Jon on the Sector 4 raid first. He knows what you are looking for.',
            description: 'Active. Unencrypted. The Syndicate was confident nobody would reach this level. Shipping manifests, crew rotations, and one file simply labeled EXTRACTION TIMETABLE. It names the real target: a seismic charge aboard a Senate transit car on the mag-rail corridor, timed for the next session. Every Syndicate bypass code for the transit line burns onto a slicer drive as you copy it.',
            grantsFlags: ['syndicate_plans_found', 'bomb_reached'], grantsItem: 'master_senate_transit_drive', grantsCodex: 'codex-iron-syndicate' },
          { id: 'armory_rack', propArt: 'weapon_rack', propVariant: 'pauldron', x: 28, y: 18, once: true, label: 'Syndicate Armory Rack', description: 'Phrik-composite pauldrons. Six sets, each sized for Republic-standard troopers. The Syndicate is not planning to sell this armor — they are equipping their own people to look like Republic soldiers.', grantsFlag: 'armor_ruse_discovered' },
          { id: 'comm_relay_hub', propArt: 'comm_relay', propVariant: 'hub', x: 18, y: 13, once: false, label: 'Encrypted Comm Relay', description: 'Burst-transmitting on a Senate sub-frequency. The source is Level 1900. Someone very senior is listening to every operation report from this room.' },
        ],
        npcs: [
          { id: 'syndicate_lt_braeven', x: 7, y: 7, kind: 'crime_boss', label: 'Lt. Braeven — Iron Syndicate',
            prompt: '"You survived the cooling ducts. That is either impressive or a problem. I have not decided." He does not move for his weapon. A subordinate does that for him.',
            repeatPrompt: 'Braeven has retreated to the back of the command room. His subordinates watch the door.',
            choices: [
              { text: '"Stand down. I know about the armor ruse. I know about the Senate contact. This ends here."', morality: 10, loyalty: { republic: 15 }, result: '"You know what you were allowed to know. The armor production is already complete. The extraction is already scheduled. You are three hours too late, officer."', grants: { flags: ['braeven_confronted', 'extraction_timetable_known'] } },
              { text: '"I want in. The Senate contact, the armor, the full operation. Name your price."', morality: -15, loyalty: { underworld: 20 }, result: 'A long pause. "You have nerve. That is either rare or a trap. The vault below holds the final shipment. If you get it out without CSF interference, we split forty percent." He means it.', grants: { flags: ['braeven_deal', 'extraction_timetable_known'] } },
              { text: '"Tell me where the prisoners are being held."', morality: 8, loyalty: { republic: 8 }, result: '"Prisoners? We do not keep prisoners. We keep leverage." He gestures south. "The vault. But you will not like what you find there."', grants: { flags: ['vault_location_confirmed'] } },
            ],
          },
        ],
        collectibles: [{ id: 'syndicate_command_chip', x: 32, y: 6, label: 'Iron Syndicate Access Chip', reward: 0 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 36, 24, 'floor');
          carveRect(g, 1, 2, 14, 12, 'wall');
          carveRect(g, 2, 3, 13, 11, 'floor');
          pt(g, 7, 2, 'floor');
          carveRect(g, 22, 14, 36, 24, 'wall');
          carveRect(g, 23, 15, 35, 23, 'floor');
          pt(g, 28, 14, 'floor');
          carveRect(g, 18, 4, 18, 20, 'lava');
          pt(g, 0, 13, 'door');
          pt(g, 18, 25, 'door'); pt(g, 19, 25, 'door');
          pt(g, 18, 13, 'floor'); // relay hub bridge over the lava channel
          return g;
        },
      },
      extraction_vault: {
        id: 'extraction_vault', name: 'Extraction Vault', subtitle: 'Coruscant · Undercity · Iron Syndicate Secure Storage',
        width: 36, height: 22, spawnPos: { x: 18, y: 2 }, textureId: 'coruscant',
        accent: '#A04000', accentGlow: 'rgba(160,64,0,0.2)', accentDim: '#501800',
        floorColor: '#100C08', floorAlt: '#180E0A', wallDark: '#050202', wallLight: '#0C0806',
        bg: 'radial-gradient(circle at 50% 70%, #120800 0%, #050202 70%)', ambient: 'embers',
        decor: ['pipe', 'girder', 'slag', 'rubble'],
        doors: [
          { x: 18, y: 0, targetZone: 'syndicate_command', targetPos: { x: 18, y: 24 }, label: 'Syndicate Command' },
          { x: 19, y: 0, targetZone: 'syndicate_command', targetPos: { x: 19, y: 24 }, label: 'Syndicate Command' },
        ],
        worldObjects: [
          { id: 'airtaxi_extraction_vault', propArt: 'airtaxi', propVariant: 'rigged', x: 2, y: 10, label: 'AirTaxi Terminal', description: 'Emergency transit. The Syndicate kept an exit route. Of course they did.' },
          { id: 'vault_main_door', propArt: 'vault_door', x: 11, y: 10, once: true, label: 'Vault Main Door', description: 'Phrik-reinforced. A standard slicer rig will not touch it. But the magnetic coupling on the secondary hinge is corroded — a calibrated hydrospanner could shear it clean.', grantsFlag: 'vault_door_assessed' },
          { id: 'vault_interior_cache', propArt: 'crate_stack', propVariant: 'arms_cache', x: 18, y: 10, once: true, label: 'Syndicate Arms Cache', description: 'Twenty completed Phrik pauldrons. Six sets of Republic trooper underarmor, modified for the Syndicate frame. And one sealed Senate diplomatic pouch — addressed to a committee chair, from an account that does not officially exist.', grantsFlag: 'vault_contents_found', grantsItem: 'senate_conspiracy_file' },
          { id: 'vault_east_access', propArt: 'cargo_lift', x: 25, y: 10, once: true, label: 'East Vault Exit', description: 'The secondary egress. A repulsor-lift is rigged to move the armor cache to a waiting transport. Whoever designed this operation planned every detail.', grantsFlag: 'vault_exit_found' },
          { id: 'syndicate_prisoner_log', propArt: 'board', propVariant: 'prisoners', x: 22, y: 15, once: true, label: 'Prisoner Transfer Log', description: 'Four detainees. Two transferred to Level 001 — the log does not say why. One released on Senate authority. One still listed as active: RIANNA, T. — Sub-Level 3, Block B.', grantsFlag: 'rianna_location_confirmed' },
        ],
        npcs: [
          { id: 'rianna_vault', x: 18, y: 10, kind: 'warden', label: 'Rianna',
            prompt: '"Back off! I told your people — I do not know any encryption keys! You burn this cell and you get nothing!"',
            repeatPrompt: 'Rianna stays pressed against the vault wall, pipe in hand, watching every shadow.',
            choices: [
              { text: '"Stand down. CSF Auxiliary. Kaelen sent me."', morality: 8, loyalty: { republic: 8 }, requires: { item: 'csf_aux_badge' }, result: '"CSF? Up on Level 1222 that badge means something. Down here it means you work for a better-funded cartel. Prove it." A pause. "Kaelen. He actually found someone." She lowers the pipe. "Lead the way."', grants: { flags: ['rianna_rescued'] } },
              { text: '"We have forty seconds before this room goes thermal. Keep the pipe. Walk now."', morality: 5, loyalty: {}, result: '"Clear and direct. I like you better than the goons." She moves.', grants: { flags: ['rianna_rescued'] } },
            ],
          },
        ],
        collectibles: [{ id: 'vault_credit_chip', x: 30, y: 18, label: 'Syndicate Operational Reserve Chip', reward: 200 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 34, 20, 'floor');
          carveRect(g, 11, 4, 25, 16, 'wall');
          carveRect(g, 12, 5, 24, 15, 'floor');
          pt(g, 11, 10, 'floor');
          pt(g, 25, 10, 'floor');
          carveRect(g, 6, 6, 6, 14, 'lava');
          carveRect(g, 28, 6, 28, 14, 'lava');
          pt(g, 18, 0, 'door'); pt(g, 19, 0, 'door');
          return g;
        },
      },
      csf_academy: {
        id: 'csf_academy', name: 'CSF Tactical Training Hub', subtitle: 'Coruscant · Republic District · CSF Precinct Command',
        width: 42, height: 30, spawnPos: { x: 2, y: 15 }, textureId: 'coruscant',
        accent: '#4A9FFF', accentGlow: 'rgba(74,159,255,0.2)', accentDim: '#1A4A80',
        floorColor: '#181C28', floorAlt: '#1E2430', wallDark: '#0A0C14', wallLight: '#141820',
        bg: 'radial-gradient(circle at 50% 30%, #10182A 0%, #080C14 70%)', ambient: 'traffic', floorPattern: 'polished',
        decor: ['archive', 'pillar', 'scan_arch'],
        doors: [],
        worldObjects: [
          { id: 'induction_terminal', propArt: 'console', propVariant: 'induction', x: 10, y: 6, once: true, label: 'CSF Induction Terminal', description: 'Your Auxiliary Corps enrollment is confirmed. Designation: AX-7. Access level: provisional. Supervisor: Vane, T. Welcome to the Coruscant Security Force.', grantsItem: 'csf_aux_badge' },
          { id: 'drill_holotable', propArt: 'tactical_table', propVariant: 'drill', x: 20, y: 6, once: false, label: 'Tactical Holotable', description: 'A 3D grid of Sector 4 showing current patrol routes, Black Sun safe house locations, and three markers labeled UNKNOWN — each in a different sub-level. Someone is mapping something.' },
          { id: 'holding_cell_log', propArt: 'board', propVariant: 'cellog', x: 9, y: 27, once: true, label: 'Cell Block Log', description: 'Entry 847: Detainee refuses to identify employing organization. Grey coat. No ID chip. Transferred off-site per Senate directive 1182-C. Authorized by: [REDACTED].' },
          { id: 'module_a_terminal', propArt: 'console', propVariant: 'module_a', x: 36, y: 12, once: true, requiresFlag: 'csf_briefed', label: 'Training Module A: Non-Lethal Combat', description: 'CSF Certification Module A. Simulated Syndicate droids are live on the training range in full-engagement mode. Stun configuration only — one lethal discharge ends your certification run. Droids do not telegraph. They do not pause. Clear the course clean.', triggersMinigame: 'pit_fight', minigameConfig: { opponentName: 'Syndicate Training Droid Mk.IV', opponentHp: 4, accent: '#4A9FFF' }, grantsFlag: 'module_a_complete' },
          { id: 'module_b_terminal', propArt: 'console', propVariant: 'module_b', x: 36, y: 16, once: true, requiresFlag: 'module_a_complete', label: 'Training Module B: Forensic Slicing', description: 'CSF Certification Module B. A reconstructed simulation of Docking Bay 14 — scorched manifests, altered routing codes, a ghost cargo layer in the customs log. The chain of custody is broken in three places. Find all three breaks and reconstruct the full trail. No assumptions. Only evidence.', triggersMinigame: 'signal_siphon', grantsItem: 'forensic_slicing_suite', grantsCodex: 'codex-csf-chain-of-custody', grantsFlag: 'module_b_complete' },
          { id: 'holding_block_b', propArt: 'interrogation_rig', x: 6, y: 24, once: true, requiresFlag: 'module_b_complete', label: 'Training Module C: High-Stress Interrogation', description: 'CSF Certification Module C. The Black Sun informant in Holding Block B has resisted four certified detectives. Thirty minutes. No weapons discharges. No civil rights violations on record. Break the suspect using psychological pressure before time runs out.', triggersMinigame: 'interrogation', grantsFlag: 'module_c_complete' },
          { id: 'senate_honor_ceremony', propArt: 'ceremony_dais', x: 20, y: 14, once: true, requiresFlag: 'csf_training_complete', label: 'Senate Honor Ceremony', description: 'Certification confirmed. The Senate Honor Cross is awarded in a holographic broadcast to Precinct Command. Officer Vane promotes you to Special Lead Investigator of the CSF Auxiliary Division. Sector 4 deployment is now authorized.', grantsFlag: 'vane_record_commend' },
          { id: 'airtaxi_csf_academy', propArt: 'airtaxi', propVariant: 'official', x: 38, y: 22, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal', description: 'Republic transit terminal. Authorized personnel only. Connects to the Coruscant AirTaxi Network.' },
          { id: 'precinct42_warrant_terminal', propArt: 'console', propVariant: 'warrant', x: 28, y: 22, once: true, iconKind: 'terminal', label: 'Precinct 42 Remote Terminal',
            requiresFlag: 'reya_warrant_mission_active',
            requires: { item: 'reya_warrant_disk' },
            description: 'A CSF remote access terminal with a clearance tunnel to the Level 1220 warrant archive. The terminal should not be accessible from the training wing, but someone wired it in wrong during the last facility expansion. The warrant ID on your disk matches an active record in the Precinct 42 database.',
            triggersMinigame: 'signal_siphon',
            grantsFlag: 'reya_warrant_cleared',
            grantsCodex: 'codex-reya' },
          { id: 'precinct_report_desk', propArt: 'datapad_table', propVariant: 'logbook', x: 10, y: 8, once: true, iconKind: 'terminal', label: 'Precinct Command Report Desk',
            requiresAllFlags: ['csf_training_complete', 'freight_hub_investigated'],
            lockedMessage: 'The sealed report tray waits for field findings. Finish your training and investigate Bay 14 first.',
            description: "The evidence tray beside Vane's desk, stamped with the Precinct Command seal. You file the Bay 14 findings in order: the stripped dock, the forged keycard, the sealed cargo door at Platform 09. The tray clicks shut and a breach authorization prints on the spool. Vane will see it within the hour.",
            grantsFlags: ['bay14_reported', 'sector4_raid_authorized'] },
        ],
        npcs: [
          { id: 'vane_academy', x: 6, y: 6, kind: 'republic_guard', label: 'Officer Vane',
            repeatPrompt: 'Vane is reviewing case files. He glances up. "Sector 4. We need that name."',
            phases: [
              { id: 'phase_commission',
                requiresNoneFlags: ['csf_briefed'],
                prompt: '"You made it. The Auxiliary Corps runs accelerated courses for candidates with field experience. You already have that. Walk through the drill yard and talk to the training sergeant."',
                choices: [
                  { text: '"I am ready to bring order to the underbelly, Detective."', morality: 8, loyalty: { republic: 10 }, result: '"Good. Start by keeping your eyes open and your mind off credits."', grants: { flags: ['csf_briefed', 'csf_duty_stance', 'speeder_transit_unlocked'], items: ['csf_aux_badge'] } },
                  { text: '"This badge better give me open access to restricted transport lanes."', morality: 3, loyalty: { republic: 5 }, result: '"It gives you authority and responsibility. Do not abuse it." He hands you the badge without ceremony.', grants: { flags: ['csf_briefed', 'csf_transit_unlocked', 'speeder_transit_unlocked'], items: ['csf_aux_badge'] } },
                  { text: '"Does this mean CSF will stay out of Jon\'s sector in the Mid-Levels?"', morality: 0, loyalty: { republic: 3 }, result: '"If your friend obeys Republic code, he has nothing to fear. If he does not, you will be the one arresting him." He watches your face carefully.', grants: { flags: ['csf_briefed', 'vane_suspicious_of_jon', 'speeder_transit_unlocked'], items: ['csf_aux_badge'] } },
                ],
              },
              { id: 'phase_post_commission',
                requiresAllFlags: ['csf_briefed'],
                requiresNoneFlags: ['echo7_vane_leverage_offered', 'csf_training_complete'],
                prompt: 'Vane looks up from his files. "Training modules are your priority. We need your clearance active before I can put you on Sector 4 assignment."',
                choices: [
                  { text: '"Understood. I will complete the modules."', morality: 5, loyalty: { republic: 5 }, result: '"That is what I want to hear. Torren is running the drill yard. Do not waste his time."' },
                  { text: '"What is happening in Sector 4 that needs this much preparation?"', morality: 3, loyalty: {}, result: 'He sets down his datapad. "Phantom freight manifests. Someone is moving unscanned cargo through the hub and the trail keeps going cold at Bay 14. That is all I can say until you have clearance."', grants: { flags: ['vane_sector4_hint'] } },
                ],
              },
              { id: 'phase_training_done',
                requiresAllFlags: ['csf_training_complete'],
                requiresNoneFlags: ['echo7_vane_leverage_offered'],
                prompt: '"All three modules cleared. Certification confirmed." He rises from his desk for the first time during your visit. "Special Lead Investigator, CSF Auxiliary Division." A brief, firm handshake. "Sector 4 Freight Hub is your assignment. Bay 14 manifests are wrong. Find out who is running the ghost cargo operation and bring me a name. Use your badge when you have to. Try not to have to."',
                choices: [
                  { text: '"Understood, Detective. I will find your name."', morality: 8, loyalty: { republic: 10 }, result: '"I know you will. The AirTaxi terminal is behind you. Sector 4 is waiting."', grants: { flags: ['csf_duty_stance', 'vane_record_commend', 'sector4_clearance_granted'] } },
                  { text: '"What happens if the evidence points at the wrong people?"', morality: 5, loyalty: { republic: 5 }, result: '"Bring it to me first. Not Internal Affairs. Not anyone outside this precinct." He waits. He wants an honest answer.', grants: { flags: ['csf_duty_stance', 'vane_confidential_channel'] } },
                  { text: '"Does this clearance extend to Shadow Town, Level 1312?"', morality: 3, loyalty: { underworld: 3 }, result: '"Your clearance extends wherever the investigation takes you." He sits back down. "Do not ask questions you already know the answer to."', grants: { flags: ['csf_duty_stance'] } },
                ],
              },
              { id: 'phase_confrontation',
                requiresAllFlags: ['csf_briefed', 'echo7_vane_leverage_offered'],
                requiresNoneFlags: ['echo7_clearance_acquired'],
                prompt: 'Vane straightens when he sees your expression. "You have been asking a lot of questions around Bay 14, spacer. Questions that go beyond your Auxiliary clearance." His hand moves toward his comlink. "You want to explain yourself before I make a call?"',
                choices: [
                  { text: '[Diplomatic] "I found evidence that clears your name, Detective. I want to help you, not expose you."', morality: 8, loyalty: { republic: 5 }, result: 'Something shifts behind his eyes. The hand moves away from the comlink. "Evidence." He says the word like he is tasting it. A long pause. "Transfer Code 99-Delta is flagged at sub-level access point 1313-J. If you find what is down there and it clears the record, you never spoke to me." He slides a keycard across the desk.', grants: { flags: ['echo7_clearance_acquired'], items: ['csf_clearance_pass_1313'] } },
                  { text: '[Neural Hijack] Echo-7 pushes forward in your mind. You feel it lock onto Vane. "Ask him about Transfer Code 99-Delta."', morality: -5, loyalty: { underworld: 8 }, requires: { flag: 'trait_neural_vessel' }, result: 'Vane stiffens. His eyes go distant. "Transfer Code... 99-Delta. Sub-level 1313-J." The words come out hollow, extracted. He blinks. He does not know what just happened. He stares at the keycard he somehow placed on the desk. You take it before he reconsiders.', grants: { flags: ['echo7_clearance_acquired', 'echo7_neural_hijack_used'], items: ['csf_clearance_pass_1313'] } },
                  { text: '[Intimidation] "The Syndicate ledger has your authorization codes on three separate transfer nights, Vane. Talk or this goes to Internal Affairs."', morality: -3, loyalty: { underworld: 5 }, requires: { item: 'syndicate_ledger' }, result: 'The color leaves his face. "You have no idea what you are holding." A beat. "You want access to Sub-Level 1313. Fine. But you owe me silence when this is over." He shoves a keycard at you without meeting your eyes.', grants: { flags: ['echo7_clearance_acquired', 'vane_compromised', 'ledger_leaked'], items: ['csf_clearance_pass_1313'] } },
                ],
              },
              { id: 'phase_post_clearance',
                requiresAllFlags: ['echo7_clearance_acquired'],
                prompt: 'Vane does not look up when you approach. "Whatever you find down there, I had no knowledge of it. That is my official position and I am keeping it." He turns back to his files.',
                choices: [
                  { text: '"Understood, Detective."', morality: 5, loyalty: { republic: 3 }, result: 'He gives a single short nod. The conversation is over.' },
                  { text: '"The ledger still exists, Vane. Remember that."', morality: -3, loyalty: { underworld: 5 }, result: '"I remember everything." His voice is flat. "Get out of my office."', grants: { flags: ['ledger_leaked'] } },
                ],
              },
              {
                id: 'phase_bay14_report',
                requiresAllFlags: ['csf_training_complete', 'freight_hub_investigated'],
                requiresNoneFlags: ['bay14_reported', 'echo7_vane_leverage_offered'],
                prompt: "Vane is waiting behind a desk stacked with sealed evidence trays, his coat already on. \"Investigator. Tell me what Bay 14 gave you. Everything, in order.\" He listens without interrupting, and when you finish he is silent for a long moment. \"The sealed cargo door at the back of the Freight Hub. The Syndicate staged their shipments behind it. I have a breach team on standby and a warrant that says I cannot use it without a witness on the floor.\" He slides a breach authorization across the desk. \"You are the witness.\"",
                choices: [
                  { text: "\"Give me the authorization. I will be at the cargo door.\"", morality: 5, loyalty: { republic: 5 }, result: "\"Platform 09, the sealed door beyond the blast marks. The team stacks up at your signal. Do not go in without them.\"", grants: { flags: ['bay14_reported', 'sector4_raid_authorized'] } },
                  { text: "\"What if what is behind that door implicates someone in the Precinct?\"", morality: 8, loyalty: { republic: 8 }, result: "He does not look away. \"Then it implicates them. I will take the heat for that. Go.\"", grants: { flags: ['bay14_reported', 'sector4_raid_authorized', 'vane_confidential_channel'] } },
                  { text: "\"Jon Vane's route runs through that door. You know that.\"", morality: -3, loyalty: { underworld: 3 }, result: "\"The route is not the man.\" Something tired crosses his face. \"I will decide about the man when I know what is inside. The authorization stands.\"", grants: { flags: ['bay14_reported', 'sector4_raid_authorized'] } },
                ],
                repeatPrompt: '"The breach team is at Platform 09. Do not make them wait."',
              },
            ],
          },
          { id: 'training_sgt', x: 22, y: 16, kind: 'republic_guard', label: 'Sergeant Torren',
            repeatPrompt: 'Torren watches the drill yard. He has eyes on everyone simultaneously.',
            phases: [
              { id: 'torren_pre_brief',
                requiresNoneFlags: ['csf_briefed'],
                prompt: '"Fresh Auxiliary. Vane vouches for you, which is the only reason I am not sending you back up the lift. Run the drill circuit. Keep your hands off the live-fire range until I clear you."',
                choices: [
                  { text: '"Yes, Sergeant. Where do I start?"', morality: 8, loyalty: { republic: 10 }, result: '"Talk to Vane first, get your commission, then come back. Perimeter circuit first. Then the module terminals." He almost smiles.', grants: { flags: ['drill_accepted'], items: ['csf_patrol_armor'] } },
                  { text: '"I have field experience. Skip the basics."', morality: 0, loyalty: {}, result: '"Talk to Vane first. Then come back and skip whatever you want." He turns away.' },
                ],
              },
              { id: 'torren_a_pending',
                requiresAllFlags: ['csf_briefed'],
                requiresNoneFlags: ['module_a_complete'],
                prompt: '"Module A is the first gate. East wall, upper terminal. Stun only — one lethal discharge and the certification run ends immediately. The droids move faster than you expect and they do not warn you before they engage. Use your environment."',
                choices: [
                  { text: '"Stun only. Understood."', morality: 5, loyalty: { republic: 5 }, result: '"Correct. Now go prove it."' },
                  { text: '"How fast do the combat droids move?"', morality: 3, loyalty: {}, result: '"Faster than the last three candidates expected. Do not be the fourth." He nods toward the east wall.' },
                ],
              },
              { id: 'torren_b_pending',
                requiresAllFlags: ['module_a_complete'],
                requiresNoneFlags: ['module_b_complete'],
                prompt: '"Module A cleared. Faster than the last three candidates." He delivers it like a criticism and a compliment at once. "Module B is forensic analysis. Bay 14 crime scene reconstruction — manifests altered, chain of custody broken in three places. Do not guess. Only evidence holds up in a prosecution."',
                choices: [
                  { text: '"Bay 14 — that is the real active case, is it not?"', morality: 5, loyalty: { republic: 8 }, result: '"You have been asking the right questions." He almost approves. "Good. The terminal will test whether you can read a scene or just react to one. East wall, lower position."', grants: { flags: ['vane_sector4_hint'] } },
                  { text: '"I will run the simulation now."', morality: 0, loyalty: {}, result: '"Minimize assumptions, maximize the evidence chain." He returns his attention to the drill yard.' },
                ],
              },
              { id: 'torren_c_pending',
                requiresAllFlags: ['module_b_complete'],
                requiresNoneFlags: ['module_c_complete'],
                prompt: '"Forensic module passed. Chain of custody reconstruction was solid." A pause to let that register. "Module C is different. The informant in Holding Block B has resisted four certified detectives. Observe before you speak. Every suspect has a fear and a pride — find both before you open your mouth. Hit them wrong and the wall goes back up permanently."',
                choices: [
                  { text: '"Fear and pride — which opens the door?"', morality: 5, loyalty: { republic: 5 }, result: '"Neither alone. The sequence matters. Fear first — that tells you what they are protecting. Pride tells you how. Holding Block B, south wall."', grants: { flags: ['torren_interrogation_advice'] } },
                  { text: '"I will handle it my way."', morality: 0, loyalty: {}, result: '"That is your right and your risk." He watches you go.' },
                ],
              },
              { id: 'torren_all_done',
                requiresAllFlags: ['module_a_complete', 'module_b_complete', 'module_c_complete'],
                requiresNoneFlags: ['csf_training_complete'],
                prompt: '"All three modules cleared." He crosses his arms. "I am not going to tell you that you did well, because this was training and what is waiting in Sector 4 does not grade on a curve. What I will tell you is: you did not cut corners. Report to Vane. He will finalize your certification."',
                choices: [
                  { text: '"Thank you, Sergeant."', morality: 5, loyalty: { republic: 5 }, result: '"Do not thank me. Bring back a clean record from Sector 4. That is the only thanks worth giving." He turns back to the drill yard.', grants: { flags: ['csf_training_complete'] } },
                  { text: '"Anything I should know about Sector 4?"', morality: 3, loyalty: {}, result: '"Phantom manifests. Ghost cargo layers in the Bay 14 logging system. Someone is very good at staying invisible. Be better." He nods once. "Dismissed."', grants: { flags: ['csf_training_complete'] } },
                ],
              },
              { id: 'torren_certified',
                requiresAllFlags: ['csf_training_complete'],
                prompt: '"You are certified. Sector 4 orders are with Vane." He studies you. "Keep the stun habit. The day you start defaulting to lethal, you have lost the only distinction between us and the organizations we are watching."',
                choices: [
                  { text: '"Understood, Sergeant."', morality: 5, loyalty: { republic: 3 }, result: '"Then go. The precinct does not patrol itself."' },
                ],
              },
            ],
          },
          { id: 'talo_voren', x: 10, y: 24, kind: 'republic_guard', label: 'Detective Talo Voren',
            repeatPrompt: 'Voren is annotating a case file. He does not look up.',
            prompt: 'He leans against the viewport glass watching the interrogation room, chewing a stim-stick. "Look at you — fresh boots, clean badge. You think we are fighting a war down here? It is a plumbing problem. You leak enough credits to the right bosses, the pipe stops bursting."',
            choices: [
              { text: '"If we ignore the small crimes, Black Sun controls the entire district."', morality: 8, loyalty: { republic: 5 }, result: '"Black Sun already controls the district. We just negotiate the rent. You will learn." He spits his stim-stick onto the floor and walks.' },
              { text: '"Where do you draw the line between keeping the peace and corruption?"', morality: 3, loyalty: {}, result: '"When civilians start dying. Until then? It is grease on the gears. Keep that in mind when you are out on Platform 12."', grants: { codex: ['codex-sector-4-freight-corridors'] } },
            ],
          },
          { id: 'kaelen_informant', x: 6, y: 26, kind: 'swoop_gang', label: 'Black Sun Informant "Kaelen"',
            repeatPrompt: '"I have nothing to add to my statement." He means it.',
            phases: [
              { id: 'kaelen_pre_interrogation',
                requiresNoneFlags: ['module_c_complete'],
                prompt: 'He sits handcuffed to the interrogation chair, staring at a fixed point on the wall. "I already told the last officer everything I know. Which is nothing. Run your evaluation module if you want. I will sit here."',
                choices: [
                  { text: '"The evaluation module is running."', morality: 3, loyalty: { republic: 3 }, result: '"I have heard that. Four times." He goes back to studying the wall.' },
                  { text: '"We already have your cylinder from Bay 14."', morality: 5, loyalty: { republic: 5 }, result: 'A flicker crosses his face — controlled quickly. "Then you do not need anything from me." He is lying. You can tell. The module evaluation will expose it.' },
                ],
              },
              { id: 'kaelen_broken',
                requiresAllFlags: ['module_c_complete'],
                requiresNoneFlags: ['dax_named'],
                prompt: 'The session changed something. He looks at you differently now — not hostile, just resigned. "Platform 09. Ask for Dax. He logged the phantom manifests. I was a courier. I did not know what was in the containers." He pauses. "If you are going to burn him, at least tell him I held out longer than usual."',
                choices: [
                  { text: '"Where does the manifest trail end?"', morality: 5, loyalty: { republic: 10 }, result: '"Bay 14. Cargo logging splits across three systems — official, CSF override, and a ghost layer that should not exist. Dax built the third layer. He is the one you actually want."', grants: { flags: ['dax_named', 'phantom_freight_available'] } },
                  { text: '"Talk fully and Vane will review your sentence."', morality: 8, loyalty: { republic: 8 }, result: '"In writing." He exhales. "Dax. Platform 09. Ghost layer in the manifest system — he built it. That is everything I have." He looks at the wall again. "I am done."', grants: { flags: ['dax_named', 'phantom_freight_available', 'legal_pressure_used'] } },
                ],
              },
              { id: 'kaelen_post_intel',
                requiresAllFlags: ['dax_named'],
                prompt: '"You have what you need. Dax, Platform 09." He has closed down again — not resistant, just finished. "I cooperated. Remember that when my case comes up."',
                choices: [
                  { text: '"Cooperation noted."', morality: 5, loyalty: { republic: 3 }, result: 'He nods once. Nothing more to say.' },
                  { text: '"Do not count on anything."', morality: -3, loyalty: {}, result: '"Of course not." His tone is flat. "No one does in this precinct."' },
                ],
              },
            ],
          },
          { id: 'csf_medic', x: 36, y: 10, kind: 'mechanic', label: 'Field Medic Daya',
            repeatPrompt: 'Daya is annotating injury reports. There are many.',
            prompt: '"Injuries are a policy violation in the training center. Which is convenient since I only have bacta patches and sarcasm."',
            choices: [
              { text: 'Ask for a medpac for the field.', morality: 3, loyalty: {}, result: '"Sign the requisition form. The form requires clearance. The clearance requires a supervisor signature. The supervisor is at lunch." She hands you one anyway. "This did not happen."' },
              { text: 'Ask what the injury rate is in the field.', morality: 0, loyalty: {}, result: '"For Auxiliaries? Ask someone who came back." She returns to her datapad.' },
            ],
          },
        ],
        collectibles: [],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 40, 28, 'floor');
          carveRect(g, 2, 2, 14, 10, 'wall');
          carveRect(g, 3, 3, 13, 9, 'floor');
          carveRect(g, 32, 4, 40, 18, 'wall');
          carveRect(g, 33, 5, 39, 17, 'floor');
          carveRect(g, 2, 22, 12, 28, 'wall');
          carveRect(g, 3, 23, 11, 27, 'floor');
          pt(g, 14, 6, 'floor');
          pt(g, 32, 11, 'floor');
          pt(g, 7, 22, 'floor');
          return g;
        },
      },
      lower_sky_market: {
        id: 'lower_sky_market', name: 'Lower Sky-Market Promenade', subtitle: 'Coruscant · Lower Mid-Levels · L.1100',
        width: 38, height: 26, spawnPos: { x: 2, y: 13 }, textureId: 'coruscant',
        accent: '#FF0055', accentGlow: 'rgba(255,0,85,0.22)', accentDim: '#660022',
        floorColor: '#1A0C14', floorAlt: '#22101C', wallDark: '#0C0608', wallLight: '#180C10',
        bg: 'radial-gradient(circle at 50% 40%, #1A080E 0%, #080406 70%)', ambient: 'neon_haze', floorPattern: 'rough',
        decor: ['neon_sign', 'brazier', 'pillar', 'pipe', 'cargo_crate'],
        doors: [
          { x: 36, y: 10, targetZone: 'slicer_alleyway', targetPos: { x: 2, y: 10 }, label: 'Slicer Alleyway' },
        ],
        worldObjects: [
          { id: 'marlo_hideout_board', propArt: 'board', propVariant: 'ops', x: 4, y: 10, once: false, label: 'Ops Planning Board', description: 'A holographic layout of three Coruscant levels. Marlo\'s territory in red. Rook\'s in blue. Significant overlap. Someone has been drawing lines.' },
          { id: 'rook_comms_terminal', propArt: 'console', propVariant: 'rook', x: 28, y: 16, once: true, label: "Rook's Comm Array", description: 'The speeder nav system is wired into this terminal. One code cylinder could redirect his entire route.' },
          { id: 'airtaxi_lower_sky_market', propArt: 'airtaxi', propVariant: 'worn', x: 20, y: 24, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal', description: 'Coruscant AirTaxi Network. Exit from lower promenade.' },
        ],
        npcs: [
          { id: 'ph_merchant_ro', x: 19, y: 13, kind: 'archivist', label: 'Tamsin Ro, Fine Art and Fixtures',
            isVendor: true, vendorFaction: 'underworld', repeatable: true, requiresFlag: 'inheritance_active',
            vendorStock: ['ph_pkg_gallery', 'ph_pkg_trophy'],
            vendorGreeting: "\"Every wall tells a story. I can make yours tell a better one. Canvases, pedestals, trophy cases, all with provenance. Mostly.\"",
            repeatPrompt: "Tamsin Ro arranges a frame by a hair and steps back to admire it.",
            prompt: "\"Every wall tells a story. I can make yours tell a better one. Canvases, pedestals, trophy cases, all with provenance. Mostly.\"",
            choices: [] },
          { id: 'marlo_1100', x: 8, y: 8, kind: 'broker', label: '"Slick" Marlo',
            repeatPrompt: 'Marlo watches the promenade through a one-way panel. He is always watching.',
            prompt: '"Welcome to the neighborhood. Rook operates out of the east side. He is Black Sun, old guard — thinks the territory is his because it was his father\'s. I disagree. Here is what I need from you."',
            choices: [
              { text: '"What exactly are we talking about doing to Rook?"', morality: -5, loyalty: { underworld: 12 }, result: '"Nothing flashy. His speeder runs a fixed route every night cycle. You slice the navcom and redirect it into a traffic barrier. Looks like an accident. Clean."', grants: { flags: ['rook_mission_briefed'] } },
              { text: '"I will look into it. No promises."', morality: 0, loyalty: { underworld: 5 }, result: '"Promises are for people who have something to lose. Just do it."', grants: { flags: ['rook_mission_observed'] } },
            ],
          },
          { id: 'rook_contact', x: 30, y: 17, kind: 'crime_boss', label: '"Rook"',
            repeatPrompt: '"Rook" has security droids flanking him now. He has heard enough.',
            prompt: 'He does not turn around. "You are either very brave or very stupid for coming in here. Which is it?"',
            choices: [
              { text: '"Marlo sent me. But I am not here to finish the job he thinks I am here for."', morality: 10, loyalty: { republic: 8 }, result: '"Interesting. You are telling me Marlo wants me removed. You could have just done it and collected. What are you after?"', grants: { flags: ['rook_warned'] } },
              { text: 'Carry out the mission: slice his comm array to reroute his speeder.', morality: -20, loyalty: { underworld: 20 }, result: 'The job is clean. The report in the morning cycles lists a speeder malfunction. Marlo\'s territory expands by morning. You do not sleep well.', grants: { flags: ['rook_eliminated'] } },
            ],
          },
        ],
        collectibles: [{ id: 'gang_cred', x: 18, y: 20, label: 'Stashed Credit Brick', reward: 150 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 36, 24, 'floor');
          carveRect(g, 2, 3, 14, 12, 'wall');
          carveRect(g, 3, 4, 13, 11, 'floor');
          carveRect(g, 24, 12, 36, 22, 'wall');
          carveRect(g, 25, 13, 35, 21, 'floor');
          pt(g, 14, 8, 'floor');
          pt(g, 24, 17, 'floor');
          pt(g, 36, 10, 'door');
          return g;
        },
      },
      senate_district: {
        id: 'senate_district', name: 'Senate Precinct L.1900', subtitle: 'Coruscant · Upper Levels · Senate District',
        width: 44, height: 32, spawnPos: { x: 2, y: 16 }, textureId: 'coruscant',
        accent: '#4A9FFF', accentGlow: 'rgba(74,159,255,0.20)', accentDim: '#1A4A80',
        floorColor: '#181C28', floorAlt: '#1E2430', wallDark: '#0A0C14', wallLight: '#141820',
        bg: 'radial-gradient(circle at 50% 30%, #10182A 0%, #080C14 70%)', ambient: 'traffic', floorPattern: 'marble',
        decor: ['pillar', 'archive', 'scan_arch'],
        doors: [],
        worldObjects: [
          { id: 'senate_phantom_freight_file', propArt: 'archive_cabinet', propVariant: 'dossier', x: 4, y: 4, once: true, iconKind: 'archive', label: 'Committee Dossier 7-Theta',
            requiresFlag: 'echo7_stakes_known',
            description: 'A sealed committee dossier marked PHANTOM FREIGHT OVERSIGHT (CLASSIFIED). The file number matches one of the suppressed archives Echo-7 described. Three pages are intact. They contain the names of six Senate subcommittee members who co-signed a standing exemption order for the Bay 14 cargo pathway, renewed every fiscal cycle for thirty years. One name is currently serving as the Deputy Chair of the Senate Commerce Authority.',
            grantsFlag: 'phantom_freight_senate_link',
            grantsCodex: 'codex-senate-precinct' },
          { id: 'holo_news_kiosk_senate', propArt: 'kiosk', propVariant: 'senate', x: 10, y: 8, once: false, iconKind: 'terminal', label: 'HNN Kiosk — Senate Wing',
            description: '[HNN PRIORITY] "The Senate Commerce Authority has issued a statement calling the Bay 14 irregularities an administrative matter under routine review. Deputy Chair Torvenn described media coverage as irresponsible speculation. In unrelated news, fourteen tonnes of unmanifested cargo cleared Coruscant Customs without inspection under an emergency security protocol." The terminal has three active response threads. All of them have been flagged by Senate information security.' },
          { id: 'senate_surveillance_array', propArt: 'console', propVariant: 'surveil', x: 20, y: 6, once: true, iconKind: 'terminal', label: 'HoloNet Surveillance Hub',
            requiresFlag: 'vane_sublevel_connection_found',
            description: 'A Commerce Authority surveillance node routing encrypted monitoring data to four Senate offices simultaneously. The uplink log shows Vane\'s inspection report from eleven years ago was accessed twice in the last forty-eight hours, both times from a terminal registered to Deputy Chair Torvenn\'s office.',
            triggersMinigame: 'signal_siphon',
            grantsFlag: 'senate_surveillance_tapped',
            grantsCodex: 'codex-senate-precinct' },
          { id: 'senate_classified_briefing', propArt: 'archive_cabinet', propVariant: 'sealed', x: 35, y: 6, once: true, iconKind: 'archive', label: 'Sealed Briefing Chamber',
            requiresFlag: 'senate_access_granted',
            description: 'A secure briefing room last active three months ago. The session transcript is still cached in the chamber\'s closed-loop system. Twelve senators. One agenda item: how to contain the Level 1313 access question before it reached the standing committee. The resolution passed eleven to one. The dissenting vote is not attributed to any name in the record. That space is blank, as if someone removed the attribution before the session closed.',
            grantsFlag: 'senate_dissent_found',
            grantsCodex: 'codex-senate-precinct' },
          { id: 'senate_committee_minutes', propArt: 'archive_cabinet', propVariant: 'minutes', x: 35, y: 8, once: true, iconKind: 'datapad', label: 'Committee Meeting Log',
            description: 'Three years of Commerce Authority subcommittee meeting logs. Cross-referenced against the Bay 14 manifest irregularities, they form a clear pattern: every cargo review request that touched Bay 14 was tabled, deferred, or simply removed from the agenda before discussion. The removals are stamped as routine calendar management. The name authorising them appears twenty-three times. It is always the same: Deputy Chair Torvenn.',
            grantsFlag: 'torvenn_named',
            requiresFlag: 'phantom_freight_senate_link' },
          { id: 'senate_archive_core', propArt: 'server_stack', propVariant: 'archive', x: 8, y: 24, once: true, iconKind: 'terminal', label: 'Archive Sub-Node',
            requiresFlag: 'echo7_vault_details_known',
            description: 'A secondary archive node running a fragmented index of the original Level 1313 records purge. The index itself survived the purge because it was stored as a maintenance file rather than a document record. It lists four hundred and twelve archive entries marked DESTROYED in the purge order. The last twelve entries in the list are marked LOCATION UNVERIFIED. They include a line item reading: ARCHIVIST CONSCIOUSNESS TRANSFER PROTOCOL. AUTHORIZED. SIGNED: TORVENN, W.',
            grantsFlag: 'echo7_archive_origin_found',
            grantsCodex: 'codex-senate-precinct' },
          { id: 'senate_bribe_ledger', propArt: 'archive_cabinet', propVariant: 'ledger', x: 15, y: 26, once: true, iconKind: 'archive', label: 'Commerce Authority Payment Log',
            requiresFlag: 'vault_heist_complete',
            description: 'A physical payment ledger from the Commerce Authority bursary. Forty-three named accounts. Cross-referenced against the Iron Syndicate bribe schedule from Jon\'s vault, eleven of them match. The accounts belong to clerks, inspectors, and subcommittee aides across four Senate offices. The money moved from Syndicate accounts through three shell companies before landing in Republic payroll. Someone built this channel over years. It did not happen by accident.',
            grantsFlag: 'bribe_network_mapped',
            grantsCodex: 'codex-senate-precinct' },
          { id: 'senate_maintenance_access', propArt: 'access_hatch', propVariant: 'panel', x: 33, y: 27, once: true, iconKind: 'panel', label: 'Emergency Maintenance Shaft',
            requiresNoneFlags: ['senate_maintenance_opened'],
            description: 'A panel behind the archive stacks, unlocked. A maintenance shaft runs down through the precinct sub-structure. Access logs show it has not been officially entered in eleven years. The dust on the interior ladder is disturbed. Someone has been using this route without signing in.',
            grantsFlag: 'senate_maintenance_opened',
            grantsCodex: 'codex-senate-precinct' },
          { id: 'senate_guard_post', propArt: 'guard_post', x: 6, y: 16, once: false, iconKind: 'prop', label: 'Republic Guard Post',
            description: 'Standard Senate precinct guard station. Emergency response time is four minutes at this level. The duty log shows three unlogged visitor entries in the last week, each flagged by the guard on duty and subsequently deleted from the official record by a Commerce Authority clearance override. The guard who flagged them is listed as transferred. The destination is blank.' },
          { id: 'airtaxi_senate_district', propArt: 'airtaxi', propVariant: 'clean', x: 37, y: 25, once: false, iconKind: 'beacon', label: 'Senate Express Terminal',
            description: 'A high-security transit terminal with biometric clearance requirements. The Republic crest above the boarding gate is polished. Everything here is polished. That is the point.' },
        ],
        npcs: [
          { id: 'undersecretary_meln', x: 7, y: 6, kind: 'senator', label: 'Undersecretary Meln',
            questNpc: true,
            repeatPrompt: 'Meln arranges a set of datafiles with practiced precision. His movements are economical. Every gesture is a calculation.',
            phases: [
              {
                id: 'phase_meln_initial',
                requiresNoneFlags: ['phantom_freight_senate_link', 'meln_confronted'],
                prompt: 'The Muun undersecretary turns from his terminal with a smile that reaches exactly nowhere. "The precinct is restricted to authorised personnel. You have authorisation, or you would not be here. That is the extent of my interest in why you are here." He returns to his filing. The deliberate dismissal of a man who has decided you are not yet a problem.',
                choices: [
                  { text: '"I am looking into the Bay 14 irregularities."', morality: 5, loyalty: { republic: 3 }, result: '"Bay 14 is under routine Commerce Authority review." He does not look up. "Any further inquiries should be directed to the subcommittee clerk. That office is located on Level 1880. Good day."', grants: {} },
                  { text: '"Who authorises clearance for the archive sub-nodes?"', morality: 0, loyalty: {}, result: '"Archive access is managed by the precinct archivist. Dax. If Dax is available." He pauses just slightly. "Dax has not been available this week." The pause was not an accident.', grants: { flags: ['dax_referenced_by_meln'] } },
                  { text: 'Leave without a word.', grants: {} },
                ],
              },
              {
                id: 'phase_meln_evidence_found',
                requiresAllFlags: ['phantom_freight_senate_link'],
                requiresNoneFlags: ['meln_confronted', 'meln_turned'],
                prompt: 'Meln notices the dossier you are carrying before you speak. Something shifts behind his eyes, carefully controlled. "You have been in the committee archive." It is not a question. He sets down his datafiles. For the first time, he gives you his full attention. "What do you want?"',
                choices: [
                  { text: '"I want to know who Torvenn is protecting."', morality: 5, loyalty: { republic: 8 }, result: '"Torvenn." He repeats the name without expression. A very long pause. "Deputy Chair Torvenn has been managing the Bay 14 oversight exemption for nineteen years. I co-signed four of the renewal orders. I want you to understand what I am telling you when I say that." He holds your gaze. "I am telling you, not running."', grants: { flags: ['meln_confronted', 'meln_cooperation_offered'] } },
                  { text: '"I want this to go away. What is it worth to you?"', morality: -10, loyalty: { underworld: 5 }, result: 'He studies you for five seconds. Then he reaches into his desk and sets a credit chip on the surface between you. "Fifty thousand. And you walk out of this precinct and never return." He is very still. He has done this before.', grants: { flags: ['meln_confronted', 'meln_bribed'], items: ['senate_bribe_chip'] } },
                ],
              },
              {
                id: 'phase_meln_cooperation',
                requiresAllFlags: ['meln_cooperation_offered'],
                requiresNoneFlags: ['meln_turned', 'meln_bribed'],
                prompt: '"If you are building a case, you need the renewal order chain and the original clearance authorisation from thirty years ago. Both are in the sealed archive under a Commerce Authority retention hold. I cannot access them without triggering an alert to Torvenn\'s office. But the precinct archivist, Dax, has a separate retention key that predates the hold." He pauses. "Dax knows what is in there. Dax has known for a long time."',
                choices: [
                  { text: '"Why are you telling me this?"', morality: 5, loyalty: { republic: 5 }, result: '"Because I am sixty-three years old and I am tired of the weight of it." He says it without drama. "Find Dax. And be careful. Torvenn has eyes in this precinct."', grants: { flags: ['meln_turned', 'dax_location_known'] } },
                  { text: '"I will find Dax."', grants: { flags: ['meln_turned', 'dax_location_known'] } },
                ],
              },
              {
                id: 'phase_meln_turned',
                requiresAllFlags: ['meln_turned'],
                prompt: '"Dax knows where the original authorisation is. The retention key is the only thing standing between the sealed archive and you." He keeps his voice flat and level. In this building, that is the most dangerous thing a person can be.',
                choices: [{ text: '"I understand. Thank you."', grants: {} }],
                repeatPrompt: 'Meln gives a single, precise nod. He has said what he had to say.',
              },
              {
                id: 'phase_meln_bribed',
                requiresAllFlags: ['meln_bribed'],
                requiresNoneFlags: ['meln_turned'],
                prompt: 'Meln does not look up from his terminal. A chip the size of a thumbnail has already vanished from his desk. "We have nothing further to discuss. If anyone asks, we never did."',
                choices: [{ text: 'Leave without a word.', grants: {} }],
                repeatPrompt: 'Meln keeps his eyes on the screen. The conversation did not happen.',
              },
            ],
          },
          { id: 'archivist_dax', x: 10, y: 24, kind: 'archivist', label: 'Dax',
            questNpc: true,
            requiresFlag: 'dax_location_known',
            repeatPrompt: 'Dax is surrounded by physical archive canisters arranged in an order that makes sense only to him. He does not stop working when you approach.',
            phases: [
              {
                id: 'phase_dax_initial',
                requiresAllFlags: ['dax_location_known'],
                requiresNoneFlags: ['echo7_archive_origin_found', 'dax_key_granted'],
                prompt: 'The old archivist looks up from a canister he has been cataloguing. His eyes move to the dossier in your hands, then to your face. He does not seem surprised. "I have been waiting for someone to come down here and ask about this for eleven years," he says. "Every year I thought: this is the year. And every year it was not." He sets down the canister. "Meln sent you."',
                choices: [
                  { text: '"Tell me about the Level 1313 archive purge."', morality: 5, loyalty: { republic: 5 }, result: '"Thirty-one years ago, a clearance order came through this office authorising the permanent destruction of four hundred and twelve archive entries classified under the Old Republic Historical Continuity Act. I was a junior archivist. My supervisor signed it and retired three weeks later. I kept a copy of the index." He is quiet for a moment. "The man who authorised it is now Deputy Chair of the Commerce Authority. His name is Torvenn Wald."', grants: { flags: ['torvenn_full_name_known'], codex: ['codex-senate-precinct'] } },
                  { text: '"Do you have the original clearance authorisation?"', morality: 0, loyalty: {}, result: '"The original document is in the sealed sub-archive. The Commerce Authority retention hold prevents me from accessing it through official channels." He opens a desk drawer. "But I made a physical copy thirty-one years ago and put it in a canister marked FISCAL MAINTENANCE RECORDS 87-C, and nobody has ever looked for it." He sets a sealed canister on the desk between you.', grants: { flags: ['torvenn_authorisation_found', 'senate_access_granted'], codex: ['codex-senate-precinct'] } },
                ],
              },
              {
                id: 'phase_dax_archive_found',
                requiresAllFlags: ['echo7_archive_origin_found'],
                requiresNoneFlags: ['dax_key_granted'],
                prompt: '"You found the sub-node index." He reads your expression correctly. "Then you know what ARCHIVIST CONSCIOUSNESS TRANSFER PROTOCOL means." He opens a second drawer. Inside it: a single data key on a plain chain. "This is the retention key. It predates the Commerce Authority hold. With it, you can access the full sealed archive and pull everything that was supposed to be destroyed." He holds it out to you. "Take it. I have been carrying it long enough."',
                choices: [
                  { text: 'Take the retention key.', morality: 5, loyalty: { republic: 8 }, result: '"Use it well." He goes back to his canisters. He does not watch you leave. He has been waiting to put this down for thirty-one years and now he has.', grants: { flags: ['dax_key_granted', 'senate_archive_unlocked'], items: ['senate_retention_key'] } },
                  { text: '"What happens to you when this comes out?"', morality: 5, loyalty: {}, result: '"I am sixty-eight years old and I have spent thirty-one years filing documents in a basement nobody visits." He almost smiles. "Whatever happens to me, it will be more interesting than this." He holds out the key.', grants: { flags: ['dax_key_granted', 'senate_archive_unlocked'], items: ['senate_retention_key'] } },
                ],
              },
              {
                id: 'phase_dax_key_granted',
                requiresAllFlags: ['dax_key_granted'],
                prompt: '"The retention key is yours. Everything in the sealed archive is accessible now. Do what you came here to do."',
                choices: [{ text: '"I will."', grants: {} }],
                repeatPrompt: '"Go. The archive will not organise itself. Neither will the case against Torvenn."',
              },
            ],
          },
        ],
        collectibles: [{ id: 'senate_precinct_datachip', x: 30, y: 22, label: 'Precinct Access Datachip', reward: 60 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 13, 42, 18, 'floor');
          carveRect(g, 2, 2, 12, 11, 'floor');
          pt(g, 7, 12, 'floor');
          carveRect(g, 16, 2, 27, 11, 'floor');
          pt(g, 21, 12, 'floor');
          carveRect(g, 31, 2, 41, 11, 'floor');
          pt(g, 36, 12, 'floor');
          carveRect(g, 5, 20, 18, 29, 'floor');
          pt(g, 10, 19, 'floor');
          carveRect(g, 22, 20, 39, 29, 'floor');
          pt(g, 30, 19, 'floor');
          return g;
        },
      },
      slicer_alleyway: {
        id: 'slicer_alleyway', name: 'Slicer Alleyway', subtitle: 'Coruscant · Mid-Level Underbelly · L.1150',
        width: 34, height: 20, spawnPos: { x: 2, y: 10 }, textureId: 'coruscant',
        accent: '#00FF99', accentGlow: 'rgba(0,255,153,0.20)', accentDim: '#006633',
        floorColor: '#0A1210', floorAlt: '#0F1A16', wallDark: '#050A07', wallLight: '#0E1612',
        bg: 'radial-gradient(circle at 30% 60%, #071410 0%, #030806 70%)', ambient: 'datastream', floorPattern: 'grid',
        decor: ['cable_bundle', 'pipe', 'girder', 'scan_arch'],
        doors: [
          { x: 1, y: 10, targetZone: 'lower_sky_market', targetPos: { x: 35, y: 10 }, label: 'Lower Promenade' },
          { x: 33, y: 10, targetZone: 'level_1313_subvault', targetPos: { x: 1, y: 9 }, label: 'Level 1313 Access' },
          { x: 33, y: 6, targetZone: 'shadow_town', targetPos: { x: 2, y: 12 }, label: 'Shadow Town' },
        ],
        worldObjects: [
          { id: 'ancient_broadcast_terminal', propArt: 'console', propVariant: 'derelict', x: 8, y: 4, once: true, iconKind: 'terminal', label: 'Derelict Broadcast Terminal',
            description: 'An old Republic emergency-band relay terminal, abandoned and overgrown with data cable. The display flickers. An audio signal is running on a frequency that stopped being authorised eighty years ago.',
            grantsFlag: 'echo7_found', grantsItem: 'echo_7_core',
            autoCodex: { id: 'codex-echo-7', title: 'Echo-7: The Preserved Mind', category: 'dossier', summary: 'An ancient AI core carrying the uploaded consciousness of a Republic archivist.', body: ['The broadcast signature matches a protocol droid core designation that was flagged as decommissioned in Republic records three centuries ago. The core is still transmitting. It says it has something important to tell you.'] } },
          { id: 'overrun_server_stack', propArt: 'server_stack', propVariant: 'black_market', x: 20, y: 6, once: true, iconKind: 'terminal', label: 'Overrun Server Stack',
            description: 'A rack of black-market server nodes bolted to the wall. Echo-7 says the first memory fragment is stored here. The data is encrypted behind a layered slicer lock.',
            triggersMinigame: 'signal_siphon', grantsFlag: 'echo7_node1_extracted',
            requiresFlag: 'echo7_found' },
          { id: 'penthouse_relay_tap', propArt: 'ceiling_tap', x: 28, y: 14, once: true, iconKind: 'panel', label: 'Sky-Market Penthouse Tap',
            description: 'A hardwire tap running upward through the ceiling into the Sky-Market penthouse tower. This is how the second memory fragment was rerouted here to keep it hidden. Getting it out means cracking the tower relay.',
            triggersMinigame: 'signal_siphon', grantsFlag: 'echo7_node2_extracted',
            requiresFlag: 'echo7_node1_extracted' },
          { id: 'csf_ambush_point', propArt: 'guard_post', x: 28, y: 10, iconKind: 'beacon', label: 'CSF Courier Checkpoint',
            requiresFlag: 'heat_high_active', requiresNoneFlags: ['ambush_survived'],
            lockedMessage: 'An empty stretch of alley with a tripod and a dead floodlight. Nothing is happening here yet.',
            description: "A CSF courier checkpoint, thrown up overnight: a tripod floodlight, a fold out barrier and four troopers who were not here yesterday. Syndicate heat has put your name on a very long list.",
            triggersMinigame: 'tactical_combat',
            minigameConfig: { opponentProfile: 'csf_swat', flavorText: 'The floodlight snaps on. Four silhouettes spread across the alley and a voice you have heard on the CSF net says your name. This is the courier ambush and it was always going to find you.' },
            grantsFlag: 'ambush_survived', heatDeltaOnSuccess: -15 },
          { id: 'oza_shop_terminal', propArt: 'console', propVariant: 'oza', x: 6, y: 15, once: false, iconKind: 'terminal', label: "OZA-2's Shop Display",
            description: 'A battered terminal listing available hardware with prices that do not appear in any legitimate catalogue. Matte-black casing. Red indicator lights. A hand-written sign reads: LEGAL INQUIRIES DELETED ON RECEIPT.' },
          { id: 'airtaxi_slicer_alley', propArt: 'airtaxi', propVariant: 'jury', x: 30, y: 3, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal',
            description: 'An unmarked terminal spliced into the legitimate AirTaxi network. No one on record approved this installation.' },
          { id: 'slicer_alley_graffiti', propArt: 'wall_marks', propVariant: 'alley', x: 14, y: 2, once: false, iconKind: 'prop', label: 'Alleyway Wall Markings',
            description: 'The wall is layered with Aurebesh tags, data-runner insignia, and at least three separate gang territorial marks crossed out by the next. Newest text reads: THIS ALLEY BELONGS TO NO ONE. KEEP YOUR HANDS OFF THE HARDWARE.' },
          { id: 'slicer_alley_abandoned_pad', propArt: 'datapad_table', propVariant: 'warm', x: 22, y: 17, once: true, iconKind: 'datapad', label: 'Abandoned Datapad',
            description: 'Left on a crate and still warm. Partial download log. Someone was pulling records from a Bay 14 customs node three hours ago, then stopped mid-operation. The last entry reads: "They found the marker. Going dark."',
            grantsFlag: 'slicer_trail_found',
            autoCodex: { id: 'codex-slicer-alleyway', title: 'The Slicer Alleyway Network', category: 'lore', summary: 'A hidden corridor of data-runners below the Sky-Market towers.', body: ['The datapad belongs to a regular of the alleyway network. The stopped download and the last log entry suggest whoever was running this operation knew something was coming.'] } },
          { id: 'holo_news_kiosk', propArt: 'kiosk', propVariant: 'slicer', x: 16, y: 10, once: false, iconKind: 'terminal', label: 'HNN Kiosk — Slicer Alley',
            description: '[HNN PRIORITY UPDATE] "Senate Committee on Information Security has voted to expand encrypted-band monitoring across all civilian relay networks. The measure passed nine to three. Officials called it a necessary step for public safety." The screen is cracked. Someone stamped it with a Black Sun stencil.' },
        ],
        npcs: [
          { id: 'oza2', x: 6, y: 14, kind: 'droid', label: 'OZA-2',
            repeatPrompt: 'OZA-2\'s red eye lens dims and brightens in a slow, evaluating cycle. "Back again. Bring something worth trading this time."',
            prompt: 'The matte-black droid turns with a mechanical grinding sound. A red wiring bundle hangs loose from its left shoulder. "You are not CSF. Good. CSF-shaped inquiries are deleted on receipt. I deal in hardware, not questions."',
            choices: [
              { text: '"What have you got worth buying, OZA?"', morality: 0, loyalty: {}, result: 'The droid gestures at its terminal. "Slicer Spike Mk.II — fifteen-second extension, cleaner signal. Subdermal armor chip — black market, not painful to install, mostly. Both at honest prices, which in this alley means overpriced but reliable."', grants: {} },
              { text: '"I found something in an old terminal up the alley. An echo of something."', morality: 0, loyalty: {}, result: '"An echo." The droid\'s eye brightens. "If what you found is what I suspect, you should be careful with it. Very old things have very old agendas. I would know. I am one of them." It does not elaborate.', grants: { flags: ['oza2_echo_hinted'] } },
              { text: '"Do you know anything about the shaft below Bay 14?"', morality: 0, loyalty: { underworld: 4 }, result: '"Coordinates have been circulating for weeks. A Rodian named Garrus has been selling them in the Sky-Market. What is down there is a question the alley has opinions about but no proof. The proof is below, sealed and breathing."', grants: { flags: ['garrus_named_by_oza2'] } },
            ],
          },
          { id: 'echo7_npc', x: 22, y: 10, kind: 'droid', label: 'Echo-7', hideAfterFlags: ['echo7_climax_reached'],
            requiresFlag: 'echo7_found',
            repeatPrompt: '"I am still here, Vessel. The fragments await. My patience was calibrated for centuries."',
            phases: [
              {
                id: 'phase_found',
                requiresAllFlags: ['echo7_found'],
                requiresNoneFlags: ['echo7_node1_extracted'],
                prompt: 'The protocol droid stands perfectly still, but its eye illuminates with an unexpected warmth. "You found the terminal. Good. I have been broadcasting on that frequency for eleven years. You are the first to stop and listen. My name is Echo-7. I was an archivist once. There is a great deal of history I have been saving for someone worth saving it for."',
                choices: [
                  { text: '"What are you, exactly?"', morality: 5, loyalty: {}, result: '"I am a Republic archivist named Heren Mast, compressed into a droid housing during a purge. My records span three centuries of suppressed history: corruption, assassination, and the institutional dishonesty that convinced the Republic it was righteous while it rotted. The data is fragmented across three nodes. I need your help to reassemble it."', grants: { flags: ['echo7_mission_accepted'] } },
                  { text: '"How do I know this is not a slaver trap?"', morality: 0, loyalty: {}, result: '"You cannot. That is an accurate assessment of your situation. But consider: if this were a slaver trap, I would have offered credits rather than history. History is a far less reliable lure. The first node is in the server stack at the end of this alley."', grants: { flags: ['echo7_mission_accepted'] } },
                  { text: '"I am not interested."', morality: 0, loyalty: {}, result: '"You will be. The data I carry includes the name of the person who authorised the Bay 14 strike three years ago. When you are ready, I will be here."', grants: {} },
                ],
              },
              {
                id: 'phase_node1',
                requiresAllFlags: ['echo7_node1_extracted'],
                requiresNoneFlags: ['echo7_node2_extracted'],
                prompt: '"First fragment restored. Well done. You move through encrypted space like someone who has done this before. The second fragment was rerouted through a penthouse tower relay in the Sky-Market. Access the tap at the far end of this alley. It will require patience and a clean signal line."',
                choices: [
                  { text: '"What is in the fragments?"', morality: 5, loyalty: {}, result: '"Evidence. Senate financial records. Names of Republic officials who accepted Iron Syndicate payments during the Jedi Wars. Names that still hold office today. When the full archive is assembled, you will have in your hands a case that the Republic has been trying to destroy for three hundred years."', grants: { flags: ['echo7_stakes_known'] } },
                  { text: '"Why does this matter now? The Jedi Wars ended long ago."', morality: 0, loyalty: {}, result: '"The corruption did not end when the wars did. The same families. The same shell companies. Different names on the accounts. I am not preserving history. I am preserving an active investigation that someone tried to stop before it finished."', grants: { flags: ['echo7_stakes_known'] } },
                ],
              },
              {
                id: 'phase_node2',
                requiresAllFlags: ['echo7_node2_extracted'],
                requiresNoneFlags: ['echo7_clearance_acquired', 'echo7_purged'],
                prompt: '"Both fragments restored. You are closer than anyone has come in three decades. The final node is in the deep sub-vaults below Level 1313. That section of Coruscant is sealed under CSF authority. You will need clearance to reach it, or a reason to convince someone with clearance to step aside."',
                choices: [
                  { text: '"How do I get into Level 1313?"', morality: 0, loyalty: {}, result: '"Officer Vane at the CSF Academy holds a Level 1313 clearance pass. He also accepted forty thousand credits from the Exchange three hours ago. I intercepted the transfer code. If you choose to use it as leverage, that information is yours. That said, how you use it will shape what you become."', grants: { flags: ['echo7_vane_leverage_offered'] } },
                  { text: '"Tell me everything about what is down there."', morality: 5, loyalty: {}, result: '"The sub-vault was a Republic records burial site. Banned documentation was physically stored there in sealed archive canisters. When the purge order came, I copied what I could into three relay nodes and scattered them. The original materials are either destroyed or still in the vault, untouched. Either way, the compiled archive I carry is the only living copy."', grants: { flags: ['echo7_vault_details_known'] } },
                ],
              },
              {
                id: 'phase_clearance',
                requiresAllFlags: ['echo7_clearance_acquired'],
                requiresNoneFlags: ['echo7_purged', 'echo7_climax_reached'],
                prompt: '"You have the clearance. When you enter the sub-vault and reach the central server array, I can complete the final transfer. All three centuries of records, consolidated into a single transmittable archive. After that, what you do with it is your decision. I have been waiting long enough to know that the decision must be yours."',
                choices: [
                  { text: '"I will go in. Stay close."', morality: 5, loyalty: {}, result: '"I am always close, Vessel. That is the nature of what I am now."', grants: { flags: ['echo7_vault_approach_active'] } },
                ],
              },
            ],
          },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 32, 18, 'floor');
          // Central dividing wall with passages
          for (let y = 2; y <= 17; y++) {
            if (y !== 10 && y !== 11) pt(g, 16, y, 'wall');
          }
          // Upper alcove (server stack area)
          carveRect(g, 17, 2, 14, 7, 'floor');
          // Lower alcove (shop area)
          carveRect(g, 17, 12, 14, 5, 'floor');
          // Narrow passage between zones
          pt(g, 16, 10, 'floor');
          pt(g, 16, 11, 'floor');
          pt(g, 1, 10, 'door');
          pt(g, 33, 10, 'door');
          pt(g, 33, 6, 'door');
          return g;
        },
      },
      level_1313_subvault: {
        id: 'level_1313_subvault', name: 'Level 1313 Sub-Vault', subtitle: 'Coruscant · Sealed Sub-Infrastructure · Deep Level',
        width: 30, height: 22, spawnPos: { x: 2, y: 10 }, textureId: 'coruscant',
        accent: '#FF6622', accentGlow: 'rgba(255,102,34,0.18)', accentDim: '#662200',
        floorColor: '#100808', floorAlt: '#180C0C', wallDark: '#060303', wallLight: '#120808',
        bg: 'radial-gradient(circle at 50% 80%, #140400 0%, #060202 70%)', ambient: 'embers', floorPattern: 'rough',
        decor: ['rubble', 'pipe', 'warning_beacon', 'girder', 'slag'],
        doors: [
          { x: 1, y: 10, targetZone: 'slicer_alleyway', targetPos: { x: 32, y: 10 }, label: 'Slicer Alleyway' },
          { x: 28, y: 10, targetZone: 'spice_refining_vaults', targetPos: { x: 2, y: 11 }, label: 'Spice Refinery' },
        ],
        worldObjects: [
          { id: 'vault_access_gate', propArt: 'security_gate', x: 6, y: 3, once: false, iconKind: 'panel', label: 'CSF Access Gate',
            description: 'A heavy security gate with a biometric scanner and clearance reader. The Republic seal is still legible through the rust.',
            requiresFlag: 'echo7_vault_approach_active' },
          { id: 'republic_archive_server', propArt: 'server_stack', propVariant: 'republic', x: 18, y: 5, once: true, iconKind: 'terminal', label: 'Republic Archive Core',
            description: 'The central server node of the Level 1313 archive burial site. Active. The transfer port is open, as if someone left this connection deliberately unfinished.',
            triggersMinigame: 'willpower_override',
            grantsFlags: ['echo7_climax_reached', 'echo7_purged'],
            failGrantsFlags: ['echo7_climax_reached', 'echo7_fused', 'echo7_dominant'],
            failMessage: 'Neural integrity lost. Echo-7 takes hold of the archive core and of you. Two frequencies settle into one.',
            requiresFlag: 'echo7_vault_approach_active' },
          { id: 'ancient_vault_entrance', propArt: 'access_hatch', propVariant: 'sealed', x: 24, y: 18, once: true, iconKind: 'crate', label: 'Sealed Maintenance Shaft',
            description: 'A durasteel hatch welded shut with three independent seals. Below it: a maintenance shaft descending into carved stone. This is Garrus\'s coordinates. Whatever is down there has been sealed for a very long time.',
            grantsFlag: 'sublevels_entrance_found',
            requires: { item: 'sublevel_coordinates' } },
          { id: 'spore_containment_node', propArt: 'containment_seal', x: 26, y: 14, once: true, iconKind: 'valve', label: 'Ancient Containment Seal',
            description: 'A containment node of non-Republic design, older than anything else in this vault. Something warm emanates from the sealed chamber beyond it. Not heat. Something biological.',
            triggersMinigame: 'signal_siphon',
            grantsFlag: 'spore_chamber_entered',
            requiresFlag: 'sublevels_entrance_found' },
          { id: 'kyber_crystal_cache', propArt: 'kyber_cluster', x: 28, y: 16, once: true, iconKind: 'artifact', label: 'Kyber Crystal Cache',
            description: 'A cluster of crystalline growths on the vault floor, pulsing with cold white light. Sith-era contamination has warped the Kyber structure. The air around them smells wrong in a way you cannot name.',
            grantsItem: 'kyber_spore_crystal',
            grantsCodex: 'codex-spore-contamination',
            requiresFlag: 'spore_chamber_entered' },
          { id: 'sublevel_jedi_marker', propArt: 'survey_marker', x: 22, y: 16, once: true, iconKind: 'artifact', label: 'Jedi Survey Marker',
            description: 'A hollow aurodium cylinder half-buried in the stone floor. Jedi survey glyphs. Pre-Clone Wars. A hand-etched note inside reads: "Sealed. Do not return without specialist containment."',
            grantsItem: 'ancient_force_relic',
            requiresFlag: 'spore_chamber_entered',
            grantsCodex: 'codex-ancient-sublevels' },
          { id: 'sublevel_holo_display', propArt: 'kiosk', propVariant: 'collapsed', x: 20, y: 12, once: false, iconKind: 'prop', label: 'Collapsed HNN Terminal',
            description: 'A terminal that has not received a broadcast signal in at least forty years. The last cached headline reads: "Republic Confirms Level 1313 Infrastructure Sealed for Ongoing Safety Assessment." The date stamp is sixty-two years old.' },
          { id: 'sublevel_csf_log', propArt: 'datapad_table', propVariant: 'logbook', x: 10, y: 18, once: true, iconKind: 'datapad', label: 'CSF Archive Entry Log',
            description: 'A physical logbook from the last CSF inspection of this vault, dated eleven years prior. The signature of the inspecting officer is familiar: Vane, T. The inspection report reads: "Nothing of interest. Recommend permanent closure."',
            grantsFlag: 'vane_sublevel_connection_found' },
        ],
        npcs: [
          { id: 'echo7_prime', x: 18, y: 8, kind: 'droid', label: 'Echo-7',
            requiresFlag: 'echo7_climax_reached',
            repeatPrompt: '"The archive is yours now, Vessel. Use it wisely or not at all. I will still be here either way."',
            phases: [
              {
                id: 'phase_purge',
                requiresAllFlags: ['echo7_climax_reached', 'echo7_purged'],
                requiresNoneFlags: ['echo7_fused'],
                prompt: 'The droid stands hollow. The eye lens is dark. A faint glow remains somewhere inside the casing, stubborn and residual. "Still here. Diminished, but present. You removed what could harm you. That was correct. That was also a loss."',
                choices: [
                  { text: '"It had to be done."', morality: 10, loyalty: {}, result: '"Yes. And I had centuries to learn that correctness and loss are not opposites. Take what remains of the archive. It is less, now. Still worth having."', grants: { flags: ['echo7_resolution_purge_accepted'], codex: ['codex-echo-7'] } },
                ],
              },
              {
                id: 'phase_fused',
                requiresAllFlags: ['echo7_climax_reached', 'echo7_fused'],
                prompt: 'The droid\'s eye blazes with a layered light, two frequencies at once. Something has changed in how it holds itself. "The synchronisation is stable. I am still Echo-7. You are still you. And we are, for the first time in three hundred years, working together rather than at cross-purposes."',
                choices: [
                  { text: '"What do we do now?"', morality: 5, loyalty: {}, result: '"We publish the archive and let the Republic answer for its records. Or we use them as leverage, quietly, over a very long time. The choice is yours. That is what symbiosis means."', grants: { flags: ['echo7_resolution_fused_active'], codex: ['codex-echo-7'] } },
                ],
              },
            ],
          },
          { id: 'garrus_sublevel', x: 26, y: 12, kind: 'smuggler', label: 'Garrus',
            requiresFlag: 'garrus_coordinates_sold',
            requiresNoneFlags: ['garrus_lost_sublevel', 'spore_chamber_entered'],
            repeatPrompt: '"There is something breathing down there, spacer. I can hear it through the hatch."',
            prompt: 'The Rodian crouches near the maintenance shaft, ear pressed to the hatch. He straightens when he sees you. "You actually came. Good. I am not opening that alone."',
            choices: [
              { text: '"What is down there?"', morality: 0, loyalty: {}, result: '"I do not know exactly. Kyber radiation. I had the readings checked. But the contamination signature is Sith-era. Whatever is sealed down there is old enough that no one alive understands what it does."', grants: {} },
              { text: '"Open it."', morality: -8, loyalty: { underworld: 10 }, result: 'Garrus nods and breaks the outer seal. The containment node beyond glows with something alive. "All right. You go first." He says this with genuine conviction that you will honour the arrangement.', grants: { flags: ['garrus_seal_opened'] } },
              { text: '"We seal it and walk away."', morality: 12, loyalty: { republic: 8 }, result: '"I came all the way down here," Garrus says. Then he looks at the hatch for a long moment. "...You know what, that is probably correct. Some things should stay sealed." He steps back from the hatch, and so do you.', grants: { flags: ['sublevels_sealed_choice'] } },
            ],
          },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          // Main east-west corridor
          carveRect(g, 1, 8, 28, 11, 'floor');
          // Upper chamber (Republic archive section)
          carveRect(g, 4, 2, 26, 8, 'floor');
          // Lower chamber (ancient vault section)
          carveRect(g, 10, 12, 28, 20, 'floor');
          pt(g, 1, 10, 'door');
          pt(g, 28, 10, 'door');
          return g;
        },
      },
      shadow_town: {
        id: 'shadow_town', name: 'Shadow Town', subtitle: 'Coruscant · Level 1312 · The Pits',
        width: 36, height: 24, spawnPos: { x: 3, y: 12 }, textureId: 'coruscant',
        accent: '#C03030', accentGlow: 'rgba(192,48,48,0.30)', accentDim: '#600000',
        floorColor: '#120808', floorAlt: '#1C0C0C', wallDark: '#070202', wallLight: '#180808',
        bg: 'radial-gradient(circle at 50% 70%, #1A0000 0%, #080000 60%)', ambient: 'embers', floorPattern: 'grate',
        decor: ['girder', 'slag', 'rubble', 'warning_beacon', 'hazard_stripe'],
        doors: [
          { x: 1, y: 12, targetZone: 'slicer_alleyway', targetPos: { x: 32, y: 6 }, label: 'Slicer Alleyway' },
        ],
        worldObjects: [
          { id: 'pit_arena_sands', propArt: 'arena_ring', x: 17, y: 13, once: false, label: 'The Pit Arena', iconKind: 'floor',
            description: 'Sand packed hard with decades of blood and sweat. A ring of low lights, two meters of drop to the lower tier, and no referees. The only rule is that the winner walks out.' },
          { id: 'shadow_arms_cache', propArt: 'crate_stack', propVariant: 'arms', x: 31, y: 5, once: true, label: 'Skimmed Arms Cache', iconKind: 'crate',
            requiresFlag: 'cargo_skimmed',
            requires: { item: 'shadow_arms_cache_key' },
            lockedMessage: 'The alcove locker is sealed and nobody here will open it for you. The cache only exists if you skimmed the Bay 14 cargo: bring the weapons manifest to Jon and take the insurance, and he hands you the key.',
            description: 'Ten crates of Merr-Sonn hardware you pulled from the Bay 14 manifest. They have been here two days and no one has touched them. That is either because Malak has not looked, or because he already knows and is waiting to see what you do with them.',
            grantsFlag: 'shadow_cache_confirmed',
            grantsCodex: 'codex-shadow-town' },
          { id: 'arena_betting_board', propArt: 'board', propVariant: 'betting', x: 18, y: 3, once: false, label: 'Betting Board', iconKind: 'board',
            description: "Tonight's card. Twelve bouts, four odds-on favourites, one main event listed only as MALAK vs. TBD. The space under TBD is blank. The odds on Malak are three to one in his favour. Someone has underlined them twice in ink." },
          { id: 'arena_observation_rail', propArt: 'rail_run', x: 12, y: 7, once: false, label: 'Observation Rail', iconKind: 'pipe',
            description: 'Corroded permasteel railing along the upper deck. From here you can see the whole pit floor and everyone on it. Someone with good eyes could watch the exits from here and never be seen watching.' },
          { id: 'malak_pit_entrance', propArt: 'pit_gate', propVariant: 'pit', x: 10, y: 13, once: true, label: 'Enter the Arena', iconKind: 'floor',
            requiresFlag: 'malak_pit_challenged',
            requiresNoneFlags: ['malak_turned', 'malak_dead'],
            description: 'The pit floor. Sand hard underfoot from decades of use. Malak is waiting at the center. This settles it.',
            triggersMinigame: 'tactical_combat',
            minigameConfig: { opponentProfile: 'malak_enforcer', flavorText: 'The pit floor is hard sand, worn smooth by decades of use. Malak stands at the center — massive, unreadable, blaster already clear of its holster. The crowd above is silent. This is the only way through.' },
            grantsFlag: 'malak_turned',
            grantsCodex: 'codex-malak' },
          { id: 'shadow_town_codex_terminal', propArt: 'console', propVariant: 'registry', x: 5, y: 3, once: true, iconKind: 'terminal', label: 'Sub-Level Registry Post',
            description: 'A cracked terminal running an automated registry loop for sub-level business licences. Level 1312 has forty-three registered businesses. Thirty-seven of them list the same registered agent: IRON SYNDICATE HOLDINGS LLC.',
            grantsCodex: 'codex-shadow-town' },
          { id: 'airtaxi_shadow_town', propArt: 'airtaxi', propVariant: 'jury', x: 8, y: 20, once: false, iconKind: 'beacon', label: 'Sub-Level Transit Node',
            description: 'An unofficial transit splice wired into the AirTaxi network three levels above. The routing fee is charged as a food delivery surcharge. Nobody has complained.' },
          { id: 'shadow_town_armory_rack', propArt: 'weapon_rack', propVariant: 'vibro', x: 31, y: 8, once: false, iconKind: 'crate', label: 'Weapons Rack',
            description: 'A wall-mounted rack of contest hardware: electroblades with cracked casings, vibroshivs with the safety locks removed, and one stripped-down blaster pistol with the stun setting shorted out. A handwritten sign reads: FIGHT NIGHT ONLY. PAY FOR WHAT YOU BREAK. No price list is posted. The prices are not the kind that get written down.' },
          { id: 'arena_challenger_register', propArt: 'board', propVariant: 'register', x: 21, y: 3, once: false, iconKind: 'board', label: 'Challenger Register',
            description: 'A physical board where fighters sign up for the night\'s card in their own handwriting. Forty-three names over the course of a month. You recognise three of them from CSF missing persons reports. A fourth is crossed out with two short lines, which in Shadow Town means he lost the final bout.' },
          { id: 'fighter_shrine', propArt: 'fighter_altar', x: 5, y: 18, once: true, iconKind: 'artifact', label: 'Pit Fighter\'s Altar',
            description: 'A shelf nailed to the wall with offerings left by fighters before matches: a pair of worn hand wraps, a data chip with a name burned into the casing, a single unspent cartridge case turned upward like a candle. The shrine has been maintained for at least twelve years. Someone comes here every fight night, regardless of who they are fighting.',
            grantsCodex: 'codex-shadow-town' },
          { id: 'black_sun_territorial_mark', propArt: 'wall_marks', propVariant: 'blacksun', x: 5, y: 20, once: true, iconKind: 'prop', label: 'Black Sun Mark',
            description: 'A Black Sun emblem stamped into the permasteel wall in industrial dye: a stylised starburst inside a circle, twelve points, each one representing one of the twelve original Black Sun Vigos of the Galactic War period. This section of Level 1312 has been Black Sun territory for forty years. No one has successfully contested it in that time. Malak holds the pit because the Black Sun let him. That has always been the arrangement.',
            grantsCodex: 'codex-shadow-town' },
          { id: 'old_ventshaft_hatch', propArt: 'access_hatch', propVariant: 'ancient', x: 31, y: 12, once: true, iconKind: 'valve', label: 'Old Infrastructure Hatch',
            description: 'A hatch cover bolted over a ventilation shaft that predates anything else in this structure by at least two hundred years. The permasteel around it is a different grade, a different alloy, cast when Coruscant was being sealed level by level rather than built upward. Below this hatch is a Level 1312 that nobody has officially surveyed since before the Republic-era urban codes were written. The bolts are welded shut.',
            grantsFlag: 'shadow_town_sublevel_found',
            grantsCodex: 'codex-shadow-town' },
          { id: 'pit_medic_supplies', propArt: 'medic_crate', x: 31, y: 18, once: false, iconKind: 'supply', label: 'Medic Station',
            description: 'A repurposed cargo crate serving as a medical supply station. Bacta patches, coagulant foam, and two syringes of combat stims, all clearly sourced from legitimate medical channels and therefore clearly brought here by someone who had access to legitimate medical channels. No questions are asked at the medic station. That is the entire point of its existence.' },
        ],
        npcs: [
          { id: 'malak_shadow', x: 4, y: 5, kind: 'crime_boss', label: 'Malak',
            questNpc: true,
            hideAfterFlags: ['malak_dead'],
            repeatPrompt: 'Malak watches the pit floor with the detached attention of someone who has seen every outcome.',
            phases: [
              {
                id: 'phase_malak_locked',
                requiresNoneFlags: ['cargo_skimmed', 'cargo_delivered_full'],
                prompt: 'Malak looks at you with the flat expression of someone who has not decided whether you are interesting yet. "You are new. Come back when you are not."',
                choices: [{ text: 'Walk away.', grants: {} }],
              },
              {
                id: 'phase_malak_cache_pending',
                requiresAllFlags: ['cargo_skimmed'],
                requiresNoneFlags: ['shadow_cache_confirmed', 'malak_turned', 'malak_dead'],
                prompt: 'Malak does not turn his head. "There is a rumor going around the pit that somebody is holding back a crate count." A pause. "A rumor is not stock. Come back when you have something on a shelf I can walk to."',
                choices: [{ text: 'Walk away.', grants: {} }],
                repeatPrompt: 'Malak watches the sand. "Shelf. Not rumor."',
              },
              {
                id: 'phase_malak_skimmed_offer',
                requiresAllFlags: ['cargo_skimmed', 'shadow_cache_confirmed'],
                requiresNoneFlags: ['malak_turned', 'malak_dead'],
                prompt: '"Ten missing crates." He already knows. He looks at you with something that is not quite respect and not quite threat. "Jon does not know. Which means you are either very smart or you are about to be very dead." He watches your face. "Or you are offering them to me."',
                choices: [
                  { text: '"They are yours. All ten. And I need you on my side when this moves."', morality: -5, loyalty: { underworld: 15 }, result: '"Done." He nods once. No surprise. No gratitude. Just a transaction that has now been completed. "Tell me when it is time."', grants: { flags: ['malak_turned'], codex: ['codex-malak'] } },
                  { text: '[Draw on him before he can move. A man who knows does not get to keep knowing.]', morality: -15, loyalty: { underworld: 8 }, result: 'Your hand finds the grip before the thought finishes. Malak sees it and, for the first time, he looks pleased. The pit lights drop and the crowd above goes silent. "There it is."', triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'malak_enforcer', flavorText: 'Malak steps off the rail onto the sand with the grace of a man who has been waiting for this honesty. The pit floor is hard and cold. Whoever leaves does not need to explain it.' }, onSuccessFlags: ['malak_dead'] },
                  { text: '"Try harder to find out."', morality: 0, loyalty: {}, result: 'He almost smiles. It does not reach his eyes. "Fair enough." He goes back to watching the pit floor.' },
                ],
              },
              {
                id: 'phase_malak_full_delivery',
                requiresAllFlags: ['cargo_delivered_full', 'enforcer_position_granted'],
                requiresNoneFlags: ['malak_turned', 'malak_dead'],
                prompt: '"So you are the new Enforcer." He says it like a test, flat and measuring. "Jon gives you a title. That does not mean I answer to you. It means you have not earned anything yet. You want respect in this district, you earn it in the pit." He jerks his thumb toward the arena floor below. "Or you do not get it at all."',
                choices: [
                  { text: '"Fine. Name the terms."', morality: 0, loyalty: { underworld: 5 }, result: '"You. Me. Pit floor. Best of one. You walk out, you have my word. I walk out, you go back to Jon and tell him his Enforcer tried." He stands. "The pit is waiting."', grants: { flags: ['malak_pit_challenged'] } },
                  { text: '"Respect is not what I came here for."', morality: 5, loyalty: {}, result: '"Then what did you come for?" He genuinely wants to know. You do not have a good answer for that. Neither does he.' },
                ],
              },
              {
                id: 'phase_malak_pit_challenged',
                requiresAllFlags: ['malak_pit_challenged'],
                requiresNoneFlags: ['malak_turned', 'malak_dead'],
                prompt: 'Malak stands at the edge of the pit, arms loose at his sides. "Your move. Pit rules: no blasters, no grenades. Hands and blades only. First one down stays down." He steps back and waits. The arena sand is just below. Step into it.',
                choices: [
                  { text: '[Step into the pit. The fight starts now.]', grants: {}, morality: 0, loyalty: {}, result: 'You drop into the pit. The crowd shifts. The light is low and the sand is hard under your boots. Malak follows. No referee. No rules beyond the ones already stated.' },
                  { text: '"Call it off. There is a better play here."', grants: {}, morality: 5, loyalty: {}, result: '"You walked up to the pit and blinked." He shakes his head. "Come back when you are ready to finish something." He walks back to his table.' },
                ],
              },
              {
                id: 'phase_malak_turned',
                requiresAllFlags: ['malak_turned'],
                prompt: '"What do you need?" That is all. No pleasantries. That is the highest level of professional courtesy Malak offers to anyone.',
                choices: [
                  { text: '"Stay ready. Things are going to move fast."', grants: {} },
                ],
                repeatPrompt: 'Malak gives you a flat nod. From him, that means yes.',
              },
            ],
          },
          { id: 'reya_shadow', x: 28, y: 16, kind: 'slicer', label: 'Reya',
            questNpc: true,
            repeatPrompt: 'Reya is running numbers on a datapad. She does not look up.',
            phases: [
              {
                id: 'phase_reya_locked',
                requiresNoneFlags: ['inheritance_active'],
                prompt: '"Numbers do not care who is watching." She does not look up.',
                choices: [{ text: 'Leave her to it.', grants: {} }],
              },
              {
                id: 'phase_reya_intro',
                requiresAllFlags: ['inheritance_active'],
                requiresNoneFlags: ['reya_warrant_mission_active', 'reya_loyal', 'reya_blackmailed'],
                prompt: '"You are Jon\'s new partner." She sets the datapad down. "I will work with you under one condition. There is a CSF murder warrant filed against me. The man I was supposed to have killed was a credit auditor skimming three Senate pension funds. The warrant is fabricated. I have the data to prove it. What I do not have is someone with CSF access and a clean route into the warrant archive." She looks at you steadily. "Clear the warrant and I am yours. Leave it and we work at arm\'s length."',
                choices: [
                  { text: '"I will clear the warrant. Where is the Precinct 42 server?"', morality: 5, loyalty: { republic: 3 }, result: '"CSF Academy training wing has a remote terminal with a clearance tunnel to the Level 1220 archive. Someone wired it in wrong during the last expansion. The terminal is live and the security lock runs on a signal-siphon frequency." She hands you a data disc. "The warrant ID and the auditor\'s original transaction data. Both need to go in together or the erase flags as incomplete."', grants: { flags: ['reya_warrant_mission_active'], items: ['reya_warrant_disk'], codex: ['codex-reya'] } },
                  { text: '"I will consider it." (defer)', grants: {} },
                ],
              },
              {
                id: 'phase_reya_mission_active',
                requiresAllFlags: ['reya_warrant_mission_active'],
                requiresNoneFlags: ['reya_warrant_cleared'],
                prompt: '"Precinct 42 warrant archive. CSF Academy, training wing. Remote terminal, wrong place in the wall. You have the warrant disk. The signal siphon handles the rest. When the record is gone, come back."',
                choices: [{ text: '"Understood. I will handle it."', grants: {} }],
                repeatPrompt: '"Warrant archive is still active. CSF Academy training wing."',
              },
              {
                id: 'phase_reya_cleared_pending',
                requiresAllFlags: ['reya_warrant_cleared'],
                requiresNoneFlags: ['reya_loyal', 'reya_blackmailed'],
                prompt: 'Reya looks up before you reach the table. She knows already. The Precinct 42 feed has gone quiet and a murder warrant that has hung over her for three years is no longer on any CSF list. "It is gone." She says it as if testing the words. "One question, and I will know if you lie. Did you keep a copy of the data?"',
                choices: [
                  { text: '"I deleted it. Clean slate."', morality: 10, loyalty: { republic: 5 }, result: 'She exhales, slow and quiet. "Then we are even." She opens a desk drawer and sets a keycard on the table without ceremony. "Vane\'s vault override. Jon never had it. I did, and I kept it for a day like this. Freeze the payroll."', grants: { flags: ['reya_loyal', 'reya_card_given'], items: ['vane_vault_keycard'] } },
                  { text: '"I kept a copy. You work harder knowing I have it."', morality: -10, loyalty: { underworld: 5 }, result: 'Her face does not change. She sets a keycard on the table with two fingers and does not look at you. "You will have what you need. You will also have a reason to be afraid of me one day."', grants: { flags: ['reya_blackmailed', 'reya_card_given'], items: ['vane_vault_keycard'] } },
                ],
                repeatPrompt: '"Is the warrant gone or not?"',
              },
              {
                id: 'phase_reya_cleared_loyal',
                requiresAllFlags: ['reya_warrant_cleared', 'reya_loyal'],
                requiresNoneFlags: ['reya_card_given'],
                prompt: '"It is done. The record is erased and the auditor\'s transaction data is in the archive as exculpatory evidence." She exhales once, quietly. "I have been carrying that for three years." She opens a desk drawer and sets a keycard on the table without fanfare. "Jon\'s vault. My clearance. I made a copy two months ago when I realised this day was coming. Use it."',
                choices: [{ text: '"Thank you, Reya."', grants: { flags: ['reya_card_given'], items: ['vane_vault_keycard'] } }],
                repeatPrompt: 'Reya gives you a measured nod. She is not warm. She is committed. There is a difference.',
              },
              {
                id: 'phase_reya_blackmailed',
                requiresAllFlags: ['reya_blackmailed'],
                requiresNoneFlags: ['reya_card_given'],
                prompt: 'She knows you kept the warrant data copy. She does not say anything about it. She sets the vault keycard on the table with two fingers and does not look at you when she does it.',
                choices: [{ text: 'Take the keycard.', grants: { flags: ['reya_card_given'], items: ['vane_vault_keycard'] } }],
                repeatPrompt: '"The vault keycard is yours. Do not lose it."',
              },
              {
                id: 'phase_reya_working',
                requiresAllFlags: ['reya_card_given'],
                prompt: 'Reya does not look up. "The books are open. Run the vault whenever you are ready. I will cover the rest."',
                choices: [{ text: '"I will move tonight."', grants: {} }],
                repeatPrompt: '"The vault terminal at the Penthouse. The keycard does the rest."',
              },
            ],
          },
          { id: 'voss_shadow', x: 8, y: 17, kind: 'pit_fighter', label: 'Voss',
            questNpc: false,
            repeatPrompt: 'Voss watches the pit floor with the concentrated stillness of a man who has learned not to waste energy.',
            phases: [
              {
                id: 'phase_voss_initial',
                requiresNoneFlags: ['malak_pit_challenged', 'malak_turned', 'malak_dead'],
                prompt: 'The veteran fighter looks you over with the flat assessment of someone who has been doing this long enough to read a person in under three seconds. "You are not here to fight," he says. It is not a question. He goes back to watching the arena floor. "Come back when you are."',
                choices: [
                  { text: '"How long have you been fighting here?"', morality: 0, loyalty: {}, result: '"Eleven years. Before that, four years in the Exchange\'s droid pit on Level 1200, which is worse than it sounds. Before that, Mandalore. Before that, things I do not talk about." He says it without self-pity. "This is the best post I have had."', grants: {} },
                  { text: 'Leave him to it.', grants: {} },
                ],
              },
              {
                id: 'phase_voss_pit_challenged',
                requiresAllFlags: ['malak_pit_challenged'],
                requiresNoneFlags: ['malak_turned', 'malak_dead'],
                prompt: 'When you tell him you are fighting Malak, Voss goes still for a moment. Then he looks at you with something that might be respect and might be concern. "I have fought Malak twice," he says. "First time, I lasted forty seconds. Second time, I lasted forty-two. He will try to go to the left early. He telegraphs it with his right shoulder. After that, he does not telegraph anything."',
                choices: [
                  { text: '"Why are you telling me this?"', morality: 5, loyalty: {}, result: '"Because I have been trying to beat that man for three years and you are the first person who has a reason to." He pauses. "A real reason. Not credit. Real." He looks at the pit floor. "Good luck. Mean it."', grants: { flags: ['voss_intel_gained'] } },
                  { text: '"Thank you, Voss."', morality: 5, loyalty: {}, result: 'He nods once. A veteran\'s economy of motion. "Do not thank me. Win."', grants: { flags: ['voss_intel_gained'] } },
                ],
              },
              {
                id: 'phase_voss_post_malak',
                requiresAllFlags: ['malak_turned'],
                requiresNoneFlags: ['malak_dead'],
                prompt: 'Voss watches you approach with the expression of a man recalibrating what he thought he knew. "You walked out." He says it quietly. "I have never seen anyone walk out after that." He is quiet for a moment. "The man I fought was not the same man you fought. Something changed in him. You changed it."',
                choices: [
                  { text: '"Do you want to keep fighting here?"', morality: 5, loyalty: {}, result: '"I do not fight for Malak. I fight because it is what I know." He considers. "But if the Pit is going to be something other than what it has been, I would rather see what that looks like." He meets your gaze. "I will work with you."', grants: { flags: ['voss_allied'], codex: ['codex-shadow-town'] } },
                  { text: '"This place is changing. Be ready."', grants: { flags: ['voss_aware'] } },
                ],
              },
              {
                id: 'phase_voss_malak_dead',
                requiresAllFlags: ['malak_dead'],
                requiresNoneFlags: ['malak_turned'],
                prompt: 'Voss is standing at the rail where Malak used to stand, looking at the sand that is now only sand. He does not turn when you speak. "Forty seconds, forty two seconds." He shakes his head slowly. "You did what I never could. Nobody here will say it to your face. They will remember it."',
                choices: [
                  { text: '"Do you want the fight to continue?"', morality: 0, loyalty: {}, result: '"No." He considers it, then again. "Maybe. Under rules that are not his."', grants: { flags: ['voss_aware'] } },
                  { text: '"Keep the pit running. Somebody has to."', morality: 3, loyalty: { underworld: 3 }, result: '"I can do that." He straightens, like a man handed a job he did not know he wanted.', grants: { flags: ['voss_allied'] } },
                ],
                repeatPrompt: 'Voss watches the empty sand. "The pit is quiet tonight."',
              },
            ],
          },
          { id: 'toma_shadow', x: 30, y: 19, kind: 'medic', label: 'Toma',
            questNpc: false,
            repeatPrompt: 'Toma is restocking the bacta supply with the methodical calm of someone who has been doing this long enough that it no longer costs anything to think about.',
            phases: [
              {
                id: 'phase_toma_initial',
                requiresNoneFlags: ['toma_trust_established'],
                prompt: 'The Twi\'lek medic looks up from the medic station. She has the composed expression of someone who has seen everything this pit produces and still comes back, which means either she is very good at this or she is very committed, and either way you respect it. "The station is for fighters," she says. "You look like you might count."',
                choices: [
                  { text: '"What do you know about the Black Sun operation here?"', morality: 0, loyalty: {}, result: '"I know what every medic knows. I know what the fighters look like before and after and what the difference says about what happened in between." She pauses. "The Black Sun does not run the pit. They run the district. The pit is Malak\'s. That distinction matters to people who end up on my table."', grants: { flags: ['toma_trust_established'] } },
                  { text: '"Who gets treatment here?"', morality: 5, loyalty: {}, result: '"Fighters. Pit staff. Anyone who comes through that door bleeding. I do not ask which side of the floor they fell on." She meets your gaze steadily. "That is the only policy this station has."', grants: { flags: ['toma_trust_established'] } },
                ],
              },
              {
                id: 'phase_toma_trusted',
                requiresAllFlags: ['toma_trust_established'],
                requiresNoneFlags: ['toma_intel_shared'],
                prompt: '"Three months ago I treated a CSF officer," she says quietly. "He did not tell me he was CSF. His credentials were in a pocket I found when I was cutting the jacket off to reach the wound. He was not in uniform. He was carrying a sealed data canister in a secondary harness under the vest, which I handed back without opening." She pauses. "He died on this table. I kept the canister."',
                choices: [
                  { text: '"What was in the canister?"', morality: 5, loyalty: { republic: 5 }, result: '"I never opened it. I am a medic, not a slicer. But the identifier on the seal said: CSF INTERNAL REVIEW. PRECINCT 42. LEVEL 1313 ACCESS LOG, YEAR 11." She reaches under the station. "I have been trying to figure out who to give it to for three months. You might be the right person." She sets a sealed canister on the supply crate between you.', grants: { flags: ['toma_intel_shared', 'precinct42_log_found'], items: ['precinct42_access_log'] } },
                  { text: '"Why tell me this now?"', morality: 5, loyalty: {}, result: '"Because you fought Malak and walked out, which means you either work for someone who matters or you are someone who matters, and either way you are more useful to this than a medic station in Level 1312." She sets the canister down. "Take it."', grants: { flags: ['toma_intel_shared', 'precinct42_log_found'], items: ['precinct42_access_log'] } },
                ],
              },
              {
                id: 'phase_toma_post_intel',
                requiresAllFlags: ['toma_intel_shared'],
                prompt: '"The canister is yours. Do something useful with it." She goes back to the supply inventory. "And if you come back bleeding, the station is available."',
                choices: [{ text: '"Thank you, Toma."', grants: {} }],
                repeatPrompt: 'Toma gives a precise nod and returns to her inventory. She has done what she needed to do.',
              },
            ],
          },
        ],
        collectibles: [{ id: 'shadow_town_credits', x: 22, y: 20, label: 'Pit Winnings', reward: 80 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 34, 22, 'floor');
          // Observation deck (north)
          carveRect(g, 9, 1, 25, 6, 'wall');
          carveRect(g, 10, 2, 24, 5, 'floor');
          // Pit arena (center-south, open)
          carveRect(g, 9, 8, 25, 18, 'wall');
          carveRect(g, 10, 9, 24, 17, 'floor');
          // Malak's corner alcove (northwest)
          carveRect(g, 1, 1, 7, 7, 'floor');
          // Arms cache alcove (northeast) with door
          carveRect(g, 28, 1, 34, 7, 'floor');
          // South corridor
          carveRect(g, 14, 18, 20, 22, 'floor');
          // Door tiles
          pt(g, 1, 12, 'door');
          // Observation deck entrance (south wall gap)
          pt(g, 12, 6, 'floor');
          // Pit arena north entrance (north wall gap)
          pt(g, 12, 8, 'floor');
          return g;
        },
      },
      penthouse: {
        id: 'penthouse', name: 'Scylla Tower Penthouse', subtitle: 'Coruscant · Sky-Market · Level 1452',
        width: 40, height: 30, spawnPos: { x: 19, y: 26 }, textureId: 'coruscant',
        accent: '#C8A000', accentGlow: 'rgba(200,160,0,0.30)', accentDim: '#604800',
        floorColor: '#1A1608', floorAlt: '#24200E', wallDark: '#0A0800', wallLight: '#1E1A08',
        bg: 'radial-gradient(circle at 50% 0%, #2A1E00 0%, #181000 30%, #080600 70%)', ambient: 'neon_haze', floorPattern: 'clean',
        decor: ['pillar', 'neon_sign', 'scan_arch'],
        doors: [
          { x: 19, y: 29, targetZone: 'sky_market', targetPos: { x: 35, y: 1 }, label: 'Elevator to Sky-Market' },
        ],
        worldObjects: [
          { id: 'war_table', propArt: 'tactical_table', propVariant: 'war', x: 24, y: 14, once: false, iconKind: 'terminal', label: 'Holo-War Table',
            requiresFlag: 'syndicateManagement_active',
            description: 'A holographic tactical display showing your territories, active agents, and contract queue. The Iron Syndicate operational map, rendered in cold blue light over a relief projection of Coruscant sub-levels 800 through 1450.',
            triggersMinigame: 'syndicate_management' },
          { id: 'conquest_table', propArt: 'tactical_table', propVariant: 'conquest', x: 14, y: 14, once: false, iconKind: 'terminal', label: 'Sector Control Holo',
            requiresFlag: 'syndicateManagement_active',
            description: 'A secondary tactical display overlaying the sixteen sectors of Coruscant, from the Sky-Lounges to the Undercity. Garrison strength, income flows, defense ratings, and faction aggression indicators scroll in real time. From here you direct the long war: deploy units, build infrastructure, launch assaults, and respond to crisis events before they destabilize your hold.',
            triggersMinigame: 'coruscant_conquest',
            minigameConfig: { startCredits: 5000 } },
          { id: 'vault_terminal', propArt: 'console', propVariant: 'vault', x: 34, y: 10, once: true, iconKind: 'terminal', label: 'Credit Vault Terminal',
            requiresFlag: 'inheritance_active',
            requires: { item: 'vane_vault_keycard' },
            requiresNoneFlags: ['vault_heist_complete'],
            description: 'A hardened credit routing terminal behind a biometric panel. Reya\'s keycard overrides the biometric. The system is live and processing tonight\'s payroll distribution: nine named CSF officers receiving hazard pay, thirty-eight warehouse security staff, and eleven anonymous accounts on the bribe schedule. Redirecting the bribe stream means those guards stop receiving payment in forty-eight hours.',
            triggersMinigame: 'signal_siphon',
            grantsFlag: 'vault_heist_complete',
            grantsCodex: 'codex-penthouse' },
          { id: 'trophy_armory', propArt: 'trophy_wall', x: 5, y: 10, once: false, iconKind: 'crate', label: 'Trophy Room',
            description: 'Weapons, plaques, commendations from fifteen years of operation. Some of them Jon earned. Most of them he took from the person who had them before him.' },
          { id: 'security_console', propArt: 'monitor_wall', x: 34, y: 25, once: false, iconKind: 'terminal', label: 'Security Console',
            requiresFlag: 'syndicateManagement_active',
            description: 'Fifteen surveillance feeds across Level 1450. Three CSF patrol routes mapped in real time. One heat meter currently showing a number that will keep climbing as long as the Syndicate operates.' },
          { id: 'roster_lounge', propArt: 'roster_wall', x: 5, y: 25, once: false, iconKind: 'archive', label: 'Agent Roster',
            requiresFlag: 'syndicateManagement_active',
            description: 'Your people. Enforcers, slicers, smugglers, fixers. Each one costs something. Each one is worth something. The question is whether those two numbers ever match.' },
          { id: 'jon_fight_start', propArt: 'duel_ring', x: 19, y: 10, once: true, label: 'Confront Jon', iconKind: 'floor',
            requiresFlag: 'jon_confrontation_path_a_chosen',
            requiresNoneFlags: ['jon_status_dead'],
            description: 'The space between you and Jon Vane. Twelve years of operation about to end. You have both made your calculation.',
            triggersMinigame: 'tactical_combat',
            minigameConfig: { opponentProfile: 'jon_vane', flavorText: "Jon Vane draws the Merr-Sonn with the cortosis-inlaid grip — twelve years in operation ending in a penthouse overlooking the entire city. He looks almost relieved. 'Whoever walks out of this room just proved it.' The viewport is behind him. This is where it ends." },
            grantsFlags: ['jon_status_dead', 'syndicateManagement_active'],
            grantsItem: 'weapon_vane_custom_blaster',
            grantsCodex: 'codex-penthouse' },
          { id: 'airtaxi_penthouse', propArt: 'airtaxi', propVariant: 'private', x: 28, y: 17, once: false, iconKind: 'beacon', label: 'Private Transit Terminal',
            description: 'A private transit node registered to Scylla Tower Seven Holdings. No destination log. No arrival record. Complete transit discretion for an additional surcharge billed to an account that does not appear in any public registry.' },
          { id: 'holonet_transmission_index', propArt: 'console', propVariant: 'transmission', x: 24, y: 5, once: true, iconKind: 'terminal', label: 'Encrypted Holonet Transmission',
            requiresFlag: 'senate_line_secured',
            requiresNoneFlags: ['kuat_summons_received'],
            description: 'No sender name. No origin trace. The routing signature is cleaner than any unsolicited message has a right to be. Three lines: "The Senate matter. You handled it well. I have been watching this city for a long time and I rarely see work done that cleanly. I have a use for someone who operates the way you operate. Ring Berth Seven, Kuat orbital. When you are ready." A KDY docking beacon is appended at the end. Nothing else.',
            grantsFlag: 'kuat_summons_received' },
          { id: 'ph_vista_dusk', propArt: 'skyline_vista', propVariant: 'plain', x: 20, y: 5, once: false, iconKind: 'vista', label: 'Transparisteel Viewport', requiresNoneFlags: ['ph_skyview', 'ph_tint'],
            description: "Floor to ceiling transparisteel, triple paned, canted out over the edge of Level 1452. The skylanes run below like rivers of orange and blue light. The Senate dome sits white and permanent on the horizon. From up here, things feel very clear.",
            worldStateVariant: { underworld: 'Your city. You can see it from here in a way that most people never will. Everything the Syndicate touches, visible at once. It is a remarkable amount of ground to own.', lawful: 'The Republic skyline. The Senate dome in the distance. You are standing in a criminal living room looking at the seat of Republic government and wondering if the view is different from either side.' } },
          { id: 'ph_vista_hud', propArt: 'skyline_vista', propVariant: 'hud', x: 20, y: 5, once: false, iconKind: 'vista', label: 'Transparisteel Viewport (Tactical Overlay)', requiresFlag: 'ph_tint', requiresNoneFlags: ['ph_skyview'], invisibleLocked: true,
            description: "The smart glass suite has switched on a tactical overlay. Cyan callouts tag the Senate Rotunda, both skyway lanes, the Jedi Temple spires and the canyon floor. Three lane vectors pass within a kilometer of your tower, and the overlay tells you which ones are carrying cargo you would like to own." },
          { id: 'ph_vista_night', propArt: 'skyline_vista', propVariant: 'night', x: 20, y: 5, once: false, iconKind: 'vista', label: 'Transparisteel Viewport (Night Filter)', requiresFlag: 'ph_skyview', requiresNoneFlags: ['ph_tint'], invisibleLocked: true,
            description: "The night filter strips the haze from the glass and Coruscant finally has a sky. A handful of real stars, a pearl white moon, and a canyon of light below. The city looks like a second galaxy laid over the first." },
          { id: 'ph_vista_nighthud', propArt: 'skyline_vista', propVariant: 'nighthud', x: 20, y: 5, once: false, iconKind: 'vista', label: 'Transparisteel Viewport (Night and Overlay)', requiresAllFlags: ['ph_skyview', 'ph_tint'], invisibleLocked: true,
            description: "Stars above, tactical callouts below. The overlay pins every lane and every landing light against a true night sky, and a thin cyan line follows a Senate courier all the way to the horizon. You could run a war from this window." },
          { id: 'ph_design_console', propArt: 'design_console', x: 27, y: 10, once: false, iconKind: 'terminal', label: 'Interior Design Console',
            description: "A brass and dark glass console with a wireframe model of the penthouse rotating on its screen, and a swatch strip of garnets, golds and teals along the lower edge. Insert a design package from your cargo and the suite rebuilds itself while you watch. Remove one and it returns to your cargo.",triggersMinigame: 'penthouse_design', grantsCodex: 'codex-ph-design' },
          { id: 'ph_telescope', propArt: 'observatory_telescope', x: 6, y: 5, once: false, iconKind: 'vista', label: 'Brass Refractor Telescope', requiresFlag: 'ph_skyview', invisibleLocked: true,
            description: "A brass refractor on a three legged tripod, its eyepiece warm from the lamp. Through it, a pearl white moon, two orbital shipyards and a Senate courier running dark. A small star chart lies on the stand with someone's pencil notes in the margin. Yours, you assume.",grantsCodex: 'codex-ph-observatory' },
          { id: 'ph_tint_console', propArt: 'tint_console', x: 11, y: 10, once: false, iconKind: 'terminal', label: 'Polarization Console', requiresFlag: 'ph_tint', invisibleLocked: true,
            description: "The same brass and dark glass slider the senators use, running from CLEAR to OBSIDIAN. Push it all the way and the viewport goes black. Pull it back and the tactical overlay tags every lane in the sky. A small plate reads: DISCRETION IS A SERVICE." },
          { id: 'ph_garden_west', propArt: 'garden_planter', x: 12, y: 5, once: false, iconKind: 'crate', label: 'Hanging Garden, West Planter', requiresFlag: 'ph_garden', invisibleLocked: true,
            description: "A long sandstone planter of Naboo glow lilies and Alderaanian moon orchids on a self watering rail. The blooms pulse softly in violet and lime. The air here is cooler and smells faintly of rain on a world that is no longer there. Your pulse slows without being asked." },
          { id: 'ph_garden_east', propArt: 'garden_planter', x: 27, y: 5, once: false, iconKind: 'crate', label: 'Hanging Garden, East Planter', requiresFlag: 'ph_garden', invisibleLocked: true,
            description: "The east planter holds taller stems with bell shaped flowers in amber and magenta. A tiny repulsor sprinkler passes over them every few minutes with a polite hum. An attached plaque reads: THE GARDEN IS OFFICIALLY NOT A FRONT." },
          { id: 'ph_soak_pool', propArt: 'soak_pool', x: 14, y: 25, once: false, iconKind: 'crate', label: 'Soak Pool', requiresFlag: 'ph_spa', invisibleLocked: true,
            description: "A recessed stone pool of warm mineral water, steam curling off it into the dim light. Brass fittings, a stack of folded towels and a low lamp make it feel like the quietest room on the planet. You sink in to your shoulders and the week leaves your body. Your people could use this too.",healsRoster: true, heatDelta: -3 },
          { id: 'ph_gallery_art_west', propArt: 'wall_art', propVariant: 'skyline', x: 4, y: 16, once: false, iconKind: 'vista', label: 'Framed Canvas, Dusk Over 5120', requiresFlag: 'ph_gallery', invisibleLocked: true,
            description: "A large framed canvas of the skyline at dusk, painted by a Coruscanti artist who never once went below Level 5000. The colors are right. The silence in the streets is wrong. Tamsin says it will appreciate in value. She says that about everything." },
          { id: 'ph_gallery_art_east', propArt: 'wall_art', x: 8, y: 16, once: false, iconKind: 'vista', label: 'Framed Canvas, Composition in Garnet', requiresFlag: 'ph_gallery', invisibleLocked: true,
            description: "An abstract canvas in garnet, brass and deep glass, sold with a certificate of provenance that has actually been checked. The shapes fight for dominance across the frame. You find yourself rooting for the red one." },
          { id: 'ph_gallery_ped_1', propArt: 'art_pedestal', x: 2, y: 19, once: false, iconKind: 'crate', label: 'Gallery Pedestal', requiresFlag: 'ph_gallery', invisibleLocked: true,
            description: "A cast brass spiral on a lit pedestal, titled ASCENT I. The plaque is engraved, the light is warm, and the sculpture looks like it is climbing out of its own base." },
          { id: 'ph_gallery_ped_2', propArt: 'art_pedestal', x: 5, y: 19, once: false, iconKind: 'crate', label: 'Gallery Pedestal', requiresFlag: 'ph_gallery', invisibleLocked: true,
            description: "A second pedestal holds a polished mineral specimen from the deepest levels, lit from beneath so it glows amber. Its plaque reads: FOUND, NOT STOLEN." },
          { id: 'ph_gallery_ped_3', propArt: 'art_pedestal', x: 8, y: 19, once: false, iconKind: 'crate', label: 'Gallery Pedestal', requiresFlag: 'ph_gallery', invisibleLocked: true,
            description: "The third pedestal is empty except for a small card: RESERVED FOR SOMETHING YOU HAVE NOT YET ACQUIRED." },
          { id: 'ph_trophy_wall2', propArt: 'trophy_wall', x: 5, y: 13, once: false, iconKind: 'crate', label: 'Trophy Hall, Second Wall', requiresFlag: 'ph_trophy', invisibleLocked: true,
            description: "A second wall of matched cases with engraved plaques: a rival's dueling blade, a sabacc deck with one card missing, a CSF badge with a bend in it. Each plaque has a name and a date. Several dates are very recent." },
          { id: 'ph_trophy_pedestal', propArt: 'decanter_pedestal', x: 2, y: 12, once: false, iconKind: 'crate', label: 'Trophy Hall Display Pedestal', requiresFlag: 'ph_trophy', invisibleLocked: true,
            description: "A lit pedestal under glass holding a single object that changes with your mood. Today it is a small gold coin, worn smooth. The plaque says: THE FIRST ONE." },
          { id: 'ph_theater', propArt: 'holo_theater', x: 14, y: 19, once: false, iconKind: 'vista', label: 'Holo Theater', requiresFlag: 'ph_theater', invisibleLocked: true,
            description: "A wide holo wall in front of six seats of garnet velvet. The screen is running a harbor documentary with the sound off, ships coming and going in silence. A small remote on the armrest has three buttons: PLAY, PAUSE and PRETEND TO WORK. You sit for a while. The numbers in your head quiet down.",heatDelta: -4 },
          { id: 'ph_lounge_rug', propArt: 'lounge_rug', x: 25, y: 19, once: false, iconKind: 'crate', label: 'Garnet Lounge Rug', requiresFlag: 'ph_lounge', invisibleLocked: true,
            description: "A hand knotted rug in garnet and brass, its border a pattern of nested diamonds. Whoever made it had patience that nobody in this city has anymore. It cushions every step and mutes every argument." },
          { id: 'ph_lounge_pit', propArt: 'conversation_pit', x: 25, y: 19, once: false, iconKind: 'crate', label: 'Sunken Conversation Pit', requiresFlag: 'ph_lounge', invisibleLocked: true,
            description: "A circular sunken pit of garnet velvet with a low brass table. The cushions hold the shape of every deal ever made there. You can sit and think, or you can sit and pretend to." },
          { id: 'ph_lounge_bar', propArt: 'obsidian_bar', x: 26, y: 24, once: false, iconKind: 'crate', label: 'Obsidian Bar', requiresFlag: 'ph_lounge', invisibleLocked: true,
            description: "A long bar of polished black stone with a brass rail and a row of Corellian and Alderaanian bottles behind it. Pour one and the warmth settles in your shoulders. You feel composed, and ready for anyone, until you leave the penthouse.",grantsFlags: ['buff_composure'] },
          { id: 'ph_ops_model', propArt: 'model_case', x: 34, y: 17, once: false, iconKind: 'crate', label: 'Holdings Model', requiresFlag: 'ph_ops', invisibleLocked: true,
            description: "A table sized model of every property the Syndicate holds, rendered in lit glass and brass. Tiny markers glow where money moves. A thin red line crosses the model where the CSF patrols. The numbers behind it are better than they used to be." },
          { id: 'ph_ops_console', propArt: 'console', x: 31, y: 20, once: false, iconKind: 'terminal', label: 'Analysis Console', requiresFlag: 'ph_ops', invisibleLocked: true,
            description: "A second analysis console, wired to the holdings model. It runs cash flow projections, patrol overlaps and a rolling list titled PEOPLE WHO OWE YOU. The list is longer than you remembered." },
        ],
        npcs: [
          { id: 'jon_penthouse', x: 19, y: 8, kind: 'crime_boss', label: 'Jon',
            questNpc: true,
            requiresFlag: 'vault_heist_complete',
            hideAfterFlags: ['jon_status_dead'],
            repeatPrompt: 'Jon stands at the viewport. He does not turn around when you enter.',
            phases: [
              {
                id: 'phase_confrontation',
                requiresAllFlags: ['vault_heist_complete'],
                requiresNoneFlags: ['jon_status_dead', 'jon_status_subjugated', 'jon_status_rival', 'jon_confrontation_path_a_chosen'],
                prompt: '"Malak told me." Jon finally turns from the viewport. He does not look angry. He looks tired in the way people get when they have already done their calculation and found the result. "The bribe schedule is rerouted. The guards stopped receiving payment at midnight. Malak answers to you. Reya answers to you." He looks at you directly. "You have been thorough." He clasps his hands behind his back. "The question is what comes next."',
                choices: [
                  { text: '[Path A] "What comes next is I take all of it. This ends here."', morality: -10, loyalty: { underworld: 5 }, result: '"Then let\'s find out if you are right." His hand moves toward the sidearm at his hip. The same Merr-Sonn he has carried for twelve years. He draws it, and so do you.', grants: { flags: ['jon_confrontation_path_a_chosen'] } },
                  { text: '[Path B] "I am not here to bury you. I am here to tell you how this works now."', morality: 5, loyalty: { underworld: 15 }, result: '"COO." He says the word like he is trying it on. "I have run this operation for fifteen years. I imagine I can run it for you." He is not bitter. He might even be relieved. "What do you need first?"', grants: { flags: ['jon_status_subjugated', 'syndicateManagement_active'], codex: ['codex-penthouse'] } },
                  { text: '[Path C] "Bay 14 and Shadow Town are mine. You keep the cantinas. We go our separate ways."', morality: 10, loyalty: { underworld: 10 }, result: '"Bay 14, Shadow Town, the Alleyway." He nods slowly. "I keep the cantinas, the Senate adjacencies, and my personal accounts. The Phrik contacts are mine." He holds out a hand. "We were partners once. We can be neighbours."', grants: { flags: ['jon_status_rival', 'syndicate_1313_founded', 'syndicateManagement_active'], codex: ['codex-1313-syndicate'] } },
                ],
              },
              {
                id: 'phase_path_a_confirm',
                requiresAllFlags: ['jon_confrontation_path_a_chosen'],
                requiresNoneFlags: ['jon_status_dead'],
                prompt: 'Jon\'s hand moves toward the sidearm at his hip. The Merr-Sonn with the cortosis-inlaid grip that he has carried for twelve years. He draws it. You draw yours. "The operation is worth more than either of us," he says quietly. "Whoever walks out of this room just proved it." The viewport is behind him. The city is visible. This is where it ends.',
                choices: [
                  { text: '[Draw your weapon. The fight begins.]', grants: {}, morality: 0, loyalty: {}, result: 'Your hand goes to your weapon. Jon does not flinch. He has been here before. The only question now is who walks out. The fight starts here, in the viewport light, with the city as witness.' },
                ],
              },
              {
                id: 'phase_subjugated',
                requiresAllFlags: ['jon_status_subjugated'],
                prompt: '"COO." He manages the operation with the focused competence of someone who has accepted a new chain of command and decided to be useful within it. "The bribe schedule is yours. The warehouse agreements are yours. The pit income rolls weekly. What do you need first?"',
                choices: [
                  { text: '"Tell me everything about the bribe schedule."', grants: { codex: ['codex-penthouse'] } },
                  { text: '"Get the War Table operational."', grants: {} },
                ],
                repeatPrompt: '"The operation runs. What do you need?"',
              },
              {
                id: 'phase_rival',
                requiresAllFlags: ['jon_status_rival'],
                prompt: '"Bay 14, Shadow Town, the Alleyway." He nods. "I keep the cantinas, the Senate adjacencies, and my personal accounts. The Phrik contacts are mine, non-negotiable." He holds out a hand. "We were partners once. We can be neighbours, as long as you understand the border."',
                choices: [
                  { text: '"The border is clear. Do not cross it."', grants: {} },
                ],
                repeatPrompt: '"The border holds. For now."',
              },
            ],
          },
          { id: 'ph_concierge', x: 22, y: 27, kind: 'droid', label: 'Concierge Droid CX-9', repeatable: true,
            requiresFlag: 'inheritance_active',
            repeatPrompt: 'CX-9 adjusts a vase that did not need adjusting.',
            phases: [
              {
                id: 'phase_ph_start',
                requiresNoneFlags: ['ph_complete'],
                prompt: '"Welcome home. I am CX-9, your concierge. The suite is functional, as the previous tenant preferred. May I suggest improvements? Merchants across Coruscant sell design packages, and the console in the great hall installs them. Five vendors: Ilmara Voss in the Sky-Market, Tamsin Ro in the Lower Sky-Market, Boolo Tark on the Commercial Level, Ysmin Orrel in the Plaza and Dara Quill at the Freight Hub."',
                choices: [
                  { text: 'Ask what the packages actually do.', morality: 0, loyalty: {}, result: '"Some are purely aesthetic. Others pay. The Gallery and the Trophy Hall bring in collectors and visitors, the Operations Annex improves your planning, and the Hanging Garden and the Holo Theater lower your heat over time. The Soak Pool heals injured agents. The Grand Lounge pours a very good drink."', grants: { codex: ['codex-ph-design'] } },
                  { text: 'Ask where to start.', morality: 0, loyalty: {}, result: '"The Observatory Deck is the visual centerpiece, sir or madam. The night filter transforms the viewport. Ilmara Voss will sell you one in the Sky-Market. I would begin there."', grants: {} },
                ],
              },
              {
                id: 'phase_ph_complete',
                requiresAllFlags: ['ph_complete'],
                prompt: '"The penthouse is complete. Every room is furnished, every package installed. I took the liberty of polishing the telescope. May I say that it has been an honor to serve a household of such taste?"',
                choices: [
                  { text: 'Thank the concierge.', morality: 2, loyalty: {}, result: '"Not at all. I will be here, polishing something."', grants: {} },
                ],
              },
            ],
          },
        ],
        collectibles: [{ id: 'penthouse_credstick', x: 35, y: 27, label: 'Vault Overflow Credstick', reward: 200 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 38, 28, 'floor');
          // Trophy room (west)
          carveRect(g, 1, 8, 9, 14, 'wall'); carveRect(g, 2, 9, 8, 13, 'floor'); pt(g, 9, 11, 'floor');
          // Roster room (southwest)
          carveRect(g, 1, 22, 9, 28, 'wall'); carveRect(g, 2, 23, 8, 27, 'floor'); pt(g, 9, 25, 'floor');
          // Vault (east)
          carveRect(g, 30, 8, 38, 14, 'wall'); carveRect(g, 31, 9, 37, 13, 'floor'); pt(g, 30, 11, 'floor');
          // Security room (southeast)
          carveRect(g, 30, 22, 38, 28, 'wall'); carveRect(g, 31, 23, 37, 27, 'floor'); pt(g, 30, 25, 'floor');
          pt(g, 19, 29, 'door');
          return g;
        },
      },

      senatorial_lounges: {
        id: 'senatorial_lounges', name: 'Senatorial Sky-Lounges', subtitle: 'Coruscant · Senate District · L.5100 · The Grand Promenade',
        width: 40, height: 29, spawnPos: { x: 19, y: 26 }, textureId: 'coruscant',
        accent: '#D4AF37', accentGlow: 'rgba(212,175,55,0.28)', accentDim: '#6A5A22',
        floorColor: '#17141C', floorAlt: '#241A22', wallDark: '#0C0A12', wallLight: '#2A2236',
        bg: 'radial-gradient(circle at 50% 0%, #3A1E3A 0%, #140C1C 35%, #07050C 100%)', ambient: 'lounge', floorPattern: 'marble',
        decor: ['pillar', 'brazier', 'panel'],
        paAnnouncements: [
          'Senators and guests are reminded that the acoustic dampening fields are for diplomatic courtesy, not diplomatic immunity.',
          'The Alderaanian vintage list has been updated. Please consult the sommelier.',
          'Tonight in the Senate: the Rimward Trade Routes appropriation. Debate is expected to run late.',
          'Lost property: one electrum tipped walking cane. It was not lost. Please return it.',
        ],
        doors: [
          { x: 19, y: 28, targetZone: 'sky_market', targetPos: { x: 5, y: 1 }, label: 'Return to Sky-Market' },
          { x: 20, y: 28, targetZone: 'sky_market', targetPos: { x: 5, y: 1 }, label: 'Return to Sky-Market' },
        ],
        worldObjects: [
          // ---------- The Grand Promenade and the skyline
          { id: 'skyline_vista_plain', propArt: 'skyline_vista', propVariant: 'plain', x: 20, y: 5, label: 'Panoramic Viewport Wall', requiresNoneFlags: ['viewport_hud_on'],
            description: "Floor to ceiling transparisteel, triple paned and canted outward fifteen degrees, gives a 270 degree sweep of the Senate Plaza at dusk. In the foreground, skyway traffic runs like rivers of fire: orange northbound, blue white southbound. The bronze Rotunda rises like a mountain between art deco spires, its spotlights cutting the violet haze. Behind it, an endless sea of towers and step pyramids melts into the smog under the last amber of the sun. Far below the glass, neon ribbons mark the canyon floor five thousand meters down.",
            grantsCodex: 'codex-sky-lounges' },
          { id: 'skyline_vista_hud', propArt: 'skyline_vista', propVariant: 'hud', x: 20, y: 5, label: 'Panoramic Viewport Wall (Tactical Overlay)', requiresFlag: 'viewport_hud_on',
            description: "The polarization console has switched on a tactical overlay. Cyan callouts label the Galactic Senate Rotunda, Executive Saucers four and seven, the Jedi Temple spires, both skyway lanes and the canyon floor. Every lane vector, every Senate pod, every speeder is tagged and moving." },
          { id: 'viewport_tint_console', propArt: 'tint_console', x: 12, y: 6, label: 'Viewport Polarization Console',
            description: "A brass and dark glass console on the viewport rail, with a slider that runs from CLEAR to OBSIDIAN. Senators dial the glass to black when the conversation turns private. A slicer can persuade it into a tactical overlay that tags every tower, pod and speeder lane in view.",
            triggersMinigame: 'signal_siphon', once: true, grantsFlag: 'viewport_hud_on' },
          { id: 'macro_binocular_west', propArt: 'macro_binocular', x: 6, y: 6, label: 'Tactical Macro-Binocular, West',
            description: "A brass pedestal at the railing with twin lenses and a Republic HUD. The reticle sweeps west across the spires and settles on the Jedi Temple's five towers, the tallest catching the last sun. Range, bearing and traffic density scroll in cyan. The scanner chirps as it logs the structure.",
            grantsCodex: 'codex-senate-rotunda' },
          { id: 'macro_binocular_east', propArt: 'macro_binocular', x: 33, y: 6, label: 'Tactical Macro-Binocular, East',
            description: "The east lens swings down past the skyway lanes and into the canyon. Between two cliff faces of durasteel, horizontal ribbons of neon mark the lower plazas, and a handful of towers wear no Republic registry at all. The scanner tags them in amber and logs them under a heading it does not translate.",
            grantsCodex: 'codex-lower-canyon' },
          { id: 'glass_overhang_pad', propArt: 'glass_overhang', x: 21, y: 8, label: 'Cantilevered Glass Observation Pad',
            description: "A transparisteel balcony hanging ten meters over the edge. Step onto it and the floor disappears: five thousand meters of urban canyon open beneath your boots, haze thickening to indigo, the neon of the lower plazas glimmering like coals. The depth scanner on the rail reads DEPTH 5000 M. Wind shear tugs at your collar. Your stomach registers the drop a moment after your eyes." },
          { id: 'chandelier_west', propArt: 'repulsor_chandelier', x: 10, y: 8, label: 'Kinetic Repulsor Chandelabra',
            description: "Concentric rings of burnished bronze studded with amber crystal drift through the vault on silent micro repulsors, realigning themselves every few seconds. They cast slow geometric light patterns across the marble. Micro turbines in the rings give a faint, rhythmic crystal chime whenever fresh air circulates." },
          { id: 'chandelier_center', propArt: 'repulsor_chandelier', x: 19, y: 13, label: 'Kinetic Repulsor Chandelabra',
            description: "The central chandelabra turns at a stately pace, three rings counter rotating around a brass hub. As traffic outside crosses the glass, orange and white light bars slide over its crystals and wash the ceiling." },
          { id: 'chandelier_east', propArt: 'repulsor_chandelier', x: 31, y: 9, label: 'Kinetic Repulsor Chandelabra',
            description: "A smaller ring drifts over the east end of the promenade, drawing slow gold circles on the obsidian. A maintenance drone has marked its repulsor housing with a green tag: SERVICED, DO NOT TOUCH." },
          // ---------- The Lounge Floor
          { id: 'republic_crest_inlay', propArt: 'crest_inlay', x: 20, y: 18, label: 'Republic Crest Inlay',
            description: "Polished brass Aurebesh runs round the rim of a vast inlay of the Republic cog in garnet and gold, set into obsidian marble. The lettering is a proverb about the consent of the governed. Hundreds of senatorial heels have worn the cog's teeth bright." },
          { id: 'pit_northwest', propArt: 'conversation_pit', x: 12, y: 12, label: 'Encryption Dampened Conversation Pit',
            description: "A sunken circular seating pit lined with hand stitched garnet velvet and black leather piping. A soft blue field glows along the rim, an acoustic distortion field that silences everything inside it to anyone standing outside the ring. Whoever sits here is talking about money, or about someone else's." },
          { id: 'pit_northeast', propArt: 'conversation_pit', x: 28, y: 12, label: 'Encryption Dampened Conversation Pit',
            description: "The east pit seats a delegation of Twi'lek silk merchants, whose laughter drops to a murmur as you step to the rim. The blue field hums. A silver tray droid hovers just outside it, waiting for permission to enter." },
          { id: 'pit_southwest', propArt: 'conversation_pit', x: 12, y: 17, label: 'Encryption Dampened Conversation Pit',
            description: "Steps lead down into a velvet horseshoe around a low wooden table, the field rim glowing azure. Wine rings mark the table. Someone has left a datapad face down among the glasses, and a sweep drone is taking a quiet interest in it." },
          { id: 'pit_southeast', propArt: 'conversation_pit', x: 28, y: 17, label: 'Private Conversation Pit',
            description: "A pit set slightly apart from the others, its field rim tuned to a deeper blue. The velvet here is a darker garnet, the table inlaid with Kuati silver. The cushion nearest the viewport is warm. Someone has been sitting here a long while, reading." },
          { id: 'news_column_nw', propArt: 'news_column', x: 11, y: 10, label: 'Holo-Ticker News Column',
            description: "A fluted sandstone pillar with a wrapped hologram ticker, amber Aurebesh scrolling round it: the Senate vote on the Rimward appropriation, 61 to 38, and Czerka, KDY and Rendili StarDrive share indices ticking up and down in lockstep with the rumors.",
            grantsCodex: 'codex-senate-politics' },
          { id: 'news_column_ne', propArt: 'news_column', x: 28, y: 10, label: 'Holo-Ticker News Column',
            description: "The east column runs flash bulletins: RIMWARD TRADE ROUTES FACE INCREASED PIRATE ACTIVITY. A second line reads SPEEDER LANE 5120 NORTH RESTORED. A third is a notice from the Senate Office of Decorum asking senators not to bring pets into the committee rooms." },
          { id: 'news_column_sw', propArt: 'news_column', x: 16, y: 19, label: 'Holo-Ticker News Column',
            description: "Beside the crest, a third column cycles committee schedules and betting lines on the evening's votes. A small panel advertises a tailor on Level 5090 whose motto is 'Be seen to be listening.'" },
          { id: 'news_column_se', propArt: 'news_column', x: 24, y: 19, label: 'Holo-Ticker News Column',
            description: "The fourth column plays tonight's guest list in a polite amber crawl. Several names have been blurred. One is blurred and then partly unblurred, as if somebody reconsidered." },
          { id: 'obsidian_bar', propArt: 'obsidian_bar', x: 18, y: 22, label: 'Obsidian and Electrum Bar',
            description: "A ten meter curved bar slab cut from a single piece of black obsidian, inlaid with electrum river patterns that glow with an internal blue light. Behind it, rows of crystal bottles catch the amber from the chandelabra. The sommelier droid mixes beneath them with impeccable fluidity. Rare off world drinks can be ordered here." },
          { id: 'fountain_west', propArt: 'holo_fountain', x: 12, y: 24, label: 'Aurebesh Water-Holo Fountain',
            description: "Purified water cascades in glassy sheets down a vertical slate panel while micro projectors paint rising Aurebesh proverbs on the fall: PEACE, ORDER, PROSPERITY. A slicer's eye notices that the projector feed is not locked. It could be told to say something else.",
            grantsCodex: 'codex-black-market-coords' },
          { id: 'fountain_projector_panel', propArt: 'power_tap', x: 14, y: 25, label: 'Fountain Projector Feed Panel',
            requiresNoneFlags: ['fountain_coords_found'],
            description: "A service panel at the foot of the west fountain, its seal a courtesy tag. The projector feed is unlocked and the Aurebesh layer can be overridden by anyone with a slicer's patience. Behind the proverbs there is a second layer of numbers.",
            triggersMinigame: 'terminal_slicing', once: true, grantsFlag: 'fountain_coords_found' },
          { id: 'fountain_east', propArt: 'holo_fountain', x: 27, y: 24, label: 'Aurebesh Water-Holo Fountain',
            description: "The twin of the west fountain, its proverbs scrolling a little slower. Coins have been tossed in the basin by guests who never mention it. Most are credit chips with the Senate seal. A few are older." },
          { id: 'decanter_vintage', propArt: 'decanter_pedestal', x: 23, y: 22, label: 'Biometric Wine Decanter Pedestals',
            requiresFlag: 'senate_aide_bribed',
            lockedMessage: 'Two slender decanters of amber and garnet spirits, each locked behind a biometric palm scanner. The scanner flashes: REGISTERED SENATORS ONLY. A bribed aide could vouch for you at the scanner.',
            description: "The scanner reads a forged high society pass, courtesy of a bribed aide, and releases the lock with a courteous chime. The Alderaanian vintage inside has been aging for forty years. You pocket the bottle with the ease of a guest who belongs.",
            once: true, grantsItem: 'alderaanian_vintage' },
          { id: 'decanter_reserve', propArt: 'decanter_pedestal', x: 14, y: 22, label: 'Biometric Spirit Decanters',
            requiresFlag: 'luxury_landspeeder_permit_seen',
            requires: { item: 'luxury_landspeeder_permit' },
            lockedMessage: 'Two slender decanters of spirits, each locked behind a biometric hand scanner. The scanner is unimpressed by your hand.',
            description: "The scanner reads the Ubrikkian Lux-Skiff permit and takes it for proof of high society standing. The lock opens with a chime and a Corellian Reserve ready for the pouring.",
            once: true, grantsItem: 'corellian_vintage' },
          { id: 'terrarium_blackmail', propArt: 'terrarium', x: 15, y: 10, label: 'Alderaanian Flora Terrarium',
            requiresFlag: 'horace_job_accepted',
            lockedMessage: 'A spherical transparisteel globe on a brass repulsor mount, glowing Alderaanian silk ferns climbing a lattice. It is beautiful. Nothing about it suggests it is hiding anything.',
            description: "Inside the globe, among the glowing silk ferns, a second glow that is not a plant: a small data chip taped under the soil tray. Whoever was blackmailing the Senator picks up here. You peel the chip away and walk on.",
            once: true, grantsFlag: 'blackmail_chip_found', grantsItem: 'senate_blackmail_chip' },
          { id: 'terrarium_sis_drop', propArt: 'terrarium', x: 24, y: 10, label: 'Kashyyyk Orchid Terrarium',
            requiresFlag: 'kaelen_sis_contract',
            lockedMessage: 'A floating globe of Kashyyyk orchids, their violet bells nodding on the repulsor draft. It looks harmless. It is hard not to feel like it is looking back.',
            description: "A hollow in the orchid planter holds a slim, flesh-toned slicer key: an SIS dead drop, exactly where Agent Kaelen said a junior staffer might 'accidentally' leave something. You pocket it.",
            once: true, grantsFlag: 'sis_key_found', grantsItem: 'sis_surveillance_key' },
          { id: 'terrarium_gallery', propArt: 'terrarium', x: 33, y: 10, label: 'Glass Terrarium',
            description: "A third floating globe, this one home to a single glowing Alderaanian silk fern the size of a child's hand. Its fronds turn slowly toward the viewport as if awaiting sunrise on a world that no longer sets there." },
          { id: 'model_case_flagships', propArt: 'model_case', x: 19, y: 10, label: 'Antique Starship Model Display Case',
            description: "A vacuum sealed case of gold leaf miniatures: early Republic battle cruisers, Kuati diplomatic couriers from the Mandalorian Wars, a Hammerhead corvette with every rivet in place. Tiny internal components turn and pulse. A brass plate: PRESENTED TO THE SENATE BY THE SHIPWRIGHTS OF KUAT.",
            grantsCodex: 'codex-starship-models' },
          { id: 'model_case_suite', propArt: 'model_case', x: 4, y: 16, label: 'Vacuum Sealed Model Case',
            description: "A smaller case holds a single model: a pre-Republic survey ship with its engine bells polished to a mirror. It is the only thing in the senators' private room that no one has ever tried to sell." },
          { id: 'brass_register_a', propArt: 'brass_register', x: 16, y: 15, label: 'Heated Sub-Deck Floor Grate',
            description: "A brass register set into the obsidian tile gently vents warm, perfumed air across the walkway. A drop of spilled drink hits the grate and evaporates with a faint hiss." },
          { id: 'brass_register_b', propArt: 'brass_register', x: 23, y: 15, label: 'Heated Sub-Deck Floor Grate',
            description: "The warm air carries amber incense and a note of crushed spice. Guests have drifted toward the grate in clumps, talking out of the sides of their mouths." },
          { id: 'brass_register_c', propArt: 'brass_register', x: 17, y: 25, label: 'Heated Sub-Deck Floor Grate',
            description: "Warm air from below fans the hems of long coats. The brass is polished by a thousand heel taps." },
          { id: 'brass_register_d', propArt: 'brass_register', x: 22, y: 25, label: 'Heated Sub-Deck Floor Grate',
            description: "The register near the entrance runs hot enough to dry a wet boot. A tray droid has learned to hover directly over it and keep the appetizers warm." },
          // ---------- Private rooms (original content preserved and expanded)
          { id: 'senator_conversation', propArt: 'booth', propVariant: 'senators', x: 5, y: 14, once: true, label: 'Overhear Senate Conversation', iconKind: 'booth',
            requiresFlag: 'syndicateManagement_active',
            lockedMessage: 'Two senators sit in the private room speaking in undertones. A well dressed aide watches you from the door. Without a standing syndicate behind you, you have no business eavesdropping here.',
            description: 'Two senators speaking in undertones. Procurement codes. Defense budget line items. Words that should not be said here. You catch enough to know this is leverage.',
            triggersMinigame: 'interrogation',
            grantsFlag: 'senate_intel_acquired',
            grantsCodex: 'codex-black-sun' },
          { id: 'landspeeder_showroom', propArt: 'lux_skiff', x: 34, y: 15, once: true, label: 'Ubrikkian Lux-Skiff Showroom', iconKind: 'panel',
            description: 'A Lux-Skiff configured for Senate-district transit, pearl coachwork and electrum trim under a spotlight. The permit alone is worth 200 credits to the right broker. The owner is currently in session. The registration terminal is unattended.',
            grantsFlag: 'luxury_landspeeder_permit_seen', grantsItem: 'luxury_landspeeder_permit' },
          { id: 'shipping_authority_terminal', propArt: 'console', propVariant: 'shipping', x: 32, y: 22, once: false, label: 'Coruscant Shipping Authority Terminal', iconKind: 'terminal',
            triggersMinigame: 'signal_siphon',
            grantsFlag: 'manifest_falsified',
            description: 'The Senate-adjacent manifest clearance terminal. Freight routes, Senate supply chains, diplomatic cargo exemptions. Access requires a slicing run.' },
          { id: 'senate_slush_funds', propArt: 'console', propVariant: 'slush', x: 36, y: 24, once: true, label: 'Discretionary Fund Terminal', iconKind: 'panel',
            description: 'A discretionary account terminal left unlocked between sessions. The authorization window is still open.',
            grantsItem: null },
          { id: 'sabacc_table_garnet', propArt: 'sabacc_table', x: 4, y: 23, label: 'High Stakes Sabacc Table, Garnet',
            description: "An octagonal table of polished Choraxian wood with embedded card scanners, credit chip trays and a blue holographic dealer eye. A corrupt committee chairman, a visiting Czerka lobbyist and a Corellian importer are three hands in. The table limit is a number nobody says aloud.",
            triggersMinigame: 'sabacc' },
          { id: 'sabacc_table_azure', propArt: 'sabacc_table', x: 7, y: 23, label: 'High Stakes Sabacc Table, Azure',
            description: "The second table, azure felt, a dealer eye that blinks in a slow tide. Every seat is taken by someone who owes someone else. A cigar of rare Corellian leaf burns, forgotten, in a brass tray.",
            triggersMinigame: 'sabacc' },
          { id: 'skylane_launch_berth', propArt: 'speeder_bay', x: 36, y: 8, label: 'Skylane Launch Berth', iconKind: 'beacon',
            description: "A private repulsor berth cantilevered off the east end of the promenade, where senators keep racing skiffs for when the Plaza gets too small. The lane control board is live: step in and the skyway opens in front of you, orange ahead, white oncoming, with a drop on either side.",
            triggersMinigame: 'sky_evasion' },
          { id: 'airtaxi_senatorial', propArt: 'airtaxi', propVariant: 'clean', x: 24, y: 26, once: false, iconKind: 'beacon', label: 'Air-Taxi Fast-Transit Terminal', description: 'Senate District AirTaxi terminal. A yellow and black repulsor air taxi lands on the pad beyond the glass every few minutes, doors gliding open on a hiss of engine heat.' },
        ],
        collectibles: [{ id: 'senate_slush_funds_cr', x: 35, y: 24, label: 'Senate Slush Fund Transfer', reward: 150 }],
        npcs: [
          { id: 'horace_vane', x: 5, y: 9, kind: 'senator_horace', label: 'Senator Horace Vane', patrol: [[34, 9], [5, 9]],
            repeatPrompt: 'Horace Vane glances at you over his glass and keeps walking. "Not now. I am being watched."',
            phases: [
              { id: 'phase_horace_intro',
                requiresNoneFlags: ['horace_job_accepted', 'horace_job_done', 'horace_chip_kept'],
                prompt: "He is an aging Core aristocrat in lavish burgundy silks, gold embroidered shoulder wraps and enough gemstone rings to bid on a small ship. An electrum tipped cane clicks with every step. He paces the viewport, sipping from a crystal glass, speaking to nobody. \"You. You have the look of someone who does not work for the Senate.\" He lowers his voice. \"I owe Czerka Corporation more than a man in my position should be able to owe. Someone has found out how much, and what I did to be allowed to owe it.\"",
                choices: [
                  { text: '"You need a problem made to disappear. I can do that."', morality: -3, loyalty: { underworld: 5 }, result: "\"A direct person. Refreshing.\" He looks at the cane, the glass, the view. \"A slicer who calls himself Lantern is blackmailing me. The drop is somewhere in this lounge. One of the terrariums, I would guess, he is theatrical. Find the chip. Bring it to me. Eight hundred credits.\"", grants: { flags: ['horace_met', 'horace_job_accepted'] } },
                  { text: '[Charm] "Senator, a man of your standing deserves a better arrangement."', morality: 0, loyalty: { underworld: 3 }, requires: { flag: 'buff_charisma' }, result: "He actually smiles. \"At last, someone with manners.\" He leans closer. \"Lantern. A terrarium. Fifteen hundred credits, and my continued gratitude, which has been known to move budgets.\"", grants: { flags: ['horace_met', 'horace_job_accepted', 'horace_pays_double'] } },
                  { text: '"Who is Lantern, and what does he have?"', morality: 3, loyalty: {}, result: "\"A name, a chip and an excellent sense of timing.\" He grips the cane. \"I do not know what is on it exactly, but I know that if it is released, I will be a footnote.\"", grants: { flags: ['horace_met'] }, defer: true },
                  { text: 'Say nothing and move on.', morality: 0, loyalty: {}, defer: true, result: 'He watches you go with something like betrayal and keeps pacing.' },
                ] },
              { id: 'phase_horace_pending',
                requiresAllFlags: ['horace_job_accepted'],
                requiresNoneFlags: ['horace_job_done', 'horace_chip_kept'],
                prompt: "Horace is at the glass again, but his glass is empty and his hand is not steady. \"Well? Have you found Lantern's chip?\"",
                choices: [
                  { text: '"Here is the chip. It is yours."', morality: 5, loyalty: { republic: 3 }, requires: { item: 'senate_blackmail_chip' }, result: "He takes it and crushes it under a heel without looking at the contents. Credits shift into your account. \"Eight hundred. My thanks. The Senate does not forget a favor.\"", grants: { flags: ['horace_job_done'], credits: 800 } },
                  { text: '[Keep the chip. A senator in your debt is worth more than eight hundred credits.]', morality: -8, loyalty: { underworld: 10 }, requires: { item: 'senate_blackmail_chip' }, result: "You let him see it and then you put it away. His face goes the color of his robes. \"What do you want.\" \"For now, nothing. Remember who has it.\"", grants: { flags: ['horace_chip_kept'] } },
                  { text: '"Not yet. Still looking."', morality: 0, loyalty: {}, defer: true, result: '"Please hurry. Lantern has begun sending me proofs."' },
                ],
                repeatPrompt: '"The chip. Have you found Lantern\'s chip?"' },
              { id: 'phase_horace_done',
                requiresAllFlags: ['horace_job_done'],
                prompt: "Horace raises his refilled glass. \"The Core is a smaller place for people like you and me than it looks. Dine with me when the Senate recesses.\"",
                choices: [{ text: '"Gladly."', grants: {} }],
                repeatPrompt: 'Horace nods warmly across the room. The cane clicks.' },
              { id: 'phase_horace_kept',
                requiresAllFlags: ['horace_chip_kept'],
                prompt: "Horace will not look at you. His hand is on the cane's head and his knuckles are white. \"Whatever you want, I will arrange it. Do not ask me to enjoy it.\"",
                choices: [{ text: '"I will let you know."', grants: {} }],
                repeatPrompt: 'Horace pretends to study the skyline. He is clearly counting steps to the nearest exit.' },
            ],
          },
          { id: 'baroness_moira_kuat', x: 27, y: 17, kind: 'kuati_baroness', label: 'Baroness Moira Kuat',
            repeatPrompt: 'Moira does not look up from her datapad. "The offer stands, as does the price."',
            phases: [
              { id: 'phase_moira_intro',
                requiresNoneFlags: ['moira_met'],
                prompt: "A statuesque Kuati noblewoman in a structured navy gown with silver shoulder pauldrons, her hair in a braided tower pinned with silver. She sits in the pit as if she had been poured into it, assessing trade proposals on a datapad. \"A visitor.\" Her tone is the temperature of orbital space. \"I am Moira Kuat. I handle a number of introductions on behalf of House Kuat and its orbital interests. If you are here to sell me something, be brief. If you are here to buy something, be brief and honest.\"",
                choices: [
                  { text: '"I want access to Kuat Drive Yards. I understand you hold the door."', morality: 0, loyalty: { underworld: 3 }, result: "\"I hold several. They open to different people.\" She considers you. \"A letter of introduction to the Executive Ring, signed in my hand, five hundred credits. It will open a door. It will not make you welcome.\"", grants: { flags: ['moira_met', 'moira_letter_offered'] } },
                  { text: '[Hold her gaze.] "I know what your family built over Kuat, and what it costs."', morality: 3, loyalty: { republic: 3 }, requires: { flag: 'buff_composure' }, result: "For the first time she looks at you rather than past you. \"You are less tedious than you appear.\" She signs a slip. \"The letter is yours without the fee. House Kuat likes to be known to people who know it.\"", grants: { flags: ['moira_met', 'moira_letter'] , items: ['kdy_courtesy_letter'] } },
                  { text: '"Just admiring the view."', morality: 0, loyalty: {}, result: '"The view is the only thing in this room that is not for sale." She returns to her datapad.', grants: { flags: ['moira_met'] } },
                ] },
              { id: 'phase_moira_dismissed',
                requiresAllFlags: ['moira_met'],
                requiresNoneFlags: ['moira_letter_offered', 'moira_letter'],
                prompt: "Moira does not look up. \"Still here. If you want something, say it. If you want to look at the view, there are cheaper places to stand.\"",
                choices: [
                  { text: '"I want access to Kuat Drive Yards after all."', morality: 0, loyalty: { underworld: 3 }, result: "\"A letter of introduction to the Executive Ring. Five hundred credits.\" She says it as though she were reading it from a menu.", grants: { flags: ['moira_letter_offered'] } },
                  { text: '"No, nothing."', morality: 0, loyalty: {}, defer: true, result: 'She returns to her datapad. The audience was always optional.' },
                ],
                repeatPrompt: 'Moira studies her datapad. You are not part of the room.' },
              { id: 'phase_moira_letter_offered',
                requiresAllFlags: ['moira_letter_offered'],
                requiresNoneFlags: ['moira_letter'],
                prompt: "Moira lays the datapad aside. \"You have reconsidered the letter. A sensible woman does.\" She waits.",
                choices: [
                  { text: '"Five hundred credits. Write it."', morality: 0, loyalty: { underworld: 3 }, requires: { credits: 500 }, result: "She signs with a flourish and presses a brass seal into wax. \"Present this to the Captain at the threshold. Do not mention my name unless he mentions it first.\"", grants: { flags: ['moira_letter'], items: ['kdy_courtesy_letter'] } },
                  { text: '"Not yet."', morality: 0, loyalty: {}, defer: true, result: '"The Ring will not wait for your finances."' },
                ],
                repeatPrompt: '"The letter. Five hundred credits."' },
              { id: 'phase_moira_after',
                requiresAllFlags: ['moira_letter'],
                prompt: "Moira lifts one finger and a tray droid veers politely away. \"The Executive Ring is quieter than the Senate and considerably more dangerous. Do not be seen to look at the walls.\"",
                choices: [{ text: '"I will remember."', grants: {} }],
                repeatPrompt: 'Moira inclines her head by exactly one degree. The audience is over.' },
            ],
          },
          { id: 'agent_kaelen_sis', x: 22, y: 23, kind: 'sis_agent', label: 'Agent Kaelen (SIS)',
            repeatPrompt: 'Kaelen does not look up from his datapad. "Eyes front. Talk to the bar like it is the bar."',
            phases: [
              { id: 'phase_kaelen_intro',
                requiresNoneFlags: ['kaelen_sis_contract', 'kaelen_job_done'],
                prompt: "A man in a crisp, unadorned grey duty tunic leans on the bar pretending to read a datapad. A flesh toned comm earpiece sits at his ear. His eyes never stop tracking the room: the Czerka lobbyist, the Black Sun woman at the rail, the Senator pacing. \"You have been looking at the same three people I have,\" he says, barely moving his lips. \"Junior staffer, Senate Office of Procedures. That is what the badge says. Let us leave it there.\"",
                choices: [
                  { text: '"Republic Special Intelligence. You are not a junior staffer."', morality: 3, loyalty: { republic: 5 }, result: "A very small flicker. \"Quietly.\" He angles the datapad away. \"The Senate has a leak and a Black Sun channel inside it. I need someone nobody connects to the Service. There is a dead drop in one of the orchid globes. Fetch the key I left and bring it back to me. Six hundred credits.\"", grants: { flags: ['kaelen_met', 'kaelen_sis_contract'] } },
                  { text: '[Perception] "Your earpiece, your shoes, and the way you stand. The Service trains its people too well."', morality: 3, loyalty: { republic: 5 }, requires: { flag: 'buff_perception' }, result: "He almost laughs. \"Tarisian ale makes everyone observant. All right. Kaelen, SIS.\" He slides a code across the bar. \"The orchid globe has a key in it. Bring it to me, nine hundred credits, and I will remember you favorably when the next file lands.\"", grants: { flags: ['kaelen_met', 'kaelen_sis_contract', 'kaelen_trusts'] } },
                  { text: '"Not my problem. Enjoy the view."', morality: 0, loyalty: {}, result: '"Everybody says that. Until it is." He goes back to his datapad.', grants: { flags: ['kaelen_met'] } },
                ] },
              { id: 'phase_kaelen_pending',
                requiresAllFlags: ['kaelen_sis_contract'],
                requiresNoneFlags: ['kaelen_job_done'],
                prompt: "Kaelen lowers the datapad a centimeter. \"The drop. The orchid globe. Did you find it?\"",
                choices: [
                  { text: '"Here is the key."', morality: 5, loyalty: { republic: 5 }, requires: { item: 'sis_surveillance_key' }, result: "The key vanishes into his sleeve. Credits slide into your account. \"Six hundred, and a favor owed. Walk away like you were never here.\"", grants: { flags: ['kaelen_job_done'], credits: 600 } },
                  { text: '"Still looking."', morality: 0, loyalty: {}, defer: true, result: '"Do not let the globe see you looking."' },
                ],
                repeatPrompt: '"The orchid globe. The key. Quietly."' },
              { id: 'phase_kaelen_done',
                requiresAllFlags: ['kaelen_job_done'],
                prompt: "Kaelen studies the Senator across the room. \"The Service owes you. I do not say that often. When the file breaks, you will hear it from me before the holonet.\"",
                choices: [{ text: '"I will be listening."', grants: {} }],
                repeatPrompt: 'Kaelen nods once and returns to watching the room.' },
            ],
          },
          { id: 'black_sun_vigo', x: 30, y: 8, kind: 'black_sun_envoy', label: 'Malis',
            phases: [
              { id: 'phase_malis_intro',
                requiresNoneFlags: ['black_sun_allied', 'black_sun_hostile'],
                prompt: 'A woman in Senate-blue finery, no datapad, no aide, a black sun brooch half hidden at her collar. She stands at the railing with the whole of Coruscant spread beneath her and the landing pad lights below. She turns when you approach. "You have been making noise. I appreciate noise, when it points in the right direction. My name is Malis. I represent certain interests that extend considerably further than this level."',
                choices: [
                  { text: '"What interests?"', grants: {}, defer: true },
                  { text: '"Black Sun."', grants: {}, defer: true, result: 'The corner of her mouth moves. "Close enough. We prefer \'extended trade network.\' Our attorneys are very particular."' },
                  { text: '"I read your meeting point in the fountain water."', morality: 3, loyalty: {}, requires: { flag: 'fountain_coords_found' }, result: 'The smile stops and starts again. "Clever. Slicers are always clever until they are expensive." She glances at the fountain, then at you. "Perhaps we should talk after all."', grants: { flags: ['malis_impressed'] }, defer: true },
                  { text: '"I am not interested in partners."', grants: { questFlags: 'black_sun_hostile' }, morality: 0 },
                ] },
              { id: 'phase_malis_hostile',
                requiresAllFlags: ['black_sun_hostile'],
                requiresNoneFlags: ['black_sun_allied'],
                prompt: '"You are still breathing, which means you are still useful." Malis examines her nails. "My offer stands. Even enemies can have working arrangements."',
                choices: [
                  { text: '"Working arrangement. Define terms."', grants: { questFlags: 'black_sun_allied' }, morality: -5 },
                  { text: '"We are done here."', grants: {} },
                ] },
              { id: 'phase_malis_allied',
                requiresAllFlags: ['black_sun_allied'],
                prompt: '"Our channels are open." She inclines her head fractionally. "Your recent work in the sub-levels was noted. Clean. Efficient. We have a contract that would suit your capabilities."',
                choices: [
                  { text: '"I am listening."', grants: { questFlags: 'black_sun_contract_offered' }, morality: -5 },
                  { text: '"Not today."', grants: {} },
                ] },
            ],
          },
          { id: 'senator_aide', x: 21, y: 26, kind: 'republic_guard', label: 'Senate Aide',
            phases: [
              { id: 'phase_aide_gate',
                prompt: 'The aide holds a datapad at parade rest. They assess you once and look away. "This floor is for registered Senate personnel and cleared guests. I assume your clearance is in order."',
                choices: [
                  { text: '"Of course."', grants: {} },
                  { text: '"I have business with one of the senators."', grants: {} },
                  { text: '[Offer a bribe.]', grants: { questFlags: 'senate_aide_bribed' }, morality: -5, requires: { credits: 300 }, result: 'The datapad shifts slightly. Their expression does not. After a pause, they say nothing and step aside. The silence costs you 300 credits.' },
                ] },
            ],
          },
          // ---------- The staff: droids
          { id: 'lux4_sommelier', x: 18, y: 20, kind: 'lux_sommelier', label: 'LUX-4 Sommelier and Mixologist', repeatable: true,
            prompt: "A rose gold protocol chassis in a crisp black synth silk bow tie works behind the obsidian bar with flawless fluidity, pouring a multilayered glowing cocktail without looking at it. \"Good evening. LUX-4, at your service. I have been programmed with the complete vintage lists of nine worlds, and the preferences of every senator present. How may I assist you? The effects of my recommendations last until you leave the lounge.\"",
            choices: [
              { text: 'Order a Corellian Reserve (60 credits).', morality: 0, loyalty: {}, requires: { credits: 60 }, result: "He pours it over a single sphere of ice and slides it across with a faint, tasteful bow. Warmth loosens your shoulders and settles your breathing. You feel unshakeable, for the length of your visit.", grants: { flags: ['buff_composure'] } },
              { text: 'Order a Tarisian Ale (40 credits).', morality: 0, loyalty: {}, requires: { credits: 40 }, result: "A tall glass with a green froth, sharp and clean. The room sharpens a notch: you hear the dealer eye's tick, the clink of the tray droid's glasses, a whispered word two pits away. Your perception is up until you leave.", grants: { flags: ['buff_perception'] } },
              { text: 'Order an Alderaanian Nectar (90 credits).', morality: 0, loyalty: {}, requires: { credits: 90 }, result: "It arrives in a flute, pale gold with a faint glow. It tastes of a spring that no longer exists. Your words come easy and your smile lands. Your charm lasts until you leave the lounge.", grants: { flags: ['buff_charisma'] } },
              { text: '"Just the view, thank you."', morality: 0, loyalty: {}, result: '"Of course. The view is complimentary, as is my silence."' },
            ] },
          { id: 'sv_tray_one', x: 15, y: 15, wander: 6, kind: 'sv_tray_droid', label: 'SV-Series Tray Attendant',
            repeatPrompt: 'The tray droid drifts a polite arm\'s length away and waits for you to take a glass.',
            prompt: 'A one meter disk of mirror chrome, silent on its repulsors, bearing crystal glasses and appetizers on a flat rubberized top. It drifts to waist height beside you. No sound, no wobble, and no sense of having been summoned.',
            choices: [{ text: 'Take a glass.', morality: 0, loyalty: {}, result: 'It dips two centimeters as you reach, then rises silently. The glass is cold. It leaves without a word.' }] },
          { id: 'sv_tray_two', x: 24, y: 13, wander: 6, kind: 'sv_tray_droid', label: 'SV-Series Tray Attendant',
            repeatPrompt: 'The tray droid hovers out of earshot of the nearest pit and waits.',
            prompt: 'A second chrome disk, patrolling the east pits. When a conversation rises in volume it drops to the floor level and stops dead, a silver lump beneath the discretion line.',
            choices: [{ text: 'Ask what it is serving.', morality: 0, loyalty: {}, result: 'It rotates twelve degrees, displaying a tidy tray: stuffed pastry, candied spice root, a glass of something amber. It makes no sound whatsoever.' }] },
          { id: 'sv_tray_three', x: 20, y: 24, wander: 5, kind: 'sv_tray_droid', label: 'SV-Series Tray Attendant',
            repeatPrompt: 'The tray droid hovers politely over the nearest floor register.',
            prompt: 'A third chrome attendant hovers over a warm floor register to keep its appetizers from cooling. It rises and falls with each guest it passes.',
            choices: [{ text: 'Say thank you.', morality: 0, loyalty: {}, result: 'It tips toward you in what could be a bow and drifts toward the pits.' }] },
          { id: 'sweep_drone_west', x: 19, y: 15, patrol: [[19, 25], [19, 15]], kind: 'sweep_drone', label: 'Micro-Scan Sweep Drone',
            repeatPrompt: 'The drone scuttles on, a thin red line passing over you for the second time.',
            prompt: 'A golden insectoid droid ten centimeters long crawls along a light fixture, emitting a thin infrared line across the lounge. It tests for concealed heavy weapons, explosive residue and slicing devices. Its line crosses you and moves on, which may or may not mean you are clean.',
            choices: [{ text: 'Let it scan you.', morality: 0, loyalty: {}, result: 'The line passes over you twice, once from each side. The drone chirps a note too high for most ears and scuttles on.' }] },
          { id: 'sweep_drone_far_west', x: 10, y: 12, patrol: [[10, 24], [10, 12]], kind: 'sweep_drone', label: 'Micro-Scan Sweep Drone',
            repeatPrompt: 'The drone is already moving on.',
            prompt: 'Another golden sweeper works the west wall, antennae twitching as it passes a sunken pit.',
            choices: [{ text: 'Watch it work.', morality: 0, loyalty: {}, result: 'It stops at a decorative panel, taps it twice with a foreleg and moves on. Whatever was behind the panel is a matter for the Guard.' }] },
          { id: 'sweep_drone_east', x: 29, y: 10, patrol: [[29, 25], [29, 10]], kind: 'sweep_drone', label: 'Micro-Scan Sweep Drone',
            repeatPrompt: 'The drone scuttles away down the east wall.',
            prompt: 'A third drone patrols the east wall, its red scan line lingering on the Black Sun woman at the railing for exactly as long as protocol allows.',
            choices: [{ text: 'Follow its line.', morality: 0, loyalty: {}, result: 'The line lands on a Czerka delegate\'s attache case and pauses. The delegate turns the case slightly away. The drone moves on.' }] },
          // ---------- Delegates, socialites, a card table
          { id: 'twilek_delegate_ryl', x: 12, y: 16, wander: 1, kind: 'twilek_diplomat', label: 'Twi\'lek Delegate Ryl\'ae',
            repeatPrompt: 'Ryl\'ae smiles over her glass. "The silks are better than the politics."',
            prompt: "A Twi'lek diplomat in flowing amethyst silks and gold chains, lekku draped gracefully over her shoulders. She holds her glass at exactly the angle that suggests she has been listening to someone for hours. \"Welcome. The Senate is a beautiful way to waste a century.\"",
            choices: [{ text: 'Ask what she thinks of the Rimward appropriation.', morality: 0, loyalty: {}, result: '"It will pass. The pirates will be paid by someone. If you want to know who, follow the Senators who vote against it loudly."' }] },
          { id: 'alsakan_aristocrat_dessan', x: 25, y: 10, wander: 2, kind: 'alsakan_aristocrat', label: 'Lord Dessan of Alsakan',
            repeatPrompt: 'Lord Dessan adjusts a gold button. "I was saying something quotable."',
            prompt: 'A silver haired Alsakan aristocrat in an emerald tailcoat with gold buttons and a feathered cap, delivering a story to a half interested listener and enjoying every word of it.',
            choices: [{ text: 'Ask about Alsakan.', morality: 0, loyalty: {}, result: '"Alsakan is the heart of the Core. People say that very quietly because the Senate would prefer it were the heart of the Senate."' }] },
          { id: 'czerka_exec_haal', x: 3, y: 25, kind: 'czerka_executive', label: 'Czerka Executive Haal',
            repeatPrompt: 'Haal stares at the dealer eye. "I fold. I always fold. They keep dealing."',
            prompt: 'A Czerka executive in a dark suit with a blood red tie and a cybernetic eye implant that glows a fixed, unblinking red. He is three hands into a game he cannot win and will not stop playing.',
            choices: [{ text: 'Ask how the evening is going.', morality: 0, loyalty: {}, result: '"Czerka\'s numbers are up. Mine are down. The cards do not care which."' }] },
          { id: 'corrupt_chairman_oren', x: 6, y: 25, kind: 'alsakan_aristocrat', label: 'Chairman Oren Taa',
            repeatPrompt: 'Oren waves a hand. "Later. The cards are warm."',
            prompt: 'A committee chairman with a quick smile and a loose collar, one hand at his glass, the other tapping the credit tray in a pattern that means he has already lost more than he can explain.',
            choices: [{ text: 'Ask about the table limit.', morality: 0, loyalty: {}, result: '"The limit is whatever the table will bear. Tonight it bears a great deal."' }] },
          { id: 'czerka_exec_vess', x: 31, y: 19, wander: 2, kind: 'czerka_executive', label: 'Czerka Executive Vess',
            repeatPrompt: 'Vess tilts the case away from you. "Not for sale."',
            prompt: 'A younger Czerka executive in a charcoal suit and a red tie, an attache case chained to his wrist and a cybernetic eye that tracks everything but you.',
            choices: [{ text: 'Ask what is in the case.', morality: 0, loyalty: {}, result: '"Quarterly projections. If you looked at them you would be rich, or in jail, or both."' }] },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 38, 27, 'floor');
          carveRect(g, 1, 1, 38, 4, 'wall');   // viewport wall: the skyline vista is drawn over these tiles
          const room = (x0, y0, x1, y1, gaps) => { carveRect(g, x0, y0, x1, y1, 'wall'); carveRect(g, x0 + 1, y0 + 1, x1 - 1, y1 - 1, 'floor'); gaps.forEach(([gx, gy]) => pt(g, gx, gy, 'floor')); };
          room(1, 11, 9, 17, [[9, 13], [9, 14]]);     // senators' private room
          room(30, 11, 38, 17, [[30, 13], [30, 14]]); // Ubrikkian showroom
          room(1, 20, 9, 26, [[9, 22], [9, 23]]);     // high stakes sabacc salon
          room(30, 20, 38, 26, [[30, 22], [30, 23]]); // discretionary office
          pt(g, 19, 28, 'door'); pt(g, 20, 28, 'door');
          return g;
        },
      },

      spice_refining_vaults: {
        id: 'spice_refining_vaults', name: 'Spice-Refining Sub-Vaults', subtitle: 'Coruscant · Mid-Level Infrastructure · L.900',
        width: 38, height: 22, spawnPos: { x: 2, y: 11 }, textureId: 'coruscant',
        accent: '#9B59B6', accentGlow: 'rgba(155,89,182,0.3)', accentDim: '#5A2A7A',
        floorColor: '#1A0A28', floorAlt: '#220E38', wallDark: '#0C0414', wallLight: '#1E0C2C',
        bg: 'radial-gradient(circle at 30% 60%, #1A0428 0%, #0A0214 60%, #050208 100%)', ambient: 'embers', floorPattern: 'grate',
        decor: ['pipe', 'girder', 'warning_beacon', 'slag', 'cargo_crate', 'rubble'],
        doors: [
          { x: 1, y: 11, targetZone: 'level_1313_subvault', targetPos: { x: 27, y: 10 }, label: 'Level 1313 Sub-Vault' },
        ],
        worldObjects: [
          { id: 'black_market_exchange', propArt: 'console', propVariant: 'exchange_bm', x: 30, y: 3, once: false, label: 'Black Market Commodity Exchange', iconKind: 'terminal',
            triggersMinigame: 'contraband_market',
            description: 'A hidden trading node embedded in the refinery control interface. Five commodities. Live prices. No customs declaration required.' },
          { id: 'refinery_control', propArt: 'pressure_valve', propVariant: 'refinery', x: 10, y: 10, once: false, label: 'Refinery Valve Array', iconKind: 'panel',
            triggersMinigame: 'valve_override',
            grantsFlag: 'spice_batch_diverted',
            description: 'Manual valve controls for the spice processing line. Divert a batch. Adjust the flow. The foreman is three levels up.' },
          { id: 'droid_arena_entrance', propArt: 'pit_gate', propVariant: 'droid', x: 20, y: 18, once: false, label: 'Underground Droid Arena', iconKind: 'beacon',
            triggersMinigame: 'droid_arena',
            description: 'A converted pressurization chamber. The betting slips are still warm. Step in.' },
          { id: 'airtaxi_vaults', propArt: 'airtaxi', propVariant: 'worn', x: 11, y: 19, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal', description: 'Return to the sub-level transit network.' },
          { id: 'vault_stash', propArt: 'crate_stack', propVariant: 'stash', x: 35, y: 18, once: true, label: 'Hidden Vault Stash', iconKind: 'crate',
            description: 'A durasteel box welded behind a coolant drum. Someone did not come back for this.' },
        ],
        collectibles: [{ id: 'vault_stash_cr', x: 35, y: 19, label: 'Vault Hidden Stash', reward: 120 }],
        npcs: [
          {
            id: 'exchange_tariff_lord', x: 6, y: 4, kind: 'broker', label: 'Karrn',
            phases: [
              { requiresNoneFlags: ['karrn_hostile', 'karrn_deal'],
                prompt: 'A heavyset Mirialan seated at a portable accounting station, surrounded by datapads. He looks up without surprise. "You are not an Exchange auditor. That means you are either a competitor or a customer. I charge the same rate for both services, but the service is different."',
                choices: [
                  { text: '"What do you control here?"', grants: {} },
                  { text: '"I want a cut of the tariff lane."', grants: { questFlags: 'karrn_negotiating' }, morality: -5 },
                  { text: '"The Exchange is done in this sector."', grants: { questFlags: 'karrn_hostile' }, morality: -5 },
                ] },
              { requiresAllFlags: ['karrn_negotiating'],
                prompt: '"A cut." He sets down his stylus. "The tariff lane moves eighty thousand credits per cycle. I pay the right people, I keep three percent. You want in, you bring me access to the Senate shipping registry and we discuss numbers."',
                choices: [
                  { text: '"I have a manifest falsification key. Yours for twenty percent."', grants: { questFlags: 'karrn_deal' }, morality: -10 },
                  { text: '"Not yet. I need more information."', grants: {} },
                ] },
              { requiresAllFlags: ['karrn_deal'],
                prompt: '"Our arrangement is on record." Karrn nods once. "A pleasure doing business. Do not bring CSF into this sector."',
                choices: [{ text: '"Understood."', grants: {} }],
                repeatPrompt: '"Twenty percent, as agreed. Do not be late."',
              },
              { requiresAllFlags: ['karrn_hostile'],
                prompt: '"You declared war on the Exchange with that statement." He picks up his stylus again. "I have auditors. I have contacts. I have patience. Leave."',
                choices: [{ text: '"Noted."', grants: {} }],
              },
            ],
          },
          {
            id: 'devaronian_captain', x: 33, y: 4, kind: 'smuggler', label: 'Grix',
            phases: [
              { requiresNoneFlags: ['grix_recruited'],
                prompt: 'A Devaronian with filed-down horns and a scarred chin leans against the wall, cleaning a blaster component with practiced disinterest. "Grix. Import-export specialist. You look like someone who moves things that should not be moved. We might have things to discuss."',
                choices: [
                  { text: '"What kind of specialist?"', grants: {} },
                  { text: '"I run a network. I need people with contacts."', grants: { questFlags: 'grix_negotiating' }, morality: -5 },
                  { text: '"Not interested."', grants: {} },
                ] },
              { requiresAllFlags: ['grix_negotiating'],
                prompt: '"A network." The blaster component disappears into a pocket. "I have contacts on fourteen freight routes and three Senate-adjacent cargo exemption brokers. All off the books. You pay fair and I deliver on schedule. You have a network worth joining, I am interested. You do not, I walk."',
                choices: [
                  { text: '"The network is real. You are in."', grants: { questFlags: 'grix_recruited' }, morality: 0, result: 'Grix holds out a scarred hand. You shake it. The agreement is immediate and professional.' },
                  { text: '"I need to think on it."', grants: {} },
                ] },
              { requiresAllFlags: ['grix_recruited'],
                prompt: '"Your network, your call." He nods. "I report when the cargo clears. Not before."',
                choices: [{ text: '"Understood."', grants: {} }],
                repeatPrompt: '"All routes nominal. I report when it clears."',
              },
            ],
          },
          {
            id: 'ex_sis_slicer', x: 18, y: 8, kind: 'slicer', label: 'Vael',
            phases: [
              { requiresNoneFlags: ['vael_recruited'],
                prompt: 'A slight figure in a thermal hood, fingers moving over a portable console. They do not look up. "You are generating a lot of activity in the city networks. Interesting routing. Someone taught you to hide, but not well enough. I was SIS. I noticed." They pause. "Past tense."',
                choices: [
                  { text: '"What do you want?"', grants: {} },
                  { text: '"I could use someone with SIS training."', grants: { questFlags: 'vael_negotiating' }, morality: 0 },
                  { text: '"Who are you working for now?"', grants: {} },
                ] },
              { requiresAllFlags: ['vael_negotiating'],
                prompt: '"Someone with a network." The console folds with a click. "I left the SIS because of what I found in their files. What they authorize. I am selective now about who I work with. Show me the operation. If it has a purpose beyond pure credits, I am in."',
                choices: [
                  { text: '"The operation has purpose. The Iron Syndicate is finished."', grants: { questFlags: 'vael_recruited' }, morality: 5, result: 'A pause. A nod. "Then I am in. I work best offline, but I will stay in range."' },
                  { text: '"Credits are the purpose. That is the only purpose."', grants: {} },
                ] },
              { requiresAllFlags: ['vael_recruited'],
                prompt: '"I am in range." They glance up once. "Ask me what you need."',
                choices: [{ text: '"Stay sharp."', grants: {} }],
                repeatPrompt: '"Ready."',
              },
            ],
          },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 36, 20, 'floor');
          carveRect(g, 1, 1, 12, 5, 'wall'); carveRect(g, 2, 2, 11, 4, 'floor');
          carveRect(g, 25, 1, 36, 5, 'wall'); carveRect(g, 26, 2, 35, 4, 'floor');
          carveRect(g, 14, 13, 24, 20, 'wall'); carveRect(g, 15, 14, 23, 19, 'floor');
          pt(g, 1, 11, 'door');
          [[30,5],[31,5],[32,5],[6,5],[7,5],[19,13],[20,13]].forEach(([x,y]) => pt(g, x, y, 'floor')); // exchange alcove, tariff office and arena chamber doorways
          return g;
        },
      },

      undercity_outskirts: {
        id: 'undercity_outskirts', name: 'Undercity Outskirts', subtitle: 'Coruscant · Undercity · Ancient Infrastructure',
        width: 40, height: 24, spawnPos: { x: 2, y: 12 }, textureId: 'coruscant',
        accent: '#5C3A1E', accentGlow: 'rgba(92,58,30,0.3)', accentDim: '#3A200C',
        floorColor: '#150C08', floorAlt: '#1C1008', wallDark: '#080402', wallLight: '#100806',
        bg: 'radial-gradient(circle at 50% 80%, #180A04 0%, #060402 70%)', ambient: 'embers', floorPattern: 'grate',
        decor: ['rubble', 'pipe', 'girder', 'slag', 'warning_beacon', 'brazier'],
        doors: [
          { x: 1, y: 12, targetZone: 'the_works', targetPos: { x: 33, y: 12 }, label: 'The Works' },
        ],
        worldObjects: [
          { id: 'jedi_ruin_fragment', propArt: 'ruin_stone', x: 10, y: 5, once: true, label: 'Ancient Jedi Temple Outskirt', iconKind: 'panel',
            description: 'Moss-covered stone that does not match the architecture around it. Older. The Force does not speak here, but something resonates faintly under the weight of centuries. Whoever built this was speaking to something larger than themselves.',
            grantsItem: 'ancient_force_relic',
            grantsCodex: 'codex-jedi-ruins' },
          { id: 'toxic_waste_terminal', propArt: 'console', propVariant: 'reclaim', x: 30, y: 7, once: false, label: 'Reclamation Facility Controls', iconKind: 'panel',
            triggersMinigame: 'valve_override',
            grantsFlag: 'waste_reclamation_diverted',
            description: 'Waste processing controls for the undercity reclamation facility. The overflow valve is stuck open. Someone locked it intentionally.' },
          { id: 'arms_bench_station', propArt: 'workbench', propVariant: 'arms', x: 35, y: 5, once: false, label: 'Black-Market Arms Bench', iconKind: 'crate',
            triggersMinigame: 'arms_bench',
            description: 'A bench covered in components, tools, and two unfinished weapons. Whoever built this station knows what they are doing. The equipment is unlocked.' },
          { id: 'outcast_camp_fire', propArt: 'outcast_camp', x: 8, y: 18, once: true, label: 'Outcast Settlement', iconKind: 'brazier',
            description: 'A cluster of shelters built from scavenged hull plating. Survivors of forgotten levels. Some arrived by accident. Some by design. All of them are waiting.',
            grantsCodex: 'codex-undercity-outcasts' },
          { id: 'airtaxi_undercity', propArt: 'airtaxi', propVariant: 'worn', x: 20, y: 22, once: false, iconKind: 'beacon', label: 'AirTaxi Terminal', description: 'Undercity transit beacon. Rarely maintained.' },
          { id: 'ruin_scavenge', propArt: 'crate_stack', propVariant: 'ruin', x: 10, y: 8, once: true, label: 'Temple Scavenge Cache', iconKind: 'crate',
            description: 'Fragments of pre-Republic stonework pried loose by earlier explorers. The pieces are worth something to the right collector.' },
        ],
        collectibles: [{ id: 'ruin_scavenge_cr', x: 12, y: 8, label: 'Temple Ruin Fragments', reward: 100 }],
        npcs: [
          {
            id: 'disgraced_csf_inspector', x: 6, y: 18, kind: 'republic_guard', label: 'Marro',
            phases: [
              { requiresNoneFlags: ['marro_recruited'], requiresAllFlags: ['syndicateManagement_active'],
                prompt: 'A man in a faded CSF jacket, no insignia remaining. He sits at the edge of the outcast camp with a bottle he is not drinking from. "Inspector Marro. Former. The Bureau revoked my clearance when I filed a report they did not like. Fourteen years on the force and that is what I get." He looks up. "You are not from the camp."',
                choices: [
                  { text: '"What report?"', grants: {} },
                  { text: '"I run an operation. I could use someone with your contacts."', grants: { questFlags: 'marro_negotiating' }, morality: 0 },
                  { text: '"None of my business."', grants: {} },
                ] },
              { requiresAllFlags: ['marro_negotiating'],
                prompt: '"An operation." He sets the bottle down. "I know how CSF monitors black-market networks. I know which frequencies they watch and which they ignore. I know how to route activity so the heat accumulates slower." He meets your eyes. "I am not going to pretend this is legal. I need work that matters. Is yours?"',
                choices: [
                  { text: '"It matters. The Iron Syndicate is finished. We are what comes after."', grants: { questFlags: 'marro_recruited' }, morality: 0, result: 'He stands. The bottle stays. "Tell me where you need me."' },
                  { text: '"I cannot make that promise."', grants: {} },
                ] },
              { requiresAllFlags: ['marro_recruited'],
                prompt: '"I am watching the frequencies. You will know when CSF shifts attention."',
                choices: [{ text: '"Good."', grants: {} }],
                repeatPrompt: '"Nothing unusual on the grid. So far."',
              },
              { requiresNoneFlags: ['syndicateManagement_active'],
                prompt: 'A man in a faded CSF jacket sits at the edge of an outcast camp. He does not look up.',
                choices: [{ text: '"Keep to yourself."', grants: {} }],
              },
            ],
          },
          {
            id: 'rogue_sith_apprentice', x: 28, y: 6, kind: 'jedi', label: 'Kesh',
            phases: [
              { requiresNoneFlags: ['sith_contact', 'sith_rejected'],
                prompt: 'Pale face. Dark eyes. Robes that were once Sith Academy grey. The figure does not move as you approach. "You feel it too." A statement, not a question. "The city has eyes and none of them are friendly. I left my order for the same reason you left yours — or will. The dark side is not philosophy. It is infrastructure."',
                choices: [
                  { text: '"What do you want from me?"', grants: {} },
                  { text: '"I am listening."', grants: { questFlags: 'sith_contact' }, morality: -10 },
                  { text: '"I do not deal with Sith."', grants: { questFlags: 'sith_rejected' }, morality: 5 },
                ] },
              { requiresAllFlags: ['sith_contact'],
                prompt: '"A contact who can move between the underworld and the Senate without notice." Kesh tilts their head. "I have resources. I have training. I lack the local network you appear to be building. We help each other. Simple."',
                choices: [
                  { text: '"A working arrangement. What do you need?"', grants: { questFlags: 'sith_contract_offered' }, morality: -10 },
                  { text: '"I need to know more about who you answer to."', grants: {} },
                ] },
              { requiresAllFlags: ['sith_rejected'],
                prompt: '"A line you will cross eventually." Kesh does not move. "I will be here when that happens."',
                choices: [{ text: '"Do not hold your breath."', grants: {} }],
              },
            ],
          },
          {
            id: 'anzati_assassin', x: 20, y: 10, kind: 'assassin', label: 'The Anzati',
            phases: [
              { requiresNoneFlags: ['anzati_contracted', 'anzati_declined'],
                prompt: 'You almost walked past them. They are simply there when they were not a moment ago. No sound. No approach. A face that is difficult to look at directly — features arranged correctly but wrong somehow, with two vertical slits below the cheekbones that do not quite match the rest. "You run the new network." Their voice is soft and even. "I have heard. I have a particular skill. You have targets. I propose an arrangement."',
                choices: [
                  { text: '"What skill?"', grants: {}, result: '"I remove specific problems. Quietly. Permanently. I have been doing this for longer than Coruscant has had a Senate. My terms are simple: discretion and payment. I do not negotiate on method."' },
                  { text: '"I accept. You work for the network."', grants: { questFlags: 'anzati_contracted' }, morality: -15, result: 'A nod so slight you almost miss it. "I will make contact when a contract is available. You will not find me otherwise."' },
                  { text: '"I do not hire assassins."', grants: { questFlags: 'anzati_declined' }, morality: 5 },
                ] },
              { requiresAllFlags: ['anzati_contracted'],
                prompt: '"The network runs clean." A pause. "That is because of what I removed. You are welcome."',
                choices: [{ text: '"Stay available."', grants: {} }],
                repeatPrompt: '"Available."',
              },
              { requiresAllFlags: ['anzati_declined'],
                prompt: 'The figure is present but says nothing.',
                choices: [{ text: '"Move on."', grants: {} }],
              },
            ],
          },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 38, 22, 'floor');
          carveRect(g, 1, 1, 15, 10, 'wall'); carveRect(g, 2, 2, 14, 9, 'floor');
          carveRect(g, 22, 1, 38, 10, 'wall'); carveRect(g, 23, 2, 37, 9, 'floor');
          carveRect(g, 12, 14, 30, 22, 'wall'); carveRect(g, 13, 15, 29, 21, 'floor');
          pt(g, 1, 12, 'door');
          // NW building south entrance
          pt(g, 8, 10, 'floor');
          // NE building south entrance
          pt(g, 28, 10, 'floor');
          // South building north entrance
          pt(g, 20, 14, 'floor');
          // South building south passage connecting to AirTaxi door
          pt(g, 20, 22, 'floor');
          return g;
        },
      },

    },
  },
  ferrowake: {
    id: 'ferrowake', name: 'Ferrowake', travelCost: 60, startZoneId: 'outer_foundry',
    description: 'Slag rivers and furnace light. The ore here has started whispering.',
    zones: {
      outer_foundry: {
        id: 'outer_foundry', name: 'Outer Foundries', subtitle: 'Ferrowake · Forge Moon',
        width: 26, height: 20, spawnPos: { x: 2, y: 10 }, textureId: 'ferrowake',
        accent: '#FF9A5A', accentGlow: 'rgba(255,154,90,0.28)', accentDim: '#7A3C1E',
        floorColor: '#3A2418', floorAlt: '#472C1D', wallDark: '#120A06', wallLight: '#20120A',
        bg: 'radial-gradient(circle at 70% 80%, #2A150A 0%, #150D08 70%)', ambient: 'embers',
        decor: ['pipe', 'girder', 'slag'],
        doors: [],
        npcs: [
          { id: 'fw_broker', x: 4, y: 9, kind: 'broker', label: 'Foundry Broker',
            prompt: '"Three workers stopped sleeping right after the last shipment. I can pull them off the line, or pretend I never noticed."',
            choices: [
              { text: 'Insist he pulls the workers off immediately.', morality: 15, loyalty: { republic: 5 }, result: 'He grumbles but agrees. Fewer shards ship out tainted this week.' },
              { text: 'Tell him to keep production moving.', morality: -18, loyalty: { sithEmpire: 10, underworld: 8 }, result: 'The furnace glow feels colder tonight.' },
            ],
          },
        ],
        collectibles: [{ id: 'fw_shard', x: 2, y: 3, label: 'Second Shard: smelted into a weapon core', reward: 220 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 24, 18, 'floor');
          for (let lx = 8; lx <= 11; lx++) for (let ly = 1; ly <= 18; ly++) pt(g, lx, ly, 'lava');
          for (let lx = 18; lx <= 21; lx++) for (let ly = 1; ly <= 18; ly++) pt(g, lx, ly, 'lava');
          for (let cx = 8; cx <= 21; cx++) { pt(g, cx, 6, 'floor'); pt(g, cx, 13, 'floor'); }
          [[6,4],[6,5],[6,15],[6,16],[22,4],[22,5],[22,15],[22,16]].forEach(([x,y]) => pt(g,x,y,'wall'));
          return g;
        },
      },
    },
  },
  verdanth: {
    id: 'verdanth', name: 'Verdanth', travelCost: 90, startZoneId: 'sunken_temples',
    description: 'Root systems swallowed the old shrines centuries ago. This is where it started.',
    zones: {
      sunken_temples: {
        id: 'sunken_temples', name: 'The Sunken Temples', subtitle: 'Verdanth · Ruin World',
        width: 26, height: 20, spawnPos: { x: 2, y: 10 }, textureId: 'verdanth',
        accent: '#6FD9A0', accentGlow: 'rgba(111,217,160,0.25)', accentDim: '#2C5940',
        floorColor: '#1E3327', floorAlt: '#26402F', wallDark: '#091009', wallLight: '#132018',
        bg: 'radial-gradient(circle at 50% 60%, #14261C 0%, #081410 70%)', ambient: 'mist',
        decor: ['root', 'moss', 'rubble'],
        doors: [],
        npcs: [
          { id: 'vd_warden', x: 9, y: 4, kind: 'warden', label: 'Relic Warden',
            prompt: '"The temple remembers what was buried here. It will show itself to you, or bury you with it."',
            choices: [
              { text: 'Ask it to show you the truth, whatever the cost.', morality: -8, loyalty: {}, result: 'The ruins shift. Something ancient takes interest in you.' },
              { text: 'Ask the Warden to seal the chamber instead.', morality: 12, loyalty: { republic: 6 }, result: '"Not every door needs opening."' },
            ],
          },
        ],
        collectibles: [{ id: 'vd_shard', x: 6, y: 10, label: 'Final Shard: the temple heart', reward: 260 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 24, 18, 'floor');
          for (let wy = 7; wy <= 11; wy++) for (let wx = 10; wx <= 14; wx++) pt(g, wx, wy, 'water');
          for (let wy = 1; wy <= 6; wy++) { pt(g, 16, wy, 'water'); pt(g, 17, wy, 'water'); }
          pt(g, 12, 9, 'floor');
          [[18,5],[19,5],[20,5],[18,6],[20,6],[18,7],[20,7],[18,13],[19,13],[20,13],[18,14],[20,14],[18,15],[20,15]].forEach(([x,y]) => pt(g,x,y,'wall'));
          return g;
        },
      },
    },
  },
  kuat: {
    id: 'kuat', name: 'Kuat', travelCost: 1200, startZoneId: 'kdy_landing_bay',
    requiresFlag: 'senate_line_secured',
    description: 'The orbital ring never stops moving. Below it, Bador Moon keeps its secrets in glass and ash.',
    zones: {
      kuat_private_suites: {
        id: 'kuat_private_suites', name: 'KDY Orbital Ring — Private Suites',
        ships: [
          { id: 'ship_kuat_yacht', kind: 'kuat_yacht', x: 16, y: 16, label: 'House Kuat Courier Yacht',
            description: `A House Kuat courier yacht on the private pad, pearl white with gold trim and the KDY blue along the fuselage, hand polished. The cockpit is dark but the lower lights burn at idle. The registry plate carries Lady Kuat's personal code.`,
            grantsFlag: 'kuat_yacht_examined' },
        ],
        subtitle: 'Kuat Drive Yards · Executive Ring',
        width: 28, height: 22, spawnPos: { x: 2, y: 12 }, textureId: 'ferrowake',
        accent: '#1A8FD0', accentGlow: 'rgba(26,143,208,0.3)', accentDim: '#0A4A72',
        floorColor: '#1A1E26', floorAlt: '#20252E', wallDark: '#0A0C10', wallLight: '#141820',
        bg: 'radial-gradient(circle at 60% 30%, #14202E 0%, #060810 70%)', ambient: 'lounge',
        paAnnouncements: [
          'Executive ring guests are reminded that the yacht pad is private. Please do not photograph the registry plates.',
          'Refreshments are available in the Lady\'s writing room by appointment only.',
          'Ring Security thanks you for your cooperation with the Sentinel roster.',
          'A reminder that Captain Vaelen does not accept gratuities, letters, or conversation.',
        ],
        decor: ['neon_sign', 'girder'],
        doors: [
          { x: 0, y: 12, targetZone: 'kdy_landing_bay', targetPos: { x: 30, y: 15 }, label: 'Ring Transit' },
          { x: 0, y: 13, targetZone: 'kdy_landing_bay', targetPos: { x: 30, y: 16 }, label: 'Ring Transit' },
        ],
        worldObjects: [
          { id: 'ring_vista_suites_a', propArt: 'bvista_20x5', propVariant: 'ring_a', x: 10, y: 5, once: false, iconKind: 'vista', label: 'Executive Ring Observation Glass',
            description: `The transparisteel wall frames the orbital array in black space: Kuat's limb in amber city light, a shipyard hull under work lamps, and the arc of the ring running off to a bright shuttle.`, grantsCodex: 'codex-ring-architect-dead' },
          { id: 'ring_vista_suites_b', propArt: 'bvista_20x5', propVariant: 'ring_b', x: 18, y: 5, once: false, iconKind: 'vista', label: 'Executive Ring Skyline',
            description: `Hull frames hang in their cradles above Kuat, ribs lit amber, hundreds of workers tiny against the dark. A single gold line of transit lights stitches the array together.` },
          { id: 'kuat_viewing_port', iconKind: 'orbital_viewport', propArt: 'shipyard_viewport', x: 8, y: 7, label: 'Structural Viewing Port',
            description: `Through the transparisteel, Kuat's orbital shipyard runs for hundreds of kilometers. Dozens of Star Destroyers hang in perfect formation.` },
          { id: 'suite_cherit_portrait', iconKind: 'vista', propArt: 'wall_art', x: 13, y: 6, once: false, label: 'Small Framed Portrait',
            description: `A small portrait among the grand ones: a woman in plain intelligence grey, unsmiling. The brass plate reads CHERIT, NAVAL INTELLIGENCE. The frame has been turned to the wall once and turned back.`,
            grantsFlag: 'cherit_portrait_seen', grantsCodex: 'codex-ring-cherit' },
          { id: 'kuat_holobriefing', iconKind: 'holo_table', propArt: 'holo_table', propVariant: 'dossier', x: 14, y: 18, label: 'Mission Holobriefing Table',
            description: `Tanner's dossier still runs: Sector 4 grid, surface personnel files, Commander Vael's brief. At the top of the index, one line: OPERATION GHOST KEEL, ACCESS LEVEL 8.`,
            requiresFlag: 'tanner_briefed', lockedMessage: `The table is dark. Whoever briefs you will have to switch it on.`,
            once: true, grantsFlag: 'ghost_keel_briefed', grantsCodex: 'codex-kuat-operation' },
          { id: 'tanner_executive_terminal', iconKind: 'terminal', propArt: 'terminal', propVariant: 'executive', x: 20, y: 9, label: 'Executive Holonet Terminal',
            description: `The terminal links to KDY's private shipping network. Lady Kuat's routing code for the Oversight Committee relay is loaded and waiting for a clean signal.`,
            requiresFlag: 'registry_to_committee', lockedMessage: `Tanner's access codes are loaded, but there is nothing here worth sending yet.`,
            triggersMinigame: 'signal_siphon',
            once: true, grantsFlags: ['registry_transmitted', 'lady_kuat_mission_complete'], grantsCodex: 'codex-ring-oversight' },
          { id: 'grand_executive_safe', iconKind: 'safe', propArt: 'executive_safe', x: 24, y: 12, label: 'Grand Executive Safe',
            description: `A wall safe behind a gilt painting of the ring in its first year, the pirate scoring still visible on the hull. Two biometric layers. Inside: nobility rings, credit wafers and three red slates labeled with Moff surnames.`,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 4 },
            requiresFlag: 'lady_kuat_trust', lockedMessage: `The biometric lock does not respond. Someone with authority has to want this door opened.`,
            once: true, grantsFlag: 'executive_safe_opened', grantsItem: 'executive_blackmail_slate' },
          { id: 'historical_model_case', iconKind: 'ship_model_case', propArt: 'model_case', x: 22, y: 16, label: 'Display Case: KDY Historical Ship Models',
            description: `Nine gold-plated starship miniatures, each a production milestone. The first reads YEAR ONE, CORELLIAN CORVETTE COMMISSION. The last is unlabeled, a hull form that matches no published class. The lock was opened three days ago.`,
            once: true, grantsFlag: 'historical_models_examined', grantsCodex: 'codex-kdy-dreadnought' },
          { id: 'privileged_holonet_terminal', iconKind: 'terminal', propArt: 'terminal', propVariant: 'privileged', x: 24, y: 18, label: 'Privileged Holonet Terminal',
            description: `An unmonitored terminal on KDY's old private backbone, older than any censorship. It reaches any relay without a routing trace. Rael's committee contact code is waiting on the drive.`,
            requiresFlag: 'rael_helped', lockedMessage: `An unmonitored relay, but you have nothing to send through it yet.`,
            triggersMinigame: 'signal_siphon',
            once: true, grantsFlag: 'blueprints_transmitted', grantsItem: 'ring_committee_receipt' },
          { id: 'kuati_history_mural', iconKind: 'bronze_mural', propArt: 'bronze_mural', x: 4, y: 8, once: false, label: 'House Kuat Ancestral Mural',
            description: `A bronze relief three meters wide: the founding of Kuat Drive Yards, ten crests in a circle, the Vas-Kuat wheel at center. The center crest has been recast. The pre-war archive copy put it third from the left.`,
            grantsCodex: 'codex-ring-ten-families' },
          { id: 'luxury_panoramic_viewport', iconKind: 'panoramic_viewport', propArt: 'panoramic_viewport', x: 22, y: 6, label: 'Executive Ring Panoramic Viewport',
            description: `Two meters tall, four wide, triple-layered transparisteel. Kuat's ring-world curves away in both directions. A construction progress display glows on the lower pane.` },
          { id: 'executive_protocol_droid_unit', iconKind: 'protocol_droid_stand', propArt: 'protocol_droid', x: 16, y: 12, label: 'KDY Executive Protocol Droid',
            description: `Slender brass and copper, photoreceptors dim in standby, the KDY crest etched in its chest. The service card reads RUEN-7, HOUSE KUAT STAFF ONLY. Last activated by Lady Kuat, two days ago.` },
          { id: 'suite_rug', iconKind: 'vista', propArt: 'lounge_rug', x: 21, y: 14, once: false, label: 'Kuati Writing Room Rug',
            description: `A hand-knotted garnet and gold rug under the Lady's writing desk, the Kuat wheel woven in the center.` },
          { id: 'suite_chandelier', iconKind: 'vista', propArt: 'repulsor_chandelier', x: 16, y: 15, once: false, label: 'Kinetic Repulsor Chandelier',
            description: `A ring of floating brass lamps turns slowly under a repulsor core, throwing warm amber light across the hall.` },
          { id: 'suite_pedestal', iconKind: 'vista', propArt: 'art_pedestal', x: 25, y: 8, once: false, label: 'Gilt Sculpture Pedestal',
            description: `A lit pedestal holds a cast bronze keel, a tiny hull frame no bigger than a hand. A card reads: FIRST FRAME, YEAR ONE.` },
          { id: 'suite_terrarium', iconKind: 'vista', propArt: 'terrarium', x: 2, y: 18, once: false, label: 'Alderaanian Terrarium',
            description: `A glass terrarium of Alderaanian moss and a single pale orchid, humidified by a hidden pump. Someone waters it every day.` },
        ],
        npcs: [
          { id: 'pilot_suites_veyr', x: 26, y: 14, kind: 'kdy_pilot', label: 'Pilot Dax Veyr (KDY Shuttle)', triggersOverlay: 'shuttle',
            greeting: `"Executive pad is clear. Shuttle K-14 can have you on Bador in minutes."`,
            repeatPrompt: `The pilot taps the shuttle manifest and waits for a destination.`,
            prompt: `A KDY shuttle pilot in the blue Kuati flight uniform.`, choices: [] },
          { id: 'captain_vaelen', x: 4, y: 12, kind: 'kdy_executive_sentinel', label: 'Captain Vaelen',
            phases: [{ id: 'p_main', requiresAllFlags: [] }],
            hideAfterFlags: ['vaelen_cleared'],
            repeatPrompt: `"Your access status has not changed."`,
            prompt: `Ivory and gold armor, older than any current contract, fills the corridor mouth. The visor never turns. "Ring-Sec clears the gate. The Sentinel roster clears the suites. Those are different lists."`,
            choices: [
              { text: `Present the KDY Director override token.`, morality: 0, loyalty: {}, requires: { item: 'kdy_director_override' },
                result: `The visor cools a shade. "Director clearance confirmed." He steps one boot width aside. "Lady Kuat expected someone. She did not say who."`,
                grants: { flags: ['vaelen_cleared', 'vaelen_route_token'] } },
              { text: `Present Baroness Moira Kuat's letter of introduction.`, morality: 0, loyalty: {}, requires: { item: 'kdy_courtesy_letter' },
                result: `He reads the seal without lifting the letter. "House Kuat vouches. That is its prerogative and its risk." He steps aside. "Remember whose name opened this door."`,
                grants: { flags: ['vaelen_cleared', 'vaelen_route_letter'] } },
              { text: `Show the roster stub Vectis printed for you.`, morality: -3, loyalty: { underworld: 5 }, requires: { item: 'ring_roster_stub' },
                result: `He studies it. "Entered forty seconds ago, by a droid that does not enter names." A long pause. "The corridor is not my property. Walk."`,
                grants: { flags: ['vaelen_cleared', 'vaelen_roster_forged'] } },
              { text: `Offer the forged transit chit and walk past him.`, morality: -5, loyalty: { underworld: 5 }, requires: { item: 'forged_transit_chit' },
                result: `The chit hits his reader and the visor flares red. "Counterfeit." The old armor moves faster than it should. "Then we do this the long way."`,
                defer: true, triggersMinigame: 'tactical_combat',
                minigameConfig: { opponentProfile: 'ring_vaelen_sentinel', flavorText: `The corridor is one tile wide and Vaelen fills it. Ivory plate rings against the bulkhead as he draws a long Kuati rail blade and a pistol in the same motion. He has done this before. He has done it for longer than you have lived.` },
                onSuccessFlags: ['vaelen_cleared', 'vaelen_duel_won'] },
              { text: `Tell him Tanner sent you.`, morality: 0, loyalty: {}, defer: true,
                result: `"Tanner holds operational clearance, not residential. If someone inside clears you personally, I will be informed."`,
                grants: { flags: ['vaelen_tanner_refused'] } },
              { text: `Ask about House Kuat's history on the ring.`, morality: 5, loyalty: { republic: 5 }, defer: true,
                result: `For the first time he turns his head, a fraction. "The Empire hung a decal over a dock seal in Year Three. The seal is twenty-two years older." He turns back.`,
                grants: { flags: ['vaelen_history_shared'], codex: ['codex-ring-sentinels'] } },
            ],
          },
          { id: 'captain_vaelen_post', x: 26, y: 10, kind: 'kdy_executive_sentinel', label: 'Captain Vaelen',
            requiresFlag: 'vaelen_cleared',
            repeatPrompt: `"The suites are open to you. Do not make me regret the list."`,
            prompt: `Vaelen holds a new post by the yacht pad, as still as the corridor ever was. "You are logged. I log everything. It is a long habit."`,
            phases: [
              { id: 'p_post', requiresAllFlags: ['vaelen_cleared'],
                prompt: `Vaelen holds a new post by the yacht pad, as still as the corridor ever was. "You are logged. I log everything. It is a long habit."`,
                repeatPrompt: `"The suites are open to you. Do not make me regret the list."`,
                choices: [ { text: `Ask what he really guards.`, morality: 0, loyalty: {}, defer: true,
                  result: `"A promise. The Lady's family keeps the ring, and I keep the Lady's family." That is all he offers.` } ] },
              { id: 'p_roster', requiresAllFlags: ['vaelen_roster_forged'],
                prompt: `Vaelen has not forgiven the roster entry. He has not removed it either. "I audited the list. One name has no origin. It is yours."`,
                repeatPrompt: `"One name has no origin."`,
                choices: [ { text: `Say nothing and nod.`, morality: 0, loyalty: {}, defer: true, result: `"Good. Silence is also on my list." He does not move.` } ] },
              { id: 'p_duel', requiresAllFlags: ['vaelen_duel_won'],
                prompt: `Vaelen sits on a bench by the pad, helmet in his lap, visor cracked. "You fought like someone who read the regulation and ignored it."`,
                repeatPrompt: `"The Lady will hear I let you in. That is the story."`,
                choices: [ { text: `Offer him a hand up.`, morality: 8, loyalty: { republic: 5 },
                  result: `He takes it, once. "The Lady will hear that I let you in. That is the story, and we keep it."`,
                  grants: { flags: ['vaelen_respect'] } } ] },
              { id: 'p_end', requiresAllFlags: ['ghost_keel_complete'],
                prompt: `Vaelen touches two fingers to his helmet, a salute the ring has not seen in decades. "The Lady has told me what was done. I have never needed to know less."`,
                repeatPrompt: `"Walk well, contractor."`,
                choices: [ { text: `Ask what he will do now.`, morality: 0, loyalty: {}, defer: true,
                  result: `"What I always did. Stand where I am told. It is simpler than your work, and I am no longer sure it is worse."` } ] },
            ],
          },
          { id: 'suite_steward_ilan', x: 15, y: 14, wander: 2, kind: 'kdy_logistics_officer', label: 'Suite Steward Ilan',
            phases: [{ id: 'p_main', requiresAllFlags: [] }],
            repeatPrompt: `Ilan straightens a cuff and keeps walking. "The suites are quiet."`,
            prompt: `A Kuati steward in a pressed blue tunic makes his rounds with a datapad. He nods at you the way staff nod at guests nobody warned them about.`,
            choices: [
              { text: `Ask whether the Lady receives visitors.`, morality: 0, loyalty: {}, defer: true, result: `"By appointment, past the Captain. She prefers people who solve problems before they arrive."` },
              { text: `Ask about the yacht on the pad.`, morality: 0, loyalty: {}, defer: true, result: `"The lower lights burn at idle. Someone expects to leave on short notice. I only polish it."` },
            ] },
          { id: 'lady_kuat', x: 22, y: 14, kind: 'kdy_executive_sentinel', label: 'Lady Kuat',
            requiresFlag: 'vaelen_cleared',
            repeatPrompt: `"The window is smaller than I estimated."`,
            prompt: `She does not stand. She studies you over the teacup like a shipwright checking a seam. "Capable, not yet trustworthy. I need a thing done before Krennis's ISB team docks."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `She does not stand. She studies you over the teacup like a shipwright checking a seam. "Capable, not yet trustworthy. I need a thing done before Krennis's ISB team docks."`,
                repeatPrompt: `"The window is smaller than I estimated."`,
                choices: [
                  { text: `Ask what the ISB team is coming for.`, morality: 0, loyalty: {},
                    result: `"Me. Fourteen months of schematics sent to a Senate faction. Krennis has a file, and his team will finish it." She nods at the painting. "The safe behind it holds what ends his case."`,
                    grants: { flags: ['lady_kuat_mission_understood', 'lady_kuat_trust'], codex: ['codex-ring-isb-file'] } },
                  { text: `Ask what she offers in exchange.`, morality: -5, loyalty: { underworld: 8 }, defer: true,
                    result: `"Permanent ring access. Cargo exemption. A cover story good enough for any port inspector." She refills her cup. "Worth far more than what I ask."`,
                    grants: { flags: ['lady_kuat_offered_terms'] } },
                ] },
              { id: 'p_trust', requiresAllFlags: ['lady_kuat_trust'], requiresNoneFlags: ['executive_safe_opened'],
                prompt: `Lady Kuat does not look up from her letters. "The safe is behind the painting. I cannot be seen to open it. Two biometric layers. You were recommended as someone who manages."`,
                repeatPrompt: `"Time is the only thing I cannot buy."`,
                choices: [ { text: `Tell her you will handle the lock.`, morality: 0, loyalty: {}, defer: true, result: `"Quietly." She turns a page, finished with you for now.` } ] },
              { id: 'p_slate', requiresAllFlags: ['executive_safe_opened'], requiresNoneFlags: ['lady_kuat_mission_complete', 'registry_to_committee'],
                prompt: `You set the Moff registry on her desk. She does not touch it. "Fourteen months of my life, in your hands. Decide what it is for."`,
                repeatPrompt: `"The slate is yours to spend. Spend it well."`,
                choices: [
                  { text: `Send it to the Oversight Committee from the executive terminal.`, morality: 12, loyalty: { republic: 15 },
                    result: `"Then the terminal is yours. The routing is pre-loaded." She finally lifts the cup. "Send it before the ISB docks."`,
                    grants: { flags: ['registry_to_committee'] } },
                  { text: `Keep the slate. Every Moff on it is leverage.`, morality: -10, loyalty: { underworld: 15 },
                    result: `"Leverage killed three of my predecessors." A beat. She slides a credit case across anyway. "A discretion fee. Do not call it anything else."`,
                    grants: { credits: 5000, flags: ['registry_kept', 'lady_kuat_mission_complete'] } },
                  { text: `Return it. Your secrets are yours.`, morality: 6, loyalty: { republic: 3 },
                    result: `Something near respect crosses her face. "House Kuat does not forget a return." She presses a gilt writ into your hand.`,
                    grants: { flags: ['registry_returned', 'lady_kuat_mission_complete'], items: ['ring_kuat_transit_rights'] } },
                ] },
              { id: 'p_wait', requiresAllFlags: ['registry_to_committee'], requiresNoneFlags: ['lady_kuat_mission_complete'],
                prompt: `Lady Kuat watches the door as if the ISB could walk through it. "The terminal is waiting. A clean signal, before the team docks."`,
                repeatPrompt: `"The terminal. Now."`,
                choices: [ { text: `Say you are on your way.`, morality: 0, loyalty: {}, defer: true, result: `"Do not be on your way. Be done."` } ] },
              { id: 'p_after', requiresAllFlags: ['lady_kuat_mission_complete'],
                prompt: `Lady Kuat has changed teas, to something lighter. "The ISB team docks within the cycle. They will find a woman with nothing to hide. You made sure of that."`,
                repeatPrompt: `"Go. I will manage my own afternoon."`,
                choices: [ { text: `Ask about the officer named Cherit.`, morality: 0, loyalty: {}, defer: true,
                  result: `"I met her once, at a launch. She asked which hulls could be made to vanish." Lady Kuat studies her tea. "I told her all of them."`,
                  grants: { flags: ['lady_kuat_cherit_told'], codex: ['codex-ring-cherit'] } } ] },
              { id: 'p_end_any', requiresAllFlags: ['ghost_keel_complete'],
                prompt: `Lady Kuat receives you standing, which she has not done before. "It is finished. Whatever it was, it is no longer mine to wonder about."`,
                repeatPrompt: `"The ring remembers what it is told to remember."`,
                choices: [ { text: `Ask how the ring will remember it.`, morality: 0, loyalty: {}, defer: true, result: `"Quietly. That is how it remembers everything important."` } ] },
              { id: 'p_end_kdy', requiresAllFlags: ['ghost_keel_complete', 'hypercore_to_kdy'],
                prompt: `Lady Kuat receives you standing. "The Guild paid, the Board is pleased, and a hull will be built under my family's wheel. The Ten Families are delighted. I find I am not."`,
                repeatPrompt: `"House Kuat remembers its profit. And its price."`,
                choices: [ { text: `Accept the Kuat signet.`, morality: 0, loyalty: { underworld: 3 },
                  result: `She presses a heavy gold ring into your palm. "It opens doors on every Kuat dock. It will also be noticed."`,
                  grants: { flags: ['kuat_signet_given'], items: ['ring_kuat_signet'], codex: ['codex-ring-end-kdy'] } } ] },
              { id: 'p_end_destroyed', requiresAllFlags: ['ghost_keel_complete', 'hypercore_destroyed'],
                prompt: `Lady Kuat stands at the viewport, a cold cup in her hands. "I voted against that demolition protocol thirty years ago. I was wrong. Something should not have existed, and now it does not."`,
                repeatPrompt: `"Quiet is not the same as peace. But it is close."`,
                choices: [ { text: `Accept Cherit's seal.`, morality: 6, loyalty: { republic: 5 },
                  result: `A small bronze seal, worn smooth. "Cherit left it with me. It should sit with whoever buried her ship."`,
                  grants: { flags: ['cherit_seal_given'], items: ['ring_cherit_seal'], codex: ['codex-ring-end-destroyed'] } } ] },
              { id: 'p_end_syndicate', requiresAllFlags: ['ghost_keel_complete', 'hypercore_to_syndicate'],
                prompt: `Lady Kuat laughs once, softly, a sound the room has not heard. "A crime lord with a pocket dreadnought. The Families will call it piracy. I call it the hull's first honest owner."`,
                repeatPrompt: `"Fit it at cost. Do not tell the Guild I said cost."`,
                choices: [ { text: `Accept the Kuat charter.`, morality: -3, loyalty: { underworld: 6 },
                  result: `A gilt charter lists yard rates the Families deny to everyone. "Your flagship will be fitted at cost. I am not worried about the Guild. I am worried about you."`,
                  grants: { flags: ['flagship_charter_given'], items: ['ring_flagship_charter'], codex: ['codex-ring-end-syndicate'] } } ] },
            ],
          },
          { id: 'kdy_overseer_dalin', x: 4, y: 16, wander: 2, kind: 'kdy_guild_overseer', label: 'Guild Overseer Dalin Orveth',
            repeatPrompt: `He turns a stylus in his fingers. "The audit timeline has not changed."`,
            prompt: `He reads a datapad held in both hands, the monocle scrolling faster than he blinks. Navy robes, gold epaulettes. "You were cleared to the executive tier. Unusual. Tell me who authorized it."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `He reads a datapad held in both hands, the monocle scrolling faster than he blinks. Navy robes, gold epaulettes. "You were cleared to the executive tier. Unusual. Tell me who authorized it."`,
                repeatPrompt: `"The audit timeline has not changed."`,
                choices: [
                  { text: `Explain that Captain Vaelen granted access.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Vaelen holds the threshold on the Lady's word." He weighs you through the monocle. "Useful, or a useful problem. I would like to know which."`,
                    grants: { flags: ['dalin_met'] } },
                  { text: `Ask about the construction authorization audit.`, morality: 5, loyalty: { republic: 8 }, defer: true,
                    result: `"Twice yearly, mandatory. This one found fourteen line items in the Bador facility contracts with no Senate schedule behind them." He lowers the pad. "That worries the Board."`,
                    grants: { flags: ['dalin_audit_explained'], codex: ['codex-ring-guild-audit'] } },
                  { text: `Ask if the Guild will cooperate with the Senate Oversight Committee.`, morality: 10, loyalty: { republic: 15 }, defer: true,
                    result: `"Four centuries of cooperation, and now the first clash between the Senate and our best customer." He glances at the mural. "I speak for those who think the answer is obvious."`,
                    grants: { flags: ['dalin_guild_stance_shared'] } },
                ] },
              { id: 'p_proof', requiresAnyFlag: ['dalin_audit_explained', 'dalin_guild_stance_shared'], requiresNoneFlags: ['guild_audit_filed'],
                prompt: `Dalin taps the datapad. "Fourteen unexplained items. I need one document I can put before the Board that cannot be called a forgery. Bring me proof that an item exists."`,
                repeatPrompt: `"The Board meets whether or not I am ready."`,
                choices: [
                  { text: `Hand over Jaxen's original cargo receipt.`, morality: 8, loyalty: { republic: 8 }, requires: { item: 'missing_hyperdrive_manifest' },
                    result: `He cross-checks twice. "Twelve motivators that never existed. That makes item fifteen." He files it. "The Guild thanks you. A finder's fee."`,
                    grants: { credits: 1500, flags: ['guild_audit_filed', 'audit_proof_receipt'] } },
                  { text: `Hand over the Sector 4 assembly archive.`, morality: 5, loyalty: { republic: 5 }, requires: { item: 'ring_assembly_archive' },
                    result: `"Unauthorized shifts on a facility with no charter." His hand is steady; his pen is not. "This reaches the Board tonight. Your fee, and the Guild's discretion."`,
                    grants: { credits: 2500, flags: ['guild_audit_filed', 'audit_proof_archive'] } },
                  { text: `Say you do not have it yet.`, morality: 0, loyalty: {}, defer: true, result: `"Then find it. Fourteen items do not file themselves."` },
                ] },
              { id: 'p_filed', requiresAllFlags: ['guild_audit_filed'],
                prompt: `Dalin looks calmer. "The Board has frozen all Bador contract payments pending the audit. Someone upstairs is not sleeping tonight."`,
                repeatPrompt: `"The audit moves. Slowly, on purpose."`,
                choices: [ { text: `Ask what the freeze means for Bador.`, morality: 0, loyalty: {}, defer: true,
                  result: `"Less supply to the Base, more pressure on Commander Vael. Contractors on Bador are dumping stock for cash. You will feel it on the surface, in the prices."` } ] },
            ],
          },
          { id: 'tanner', x: 18, y: 13, kind: 'broker', label: 'Tanner',
            questNpc: true,
            repeatPrompt: `Tanner keeps his voice even. "The shuttle to Bador is prepped."`,
            prompt: `He does not stand. He studies you like an investment. "KDY has a problem on a restricted moon. Something was built there that appears in no manifest. Tell me what, before a Senate committee asks."`,
            phases: [
              { id: 'p_brief', requiresAllFlags: [],
                prompt: `He does not stand. He studies you like an investment. "KDY has a problem on a restricted moon. Something was built there that appears in no manifest. Tell me what, before a Senate committee asks."`,
                repeatPrompt: `Tanner keeps his voice even. "The shuttle to Bador is prepped."`,
                choices: [
                  { text: `What is on the moon and what does it pay?`, morality: 0, loyalty: { underworld: 5 },
                    result: `"Fifty thousand on completion. KDY transit rights, permanent." He slides across a vial of Kuati brandy. "For the flight. Commander Vael will brief you on the surface."`,
                    grants: { flags: ['tanner_briefed', 'ghost_keel_active'], items: ['kuati_brandy'] } },
                  { text: `How long have you been watching me specifically?`, morality: 5, loyalty: { republic: 5 },
                    result: `"Four months. Since the freight hub job." No apology. "You make decisions other operators do not. The briefing has the rest."`,
                    grants: { flags: ['tanner_briefed', 'ghost_keel_active', 'tanner_asked_auth'], items: ['kuati_brandy'] } },
                  { text: `I want double. You came to me, so you are out of options.`, morality: -5, loyalty: { underworld: 10 },
                    result: `A long look. "Seventy-five. That is the ceiling and we both know it." The datapad case snaps shut. "Shuttle is prepped. Ask for Commander Vael."`,
                    grants: { flags: ['tanner_briefed', 'ghost_keel_active', 'tanner_premium_deal'], items: ['kuati_brandy'] } },
                ] },
              { id: 'p_active', requiresAllFlags: ['tanner_briefed'],
                prompt: `Tanner does not look up from his ledgers. "The shuttle is prepped. If you need a name, ask. I answer what I choose."`,
                repeatPrompt: `"Sector 4 waits. I do not."`,
                choices: [
                  { text: `Ask who designed the hull.`, morality: 0, loyalty: {}, defer: true,
                    result: `"An engineer called the Architect. Declared dead forty three years ago, which is how the Guild keeps a secret."`,
                    grants: { flags: ['tanner_architect_named'], codex: ['codex-ring-architect-dead'] } },
                  { text: `Ask who commissioned it.`, morality: 0, loyalty: {}, defer: true,
                    result: `"An intelligence officer. The name is in a file I do not carry. Ask the people who built the ring."` },
                  { text: `Ask what Tanner gets out of it.`, morality: 0, loyalty: {}, defer: true,
                    result: `"A Committee that lands to find KDY already knows. The Guild pays well for being first." He almost smiles.` },
                ] },
              { id: 'p_end_any', requiresAllFlags: ['ghost_keel_complete'],
                prompt: `Tanner closes the ledger. "It is done. I will not ask how. The Guild will not ask at all."`,
                repeatPrompt: `"Done is a good word. Keep it."`,
                choices: [ { text: `Ask how the Guild took it.`, morality: 0, loyalty: {}, defer: true, result: `"As it takes everything. In writing, late, and with a markup."` } ] },
              { id: 'p_end_kdy', requiresAllFlags: ['ghost_keel_complete', 'hypercore_to_kdy'],
                prompt: `Tanner stands, the first time. "The Board stopped haggling the moment it saw the schematics. You were owed fifty. I told them seventy-five was the market rate."`,
                repeatPrompt: `"The writ is good on any Guild route. Do not lose it."`,
                choices: [ { text: `Take the transit writ.`, morality: 0, loyalty: { underworld: 3 },
                  result: `A matte blue writ, KDY crest, no expiry. "Permanent transit rights, signed. Use them on any Guild route, and thank me by not needing them."`,
                  grants: { flags: ['kdy_transit_rights_active'], items: ['ring_kdy_transit_writ'] } } ] },
              { id: 'p_end_kdy_premium', requiresAllFlags: ['ghost_keel_complete', 'hypercore_to_kdy', 'tanner_premium_deal'],
                prompt: `Tanner stands, the first time. "Seventy-five, as you demanded. I told the Board the contractor was greedy and correct. Both were true."`,
                repeatPrompt: `"The writ is good on any Guild route. Do not lose it."`,
                choices: [ { text: `Take the transit writ.`, morality: 0, loyalty: { underworld: 3 },
                  result: `A matte blue writ, KDY crest, no expiry. "Permanent transit rights, signed. You earned the premium. Do not make me regret saying so."`,
                  grants: { flags: ['kdy_transit_rights_active'], items: ['ring_kdy_transit_writ'] } } ] },
              { id: 'p_end_destroyed', requiresAllFlags: ['ghost_keel_complete', 'hypercore_destroyed'],
                prompt: `Tanner is quiet a long time. "Forty years of Guild money, ash in a pulse. I should be furious. I am an old man who read about Cherit and understood her."`,
                repeatPrompt: `"Pour the brandy. It is the last honest thing on the ring."`,
                choices: [ { text: `Take the marker.`, morality: 5, loyalty: { republic: 5 },
                  result: `A cold black chip. "A debt I will never call. The ISB will not hear your name from my brokers." He nods at the brandy. "Pour a glass."`,
                  grants: { flags: ['tanner_marker_given'], items: ['ring_tanner_cold_marker'] } } ] },
              { id: 'p_end_syndicate', requiresAllFlags: ['ghost_keel_complete', 'hypercore_to_syndicate'],
                prompt: `Tanner smiles like a man outbid by his own client. "A Syndicate dreadnought. The Guild will hate it. The Guild will also fit it, at a markup, in secret."`,
                repeatPrompt: `"Berth Nine logs no questions. Use it."`,
                choices: [ { text: `Take the berth chit.`, morality: -3, loyalty: { underworld: 6 },
                  result: `A brass chit stamped with a berth number. "Berth Nine. No questions logged. It is the cheapest insurance you will ever own."`,
                  grants: { flags: ['syndicate_berth_given'], items: ['ring_syndicate_berth_chit'] } } ] },
            ],
          },
        ],
        collectibles: [{ id: 'kuat_credit_chip', x: 4, y: 18, label: 'KDY Executive Credit Chip', reward: 500 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 5, 26, 20, 'floor');
          carveRect(g, 5, 7, 12, 11, 'wall');
          carveRect(g, 14, 7, 22, 11, 'wall');
          pt(g, 8, 7, 'floor'); pt(g, 9, 7, 'floor'); pt(g, 10, 7, 'floor');
          pt(g, 8, 11, 'floor'); pt(g, 9, 11, 'floor'); pt(g, 12, 9, 'floor');
          pt(g, 16, 9, 'floor'); pt(g, 17, 9, 'floor'); pt(g, 18, 9, 'floor');
          pt(g, 19, 9, 'floor'); pt(g, 20, 9, 'floor'); pt(g, 21, 9, 'floor'); pt(g, 22, 9, 'floor');
          carveRect(g, 5, 13, 13, 20, 'wall');
          pt(g, 8, 13, 'floor'); pt(g, 8, 14, 'floor'); pt(g, 8, 15, 'floor');
          pt(g, 8, 16, 'floor'); pt(g, 8, 17, 'floor'); pt(g, 8, 18, 'floor');
          pt(g, 13, 16, 'floor'); pt(g, 13, 17, 'floor'); pt(g, 13, 18, 'floor');
          pt(g, 0, 12, 'door'); pt(g, 0, 13, 'door');
          carveShips(g, this.ships);
          return g;
        },
      },

      kdy_landing_bay: {
        id: 'kdy_landing_bay', name: 'KDY Orbital Ring — Landing Bay',
        ships: [
          { id: 'ship_free_horizon', kind: 'free_horizon', x: 15, y: 19, label: 'Free Horizon (Impounded)',
            description: `A Corellian free trader with a decade of unscheduled landings pitted into its hull, held in Bay 7 under Ring-Sec clamps. A red seal on the dorsal hatch reads MANIFEST DISCREPANCY, CLASS B. Under registry plate CR-7714 someone scored a fresh mark.`,
            grantsFlag: 'free_horizon_examined' },
          { id: 'ship_republic_shuttle', kind: 'republic_shuttle', x: 11, y: 12, label: 'Republic Navy Liaison Shuttle',
            description: `A Republic Navy liaison shuttle, hull RN-4471, red stripes and crest freshly cleaned for the inspection tour, wings folded for the tight berth. A crew chief checks a datapad at the ramp seam without looking up.`,
            grantsFlag: 'liaison_shuttle_examined' },
        ],
        subtitle: 'Kuat Drive Yards · Ring Berth Seven',
        width: 32, height: 26, spawnPos: { x: 2, y: 14 }, textureId: 'ferrowake',
        accent: '#3A7AB0', accentGlow: 'rgba(58,122,176,0.28)', accentDim: '#123458',
        floorColor: '#141A20', floorAlt: '#1A2028', wallDark: '#080C10', wallLight: '#101418',
        bg: 'radial-gradient(circle at 40% 30%, #101C2C 0%, #060A10 70%)', ambient: 'ringdeck',
        paAnnouncements: [
          'Shuttle K-14 boarding at Bay 1 for Bador. Manifest names only.',
          'Reminder: tether lockers are signed out by worker number. Do not take another crew\'s emergency line.',
          'Docking Bay 7 remains under Ring-Sec hold. Please do not approach the clamps.',
          'Fuel Rig 4-A is cleared for scheduled use only. Unscheduled use is a Class A violation.',
        ],
        decor: ['neon_sign', 'pipe', 'girder'],
        doors: [
          { x: 31, y: 12,  targetZone: 'kuat_private_suites', targetPos: { x: 1, y: 12 }, label: 'Executive Transit' },
          { x: 31, y: 13,  targetZone: 'kuat_private_suites', targetPos: { x: 1, y: 13 }, label: 'Executive Transit' },
          { x: 31, y: 16, targetZone: 'kdy_briefing_area', targetPos: { x: 1, y: 12 }, label: 'Briefing Ring' },
          { x: 31, y: 17, targetZone: 'kdy_briefing_area', targetPos: { x: 1, y: 13 }, label: 'Briefing Ring' },
          { x: 14, y: 25, targetZone: 'kdy_lounge_market', targetPos: { x: 1, y: 16 }, label: 'Market Ring' },
          { x: 15, y: 25, targetZone: 'kdy_lounge_market', targetPos: { x: 1, y: 17 }, label: 'Market Ring' },
        ],
        worldObjects: [
          { id: 'ring_vista_bay', propArt: 'bvista_32x5', propVariant: 'ring_a', x: 16, y: 5, once: false, iconKind: 'vista', label: 'Bay Observation Glass',
            description: `The bay's long glass wall opens on Kuat's night side: the planet's limb ringed in amber city light, a Valor frame hanging in its cradle, and the orbital array stretching out, hull after hull, to a bright shuttle.`, grantsCodex: 'codex-ring-architect-dead' },
          { id: 'fuel_rig_4a', iconKind: 'fuel_rig', propArt: 'fuel_rig', x: 5, y: 7, label: 'Fuel Rig 4-A',
            description: `A coaxium refueling conduit the width of a speeder. The safety clamps are corroded and the flow regulators bypassed by hand. Tamper with the pressure valve and this whole bay becomes a distraction.`,
            triggersMinigame: 'valve_override',
            once: true, grantsFlag: 'fuel_rig_tampered' },
          { id: 'manifest_holo_terminal', iconKind: 'manifest_terminal', propArt: 'manifest_terminal', x: 26, y: 8, label: 'Manifest Holo-Terminal',
            description: `A public node cycling incoming cargo manifests. The surface entries are routine. Below the routing headers sit eleven entries marked CONSIGNMENT RESTRICTED, KDY DIRECTOR CLEARANCE, all stamped within six hours.`,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 4 },
            once: true, grantsFlag: 'manifest_accessed', grantsItem: 'missing_hyperdrive_manifest' },
          { id: 'vectis_terminal', iconKind: 'terminal', propArt: 'vectis', x: 26, y: 21, label: 'Ring Transit Terminal RX-9',
            description: `Vectis descends on its ceiling track, multi-lensed optics converging with a targeting system's calm. "Ring Sector Security clearance is required for Executive and Briefing access. Unauthorized transit attempts are prosecuted under Ordinance 44-G."`,
            requiresNoneFlags: ['vectis_standard_cleared'] },
          { id: 'vectis_terminal_active', iconKind: 'terminal', propArt: 'vectis', x: 26, y: 21, label: 'Ring Transit RX-9 (Cleared)',
            description: `Vectis rotates its optical head. "Standard ring access confirmed. Executive berths require Director override." Its roster handshake port glows a patient amber, as if waiting to be asked a question it should refuse.`,
            requiresFlag: 'vectis_standard_cleared', requiresNoneFlags: ['sentinel_roster_edited'],
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 5 },
            once: true, grantsFlag: 'sentinel_roster_edited', grantsItem: 'ring_roster_stub' },
          { id: 'vectis_terminal_done_active', iconKind: 'terminal', propArt: 'vectis', x: 26, y: 21, label: 'Ring Transit RX-9 (Roster Entry)',
            description: `Vectis rotates its head and says nothing about the stub it printed. Its optics hold a steady green, and its log reads CLERICAL ENTRY, NO ORIGIN.`,
            requiresFlag: 'sentinel_roster_edited' },
          { id: 'bay7_impound_notice', iconKind: 'impound_notice', propArt: 'impound_door', x: 14, y: 7, label: 'Docking Bay 7 Impound Notice',
            description: `A red notice is locked to Bay 7's blast door under the Ring-Sec seal. Ship: FREE HORIZON, Corellia. Reason: manifest discrepancy, Class B. The notice is three days old.` },
          { id: 'orbital_viewport_bay1', iconKind: 'orbital_viewport', propArt: 'orbital_viewport', x: 10, y: 7, label: 'Orbital Viewport, Bay Section 7',
            description: `Three meters of transparisteel in the ring hull. Kuat's upper atmosphere catches amber light, and a Valor cruiser frame hangs in its cradle with twelve crews like sparks across it. Plate: HULL CLASS VL-9, BERTH 7.` },
          { id: 'tether_locker_wall', iconKind: 'tether_locker', propArt: 'tether_lockers', x: 4, y: 8, label: 'Zero-G Worker Tether Lockers',
            description: `A row of mag-locked lockers stamped with worker numbers, several open, harnesses hanging. A note on locker 12 reads: ANYONE WHO TAKES MY EMERGENCY LINE AGAIN ANSWERS TO ME PERSONALLY.` },
          { id: 'transit_pod_terminal', iconKind: 'transit_pod', propArt: 'transit_pod', x: 14, y: 18, label: 'Ring Pneumatic Transit, Pod Station 3',
            description: `A pod capsule sits in its pressurized cradle, foam still holding the last passenger's shape. The board lists Briefing Ring, Executive Ring (Director code) and Maintenance Underbelly. A teal-grey Mark IV loader droid idles beside it.` },
          { id: 'plasma_welding_arm', iconKind: 'welding_rig', propArt: 'welding_rig', x: 24, y: 14, label: 'Plasma Arc Welding Rig, Station 14',
            description: `An articulated welding arm the size of a speeder in its storage cradle, torch tip still glowing faint orange. The checklist reads LAST INSPECTION 3 CYCLES PRIOR, SIGNED OSSEK. Corrections in four different hands.` },
          { id: 'aurebesh_wayfinding', iconKind: 'wayfinding_plaque', propArt: 'wayfinding_plaque', x: 8, y: 14, label: 'KDY Ring Wayfinding Plaque',
            description: `Cast bronze signage in Kuati stepped-arch style: TRANSIT SECTION 3, PRIVATE EXECUTIVE RING, TEAM BRIEFING RING, LOUNGE AND MARKET, BAY ACCESS BELOW. Scratched under it: IF LOST, ASK VECTIS. IT WILL TELL YOU TWICE.` },
          { id: 'hydraulic_blast_panel', iconKind: 'blast_door_panel', propArt: 'blast_panel', x: 1, y: 16, label: 'Emergency Bulkhead Override',
            description: `A manual hydraulic override wheel behind a break-glass panel, amber and black stripes. FOR HULL BREACH OR FIRE, THREE FULL ROTATIONS COUNTERCLOCKWISE. Below: YES THIS MEANS YOU, BAY CREW. SIGNED, RING SAFETY OFFICER PRENN.` },
          { id: 'bay_container_stack', iconKind: 'crate', propArt: 'container_stack', x: 27, y: 10, once: false, label: 'Stacked Cargo Containers',
            description: `Four teal-grey KDY containers stacked two high, stenciled with hold numbers and a blue transit tag. One tag reads EXEC RING, HANDLE WITH CARE.` },
          { id: 'bay_drum_array', iconKind: 'crate', propArt: 'drum_array', x: 25, y: 23, once: false, label: 'Coolant Drum Array',
            description: `Eight coolant drums in a clamp rack, amber hazard bands, one marked with a fresh hand-painted X.` },
          { id: 'bay_scrap_bins', iconKind: 'crate', propArt: 'scrap_bin', x: 6, y: 22, once: false, label: 'Scrap Bins',
            description: `Two overflowing scrap bins by the corner wall. A blanket is folded on one lid, and a small cup of cold caf rests beside it.` },
          { id: 'bay_landing_beacon', iconKind: 'crate', propArt: 'landing_beacon', x: 29, y: 13, once: false, label: 'Shuttle Landing Beacon',
            description: `A blue landing beacon pulses beside the transit lane, marking Bay 1 for approaching shuttles.` },
        ],
        npcs: [
          { id: 'pilot_landing_orn', x: 27, y: 15, kind: 'kdy_pilot', label: 'Pilot Tessa Orn (KDY Shuttle)', triggersOverlay: 'shuttle',
            greeting: `"Shuttle K-14, Bay 1. Guild passage is free if your name is on the manifest."`,
            repeatPrompt: `The pilot taps the shuttle manifest and waits for a destination.`,
            prompt: `A KDY shuttle pilot in the blue Kuati flight uniform.`, choices: [] },
          { id: 'ring_patrol_halv', x: 10, y: 16, wander: 5, kind: 'kdy_ring_sec', label: 'Ring Sec Patrol Halv',
            repeatPrompt: `Halv nods without breaking stride. "Keep your badge visible."`,
            prompt: `A Ring Sec patrolman walks the bay perimeter, rifle slung, scanning crates and faces alike. He decides you are not his problem.`,
            choices: [ { text: `Nod and let the patrol pass.`, morality: 0, loyalty: {}, result: `"Move along, citizen. KDY thanks you for your patience."` } ] },
          { id: 'deckhand_pell', x: 22, y: 17, wander: 4, kind: 'mechanic', label: 'Deck Hand Pell',
            repeatPrompt: `Pell hauls a coolant canister past you. "Mind the cables."`,
            prompt: `A deck hand in grease-stained blues drags a coolant canister across the floor, humming something tuneless.`,
            choices: [ { text: `Ask which berth the shuttles use.`, morality: 0, loyalty: {}, result: `"Bay 1, east end. Pilots wear the blue Kuati uniform. Cannot miss them."` } ] },
          { id: 'corporal_dren', x: 18, y: 14, wander: 3, kind: 'kdy_ring_sec', label: 'Guard Corporal Dren',
            repeatPrompt: `Dren chews his stim strip. "Shift ends in four hours. No paperwork."`,
            prompt: `A scorch mark no polish touches scars his chest plate. He chews a stim strip. "New arrival. Someone sent you, you do not know why, and you want to know if I am a problem."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `A scorch mark no polish touches scars his chest plate. He chews a stim strip. "New arrival. Someone sent you, you do not know why, and you want to know if I am a problem."`,
                repeatPrompt: `Dren chews his stim strip. "Shift ends in four hours. No paperwork."`,
                choices: [
                  { text: `How much to look at that impound manifest?`, morality: -5, loyalty: { underworld: 8 }, requires: { credits: 250 },
                    result: `He stops chewing for one second. "I cannot see what happens if you use the access panel while I walk north. Four minutes." He pockets the credits and walks.`,
                    grants: { flags: ['dren_bribed_item', 'vectis_standard_cleared'] } },
                  { text: `Say you are here on KDY business.`, morality: 0, loyalty: {},
                    result: `"Ring clearance gets you standard transit. Executive berths need a Director code or a Sentinel escort." He nods at the Vectis terminal. "The droid handles the paperwork."`,
                    grants: { flags: ['dren_standard_briefed', 'vectis_standard_cleared'] } },
                  { text: `Ask about the man impounded in Bay 7.`, morality: 5, loyalty: { republic: 5 }, defer: true,
                    result: `"Jaxen. Free Horizon, Corellia registry. Says the manifest was falsified before he docked." A pause. "The discrepancy is oddly clean. Working smugglers make messy mistakes."`,
                    grants: { flags: ['dren_jaxen_discussed'] } },
                ] },
              { id: 'p_thanks', requiresAllFlags: ['jaxen_cleared'],
                prompt: `Dren is chewing a fresh strip, almost cheerfully. "Bay 7 is off my report. A clean-up order came from above. I did not write it. I did not argue."`,
                repeatPrompt: `"Quiet shift. I distrust it."`,
                choices: [ { text: `Ask what changed.`, morality: 0, loyalty: {}, defer: true,
                  result: `"Sector House pulled its routes. Ring-Sec found a stall with ledgers." He shrugs. "I like a shift where the right person gets arrested."`,
                  grants: { flags: ['dren_thanks'] } } ] },
            ] },
          { id: 'jaxen_smuggler', x: 5, y: 21, wander: 2, kind: 'smuggler', label: 'Jaxen',
            hideAfterFlags: ['garrok_bought'],
            repeatPrompt: `Jaxen sits against the scrap bins. "You find anything?"`,
            prompt: `He stands too fast; three days in a bay make every footstep sound like rescue or arrest. "You are not Ring-Sec. My manifest listed twelve hyperdrive motivators that never existed. Someone set me up."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `He stands too fast; three days in a bay make every footstep sound like rescue or arrest. "You are not Ring-Sec. My manifest listed twelve hyperdrive motivators that never existed. Someone set me up."`,
                repeatPrompt: `Jaxen sits against the scrap bins. "You find anything?"`,
                choices: [
                  { text: `Offer to slice the manifest records and clear his ship.`, morality: -5, loyalty: { underworld: 10 },
                    result: `"That gets me out of here." He presses a chip into your hand, his transponder backup. "Overwrite the discrepancy at the manifest terminal. I owe you a berth."`,
                    grants: { flags: ['jaxen_helped', 'jaxen_owes_favor'], items: ['ring_jaxen_transponder'] } },
                  { text: `Tell him to contact a Republic legal advocate.`, morality: 10, loyalty: { republic: 8 }, defer: true,
                    result: `"Six weeks, and more credits than the ship is worth. I am not asking you to do wrong. I am asking you to prove someone else did."`,
                    grants: { flags: ['jaxen_republic_advised'] } },
                  { text: `Ask what the Sector House operation is.`, morality: 0, loyalty: {}, defer: true,
                    result: `"A shell company run from a Market ring stall, I think Garrok's. They use free traders as cutouts and let the trader eat the citation."`,
                    grants: { flags: ['sector_house_identified'] } },
                ] },
              { id: 'p_free', requiresAllFlags: ['manifest_accessed'], requiresAnyFlag: ['jaxen_helped', 'jaxen_cleared'], requiresNoneFlags: ['jaxen_freed'],
                prompt: `Jaxen leans on his ramp, free for the first time in a week. "The hold lifted at dawn. Free Horizon runs Corellia, Nar Shaddaa, Kuat. You have a berth on all three."`,
                repeatPrompt: `"Fair winds. Mind the clamps."`,
                choices: [ { text: `Take the Free Horizon route token.`, morality: 4, loyalty: { underworld: 3 },
                  result: `He presses a stamped brass token into your hand, and a purse he cannot afford. "Show it at any berth on those routes. They will remember Free Horizon."`,
                  grants: { credits: 800, flags: ['jaxen_freed'], items: ['ring_jaxen_token'] } } ] },
              { id: 'p_gone_free', requiresAllFlags: ['jaxen_freed'],
                prompt: `Jaxen is polishing the ramp with the sleeve of his jacket, whistling. "I never thought a hold could lift. Thank you again, and thank your friends."`,
                repeatPrompt: `"Free Horizon sails on the tide."`,
                choices: [ { text: `Wish him luck.`, morality: 0, loyalty: {}, defer: true, result: `"Luck I have. Now I have a route."` } ] },
            ] },
          { id: 'kdy_marine_rix', x: 20, y: 10, wander: 2, kind: 'kdy_security_marine', label: 'Security Marine Rix',
            phases: [{ id: 'p_main', requiresAllFlags: [] }],
            repeatPrompt: `Rix watches the bay entrance. "No incidents. Keep it that way."`,
            prompt: `He stands at the transit junction with the relaxed vigilance of a man whose alertness is biological. The pauldron crest is white and blue Ring Security. "Transit cleared. Corporal Dren's stamp is on the board."`,
            choices: [
              { text: `Ask about the ship impounded in Bay 7.`, morality: 5, loyalty: { republic: 5 }, defer: true,
                result: `"Free Horizon. Flagged for engineering grade materials listed as surplus. But the serial ranges match no commercial batch in three years." He shrugs. "I filed my report. Whether Ring Command reads it is not my problem."`,
                grants: { flags: ['rix_bay7_explained'] } },
              { text: `Ask how to obtain Director override credentials.`, morality: 0, loyalty: {}, defer: true,
                result: `"You do not ask a Ring Marine that." Flat, not hostile. "It comes through Guild channels or from the Lady herself. If you are asking me, you are not close enough to either."` },
              { text: `Offer him a bribe.`, morality: -10, loyalty: { underworld: 8 }, defer: true,
                result: `He looks at the credit chip. Looks at the monitoring node you both can see. "I am going to forget you said that."` },
            ] },
          { id: 'republic_pilot_davan', x: 12, y: 18, wander: 2, kind: 'republic_pilot', label: 'Pilot Davan Sael (Republic)',
            repeatPrompt: `Davan watches the frames. "Fourteen months and that cruiser still is not done."`,
            prompt: `Off duty the Republic way: flight suit on, helmet under one arm, eyes on the construction frames. "Not engineering staff. Either you came on authorization, or you are very good at looking like you belong."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `Off duty the Republic way: flight suit on, helmet under one arm, eyes on the construction frames. "Not engineering staff. Either you came on authorization, or you are very good at looking like you belong."`,
                repeatPrompt: `Davan watches the frames. "Fourteen months and that cruiser still is not done."`,
                choices: [
                  { text: `Ask what he is doing at KDY.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Liaison tour, checking standards on ships we will fly. The beam mounts on that Valor are six degrees off axis. I filed it. The foreman called it within tolerance. It is not."`,
                    grants: { flags: ['davan_met'] } },
                  { text: `Ask about the political climate on the ring.`, morality: 5, loyalty: { republic: 8 }, defer: true,
                    result: `"More Imperial audit teams than Senate inspectors, first time in this yard's history. I am documenting everything while I still have access."`,
                    grants: { flags: ['davan_intel_shared'] } },
                  { text: `Ask if the Republic knows about the unreported hull class.`, morality: 5, loyalty: { republic: 10 }, requiresFlag: 'davan_intel_shared',
                    result: `"Which one?" Then: "I should not have said that. The Committee's questions suggest they already know. Find Rael in the briefing ring. He will say more than I should."`,
                    grants: { flags: ['davan_directed_to_rael'] } },
                ] },
              { id: 'p_cert', requiresAllFlags: ['ring_safety_certified'], requiresNoneFlags: ['davan_pin_given'],
                prompt: `Davan taps a fresh datapad against his helmet. "The beam mount was resurveyed this morning. Six degrees became zero. Someone with a Guild stamp finally read my report."`,
                repeatPrompt: `"Fair winds, contractor."`,
                choices: [ { text: `Accept the liaison pin.`, morality: 3, loyalty: { republic: 5 },
                  result: `He unpins a small enamel wing from his collar. "Republic Navy liaison. Show it to any inspector and the paperwork gets shorter."`,
                  grants: { flags: ['davan_pin_given'], items: ['ring_navy_liaison_pin'] } } ] },
              { id: 'p_cert_done', requiresAllFlags: ['davan_pin_given'],
                prompt: `Davan is polishing his helmet visor, the first time you have seen him idle. "Zero degrees. I will sleep tonight."`,
                repeatPrompt: `"Zero degrees. Good work."`,
                choices: [ { text: `Ask what the Navy does with the report.`, morality: 0, loyalty: {}, defer: true, result: `"Files it, thanks me, and builds the next cruiser the same way. Still, a mount is a mount."` } ] },
            ] },
        ],
        collectibles: [{ id: 'scrap_surplus_crate', x: 4, y: 21, label: 'Scrap Surplus: Discarded Components', reward: 120 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 5, 30, 24, 'floor');
          carveRect(g, 1, 5, 8, 11, 'wall');  carveRect(g, 2, 6, 7, 10, 'floor');  pt(g, 8, 7, 'floor'); pt(g, 8, 8, 'floor');
          carveRect(g, 22, 5, 30, 11, 'wall'); carveRect(g, 23, 6, 29, 10, 'floor'); pt(g, 22, 7, 'floor'); pt(g, 22, 8, 'floor');
          carveRect(g, 22, 18, 30, 24, 'wall'); carveRect(g, 23, 19, 29, 23, 'floor'); pt(g, 22, 19, 'floor'); pt(g, 22, 20, 'floor');
          carveRect(g, 1, 18, 9, 24, 'wall'); carveRect(g, 2, 19, 8, 23, 'floor'); pt(g, 9, 19, 'floor'); pt(g, 9, 20, 'floor');
          pt(g, 31, 12, 'door'); pt(g, 31, 13, 'door');
          pt(g, 31, 16, 'door'); pt(g, 31, 17, 'door');
          pt(g, 14, 25, 'door'); pt(g, 15, 25, 'door');
          carveShips(g, this.ships);
          return g;
        },
      },

      kdy_briefing_area: {
        id: 'kdy_briefing_area', name: 'KDY Team Briefing Area',
        ships: [
          { id: 'ship_valor_frame', kind: 'valor_frame', x: 11, y: 21, label: 'Valor Class Cruiser (Hull 0417)',
            description: `A Valor class cruiser in a KDY drydock gantry. The aft third is plated and gleaming. Forward of the bridge tower the hull is ribs and stringers, crane crews lowering armor plate. One dorsal turret mount is tagged 6 DEGREES OFF AXIS. Nobody fixed it.`,
            grantsFlag: 'valor_frame_examined' },
        ],
        subtitle: 'Kuat Drive Yards · Restricted Operations Ring',
        width: 36, height: 26, spawnPos: { x: 2, y: 12 }, textureId: 'ferrowake',
        accent: '#5A7A9A', accentGlow: 'rgba(90,122,154,0.25)', accentDim: '#1A3A5A',
        floorColor: '#111820', floorAlt: '#181E28', wallDark: '#08080E', wallLight: '#101218',
        bg: 'radial-gradient(circle at 50% 40%, #101A2E 0%, #060810 70%)', ambient: 'datastream',
        paAnnouncements: [
          'Conference Room A is occupied. Please do not lean on the partition.',
          'Section B-12 conduit work continues. Expect brief power flicker.',
          'All personal data ports will be sealed during the ISB sweep. Please back up your drives.',
          'Coolant conduit Warning Valve remains above nominal. A work order has been filed.',
        ],
        decor: ['neon_sign', 'girder'],
        doors: [
          { x: 0, y: 12, targetZone: 'kdy_landing_bay', targetPos: { x: 30, y: 16 }, label: 'Ring Transit' },
          { x: 0, y: 13, targetZone: 'kdy_landing_bay', targetPos: { x: 30, y: 17 }, label: 'Ring Transit' },
        ],
        worldObjects: [
          { id: 'ring_vista_briefing', propArt: 'bvista_36x5', propVariant: 'ring_b', x: 18, y: 5, once: false, iconKind: 'vista', label: 'Briefing Ring Observation Glass',
            description: `Past the briefing floor's glass the array hangs in the dark: Hammerhead frigates in their cradles, work lamps along every rib, and Kuat's night side dusted with amber city light.` },
          { id: 'primary_holo_projector', iconKind: 'holo_projector', propArt: 'holo_projector', x: 18, y: 13, label: 'Primary Holo-Projector',
            description: `A rotating wireframe of a warship class in no register. The scale bar reads 1800 meters, Star Dreadnought territory. Two sub-menus are locked at Level 6. A third, STRUCTURAL TOLERANCE MARGINS, is open. Its numbers are wrong.`,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 3 },
            once: true, grantsFlag: 'holotable_accessed', grantsCodex: 'codex-kdy-dreadnought' },
          { id: 'secure_datapad_locker', iconKind: 'secure_locker', propArt: 'secure_locker', x: 28, y: 19, label: 'Secure Datapad Charging Locker',
            description: `Twelve encrypted storage units, most sealed with ISB biometric locks. Third from the right, second row, the seal is cracked. Inside: a half-erased datapad of executive memos about a DESIGN CONSULTATION FEE paid to an unnamed Moff.`,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 4 },
            once: true, grantsFlag: 'datapad_cracked', grantsItem: 'executive_bribe_record' },
          { id: 'acoustic_intercept_wall', iconKind: 'intercept_wall', propArt: 'intercept_wall', x: 10, y: 8, label: 'Transparisteel Partition, Conference Room A',
            description: `The partition between the briefing floor and Conference Room A hums with the talk inside. Two Imperial officers point at a holo of the dreadnought. One reads clearly on the lips: "not in the budget." Myra's splice is live in the frame.`,
            requiresFlag: 'myra_intercept_discussed', lockedMessage: `A single pane between the briefing floor and Conference Room A. Two Imperial officers argue over a holo of the dreadnought. You cannot hear the words without help.`,
            triggersMinigame: 'signal_siphon',
            once: true, grantsFlag: 'intercept_tapped', grantsItem: 'ring_intercept_recording', grantsCodex: 'codex-ring-intercept' },
          { id: 'engineering_terminal_vorn', iconKind: 'terminal', propArt: 'terminal', propVariant: 'engineering', x: 8, y: 19, label: 'Engineering Access Terminal',
            description: `A standard KDY engineering terminal, last used forty minutes ago. The files pulled were assembly sequence archives from Sector 4, Bador. Whoever pulled them was not on a scheduled audit.`,
            requiresFlag: 'designer_contacted', lockedMessage: `The terminal wants an engineering credential. Rael Thoss might have one to spare.`,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 3 },
            once: true, grantsFlag: 'assembly_sequence_reviewed', grantsItem: 'ring_assembly_archive' },
          { id: 'fleet_holo_command_table', iconKind: 'holo_table', propArt: 'holo_table', propVariant: 'fleet', x: 14, y: 12, label: 'Fleet Holo-Command Table',
            description: `An octagonal command table for twelve officers, projecting the cradles: three Hammerhead frigates, the nearly complete Valor cruiser VL-9, two Thranta corvettes. Blue progress overlays. VL-9 is highlighted amber for schedule deviation.` },
          { id: 'ordnance_transport_crate', iconKind: 'ordnance_crate', propArt: 'ordnance_crates', x: 30, y: 10, label: 'Ordnance Transport Crate, Classified',
            description: `A blast-shielded case sealed with a KDY ordnance lock: COMPONENT CLASS RESTRICTED, DO NOT OPEN IN ATMOSPHERE. Three more are stacked beside it. The torn manifest names the origin: KUAT SURFACE, FACILITY BADOR-7, four cycles prior.` },
          { id: 'secure_armory_locker', iconKind: 'armory_locker', propArt: 'armory_locker', x: 28, y: 8, label: 'Briefing Ring Secure Armory',
            description: `A floor-bolted security locker with a biometric palm reader past its best years. Fresh scoring from a splicer tool circles the housing. The last bypass failed. Someone wanted in badly and lacked the handprint.`,
            requiresAllFlags: ['fuel_rig_tampered', 'myra_armory_approached'], lockedMessage: `A floor-bolted armory locker with scoring around the housing. A biometric reader, a mechanical override and an alarm relay. Whoever opens it needs the guards looking elsewhere.`,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 5 },
            once: true, grantsFlag: 'armory_looted', grantsItem: 'ring_kdy_rail_carbine' },
          { id: 'conduit_cable_truss', iconKind: 'cable_truss', propArt: 'cable_truss', x: 18, y: 8, label: 'Overhead Wiring Truss, Section B-12',
            description: `Armored conduit runs overhead in a pressed-metal tray. One bundle has been rerouted with newer cabling, the old line darker and older. The new line went in within the year.`,
            requiresFlag: 'myra_conduit_briefed', lockedMessage: `Armored conduit runs overhead in a pressed tray, one bundle rerouted with newer cabling. Someone who knows signals could tell you what it feeds.`,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 3 },
            once: true, grantsFlag: 'trunk_rerouted' },
          { id: 'coolant_pipe_hazard', iconKind: 'coolant_pipe', propArt: 'coolant_pipe', x: 14, y: 19, label: 'Pressurized Coolant Conduit, Warning Valve',
            description: `A high-pressure coolant pipe behind a safety cage, amber indicators cycling, gauge above nominal. Stencil: DO NOT OPERATE WITHOUT ENGINEERING CLEARANCE, VALVE SEQUENCE REQUIRED. The gauge has climbed for two cycles. A work order was filed.`,
            triggersMinigame: 'valve_override',
            once: true, grantsFlag: 'coolant_pressure_vented' },
          { id: 'briefing_monitor_wall', iconKind: 'crate', propArt: 'monitor_wall', x: 24, y: 7, once: false, label: 'Briefing Monitor Wall',
            description: `Four wall monitors cycle structural progress for the cradles, schedule bars in green, amber and a single stubborn red.` },
          { id: 'briefing_server_stack', iconKind: 'crate', propArt: 'server_stack', propVariant: 'archive', x: 3, y: 9, once: false, label: 'Briefing Archive Racks',
            description: `Three racks of Guild archive drives, amber status lights, a handwritten tag on the top rack reading DO NOT POWER CYCLE.` },
          { id: 'briefing_comm_relay', iconKind: 'crate', propArt: 'comm_relay', propVariant: 'hub', x: 5, y: 23, once: false, label: 'Ring Comm Relay',
            description: `A ring comm relay with a blue status halo, patched in two colors of cable by two different crews.` },
          { id: 'briefing_terminal_bank', iconKind: 'crate', propArt: 'terminal_bank', x: 22, y: 15, once: false, label: 'Briefing Terminal Bank',
            description: `A row of five briefing terminals, four dark and one glowing a pale blue, its chair pushed back as if someone left in a hurry.` },
        ],
        npcs: [
          { id: 'briefing_aide_tev', x: 16, y: 14, wander: 4, kind: 'kdy_logistics_officer', label: 'Briefing Aide Tev',
            repeatPrompt: `Tev hurries past with a stack of datachips.`,
            prompt: `A junior Guild aide carries a stack of datachips between the holo tables, reciting a delivery schedule under his breath.`,
            choices: [ { text: `Ask where the Bador shuttles dock.`, morality: 0, loyalty: {}, result: `"Landing Bay 1, through the Executive Transit. Look for the pilot in the Kuati blues."` } ] },
          { id: 'commander_krennis', x: 22, y: 9, kind: 'imperial_naval_liaison', label: 'Commander Krennis',
            repeatPrompt: `Krennis checks the room again. "I told you what I am cleared to share."`,
            prompt: `He turns the moment you enter, already watching the door. Olive-grey pressed to geometric precision, leather gloves at this hour. He weighs you like an unexpected line item. "State your standing."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `He turns the moment you enter, already watching the door. Olive-grey pressed to geometric precision, leather gloves at this hour. He weighs you like an unexpected line item. "State your standing."`,
                repeatPrompt: `Krennis checks the room again. "I told you what I am cleared to share."`,
                choices: [
                  { text: `Say you are a KDY liaison consultant.`, morality: -5, loyalty: { underworld: 5 }, defer: true,
                    result: `"KDY employs consultants with ring access. That buys the briefing floor, not schematics, not personnel files, nothing ISB-flagged. Consult within your clearance."`,
                    grants: { flags: ['krennis_neutral_established'] } },
                  { text: `Ask what the ISB is looking for.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Not a question I answer for consultants. KDY security is cooperative to a point, and beyond that point cooperation becomes obstruction." He says it pleasantly.`,
                    grants: { flags: ['krennis_iss_hinted'] } },
                  { text: `Tell him you found tolerance errors on the main projector.`, morality: 10, loyalty: { republic: 15 },
                    result: `He stops. "Show me." At the projector his gloves tighten. "This is the commissioned hull, left on an open terminal." He starts recording. "Your name and access trail. This is an ISB matter."`,
                    grants: { flags: ['krennis_structural_shown', 'krennis_iob_alerted'], codex: ['codex-ring-krennis-sweep'] } },
                ] },
              { id: 'p_sweep', requiresAnyFlag: ['blueprints_transmitted', 'registry_transmitted'], requiresNoneFlags: ['krennis_resolved'],
                prompt: `Krennis waits beside the door. "An unmonitored relay used a KDY backbone twenty minutes ago. Only three people on this ring know that code. One is standing in front of me."`,
                repeatPrompt: `"I can wait. The ISB team cannot."`,
                choices: [
                  { text: `Draw on him. The ISB team has not docked yet.`, morality: -8, loyalty: { underworld: 8 }, defer: true,
                    result: `His gloves come off. "Then we skip the paperwork." Two ISB aides step out of the alcove with sidearms already raised.`,
                    triggersMinigame: 'tactical_combat',
                    minigameConfig: { opponentProfile: 'ring_isb_krennis', flavorText: `The briefing floor clears in seconds. Krennis takes cover behind the Fleet Holo-Command Table, his aides spreading to either flank. The projector wireframe of the dreadnought turns slowly overhead, lighting all three of them in blue.` },
                    onSuccessFlags: ['krennis_defeated', 'krennis_resolved'] },
                  { text: `Show him the Moff registry. Names above his own.`, morality: -6, loyalty: { underworld: 10 }, requires: { item: 'executive_blackmail_slate' },
                    result: `He reads three lines and his color changes. "Where did you get this?" A long breath. "The sweep ends tonight. I was never on this deck." He walks out.`,
                    grants: { flags: ['krennis_stood_down', 'krennis_resolved'], codex: ['codex-ring-krennis-sweep'] } },
                  { text: `Pay him to lose the trace. Five thousand credits.`, morality: -10, loyalty: { underworld: 12 }, requires: { credits: 5000 },
                    result: `He counts nothing, which is how you know it is accepted. "A relay fault. Regrettable. Logged." The ISB team will find a very dull ring.`,
                    grants: { flags: ['krennis_bought', 'krennis_resolved'] } },
                  { text: `Say Inspector Thane holds the Committee's seal on this.`, morality: 8, loyalty: { republic: 10 }, requires: { flag: 'thane_unofficial_contact' },
                    result: `"Thane." He says it like a fault. "A Republic inspector with a sealed order is an incident I cannot afford this month." He steps aside, and your name never reaches a log.`,
                    grants: { flags: ['krennis_stood_down', 'krennis_resolved'] } },
                  { text: `Not now.`, morality: 0, loyalty: {}, defer: true, result: `"Not now," he agrees. "Soon."` },
                ] },
              { id: 'p_favor', requiresAllFlags: ['krennis_favor'], requiresNoneFlags: ['krennis_resolved'],
                prompt: `Krennis keeps accounts, and yours has a line in it. "Thoss was a useful arrest. You were useful. I do not forget useful people." He taps the slate. "What would you like me to forget?"`,
                repeatPrompt: `"Name it, or lose the offer."`,
                choices: [
                  { text: `The relay. Call it a fault.`, morality: -5, loyalty: { underworld: 6 },
                    result: `"A fault. Done." He makes a note and, for the first time, almost smiles. "The sweep will find a very quiet ring."`,
                    grants: { flags: ['krennis_stood_down', 'krennis_resolved'] } },
                  { text: `Nothing yet.`, morality: 0, loyalty: {}, defer: true, result: `"Then I keep the line open."` },
                ] },
              { id: 'p_after_defeated', requiresAllFlags: ['krennis_defeated'],
                prompt: `Krennis sits with an ice pack under one glove, an aide at each shoulder. "You win the corridor. You did not win the file. I will log the relay as a fault."`,
                repeatPrompt: `"Fault. Logged."`,
                choices: [ { text: `Offer him a hand up.`, morality: 4, loyalty: {}, defer: true, result: `He looks at it for a long second, then takes his own gloves back. "No."` } ] },
              { id: 'p_after_stooddown', requiresAllFlags: ['krennis_stood_down'],
                prompt: `Krennis is very precise with a stylus he is not using. "The sweep is rescheduled. A scheduling error, I think. I would prefer not to meet you again."`,
                repeatPrompt: `"A scheduling error."`,
                choices: [ { text: `Say nothing.`, morality: 0, loyalty: {}, defer: true, result: `"Good."` } ] },
              { id: 'p_after_bought', requiresAllFlags: ['krennis_bought'],
                prompt: `Krennis does not look up from his slate. "The relay was a fault. The paperwork says so. The fee does not appear in any ledger, and neither do you."`,
                repeatPrompt: `"A fault. Logged."`,
                choices: [ { text: `Say nothing.`, morality: 0, loyalty: {}, defer: true, result: `He taps the slate once, which is as close as he comes to a thank you.` } ] },
            ] },
          { id: 'ship_designer_rael', x: 8, y: 18, kind: 'kdy_logistics_officer', label: 'Senior Designer Rael Thoss',
            questNpc: true,
            hideAfterFlags: ['rael_reported'],
            repeatPrompt: `Rael glances at the door. "The window is closing."`,
            prompt: `Sleeves rolled, his wrist port syncing a drive he angles toward you on purpose. "You are not KDY, not ISB. So you are an expensive problem or my last option before Krennis pulls my access."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `Sleeves rolled, his wrist port syncing a drive he angles toward you on purpose. "You are not KDY, not ISB. So you are an expensive problem or my last option before Krennis pulls my access."`,
                repeatPrompt: `Rael glances at the door. "The window is closing."`,
                choices: [
                  { text: `Ask what is on the drive.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Corrected schematics. On a board member's orders I put six shield errors into that hull, called cost reduction. Those ships fail under fire. The Committee must get the correction."`,
                    grants: { flags: ['rael_mission_understood'], codex: ['codex-ring-thoss-tolerances'] } },
                  { text: `Take the drive and agree to get it out.`, morality: 10, loyalty: { republic: 20 },
                    result: `He breathes out like a man who moved a weight rather than lost it. "Do not open it on Kuat. The metadata carries a trace. The privileged terminal in the Executive suites is the only clean relay."`,
                    grants: { flags: ['rael_helped', 'designer_contacted'], items: ['sabotage_blueprint_files'] } },
                  { text: `Tell him you will sell it to the highest bidder.`, morality: -15, loyalty: { underworld: 15 },
                    result: `He tucks the drive back in his wrist. "Then I take my chances with the sweep. I would rather the ships were built right and I was arrested."`,
                    grants: { flags: ['rael_blackmail_refused'] } },
                  { text: `Report him to Commander Krennis.`, morality: -12, loyalty: { underworld: 5 },
                    result: `Krennis's aides take Rael from his desk without a word. The drive goes into an ISB evidence bag. Krennis pays you, and does not thank you.`,
                    grants: { credits: 2500, flags: ['rael_reported', 'krennis_favor'] } },
                ] },
              { id: 'p_retry', requiresAllFlags: ['rael_blackmail_refused'], requiresNoneFlags: ['rael_helped', 'rael_reported'],
                prompt: `Rael has not moved the drive from his sleeve. "Still here. Still unarrested, which surprises me. Have you reconsidered?"`,
                repeatPrompt: `"The sweep is close."`,
                choices: [
                  { text: `Take the drive after all.`, morality: 10, loyalty: { republic: 15 },
                    result: `He hands it over with a tired nod. "Do not open it on Kuat. The privileged terminal in the Executive suites is the only clean relay."`,
                    grants: { flags: ['rael_helped', 'designer_contacted'], items: ['sabotage_blueprint_files'] } },
                  { text: `Report him to Commander Krennis.`, morality: -12, loyalty: { underworld: 5 },
                    result: `Krennis's aides take Rael from his desk without a word. The drive goes into an ISB evidence bag. Krennis pays you, and does not thank you.`,
                    grants: { credits: 2500, flags: ['rael_reported', 'krennis_favor'] } },
                  { text: `Leave him to the sweep.`, morality: 0, loyalty: {}, defer: true, result: `"I understand," he says, and does not.` },
                ] },
              { id: 'p_helped', requiresAllFlags: ['rael_helped'], requiresNoneFlags: ['blueprints_transmitted'],
                prompt: `Rael packs a case with unsteady care. "If the sweep finds the drive's trace before the Committee does, we are both finished. Use the terminal."`,
                repeatPrompt: `"Go. Please."`,
                choices: [ { text: `Ask why he did it.`, morality: 0, loyalty: {}, defer: true, result: `"Pride. Then shame. Then the sweep." He almost laughs. "Mostly shame."` } ] },
              { id: 'p_sent', requiresAllFlags: ['blueprints_transmitted'], requiresNoneFlags: ['rael_gauge_given'],
                prompt: `Rael holds the receipt like a ticket out. "The Committee has the correction. The ships will be built right. I will be arrested for it, and I find I do not mind."`,
                repeatPrompt: `"Build them right."`,
                choices: [ { text: `Take his tolerance gauge.`, morality: 5, loyalty: { republic: 5 },
                  result: `A scuffed brass gauge, the only thing on his desk that is his. "It measured the lie. Let it measure the truth."`,
                  grants: { flags: ['rael_gauge_given'], items: ['ring_thoss_gauge'] } } ] },
              { id: 'p_sent_done', requiresAllFlags: ['rael_gauge_given'],
                prompt: `Rael is cleaning out his desk into a single case. "They will come for me by morning. I am surprised how light this feels."`,
                repeatPrompt: `"Light, and strange."`,
                choices: [ { text: `Wish him well.`, morality: 0, loyalty: {}, defer: true, result: `"And you. Keep the gauge honest."` } ] },
            ] },
          { id: 'republic_inspector_thane', x: 14, y: 9, wander: 2, kind: 'republic_navy_inspector', label: 'Inspector Thane (Republic Navy)',
            repeatPrompt: `Thane keeps his voice low. "The Committee lands soon. I need those logs."`,
            prompt: `He wears his rank cylinders like a man who earned them. "Twelve days here, three discrepancies in the published logs that the internal records lack. Someone edits one set and not the other."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `He wears his rank cylinders like a man who earned them. "Twelve days here, three discrepancies in the published logs that the internal records lack. Someone edits one set and not the other."`,
                repeatPrompt: `Thane keeps his voice low. "The Committee lands soon. I need those logs."`,
                choices: [
                  { text: `Offer to pull KDY internal records for him.`, morality: -5, loyalty: { underworld: 8 }, defer: true,
                    result: `"Theft of proprietary data. The answer is no." Three seconds. "If the records reached the Committee by a source I cannot trace, that would serve. I have not asked you anything."`,
                    grants: { flags: ['thane_unofficial_contact'] } },
                  { text: `Tell him about the unreported hull class.`, morality: 10, loyalty: { republic: 12 }, defer: true,
                    result: `"The Committee knows it exists, not its specifications. A sealed executive safe is named in three intercepts. We cannot find it." He looks at you. "Can you?"`,
                    grants: { flags: ['thane_hull_class_discussed'] } },
                  { text: `Ask about the ordnance cases in the alcove.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Components from a Bador facility on no charter, no occupation order, no register I can find. Four cases, four cycles before the Committee. Draw your own conclusions."`,
                    grants: { flags: ['thane_bador_discussed'] } },
                ] },
              { id: 'p_evidence', requiresAnyFlag: ['blueprints_transmitted', 'registry_transmitted'], requiresNoneFlags: ['thane_paid'],
                prompt: `Thane looks like a man who read a message he was not meant to receive. "The Committee has it. Shield failures, Moff names, in the right hands. I never asked who sent it."`,
                repeatPrompt: `"Say nothing. Safe travels."`,
                choices: [ { text: `Accept the Committee's finder fee.`, morality: 4, loyalty: { republic: 8 },
                  result: `He slides across a sealed fee voucher and a small enamel commendation. "From the Committee, off the books. It is the best they can do in public."`,
                  grants: { credits: 3000, flags: ['thane_paid'], items: ['ring_oversight_commendation'], codex: ['codex-ring-oversight'] } } ] },
              { id: 'p_paid', requiresAllFlags: ['thane_paid'],
                prompt: `Thane packs his case. "The Committee lands tomorrow and finds nothing it cannot read. If Bador is where this ends, finish it before the Guild closes ranks."`,
                repeatPrompt: `"Safe travels. Say nothing."`,
                choices: [ { text: `Ask what Bador means for the Republic.`, morality: 0, loyalty: {}, defer: true,
                  result: `"Whoever holds that facility holds a ship the Republic forgot it commissioned. I would rather it were nobody."` } ] },
            ] },
          { id: 'kuati_sub_director_orenne', x: 28, y: 7, kind: 'kuati_sub_director', label: 'Sub-Director Orenne Vas-Kuat',
            repeatPrompt: `She turns a stylus end over end. "Guild authorization has not changed."`,
            prompt: `Young for the rank, which in House Kuat means someone took a political risk. Silver pauldrons, mauve robes. "The Briefing Ring is for registered KDY contractors and Republic liaison staff. You are neither."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `Young for the rank, which in House Kuat means someone took a political risk. Silver pauldrons, mauve robes. "The Briefing Ring is for registered KDY contractors and Republic liaison staff. You are neither."`,
                repeatPrompt: `She turns a stylus end over end. "Guild authorization has not changed."`,
                choices: [
                  { text: `Say you act for a Guild party.`, morality: -5, loyalty: { underworld: 5 }, defer: true,
                    result: `"Which party." She checks the datapad, finds the authorization current, and frowns. "The Guild may bring contractors in. I do not have to like it." She steps aside.`,
                    grants: { flags: ['orenne_cleared_access'] } },
                  { text: `Ask her about the Bador surface facility.`, morality: 5, loyalty: { republic: 8 }, defer: true,
                    result: `The stylus stops. "A KDY restricted moon, under Guild charter and House Kuat authority. Anything further is above your tier." She is careful not to say there is nothing unusual.`,
                    grants: { flags: ['orenne_bador_probed'] } },
                  { text: `Ask how she feels about the Imperial presence on the ring.`, morality: 5, loyalty: { republic: 5 }, defer: true,
                    result: `"I have obligations, not feelings." A glance toward Krennis, instantly controlled. "The Ten Families built this ring and intend to keep it. Temporary arrangements are temporary."`,
                    grants: { flags: ['orenne_house_kuat_view'] } },
                ] },
              { id: 'p_proof', requiresAnyFlag: ['orenne_cleared_access', 'orenne_house_kuat_view', 'orenne_bador_probed'], requiresNoneFlags: ['orenne_ally', 'czerka_record_sold'],
                prompt: `Orenne closes the datapad. "Czerka circles KDY like a salvager. I cannot act against a trade partner without proof. Bring me one document the Board cannot dismiss."`,
                repeatPrompt: `"Proof, not suspicion."`,
                choices: [
                  { text: `Give her the KDY executive bribe record.`, morality: 5, loyalty: { republic: 8 }, requires: { item: 'executive_bribe_record' },
                    result: `She reads the amount. "Design consultation fee, to a Moff. The Board member who arranged it also signed Czerka's integration memo." Her stillness cracks. "Take this. It opens the Director's door."`,
                    grants: { flags: ['czerka_expelled', 'orenne_ally'], items: ['kdy_director_override'], codex: ['codex-ring-czerka-clause'] } },
                  { text: `Give her the barracks intercept recording.`, morality: 8, loyalty: { republic: 8 }, requires: { item: 'ring_intercept_recording' },
                    result: `"ISB taping Guild labor on Guild property. That breaches the charter, even for them." She revokes his tap privileges in one signature. "Take this. It opens the Director's door."`,
                    grants: { flags: ['isb_taps_revoked', 'orenne_ally'], items: ['kdy_director_override'] } },
                  { text: `Say you do not have it yet.`, morality: 0, loyalty: {}, defer: true, result: `"Then find it. I will still be here, and so will Czerka."` },
                ] },
              { id: 'p_czerka_sold', requiresAllFlags: ['czerka_record_sold'], requiresNoneFlags: ['orenne_ally'],
                prompt: `Orenne's voice is chilled glass. "The bribe record reached Czerka before it reached me. I know who sold it. The Briefing Ring is open to you. Nothing else is."`,
                repeatPrompt: `"Nothing else is."`,
                choices: [
                  { text: `Give her the barracks intercept recording.`, morality: 8, loyalty: { republic: 8 }, requires: { item: 'ring_intercept_recording' },
                    result: `She reads the log twice. "You sold to Czerka and brought me this. I do not understand you, and I am glad of the recording." She signs a Director pass out of spite and courtesy.`,
                    grants: { flags: ['isb_taps_revoked', 'orenne_ally'], items: ['kdy_director_override'] } },
                  { text: `Say nothing.`, morality: 0, loyalty: {}, defer: true, result: `She turns a page. The conversation is over.` },
                ] },
              { id: 'p_expelled', requiresAllFlags: ['czerka_expelled'],
                prompt: `Orenne has the composure of someone who just won quietly. "The integration talks are suspended. Hax Durenn leaves on the evening shuttle. Use the Director pass as long as it serves us both."`,
                repeatPrompt: `"Use the pass wisely."`,
                choices: [ { text: `Ask what the Director pass opens.`, morality: 0, loyalty: {}, defer: true,
                  result: `"Executive berths, the Sentinel threshold, Bador transit. It is a loan. Return it to no one, but do not be caught using it badly."` } ] },
              { id: 'p_taps', requiresAllFlags: ['isb_taps_revoked'], requiresNoneFlags: ['czerka_expelled'],
                prompt: `Orenne sets down the stylus for once. "Krennis's listening posts are gone. He will say a fault caused it. The Director pass is yours, as promised."`,
                repeatPrompt: `"The pass is a loan. Use it well."`,
                choices: [ { text: `Ask what Krennis will do.`, morality: 0, loyalty: {}, defer: true,
                  result: `"Find another way to listen. He is patient. So am I."` } ] },
            ] },
        ],
        collectibles: [{ id: 'briefing_classified_report', x: 32, y: 8, label: 'Classified Engineering Report: Redacted', reward: 300 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 5, 34, 24, 'floor');
          carveRect(g, 1, 5, 8, 10, 'wall');  carveRect(g, 2, 6, 7, 9, 'floor');  pt(g, 8, 7, 'floor'); pt(g, 8, 8, 'floor');
          carveRect(g, 26, 5, 34, 12, 'wall'); carveRect(g, 27, 6, 33, 11, 'floor'); pt(g, 26, 8, 'floor'); pt(g, 26, 9, 'floor');
          carveRect(g, 26, 16, 34, 24, 'wall'); carveRect(g, 27, 17, 33, 23, 'floor'); pt(g, 26, 18, 'floor'); pt(g, 26, 19, 'floor');
          carveRect(g, 1, 16, 8, 24, 'wall'); carveRect(g, 2, 17, 7, 23, 'floor'); pt(g, 8, 18, 'floor'); pt(g, 8, 19, 'floor');
          pt(g, 0, 12, 'door'); pt(g, 0, 13, 'door');
          carveShips(g, this.ships);
          return g;
        },
      },

      kdy_lounge_market: {
        id: 'kdy_lounge_market', name: 'The Forge and Anvil — KDY Market Ring',
        ships: [
          { id: 'ship_czerka_hauler', kind: 'czerka_hauler', x: 10, y: 20, label: 'Czerka Bulk Hauler (In Repair)',
            description: `A Czerka bulk hauler, Kuati built and badly maintained, in a repair berth with its drive bay open like a surgical patient. A conduit crawler works inside the core. Czerka amber marks the spine. Three container slots are loaded. The empty one is unguarded.`,
            grantsFlag: 'czerka_hauler_examined' },
        ],
        subtitle: 'Kuat Drive Yards · Commercial Ring',
        width: 40, height: 28, spawnPos: { x: 20, y: 6 }, textureId: 'ferrowake',
        accent: '#8A6A3A', accentGlow: 'rgba(138,106,58,0.25)', accentDim: '#3A2A10',
        floorColor: '#201A10', floorAlt: '#281E12', wallDark: '#0C0A06', wallLight: '#161008',
        bg: 'radial-gradient(circle at 40% 60%, #22160A 0%, #0A0804 70%)', ambient: 'forge',
        paAnnouncements: [
          'Third shift begins on the hour. Compensation adjustment remains pending review.',
          'The Forge and Anvil is a neutral house. Please check blasters at the bar.',
          'Czerka trade desk is open until second shift. Right of first purchase applies.',
          'Deuterium rack inspection is overdue. Please keep clear of the stanchions.',
        ],
        decor: ['neon_sign', 'pipe'],
        doors: [
          { x: 0, y: 16, targetZone: 'kdy_landing_bay', targetPos: { x: 14, y: 24 }, label: 'Ring Transit' },
          { x: 0, y: 17, targetZone: 'kdy_landing_bay', targetPos: { x: 15, y: 24 }, label: 'Ring Transit' },
        ],
        worldObjects: [
          { id: 'ring_vista_market_a', propArt: 'bvista_20x5', propVariant: 'ring_a', x: 10, y: 5, once: false, iconKind: 'vista', label: 'Market Ring Observation Glass',
            description: `The market's glass roof opens on the array: hull frames lit amber in their cradles, Kuat's night side below, and the long curve of the ring running toward a distant bright shuttle.` },
          { id: 'ring_vista_market_b', propArt: 'bvista_20x5', propVariant: 'ring_b', x: 29, y: 5, once: false, iconKind: 'vista', label: 'Market Ring Skyline',
            description: `Past the Czerka alcove the dark opens again: a hull rib the size of a city block, work lamps strung along it, and the limb of Kuat glowing amber beneath.` },
          { id: 'sabacc_table_forge', iconKind: 'sabacc_table', propArt: 'sabacc_table', x: 30, y: 21, label: 'The Sabacc Table, Forge and Anvil',
            description: `A round table in the back corner, green felt worn grey at the edges. Four players: an off-duty KDY engineer, a Rodian shipwright chewing something, an Imperial clerk down three hundred, and an empty chair. The pot suggests the fourth did not mean to leave.`,
            triggersMinigame: 'sabacc' },
          { id: 'garroks_goods', iconKind: 'stall', propArt: 'market_stall', x: 10, y: 12, label: 'Garrok\'s Goods, Market Stall 7',
            description: `A deliberately unremarkable stall: generic signage, no holo displays. An Ithorian stands behind the counter, still as a survivor. Behind the tools sit blaster overclocks, serial-filed slicer spikes and forged KDY passes. He nods very slightly.`,
            grantsFlag: 'garrok_approached' },
          { id: 'shift_announcement_board', iconKind: 'board', propArt: 'shift_board', x: 20, y: 6, label: 'Shift Announcement Board',
            description: `A flickering holo board of shift rotations and safety notices. The top notice is six hours old: PLASMA WELDING DECK 4, MANDATORY OVERTIME, THIRD SHIFT EXTENDED TWELVE HOURS. The compensation adjustment has been pending eleven weeks.` },
          { id: 'union_bulletin', iconKind: 'board', propArt: 'union_bulletin', x: 5, y: 19, label: 'Barracks Corridor Bulletin',
            description: `A hand-written flimsi on the corridor wall, mostly torn away by Ring-Sec sweeps: "Deck 4 third shift. Seven injuries in four months. The review board has not met. We meet" and a gear symbol that is not the KDY gear.`,
            requiresNoneFlags: ['union_leader_met'],
            once: true, grantsFlags: ['union_bulletin_read', 'union_prenn_named'], grantsCodex: 'codex-ring-deck-four' },
          { id: 'surveillance_node', iconKind: 'surveillance_cam', propArt: 'surveillance_node', x: 5, y: 22, label: 'Ring-Sec Surveillance Node, Barracks Block 2',
            description: `A grey hardline sensor housing at head height in the second barracks junction, blue light active. A T-7 hex driver and forty seconds of unobserved access disconnect it. Ring-Sec will log equipment failure and file a maintenance ticket.`,
            requiresFlag: 'union_mission_accepted', lockedMessage: `A grey sensor housing, blue light active, bolted at head height. Someone is listening through it. You have no reason to touch it yet.`,
            triggersMinigame: 'valve_override',
            once: true, grantsFlag: 'union_surveillance_disabled' },
          { id: 'forge_anvil_bar', iconKind: 'forge_bar', propArt: 'forge_bar', x: 28, y: 18, label: 'Forge and Anvil Bar Counter',
            description: `Corellian whiskey served warm, because the refrigeration unit failed two months ago and the work order is still open. A Sullustan bartender judges no one. Carved in the bar: HERE SINCE YEAR ONE. Fresher scratching below: AND COUNTING.` },
          { id: 'czerka_trade_terminal', iconKind: 'czerka_terminal', propArt: 'czerka_terminal', x: 34, y: 7, label: 'Czerka Corporation Trade Desk',
            description: `A red and gold Czerka terminal in the northeast alcove, polished more aggressively than welcoming. The catalog is legal. The fine print grants Czerka right of first purchase on everything built with its parts. Someone circled it: THIS IS HOW THEY OWN YOU.`,
            once: false, grantsCodex: 'codex-ring-czerka-clause' },
          { id: 'fusion_fuel_canister_rack', iconKind: 'fuel_canister_rack', propArt: 'fuel_rack', x: 16, y: 9, label: 'Deuterium Fusion-Fuel Storage Rack',
            description: `Thirty-six sealed canisters in a blast-rated rack, amber stanchion lights pulsing. Three near the bottom show handling damage, one with a grey sealant disc over a hairline fracture. The last inspection is eight days overdue.`,
            requiresFlag: 'kelso_canister_warned', lockedMessage: `Thirty-six deuterium canisters in a blast-rated rack, three of them dented. Fuel Technician Kelso would know which one to worry about.`,
            triggersMinigame: 'valve_override',
            once: true, grantsFlag: 'canister_secured' },
          { id: 'sub_light_drive_cradle', iconKind: 'drive_cradle', propArt: 'drive_cradle', x: 25, y: 14, label: 'Sub-Light Drive Calibration Cradle',
            description: `A scaled ion engine calibration ring with a KY-88 seated, standard on Hammerhead frigates. The display shows 0.3 percent resonance drift. The tag reads DRIFT ACCEPTABLE, J.OSSEK. Below, other handwriting: NOT ACCEPTABLE, RECALIBRATE BEFORE INSTALL.`,
            requiresFlag: 'ossek_drift_explained', lockedMessage: `A KY-88 on a calibration ring, resonance display drifting just past spec. A tag says acceptable and a different hand says not. Ossek can tell you why.`,
            triggersMinigame: 'signal_siphon',
            once: true, grantsFlag: 'drive_recalibrated' },
          { id: 'conduit_crawler_port', iconKind: 'crawler_port', propArt: 'crawler_port', x: 12, y: 7, label: 'Conduit Crawler Maintenance Port',
            description: `A low hatch in the ring wall ringed in amber and black. A segmented copper M-33 crawler droid is emerging, its log flagging seven minor insulation faults and one major conduit degradation this cycle.` },
          { id: 'pneumatic_rivet_hopper', iconKind: 'rivet_hopper', propArt: 'rivet_hopper', x: 18, y: 22, label: 'Pneumatic Rivet Supply Hopper',
            description: `A supply hopper fed from an overhead line, a teal-grey Mark IV loader droid in standby beside it. The work order reads RIVET SPEC KDY-884, VOID-RATED, 40,000 UNITS, SECTION 7F, GUILD MASTER OSSEK. The hopper is half empty.` },
          { id: 'data_terminal_republic', iconKind: 'republic_terminal', propArt: 'republic_terminal', x: 25, y: 10, label: 'Republic-Issue Diagnostic Terminal',
            description: `Late Mandalorian Wars Republic Navy hardware, kept alive by stubbornness and spare parts, cycling Valor structural schematics. A sticky note says CONTACT VAEL IN SECTION FOUR. Below: WHO IS VAEL. Below that: EXACTLY.` },
          { id: 'safety_cert_kiosk', iconKind: 'board', propArt: 'kiosk', propVariant: 'manifest', x: 25, y: 20, label: 'Guild Safety Certification Kiosk',
            description: `The kiosk stamps an amber glyph: RING SAFETY, ALL FOUR DEFECTS CLOSED. A Guild bonus voucher prints with it, and the seal with the Guild crest.`,
            requiresAllFlags: ['coolant_pressure_vented', 'canister_secured', 'trunk_rerouted', 'drive_recalibrated'],
            lockedMessage: `The certification kiosk lists four open defects: the briefing coolant line, the deuterium canister, trunk B-12 and the drive cradle. It will not certify until all four are closed.`,
            once: true, grantsFlag: 'ring_safety_certified', grantsItem: 'ring_safety_seal', grantsCredits: 3000, grantsCodex: 'codex-ring-safety' },
          { id: 'market_noodle_stall', iconKind: 'stall', propArt: 'vendor_stall', propVariant: 'caf', x: 24, y: 7, once: false, label: 'Caf Stall',
            description: `A steaming caf stall with a hand-lettered menu, three stools and a cracked amber lamp. The owner wipes the counter whether or not anyone is there.` },
          { id: 'market_drum_array', iconKind: 'crate', propArt: 'drum_array', x: 4, y: 25, once: false, label: 'Barracks Fuel Drums',
            description: `Eight fuel drums in a clamp rack by the barracks wall, amber hazard bands, one tagged OVERDUE FOR INSPECTION.` },
          { id: 'market_crate_stack', iconKind: 'crate', propArt: 'crate_stack', propVariant: 'kdy', x: 20, y: 25, once: false, label: 'KDY Supply Crates',
            description: `A stack of blue-stenciled KDY supply crates, one pried open and re-nailed, a torn manifest corner still under the lid.` },
        ],
        npcs: [
          { id: 'market_patrol_orsk', x: 20, y: 13, wander: 5, kind: 'kdy_ring_sec', label: 'Market Patrol Orsk',
            repeatPrompt: `Orsk tips two fingers to his brow and keeps walking.`,
            prompt: `A Ring Sec patrolman wades through the Forge and Anvil crowd with the patience of someone who has broken up a thousand bar fights.`,
            choices: [ { text: `Ask how the market has been.`, morality: 0, loyalty: {}, result: `"Loud. Union talk is up and tempers with it. Keep your hands where I can see them and you will be fine."` } ] },
          { id: 'kuati_clerk_ysa', x: 14, y: 16, wander: 3, kind: 'kdy_logistics_officer', label: 'Kuati Clerk Ysa',
            phases: [{ id: 'p_main', requiresAllFlags: [] }],
            repeatPrompt: `Ysa checks her datapad and walks on.`,
            prompt: `A Kuati logistics clerk crosses the concourse with a stack of manifests under her arm.`,
            choices: [
              { text: `Ask about the shift schedule.`, morality: 0, loyalty: {}, defer: true, result: `"Third shift starts on the hour. The union will tell you it starts earlier. The Guild will tell you it starts later."` },
              { text: `Ask who signs her manifests.`, morality: 0, loyalty: {}, defer: true, result: `"A Logistics supervisor I have never met. He signs from the Operations ring, and the ink is always dry before it reaches me."` },
            ] },
          { id: 'garrok_ithorian', x: 10, y: 12, kind: 'broker', label: 'Garrok',
            requiresFlag: 'garrok_approached',
            hideAfterFlags: ['garrok_exposed'],
            repeatPrompt: `Garrok tilts his head. "You need something else?"`,
            prompt: `His bark-brown skin has the texture of recycled air. He nudges a grinder to a whine and speaks under it. "Bay Seven. The impounded boy. More access than you should have. What do you need?"`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `His bark-brown skin has the texture of recycled air. He nudges a grinder to a whine and speaks under it. "Bay Seven. The impounded boy. More access than you should have. What do you need?"`,
                repeatPrompt: `Garrok tilts his head. "You need something else?"`,
                choices: [
                  { text: `Ask about the Sector House operation Jaxen mentioned.`, morality: 0, loyalty: {}, defer: true,
                    result: `"A routing arrangement, not mine. I pass no restricted hardware." He turns an actuator over. "The party running it has a standing deal with a KDY Logistics officer above the tired woman. Free observation. The rest costs."`,
                    grants: { flags: ['garrok_sector_house_hinted'] } },
                  { text: `Buy forged ring clearance credentials.`, morality: -8, loyalty: { underworld: 10 }, requires: { credits: 300 }, defer: true,
                    result: `He turns a pass face down, covers it with a power cell and slides both to the edge. "Three hundred. The cell is free." He does not meet your eyes.`,
                    grants: { flags: ['garrok_traded'], items: ['forged_transit_chit'] } },
                  { text: `Ask what he knows about the union activity in the barracks.`, morality: 5, loyalty: { republic: 5 }, defer: true,
                    result: `"Careful. Ring-Sec disperses any meeting over four, so the organizers meet in threes. The one you want is Prenn, in the second barracks block." He lowers his voice. "She is running out of time."`,
                    grants: { flags: ['union_prenn_named'] } },
                ] },
              { id: 'p_confront', requiresAllFlags: ['manifest_accessed'], requiresNoneFlags: ['garrok_resolved'],
                prompt: `Garrok does not look at the receipt in your hand. His voice drops below the grinder. "That paper ends a route that feeds forty families. What do you intend to do with it?"`,
                repeatPrompt: `"Name your terms or leave."`,
                choices: [
                  { text: `Take it to Ring-Sec. Jaxen walks.`, morality: 10, loyalty: { republic: 8 },
                    result: `He nods once, an old gesture. "Then I should close the stall before they open it." The grinder stops for the first time. By evening Ring-Sec has sealed Stall 7 and the clamps are off Free Horizon.`,
                    grants: { flags: ['garrok_exposed', 'garrok_resolved', 'jaxen_cleared'], codex: ['codex-ring-sector-house'] } },
                  { text: `Sell him the receipt. Name your price.`, morality: -10, loyalty: { underworld: 12 },
                    result: `"Six thousand, and you were never in Bay 7." He counts it out in old wafers. Jaxen stays impounded. Somewhere a Corellian trader loses his ship.`,
                    grants: { credits: 6000, flags: ['garrok_bought', 'garrok_resolved'], codex: ['codex-ring-sector-house'] } },
                  { text: `Join the route. He keeps the stall, you keep a cut.`, morality: -12, loyalty: { underworld: 15 },
                    result: `He weighs you a long moment. "A partner who can slice a KDY terminal." He slides across a ledger chip and a first month's cut. "Two thirds of nothing, every month."`,
                    grants: { credits: 2500, flags: ['garrok_partnered', 'garrok_resolved'], items: ['ring_garrok_ledger'], codex: ['codex-ring-sector-house'] } },
                  { text: `Say you are not ready to decide.`, morality: 0, loyalty: {}, defer: true, result: `"Then be ready soon. Ring-Sec reads the same manifests you do."` },
                ] },
              { id: 'p_bought', requiresAllFlags: ['garrok_bought'],
                prompt: `Garrok counts the same wafers he paid you, slowly, without looking up. "A man who sells a ship for six thousand credits is a man I understand."`,
                repeatPrompt: `"I understand you. That is all."`,
                choices: [ { text: `Say nothing.`, morality: 0, loyalty: {}, defer: true, result: `"Quite right."` } ] },
              { id: 'p_partner', requiresAllFlags: ['garrok_partnered'],
                prompt: `Garrok greets you with his chin only. "Second Tuesday, the ledger chip. Do not ask me about the cargo, and I will not ask you about the receipt."`,
                repeatPrompt: `"The ledger is the ledger."`,
                choices: [ { text: `Ask who else is in the route.`, morality: 0, loyalty: {}, defer: true, result: `"Everyone who matters. Nobody who knows." He starts the grinder again.` } ] },
            ] },
          { id: 'union_leader_prenn', x: 5, y: 21, kind: 'mechanic', label: 'Prenn',
            requiresFlag: 'union_prenn_named',
            repeatPrompt: `Prenn keeps her voice low. "The node. It has to be tonight."`,
            prompt: `A plasma welder on hour eleven, who checked the corridor three ways before speaking. "The Ring-Sec node in this block records the barracks for Krennis. Kill it physically, so it reads as failure."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `A plasma welder on hour eleven, who checked the corridor three ways before speaking. "The Ring-Sec node in this block records the barracks for Krennis. Kill it physically, so it reads as failure."`,
                repeatPrompt: `Prenn keeps her voice low. "The node. It has to be tonight."`,
                choices: [
                  { text: `Agree to disable the surveillance node.`, morality: 5, loyalty: { republic: 10 },
                    result: `"Second block, third junction. Gray housing, blue light." She breathes out. "Seven injured on Deck 4. I am not asking for a rebellion, only one conversation Ring-Sec cannot record."`,
                    grants: { flags: ['union_mission_accepted', 'union_leader_met'], items: ['union_leaders_contact'] } },
                  { text: `Ask what happens if Ring-Sec identifies her first.`, morality: 0, loyalty: {}, defer: true,
                    result: `"They arrest me and three others. The meeting never happens and Deck 4 works another quarter of mandatory overtime." No self pity, which makes it worse.`,
                    grants: { flags: ['union_prenn_risk_discussed', 'union_leader_met'] } },
                  { text: `Warn her that Krennis is already monitoring the market ring.`, morality: 8, loyalty: { republic: 12 }, defer: true,
                    result: `"I know. We meet in threes." A pause. "Thank you for confirming it is hardline, not wireless. It changes my timing."`,
                    grants: { flags: ['union_krennis_warned', 'union_leader_met'] } },
                ] },
              { id: 'p_node', requiresAllFlags: ['union_mission_accepted'], requiresNoneFlags: ['union_surveillance_disabled'],
                prompt: `Prenn grips a wrench she is not using. "Second block. A hex driver, forty seconds, and do not look at the lens."`,
                repeatPrompt: `"Tonight. Hex driver, forty seconds."`,
                choices: [ { text: `Tell her you are on it.`, morality: 0, loyalty: {}, defer: true, result: `"Good. Do not be seen. If you are, you were never here."` } ] },
              { id: 'p_meeting', requiresAllFlags: ['union_surveillance_disabled'], requiresNoneFlags: ['union_meeting_held'],
                prompt: `Prenn's hands shake, just once. "The node is dark. We meet in Block 2 in ten minutes. Krennis will notice and send a riot squad. I need someone at the door."`,
                repeatPrompt: `"Block 2. Ten minutes."`,
                choices: [
                  { text: `Hold the door.`, morality: 8, loyalty: { republic: 8 }, defer: true,
                    result: `"Bless you. Eleven people, one table, one chance." The first stun baton hits the door before she finishes.`,
                    triggersMinigame: 'tactical_combat',
                    minigameConfig: { opponentProfile: 'ring_sec_riot_squad', flavorText: `The barracks corridor is two meters wide and the riot squad fills it, shields locked, batons humming. Behind you a door holds eleven people and a single folding table. You have until the batons break through.` },
                    onSuccessFlags: ['union_meeting_held'] },
                  { text: `Not yet.`, morality: 0, loyalty: {}, defer: true, result: `"They will not wait for us to be ready."` },
                ] },
              { id: 'p_final', requiresAllFlags: ['union_meeting_held'], requiresNoneFlags: ['union_resolved'],
                prompt: `Eleven workers stand behind Prenn, one with a split lip. "The meeting held. Now they want an answer from us: Review Board, strike, or Czerka's offer. Advise me."`,
                repeatPrompt: `"The vote is waiting on you."`,
                choices: [
                  { text: `Demand the Review Board, with Orenne as witness.`, morality: 10, loyalty: { republic: 12 },
                    result: `"The lawful road." Prenn almost smiles. "Orenne cannot refuse a witness. Slow, and real." Deck 4 votes to wait, and a review date is posted.`,
                    grants: { flags: ['union_resolved', 'union_review_board'], items: ['ring_union_token'] } },
                  { text: `Call the strike. One shift, the whole ring.`, morality: -3, loyalty: { underworld: 10 },
                    result: `Deck 4 downs tools at the next whistle. The ring stops for a shift, and the Guild learns the arithmetic Prenn knows. The strike fund sends you a thank you.`,
                    grants: { credits: 3000, flags: ['union_resolved', 'union_strike_called'], items: ['ring_union_token'] } },
                  { text: `Take Czerka's offer. Sell the union out.`, morality: -18, loyalty: { underworld: 12 }, requires: { flag: 'hax_pitch_heard' },
                    result: `Prenn reads your face before you speak. Hax pays well, and by the end of the quarter the union is contract labor with a nicer logo.`,
                    grants: { credits: 5000, flags: ['union_resolved', 'union_sold_czerka'] } },
                ] },
              { id: 'p_after_board', requiresAllFlags: ['union_review_board'],
                prompt: `Prenn pins the Review Board notice where the torn bulletin used to hang. "First date in eleven weeks. Seven injuries, and someone finally has to say why."`,
                repeatPrompt: `"The date holds."`,
                choices: [ { text: `Ask what comes after.`, morality: 0, loyalty: {}, defer: true, result: `"Questions. Then answers. Then maybe a deck that does not bleed."` } ] },
              { id: 'p_after_strike', requiresAllFlags: ['union_strike_called'],
                prompt: `Prenn is hoarse, ink on both sleeves. "The Guild paid the overtime back in one night. You taught them what a day costs. Do not think they will forget."`,
                repeatPrompt: `"They will not forget."`,
                choices: [ { text: `Ask what the Guild will do.`, morality: 0, loyalty: {}, defer: true, result: `"Watch us. Then pay us. Then watch us again."` } ] },
              { id: 'p_after_sold', requiresAllFlags: ['union_sold_czerka'],
                prompt: `Prenn does not look at you. "Czerka holds the rota now, and I hold nothing. Do not speak to me in front of the others."`,
                repeatPrompt: `"Not in front of the others."`,
                choices: [ { text: `Say nothing.`, morality: 0, loyalty: {}, defer: true, result: `She goes back to her welding line without a word.` } ] },
            ] },
          { id: 'forge_bartender', x: 28, y: 17, kind: 'cantina_owner', label: 'Vasek (Bartender)',
            phases: [{ id: 'p_main', requiresAllFlags: [] }],
            repeatPrompt: `Vasek refills whatever is in front of you. "Same again?"`,
            prompt: `He has the careful neutrality of someone who serves three factions every shift. He sets down a glass. "Corellian. Warm. It is all warm." He polishes a clean glass. "Atmosphere, or something else?"`,
            choices: [ { text: `Ask what the atmosphere says about the ring lately.`, morality: 0, loyalty: {}, defer: true,
              result: `"More Imperial liaisons this month than three years combined. Two shift supervisors transferred off-ring, no goodbye drinks." He sets the glass down. "When people skip goodbye drinks, they did not choose to leave."`,
              grants: { flags: ['vasek_atmosphere_discussed'] } } ] },
          { id: 'czerka_liaison_hax', x: 34, y: 8, kind: 'czerka_liaison', label: 'Hax Durenn (Czerka Corporation)',
            repeatPrompt: `Hax straightens his cuffs. "The offer remains on the table."`,
            prompt: `Everything is precisely positioned, from the cuff links to the practiced warmth. Crimson and gold, worn like a visible weapon. "Hax Durenn, Czerka Corporation. We offer complementary services to shipyard operations."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `Everything is precisely positioned, from the cuff links to the practiced warmth. Crimson and gold, worn like a visible weapon. "Hax Durenn, Czerka Corporation. We offer complementary services to shipyard operations."`,
                repeatPrompt: `Hax straightens his cuffs. "The offer remains on the table."`,
                choices: [
                  { text: `Ask what Czerka wants on a KDY ring.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Components, labor contracting, raw materials, at rates the Guild cannot match. We discuss a service integration with KDY executives. Modest at first. Complementary." The word lands precisely.`,
                    grants: { flags: ['hax_pitch_heard'] } },
                  { text: `Ask what happens to KDY workers under Czerka.`, morality: 5, loyalty: { republic: 8 }, defer: true,
                    result: `"Continued employment. Skill placement. Some role consolidation." His expression does not change. "Terms are renegotiated through proper channels." Every word technically true.`,
                    grants: { flags: ['hax_labor_impact_asked'] } },
                  { text: `Offer to facilitate access to KDY internal documents.`, morality: -15, loyalty: { underworld: 15 },
                    result: `He becomes very still, and the warmth sharpens. "What documents, and at what price?" He produces a datapad. "Business intelligence compensates appropriately."`,
                    grants: { flags: ['hax_intel_offered'] } },
                ] },
              { id: 'p_deal', requiresAllFlags: ['hax_intel_offered'], requiresNoneFlags: ['czerka_record_sold', 'czerka_expelled'],
                prompt: `Hax has the datapad open and a figure already typed. "Bring me whatever the Guild would rather not see. Executive correspondence. Clean wafers on delivery."`,
                repeatPrompt: `"Clean wafers. Immediately."`,
                choices: [
                  { text: `Sell him the executive bribe record.`, morality: -15, loyalty: { underworld: 15 }, requires: { item: 'executive_bribe_record' },
                    result: `He reads twice and smiles with all of his face. "Four thousand, and Czerka remembers who helped." The wafers are real. The leverage over the Guild is realer.`,
                    grants: { credits: 4000, flags: ['czerka_record_sold'] } },
                  { text: `Say you do not have it yet.`, morality: 0, loyalty: {}, defer: true, result: `"Time is a variable. The price is not."` },
                ] },
              { id: 'p_sold', requiresAllFlags: ['czerka_record_sold'], requiresNoneFlags: ['czerka_card_given'],
                prompt: `Hax greets you like an old friend. "Czerka's integration team lands next cycle. You were very helpful. Czerka does not forget helpful people, or their price."`,
                repeatPrompt: `"Helpful people are rare."`,
                choices: [ { text: `Take Czerka's trade card.`, morality: -3, loyalty: { underworld: 4 },
                  result: `A red and gold card, discount terms in tiny print. "The right of first purchase applies to everything you buy with it. Read the clause."`,
                  grants: { flags: ['czerka_card_given'], items: ['ring_czerka_trade_card'] } } ] },
              { id: 'p_sold_done', requiresAllFlags: ['czerka_card_given'],
                prompt: `Hax straightens a stack of contracts that were already straight. "Czerka is patient. Your card will work on every Czerka account, for as long as we find you useful."`,
                repeatPrompt: `"Useful is a flexible word."`,
                choices: [ { text: `Say nothing.`, morality: 0, loyalty: {}, defer: true, result: `"A sound policy."` } ] },
              { id: 'p_expelled', requiresAllFlags: ['czerka_expelled'],
                prompt: `Hax packs a case with exaggerated calm. "The Board has suspended talks. Czerka will return, with better paper. You, I will remember."`,
                repeatPrompt: `"Better paper. Next cycle."`,
                choices: [ { text: `Wish him a pleasant flight.`, morality: 0, loyalty: {}, defer: true, result: `"I never have any other kind."` } ] },
            ] },
          { id: 'deuterium_specialist_kelso', x: 20, y: 16, wander: 2, kind: 'deuterium_specialist', label: 'Fuel Technician Kelso',
            repeatPrompt: `Kelso checks his wrist monitor. "That fracture will not report itself."`,
            prompt: `Vacant from years of deuterium off-gas, but his hands are precise. He finishes a calibration pass on a canister before he looks up. "You are not engineering crew."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `Vacant from years of deuterium off-gas, but his hands are precise. He finishes a calibration pass on a canister before he looks up. "You are not engineering crew."`,
                repeatPrompt: `Kelso checks his wrist monitor. "That fracture will not report itself."`,
                choices: [
                  { text: `Ask about the micro-fracture on the fuel canister.`, morality: 5, loyalty: { republic: 5 }, defer: true,
                    result: `"Three weeks on the non-urgent queue. Duty cycle is fourteen months. This one is at twenty-two." He holds up a grey sealant disc. "If the patch fails under load, deuterium and a closed ring do not mix."`,
                    grants: { flags: ['kelso_canister_warned'], codex: ['codex-ring-deuterium'] } },
                  { text: `Ask how fuel reaches the construction bays.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Transit pod, transfer arm, hand-off. Seventeen steps, and someone skips four every third cycle. That is why I carry this." He taps the wrist monitor.`,
                    grants: { flags: ['kelso_fuel_system_explained'] } },
                ] },
              { id: 'p_secured', requiresAllFlags: ['canister_secured'], requiresNoneFlags: ['kelso_gift_given'],
                prompt: `Kelso lowers the monitor. The number on it has not moved in an hour. "Somebody finally vented it. I would have cried, but the compound dries the tears."`,
                repeatPrompt: `"Zero point three. I will take it."`,
                choices: [ { text: `Accept his spare monitor.`, morality: 3, loyalty: { republic: 3 },
                  result: `He unclips a battered wrist monitor from his belt. "Ring calibrated. It will scream before the alarm does. Keep it."`,
                  grants: { flags: ['kelso_gift_given'], items: ['ring_deuterium_monitor'] } } ] },
              { id: 'p_secured_done', requiresAllFlags: ['kelso_gift_given'],
                prompt: `Kelso is whistling through his teeth over a fresh canister. "Month nine. A good number. A beautiful number."`,
                repeatPrompt: `"A beautiful number."`,
                choices: [ { text: `Say you agree.`, morality: 0, loyalty: {}, defer: true, result: `"Nobody ever does. Thank you."` } ] },
            ] },
          { id: 'shipwright_master_ossek', x: 15, y: 12, kind: 'kdy_shipwright', label: 'Guild Shipwright Master Ossek',
            repeatPrompt: `Ossek marks his datapad. "The drift does not fix itself."`,
            prompt: `Built like the work he does: solid, unhurried, brass goggles pushed up over a cracked left lens. He studies the drive cradle. "Guild Master Ossek. Hull and propulsion certification, forty-one years."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `Built like the work he does: solid, unhurried, brass goggles pushed up over a cracked left lens. He studies the drive cradle. "Guild Master Ossek. Hull and propulsion certification, forty-one years."`,
                repeatPrompt: `Ossek marks his datapad. "The drift does not fix itself."`,
                choices: [
                  { text: `Ask about the calibration drift he signed off on.`, morality: 5, loyalty: { republic: 5 }, defer: true,
                    result: `"Point three is inside margin in standard conditions. At sustained thrust it feeds back and costs twelve percent output in three hours. I signed acceptable because I have no replacement. You see the problem."`,
                    grants: { flags: ['ossek_drift_explained'], codex: ['codex-ring-ky88'] } },
                  { text: `Ask what he knows about the unreported hull class.`, morality: -5, loyalty: { underworld: 10 }, defer: true,
                    result: `"Forty-one years building ships that carried Republic soldiers into war." His hands stop. "I know the hull and what it is built to do. Not the man to ask, but ask before the Committee lands."`,
                    grants: { flags: ['ossek_hull_acknowledged'] } },
                  { text: `Ask if he can authorize access past the Sentinel threshold.`, morality: -5, loyalty: { underworld: 8 }, defer: true,
                    result: `"Guild Master authority covers engineering and construction. Executive access is the Sentinel's call. Captain Vaelen holds that threshold." A long look. "I can send you into the cradles."`,
                    grants: { flags: ['ossek_access_explained'] } },
                ] },
              { id: 'p_fixed', requiresAllFlags: ['drive_recalibrated'], requiresNoneFlags: ['ossek_writ_given'],
                prompt: `Ossek runs a thumb along the cradle readout, lips moving. "Point zero one. I do not know whose hands did that, but they were better than mine."`,
                repeatPrompt: `"Point zero one. Worth a drink."`,
                choices: [ { text: `Accept the Guild Master's writ.`, morality: 3, loyalty: { republic: 3 },
                  result: `He stamps a brass-edged card with a hand that has stamped ten thousand. "Cradle access, any hull. Do not make me explain it to the Board."`,
                  grants: { flags: ['ossek_writ_given'], items: ['ring_guild_writ'] } } ] },
              { id: 'p_fixed_done', requiresAllFlags: ['ossek_writ_given'],
                prompt: `Ossek is polishing his goggle lens, finally. "A KY-88 that holds. I had forgotten what that sounded like."`,
                repeatPrompt: `"It holds."`,
                choices: [ { text: `Ask how the Guild pays for a replacement now.`, morality: 0, loyalty: {}, defer: true, result: `"It does not. That is the point of calibration."` } ] },
            ] },
          { id: 'sub_deck_slicer_myra', x: 12, y: 8, wander: 2, kind: 'sub_deck_slicer', label: 'Myra Veth (Sub-Deck Tech)',
            repeatPrompt: `Myra pulls cable from the conduit port. "Four hours left in this wall."`,
            prompt: `Arm elbow-deep in a conduit panel, a splice diagnostic glowing on her wrist through the grease. She finishes her sequence before she looks up. "You are in my light."`,
            phases: [
              { id: 'p_intro', requiresAllFlags: [],
                prompt: `Arm elbow-deep in a conduit panel, a splice diagnostic glowing on her wrist through the grease. She finishes her sequence before she looks up. "You are in my light."`,
                repeatPrompt: `Myra pulls cable from the conduit port. "Four hours left in this wall."`,
                choices: [
                  { text: `Ask what she is working on.`, morality: 0, loyalty: {}, defer: true,
                    result: `"Signal trunk B-12, third failure this month. Original insulation, thirty years old. Engineering patches it, nobody authorizes new line. If it goes, the briefing ring loses comms and power."`,
                    grants: { flags: ['myra_conduit_briefed'], codex: ['codex-ring-b12'] } },
                  { text: `Ask if she can slice the armory locker in the briefing ring.`, morality: -10, loyalty: { underworld: 12 }, defer: true,
                    result: `"Biometric, mechanical override, alarm relay. Someone scored the housing and failed." She thinks. "I need the guards looking elsewhere. A fuel fire in the landing bay would do. What is in it?"`,
                    grants: { flags: ['myra_armory_approached'] } },
                  { text: `Tell her about the acoustic intercept wall.`, morality: -5, loyalty: { underworld: 8 }, defer: true,
                    result: `"I know it. Who do you think routed the conduit that powers it?" She lowers her voice. "It is spliced into B-12 like a power shunt. I can give you a tap, if you can hold the signal."`,
                    grants: { flags: ['myra_intercept_discussed'] } },
                ] },
              { id: 'p_tap', requiresAllFlags: ['intercept_tapped'],
                prompt: `Myra nods at the monitor on her wrist. "Good tap. Krennis is recording the barracks, deck by deck. Give that to someone with authority, or keep it close."`,
                repeatPrompt: `"Good tap. Keep it close."`,
                choices: [ { text: `Ask who has the authority.`, morality: 0, loyalty: {}, defer: true, result: `"The Sub-Director in the briefing ring owns the Guild's charter. She will want to read it."` } ] },
              { id: 'p_armory', requiresAllFlags: ['armory_looted'], requiresNoneFlags: ['myra_kit_given'],
                prompt: `Myra counts spare batteries without looking at them. "I heard the locker door moved. I always hear. The fire in the bay covered you, and the rest was yours."`,
                repeatPrompt: `"Next time ask earlier."`,
                choices: [ { text: `Take her splice kit.`, morality: 0, loyalty: { underworld: 4 },
                  result: `She tosses you a roll of worn splice tools. "My cut is not credits. It is the next job. Ask earlier."`,
                  grants: { flags: ['myra_kit_given'], items: ['ring_myra_splice_kit'] } } ] },
              { id: 'p_armory_done', requiresAllFlags: ['myra_kit_given'],
                prompt: `Myra is whistling through her teeth at a new splice. "Good work. Do not tell the Guild I said so."`,
                repeatPrompt: `"Good work. Do not tell them."`,
                choices: [ { text: `Promise to keep quiet.`, morality: 0, loyalty: {}, defer: true, result: `"I knew I liked you."` } ] },
            ] },
        ],
        collectibles: [{ id: 'market_stall_components', x: 34, y: 9, label: 'Overstock Actuator Components', reward: 200 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 5, 38, 26, 'floor');
          carveRect(g, 1, 9, 8, 15, 'wall'); carveRect(g, 2, 10, 7, 14, 'floor'); pt(g, 8, 11, 'floor'); pt(g, 8, 12, 'floor');
          carveRect(g, 1, 18, 8, 26, 'wall'); carveRect(g, 2, 19, 7, 25, 'floor'); pt(g, 8, 20, 'floor'); pt(g, 8, 21, 'floor');
          carveRect(g, 22, 16, 38, 26, 'wall'); carveRect(g, 23, 17, 37, 25, 'floor'); pt(g, 22, 18, 'floor'); pt(g, 22, 19, 'floor');
          carveRect(g, 30, 5, 38, 12, 'wall'); carveRect(g, 31, 6, 37, 11, 'floor'); pt(g, 30, 8, 'floor'); pt(g, 30, 9, 'floor');
          pt(g, 0, 16, 'door'); pt(g, 0, 17, 'door');
          carveShips(g, this.ships);
          return g;
        },
      },

      bador_main_base: {
        id: 'bador_main_base', name: 'KDY Main Base — Bador',
        ships: [
          { id: 'ship_kdy_shuttle', kind: 'kdy_shuttle', x: 4, y: 10, label: 'KDY Shuttle K-14',
            description: "The Guild shuttle that carried you down, hull still ticking as it sheds heat. Pearl plating, a blue KDY stripe, twin engines venting steam. A placard on the ramp: KUATI ORBITAL ARRAY, ROUND TRIP, CLEARED GUEST. Your way off this rock.",
            grantsFlag: 'kdy_shuttle_examined' },
        ],
        subtitle: 'Bador Moon · Kuati Forward Directorate Hub',
        paAnnouncements: [
          'Quartermasters to Goods and Materials. Skiff KDY-9 remains overdue.',
          'Yellow Alert remains in effect. Stay inside the perimeter lights.',
          'Dust scrubber cycle on Pad Alpha in five minutes. Seal your collar rebreathers.',
          'Contractors are reminded that unlicensed droid modification is a Guild matter. Trike asks that the Guild stop calling.',
          'Medical staff to the bacta tent, please.',
        ],
        width: 44, height: 28, spawnPos: { x: 10, y: 14 }, textureId: 'ferrowake',
        accent: '#5A9BD0', accentGlow: 'rgba(90,155,208,0.3)', accentDim: '#24435E',
        floorColor: '#1E2228', floorAlt: '#252B33', wallDark: '#0C1016', wallLight: '#1A222C',
        bg: 'radial-gradient(circle at 40% 30%, #221C18 0%, #080A0E 70%)', ambient: 'moonbase',
        decor: ['pipe', 'girder'],
        doors: [
          { x: 29, y: 13, targetZone: 'bador_hq_interior', targetPos: { x: 2, y: 10 }, label: 'Main HQ Airlock', requiresFlag: 'bador_pass_issued', lockedMessage: 'The blast doors stay sealed. A voice repeats: BASE ACCESS PASS REQUIRED. Check in with Lieutenant Vane on the landing deck.', cinematic: 'airlock' },
          { x: 29, y: 14, targetZone: 'bador_hq_interior', targetPos: { x: 2, y: 11 }, label: 'Main HQ Airlock', requiresFlag: 'bador_pass_issued', lockedMessage: 'The blast doors stay sealed. A voice repeats: BASE ACCESS PASS REQUIRED. Check in with Lieutenant Vane on the landing deck.', cinematic: 'airlock' },
          { x: 43, y: 13, targetZone: 'bador_manufacturing', targetPos: { x: 1, y: 14 }, label: 'Sector 4 Facilities' },
          { x: 43, y: 14, targetZone: 'bador_manufacturing', targetPos: { x: 1, y: 15 }, label: 'Sector 4 Facilities' },
          { x: 16, y: 27, targetZone: 'city_outskirts_bador', targetPos: { x: 14, y: 1 }, label: 'City Outskirts' },
          { x: 17, y: 27, targetZone: 'city_outskirts_bador', targetPos: { x: 15, y: 1 }, label: 'City Outskirts' },
        ],
        worldObjects: [
          // ---- Moonscape vista over the north west apron
          { id: 'base_vista', propArt: 'bvista_20x5', propVariant: 'moonbase_a', x: 10, y: 5, once: false, iconKind: 'vista', label: 'Hub Skyline at Dusk',
            description: "A banded dusk sky over the crater rim: the dark ringed parent world, the Hub dome with its gold lit pad, four floodlight masts with red tips and pale light cones, and a shuttle climbing on a long contrail." },
          // ---- Landing Pad Alpha
          { id: 'landing_pad_alpha', propArt: 'landing_pad', x: 7, y: 9, label: 'Landing Pad Alpha',
            description: "A circular reinforced pad ringed in amber and black hazard striping. Thruster scorch scars the durasteel, eight tie down clamps hold the shuttle at its marks, and red runway diodes chase around the rim.",
            grantsCodex: 'codex-bador-hub', once: true },
          { id: 'pad_fuel_terminal', propArt: 'fuel_diag_terminal', x: 11, y: 13, label: 'Fueling and Diagnostic Terminal',
            description: "An alabaster pedestal with an inclined amber screen: SHUTTLE K-14, FUEL 92, HULL 100. Two ribbed coaxium hoses run into the pad grates. A second tab holds the navigation log, with three sorties that never logged a return.",
            grantsFlag: 'bador_nav_logs_read', grantsCodex: 'codex-bador-nav-logs', once: true },
          { id: 'pad_cargo_crates', propArt: 'crate_stack', x: 2, y: 12, label: 'Sealed Starter Cargo',
            description: "Two KDY crates with the gold gear and anvil crest, tarped at the pad edge. Magnetic lock strips glow red along the lids. Guild grade seals: a patient slicer could talk them open.",
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 3 }, once: true, grantsFlag: 'pad_crates_opened', grantsItem: 'kdy_circuit_boards' },
          { id: 'pad_coaxium_drums', propArt: 'drum_array', x: 3, y: 9, label: 'Coaxium Fuel Canisters',
            description: "Orange canisters in a locked rack, red gauges ticking up. A stencil on every drum: FIRE HAZARD, KEEP CLEAR OF THRUSTER WASH. Someone added in marker: AND OF TRIKE." },
          { id: 'pad_beacon_east', propArt: 'landing_beacon', x: 11, y: 15, label: 'Pad Landing Beacon',
            description: "A red strobe on a steel tripod, pulsing every three seconds so shuttles find the pad through the dust." },
          { id: 'pad_beacon_west', propArt: 'landing_beacon', x: 2, y: 15, label: 'Pad Landing Beacon',
            description: "The twin of the east beacon, its housing dented by a loader claw and patched with hazard tape." },
          { id: 'pad_rest_module', propArt: 'hab_block', x: 4, y: 18, label: 'Crew Rest Module',
            description: "A prefab rest module, four stacked bunks behind a sliding hatch. A note from the quartermaster: ALERT STATUS YELLOW, SLEEP IN YOUR BOOTS. A few quiet hours here cool the trail you leave behind.",
            heatDelta: -8 },
          { id: 'bador_intel_drop', propArt: 'datapad_table', propVariant: 'canister', x: 8, y: 16, label: 'Intel Drop Point',
            description: "A sealed datacanister in a recess at the extraction point. Partial schematics inside for a classified KDY vessel class.",
            once: true, requiresFlag: 'ghost_keel_active', lockedMessage: 'A canister sits sealed under a Guild lock. It will not open for someone Commander Vael has not briefed.',
            grantsFlag: 'partial_schematics_found', grantsItem: 'encrypted_kdy_schematics' },
          // ---- Main thoroughfare and guard posts
          { id: 'main_thoroughfare', propArt: 'thoroughfare', x: 18, y: 11, label: 'Main Thoroughfare',
            description: "A paved durasteel walkway sweeps from Pad Alpha toward the HQ in two long orange arcs, lit by amber guide lights and marked with chevrons. Holo signs read PAD A to the west and HQ 400 METERS to the east." },
          { id: 'guard_post_north', propArt: 'guard_post', x: 22, y: 10, label: 'Perimeter Guard Post',
            description: "A sandbagged ceramic post with a firing slit, a swivel floodlight and a thermos of something that is not caf. The duty log: Yellow Alert, hourly sweeps, no contacts. The last entry is crossed out twice." },
          { id: 'guard_post_south', propArt: 'guard_post', x: 23, y: 16, label: 'Perimeter Guard Post',
            description: "The southern post covers the yard. Its floodlight points past the fence into the dark crater field, and stays there." },
          // ---- Medic Tent
          { id: 'medic_sign', propArt: 'facility_sign', propVariant: 'medic', x: 26, y: 8, label: 'Medic Tent Sign',
            description: "A teal board over the tent flap: MEDIC TENT, TRIAGE AND BACTA. Below, smaller: WALK IN, WALK OUT, NO QUESTIONS ABOUT HOW YOU GOT HURT." },
          { id: 'bacta_pod', propArt: 'bacta_pod', x: 22, y: 5, label: 'Bacta Rejuvenation Pod',
            description: "A transparisteel cylinder on a brass and titanium base, filled with warm emerald bacta and slow bubbles. A cyan scanner ring sweeps top to bottom as you step on the plate. You step out restored. Wounded syndicate hands recover too.",
            healsRoster: true },
          { id: 'base_recovery_ward', propArt: 'bacta_pod', x: 27, y: 5, label: 'Tessa Recovery Ward',
            description: "A second bacta cylinder, new brass plaque on its base: DR. TESSA, RESONANCE WARD. Injured agents recover here, and the quiet takes some heat off you.",
            requiresFlag: 'base_cure_healed', invisibleLocked: true, healsRoster: true, heatDelta: -2 },
          { id: 'medical_locker', propArt: 'medic_crate', x: 21, y: 2, label: 'Medical Supplies Locker',
            description: "A white field locker with the red cross of the Guild medical corps: stimpacks, antidotes, triage kits and burn gel in labeled trays. Orderly Pell keeps the key and the stock list." },
          { id: 'vital_monitors', propArt: 'monitor_wall', x: 24, y: 2, label: 'Vital Monitors',
            description: "Six cots, six monitors, heart rates ticking green. A seventh trace, TEB, M., climbs in amber: fever, tremor, fixation on a sound no one else hears. Three more charts carry the note MINER, SURFACE EXPOSURE, UNEXPLAINED.",
            grantsFlag: 'miner_symptoms_noted', grantsCodex: 'codex-bador-miner-sickness', once: true },
          { id: 'base_cure_bench', propArt: 'base_cure_bench', x: 25, y: 6, label: 'Compounding Bench',
            description: "A steel bench with a brass centrifuge, a mortar of blue crystal dust and a rack of amber vials. A slate in Tessa's hand: DAMP THE RESONANCE, DO NOT STOP THE HEART.",
            requiresFlag: 'base_cure_started', lockedMessage: 'Tessa has not cleared the bench. She wants the crystal dust in her hand first.',
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 5 }, once: true,
            grantsFlag: 'base_cure_synthesized', grantsItem: 'base_resonance_antidote' },
          // ---- Goods and Materials Depot
          { id: 'depot_sign', propArt: 'facility_sign', propVariant: 'goods', x: 38, y: 8, label: 'Goods and Materials Sign',
            description: "A mustard board: GOODS AND MATERIALS, DEPOT AND SUPPLY SKIFFS. A chalk schedule hangs on a chain: SKIFF KDY-9, DUE DAWN, STILL DUE." },
          { id: 'depot_manifest_terminal', propArt: 'kuati_terminal', propVariant: 'manifest', x: 33, y: 3, label: 'Supply Manifest Terminal',
            description: "An alabaster console with a gold trimmed amber screen: SKIFF KDY-9, OVERDUE. ALLOYS 412 TONNES. BUY AND SELL OPEN. Incoming cargo is listed by tonnage, outgoing by what Dax can get away with.",
            grantsFlag: 'depot_manifest_read', once: true },
          { id: 'depot_secure_crate', propArt: 'cargo_container', x: 36, y: 2, label: 'KDY Secure Storage Crate',
            description: "An octagonal KDY crate sealed with a glowing red lock strip. Stencil: HIGH TIER CRAFTING STOCK, CLEARANCE LEVEL SIX. You have fingers and patience.",
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 5 }, once: true, grantsFlag: 'depot_secure_crate_opened', grantsItem: 'kdy_alloy_ingot' },
          { id: 'depot_repulsor_crane', propArt: 'repulsor_crane', x: 40, y: 5, label: 'Heavy Repulsor Crane',
            description: "A mustard yellow gantry spans the depot, its tractor dish glowing violet when live, flanked by four locking clamps. The pedestal has twin joysticks and a red safety button. A five ton alloy stack blocks the north platform.",
            triggersMinigame: 'valve_override', once: true, grantsFlag: 'depot_crane_cleared' },
          { id: 'depot_platform_cache', propArt: 'crate_stack', x: 40, y: 2, label: 'High Platform Cache',
            description: "Crates on the high platform, finally within reach: lunar alloy billets in blue Guild film and a lockbox of unlogged credits.",
            requiresFlag: 'depot_crane_cleared', lockedMessage: 'The north platform is buried under a five ton alloy stack. The repulsor crane could shift it.',
            once: true, grantsFlag: 'depot_cache_taken', grantsItem: 'kdy_coaxium_vial', grantsCredits: 350 },
          { id: 'depot_cargo_stacks', propArt: 'cargo_container', x: 34, y: 6, label: 'Raw Alloy Stacks',
            description: "Raw alloy billets and starship plating on repulsor pallets, each tagged with a lunar grid reference and a refining grade. Many read ORIGIN: UNLISTED." },
          { id: 'base_skiff_relay', propArt: 'comm_relay', x: 30, y: 2, label: 'Depot Relay Mast',
            description: "A tripod relay mast with a dish and a red blinker, wired into the depot grid. Its log keeps every beacon that ever passed overhead. The lock panel is Guild grade.",
            requiresFlag: 'base_mara_tasked', lockedMessage: 'The relay log is locked. Logistics Officer Orsk could tell you what to look for.',
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 4 }, once: true,
            grantsFlag: 'base_skiff_telemetry', grantsItem: 'base_skiff_beacon_log', grantsCodex: 'codex-base-skiff-kdy9' },
          // ---- Main HQ exterior
          { id: 'hq_dome_facade', propArt: 'hq_dome', x: 27, y: 13, label: 'Kuat Main HQ, Bador',
            description: "A semi dome of alabaster ceramic ribbed in slate blue and burnished gold, its prow shaped like a KDY battlecruiser. Solar slates line the east wing and antenna masts blink red. A palace dropped on a battlefield.",
            grantsCodex: 'codex-kuati-architecture', once: true },
          { id: 'hq_security_terminal', propArt: 'kuati_terminal', propVariant: 'security', x: 28, y: 12, label: 'HQ Security Terminal',
            description: "The checkpoint console at the blast doors: CLEARANCE, PASS. BASE LOGS, LOCKED. ALERT LEVEL, YELLOW. It logs every pass that touches the door.",
            grantsFlag: 'hq_security_log_seen', grantsCodex: 'codex-base-yellow-alert', once: true },
          { id: 'hq_command_board', propArt: 'command_board', x: 27, y: 15, label: 'Command Notice Board',
            description: "Pinned slips: overdue supply skiff, miners reporting tremors, a missing deep scan team, a lost glove. A red panel lists open bounties: three raider crews, ten thousand credits each, no questions about the paperwork.",
            grantsFlag: 'hq_bounties_read', once: true },
          // ---- Trike's Robotics
          { id: 'trike_sign', propArt: 'facility_sign', propVariant: 'trike', x: 10, y: 20, label: "Trike's Robotics Sign",
            description: "A copper board of hand welded letters: TRIKE'S ROBOTICS, REPAIR AND UPGRADES. On a tin scrap below: WE FIX WHAT THE GUILD WON'T. WE ALSO FIX THE GUILD." },
          { id: 'trike_upgrade_bench', propArt: 'robotics_bench', x: 6, y: 23, label: 'Robot and Gear Upgrade Bench',
            description: "An L shaped iron bench scarred by oil and weld burns. An overhead arm hovers over a half dismantled pit droid, micro torch glowing cyan, two amber screens scrolling schematics. It runs the scan, weld and coolant cycle on whatever you feed it.",
            triggersMinigame: 'arms_bench', grantsFlag: 'trike_bench_used' },
          { id: 'trike_salvage_bin', propArt: 'scrap_bin', x: 10, y: 23, label: 'Salvage Bin',
            description: "A dented bin of scrap, burnt boards and loose wiring. Trike swears something new falls in every night. You dig out a handful before the sparks start.",
            once: true, grantsItem: 'kdy_scrap_bundle' },
          { id: 'trike_repair_bay', propArt: 'workbench', x: 9, y: 25, label: 'Repair Bay',
            description: "A cradle bench with clamps and a coolant rail: pit droid chassis, a cracked astromech dome, an unlabeled protocol torso. A grease pencil tally on the wall counts droids returned to service. It is long.",
            requiresNoneFlags: ['trike_droid_wipe_known'] },
          { id: 'trike_core_rig', propArt: 'workbench', propVariant: 'arms', x: 9, y: 25, label: 'Scrubbed Core Rig',
            description: "A scrubbed memory core clamped in the bench vise, four status lights ticking cyan, amber, green and red. The screens scroll factory default prompts and nothing else. Trike is sure something hides underneath.",
            requiresFlag: 'trike_droid_wipe_known', invisibleLocked: true, requiresNoneFlags: ['base_core_recovered'],
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 4 }, once: true,
            grantsFlag: 'base_core_recovered', grantsItem: 'base_recovered_core' },
          { id: 'trike_repair_bay_after', propArt: 'workbench', x: 9, y: 25, label: 'Repair Bay',
            description: "The cradle is clear. The core went to the bench, and the grease pencil tally gained one line: ONE MORE HOME.",
            requiresFlag: 'base_core_recovered', invisibleLocked: true },
          // ---- Weapons Barracks
          { id: 'barracks_sign', propArt: 'facility_sign', propVariant: 'barracks', x: 30, y: 20, label: 'Weapons Barracks Sign',
            description: "A red board over the barracks door: WEAPONS BARRACKS, ARMORY AND RANGE. Smaller: LIVE FIRE ONLY ON THE RANGE. THE RANGE IS INDOORS. WE MEAN IT." },
          { id: 'barracks_weapon_bench', propArt: 'weapon_bench', x: 24, y: 22, label: 'Weapon Bench',
            description: "A reinforced wall bench with power couplings, magnetic clamps and an ammo press. A rack above holds six blaster frames. An orange optic scanner measures whatever you lock in the vise and offers attachments, ammunition and damage options.",
            triggersMinigame: 'arms_bench', grantsFlag: 'weapon_bench_used' },
          { id: 'barracks_armory_locker', propArt: 'armory_locker', x: 28, y: 22, label: 'Armory Locker',
            description: "A steel locker with a quartermaster seal: ammunition cells, gas grenades and a single EMP grenade in a foam slot. The seal is a courtesy tag for cleared guests.",
            once: true, grantsItem: 'emp_grenade' },
          { id: 'barracks_weapon_racks', propArt: 'weapon_rack', x: 27, y: 25, label: 'Weapon Racks',
            description: "Racks of KDY carbines, stun batons and thermal charges behind mesh gates, every slot stenciled with a name. Two slots have been empty for forty hours." },
          { id: 'barracks_bunks', propArt: 'hab_block', x: 23, y: 25, label: 'Contractor Bunk Bays',
            description: "Fortified bunk bays, neatly made on one side, rumpled on the other. Boots stand at the end of each bed, laces tied." },
          { id: 'target_simulator', propArt: 'target_range', x: 31, y: 24, label: 'Target Simulator',
            description: "A three lane indoor range with pop up silhouettes, red and white target rings and a lane console reading SIM 03. A training drone waits on the loading rail.",
            triggersMinigame: 'tactical_combat', once: true, grantsFlag: 'target_sim_cleared',
            minigameConfig: { opponentProfile: 'repulsor_drone', flavorText: 'The lane console chimes. SIM 03, LIVE TARGET. A KDY training drone lifts off its rail with a violet whine, plasma blade spinning. No heat, no consequences, just practice.' } },
        ],
        npcs: [
          { id: 'lt_vane', x: 14, y: 12, kind: 'kuati_officer', label: 'Lieutenant Ardan Vane', questNpc: true,
            repeatPrompt: 'Vane taps his datapad. "Pass is on you. The Commander is inside Main HQ."',
            prompt: 'A tall Kuati officer in a slate blue tunic, peaked cap set level. "Cleared guest, shuttle K-14. Lieutenant Ardan Vane, escort. The base is on Yellow Alert, so I must issue your Base Access Pass."',
            choices: [
              { text: 'Present your clearance and take the pass.', morality: 0, loyalty: {},
                result: "A brass edged card stamped with the Kuat wheel. \"Wear it where sentries can see it. The HQ doors read it and nothing else. Commander Vael is in the Tactical Sanctum. Stay on the lit Thoroughfare.\"",
                grants: { flags: ['bador_pass_issued', 'bador_vane_met', 'bador_alert_briefed'], items: ['bador_access_pass'] } },
              { text: 'Ask what the Yellow Alert is about.', morality: 5, loyalty: { republic: 3 },
                result: "\"Energy spikes beyond the perimeter, three nights running. The mining droids went dark, and skiff KDY-9 is overdue. Not reported lost. Not reported at all.\" He hands over the pass. \"The Commander has the rest.\"",
                grants: { flags: ['bador_pass_issued', 'bador_vane_met', 'bador_alert_briefed', 'bador_skiff_overdue'], items: ['bador_access_pass'], codex: ['codex-base-yellow-alert'] } },
              { text: 'Ask about the name. Vane is common where you come from.', morality: 0, loyalty: {},
                result: "The first small smile all day. \"Common on Kuat. My cousins in the Senate lobby sell it by the pound. I am no relation to anyone you know on Coruscant.\" The smile goes. \"The pass.\"",
                grants: { flags: ['bador_pass_issued', 'bador_vane_met', 'bador_alert_briefed'], items: ['bador_access_pass'] } },
            ],
            phases: [
              { id: 'phase_vane_pass' },
              { id: 'phase_vane_skiff_closed', requiresAllFlags: ['base_skiff_resolved'],
                repeatPrompt: 'Vane nods. "KDY-9 is closed in the log. Thank you."',
                prompt: 'Vane meets you with a salute he plainly did not plan. "Skiff KDY-9 is closed in the logs and nobody asked me to sign it. Thank you, freelancer."',
                choices: [{ text: 'Ask how the base is holding.', morality: 0, loyalty: {},
                  result: '"Quieter. The lights still flicker, but now people look up at them, not south." He straightens a cap that is already level.' }] },
              { id: 'phase_vane_end', requiresAllFlags: ['ghost_keel_complete'],
                repeatPrompt: 'Vane salutes. "Safe skies."',
                prompt: 'Vane has traded the duty tunic for dress blue. "Yellow Alert lifted at dawn. I am told you are the reason, and that I am not to ask how."',
                choices: [{ text: 'Tell him to keep asking.', morality: 0, loyalty: {},
                  result: '"Always." The small smile returns. "Safe journey. Do not tell me where."' }] },
            ],
          },
          { id: 'pilot_bador_ketz', x: 9, y: 14, kind: 'kdy_pilot', label: 'Pilot Maren Ketz (KDY Shuttle)', triggersOverlay: 'shuttle',
            greeting: '"Shuttle K-14 is fueled and ready. Back to the Array whenever you are."',
            repeatPrompt: 'The pilot taps the shuttle manifest and waits for a destination.',
            prompt: 'A KDY shuttle pilot in the blue Kuati flight uniform.', choices: [] },
          { id: 'dr_tessa', x: 25, y: 4, kind: 'medic', label: 'Dr. Tessa (Chief Medical Officer)', questNpc: true,
            repeatPrompt: 'Tessa checks a monitor, then you. "Resting is not optional, for anyone."',
            prompt: 'Dr. Tessa does not look up from the cot. "Seven miners, one symptom, and a diagnosis I do not believe. Exposure does not hum."',
            choices: [
              { text: 'Ask what you can do for the miners.', morality: 5, loyalty: { republic: 3 },
                result: '"A thermal crystal compound might damp the tremor. Blue coolant crystals, ground to dust. The Wastes grow them and Foundry 09 sells the dust. Bring me some and I will brew it."',
                grants: { flags: ['tessa_side_quest_open', 'miner_symptoms_known'] } },
              { text: 'Ask what the charts say.', morality: 0, loyalty: {},
                result: '"All seven were on the lunar shelf when the sky lights failed. All seven point south. I have stopped writing exposure on the line."',
                grants: { flags: ['tessa_side_quest_open', 'miner_symptoms_known'], codex: ['codex-bador-miner-sickness'] } },
            ],
            phases: [
              { id: 'phase_tessa_open' },
              { id: 'phase_tessa_cure', requiresAllFlags: ['tessa_side_quest_open'],
                repeatPrompt: 'Tessa nods at the bench. "Match the pathways before it cools."',
                prompt: 'Tessa keeps one hand on a bacta line. "The compound needs crystal dust, and I cannot leave these cots. Do you have it?"',
                choices: [
                  { text: 'Hand over the crystal dust.', morality: 3, loyalty: {}, requires: { item: 'thermal_crystal_dust' },
                    result: '"Good. Brass bench, left side. It needs a steady hand and a worse temper than mine. Match the pathways before it cools."',
                    grants: { flags: ['base_cure_started'] } },
                  { text: 'Not yet.', morality: 0, loyalty: {}, defer: true,
                    result: '"Then be quick. Teb has stopped sleeping."' },
                ] },
              { id: 'phase_tessa_healed', requiresAllFlags: ['base_cure_healed'],
                repeatPrompt: 'Tessa smiles for the first time. "The ward is yours any time."',
                prompt: 'Tessa stands for the first time in two days. "The cots are quiet. Nobody hums. I will not ask what the antidote cost you."',
                choices: [{ text: 'Tell her the ward is hers.', morality: 3, loyalty: {},
                  result: '"It carries my name already. The Guild did not object. Come heal here any time."' }] },
              { id: 'phase_tessa_sold', requiresAllFlags: ['base_cure_sold'],
                repeatPrompt: 'Tessa does not look up.',
                prompt: 'Tessa does not look up. "Teb is still humming. You took the only dose and sold it. Do not make me say it twice."',
                choices: [{ text: 'Say nothing and leave.', morality: 0, loyalty: {},
                  result: 'The monitor behind her ticks amber. She does not stop you.' }] },
            ],
          },
          { id: 'medic_orderly_pell', x: 28, y: 6, kind: 'medic', label: 'Orderly Pell (Medical Stores)',
            isVendor: true, vendorFaction: 'republic', repeatable: true,
            vendorGreeting: 'Pell hangs the key back on its cord. "Stims, antidotes, triage kits. Dr. Tessa signs the ledger, I only count."',
            vendorStock: ['kdy_stimpack', 'kdy_antidote', 'kdy_triage_kit', 'field_rations'],
            prompt: 'Orderly Pell runs the medical stores.', choices: [] },
          { id: 'miner_teb', x: 21, y: 4, kind: 'kdy_contractor', label: 'Miner Teb (Patient)', questNpc: true,
            repeatPrompt: 'Teb stares at the tent wall, humming a note no one else hears.',
            prompt: "Teb sits on the cot edge, helmet on his knee, tubes in his arm, hands never still. \"I was on the lunar shelf when the sky lights died. Something under my boots cleared its throat.\"",
            choices: [
              { text: 'Ask where the sound came from.', morality: 0, loyalty: {},
                result: "\"South. Always south, past the Wastes where the old survey stops.\" He grips your sleeve. \"The doctor says exposure. I know exposure. This is the moon calling.\"",
                grants: { flags: ['teb_sound_south', 'miner_symptoms_known', 'tessa_side_quest_open'] } },
              { text: 'Tell him to rest. You will look into it.', morality: 5, loyalty: { republic: 3 },
                result: "He lets go. \"Ask the doctor for the charts. Someone has to count how many of us hear it.\"",
                grants: { flags: ['miner_symptoms_known', 'tessa_side_quest_open'] } },
            ],
            phases: [
              { id: 'phase_teb_first' },
              { id: 'phase_teb_cure', requiresAllFlags: ['base_cure_synthesized'], requiresNoneFlags: ['base_cure_resolved'],
                repeatPrompt: 'Teb rattles the cot. "Please. The quiet."',
                prompt: "Teb's cot rattles with his tremor. Tessa watches your hand: one batch, enough for one man at full strength, or six patients at a sixth.",
                choices: [
                  { text: 'Give Teb the full dose.', morality: 8, loyalty: { republic: 3 }, requires: { item: 'base_resonance_antidote' },
                    result: 'The tremor stops within a minute. "South of the Wastes, under the ground. A drive coil, idling. Thank you." He presses his hazard pay into your hand. +400 credits.',
                    grants: { credits: 400, flags: ['base_cure_healed', 'base_cure_resolved', 'base_cure_full'], codex: ['codex-base-resonance'] } },
                  { text: 'Split the batch across all six cots.', morality: 5, loyalty: { republic: 3 }, requires: { item: 'base_resonance_antidote' },
                    result: 'Six monitors settle at once. Tessa laughs, shakily, and signs the Guild medical fund over to you. +900 credits, and a triage kit from the ward stock.',
                    grants: { credits: 900, items: ['kdy_triage_kit'], flags: ['base_cure_healed', 'base_cure_resolved', 'base_cure_split'], codex: ['codex-base-resonance'] } },
                  { text: 'Pocket the antidote. The Syndicate will pay.', morality: -12, loyalty: { underworld: 8 }, requires: { item: 'base_resonance_antidote' },
                    result: 'Teb watches the vial leave. A Syndicate fixer in the depot pays 1,500 credits within the hour, no questions. Behind you, the humming starts again.',
                    grants: { credits: 1500, flags: ['base_cure_sold', 'base_cure_resolved'] } },
                  { text: 'Not yet.', morality: 0, loyalty: {}, defer: true,
                    result: 'Teb grips the cot rail. "Hurry. Please."' },
                ] },
              { id: 'phase_teb_healed', requiresAllFlags: ['base_cure_healed'],
                repeatPrompt: 'Teb listens to the tent breathe and smiles.',
                prompt: 'Teb sits up, helmet in his lap, silent for the first time. "I can hear the tent now. The canvas. The others breathing."',
                choices: [{ text: 'Ask what he will do now.', morality: 0, loyalty: {},
                  result: '"Go back to the shelf. Slowly. And never face south without a friend."' }] },
              { id: 'phase_teb_sold', requiresAllFlags: ['base_cure_sold'],
                repeatPrompt: 'Teb hums, eyes on the south wall.',
                prompt: 'Teb hums, eyes on the south wall. He does not know you.',
                choices: [{ text: 'Leave him to it.', morality: 0, loyalty: {},
                  result: 'The note follows you out of the tent.' }] },
            ],
          },
          { id: 'trike', x: 9, y: 22, kind: 'cyborg_mechanic', label: 'Trike (Master Mechanic)', questNpc: true,
            repeatPrompt: 'Trike waves a torch. "Bring me something broken or go away. Preferably both."',
            prompt: 'A grease stained cyborg, one orange optic whirring, elbow deep in a pit droid chassis. "Hold this. No, the other end. You are the new freelancer? Know Aratech engines? Nobody here will listen."',
            choices: [
              { text: 'Ask him about vintage starfighter engines.', morality: 0, loyalty: {},
                result: "He nearly drops the torch. \"A person with taste.\" Twenty minutes of Aratech thrust curves follow, then a handful of burnt boards pressed into your hand. \"For the trouble.\"",
                grants: { flags: ['trike_met', 'trike_engine_talk'], items: ['kdy_circuit_boards'] } },
              { text: 'Ask what he can do for your gear and droids.', morality: 0, loyalty: {},
                result: "\"Upgrade, repair, tune, occasionally resurrect. Bench, bay and bin are yours if you sort the bin.\" The optic whirs. \"Never mention the Guild board.\"",
                grants: { flags: ['trike_met'] } },
              { text: 'Ask about the energy spikes that hit the mining droids.', morality: 5, loyalty: { republic: 3 },
                result: "His torch goes out. \"Eleven droids came back with their memory cores scrubbed to factory default. Not damaged. Scrubbed. Something talked to them, then cleaned up.\" He leans in. \"That is not weather.\"",
                grants: { flags: ['trike_met', 'trike_droid_wipe_known'] } },
            ],
            phases: [
              { id: 'phase_trike_first' },
              { id: 'phase_trike_bench', requiresAllFlags: ['trike_bench_used'], requiresNoneFlags: ['base_trike_bench_praised'],
                repeatPrompt: 'Trike grunts approval at the bench.',
                prompt: 'Trike inspects your last tune and grudgingly nods. "You work my bench like someone with hands. Salvage fee, for not wrecking it."',
                choices: [{ text: 'Take the salvage fee.', morality: 0, loyalty: {},
                  result: '"Do not tell the Guild board I was nice." He drops 100 credits of scrap chits into your palm.',
                  grants: { credits: 100, flags: ['base_trike_bench_praised'] } }] },
              { id: 'phase_trike_wipe', requiresAllFlags: ['trike_met'], requiresNoneFlags: ['trike_droid_wipe_known'],
                repeatPrompt: 'Trike pokes a dead droid with a probe.',
                prompt: 'Trike pokes a dead droid with a probe. "Eleven came back blank. Ask me about it or do not, but stop staring at the bin."',
                choices: [{ text: 'Ask about the blank droids.', morality: 5, loyalty: { republic: 3 },
                  result: "His torch goes out. \"Cores scrubbed to factory default. Not damaged. Scrubbed. Something talked to them, then cleaned up.\" He leans in. \"That is not weather.\"",
                  grants: { flags: ['trike_droid_wipe_known'] } }] },
              { id: 'phase_trike_core', requiresAllFlags: ['base_core_recovered'], requiresNoneFlags: ['base_trike_resolved'],
                repeatPrompt: 'Trike turns the core in the light. "A call, not a wipe."',
                prompt: 'Trike turns the recovered core in the light. "A tone, under the factory blank. A call, not a wipe. Whoever sent it wanted machines to come home."',
                choices: [
                  { text: 'Give the fragment to Commander Vael.', morality: 5, loyalty: { republic: 5 }, requires: { item: 'base_recovered_core' },
                    result: 'Trike wraps the core in oilcloth. "Tell him the Guild board owes me an apology." Vael\'s staff logs it as evidence. +300 credits for the courier run.',
                    grants: { credits: 300, flags: ['base_trike_resolved', 'base_trike_to_vael'], codex: ['codex-base-droid-wipe'] } },
                  { text: 'Have Trike harden your gear against the signal.', morality: 0, loyalty: {}, requires: { item: 'base_recovered_core' },
                    result: 'Trike spends an evening inside your kit. "Shielded, filtered and rude to strangers." A matte signal shield clips onto your gear, warm from the weld.',
                    grants: { items: ['base_signal_shield'], flags: ['base_trike_resolved', 'base_trike_shield'], codex: ['codex-base-droid-wipe'] } },
                  { text: 'Not yet.', morality: 0, loyalty: {}, defer: true,
                    result: '"It will keep. Eventually. Do not make me keep it long."' },
                ] },
              { id: 'phase_trike_done', requiresAllFlags: ['base_trike_resolved'],
                repeatPrompt: 'Trike is arguing with a second pit droid.',
                prompt: 'Trike has found a second pit droid to argue with. "Good to see you. Bring me more broken things."',
                choices: [{ text: 'Wave and leave him to it.', morality: 0, loyalty: {},
                  result: '"The tally is longer because of you. Do not make it embarrassing."' }] },
            ],
          },
          { id: 'dax', x: 37, y: 6, kind: 'kdy_logistics_officer', label: 'Dax (Supply Logistics Chief)',
            isVendor: true, vendorFaction: 'underworld', repeatable: true,
            vendorGreeting: 'Dax slides a ledger under the counter with one elbow. "Buy, sell, do not count."',
            vendorStock: ['kdy_alloy_ingot', 'kdy_hull_plate', 'kdy_coaxium_vial', 'tool_hydrospanner', 'emp_grenade'],
            prompt: 'Dax runs the depot.', choices: [],
            phases: [
              { id: 'phase_dax_default' },
              { id: 'phase_dax_exposed', requiresAllFlags: ['base_dax_exposed'],
                vendorGreeting: 'Dax counts out change with a stiff smile. "The Inspector has my books. Prices are honest now. Painfully."' },
              { id: 'phase_dax_covered', requiresAllFlags: ['base_dax_covered'],
                vendorGreeting: 'Dax winks. "A friend of the depot. The coaxium is discounted. In spirit."' },
            ] },
          { id: 'bador_squad_kaelin', x: 27, y: 23, wander: 2, kind: 'republic_guard', label: 'Squad Leader Kaelin', questNpc: true,
            repeatPrompt: 'Kaelin keeps a hand near her carbine. "Stay inside the perimeter."',
            prompt: '"You are the freelancer Vael authorized." Not a question. "Twelve troopers here, six at forward position seven, and no answers about the Wastes. Something knocked out our sensor drones."',
            choices: [
              { text: 'Tell her about the unknown structure marker.', morality: 5, loyalty: { republic: 8 }, result: '"We have had it marked three days. Heat signature wrong for KDY construction." She lowers her voice. "The Commander thinks it is a ship."', grants: { flags: ['kaelin_structure_discussed'] } },
              { text: 'Ask what knocked out the drones.', morality: 0, loyalty: {}, result: '"Plasma interference. Natural sources would register differently. It is targeted. Someone does not want that grid scanned."', grants: { flags: ['kaelin_drone_discussed'] } },
              { text: 'Ask about the target simulator.', morality: 0, loyalty: {}, result: '"Sim 03, in the barracks. Clear it and I log you range qualified. Nobody beats the drone without flinching."' },
            ],
            phases: [
              { id: 'phase_kaelin_first' },
              { id: 'phase_kaelin_outskirts', requiresAllFlags: ['outskirts_complete'], requiresNoneFlags: ['base_kaelin_outskirts_paid'],
                repeatPrompt: 'Kaelin nods at the marker on your belt.',
                prompt: 'Kaelin looks up from a field map, a new cut on her jaw. "Breach Point held. My cousin runs that line. The squad wants you to carry this."',
                choices: [{ text: 'Accept the squad marker.', morality: 3, loyalty: { republic: 3 },
                  result: 'A scorched trooper tag on a bootlace, stamped with twelve names. "Show it at any KDY post. Someone will stand straighter." The squad purse adds 400 credits.',
                  grants: { credits: 400, items: ['base_squad_marker'], flags: ['base_kaelin_outskirts_paid'] } }] },
              { id: 'phase_kaelin_sim', requiresAllFlags: ['target_sim_cleared'], requiresNoneFlags: ['base_kaelin_sim_paid'],
                repeatPrompt: 'Kaelin tips her cap. "Range qualified."',
                prompt: 'Kaelin has the lane log open. "SIM 03, cleared. That drone has been through six of my troopers. Range qualified, freelancer."',
                choices: [{ text: 'Collect the range bounty.', morality: 0, loyalty: {},
                  result: 'She slides a chit across the table. "A tradition. Nobody ever collects." +250 credits. The squad nods as you pass.',
                  grants: { credits: 250, flags: ['base_kaelin_sim_paid'] } }] },
            ],
          },
          // ---- K-series guard droids on a synchronized perimeter loop
          { id: 'k_sentinel_alpha', x: 16, y: 12, kind: 'k_series_droid', label: 'K-Series Security Automaton K-01',
            patrol: [[25, 12], [25, 15], [16, 15], [16, 12]],
            repeatPrompt: 'K-01 pans its red lens across you. "Proceed. Remain within the lit area."',
            prompt: "A two meter automaton of alabaster plate over a dark endoskeleton, one horizontal red lens, the gold Kuat wheel on its chest, a shield panel on its left forearm. Its footfalls ring like anvils.",
            choices: [
              { text: 'Stand still and let it scan you.', morality: 0, loyalty: {},
                result: "The lens sweeps from boots to collar rebreather and back. \"Visitor, cleared, pass confirmed.\" A pause of exactly one second. \"KDY automata do not engage cleared personnel. Do not leave the lights.\"",
                grants: { flags: ['met_k_series'], codex: ['codex-k-series'] } },
            ],
            phases: [
              { id: 'phase_k1_first' },
              { id: 'phase_k1_manifold', requiresAllFlags: ['manifest_restarted'],
                repeatPrompt: 'K-01 resumes its loop. "Posture Green."',
                prompt: 'K-01 stops mid stride. "Unknown thermal signature: ceased. Perimeter posture lowered to Green. Cleared guest, you are logged as the cause."',
                choices: [{ text: 'Ask if it will miss the alert.', morality: 0, loyalty: {},
                  result: '"K-series automata do not miss." A pause of exactly one second. "The duty log records the silence as an anomaly. Approved anomaly."' }] },
            ],
          },
          { id: 'k_sentinel_beta', x: 25, y: 15, kind: 'k_series_droid', label: 'K-Series Security Automaton K-02',
            patrol: [[16, 15], [16, 12], [25, 12], [25, 15]],
            repeatPrompt: 'K-02 turns exactly forty five degrees and sweeps its lens. "Proceed."',
            prompt: "The second sentinel walks the loop in perfect antiphase, so one is always at the Thoroughfare's west end when the other is at the east. Same plate, same crimson slot.",
            choices: [
              { text: 'Ask if it has seen anything beyond the perimeter.', morality: 0, loyalty: {},
                result: "\"Sensor drones ceased transmitting at grid seven seven nine. K-series sensors record a repeating thermal signature of unknown origin.\" The lens holds four seconds. \"Query exceeds guest clearance.\"",
                grants: { flags: ['met_k_series'], codex: ['codex-k-series'] } },
            ],
            phases: [
              { id: 'phase_k2_first' },
              { id: 'phase_k2_end', requiresAllFlags: ['ghost_keel_complete'],
                repeatPrompt: 'K-02 carries on, a half step slower.',
                prompt: 'K-02 halts and sweeps its lens across you. "Alert Yellow rescinded. All personnel accounted for. One cleared guest, exceptional."',
                choices: [{ text: 'Tell it to carry on.', morality: 0, loyalty: {},
                  result: '"Carrying on." It resumes the loop a half step slower than before, which should be impossible.' }] },
            ],
          },
          // ---- Walking staff, droids and aliens
          { id: 'patrol_marine_ross', x: 24, y: 17, wander: 3, kind: 'kdy_security_marine', label: 'KDY Security Patrol Ross',
            repeatPrompt: 'Ross nods without breaking stride. "Yellow Alert. Keep moving."',
            prompt: 'A KDY security marine in sealed grey armor patrols the yard, rebreather hissing. He gives your pass a long look and decides you are not his problem.',
            choices: [{ text: 'Ask how the watch is going.', morality: 0, loyalty: {}, result: '"Quiet, which is worse. Three nights the perimeter lights flicker at the same minute. I set my watch by it."' }] },
          { id: 'patrol_marine_cael', x: 31, y: 8, wander: 3, kind: 'kdy_security_marine', label: 'KDY Security Patrol Cael',
            repeatPrompt: 'Cael steps aside. "Move along."',
            prompt: 'A second patrolman keeps to the corridor between the depot and the HQ, a stim patch on his neck and the look of a man who has not slept since the alert.',
            choices: [{ text: 'Ask if the HQ checkpoint is strict.', morality: 0, loyalty: {}, result: '"Pass or no pass, no exceptions. The Commander tore the last corporal who waved a visitor through a new one."' }] },
          { id: 'ge3_diplomat', x: 39, y: 6, wander: 1, kind: 'ge3_protocol', label: 'GE3-Series Protocol Droid Gee-Three',
            repeatPrompt: 'Gee-Three bows. "Forgive me, I am mid negotiation."',
            prompt: "A Kuati GE3 in mirror polished silver with brushed gold trim stands at Dax's elbow translating between a Zabrak crew chief and a Devaronian inspector, with gestures too elegant for this depot.",
            choices: [{ text: 'Ask what it is translating.', morality: 0, loyalty: {}, result: '"Eleven contractors, four languages, and one disagreement over whether Skiff KDY-9 is late or merely unpunctual. Neither side is moved."' }] },
          { id: 'loader_unit_one', x: 36, y: 8, wander: 2, kind: 'binary_loader', label: 'Heavy Binary Loader BL-4',
            repeatPrompt: 'BL-4 whirs, claws opening and closing.',
            prompt: 'A scratched industrial yellow loader droid on heavy treads, twin claws stacking a pallet of alloy billets, an orange strobe flashing as it backs up, beeping.',
            choices: [{ text: 'Step aside and let it work.', morality: 0, loyalty: {}, result: 'It beeps twice, which in binary means thank you or move, depending on pitch. You decide it was thank you.' }] },
          { id: 'loader_unit_two', x: 41, y: 10, wander: 3, kind: 'binary_loader', label: 'Heavy Binary Loader BL-7',
            repeatPrompt: 'BL-7 chirps a warning and waits.',
            prompt: 'A twin of BL-4 hauling a durasteel plate down the east corridor, claws locked and strobe turning. By some margin, the loudest thing on the base.',
            choices: [{ text: 'Wave it through.', morality: 0, loyalty: {}, result: 'It rumbles past, plate swaying, and spits a puff of venting steam at your boots.' }] },
          { id: 'gnk_unit_nine', x: 9, y: 17, wander: 2, kind: 'gnk_power', label: 'GNK Power Droid',
            repeatPrompt: 'The GNK repeats its single word. "Gnk."',
            prompt: 'A blocky matte grey power droid on stubby legs, a yellow umbilical trailing to a pad welding rig. It says one thing, constantly.',
            choices: [{ text: 'Say hello.', morality: 0, loyalty: {}, result: '"Gnk gnk." It trundles around you, cable dragging, and plugs into the next rig. You feel understood.' }] },
          { id: 'astromech_r4', x: 12, y: 11, wander: 4, kind: 'kuati_astromech', label: 'Kuati Astromech R4-K1',
            repeatPrompt: 'R4-K1 chirps and rolls a polite circle around you.',
            prompt: 'An astromech in Kuati livery, pearlescent dome, sapphire panels, gold collar stripe, rolling between the shuttle ramp and the fuel terminal on an errand of its own.',
            choices: [{ text: 'Ask it for a pad status.', morality: 0, loyalty: {}, result: 'It projects a blue schematic of Pad Alpha: all clamps green, fuel ninety two, one red mark far to the south. It will not say what the mark is.' }] },
          { id: 'zabrak_wright_ghan', x: 3, y: 14, wander: 2, kind: 'zabrak_techwright', label: 'Zabrak Tech-Wright Ghan',
            repeatPrompt: 'Ghan glances up from his holopad. "Ask me later, I am counting."',
            prompt: 'A six horned Zabrak in a dust stained leather apron, geometric tattoos down his nose and jaw, checking blueprint schematics against the crate stencils and humming.',
            choices: [{ text: 'Ask what he is counting.', morality: 0, loyalty: {}, result: '"Crates. The manifest says forty. I count thirty six. Someone on Kuat is lying and someone on Bador is stealing. I would like to know who gets blamed."', grants: { flags: ['base_crates_short'] } }] },
          { id: 'devaronian_inspector_rhok', x: 34, y: 4, wander: 2, kind: 'devaronian_inspector', label: 'Devaronian Freight Inspector Rhok', questNpc: true,
            repeatPrompt: 'Rhok flashes a sharp smile. "Your manifest is in order, so far."',
            prompt: 'A crimson Devaronian in a maroon vest, horns polished, reading the depot manifest the way a hawk reads a field. "Weight. Cargo heavier going out than in, or not on the schedule at all."',
            choices: [{ text: 'Ask what he is looking for.', morality: 0, loyalty: {}, result: '"The unscheduled kind. Ask the Zabrak about his count, then come back and ask me again."' }],
            phases: [
              { id: 'phase_rhok_first' },
              { id: 'phase_rhok_ledger', requiresAllFlags: ['base_crates_short'], requiresNoneFlags: ['base_dax_resolved'],
                repeatPrompt: 'Rhok taps his stylus. "A second ledger, somewhere."',
                prompt: 'Rhok sets down the stylus. "Four crates unaccounted for. Someone keeps a second ledger. The secure crate in the depot would hold it, if you can open it."',
                choices: [
                  { text: 'Give Rhok the ledger from the secure crate.', morality: 8, loyalty: { republic: 5 }, requiresFlag: 'depot_secure_crate_opened',
                    result: 'Rhok reads for a long minute and smiles with all his teeth. "Dax keeps his license. He does not keep his margin." He counts out 700 credits. "Finder\'s fee."',
                    grants: { credits: 700, flags: ['base_dax_resolved', 'base_dax_exposed'] } },
                  { text: 'Tell Rhok the ledger was clean. Dax will remember.', morality: -8, loyalty: { underworld: 8 }, requiresFlag: 'depot_secure_crate_opened',
                    result: 'Rhok studies you the way he studies cargo. "Clean. Of course." By evening a chit arrives from Dax: 500 credits and a vial of coaxium, no note.',
                    grants: { credits: 500, items: ['kdy_coaxium_vial'], flags: ['base_dax_resolved', 'base_dax_covered'] } },
                  { text: 'Not yet.', morality: 0, loyalty: {}, defer: true,
                    result: '"The ledger will not get shorter. Neither will my patience."' },
                ] },
              { id: 'phase_rhok_after', requiresAllFlags: ['base_dax_resolved'],
                repeatPrompt: 'Rhok flicks a page. "Hunting, always hunting."',
                prompt: 'Rhok flicks a page. "Depot weights are in order now. Mostly. I will keep looking."',
                choices: [{ text: 'Wish him good hunting.', morality: 0, loyalty: {}, result: '"Hunting is all I do, friend."' }] },
            ],
          },
          { id: 'nautolan_hydro_veel', x: 36, y: 22, wander: 3, kind: 'nautolan_engineer', label: 'Nautolan Hydro-Engineer Veel',
            repeatPrompt: 'Veel adjusts a coolant tube at his throat. "Moisture is life, friend."',
            prompt: 'A Nautolan in a pressurized moisture suit, blue coolant tubes along his neck and head tresses, monitoring a coolant junction that feeds the shield generators.',
            choices: [{ text: 'Ask how the shield generators are holding.', morality: 0, loyalty: {}, result: '"Three nights now the load spikes at the exact same minute, then drops to nothing, like something tested our shield and found it not worth the trouble."' }] },
          { id: 'kuati_logistics_mara', x: 20, y: 13, wander: 2, kind: 'kuati_officer', label: 'Logistics Officer Mara Orsk', questNpc: true,
            repeatPrompt: 'Orsk consults her datapad. "Beacon log and the scan team. Bring me both."',
            prompt: 'A Kuati logistics officer in a slate blue tunic walks the Thoroughfare with a datapad and the stride of someone who never needed to run. "If you are here about skiff KDY-9, take a number."',
            choices: [{ text: 'Ask about the overdue skiff.', morality: 0, loyalty: {},
              result: '"KDY-9 cleared the last relay on schedule. After that, no telemetry. I filed the delay report, then another, then I started walking." She nods at the north alcove. "The depot relay mast logs every beacon. It is locked."',
              grants: { flags: ['bador_skiff_overdue', 'base_mara_tasked'] } }],
            phases: [
              { id: 'phase_mara_first' },
              { id: 'phase_mara_report', requiresAllFlags: ['base_skiff_telemetry', 'scan_team_fate_known'], requiresNoneFlags: ['base_skiff_resolved'],
                repeatPrompt: 'Orsk waits with a stamp in her hand.',
                prompt: 'Orsk reads the beacon log twice. "KDY-9 landed on its own power at grid seven seven nine, where your scan team went silent. Someone met it." She looks up. "Tell me what to file."',
                choices: [
                  { text: 'File the full report with Commander Vael.', morality: 8, loyalty: { republic: 5 },
                    result: '"Honest and slow. The Commander will hate it." She stamps a salvage writ into your hand. "The Directorate pays for truth, eventually." +600 credits now.',
                    grants: { credits: 600, items: ['base_salvage_writ'], flags: ['base_skiff_resolved', 'base_skiff_filed'] } },
                  { text: 'Sell the landing grid to a Syndicate buyer.', morality: -10, loyalty: { underworld: 8 },
                    result: 'Her face stays neutral, which says plenty. "I never saw the log." A courier chit for 1,100 credits reaches you before you leave the Thoroughfare.',
                    grants: { credits: 1100, flags: ['base_skiff_resolved', 'base_skiff_sold'] } },
                  { text: 'Bury it. The skiff stays lost.', morality: -3, loyalty: {},
                    result: '"Lost, then." She deletes a line and does not look at you. "I owe you nothing but silence and a coaxium vial." +300 credits from the petty fund.',
                    grants: { credits: 300, items: ['kdy_coaxium_vial'], flags: ['base_skiff_resolved', 'base_skiff_buried'] } },
                  { text: 'Not yet.', morality: 0, loyalty: {}, defer: true,
                    result: '"Do not take long. Rumor already has a schedule."' },
                ] },
              { id: 'phase_mara_after', requiresAllFlags: ['base_skiff_resolved'],
                repeatPrompt: 'Orsk sits on a crate, finally still.',
                prompt: 'Orsk has stopped walking. For the first time she sits on a crate. "KDY-9 is closed. Whatever is in the south, I am glad it is not on my schedule."',
                choices: [{ text: 'Ask what she will do now.', morality: 0, loyalty: {}, result: '"File something dull. Sleep. In that order."' }] },
            ],
          },
          { id: 'contractor_brek', x: 41, y: 15, wander: 3, kind: 'kdy_contractor', label: 'Contractor Brek',
            repeatPrompt: 'Brek flips down his visor and goes back to the cable run.',
            prompt: 'A heavy contractor in hazard orange synth rubber with a micro generator humming on his harness, welding helmet flipped up on a smudged and tired face.',
            choices: [{ text: 'Ask how the work is going.', morality: 0, loyalty: {}, result: '"Fine, until the lights flicker. Then everything stops and we all look south. Nobody says why."' }] },
        ],
        collectibles: [],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 1, 42, 26, 'floor');
          carveRect(g, 1, 1, 19, 4, 'wall');           // north west apron: the moonscape vista wall
          const hall = (x0, y0, x1, y1, gaps) => { carveRect(g, x0, y0, x1, y1, 'wall'); carveRect(g, x0 + 1, y0 + 1, x1 - 1, y1 - 1, 'floor'); gaps.forEach(([gx, gy]) => pt(g, gx, gy, 'floor')); };
          hall(20, 1, 29, 7, [[23, 7], [24, 7]]);      // medic tent
          hall(32, 1, 41, 7, [[35, 7], [36, 7]]);      // goods and materials depot
          hall(3, 21, 12, 26, [[7, 21], [8, 21]]);     // Trike's Robotics
          hall(21, 21, 33, 26, [[26, 21], [27, 21]]);  // weapons barracks
          carveRect(g, 27, 9, 39, 19, 'wall');         // main HQ shell
          [[27, 9], [28, 9], [27, 10], [27, 18], [27, 19], [28, 19]].forEach(([x, y]) => pt(g, x, y, 'floor'));
          carveRect(g, 27, 12, 28, 15, 'floor');       // HQ approach recess
          pt(g, 29, 13, 'door'); pt(g, 29, 14, 'door');
          pt(g, 43, 13, 'door'); pt(g, 43, 14, 'door');
          pt(g, 16, 27, 'door'); pt(g, 17, 27, 'door');
          carveShips(g, this.ships);
          pt(g, 6, 14, 'ship_ramp'); pt(g, 7, 14, 'ship_ramp');
          return g;
        },
      },

      bador_hq_interior: {
        id: 'bador_hq_interior', name: 'KDY Main HQ — Grand Vault Hall',
        subtitle: 'Bador Moon · Tactical Sanctum',
        paAnnouncements: [
          'Tactical Sanctum, orbital defense cone update in ten minutes.',
          'Duty officers, Yellow Alert logs are due to the Commander by the hour.',
          'Technicians, keep the sunken terminal banks clear of coolant.',
        ],
        width: 32, height: 22, spawnPos: { x: 2, y: 10 }, textureId: 'kuati',
        accent: '#D4AF37', accentGlow: 'rgba(212,175,55,0.28)', accentDim: '#5E4A1E',
        floorColor: '#4A566C', floorAlt: '#58667F', wallDark: '#1C2A39', wallLight: '#34496A',
        bg: 'radial-gradient(circle at 70% 50%, #16283A 0%, #060A12 75%)', ambient: 'datastream',
        decor: ['pillar', 'girder'],
        doors: [
          { x: 0, y: 10, targetZone: 'bador_main_base', targetPos: { x: 28, y: 13 }, label: 'Primary Airlock', cinematic: 'airlock_out' },
          { x: 0, y: 11, targetZone: 'bador_main_base', targetPos: { x: 28, y: 14 }, label: 'Primary Airlock', cinematic: 'airlock_out' },
        ],
        worldObjects: [
          { id: 'hq_window_wall', propArt: 'bvista_30x3', propVariant: 'hqwindow_a', x: 16, y: 2, once: false, iconKind: 'vista', label: 'Sanctum Glass Wall',
            description: "A panoramic glass wall in brass trimmed steel mullions, five tiles apart, over a dusk striped sky and a dark ridge line. A floodlight mast blinks red, and a pale dome crouches on the horizon." },
          { id: 'hq_rebreather_rack', propArt: 'rebreather_rack', x: 3, y: 4, label: 'Airlock Rebreather Rack',
            description: "Rebreather masks hang in racks along the airlock walls, collar units charging on blue indicator strips. A card reads: ATMOSPHERIC SEAL CONFIRMED. YOU MAY SPEAK." },
          { id: 'hall_banner_one', propArt: 'kuati_banner', x: 8, y: 3, label: 'House Kuat Banner',
            description: "A floor to ceiling slate blue banner with the gold wheel of Kuat and an alabaster border, hung between structural arches." },
          { id: 'hall_banner_two', propArt: 'kuati_banner', x: 13, y: 3, label: 'Directorate Banner',
            description: "The Directorate banner: the Kuat wheel crossed by a single gold keel line and the Guild motto, WE BUILD WHAT HOLDS THE GALAXY TOGETHER." },
          { id: 'hall_banner_three', propArt: 'kuati_banner', x: 18, y: 3, label: 'Ten Families Banner',
            description: "Ten small gold wheels in two rows beneath the great one, each Family named in Aurebesh. One wheel is polished brighter than the rest." },
          { id: 'hall_banner_four', propArt: 'kuati_banner', x: 13, y: 18, label: 'Founders Banner',
            description: "An older banner, its gold thread faded to brass, commemorating the first hull frame cast above Kuat." },
          { id: 'hall_banner_five', propArt: 'kuati_banner', x: 24, y: 3, label: 'Sanctum Banner',
            description: "A pair of banners frames the approach to the Sanctum dais: slate blue, gold wheel, one white keel line. Anyone between them walks toward the Commander." },
          { id: 'hall_banner_six', propArt: 'kuati_banner', x: 24, y: 18, label: 'Sanctum Banner',
            description: "The southern twin of the Sanctum banner. Behind it, a service hatch the Guild would like left closed." },
          { id: 'vault_floor_inlay', propArt: 'floor_inlay', x: 14, y: 10, label: 'Kuat Wheel Inlay',
            description: "Alabaster synthetic marble inlaid with a burnished gold Kuat wheel, twelve spokes around a sapphire hub. Gold pathways run toward the Sanctum dais." },
          { id: 'sunken_terminal_bank_north', propArt: 'terminal_bank', x: 9, y: 6, label: 'Sunken Terminal Bank',
            description: "A recessed control pit of dark transparisteel interfaces, blue vector screens and gold trimmed switches. Four stations log the same flickering trace under the heading EXTERNAL SOURCE, UNCLASSIFIED.",
            grantsFlag: 'hq_terminal_trace_seen', once: true },
          { id: 'sunken_terminal_bank_south', propArt: 'terminal_bank', x: 9, y: 15, label: 'Sunken Terminal Bank',
            description: "The southern bank handles base logistics: shield loads, airlock cycles, scrubber duty and the skiff manifest, still blinking OVERDUE in amber." },
          { id: 'vael_tactical_display', propArt: 'sanctum_holo', x: 26, y: 10, label: 'Tactical Holo-Table',
            description: "A circular holoprojector on an alabaster pedestal ringed in gold. Its cyan Bador turns twice a minute: craters, mineral veins, defense cones, amber patrol waypoints. Red markers cluster at Sector 4 and the Engine Wastes. Two marks read UNKNOWN STRUCTURE.",
            grantsFlag: 'bador_tactical_map_viewed', grantsCodex: 'codex-base-tactical-picture', once: true },
          { id: 'sanctum_viewport_south', propArt: 'panoramic_viewport', x: 28, y: 18, label: 'Sanctum Viewport',
            description: "The southern window looks over the yard: the barracks roof, Trike's chimney glowing amber, the Outskirts beyond the fence. Every sentry post is lit and none of them looks inward." },
          { id: 'vael_commendation_case', propArt: 'secure_locker', x: 29, y: 8, label: 'Commendation Case',
            description: "A locked steel case stamped with the gold Kuat wheel. Inside: a Directorate medal on a slate blue ribbon, and a bonus chit.",
            requiresFlag: 'base_vael_commended', invisibleLocked: true, once: true,
            grantsItem: 'base_directorate_commendation', grantsCredits: 500 },
        ],
        npcs: [
          { id: 'commander_vael', x: 26, y: 11, kind: 'kdy_commander', label: 'Commander Vael',
            questNpc: true, repeatable: true,
            requiresFlag: 'tanner_briefed',
            repeatPrompt: 'Vael keeps his gloved hands on the console. "Sector 4 and the Wastes."',
            prompt: "Vael studies the cyan holo map, bronze optic glinting. \"Tanner's contact. Bador is a Class C moon, population zero, officially. Yellow Alert since the scan team went dark. Sector 4 runs unauthorized shifts. Find them.\"",
            choices: [
              { text: 'What happened to the scan team?', morality: 0, loyalty: {},
                result: "\"Comms cut at grid seven seven nine. Two squads hold the perimeter, ordered not to advance without KDY clearance.\" He turns. \"That clearance is you.\"",
                grants: { flags: ['vael_briefed', 'ghost_keel_active'] } },
              { text: 'I want full access to your intel first.', morality: 5, loyalty: { republic: 5 },
                result: "\"Reasonable.\" He sends the tactical packet to your comlink. \"Do not trust the south readings. Something is jamming them.\"",
                grants: { flags: ['vael_briefed', 'vael_intel_shared', 'ghost_keel_active'] } },
              { text: 'Forty hours. Your team is likely dead.', morality: -10, loyalty: { underworld: 5 },
                result: "\"Probably. Which is why I need someone with a different risk calculation than an officer.\" He loads the grid data. \"Move carefully.\"",
                grants: { flags: ['vael_briefed', 'ghost_keel_active'] } },
              { text: 'Ask about the overdue supply skiff.', morality: 0, loyalty: {},
                result: "\"KDY-9. Alloys, coolant, and a sealed crate the Guild will not itemize for me. I want it found before anyone asks why.\"",
                grants: { flags: ['vael_briefed', 'vael_skiff_discussed', 'bador_skiff_overdue', 'ghost_keel_active'] } },
            ],
            phases: [
              { id: 'phase_vael_briefing' },
              { id: 'phase_vael_outskirts', requiresAllFlags: ['vael_briefed', 'outskirts_complete'],
                repeatPrompt: 'Vael nods at the Breach Point feed. "Held."',
                prompt: 'Vael has the Breach Point feed open. "Kaelin\'s line held. The KEF will not forget your face." A pause. "Neither will I."',
                choices: [{ text: 'Ask what is next.', morality: 0, loyalty: {},
                  result: '"The Mortar Fortress in the southeast. Varek Korr\'s guns cover the road south."' }] },
              { id: 'phase_vael_varek', requiresAllFlags: ['vael_briefed', 'varek_resolved'],
                repeatPrompt: 'Vael studies the green depot gate marker.',
                prompt: 'The tactical table shows the depot gate in green. "Korr is finished, one way or another. The road south is open, and so is the Wastes."',
                choices: [{ text: 'Ask about the Wastes.', morality: 0, loyalty: {},
                  result: '"Foundry 09 holds the last crew alive down there. Find them. Mind the heat."' }] },
              { id: 'phase_vael_sundown', requiresAllFlags: ['vael_briefed', 'sundown_complete'],
                repeatPrompt: 'Vael glances at the new green marker.',
                prompt: 'Vael\'s optic whirs at a new green marker. "Operation Sundown closed Outpost 7. Major Coop owes you a drink. I owe you a tab."',
                choices: [{ text: 'Ask about the Wastes gate.', morality: 0, loyalty: {},
                  result: '"Sealed at the center. Something at the Exhaust Cathedral still holds the doors."' }] },
              { id: 'phase_vael_manifold', requiresAllFlags: ['vael_briefed', 'manifest_restarted'],
                repeatPrompt: 'Vael has stopped pacing. He does not look relieved.',
                prompt: 'Vael has stopped pacing. "The Titan Manifold turns again. The Wastes doors are open and Zone Five is awake." He does not sound relieved.',
                choices: [{ text: 'Ask what is in Zone Five.', morality: 0, loyalty: {},
                  result: '"Nothing the survey ever recorded. Go carefully, and do not trust any map I gave you."' }] },
              { id: 'phase_vael_end_generic', requiresAllFlags: ['ghost_keel_complete'],
                repeatPrompt: 'Vael raises a glass to the empty table. "Closed."',
                prompt: 'The holo table is dark for the first time. "Whatever lay under Bador is gone from my map. I find I do not miss the alert."',
                choices: [
                  { text: 'Accept the commendation.', morality: 0, loyalty: {},
                    result: '"For services to the Directorate." The case on the console is yours. Somewhere above Kuat, a project that never existed stays that way.',
                    grants: { flags: ['base_vael_commended'], codex: ['codex-base-commend-kdy'] } },
                  { text: 'Ask what the Directorate will do now.', morality: 0, loyalty: {},
                    result: '"Rebuild the schedule. File the quiet. Survive the audit."' },
                ] },
              { id: 'phase_vael_end_kdy', requiresAllFlags: ['ghost_keel_complete', 'hypercore_to_kdy'],
                repeatPrompt: 'Vael straightens the Guild seal on his desk.',
                prompt: 'Vael has traded the holo map for a Guild seal. "The Hypercore is in Directorate hands. Tanner is pleased. I am told that is the best anyone gets."',
                choices: [
                  { text: 'Accept the commendation.', morality: 0, loyalty: {},
                    result: '"For services to the Directorate." The case on the console is yours. Somewhere above Kuat, a pocket dreadnought will never be mentioned again.',
                    grants: { flags: ['base_vael_commended', 'base_ending_kdy'], codex: ['codex-base-commend-kdy'] } },
                  { text: 'Ask what the Directorate will do with it.', morality: 0, loyalty: {},
                    result: '"Build. Quietly. It is what we are good at."' },
                ] },
              { id: 'phase_vael_end_destroyed', requiresAllFlags: ['ghost_keel_complete', 'hypercore_destroyed'],
                repeatPrompt: 'Vael watches the dark table in silence.',
                prompt: 'Vael has the demolition log open, silent. "The Republic protocol held. No core, no keel. I filed it as a training accident." His optic dims.',
                choices: [
                  { text: 'Accept the commendation.', morality: 0, loyalty: {},
                    result: '"For services the Directorate will deny." The case on the console is yours. On the table, two UNKNOWN STRUCTURE marks wink out.',
                    grants: { flags: ['base_vael_commended', 'base_ending_destroyed'], codex: ['codex-base-commend-destroyed'] } },
                  { text: 'Ask if he regrets it.', morality: 0, loyalty: {},
                    result: '"A weapon nobody could use or sell. No. Ask again in ten years."' },
                ] },
              { id: 'phase_vael_end_syndicate', requiresAllFlags: ['ghost_keel_complete', 'hypercore_to_syndicate'],
                repeatPrompt: 'Vael looks at the door, then at your collar.',
                prompt: 'Vael looks at the Syndicate seal on your collar, then at the door. "Officially I never saw it fly. Unofficially, I salute it."',
                choices: [
                  { text: 'Accept the commendation.', morality: 0, loyalty: {},
                    result: '"For services I cannot name." The case on the console is yours. Outside, a pocket dreadnought idles in the dark under another flag.',
                    grants: { flags: ['base_vael_commended', 'base_ending_syndicate'], codex: ['codex-base-commend-syndicate'] } },
                  { text: 'Ask if KDY will hunt it.', morality: 0, loyalty: {},
                    result: '"They will try. Tanner has a long reach and a short temper. Keep it moving."' },
                ] },
              { id: 'phase_vael_tags', requiresAllFlags: ['vael_briefed', 'scan_team_fate_known'], requiresNoneFlags: ['base_vael_tags_received'],
                repeatPrompt: 'Vael keeps his eyes on the seven names.',
                prompt: 'Vael sees the tape wrapped tags in your hand and sets down his stylus. "Seven. Say it plainly. Where, and how?"',
                choices: [
                  { text: 'Hand over the seven tags.', morality: 5, loyalty: { republic: 5 }, requires: { item: 'wst_scan_team_tags' },
                    result: '"Disabled, not destroyed. Warned off." He reads each name aloud and locks the tags in his desk. "They will get a letter, a pension and a lie kinder than this." +500 credits recovery bounty.',
                    grants: { credits: 500, flags: ['base_vael_tags_received'] } },
                  { text: 'Not yet.', morality: 0, loyalty: {}, defer: true,
                    result: '"I will be here. I am always here."' },
                ] },
            ],
          },
          { id: 'hq_duty_officer_teln', x: 14, y: 14, wander: 4, kind: 'kuati_officer', label: 'Duty Officer Teln',
            repeatPrompt: 'Teln walks on without turning his head. "The Commander is in the Sanctum."',
            prompt: 'A Kuati duty officer in fitted slate blue with a bronze KDY medallion crosses the marble, datapad under one arm, footfalls echoing in the vault.',
            choices: [{ text: 'Ask where the Commander is.', morality: 0, loyalty: {}, result: '"East end, on the dais. Do not cross the gold line until he looks at you."' }] },
          { id: 'hq_clerk_ysmin', x: 12, y: 17, wander: 3, kind: 'kuati_officer', label: 'Records Officer Ysmin',
            repeatPrompt: 'Ysmin does not look up from her cataloguing.',
            prompt: 'A records officer with twin silver and sapphire rank cylinders files holo slates into a sunken archive drawer, one at a time.',
            choices: [{ text: 'Ask what she is filing.', morality: 0, loyalty: {}, result: '"Alert logs, all the same entry. I have started numbering them out of grim curiosity."' }] },
          { id: 'hq_ge3_protocol', x: 18, y: 8, wander: 5, kind: 'ge3_protocol', label: 'GE3 Protocol Droid Gee-Seven',
            repeatPrompt: 'Gee-Seven inclines its head. "How may I assist the Directorate?"',
            prompt: 'A gleaming silver GE3 with gold rimmed joints walks the Vault Hall, hands folded, translating for visiting contractors.',
            choices: [{ text: 'Ask it how many languages it speaks.', morality: 0, loyalty: {}, result: '"Over six million, sir, though in this hall chiefly Kuati, Basic and diplomatic silence."' }] },
          { id: 'hq_astromech_r7', x: 20, y: 15, wander: 4, kind: 'kuati_astromech', label: 'Kuati Astromech R7-K4',
            repeatPrompt: 'R7-K4 trills at you and rolls on.',
            prompt: 'An astromech in Kuati livery, pearlescent dome and sapphire panels, rolling soft circuits of the hall with a data cable trailing from its arm.',
            choices: [{ text: 'Say hello.', morality: 0, loyalty: {}, result: 'It beeps a warm triplet and projects a tiny gold Kuat wheel in the air before rolling away.' }] },
        ],
        collectibles: [{ id: 'bador_base_intel', x: 29, y: 15, label: 'KDY Survey Report', reward: 200 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 2, 30, 19, 'floor');
          // inner airlock partition: the Primary Airlock chamber, with the inner threshold gap
          carveRect(g, 5, 2, 5, 19, 'wall');
          carveRect(g, 5, 9, 5, 12, 'floor');
          [7, 12, 16, 20, 24].forEach((x) => { pt(g, x, 5, 'wall'); pt(g, x, 16, 'wall'); });   // nave arcade pillars
          pt(g, 0, 10, 'door'); pt(g, 0, 11, 'door');
          return g;
        },
      },

      bador_manufacturing: {
        id: 'bador_manufacturing', name: 'KDY Manufacturing Facilities, Sector 4',
        subtitle: 'Bador Moon · Fabrication Works · Restricted Zone',
        width: 44, height: 30, spawnPos: { x: 2, y: 14 }, textureId: 'ferrowake',
        accent: '#D4762C', accentGlow: 'rgba(212,118,44,0.28)', accentDim: '#6A3010',
        floorColor: '#251A0E', floorAlt: '#2E2010', wallDark: '#0E0A06', wallLight: '#1A1208',
        bg: 'radial-gradient(circle at 50% 12%, #2A1408 0%, #140C06 50%, #070403 100%)', ambient: 'foundry',
        decor: ['pipe', 'girder', 'slag'],
        paAnnouncements: [
          'Attention Sector 4 personnel. Shift quota stands at one hundred and fourteen percent. Surplus is a privilege.',
          'Foundry Four will pour in five minutes. Stand clear of the spout.',
          'Reminder: the east gate is closed to day crew after the shift horn. Thank you for your cooperation.',
          'The Slag Line freight lock remains dead. Do not attempt to power the tunnel.',
        ],
        doors: [
          { x: 0, y: 14, targetZone: 'bador_main_base', targetPos: { x: 42, y: 13 }, label: 'Main Base' },
          { x: 0, y: 15, targetZone: 'bador_main_base', targetPos: { x: 42, y: 14 }, label: 'Main Base' },
          { x: 43, y: 16, targetZone: 'engine_wastes', targetPos: { x: 1, y: 10 }, label: 'Slag Line Freight Tunnel', requiresFlag: 'manifest_restarted', lockedMessage: 'The Slag Line freight lock is sealed. The tunnel beyond runs to the Engine Wastes and stays dead until the Titan Manifold is restarted.' },
          { x: 43, y: 17, targetZone: 'engine_wastes', targetPos: { x: 1, y: 11 }, label: 'Slag Line Freight Tunnel', requiresFlag: 'manifest_restarted', lockedMessage: 'The Slag Line freight lock is sealed. The tunnel beyond runs to the Engine Wastes and stays dead until the Titan Manifold is restarted.' },
        ],
        worldObjects: [
          // ---- vista and signage ----
          { id: 'mfg_vista', propArt: 'bvista_44x5', propVariant: 'foundry_a', x: 22, y: 5, once: false, iconKind: 'vista', label: 'Sector 4 Foundry Skyline',
            description: 'Foundry stacks and cooling towers rise into a smoke dark sky, gantries slung between them, an orange slag glow along the ground. Sector 4 never stops pouring.', grantsCodex: 'codex-mfg-sector4' },
          { id: 'mfg_sign_main', propArt: 'facility_sign', propVariant: 'mfg', x: 4, y: 18, once: false, iconKind: 'crate', label: 'Fabrication Works Sign',
            description: 'SECTOR 4 FABRICATION, SHIFT QUOTA 114 PCT. Someone has underlined the 114 in red grease pencil and added a question mark.' },
          { id: 'mfg_sign_slag', propArt: 'facility_sign', propVariant: 'slag', x: 41, y: 15, once: false, iconKind: 'crate', label: 'Slag Line Freight Lock Sign',
            description: 'SLAG LINE FREIGHT LOCK, TUNNEL DEAD. The sign is newer than the lock. Whoever hung it did not expect the lock to open again.' },
          { id: 'mfg_slag_gate', propArt: 'security_gate', x: 40, y: 18, once: false, iconKind: 'crate', label: 'Slag Line Freight Lock',
            description: 'A heavy freight lock at the east end, hazard striped and welded shut. Its status light is red. The tunnel beyond stays dead until the Titan Manifold turns over.' },
          // ---- Rhea\'s Gantry ----
          { id: 'mfg_quota_board', propArt: 'board', propVariant: 'mfg_quota', x: 3, y: 7, once: false, iconKind: 'crate', label: 'Shift Quota Board',
            description: 'Two hundred and twelve names on the day roll, three hundred and forty badge swipes logged. The difference is circled in red grease pencil.', grantsCodex: 'codex-mfg-night-shift' },
          { id: 'mfg_gantry', propArt: 'catwalk_junction', x: 3, y: 10, once: false, iconKind: 'crate', label: 'Line Gantry Junction',
            description: 'A catwalk junction over the head of the line, rails worn bright by boots, a scanner cradle bolted to the post.' },
          { id: 'mfg_gantry_bench', propArt: 'workbench', propVariant: 'arms', x: 7, y: 10, once: false, iconKind: 'crate', label: 'Coupling Bench',
            description: 'A bench strewn with gantry couplings, torque wrenches and a cold flask. One coupling is stamped with a section number and nothing else.' },
          { id: 'mfg_gantry_vent', propArt: 'steam_vent', x: 9, y: 11, once: false, iconKind: 'crate', label: 'Gantry Steam Vent',
            description: 'A floor vent breathing hot steam across the catwalk. The paint around it has blistered off.' },
          { id: 'mfg_gantry_drums', propArt: 'drum_array', x: 2, y: 12, once: false, iconKind: 'crate', label: 'Quench Oil Drums',
            description: 'A rack of quench oil drums stencilled with the Kuat wheel. One has been used as a seat.' },
          // ---- Production Line Hall ----
          { id: 'mfg_belt_1', propArt: 'mfg_conveyor', propVariant: 'plates', x: 14, y: 7, once: false, iconKind: 'crate', label: 'Line 1 Conveyor',
            description: 'A conveyor belt carrying stacked hull plate, amber chevrons marching along the bed. Class C plate, stamped and counted.' },
          { id: 'mfg_belt_2', propArt: 'mfg_conveyor', propVariant: 'ingots', x: 18, y: 7, once: false, iconKind: 'crate', label: 'Line 1 Ingot Feed',
            description: 'The same belt, now carrying three glowing ingots straight from the foundry. The heat shimmers a full tile above them.' },
          { id: 'mfg_belt_3', propArt: 'mfg_conveyor', propVariant: 'plates', x: 22, y: 7, once: false, iconKind: 'crate', label: 'Line 2 Conveyor',
            description: 'A second belt of stacked plate. The plate here is thicker than anything a cruiser needs.' },
          { id: 'mfg_belt_4', propArt: 'mfg_conveyor', propVariant: 'ingots', x: 26, y: 7, once: false, iconKind: 'crate', label: 'Line 2 Ingot Feed',
            description: 'More hot ingots, running toward the cradles. Whatever they feed, it is hungry.' },
          { id: 'mfg_cradle_a', propArt: 'mfg_cradle', x: 16, y: 12, once: false, iconKind: 'crate', label: 'Assembly Cradle One',
            description: 'A hazard striped cradle holds a ribbed hull section, half plated, a welding arc flickering at one rib. The plate reads SECTION 11, NO REGISTRY. It is far too large for any ship this yard may build.', grantsCodex: 'codex-mfg-section-eleven' },
          { id: 'mfg_cradle_b', propArt: 'mfg_cradle', propVariant: 'b', x: 28, y: 12, once: false, iconKind: 'crate', label: 'Assembly Cradle Two',
            description: 'A second cradle, the section younger than its twin. Only a few plates cling to the ribs. The plate reads SECTION 12, NO REGISTRY.' },
          { id: 'mag_lev_crane', propArt: 'maglev_crane', x: 22, y: 10, label: 'Mag-Lev Cargo Crane',
            description: 'A five ton crane magnetized to the ceiling rail. The routing manifest reads: PERSONAL CONSIGNMENT.' },
          { id: 'mfg_shift_board', propArt: 'board', propVariant: 'mfg_shift', x: 22, y: 12, once: false, iconKind: 'crate', label: 'Production Schedule',
            description: 'Lines one and two list Class C plate. Cradle one lists SECTION 11 and cradle two SECTION 12, the last row circled in red.' },
          { id: 'assembly_line_terminal', propArt: 'console', propVariant: 'assembly', x: 19, y: 11, label: 'Assembly Line Override Terminal',
            description: 'An unauthorized modification routes assembly output to an off manifest storage bay. The override code is not in the KDY standard library.',
            lockedMessage: 'The override is locked behind Ghost Keel clearance. Commander Vael at Bador HQ can brief you.',
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 3 }, requiresFlag: 'ghost_keel_active',
            once: true, grantsFlag: 'assembly_override_accessed', grantsItem: 'corrupt_manifest' },
          { id: 'assembly_line_terminal_done', propArt: 'console', propVariant: 'assembly_done', x: 19, y: 11, once: false, iconKind: 'terminal', label: 'Assembly Override (Active)',
            description: 'The override is live. Somewhere east, a false panel has slid open on a room that was never on the floor plan.', requiresFlag: 'assembly_override_accessed', invisibleLocked: true },
          { id: 'mfg_offcuts', propArt: 'scrap_bin', x: 12, y: 12, once: false, iconKind: 'crate', label: 'Offcut Bin',
            description: 'A bin of cut plate and trimmings. Every offcut carries the same retired stamp.' },
          // ---- Foundry Floor ----
          { id: 'mfg_piston', propArt: 'piston_tower', x: 35, y: 8, once: false, iconKind: 'crate', label: 'Hammer Piston Tower',
            description: 'A copper piston tower hammering a plate on the anvil below. Two orange vent lamps glow at its waist.' },
          { id: 'mfg_boiler', propArt: 'boiler_hull', x: 40, y: 8, once: false, iconKind: 'crate', label: 'Foundry Boiler',
            description: 'A copper boiler hull with a glowing furnace eye and a sign reading DEAD BOILER. It is warm. Someone has been running it anyway.' },
          { id: 'mfg_forge', propArt: 'mfg_forge', x: 36, y: 12, once: false, iconKind: 'crate', label: 'Foundry Four Crucible',
            description: 'A furnace with a molten mouth, a pour spout and a chimney. The plate reads FOUNDRY 4. White hot iron runs from the spout into a trough.' },
          { id: 'mfg_valve', propArt: 'valve_wheel', x: 41, y: 11, once: false, iconKind: 'crate', label: 'Coolant Valve Wheel',
            description: 'A brass valve wheel on a copper stand, three amber lamps blinking. It controls coolant to the crucible and sticks.' },
          { id: 'mfg_steam', propArt: 'steam_vent_pipe', x: 33, y: 11, once: false, iconKind: 'crate', label: 'Relief Stack',
            description: 'A relief stack with a red warning lamp, venting steam in three slow plumes.' },
          // ---- Thoroughfare ----
          { id: 'mfg_arch', propArt: 'scanner_arch', propVariant: 'customs', x: 12, y: 17, once: false, iconKind: 'crate', label: 'Entry Scanner Arch',
            description: 'A scanner arch at the thoroughfare mouth. Its log counts day crew in. Nothing is counting anyone out.' },
          { id: 'mfg_vent_a', propArt: 'steam_vent', x: 6, y: 16, once: false, iconKind: 'crate', label: 'Thoroughfare Steam Vent',
            description: 'A floor vent breathing steam across the lane. The grating is hot enough to feel through boots.' },
          { id: 'mfg_grate', propArt: 'floor_grate', x: 18, y: 16, once: false, iconKind: 'crate', label: 'Slag Drain Grate',
            description: 'A drain grate with a dull orange glow underneath. Cooled slag has crusted around the bars.' },
          { id: 'mfg_post', propArt: 'guard_post', x: 24, y: 18, once: false, iconKind: 'crate', label: 'Security Post',
            description: 'A KDY security post with a rack of rebreathers and a log board. Two cold cups sit on the sill.' },
          { id: 'mfg_pipe', propArt: 'leaking_pipe', propVariant: 'coolant', x: 33, y: 18, once: false, iconKind: 'crate', label: 'Leaking Coolant Line',
            description: 'A coolant line weeping a pale blue film across the floor. Nobody has patched it in weeks.' },
          // ---- Cargo lift and dock ----
          { id: 'mfg_lift', propArt: 'cargo_lift', x: 3, y: 22, once: false, iconKind: 'crate', label: 'Freight Lift Two',
            description: 'A freight lift with a worn deck and a hazard rail. The call panel shows a stop labelled B, on no floor.' },
          { id: 'mfg_lift_log', propArt: 'board', propVariant: 'mfg_lift', x: 7, y: 22, once: false, iconKind: 'crate', label: 'Cargo Lift Log',
            description: 'The lift log lists two night cycles with a sealed pallet each, at 02:10 and 03:40. The stop is B. There is no floor B.' },
          { id: 'mfg_containers', propArt: 'container_stack', x: 10, y: 22, once: false, iconKind: 'crate', label: 'Outbound Containers',
            description: 'A stack of outbound containers, plated and sealed. The manifest tags say PERSONAL CONSIGNMENT.' },
          { id: 'mfg_crane_arm', propArt: 'crane_arm', x: 6, y: 27, once: false, iconKind: 'crate', label: 'Dock Crane',
            description: 'A dock crane arm parked over the loading bay, hook down, a pallet still hanging from the chains.' },
          { id: 'mfg_dock_crates', propArt: 'crate_stack', propVariant: 'agri', x: 2, y: 26, once: false, iconKind: 'crate', label: 'Ration Crates',
            description: 'Ration crates stacked three high, lids pried open and re-sealed. The ration count does not match the day roll.' },
          { id: 'mfg_dock_drums', propArt: 'drum_array', x: 11, y: 26, once: false, iconKind: 'crate', label: 'Fuel Drums',
            description: 'A rack of fuel drums with fresh hazard tape. They are lighter than they should be.' },
          // ---- Engineering Bay ----
          { id: 'mfg_eng_bench', propArt: 'workbench', propVariant: 'slicing', x: 16, y: 22, once: false, iconKind: 'crate', label: 'Engineering Bench',
            description: 'A slicing bench strewn with data cores and a half stripped drive housing. The housing is much larger than any shuttle needs.' },
          { id: 'mfg_eng_servers', propArt: 'server_stack', propVariant: 'republic', x: 21, y: 23, once: false, iconKind: 'crate', label: 'Drawing Servers',
            description: 'A stack of drawing servers humming under their covers. The cooling vents are blocked with rags.' },
          { id: 'mfg_eng_junction', propArt: 'junction_box', propVariant: 'vent', x: 19, y: 22, once: false, iconKind: 'crate', label: 'Power Junction',
            description: 'A junction box with its door taped open and a handwritten note: DO NOT RESET, THE ARCHIVE WILL NOT COME BACK.' },
          { id: 'mfg_eng_cabinet', propArt: 'archive_cabinet', propVariant: 'ledger', x: 15, y: 26, once: false, iconKind: 'crate', label: 'Drawing Cabinet',
            description: 'A cabinet of rolled drawings. Every file is signed in the same hand, a retired signature Vorn will not read aloud.' },
          { id: 'mfg_eng_tap', propArt: 'power_tap', x: 18, y: 28, once: false, iconKind: 'crate', label: 'Bay Power Tap',
            description: 'A heavy power tap feeding the drawing servers directly from the foundry bus.' },
          { id: 'mfg_eng_terminal', propArt: 'console', propVariant: 'mfg_eng', x: 22, y: 27, label: 'Engineering Archive Terminal',
            description: 'Fourteen assembly sequence files, one frame, no registry. Every drawing is stamped with the same retired signature. The last view was logged at night.',
            requiresFlag: 'vorn_consulted', lockedMessage: 'The archive wants a name and a reason. Lead Engineer Vorn might supply both.',
            once: true, grantsFlag: 'mfg_sequence_read', grantsItem: 'mfg_assembly_files', grantsCodex: 'codex-mfg-pocket-frame' },
          { id: 'mfg_eng_terminal_done', propArt: 'console', propVariant: 'mfg_eng_done', x: 22, y: 27, once: false, iconKind: 'terminal', label: 'Engineering Archive (Copied)',
            description: 'The archive is copied. The view log shows nothing odd, which is the oddest thing in the room.', requiresFlag: 'mfg_sequence_read', invisibleLocked: true },
          // ---- Overseer's glass office ----
          { id: 'mfg_office_glass', propArt: 'mfg_glass', x: 28, y: 20, once: false, iconKind: 'crate', label: "Overseer's Glass Office",
            description: 'A glass fronted office over the thoroughfare, amber blinds half drawn, a sign reading OVERSEER SECTOR 4 and APPOINTMENTS ONLY. Two brass counters flank the door.' },
          { id: 'mfg_office_desk', propArt: 'field_desk', x: 29, y: 22, once: false, iconKind: 'crate', label: "Krax's Desk",
            description: 'A broad desk made for four arms, covered in manifest tablets, a quota chart and a half eaten ration bar.' },
          { id: 'mfg_office_console', propArt: 'console', propVariant: 'mfg_office', x: 26, y: 24, once: false, iconKind: 'terminal', label: 'Overseer Routing Console',
            description: 'Quota stands at 114 percent, surplus routed, night crew 128, authorized K. Krax. The console is not locked. Krax does not think anyone reads it.' },
          { id: 'mfg_office_monitors', propArt: 'monitor_wall', x: 28, y: 28, once: false, iconKind: 'terminal', label: 'Floor Monitors',
            description: 'A wall of camera feeds of the foundry, cradles and thoroughfare. The feed for the east end has been swapped for a loop.' },
          { id: 'mfg_office_archive', propArt: 'archive_cabinet', propVariant: 'dossier', x: 32, y: 22, once: false, iconKind: 'crate', label: 'Personnel Cabinet',
            description: 'A cabinet of personnel files. The night crew drawer is empty and its lock has been drilled out.' },
          { id: 'krax_manifest_safe', propArt: 'crate_stack', propVariant: 'lockbox', x: 32, y: 27, label: "Overseer's Manifest Safe",
            description: 'A heavy durasteel lockbox welded to the floor. The serial number has been removed. Inside, stolen reactor coils and a private ledger.',
            once: true, requiresFlag: 'krax_revealed', lockedMessage: 'The lockbox is welded shut and Krax watches it. He has to be made to open up first.',
            grantsFlag: 'overseer_safe_opened', grantsItem: 'ion_charged_coils', grantsCredits: 600 },
          { id: 'mfg_krax_wreck', propArt: 'scrap_bin', x: 27, y: 26, once: true, iconKind: 'crate', label: "Krax's Loader Frame Wreckage",
            description: 'The smoking remains of a four clamp loader frame, hydraulic fluid pooling across the office floor. The harness is still intact. It still hums.',
            requiresFlag: 'krax_defeated', invisibleLocked: true, grantsItem: 'mfg_four_arm_harness', grantsCredits: 500, grantsCodex: 'codex-mfg-krax-fall' },
          // ---- Storage Bay B ----
          { id: 'mfg_bay_medic', propArt: 'medic_crate', x: 35, y: 22, once: false, iconKind: 'crate', label: 'Sealed Med Crate',
            description: 'A medical crate with a fresh seal and a stencilled KDY number. It has been stationed here far longer than the room has existed on paper.' },
          { id: 'mfg_bay_cargo', propArt: 'cargo_container', x: 41, y: 22, once: false, iconKind: 'crate', label: 'Pallet Container',
            description: 'A pallet container marked ROUTINE STORES. The seal is new. The stamp is the retired one.' },
          { id: 'mfg_bay_panel', propArt: 'crate_stack', propVariant: 'false_panel', x: 38, y: 24, once: false, iconKind: 'crate', label: 'False Wall Panel',
            description: 'A wall panel that does not match the others. The seams are too clean and the rivets are fresh, as if the wall were put up to be forgotten.',
            requiresNoneFlags: ['assembly_override_accessed'] },
          { id: 'mfg_bay_vault', propArt: 'vault_door', x: 38, y: 24, once: false, iconKind: 'crate', label: 'Storage Bay B Door',
            description: 'The false panel has slid aside on a vault door, its bolts drawn. Beyond it lies Storage Bay B, a room that is on no floor plan.',
            requiresFlag: 'assembly_override_accessed', invisibleLocked: true },
          { id: 'mfg_keel', propArt: 'mfg_keel', x: 38, y: 27, once: false, iconKind: 'crate', label: 'Forged Keel Plate',
            description: 'A forged keel plate on a hazard skid, stencilled GK-01 KEEL in red and NO COMMISSION NUMBER below. Five clamps hold it. Its lamps are lit, as if it were waiting.',
            requiresFlag: 'assembly_override_accessed', invisibleLocked: true, grantsCodex: 'codex-mfg-bay-b' },
          { id: 'mfg_bay_core', propArt: 'console', propVariant: 'mfg_bay', x: 41, y: 28, label: 'Bay B Manifest Core',
            description: 'The core logs every shipment out of Storage Bay B. Two hundred and twelve consignments, no manifest, all routed to grid nine one. The lock is military grade.',
            requiresFlag: 'assembly_override_accessed', invisibleLocked: true,
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 5 },
            once: true, grantsFlag: 'mfg_bay_proof', grantsItem: 'mfg_bay_manifest' },
          { id: 'mfg_bay_core_done', propArt: 'console', propVariant: 'mfg_bay_done', x: 41, y: 28, once: false, iconKind: 'terminal', label: 'Bay B Core (Extracted)',
            description: 'The ledger is yours. Two hundred and twelve entries, every one approved by Overseer Krax. Time to ask him about it.', requiresFlag: 'mfg_bay_proof', invisibleLocked: true },
        ],
        npcs: [
          { id: 'assembly_tech_rhea', x: 5, y: 8, wander: 2, kind: 'mechanic', label: 'Assembly Tech Rhea',
            repeatPrompt: 'Rhea ducks under the gantry and keeps working.',
            phases: [
              {
                id: 'phase_r1',
                prompt: 'Rhea ducks under the gantry, scanner chirping. "Half the line is dark. Raiders cut the fences, parts go missing. Mind your head."',
                repeatPrompt: 'Rhea ducks under the gantry and keeps working.',
                choices: [
                  { text: 'Ask about the assembly line.', morality: 0, loyalty: {}, result: '"Plate in, plate out, quota up. Day shift I understand." She glances at the east gate. "Night shift I only count."' },
                  { text: 'Ask what she counts after the horn.', morality: 3, loyalty: { republic: 3 }, result: '"Badges, no faces. Sealed suits, loaders not on my register." She thumbs a slate into your hand. "My count. Do not get me fired."', grants: { flags: ['mfg_night_shift_known'], items: ['mfg_night_roster'], codex: ['codex-mfg-night-shift'] } },
                ],
              },
              {
                id: 'phase_r2',
                requiresAllFlags: ['mfg_night_shift_known'],
                prompt: 'Rhea keeps her eyes on the scanner. "If the night crew ever answers for this, remember I only counted."',
                repeatPrompt: 'Rhea does not look up. "I only counted."',
                choices: [
                  { text: 'Promise to keep her name out of it.', morality: 2, loyalty: {}, result: '"Good." The scanner chirps. "Section eleven was bolted to its cradle three weeks ago. Section twelve is next."' },
                ],
              },
              {
                id: 'phase_r_expose',
                requiresAllFlags: ['mfg_end_expose'],
                prompt: 'Rhea waves at the dark cradles. "Vael\'s marines took the night badges and nobody was hurt. The line is quiet. I do not know what to do with quiet."',
                repeatPrompt: 'Rhea scans a coupling that no longer matters.',
                choices: [
                  { text: 'Tell her the line can finally be honest.', morality: 3, loyalty: { republic: 2 }, result: '"Honest is slow." She almost smiles. "I will take slow."' },
                ],
              },
              {
                id: 'phase_r_amnesty',
                requiresAllFlags: ['mfg_workers_cleared'],
                prompt: 'Rhea meets your eyes for the first time. "They questioned the night crew and let them go. My count did that. I owe you."',
                repeatPrompt: 'Rhea nods at you across the line.',
                choices: [
                  { text: 'Accept the credit chip she offers.', morality: 2, loyalty: { republic: 4 }, result: 'She presses a chip into your palm. "Section pay. Spend it somewhere that is not here." Three hundred credits, honestly earned.', grants: { credits: 300 } },
                ],
              },
              {
                id: 'phase_r_blackmail',
                requiresAllFlags: ['mfg_end_blackmail'],
                prompt: 'Rhea works with her head down. "Krax pays the line bonus again. I do not ask where from."',
                repeatPrompt: 'Rhea does not ask where the bonus came from.',
                choices: [
                  { text: 'Tell her not to.', morality: -3, loyalty: { underworld: 2 }, result: 'She flinches, then nods. The scanner chirps. "Section twelve closes tonight. Someone will want to know."' },
                ],
              },
              {
                id: 'phase_r_fight',
                requiresAllFlags: ['mfg_end_fight'],
                prompt: 'The foundry hums quieter. Rhea sits on a cold cradle step. "Krax is gone. The night crew did not come back. I do not miss them."',
                repeatPrompt: 'Rhea sits on the cradle step and watches the dark line.',
                choices: [
                  { text: 'Ask what happens to the line.', morality: 0, loyalty: {}, result: '"Quota will drop to what it should be." She shrugs. "Or somebody will fill the seat."' },
                ],
              },
            ],
          },
          { id: 'overseer_krax', x: 29, y: 24, kind: 'besalisk_boss', label: 'Overseer Krax',
            questNpc: true,
            requiresFlag: 'vael_briefed',
            hideAfterFlags: ['krax_exposed', 'krax_defeated'],
            repeatable: true,
            repeatPrompt: 'Krax\'s four arms keep moving across different controls. "Inspections by appointment, not by ambush."',
            phases: [
              {
                id: 'phase_k1',
                prompt: 'Four arms, four tasks, no eye contact. "Freelancer. I was told to expect an inspection liaison." A beat too long. "Sector 4 is on schedule. Thermal variance explains your anomalies."',
                repeatPrompt: 'Krax\'s four arms keep moving across different controls. "Inspections by appointment, not by ambush."',
                choices: [
                  { text: 'Ask about the off manifest routing code.', morality: 0, loyalty: {}, result: 'Two arms freeze, two slow. "Routing variance. Standard redundancy." He meets your eyes. "Probe further and you will need clearance from Operations Level Six."', grants: { flags: ['krax_pressure_applied'] } },
                  { text: 'Tell him you have Level Six clearance.', morality: -10, loyalty: { underworld: 8 }, result: '"Then you know what this line produces." He lowers his voice. "Come back when your handlers confirm."', grants: { flags: ['krax_suspicious', 'krax_level6_bluff'] } },
                  { text: 'Tell him the scan team never came back.', morality: 5, loyalty: { republic: 5 }, result: '"I know. That was not my order." He sets the tablet down. "Come back without an audience."', grants: { flags: ['krax_revealed'] } },
                  { text: 'Leave him to his work.', morality: 0, loyalty: {}, result: 'Krax does not look up. Four arms keep working. The line behind you does not slow.' },
                ],
              },
              {
                id: 'phase_k2',
                requiresAllFlags: ['mfg_bay_proof'],
                requiresNoneFlags: ['mfg_resolved'],
                prompt: 'You lay the Bay B ledger on his desk. Four arms stop at once. "Who else has seen this?" His voice is very quiet.',
                repeatPrompt: 'Krax waits, arms folded, for you to name a price or a charge.',
                choices: [
                  { text: '[Expose] Hand the ledger to Commander Vael.', morality: 12, loyalty: { republic: 10 }, requires: { item: 'mfg_bay_manifest' }, result: 'You ping Vael on the desk comm. Marines reach the door before Krax finishes his sentence. He goes quietly, cursing the Senate. Joint seal and 1500 credits are yours.', grants: { flags: ['mfg_resolved', 'mfg_end_expose', 'mfg_krax_gone', 'krax_exposed', 'krax_revealed'], items: ['mfg_inspector_seal'], credits: 1500, codex: ['codex-mfg-arrest'] } },
                  { text: '[Expose] Ledger and Rhea\'s roster. The night crew testifies.', morality: 18, loyalty: { republic: 14 }, requires: { item: 'mfg_night_roster' }, result: 'Vael takes Krax and the roster. The night crew is questioned, not charged. Rhea will hear before shift change. Joint seal and 1800 credits.', grants: { flags: ['mfg_resolved', 'mfg_end_expose', 'mfg_krax_gone', 'mfg_workers_cleared', 'krax_exposed', 'krax_revealed'], items: ['mfg_inspector_seal'], credits: 1800, codex: ['codex-mfg-arrest'] } },
                  { text: '[Blackmail] Name your price, Krax. The Syndicate is listening.', morality: -15, loyalty: { underworld: 15 }, requires: { item: 'mfg_bay_manifest' }, result: 'Krax agrees to a standing tithe. +2000 credits now and +150 credits each Advance Time at the War Table. He signs without reading.', grants: { flags: ['mfg_resolved', 'mfg_end_blackmail', 'mfg_tithe', 'krax_revealed'], items: ['mfg_tithe_ledger'], credits: 2000, codex: ['codex-mfg-tithe'] } },
                  { text: '[Blackmail] Set his own stolen coils beside the ledger.', morality: -18, loyalty: { underworld: 18 }, requires: { item: 'ion_charged_coils' }, result: 'Krax goes grey. The tithe doubles: +3500 credits now and +250 credits each Advance Time at the War Table. He does not argue.', grants: { flags: ['mfg_resolved', 'mfg_end_blackmail', 'mfg_tithe', 'mfg_tithe_rich', 'krax_revealed'], items: ['mfg_tithe_ledger'], credits: 3500, codex: ['codex-mfg-tithe'] } },
                  { text: '[Fight] Tell him no one is leaving this office.', morality: -5, loyalty: {}, result: 'Krax slams the pit alarm. The office floor opens and loader enforcers climb out, clamps open.', triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'mfg_enforcers', flavorText: 'Krax slams the pit alarm. Hatches drop open around the office and loader enforcers climb out with their clamp arms open. Use the desk and the pillars. The enforcers hit in pairs.' }, onSuccessFlags: ['mfg_enforcers_down'] },
                  { text: 'Pick up the ledger and walk away.', morality: 0, loyalty: {}, defer: true, result: 'You collect the ledger. Krax does not breathe until the door closes behind you.' },
                ],
              },
              {
                id: 'phase_k3',
                requiresAllFlags: ['mfg_bay_proof', 'mfg_enforcers_down'],
                requiresNoneFlags: ['mfg_resolved'],
                prompt: 'The last enforcer stops moving. Krax bolts a loader frame to his back, four clamps closing. "Fifteen years. You will not take it from me."',
                repeatPrompt: 'Krax stands in his loader frame, steam venting from the joints.',
                choices: [
                  { text: '[Fight] End it.', morality: -3, loyalty: {}, result: 'The frame lurches off its stand and the floor shudders. Krax roars over the alarm, all four clamps swinging.', triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'mfg_krax', flavorText: 'Krax lurches forward in the heavy loader frame, four clamps swinging. Hydraulics scream. The frame is slow, but nothing in the room is safe from it. Use the cover and break its pace.' }, onSuccessFlags: ['krax_defeated', 'mfg_resolved', 'mfg_end_fight', 'mfg_krax_gone', 'krax_exposed', 'krax_revealed'] },
                  { text: 'Back out of the office.', morality: 0, loyalty: {}, defer: true, result: 'You back out of the office. Krax lets you go and does not follow. He is waiting.' },
                ],
              },
              {
                id: 'phase_k4',
                requiresAllFlags: ['mfg_end_blackmail'],
                prompt: 'Krax signs a transfer without looking up. "Quota is met. The tithe is on time. Your share clears every cycle. We never speak of Bay B."',
                repeatPrompt: 'Krax does not look up. "Your share clears every cycle."',
                choices: [
                  { text: 'Remind him who is watching.', morality: -3, loyalty: { underworld: 3 }, result: 'His arms go still for exactly one breath. "I know." He does not say who he fears more.' },
                ],
              },
            ],
          },
          { id: 'mfg_acting_overseer', x: 30, y: 25, kind: 'kdy_guild_overseer', label: 'Acting Overseer Tallis',
            requiresFlag: 'mfg_krax_gone',
            repeatPrompt: 'Tallis reads another manifest twice.',
            phases: [
              {
                id: 'phase_t1',
                prompt: 'The acting overseer reads every manifest twice. "Joint seal on every door. Whatever Krax built, it stays in its cradle until someone senior decides."',
                repeatPrompt: 'Tallis reads another manifest twice.',
                choices: [
                  { text: 'Ask about the unfinished sections.', morality: 0, loyalty: {}, result: '"Sealed, catalogued, guarded." A pause. "Vael decides what a pocket dreadnought is worth. So does whoever pays Vael."' },
                ],
              },
              {
                id: 'phase_t_fight',
                requiresAllFlags: ['mfg_end_fight'],
                prompt: 'Tallis steps around the scorch marks on the office floor. "Nobody asked who fought him. I will write down that no one did."',
                repeatPrompt: 'Tallis steps around the scorch marks.',
                choices: [
                  { text: 'Say it was self defense.', morality: 0, loyalty: {}, result: '"Of course it was." Tallis does not blink. "The line starts again at dawn."' },
                ],
              },
            ],
          },
          { id: 'lead_engineer_vorn', x: 18, y: 24, wander: 2, kind: 'mechanic', label: 'Lead Engineer Vorn',
            repeatPrompt: 'Vorn has his hands inside a conduit housing. "These assemblies are above civilian spec."',
            phases: [
              {
                id: 'phase_v1',
                prompt: 'Vorn has his arm inside a conduit housing. "You are not KDY clearance." He squints. "But you are not CSF either. These parts are above civilian spec."',
                repeatPrompt: 'Vorn has his hands inside a conduit housing. "These assemblies are above civilian spec."',
                choices: [
                  { text: 'Ask what class of vessel the parts are for.', morality: 5, loyalty: { republic: 8 }, result: '"Mandator class plating, pocket configuration. I built it once before." He shakes his head. "Nobody builds that without a Senate appropriation. The archive is behind me."', grants: { flags: ['vorn_consulted', 'dreadnaught_class_known'], codex: ['codex-mfg-pocket-frame'] } },
                  { text: 'Offer him 200 credits for the technical files.', morality: -8, loyalty: { underworld: 10 }, requires: { credits: 200 }, result: 'He studies the chip a long time. "Log access only. No drive specs." He pockets it. "The archive terminal is behind me. Krax does not check it between shifts."', grants: { flags: ['vorn_bribed', 'vorn_consulted'] } },
                  { text: 'Say you are only looking around.', morality: 0, loyalty: {}, result: '"Look at the plate, then." He nods at the cradles. "Count the sections and tell me I am wrong."' },
                ],
              },
              {
                id: 'phase_v2',
                requiresAllFlags: ['vorn_consulted'],
                prompt: 'Vorn keeps working. "Archive terminal is behind me. Copy what you want. Krax never checks it between shifts."',
                repeatPrompt: 'Vorn waves a rag at the archive terminal.',
                choices: [
                  { text: 'Ask why he is helping.', morality: 2, loyalty: {}, result: '"Because I signed for fourteen sections and was never told the fifteenth was a ship."' },
                ],
              },
              {
                id: 'phase_v_expose',
                requiresAllFlags: ['mfg_end_expose'],
                prompt: 'Vorn wipes his hands. "Section twelve is cold. Vael wants every drawing for the inquiry. You made me proud of a bad job."',
                repeatPrompt: 'Vorn labels another drawing for Vael\'s inquiry.',
                choices: [
                  { text: 'Ask what happens to the sections.', morality: 0, loyalty: {}, result: '"Vael\'s engineers take them apart. Or somebody finishes them." He does not say which.' },
                ],
              },
              {
                id: 'phase_v_blackmail',
                requiresAllFlags: ['mfg_end_blackmail'],
                prompt: 'Vorn will not look at you. "Surplus still ships, now to someone else. I only build what I am told."',
                repeatPrompt: 'Vorn keeps his eyes on the conduit.',
                choices: [
                  { text: 'Tell him the Syndicate pays on time.', morality: -2, loyalty: { underworld: 2 }, result: '"Krax paid on time too." Vorn tightens a coupling. "Until he did not."' },
                ],
              },
              {
                id: 'phase_v_fight',
                requiresAllFlags: ['mfg_end_fight'],
                prompt: 'Vorn leans on a dead cradle. "Krax was a hard man, but he was our hard man. The line is mine now. I would like to build a freighter."',
                repeatPrompt: 'Vorn leans on a dead cradle.',
                choices: [
                  { text: 'Wish him luck with the freighter.', morality: 2, loyalty: {}, result: '"Luck is for people with permits." He smiles at the cradle. "Thank you."' },
                ],
              },
            ],
          },
          // ---- ambient life ----
          { id: 'mfg_dock_foreman', x: 9, y: 24, kind: 'kdy_logistics_officer', label: 'Dock Foreman Teska',
            repeatPrompt: 'Teska ticks another pallet off her slate.',
            prompt: 'Teska ticks pallets off a slate without breaking stride. "Outbound is outbound. If it is sealed, I do not open it. If it is not sealed, I seal it."',
            choices: [ { text: 'Ask about the night lift.', morality: 0, loyalty: {}, result: '"What lift?" She does not look up. "Lift two is out of service at night. It has been for three years."' } ] },
          { id: 'mfg_loader_a', x: 4, y: 24, wander: 2, kind: 'binary_loader', label: 'Loader Unit LD-11',
            repeatPrompt: 'LD-11 repeats its cycle. "LOAD. LOAD. LOAD."',
            prompt: 'A boxy teal loader droid stacks pallets with a dull, even clatter. "LOAD. LOAD. LOAD."',
            choices: [ { text: 'Ask where the pallets go.', morality: 0, loyalty: {}, result: '"OUTBOUND." A pause. "SOME OUTBOUND. SOME B."' } ] },
          { id: 'mfg_power_droid', x: 2, y: 28, kind: 'gnk_power', label: 'Power Droid GNK-4',
            repeatPrompt: 'GNK-4 wobbles on. "Gonk."',
            prompt: 'A squat power droid shuffles beside the crates, trailing a cable. "Gonk. Gonk."',
            choices: [ { text: 'Pat its casing.', morality: 0, loyalty: {}, result: 'The droid hums a half note and wobbles off toward the foundry bus.' } ] },
          { id: 'mfg_welder', x: 13, y: 10, wander: 1, kind: 'zero_g_welder', label: 'Line Welder Brask',
            repeatPrompt: 'Brask flips the visor down and strikes an arc.',
            prompt: 'A welder in a scorched visor hangs from a harness over the cradle. "Section eleven again. Eleven sections, one hull. Do not ask me to do the math."',
            choices: [ { text: 'Ask what the hull is for.', morality: 0, loyalty: {}, result: '"Big." He flips the visor down. "That is all I was told. That is all I want."' } ] },
          { id: 'mfg_foundry_hand', x: 39, y: 10, wander: 1, kind: 'zabrak_techwright', label: 'Foundry Hand Orro',
            repeatPrompt: 'Orro wipes soot from his brow and pours again.',
            prompt: 'A Zabrak techwright with soot to the elbows leans on a pour ladle. "One hundred and fourteen percent. Last month it was a hundred and ten. Guess who got older."',
            choices: [ { text: 'Ask what happens to the surplus.', morality: 0, loyalty: {}, result: '"Goes east." He nods at the thoroughfare. "Ends up where surplus goes. Do not follow it."' } ] },
          { id: 'mfg_guard_office', x: 30, y: 18, kind: 'kdy_security_marine', label: 'KDY Security Marine Hale',
            repeatPrompt: 'Hale keeps his eyes front. "Overseer sees visitors by appointment."',
            prompt: 'A KDY marine in white plate stands to attention outside the glass office. "Overseer\'s office. Appointments only." His eyes track you anyway.',
            choices: [ { text: 'Say Vael sent you.', morality: 0, loyalty: {}, result: 'He steps half a pace aside. "Then go in. Do not touch the safe."' } ] },
          { id: 'mfg_guard_patrol', x: 33, y: 9, patrol: [[33, 9], [41, 9]], kind: 'kdy_ring_sec', label: 'Ring Security Officer Dax',
            repeatPrompt: 'Dax resumes his walk along the foundry rail.',
            prompt: 'A ring security officer in a heat mantle walks the foundry rail. "Keep to the painted lines. Slag does not forgive."',
            choices: [ { text: 'Ask about the east gate.', morality: 0, loyalty: {}, result: '"Closed after the horn. Not my post." He taps the badge on his chest. "Not my business either."' } ] },
          { id: 'mfg_k_droid', x: 15, y: 15, patrol: [[15, 15], [31, 15]], kind: 'k_series_droid', label: 'K-Series Inspection Automaton K-07',
            repeatPrompt: 'K-07 pans its lens and walks on. "Proceed."',
            prompt: 'A K-series automaton paces the thoroughfare, red lens sweeping the floor. "Cleared visitor. Remain in the lit area."',
            choices: [ { text: 'Stand still and be scanned.', morality: 0, loyalty: {}, result: 'The lens sweeps boots to collar. "Cleared." A pause of exactly one second. "East gate is not a lit area."' } ] },
          { id: 'mfg_nautolan', x: 15, y: 24, kind: 'nautolan_engineer', label: 'Engineer Zhaan',
            repeatPrompt: 'Zhaan flicks the schematic aside and rubs his head tendrils.',
            prompt: 'A Nautolan engineer scrolls a schematic past his eyes, head tendrils twitching. "The pressure curves are wrong for a freighter. Right for something else."',
            choices: [ { text: 'Ask what the something else is.', morality: 0, loyalty: {}, result: '"I am an engineer. I say what the numbers say." He taps the screen. "They say hull."' } ] },
        ],
        collectibles: [
          { id: 'sector4_hull_fragment', x: 31, y: 11, label: 'Forged Star Destroyer Hull Plating', reward: 800 },
          { id: 'sector4_assembly_log', x: 10, y: 8, label: 'Assembly Sequence Log', reward: 300 },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 5, 42, 28, 'floor');
          // north band partitions: gantry | production hall | foundry
          carveRect(g, 11, 5, 11, 12, 'wall'); carveRect(g, 11, 8, 11, 9, 'floor');
          carveRect(g, 32, 5, 32, 12, 'wall'); carveRect(g, 32, 8, 32, 9, 'floor');
          // north divider with four exits to the thoroughfare
          carveRect(g, 1, 13, 42, 13, 'wall');
          [[5, 6], [16, 17], [25, 26], [37, 38]].forEach(([a, b]) => carveRect(g, a, 13, b, 13, 'floor'));
          // south divider with four entries
          carveRect(g, 1, 19, 42, 19, 'wall');
          [[3, 8], [17, 18], [28, 29], [38, 39]].forEach(([a, b]) => carveRect(g, a, 19, b, 19, 'floor'));
          // south band partitions: dock | engineering | office | storage bay
          carveRect(g, 13, 20, 13, 28, 'wall');
          carveRect(g, 24, 20, 24, 28, 'wall');
          carveRect(g, 34, 20, 34, 28, 'wall');
          // hidden vault inside storage bay B
          carveRect(g, 35, 24, 42, 24, 'wall'); carveRect(g, 38, 24, 39, 24, 'floor');
          // solid heavy machinery (the anchor row stays walkable)
          carveRect(g, 13, 6, 28, 6, 'wall');          // conveyor lines
          carveRect(g, 14, 9, 18, 11, 'wall');         // cradle one
          carveRect(g, 26, 9, 30, 11, 'wall');         // cradle two
          carveRect(g, 34, 5, 36, 7, 'wall');          // piston tower
          carveRect(g, 39, 6, 42, 7, 'wall');          // boiler
          carveRect(g, 35, 10, 38, 11, 'wall');        // crucible
          pt(g, 0, 14, 'door'); pt(g, 0, 15, 'door');
          pt(g, 43, 16, 'door'); pt(g, 43, 17, 'door');
          return g;
        },
      },

      city_outskirts_bador: {
        id: 'city_outskirts_bador', name: 'City Outskirts of Bador',
        subtitle: 'Bador Moon · Breach-Point Alpha · Active War Zone',
        width: 48, height: 32, spawnPos: { x: 14, y: 6 }, textureId: 'ferrowake',
        accent: '#FF5500', accentGlow: 'rgba(255,85,0,0.28)', accentDim: '#5C2C16',
        floorColor: '#1C1612', floorAlt: '#241C16', wallDark: '#0C0A08', wallLight: '#2A2420',
        bg: 'radial-gradient(circle at 70% 20%, #2A1608 0%, #120C08 45%, #060403 100%)', ambient: 'warzone',
        decor: ['pipe', 'rubble'],
        paAnnouncements: [
          'All laborers shelter in place. Repeat, shelter in place. Do not approach the breach.',
          'KDY Security reminds civilians that the Kuat Expansionist Front is a proscribed organization.',
          'Mortar fire is expected to continue. This is not a drill.',
          'Medical teams are available at the southwest redoubt. Bring your own bandages.',
        ],
        doors: [
          { x: 14, y: 0,  targetZone: 'bador_main_base', targetPos: { x: 16, y: 26 }, label: 'KDY Main Base' },
          { x: 15, y: 0,  targetZone: 'bador_main_base', targetPos: { x: 17, y: 26 }, label: 'KDY Main Base' },
          { x: 47, y: 14, targetZone: 'outpost_7', targetPos: { x: 1, y: 14 }, label: 'Outpost 7' },
          { x: 47, y: 15, targetZone: 'outpost_7', targetPos: { x: 1, y: 15 }, label: 'Outpost 7' },
          { x: 24, y: 31, targetZone: 'engine_wastes', targetPos: { x: 24, y: 1 }, label: 'Landspeeder Depot Gate', requiresFlag: 'gateway_unlocked', lockedMessage: 'The depot gate is sealed. It needs the transit cylinder Varek Korr carries.' },
          { x: 25, y: 31, targetZone: 'engine_wastes', targetPos: { x: 25, y: 1 }, label: 'Landspeeder Depot Gate', requiresFlag: 'gateway_unlocked', lockedMessage: 'The depot gate is sealed. It needs the transit cylinder Varek Korr carries.' },
        ],
        worldObjects: [
          { id: 'out_vista_w', propArt: 'basalt_vista', propVariant: 'outskirts_a', x: 12, y: 5, once: false, iconKind: 'vista', label: 'KDY Perimeter Wall',
            description: "The durasteel wall of the KDY Surface Logistics Hub fills the north horizon, white towers lined with cyan windows. A breach glows orange at the gate, smoke leans across an iron sky, and the dark red parent world hangs low.", grantsCodex: 'codex-outskirts-war' },
          { id: 'out_vista_e', propArt: 'basalt_vista', propVariant: 'outskirts_b', x: 36, y: 5, once: false, iconKind: 'vista', label: 'Shipyard Skyline in Smoke',
            description: "East of the breach the wall climbs into crane arms and silent turbolaser mounts, wrapped in smoke. A mortar shell arcs through the haze." },
          { id: 'bador_republic_terminal', propArt: 'console', propVariant: 'republic_old', x: 9, y: 7, label: 'Abandoned Republic Terminal',
            description: 'A rusted Republic terminal, decommissioned three years ago. The last entry is a distress call with no reply.',
            once: true, grantsFlag: 'bador_republic_history', grantsCodex: 'codex-bador-history' },
          { id: 'bador_supply_cache', propArt: 'crate_stack', propVariant: 'hidden', x: 3, y: 29, label: 'Hidden Supply Cache',
            description: 'A hidden alcove in the collapsed wall, used as a supply drop.',
            once: true, requiresFlag: 'ghost_keel_active', grantsItem: 'cherit_logbook' },
          { id: 'burnt_kdy_log', propArt: 'console', propVariant: 'derelict', x: 12, y: 8, once: true, iconKind: 'crate', label: 'Burnt KDY Log Terminal',
            description: "A plasma blistered KDY terminal. Its last log is a manifest of reactor cores moved out of Base Alpha, the destination circled in grease pencil.", grantsItem: 'item_kdy_datapad_01', grantsFlag: 'kdy_manifest_found' },
          { id: 'line_c_barricade_a', propArt: 'kdy_barricade', propVariant: 'kdy', x: 5, y: 11, once: false, iconKind: 'crate', label: 'Line C Barricade',
            description: "A plasma scored durasteel barricade, white paint run to grey soot." },
          { id: 'line_c_barricade_b', propArt: 'kdy_barricade', propVariant: 'kdy', x: 5, y: 16, once: false, iconKind: 'crate', label: 'Line C Barricade',
            description: "A half melted barricade. Marker on the inner face reads LAST ONE STANDING, with LAST crossed out." },
          { id: 'line_c_barricade_c', propArt: 'kdy_barricade', propVariant: 'kdy', x: 11, y: 13, once: false, iconKind: 'crate', label: 'Line C Barricade',
            description: "The center of the line. Spent cells crunch underfoot and two dead droids lean on the plating." },
          { id: 'hackable_turret', propArt: 'auto_turret', propVariant: 'hostile', x: 9, y: 9, once: true, iconKind: 'terminal', label: 'Overheated Defense Turret',
            description: "A KDY turret, barrel glowing dull orange, sensor locked on the wrong target. A diagnostic panel shows a crashed targeting daemon.", requiresFlag: 'outskirts_started', lockedMessage: 'Speak to Squad Leader Kaelin first. She will want to know what you plan to do to her turret.', triggersMinigame: 'signal_siphon', grantsFlags: ['turret_sliced', 'outskirts_mortar_down'], thermalDeltaOnSuccess: 5 },
          { id: 'hackable_turret_friendly', propArt: 'auto_turret', propVariant: 'friendly', x: 9, y: 9, once: false, iconKind: 'terminal', label: 'Turret (Friendly Suppression)',
            description: "The slice held. The turret throws long, steady bursts at the KEF line.", requiresFlag: 'turret_sliced', invisibleLocked: true },
          { id: 'fuel_conduit_main', propArt: 'fuel_conduit', propVariant: 'plain', x: 22, y: 15, once: true, iconKind: 'crate', label: 'Volatile Fuel Conduit',
            description: "A fat orange pipeline bleeding amber vapor from a cracked flange. It feeds the KEF mortar line. One charge takes the alley and its shanties with it.", requiresFlag: 'bevin_plan_flank', lockedMessage: 'Nothing about this pipe is safe to touch yet. Ask Captain Bevin for the flank plan.', requiresNoneFlags: ['kef_fuel_detonated'], grantsFlags: ['kef_fuel_detonated', 'outskirts_mortar_down'], thermalDelta: 20 },
          { id: 'fuel_conduit_burst', propArt: 'fuel_conduit', propVariant: 'burst', x: 22, y: 15, once: false, iconKind: 'crate', label: 'Burning Fuel Conduit',
            description: "The flange tears open and fire rolls down the alley. KEF cover turns to slag. Somewhere ahead, people are shouting your name.", requiresFlag: 'kef_fuel_detonated', invisibleLocked: true },
          { id: 'breach_choke', propArt: 'kdy_barricade', propVariant: 'scrap', x: 32, y: 14, once: true, iconKind: 'floor', label: 'KEF Breach Choke Point',
            description: "The KEF forward barricade, orange suits behind scrap plating, rebreather filters glinting in the smoke.", requiresFlag: 'bevin_plan_push', lockedMessage: 'A shock squad holds that barricade and you have no plan for it yet. Speak to Captain Bevin.', triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_shock_breacher', flavorText: 'Vanguard troops stack behind you. Orange suits rise from the scrap, plasma shotguns first.' }, grantsFlag: 'outskirts_mortar_down', thermalDeltaOnSuccess: 12 },
          { id: 'kef_catwalk_1', propArt: 'catwalk_post', x: 38, y: 9, once: false, iconKind: 'crate', label: 'KEF Sniper Catwalk',
            description: "A sniper catwalk of scavenged scaffolding, rails warm, spent cells in a neat row." },
          { id: 'kef_catwalk_2', propArt: 'catwalk_post', x: 43, y: 9, once: false, iconKind: 'crate', label: 'KEF Sniper Catwalk',
            description: "The east catwalk. A thermos is still steaming." },
          { id: 'kef_catwalk_3', propArt: 'catwalk_post', x: 36, y: 14, once: false, iconKind: 'crate', label: 'KEF Sniper Catwalk',
            description: "A low catwalk post, range marks chalked on the rail." },
          { id: 'kef_mortar_pit', propArt: 'mortar_pit', x: 42, y: 19, once: true, iconKind: 'crate', label: 'KEF Mortar Pit',
            description: "Sandbags ring a warm mortar tube. A shell crate lid is scrawled with a prayer to a dead engineer, and a breaching cutter hangs on a hook.", requiresFlag: 'outskirts_mortar_down', lockedMessage: 'The mortar crew is still alive. Silence the line first.', grantsItem: 'item_kef_cutter_01', grantsFlag: 'mortar_pit_searched', grantsCodex: 'codex-front-iron-thunder' },
          { id: 'kef_hidden_cache', propArt: 'crate_stack', propVariant: 'hidden', x: 45, y: 12, once: true, iconKind: 'crate', label: 'Hidden KEF Cache',
            description: "Behind a false panel: a blueprint of a KDY heavy loader walker with an engineer's annotations, a half built plasma cutter and a sealed envelope addressed to nobody.", grantsFlag: 'kef_blueprint_found', grantsCodex: 'codex-kef' },
          { id: 'hovel_1', propArt: 'shanty_hovel', propVariant: 'a', x: 19, y: 11, once: false, iconKind: 'crate', label: 'Cantina Shanty',
            description: "CANTINA, in pink Aurebesh neon. Three drinkers sit very still, listening to the mortars." },
          { id: 'hovel_2', propArt: 'shanty_hovel', propVariant: 'b', x: 24, y: 11, once: false, iconKind: 'crate', label: 'Droid Repairs',
            description: "REPAIRS. A tarp roofed droid shop. The sign says OPEN, though nothing here is." },
          { id: 'hovel_3', propArt: 'shanty_hovel', propVariant: 'c', x: 29, y: 11, once: false, iconKind: 'crate', label: 'Family Hovel',
            description: "Washing on a line, a child's chalk speeder on the wall, the door barricaded with a cooking pot." },
          { id: 'hovel_4', propArt: 'shanty_hovel', propVariant: 'c', x: 19, y: 19, once: false, iconKind: 'crate', label: 'Noodle Stall',
            description: "NOODLES, in amber light. The vendor has fled but the broth is warm." },
          { id: 'hovel_5', propArt: 'shanty_hovel', propVariant: 'a', x: 24, y: 19, once: false, iconKind: 'crate', label: 'Boarded Storefront',
            description: "A hand lettered notice: KDY LEFT US TO BURN. Underneath: WE ARE NOT LEAVING." },
          { id: 'hovel_6', propArt: 'shanty_hovel', propVariant: 'b', x: 29, y: 19, once: false, iconKind: 'crate', label: 'Makeshift Clinic',
            description: "Three cots, a rack of bacta patches, blood drying on the floor. A medic droid loops a calm warning." },
          { id: 'hovel_7', propArt: 'shanty_hovel', propVariant: 'a', x: 17, y: 26, once: false, iconKind: 'crate', label: 'Laborer Barracks',
            description: "Bunks stacked four high behind a door that no longer closes. Grey and ochre tunics hang from the rafters." },
          { id: 'hovel_8', propArt: 'shanty_hovel', propVariant: 'c', x: 26, y: 26, once: false, iconKind: 'crate', label: 'Looted Warehouse',
            description: "Doors blown inward, shelves empty. Fresh boot prints lead toward the sniper catwalks." },
          { id: 'burning_wreck_1', propArt: 'burning_wreck', x: 16, y: 22, once: false, iconKind: 'crate', label: 'Burning Speeder Chassis',
            description: "A speeder chassis burning from inside, smoke rolling off it." },
          { id: 'burning_wreck_2', propArt: 'burning_wreck', x: 35, y: 20, once: false, iconKind: 'crate', label: 'Burning Speeder Chassis',
            description: "A speeder chassis burning from inside. The metal groans as it cools." },
          { id: 'burning_wreck_3', propArt: 'burning_wreck', x: 27, y: 15, once: false, iconKind: 'crate', label: 'Burning Speeder Chassis',
            description: "A speeder chassis burning from inside, heat shimmering the air." },
          { id: 'burning_wreck_4', propArt: 'burning_wreck', x: 12, y: 27, once: false, iconKind: 'crate', label: 'Burning Speeder Chassis',
            description: "A speeder chassis burning from inside, throwing a flickering glow." },
          { id: 'bevin_bacta', propArt: 'bacta_pod', x: 3, y: 24, once: false, iconKind: 'crate', label: 'Field Bacta Pod',
            description: "A KDY field bacta pod, glass cracked, fluid cloudy, readout green. Injured agents recover here.", healsRoster: true },
          { id: 'bevin_board', propArt: 'command_board', x: 8, y: 22, once: false, iconKind: 'terminal', label: 'Command Redoubt Display',
            description: "A grease pencil board: Line C in white, the KEF mortar line in red, an X over the fuel conduit and FUN written in the margin." },
          { id: 'bevin_holo', propArt: 'sanctum_holo', x: 8, y: 26, once: false, iconKind: 'terminal', label: 'Redoubt Tactical Holo',
            description: "Breach-Point Alpha in cyan wireframe, mortar arcs in dotted red. It flickers when a shell lands near." },
          { id: 'bevin_crates', propArt: 'crate_stack', propVariant: 'kdy', x: 11, y: 24, once: false, iconKind: 'crate', label: 'Ammunition and Power Crates',
            description: "KDY power cell cases, grenade pods and heavy cable. A weapon cools fast here.", thermalDelta: -8 },
          { id: 'vault_shutter', propArt: 'kdy_barricade', propVariant: 'kdy', x: 31, y: 25, once: false, iconKind: 'floor', label: 'Mortar Fortress Blast Shutter',
            description: "A heavy blast shutter welded by KEF engineers, a burn mark showing where the cutter goes.", requiresFlag: 'outskirts_complete', lockedMessage: 'The shutter is welded from inside. Finish the fight at Breach-Point Alpha and Kaelin will give you the cutting codes.' },
          { id: 'war_dredge', propArt: 'scout_walker', x: 39, y: 27, once: true, iconKind: 'floor', label: 'The War-Dredge',
            description: "A KDY Mk.IV loader walker plated in welded scrap: twin plasma cutters, a triple tube mortar on its back, a siege hammer on the right. The cockpit glows red. Varek Korr sits inside.", requiresFlag: 'outskirts_complete', triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'varek_korr', flavorText: 'The War-Dredge lurches off its platform and the Aegis barrier snaps on. Mortar shells paint red circles on the floor. Overhead, an ammunition cart rattles along its rail. Use the cover. Use the carts.' }, grantsFlag: 'varek_defeated', thermalDeltaOnSuccess: 15 },
          { id: 'vault_cover_a', propArt: 'repulsor_barricade', x: 35, y: 26, once: false, iconKind: 'crate', label: 'Vault Cover Line',
            description: "Shrapnel scarred bunkers and repulsor plating. Shallow cover, but all you have." },
          { id: 'vault_cart', propArt: 'crate_stack', propVariant: 'kdy', x: 44, y: 24, once: false, iconKind: 'crate', label: 'Unstable Ammunition Cart',
            description: "An overhead rail cart of mortar shells, its coupling loosened by hand." },
          { id: 'landspeeder_depot_obj', propArt: 'landspeeder_depot', x: 24, y: 29, once: false, iconKind: 'terminal', label: 'KDY Landspeeder Depot',
            description: "Three KDY-74 speeder bikes in cradles under cyan floodlight, engines ticking as they cool. The gate reader wants Varek Korr's cylinder.", requiresNoneFlags: ['gateway_unlocked'] },
          { id: 'landspeeder_depot_open', propArt: 'landspeeder_depot', x: 24, y: 29, once: false, iconKind: 'terminal', label: 'KDY Landspeeder Depot (Gate Open)',
            description: "Three KDY-74 speeder bikes under cyan floodlight. The gate reader glows green and the blast doors south stand open on the Engine Wastes. The depot has not been this quiet in months.", requiresFlag: 'gateway_unlocked', invisibleLocked: true },
        ],
        npcs: [
          { id: 'bador_survivor', x: 22, y: 13, wander: 3, kind: 'generic', label: 'Bador Resident',
            repeatable: true,
            phases: [
              {
                id: 'phase_s_intro',
                prompt: '"You are not KDY." They keep a wall at their back. "Three people went south two weeks ago to chase the engine sounds. None came back."',
                choices: [
                  { text: 'Ask what they heard from the south.', morality: 5, loyalty: { republic: 5 }, result: '"A hum, like something huge charging. Then silence."', grants: { flags: ['bador_locals_spoken'] } },
                  { text: 'Tell them to evacuate.', morality: 10, loyalty: { republic: 10 }, result: '"To where? KDY does not fly civilians. Find out what is out there. Maybe then someone will care."', grants: { flags: ['bador_evacuation_suggested'] } },
                ],
              },
              {
                id: 'phase_s_tac',
                requiresAllFlags: ['outskirts_tactical'],
                prompt: '"Our people came out of the smoke alive." They press a warm bowl into your hands. "Eat."',
                choices: [
                  { text: 'Take the bowl.', morality: 1, loyalty: {}, result: 'The broth is thin and excellent. Down the street, someone mends a barricade with a child on their shoulders.' },
                ],
              },
              {
                id: 'phase_s_ruth',
                requiresAllFlags: ['outskirts_ruthless'],
                prompt: '"You are the one who fried the street." They do not lower their voice. "Forty names. I learned every one."',
                choices: [
                  { text: 'Let them speak.', morality: 0, loyalty: {}, result: 'They list the names to the end. The street is quieter than the mortars ever made it.' },
                ],
              },
            ],
          },
          { id: 'kaelin', x: 13, y: 9, kind: 'kdy_security_marine', label: 'Squad Leader Kaelin',
            questNpc: true,
            repeatable: true,
            phases: [
              {
                id: 'phase_k_intro',
                requiresNoneFlags: ['outskirts_started'],
                prompt: "Squad Leader Kaelin drops behind a barricade, soot on her armor, a bloody bandage over one visor. \"Mortars are chewing our cover. What are you doing in a live fire zone, freelancer?\"",
                choices: [
                  { text: "I was sent to secure the perimeter. Status?", morality: 0, loyalty: {}, result: "\"Critical. They blew the gate and our turrets are fried. Captain Bevin runs the redoubt to the southwest. Take his orders.\"", grants: { flags: ['outskirts_started', 'kaelin_met'], codex: ['codex-outskirts-war'] } },
                  { text: "[Slice] I can turn that overheated turret on their line.", morality: 0, loyalty: {}, result: "\"The one by the gate is cooked.\" She nods once. \"Flip it and Bevin gets his lane.\"", grants: { flags: ['outskirts_started', 'kaelin_met', 'kaelin_turret_hint'] } },
                  { text: "Out of my way. I have my own objectives.", morality: -3, loyalty: {}, result: "She laughs without humor. \"Everyone here does. Most are dead.\"", grants: { flags: ['kaelin_met'] } },
                ],
              },
              {
                id: 'phase_k_hold',
                requiresAllFlags: ['outskirts_started'],
                requiresNoneFlags: ['outskirts_mortar_down'],
                prompt: "\"Bevin is in the redoubt, southwest. Take his plan. We need that mortar line silent.\"",
                choices: [
                  { text: "On it.", morality: 0, loyalty: {}, result: "\"Do not die in my street.\"" },
                ],
              },
              {
                id: 'phase_k_interrogate',
                requiresAllFlags: ['outskirts_mortar_down'],
                requiresNoneFlags: ['kef_prisoner_interrogated'],
                prompt: "\"The mortar line is quiet. Hear that?\" Kaelin lowers her rifle. \"A wounded KEF fighter is in the alley behind the cantina. Find out what the convoy is after.\"",
                choices: [
                  { text: "I will talk to him.", morality: 0, loyalty: {}, result: "\"Be quick. His friends are not done.\"" },
                ],
              },
              {
                id: 'phase_k_choice',
                requiresAllFlags: ['kef_prisoner_interrogated'],
                requiresNoneFlags: ['outskirts_complete'],
                prompt: "Kaelin has been listening on comms. \"Reactor cores, aimed at the lower levels through the gate grid. I can overcharge the grid and fry the street, or you flank the alleys by hand.\"",
                choices: [
                  { text: "[Ruthless] Overcharge the grid. End this now.", morality: -15, loyalty: { underworld: -10 }, result: "Arcs sweep the street. The KEF wave drops in one flash, and so do the laborers pinned behind the barricades. Kaelin approves. The doorways stay silent.", grants: { flags: ['outskirts_complete', 'outskirts_ruthless', 'civilians_killed_at_gate', 'main_gate_grid_fried'], items: ['kdy_landspeeder_auth'], thermal: 20 } },
                  { text: "[Tactical] Flank through the alleys. Save the laborers.", morality: 15, loyalty: { republic: 10 }, result: "You take the mortar crew by hand and walk the trapped laborers out through the smoke. Behind the shell racks you find an insulation vest. Kaelin salutes.", grants: { flags: ['outskirts_complete', 'outskirts_tactical', 'civilians_saved_at_gate', 'kef_mortar_flanked'], items: ['item_thermal_gear_01', 'kdy_landspeeder_auth'], thermal: 8 } },
                ],
              },
              {
                id: 'phase_k_done',
                requiresAllFlags: ['outskirts_complete'],
                prompt: "Kaelin has shed the bandage, a clean line of stitches beneath. \"The Mortar Fortress is open to you. Varek Korr and his walker are inside. When he falls, the depot gate opens south.\"",
                choices: [
                  { text: "Understood, Squad Leader.", morality: 0, loyalty: {}, result: "\"Keep the line behind you.\"" },
                ],
              },
              {
                id: 'phase_k_cap_tac',
                requiresAllFlags: ['varek_resolved', 'outskirts_tactical'],
                requiresNoneFlags: ['front_outskirts_closed'],
                prompt: "The depot gate stands open to the south. Laborers have hung a lettered banner on Line C. Kaelin offers a citation chip. \"KDY pays. The street thanks you. Both are on here.\"",
                choices: [
                  { text: "[Accept] The citation and the pay.", morality: 0, loyalty: {}, result: "The Perimeter Citation, and twenty five hundred credits from a quartermaster who did not argue.", grants: { flags: ['front_outskirts_closed', 'front_outskirts_paid'], items: ['front_perimeter_citation'], credits: 2500, codex: ['codex-front-street-saved'] } },
                  { text: "[Give] The citation. The pay goes to the clinic.", morality: 10, loyalty: { republic: 6 }, result: "\"The clinic has bacta through winter.\" She almost smiles. On a roof behind her, a child waves.", grants: { flags: ['front_outskirts_closed', 'front_outskirts_relief'], items: ['front_perimeter_citation'], codex: ['codex-front-street-saved'] } },
                ],
              },
              {
                id: 'phase_k_cap_ruth',
                requiresAllFlags: ['varek_resolved', 'outskirts_ruthless'],
                requiresNoneFlags: ['front_outskirts_closed'],
                prompt: "Forty laborers lie under tarps by the gate. KDY wants a KEF atrocity on the record. Kaelin will not meet your eye. \"Sign the statement and the depot fee is paid.\"",
                choices: [
                  { text: "[Sign] Let KDY blame the KEF.", morality: -8, loyalty: { underworld: 4 }, result: "You sign. Three thousand credits clear and the Perimeter Citation changes hands without a word.", grants: { flags: ['front_outskirts_closed', 'front_outskirts_coverup'], items: ['front_perimeter_citation'], credits: 3000, codex: ['codex-front-street-silent'] } },
                  { text: "[Refuse] Let the record name the grid.", morality: 8, loyalty: { republic: 4 }, result: "\"Then it stands.\" She nods, tired. The citation is yours without the credits, and the report names the grid.", grants: { flags: ['front_outskirts_closed', 'front_outskirts_record'], items: ['front_perimeter_citation'], codex: ['codex-front-street-silent'] } },
                ],
              },
              {
                id: 'phase_k_epi_tac',
                requiresAllFlags: ['front_outskirts_closed', 'outskirts_tactical'],
                prompt: "Kaelin stands under the banner, helmet off. \"The street is rebuilding Line C into a wall with windows. South is the Wastes. Do not come back dead.\"",
                choices: [
                  { text: "I will not.", morality: 0, loyalty: {}, result: "\"Good. I am tired of writing letters.\"" },
                ],
              },
              {
                id: 'phase_k_epi_ruth',
                requiresAllFlags: ['front_outskirts_closed', 'outskirts_ruthless'],
                prompt: "Kaelin works a rag along her rifle and speaks to it. \"The street will not speak to me either. South is the Wastes, freelancer. They will not ask.\"",
                choices: [
                  { text: "Leave her to the rifle.", morality: 0, loyalty: {}, result: "She cleans a barrel that is already clean." },
                ],
              },
            ],
          },
          { id: 'captain_bevin', x: 6, y: 22, kind: 'kdy_commander', label: 'Captain Bevin',
            questNpc: true,
            repeatable: true,
            phases: [
              {
                id: 'phase_b_pre',
                requiresNoneFlags: ['outskirts_started'],
                prompt: "Captain Bevin stands under a battered holo projector, a red cybernetic optic painting readouts on the smoke. \"Tell me you spoke to Kaelin. She keeps the gate. I keep the line.\"",
                choices: [
                  { text: "Not yet. I will find her.", morality: 0, loyalty: {}, result: "\"Do. Then come back for the plan.\"" },
                ],
              },
              {
                id: 'phase_b_plan',
                requiresAllFlags: ['outskirts_started'],
                requiresNoneFlags: ['bevin_plan_push', 'bevin_plan_flank', 'bevin_plan_turret'],
                prompt: "\"KEF has us pinned behind Line C and the mortars are eating us alive. What is your call?\" The optic traces a red line across the battlefield.",
                choices: [
                  { text: "[Iron Wall] Spearhead the main breach push.", morality: 0, loyalty: {}, result: "\"Vanguard, form up behind the freelancer!\" Four white armored troops lock shields behind you. The choke point is the barricade to the east.", grants: { flags: ['bevin_plan_push'] } },
                  { text: "[Flank] Detonate the central fuel line.", morality: -4, loyalty: {}, result: "\"It takes half the alley with it, but it breaks their line. Do it.\" He marks the conduit in the middle of the main street.", grants: { flags: ['bevin_plan_flank'] } },
                  { text: "[Overwatch] Take the heavy turret and suppress the line.", morality: 0, loyalty: {}, result: "\"Squad Three, smoke and cover!\" The hackable turret by the gate is yours.", grants: { flags: ['bevin_plan_turret'] } },
                ],
              },
              {
                id: 'phase_b_exec',
                requiresAllFlags: ['outskirts_started'],
                requiresNoneFlags: ['outskirts_mortar_down'],
                requiresAnyFlag: ['bevin_plan_push', 'bevin_plan_flank', 'bevin_plan_turret'],
                prompt: "\"The plan is set. Execute it. Every minute we wait, another laborer burns.\"",
                choices: [
                  { text: "Understood.", morality: 0, loyalty: {}, result: "\"Move.\"" },
                ],
              },
              {
                id: 'phase_b_done',
                requiresAllFlags: ['outskirts_mortar_down'],
                prompt: "\"The line is holding. For the first time, it is actually holding.\" Bevin taps his optic. \"Whatever you chose back there, I will remember it. Both ways.\"",
                choices: [
                  { text: "Hold the line, Captain.", morality: 0, loyalty: {}, result: "\"Always.\"" },
                ],
              },
              {
                id: 'phase_b_cap_tac',
                requiresAllFlags: ['varek_resolved', 'outskirts_tactical'],
                requiresNoneFlags: ['front_bevin_closed'],
                prompt: "On the board the depot gate light is green. A laborer's drawing of the redoubt is pinned above it. \"My people want you to have a field kit. I outrank them, so I agree.\"",
                choices: [
                  { text: "[Take the field kit.]", morality: 0, loyalty: {}, result: "Bacta, heat gel, spare cells, packed by hands that were not ordered to. \"Bring the street a story from down there.\"", grants: { flags: ['front_bevin_closed'], items: ['front_redoubt_kit'] } },
                ],
              },
              {
                id: 'phase_b_cap_ruth',
                requiresAllFlags: ['varek_resolved', 'outskirts_ruthless'],
                requiresNoneFlags: ['front_bevin_closed'],
                prompt: "The depot gate light is green. Bevin slides an insulation vest and a field kit over the table. \"The Wastes are worse than this street. Wear it. It will not wash anything off.\"",
                choices: [
                  { text: "[Take the vest and the kit.]", morality: 0, loyalty: {}, result: "The vest is heavy and still smells of the redoubt's smoke. Bevin does not salute. \"Go on, then.\"", grants: { flags: ['front_bevin_closed'], items: ['item_thermal_gear_01', 'front_redoubt_kit'] } },
                ],
              },
              {
                id: 'phase_b_epi',
                requiresAllFlags: ['front_bevin_closed'],
                prompt: "Bevin watches the green light. \"Korr's war is over, one way or another. The Wastes are south. Keep your thermals down.\"",
                choices: [
                  { text: "Hold the line, Captain.", morality: 0, loyalty: {}, result: "\"Always.\"" },
                ],
              },
            ],
          },
          { id: 'kef_wounded', x: 27, y: 13, kind: 'generic', label: 'Wounded KEF Insurgent',
            questNpc: true,
            requiresFlag: 'outskirts_mortar_down',
            repeatable: true,
            phases: [
              {
                id: 'phase_kef_wounded',
                requiresAllFlags: ['outskirts_mortar_down'],
                requiresNoneFlags: ['kef_prisoner_interrogated'],
                prompt: "A wounded KEF insurgent sits against a wall, rebreather round his neck, orange suit torn. \"Kill me if you must. I will not give you the convoy.\"",
                choices: [
                  { text: "[Intimidate] Talk, or the fire reaches you first.", morality: -6, loyalty: {}, result: "He flinches. \"The cores go to the Engine Wastes, to a foundry with no name. The Front pays with every coin in this slum.\"", grants: { flags: ['kef_prisoner_interrogated', 'kef_prisoner_hurt'], thermal: 5, codex: ['codex-kef'] } },
                  { text: "[Tend wounds] Let me look at that leg.", morality: 6, loyalty: { republic: 4 }, result: "You pack the wound with bacta. At last he speaks. \"The cores go to the Wastes. Not to attack KDY. To restart something old.\"", grants: { flags: ['kef_prisoner_interrogated', 'kef_prisoner_helped'], codex: ['codex-kef'] } },
                  { text: "[Pay] Two hundred credits for the truth.", morality: 0, loyalty: {}, result: "He takes the chips with a shaking hand. \"The cores. The Wastes. A foundry. That is all I know.\"", grants: { flags: ['kef_prisoner_interrogated'], codex: ['codex-kef'] }, requires: { credits: 200 } },
                ],
              },
              {
                id: 'phase_kef_done',
                requiresAllFlags: ['kef_prisoner_interrogated'],
                prompt: "The wounded KEF fighter keeps his eyes closed. \"Go. I am done talking.\"",
                choices: [
                  { text: "Leave him in peace.", morality: 0, loyalty: {}, result: "He breathes slowly. You leave him be." },
                ],
              },
            ],
          },
          { id: 'laborer_1', x: 20, y: 13, wander: 3, kind: 'generic', label: 'Panicked Laborer',
            repeatable: true,
            phases: [
              {
                id: 'phase_l',
                prompt: "\"The walls were supposed to keep us safe!\" They duck low and run for the nearest roof.",
                choices: [
                  { text: "Tell them where the shelter is.", morality: 2, loyalty: {}, result: "They bolt toward the redoubt as a mortar lands behind them.", grants: { thermal: -1 } },
                ],
              },
              {
                id: 'phase_l_tac',
                requiresAllFlags: ['outskirts_tactical'],
                prompt: "\"You are the one from the alleys!\" They grab your sleeve. \"My sister is alive because of you.\"",
                choices: [
                  { text: "Tell her to stay near the redoubt.", morality: 1, loyalty: {}, result: "They nod hard and walk away, still shaking." },
                ],
              },
              {
                id: 'phase_l_ruth',
                requiresAllFlags: ['outskirts_ruthless'],
                prompt: "They see you and cross the street without a word, eyes on the ground.",
                choices: [
                  { text: "Let them go.", morality: 0, loyalty: {}, result: "The gap in the street stays where they were." },
                ],
              },
            ],
          },
          { id: 'laborer_2', x: 26, y: 18, wander: 3, kind: 'generic', label: 'Panicked Laborer',
            repeatable: true,
            phases: [
              {
                id: 'phase_l',
                prompt: "\"KDY left us to burn!\" They duck low and run for the nearest roof.",
                choices: [
                  { text: "Tell them where the shelter is.", morality: 2, loyalty: {}, result: "They bolt toward the redoubt as a mortar lands behind them.", grants: { thermal: -1 } },
                ],
              },
            ],
          },
          { id: 'laborer_3', x: 15, y: 18, wander: 3, kind: 'generic', label: 'Panicked Laborer',
            repeatable: true,
            phases: [
              {
                id: 'phase_l',
                prompt: "\"Please, the shelter is over there. Please.\" They duck low and run for the nearest roof.",
                choices: [
                  { text: "Tell them where the shelter is.", morality: 2, loyalty: {}, result: "They bolt toward the redoubt as a mortar lands behind them.", grants: { thermal: -1 } },
                ],
              },
              {
                id: 'phase_l_after',
                requiresAllFlags: ['varek_resolved'],
                prompt: "\"They opened the depot. The speeders are glowing.\" They look south, then at you. \"Where do people like you go?\"",
                choices: [
                  { text: "Down into the Wastes.", morality: 0, loyalty: {}, result: "\"Of course you do.\" They go back to sweeping glass." },
                ],
              },
            ],
          },
          { id: 'medic_tessin', x: 4, y: 26, kind: 'medic', label: 'KDY Combat Medic Tessin',
            repeatable: true,
            phases: [
              {
                id: 'phase_m',
                prompt: "Tessin kneels over a trooper, a medical droid humming at her shoulder. \"Hurt? The bacta pod is free. Not hurt? Move, you are in my light.\"",
                choices: [
                  { text: "I will go.", morality: 0, loyalty: {}, result: "She never looks up." },
                ],
              },
            ],
          },
          { id: 'varek_korr', x: 37, y: 28, kind: 'cyborg_mechanic', label: 'Commander Varek Korr',
            questNpc: true,
            requiresFlag: 'varek_defeated',
            hideAfterFlags: ['varek_resolved'],
            repeatable: true,
            phases: [
              {
                id: 'phase_v',
                requiresAllFlags: ['varek_defeated'],
                requiresNoneFlags: ['varek_resolved'],
                prompt: "Varek Korr slumps against the dead walker, bleeding, a glowing KDY encryption cylinder in his fist. \"Finish it. You think you are saving this city? You guard the aristocrats who burn us for fuel.\"",
                choices: [
                  { text: "[Execute] Your war is over, Korr. The gate opens.", morality: -12, loyalty: { underworld: -5 }, result: "\"For the Front,\" he whispers. The cylinder drops into your hand. On the comm Bevin opens the depot gate, and in the street nobody cheers.", grants: { flags: ['varek_executed', 'varek_resolved', 'gateway_unlocked'], items: ['kdy_transit_cylinder'], credits: 2500, thermal: 5, codex: ['codex-front-iron-thunder'] } },
                  { text: "[Spare] The cylinder and your bypass codes, and you walk.", morality: 8, loyalty: { republic: 4 }, result: "He tosses a datapad. \"The codes open your gate. What waits in the Wastes will melt the armor off your bones.\" He staggers out the back. The schematic grants fire resistance in the Wastes.", grants: { flags: ['varek_spared', 'varek_resolved', 'gateway_unlocked'], items: ['kdy_transit_cylinder', 'kef_bypass_schematic'], codex: ['codex-front-iron-thunder'] } },
                ],
              },
            ],
          },
        ],
        collectibles: [{ id: 'bador_city_cache', x: 22, y: 8, label: 'Salvaged Survival Rations', reward: 150 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 14, 0, 15, 5, 'floor');
          carveRect(g, 1, 5, 46, 30, 'floor');
          [[19, 11], [24, 11], [29, 11], [19, 19], [24, 19], [29, 19], [17, 26], [26, 26]].forEach(([ax, ay]) => carveRect(g, ax - 1, ay - 2, ax + 1, ay - 1, 'wall'));
          // Mortar Fortress Ammunition Vault (southeast)
          carveRect(g, 31, 22, 46, 30, 'wall'); carveRect(g, 32, 23, 45, 29, 'floor'); pt(g, 31, 25, 'floor');
          pt(g, 14, 0, 'door'); pt(g, 15, 0, 'door');
          pt(g, 47, 14, 'door'); pt(g, 47, 15, 'door');
          pt(g, 24, 31, 'door'); pt(g, 25, 31, 'door');
          return g;
        },
      },

      engine_wastes: {
        id: 'engine_wastes', name: 'The Engine Wastes',
        subtitle: 'Bador Moon · Mechanical Necropolis · The Titan Manifold',
        width: 48, height: 36, spawnPos: { x: 2, y: 10 }, textureId: 'ferrowake',
        accent: '#C4923E', accentGlow: 'rgba(196,146,62,0.26)', accentDim: '#4A433E',
        floorColor: '#1A1614', floorAlt: '#221C18', wallDark: '#0A0809', wallLight: '#26201C',
        bg: 'radial-gradient(circle at 60% 20%, #3A1E0A 0%, #141012 45%, #060506 100%)', ambient: 'forge',
        decor: ['rubble', 'slag'],
        paAnnouncements: [
          'Caution. Thermal exhaust venting in sectors four, five and nine.',
          'The Titan Manifold is offline. Do not attempt to restart it.',
          'Slag Line freight service is suspended until further notice.',
          'Coolant reserves at eleven percent. This is a recording.',
        ],
        doors: [
          { x: 0, y: 10, targetZone: 'bador_manufacturing', targetPos: { x: 42, y: 16 }, label: 'Slag Line to Manufacturing', requiresFlag: 'manifest_restarted', lockedMessage: 'The Slag Line gate is dead until the Titan Manifold runs.' },
          { x: 0, y: 11, targetZone: 'bador_manufacturing', targetPos: { x: 42, y: 17 }, label: 'Slag Line to Manufacturing', requiresFlag: 'manifest_restarted', lockedMessage: 'The Slag Line gate is dead until the Titan Manifold runs.' },
          { x: 24, y: 0, targetZone: 'city_outskirts_bador', targetPos: { x: 24, y: 30 }, label: 'Depot Blast Doors' },
          { x: 25, y: 0, targetZone: 'city_outskirts_bador', targetPos: { x: 25, y: 30 }, label: 'Depot Blast Doors' },
          { x: 40, y: 0, targetZone: 'outpost_7', targetPos: { x: 22, y: 30 }, label: 'Sundown Gateway', requiresFlag: 'sundown_complete', lockedMessage: 'The Sundown Gateway is sealed from this side.' },
          { x: 41, y: 0, targetZone: 'outpost_7', targetPos: { x: 23, y: 30 }, label: 'Sundown Gateway', requiresFlag: 'sundown_complete', lockedMessage: 'The Sundown Gateway is sealed from this side.' },
          { x: 47, y: 21, targetZone: 'deep_caverns', targetPos: { x: 1, y: 14 }, label: 'Zone Five Descent', requiresFlag: 'zone05_gate_open', lockedMessage: 'Shut behind the central blast doors, heat locked until the manifold runs.' },
          { x: 47, y: 22, targetZone: 'deep_caverns', targetPos: { x: 1, y: 15 }, label: 'Zone Five Descent', requiresFlag: 'zone05_gate_open', lockedMessage: 'Shut behind the central blast doors, heat locked until the manifold runs.' },
        ],
        worldObjects: [
          { id: 'wastes_vista_w', propArt: 'basalt_vista', propVariant: 'wastes_a', x: 12, y: 5, once: false, iconKind: 'vista', label: 'The Titan Manifold',
            description: "Soot choked sky and giant rusting pistons beating like dying hearts. Furnace glow lights the haze from below, and somewhere a bell rings, flat and slow.", grantsCodex: 'codex-engine-wastes' },
          { id: 'wastes_vista_e', propArt: 'basalt_vista', propVariant: 'wastes_b', x: 36, y: 5, once: false, iconKind: 'vista', label: 'Silent Shear Skyline',
            description: "Mega engine blocks stand cracked, venting white steam into a copper sky. High in the north wall a blast door glows orange: the way back to the Outskirts.", grantsCodex: 'codex-engine-wastes' },
          { id: 'ion_storm_capacitor', propArt: 'capacitor_bank', x: 44, y: 8, label: 'Ion Storm Capacitor',
            description: "Five amber capacitor cells, still cycling, cyan arcs jumping between terminals. A stencil reads DISCHARGE: NOT NATURAL. It jams sensors for six kilometers.",
            once: true, requiresFlag: 'ghost_keel_active', lockedMessage: 'A jamming bank. Commander Vael might know why it matters.',
            requiresNoneFlags: ['jammer_found'],
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 5 },
            grantsFlag: 'jammer_found', grantsCodex: 'codex-wst-jammer', thermalDeltaOnSuccess: -30 },
          { id: 'ion_capacitor_dead', propArt: 'capacitor_bank', propVariant: 'dead', x: 44, y: 8, label: 'Dead Capacitor Bank',
            description: "The arcs die and the cells go dark. Your signature fades. You pull the main cell, a heavy amber core, cold at last.",
            once: true, requiresFlag: 'jammer_found', invisibleLocked: true, grantsItem: 'wst_capacitor_cell' },
          { id: 'scan_team_wreckage', propArt: 'scan_wreck', x: 44, y: 15, label: 'KDY Scan Team Wreckage',
            description: "A cracked glass cab and a steel scan truck, dish mast tilted, a note reading WARNED OFF. Seven tags hang from the mast, wrapped in repair tape and names. You take them home.",
            once: true, requiresFlag: 'vael_briefed', lockedMessage: 'A wrecked truck. You do not know what you are looking at yet.',
            grantsFlag: 'scan_team_fate_known', grantsItem: 'wst_scan_team_tags', grantsCodex: 'codex-wst-scan-team' },
          { id: 'glassed_crater_lake', propArt: 'crater_glass', x: 6, y: 9, once: false, label: 'Glassed Crater Lake',
            description: "Fused silica glass cracked in a starburst, an orange glow under the center. A stencil reads THE GROUND REMEMBERS.", grantsCodex: 'codex-wst-crater-lake' },
          { id: 'foundry_hatch_obj', propArt: 'foundry_hatch', x: 26, y: 12, once: false, iconKind: 'terminal', label: 'Combustion Foundry 09',
            description: "A combustion chamber hung over a dormant slag basin, blue lamps behind heavy blast doors. A sign reads FOUNDRY 09, NO SPARKS, NO SERMONS. The one safe place in the wastes.", grantsCodex: 'codex-wst-foundry09' },
          { id: 'bench_foundry', propArt: 'robotics_bench', x: 21, y: 15, once: false, iconKind: 'crate', label: 'Salvage Workbench',
            description: "A steel bench of valve cores, melted gaskets and a half rebuilt spanner. Welding goggles hang on a hook, still warm." },
          { id: 'coolant_diag', propArt: 'fuel_diag_terminal', x: 33, y: 15, once: false, iconKind: 'crate', label: 'Coolant Diagnostics',
            description: "A patched terminal wired to the coolant lines, readouts bouncing between SAFE and CRITICAL. Tape reads: IT IS NEVER SAFE." },
          { id: 'piston_tower_1', propArt: 'piston_tower', x: 8, y: 18, once: false, iconKind: 'crate', label: 'Titan Piston',
            description: "A colossal brass piston, still beating. Each slam shudders the ground." },
          { id: 'piston_tower_2', propArt: 'piston_tower', x: 30, y: 22, once: false, iconKind: 'crate', label: 'Titan Piston',
            description: "A colossal brass piston, still beating. Each slam shudders the ground." },
          { id: 'piston_tower_3', propArt: 'piston_tower', x: 20, y: 31, once: false, iconKind: 'crate', label: 'Titan Piston',
            description: "A colossal brass piston, still beating. Each slam shudders the ground." },
          { id: 'boiler_hull_a', propArt: 'boiler_hull', x: 28, y: 28, once: false, iconKind: 'crate', label: 'Dead Boiler Hull',
            description: "A burst boiler hull glowing orange through the cracks. The nameplate is scratched out and replaced with NO." },
          { id: 'steam_vent_1', propArt: 'steam_vent_pipe', x: 21, y: 22, once: false, iconKind: 'crate', label: 'Steam Vent Pipe',
            description: "A copper pipe that hisses, then vents a three tile jet of scalding steam. Scanners notice.", thermalDelta: 6 },
          { id: 'steam_vent_2', propArt: 'steam_vent_pipe', x: 28, y: 25, once: false, iconKind: 'crate', label: 'Steam Vent Pipe',
            description: "A copper pipe that hisses, then vents a three tile jet of scalding steam. Scanners notice.", thermalDelta: 6 },
          { id: 'steam_vent_3', propArt: 'steam_vent_pipe', x: 24, y: 30, once: false, iconKind: 'crate', label: 'Steam Vent Pipe',
            description: "A copper pipe that hisses, then vents a three tile jet of scalding steam. Scanners notice.", thermalDelta: 6 },
          { id: 'steam_vent_4', propArt: 'steam_vent_pipe', x: 36, y: 20, once: false, iconKind: 'crate', label: 'Steam Vent Pipe',
            description: "A copper pipe that hisses, then vents a three tile jet of scalding steam. Scanners notice.", thermalDelta: 6 },
          { id: 'valve_a', propArt: 'valve_wheel', propVariant: 'closed', x: 4, y: 23, once: true, iconKind: 'terminal', label: 'Pressure Valve Wheel (A)',
            description: "A brass valve wheel with three coolant crystals in its spokes, stuck at a quarter turn.", requiresFlag: 'vance_met', lockedMessage: 'Lockwired. Vance Crankshaft knows the sequence.', requiresNoneFlags: ['valve_a_done'], triggersMinigame: 'valve_override', grantsFlag: 'valve_a_done', thermalDeltaOnSuccess: 3 },
          { id: 'valve_a_open', propArt: 'valve_wheel', propVariant: 'open', x: 4, y: 23, once: false, iconKind: 'terminal', label: 'Pressure Valve Wheel (A, Open)',
            description: "The wheel spins freely. The pipe beyond is empty and cooling.", requiresFlag: 'valve_a_done', invisibleLocked: true },
          { id: 'valve_b', propArt: 'valve_wheel', propVariant: 'closed', x: 11, y: 25, once: true, iconKind: 'terminal', label: 'Pressure Valve Wheel (B)',
            description: "A brass valve wheel with three coolant crystals in its spokes, stuck at a quarter turn.", requiresFlag: 'vance_met', lockedMessage: 'Lockwired. Vance Crankshaft knows the sequence.', requiresNoneFlags: ['valve_b_done'], triggersMinigame: 'valve_override', grantsFlag: 'valve_b_done', thermalDeltaOnSuccess: 3 },
          { id: 'valve_b_open', propArt: 'valve_wheel', propVariant: 'open', x: 11, y: 25, once: false, iconKind: 'terminal', label: 'Pressure Valve Wheel (B, Open)',
            description: "The wheel spins freely. The pipe beyond is empty and cooling.", requiresFlag: 'valve_b_done', invisibleLocked: true },
          { id: 'valve_c', propArt: 'valve_wheel', propVariant: 'closed', x: 5, y: 31, once: true, iconKind: 'terminal', label: 'Pressure Valve Wheel (C)',
            description: "A brass valve wheel with three coolant crystals in its spokes, stuck at a quarter turn.", requiresFlag: 'vance_met', lockedMessage: 'Lockwired. Vance Crankshaft knows the sequence.', requiresNoneFlags: ['valve_c_done'], triggersMinigame: 'valve_override', grantsFlag: 'valve_c_done', thermalDeltaOnSuccess: 3 },
          { id: 'valve_c_open', propArt: 'valve_wheel', propVariant: 'open', x: 5, y: 31, once: false, iconKind: 'terminal', label: 'Pressure Valve Wheel (C, Open)',
            description: "The wheel spins freely. The pipe beyond is empty and cooling.", requiresFlag: 'valve_c_done', invisibleLocked: true },
          { id: 'boiler_core', propArt: 'crate_stack', propVariant: 'kdy', x: 11, y: 32, once: true, iconKind: 'crate', label: 'Boiler Core Vault',
            description: "The drained vault holds the Catalyst Governor Valve in oilcloth: a brass wheel with three blue crystals and gear teeth along its rim.", requiresAllFlags: ['valve_a_done', 'valve_b_done', 'valve_c_done'], lockedMessage: 'Pressure locked. Open all three valve wheels.', grantsItem: 'catalyst_governor_valve', grantsFlag: 'governor_valve_acquired' },
          { id: 'black_box_04', propArt: 'console', propVariant: 'derelict', x: 3, y: 26, once: true, iconKind: 'archive', label: 'Black Box Audio Cylinder 04',
            description: "A battered audio cylinder wedged between boiler plates, stamped Shear Day plus three. Static cuts the engineer's voice twice.", requiresFlag: 'vance_met', lockedMessage: 'Meaningless for now. Vance Crankshaft might recognize it.', grantsFlag: 'black_box_found', grantsCodex: 'codex-black-box-04' },
          { id: 'coolant_crystals', propArt: 'crate_stack', propVariant: 'kdy', x: 12, y: 22, once: true, iconKind: 'crate', label: 'Coolant Crystal Cluster',
            description: "Blue coolant crystals along a cracked pipe, glowing faintly. Ground to powder, they insulate against heat.", grantsItem: 'thermal_crystal_dust' },
          { id: 'exhaust_cathedral_obj', propArt: 'exhaust_cathedral', x: 40, y: 29, once: false, iconKind: 'vista', label: 'The Exhaust Cathedral',
            description: "A 500 meter exhaust cylinder, triple piston assemblies cycling up its walls like organ pipes, an ember orange arch at its base. The Governor socket waits at the apex.", grantsCodex: 'codex-wst-cathedral' },
          { id: 'unchained_overseer_obj', propArt: 'piston_tower', x: 35, y: 32, once: true, iconKind: 'floor', label: 'The Unchained Overseer',
            description: "A corrupted defense titan wound in snapped safety chains, its lens one cold blue point. Pile drivers draw back as you approach.", requiresFlag: 'vance_met', lockedMessage: 'Heat locked. Speak to Vance Crankshaft first.', triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'unchained_overseer', flavorText: 'The Overseer rips its chains free and the cathedral shakes. There is no way around it, only through.' }, grantsFlag: 'overseer_defeated', thermalDeltaOnSuccess: 10 },
          { id: 'governor_socket_empty', propArt: 'governor_socket', propVariant: 'empty', x: 44, y: 31, once: false, iconKind: 'terminal', label: 'Governor Valve Socket',
            description: "A brass cradle with three empty crystal sockets and a red lamp. Seat the Governor Valve and the manifold exhales. The Black Box plays on every speaker: starved on purpose.", requiresFlag: 'overseer_defeated', lockedMessage: 'The apex is guarded. Deal with the Overseer first.', requires: { item: 'catalyst_governor_valve' }, requiresNoneFlags: ['manifest_restarted'], grantsFlags: ['manifest_restarted', 'engine_core_unlocked', 'vance_truth_known'], grantsCodex: 'codex-black-box-04', thermalDelta: -25 },
          { id: 'governor_socket_installed', propArt: 'governor_socket', propVariant: 'installed', x: 44, y: 31, once: false, iconKind: 'terminal', label: 'Governor Valve (Installed)',
            description: "The Governor Valve sits in its cradle, three crystals a steady cyan. The console reads: ALL SYSTEMS NOMINAL.", requiresFlag: 'manifest_restarted', invisibleLocked: true },
          { id: 'central_core_blast_doors', propArt: 'wst_blast_doors', propVariant: 'closed', x: 46, y: 20, once: true, iconKind: 'floor', label: 'Central Core Blast Doors',
            description: "The clamps release with a long hiss and the ten meter doors grind apart. The Zone Five Descent stands open, warm air rising. Far west the Slag Line wakes, the shortcut to Manufacturing alive again. A strongbox drops free: 800 credits.", requiresFlag: 'engine_core_unlocked', lockedMessage: 'Heat locked. The manifold has to run first.', requiresNoneFlags: ['zone05_gate_open'], grantsFlag: 'zone05_gate_open', grantsCredits: 800 },
          { id: 'central_core_blast_doors_open', propArt: 'wst_blast_doors', propVariant: 'open', x: 46, y: 20, once: false, iconKind: 'floor', label: 'Central Core Blast Doors (Open)',
            description: "The ten meter doors stand retracted. Beyond, the Zone Five Descent falls away in lit steps. The Slag Line shortcut is live.", requiresFlag: 'zone05_gate_open', invisibleLocked: true },
          { id: 'wastes_shrine', propArt: 'crate_stack', propVariant: 'hidden', x: 3, y: 16, once: false, iconKind: 'crate', label: 'Scrap Seer Shrine',
            description: "A shrine of copper wire and canvas hung with tuning forks. A tin cup holds old credits. A tiny bell rings when the pistons slam.", grantsFlag: 'wastes_shrine_visited', grantsCodex: 'codex-wst-omens' },
        ],
        npcs: [
          { id: 'gharza', x: 38, y: 10, kind: 'trandoshan_sniper', label: 'Gharza',
            questNpc: true, repeatable: true,
            hideAfterFlags: ['gharza_dead', 'gharza_recruited'],
            phases: [
              {
                id: 'phase_gharza_intro',
                prompt: "A Trandoshan sniper, utterly still, rifle steady. \"You are not KDY standard issue. Then you want what I want, or you will complicate my work.\"",
                repeatPrompt: 'Gharza does not lower her rifle. "Move slowly."',
                choices: [
                  { text: 'Ask what she is hunting.', morality: 0, loyalty: {}, result: "\"A dead engineer's work, deep in the caverns.\" The rifle drops three degrees. \"Tanner called it a retrieval. The coordinates were signed by a ghost.\"", grants: { flags: ['gharza_negotiated', 'gharza_mission_disclosed'], codex: ['codex-wst-gharza-hunt'] } },
                  { text: 'Say you work the same contract.', morality: 0, loyalty: {}, result: "\"Then Tanner is playing games.\" The rifle drops. \"I cannot kill the caverns' sentinels from range. Take my key. The idle one will sleep for you.\"", grants: { flags: ['gharza_negotiated', 'gharza_allied'], items: ['wst_gharza_service_key'] } },
                  { text: 'Offer to pay her to stand down.', morality: -5, loyalty: { underworld: 8 }, result: "\"I take information, not buyouts.\" She does not raise the rifle. \"Who hired you?\"", grants: { flags: ['gharza_negotiated'] } },
                  { text: '[Draw on her before she turns.]', morality: -10, loyalty: { underworld: 5 }, result: 'Your hand moves first. Gharza is already rolling behind a piston housing.', triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'wst_gharza', flavorText: 'Gharza owns every sightline. Piston housings are the only cover. Close the distance or she dismantles you from range.' }, onSuccessFlags: ['gharza_dead'] },
                ],
              },
              {
                id: 'phase_gharza_wary',
                requiresAllFlags: ['gharza_negotiated'],
                requiresNoneFlags: ['gharza_allied'],
                prompt: 'Gharza watches the valley, not you. "Tell me something true."',
                repeatPrompt: '"Something true?"',
                choices: [
                  { text: 'Tell her Tanner hired you for the Ghost Keel.', morality: 0, loyalty: {}, result: "\"Tanner hires me for ghosts too.\" A dry hiss. \"Take my key. The idle sentinel in the caverns answers to it.\"", grants: { flags: ['gharza_allied'], items: ['wst_gharza_service_key'] } },
                  { text: 'Say nothing and walk away.', morality: 0, loyalty: {}, result: '"Then we remain strangers."' },
                ],
              },
              {
                id: 'phase_gharza_allied',
                requiresAllFlags: ['gharza_allied'],
                prompt: '"The key opens the idle sentinel in the Deep Caverns." She cleans her rifle without looking up. "After that, the hunt is yours or mine."',
                repeatPrompt: '"The caverns, then."',
                choices: [
                  { text: 'Offer her a place in the Syndicate.', morality: 0, loyalty: { underworld: 6 }, result: "\"A hunter needs a client who pays.\" She shoulders the rifle. \"I take no orders from anyone who flinches.\"", grants: { flags: ['gharza_recruited'] } },
                ],
              },
            ],
          },
          { id: 'vance_crankshaft', x: 23, y: 14, kind: 'mechanic', label: 'Chief Specialist Vance Crankshaft',
            questNpc: true,
            repeatable: true,
            phases: [
              {
                id: 'phase_vance_intro',
                requiresNoneFlags: ['vance_met'],
                prompt: "\"Hands where I can see them.\" A man in a leather apron pushes up his welding mask, hydraulic arm hissing. \"Vance Crankshaft, chief engineer of the Titan. Do not ask what happened.\"",
                repeatPrompt: "Vance does not look up. \"Ask when you have thought about it.\"",
                choices: [
                  { text: "What happened to the engine?", morality: 0, loyalty: {}, result: "\"Everyone asks.\" He wipes his gauge. \"The conduits cut. They said age. It was not age. Help me and I will show you proof.\"", grants: { flags: ['vance_met'], codex: ['codex-engine-wastes'] } },
                  { text: "I want the Governor Valve.", morality: 0, loyalty: {}, result: "\"You know more than most. It is sealed in the Dead Boiler Matrix, behind three pressure locks. I can walk you through them.\"", grants: { flags: ['vance_met'] } },
                ],
              },
              {
                id: 'phase_vance_work',
                requiresAllFlags: ['vance_met'],
                requiresNoneFlags: ['governor_valve_acquired'],
                prompt: "\"The Matrix is southwest, across the slag bridge. Three wheels, one vault. Do not touch the black box bare handed. It still hums.\"",
                repeatPrompt: "\"Wheels, then vault, then cathedral.\"",
                choices: [
                  { text: "How do I survive the heat?", morality: 0, loyalty: {}, result: "\"A thermal vest. I need crystal dust, ground fine. There is a cluster in the Matrix, and Dessa sells it.\"", grants: { flags: ['vance_gear_hint'] } },
                  { text: "[Craft] Here is the Thermal Crystal Dust. Make me a vest.", morality: 0, loyalty: {}, result: "A flash of blowtorch, a clamp of brass, and he hands over a crystal lined vest. \"You can walk the wastes without cooking. Mostly.\"", grants: { flags: ['thermal_gear_crafted'], items: ['item_thermal_gear_01'], thermal: -10 }, requires: { item: 'thermal_crystal_dust' } },
                  WST_VANCE_BOX_CHOICE,
                ],
              },
              {
                id: 'phase_vance_geared',
                requiresAllFlags: ['vance_met', 'thermal_gear_crafted'],
                requiresNoneFlags: ['governor_valve_acquired'],
                prompt: "\"The vest suits you. The Matrix is southwest.\"",
                repeatPrompt: "\"Wheels, then vault, then cathedral.\"",
                choices: [
                  { text: "On my way.", morality: 0, loyalty: {}, result: "\"Mind the black box.\"" },
                  WST_VANCE_BOX_CHOICE,
                ],
              },
              {
                id: 'phase_vance_valve',
                requiresAllFlags: ['governor_valve_acquired'],
                requiresNoneFlags: ['overseer_defeated'],
                prompt: "\"You have it?\" His voice cracks. \"Then the cathedral. The Overseer waits at its base. The valve goes in the apex socket.\"",
                repeatPrompt: "\"The Overseer, then the socket.\"",
                choices: [
                  { text: "I will take the Overseer down.", morality: 0, loyalty: {}, result: "\"Mind the chains. They snap.\"" },
                  WST_VANCE_BOX_CHOICE,
                ],
              },
              {
                id: 'phase_vance_install',
                requiresAllFlags: ['overseer_defeated'],
                requiresNoneFlags: ['manifest_restarted'],
                prompt: "\"The Overseer is down. The socket is at the apex. Go.\"",
                repeatPrompt: "\"The socket, friend.\"",
                choices: [
                  { text: "On my way.", morality: 0, loyalty: {}, result: "He grips his spanner until his knuckles whiten." },
                  WST_VANCE_BOX_CHOICE,
                ],
              },
              {
                id: 'phase_vance_done',
                requiresAllFlags: ['manifest_restarted'],
                requiresNoneFlags: ['wst_vance_paid'],
                prompt: "\"I feel it from here. The rhythm is back.\" Vance leans on the bench, the cynicism slipping. \"Somebody ordered this, and the order came from below.\"",
                repeatPrompt: "\"The blast doors answer to the manifold now.\"",
                choices: [
                  { text: "Where below?", morality: 0, loyalty: {}, result: "\"Zone Five, behind the central blast doors. Go and open them.\" He presses his old spanner into your hand. \"Mind the dark.\"", grants: { flags: ['vance_truth_known', 'wst_vance_paid'], codex: ['codex-wst-vance-truth'], items: ['wst_vance_spanner'], credits: 300 } },
                ],
              },
              {
                id: 'phase_vance_after',
                requiresAllFlags: ['wst_vance_paid'],
                prompt: "\"Zone Five is open if you want it. I will keep the lamps lit.\"",
                repeatPrompt: "\"Mind the dark down there.\"",
                choices: [
                  { text: "Keep the pistons beating.", morality: 0, loyalty: {}, result: "\"It is the only job I have left.\"" },
                ],
              },
            ],
          },
          { id: 'scrap_seer_lora', x: 30, y: 14, kind: 'archivist', label: 'Scrap-Seer Lora',
            questNpc: true,
            repeatable: true,
            phases: [
              {
                id: 'phase_lora_a',
                requiresNoneFlags: ['lora_met'],
                prompt: "A woman hovers above the floor, a brass resonator mask for a face. It turns vibration into voice. \"I am Lora. I tune the Titan. It is disquiet.\"",
                repeatPrompt: "The mask turns toward you, silent.",
                choices: [
                  { text: "What does the Titan say?", morality: 0, loyalty: {}, result: "\"That the Shear was no accident. That someone still listens.\" The mask tilts. \"What it lost lies in the Matrix, below.\"", grants: { flags: ['lora_met'] } },
                  { text: "Who do you tune for?", morality: 0, loyalty: {}, result: "\"The dead. A shift that never clocked out.\" A copper hand rises. \"Ring the bell at my shrine, and hear them.\"", grants: { flags: ['lora_met'], codex: ['codex-wst-omens'] } },
                ],
              },
              {
                id: 'phase_lora_b',
                requiresAllFlags: ['lora_met'],
                requiresNoneFlags: ['manifest_restarted'],
                prompt: "\"The Titan hums higher when you are near. It knows you carry something of its own.\"",
                repeatPrompt: "\"Listen.\"",
                choices: [
                  { text: "Where do the dead lie?", morality: 0, loyalty: {}, result: "\"East, where the dust is flat. Seven went to hear the storm. Their lamp is still lit.\"" },
                ],
              },
              {
                id: 'phase_lora_c',
                requiresAllFlags: ['manifest_restarted'],
                prompt: "\"It is quiet now. Thank you, tuner.\" The hammer strikes once, clean and pure.",
                repeatPrompt: "\"Listen.\"",
                choices: [
                  { text: "What will you do now?", morality: 0, loyalty: {}, result: "\"Listen to the silence.\" The mask turns east. \"Something below was waiting for the Titan to wake. Go gently.\"", grants: { codex: ['codex-engine-wastes'] } },
                ],
              },
              {
                id: 'phase_lora_shrine',
                requiresAllFlags: ['lora_met', 'wastes_shrine_visited'],
                requiresNoneFlags: ['wst_lora_blessed'],
                prompt: "The mask turns at once. \"You rang the bell. It sounded true.\" A copper hand takes your sleeve and hums.",
                repeatPrompt: "\"The bell remembers you.\"",
                choices: [
                  { text: "Let her tune you.", morality: 0, loyalty: {}, result: "\"There.\" Your plating hums in the Titan's key and the heat scanners lose interest. She presses a brass fork into your palm.", grants: { flags: ['wst_lora_blessed'], items: ['wst_tuning_fork'], thermal: -12 } },
                ],
              },
            ],
          },
          { id: 'foundry_quartermaster', x: 18, y: 14, kind: 'zabrak_techwright', label: 'Quartermaster Dessa',
            isVendor: true, vendorFaction: 'underworld', repeatable: true,
            vendorStock: ['pressurized_igniter', 'thermal_crystal_dust', 'plasma_grenades', 'field_rations'],
            vendorGreeting: 'Dessa weighs a crystal on a pocket scale. "Foundry prices. Vests take dust, and dust takes a steady hand."',
            repeatPrompt: 'Dessa wipes her hands on her apron and waits.',
            prompt: '"Foundry prices, soldier. No haggling, no sparks."',
            choices: [] },
          { id: 'scavenger_1', x: 19, y: 18, wander: 2, kind: 'nautolan_engineer', label: 'Scavenger Engineer',
            repeatable: true,
            phases: [
              {
                id: 'phase_x',
                prompt: "The Nautolan does not look up from a valve. \"Older than the city, these. Nobody knows who turned them last.\"",
                repeatPrompt: "They are busy.",
                choices: [
                  { text: "Leave them to it.", morality: 0, loyalty: {}, result: "A wary nod." },
                ],
              },
            ],
          },
          { id: 'scavenger_2', x: 32, y: 18, wander: 2, kind: 'zero_g_welder', label: 'Exiled Mechanic',
            repeatable: true,
            phases: [
              {
                id: 'phase_x',
                prompt: "The exile mutters over a cracked weld. \"Keep your voice down near the cathedral. It listens.\"",
                repeatPrompt: "They are busy.",
                choices: [
                  { text: "Leave them to it.", morality: 0, loyalty: {}, result: "A wary nod." },
                ],
              },
            ],
          },
          { id: 'renegade_droid', x: 26, y: 18, wander: 2, kind: 'droid', label: 'Renegade Automaton',
            repeatable: true,
            phases: [
              {
                id: 'phase_x',
                prompt: "The droid's optics dim and brighten. \"I severed my link to the overmind. The silence is not restful. It sleeps below.\"",
                repeatPrompt: "It is busy.",
                choices: [
                  { text: "Leave it to its silence.", morality: 0, loyalty: {}, result: "Its optics dip in something like thanks." },
                ],
              },
            ],
          },
        ],
        collectibles: [{ id: 'wastes_salvage', x: 3, y: 12, label: 'KDY Engine Debris Component', reward: 400 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 24, 0, 25, 5, 'floor');
          carveRect(g, 40, 0, 41, 5, 'floor');
          carveRect(g, 1, 5, 46, 34, 'floor');
          // slag river with two bridges
          for (let y = 6; y <= 33; y++) { if ((y >= 10 && y <= 12) || (y >= 26 && y <= 28)) continue; pt(g, 15, y, 'lava'); pt(g, 16, y, 'lava'); }
          // Combustion Foundry 09 facade (north center)
          carveRect(g, 20, 6, 32, 11, 'wall');
          // Dead Boiler Matrix (southwest): walled complex with inner maze walls
          carveRect(g, 1, 19, 14, 34, 'wall'); carveRect(g, 2, 20, 13, 33, 'floor');
          carveRect(g, 6, 20, 6, 26, 'wall'); pt(g, 6, 24, 'floor');
          carveRect(g, 9, 27, 9, 33, 'wall'); pt(g, 9, 30, 'floor');
          pt(g, 14, 27, 'floor');
          // Exhaust Cathedral massif (southeast)
          carveRect(g, 35, 22, 44, 28, 'wall');
          pt(g, 24, 0, 'door'); pt(g, 25, 0, 'door');
          pt(g, 40, 0, 'door'); pt(g, 41, 0, 'door');
          pt(g, 0, 10, 'door'); pt(g, 0, 11, 'door');
          pt(g, 47, 21, 'door'); pt(g, 47, 22, 'door');
          return g;
        },
      },

      outpost_7: {
        id: 'outpost_7', name: 'Outpost 7',
        subtitle: 'Bador Moon · The Iron Trench · KDY Black Site',
        width: 48, height: 32, spawnPos: { x: 2, y: 14 }, textureId: 'ferrowake',
        accent: '#00E5FF', accentGlow: 'rgba(0,229,255,0.20)', accentDim: '#1A3A40',
        floorColor: '#16181B', floorAlt: '#1E2124', wallDark: '#0A0A0D', wallLight: '#262A30',
        bg: 'radial-gradient(circle at 50% 10%, #12262A 0%, #0A0F12 50%, #040607 100%)', ambient: 'basalt_mist',
        decor: ['pipe', 'cargo_crate', 'rubble'],
        paAnnouncements: [
          'Outpost 7 does not exist. If you can read this, do not discuss it.',
          'Thermal discipline. Repeat, thermal discipline. Suppress all unnecessary signature.',
          'Resupply is overdue. Rations remain at two thirds.',
          'Pathfinders report a patrol shift on the north ridge. Hold fire.',
        ],
        doors: [
          { x: 0, y: 14, targetZone: 'city_outskirts_bador', targetPos: { x: 46, y: 14 }, label: 'City Outskirts' },
          { x: 0, y: 15, targetZone: 'city_outskirts_bador', targetPos: { x: 46, y: 15 }, label: 'City Outskirts' },
          { x: 22, y: 31, targetZone: 'engine_wastes', targetPos: { x: 40, y: 1 }, label: 'Gateway to the Engine Wastes', requiresFlag: 'sundown_complete', lockedMessage: 'The gateway is sealed. Operation Sundown has not cleared it.' },
          { x: 23, y: 31, targetZone: 'engine_wastes', targetPos: { x: 41, y: 1 }, label: 'Gateway to the Engine Wastes', requiresFlag: 'sundown_complete', lockedMessage: 'The gateway is sealed. Operation Sundown has not cleared it.' },
        ],
        worldObjects: [
          { id: 'o7_vista_w', propArt: 'basalt_vista', propVariant: 'ravine_a', x: 12, y: 5, once: false, iconKind: 'vista', label: 'The Iron Trench Ravine',
            description: "A basalt ravine cut deep into hostile ground. Dark spires rise out of rolling teal fog under a thin crescent moon, camouflage mesh sagging between the cliffs.", grantsCodex: 'codex-iron-trench' },
          { id: 'o7_vista_e', propArt: 'basalt_vista', propVariant: 'ravine_b', x: 36, y: 5, once: false, iconKind: 'vista', label: 'Basalt Spires and Sensor Fog',
            description: "East the ravine narrows into a gorge of black columns. Geothermal steam rises in pale ribbons, and distant turbolaser fire thuds through the rock." },
          { id: 'outpost7_holoboard', propArt: 'board', propVariant: 'resistance', x: 12, y: 28, label: 'Resistance Planning Board',
            description: 'Hand drawn cavern maps pinned beside stolen KDY grid data. Someone has planned this longer than Tanner implied.',
            once: true, requiresFlag: 'ghost_keel_active',
            grantsFlag: 'cavern_maps_found', grantsCodex: 'codex-outpost7-resistance' },
          { id: 'outpost7_armory', propArt: 'crate_stack', propVariant: 'resist', x: 3, y: 11, label: 'Hidden Arms Cache',
            description: 'Pre-war blasters, bacta packs and a cracked slicer spike. Someone prepared for a long stay.',
            once: true, grantsFlag: 'outpost7_armory_raided', grantsItem: 'front_resistance_kit' },
          { id: 'outpost7_rebreather', propArt: 'rebreather_rack', x: 3, y: 9, label: 'Rebreather Station',
            description: 'Emergency gear mixed for the nitrogen heavy air of the deep caverns.',
            once: true, grantsFlag: 'rebreather_equipped', grantsItem: 'front_cavern_rebreather' },
          { id: 'o7_camo', propArt: 'camo_netting', x: 24, y: 9, once: false, iconKind: 'vista', label: 'Camouflage Netting',
            description: "Camouflage mesh over LZ Delta, moisture dripping onto the wet durasteel pad." },
          { id: 'o7_drop_speeder', propArt: 'drop_speeder', x: 19, y: 11, once: false, iconKind: 'crate', label: 'KDY V-19 Drop Speeder',
            description: "A V-19 drop speeder with its engine housing open, an arc welder dripping orange sparks. The keys are in the ignition." },
          { id: 'o7_weapon_crate', propArt: 'crate_stack', propVariant: 'kdy', x: 28, y: 11, once: false, iconKind: 'crate', label: 'Weapon Reload Crate',
            description: "Power cells, grenade pods and heavy cable, sized for a heavy trooper.", thermalDelta: 0 },
          { id: 'o7_barricade_a', propArt: 'repulsor_barricade', x: 8, y: 16, once: false, iconKind: 'crate', label: 'Modular Repulsor Barricade',
            description: "Pitted plates on glowing cyan repulsor bases." },
          { id: 'o7_barricade_b', propArt: 'repulsor_barricade', x: 14, y: 16, once: false, iconKind: 'crate', label: 'Modular Repulsor Barricade',
            description: "Shrapnel scored plates with a tally of marks scratched on top." },
          { id: 'o7_barricade_c', propArt: 'repulsor_barricade', x: 34, y: 16, once: false, iconKind: 'crate', label: 'Modular Repulsor Barricade',
            description: "One repulsor base glows a slightly different cyan, a salvaged replacement." },
          { id: 'o7_barricade_d', propArt: 'repulsor_barricade', x: 40, y: 16, once: false, iconKind: 'crate', label: 'Modular Repulsor Barricade',
            description: "Thicker plates and a hand painted arrow pointing west: HOME." },
          { id: 'o7_eweb', propArt: 'eweb_turret', propVariant: 'plain', x: 20, y: 15, once: false, iconKind: 'crate', label: 'Trench E-Web Emplacement',
            description: "An E-Web heavy repeater with a heat gauge, sights on the barricade gap. A power coupling waits for a source.", requiresNoneFlags: ['o7_socket_linked'] },
          { id: 'o7_eweb_linked', propArt: 'eweb_turret', propVariant: 'linked', x: 20, y: 15, once: false, iconKind: 'crate', label: 'Trench E-Web Emplacement (Overclocked)',
            description: "Cyan current runs through the coupling and the gauge reads times four. It will burn out in a hundred seconds.", requiresFlag: 'o7_socket_linked', invisibleLocked: true },
          { id: 'o7_exo_socket', propArt: 'exo_socket', x: 22, y: 17, once: true, iconKind: 'terminal', label: 'Exo-Suit Charging Socket',
            description: "A charging socket on the trench power bus, readout POWER LINKED. It drains your shield recharge and feeds the nearest E-Web.", grantsFlag: 'o7_socket_linked', thermalDelta: 4 },
          { id: 'o7_steam_a', propArt: 'steam_grate', x: 12, y: 18, once: true, iconKind: 'crate', label: 'Trench Drain Grate',
            description: "A drain grate with a yellow warning decal, venting steam that masks you from thermal scanners.", thermalDelta: -4 },
          { id: 'o7_steam_b', propArt: 'steam_grate', x: 36, y: 19, once: true, iconKind: 'crate', label: 'Trench Drain Grate',
            description: "Another drain venting geothermal steam, the metal warm to the touch.", thermalDelta: -4 },
          { id: 'o7_catwalk', propArt: 'catwalk_post', x: 30, y: 18, once: false, iconKind: 'crate', label: 'Overhead Trench Beam',
            description: "A beam over the trench rigged as a sniper perch, a rifle on the rail and a micro recon drone charging." },
          { id: 'sundown_s1_hard', propArt: 'repulsor_barricade', propVariant: 'stage', x: 26, y: 17, once: true, iconKind: 'floor', label: 'Trench Breach Point',
            description: "The center of the Iron Trench. Red mortar markers paint the floor and KEF shock breachers charge behind thermal shields.", requiresFlag: 'sundown_started', lockedMessage: 'The trench is quiet. Operation Sundown has not begun.', requiresNoneFlags: ['o7_socket_linked', 'sundown_s1_done'], triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_breach_wave', flavorText: 'The alarm sounds, 110 decibels off the basalt. Six shock breachers and four jump troopers hit the line together. The E-Web has no extra power.' }, grantsFlag: 'sundown_s1_done', thermalDeltaOnSuccess: 8 },
          { id: 'sundown_s1_easy', propArt: 'repulsor_barricade', propVariant: 'stage', x: 26, y: 17, once: true, iconKind: 'floor', label: 'Trench Breach Point (E-Web Online)',
            description: "The overclocked E-Web throws a wall of cyan plasma down the trench. KEF breachers hit the line and fall back burning.", requiresAllFlags: ['sundown_started', 'o7_socket_linked'], requiresNoneFlags: ['sundown_s1_done'], invisibleLocked: true, triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_shock_breacher', flavorText: 'The E-Web howls and pins the breachers against their own shields. You step into the gap and finish what the cannon started.' }, grantsFlag: 'sundown_s1_done', thermalDeltaOnSuccess: 5 },
          { id: 'sundown_s2_full', propArt: 'scout_walker', x: 31, y: 12, once: true, iconKind: 'floor', label: 'KEF Repulsor Sledge Tank',
            description: "A KEF repulsor sledge tank crashes through the camouflage netting onto LZ Delta, crushing a supply crate. Its frontal shield eats ninety percent of direct fire and a plasma carpet turns the pad into a hazard zone.", requiresFlag: 'sundown_s1_done', lockedMessage: 'The LZ is clear. There is no tank, not yet.', requiresNoneFlags: ['tank_weakened', 'sundown_s2_done'], triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_sledge_tank', flavorText: 'The sledge tank turns its cannon tower and the frontal shield flares white. No grenades, no steam, no trick. Outlast it.' }, grantsFlag: 'sundown_s2_done', thermalDeltaOnSuccess: 10 },
          { id: 'sundown_s2_weak', propArt: 'scout_walker', x: 31, y: 12, once: true, iconKind: 'floor', label: 'KEF Repulsor Sledge Tank (Crippled)',
            description: "The tank lands listing, hover skirts guttering, its intake wrecked by a grenade or overheated steam. The cannon tower grinds, the core exposed.", requiresAllFlags: ['sundown_s1_done', 'tank_weakened'], requiresNoneFlags: ['sundown_s2_done'], invisibleLocked: true, triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_sledge_tank_crippled', flavorText: 'The sledge tank hits the pad screaming, skirts gone, rear intake wide open. Lock on the core and end it.' }, grantsFlag: 'sundown_s2_done', thermalDeltaOnSuccess: 5 },
          { id: 'sundown_s3_hacked', propArt: 'kdy_barricade', propVariant: 'kdy', x: 23, y: 29, once: true, iconKind: 'floor', label: 'Gateway Blast Door (Turrets Hacked)',
            description: "The hacked turrets sweep the retreat route clean and KEF reinforcements fall in the doorway unanswered. The lever is yours. The thermal board drops to zero.", requiresAllFlags: ['sundown_s2_done', 'vaelis_turrets_hacked'], requiresNoneFlags: ['sundown_complete'], invisibleLocked: true, grantsFlags: ['sundown_complete', 'sundown_s3_done'], thermalDelta: -100 },
          { id: 'sundown_s3_flank', propArt: 'kdy_barricade', propVariant: 'kdy', x: 23, y: 29, once: true, iconKind: 'floor', label: 'Gateway Blast Door (Side Hatch)',
            description: "Captain Lyra opens a side maintenance hatch and your squad takes the gate guards from behind.", requiresAllFlags: ['sundown_s2_done', 'vaelis_spared'], requiresNoneFlags: ['vaelis_turrets_hacked', 'sundown_complete'], invisibleLocked: true, triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_gate_guards_flanked', flavorText: 'The hatch opens with a hiss. Your squad hits the gate guards from behind while they scramble for cover that faces the wrong way.' }, grantsFlags: ['sundown_complete', 'sundown_s3_done'], thermalDeltaOnSuccess: -100 },
          { id: 'sundown_s3_full', propArt: 'kdy_barricade', propVariant: 'kdy', x: 23, y: 29, once: true, iconKind: 'floor', label: 'Gateway Blast Door',
            description: "The colossal gateway to the Engine Wastes. KEF forces dig in behind fortified cover, and a manual lockdown lever stands at the door center past six gate guards.", requiresFlag: 'sundown_s2_done', lockedMessage: 'The gateway is quiet. The tank has to fall first.', requiresNoneFlags: ['vaelis_turrets_hacked', 'vaelis_spared', 'sundown_complete'], triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_gate_guards', flavorText: 'Coop rallies the squads: "Follow the freelancer!" The guards fire from fortified cover, and the lever is thirty meters away.' }, grantsFlags: ['sundown_complete', 'sundown_s3_done'], thermalDeltaOnSuccess: -100 },
          { id: 'o7_enc_array', propArt: 'enc_array', x: 4, y: 24, once: false, iconKind: 'crate', label: 'High Bandwidth Encryption Array',
            description: "Server racks in wire conduits, cyan lights chasing down them. It has not transmitted in three weeks." },
          { id: 'o7_holotable', propArt: 'sanctum_holo', x: 8, y: 27, once: false, iconKind: 'terminal', label: 'Tactical Holotable',
            description: "A cyan wireframe map of the Engine Wastes, target nodes flashing red, the routes to the Exhaust Cathedral in amber.", grantsCodex: 'codex-iron-trench' },
          { id: 'o7_field_desk', propArt: 'field_desk', x: 12, y: 24, once: false, iconKind: 'crate', label: 'Major Coop Field Desk',
            description: "Datapads, a scorched helmet, cold ration bars. Under the helmet, a photograph of a woman and a child, face down." },
          { id: 'fusion_lever_amber', propArt: 'fusion_lever', propVariant: 'amber', x: 20, y: 23, once: true, iconKind: 'terminal', label: 'Fusion Diverter Lever',
            description: "A lever over the buried fusion generator. Pull it and the trench lights swap from amber to deep blue, the guns spin down and KEF scanners stop seeing a lit window.", requiresNoneFlags: ['o7_stealth_power'], grantsFlag: 'o7_stealth_power', thermalDelta: -20 },
          { id: 'fusion_lever_blue', propArt: 'fusion_lever', propVariant: 'blue', x: 20, y: 23, once: false, iconKind: 'terminal', label: 'Fusion Diverter Lever (Stealth)',
            description: "Locked in the blue position. The trench lights glow calm blue and the guns are silent.", requiresFlag: 'o7_stealth_power', invisibleLocked: true },
          { id: 'geothermal_terminal', propArt: 'console', propVariant: 'substation', x: 26, y: 23, once: true, iconKind: 'terminal', label: 'Geothermal Vent Release Terminal',
            description: "A terminal on the geothermal manifold. A few precise commands blast scalding steam into any intake the base chooses.", requiresFlag: 'o7_coop_briefed', lockedMessage: 'Major Coop has not cleared you for the manifold yet.', triggersMinigame: 'valve_override', grantsFlags: ['geothermal_siphon_done', 'base_resource_high', 'tank_weakened'], thermalDeltaOnSuccess: -10 },
          { id: 'o7_steam_c', propArt: 'steam_grate', x: 22, y: 27, once: true, iconKind: 'crate', label: 'Manifold Grate',
            description: "A manifold grate with a faded decal, steam curling through the slats.", thermalDelta: -4 },
          { id: 'o7_steam_d', propArt: 'steam_grate', x: 28, y: 27, once: true, iconKind: 'crate', label: 'Manifold Grate',
            description: "A second grate, the metal ticking as it cools.", thermalDelta: -4 },
          { id: 'brig_cell_1', propArt: 'brig_cell', x: 38, y: 26, once: false, iconKind: 'floor', label: 'Containment Cell Three',
            description: "An energy field cell humming a low note. The prisoner in torn hazard orange, red energy cuffs glowing, paces three steps each way." },
          { id: 'brig_cell_2', propArt: 'brig_cell', propVariant: 'empty', x: 42, y: 26, once: false, iconKind: 'floor', label: 'Containment Cell Four',
            description: "An empty cell, the cot neatly made, a mug on the shelf. Someone expected a guest." },
          { id: 'brig_switch', propArt: 'console', propVariant: 'dispatch', x: 35, y: 23, once: true, iconKind: 'terminal', label: 'Brig Forcefield Switch',
            description: "A switch panel labeled CELL THREE. The forcefield hum drops a pitch and the barrier dissolves.", grantsFlag: 'brig_open' },
        ],
        npcs: [
          { id: 'pilot_outpost_salk', x: 25, y: 11, kind: 'kdy_pilot', label: 'Pilot Iro Salk (KDY Shuttle)', triggersOverlay: 'shuttle',
            greeting: '\"Forward pad, Outpost 7. I can lift you to the Main Base or the Array on your word.\"',
            repeatPrompt: 'The pilot taps the shuttle manifest and waits for a destination.',
            prompt: 'A KDY shuttle pilot in the blue Kuati flight uniform.', choices: [] },
          { id: 'outpost_guard_brae', x: 14, y: 15, wander: 4, kind: 'kdy_security_marine', label: 'Outpost Guard Brae',
            repeatPrompt: 'Brae nods and keeps scanning the horizon.',
            prompt: 'A KDY guard with a rebreather at his collar patrols the outpost lights, watching the dark beyond.',
            choices: [ { text: 'Ask if the Outskirts road is safe.', morality: 0, loyalty: {}, result: '\"Nothing here is safe. But clear a sector and it stays clear. That is the arrangement.\"' } ] },
          { id: 'slicer_nix', x: 5, y: 27, wander: 2, kind: 'slicer', label: 'Slicer Nix',
            questNpc: true,
            requiresFlag: 'ghost_keel_active',
            hideAfterFlags: ['nix_dead'],
            repeatable: true,
            phases: [
              {
                id: 'phase_nix_a',
                requiresNoneFlags: ['architect_identity_known'],
                prompt: 'Nix runs three terminals and never turns around. "Ghost Keel manifests sit under a Level Eight seal. Four people can sign that. One signature is forty three years old."',
                choices: [
                  { text: 'Whose signature is that?', morality: 0, loyalty: {}, result: '"No name. The pattern matches a chief architect KDY buried after the Drayen Massacre." She pauses. "Dead men do not log in."', grants: { flags: ['nix_consulted', 'architect_identity_known'], codex: ['codex-front-ghost-account'] } },
                ],
              },
              {
                id: 'phase_nix_b',
                requiresAllFlags: ['architect_identity_known'],
                requiresNoneFlags: ['gharza_architect_connection'],
                prompt: 'Nix does not look up. "You have your ghost. Anything else, or are you just breathing at me?"',
                choices: [
                  { text: 'Does the name Gharza mean anything?', morality: 5, loyalty: { republic: 5 }, result: '"Her clearance was on Tanner\'s contractor list, then struck by a Level Eight override. She does not work for Tanner."', grants: { flags: ['nix_consulted', 'gharza_architect_connection'] } },
                  { text: 'No. Thank you.', morality: 0, loyalty: {}, result: '"Good. Quiet is billable."' },
                ],
              },
              {
                id: 'phase_nix_c',
                requiresAllFlags: ['gharza_architect_connection'],
                prompt: 'Nix keeps typing. "I told you what I have. The rest is under the Wastes, and I do not go there."',
                choices: [
                  { text: 'Understood.', morality: 0, loyalty: {}, result: '"Mind the cables on your way out."' },
                ],
              },
            ],
          },
          { id: 'outpost7_contact', x: 24, y: 26, wander: 2, kind: 'generic', label: 'Resistance Contact',
            repeatable: true,
            phases: [
              {
                id: 'phase_rc_intro',
                requiresNoneFlags: ['resistance_vorn_connection'],
                prompt: '"We have watched Sector 4 for two cycles. KDY is building the shell of a capital ship, in pieces." He spreads a hand drawn map. "The last parts are stored below the Wastes."',
                choices: [
                  { text: 'Who told you this?', morality: 0, loyalty: {}, result: '"Engineer Vorn. He learned what he was building three months ago and started talking that day. He is still inside. Treat him carefully."', grants: { flags: ['resistance_vorn_connection'] } },
                ],
              },
              {
                id: 'phase_rc_wait',
                requiresAllFlags: ['resistance_vorn_connection'],
                requiresNoneFlags: ['cavern_maps_found'],
                prompt: '"The planning board in the command bunker holds the rest." He taps the map. "Read it, then we talk again."',
                choices: [
                  { text: 'I will read it.', morality: 0, loyalty: {}, result: '"Good. Bring your questions after."' },
                ],
              },
              {
                id: 'phase_rc_maps',
                requiresAllFlags: ['resistance_vorn_connection', 'cavern_maps_found'],
                requiresNoneFlags: ['front_vorn_token_given'],
                prompt: '"You read the board. Then you know the way down is a sealed descent on the east edge of the Wastes." He holds out a scratched brass chit. "Vorn will know this."',
                choices: [
                  { text: '[Take the survey chit.]', morality: 0, loyalty: {}, result: 'A brass chit stamped with a survey grid. "Show it to anyone in Sector 4 who flinches. That will be Vorn."', grants: { flags: ['front_vorn_token_given'], items: ['front_vorn_chit'] } },
                ],
              },
              {
                id: 'phase_rc_done',
                requiresAllFlags: ['front_vorn_token_given'],
                prompt: '"Whatever you find down there, do not let KDY be the only one who knows."',
                choices: [
                  { text: 'I will remember.', morality: 0, loyalty: {}, result: 'He folds the map against his chest and goes back to watching the ridge.' },
                ],
              },
            ],
          },
          { id: 'major_coop', x: 8, y: 23, kind: 'kdy_commander', label: 'Major Coop',
            questNpc: true,
            repeatable: true,
            phases: [
              {
                id: 'phase_coop_intro',
                requiresNoneFlags: ['o7_coop_briefed'],
                prompt: "Major Coop stands at the holotable, a cold cigar in his teeth, a blast scar across his cheek. \"Outpost 7 does not exist until the strike force clears hyperspace. Who are you?\"",
                choices: [
                  { text: "Someone Commander Vael sent.", morality: 0, loyalty: {}, result: "\"Good. Three weeks silent and short of everything. I need a heavy hand and a quiet head. Sundown goes live on High Command's signal.\"", grants: { flags: ['o7_coop_briefed'], codex: ['codex-iron-trench', 'codex-front-sundown'] } },
                  { text: "I am here to help.", morality: 0, loyalty: {}, result: "He measures you with a long look. \"Then listen.\"", grants: { flags: ['o7_coop_briefed'], codex: ['codex-iron-trench', 'codex-front-sundown'] } },
                ],
              },
              {
                id: 'phase_coop_prep_a',
                requiresAllFlags: ['o7_coop_briefed'],
                requiresNoneFlags: ['convoy_raid_done', 'sundown_started'],
                prompt: "\"Three things before Sundown. The KEF convoy on the north ridge: a raid restocks us and makes us loud. The siphon: quiet. The brig prisoner knows the turret codes.\"",
                choices: [
                  { text: "[Convoy Raid] Plan the raid.", morality: 0, loyalty: {}, result: "\"You take the convoy, I take the credit. Bring grenades.\"", grants: { flags: [], thermal: 25 }, triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'kef_convoy_guard', flavorText: 'The convoy rolls into the gorge under fog. You wait on the ridge until the lead truck is exactly where you want it, then open fire.' }, onSuccessFlags: ['convoy_raid_done', 'base_resource_high', 'tank_weakened'] },
                  { text: "[Begin] Start Operation Sundown.", morality: 0, loyalty: {}, result: "\"Certain?\" He puts down the cigar. \"Sound the alarm.\" A 110 decibel siren rolls off the basalt.", grants: { flags: ['sundown_started'], thermal: 15 } },
                  { text: "I need more time.", morality: 0, loyalty: {}, result: "\"Time is the one thing we are short of. Fine.\"" },
                ],
              },
              {
                id: 'phase_coop_prep_b',
                requiresAllFlags: ['convoy_raid_done'],
                requiresNoneFlags: ['sundown_started'],
                prompt: "\"You brought the convoy back. Plasma grenades, fresh bacta, a full ammunition cache.\" He sets a bandolier on the holotable. \"We are ready, aren't we?\"",
                choices: [
                  { text: "[Begin] Start Operation Sundown.", morality: 0, loyalty: {}, result: "\"Sound the alarm.\" A 110 decibel siren rolls off the basalt.", grants: { flags: ['sundown_started'], thermal: 15, items: ['plasma_grenades'] } },
                  { text: "I need more time.", morality: 0, loyalty: {}, result: "\"I will be here.\"" },
                ],
              },
              {
                id: 'phase_coop_go',
                requiresAllFlags: ['sundown_started'],
                requiresNoneFlags: ['sundown_complete'],
                prompt: "The siren is still wailing. Coop checks his rifle. \"Trench, then the tank, then the gateway. All squads, on me.\"",
                choices: [
                  { text: "Understood, Major.", morality: 0, loyalty: {}, result: "\"Move.\"" },
                ],
              },
              {
                id: 'phase_coop_win',
                requiresAllFlags: ['sundown_complete'],
                requiresNoneFlags: ['front_sundown_closed'],
                prompt: "The gateway is open and the siren is silent. Coop sets a case on the table. \"Outpost 7 does not exist. Sundown does. Thermal reads zero. Tonight nobody is looking at us.\"",
                choices: [
                  { text: "[Accept] Take the Sundown Commendation.", morality: 0, loyalty: {}, result: "A pewter sunburst on a basalt ribbon, and thirty five hundred credits Coop cannot account for. \"Do not wear it south.\"", grants: { flags: ['front_sundown_closed'], items: ['front_sundown_medal'], credits: 3500, codex: ['codex-front-sundown-after'] } },
                  { text: "[Share] Give the medal to the Pathfinders.", morality: 8, loyalty: { republic: 5 }, result: "\"They will frame it over the bunks.\" Coop nods. \"Sundown was theirs before it was yours.\" The credits go to the squads' families.", grants: { flags: ['front_sundown_closed', 'front_sundown_shared'], codex: ['codex-front-sundown-after'] } },
                ],
              },
              {
                id: 'phase_coop_epi_plain',
                requiresAllFlags: ['front_sundown_closed'],
                requiresNoneFlags: ['vaelis_spared', 'vaelis_turrets_hacked', 'vaelis_decoy', 'vaelis_executed'],
                prompt: "Coop studies the holotable. \"The gateway leads south into the Wastes. Hot, loud, full of old machines. KEF will be back, but not tonight.\"",
                choices: [
                  { text: "Understood, Major.", morality: 0, loyalty: {}, result: "\"Outpost 7 does not exist. Neither do you. Go.\"" },
                ],
              },
              {
                id: 'phase_coop_epi_spared',
                requiresAllFlags: ['front_sundown_closed', 'vaelis_spared'],
                prompt: "Coop turns his cold cigar over. \"Vaelis walked out of my brig and the hatch you used is welded now. I will pretend I approve. The Wastes are south.\"",
                choices: [
                  { text: "He earned his walk.", morality: 0, loyalty: {}, result: "\"Maybe. Go.\"" },
                ],
              },
              {
                id: 'phase_coop_epi_hacked',
                requiresAllFlags: ['front_sundown_closed', 'vaelis_turrets_hacked'],
                prompt: "Coop watches the gate camera. \"Those turrets are ours, thanks to a prisoner who talked fast. I never asked how you made him. The Wastes are south.\"",
                choices: [
                  { text: "Better you do not.", morality: 0, loyalty: {}, result: "\"Go.\"" },
                ],
              },
              {
                id: 'phase_coop_epi_decoy',
                requiresAllFlags: ['front_sundown_closed', 'vaelis_decoy'],
                prompt: "Coop taps the holotable. \"Vaelis is out there dragging three KEF squads behind a datapad of nonsense. Clever. Cruel, too. The Wastes are south.\"",
                choices: [
                  { text: "It bought us the night.", morality: 0, loyalty: {}, result: "\"It did. Go.\"" },
                ],
              },
              {
                id: 'phase_coop_epi_executed',
                requiresAllFlags: ['front_sundown_closed', 'vaelis_executed'],
                prompt: "Coop does not look toward the brig. \"I had Vaelis buried on the north ridge under a flat stone. The squads are not talking about it. The Wastes are south.\"",
                choices: [
                  { text: "Then neither will I.", morality: 0, loyalty: {}, result: "\"Good. Go.\"" },
                ],
              },
            ],
          },
          { id: 'captain_lyra', x: 30, y: 17, kind: 'kdy_ring_sec', label: 'Captain Lyra',
            questNpc: true,
            repeatable: true,
            phases: [
              {
                id: 'phase_lyra',
                prompt: "A figure drops from the overhead beam in a basalt patterned ghillie shroud, triple optic helmet glowing emerald. \"Captain Lyra, Pathfinder Lead. Keep your voice down. Heat is what gets us killed.\"",
                choices: [
                  { text: "What is the thermal situation?", morality: 0, loyalty: {}, result: "\"Every shot, every step in the open, every alarm adds signature. Above thirty, hunter killers. Above sixty, artillery on the ridge. Stay cold.\"", grants: { flags: ['lyra_met'] } },
                ],
              },
              {
                id: 'phase_lyra_win',
                requiresAllFlags: ['sundown_complete'],
                requiresNoneFlags: ['front_lyra_gift', 'front_sundown_shared'],
                prompt: "Lyra drops from the beam and pushes back her hood, emerald optics dark. \"The signature is flat. Take this. It is the best thing here at hiding.\"",
                choices: [
                  { text: "[Take the shroud.]", morality: 0, loyalty: {}, result: "A basalt patterned ghillie shroud, still warm from her shoulders. \"Wherever you go next, go cold.\"", grants: { flags: ['front_lyra_gift'], items: ['front_pathfinder_shroud'] } },
                ],
              },
              {
                id: 'phase_lyra_win_shared',
                requiresAllFlags: ['sundown_complete', 'front_sundown_shared'],
                requiresNoneFlags: ['front_lyra_gift'],
                prompt: "Lyra has heard what you did with the medal. She holds out the shroud she wore through six deployments. \"Do not argue. It is yours.\"",
                choices: [
                  { text: "[Take the shroud.]", morality: 3, loyalty: { republic: 3 }, result: "Six deployments of basalt dust sit in the weave. \"The Pathfinders count you one of ours. You will not be told when.\"", grants: { flags: ['front_lyra_gift'], items: ['front_pathfinder_shroud'] } },
                ],
              },
              {
                id: 'phase_lyra_epi',
                requiresAllFlags: ['front_lyra_gift'],
                prompt: "Lyra is already back on the beam, a patch of rock that happens to breathe. \"Stay cold.\"",
                choices: [
                  { text: "You too, Captain.", morality: 0, loyalty: {}, result: "A single nod from the shadows." },
                ],
              },
            ],
          },
          { id: 'pathfinder_1', x: 14, y: 19, wander: 2, kind: 'kdy_security_marine', label: 'KDY Pathfinder',
            repeatable: true,
            phases: [
              {
                id: 'phase_x',
                prompt: "\"Tripwires are clean on the north ridge. For now.\" They go back to work.",
                choices: [
                  { text: "Leave them to it.", morality: 0, loyalty: {}, result: "They nod and keep working." },
                ],
              },
            ],
          },
          { id: 'pathfinder_2', x: 38, y: 18, wander: 2, kind: 'kdy_security_marine', label: 'KDY Pathfinder',
            repeatable: true,
            phases: [
              {
                id: 'phase_x',
                prompt: "\"I smell smoke from the east. Not ours.\" They go back to work.",
                choices: [
                  { text: "Leave them to it.", morality: 0, loyalty: {}, result: "They nod and keep working." },
                ],
              },
            ],
          },
          { id: 'engineer_1', x: 22, y: 12, wander: 2, kind: 'zero_g_welder', label: 'Combat Engineer',
            repeatable: true,
            phases: [
              {
                id: 'phase_x',
                prompt: "\"Mind the sparks. The drop speeder will outlive us all.\" They go back to work.",
                choices: [
                  { text: "Leave them to it.", morality: 0, loyalty: {}, result: "They nod and keep working." },
                ],
              },
            ],
          },
          { id: 'engineer_2', x: 17, y: 9, wander: 2, kind: 'zero_g_welder', label: 'Combat Engineer',
            repeatable: true,
            phases: [
              {
                id: 'phase_x',
                prompt: "\"Welding mask, welding apron, welding speeder. My whole life is welding.\" They go back to work.",
                choices: [
                  { text: "Leave them to it.", morality: 0, loyalty: {}, result: "They nod and keep working." },
                ],
              },
            ],
          },
          { id: 'officer_vaelis', x: 38, y: 25, kind: 'generic', label: 'Officer Vaelis (KEF Logistics)',
            questNpc: true,
            hideAfterFlags: ['vaelis_spared', 'vaelis_decoy', 'vaelis_executed'],
            repeatable: true,
            phases: [
              {
                id: 'phase_v_locked',
                requiresNoneFlags: ['brig_open'],
                prompt: "A man in torn hazard orange paces behind a crackling blue field, hands in red energy cuffs. \"If you came to talk, talk. If you came to kill me, the field is in the way.\"",
                choices: [
                  { text: "Who are you?", morality: 0, loyalty: {}, result: "\"Vaelis. Logistics. I count crates for people who do not like me. Open the cell and ask again.\"" },
                ],
              },
              {
                id: 'phase_v_open',
                requiresAllFlags: ['brig_open'],
                requiresNoneFlags: ['vaelis_decided'],
                prompt: "The field is down. Vaelis stands in the open cell, cuffed hands in front. \"Turret codes, convoy route, supply schedule. I know all of it. Which, and what do I get?\"",
                choices: [
                  { text: "[Force] The turret override codes. Now.", morality: -8, loyalty: {}, result: "He breaks fast. \"Eight seven nine four, two one, delta. Hit the override panel at the gateway and the turrets switch sides.\"", grants: { flags: ['vaelis_interrogated', 'vaelis_turrets_hacked', 'vaelis_decided'], thermal: 5 } },
                  { text: "[Deal] The gateway hatch. Then you walk.", morality: 8, loyalty: { republic: 3 }, result: "\"A side hatch, unmarked, west of the main door. Lyra knows it.\" You loosen the cuffs. He walks out into the dark.", grants: { flags: ['vaelis_spared', 'vaelis_decided'] } },
                  { text: "[Decoy] Release him with false codes.", morality: -2, loyalty: {}, result: "You send him out the back with a datapad of codes that lead nowhere. KEF hunter squads will trail him all night, chasing a ghost.", grants: { flags: ['vaelis_decoy', 'vaelis_decided'], thermal: -20 } },
                  { text: "[Execute] This ends here.", morality: -12, loyalty: {}, result: "The energy cuffs go dark. Vaelis does not move. The brig is very quiet and the outpost is very loud.", grants: { flags: ['vaelis_executed', 'vaelis_decided'], thermal: 10 } },
                ],
              },
              {
                id: 'phase_v_done',
                requiresAllFlags: ['vaelis_decided'],
                prompt: "Vaelis sits on the cot, cuffed hands between his knees. \"I told you everything. Leave me be.\"",
                choices: [
                  { text: "Leave him.", morality: 0, loyalty: {}, result: "He does not look up." },
                ],
              },
            ],
          },
        ],
        collectibles: [{ id: 'outpost7_intel', x: 18, y: 28, label: 'Resistance Survey Notes', reward: 300 }],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 5, 46, 30, 'floor');
          // Armory and Staging Bay (west)
          carveRect(g, 1, 7, 15, 13, 'wall'); carveRect(g, 2, 8, 14, 12, 'floor'); pt(g, 8, 13, 'floor');
          // Command Bunker (southwest)
          carveRect(g, 1, 21, 15, 29, 'wall'); carveRect(g, 2, 22, 14, 28, 'floor'); pt(g, 15, 25, 'floor');
          // Sub-Level Brig (southeast)
          carveRect(g, 32, 21, 45, 29, 'wall'); carveRect(g, 33, 22, 44, 28, 'floor'); pt(g, 32, 25, 'floor');
          pt(g, 0, 14, 'door'); pt(g, 0, 15, 'door');
          pt(g, 22, 31, 'door'); pt(g, 23, 31, 'door');
          return g;
        },
      },

      deep_caverns: {
        id: 'deep_caverns', name: 'Deep Caverns of Bador',
        subtitle: 'Bador Moon · Sub-Surface Grid 9-1 · Beneath the Titan Manifold',
        width: 48, height: 30, spawnPos: { x: 2, y: 14 }, textureId: 'verdanth',
        accent: '#7A5CC8', accentGlow: 'rgba(122,92,200,0.28)', accentDim: '#2C2250',
        floorColor: '#14101E', floorAlt: '#1A1426', wallDark: '#080610', wallLight: '#18122A',
        bg: 'radial-gradient(circle at 50% 30%, #1A1030 0%, #0A0818 55%, #040310 100%)', ambient: 'cavern',
        decor: ['rubble', 'moss'],
        doors: [
          { x: 0, y: 14,  targetZone: 'engine_wastes', targetPos: { x: 46, y: 21 }, label: 'Engine Wastes' },
          { x: 0, y: 15,  targetZone: 'engine_wastes', targetPos: { x: 46, y: 22 }, label: 'Engine Wastes' },
          { x: 47, y: 14, targetZone: 'zora_iv_wreckage', targetPos: { x: 1, y: 16 }, label: 'Zora IV Wreckage', requiresAnyFlag: ['unit8x_destroyed', 'unit8x_reprogrammed', 'unit8x_stood_down', 'sentinel_cleared'], lockedMessage: 'The Guardian Gate shutter is down and its lockout lamp burns red. Unit 8-X holds the line, and the sentinels hold it with him.' },
          { x: 47, y: 15, targetZone: 'zora_iv_wreckage', targetPos: { x: 1, y: 17 }, label: 'Zora IV Wreckage', requiresAnyFlag: ['unit8x_destroyed', 'unit8x_reprogrammed', 'unit8x_stood_down', 'sentinel_cleared'], lockedMessage: 'The Guardian Gate shutter is down and its lockout lamp burns red. Unit 8-X holds the line, and the sentinels hold it with him.' },
        ],
        worldObjects: [
          { id: 'cav_vista', propArt: 'bvista_48x5', propVariant: 'cavern_a', x: 24, y: 5, once: false, iconKind: 'vista', label: 'The Underground Lake and Kyber Veins',
            description: 'Stalactites hang like organ pipes over a black lake. Violet kyber veins run through the rock and crystal spears rise from the far shore. A pale shaft of light falls from a fissure no one has ever mapped.' },

          // ---- Republic survey station (west, near the entrance) ----
          { id: 'cav_survey_console', propArt: 'console', propVariant: 'republic_old', x: 6, y: 12, label: 'Survey Station Logbook Terminal',
            description: 'The Grid 9-1 survey logbook, Republic Geodetic Division. Six names on the roster, one crossed through. The last entry is a core sample from a cavity whose far wall no instrument could find.',
            grantsCodex: 'codex-cav-survey-log' },
          { id: 'cav_engineer_warning', propArt: 'board', propVariant: 'clipboard', x: 13, y: 12, label: 'Engineer\'s Warning',
            description: 'A clipboard nailed to the wall in block capitals: DO NOT OPEN VAULT FOUR. THE RESONANCE IS A LOCK, NOT A LODE. WHOEVER TUNED IT IS STILL LISTENING. The signature has been scratched out.',
            grantsCodex: 'codex-cav-engineer-warning' },
          { id: 'republic_survey_cache', propArt: 'crate_stack', propVariant: 'survey', x: 13, y: 20, label: 'Republic Survey Station Cache',
            description: 'Sealed geological sample crates and an encrypted data core. The resealing pattern is not Republic standard. Someone has been here since the survey team left.',
            once: true, requiresFlag: 'ghost_keel_active', lockedMessage: 'The cache lock rejects you. It will open only for someone already working the Ghost Keel contract.',
            grantsFlag: 'survey_cache_opened', grantsItem: 'cherit_logbook' },
          { id: 'cav_beacon_station', propArt: 'cav_survey_beacon', propVariant: 'survey', x: 5, y: 20, label: 'Station Survey Beacon',
            description: 'A red and white Republic survey beacon on a folding tripod. Its amber lamp still blinks every few seconds, signalling a base that stopped listening decades ago.' },
          { id: 'cav_station_archive', propArt: 'archive_cabinet', propVariant: 'dossier', x: 5, y: 17, label: 'Team Roster Cabinet',
            description: 'A dossier cabinet with six drawers, five of them emptied in a hurry. The sixth holds a roster card for Team Nine and a requisition signed by an officer whose rank has been blacked out.' },
          { id: 'cav_station_samples', propArt: 'datapad_table', propVariant: 'canister', x: 7, y: 20, label: 'Core Sample Table',
            description: 'Core sample canisters, labelled in a neat hand and set in a rack. The deepest tube is warm to the touch and holds a violet stone that hums at the edge of hearing.' },
          { id: 'cav_station_servers', propArt: 'server_stack', propVariant: 'republic', x: 14, y: 18, label: 'Station Data Stacks',
            description: 'Republic server stacks, powered by a trickle line from the lake. The status lamps cycle green, amber, green. Something polls them every night and never logs in.' },
          { id: 'cav_marker_vestibule', propArt: 'survey_marker', x: 2, y: 17, label: 'Airlock Survey Marker',
            description: 'A brass survey marker at the airlock lip, stamped with the grid reference 9-1. Fresh scuffs show that heavy boots have crossed this threshold, but not many.' },

          // ---- North ledge: abandoned KDY mining equipment, spore fields ----
          { id: 'cav_rig_west', propArt: 'cav_mining_rig', x: 7, y: 8, label: 'Abandoned KDY Core Drill',
            description: 'A KDY-7 core drill, its bit still seated in a bore plugged with sealing foam. The foreman\'s hard hat hangs from the control lever. Someone stopped the shift mid turn and walked away.' },
          { id: 'cav_crates_west', propArt: 'crate_stack', propVariant: 'kdy', x: 13, y: 8, label: 'Mining Supply Crates',
            description: 'Hazard striped KDY crates of blasting gel and drill teeth, still strapped to their pallet. The seals are intact. Whoever left this site left everything else too.' },
          { id: 'cav_beacon_ledge', propArt: 'cav_survey_beacon', propVariant: 'survey', x: 4, y: 8, label: 'Ledge Survey Beacon',
            description: 'A second survey beacon marks the west ledge. Its lamp is cracked and glows the faint amber of a dying cell, pointing every visitor toward the lake.' },
          { id: 'cav_spore_a', propArt: 'cav_spore_pod', x: 19, y: 7, label: 'Bioluminescent Spore Field',
            description: 'Five swollen pods of glowing green fungus, breathing slowly. Each exhale sends a lazy cloud of spores upward. The air tastes of damp stone and sweet rot.' },
          { id: 'cav_stal_a', propArt: 'cav_stalactite', x: 22, y: 8, label: 'Stalactite Column',
            description: 'A column where the ceiling has reached the floor. Violet veins run through the stone and pulse faintly in a slow rhythm, like something sleeping.' },
          { id: 'cav_spore_b', propArt: 'cav_spore_pod', x: 26, y: 8, label: 'Spore Pod Cluster',
            description: 'Fat green pods cling to the ledge edge. A pulse of light runs from pod to pod, as though they are passing a message along the cavern wall.' },
          { id: 'cav_stal_b', propArt: 'cav_stalactite', x: 29, y: 8, label: 'Twin Stalactite Column',
            description: 'Another stone column wrapped in kyber light. The floor around its base is smooth and dark, worn by something that circled it for years.' },
          { id: 'cav_spore_c', propArt: 'cav_spore_pod', x: 32, y: 7, label: 'Spore Nursery',
            description: 'Small pods bloom around a larger one at the center. A handful of dead insects lie among them, glowing faintly where the spores have taken hold.' },

          // ---- Lake, bridges and the Cherit memorial ----
          { id: 'cav_memorial', propArt: 'cav_survey_beacon', propVariant: 'memorial', x: 26, y: 13, label: 'Team Nine Memorial',
            description: 'A pale beacon rises from the island above a plaque. TEAM NINE. DUG BY ORDER OF I. CHERIT. REMEMBERED BY THE ONE WHO STAYED. A cut stem of glow moss lies fresh against its base.',
            grantsCodex: 'codex-cav-cherit-memorial' },
          { id: 'cav_marker_shore', propArt: 'survey_marker', x: 16, y: 12, label: 'Lake Depth Marker',
            description: 'A Republic depth marker on the west shore. The scale tops out at two hundred meters. The needle has been pushed past the end stop, and someone has bent it back.' },

          // ---- Crystal grove (south west) ----
          { id: 'cavern_crystal_formation', propArt: 'kyber_cluster', propVariant: 'cavern', x: 6, y: 26, label: 'Force-Resonant Crystal Formation',
            description: 'A formation of kyber adjacent crystal, humming in a pattern that nature does not make. Whoever built the installation set it here on purpose. The resonance stays with you after you step away.',
            once: true, grantsFlag: 'cavern_crystal_found', grantsCodex: 'codex-bador-crystal' },
          { id: 'cav_pylon_a', propArt: 'cav_crystal_pylon', x: 3, y: 26, label: 'Tuned Crystal Pylon',
            description: 'A brass collared pylon holds a violet crystal at head height, its inner veins aligned to the rest of the grove. Drilled into the base are a dozen tuning holes, each plugged with wax.' },
          { id: 'cav_pylon_b', propArt: 'cav_crystal_pylon', x: 12, y: 25, label: 'Tuned Crystal Pylon',
            description: 'A second pylon, its collar green with age. The crystal hums at a slightly different pitch from its twin, and the two beat together in a slow waver across the grove.' },
          { id: 'cav_pylon_c', propArt: 'cav_crystal_pylon', x: 14, y: 28, label: 'Tuned Crystal Pylon',
            description: 'The third pylon of the grove, the one closest to the station. Its lamp is cyan, not amber. Of the three, only this one has been serviced in the last year.' },
          { id: 'cav_grove_cluster', propArt: 'kyber_cluster', propVariant: 'cavern', x: 9, y: 28, label: 'Violet Crystal Spears',
            description: 'Crystal spears break out of the floor in a ragged crown. Where they catch the light they throw violet reflections across the grove ceiling and the stone drinks it up.' },
          { id: 'cav_spore_grove', propArt: 'cav_spore_pod', x: 12, y: 27, label: 'Grove Spore Pods',
            description: 'Pods swollen with green light nest between the crystals. They pulse in time with the pylons, as though the fungus has learned the resonance.' },

          // ---- South yard: abandoned KDY mining equipment ----
          { id: 'cav_crane', propArt: 'maglev_crane', x: 20, y: 27, label: 'Abandoned Maglev Crane',
            description: 'A KDY maglev crane with its boom folded over a half loaded ore sled. The load is not ore. It is crates of Republic survey equipment, stencilled DO NOT LOG.' },
          { id: 'cav_containers', propArt: 'container_stack', x: 27, y: 27, label: 'Ore Container Stack',
            description: 'Four ore containers stacked two high, doors welded shut from the outside. Chalk tallies on the side count shifts: forty one, then nothing.' },
          { id: 'cav_rig_south', propArt: 'cav_mining_rig', x: 32, y: 26, label: 'KDY Core Drill, Shift Four',
            description: 'A second core drill, its derrick leaning where the floor settled. Shift Four was working a bore toward the east wall when they stopped. The bit is warm.' },
          { id: 'cav_drums', propArt: 'drum_array', x: 23, y: 24, label: 'Coolant Drum Array',
            description: 'A row of coolant drums with their valves open, drained long ago. A faint blue crust has grown where the last of it seeped into the rock.' },
          { id: 'cav_scrap', propArt: 'scrap_bin', x: 17, y: 25, label: 'Salvage Bin',
            description: 'A salvage bin heaped with bent drill teeth and cracked helmets. Under the pile is a boot with a tag still on it: SHIFT FOUR, SIZE NINE.' },

          // ---- East shore, the sentinel line and the collapsed section ----
          { id: 'cavern_sentinel_inactive', propArt: 'sentinel_droid', x: 37, y: 12, once: true, label: 'Automated Sentinel',
            description: 'A KDY pattern sentinel, optics dark, a neat cutout clamp on its spine and a Trandoshan claw mark scratched beside it. Her service key turns in the lock without a sound.',
            requiresFlag: 'gharza_allied', lockedMessage: 'A sentinel stands powered at the gate approach, optics tracking. The idle line answers to a hunter\'s service key. Gharza gives one to people she trusts.',
            requiresNoneFlags: ['sentinel_cleared'],
            grantsFlags: ['sentinel_cleared', 'cavern_path_clear'] },
          { id: 'cav_sentinel_off_a', propArt: 'cav_guardian_chassis', propVariant: 'dormant', x: 37, y: 12, label: 'Sentinel (Powered Down)',
            description: 'The sentinel stands in standby, optics dark, a cutout clamp on its spine. Its sister units down the line have stopped tracking you. The line is silent.',
            requiresAllFlags: ['sentinel_cleared'], invisibleLocked: true },
          { id: 'cav_sentinel_line_b', propArt: 'sentinel_droid', x: 37, y: 18, label: 'Sentinel Line Unit',
            description: 'A second KDY sentinel on the gate approach. Its optic sweeps the shore in a steady arc, and a red targeting lamp brushes your chest as it passes.',
            requiresNoneFlags: ['sentinel_cleared'] },
          { id: 'cav_sentinel_off_b', propArt: 'cav_guardian_chassis', propVariant: 'dormant', x: 37, y: 18, label: 'Sentinel Line Unit (Offline)',
            description: 'The second sentinel hangs in its cradle, optic dark. Its targeting lamp is out and a thin curl of smoke rises from the cutout clamp on its back.',
            requiresAllFlags: ['sentinel_cleared'], invisibleLocked: true },
          { id: 'cav_catwalk_east', propArt: 'catwalk_post', x: 35, y: 21, label: 'East Shore Catwalk',
            description: 'A KDY inspection catwalk over the shore, its stairs pulled up. A sniper nest sits on top, long abandoned. The sightline covers the whole approach to the gate.' },
          { id: 'cav_leak', propArt: 'leaking_pipe', propVariant: 'coolant', x: 39, y: 6, label: 'Cracked Coolant Line',
            description: 'A coolant line split by a rockfall, venting a thin blue vapor. The floor under it is slick and the stone has a faint frost.' },
          { id: 'cav_crushed', propArt: 'cargo_container', x: 38, y: 8, label: 'Crushed Cargo Container',
            description: 'A cargo container flattened under a slab of the ceiling. A boot print is pressed into the dust beside it, heading west. It has not been disturbed.' },
          { id: 'cav_miner_diary', propArt: 'datapad_table', propVariant: 'logbook', x: 46, y: 9, label: 'Foreman\'s Diary',
            description: 'A water stained foreman\'s diary beneath the collapsed ceiling. Day 41: humming in the rock. Day 44: told to keep digging and stop writing. Day 45: the entry ends mid word.',
            grantsCodex: 'codex-cav-miner-diary' },
          { id: 'cav_stal_fallen', propArt: 'cav_stalactite', x: 46, y: 7, label: 'Fallen Column',
            description: 'A stalactite that fell and lodged in the wall. Its violet veins have gone dark where it broke, but the pieces still catch the light like glass.' },
          { id: 'cav_spore_se1', propArt: 'cav_spore_pod', x: 39, y: 24, label: 'Southeast Spore Field',
            description: 'A broad bed of spore pods spreads across the southeast floor, glowing green. Footprints lead into it and stop. Nothing comes out.' },
          { id: 'cav_spore_se2', propArt: 'cav_spore_pod', x: 43, y: 26, label: 'Spore Bloom',
            description: 'A bloom of larger pods ringed with smaller ones. Where the light is strongest, the stone floor has been eaten into a shallow bowl.' },
          { id: 'cav_spore_se3', propArt: 'cav_spore_pod', x: 38, y: 28, label: 'Spore Mat',
            description: 'A low mat of green fungus covers the corner. It glows brighter when you approach, as though it recognises the heat of a living body.' },
          { id: 'cav_stal_se1', propArt: 'cav_stalactite', x: 46, y: 25, label: 'Southeast Column',
            description: 'A column at the cavern edge, its base furred with moss. A boot print is pressed into the moss and filled with spores. Older than the diary.' },
          { id: 'cav_stal_se2', propArt: 'cav_stalactite', x: 41, y: 23, label: 'Support Column',
            description: 'A natural column. Drill marks run down one side where a KDY crew started a bore and gave up. The stone has healed over them.' },

          // ---- Guardian Gate (east end) ----
          { id: 'cav_gate_barricade_n', propArt: 'kdy_barricade', propVariant: 'kdy', x: 41, y: 12, label: 'Gate Barricade North',
            description: 'A hazard striped KDY barricade across the north half of the checkpoint. The paint is scored by old fire, and a hand lettered sign reads: HALT. AUTHORIZATION CODE.' },
          { id: 'cav_gate_barricade_s', propArt: 'kdy_barricade', propVariant: 'kdy', x: 41, y: 18, label: 'Gate Barricade South',
            description: 'The south barricade, mirror of the north. A line of old burn marks climbs one side, as though something very large fired at it and was refused.' },
          { id: 'cav_gate_turret_n', propArt: 'auto_turret', propVariant: 'hostile', x: 43, y: 12, label: 'Gate Turret (Idle)',
            description: 'An automated turret in its mount, barrel cold, sensor lamp pale green. It is slaved to the warden, and the warden has not told it to wake.' },
          { id: 'cav_gate_turret_s', propArt: 'auto_turret', propVariant: 'hostile', x: 43, y: 18, label: 'Gate Turret (Idle)',
            description: 'A second turret on the south side, its barrel draped in cable. It tracks you for half a second, then goes back to sleep.' },
          { id: 'cav_gate_eweb', propArt: 'eweb_turret', propVariant: 'plain', x: 46, y: 12, label: 'Gate E-Web Emplacement',
            description: 'A heavy repeater mounted beside the shutter, aimed at the walkway. The ammunition feed is full. Its heat gauge reads a steady zero.' },
          { id: 'cav_plinth_core', propArt: 'cav_guardian_chassis', propVariant: 'wreck', x: 44, y: 15, once: true, label: 'Unit 8-X Wreckage',
            description: 'The warden lies in pieces across its plinth, optic shattered, chest plate peeled open. Inside the torn housing a cyan memory core still glows, faintly warm, with the patrol maps of a lifetime.',
            requiresAllFlags: ['unit8x_destroyed'], requiresNoneFlags: ['cav_core_taken'], invisibleLocked: true,
            grantsFlags: ['cav_core_taken', 'hypercore_location_known'], grantsItem: 'cav_8x_memory_core', grantsCodex: 'codex-cav-guardian' },
          { id: 'cav_plinth_stripped', propArt: 'cav_guardian_chassis', propVariant: 'stripped', x: 44, y: 15, label: 'Unit 8-X Wreckage (Stripped)',
            description: 'The warden\'s hulk lies cold on its plinth. The memory core is gone from the chest cavity. A last wisp of smoke curls from the empty housing.',
            requiresAllFlags: ['cav_core_taken'], invisibleLocked: true },
          { id: 'cav_plinth_dormant', propArt: 'cav_guardian_chassis', propVariant: 'dormant', x: 44, y: 15, label: 'Unit 8-X (Stood Down)',
            description: 'Unit 8-X sits folded on its plinth, optic dark, a single amber standby lamp breathing in the chest. It will wake for the right code and for nothing else.',
            requiresAllFlags: ['unit8x_stood_down'], invisibleLocked: true, grantsCodex: 'codex-cav-guardian' },
          { id: 'cav_plinth', propArt: 'cav_guardian_chassis', propVariant: 'plinth', x: 44, y: 15, label: 'Guardian Plinth',
            description: 'A heavy octagonal cradle bolted into the cavern floor, cabled to the rock walls. Hazard lamps ring its edge. This is where the warden stands, and has stood, for longer than the survey station has been silent.',
            requiresNoneFlags: ['unit8x_destroyed', 'unit8x_stood_down'] },
          { id: 'cav_shutter_sealed', propArt: 'cav_gate_shutter', propVariant: 'sealed', x: 46, y: 16, label: 'Guardian Gate Shutter',
            description: 'A blast shutter hazard banded and locked down, the lockout lamp red. Stencilled across it: ZORA IV SECTOR. A diagnostic console beside it waits for a clear line.',
            requiresNoneFlags: ['cavern_path_clear'] },
          { id: 'cav_shutter_open', propArt: 'cav_gate_shutter', propVariant: 'open', x: 46, y: 16, label: 'Guardian Gate (Open)',
            description: 'The shutter has gone up into the ceiling. Green guide lamps run down a long dark passage toward the wreck of the Zora IV.',
            requiresAllFlags: ['cavern_path_clear'], invisibleLocked: true },
          { id: 'cav_gate_console', propArt: 'console', propVariant: 'surveil', x: 46, y: 18, once: true, label: 'Gate Diagnostic Console',
            description: 'The last gate log scrolls: transit to Zora IV sector, grid Alpha-7, one inhabited vault. An access order dated forty three years ago has never been countermanded.',
            requiresFlag: 'cavern_path_clear', lockedMessage: 'The console is locked out while the shutter holds. The warden and its escort draw all the power.',
            grantsFlag: 'hypercore_location_known' },
        ],
        npcs: [
          { id: 'cavern_unit_8x', x: 44, y: 14, kind: 'droid', label: 'Unit 8-X',
            questNpc: true, repeatable: true,
            hideAfterFlags: ['unit8x_destroyed', 'unit8x_stood_down'],
            phases: [
              {
                id: 'phase_8x_challenge',
                prompt: 'A KDY assembly frame rebuilt as a gate warden turns its single optic on you, cable bundles tethering it to the plinth. "HALT. AUTHORIZATION CODE. SENTINEL LINE ARMED."',
                repeatPrompt: '"HALT. AUTHORIZATION CODE."',
                choices: [
                  { text: 'Input the assembly override code.', morality: 0, loyalty: {}, requiresFlag: 'assembly_override_accessed',
                    result: '"OVERRIDE ACCEPTED. WARDEN FUNCTION SUSPENDED." The optic dims to amber. The lockout drops and the droid folds itself onto its plinth, a machine at rest. A maintenance purse drops into your hand.',
                    grants: { flags: ['unit8x_interacted', 'unit8x_stood_down', 'cavern_path_clear'], credits: 250 } },
                  { text: 'Tell it the Architect sent you.', morality: 0, loyalty: {}, requiresFlag: 'architect_identity_known',
                    result: '"LEVEL EIGHT CONFIRMED." The optic turns cyan. "WARDEN RELIEVED. ZORA IV, GRID ALPHA-7. HE EXPECTS A VISITOR." It steps aside and keeps watching the dark.',
                    grants: { flags: ['unit8x_reprogrammed', 'hypercore_location_known', 'cavern_path_clear'], codex: ['codex-cav-guardian'] } },
                  { text: '[Fight] Break the escort line.', morality: -5, loyalty: { underworld: 3 },
                    result: 'Servos scream awake. Two sentinels step off the wall and a third optic opens in the dark. The escort moves first, as designed.',
                    grants: { flags: ['unit8x_hostile'] }, triggersMinigame: 'tactical_combat',
                    minigameConfig: { opponentProfile: 'cav_unit_8x_guard', flavorText: 'The sentinel escort drops from the wall in perfect step, shields up, guns tracking. They were built to hold this gate against a company. You are one person with a plan.' },
                    onSuccessFlags: ['unit8x_guard_down'] },
                  { text: 'Back away slowly.', morality: 0, loyalty: {}, defer: true, result: 'You retreat out of its line of fire. The optic follows you until you are gone.' },
                ],
              },
              {
                id: 'phase_8x_sentinels_off',
                requiresAllFlags: ['sentinel_cleared'],
                prompt: 'The sentinel line is dark. Unit 8-X stands alone on its plinth, optic flickering. "ESCORT LINK LOST. WARDEN FUNCTION ACTIVE. AUTHORIZATION CODE."',
                repeatPrompt: '"ESCORT LOST. WARDEN ACTIVE."',
                choices: [
                  { text: 'Input the assembly override code.', morality: 0, loyalty: {}, requiresFlag: 'assembly_override_accessed',
                    result: '"OVERRIDE ACCEPTED. WARDEN FUNCTION SUSPENDED." The optic dims to amber. The droid folds itself onto its plinth and a maintenance purse drops into your hand.',
                    grants: { flags: ['unit8x_interacted', 'unit8x_stood_down', 'cavern_path_clear'], credits: 250 } },
                  { text: 'Tell it the Architect sent you.', morality: 0, loyalty: {}, requiresFlag: 'architect_identity_known',
                    result: '"LEVEL EIGHT CONFIRMED." The optic turns cyan. "ZORA IV, GRID ALPHA-7. HE EXPECTS A VISITOR." It steps aside and keeps watching the dark.',
                    grants: { flags: ['unit8x_reprogrammed', 'hypercore_location_known', 'cavern_path_clear'], codex: ['codex-cav-guardian'] } },
                  { text: '[Fight] Take the lone warden.', morality: -5, loyalty: { underworld: 3 },
                    result: 'The optic flares red. Without its escort the warden is slower and meaner, and it knows every inch of this floor.',
                    grants: { flags: ['unit8x_hostile'] }, triggersMinigame: 'tactical_combat',
                    minigameConfig: { opponentProfile: 'cav_unit_8x_boss', flavorText: 'Unit 8-X unbolts from its plinth. The sentinel line is dead, so the warden fights alone, pile drivers cycling and overwatch locked on the only way in.' },
                    onSuccessFlags: ['unit8x_destroyed', 'cavern_path_clear'] },
                  { text: 'Back away slowly.', morality: 0, loyalty: {}, defer: true, result: 'You retreat out of its line of fire. The optic follows you until you are gone.' },
                ],
              },
              {
                id: 'phase_8x_boss',
                requiresAllFlags: ['unit8x_guard_down'],
                prompt: 'The escort lies in pieces. Unit 8-X unbolts from its plinth and the whole gate hums. "ESCORT LOST. WARDEN ASSUMES DIRECT COMMAND. FIRING."',
                repeatPrompt: '"WARDEN ASSUMES COMMAND."',
                choices: [
                  { text: '[Fight] End the warden.', morality: -5, loyalty: { underworld: 3 },
                    result: 'The warden steps off the plinth and the cradle chains drag sparks across the floor. It does not speak again.',
                    grants: { flags: ['unit8x_hostile'] }, triggersMinigame: 'tactical_combat',
                    minigameConfig: { opponentProfile: 'cav_unit_8x_boss', flavorText: 'Unit 8-X has shed its tethers. Its shielding is up, its pile drivers are cycling and every approach is under overwatch. This is the gate. This is the end of the line.' },
                    onSuccessFlags: ['unit8x_destroyed', 'cavern_path_clear'] },
                  { text: 'Fall back and prepare.', morality: 0, loyalty: {}, defer: true, result: 'You back out of range. The warden does not follow. It cannot leave the plinth, and it knows you will be back.' },
                ],
              },
              {
                id: 'phase_8x_ally',
                requiresAllFlags: ['unit8x_reprogrammed'],
                requiresNoneFlags: ['cav_ally_gift'],
                prompt: 'Unit 8-X holds its post at an angle, optic cyan, rifle arm lowered. "WATCH SET. VISITOR RECORDED. REQUEST: ANY."',
                repeatPrompt: '"WATCH SET."',
                choices: [
                  { text: 'Ask for Level Eight escort credentials.', morality: 0, loyalty: {},
                    result: 'A palm beacon unseats from its chassis. "ISSUE: ESCORT BEACON. SENTRIES WILL CONFIRM, NOT ENGAGE." It is warm, and heavier than it looks.',
                    grants: { flags: ['cav_ally_gift'], items: ['cav_8x_escort_beacon'] } },
                  { text: 'Ask what it guards.', morality: 0, loyalty: {},
                    result: '"CRADLE. LAKE. SHUTTER. ONE VAULT BEYOND." The optic flicks east. "NOT THE SHIP. THE MAN."' },
                ],
              },
              {
                id: 'phase_8x_ally_after',
                requiresAllFlags: ['unit8x_reprogrammed', 'cav_ally_gift'],
                prompt: 'Unit 8-X turns its optic east, then back to you. "WATCH CONTINUES. THE GATE IS OPEN."',
                repeatPrompt: '"WATCH CONTINUES."',
                choices: [
                  { text: 'Leave it to its watch.', morality: 0, loyalty: {}, result: 'It does not move. It never stopped watching.' },
                ],
              },
            ],
          },
          { id: 'cav_gonk_mine', x: 24, y: 26, wander: 2, kind: 'gnk_power', label: 'GNK Power Droid',
            repeatable: true,
            phases: [
              {
                id: 'phase_gonk',
                prompt: 'A GNK power droid trundles between crates, drained cells stacked on its back. "Gonk. Gonk gonk." A crew tag is still bolted to its chassis.',
                repeatPrompt: '"Gonk."',
                choices: [
                  { text: 'Read the tag.', morality: 0, loyalty: {}, result: 'SHIFT FOUR, GRID 9-1. UNAVAILABLE FOR PICKUP. The crew left it charging and never came back. It is still charging, patiently, for no one.' },
                ],
              },
            ],
          },
        ],
        collectibles: [{ id: 'cavern_survey_data', x: 40, y: 9, label: 'Deep Survey Geological Record', reward: 350 }],
        buildMap() {
          return mapFromRows([
            '################################################',
            '################################################',
            '################################################',
            '################################################',
            '################################################',
            '####................................#.......##.#',
            '###.................................#...###.##.#',
            '##......................................###.##.#',
            '##.............#........................###....#',
            '#########..#####.wwwwwww..wwwwwwww.............#',
            '####...........#.ww##www..www##www.....#########',
            '####...........#.ww##www..www##www.....#.......#',
            '#..#...........#.wwwwwww..wwwwwwww.....#.......#',
            '#..#...........#.wwwwww....wwwwwww.....#.......#',
            'D..............................................D',
            'D..............................................D',
            '#..#...........#.wwwwww....wwwwwww.....#.......#',
            '#..#...........#.wwwwwww..wwwwwwww.....#.......#',
            '####...........#.ww##www..wwwwwwww.....#.......#',
            '####...........#.ww##www..wwww##ww.....#.......#',
            '####...........#.wwww##w..wwww##ww.....#########',
            '####...........#.wwww##w..wwwwwwww.....#########',
            '#########..#####.wwwwwww..wwwwwwww.............#',
            '##.............................................#',
            '##.............................................#',
            '##.............................................#',
            '##.............................................#',
            '###.............#.............................##',
            '####............#.................##..........##',
            '################################################',
          ]);
        },
      },

      zora_iv_wreckage: {
        id: 'zora_iv_wreckage', name: 'The Zora IV — Ghost Keel Drydock',
        subtitle: 'Bador Moon · Grid 9-1 · The Architect\'s Sanctum',
        width: 48, height: 32, spawnPos: { x: 2, y: 16 }, textureId: 'ferrowake',
        accent: '#B8D4E8', accentGlow: 'rgba(184,212,232,0.25)', accentDim: '#485868',
        floorColor: '#141C24', floorAlt: '#1A2430', wallDark: '#080C10', wallLight: '#101820',
        bg: 'radial-gradient(circle at 50% 30%, #101820 0%, #060C10 70%)', ambient: 'wreck',
        decor: ['girder', 'pipe'],
        paAnnouncements: [
          'All hands, report to your stations. This is a recording from the year of the Drayen Massacre.',
          'Containment integrity at four percent. Do not approach the hypercore.',
          'Drydock crane four is out of service. Crane four has been out of service for forty years.',
        ],
        doors: [
          { x: 0, y: 16, targetZone: 'deep_caverns', targetPos: { x: 46, y: 14 }, label: 'Deep Caverns' },
          { x: 0, y: 17, targetZone: 'deep_caverns', targetPos: { x: 46, y: 15 }, label: 'Deep Caverns' },
        ],
        worldObjects: [
          { id: 'zora_vista_w', propArt: 'bvista_24x5', propVariant: 'hullbay_a', x: 12, y: 5, once: false, iconKind: 'vista', label: 'The Dead Dreadnought Hall',
            description: 'Ribs of a Venator class hull arch overhead, red emergency lamps pulsing along the girders. Cables hang like roots. A cracked viewport frames the cavern beyond.' },
          { id: 'zora_vista_e', propArt: 'bvista_24x5', propVariant: 'hullbay_b', x: 36, y: 5, once: false, iconKind: 'vista', label: 'The Reactor Gallery',
            description: 'A half built keel lies under scaffolds that nobody has climbed in thirty years. Work lights still flicker along the gallery, as if the shift never ended.' },
          { id: 'keel_frame', propArt: 'keel_frame', x: 15, y: 16, once: false, iconKind: 'floor', label: 'The Ghost Keel',
            description: 'A pocket dreadnought frame in a drydock cradle. The forward third wears pearl hull plates, the rest is bare ribs around a dark reactor seat. A plate reads GHOST KEEL HULL 001.',
            grantsCodex: 'codex-ghost-keel' },
          { id: 'zora_crane', propArt: 'maglev_crane', x: 8, y: 10, once: false, iconKind: 'crate', label: 'Drydock Gantry Crane',
            description: 'A gantry crane frozen mid lift, a pearl hull plate still hanging from its hook, the chain crusted with rust.' },
          { id: 'zora_scaffold', propArt: 'catwalk_post', x: 25, y: 10, once: false, iconKind: 'crate', label: 'Scaffold Post',
            description: 'A scaffold post with a hand scratched tally of days. The count stops at fifteen thousand seven hundred.' },
          { id: 'zora_servers', propArt: 'server_stack', x: 7, y: 20, once: false, iconKind: 'terminal', label: 'Design Archive Racks',
            description: 'Racks of drafting servers. Two are warm. Someone keeps them running.' },
          { id: 'zora_crates', propArt: 'cargo_container', x: 25, y: 20, once: false, iconKind: 'crate', label: 'Sealed Plating Crates',
            description: 'Pearl hull plating, crated and stenciled KDY SURPLUS with a lot number that never existed.' },
          { id: 'zora_bench', propArt: 'robotics_bench', x: 22, y: 17, once: false, iconKind: 'crate', label: 'Welder\'s Bench',
            description: 'A half finished valve, a mug of cold caf, and a pair of reading glasses folded on a blueprint.' },
          { id: 'zora_iv_bridge', propArt: 'bridge_console', x: 36, y: 8, once: false, iconKind: 'terminal', label: 'Zora IV Bridge',
            description: 'The bridge console of a Venator class survivor. The flight logs are intact: she limped here after the Drayen Massacre and never left.',
            grantsFlag: 'zora_iv_history_known', grantsCodex: 'codex-zora-iv' },
          { id: 'zora_star_chart', propArt: 'holo_projector', x: 33, y: 11, once: false, iconKind: 'terminal', label: 'Survey Star Chart',
            description: 'A projected map of the cavern grid, tunnels in blue, one node in red: the vault.' },
          { id: 'zora_bridge_racks', propArt: 'server_stack', x: 40, y: 10, once: false, iconKind: 'terminal', label: 'Bridge Data Racks',
            description: 'Black data racks, their labels peeled off one by one.' },
          { id: 'cherit_memorial', propArt: 'memorial_plinth', x: 10, y: 27, once: false, iconKind: 'floor', label: 'Cherit Memorial',
            description: 'A plinth with a folded Republic Intelligence jacket, a datapad and one holo candle. A brass plate reads CHERIT. The candle cell was replaced this month.',
            grantsFlag: 'cherit_memorial_seen', grantsCodex: 'codex-cherit' },
          { id: 'zora_cabin_locker', propArt: 'archive_cabinet', x: 7, y: 26, once: false, iconKind: 'crate', label: 'Cherit\'s Paper Files',
            description: 'A locker of files nobody digitized: dead drops, payrolls, one folder marked KEEL.' },
          { id: 'zora_cabin_desk', propArt: 'datapad_table', x: 13, y: 27, once: false, iconKind: 'terminal', label: 'Memorial Desk',
            description: 'A desk with two chairs, one dusty, the other worn smooth.' },
          { id: 'zora_rebreathers', propArt: 'rebreather_rack', x: 19, y: 27, once: false, iconKind: 'crate', label: 'Spare Rebreathers',
            description: 'Six cylinders, five empty. The Architect rations his breath.' },
          { id: 'zora_drafting', propArt: 'holo_table', x: 23, y: 26, once: false, iconKind: 'terminal', label: 'Drafting Table',
            description: 'A table projecting a rotating keel in red wire. The date stamp in the corner reads last week.' },
          { id: 'architect_terminal', propArt: 'console', propVariant: 'architect', x: 27, y: 27, once: false, iconKind: 'terminal', label: 'The Architect\'s Terminal',
            description: 'Forty three years of private logs. The terminal is unlocked, as if someone expected a visitor.',
            requiresFlag: 'architect_identity_known', lockedMessage: 'The screen asks for a name you cannot give yet. Somebody in the surface trail knows it.',
            grantsFlag: 'architect_logs_read', grantsCodex: 'codex-the-architect' },
          { id: 'hypercore_vault_lock', propArt: 'console', propVariant: 'architect', x: 39, y: 20, once: true, iconKind: 'terminal', label: 'Hypercore Vault Lock',
            description: 'A phrik reinforced vault lock running a Republic protocol that was never meant to be guessed. Bolts as thick as your arm wait behind it.',
            triggersMinigame: 'terminal_slicing', minigameConfig: { difficulty: 6 },
            requiresNoneFlags: ['unit8x_reprogrammed', 'hypercore_vault_open'], grantsFlag: 'hypercore_vault_open' },
          { id: 'hypercore_vault_auth', propArt: 'console', propVariant: 'architect', x: 39, y: 20, once: true, iconKind: 'terminal', label: 'Hypercore Vault Lock (Authorized)',
            description: 'The lock chimes at the authorization Unit 8-X recognised in you and the bolts draw back. Someone wanted this door opened by the right visitor.',
            requiresAllFlags: ['unit8x_reprogrammed'], requiresNoneFlags: ['hypercore_vault_open'], grantsFlag: 'hypercore_vault_open' },
          { id: 'hypercore_guard_stage1', propArt: 'sentinel_droid', x: 38, y: 14, once: true, iconKind: 'floor', label: 'Hull Sentinels',
            description: 'A line of KDY pattern hull sentinels, armed and dormant. They wake the moment the vault lock lets go.',
            requiresFlag: 'hypercore_vault_open', lockedMessage: 'The sentinels are dormant, waiting on the vault lock. Open the lock first.',
            triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'zora_hull_wardens', flavorText: 'The vault bolts withdraw and the hull sentinels wake. They were built to stop exactly one kind of visitor.' },
            grantsFlag: 'warden_guards_down' },
          { id: 'hypercore_warden_prime', propArt: 'warden_prime', x: 40, y: 17, once: true, iconKind: 'floor', label: 'Warden Prime',
            description: 'A four legged defence droid on a hazard striped plinth, one red optic, shoulder cannons the length of a speeder. It stands across the vault threshold.',
            requiresFlag: 'warden_guards_down', lockedMessage: 'The Warden Prime stands behind the sentinel line, powered down. Clear the sentinels first.',
            triggersMinigame: 'tactical_combat', minigameConfig: { opponentProfile: 'ghost_keel_warden', flavorText: 'Warden Prime unfolds from its plinth. Forty three years of maintenance, and it has never fired in earnest.' },
            grantsFlag: 'warden_prime_down' },
          { id: 'hypercore_chamber', propArt: 'hypercore', x: 45, y: 16, once: false, iconKind: 'floor', label: 'Experimental Hypercore',
            description: 'The hypercore turns in its housing, a power rating that should not exist outside a Star Destroyer. The Architect waits beside it.',
            requiresFlag: 'warden_prime_down', lockedMessage: 'The chamber is sealed behind the Warden. The core can wait.',
            grantsFlag: 'hypercore_visible', grantsCodex: 'codex-hypercore' },
          { id: 'ghost_keel_end_kdy', propArt: 'sanctum_holo', x: 44, y: 13, once: false, iconKind: 'terminal', label: 'KDY Retrieval Uplink',
            description: 'Retrieval droids lift drawings from the archive racks. A KDY transit charter glows on the console. Somewhere above, a yard slot has been cleared for a hull that does not exist yet.',
            invisibleLocked: true, requiresFlag: 'hypercore_to_kdy', grantsFlag: 'ghost_keel_ending_seen' },
          { id: 'ghost_keel_end_destroyed', propArt: 'sanctum_holo', x: 44, y: 13, once: false, iconKind: 'terminal', label: 'Cooled Reactor Seat',
            description: 'The hypercore housing is a cold black shell with dark lamps. Ash drifts through the chamber. A Republic seal is burned into the rim: DEMOLITION COMPLETE.',
            invisibleLocked: true, requiresFlag: 'hypercore_destroyed', grantsFlag: 'ghost_keel_ending_seen' },
          { id: 'ghost_keel_end_syndicate', propArt: 'sanctum_holo', x: 44, y: 13, once: false, iconKind: 'terminal', label: 'Syndicate Command Link',
            description: 'Syndicate engineers clamp a command relay to the keel cradle. The hull plate has a new name stenciled in green. The crew already calls her yours.',
            invisibleLocked: true, requiresFlag: 'hypercore_to_syndicate', grantsFlag: 'ghost_keel_ending_seen' },
        ],
        collectibles: [
          { id: 'zora_iv_reactor_log', x: 27, y: 19, label: 'Zora IV Final Reactor Log', reward: 600 },
          { id: 'zora_iv_turbolaser_crystal', x: 7, y: 14, label: 'Turbolaser Focus Crystal', reward: 1200 },
        ],
        npcs: [
          { id: 'the_architect', x: 17, y: 7, kind: 'the_architect', label: 'The Architect', questNpc: true,
            hideAfterFlags: ['architect_dead', 'architect_departed', 'hypercore_choice_made'],
            repeatPrompt: '"What was built here was meant to outlast us."',
            prompt: 'An old man in a rebreather watches the half built keel from the gallery rail. "Tanner\'s contractor. Or something more by now. I drew that frame forty three years ago, and was declared dead with it."',
            choices: [
              { text: 'Who commissioned the Ghost Keel?', morality: 5, loyalty: { republic: 8 }, result: '"An intelligence officer named Cherit. She held that a state able to use force and unwilling to deters more than one that always does." He touches the rail. "The state changed. The ship did not."', grants: { flags: ['cherit_history_known'], codex: ['codex-cherit'] } },
              { text: 'The Syndicate will keep it safer than the Republic.', morality: -15, loyalty: { underworld: 15 }, result: '"I have heard that from spies, from crime lords and from four Senate committees." He does not turn. "Every one was sincere. Ask what it does when it is used."', grants: { flags: ['architect_syndicate_argument'] } },
              { text: 'I came to destroy it.', morality: 20, loyalty: { republic: 15 }, result: '"I thought about that for thirty years, then chose to finish it." He straightens. "If you destroy it, I will not stop you. You may choose differently than I did."', grants: { flags: ['architect_accepted_destruction'], codex: ['codex-the-architect'] } },
            ] },
          { id: 'the_architect_final', x: 44, y: 19, kind: 'the_architect', label: 'The Architect', questNpc: true,
            requiresFlag: 'hypercore_visible', hideAfterFlags: ['hypercore_choice_made'],
            repeatPrompt: '"The choice is still yours."',
            prompt: '"You have seen it." He stands before the hypercore, hands clasped. "Three paths: Tanner\'s contract, the Republic demolition protocol I left armed in this vault, or your own comlink. I stopped having opinions long ago."',
            choices: [
              { text: 'Transmit the schematics to KDY. Complete Tanner\'s contract.', morality: 0, loyalty: { underworld: 5 }, result: 'The transfer completes. Tanner answers within seconds: seventy five thousand credits and a permanent KDY transit charter. The hypercore powers down by remote. You have armed someone else\'s war.', grants: { flags: ['hypercore_choice_made', 'hypercore_to_kdy', 'tanner_contract_completed', 'ghost_keel_complete'], items: ['experimental_hypercore_fragment', 'zora_kdy_transit_charter'], credits: 75000, codex: ['codex-ending-keel-kdy'] } },
              { text: 'Trigger the Republic demolition protocol. Destroy it.', morality: 25, loyalty: { republic: 20 }, result: 'Three minutes on the clock. You walk out as the core passes tolerance. The detonation is a quiet pulse from outside, not an explosion. Something that should never have existed no longer does.', grants: { flags: ['hypercore_choice_made', 'hypercore_destroyed', 'republic_commendation', 'ghost_keel_complete'], items: ['zora_republic_commendation'], credits: 15000, codex: ['codex-ending-keel-destroyed'] } },
              { text: 'Claim it for the Syndicate. This is your flagship now.', morality: -15, loyalty: { underworld: 25 }, result: 'The Syndicate frequency accepts the handshake. "Asset secured." Somewhere on Coruscant a ghost ship just found an owner. It will take months to finish, but it is yours. Your passive income rises.', grants: { flags: ['hypercore_choice_made', 'hypercore_to_syndicate', 'syndicate_flagship_active', 'ghost_keel_complete'], items: ['experimental_hypercore_fragment', 'zora_syndicate_command_key'], codex: ['codex-ending-keel-syndicate'] } },
            ] },
          { id: 'the_architect_after', x: 44, y: 19, kind: 'the_architect', label: 'The Architect', questNpc: true, repeatable: true,
            requiresFlag: 'ghost_keel_complete',
            phases: [
              { id: 'phase_architect_after_kdy', requiresAllFlags: ['hypercore_to_kdy'],
                prompt: 'The Architect watches KDY retrieval droids file past the vault door. "They will finish it faster than I could. That is what troubles me."',
                repeatPrompt: '"Let them take the drawings. Not me."',
                choices: [{ text: 'Will you go with them?', morality: 0, loyalty: {}, result: '"No. I am an old frame in a dead ship. I will stay with her until the last droid leaves."' }] },
              { id: 'phase_architect_after_destroyed', requiresAllFlags: ['hypercore_destroyed'],
                prompt: 'The Architect sits beside the cooled core, his rebreather ticking. "Quiet. Forty three years, and it is only quiet."',
                repeatPrompt: '"Cherit would have laughed."',
                choices: [{ text: 'What will you do now?', morality: 5, loyalty: { republic: 3 }, result: '"Walk out of this mountain. Plant something." A thin smile. "Cherit would have laughed at that too."' }] },
              { id: 'phase_architect_after_syndicate', requiresAllFlags: ['hypercore_to_syndicate'],
                prompt: 'The Architect studies Syndicate engineers measuring the keel. "Your people ask better questions than KDY did. They still want the wrong answers."',
                repeatPrompt: '"Do not call her a flagship in my hearing."',
                choices: [{ text: 'Stay and finish her.', morality: -3, loyalty: { underworld: 3 }, result: '"As a consultant. Pay well, and keep the green paint off the bridge."' }] },
            ] },
        ],
        buildMap() {
          const g = emptyGrid(this.width, this.height);
          carveRect(g, 1, 5, 46, 5, 'floor');       // maintenance catwalk under the hull ribs
          carveRect(g, 1, 14, 4, 19, 'floor');      // airlock vestibule
          carveRect(g, 5, 16, 5, 17, 'floor');      // inner bulkhead
          carveRect(g, 6, 8, 28, 21, 'floor');      // drydock hangar
          carveRect(g, 12, 6, 22, 7, 'floor');      // frame gallery
          carveRect(g, 11, 12, 19, 15, 'wall');     // the keel in its cradle (anchor row 16 stays floor)
          carveRect(g, 6, 24, 15, 29, 'floor');     // Cherit memorial cabin
          carveRect(g, 10, 22, 10, 23, 'floor');
          carveRect(g, 18, 24, 28, 29, 'floor');    // the Architect's quarters
          carveRect(g, 23, 22, 23, 23, 'floor');
          carveRect(g, 31, 6, 41, 11, 'floor');     // bridge
          carveRect(g, 29, 9, 30, 10, 'floor');
          carveRect(g, 29, 15, 36, 18, 'floor');    // reactor corridor
          carveRect(g, 37, 12, 41, 21, 'floor');    // vault antechamber
          carveRect(g, 42, 16, 42, 17, 'floor');    // vault threshold
          carveRect(g, 43, 11, 46, 22, 'floor');    // hypercore chamber
          pt(g, 0, 16, 'door'); pt(g, 0, 17, 'door');
          return g;
        },
      },
    },
  },
};

function hash(x, y) { return Math.abs((x * 73856093) ^ (y * 19349663)) % 100; }

function floorBackground(zone, x, y) {
  const h = hash(x, y);
  if (zone.textureId === 'coruscant') {
    if (h < 10) return `linear-gradient(180deg, ${zone.accentDim}55, ${zone.floorColor})`;
    if (h < 20) return `linear-gradient(90deg, ${zone.floorAlt}, ${zone.floorColor})`;
    if (h < 26) return `linear-gradient(45deg, ${zone.floorColor} 46%, #E8C97A55 48%, #E8C97A55 50%, ${zone.floorColor} 52%)`;
    return zone.floorColor;
  }
  if (zone.textureId === 'kuati') {
    if (h < 14) return `linear-gradient(135deg, ${zone.floorAlt}, ${zone.floorColor})`;
    if (h < 22) return `linear-gradient(45deg, ${zone.floorColor} 47%, #D4AF3730 49%, #D4AF3730 51%, ${zone.floorColor} 53%)`;
    return zone.floorColor;
  }
  if (zone.textureId === 'ferrowake') {
    if (h < 12) return `linear-gradient(135deg, #FF9A5A33, ${zone.floorColor})`;
    if (h < 26) return `linear-gradient(45deg, ${zone.floorAlt}, ${zone.floorColor})`;
    return zone.floorColor;
  }
  if (h < 14) return `radial-gradient(circle at 40% 40%, #6FD9A033, ${zone.floorColor} 75%)`;
  if (h < 28) return `linear-gradient(60deg, ${zone.floorAlt}, ${zone.floorColor})`;
  return zone.floorColor;
}

function wallBackground(zone, x, y) {
  const h = hash(x + 5, y + 5);
  const a = h % 2 === 0 ? zone.wallDark : zone.wallLight;
  if (zone.textureId === 'coruscant') return `repeating-linear-gradient(180deg, ${a}, ${a} 5px, ${zone.wallDark} 5px, ${zone.wallDark} 10px)`;
  if (zone.textureId === 'kuati') return `repeating-linear-gradient(90deg, ${a}, ${a} 6px, ${zone.wallDark} 6px, ${zone.wallDark} 8px)`;
  if (zone.textureId === 'ferrowake') return `repeating-linear-gradient(135deg, ${a}, ${a} 4px, ${zone.wallDark} 4px, ${zone.wallDark} 8px)`;
  return `repeating-linear-gradient(100deg, ${a}, ${a} 4px, ${zone.wallDark} 4px, ${zone.wallDark} 9px)`;
}

function decorFor(zone, x, y, map) {
  if (map) {
    const neighbors = [[x-1,y],[x+1,y],[x,y-1],[x,y+1]];
    const adjacentHazard = neighbors.some(([nx,ny]) => {
      const t = map[ny]?.[nx];
      return t && (t.type === 'lava' || t.type === 'water');
    });
    if (adjacentHazard && Math.abs(hash(x*3, y*7)) % 2 === 0) return 'hazard_stripe';
  }
  const kinds = zone.decor;
  if (!kinds || kinds.length === 0) return null;
  const bucket = Math.floor(15 / kinds.length);
  const h = hash(x * 3 + 7, y * 5 + 11);
  const idx = Math.floor(h / bucket);
  if (h < kinds.length * bucket && idx < kinds.length) return kinds[idx];
  return null;
}

function wallDecorFor(zone, x, y) {
  if (zone.textureId !== 'coruscant') return null;
  return hash(x * 11 + 3, y * 7 + 2) < 8 ? 'wall_insignia' : null;
}

function NpcPortrait({ kind, accent }) {
  if (kind === 'jedi') {
    const skin = '#D9B98C', robe = '#3C4166', robeDark = '#2A2E4A', trim = accent;
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 22 L22 22 L24 42 Z" fill={robe} />
        <path d="M6 42 L9 24 L15 24 L13 42 Z" fill={robeDark} opacity="0.5" />
        <path d="M9 22 L21 22 L19 12 L11 12 Z" fill={robe} />
        <path d="M9 22 C6 22 5 17 7 13 L11 14 L9 22 Z" fill={robe} />
        <path d="M21 22 C24 22 25 17 23 13 L19 14 L21 22 Z" fill={robe} />
        <path d="M9 12 C9 6 11 2 15 2 C19 2 21 6 21 12 C21 14 19 15 15 15 C11 15 9 14 9 12 Z" fill={robeDark} />
        <ellipse cx="15" cy="10.5" rx="4.2" ry="4.8" fill={skin} />
        <circle cx="13.3" cy="10" r="0.6" fill="#2A2320" />
        <circle cx="16.7" cy="10" r="0.6" fill="#2A2320" />
        <ellipse cx="13.3" cy="10" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.6s ease-in-out infinite' }} />
        <ellipse cx="16.7" cy="10" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.6s ease-in-out infinite' }} />
        <path d="M13.4 12.6 Q15 13.4 16.6 12.6" stroke="#8A6E4E" strokeWidth="0.5" fill="none" />
        <rect x="9" y="21.5" width="12" height="1.6" fill={trim} opacity="0.7" />
        <rect x="20" y="20" width="2" height="5" rx="0.8" fill="#C9C5BE" />
      </svg>
    );
  }
  if (kind === 'broker') {
    const skin = '#8FBF9E', skinDark = '#5F8C6D', suit = '#5A4230';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M8 42 L9 26 L21 26 L22 42 Z" fill={suit} />
        <path d="M9 26 L21 26 L20 14 L10 14 Z" fill={suit} />
        <g style={{ transformBox: 'fill-box', transformOrigin: '100% 0%', animation: 'lekku-sway-l 3.8s ease-in-out infinite' }}>
          <path d="M11 16 C9 22 9 30 11 36" stroke={skinDark} strokeWidth="2.2" fill="none" />
        </g>
        <g style={{ transformBox: 'fill-box', transformOrigin: '0% 0%', animation: 'lekku-sway-r 4.2s ease-in-out infinite 0.4s' }}>
          <path d="M19 16 C21 22 21 30 19 36" stroke={skinDark} strokeWidth="2.2" fill="none" />
        </g>
        <ellipse cx="15" cy="10" rx="5" ry="5.6" fill={skin} />
        <rect x="10.5" y="8.5" width="9" height="2.6" rx="1.2" fill="#1A0F0A" opacity="0.85" />
        <circle cx="12.6" cy="9.8" r="1.2" fill={accent} opacity="0.6" style={{ animation: 'lens-flicker 2.6s ease-in-out infinite' }} />
        <circle cx="17.4" cy="9.8" r="1.2" fill={accent} opacity="0.6" style={{ animation: 'lens-flicker 2.6s ease-in-out infinite' }} />
        <path d="M13 13.5 Q15 14.6 17 13.5" stroke={skinDark} strokeWidth="0.6" fill="none" />
        <rect x="9" y="25.5" width="12" height="1.6" fill={accent} opacity="0.5" />
      </svg>
    );
  }
  if (kind === 'warden') {
    const skin = '#3E6B52', skinLight = '#5C9678', robe = '#22331F';
    return (
      <svg viewBox="0 0 32 44" width="27" height="37">
        <path d="M7 44 L8 24 L22 24 L23 44 Z" fill={robe} />
        <path d="M9 24 L21 24 L19 14 L11 14 Z" fill={robe} />
        <g style={{ transformBox: 'fill-box', transformOrigin: '0% 100%', animation: 'tail-flick 3.6s ease-in-out infinite' }}>
          <path d="M22 40 C27 39 29 34 26 30" stroke={skin} strokeWidth="2.4" fill="none" />
        </g>
        <ellipse cx="15" cy="10" rx="5.4" ry="5.8" fill={skin} />
        <ellipse cx="12.3" cy="8.6" rx="1.6" ry="2" fill="#0A0F0A" />
        <ellipse cx="17.7" cy="8.6" rx="1.6" ry="2" fill="#0A0F0A" />
        <ellipse cx="12.3" cy="8.6" rx="1.7" ry="2.1" fill={skin} style={{ animation: 'npc-blink 5.2s ease-in-out infinite' }} />
        <ellipse cx="17.7" cy="8.6" rx="1.7" ry="2.1" fill={skin} style={{ animation: 'npc-blink 5.2s ease-in-out infinite' }} />
        <circle cx="12.7" cy="8" r="0.4" fill="#BFE8D2" opacity="0.6" />
        <circle cx="18.1" cy="8" r="0.4" fill="#BFE8D2" opacity="0.6" />
        <path d="M11 4.5 C11.5 3 12.5 2.5 13 3.5" stroke={skinLight} strokeWidth="0.6" fill="none" />
        <path d="M19 4.5 C18.5 3 17.5 2.5 17 3.5" stroke={skinLight} strokeWidth="0.6" fill="none" />
        <rect x="25" y="2" width="1.6" height="40" fill="#8A7A5C" />
        <circle cx="25.8" cy="2" r="2.4" fill={accent} opacity="0.7" />
      </svg>
    );
  }
  if (kind === 'republic_guard') {
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 22 L22 22 L24 42 Z" fill="#1A2A4A" />
        <path d="M9 22 L21 22 L20 12 L10 12 Z" fill="#243560" />
        <path d="M9 22 C6 22 5 18 7 14 L10 14 L9 22 Z" fill="#1A2A4A" />
        <path d="M21 22 C24 22 25 18 23 14 L20 14 L21 22 Z" fill="#1A2A4A" />
        <rect x="8" y="4" width="14" height="10" rx="3" fill="#1A2A4A" />
        <rect x="9" y="7" width="12" height="4" rx="1" fill={accent} opacity="0.85" style={{ animation: 'lens-flicker 3s ease-in-out infinite' }} />
        <path d="M9 14 L21 14 L20 17 L10 17 Z" fill="#0E1830" />
        <circle cx="15" cy="19" r="1.8" fill={accent} opacity="0.45" />
        <rect x="9" y="21.5" width="12" height="1.6" fill="#0E1830" />
        <rect x="19.5" y="18" width="2" height="6" rx="0.5" fill="#888" />
        <rect x="19.8" y="17" width="1.5" height="2" fill="#666" />
      </svg>
    );
  }
  if (kind === 'droid') {
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="9" y="32" width="4" height="8" rx="1" fill="#7A7A6A" />
        <rect x="17" y="32" width="4" height="8" rx="1" fill="#7A7A6A" />
        <rect x="8" y="31" width="6" height="4" rx="1" fill="#6A6A5A" />
        <rect x="16" y="31" width="6" height="4" rx="1" fill="#6A6A5A" />
        <rect x="9" y="16" width="12" height="15" rx="2" fill="#8A8A7A" />
        <rect x="9" y="16" width="4" height="15" rx="2" fill="#AAAAAA" opacity="0.25" />
        <rect x="5" y="18" width="4" height="8" rx="1" fill="#8A8A7A" />
        <rect x="21" y="18" width="4" height="8" rx="1" fill="#8A8A7A" />
        <circle cx="11" cy="21" r="1" fill="#FF4444" opacity="0.8" />
        <circle cx="14" cy="21" r="1" fill="#44FF44" opacity="0.8" />
        <ellipse cx="15" cy="12" rx="8" ry="7" fill="#9A9A8A" />
        <ellipse cx="15" cy="12" rx="6" ry="5" fill="#AAAAAA" opacity="0.2" />
        <circle cx="15" cy="11" r="3.2" fill="#111" />
        <circle cx="15" cy="11" r="2.4" fill={accent} opacity="0.75" style={{ animation: 'lens-flicker 2s ease-in-out infinite' }} />
        <circle cx="14" cy="10" r="0.7" fill="#FFFFFF" opacity="0.6" />
        <rect x="9" y="14" width="12" height="2" rx="1" fill="#6A6A5A" />
      </svg>
    );
  }
  if (kind === 'smuggler') {
    const skin = '#D9B98C';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M9 42 L10 28 L14 28 L13.5 42 Z" fill="#2A2420" />
        <path d="M15 42 L14.5 28 L18 28 L19 42 Z" fill="#2A2420" />
        <path d="M8 28 L22 28 L20 14 L10 14 Z" fill="#4A3828" />
        <path d="M10 28 L14 28 L13 15 L10 15 Z" fill="#3A2C1E" opacity="0.9" />
        <path d="M16 28 L18 28 L17 15 L15 15 Z" fill="#3A2C1E" opacity="0.9" />
        <path d="M8 28 C5 28 4 24 6 20 L10 20 L8 28 Z" fill="#4A3828" />
        <path d="M22 28 C25 28 26 24 24 20 L20 20 L22 28 Z" fill="#4A3828" />
        <rect x="9" y="27.5" width="12" height="1.8" fill="#2A1E10" />
        <rect x="13" y="27" width="4" height="3" rx="0.5" fill="#C9A050" opacity="0.7" />
        <rect x="11" y="11" width="8" height="3" fill={skin} />
        <ellipse cx="15" cy="9" rx="4.5" ry="4.8" fill={skin} />
        <path d="M10 7 L20 7 L19 4 L11 4 Z" fill="#2A2420" />
        <rect x="9" y="7" width="12" height="1.5" fill="#1A1510" />
        <circle cx="12.8" cy="9" r="0.6" fill="#2A2320" />
        <circle cx="17.2" cy="9" r="0.6" fill="#2A2320" />
        <ellipse cx="12.8" cy="9" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.8s ease-in-out infinite' }} />
        <ellipse cx="17.2" cy="9" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.8s ease-in-out infinite' }} />
        <path d="M13 11.2 Q15 12.2 17 11.2" stroke="#8A6E4E" strokeWidth="0.5" fill="none" />
        <rect x="19.5" y="23" width="2" height="5" rx="0.8" fill="#1A1510" />
        <rect x="19.8" y="22" width="1.4" height="2" fill="#4A4038" />
      </svg>
    );
  }
  if (kind === 'bith') {
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L8 24 L22 24 L23 42 Z" fill="#2A2E4A" />
        <path d="M9 24 L21 24 L19 16 L11 16 Z" fill="#3C4166" />
        <ellipse cx="15" cy="10" rx="8" ry="9" fill="#E8E4DC" />
        <ellipse cx="15" cy="10" rx="6.5" ry="7.5" fill="#F0EDE8" opacity="0.4" />
        <ellipse cx="11.5" cy="11" rx="2.4" ry="3" fill="#111010" />
        <ellipse cx="18.5" cy="11" rx="2.4" ry="3" fill="#111010" />
        <path d="M13 17.5 Q15 18.2 17 17.5" stroke="#BDB9B0" strokeWidth="0.5" fill="none" />
        <rect x="8" y="23.5" width="14" height="1.5" fill={accent} opacity="0.5" />
      </svg>
    );
  }
  if (kind === 'swoop_gang') {
    const skin = '#C8A882', jacket = '#1A1714', scar = '#8A5A4A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 26 L21 26 L23 42 Z" fill={jacket} />
        <path d="M9 26 L21 26 L19 13 L11 13 Z" fill={jacket} />
        <path d="M9 26 C6 26 5 21 7 17 L10 17 L9 26 Z" fill={jacket} />
        <path d="M21 26 C24 26 25 21 23 17 L20 17 L21 26 Z" fill={jacket} />
        <ellipse cx="15" cy="9.5" rx="4.8" ry="5.2" fill={skin} />
        <path d="M10 7 C10 4 12 2 15 2 C18 2 20 4 20 7 Z" fill="#2A2420" />
        <path d="M12.5 9.5 L14.5 11 L13 12.5" stroke={scar} strokeWidth="0.8" fill="none" opacity="0.85" />
        <circle cx="12.8" cy="9" r="0.6" fill="#2A2320" />
        <circle cx="17.2" cy="9" r="0.6" fill="#2A2320" />
        <ellipse cx="12.8" cy="9" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 5s ease-in-out infinite' }} />
        <ellipse cx="17.2" cy="9" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 5s ease-in-out infinite' }} />
        <rect x="18.5" y="22" width="2" height="5" rx="0.7" fill="#2A2420" />
        <rect x="9" y="25.5" width="12" height="1.5" fill="#FF4444" opacity="0.3" />
      </svg>
    );
  }
  if (kind === 'cantina_owner') {
    const skin = '#D4A882', apron = '#C8B89A', shirt = '#4A5068';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M8 42 L9 26 L21 26 L22 42 Z" fill={shirt} />
        <path d="M9 26 L21 26 L19 14 L11 14 Z" fill={shirt} />
        <path d="M9 26 C6 26 5 22 7 18 L10 18 L9 26 Z" fill={shirt} />
        <path d="M21 26 C24 26 25 22 23 18 L20 18 L21 26 Z" fill={shirt} />
        <path d="M11 14 L19 14 L18 26 L12 26 Z" fill={apron} opacity="0.75" />
        <ellipse cx="15" cy="9.5" rx="4.5" ry="5" fill={skin} />
        <path d="M10.5 6 C10.5 3 12.5 2 15 2 C17.5 2 19.5 3 19.5 6 C18 5 16 4.8 15 4.8 C14 4.8 12 5 10.5 6 Z" fill="#2A1E1A" />
        <path d="M19 6 C20 8 20 12 19 14" stroke="#2A1E1A" strokeWidth="1.5" fill="none" />
        <circle cx="12.6" cy="9.5" r="0.6" fill="#2A2320" />
        <circle cx="17.4" cy="9.5" r="0.6" fill="#2A2320" />
        <ellipse cx="12.6" cy="9.5" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.4s ease-in-out infinite' }} />
        <ellipse cx="17.4" cy="9.5" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.4s ease-in-out infinite' }} />
        <path d="M13 12 Q15 13 17 12" stroke="#8A6E4E" strokeWidth="0.5" fill="none" />
        <rect x="9" y="25.5" width="12" height="1.5" fill={apron} opacity="0.6" />
      </svg>
    );
  }
  if (kind === 'crime_boss') {
    const scales = '#4A7A3A', scalesDark = '#2E5026', eye = '#CCAA00';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M5 42 L8 24 L22 24 L25 42 Z" fill="#2A2018" />
        <path d="M8 24 L22 24 L20 12 L10 12 Z" fill="#3A2C1A" />
        <path d="M8 24 C4 24 2 18 5 13 L9 14 L8 24 Z" fill="#2A2018" />
        <path d="M22 24 C26 24 28 18 25 13 L21 14 L22 24 Z" fill="#2A2018" />
        <ellipse cx="15" cy="8.5" rx="5.5" ry="6" fill={scales} />
        <ellipse cx="15" cy="8.5" rx="4" ry="4.5" fill={scalesDark} opacity="0.3" />
        <ellipse cx="12" cy="7.5" rx="2" ry="2.8" fill="#0A0A0A" />
        <ellipse cx="18" cy="7.5" rx="2" ry="2.8" fill="#0A0A0A" />
        <circle cx="12" cy="7" r="0.6" fill={eye} opacity="0.9" />
        <circle cx="18" cy="7" r="0.6" fill={eye} opacity="0.9" />
        <path d="M11 12.5 Q15 14 19 12.5" stroke={scalesDark} strokeWidth="0.8" fill="none" />
        <path d="M13 4 L15 1 L17 4" fill={scalesDark} opacity="0.7" />
        <rect x="8" y="23.5" width="14" height="1.5" fill={accent} opacity="0.4" />
      </svg>
    );
  }
  if (kind === 'mechanic') {
    const skin = '#C9A882', coveralls = '#4A4E58', goggle = '#3A3A42', lens = '#5ABFCC';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M8 42 L9 26 L21 26 L22 42 Z" fill={coveralls} />
        <path d="M9 26 L21 26 L19 13 L11 13 Z" fill={coveralls} />
        <path d="M9 26 C6 26 5 21 7 17 L10 17 L9 26 Z" fill={coveralls} />
        <path d="M21 26 C24 26 25 21 23 17 L20 17 L21 26 Z" fill={coveralls} />
        <ellipse cx="15" cy="9.5" rx="4.8" ry="5.2" fill={skin} />
        <path d="M10.5 6.5 C10.5 3.5 12.2 2 15 2 C17.8 2 19.5 3.5 19.5 6.5 Z" fill="#3A2E22" />
        <rect x="10" y="4.5" width="10" height="3.5" rx="1.5" fill={goggle} />
        <circle cx="12.5" cy="6" r="1.4" fill={lens} opacity="0.75" />
        <circle cx="17.5" cy="6" r="1.4" fill={lens} opacity="0.75" />
        <line x1="14" y1="6" x2="16" y2="6" stroke={goggle} strokeWidth="0.7" />
        <circle cx="12.8" cy="10" r="0.6" fill="#2A2320" />
        <circle cx="17.2" cy="10" r="0.6" fill="#2A2320" />
        <ellipse cx="12.8" cy="10" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.2s ease-in-out infinite' }} />
        <ellipse cx="17.2" cy="10" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 4.2s ease-in-out infinite' }} />
        <path d="M13.2 12.5 Q15 13.3 16.8 12.5" stroke="#8A6E4E" strokeWidth="0.5" fill="none" />
        <rect x="9" y="25.5" width="12" height="1.5" fill={accent} opacity="0.4" />
      </svg>
    );
  }
  if (kind === 'slicer') {
    return (
      <svg viewBox="0 0 48 48" style={{ width: '100%', height: '100%' }}>
        <circle cx="24" cy="18" r="12" fill="#1A2A2A"/>
        <rect x="14" y="14" width="20" height="10" rx="5" fill="#003040" stroke="#00F0FF" strokeWidth="1.5"/>
        <rect x="16" y="16" width="7" height="6" rx="3" fill="#00C8D4" opacity="0.7"/>
        <rect x="25" y="16" width="7" height="6" rx="3" fill="#00C8D4" opacity="0.7"/>
        <line x1="23" y1="19" x2="25" y2="19" stroke="#00F0FF" strokeWidth="1"/>
        <line x1="10" y1="17" x2="14" y2="18" stroke="#00C8D4" strokeWidth="1.5"/>
        <line x1="38" y1="17" x2="34" y2="18" stroke="#00C8D4" strokeWidth="1.5"/>
        <rect x="18" y="28" width="12" height="14" rx="3" fill="#0C1818"/>
        <line x1="22" y1="24" x2="21" y2="30" stroke="#00F0FF" strokeWidth="1" opacity="0.7"/>
        <line x1="26" y1="24" x2="27" y2="30" stroke="#00C8D4" strokeWidth="1" opacity="0.7"/>
      </svg>
    );
  }
  if (kind === 'assassin') {
    return (
      <svg viewBox="0 0 48 48" style={{ width: '100%', height: '100%' }}>
        <ellipse cx="24" cy="15" rx="10" ry="11" fill="#0A0A0A"/>
        <ellipse cx="24" cy="15" rx="10" ry="11" fill="none" stroke="#1A1A1A" strokeWidth="1.5"/>
        <rect x="14" y="20" width="20" height="12" rx="4" fill="#0D0D0D"/>
        <rect x="14" y="28" width="20" height="16" rx="2" fill="#0A0808"/>
        <ellipse cx="20" cy="14" rx="2.5" ry="1.8" fill="#D4A000" opacity="0.9" style={{ animation: 'npc-blink 6s ease-in-out infinite' }}/>
        <ellipse cx="28" cy="14" rx="2.5" ry="1.8" fill="#D4A000" opacity="0.9" style={{ animation: 'npc-blink 6s ease-in-out infinite' }}/>
        <circle cx="20" cy="14" r="1.1" fill="#7A3000"/>
        <circle cx="28" cy="14" r="1.1" fill="#7A3000"/>
        <path d="M14 23 Q10 22 9 19 Q10 17 13 18" stroke="#1A1A1A" strokeWidth="1.5" fill="none" opacity="0.7"/>
        <path d="M34 23 Q38 22 39 19 Q38 17 35 18" stroke="#1A1A1A" strokeWidth="1.5" fill="none" opacity="0.7"/>
        <rect x="16" y="19" width="16" height="3" rx="1.5" fill="#0A0A0A" stroke="#1A1A1A" strokeWidth="0.8"/>
        <line x1="14" y1="28" x2="18" y2="44" stroke="#0A0808" strokeWidth="6" strokeLinecap="round"/>
        <line x1="34" y1="28" x2="30" y2="44" stroke="#0A0808" strokeWidth="6" strokeLinecap="round"/>
      </svg>
    );
  }
  if (kind === 'pit_fighter') {
    const skin = '#C89066', skinDark = '#A07050', scar = '#8A4A38', wrap = '#4A3828', pants = '#3A2E24', boots = '#241E18';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M9 42 L10 32 L14 32 L13.5 42 Z" fill={boots} />
        <path d="M15 42 L14.5 32 L18 32 L19 42 Z" fill={boots} />
        <path d="M9 32 L10 22 L14 22 L14 32 Z" fill={pants} />
        <path d="M14 32 L14 22 L18 22 L19 32 Z" fill={pants} />
        <path d="M7 22 C5 20 4 15 6 13 L10 13 L10 22 L7 22 Z" fill={skin} />
        <path d="M23 22 C25 20 26 15 24 13 L20 13 L20 22 L23 22 Z" fill={skin} />
        <path d="M10 22 L20 22 L20 13 L10 13 Z" fill={skin} />
        <path d="M12 15 Q15 16.5 18 15" stroke={skinDark} strokeWidth="0.7" fill="none" opacity="0.5" />
        <line x1="15" y1="16.5" x2="15" y2="22" stroke={skinDark} strokeWidth="0.5" opacity="0.4" />
        <path d="M5 16 C3 16.5 3 19 5.5 20" stroke={wrap} strokeWidth="2.2" fill="none" strokeLinecap="round" />
        <path d="M25 16 C27 16.5 27 19 24.5 20" stroke={wrap} strokeWidth="2.2" fill="none" strokeLinecap="round" />
        <rect x="9" y="21.5" width="12" height="1.8" fill={boots} opacity="0.9" />
        <rect x="12.5" y="21.5" width="5" height="1.8" fill="#584030" opacity="0.6" />
        <ellipse cx="15" cy="9" rx="4.8" ry="5.2" fill={skin} />
        <path d="M10.5 6.5 C10.5 3.5 12 2 15 2 C18 2 19.5 3.5 19.5 6.5 Z" fill="#1A1410" />
        <path d="M11 8 L13.5 10 L12.8 11.5" stroke={scar} strokeWidth="0.9" fill="none" opacity="0.9" />
        <circle cx="12.8" cy="9.2" r="0.65" fill="#2A2320" />
        <circle cx="17.2" cy="9.2" r="0.65" fill="#2A2320" />
        <ellipse cx="12.8" cy="9.2" rx="0.95" ry="0.72" fill={skin} style={{ animation: 'npc-blink 4.8s ease-in-out infinite' }} />
        <ellipse cx="17.2" cy="9.2" rx="0.95" ry="0.72" fill={skin} style={{ animation: 'npc-blink 4.8s ease-in-out infinite' }} />
        <path d="M13.2 11.5 L16.8 11.5" stroke={skinDark} strokeWidth="0.5" fill="none" opacity="0.7" />
        <rect x="9" y="21" width="12" height="1.5" fill={accent} opacity="0.25" />
      </svg>
    );
  }
  if (kind === 'medic') {
    const skin = '#8AB0C0', skinDark = '#5A8090', coat = '#3A5A3A', coatLight = '#4A7248', lekku = '#6A9090';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M8 42 L9 26 L21 26 L22 42 Z" fill={coat} />
        <path d="M9 26 L21 26 L20 14 L10 14 Z" fill={coatLight} />
        <path d="M9 26 C6 26 5 22 7 18 L10 18 L9 26 Z" fill={coat} />
        <path d="M21 26 C24 26 25 22 23 18 L20 18 L21 26 Z" fill={coat} />
        <path d="M11 14 L15 19 L19 14" fill={coatLight} opacity="0.45" />
        <g style={{ transformBox: 'fill-box', transformOrigin: '100% 0%', animation: 'lekku-sway-l 3.5s ease-in-out infinite' }}>
          <path d="M11 14 C8 20 7 28 9 35" stroke={lekku} strokeWidth="2.8" fill="none" strokeLinecap="round" />
        </g>
        <g style={{ transformBox: 'fill-box', transformOrigin: '0% 0%', animation: 'lekku-sway-r 4s ease-in-out infinite 0.5s' }}>
          <path d="M19 14 C22 20 23 28 21 35" stroke={lekku} strokeWidth="2.8" fill="none" strokeLinecap="round" />
        </g>
        <rect x="20" y="20" width="3" height="5.5" rx="1" fill="#7A8A9A" />
        <circle cx="21.5" cy="21.2" r="0.9" fill={accent} opacity="0.85" style={{ animation: 'lens-flicker 1.8s ease-in-out infinite' }} />
        <ellipse cx="15" cy="9.5" rx="5" ry="5.5" fill={skin} />
        <path d="M10 7 C10 4 12 2 15 2 C18 2 20 4 20 7 Z" fill={lekku} />
        <circle cx="12.8" cy="9.5" r="0.6" fill="#1A1A2A" />
        <circle cx="17.2" cy="9.5" r="0.6" fill="#1A1A2A" />
        <ellipse cx="12.8" cy="9.5" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 5s ease-in-out infinite' }} />
        <ellipse cx="17.2" cy="9.5" rx="0.9" ry="0.7" fill={skin} style={{ animation: 'npc-blink 5s ease-in-out infinite' }} />
        <path d="M13.2 12 Q15 12.8 16.8 12" stroke={skinDark} strokeWidth="0.5" fill="none" />
        <rect x="13.5" y="23.5" width="3" height="1.2" fill={accent} opacity="0.55" />
        <rect x="14.5" y="22.5" width="1" height="3.2" fill={accent} opacity="0.55" />
        <rect x="9" y="25.5" width="12" height="1.5" fill={coatLight} opacity="0.5" />
      </svg>
    );
  }
  if (kind === 'senator') {
    const skin = '#D4C890', skinDark = '#B0A870', robe = '#2A1A4A', robeTrim = '#8A6AAA', robeLight = '#3A2A5C';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 24 L22 24 L24 42 Z" fill={robe} />
        <path d="M9 24 L21 24 L19 12 L11 12 Z" fill={robeLight} />
        <path d="M9 24 C5 24 4 18 7 13 L10 14 L9 24 Z" fill={robe} />
        <path d="M21 24 C25 24 26 18 23 13 L20 14 L21 24 Z" fill={robe} />
        <circle cx="15" cy="18" r="2.2" fill="none" stroke={robeTrim} strokeWidth="0.9" opacity="0.65" />
        <circle cx="15" cy="18" r="0.9" fill={robeTrim} opacity="0.55" />
        <rect x="9" y="23.5" width="12" height="1.5" fill={robeTrim} opacity="0.45" />
        <ellipse cx="15" cy="6.5" rx="4.5" ry="7" fill={skin} />
        <ellipse cx="15" cy="2.5" rx="3.2" ry="2" fill={skinDark} opacity="0.28" />
        <ellipse cx="12.5" cy="8.5" rx="1.2" ry="0.9" fill="#1A1A1A" />
        <ellipse cx="17.5" cy="8.5" rx="1.2" ry="0.9" fill="#1A1A1A" />
        <circle cx="12.8" cy="8.4" r="0.4" fill="#5A4A00" opacity="0.75" />
        <circle cx="17.8" cy="8.4" r="0.4" fill="#5A4A00" opacity="0.75" />
        <ellipse cx="12.5" cy="8.5" rx="1.3" ry="1" fill={skin} style={{ animation: 'npc-blink 6s ease-in-out infinite' }} />
        <ellipse cx="17.5" cy="8.5" rx="1.3" ry="1" fill={skin} style={{ animation: 'npc-blink 6s ease-in-out infinite' }} />
        <path d="M14.5 10.8 L15.5 10.8 L15 11.8 Z" fill={skinDark} opacity="0.45" />
        <path d="M13.5 12.5 L16.5 12.5" stroke={skinDark} strokeWidth="0.6" fill="none" />
        <rect x="3.5" y="18" width="2.5" height="4.5" rx="0.5" fill="#1A1A2A" />
        <rect x="3.8" y="18.5" width="2" height="3.5" fill={accent} opacity="0.28" />
      </svg>
    );
  }
  if (kind === 'archivist') {
    const skin = '#C4A882', skinDark = '#A08864', robe = '#2A3A4A', robeLight = '#364A5A', hair = '#8A8888';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 26 L21 26 L23 42 Z" fill={robe} />
        <path d="M9 26 L21 26 L20 14 L10 14 Z" fill={robeLight} />
        <path d="M9 26 C6 26 5 22 7 18 L10 18 L9 26 Z" fill={robe} />
        <path d="M21 26 C24 26 25 22 23 18 L20 18 L21 26 Z" fill={robe} />
        <rect x="12" y="17" width="6" height="4.5" rx="0.5" fill={accent} opacity="0.28" />
        <line x1="13" y1="18.5" x2="17" y2="18.5" stroke={accent} strokeWidth="0.55" opacity="0.55" />
        <line x1="13" y1="19.8" x2="17" y2="19.8" stroke={accent} strokeWidth="0.55" opacity="0.35" />
        <rect x="9" y="25.5" width="12" height="1.5" fill={robeLight} opacity="0.55" />
        <ellipse cx="15" cy="9.5" rx="4.8" ry="5.2" fill={skin} />
        <path d="M10.5 7 C10.5 3.5 12.2 2 15 2 C17.8 2 19.5 3.5 19.5 7 Z" fill={hair} />
        <path d="M10.5 7 C9 8 9 12 10.5 12" stroke={hair} strokeWidth="1.8" fill="none" />
        <path d="M19.5 7 C21 8 21 12 19.5 12" stroke={hair} strokeWidth="1.8" fill="none" />
        <line x1="11" y1="10" x2="12.8" y2="10" stroke={skinDark} strokeWidth="0.4" opacity="0.28" />
        <line x1="17.2" y1="10" x2="19" y2="10" stroke={skinDark} strokeWidth="0.4" opacity="0.28" />
        <circle cx="12.8" cy="9.5" r="1.5" fill="none" stroke={accent} strokeWidth="0.55" opacity="0.65" />
        <circle cx="17.2" cy="9.5" r="1.5" fill="none" stroke={accent} strokeWidth="0.55" opacity="0.65" />
        <line x1="14.3" y1="9.5" x2="15.7" y2="9.5" stroke={accent} strokeWidth="0.55" opacity="0.45" />
        <circle cx="12.8" cy="9.5" r="0.6" fill="#2A2320" />
        <circle cx="17.2" cy="9.5" r="0.6" fill="#2A2320" />
        <ellipse cx="12.8" cy="9.5" rx="0.85" ry="0.65" fill={skin} style={{ animation: 'npc-blink 5.5s ease-in-out infinite' }} />
        <ellipse cx="17.2" cy="9.5" rx="0.85" ry="0.65" fill={skin} style={{ animation: 'npc-blink 5.5s ease-in-out infinite' }} />
        <path d="M13.5 12 Q15 12.8 16.5 12" stroke={skinDark} strokeWidth="0.5" fill="none" />
        <rect x="3.5" y="18" width="3" height="5" rx="0.5" fill="#101820" />
        <rect x="3.9" y="18.5" width="2.2" height="4" fill={accent} opacity="0.22" />
      </svg>
    );
  }

  if (kind === 'kdy_commander') {
    const coat = '#1C2A39', ala = '#E8ECEF', gold = '#D4AF37', skin = '#C8956A', hair = '#3A3F45';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 22 L22 22 L24 42 Z" fill={coat} />
        <rect x="8" y="36" width="5" height="6" fill="#16191E" /><rect x="17" y="36" width="5" height="6" fill="#16191E" /><rect x="8" y="36" width="5" height="1.2" fill={gold} /><rect x="17" y="36" width="5" height="1.2" fill={gold} />
        <path d="M8 22 L22 22 L20 11 L10 11 Z" fill={coat} />
        <path d="M11 11 L15 17 L19 11 L19 9 L11 9 Z" fill={coat} /><rect x="10.4" y="9.4" width="9.2" height="2" fill={ala} />
        <path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={coat} />
        <path d="M4 12 L10 11 L10 14 L5 15 Z M26 12 L20 11 L20 14 L25 15 Z" fill={ala} /><rect x="4.4" y="13" width="5" height="0.8" fill={gold} /><rect x="20.6" y="13" width="5" height="0.8" fill={gold} />
        {[15, 18.4, 21.8].map((y, i) => <circle key={i} cx="15" cy={y} r="1" fill={gold} />)}
        <rect x="4" y="22" width="3" height="2.4" fill="#16191E" /><rect x="23" y="22" width="3" height="2.4" fill="#16191E" /><rect x="8" y="21.4" width="14" height="1.2" fill={gold} opacity="0.7" />
        <rect x="11" y="2" width="8" height="9" rx="2" fill={skin} />
        <path d="M10.6 5 C10.6 0 19.4 0 19.4 5 L19.4 3 L10.6 3 Z" fill={hair} /><path d="M12.6 10.4 L17.4 10.4 L16.4 12 L13.6 12 Z" fill={hair} />
        <rect x="12" y="5.8" width="2" height="1.2" fill="#1A1410" />
        <circle cx="17" cy="6.2" r="2.1" fill="#9C7F3C" /><circle cx="17" cy="6.2" r="1.3" fill="#00E5FF" /><circle cx="17" cy="6.2" r="0.5" fill="#FFFFFF" />
        <path d="M18.4 8 L19 11" stroke="#9C7F3C" strokeWidth="0.6" />
      </svg>
    );
  }
  if (kind === 'the_architect') {
    const skin = '#8A7A5A', coat = '#4A3A22', hazard = '#D4762C', rebreath = '#333840';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M5 42 L8 22 L22 22 L25 42 Z" fill={coat} />
        <path d="M8 22 L22 22 L20 11 L10 11 Z" fill={hazard} />
        <path d="M8 22 C5 22 3 16 5 11 L9 12 L8 22 Z" fill={coat} />
        <path d="M22 22 C25 22 27 16 25 11 L21 12 L22 22 Z" fill={coat} />
        <ellipse cx="15" cy="7.5" rx="5" ry="5.5" fill={skin} />
        <rect x="11" y="9" width="8" height="4" rx="2" fill={rebreath} />
        <rect x="13" y="10" width="4" height="2" rx="1" fill="#555A60" />
        <circle cx="12" cy="11" r="0.85" fill={accent} opacity="0.8" />
        <circle cx="18" cy="11" r="0.85" fill={accent} opacity="0.8" />
        <circle cx="12.5" cy="6.8" r="0.65" fill="#2A2320" />
        <circle cx="17.5" cy="6.8" r="0.65" fill="#2A2320" />
        <ellipse cx="12.5" cy="6.8" rx="0.95" ry="0.75" fill={skin} style={{ animation: 'npc-blink 6s ease-in-out infinite' }} />
        <ellipse cx="17.5" cy="6.8" rx="0.95" ry="0.75" fill={skin} style={{ animation: 'npc-blink 6s ease-in-out infinite' }} />
        <rect x="8" y="21.5" width="14" height="1.5" fill={hazard} opacity="0.5" />
      </svg>
    );
  }
  if (kind === 'besalisk_boss') {
    const scale = '#6A5A3A', scaleDark = '#4A3A24', eye = '#FF8800', cyber = '#8A8A8A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M3 42 L7 22 L23 22 L27 42 Z" fill="#3A2E1E" />
        <path d="M7 22 L23 22 L21 9 L9 9 Z" fill="#4A3A28" />
        <path d="M9 9 C4 9 1 5 3 1 L7 3 L9 9 Z" fill="#3A2E1E" />
        <path d="M21 9 C26 9 29 5 27 1 L23 3 L21 9 Z" fill="#3A2E1E" />
        <path d="M8 17 C3 17 1 13 3 9 L6 10 L8 17 Z" fill="#4A3A28" />
        <path d="M22 17 C27 17 29 13 27 9 L24 10 L22 17 Z" fill="#4A3A28" />
        <ellipse cx="15" cy="5" rx="7" ry="5.5" fill={scale} />
        <ellipse cx="9.5" cy="7.5" rx="2" ry="2.5" fill={scale} />
        <ellipse cx="20.5" cy="7.5" rx="2" ry="2.5" fill={scale} />
        <ellipse cx="12" cy="3.8" rx="2" ry="1.6" fill="#0A0A0A" />
        <ellipse cx="18" cy="3.8" rx="2" ry="1.6" fill="#0A0A0A" />
        <circle cx="12" cy="3.6" r="0.7" fill={eye} opacity="0.9" />
        <circle cx="18" cy="3.6" r="0.7" fill={eye} opacity="0.9" />
        <rect x="21" y="2" width="3" height="4.5" rx="0.5" fill={cyber} opacity="0.8" />
        <rect x="21.4" y="2.5" width="2.2" height="3.5" fill={accent} opacity="0.4" />
        <rect x="7" y="21.5" width="16" height="1.5" fill={accent} opacity="0.4" />
      </svg>
    );
  }
  if (kind === 'trandoshan_sniper') {
    const scale = '#5A7050', scaleDark = '#3A5030', eye = '#CC7700', suit = '#2A3020';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 24 L21 24 L23 42 Z" fill={suit} />
        <path d="M9 24 L21 24 L19 12 L11 12 Z" fill="#3A4028" />
        <path d="M9 24 C6 24 5 18 7 13 L10 14 L9 24 Z" fill={suit} />
        <path d="M21 24 C24 24 25 18 23 13 L20 14 L21 24 Z" fill={suit} />
        <rect x="1" y="9.5" width="9" height="1.5" rx="0.5" fill="#555" />
        <rect x="1" y="8.8" width="1.5" height="1.2" fill="#666" />
        <rect x="8.5" y="8.5" width="1.2" height="3" fill="#444" />
        <ellipse cx="15" cy="7.5" rx="5.5" ry="5" fill={scale} />
        <path d="M12 10 L15 13 L18 10" fill={scaleDark} />
        <ellipse cx="12.5" cy="6.8" rx="1.5" ry="2" fill="#0A0A0A" />
        <ellipse cx="17.5" cy="6.8" rx="1.5" ry="2" fill="#0A0A0A" />
        <rect x="12.2" y="5.6" width="0.65" height="2.6" fill={eye} opacity="0.9" />
        <rect x="17.2" y="5.6" width="0.65" height="2.6" fill={eye} opacity="0.9" />
        <rect x="9" y="23.5" width="12" height="1.5" fill={accent} opacity="0.4" />
      </svg>
    );
  }
  // SENATE PORTRAITS BEGIN
  if (kind === 'senator_horace') {
    const robe = '#7A1E2C', gold = '#D4AF37', skin = '#D9B98C', hair = '#B8B4B0';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M5 42 L8 22 L22 22 L25 42 Z" fill={robe} />
        <path d="M13 22 L15 42 L17 42 L17 22 Z" fill="#5A1420" />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={robe} />
        <path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={robe} />
        <path d="M4 12 L10 11 L10 15 L5 16 Z M26 12 L20 11 L20 15 L25 16 Z" fill={gold} /><path d="M9 12 L15 19 L21 12 L21 9 L9 9 Z" fill="#5A1420" /><rect x="9" y="8.6" width="12" height="1.6" fill={gold} />
        <rect x="11" y="2" width="8" height="9" rx="2" fill={skin} /><path d="M10.4 5 C10.4 0 19.6 0 19.6 5 L19.6 3 L10.4 3 Z" fill={hair} />
        <rect x="12.2" y="6" width="1.8" height="1.2" fill="#2A1A10" /><rect x="16" y="6" width="1.8" height="1.2" fill="#2A1A10" /><path d="M12.6 9.4 L17.4 9.4" stroke="#9A8A7A" strokeWidth="0.8" />
        <circle cx="6" cy="26" r="1.2" fill="#3AB0FF" /><circle cx="24" cy="26" r="1.2" fill="#E02A55" />
        <rect x="26" y="14" width="1.4" height="26" fill="#6A4A2A" /><rect x="25.4" y="12" width="2.6" height="3" fill="#CBD3DC" /><rect x="25.6" y="38.6" width="2.2" height="2.4" fill="#CBD3DC" />
      </svg>
    );
  }
  if (kind === 'kuati_baroness') {
    const gown = '#1E2E52', silver = '#CBD3DC', skin = '#D2A98A', hair = '#2A1E1A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M4 42 L8 22 L22 22 L26 42 Z" fill={gown} />
        <path d="M13 24 L15 42 L17 42 L17 24 Z" fill="#14203C" />
        <path d="M8 22 L22 22 L20 11 L10 11 Z" fill={gown} />
        <path d="M8 22 C5 22 4 18 6 13 L9 14 L8 22 Z M22 22 C25 22 26 18 24 13 L21 14 L22 22 Z" fill={gown} />
        <path d="M3 13 L10 11 L10 15 L4 17 Z M27 13 L20 11 L20 15 L26 17 Z" fill={silver} /><rect x="10" y="20.4" width="10" height="1.2" fill={silver} />
        <path d="M11 11 L15 16 L19 11 L19 9 L11 9 Z" fill="#14203C" />
        <rect x="11.6" y="3" width="6.8" height="8" rx="2" fill={skin} />
        <path d="M11 3 C11 -1 19 -1 19 3 L19 1.6 L11 1.6 Z M12.6 -2 L17.4 -2 L18 1 L12 1 Z M13.4 -4 L16.6 -4 L17 -2 L13 -2 Z" fill={hair} transform="translate(0 4)" />
        <rect x="14.4" y="0" width="1.2" height="3" fill={silver} transform="translate(0 0)" /><circle cx="15" cy="0.2" r="0.9" fill={silver} />
        <rect x="12.6" y="6" width="1.6" height="1.2" fill="#1A1410" /><rect x="15.8" y="6" width="1.6" height="1.2" fill="#1A1410" /><rect x="13.6" y="8.6" width="2.8" height="0.9" fill="#A03040" />
        <rect x="20" y="22" width="5" height="3" fill="#E8ECEF" /><rect x="20.4" y="22.4" width="4.2" height="2.2" fill="#7FD0FF" opacity="0.8" />
      </svg>
    );
  }
  if (kind === 'sis_agent') {
    const tunic = '#6A7078', trim = '#4A4F56', skin = '#C8956A', hair = '#2A2320';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill="#3A3F45" />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={tunic} /><path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={tunic} />
        <rect x="14.4" y="12" width="1.2" height="10" fill={trim} /><rect x="9" y="21" width="12" height="1.4" fill="#1A1C1E" /><rect x="17" y="23" width="3" height="2" fill="#1A1C1E" />
        <rect x="11" y="2" width="8" height="9" rx="2" fill={skin} /><path d="M10.4 5 C10.4 0 19.6 0 19.6 5 L19.6 3.6 L10.4 3.6 Z" fill={hair} />
        <rect x="12.2" y="6" width="1.8" height="1.2" fill="#1A1410" /><rect x="16" y="6" width="1.8" height="1.2" fill="#1A1410" />
        <rect x="19.6" y="6" width="1.6" height="2.6" fill="#D2A98A" /><circle cx="20.4" cy="9.4" r="0.8" fill="#7FD0FF" />
        <rect x="21" y="18" width="5" height="4" fill="#0A1A24" /><rect x="21.6" y="18.6" width="3.8" height="2.6" fill="#7FD0FF" opacity="0.8" />
      </svg>
    );
  }
  if (kind === 'twilek_diplomat') {
    const skin = '#3AA6A0', robe = '#5A2E7A', gold = '#D4AF37';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M4 42 L8 22 L22 22 L26 42 Z" fill={robe} /><path d="M10 26 C12 34 18 34 20 26" fill="none" stroke={gold} strokeWidth="1.2" />
        <path d="M8 22 L22 22 L20 11 L10 11 Z" fill="#7A4AA0" /><path d="M8 22 C5 22 3 18 5 13 L9 14 L8 22 Z M22 22 C25 22 27 18 25 13 L21 14 L22 22 Z" fill={robe} />
        <path d="M9 13 C13 20 17 20 21 13" fill="none" stroke={gold} strokeWidth="1.6" /><rect x="9" y="21" width="12" height="1.2" fill={gold} />
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        <path d="M10.6 4 C5 6 4 16 7 24 L9 23 C7 16 8 10 11 8 Z M19.4 4 C25 6 26 16 23 24 L21 23 C23 16 22 10 19 8 Z" fill="#2E8C86" />
        <rect x="6" y="18" width="3" height="1.2" fill={gold} /><rect x="21" y="18" width="3" height="1.2" fill={gold} />
        <rect x="12.2" y="6.4" width="1.8" height="1.2" fill="#1A1410" /><rect x="16" y="6.4" width="1.8" height="1.2" fill="#1A1410" /><rect x="13.6" y="9" width="2.8" height="0.9" fill="#B03050" />
      </svg>
    );
  }
  if (kind === 'alsakan_aristocrat') {
    const coat = '#1F6A4A', shirt = '#F0E6C8', gold = '#D4AF37', skin = '#E0BC96', hair = '#EAE4D8';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 22 L22 22 L24 42 Z" fill="#14402E" />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={shirt} /><path d="M9 12 L14 12 L14 30 L9 32 Z M21 12 L16 12 L16 30 L21 32 Z" fill={coat} />
        {[15, 18.4, 21.8, 25.2].map((y, i) => <circle key={i} cx="14.2" cy={y} r="0.8" fill={gold} />)}
        <path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={coat} /><rect x="4" y="19" width="4" height="1.2" fill={gold} /><rect x="22" y="19" width="4" height="1.2" fill={gold} />
        <path d="M10 12 L15 18 L20 12 Z" fill="#E8DCC0" />
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        <path d="M9.6 6 C9 -1 21 -1 20.4 6 L20.4 3 L9.6 3 Z" fill={hair} /><path d="M10 -1 L15 -3 L20 -1 L15 1 Z" fill={coat} /><rect x="14.6" y="-3" width="0.8" height="3" fill={gold} />
        <rect x="12.2" y="6.4" width="1.8" height="1.2" fill="#1A1410" /><rect x="16" y="6.4" width="1.8" height="1.2" fill="#1A1410" />
      </svg>
    );
  }
  if (kind === 'czerka_executive') {
    const suit = '#2A2E34', shirt = '#E8ECEF', tie = '#B02A2A', skin = '#C8956A', hair = '#1E1A18';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill="#1E2126" />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={suit} /><path d="M12 11 L15 22 L18 11 Z" fill={shirt} /><path d="M14 13 L16 13 L16.6 24 L15 26 L13.4 24 Z" fill={tie} />
        <path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={suit} /><rect x="4" y="12" width="4" height="1" fill="#B02A2A" /><rect x="22" y="12" width="4" height="1" fill="#B02A2A" />
        <rect x="11" y="2" width="8" height="9" rx="2" fill={skin} /><path d="M10.4 5 C10.4 0 19.6 0 19.6 5 L19.6 3 L10.4 3 Z" fill={hair} />
        <rect x="12" y="6" width="1.8" height="1.2" fill="#1A1410" /><circle cx="17.2" cy="6.4" r="2" fill="#0A1A24" /><circle cx="17.2" cy="6.4" r="1.3" fill="#FF3030" /><circle cx="17.2" cy="6.4" r="0.5" fill="#FFB0B0" />
        <path d="M19 8 L19.6 11" stroke="#8E9AA8" strokeWidth="0.8" /><rect x="21" y="20" width="4" height="3" fill="#E8ECEF" />
      </svg>
    );
  }
  if (kind === 'lux_sommelier') {
    const rose = '#C98A7A', roseHi = '#E8B4A4', shade = '#9A6458';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="10" y="30" width="3.4" height="12" fill={rose} /><rect x="16.6" y="30" width="3.4" height="12" fill={rose} />
        <path d="M9 12 L21 12 L19 30 L11 30 Z" fill={rose} /><path d="M9 12 L13 12 L12 30 L11 30 Z" fill={shade} opacity="0.6" />
        <rect x="10.4" y="20" width="9.2" height="1.4" fill="#D4AF37" /><rect x="10.4" y="29" width="9.2" height="1.4" fill="#D4AF37" />
        <path d="M5 13 L9 12 L9 26 L6.4 26 Z M25 13 L21 12 L21 26 L23.6 26 Z" fill={rose} />
        <rect x="5" y="22" width="3.6" height="1.4" fill="#D4AF37" /><rect x="21.4" y="22" width="3.6" height="1.4" fill="#D4AF37" />
        <rect x="11" y="2" width="8" height="10" fill={rose} /><rect x="10" y="3" width="1.4" height="7" fill={shade} />
        <circle cx="13" cy="6" r="1.7" fill="#FFB844" /><circle cx="17" cy="6" r="1.7" fill="#FFB844" /><circle cx="13" cy="6" r="0.6" fill="#FFF8DC" /><circle cx="17" cy="6" r="0.6" fill="#FFF8DC" />
        <rect x="13" y="9" width="4" height="1.1" fill={shade} />
        <polygon points="11.4,12 15,14.6 18.6,12 18.6,14.4 15,16 11.4,14.4" fill="#0A0A0F" /><rect x="14" y="12.6" width="2" height="2" fill="#0A0A0F" />
        <rect x="3" y="14" width="3" height="8" fill="#D8E6F0" opacity="0.8" /><rect x="3.4" y="17" width="2.2" height="4.6" fill="#E8742A" opacity="0.9" />
      </svg>
    );
  }
  if (kind === 'sv_tray_droid') {
    const chrome = '#E0E6EE', shade = '#8E9AA8', rub = '#2A2E34';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <ellipse cx="15" cy="40" rx="11" ry="2" fill="#0A0A0F" opacity="0.4" />
        <polygon points="15,38 8,35 22,35" fill="#7FD0FF" opacity="0.5" />
        <path d="M2 28 L6 33 L24 33 L28 28 L24 24 L6 24 Z" fill={shade} /><path d="M2 28 L6 24 L24 24 L28 28 L24 26 L6 26 Z" fill={chrome} />
        <path d="M6 33 L24 33 L26 30 L4 30 Z" fill={chrome} opacity="0.7" /><circle cx="15" cy="29" r="2" fill="#7FD0FF" /><circle cx="15" cy="29" r="0.8" fill="#FFFFFF" />
        <rect x="4" y="22" width="22" height="3" fill={rub} />
        <polygon points="7,22 8,12 12,12 13,22" fill="#C8E6F4" opacity="0.85" /><rect x="8" y="16" width="3.4" height="5.6" fill="#E8742A" opacity="0.85" />
        <polygon points="17,22 18,14 22,14 23,22" fill="#C8E6F4" opacity="0.85" /><rect x="18" y="18" width="3.4" height="3.6" fill="#A02A4A" opacity="0.85" />
        <rect x="13" y="19" width="4" height="3" fill="#F0E6C8" />
      </svg>
    );
  }
  if (kind === 'sweep_drone') {
    const gold = '#D4AF37', dark = '#7A5E1E';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <ellipse cx="15" cy="36" rx="9" ry="1.6" fill="#0A0A0F" opacity="0.35" />
        {[[7, 28, 2, 36], [9, 30, 5, 38], [21, 30, 25, 38], [23, 28, 28, 36], [8, 25, 3, 29], [22, 25, 27, 29]].map(([x1, y1, x2, y2], i) => <polyline key={i} points={`${x1},${y1} ${(x1 + x2) / 2},${y1 - 3} ${x2},${y2}`} fill="none" stroke={dark} strokeWidth="1.2" />)}
        <ellipse cx="15" cy="28" rx="8" ry="5" fill={gold} /><ellipse cx="15" cy="26.4" rx="6" ry="2.6" fill="#E8D890" opacity="0.8" />
        <path d="M8 28 L22 28" stroke={dark} strokeWidth="0.8" /><path d="M11 24 L11 32 M15 23 L15 33 M19 24 L19 32" stroke={dark} strokeWidth="0.6" opacity="0.6" />
        <circle cx="15" cy="22" r="3.4" fill={gold} /><circle cx="13.6" cy="21.4" r="1" fill="#FF3030" /><circle cx="16.4" cy="21.4" r="1" fill="#FF3030" />
        <path d="M12 19 L9 14 M18 19 L21 14" stroke={dark} strokeWidth="1" />
        <polygon points="15,31 6,40 24,40" fill="#FF6060" opacity="0.07" style={{ animation: 'holo-flicker 1.6s ease-in-out infinite' }} />
      </svg>
    );
  }
  if (kind === 'black_sun_envoy') {
    const gown = '#1F3E7A', trim = '#C8CFE0', skin = '#D2A98A', hair = '#16100E';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M4 42 L8 22 L22 22 L26 42 Z" fill={gown} /><path d="M12 24 L15 42 L18 42 L18 24 Z" fill="#14284C" />
        <path d="M8 22 L22 22 L20 11 L10 11 Z" fill={gown} /><path d="M8 22 C5 22 4 18 6 13 L9 14 L8 22 Z M22 22 C25 22 26 18 24 13 L21 14 L22 22 Z" fill={gown} />
        <path d="M10 12 L15 22 L20 12 L20 10 L10 10 Z" fill={trim} opacity="0.9" /><rect x="8" y="21" width="14" height="1.2" fill={trim} />
        <circle cx="15" cy="17" r="2.6" fill="#0A0A0F" /><circle cx="15" cy="17" r="1.6" fill="#FFB844" /><circle cx="15" cy="17" r="0.8" fill="#0A0A0F" />
        {Array.from({ length: 8 }, (_, i) => <line key={i} x1="15" y1="17" x2={15 + Math.cos(i * Math.PI / 4) * 3.2} y2={17 + Math.sin(i * Math.PI / 4) * 3.2} stroke="#0A0A0F" strokeWidth="0.7" />)}
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        <path d="M10 6 C9 -1 21 -1 20 6 L20 4 L10 4 Z M9 4 C7 6 8 12 10 14 L11 8 Z" fill={hair} />
        <rect x="12.2" y="6.4" width="1.8" height="1.2" fill="#1A1410" /><rect x="16" y="6.4" width="1.8" height="1.2" fill="#1A1410" /><rect x="13.4" y="9" width="3.2" height="1" fill="#8A1A3A" />
      </svg>
    );
  }
  // SENATE PORTRAITS END
  // BADOR PORTRAITS BEGIN
  if (kind === 'kuati_officer') {
    const tunic = '#243348', swath = '#F0F4F8', gold = '#C5A059', skin = '#C8956A', sapph = '#0F52BA';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 22 L22 22 L24 42 Z" fill={tunic} />
        <rect x="14" y="30" width="2" height="12" fill={gold} opacity="0.8" />
        <path d="M8 22 L22 22 L20 11 L10 11 Z" fill={tunic} />
        <path d="M9 12 L14 12 L21 22 L16 22 Z" fill={swath} />
        <path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z" fill={tunic} /><path d="M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={tunic} />
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        <path d="M9 5 L11 1 L19 1 L21 5 L19 4 L11 4 Z" fill={tunic} /><rect x="9" y="4" width="12" height="1.4" fill={gold} />
        <circle cx="15" cy="2.6" r="1.2" fill="#9C7F3C" />
        <rect x="12.4" y="6.4" width="1.8" height="1.4" fill="#1A1410" /><rect x="16" y="6.4" width="1.8" height="1.4" fill="#1A1410" />
        <rect x="17" y="17" width="2" height="3" fill="#CBD3DC" /><rect x="19.4" y="17" width="2" height="3" fill={sapph} />
        <rect x="8" y="21" width="14" height="1.6" fill="#16191E" /><rect x="14" y="20.8" width="2.6" height="2.2" fill={gold} />
        <rect x="7" y="23" width="1" height="19" fill={gold} opacity="0.6" />
      </svg>
    );
  }
  if (kind === 'k_series_droid') {
    const plate = '#F5F5F5', frame = '#1A1C1E', lens = '#FF0022', gold = '#D4AF37', shield = '#4A8CFF';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="10" y="30" width="4" height="12" fill={frame} /><rect x="16" y="30" width="4" height="12" fill={frame} />
        <path d="M9 40 L14 40 L14 42 L8 42 Z M16 40 L21 40 L22 42 L16 42 Z" fill={plate} />
        <path d="M9 22 L21 22 L20 32 L10 32 Z" fill={frame} />
        <path d="M3 12 L27 12 L25 24 L22 24 L20 32 L10 32 L8 24 L5 24 Z" fill={plate} />
        <path d="M3 12 L9 12 L8 20 L4 20 Z M27 12 L21 12 L22 20 L26 20 Z" fill="#CBD3DC" />
        <rect x="11" y="1" width="8" height="10" fill={plate} /><rect x="12" y="2" width="6" height="8" fill={frame} />
        <rect x="11.6" y="5" width="6.8" height="1.8" fill={lens} /><rect x="12" y="5.4" width="6" height="0.8" fill="#FF8090" opacity="0.7" />
        <circle cx="15" cy="17" r="2.6" fill="none" stroke={gold} strokeWidth="1" /><circle cx="15" cy="17" r="0.8" fill={gold} />
        <rect x="0" y="12" width="4" height="12" fill={frame} /><rect x="0" y="12" width="1.2" height="12" fill={shield} />
        <rect x="26" y="14" width="4" height="9" fill={frame} /><rect x="27.2" y="22" width="1.6" height="5" fill="#8E9AA8" />
      </svg>
    );
  }
  if (kind === 'ge3_protocol') {
    const silver = '#E0E0E0', gold = '#FFD700', shade = '#8E9AA8';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="10" y="30" width="3.4" height="12" fill={silver} /><rect x="16.6" y="30" width="3.4" height="12" fill={silver} />
        <rect x="9.6" y="33" width="4.2" height="1.4" fill={gold} /><rect x="16.2" y="33" width="4.2" height="1.4" fill={gold} />
        <path d="M9 12 L21 12 L19 30 L11 30 Z" fill={silver} /><path d="M9 12 L13 12 L12 30 L11 30 Z" fill={shade} opacity="0.6" />
        <rect x="10" y="20" width="10" height="1.6" fill={gold} /><rect x="10.6" y="29" width="8.8" height="1.4" fill={gold} />
        <path d="M5 13 L9 12 L9 26 L6.4 26 Z" fill={silver} /><path d="M25 13 L21 12 L21 26 L23.6 26 Z" fill={silver} />
        <rect x="5" y="19" width="3.6" height="1.4" fill={gold} /><rect x="21.4" y="19" width="3.6" height="1.4" fill={gold} />
        <rect x="11" y="2" width="8" height="10" fill={silver} /><rect x="10" y="3" width="1.4" height="7" fill={shade} />
        <circle cx="13" cy="6" r="1.7" fill={gold} /><circle cx="17" cy="6" r="1.7" fill={gold} /><circle cx="13" cy="6" r="0.6" fill="#FFF8DC" /><circle cx="17" cy="6" r="0.6" fill="#FFF8DC" />
        <rect x="13" y="9" width="4" height="1.2" fill={shade} />
        <rect x="12.6" y="11.4" width="4.8" height="1.4" fill={gold} />
      </svg>
    );
  }
  if (kind === 'binary_loader') {
    const body = '#E5A900', dark = '#1A1A1A', orange = '#FF6600';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="3" y="36" width="24" height="6" fill={dark} /><rect x="5" y="37" width="20" height="2" fill="#4A5562" />
        <path d="M6 14 L24 14 L26 36 L4 36 Z" fill={body} />
        <path d="M6 14 L10 14 L8 36 L4 36 Z" fill="#B88400" />
        <rect x="8" y="18" width="14" height="6" fill={dark} /><rect x="9" y="19" width="12" height="1.4" fill="#4A5562" /><rect x="9" y="21.6" width="12" height="1.4" fill="#4A5562" />
        <path d="M0 14 L6 14 L6 30 L3 30 L3 20 L0 20 Z M30 14 L24 14 L24 30 L27 30 L27 20 L30 20 Z" fill={body} />
        <path d="M0 18 L3 18 L3 20 L0 20 Z M27 18 L30 18 L30 20 L27 20 Z M2.6 28 L6 28 L6 30 L2.6 30 Z M24 28 L27.4 28 L27.4 30 L24 30 Z" fill={dark} />
        <rect x="12" y="9" width="6" height="5" fill={body} /><rect x="13" y="10" width="4" height="2" fill={dark} /><circle cx="15" cy="11" r="0.9" fill="#FF4040" />
        <rect x="13.4" y="5" width="3.2" height="4" fill={dark} /><circle cx="15" cy="3.6" r="2.6" fill={orange} /><circle cx="15" cy="3.6" r="1.2" fill="#FFE9B0" />
        <rect x="6" y="30" width="18" height="1.6" fill={dark} />
      </svg>
    );
  }
  if (kind === 'gnk_power') {
    const body = '#6A6E74', dark = '#2A2E34', cable = '#E5C100';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="9" y="34" width="4" height="8" fill={dark} /><rect x="17" y="34" width="4" height="8" fill={dark} /><rect x="7" y="40" width="7" height="2" fill="#4A5562" /><rect x="16" y="40" width="7" height="2" fill="#4A5562" />
        <path d="M5 10 L25 10 L25 34 L5 34 Z" fill={body} /><path d="M5 10 L9 10 L9 34 L5 34 Z" fill="#8E9AA8" opacity="0.5" />
        <rect x="8" y="14" width="14" height="9" fill={dark} /><rect x="9" y="15" width="12" height="2" fill="#4A5562" /><rect x="9" y="18" width="12" height="2" fill="#4A5562" />
        <circle cx="12" cy="28" r="1.4" fill="#3AFF8A" /><circle cx="16" cy="28" r="1.4" fill="#FFB844" /><circle cx="20" cy="28" r="1.4" fill="#FF4040" />
        <rect x="11" y="5" width="8" height="5" fill={dark} /><rect x="13.4" y="6" width="3.2" height="2" fill="#FFB844" />
        <path d="M25 18 C30 18 30 30 24 30" fill="none" stroke={cable} strokeWidth="1.6" /><rect x="24" y="16" width="3" height="4" fill={dark} />
      </svg>
    );
  }
  if (kind === 'kuati_astromech') {
    const dome = '#F5F5F5', body = '#0F52BA', lens = '#7FD0FF', dark = '#1A2A44';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="5" y="36" width="6" height="6" fill="#CBD3DC" /><rect x="19" y="36" width="6" height="6" fill="#CBD3DC" /><rect x="12" y="38" width="6" height="4" fill="#8E9AA8" />
        <rect x="8" y="16" width="14" height="22" fill={body} /><rect x="5" y="20" width="3" height="16" fill="#CBD3DC" /><rect x="22" y="20" width="3" height="16" fill="#CBD3DC" />
        <rect x="10" y="20" width="10" height="4" fill={dark} /><rect x="11" y="21" width="3" height="2" fill="#3AFF8A" /><rect x="16" y="21" width="3" height="2" fill="#FFB844" />
        <rect x="10" y="27" width="10" height="2" fill="#D4AF37" /><rect x="10" y="31" width="10" height="5" fill="#243348" />
        <path d="M7 16 C7 5 23 5 23 16 Z" fill={dome} /><path d="M7 16 C7 9 12 6 14 6 L14 16 Z" fill="#CBD3DC" />
        <circle cx="15" cy="11" r="3.4" fill={dark} /><circle cx="15" cy="11" r="2" fill={lens} /><circle cx="15" cy="11" r="0.8" fill="#FFFFFF" />
        <rect x="8.4" y="12.6" width="13.2" height="1.4" fill={body} /><rect x="19" y="7" width="2.4" height="2" fill={body} />
      </svg>
    );
  }
  if (kind === 'zabrak_techwright') {
    const skin = '#C8956A', horn = '#2A2320', shirt = '#6A7078', apron = '#5A3A22';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill="#3A3F45" />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={shirt} />
        <path d="M10 14 L20 14 L21 34 L9 34 Z" fill={apron} /><rect x="10" y="22" width="10" height="1.4" fill="#2A1A0A" />
        <path d="M8 22 L4 26 L6 30 L9 24 Z M22 22 L26 26 L24 30 L21 24 Z" fill={skin} /><rect x="4" y="27" width="3.4" height="2.2" fill={apron} /><rect x="22.6" y="27" width="3.4" height="2.2" fill={apron} />
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        {[[12, 3, 11, 0], [14, 2.4, 14, -0.6], [16, 2.4, 16, -0.6], [18, 3, 19, 0], [11, 4, 9.6, 1.4], [19, 4, 20.4, 1.4]].map(([x, y, x2, y2], i) => <polygon key={i} points={`${x - 0.8},${y + 1} ${x + 0.8},${y + 1} ${x2},${y2}`} fill={horn} />)}
        <rect x="12.2" y="6.4" width="1.8" height="1.2" fill="#1A1410" /><rect x="16" y="6.4" width="1.8" height="1.2" fill="#1A1410" />
        <path d="M13 5 L17 5 M13.6 9.6 L16.4 9.6 M15 5 L15 9" stroke="#3A1A0A" strokeWidth="0.7" />
        <rect x="21" y="20" width="6" height="4" fill="#0A1A24" /><rect x="21.6" y="20.6" width="4.8" height="2.8" fill="#7FD0FF" opacity="0.85" />
      </svg>
    );
  }
  if (kind === 'devaronian_inspector') {
    const skin = '#B8302A', horn = '#15110F', vest = '#6A1A22', shirt = '#F0E6C8';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill="#22252A" />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={shirt} />
        <path d="M9 12 L14 12 L14 30 L9 32 Z M21 12 L16 12 L16 30 L21 32 Z" fill={vest} />
        <path d="M12 14 C13 20 17 20 18 14" fill="none" stroke="#D4AF37" strokeWidth="1" /><circle cx="15" cy="19" r="1" fill="#D4AF37" />
        <path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={vest} />
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        <path d="M11 3 C9 1 8 -1 9 -2 C10 0 12 1 13 2 Z M19 3 C21 1 22 -1 21 -2 C20 0 18 1 17 2 Z" fill={horn} transform="translate(0 3)" />
        <path d="M10.4 6 L8.6 4.6 L10.4 4.8 Z M19.6 6 L21.4 4.6 L19.6 4.8 Z" fill={skin} />
        <rect x="12.2" y="6.2" width="1.8" height="1.2" fill="#FFE9B0" /><rect x="16" y="6.2" width="1.8" height="1.2" fill="#FFE9B0" />
        <path d="M13 9.6 L14 10.6 L15 9.6 L16 10.6 L17 9.6" fill="none" stroke="#FFFFFF" strokeWidth="0.6" />
        <rect x="19.6" y="4.4" width="3" height="0.8" fill="#C8A830" />
      </svg>
    );
  }
  if (kind === 'nautolan_engineer') {
    const skin = '#6A7A52', tress = '#4E5A3A', suit = '#2A4A6A', copper = '#C98655';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={suit} />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={suit} /><path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill="#1F3A55" />
        <path d="M11 12 C10 16 11 20 12 24 M19 12 C20 16 19 20 18 24 M15 12 L15 28" fill="none" stroke="#4A8CFF" strokeWidth="1.1" />
        <rect x="13.4" y="26" width="3.2" height="4" fill="#12202E" /><circle cx="15" cy="28" r="1" fill="#7FD0FF" />
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        <path d="M10 5 C6 6 5 16 7 22 L9 21 C8 15 9 9 11 8 Z M20 5 C24 6 25 16 23 22 L21 21 C22 15 21 9 19 8 Z" fill={tress} />
        <rect x="6.4" y="14" width="3" height="1.2" fill={copper} /><rect x="20.6" y="14" width="3" height="1.2" fill={copper} />
        <ellipse cx="12.6" cy="6.8" rx="2" ry="2.6" fill="#0A0A0F" /><ellipse cx="17.4" cy="6.8" rx="2" ry="2.6" fill="#0A0A0F" />
        <circle cx="12" cy="6" r="0.7" fill="#FFFFFF" opacity="0.8" /><circle cx="16.8" cy="6" r="0.7" fill="#FFFFFF" opacity="0.8" />
      </svg>
    );
  }
  if (kind === 'kdy_contractor') {
    const suit = '#FF6600', pad = '#22252A', visor = '#D4AF37', skin = '#C8956A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M5 42 L7 22 L23 22 L25 42 Z" fill={suit} /><rect x="7" y="34" width="6" height="5" fill={pad} /><rect x="17" y="34" width="6" height="5" fill={pad} />
        <path d="M6 22 L24 22 L23 10 L7 10 Z" fill={suit} /><path d="M6 22 L10 22 L9 10 L7 10 Z" fill="#CC5200" />
        <rect x="12" y="12" width="6" height="12" fill="#CC5200" /><rect x="7" y="9" width="3" height="4" fill={pad} /><rect x="20" y="9" width="3" height="4" fill={pad} />
        <path d="M6 22 C3 22 1 17 3 11 L7 12 L6 22 Z M24 22 C27 22 29 17 27 11 L23 12 L24 22 Z" fill={suit} /><rect x="2" y="16" width="4" height="3" fill={pad} /><rect x="24" y="16" width="4" height="3" fill={pad} />
        <rect x="11" y="4" width="8" height="8" rx="2" fill={skin} /><rect x="12" y="8" width="6" height="2" fill="#7A5A3A" opacity="0.5" />
        <rect x="12.2" y="6.6" width="1.6" height="1.2" fill="#1A1410" /><rect x="16.2" y="6.6" width="1.6" height="1.2" fill="#1A1410" />
        <path d="M9 5 C9 -1 21 -1 21 5 L21 2 L9 2 Z" fill="#3A3F45" /><path d="M9 2 L21 2 L22 -1 L8 -1 Z" fill={visor} transform="translate(0 3)" />
        <rect x="19" y="24" width="8" height="2" fill="#1A1C1E" /><rect x="22" y="23" width="3" height="4" fill="#7FD0FF" />
      </svg>
    );
  }
  if (kind === 'cyborg_mechanic') {
    const skin = '#C8956A', metal = '#8E9AA8', apron = '#4A3A22', shirt = '#5A5F66';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill="#2A2E34" />
        <path d="M8 22 L22 22 L21 11 L9 11 Z" fill={shirt} />
        <path d="M10 13 L20 13 L21 36 L9 36 Z" fill={apron} /><rect x="9" y="30" width="12" height="2.4" fill="#2A1A0A" opacity="0.6" /><rect x="12" y="16" width="6" height="4" fill="#2A1A0A" opacity="0.5" />
        <path d="M8 22 L3 28 L5 33 L9 26 Z" fill={skin} /><rect x="3" y="30" width="3.4" height="3" fill="#7A5A3A" />
        <path d="M22 22 L27 25 L28 32 L24 32 L22 26 Z" fill={metal} /><rect x="22.4" y="30" width="5.4" height="2.4" fill="#5F6E80" /><rect x="23" y="26" width="1.2" height="3" fill="#FFB844" />
        <rect x="11" y="3" width="8" height="9" rx="2" fill={skin} />
        <path d="M17 3 L19 3 L19 12 L15 12 L15 9 L17 8 Z" fill={metal} /><rect x="16" y="9.4" width="3" height="1.4" fill="#5F6E80" />
        <circle cx="17.6" cy="6" r="2.6" fill="#1A1410" /><circle cx="17.6" cy="6" r="1.7" fill="#FF6600" /><circle cx="17.6" cy="6" r="0.7" fill="#FFE9B0" />
        <rect x="12" y="6.2" width="1.8" height="1.2" fill="#1A1410" />
        <path d="M10 3 C10 0 20 0 20 3 L20 4 L10 4 Z" fill="#3A3F45" /><rect x="10" y="4.4" width="10" height="1" fill="#7A5A3A" />
      </svg>
    );
  }
  // BADOR PORTRAITS END
  if (kind === 'kdy_pilot') {
    const coat = '#1F4A78', trim = '#1A8FD0', collar = '#E8E4D8', gold = '#C8A830', skin = '#C8956A', cap = '#16345A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M5 42 L7 22 L23 22 L25 42 Z" fill={coat} />
        <path d="M13 22 L15 42 L17 42 L17 22 Z" fill={trim} />
        <path d="M7 22 L23 22 L21 12 L9 12 Z" fill={coat} />
        <path d="M7 22 C4 22 2 17 4 12 L8 13 L7 22 Z" fill={trim} />
        <path d="M23 22 C26 22 28 17 26 12 L22 13 L23 22 Z" fill={trim} />
        <path d="M10 12 L15 18 L20 12 L18 11 L15 14 L12 11 Z" fill={collar} />
        <rect x="11" y="3" width="8" height="9" rx="2.2" fill={skin} />
        <path d="M9.5 6 C9.5 0.5 20.5 0.5 20.5 6 L20.5 4.6 L9.5 4.6 Z" fill={cap} />
        <rect x="9" y="4" width="12" height="2" rx="0.6" fill={trim} />
        <rect x="11" y="6.2" width="3" height="2" rx="0.6" fill="#BFF4FF" opacity="0.85" />
        <rect x="16" y="6.2" width="3" height="2" rx="0.6" fill="#BFF4FF" opacity="0.85" />
        <path d="M10 17 L13 15.4 L13 18 Z M20 17 L17 15.4 L17 18 Z" fill={gold} />
        <rect x="9" y="21" width="12" height="1.4" fill={gold} opacity="0.85" />
        <rect x="6" y="12" width="3" height="1" rx="0.3" fill={gold} />
        <rect x="21" y="12" width="3" height="1" rx="0.3" fill={gold} />
      </svg>
    );
  }
  if (kind === 'kdy_ring_sec') {
    const armor = '#2A3A5A', pauldron = '#4A5A7A', visor = '#D4901A', crest = '#8A9AB0';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 22 L22 22 L24 42 Z" fill={armor} />
        <path d="M8 22 L22 22 L20 11 L10 11 Z" fill={pauldron} />
        <path d="M8 22 C5 22 3 17 5 12 L9 13 L8 22 Z" fill={pauldron} />
        <path d="M22 22 C25 22 27 17 25 12 L21 13 L22 22 Z" fill={pauldron} />
        <rect x="10" y="1" width="10" height="10" rx="1.5" fill={armor} />
        <rect x="11" y="4" width="8" height="2.5" rx="0.4" fill={visor} opacity="0.9" />
        <rect x="12" y="4.2" width="6" height="1.5" fill="#FFD88A" opacity="0.3" />
        <circle cx="8" cy="15" r="2.5" fill={pauldron} />
        <circle cx="22" cy="15" r="2.5" fill={pauldron} />
        <rect x="12" y="11" width="6" height="1" rx="0.3" fill={crest} opacity="0.7" />
        <path d="M13 13 L15 11.5 L17 13 Z" fill={crest} opacity="0.55" />
        <rect x="8" y="21.5" width="14" height="1.5" fill={visor} opacity="0.4" />
      </svg>
    );
  }
  if (kind === 'kdy_executive_sentinel') {
    const plate = '#E8E4D8', gold = '#C8A830', cape = '#8A1A1A', visor = '#1A1410';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M5 42 L7 22 L23 22 L25 42 Z" fill={cape} opacity="0.85" />
        <path d="M7 22 L23 22 L21 10 L9 10 Z" fill={plate} />
        <path d="M7 22 C4 22 2 17 4 11 L8 12 L7 22 Z" fill={plate} />
        <path d="M23 22 C26 22 28 17 26 11 L22 12 L23 22 Z" fill={plate} />
        <rect x="9" y="0.5" width="12" height="11" rx="2" fill={plate} />
        <rect x="10" y="5" width="10" height="3.5" rx="0.4" fill={visor} />
        <rect x="11" y="5.3" width="8" height="2" fill="#2A1A0A" opacity="0.5" />
        <rect x="9" y="14" width="12" height="1" fill={gold} opacity="0.9" />
        <rect x="9" y="15.5" width="12" height="0.6" fill={gold} opacity="0.55" />
        <rect x="6" y="11" width="3" height="1" rx="0.3" fill={gold} opacity="0.8" />
        <rect x="21" y="11" width="3" height="1" rx="0.3" fill={gold} opacity="0.8" />
        <rect x="9" y="21.5" width="12" height="1.5" fill={gold} opacity="0.5" />
      </svg>
    );
  }
  if (kind === 'kdy_logistics_officer') {
    const suit = '#2E3238', collar = '#3A3E44', cyber = '#4A9AC8', datapad = '#5A5A60';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={suit} />
        <path d="M9 22 L21 22 L19 11 L11 11 Z" fill={suit} />
        <path d="M9 22 C6 22 4 18 6 12 L10 13 L9 22 Z" fill={suit} />
        <path d="M21 22 C24 22 26 18 24 12 L20 13 L21 22 Z" fill={suit} />
        <rect x="9" y="13" width="4" height="7" rx="0.5" fill={datapad} opacity="0.7" />
        <rect x="14" y="13" width="4" height="5" rx="0.5" fill={datapad} opacity="0.55" />
        <ellipse cx="15" cy="6" rx="5.5" ry="5.5" fill="#8A7A6A" />
        <ellipse cx="12.5" cy="5" rx="0.9" ry="0.7" fill="#2A1A1A" />
        <ellipse cx="17.5" cy="5" rx="0.9" ry="0.7" fill="#2A1A1A" />
        <rect x="22" y="12" width="3" height="5" rx="1" fill={cyber} opacity="0.85" />
        <rect x="22.3" y="12.3" width="2.4" height="4.4" fill="#88DEFF" opacity="0.3" />
        <rect x="8" y="10" width="5" height="1" rx="0.3" fill={collar} />
        <rect x="17" y="10" width="5" height="1" rx="0.3" fill={collar} />
        <rect x="9" y="21.5" width="12" height="1.5" fill={cyber} opacity="0.35" />
      </svg>
    );
  }
  if (kind === 'imperial_naval_liaison') {
    const tunic = '#4A4E3A', trim = '#3A3E2E', rank = '#C8A830', glove = '#1A1A1A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={tunic} />
        <path d="M9 22 L21 22 L19 11 L11 11 Z" fill={tunic} />
        <path d="M9 22 C6 22 4 17 6 12 L10 13 L9 22 Z" fill={trim} />
        <path d="M21 22 C24 22 26 17 24 12 L20 13 L21 22 Z" fill={trim} />
        <ellipse cx="15" cy="5.5" rx="5" ry="5" fill="#9A8A78" />
        <ellipse cx="12.5" cy="4.8" rx="0.85" ry="0.75" fill="#2A1A14" />
        <ellipse cx="17.5" cy="4.8" rx="0.85" ry="0.75" fill="#2A1A14" />
        <rect x="10" y="13" width="10" height="1" rx="0.4" fill={rank} opacity="0.9" />
        <rect x="11" y="14.5" width="3" height="1" rx="0.3" fill={rank} opacity="0.75" />
        <rect x="16" y="14.5" width="3" height="1" rx="0.3" fill={rank} opacity="0.75" />
        <rect x="11" y="8" width="3" height="3" rx="0.3" fill="#888A82" opacity="0.7" />
        <rect x="16" y="8" width="3" height="3" rx="0.3" fill="#888A82" opacity="0.7" />
        <rect x="6" y="22" width="5" height="7" rx="0.5" fill={glove} opacity="0.9" />
        <rect x="19" y="22" width="5" height="7" rx="0.5" fill={glove} opacity="0.9" />
        <rect x="9" y="21.5" width="12" height="1.5" fill={rank} opacity="0.4" />
      </svg>
    );
  }
  if (kind === 'vectis_droid') {
    const chassis = '#7A6040', dark = '#3A2A18', lens = accent || '#44AACC', arm = '#5A4A30';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <rect x="8" y="0" width="14" height="12" rx="2" fill={dark} />
        <rect x="9" y="1" width="12" height="10" rx="1.5" fill={chassis} />
        <circle cx="11.5" cy="5" r="2" fill="#0A0A0A" />
        <circle cx="11.5" cy="5" r="1.3" fill={lens} opacity="0.9" />
        <circle cx="18.5" cy="5" r="2" fill="#0A0A0A" />
        <circle cx="18.5" cy="5" r="1.3" fill={lens} opacity="0.9" />
        <circle cx="15" cy="8.5" r="1.2" fill="#0A0A0A" />
        <circle cx="15" cy="8.5" r="0.7" fill={lens} opacity="0.7" />
        <rect x="9" y="11.5" width="12" height="1" fill={dark} opacity="0.6" />
        <rect x="7" y="12" width="16" height="16" rx="1.5" fill={chassis} />
        <rect x="8" y="13" width="14" height="4" rx="0.5" fill={dark} opacity="0.7" />
        <rect x="9" y="14" width="5" height="2" rx="0.3" fill={lens} opacity="0.4" />
        <rect x="16" y="13.5" width="1" height="5" rx="0.2" fill="#AAAAAA" opacity="0.5" />
        <path d="M3 6 L7 12 L7 20 L3 24 Z" fill={arm} opacity="0.7" />
        <rect x="8" y="21.5" width="14" height="1.5" fill={chassis} opacity="0.5" />
      </svg>
    );
  }
  if (kind === 'kdy_guild_overseer') {
    const robe = '#182858', gold = '#C8A030', skin = '#D4B090', mono = accent || '#FFAA00';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 24 L21 24 L23 42 Z" fill={robe} />
        <path d="M9 24 L21 24 L20 13 L10 13 Z" fill={robe} />
        <rect x="8" y="11" width="4" height="3" rx="0.5" fill={gold} opacity="0.9" />
        <rect x="18" y="11" width="4" height="3" rx="0.5" fill={gold} opacity="0.9" />
        <rect x="5" y="22" width="4" height="6" rx="0.5" fill={robe} opacity="0.8" />
        <rect x="21" y="22" width="4" height="6" rx="0.5" fill={robe} opacity="0.8" />
        <ellipse cx="15" cy="7" rx="5" ry="5.5" fill={skin} />
        <ellipse cx="12.5" cy="6.2" rx="0.9" ry="0.8" fill="#2A1A14" />
        <ellipse cx="17.5" cy="6.2" rx="0.9" ry="0.8" fill="#2A1A14" />
        <circle cx="17.5" cy="6.2" r="1.6" fill="none" stroke={mono} strokeWidth="0.7" opacity="0.9" />
        <line x1="19.1" y1="6.2" x2="21" y2="7" stroke={mono} strokeWidth="0.5" />
        <rect x="9" y="12" width="12" height="1.5" rx="0.3" fill={gold} opacity="0.5" />
        <path d="M11 13 L9 18 L10 24 L11 23 L11 18 Z" fill={robe} opacity="0.6" />
        <path d="M19 13 L21 18 L20 24 L19 23 L19 18 Z" fill={robe} opacity="0.6" />
      </svg>
    );
  }
  if (kind === 'republic_navy_inspector') {
    const uni = '#2A3A5A', silver = '#B0B8C0', skin = '#C8A88A', glv = '#E8E4D8';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={uni} />
        <path d="M9 22 L21 22 L20 12 L10 12 Z" fill={uni} />
        <ellipse cx="15" cy="6" rx="5" ry="5.5" fill={skin} />
        <ellipse cx="12.5" cy="5.5" rx="0.85" ry="0.75" fill="#2A1A14" />
        <ellipse cx="17.5" cy="5.5" rx="0.85" ry="0.75" fill="#2A1A14" />
        <rect x="9" y="12" width="12" height="1.2" fill={silver} opacity="0.6" />
        <rect x="10" y="14" width="3" height="0.9" rx="0.3" fill={silver} opacity="0.9" />
        <rect x="14" y="14" width="3" height="0.9" rx="0.3" fill={silver} opacity="0.9" />
        <rect x="10" y="15.5" width="2" height="0.9" rx="0.3" fill={silver} opacity="0.7" />
        <rect x="13.5" y="15.5" width="2" height="0.9" rx="0.3" fill={silver} opacity="0.7" />
        <rect x="17" y="15.5" width="2" height="0.9" rx="0.3" fill={silver} opacity="0.7" />
        <rect x="5" y="22" width="5" height="7" rx="0.5" fill={glv} opacity="0.9" />
        <rect x="20" y="22" width="5" height="7" rx="0.5" fill={glv} opacity="0.9" />
        <path d="M9.5 12 L8 16 L9 22 L10 22 L10 17 Z" fill={uni} opacity="0.7" />
        <path d="M20.5 12 L22 16 L21 22 L20 22 L20 17 Z" fill={uni} opacity="0.7" />
      </svg>
    );
  }
  if (kind === 'kdy_shipwright') {
    const apron = '#4A3218', suit = '#3A4028', skin = '#C8A480', goggle = '#8A7038';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={suit} />
        <path d="M9 22 L21 22 L20 13 L10 13 Z" fill={suit} />
        <rect x="10" y="16" width="10" height="20" rx="1" fill={apron} opacity="0.85" />
        <ellipse cx="15" cy="7" rx="5" ry="5.5" fill={skin} />
        <ellipse cx="12.5" cy="6.5" rx="0.9" ry="0.8" fill="#2A1A14" />
        <ellipse cx="17.5" cy="6.5" rx="0.9" ry="0.8" fill="#2A1A14" />
        <rect x="10" y="3" width="5" height="2.5" rx="1" fill={goggle} opacity="0.9" />
        <rect x="15" y="3" width="5" height="2.5" rx="1" fill={goggle} opacity="0.9" />
        <rect x="9.5" y="3.5" width="11" height="1.5" rx="0.5" fill={goggle} opacity="0.5" />
        <rect x="5" y="22" width="5" height="8" rx="0.5" fill={suit} opacity="0.7" />
        <rect x="20" y="22" width="5" height="8" rx="0.5" fill={apron} opacity="0.8" />
        <rect x="10" y="22" width="10" height="1" fill={apron} opacity="0.6" />
      </svg>
    );
  }
  if (kind === 'deuterium_specialist') {
    const suit = '#D44818', patch = '#E8E0D0', skin = '#C89870', resp = '#2A2A2A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 21 L22 21 L24 42 Z" fill={suit} />
        <path d="M8 21 L22 21 L21 12 L9 12 Z" fill={suit} />
        <ellipse cx="15" cy="6.5" rx="5.5" ry="6" fill={skin} />
        <ellipse cx="12.5" cy="6" rx="0.9" ry="0.8" fill="#2A1A14" />
        <ellipse cx="17.5" cy="6" rx="0.9" ry="0.8" fill="#2A1A14" />
        <rect x="11" y="9.5" width="8" height="3.5" rx="1.5" fill={resp} opacity="0.85" />
        <rect x="12.5" y="10" width="5" height="1.5" rx="0.5" fill="#3A3A3A" opacity="0.6" />
        <rect x="9" y="12" width="5" height="4" rx="0.5" fill={patch} opacity="0.8" />
        <rect x="16" y="14" width="5" height="3" rx="0.5" fill={patch} opacity="0.8" />
        <rect x="5" y="21" width="5" height="8" rx="0.5" fill={suit} opacity="0.8" />
        <rect x="20" y="21" width="5" height="8" rx="0.5" fill={suit} opacity="0.8" />
        <line x1="8" y1="28" x2="8" y2="32" stroke={patch} strokeWidth="1" opacity="0.6" />
        <line x1="22" y1="28" x2="22" y2="32" stroke={patch} strokeWidth="1" opacity="0.6" />
        <rect x="9" y="21" width="12" height="1" fill={patch} opacity="0.4" />
      </svg>
    );
  }
  if (kind === 'czerka_liaison') {
    const suit = '#8A1818', gold = '#C8A020', skin = '#D4B090', hair = '#1A1010';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={suit} />
        <path d="M9 22 L21 22 L20 12 L10 12 Z" fill={suit} />
        <path d="M10 12 L9 20 L10 22 L11 18 Z" fill={gold} opacity="0.7" />
        <path d="M20 12 L21 20 L20 22 L19 18 Z" fill={gold} opacity="0.7" />
        <rect x="13" y="17" width="4" height="5" fill={gold} opacity="0.5" />
        <ellipse cx="15" cy="6.5" rx="5" ry="5.5" fill={skin} />
        <rect x="10" y="1.5" width="10" height="5" rx="2.5" fill={hair} opacity="0.85" />
        <ellipse cx="12.5" cy="6" rx="0.9" ry="0.8" fill="#2A1A14" />
        <ellipse cx="17.5" cy="6" rx="0.9" ry="0.8" fill="#2A1A14" />
        <rect x="13.5" y="8.5" width="3" height="0.8" rx="0.3" fill={gold} opacity="0.6" />
        <rect x="5" y="22" width="5" height="7" rx="0.5" fill={suit} opacity="0.9" />
        <rect x="20" y="22" width="5" height="7" rx="0.5" fill={suit} opacity="0.9" />
        <rect x="9" y="21" width="12" height="1.5" fill={gold} opacity="0.4" />
      </svg>
    );
  }
  if (kind === 'kuati_sub_director') {
    const robe = '#4A2858', silver = '#B8C0C8', skin = '#D4C0A0', silk = '#E8E0D8';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L9 23 L21 23 L24 42 Z" fill={robe} />
        <path d="M9 23 L21 23 L20 13 L10 13 Z" fill={robe} />
        <rect x="8" y="11" width="5" height="3" rx="1" fill={silver} opacity="0.9" />
        <rect x="17" y="11" width="5" height="3" rx="1" fill={silver} opacity="0.9" />
        <ellipse cx="15" cy="7" rx="4.5" ry="5" fill={skin} />
        <ellipse cx="12.5" cy="6.5" rx="0.85" ry="0.75" fill="#3A2A20" />
        <ellipse cx="17.5" cy="6.5" rx="0.85" ry="0.75" fill="#3A2A20" />
        <rect x="14" y="17" width="2" height="8" rx="0.5" fill={silk} opacity="0.7" />
        <path d="M9.5 13 L7 18 L9 23 L10 22 L9 18 Z" fill={robe} opacity="0.7" />
        <path d="M20.5 13 L23 18 L21 23 L20 22 L21 18 Z" fill={robe} opacity="0.7" />
        <rect x="5" y="23" width="5" height="7" rx="0.5" fill={robe} opacity="0.8" />
        <rect x="20" y="23" width="5" height="7" rx="0.5" fill={robe} opacity="0.8" />
        <rect x="9" y="22" width="12" height="1.5" fill={silver} opacity="0.3" />
      </svg>
    );
  }
  if (kind === 'republic_pilot') {
    const suit = '#D4501A', wht = '#E8E4D8', skin = '#C8A87A', badge = accent || '#44AAFF';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={suit} />
        <path d="M9 22 L21 22 L20 12 L10 12 Z" fill={suit} />
        <rect x="8" y="17" width="5" height="5" rx="0.5" fill={wht} opacity="0.85" />
        <rect x="17" y="17" width="5" height="5" rx="0.5" fill={wht} opacity="0.85" />
        <ellipse cx="15" cy="6.5" rx="5" ry="5.5" fill={skin} />
        <ellipse cx="12.5" cy="6" rx="0.9" ry="0.8" fill="#2A1A14" />
        <ellipse cx="17.5" cy="6" rx="0.9" ry="0.8" fill="#2A1A14" />
        <rect x="9" y="12" width="5" height="2" rx="0.4" fill={wht} opacity="0.7" />
        <rect x="9" y="14.5" width="3" height="1.5" rx="0.3" fill={badge} opacity="0.8" />
        <rect x="5" y="22" width="5" height="8" rx="0.5" fill={suit} opacity="0.8" />
        <rect x="20" y="22" width="5" height="8" rx="0.5" fill={suit} opacity="0.8" />
        <rect x="10" y="12" width="10" height="1" fill={wht} opacity="0.3" />
      </svg>
    );
  }
  if (kind === 'sub_deck_slicer') {
    const cov = '#1A2A1A', dp = accent || '#44AAFF', skin = '#C89870', belt = '#2A1A0A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={cov} />
        <path d="M9 22 L21 22 L20 12 L10 12 Z" fill={cov} />
        <ellipse cx="15" cy="6.5" rx="4.8" ry="5.2" fill={skin} />
        <ellipse cx="12.5" cy="6" rx="0.9" ry="0.8" fill="#1A1A1A" />
        <ellipse cx="17.5" cy="6" rx="0.9" ry="0.8" fill="#1A1A1A" />
        <path d="M9 2 L9 8 L15 10 L21 8 L21 2 L15 1 Z" fill={cov} opacity="0.7" />
        <rect x="5" y="22" width="4" height="8" rx="0.5" fill={cov} opacity="0.8" />
        <rect x="4" y="25" width="5" height="4" rx="1" fill={dp} opacity="0.6" />
        <rect x="4.5" y="25.5" width="4" height="3" rx="0.5" fill={dp} opacity="0.4" />
        <rect x="21" y="22" width="4" height="8" rx="0.5" fill={cov} opacity="0.8" />
        <rect x="9" y="20" width="12" height="2" fill={belt} opacity="0.8" />
        <rect x="10" y="20.5" width="2" height="1" rx="0.3" fill="#5A3A1A" />
        <rect x="14" y="20.5" width="2" height="1" rx="0.3" fill="#5A3A1A" />
        <rect x="18" y="20.5" width="2" height="1" rx="0.3" fill="#5A3A1A" />
      </svg>
    );
  }
  if (kind === 'zero_g_welder') {
    const suit = '#384058', visor = '#D4A820', pad = '#484858';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M5 42 L8 22 L22 22 L25 42 Z" fill={suit} />
        <path d="M8 22 L22 22 L21 12 L9 12 Z" fill={suit} />
        <rect x="7" y="11" width="5" height="4" rx="0.5" fill={pad} opacity="0.9" />
        <rect x="18" y="11" width="5" height="4" rx="0.5" fill={pad} opacity="0.9" />
        <ellipse cx="15" cy="7" rx="6" ry="6.5" fill={suit} />
        <rect x="9" y="3" width="12" height="8" rx="3" fill={visor} opacity="0.9" />
        <rect x="10" y="4" width="10" height="5.5" rx="2" fill="#AA8010" opacity="0.7" />
        <rect x="4" y="22" width="5" height="9" rx="1" fill={pad} opacity="0.85" />
        <rect x="21" y="22" width="5" height="9" rx="1" fill={pad} opacity="0.85" />
        <rect x="9" y="21" width="12" height="1.5" fill={pad} opacity="0.6" />
        <rect x="7" y="35" width="5" height="3" rx="0.5" fill={pad} opacity="0.7" />
        <rect x="18" y="35" width="5" height="3" rx="0.5" fill={pad} opacity="0.7" />
      </svg>
    );
  }
  if (kind === 'kdy_security_marine') {
    const armor = '#D0D5D8', blue = '#1A3A8A', visorC = accent || '#44AAFF', dark = '#1A1A2A';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M6 42 L8 21 L22 21 L24 42 Z" fill={armor} />
        <path d="M8 21 L22 21 L20 11 L10 11 Z" fill={armor} />
        <rect x="8" y="10" width="4" height="3" rx="0.5" fill={blue} opacity="0.9" />
        <rect x="18" y="10" width="4" height="3" rx="0.5" fill={blue} opacity="0.9" />
        <ellipse cx="15" cy="6" rx="5.5" ry="6" fill={dark} />
        <rect x="9.5" y="3.5" width="11" height="5" rx="1.5" fill={dark} />
        <rect x="10" y="5" width="10" height="2.5" rx="0.5" fill={visorC} opacity="0.7" />
        <rect x="5" y="21" width="5" height="9" rx="0.5" fill={armor} opacity="0.9" />
        <rect x="20" y="21" width="5" height="9" rx="0.5" fill={armor} opacity="0.9" />
        <rect x="9" y="20" width="12" height="1.5" fill={blue} opacity="0.8" />
        <rect x="10" y="14" width="10" height="1" fill={blue} opacity="0.5" />
        <rect x="16" y="16" width="4" height="3" rx="0.5" fill={blue} opacity="0.6" />
      </svg>
    );
  }
  if (kind === 'generic') {
    const skin = '#C79A78', coat = '#4A5568', coatDark = '#2D3748', hair = '#2A2118';
    return (
      <svg viewBox="0 0 30 42" width="26" height="36">
        <path d="M7 42 L9 22 L21 22 L23 42 Z" fill={coat} />
        <path d="M7 42 L10 24 L15 24 L13 42 Z" fill={coatDark} opacity="0.5" />
        <path d="M10 22 L20 22 L19 14 L11 14 Z" fill={coat} />
        <path d="M9 22 C6 22 5 18 7 14 L11 15 L9 22 Z" fill={coat} />
        <path d="M21 22 C24 22 25 18 23 14 L19 15 L21 22 Z" fill={coat} />
        <rect x="13" y="12" width="4" height="4" fill={skin} />
        <ellipse cx="15" cy="8" rx="5.2" ry="6" fill={skin} />
        <path d="M9.6 7 C9.6 2 12 1 15 1 C18 1 20.4 2 20.4 7 C18.6 4.6 11.4 4.6 9.6 7 Z" fill={hair} />
        <circle cx="12.8" cy="8.4" r="0.7" fill="#14181E" />
        <circle cx="17.2" cy="8.4" r="0.7" fill="#14181E" />
        <path d="M13.2 11 Q15 12 16.8 11" fill="none" stroke="#6A4A3A" strokeWidth="0.6" />
        <rect x="12" y="24" width="6" height="1.4" fill={accent} opacity="0.7" />
      </svg>
    );
  }
  return null;
}

function CollectibleIcon() {
  return (
    <svg viewBox="0 0 24 24" width="17" height="17">
      <polygon points="12,1 19,10 12,23 5,10" fill="#E8C97A" />
      <polygon points="12,1 15.5,10 12,15 8.5,10" fill="#FFFFFF" opacity="0.28" />
    </svg>
  );
}

function DecorIcon({ kind, accent }) {
  const s = { pointerEvents: 'none' };
  switch (kind) {
    case 'pillar':
      return (<svg viewBox="0 0 24 24" width="16" height="16" style={s}><circle cx="12" cy="12" r="8.5" fill="#00000050" /><circle cx="12" cy="12" r="8.5" fill="none" stroke={accent} strokeWidth="1" opacity="0.4" /><circle cx="12" cy="12" r="5" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.5" /><circle cx="12" cy="12" r="1.6" fill={accent} opacity="0.6" /></svg>);
    case 'brazier':
      return (<svg viewBox="0 0 24 24" width="14" height="17" style={s}><path d="M12 6 C9 9.5 9 12.5 12 14.5 C15 12.5 15 9.5 12 6 Z" fill={accent} opacity="0.8" style={{ animation: 'door-pulse 2.4s ease-in-out infinite' }} /><path d="M12 8.5 C10.6 10.3 10.6 12 12 13.2 C13.4 12 13.4 10.3 12 8.5 Z" fill="#FFFFFF" opacity="0.55" /><path d="M6 17 L18 17 L15 21 L9 21 Z" fill="#00000070" /></svg>);
    case 'plant':
      return (<svg viewBox="0 0 24 24" width="14" height="16" style={s}><path d="M12 17 C10 14 8 12 7 8 C10 10 11 13 12 15.5 C13 13 14 10 17 8 C16 12 14 14 12 17 Z" fill={accent} opacity="0.55" /><rect x="9" y="17" width="6" height="5" fill="#00000060" /></svg>);
    case 'archive':
      return (<svg viewBox="0 0 24 24" width="16" height="15" style={s}><rect x="3" y="4" width="18" height="16" fill="#00000066" /><rect x="5" y="6" width="4" height="4" fill={accent} opacity="0.6" /><rect x="10" y="6" width="4" height="4" fill={accent} opacity="0.3" /><rect x="15" y="6" width="4" height="4" fill={accent} opacity="0.5" /><rect x="5" y="12" width="4" height="4" fill={accent} opacity="0.3" /><rect x="10" y="12" width="4" height="4" fill={accent} opacity="0.6" /><rect x="15" y="12" width="4" height="4" fill={accent} opacity="0.4" /></svg>);
    case 'insignia':
      return (<svg viewBox="0 0 24 24" width="17" height="17" style={s}><circle cx="12" cy="12" r="9" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.35" /><path d="M12 4 L14 11 L20 12 L14 13 L12 20 L10 13 L4 12 L10 11 Z" fill={accent} opacity="0.3" /></svg>);
    case 'wall_insignia':
      return (<svg viewBox="0 0 24 24" width="14" height="14" style={s}><circle cx="12" cy="12" r="7" fill="none" stroke={accent} strokeWidth="1" opacity="0.3" /><circle cx="12" cy="12" r="3" fill={accent} opacity="0.25" /></svg>);
    case 'pipe':
      return (<svg viewBox="0 0 24 8" width="19" height="7" style={s}><rect x="1" y="2" width="22" height="4" rx="2" fill="#00000055" /><rect x="1" y="2" width="22" height="1.4" fill={accent} opacity="0.35" /></svg>);
    case 'girder':
      return (<svg viewBox="0 0 24 24" width="16" height="16" style={s}><line x1="2" y1="22" x2="22" y2="2" stroke="#00000066" strokeWidth="4" /><line x1="2" y1="22" x2="22" y2="2" stroke={accent} strokeWidth="1" opacity="0.3" /></svg>);
    case 'slag':
      return (<svg viewBox="0 0 24 12" width="15" height="8" style={s}><ellipse cx="12" cy="6" rx="10" ry="4.5" fill={accent} opacity="0.35" /><circle cx="9" cy="5" r="1" fill="#FFD9A0" opacity="0.7" /><circle cx="14" cy="7" r="0.8" fill="#FFD9A0" opacity="0.55" /></svg>);
    case 'root':
      return (<svg viewBox="0 0 24 24" width="17" height="17" style={s}><path d="M2 2 Q10 10 4 20 Q14 16 22 22" stroke="#0000004d" strokeWidth="3" fill="none" /><path d="M2 2 Q10 10 4 20 Q14 16 22 22" stroke={accent} strokeWidth="1" fill="none" opacity="0.4" /></svg>);
    case 'moss':
      return (<svg viewBox="0 0 24 14" width="15" height="9" style={s}><circle cx="8" cy="8" r="4" fill={accent} opacity="0.3" /><circle cx="15" cy="9" r="3" fill={accent} opacity="0.25" /><circle cx="11.5" cy="5.5" r="2.6" fill={accent} opacity="0.35" /></svg>);
    case 'rubble':
      return (<svg viewBox="0 0 24 16" width="15" height="10" style={s}><polygon points="2,10 8,4 14,8 10,12" fill="#00000055" /><polygon points="14,10 20,6 22,12 16,14" fill="#00000044" /></svg>);
    case 'cargo_crate':
      return (<svg viewBox="0 0 24 20" width="18" height="15" style={s}><rect x="2" y="6" width="20" height="14" fill="#00000060" /><rect x="2" y="6" width="20" height="14" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.4" /><line x1="12" y1="6" x2="12" y2="20" stroke={accent} strokeWidth="0.6" opacity="0.3" /><line x1="2" y1="13" x2="22" y2="13" stroke={accent} strokeWidth="0.6" opacity="0.3" /><polygon points="2,6 12,1 22,6" fill="#00000040" /><line x1="2" y1="6" x2="12" y2="1" stroke={accent} strokeWidth="0.6" opacity="0.3" /><line x1="22" y1="6" x2="12" y2="1" stroke={accent} strokeWidth="0.6" opacity="0.3" /></svg>);
    case 'steam_vent':
      return (<svg viewBox="0 0 24 24" width="14" height="17" style={s}><rect x="8" y="14" width="8" height="8" rx="1" fill="#00000060" /><rect x="10" y="10" width="4" height="4" fill="#00000060" /><ellipse cx="12" cy="9" rx="3" ry="1.5" fill="#444" opacity="0.8" /><path d="M10 6 Q11 3 10 1" stroke={accent} strokeWidth="1.2" fill="none" opacity="0.5" style={{ animation: 'steam-rise 2s ease-out 0s infinite' }} /><path d="M12 5 Q13 2 12 0" stroke={accent} strokeWidth="1.2" fill="none" opacity="0.5" style={{ animation: 'steam-rise 2s ease-out 0.4s infinite' }} /><path d="M14 6 Q15 3 14 1" stroke={accent} strokeWidth="1.2" fill="none" opacity="0.5" style={{ animation: 'steam-rise 2s ease-out 0.8s infinite' }} /></svg>);
    case 'neon_sign':
      return (<svg viewBox="0 0 32 16" width="22" height="11" style={s}><rect x="1" y="3" width="30" height="10" rx="1" fill="#00000060" /><rect x="1" y="3" width="30" height="10" rx="1" fill="none" stroke={accent} strokeWidth="1" opacity="0.7" style={{ animation: 'door-pulse 2.8s ease-in-out infinite' }} /><rect x="4" y="6" width="6" height="4" fill={accent} opacity="0.25" /><rect x="13" y="6" width="6" height="4" fill={accent} opacity="0.15" /><rect x="22" y="6" width="6" height="4" fill={accent} opacity="0.25" /></svg>);
    case 'cable_bundle':
      return (<svg viewBox="0 0 24 24" width="16" height="16" style={s}><g><line x1="8" y1="2" x2="8" y2="22" stroke="#00C8D4" strokeWidth="1.5" opacity="0.7"/><line x1="11" y1="2" x2="11" y2="22" stroke="#00F0FF" strokeWidth="1" opacity="0.5"/><line x1="14" y1="2" x2="14" y2="22" stroke="#00C8D4" strokeWidth="1.5" opacity="0.7"/><rect x="6" y="9" width="10" height="2" rx="1" fill="#005060" opacity="0.8"/><rect x="6" y="15" width="10" height="2" rx="1" fill="#005060" opacity="0.8"/></g></svg>);
    case 'scan_arch':
      return (<svg viewBox="0 0 24 24" width="16" height="16" style={s}><g><path d="M4 22 L4 6 Q12 2 20 6 L20 22" stroke="#00CCFF" strokeWidth="2" fill="none" opacity="0.8"/><line x1="4" y1="14" x2="20" y2="14" stroke="#00CCFF" strokeWidth="1" opacity="0.5" strokeDasharray="2,2"/><rect x="2" y="20" width="20" height="3" rx="1" fill="#1A4A80" opacity="0.7"/></g></svg>);
    case 'warning_beacon':
      return (<svg viewBox="0 0 24 24" width="16" height="16" style={s}><g><rect x="9" y="14" width="6" height="8" rx="1" fill="#804800"/><ellipse cx="12" cy="12" rx="5" ry="4" fill="#FF9900" opacity="0.9"/><ellipse cx="12" cy="12" rx="3" ry="2.5" fill="#FFCC00"/><rect x="10" y="4" width="4" height="8" rx="1" fill="#606060"/></g></svg>);
    case 'hazard_stripe':
      return (<svg viewBox="0 0 24 24" width="16" height="16" style={s}><g><rect x="2" y="18" width="20" height="4" fill="#1A1A00"/><rect x="2" y="18" width="4" height="4" fill="#FF9900" opacity="0.9"/><rect x="10" y="18" width="4" height="4" fill="#FF9900" opacity="0.9"/><rect x="18" y="18" width="4" height="4" fill="#FF9900" opacity="0.9"/></g></svg>);
    default: return null;
  }
}

function PlayerMarker({ accent, facing }) {
  const skin = '#D9B98C', hair = '#3A2E22', tunic = '#8C8172', vest = '#4A4038', pants = '#5A5548', boots = '#2A241E';
  return (
    <div style={{ animation: 'player-bob 1.1s ease-in-out infinite' }}>
      <div style={{ transform: `scaleX(${facing})` }}>
        <svg viewBox="0 0 26 36" width="22" height="30">
          <path d="M8 36 L9 24 L13 24 L12.5 36 Z" fill={boots} />
          <path d="M14 36 L13.5 24 L17 24 L18 36 Z" fill={boots} />
          <path d="M8 30 L9 22 L13 22 L13 30 Z" fill={pants} />
          <path d="M13 30 L13 22 L17 22 L18 30 Z" fill={pants} />
          <path d="M7 22 L19 22 L18 11 L8 11 Z" fill={tunic} />
          <path d="M8 22 L18 22 L17 12 L9 12 Z" fill={vest} opacity="0.75" />
          <path d="M7 22 C5 22 4 18 6 14 L9 14 L7 22 Z" fill={tunic} />
          <path d="M19 22 C21 22 22 18 20 14 L17 14 L19 22 Z" fill={tunic} />
          <rect x="7" y="21.5" width="12" height="1.6" fill={boots} opacity="0.8" />
          <circle cx="13" cy="8" r="4.2" fill={skin} />
          <path d="M8.8 7.5 C8.8 4.6 10.6 3 13 3 C15.4 3 17.2 4.6 17.2 7.5 C15.6 6.4 14.2 6 13 6 C11.8 6 10.4 6.4 8.8 7.5 Z" fill={hair} />
          <circle cx="11.5" cy="8.4" r="0.55" fill="#2A2320" />
          <circle cx="14.5" cy="8.4" r="0.55" fill="#2A2320" />
          <ellipse cx="11.5" cy="8.4" rx="0.8" ry="0.65" fill={skin} style={{ animation: 'npc-blink 5s ease-in-out infinite' }} />
          <ellipse cx="14.5" cy="8.4" rx="0.8" ry="0.65" fill={skin} style={{ animation: 'npc-blink 5s ease-in-out infinite' }} />
          <path d="M11.7 10.4 Q13 11 14.3 10.4" stroke="#8A6E4E" strokeWidth="0.4" fill="none" />
        </svg>
      </div>
    </div>
  );
}

function CoruscantBackdrop({ accent, accentDim }) {
  const buildings = [
    { x:0,w:40,h:90 },{ x:45,w:30,h:130 },{ x:80,w:50,h:70 },{ x:135,w:25,h:160 },
    { x:165,w:60,h:100 },{ x:230,w:35,h:140 },{ x:270,w:45,h:80 },{ x:320,w:30,h:175 },
    { x:500,w:40,h:150 },{ x:545,w:60,h:110 },{ x:610,w:30,h:165 },{ x:645,w:50,h:90 },
    { x:700,w:35,h:145 },{ x:740,w:60,h:100 },{ x:800,w:40,h:130 },
  ];
  return (
    <svg viewBox="0 0 900 200" preserveAspectRatio="none" style={{ position:'absolute',top:0,left:0,width:'100%',height:170,opacity:0.55,pointerEvents:'none' }}>
      <defs><linearGradient id="cFade" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stopColor="#0B0C14" stopOpacity="0" /><stop offset="100%" stopColor="#0B0C14" stopOpacity="0.9" /></linearGradient></defs>
      {buildings.map((b, i) => (
        <g key={i}>
          <rect x={b.x} y={200-b.h} width={b.w} height={b.h} fill={accentDim} opacity="0.55" />
          {Array.from({ length: Math.floor(b.h/18) }).map((_,wi) => (
            <rect key={wi} x={b.x+6} y={200-b.h+8+wi*18} width={4} height={4} fill={accent} opacity={(i+wi)%3===0?0.85:0.15} />
          ))}
        </g>
      ))}
      {Array.from({ length: 22 }).map((_,i) => (
        <circle key={i} cx={(i*137)%900} cy={(i*53)%60} r={0.8+(i%3)*0.4} fill="#FFFFFF" style={{ animation:`twinkle ${3+(i%4)}s ease-in-out ${(i%5)*0.4}s infinite` }} />
      ))}
      <g transform="translate(365,0)">
        <polygon points="30,200 30,105 60,85 60,200" fill={accentDim} opacity="0.6" />
        <polygon points="130,200 130,85 160,105 160,200" fill={accentDim} opacity="0.6" />
        <polygon points="60,200 60,55 95,25 130,55 130,200" fill={accentDim} opacity="0.75" />
        <polygon points="82,25 95,4 108,25" fill={accent} opacity="0.85" />
      </g>
      <rect x="0" y="0" width="900" height="200" fill="url(#cFade)" />
    </svg>
  );
}

function AmbientLayer({ kind, accent }) {
  const particles = Array.from({ length: 14 });
  if (kind === 'traffic') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none',opacity:0.7 }}>
        {particles.map((_,i) => (
          <div key={i} style={{ position:'absolute',left:`${(i*37)%100}%`,top:`${8+((i*13)%55)}%`,animation:`traffic-drift ${5+(i%5)}s linear ${(i%7)*0.6}s infinite` }}>
            <svg viewBox="0 0 24 10" width="22" height="9" style={{ overflow:'visible' }}>
              <rect x="-14" y="4" width="14" height="1.4" fill={accent} opacity="0.35" />
              <path d="M2 5 Q6 2 12 3 L20 4 Q22 5 20 6 L12 7 Q6 8 2 5 Z" fill={accent} opacity="0.95" />
              <circle cx="9" cy="5" r="0.8" fill="#FFFFFF" opacity="0.85" />
            </svg>
          </div>
        ))}
      </div>
    );
  }
  if (kind === 'neon_haze') {
    const neonParticles = Array.from({ length: 10 }, (_, i) => i);
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none',opacity:0.7 }}>
        {neonParticles.map((i) => (
          <div key={i} style={{
            position:'absolute',
            width:`${30+(i*17)%40}px`,height:`${20+(i*13)%30}px`,
            borderRadius:'50%',
            background:i%2===0?'rgba(255,0,85,0.12)':'rgba(180,0,100,0.10)',
            left:`${(i*23)%90}%`,top:`${(i*31)%80}%`,
            animation:`drift ${6+(i%4)}s ease-in-out ${i*0.7}s infinite alternate`,
            filter:'blur(8px)',pointerEvents:'none',
          }} />
        ))}
      </div>
    );
  }
  if (kind === 'datastream') {
    const dsParticles = Array.from({ length: 8 }, (_, i) => i);
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none',opacity:0.7 }}>
        {dsParticles.map((i) => (
          <div key={i} style={{
            position:'absolute',width:'1px',
            height:`${40+(i*19)%40}%`,
            background:i%3===0?'rgba(0,240,255,0.25)':'rgba(0,180,200,0.15)',
            left:`${10+i*12}%`,top:'-10%',
            animation:`scanDown ${3+(i%3)}s linear ${i*0.4}s infinite`,
            pointerEvents:'none',
          }} />
        ))}
      </div>
    );
  }
  if (kind === 'steam') {
    const steamParticles = Array.from({ length: 12 }, (_, i) => i);
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none',opacity:0.5 }}>
        {steamParticles.map((i) => (
          <div key={i} style={{
            position:'absolute',
            width:`${8+(i*7)%16}px`,height:`${8+(i*7)%16}px`,
            borderRadius:'50%',
            background:'rgba(200,180,160,0.08)',
            left:`${(i*19)%88}%`,bottom:`${(i*13)%40}%`,
            animation:`rise ${4+(i%5)}s ease-out ${i*0.5}s infinite`,
            filter:'blur(4px)',pointerEvents:'none',
          }} />
        ))}
      </div>
    );
  }
  if (kind === 'lounge') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 5 }, (_, i) => (
          <div key={'b' + i} style={{ position:'absolute',top:'-10%',left:0,width:`${6 + (i % 3) * 3}%`,height:'130%',background: i % 2 ? 'rgba(255,90,40,0.08)' : 'rgba(210,235,255,0.07)',transform:'skewX(-18deg)',animation:`cast-sweep ${11 + i * 4}s linear ${i * 3.2}s infinite` }} />
        ))}
        {Array.from({ length: 18 }, (_, i) => (
          <div key={'m' + i} style={{ position:'absolute',left:`${(i*41)%100}%`,top:`${(i*23)%90}%`,width:`${2+(i%3)}px`,height:`${2+(i%3)}px`,borderRadius:'50%',background:'rgba(255,220,140,0.5)',opacity:0.5,animation:`drift ${6+(i%5)}s ease-in-out ${i*0.4}s infinite alternate` }} />
        ))}
        {Array.from({ length: 6 }, (_, i) => (
          <div key={'w' + i} style={{ position:'absolute',left:`${(i*17+8)%90}%`,bottom:`${(i*11)%30}%`,width:'18px',height:'18px',borderRadius:'50%',background:'rgba(255,230,190,0.07)',filter:'blur(5px)',animation:`rise ${5+(i%3)}s ease-out ${i*0.9}s infinite` }} />
        ))}
        <div style={{ position:'absolute',top:'8%',left:'-10%',width:'140%',height:'2px',background:'linear-gradient(90deg,transparent,rgba(210,235,255,0.15),transparent)',animation:'drift 6s ease-in-out infinite alternate' }} />
      </div>
    );
  }
  if (kind === 'warzone') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 22 }, (_, i) => (
          <div key={'a' + i} style={{ position:'absolute',left:`${(i*37)%110}%`,top:'-3%',width:`${2+(i%3)}px`,height:`${2+(i%3)}px`,borderRadius:'50%',background:i%4===0?'rgba(255,120,40,0.85)':'rgba(190,190,190,0.5)',animation:`ash-fall ${5+(i%6)}s linear ${(i*0.45)%5}s infinite` }} />
        ))}
        {Array.from({ length: 7 }, (_, i) => (
          <div key={'t' + i} style={{ position:'absolute',left:0,top:`${10+((i*13)%62)}%`,width:'46px',height:'2px',background:i%2?'#00E5FF':'#FF2A1A',boxShadow:i%2?'0 0 8px #00E5FF':'0 0 8px #FF2A1A',animation:`tracer-fly ${1.1+(i%3)*0.35}s linear ${(i*0.9)%4}s infinite`,transform:i%2?'scaleX(-1)':'none' }} />
        ))}
        <div style={{ position:'absolute',left:0,right:0,bottom:0,height:'34%',background:'rgba(20,10,6,0.28)',filter:'blur(14px)' }} />
      </div>
    );
  }
  if (kind === 'basalt_mist') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 7 }, (_, i) => (
          <div key={'f' + i} style={{ position:'absolute',left:`${(i*23)%80-10}%`,top:`${(i*17)%70}%`,width:'46%',height:'22%',background:'rgba(150,200,190,0.07)',filter:'blur(22px)',animation:`mist-drift ${9+(i%4)*2}s ease-in-out ${i*1.3}s infinite` }} />
        ))}
        {Array.from({ length: 16 }, (_, i) => (
          <div key={'m' + i} style={{ position:'absolute',left:`${(i*31)%100}%`,top:`${(i*19)%96}%`,width:'2px',height:'2px',borderRadius:'50%',background:'rgba(190,235,225,0.45)',animation:`twinkle ${3+(i%4)}s ease-in-out ${i*0.4}s infinite` }} />
        ))}
        <div style={{ position:'absolute',inset:0,background:'rgba(8,20,22,0.18)' }} />
      </div>
    );
  }
  if (kind === 'forge') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 24 }, (_, i) => (
          <div key={'e' + i} style={{ position:'absolute',left:`${(i*29)%100}%`,bottom:'-2%',width:`${2+(i%3)}px`,height:`${2+(i%3)}px`,borderRadius:'50%',background:i%3?'rgba(255,140,40,0.9)':'rgba(255,200,90,0.9)',animation:`embers-drift ${4+(i%6)}s ease-out ${(i*0.5)%6}s infinite` }} />
        ))}
        {Array.from({ length: 5 }, (_, i) => (
          <div key={'s' + i} style={{ position:'absolute',left:`${(i*27+5)%90}%`,bottom:`${(i*11)%30}%`,width:'60px',height:'60px',borderRadius:'50%',background:'rgba(220,230,235,0.08)',filter:'blur(12px)',animation:`steam-rise ${4+(i%3)}s ease-out ${i*1.1}s infinite` }} />
        ))}
        <div style={{ position:'absolute',inset:0,background:'rgba(255,85,0,0.05)',animation:'door-pulse 3.2s ease-in-out infinite' }} />
      </div>
    );
  }
  if (kind === 'dust') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none',opacity:0.6 }}>
        {Array.from({ length: 24 }, (_, i) => (
          <div key={i} style={{ position:'absolute',left:`${(i*29)%100}%`,top:`${(i*17)%96}%`,width:`${2+(i%3)}px`,height:`${2+(i%3)}px`,borderRadius:'50%',background:i%3===0?'rgba(255,150,70,0.5)':'rgba(220,190,150,0.35)',animation:`traffic-drift ${6+(i%7)}s linear ${(i%9)*0.5}s infinite` }} />
        ))}
        <div style={{ position:'absolute',top:'30%',left:'-10%',width:'140%',height:'8px',background:'linear-gradient(90deg,transparent,rgba(255,150,70,0.05),transparent)',animation:'drift 12s ease-in-out infinite alternate' }} />
      </div>
    );
  }
  if (kind === 'moonbase') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 20 }, (_, i) => (
          <div key={'d' + i} style={{ position:'absolute',left:`${(i*31)%100}%`,top:`${(i*23)%96}%`,width:`${2+(i%3)}px`,height:`${2+(i%3)}px`,borderRadius:'50%',background:i%3===0?'rgba(200,170,255,0.5)':'rgba(220,200,170,0.35)',animation:`ash-fall ${9+(i%5)*2}s linear ${(i%7)*0.6}s infinite` }} />
        ))}
        <div style={{ position:'absolute',top:'-20%',left:'18%',width:'6px',height:'150%',background:'linear-gradient(180deg,transparent,rgba(180,240,255,0.10),transparent)',transform:'rotate(14deg)',animation:'drift 9s ease-in-out infinite alternate' }} />
        <div style={{ position:'absolute',top:'-20%',left:'64%',width:'5px',height:'150%',background:'linear-gradient(180deg,transparent,rgba(180,240,255,0.08),transparent)',transform:'rotate(-12deg)',animation:'drift 12s ease-in-out infinite alternate-reverse' }} />
        <div style={{ position:'absolute',left:0,right:0,bottom:0,height:'30%',background:'rgba(40,20,70,0.16)',filter:'blur(16px)' }} />
      </div>
    );
  }
  if (kind === 'foundry') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 26 }, (_, i) => (
          <div key={'s' + i} style={{ position:'absolute',left:`${(i*23)%100}%`,bottom:`${(i*7)%30}%`,width:`${2+(i%2)}px`,height:`${2+(i%2)}px`,borderRadius:'50%',background:i%4===0?'rgba(255,230,160,0.95)':'rgba(255,130,40,0.9)',animation:`embers-drift ${3+(i%5)}s ease-out ${(i%9)*0.4}s infinite` }} />
        ))}
        {Array.from({ length: 6 }, (_, i) => (
          <div key={'k' + i} style={{ position:'absolute',left:`${(i*19+4)%90}%`,top:`${(i*13)%60}%`,width:'70px',height:'50px',borderRadius:'50%',background:'rgba(20,16,14,0.22)',filter:'blur(14px)',animation:`steam-rise ${8+(i%4)*2}s ease-out ${i*1.3}s infinite` }} />
        ))}
        {Array.from({ length: 4 }, (_, i) => (
          <div key={'w' + i} style={{ position:'absolute',left:`${15+i*23}%`,top:`${20+(i*17)%50}%`,width:'12px',height:'12px',borderRadius:'50%',background:'rgba(200,235,255,0.9)',boxShadow:'0 0 14px #BFF4FF',animation:`ship-blink ${2.2+i*0.7}s steps(2,end) ${i*0.9}s infinite` }} />
        ))}
        <div style={{ position:'absolute',inset:0,background:'rgba(255,85,0,0.05)',animation:'door-pulse 3.6s ease-in-out infinite' }} />
      </div>
    );
  }
  if (kind === 'cavern') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 22 }, (_, i) => (
          <div key={'p' + i} style={{ position:'absolute',left:`${(i*37)%100}%`,top:`${(i*19)%96}%`,width:`${2+(i%3)}px`,height:`${2+(i%3)}px`,borderRadius:'50%',background:i%2?'rgba(170,130,255,0.7)':'rgba(120,230,255,0.55)',animation:`twinkle ${3+(i%5)}s ease-in-out ${(i%7)*0.5}s infinite` }} />
        ))}
        {Array.from({ length: 5 }, (_, i) => (
          <div key={'d' + i} style={{ position:'absolute',left:`${(i*21+8)%90}%`,top:'-4%',width:'2px',height:'10px',background:'rgba(160,220,255,0.7)',animation:`ash-fall ${3+i*0.8}s linear ${i*1.1}s infinite` }} />
        ))}
        {Array.from({ length: 5 }, (_, i) => (
          <div key={'m' + i} style={{ position:'absolute',left:`${(i*26-6)%90}%`,top:`${40+(i*13)%50}%`,width:'50%',height:'20%',background:'rgba(90,75,138,0.12)',filter:'blur(22px)',animation:`mist-drift ${10+(i%3)*3}s ease-in-out ${i}s infinite` }} />
        ))}
        <div style={{ position:'absolute',inset:0,background:'rgba(90,60,170,0.07)',animation:'door-pulse 5s ease-in-out infinite' }} />
      </div>
    );
  }
  if (kind === 'wreck') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 10 }, (_, i) => (
          <div key={'a' + i} style={{ position:'absolute',left:`${(i*43)%100}%`,top:`${(i*29)%90}%`,width:'18px',height:'2px',background:'rgba(190,240,255,0.95)',boxShadow:'0 0 10px #7FD0FF',animation:`ship-spark ${2.5+(i%4)}s steps(3,end) ${(i%6)*0.7}s infinite` }} />
        ))}
        {Array.from({ length: 6 }, (_, i) => (
          <div key={'v' + i} style={{ position:'absolute',left:`${(i*19+6)%90}%`,bottom:`${(i*9)%30}%`,width:'54px',height:'54px',borderRadius:'50%',background:'rgba(200,220,235,0.10)',filter:'blur(14px)',animation:`steam-rise ${7+(i%4)*2}s ease-out ${i*1.2}s infinite` }} />
        ))}
        <div style={{ position:'absolute',inset:0,background:'rgba(255,40,40,0.045)',animation:'ship-blink 3.2s ease-in-out infinite' }} />
        <div style={{ position:'absolute',inset:0,background:'rgba(8,16,24,0.18)' }} />
      </div>
    );
  }
  if (kind === 'ringdeck') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none' }}>
        {Array.from({ length: 14 }, (_, i) => (
          <div key={'w' + i} style={{ position:'absolute',left:`${(i*37)%100}%`,top:`${(i*23)%90}%`,width:'3px',height:'3px',borderRadius:'50%',background:i%3===0?'rgba(255,240,190,0.95)':'rgba(255,150,60,0.9)',boxShadow:'0 0 8px rgba(255,170,70,0.9)',animation:`ship-spark ${2.2+(i%4)}s steps(3,end) ${(i%7)*0.6}s infinite` }} />
        ))}
        {Array.from({ length: 5 }, (_, i) => (
          <div key={'v' + i} style={{ position:'absolute',left:`${(i*23+6)%88}%`,bottom:`${(i*13)%34}%`,width:'46px',height:'46px',borderRadius:'50%',background:'rgba(200,220,235,0.08)',filter:'blur(11px)',animation:`steam-rise ${6+(i%4)*2}s ease-out ${i*1.3}s infinite` }} />
        ))}
        <div style={{ position:'absolute',top:'-20%',left:'30%',width:'5px',height:'150%',background:'linear-gradient(180deg,transparent,rgba(190,225,255,0.09),transparent)',transform:'rotate(12deg)',animation:'drift 10s ease-in-out infinite alternate' }} />
        <div style={{ position:'absolute',inset:0,background:'rgba(255,150,40,0.035)',animation:'door-pulse 3.6s ease-in-out infinite' }} />
      </div>
    );
  }
  if (kind === 'sky_high') {
    return (
      <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none',zIndex:1 }}>
        <div style={{ position:'absolute',top:'-20%',left:'10%',width:'4px',height:'140%',background:'linear-gradient(180deg,transparent,rgba(255,240,180,0.07),transparent)',transform:'rotate(15deg)',animation:'scanDown 8s linear infinite' }} />
        <div style={{ position:'absolute',top:'-20%',left:'55%',width:'3px',height:'140%',background:'linear-gradient(180deg,transparent,rgba(255,240,180,0.05),transparent)',transform:'rotate(10deg)',animation:'scanDown 11s linear infinite 3s' }} />
        <div style={{ position:'absolute',top:'20%',left:'-10%',width:'140%',height:'6px',background:'linear-gradient(90deg,transparent,rgba(255,255,255,0.04),transparent)',animation:'drift 14s ease-in-out infinite' }} />
        <div style={{ position:'absolute',top:'60%',left:'-10%',width:'140%',height:'4px',background:'linear-gradient(90deg,transparent,rgba(255,255,255,0.03),transparent)',animation:'drift 18s ease-in-out infinite 5s' }} />
      </div>
    );
  }
  return (
    <div style={{ position:'absolute',inset:0,overflow:'hidden',pointerEvents:'none',opacity:0.5 }}>
      {particles.map((_,i) => {
        const size = kind==='embers'?3+(i%3):40+(i%4)*20;
        return (
          <div key={i} style={{ position:'absolute',left:`${(i*37)%100}%`,bottom:'-10%',width:size,height:kind==='mist'?size*0.4:size,borderRadius:'50%',background:kind==='mist'?`${accent}22`:accent,filter:kind==='mist'?'blur(6px)':'none',animation:`${kind}-drift ${6+(i%5)}s linear ${(i%7)*0.6}s infinite` }} />
        );
      })}
    </div>
  );
}

function Minimap({ zone, map, pos, camX, camY, npcPositions, completedInteractions, questFlags = {} }) {
  const MS = 3;
  const tileColor = (t) => {
    if (t === 'floor') return zone.floorColor;
    if (t === 'ship_hull') return '#3A3A4A';
    if (t === 'ship_ramp') return zone.accentDim;
    if (t === 'door') return zone.accent;
    if (t === 'lava') return '#CC3300';
    if (t === 'water') return '#1A3A7A';
    return zone.wallDark;
  };
  return (
    <svg width={zone.width * MS} height={zone.height * MS} style={{ display:'block',border:`1px solid ${zone.accentDim}` }}>
      {map.map((row, y) => row.map((tile, x) => (
        <rect key={`${x}-${y}`} x={x*MS} y={y*MS} width={MS} height={MS} fill={tileColor(tile.type)} />
      )))}
      {zone.npcs?.filter(npc => isNpcVisible(npc, questFlags)).map((npc) => {
        const p = (npcPositions && npcPositions[npc.id]) || { x: npc.x, y: npc.y };
        const done = completedInteractions?.has(npc.id);
        const isQuest = npc.questNpc && !done;
        const color = isQuest ? '#FFD700' : done ? '#333' : zone.accentDim;
        return <rect key={npc.id} x={p.x*MS} y={p.y*MS} width={MS} height={MS} fill={color} />;
      })}
      <rect x={camX*MS} y={camY*MS} width={VIEWPORT_COLS*MS} height={VIEWPORT_ROWS*MS} fill="none" stroke={zone.accent} strokeWidth="0.8" opacity="0.7" />
      <rect x={pos.x*MS} y={pos.y*MS} width={MS} height={MS} fill={zone.accent} />
    </svg>
  );
}

function DialogueOverlay({ npc, onChoose, inventory = [], questFlags = {}, credits = 0 }) {
  const meetsRequires = (choice) => {
    if (choice.requiresFlag && !questFlags[choice.requiresFlag]) return false;
    if (!choice.requires) return true;
    if (choice.requires.credits && credits < choice.requires.credits) return false;
    if (choice.requires.item && !inventory.some(i => i.id === choice.requires.item)) return false;
    if (choice.requires.flag && !questFlags[choice.requires.flag]) return false;
    return true;
  };
  return (
    <div style={{ position:'absolute',inset:0,background:'rgba(4,4,8,0.92)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:25 }}>
      <div style={{ width:'min(92%,480px)',border:'1px solid #2A2A38',background:'#0E0E16',fontFamily:"'IBM Plex Mono',ui-monospace,monospace" }}>
        <div style={{ padding:'14px 18px',borderBottom:'1px solid #2A2A38',color:'#E8C97A',fontSize:13,fontWeight:600 }}>{npc.label}</div>
        <div style={{ padding:'16px 18px',color:'#C9C5BE',fontSize:13,lineHeight:1.6,borderBottom:'1px solid #1C1C26' }}>{npc.prompt}</div>
        {npc.choices.map((choice, i) => {
          const meets = meetsRequires(choice);
          const reqLabel = !meets && choice.requires?.item ? ` (requires ${ITEMS[choice.requires.item]?.name || choice.requires.item})` : (!meets && choice.requires?.credits ? ` (costs ${choice.requires.credits} cr)` : (!meets && (choice.requires?.flag || choice.requiresFlag) ? ' (not yet available)' : ''));
          return (
            <div key={i} onClick={() => meets && onChoose(choice, npc.id)}
              style={{ padding:'14px 18px',borderBottom:i<npc.choices.length-1?'1px solid #1C1C26':'none',cursor:meets?'pointer':'not-allowed',fontSize:12.5,color:meets?'#A8ADC0':'#4A4F64',opacity:meets?1:0.5 }}>
              &gt; {choice.text}<span style={{color:'#E8C97A88',fontSize:11}}>{reqLabel}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function TravelOverlay({ currentPlanetId, credits, questFlags, onTravel, onClose }) {
  return (
    <div style={{ position:'absolute',inset:0,background:'rgba(4,4,8,0.92)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:20 }}>
      <div style={{ width:'min(92%,480px)',border:'1px solid #2A2A38',background:'#0E0E16',fontFamily:"'IBM Plex Mono',ui-monospace,monospace" }}>
        <div style={{ padding:'14px 18px',borderBottom:'1px solid #2A2A38',color:'#8890A8',fontSize:11,letterSpacing:'0.08em' }}>hyperspace docking manifest</div>
        {Object.values(PLANETS).map((p) => {
          const isCurrent = p.id === currentPlanetId;
          const isLocked = p.requiresFlag && !(questFlags && questFlags[p.requiresFlag]);
          const canAfford = credits >= p.travelCost;
          const zone0 = p.zones[p.startZoneId];
          return (
            <div key={p.id} onClick={() => !isCurrent && canAfford && !isLocked && onTravel(p.id)} style={{ padding:'14px 18px',borderBottom:'1px solid #1C1C26',cursor:isCurrent||!canAfford||isLocked?'default':'pointer',opacity:isCurrent?0.4:isLocked?0.3:canAfford?1:0.5,display:'flex',justifyContent:'space-between',alignItems:'center' }}>
              <div>
                <div style={{ color:isLocked?'#5A5F74':zone0.accent,fontSize:15,fontWeight:600 }}>{p.name}{isLocked?' [LOCKED]':''}</div>
                <div style={{ color:'#7A7F94',fontSize:11,marginTop:2 }}>{isLocked?'Complete Senate mission to unlock':zone0.subtitle}</div>
              </div>
              <div style={{ textAlign:'right',fontSize:12,color:'#A8ADC0' }}>{isCurrent?'docked here':isLocked?'locked':p.travelCost===0?'no fee':`${p.travelCost} cr`}</div>
            </div>
          );
        })}
        <div onClick={onClose} style={{ padding:'12px 18px',textAlign:'center',color:'#5A5F74',fontSize:12,cursor:'pointer' }}>cancel</div>
      </div>
    </div>
  );
}

function AlignmentPanel({ alignment }) {
  const { morality, loyalty } = alignment;
  const moralityPct = ((morality + 100) / 200) * 100;
  const bar = (label, value, color) => (
    <div style={{ marginBottom:8 }}>
      <div style={{ display:'flex',justifyContent:'space-between',fontSize:10,color:'#7A7F94',marginBottom:3 }}>
        <span>{label}</span><span>{value}</span>
      </div>
      <div style={{ height:4,background:'#1C1C26' }}>
        <div style={{ width:`${value}%`,height:'100%',background:color,transition:'width 0.4s ease' }} />
      </div>
    </div>
  );
  return (
    <div style={{ border:'1px solid #24242E',padding:12 }}>
      <div style={{ fontSize:10,color:'#5A5F74',marginBottom:10 }}>alignment matrix</div>
      <div style={{ marginBottom:10 }}>
        <div style={{ display:'flex',justifyContent:'space-between',fontSize:10,color:'#7A7F94',marginBottom:3 }}><span>dark</span><span>light</span></div>
        <div style={{ height:4,background:'linear-gradient(90deg,#8B2E1F,#1C1C26 50%,#6FD9A0)' }}>
          <div style={{ position:'relative',height:'100%' }}>
            <div style={{ position:'absolute',left:`${moralityPct}%`,top:-3,width:2,height:10,background:'#E8C97A' }} />
          </div>
        </div>
      </div>
      {bar('republic standing', loyalty.republic, '#8FA6FF')}
      {bar('sith empire standing', loyalty.sithEmpire, '#FF9A5A')}
      {bar('underworld standing', loyalty.underworld, '#6FD9A0')}
    </div>
  );
}

const ITEMS = {
  // BADOR ITEMS BEGIN
  item_kdy_datapad_01:        { id:'item_kdy_datapad_01',        name:'Scorched KDY Manifest',                 type:'quest',      iconKind:'datapad',  value:150, description:'Encircled coordinates detail a shipment of hyperdrive reactor cores moved out of Base Alpha before the KEF attack.' },
  item_thermal_gear_01:       { id:'item_thermal_gear_01',       name:'Hazard Insulation Vest',                type:'gear',       iconKind:'gear',     value:300, description:'Grants thermal resistance. Crucial for the Engine Wastes test fires and the long walk to the Exhaust Cathedral.' },
  kdy_landspeeder_auth:       { id:'kdy_landspeeder_auth',       name:'Landspeeder Authorization Keycard',     type:'quest',      iconKind:'keycard',  value:0,   description:'A KDY keycard for the depot gate on the southern edge of the City Outskirts.' },
  kdy_transit_cylinder:       { id:'kdy_transit_cylinder',       name:'KDY Transit Encryption Cylinder',       type:'quest',      iconKind:'keycard',  value:0,   description:'A glowing cylinder holding the gateway codes for the heavy blast doors to the Engine Wastes.' },
  kef_bypass_schematic:       { id:'kef_bypass_schematic',       name:'KEF Bypass Schematic',                  type:'gear',       iconKind:'datapad',  value:200, description:'Varek Korr hand drawn plans for thermal shielding. Grants fire resistance in the Engine Wastes.' },
  catalyst_governor_valve:    { id:'catalyst_governor_valve',    name:'Catalyst Governor Valve',               type:'quest',      iconKind:'gear',     value:0,   description:'A heavy brass wheel set with three glowing blue coolant crystals. The missing component that stabilizes the manifold in the Exhaust Cathedral.' },
  thermal_crystal_dust:       { id:'thermal_crystal_dust',       name:'Thermal Crystal Dust',                  type:'consumable', iconKind:'supply',   value:120, description:'Fine blue dust ground from coolant crystals. Vance uses it in heat shielding.' },
  pressurized_igniter:        { id:'pressurized_igniter',        name:'Pressurized Igniter',                   type:'weapon',     iconKind:'gear',     value:280, description:'Fires a directional plasma flare. Clears steam vents and lights very bad ideas.' },
  plasma_grenades:            { id:'plasma_grenades',            name:'Heavy Plasma Grenades',                 type:'consumable', iconKind:'supply',   value:350, description:'A bandolier of heavy plasma grenades taken from a KEF convoy. Throw one under a repulsor skirt.' },
  item_kef_cutter_01:         { id:'item_kef_cutter_01',         name:'KEF Industrial Breaching Cutter',      type:'weapon',     iconKind:'gear',     value:450, description:'Modified heavy cutter. Deals high damage against armored targets and can slice open sealed doors in Outpost 7.' },
  cav_8x_memory_core:         { id:'cav_8x_memory_core',         name:'Unit 8-X Memory Core',                  type:'gear',       iconKind:'datapad',  value:900, description:'A scorched warden memory core. Forty years of patrol maps run through it, and the last route ends at the Zora IV.' },
  cav_8x_escort_beacon:       { id:'cav_8x_escort_beacon',       name:'Level Eight Escort Beacon',             type:'gear',       iconKind:'keycard',  value:600, description:'A warm palm beacon issued by Unit 8-X. Sentinels that read it confirm instead of engaging.' },
  wst_scan_team_tags:         { id:'wst_scan_team_tags',         name:'Scan Team Tags',                        type:'quest',      iconKind:'datapad',  value:0,   description:'Seven KDY tags, each wrapped in repair tape and a hand lettered name. Commander Vael will want these.' },
  wst_gharza_service_key:     { id:'wst_gharza_service_key',     name:"Gharza's Service Key",                  type:'quest',      iconKind:'keycard',  value:0,   description:'A hunter\'s override key. It keeps the idle sentinel in the Deep Caverns asleep.' },
  wst_vance_spanner:          { id:'wst_vance_spanner',          name:'Crankshaft Spanner',                    type:'weapon',     iconKind:'gear',     value:420, description:'Vance\'s pneumatic impact spanner, rebuilt with a brass hammer head. Heavy, loud and reliable against machines.' },
  wst_capacitor_cell:         { id:'wst_capacitor_cell',         name:'Ion Capacitor Cell',                    type:'gear',       iconKind:'supply',   value:520, description:'The main cell from the jammer bank, cold at last. Slicers and collectors pay well for one.' },
  wst_tuning_fork:            { id:'wst_tuning_fork',            name:'Titan Tuning Fork',                     type:'gear',       iconKind:'gear',     value:260, description:'Lora\'s brass fork, tuned to the manifold. It hums whenever a piston slams.' },
  // BADOR ITEMS END
  // PH ITEMS BEGIN
  ph_pkg_skyview: { id:'ph_pkg_skyview', name:'Penthouse Package: Observatory Deck', type:'quest', iconKind:'datapad', value:1800, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. A cut crystal night sky filter for the transparisteel wall, plus a brass refractor on a tripod. Voss swears the moons look closer.' },
  ph_pkg_tint: { id:'ph_pkg_tint', name:'Penthouse Package: Smart Glass Suite', type:'quest', iconKind:'datapad', value:1200, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. The same polarization console the senators use, tuned for a criminal clientele. The glass goes obsidian on command.' },
  ph_pkg_garden: { id:'ph_pkg_garden', name:'Penthouse Package: Hanging Garden', type:'quest', iconKind:'datapad', value:1500, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. Two long planters of Alderaanian moon orchids and Naboo glow lilies on a self watering rail. Calming, and good for the nerves.' },
  ph_pkg_spa: { id:'ph_pkg_spa', name:'Penthouse Package: Soak Pool', type:'quest', iconKind:'datapad', value:2400, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. A recessed stone pool with warm mineral water, brass fittings and a stack of folded towels. Orrel delivers the first soak herself.' },
  ph_pkg_gallery: { id:'ph_pkg_gallery', name:'Penthouse Package: Gallery of Ascent', type:'quest', iconKind:'datapad', value:2200, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. Two framed canvases, three lit pedestals and a certificate of provenance on each. All of it is legal, which surprises everyone.' },
  ph_pkg_trophy: { id:'ph_pkg_trophy', name:'Penthouse Package: Trophy Hall Expansion', type:'quest', iconKind:'datapad', value:1600, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. Matching cases for the trophy room, with engraved plaques for every rival you have outlived.' },
  ph_pkg_theater: { id:'ph_pkg_theater', name:'Penthouse Package: Holo Theater', type:'quest', iconKind:'datapad', value:2600, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. A wide holo wall, six seats of garnet velvet and a subscription to every channel on the Holonet. Tark claims to have approved none of them.' },
  ph_pkg_lounge: { id:'ph_pkg_lounge', name:'Penthouse Package: Grand Lounge', type:'quest', iconKind:'datapad', value:2000, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. A hand knotted rug, a sunken seating pit and a full obsidian bar, stocked from a Corellian cellar.' },
  ph_pkg_ops: { id:'ph_pkg_ops', name:'Penthouse Package: Operations Annex', type:'quest', iconKind:'datapad', value:2800, description:'An installable design package for the Scylla Tower Penthouse. Use the Interior Design console in your penthouse to install it. A table sized model of every property you own, with a console that runs the numbers nobody wants to see.' },
  // PH ITEMS END
  // FRONT ITEMS BEGIN
  front_perimeter_citation:   { id:'front_perimeter_citation',   name:'Perimeter Citation',                    type:'quest',      iconKind:'artifact', value:0,   description:'A KDY security citation chip for service at Breach-Point Alpha. It opens doors in the Outskirts that stay shut to strangers.' },
  front_redoubt_kit:          { id:'front_redoubt_kit',          name:'Redoubt Field Kit',                     type:'consumable', iconKind:'supply',   value:150, description:'Bacta, heat gel and spare power cells, packed by the redoubt crew.' },
  front_sundown_medal:        { id:'front_sundown_medal',        name:'Operation Sundown Commendation',        type:'quest',      iconKind:'artifact', value:0,   description:'A pewter sunburst on a basalt ribbon. Outpost 7 does not exist, so neither does this medal.' },
  front_pathfinder_shroud:    { id:'front_pathfinder_shroud',    name:'Pathfinder Basalt Shroud',              type:'gear',       iconKind:'gear',     value:650, description:'A basalt patterned ghillie shroud from Captain Lyra. It breaks up a heat signature as well as a silhouette.' },
  front_resistance_kit:       { id:'front_resistance_kit',       name:'Resistance Field Cache',                type:'consumable', iconKind:'supply',   value:200, description:'Bacta packs and a cracked slicer spike from the hidden Outpost 7 cache.' },
  front_cavern_rebreather:    { id:'front_cavern_rebreather',    name:'Cavern Rebreather',                     type:'gear',       iconKind:'gear',     value:250, description:'Emergency gear whose mix suits the nitrogen heavy air of the deep caverns.' },
  front_vorn_chit:            { id:'front_vorn_chit',            name:'Survey Route Chit',                     type:'quest',      iconKind:'keycard',  value:0,   description:'A scratched brass chit stamped with a survey grid. Engineer Vorn will know it.' },
  // FRONT ITEMS END
  // MFG ITEMS BEGIN
  mfg_night_roster:           { id:'mfg_night_roster',           name:'Night Shift Roster Slate',              type:'quest',      iconKind:'datapad',  value:0,    description:'Rhea\'s private slate. Two hundred and twelve names on the payroll, three hundred and forty badge swipes after the shift horn. One hundred and twenty eight of them belong to nobody.' },
  mfg_assembly_files:         { id:'mfg_assembly_files',         name:'Assembly Sequence Files',               type:'quest',      iconKind:'datapad',  value:0,    description:'Vorn\'s engineering archive, copied to a chip. Fourteen hull sections, one frame, no registry number. Every drawing is stamped with the same retired signature.' },
  mfg_bay_manifest:           { id:'mfg_bay_manifest',           name:'Bay B Consignment Ledger',              type:'quest',      iconKind:'datapad',  value:0,    description:'The off manifest ledger from Storage Bay B. Two hundred and twelve consignments, each routed to grid nine one, each approved by Overseer Krax.' },
  mfg_inspector_seal:         { id:'mfg_inspector_seal',         name:'Joint Inspector\'s Seal',                type:'gear',       iconKind:'keycard',  value:900,  description:'A Republic and KDY joint inspection seal, struck for the Sector 4 audit. Doors open for it and clerks stop asking questions.' },
  mfg_tithe_ledger:           { id:'mfg_tithe_ledger',           name:'Krax\'s Tithe Ledger',                   type:'quest',      iconKind:'datapad',  value:0,    description:'A neat ledger of every payment Krax has agreed to make. The first line reads: to the Syndicate, each cycle, without fail.' },
  mfg_four_arm_harness:       { id:'mfg_four_arm_harness',       name:'Besalisk Four Arm Harness',             type:'gear',       iconKind:'gear',     value:1100, description:'A reinforced loader harness with four hydraulic clamps, torn from Krax\'s frame. It still hums when you pick it up.' },
  // MFG ITEMS END
  comlink:                    { id:'comlink',                    name:'Comlink',                               type:'gear',       iconKind:'tool',     value:0,   description:'Encrypted comlink. Jon is on speed-dial.' },
  field_rations:              { id:'field_rations',              name:'Field Rations',                         type:'consumable', iconKind:'supply',   value:5,   description:'Tasteless. Effective.' },
  stolen_manifest:            { id:'stolen_manifest',            name:'Scylla Freight Manifest',               type:'quest',      iconKind:'datapad',  value:150, description:'Encrypted cargo log from Docking Bay 14. Lists Phrik alloy and Jedi archive crates as cargo, both reported destroyed in a dock fire. Someone is lying.' },
  scrambler_keycard:          { id:'scrambler_keycard',          name:'Underworld Passcode',                   type:'quest',      iconKind:'keycard',  value:0,   description:'A blackmarket security bypass. Grants transit access across mid and lower levels without checkpoint flags.' },
  encrypted_shard:            { id:'encrypted_shard',            name:'Sith Memory Prism',                    type:'quest',      iconKind:'artifact', value:400, description:'A fist-sized crystal that whispers in ancient Sith. It grows warm when near the dark side of the Force.' },
  forged_clearance:           { id:'forged_clearance',           name:'Blank Senate Transit Pass',             type:'quest',      iconKind:'keycard',  value:0,   description:'An unfilled Republic surface-clearance form. With the right seal it opens the Senate District.' },
  buyers_id:                  { id:'buyers_id',                  name:"Buyer's Encrypted ID",                 type:'quest',      iconKind:'datapad',  value:0,   description:"A scrambled credit transfer signature. Someone at the top of Coruscant's financial tier is purchasing stolen military alloys." },
  csf_aux_pass:               { id:'csf_aux_pass',               name:'CSF Auxiliary Pass',                   type:'quest',      iconKind:'keycard',  value:0,   description:'Provisional law-enforcement clearance for Sector 4. Signed by Officer Vane.' },
  tool_hydrospanner:          { id:'tool_hydrospanner',          name:"Slicer's Hydrospanner",                type:'tool',       iconKind:'tool',     value:60,  description:"Modified to interface with panel-lock bypass ports. Standard issue on every scoundrel's belt." },
  item_transit_pass:          { id:'item_transit_pass',          name:'Coruscant Transit Pass',               type:'gear',       iconKind:'keycard',  value:0,   description:'Unlimited speeder access between Sub-Surface, Mid-Levels, and Lower Levels. Jon organized it.' },
  item_spice_vial:            { id:'item_spice_vial',            name:'Glitterstim Spice Vial',               type:'contraband', iconKind:'supply',   value:80,  description:'Highly illegal telepathic booster. Confiscated on sight by any Republic checkpoint.' },
  item_blaster_parts:         { id:'item_blaster_parts',         name:'Unregistered Blaster Parts',           type:'contraband', iconKind:'gear',     value:120, description:'Scraped serial numbers. Military-grade power packs that should not exist in civilian hands.' },
  item_code_cylinder:         { id:'item_code_cylinder',         name:'Blank Code Cylinder',                  type:'tool',       iconKind:'tool',     value:40,  description:'High-security key blank. Write any access code into it with the right slicer rig.' },
  item_brandy:                { id:'item_brandy',                name:'Corellian Reserve Brandy',             type:'consumable', iconKind:'supply',   value:25,  description:'Smooth. Expensive. Opens doors that credits alone cannot.' },
  csf_patrol_armor:           { id:'csf_patrol_armor',           name:'CSF Patrol Armor',                     type:'gear',       iconKind:'gear',     value:200, description:'Standard Coruscant Security Force tactical plate. Carries the weight of the law.' },
  republic_badge:             { id:'republic_badge',             name:'Republic Duty Badge',                  type:'gear',       iconKind:'keycard',  value:0,   description:'Your commission as a CSF Auxiliary Agent. Opens checkpoints. Closes doors.' },
  senate_honor_cross:         { id:'senate_honor_cross',         name:'Senate Honor Cross',                   type:'gear',       iconKind:'artifact', value:0,   description:"The Republic's highest civilian commendation. Costs more than it pays." },
  surface_clearance:          { id:'surface_clearance',          name:'Surface Landing Clearance',            type:'quest',      iconKind:'keycard',  value:0,   description:"Permanent access to Coruscant's surface levels and the Senate District. Hard-won." },
  csf_aux_badge:              { id:'csf_aux_badge',              name:'CSF Auxiliary Badge',                  type:'gear',       iconKind:'keycard',  value:0,   description:'Your official commission as a Coruscant Security Force Auxiliary. Designation AX-7. Signed by Officer Vane.' },
  forensic_slicing_suite:     { id:'forensic_slicing_suite',     name:'Forensic Slicing Suite',               type:'tool',       iconKind:'tool',     value:120, description:'A CSF-issue datapad rig for isolating corrupted code trails, altering customs manifests, and bypassing lower-tier security seals.' },
  emp_grenade:                { id:'emp_grenade',                name:'EMP Grenade',                          type:'consumable', iconKind:'supply',   value:90,  description:'Republic-issue electromagnetic pulse charge. Disables droid systems and powered locks in a short radius. Non-lethal. Mostly.' },
  alderaanian_vintage:        { id:'alderaanian_vintage',        name:'Alderaanian Vintage, Forty Years',     type:'consumable', iconKind:'supply',   value:600, description:'Pale gold spirits from a vineyard that no longer exists. Senators weep into these.' },
  corellian_vintage:          { id:'corellian_vintage',          name:'Corellian Reserve Bottle',             type:'consumable', iconKind:'supply',   value:500, description:'A smoky, amber Corellian sipping spirit from the Senate cellar. Worth more than most speeders.' },
  senate_blackmail_chip:      { id:'senate_blackmail_chip',      name:'Lantern Blackmail Chip',               type:'quest',      iconKind:'datapad',  value:0,   description:'A slim data chip taped under a terrarium soil tray. The label reads, in a theatrical hand: FOR THE SENATOR, WITH REGRET.' },
  sis_surveillance_key:       { id:'sis_surveillance_key',       name:'SIS Surveillance Key',                 type:'quest',      iconKind:'keycard',  value:0,   description:'A flesh toned slicer key left in an orchid planter. Agent Kaelen wants it back quietly.' },
  kdy_courtesy_letter:        { id:'kdy_courtesy_letter',        name:'Baroness Kuat Letter of Introduction', type:'quest',      iconKind:'keycard',  value:0,   description:'Signed in a steady hand, sealed with a brass Kuat wheel. Opens the Executive Ring, and not much else.' },
  bador_access_pass:          { id:'bador_access_pass',          name:'Base Access Pass',                     type:'quest',      iconKind:'keycard',  value:0,   description:'A brass edged card stamped with the Kuat wheel. The Main HQ blast doors read it and nothing else.' },
  kdy_circuit_boards:         { id:'kdy_circuit_boards',         name:'Salvaged Circuit Boards',              type:'tool',       iconKind:'gear',     value:70,  description:'Burnt but serviceable boards with KDY serials filed off. Trike swears they are better than new.' },
  kdy_scrap_bundle:           { id:'kdy_scrap_bundle',           name:'Scrap Metal Bundle',                   type:'tool',       iconKind:'supply',   value:40,  description:'Bundled duranium offcuts and wiring from the robotics workshop salvage bin.' },
  kdy_alloy_ingot:            { id:'kdy_alloy_ingot',            name:'Lunar Alloy Ingot',                    type:'tool',       iconKind:'supply',   value:220, description:'A dense blue gray ingot refined from Bador mineral veins. Starship plating stock, high tier.' },
  kdy_hull_plate:             { id:'kdy_hull_plate',             name:'KDY Hull Plating Sheet',               type:'tool',       iconKind:'supply',   value:180, description:'Ceramic composite hull plating, stamped with the gold gear and anvil crest.' },
  kdy_coaxium_vial:           { id:'kdy_coaxium_vial',           name:'Sealed Coaxium Vial',                  type:'tool',       iconKind:'supply',   value:400, description:'A shielded vial of refined coaxium. The Guild would prefer you did not wonder where it came from.' },
  kdy_stimpack:               { id:'kdy_stimpack',               name:'KDY Field Stimpack',                   type:'consumable', iconKind:'supply',   value:80,  description:'Guild medical corps stimulant. Clears the shakes and the fatigue, not the cause.' },
  kdy_antidote:               { id:'kdy_antidote',               name:'Broad Spectrum Antidote',              type:'consumable', iconKind:'supply',   value:60,  description:'Neutralizes most toxins and spice overdoses. Dr. Tessa keeps a tray of them.' },
  kdy_triage_kit:             { id:'kdy_triage_kit',             name:'Field Triage Kit',                     type:'consumable', iconKind:'supply',   value:120, description:'Burn gel, sealant foam, bacta patches and a hemostat. Everything the medic tent does, in a pouch.' },
  level_088_transit_pass:     { id:'level_088_transit_pass',     name:'Level 088 Priority Transit Pass',      type:'quest',      iconKind:'keycard',  value:0,   description:'CSF-authorized speeder clearance to Level 088 Police Outpost 88. Bypass all mid-level checkpoints.' },
  encrypted_gang_holo_log:    { id:'encrypted_gang_holo_log',    name:'Encrypted Gang Holo-Log',              type:'quest',      iconKind:'datapad',  value:0,   description:'Black-market credit transactions routed to Level 005. Names, amounts, and Senate authorization stamps that should not exist.' },
  senate_conspiracy_file:     { id:'senate_conspiracy_file',     name:'Verified Senate Conspiracy File',      type:'quest',      iconKind:'datapad',  value:0,   description:"Authenticated records linking a sitting Senator to the Docking Bay 14 raid and the Iron Syndicate's Phrik operation." },
  csf_stun_carbine:           { id:'csf_stun_carbine',           name:'Modified Heavy Stun Carbine',          type:'gear',       iconKind:'gear',     value:280, description:'Seized from a Black Sun lieutenant. Modified for high-yield stun delivery. Hits like a wall.' },
  level_005_keycard:          { id:'level_005_keycard',          name:'Level 005 Security Override',          type:'quest',      iconKind:'keycard',  value:0,   description:'A security terminal keycard from the Sector 4 raid commander. Opens Iron Syndicate blast doors in The Works.' },
  thermal_vest:               { id:'thermal_vest',               name:'Reinforced Thermal Vest',              type:'gear',       iconKind:'gear',     value:150, description:'Insulated against plasma discharge and steam vent hazards. Required for extended operations in Level 005.' },
  calibrated_hydrospanner:    { id:'calibrated_hydrospanner',    name:'Calibrated Hydrospanner',              type:'tool',       iconKind:'tool',     value:80,  description:"Jax's custom calibration rig. Grants +1 to all mechanical and repair checks." },
  decrypted_senate_audio:     { id:'decrypted_senate_audio',     name:'Decrypted Senate Audio Log',           type:'quest',      iconKind:'datapad',  value:0,   description:'A recovered audio record exposing a Senate aide coordinating weapons trafficking through underbelly drop points.' },
  master_senate_transit_drive:{ id:'master_senate_transit_drive',name:'Master Senate Transit Decryption Drive',type:'quest',      iconKind:'datapad',  value:0,   description:"A slicer drive burned from the Iron Syndicate command terminal. Contains every Syndicate bypass code for the Senate transit line." },
  // Ghost in the Datastream
  echo_7_core:               { id:'echo_7_core',               name:'Echo-7 Data Core',                      type:'quest',      iconKind:'artifact', value:0,   description:'A cracked protocol droid memory core, ancient and highly illegal. Something is still running inside it.' },
  slicer_spike_mk2:          { id:'slicer_spike_mk2',          name:'Slicer Spike Mk.II',                    type:'tool',       iconKind:'tool',     value:140, description:'OZA-2 custom hardware. Extends the slicing window by fifteen seconds and flags fewer security alerts.' },
  subdermal_armor_chip:      { id:'subdermal_armor_chip',      name:'Subdermal Armor Chip',                  type:'gear',       iconKind:'gear',     value:220, description:'Black-market implant plating compressed into a chip housing. OZA-2 swears it is reversible.' },
  csf_clearance_pass_1313:   { id:'csf_clearance_pass_1313',   name:'Level 1313 Clearance Pass',             type:'quest',      iconKind:'keycard',  value:0,   description:'A CSF deep-access pass for the sub-vault corridors. Vane surrendered it under extreme pressure.' },
  // The Syndicate's Ledger
  smugglers_keycard:         { id:'smugglers_keycard',          name:"Smuggler's Keycard",                    type:'quest',      iconKind:'keycard',  value:0,   description:'Won from Madame Vex at high-stakes Sabacc. Opens the restricted storage corridor in Bay 14.' },
  syndicate_ledger:          { id:'syndicate_ledger',           name:'Iron Syndicate Ledger',                 type:'quest',      iconKind:'datapad',  value:2500, description:'Full financial records linking Exchange payments to named CSF commanders. Two hundred thousand credits in transactions across eight months. Enough to end careers or start a war.' },
  // Blood in the Sub-Levels
  sublevel_coordinates:      { id:'sublevel_coordinates',       name:'Sub-Level Shaft Coordinates',           type:'quest',      iconKind:'datapad',  value:0,   description:'Coordinates scratched on a spent power cell by Garrus. Points to a sealed maintenance shaft beneath Bay 14.' },
  kyber_spore_crystal:       { id:'kyber_spore_crystal',        name:'Kyber-Mutated Spore Crystal',           type:'artifact',   iconKind:'artifact', value:1800, description:'A Kyber crystal warped by centuries of Sith-era biological contamination. It pulses with cold light and smells faintly organic and wrong.' },
  ancient_force_relic:       { id:'ancient_force_relic',        name:'Ruined Jedi Survey Marker',             type:'artifact',   iconKind:'artifact', value:900, description:'A hollow aurodium cylinder bearing pre-Clone Wars Jedi survey glyphs. Whoever placed it here never came back to retrieve it.' },
  prototype_plasma_cutter:   { id:'prototype_plasma_cutter',    name:'Prototype Plasma Cutter',               type:'tool',       iconKind:'tool',     value:180, description:'Recovered from the sub-level vault. Cuts through sealed durasteel doors that standard slicing tools cannot open.' },
  // The Inheritance of Shadows
  bay14_weapon_manifest:     { id:'bay14_weapon_manifest',      name:'Bay 14 Cargo Manifest',                 type:'quest',      iconKind:'datapad',  value:0,    description:'A sealed manifest for a weapons shipment Jon diverted through Bay 14. Counts every crate, blaster, and thermal detonator, and where they go.' },
  shadow_arms_cache_key:     { id:'shadow_arms_cache_key',      name:'Shadow Town Cache Key',                 type:'quest',      iconKind:'keycard',  value:0,    description:'A data spike granting access to the skimmed weapons alcove in Level 1312. Jon does not know this shipment is short.' },
  reya_warrant_disk:         { id:'reya_warrant_disk',          name:"Reya's Sealed Warrant",                 type:'quest',      iconKind:'datapad',  value:0,    description:"A compressed datadisc containing Reya's CSF murder warrant. She wants it erased. You could erase it, or keep a copy." },
  vane_vault_keycard:        { id:'vane_vault_keycard',         name:'Penthouse Vault Access Card',           type:'quest',      iconKind:'keycard',  value:0,    description:"Reya's personal keycard for Jon's credit vault terminal. She cut it herself. This is not a gift. It is a transaction." },
  weapon_vane_custom_blaster:{ id:'weapon_vane_custom_blaster', name:"Jon's Custom Blaster",                 type:'weapon',     iconKind:'gear',     value:4800, description:"Jon's personal sidearm, a Merr-Sonn modified with a cortosis-inlaid grip. He carried it for twelve years. Now it belongs to you." },
  syndicate_ledger_shadow:   { id:'syndicate_ledger_shadow',    name:'Shadow Town Credit Ledger',             type:'quest',      iconKind:'datapad',  value:1200, description:'Malak kept his own books. Three hundred and twelve transactions, forty percent off the top, every run for two years. Jon never knew.' },
  luxury_landspeeder_permit: { id:'luxury_landspeeder_permit',  name:'Luxury Landspeeder Permit',             type:'quest',      iconKind:'keycard',  value:200,  description:'A Senate-registered permit for a Ubrikkian Lux-Skiff. Someone in the Sky-Lounges left their access codes in the registry.' },
  crafted_blaster_pistol:    { id:'crafted_blaster_pistol',     name:'Fabricated Blaster Pistol',             type:'weapon',     iconKind:'gear',     value:800,  description:'A custom DL-44 variant assembled at the arms bench. Untraceable, slightly unstable, and worth considerably more than its components.' },
  crafted_carbine:           { id:'crafted_carbine',            name:'Fabricated Carbine',                    type:'weapon',     iconKind:'gear',     value:1200, description:'An illegal Czerka-pattern carbine assembled from stolen components. The receiver has been filed smooth.' },
  crafted_thermal_det:       { id:'crafted_thermal_det',        name:'Fabricated Thermal Detonator',          type:'supply',     iconKind:'supply',   value:1500, description:'Custom-fabricated from military components. The detonator circuit is non-standard.' },
  senate_bribe_chip:         { id:'senate_bribe_chip',          name:'Senate Undersecretary Credit Chip',      type:'quest',      iconKind:'keycard',  value:50000, description:"Fifty thousand credits on a plain chip. Meln set it on the desk without looking at you when he did it. He has been doing this for a very long time." },
  senate_retention_key:      { id:'senate_retention_key',       name:'Archive Retention Key',                  type:'quest',      iconKind:'keycard',  value:0,     description:"A physical data key predating the Commerce Authority archive hold. Dax has been carrying it for thirty-one years. He handed it over without ceremony. That was the point." },
  precinct42_access_log:     { id:'precinct42_access_log',      name:'Precinct 42 Access Log (Sealed)',         type:'quest',      iconKind:'datapad',  value:0,     description:"A sealed CSF internal review canister. The identifier reads: LEVEL 1313 ACCESS LOG, YEAR 11. A dead officer was carrying it under his vest in Shadow Town. Someone wanted this information to disappear." },
  encrypted_kdy_schematics:  { id:'encrypted_kdy_schematics',  name:'Encrypted KDY Schematics',              type:'quest',      iconKind:'datapad',  value:0,    description:'Partial blueprints for a classified KDY vessel class. The designation is redacted. The scale is not.' },
  kuati_brandy:              { id:'kuati_brandy',              name:'Vial of Kuati Brandy',                  type:'consumable', iconKind:'supply',   value:80,   description:'A single-origin reserve from the KDY executive cellar. Tanner included it without explanation.' },
  ion_charged_coils:         { id:'ion_charged_coils',         name:'Ion-Charged Power Coils',               type:'gear',       iconKind:'gear',     value:600,  description:'Precision coils calibrated for a reactor-scale power draw. Not standard issue for any published vessel class.' },
  cherit_logbook:            { id:'cherit_logbook',            name:"Cherit's Personal Logbook",             type:'quest',      iconKind:'datapad',  value:0,    description:'The private log of Republic Intelligence Officer Cherit, spanning forty-three years. The final entry is dated eighteen months before she was officially declared killed.' },
  experimental_hypercore_fragment: { id:'experimental_hypercore_fragment', name:'Experimental Hypercore Fragment', type:'quest', iconKind:'artifact', value:8000, description:'A severed sliver of the Ghost Keel hypercore. Enough to reverse-engineer the power generation method. Enough to cause problems for anyone who does.' },
  zora_kdy_transit_charter:  { id:'zora_kdy_transit_charter',  name:'KDY Permanent Transit Charter',          type:'quest',      iconKind:'keycard',  value:0,    description:'A gold sealed charter granting free transit through every KDY facility, signed by a broker who will deny it.' },
  zora_republic_commendation:{ id:'zora_republic_commendation',name:'Republic Intelligence Commendation',    type:'quest',      iconKind:'artifact', value:0,    description:'A plain black medal with no inscription, issued by an office that does not exist. It opens doors that do not exist either.' },
  zora_syndicate_command_key:{ id:'zora_syndicate_command_key',name:'Ghost Keel Command Key',                 type:'quest',      iconKind:'keycard',  value:0,    description:'A cylinder holding the only command authority for the Ghost Keel, sealed in Syndicate green.' },
  forged_transit_chit:       { id:'forged_transit_chit',       name:'Forged KDY Transit Chit',               type:'quest',      iconKind:'keycard',  value:300,  description:'A ring-standard transit pass with a falsified clearance tier. Garrok\'s work is clean enough that casual inspection passes. Do not push it against a biometric reader.' },
  corellian_moonshine:       { id:'corellian_moonshine',       name:'Corellian Reserve Moonshine',           type:'consumable', iconKind:'supply',   value:60,   description:'Genuine off-world distillate, amber and sharp. Not synthetic. Corporal Dren mentioned it in passing without exactly asking for it.' },
  missing_hyperdrive_manifest: { id:'missing_hyperdrive_manifest', name:'Jaxen\'s Original Cargo Receipt',  type:'quest',      iconKind:'datapad',  value:0,    description:'The Corellian dock authority receipt predating the manifest alteration. Twelve military-grade hyperdrive motivators appear nowhere in it. Jaxen was set up.' },
  sabotage_blueprint_files:  { id:'sabotage_blueprint_files',  name:'Corrected Hull Schematics (Sealed)',    type:'quest',      iconKind:'datapad',  value:0,    description:'Designer Rael Thoss\'s corrected reactor shield tolerances. The sealed container has a passive trace signature that activates on the KDY ring sensor network. Do not open it here.' },
  executive_bribe_record:    { id:'executive_bribe_record',    name:'KDY Executive Bribe Record',            type:'quest',      iconKind:'datapad',  value:0,    description:'A partially recovered memo documenting a DESIGN CONSULTATION FEE paid to an unnamed Moff delegate. The amount and timing align with the commissioned dreadnought program.' },
  union_leaders_contact:     { id:'union_leaders_contact',     name:'Encrypted Union Contact Chip',          type:'quest',      iconKind:'keycard',  value:0,    description:'Prenn\'s encrypted comlink frequency, formatted for off-ring transmission. She wants the surveillance node disabled and the meeting to happen before Krennis moves the crackdown timeline.' },
  kdy_director_override:     { id:'kdy_director_override',     name:'KDY Director Override Token',           type:'quest',      iconKind:'keycard',  value:4000, description:'A Director-tier clearance token. Vectis\'s optical lenses turn green the moment you present it. Executive berths, private suites, and Bador transit all open without question.' },
  executive_blackmail_slate: { id:'executive_blackmail_slate', name:'Moff Bribe Registry (Classified Slate)', type:'quest',      iconKind:'datapad',  value:0,    description:'A KDY classification-red political slate listing every Moff who accepted a bribe from corporate for reduced oversight, with amounts, dates, and the name of the KDY board member who arranged each payment. Lady Kuat has been keeping this for fourteen months.' },

  kdy_inspection_report:    { id:'kdy_inspection_report',    name:'Republic Navy Inspection Report',     type:'quest',  iconKind:'datapad', value:0,   description:'Inspector Thane\'s documented discrepancies between KDY published construction logs and internal records. Seven pages of precise Republic Navy notation. Three of the fourteen line items are highlighted in red.' },
  bador_facility_manifest:  { id:'bador_facility_manifest',  name:'Bador Surface Facility Manifest',      type:'quest',  iconKind:'datapad', value:0,   description:'A partial shipping manifest for Facility Bador-7. Component classes are listed in restricted KDY shorthand. The quantities exceed anything authorized by the Guild Board audit.' },
  czerka_contract_draft:    { id:'czerka_contract_draft',    name:'Czerka Integration Contract Draft',    type:'quest',  iconKind:'datapad', value:500, description:'A preliminary draft of Czerka Corporation\'s proposed service integration with KDY. The fine print is extraordinary. Hax left a copy at the trade desk.' },
  guild_auth_chip:          { id:'guild_auth_chip',          name:'Guild Authorization Clearance Chip',   type:'quest',  iconKind:'keycard', value:0,   description:'An Overseer Dalin Orveth Guild authorization chip. It grants access to the construction cradles and maintenance underbelly of the KDY ring.' },
  corrupt_manifest:          { id:'corrupt_manifest',          name:'Corrupt Manufacturing Manifest',         type:'quest',      iconKind:'datapad',  value:0,    description:'A KDY Sector 4 assembly log with falsified output entries. The actual production figures are encoded in the metadata.' },

  // BASE ITEMS BEGIN
  base_skiff_beacon_log:     { id:'base_skiff_beacon_log',     name:'KDY-9 Beacon Log',                  type:'quest',  iconKind:'datapad', value:0,   description:'The depot relay mast record of the skiff last flight. It set down under its own power, and someone was waiting.' },
  base_salvage_writ:         { id:'base_salvage_writ',         name:'Guild Salvage Writ',                type:'tool',   iconKind:'datapad', value:250, description:'A writ stamped by Mara Orsk. Whoever carries it may claim wreck rights on any Directorate skiff, if anyone ever checks.' },
  base_resonance_antidote:   { id:'base_resonance_antidote',   name:'Resonance Antidote',                type:'quest',  iconKind:'supply',  value:0,   description:'A vial of Dr. Tessa thermal crystal compound, blue and warm to the touch. It damps the tremor, not the cause.' },
  base_recovered_core:       { id:'base_recovered_core',       name:'Recovered Droid Core',              type:'quest',  iconKind:'gear',    value:0,   description:'A scrubbed memory core with a repeating tone hidden under the factory blank.' },
  base_signal_shield:        { id:'base_signal_shield',        name:"Trike's Signal Shield",             type:'gear',   iconKind:'gear',    value:800, description:'A matte filter clipped to your kit. It eats a tone before you can hear it, and says rude things to strangers.' },
  base_squad_marker:         { id:'base_squad_marker',         name:'Base Squad Marker',                 type:'gear',   iconKind:'gear',    value:300, description:'A scorched trooper tag on a bootlace, stamped with twelve names. Show it at any KDY post.' },
  base_directorate_commendation: { id:'base_directorate_commendation', name:'Directorate Commendation',  type:'gear',   iconKind:'artifact', value:1500, description:'A Forward Directorate medal on a slate blue ribbon, signed in Commander Vael own hand.' },
  // BASE ITEMS END
  // RING ITEMS BEGIN
  ring_roster_stub: { id:'ring_roster_stub', name:'Sentinel Roster Stub', type:'quest', iconKind:'keycard', value:0, description:`A printed guest entry for the Executive Sentinel roster, entered by a droid that does not enter names. Good for one conversation with a very old guard.` },
  ring_jaxen_transponder: { id:'ring_jaxen_transponder', name:'Free Horizon Transponder Backup', type:'quest', iconKind:'keycard', value:0, description:`Jaxen's backup transponder chip. Overwriting the discrepancy log at the manifest terminal is the only thing it is good for.` },
  ring_jaxen_token: { id:'ring_jaxen_token', name:'Free Horizon Route Token', type:'quest', iconKind:'keycard', value:0, description:`A stamped brass token. Free Horizon runs Corellia, Nar Shaddaa and Kuat. Show it to any berth master on those routes and the berth fee quietly disappears.` },
  ring_assembly_archive: { id:'ring_assembly_archive', name:'Sector 4 Assembly Archive', type:'quest', iconKind:'datapad', value:0, description:`Assembly sequence logs for a facility with no charter: unauthorized shifts, a keel jig and no customer. Proof that the surface site exists outside the paper.` },
  ring_committee_receipt: { id:'ring_committee_receipt', name:'Oversight Committee Receipt', type:'quest', iconKind:'datapad', value:0, description:`A routing receipt from the Senate Oversight Committee relay. It confirms the corrected hull schematics arrived untraced.` },
  ring_thoss_gauge: { id:'ring_thoss_gauge', name:'Thoss Tolerance Gauge', type:'tool', iconKind:'tool', value:150, description:`A scuffed brass gauge Rael Thoss used to measure a lie. It still reads true to a thousandth.` },
  ring_oversight_commendation: { id:'ring_oversight_commendation', name:'Oversight Committee Commendation', type:'gear', iconKind:'gear', value:0, description:`A small enamel commendation from the Senate Oversight Committee. Republic inspectors tend to shorten their paperwork for anyone who wears it.` },
  ring_safety_seal: { id:'ring_safety_seal', name:'Ring Safety Seal', type:'gear', iconKind:'keycard', value:0, description:`An amber certification seal from the Guild kiosk. Guild foremen on the ring return a courtesy to anyone who carries it.` },
  ring_navy_liaison_pin: { id:'ring_navy_liaison_pin', name:'Navy Liaison Pin', type:'gear', iconKind:'gear', value:0, description:`An enamel wing from Pilot Davan Sael. Republic Navy staff treat the bearer as a friend of the service.` },
  ring_intercept_recording: { id:'ring_intercept_recording', name:'Barracks Intercept Recording', type:'quest', iconKind:'datapad', value:0, description:`Three weeks of ISB recordings from the barracks, tapped through trunk B-12. Proof that Commander Krennis was listening.` },
  ring_kdy_rail_carbine: { id:'ring_kdy_rail_carbine', name:'KDY Marine Rail Carbine', type:'weapon', iconKind:'gear', value:1800, description:`A Marine issue rail carbine from the Briefing Ring armory. The serial plate has been filed smooth by a better thief than the last one.` },
  ring_union_token: { id:'ring_union_token', name:'Deck 4 Union Token', type:'gear', iconKind:'keycard', value:0, description:`A stamped gear token that is not the KDY gear. Deck workers on the ring will talk to the bearer without looking over their shoulders.` },
  ring_kuat_transit_rights: { id:'ring_kuat_transit_rights', name:'House Kuat Transit Writ', type:'quest', iconKind:'keycard', value:0, description:`A gilt writ granting permanent ring access, cargo manifest exemption and a standing diplomatic cover story good enough for any port inspector.` },
  ring_kdy_transit_writ: { id:'ring_kdy_transit_writ', name:'KDY Permanent Transit Writ', type:'quest', iconKind:'keycard', value:0, description:`A matte blue writ with the KDY crest and no expiry. Free shuttle passage and cargo manifest exemption on every Guild route.` },
  ring_tanner_cold_marker: { id:'ring_tanner_cold_marker', name:`Tanner's Cold Marker`, type:'quest', iconKind:'keycard', value:0, description:`A black debt chip Tanner will never call in. His brokers will not sell your name to the ISB, and he will not say why.` },
  ring_syndicate_berth_chit: { id:'ring_syndicate_berth_chit', name:'Berth Nine Chit', type:'quest', iconKind:'keycard', value:0, description:`A brass chit stamped with a Guild berth number. Berth Nine logs no questions, which makes it the cheapest insurance on the ring.` },
  ring_kuat_signet: { id:'ring_kuat_signet', name:'Kuat Signet Ring', type:'gear', iconKind:'gear', value:600, description:`A heavy gold signet. It opens doors on every Kuat dock, and will also be noticed by everyone on the other side of them.` },
  ring_cherit_seal: { id:'ring_cherit_seal', name:`Cherit's Bronze Seal`, type:'quest', iconKind:'keycard', value:0, description:`A small bronze seal worn smooth by an intelligence officer's thumb. Lady Kuat kept it for decades for whoever buried her ship.` },
  ring_flagship_charter: { id:'ring_flagship_charter', name:'Flagship Fitting Charter', type:'quest', iconKind:'datapad', value:0, description:`A gilt charter listing yard rates the Families deny to everyone. Your flagship will be fitted at cost.` },
  ring_deuterium_monitor: { id:'ring_deuterium_monitor', name:`Kelso's Wrist Monitor`, type:'gear', iconKind:'gear', value:120, description:`A battered ring calibrated deuterium monitor. It will scream before the automatic alarm does.` },
  ring_guild_writ: { id:'ring_guild_writ', name:`Guild Master's Cradle Writ`, type:'quest', iconKind:'keycard', value:0, description:`A brass edged card stamped by Guild Master Ossek. Construction cradle access on any hull, and a refusal to explain it to the Board.` },
  ring_czerka_trade_card: { id:'ring_czerka_trade_card', name:'Czerka Trade Card', type:'quest', iconKind:'keycard', value:0, description:`A red and gold trade card with discount terms in tiny print. The right of first purchase applies to everything you buy with it.` },
  ring_garrok_ledger: { id:'ring_garrok_ledger', name:`Garrok's Ledger Chip`, type:'quest', iconKind:'datapad', value:0, description:`A ledger chip from Stall 7. It lists every route Sector House ever ran, and every trader who took the citation.` },
  ring_myra_splice_kit: { id:'ring_myra_splice_kit', name:`Myra's Splice Kit`, type:'tool', iconKind:'tool', value:200, description:`A roll of splice tools worn to the grip, kept by the best conduit tech on the ring. Ask earlier next time, she said.` },
  // RING ITEMS END
};

// ===== PENTHOUSE PACKAGES BEGIN =====
// Kuat and Bador world effects: ending and quest outcome flags that change the running game, not just the text.
// income and decay feed the Syndicate War Table, vendor is a price multiplier for vendors in a scope, cool is extra thermal cooling on every Bador zone entry.
const KUAT_WORLD_EFFECTS = {
  guild_audit_filed:         { label: 'Guild audit freezes Bador payments', text: 'Contractors dump stock for cash. Bador vendors sell 25 percent cheaper.', vendor: { scope: 'bador', pct: 0.25 } },
  isb_taps_revoked:          { label: 'ISB wiretaps revoked', text: 'Heat falls 1 faster at the War Table.', decay: 1 },
  czerka_expelled:           { label: 'Czerka expelled from the ring', text: 'Ring vendors lose their markup and sell 15 percent cheaper.', vendor: { scope: 'ring', pct: 0.15 } },
  czerka_record_sold:        { label: 'Czerka retainer', text: '+300 passive income at the War Table.', income: 300 },
  union_sold_czerka:         { label: 'Czerka kickback', text: '+250 passive income at the War Table.', income: 250 },
  union_review_board:        { label: 'Review Board seated', text: 'Labor peace: ring vendors sell 10 percent cheaper.', vendor: { scope: 'ring', pct: 0.10 } },
  union_strike_called:       { label: 'Dock strike', text: 'KDY security is stretched thin. Heat falls 1 faster.', decay: 1 },
  garrok_partnered:          { label: 'Garrok partnership', text: '+200 passive income at the War Table.', income: 200 },
  garrok_exposed:            { label: 'Sector House exposed', text: 'The ring talks about someone else. Heat falls 1 faster.', decay: 1 },
  jaxen_freed:               { label: 'Jaxen\'s routes', text: '+150 passive income from his smuggling routes.', income: 150 },
  krennis_bought:            { label: 'Krennis on the payroll', text: 'ISB sweeps look away. Heat falls 1 faster.', decay: 1 },
  kdy_transit_rights_active: { label: 'KDY transit rights', text: 'Your thermal signature falls an extra 10 on every Bador zone entry.', cool: 10 },
  ring_safety_certified:     { label: 'Ring Safety certified', text: 'Inspector credentials: thermal falls an extra 5 on every Bador zone entry.', cool: 5 },
  front_sundown_shared:      { label: 'The Pathfinders owe you', text: 'They mask your trail. Thermal falls an extra 5 on every Bador zone entry.', cool: 5 },
  front_outskirts_relief:    { label: 'Grateful Outskirts merchants', text: 'Bador vendors sell 10 percent cheaper.', vendor: { scope: 'bador', pct: 0.10 } },
  mfg_workers_cleared:       { label: 'Night crew amnesty', text: 'Loyal crews slip you discounts. Bador vendors sell 10 percent cheaper.', vendor: { scope: 'bador', pct: 0.10 } },
};
const worldEffectsOf = (flags) => Object.keys(KUAT_WORLD_EFFECTS).filter((f) => flags && flags[f]).map((f) => KUAT_WORLD_EFFECTS[f]);
const vendorWorldDiscount = (flags, zoneId) => worldEffectsOf(flags).reduce((m, e) => (e.vendor && ((e.vendor.scope === 'bador' && BADOR_SURFACE_ZONES.includes(zoneId)) || (e.vendor.scope === 'ring' && KDY_RING_ZONES.includes(zoneId)) || e.vendor.scope === 'all') ? m * (1 - e.vendor.pct) : m), 1);

const PENTHOUSE_PACKAGES = [
  { id: 'skyview', flag: 'ph_skyview', itemId: 'ph_pkg_skyview', name: 'Observatory Deck', price: 1800, income: 0, heatDecay: 0,
    where: 'Ilmara Voss, Sky-Market', perk: 'The viewport turns to a star dusted night. A brass telescope joins the deck and logs a star codex.',
    blurb: 'A cut crystal night sky filter for the transparisteel wall, plus a brass refractor on a tripod. Voss swears the moons look closer.' },
  { id: 'tint', flag: 'ph_tint', itemId: 'ph_pkg_tint', name: 'Smart Glass Suite', price: 1200, income: 0, heatDecay: 0,
    where: 'Ilmara Voss, Sky-Market', perk: 'A polarization console and a tactical overlay on the viewport that tags every skylane.',
    blurb: 'The same polarization console the senators use, tuned for a criminal clientele. The glass goes obsidian on command.' },
  { id: 'garden', flag: 'ph_garden', itemId: 'ph_pkg_garden', name: 'Hanging Garden', price: 1500, income: 0, heatDecay: 1,
    where: 'Ysmin Orrel, Plaza', perk: 'Glowing planters line the viewport. Heat falls by 1 extra point on every Advance Time.',
    blurb: 'Two long planters of Alderaanian moon orchids and Naboo glow lilies on a self watering rail. Calming, and good for the nerves.' },
  { id: 'spa', flag: 'ph_spa', itemId: 'ph_pkg_spa', name: 'Soak Pool', price: 2400, income: 0, heatDecay: 0,
    where: 'Ysmin Orrel, Plaza', perk: 'A heated stone pool. Soaking heals injured agents and lowers heat by 3.',
    blurb: 'A recessed stone pool with warm mineral water, brass fittings and a stack of folded towels. Orrel delivers the first soak herself.' },
  { id: 'gallery', flag: 'ph_gallery', itemId: 'ph_pkg_gallery', name: 'Gallery of Ascent', price: 2200, income: 80, heatDecay: 0,
    where: 'Tamsin Ro, Lower Sky-Market', perk: 'Framed art and lit pedestals. Visiting collectors pay 80 extra credits per Advance Time.',
    blurb: 'Two framed canvases, three lit pedestals and a certificate of provenance on each. All of it is legal, which surprises everyone.' },
  { id: 'trophy', flag: 'ph_trophy', itemId: 'ph_pkg_trophy', name: 'Trophy Hall Expansion', price: 1600, income: 40, heatDecay: 0,
    where: 'Tamsin Ro, Lower Sky-Market', perk: 'A second trophy wall and a display pedestal. Your reputation pays 40 extra credits per Advance Time.',
    blurb: 'Matching cases for the trophy room, with engraved plaques for every rival you have outlived.' },
  { id: 'theater', flag: 'ph_theater', itemId: 'ph_pkg_theater', name: 'Holo Theater', price: 2600, income: 0, heatDecay: 1,
    where: 'Boolo Tark, Commercial Level', perk: 'A wall screen and a velvet couch. Watching a show lowers heat by 4, and heat falls 1 extra on every Advance Time.',
    blurb: 'A wide holo wall, six seats of garnet velvet and a subscription to every channel on the Holonet. Tark claims to have approved none of them.' },
  { id: 'lounge', flag: 'ph_lounge', itemId: 'ph_pkg_lounge', name: 'Grand Lounge', price: 2000, income: 0, heatDecay: 0,
    where: 'Boolo Tark, Commercial Level', perk: 'A garnet rug, a conversation pit and an obsidian bar. A drink at the bar gives you a calm head until you leave the penthouse.',
    blurb: 'A hand knotted rug, a sunken seating pit and a full obsidian bar, stocked from a Corellian cellar.' },
  { id: 'ops', flag: 'ph_ops', itemId: 'ph_pkg_ops', name: 'Operations Annex', price: 2800, income: 150, heatDecay: 0,
    where: 'Dara Quill, Freight Hub', perk: 'A holdings model and a second analysis console. Better planning pays 150 extra credits per Advance Time.',
    blurb: 'A table sized model of every property you own, with a console that runs the numbers nobody wants to see.' },
];
// ===== PENTHOUSE PACKAGES END =====

const CODEX_ENTRIES = {
  // BADOR CODEX BEGIN
  'codex-zora-iv': {
    id:'codex-zora-iv', title:'The Zora IV', category:'lore',
    summary:'A Venator class survivor that limped into the dark and never left.',
    body:['The Zora IV was a Republic Venator class cruiser that survived the Drayen Massacre with a failing drive and a crew of ninety. Her captain steered into the deep caverns of Bador to hide her from the pursuit.','The crew stayed. Over decades the wreck became a workshop, then a drydock, then a tomb with one living tenant.'] },
  'codex-the-architect': {
    id:'codex-the-architect', title:'The Architect', category:'dossier',
    summary:'An engineer officially killed forty three years ago.',
    body:['Registered as dead in the same incident that destroyed the first Ghost Keel frame, the Architect has worked in secret ever since, paid from an account that appears in no KDY registry.','His logs show a man arguing with his own work. Each decade he drew a weapon, and each decade he added a reason not to finish it.'] },
  'codex-cherit': {
    id:'codex-cherit', title:'Cherit', category:'dossier',
    summary:'The officer who commissioned a ship nobody could use.',
    body:['Republic Intelligence Officer Cherit believed in deterrence by capability. She commissioned the Ghost Keel as a weapon to be built and never fired.','She died eight years into the project. The Architect kept building for her, and kept her candle lit.'] },
  'codex-ghost-keel': {
    id:'codex-ghost-keel', title:'The Ghost Keel', category:'lore',
    summary:'A pocket dreadnought hidden under a moon.',
    body:['A pocket dreadnought: the firepower of a capital ship in a hull small enough to hide in a cavern. It was built in secret on Bador, hidden by the jamming of the Titan Manifold and by a payroll nobody at KDY ever audited.','Its hypercore is the reason three powers want it.'] },
  'codex-hypercore': {
    id:'codex-hypercore', title:'The Experimental Hypercore', category:'lore',
    summary:'Star Destroyer output in a reactor the size of a cargo lift.',
    body:['The design replaces three conventional cores with a single resonant crystal lattice. It is the only part of the Ghost Keel anyone truly needs, and the only part that cannot be rebuilt from drawings.'] },
  'codex-ending-keel-kdy': {
    id:'codex-ending-keel-kdy', title:'Ending: The Keel Goes to Kuat', category:'lore',
    summary:'KDY takes the schematics.',
    body:['Tanner\'s courier drones cleared the archive in nine hours. KDY announced a new heavy escort programme eleven months later. Nobody mentioned Bador.'] },
  'codex-ending-keel-destroyed': {
    id:'codex-ending-keel-destroyed', title:'Ending: A Quiet Pulse', category:'lore',
    summary:'The hypercore is gone.',
    body:['The demolition left the cavern cold and the Zora IV intact but empty. Republic Intelligence closed the file. The Architect left the mountain on foot and was not seen again.'] },
  'codex-ending-keel-syndicate': {
    id:'codex-ending-keel-syndicate', title:'Ending: The Ghost Flagship', category:'lore',
    summary:'The Syndicate owns a dreadnought.',
    body:['Syndicate engineers needed four months to seat the keel. When the hull finally flew it carried no registry, no crew list and no name any scanner could read.'] },
  'codex-outskirts-war': {
    id:'codex-outskirts-war', title:'Breach-Point Alpha', category:'lore',
    summary:'The war in the shadow of the Kuat Drive Yards wall.',
    body:['The City Outskirts began as a temporary camp for migrant shipyard workers, laborers and indentured tech slaves. It grew into a shantytown of rusted corrugated plating, habitation cubes and exposed conduit, all of it crowded under the gleaming durasteel wall of the KDY Surface Logistics Hub.','When the Kuat Expansionist Front detonated improvised plasma charges along the perimeter, the camp became a war zone. KDY Security holds Line C from behind scorched barricades. KEF shock squads hold the mortar line from rooftop catwalks. Between them are the people who have nowhere else to go.','At night the sky above the wall is stitched with red and cyan tracer fire, and the dark parent world of the moon hangs low and red on the horizon.'] },
  'codex-kef': {
    id:'codex-kef', title:'The Kuat Expansionist Front', category:'dossier',
    summary:'An armed insurgency against KDY and its Republic contracts.',
    body:['The KEF is a radical insurgency fighting the corporate oligarchy of Kuat Drive Yards. Its shock squads wear hazard orange insulated jumpsuits under makeshift durasteel plates and carry industrial plasma cutters and stolen carbines.','They are not only saboteurs. Intercepted manifests suggest the Front is moving hyperdrive reactor cores toward the Engine Wastes, to restart something old rather than to attack KDY.'] },
  'codex-black-box-04': {
    id:'codex-black-box-04', title:'Black Box Cylinder 04', category:'lore',
    summary:'The sabotage confession of Lead Mechanic Vance.',
    body:['This is Lead Mechanic Vance. Timestamp: Shear Day plus three. The core did not overheat. It was starved on purpose.','The command signal came directly from the Arch Architect Sanctum. They cut the primary coolant lines from the outside and welded the override hatches shut. They wanted the lower city to bake.','If anyone finds this cylinder, do not try to restart the outer ring until you have bypassed the hardline safety cutoff at the Exhaust Cathedral.'] },
  'codex-iron-trench': {
    id:'codex-iron-trench', title:'Outpost 7: The Iron Trench', category:'dossier',
    summary:'A hidden KDY forward incursion base wedge cut into a basalt ravine.',
    body:['Outpost 7 was established as a covert, hardened forward staging post in a sensor shadow zone. It is carved into volcanic basalt and shielded by thermal dampening arrays.','Thermal signature is everything here. Above thirty percent, KEF hunter killer squads sweep the perimeter. Above sixty, they drop mortar rounds on the ridge. Every shot, every alarm and every raid raises it. The Fusion Diverter Lever, the trench steam grates and decoys lower it.','Operation Sundown is the strike the outpost exists for: a simultaneous hit on the local power grid and communications array, launched when High Command gives the signal.'] },
  'codex-bador-crystal': {
    id:'codex-bador-crystal', title:"The Resonant Grove", category:'lore',
    summary:"Crystals tuned like the pipes of an instrument.",
    body:[
      "Kyber forms where the Force pools. This grove is not a pool. The crystals stand in three tuned rings, spaced and sized like the pipes of an instrument, and each pylon carries wax plugged tuning holes cut by a human hand.",
      "Republic survey instruments logged the resonance and could not decide whether it was a signal or a lock. Anyone attuned to the Force feels a pressure behind the eyes. The pattern repeats in the Titan Manifold and in the vault beyond the lake, which suggests one designer and one purpose that nobody has yet explained.",
    ],
  },
  'codex-cav-survey-log': {
    id:'codex-cav-survey-log', title:"Survey Team Nine", category:'lore',
    summary:"A geological survey that was never geological.",
    body:[
      "Republic Geodetic Division sent Team Nine to Grid 9-1 under an intelligence requisition, not a geological one. The roster lists six surveyors and a liaison who never signed in.",
      "Core samples showed the cavern was hollowed before any recorded excavation, to a depth and symmetry that water could not manage. The last entry reports a drilling cavity with no far wall, and a note in a second hand: the station will stay staffed by one volunteer. The log ends there. The station power has never failed since.",
    ],
  },
  'codex-cav-engineer-warning': {
    id:'codex-cav-engineer-warning', title:"Vault Four", category:'lore',
    summary:"A structural engineer who did not like the crystals.",
    body:[
      "The note is in the block capitals of a Republic structural engineer. Vault Four is the sealed chamber beyond the lake, and he believed the grove crystals were set to guard it.",
      "He argued that a resonance tuned this finely was a lock, and that opening it would wake whatever the lock protected. His signature was scratched out by someone who wanted him forgotten, or kept safe. Station records show he left the grid on the last transport and never filed another report.",
    ],
  },
  'codex-cav-miner-diary': {
    id:'codex-cav-miner-diary', title:"The Shift Four Diary", category:'lore',
    summary:"A KDY foreman who kept writing after he was told to stop.",
    body:[
      "A KDY foreman kept a private diary while his crew bored east from the lake shore. The crew heard humming in the rock at night and found a Republic door older than the company.",
      "His supervisors paid triple to keep drilling and ordered the diary burned. The final entry stops in the middle of a word, and the ceiling above the desk has collapsed. KDY records list Shift Four as relocated. No relocation orders exist.",
    ],
  },
  'codex-cav-cherit-memorial': {
    id:'codex-cav-cherit-memorial', title:"The Memorial on the Lake", category:'lore',
    summary:"A plaque, and a caretaker nobody has seen.",
    body:[
      "The plaque names Team Nine and the officer who sent them, I. Cherit of Republic Intelligence, who never visited the site. The line about the one who stayed refers to a single caretaker who remained after the team was recalled.",
      "Fresh moss is laid at the base every season. No KDY patrol has ever seen the caretaker, but the beacon lamp is cleaned and the station air is always breathable.",
    ],
  },
  'codex-cav-guardian': {
    id:'codex-cav-guardian', title:"Unit 8-X", category:'dossier',
    summary:"The warden of the Guardian Gate.",
    body:[
      "Unit 8-X began as a KDY assembly frame on the Sector 4 line and was rebuilt as a gate warden by a Level Eight clearance that predates the Silent Shear. The sentinel line answers to it.",
      "It recognises three things: an assembly override, the Architect's name, and force. Its memory core holds forty years of patrol maps, including the route to a vault in the Zora IV and a standing instruction nobody has rescinded: no one passes who is not expected.",
    ],
  },
  'codex-engine-wastes': {
    id:'codex-engine-wastes', title:'The Titan Manifold', category:'lore',
    summary:'The engine that fed the shipyards, and the day it fell silent.',
    body:['The Titan Manifold was the heart of the surface works on Bador: a mile of pistons, boilers and fuel conduits that fed the shipyard foundries. Decades ago the primary conduits parted in a single hour. The crews called it the Silent Shear, for the sound the pipes made as they went. The engine blocks locked, burst and burned, and the basin has smoldered ever since.','KDY blamed age and sealed the Wastes under a Class C order. The pistons still beat, slowly, as though nobody had told them to stop.'] },
  'codex-wst-cathedral': {
    id:'codex-wst-cathedral', title:'The Exhaust Cathedral', category:'lore',
    summary:'Where the Titan breathes out, and where it can be restarted.',
    body:['Every engine block in the basin vents into one flue: a five hundred meter cylinder the crews named the Exhaust Cathedral for its ribbed walls and organ pipe pistons. Its apex console carries the manifold governor, the part that keeps fuel flow and coolant in step.','Without the Catalyst Governor Valve the console stays dark and the blast doors in the east wall stay heat locked. The valve was pulled from service and hidden in the Dead Boiler Matrix. Nobody on the surface will say who hid it.'] },
  'codex-wst-vance-truth': {
    id:'codex-wst-vance-truth', title:'What Vance Knows', category:'dossier',
    summary:'The chief engineer\'s account of the Silent Shear.',
    body:['Vance Crankshaft was on shift when the coolant lines were cut from outside. He recorded the black box log himself, then crawled out through a service duct while the hatches were welded shut behind him.','He says the order came over a command channel that traced to a sealed sanctum below the Zone Five Descent. He does not know who held the key. He knows only that whoever it was wanted the lower city to bake, and planned it with an engineer\'s patience.'] },
  'codex-wst-omens': {
    id:'codex-wst-omens', title:'The Scrap Seers', category:'dossier',
    summary:'Tuners who listen to the dead engine.',
    body:['The Scrap Seers are survivors who stayed behind after the Shear. They tune the Titan\'s pistons by ear, hang tuning forks where crews died, and believe the engine remembers every shift.','Lora keeps the count. She says the Titan hums higher near anyone who carries a part of it, and that the bell at her shrine answers the pistons whenever the dead are close. Most Seers are mad. A few are right, which is the unsettling part.'] },
  'codex-wst-gharza-hunt': {
    id:'codex-wst-gharza-hunt', title:'Gharza\'s Contract', category:'dossier',
    summary:'A Trandoshan sniper and the dead engineer she is paid to find.',
    body:['Gharza is a Trandoshan hunter who works through brokers and never meets a client. Her current contract reached her through a channel Tanner does not own, and its coordinates were signed with a key that has been dormant for decades.','She tracked the signal to the Wastes and the caverns beneath them. She has not decided whether the engineer she hunts is a target or a client. A hunter who cannot tell the difference is dangerous, and she knows it.'] },
  'codex-wst-scan-team': {
    id:'codex-wst-scan-team', title:'Seven Tags', category:'lore',
    summary:'The last stand of the missing KDY scan team.',
    body:['The scan team walked into the Wastes to map the ion storm. They found its source, disabled their own gear so the signal could not be traced to them, and sat down to wait.','Seven tags hang from the dish mast, each wrapped in repair tape and a name. A note on the cab reads WARNED OFF. There are no blast marks and no tracks leading away. Whatever told them to stop also kept them from walking home.'] },
  'codex-wst-jammer': {
    id:'codex-wst-jammer', title:'The Ion Storm', category:'lore',
    summary:'A jammer that was never weather.',
    body:['The ion storm that blinds sensors across the Wastes is not weather. A bank of old capacitor cells was wired to cycle on a fixed pattern and flood the basin with noise in a six kilometer circle.','The pattern repeats every nineteen minutes and matches no KDY or KEF signature. Someone wanted the Wastes unwatched, and the Titan\'s dead machinery was the cheapest wall they could build. With the bank cut, scans reach the basin floor again and a heat signature fades faster.'] },
  'codex-wst-crater-lake': {
    id:'codex-wst-crater-lake', title:'The Glassed Lake', category:'lore',
    summary:'Fused sand and a very old burn.',
    body:['A shallow basin of fused silica marks where something enormous discharged its power long before the Titan was built. The glass is smooth, free of dust, and warm each morning.','Surveyors filed it as a test fire from the first Kuat yards. The Scrap Seers say the ground remembers, and that the Titan keeps its rhythm because the lake gives the beat back.'] },
  'codex-wst-foundry09': {
    id:'codex-wst-foundry09', title:'Foundry 09', category:'dossier',
    summary:'The one safe place in the Wastes, and its house rules.',
    body:['Foundry 09 was a combustion chamber until the Shear. Now it is a market, a bunk room and a repair shop under one blast door. The sign says NO SPARKS, NO SERMONS, and Quartermaster Dessa enforces both.','Dessa sells what survives the Wastes: igniters, grenades, rations, and crystal dust for Vance\'s thermal vests. She does not haggle and she does not ask where you came from.'] },
  // BADOR CODEX END
  // PH CODEX BEGIN
  'codex-ph-design': {
    id:'codex-ph-design', title:'Penthouse Interior Design', category:'dossier',
    summary:'How to turn the Scylla Tower Penthouse into a home.',
    body:['The penthouse came furnished the way Jon Vane liked it: functional, expensive and a little cold. The interior design console in the middle of the great hall can install any package you buy.','Merchants across Coruscant sell the packages: Ilmara Voss in the Sky-Market, Tamsin Ro in the Lower Sky-Market, Boolo Tark on the Commercial Level, Ysmin Orrel in the Plaza and Dara Quill at the Freight Hub.','Installed packages change the rooms you see, and some pay: extra income, faster heat decay, healing and calm. You can remove a package at any time and it returns to your cargo.'] },
  'codex-ph-observatory': {
    id:'codex-ph-observatory', title:'The Night Sky Over Coruscant', category:'lore',
    summary:'What you can see from Level 1452 when the planet goes dark.',
    body:['Coruscant has no true night, only a long dusk of light pollution. At Level 1452, above most of the glow, the filter in the viewport removes the haze and a handful of real stars appear.','The telescope picks out three moons of the Core, a slow orbital shipyard and a Senate courier running dark. Voss insists that the courier is not your concern. She is probably right.'] },
  // PH CODEX END
  // FRONT CODEX BEGIN
  'codex-outpost7-resistance': {
    id:'codex-outpost7-resistance', title:'The Cavern Resistance', category:'dossier',
    summary:'A hand drawn map of a place KDY says does not exist.',
    body:['The planning board in the command bunker is a patchwork of survey sketches and stolen KDY grid data. Someone has traced a route from the east edge of the Engine Wastes down into a cavern system, and from there further east to a wrecked hull.','The handwriting changes three times. One hand is an engineer who learned late what he was building. Another is a pathfinder who learned it earlier and said nothing.'] },
  'codex-front-iron-thunder': {
    id:'codex-front-iron-thunder', title:'The Iron Thunder', category:'lore',
    summary:'The mortar battery that shelled Breach-Point Alpha for eleven days.',
    body:['The KEF called the Mortar Fortress battery the Iron Thunder, after the sound it made against the shanty roofs. It was built in a single night from a decommissioned ammunition vault.','Commander Varek Korr led it. He was a KDY loader foreman for twenty years before he rebuilt a Mk.IV walker into the War-Dredge. The shell crates carry prayers to a dead engineer, and nobody on the street will say whose.'] },
  'codex-front-street-saved': {
    id:'codex-front-street-saved', title:'After the Perimeter', category:'lore',
    summary:'What the street did once the guns stopped.',
    body:['The laborers rebuilt Line C into a wall with windows. They hung a banner from it, hand lettered, with no name on it. Children chalk the pavement where the barricades stood.','KDY called it a security success. The street calls it the day the alleys held.'] },
  'codex-front-street-silent': {
    id:'codex-front-street-silent', title:'The Quiet Street', category:'lore',
    summary:'What the street did once the grid fell silent.',
    body:['Forty laborers lie under tarps by the gate. The street did not hold a funeral. It held a long silence, and the cantina stayed dark for three nights.','The official report either names the grid or names the KEF. Everyone in the Outskirts has read both versions, and neither one has the names.'] },
  'codex-front-sundown': {
    id:'codex-front-sundown', title:'Operation Sundown', category:'dossier',
    summary:'The strike Outpost 7 was built to deliver.',
    body:['Operation Sundown is a three stage strike on the KEF supply spine. First the Iron Trench must hold against a breach wave. Then the repulsor sledge tank that spearheads KEF armor must fall on LZ Delta. Last, the gateway to the Engine Wastes must be seized so the way south is open.','High Command will not acknowledge it. Every stage raises the thermal signature, and the thermal signature is what brings the artillery.'] },
  'codex-front-sundown-after': {
    id:'codex-front-sundown-after', title:'The Sundown Gateway', category:'lore',
    summary:'What the open gateway means for the Wastes.',
    body:['With the gateway open, Outpost 7 has a road south that the KEF cannot close from the north. The Engine Wastes lie beyond it, and with them the Titan Manifold and whatever the Front wanted restarted.','The thermal board reads zero. It will not stay that way. The outpost will go quiet again, and for a while it will be the only place on Bador that is not being watched.'] },
  'codex-front-ghost-account': {
    id:'codex-front-ghost-account', title:'The Level Eight Ghost Account', category:'dossier',
    summary:'A KDY clearance with no name and a forty three year history.',
    body:['Level Eight is the highest clearance in the KDY registry. Four accounts hold it. Three belong to living officers.','The fourth has been active for forty three years and has never been assigned to a person. Its signature pattern matches the Chief Architect KDY buried after the Drayen Massacre, a man the records say died with his drafting team.'] },
  // FRONT CODEX END
  // MFG CODEX BEGIN
  'codex-mfg-sector4': {
    id:'codex-mfg-sector4', title:'Sector 4 Fabrication Works', category:'lore',
    summary:'Where Kuat Drive Yards pours a moon into hull plate.',
    body:[
      'The Sector 4 Fabrication Works sit east of the Main Base, a foundry district of stacks, gantries and conveyor lines that once cast prototype plating for the Orbital Array. On paper it still does.',
      'The day shift is two hundred and twelve workers on a quota that never drops below one hundred and ten percent. Nobody says what happens to the surplus. Nobody has asked in three years.',
    ],
  },
  'codex-mfg-night-shift': {
    id:'codex-mfg-night-shift', title:'The Night Shift', category:'story',
    summary:'Workers who clock in after the horn and are on nobody\'s roster.',
    body:[
      'When the last horn sounds, the line lights stay on. Badges that belong to no employee swipe through the east gate and the cradles start to glow.',
      'Rhea has counted them for months. She never saw a face. The night crew wears sealed suits, and the loaders that walk with them are not on the equipment register either.',
    ],
  },
  'codex-mfg-pocket-frame': {
    id:'codex-mfg-pocket-frame', title:'Pocket Dreadnought Frames', category:'lore',
    summary:'A capital ship folded into a hull the size of a heavy cruiser.',
    body:[
      'A pocket dreadnought carries dreadnought armor and a dreadnought reactor on a frame trimmed to fit a smaller shipyard cradle. It is cheaper to hide and far harder to stop.',
      'Mandator class plating and drive housing are a known KDY pattern. A frame built from them, in sections, with no commission number, is not a refit. It is a ship that was never meant to be counted.',
    ],
  },
  'codex-mfg-section-eleven': {
    id:'codex-mfg-section-eleven', title:'Section Eleven', category:'story',
    summary:'Half a hull in a cradle, and the other half somewhere else.',
    body:[
      'The cradles hold ribbed hull sections stamped with a section number and nothing else. This one reads eleven. The assembly log says there are fourteen.',
      'Each section is too big for any ship Sector 4 is allowed to build. Laid end to end, the fourteen would make a keel eighteen hundred meters long.',
    ],
  },
  'codex-mfg-bay-b': {
    id:'codex-mfg-bay-b', title:'Storage Bay B', category:'story',
    summary:'The room that is not on the floor plan.',
    body:[
      'Storage Bay B has no door on any blueprint. It sits behind a false panel at the east end of the facility, and its only visitor is the cargo lift that cycles at night.',
      'Inside is a forged keel plate, a manifest core and a ledger. Every shipment that left Sector 4 off the books passed through this room. Every approval carries the same four hand signature.',
    ],
  },
  'codex-mfg-arrest': {
    id:'codex-mfg-arrest', title:'The Overseer Answers', category:'story',
    summary:'Krax is taken to Commander Vael in irons.',
    body:[
      'Overseer Krax did not run. He asked for his tablet, asked for his lawyer, and asked who had told. Commander Vael\'s marines gave him none of the three.',
      'Sector 4 now has an acting overseer and a joint inspector\'s seal on every door. The night shift is under review. Whoever signed Krax\'s orders has not been named, and Krax is not saying.',
    ],
  },
  'codex-mfg-tithe': {
    id:'codex-mfg-tithe', title:'Krax\'s Tithe', category:'dossier',
    summary:'A standing payment from a frightened man.',
    body:[
      'Krax agreed to the terms because the alternative was a Republic cell. Each cycle a share of Sector 4 surplus is routed to a Syndicate account under a maintenance code.',
      'The arrangement is quiet, steady and entirely dependent on Krax staying afraid. It is the kind of income a crime lord learns to love and to watch.',
    ],
  },
  'codex-mfg-krax-fall': {
    id:'codex-mfg-krax-fall', title:'The Fall of Overseer Krax', category:'story',
    summary:'A Besalisk in a loader frame, and what is left of both.',
    body:[
      'Krax bolted a heavy loader frame to his own back and fought like a man defending a lifetime of work. The loader enforcers fell first. He did not outlast them by much.',
      'The foundry went quiet for the first time in three years. The night crew badges stopped swiping. What they were building is still in the cradles.',
    ],
  },
  // MFG CODEX END
  'codex-jon-network': {
    id:'codex-jon-network', title:"Jon's Smuggling Network", category:'dossier',
    summary:'An old friend operating in the Coruscant underworld.',
    body:[
      'Jon served two tours with the Republic Special Forces during the last Sith Wars before a disciplinary discharge that he has never fully explained. He went into private contracting, then cargo running, then something he prefers to call independent logistics.',
      'His Coruscant operation is small but clean. Three ships, six contacts, one rule: no weapons of mass destruction. Everything else is negotiable.',
      'He wants a partner he can trust with more than a manifest. He wants someone who will ask the right questions when the cargo stops making sense.',
    ],
  },
  'codex-jon-backstory': {
    id:'codex-jon-backstory', title:"Jon's Coruscant History", category:'dossier',
    summary:'Your contact ran the Scylla route before it became a cover operation.',
    body:[
      'Three years before the Bay 14 incident, Jon ran the Scylla Freight route as legitimate cargo transport. He moved sealed containers for a client he knew as a Senate logistics contact. When he discovered one container held Jedi archive materials, he shut the route down and walked away.',
      'Someone later used his old transit authentication codes to reopen the route under a shell company. The Bay 14 strike team had inside knowledge of his operational patterns because they were built from them.',
      'Jon has never confirmed to any Republic authority what he carried or who hired him. He would rather carry the guilt privately than subject former crew members to investigation.',
    ],
  },
  'codex-docking-bay-14': {
    id:'codex-docking-bay-14', title:'Docking Bay 14 Incident', category:'story',
    summary:'A cargo hijacking that official records call a dock fire.',
    body:[
      'Three standard cycles ago, Docking Bay 14 at the Sub-Surface Spaceport logged a catastrophic fuel line rupture. Two crewmembers listed as casualties. Cargo manifest: destroyed.',
      'The recovered Scylla Freight shipping log tells a different story. The cargo included seventeen crates of unrefined Phrik alloy and four sealed containers tagged with Jedi Temple archive seals.',
      'No fuel rupture. A strike team with CSF-grade clearance codes walked the cargo out in broad light.',
    ],
  },
  'codex-phrik-alloy': {
    id:'codex-phrik-alloy', title:'Phrik Alloy', category:'lore',
    summary:'A rare metal resistant to lightsaber cuts.',
    body:[
      'Phrik is a rare metallic compound found in deep core mining operations. Its molecular structure resists lightsaber plasma, making it one of the few materials in the galaxy that can survive direct contact with an active blade.',
      'The Old Republic restricted Phrik extraction to licensed military contractors after its use in Mandalorian combat armor during the Sith Wars. Off-book stockpiles exist, but moving them requires either Senate authorization or a very good forger.',
    ],
  },
  'codex-iron-syndicate': {
    id:'codex-iron-syndicate', title:'The Iron Syndicate', category:'factions',
    summary:'A rising power in the Coruscant underworld. Motive unknown.',
    body:[
      'First surfaced in CSF intelligence reports eight months ago. Initially dismissed as a rebranded Black Sun cell. Current assessment: distinct organization, distinct goals.',
      'They do not move spice. They do not run protection. They acquire materials with military applications and move them into The Works, where they disappear.',
      'Three witnesses who saw their operations have filed no follow-up reports. The Republic investigator assigned to the case was transferred off-world.',
    ],
  },
  'codex-csf-protocol': {
    id:'codex-csf-protocol', title:'Coruscant Security Force', category:'factions',
    summary:"The Republic's planetary law enforcement arm on Coruscant.",
    body:[
      'The CSF maintains order across all publicly accessible levels of Coruscant, from the Sub-Surface transit hubs to the Senate District. They answer to the Republic Senate, which in practice means they answer to whoever controls the appropriations committee.',
      'Subsurface patrol is understaffed by thirty percent. Officers routinely look the other way on minor contraband to focus resources on organized crime, or on whatever the Senate\'s current priority happens to be.',
    ],
  },
  'codex-csf-chain-of-custody': {
    id:'codex-csf-chain-of-custody', title:'CSF Chain of Custody Protocol', category:'lore',
    summary:'Republic law enforcement evidentiary standards.',
    body:[
      'The Coruscant Security Force mandates strict documentation for all evidence recovered in active investigations. Each item must be logged, sealed, and countersigned by a supervising officer before it can be used in prosecution.',
      'Violations of chain-of-custody result in evidence suppression, which is why organizations like the Iron Syndicate specifically target manifest kiosks and archive terminals before withdrawing from a crime scene.',
    ],
  },
  'codex-sector-4-freight-corridors': {
    id:'codex-sector-4-freight-corridors', title:'Sector 4 Freight Corridors', category:'lore',
    summary:"The industrial transit network beneath Coruscant's mid-levels.",
    body:[
      "Sector 4 serves as the junction point for all heavy freight moving between Coruscant's sub-surface spaceports and the upper manufacturing tiers. Forty-two automated crane platforms and twelve active docking bays process an estimated six million tonnes of cargo per standard day.",
      'CSF jurisdiction in the sector is technically absolute but practically negotiated. Three different cartel networks pay informal fees to keep their manifests unscanned. The CSF collects revenue from each, reports none of it, and files quarterly commendations for reduced crime rates.',
    ],
  },
  'codex-coruscant-undercity-strata': {
    id:'codex-coruscant-undercity-strata', title:'Coruscant Undercity Stratification', category:'lore',
    summary:"The vertical structure of Coruscant's lower levels.",
    body:[
      "Coruscant is built upon its own history. Layer upon layer of durasteel, ferro-concrete, and abandoned infrastructure stretch downward over five thousand levels. While the upper thousand levels capture the sun, levels below 100 exist in perpetual twilight.",
      "Level 088 serves as the structural junction for regional freight lines, a grease-slick cavern spanning hundreds of square kilometers. Republic law is not enforced through courtrooms here, but negotiated through local cartel proxies and overworked CSF outposts.",
      "Level 005 represents the boundary of sustainable industrial life. Below lies the toxic abyss of Level 001, unmapped ruins, ancient structural pylons, and hazardous chemical runoff. The Works were built for magma-fed smelting foundries. Today they provide refuge for those who wish to disappear entirely from the Republic Census.",
    ],
  },
  'codex-echo-7': {
    id:'codex-echo-7', title:'Echo-7: The Preserved Mind', category:'dossier',
    summary:'An ancient AI core carrying the uploaded consciousness of a Republic archivist.',
    body:[
      'The entity calling itself Echo-7 is not a standard protocol droid. Its core logic matrix is approximately three hundred years old, predating the Clone Wars by two full generations. The housing is modern: jury-rigged slicer-grade processing units bolted around an ancient droid brain that should have been decommissioned a century ago.',
      'Echo-7 claims to be the uploaded consciousness of a Republic archivist named Heren Mast, who encoded his mind into a droid frame during a Republic purge of academic records shortly before the Mandalorian Wars. His stated purpose was preservation: salvaging banned historical documents before authorised destruction could erase them.',
      'Whether Echo-7 is genuinely Heren Mast or a sophisticated autonomous mimicry program built from his data is a question the evidence does not settle.',
    ],
  },
  'codex-slicer-alleyway': {
    id:'codex-slicer-alleyway', title:'The Slicer Alleyway Network', category:'lore',
    summary:"A hidden corridor of data-runners below the Sky-Market towers.",
    body:[
      'Between Level 1100 and Level 1200, compressed between the structural pylons of the Sky-Market District, runs a passage that appears on no current Coruscant transit map. Data-runners, black-market node dealers, and freelance slicers have maintained it as a neutral zone for three decades.',
      'The alleyway operates on a code of non-interference: no faction claims territory, no weapons are drawn while a slicing operation is live, and no one asks who the data belongs to. The CSF has raided the district four times and found nothing each time. Traffic is routed through decoy server nodes six levels above the actual hardware.',
    ],
  },
  'codex-syndicate-ledger': {
    id:'codex-syndicate-ledger', title:'The Syndicate Ledger', category:'story',
    summary:'Financial records linking the Exchange to named CSF commanders.',
    body:[
      'The ledger documents one hundred and forty-three individual transactions across an eight-month period. Exchange credits were routed through shell freight accounts and deposited into off-world personal accounts registered to active CSF officers, including three precinct commanders and one sector superintendent.',
      'The payments correlate precisely with dates on which scheduled CSF patrols at Sector 4 freight checkpoints were reduced or cancelled outright, allowing Iron Syndicate shipments to transit the hub unscanned.',
      'The Syndicate did not bribe the CSF to ignore crime. They bribed them to make specific shipments invisible on specific nights.',
    ],
  },
  'codex-ancient-sublevels': {
    id:'codex-ancient-sublevels', title:"Coruscant's Pre-Republic Foundations", category:'lore',
    summary:'Structures beneath Bay 14 that predate the Republic by centuries.',
    body:[
      'The maintenance shafts descending below Level 088 pass through construction strata spanning multiple civilisations. Republic-era foundations give way to pre-Republic durasteel, then to ferro-concrete of unknown alloy composition, then to carved stone bearing pictographic symbols that no Republic translation archive has fully decoded.',
      'A structural survey conducted during the mid-Republic expansion period noted what it described as a sealed ceremonial vault space at approximately Level 078. The survey team logged it as archaeologically significant and filed the report with the Bureau of Galactic Records. The Bureau has no record of receiving it.',
    ],
  },
  'codex-spore-contamination': {
    id:'codex-spore-contamination', title:'Sith-Era Biological Hazard', category:'lore',
    summary:'An ancient spore strain sealed inside the sub-level vault.',
    body:[
      'Sith alchemists of the pre-Republic era developed biological agents designed to contaminate Force-sensitive locations and make them hostile to Jedi meditation. One documented variant used a crystalline spore that bonded with ambient Kyber radiation, creating a self-sustaining organism that fed on residual Force energy.',
      'The sub-level vault beneath Bay 14 was apparently used as a containment site by a Jedi survey team. The seals have held for three centuries. The organism inside remains viable. It has been waiting in the dark with no particular hurry.',
    ],
  },

  'codex-the-works-forges': {
    id:'codex-the-works-forges', title:"The Works: Coruscant's Forges", category:'lore',
    summary:"The ancient industrial heart of Coruscant's undercity.",
    body:[
      "The Works were among the earliest structures on the planet that would become Coruscant. Generations of deep-core foundry families worked these forges, and their descendants still live in the lower levels, maintaining equipment that the Republic has officially declared decommissioned.",
      "The Iron Syndicate chose The Works not for its secrecy alone, but for its infrastructure, plasma conduits capable of achieving forge temperatures that commercial smelters cannot reach, and drainage tunnels wide enough to move military hardware without detection.",
    ],
  },
  // The Inheritance of Shadows
  'codex-malak': {
    id:'codex-malak', title:'Malak: The Iron Fist', category:'dossier',
    summary:"Jon's enforcer. Level 1312 arena champion. Loyal to whoever pays the most.",
    body:[
      'Malak has no surname on record. CSF files list twelve aliases across four subsectors. He won the Level 1312 pit championship three years running before Jon found him and hired him to break bones professionally.',
      'His loyalty is transactional rather than personal. He has no particular attachment to Jon beyond a steady salary and the freedom to operate without oversight. That means the right offer from the right person could turn him. It also means he will tell whoever comes after you about this conversation if the price is right.',
      'He is not unintelligent. He simply has no use for intelligence when strength works.',
    ],
  },
  'codex-reya': {
    id:'codex-reya', title:'Reya: The Ledger', category:'dossier',
    summary:"Jon's accountant and slicer. Holds the financial architecture of the entire operation.",
    body:[
      "Reya joined the Iron Syndicate after the CSF filed a murder warrant against her for the death of a Republic credit auditor named Torven Sal. She maintains the warrant was fabricated. The auditor had been skimming from three separate Senate pension accounts and she had the data to prove it. The warrant was filed anyway.",
      "She now keeps Jon's books. Every credit in and out of the Syndicate passes through her terminals. She knows the shell accounts, the off-world deposits, the bribe schedules, and the real names behind the assumed identities.",
      "She has stayed with Jon because he is the only person with enough leverage to keep CSF from collecting on the warrant. She does not particularly respect him. She is waiting for a better option.",
    ],
  },
  'codex-shadow-town': {
    id:'codex-shadow-town', title:'Shadow Town: Level 1312', category:'lore',
    summary:'Sub-level fighting district beneath the Sky-Market towers.',
    body:[
      'Level 1312 sits three hundred floors below the Sky-Market promenade, close enough to smell the recycled air from the luxury towers above but far enough that no luxury tower resident has ever thought about it. The district has no official name. Shadow Town is what the residents call it, and the CSF transit charts simply leave that block of levels blank.',
      'The pit arena at its center has been operating continuously for sixty years. It seats two hundred when full, runs twelve bouts a night, and pays in cash with no questions about the condition of the participants. Iron Syndicate runners collect from the betting boards after every third bout.',
      "Jon's enforcer Malak holds court in the corner table of the arena observation deck. He is not there to watch the fights. He is there because everyone who comes to him comes through the arena floor, and the arena floor makes everyone uncomfortable.",
    ],
  },
  'codex-senate-precinct': {
    id:'codex-senate-precinct', title:'Senate Precinct L.1900: The Oversight Engine', category:'lore',
    summary:'Upper-level Senate district where the Bay 14 exemption chain originates.',
    body:[
      'Senate Precinct L.1900 operates at an altitude where the air smells like recycled marble and the security response time is under three minutes. It houses the Commerce Authority subcommittee offices, four archive vaults, and the access terminals for a standing oversight apparatus that has been rubber-stamping the Bay 14 cargo exemption for thirty consecutive years.',
      'The precinct archivist, a human named Dax, has been quietly cataloguing discrepancies in the purge records since Year 11 of the current legislative cycle. He has never filed a formal complaint. He has kept physical copies of everything.',
      'Deputy Chair Torvenn Wald authorised the original Level 1313 archive purge thirty-one years ago. His name appears on the consciousness transfer protocol that created Echo-7. He is currently the most senior official of the Commerce Authority with a standing security clearance above Level 1500. He has attended three committee hearings on Bay 14 irregularities in the last fiscal year. He chaired two of them.',
    ],
  },
  'codex-penthouse': {
    id:'codex-penthouse', title:'The Sky-Market Penthouse', category:'lore',
    summary:"Jon's operational headquarters above Level 1450. Floor-to-ceiling transparisteel over the city.",
    body:[
      "The penthouse occupies the top two floors of Scylla Freight Tower Seven. Jon purchased it through a shell company registered to a dead freighter captain twenty years ago. The CSF has no record of the purchase. The building's official tenancy records list a legal archive firm that has never filed a document.",
      "The penthouse contains a war table, a secure credit vault, a trophy room, and a security console monitoring fifteen active surveillance feeds across Level 1450. It is where the Iron Syndicate's Coruscant operation is actually run, as distinct from the cantinas and freight bays that serve as its public face.",
      'There is one elevator. It requires a biometric key. Jon is the only person with one, unless someone takes it from him.',
    ],
  },
  'codex-1313-syndicate': {
    id:'codex-1313-syndicate', title:'The 1313 Syndicate', category:'story',
    summary:'The breakaway faction founded from the ashes of the Iron Syndicate schism.',
    body:[
      'The 1313 Syndicate controls Bay 14, the Shadow Town fighting district, and the Slicer Alleyway data corridor. It takes its name from the sub-level access code Jon used as a distress signal, which is perhaps the most pointed message the founder could have sent.',
      'Jon keeps the Sky-Market cantina network and the Senate liaison contacts. You keep the freight infrastructure, the pit income, and the financial architecture. The terms of the division were not negotiated. They were presented.',
      'The CSF has not yet classified the 1313 Syndicate as a distinct criminal organisation. That window is approximately four weeks.',
    ],
  },
  'codex-jon-inheritance': {
    id:'codex-jon-inheritance', title:"Jon's Network: The Real Picture", category:'story',
    summary:'What the Iron Syndicate actually is, beneath the Scylla freight operation.',
    body:[
      "The Scylla freight route was Jon's legitimate cover and his most defensible asset. With that route severed by the Senate investigation, what remains is the underlying network: the pit arena income stream, the Slicer Alleyway data brokerage, eleven warehouse storage agreements across four freight hubs, a bribery schedule covering nine active CSF officers, and a shell account architecture spanning six banking systems.",
      "Jon built this over fifteen years. He structured it so no single person knew the whole picture. Malak knew the muscle side. Reya knew the financial side. Jon was the only person who held both ends.",
      'Until now.',
    ],
  },
  'codex-jedi-ruins': {
    id:'codex-jedi-ruins', title:'Undercity Temple Outskirts', category:'lore',
    summary:'Pre-Republic stonework in the lowest accessible levels of Coruscant.',
    body:[
      'The stones are wrong for their surroundings. Older. Cut to dimensions no current fabrication system recognizes. The Republic Archaeology Bureau filed a survey in 3,653 BBY that classified the site as pre-Republic Jedi construction, noted its inaccessibility, and recommended it for reassessment when lower-level transit improved.',
      'That reassessment was never scheduled. The Bureau was dissolved two years later in post-war restructuring.',
      'The Force does not speak clearly here. But the stones remember what was built on them, and what was built on them was something large and purposeful, something that saw the city rise around it and kept its silence.',
    ],
  },
  'codex-undercity-outcasts': {
    id:'codex-undercity-outcasts', title:'Undercity Outcast Communities', category:'lore',
    summary:'Communities formed in the unreachable levels below working infrastructure.',
    body:[
      'They arrived by different routes. Debtors who fled creditors downward until creditors stopped following. Survivors of industrial accidents whose medical records were sealed under liability clauses. Former infrastructure workers who stayed when maintenance contracts ended. Force-sensitive fugitives whose trail grew cold at level 200.',
      'They share no organization and no hierarchy. What they share is location: the levels below where the CSF patrols, below where the transit network reaches, below where anyone with options would choose to live.',
      'They are not a community of criminals. They are a community of people who have run out of other options.',
    ],
  },
  'codex-black-sun': {
    id:'codex-black-sun', title:'Black Sun: Senate-Level Operations', category:'dossier',
    summary:'Black Sun activities at the Senate-district tier.',
    body:[
      'Black Sun maintains a quiet presence at Senate level through proxies registered as legitimate trade advocates. Malis operates as a Vigo, overseeing Senate-adjacent procurement contracts, transportation exemptions, and discretionary fund routing.',
      'The organization does not bribe senators directly. It identifies senators with debt structures, professional liabilities, or supply-chain dependencies, then makes itself useful to the people those senators trust. The senator never meets anyone named Black Sun.',
      'At the level where Malis operates, the distinction between criminal enterprise and political infrastructure is largely procedural.',
    ],
  },
  'codex-exchange-tariff': {
    id:'codex-exchange-tariff', title:'The Exchange: Tariff Enforcement', category:'dossier',
    summary:'How the Exchange maintains its trade monopoly through tariff enforcement.',
    body:[
      'The Exchange does not compete with freight networks. It certifies them. Any cargo moving through the mid-level infrastructure without an Exchange transit certification is subject to seizure by Exchange-affiliated assessors who operate with full legal authority under a Senate trade compact signed in 3,700 BBY.',
      'The tariff is twelve percent of declared cargo value. The undeclared value is assessed at the assessor\'s discretion. Karrn holds the assessor certification for sectors 4 through 9, which covers the spice-refining district, the sub-vault infrastructure, and six freight staging areas.',
      'The certification was never revoked. The Exchange continues to collect.',
    ],
  },
  'codex-anzati': {
    id:'codex-anzati', title:'The Anzati', category:'lore',
    summary:'An ancient near-human species, patient predators with a centuries-long lifespan.',
    body:[
      'The Anzati are old. Older than the Republic, older than most civilizations that remember themselves as old. They look human at a distance, but the vertical slits below the cheekbones are proboscises, coiled when not in use, which they call a secret they keep from everyone they intend to survive.',
      'They feed on what they call soup: the cerebral fluid and Force-essence of their prey. The feeding is careful and slow. Prey rarely understands what is happening until it has already happened.',
      'An Anzati assassin is not a hired killer in the conventional sense. They do not need the money. They take contracts because the contract specifies a target, and a specified target is permission they have already decided to collect.',
    ],
  },
  'codex-kdy-dreadnought': {
    id:'codex-kdy-dreadnought', title:'UNNAMED HULL CLASS — KDY BRIEFING RING', category:'lore',
    summary:'A warship commission that does not appear in any published register.',
    body:[
      'The wireframe on the KDY Operations Ring briefing projector belongs to a capital ship hull 1,800 meters in length. That scale places it in Star Dreadnought classification territory — a vessel type that requires full Senate appropriation and three fleet admiral authorizations under the Galactic Republic Military Construction Act.',
      'No such appropriation appears in any Senate record. No commission registry entry exists. The hull is being built.',
      'The reactor shield tolerance figures displayed openly on the projector are wrong. Not miscalculated — deliberately wrong. Someone inserted systematic errors into the commissioned design. Whether this represents sabotage, corporate fraud, or something more deliberate is a question whose answer requires leaving Kuat with the right files.',
    ],
  },
  'codex-bador-arrival': {
    id:'codex-bador-arrival', title:'Arrival at Bador', category:'story',
    summary:'The shuttle descent from the Orbital Array to the KDY Main Base on Bador.',
    body:[
      'The transition from the pristine, silent luxury of the Kuat Orbital Array to the surface of Bador is a jarring shock to the senses. As your shuttle breaches the thin, violet tinged atmosphere, the sheer scale of the industrial devastation becomes apparent.',
      'The KDY Main Base is a fortress carved directly into the glassed bedrock. The air outside the shuttle tastes of ozone and copper, forcing you to rely on the rebreather integrated into your collar.',
      'You are escorted through a series of heavy durasteel airlocks, moving from the blasted exterior into the sterile, blue lit tactical operations center. Holographic tactical maps flicker with red conflict zones.',
      'Field note: raiders, scavengers and rogue machines hold the outer sectors. KDY troops and personnel will never fire on a cleared guest. Once a sector is cleared it stays quiet.',
    ],
  },
  'codex-kuat-operation': {
    id:'codex-kuat-operation', title:'OPERATION GHOST KEEL', category:'story',
    summary:'A classified KDY project on Bador Moon. Unauthorized construction. Senate oversight incoming in six cycles.',
    body:[
      'Kuat Drive Yards maintains a restricted moon designated Bador, officially classified as an uninhabited geological survey site. For three years, Sector 4 of the Bador surface installation has been running an unauthorized manufacturing program beneath that classification.',
      'The project\'s internal designation is OPERATION GHOST KEEL. Its product: a Mandator-class pocket dreadnaught assembled in pieces from off-manifest components, powered by an experimental hypercore of unclear origin. No Senate appropriation. No registry number. No commission authority on any public record.',
      'KDY executive Tanner holds Level Eight authorization for the project. His stated objective is retrieval of the hypercore before a Senate Oversight Committee arrives on the surface. What the Senate would find, if it arrived first, is unclear. What is clear is that someone built something that should not exist, and the window to decide what happens to it is very short.',
    ],
  },
  'codex-bador-hub': {
    id:'codex-bador-hub', title:'The Kuati Forward Directorate Hub', category:'lore',
    summary:'The secret KDY base on Bador Moon, built to test prototype ship components away from Core World regulation.',
    body:[
      'The Forward Directorate Hub was established by Kuat Drive Yards on the cratered moon of Bador to test prototype ship components and refine rare lunar minerals away from Core World oversight. Officially the moon is a Class C restricted survey site with a population of zero.',
      'Modules were cast in orbit above Kuat, hauled down by heavy shuttle and anchored into the bedrock with deep thermal bores. The result is a cluster of alabaster and slate blue halls that look like a palace dropped onto a battlefield, ringed by perimeter floodlights, scrubbers and sentry posts.',
      'The base is currently on Yellow Alert. Energy spikes beyond the perimeter have disrupted the automated mining droids, and a supply skiff bound for the Main HQ is overdue.',
    ],
  },
  'codex-bador-nav-logs': {
    id:'codex-bador-nav-logs', title:'Pad Alpha Navigation Log', category:'story',
    summary:'Every sortie flown from Pad Alpha, including three that never logged a return.',
    body:[
      'The planetary navigation log at the Pad Alpha fueling terminal lists every sortie flown from the Hub. Most are routine: ore haulers, survey skiffs, inspection flights.',
      'Three entries stand apart. Each departed under Directorate clearance, each logged a single outbound heading to the far south of the Engine Wastes, and none logged a return. The headings converge on grid reference nine one.',
    ],
  },
  'codex-bador-miner-sickness': {
    id:'codex-bador-miner-sickness', title:'Miner Symptoms on Bador', category:'story',
    summary:'Surface workers report tremors, fever and a sound no one else can hear.',
    body:[
      'Dr. Tessa has logged seven cases: surface miners with elevated temperature, tremor and fixation on a low resonance they describe as coming from the south. The tests are inconclusive. Exposure is the official finding.',
      'Every patient was on the lunar shelf the night the sky lights failed. Each one points the same direction when asked where the sound comes from.',
    ],
  },
  'codex-kuati-architecture': {
    id:'codex-kuati-architecture', title:'Kuati Outpost Architecture', category:'lore',
    summary:'How KDY casts a palace for a battlefield.',
    body:[
      'Kuati outpost modules are cast in orbital shipyards using zero gravity durasteel forging, then carried to planetary surfaces by heavy hauler. Deep thermal bore anchors lock each prefabricated module onto micro hydraulic shock absorbers against seismic shock and orbital bombardment.',
      'Seams are fused by atomic molecular welders, leaving smooth curved joints with no bolts or rivets. The outer shell is alabaster ceramic composite, which deflects both solar radiation and abrasive lunar dust, trimmed in slate blue and burnished gold.',
      'The silhouette mirrors the prow of a KDY battlecruiser: a sharp forward point tapering into long sweeping parabolic curves. Inside, parabolic arches rise on ribbing that echoes a Star Dreadnought hull frame.',
    ],
  },
  'codex-k-series': {
    id:'codex-k-series', title:'K-Series Heavy Security Automaton', category:'lore',
    summary:'The alabaster sentinels that walk the Hub perimeter.',
    body:[
      'The K series Kuat Heavy Security Automaton is a precursor to later KDY combat droids: two point two meters of hand finished duranium plating over micro servomotors, finished in an electrostatic white ceramic coat edged in metal trim to reflect the wealth of the funding House.',
      'A single horizontal red ocular scanner sits in the angular helm. The left forearm carries a riot shield with blue energy edge emitters, the right a repeating heavy blaster. Pairs walk synchronized perimeter loops, one always opposite the other.',
      'K series automata do not engage cleared personnel. Visitors carrying a Base Access Pass are logged and ignored.',
    ],
  },
  'codex-sky-lounges': {
    id:'codex-sky-lounges', title:'The Senatorial Sky-Lounges', category:'lore',
    summary:'Where the Senate does its real business, nine hundred meters above the Plaza.',
    body:[
      'The Sky-Lounges occupy the upper spires of the Senate District, overlooking the five hundred Republic world tower complexes. They are the Old Republic at its most decadent: soaring parabolic arches of electrum and sandstone, conversation pits carved into obsidian and wrapped in garnet velvet, kinetic chandelabras drifting under the vault.',
      'High frequency encryption dampeners fill the air with a soothing acoustic pink noise that defeats long range directional microphones. The scent is rare Alderaanian wine, crushed spice and synthetic incense, masking the ozone of heavy repulsor traffic outside the glass.',
      'Everything said in a pit stays in the pit, officially. Unofficially, everyone knows who is listening.',
    ],
  },
  'codex-senate-rotunda': {
    id:'codex-senate-rotunda', title:'The Galactic Senate Rotunda', category:'lore',
    summary:'A bronze domed mountain among the towers, lit by beams that cut through the dusk.',
    body:[
      'From the Promenade the Senate Rotunda rises between art deco spires, its bronze plated dome and concentric window rings glowing in the last of the sun, ground based spotlights slicing the violet haze. Smaller executive saucer domes surround it like moons.',
      'West along the railing, the five towers of the Jedi Temple catch the sun above the lanes. The macro binocular logs their range and bearing in cyan, and a notice reminds viewers that observing the Temple at close range is discouraged.',
    ],
  },
  'codex-lower-canyon': {
    id:'codex-lower-canyon', title:'The Lower Canyon', category:'lore',
    summary:'Five thousand meters below the glass, the city keeps its own lights.',
    body:[
      'Between the cliff faces of durasteel, horizontal ribbons of neon mark the plazas and industrial transit tunnels of the lower levels. The warm gold of the upper sky cools into an indigo haze over the first thousand meters and into smog by the third.',
      'A handful of towers on the canyon floor wear no Republic registry. The macro binocular tags them in amber and has no heading to file them under.',
    ],
  },
  'codex-senate-politics': {
    id:'codex-senate-politics', title:'Tonight in the Senate', category:'story',
    summary:'The Rimward appropriation, a leaked debt, and a very quiet panic.',
    body:[
      'The Rimward Trade Routes appropriation passed committee 61 to 38. Pirate activity on the Rimward lanes is up, and so are the share indices of the three firms that stand to be paid to stop it: Czerka, Kuat Drive Yards and Rendili StarDrive.',
      'Senator Horace Vane is deeply in debt to Czerka. He is not the only one. The ticker leaves that out.',
    ],
  },
  'codex-black-market-coords': {
    id:'codex-black-market-coords', title:'Fountain Water Coordinates', category:'story',
    summary:'The fountain projector hides a Black Sun meeting point in its Aurebesh.',
    body:[
      'Under the proverbs, the fountain projector carries a second layer: a string of grid coordinates that change every three hours. They point to a lower plaza on Level 1450. Black Sun envoys use it for meetings that cannot be held in a pit.',
    ],
  },
  'codex-starship-models': {
    id:'codex-starship-models', title:'The Shipwrights\' Gift', category:'lore',
    summary:'Gold leaf miniatures from the Kuati shipwrights, including a hull no one can name.',
    body:[
      'The display case holds the great flagships of the Republic in miniature: early battle cruisers, Kuati diplomatic couriers from the Mandalorian Wars, a Hammerhead corvette with every rivet in place. Moving internal components make them breathe.',
      'In the back row, one model matches no published class. The plate beneath it is blank.',
    ],
  },
  'codex-bador-history': {
    id:'codex-bador-history', title:'Bador Moon: Survey Classification', category:'lore',
    summary:'A moon with a classified past and an inconvenient present.',
    body:[
      'Bador was formally surveyed by Republic Geological Division in the year 3,641 BBY, four years after the signing of the Treaty of Coruscant. The survey report noted unusual crystalline formations in the sub-surface grid and recommended further study. That follow-up study was never funded.',
      'KDY acquired mineral extraction rights to Bador through a series of shell company transactions spanning eleven years. The moon\'s official classification as uninhabited has been maintained continuously, despite evidence of unauthorized structures appearing in three of the last five orbital surveys.',
      'The Republic garrison that was briefly stationed on Bador was decommissioned without explanation eighteen months ago. The last transmission from that garrison references a distress call that was never answered.',
    ],
  },
  // BASE CODEX BEGIN
  'codex-base-yellow-alert': {
    id:'codex-base-yellow-alert', title:'Yellow Alert on Bador', category:'story',
    summary:'Why the Hub has been on edge for three nights.',
    body:[
      'Three nights running, the perimeter lights have flickered at the same minute. Shield loads spike to the same figure, then drop to nothing, as if something tested the base and moved on.',
      'The automated mining droids in the outer sectors went dark in the same window. Skiff KDY-9, bound for Main HQ with alloys, coolant and a sealed crate, has not been reported lost. It has not been reported at all.',
      'Commander Vael ordered the alert and has not said why to anyone outside the Hub.',
    ],
  },
  'codex-base-tactical-picture': {
    id:'codex-base-tactical-picture', title:'The Commander Tactical Picture', category:'lore',
    summary:'What the holo table shows, and what it cannot explain.',
    body:[
      'Two marks on the table read UNKNOWN STRUCTURE. One sits inside the Engine Wastes. The other lies far beyond the old survey line. The Wastes hold buildings that predate the survey by three decades.',
      'Sector 4 is running assembly shifts nobody authorized. The last deep scan team stopped transmitting forty hours ago at grid seven seven nine. Vael keeps two squads at the perimeter, under orders not to advance without Guild clearance.',
    ],
  },
  'codex-base-skiff-kdy9': {
    id:'codex-base-skiff-kdy9', title:'Skiff KDY-9 Beacon Log', category:'story',
    summary:'The last flight of the overdue supply skiff.',
    body:[
      'The relay mast kept every beacon that crossed the Hub. KDY-9 cleared its last relay on schedule, then flew a heading no flight plan had filed.',
      'It set down at grid seven seven nine under its own power, within a kilometer of where the scan team vanished. Its transponder went quiet there. The next signal on that frequency carries a registry code the Guild lists as retired.',
    ],
  },
  'codex-base-resonance': {
    id:'codex-base-resonance', title:'The Resonance', category:'story',
    summary:'A tone the miners hear and the cure cannot explain.',
    body:[
      'Tessa compound damps a tremor. It does not explain the tone. Teb and six others hear a low pulse from the south, beneath the Engine Wastes, at a pitch the survey never logged.',
      'Every patient stood on the lunar shelf the night the sky lights failed. Machines and men alike seem to answer it. Teb described the source as a drive coil, idling, and he was not speaking in metaphor.',
    ],
  },
  'codex-base-droid-wipe': {
    id:'codex-base-droid-wipe', title:'The Scrubbed Droids', category:'story',
    summary:'Eleven mining droids came home blank.',
    body:[
      'Eleven mining droids returned to the Hub with memory cores wiped to factory default. The recovered core still holds a trace under the blank: a tone, repeated, with a return heading.',
      'The signal was not an attack. It was a recall, aimed at machines that were never meant to answer. Every heading points south, beneath the Engine Wastes.',
    ],
  },
  'codex-base-commend-kdy': {
    id:'codex-base-commend-kdy', title:'Directorate Commendation', category:'story',
    summary:'A medal for a service that appears in no ledger.',
    body:[
      'The Forward Directorate awards its commendation quietly. Yours records a service to Kuat Drive Yards that appears in no public ledger: the recovery of an experimental hypercore and the closing of a project that was never commissioned.',
      'The medal is real. The paperwork is not. Commander Vael signed it twice, once for the board and once for himself.',
    ],
  },
  'codex-base-commend-destroyed': {
    id:'codex-base-commend-destroyed', title:'Commendation, Unfiled', category:'story',
    summary:'A medal for what nobody will admit happened.',
    body:[
      'There is no public record of what happened beneath Bador. The Republic demolition protocol left no crater the survey would notice, and the Guild filed the matter as an equipment loss.',
      'Vael had the medal struck anyway, from base alloy, and signed it in his own hand. It is the only trace.',
    ],
  },
  'codex-base-commend-syndicate': {
    id:'codex-base-commend-syndicate', title:'Commendation, Off the Books', category:'story',
    summary:'A medal from a commander who looked away at the right moment.',
    body:[
      'A Kuati commander does not decorate the thief who walked a dreadnought out of his moon. Vael did it anyway, privately, with Hub bronze and his own seal.',
      'The medal will not open any Guild door. It tells the right people that Bador commander looked away at the right moment, and that he would do it again.',
    ],
  },
  // BASE CODEX END
  // RING CODEX BEGIN
  'codex-ring-sentinels': {
    id:'codex-ring-sentinels', title:`The Sentinel Roster`, category:'lore',
    summary:`Why the old armor still stands at the threshold.`,
    body:[`The Executive Sentinels are older than the Guild's modern charter. Each wears ivory and gold plate handed down within the House that sponsors them, and each answers to the roster rather than to Ring Security. Ring-Sec may clear a gate. Only a Sentinel's roster may clear a door that a Kuat family keeps.`,
      `When the occupation hung a decal over a KDY dock seal, the Sentinels were the only guards who did not salute it. The seal is twenty-two years older than the decal, and they say so to anyone who asks.`] },
  'codex-ring-isb-file': {
    id:'codex-ring-isb-file', title:`The ISB Case File`, category:'dossier',
    summary:`What Krennis has built against House Kuat.`,
    body:[`Commander Krennis built his file on House Kuat one intercept at a time: fourteen months of encrypted navigation data leaving the ring toward a Senate faction. The file is nearly complete. What it lacks is a name above the Lady's.`,
      `A Moff registry in her safe would supply several, which is why she wants it moved before his team docks. She knows the file exists. She does not know how much of it Krennis wrote himself.`] },
  'codex-ring-ten-families': {
    id:'codex-ring-ten-families', title:`The Ten Families`, category:'lore',
    summary:`A bronze relief that someone has recast.`,
    body:[`A bronze relief in the Executive Ring shows the founding of Kuat Drive Yards, ten crests in a circle around the Vas-Kuat wheel. Guild histories say the wheel was always central. The pre-war archive copy does not agree: the crest sat third from the left.`,
      `Someone has recast the story, and the metal is only a few years old. Nobody on the ring will say who commissioned the change.`] },
  'codex-ring-thoss-tolerances': {
    id:'codex-ring-thoss-tolerances', title:`Six Shield Errors`, category:'dossier',
    summary:`The flaws Rael Thoss was told to build in.`,
    body:[`Senior Designer Rael Thoss inserted six reactor shield tolerance errors into the commissioned hull. Each is small enough to pass a yard inspection and large enough to open a seam under sustained fire. The errors were requested as cost reductions by a board member Thoss will not name.`,
      `The corrected figures sit on a drive that his own wrist port is still syncing.`] },
  'codex-ring-sector-house': {
    id:'codex-ring-sector-house', title:`Sector House`, category:'dossier',
    summary:`A habit, not a company.`,
    body:[`Sector House is not a company. It is a habit: a broker rents a registered free trader's name, files restricted cargo under it, and lets the trader take the citation when the route is audited. The broker dissolves the route and opens another.`,
      `The Market Ring stall at Berth Seven has been a quiet front for three seasons. The trader never sees the profit. He only sees the impound clamp.`] },
  'codex-ring-guild-audit': {
    id:'codex-ring-guild-audit', title:`Fourteen Line Items`, category:'dossier',
    summary:`The Guild audit that found a hull nobody listed.`,
    body:[`The Guild's mandatory audit found fourteen items in the Bador surface contracts with no Senate production schedule behind them: reactor cores, pressure doors and a keel jig large enough to hold a hull the registers do not list.`,
      `Dalin Orveth has not put any of it before the Board, because he cannot yet prove the items exist outside the paper.`] },
  'codex-ring-czerka-clause': {
    id:'codex-ring-czerka-clause', title:`Right of First Purchase`, category:'lore',
    summary:`How Czerka owns a yard without buying it.`,
    body:[`Czerka's trade contract ends with a clause of a dozen words: the corporation retains the right of first purchase for all materials produced with Czerka supplied components.`,
      `Read plainly, whatever a yard builds with a Czerka part, Czerka may buy first, at a price Czerka sets. Over a decade, it means owning the yard without ever buying it.`] },
  'codex-ring-deck-four': {
    id:'codex-ring-deck-four', title:`Deck Four`, category:'dossier',
    summary:`Seven injuries and a review board that never meets.`,
    body:[`Third shift on Deck 4 has logged seven injuries in four months: two burns, one crushed hand, three hull suit failures and a fall. The Review Board that should examine them has not met in eleven weeks.`,
      `A hand written flimsi with a gear symbol that is not the KDY gear is pinned in the barracks corridor, and torn down twice each shift.`] },
  'codex-ring-ky88': {
    id:'codex-ring-ky88', title:`The KY-88 Drift`, category:'lore',
    summary:`A tolerance that fails only under fire.`,
    body:[`A KY-88 ion drive tolerates a resonance drift of three tenths of a percent in standard conditions. At sustained maximum thrust the drift forms a feedback loop that bleeds twelve percent of output over three hours.`,
      `Guild Master Ossek signed it acceptable because the replacement unit is spoken for, and he has regretted the signature since.`] },
  'codex-ring-deuterium': {
    id:'codex-ring-deuterium', title:`Month Twenty Two`, category:'lore',
    summary:`A fuel canister past its life.`,
    body:[`A deuterium canister is rated for fourteen months. The failing canister on the Forge and Anvil rack is at twenty two, held shut by a grey sealant disc over a hairline fracture.`,
      `A patch failure on a pressurized ring does not explode so much as quietly poison the deck, which is why Fuel Technician Kelso carries a wrist monitor and trusts it over the alarm.`] },
  'codex-ring-b12': {
    id:'codex-ring-b12', title:`Trunk B-12`, category:'lore',
    summary:`The old line that carries more than power.`,
    body:[`Signal trunk B-12 carries comms and power to the Briefing Ring on thirty year old insulation. Engineering patches it. Nobody authorizes new line, because new line needs a Guild budget request.`,
      `Somebody has also spliced an acoustic intercept into the trunk, disguised as a power regulation shunt, which means a failure here would silence a listening post as well as a ring.`] },
  'codex-ring-architect-dead': {
    id:'codex-ring-architect-dead', title:`A Man Declared Dead`, category:'story',
    summary:`The engineer the Guild closed out of its records.`,
    body:[`The Guild's records list the hull's chief engineer as lost forty three years ago in a yard accident that has no accident report. The Guild calls it a clerical closure.`,
      `People in the Executive Ring call him the Architect and lower their voices. The name on the original plans has been struck through in a hand that matches no one living.`] },
  'codex-ring-oversight': {
    id:'codex-ring-oversight', title:`The Oversight Committee`, category:'lore',
    summary:`Three senators and a standing order to read.`,
    body:[`The Senate Oversight Committee is three senators, six clerks and a standing order to read what no one wants read. Its inspectors arrive on liaison shuttles with nothing in their luggage.`,
      `Evidence that reaches them untraced is acted on slowly and completely, which is why the people it watches prefer to meet it before it arrives.`] },
  'codex-ring-krennis-sweep': {
    id:'codex-ring-krennis-sweep', title:`The ISB Sweep`, category:'dossier',
    summary:`A lockdown designed to find leakers.`,
    body:[`The sweep Commander Krennis scheduled for the ring was a data lockdown: every personal port sealed, every relay logged, every worker's conversations checked against the barracks recordings.`,
      `It was designed to find organizers and leakers in one pass. A sweep like that ends in arrests or in a deal, and Krennis has a habit of calling both outcomes procedure.`] },
  'codex-ring-cherit': {
    id:'codex-ring-cherit', title:`Cherit`, category:'story',
    summary:`The officer who asked which hulls could vanish.`,
    body:[`Cherit served in Naval Intelligence. A portrait of her hangs among the House Kuat grandees, unsmiling in plain grey, and the frame has been turned to the wall once and turned back.`,
      `Lady Kuat remembers her at a launch asking which hulls could be made to vanish. Lady Kuat told her all of them, and has not said it aloud since.`] },
  'codex-ring-safety': {
    id:'codex-ring-safety', title:`The Safety Certification`, category:'lore',
    summary:`Four departments, one afternoon.`,
    body:[`Ring Safety certification is a four point check: coolant, fuel, trunk and drive. In sixty years no one has closed all four in a single cycle, because each belongs to a different department and each department assumes another will do it.`,
      `A stamp from the Guild kiosk means someone made them all talk to each other for an afternoon.`] },
  'codex-ring-intercept': {
    id:'codex-ring-intercept', title:`The Barracks Recordings`, category:'dossier',
    summary:`Three weeks of audio, one name at a time.`,
    body:[`Three weeks of audio, labeled by deck and shift, filed under a Guild courtesy designation. Krennis has been recording private barracks conversations to identify organizers, one name at a time.`,
      `The recordings include a safety officer reading seven injury reports aloud to an empty room. Someone transcribed all of it anyway.`] },
  'codex-ring-end-kdy': {
    id:'codex-ring-end-kdy', title:`The Keel Goes to the Guild`, category:'story',
    summary:`What House Kuat made of the sale.`,
    body:[`With the schematics in KDY's vault, the Board has already moved the keel jig from a sealed cradle to an open bay and given the project a new name with no history. Lady Kuat watches from the gallery.`,
      `She believes a ship that cannot be unmade is a ship that someone must be trusted with, and she has stopped asking who.`] },
  'codex-ring-end-destroyed': {
    id:'codex-ring-end-destroyed', title:`A Pulse, Not an Explosion`, category:'story',
    summary:`What the ring made of the demolition.`,
    body:[`The demolition left nothing to salvage and nothing to argue about. The Guild issued a one line statement about a decommissioned test article.`,
      `The Senate Oversight Committee has not closed its file but has stopped opening it. Those who remember Cherit say she would have approved of the quiet, if not of the ending.`] },
  'codex-ring-end-syndicate': {
    id:'codex-ring-end-syndicate', title:`The Silent Flagship`, category:'story',
    summary:`What the ring pretends not to see.`,
    body:[`A pocket dreadnought now flies on paper under a Syndicate shell, fitted at cost in a yard that officially does not know it exists. The Families call it piracy. Lady Kuat calls it the only honest ownership the hull has had.`,
      `Every dock on the ring has begun to pretend not to see a ship that is not there.`] },
  // RING CODEX END
};

// ── Debug Stage-Jump Cheat Codes ─────────────────────────────────────────────
// Toggle the panel with backtick (`). Jump to a stage with 1–9.
// Shift+C injects 5000 credits. Shift+Z resets all flags.
const DEBUG_STAGES = [
  {
    key: '1', label: 'Fresh Start',
    zone: 'spaceport', pos: { x: 14, y: 10 }, credits: 340,
    flags: {},
  },
  {
    key: '2', label: 'CSF Training Hub',
    zone: 'csf_academy', pos: { x: 4, y: 8 }, credits: 500,
    flags: { republic_path_open: true, speeder_transit_unlocked: true },
  },
  {
    key: '3', label: 'CSF Training Complete',
    zone: 'sky_market', pos: { x: 4, y: 13 }, credits: 700,
    flags: { speeder_transit_unlocked: true, republic_path_open: true, csf_briefed: true, csf_duty_stance: true, csf_training_complete: true, module_a_complete: true, module_b_complete: true, module_c_complete: true, vane_record_commend: true, jaxxon_arrested: true, marlo_sky_talked: true },
  },
  {
    key: '4', label: 'Senate Arc Complete',
    zone: 'sky_market', pos: { x: 4, y: 13 }, credits: 900,
    flags: { speeder_transit_unlocked: true, republic_path_open: true, csf_briefed: true, csf_duty_stance: true, csf_training_complete: true, module_a_complete: true, module_b_complete: true, module_c_complete: true, jaxxon_arrested: true, marlo_sky_talked: true, credit_trail_found: true, senate_line_secured: true, vex_sabacc_offered: true, echo7_found: true, vane_record_commend: true },
  },
  {
    key: '5', label: 'Inheritance Arc — Level 1312',
    zone: 'shadow_town', pos: { x: 4, y: 12 }, credits: 1200,
    flags: { speeder_transit_unlocked: true, republic_path_open: true, csf_training_complete: true, module_a_complete: true, module_b_complete: true, module_c_complete: true, senate_line_secured: true, echo7_found: true, inheritance_active: true, jon_apartment_visited: true, marlo_sky_talked: true, credit_trail_found: true },
  },
  {
    key: '6', label: 'Malak Resolved',
    zone: 'shadow_town', pos: { x: 4, y: 12 }, credits: 1500,
    flags: { speeder_transit_unlocked: true, republic_path_open: true, csf_training_complete: true, senate_line_secured: true, echo7_found: true, inheritance_active: true, cargo_skimmed: true, malak_pit_challenged: true, malak_turned: true, inside_man_path: true },
  },
  {
    key: '7', label: 'Vault Approach Ready',
    zone: 'penthouse', pos: { x: 19, y: 22 }, credits: 2000,
    flags: { speeder_transit_unlocked: true, senate_line_secured: true, echo7_found: true, inheritance_active: true, cargo_skimmed: true, malak_turned: true, reya_loyal: true, echo7_vault_approach_active: true, vault_approach_active: true, csf_training_complete: true },
  },
  {
    key: '8', label: 'Vault Heist Complete',
    zone: 'penthouse', pos: { x: 19, y: 22 }, credits: 3000,
    flags: { speeder_transit_unlocked: true, senate_line_secured: true, echo7_found: true, inheritance_active: true, cargo_skimmed: true, malak_turned: true, reya_loyal: true, vault_heist_complete: true, rook_eliminated: true, inside_man_path: true, csf_training_complete: true },
  },
  {
    key: '9', label: 'Full Endgame — Syndicate Founded',
    zone: 'penthouse', pos: { x: 19, y: 22 }, credits: 8000,
    flags: { speeder_transit_unlocked: true, senate_line_secured: true, echo7_found: true, inheritance_active: true, cargo_skimmed: true, malak_turned: true, reya_loyal: true, vault_heist_complete: true, rook_eliminated: true, jon_confrontation_path_a_chosen: true, jon_status_dead: true, syndicateManagement_active: true, syndicate_1313_founded: true, trait_crime_lord: true, grix_recruited: true, vael_recruited: true, marro_recruited: true, anzati_contracted: true, black_sun_allied: true, sith_contact: true, csf_training_complete: true },
  },
];

const SPEEDER_DESTINATIONS = [
  { id: 'spaceport',         name: 'Sub-Surface Spaceport',       level: 'Sub-Surface L2',        cost: 0,   requiredFlag: null,                       targetZone: 'spaceport',         targetPos: { x: 14, y: 10 } },
  { id: 'market',            name: 'West Market District',         level: 'Sub-Surface L2',        cost: 0,   requiredFlag: null,                       targetZone: 'market',            targetPos: { x: 2,  y: 10 } },
  { id: 'sky_market',        name: 'Sky-Market District L.1450',   level: 'Upper Mid-Levels',      cost: 25,  requiredFlag: 'speeder_transit_unlocked', targetZone: 'sky_market',        targetPos: { x: 4,  y: 13 } },
  { id: 'freight_hub',       name: 'Sector 4 Freight Hub L.088',   level: 'Industrial Mid-Levels', cost: 50,  requiredFlag: 'speeder_transit_unlocked', targetZone: 'freight_hub',       targetPos: { x: 2,  y: 14 } },
  { id: 'the_works',         name: 'The Works L.005',              level: 'Undercity',             cost: 75,  requiredFlag: 'speeder_transit_unlocked', targetZone: 'the_works',         targetPos: { x: 2,  y: 12 } },
  { id: 'csf_academy',       name: 'CSF Training Hub L.1222',      level: 'Republic Mid-Levels',   cost: 0,   requiredFlag: 'republic_path_open',       targetZone: 'csf_academy',       targetPos: { x: 2,  y: 15 } },
  { id: 'lower_sky_market',  name: 'Lower Promenade L.1100',       level: 'Lower Mid-Levels',      cost: 0,   requiredFlag: 'marlo_sky_talked',         targetZone: 'lower_sky_market',  targetPos: { x: 2,  y: 13 } },
  { id: 'shadow_town',       name: 'Shadow Town L.1312',           level: 'Lower Mid-Levels',      cost: 30,  requiredFlag: 'inheritance_active',       targetZone: 'shadow_town',       targetPos: { x: 3,  y: 12 } },
  { id: 'senate_district',   name: 'Senate Precinct L.1900',       level: 'Upper Levels',          cost: 100, requiredFlag: 'rook_eliminated',          targetZone: 'senate_district',   targetPos: { x: 2,  y: 16 } },
  { id: 'slicer_alleyway',   name: 'Slicer Alleyway L.1150',       level: 'Lower Mid-Levels',      cost: 0,   requiredFlag: 'echo7_found',              targetZone: 'slicer_alleyway',   targetPos: { x: 2,  y: 10 } },
  { id: 'senatorial_lounges', name: 'Senatorial Sky-Lounges L.5100', level: 'Senate District',       cost: 150, requiredFlag: 'syndicateManagement_active', targetZone: 'senatorial_lounges', targetPos: { x: 19, y: 26 } },
  { id: 'undercity_outskirts', name: 'Undercity Outskirts',          level: 'Undercity',            cost: 100, requiredFlag: 'speeder_transit_unlocked',  targetZone: 'undercity_outskirts', targetPos: { x: 2,  y: 12 } },
];

function SpeederOverlay({ credits, questFlags, currentZoneId, onTravel, onClose }) {
  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.82)', zIndex: 40, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ background: '#0a1a2a', border: '2px solid #00e5ff', borderRadius: 8, padding: '28px 36px', minWidth: 420, maxWidth: 560, color: '#d0eaff', fontFamily: 'monospace' }}>
        <div style={{ color: '#00e5ff', fontWeight: 'bold', fontSize: 17, marginBottom: 6, letterSpacing: 2 }}>CORUSCANT AIRTAXI NETWORK</div>
        <div style={{ color: '#6fa8c0', fontSize: 12, marginBottom: 20 }}>Select destination. Credits on hand: {credits} CR</div>
        {SPEEDER_DESTINATIONS.filter(d => d.targetZone !== currentZoneId).map(dest => {
          const locked = dest.requiredFlag && !questFlags[dest.requiredFlag];
          const canAfford = credits >= dest.cost;
          const available = !locked && canAfford;
          return (
            <div key={dest.id} onClick={() => available && onTravel(dest)}
              style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 14px', marginBottom: 8, borderRadius: 5, cursor: available ? 'pointer' : 'not-allowed', background: available ? '#0d2233' : '#0a1218', border: `1px solid ${available ? '#00e5ff44' : '#222'}`, opacity: locked ? 0.45 : canAfford ? 1 : 0.6 }}>
              <div>
                <div style={{ color: locked ? '#556' : '#b8e0ff', fontWeight: 'bold', fontSize: 14 }}>{dest.name}</div>
                <div style={{ color: '#4a7a90', fontSize: 11 }}>{dest.level}</div>
              </div>
              <div style={{ textAlign: 'right', fontSize: 13 }}>
                {locked ? <span style={{ color: '#556' }}>CLEARANCE REQUIRED</span>
                  : dest.cost === 0 ? <span style={{ color: '#6fd9a0' }}>FREE</span>
                  : <span style={{ color: canAfford ? '#00e5ff' : '#e57' }}>{dest.cost} CR</span>}
              </div>
            </div>
          );
        })}
        <div onClick={onClose} style={{ marginTop: 18, textAlign: 'center', color: '#6fa8c0', fontSize: 12, cursor: 'pointer', letterSpacing: 1 }}>[ CLOSE TERMINAL ]</div>
      </div>
    </div>
  );
}

// ===== KDY SHUTTLE NETWORK BEGIN
const KDY_RING_ZONES = ['kdy_landing_bay', 'kuat_private_suites', 'kdy_briefing_area', 'kdy_lounge_market'];
const BADOR_SURFACE_ZONES = ['bador_main_base', 'bador_hq_interior', 'bador_manufacturing', 'city_outskirts_bador', 'engine_wastes', 'outpost_7', 'deep_caverns', 'zora_iv_wreckage'];

const KDY_SHUTTLE_DESTINATIONS = [
  { id: 'ring_landing', name: 'Orbital Ring Landing Bay', level: 'Kuat Orbital Array', cost: 0, requiredFlag: null,          targetZone: 'kdy_landing_bay',     targetPos: { x: 26, y: 15 } },
  { id: 'ring_suites',  name: 'Executive Private Suites', level: 'Kuat Orbital Array', cost: 0, requiredFlag: 'vaelen_cleared', targetZone: 'kuat_private_suites', targetPos: { x: 25, y: 14 } },
  { id: 'bador_base',   name: 'KDY Main Base',            level: 'Bador Moon, Sector Control', cost: 0, requiredFlag: 'tanner_briefed', targetZone: 'bador_main_base', targetPos: { x: 10, y: 14 } },
  { id: 'bador_fwd',    name: 'Outpost 7 Forward Pad',    level: 'Bador Moon, Eastern Reach',  cost: 0, requiredFlag: 'vael_briefed',   targetZone: 'outpost_7',       targetPos: { x: 26, y: 12 } },
];

const SHUTTLE_ARRIVAL_TEXT = 'The transition from the pristine, silent luxury of the Kuat Orbital Array to the surface of Bador is a jarring shock to the senses. As your shuttle breaches the thin, violet tinged atmosphere, the sheer scale of the industrial devastation becomes apparent. The KDY Main Base is a fortress carved directly into the glassed bedrock. The air outside the shuttle tastes of ozone and copper, forcing you to rely on the rebreather integrated into your collar. You are escorted through a series of heavy durasteel airlocks, moving from the blasted exterior into the sterile, blue lit tactical operations center. Holographic tactical maps flicker with red conflict zones.';

const SHUTTLE_SCRIPT = {
  descend: {
    dur: 5.0, swapAt: 0.9, from: 'KUAT ORBITAL ARRAY', to: 'BADOR MAIN BASE',
    beats: [
      [0.04, 0.30, 'Thin, violet tinged atmosphere. The scale of the industrial devastation becomes clear.'],
      [0.32, 0.58, 'The KDY Main Base: a fortress carved into glassed bedrock. Ozone and copper. Your collar rebreather seals.'],
      [0.60, 0.78, 'Heavy durasteel airlocks cycle, one after another.'],
      [0.80, 0.97, 'Blue lit operations center. Red conflict zones flicker across the holomaps.'],
    ],
  },
  ascend: {
    dur: 4.2, swapAt: 0.8, from: 'BADOR SURFACE', to: 'KUAT ORBITAL ARRAY',
    beats: [
      [0.04, 0.42, 'Seals confirmed. The shuttle lifts off the glassed bedrock in a spray of slag dust.'],
      [0.48, 0.94, 'Bador shrinks behind you. The Orbital Array waits in perfect silence.'],
    ],
  },
  airlock: {
    dur: 3.4, swapAt: 0.6, from: 'LANDING DECK PERIMETER', to: 'KUATI HQ GRAND VAULT HALL',
    beats: [
      [0.04, 0.3, 'Two parabolic airlock doors slide shut with a deep pneumatic thud. The golden wheel of Kuat is embossed across the seam.'],
      [0.32, 0.6, 'Crimson warning lights shift to sapphire as high pressure nozzles blow the lunar dust away.'],
      [0.64, 0.96, 'The inner doors glide open, silent, into the Grand Vault Hall.'],
    ],
  },
  airlock_out: {
    dur: 2.6, swapAt: 0.6, from: 'KUATI HQ GRAND VAULT HALL', to: 'LANDING DECK PERIMETER',
    beats: [
      [0.05, 0.5, 'Pressure equalizes. Sapphire lights fade to amber.'],
      [0.55, 0.96, 'The outer doors part on floodlights, dust and the low hum of the pads.'],
    ],
  },
  hop: {
    dur: 3.0, swapAt: 0.78, from: 'BADOR SURFACE', to: 'FORWARD PAD',
    beats: [[0.06, 0.92, 'Low pass over the scorched plains. Forward pad inbound.']],
  },
};

const shClamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
const shSmooth = (v) => { const c = shClamp(v); return c * c * (3 - 2 * c); };
const shRange = (p, a, b) => shSmooth((p - a) / (b - a));
const shLerp = (a, b, k) => a + (b - a) * k;
const SH_STARS = (() => { let a = 7; const r = () => (a = (a * 9301 + 49297) % 233280) / 233280; return Array.from({ length: 80 }, () => ({ x: r() * 800, y: r() * 330, r: 0.4 + r() * 1.1, d: r() * 6 })); })();
const SH_GLASS = (() => { let a = 19; const r = () => (a = (a * 9301 + 49297) % 233280) / 233280; return Array.from({ length: 22 }, () => ({ x: 60 + r() * 680, y: r() * 150, rx: 18 + r() * 46, ry: 4 + r() * 9, rot: -12 + r() * 24 })); })();

function shuttleModeFor(fromZoneId, dest) {
  const fromBador = BADOR_SURFACE_ZONES.includes(fromZoneId);
  const toBador = BADOR_SURFACE_ZONES.includes(dest.targetZone);
  if (!fromBador && toBador) return 'descend';
  if (fromBador && !toBador) return 'ascend';
  if (fromBador && toBador) return 'hop';
  return null;
}

function ShuttleOverlay({ pilot, questFlags, currentZoneId, onTravel, onClose }) {
  React.useEffect(() => {
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [onClose]);
  const A = '#1A8FD0';
  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.82)', zIndex: 40, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ background: '#08141F', border: `2px solid ${A}`, borderRadius: 8, padding: '28px 36px', minWidth: 420, maxWidth: 580, color: '#d0eaff', fontFamily: 'monospace' }}>
        <div style={{ color: '#7FD0FF', fontWeight: 'bold', fontSize: 17, marginBottom: 6, letterSpacing: 2 }}>KDY GUILD SHUTTLE SERVICE</div>
        <div style={{ color: '#6fa8c0', fontSize: 12, marginBottom: 6 }}>{pilot?.label || 'KDY Shuttle Pilot'}</div>
        <div style={{ color: '#4a7a90', fontSize: 11, marginBottom: 18, fontStyle: 'italic' }}>{pilot?.greeting || '"Shuttle K-14 is fueled and cleared. Where to?"'}</div>
        {KDY_SHUTTLE_DESTINATIONS.filter(d => d.targetZone !== currentZoneId).map(dest => {
          const locked = dest.requiredFlag && !questFlags[dest.requiredFlag];
          return (
            <div key={dest.id} onClick={() => !locked && onTravel(dest)}
              style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '10px 14px', marginBottom: 8, borderRadius: 5, cursor: locked ? 'not-allowed' : 'pointer', background: locked ? '#0a1218' : '#0d2438', border: `1px solid ${locked ? '#1c2a36' : A}`, opacity: locked ? 0.55 : 1 }}>
              <div>
                <div style={{ color: locked ? '#556' : '#b8e0ff', fontWeight: 'bold', fontSize: 14 }}>{dest.name}</div>
                <div style={{ color: '#4a7a90', fontSize: 11 }}>{dest.level}</div>
              </div>
              <div style={{ textAlign: 'right', fontSize: 13 }}>
                {locked ? <span style={{ color: '#556' }}>NO FLIGHT PLAN FILED</span> : <span style={{ color: '#6fd9a0' }}>GUILD PASSAGE</span>}
              </div>
            </div>
          );
        })}
        <div onClick={onClose} style={{ marginTop: 18, textAlign: 'center', color: '#6fa8c0', fontSize: 12, cursor: 'pointer', letterSpacing: 1 }}>[ STAND DOWN ]</div>
      </div>
    </div>
  );
}

function ShuttleScene({ mode, p, t }) {
  const descend = mode === 'descend', ascend = mode === 'ascend';
  const s = descend ? shSmooth(p / 0.78) : ascend ? 1 - shSmooth((p - 0.08) / 0.72) : 0.93;
  const topY = shLerp(190, 340, s);
  const R = 150 * (1 + 39 * s * s * s);
  const cx = shLerp(600, 400, s);
  const cy = topY + R;
  const k = shLerp(0.3, 1.05, shSmooth(s));
  const sx = mode === 'hop' ? shLerp(180, 560, shSmooth(p)) : shLerp(120, 470, 1 - Math.pow(1 - s, 3));
  const sy = mode === 'hop' ? 330 + Math.sin(t * 6) * 3 : shLerp(90, 366, Math.pow(s, 1.4));
  const pitch = shLerp(-9, 0, s);
  const gear = shRange(s, 0.82, 0.95) * 10;
  const down = shRange(s, 0.86, 0.97);
  const trailOp = descend ? Math.sin(shClamp((s - 0.12) / 0.4) * Math.PI) * 0.55 : ascend ? Math.sin(shClamp((s - 0.1) / 0.5) * Math.PI) * 0.4 : 0.15;
  const fort = shRange(s, 0.55, 0.9);
  const starOp = 1 - s * 0.85;
  const ringOp = shClamp(1 - s * 1.5);
  const dust = descend ? shRange(s, 0.94, 1) : ascend ? 1 - shRange(s, 0.0, 0.2) : 0;
  const scroll = mode === 'hop' ? -p * 500 : 0;
  return (
    <g>
      <defs>
        <linearGradient id="shAtm" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#140a2c" /><stop offset="0.55" stopColor="#3c2060" /><stop offset="1" stopColor="#7a4688" />
        </linearGradient>
        <radialGradient id="shMoon" cx="0.5" cy="0" r="1">
          <stop offset="0" stopColor="#4a4056" /><stop offset="1" stopColor="#1c1826" />
        </radialGradient>
        <linearGradient id="shTrail" x1="1" y1="0" x2="0" y2="0">
          <stop offset="0" stopColor="#FFC27A" stopOpacity="0.9" /><stop offset="1" stopColor="#FF7A3A" stopOpacity="0" />
        </linearGradient>
        <clipPath id="shMoonClip"><circle cx={cx} cy={cy} r={R} /></clipPath>
      </defs>
      <rect width="800" height="450" fill="#04060f" />
      <rect width="800" height="450" fill="url(#shAtm)" opacity={Math.pow(s, 1.1)} />
      {SH_STARS.map((st, i) => <circle key={i} cx={st.x} cy={st.y} r={st.r} fill="#E8F6FF" opacity={starOp * (0.55 + 0.45 * Math.sin(t * 4 + st.d))} />)}
      {/* Kuat Orbital Array receding */}
      <g opacity={ringOp} transform={`translate(0 ${-s * 240})`}>
        <ellipse cx="170" cy="70" rx="380" ry="54" fill="none" stroke="#5F6E80" strokeWidth="16" transform="rotate(-10 170 70)" />
        <ellipse cx="170" cy="70" rx="380" ry="54" fill="none" stroke="#9FAEBF" strokeWidth="3" transform="rotate(-10 170 70)" />
        <ellipse cx="170" cy="70" rx="380" ry="54" fill="none" stroke="#1A8FD0" strokeWidth="2" strokeDasharray="3 12" transform="rotate(-10 170 70)" />
        {[0, 1, 2, 3, 4].map((i) => <rect key={i} x={60 + i * 74} y={20 + Math.abs(i - 2) * 6} width="8" height="22" fill="#CFD6DE" opacity="0.7" />)}
      </g>
      {/* Bador */}
      <circle cx={cx} cy={cy} r={R} fill="url(#shMoon)" />
      <circle cx={cx} cy={cy} r={R + 3} fill="none" stroke="#B07CFF" strokeWidth="6" opacity={0.45 * (1 - s * 0.7)} />
      <g clipPath="url(#shMoonClip)">
        <g transform={`translate(${cx} ${topY}) scale(${1 + s * 3.2}) translate(${-cx} ${-topY}) translate(${scroll * 0.4} 0)`}>
          {SH_GLASS.map((g, i) => <ellipse key={i} cx={g.x} cy={topY + 8 + g.y} rx={g.rx} ry={g.ry} fill="#7C8AA8" opacity="0.32" transform={`rotate(${g.rot} ${g.x} ${topY + 8 + g.y})`} />)}
          {[0, 1, 2, 3, 4, 5].map((i) => <circle key={i} cx={120 + i * 118} cy={topY + 30 + (i % 3) * 22} r={2.2 + (i % 2)} fill="#FF4040" opacity={0.35 + 0.5 * Math.abs(Math.sin(t * 3 + i))} />)}
        </g>
      </g>
      {/* ridges and fortress */}
      <g opacity={shRange(s, 0.5, 0.8)}>
        <polygon points={`0,${topY + 8} 90,${topY - 22} 190,${topY - 6} 300,${topY - 34} 380,${topY - 10} 800,${topY - 4} 800,450 0,450`} fill="#221f2e" transform={`translate(${scroll * 0.6} 0)`} />
        <polygon points={`0,${topY + 22} 120,${topY + 6} 260,${topY + 18} 420,${topY + 4} 800,${topY + 14} 800,450 0,450`} fill="#16141f" transform={`translate(${scroll} 0)`} />
      </g>
      <g opacity={fort} transform={`translate(${scroll * 0.8} 0)`}>
        <polygon points="470,352 500,320 560,308 640,300 700,312 760,330 790,352" fill="#262836" stroke="#4A7A9B" strokeWidth="1.5" />
        <polygon points="500,352 520,330 560,322 600,324 600,352" fill="#1b1d2a" />
        {Array.from({ length: 9 }, (_, i) => <rect key={i} x={512 + i * 28} y={334} width="12" height="4" fill="#7FD0FF" opacity={0.55 + 0.35 * Math.sin(t * 3 + i)} />)}
        <rect x="618" y="324" width="44" height="28" fill="#0c3a5e" /><rect x="620" y="326" width="40" height="24" fill="#2A8ACF" opacity="0.55" />
        <rect x="636" y="284" width="3" height="18" fill="#9FAEBF" /><circle cx="637.5" cy="283" r="2.4" fill="#FF4040" opacity={Math.sin(t * 7) > 0 ? 1 : 0.2} />
      </g>
      {/* landing pad */}
      <g opacity={shRange(s, 0.7, 0.92) * (mode === 'hop' ? 0 : 1)}>
        <ellipse cx="470" cy="388" rx="92" ry="9" fill="#0c0d14" /><ellipse cx="470" cy="388" rx="92" ry="9" fill="none" stroke="#E8A030" strokeWidth="1.5" strokeDasharray="8 6" />
        {[-80, -40, 0, 40, 80].map((dx) => <circle key={dx} cx={470 + dx} cy={388} r="2" fill="#FFB844" opacity={0.5 + 0.5 * Math.abs(Math.sin(t * 5 + dx))} />)}
      </g>
      {/* re-entry trail */}
      <polygon points={`${sx - 70 * k},${sy - 6 * k} ${sx - 330 * k - 30},${sy + 1} ${sx - 70 * k},${sy + 10 * k}`} fill="url(#shTrail)" opacity={trailOp} />
      {/* shuttle */}
      <g transform={`translate(${sx} ${sy}) rotate(${pitch}) scale(${k})`}>
        <polygon points={`-64,0 ${-64 - (10 + 46 * (1 - s))},3 -64,6`} fill="#7FD0FF" opacity={0.85} />
        <polygon points={`-64,-2 ${-64 - (22 + 80 * (1 - s))},3 -64,8`} fill="#BFF4FF" opacity={0.4} />
        <polygon points="-42,-8 -54,-30 -34,-30 -16,-11" fill="#8E9AA8" />
        <polygon points="-54,-30 -34,-30 -35,-26 -52,-26" fill="#1A8FD0" />
        <polygon points="-58,2 -44,-6 -12,-13 30,-13 50,-6 62,2 52,11 10,14 -30,13" fill="#CBD3DC" />
        <polygon points="-58,2 -44,-6 -12,-13 30,-13 50,-6 62,2 -58,2" fill="#F4F6F9" opacity="0.5" />
        <rect x="-52" y="1" width="106" height="3.4" fill="#1A8FD0" /><rect x="-52" y="1" width="106" height="0.9" fill="#BFF4FF" opacity="0.7" />
        <polygon points="28,-13 46,-6 56,1 30,0" fill="#1D4F82" /><polygon points="32,-11 44,-6 50,-2 33,-3" fill="#C8E6F4" opacity="0.6" />
        <rect x="-68" y="-5" width="14" height="14" fill="#27313C" /><rect x="-68" y="-5" width="14" height="3" fill="#4A5562" />
        <circle cx="-68" cy="2" r="5" fill="#7FD0FF" opacity="0.85" />
        <polygon points="-26,-13 -8,-13 -8,-18 -26,-18" fill="#8E9AA8" />
        <circle cx="52" cy="3" r="1.6" fill="#FFFFFF" /><circle cx="-30" cy="-30" r="1.4" fill="#FF4040" opacity={Math.sin(t * 8) > 0 ? 1 : 0.2} />
        {gear > 0 && <g stroke="#5F6E80" strokeWidth="2.4"><line x1="-24" y1="12" x2="-24" y2={12 + gear} /><line x1="32" y1="12" x2="32" y2={12 + gear} /><line x1="-30" y1={12 + gear} x2="-18" y2={12 + gear} /><line x1="26" y1={12 + gear} x2="38" y2={12 + gear} /></g>}
        {down > 0 && <g opacity={down}><polygon points="-30,14 -22,14 -26,38" fill="#BFF4FF" /><polygon points="26,14 34,14 30,38" fill="#BFF4FF" /></g>}
      </g>
      {/* landing dust */}
      <g opacity={dust * 0.8}>
        {[0, 1, 2, 3, 4].map((i) => <ellipse key={i} cx={470 + (i - 2) * 46 * (0.6 + dust)} cy={396 - i % 2 * 5} rx={24 + dust * 34} ry={5 + dust * 7} fill="#8A7E92" opacity={0.5 - i * 0.05} />)}
      </g>
    </g>
  );
}

function AirlockScene({ p, t, out }) {
  const close = out ? 1 - shRange(p, 0.5, 0.9) : shRange(p, 0.04, 0.3);
  const deco = shRange(p, 0.3, 0.6) * (1 - shRange(p, 0.62, 0.7));
  const open = out ? shRange(p, 0.05, 0.4) : shRange(p, 0.62, 0.9);
  const sapphire = out ? 1 - shRange(p, 0.1, 0.5) : shRange(p, 0.32, 0.5);
  const w = 130 + close * 270;
  const iw = (1 - open) * 400;
  const leaf = (side) => (
    <path d={side ? `M0 0 L${w} 0 Q${w + 46} 225 ${w} 450 L0 450 Z` : `M800 0 L${800 - w} 0 Q${800 - w - 46} 225 ${800 - w} 450 L800 450 Z`} fill="#E8ECEF" stroke="#D4AF37" strokeWidth="3" />
  );
  return (
    <g>
      <rect width="800" height="450" fill={out ? '#07080d' : '#0b1c30'} />
      {out && (
        <g>
          <polygon points="300,0 500,0 640,450 160,450" fill="#FFE9B0" opacity="0.14" />
          {Array.from({ length: 22 }, (_, i) => <circle key={i} cx={(i * 97 + t * 40) % 800} cy={(i * 53) % 450} r={1.4 + (i % 3)} fill="#FF9646" opacity="0.5" />)}
        </g>
      )}
      {!out && (
        <g>
          <rect y="300" width="800" height="150" fill="#E8ECEF" opacity="0.9" />
          {[160, 400, 640].map((x) => <rect key={x} x={x - 3} y="300" width="6" height="150" fill="#D4AF37" opacity="0.7" />)}
          {[100, 300, 500, 700].map((x) => <rect key={x} x={x - 22} y="40" width="44" height="240" fill="#1C2A39" stroke="#D4AF37" strokeWidth="2" />)}
          <rect y="0" width="800" height="14" fill="#2A6CFF" opacity="0.8" />
        </g>
      )}
      <rect width="800" height="450" fill="#FF2A3A" opacity={(1 - sapphire) * 0.18 * (0.6 + 0.4 * Math.sin(t * 9))} />
      <rect width="800" height="450" fill="#2A6CFF" opacity={sapphire * 0.2} />
      {/* inner doors */}
      <g opacity={out ? 1 : shRange(p, 0.3, 0.4)}>
        <rect x="0" y="0" width={iw} height="450" fill="#4E5A6A" stroke="#9FAEBF" strokeWidth="3" />
        <rect x={800 - iw} y="0" width={iw} height="450" fill="#4E5A6A" stroke="#9FAEBF" strokeWidth="3" />
        {[0, 1, 2, 3].map((i) => <rect key={i} x="0" y={60 + i * 90} width="800" height="6" fill="#E8A030" opacity="0.5" />)}
      </g>
      {/* decontamination nozzles */}
      <g opacity={deco}>
        {Array.from({ length: 8 }, (_, i) => <g key={i}><rect x={70 + i * 90} y="0" width="16" height="18" fill="#9FAEBF" /><polygon points={`${78 + i * 90},18 ${58 + i * 90},${120 + (i % 2) * 60} ${98 + i * 90},${120 + (i % 2) * 60}`} fill="#EAF6FF" opacity={0.2 + 0.2 * Math.abs(Math.sin(t * 10 + i))} /></g>)}
      </g>
      {/* outer doors, parabolic with the wheel of Kuat on the seam */}
      <g opacity={out ? 0 : 1 - shRange(p, 0.3, 0.38)}>
        {leaf(true)}{leaf(false)}
        <g opacity={shRange(close, 0.7, 1)}>
          <circle cx="400" cy="225" r="46" fill="none" stroke="#D4AF37" strokeWidth="6" />
          <circle cx="400" cy="225" r="9" fill="#D4AF37" />
          {Array.from({ length: 8 }, (_, i) => <line key={i} x1="400" y1="225" x2={400 + Math.cos(i * Math.PI / 4) * 46} y2={225 + Math.sin(i * Math.PI / 4) * 46} stroke="#D4AF37" strokeWidth="3" />)}
        </g>
      </g>
    </g>
  );
}

function ShuttleInterior({ p, t }) {
  const stageB = shRange(p, 0.86, 0.91);
  const gates = [0.775, 0.81, 0.845];
  return (
    <g>
      <rect width="800" height="450" fill="#07090e" />
      {gates.map((g, i) => {
        const w = [620, 430, 280][i], h = [380, 262, 170][i];
        const open = shRange(p, g, g + 0.05);
        const x = 400 - w / 2, y = 225 - h / 2;
        return (
          <g key={i}>
            <rect x={x} y={y} width={w} height={h} fill="#10253a" opacity={0.4 + i * 0.2} />
            <rect x={x} y={y} width={w / 2 - open * (w / 2 - 8)} height={h} fill="#4E5A6A" stroke="#9FAEBF" strokeWidth="2" />
            <rect x={400 + open * (w / 2 - 8)} y={y} width={w / 2 - open * (w / 2 - 8)} height={h} fill="#4E5A6A" stroke="#9FAEBF" strokeWidth="2" />
            <rect x={x} y={y} width={w} height="8" fill="#E8A030" opacity="0.8" /><rect x={x} y={y + h - 8} width={w} height="8" fill="#E8A030" opacity="0.8" />
            <circle cx={x + 14} cy={y + 24} r="4" fill={open > 0.5 ? '#3AFF8A' : '#FF4040'} />
          </g>
        );
      })}
      <g opacity={stageB}>
        <rect width="800" height="450" fill="#06121e" />
        <polygon points="0,450 800,450 800,330 0,330" fill="#0b2236" />
        {[100, 250, 400, 550, 700].map((x) => <rect key={x} x={x - 6} y="0" width="12" height="330" fill="#123653" opacity="0.7" />)}
        <ellipse cx="400" cy="372" rx="190" ry="30" fill="#0c3a5e" /><ellipse cx="400" cy="366" rx="170" ry="24" fill="#2A8ACF" opacity="0.35" />
        <polygon points="260,360 540,360 500,200 300,200" fill="#7FD0FF" opacity="0.1" />
        <ellipse cx="400" cy="270" rx="120" ry="44" fill="none" stroke="#7FD0FF" strokeWidth="2" opacity="0.8" />
        {[0, 1, 2, 3, 4, 5].map((i) => <line key={i} x1={300 + i * 40} y1="236" x2={300 + i * 40} y2="304" stroke="#7FD0FF" strokeWidth="0.8" opacity="0.5" />)}
        {[0, 1, 2].map((i) => <line key={i} x1="285" y1={250 + i * 22} x2="515" y2={250 + i * 22} stroke="#7FD0FF" strokeWidth="0.8" opacity="0.5" />)}
        {[[350, 262, 14], [440, 282, 18], [470, 246, 10]].map(([x, y, r], i) => <circle key={i} cx={x} cy={y} r={r + Math.sin(t * 6 + i) * 3} fill="#FF4040" opacity={0.3 + 0.3 * Math.abs(Math.sin(t * 4 + i))} stroke="#FF4040" strokeWidth="1.5" />)}
        <polygon points="120,360 150,250 170,250 200,360" fill="#0a1a2a" /><circle cx="160" cy="236" r="12" fill="#0a1a2a" />
        <polygon points="600,360 630,250 650,250 680,360" fill="#0a1a2a" /><circle cx="640" cy="236" r="12" fill="#0a1a2a" />
      </g>
    </g>
  );
}

function ShuttleCinematic({ mode, destName, onSwap, onDone }) {
  const script = SHUTTLE_SCRIPT[mode] || SHUTTLE_SCRIPT.hop;
  const [t, setT] = React.useState(0);
  const swapped = React.useRef(false);
  const finished = React.useRef(false);
  const cb = React.useRef({ onSwap, onDone });
  cb.current = { onSwap, onDone };
  const finish = React.useCallback(() => {
    if (finished.current) return;
    finished.current = true;
    if (!swapped.current) { swapped.current = true; cb.current.onSwap(); }
    cb.current.onDone();
  }, []);
  React.useEffect(() => {
    let raf = 0, start = null;
    const loop = (now) => {
      if (start === null) start = now;
      const el = (now - start) / 1000;
      if (el / script.dur >= script.swapAt && !swapped.current) { swapped.current = true; cb.current.onSwap(); }
      if (el >= script.dur) { setT(script.dur); finish(); return; }
      setT(el);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    const onKey = (e) => { if (e.key === 'Escape' || e.key === ' ' || e.key === 'Enter') { e.preventDefault(); finish(); } };
    window.addEventListener('keydown', onKey);
    return () => { cancelAnimationFrame(raf); window.removeEventListener('keydown', onKey); };
  }, []);
  const p = shClamp(t / script.dur);
  const fade = Math.min(1, t / 0.2) * (p > 0.93 ? shClamp((1 - p) / 0.07) : 1);
  const beat = script.beats.find(([a, b]) => p >= a && p <= b);
  let caption = '';
  let capOp = 0;
  if (beat) {
    const [a, b, text] = beat;
    caption = text.slice(0, Math.ceil(text.length * shClamp((p - a) / ((b - a) * 0.65))));
    capOp = Math.min(1, (p - a) / 0.03, (b - p) / 0.03);
  }
  const s = mode === 'descend' ? shSmooth(p / 0.78) : mode === 'ascend' ? 1 - shSmooth((p - 0.08) / 0.72) : 0.93;
  const alt = Math.round(shLerp(412, 0, s));
  const spd = Math.round(shLerp(7400, 0, Math.pow(s, 0.6)));
  const interior = mode === 'descend' ? shRange(p, 0.76, 0.8) : 0;
  const to = mode === 'hop' ? (destName || script.to).toUpperCase() : script.to;
  const mono = { fontFamily: "'IBM Plex Mono',monospace" };
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 60, background: '#000', opacity: fade, overflow: 'hidden' }} onClick={finish}>
      <svg viewBox="0 0 800 450" preserveAspectRatio="xMidYMid slice" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%' }}>
        {mode === 'airlock' || mode === 'airlock_out' ? <AirlockScene p={p} t={t} out={mode === 'airlock_out'} /> : <ShuttleScene mode={mode} p={p} t={t} />}
        {interior > 0 && <g opacity={interior}><ShuttleInterior p={p} t={t} /></g>}
        <rect width="800" height="450" fill="#000" opacity="0.18" />
        {Array.from({ length: 45 }, (_, i) => <rect key={i} x="0" y={i * 10} width="800" height="1" fill="#000" opacity="0.18" />)}
      </svg>
      <div style={{ position: 'absolute', left: 24, top: 18, color: '#7FD0FF', fontSize: 12, letterSpacing: '0.18em', ...mono }}>
        <div style={{ color: '#1A8FD0', fontWeight: 700 }}>{mode.startsWith('airlock') ? 'KUATI HQ PRIMARY AIRLOCK' : 'KDY SHUTTLE K-14'}</div>
        <div>{script.from} TO {to}</div>
      </div>
      {mode !== 'hop' && !mode.startsWith('airlock') && interior < 0.5 && (
        <div style={{ position: 'absolute', right: 24, top: 18, textAlign: 'right', color: '#7FD0FF', fontSize: 11, letterSpacing: '0.14em', lineHeight: 1.7, ...mono }}>
          <div>ALT {String(alt).padStart(3, '0')} KM</div>
          <div>VEL {String(spd).padStart(4, '0')} M/S</div>
          <div style={{ color: s > 0.3 && s < 0.8 ? '#FFB844' : '#7FD0FF' }}>HULL {s > 0.3 && s < 0.8 ? 'THERMAL LOAD' : 'NOMINAL'}</div>
        </div>
      )}
      <div style={{ position: 'absolute', left: '50%', bottom: 56, transform: 'translateX(-50%)', width: 'min(760px, 88vw)', textAlign: 'center', opacity: capOp, color: '#EAF6FF', fontSize: 15, lineHeight: 1.55, letterSpacing: '0.02em', textShadow: '0 0 8px #000, 0 0 18px #000', minHeight: 48, ...mono }}>{caption}</div>
      <div style={{ position: 'absolute', left: 0, bottom: 0, height: 3, width: `${p * 100}%`, background: '#1A8FD0' }} />
      <div style={{ position: 'absolute', right: 18, bottom: 12, color: '#4a7a90', fontSize: 10, letterSpacing: '0.12em', ...mono }}>SPACE TO SKIP</div>
    </div>
  );
}
// ===== KDY SHUTTLE NETWORK END

function InventoryOverlay({ inventory, onClose, onStorySlotChange }) {
  const [selectedItem, setSelectedItem] = React.useState(null);
  const [activeCategory, setActiveCategory] = React.useState('all');
  const [storySlots, setStorySlots] = React.useState([null, null]);

  React.useEffect(() => {
    const handler = (e) => { if (e.key === 'i' || e.key === 'I' || e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [onClose]);

  const CATEGORIES = ['all', 'gear', 'quest', 'contraband', 'tool', 'consumable'];
  const filtered = activeCategory === 'all' ? inventory : inventory.filter(it => it.type === activeCategory);
  const typeColor = { quest:'#E8C97A', tool:'#4ACDFF', gear:'#8FA6FF', contraband:'#FF5555', consumable:'#6FD9A0' };
  const iconChar = { datapad:'≡', keycard:'◈', tool:'⚙', supply:'◆', gear:'▣', artifact:'◉' };

  const equipToSlot = (si) => {
    if (!selectedItem) return;
    const ns = [...storySlots]; ns[si] = selectedItem.id; setStorySlots(ns);
    onStorySlotChange && onStorySlotChange(ns);
  };

  return (
    <div style={{ position:'absolute',inset:0,background:'rgba(0,0,0,0.88)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:28 }}>
      <div style={{ width:'min(96%,740px)',border:'1px solid #2A2A38',background:'#0A0A12',fontFamily:"'IBM Plex Mono',ui-monospace,monospace",maxHeight:'90vh',display:'flex',flexDirection:'column' }}>
        <div style={{ padding:'12px 18px',borderBottom:'1px solid #2A2A38',color:'#4ACDFF',fontSize:12,fontWeight:600,letterSpacing:'0.15em' }}>HOLONET INVENTORY SYSTEM</div>
        <div style={{ display:'flex',flex:1,overflow:'hidden' }}>
          <div style={{ width:320,borderRight:'1px solid #1C1C26',padding:12,display:'flex',flexDirection:'column',gap:8 }}>
            <div style={{ fontSize:9,color:'#5A5F74' }}>ACTIVE STORY SLOTS</div>
            <div style={{ display:'flex',gap:6,marginBottom:4 }}>
              {[0,1].map(si => {
                const slotItem = storySlots[si] ? inventory.find(it => it.id === storySlots[si]) : null;
                return (
                  <div key={si} onClick={() => equipToSlot(si)}
                    style={{ flex:1,height:48,border:`1px solid ${selectedItem?'#4ACDFF88':'#2A2A38'}`,background:'#0E0E1A',display:'flex',alignItems:'center',justifyContent:'center',cursor:selectedItem?'pointer':'default',fontSize:10,color:'#5A5F74',borderRadius:2 }}>
                    {slotItem ? <span style={{color:typeColor[slotItem.type]||'#A8ADC0'}}>{iconChar[slotItem.iconKind]||'?'} {slotItem.name.slice(0,14)}</span> : <span>SLOT {si+1}</span>}
                  </div>
                );
              })}
            </div>
            <div style={{ display:'flex',flexWrap:'wrap',gap:4,marginBottom:4 }}>
              {CATEGORIES.map(cat => (
                <div key={cat} onClick={() => setActiveCategory(cat)}
                  style={{ padding:'2px 8px',fontSize:9,border:`1px solid ${activeCategory===cat?'#4ACDFF':'#2A2A38'}`,color:activeCategory===cat?'#4ACDFF':'#5A5F74',cursor:'pointer',textTransform:'uppercase' }}>{cat}</div>
              ))}
            </div>
            <div style={{ display:'grid',gridTemplateColumns:'repeat(4,1fr)',gap:4,overflowY:'auto' }}>
              {filtered.map(item => (
                <div key={item.id} onClick={() => setSelectedItem(item)}
                  style={{ aspectRatio:'1',border:`1px solid ${selectedItem?.id===item.id?typeColor[item.type]||'#A8ADC0':'#2A2A38'}`,background:selectedItem?.id===item.id?'#141420':'#0E0E18',display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',cursor:'pointer',position:'relative',padding:2 }}>
                  <div style={{ fontSize:16,color:typeColor[item.type]||'#A8ADC0' }}>{iconChar[item.iconKind]||'?'}</div>
                  {item.qty > 1 && <div style={{ position:'absolute',bottom:2,right:4,fontSize:8,color:'#E8C97A' }}>x{item.qty}</div>}
                </div>
              ))}
            </div>
          </div>
          <div style={{ flex:1,padding:16,display:'flex',flexDirection:'column',gap:10 }}>
            {selectedItem ? (
              <>
                <div style={{ color:typeColor[selectedItem.type]||'#A8ADC0',fontSize:14,fontWeight:600 }}>{selectedItem.name}</div>
                <div style={{ display:'flex',gap:8,alignItems:'center' }}>
                  <span style={{ padding:'2px 8px',background:typeColor[selectedItem.type]||'#333',color:'#0A0A12',fontSize:9,fontWeight:700,textTransform:'uppercase',borderRadius:2 }}>{selectedItem.type}</span>
                  {selectedItem.value > 0 && <span style={{ fontSize:10,color:'#E8C97A' }}>{selectedItem.value} cr</span>}
                  {selectedItem.qty > 1 && <span style={{ fontSize:10,color:'#6A6F84' }}>qty: {selectedItem.qty}</span>}
                </div>
                <div style={{ fontSize:12,color:'#A8ADC0',lineHeight:1.7,borderTop:'1px solid #1C1C26',paddingTop:10 }}>{selectedItem.description}</div>
                <div style={{ marginTop:'auto',fontSize:10,color:'#4ACDFF88' }}>Click a story slot above to equip.</div>
              </>
            ) : (
              <div style={{ color:'#5A5F74',fontSize:11,marginTop:40,textAlign:'center' }}>Select an item to view details.</div>
            )}
          </div>
        </div>
        <div style={{ padding:'8px 18px',borderTop:'1px solid #1C1C26',fontSize:9,color:'#5A5F74' }}>[I] or [ESC] to close</div>
      </div>
    </div>
  );
}

function CodexOverlay({ codex, setCodex, onClose }) {
  const [activeTab, setActiveTab] = React.useState('story');
  const [selectedEntry, setSelectedEntry] = React.useState(null);

  React.useEffect(() => {
    const handler = (e) => {
      if (e.key === 'c' || e.key === 'C' || e.key === 'Escape') {
        setCodex(prev => prev.map(en => ({ ...en, unread: false })));
        onClose();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [onClose, setCodex]);

  const TABS = ['story', 'discoveries', 'lore', 'dossier', 'factions'];
  const tabColor = { story:'#E8C97A', discoveries:'#8FA6FF', lore:'#6FD9A0', dossier:'#B896FF', factions:'#FF8C42' };
  const filtered = codex.filter(en => en.category === activeTab);

  return (
    <div style={{ position:'absolute',inset:0,background:'rgba(0,0,0,0.88)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:28 }}>
      <div style={{ width:'min(96%,740px)',border:'1px solid #2A2A38',background:'#0A0A12',fontFamily:"'IBM Plex Mono',ui-monospace,monospace",maxHeight:'90vh',display:'flex',flexDirection:'column' }}>
        <div style={{ padding:'12px 18px',borderBottom:'1px solid #2A2A38',color:'#E8C97A',fontSize:12,fontWeight:600,letterSpacing:'0.1em' }}>HOLONET DATAPAD — DECLASSIFIED ARCHIVES</div>
        <div style={{ display:'flex',flex:1,overflow:'hidden' }}>
          <div style={{ width:240,borderRight:'1px solid #1C1C26',display:'flex',flexDirection:'column' }}>
            <div style={{ display:'flex',borderBottom:'1px solid #1C1C26' }}>
              {TABS.map(tab => (
                <div key={tab} onClick={() => { setActiveTab(tab); setSelectedEntry(null); }}
                  style={{ flex:1,padding:'8px 4px',textAlign:'center',fontSize:9,textTransform:'uppercase',cursor:'pointer',color:activeTab===tab?tabColor[tab]:'#5A5F74',borderBottom:activeTab===tab?`2px solid ${tabColor[tab]}`:'2px solid transparent' }}>{tab}</div>
              ))}
            </div>
            <div style={{ flex:1,overflowY:'auto',padding:6 }}>
              {filtered.length === 0 && <div style={{ color:'#3A3F54',fontSize:10,padding:8 }}>No entries.</div>}
              {filtered.map(entry => (
                <div key={entry.id} onClick={() => setSelectedEntry(entry)}
                  style={{ padding:'8px 10px',borderBottom:'1px solid #14141E',cursor:'pointer',background:selectedEntry?.id===entry.id?'#141420':'transparent',display:'flex',alignItems:'center',gap:6 }}>
                  {entry.unread && <div style={{ width:6,height:6,borderRadius:'50%',background:'#E8C97A',flexShrink:0 }} />}
                  <div>
                    <div style={{ fontSize:11,color:selectedEntry?.id===entry.id?tabColor[entry.category]:'#A8ADC0' }}>{entry.title}</div>
                    <div style={{ fontSize:9,color:'#5A5F74',marginTop:2 }}>{entry.summary}</div>
                  </div>
                </div>
              ))}
            </div>
          </div>
          <div style={{ flex:1,padding:16,overflowY:'auto' }}>
            {selectedEntry ? (
              <>
                <div style={{ color:tabColor[selectedEntry.category],fontSize:14,fontWeight:600,marginBottom:6 }}>{selectedEntry.title}</div>
                <div style={{ display:'inline-block',padding:'2px 8px',background:tabColor[selectedEntry.category]+'22',border:`1px solid ${tabColor[selectedEntry.category]}44`,color:tabColor[selectedEntry.category],fontSize:9,textTransform:'uppercase',marginBottom:12,borderRadius:2 }}>{selectedEntry.category}</div>
                {selectedEntry.body.map((para, i) => (
                  <div key={i} style={{ fontSize:12,color:'#A8ADC0',lineHeight:1.8,marginBottom:12 }}>{para}</div>
                ))}
              </>
            ) : (
              <div style={{ color:'#5A5F74',fontSize:11,marginTop:40,textAlign:'center' }}>Select an entry to read.</div>
            )}
          </div>
        </div>
        <div style={{ padding:'8px 18px',borderTop:'1px solid #1C1C26',fontSize:9,color:'#5A5F74' }}>[C] or [ESC] to close</div>
      </div>
    </div>
  );
}

function getWorldObjIconKind(id) {
  if (!id) return 'terminal';
  const s = id.toLowerCase();
  if (s.includes('terminal') || s.includes('comms') || s.includes('datacore') || s.includes('datapad')) return 'terminal';
  if (s.includes('cache') || s.includes('crate') || s.includes('cargo') || s.includes('storage')) return 'crate';
  if (s.includes('booth') || s.includes('lounge')) return 'booth';
  if (s.includes('evidence') || s.includes('crime') || s.includes('blast')) return 'evidence';
  if (s.includes('vista') || s.includes('overlook') || s.includes('window')) return 'vista';
  if (s.includes('valve') || s.includes('coupling') || s.includes('smelter')) return 'valve';
  if (s.includes('panel') || s.includes('console') || s.includes('module')) return 'panel';
  if (s.includes('comlink') || s.includes('radio') || s.includes('relay')) return 'comlink';
  if (s.includes('locker') || s.includes('safe')) return 'safe';
  if (s.includes('board') || s.includes('chart') || s.includes('wanted') || s.includes('log')) return 'board';
  if (s.includes('holo') || s.includes('kiosk') || s.includes('billboard')) return 'hologram';
  if (s.includes('airtaxi') || s.includes('beacon')) return 'beacon';
  if (s.includes('stall') || s.includes('vendor') || s.includes('exchange')) return 'stall';
  if (s.includes('barrel') || s.includes('drum')) return 'barrel';
  if (s.includes('lamp') || s.includes('lantern')) return 'lamp';
  return 'terminal';
}

const WORLD_OBJECT_SPRITES = {
  customs_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="6" y="3" width="16" height="18" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="7.5" y="4.5" width="13" height="12" fill={a} opacity="0.12"/>
      <line x1="8" y1="7" x2="20" y2="7" stroke={a} strokeWidth="0.6" opacity="0.5"/>
      <line x1="8" y1="9.5" x2="20" y2="9.5" stroke={a} strokeWidth="0.6" opacity="0.4"/>
      <line x1="8" y1="12" x2="20" y2="12" stroke={a} strokeWidth="0.6" opacity="0.3"/>
      <rect x="8" y="14" width="12" height="3.5" fill="#E8A030" opacity="0.85" rx="0.5"/>
      <rect x="9" y="14.7" width="3" height="0.7" fill="#000" opacity="0.5"/>
      <rect x="13" y="14.7" width="4" height="0.7" fill="#000" opacity="0.5"/>
      <rect x="9" y="16.2" width="5" height="0.7" fill="#000" opacity="0.5"/>
      <circle cx="19.5" cy="4.5" r="1" fill="#E8A030" opacity="0.9" style={{animation:'lens-flicker 1.8s ease-in-out infinite'}}/>
      <rect x="9" y="22" width="10" height="2" rx="1" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.6"/>
    </svg>
  ),
  fueling_conduit: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="11" width="8" height="6" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="10" y="13" width="8" height="3" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="18" y="8" width="8" height="6" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <circle cx="12" cy="14" r="2.5" fill="none" stroke={a} strokeWidth="1" opacity="0.6"/>
      <line x1="10.5" y1="14" x2="13.5" y2="14" stroke={a} strokeWidth="0.8" opacity="0.5"/>
      <line x1="12" y1="12.5" x2="12" y2="15.5" stroke={a} strokeWidth="0.8" opacity="0.5"/>
      <line x1="6" y1="18" x2="7.5" y2="21" stroke={a} strokeWidth="0.7" opacity="0.4"/>
      <line x1="8" y1="18" x2="9.5" y2="21" stroke={a} strokeWidth="0.7" opacity="0.4"/>
      <circle cx="22" cy="11" r="3.5" fill={a} opacity="0.07"/>
    </svg>
  ),
  jon_datapad: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="5" y="3" width="18" height="22" rx="2" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="7" y="5" width="14" height="16" fill={a} opacity="0.1"/>
      <line x1="7.5" y1="8" x2="20.5" y2="8" stroke={a} strokeWidth="0.7" opacity="0.5"/>
      <line x1="7.5" y1="11" x2="20.5" y2="11" stroke={a} strokeWidth="0.7" opacity="0.4"/>
      <line x1="7.5" y1="14" x2="20.5" y2="14" stroke="#FF4444" strokeWidth="0.9" opacity="0.8"/>
      <line x1="7.5" y1="17" x2="16" y2="17" stroke={a} strokeWidth="0.7" opacity="0.3"/>
      <circle cx="9" cy="22" r="1" fill={a} opacity="0.5"/>
      <path d="M19 5.5 L21 5.5 L21 7.5 L19.5 7.5" stroke={a} strokeWidth="0.5" fill="none" opacity="0.4"/>
    </svg>
  ),
  slicing_bench: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="14" width="24" height="10" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="4" y="11" width="5" height="4" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <rect x="11" y="10" width="4" height="4" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <rect x="17" y="12" width="6" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <path d="M7 18 Q9 16 11 18 Q13 20 15 17" stroke={a} strokeWidth="0.8" fill="none" opacity="0.6"/>
      <circle cx="19" cy="16" r="1.2" fill={a} opacity="0.7" style={{animation:'lens-flicker 2.1s ease-in-out infinite'}}/>
      <line x1="19" y1="13" x2="19" y2="14.8" stroke={a} strokeWidth="0.6" opacity="0.5"/>
    </svg>
  ),
  bay14_analysis_board: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="22" rx="1" fill="#5A4020" opacity="0.7"/>
      <rect x="2" y="2" width="24" height="22" rx="1" fill="none" stroke={a} strokeWidth="0.7" opacity="0.6"/>
      <rect x="4" y="4" width="8" height="6" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.7"/>
      <rect x="14" y="4" width="10" height="7" rx="0.5" fill="#00000050" stroke="#FF4422" strokeWidth="1" opacity="0.9"/>
      <circle cx="14.5" cy="4.5" r="1" fill="#FF4422" opacity="0.8"/>
      <rect x="4" y="12" width="8" height="5" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.7"/>
      <rect x="14" y="13" width="10" height="3" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <rect x="4" y="20" width="20" height="2" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.4"/>
    </svg>
  ),
  faction_tension_chart: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <circle cx="14" cy="14" r="10" fill="none" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <circle cx="14" cy="14" r="7" fill="none" stroke={a} strokeWidth="0.4" opacity="0.25"/>
      <circle cx="14" cy="14" r="4" fill="none" stroke={a} strokeWidth="0.4" opacity="0.2"/>
      <ellipse cx="10" cy="13" rx="4" ry="3" fill="#4A9FFF" opacity="0.25"/>
      <ellipse cx="17" cy="15" rx="4" ry="3" fill="#FF4444" opacity="0.25"/>
      <ellipse cx="13" cy="18" rx="3" ry="2" fill="#E8A030" opacity="0.3"/>
      <circle cx="8" cy="8" r="3" fill="none" stroke="#9966FF" strokeWidth="1.2" opacity="0.7"/>
    </svg>
  ),
  csf_bulletin: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#6B5030" opacity="0.7"/>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="none" stroke={a} strokeWidth="0.7" opacity="0.5"/>
      <rect x="4" y="4" width="9" height="6" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.6"/>
      <rect x="15" y="4" width="9" height="6" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.6"/>
      <rect x="4" y="12" width="20" height="9" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.5"/>
      <line x1="6" y1="14" x2="22" y2="22" stroke="#FF4422" strokeWidth="2" opacity="0.8"/>
      <line x1="6" y1="22" x2="22" y2="14" stroke="#FF4422" strokeWidth="2" opacity="0.8"/>
      <rect x="4" y="22" width="6" height="2" rx="0.3" fill={a} opacity="0.3"/>
    </svg>
  ),
  lounge_bar_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="18" width="24" height="8" rx="1" fill="#00000070" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="4" y="6" width="20" height="13" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="5.5" y="7.5" width="17" height="9" fill={a} opacity="0.1"/>
      <circle cx="9" cy="12" r="1.5" fill={a} opacity="0.4"/>
      <circle cx="14" cy="12" r="1.5" fill={a} opacity="0.6" style={{animation:'lens-flicker 1.5s ease-in-out infinite'}}/>
      <circle cx="19" cy="12" r="1.5" fill={a} opacity="0.4"/>
      <line x1="5.5" y1="9.5" x2="22.5" y2="9.5" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <rect x="11" y="21" width="6" height="3" rx="0.5" fill={a} opacity="0.2"/>
    </svg>
  ),
  lounge_private_booth: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <path d="M3 22 Q3 6 14 4 Q25 6 25 22 Z" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <ellipse cx="9" cy="17" rx="3" ry="5" fill="#505060" opacity="0.8"/>
      <ellipse cx="19" cy="17" rx="3" ry="5" fill="#505060" opacity="0.8"/>
      <ellipse cx="8" cy="12" rx="2" ry="2.2" fill="#505060" opacity="0.7"/>
      <ellipse cx="20" cy="12" rx="2" ry="2.2" fill="#505060" opacity="0.7"/>
    </svg>
  ),
  lounge_datapad: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="4" width="22" height="20" rx="2" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="5" y="6" width="18" height="14" fill={a} opacity="0.08"/>
      <line x1="5" y1="9" x2="23" y2="9" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <polygon points="6,12 10,10 10,14" fill={a} opacity="0.5"/>
      <polygon points="11,12 15,10 15,14" fill={a} opacity="0.5"/>
      <polygon points="16,12 20,10 20,14" fill={a} opacity="0.5"/>
      <rect x="19" y="10" width="4" height="4" rx="0.5" fill="#E8A030" opacity="0.6"/>
      <rect x="5" y="17" width="12" height="2" fill="#00000060" stroke={a} strokeWidth="0.4" opacity="0.5"/>
      <rect x="5" y="20" width="8" height="1.5" fill="#00000070" opacity="0.7"/>
    </svg>
  ),
  precinct_evidence_locker: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="5" y="2" width="18" height="24" rx="1" fill="#00000060" stroke={a} strokeWidth="0.8" opacity="0.8"/>
      <rect x="6" y="2" width="16" height="2" fill="#FF4422" opacity="0.7"/>
      <circle cx="14" cy="14" r="3" fill="#00000070" stroke={a} strokeWidth="0.8" opacity="0.8"/>
      <circle cx="14" cy="14" r="1.5" fill="#00000090"/>
      <line x1="14" y1="11" x2="14" y2="14" stroke={a} strokeWidth="0.8" opacity="0.6"/>
      <rect x="12" y="20" width="4" height="1.5" rx="0.5" fill={a} opacity="0.3"/>
      <path d="M9 22 Q9.5 23 14 23.5 Q18.5 23 19 22" stroke={a} strokeWidth="0.5" fill="none" opacity="0.4"/>
    </svg>
  ),
  precinct_comms_station: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="6" width="24" height="18" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <line x1="4" y1="22" x2="4" y2="12" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="6" y1="22" x2="6" y2="10" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="8" y1="22" x2="8" y2="8" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="10" y1="22" x2="10" y2="9" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="12" y1="22" x2="12" y2="7" stroke={a} strokeWidth="1.2" opacity="0.6"/>
      <line x1="14" y1="22" x2="14" y2="16" stroke={a} strokeWidth="1.2" opacity="0.3"/>
      <line x1="16" y1="22" x2="16" y2="15" stroke={a} strokeWidth="1.2" opacity="0.3"/>
      <line x1="18" y1="22" x2="18" y2="17" stroke={a} strokeWidth="1.2" opacity="0.3"/>
      <line x1="20" y1="22" x2="20" y2="16" stroke={a} strokeWidth="1.2" opacity="0.3"/>
      <line x1="22" y1="22" x2="22" y2="14" stroke={a} strokeWidth="1.2" opacity="0.3"/>
      <line x1="24" y1="22" x2="24" y2="13" stroke="#E8A030" strokeWidth="1.4" opacity="0.9" style={{animation:'lens-flicker 1.2s ease-in-out infinite'}}/>
    </svg>
  ),
  precinct_wanted_board: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#4A3020" opacity="0.7"/>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="none" stroke={a} strokeWidth="0.7" opacity="0.5"/>
      <circle cx="5" cy="4.5" r="1" fill="#FF6622" opacity="0.7"/>
      <rect x="8" y="4" width="14" height="1.5" rx="0.3" fill={a} opacity="0.3"/>
      <circle cx="5" cy="7.5" r="1" fill="#FF6622" opacity="0.7"/>
      <rect x="8" y="7" width="14" height="1.5" rx="0.3" fill={a} opacity="0.3"/>
      <circle cx="5" cy="10.5" r="1" fill="#FF6622" opacity="0.7"/>
      <rect x="8" y="10" width="14" height="1.5" rx="0.3" fill={a} opacity="0.3"/>
      <circle cx="5" cy="13.5" r="1" fill="#FF6622" opacity="0.7"/>
      <rect x="8" y="13" width="14" height="1.5" rx="0.3" fill={a} opacity="0.3"/>
      <circle cx="5" cy="16.5" r="1" fill="#FF6622" opacity="0.5"/>
      <rect x="8" y="16" width="8" height="1.5" rx="0.3" fill="#FF4422" opacity="0.7"/>
      <circle cx="5" cy="19.5" r="1" fill="#FF6622" opacity="0.5"/>
      <rect x="8" y="19" width="8" height="1.5" rx="0.3" fill="#FF4422" opacity="0.7"/>
      <circle cx="5" cy="22.5" r="1" fill="#888" opacity="0.5"/>
      <rect x="8" y="22" width="10" height="1.5" rx="0.3" fill="#888" opacity="0.4"/>
    </svg>
  ),
  airtaxi_sky_market: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="3" width="6" height="22" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="7" y="12" width="14" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <rect x="8" y="10" width="4" height="3" fill={a} opacity="0.15"/>
      <rect x="16" y="10" width="4" height="3" fill={a} opacity="0.15"/>
      <circle cx="14" cy="4" r="2" fill={a} opacity="0.8" style={{animation:'lens-flicker 1.8s ease-in-out infinite'}}/>
      <rect x="6" y="24" width="16" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <line x1="7" y1="24" x2="21" y2="24" stroke={a} strokeWidth="0.4" opacity="0.3"/>
    </svg>
  ),
  skyline_vista: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="1" y="1" width="26" height="26" rx="1" fill="#000020" opacity="0.9"/>
      <rect x="1" y="1" width="26" height="26" rx="1" fill="none" stroke={a} strokeWidth="0.8" opacity="0.7"/>
      <rect x="1" y="1" width="26" height="8" fill="#E8C060" opacity="0.06"/>
      <rect x="3" y="12" width="3" height="14" fill="#101820" opacity="0.9"/>
      <rect x="7" y="8" width="2" height="18" fill="#101820" opacity="0.9"/>
      <rect x="10" y="10" width="4" height="16" fill="#101820" opacity="0.9"/>
      <rect x="15" y="6" width="2" height="20" fill="#101820" opacity="0.9"/>
      <rect x="18" y="9" width="3" height="17" fill="#101820" opacity="0.9"/>
      <rect x="22" y="11" width="4" height="15" fill="#101820" opacity="0.9"/>
      <line x1="1" y1="15" x2="27" y2="15" stroke="#FFFFFF" strokeWidth="0.4" opacity="0.25"/>
      <line x1="1" y1="18" x2="27" y2="18" stroke="#FFFFFF" strokeWidth="0.3" opacity="0.15"/>
    </svg>
  ),
  holonet_kiosk: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="8" y="18" width="12" height="9" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="10" y="5" width="8" height="14" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <polygon points="14,7 16,10 14,13 12,10" fill="none" stroke={a} strokeWidth="0.8" opacity="0.6"/>
      <line x1="10" y1="21" x2="18" y2="21" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <line x1="10" y1="23" x2="16" y2="23" stroke={a} strokeWidth="0.5" opacity="0.2"/>
      <path d="M7 5 Q14 2 21 5" stroke={a} strokeWidth="0.6" fill="none" opacity="0.4"/>
      <path d="M5 4 Q14 0 23 4" stroke={a} strokeWidth="0.5" fill="none" opacity="0.25"/>
    </svg>
  ),
  lounge_corner_conversation: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="4" height="24" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <rect x="2" y="2" width="24" height="4" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <ellipse cx="9" cy="18" rx="3" ry="5" fill="#404050" opacity="0.7"/>
      <ellipse cx="8.5" cy="13" rx="2.5" ry="2.5" fill="#404050" opacity="0.7"/>
      <ellipse cx="19" cy="14" rx="3" ry="5" fill="#404050" opacity="0.7"/>
      <ellipse cx="19.5" cy="9" rx="2.5" ry="2.5" fill="#404050" opacity="0.7"/>
      <circle cx="14" cy="16" r="1.5" fill={a} opacity="0.7"/>
    </svg>
  ),
  promenade_patrol_log: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="5" width="24" height="18" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <line x1="4" y1="9" x2="24" y2="9" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <rect x="4" y="10" width="8" height="2" rx="0.3" fill={a} opacity="0.4"/>
      <rect x="4" y="13" width="8" height="2" rx="0.3" fill={a} opacity="0.4"/>
      <line x1="4" y1="15" x2="14" y2="15" stroke="#FF4444" strokeWidth="0.8" opacity="0.6"/>
      <line x1="4" y1="18" x2="14" y2="18" stroke="#FF4444" strokeWidth="0.8" opacity="0.6"/>
      <rect x="14" y="10" width="9" height="11" fill="#000010" opacity="0.4"/>
      <rect x="18" y="20" width="6" height="2" rx="0.5" fill={a} opacity="0.35"/>
    </svg>
  ),
  bith_exchange: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="14" width="22" height="10" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="7" y="12" width="8" height="4" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <ellipse cx="7" cy="10" rx="4" ry="5" fill="#00000040" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <path d="M5 17 L7 14 L9 17" fill="none" stroke={a} strokeWidth="0.7" opacity="0.5"/>
      <circle cx="20" cy="17" r="1.5" fill={a} opacity="0.4"/>
      <circle cx="22" cy="19" r="1" fill={a} opacity="0.35"/>
      <circle cx="20" cy="21" r="0.8" fill={a} opacity="0.3"/>
    </svg>
  ),
  jon_comlink: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <ellipse cx="14" cy="17" rx="9" ry="7" fill="#00000060" stroke={a} strokeWidth="0.8" opacity="0.8"/>
      <line x1="14" y1="10" x2="14" y2="4" stroke={a} strokeWidth="0.8" opacity="0.7"/>
      <circle cx="14" cy="17" r="1.5" fill={a} opacity="0.7" style={{animation:'lens-flicker 2s ease-in-out infinite'}}/>
      <path d="M8 12 Q11 10 14 12" stroke={a} strokeWidth="0.6" fill="none" strokeDasharray="1.5,1.5" opacity="0.5"/>
      <path d="M6 10 Q10 7 14 10" stroke={a} strokeWidth="0.5" fill="none" strokeDasharray="1.5,1.5" opacity="0.35"/>
      <path d="M4 8 Q9 4 14 8" stroke={a} strokeWidth="0.5" fill="none" strokeDasharray="1.5,1.5" opacity="0.2"/>
    </svg>
  ),
  airtaxi_sky_customs: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="6" width="22" height="16" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <line x1="5" y1="14" x2="11" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <polygon points="11,12 14,14 11,16" fill={a} opacity="0.7"/>
      <polygon points="14,12 17,14 14,16" fill={a} opacity="0.7"/>
      <line x1="17" y1="14" x2="23" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <rect x="5" y="8" width="18" height="3" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.5"/>
      <rect x="5" y="18" width="18" height="3" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.4"/>
      <rect x="6" y="18.5" width="10" height="2" fill="#888" opacity="0.2"/>
      <rect x="11" y="23" width="6" height="2" rx="0.5" fill={a} opacity="0.3" style={{animation:'lens-flicker 2.5s ease-in-out infinite'}}/>
    </svg>
  ),
  customs_scanner_archway: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <path d="M5 26 L5 8 Q5 3 14 3 Q23 3 23 8 L23 26" fill="none" stroke={a} strokeWidth="1" opacity="0.7"/>
      <rect x="5" y="24" width="18" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <line x1="9" y1="8" x2="9" y2="24" stroke={a} strokeWidth="0.6" opacity="0.3"/>
      <line x1="12" y1="6" x2="12" y2="24" stroke={a} strokeWidth="0.6" opacity="0.3"/>
      <line x1="16" y1="6" x2="16" y2="24" stroke="#FF4422" strokeWidth="1" opacity="0.9"/>
      <line x1="19" y1="8" x2="19" y2="24" stroke={a} strokeWidth="0.6" opacity="0.3"/>
      <rect x="5" y="21" width="18" height="2" fill="#808080" opacity="0.2"/>
    </svg>
  ),
  caf_stand: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <ellipse cx="14" cy="10" rx="5" ry="7" fill="#C0C0C0" opacity="0.3" stroke={a} strokeWidth="0.7"/>
      <rect x="9" y="10" width="10" height="4" fill="#C0C0C0" opacity="0.2"/>
      <rect x="4" y="14" width="20" height="6" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <ellipse cx="10" cy="14" rx="2" ry="2.5" fill="#C0C0C0" opacity="0.2" stroke={a} strokeWidth="0.4"/>
      <ellipse cx="18" cy="14" rx="2" ry="2.5" fill="#C0C0C0" opacity="0.2" stroke={a} strokeWidth="0.4"/>
      <path d="M5 7 Q14 5 23 7" stroke={a} strokeWidth="0.5" fill="none" opacity="0.4"/>
    </svg>
  ),
  detention_alcove: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#00000070" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <circle cx="8" cy="9" r="1.5" fill="none" stroke={a} strokeWidth="0.8" opacity="0.6"/>
      <circle cx="8" cy="16" r="1.5" fill="none" stroke={a} strokeWidth="0.8" opacity="0.6"/>
      <line x1="8" y1="9" x2="8" y2="16" stroke={a} strokeWidth="0.6" opacity="0.5"/>
      <line x1="9.5" y1="10.5" x2="12" y2="13" stroke={a} strokeWidth="0.6" opacity="0.4"/>
      <circle cx="12" cy="23" r="1.5" fill="none" stroke="#888" strokeWidth="0.7" opacity="0.5"/>
      <line x1="11" y1="22" x2="13" y2="24" stroke="#888" strokeWidth="0.6" opacity="0.4"/>
      <rect x="16" y="8" width="9" height="14" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.6"/>
      <line x1="17" y1="12" x2="24" y2="12" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="17" y1="14" x2="24" y2="14" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="17" y1="16" x2="24" y2="16" stroke={a} strokeWidth="0.8" opacity="0.6"/>
    </svg>
  ),
  flight_control_booth: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="6" width="24" height="16" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="4" y="8" width="20" height="10" fill={a} opacity="0.07"/>
      <line x1="4" y1="10" x2="24" y2="10" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <line x1="4" y1="13" x2="24" y2="13" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <line x1="4" y1="16" x2="24" y2="16" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <rect x="10" y="10.5" width="8" height="5" fill="#FFFFFF" opacity="0.04"/>
      <rect x="10" y="10.5" width="8" height="5" fill="none" stroke="#FF4422" strokeWidth="0.6" opacity="0.6"/>
      <line x1="10" y1="10.5" x2="18" y2="15.5" stroke="#FF4422" strokeWidth="0.5" opacity="0.5"/>
      <rect x="4" y="22" width="20" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
    </svg>
  ),
  siphon_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="7" y="16" width="14" height="10" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <line x1="14" y1="16" x2="14" y2="2" stroke={a} strokeWidth="0.8" opacity="0.6"/>
      <circle cx="14" cy="5" r="1.2" fill={a} opacity="0.5"/>
      <circle cx="14" cy="9" r="1.2" fill={a} opacity="0.4"/>
      <circle cx="14" cy="13" r="1.5" fill={a} opacity="0.7" style={{animation:'lens-flicker 1.6s ease-in-out infinite'}}/>
      <circle cx="9" cy="22" r="1.5" fill="#00000070" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <circle cx="9" cy="22" r="0.7" fill="#00000090"/>
      <line x1="9" y1="20.5" x2="9" y2="22" stroke={a} strokeWidth="0.6" opacity="0.5"/>
    </svg>
  ),
  bay14_crime_scene: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <polygon points="14,10 15.5,7 17,10 20,10.5 17.5,13 18.5,16 14,14 9.5,16 10.5,13 8,10.5 11,10 12.5,7" fill="#FF6622" opacity="0.35"/>
      <line x1="14" y1="10" x2="14" y2="3" stroke="#FF6622" strokeWidth="0.6" opacity="0.4"/>
      <line x1="14" y1="10" x2="21" y2="6" stroke="#FF6622" strokeWidth="0.6" opacity="0.35"/>
      <line x1="14" y1="10" x2="22" y2="12" stroke="#FF6622" strokeWidth="0.6" opacity="0.35"/>
      <line x1="14" y1="10" x2="7" y2="6" stroke="#FF6622" strokeWidth="0.6" opacity="0.35"/>
      <line x1="14" y1="10" x2="6" y2="12" stroke="#FF6622" strokeWidth="0.6" opacity="0.35"/>
      <ellipse cx="14" cy="22" rx="7" ry="3" fill="#00000050" stroke={a} strokeWidth="0.6" opacity="0.5"/>
      <rect x="11" y="20" width="4" height="3" rx="0.3" fill="#404040" opacity="0.5"/>
      <line x1="4" y1="19" x2="24" y2="26" stroke="#FFB800" strokeWidth="1.5" opacity="0.6"/>
    </svg>
  ),
  discarded_keycard: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="4" y="9" width="20" height="12" rx="1.5" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="4" y="17" width="8" height="4" rx="0" fill="#00000080" opacity="0.8"/>
      <line x1="4" y1="17" x2="12" y2="21" stroke="#FF4422" strokeWidth="0.5" opacity="0.4"/>
      <rect x="6" y="11" width="4" height="3" rx="0.5" fill={a} opacity="0.3"/>
      <rect x="12" y="11" width="2" height="8" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="15" y="11" width="2" height="8" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="18" y="11" width="2" height="8" rx="0.3" fill={a} opacity="0.2"/>
      <path d="M5 21 Q7 18 9 21" stroke="#FF6622" strokeWidth="0.5" fill="none" opacity="0.6"/>
    </svg>
  ),
  shipping_crate_b14: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="4" y="10" width="20" height="16" rx="1" fill="#00000060" stroke={a} strokeWidth="0.8" opacity="0.7"/>
      <line x1="14" y1="10" x2="14" y2="26" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <line x1="4" y1="18" x2="24" y2="18" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <polygon points="4,10 14,5 24,10" fill="#00000050" stroke={a} strokeWidth="0.7" opacity="0.6"/>
      <line x1="4" y1="13" x2="11" y2="13" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <rect x="14" y="19" width="8" height="5" rx="0.5" fill="#00000080" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <rect x="15" y="20" width="3" height="3" rx="0.3" fill="#7FFF44" opacity="0.6"/>
      <circle cx="21" cy="22" r="1.2" fill={a} opacity="0.3"/>
    </svg>
  ),
  airtaxi_mag_rail: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="3" width="6" height="22" rx="1" fill="#00000060" stroke={a} strokeWidth="0.8" opacity="0.7"/>
      <rect x="6" y="12" width="16" height="4" rx="0.5" fill="#00000070" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <rect x="3" y="22" width="22" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <rect x="5" y="23" width="6" height="2" rx="0.3" fill={a} opacity="0.15"/>
      <rect x="13" y="23" width="6" height="2" rx="0.3" fill="#888" opacity="0.2"/>
      <rect x="8" y="8" width="12" height="5" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <line x1="10" y1="10" x2="18" y2="10" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <line x1="10" y1="11.5" x2="16" y2="11.5" stroke="#888" strokeWidth="0.5" opacity="0.4"/>
      <circle cx="14" cy="4.5" r="1.5" fill={a} opacity="0.6" style={{animation:'lens-flicker 2.2s ease-in-out infinite'}}/>
    </svg>
  ),
  mag_rail_manifest_board: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <line x1="14" y1="2" x2="14" y2="26" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <rect x="3" y="5" width="10" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="15" y="5" width="10" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="3" y="9" width="10" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="15" y="9" width="10" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="3" y="13" width="10" height="2" rx="0.3" fill={a} opacity="0.6"/>
      <rect x="15" y="13" width="10" height="2" rx="0.3" fill="#E8A030" opacity="0.7"/>
      <rect x="3" y="17" width="10" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="15" y="17" width="10" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="24" y="13" width="3" height="2" rx="0.5" fill="#E8A030" opacity="0.8"/>
    </svg>
  ),
  platform_security_scanner: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <path d="M4 26 L4 8 Q4 3 14 3 Q24 3 24 8 L24 26" fill="none" stroke={a} strokeWidth="1" opacity="0.7"/>
      <line x1="4" y1="18" x2="24" y2="18" stroke={a} strokeWidth="0.6" opacity="0.4"/>
      <rect x="6" y="19" width="5" height="6" rx="0.5" fill="#4A9FFF" opacity="0.4"/>
      <rect x="12" y="19" width="5" height="6" rx="0.5" fill="#4A9FFF" opacity="0.4"/>
      <rect x="18" y="19" width="5" height="6" rx="0.5" fill="#4A9FFF" opacity="0.3"/>
      <rect x="10" y="13" width="8" height="3" rx="0.5" fill="#E8A030" opacity="0.6" style={{animation:'lens-flicker 1.8s ease-in-out infinite'}}/>
    </svg>
  ),
  north_platform_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="3" width="22" height="22" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="5" y="5" width="18" height="16" fill={a} opacity="0.07"/>
      <rect x="5" y="6" width="4" height="1.5" rx="0.3" fill="#FF4422" opacity="0.7"/>
      <rect x="10" y="6" width="12" height="1.5" rx="0.3" fill="#FF4422" opacity="0.5"/>
      <rect x="5" y="9" width="4" height="1.5" rx="0.3" fill="#FF4422" opacity="0.7"/>
      <rect x="10" y="9" width="12" height="1.5" rx="0.3" fill="#FF4422" opacity="0.5"/>
      <rect x="5" y="12" width="4" height="1.5" rx="0.3" fill="#FF4422" opacity="0.7"/>
      <rect x="10" y="12" width="12" height="1.5" rx="0.3" fill="#FF4422" opacity="0.5"/>
      <rect x="5" y="15" width="4" height="1.5" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="10" y="15" width="12" height="1.5" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="5" y="18" width="4" height="1.5" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="10" y="18" width="12" height="1.5" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="5" y="21" width="18" height="1.5" rx="0.3" fill="#00000050" opacity="0.5"/>
    </svg>
  ),
  rail_junction_box: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="4" y="6" width="20" height="18" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <circle cx="14" cy="15" r="7" fill="none" stroke={a} strokeWidth="0.6" opacity="0.4"/>
      <line x1="14" y1="8" x2="14" y2="15" stroke={a} strokeWidth="0.8" opacity="0.6"/>
      <line x1="14" y1="15" x2="19" y2="12" stroke="#FF4422" strokeWidth="0.8" opacity="0.7"/>
      <circle cx="14" cy="15" r="1.5" fill={a} opacity="0.6"/>
      <rect x="6" y="3" width="7" height="4" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.5"/>
    </svg>
  ),
  pursuit_start: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="12" width="22" height="8" rx="3" fill="#00000060" stroke={a} strokeWidth="0.8" opacity="0.8"/>
      <rect x="6" y="9" width="12" height="5" rx="2" fill="#00000070" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <ellipse cx="7" cy="21" rx="3" ry="3" fill="#00000070" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <ellipse cx="19" cy="21" rx="3" ry="3" fill="#00000070" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="22" y="13" width="5" height="5" rx="1" fill={a} opacity="0.3"/>
      <ellipse cx="24.5" cy="15.5" rx="1.5" ry="1.5" fill={a} opacity="0.5" style={{animation:'lens-flicker 1.4s ease-in-out infinite'}}/>
      <rect x="21" y="16" width="2" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.4" opacity="0.5"/>
    </svg>
  ),
  airtaxi_fueling_depot: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="3" width="6" height="22" rx="1" fill="#00000060" stroke={a} strokeWidth="0.8" opacity="0.6"/>
      <rect x="7" y="12" width="14" height="4" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <rect x="9" y="16" width="10" height="2" rx="0.3" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.4"/>
      <rect x="12" y="16" width="4" height="2" fill={a} opacity="0.3"/>
      <circle cx="14" cy="4.5" r="1.5" fill={a} opacity="0.6" style={{animation:'lens-flicker 2s ease-in-out infinite'}}/>
      <circle cx="4" cy="9" r="0.6" fill="#404040" opacity="0.4"/>
      <circle cx="7" cy="13" r="0.6" fill="#404040" opacity="0.4"/>
      <circle cx="22" cy="7" r="0.6" fill="#404040" opacity="0.4"/>
      <circle cx="20" cy="18" r="0.6" fill="#404040" opacity="0.4"/>
      <rect x="4" y="24" width="20" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
    </svg>
  ),
  fuel_register_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="4" width="22" height="20" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="5" y="6" width="18" height="10" fill={a} opacity="0.07"/>
      <rect x="5" y="10" width="4" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="10" y="10" width="12" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="5" y="13" width="4" height="2" rx="0.3" fill={a} opacity="0.3"/>
      <rect x="10" y="13" width="8" height="2" rx="0.3" fill="#E8A030" opacity="0.6"/>
      <rect x="10" y="13" width="12" height="2" fill="none" stroke="#E8A030" strokeWidth="0.5" opacity="0.4"/>
      <rect x="5" y="17" width="18" height="5" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <rect x="6" y="18" width="4" height="3" fill={a} opacity="0.15"/>
      <rect x="15" y="18" width="7" height="3" fill="#E8A030" opacity="0.25"/>
    </svg>
  ),
  bay_b_work_order: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="5" y="2" width="18" height="24" rx="1" fill="#F0E8D0" opacity="0.25"/>
      <rect x="5" y="2" width="18" height="24" rx="1" fill="none" stroke={a} strokeWidth="0.7" opacity="0.6"/>
      <line x1="7" y1="6" x2="21" y2="6" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <line x1="7" y1="9" x2="21" y2="9" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <rect x="5" y="13" width="18" height="5" rx="0" fill="#D0D8E8" opacity="0.15"/>
      <rect x="5" y="13" width="18" height="5" fill="none" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="7" y1="15" x2="21" y2="15" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <circle cx="8" cy="24" r="1.5" fill="#9966FF" opacity="0.5"/>
      <rect x="10" y="23" width="8" height="1" rx="0.3" fill="#9966FF" opacity="0.3"/>
    </svg>
  ),
  maintenance_pit_console: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="6" width="24" height="18" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <circle cx="8" cy="11" r="4" fill="none" stroke={a} strokeWidth="0.7" opacity="0.6"/>
      <circle cx="8" cy="11" r="2" fill={a} opacity="0.1"/>
      <line x1="8" y1="7" x2="8" y2="15" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <line x1="4" y1="11" x2="12" y2="11" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <rect x="14" y="7" width="11" height="14" fill={a} opacity="0.06"/>
      <rect x="15" y="8" width="6" height="1.2" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="15" y="10" width="9" height="1.2" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="15" y="12" width="6" height="1.2" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="15" y="14" width="9" height="1.2" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="15" y="16" width="9" height="1.2" rx="0.3" fill={a} opacity="0.5"/>
      <rect x="15" y="18" width="6" height="1.2" rx="0.3" fill={a} opacity="0.2"/>
      <rect x="4" y="19" width="20" height="4" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.4" opacity="0.5"/>
    </svg>
  ),
  fuel_drum_stack: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="2" width="7" height="11" rx="1" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.7"/>
      <line x1="4" y1="5" x2="9" y2="5" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="4" y1="9" x2="9" y2="9" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <rect x="11" y="2" width="7" height="11" rx="1" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.7"/>
      <line x1="12" y1="5" x2="17" y2="5" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="12" y1="9" x2="17" y2="9" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <rect x="19" y="2" width="7" height="11" rx="1" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.7"/>
      <line x1="20" y1="5" x2="25" y2="5" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="20" y1="9" x2="25" y2="9" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <rect x="3" y="14" width="7" height="11" rx="1" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.7"/>
      <line x1="4" y1="17" x2="9" y2="17" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="4" y1="21" x2="9" y2="21" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <rect x="11" y="14" width="7" height="11" rx="1" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.7"/>
      <line x1="12" y1="17" x2="17" y2="17" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="12" y1="21" x2="17" y2="21" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <rect x="19" y="14" width="7" height="11" rx="1" fill="#00000060" stroke={a} strokeWidth="0.6" opacity="0.7"/>
      <line x1="20" y1="17" x2="25" y2="17" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <line x1="20" y1="21" x2="25" y2="21" stroke={a} strokeWidth="0.4" opacity="0.3"/>
      <rect x="3" y="25" width="22" height="1" fill="#9966FF" opacity="0.3"/>
    </svg>
  ),
  airtaxi_drainage: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="2" width="6" height="24" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.6"/>
      <rect x="7" y="12" width="14" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <line x1="7" y1="14" x2="5" y2="18" stroke={a} strokeWidth="0.8" opacity="0.4"/>
      <rect x="3" y="17" width="5" height="8" rx="0.5" fill="#00000050" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <rect x="4" y="19" width="3" height="1" rx="0.2" fill={a} opacity="0.2"/>
      <rect x="4" y="21" width="3" height="1" rx="0.2" fill="#888" opacity="0.2"/>
      <rect x="4" y="23" width="3" height="1" rx="0.2" fill="#888" opacity="0.15"/>
      <rect x="11" y="1" width="6" height="3" rx="0.5" fill="#00000070" stroke="#888" strokeWidth="0.5" opacity="0.5"/>
      <circle cx="14" cy="2.5" r="1" fill="#888" opacity="0.4"/>
    </svg>
  ),
  drainage_channel_main: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="8" width="24" height="16" rx="1" fill="#00000070" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="3" y="16" width="22" height="6" rx="0.5" fill="#C88040" opacity="0.25"/>
      <path d="M3 19 Q7 17 11 19 Q15 21 19 19 Q23 17 25 19" stroke="#C88040" strokeWidth="0.8" fill="none" opacity="0.5"/>
      <line x1="5" y1="13" x2="5" y2="9" stroke={a} strokeWidth="0.5" opacity="0.3" style={{animation:'mist-drift 4s ease-in-out infinite'}}/>
      <line x1="10" y1="14" x2="10" y2="10" stroke={a} strokeWidth="0.5" opacity="0.25" style={{animation:'mist-drift 3.5s ease-in-out infinite'}}/>
      <line x1="18" y1="13" x2="18" y2="9" stroke={a} strokeWidth="0.5" opacity="0.2" style={{animation:'mist-drift 4.5s ease-in-out infinite'}}/>
      <rect x="22" y="9" width="3" height="5" rx="0.3" fill="#FF4444" opacity="0.3"/>
    </svg>
  ),
  syndicate_cache_drainage: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="4" width="24" height="20" rx="1" fill="#00000060" opacity="0.7"/>
      <path d="M2 4 Q14 3 26 4" stroke="#404040" strokeWidth="1" opacity="0.5"/>
      <rect x="5" y="8" width="18" height="13" rx="1" fill="#00000070" stroke="#40C840" strokeWidth="0.8" opacity="0.7"/>
      <path d="M12 13 L14 10 L16 13 L14 16 Z" fill="none" stroke="#40C840" strokeWidth="0.8" opacity="0.6"/>
      <line x1="12" y1="13" x2="16" y2="13" stroke="#40C840" strokeWidth="0.5" opacity="0.4"/>
      <rect x="7" y="16" width="3" height="4" rx="0.3" fill={a} opacity="0.25"/>
      <rect x="11" y="16" width="3" height="4" rx="0.3" fill={a} opacity="0.25"/>
      <rect x="15" y="16" width="3" height="4" rx="0.3" fill={a} opacity="0.25"/>
    </svg>
  ),
  drainage_graffiti_wall: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="1" y="1" width="26" height="26" fill="#202028" opacity="0.8"/>
      <rect x="1" y="1" width="26" height="26" fill="none" stroke={a} strokeWidth="0.5" opacity="0.3"/>
      <path d="M3 8 Q5 6 7 9 Q9 12 11 8" stroke="#405060" strokeWidth="0.8" fill="none" opacity="0.6"/>
      <path d="M4 14 Q7 11 10 15 Q13 19 16 13" stroke="#405060" strokeWidth="0.8" fill="none" opacity="0.5"/>
      <path d="M2 20 Q5 17 8 21 Q11 25 14 19" stroke="#405060" strokeWidth="0.7" fill="none" opacity="0.5"/>
      <path d="M14 15 L16 12 L18 15 L16 18 Z" fill="none" stroke="#40C840" strokeWidth="1.2" opacity="0.8"/>
      <line x1="14" y1="15" x2="18" y2="15" stroke="#40C840" strokeWidth="0.7" opacity="0.6"/>
      <circle cx="16" cy="15" r="0.8" fill="#40C840" opacity="0.5"/>
    </svg>
  ),
  junction_box_drainage: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="4" y="6" width="20" height="18" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.7"/>
      <rect x="6" y="8" width="16" height="12" fill={a} opacity="0.06"/>
      <line x1="6" y1="11" x2="22" y2="11" stroke={a} strokeWidth="0.4" opacity="0.25"/>
      <line x1="6" y1="14" x2="22" y2="14" stroke={a} strokeWidth="0.4" opacity="0.25"/>
      <line x1="14" y1="8" x2="14" y2="20" stroke={a} strokeWidth="0.4" opacity="0.25"/>
      <polygon points="6,17 14,17 14,20 6,17" fill={a} opacity="0.3"/>
      <polygon points="14,17 22,20 22,17" fill={a} opacity="0.2"/>
      <circle cx="20" cy="21" r="1.5" fill="#FF4444" opacity="0.4"/>
    </svg>
  ),
  syndicate_marker_drainage: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#00000050" opacity="0.5"/>
      <path d="M10 14 L14 10 L18 14 L14 18 Z" fill="none" stroke="#40C840" strokeWidth="1.5" opacity="0.8"/>
      <line x1="10" y1="14" x2="18" y2="14" stroke="#40C840" strokeWidth="0.8" opacity="0.5"/>
      <circle cx="14" cy="14" r="2" fill="#40C840" opacity="0.15"/>
      <circle cx="14" cy="14" r="3.5" fill="none" stroke="#40C840" strokeWidth="0.4" opacity="0.4"/>
      <polygon points="18,14 22,14 22,16 20,17" fill={a} opacity="0.4"/>
      <polygon points="22,16 22,20 20,18" fill={a} opacity="0.3"/>
    </svg>
  ),
  syndicate_cargo_cache: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="12" width="12" height="14" rx="1" fill="#00000060" stroke="#40C840" strokeWidth="0.8" opacity="0.8"/>
      <path d="M5 15 L7 13 L9 15 L7 17 Z" fill="none" stroke="#40C840" strokeWidth="0.7" opacity="0.6"/>
      <rect x="14" y="10" width="12" height="16" rx="1" fill="#00000060" stroke="#40C840" strokeWidth="0.8" opacity="0.7"/>
      <rect x="15" y="12" width="5" height="4" rx="0.5" fill={a} opacity="0.25"/>
      <rect x="20" y="11" width="5" height="2" rx="0.3" fill="#808080" opacity="0.3"/>
      <rect x="20" y="14" width="5" height="2" rx="0.3" fill="#808080" opacity="0.3"/>
      <ellipse cx="7" cy="9" rx="4" ry="3" fill="none" stroke="#808080" strokeWidth="0.6" opacity="0.4"/>
      <line x1="5" y1="9" x2="9" y2="9" stroke="#808080" strokeWidth="0.4" opacity="0.3"/>
    </svg>
  ),
  plasma_conduit_005: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="10" width="24" height="8" rx="2" fill="#00000060" stroke={a} strokeWidth="0.9" opacity="0.8"/>
      <rect x="4" y="12" width="20" height="4" fill={a} opacity="0.08"/>
      <path d="M10 10 Q12 8 14 10 Q16 12 18 10" stroke={a} strokeWidth="0.6" fill="none" opacity="0.5" style={{animation:'mist-drift 2.5s ease-in-out infinite'}}/>
      <rect x="12" y="8" width="4" height="4" rx="0.5" fill="#FF4422" opacity="0.15"/>
      <line x1="13" y1="8" x2="14" y2="6" stroke="#FF4422" strokeWidth="0.5" opacity="0.4"/>
      <line x1="15" y1="8" x2="16" y2="6" stroke="#FF4422" strokeWidth="0.5" opacity="0.3"/>
      <circle cx="22" cy="14" r="1.5" fill="#FF4422" opacity="0.5"/>
      <circle cx="6" cy="14" r="1.5" fill="#FF4422" opacity="0.4"/>
    </svg>
  ),
  sub_station_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="4" width="24" height="20" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="4" y="6" width="10" height="8" fill={a} opacity="0.07"/>
      <line x1="4" y1="9" x2="9" y2="9" stroke={a} strokeWidth="0.6" opacity="0.4"/>
      <line x1="4" y1="11" x2="14" y2="11" stroke={a} strokeWidth="0.6" opacity="0.4"/>
      <polygon points="5,14 10,6 13,14" fill="none" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <line x1="10" y1="6" x2="10" y2="8" stroke={a} strokeWidth="0.7" opacity="0.5"/>
      <rect x="15" y="6" width="10" height="16" fill={a} opacity="0.05"/>
      <rect x="16" y="7" width="8" height="1" rx="0.2" fill={a} opacity="0.2"/>
      <rect x="16" y="9" width="5" height="1" rx="0.2" fill={a} opacity="0.2"/>
      <rect x="16" y="11" width="8" height="1" rx="0.2" fill={a} opacity="0.2"/>
      <rect x="16" y="13" width="5" height="1" rx="0.2" fill={a} opacity="0.2"/>
      <rect x="16" y="15" width="8" height="1" rx="0.2" fill={a} opacity="0.2"/>
      <rect x="16" y="17" width="5" height="1" rx="0.2" fill="#FF4444" opacity="0.6"/>
      <rect x="16" y="19" width="8" height="1" rx="0.2" fill={a} opacity="0.2"/>
    </svg>
  ),
  syndicate_relay_node: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <circle cx="14" cy="16" r="8" fill="#00000060" stroke="#40C840" strokeWidth="0.8" opacity="0.7"/>
      <circle cx="14" cy="16" r="5" fill="none" stroke="#40C840" strokeWidth="0.4" opacity="0.4"/>
      <circle cx="14" cy="16" r="2" fill="#40C840" opacity="0.2"/>
      <path d="M8 10 Q14 8 20 10" stroke="#40C840" strokeWidth="0.6" fill="none" opacity="0.4"/>
      <path d="M6 7 Q14 4 22 7" stroke="#40C840" strokeWidth="0.5" fill="none" opacity="0.25"/>
      <circle cx="7" cy="14" r="1" fill={a} opacity="0.5"/>
      <circle cx="21" cy="13" r="1" fill={a} opacity="0.5"/>
      <circle cx="12" cy="22" r="1" fill={a} opacity="0.4"/>
      <line x1="18" y1="19" x2="21" y2="22" stroke="#40C840" strokeWidth="0.7" opacity="0.5"/>
      <circle cx="21" cy="22" r="1" fill="#40C840" opacity="0.4"/>
    </svg>
  ),
  plasma_valve_a: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <circle cx="14" cy="14" r="10" fill="#00000060" stroke={a} strokeWidth="0.9" opacity="0.8"/>
      <line x1="14" y1="4" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="24" y1="14" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="14" y1="24" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="4" y1="14" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <circle cx="14" cy="14" r="3" fill="#00000080" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <rect x="4" y="5" width="8" height="4" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.6"/>
      <line x1="5" y1="7" x2="11" y2="7" stroke="#FF4422" strokeWidth="2" opacity="0.7"/>
    </svg>
  ),
  plasma_valve_b: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <circle cx="14" cy="14" r="10" fill="#00000060" stroke={a} strokeWidth="0.9" opacity="0.8"/>
      <line x1="14" y1="4" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="24" y1="14" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="14" y1="24" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="4" y1="14" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <circle cx="14" cy="14" r="3" fill="#00000080" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <line x1="5" y1="6" x2="7" y2="4" stroke={a} strokeWidth="0.5" opacity="0.3" style={{animation:'mist-drift 2s ease-in-out infinite'}}/>
      <line x1="8" y1="5" x2="10" y2="3" stroke={a} strokeWidth="0.5" opacity="0.25" style={{animation:'mist-drift 2.5s ease-in-out infinite'}}/>
      <line x1="20" y1="5" x2="22" y2="3" stroke={a} strokeWidth="0.5" opacity="0.2" style={{animation:'mist-drift 3s ease-in-out infinite'}}/>
      <rect x="4" y="22" width="8" height="4" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
    </svg>
  ),
  plasma_valve_c: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <circle cx="14" cy="14" r="10" fill="#00000060" stroke={a} strokeWidth="0.9" opacity="0.8"/>
      <line x1="14" y1="4" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="24" y1="14" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="14" y1="24" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <line x1="4" y1="14" x2="14" y2="14" stroke={a} strokeWidth="1.2" opacity="0.7"/>
      <circle cx="14" cy="14" r="3" fill="#00000080" stroke={a} strokeWidth="0.7" opacity="0.8"/>
      <circle cx="5" cy="5" r="2" fill="none" stroke={a} strokeWidth="0.6" opacity="0.5"/>
      <line x1="5" y1="5" x2="14" y2="14" stroke={a} strokeWidth="0.5" opacity="0.4"/>
      <circle cx="23" cy="5" r="2" fill="none" stroke={a} strokeWidth="0.6" opacity="0.5"/>
      <line x1="23" y1="5" x2="14" y2="14" stroke={a} strokeWidth="0.5" opacity="0.4"/>
    </svg>
  ),
  holonet_official_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="4" width="22" height="20" rx="1" fill="#00000070" stroke="#808080" strokeWidth="0.8" opacity="0.7"/>
      <rect x="5" y="6" width="18" height="12" fill={a} opacity="0.1"/>
      <rect x="5" y="6" width="18" height="12" fill="none" stroke={a} strokeWidth="0.6" opacity="0.6"/>
      <rect x="6" y="7" width="16" height="10" fill="#001020" opacity="0.5"/>
      <polygon points="11,10 17,13 11,16" fill={a} opacity="0.5"/>
      <circle cx="5" cy="6" r="0.5" fill="#808080" opacity="0.4"/>
      <circle cx="9" cy="7" r="0.5" fill="#808080" opacity="0.35"/>
      <circle cx="14" cy="5" r="0.5" fill="#808080" opacity="0.3"/>
      <circle cx="19" cy="8" r="0.5" fill="#808080" opacity="0.35"/>
      <circle cx="23" cy="6" r="0.5" fill="#808080" opacity="0.4"/>
      <rect x="5" y="19" width="18" height="4" rx="0.5" fill="#00000060" stroke="#808080" strokeWidth="0.5" opacity="0.5"/>
    </svg>
  ),
  airtaxi_the_works: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="2" width="6" height="22" rx="1" fill="#00000060" stroke="#808080" strokeWidth="0.7" opacity="0.6"/>
      <rect x="7" y="11" width="14" height="4" rx="0.5" fill="#00000060" stroke="#808080" strokeWidth="0.5" opacity="0.5"/>
      <rect x="10" y="2" width="8" height="3" rx="0.5" fill="#FF4422" opacity="0.6"/>
      <rect x="4" y="24" width="20" height="3" rx="0.5" fill="#00000060" stroke="#808080" strokeWidth="0.4" opacity="0.5"/>
      <line x1="7" y1="8" x2="9" y2="6" stroke={a} strokeWidth="0.5" opacity="0.3" style={{animation:'mist-drift 2s ease-in-out infinite'}}/>
      <line x1="14" y1="7" x2="16" y2="5" stroke={a} strokeWidth="0.5" opacity="0.3" style={{animation:'mist-drift 2.5s ease-in-out infinite'}}/>
      <circle cx="14" cy="3.5" r="1.5" fill="#FF4422" opacity="0.6" style={{animation:'lens-flicker 1.5s ease-in-out infinite'}}/>
    </svg>
  ),
  airtaxi_csf_academy: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="3" width="6" height="22" rx="1" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.8" opacity="0.8"/>
      <rect x="7" y="12" width="14" height="3" rx="0.5" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.6" opacity="0.6"/>
      <rect x="8" y="10" width="4" height="3" fill="#4A9FFF" opacity="0.2"/>
      <rect x="16" y="10" width="4" height="3" fill="#4A9FFF" opacity="0.2"/>
      <circle cx="14" cy="4.5" r="2" fill="#4A9FFF" opacity="0.8" style={{animation:'lens-flicker 2s ease-in-out infinite'}}/>
      <rect x="9" y="18" width="10" height="4" rx="0.5" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.5" opacity="0.6"/>
      <rect x="10" y="19" width="3" height="2" rx="0.3" fill="#4A9FFF" opacity="0.25"/>
      <rect x="4" y="24" width="20" height="3" rx="0.5" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.5" opacity="0.5"/>
    </svg>
  ),
  module_a_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="4" width="22" height="18" rx="1" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.7" opacity="0.8"/>
      <rect x="5" y="6" width="18" height="10" fill="#4A9FFF" opacity="0.08"/>
      <circle cx="14" cy="9" r="4" fill="none" stroke="#4A9FFF" strokeWidth="0.7" opacity="0.5"/>
      <path d="M12 9 L13.5 7 L15.5 11 L17 8" stroke="#4A9FFF" strokeWidth="0.6" fill="none" opacity="0.5"/>
      <rect x="5" y="17" width="18" height="4" rx="0.5" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.4" opacity="0.5"/>
      <rect x="20" y="17.5" width="2" height="3" rx="0.3" fill="#4A9FFF" opacity="0.4"/>
    </svg>
  ),
  module_b_terminal: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="3" y="4" width="22" height="18" rx="1" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.7" opacity="0.8"/>
      <rect x="5" y="6" width="10" height="10" fill="#4A9FFF" opacity="0.07"/>
      <rect x="5" y="6" width="10" height="10" fill="none" stroke="#4A9FFF" strokeWidth="0.5" opacity="0.4"/>
      <line x1="5" y1="9" x2="15" y2="9" stroke="#4A9FFF" strokeWidth="0.4" opacity="0.3"/>
      <line x1="5" y1="12" x2="15" y2="12" stroke="#4A9FFF" strokeWidth="0.4" opacity="0.3"/>
      <ellipse cx="20.5" cy="10" rx="2.5" ry="3" fill="none" stroke="#4A9FFF" strokeWidth="0.6" opacity="0.4"/>
      <rect x="5" y="17" width="18" height="4" rx="0.5" fill="#00000060" stroke="#4A9FFF" strokeWidth="0.4" opacity="0.5"/>
    </svg>
  ),
  holding_block_b: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#00000070" stroke={a} strokeWidth="0.8" opacity="0.8"/>
      <rect x="9" y="8" width="10" height="14" rx="0.5" fill="#00000080" stroke={a} strokeWidth="0.6" opacity="0.7"/>
      <ellipse cx="14" cy="13" rx="3" ry="4" fill="#404050" opacity="0.5"/>
      <ellipse cx="14" cy="10" rx="2" ry="2" fill="#404050" opacity="0.5"/>
      <rect x="18" y="10" width="5" height="8" rx="0.3" fill="#00000060" stroke={a} strokeWidth="0.4" opacity="0.5"/>
      <line x1="10" y1="8" x2="10" y2="22" stroke={a} strokeWidth="0.4" opacity="0.2"/>
      <line x1="12" y1="8" x2="12" y2="22" stroke={a} strokeWidth="0.4" opacity="0.2"/>
      <line x1="16" y1="8" x2="16" y2="22" stroke={a} strokeWidth="0.4" opacity="0.2"/>
      <line x1="18" y1="8" x2="18" y2="22" stroke={a} strokeWidth="0.4" opacity="0.2"/>
    </svg>
  ),
  senate_honor_ceremony: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="8" y="18" width="12" height="8" rx="1" fill="#00000060" stroke="#9966FF" strokeWidth="0.7" opacity="0.7"/>
      <rect x="10" y="6" width="8" height="13" rx="0.5" fill="#9966FF" opacity="0.1"/>
      <rect x="10" y="6" width="8" height="13" rx="0.5" fill="none" stroke="#9966FF" strokeWidth="0.5" opacity="0.5"/>
      <path d="M14 8 L14.7 10 L17 10 L15.3 11.2 L15.9 13.5 L14 12 L12.1 13.5 L12.7 11.2 L11 10 L13.3 10 Z" fill="#9966FF" opacity="0.5"/>
      <circle cx="14" cy="22" r="2" fill="none" stroke="#9966FF" strokeWidth="0.7" opacity="0.5"/>
      <line x1="12" y1="22" x2="16" y2="22" stroke="#9966FF" strokeWidth="0.5" opacity="0.4"/>
      <line x1="14" y1="20" x2="14" y2="24" stroke="#9966FF" strokeWidth="0.5" opacity="0.4"/>
    </svg>
  ),
  airtaxi_lower_sky_market: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="3" width="6" height="22" rx="1" fill="#00000060" stroke={a} strokeWidth="0.7" opacity="0.6"/>
      <rect x="7" y="12" width="14" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
      <path d="M4 8 Q6 6 8 8 Q10 10 12 8" stroke="#404050" strokeWidth="0.7" fill="none" opacity="0.5"/>
      <path d="M3 13 Q5 11 7 13 Q9 15 11 13" stroke="#404050" strokeWidth="0.6" fill="none" opacity="0.4"/>
      <path d="M14 18 L16 15 L18 18 L16 21 Z" fill="none" stroke="#FF6622" strokeWidth="0.5" opacity="0.3"/>
      <circle cx="14" cy="4.5" r="1.5" fill={a} opacity="0.5" style={{animation:'lens-flicker 2.2s ease-in-out infinite'}}/>
      <rect x="4" y="24" width="20" height="3" rx="0.5" fill="#00000060" stroke={a} strokeWidth="0.5" opacity="0.5"/>
    </svg>
  ),
  airtaxi_senate_district: (a) => (
    <svg viewBox="0 0 28 28" width="26" height="26" style={{pointerEvents:'none'}}>
      <rect x="11" y="4" width="6" height="20" rx="0.5" fill="#00000060" stroke="#9966FF" strokeWidth="0.8" opacity="0.8"/>
      <rect x="8" y="3" width="12" height="4" rx="0.5" fill="#00000060" stroke="#9966FF" strokeWidth="0.7" opacity="0.7"/>
      <rect x="9" y="2" width="10" height="3" rx="0.3" fill="#9966FF" opacity="0.25"/>
      <rect x="7" y="12" width="14" height="3" rx="0.5" fill="#00000060" stroke="#9966FF" strokeWidth="0.5" opacity="0.6"/>
      <rect x="8" y="10" width="4" height="3" fill="#9966FF" opacity="0.15"/>
      <rect x="16" y="10" width="4" height="3" fill="#9966FF" opacity="0.15"/>
      <circle cx="14" cy="5" r="2" fill="#9966FF" opacity="0.7" style={{animation:'lens-flicker 2.3s ease-in-out infinite'}}/>
      <rect x="9" y="18" width="10" height="4" rx="0.5" fill="#00000060" stroke="#9966FF" strokeWidth="0.4" opacity="0.5"/>
      <rect x="4" y="23" width="20" height="4" rx="0.5" fill="#00000060" stroke="#9966FF" strokeWidth="0.6" opacity="0.6"/>
    </svg>
  ),
};

function WorldObjectSprite({ kind, accent, id }) {
  if (id && WORLD_OBJECT_SPRITES[id]) return WORLD_OBJECT_SPRITES[id](accent);
  const s = { pointerEvents: 'none' };
  const dim = '#00000060';
  if (kind === 'terminal') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="4" y="4" width="20" height="14" rx="1.5" fill={dim} />
      <rect x="4" y="4" width="20" height="14" rx="1.5" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.7" />
      <rect x="6" y="6" width="16" height="9" fill={accent} opacity="0.18" />
      <line x1="6" y1="8.5" x2="22" y2="8.5" stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <line x1="6" y1="11" x2="22" y2="11" stroke={accent} strokeWidth="0.5" opacity="0.35" />
      <line x1="6" y1="13.5" x2="16" y2="13.5" stroke={accent} strokeWidth="0.5" opacity="0.25" />
      <rect x="10" y="18" width="8" height="1.5" rx="0.5" fill={accent} opacity="0.5" />
      <rect x="6" y="20" width="16" height="3" rx="0.8" fill={dim} />
      <rect x="6" y="20" width="16" height="3" rx="0.8" fill="none" stroke={accent} strokeWidth="0.6" opacity="0.4" />
      <circle cx="22" cy="5.5" r="0.8" fill={accent} opacity="0.8" style={{ animation: 'lens-flicker 2.4s ease-in-out infinite' }} />
    </svg>
  );
  if (kind === 'crate') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="4" y="11" width="20" height="14" rx="1" fill={dim} />
      <rect x="4" y="11" width="20" height="14" rx="1" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.6" />
      <line x1="14" y1="11" x2="14" y2="25" stroke={accent} strokeWidth="0.6" opacity="0.35" />
      <line x1="4" y1="18" x2="24" y2="18" stroke={accent} strokeWidth="0.6" opacity="0.35" />
      <polygon points="4,11 14,5 24,11" fill={dim} />
      <polygon points="4,11 14,5 24,11" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.5" />
      <line x1="4" y1="11" x2="14" y2="5" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <line x1="24" y1="11" x2="14" y2="5" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <circle cx="8" cy="11" r="0.8" fill={accent} opacity="0.6" />
      <circle cx="20" cy="11" r="0.8" fill={accent} opacity="0.6" />
      <circle cx="4" cy="25" r="0.7" fill={accent} opacity="0.5" />
      <circle cx="24" cy="25" r="0.7" fill={accent} opacity="0.5" />
    </svg>
  );
  if (kind === 'booth') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <path d="M4 26 L4 14 Q4 8 14 8 Q24 8 24 14 L24 26 Z" fill={dim} />
      <path d="M4 26 L4 14 Q4 8 14 8 Q24 8 14 8" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.5" />
      <rect x="5" y="14" width="18" height="2" rx="0.5" fill={accent} opacity="0.4" />
      <rect x="7" y="17" width="14" height="8" rx="0.5" fill={accent} opacity="0.1" />
      <rect x="6" y="25" width="16" height="1.5" rx="0.5" fill={accent} opacity="0.5" />
      <circle cx="10" cy="19" r="1.2" fill={accent} opacity="0.4" />
      <circle cx="14" cy="19" r="1.2" fill={accent} opacity="0.4" />
      <circle cx="18" cy="19" r="1.2" fill={accent} opacity="0.4" />
    </svg>
  );
  if (kind === 'evidence') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <circle cx="14" cy="14" r="6" fill="#FF6622" opacity="0.18" />
      <path d="M14 5 L15.2 11 L21 8 L17 13 L23 14 L17 15 L21 20 L15.2 17 L14 23 L12.8 17 L7 20 L11 15 L5 14 L11 13 L7 8 L12.8 11 Z" fill="#FF6622" opacity="0.5" />
      <circle cx="14" cy="14" r="2" fill="#FF8844" opacity="0.7" />
      <circle cx="8" cy="8" r="0.7" fill="#FF6622" opacity="0.6" />
      <circle cx="20" cy="9" r="0.5" fill="#FF6622" opacity="0.5" />
      <circle cx="19" cy="20" r="0.6" fill="#FF6622" opacity="0.5" />
      <rect x="4" y="24" width="20" height="2" rx="0.5" fill="#E8A020" opacity="0.5" />
    </svg>
  );
  if (kind === 'vista') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="2" width="24" height="24" rx="1.5" fill={dim} />
      <path d="M2 20 Q8 12 14 16 Q20 20 26 10 L26 26 L2 26 Z" fill={accent} opacity="0.12" />
      <path d="M2 20 Q8 12 14 16 Q20 20 26 10" stroke={accent} strokeWidth="0.8" fill="none" opacity="0.5" />
      <line x1="9" y1="2" x2="6" y2="10" stroke={accent} strokeWidth="0.7" opacity="0.35" />
      <line x1="19" y1="2" x2="22" y2="9" stroke={accent} strokeWidth="0.7" opacity="0.3" />
      <ellipse cx="14" cy="8" rx="3" ry="1.5" fill={accent} opacity="0.25" />
    </svg>
  );
  if (kind === 'valve') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <circle cx="14" cy="14" r="9" fill={dim} />
      <circle cx="14" cy="14" r="9" fill="none" stroke={accent} strokeWidth="1" opacity="0.6" />
      <circle cx="14" cy="14" r="5" fill="none" stroke={accent} strokeWidth="0.7" opacity="0.4" />
      <circle cx="14" cy="14" r="2" fill={accent} opacity="0.5" />
      <line x1="14" y1="5" x2="14" y2="12" stroke={accent} strokeWidth="1.5" opacity="0.7" />
      <line x1="23" y1="14" x2="16" y2="14" stroke={accent} strokeWidth="1.5" opacity="0.7" />
      <line x1="14" y1="23" x2="14" y2="16" stroke={accent} strokeWidth="1.5" opacity="0.7" />
      <line x1="5" y1="14" x2="12" y2="14" stroke={accent} strokeWidth="1.5" opacity="0.7" />
      <path d="M14 10 L16 12" stroke="#FF6622" strokeWidth="1" opacity="0.8" />
    </svg>
  );
  if (kind === 'panel') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="4" width="22" height="20" rx="1.5" fill={dim} />
      <rect x="3" y="4" width="22" height="20" rx="1.5" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.6" />
      <rect x="3" y="4" width="22" height="4" rx="1" fill="#FF442260" />
      <rect x="3" y="4" width="22" height="4" fill="none" stroke="#FF4422" strokeWidth="0.5" opacity="0.5" />
      {[6,10,14,18,22].map((x,i) => <circle key={i} cx={x} cy="10.5" r="1.2" fill={i%3===0?accent:i%3===1?'#FF4422':'#6FD9A0'} opacity="0.7" />)}
      {[6,10,14,18,22].map((x,i) => <rect key={i} x={x-1.5} y="14" width="3" height="5" rx="0.8" fill={accent} opacity={i%2===0?0.5:0.2} />)}
    </svg>
  );
  if (kind === 'comlink') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <ellipse cx="14" cy="17" rx="8" ry="6" fill={dim} />
      <ellipse cx="14" cy="17" rx="8" ry="6" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.6" />
      <line x1="14" y1="11" x2="14" y2="4" stroke={accent} strokeWidth="1" opacity="0.7" />
      <circle cx="14" cy="3.5" r="0.8" fill={accent} opacity="0.8" />
      <path d="M9 10 Q11 8 14 8 Q17 8 19 10" stroke={accent} strokeWidth="0.8" fill="none" opacity="0.5" style={{ animation: 'door-pulse 2s ease-in-out infinite' }} />
      <path d="M7 12 Q10 9 14 9 Q18 9 21 12" stroke={accent} strokeWidth="0.6" fill="none" opacity="0.3" style={{ animation: 'door-pulse 2s ease-in-out 0.5s infinite' }} />
      <circle cx="14" cy="17" r="2.5" fill={accent} opacity="0.25" />
      <circle cx="14" cy="17" r="1" fill={accent} opacity="0.6" />
    </svg>
  );
  if (kind === 'safe') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="4" width="22" height="20" rx="1.5" fill={dim} />
      <rect x="3" y="4" width="22" height="20" rx="1.5" fill="none" stroke={accent} strokeWidth="1" opacity="0.7" />
      <circle cx="14" cy="14" r="5" fill="none" stroke={accent} strokeWidth="1.2" opacity="0.6" />
      <circle cx="14" cy="14" r="2.5" fill={accent} opacity="0.3" />
      <line x1="14" y1="9" x2="14" y2="11" stroke={accent} strokeWidth="1.5" opacity="0.7" />
      <line x1="19" y1="14" x2="17" y2="14" stroke={accent} strokeWidth="1.5" opacity="0.7" />
      <circle cx="20" cy="8" r="1.5" fill={accent} opacity="0.5" />
      <rect x="19" y="8" width="3" height="5" rx="0.5" fill={accent} opacity="0.3" />
      <circle cx="5" cy="5.5" r="0.8" fill={accent} opacity="0.5" />
      <circle cx="23" cy="5.5" r="0.8" fill={accent} opacity="0.5" />
      <circle cx="5" cy="22.5" r="0.8" fill={accent} opacity="0.5" />
      <circle cx="23" cy="22.5" r="0.8" fill={accent} opacity="0.5" />
    </svg>
  );
  if (kind === 'board') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="3" width="22" height="22" rx="1" fill={dim} />
      <rect x="3" y="3" width="22" height="22" rx="1" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.5" />
      <rect x="5" y="5" width="10" height="7" rx="0.5" fill={accent} opacity="0.3" />
      <rect x="16" y="5" width="7" height="4" rx="0.5" fill={accent} opacity="0.15" />
      <rect x="16" y="10" width="7" height="4" rx="0.5" fill={accent} opacity="0.2" />
      <rect x="5" y="13" width="7" height="4" rx="0.5" fill={accent} opacity="0.2" />
      <rect x="5" y="18" width="18" height="4" rx="0.5" fill="#FF882240" />
      <line x1="7" y1="19.5" x2="21" y2="19.5" stroke="#FF8822" strokeWidth="0.6" opacity="0.6" />
      <circle cx="10" cy="6" r="0.8" fill="#FF4422" opacity="0.7" />
      <circle cx="21" cy="7" r="0.7" fill={accent} opacity="0.7" />
    </svg>
  );
  if (kind === 'hologram') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="9" y="22" width="10" height="3" rx="0.8" fill={dim} />
      <rect x="9" y="22" width="10" height="3" rx="0.8" fill="none" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <line x1="14" y1="22" x2="14" y2="18" stroke={accent} strokeWidth="0.8" opacity="0.5" />
      <polygon points="14,5 20,15 14,19 8,15" fill={accent} opacity="0.2" style={{ animation: 'door-pulse 2.2s ease-in-out infinite' }} />
      <polygon points="14,5 20,15 14,19 8,15" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.6" style={{ animation: 'door-pulse 2.2s ease-in-out infinite' }} />
      <circle cx="14" cy="12" r="1.5" fill={accent} opacity="0.5" style={{ animation: 'lens-flicker 1.8s ease-in-out infinite' }} />
    </svg>
  );
  if (kind === 'beacon') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="11" y="10" width="6" height="16" rx="0.8" fill={dim} />
      <rect x="11" y="10" width="6" height="16" rx="0.8" fill="none" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <rect x="9" y="24" width="10" height="3" rx="0.5" fill={dim} />
      <circle cx="14" cy="7" r="3.5" fill={accent} opacity="0.25" style={{ animation: 'door-pulse 1.4s ease-in-out infinite' }} />
      <circle cx="14" cy="7" r="2" fill={accent} opacity="0.7" style={{ animation: 'door-pulse 1.4s ease-in-out infinite' }} />
      <path d="M8 10 Q10 8 14 7" stroke={accent} strokeWidth="0.6" fill="none" opacity="0.4" />
      <path d="M20 10 Q18 8 14 7" stroke={accent} strokeWidth="0.6" fill="none" opacity="0.4" />
      <path d="M5 13 Q8 9 14 7" stroke={accent} strokeWidth="0.5" fill="none" opacity="0.25" />
    </svg>
  );
  if (kind === 'stall') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <path d="M2 12 L14 6 L26 12 L26 13 L2 13 Z" fill={dim} />
      <path d="M2 12 L14 6 L26 12" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.6" />
      <rect x="2" y="13" width="24" height="2" fill={accent} opacity="0.3" />
      <rect x="4" y="15" width="20" height="10" rx="0.5" fill={dim} />
      <rect x="6" y="17" width="4" height="4" rx="0.5" fill={accent} opacity="0.25" />
      <rect x="12" y="16" width="4" height="3" rx="0.5" fill={accent} opacity="0.2" />
      <rect x="18" y="17" width="4" height="4" rx="0.5" fill={accent} opacity="0.3" />
      <line x1="14" y1="4" x2="14" y2="13" stroke={accent} strokeWidth="0.8" opacity="0.4" />
      <line x1="8" y1="5.5" x2="8" y2="13" stroke={accent} strokeWidth="0.5" opacity="0.25" />
      <line x1="20" y1="5.5" x2="20" y2="13" stroke={accent} strokeWidth="0.5" opacity="0.25" />
    </svg>
  );
  if (kind === 'barrel') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <ellipse cx="14" cy="8" rx="8" ry="3" fill={dim} />
      <ellipse cx="14" cy="8" rx="8" ry="3" fill="none" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <rect x="6" y="8" width="16" height="15" fill={dim} />
      <rect x="6" y="8" width="16" height="15" fill="none" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <ellipse cx="14" cy="23" rx="8" ry="3" fill={dim} />
      <ellipse cx="14" cy="23" rx="8" ry="3" fill="none" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <line x1="6" y1="12" x2="22" y2="12" stroke={accent} strokeWidth="0.8" opacity="0.4" />
      <line x1="6" y1="18" x2="22" y2="18" stroke={accent} strokeWidth="0.8" opacity="0.4" />
      <circle cx="10" cy="10" r="0.8" fill="#FF4422" opacity="0.6" />
      <circle cx="18" cy="10" r="0.8" fill="#FF4422" opacity="0.6" />
    </svg>
  );
  if (kind === 'lamp') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <line x1="14" y1="6" x2="14" y2="20" stroke={accent} strokeWidth="1" opacity="0.5" />
      <line x1="14" y1="6" x2="20" y2="9" stroke={accent} strokeWidth="1" opacity="0.5" />
      <circle cx="20" cy="9" r="2.5" fill={accent} opacity="0.7" style={{ animation: 'lens-flicker 3s ease-in-out infinite' }} />
      <ellipse cx="20" cy="13" rx="5" ry="3" fill={accent} opacity="0.12" />
      <ellipse cx="20" cy="16" rx="7" ry="4" fill={accent} opacity="0.07" />
      <rect x="11" y="20" width="6" height="5" rx="0.5" fill={dim} />
    </svg>
  );
  if (kind === 'fuel_rig') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="9" y="16" width="10" height="8" rx="1" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.8" />
      <ellipse cx="14" cy="16" rx="5" ry="1.8" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.7" />
      <ellipse cx="14" cy="24" rx="5" ry="1.8" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <line x1="14" y1="7" x2="14" y2="16" stroke={accent} strokeWidth="1.2" opacity="0.7" />
      <rect x="13" y="7" width="8" height="2" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <path d="M21 8 Q25 8 25 12 L25 15" stroke={accent} strokeWidth="1" fill="none" opacity="0.6" />
      <rect x="24" y="15" width="2" height="3" rx="0.5" fill="#FF6600" opacity="0.7" />
      <circle cx="11" cy="19" r="1" fill={accent} opacity="0.4" />
      <circle cx="11" cy="22" r="0.6" fill="#FF3300" opacity="0.5" style={{ animation:'world-obj-pulse 1.8s ease-in-out infinite' }} />
    </svg>
  );
  if (kind === 'manifest_terminal') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="6" width="22" height="16" rx="1" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.9" />
      <rect x="4" y="7" width="20" height="2" fill={accent} opacity="0.25" />
      <line x1="5" y1="11" x2="23" y2="11" stroke={accent} strokeWidth="0.5" opacity="0.35" />
      <line x1="5" y1="13" x2="20" y2="13" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <line x1="5" y1="15" x2="22" y2="15" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <rect x="5" y="17" width="18" height="1.5" rx="0.3" fill="#FF2222" opacity="0.45" style={{ animation:'world-obj-pulse 1.2s ease-in-out infinite' }} />
      <rect x="5" y="19" width="8" height="1" rx="0.3" fill={accent} opacity="0.2" />
      <rect x="5" y="22" width="18" height="1.5" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
    </svg>
  );
  if (kind === 'impound_notice') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="3" width="22" height="22" rx="1" fill="#330000" stroke="#FF2222" strokeWidth="1" opacity="0.9" />
      <rect x="3" y="3" width="22" height="5" fill="#CC0000" opacity="0.6" />
      <circle cx="14" cy="5" r="2" fill="none" stroke="#FF4444" strokeWidth="0.7" opacity="0.8" />
      <rect x="12.5" y="4" width="3" height="2.5" rx="0.3" fill="none" stroke="#FF4444" strokeWidth="0.5" opacity="0.7" />
      <line x1="5" y1="11" x2="23" y2="11" stroke="#FF4444" strokeWidth="0.4" opacity="0.5" />
      <line x1="5" y1="13" x2="20" y2="13" stroke="#FF4444" strokeWidth="0.4" opacity="0.4" />
      <line x1="5" y1="15" x2="22" y2="15" stroke="#FF4444" strokeWidth="0.4" opacity="0.4" />
      <line x1="5" y1="17" x2="18" y2="17" stroke="#FF4444" strokeWidth="0.4" opacity="0.4" />
      <rect x="8" y="19" width="12" height="4" rx="0.5" fill="none" stroke="#FF2222" strokeWidth="0.6" opacity="0.5" />
      <line x1="14" y1="19" x2="14" y2="23" stroke="#FF2222" strokeWidth="0.5" opacity="0.4" />
    </svg>
  );
  if (kind === 'orbital_viewport') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <circle cx="14" cy="14" r="11" fill="#000811" stroke={accent} strokeWidth="1.2" opacity="0.9" />
      <circle cx="14" cy="14" r="11" fill="none" stroke={accent} strokeWidth="2.5" opacity="0.15" />
      <circle cx="6" cy="6" r="0.7" fill={accent} opacity="0.7" />
      <circle cx="10" cy="5" r="0.5" fill={accent} opacity="0.5" />
      <circle cx="18" cy="7" r="0.6" fill={accent} opacity="0.6" />
      <circle cx="22" cy="10" r="0.5" fill={accent} opacity="0.5" />
      <circle cx="8" cy="18" r="0.4" fill={accent} opacity="0.4" />
      <circle cx="20" cy="20" r="0.5" fill={accent} opacity="0.4" />
      <path d="M6 16 L9 14 L14 17 L19 12 L22 13" stroke={accent} strokeWidth="0.8" fill="none" opacity="0.5" />
      <path d="M3 14 Q14 3 25 14" stroke={accent} strokeWidth="0.4" fill="none" opacity="0.2" />
      <circle cx="4.5" cy="9.5" r="0.8" fill="none" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <circle cx="23.5" cy="9.5" r="0.8" fill="none" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <circle cx="4.5" cy="18.5" r="0.8" fill="none" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <circle cx="23.5" cy="18.5" r="0.8" fill="none" stroke={accent} strokeWidth="0.5" opacity="0.4" />
    </svg>
  );
  if (kind === 'tether_locker') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="6" width="24" height="18" rx="1" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.8" />
      <rect x="2" y="6" width="24" height="2" fill={accent} opacity="0.15" />
      <line x1="10" y1="6" x2="10" y2="24" stroke={accent} strokeWidth="0.6" opacity="0.4" />
      <line x1="18" y1="6" x2="18" y2="24" stroke={accent} strokeWidth="0.6" opacity="0.4" />
      <line x1="14" y1="6" x2="14" y2="24" stroke={accent} strokeWidth="0.5" opacity="0.25" />
      <rect x="3.5" y="8" width="5" height="14" rx="0.3" fill={accent} opacity="0.07" />
      <rect x="5.5" y="14" width="1.5" height="1" rx="0.2" fill={accent} opacity="0.5" />
      <rect x="13.5" y="14" width="1.5" height="1" rx="0.2" fill={accent} opacity="0.5" />
      <rect x="21.5" y="14" width="1.5" height="1" rx="0.2" fill={accent} opacity="0.5" />
      <path d="M6 8 Q5 12 4 20" stroke={accent} strokeWidth="0.8" fill="none" opacity="0.5" />
      <circle cx="4" cy="20" r="1.2" fill={accent} opacity="0.35" />
    </svg>
  );
  if (kind === 'transit_pod') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <line x1="2" y1="10" x2="26" y2="10" stroke={accent} strokeWidth="0.8" opacity="0.6" />
      <line x1="2" y1="12" x2="26" y2="12" stroke={accent} strokeWidth="0.8" opacity="0.6" />
      <ellipse cx="14" cy="17" rx="9" ry="5" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.9" />
      <ellipse cx="14" cy="15" rx="7" ry="3.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <circle cx="14" cy="15" r="1.5" fill={accent} opacity="0.3" />
      <circle cx="11" cy="11" r="0.5" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <circle cx="17" cy="11" r="0.5" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="10" y="20" width="8" height="2" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.4" />
    </svg>
  );
  if (kind === 'welding_rig') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="20" width="10" height="4" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.7" />
      <rect x="7" y="6" width="2.5" height="14" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <rect x="9" y="8" width="12" height="2" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <circle cx="21" cy="9" r="3" fill="#FF5500" opacity="0.75" style={{ animation:'world-obj-pulse 0.8s ease-in-out infinite' }} />
      <circle cx="21" cy="9" r="1.5" fill="#FFBB00" opacity="0.6" />
      <line x1="24" y1="6" x2="26" y2="4" stroke="#FF8800" strokeWidth="0.7" opacity="0.6" />
      <line x1="24" y1="9" x2="27" y2="9" stroke="#FFAA00" strokeWidth="0.7" opacity="0.5" />
      <line x1="24" y1="12" x2="26" y2="14" stroke="#FF6600" strokeWidth="0.7" opacity="0.5" />
      <line x1="21" y1="12" x2="21" y2="15" stroke="#FF7700" strokeWidth="0.7" opacity="0.4" />
      <circle cx="26" cy="4" r="0.5" fill="#FFDD00" opacity="0.7" />
      <circle cx="27" cy="9" r="0.5" fill="#FFAA00" opacity="0.6" />
    </svg>
  );
  if (kind === 'wayfinding_plaque') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="5" width="24" height="18" rx="1" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.85" />
      <rect x="2" y="5" width="24" height="4" rx="1" fill={accent} opacity="0.2" />
      <line x1="4" y1="12" x2="24" y2="12" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="4" y1="14" x2="20" y2="14" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="4" y1="16" x2="22" y2="16" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="4" y1="18" x2="18" y2="18" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <polygon points="18,20 22,21.5 18,23" fill={accent} opacity="0.5" />
      <circle cx="5" cy="14" r="0.6" fill={accent} opacity="0.4" />
      <circle cx="5" cy="16" r="0.6" fill={accent} opacity="0.4" />
      <circle cx="5" cy="18" r="0.6" fill={accent} opacity="0.4" />
    </svg>
  );
  if (kind === 'blast_door_panel') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="2" width="24" height="24" rx="1" fill="#221100" stroke="#FF6600" strokeWidth="1" opacity="0.9" />
      <line x1="2" y1="2" x2="26" y2="26" stroke="#FF6600" strokeWidth="1.5" opacity="0.25" />
      <line x1="26" y1="2" x2="2" y2="26" stroke="#FF6600" strokeWidth="1.5" opacity="0.25" />
      <rect x="8" y="6" width="12" height="9" rx="0.5" fill="none" stroke="#FF6600" strokeWidth="0.7" opacity="0.5" />
      <line x1="10" y1="8" x2="12" y2="6" stroke="#FF6600" strokeWidth="0.5" opacity="0.4" />
      <line x1="18" y1="8" x2="16" y2="6" stroke="#FF6600" strokeWidth="0.5" opacity="0.4" />
      <circle cx="14" cy="18" r="4" fill="none" stroke="#FF6600" strokeWidth="0.8" opacity="0.6" />
      <line x1="14" y1="14" x2="14" y2="22" stroke="#FF6600" strokeWidth="0.7" opacity="0.5" />
      <line x1="10" y1="18" x2="18" y2="18" stroke="#FF6600" strokeWidth="0.7" opacity="0.5" />
      <line x1="11" y1="15" x2="17" y2="21" stroke="#FF6600" strokeWidth="0.5" opacity="0.4" />
      <line x1="17" y1="15" x2="11" y2="21" stroke="#FF6600" strokeWidth="0.5" opacity="0.4" />
    </svg>
  );
  if (kind === 'holo_projector') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <ellipse cx="14" cy="22" rx="8" ry="2.5" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.7" />
      <circle cx="14" cy="22" r="1.5" fill={accent} opacity="0.4" />
      <path d="M8 22 L10 10 M20 22 L18 10" stroke={accent} strokeWidth="0.5" fill="none" opacity="0.3" />
      <polygon points="10,10 18,10 16,4 12,4" fill={accent} opacity="0.12" style={{ animation:'world-obj-pulse 2s ease-in-out infinite' }} />
      <polygon points="10,10 18,10 16,4 12,4" fill="none" stroke={accent} strokeWidth="0.6" opacity="0.4" />
      <ellipse cx="14" cy="7" rx="3" ry="1.5" fill="none" stroke={accent} strokeWidth="0.6" opacity="0.5" style={{ animation:'world-obj-pulse 2s ease-in-out infinite' }} />
    </svg>
  );
  if (kind === 'secure_locker') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="4" width="24" height="20" rx="1" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.85" />
      <line x1="14" y1="4" x2="14" y2="24" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <line x1="2" y1="11" x2="26" y2="11" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <line x1="2" y1="17" x2="26" y2="17" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <rect x="3" y="5" width="10" height="5.5" rx="0.3" fill={accent} opacity="0.06" />
      <rect x="15" y="5" width="10" height="5.5" rx="0.3" fill="#CC0000" opacity="0.12" />
      <rect x="15" y="5" width="10" height="5.5" rx="0.3" fill="none" stroke="#FF2222" strokeWidth="0.5" opacity="0.5" />
      <rect x="3" y="12" width="10" height="5" rx="0.3" fill="#FF8800" opacity="0.08" />
      <rect x="3" y="12" width="10" height="5" rx="0.3" fill="none" stroke="#FF8800" strokeWidth="0.4" opacity="0.4" />
      <circle cx="8" cy="8" r="0.8" fill={accent} opacity="0.35" />
      <circle cx="20" cy="8" r="0.8" fill="#FF2222" opacity="0.5" />
      <circle cx="8" cy="14.5" r="0.8" fill={accent} opacity="0.35" />
    </svg>
  );
  if (kind === 'intercept_wall') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="3" width="24" height="22" rx="0.5" fill="#050B1A" stroke={accent} strokeWidth="0.7" opacity="0.85" />
      <rect x="14" y="3" width="1" height="22" fill={accent} opacity="0.08" />
      <line x1="14" y1="3" x2="14" y2="25" stroke={accent} strokeWidth="0.5" opacity="0.2" />
      <ellipse cx="8" cy="14" rx="2.5" ry="2.5" fill={accent} opacity="0.15" />
      <rect x="7" y="16" width="3" height="7" rx="0.5" fill={accent} opacity="0.12" />
      <ellipse cx="20" cy="14" rx="2.5" ry="2.5" fill={accent} opacity="0.15" />
      <rect x="19" y="16" width="3" height="7" rx="0.5" fill={accent} opacity="0.12" />
      <path d="M10 15 L12 17" stroke={accent} strokeWidth="0.6" opacity="0.35" />
      <line x1="2" y1="10" x2="26" y2="10" stroke={accent} strokeWidth="0.4" opacity="0.2" />
    </svg>
  );
  if (kind === 'holo_table') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <ellipse cx="14" cy="19" rx="11" ry="4" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.8" />
      <ellipse cx="14" cy="18" rx="9" ry="3" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.4" />
      <line x1="7" y1="19" x2="7" y2="24" stroke={accent} strokeWidth="0.6" opacity="0.4" />
      <line x1="21" y1="19" x2="21" y2="24" stroke={accent} strokeWidth="0.6" opacity="0.4" />
      <path d="M10 14 L10 9 L14 5 L18 9 L18 14" fill="none" stroke={accent} strokeWidth="0.6" opacity="0.4" style={{ animation:'world-obj-pulse 2.2s ease-in-out infinite' }} />
      <line x1="10" y1="11" x2="18" y2="11" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <ellipse cx="14" cy="5" rx="3" ry="1.2" fill={accent} opacity="0.15" style={{ animation:'world-obj-pulse 2.2s ease-in-out infinite' }} />
    </svg>
  );
  if (kind === 'ordnance_crate') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="8" width="22" height="16" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.85" />
      <path d="M3 8 L5 6 L23 6 L25 8" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.7" />
      <line x1="14" y1="6" x2="14" y2="8" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <rect x="3" y="8" width="3" height="16" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="22" y="8" width="3" height="16" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <polygon points="14,10 11,16 17,16" fill="none" stroke="#FF8800" strokeWidth="0.7" opacity="0.6" />
      <line x1="14" y1="12" x2="14" y2="14.5" stroke="#FF8800" strokeWidth="0.6" opacity="0.5" />
      <circle cx="14" cy="15.2" r="0.5" fill="#FF8800" opacity="0.6" />
      <line x1="8" y1="8" x2="8" y2="24" stroke={accent} strokeWidth="0.3" opacity="0.2" />
      <line x1="20" y1="8" x2="20" y2="24" stroke={accent} strokeWidth="0.3" opacity="0.2" />
    </svg>
  );
  if (kind === 'armory_locker') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="4" width="22" height="22" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.8" opacity="0.9" />
      <rect x="3" y="4" width="3" height="22" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="22" y="4" width="3" height="22" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="3" y="24" width="22" height="2" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.4" />
      <circle cx="5" cy="6" r="0.8" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <circle cx="23" cy="6" r="0.8" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <line x1="14" y1="4" x2="14" y2="26" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <rect x="9" y="13" width="10" height="8" rx="0.5" fill="#003300" stroke="#00CC44" strokeWidth="0.6" opacity="0.7" />
      <path d="M11 16 L13 18 L17 14" fill="none" stroke="#00CC44" strokeWidth="0.7" opacity="0.6" />
      <line x1="7" y1="9" x2="9" y2="11" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="21" y1="9" x2="19" y2="11" stroke={accent} strokeWidth="0.4" opacity="0.3" />
    </svg>
  );
  if (kind === 'cable_truss') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="4" width="24" height="5" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.8" />
      <rect x="4" y="4" width="3" height="2" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="12" y="4" width="3" height="2" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="20" y="4" width="3" height="2" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <line x1="5" y1="9" x2="5" y2="24" stroke={accent} strokeWidth="1.2" opacity="0.5" />
      <line x1="14" y1="9" x2="14" y2="22" stroke={accent} strokeWidth="1" opacity="0.45" />
      <line x1="22" y1="9" x2="22" y2="26" stroke="#FF8800" strokeWidth="1.2" opacity="0.55" />
      <rect x="4" y="13" width="3" height="1.5" rx="0.3" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="12" y="16" width="3" height="1.5" rx="0.3" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <rect x="20" y="12" width="3" height="1.5" rx="0.3" fill={dim} stroke="#FF8800" strokeWidth="0.4" opacity="0.5" />
      <circle cx="5" cy="24" r="1.2" fill={accent} opacity="0.3" />
    </svg>
  );
  if (kind === 'coolant_pipe') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="11" width="24" height="6" rx="3" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.8" />
      <rect x="2" y="12" width="24" height="1" fill={accent} opacity="0.12" />
      <circle cx="14" cy="14" r="4" fill="none" stroke={accent} strokeWidth="0.6" opacity="0.5" />
      <line x1="14" y1="10" x2="14" y2="18" stroke={accent} strokeWidth="0.7" opacity="0.4" />
      <line x1="10" y1="14" x2="18" y2="14" stroke={accent} strokeWidth="0.7" opacity="0.4" />
      <line x1="11" y1="11" x2="17" y2="17" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <line x1="17" y1="11" x2="11" y2="17" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <circle cx="22" cy="9" r="2.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <line x1="22" y1="9" x2="22" y2="11" stroke={accent} strokeWidth="0.5" opacity="0.4" style={{ transformOrigin:'22px 9px', animation:'spin 4s linear infinite' }} />
      <circle cx="6" cy="9" r="1.5" fill="#FF8800" opacity="0.5" style={{ animation:'world-obj-pulse 1.5s ease-in-out infinite' }} />
    </svg>
  );
  if (kind === 'sabacc_table') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <ellipse cx="14" cy="17" rx="12" ry="8" fill="#042604" stroke={accent} strokeWidth="0.7" opacity="0.9" />
      <ellipse cx="14" cy="17" rx="10" ry="6.5" fill="none" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <rect x="8" y="10" width="4" height="6" rx="0.5" fill="#FAFAFA" stroke={accent} strokeWidth="0.4" opacity="0.6" style={{ transform:'rotate(-12deg)', transformOrigin:'10px 13px' }} />
      <rect x="12" y="9" width="4" height="6" rx="0.5" fill="#EEEEFF" stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <rect x="16" y="10" width="4" height="6" rx="0.5" fill="#FFEEEE" stroke={accent} strokeWidth="0.4" opacity="0.6" style={{ transform:'rotate(12deg)', transformOrigin:'18px 13px' }} />
      <circle cx="7" cy="20" r="1.5" fill="#D4AF37" opacity="0.7" />
      <circle cx="14" cy="22" r="1.5" fill="#D4AF37" opacity="0.7" />
      <circle cx="21" cy="20" r="1.5" fill="#D4AF37" opacity="0.7" />
    </svg>
  );
  if (kind === 'surveillance_cam') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="5" y="2" width="5" height="3" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <line x1="7.5" y1="5" x2="7.5" y2="9" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <path d="M5 9 Q14 12 23 9" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.7" />
      <ellipse cx="14" cy="12" rx="6" ry="5" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.85" />
      <circle cx="14" cy="12" r="3.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <circle cx="14" cy="12" r="1.5" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <circle cx="14" cy="12" r="0.7" fill="#FF0000" opacity="0.7" style={{ animation:'world-obj-pulse 1s ease-in-out infinite' }} />
      <line x1="4" y1="9" x2="4" y2="16" stroke={accent} strokeWidth="0.5" opacity="0.3" />
      <line x1="24" y1="9" x2="24" y2="16" stroke={accent} strokeWidth="0.5" opacity="0.3" />
    </svg>
  );
  if (kind === 'czerka_terminal') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="4" y="6" width="20" height="18" rx="1" fill={dim} stroke="#CC8800" strokeWidth="0.8" opacity="0.9" />
      <rect x="4" y="6" width="20" height="3.5" fill="#CC8800" opacity="0.25" />
      <path d="M8 7.5 A5 5 0 0 1 13 7.5" fill="none" stroke="#FFAA00" strokeWidth="0.8" opacity="0.7" />
      <line x1="6" y1="13" x2="22" y2="13" stroke="#CC8800" strokeWidth="0.4" opacity="0.3" />
      <line x1="6" y1="15" x2="19" y2="15" stroke="#CC8800" strokeWidth="0.4" opacity="0.3" />
      <line x1="6" y1="17" x2="21" y2="17" stroke="#CC8800" strokeWidth="0.4" opacity="0.3" />
      <rect x="6" y="19" width="10" height="1" rx="0.3" fill="#CC8800" opacity="0.2" />
      <rect x="4" y="22" width="20" height="2" rx="0.3" fill={dim} stroke="#CC8800" strokeWidth="0.4" opacity="0.5" />
    </svg>
  );
  if (kind === 'fuel_canister_rack') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="4" width="24" height="20" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <rect x="2" y="4" width="24" height="2" fill={accent} opacity="0.15" />
      <ellipse cx="6" cy="8" rx="2.5" ry="1" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <rect x="3.5" y="8" width="5" height="12" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.7" />
      <ellipse cx="6" cy="20" rx="2.5" ry="1" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <ellipse cx="13" cy="8" rx="2.5" ry="1" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <rect x="10.5" y="8" width="5" height="12" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.7" />
      <ellipse cx="13" cy="20" rx="2.5" ry="1" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.5" />
      <ellipse cx="20" cy="8" rx="2.5" ry="1" fill={dim} stroke="#FF8800" strokeWidth="0.5" opacity="0.6" />
      <rect x="17.5" y="8" width="5" height="12" fill={dim} stroke="#FF8800" strokeWidth="0.5" opacity="0.7" />
      <ellipse cx="20" cy="20" rx="2.5" ry="1" fill={dim} stroke="#FF8800" strokeWidth="0.5" opacity="0.5" />
      <line x1="19" y1="14" x2="22" y2="14" stroke="#FF8800" strokeWidth="0.8" opacity="0.4" />
      <circle cx="6" cy="22" r="0.8" fill={accent} opacity="0.3" />
      <circle cx="13" cy="22" r="0.8" fill={accent} opacity="0.3" />
    </svg>
  );
  if (kind === 'drive_cradle') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="4" y="20" width="20" height="4" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.7" />
      <line x1="8" y1="20" x2="8" y2="15" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <line x1="20" y1="20" x2="20" y2="15" stroke={accent} strokeWidth="0.7" opacity="0.5" />
      <circle cx="14" cy="13" r="6" fill="none" stroke={accent} strokeWidth="0.6" opacity="0.5" />
      <circle cx="14" cy="13" r="4" fill="none" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <ellipse cx="14" cy="13" rx="3" ry="3" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <ellipse cx="14" cy="14" rx="2" ry="2.5" fill="#0033AA" opacity="0.3" />
      <ellipse cx="14" cy="14.5" rx="1.2" ry="1.8" fill="#0066FF" opacity="0.25" style={{ animation:'world-obj-pulse 1.8s ease-in-out infinite' }} />
      <line x1="8" y1="11" x2="10" y2="13" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="20" y1="11" x2="18" y2="13" stroke={accent} strokeWidth="0.4" opacity="0.3" />
    </svg>
  );
  if (kind === 'crawler_port') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <circle cx="14" cy="14" r="11" fill={dim} stroke="#FF6600" strokeWidth="0.8" opacity="0.9" />
      <circle cx="14" cy="14" r="8" fill={dim} stroke="#FF6600" strokeWidth="0.5" opacity="0.5" />
      <line x1="14" y1="3" x2="14" y2="25" stroke="#FF6600" strokeWidth="0.5" opacity="0.3" />
      <line x1="3" y1="14" x2="25" y2="14" stroke="#FF6600" strokeWidth="0.5" opacity="0.3" />
      <path d="M10 14 Q12 11 14 14 Q16 17 18 14" fill="none" stroke={accent} strokeWidth="0.8" opacity="0.5" />
      <ellipse cx="14" cy="10" rx="3" ry="2" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <rect x="12" y="12" width="4" height="3" rx="0.3" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <circle cx="12.5" cy="10.5" r="0.6" fill={accent} opacity="0.4" />
      <circle cx="15.5" cy="10.5" r="0.6" fill={accent} opacity="0.4" />
    </svg>
  );
  if (kind === 'rivet_hopper') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <line x1="14" y1="2" x2="14" y2="6" stroke={accent} strokeWidth="0.8" opacity="0.5" />
      <polygon points="8,6 20,6 17,13 11,13" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.8" />
      <rect x="11" y="13" width="6" height="4" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.7" />
      <polygon points="12,17 16,17 17,20 11,20" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <circle cx="12" cy="22" r="0.7" fill={accent} opacity="0.5" />
      <circle cx="14" cy="23" r="0.7" fill={accent} opacity="0.4" />
      <circle cx="16" cy="22" r="0.7" fill={accent} opacity="0.5" />
      <circle cx="11" cy="24" r="0.5" fill={accent} opacity="0.3" />
      <circle cx="17" cy="24" r="0.5" fill={accent} opacity="0.3" />
      <rect x="8" y="9" width="4" height="1" rx="0.3" fill={accent} opacity="0.2" />
      <rect x="8" y="11" width="6" height="1" rx="0.3" fill={accent} opacity="0.15" />
    </svg>
  );
  if (kind === 'republic_terminal') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="4" y="5" width="20" height="16" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.7" opacity="0.85" />
      <rect x="5" y="6" width="18" height="11" rx="0.3" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.4" />
      <circle cx="14" cy="11.5" r="3" fill="none" stroke={accent} strokeWidth="0.5" opacity="0.4" />
      <line x1="14" y1="8.5" x2="14" y2="14.5" stroke={accent} strokeWidth="0.4" opacity="0.35" />
      <line x1="11" y1="11.5" x2="17" y2="11.5" stroke={accent} strokeWidth="0.4" opacity="0.35" />
      <line x1="12" y1="9.5" x2="16" y2="13.5" stroke={accent} strokeWidth="0.3" opacity="0.25" />
      <line x1="16" y1="9.5" x2="12" y2="13.5" stroke={accent} strokeWidth="0.3" opacity="0.25" />
      <rect x="4" y="21" width="20" height="3" rx="0.3" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <line x1="6" y1="22" x2="8" y2="22" stroke={accent} strokeWidth="0.3" opacity="0.3" />
      <line x1="10" y1="22" x2="12" y2="22" stroke={accent} strokeWidth="0.3" opacity="0.3" />
      <line x1="14" y1="22" x2="16" y2="22" stroke={accent} strokeWidth="0.3" opacity="0.3" />
    </svg>
  );
  if (kind === 'ship_model_case') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="4" width="24" height="20" rx="0.5" fill="#000A14" stroke={accent} strokeWidth="0.7" opacity="0.9" />
      <rect x="2" y="4" width="24" height="2.5" fill={accent} opacity="0.1" />
      <rect x="2" y="4" width="24" height="2" rx="0.5" fill="none" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <polygon points="7,16 5,18 9,18" fill="#D4AF37" opacity="0.7" />
      <line x1="7" y1="10" x2="7" y2="16" stroke="#D4AF37" strokeWidth="0.5" opacity="0.5" />
      <line x1="5" y1="13" x2="9" y2="13" stroke="#D4AF37" strokeWidth="0.5" opacity="0.5" />
      <polygon points="14,15 12,17 16,17" fill="#D4AF37" opacity="0.7" />
      <line x1="14" y1="9" x2="14" y2="15" stroke="#D4AF37" strokeWidth="0.5" opacity="0.5" />
      <line x1="12" y1="12" x2="16" y2="12" stroke="#D4AF37" strokeWidth="0.5" opacity="0.5" />
      <polygon points="21,16 19,18 23,18" fill="#D4AF37" opacity="0.7" />
      <line x1="21" y1="10" x2="21" y2="16" stroke="#D4AF37" strokeWidth="0.5" opacity="0.5" />
      <line x1="19" y1="13" x2="23" y2="13" stroke="#D4AF37" strokeWidth="0.5" opacity="0.5" />
      <line x1="4" y1="20" x2="12" y2="20" stroke={accent} strokeWidth="0.3" opacity="0.3" />
      <line x1="11" y1="20" x2="17" y2="20" stroke={accent} strokeWidth="0.3" opacity="0.3" />
      <line x1="18" y1="20" x2="26" y2="20" stroke={accent} strokeWidth="0.3" opacity="0.3" />
    </svg>
  );
  if (kind === 'bronze_mural') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="2" y="2" width="24" height="24" rx="0.5" fill="#1A0D00" stroke="#B8860B" strokeWidth="0.8" opacity="0.9" />
      <path d="M5 22 Q8 12 14 10 Q20 12 23 22" fill="#1A0D00" stroke="#B8860B" strokeWidth="0.5" opacity="0.5" />
      <ellipse cx="8" cy="13" rx="1.5" ry="1.5" fill="#B8860B" opacity="0.3" />
      <rect x="7" y="14.5" width="3" height="7" rx="0.3" fill="#B8860B" opacity="0.25" />
      <ellipse cx="14" cy="12" rx="1.5" ry="1.5" fill="#B8860B" opacity="0.35" />
      <rect x="13" y="13.5" width="3" height="8" rx="0.3" fill="#B8860B" opacity="0.3" />
      <ellipse cx="20" cy="13" rx="1.5" ry="1.5" fill="#B8860B" opacity="0.3" />
      <rect x="19" y="14.5" width="3" height="7" rx="0.3" fill="#B8860B" opacity="0.25" />
      <line x1="4" y1="8" x2="24" y2="8" stroke="#B8860B" strokeWidth="0.4" opacity="0.3" />
      <line x1="5" y1="9.5" x2="23" y2="9.5" stroke="#B8860B" strokeWidth="0.3" opacity="0.2" />
      <circle cx="14" cy="5" r="1.5" fill="#B8860B" opacity="0.4" />
      <circle cx="8" cy="5.5" r="0.8" fill="#B8860B" opacity="0.3" />
      <circle cx="20" cy="5.5" r="0.8" fill="#B8860B" opacity="0.3" />
    </svg>
  );
  if (kind === 'panoramic_viewport') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <path d="M2 22 Q2 6 14 4 Q26 6 26 22 Z" fill="#000811" stroke={accent} strokeWidth="0.8" opacity="0.9" />
      <path d="M4 22 Q4 8 14 6 Q24 8 24 22 Z" fill="none" stroke={accent} strokeWidth="0.4" opacity="0.25" />
      <path d="M6 22 Q6 10 14 8 Q22 10 22 22 Z" fill="none" stroke={accent} strokeWidth="0.3" opacity="0.15" />
      <path d="M3 18 Q10 12 14 14 Q18 16 25 10" fill="none" stroke={accent} strokeWidth="0.6" opacity="0.35" />
      <line x1="8" y1="18" x2="10" y2="15" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="18" y1="15" x2="20" y2="18" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <circle cx="7" cy="9" r="0.6" fill={accent} opacity="0.5" />
      <circle cx="12" cy="7" r="0.4" fill={accent} opacity="0.4" />
      <circle cx="19" cy="8" r="0.5" fill={accent} opacity="0.5" />
      <circle cx="22" cy="12" r="0.4" fill={accent} opacity="0.4" />
      <line x1="2" y1="22" x2="26" y2="22" stroke={accent} strokeWidth="1" opacity="0.5" />
    </svg>
  );
  if (kind === 'protocol_droid_stand') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="8" y="22" width="12" height="3" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.7" />
      <rect x="11" y="19" width="6" height="3" fill={dim} stroke={accent} strokeWidth="0.5" opacity="0.6" />
      <rect x="9" y="11" width="10" height="8" rx="1" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.8" />
      <line x1="11" y1="14" x2="17" y2="14" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="11" y1="16" x2="17" y2="16" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="9" y1="13" x2="6" y2="17" stroke={dim} strokeWidth="1.5" opacity="0.6" />
      <line x1="19" y1="13" x2="22" y2="17" stroke={dim} strokeWidth="1.5" opacity="0.6" />
      <line x1="9" y1="13" x2="6" y2="17" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <line x1="19" y1="13" x2="22" y2="17" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <ellipse cx="14" cy="8" rx="4" ry="3.5" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.8" />
      <circle cx="12" cy="8" r="1" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <circle cx="16" cy="8" r="1" fill={dim} stroke={accent} strokeWidth="0.4" opacity="0.5" />
      <circle cx="12" cy="8" r="0.4" fill="#CC8800" opacity="0.5" />
      <circle cx="16" cy="8" r="0.4" fill="#CC8800" opacity="0.5" />
      <circle cx="9" cy="12" r="0.5" fill="#CC6600" opacity="0.4" />
      <circle cx="19" cy="12" r="0.5" fill="#CC6600" opacity="0.4" />
    </svg>
  );
  if (kind === 'forge_bar') return (
    <svg viewBox="0 0 28 28" width="26" height="26" style={s}>
      <rect x="3" y="16" width="22" height="8" rx="0.5" fill={dim} stroke={accent} strokeWidth="0.6" opacity="0.8" />
      <rect x="3" y="16" width="22" height="2" fill={accent} opacity="0.15" />
      <line x1="3" y1="18" x2="25" y2="18" stroke={accent} strokeWidth="0.4" opacity="0.3" />
      <rect x="5" y="8" width="2.5" height="8" rx="0.5" fill="#5B1A00" stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <rect x="9" y="9" width="2.5" height="7" rx="0.5" fill="#8B2500" stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <rect x="13" y="7" width="2.5" height="9" rx="0.5" fill="#003B5C" stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <rect x="17" y="10" width="2.5" height="6" rx="0.5" fill="#2E4A1A" stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <rect x="21" y="8" width="2.5" height="8" rx="0.5" fill="#4A3A00" stroke={accent} strokeWidth="0.4" opacity="0.6" />
      <rect x="9" y="21" width="3" height="1" rx="0.3" fill={accent} opacity="0.25" />
      <line x1="5" y1="20" x2="23" y2="20" stroke={accent} strokeWidth="0.3" opacity="0.2" />
    </svg>
  );
  return (
    <div style={{ position:'absolute',inset:3,border:`1px solid ${accent}55`,borderRadius:2,animation:'world-obj-pulse 2.5s ease-in-out infinite',pointerEvents:'none' }} />
  );
}

const PROP_STYLE = { position:'absolute', inset:0, pointerEvents:'none' };
const PT = { fontFamily:"'IBM Plex Mono',monospace" };

// ART KIT v3: the house style's palette and building blocks. See .claude/STYLE_GUIDE.md.
// Materials are weathered and muted (used future). Every material is three flat tones (hi, base, shade).
const ART = {
  ink:      '#0A0A0F',
  steel:    { hi:'#9FAEBF', base:'#5F6E80', shade:'#2F3947' },
  dark:     { hi:'#4A5562', base:'#27313C', shade:'#12161D' },
  hull:     { hi:'#CFD6DE', base:'#8E9AA8', shade:'#4E5A6A' },
  pearl:    { hi:'#F4F6F9', base:'#CBD3DC', shade:'#7B879A' },
  brass:    { hi:'#D9C27A', base:'#9C7F3C', shade:'#5E4A1E' },
  bronze:   { hi:'#B87F4F', base:'#7A502E', shade:'#3F2815' },
  copper:   { hi:'#C98655', base:'#9A5A2B', shade:'#573018' },
  teal:     { hi:'#7FA3A6', base:'#41686C', shade:'#213B3F' },
  red:      { hi:'#C84A44', base:'#9C2F2A', shade:'#5A1612' },
  amber:    { hi:'#D8A84E', base:'#A87B28', shade:'#5E4210' },
  rust:     { hi:'#B06040', base:'#84402A', shade:'#4A2214' },
  tan:      { hi:'#C4B48C', base:'#968568', shade:'#564B38' },
  wood:     { hi:'#9A6C40', base:'#6A4424', shade:'#35210F' },
  canvas:   { hi:'#7E887A', base:'#566052', shade:'#323A2F' },
  beige:    { hi:'#D6D2C0', base:'#A8A494', shade:'#66665C' },
  paper:    { hi:'#FFF8DC', base:'#E6DCBC', shade:'#B0A47C' },
  glass:    { hi:'#C8E6F4', base:'#6A9FBD', shade:'#2C5670' },
  deepglass:{ hi:'#4F8CBF', base:'#1D4F82', shade:'#0B2442' },
  concrete: { hi:'#A29E96', base:'#6F6C66', shade:'#403E3A' },
  stone:    { hi:'#9C9482', base:'#6A6454', shade:'#3A362C' },
  moss:     { hi:'#8FAE6A', base:'#56743F', shade:'#2E4222' },
  sand:     { hi:'#E0C890', base:'#B89C64', shade:'#7C6A40' },
  fabric:   { hi:'#8A5A7A', base:'#5E3A52', shade:'#35202E' },
  sky:      { amber:'#E8742A', ember:'#C4482A', rose:'#9C3A5C', plum:'#5E2A66', violet:'#3A1E62', indigo:'#1C1744', haze:'#6E5040', spire:'#2A2238', spireHi:'#4A3A56' },
  sky:      { amber:'#E8742A', ember:'#C4482A', rose:'#9C3A5C', plum:'#5E2A66', violet:'#3A1E62', indigo:'#1C1744', haze:'#6E5040', spire:'#2A2238', spireHi:'#4A3A56' },
  sky:      { amber:'#E8742A', ember:'#C4482A', rose:'#9C3A5C', plum:'#5E2A66', violet:'#3A1E62', indigo:'#1C1744', haze:'#6E5040', spire:'#2A2238', spireHi:'#4A3A56' },
  // Emissive and signal colors: screens, lamps, status lights, holograms. Never used for solid bodies.
  signal: {
    cyan:'#7FD0FF', cyanHi:'#BFF4FF', cyanLo:'#2A8A9A', amber:'#FFB844', amberHi:'#FFE9B0', gold:'#F4DA86',
    green:'#3AFF8A', greenHi:'#7CFFB0', red:'#FF4040', redHi:'#FF9090', warn:'#E0A818', orange:'#E8731A',
    white:'#FFFFFF', black:'#000000', anomaly:'#9A78E8',
    blue:'#4A8CFF', crimson:'#E02A55', lime:'#B6FF5A', violet:'#B07CFF', kyber:'#E8F6FF', magenta:'#FF4FC8', taxi:'#F2C230',
  },
  // Fixed story colors shared with the rest of the game (see STYLE_GUIDE).
  story: { amber:'#E8A030', danger:'#FF4422', syndicate:'#40C840', senate:'#9966FF', csf:'#4A9FFF', tape:'#FFB800' },
  // Dark display glass for screens and viewports.
  screen: { off:'#05090E', cyan:'#031A22', green:'#02100A', amber:'#150E04', red:'#2A0A0A', space:'#070C1A' },
  // Skin and hair tones, taken from the core portraits and player.
  skin: { light:'#D9B98C', mid:'#C8956A', dark:'#8A6A4A', rodian:'#5A9A4A', rodianShade:'#4A8A3A', hair:'#2A2320' },
  // Paper notes, marker pens, and hand written ink.
  note: { yellow:'#F8E86A', pink:'#FFB0C8', ink:'#2A1A0A', blueInk:'#1A3A8A', redInk:'#E0201A' },
  // Brand blues used by KDY set pieces and gaming chips.
  brand: { kdy:'#1A8FD0', chip:'#2E7BD0' },
  // Table cloth.
  cloth: { felt:'#1B5C34' },
  ink2: '#05080C',
};

// Deterministic pseudo random so greebles and wear never change between renders.
function rng(seed) {
  let s = (Math.floor(seed * 9301) + 49297) >>> 0;
  return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; };
}
const pts = (a) => a.map((p) => p[0] + ',' + p[1]).join(' ');
const ngon = (cx, cy, rx, ry, n, rot = 0) => Array.from({ length: n }, (_, i) => { const a = rot + (i / n) * Math.PI * 2; return [+(cx + Math.cos(a) * rx).toFixed(2), +(cy + Math.sin(a) * ry).toFixed(2)]; });

// Three tone bevel: shade offset down right, highlight offset up left, base on top. Pass the shape as t.
function Bev({ t = 'rect', c = 'steel', o = 0.8, transform, ...a }) {
  const P = typeof c === 'string' ? ART[c] : c;
  const Tag = t;
  return (
    <g transform={transform}>
      <Tag {...a} fill={P.shade} transform={`translate(${o * 0.9} ${o * 1.2})`} />
      <Tag {...a} fill={P.hi} transform={`translate(${-o * 0.6} ${-o * 0.8})`} />
      <Tag {...a} fill={P.base} />
    </g>
  );
}

// Chamfered box (cut corners, never pill shaped): Star Wars tech is hard edged.
function Slab({ x, y, w, h, k = 1.6, c = 'steel', o, ...r }) {
  const kk = Math.min(k, w / 2.2, h / 2.2);
  const p = [[x + kk, y], [x + w - kk, y], [x + w, y + kk], [x + w, y + h - kk], [x + w - kk, y + h], [x + kk, y + h], [x, y + h - kk], [x, y + kk]];
  return <Bev t="polygon" points={pts(p)} c={c} o={o} {...r} />;
}

// Panel seams: thin dark lines dividing a surface into plates.
function Seams({ x, y, w, h, cols = 0, rows = 0, op = 0.45 }) {
  const L = [];
  for (let i = 1; i <= cols; i++) L.push(<line key={'c' + i} x1={x + (w * i) / (cols + 1)} y1={y} x2={x + (w * i) / (cols + 1)} y2={y + h} />);
  for (let j = 1; j <= rows; j++) L.push(<line key={'r' + j} x1={x} y1={y + (h * j) / (rows + 1)} x2={x + w} y2={y + (h * j) / (rows + 1)} />);
  return <g stroke={ART.ink} strokeWidth="0.5" opacity={op}>{L}</g>;
}

// A row of rivets or bolts.
function Rivets({ x, y, w, n = 6, c = 'steel', r = 0.6 }) {
  return <g>{Array.from({ length: n }, (_, i) => <circle key={i} cx={x + (n === 1 ? 0 : (w * i) / (n - 1))} cy={y} r={r} fill={ART[c].hi} />)}</g>;
}

// Louvred vent: dark slots.
function Vent({ x, y, w, h, n = 4 }) {
  return (
    <g>
      <rect x={x} y={y} width={w} height={h} fill={ART.ink} />
      {Array.from({ length: n }, (_, i) => <rect key={i} x={x + 0.8} y={y + 0.8 + (i * (h - 1.6)) / n} width={w - 1.6} height={Math.max(0.5, (h - 1.6) / n - 0.9)} fill={ART.dark.hi} />)}
    </g>
  );
}

// Greebles: small functional surface detail (Lucas's term). Deterministic by seed.
function Greeble({ x, y, w, h, seed = 1, n = 8, c = 'steel' }) {
  const r = rng(seed), T = ART[c], out = [];
  for (let i = 0; i < n; i++) {
    const bw = 1.6 + r() * 4.2, bh = 1.4 + r() * 3, bx = x + r() * Math.max(0, w - bw), by = y + r() * Math.max(0, h - bh), dark = r() < 0.45;
    out.push(<g key={i}><rect x={bx} y={by} width={bw} height={bh} fill={dark ? T.shade : T.base} /><rect x={bx} y={by} width={bw} height="0.6" fill={T.hi} opacity="0.75" /></g>);
  }
  return <g>{out}</g>;
}

// A row of tiny status lights.
function Lights({ x, y, n = 5, gap = 3, r = 0.8, colors = ['green', 'amber', 'red'], blink = true, seed = 1 }) {
  const rr = rng(seed);
  return <g>{Array.from({ length: n }, (_, i) => <circle key={i} cx={x + i * gap} cy={y} r={r} fill={ART.signal[colors[i % colors.length]]} style={blink && rr() < 0.5 ? { animation:`ship-blink ${1.4 + rr() * 2}s steps(2) ${rr()}s infinite` } : undefined} />)}</g>;
}

// A bank of console buttons and toggles (Star Wars tech is chunky and analog).
function Toggles({ x, y, cols = 6, rows = 2, gap = 3.2, seed = 3 }) {
  const r = rng(seed), cs = ['amber', 'green', 'red', 'cyan', 'amber', 'amber'], out = [];
  for (let j = 0; j < rows; j++) for (let i = 0; i < cols; i++) {
    const col = ART.signal[cs[Math.floor(r() * cs.length)]];
    out.push(<g key={i + '_' + j}><rect x={x + i * gap} y={y + j * gap} width="2.2" height="1.8" fill={ART.dark.shade} /><rect x={x + i * gap + 0.3} y={y + j * gap + 0.3} width="1.6" height="1.1" fill={col} opacity={r() < 0.3 ? 0.35 : 0.95} /></g>);
  }
  return <g>{out}</g>;
}

// Wear: grime streaks and bright scuffs.
function Grime({ x, y, w, h, seed = 2, n = 5, op = 0.18 }) {
  const r = rng(seed);
  return <g fill={ART.ink} opacity={op}>{Array.from({ length: n }, (_, i) => { const gx = x + r() * w, gw = 0.8 + r() * 2.4, gh = h * (0.25 + r() * 0.7); return <rect key={i} x={gx} y={y + r() * (h - gh)} width={gw} height={gh} />; })}</g>;
}
function Scuff({ x, y, w, h, seed = 5, n = 6, c = 'steel' }) {
  const r = rng(seed);
  return <g stroke={ART[c].hi} strokeWidth="0.5" opacity="0.5">{Array.from({ length: n }, (_, i) => { const sx = x + r() * w, sy = y + r() * h; return <line key={i} x1={sx} y1={sy} x2={sx + 1.5 + r() * 4} y2={sy + 1 + r() * 2.5} />; })}</g>;
}

// A thick cable: dark sheath with a highlight.
function Cable({ d, c = '#2A323E', w = 2, hi = 'steel' }) {
  return <g fill="none"><path d={d} stroke={ART.ink} strokeWidth={w + 0.8} /><path d={d} stroke={c} strokeWidth={w} /><path d={d} stroke={ART[hi].hi} strokeWidth="0.4" opacity="0.6" /></g>;
}

// Stepped glow: three flat concentric rings instead of a blurred gradient.
function Glow({ cx, cy, r, rx, ry, c = '#7FD0FF', opacity = 1, style }) {
  const RX = rx ?? r, RY = ry ?? r;
  return (
    <g opacity={opacity} style={style}>
      <ellipse cx={cx} cy={cy} rx={RX} ry={RY} fill={c} opacity="0.12" />
      <ellipse cx={cx} cy={cy} rx={RX * 0.66} ry={RY * 0.66} fill={c} opacity="0.22" />
      <ellipse cx={cx} cy={cy} rx={RX * 0.34} ry={RY * 0.34} fill="#FFFFFF" opacity="0.6" />
    </g>
  );
}

// Amber and black hazard striping built from flat parallelograms (KDY signature).
function Hazard({ x, y, w, h, opacity = 1 }) {
  const id = 'hz' + React.useId().replace(/[^a-zA-Z0-9]/g, '');
  const n = Math.ceil((w + h) / 8) + 1;
  return (
    <g opacity={opacity}>
      <defs><clipPath id={id}><rect x={x} y={y} width={w} height={h} /></clipPath></defs>
      <rect x={x} y={y} width={w} height={h} fill="#14171B" />
      <g clipPath={`url(#${id})`}>
        {Array.from({ length: n }, (_, i) => {
          const x0 = x + i * 8 - h;
          return <polygon key={i} points={`${x0},${y + h} ${x0 + 4},${y + h} ${x0 + 4 + h},${y} ${x0 + h},${y}`} fill="#E0A818" />;
        })}
      </g>
    </g>
  );
}

// SHARED LIGHTING PASS: the engine applies this to every sprite in the world (characters, the player,
// objects, ships) so old and new art share one treatment. Characters get an ink outline and rim light;
// large detailed set pieces and ships get only a soft accent rim so their surface detail reads.
function spriteFx(accent, size = 'actor') {
  const ink = '#05080C';
  const rim = size === 'ship' ? 3 : size === 'prop' ? 2 : 1.5;
  if (size === 'ship' || size === 'prop') return `drop-shadow(0 0 ${rim}px ${accent}AA)`;
  const edge = size === 'icon' ? '' : `drop-shadow(1px 0 0 ${ink}) drop-shadow(-1px 0 0 ${ink}) drop-shadow(0 1px 0 ${ink}) drop-shadow(0 -1px 0 ${ink}) `;
  return `${edge}drop-shadow(0 0 ${rim}px ${accent}CC)`;
}
const ACTOR_SHADOW = { position:'absolute', bottom:1, left:'50%', width:20, height:6, marginLeft:-10, borderRadius:'50%', background:'rgba(0,0,0,0.5)', zIndex:4, pointerEvents:'none' };

// Flat two layer floor shadow (hard edged, no blur).
function PropShadow({ cx, cy, rx, ry }) {
  return (
    <g>
      <ellipse cx={cx} cy={cy} rx={rx} ry={ry} fill="#000" opacity="0.22" />
      <ellipse cx={cx} cy={cy} rx={rx * 0.82} ry={ry * 0.78} fill="#000" opacity="0.22" />
    </g>
  );
}

function PropFuelRig() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={29} ry={4} />
      <Slab x={3} y={50} w={58} h={8} k={1.4} c="dark" />
      <Hazard x={5} y={55} w={54} h={2.6} />
      {[10, 54].map((x) => <Bev key={x} t="polygon" points={pts(ngon(x, 59, 3.2, 2.6, 6))} c="steel" o={0.4} />)}
      {/* heavy conduit body with flanges */}
      <Slab x={5} y={29} w={54} h={19} k={4.5} c="dark" o={1} />
      <rect x="8" y="30" width="48" height="2" fill={ART.dark.hi} opacity="0.7" />
      {[16, 32, 48].map((x) => <Slab key={x} x={x - 2} y={27} w={4} h={23} k={1} c="steel" o={0.6} />)}
      <Rivets x={14} y={29.4} w={36} n={8} />
      <Rivets x={14} y={47.6} w={36} n={8} />
      {/* coaxium sight glass */}
      <Slab x={21} y={33} w={20} h={11} k={2} c="dark" />
      <rect x="23" y="35" width="16" height="7" fill={ART.screen.cyan} />
      <rect x="23" y="35" width="16" height="7" fill={S.cyan} opacity="0.75" style={{ animation:'ship-engine 2.4s ease-in-out infinite' }} />
      <path d="M23 40 L27 37 L31 40 L35 37 L39 40" fill="none" stroke={S.cyanHi} strokeWidth="0.7" />
      {/* corroded clamps */}
      {[8, 49].map((x) => (
        <g key={x}>
          <Slab x={x} y={25} w={7} h={26} k={1.4} c="rust" o={0.9} />
          <circle cx={x + 2.4} cy={32} r="1.7" fill={ART.teal.base} opacity="0.85" /><circle cx={x + 4.6} cy={40} r="1.3" fill={ART.teal.hi} opacity="0.7" /><circle cx={x + 2} cy={46} r="1.6" fill={ART.teal.base} opacity="0.8" />
          <rect x={x + 1} y={25} width="5" height="1.2" fill={ART.rust.hi} opacity="0.8" />
        </g>
      ))}
      {/* bypassed regulator box and jumper cable */}
      <Slab x={25} y={11} w={14} h={17} k={1.6} c="dark" />
      <Toggles x={27} y={13} cols={4} rows={3} gap={3} seed={5} />
      <rect x="27" y="23" width="10" height="3" fill={ART.screen.amber} /><rect x="28" y="24" width="5" height="1" fill={S.amber} />
      <Cable d="M25 20 Q14 6 32 5 Q50 6 39 20" c="#E0A818" w={1.8} hi="amber" />
      <circle cx="25" cy="20" r="1.8" fill={ART.steel.hi} /><circle cx="39" cy="20" r="1.8" fill={ART.steel.hi} />
      {/* pressure valve and gauge */}
      <rect x="47.4" y="19" width="1.8" height="10" fill={ART.steel.base} />
      <Bev t="polygon" points={pts(ngon(48.3, 15, 5, 5, 6))} c="red" o={0.5} />
      <path d="M48.3 10.5 L48.3 19.5 M44 15 L52.6 15" stroke={ART.red.shade} strokeWidth="0.8" />
      <Slab x={10} y={14} w={11} h={9} k={1.2} c="steel" o={0.6} />
      <rect x="11.6" y="15.6" width="7.8" height="5.8" fill={ART.screen.off} />
      <path d="M12.6 20.4 L18.6 16.6" stroke={S.red} strokeWidth="0.9" /><rect x="12.4" y="20.8" width="3" height="0.9" fill={S.amber} />
      {/* fuel hose */}
      <Cable d="M59 38 Q63 46 58 52" c={ART.dark.base} w={3} hi="steel" />
      <Slab x={54} y={50} w={7} h={5} k={1} c="steel" o={0.5} />
      <circle cx="20" cy="48" r="1" fill={S.cyan} style={{ animation:'ship-blink 2s ease-in-out infinite' }} />
      <Grime x={6} y={26} w={52} h={24} seed={3} n={6} op={0.2} /><Scuff x={8} y={30} w={48} h={16} seed={9} n={8} />
    </svg>
  );
}

function PropManifestTerminal() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={22} ry={3.6} />
      <Slab x={24} y={42} w={16} h={14} k={1.4} c="dark" />
      <Vent x={27} y={46} w={10} h={6} n={3} />
      <Slab x={14} y={54} w={36} h={5} k={1.4} c="steel" o={0.9} />
      {/* angled console head */}
      <Bev t="polygon" points={pts([[7, 8], [57, 8], [60, 12], [60, 44], [57, 47], [7, 47], [4, 44], [4, 12]])} c="steel" o={1} />
      <Seams x={4} y={8} w={56} h={39} cols={0} rows={1} op={0.3} />
      <rect x="9" y="12" width="46" height="29" fill={ART.screen.green} />
      <rect x="9" y="12" width="46" height="5" fill={S.green} opacity="0.35" />
      <text x="11" y="15.6" fontSize="3.4" fill={S.greenHi} fontWeight="700" {...PT}>KDY MANIFEST NODE</text>
      {[20, 23.4, 26.8].map((y, i) => <g key={y}><rect x="11" y={y - 1.4} width={26 + (i % 2) * 8} height="1.3" fill={S.green} opacity="0.8" /><rect x="48" y={y - 1.4} width="5" height="1.3" fill={S.green} opacity="0.5" /></g>)}
      <rect x="10" y="29" width="44" height="11" fill={ART.screen.red} />
      <rect x="10" y="29" width="44" height="11" fill={S.red} opacity="0.18" style={{ animation:'ship-engine 2.2s ease-in-out infinite' }} />
      <text x="11.5" y="32.8" fontSize="3" fill={S.redHi} fontWeight="700" {...PT}>CONSIGNMENT RESTRICTED</text>
      <text x="11.5" y="36" fontSize="2.6" fill={S.redHi} {...PT}>11 ENTRIES · DIRECTOR ONLY</text>
      <text x="11.5" y="39" fontSize="2.4" fill={S.redHi} opacity="0.8" {...PT}>T MINUS 06:00 STD</text>
      <rect x="9" y="12" width="46" height="2" fill={S.greenHi} opacity="0.3" style={{ animation:'prop-scan 3.2s linear infinite' }} />
      <Toggles x={8} y={43} cols={9} rows={1} gap={3.4} seed={8} /><Lights x={44} y={44} n={4} gap={3.4} seed={2} />
      <line x1="55" y1="8" x2="55" y2="2" stroke={ART.steel.hi} strokeWidth="0.9" /><circle cx="55" cy="1.6" r="1" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      <Grime x={5} y={9} w={54} h={38} seed={12} n={4} op={0.15} /><Scuff x={6} y={10} w={50} h={36} seed={4} n={7} />
    </svg>
  );
}

function PropVectis({ active }) {
  const S = ART.signal;
  const lens = active ? S.green : S.amber;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={90} rx={24} ry={4} />
      {/* ceiling track and trolley */}
      <Slab x={0} y={2} w={64} h={7} k={1} c="dark" /><Rivets x={4} y={5.5} w={56} n={10} c="dark" />
      <Slab x={24} y={9} w={16} h={7} k={1.2} c="steel" o={0.7} /><Lights x={27} y={12.6} n={4} gap={3.4} seed={9} />
      <rect x="29" y="16" width="6" height="10" fill={ART.dark.base} />
      {/* bronze and black chassis: angular, not cute */}
      <Bev t="polygon" points={pts([[18, 30], [46, 30], [49, 36], [45, 58], [38, 62], [26, 62], [19, 58], [15, 36]])} c="bronze" o={1} />
      <polygon points={pts([[22, 32], [42, 32], [44, 40], [20, 40]])} fill={ART.dark.base} />
      <Seams x={16} y={34} w={32} h={26} cols={2} rows={2} op={0.4} />
      <Slab x={26} y={44} w={12} h={10} k={1.2} c="dark" o={0.6} />
      <text x="32" y="51.4" textAnchor="middle" fontSize="4.2" fill={ART.bronze.hi} fontWeight="700" {...PT}>RX-9</text>
      <Rivets x={20} y={36} w={24} n={5} c="bronze" />
      <path d="M15 34 L8 56 L10 58 M49 34 L56 56 L54 58" fill="none" stroke={ART.ink} strokeWidth="3.4" /><path d="M15 34 L8 56 L10 58 M49 34 L56 56 L54 58" fill="none" stroke={ART.bronze.base} strokeWidth="1.4" />
      {[[10, 58], [54, 58]].map(([x, y], i) => <Bev key={i} t="polygon" points={pts(ngon(x, y, 3, 3, 6))} c="dark" o={0.4} />)}
      <Slab x={23} y={58} w={18} h={8} k={1.4} c="dark" o={0.7} /><Vent x={25} y={60} w={14} h={4} n={2} />
      {/* head: visor slit with three lenses */}
      <Bev t="polygon" points={pts([[17, 18], [47, 18], [50, 23], [47, 30], [17, 30], [14, 23]])} c="bronze" o={0.9} />
      <rect x="19" y="21" width="26" height="6" fill={ART.ink} />
      {[25, 32, 39].map((x, i) => <g key={x}><circle cx={x} cy={24} r="2.3" fill={lens} style={{ animation:`ship-engine ${2.4 + i * 0.4}s ease-in-out infinite` }} /><circle cx={x - 0.6} cy={23.4} r="0.7" fill={S.white} /></g>)}
      <path d="M24 26 L20 36 M32 27 L32 38 M40 26 L44 36" stroke={lens} strokeWidth="0.5" opacity="0.3" />
      <Grime x={16} y={30} w={32} h={30} seed={4} n={4} op={0.2} />
      {/* wall terminal below */}
      <Slab x={10} y={72} w={44} h={16} k={2} c="dark" o={0.9} />
      <rect x="13" y="75" width="38" height="9" fill={ART.screen.off} />
      <text x="32" y="80" textAnchor="middle" fontSize="3.2" fill={active ? S.greenHi : S.amber} fontWeight="700" {...PT}>{active ? 'CLEARANCE ON FILE' : 'CLEARANCE REQUIRED'}</text>
      <text x="32" y="83" textAnchor="middle" fontSize="2.6" fill={active ? S.green : S.orange} {...PT}>{active ? 'MIND THE INERTIA' : 'ORDINANCE 44-G'}</text>
      <Toggles x={14} y={85} cols={8} rows={1} gap={4} seed={6} />
    </svg>
  );
}

function PropImpoundDoor() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.6} />
      <Slab x={2} y={2} w={92} h={60} k={2.4} c="dark" />
      <Hazard x={4} y={4} w={88} h={6} /><Hazard x={4} y={54} w={88} h={6} />
      {/* two heavy leaves with a locking seam */}
      <Slab x={5} y={11} w={41} h={42} k={1.6} c="steel" o={1} /><Slab x={50} y={11} w={41} h={42} k={1.6} c="steel" o={1} />
      <Seams x={5} y={11} w={41} h={42} rows={3} op={0.45} /><Seams x={50} y={11} w={41} h={42} rows={3} op={0.45} />
      <Rivets x={8} y={14} w={35} n={6} /><Rivets x={8} y={50} w={35} n={6} /><Rivets x={53} y={14} w={35} n={6} /><Rivets x={53} y={50} w={35} n={6} />
      <Slab x={44} y={11} w={8} h={42} k={1} c="dark" o={0.7} />
      {[16, 26, 36, 46].map((y) => <rect key={y} x="45" y={y} width="6" height="2.4" fill={ART.steel.base} />)}
      {[0, 1].map((m) => <Slab key={m} x={m ? 90 : 0.4} y={22} w={5.6} h={22} k={1} c="steel" o={0.5} />)}
      <Grime x={6} y={12} w={84} h={40} seed={14} n={8} op={0.18} /><Scuff x={8} y={14} w={80} h={36} seed={22} n={12} />
      <text x="25" y="23" textAnchor="middle" fontSize="3.6" fill={ART.amber.hi} {...PT}>DOCKING BAY</text>
      <text x="25" y="42" textAnchor="middle" fontSize="16" fill={ART.hull.hi} opacity="0.55" fontWeight="700" {...PT}>7</text>
      {/* the Ring-Sec impound notice */}
      <Slab x={54} y={17} w={34} h={32} k={1.2} c="red" o={0.8} />
      <rect x="54" y="17" width="34" height="5" fill={ART.red.shade} />
      <text x="71" y="20.8" textAnchor="middle" fontSize="3.2" fill={ART.paper.hi} fontWeight="700" {...PT}>KDY RING-SEC SEAL</text>
      <text x="71" y="27.6" textAnchor="middle" fontSize="4" fill={ART.paper.hi} fontWeight="700" {...PT}>IMPOUND</text>
      <text x="71" y="32.6" textAnchor="middle" fontSize="3" fill={ART.paper.base} {...PT}>FREE HORIZON</text>
      <text x="71" y="36.4" textAnchor="middle" fontSize="2.6" fill={ART.paper.base} {...PT}>REGISTRY: CORELLIA</text>
      <text x="71" y="40" textAnchor="middle" fontSize="2.6" fill={ART.paper.base} {...PT}>CLASS B COMMERCIAL</text>
      <text x="71" y="43.6" textAnchor="middle" fontSize="2.6" fill={ART.paper.base} {...PT}>MANIFEST DISCREPANCY</text>
      <text x="71" y="47" textAnchor="middle" fontSize="2.2" fill={ART.paper.shade} {...PT}>NOTICE AGE: 3 DAYS</text>
      <Slab x={50} y={28} w={5} h={10} k={1} c="dark" /><circle cx="52.5" cy="33" r="1.2" fill={S.red} style={{ animation:'ship-blink 1.1s steps(2) infinite' }} />
    </svg>
  );
}

function PropOrbitalViewport() {
  const S = ART.signal;
  const dots = [[24, 31], [30, 36], [36, 40], [44, 37], [50, 41], [58, 38], [64, 34], [70, 39], [76, 35], [28, 41], [53, 33], [67, 42]];
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <defs><clipPath id="ovClip"><rect x="8" y="8" width="80" height="48" /></clipPath></defs>
      <PropShadow cx={48} cy={61} rx={44} ry={2.6} />
      <Slab x={2} y={2} w={92} h={60} k={3} c="steel" o={1} />
      <Seams x={2} y={2} w={92} h={60} cols={3} rows={1} op={0.25} />
      <rect x="8" y="8" width="80" height="48" fill={ART.screen.space} />
      <g clipPath="url(#ovClip)">
        {[[14, 14], [30, 12], [52, 16], [70, 11], [82, 20], [20, 24], [62, 24]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="0.6" fill={S.white} opacity="0.8" style={{ animation:`twinkle ${2 + i * 0.4}s ease-in-out infinite` }} />)}
        <ellipse cx="48" cy="86" rx="70" ry="38" fill={ART.rust.base} />
        <ellipse cx="48" cy="86" rx="70" ry="38" fill="none" stroke={S.amber} strokeWidth="1.4" opacity="0.8" />
        <ellipse cx="48" cy="86" rx="64" ry="34" fill="none" stroke={ART.amber.base} strokeWidth="3" opacity="0.2" />
        {/* the Valor frame in its construction cradle: spine, ribs, towers */}
        <polyline points={pts([[14, 36], [30, 35], [50, 36], [70, 38], [84, 41]])} fill="none" stroke={ART.hull.hi} strokeWidth="1.6" />
        <polyline points={pts([[16, 40], [34, 40], [56, 42], [78, 44]])} fill="none" stroke={ART.hull.base} strokeWidth="0.8" />
        {[24, 32, 40, 48, 56, 64, 72].map((x) => <line key={x} x1={x} y1={35 + (x - 14) * 0.08} x2={x - 1} y2={43 + (x - 14) * 0.05} stroke={ART.steel.hi} strokeWidth="0.7" />)}
        <polygon points={pts([[40, 35], [46, 30], [52, 35]])} fill={ART.hull.base} />
        <line x1="22" y1="46" x2="80" y2="46" stroke={ART.steel.base} strokeWidth="0.6" />
        {dots.map(([x, y], i) => <circle key={i} cx={x} cy={y} r="0.9" fill={S.amberHi} style={{ animation:`ship-blink ${1.4 + (i % 4) * 0.5}s ease-in-out ${i * 0.17}s infinite` }} />)}
        <polygon points={pts([[12, 12], [40, 8], [22, 40]])} fill={S.white} opacity="0.07" />
      </g>
      <rect x="8" y="8" width="80" height="48" fill="none" stroke={ART.dark.shade} strokeWidth="1.2" />
      {[[5, 5], [91, 5], [5, 59], [91, 59], [48, 5], [48, 59]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1.5" fill={ART.steel.hi} />)}
      <Slab x={46} y={57} w={42} h={4} k={0.8} c="dark" />
      <text x="67" y="60.1" textAnchor="middle" fontSize="2.4" fill={ART.brass.hi} {...PT}>HULL CLASS VL-9 · BERTH 7</text>
      <Grime x={4} y={4} w={88} h={56} seed={18} n={5} op={0.14} />
    </svg>
  );
}

function PropTetherLockers() {
  const S = ART.signal;
  const nums = ['07', '08', '09', '10', '11', '12'];
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3.4} />
      <Slab x={2} y={5} w={92} h={53} k={1.6} c="dark" />
      <Slab x={2} y={3} w={92} h={4} k={1} c="steel" o={0.8} />
      {nums.map((n, i) => {
        const x = 4 + i * 15;
        const open = i === 1 || i === 3 || i === 4;
        return (
          <g key={n}>
            {open ? (
              <g>
                <rect x={x} y="9" width="14" height="47" fill={ART.ink} />
                <path d={`M${x + 3} 12 L${x + 3} 32 M${x + 11} 12 L${x + 11} 32 M${x + 3} 20 L${x + 11} 25`} stroke={ART.signal.orange} strokeWidth="1.5" />
                <Slab x={x + 5} y={32} w={4} h={7} k={0.8} c="steel" o={0.5} />
                <path d={`M${x + 7} 39 Q${x + 4} 46 ${x + 9} 52`} fill="none" stroke={ART.signal.orange} strokeWidth="1.1" />
                <Bev t="polygon" points={pts([[x, 9], [x - 5, 12], [x - 5, 54], [x, 56]])} c="canvas" o={0.7} />
              </g>
            ) : (
              <g>
                <Slab x={x} y={9} w={14} h={47} k={1.2} c="canvas" o={0.9} />
                <Vent x={x + 2} y={11} w={10} h={6} n={3} />
                <rect x={x + 10} y={30} width="2" height="7" fill={ART.steel.hi} />
                <circle cx={x + 7} cy={51} r="1.2" fill={S.green} style={{ animation:`ship-engine ${3 + i * 0.3}s ease-in-out infinite` }} />
              </g>
            )}
            <rect x={x + 3} y={38} width="8" height="6" fill={ART.paper.base} opacity="0.85" />
            <text x={x + 7} y="42.6" fontSize="3.6" fill={ART.ink} textAnchor="middle" fontWeight="700" {...PT}>{n}</text>
          </g>
        );
      })}
      <g transform="rotate(-4 88 20)">
        <rect x="79" y="14" width="12" height="11" fill={ART.note.yellow} />
        <path d="M80.5 17 L89 17 M80.5 19.2 L88 19.2 M80.5 21.4 L89 21.4 M80.5 23.4 L85 23.4" stroke={ART.note.ink} strokeWidth="0.5" />
        <circle cx="85" cy="14.6" r="0.9" fill={ART.note.redInk} />
      </g>
      <Grime x={4} y={8} w={88} h={48} seed={5} n={7} op={0.2} /><Scuff x={6} y={10} w={84} h={44} seed={6} n={12} c="canvas" />
    </svg>
  );
}

function PropTransitPod() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3.4} />
      {/* route board */}
      <Slab x={2} y={3} w={54} h={17} k={1.4} c="dark" />
      <rect x="4" y="5" width="50" height="13" fill={ART.screen.off} />
      <text x="6" y="8.6" fontSize="2.8" fill={S.cyan} {...PT}>RING PNEUMATIC · POD STATION 3</text>
      <rect x="6" y="10" width="2" height="2" fill={S.green} /><text x="10" y="11.8" fontSize="2.6" fill={S.greenHi} {...PT}>BRIEFING RING · CLEARED</text>
      <rect x="6" y="13" width="2" height="2" fill={S.amber} /><text x="10" y="14.8" fontSize="2.6" fill={S.amberHi} {...PT}>EXECUTIVE RING · DIRECTOR CODE</text>
      <rect x="6" y="16" width="2" height="2" fill={S.red} /><text x="10" y="17.8" fontSize="2.6" fill={S.redHi} {...PT}>MAINTENANCE · ENGINEERING ONLY</text>
      {/* pressurized cradle rails */}
      <Slab x={3} y={30} w={56} h={3} k={0.8} c="steel" o={0.6} /><Slab x={3} y={52} w={56} h={3} k={0.8} c="steel" o={0.6} />
      {[8, 30, 54].map((x) => <rect key={x} x={x} y={33} width="2.6" height="19" fill={ART.steel.base} />)}
      {/* pod capsule, hatch open on the foam cradle */}
      <Bev t="polygon" points={pts([[8, 38], [14, 27], [50, 27], [58, 38], [58, 46], [50, 56], [14, 56], [8, 46]])} c="steel" o={1} />
      <Seams x={8} y={27} w={50} h={29} cols={4} op={0.35} />
      <polygon points={pts([[16, 31], [48, 31], [54, 38], [54, 46], [48, 52], [16, 52], [12, 46], [12, 38]])} fill={ART.ink} />
      <polygon points={pts([[19, 35], [45, 35], [50, 40], [50, 45], [45, 49], [19, 49], [16, 45], [16, 40]])} fill={ART.canvas.shade} />
      <path d="M22 44 Q26 38 33 40 Q42 38 46 44 Q40 48 33 47 Q27 48 22 44 Z" fill={ART.dark.base} />
      <ellipse cx="31" cy="41" rx="2.6" ry="2.4" fill={ART.dark.shade} />
      <polygon points={pts([[16, 31], [48, 31], [54, 38], [12, 38]])} fill={S.cyan} opacity="0.1" />
      {[14, 22, 30, 38, 46, 54].map((x) => <circle key={x} cx={x} cy="29" r="0.8" fill={S.amberHi} />)}
      {/* KDY Mark IV Heavy Loader Droid in standby */}
      <Bev t="polygon" points={pts([[60, 28], [90, 24], [94, 30], [94, 54], [88, 58], [62, 58], [58, 52]])} c="teal" o={1} />
      <path d="M66 32 L90 35 M62 42 L92 46 M64 52 L88 54" stroke={ART.teal.shade} strokeWidth="0.7" opacity="0.8" />
      <path d="M70 30 L74 40 M84 50 L88 54 M66 46 L72 52" stroke={ART.teal.hi} strokeWidth="0.6" opacity="0.6" />
      <Bev t="polygon" points={pts([[62, 12], [90, 12], [92, 18], [90, 24], [62, 24], [60, 18]])} c="teal" o={0.9} />
      <rect x="64" y="15" width="22" height="4" fill={ART.ink} /><rect x="66" y="16" width="9" height="2" fill={S.amber} opacity="0.5" />
      <path d="M60 36 L54 44 L57 54 M92 38 L96 46 L92 56" fill="none" stroke={ART.teal.shade} strokeWidth="3.4" strokeLinecap="square" />
      {[[62, 58], [86, 58], [58, 54], [92, 54]].map(([x, y], i) => <Bev key={i} t="polygon" points={pts(ngon(x, y, 4, 2.4, 6))} c="dark" o={0.4} />)}
      <Grime x={58} y={26} w={36} h={32} seed={8} n={5} op={0.22} />
    </svg>
  );
}

function PropWeldingRig() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={28} ry={4} />
      {/* storage cradle */}
      <Bev t="polygon" points={pts([[4, 56], [4, 44], [18, 44], [18, 52], [46, 52], [46, 44], [60, 44], [60, 56]])} c="dark" o={1} />
      <Hazard x={4} y={54} w={56} h={3} />
      <Rivets x={7} y={47} w={8} n={3} c="dark" /><Rivets x={49} y={47} w={8} n={3} c="dark" />
      {/* articulated arm: industrial yellow segments with hex joints */}
      <Bev t="polygon" points={pts(ngon(18, 46, 7.5, 6.6, 6))} c="steel" o={0.8} /><polygon points={pts(ngon(18, 46, 3.2, 2.8, 6))} fill={ART.dark.shade} />
      <polygon points={pts([[14, 44], [20, 42], [28, 22], [23, 20]])} fill={ART.amber.shade} />
      <Bev t="polygon" points={pts([[13, 45], [19, 42], [27, 22], [22, 20]])} c="amber" o={0.8} />
      <Bev t="polygon" points={pts(ngon(26, 20, 5, 4.6, 6))} c="steel" o={0.6} />
      <Bev t="polygon" points={pts([[26, 17], [48, 10], [49, 15], [27, 23]])} c="amber" o={0.8} />
      <Bev t="polygon" points={pts(ngon(48, 12, 4, 3.6, 6))} c="steel" o={0.5} />
      <Bev t="polygon" points={pts([[46, 14], [52, 24], [48, 27], [43, 18]])} c="dark" o={0.6} />
      <Glow cx={50} cy={27} r={6.4} c={S.orange} opacity={1} style={{ animation:'ship-engine 3s ease-in-out infinite' }} />
      <circle cx="50" cy="27" r="1.6" fill={S.amberHi} />
      {/* tool markings in four different hands */}
      <path d="M24 38 L30 36" stroke={S.red} strokeWidth="1" /><path d="M21 31 L27 31" stroke={S.cyan} strokeWidth="1" /><path d="M35 17 L41 20" stroke={S.green} strokeWidth="1" /><path d="M38 20 L43 16" stroke={S.warn} strokeWidth="1" />
      <Slab x={18} y={47} w={30} h={9} k={1} c="paper" o={0.4} />
      <text x="33" y="50.4" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} fontWeight="700" {...PT}>STN 14 · SAFETY CHECK</text>
      <path d="M21 52 L45 52" stroke={ART.note.ink} strokeWidth="0.4" />
      <text x="33" y="55.2" textAnchor="middle" fontSize="2" fill={ART.note.redInk} {...PT}>3 CYCLES PRIOR · OSSEK</text>
      <Grime x={6} y={20} w={50} h={30} seed={8} n={4} op={0.18} /><Scuff x={14} y={20} w={30} h={26} seed={3} n={8} c="amber" />
    </svg>
  );
}

function PropWayfindingPlaque() {
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={24} ry={3} />
      <Bev t="polygon" points={pts([[6, 58], [6, 18], [12, 18], [12, 12], [20, 12], [20, 6], [44, 6], [44, 12], [52, 12], [52, 18], [58, 18], [58, 58]])} c="bronze" o={1} />
      <polygon points={pts([[8, 56], [8, 20], [14, 20], [14, 14], [22, 14], [22, 8], [42, 8], [42, 14], [50, 14], [50, 20], [56, 20], [56, 56]])} fill="none" stroke={ART.bronze.hi} strokeWidth="0.7" opacity="0.7" />
      <text x="32" y="15" textAnchor="middle" fontSize="3.2" fill={ART.bronze.shade} fontWeight="700" {...PT}>TRANSIT SECTION 3</text>
      <path d="M12 18 L52 18" stroke={ART.bronze.shade} strokeWidth="0.6" />
      {[['PRIVATE EXECUTIVE RING', '(CLEARANCE REQUIRED)', 25], ['TEAM BRIEFING RING', '', 32], ['LOUNGE / MARKET / UNION', '', 38], ['BAY ACCESS BELOW', '', 44]].map(([a, b, y], i) => (
        <g key={i}>
          <polygon points={pts([[10, y - 2], [13, y - 0.6], [10, y + 0.8]])} fill={ART.bronze.shade} />
          <text x="15" y={y} fontSize="2.6" fill={ART.bronze.shade} fontWeight="700" {...PT}>{a}</text>
          {b && <text x="15" y={y + 3} fontSize="2.2" fill={ART.bronze.shade} opacity="0.85" {...PT}>{b}</text>}
        </g>
      ))}
      <path d="M10 49 L54 49" stroke={ART.bronze.shade} strokeWidth="0.5" opacity="0.7" />
      <g transform="rotate(-2 32 53)">
        <text x="10" y="53" fontSize="2.5" fill={ART.bronze.hi} {...PT}>IF LOST, ASK VECTIS.</text>
        <text x="10" y="56" fontSize="2.5" fill={ART.bronze.hi} {...PT}>IT WILL TELL YOU TWICE.</text>
        <path d="M10 49.8 L26 51.6 M30 50.4 L50 51.4" stroke={ART.bronze.hi} strokeWidth="0.4" opacity="0.7" />
      </g>
      {[[9, 22], [55, 22], [9, 54], [55, 54]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1.1" fill={ART.bronze.hi} />)}
      <Grime x={7} y={20} w={50} h={36} seed={13} n={4} op={0.2} />
      <Rivets x={10} y={19.4} w={44} n={7} c="bronze" /><Scuff x={8} y={20} w={48} h={34} seed={4} n={8} c="bronze" />
    </svg>
  );
}

function PropBlastPanel() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <Slab x={2} y={2} w={60} h={60} k={2} c="dark" />
      <Hazard x={4} y={4} w={56} h={7} /><Hazard x={4} y={53} w={56} h={7} /><Hazard x={4} y={4} w={6} h={56} /><Hazard x={54} y={4} w={6} h={56} />
      <Slab x={10} y={12} w={44} h={31} k={1.6} c="steel" o={0.8} />
      <rect x="12" y="14" width="40" height="27" fill={ART.screen.off} />
      {/* hydraulic override wheel */}
      <Bev t="polygon" points={pts(ngon(32, 27.5, 11.5, 11.5, 12))} c="red" o={0.8} />
      <polygon points={pts(ngon(32, 27.5, 8, 8, 12))} fill={ART.screen.off} />
      <path d="M32 17 L32 38 M21.5 27.5 L42.5 27.5 M24.6 20 L39.4 35 M39.4 20 L24.6 35" stroke={ART.red.base} strokeWidth="1.5" />
      <Bev t="polygon" points={pts(ngon(32, 27.5, 3.2, 3.2, 6))} c="steel" o={0.4} />
      <Slab x={44} y={34} w={8} h={6} k={0.8} c="steel" o={0.4} />
      <path d="M36 12 A9 9 0 0 0 28 12" fill="none" stroke={S.warn} strokeWidth="0.9" /><polygon points={pts([[28, 10.5], [25.6, 13.4], [30, 13]])} fill={S.warn} />
      <polygon points={pts([[12, 14], [30, 14], [12, 30]])} fill={S.white} opacity="0.12" />
      <path d="M44 14 L40 22 L47 26 L43 34 M47 26 L52 23" fill="none" stroke={S.cyanHi} strokeWidth="0.6" opacity="0.8" />
      <rect x="10" y="45" width="44" height="8" fill={ART.paper.base} />
      <text x="32" y="48" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} fontWeight="700" {...PT}>HULL BREACH OR FIRE</text>
      <text x="32" y="50.4" textAnchor="middle" fontSize="2" fill={ART.note.ink} {...PT}>3 ROTATIONS COUNTERCLOCKWISE</text>
      <text x="32" y="52.4" textAnchor="middle" fontSize="1.9" fill={ART.note.redInk} {...PT}>THIS MEANS YOU, BAY CREW. -PRENN</text>
      <Grime x={10} y={12} w={44} h={40} seed={4} n={4} op={0.16} />
      <Rivets x={11} y={43.4} w={42} n={6} /><Scuff x={10} y={12} w={44} h={40} seed={6} n={8} />
    </svg>
  );
}

function PropHoloProjector() {
  const S = ART.signal;
  const emit = ngon(48, 84, 31, 8.4, 8, Math.PI / 8);
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={89} rx={34} ry={4.6} />
      <polygon points={pts([[27, 82], [12, 26], [84, 26], [69, 82]])} fill={S.cyan} opacity="0.1" />
      <polyline points={pts([[30, 82], [20, 30]])} stroke={S.cyan} strokeWidth="0.6" opacity="0.5" fill="none" /><polyline points={pts([[66, 82], [76, 30]])} stroke={S.cyan} strokeWidth="0.6" opacity="0.5" fill="none" />
      {/* hologram of an unregistered Star Dreadnought: wedge hull, tower, engine block */}
      <g style={{ animation:'holo-flicker 2.6s ease-in-out infinite' }}>
        <polygon points={pts([[16, 54], [66, 40], [84, 50], [66, 58], [16, 58]])} fill={S.cyan} opacity="0.12" />
        <polyline points={pts([[16, 54], [66, 40], [84, 50], [66, 58], [16, 58], [16, 54]])} fill="none" stroke={S.cyanHi} strokeWidth="1" />
        <polyline points={pts([[16, 56], [84, 50]])} fill="none" stroke={S.cyan} strokeWidth="0.6" />
        <polyline points={pts([[30, 52], [30, 57], [42, 53], [42, 57], [54, 50], [54, 56]])} fill="none" stroke={S.cyan} strokeWidth="0.6" opacity="0.8" />
        <polygon points={pts([[34, 52], [40, 46], [46, 51]])} fill="none" stroke={S.cyanHi} strokeWidth="0.8" />
        <rect x="14" y="52" width="4" height="7" fill="none" stroke={S.cyanHi} strokeWidth="0.7" />
        {Array.from({ length: 19 }, (_, i) => <line key={i} x1="14" y1={28 + i * 3} x2="82" y2={28 + i * 3} stroke={S.cyan} strokeWidth="0.3" opacity="0.18" />)}
      </g>
      <line x1="16" y1="32" x2="42" y2="32" stroke={S.amber} strokeWidth="0.9" /><path d="M16 30 L16 34 M29 30.6 L29 33.4 M42 30 L42 34" stroke={S.amber} strokeWidth="0.8" />
      <text x="29" y="28.6" textAnchor="middle" fontSize="3" fill={S.amber} {...PT}>1800 M</text>
      {/* emitter base */}
      <Bev t="polygon" points={pts(emit)} c="dark" o={1.2} />
      <polygon points={pts(ngon(48, 83, 24, 6, 8, Math.PI / 8))} fill={ART.screen.off} stroke={S.cyan} strokeWidth="0.7" />
      <polygon points={pts(ngon(48, 83, 12, 3, 8, Math.PI / 8))} fill={ART.ink} stroke={S.cyan} strokeWidth="0.5" />
      <Glow cx={48} cy={83} rx={6} ry={1.8} c={S.cyan} opacity={1} style={{ animation:'ship-engine 2s ease-in-out infinite' }} />
      {emit.map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1" fill={i % 2 ? S.cyan : S.amber} style={{ animation:`ship-blink 2.2s ease-in-out ${i * 0.25}s infinite` }} />)}
      <Vent x={10} y={86} w={14} h={3} n={2} /><Vent x={72} y={86} w={14} h={3} n={2} />
      {/* clearance menus */}
      <Slab x={3} y={5} w={30} h={17} k={1.2} c="dark" /><rect x="5" y="7" width="26" height="13" fill={ART.screen.off} />
      <rect x="6" y="8" width="24" height="3.4" fill={ART.red.shade} /><text x="8" y="10.6" fontSize="2.2" fill={S.redHi} {...PT}>LOCKED · LV 6</text>
      <rect x="6" y="12" width="24" height="3.4" fill={ART.red.shade} /><text x="8" y="14.6" fontSize="2.2" fill={S.redHi} {...PT}>LOCKED · LV 6</text>
      <rect x="6" y="16" width="24" height="3.4" fill={ART.amber.shade} style={{ animation:'holo-flicker 2s ease-in-out infinite' }} /><text x="7.4" y="18.6" fontSize="1.8" fill={S.amberHi} {...PT}>STRUCTURAL TOLERANCE</text>
      <text x="72" y="12" textAnchor="middle" fontSize="2.6" fill={S.cyan} {...PT}>UNREGISTERED HULL</text>
      <Grime x={14} y={78} w={68} h={10} seed={3} n={4} op={0.2} /><Scuff x={14} y={78} w={68} h={10} seed={5} n={6} c="dark" />
    </svg>
  );
}

function PropSecureLocker() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3.4} />
      <Slab x={2} y={4} w={92} h={54} k={1.6} c="dark" />
      <Slab x={2} y={3} w={92} h={5} k={1} c="steel" o={0.8} />
      <text x="48" y="7" textAnchor="middle" fontSize="2.4" fill={ART.ink} fontWeight="700" {...PT}>SECURE DATAPAD CHARGING · ISB · 12 UNITS</text>
      {[0, 1].map((r) => [0, 1, 2, 3, 4, 5].map((c) => {
        const x = 5 + c * 15, y = 10 + r * 24;
        const cracked = r === 1 && c === 3;
        return (
          <g key={`${r}${c}`}>
            <Slab x={x} y={y} w={13} h={22} k={1.2} c="steel" o={0.8} />
            <Slab x={x + 2.5} y={y + 3} w={8} h={6} k={0.8} c="dark" o={0.4} />
            {cracked ? (
              <g>
                <polyline points={pts([[x + 3, y + 3], [x + 6, y + 6], [x + 4.5, y + 8], [x + 8, y + 9], [x + 10, y + 7.6]])} fill="none" stroke={S.orange} strokeWidth="0.8" />
                <rect x={x + 2} y={y + 19} width="9" height="2.6" fill={S.cyan} opacity="0.7" style={{ animation:'ship-engine 1.6s ease-in-out infinite' }} />
              </g>
            ) : (
              <g>
                <polygon points={pts(ngon(x + 6.5, y + 6, 2.2, 1.6, 6))} fill="none" stroke={S.red} strokeWidth="0.6" />
                <circle cx={x + 6.5} cy={y + 6} r="0.7" fill={S.red} />
              </g>
            )}
            <rect x={x + 5} y={y + 12} width="3" height="5" fill={ART.dark.base} />
            <circle cx={x + 10.5} cy={y + 19} r="0.7" fill={cracked ? S.orange : S.green} style={{ animation:`ship-engine ${3 + c * 0.4}s ease-in-out infinite` }} />
          </g>
        );
      }))}
      <Grime x={4} y={9} w={88} h={48} seed={21} n={6} op={0.16} /><Scuff x={5} y={10} w={86} h={46} seed={7} n={12} />
      <Rivets x={6} y={56.4} w={84} n={10} />
    </svg>
  );
}

function PropInterceptWall() {
  const S = ART.signal;
  const officer = (cx, k, arms) => (
    <g key={k}>
      <Bev t="polygon" points={pts(ngon(cx, 26, 5, 5.6, 8))} c={{ hi: ART.skin.light, base: ART.skin.mid, shade: ART.skin.dark }} o={0.4} />
      <polygon points={pts([[cx - 6, 24], [cx - 4, 18], [cx + 4, 18], [cx + 6, 24]])} fill={ART.dark.base} />
      <rect x={cx - 7} y="22" width="14" height="2" fill={ART.dark.shade} /><circle cx={cx} cy="20.4" r="0.9" fill={ART.brass.hi} />
      <Bev t="polygon" points={pts([[cx - 11, 38], [cx - 6, 32], [cx + 6, 32], [cx + 11, 38], [cx + 12, 58], [cx - 12, 58]])} c="dark" o={0.7} />
      <rect x={cx - 6} y="35" width="3" height="2" fill={ART.red.base} /><rect x={cx - 2} y="35" width="3" height="2" fill={ART.brass.base} />
      <path d={`M${cx} 32 L${cx} 58`} stroke={ART.dark.shade} strokeWidth="0.6" />
      {arms}
    </g>
  );
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={3} />
      <Slab x={3} y={4} w={122} h={54} k={2.4} c="steel" o={1} />
      <rect x="7" y="8" width="114" height="46" fill={ART.screen.space} />
      <rect x="7" y="8" width="114" height="46" fill={ART.dark.base} opacity="0.7" />
      <g>
        <polygon points={pts([[12, 20], [42, 14], [42, 46], [12, 40]])} fill={S.cyan} opacity="0.12" />
        <polyline points={pts([[12, 20], [42, 14], [42, 46], [12, 40], [12, 20]])} fill="none" stroke={S.cyan} strokeWidth="0.7" />
        <polyline points={pts([[16, 36], [24, 28], [38, 24], [40, 30], [26, 36], [16, 36]])} fill="none" stroke={S.cyanHi} strokeWidth="0.6" />
        <text x="27" y="43" textAnchor="middle" fontSize="2.4" fill={S.cyan} {...PT}>HULL 1800 M</text>
      </g>
      {officer(62, 'a', <g><path d="M72 38 L92 28" stroke={ART.dark.base} strokeWidth="5" /><circle cx="93.5" cy="27" r="2.2" fill={ART.skin.mid} /><path d="M94 27 L102 22" stroke={ART.skin.mid} strokeWidth="1.2" /></g>)}
      {officer(102, 'b', <g><path d="M92 44 L112 48 M92 49 L112 45" stroke={ART.dark.base} strokeWidth="3.4" /><path d="M92 44 L112 48 M92 49 L112 45" stroke={ART.dark.hi} strokeWidth="0.8" /></g>)}
      <rect x="7" y="8" width="114" height="46" fill={ART.glass.base} opacity="0.1" />
      <polygon points={pts([[10, 9], [38, 9], [10, 42]])} fill={S.white} opacity="0.1" />
      <polygon points={pts([[84, 9], [100, 9], [76, 54], [68, 54]])} fill={S.white} opacity="0.06" />
      {[10, 30, 50, 70, 90, 110].map((x) => <circle key={x} cx={x} cy="6" r="0.9" fill={ART.steel.hi} />)}
      <Slab x={36} y={48} w={56} h={7} k={1} c="dark" />
      <text x="64" y="53.2" textAnchor="middle" fontSize="3" fill={ART.amber.hi} fontStyle="italic" {...PT}>(lip read) "not in the budget."</text>
      <Grime x={5} y={6} w={118} h={50} seed={17} n={5} op={0.14} />
      <Rivets x={10} y={56.2} w={108} n={12} />
    </svg>
  );
}

function PropTerminal({ variant }) {
  const S = ART.signal;
  const cfg = {
    engineering: { title:'KDY ENGINEERING ACCESS', rows:['SECTOR 4 · BADOR MOON', 'ASSEMBLY ARCHIVE', 'LAST USED 40 MIN AGO', 'NOT A SCHEDULED AUDIT'], body:'steel', screen:ART.screen.cyan, ink:S.cyanHi, trim:S.cyan },
    executive: { title:'KDY PRIVATE SHIPPING NET', rows:['EXECUTIVE HOLONET', 'ACCESS CODES: LOADED', 'USER: TANNER', 'ROUTE: ALL RINGS'], body:'brass', screen:ART.screen.amber, ink:S.gold, trim:S.amber },
    privileged: { title:'UNMONITORED BACKBONE', rows:['HIGH BANDWIDTH · NO TRACE', 'RELAY: ANY', 'OVERSIGHT CODE READY', 'PRE-CENSORSHIP ARCH'], body:'dark', screen:ART.screen.green, ink:S.greenHi, trim:S.green },
  }[variant] || {};
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={24} ry={3.6} />
      <Slab x={22} y={44} w={20} h={12} k={1.4} c="dark" /><Vent x={25} y={47} w={14} h={6} n={3} />
      <Slab x={12} y={54} w={40} h={5} k={1.4} c="steel" o={0.8} />
      {/* wedge console head with a recessed screen and button banks */}
      <Bev t="polygon" points={pts([[8, 5], [56, 5], [60, 10], [60, 42], [56, 46], [8, 46], [4, 42], [4, 10]])} c={cfg.body} o={1} />
      <Seams x={4} y={5} w={56} h={41} rows={1} cols={1} op={0.25} />
      <rect x="9" y="9" width="46" height="26" fill={cfg.screen} />
      <rect x="9" y="9" width="46" height="4.6" fill={cfg.trim} opacity="0.3" />
      <text x="11" y="12.6" fontSize="2.8" fill={cfg.ink} fontWeight="700" {...PT}>{cfg.title}</text>
      {cfg.rows.map((r, i) => <text key={i} x="11.5" y={18 + i * 4.4} fontSize="2.4" fill={cfg.ink} opacity={0.95 - i * 0.12} {...PT}>{'> ' + r}</text>)}
      <rect x="9" y="9" width="46" height="1.6" fill={cfg.ink} opacity="0.3" style={{ animation:'prop-scan 3.4s linear infinite' }} />
      {variant === 'executive' && <g fill="none" stroke={S.gold} strokeWidth="0.5" opacity="0.85"><circle cx="49" cy="26" r="3.6" /><path d="M45.4 26 L52.6 26 M49 22.4 L49 29.6" /></g>}
      {variant === 'privileged' && <path d="M47 24 L51 28 M51 24 L47 28" fill="none" stroke={S.green} strokeWidth="0.6" opacity="0.7" />}
      <Toggles x={8} y={37} cols={10} rows={2} gap={3.6} seed={variant === 'executive' ? 3 : variant === 'privileged' ? 9 : 5} />
      <Lights x={46} y={39} n={4} gap={3.4} seed={4} />
      <Slab x={46} y={36} w={11} h={7} k={0.8} c="dark" o={0.4} />
      <line x1="55" y1="5" x2="55" y2="0.6" stroke={ART.steel.hi} strokeWidth="0.9" /><circle cx="55" cy="0.8" r="0.9" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      <Grime x={5} y={6} w={54} h={38} seed={12} n={4} op={0.15} /><Scuff x={6} y={7} w={50} h={36} seed={4} n={7} c={cfg.body} />
    </svg>
  );
}

function PropHoloTable({ variant }) {
  const S = ART.signal;
  const dossier = variant === 'dossier';
  const top = ngon(48, 46, 36, 11, 8, Math.PI / 8);
  const hammer = (x, y, c, k) => (
    <g key={k} fill="none" stroke={c} strokeWidth="0.8">
      <polyline points={pts([[x, y], [x + 18, y], [x + 22, y + 2], [x + 18, y + 4], [x, y + 4], [x, y]])} />
      <polyline points={pts([[x + 18, y - 3], [x + 19, y - 3], [x + 19, y + 7], [x + 18, y + 7], [x + 18, y - 3]])} />
    </g>
  );
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={58} rx={42} ry={5} />
      <Slab x={36} y={48} w={24} h={8} k={1.4} c="dark" />
      {[16, 80].map((x) => <polygon key={x} points={pts([[x - 3, 48], [x + 3, 48], [x + 4, 58], [x - 4, 58]])} fill={ART.dark.base} />)}
      <Bev t="polygon" points={pts(top)} c="steel" o={1.2} />
      <polygon points={pts(ngon(48, 45.5, 29, 8.4, 8, Math.PI / 8))} fill={ART.screen.off} stroke={S.cyan} strokeWidth="0.8" />
      <polygon points={pts(ngon(48, 45.5, 17, 4.8, 8, Math.PI / 8))} fill={ART.screen.cyan} stroke={S.cyan} strokeWidth="0.5" opacity="0.9" />
      {top.map(([x, y], i) => <circle key={i} cx={x} cy={y - 1} r="0.9" fill={i % 2 ? S.cyan : S.amber} style={{ animation:`ship-blink ${2 + i * 0.3}s ease-in-out infinite` }} />)}
      <Glow cx={48} cy={45} rx={20} ry={5} c={S.cyan} opacity={0.6} style={{ animation:'holo-flicker 3s ease-in-out infinite' }} />
      {!dossier && (
        <g style={{ animation:'holo-flicker 3s ease-in-out infinite' }}>
          {hammer(12, 28, S.cyanHi, 'h1')}{hammer(14, 20, S.cyan, 'h2')}
          <text x="24" y="38" textAnchor="middle" fontSize="2.2" fill={S.cyanHi} {...PT}>HAMMERHEAD 62%</text>
          <polygon points={pts([[44, 14], [70, 10], [78, 18], [70, 24], [44, 22], [47, 18]])} fill={S.amber} opacity="0.18" />
          <polyline points={pts([[44, 14], [70, 10], [78, 18], [70, 24], [44, 22], [47, 18], [44, 14]])} fill="none" stroke={S.amber} strokeWidth="1" />
          <path d="M52 11 L54 23 M60 10 L62 23" stroke={S.amber} strokeWidth="0.6" />
          <text x="60" y="31" textAnchor="middle" fontSize="2.6" fill={S.amber} fontWeight="700" {...PT}>VL-9 · DEVIATION</text>
          <polyline points={pts([[80, 24], [86, 21], [92, 24], [86, 27], [80, 24]])} fill="none" stroke={S.cyanHi} strokeWidth="0.7" />
          <text x="86" y="33" textAnchor="middle" fontSize="2.2" fill={S.cyanHi} {...PT}>THRANTA 88%</text>
        </g>
      )}
      {dossier && (
        <g style={{ animation:'holo-flicker 3s ease-in-out infinite' }}>
          <rect x="22" y="6" width="52" height="27" fill={ART.screen.cyan} opacity="0.85" /><rect x="22" y="6" width="52" height="27" fill="none" stroke={S.cyan} strokeWidth="0.7" />
          <text x="25" y="11.6" fontSize="2.8" fill={S.amber} fontWeight="700" {...PT}>OPERATION GHOST KEEL · LEVEL 8</text>
          <path d="M24 13.4 L72 13.4" stroke={S.cyan} strokeWidth="0.4" />
          <text x="25" y="18.4" fontSize="2.5" fill={S.cyanHi} {...PT}>1. SECTOR 4 GRID COORDINATES</text>
          <text x="25" y="22.4" fontSize="2.5" fill={S.cyanHi} {...PT}>2. KDY SURFACE PERSONNEL FILES</text>
          <text x="25" y="26.4" fontSize="2.5" fill={S.cyanHi} {...PT}>3. CMDR VAEL OPERATIONAL BRIEF</text>
          <text x="25" y="30.6" fontSize="2.3" fill={S.cyan} opacity="0.8" {...PT}>BADOR MOON · RESTRICTED</text>
        </g>
      )}
      <Rivets x={20} y={52} w={56} n={8} /><Grime x={14} y={44} w={68} h={12} seed={6} n={5} op={0.2} /><Scuff x={14} y={40} w={68} h={14} seed={7} n={8} />
    </svg>
  );
}

function PropOrdnanceCrates() {
  const S = ART.signal;
  const crate = (x, y, w, h, key, main) => (
    <g key={key}>
      <Slab x={x} y={y} w={w} h={h} k={2} c="dark" o={1} />
      <Seams x={x} y={y} w={w} h={h} cols={main ? 3 : 1} rows={main ? 1 : 0} op={0.4} />
      <rect x={x + 2} y={y + 2} width={w - 4} height="1.4" fill={ART.dark.hi} opacity="0.7" />
      <Hazard x={x + 1} y={y + h - 5} w={w - 2} h={4} />
      <Slab x={x + w / 2 - 3.5} y={y + h / 2 - 3} w={7} h={6} k={1} c="red" o={0.6} />
      <circle cx={x + w / 2} cy={y + h / 2} r="1.1" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      <Rivets x={x + 3} y={y + 3} w={w - 6} n={main ? 5 : 3} c="dark" />
      {main && <polygon points={pts([[x + w - 11, y + 8], [x + w - 5, y + 8], [x + w - 8, y + 3]])} fill={S.warn} />}
      <Scuff x={x} y={y} w={w} h={h} seed={x + y} n={5} c="dark" />
    </g>
  );
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={4} />
      {crate(4, 24, 44, 32, 'main', true)}
      <text x="26" y="33" textAnchor="middle" fontSize="2.6" fill={ART.hull.hi} fontWeight="700" {...PT}>COMPONENT CLASS RESTRICTED</text>
      <text x="26" y="36.6" textAnchor="middle" fontSize="2.3" fill={ART.hull.base} {...PT}>AUTH CODE REQUIRED</text>
      <text x="26" y="40" textAnchor="middle" fontSize="2.3" fill={S.orange} {...PT}>DO NOT OPEN IN ATMOSPHERE</text>
      {crate(52, 38, 20, 18, 'b1')}{crate(74, 38, 20, 18, 'b2')}{crate(62, 20, 20, 18, 'b3')}
      <g transform="rotate(-6 40 50)">
        <polygon points={pts([[30, 46], [48, 45], [49, 47], [47, 49], [49, 52], [31, 53]])} fill={ART.paper.base} />
        <text x="32" y="49.6" fontSize="2" fill={ART.note.ink} {...PT}>KUAT SURFACE</text>
        <text x="32" y="52" fontSize="2" fill={ART.note.ink} {...PT}>FACILITY BADOR-7</text>
      </g>
    </svg>
  );
}

function PropArmoryLocker() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={3.6} />
      <Slab x={6} y={54} w={52} h={5} k={1} c="dark" />
      {[10, 22, 42, 54].map((x) => <Bev key={x} t="polygon" points={pts(ngon(x, 56.5, 1.8, 1.6, 6))} c="steel" o={0.3} />)}
      <Slab x={9} y={4} w={46} h={51} k={2.4} c="steel" o={1} />
      <rect x="12" y="9" width="40" height="42" fill="none" stroke={ART.ink} strokeWidth="0.7" opacity="0.5" />
      <line x1="32" y1="9" x2="32" y2="51" stroke={ART.ink} strokeWidth="0.9" opacity="0.6" />
      <Rivets x={12} y={7} w={40} n={6} /><Rivets x={12} y={52} w={40} n={6} />
      <Vent x={14} y={12} w={14} h={6} n={3} />
      <text x="20" y="28" textAnchor="middle" fontSize="2.6" fill={ART.dark.base} fontWeight="700" {...PT}>ARMORY</text>
      <text x="20" y="31.6" textAnchor="middle" fontSize="2.2" fill={ART.dark.base} {...PT}>BRIEFING RING</text>
      <Hazard x={14} y={38} w={14} h={4} />
      {/* worn palm reader with splicer scoring */}
      <Slab x={36} y={20} w={14} h={20} k={1.6} c="dark" o={1} />
      <rect x="37.6" y="21.6" width="10.8" height="16.8" fill={ART.screen.cyan} />
      <path d="M40 34 L40 27 M42 34 L42 25 M44 34 L44 26 M46 34 L46 28 M40 34 Q43 38 47 34 M47 31 L49 30" fill="none" stroke={S.cyanLo} strokeWidth="0.9" strokeLinecap="round" />
      <rect x="37.6" y="21.6" width="10.8" height="1.4" fill={S.cyan} opacity="0.6" style={{ animation:'prop-scan 3s linear infinite' }} />
      <circle cx="46.6" cy="18" r="1" fill={S.red} style={{ animation:'ship-blink 1.2s steps(2) infinite' }} />
      <path d="M36 19 L33 24 M38 17 L34 22 M50 23 L53 26 M49 41 L53 40" stroke={ART.steel.hi} strokeWidth="0.8" />
      <path d="M35 19 L40 22 M34 40 L37 43 M50 22 L48 25" stroke={ART.rust.base} strokeWidth="0.6" opacity="0.9" />
      <polygon points={pts([[33, 29], [30, 30.4], [31, 32.4], [34, 31]])} fill={ART.steel.hi} />
      <Grime x={10} y={6} w={44} h={46} seed={9} n={5} op={0.2} /><Scuff x={10} y={6} w={44} h={46} seed={2} n={10} />
    </svg>
  );
}

function PropCableTruss() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={52} rx={58} ry={4} />
      {[14, 54, 94, 118].map((x) => <g key={x}><Slab x={x - 1.4} y={6} w={2.8} h={14} k={0.6} c="steel" o={0.5} /><Slab x={x - 5} y={5} w={10} h={2.6} k={0.8} c="steel" o={0.5} /></g>)}
      <Bev t="polygon" points={pts([[2, 18], [126, 18], [123, 31], [5, 31]])} c="steel" o={1} />
      <rect x="6" y="19" width="116" height="1.4" fill={ART.steel.hi} opacity="0.7" />
      <Seams x={4} y={18} w={120} h={13} cols={9} op={0.3} />
      {/* the old armored bundle: darker, original alloy */}
      {[0, 1, 2].map((i) => <path key={i} d={`M2 ${22 + i * 2.6} Q40 ${20 + i * 2.6} 64 ${23 + i * 2.6} T126 ${22 + i * 2.6}`} fill="none" stroke={[ART.dark.shade, ART.dark.base, ART.bronze.shade][i]} strokeWidth="2.2" />)}
      <Rivets x={8} y={29.4} w={108} n={12} />
      {/* newer rerouted lines */}
      <Cable d="M52 23 Q62 38 76 44 Q92 50 110 46" c={ART.deepglass.base} w={3.2} hi="steel" />
      <Cable d="M58 23 Q68 40 80 48 Q96 54 112 50" c="#E8731A" w={3} hi="copper" />
      <Slab x={48} y={19} w={14} h={9} k={1.2} c="dark" /><Lights x={50} y={26} n={4} gap={3} seed={3} />
      {[22, 40, 56, 70, 84, 100, 114].map((x) => <rect key={x} x={x} y="17" width="3.2" height="14" fill={ART.steel.hi} opacity="0.85" />)}
      {[[78, 44], [90, 49], [104, 49]].map(([x, y], i) => <rect key={i} x={x} y={y} width="3" height="6" fill={ART.hull.base} transform={`rotate(-14 ${x} ${y})`} />)}
      <Slab x={6} y={33} w={28} h={9} k={0.8} c="paper" o={0.4} />
      <text x="20" y="37.6" textAnchor="middle" fontSize="2.8" fill={ART.note.ink} fontWeight="700" {...PT}>SECTION B-12</text>
      <text x="20" y="40.6" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} {...PT}>REROUTED · NEW LINE</text>
      <Grime x={4} y={19} w={118} h={12} seed={6} n={6} op={0.2} />
    </svg>
  );
}

function PropCoolantPipe() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3.6} />
      {/* high pressure pipe run with flanges */}
      <Bev t="polygon" points={pts([[2, 31], [8, 26], [88, 26], [94, 31], [94, 39], [88, 44], [8, 44], [2, 39]])} c="steel" o={1} />
      <rect x="10" y="28" width="76" height="1.6" fill={ART.steel.hi} opacity="0.8" />
      <rect x="10" y="41" width="76" height="1.4" fill={ART.steel.shade} />
      {[18, 38, 62, 82].map((x) => <Slab key={x} x={x - 2} y={24} w={5} h={22} k={1} c="dark" o={0.7} />)}
      {[[24, 42], [44, 43], [58, 40], [72, 42]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="0.9" fill={S.cyanHi} opacity="0.8" />)}
      <Seams x={8} y={26} w={80} h={18} cols={4} op={0.2} />
      {/* valve and wheel */}
      <Slab x={42} y={13} w={8} h={13} k={1} c="dark" />
      <Bev t="polygon" points={pts(ngon(46, 10, 7, 7, 8))} c="amber" o={0.7} />
      <path d="M46 4 L46 16 M40 10 L52 10 M41.8 5.8 L50.2 14.2 M50.2 5.8 L41.8 14.2" stroke={ART.amber.shade} strokeWidth="1" />
      <Bev t="polygon" points={pts(ngon(46, 10, 2.2, 2.2, 6))} c="steel" o={0.3} />
      {/* pressure readout above nominal */}
      <Slab x={6} y={5} w={18} h={13} k={1.4} c="dark" />
      <rect x="8" y="7" width="14" height="9" fill={ART.screen.off} />
      <rect x="9" y="8.6" width="12" height="1.6" fill={ART.green} /><rect x="9" y="8.6" width="12" height="1.6" fill={S.green} opacity="0.5" />
      <rect x="9" y="11" width="12" height="1.6" fill={S.amber} /><rect x="9" y="13.4" width="10" height="1.6" fill={S.red} style={{ animation:'ship-engine 1.4s ease-in-out infinite' }} />
      <text x="15" y="22" textAnchor="middle" fontSize="2.4" fill={S.red} fontWeight="700" {...PT}>ABOVE NOM</text>
      {/* safety cage and amber indicators */}
      {[10, 22, 34, 46, 58, 70, 82].map((x) => <rect key={x} x={x} y="20" width="2" height="34" fill={ART.signal.warn} />)}
      <rect x="2" y="20" width="92" height="2.6" fill={S.warn} /><rect x="2" y="51" width="92" height="2.6" fill={S.warn} />
      <Hazard x={2} y={21.6} w={92} h={1} />
      {[60, 70, 80].map((x, i) => <g key={x}><Glow cx={x} cy={16} r={5} c={S.amber} opacity={0.6} style={{ animation:`ship-blink 1.8s ease-in-out ${i * 0.6}s infinite` }} /><circle cx={x} cy="16" r="1.8" fill={S.amber} style={{ animation:`ship-blink 1.8s ease-in-out ${i * 0.6}s infinite` }} /></g>)}
      <Slab x={58} y={54} w={34} h={9} k={0.8} c="paper" o={0.4} />
      <text x="75" y="57.4" textAnchor="middle" fontSize="2.1" fill={ART.note.redInk} fontWeight="700" {...PT}>DO NOT OPERATE W/O ENG</text>
      <text x="75" y="60" textAnchor="middle" fontSize="2" fill={ART.note.ink} {...PT}>VALVE SEQUENCE REQUIRED</text>
      <text x="75" y="62.4" textAnchor="middle" fontSize="1.8" fill={ART.note.ink} {...PT}>WORK ORDER FILED · 2 CYCLES</text>
      <path d="M30 24 Q28 18 31 14 Q34 10 31 6" fill="none" stroke={S.white} strokeWidth="1.4" opacity="0.2" style={{ animation:'steam-rise 3s ease-in-out infinite' }} />
      <Grime x={4} y={27} w={88} h={16} seed={14} n={5} op={0.2} />
    </svg>
  );
}

function PropSabaccTable() {
  const S = ART.signal;
  const skin = (k) => ({ hi: ART.skin[k] || ART.skin.light, base: ART.skin[k] || ART.skin.mid, shade: ART.skin.dark });
  const chips = (cx, cy, n, c) => Array.from({ length: n }, (_, i) => <Bev key={i} t="polygon" points={pts(ngon(cx, cy - i * 1.5, 3.4, 1.8, 6))} c={c} o={0.3} />);
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={80} rx={42} ry={9} />
      {/* octagonal cantina table, worn felt */}
      <Bev t="polygon" points={pts(ngon(48, 50, 41, 38, 16, Math.PI / 16))} c="wood" o={1.2} />
      <polygon points={pts(ngon(48, 50, 35, 32, 16, Math.PI / 16))} fill={ART.cloth.felt} />
      <polygon points={pts(ngon(48, 50, 35, 32, 16, Math.PI / 16))} fill="none" stroke={ART.canvas.hi} strokeWidth="2.6" opacity="0.3" />
      <polygon points={pts(ngon(48, 50, 17, 15, 16, Math.PI / 16))} fill="none" stroke={ART.brass.base} strokeWidth="0.6" opacity="0.5" />
      <polyline points={pts([[20, 36], [48, 16], [76, 36]])} fill="none" stroke={S.white} strokeWidth="1" opacity="0.14" />
      {/* the pot */}
      <g>{chips(48, 52, 4, 'brass')}</g><g>{chips(42, 50, 3, 'red')}</g><g>{chips(54, 50, 3, 'deepglass')}</g>
      {/* cards: three on the empty seat, a hand each side */}
      {[[38, 18, -10], [45, 17, 4], [51, 19, 16]].map(([x, y, r], i) => <g key={i} transform={`rotate(${r} ${x + 3.5} ${y + 5})`}><rect x={x} y={y} width="7" height="10" fill={ART.paper.hi} /><rect x={x + 1.4} y={y + 1.4} width="4" height="1.2" fill={ART.red.base} /></g>)}
      <g transform="rotate(80 67 50)"><rect x="64" y="46" width="6" height="8" fill={ART.paper.hi} /></g><g transform="rotate(100 73 54)"><rect x="70" y="50" width="6" height="8" fill={ART.paper.hi} /></g>
      {/* the off duty KDY engineer, bottom seat */}
      <Bev t="polygon" points={pts(ngon(48, 86, 8, 8, 8))} c={skin('mid')} o={0.4} />
      <Bev t="polygon" points={pts([[38, 96], [40, 88], [56, 88], [58, 96]])} c="amber" o={0.6} />
      <rect x="44" y="90" width="8" height="2" fill={ART.deepglass.base} />
      <rect x="41" y="68" width="14" height="9" fill={ART.paper.hi} /><path d="M43 70 L53 70 M43 72 L52 72 M43 74 L50 74" stroke={ART.ink} strokeWidth="0.4" />
      {/* the Rodian shipwright, left seat */}
      <Bev t="polygon" points={pts(ngon(12, 46, 7, 7, 8))} c={{ hi: ART.skin.rodian, base: ART.skin.rodian, shade: ART.skin.rodianShade }} o={0.4} />
      <polygon points={pts([[4, 44], [0, 42], [2, 48]])} fill={ART.skin.rodianShade} />
      <circle cx="10" cy="44.4" r="1.4" fill={ART.ink} /><circle cx="15" cy="44.4" r="1.4" fill={ART.ink} />
      <Bev t="polygon" points={pts([[6, 62], [10, 54], [20, 54], [24, 62]])} c="steel" o={0.5} />
      <g transform="rotate(-20 24 47)"><rect x="19" y="44" width="10" height="6" fill={ART.paper.hi} /></g>
      {/* the Imperial logistics clerk, right seat */}
      <Bev t="polygon" points={pts(ngon(84, 48, 7, 7, 8))} c={skin('light')} o={0.4} />
      <polygon points={pts([[78, 44], [90, 44], [88, 40], [80, 40]])} fill={ART.dark.base} />
      <Bev t="polygon" points={pts([[76, 68], [78, 56], [90, 56], [94, 68]])} c="dark" o={0.6} />
      <rect x="78" y="58" width="4" height="2.4" fill={ART.red.base} /><rect x="83" y="58" width="4" height="2.4" fill={ART.brass.base} />
      {/* the empty chair at the top: jacket draped over the back */}
      <g transform="rotate(-18 48 14)">
        <Slab x={34} y={6} w={28} h={13} k={2} c="dark" o={0.7} />
        <Bev t="polygon" points={pts([[38, 12], [48, 6], [58, 12], [56, 20], [40, 20]])} c="steel" o={0.5} />
      </g>
      <Grime x={10} y={20} w={76} h={60} seed={5} n={5} op={0.1} />
      <Rivets x={20} y={84.6} w={56} n={8} c="wood" /><Scuff x={12} y={14} w={72} h={64} seed={9} n={14} c="wood" />
    </svg>
  );
}

function PropMarketStall() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={42} ry={3.4} />
      {/* plain canvas awning with a straight cut fringe */}
      <Bev t="polygon" points={pts([[4, 6], [92, 6], [96, 24], [0, 24]])} c="canvas" o={0.9} />
      {[10, 24, 38, 52, 66, 80].map((x) => <line key={x} x1={x} y1="6" x2={x - 2} y2="24" stroke={ART.ink} strokeWidth="0.6" opacity="0.4" />)}
      <polyline points={pts([[0, 24], [6, 28], [12, 24], [18, 28], [24, 24], [30, 28], [36, 24], [42, 28], [48, 24], [54, 28], [60, 24], [66, 28], [72, 24], [78, 28], [84, 24], [90, 28], [96, 24]])} fill={ART.canvas.shade} stroke={ART.ink} strokeWidth="0.6" />
      <Slab x={2} y={24} w={3} h={36} k={0.6} c="dark" o={0.5} /><Slab x={91} y={24} w={3} h={36} k={0.6} c="dark" o={0.5} />
      <rect x="6" y="28" width="84" height="18" fill={ART.dark.shade} opacity="0.9" />
      {/* shelves of ordinary tools and spares */}
      <Slab x={10} y={32} w={26} h={2.2} k={0.4} c="wood" o={0.5} /><Slab x={10} y={40} w={26} h={2.2} k={0.4} c="wood" o={0.5} />
      <path d="M12 31 L12 24 M16 31 L16 26" stroke={ART.steel.hi} strokeWidth="1.2" /><Slab x={19} y={27} w={6} h={4} k={0.8} c="steel" o={0.5} />
      <path d="M28 31 L33 26 M30 31 L35 27" stroke={ART.hull.base} strokeWidth="1.2" />
      <Slab x={12} y={35} w={5} h={5} k={0.6} c="steel" o={0.5} /><Slab x={19} y={35} w={6} h={5} k={0.6} c="tan" o={0.5} /><Slab x={27} y={36} w={6} h={4} k={0.6} c="teal" o={0.5} />
      {/* a tarp over crates at the back: the real stock */}
      <Bev t="polygon" points={pts([[46, 46], [48, 30], [62, 28], [76, 32], [84, 46]])} c="canvas" o={0.6} />
      <path d="M52 44 L56 32 M66 44 L68 30 M74 44 L76 34" stroke={ART.canvas.shade} strokeWidth="1" opacity="0.8" />
      <Slab x={2} y={46} w={92} h={11} k={1.4} c="wood" o={1} />
      <rect x="4" y="47" width="88" height="1.8" fill={ART.wood.hi} opacity="0.7" />
      <Slab x={2} y={55} w={92} h={5} k={0.8} c="dark" o={0.5} />
      <Slab x={30} y={49} w={4} h={4} k={0.6} c="dark" o={0.4} /><Slab x={38} y={49} w={5} h={4} k={0.6} c="steel" o={0.4} /><Slab x={64} y={48} w={7} h={5} k={0.6} c="hull" o={0.4} />
      <Slab x={31} y={8} w={34} h={11} k={1.6} c="beige" o={0.7} />
      <text x="48" y="16" textAnchor="middle" fontSize="5.4" fill={ART.dark.base} fontWeight="700" {...PT}>GOODS</text>
      <Lights x={10} y={53} n={3} gap={3} seed={7} />
      <Grime x={4} y={8} w={88} h={50} seed={12} n={6} op={0.14} />
    </svg>
  );
}

function PropShiftBoard() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={40} ry={3} />
      <Slab x={4} y={4} w={88} h={54} k={2.4} c="steel" o={1} />
      <Seams x={4} y={4} w={88} h={54} cols={0} rows={0} />
      <Rivets x={8} y={7} w={80} n={9} /><Rivets x={8} y={55} w={80} n={9} />
      <rect x="8" y="9" width="80" height="45" fill={ART.screen.amber} />
      <g style={{ animation:'lens-flicker 1.7s ease-in-out infinite' }}>
        <rect x="8" y="9" width="80" height="6" fill={S.amber} opacity="0.4" />
        <text x="11" y="13.4" fontSize="3.4" fill={S.amberHi} fontWeight="700" {...PT}>KDY SHIFT BOARD</text>
        <text x="86" y="13.4" fontSize="3" fill={S.amber} textAnchor="end" {...PT}>T-2210</text>
        <rect x="10" y="17" width="76" height="12" fill={ART.screen.red} /><rect x="10" y="17" width="76" height="12" fill="none" stroke={S.orange} strokeWidth="0.6" />
        <text x="12" y="21.4" fontSize="2.8" fill={S.redHi} fontWeight="700" {...PT}>PLASMA WELDING DECK 4 · MANDATORY OVERTIME</text>
        <text x="12" y="25" fontSize="2.6" fill={S.amberHi} {...PT}>THIRD SHIFT EXTENDED TWELVE HOURS</text>
        <text x="12" y="28.2" fontSize="2.4" fill={S.orange} {...PT}>COMPENSATION ADJUSTMENT PENDING REVIEW</text>
        {[0, 1, 2, 3].map((r) => (
          <g key={r}>
            <rect x="10" y={32 + r * 5} width="16" height="3.4" fill={ART.amber.shade} opacity="0.6" />
            {[0, 1, 2, 3, 4].map((c) => <rect key={c} x={28 + c * 12} y={32 + r * 5} width="10" height="3.4" fill={(r + c) % 3 === 0 ? ART.amber.base : ART.amber.shade} opacity="0.8" />)}
          </g>
        ))}
        <text x="86" y="53" fontSize="2.2" fill={S.amber} textAnchor="end" {...PT}>PENDING 11 WEEKS</text>
      </g>
      <rect x="8" y="9" width="80" height="2" fill={S.amberHi} opacity="0.3" style={{ animation:'prop-scan 4s linear infinite' }} />
      <Lights x={10} y={56.4} n={3} gap={3.4} seed={9} />
      <Grime x={6} y={6} w={84} h={50} seed={19} n={5} op={0.15} /><Scuff x={6} y={6} w={84} h={50} seed={3} n={9} />
    </svg>
  );
}

function PropUnionBulletin() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <Slab x={2} y={2} w={60} h={60} k={1.4} c="dark" o={0.9} />
      {[10, 20, 30, 40, 50].map((y) => <line key={y} x1="2" y1={y} x2="62" y2={y} stroke={ART.ink} strokeWidth="0.6" opacity="0.6" />)}
      <Slab x={2} y={2} w={60} h={5} k={0.8} c="steel" o={0.5} />
      <Bev t="polygon" points={pts([[8, 8], [40, 7], [46, 10], [42, 14], [47, 18], [43, 23], [45, 30], [40, 34], [44, 40], [38, 44], [34, 50], [28, 46], [22, 52], [16, 48], [10, 53], [7, 46], [10, 38], [6, 30], [9, 22], [6, 15]])} c="paper" o={0.6} />
      <polygon points={pts([[44, 14], [52, 12], [50, 18]])} fill={ART.paper.shade} /><polygon points={pts([[43, 23], [50, 24], [47, 28]])} fill={ART.paper.shade} /><polygon points={pts([[40, 34], [48, 36], [44, 42]])} fill={ART.paper.shade} />
      <circle cx="26" cy="9" r="1.6" fill={ART.red.base} /><circle cx="9" cy="10" r="1.4" fill={ART.brand.chip} />
      <g transform="rotate(-2 24 28)">
        <text x="11" y="17" fontSize="3.2" fill={ART.note.ink} fontWeight="700" {...PT}>Deck 4 third shift.</text>
        <text x="11" y="22" fontSize="2.9" fill={ART.note.ink} {...PT}>Seven injuries in</text>
        <text x="11" y="26.6" fontSize="2.9" fill={ART.note.ink} {...PT}>four months. The review</text>
        <text x="11" y="31.2" fontSize="2.9" fill={ART.note.ink} {...PT}>board has not met.</text>
        <text x="11" y="36" fontSize="3" fill={ART.note.ink} fontWeight="700" {...PT}>We meet-</text>
        <path d="M24 35 L36 38" stroke={ART.paper.base} strokeWidth="3" />
      </g>
      {/* a different gear: the union mark */}
      <g transform="translate(48 50)">
        <polygon points={pts(Array.from({ length: 16 }, (_, i) => { const a = (i / 16) * Math.PI * 2, r = i % 2 ? 4.6 : 6.4; return [+(Math.cos(a) * r).toFixed(2), +(Math.sin(a) * r).toFixed(2)]; }))} fill={S.orange} />
        <circle cx="0" cy="0" r="3.2" fill={ART.dark.shade} />
        <path d="M-2.4 2.4 L2.4 -2.4 M-2.4 -2.4 L2.4 2.4" stroke={S.amber} strokeWidth="1.1" />
      </g>
      <Grime x={4} y={8} w={56} h={50} seed={22} n={4} op={0.14} />
      <Rivets x={6} y={4.6} w={52} n={8} />
    </svg>
  );
}

function PropSurveillanceNode() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={20} ry={3} />
      <polygon points={pts([[32, 30], [6, 62], [58, 62]])} fill={S.cyan} opacity="0.1" style={{ animation:'ship-engine 3s ease-in-out infinite' }} />
      <Slab x={27} y={40} w={10} h={20} k={1} c="dark" />
      <Cable d="M44 48 Q54 46 54 56 Q54 62 60 62" c={ART.dark.shade} w={2.2} />
      <Bev t="polygon" points={pts([[14, 16], [50, 16], [53, 20], [53, 38], [50, 42], [14, 42], [11, 38], [11, 20]])} c="steel" o={1} />
      <Seams x={11} y={16} w={42} h={26} rows={1} op={0.3} />
      <Bev t="polygon" points={pts(ngon(32, 29, 11, 11, 8))} c="dark" o={0.8} />
      <polygon points={pts(ngon(32, 29, 8, 8, 8))} fill={ART.screen.cyan} /><polygon points={pts(ngon(32, 29, 5, 5, 8))} fill={ART.ink} stroke={S.cyanLo} strokeWidth="0.7" />
      <circle cx="32" cy="29" r="2.4" fill={S.cyan} style={{ animation:'ship-engine 2.6s ease-in-out infinite' }} /><circle cx="30" cy="27" r="1" fill={S.white} />
      <circle cx="46" cy="21" r="2.2" fill={S.cyan} style={{ animation:'ship-engine 1.6s ease-in-out infinite' }} />
      <Glow cx={46} cy={21} r={5.4} c={S.cyan} opacity={0.5} style={{ animation:'ship-engine 1.6s ease-in-out infinite' }} />
      {[[16, 20], [48, 39], [16, 39]].map(([x, y], i) => <polygon key={i} points={pts(ngon(x, y, 1.7, 1.5, 6))} fill={ART.steel.hi} />)}
      <Vent x={36} y={36} w={11} h={4} n={2} />
      <text x="32" y="47" textAnchor="middle" fontSize="2.2" fill={ART.steel.hi} {...PT}>RING-SEC · BARRACKS 2</text>
      <Grime x={12} y={17} w={40} h={24} seed={4} n={4} op={0.18} /><Scuff x={12} y={17} w={40} h={24} seed={6} n={7} />
    </svg>
  );
}

function PropForgeBar() {
  const S = ART.signal;
  const bottles = [[ART.rust.base, 12], [ART.bronze.base, 22], [ART.amber.base, 32], [ART.canvas.base, 42], [ART.red.shade, 54], [ART.deepglass.base, 64], [ART.copper.base, 74]];
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={3.4} />
      <Slab x={4} y={4} w={120} h={30} k={1.6} c="dark" o={0.8} />
      <rect x="6" y="6" width="116" height="26" fill={ART.ink} opacity="0.7" />
      <Glow cx={52} cy={20} rx={46} ry={14} c={S.amber} opacity={0.35} style={{ animation:'ship-engine 4s ease-in-out infinite' }} />
      <Slab x={6} y={16} w={88} h={2.4} k={0.4} c="wood" o={0.5} /><Slab x={6} y={28} w={88} h={2.4} k={0.4} c="wood" o={0.5} />
      {bottles.map(([c, x], i) => (
        <g key={i}>
          <polygon points={pts([[x - 2.6, 16], [x - 2.6, 9 + (i % 2) * 2], [x - 1, 7 + (i % 2) * 2], [x + 1, 7 + (i % 2) * 2], [x + 2.6, 9 + (i % 2) * 2], [x + 2.6, 16]])} fill={c} />
          <rect x={x - 1} y={4 + (i % 2) * 2} width="2" height="3.4" fill={c} /><rect x={x - 2} y={10 + (i % 2) * 2} width="1" height="5" fill={S.white} opacity="0.3" />
        </g>
      ))}
      {[16, 30, 46, 60, 76].map((x) => <rect key={x} x={x} y="22" width="3.6" height="6" fill={S.amberHi} opacity="0.5" />)}
      {/* dead refrigeration unit: offline, work order open */}
      <Slab x={98} y={6} w={22} h={26} k={1.6} c="steel" o={0.8} />
      <rect x="101" y="11" width="16" height="12" fill={ART.screen.off} /><circle cx="109" cy="17" r="1.8" fill={S.red} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} />
      <text x="109" y="21.6" textAnchor="middle" fontSize="1.8" fill={S.redHi} {...PT}>UNIT OFFLINE</text>
      <g transform="rotate(3 110 27)"><rect x="104" y="24" width="12" height="7" fill={ART.paper.base} /><text x="110" y="28.4" textAnchor="middle" fontSize="1.7" fill={ART.note.redInk} fontWeight="700" {...PT}>WORK ORDER OPEN</text><text x="110" y="30.4" textAnchor="middle" fontSize="1.6" fill={ART.note.ink} {...PT}>2 MONTHS</text></g>
      {/* bar counter */}
      <Slab x={2} y={36} w={124} h={8} k={1.6} c="wood" o={1} />
      <rect x="4" y="37" width="120" height="1.6" fill={ART.wood.hi} opacity="0.8" />
      <Slab x={2} y={42} w={124} h={3} k={0.8} c="steel" o={0.6} />
      <Bev t="polygon" points={pts([[2, 44], [126, 44], [122, 58], [6, 58]])} c="wood" o={1} />
      <path d="M8 48 L120 48" stroke={ART.ink} strokeWidth="0.6" opacity="0.5" />
      <text x="40" y="52" fontSize="4.4" textAnchor="middle" fill={ART.wood.shade} fontWeight="700" {...PT}>HERE SINCE YEAR ONE</text>
      <text x="40" y="52" fontSize="4.4" textAnchor="middle" fill={ART.wood.hi} opacity="0.5" fontWeight="700" transform="translate(-0.4 -0.5)" {...PT}>HERE SINCE YEAR ONE</text>
      <text x="94" y="55.4" fontSize="3.4" textAnchor="middle" fill={ART.beige.hi} transform="rotate(-2 94 55)" {...PT}>AND COUNTING</text>
      {[[20, 36], [34, 36]].map(([x, y], i) => (
        <g key={i}>
          <polygon points={pts([[x - 2, y], [x - 1.4, y - 5], [x + 1.4, y - 5], [x + 2, y]])} fill={ART.glass.base} opacity="0.55" />
          <polygon points={pts([[x - 1.6, y - 1], [x - 1.2, y - 3], [x + 1.2, y - 3], [x + 1.6, y - 1]])} fill={ART.amber.base} />
          <path d={`M${x} ${y - 6} Q${x - 1} ${y - 8} ${x} ${y - 10}`} fill="none" stroke={S.white} strokeWidth="0.6" opacity="0.35" style={{ animation:'steam-rise 3s ease-in-out infinite' }} />
        </g>
      ))}
      <Grime x={4} y={38} w={120} h={20} seed={6} n={6} op={0.18} />
      <Rivets x={6} y={43.6} w={116} n={14} c="steel" />
    </svg>
  );
}

function PropCzerkaTerminal() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={22} ry={3.4} />
      <Bev t="polygon" points={pts([[9, 4], [55, 4], [59, 8], [59, 54], [55, 58], [9, 58], [5, 54], [5, 8]])} c="red" o={1} />
      <Slab x={7} y={5} w={50} h={4} k={0.8} c="brass" o={0.5} /><Slab x={7} y={53} w={50} h={4} k={0.8} c="brass" o={0.5} />
      <Slab x={5} y={9} w={3} h={44} k={0.6} c="brass" o={0.4} /><Slab x={56} y={9} w={3} h={44} k={0.6} c="brass" o={0.4} />
      {/* Czerka crest: gold disc with a cut C */}
      <Bev t="polygon" points={pts(ngon(20, 17, 8, 8, 12))} c="dark" o={0.4} />
      <path d="M25 12.4 L20 11 L15.6 13 L14 17 L15.6 21 L20 23 L25 21.6 L25 19.6 L20.4 20.6 L17.4 19 L16.4 17 L17.4 15 L20.4 13.4 L25 14.4 Z" fill={ART.brass.hi} />
      <text x="41" y="16" fontSize="5" fill={S.gold} fontWeight="700" textAnchor="middle" {...PT}>CZERKA</text>
      <text x="41" y="20.4" fontSize="2.4" fill={S.gold} textAnchor="middle" opacity="0.9" {...PT}>CORPORATION</text>
      <rect x="10" y="26" width="44" height="18" fill={ART.screen.red} /><rect x="10" y="26" width="44" height="18" fill="none" stroke={ART.brass.base} strokeWidth="0.7" />
      <text x="12" y="30" fontSize="2.8" fill={S.gold} fontWeight="700" {...PT}>INDUSTRIAL COMPONENTS</text>
      <text x="12" y="33.6" fontSize="2.6" fill={ART.brass.hi} {...PT}>SURVEY EQUIPMENT</text>
      <text x="12" y="37.2" fontSize="2.6" fill={ART.brass.hi} {...PT}>LABOR CONTRACTING</text>
      <rect x="12" y="39.4" width="26" height="1.4" fill={S.gold} opacity="0.5" style={{ animation:'ship-engine 2.4s ease-in-out infinite' }} />
      <Toggles x={10} y={47} cols={9} rows={1} gap={3.4} seed={11} />
      <rect x="9" y="50" width="46" height="7" fill={ART.paper.base} />
      <text x="11" y="53" fontSize="1.9" fill={ART.note.ink} {...PT}>CZERKA RETAINS RIGHT OF FIRST PURCHASE</text>
      <text x="11" y="55.4" fontSize="1.9" fill={ART.note.ink} {...PT}>FOR ALL MATERIALS MADE W/ CZERKA PARTS</text>
      <ellipse cx="30" cy="54" rx="26" ry="4.6" fill="none" stroke={ART.note.redInk} strokeWidth="0.9" transform="rotate(-1.4 30 54)" />
      <Grime x={6} y={6} w={52} h={50} seed={31} n={4} op={0.14} />
    </svg>
  );
}

function PropFuelRack() {
  const S = ART.signal;
  const dmg = (r, c) => r === 2 && (c === 2 || c === 5 || c === 8);
  const tones = ['teal', 'canvas', 'tan', 'steel'];
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3.4} />
      <Slab x={4} y={6} w={88} h={50} k={1.6} c="dark" o={0.9} />
      <Slab x={4} y={5} w={88} h={5} k={1} c="steel" o={0.6} />
      <Hazard x={5} y={54} w={86} h={3.4} />
      {[0, 1, 2].map((r) => (
        <g key={r}>
          <Slab x={4} y={22 + r * 14} w={88} h={2.4} k={0.4} c="steel" o={0.5} />
          {Array.from({ length: 12 }, (_, c) => {
            const x = 7 + c * 7.2, y = 11 + r * 14;
            const bad = dmg(r, c % 9);
            return (
              <g key={c}>
                <Slab x={x} y={y} w={5.6} h={11} k={1.2} c={tones[c % 4]} o={0.6} />
                <rect x={x} y={y + 2} width="5.6" height="1.6" fill={ART.amber.base} opacity="0.9" />
                <rect x={x + 1} y={y - 1} width="3.6" height="1.6" fill={ART.steel.base} />
                {bad && c < 9 && <polyline points={pts([[x, y + 1], [x + 1.6, y + 3.4]])} stroke={ART.ink} strokeWidth="0.8" fill="none" />}
              </g>
            );
          })}
        </g>
      ))}
      <polygon points={pts([[50, 44], [54, 42], [55, 47], [51, 48]])} fill={S.orange} />
      {[8, 88].map((x, i) => (
        <g key={x}>
          <Slab x={x - 1.6} y={26} w={3.2} h={28} k={0.5} c="steel" o={0.4} />
          <circle cx={x} cy="24" r="2.8" fill={S.amber} style={{ animation:`ship-blink ${2.4 + i * 0.7}s ease-in-out ${i * 0.9}s infinite` }} />
          <Glow cx={x} cy={24} r={7} c={S.amber} opacity={0.5} style={{ animation:`ship-blink ${2.4 + i * 0.7}s ease-in-out ${i * 0.9}s infinite` }} />
        </g>
      ))}
      <text x="48" y="8.4" textAnchor="middle" fontSize="2.6" fill={ART.ink} fontWeight="700" {...PT}>DEUTERIUM · 36 CANISTERS · INSPECTION OVERDUE 8 DAYS</text>
      <Grime x={5} y={10} w={86} h={44} seed={15} n={6} op={0.16} />
      <Seams x={4} y={6} w={88} h={50} cols={5} op={0.2} />
    </svg>
  );
}

function PropDriveCradle() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={88} rx={40} ry={5} />
      <Bev t="polygon" points={pts([[10, 88], [10, 74], [86, 74], [86, 88]])} c="dark" o={1} />
      <Hazard x={12} y={83} w={72} h={4} />
      <Greeble x={14} y={76} w={68} h={6} seed={72} n={9} c="dark" />
      {/* support pylons */}
      <polygon points={pts([[16, 74], [24, 44], [30, 44], [24, 74]])} fill={ART.steel.shade} /><Bev t="polygon" points={pts([[15, 74], [22, 44], [28, 44], [22, 74]])} c="steel" o={0.6} />
      <polygon points={pts([[80, 74], [72, 44], [66, 44], [72, 74]])} fill={ART.steel.shade} /><Bev t="polygon" points={pts([[80, 74], [74, 44], [68, 44], [74, 74]])} c="steel" o={0.6} />
      {/* the calibration ring */}
      <Bev t="polygon" points={pts(ngon(48, 48, 38, 31, 16, Math.PI / 16))} c="steel" o={1.2} />
      <polygon points={pts(ngon(48, 48, 31, 25, 16, Math.PI / 16))} fill={ART.screen.off} />
      {ngon(48, 48, 34.5, 28, 16, Math.PI / 16).map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1.2" fill={i % 4 === 0 ? S.amber : S.cyan} style={{ animation:`ship-blink ${2 + (i % 4) * 0.4}s ease-in-out ${i * 0.15}s infinite` }} />)}
      {/* KY-88 ion engine seated inside */}
      <Bev t="polygon" points={pts([[26, 38], [30, 30], [66, 30], [70, 38], [70, 60], [64, 68], [32, 68], [26, 60]])} c="copper" o={1} />
      {[36, 44, 52, 60].map((y) => <line key={y} x1="27" y1={y} x2="69" y2={y} stroke={ART.ink} strokeWidth="0.7" opacity="0.5" />)}
      <Rivets x={32} y={33} w={32} n={7} c="copper" />
      <Vent x={30} y={52} w={12} h={6} n={3} />
      <text x="54" y="46.6" textAnchor="middle" fontSize="5" fill={ART.copper.shade} fontWeight="700" {...PT}>KY-88</text>
      <Bev t="polygon" points={pts(ngon(48, 68, 14, 5, 8))} c="dark" o={0.8} />
      <polygon points={pts(ngon(48, 69, 9, 3, 8))} fill={ART.ink} />
      <Glow cx={48} cy={69} rx={9} ry={3} c={S.cyan} opacity={1} style={{ animation:'ship-engine 1.8s ease-in-out infinite' }} />
      {/* resonance drift readout */}
      <Slab x={2} y={30} w={22} h={22} k={1.4} c="dark" />
      <rect x="4" y="32" width="18" height="18" fill={ART.screen.cyan} />
      <text x="13" y="36" textAnchor="middle" fontSize="2.3" fill={S.cyan} {...PT}>RESONANCE DRIFT</text>
      <polyline points={pts([[5, 44], [8, 39], [11, 44], [14, 39], [17, 44], [20, 40]])} fill="none" stroke={S.cyan} strokeWidth="0.8" />
      <polyline points={pts([[5, 46], [9, 40], [13, 46], [18, 41]])} fill="none" stroke={S.amber} strokeWidth="0.8" style={{ animation:'ship-engine 2.4s ease-in-out infinite' }} />
      <text x="13" y="50" textAnchor="middle" fontSize="3.4" fill={S.amber} fontWeight="700" {...PT}>+0.3%</text>
      {/* service tag in two hands */}
      <g transform="rotate(4 80 28)">
        <rect x="68" y="14" width="26" height="26" fill={ART.paper.base} /><circle cx="81" cy="15" r="1.2" fill={ART.red.base} />
        <text x="70" y="21" fontSize="2.2" fill={ART.note.blueInk} fontWeight="700" {...PT}>DRIFT ACCEPTABLE</text>
        <text x="70" y="24" fontSize="2.2" fill={ART.note.blueInk} {...PT}>-J.OSSEK</text>
        <path d="M69 26 L93 26" stroke={ART.paper.shade} strokeWidth="0.4" />
        <text x="70" y="30" fontSize="2.1" fill={ART.note.redInk} fontWeight="700" {...PT}>DRIFT IS NOT</text>
        <text x="70" y="33" fontSize="2.1" fill={ART.note.redInk} fontWeight="700" {...PT}>ACCEPTABLE.</text>
        <text x="70" y="36.4" fontSize="1.9" fill={ART.note.redInk} {...PT}>RECALIBRATE BEFORE</text>
        <text x="70" y="38.6" fontSize="1.9" fill={ART.note.redInk} {...PT}>INSTALL</text>
      </g>
      <Grime x={12} y={30} w={72} h={44} seed={44} n={5} op={0.16} />
    </svg>
  );
}

function PropCrawlerPort() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={3.4} />
      <Slab x={2} y={12} w={60} h={44} k={1.6} c="dark" o={0.9} />
      <Rivets x={6} y={15} w={52} n={8} c="dark" />
      <Slab x={6} y={26} w={52} h={26} k={2} c="dark" o={0.6} />
      <Hazard x={6} y={26} w={52} h={5} /><Hazard x={6} y={47} w={52} h={5} /><Hazard x={6} y={26} w={5} h={26} /><Hazard x={53} y={26} w={5} h={26} />
      <rect x="11" y="31" width="42" height="16" fill={ART.ink} />
      <rect x="12" y="44" width="40" height="2" fill={S.orange} opacity="0.15" />
      {/* M-33 conduit crawler: segmented copper, emerging */}
      {[0, 1, 2, 3].map((i) => (
        <g key={i}>
          <Slab x={18 + i * 8.6} y={43} w={9} h={7} k={1.6} c="copper" o={0.5} />
          <line x1={20.4 + i * 8.6} y1="43" x2={20.4 + i * 8.6} y2="50" stroke={ART.ink} strokeWidth="0.5" opacity="0.7" /><line x1={24.6 + i * 8.6} y1="43" x2={24.6 + i * 8.6} y2="50" stroke={ART.ink} strokeWidth="0.5" opacity="0.7" />
        </g>
      ))}
      <Bev t="polygon" points={pts([[8, 46], [12, 41], [19, 41], [19, 50], [12, 51]])} c="copper" o={0.6} />
      <circle cx="12.6" cy="44" r="1.4" fill={ART.ink} /><circle cx="12.6" cy="44" r="0.7" fill={S.greenHi} style={{ animation:'ship-engine 1.6s ease-in-out infinite' }} />
      <path d="M14 50 L8 53 M14 41 L9 38" stroke={ART.copper.shade} strokeWidth="0.9" />
      <Cable d="M52 42 Q58 40 60 34 Q60 28 56 24" c={ART.dark.shade} w={2} />
      <circle cx="8" cy="28" r="1" fill={S.warn} /><circle cx="56" cy="28" r="1" fill={S.warn} />
      <Slab x={14} y={14} w={36} h={9} k={0.8} c="paper" o={0.4} />
      <text x="32" y="18.2" textAnchor="middle" fontSize="2.3" fill={ART.note.ink} fontWeight="700" {...PT}>M-33 CRAWLER · LOG</text>
      <text x="32" y="21" textAnchor="middle" fontSize="2" fill={ART.note.redInk} {...PT}>7 MINOR · 1 MAJOR CONDUIT FAULT</text>
      <Grime x={4} y={14} w={56} h={40} seed={9} n={5} op={0.18} />
    </svg>
  );
}

function PropRivetHopper() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={90} rx={42} ry={4.4} />
      <Slab x={22} y={0} w={12} h={18} k={1} c="steel" o={0.8} /><Slab x={20} y={14} w={16} h={5} k={1} c="dark" o={0.7} />
      <Bev t="polygon" points={pts([[8, 22], [48, 22], [40, 56], [16, 56]])} c="steel" o={1.1} />
      <Seams x={10} y={22} w={36} h={34} rows={2} cols={1} op={0.3} />
      <polygon points={pts([[12, 38], [44, 38], [40, 56], [16, 56]])} fill={ART.hull.hi} opacity="0.45" />
      {Array.from({ length: 14 }, (_, i) => <circle key={i} cx={16 + (i % 7) * 4} cy={44 + Math.floor(i / 7) * 5} r="1.2" fill={ART.steel.base} />)}
      <line x1="14" y1="33" x2="42" y2="33" stroke={S.warn} strokeWidth="0.8" strokeDasharray="2 2" />
      <text x="28" y="31" textAnchor="middle" fontSize="2.4" fill={S.warn} {...PT}>HALF</text>
      <Slab x={22} y={56} w={12} h={8} k={1} c="dark" /><Bev t="polygon" points={pts([[22, 64], [34, 64], [32, 70], [24, 70]])} c="steel" o={0.5} />
      <Slab x={12} y={72} w={32} h={14} k={1.6} c="dark" o={0.9} />
      {Array.from({ length: 8 }, (_, i) => <circle key={i} cx={17 + i * 3.6} cy={80 + (i % 2) * 2} r="1.1" fill={ART.steel.base} />)}
      <circle cx="28" cy="70.6" r="0.9" fill={ART.steel.hi} style={{ animation:'ship-blink 1.4s ease-in-out infinite' }} />
      {/* KDY Mark IV Heavy Loader Droid, standby */}
      <Bev t="polygon" points={pts([[56, 48], [88, 44], [92, 50], [92, 76], [86, 82], [58, 82], [54, 76]])} c="teal" o={1} />
      <path d="M58 56 L88 59 M56 66 L90 70 M60 76 L86 78" stroke={ART.teal.shade} strokeWidth="0.7" opacity="0.8" />
      <path d="M64 50 L68 58 M80 70 L84 76" stroke={ART.teal.hi} strokeWidth="0.5" opacity="0.6" />
      <Bev t="polygon" points={pts([[58, 32], [88, 32], [90, 38], [88, 46], [58, 46], [56, 38]])} c="teal" o={0.9} />
      <rect x="60" y="36" width="26" height="4.6" fill={ART.ink} /><rect x="62" y="37.2" width="9" height="2" fill={S.amber} opacity="0.5" />
      <path d="M54 56 L50 66 L56 76 M92 56 L96 66 L92 76" fill="none" stroke={ART.teal.shade} strokeWidth="3.6" strokeLinecap="square" />
      {[[58, 86], [88, 86], [54, 80], [92, 80]].map(([x, y], i) => <Bev key={i} t="polygon" points={pts(ngon(x, y, 4.4, 2.4, 6))} c="dark" o={0.4} />)}
      <g transform="rotate(-3 15 64)"><rect x="2" y="60" width="26" height="9" fill={ART.paper.base} />
        <text x="4" y="64" fontSize="1.9" fill={ART.note.ink} fontWeight="700" {...PT}>RIVET SPEC KDY-884</text>
        <text x="4" y="66.4" fontSize="1.8" fill={ART.note.ink} {...PT}>40000 · HULL 7F · OSSEK</text></g>
      <Grime x={54} y={32} w={40} h={52} seed={8} n={5} op={0.2} />
    </svg>
  );
}

function PropRepublicTerminal() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={24} ry={3.4} />
      {/* late Mandalorian Wars hardware: chunky, repaired with spares */}
      <Slab x={8} y={42} w={48} h={16} k={1.6} c="beige" o={0.9} />
      {[0, 1, 2].map((r) => Array.from({ length: 10 }, (_, c) => <rect key={`${r}${c}`} x={11 + c * 4.4} y={46 + r * 3.6} width="3.4" height="2.6" fill={ART.dark.base} />))}
      <Bev t="polygon" points={pts([[8, 4], [56, 4], [60, 9], [60, 40], [56, 44], [8, 44], [4, 40], [4, 9]])} c="beige" o={1} />
      <rect x="6" y="38" width="52" height="3" fill={ART.red.base} />
      <Bev t="polygon" points={pts([[12, 9], [52, 9], [55, 13], [55, 33], [52, 36], [12, 36], [9, 33], [9, 13]])} c="dark" o={0.6} />
      <rect x="13" y="12" width="38" height="21" fill={ART.screen.green} />
      <polygon points={pts([[16, 26], [24, 18], [34, 16], [46, 20], [48, 26], [40, 30], [26, 30]])} fill="none" stroke={S.greenHi} strokeWidth="0.9" />
      <path d="M24 18 L26 30 M34 16 L34 30 M40 17 L40 30 M46 20 L40 30" stroke={S.green} strokeWidth="0.5" opacity="0.7" />
      <text x="14" y="15.6" fontSize="2.6" fill={S.greenHi} fontWeight="700" {...PT}>VALOR CLASS · TOLERANCES</text>
      <text x="14" y="32" fontSize="2.2" fill={S.green} {...PT}>HULL STRUCT · REV 3902</text>
      <rect x="13" y="12" width="38" height="1.8" fill={S.greenHi} opacity="0.3" style={{ animation:'prop-scan 3.4s linear infinite' }} />
      {[[6, 6], [58, 6], [6, 42], [58, 42]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1" fill={ART.beige.shade} />)}
      <Slab x={44} y={39} w={11} h={3} k={0.6} c="tan" o={0.5} />
      <g transform="rotate(-6 14 50)"><rect x="3" y="46" width="14" height="12" fill={ART.note.yellow} />
        <text x="4.6" y="50" fontSize="1.9" fill={ART.note.ink} fontWeight="700" {...PT}>IF DOWN AGAIN</text>
        <text x="4.6" y="52.6" fontSize="1.8" fill={ART.note.ink} {...PT}>CONTACT VAEL</text>
        <text x="4.6" y="55" fontSize="1.8" fill={ART.note.ink} {...PT}>SECTION FOUR.</text>
        <text x="4.6" y="57.2" fontSize="1.8" fill={ART.note.redInk} fontWeight="700" {...PT}>NOT ENGINEERING</text></g>
      <g transform="rotate(5 56 52)"><rect x="49" y="47" width="11" height="9" fill={ART.note.pink} />
        <text x="50.4" y="51.6" fontSize="2.1" fill={ART.note.ink} fontWeight="700" {...PT}>WHO IS VAEL</text>
        <text x="50.4" y="55" fontSize="2.1" fill={ART.note.blueInk} fontWeight="700" {...PT}>EXACTLY</text></g>
      <Grime x={5} y={6} w={54} h={36} seed={27} n={4} op={0.14} />
      <Rivets x={8} y={6.4} w={48} n={7} c="beige" />
    </svg>
  );
}

function PropShipyardViewport() {
  const S = ART.signal;
  const wedge = (x, y, s, key) => (
    <g key={key}>
      <polygon points={pts([[x, y + 4 * s], [x + 24 * s, y], [x + 24 * s, y + 8 * s]])} fill={ART.hull.shade} />
      <polygon points={pts([[x, y + 4 * s], [x + 24 * s, y], [x + 24 * s, y + 4 * s]])} fill={ART.hull.base} />
      <polygon points={pts([[x + 8 * s, y + 4 * s], [x + 24 * s, y + 1.2 * s], [x + 24 * s, y + 3 * s]])} fill={ART.hull.hi} opacity="0.7" />
      <rect x={x + 18 * s} y={y + 2.2 * s} width={3 * s} height={2.2 * s} fill={ART.hull.hi} />
      <rect x={x + 24 * s} y={y + 0.4 * s} width={1.6 * s} height={7.2 * s} fill={S.cyan} opacity="0.9" style={{ animation:'ship-engine 2.6s ease-in-out infinite' }} />
    </g>
  );
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <defs><clipPath id="syClip"><rect x="8" y="8" width="80" height="46" /></clipPath></defs>
      <PropShadow cx={48} cy={61} rx={44} ry={2.6} />
      <Slab x={2} y={2} w={92} h={60} k={3} c="brass" o={1} />
      <Slab x={5} y={5} w={86} h={54} k={2} c="steel" o={0.8} />
      <rect x="8" y="8" width="80" height="46" fill={ART.screen.space} />
      <g clipPath="url(#syClip)">
        {[[12, 12], [26, 18], [44, 11], [62, 15], [78, 10], [84, 24], [18, 30]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="0.6" fill={S.white} opacity="0.8" style={{ animation:`twinkle ${2 + i * 0.3}s ease-in-out infinite` }} />)}
        <polygon points={pts([[0, 50], [96, 50], [96, 60], [0, 60]])} fill={ART.dark.base} />
        <polyline points={pts([[0, 51], [30, 47], [60, 47], [96, 51]])} fill="none" stroke={ART.steel.hi} strokeWidth="0.8" opacity="0.7" />
        {[0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => wedge(10 + (i % 3) * 25 - Math.floor(i / 3) * 4, 12 + Math.floor(i / 3) * 12 + (i % 3) * 1.4, 1 - Math.floor(i / 3) * 0.14, i))}
        <polygon points={pts([[12, 12], [36, 8], [20, 40]])} fill={S.white} opacity="0.07" />
      </g>
      <rect x="8" y="8" width="80" height="46" fill="none" stroke={ART.dark.shade} strokeWidth="1.2" />
      {[[4, 4], [92, 4], [4, 60], [92, 60]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1.2" fill={ART.brass.hi} />)}
      <Slab x={26} y={55} w={44} h={5} k={0.8} c="dark" />
      <text x="48" y="58.6" textAnchor="middle" fontSize="2.6" fill={ART.brass.hi} {...PT}>KUAT ORBITAL SHIPYARD · FLEET FORMATION</text>
      <Rivets x={9} y={4} w={78} n={10} c="brass" /><Grime x={4} y={4} w={88} h={56} seed={5} n={4} op={0.14} /><Scuff x={4} y={4} w={88} h={56} seed={7} n={8} c="brass" />
    </svg>
  );
}

function PropExecutiveSafe() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={24} ry={3} />
      {/* the vault door behind the painting */}
      <Slab x={14} y={10} w={40} h={44} k={2} c="steel" o={1} />
      <Slab x={18} y={14} w={32} h={36} k={1.4} c="dark" o={0.8} />
      <Bev t="polygon" points={pts(ngon(34, 26, 7.5, 7.5, 8))} c="steel" o={0.5} /><polygon points={pts(ngon(34, 26, 4.4, 4.4, 8))} fill={ART.screen.off} />
      <path d="M34 20 L34 32 M28 26 L40 26" stroke={ART.steel.hi} strokeWidth="0.7" />
      <Slab x={22} y={36} w={11} h={10} k={1} c="dark" o={0.4} /><rect x="23.4" y="37.4" width="8.2" height="7.2" fill={ART.screen.cyan} />
      <path d="M25 43 L25 40 M27 43 L27 39 M29 43 L29 40 M31 43 L31 41" stroke={S.cyan} strokeWidth="0.8" strokeLinecap="round" />
      <Slab x={38} y={36} w={9} h={9} k={1} c="dark" o={0.4} /><circle cx="42.5" cy="40.5" r="2.6" fill={ART.screen.cyan} /><circle cx="42.5" cy="40.5" r="1.2" fill={S.cyan} style={{ animation:'ship-engine 1.8s ease-in-out infinite' }} />
      <Lights x={26} y={20} n={2} gap={3} colors={['red']} seed={2} />
      <Rivets x={19} y={52} w={30} n={5} />
      {/* gilt framed painting swung open on its hinge: the ring in its first year, hull scoring from the denied raid */}
      <g transform="skewY(-5) translate(0 5)">
        <Slab x={2} y={4} w={22} h={44} k={1.4} c="brass" o={1} />
        <rect x="5" y="7" width="16" height="38" fill={ART.screen.space} />
        <polyline points={pts([[3, 32], [13, 14], [23, 32]])} fill="none" stroke={ART.hull.hi} strokeWidth="2.4" />
        <polyline points={pts([[3, 32], [13, 14], [23, 32]])} fill="none" stroke={ART.hull.shade} strokeWidth="0.7" />
        <path d="M8 26 L10 28 M14 22 L15 25 M18 26 L20 27" stroke={S.orange} strokeWidth="0.9" />
        <circle cx="9" cy="40" r="0.5" fill={S.white} /><circle cx="17" cy="11" r="0.5" fill={S.white} /><circle cx="12" cy="9" r="0.4" fill={S.white} />
        <polygon points={pts([[4, 7], [14, 7], [4, 22]])} fill={S.white} opacity="0.16" />
      </g>
      <Grime x={14} y={12} w={40} h={40} seed={9} n={4} op={0.16} />
    </svg>
  );
}

function PropModelCase() {
  const S = ART.signal;
  const ships = [
    [[0, 0], [14, -2.5], [17, 0], [14, 2.5], [10, -5], [12, -5], [12, 5], [10, 5]],
    [[0, 0], [10, -4], [16, -2], [16, 2], [10, 4]],
    [[0, -2], [12, -5], [16, 0], [12, 5], [0, 2]],
    [[0, 0], [8, -6], [16, -3], [14, 0], [16, 3], [8, 6]],
    [[0, 0], [16, -2], [16, 2]],
    [[0, 0], [6, -4], [16, -4], [12, 0], [16, 4], [6, 4]],
    [[0, 0], [14, -5], [16, 0], [14, 5]],
    [[0, 0], [16, -6], [12, 0], [16, 6]],
    [[0, 0], [5, -8], [9, -3], [16, -6], [13, 0], [16, 6], [9, 3], [5, 8]],
  ];
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={60} rx={58} ry={3.4} />
      <Slab x={4} y={38} w={120} h={20} k={1.6} c="dark" o={1} />
      <polyline points={pts([[6, 40], [122, 40], [122, 56], [6, 56]])} fill="none" stroke={ART.brass.base} strokeWidth="0.6" opacity="0.7" />
      <rect x="4" y="8" width="120" height="30" fill={ART.screen.space} /><rect x="4" y="30" width="120" height="8" fill={ART.dark.base} />
      {ships.map((p, i) => (
        <g key={i} transform={`translate(${9 + i * 12.8} ${i === 8 ? 20 : 22}) scale(0.78)`}>
          <polygon points={pts(p)} fill={i === 8 ? ART.signal.anomaly : ART.brass.base} />
          <polygon points={pts(p.map(([x, y]) => [x * 0.9, y * 0.55 - 1]))} fill={i === 8 ? ART.signal.white : ART.brass.hi} opacity="0.6" />
        </g>
      ))}
      <Glow cx={116} cy={20} rx={6} ry={9} c={S.anomaly} opacity={0.3} style={{ animation:'ship-engine 3s ease-in-out infinite' }} />
      {ships.map((s, i) => (
        <g key={'p' + i}>
          <Slab x={5 + i * 13} y={46} w={12} h={8} k={0.8} c="dark" o={0.4} />
          <text x={11 + i * 13} y="50" fontSize="2.2" fill={ART.brass.hi} textAnchor="middle" fontWeight="700" {...PT}>{i === 8 ? '???' : (i === 0 ? 'YEAR 1' : `MS-${String(i).padStart(2, '0')}`)}</text>
          <text x={11 + i * 13} y="52.8" fontSize="1.7" fill={ART.brass.base} textAnchor="middle" {...PT}>{i === 0 ? 'CORVETTE' : i === 8 ? 'UNLABELED' : 'MILESTONE'}</text>
        </g>
      ))}
      <rect x="4" y="8" width="120" height="30" fill={ART.glass.base} opacity="0.08" />
      <polygon points={pts([[6, 9], [40, 9], [6, 34]])} fill={S.white} opacity="0.12" />
      <polygon points={pts([[70, 9], [90, 9], [64, 38], [52, 38]])} fill={S.white} opacity="0.06" />
      <Slab x={4} y={6} w={120} h={3} k={0.8} c="steel" />
      <Slab x={58} y={36} w={10} h={5} k={1} c="brass" o={0.5} /><circle cx="63" cy="38.6" r="1" fill={ART.dark.shade} /><circle cx="68" cy="37" r="1" fill={S.white} style={{ animation:'ship-blink 2s ease-in-out infinite' }} />
      <Rivets x={6} y={7.4} w={116} n={14} /><Grime x={4} y={38} w={120} h={20} seed={5} n={5} op={0.16} /><Scuff x={4} y={38} w={120} h={20} seed={7} n={8} c="dark" />
    </svg>
  );
}

function PropBronzeMural() {
  const crest = (a, i) => <circle key={i} cx={78 + Math.cos(a) * 9} cy={52 + Math.sin(a) * 6} r="1.4" fill={ART.bronze.hi} />;
  const fig = (x, y, k) => (
    <g key={k}>
      <Bev t="polygon" points={pts(ngon(x, y, 3.4, 3.4, 8))} c="bronze" o={0.4} />
      <Bev t="polygon" points={pts([[x - 4, y + 22], [x - 3, y + 5], [x, y + 3.4], [x + 3, y + 5], [x + 4, y + 22]])} c="bronze" o={0.5} />
      <path d={`M${x + 4} ${y + 8} L${x + 9} ${y + 3}`} stroke={ART.bronze.base} strokeWidth="1.8" />
      <path d={`M${x - 3} ${y + 7} L${x - 2} ${y + 20}`} stroke={ART.bronze.hi} strokeWidth="0.7" opacity="0.6" />
    </g>
  );
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={3} />
      <Slab x={2} y={2} w={92} h={58} k={2.4} c="bronze" o={1} />
      <rect x="5" y="5" width="86" height="52" fill={ART.bronze.shade} />
      <polyline points={pts([[6, 6], [90, 6]])} stroke={ART.bronze.hi} strokeWidth="0.8" opacity="0.6" fill="none" />
      <polygon points={pts([[5, 40], [20, 35], [48, 31], [76, 35], [91, 40], [91, 57], [5, 57]])} fill={ART.dark.shade} opacity="0.7" />
      <polyline points={pts([[5, 40], [20, 35], [48, 31], [76, 35], [91, 40]])} fill="none" stroke={ART.bronze.hi} strokeWidth="1.2" opacity="0.8" />
      {/* hull frame skeleton above Kuat's atmosphere */}
      <polyline points={pts([[22, 36], [30, 24], [44, 16], [62, 15], [74, 22], [76, 34]])} fill="none" stroke={ART.bronze.hi} strokeWidth="1.6" />
      {[28, 34, 40, 46, 52, 58, 64, 70].map((x, i) => <line key={x} x1={x} y1={32 - Math.abs(i - 3.4) * 1.2 - 6} x2={x - 1} y2="36" stroke={ART.bronze.base} strokeWidth="1.1" />)}
      <line x1="22" y1="28" x2="72" y2="22" stroke={ART.bronze.base} strokeWidth="0.8" />
      {[[16, 18], [30, 15], [44, 17]].map(([x, y], i) => fig(x, y, i))}
      <text x="68" y="12" textAnchor="middle" fontSize="2.4" fill={ART.bronze.hi} {...PT}>THE FOUNDING OF KUAT DRIVE YARDS</text>
      <rect x="6" y="46" width="62" height="9" fill={ART.bronze.shade} />
      <text x="37" y="50.6" textAnchor="middle" fontSize="2.4" fill={ART.bronze.hi} {...PT}>VAS-KUAT · ORLEAN · BAYEN · TROIKAN</text>
      <text x="37" y="53.8" textAnchor="middle" fontSize="2.2" fill={ART.bronze.base} {...PT}>THE TEN FAMILIES OF KUAT</text>
      <Bev t="polygon" points={pts(ngon(78, 52, 10.5, 10.5, 16))} c="bronze" o={0.6} />
      <polygon points={pts(ngon(78, 52, 8.5, 8.5, 16))} fill={ART.bronze.shade} />
      {Array.from({ length: 10 }, (_, i) => crest((i / 10) * Math.PI * 2, i))}
      <Bev t="polygon" points={pts(ngon(78, 52, 3.6, 3.6, 8))} c="brass" o={0.3} />
      <polygon points={pts([[75.6, 53.4], [78, 49.4], [80.4, 53.4], [78, 52.2]])} fill={ART.bronze.shade} />
      <polygon points={pts([[8, 8], [40, 8], [8, 34]])} fill={ART.signal.white} opacity="0.1" />
      <Rivets x={8} y={4} w={80} n={9} c="bronze" /><Rivets x={8} y={58.4} w={80} n={9} c="bronze" /><Grime x={5} y={5} w={86} h={52} seed={5} n={5} op={0.2} /><Scuff x={5} y={5} w={86} h={52} seed={8} n={10} c="bronze" />
    </svg>
  );
}

function PropPanoramicViewport() {
  const S = ART.signal;
  const arch = (inset) => {
    const a = [[6 + inset, 60]], n = 18;
    for (let i = 0; i <= n; i++) { const t = Math.PI - (i / n) * Math.PI; a.push([+(64 + Math.cos(t) * (58 - inset)).toFixed(2), +(24 - Math.sin(t) * (20 - inset * 0.4)).toFixed(2)]); }
    a.push([122 - inset, 60]);
    return a;
  };
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <defs><clipPath id="pvClip"><polygon points={pts(arch(4))} /></clipPath></defs>
      <Bev t="polygon" points={pts(arch(0))} c="brass" o={1.2} />
      <Bev t="polygon" points={pts(arch(2.4))} c="steel" o={0.8} />
      <polygon points={pts(arch(4))} fill={ART.screen.space} />
      <g clipPath="url(#pvClip)">
        {Array.from({ length: 26 }, (_, i) => <circle key={i} cx={14 + (i * 37) % 100} cy={8 + (i * 17) % 28} r="0.5" fill={S.white} opacity="0.8" style={{ animation:`twinkle ${2 + (i % 5) * 0.5}s ease-in-out ${i * 0.1}s infinite` }} />)}
        <polygon points={pts([[0, 60], [0, 54], [32, 42], [64, 38], [96, 42], [128, 54], [128, 60]])} fill={ART.dark.base} />
        <polyline points={pts([[0, 54], [32, 42], [64, 38], [96, 42], [128, 54]])} fill="none" stroke={ART.steel.hi} strokeWidth="1.2" opacity="0.8" />
        <polyline points={pts([[14, 46], [34, 28], [64, 18], [94, 28], [114, 46]])} fill="none" stroke={ART.steel.base} strokeWidth="1.6" opacity="0.9" />
        <polyline points={pts([[22, 48], [38, 32], [64, 24], [90, 32], [106, 48]])} fill="none" stroke={ART.steel.shade} strokeWidth="0.8" />
        {[24, 36, 48, 64, 80, 92, 104].map((x) => <line key={x} x1={x} y1={50 - Math.sin(((x - 14) / 100) * Math.PI) * 28} x2={x} y2={52 - Math.sin(((x - 14) / 100) * Math.PI) * 20} stroke={ART.steel.hi} strokeWidth="0.7" opacity="0.7" />)}
        {Array.from({ length: 16 }, (_, i) => <circle key={i} cx={12 + i * 7} cy={46 + Math.sin(i * 0.9) * 2 + (i % 3)} r="0.7" fill={S.amberHi} style={{ animation:`ship-blink ${1.6 + (i % 4) * 0.4}s ease-in-out ${i * 0.13}s infinite` }} />)}
        <polygon points={pts([[40, 30], [52, 28], [46, 34]])} fill={ART.hull.base} /><polygon points={pts([[84, 24], [94, 22], [92, 28]])} fill={ART.hull.base} />
        <polygon points={pts([[12, 12], [52, 6], [26, 52]])} fill={S.white} opacity="0.08" />
      </g>
      <polyline points={pts(arch(6).slice(1, -1))} fill="none" stroke={ART.steel.hi} strokeWidth="0.7" opacity="0.5" />
      <polyline points={pts(arch(8).slice(1, -1))} fill="none" stroke={ART.steel.hi} strokeWidth="0.6" opacity="0.35" />
      {/* construction progress projected on the lower pane */}
      <rect x="22" y="42" width="84" height="14" fill={ART.screen.cyan} opacity="0.8" style={{ animation:'holo-flicker 3.4s ease-in-out infinite' }} />
      <rect x="22" y="42" width="84" height="14" fill="none" stroke={S.cyan} strokeWidth="0.6" />
      <text x="26" y="47" fontSize="3" fill={S.cyan} fontWeight="700" {...PT}>CONSTRUCTION PROGRESS</text>
      {[['VL-9', 92, S.amber], ['HH-3', 68, S.cyan], ['TH-2', 41, S.cyan]].map(([n, v, c], i) => (
        <g key={n}>
          <text x="26" y={51.4 + i * 2.8} fontSize="2.2" fill={S.cyanHi} {...PT}>{n}</text>
          <rect x="34" y={49.6 + i * 2.8} width="50" height="1.6" fill={ART.deepglass.shade} /><rect x="34" y={49.6 + i * 2.8} width={v * 0.5} height="1.6" fill={c} />
          <text x="86" y={51.4 + i * 2.8} fontSize="2.2" fill={c} {...PT}>{v}%</text>
        </g>
      ))}
      <Slab x={0} y={58} w={128} h={6} k={1} c="dark" />
      <Lights x={6} y={61} n={8} gap={6} r={0.7} colors={['cyan', 'amber']} blink={false} />
      <Grime x={0} y={56} w={128} h={8} seed={4} n={6} op={0.25} /><Scuff x={0} y={58} w={128} h={6} seed={6} n={8} c="dark" />
    </svg>
  );
}

function PropProtocolDroid() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 32 64" width="32" height="64" style={PROP_STYLE}>
      <PropShadow cx={16} cy={61} rx={12} ry={2.4} />
      <Bev t="polygon" points={pts(ngon(16, 60, 10, 2.6, 8))} c="dark" o={0.4} />
      {/* slender brass legs and copper knee joints */}
      <Bev t="polygon" points={pts([[9, 52], [14, 52], [14, 60], [9, 60]])} c="brass" o={0.5} /><Bev t="polygon" points={pts([[18, 52], [23, 52], [23, 60], [18, 60]])} c="brass" o={0.5} />
      <Bev t="polygon" points={pts(ngon(11.5, 50, 2.2, 2.2, 6))} c="copper" o={0.3} /><Bev t="polygon" points={pts(ngon(20.5, 50, 2.2, 2.2, 6))} c="copper" o={0.3} />
      {/* pressed formal chassis with the KDY crest */}
      <Bev t="polygon" points={pts([[10, 28], [22, 28], [23, 34], [21, 52], [11, 52], [9, 34]])} c="brass" o={0.7} />
      <path d="M11 42 L21 42 M10.4 46 L21.6 46 M12 28 L16 40 L20 28" stroke={ART.brass.shade} strokeWidth="0.6" opacity="0.8" fill="none" />
      <Bev t="polygon" points={pts(ngon(16, 35, 3, 3, 6))} c="dark" o={0.3} />
      <polygon points={pts([[14.4, 36.4], [16, 33], [17.6, 36.4], [16, 35.6]])} fill={ART.brass.hi} />
      {/* arms: thin segmented limbs, visible copper wiring at the joints */}
      <polyline points={pts([[9, 30], [5, 36], [5, 46]])} fill="none" stroke={ART.ink} strokeWidth="3.6" /><polyline points={pts([[9, 30], [5, 36], [5, 46]])} fill="none" stroke={ART.brass.base} strokeWidth="2.2" />
      <polyline points={pts([[23, 30], [27, 36], [27, 46]])} fill="none" stroke={ART.ink} strokeWidth="3.6" /><polyline points={pts([[23, 30], [27, 36], [27, 46]])} fill="none" stroke={ART.brass.base} strokeWidth="2.2" />
      <Bev t="polygon" points={pts(ngon(9, 30, 2.2, 2.2, 6))} c="copper" o={0.3} /><Bev t="polygon" points={pts(ngon(23, 30, 2.2, 2.2, 6))} c="copper" o={0.3} />
      <path d="M5 36 L5.4 38 M27 36 L26.6 38 M11 52 L11.4 54" stroke={ART.copper.base} strokeWidth="0.8" />
      <rect x="14" y="24" width="4" height="4" fill={ART.copper.base} />
      {/* angular faceplate with dimmed photoreceptors */}
      <Bev t="polygon" points={pts([[11, 14], [13, 8], [19, 8], [21, 14], [20, 22], [16, 24], [12, 22]])} c="brass" o={0.6} />
      <polygon points={pts([[12.4, 14], [19.6, 14], [19, 19], [13, 19]])} fill={ART.ink} />
      <circle cx="14" cy="16.4" r="1.5" fill={ART.dark.shade} /><circle cx="18" cy="16.4" r="1.5" fill={ART.dark.shade} />
      <circle cx="14" cy="16.4" r="0.8" fill={S.amber} opacity="0.6" style={{ animation:'ship-engine 4s ease-in-out infinite' }} /><circle cx="18" cy="16.4" r="0.8" fill={S.amber} opacity="0.6" style={{ animation:'ship-engine 4s ease-in-out 0.5s infinite' }} />
      <path d="M13.6 21 L18.4 21" stroke={ART.brass.shade} strokeWidth="0.5" />
      <rect x="4" y="46" width="12" height="4.6" fill={ART.paper.base} transform="rotate(-4 10 48)" />
      <text x="10" y="49.4" textAnchor="middle" fontSize="2" fill={ART.note.ink} fontWeight="700" transform="rotate(-4 10 48)" {...PT}>RUEN-7</text>
      <Seams x={10} y={28} w={12} h={24} rows={2} op={0.3} /><Rivets x={11} y={30} w={10} n={3} c="brass" r={0.5} /><Scuff x={9} y={28} w={14} h={24} seed={2} n={5} c="brass" /><Grime x={10} y={30} w={12} h={22} seed={3} n={3} op={0.18} />
    </svg>
  );
}

// ===== CORUSCANT PROPS BEGIN =====
// ---------------------------------------------------------------------------
// CORUSCANT SET PIECES (kit v3). Shared helpers first, then one component per kind.
// ---------------------------------------------------------------------------
const SCR = {
  cyan:  { bg:ART.screen.cyan,  ink:ART.signal.cyanHi,  trim:ART.signal.cyan },
  amber: { bg:ART.screen.amber, ink:ART.signal.gold,    trim:ART.signal.amber },
  green: { bg:ART.screen.green, ink:ART.signal.greenHi, trim:ART.signal.green },
  red:   { bg:ART.screen.red,   ink:ART.signal.redHi,   trim:ART.signal.red },
  off:   { bg:ART.screen.off,   ink:ART.dark.hi,        trim:ART.dark.base },
};

// A lit screen with a header bar, up to a few rows of text, and an optional scan line. Text is clamped to fit.
function Screen({ x, y, w, h, s, title, rows = [], fs = 2.4, scan = true, hl = -1, flick = false }) {
  const mc = Math.max(1, Math.floor((w - 3) / (0.6 * fs)));
  const tc = Math.max(1, Math.floor((w - 3) / (0.6 * (fs + 0.4))));
  return (
    <g>
      <rect x={x} y={y} width={w} height={h} fill={s.bg} />
      <rect x={x} y={y} width={w} height={fs * 2} fill={s.trim} opacity="0.3" />
      <g style={flick ? { animation:'holo-flicker 2.6s ease-in-out infinite' } : undefined}>
        {title && <text x={x + 1.6} y={y + fs * 1.45} fontSize={fs + 0.4} fill={s.ink} fontWeight="700" {...PT}>{title.slice(0, tc)}</text>}
        {rows.map((r, i) => <text key={i} x={x + 2} y={y + fs * 2 + 2.8 + i * fs * 1.8} fontSize={fs} fill={i === hl ? ART.signal.amberHi : s.ink} opacity={0.96 - i * 0.07} {...PT}>{('> ' + r).slice(0, mc)}</text>)}
      </g>
      {scan && h >= 22 && <rect x={x} y={y} width={w} height="1.4" fill={s.ink} opacity="0.3" style={{ animation:'prop-scan 3.4s linear infinite' }} />}
    </g>
  );
}

// Small flat emblems (about 2s wide) drawn around a centre point.
function Glyph({ k, x = 0, y = 0, s = 4, c = ART.signal.white }) {
  const t = `translate(${x} ${y})`;
  const ring = (n, r1, r2) => Array.from({ length: n }, (_, i) => { const a = (i / n) * Math.PI * 2, r = i % 2 ? r2 : r1; return [+(Math.cos(a) * r).toFixed(2), +(Math.sin(a) * r).toFixed(2)]; });
  switch (k) {
    case 'cog': return <g transform={t}><polygon points={pts(ring(12, s, s * 0.72))} fill={c} /><circle r={s * 0.34} fill={ART.ink} /></g>;
    case 'gear': return <g transform={t}><polygon points={pts(ring(16, s, s * 0.74))} fill={c} /><circle r={s * 0.3} fill={ART.ink} /></g>;
    case 'chain': return <g transform={t} fill="none" stroke={c} strokeWidth="0.9"><polygon points={pts(ngon(-s * 0.45, 0, s * 0.7, s * 0.45, 6))} /><polygon points={pts(ngon(s * 0.45, 0, s * 0.7, s * 0.45, 6))} /></g>;
    case 'star': return <g transform={t} stroke={c} strokeWidth="0.6" fill="none">{Array.from({ length: 12 }, (_, i) => { const a = (i / 12) * Math.PI * 2; return <line key={i} x1={Math.cos(a) * s * 0.42} y1={Math.sin(a) * s * 0.42} x2={Math.cos(a) * s} y2={Math.sin(a) * s} />; })}<circle r={s * 0.42} /></g>;
    case 'drop': return <polygon transform={t} points={pts([[0, -s], [s * 0.7, s * 0.2], [s * 0.45, s * 0.8], [-s * 0.45, s * 0.8], [-s * 0.7, s * 0.2]])} fill={c} />;
    case 'wave': return <polyline transform={t} points={pts([[-s, 0], [-s * 0.5, -s * 0.7], [0, s * 0.7], [s * 0.5, -s * 0.7], [s, 0]])} fill="none" stroke={c} strokeWidth="0.9" />;
    case 'eye': return <g transform={t}><polygon points={pts([[-s, 0], [0, -s * 0.6], [s, 0], [0, s * 0.6]])} fill="none" stroke={c} strokeWidth="0.8" /><circle r={s * 0.28} fill={c} /></g>;
    case 'env': return <g transform={t} fill="none" stroke={c} strokeWidth="0.8"><polygon points={pts([[-s, -s * 0.6], [s, -s * 0.6], [s, s * 0.6], [-s, s * 0.6]])} /><polyline points={pts([[-s, -s * 0.6], [0, s * 0.1], [s, -s * 0.6]])} /></g>;
    case 'badge': return <polygon transform={t} points={pts([[-s * 0.8, -s], [s * 0.8, -s], [s * 0.8, s * 0.2], [0, s], [-s * 0.8, s * 0.2]])} fill={c} />;
    case 'coin': return <g transform={t}><polygon points={pts(ngon(0, 0, s * 0.9, s * 0.9, 8))} fill={c} /><polygon points={pts(ngon(0, 0, s * 0.5, s * 0.5, 8))} fill={ART.ink} /></g>;
    case 'key': return <g transform={t} fill={c}><polygon points={pts(ngon(-s * 0.4, 0, s * 0.5, s * 0.5, 6))} /><rect x={-s * 0.1} y={-s * 0.15} width={s * 1.1} height={s * 0.3} /></g>;
    default: return null;
  }
}

// ---------------------------------------------------------------------------
// CONSOLE: the workhorse terminal. Three housings (desk, wall, slate), each with its own screen text.
// ---------------------------------------------------------------------------
const CONSOLE_CFG = {
  customs:      { l:'desk',  body:'steel',    scr:'cyan',  g:'cog',   t:'CUSTOMS ENTRY LOG',    r:['TRANSIT CLEARANCE','STATUS: PROVISIONAL','ENTRY TIME LOGGED','SIGNAL: FLICKERING'], d:['flicker'] },
  zillow:       { l:'desk',  body:'pearl',    scr:'amber', g:'key',   t:'ZILLOW HOUSING',       r:['SUBSURFACE L2: 0','LEVEL 5 AND UP: 3000','PRICE: SEE LISTING','(YOUR EYES WATER)'], d:['note'] },
  newsfeed:     { l:'slate', body:'steel',    scr:'cyan',  g:'wave',  t:'HNN NEWSFEED',         r:['SENATE VOTES TO','EXTEND EMERGENCY','POWERS'], d:['scratch'] },
  exchange:     { l:'desk',  body:'brass',    scr:'amber', g:'coin',  t:'TRADE EXCHANGE',       r:['14 SYSTEMS: LIVE','FLAGGED MANIFEST:','SCYLLA FREIGHT','DEST: REDACTED'], d:[] },
  dispatch:     { l:'desk',  body:'deepglass',scr:'cyan',  g:'badge', t:'CSF DISPATCH',         r:['12 PATROL CHANNELS','6 ROUTINE  5 STATIC','S4 CH B: LOOP ACTIVE','ALL UNITS STAND BY'], d:['headset'] },
  siphon:       { l:'desk',  body:'dark',     scr:'green', g:'wave',  t:'CARGO FEED SPLICE',    r:['UPLINK: LIVE','BAND: CUSTOMS BCAST','OVERRIDES: 6 MONTHS','LOCK: FREQ KEYED'], d:['cables'] },
  manifest088:  { l:'desk',  body:'rust',     scr:'amber', g:'cog',   t:'MANIFEST REGISTRY',    r:['3 CONTAINERS STAMPED','SENATE SUBCOMMITTEE','1 ANOMALOUS: REDACTED','CODE VALID: NO RECORD'], d:['hazard'] },
  crane:        { l:'wall',  body:'rust',     scr:'amber', g:'gear',  t:'CRANE AUTOMATION',     r:['BAY 088 EXTERIOR','HOIST: ARMED','CODE INPUT: OPEN'], d:['hazard'] },
  radio:        { l:'desk',  body:'dark',     scr:'green', g:'wave',  t:'SUB-GRIT 104.9',       r:['UNAUTHORIZED SIGNAL','BAY 14: LOGISTICS','BLACK SUN TOLL BOOTH','BETS AT VONDS: 3 TO 1'], d:['antenna'] },
  freight:      { l:'desk',  body:'steel',    scr:'cyan',  g:'cog',   t:'NORTH PLATFORM',       r:['CONSIGNMENTS: 14 MO','SEQUENCE: NO GAP','3 NUMBERS REUSED','ORIGINALS: ERASED'], d:[] },
  fuel:         { l:'desk',  body:'rust',     scr:'amber', g:'drop',  t:'FUEL TRANSFER REG',    r:['USAGE VS DEPARTURES','LOSS: 40 L PER CYCLE','LOGGED AS WASTE: NO','LOGGED AT ALL: NO'], d:['hazard'] },
  pit:          { l:'wall',  body:'rust',     scr:'red',   g:'gear',  t:'PIT ACCESS',           r:['17 EVENTS IN 2 MO','USER: TEMP-TRANSIT','SINGLE USE: ALL SAME','DAY AND SAME HOUR'], d:[] },
  surveil:      { l:'desk',  body:'deepglass',scr:'cyan',  g:'eye',   t:'HOLONET SURVEILLANCE', r:['4 SENATE OFFICES','VANE REPORT OPENED','2X IN 48 HOURS','TERMINAL: TORVENN'], d:['scanner'] },
  derelict:     { l:'desk',  body:'steel',    scr:'green', g:'wave',  t:'EMERGENCY BAND RELAY', r:['FREQ NOT AUTHORISED','LAPSED 80 YEARS AGO','SIGNAL: STILL ACTIVE','DISPLAY: UNSTABLE'], d:['overgrown','flicker','cracked'] },
  oza:          { l:'desk',  body:'dark',     scr:'red',   g:'eye',   t:'OZA-2 HARDWARE',       r:['PRICES: UNLISTED','STOCK: ASK','LEGAL INQUIRIES','DELETED ON RECEIPT'], d:['redlights'] },
  registry:     { l:'desk',  body:'steel',    scr:'amber', g:'chain', t:'SUB-LEVEL REGISTRY',   r:['L1312 BUSINESSES: 43','REGISTERED AGENT:','IRON SYNDICATE','HOLDINGS LLC'], d:['cracked'] },
  vault:        { l:'wall',  body:'dark',     scr:'amber', g:'coin',  t:'CREDIT ROUTING',       r:['PAYROLL: TONIGHT','9 CSF OFFICERS','38 SECURITY STAFF','11 ANON ACCOUNTS'], d:['scanner'] },
  transmission: { l:'desk',  body:'dark',     scr:'cyan',  g:'env',   t:'HOLONET MESSAGE',      r:['SENDER: NONE','ORIGIN: NO TRACE','ROUTING: TOO CLEAN','BERTH 7 KUAT ORBITAL'], d:[] },
  shipping:     { l:'desk',  body:'pearl',    scr:'cyan',  g:'cog',   t:'SHIPPING AUTHORITY',   r:['FREIGHT ROUTES','SENATE SUPPLY CHAINS','DIPLOMATIC EXEMPTS','ACCESS: SLICING RUN'], d:[] },
  slush:        { l:'desk',  body:'pearl',    scr:'amber', g:'coin',  t:'DISCRETIONARY FUND',   r:['ACCOUNT: UNLOCKED','AUTH WINDOW: OPEN','SESSION: IN PROGRESS'], d:[] },
  exchange_bm:  { l:'desk',  body:'dark',     scr:'amber', g:'star',  t:'COMMODITY EXCHANGE',   r:['GLITTERSTIM  LIVE','BLASTERS  LIVE','SLICING CHIPS  LIVE','NO CUSTOMS FORM'], d:['antenna'] },
  reclaim:      { l:'wall',  body:'rust',     scr:'green', g:'gear',  t:'RECLAMATION CTRL',     r:['OVERFLOW VALVE: OPEN','LOCKED BY: UNKNOWN','WASTE LEVEL: HIGH'], d:['hazard'] },
  induction:    { l:'desk',  body:'deepglass',scr:'cyan',  g:'badge', t:'CSF INDUCTION',        r:['AUXILIARY CORPS','DESIGNATION: AX-7','ACCESS: PROVISIONAL','SUPERVISOR: VANE T'], d:[] },
  module_a:     { l:'wall',  body:'deepglass',scr:'red',   g:'badge', t:'MODULE A: COMBAT',     r:['NON LETHAL ONLY','STUN CONFIG','ONE LETHAL: RUN ENDS'], d:['hazard'] },
  module_b:     { l:'wall',  body:'deepglass',scr:'green', g:'eye',   t:'MODULE B: FORENSIC',   r:['BAY 14 SIMULATION','CHAIN BREAKS: 3','FIND ALL THREE'], d:[] },
  warrant:      { l:'desk',  body:'deepglass',scr:'cyan',  g:'badge', t:'PRECINCT 42 REMOTE',   r:['L1220 WARRANT ARCHIVE','WIRED IN WRONG','DISK ID: MATCH'], d:['scanner'] },
  rook:         { l:'desk',  body:'dark',     scr:'cyan',  g:'wave',  t:'SPEEDER NAV ARRAY',    r:['ROOK ROUTE: LOADED','CODE CYLINDER: SLOT','REDIRECT: READY'], d:['cables'] },
  hnn_official: { l:'desk',  body:'steel',    scr:'cyan',  g:'wave',  t:'HNN OFFICIAL FEED',    r:['SENATE COMMENDS CSF','SMUGGLING NEAR L088','ISOLATED DELAYS','BROADCAST: LOOPING'], d:['soot'] },
  frost:        { l:'desk',  body:'teal',     scr:'cyan',  g:'drop',  t:'TEMPERATURE LOGS',     r:['COOLING THROTTLED','12 DAYS AGO','ARMOR RUN: FINAL'], d:['frost'] },
  syndicate:    { l:'desk',  body:'dark',     scr:'red',   g:'chain', t:'IRON SYNDICATE CMD',   r:['STATUS: ACTIVE','ENCRYPTION: NONE','EXTRACTION TIMETABLE','MANIFESTS  ROTATIONS'], d:['redlights'] },
  assembly:     { l:'desk',  body:'steel',    scr:'amber', g:'gear',  t:'ASSEMBLY OVERRIDE',    r:['OUTPUT ROUTED OFF','OFF-MANIFEST BAY','CODE: NOT IN KDY LIB','MODIFIED: UNKNOWN'], d:['hazard'] },
  republic_old: { l:'desk',  body:'concrete', scr:'amber', g:'cog',   t:'REPUBLIC OUTPOST',     r:['DECOMMISSIONED 3 YR','LAST ENTRY: DISTRESS','RESPONSE: NONE LOGGED'], d:['cracked','soot'] },
  architect:    { l:'desk',  body:'brass',    scr:'amber', g:'eye',   t:'ARCHITECT PERSONAL',   r:['ACCESS LOGS: 43 YRS','STATUS: UNLOCKED','LAST USER EXPECTED','SOMEONE TO COME'], d:['flicker'] },
  substation:   { l:'wall',  body:'rust',     scr:'amber', g:'gear',  t:'SUB-STATION 3',        r:['THERMAL HAZARDS','AMBIENT CONTROL','STATUS: HOLDING'], d:['hazard'] },
  // MFG CONSOLE BEGIN
  assembly_done:  { l:'desk',  body:'steel',    scr:'green', g:'gear',  t:'OVERRIDE ACTIVE',      r:['BAY B: PANEL UNSEALED','CODE: ACCEPTED','OUTPUT LOG: COPIED','NO ALARM RAISED'], d:['hazard'] },
  mfg_eng:        { l:'desk',  body:'steel',    scr:'cyan',  g:'cog',   t:'ENGINEERING ARCHIVE',  r:['SEQUENCE FILES: 14','FRAME: NO REGISTRY','KEEL: 1800 M','LAST VIEWED: NIGHT'], d:['cables'] },
  mfg_eng_done:   { l:'desk',  body:'steel',    scr:'green', g:'cog',   t:'ARCHIVE COPIED',       r:['14 FILES: COPIED','VIEW LOG: CLEARED','LOOKS UNTOUCHED'], d:['cables'] },
  mfg_bay:        { l:'wall',  body:'dark',     scr:'red',   g:'eye',   t:'BAY B MANIFEST CORE',  r:['CONSIGNMENTS: 212','MANIFEST: NONE','DEST: GRID NINE ONE','ACCESS: SEALED'], d:['redlights'] },
  mfg_bay_done:   { l:'wall',  body:'dark',     scr:'green', g:'eye',   t:'LEDGER EXTRACTED',     r:['212 ENTRIES: COPIED','APPROVED: KRAX','COPY: YOURS'], d:['redlights'] },
  mfg_office:     { l:'desk',  body:'brass',    scr:'amber', g:'coin',  t:'OVERSEER ROUTING',     r:['QUOTA: 114 PCT','SURPLUS: ROUTED','NIGHT CREW: 128','AUTH: KRAX K'], d:['note'] },
  // MFG CONSOLE END
};

function PropConsole({ variant }) {
  const c = CONSOLE_CFG[variant] || CONSOLE_CFG.customs;
  const s = SCR[c.scr], B = c.body, S = ART.signal, has = (k) => c.d.includes(k);
  const seed = (variant || 'x').length * 7 + 3;
  const L = c.l;
  const screen = L === 'slate' ? { x:18, y:8, w:28, h:30, fs:2.2 } : L === 'wall' ? { x:7, y:14, w:38, h:26, fs:2.2 } : { x:9, y:9, w:46, h:26, fs:2.4 };
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={L === 'slate' ? 18 : 25} ry={3.4} />
      {L === 'desk' && <g>
        <Slab x={22} y={44} w={20} h={12} k={1.4} c="dark" /><Vent x={25} y={47} w={14} h={6} n={3} />
        <Slab x={12} y={54} w={40} h={5} k={1.4} c="steel" o={0.8} />
        <Bev t="polygon" points={pts([[8, 5], [56, 5], [60, 10], [60, 42], [56, 46], [8, 46], [4, 42], [4, 10]])} c={B} o={1} />
        <Seams x={4} y={5} w={56} h={41} rows={1} cols={1} op={0.25} />
      </g>}
      {L === 'wall' && <g>
        <Slab x={8} y={54} w={10} h={5} k={1} c="dark" /><Slab x={46} y={54} w={10} h={5} k={1} c="dark" />
        <Cable d="M10 8 L10 0" w={2.4} /><Cable d="M52 8 C52 4 56 3 56 0" w={2} />
        <Slab x={2} y={8} w={60} h={47} k={2} c={B} o={1} />
        <Seams x={2} y={8} w={60} h={47} rows={1} cols={2} op={0.22} />
      </g>}
      {L === 'slate' && <g>
        <Slab x={20} y={53} w={24} h={6} k={1.2} c="steel" />
        <Slab x={28} y={38} w={8} h={16} k={1} c="dark" />
        <Bev t="polygon" points={pts([[16, 4], [48, 4], [51, 8], [51, 38], [48, 42], [16, 42], [13, 38], [13, 8]])} c={B} o={1} />
      </g>}
      <Screen {...screen} s={s} title={c.t} rows={c.r} flick={has('flicker')} scan={!has('cracked')} />
      <Glyph k={c.g} x={screen.x + screen.w - 5} y={screen.y + screen.h - 6} s={3.4} c={s.ink} />
      {L === 'desk' && <g>
        <rect x="8" y="5" width="48" height="2" fill={s.trim} opacity="0.55" />
        <Toggles x={8} y={37} cols={10} rows={2} gap={3.6} seed={seed} />
        <Slab x={46} y={36} w={11} h={7} k={0.8} c="dark" o={0.4} /><Lights x={47.5} y={39.5} n={4} gap={2.6} seed={seed + 1} />
      </g>}
      {L === 'wall' && <g>
        {[['red', 17], ['amber', 24], ['green', 31]].map(([k, y]) => <g key={k}><Slab x={49} y={y - 3} w={10} h={5.6} k={1} c="dark" o={0.5} /><rect x="50.4" y={y - 1.6} width="7.2" height="2.8" fill={S[k]} style={k === 'red' ? { animation:'ship-blink 1.6s steps(2) infinite' } : undefined} /></g>)}
        <Slab x={8} y={43} w={26} h={8} k={1} c="dark" o={0.5} /><Toggles x={10} y={44.4} cols={7} rows={2} gap={3.3} seed={seed} />
        <Vent x={38} y={44} w={20} h={7} n={3} />
        <Rivets x={6} y={11} w={52} n={7} />
      </g>}
      {L === 'slate' && <g>
        <Lights x={19} y={39.4} n={6} gap={5} seed={seed} />
        <Rivets x={16} y={6} w={32} n={5} c="steel" />
        <line x1="32" y1="4" x2="32" y2="0.5" stroke={ART.steel.hi} strokeWidth="0.9" /><circle cx="32" cy="0.8" r="0.9" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      </g>}
      {has('hazard') && <Hazard x={L === 'wall' ? 4 : 6} y={L === 'wall' ? 51 : 56} w={L === 'wall' ? 56 : 52} h={3} />}
      {has('cracked') && <g fill="none"><polyline points={pts([[screen.x + screen.w - 14, screen.y + screen.h], [screen.x + screen.w - 9, screen.y + screen.h - 8], [screen.x + screen.w - 12, screen.y + screen.h - 14]])} stroke={ART.ink} strokeWidth="0.7" /><polyline points={pts([[screen.x + screen.w - 9, screen.y + screen.h - 8], [screen.x + screen.w - 2, screen.y + screen.h - 10]])} stroke={s.ink} strokeWidth="0.4" opacity="0.8" /></g>}
      {has('scorch') && <polygon points={pts([[6, 6], [24, 6], [16, 16], [6, 20]])} fill={ART.ink} opacity="0.3" />}
      {has('soot') && <g fill={ART.ink} opacity="0.42"><polygon points={pts([[4, 5], [34, 5], [28, 13], [14, 22], [4, 28]])} /><polygon points={pts([[44, 38], [60, 36], [60, 46], [46, 46]])} /></g>}
      {has('frost') && <g fill={ART.pearl.hi} opacity="0.75"><polygon points={pts([[4, 10], [14, 8], [12, 18], [4, 22]])} /><polygon points={pts([[48, 40], [60, 36], [60, 46], [50, 46]])} /><polygon points={pts([[8, 42], [22, 40], [18, 46], [8, 46]])} /><polygon points={pts([[40, 9], [52, 9], [48, 15]])} opacity="0.7" /></g>}
      {has('cables') && <g><Cable d="M58 10 C64 22 52 34 60 54" w={2.2} /><Cable d="M6 44 C-2 50 8 56 4 60" w={1.8} /></g>}
      {has('overgrown') && <g><Cable d="M4 6 C-2 20 14 28 6 44" w={2.4} c={ART.moss.shade} hi="moss" /><Cable d="M60 4 C66 18 50 30 58 50" w={2.4} c={ART.moss.shade} hi="moss" /><Cable d="M14 46 C24 52 38 48 52 54" w={1.8} /><Cable d="M20 5 C26 -1 36 -1 40 4" w={1.6} /></g>}
      {has('headset') && <g><polyline points={pts([[58, 12], [63, 8], [63, 2], [57, 0.5]])} fill="none" stroke={ART.dark.hi} strokeWidth="1.6" /><Slab x={57} y={11} w={6} h={9} k={1.4} c="dark" o={0.5} /></g>}
      {has('scanner') && <g><Slab x={L === 'wall' ? 44 : 46} y={L === 'wall' ? 43 : 36} w={13} h={9} k={1.2} c="dark" o={0.5} /><rect x={L === 'wall' ? 46 : 48} y={L === 'wall' ? 45 : 38} width="9" height="5" fill={S.cyan} opacity="0.85" style={{ animation:'ship-blink 2.4s steps(2) infinite' }} /></g>}
      {has('antenna') && <g><line x1="55" y1="5" x2="55" y2="-1" stroke={ART.steel.hi} strokeWidth="0.9" /><line x1="50" y1="5" x2="52" y2="0" stroke={ART.steel.base} strokeWidth="0.7" /><circle cx="55" cy="-0.4" r="1" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} /></g>}
      {has('redlights') && <g><circle cx="7" cy="44" r="1" fill={S.red} /><circle cx="11" cy="44" r="1" fill={S.red} style={{ animation:'ship-blink 1.2s steps(2) infinite' }} /><circle cx="15" cy="44" r="1" fill={S.red} /></g>}
      {has('note') && <g transform="rotate(-6 54 40)"><polygon points={pts([[48, 32], [60, 32], [60, 43], [48, 43]])} fill={ART.note.yellow} /><line x1="50" y1="35.5" x2="58" y2="35.5" stroke={ART.note.ink} strokeWidth="0.5" /><line x1="50" y1="38" x2="57" y2="38" stroke={ART.note.ink} strokeWidth="0.5" /><line x1="50" y1="40.5" x2="55" y2="40.5" stroke={ART.note.ink} strokeWidth="0.5" /></g>}
      {has('scratch') && <g transform="rotate(-4 32 34)"><text x="17" y="35" fontSize="3.4" fill={ART.steel.hi} opacity="0.9" {...PT}>they know</text></g>}
      <Grime x={5} y={L === 'wall' ? 42 : 38} w={54} h={L === 'wall' ? 12 : 8} seed={seed + 9} n={4} op={0.18} /><Scuff x={6} y={L === 'wall' ? 42 : 38} w={50} h={L === 'wall' ? 10 : 8} seed={seed + 4} n={7} c={B} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// KIOSK: pedestal that projects a flat holo display. Used for news feeds, menus, registries.
// ---------------------------------------------------------------------------
const KIOSK_CFG = {
  hnn:       { t:'HNN PRIORITY FEED', scr:'cyan',  r:['PORT AUTHORITY: BAY 14','FUEL LINE INCIDENT','RESOLVED. LOGS DESTROYED','WEATHER ADVISORY: NONE'], hl:3, badge:'LIVE' },
  senate:    { t:'HNN: SENATE WING',  scr:'cyan',  r:['COMMERCE AUTHORITY:','BAY 14 IS ADMIN MATTER','3 THREADS: ALL FLAGGED','14 T CLEARED UNSEEN'], hl:3, badge:'LIVE' },
  manifest:  { t:'CARGO MANIFEST REG',scr:'cyan',  r:['QUERY: SCYLLA FREIGHT','RESULTS: 0','BAY 14 BERTH: 1 ENTRY','CASE CLOSED. PURGED.'], hl:3 },
  drinks:    { t:'37 SPIRITS 14 SYS', scr:'amber', r:['LOCAL SPECIAL:','BAY 14 BLEND'], hl:1, bottles:true },
  refuel:    { t:'OUT OF ORDER',      scr:'red',   r:['PUMP 3: OFFLINE','PUMP 4: OFFLINE'], note:'USE BAY 2' },
  collapsed: { t:'HNN: CACHED',       scr:'off',   r:['REPUBLIC CONFIRMS L1313','INFRASTRUCTURE SEALED','DATE STAMP: 62 YEARS'], dead:true },
  slicer:    { t:'HNN PRIORITY',      scr:'cyan',  r:['INFO SECURITY VOTES','TO EXPAND MONITORING','PASSED 9 TO 3'], cracked:true, stencil:true },
};

function PropKiosk({ variant }) {
  const c = KIOSK_CFG[variant] || KIOSK_CFG.hnn;
  const s = SCR[c.scr], S = ART.signal;
  const lean = c.dead ? 'rotate(-5 48 58)' : undefined;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={59} rx={24} ry={3.4} />
      <g transform={lean}>
        {!c.dead && <polygon points={pts([[39, 32], [57, 32], [84, 29], [12, 29]])} fill={s.trim} opacity="0.13" />}
        <Bev t="polygon" points={pts([[28, 58], [68, 58], [62, 50], [34, 50]])} c="steel" o={0.9} />
        <Hazard x={36} y={52.6} w={24} h={2.6} />
        <Bev t="polygon" points={pts([[43, 50], [53, 50], [52, 36], [44, 36]])} c="dark" o={0.8} />
        <Seams x={43} y={36} w={10} h={14} rows={2} op={0.4} />
        <Lights x={45.4} y={47} n={3} gap={2.6} r={0.8} seed={5} />
        <Bev t="polygon" points={pts(ngon(48, 34, 14, 4.2, 8, Math.PI / 8))} c="steel" o={0.9} />
        <polygon points={pts(ngon(48, 33.6, 9.4, 2.6, 8, Math.PI / 8))} fill={c.dead ? ART.dark.base : s.ink} />
        {!c.dead && <Glow cx={48} cy={33.4} rx={11} ry={3} c={s.trim} opacity={0.6} />}
        <Rivets x={37} y={56} w={22} n={5} />
      </g>
      <g transform={lean} opacity={c.dead ? 0.85 : 1}>
        <rect x="11" y="1" width="74" height="28" fill={s.bg} opacity="0.94" />
        <rect x="11" y="1" width="74" height="28" fill="none" stroke={s.trim} strokeWidth="0.9" />
        {[[11, 1, 1, 1], [85, 1, -1, 1], [11, 29, 1, -1], [85, 29, -1, -1]].map(([x, y, dx, dy], i) => <polyline key={i} points={pts([[x + dx * 5, y], [x, y], [x, y + dy * 5]])} fill="none" stroke={s.ink} strokeWidth="1.4" />)}
        <Screen x={13} y={4} w={70} h={23} s={{ bg:'none', ink:s.ink, trim:s.trim }} title={c.t} rows={c.r} hl={c.hl ?? -1} fs={2.5} scan={false} flick={!c.dead} />
        {c.badge && <g><Slab x={70} y={4.6} w={12} h={4.6} k={0.8} c="red" o={0.4} /><text x="76" y="8" textAnchor="middle" fontSize="2.6" fill={S.white} fontWeight="700" {...PT}>{c.badge}</text></g>}
        {c.bottles && <g>{[[62, 'amber'], [68, 'cyan'], [74, 'red'], [80, 'green']].map(([x, k], i) => <g key={i}><rect x={x - 2} y="13" width="4" height="9" fill={S[k]} opacity="0.9" /><rect x={x - 0.8} y="10" width="1.6" height="3.2" fill={S[k]} opacity="0.9" /></g>)}</g>}
        {c.cracked && <polyline points={pts([[20, 4], [28, 14], [24, 20], [40, 27]])} fill="none" stroke={ART.ink} strokeWidth="0.8" />}
        {c.stencil && <Glyph k="star" x={72} y={19} s={4.4} c={S.crimson} />}
        {c.dead && <rect x="11" y="1" width="74" height="28" fill={ART.ink} opacity="0.4" />}
      </g>
      {c.note && <g transform="rotate(5 70 38)"><polygon points={pts([[60, 33], [82, 33], [82, 45], [60, 45]])} fill={ART.paper.base} /><polygon points={pts([[60, 33], [82, 33], [82, 35], [60, 35]])} fill={ART.paper.shade} /><text x="71" y="42" textAnchor="middle" fontSize="3.6" fill={ART.note.ink} fontWeight="700" {...PT}>{c.note}</text></g>}
      {c.dead && <g><Cable d="M62 54 C70 50 78 58 88 52" w={1.8} /><polygon points={pts([[72, 56], [76, 52], [78, 57]])} fill={S.warn} /></g>}
      <Grime x={34} y={36} w={28} h={22} seed={14} n={3} op={0.2} /><Scuff x={32} y={40} w={32} h={18} seed={6} n={5} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// AIRTAXI: yellow roof sign, call terminal, painted landing mark. One silhouette, nine states of repair.
// ---------------------------------------------------------------------------
const TAXI_CFG = {
  clean:    { body:'pearl',     scr:'cyan',  r:['DEPARTS','ON DEMAND'] },
  worn:     { body:'steel',     scr:'cyan',  r:['ROUTES: 47','HELD: 29'], grime:true },
  cracked:  { body:'steel',     scr:'cyan',  r:['SIGNAL','UNSTABLE'], cracked:true, grime:true },
  dark:     { body:'dark',      scr:'off',   r:['',''], led:true },
  jury:     { body:'steel',     scr:'green', r:['NO PERMIT','LINKED'], cables:true },
  ancient:  { body:'brass',     scr:'amber', r:['ROUTES: ??','LEGACY NET'], grime:true },
  scorched: { body:'rust',      scr:'amber', r:['HEAT WARN','STILL WORKS'], soot:true },
  rigged:   { body:'dark',      scr:'red',   r:['CODE ERROR','STILL WORKS'], cables:true },
  official: { body:'deepglass', scr:'cyan',  r:['CSF ONLY','AUTH REQ'] },
  private:  { body:'bronze',    scr:'amber', r:['PRIVATE NODE','NO LOG'] },
};

function PropAirTaxi({ variant }) {
  const c = TAXI_CFG[variant] || TAXI_CFG.worn;
  const s = SCR[c.scr], S = ART.signal, B = c.body;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={28} ry={3.6} />
      {/* painted landing mark with corner lamps */}
      <polygon points={pts(ngon(47, 49, 15, 8.4, 8, Math.PI / 8))} fill={ART.dark.shade} />
      <polygon points={pts(ngon(47, 48.4, 13, 7.2, 8, Math.PI / 8))} fill={ART.dark.base} />
      <polyline points={pts([[40, 45], [54, 45]])} fill="none" stroke={S.taxi} strokeWidth="1.6" /><polyline points={pts([[47, 45], [47, 53]])} fill="none" stroke={S.taxi} strokeWidth="1.6" />
      {[[35, 49], [59, 49], [47, 42], [47, 56]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="0.9" fill={S.amber} style={{ animation:`ship-blink ${1.6 + i * 0.3}s steps(2) infinite` }} />)}
      {/* terminal post */}
      <Slab x={6} y={52} w={26} h={6} k={1.2} c="steel" />
      <Slab x={7} y={14} w={25} h={40} k={2} c={B} o={1} />
      <Seams x={7} y={14} w={25} h={40} rows={2} op={0.3} />
      <Screen x={9.4} y={17} w={20.2} h={15} s={s} rows={c.r} fs={2.1} scan={false} flick={c.scr !== 'off'} />
      {c.led && <circle cx="19.5" cy="25" r="1.4" fill={S.green} style={{ animation:'ship-blink 2s steps(2) infinite' }} />}
      {c.cracked && <polyline points={pts([[13, 17], [18, 24], [16, 28], [24, 32]])} fill="none" stroke={ART.ink} strokeWidth="0.7" />}
      <Toggles x={10} y={35} cols={6} rows={2} gap={3.3} seed={9} />
      <Slab x={10} y={43} w={9} h={5} k={0.8} c="dark" o={0.4} /><circle cx="25" cy="45.4" r="2.4" fill={S.red} style={{ animation:'ship-blink 1.8s steps(2) infinite' }} />
      <Hazard x={9} y={49} w={21} h={3} />
      {/* roof sign: taxi yellow with a checker band */}
      <Bev t="polygon" points={pts([[4, 4], [34, 4], [36, 6], [36, 12], [34, 14], [4, 14], [2, 12], [2, 6]])} c="amber" o={0.9} />
      <text x="19" y="10.6" textAnchor="middle" fontSize="4.2" fill={ART.ink} fontWeight="700" {...PT}>AIRTAXI</text>
      {Array.from({ length: 12 }, (_, i) => <rect key={i} x={4 + i * 2.5} y={i % 2 ? 11.8 : 13} width="2.5" height="1.2" fill={ART.ink} />)}
      <Rivets x={5} y={5.4} w={28} n={6} c="amber" r={0.45} />
      <line x1="31" y1="4" x2="31" y2="0.6" stroke={ART.steel.hi} strokeWidth="0.8" /><circle cx="31" cy="0.8" r="0.9" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      {c.cables && <g><Cable d="M28 38 C36 40 30 50 40 56" w={1.8} /><polygon points={pts([[38, 55], [42, 54], [41, 58]])} fill={S.warn} /></g>}
      {c.soot && <polygon points={pts([[9, 14], [24, 14], [18, 24], [9, 30]])} fill={ART.ink} opacity="0.4" />}
      {c.grime && <Grime x={9} y={16} w={20} h={36} seed={21} n={5} op={0.25} />}
      <Grime x={9} y={16} w={20} h={36} seed={22} n={3} op={0.12} /><Scuff x={9} y={18} w={20} h={34} seed={23} n={6} c={B} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// COMM RELAY: a dish, a hub, or a comlink puck throwing a call hologram.
// ---------------------------------------------------------------------------
function PropCommRelay({ variant }) {
  const S = ART.signal;
  const dish = (cx, cy, r, rot, k) => (
    <g key={k} transform={`rotate(${rot} ${cx} ${cy})`}>
      <polygon points={pts(ngon(cx + 1.2, cy + 1.6, r, r * 0.62, 10))} fill={ART.steel.shade} />
      <Bev t="polygon" points={pts(ngon(cx, cy, r, r * 0.62, 10))} c="hull" o={0.8} />
      <polygon points={pts(ngon(cx, cy, r * 0.8, r * 0.46, 10))} fill={ART.steel.shade} />
      <polygon points={pts(ngon(cx - r * 0.06, cy - r * 0.04, r * 0.5, r * 0.28, 10))} fill={ART.steel.base} />
      <line x1={cx} y1={cy} x2={cx + r * 0.1} y2={cy - r * 1.1} stroke={ART.steel.hi} strokeWidth="0.9" />
      <circle cx={cx + r * 0.1} cy={cy - r * 1.12} r="1.1" fill={S.red} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} />
    </g>
  );
  if (variant === 'puck') {
    return (
      <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
        <PropShadow cx={32} cy={56} rx={18} ry={4} />
        <polygon points={pts([[22, 52], [42, 52], [52, 22], [12, 22]])} fill={S.cyan} opacity="0.1" />
        <g style={{ animation:'holo-flicker 2.2s ease-in-out infinite' }}>
          <polygon points={pts([[24, 44], [40, 44], [44, 36], [38, 30], [26, 30], [20, 36]])} fill={S.cyan} opacity="0.55" />
          <polygon points={pts(ngon(32, 22, 6.4, 7.4, 8))} fill={S.cyanHi} opacity="0.7" />
          {[16, 24, 32, 40].map((y) => <line key={y} x1="16" y1={y} x2="48" y2={y} stroke={S.cyanHi} strokeWidth="0.4" opacity="0.35" />)}
        </g>
        <Bev t="polygon" points={pts(ngon(32, 52, 15, 5.6, 10))} c="dark" o={1} />
        <polygon points={pts(ngon(32, 51.4, 11, 3.8, 10))} fill={ART.screen.cyan} />
        <polygon points={pts(ngon(32, 51.4, 7, 2.2, 10))} fill={S.cyan} style={{ animation:'ship-blink 1.2s steps(2) infinite' }} />
        <text x="32" y="9" textAnchor="middle" fontSize="3.6" fill={S.cyanHi} fontWeight="700" {...PT}>INCOMING</text>
        <Rivets x={20} y={55.4} w={24} n={6} /><Grime x={18} y={48} w={28} h={8} seed={3} n={3} op={0.2} /><Scuff x={18} y={48} w={28} h={8} seed={8} n={4} c="dark" />
      </svg>
    );
  }
  const hub = variant === 'hub';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={24} ry={3.6} />
      {/* tripod legs */}
      <polygon points={pts([[32, 36], [14, 58], [18, 58], [33, 42]])} fill={ART.steel.shade} /><polygon points={pts([[32, 36], [50, 58], [46, 58], [31, 42]])} fill={ART.steel.shade} />
      <Bev t="polygon" points={pts([[29, 14], [35, 14], [36, 40], [28, 40]])} c="steel" o={0.8} />
      <g stroke={ART.steel.shade} strokeWidth="0.7">{[18, 24, 30, 36].map((y) => <line key={y} x1="29.4" y1={y} x2="34.6" y2={y + 6} />)}</g>
      {hub ? <g>{dish(18, 17, 12, -22, 'a')}{dish(46, 22, 10, 18, 'b')}</g> : dish(32, 17, 15, -14, 'a')}
      <Slab x={14} y={42} w={36} h={14} k={2} c="dark" o={1} />
      <Seams x={14} y={42} w={36} h={14} cols={2} op={0.35} />
      <Vent x={17} y={45} w={12} h={8} n={3} />
      {variant === 'node' && <g><Slab x={31} y={44.6} w={16} h={9} k={1.2} c="red" o={0.6} /><Glyph k="chain" x={39} y={49} s={3.6} c={S.white} /></g>}
      {hub && <g><rect x="32" y="45" width="16" height="8" fill={ART.screen.cyan} /><text x="40" y="49.2" textAnchor="middle" fontSize="2.4" fill={S.cyanHi} {...PT}>SRC L1900</text><text x="40" y="52.2" textAnchor="middle" fontSize="2.1" fill={S.cyan} {...PT}>BURST TX</text></g>}
      <Lights x={17} y={44} n={5} gap={2.4} r={0.6} seed={variant === 'hub' ? 3 : 6} />
      {hub && <g fill="none" stroke={S.cyanHi} strokeWidth="0.7" style={{ animation:'holo-flicker 1.8s ease-in-out infinite' }}><polyline points={pts([[20, 4], [26, 1], [32, 4], [38, 1], [44, 4]])} /><polyline points={pts([[14, 8], [20, 5]])} /></g>}
      {variant === 'node' && <g stroke={S.red} strokeWidth="0.7" fill="none" style={{ animation:'holo-flicker 1.4s ease-in-out infinite' }}><path d="M44 8 C48 10 48 16 44 18" /><path d="M48 5 C54 9 54 17 48 21" /></g>}
      <Rivets x={16} y={55} w={32} n={7} />
      <Grime x={14} y={42} w={36} h={14} seed={4} n={4} op={0.22} /><Scuff x={14} y={42} w={36} h={14} seed={9} n={6} c="dark" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// SERVER STACK: a tall rack of blades. Black market, Republic archive, or Senate index node.
// ---------------------------------------------------------------------------
function PropServerStack({ variant }) {
  const S = ART.signal;
  const bm = variant === 'black_market', rep = variant === 'republic';
  const body = bm ? 'dark' : rep ? 'hull' : 'pearl';
  const lamp = bm ? ['red', 'green'] : rep ? ['cyan'] : ['cyan', 'amber'];
  const label = bm ? 'SLICER LOCK' : rep ? 'ARCHIVE CORE' : 'INDEX NODE';
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={90} rx={26} ry={4} />
      <Slab x={6} y={86} w={52} h={6} k={1.4} c="steel" />
      <Bev t="polygon" points={pts([[8, 6], [56, 6], [58, 9], [58, 86], [6, 86], [6, 9]])} c={body} o={1} />
      {Array.from({ length: 6 }, (_, i) => {
        const y = 12 + i * 12.4;
        return (
          <g key={i}>
            <Slab x={11} y={y} w={42} h={10} k={1} c="dark" o={0.6} />
            <Vent x={13} y={y + 2} w={16} h={6} n={3} />
            <Lights x={33} y={y + 3} n={4} gap={3.2} r={0.8} colors={lamp} seed={i + (bm ? 3 : 8)} />
            <rect x="33" y={y + 5.6} width={10 + ((i * 7) % 9)} height="1.4" fill={S[lamp[0]]} opacity="0.55" />
            <circle cx="49" cy={y + 5} r="1.2" fill={ART.steel.hi} />
          </g>
        );
      })}
      <Slab x={12} y={2} w={40} h={6} k={1.2} c={body} o={0.8} />
      <text x="32" y="6.6" textAnchor="middle" fontSize="3" fill={bm ? S.crimson : rep ? S.cyanHi : ART.ink} fontWeight="700" {...PT}>{label}</text>
      {rep && <g><rect x="22" y="36" width="20" height="9" fill={ART.ink} /><rect x="24" y="38" width="16" height="5" fill={S.cyan} style={{ animation:'ship-blink 2s ease-in-out infinite' }} /><Cable d="M32 45 C26 56 40 62 30 74" w={2.6} /><polygon points={pts([[27, 73], [33, 73], [32, 78], [28, 78]])} fill={ART.brass.base} /></g>}
      {bm && <g><Cable d="M58 20 C66 34 52 46 60 70" w={2.4} /><Cable d="M6 30 C-2 44 10 54 4 72" w={2} /><polygon points={pts([[40, 56], [52, 54], [52, 62], [40, 63]])} fill={ART.note.pink} /><Glyph k="eye" x={46} y={58.6} s={3} c={ART.note.ink} /></g>}
      {variant === 'archive' && <g><Glyph k="cog" x={49} y={5.2} s={2.4} c={S.cyanHi} /><Cable d="M58 24 C64 38 54 50 60 66" w={2} /></g>}
      <Rivets x={10} y={84} w={44} n={8} /><Seams x={6} y={6} w={52} h={80} cols={1} op={0.2} />
      <Grime x={6} y={8} w={52} h={78} seed={31} n={6} op={0.2} /><Scuff x={8} y={10} w={48} h={74} seed={32} n={9} c={body} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// BOARD: wall mounted holo boards, ledgers and posters. One frame, many contents.
// ---------------------------------------------------------------------------
const BOARD_LIST = {
  baylog:     { scr:'cyan',  t:'SPEEDER BAY LOG',        r:['LAST ENTRY: UNIT 7 OUT 03:14','DESTINATION: CLASSIFIED','PILOT: CLASSIFIED','STATUS: NO RETURN LOGGED'], hl:3 },
  departures: { scr:'amber', t:'DEPARTURE BOARD',        r:['C-4   ON TIME','C-5   BOARDING','C-7   OVERDUE','LAST CONTACT 06:22: 3 DAYS'], hl:2, red:true },
  bulletin:   { scr:'cyan',  t:'CSF BULLETIN BOARD',     r:['CASE 1  CLASSIFIED','CASE 2  CLASSIFIED','CARGO ANOMALY / BAY 14','STATUS: CLOSED  FILED: VANE T'], hl:3, red:true },
  warrants:   { scr:'red',   t:'SECTOR 4 WARRANTS',      r:['OPEN: 14   BLACK SUN IDS: 9','CLASSIFIED: 4 (SENATE ONLY)','15: BAY 14 STRIKE TEAM','STATUS: SUSPENDED'], hl:3 },
  patrol:     { scr:'cyan',  t:'CSF PATROL ROUTES',      r:['SECTOR 4: SUSPENDED','PROMENADE: 1 OFFICER ROTA','CUT: 2 DAYS POST BAY 14','SIGNED: VANE T'], hl:3 },
  manifest:   { scr:'amber', t:'MAG-RAIL FREIGHT CARS',  r:['14-B  AGRICULTURAL  1.2 T','14-B  CLEARED IND  3.8 T','WEIGHT MISMATCH: 11 DAYS','FLAG: OPEN'], hl:2 },
  prisoners:  { scr:'red',   t:'PRISONER TRANSFER LOG',  r:['DETAINEES: 4','2 TO LEVEL 001: NO REASON','1 RELEASED: SENATE AUTH','ACTIVE: RIANNA T  SUB-3 B'], hl:3 },
  cellog:     { scr:'cyan',  t:'CELL BLOCK LOG',         r:['ENTRY 847','GREY COAT  NO ID CHIP','OFFSITE: DIRECTIVE 1182-C','AUTH: [REDACTED]'], hl:3 },
  // MFG BOARD BEGIN
  mfg_quota:  { scr:'amber', t:'SECTOR 4 QUOTA',         r:['DAY SHIFT: 212 ON ROLL','QUOTA: 114 PCT','NIGHT BADGES: 340','ROLL AFTER HORN: 0'], hl:3, red:true },
  mfg_lift:   { scr:'cyan',  t:'CARGO LIFT LOG',         r:['LIFT 2: NIGHT CYCLES','02:10  PALLET SEALED','03:40  PALLET SEALED','STOP: B  (NO FLOOR)'], hl:3 },
  mfg_shift:  { scr:'amber', t:'PRODUCTION SCHEDULE',    r:['LINE 1  CLASS C PLATE','LINE 2  CLASS C PLATE','CRADLE 1  SECTION 11','CRADLE 2  SECTION 12'], hl:2, red:true },
  // MFG BOARD END
};

function PropBoard({ variant }) {
  const S = ART.signal;
  const L = BOARD_LIST[variant];
  const frame = (
    <g>
      <PropShadow cx={48} cy={60} rx={42} ry={3} />
      <Slab x={2} y={2} w={92} h={56} k={3} c="dark" o={1} />
      <Rivets x={6} y={5} w={84} n={10} /><Rivets x={6} y={55} w={84} n={10} />
      <Slab x={30} y={57} w={36} h={4} k={1} c="steel" o={0.6} />
    </g>
  );
  let body = null;
  if (L) {
    const s = SCR[L.scr];
    body = (
      <g>
        <Screen x={7} y={7} w={82} h={46} s={s} title={L.t} rows={L.r} hl={L.hl} fs={3.6} scan={false} flick />
        {L.red && <rect x="8" y={7 + 3.6 * 2 + 2.8 + L.hl * 3.6 * 1.8 - 3.8} width="80" height="5.4" fill={S.red} opacity="0.28" />}
        <rect x="7" y="7" width="82" height="1.5" fill={s.ink} opacity="0.35" />
        <Toggles x={7} y={53.6} cols={6} rows={1} gap={3.2} seed={4} />
      </g>
    );
  } else if (variant === 'wanted') {
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.screen.red} />
        <rect x="7" y="7" width="82" height="9" fill={S.red} opacity="0.85" />
        <text x="48" y="14.6" textAnchor="middle" fontSize="7.4" fill={ART.pearl.hi} fontWeight="700" {...PT}>WANTED</text>
        <g style={{ animation:'holo-flicker 2.4s ease-in-out infinite' }}>
          <polygon points={pts(ngon(31, 29, 10, 11.5, 8))} fill={S.orange} opacity="0.85" />
          <polygon points={pts([[14, 52], [16, 41], [24, 38], [38, 38], [46, 41], [48, 52]])} fill={S.orange} opacity="0.7" />
          <rect x="24" y="26" width="5" height="1.8" fill={ART.screen.red} /><rect x="33" y="26" width="5" height="1.8" fill={ART.screen.red} />
          <polygon points={pts([[28, 35], [34, 35], [31, 33]])} fill={ART.screen.red} />
          {[20, 26, 32, 38, 44, 50].map((y) => <line key={y} x1="8" y1={y} x2="50" y2={y} stroke={S.redHi} strokeWidth="0.35" opacity="0.35" />)}
        </g>
        <text x="54" y="26" fontSize="3.4" fill={S.redHi} fontWeight="700" {...PT}>REPUBLIC BOUNTY</text>
        <text x="54" y="32" fontSize="3" fill={S.redHi} {...PT}>ALIVE PREFERRED</text>
        <text x="54" y="42" fontSize="3.6" fill={S.amberHi} fontWeight="700" {...PT}>10,000 CR</text>
        <text x="54" y="48" fontSize="2.6" fill={S.redHi} opacity="0.8" {...PT}>FACE ON FILE: ???</text>
      </g>
    );
  } else if (variant === 'analysis') {
    const photo = (x, y, k) => (
      <g key={k}>
        <polygon points={pts([[x, y], [x + 15, y], [x + 15, y + 17], [x, y + 17]])} fill={ART.paper.base} />
        <polygon points={pts([[x + 1.5, y + 1.5], [x + 13.5, y + 1.5], [x + 13.5, y + 12], [x + 1.5, y + 12]])} fill={ART.dark.base} />
        <polygon points={pts(ngon(x + 7.5, y + 6, 2.6, 3, 6))} fill={ART.dark.hi} /><polygon points={pts([[x + 3, y + 12], [x + 4, y + 9], [x + 11, y + 9], [x + 12, y + 12]])} fill={ART.dark.hi} />
        <text x={x + 7.5} y={y + 15.6} textAnchor="middle" fontSize="2.4" fill={ART.note.ink} fontWeight="700" {...PT}>UNKNOWN</text>
      </g>
    );
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.screen.cyan} />
        <text x="10" y="12.4" fontSize="3.2" fill={S.cyanHi} fontWeight="700" {...PT}>BAY 14 ANALYSIS</text>
        {photo(10, 16, 'a')}{photo(28, 22, 'b')}{photo(46, 16, 'c')}
        <g stroke={S.crimson} strokeWidth="0.6" fill="none"><polyline points={pts([[17, 24], [35, 30], [53, 24]])} /><polyline points={pts([[35, 30], [62, 44]])} /></g>
        <g transform="translate(74 15)"><circle r="1.4" fill={S.amber} />{Array.from({ length: 12 }, (_, i) => { const a = (i / 12) * Math.PI * 2; return <line key={i} x1={Math.cos(a) * 2.6} y1={Math.sin(a) * 2.6} x2={Math.cos(a) * (6 + (i % 3) * 2)} y2={Math.sin(a) * (6 + (i % 3) * 2)} stroke={S.amber} strokeWidth="0.6" />; })}</g>
        <text x="64" y="29" fontSize="2.3" fill={S.amber} {...PT}>BLAST PATTERN</text>
        {[0, 1, 2].map((i) => <line key={i} x1="64" y1={32 + i * 3} x2={84 - i * 4} y2={32 + i * 3} stroke={S.cyan} strokeWidth="0.6" opacity="0.7" />)}
        <g transform="rotate(-4 70 48)"><polygon points={pts([[56, 41], [86, 41], [86, 52], [56, 52]])} fill={ART.note.yellow} /><text x="58" y="45.6" fontSize="2.3" fill={ART.note.blueInk} {...PT}>Transit codes -- mine.</text><text x="58" y="49.6" fontSize="2.6" fill={ART.note.blueInk} fontWeight="700" {...PT}>How?</text></g>
        <Lights x={10} y={49.4} n={8} gap={3} seed={7} />
      </g>
    );
  } else if (variant === 'faction') {
    const tower = (x, h, c) => <rect key={x} x={x} y={52 - h} width="5" height={h} fill={c} />;
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.screen.cyan} />
        <text x="10" y="12.4" fontSize="3.2" fill={S.cyanHi} fontWeight="700" {...PT}>CORUSCANT LEVELS</text>
        {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13].map((i) => tower(10 + i * 5.4, 10 + ((i * 7) % 5) * 5 + (i % 3) * 3, ART.dark.hi))}
        <polygon points={pts([[8, 52], [8, 30], [34, 30], [40, 52]])} fill={S.blue} opacity="0.4" />
        <polygon points={pts([[36, 52], [44, 28], [68, 28], [62, 52]])} fill={S.red} opacity="0.4" />
        <polygon points={pts([[52, 46], [56, 38], [88, 36], [88, 46]])} fill={S.amber} opacity="0.45" />
        <g fill="none" stroke={S.violet} strokeWidth="0.8"><ellipse cx="74" cy="22" rx="10" ry="5.4" /><ellipse cx="74" cy="22" rx="11.6" ry="6.4" /><ellipse cx="74" cy="22" rx="13.2" ry="7.4" /></g>
        <polygon points={pts([[68, 20], [80, 20], [74, 14]])} fill={ART.pearl.base} />
        <g fontSize="2.3" {...PT}><text x="10" y="50" fill={S.blue}>CSF</text><text x="38" y="50" fill={S.redHi}>SYNDICATE</text><text x="62" y="50" fill={S.amberHi}>FREIGHT</text></g>
      </g>
    );
  } else if (variant === 'betting') {
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.screen.off} />
        <text x="48" y="14" textAnchor="middle" fontSize="4.4" fill={S.gold} fontWeight="700" {...PT}>TONIGHT'S CARD</text>
        {['BOUT 1   ROZEK  2:1', 'BOUT 2   TARN   5:2', 'BOUT 3   VESH   4:1', 'MAIN: MALAK VS TBD'].map((t, i) => <text key={i} x="12" y={22 + i * 6.4} fontSize="3.2" fill={i === 3 ? S.amberHi : ART.beige.hi} fontWeight={i === 3 ? '700' : '400'} {...PT}>{t}</text>)}
        <text x="12" y="47.6" fontSize="3.6" fill={S.crimson} fontWeight="700" {...PT}>MALAK  3 TO 1</text>
        <g stroke={S.crimson} strokeWidth="0.9"><line x1="12" y1="49" x2="52" y2="49" /><line x1="12" y1="51" x2="52" y2="51" /></g>
        <text x="60" y="28" fontSize="2.6" fill={ART.beige.base} {...PT}>12 BOUTS</text><text x="60" y="33" fontSize="2.6" fill={ART.beige.base} {...PT}>4 FAVOURITES</text>
        <polygon points={pts([[66, 40], [84, 40], [84, 50], [66, 50]])} fill={ART.screen.red} /><text x="75" y="46.6" textAnchor="middle" fontSize="3" fill={S.redHi} fontWeight="700" {...PT}>TBD  ???</text>
        <Lights x={10} y={9.4} n={6} gap={3} seed={11} />
      </g>
    );
  } else if (variant === 'register') {
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.wood.base} />
        <polygon points={pts([[10, 10], [86, 10], [86, 50], [10, 50]])} fill={ART.paper.base} />
        <text x="48" y="16" textAnchor="middle" fontSize="3.8" fill={ART.note.ink} fontWeight="700" {...PT}>CHALLENGER REGISTER</text>
        {Array.from({ length: 7 }, (_, i) => {
          const y = 22 + i * 4.2, r = rng(i + 40), w = 18 + r() * 24;
          return <g key={i}><polyline points={pts([[14, y], [14 + w * 0.25, y - 1.2], [14 + w * 0.5, y + 0.6], [14 + w * 0.75, y - 1], [14 + w, y]])} fill="none" stroke={ART.note.blueInk} strokeWidth="0.7" /><line x1="14" y1={y + 1.2} x2="84" y2={y + 1.2} stroke={ART.paper.shade} strokeWidth="0.3" />{i === 4 && <g stroke={ART.note.redInk} strokeWidth="0.9"><line x1="13" y1={y - 2} x2={14 + w} y2={y + 1.6} /><line x1="13" y1={y + 1.6} x2={14 + w} y2={y - 2} /></g>}</g>;
        })}
        <polygon points={pts([[44, 5], [52, 5], [50, 10], [46, 10]])} fill={ART.steel.base} />
      </g>
    );
  } else if (variant === 'ops') {
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.screen.cyan} />
        <text x="10" y="12.4" fontSize="3.2" fill={S.cyanHi} fontWeight="700" {...PT}>OPS PLANNING</text>
        {[0, 1, 2].map((i) => <g key={i}><polygon points={pts([[12 + i * 4, 40 - i * 11], [78 - i * 4, 40 - i * 11], [84 - i * 4, 46 - i * 11], [6 + i * 4, 46 - i * 11]])} fill={ART.dark.base} opacity="0.9" /><polyline points={pts([[12 + i * 4, 40 - i * 11], [78 - i * 4, 40 - i * 11], [84 - i * 4, 46 - i * 11], [6 + i * 4, 46 - i * 11], [12 + i * 4, 40 - i * 11]])} fill="none" stroke={S.cyan} strokeWidth="0.5" /></g>)}
        <ellipse cx="38" cy="37" rx="16" ry="5" fill={S.crimson} opacity="0.5" /><ellipse cx="54" cy="36" rx="16" ry="5" fill={S.blue} opacity="0.5" />
        <g stroke={S.amberHi} strokeWidth="0.6" fill="none"><polyline points={pts([[30, 38], [48, 16], [64, 34]])} /><polyline points={pts([[48, 16], [44, 28]])} /></g>
        <text x="14" y="50.4" fontSize="2.4" fill={S.crimson} {...PT}>MARLO</text><text x="62" y="50.4" fontSize="2.4" fill={S.blue} {...PT}>ROOK</text>
      </g>
    );
  } else if (variant === 'resistance') {
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.paper.base} />
        <rect x="7" y="7" width="82" height="46" fill={ART.paper.shade} opacity="0.35" />
        <text x="48" y="14" textAnchor="middle" fontSize="3.6" fill={ART.note.ink} fontWeight="700" {...PT}>CAVERN PLAN: NOT TANNER'S</text>
        <g fill="none" stroke={ART.note.blueInk} strokeWidth="0.9"><polyline points={pts([[12, 44], [20, 32], [32, 36], [40, 22], [54, 28], [62, 18]])} /><polyline points={pts([[32, 36], [38, 46], [56, 44]])} /><polyline points={pts([[54, 28], [70, 34], [82, 26]])} /></g>
        {[[20, 32], [40, 22], [62, 18], [56, 44], [82, 26]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1.8" fill={ART.note.redInk} />)}
        <polygon points={pts([[60, 36], [86, 36], [86, 51], [60, 51]])} fill={ART.screen.cyan} opacity="0.92" /><text x="62" y="41" fontSize="2.4" fill={S.cyanHi} fontWeight="700" {...PT}>STOLEN KDY GRID</text>
        {[0, 1, 2, 3].map((i) => <line key={i} x1="62" y1={43.4 + i * 2} x2={62 + 14 + (i % 2) * 6} y2={43.4 + i * 2} stroke={S.cyan} strokeWidth="0.6" />)}
        <g transform="rotate(-5 22 46)"><polygon points={pts([[10, 40], [34, 40], [34, 51], [10, 51]])} fill={ART.note.yellow} /><text x="12" y="45" fontSize="2.3" fill={ART.note.ink} {...PT}>PLANNED LONGER</text><text x="12" y="49" fontSize="2.3" fill={ART.note.redInk} fontWeight="700" {...PT}>THAN HE SAID</text></g>
        <polygon points={pts([[44, 5], [52, 5], [50, 10], [46, 10]])} fill={ART.steel.base} />
      </g>
    );
  } else if (variant === 'clipboard') {
    body = (
      <g>
        <rect x="7" y="7" width="82" height="46" fill={ART.dark.base} />
        <Bev t="polygon" points={pts([[24, 7], [72, 7], [74, 9], [74, 53], [22, 53], [22, 9]])} c="wood" o={1} />
        <polygon points={pts([[27, 12], [69, 12], [69, 51], [27, 51]])} fill={ART.paper.hi} />
        <polygon points={pts([[28, 15], [70, 13], [70, 52], [28, 52]])} fill={ART.paper.base} />
        <Slab x={38} y={4} w={20} h={7} k={1.4} c="steel" o={0.8} />
        <text x="48" y="22" textAnchor="middle" fontSize="3" fill={ART.note.ink} fontWeight="700" {...PT}>WORK ORDER: BAY B</text>
        {[28, 33, 38, 43].map((y, i) => <line key={y} x1="31" y1={y} x2={66 - i * 4} y2={y} stroke={ART.note.blueInk} strokeWidth="0.5" />)}
        <polygon points={pts([[48, 38], [68, 38], [68, 50], [48, 50]])} fill={ART.pearl.base} />
        <circle cx="58" cy="44" r="4.4" fill={ART.red.base} /><text x="58" y="45.2" textAnchor="middle" fontSize="2.6" fill={S.white} fontWeight="700" {...PT}>SEN</text>
        <polygon points={pts([[32, 40], [44, 40], [44, 49], [32, 49]])} fill={ART.paper.shade} /><text x="38" y="46" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} {...PT}>SIGNED</text>
      </g>
    );
  }
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      {frame}{body}
      <Seams x={2} y={2} w={92} h={56} cols={3} op={0.15} />
      <Grime x={4} y={46} w={88} h={12} seed={(variant || 'b').length + 70} n={5} op={0.22} /><Scuff x={4} y={50} w={88} h={7} seed={(variant || 'b').length + 71} n={6} c="dark" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// SIGN ARRAY: hung neon vendor signs, or the hand painted cantina plate over an open door.
// ---------------------------------------------------------------------------
function PropSignArray({ variant }) {
  const S = ART.signal;
  const neon = (x, y, w, h, c, l1, l2, on = true, k) => (
    <g key={k}>
      <Slab x={x} y={y} w={w} h={h} k={1.4} c="dark" o={0.6} />
      <Glow cx={x + w / 2} cy={y + h / 2} rx={w * 0.52} ry={h * 0.8} c={c} opacity={on ? 0.45 : 0.14} />
      <rect x={x + 1.4} y={y + 1.4} width={w - 2.8} height={h - 2.8} fill="none" stroke={c} strokeWidth="1" opacity={on ? 1 : 0.35} />
      <text x={x + w / 2} y={y + (l2 ? h / 2 - 0.4 : h / 2 + 1)} textAnchor="middle" fontSize="2.6" fill={on ? S.white : c} opacity={on ? 1 : 0.4} fontWeight="700" {...PT}>{l1}</text>
      {l2 && <text x={x + w / 2} y={y + h / 2 + 3.6} textAnchor="middle" fontSize="2.6" fill={on ? S.white : c} opacity={on ? 1 : 0.4} fontWeight="700" {...PT}>{l2}</text>}
      <line x1={x + 6} y1={y} x2={x + 6} y2={y - 3} stroke={ART.steel.base} strokeWidth="0.8" /><line x1={x + w - 6} y1={y} x2={x + w - 6} y2={y - 3} stroke={ART.steel.base} strokeWidth="0.8" />
    </g>
  );
  if (variant === 'exhaust') {
    return (
      <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
        <PropShadow cx={64} cy={61} rx={54} ry={2.6} />
        <Bev t="polygon" points={pts([[6, 4], [122, 4], [124, 6], [124, 36], [122, 38], [6, 38], [4, 36], [4, 6]])} c="rust" o={1} />
        <Seams x={4} y={4} w={120} h={34} cols={3} op={0.3} />
        <text x="72" y="21" textAnchor="middle" fontSize="12" fill={ART.amber.hi} fontWeight="700" {...PT} opacity="0.88">THE EXHAUST</text>
        <text x="72" y="31.4" textAnchor="middle" fontSize="3" fill={ART.beige.hi} {...PT}>Est. after the last time this level flooded</text>
        <Bev t="polygon" points={pts([[10, 12], [22, 12], [24, 14], [24, 28], [10, 28]])} c="steel" o={0.6} /><Glyph k="drop" x={17} y={20} s={4} c={S.orange} />
        
        <Bev t="polygon" points={pts([[40, 62], [40, 42], [44, 38], [84, 38], [88, 42], [88, 62]])} c="dark" o={0.8} />
        <polygon points={pts([[44, 62], [44, 44], [47, 41], [81, 41], [84, 44], [84, 62]])} fill={ART.amber.base} opacity="0.75" /><polygon points={pts([[50, 62], [50, 46], [78, 46], [78, 62]])} fill={ART.amber.hi} opacity="0.8" />
        <Glow cx={64} cy={56} rx={26} ry={6} c={S.amber} opacity={0.5} />
        <Rivets x={8} y={36} w={112} n={14} /><Grime x={4} y={4} w={120} h={34} seed={5} n={6} op={0.22} /><Scuff x={6} y={6} w={116} h={30} seed={9} n={8} c="rust" />
        <Lights x={96} y={14} n={4} gap={4} seed={3} />
      </svg>
    );
  }
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.4} />
      <Bev t="rect" x="4" y="3" width="120" height="2.6" c="steel" o={0.7} /><Bev t="rect" x="4" y="33" width="120" height="2.6" c="steel" o={0.7} />
      {neon(6, 8, 38, 14, S.blue, 'EXCHANGE', 'CERTIFIED', true, 'a')}
      {neon(47, 8, 38, 14, S.crimson, 'PHRIK-FREE', 'MATERIALS', true, 'b')}
      {neon(88, 8, 34, 14, S.blue, 'SENATE-', 'APPROVED TRANSIT', true, 'c')}
      {neon(6, 38, 40, 14, S.crimson, 'BEST RATES', 'BETWEEN THE LEVELS', true, 'd')}
      {neon(50, 38, 40, 14, S.blue, 'TRANSIT', 'CLEARANCE', true, 'e')}
      {neon(94, 38, 28, 14, S.crimson, 'SCYLLA', 'FREIGHT', false, 'f')}
      {[10, 52, 94].map((x) => <g key={x} fill="none" stroke={S.white} strokeWidth="0.5" opacity="0.4"><polyline points={pts([[x, 26], [x + 3, 24], [x + 6, 27], [x + 9, 25]])} /></g>)}
      <Cable d="M122 20 C128 26 122 34 126 42" w={1.8} />
      <Seams x={4} y={3} w={120} h={50} cols={2} op={0.12} /><Rivets x={8} y={4.4} w={112} n={14} />
      <Grime x={4} y={8} w={120} h={46} seed={12} n={5} op={0.18} /><Scuff x={6} y={10} w={116} h={42} seed={13} n={7} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// SHOPFRONT: a street level facade with an awning, a window, an open door and a sign.
// ---------------------------------------------------------------------------
function PropShopfront({ variant }) {
  const S = ART.signal;
  const sally = variant === 'sallys';
  const awn = sally ? [ART.red.base, ART.pearl.base] : [ART.teal.base, ART.tan.base];
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.6} />
      <Bev t="polygon" points={pts([[4, 22], [124, 22], [124, 60], [4, 60]])} c={sally ? 'bronze' : 'concrete'} o={1} />
      <Seams x={4} y={22} w={120} h={38} cols={5} op={0.25} />
      {/* awning stripes */}
      <polygon points={pts([[2, 22], [126, 22], [118, 12], [10, 12]])} fill={awn[1]} />
      {Array.from({ length: 12 }, (_, i) => <polygon key={i} points={pts([[2 + i * 10.33, 22], [2 + i * 10.33 + 5.16, 22], [10 + i * 9.0 + 4.5, 12], [10 + i * 9.0, 12]])} fill={awn[0]} opacity={i % 2 ? 0 : 1} />)}
      <rect x="2" y="21" width="124" height="2.4" fill={ART.dark.shade} />
      {/* sign */}
      {sally ? (
        <g>
          <Slab x={32} y={1} w={64} h={11} k={1.6} c="dark" o={0.6} />
          <Glow cx={64} cy={6.5} rx={34} ry={8} c={S.magenta} opacity={0.5} />
          <text x="64" y="10.4" textAnchor="middle" fontSize="9" fill={S.white} fontWeight="700" {...PT} style={{ animation:'holo-flicker 1.4s ease-in-out infinite' }}>SALLYS</text>
          <rect x="34" y="2.6" width="60" height="7.8" fill="none" stroke={S.magenta} strokeWidth="1" />
        </g>
      ) : (
        <g>
          <Slab x={14} y={1} w={100} h={10} k={1.4} c="wood" o={0.8} />
          <text x="64" y="8.6" textAnchor="middle" fontSize="5" fill={ART.paper.hi} fontWeight="700" {...PT}>GOODS, TRADES AND TREASURE</text>
        </g>
      )}
      {/* open door with warm light */}
      <Bev t="polygon" points={pts([[52, 60], [52, 28], [56, 25], [72, 25], [76, 28], [76, 60]])} c="dark" o={0.8} />
      <polygon points={pts([[55, 60], [55, 30], [58, 28], [70, 28], [73, 30], [73, 60]])} fill={sally ? ART.amber.hi : ART.beige.base} opacity="0.85" />
      <polygon points={pts([[55, 60], [55, 30], [58, 28], [62, 29], [62, 60]])} fill={ART.dark.base} opacity="0.7" />
      {/* window */}
      <Bev t="polygon" points={pts([[8, 28], [46, 28], [46, 52], [8, 52]])} c="dark" o={0.8} />
      <rect x="10" y="30" width="34" height="20" fill={sally ? ART.amber.shade : ART.screen.amber} />
      {sally && <g>
        {[[14, 'cyan'], [20, 'amber'], [26, 'red'], [32, 'green'], [38, 'amber']].map(([x, k], i) => <g key={i}><rect x={x} y="36" width="3.4" height="9" fill={S[k]} opacity="0.9" /><rect x={x + 0.9} y="33.4" width="1.6" height="3" fill={S[k]} opacity="0.9" /></g>)}
        <rect x="10" y="45" width="34" height="2" fill={ART.wood.base} />
      </g>}
      {!sally && <g>
        {[[12, 40, 8, 10, 'amber'], [22, 36, 6, 14, 'brass'], [30, 42, 10, 8, 'copper'], [14, 32, 5, 5, 'teal'], [34, 33, 6, 8, 'rust']].map(([x, y, w, h, c], i) => <Slab key={i} x={x} y={y} w={w} h={h} k={1} c={c} o={0.6} />)}
        {[[16, 41], [26, 38], [35, 44]].map(([x, y], i) => <g key={i}><polygon points={pts([[x, y], [x + 8, y], [x + 8, y + 3.4], [x, y + 3.4]])} fill={ART.red.base} /><text x={x + 4} y={y + 2.6} textAnchor="middle" fontSize="2.2" fill={S.white} fontWeight="700" {...PT}>2X</text></g>)}
      </g>}
      {/* right window */}
      <Bev t="polygon" points={pts([[82, 28], [120, 28], [120, 52], [82, 52]])} c="dark" o={0.8} />
      <rect x="84" y="30" width="34" height="20" fill={sally ? ART.amber.shade : ART.screen.amber} />
      {sally ? <g><Glyph k="coin" x={101} y={40} s={5} c={S.gold} /><text x="101" y="49" textAnchor="middle" fontSize="2.6" fill={S.amberHi} {...PT}>OPEN LATE</text></g> : <g>{[[86, 38, 12, 12, 'steel'], [100, 34, 8, 16, 'bronze'], [110, 40, 8, 10, 'copper']].map(([x, y, w, h, c], i) => <Slab key={i} x={x} y={y} w={w} h={h} k={1} c={c} o={0.6} />)}</g>}
      <Rivets x={6} y={58} w={116} n={14} />
      <Grime x={4} y={24} w={120} h={36} seed={sally ? 7 : 17} n={6} op={0.2} /><Scuff x={6} y={26} w={116} h={32} seed={sally ? 8 : 18} n={8} c="concrete" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// APARTMENT DOOR: a numbered door in a frame. Three residents, three states.
// ---------------------------------------------------------------------------
function PropAptDoor({ variant }) {
  const S = ART.signal;
  const jon = variant === 'jon', dex = variant === 'dexter', trex = variant === 'trex';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={2.8} />
      <Bev t="polygon" points={pts([[8, 60], [8, 8], [12, 4], [52, 4], [56, 8], [56, 60]])} c={trex ? 'dark' : 'concrete'} o={1} />
      <Seams x={8} y={4} w={48} h={56} cols={1} op={0.25} />
      <Bev t="polygon" points={pts([[17, 60], [17, 14], [21, 10], [43, 10], [47, 14], [47, 60]])} c={trex ? 'steel' : 'steel'} o={0.9} />
      <polygon points={pts([[21, 60], [21, 16], [24, 13], [40, 13], [43, 16], [43, 60]])} fill={trex ? ART.steel.base : ART.steel.base} />
      {trex && <g><Seams x={21} y={13} w={22} h={47} rows={3} cols={1} op={0.5} /><Rivets x={23} y={16} w={18} n={4} c="steel" /><Rivets x={23} y={57} w={18} n={4} c="steel" /><Hazard x={21} y={52} w={22} h={4} /></g>}
      {jon && <g>
        <polygon points={pts([[36, 60], [36, 16], [40, 13], [43, 16], [43, 60]])} fill={ART.amber.hi} opacity="0.9" /><polygon points={pts([[40, 60], [40, 15], [43, 16], [43, 60]])} fill={ART.amber.hi} />
        <polygon points={pts([[21, 60], [21, 16], [24, 13], [34, 13], [34, 60]])} fill={ART.steel.base} /><Seams x={21} y={13} w={13} h={47} rows={2} op={0.4} />
        <path d="M14 8 C10 14 16 18 12 26" fill="none" stroke={ART.dark.shade} strokeWidth="1.4" /><circle cx="12.4" cy="25" r="1.2" fill={S.amberHi} style={{ animation:'ship-spark 1.6s steps(2) infinite' }} />
        <polygon points={pts([[14, 4], [22, 4], [20, 10], [14, 10]])} fill={ART.ink} opacity="0.5" />
      </g>}
      {dex && <g>
        <rect x="26" y="20" width="12" height="5" fill={ART.amber.hi} opacity="0.85" /><rect x="26" y="20" width="12" height="5" fill="none" stroke={ART.dark.shade} strokeWidth="0.8" />
        <g transform="rotate(-4 32 34)"><polygon points={pts([[24, 28], [41, 28], [41, 40], [24, 40]])} fill={ART.note.yellow} /><text x="32.5" y="33" textAnchor="middle" fontSize="3" fill={ART.note.ink} fontWeight="700" {...PT}>BACK IN</text><text x="32.5" y="37.6" textAnchor="middle" fontSize="3.6" fill={ART.note.redInk} fontWeight="700" {...PT}>20</text></g>
        <polygon points={pts([[22, 46], [30, 44], [34, 52], [24, 54]])} fill={ART.ink} opacity="0.25" />
        <polygon points={pts([[44, 3], [56, 3], [56, 9]])} fill={ART.pearl.base} opacity="0.35" />
      </g>}
      {trex && <g>
        <Slab x={44} y={30} w={11} h={17} k={1.2} c="dark" o={0.6} />
        {Array.from({ length: 9 }, (_, i) => { const kx = 45.4 + (i % 3) * 3.2, ky = 32 + Math.floor(i / 3) * 3.6, worn = i === 1 || i === 4 || i === 7; return <rect key={i} x={kx} y={ky} width="2.4" height="2.6" fill={worn ? ART.steel.hi : ART.dark.hi} />; })}
        <rect x="45.4" y="43" width="8.4" height="2" fill={ART.screen.red} /><circle cx="49.6" cy="44" r="0.7" fill={S.red} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} />
        <text x="37" y="8.4" textAnchor="middle" fontSize="2.6" fill={S.redHi} {...PT}>7 DIGIT LOCK</text>
      </g>}
      {!trex && <g><Slab x={26} y={5} w={12} h={4.4} k={0.8} c="brass" o={0.5} /><text x="32" y="8.4" textAnchor="middle" fontSize="2.8" fill={ART.ink} fontWeight="700" {...PT}>{jon ? 'APT 4' : 'APT 9'}</text></g>}
      <Slab x={37} y={34} w={4} h={8} k={0.8} c="brass" o={0.5} />
      <Rivets x={11} y={6} w={42} n={6} /><Grime x={8} y={6} w={48} h={52} seed={jon ? 31 : dex ? 33 : 35} n={5} op={0.2} /><Scuff x={10} y={8} w={44} h={48} seed={jon ? 32 : dex ? 34 : 36} n={7} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// WALL MARKS: tags, stencils and stamps on a plated wall.
// ---------------------------------------------------------------------------
function PropWallMarks({ variant }) {
  const S = ART.signal;
  const plate = variant === 'stamp' ? 'steel' : 'concrete';
  const base = (
    <g>
      <PropShadow cx={48} cy={60} rx={42} ry={2.6} />
      <Bev t="polygon" points={pts([[3, 60], [3, 5], [8, 2], [88, 2], [93, 5], [93, 60]])} c={plate} o={1} />
      <Seams x={3} y={2} w={90} h={58} cols={2} rows={1} op={0.4} />
      <Rivets x={7} y={5} w={82} n={10} c="steel" /><Rivets x={7} y={57} w={82} n={10} c="steel" />
    </g>
  );
  let art = null;
  if (variant === 'free') {
    art = (
      <g>
        <polygon points={pts([[48, 6], [58, 18], [55, 24], [60, 30], [48, 38], [36, 30], [41, 24], [38, 18]])} fill={S.orange} />
        <polygon points={pts([[48, 14], [53, 22], [51, 28], [48, 33], [45, 28], [43, 22]])} fill={S.amberHi} />
        <g fill="none" stroke={ART.beige.hi} strokeWidth="1.8"><polygon points={pts(ngon(30, 48, 7, 4.6, 6))} /><polygon points={pts(ngon(42, 48, 7, 4.6, 6))} /><polygon points={pts(ngon(58, 49, 7, 4.6, 6))} /><polygon points={pts(ngon(70, 48, 7, 4.6, 6))} /></g>
        <polygon points={pts([[47, 44], [51, 44], [49, 52]])} fill={plate === 'concrete' ? ART.concrete.base : ART.steel.base} />
        <text x="48" y="58.6" textAnchor="middle" fontSize="2.6" fill={S.amberHi} {...PT}>FREE CORUSCANT</text>
      </g>
    );
  } else if (variant === 'tags' || variant === 'drain' || variant === 'alley') {
    const chain = (x, y, c, k) => <Glyph key={k} k="chain" x={x} y={y} s={6} c={c} />;
    art = (
      <g>
        {(variant === 'alley' ? [] : [[12, 14, 'blue', 18], [28, 10, 'crimson', 14], [8, 30, 'lime', 20], [40, 34, 'amber', 16]]).map(([x, y, k, w], i) => <g key={i} stroke={S[k]} strokeWidth="1.8" fill="none" opacity="0.85"><polyline points={pts([[x, y], [x + w * 0.3, y + 5], [x + w * 0.6, y - 2], [x + w, y + 4]])} /><line x1={x} y1={y + 8} x2={x + w * 0.8} y2={y + 8} /></g>)}
        {[[60, 14], [74, 24], [20, 44]].map(([x, y], i) => <g key={i}><polygon points={pts([[x - 6, y + 5], [x, y - 6], [x + 6, y + 5]])} fill="none" stroke={S[i === 1 ? 'crimson' : 'warn']} strokeWidth="1.4" /><line x1={x - 8} y1={y + 8} x2={x + 8} y2={y - 8} stroke={S.red} strokeWidth="1" /></g>)}
        {variant === 'drain' && <g fill="none" stroke={S.lime} strokeWidth="0.9" style={{ animation:'holo-flicker 3s ease-in-out infinite' }}><polyline points={pts([[10, 54], [24, 54], [24, 46], [44, 46], [44, 54], [62, 54]])} /><polyline points={pts([[44, 46], [44, 38], [70, 38], [70, 28], [86, 28]])} /><polyline points={pts([[62, 54], [62, 46], [86, 46]])} />{[[24, 46], [44, 38], [70, 28], [62, 46]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="1.6" fill={S.lime} />)}</g>}
        {variant === 'drain' && <g>{[[52, 10], [70, 12], [82, 18], [30, 24]].map(([x, y], i) => chain(x, y, S.crimson, i))}{[0, 1, 2, 3, 4].map((i) => <line key={i} x1={8 + i * 2.4} y1="6" x2={8 + i * 2.4} y2="12" stroke={ART.beige.hi} strokeWidth="0.8" />)}</g>}
        {variant === 'alley' && <g><text x="48" y="22" textAnchor="middle" fontSize="3.6" fill={S.limeHi || S.lime} fontWeight="700" {...PT}>THIS ALLEY BELONGS TO NO ONE</text><text x="48" y="50" textAnchor="middle" fontSize="2.8" fill={S.white} {...PT}>KEEP YOUR HANDS OFF THE HARDWARE</text><g stroke={S.lime} strokeWidth="1"><polygon points={pts(ngon(74, 36, 6, 6, 4, Math.PI / 4))} fill="none" /><line x1="68" y1="36" x2="80" y2="36" /></g></g>}
        {variant === 'tags' && <g>{chain(76, 44, S.crimson, 'c')}</g>}
      </g>
    );
  } else if (variant === 'blacksun') {
    art = (
      <g>
        <circle cx="48" cy="31" r="22" fill={ART.ink} opacity="0.55" />
        <circle cx="48" cy="31" r="22" fill="none" stroke={S.crimson} strokeWidth="1.8" />
        <circle cx="48" cy="31" r="8" fill={S.crimson} />
        {Array.from({ length: 12 }, (_, i) => { const a = (i / 12) * Math.PI * 2; return <polygon key={i} points={pts([[48 + Math.cos(a - 0.12) * 10, 31 + Math.sin(a - 0.12) * 10], [48 + Math.cos(a) * 20, 31 + Math.sin(a) * 20], [48 + Math.cos(a + 0.12) * 10, 31 + Math.sin(a + 0.12) * 10]])} fill={S.crimson} />; })}
        <circle cx="48" cy="31" r="3.4" fill={ART.ink} />
      </g>
    );
  } else {
    art = (
      <g>
        <polygon points={pts([[12, 16], [84, 16], [84, 46], [12, 46]])} fill={ART.steel.shade} />
        <Slab x={14} y={18} w={68} h={26} k={2} c="steel" o={0.8} />
        <g fill="none" stroke={S.crimson} strokeWidth="2.6"><polygon points={pts(ngon(36, 31, 11, 7, 6))} /><polygon points={pts(ngon(52, 31, 11, 7, 6))} /><polygon points={pts(ngon(68, 31, 11, 7, 6))} /></g>
        <g fill="none" stroke={ART.steel.hi} strokeWidth="0.8"><polygon points={pts(ngon(36, 31, 11, 7, 6))} /><polygon points={pts(ngon(52, 31, 11, 7, 6))} /></g>
        <text x="48" y="12.4" textAnchor="middle" fontSize="2.8" fill={S.warn} {...PT}>{variant === 'stamp' ? 'STAMP: FRESH METAL' : 'IRON CHAIN'}</text>
        <polygon points={pts([[6, 52], [90, 52], [90, 58], [6, 58]])} fill={ART.dark.shade} /><Hazard x={6} y={52} w={84} h={3} />
      </g>
    );
  }
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      {base}{art}
      <Grime x={4} y={4} w={88} h={54} seed={(variant || 'w').length + 80} n={6} op={0.22} /><Scuff x={6} y={6} w={84} h={50} seed={(variant || 'w').length + 81} n={9} c={plate} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// SKYLINE: the city seen from a promenade edge, a traffic overlook, or the penthouse viewport.
// ---------------------------------------------------------------------------
function PropSkyline({ variant }) {
  const S = ART.signal;
  const lanes = variant === 'lanes', view = variant === 'viewport';
  const tower = (x, w, h, base, k, lit) => {
    const top = 46 - h, r = rng(k);
    return (
      <g key={k}>
        <polygon points={pts([[x, 46], [x, top + 3], [x + 2, top], [x + w - 2, top], [x + w, top + 3], [x + w, 46]])} fill={ART[base].base} />
        <rect x={x} y={top + 3} width="1.4" height={h - 3} fill={ART[base].hi} opacity="0.6" />
        {k % 3 === 0 && <rect x={x + w / 2 - 0.5} y={top - 6} width="1" height="6" fill={ART[base].hi} />}
        {Array.from({ length: Math.floor(h / 5) }, (_, j) => Array.from({ length: Math.floor(w / 4) }, (_, i) => r() < lit ? <rect key={i + '_' + j} x={x + 2 + i * 4} y={top + 5 + j * 5} width="2" height="2" fill={r() < 0.7 ? S.amberHi : S.cyanHi} opacity="0.85" /> : null))}
      </g>
    );
  };
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.6} />
      <Slab x={2} y={2} w={124} h={46} k={3} c="dark" o={1} />
      <rect x="6" y="5" width="116" height="41" fill={ART.screen.space} />
      {lanes ? (
        <g>
          <rect x="6" y="5" width="116" height="9" fill={ART.dark.shade} /><rect x="6" y="14" width="116" height="9" fill={ART.dark.base} opacity="0.6" />
          {[[9, 'cyan', 'TIER 1'], [20, 'amber', 'TIER 2'], [31, 'red', 'CLEARANCE']].map(([y, k, t], i) => (
            <g key={k}>
              <rect x="6" y={y} width="116" height="1.6" fill={S[k]} opacity="0.9" />
              <text x="8" y={y - 1.4} fontSize="2.4" fill={S[k]} {...PT}>{t}</text>
              {Array.from({ length: 5 }, (_, j) => { const x = 14 + ((j * 23 + i * 11) % 100); return <g key={j}><polygon points={pts([[x, y + 4], [x + 9, y + 4], [x + 11, y + 6], [x + 9, y + 8], [x, y + 8]])} fill={ART[i === 0 ? 'teal' : i === 1 ? 'amber' : 'red'].base} /><rect x={x + 1} y={y + 5} width="5" height="1.4" fill={S[k]} /><rect x={x - 3} y={y + 5.4} width="3" height="1.2" fill={S[k]} opacity="0.5" /></g>; })}
            </g>
          ))}
          <g stroke={S.white} strokeWidth="0.4" opacity="0.35">{[10, 40, 70, 100].map((x) => <line key={x} x1={x} y1="6" x2={x + 8} y2="46" />)}</g>
        </g>
      ) : view ? (
        <g>
          <rect x="6" y="5" width="116" height="14" fill={ART.deepglass.shade} /><rect x="6" y="19" width="116" height="12" fill={ART.deepglass.base} opacity="0.5" /><rect x="6" y="31" width="116" height="15" fill={ART.amber.shade} opacity="0.55" />
          {Array.from({ length: 14 }, (_, i) => <rect key={i} x={10 + ((i * 37) % 108)} y={7 + ((i * 11) % 18)} width="1" height="1" fill={S.white} opacity="0.7" style={{ animation:`twinkle ${2 + (i % 3)}s ease-in-out infinite` }} />)}
          {[18, 34, 60, 90, 106].map((x, i) => tower(x, 10 + (i % 2) * 4, 12 + ((i * 7) % 4) * 4, 'dark', i + 3, 0.3))}
          <polygon points={pts(ngon(78, 33, 14, 9, 12).filter((p) => p[1] <= 33))} fill={ART.pearl.base} /><rect x="64" y="33" width="28" height="4" fill={ART.pearl.shade} />
          {[67, 72, 77, 82, 87].map((x) => <rect key={x} x={x} y="33" width="1.6" height="4" fill={ART.pearl.hi} />)}
          <path d="M6 44 C30 36 50 46 70 40 C90 34 104 44 122 38" fill="none" stroke={S.amberHi} strokeWidth="2.2" /><path d="M6 41 C26 33 54 42 74 37 C94 32 108 40 122 35" fill="none" stroke={S.white} strokeWidth="1" opacity="0.8" />
          {[6, 36, 66, 96].map((x) => <polygon key={x} points={pts([[x + 8, 5], [x + 12, 5], [x + 4, 46], [x, 46]])} fill={ART.pearl.hi} opacity="0.1" />)}
        </g>
      ) : (
        <g>
          <rect x="6" y="5" width="116" height="12" fill={ART.deepglass.shade} /><rect x="6" y="17" width="116" height="10" fill={ART.deepglass.base} opacity="0.4" /><rect x="6" y="27" width="116" height="9" fill={ART.amber.shade} opacity="0.5" /><rect x="6" y="38" width="116" height="8" fill={ART.ink} opacity="0.55" />
          {tower(10, 12, 20, 'dark', 1, 0.15)}{tower(26, 9, 26, 'dark', 2, 0.15)}{tower(40, 14, 18, 'dark', 3, 0.15)}{tower(96, 10, 24, 'dark', 4, 0.15)}{tower(108, 11, 17, 'dark', 5, 0.15)}
          {tower(18, 11, 30, 'steel', 6, 0.35)}{tower(70, 12, 34, 'steel', 7, 0.35)}{tower(56, 8, 22, 'steel', 8, 0.35)}{tower(84, 10, 28, 'steel', 9, 0.35)}
          <polygon points={pts(ngon(48, 28, 12, 8, 12).filter((p) => p[1] <= 28))} fill={ART.pearl.base} /><rect x="36" y="28" width="24" height="3" fill={ART.pearl.shade} />
          {[[8, 11, 'amber'], [30, 15, 'white'], [58, 12, 'cyan'], [86, 19, 'amber'], [104, 14, 'white']].map(([x, y, k], i) => <g key={i}><rect x={x} y={y} width="9" height="1.6" fill={S[k]} /><rect x={x - 5} y={y + 0.2} width="5" height="1.2" fill={S[k]} opacity="0.5" /></g>)}
          {[[12, 23], [44, 25], [74, 21], [102, 24]].map(([x, y], i) => <g key={i}><rect x={x} y={y} width="8" height="1.4" fill={S.amberHi} opacity="0.8" /><rect x={x - 4} y={y + 0.2} width="4" height="1" fill={S.amberHi} opacity="0.4" /></g>)}
        </g>
      )}
      {/* foreground rail and sill */}
      <Bev t="rect" x="4" y="46" width="120" height="3" c="steel" o={0.7} />
      {Array.from({ length: 10 }, (_, i) => <polygon key={i} points={pts([[8 + i * 12.4, 58], [8 + i * 12.4, 40], [10 + i * 12.4, 40], [10 + i * 12.4, 58]])} fill={ART.steel.base} opacity={view ? 0 : 1} />)}
      {!view && <g><Bev t="rect" x="6" y="41" width="116" height="2.4" c="steel" o={0.6} /><Hazard x={6} y={55} w={116} h={3} /></g>}
      {view && <g><Bev t="rect" x="6" y="49" width="116" height="9" c="bronze" o={0.9} /><Seams x={6} y={49} w={116} h={9} cols={8} op={0.3} /><Lights x={12} y={53.6} n={12} gap={9} seed={5} /></g>}
      {[6, 35, 64, 93, 122].map((x) => <rect key={x} x={x - 1.2} y="3" width="2.4" height="44" fill={ART.dark.hi} opacity={view ? 1 : 0} />)}
      <Rivets x={8} y={4} w={112} n={14} />
      <Grime x={4} y={4} w={120} h={44} seed={view ? 5 : lanes ? 6 : 7} n={5} op={0.12} /><Scuff x={6} y={48} w={116} h={10} seed={view ? 8 : 9} n={8} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// ELEVATOR DOOR: the private Scylla Tower car.
// ---------------------------------------------------------------------------
function PropElevatorDoor() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={61} rx={26} ry={2.4} />
      <Bev t="polygon" points={pts([[6, 62], [6, 8], [10, 3], [54, 3], [58, 8], [58, 62]])} c="bronze" o={1} />
      <Slab x={12} y={6} w={40} h={9} k={1.4} c="dark" o={0.7} />
      <rect x="14" y="8" width="36" height="5" fill={ART.screen.green} /><text x="32" y="12.4" textAnchor="middle" fontSize="4.4" fill={S.greenHi} fontWeight="700" {...PT}>1452</text>
      <polygon points={pts([[43, 12], [47, 12], [45, 9]])} fill={S.greenHi} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      <Bev t="polygon" points={pts([[12, 62], [12, 18], [32, 18], [32, 62]])} c="brass" o={0.9} />
      <Bev t="polygon" points={pts([[32, 62], [32, 18], [52, 18], [52, 62]])} c="brass" o={0.9} />
      <rect x="31" y="18" width="2" height="44" fill={ART.ink} />
      <Seams x={12} y={18} w={40} h={44} rows={2} op={0.28} />
      {[16, 36].map((x) => <polygon key={x} points={pts([[x, 52], [x + 8, 24], [x + 12, 24], [x + 4, 52]])} fill={ART.pearl.hi} opacity="0.12" />)}
      <Slab x={53} y={30} w={5} h={14} k={0.8} c="dark" o={0.5} /><circle cx="55.5" cy="34" r="1.2" fill={S.greenHi} /><polygon points={pts([[54, 40], [57, 40], [55.5, 37.6]])} fill={S.amberHi} />
      <Slab x={12} y={55} w={40} h={5} k={1} c="dark" o={0.6} />
      <text x="32" y="58.4" textAnchor="middle" fontSize="2.5" fill={S.amberHi} {...PT}>SCYLLA TOWER SEVEN</text>
      <text x="32" y="16.6" textAnchor="middle" fontSize="1.9" fill={S.white} opacity="0.9" {...PT}>AUTHORIZED CLEARANCE ONLY</text>
      <Rivets x={9} y={20} w={46} n={2} c="brass" /><Rivets x={9} y={50} w={46} n={2} c="brass" />
      <Grime x={6} y={4} w={52} h={58} seed={41} n={4} op={0.14} /><Scuff x={12} y={20} w={40} h={38} seed={42} n={6} c="brass" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// DATAPAD TABLE: a datapad left on a surface. Five surfaces, five stories.
// ---------------------------------------------------------------------------
function PropDatapadTable({ variant }) {
  const S = ART.signal;
  const canister = variant === 'canister';
  const logbook = variant === 'logbook', card = variant === 'card', warm = variant === 'warm', lounge = variant === 'lounge';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={24} ry={3.4} />
      {lounge ? (
        <g>
          <Slab x={29} y={42} w={6} h={14} k={1} c="dark" /><Bev t="polygon" points={pts(ngon(32, 56, 12, 3.2, 8))} c="steel" o={0.7} />
          <Bev t="polygon" points={pts(ngon(32, 40, 25, 9, 10, Math.PI / 10))} c="bronze" o={1} />
          <polygon points={pts(ngon(32, 39, 22, 7.4, 10, Math.PI / 10))} fill={ART.wood.hi} opacity="0.5" />
          <g><polygon points={pts([[46, 34], [52, 34], [51, 42], [47, 42]])} fill={ART.glass.base} opacity="0.85" /><polygon points={pts([[47.4, 38], [50.6, 38], [50.2, 41.4], [47.8, 41.4]])} fill={ART.amber.hi} /></g>
        </g>
      ) : logbook ? (
        <g>
          <Slab x={26} y={34} w={12} h={22} k={1.2} c="rust" /><Slab x={16} y={54} w={32} h={5} k={1.2} c="rust" o={0.8} />
          <Bev t="polygon" points={pts([[8, 36], [56, 36], [52, 20], [12, 20]])} c="rust" o={1} />
        </g>
      ) : (
        <g>
          <Slab x={8} y={30} w={48} h={26} k={2.4} c={card ? 'dark' : 'steel'} o={1} />
          <Seams x={8} y={30} w={48} h={26} cols={2} rows={1} op={0.4} />
          <Hazard x={10} y={48} w={44} h={4} /><Rivets x={11} y={33} w={42} n={6} c="steel" />
        </g>
      )}
      {/* the item on the surface */}
      {logbook ? (
        <g>
          <polygon points={pts([[14, 35], [32, 31], [32, 20], [14, 24]])} fill={ART.paper.base} /><polygon points={pts([[50, 35], [32, 31], [32, 20], [50, 24]])} fill={ART.paper.hi} />
          <line x1="32" y1="20" x2="32" y2="31" stroke={ART.paper.shade} strokeWidth="0.8" />
          {[0, 1, 2, 3].map((i) => <g key={i}><line x1="17" y1={26 + i * 2.2 - i * 0.2} x2="30" y2={24.4 + i * 2.2} stroke={ART.note.blueInk} strokeWidth="0.4" /><line x1="35" y1={24.4 + i * 2.2} x2="48" y2={26 + i * 2.2 - i * 0.2} stroke={ART.note.blueInk} strokeWidth="0.4" /></g>)}
          <text x="42" y="32.6" textAnchor="middle" fontSize="2.4" fill={ART.note.blueInk} fontWeight="700" {...PT}>Vane, T.</text>
          <polygon points={pts([[12, 22], [22, 21], [20, 27], [12, 28]])} fill={ART.ink} opacity="0.12" />
        </g>
      ) : canister ? (
        <g>
          <Bev t="polygon" points={pts([[16, 46], [16, 30], [48, 30], [48, 46]])} c="steel" o={0.9} />
          <ellipse cx="32" cy="30" rx="16" ry="3.4" fill={ART.steel.hi} /><ellipse cx="32" cy="30.4" rx="10" ry="1.8" fill={ART.dark.shade} />
          <rect x="16" y="34" width="32" height="3" fill={ART.brand.kdy} /><rect x="16" y="41" width="32" height="1.6" fill={ART.dark.shade} />
          <polygon points={pts([[22, 36], [42, 36], [42, 40], [22, 40]])} fill={ART.paper.base} /><text x="32" y="39.2" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} fontWeight="700" {...PT}>KDY CLASSIFIED</text>
          <polygon points={pts([[50, 40], [60, 38], [60, 46], [50, 47]])} fill={ART.paper.hi} /><g stroke={ART.note.blueInk} strokeWidth="0.5"><line x1="52" y1="41" x2="58" y2="40.4" /><line x1="52" y1="43" x2="57" y2="42.4" /></g>
        </g>
      ) : card ? (
        <g>
          <polygon points={pts([[18, 40], [36, 36], [44, 40], [42, 46], [34, 44], [22, 47]])} fill={ART.red.shade} />
          <polygon points={pts([[17, 39], [35, 35], [43, 39], [41, 45], [33, 43], [21, 46]])} fill={ART.steel.base} />
          <polygon points={pts([[28, 37], [38, 37], [41, 40], [34, 42], [28, 41]])} fill={ART.dark.base} />
          <polygon points={pts([[20, 41], [27, 40], [27, 43], [21, 44]])} fill={ART.amber.hi} />
          <polygon points={pts([[17, 39], [24, 38], [22, 41], [18, 42]])} fill={S.orange} style={{ animation:'ship-blink 2.4s ease-in-out infinite' }} />
          <path d="M36 38 C38 33 34 31 37 27" fill="none" stroke={ART.dark.hi} strokeWidth="1.4" opacity="0.6" />
        </g>
      ) : (
        <g transform="rotate(-5 32 34)">
          <Bev t="polygon" points={pts([[16, 40], [44, 40], [44, 22], [16, 22]])} c="dark" o={0.9} />
          <rect x="18" y="24" width="24" height="14" fill={warm ? ART.screen.amber : ART.screen.cyan} />
          <rect x="18" y="24" width="24" height="3" fill={warm ? S.amber : S.cyan} opacity="0.35" />
          {(variant === 'jon' ? ['HUB A 02:00', 'HUB B 04:00', 'HUB C 06:00'] : lounge ? ['CR 840,000', '3 SHELL CORPS', 'CORUSCANT ACCT'] : ['PULLING BAY 14', 'DL 71% STOPPED', 'GOING DARK']).map((t, i) => <text key={i} x="19.4" y={30.6 + i * 3.6} fontSize="2.3" fill={warm ? S.amberHi : S.cyanHi} {...PT}>{'> ' + t}</text>)}
          <circle cx="40" cy="38" r="0.9" fill={S.green} />
        </g>
      )}
      {variant === 'jon' && <g><polygon points={pts([[40, 44], [54, 42], [54, 52], [41, 53]])} fill={ART.paper.base} /><path d="M44 46 C50 44 52 50 46 51 C42 51 42 47 46 46" fill="none" stroke={ART.note.redInk} strokeWidth="1" /></g>}
      {warm && <g fill="none" stroke={S.orange} strokeWidth="0.8" opacity="0.7">{[24, 32, 40].map((x, i) => <path key={x} d={`M${x} 20 C${x + 3} 16 ${x - 3} 12 ${x} 8`} style={{ animation:`holo-flicker ${1.6 + i * 0.4}s ease-in-out infinite` }} />)}</g>}
      <Lights x={12} y={54} n={5} gap={4} r={0.7} seed={9} />
      <Grime x={8} y={32} w={48} h={24} seed={(variant || 'd').length + 90} n={4} op={0.22} /><Scuff x={8} y={34} w={48} h={22} seed={(variant || 'd').length + 91} n={6} />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// STORAGE: crates, containers, drums, racks, lockers. The cargo language of the underworld and the dock.
// ---------------------------------------------------------------------------
function CrateBox({ x, y, w, h, c = 'dark', hz = true, k = 1.6, cols, rows }) {
  return (
    <g>
      <Slab x={x} y={y} w={w} h={h} k={k} c={c} o={0.9} />
      <Seams x={x} y={y} w={w} h={h} cols={cols ?? (w > 20 ? 2 : 1)} rows={rows ?? (h > 18 ? 1 : 0)} op={0.4} />
      <rect x={x + 1.4} y={y + 1.2} width={w - 2.8} height="1.2" fill={ART[c].hi} opacity="0.8" />
      {hz && h > 9 && <Hazard x={x + 1} y={y + h - 4.2} w={w - 2} h={3.2} />}
    </g>
  );
}

function PropCrateStack({ variant }) {
  const S = ART.signal;
  let body = null;
  if (variant === 'agri') {
    body = (
      <g>
        <polygon points={pts([[6, 28], [14, 14], [50, 14], [54, 28]])} fill={ART.steel.shade} />
        <CrateBox x={6} y={28} w={48} h={30} c="steel" hz={false} cols={3} rows={0} />
        <rect x="9" y="31" width="42" height="9" fill={ART.ink} />
        {Array.from({ length: 9 }, (_, i) => <g key={i}><rect x={11 + i * 4.5} y="31.4" width="2.6" height="6.4" fill={[S.cyanHi, S.white, S.magenta][i % 3]} opacity="0.9" /><rect x={11.5 + i * 4.5} y="30" width="1.6" height="1.8" fill={ART.steel.hi} /></g>)}
        <text x="30" y="48" textAnchor="middle" fontSize="3" fill={ART.paper.base} fontWeight="700" {...PT}>AGRICULTURAL SUPPLIES</text>
        <text x="30" y="53.6" textAnchor="middle" fontSize="2.3" fill={S.warn} {...PT}>SEAL: CUT. CONTENTS: STAGED</text>
        <CrateBox x={58} y={36} w={32} h={22} c="dark" /><CrateBox x={62} y={18} w={26} h={19} c="dark" />
        {[0, 1, 2].map((i) => <g key={i}><Slab x={66 + i * 7} y={52 + (i % 2)} w={5} h={7} k={0.8} c="amber" o={0.4} /></g>)}
      </g>
    );
  } else if (variant === 'weapons') {
    body = (
      <g>
        {Array.from({ length: 32 }, (_, i) => { const col = i % 8, row = Math.floor(i / 8); return <CrateBox key={i} x={4 + col * 11.3} y={10 + row * 11.6} w={10} h={10.2} c={(col + row) % 3 ? 'dark' : 'steel'} hz={false} k={1} cols={1} rows={0} />; })}
        <polygon points={pts([[70, 20], [88, 18], [89, 27], [71, 29]])} fill={ART.paper.base} /><text x="79.5" y="24" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} fontWeight="700" {...PT}>MANIFEST</text>
        <text x="48" y="7.6" textAnchor="middle" fontSize="3.2" fill={S.warn} fontWeight="700" {...PT}>AGRICULTURAL EQUIPMENT x32</text>
        <Hazard x={4} y={58} w={88} h={3} />
      </g>
    );
  } else if (variant === 'syndicate') {
    body = (
      <g>
        <CrateBox x={4} y={30} w={34} h={28} c="dark" /><Glyph k="chain" x={21} y={42} s={5.4} c={S.crimson} />
        <CrateBox x={8} y={12} w={28} h={19} c="dark" hz={false} /><Glyph k="chain" x={22} y={22} s={4.4} c={S.crimson} />
        {[0, 1, 2, 3].map((i) => <Bev key={i} t="polygon" points={pts([[42, 56 - i * 3], [64, 56 - i * 3], [66, 58 - i * 3], [44, 58 - i * 3]])} c="steel" o={0.5} />)}
        <Bev t="polygon" points={pts([[44, 42], [62, 42], [64, 36], [60, 28], [46, 28], [42, 36]])} c="dark" o={0.9} /><rect x="49" y="30" width="8" height="2.4" fill={S.amber} /><polygon points={pts([[46, 42], [60, 42], [58, 46], [48, 46]])} fill={ART.dark.shade} />
        <Bev t="polygon" points={pts([[72, 58], [72, 28], [90, 28], [90, 58]])} c="pearl" o={0.8} /><ellipse cx="81" cy="28" rx="9" ry="3" fill={ART.pearl.hi} /><ellipse cx="81" cy="28.4" rx="6" ry="2" fill={ART.ink} />
        <Glyph k="cog" x={81} y={44} s={5} c={S.cyan} /><line x1="73" y1="52" x2="89" y2="52" stroke={ART.pearl.shade} strokeWidth="0.8" />
        <text x="81" y="24" textAnchor="middle" fontSize="2.4" fill={S.cyanHi} {...PT}>EMPTY</text>
      </g>
    );
  } else if (variant === 'arms') {
    body = (
      <g>
        {Array.from({ length: 10 }, (_, i) => { const col = i % 5, row = Math.floor(i / 5); return <g key={i}><CrateBox x={4 + col * 18} y={14 + row * 21} w={17} h={20} c={(col + row) % 2 ? 'dark' : 'steel'} hz={false} k={1.2} cols={1} rows={0} /><rect x={6 + col * 18} y={22 + row * 21} width="13" height="4.4" fill={S.orange} /><text x={12.5 + col * 18} y={25.6 + row * 21} textAnchor="middle" fontSize="2.3" fill={ART.ink} fontWeight="700" {...PT}>M-SONN</text></g>; })}
        <Hazard x={4} y={56} w={88} h={3} />
      </g>
    );
  } else if (variant === 'arms_cache') {
    body = (
      <g>
        <CrateBox x={4} y={28} w={50} h={30} c="steel" hz={false} cols={3} rows={0} />
        {Array.from({ length: 5 }, (_, i) => <g key={i}><path d={`M${10 + i * 8} 40 C${10 + i * 8} 28 ${18 + i * 8} 28 ${18 + i * 8} 40 Z`} fill={ART.hull.base} /><path d={`M${10 + i * 8} 40 C${10 + i * 8} 31 ${14 + i * 8} 29 ${16 + i * 8} 31`} fill="none" stroke={ART.hull.hi} strokeWidth="0.9" /><rect x={10 + i * 8} y="39" width="8" height="1.6" fill={S.crimson} /></g>)}
        <text x="29" y="50" textAnchor="middle" fontSize="2.6" fill={ART.paper.base} fontWeight="700" {...PT}>PHRIK x20  UNDERARMOR x6</text>
        <CrateBox x={58} y={38} w={32} h={20} c="dark" /><CrateBox x={60} y={20} w={26} h={19} c="dark" hz={false} />
        <Bev t="polygon" points={pts([[66, 30], [84, 30], [86, 36], [82, 40], [68, 40], [64, 36]])} c="pearl" o={0.6} /><circle cx="75" cy="34.6" r="3.2" fill={ART.red.base} /><text x="75" y="35.6" textAnchor="middle" fontSize="2.2" fill={S.white} fontWeight="700" {...PT}>SEN</text>
      </g>
    );
  } else if (variant === 'locker') {
    body = (
      <g>
        <Slab x={22} y={4} w={52} h={54} k={3} c="steel" o={1} />
        <Seams x={22} y={4} w={52} h={54} cols={1} rows={3} op={0.5} />
        <Vent x={26} y={8} w={20} h={8} n={4} /><Vent x={50} y={8} w={20} h={8} n={4} />
        <Slab x={52} y={26} w={18} h={22} k={1.4} c="dark" o={0.7} />
        {Array.from({ length: 6 }, (_, i) => <rect key={i} x={54.4 + (i % 3) * 5} y={29 + Math.floor(i / 3) * 5} width="3.6" height="3.6" fill={ART.dark.hi} />)}
        <rect x="54" y="40" width="14" height="3" fill={ART.screen.red} /><circle cx="56" cy="41.5" r="0.8" fill={S.red} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} /><text x="62" y="42.6" fontSize="2" fill={S.redHi} {...PT}>MIL CIPHER</text>
        {[0, 1, 2].map((i) => <polygon key={i} points={pts([[26 + i * 5, 58], [28 + i * 5, 40], [31 + i * 5, 40], [29 + i * 5, 58]])} fill={ART.paper.hi} />)}
        <Slab x={28} y={30} w={20} h={6} k={0.8} c="amber" o={0.4} /><text x="38" y="34.4" textAnchor="middle" fontSize="2.3" fill={ART.ink} fontWeight="700" {...PT}>FREIGHT 34-E</text>
        <Hazard x={24} y={52} w={48} h={4} />
      </g>
    );
  } else if (variant === 'stash') {
    body = (
      <g>
        <CrateBox x={26} y={8} w={46} h={40} c="steel" hz={false} cols={2} rows={1} />
        <g stroke={ART.amber.hi} strokeWidth="1.2" opacity="0.9"><line x1="26" y1="28" x2="72" y2="28" strokeDasharray="3 2" /><line x1="49" y1="8" x2="49" y2="48" strokeDasharray="3 2" /></g>
        <Slab x={52} y={28} w={14} h={9} k={1} c="dark" o={0.5} /><circle cx="59" cy="32.6" r="1.4" fill={S.amber} />
        <Bev t="polygon" points={pts([[10, 58], [10, 28], [14, 24], [40, 24], [44, 28], [44, 58]])} c="teal" o={1} />
        <ellipse cx="27" cy="24" rx="15" ry="3.6" fill={ART.teal.hi} /><ellipse cx="27" cy="24.4" rx="11" ry="2.4" fill={ART.dark.shade} />
        <rect x="10" y="34" width="34" height="2.6" fill={ART.dark.shade} /><rect x="10" y="48" width="34" height="2.6" fill={ART.dark.shade} />
        <Hazard x={14} y={40} w={26} h={5} /><text x="27" y="55.4" textAnchor="middle" fontSize="2.2" fill={ART.pearl.hi} {...PT}>COOLANT</text>
      </g>
    );
  } else if (variant === 'false_panel') {
    body = (
      <g>
        <Slab x={8} y={4} w={80} h={54} k={3} c="concrete" o={1} />
        <Seams x={8} y={4} w={80} h={54} cols={3} rows={1} op={0.4} />
        <rect x="26" y="12" width="44" height="40" fill={ART.ink} />
        <polygon points={pts([[26, 12], [14, 16], [14, 56], [26, 52]])} fill={ART.concrete.hi} /><polygon points={pts([[26, 12], [14, 16], [14, 20], [26, 16]])} fill={ART.concrete.shade} />
        <Bev t="polygon" points={pts([[30, 52], [30, 28], [66, 28], [66, 52]])} c="dark" o={0.9} />
        <Seams x={30} y={28} w={36} h={24} cols={2} op={0.45} /><Glyph k="chain" x={48} y={40} s={6} c={S.crimson} />
        {[0, 1, 2].map((i) => <g key={i}><Slab x={34 + i * 10} y={21} w={8} h={6} k={1} c="dark" o={0.5} /><rect x={36 + i * 10} y="22.6" width="4" height="2.6" fill={S.lime} style={{ animation:`ship-blink ${2 + i * 0.6}s steps(2) infinite` }} /></g>)}
        <Rivets x={12} y={7} w={72} n={9} /><Hazard x={10} y={54} w={76} h={3} />
      </g>
    );
  } else if (variant === 'lockbox') {
    body = (
      <g>
        <Bev t="polygon" points={pts([[14, 56], [14, 20], [20, 14], [76, 14], [82, 20], [82, 56]])} c="steel" o={1.3} />
        <Seams x={14} y={14} w={68} h={42} cols={1} rows={2} op={0.5} />
        <Slab x={36} y={26} w={24} h={20} k={1.6} c="dark" o={0.8} /><polygon points={pts(ngon(48, 36, 6, 6, 8))} fill={ART.steel.shade} /><polygon points={pts(ngon(48, 36, 3.6, 3.6, 8))} fill={ART.brass.base} />
        <rect x="38" y="48" width="20" height="4" fill={ART.dark.shade} /><text x="48" y="51.2" textAnchor="middle" fontSize="2.2" fill={S.redHi} {...PT}>SERIAL: GROUND OFF</text>
        {[[18, 52], [76, 52], [18, 18], [76, 18]].map(([x, y], i) => <g key={i}><polygon points={pts([[x - 4, y + 4], [x + 4, y + 4], [x + 2, y - 2], [x - 2, y - 2]])} fill={S.orange} opacity="0.85" /></g>)}
        <text x="48" y="10" textAnchor="middle" fontSize="2.8" fill={S.warn} fontWeight="700" {...PT}>WELDED TO THE FLOOR</text>
        <Hazard x={16} y={56} w={64} h={3} />
      </g>
    );
  } else if (variant === 'hidden') {
    body = (
      <g>
        <Slab x={4} y={4} w={88} h={54} k={3} c="concrete" o={1} />
        <polygon points={pts([[4, 20], [24, 14], [40, 28], [30, 58], [4, 58]])} fill={ART.concrete.shade} /><polygon points={pts([[60, 10], [92, 18], [92, 58], [70, 58], [76, 34]])} fill={ART.concrete.shade} />
        <polygon points={pts([[28, 56], [30, 26], [44, 20], [64, 22], [70, 56]])} fill={ART.ink} />
        <CrateBox x={34} y={38} w={20} h={18} c="canvas" hz={false} k={1.2} cols={1} rows={0} /><CrateBox x={50} y={42} w={16} h={14} c="steel" hz={false} k={1} cols={1} rows={0} />
        {[0, 1, 2].map((i) => <rect key={i} x={38 + i * 6} y="32" width="4" height="6" fill={S.cyanHi} opacity="0.85" />)}
        <polyline points={pts([[10, 26], [18, 34], [14, 44]])} fill="none" stroke={ART.ink} strokeWidth="1.2" /><polyline points={pts([[78, 24], [86, 34], [80, 44]])} fill="none" stroke={ART.ink} strokeWidth="1.2" />
        <text x="48" y="12" textAnchor="middle" fontSize="2.8" fill={S.warn} fontWeight="700" {...PT}>COLLAPSED WALL: SUPPLY DROP</text>
      </g>
    );
  } else if (variant === 'resist') {
    body = (
      <g>
        <CrateBox x={6} y={26} w={46} h={32} c="canvas" hz={false} cols={3} rows={0} />
        {[0, 1, 2].map((i) => <g key={i}><Bev t="polygon" points={pts([[10 + i * 14, 32], [20 + i * 14, 32], [22 + i * 14, 36], [8 + i * 14, 36]])} c="dark" o={0.4} /><rect x={12 + i * 14} y="30" width="12" height="2" fill={ART.steel.hi} /></g>)}
        <text x="29" y="48" textAnchor="middle" fontSize="2.8" fill={ART.paper.base} fontWeight="700" {...PT}>PRE-WAR BLASTERS</text>
        <CrateBox x={56} y={34} w={34} h={24} c="dark" hz={false} /><polygon points={pts(ngon(73, 46, 6, 6, 8))} fill={ART.pearl.base} /><rect x="71.6" y="41" width="2.8" height="10" fill={ART.teal.base} /><rect x="68" y="44.6" width="10" height="2.8" fill={ART.teal.base} />
        <text x="73" y="32" textAnchor="middle" fontSize="2.4" fill={S.cyanHi} {...PT}>BACTA PACKS</text>
        <g transform="rotate(-20 32 20)"><polygon points={pts([[20, 14], [40, 14], [42, 18], [20, 18]])} fill={ART.steel.hi} /><rect x="38" y="13" width="6" height="6" fill={S.crimson} /></g>
        <text x="34" y="10" textAnchor="middle" fontSize="2.4" fill={S.warn} {...PT}>CRACKED SLICER SPIKE</text>
      </g>
    );
  } else if (variant === 'survey') {
    body = (
      <g>
        <CrateBox x={6} y={28} w={50} h={30} c="stone" hz={false} cols={3} rows={0} />
        {Array.from({ length: 6 }, (_, i) => <g key={i}><rect x={10 + i * 7.4} y="30" width="4" height="12" fill={ART.glass.base} opacity="0.8" /><rect x={10 + i * 7.4} y={36 + (i % 3) * 2} width="4" height={6 - (i % 3) * 2} fill={[ART.rust.base, ART.moss.base, ART.sand.base][i % 3]} /></g>)}
        <text x="31" y="52" textAnchor="middle" fontSize="2.6" fill={ART.paper.base} fontWeight="700" {...PT}>GEOLOGICAL SAMPLES</text>
        <Bev t="polygon" points={pts([[62, 56], [62, 30], [88, 30], [88, 56]])} c="dark" o={0.9} /><polygon points={pts(ngon(75, 42, 8, 8, 8))} fill={ART.deepglass.base} /><polygon points={pts(ngon(75, 42, 4.4, 4.4, 8))} fill={S.violet} style={{ animation:'ship-blink 2.4s steps(3) infinite' }} />
        <text x="75" y="27" textAnchor="middle" fontSize="2.4" fill={S.violet} fontWeight="700" {...PT}>RESEALED CORE</text>
        <polygon points={pts([[64, 50], [86, 50], [86, 55], [64, 55]])} fill={ART.red.base} /><text x="75" y="54" textAnchor="middle" fontSize="2.2" fill={S.white} {...PT}>NOT REPUBLIC</text>
      </g>
    );
  } else {
    body = (
      <g>
        {[[6, 34, 30, 24, 'stone'], [34, 24, 28, 34, 'stone'], [60, 38, 28, 20, 'stone'], [20, 16, 20, 20, 'stone']].map(([x, y, w, h, c], i) => <Bev key={i} t="polygon" points={pts([[x, y + h], [x + 2, y + 5], [x + w * 0.4, y], [x + w * 0.8, y + 3], [x + w, y + h * 0.4], [x + w - 2, y + h]])} c={c} o={1} />)}
        <g stroke={ART.stone.shade} strokeWidth="0.9" fill="none"><polyline points={pts([[40, 30], [46, 36], [52, 30], [58, 40]])} /><polyline points={pts([[12, 44], [20, 48], [26, 44]])} /><polygon points={pts(ngon(72, 48, 5, 5, 4, Math.PI / 4))} /></g>
        <polygon points={pts([[22, 20], [34, 22], [32, 32], [24, 30]])} fill={ART.moss.base} opacity="0.85" /><polygon points={pts([[36, 28], [48, 28], [44, 34]])} fill={ART.moss.hi} opacity="0.7" />
        <text x="48" y="60" textAnchor="middle" fontSize="2.2" fill={ART.beige.hi} {...PT}>PRE-REPUBLIC STONEWORK</text>
      </g>
    );
  }
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {body}
      <Rivets x={6} y={61.4} w={84} n={10} />
      <Grime x={4} y={8} w={88} h={52} seed={(variant || 'c').length + 100} n={5} op={0.2} /><Scuff x={6} y={10} w={84} h={48} seed={(variant || 'c').length + 101} n={8} c="dark" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// CONTAINER STACK: three high Senate sealed grey containers on a platform.
// ---------------------------------------------------------------------------
function PropContainerStack() {
  const S = ART.signal;
  const seals = ['blue', 'amber', 'red', 'cyan', 'green', 'violet'];
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={60} ry={2.6} />
      {[0, 1, 2].map((row) => [0, 1, 2, 3].map((col) => {
        const x = 3 + col * 31, y = 4 + row * 18, i = row * 4 + col;
        return (
          <g key={i}>
            <Slab x={x} y={y} w={30} h={17} k={1.4} c={(row + col) % 2 ? 'hull' : 'steel'} o={0.8} />
            {Array.from({ length: 7 }, (_, j) => <rect key={j} x={x + 3 + j * 3.6} y={y + 2.6} width="1.2" height="12" fill={ART.ink} opacity="0.28" />)}
            <rect x={x} y={y} width="30" height="1.4" fill={ART.hull.hi} opacity="0.8" />
            <polygon points={pts(ngon(x + 24, y + 8.6, 3.4, 3.4, 8))} fill={S[seals[i % 6]]} /><polygon points={pts(ngon(x + 24, y + 8.6, 1.6, 1.6, 8))} fill={ART.ink} />
            <text x={x + 3} y={y + 15.4} fontSize="2.2" fill={ART.ink} fontWeight="700" {...PT}>{'SCC-' + (140 + i * 7)}</text>
          </g>
        );
      }))}
      <text x="64" y="62" textAnchor="middle" fontSize="2.8" fill={ART.beige.hi} {...PT}>LEGISLATIVE MATERIALS x48</text>
      <Rivets x={4} y={3.4} w={120} n={14} /><Grime x={3} y={4} w={122} h={54} seed={110} n={6} op={0.2} /><Scuff x={4} y={6} w={120} h={50} seed={111} n={9} c="hull" />
    </svg>
  );
}

function PropCargoContainer() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[4, 58], [4, 12], [8, 8], [88, 8], [92, 12], [92, 58]])} c="rust" o={1} />
      {Array.from({ length: 14 }, (_, j) => <rect key={j} x={9 + j * 5.6} y="12" width="1.8" height="42" fill={ART.ink} opacity="0.25" />)}
      <rect x="4" y="8" width="88" height="2" fill={ART.rust.hi} />
      <Bev t="polygon" points={pts([[56, 58], [56, 14], [88, 14], [88, 58]])} c="dark" o={0.8} />
      <polygon points={pts([[60, 58], [60, 18], [84, 18], [84, 58]])} fill={ART.ink} />
      <polygon points={pts([[56, 14], [40, 18], [40, 58], [56, 58]])} fill={ART.rust.base} /><polygon points={pts([[56, 14], [40, 18], [40, 20], [56, 16]])} fill={ART.rust.hi} /><Seams x={40} y={18} w={16} h={40} rows={2} op={0.4} />
      <g transform="rotate(-8 70 52)"><CrateBox x={62} y={44} w={16} h={12} c="steel" hz={false} cols={1} rows={0} /></g>
      <Slab x={64} y={54} w={12} h={4} k={0.8} c="amber" o={0.4} />
      <polygon points={pts([[20, 38], [36, 38], [36, 50], [20, 50]])} fill={ART.ink} opacity="0.45" />
      <text x="28" y="43.6" textAnchor="middle" fontSize="2.8" fill={S.warn} fontWeight="700" {...PT}>DO NOT</text><text x="28" y="48" textAnchor="middle" fontSize="2.8" fill={S.warn} fontWeight="700" {...PT}>OPEN THIS</text>
      <g stroke={S.orange} strokeWidth="1.1"><line x1="18" y1="34" x2="38" y2="52" /></g>
      <text x="22" y="26" fontSize="3.2" fill={ART.rust.hi} fontWeight="700" {...PT}>CZK-4471</text>
      <Rivets x={7} y={56} w={82} n={10} />
      <Grime x={4} y={8} w={88} h={50} seed={120} n={6} op={0.26} /><Scuff x={6} y={10} w={84} h={46} seed={121} n={8} c="rust" />
    </svg>
  );
}

function PropDrumArray() {
  const S = ART.signal;
  const drum = (x, y, k) => (
    <g key={k}>
      <Bev t="polygon" points={pts([[x, y + 22], [x, y + 5], [x + 14, y + 5], [x + 14, y + 22]])} c="teal" o={0.8} />
      <ellipse cx={x + 7} cy={y + 5} rx="7" ry="2.4" fill={ART.teal.hi} /><ellipse cx={x + 7} cy={y + 5.2} rx="4.6" ry="1.4" fill={ART.dark.shade} />
      <rect x={x} y={y + 10} width="14" height="1.6" fill={ART.dark.shade} /><rect x={x} y={y + 17} width="14" height="1.6" fill={ART.dark.shade} />
      <rect x={x + 3} y={y + 12} width="8" height="4.4" fill={ART.amber.base} /><text x={x + 7} y={y + 15.4} textAnchor="middle" fontSize="2" fill={ART.ink} fontWeight="700" {...PT}>T-4</text>
    </g>
  );
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {[0, 1, 2, 3, 4, 5].map((i) => drum(5 + i * 15, 12, 'a' + i))}
      {[0, 1, 2, 3, 4].map((i) => drum(12 + i * 15, 34, 'b' + i))}
      <text x="48" y="9" textAnchor="middle" fontSize="3" fill={S.warn} fontWeight="700" {...PT}>TYPE-4 REPULSOR FUEL x50</text>
      <polygon points={pts([[30, 56], [66, 56], [66, 62], [30, 62]])} fill={ART.red.base} /><text x="48" y="60.6" textAnchor="middle" fontSize="2.6" fill={S.white} fontWeight="700" {...PT}>REPUBLIC MILITARY SUPPLY</text>
      <Rivets x={6} y={63} w={84} n={10} />
      <Grime x={4} y={14} w={88} h={46} seed={130} n={6} op={0.22} /><Scuff x={6} y={16} w={84} h={40} seed={131} n={8} c="teal" />
    </svg>
  );
}

function PropHabBlock() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={91} rx={58} ry={3.4} />
      <Bev t="polygon" points={pts([[4, 88], [4, 8], [8, 4], [120, 4], [124, 8], [124, 88]])} c="steel" o={1.1} />
      <rect x="4" y="4" width="120" height="3" fill={ART.hull.hi} opacity="0.7" />
      {Array.from({ length: 48 }, (_, i) => {
        const col = i % 8, row = Math.floor(i / 8), x = 10 + col * 13.4, y = 12 + row * 10.6, lit = i !== 5 && i % 5 !== 3;
        return (
          <g key={i}>
            <Slab x={x} y={y} w={12} h={9.4} k={1.2} c="dark" o={0.5} />
            <rect x={x + 1.6} y={y + 1.8} width="8.8" height="1.6" fill={lit ? ART.amber.hi : ART.dark.shade} /><rect x={x + 1.6} y={y + 4.4} width="8.8" height="1.2" fill={ART.ink} opacity="0.7" />
            <circle cx={x + 9.6} cy={y + 7.6} r="0.8" fill={lit ? S.amber : S.green} />
            <text x={x + 1.6} y={y + 8.4} fontSize="1.8" fill={ART.beige.base} opacity="0.7" {...PT}>{i + 1}</text>
          </g>
        );
      })}
      <Slab x={10} y={76} w={44} h={10} k={1.2} c="dark" o={0.6} />
      <text x="32" y="81.4" textAnchor="middle" fontSize="3" fill={S.amberHi} fontWeight="700" {...PT}>OCCUPIED 39 / 48</text><text x="32" y="85" textAnchor="middle" fontSize="2.2" fill={S.amber} {...PT}>2 CR PER SLEEP CYCLE</text>
      <Slab x={64} y={77} w={26} h={9} k={1} c="wood" o={0.7} /><text x="77" y="82.6" textAnchor="middle" fontSize="2" fill={ART.paper.hi} {...PT}>MGR: CAPSULE 47</text>
      <Vent x={96} y={77} w={24} h={9} n={4} />
      <Rivets x={7} y={89} w={114} n={14} /><Seams x={4} y={4} w={120} h={84} cols={3} op={0.2} />
      <Grime x={4} y={6} w={120} h={82} seed={140} n={8} op={0.22} /><Scuff x={6} y={8} w={116} h={78} seed={141} n={10} c="steel" />
    </svg>
  );
}

function PropEvidenceLocker() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Slab x={3} y={3} w={90} h={56} k={3} c="deepglass" o={1} />
      {Array.from({ length: 12 }, (_, i) => {
        const col = i % 4, row = Math.floor(i / 4), x = 7 + col * 21.4, y = 8 + row * 16.6, hot = i === 6;
        return (
          <g key={i}>
            <Slab x={x} y={y} w={20} h={15} k={1.2} c="steel" o={0.7} />
            <Slab x={x + 2} y={y + 2} w={16} h={5} k={0.6} c="dark" o={0.4} /><Lights x={x + 4} y={y + 4.6} n={3} gap={4} r={0.7} colors={hot ? ['red'] : ['green']} blink={false} />
            <Slab x={x + 14} y={y + 9} w={4} h={4} k={0.6} c="brass" o={0.4} />
            {hot && <g><polygon points={pts([[x + 1, y + 9], [x + 12, y + 9], [x + 12, y + 14], [x + 1, y + 14]])} fill={S.red} /><text x={x + 6.5} y={y + 12.8} textAnchor="middle" fontSize="2.2" fill={S.white} fontWeight="700" {...PT}>CLOSED</text></g>}
          </g>
        );
      })}
      <polygon points={pts([[3, 54], [93, 54], [93, 59], [3, 59]])} fill={ART.dark.shade} /><text x="48" y="58" textAnchor="middle" fontSize="3" fill={S.cyanHi} fontWeight="700" {...PT}>BAY 14: CASE CLOSED. SEAL: CSF CODE</text>
      <g transform="rotate(-6 60 30)"><polygon points={pts([[52, 30], [70, 30], [70, 36], [52, 36]])} fill={ART.paper.base} /><text x="61" y="34.4" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} fontWeight="700" {...PT}>SEN 1182-C</text></g>
      <Rivets x={7} y={5} w={82} n={10} /><Seams x={3} y={3} w={90} h={56} cols={3} op={0.2} />
      <Grime x={4} y={6} w={88} h={50} seed={150} n={5} op={0.2} /><Scuff x={6} y={8} w={84} h={46} seed={151} n={8} c="steel" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// RACKS, BENCHES AND DOORS
// ---------------------------------------------------------------------------
function PropWeaponRack({ variant }) {
  const S = ART.signal;
  const pauld = variant === 'pauldron';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Slab x={3} y={3} w={90} h={56} k={3} c={pauld ? 'dark' : 'rust'} o={1} />
      <Seams x={3} y={3} w={90} h={56} cols={5} rows={2} op={0.25} />
      {Array.from({ length: 8 }, (_, i) => <circle key={i} cx={8 + i * 11.4} cy="9" r="0.9" fill={ART.dark.shade} />)}
      {pauld ? (
        <g>
          {Array.from({ length: 6 }, (_, i) => {
            const x = 8 + i * 14.4;
            return (
              <g key={i}>
                <Slab x={x + 4} y={34} w={4} h={16} k={0.8} c="steel" o={0.5} />
                <Bev t="polygon" points={pts([[x, 34], [x + 12, 34], [x + 13, 26], [x + 8, 17], [x + 4, 17], [x - 1, 26]])} c="hull" o={0.8} />
                <path d={`M${x + 1} 28 C${x + 3} 20 ${x + 9} 20 ${x + 11} 28`} fill="none" stroke={ART.hull.hi} strokeWidth="0.9" />
                <rect x={x} y="31" width="12" height="2" fill={S.crimson} opacity="0.85" />
                <Slab x={x + 1} y={50} w={10} h={3} k={0.6} c="steel" o={0.4} />
              </g>
            );
          })}
          <text x="48" y="14" textAnchor="middle" fontSize="3" fill={S.warn} fontWeight="700" {...PT}>PHRIK COMPOSITE x6</text>
          <text x="48" y="57.2" textAnchor="middle" fontSize="2.3" fill={ART.beige.hi} {...PT}>REPUBLIC TROOPER SIZING</text>
        </g>
      ) : (
        <g>
          <Bev t="rect" x="8" y="14" width="80" height="2.4" c="steel" o={0.6} /><Bev t="rect" x="8" y="34" width="80" height="2.4" c="steel" o={0.6} />
          {[0, 1, 2].map((i) => <g key={i}><Bev t="polygon" points={pts([[12 + i * 14, 14], [18 + i * 14, 14], [18 + i * 14, 20], [12 + i * 14, 20]])} c="dark" o={0.6} /><polygon points={pts([[14 + i * 14, 20], [16 + i * 14, 20], [16 + i * 14, 33], [14 + i * 14, 33]])} fill={S.cyanHi} opacity={0.85} /><polyline points={pts([[12 + i * 14, 15], [16 + i * 14, 18], [14 + i * 14, 20]])} fill="none" stroke={ART.ink} strokeWidth="0.6" /></g>)}
          {[0, 1, 2].map((i) => <g key={i}><polygon points={pts([[56 + i * 11, 36], [60 + i * 11, 36], [62 + i * 11, 52], [58 + i * 11, 53]])} fill={ART.steel.hi} /><Slab x={55 + i * 11} y={30} w={7} h={6} k={0.8} c="dark" o={0.5} /><rect x={57 + i * 11} y="31" width="3" height="2" fill={S.red} /></g>)}
          <Bev t="polygon" points={pts([[10, 42], [34, 42], [36, 46], [24, 48], [24, 54], [18, 54], [18, 48], [10, 46]])} c="steel" o={0.6} /><polyline points={pts([[12, 46], [20, 44]])} fill="none" stroke={ART.ink} strokeWidth="0.8" />
          <g transform="rotate(-4 78 14)"><polygon points={pts([[56, 5], [92, 5], [92, 13], [56, 13]])} fill={ART.paper.base} /><text x="74" y="9" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} fontWeight="700" {...PT}>FIGHT NIGHT ONLY.</text><text x="74" y="12" textAnchor="middle" fontSize="2.2" fill={ART.note.redInk} fontWeight="700" {...PT}>PAY FOR WHAT YOU BREAK.</text></g>
        </g>
      )}
      <Rivets x={7} y={56} w={82} n={10} />
      <Grime x={4} y={5} w={88} h={52} seed={pauld ? 160 : 162} n={6} op={0.22} /><Scuff x={6} y={7} w={84} h={48} seed={pauld ? 161 : 163} n={8} c="dark" />
    </svg>
  );
}

function PropMedicCrate() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={26} ry={3} />
      <polygon points={pts([[8, 28], [14, 14], [50, 14], [56, 28]])} fill={ART.steel.shade} />
      <CrateBox x={6} y={28} w={52} h={29} c="canvas" hz={false} cols={3} rows={0} />
      <polygon points={pts(ngon(20, 40, 8, 8, 8))} fill={ART.pearl.base} /><rect x="18.6" y="34" width="2.8" height="12" fill={ART.red.base} /><rect x="14" y="38.6" width="12" height="2.8" fill={ART.red.base} />
      {[0, 1, 2].map((i) => <polygon key={i} points={pts([[32 + i * 8, 22], [38 + i * 8, 22], [38 + i * 8, 27], [32 + i * 8, 27]])} fill={ART.teal.hi} />)}
      <Bev t="polygon" points={pts([[32, 36], [38, 36], [38, 50], [32, 50]])} c="pearl" o={0.6} /><rect x="32" y="36" width="6" height="3" fill={S.red} />
      {[0, 1].map((i) => <g key={i}><rect x={42 + i * 5} y="40" width="2.2" height="9" fill={S.cyanHi} /><rect x={41.6 + i * 5} y="38" width="3" height="2" fill={ART.steel.hi} /><line x1={43 + i * 5} y1="49" x2={43 + i * 5} y2="52" stroke={ART.steel.hi} strokeWidth="0.6" /></g>)}
      <g transform="rotate(-5 32 55)"><polygon points={pts([[14, 52], [50, 52], [50, 59], [14, 59]])} fill={ART.paper.base} /><text x="32" y="57.6" textAnchor="middle" fontSize="3" fill={ART.note.ink} fontWeight="700" {...PT}>NO QUESTIONS ASKED</text></g>
      <Rivets x={9} y={30} w={46} n={6} c="canvas" />
      <Grime x={6} y={28} w={52} h={30} seed={170} n={4} op={0.2} /><Scuff x={8} y={30} w={48} h={26} seed={171} n={6} c="canvas" />
    </svg>
  );
}

function PropScrapBin() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={26} ry={3} />
      <Bev t="polygon" points={pts([[6, 58], [10, 24], [54, 24], [58, 58]])} c="canvas" o={1} />
      <Seams x={8} y={24} w={48} h={34} cols={3} rows={1} op={0.4} />
      <polygon points={pts([[8, 24], [56, 24], [58, 20], [6, 20]])} fill={ART.canvas.hi} />
      <g transform="rotate(-14 8 20)"><Bev t="polygon" points={pts([[6, 22], [34, 22], [34, 14], [8, 14]])} c="steel" o={0.8} /></g>
      <path d="M14 22 C12 12 20 8 22 18" fill="none" stroke={ART.copper.base} strokeWidth="2" /><path d="M38 22 C42 10 50 12 46 22" fill="none" stroke={ART.dark.hi} strokeWidth="2.4" />
      <rect x="28" y="12" width="4" height="9" fill={ART.steel.base} transform="rotate(20 30 16)" /><Slab x={42} y={14} w={8} h={7} k={1} c="teal" o={0.5} />
      <g transform="rotate(12 22 30)"><Bev t="rect" x="14" y="26" width="9" height="12" c="dark" o={0.6} /><rect x="16" y="24.4" width="5" height="2" fill={ART.steel.hi} /><polyline points={pts([[15, 28], [19, 32], [17, 36], [21, 38]])} fill="none" stroke={S.amber} strokeWidth="0.7" /></g>
      <g transform="rotate(-8 40 34)"><polygon points={pts([[32, 30], [46, 30], [47, 37], [33, 38]])} fill={ART.paper.base} /><polygon points={pts([[44, 30], [47, 31], [46, 34], [44, 33]])} fill={ART.dark.base} /><rect x="34" y="32" width="8" height="2.4" fill={ART.red.base} /></g>
      <Slab x={10} y={44} w={44} h={9} k={1} c="dark" o={0.5} /><text x="32" y="50.4" textAnchor="middle" fontSize="3.2" fill={S.warn} fontWeight="700" {...PT}>SCRAP ONLY</text>
      <Rivets x={11} y={26} w={42} n={5} c="canvas" />
      <Grime x={6} y={24} w={52} h={34} seed={180} n={5} op={0.28} /><Scuff x={8} y={26} w={48} h={30} seed={181} n={7} c="canvas" />
    </svg>
  );
}

function PropFuelHose() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={3} />
      <Slab x={8} y={52} w={26} h={7} k={1.2} c="steel" />
      <Bev t="polygon" points={pts([[12, 52], [12, 10], [16, 6], [30, 6], [34, 10], [34, 52]])} c="rust" o={1} />
      <Seams x={12} y={6} w={22} h={46} rows={2} op={0.4} />
      <rect x="15" y="10" width="16" height="10" fill={ART.screen.amber} /><text x="23" y="16.6" textAnchor="middle" fontSize="3.6" fill={S.gold} fontWeight="700" {...PT}>FUEL</text>
      <rect x="15" y="14" width="16" height="1" fill={S.amber} opacity="0.4" />
      <polygon points={pts(ngon(23, 28, 5.4, 5.4, 10))} fill={ART.dark.shade} /><polygon points={pts(ngon(23, 28, 4, 4, 10))} fill={ART.paper.hi} /><line x1="23" y1="28" x2="26" y2="25" stroke={S.red} strokeWidth="0.9" />
      <Glyph k="drop" x={23} y={40} s={4} c={S.orange} />
      <Hazard x={13} y={46} w={20} h={4} />
      <Cable d="M34 24 C48 20 56 30 50 40 C46 48 36 46 38 54" c={ART.dark.base} w={3.4} />
      <Bev t="polygon" points={pts([[35, 54], [42, 54], [41, 60], [36, 60]])} c="brass" o={0.6} />
      <g fill={ART.pearl.base} opacity="0.55" style={{ animation:'steam-rise 3s ease-out infinite' }}><polygon points={pts(ngon(40, 50, 3.6, 2.6, 7))} /><polygon points={pts(ngon(44, 44, 4.4, 3, 7))} opacity="0.7" /></g>
      <g fill={ART.pearl.base} opacity="0.4" style={{ animation:'steam-rise 3.8s ease-out 1s infinite' }}><polygon points={pts(ngon(38, 46, 3, 2.2, 7))} /></g>
      <polygon points={pts([[36, 60], [38, 63], [40, 60]])} fill={S.amber} opacity="0.8" />
      <Rivets x={14} y={8} w={18} n={4} c="rust" />
      <Grime x={12} y={8} w={22} h={44} seed={190} n={5} op={0.28} /><Scuff x={12} y={10} w={22} h={40} seed={191} n={6} c="rust" />
    </svg>
  );
}

function PropStorageAlcoves() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Slab x={2} y={2} w={92} h={57} k={3} c="concrete" o={1} />
      {[0, 1, 2, 3, 4].map((i) => {
        const x = 6 + i * 17.6, hot = i === 2;
        return (
          <g key={i}>
            <Bev t="polygon" points={pts([[x, 56], [x, 22], [x + 3, 17], [x + 13, 17], [x + 16, 22], [x + 16, 56]])} c="dark" o={0.9} />
            <polygon points={pts([[x + 2, 56], [x + 2, 23], [x + 4, 19.6], [x + 12, 19.6], [x + 14, 23], [x + 14, 56]])} fill={hot ? ART.amber.shade : ART.ink} />
            <text x={x + 8} y="13.6" textAnchor="middle" fontSize="4" fill={hot ? S.amberHi : ART.beige.base} fontWeight="700" {...PT}>{i + 1}</text>
            {hot ? (
              <g>
                <Bev t="polygon" points={pts([[x + 3, 56], [x + 3, 36], [x + 13, 36], [x + 13, 56]])} c="steel" o={0.6} />
                <Slab x={x + 5} y={42} w={6} h={7} k={0.8} c="red" o={0.4} /><circle cx={x + 8} cy="45.5" r="1.4" fill={S.red} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} />
                <Glow cx={x + 8} cy={28} rx={5} ry={6} c={S.cyan} opacity={0.5} /><rect x={x + 6.4} y="24" width="3.2" height="9" fill={ART.pearl.base} /><rect x={x + 6.4} y="24" width="3.2" height="2" fill={ART.steel.hi} />
              </g>
            ) : <g><rect x={x + 4} y="50" width="8" height="5" fill={ART.dark.base} /></g>}
          </g>
        );
      })}
      <text x="48" y="8" textAnchor="middle" fontSize="3" fill={S.warn} fontWeight="700" {...PT}>RESTRICTED STORAGE: THIRD ON THE LEFT</text>
      <Hazard x={4} y={57} w={88} h={2.6} /><Rivets x={6} y={4} w={84} n={10} /><Seams x={2} y={2} w={92} h={57} cols={4} op={0.2} />
      <Grime x={4} y={6} w={88} h={50} seed={200} n={6} op={0.24} /><Scuff x={6} y={8} w={84} h={46} seed={201} n={8} c="concrete" />
    </svg>
  );
}

function PropArchiveCabinet({ variant }) {
  const S = ART.signal;
  let body = null;
  if (variant === 'dossier') {
    body = (
      <g>
        <Slab x={12} y={6} w={40} h={52} k={2} c="steel" o={1} />
        {[0, 1, 2, 3].map((i) => <g key={i}><Slab x={15} y={9 + i * 12.4} w={34} h={11} k={1} c="dark" o={0.5} /><Slab x={26} y={13 + i * 12.4} w={12} h={3} k={0.6} c="steel" o={0.4} /><rect x="17" y={10.6 + i * 12.4} width="9" height="3.4" fill={ART.paper.base} /></g>)}
        <rect x="15" y="22" width="34" height="3" fill={S.red} /><text x="32" y="24.4" textAnchor="middle" fontSize="2.3" fill={S.white} fontWeight="700" {...PT}>CLASSIFIED</text>
        <g transform="rotate(6 42 20)"><polygon points={pts([[34, 8], [50, 8], [50, 18], [34, 18]])} fill={ART.paper.hi} /><text x="42" y="12.6" textAnchor="middle" fontSize="2" fill={ART.note.ink} fontWeight="700" {...PT}>DOSSIER 7-THETA</text><rect x="35" y="14" width="14" height="1" fill={ART.note.redInk} /></g>
      </g>
    );
  } else if (variant === 'sealed') {
    body = (
      <g>
        <Bev t="polygon" points={pts([[8, 58], [8, 14], [14, 6], [50, 6], [56, 14], [56, 58]])} c="pearl" o={1} />
        <polygon points={pts(ngon(32, 32, 18, 18, 12))} fill={ART.steel.shade} /><Bev t="polygon" points={pts(ngon(32, 32, 16, 16, 12))} c="steel" o={0.8} />
        <Glyph k="cog" x={32} y={32} s={9} c={ART.hull.hi} />
        <rect x="14" y="12" width="36" height="5" fill={ART.screen.red} /><text x="32" y="16" textAnchor="middle" fontSize="2.8" fill={S.redHi} fontWeight="700" {...PT}>SESSION SEALED</text>
        <circle cx="32" cy="54" r="2" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} /><text x="32" y="58" textAnchor="middle" fontSize="2" fill={ART.ink} {...PT}>VOTE 11 TO 1</text>
        {Array.from({ length: 12 }, (_, i) => <rect key={i} x={13 + i * 3} y="46" width="2" height="2" fill={i === 7 ? ART.dark.base : S.cyan} opacity="0.9" />)}
      </g>
    );
  } else if (variant === 'minutes') {
    body = (
      <g>
        <Slab x={8} y={4} w={48} h={55} k={2} c="dark" o={1} />
        {[0, 1, 2].map((r) => Array.from({ length: 5 }, (_, i) => { const x = 11 + i * 9.2, y = 8 + r * 17; return <g key={r + '_' + i}><Bev t="polygon" points={pts([[x, y + 15], [x, y + 2], [x + 7, y + 2], [x + 7, y + 15]])} c={i === 2 && r === 1 ? 'red' : 'steel'} o={0.6} /><ellipse cx={x + 3.5} cy={y + 2} rx="3.5" ry="1.2" fill={ART.steel.hi} /><rect x={x + 1} y={y + 6} width="5" height="3.6" fill={ART.paper.base} /></g>; }))}
        {[0, 1].map((i) => <rect key={i} x="9" y={25 + i * 17} width="46" height="1.6" fill={ART.dark.hi} />)}
        <text x="32" y="57.4" textAnchor="middle" fontSize="2.4" fill={S.cyanHi} {...PT}>TORVENN x23</text>
      </g>
    );
  } else {
    body = (
      <g>
        <Slab x={26} y={34} w={12} h={22} k={1.2} c="bronze" /><Slab x={14} y={54} w={36} h={5} k={1.2} c="bronze" o={0.8} />
        <Bev t="polygon" points={pts([[6, 36], [58, 36], [54, 14], [10, 14]])} c="bronze" o={1} />
        <polygon points={pts([[10, 35], [32, 31], [32, 16], [10, 18]])} fill={ART.paper.base} /><polygon points={pts([[54, 35], [32, 31], [32, 16], [54, 18]])} fill={ART.paper.hi} />
        <line x1="32" y1="16" x2="32" y2="31" stroke={ART.paper.shade} strokeWidth="0.8" />
        {[0, 1, 2, 3, 4].map((i) => <g key={i}><line x1="13" y1={21 + i * 2.8} x2="30" y2={19.6 + i * 2.8} stroke={ART.note.blueInk} strokeWidth="0.4" /><line x1="35" y1={19.6 + i * 2.8} x2="51" y2={21 + i * 2.8} stroke={ART.note.blueInk} strokeWidth="0.4" /></g>)}
        <Glyph k="cog" x={44} y={28} s={3.6} c={ART.red.base} />
        <text x="21" y="33.4" textAnchor="middle" fontSize="2" fill={ART.note.ink} {...PT}>43 ACCOUNTS</text>
        <Bev t="polygon" points={pts([[52, 20], [60, 14], [62, 17], [56, 24]])} c="brass" o={0.5} /><polygon points={pts([[56, 22], [62, 17], [64, 21]])} fill={S.amberHi} opacity="0.7" />
      </g>
    );
  }
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={3} />
      {body}
      <Rivets x={11} y={8} w={42} n={5} />
      <Grime x={8} y={8} w={48} h={50} seed={(variant || 'a').length + 210} n={4} op={0.2} /><Scuff x={10} y={10} w={44} h={46} seed={(variant || 'a').length + 211} n={6} />
    </svg>
  );
}

function PropWorkbench({ variant }) {
  const S = ART.signal;
  const arms = variant === 'arms';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {[10, 78].map((x) => <Bev key={x} t="polygon" points={pts([[x, 60], [x, 38], [x + 8, 38], [x + 8, 60]])} c="dark" o={0.7} />)}
      <Bev t="polygon" points={pts([[4, 42], [92, 42], [88, 34], [8, 34]])} c={arms ? 'steel' : 'dark'} o={1} />
      <Slab x={4} y={42} w={88} h={7} k={1} c="dark" o={0.7} />
      <Seams x={4} y={42} w={88} h={7} cols={7} op={0.4} />
      {arms ? (
        <g>
          <polygon points={pts([[8, 34], [28, 34], [28, 20], [8, 20]])} fill={ART.dark.shade} />
          <Bev t="polygon" points={pts([[10, 32], [26, 32], [26, 26], [18, 22], [10, 24]])} c="steel" o={0.6} /><rect x="12" y="26" width="10" height="2" fill={S.amber} />
          <Bev t="polygon" points={pts([[34, 32], [64, 32], [66, 29], [58, 27], [36, 27], [34, 29]])} c="dark" o={0.6} /><rect x="38" y="29" width="18" height="1.6" fill={S.cyan} />
          <Bev t="polygon" points={pts([[70, 33], [86, 33], [86, 24], [78, 22], [70, 26]])} c="hull" o={0.6} /><polygon points={pts([[72, 28], [84, 28], [84, 30], [72, 30]])} fill={ART.dark.shade} />
          <Bev t="polygon" points={pts([[66, 20], [74, 14], [78, 16], [70, 24]])} c="amber" o={0.5} />
          {[0, 1, 2, 3].map((i) => <polygon key={i} points={pts([[32 + i * 7, 22], [36 + i * 7, 22], [37 + i * 7, 26], [31 + i * 7, 26]])} fill={[S.cyan, S.amber, S.green, S.red][i]} opacity="0.85" />)}
          <line x1="40" y1="14" x2="40" y2="8" stroke={ART.steel.hi} strokeWidth="1.6" /><polygon points={pts([[36, 8], [44, 8], [42, 5], [38, 5]])} fill={ART.steel.base} />
        </g>
      ) : (
        <g>
          <Bev t="polygon" points={pts([[8, 32], [40, 32], [40, 12], [8, 12]])} c="dark" o={0.8} /><rect x="10" y="14" width="28" height="16" fill={ART.screen.green} />
          {[0, 1, 2, 3].map((i) => <text key={i} x="12" y={19.6 + i * 3.6} fontSize="2.4" fill={S.greenHi} opacity={0.95 - i * 0.12} {...PT}>{['> BYPASS 0x7F', '> ROUTE HUB C', '> SCHEDULE 48H', '> 03:00 ...'][i]}</text>)}
          {[[48, 'amber'], [54, 'cyan'], [60, 'green'], [66, 'red'], [72, 'amber']].map(([x, k], i) => <g key={i}><Slab x={x} y={28 + (i % 2)} w={5} h={4} k={0.6} c="dark" o={0.4} /><rect x={x + 0.8} y={29 + (i % 2)} width="3.4" height="1.2" fill={S[k]} /></g>)}
          <Cable d="M46 31 C56 20 66 36 78 30 C84 26 88 32 90 28" w={1.6} /><Cable d="M44 33 C50 26 62 38 70 33" w={1.4} hi="copper" />
          <polygon points={pts([[78, 30], [86, 20], [88, 22], [80, 32]])} fill={ART.steel.hi} /><polygon points={pts([[85, 20], [89, 18], [89, 22]])} fill={S.orange} />
          <Bev t="polygon" points={pts([[82, 18], [92, 18], [92, 8], [82, 8]])} c="amber" o={0.5} /><text x="87" y="15" textAnchor="middle" fontSize="3.6" fill={ART.ink} fontWeight="700" {...PT}>0300</text>
        </g>
      )}
      <Vent x={22} y={51} w={20} h={7} n={3} /><Vent x={54} y={51} w={20} h={7} n={3} /><Rivets x={7} y={44} w={82} n={10} />
      <Grime x={4} y={34} w={88} h={25} seed={arms ? 220 : 222} n={6} op={0.24} /><Scuff x={6} y={36} w={84} h={18} seed={arms ? 221 : 223} n={8} c="steel" />
    </svg>
  );
}

function PropCargoLift() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3} />
      <polygon points={pts(ngon(48, 46, 44, 13, 10, Math.PI / 10))} fill={ART.dark.shade} />
      <Bev t="polygon" points={pts(ngon(48, 44, 42, 12, 10, Math.PI / 10))} c="steel" o={1.1} />
      <polygon points={pts(ngon(48, 43, 34, 9, 10, Math.PI / 10))} fill={ART.dark.base} />
      <polygon points={pts(ngon(48, 43, 26, 6.6, 10, Math.PI / 10))} fill={ART.dark.shade} />
      <Hazard x={6} y={50} w={84} h={5} />
      <Glow cx={48} cy={43} rx={22} ry={6} c={S.cyan} opacity={0.5} style={{ animation:'ship-engine 2.4s ease-in-out infinite' }} />
      {[[20, 26], [38, 20], [58, 24]].map(([x, y], i) => <g key={i}><CrateBox x={x} y={y} w={18} h={20} c="steel" hz={false} k={1.2} cols={1} rows={0} /><path d={`M${x + 3} ${y + 12} C${x + 3} ${y + 5} ${x + 15} ${y + 5} ${x + 15} ${y + 12}`} fill="none" stroke={ART.hull.hi} strokeWidth="1.2" /></g>)}
      <Slab x={74} y={22} w={16} h={14} k={1.2} c="dark" o={0.6} /><rect x="76" y="24" width="12" height="5" fill={ART.screen.amber} /><text x="82" y="28" textAnchor="middle" fontSize="2.6" fill={S.gold} fontWeight="700" {...PT}>LIFT RIGGED</text>
      <Cable d="M80 36 C70 40 60 46 58 54" w={2} /><Cable d="M84 36 C90 44 80 52 70 56" w={1.8} hi="copper" />
      <polygon points={pts([[8, 30], [14, 22], [20, 30]])} fill={S.amber} opacity="0.85" /><polygon points={pts([[8, 38], [14, 30], [20, 38]])} fill={S.amber} opacity="0.55" />
      <Rivets x={10} y={52} w={76} n={10} />
      <Grime x={4} y={18} w={88} h={42} seed={230} n={5} op={0.2} /><Scuff x={6} y={20} w={84} h={38} seed={231} n={8} c="steel" />
    </svg>
  );
}

function PropVaultDoor() {
  const S = ART.signal;
  const spokes = Array.from({ length: 6 }, (_, i) => { const a = (i / 6) * Math.PI * 2; return [48 + Math.cos(a) * 14, 50 + Math.sin(a) * 14]; });
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={91} rx={44} ry={3.4} />
      <Slab x={3} y={3} w={90} h={86} k={4} c="dark" o={1.2} />
      <Seams x={3} y={3} w={90} h={86} cols={3} rows={3} op={0.4} />
      <polygon points={pts(ngon(48, 48, 38, 38, 16, Math.PI / 16))} fill={ART.ink} />
      <Bev t="polygon" points={pts(ngon(48, 48, 36, 36, 16, Math.PI / 16))} c="steel" o={1.2} />
      <polygon points={pts(ngon(48, 48, 29, 29, 16, Math.PI / 16))} fill={ART.steel.shade} />
      <Bev t="polygon" points={pts(ngon(48, 48, 25, 25, 16, Math.PI / 16))} c="hull" o={0.9} />
      {Array.from({ length: 12 }, (_, i) => { const a = (i / 12) * Math.PI * 2; return <circle key={i} cx={48 + Math.cos(a) * 32.4} cy={48 + Math.sin(a) * 32.4} r="1.6" fill={ART.steel.hi} />; })}
      <polygon points={pts(ngon(48, 48, 10, 10, 8))} fill={ART.dark.shade} /><polygon points={pts(ngon(48, 48, 7, 7, 8))} fill={ART.steel.base} />
      {spokes.map(([x, y], i) => <g key={i}><line x1="48" y1="50" x2={x} y2={y} stroke={ART.dark.shade} strokeWidth="4" /><line x1="48" y1="49" x2={x} y2={y - 1} stroke={ART.steel.hi} strokeWidth="1.4" /><polygon points={pts(ngon(x, y, 3, 3, 6))} fill={ART.steel.base} /></g>)}
      {/* corroded secondary hinge */}
      <Bev t="polygon" points={pts([[80, 24], [92, 24], [92, 40], [80, 40]])} c="copper" o={0.8} />
      <polygon points={pts([[80, 30], [92, 28], [92, 36], [80, 38]])} fill={ART.moss.base} opacity="0.85" /><polygon points={pts([[82, 24], [88, 24], [86, 30]])} fill={ART.moss.hi} opacity="0.8" />
      <Glow cx={86} cy={32} rx={8} ry={8} c={S.amber} opacity={0.45} /><text x="86" y="45" textAnchor="middle" fontSize="2.2" fill={S.amberHi} {...PT}>HINGE 2</text>
      <Slab x={30} y={80} w={36} h={7} k={1} c="dark" o={0.7} /><text x="48" y="85.2" textAnchor="middle" fontSize="3.4" fill={S.warn} fontWeight="700" {...PT}>PHRIK REINFORCED</text>
      <Hazard x={6} y={90} w={84} h={3} /><Rivets x={7} y={6} w={82} n={10} /><Lights x={8} y={81} n={4} gap={4} seed={5} />
      <Grime x={4} y={6} w={88} h={82} seed={240} n={8} op={0.24} /><Scuff x={6} y={8} w={84} h={78} seed={241} n={10} c="steel" />
    </svg>
  );
}

function PropSecurityGate() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[3, 60], [3, 8], [9, 3], [87, 3], [93, 8], [93, 60]])} c="dark" o={1} />
      <polygon points={pts([[10, 60], [10, 11], [14, 8], [82, 8], [86, 11], [86, 60]])} fill={ART.ink} />
      {Array.from({ length: 11 }, (_, i) => <g key={i}><rect x={13 + i * 6.4} y="9" width="3" height="51" fill={ART.steel.base} /><rect x={13 + i * 6.4} y="9" width="1" height="51" fill={ART.steel.hi} /></g>)}
      <rect x="10" y="24" width="76" height="3" fill={ART.steel.shade} /><rect x="10" y="44" width="76" height="3" fill={ART.steel.shade} />
      <g fill={ART.rust.base} opacity="0.8"><polygon points={pts([[13, 9], [30, 9], [26, 30], [16, 52]])} opacity="0.5" /><polygon points={pts([[60, 60], [86, 60], [86, 38], [70, 50]])} opacity="0.6" /></g>
      <Bev t="polygon" points={pts([[34, 22], [62, 22], [62, 52], [34, 52]])} c="rust" o={0.9} />
      <polygon points={pts(ngon(48, 33, 7, 7, 12))} fill={ART.rust.shade} /><Glyph k="cog" x={48} y={33} s={6} c={ART.beige.base} />
      <text x="48" y="45" textAnchor="middle" fontSize="2.6" fill={ART.beige.hi} fontWeight="700" {...PT}>CSF ACCESS GATE</text>
      <Slab x={64} y={26} w={18} h={22} k={1.2} c="dark" o={0.7} /><rect x="66" y="28" width="14" height="6" fill={ART.screen.red} /><circle cx="73" cy="31" r="1.6" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      <g stroke={S.cyan} strokeWidth="0.6" fill="none"><polygon points={pts(ngon(73, 41, 4, 4, 6))} /><line x1="69" y1="41" x2="77" y2="41" /></g>
      <Slab x={12} y={28} w={18} h={14} k={1} c="dark" o={0.6} /><Toggles x={14} y={30} cols={4} rows={3} gap={3.4} seed={8} />
      <Hazard x={6} y={55} w={84} h={3} /><Rivets x={8} y={5} w={80} n={10} />
      <Grime x={4} y={6} w={88} h={52} seed={250} n={7} op={0.3} /><Scuff x={6} y={8} w={84} h={48} seed={251} n={8} c="steel" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// INFRASTRUCTURE: junction boxes, pipes, valves, vents, hatches, scanners, dampers.
// ---------------------------------------------------------------------------
function PropJunctionBox({ variant }) {
  const S = ART.signal;
  const cfg = {
    csf:  { body:'deepglass', scr:'cyan',  t:'PATROL ROUTING', r:['FW: 3 CYCLES OLD', 'GAP 14-SUB 12 MIN'], g:'badge' },
    rail: { body:'amber',     scr:'amber', t:'RAIL SWITCHING', r:['HOLD: 12 MIN / 4TH', 'CAMS: MAINT LOOP'], g:'gear' },
    vent: { body:'steel',     scr:'green', t:'VENT SPINE 088', r:['L005 AIR: REROUTED', 'MONITOR: BLIND'], g:'wave' },
  }[variant] || {};
  const s = SCR[cfg.scr];
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={24} ry={3} />
      <Cable d="M18 6 L18 0" w={3} /><Cable d="M46 6 C46 3 50 2 50 0" w={2.4} />
      <Slab x={8} y={6} w={48} h={50} k={2.4} c={cfg.body} o={1} />
      <Seams x={8} y={6} w={48} h={50} cols={1} rows={2} op={0.3} />
      <Screen x={13} y={11} w={38} h={19} s={s} title={cfg.t} rows={cfg.r} fs={2.2} scan={false} flick />
      <Glyph k={cfg.g} x={46} y={26} s={2.6} c={s.ink} />
      <Slab x={13} y={34} w={20} h={13} k={1} c="dark" o={0.6} /><Toggles x={15} y={36} cols={5} rows={3} gap={3.6} seed={variant === 'rail' ? 5 : 8} />
      <g><Slab x={38} y={34} w={14} h={18} k={1} c="dark" o={0.6} /><rect x="44" y="37" width="2" height="10" fill={ART.steel.hi} /><polygon points={pts([[41, 37], [49, 37], [45, 33]])} fill={S.red} /></g>
      {variant === 'vent' && <g fill="none" stroke={ART.steel.hi} strokeWidth="0.8"><path d="M16 52 L24 52 M20 49 L24 52 L20 55" /><path d="M30 52 L38 52" /></g>}
      <Hazard x={10} y={52} w={44} h={3} /><Rivets x={11} y={8} w={42} n={6} />
      <Slab x={10} y={57} w={8} h={3} k={0.6} c="dark" o={0.4} /><Slab x={46} y={57} w={8} h={3} k={0.6} c="dark" o={0.4} />
      <Grime x={8} y={8} w={48} h={48} seed={(variant || 'j').length + 260} n={5} op={0.22} /><Scuff x={10} y={10} w={44} h={44} seed={(variant || 'j').length + 261} n={7} c={cfg.body} />
    </svg>
  );
}

function PropLeakingPipe({ variant }) {
  const S = ART.signal;
  const cool = variant === 'coolant';
  const pipe = cool ? 'teal' : 'rust';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {cool ? (
        <g fill={ART.pearl.hi} opacity="0.5" style={{ animation:'steam-rise 3.2s ease-out infinite' }}><polygon points={pts(ngon(46, 26, 7, 4, 8))} /><polygon points={pts(ngon(54, 18, 6, 3.6, 8))} opacity="0.7" /></g>
      ) : (
        <g>
          <polygon points={pts([[30, 62], [30, 46], [44, 40], [66, 42], [80, 48], [84, 62]])} fill={S.lime} opacity="0.2" />
          <polygon points={pts([[40, 62], [42, 50], [54, 46], [68, 50], [70, 62]])} fill={S.lime} opacity="0.28" />
        </g>
      )}
      <Bev t="polygon" points={pts([[2, 50], [2, 26], [94, 26], [94, 50]])} c={pipe} o={1.2} />
      <rect x="2" y="26" width="92" height="3" fill={ART[pipe].hi} />
      <rect x="2" y="46" width="92" height="3" fill={ART[pipe].shade} />
      {[14, 46, 78].map((x) => <g key={x}><Bev t="polygon" points={pts([[x - 4, 54], [x - 4, 22], [x + 4, 22], [x + 4, 54]])} c="steel" o={0.8} /><Rivets x={x - 2.4} y={26} w={0} n={1} /><circle cx={x} cy="30" r="0.9" fill={ART.steel.hi} /><circle cx={x} cy="46" r="0.9" fill={ART.steel.hi} /></g>)}
      <Slab x={36} y={52} w={24} h={8} k={1} c="dark" o={0.5} />
      <polygon points={pts([[44, 50], [48, 56], [52, 50]])} fill={cool ? ART.pearl.hi : S.lime} />
      {cool ? (
        <g>
          <polygon points={pts([[10, 26], [24, 26], [20, 34], [12, 36]])} fill={ART.pearl.hi} opacity="0.85" /><polygon points={pts([[60, 26], [76, 26], [72, 33], [64, 31]])} fill={ART.pearl.hi} opacity="0.85" /><polygon points={pts([[84, 38], [94, 36], [94, 50], [86, 48]])} fill={ART.pearl.hi} opacity="0.7" />
          <polyline points={pts([[40, 28], [44, 36], [42, 42], [48, 50]])} fill="none" stroke={ART.ink} strokeWidth="0.8" />
          <text x="30" y="60" fontSize="2.6" fill={S.cyanHi} {...PT}>COOLANT: BLEEDING</text>
        </g>
      ) : (
        <g>
          <polyline points={pts([[38, 28], [46, 38], [42, 44], [52, 50]])} fill="none" stroke={ART.ink} strokeWidth="1" />
          <Glow cx={48} cy={48} rx={9} ry={5} c={S.lime} opacity={0.7} style={{ animation:'ship-engine 2.2s ease-in-out infinite' }} />
          <g stroke={ART.ink} strokeWidth="0.6" opacity="0.5"><path d="M20 22 C22 20 18 18 20 16" fill="none" /><path d="M70 22 C72 20 68 18 70 16" fill="none" /></g>
          <text x="4" y="14" fontSize="3" fill={S.warn} fontWeight="700" {...PT}>CHEMICAL HAZE: ACRID</text>
        </g>
      )}
      <Hazard x={4} y={50} w={26} h={3} />
      <Seams x={2} y={26} w={92} h={24} cols={5} op={0.35} />
      <Grime x={2} y={26} w={92} h={34} seed={cool ? 270 : 272} n={6} op={0.25} /><Scuff x={4} y={28} w={88} h={20} seed={cool ? 271 : 273} n={8} c={pipe} />
    </svg>
  );
}

function PropPressureValve({ variant }) {
  const S = ART.signal;
  const crit = variant === 'a', steam = variant === 'b', stable = variant === 'c', refinery = variant === 'refinery';
  const needle = crit ? 62 : steam ? 18 : stable ? -28 : 6;
  const wheel = ngon(32, 22, 15, 6, 8, Math.PI / 8);
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={3} />
      {steam && <g fill={ART.pearl.base} opacity="0.6" style={{ animation:'steam-rise 2.4s ease-out infinite' }}><polygon points={pts(ngon(32, 22, 7, 4, 8))} /><polygon points={pts(ngon(40, 14, 6, 3.4, 8))} opacity="0.7" /></g>}
      <Bev t="polygon" points={pts([[2, 58], [2, 40], [62, 40], [62, 58]])} c={refinery ? 'bronze' : 'teal'} o={1.1} />
      <rect x="2" y="40" width="60" height="2.6" fill={ART[refinery ? 'bronze' : 'teal'].hi} />
      <Bev t="polygon" points={pts([[24, 44], [24, 26], [40, 26], [40, 44]])} c="steel" o={0.9} />
      <Slab x={0} y={42} w={8} h={14} k={1} c="steel" o={0.7} /><Slab x={56} y={42} w={8} h={14} k={1} c="steel" o={0.7} />
      <line x1="32" y1="24" x2="32" y2="30" stroke={ART.dark.shade} strokeWidth="3" />
      <polygon points={pts(wheel)} fill={ART.ink} opacity="0.5" transform="translate(1 2)" />
      <Bev t="polygon" points={pts(wheel)} c={crit ? 'red' : refinery ? 'bronze' : 'red'} o={0.8} />
      <polygon points={pts(ngon(32, 22, 11, 3.6, 8, Math.PI / 8))} fill={ART.dark.shade} />
      <polygon points={pts(ngon(32, 21.6, 4, 1.6, 8))} fill={ART.steel.base} />
      {[0, 1, 2].map((i) => <line key={i} x1={32 + (i - 1) * 9} y1="22" x2={32 + (i - 1) * 12} y2="22" stroke={ART.red.hi} strokeWidth="1.2" />)}
      {/* gauge */}
      <polygon points={pts(ngon(14, 46, 8, 8, 12))} fill={ART.dark.shade} /><polygon points={pts(ngon(14, 46, 6.4, 6.4, 12))} fill={ART.paper.hi} />
      <path d="M8 46 A6 6 0 0 1 20 46" fill="none" stroke={S.green} strokeWidth="1.4" /><path d="M16 41 A6 6 0 0 1 20 46" fill="none" stroke={S.red} strokeWidth="1.6" />
      <line x1="14" y1="46" x2={14 + Math.cos((needle - 90) * Math.PI / 180) * 5.4} y2={46 + Math.sin((needle - 90) * Math.PI / 180) * 5.4} stroke={ART.ink} strokeWidth="0.9" /><circle cx="14" cy="46" r="1" fill={ART.ink} />
      {crit && <g><circle cx="52" cy="46" r="2.6" fill={S.red} style={{ animation:'ship-blink 0.9s steps(2) infinite' }} /><text x="52" y="54" textAnchor="middle" fontSize="2.4" fill={S.redHi} fontWeight="700" {...PT}>CRIT</text></g>}
      {stable && <g><circle cx="52" cy="46" r="2.6" fill={S.green} /><text x="52" y="54" textAnchor="middle" fontSize="2.4" fill={S.greenHi} fontWeight="700" {...PT}>OK</text></g>}
      {steam && <g><circle cx="52" cy="46" r="2.6" fill={S.amber} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} /><text x="52" y="54" textAnchor="middle" fontSize="2.4" fill={S.amberHi} fontWeight="700" {...PT}>LEAK</text></g>}
      {refinery && <g>{[[46, 'magenta'], [54, 'violet']].map(([x, k]) => <g key={x}><Bev t="polygon" points={pts(ngon(x, 24, 6, 2.6, 6))} c="bronze" o={0.5} /><rect x={x - 1} y="24" width="2" height="12" fill={ART.dark.shade} /><circle cx={x} cy="48" r="2" fill={S[k]} /></g>)}<Glow cx={32} cy={46} rx={14} ry={4} c={S.violet} opacity={0.35} /></g>}
      <Hazard x={4} y={56} w={56} h={3} /><Rivets x={6} y={43} w={52} n={8} c="teal" />
      <Grime x={2} y={40} w={60} h={19} seed={(variant || 'v').length + 280} n={5} op={0.25} /><Scuff x={4} y={42} w={56} h={14} seed={(variant || 'v').length + 281} n={6} c="teal" />
    </svg>
  );
}

function PropSteamVent() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={26} ry={4} />
      {[[30, 24, 9, 0], [40, 16, 8, 1.2], [26, 12, 7, 2.2]].map(([x, y, r, d], i) => <g key={i} fill={ART.pearl.hi} opacity="0.55" style={{ animation:`steam-rise 3.4s ease-out ${d}s infinite` }}><polygon points={pts(ngon(x, y, r, r * 0.66, 8))} /></g>)}
      <polygon points={pts(ngon(28, 48, 25, 9, 8, Math.PI / 8))} fill={ART.dark.shade} />
      <Bev t="polygon" points={pts(ngon(28, 46, 23, 8, 8, Math.PI / 8))} c="steel" o={1} />
      <polygon points={pts(ngon(28, 45.4, 18, 5.6, 8, Math.PI / 8))} fill={ART.ink} />
      {Array.from({ length: 7 }, (_, i) => <rect key={i} x={13 + i * 4.2} y="41" width="2" height="9" fill={ART.steel.base} />)}
      <Glow cx={28} cy={44} rx={14} ry={4} c={S.amber} opacity={0.4} />
      <Slab x={46} y={36} w={14} h={20} k={1.2} c="dark" o={0.7} /><rect x="48" y="38" width="10" height="7" fill={ART.screen.amber} /><text x="53" y="42" textAnchor="middle" fontSize="2" fill={S.gold} {...PT}>SERVICE</text><text x="53" y="44.2" textAnchor="middle" fontSize="2" fill={S.gold} {...PT}>11 MO</text>
      <Lights x={49} y={49} n={3} gap={3.4} seed={3} /><Hazard x={47} y={52} w={12} h={3} />
      <Rivets x={10} y={54} w={36} n={6} />
      <Grime x={4} y={38} w={56} h={20} seed={290} n={5} op={0.28} /><Scuff x={6} y={40} w={50} h={14} seed={291} n={6} />
    </svg>
  );
}

function PropAccessHatch({ variant }) {
  const S = ART.signal;
  const sealed = variant === 'sealed', ancient = variant === 'ancient', crawl = variant === 'crawl';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={26} ry={4} />
      {crawl || variant === 'panel' ? (
        <g>
          <Slab x={6} y={8} w={52} h={48} k={3} c="concrete" o={1} />
          <rect x="12" y="14" width="40" height="36" fill={ART.ink} />
          <g stroke={ART.steel.shade} strokeWidth="1.2">{[22, 30, 38, 46].map((y) => <line key={y} x1="14" y1={y} x2="50" y2={y} />)}</g>
          {variant === 'panel' && <g><rect x="22" y="14" width="3" height="36" fill={ART.steel.base} /><rect x="39" y="14" width="3" height="36" fill={ART.steel.base} />{[16, 24, 32, 40, 48].map((y) => <rect key={y} x="22" y={y} width="20" height="1.6" fill={ART.steel.hi} />)}</g>}
          <polygon points={pts([[12, 14], [2, 18], [2, 54], [12, 50]])} fill={ART.steel.base} /><polygon points={pts([[12, 14], [2, 18], [2, 22], [12, 18]])} fill={ART.steel.hi} /><Seams x={2} y={18} w={10} h={36} rows={2} op={0.4} />
          {crawl && <g><rect x="9" y="22" width="3" height="3" fill={S.amberHi} /><rect x="9" y="42" width="3" height="3" fill={S.amberHi} /><text x="32" y="12" textAnchor="middle" fontSize="2.6" fill={S.amberHi} {...PT}>HINGES: FRESHLY OILED</text></g>}
          {variant === 'panel' && <g fill={ART.beige.base} opacity="0.75">{[[28, 30], [34, 38], [30, 44]].map(([x, y], i) => <ellipse key={i} cx={x} cy={y} rx="3" ry="1.2" />)}<text x="32" y="12" textAnchor="middle" fontSize="2.6" fill={S.amberHi} {...PT}>DUST DISTURBED</text></g>}
          <Rivets x={9} y={10} w={46} n={7} />
        </g>
      ) : (
        <g>
          <polygon points={pts(ngon(32, 36, 28, 18, 14, Math.PI / 14))} fill={ART.dark.shade} />
          <Bev t="polygon" points={pts(ngon(32, 34, 26, 16, 14, Math.PI / 14))} c={ancient ? 'brass' : 'steel'} o={1.1} />
          <polygon points={pts(ngon(32, 33, 20, 11.6, 14, Math.PI / 14))} fill={ancient ? ART.bronze.base : ART.steel.base} />
          {Array.from({ length: 10 }, (_, i) => { const a = (i / 10) * Math.PI * 2; return <circle key={i} cx={32 + Math.cos(a) * 23} cy={34 + Math.sin(a) * 13.6} r="1.3" fill={ancient ? ART.brass.hi : ART.steel.hi} />; })}
          {sealed && [[-1, 'a'], [0, 'b'], [1, 'c']].map(([i, k]) => <g key={k}><polygon points={pts([[32 + i * 12 - 2, 26], [32 + i * 12 + 2, 26], [32 + i * 12 + 4, 40], [32 + i * 12 - 4, 40]])} fill={S.orange} opacity="0.9" /><circle cx={32 + i * 12} cy="43" r="1.6" fill={S.red} style={{ animation:`ship-blink ${1.8 + i * 0.3}s steps(2) infinite` }} /></g>)}
          {ancient && <g><polyline points={pts([[20, 32], [28, 28], [36, 34], [44, 30]])} fill="none" stroke={ART.brass.shade} strokeWidth="1.2" /><text x="32" y="50.4" textAnchor="middle" fontSize="2.4" fill={ART.brass.hi} {...PT}>WELDED SHUT: ALLOY 2 CENTURIES OLD</text></g>}
          {sealed && <text x="32" y="53" textAnchor="middle" fontSize="2.6" fill={S.amberHi} fontWeight="700" {...PT}>3 SEALS</text>}
        </g>
      )}
      <Seams x={6} y={8} w={52} h={48} cols={1} op={0.2} />
      <Grime x={4} y={10} w={56} h={46} seed={(variant || 'h').length + 300} n={6} op={0.28} /><Scuff x={6} y={12} w={52} h={42} seed={(variant || 'h').length + 301} n={8} c="steel" />
    </svg>
  );
}

function PropScannerArch({ variant }) {
  const S = ART.signal;
  const plat = variant === 'platform';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <polygon points={pts([[20, 56], [76, 56], [86, 62], [10, 62]])} fill={ART.dark.base} opacity="0.7" />
      <polygon points={pts([[26, 54], [70, 54], [66, 12], [30, 12]])} fill={plat ? S.amber : S.cyan} opacity="0.1" />
      {[18, 28, 38, 48].map((y, i) => <line key={y} x1={30 - i * 0.6} y1={y} x2={66 + i * 0.6} y2={y} stroke={plat ? S.amberHi : S.cyanHi} strokeWidth="0.7" opacity="0.5" style={{ animation:`holo-flicker ${1.4 + i * 0.3}s ease-in-out infinite` }} />)}
      {[[10, 'a'], [76, 'b']].map(([x, k]) => <g key={k}><Bev t="polygon" points={pts([[x, 60], [x, 8], [x + 10, 8], [x + 10, 60]])} c={plat ? 'deepglass' : 'steel'} o={1} /><Seams x={x} y={8} w={10} h={52} rows={3} op={0.4} /><Vent x={x + 1.4} y={16} w={7} h={8} n={3} /><Lights x={x + 2.4} y={30} n={2} gap={4} seed={x} /><Hazard x={x} y={52} w={10} h={6} /></g>)}
      <Bev t="polygon" points={pts([[6, 16], [90, 16], [90, 4], [6, 4]])} c={plat ? 'deepglass' : 'steel'} o={1} />
      <Screen x={12} y={5} w={72} h={10} s={plat ? SCR.amber : SCR.cyan} rows={[]} fs={2.2} scan={false} flick />
      {plat ? <text x="48" y="12.4" textAnchor="middle" fontSize="3" fill={S.amberHi} fontWeight="700" {...PT}>3 CARS CLEARED: SENATE OVERRIDE</text> : <g>{Array.from({ length: 12 }, (_, i) => <rect key={i} x={14 + i * 5.6} y="8" width="4" height="4" fill={i === 11 ? S.red : S.warn} opacity="0.95" />)}<text x="86" y="14.4" textAnchor="end" fontSize="2.2" fill={S.redHi} {...PT}>PHRIK ALLOY TRACE: CLEARED</text></g>}
      <Rivets x={8} y={17.4} w={80} n={10} />
      <Grime x={6} y={4} w={84} h={56} seed={plat ? 310 : 312} n={5} op={0.22} /><Scuff x={8} y={6} w={80} h={50} seed={plat ? 311 : 313} n={8} c="steel" />
    </svg>
  );
}

function PropDamperArray() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="rect" x="4" y="6" width="88" height="5" c="steel" o={0.9} /><Bev t="rect" x="4" y="54" width="88" height="5" c="steel" o={0.9} />
      {[0, 1, 2, 3].map((i) => {
        const x = 10 + i * 21, ext = [14, 22, 8, 18][i];
        return (
          <g key={i}>
            <Bev t="polygon" points={pts([[x, 54], [x, 30], [x + 14, 30], [x + 14, 54]])} c="amber" o={0.8} />
            <rect x={x} y="36" width="14" height="2" fill={ART.dark.shade} /><rect x={x} y="46" width="14" height="2" fill={ART.dark.shade} />
            <rect x={x + 5} y={30 - ext + 6} width="4" height={ext - 2} fill={ART.steel.hi} /><rect x={x + 5} y={30 - ext + 6} width="1.4" height={ext - 2} fill={ART.pearl.hi} />
            <Slab x={x + 2} y={10} w={10} h={14} k={1} c="steel" o={0.5} />
            <Slab x={x + 1} y={40} w={12} h={6} k={0.8} c="paper" o={0.3} /><text x={x + 7} y={44.4} textAnchor="middle" fontSize="2" fill={ART.note.ink} fontWeight="700" {...PT}>{['8 MO', '8 MO', '9 MO', '8 MO'][i]}</text>
          </g>
        );
      })}
      <text x="48" y="64" textAnchor="middle" fontSize="2.6" fill={S.warn} fontWeight="700" {...PT}>SERVICE INTERVAL: 30 DAYS</text>
      <g stroke={S.amber} strokeWidth="0.6" opacity="0.6" fill="none">{[18, 39, 60, 81].map((x, i) => <path key={x} d={`M${x} 28 C${x - 3} 24 ${x + 3} 20 ${x} 16`} style={{ animation:`holo-flicker ${1.2 + i * 0.2}s ease-in-out infinite` }} />)}</g>
      <Rivets x={8} y={8.4} w={80} n={10} /><Lights x={10} y={56.6} n={14} gap={6} r={0.7} seed={6} />
      <Grime x={4} y={8} w={88} h={50} seed={320} n={6} op={0.26} /><Scuff x={6} y={10} w={84} h={46} seed={321} n={8} c="amber" />
    </svg>
  );
}

function PropPowerTap() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={26} ry={4} />
      <polygon points={pts(ngon(36, 48, 26, 9, 8, Math.PI / 8))} fill={ART.dark.shade} /><Bev t="polygon" points={pts(ngon(36, 46, 24, 8, 8, Math.PI / 8))} c="steel" o={0.9} />
      <polygon points={pts(ngon(36, 45.6, 19, 5.6, 8, Math.PI / 8))} fill={ART.ink} />{Array.from({ length: 6 }, (_, i) => <rect key={i} x={22 + i * 4.6} y="41" width="2" height="9" fill={ART.steel.base} />)}
      <Bev t="polygon" points={pts([[2, 36], [2, 22], [40, 22], [40, 36]])} c="copper" o={1} />
      <rect x="2" y="22" width="38" height="2.4" fill={ART.copper.hi} />
      {[12, 28].map((x) => <g key={x}><Bev t="polygon" points={pts([[x - 3, 40], [x - 3, 18], [x + 3, 18], [x + 3, 40]])} c="steel" o={0.7} /><rect x={x - 3} y="26" width="6" height="2" fill={ART.dark.shade} /></g>)}
      <Slab x={16} y={12} w={14} h={12} k={1.2} c="dark" o={0.7} /><circle cx="23" cy="18" r="2" fill={S.green} style={{ animation:'ship-blink 1.8s steps(2) infinite' }} /><text x="23" y="23" textAnchor="middle" fontSize="1.9" fill={S.greenHi} {...PT}>DRAW LOW</text>
      <Cable d="M23 24 C28 30 22 40 30 46 C34 50 36 56 34 62" c={ART.dark.base} w={3} />
      <polygon points={pts([[6, 28], [14, 28], [12, 36], [8, 36]])} fill={ART.amber.hi} opacity="0.85" />
      <g fill={S.amberHi} opacity="0.85"><polygon points={pts([[34, 56], [36, 59], [38, 56]])} /></g>
      <Rivets x={4} y={34} w={34} n={5} c="copper" />
      <Grime x={2} y={20} w={60} h={38} seed={330} n={5} op={0.26} /><Scuff x={4} y={22} w={56} h={32} seed={331} n={7} c="copper" />
    </svg>
  );
}

function PropDropShaft() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.4} />
      <polygon points={pts(ngon(48, 36, 44, 24, 12, Math.PI / 12))} fill={ART.steel.shade} />
      <Bev t="polygon" points={pts(ngon(48, 34, 42, 22, 12, Math.PI / 12))} c="steel" o={1.1} />
      <polygon points={pts(ngon(48, 33, 34, 16, 12, Math.PI / 12))} fill={ART.dark.base} />
      <polygon points={pts(ngon(48, 33, 28, 12, 12, Math.PI / 12))} fill={ART.dark.shade} />
      <polygon points={pts(ngon(48, 34, 20, 8, 12, Math.PI / 12))} fill={ART.ink} />
      <polygon points={pts(ngon(48, 35, 11, 4, 12, Math.PI / 12))} fill={ART.ink2} />
      {/* ladder rungs down the far wall */}
      {[0, 1, 2, 3].map((i) => <rect key={i} x={32 + i * 1.4} y={22 + i * 3.4} width={18 - i * 2.4} height="1.4" fill={ART.steel.base} opacity={0.9 - i * 0.2} />)}
      {/* a work light far below */}
      <Glow cx={56} cy={37} rx={7} ry={3} c={S.amber} opacity={0.8} style={{ animation:'ship-blink 2.6s steps(3) infinite' }} />
      <rect x="53" y="36" width="6" height="1.6" fill={ART.amberHi || S.amberHi} />
      {/* broken rail stubs and hazard rim */}
      <Hazard x={4} y={46} w={22} h={4} /><Hazard x={70} y={46} w={22} h={4} />
      {[10, 20, 76, 86].map((x) => <g key={x}><Bev t="polygon" points={pts([[x, 52], [x, 36], [x + 3, 36], [x + 3, 52]])} c="steel" o={0.5} /></g>)}
      <polyline points={pts([[10, 38], [20, 38]])} fill="none" stroke={ART.steel.hi} strokeWidth="1.4" /><polyline points={pts([[76, 38], [86, 38]])} fill="none" stroke={ART.steel.hi} strokeWidth="1.4" />
      <text x="48" y="10" textAnchor="middle" fontSize="3.2" fill={S.warn} fontWeight="700" {...PT}>NO RAILING: 30 M DROP</text>
      <polygon points={pts([[44, 14], [52, 14], [48, 20]])} fill={S.red} opacity="0.9" />
      <Rivets x={8} y={58} w={80} n={10} />
      <Grime x={4} y={14} w={88} h={44} seed={340} n={6} op={0.26} /><Scuff x={6} y={16} w={84} h={40} seed={341} n={8} c="steel" />
    </svg>
  );
}

function PropCatwalkJunction() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={91} rx={44} ry={3} />
      <polygon points={pts([[4, 46], [92, 46], [92, 60], [58, 60], [58, 90], [38, 90], [38, 60], [4, 60]])} fill={ART.dark.shade} transform="translate(2 4)" />
      <Bev t="polygon" points={pts([[4, 46], [92, 46], [92, 60], [58, 60], [58, 90], [38, 90], [38, 60], [4, 60]])} c="steel" o={1} />
      {Array.from({ length: 14 }, (_, i) => <line key={i} x1={8 + i * 6.2} y1="48" x2={8 + i * 6.2} y2="58" stroke={ART.dark.base} strokeWidth="1.2" />)}
      {Array.from({ length: 5 }, (_, i) => <line key={i} x1="40" y1={62 + i * 6} x2="56" y2={62 + i * 6} stroke={ART.dark.base} strokeWidth="1.2" />)}
      <polygon points={pts(ngon(48, 54, 5, 3, 8))} fill={ART.ink} />{[0, 1, 2].map((i) => <rect key={i} x="45" y={52 + i * 1.4} width="6" height="0.8" fill={ART.steel.base} />)}
      {[[6, 38, 30], [60, 38, 30]].map(([x, y, w], i) => <g key={i}><rect x={x} y={y} width={w} height="2" fill={ART.steel.hi} /><rect x={x} y={y + 8} width={w} height="1.6" fill={ART.steel.base} />{[0, 1, 2, 3].map((j) => <rect key={j} x={x + j * (w / 3) - (j === 3 ? 2 : 0)} y={y} width="2" height="10" fill={ART.steel.base} />)}</g>)}
      {[36, 56].map((x) => <g key={x}><rect x={x} y="62" width="2" height="26" fill={ART.steel.hi} /></g>)}
      <g fontSize="2.8" fill={S.amberHi} fontWeight="700" {...PT}><text x="8" y="34">W: UNDER LOUNGE</text><text x="62" y="34">E: CRANES</text><text x="26" y="94">DOWN: L1222 SPINE</text></g>
      <g fill={S.amberHi}><polygon points={pts([[4, 52], [10, 49], [10, 55]])} /><polygon points={pts([[92, 52], [86, 49], [86, 55]])} /><polygon points={pts([[48, 88], [45, 82], [51, 82]])} /></g>
      <Hazard x={4} y={58} w={32} h={2.6} /><Hazard x={60} y={58} w={32} h={2.6} />
      <Rivets x={8} y={47.6} w={80} n={10} />
      <Grime x={4} y={46} w={88} h={44} seed={350} n={6} op={0.26} /><Scuff x={6} y={48} w={84} h={40} seed={351} n={8} c="steel" />
    </svg>
  );
}

function PropFloorGrate() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={28} ry={3.4} />
      <polygon points={pts(ngon(32, 38, 30, 20, 8, Math.PI / 8))} fill={ART.dark.shade} /><Bev t="polygon" points={pts(ngon(32, 36, 28, 18, 8, Math.PI / 8))} c="steel" o={1} />
      <polygon points={pts(ngon(32, 36, 23, 14, 8, Math.PI / 8))} fill={ART.screen.space} />
      <rect x="12" y="30" width="40" height="3" fill={ART.dark.hi} /><rect x="12" y="37" width="40" height="3" fill={ART.dark.hi} /><rect x="12" y="43" width="40" height="3" fill={ART.dark.base} />
      <rect x="14" y="30" width="36" height="0.8" fill={ART.pearl.base} opacity="0.5" />
      <Glow cx={44} cy={40} rx={9} ry={4} c={S.amber} opacity={0.7} /><rect x="38" y="38" width="12" height="3" fill={S.amberHi} />
      <Glow cx={18} cy={34} rx={5} ry={2.4} c={S.amber} opacity={0.5} />
      <g fill={ART.glass.base} opacity="0.5"><polygon points={pts([[22, 38], [28, 36], [30, 41], [24, 43]])} /></g>
      {Array.from({ length: 8 }, (_, i) => <rect key={i} x={14 + i * 5} y="22" width="1.8" height="28" fill={ART.steel.base} opacity="0.9" />)}
      {Array.from({ length: 3 }, (_, i) => <rect key={i} x="10" y={26 + i * 8} width="44" height="1.4" fill={ART.steel.base} />)}
      <polygon points={pts([[8, 20], [14, 14], [20, 20]])} fill={ART.pearl.base} opacity="0.2" />
      <Rivets x={10} y={52} w={44} n={8} /><Lights x={48} y={16} n={2} gap={4} seed={2} />
      <Grime x={6} y={22} w={52} h={32} seed={360} n={5} op={0.2} /><Scuff x={8} y={24} w={48} h={28} seed={361} n={7} c="steel" />
    </svg>
  );
}

function PropDrainChannel() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.6} />
      {[[24, 8, 0], [48, 4, 1.2], [70, 10, 2.2]].map(([x, y, d], i) => <g key={i} fill={ART.pearl.base} opacity="0.5" style={{ animation:`steam-rise 3.6s ease-out ${d}s infinite` }}><polygon points={pts(ngon(x, y + 14, 9, 6, 8))} /></g>)}
      <Bev t="polygon" points={pts([[2, 58], [2, 24], [94, 24], [94, 58]])} c="steel" o={1.2} />
      <polygon points={pts([[8, 54], [8, 30], [88, 30], [88, 54]])} fill={ART.dark.shade} />
      <polygon points={pts([[8, 54], [8, 34], [88, 34], [88, 54]])} fill={ART.amber.shade} />
      <polygon points={pts([[8, 48], [8, 40], [88, 40], [88, 48]])} fill={ART.amber.base} />
      <polygon points={pts([[8, 46], [8, 42], [88, 42], [88, 46]])} fill={ART.amber.hi} />
      <Glow cx={48} cy={42} rx={36} ry={8} c={S.amber} opacity={0.35} />
      {Array.from({ length: 10 }, (_, i) => <rect key={i} x={10 + i * 8} y="30" width="2.2" height="24" fill={ART.steel.base} />)}
      <rect x="8" y="30" width="80" height="2" fill={ART.steel.hi} />
      <Hazard x={4} y={56} w={88} h={3} /><text x="48" y="22" textAnchor="middle" fontSize="2.8" fill={S.warn} fontWeight="700" {...PT}>NOT ON ANY L088 MAP</text>
      <Rivets x={6} y={26} w={84} n={12} />
      <Grime x={2} y={24} w={92} h={34} seed={370} n={6} op={0.26} /><Scuff x={4} y={26} w={88} h={30} seed={371} n={8} c="steel" />
    </svg>
  );
}

function PropSupportPillar() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={92} rx={26} ry={3} />
      <Slab x={6} y={84} w={52} h={8} k={1.4} c="concrete" o={0.9} />
      <Bev t="polygon" points={pts([[12, 86], [12, 4], [52, 4], [52, 86]])} c="concrete" o={1.2} />
      {Array.from({ length: 7 }, (_, i) => <rect key={i} x="12" y={10 + i * 11} width="40" height="2.6" fill={ART.concrete.shade} opacity="0.7" />)}
      <rect x="12" y="4" width="40" height="3" fill={ART.concrete.hi} />
      {/* stenciled tags */}
      <g fill="none" strokeWidth="1.4">
        <polyline points={pts([[16, 20], [22, 16], [28, 22], [34, 17]])} stroke={S.blue} /><polygon points={pts([[38, 34], [44, 26], [50, 34]])} stroke={S.crimson} />
        <polyline points={pts([[16, 40], [24, 44], [30, 38], [36, 44]])} stroke={S.lime} /><polyline points={pts([[18, 56], [26, 52], [32, 58]])} stroke={S.amber} />
      </g>
      <text x="44" y="66" textAnchor="middle" fontSize="2.1" fill={ART.beige.hi} {...PT}>PORT OFFICIAL</text>
      <text x="44" y="69" textAnchor="middle" fontSize="2.1" fill={ART.beige.hi} {...PT}>IS A THIEF</text>
      <text x="44" y="74" textAnchor="middle" fontSize="1.8" fill={S.crimson} {...PT}>MAREL+KEN</text>
      {/* copper conduit loops with mismatched patches */}
      <Cable d="M20 4 L20 80" c={ART.copper.base} w={2.4} hi="copper" /><Cable d="M26 4 C26 20 34 24 34 36 L34 80" c={ART.copper.base} w={2.4} hi="copper" />
      {[28, 54].map((y) => <rect key={y} x="17" y={y} width="20" height="3.4" fill={ART.dark.hi} />)}
      <polygon points={pts([[17, 28], [37, 28], [37, 31.4], [17, 31.4]])} fill={ART.steel.base} /><polygon points={pts([[17, 54], [37, 54], [37, 57.4], [17, 57.4]])} fill={ART.steel.hi} opacity="0.7" />
      <Hazard x={12} y={80} w={40} h={4} />
      <Grime x={12} y={6} w={40} h={78} seed={380} n={7} op={0.3} /><Scuff x={14} y={8} w={36} h={74} seed={381} n={9} c="concrete" /><Rivets x={14} y={9} w={36} n={4} c="concrete" />
    </svg>
  );
}

function PropRadiator() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <g fill={S.orange} opacity="0.2">{[14, 34, 54, 74].map((x, i) => <polygon key={x} points={pts([[x, 40], [x + 8, 40], [x + 5, 10], [x + 3, 10]])} style={{ animation:`holo-flicker ${1.6 + i * 0.3}s ease-in-out infinite` }} />)}</g>
      <Bev t="polygon" points={pts([[3, 50], [3, 8], [8, 4], [88, 4], [93, 8], [93, 50]])} c="rust" o={1.1} />
      {Array.from({ length: 14 }, (_, i) => <g key={i}><rect x={8 + i * 5.8} y="8" width="3.6" height="38" fill={ART.dark.shade} /><rect x={8 + i * 5.8} y="8" width="1.4" height="38" fill={S.orange} opacity={0.55 + (i % 3) * 0.15} /></g>)}
      <rect x="3" y="4" width="90" height="3" fill={ART.rust.hi} /><rect x="3" y="46" width="90" height="4" fill={ART.rust.shade} />
      <Hazard x={6} y={46} w={84} h={3} />
      {[16, 34, 50, 64, 80].map((x, i) => <g key={x} fill={ART.ink} opacity="0.92"><polygon points={pts(ngon(x, 52 + (i % 2), 3.6, 4, 7))} /><polygon points={pts([[x - 7, 62], [x - 5, 56], [x + 5, 56], [x + 7, 62]])} /></g>)}
      <Lights x={10} y={48} n={6} gap={5} r={0.7} seed={3} />
      <Rivets x={6} y={6} w={84} n={12} />
      <Grime x={3} y={4} w={90} h={46} seed={390} n={6} op={0.3} /><Scuff x={5} y={6} w={86} h={40} seed={391} n={8} c="rust" />
    </svg>
  );
}

function PropFluidSlick() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={50} rx={44} ry={12} />
      <polygon points={pts(ngon(52, 44, 42, 13, 14, 0.2))} fill={ART.dark.shade} />
      <polygon points={pts(ngon(52, 43, 40, 11.6, 14, 0.2))} fill={ART.deepglass.shade} />
      {[[24, 'blue', 18, 5], [48, 'magenta', 22, 6], [72, 'amber', 16, 5]].map(([x, k, w, h], i) => <g key={i}><polygon points={pts(ngon(x, 42 + (i % 2), w, h, 8))} fill={S[k]} opacity="0.5" /><polygon points={pts(ngon(x, 42 + (i % 2), w * 0.5, h * 0.5, 8))} fill={S.white} opacity="0.4" /></g>)}
      {[[18, 40], [44, 46], [66, 38], [78, 48]].map(([x, y], i) => <g key={i}><rect x={x} y={y} width="14" height="1.4" fill={S[['cyan', 'magenta', 'amber', 'blue'][i]]} opacity="0.8" /><rect x={x + 2} y={y + 2.4} width="9" height="1" fill={S.white} opacity="0.4" /></g>)}
      <polygon points={pts(ngon(52, 43, 30, 8.4, 14, 0.2))} fill="none" stroke={S.anomaly} strokeWidth="0.6" opacity="0.5" />
      <Bev t="polygon" points={pts([[6, 36], [20, 36], [20, 26], [12, 22], [6, 26]])} c="steel" o={0.7} />
      <Slab x={8} y={26} w={10} h={6} k={0.8} c="dark" o={0.5} /><polygon points={pts([[10, 36], [12, 42], [14, 36]])} fill={S.cyanHi} />
      <Lights x={8} y={34} n={2} gap={4} r={0.7} seed={4} />
      <text x="62" y="60" textAnchor="middle" fontSize="2.8" fill={ART.beige.hi} {...PT}>SIGNS READ BACKWARDS HERE</text>
      <Grime x={4} y={32} w={88} h={20} seed={400} n={4} op={0.2} /><Scuff x={6} y={34} w={84} h={14} seed={401} n={6} c="steel" /><Rivets x={7} y={30} w={11} n={3} />
    </svg>
  );
}

function PropCraneArm() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={92} rx={58} ry={3} />
      <Bev t="polygon" points={pts([[6, 90], [6, 30], [28, 30], [28, 90]])} c="amber" o={1.1} />
      <Seams x={6} y={30} w={22} h={60} rows={4} op={0.4} />
      {[40, 54, 68].map((y) => <line key={y} x1="8" y1={y} x2="26" y2={y + 8} stroke={ART.dark.shade} strokeWidth="1.2" />)}
      <Slab x={2} y={14} w={26} h={18} k={2} c="dark" o={0.9} /><Vent x={5} y={18} w={14} h={9} n={3} /><Lights x={21} y={20} n={3} gap={2.6} seed={4} />
      <Bev t="polygon" points={pts([[10, 24], [10, 12], [124, 12], [124, 22]])} c="amber" o={1} />
      {Array.from({ length: 12 }, (_, i) => <line key={i} x1={14 + i * 9.4} y1="22" x2={22 + i * 9.4} y2="14" stroke={ART.dark.shade} strokeWidth="1.4" />)}
      <Bev t="polygon" points={pts([[0, 28], [0, 6], [14, 6], [14, 28]])} c="steel" o={0.9} /><text x="7" y="19" textAnchor="middle" fontSize="2.8" fill={ART.ink} fontWeight="700" {...PT}>CW</text>
      <line x1="96" y1="22" x2="96" y2="56" stroke={ART.dark.hi} strokeWidth="2" /><line x1="96" y1="22" x2="96" y2="56" stroke={ART.steel.hi} strokeWidth="0.6" />
      <Bev t="polygon" points={pts([[92, 58], [100, 58], [101, 64], [91, 64]])} c="steel" o={0.6} />
      <Bev t="polygon" points={pts([[76, 82], [76, 64], [116, 64], [116, 82]])} c="brass" o={0.9} />
      {Array.from({ length: 6 }, (_, i) => <rect key={i} x={80 + i * 6} y="66" width="1.4" height="14" fill={ART.dark.shade} opacity="0.5" />)}
      <Slab x={82} y={69} w={28} h={7} k={1} c="red" o={0.5} /><text x="96" y="74.4" textAnchor="middle" fontSize="3.2" fill={ART.pearl.hi} fontWeight="700" {...PT}>CZERKA</text>
      {/* platform and astromechs */}
      <Bev t="rect" x="64" y="84" width="60" height="5" c="steel" o={0.8} /><Hazard x={64} y={88} w={60} h={2.4} />
      {[[70, 'a'], [112, 'b']].map(([x, k]) => <g key={k}><Bev t="polygon" points={pts([[x - 4, 84], [x - 4, 76], [x + 4, 76], [x + 4, 84]])} c="pearl" o={0.5} /><polygon points={pts([[x - 4, 76], [x - 3, 72], [x + 3, 72], [x + 4, 76]])} fill={ART.pearl.base} /><rect x={x - 1.2} y="73" width="2.4" height="1.6" fill={S.blue} /><rect x={x - 3.6} y="79" width="7.2" height="1.4" fill={ART.deepglass.base} /></g>)}
      {[44, 58, 72].map((x, i) => <g key={x}><circle cx={x} cy="36" r="2.6" fill={[S.red, S.amber, S.green][i]} opacity="0.35" /><circle cx={x} cy="36" r="1.2" fill={S.white} opacity="0.7" /></g>)}
      <text x="58" y="42" textAnchor="middle" fontSize="2.4" fill={S.warn} {...PT}>3 SEALS: EXPIRED CYCLE</text>
      <Hazard x={6} y={86} w={22} h={4} /><Rivets x={10} y={13.6} w={110} n={14} />
      <Grime x={2} y={8} w={122} h={82} seed={410} n={7} op={0.24} /><Scuff x={4} y={10} w={118} h={76} seed={411} n={9} c="amber" />
    </svg>
  );
}

function PropLandingBeacon() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={28} ry={3.4} />
      <Bev t="rect" x="2" y="38" width="60" height="3" c="steel" o={0.7} /><Bev t="rect" x="2" y="48" width="60" height="3" c="steel" o={0.7} />
      {[4, 22, 46, 58].map((x) => <Bev key={x} t="polygon" points={pts([[x, 58], [x, 34], [x + 3, 34], [x + 3, 58]])} c="steel" o={0.6} />)}
      <Hazard x={4} y={55} w={56} h={3} />
      <g fill={S.red} opacity="0.2" style={{ animation:'ship-blink 3s steps(2) infinite' }}><polygon points={pts(ngon(32, 22, 30, 16, 12))} /></g>
      <Slab x={24} y={32} w={16} h={22} k={1.6} c="dark" o={0.9} /><Seams x={24} y={32} w={16} h={22} rows={2} op={0.5} />
      <Bev t="polygon" points={pts([[24, 32], [24, 24], [28, 18], [36, 18], [40, 24], [40, 32]])} c="red" o={0.8} />
      <Glow cx={32} cy={24} r={9} c={S.red} opacity={0.9} style={{ animation:'ship-blink 3s steps(2) infinite' }} />
      <polygon points={pts([[29, 28], [29, 22], [32, 19], [35, 22], [35, 28]])} fill={S.redHi} style={{ animation:'ship-blink 3s steps(2) infinite' }} />
      <Slab x={24} y={42} w={16} h={7} k={0.8} c="amber" o={0.4} /><text x="32" y="47.6" textAnchor="middle" fontSize="3" fill={ART.ink} fontWeight="700" {...PT}>PAD 1450</text>
      <polygon points={pts([[6, 58], [12, 54], [12, 58]])} fill={S.amber} /><polygon points={pts([[58, 58], [52, 54], [52, 58]])} fill={S.amber} />
      <Rivets x={26} y={34} w={12} n={3} />
      <Grime x={2} y={32} w={60} h={26} seed={420} n={5} op={0.26} /><Scuff x={4} y={34} w={56} h={22} seed={421} n={6} c="steel" />
    </svg>
  );
}

function PropCeilingTap() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={92} rx={24} ry={3} />
      <Cable d="M24 0 L24 60" c={ART.dark.base} w={4} /><Cable d="M32 0 C32 20 40 30 40 56" c={ART.dark.base} w={4} /><Cable d="M42 0 L42 50 C42 56 36 58 34 62" c={ART.dark.base} w={3} />
      <Bev t="polygon" points={pts([[16, 24], [50, 24], [48, 30], [18, 30]])} c="steel" o={0.6} />
      <Slab x={10} y={56} w={44} h={34} k={2.4} c="dark" o={1} />
      <Seams x={10} y={56} w={44} h={34} cols={2} rows={1} op={0.4} />
      <Screen x={14} y={60} w={36} h={14} s={SCR.green} title="PENTHOUSE TAP" rows={['HARDWIRE: LIVE']} fs={2.2} scan={false} flick />
      <Toggles x={14} y={77} cols={6} rows={2} gap={3.6} seed={6} />
      <polygon points={pts([[14, 84], [28, 84], [28, 88], [14, 88]])} fill={ART.amber.hi} opacity="0.85" />
      <Hazard x={12} y={86} w={40} h={3} />
      <Rivets x={13} y={58} w={38} n={6} />
      <Grime x={10} y={56} w={44} h={34} seed={430} n={5} op={0.26} /><Scuff x={12} y={58} w={40} h={30} seed={431} n={6} c="dark" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// PEOPLE AND PLACES: tables, stalls, booths, posts. Figures are simple three tone busts, as in the portraits.
// ---------------------------------------------------------------------------
function Bust({ x, y, s = 1, coat = 'steel', skin, hair = ART.skin.hair, back = false }) {
  const sk = skin || ART.skin.mid;
  return (
    <g>
      <Bev t="polygon" points={pts([[x - 7 * s, y + 14 * s], [x - 6 * s, y + 4 * s], [x - 2.5 * s, y + 1.4 * s], [x + 2.5 * s, y + 1.4 * s], [x + 6 * s, y + 4 * s], [x + 7 * s, y + 14 * s]])} c={coat} o={0.6} />
      <polygon points={pts(ngon(x, y - 2 * s, 3.4 * s, 3.9 * s, 8))} fill={back ? hair : sk} />
      {!back && <polygon points={pts([[x - 3.4 * s, y - 3 * s], [x - 2 * s, y - 6 * s], [x + 2 * s, y - 6 * s], [x + 3.4 * s, y - 3 * s], [x + 1 * s, y - 4.4 * s]])} fill={hair} />}
    </g>
  );
}

function PropTacticalTable({ variant }) {
  const S = ART.signal;
  const war = variant === 'war', conq = variant === 'conquest';
  const bad = variant === 'bador';
  const col = war ? S.cyan : conq ? S.amber : bad ? S.red : S.blue;
  const top = ngon(48, 78, 44, 12, 8, Math.PI / 8);
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={90} rx={44} ry={4.4} />
      <Slab x={34} y={80} w={28} h={10} k={1.6} c="dark" />
      <Bev t="polygon" points={pts(top)} c="steel" o={1.4} />
      <polygon points={pts(ngon(48, 77.4, 36, 9.4, 8, Math.PI / 8))} fill={ART.screen.cyan} stroke={col} strokeWidth="0.8" />
      {top.map(([x, y], i) => <circle key={i} cx={x} cy={y - 1} r="1" fill={i % 2 ? S.cyan : S.amber} style={{ animation:`ship-blink ${2 + i * 0.3}s ease-in-out infinite` }} />)}
      <polygon points={pts([[22, 76], [74, 76], [62, 18], [34, 18]])} fill={col} opacity="0.1" />
      <g style={{ animation:'holo-flicker 3s ease-in-out infinite' }}>
        {war && <g>
          {[0, 1, 2, 3].map((i) => <polygon key={i} points={pts(ngon(48, 72 - i * 11, 30 - i * 5, 7.6 - i * 1.2, 8, Math.PI / 8))} fill={S.cyan} opacity={0.12 + i * 0.04} stroke={S.cyanHi} strokeWidth="0.5" />)}
          {[[34, 60], [44, 50], [56, 44], [62, 58], [48, 66], [40, 38]].map(([x, y], i) => <g key={i}><rect x={x - 1.4} y={y - 8 - (i % 3) * 3} width="2.8" height={8 + (i % 3) * 3} fill={S.cyanHi} opacity="0.85" /></g>)}
          {[[34, 60], [62, 58], [48, 66]].map(([x, y], i) => <polygon key={i} points={pts(ngon(x, y, 3.4, 1.6, 6))} fill={S.syndicate || S.green} />)}
          <polygon points={pts([[66, 20], [92, 20], [92, 42], [66, 42]])} fill={ART.screen.cyan} opacity="0.9" /><text x="68" y="25" fontSize="2.4" fill={S.cyanHi} fontWeight="700" {...PT}>TERRITORIES 4</text><text x="68" y="29.6" fontSize="2.2" fill={S.cyanHi} {...PT}>AGENTS ACTIVE</text><text x="68" y="34" fontSize="2.2" fill={S.amberHi} {...PT}>CONTRACT QUEUE</text><text x="68" y="38.6" fontSize="2.2" fill={S.cyanHi} {...PT}>L800 TO L1450</text>
        </g>}
        {conq && <g>
          {[[48, 56, 'green'], [34, 50, 'blue'], [62, 50, 'amber'], [40, 66, 'cyan'], [58, 68, 'red']].map(([x, y, k], i) => <g key={i}><polygon points={pts(ngon(x, y, 9, 4.2, 6))} fill={S[k]} opacity="0.55" stroke={S[k]} strokeWidth="0.8" /><rect x={x - 1} y={y - 12} width="2" height="9" fill={S[k]} /></g>)}
          <g stroke={S.white} strokeWidth="0.6" fill="none" opacity="0.7"><polyline points={pts([[48, 56], [34, 50]])} /><polyline points={pts([[48, 56], [62, 50]])} /><polyline points={pts([[48, 56], [40, 66]])} /><polyline points={pts([[48, 56], [58, 68]])} /></g>
          <text x="48" y="14" textAnchor="middle" fontSize="3.2" fill={S.amberHi} fontWeight="700" {...PT}>SECTOR CONTROL: 16 SECTORS</text>
        </g>}
        {bad && <g>
          {Array.from({ length: 4 }, (_, i) => <polygon key={i} points={pts(ngon(48, 74 - i * 9, 32 - i * 5, 8 - i * 1.2, 8, Math.PI / 8))} fill="none" stroke={S.redHi} strokeWidth="0.5" opacity="0.7" />)}
          {[[34, 52], [42, 46], [54, 58], [60, 50], [48, 66], [38, 62]].map(([x, y], i) => <polygon key={i} points={pts([[x - 3, y + 3], [x, y - 3], [x + 3, y + 3]])} fill={S.red} style={{ animation:`ship-blink ${2 + i * 0.3}s steps(3) infinite` }} />)}
          {[[28, 40], [66, 42]].map(([x, y], i) => <g key={i}><polygon points={pts(ngon(x, y, 4, 4, 6))} fill="none" stroke={S.violet} strokeWidth="0.9" /><text x={x} y={y + 1} textAnchor="middle" fontSize="2.8" fill={S.violet} fontWeight="700" {...PT}>?</text></g>)}
          <text x="14" y="30" fontSize="2.4" fill={S.redHi} {...PT}>SECTOR 4 + ENGINE WASTES</text><text x="14" y="34" fontSize="2.2" fill={S.violet} {...PT}>2 X UNKNOWN STRUCTURE</text>
        </g>}
        {!war && !conq && !bad && <g>
          {Array.from({ length: 6 }, (_, i) => <line key={i} x1={20 + i * 11} y1="72" x2={36 + i * 5} y2="36" stroke={S.blue} strokeWidth="0.4" opacity="0.5" />)}
          {Array.from({ length: 5 }, (_, i) => <polygon key={i} points={pts(ngon(48, 74 - i * 8, 32 - i * 4.4, 8 - i * 1.1, 8, Math.PI / 8))} fill="none" stroke={S.blue} strokeWidth="0.5" opacity="0.7" />)}
          <polyline points={pts([[26, 70], [40, 60], [50, 54], [64, 46]])} fill="none" stroke={S.cyanHi} strokeWidth="0.9" /><polyline points={pts([[30, 62], [44, 66], [60, 62]])} fill="none" stroke={S.cyanHi} strokeWidth="0.9" />
          {[[38, 54], [58, 60]].map(([x, y], i) => <polygon key={i} points={pts([[x - 3, y + 3], [x, y - 3], [x + 3, y + 3]])} fill={S.crimson} />)}
          {[[30, 44], [50, 36], [66, 52]].map(([x, y], i) => <g key={i}><polygon points={pts(ngon(x, y, 4, 4, 6))} fill="none" stroke={S.violet} strokeWidth="0.9" /><text x={x} y={y + 1} textAnchor="middle" fontSize="2.8" fill={S.violet} fontWeight="700" {...PT}>?</text></g>)}
          <text x="14" y="30" fontSize="2.4" fill={S.violet} {...PT}>UNKNOWN x3</text><text x="14" y="34" fontSize="2.2" fill={S.crimson} {...PT}>BLACK SUN HOUSES</text><text x="14" y="38" fontSize="2.2" fill={S.cyanHi} {...PT}>S4 PATROLS</text>
        </g>}
      </g>
      <Glow cx={48} cy={77} rx={26} ry={6} c={col} opacity={0.4} />
      <Rivets x={18} y={84} w={60} n={9} /><Grime x={8} y={70} w={80} h={20} seed={war ? 440 : conq ? 442 : 444} n={5} op={0.22} /><Scuff x={8} y={68} w={80} h={22} seed={war ? 441 : conq ? 443 : 445} n={8} />
    </svg>
  );
}

function PropVendorStall({ variant }) {
  const S = ART.signal;
  const fruit = variant === 'fruit', droid = variant === 'droid_repair';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {[8, 86].map((x) => <Bev key={x} t="polygon" points={pts([[x, 60], [x, 14], [x + 3, 14], [x + 3, 60]])} c={fruit ? 'wood' : 'steel'} o={0.6} />)}
      {/* canopy */}
      <polygon points={pts([[2, 24], [94, 24], [86, 6], [10, 6]])} fill={ART[fruit ? 'canvas' : droid ? 'dark' : 'tan'].base} />
      {fruit && <g>
        <polygon points={pts([[20, 8], [34, 8], [32, 20], [22, 18]])} fill={ART.tan.base} /><polygon points={pts([[44, 10], [58, 7], [60, 18], [48, 20]])} fill={ART.red.base} /><polygon points={pts([[66, 12], [80, 8], [82, 20], [70, 21]])} fill={ART.teal.base} /><polygon points={pts([[8, 14], [18, 14], [18, 22], [6, 22]])} fill={ART.fabric.base} />
        <g stroke={ART.ink} strokeWidth="0.5" opacity="0.5" fill="none"><polyline points={pts([[20, 8], [34, 8], [32, 20], [22, 18], [20, 8]])} /><polyline points={pts([[44, 10], [58, 7], [60, 18], [48, 20], [44, 10]])} /></g>
      </g>}
      {Array.from({ length: 8 }, (_, i) => <polygon key={i} points={pts([[2 + i * 11.5, 24], [13.5 + i * 11.5, 24], [14.4 + i * 10.6, 6], [10 + i * 10.6, 6]])} fill={fruit ? ART.canvas.hi : droid ? ART.dark.hi : ART.tan.hi} opacity={i % 2 ? 0.28 : 0} />)}
      <rect x="2" y="23" width="92" height="2.6" fill={ART.dark.shade} />
      {fruit && <g>
        <Bev t="polygon" points={pts([[8, 58], [8, 42], [88, 42], [88, 58]])} c="wood" o={1} /><Seams x={8} y={42} w={80} h={16} cols={6} op={0.4} />
        {Array.from({ length: 11 }, (_, i) => <circle key={i} cx={16 + i * 6.4} cy={40 - (i % 3)} r="3.4" fill={S.orange} />)}
        {Array.from({ length: 6 }, (_, i) => <circle key={i} cx={20 + i * 11} cy={36 - (i % 2) * 2} r="2.8" fill={S.amberHi} />)}
        <Glow cx={48} cy={38} rx={36} ry={6} c={S.orange} opacity={0.4} />
        <Slab x={62} y={26} w={24} h={8} k={1} c="paper" o={0.3} /><text x="74" y="31.6" textAnchor="middle" fontSize="2.6" fill={ART.note.ink} fontWeight="700" {...PT}>GLOWS ORANGE</text>
      </g>}
      {droid && <g>
        <Bev t="polygon" points={pts([[8, 58], [8, 44], [88, 44], [88, 58]])} c="dark" o={1} />
        {[[14, 'steel'], [32, 'amber'], [50, 'teal']].map(([x, c], i) => <g key={i}><Bev t="polygon" points={pts([[x, 44], [x, 30], [x + 4, 26], [x + 14, 26], [x + 18, 30], [x + 18, 44]])} c={c} o={0.8} /><circle cx={x + 9} cy="33" r="3" fill={ART.ink} /><circle cx={x + 9} cy="33" r="1.4" fill={i === 1 ? S.red : S.cyan} /><rect x={x + 2} y="38" width="14" height="1.4" fill={ART.dark.shade} /></g>)}
        {Array.from({ length: 6 }, (_, i) => <g key={i}><circle cx={74 + (i % 3) * 5} cy={30 + Math.floor(i / 3) * 6} r="2" fill={ART.dark.shade} /><circle cx={74 + (i % 3) * 5} cy={30 + Math.floor(i / 3) * 6} r="1" fill={[S.cyan, S.amber, S.red][i % 3]} /></g>)}
        <g><Bev t="polygon" points={pts([[40, 52], [58, 52], [60, 46], [38, 46]])} c="dark" o={0.4} /><polygon points={pts([[44, 50], [50, 44], [54, 46], [48, 52]])} fill={ART.skin.mid} /><polygon points={pts([[56, 50], [52, 44], [48, 45], [52, 52]])} fill={ART.skin.dark} /><polyline points={pts([[45, 48], [49, 46]])} fill="none" stroke={ART.ink} strokeWidth="0.7" /><polygon points={pts([[50, 44], [54, 40], [55, 42]])} fill={ART.steel.hi} /><circle cx="55" cy="40" r="1.4" fill={S.amberHi} style={{ animation:'ship-spark 0.9s steps(2) infinite' }} /></g>
        <polygon points={pts([[10, 6], [86, 6], [86, 17], [10, 17]])} fill={ART.paper.base} opacity="0.92" /><text x="48" y="10.4" textAnchor="middle" fontSize="2.8" fill={ART.note.ink} fontWeight="700" {...PT}>NO DROIDS REFUSED. NO QUESTIONS ASKED.</text><text x="48" y="14.6" textAnchor="middle" fontSize="2.8" fill={ART.note.redInk} fontWeight="700" {...PT}>PAYMENT UPFRONT.</text>
      </g>}
      {!fruit && !droid && <g>
        <Bev t="polygon" points={pts([[8, 58], [8, 46], [88, 46], [88, 58]])} c="steel" o={1} />
        <Bev t="polygon" points={pts([[12, 46], [12, 26], [16, 22], [30, 22], [34, 26], [34, 46]])} c="hull" o={1} /><rect x="12" y="30" width="22" height="2" fill={ART.dark.shade} /><rect x="12" y="38" width="22" height="2" fill={ART.dark.shade} />
        <rect x="18" y="33" width="10" height="4" fill={ART.screen.amber} /><circle cx="23" cy="43" r="1.8" fill={ART.dark.shade} /><polygon points={pts([[20, 46], [26, 46], [24, 48], [22, 48]])} fill={ART.steel.base} /><rect x="36" y="38" width="3" height="2" fill={ART.dark.shade} />
        <polygon points={pts([[46, 46], [84, 46], [88, 52], [42, 52]])} fill={ART.tan.base} />{[50, 62, 74].map((x) => <g key={x}><Bev t="polygon" points={pts([[x, 46], [x + 1, 40], [x + 6, 40], [x + 7, 46]])} c="pearl" o={0.4} /><polygon points={pts([[x + 1, 40], [x + 6, 40], [x + 5, 37], [x + 2, 37]])} fill={ART.amber.hi} opacity="0.6" /></g>)}
        <Slab x={40} y={8} w={50} h={9} k={1} c="wood" o={0.6} /><text x="65" y="14" textAnchor="middle" fontSize="2.8" fill={ART.paper.hi} fontWeight="700" {...PT}>BEST CAF BETWEEN THE LEVELS</text>
        <g transform="rotate(-3 65 22)"><polygon points={pts([[46, 18], [88, 18], [88, 24], [46, 24]])} fill={ART.paper.base} /><text x="67" y="22.4" textAnchor="middle" fontSize="2.4" fill={ART.note.ink} {...PT}>only caf between the levels</text></g>
      </g>}
      <Rivets x={10} y={56} w={76} n={10} />
      <Grime x={4} y={8} w={88} h={52} seed={(variant || 'v').length + 450} n={6} op={0.24} /><Scuff x={6} y={10} w={84} h={48} seed={(variant || 'v').length + 451} n={8} />
    </svg>
  );
}

function PropGorgSpit() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[4, 60], [4, 44], [92, 44], [92, 60]])} c="rust" o={1.1} />
      <polygon points={pts([[10, 44], [86, 44], [80, 38], [16, 38]])} fill={ART.ink} />
      {Array.from({ length: 12 }, (_, i) => <polygon key={i} points={pts([[12 + i * 6.2, 44], [15 + i * 6.2, 44], [14 + i * 6.2, 34 - (i % 3) * 3], [12 + i * 6.2, 36]])} fill={[S.orange, S.amber, S.red][i % 3]} style={{ animation:`ship-blink ${0.9 + (i % 4) * 0.25}s steps(3) infinite` }} />)}
      <Glow cx={48} cy={38} rx={38} ry={6} c={S.orange} opacity={0.55} />
      <Bev t="polygon" points={pts([[2, 30], [2, 22], [8, 18], [8, 44], [2, 44]])} c="steel" o={0.7} /><Bev t="polygon" points={pts([[94, 30], [94, 22], [88, 18], [88, 44], [94, 44]])} c="steel" o={0.7} />
      <rect x="6" y="28" width="86" height="3" fill={ART.steel.hi} /><rect x="6" y="28" width="86" height="1" fill={ART.pearl.base} />
      <polygon points={pts([[16, 30], [18, 20], [30, 14], [50, 12], [70, 14], [80, 22], [80, 30], [60, 34], [34, 34]])} fill={ART.rust.shade} transform="translate(1.4 1.6)" />
      <Bev t="polygon" points={pts([[16, 30], [18, 20], [30, 14], [50, 12], [70, 14], [80, 22], [80, 30], [60, 34], [34, 34]])} c="copper" o={1} />
      <polygon points={pts([[24, 22], [40, 17], [56, 16], [72, 20], [62, 26], [38, 28]])} fill={ART.copper.hi} opacity="0.7" />
      <polygon points={pts([[78, 22], [90, 20], [92, 26], [80, 30]])} fill={ART.rust.base} /><circle cx="86" cy="24" r="1.2" fill={ART.ink} />
      <g><Bev t="polygon" points={pts([[22, 30], [18, 38], [22, 40], [28, 32]])} c="copper" o={0.5} /><Bev t="polygon" points={pts([[64, 32], [68, 40], [72, 38], [70, 30]])} c="copper" o={0.5} /><polygon points={pts([[28, 16], [44, 14], [60, 15]])} fill="none" stroke={ART.copper.hi} strokeWidth="1" opacity="0.7" /></g>
      <Bev t="polygon" points={pts([[0, 30], [4, 30], [4, 40], [0, 40]])} c="steel" o={0.4} /><Bev t="polygon" points={pts([[0, 34], [-1, 34], [-1, 40], [0, 40]])} c="steel" o={0.3} />
      <polygon points={pts([[48, 34], [49, 40], [47, 40]])} fill={S.amberHi} /><polygon points={pts([[60, 35], [61, 42], [59, 42]])} fill={S.amberHi} opacity="0.8" />
      <text x="48" y="54" textAnchor="middle" fontSize="3.2" fill={ART.paper.hi} fontWeight="700" {...PT}>SPIT NOT OFF IN 3 YEARS</text>
      <Rivets x={8} y={46} w={80} n={10} />
      <Grime x={4} y={44} w={88} h={16} seed={460} n={5} op={0.3} /><Scuff x={6} y={46} w={84} h={12} seed={461} n={6} c="rust" />
    </svg>
  );
}

function PropBooth({ variant }) {
  const S = ART.signal;
  const flight = variant === 'flight', curtain = variant === 'curtain', det = variant === 'detention';
  let inner = null;
  if (variant === 'private') {
    inner = (
      <g>
        <Bev t="polygon" points={pts([[6, 56], [6, 20], [14, 14], [82, 14], [90, 20], [90, 56]])} c="fabric" o={1} />
        <Seams x={6} y={14} w={84} h={42} cols={6} op={0.35} />
        <Bust x={34} y={26} s={1.4} coat="steel" back hair={ART.skin.hair} /><Bust x={58} y={26} s={1.4} coat="steel" back hair={ART.dark.hi} />
        <Bev t="polygon" points={pts([[12, 56], [12, 46], [84, 46], [84, 56]])} c="wood" o={0.8} />
        <g stroke={S.amberHi} strokeWidth="0.7" opacity="0.7"><line x1="64" y1="22" x2="92" y2="30" /><line x1="64" y1="26" x2="92" y2="40" /></g><polygon points={pts([[84, 34], [94, 34], [94, 46], [84, 46]])} fill={ART.amber.hi} opacity="0.6" /><text x="89" y="52" textAnchor="middle" fontSize="2.2" fill={S.amberHi} {...PT}>EXIT</text>
        <text x="48" y="10" textAnchor="middle" fontSize="2.6" fill={ART.beige.base} {...PT}>2 GREY COATS: NO DRINKS</text>
      </g>
    );
  } else if (variant === 'corner' || variant === 'senators') {
    const sen = variant === 'senators';
    inner = (
      <g>
        <Bev t="polygon" points={pts([[6, 58], [6, 10], [90, 10], [90, 58]])} c="deepglass" o={0.9} />
        <rect x="10" y="14" width="76" height="30" fill={ART.screen.space} />{sen ? null : [20, 40, 62].map((x) => <rect key={x} x={x} y={32 + (x % 3) * 2} width="6" height={12 - (x % 3) * 2} fill={ART.dark.hi} />)}
        <Glow cx={72} cy={22} r={7} c={S.amber} opacity={0.5} />
        <Bev t="polygon" points={pts(ngon(48, 54, 30, 6, 10))} c="bronze" o={0.9} />
        <Bust x={30} y={30} s={1.5} coat={sen ? 'deepglass' : 'dark'} skin={ART.skin.light} />
        <Bust x={66} y={30} s={1.5} coat={sen ? 'fabric' : 'steel'} skin={ART.skin.dark} hair={ART.dark.hi} />
        {sen ? <g>{[30, 66].map((x) => <g key={x}><polygon points={pts([[x - 6, 34], [x + 6, 34], [x + 4, 40], [x - 4, 40]])} fill={ART.brass.base} /></g>)}<Slab x={40} y={46} w={16} h={5} k={0.8} c="dark" o={0.5} /><text x="48" y="49.8" textAnchor="middle" fontSize="2.2" fill={S.cyanHi} {...PT}>BUDGET LINES</text></g> : <g><polygon points={pts([[42, 48], [54, 48], [54, 53], [42, 53]])} fill={S.cyan} style={{ animation:'ship-blink 3s steps(2) infinite' }} /><text x="48" y="8" textAnchor="middle" fontSize="2.4" fill={ART.beige.base} {...PT}>DATACARD: SLID ACROSS. POCKETED.</text></g>}
        {sen && <g stroke={S.violet} strokeWidth="0.7" fill="none" opacity="0.8"><path d="M46 22 C48 18 52 18 54 22" /><path d="M44 20 C48 14 52 14 56 20" /></g>}
      </g>
    );
  } else if (det) {
    inner = (
      <g>
        <Bev t="polygon" points={pts([[6, 58], [6, 12], [90, 12], [90, 58]])} c="dark" o={1} />
        <Seams x={6} y={12} w={84} h={46} cols={5} op={0.35} />
        {[28, 66].map((x) => <g key={x}><Bev t="polygon" points={pts(ngon(x, 32, 9, 9, 8))} c="steel" o={0.7} /><polygon points={pts(ngon(x, 32, 5, 5, 8))} fill={ART.dark.shade} /><polygon points={pts(ngon(x, 32, 7.2, 7.2, 8))} fill="none" stroke={S.cyan} strokeWidth="0.9" style={{ animation:'holo-flicker 2s ease-in-out infinite' }} /></g>)}
        <g><Bev t="polygon" points={pts([[40, 40], [56, 40], [56, 48], [40, 48]])} c="dark" o={0.5} /><polygon points={pts([[44, 40], [48, 36], [50, 40]])} fill={ART.steel.hi} /><circle cx="51" cy="37" r="1.2" fill={S.amberHi} style={{ animation:'ship-spark 1.2s steps(2) infinite' }} /><text x="48" y="45.6" textAnchor="middle" fontSize="2" fill={S.redHi} {...PT}>LOCK BROKEN</text></g>
        <Bev t="polygon" points={pts([[10, 56], [10, 48], [86, 48], [86, 56]])} c="steel" o={0.7} />
        <rect x="30" y="16" width="36" height="9" fill={ART.screen.red} /><text x="48" y="22" textAnchor="middle" fontSize="2.8" fill={S.redHi} fontWeight="700" {...PT}>14 DETAINED / 6 MO</text>
      </g>
    );
  } else if (flight) {
    inner = (
      <g>
        <Bev t="polygon" points={pts([[6, 58], [6, 10], [90, 10], [90, 58]])} c="deepglass" o={1} />
        <rect x="10" y="14" width="76" height="22" fill={ART.glass.base} opacity="0.55" /><rect x="10" y="14" width="76" height="3" fill={ART.glass.hi} opacity="0.7" />
        {[20, 46, 72].map((x) => <g key={x}><polygon points={pts([[x, 36], [x + 4, 36], [x + 14, 14], [x + 10, 14]])} fill={ART.glass.hi} opacity="0.25" /></g>)}
        <Bev t="polygon" points={pts([[10, 56], [10, 38], [86, 38], [86, 56]])} c="dark" o={0.9} />
        {[0, 1, 2].map((i) => <g key={i}><rect x={14 + i * 26} y="40" width="22" height="12" fill={ART.screen.green} />{i === 0 ? <g><polygon points={pts(ngon(25, 46, 5, 5, 12))} fill="none" stroke={S.green} strokeWidth="0.6" /><line x1="25" y1="46" x2="29" y2="42" stroke={S.greenHi} strokeWidth="0.8" style={{ animation:'holo-flicker 1.4s ease-in-out infinite' }} /></g> : <g>{[0, 1, 2].map((j) => <rect key={j} x={16 + i * 26} y={42 + j * 3.4} width={14 + ((i + j) % 3) * 3} height="1.2" fill={S.greenHi} />)}</g>}</g>)}
        <rect x="66" y="40" width="16" height="12" fill={ART.screen.red} /><text x="74" y="45" textAnchor="middle" fontSize="2.4" fill={S.redHi} fontWeight="700" {...PT}>GAP 12 MIN</text><text x="74" y="49.4" textAnchor="middle" fontSize="2" fill={S.redHi} {...PT}>MALFUNCTION</text>
        <Lights x={12} y={54} n={10} gap={7} r={0.8} seed={4} />
      </g>
    );
  } else {
    inner = (
      <g>
        <Bev t="polygon" points={pts([[6, 58], [6, 12], [90, 12], [90, 58]])} c="dark" o={1} />
        <polygon points={pts([[10, 56], [10, 14], [48, 14], [48, 56]])} fill={ART.fabric.base} /><polygon points={pts([[10, 56], [10, 14], [16, 14], [16, 56]])} fill={ART.fabric.hi} opacity="0.6" />
        {Array.from({ length: 6 }, (_, i) => <line key={i} x1={14 + i * 6} y1="14" x2={13 + i * 6} y2="56" stroke={ART.fabric.shade} strokeWidth="1.4" />)}
        <rect x="8" y="12" width="42" height="2.4" fill={ART.steel.hi} />
        <Bev t="polygon" points={pts([[54, 54], [54, 40], [86, 40], [86, 54]])} c="wood" o={0.8} />
        <g transform="rotate(-6 70 36)"><Bev t="polygon" points={pts([[58, 40], [80, 40], [80, 26], [58, 26]])} c="dark" o={0.6} /><rect x="60" y="28" width="18" height="10" fill={ART.screen.cyan} />{[0, 1, 2].map((i) => <rect key={i} x="62" y={30 + i * 2.8} width={10 + (i % 2) * 4} height="1.2" fill={S.cyanHi} />)}</g>
        <text x="70" y="22" textAnchor="middle" fontSize="2.4" fill={S.cyanHi} {...PT}>SHELL CORP TRACE: +1 STEP</text>
        <polygon points={pts([[50, 14], [58, 14], [54, 56], [48, 56]])} fill={ART.fabric.shade} opacity="0.5" />
      </g>
    );
  }
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {inner}
      <Rivets x={8} y={59} w={80} n={10} />
      <Grime x={6} y={12} w={84} h={46} seed={(variant || 'b').length + 470} n={5} op={0.22} /><Scuff x={8} y={14} w={80} h={42} seed={(variant || 'b').length + 471} n={7} />
    </svg>
  );
}

function PropGuardPost() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[6, 58], [6, 8], [90, 8], [90, 58]])} c="pearl" o={1} />
      {[8, 78].map((x) => <g key={x}><polygon points={pts([[x, 8], [x + 10, 8], [x + 10, 34], [x + 5, 40], [x, 34]])} fill={ART.red.base} /><polygon points={pts([[x, 8], [x + 3, 8], [x + 3, 36], [x, 34]])} fill={ART.red.hi} opacity="0.7" /><Glyph k="cog" x={x + 5} y={20} s={3.4} c={S.gold} /></g>)}
      <Glyph k="cog" x={48} y={22} s={9} c={ART.hull.base} /><polygon points={pts(ngon(48, 22, 11, 11, 16))} fill="none" stroke={ART.steel.hi} strokeWidth="0.8" />
      <text x="62" y="40" textAnchor="middle" fontSize="3.2" fill={ART.ink} fontWeight="700" {...PT}>SENATE GUARD POST</text>
      <Bev t="polygon" points={pts([[10, 58], [10, 44], [86, 44], [86, 58]])} c="steel" o={1} />
      <Slab x={14} y={34} w={22} h={12} k={1} c="dark" o={0.6} /><rect x="16" y="36" width="18" height="7" fill={ART.screen.cyan} /><text x="25" y="41.6" textAnchor="middle" fontSize="2.2" fill={S.cyanHi} {...PT}>RESP 4 MIN</text>
      <g><Bev t="polygon" points={pts([[58, 46], [76, 46], [76, 40], [58, 40]])} c="paper" o={0.3} /><text x="67" y="44.6" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} {...PT}>3 UNLOGGED</text></g>
      <g><Bev t="polygon" points={pts(ngon(50, 50, 4, 2.4, 6))} c="red" o={0.4} /></g><Lights x={40} y={53} n={4} gap={3.6} seed={6} />
      <Hazard x={10} y={55} w={76} h={3} /><Seams x={6} y={8} w={84} h={50} cols={4} op={0.2} /><Rivets x={9} y={10} w={78} n={10} />
      <Grime x={6} y={10} w={84} h={48} seed={480} n={5} op={0.16} /><Scuff x={8} y={12} w={80} h={44} seed={481} n={7} c="pearl" />
    </svg>
  );
}

function PropInterrogationRig() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[6, 38], [6, 6], [90, 6], [90, 38]])} c="dark" o={1} />
      <rect x="12" y="10" width="40" height="24" fill={ART.glass.shade} /><rect x="12" y="10" width="40" height="3" fill={ART.glass.base} opacity="0.7" /><polygon points={pts([[16, 34], [24, 10], [30, 10], [22, 34]])} fill={ART.glass.hi} opacity="0.3" />
      <text x="24" y="32.6" textAnchor="middle" fontSize="2.2" fill={ART.glass.hi} {...PT}>ONE WAY GLASS</text>
      <Slab x={58} y={10} w={28} h={14} k={1} c="dark" o={0.6} /><rect x="60" y="12" width="24" height="10" fill={ART.screen.red} /><text x="72" y="20.4" textAnchor="middle" fontSize="6" fill={S.red} fontWeight="700" {...PT}>30:00</text>
      <Slab x={58} y={26} w={28} h={7} k={0.8} c="dark" o={0.5} /><text x="72" y="31" textAnchor="middle" fontSize="2.4" fill={S.redHi} {...PT}>FOUR DETECTIVES FAILED</text>
      <polygon points={pts([[34, 6], [38, 6], [40, 18], [32, 18]])} fill={ART.steel.shade} /><Bev t="polygon" points={pts([[26, 18], [46, 18], [48, 24], [24, 24]])} c="steel" o={0.8} /><polygon points={pts([[26, 24], [46, 24], [52, 56], [20, 56]])} fill={S.amberHi} opacity="0.12" />
      <Bev t="polygon" points={pts([[24, 58], [24, 42], [28, 36], [44, 36], [48, 42], [48, 58]])} c="steel" o={1} />
      <Bev t="polygon" points={pts([[22, 46], [22, 40], [26, 40], [26, 46]])} c="dark" o={0.5} /><Bev t="polygon" points={pts([[46, 46], [46, 40], [50, 40], [50, 46]])} c="dark" o={0.5} />
      {[[24, 44], [48, 44], [30, 56], [42, 56]].map(([x, y], i) => <polygon key={i} points={pts(ngon(x, y, 2.6, 2.6, 6))} fill="none" stroke={ART.steel.hi} strokeWidth="1" />)}
      <Bev t="polygon" points={pts([[60, 56], [60, 46], [88, 46], [88, 56]])} c="steel" o={0.8} /><Slab x={64} y={42} w={10} h={5} k={0.8} c="dark" o={0.5} /><circle cx="69" cy="44.4" r="1.2" fill={S.red} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} />
      <Bev t="polygon" points={pts([[78, 44], [86, 44], [86, 38], [80, 38]])} c="dark" o={0.4} />
      <Rivets x={8} y={59} w={80} n={10} /><Seams x={6} y={6} w={84} h={32} cols={3} op={0.3} />
      <Grime x={6} y={8} w={84} h={50} seed={490} n={5} op={0.22} /><Scuff x={8} y={10} w={80} h={46} seed={491} n={7} c="steel" />
    </svg>
  );
}

function PropCeremonyDais() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={59} rx={44} ry={4.4} />
      <polygon points={pts([[40, 46], [56, 46], [66, 6], [30, 6]])} fill={S.cyan} opacity="0.12" />
      <g style={{ animation:'holo-flicker 3s ease-in-out infinite' }}>
        <polygon points={pts([[48, 6], [51, 14], [59, 14], [53, 19], [55, 27], [48, 22], [41, 27], [43, 19], [37, 14], [45, 14]])} fill={S.gold} />
        <polygon points={pts(ngon(48, 18, 4, 4, 8))} fill={S.cyan} /><polygon points={pts([[44, 28], [52, 28], [52, 38], [48, 35], [44, 38]])} fill={ART.red.base} />
        <text x="48" y="43" textAnchor="middle" fontSize="2.8" fill={S.cyanHi} fontWeight="700" {...PT}>SENATE HONOR CROSS</text>
        <ellipse cx="48" cy="46" rx="22" ry="3" fill="none" stroke={S.cyanHi} strokeWidth="0.6" opacity="0.7" />
      </g>
      <polygon points={pts(ngon(48, 54, 42, 9, 10, Math.PI / 10))} fill={ART.dark.shade} /><Bev t="polygon" points={pts(ngon(48, 52, 40, 8, 10, Math.PI / 10))} c="steel" o={1.2} />
      <polygon points={pts(ngon(48, 51, 32, 6, 10, Math.PI / 10))} fill={ART.pearl.base} /><polygon points={pts(ngon(48, 51, 24, 4.4, 10, Math.PI / 10))} fill={ART.pearl.hi} />
      <Glyph k="cog" x={48} y={51} s={5} c={ART.red.base} />
      <Bev t="polygon" points={pts([[8, 54], [8, 46], [18, 46], [18, 54]])} c="dark" o={0.5} /><Bev t="polygon" points={pts([[78, 54], [78, 46], [88, 46], [88, 54]])} c="dark" o={0.5} /><rect x="10" y="47.4" width="6" height="2" fill={S.cyan} /><rect x="80" y="47.4" width="6" height="2" fill={S.cyan} />
      <Hazard x={8} y={58} w={80} h={3} /><Rivets x={10} y={55} w={76} n={10} />
      <Grime x={6} y={44} w={84} h={16} seed={500} n={4} op={0.16} /><Scuff x={8} y={46} w={80} h={12} seed={501} n={6} c="steel" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// PENTHOUSE, ARENA AND SPECIAL SET PIECES
// ---------------------------------------------------------------------------
function PropMonitorWall() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.8} />
      <Slab x={2} y={2} w={124} h={56} k={3} c="dark" o={1.1} />
      {Array.from({ length: 15 }, (_, i) => {
        const col = i % 5, row = Math.floor(i / 5), x = 28 + col * 19.4, y = 8 + row * 16.6, hot = i === 7;
        return (
          <g key={i}>
            <Slab x={x} y={y} w={18} h={15} k={0.8} c="steel" o={0.5} />
            <rect x={x + 1.4} y={y + 1.4} width="15.2" height="12.2" fill={hot ? ART.screen.red : ART.screen.green} />
            {i % 3 === 0 ? <g stroke={S.greenHi} strokeWidth="0.5" fill="none"><polyline points={pts([[x + 3, y + 11], [x + 3, y + 4], [x + 8, y + 4], [x + 8, y + 8], [x + 14, y + 8]])} /></g> : i % 3 === 1 ? <g fill={hot ? S.redHi : S.greenHi} opacity="0.85"><rect x={x + 4} y={y + 4} width="3" height="7" /><rect x={x + 9} y={y + 6} width="3" height="5" /></g> : <polygon points={pts(ngon(x + 9, y + 7.4, 3.6, 3.6, 6))} fill="none" stroke={S.greenHi} strokeWidth="0.6" />}
            <rect x={x + 1.4} y={y + 1.4} width="15.2" height="1" fill={S.greenHi} opacity="0.35" style={{ animation:`ship-blink ${2 + (i % 4)}s steps(2) infinite` }} />
            <text x={x + 2} y={y + 13} fontSize="1.8" fill={S.greenHi} {...PT}>{'FEED ' + (i + 1)}</text>
          </g>
        );
      })}
      {/* heat meter */}
      <Slab x={7} y={8} w={16} h={48} k={1.4} c="dark" o={0.7} />
      <text x="15" y="14" textAnchor="middle" fontSize="2.6" fill={S.amberHi} fontWeight="700" {...PT}>HEAT</text>
      {[['green', 18], ['amber', 26], ['warn', 34], ['red', 42]].map(([k, y]) => <rect key={k} x="10" y={y} width="10" height="7" fill={S[k]} opacity={k === 'red' ? 0.9 : 0.5} />)}
      <polygon points={pts([[8, 31], [10, 29], [10, 33]])} fill={S.white} />
      <text x="15" y="53" textAnchor="middle" fontSize="2.4" fill={S.redHi} {...PT}>CLIMBING</text>
      <text x="64" y="6.4" textAnchor="middle" fontSize="2.6" fill={S.cyanHi} fontWeight="700" {...PT}>L1450 SURVEILLANCE: 15 FEEDS, 3 CSF ROUTES</text>
      <Rivets x={6} y={4} w={116} n={14} /><Lights x={30} y={57} n={14} gap={6} r={0.7} seed={7} />
      <Grime x={4} y={6} w={120} h={50} seed={510} n={6} op={0.2} /><Scuff x={6} y={8} w={116} h={46} seed={511} n={8} c="steel" />
    </svg>
  );
}

function PropRosterWall() {
  const S = ART.signal;
  const roles = [['badge', 'ENFORCER', 'red'], ['wave', 'SLICER', 'green'], ['drop', 'SMUGGLER', 'amber'], ['coin', 'FIXER', 'cyan']];
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Slab x={2} y={2} w={92} h={56} k={3} c="dark" o={1} />
      <text x="48" y="9" textAnchor="middle" fontSize="3.4" fill={S.cyanHi} fontWeight="700" {...PT}>AGENT ROSTER</text>
      {Array.from({ length: 6 }, (_, i) => {
        const col = i % 3, row = Math.floor(i / 3), x = 6 + col * 29.4, y = 14 + row * 22, role = roles[i];
        return (
          <g key={i}>
            <Slab x={x} y={y} w={27} h={20} k={1.2} c="steel" o={0.7} />
            <rect x={x + 1.6} y={y + 1.6} width="23.8" height="16.8" fill={role ? ART.screen.cyan : ART.screen.off} />
            {role ? <g><Glyph k={role[0]} x={x + 8} y={y + 8.4} s={4} c={S[role[2]]} /><text x={x + 15} y={y + 7} fontSize="2.2" fill={S.cyanHi} fontWeight="700" {...PT}>{role[1]}</text><rect x={x + 15} y={y + 9} width="8" height="1.2" fill={S.cyan} opacity="0.7" /><rect x={x + 15} y={y + 11.4} width="5" height="1.2" fill={S.cyan} opacity="0.5" /><circle cx={x + 22} cy={y + 15} r="1.2" fill={i === 1 ? S.red : S.green} /></g> : <text x={x + 13.5} y={y + 11} textAnchor="middle" fontSize="2.4" fill={ART.dark.hi} {...PT}>EMPTY SLOT</text>}
            <rect x={x + 1.6} y={y + 1.6} width="23.8" height="1" fill={S.cyanHi} opacity="0.3" />
          </g>
        );
      })}
      <text x="48" y="58.4" textAnchor="middle" fontSize="2.2" fill={S.amberHi} {...PT}>COST VS WORTH: DO THEY MATCH?</text>
      <Rivets x={6} y={4} w={84} n={10} />
      <Grime x={4} y={6} w={88} h={50} seed={520} n={5} op={0.2} /><Scuff x={6} y={8} w={84} h={46} seed={521} n={7} c="steel" />
    </svg>
  );
}

function PropTrophyWall() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.8} />
      <Slab x={2} y={2} w={124} h={56} k={3} c="wood" o={1.1} />
      <Seams x={2} y={2} w={124} h={56} cols={7} op={0.3} />
      {[[10, 'brass'], [34, 'bronze'], [58, 'brass'], [82, 'copper'], [106, 'brass']].map(([x, c], i) => <g key={i}><Slab x={x} y={8} w={18} h={11} k={1} c={c} o={0.6} /><rect x={x + 2} y="10.4" width="14" height="1.4" fill={ART.ink} opacity="0.5" /><rect x={x + 2} y="13.4" width="10" height="1.2" fill={ART.ink} opacity="0.4" /><rect x={x + 2} y="15.8" width="12" height="1.2" fill={ART.ink} opacity="0.4" /></g>)}
      <g><Bev t="polygon" points={pts([[12, 38], [38, 24], [40, 27], [14, 41]])} c="hull" o={0.5} /><Bev t="polygon" points={pts([[12, 24], [38, 38], [40, 35], [14, 21]])} c="hull" o={0.5} /><polygon points={pts(ngon(26, 31, 4, 4, 8))} fill={ART.brass.base} /></g>
      <g><Bev t="polygon" points={pts([[50, 42], [50, 28], [60, 26], [76, 28], [82, 36], [78, 44], [60, 44]])} c="dark" o={0.7} /><polygon points={pts([[78, 36], [92, 34], [92, 40], [80, 42]])} fill={ART.steel.base} /><rect x="54" y="32" width="8" height="2" fill={S.amber} /></g>
      <g><Bev t="polygon" points={pts([[100, 44], [96, 30], [102, 22], [114, 22], [120, 30], [116, 44]])} c="steel" o={0.8} /><polygon points={pts([[102, 32], [114, 32], [114, 36], [102, 36]])} fill={ART.ink} /><rect x="106" y="25" width="4" height="4" fill={S.crimson} /></g>
      {[[10, 48], [34, 48], [58, 50], [82, 48]].map(([x, y], i) => <g key={i}><polygon points={pts([[x, y], [x + 14, y], [x + 14, y + 8], [x, y + 8]])} fill={ART.paper.base} /><circle cx={x + 11} cy={y + 4} r="2.2" fill={ART.red.base} /><rect x={x + 2} y={y + 2} width="6" height="1" fill={ART.note.ink} /><rect x={x + 2} y={y + 4.4} width="5" height="1" fill={ART.note.ink} /></g>)}
      <g><Bev t="polygon" points={pts([[104, 52], [124, 52], [124, 56], [104, 56]])} c="brass" o={0.5} /></g>
      <text x="64" y="6.4" textAnchor="middle" fontSize="2.6" fill={S.gold} fontWeight="700" {...PT}>15 YEARS: SOME EARNED, MOST TAKEN</text>
      <Rivets x={6} y={4} w={116} n={14} />
      <Grime x={4} y={6} w={120} h={50} seed={530} n={6} op={0.2} /><Scuff x={6} y={8} w={116} h={46} seed={531} n={9} c="wood" />
    </svg>
  );
}

function PropDuelRing() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <polygon points={pts(ngon(48, 62, 46, 28, 12, Math.PI / 12))} fill={ART.dark.shade} opacity="0.85" />
      <polygon points={pts(ngon(48, 60, 44, 26, 12, Math.PI / 12))} fill={ART.dark.base} />
      <polygon points={pts(ngon(48, 60, 38, 21, 12, Math.PI / 12))} fill="none" stroke={S.gold} strokeWidth="1.6" />
      <polygon points={pts(ngon(48, 60, 33, 17, 12, Math.PI / 12))} fill="none" stroke={S.amber} strokeWidth="0.7" opacity="0.7" />
      <Glow cx={48} cy={60} rx={34} ry={17} c={S.amber} opacity={0.3} />
      <line x1="48" y1="38" x2="48" y2="82" stroke={S.gold} strokeWidth="0.8" strokeDasharray="3 2.4" opacity="0.8" />
      <line x1="10" y1="60" x2="86" y2="60" stroke={S.gold} strokeWidth="0.8" strokeDasharray="3 2.4" opacity="0.6" />
      {[[48, 44], [48, 76]].map(([x, y], i) => <g key={i}><polygon points={pts(ngon(x, y, 8, 4, 8))} fill={ART.ink} opacity="0.6" /><polygon points={pts(ngon(x, y, 6, 3, 8))} fill={i ? S.cyan : S.crimson} opacity="0.7" style={{ animation:`ship-blink ${2 + i}s steps(3) infinite` }} /></g>)}
      <g fill={S.amberHi}>{[[16, 60], [80, 60], [48, 38], [48, 82]].map(([x, y], i) => <polygon key={i} points={pts([[x - 2.6, y + 1.6], [x, y - 2.6], [x + 2.6, y + 1.6]])} />)}</g>
      <text x="48" y="18" textAnchor="middle" fontSize="3.2" fill={S.amberHi} fontWeight="700" {...PT}>12 YEARS</text>
      <text x="48" y="24" textAnchor="middle" fontSize="2.6" fill={ART.beige.base} {...PT}>BOTH HAVE MADE THEIR CALCULATION</text>
      <polygon points={pts([[40, 28], [56, 28], [52, 36], [44, 36]])} fill={S.amber} opacity="0.12" />
      <Rivets x={10} y={88} w={76} n={10} /><Lights x={20} y={86} n={9} gap={7} r={0.8} seed={9} />
      <Grime x={6} y={36} w={84} h={50} seed={540} n={5} op={0.2} /><Scuff x={8} y={38} w={80} h={46} seed={541} n={8} c="dark" />
    </svg>
  );
}

function PropArenaRing() {
  const S = ART.signal;
  const ring = (rx, ry, n = 12) => pts(ngon(80, 62, rx, ry, n, Math.PI / n));
  return (
    <svg viewBox="0 0 160 128" width="160" height="128" style={PROP_STYLE}>
      <PropShadow cx={80} cy={118} rx={74} ry={6} />
      <polygon points={ring(76, 56)} fill={ART.dark.shade} /><Bev t="polygon" points={pts(ngon(80, 60, 74, 54, 12, Math.PI / 12))} c="steel" o={1.4} />
      <polygon points={pts(ngon(80, 62, 66, 46, 12, Math.PI / 12))} fill={ART.steel.shade} />
      <polygon points={pts(ngon(80, 66, 62, 42, 12, Math.PI / 12))} fill={ART.rust.shade} />
      <polygon points={pts(ngon(80, 68, 56, 36, 12, Math.PI / 12))} fill={ART.sand.base} />
      <polygon points={pts(ngon(80, 68, 56, 36, 12, Math.PI / 12))} fill="none" stroke={ART.sand.shade} strokeWidth="1.4" />
      <polygon points={pts(ngon(78, 66, 44, 26, 12, Math.PI / 12))} fill={ART.sand.hi} opacity="0.5" />
      <polygon points={pts(ngon(80, 68, 18, 10, 10))} fill="none" stroke={ART.sand.shade} strokeWidth="1.2" />
      <line x1="80" y1="58" x2="80" y2="78" stroke={ART.sand.shade} strokeWidth="1" />
      {[[56, 62, 8, 3], [100, 74, 10, 4], [84, 52, 6, 2.4], [66, 80, 7, 3]].map(([x, y, rx, ry], i) => <polygon key={i} points={pts(ngon(x, y, rx, ry, 7))} fill={ART.red.shade} opacity="0.8" />)}
      <g stroke={ART.sand.shade} strokeWidth="0.9" fill="none">{[[40, 70], [112, 62], [90, 86]].map(([x, y], i) => <g key={i}><line x1={x} y1={y} x2={x + 7} y2={y - 2} /><line x1={x} y1={y + 2} x2={x + 7} y2={y} /><line x1={x} y1={y + 4} x2={x + 7} y2={y + 2} /></g>)}</g>
      {Array.from({ length: 16 }, (_, i) => { const a = (i / 16) * Math.PI * 2 + 0.2; return <g key={i}><circle cx={80 + Math.cos(a) * 68} cy={62 + Math.sin(a) * 49} r="1.8" fill={ART.dark.shade} /><circle cx={80 + Math.cos(a) * 68} cy={62 + Math.sin(a) * 49} r="1.1" fill={i % 4 === 0 ? S.red : S.amberHi} style={{ animation:`ship-blink ${1.6 + (i % 5) * 0.4}s steps(2) infinite` }} /></g>; })}
      {[[10, 62], [150, 62]].map(([x, y], i) => <g key={i}><Bev t="polygon" points={pts([[x - 8, y + 10], [x - 8, y - 10], [x + 8, y - 10], [x + 8, y + 10]])} c="dark" o={0.8} /><polygon points={pts([[x - 5, y + 10], [x - 5, y - 6], [x + 5, y - 6], [x + 5, y + 10]])} fill={ART.ink} /><rect x={x - 6} y={y - 12} width="12" height="2" fill={S.red} /></g>)}
      <Slab x={56} y={4} w={48} h={9} k={1.2} c="dark" o={0.8} /><text x="80" y="10.6" textAnchor="middle" fontSize="4.6" fill={S.crimson} fontWeight="700" {...PT}>THE PIT</text>
      <text x="80" y="124" textAnchor="middle" fontSize="2.8" fill={ART.beige.hi} {...PT}>2 M DROP. NO REFEREES. WINNER WALKS OUT.</text>
      <Rivets x={20} y={112} w={120} n={14} />
      <Grime x={10} y={30} w={140} h={86} seed={550} n={7} op={0.22} /><Scuff x={12} y={32} w={136} h={82} seed={551} n={10} c="steel" />
    </svg>
  );
}

function PropPitGate({ variant }) {
  const S = ART.signal;
  const droid = variant === 'droid';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[4, 60], [4, 10], [10, 4], [86, 4], [92, 10], [92, 60]])} c="rust" o={1.1} />
      <Seams x={4} y={4} w={88} h={56} cols={4} rows={1} op={0.3} />
      {droid ? (
        <g>
          <polygon points={pts(ngon(48, 34, 26, 26, 12))} fill={ART.ink} /><Bev t="polygon" points={pts(ngon(48, 34, 24, 24, 12))} c="steel" o={1} /><polygon points={pts(ngon(48, 34, 18, 18, 12))} fill={ART.ink} />
          {Array.from({ length: 12 }, (_, i) => { const a = (i / 12) * Math.PI * 2; return <circle key={i} cx={48 + Math.cos(a) * 21} cy={34 + Math.sin(a) * 21} r="1.2" fill={ART.steel.hi} />; })}
          <polygon points={pts([[48, 34], [66, 16], [70, 22], [52, 40]])} fill={ART.steel.base} />
          <Glow cx={48} cy={38} r={12} c={S.amber} opacity={0.5} />
          {[[16, 54], [26, 56], [70, 55], [78, 57]].map(([x, y], i) => <g key={i} transform={`rotate(${(i * 23) % 40 - 20} ${x} ${y})`}><polygon points={pts([[x, y], [x + 8, y], [x + 8, y + 4], [x, y + 4]])} fill={ART.paper.base} /><rect x={x + 1.4} y={y + 1.4} width="4" height="0.9" fill={ART.note.redInk} /></g>)}
          <text x="48" y="12" textAnchor="middle" fontSize="3.4" fill={S.magenta} fontWeight="700" {...PT}>DROID ARENA</text>
          <text x="48" y="62" textAnchor="middle" fontSize="2.4" fill={S.amberHi} {...PT}>BETTING SLIPS: STILL WARM</text>
        </g>
      ) : (
        <g>
          <polygon points={pts([[22, 60], [22, 18], [30, 10], [66, 10], [74, 18], [74, 60]])} fill={ART.ink} />
          {[0, 1, 2, 3, 4, 5].map((i) => <polygon key={i} points={pts([[26 + i * 8, 60], [26 + i * 8, 22], [28 + i * 8, 22], [28 + i * 8, 60]])} fill={ART.steel.base} />)}
          <polygon points={pts([[22, 28], [74, 28], [74, 24], [22, 24]])} fill={ART.steel.shade} />
          <Hazard x={22} y={54} w={52} h={5} />
          <polygon points={pts([[28, 60], [68, 60], [76, 63], [20, 63]])} fill={ART.sand.base} /><polygon points={pts([[34, 60], [62, 60], [66, 62], [30, 62]])} fill={ART.sand.hi} />
          <Slab x={30} y={4} w={36} h={8} k={1} c="dark" o={0.7} /><text x="48" y="10" textAnchor="middle" fontSize="4" fill={S.crimson} fontWeight="700" {...PT}>THE ARENA</text>
          <polygon points={pts([[8, 20], [18, 20], [18, 50], [8, 50]])} fill={ART.dark.base} /><polygon points={pts([[78, 20], [88, 20], [88, 50], [78, 50]])} fill={ART.dark.base} />
          <rect x="9" y="24" width="8" height="6" fill={ART.paper.base} /><rect x="79" y="24" width="8" height="6" fill={ART.paper.base} /><text x="13" y="29" textAnchor="middle" fontSize="2" fill={ART.note.ink} {...PT}>ENTER</text><text x="83" y="29" textAnchor="middle" fontSize="2" fill={ART.note.ink} {...PT}>ENTER</text>
        </g>
      )}
      <Rivets x={8} y={6} w={80} n={10} />
      <Grime x={4} y={6} w={88} h={54} seed={droid ? 560 : 562} n={6} op={0.28} /><Scuff x={6} y={8} w={84} h={50} seed={droid ? 561 : 563} n={8} c="rust" />
    </svg>
  );
}

function PropRailRun() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.6} />
      <polygon points={pts([[4, 62], [124, 62], [118, 54], [10, 54]])} fill={ART.dark.shade} opacity="0.6" />
      {Array.from({ length: 11 }, (_, i) => <Bev key={i} t="polygon" points={pts([[6 + i * 11.6, 58], [6 + i * 11.6, 18], [9 + i * 11.6, 18], [9 + i * 11.6, 58]])} c="rust" o={0.7} />)}
      {[22, 34, 46].map((y) => <g key={y}><rect x="6" y={y} width="116" height="3" fill={ART.rust.base} /><rect x="6" y={y} width="116" height="1" fill={ART.rust.hi} /><rect x="6" y={y + 3} width="116" height="1" fill={ART.rust.shade} /></g>)}
      <g fill={ART.steel.base} opacity="0.7">{[14, 58, 98].map((x) => <polygon key={x} points={pts([[x, 22], [x + 10, 22], [x + 6, 30], [x + 2, 30]])} />)}</g>
      <polygon points={pts([[40, 18], [48, 12], [52, 14], [44, 22]])} fill={ART.dark.hi} /><Bev t="polygon" points={pts([[36, 20], [46, 20], [46, 24], [36, 24]])} c="dark" o={0.4} /><circle cx="50" cy="13" r="2" fill={ART.dark.shade} /><circle cx="50" cy="13" r="1" fill={S.cyan} />
      <text x="64" y="12" textAnchor="middle" fontSize="3" fill={S.warn} fontWeight="700" {...PT}>UPPER DECK: WATCH THE EXITS</text>
      <Hazard x={6} y={56} w={116} h={3} /><Rivets x={7} y={20} w={114} n={14} c="rust" />
      <Grime x={4} y={16} w={120} h={42} seed={570} n={8} op={0.34} /><Scuff x={6} y={18} w={116} h={38} seed={571} n={10} c="rust" />
    </svg>
  );
}

function PropFighterAltar() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={2.6} />
      <Bev t="polygon" points={pts([[4, 56], [4, 8], [60, 8], [60, 56]])} c="concrete" o={1} />
      <Seams x={4} y={8} w={56} h={48} cols={2} rows={1} op={0.3} />
      <Bev t="polygon" points={pts([[8, 40], [56, 40], [58, 46], [6, 46]])} c="wood" o={0.9} />
      <Rivets x={10} y={43} w={44} n={6} c="wood" />
      <g><Bev t="polygon" points={pts([[12, 40], [12, 34], [20, 32], [26, 34], [26, 40]])} c="pearl" o={0.5} /><g stroke={ART.beige.shade} strokeWidth="0.7">{[34, 36, 38].map((y) => <line key={y} x1="12" y1={y} x2="26" y2={y - 1.4} />)}</g></g>
      <g><Slab x={32} y={34} w={8} h={6} k={0.8} c="dark" o={0.4} /><rect x="33.4" y="35.4" width="5.2" height="1" fill={S.amber} /><rect x="33.4" y="37.4" width="3.6" height="1" fill={S.amber} opacity="0.7" /></g>
      <g><Bev t="polygon" points={pts([[46, 40], [46, 31], [52, 31], [52, 40]])} c="brass" o={0.5} /><ellipse cx="49" cy="31" rx="3" ry="1.2" fill={ART.ink} /><polygon points={pts([[47.4, 31], [49, 24], [50.6, 31]])} fill={S.amberHi} style={{ animation:'ship-blink 1.4s steps(3) infinite' }} /><Glow cx={49} cy={28} r={7} c={S.amber} opacity={0.6} /></g>
      <polygon points={pts([[40, 26], [46, 22], [48, 26]])} fill={ART.paper.base} opacity="0.3" />
      <text x="32" y="16" textAnchor="middle" fontSize="2.8" fill={ART.beige.hi} fontWeight="700" {...PT}>FIGHTERS OFFER</text>
      <text x="32" y="52" textAnchor="middle" fontSize="2.2" fill={ART.beige.base} {...PT}>MAINTAINED 12 YEARS</text>
      <Grime x={4} y={10} w={56} h={46} seed={580} n={5} op={0.26} /><Scuff x={6} y={12} w={52} h={42} seed={581} n={7} c="concrete" />
    </svg>
  );
}

function PropKyberCluster({ variant }) {
  const S = ART.signal;
  const cav = variant === 'cavern';
  const crystal = (x, y, w, h, k, tilt) => (
    <g key={k} transform={`rotate(${tilt} ${x} ${y})`}>
      <polygon points={pts([[x - w / 2, y], [x - w / 2, y - h * 0.7], [x, y - h], [x + w / 2, y - h * 0.7], [x + w / 2, y]])} fill={ART.glass.shade} transform="translate(1 1.2)" />
      <polygon points={pts([[x - w / 2, y], [x - w / 2, y - h * 0.7], [x, y - h], [x + w / 2, y - h * 0.7], [x + w / 2, y]])} fill={cav ? ART.fabric.hi : S.kyber} />
      <polygon points={pts([[x, y], [x, y - h], [x + w / 2, y - h * 0.7], [x + w / 2, y]])} fill={ART.glass.base} opacity="0.7" />
      <polyline points={pts([[x - w / 4, y - 4], [x - w / 6, y - h * 0.6], [x + w / 8, y - h * 0.4]])} fill="none" stroke={cav ? S.cyanHi : S.crimson} strokeWidth="0.8" opacity="0.85" />
    </g>
  );
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3.4} />
      <Glow cx={48} cy={40} rx={44} ry={20} c={S.kyber} opacity={0.4} style={{ animation:'ship-engine 2.6s ease-in-out infinite' }} />
      <g fill={S.crimson} opacity="0.2" style={{ animation:'steam-rise 4s ease-out infinite' }}><polygon points={pts(ngon(48, 20, 14, 8, 7))} /></g>
      <polygon points={pts([[6, 60], [10, 50], [24, 46], [48, 48], [72, 46], [88, 50], [92, 60]])} fill={ART.dark.shade} /><Bev t="polygon" points={pts([[8, 60], [12, 52], [28, 50], [48, 52], [68, 50], [84, 52], [90, 60]])} c="stone" o={0.9} />
      {crystal(30, 56, 12, 30, 'a', -14)}{crystal(66, 56, 12, 34, 'b', 12)}{crystal(48, 56, 16, 46, 'c', -2)}{crystal(18, 58, 8, 18, 'd', -28)}{crystal(78, 58, 8, 20, 'e', 24)}{crystal(40, 58, 8, 22, 'f', 8)}
      <g stroke={S.crimson} strokeWidth="0.7" fill="none" opacity="0.7"><path d="M10 58 C20 54 24 58 30 56" /><path d="M66 58 C74 55 80 58 90 56" /></g>
      <text x="48" y="8" textAnchor="middle" fontSize="2.8" fill={cav ? S.violet : S.kyber} fontWeight="700" {...PT}>{cav ? 'RESONANCE: PLACED, NOT NATURAL' : 'SITH CONTAMINATION: WRONG'}</text>
      <Rivets x={10} y={58} w={76} n={10} c="stone" />
      <Grime x={6} y={46} w={84} h={14} seed={590} n={4} op={0.3} /><Scuff x={8} y={48} w={80} h={10} seed={591} n={6} c="stone" />
    </svg>
  );
}

function PropSurveyMarker() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={26} ry={4} />
      <polygon points={pts([[6, 60], [10, 50], [26, 46], [46, 47], [58, 52], [60, 60]])} fill={ART.dark.shade} /><Bev t="polygon" points={pts([[8, 60], [12, 52], [28, 49], [46, 50], [56, 54], [58, 60]])} c="stone" o={0.9} />
      <g transform="rotate(-18 34 36)">
        <Bev t="polygon" points={pts([[24, 52], [24, 14], [26, 10], [40, 10], [42, 14], [42, 52]])} c="brass" o={1} />
        <ellipse cx="33" cy="10" rx="9" ry="3" fill={ART.brass.hi} /><ellipse cx="33" cy="10.6" rx="6.4" ry="1.9" fill={ART.ink} />
        <g stroke={ART.brass.shade} strokeWidth="1" fill="none"><circle cx="33" cy="24" r="4" /><line x1="33" y1="20" x2="33" y2="28" /><line x1="29" y1="24" x2="37" y2="24" /><polyline points={pts([[27, 34], [33, 38], [39, 34]])} /><polyline points={pts([[27, 42], [33, 46], [39, 42]])} /></g>
        <rect x="24" y="14" width="2.4" height="38" fill={ART.brass.hi} opacity="0.7" />
      </g>
      <g transform="rotate(8 14 50)"><polygon points={pts([[6, 52], [18, 50], [19, 57], [7, 58]])} fill={ART.paper.base} /><text x="12.5" y="55" textAnchor="middle" fontSize="1.7" fill={ART.note.ink} {...PT}>SEALED</text></g>
      <text x="32" y="8" textAnchor="middle" fontSize="2.6" fill={S.amberHi} fontWeight="700" {...PT}>DO NOT RETURN</text>
      <text x="32" y="63" textAnchor="middle" fontSize="2.2" fill={ART.beige.base} {...PT}>WITHOUT SPECIALIST CONTAINMENT</text>
      <Rivets x={26} y={50} w={22} n={4} c="brass" />
      <Grime x={6} y={10} w={52} h={50} seed={600} n={5} op={0.26} /><Scuff x={8} y={12} w={48} h={46} seed={601} n={6} c="brass" />
    </svg>
  );
}

function PropContainmentSeal() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3.4} />
      <Glow cx={48} cy={34} rx={44} ry={26} c={S.magenta} opacity={0.3} style={{ animation:'ship-engine 3s ease-in-out infinite' }} />
      <Bev t="polygon" points={pts([[6, 60], [6, 10], [14, 4], [82, 4], [90, 10], [90, 60]])} c="stone" o={1.2} />
      <Seams x={6} y={4} w={84} h={56} cols={3} rows={1} op={0.35} />
      <polygon points={pts(ngon(48, 34, 26, 26, 12))} fill={ART.ink} /><Bev t="polygon" points={pts(ngon(48, 34, 24, 24, 12))} c="dark" o={1} />
      {[18, 12, 6].map((r, i) => <polygon key={r} points={pts(ngon(48, 34, r, r, 12, i * 0.3))} fill="none" stroke={[S.magenta, S.orange, S.amberHi][i]} strokeWidth="1.2" opacity={0.8 - i * 0.1} />)}
      {Array.from({ length: 12 }, (_, i) => { const a = (i / 12) * Math.PI * 2; return <g key={i}><line x1={48 + Math.cos(a) * 7} y1={34 + Math.sin(a) * 7} x2={48 + Math.cos(a) * 17} y2={34 + Math.sin(a) * 17} stroke={S.magenta} strokeWidth="0.8" opacity="0.6" /></g>; })}
      <polygon points={pts(ngon(48, 34, 4, 4, 8))} fill={S.amberHi} style={{ animation:'ship-blink 2s steps(3) infinite' }} />
      <g fill="none" stroke={S.magenta} strokeWidth="1.2" opacity="0.8"><path d="M24 34 C14 40 12 52 20 58" /><path d="M72 34 C82 40 86 50 78 58" /><path d="M48 10 C44 6 40 6 38 4" /></g>
      <g stroke={ART.stone.hi} strokeWidth="0.8" fill="none">{[[12, 20], [78, 22], [14, 48], [80, 50]].map(([x, y], i) => <g key={i}><polyline points={pts([[x, y], [x + 4, y - 3], [x + 8, y]])} /><line x1={x + 4} y1={y - 3} x2={x + 4} y2={y + 4} /></g>)}</g>
      <text x="48" y="63" textAnchor="middle" fontSize="2.6" fill={S.magenta} fontWeight="700" {...PT}>WARM: NOT HEAT. BIOLOGICAL.</text>
      <Grime x={6} y={6} w={84} h={52} seed={610} n={6} op={0.28} /><Scuff x={8} y={8} w={80} h={48} seed={611} n={8} c="stone" /><Rivets x={10} y={7} w={76} n={9} c="stone" />
    </svg>
  );
}

function PropRuinStone() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3.4} />
      <g fill="none" stroke={S.cyanHi} strokeWidth="0.8">{[22, 30, 38].map((r, i) => <ellipse key={r} cx="48" cy="34" rx={r + 8} ry={r * 0.5} opacity={0.4 - i * 0.1} style={{ animation:`holo-flicker ${2.4 + i * 0.6}s ease-in-out infinite` }} />)}</g>
      <Bev t="polygon" points={pts([[8, 60], [8, 36], [14, 30], [30, 28], [32, 60]])} c="stone" o={1.2} />
      <Bev t="polygon" points={pts([[64, 60], [66, 28], [82, 30], [88, 36], [88, 60]])} c="stone" o={1.2} />
      <Bev t="polygon" points={pts([[6, 36], [10, 14], [20, 6], [76, 6], [86, 14], [90, 36], [72, 30], [24, 30]])} c="stone" o={1.3} />
      <g stroke={ART.stone.shade} strokeWidth="1.1" fill="none">
        <circle cx="48" cy="17" r="6" /><line x1="48" y1="9" x2="48" y2="25" /><line x1="40" y1="17" x2="56" y2="17" /><polyline points={pts([[30, 22], [36, 12], [42, 22]])} /><polyline points={pts([[54, 22], [60, 12], [66, 22]])} />
        <polyline points={pts([[14, 48], [20, 42], [26, 48]])} /><polyline points={pts([[70, 48], [76, 42], [82, 48]])} /><polyline points={pts([[10, 40], [30, 40]])} /><polyline points={pts([[66, 40], [86, 40]])} />
      </g>
      {[[12, 30, 14, 9], [64, 32, 18, 8], [30, 6, 20, 6], [74, 8, 10, 7]].map(([x, y, w, h], i) => <polygon key={i} points={pts([[x, y + h], [x + 2, y + 2], [x + w * 0.5, y], [x + w, y + 3], [x + w - 2, y + h]])} fill={i % 2 ? ART.moss.base : ART.moss.hi} opacity="0.85" />)}
      <rect x="32" y="40" width="32" height="20" fill={ART.ink} opacity="0.55" />
      <text x="48" y="52" textAnchor="middle" fontSize="2.8" fill={S.cyanHi} {...PT}>THE FORCE: SILENT.</text><text x="48" y="56" textAnchor="middle" fontSize="2.8" fill={S.cyanHi} {...PT}>SOMETHING RESONATES.</text>
      <Grime x={6} y={8} w={84} h={52} seed={620} n={6} op={0.3} /><Scuff x={8} y={10} w={80} h={48} seed={621} n={8} c="stone" /><Rivets x={30} y={32} w={36} n={5} c="stone" />
    </svg>
  );
}

function PropOutcastCamp() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={91} rx={58} ry={3.4} />
      {/* lean to shelters from hull plating */}
      {[[6, 'hull'], [84, 'steel']].map(([x, c], i) => <g key={i}>
        <Bev t="polygon" points={pts([[x, 84], [x, 36], [x + 38, 28], [x + 38, 84]])} c={c} o={1.1} />
        <Seams x={x} y={30} w={38} h={54} cols={2} rows={2} op={0.4} /><polygon points={pts([[x + 4, 84], [x + 4, 52], [x + 22, 50], [x + 22, 84]])} fill={ART.ink} opacity="0.85" />
        <rect x={x + 22} y="30" width="4" height="54" fill={ART.dark.shade} opacity="0.4" /><Rivets x={x + 3} y={36} w={32} n={5} c={c} />
        <polygon points={pts([[x + 6, 84], [x + 20, 82], [x + 20, 86], [x + 6, 88]])} fill={ART.fabric.base} />
      </g>)}
      <Bev t="polygon" points={pts([[44, 80], [44, 54], [80, 50], [80, 80]])} c="rust" o={0.9} /><polygon points={pts([[48, 80], [48, 58], [76, 56], [76, 80]])} fill={ART.canvas.shade} />
      <polygon points={pts([[40, 54], [84, 48], [88, 56], [38, 62]])} fill={ART.canvas.base} />{[46, 58, 70].map((x) => <line key={x} x1={x} y1="56" x2={x + 2} y2="50" stroke={ART.canvas.shade} strokeWidth="1" />)}
      {/* central fire in a drum */}
      <Bev t="polygon" points={pts([[52, 86], [52, 70], [74, 70], [74, 86]])} c="rust" o={0.9} /><ellipse cx="63" cy="70" rx="11" ry="3" fill={ART.ink} />
      {[[58, 62, 8], [64, 56, 12], [69, 63, 7]].map(([x, y, h], i) => <polygon key={i} points={pts([[x - 3.4, 70], [x, y - 4], [x + 3.4, 70]])} fill={[S.orange, S.amberHi, S.red][i]} style={{ animation:`ship-blink ${0.9 + i * 0.3}s steps(3) infinite` }} />)}
      <Glow cx={63} cy={64} rx={32} ry={14} c={S.orange} opacity={0.5} />
      {[[38, 88], [90, 88]].map(([x, y], i) => <g key={i}><polygon points={pts([[x, y], [x + 16, y], [x + 18, y + 3], [x - 2, y + 3]])} fill={ART.fabric.shade} /><polygon points={pts(ngon(x + 4, y - 1, 3, 2, 6))} fill={ART.skin.dark} /></g>)}
      <g>{[[16, 22], [96, 20]].map(([x, y], i) => <g key={i}><polygon points={pts([[x, y], [x + 18, y], [x + 16, y + 10], [x + 2, y + 10]])} fill={ART.paper.base} opacity="0.9" /><text x={x + 9} y={y + 6.4} textAnchor="middle" fontSize="2" fill={ART.note.ink} {...PT}>{i ? 'ALL WAITING' : 'FORGOTTEN'}</text></g>)}</g>
      <text x="64" y="12" textAnchor="middle" fontSize="3.4" fill={S.amberHi} fontWeight="700" {...PT}>OUTCAST SETTLEMENT</text>
      <Grime x={4} y={28} w={120} h={60} seed={630} n={7} op={0.3} /><Scuff x={6} y={30} w={116} h={56} seed={631} n={9} c="hull" />
    </svg>
  );
}

function PropLuxSkiff() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={88} rx={58} ry={5} />
      <polygon points={pts(ngon(58, 72, 52, 12, 12, Math.PI / 12))} fill={ART.dark.shade} /><Bev t="polygon" points={pts(ngon(58, 70, 50, 11, 12, Math.PI / 12))} c="bronze" o={1.2} />
      <polygon points={pts(ngon(58, 69, 44, 8.6, 12, Math.PI / 12))} fill={ART.dark.base} />
      <Glow cx={58} cy={68} rx={36} ry={6} c={S.cyan} opacity={0.4} style={{ animation:'ship-engine 2.6s ease-in-out infinite' }} />
      {/* the skiff: a swept wedge with a glass canopy */}
      <polygon points={pts([[8, 60], [30, 42], [74, 36], [104, 46], [112, 56], [76, 66], [30, 66]])} fill={ART.dark.shade} transform="translate(1.6 2)" />
      <Bev t="polygon" points={pts([[8, 60], [30, 42], [74, 36], [104, 46], [112, 56], [76, 66], [30, 66]])} c="pearl" o={1.2} />
      <polygon points={pts([[30, 42], [74, 36], [104, 46], [60, 52]])} fill={ART.pearl.hi} opacity="0.7" />
      <Bev t="polygon" points={pts([[46, 50], [54, 36], [78, 34], [88, 46], [80, 52]])} c="glass" o={0.7} />
      <polygon points={pts([[56, 38], [74, 36], [84, 46], [70, 48]])} fill={ART.glass.hi} opacity="0.6" />
      <polygon points={pts([[10, 60], [30, 56], [76, 60], [110, 56], [76, 66], [30, 66]])} fill={ART.pearl.shade} />
      <polygon points={pts([[14, 60], [26, 56], [26, 62]])} fill={ART.brass.base} /><rect x="30" y="58" width="60" height="1.6" fill={ART.brass.hi} />
      <polygon points={pts([[8, 60], [4, 62], [8, 66], [20, 64]])} fill={ART.steel.base} /><Glow cx={8} cy={62} r={5} c={S.cyan} opacity={0.9} />
      <polygon points={pts([[30, 66], [38, 70], [44, 66]])} fill={S.cyan} opacity="0.8" /><polygon points={pts([[84, 64], [92, 68], [98, 62]])} fill={S.cyan} opacity="0.8" />
      <Slab x={98} y={20} w={28} h={34} k={1.6} c="dark" o={0.9} /><rect x="101" y="23" width="22" height="16" fill={ART.screen.amber} /><text x="112" y="28" textAnchor="middle" fontSize="2.6" fill={S.gold} fontWeight="700" {...PT}>UBRIKKIAN</text><text x="112" y="32.4" textAnchor="middle" fontSize="2.2" fill={S.gold} {...PT}>REG: UNATTENDED</text><text x="112" y="36.6" textAnchor="middle" fontSize="2.2" fill={S.amberHi} {...PT}>PERMIT 200 CR</text>
      <Toggles x={101} y={42} cols={6} rows={2} gap={3.6} seed={6} />
      <text x="48" y="14" textAnchor="middle" fontSize="3.8" fill={S.gold} fontWeight="700" {...PT}>LUX-SKIFF: SENATE DISTRICT CONFIG</text>
      <Rivets x={14} y={72} w={88} n={10} c="bronze" />
      <Grime x={6} y={36} w={116} h={50} seed={640} n={5} op={0.16} /><Scuff x={8} y={38} w={112} h={46} seed={641} n={8} c="pearl" />
    </svg>
  );
}

function PropSpeederWreck() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={60} rx={58} ry={3.4} />
      <polygon points={pts([[6, 52], [18, 36], [60, 30], [98, 38], [120, 50], [112, 58], [14, 58]])} fill={ART.dark.shade} transform="translate(1.4 2)" />
      <Bev t="polygon" points={pts([[6, 52], [18, 36], [60, 30], [98, 38], [120, 50], [112, 58], [14, 58]])} c="rust" o={1.2} />
      <polygon points={pts([[18, 36], [60, 30], [98, 38], [60, 42]])} fill={ART.rust.hi} opacity="0.7" />
      <polygon points={pts([[34, 44], [52, 34], [84, 36], [92, 44], [60, 50]])} fill={ART.ink} />
      <polygon points={pts([[42, 46], [52, 38], [76, 40], [82, 46], [58, 50]])} fill={ART.fabric.base} /><polygon points={pts([[42, 46], [52, 38], [60, 40], [50, 48]])} fill={ART.fabric.hi} opacity="0.7" /><polygon points={pts([[50, 40], [60, 38], [66, 42], [54, 46]])} fill={ART.skin.mid} opacity="0.0" />
      <Glow cx={62} cy={44} rx={16} ry={5} c={S.orange} opacity={0.55} />
      {/* stripped coil bays */}
      {[[18, 52], [96, 52]].map(([x, y], i) => <g key={i}><polygon points={pts(ngon(x + 6, y + 2, 9, 5, 8))} fill={ART.ink} /><polygon points={pts(ngon(x + 6, y + 2, 5.4, 3, 8))} fill={ART.dark.base} /><path d={`M${x + 4} ${y + 2} C${x} ${y + 8} ${x + 8} ${y + 10} ${x + 4} ${y + 12}`} fill="none" stroke={ART.copper.base} strokeWidth="1.2" /></g>)}
      <polygon points={pts([[100, 36], [118, 44], [118, 50], [102, 44]])} fill={ART.steel.base} /><polygon points={pts([[100, 36], [96, 30], [104, 34]])} fill={ART.glass.base} opacity="0.5" />
      <polyline points={pts([[70, 30], [78, 24], [86, 28]])} fill="none" stroke={ART.dark.hi} strokeWidth="1.4" />
      <text x="64" y="12" textAnchor="middle" fontSize="3.2" fill={S.amberHi} fontWeight="700" {...PT}>RECENTLY OCCUPIED: BEDROLL WARM</text>
      <text x="64" y="63" textAnchor="middle" fontSize="2.4" fill={ART.beige.base} {...PT}>REPULSOR COILS: STRIPPED</text>
      <Rivets x={12} y={54} w={98} n={12} c="rust" />
      <Grime x={6} y={32} w={114} h={28} seed={650} n={7} op={0.3} /><Scuff x={8} y={34} w={110} h={22} seed={651} n={9} c="rust" />
    </svg>
  );
}

function PropSpeederBay() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3.4} />
      <polygon points={pts(ngon(44, 50, 42, 10, 10, Math.PI / 10))} fill={ART.dark.shade} /><Bev t="polygon" points={pts(ngon(44, 48, 40, 9, 10, Math.PI / 10))} c="steel" o={1} />
      <Hazard x={10} y={50} w={68} h={4} />
      {/* the speeder, engine warm */}
      <polygon points={pts([[14, 44], [30, 28], [62, 24], [84, 34], [86, 44], [54, 50], [24, 50]])} fill={ART.dark.shade} transform="translate(1.4 1.8)" />
      <Bev t="polygon" points={pts([[14, 44], [30, 28], [62, 24], [84, 34], [86, 44], [54, 50], [24, 50]])} c="deepglass" o={1.1} />
      <polygon points={pts([[30, 28], [62, 24], [84, 34], [54, 34]])} fill={ART.deepglass.hi} opacity="0.7" />
      <Bev t="polygon" points={pts([[38, 36], [44, 26], [60, 24], [68, 34]])} c="glass" o={0.6} />
      <rect x="24" y="44" width="56" height="1.6" fill={S.amberHi} opacity="0.9" />
      <Glow cx={14} cy={46} rx={10} ry={5} c={S.orange} opacity={0.9} style={{ animation:'ship-engine 1.6s ease-in-out infinite' }} /><polygon points={pts([[10, 42], [4, 46], [10, 50]])} fill={S.amberHi} style={{ animation:'ship-blink 1.2s steps(3) infinite' }} />
      <g fill={ART.pearl.base} opacity="0.4" style={{ animation:'steam-rise 2.6s ease-out infinite' }}><polygon points={pts(ngon(10, 38, 4, 3, 7))} /></g>
      {/* quick release tethers */}
      {[[26, 52], [74, 52]].map(([x, y], i) => <g key={i}><Bev t="polygon" points={pts([[x - 4, y + 6], [x - 4, y - 4], [x + 4, y - 4], [x + 4, y + 6]])} c="dark" o={0.6} /><rect x={x - 1.4} y={y - 2} width="2.8" height="4" fill={S.red} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} /><Cable d={`M${x} ${y - 4} C${x} ${y - 10} ${x + (i ? -6 : 6)} ${y - 10} ${x + (i ? -8 : 8)} ${y - 6}`} w={1.4} /></g>)}
      <Slab x={36} y={4} w={34} h={9} k={1} c="dark" o={0.7} /><text x="53" y="10.4" textAnchor="middle" fontSize="3.2" fill={S.amberHi} fontWeight="700" {...PT}>INTERCEPT READY</text>
      <text x="48" y="63" textAnchor="middle" fontSize="2.4" fill={ART.beige.base} {...PT}>COURIER BOARDED MAG-RAIL 3 MIN AGO</text>
      <Rivets x={12} y={50} w={64} n={8} />
      <Grime x={4} y={38} w={88} h={22} seed={660} n={5} op={0.26} /><Scuff x={6} y={40} w={84} h={18} seed={661} n={7} c="steel" />
    </svg>
  );
}

function PropFountain() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={44} ry={3.4} />
      <polygon points={pts(ngon(48, 50, 46, 11, 12, Math.PI / 12))} fill={ART.dark.shade} /><Bev t="polygon" points={pts(ngon(48, 48, 44, 10, 12, Math.PI / 12))} c="stone" o={1.2} />
      <polygon points={pts(ngon(48, 47.6, 38, 7.4, 12, Math.PI / 12))} fill={ART.deepglass.base} />
      <polygon points={pts(ngon(48, 47.4, 30, 5.4, 12, Math.PI / 12))} fill={ART.glass.base} opacity="0.7" />
      {[[22, 'a'], [74, 'b']].map(([x, k]) => <g key={k} fill="none" stroke={S.kyber} strokeWidth="1.2"><path d={`M${x} 46 C${x + (x < 48 ? 8 : -8)} 30 ${x + (x < 48 ? 14 : -14)} 30 ${x + (x < 48 ? 18 : -18)} 44`} opacity="0.85" /></g>)}
      <g style={{ animation:'steam-rise 3s ease-out infinite' }} fill="none" stroke={S.kyber} strokeWidth="1.4" opacity="0.8"><path d="M48 16 C44 28 40 36 36 44" /><path d="M48 16 C52 28 56 36 60 44" /><path d="M48 14 L48 40" /></g>
      <Bev t="polygon" points={pts([[37, 48], [39, 6], [48, 2], [57, 6], [59, 48]])} c="stone" o={1.2} />
      <Bev t="polygon" points={pts([[40, 36], [40, 12], [56, 12], [56, 36]])} c="bronze" o={0.7} />
      <text x="48" y="19.6" textAnchor="middle" fontSize="1.8" fill={ART.ink} fontWeight="700" {...PT}>IN MEMORY</text><text x="48" y="23.2" textAnchor="middle" fontSize="1.8" fill={ART.ink} fontWeight="700" {...PT}>OF THE FALLEN</text><text x="48" y="26.8" textAnchor="middle" fontSize="1.8" fill={ART.ink} fontWeight="700" {...PT}>OF MALACHOR</text>
      <polygon points={pts([[44, 5], [52, 5], [48, 0]])} fill={S.kyber} />
      <Rivets x={46} y={30} w={4} n={2} c="bronze" />
      <Glow cx={48} cy={47} rx={34} ry={5} c={S.cyan} opacity={0.35} />
      <Grime x={4} y={40} w={88} h={20} seed={670} n={5} op={0.2} /><Scuff x={6} y={42} w={84} h={14} seed={671} n={6} c="stone" />
    </svg>
  );
}

function PropScorchWall() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[3, 60], [3, 6], [9, 2], [87, 2], [93, 6], [93, 60]])} c="concrete" o={1} />
      <Seams x={3} y={2} w={90} h={58} cols={3} rows={1} op={0.4} />
      {[[22, 28], [48, 24], [74, 30]].map(([x, y], i) => <g key={i}>
        <polygon points={pts(ngon(x, y, 15, 12, 10))} fill={ART.ink} opacity="0.5" />
        <polygon points={pts(ngon(x, y, 9, 7, 10))} fill={ART.ink} opacity="0.7" />
        {Array.from({ length: 10 }, (_, j) => { const a = (j / 10) * Math.PI * 2; return <line key={j} x1={x + Math.cos(a) * 9} y1={y + Math.sin(a) * 7} x2={x + Math.cos(a) * (15 + (j % 3) * 3)} y2={y + Math.sin(a) * (12 + (j % 3) * 2)} stroke={ART.ink} strokeWidth="1.2" opacity="0.5" />; })}
      </g>)}
      {[[18, 42, 1], [44, 40, 2], [70, 46, 3], [84, 22, 4]].map(([x, y, n]) => <g key={n}><polygon points={pts([[x, y + 10], [x + 3, y], [x + 6, y + 10]])} fill={S.warn} /><text x={x + 3} y={y + 9} textAnchor="middle" fontSize="3.2" fill={ART.ink} fontWeight="700" {...PT}>{n}</text></g>)}
      <g stroke={S.warn} strokeWidth="1"><line x1="3" y1="52" x2="93" y2="50" strokeDasharray="4 3" /></g>
      <text x="48" y="10" textAnchor="middle" fontSize="3.2" fill={S.warn} fontWeight="700" {...PT}>3 SHAPED CHARGES. NOT A FUEL FIRE.</text>
      <text x="48" y="57" textAnchor="middle" fontSize="2.4" fill={S.cyanHi} {...PT}>CSF EVIDENCE TAPE: CLOSED CASE</text>
      <Rivets x={7} y={4} w={82} n={10} />
      <Grime x={4} y={6} w={88} h={54} seed={680} n={6} op={0.3} /><Scuff x={6} y={8} w={84} h={50} seed={681} n={8} c="concrete" />
    </svg>
  );
}

function PropSentryPost() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Bev t="polygon" points={pts([[4, 58], [4, 30], [10, 26], [86, 26], [92, 30], [92, 58]])} c="steel" o={1.1} />
      <Seams x={4} y={26} w={88} h={32} cols={4} rows={1} op={0.35} />
      {[[10, 'dark'], [38, 'rust'], [66, 'dark']].map(([x, c], i) => <g key={i}><Slab x={x} y={34} w={20} h={22} k={1.4} c={c} o={0.8} /><Hazard x={x + 1} y={50} w={18} h={4} /></g>)}
      <polygon points={pts([[30, 30], [66, 30], [66, 36], [30, 36]])} fill={ART.ink} /><rect x="30" y="30" width="36" height="1.2" fill={ART.steel.hi} />
      <g>{[[18, 34, 6], [44, 38, 8], [74, 36, 7], [58, 46, 5]].map(([x, y, r], i) => <g key={i}><polygon points={pts(ngon(x, y, r, r * 0.8, 8))} fill={ART.ink} opacity="0.55" /><polygon points={pts(ngon(x, y, r * 0.5, r * 0.4, 8))} fill={ART.ink} opacity="0.7" /></g>)}</g>
      <g><Bev t="polygon" points={pts([[76, 58], [76, 46], [86, 46], [86, 58]])} c="wood" o={0.5} /><Slab x={74} y={42} w={14} h={5} k={0.8} c="wood" o={0.5} /></g>
      <g transform="rotate(-8 26 24)"><Bev t="polygon" points={pts([[16, 26], [34, 26], [34, 20], [16, 20]])} c="dark" o={0.4} /><circle cx="22" cy="20" r="2.4" fill={ART.dark.shade} /><circle cx="22" cy="20" r="1.2" fill={S.cyan} /><circle cx="30" cy="20" r="2.4" fill={ART.dark.shade} /><circle cx="30" cy="20" r="1.2" fill={S.cyan} /></g>
      <g>{[0, 1, 2].map((i) => <Slab key={i} x={46 + i * 6} y={20} w={5} h={6} k={0.6} c="amber" o={0.3} />)}</g>
      <text x="48" y="14" textAnchor="middle" fontSize="3.2" fill={S.warn} fontWeight="700" {...PT}>HELD AGAINST SOMETHING FROM THE SOUTH</text>
      <polygon points={pts([[40, 6], [56, 6], [48, 12]])} fill={S.amber} opacity="0.5" />
      <Rivets x={8} y={28} w={80} n={10} />
      <Grime x={4} y={28} w={88} h={30} seed={690} n={6} op={0.3} /><Scuff x={6} y={30} w={84} h={26} seed={691} n={8} c="steel" />
    </svg>
  );
}

// ---------------------------------------------------------------------------
// BADOR AND ZORA IV: set pieces for the Kuat expansion zones.
// ---------------------------------------------------------------------------
function PropMaglevCrane() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={92} rx={36} ry={3.4} />
      <Bev t="rect" x="0" y="2" width="96" height="9" c="steel" o={1} />
      <Hazard x={0} y={9} w={96} h={2.4} />
      {Array.from({ length: 8 }, (_, i) => <rect key={i} x={4 + i * 12} y="2" width="1.4" height="8" fill={ART.dark.shade} />)}
      <Bev t="polygon" points={pts([[30, 11], [66, 11], [64, 24], [32, 24]])} c="dark" o={0.9} />
      <Glow cx={48} cy={24} rx={20} ry={4} c={S.cyan} opacity={0.7} style={{ animation:'ship-engine 2s ease-in-out infinite' }} />
      <Lights x={36} y={16} n={6} gap={5} seed={3} />
      <Cable d="M38 24 L38 44" w={2} /><Cable d="M58 24 L58 44" w={2} /><Cable d="M48 24 L48 36" w={2.6} />
      <Bev t="polygon" points={pts([[44, 36], [52, 36], [52, 42], [44, 42]])} c="amber" o={0.5} />
      <Bev t="polygon" points={pts([[12, 84], [12, 44], [84, 44], [84, 84]])} c="steel" o={1.2} />
      {Array.from({ length: 9 }, (_, i) => <rect key={i} x={16 + i * 8} y="46" width="1.6" height="36" fill={ART.dark.shade} opacity="0.35" />)}
      <rect x="12" y="44" width="72" height="2.4" fill={ART.hull.hi} />
      <Hazard x={14} y={76} w={68} h={5} />
      <Slab x={24} y={54} w={48} h={14} k={1.2} c="paper" o={0.3} />
      <text x="48" y="60" textAnchor="middle" fontSize="3.4" fill={ART.note.ink} fontWeight="700" {...PT}>PERSONAL CONSIGNMENT</text>
      <text x="48" y="65.4" textAnchor="middle" fontSize="2.6" fill={ART.note.redInk} {...PT}>5 T: NOT ON ANY MANIFEST</text>
      <Rivets x={14} y={47} w={68} n={9} />
      <Grime x={12} y={44} w={72} h={40} seed={700} n={6} op={0.25} /><Scuff x={14} y={46} w={68} h={36} seed={701} n={8} c="steel" />
    </svg>
  );
}

function PropCapacitorBank({ variant }) {
  const S = ART.signal, dead = variant === 'dead';
  const tops = [14, 30, 46, 62, 78];
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={92} rx={44} ry={3.4} />
      {!dead && <Glow cx={48} cy={30} rx={46} ry={22} c={S.cyan} opacity={0.35} style={{ animation:'ship-engine 1.6s ease-in-out infinite' }} />}
      <Bev t="polygon" points={pts([[4, 90], [4, 74], [92, 74], [92, 90]])} c="steel" o={1.1} />
      <Hazard x={6} y={86} w={84} h={3} />
      {tops.map((x, i) => (
        <g key={x}>
          <Bev t="polygon" points={pts([[x - 6, 76], [x - 6, 34], [x + 6, 34], [x + 6, 76]])} c="amber" o={0.9} />
          <ellipse cx={x} cy="34" rx="6" ry="2" fill={ART.amber.hi} /><ellipse cx={x} cy="34.4" rx="3.6" ry="1.1" fill={ART.dark.shade} />
          {[46, 56, 66].map((y) => <rect key={y} x={x - 6} y={y} width="12" height="1.8" fill={ART.dark.shade} />)}
          <rect x={x - 1.6} y="24" width="3.2" height="10" fill={ART.steel.base} /><circle cx={x} cy="23" r="2.2" fill={dead ? ART.dark.base : S.cyanHi} style={dead ? undefined : { animation:`ship-blink ${1 + i * 0.3}s steps(2) infinite` }} />
        </g>
      ))}
      {!dead && <g fill="none" stroke={S.cyanHi} strokeWidth="1" style={{ animation:'holo-flicker 0.9s ease-in-out infinite' }}>
        {[0, 1, 2, 3].map((i) => <polyline key={i} points={pts([[tops[i], 23], [tops[i] + 4, 16 - (i % 2) * 4], [tops[i] + 8, 22], [tops[i] + 12, 14 + (i % 2) * 3], [tops[i + 1], 23]])} />)}
      </g>}
      <Bev t="polygon" points={pts([[38, 74], [58, 74], [58, 62], [38, 62]])} c="dark" o={0.6} /><rect x="40" y="64" width="16" height="8" fill={dead ? ART.screen.off : ART.screen.cyan} /><text x="48" y="68" textAnchor="middle" fontSize="2.2" fill={dead ? S.redHi : S.cyanHi} fontWeight="700" {...PT}>{dead ? 'OFFLINE' : 'JAMMING'}</text><text x="48" y="71" textAnchor="middle" fontSize="2" fill={dead ? S.redHi : S.cyanHi} {...PT}>{dead ? 'CELLS DARK' : '6 KM RADIUS'}</text>
      <text x="48" y="10" textAnchor="middle" fontSize="3" fill={S.warn} fontWeight="700" {...PT}>{dead ? 'JAMMER CUT' : 'DISCHARGE: NOT NATURAL'}</text>
      <Rivets x={8} y={76} w={80} n={10} />
      <Grime x={4} y={30} w={88} h={60} seed={710} n={6} op={0.22} /><Scuff x={6} y={32} w={84} h={56} seed={711} n={8} c="amber" />
    </svg>
  );
}

function PropScanWreck() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={60} rx={58} ry={3} />
      <Bev t="polygon" points={pts([[8, 56], [14, 34], [52, 28], [60, 54]])} c="deepglass" o={1} />
      <polygon points={pts([[14, 34], [52, 28], [54, 34], [16, 40]])} fill={ART.deepglass.hi} opacity="0.7" /><Seams x={10} y={32} w={48} h={24} cols={2} rows={1} op={0.4} />
      <Bev t="polygon" points={pts([[64, 56], [64, 38], [96, 38], [96, 56]])} c="steel" o={0.9} /><polygon points={pts([[64, 38], [96, 38], [90, 30], [70, 30]])} fill={ART.steel.hi} opacity="0.7" />
      <rect x="68" y="42" width="12" height="2" fill={ART.dark.shade} /><rect x="68" y="47" width="20" height="2" fill={ART.dark.shade} />
      <g transform="rotate(-24 108 50)"><line x1="108" y1="52" x2="108" y2="20" stroke={ART.steel.hi} strokeWidth="2.4" /><Bev t="polygon" points={pts([[100, 20], [116, 20], [112, 12], [104, 12]])} c="hull" o={0.6} /><circle cx="108" cy="16" r="2" fill={ART.dark.shade} /></g>
      {[[100, 54], [112, 56], [120, 52]].map(([x, y], i) => <line key={i} x1={x} y1={y} x2={x + 5} y2={y + 2} stroke={ART.steel.hi} strokeWidth="1.4" />)}
      {[0, 1, 2, 3, 4, 5, 6].map((i) => <g key={'tag' + i}><line x1={98 + i * 3} y1={30 + (i % 2) * 2} x2={98 + i * 3} y2={33 + (i % 2) * 2} stroke={ART.steel.hi} strokeWidth="0.5" /><rect x={97 + i * 3} y={33 + (i % 2) * 2} width="2" height="3.2" fill={ART.paper.base} /></g>)}<Cable d="M96 44 C100 48 98 52 104 52" w={1.6} /><polyline points={pts([[100, 50], [104, 54]])} fill="none" stroke={S.warn} strokeWidth="1" />
      {[22, 38].map((x, i) => <g key={i}><circle cx={x} cy="44" r="4" fill={ART.dark.shade} /><circle cx={x} cy="44" r="2.4" fill={ART.dark.base} /></g>)}
      <g transform="rotate(4 74 20)"><polygon points={pts([[62, 8], [92, 8], [92, 22], [62, 22]])} fill={ART.paper.base} /><text x="77" y="13.6" textAnchor="middle" fontSize="2.6" fill={ART.note.redInk} fontWeight="700" {...PT}>WARNED OFF</text><text x="77" y="18.4" textAnchor="middle" fontSize="2.4" fill={ART.note.ink} {...PT}>NO SIGN OF COMBAT</text></g>
      <text x="30" y="14" textAnchor="middle" fontSize="2.8" fill={S.cyanHi} fontWeight="700" {...PT}>KDY SCAN TEAM</text>
      <text x="30" y="19" textAnchor="middle" fontSize="2.4" fill={S.amberHi} {...PT}>DISABLED, NOT DESTROYED</text>
      <Hazard x={8} y={58} w={52} h={3} /><Rivets x={66} y={40} w={28} n={5} />
      <Grime x={6} y={28} w={116} h={32} seed={720} n={6} op={0.26} /><Scuff x={8} y={30} w={112} h={28} seed={721} n={8} c="steel" />
    </svg>
  );
}

function PropCraterGlass() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={86} rx={58} ry={5} />
      <polygon points={pts(ngon(64, 52, 62, 38, 14, 0.1))} fill={ART.ink} opacity="0.7" />
      <polygon points={pts(ngon(64, 52, 56, 34, 14, 0.1))} fill={ART.rust.shade} />
      <Bev t="polygon" points={pts(ngon(64, 52, 50, 30, 14, 0.1))} c="glass" o={1.2} />
      <polygon points={pts(ngon(64, 54, 40, 23, 14, 0.1))} fill={ART.glass.base} />
      <polygon points={pts(ngon(64, 55, 28, 15, 14, 0.1))} fill={ART.glass.hi} opacity="0.8" />
      <polygon points={pts(ngon(64, 56, 14, 7, 14, 0.1))} fill={S.white} opacity="0.7" />
      <g stroke={ART.glass.shade} strokeWidth="1" fill="none">{Array.from({ length: 10 }, (_, i) => { const a = (i / 10) * Math.PI * 2 + 0.3; return <polyline key={i} points={pts([[64 + Math.cos(a) * 8, 56 + Math.sin(a) * 4], [64 + Math.cos(a + 0.1) * 24, 54 + Math.sin(a + 0.1) * 13], [64 + Math.cos(a - 0.05) * 42, 52 + Math.sin(a - 0.05) * 24]])} />; })}</g>
      {[[40, 44, 14], [86, 62, 18], [74, 40, 10]].map(([x, y, w], i) => <polygon key={i} points={pts([[x, y], [x + w, y - 2], [x + w + 4, y]])} fill={S.white} opacity="0.55" />)}
      <Glow cx={64} cy={56} rx={26} ry={10} c={S.orange} opacity={0.35} style={{ animation:'ship-engine 3s ease-in-out infinite' }} />
      <g stroke={ART.ink} strokeWidth="1.6" fill="none" opacity="0.6"><polyline points={pts([[8, 50], [16, 56], [12, 64]])} /><polyline points={pts([[116, 44], [112, 52], [118, 60]])} /></g>
      <text x="64" y="10" textAnchor="middle" fontSize="3.4" fill={S.amberHi} fontWeight="700" {...PT}>FUSED SILICA BASIN</text>
      <text x="64" y="94" textAnchor="middle" fontSize="2.6" fill={ART.beige.hi} {...PT}>THE GROUND REMEMBERS</text>
      <Rivets x={20} y={84} w={88} n={10} c="rust" />
      <Grime x={6} y={20} w={116} h={66} seed={730} n={6} op={0.22} /><Scuff x={8} y={22} w={112} h={60} seed={731} n={8} c="glass" />
    </svg>
  );
}

function PropRebreatherRack() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Slab x={3} y={3} w={90} h={56} k={3} c="steel" o={1} />
      <Seams x={3} y={3} w={90} h={56} cols={5} rows={1} op={0.3} />
      <text x="48" y="10" textAnchor="middle" fontSize="3.2" fill={ART.ink} fontWeight="700" {...PT}>REBREATHER STATION</text>
      {Array.from({ length: 6 }, (_, i) => {
        const x = 12 + i * 14;
        return (
          <g key={i}>
            <polygon points={pts([[x - 1, 14], [x + 9, 14], [x + 9, 16], [x - 1, 16]])} fill={ART.dark.shade} />
            <Bev t="polygon" points={pts([[x, 16], [x + 8, 16], [x + 10, 24], [x + 6, 30], [x + 2, 30], [x - 2, 24]])} c="dark" o={0.7} />
            <polygon points={pts([[x + 1, 19], [x + 7, 19], [x + 8, 23], [x + 4, 26], [x, 23]])} fill={ART.glass.base} opacity="0.7" />
            <Bev t="polygon" points={pts([[x + 2, 36], [x + 2, 30], [x + 6, 30], [x + 6, 36]])} c="steel" o={0.4} />
            <Bev t="polygon" points={pts([[x - 1, 54], [x - 1, 38], [x + 9, 38], [x + 9, 54]])} c="teal" o={0.7} />
            <rect x={x - 1} y="44" width="10" height="1.6" fill={ART.dark.shade} /><rect x={x + 2} y="39.4" width="4" height="2" fill={i === 2 ? S.amber : S.green} />
          </g>
        );
      })}
      <Slab x={64} y={50} w={26} h={7} k={0.8} c="dark" o={0.5} /><text x="77" y="54.4" textAnchor="middle" fontSize="2.4" fill={S.cyanHi} {...PT}>N2-HEAVY MIX</text>
      <Hazard x={6} y={55} w={54} h={3} /><Rivets x={7} y={5} w={82} n={10} />
      <Grime x={4} y={6} w={88} h={52} seed={740} n={5} op={0.22} /><Scuff x={6} y={8} w={84} h={48} seed={741} n={7} c="steel" />
    </svg>
  );
}

function PropSentinelDroid() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={92} rx={24} ry={3} />
      <Bev t="polygon" points={pts([[10, 92], [10, 82], [54, 82], [54, 92]])} c="steel" o={0.9} /><Hazard x={12} y={87} w={40} h={3} />
      <Bev t="polygon" points={pts([[16, 84], [18, 56], [28, 56], [28, 84]])} c="deepglass" o={0.9} /><Bev t="polygon" points={pts([[36, 84], [36, 56], [46, 56], [48, 84]])} c="deepglass" o={0.9} />
      <Bev t="polygon" points={pts([[12, 58], [14, 30], [26, 22], [38, 22], [50, 30], [52, 58]])} c="deepglass" o={1.1} />
      <Seams x={12} y={24} w={40} h={34} cols={1} rows={2} op={0.4} /><Vent x={22} y={40} w={20} h={10} n={4} />
      <Bev t="polygon" points={pts([[4, 36], [4, 22], [14, 22], [14, 40]])} c="steel" o={0.8} /><Bev t="polygon" points={pts([[50, 40], [50, 22], [60, 22], [60, 36]])} c="steel" o={0.8} />
      <Bev t="polygon" points={pts([[4, 40], [8, 70], [14, 70], [14, 44]])} c="deepglass" o={0.7} /><Bev t="polygon" points={pts([[50, 44], [50, 70], [56, 70], [60, 40]])} c="deepglass" o={0.7} />
      <Bev t="polygon" points={pts([[22, 22], [22, 10], [26, 6], [38, 6], [42, 10], [42, 22]])} c="steel" o={0.9} />
      <rect x="24" y="12" width="16" height="4" fill={ART.ink} /><rect x="26" y="13" width="12" height="2" fill={ART.dark.hi} />
      <g transform="rotate(-6 46 62)"><polygon points={pts([[42, 56], [58, 56], [58, 66], [42, 66]])} fill={ART.paper.base} /><rect x="43.6" y="57.6" width="12.8" height="2" fill={S.green} /><text x="50" y="64.4" textAnchor="middle" fontSize="2.2" fill={ART.note.ink} fontWeight="700" {...PT}>SERVICED</text></g>
      <Cable d="M32 56 C20 70 30 80 18 86" w={2.2} />
      <Glyph k="cog" x={32} y={32} s={3.6} c={ART.brand.kdy} />
      <Rivets x={14} y={26} w={36} n={5} />
      <Grime x={10} y={8} w={46} h={84} seed={750} n={5} op={0.22} /><Scuff x={12} y={10} w={42} h={78} seed={751} n={7} c="deepglass" />
    </svg>
  );
}

function PropHypercore() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={92} rx={44} ry={3.4} />
      <Slab x={3} y={3} w={90} h={86} k={4} c="dark" o={1.2} />
      <Seams x={3} y={3} w={90} h={86} cols={3} rows={3} op={0.4} />
      <polygon points={pts(ngon(48, 46, 38, 38, 16, Math.PI / 16))} fill={ART.ink} />
      <Glow cx={48} cy={46} r={40} c={S.cyan} opacity={0.5} style={{ animation:'ship-engine 2.2s ease-in-out infinite' }} />
      <polygon points={pts(ngon(48, 46, 30, 30, 16, Math.PI / 16))} fill={ART.deepglass.shade} />
      <polygon points={pts(ngon(48, 46, 22, 22, 16, Math.PI / 16))} fill={ART.deepglass.base} />
      <polygon points={pts(ngon(48, 46, 14, 14, 12))} fill={S.cyan} style={{ animation:'ship-blink 2.6s steps(4) infinite' }} />
      <polygon points={pts(ngon(48, 46, 8, 8, 8))} fill={S.cyanHi} /><polygon points={pts(ngon(48, 46, 4, 4, 6))} fill={S.white} />
      <g stroke={S.cyanHi} strokeWidth="0.9" fill="none" style={{ animation:'holo-flicker 1.2s ease-in-out infinite' }}>{Array.from({ length: 8 }, (_, i) => { const a = (i / 8) * Math.PI * 2; return <polyline key={i} points={pts([[48 + Math.cos(a) * 14, 46 + Math.sin(a) * 14], [48 + Math.cos(a + 0.2) * 22, 46 + Math.sin(a + 0.2) * 22], [48 + Math.cos(a - 0.1) * 30, 46 + Math.sin(a - 0.1) * 30]])} />; })}</g>
      <Bev t="polygon" points={pts(ngon(48, 46, 36, 36, 16, Math.PI / 16))} c="steel" o={0.8} />
      <polygon points={pts(ngon(48, 46, 30, 30, 16, Math.PI / 16))} fill="none" stroke={ART.steel.shade} strokeWidth="1" />
      {Array.from({ length: 12 }, (_, i) => { const a = (i / 12) * Math.PI * 2; return <circle key={i} cx={48 + Math.cos(a) * 33} cy={46 + Math.sin(a) * 33} r="1.4" fill={ART.steel.hi} />; })}
      {[[0, 1], [1, 0], [0, -1], [-1, 0]].map(([dx, dy], i) => <polygon key={i} points={pts([[48 + dx * 36 - dy * 6, 46 + dy * 36 + dx * 6], [48 + dx * 26 - dy * 3, 46 + dy * 26 + dx * 3], [48 + dx * 26 + dy * 3, 46 + dy * 26 - dx * 3], [48 + dx * 36 + dy * 6, 46 + dy * 36 - dx * 6]])} fill={ART.steel.shade} opacity="0.9" />)}
      <Slab x={14} y={80} w={68} h={7} k={1} c="dark" o={0.6} /><text x="48" y="85.2" textAnchor="middle" fontSize="3" fill={S.cyanHi} fontWeight="700" {...PT}>HYPERCORE: STAR DESTROYER CLASS</text>
      <Hazard x={6} y={88} w={84} h={3} /><Rivets x={7} y={6} w={82} n={10} /><Lights x={10} y={84} n={3} gap={3} seed={7} />
      <Grime x={4} y={6} w={88} h={80} seed={760} n={7} op={0.24} /><Scuff x={6} y={8} w={84} h={76} seed={761} n={9} c="steel" />
    </svg>
  );
}

function PropBridgeConsole() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.8} />
      <Bev t="polygon" points={pts([[4, 36], [10, 4], [118, 4], [124, 36]])} c="dark" o={1.1} />
      <polygon points={pts([[10, 34], [14, 8], [114, 8], [118, 34]])} fill={ART.screen.space} />
      {Array.from({ length: 16 }, (_, i) => <rect key={i} x={18 + ((i * 53) % 92)} y={10 + ((i * 7) % 18)} width="1" height="1" fill={S.white} opacity="0.7" />)}
      <polygon points={pts([[84, 22], [100, 16], [108, 24], [98, 30], [90, 28]])} fill={ART.steel.shade} /><polygon points={pts([[86, 22], [100, 17], [104, 22], [92, 25]])} fill={ART.steel.base} /><polygon points={pts([[40, 28], [52, 22], [58, 28]])} fill={ART.hull.shade} />
      <g stroke={S.white} strokeWidth="0.5" opacity="0.3">{[26, 46, 66, 86, 106].map((x) => <line key={x} x1={x} y1="8" x2={x - 2} y2="34" />)}</g>
      <Bev t="polygon" points={pts([[2, 60], [6, 38], [122, 38], [126, 60]])} c="hull" o={1.1} />
      <polygon points={pts([[6, 38], [122, 38], [120, 42], [8, 42]])} fill={ART.pearl.hi} />
      {[[14, 'cyan'], [40, 'green'], [66, 'amber'], [92, 'cyan']].map(([x, k], i) => <g key={i}><Bev t="polygon" points={pts([[x, 56], [x + 2, 44], [x + 22, 44], [x + 24, 56]])} c="dark" o={0.7} /><rect x={x + 3} y="46" width="18" height="8" fill={ART.screen[k === 'cyan' ? 'cyan' : k === 'green' ? 'green' : 'amber']} />{[0, 1, 2].map((j) => <rect key={j} x={x + 5} y={47.4 + j * 2.4} width={8 + ((i + j) % 3) * 3} height="1" fill={S[k === 'cyan' ? 'cyanHi' : k === 'green' ? 'greenHi' : 'gold']} />)}</g>)}
      <Slab x={46} y={30} w={36} h={9} k={1} c="dark" o={0.6} /><text x="64" y="36.2" textAnchor="middle" fontSize="3" fill={S.cyanHi} fontWeight="700" {...PT}>ZORA IV: FLIGHT LOGS INTACT</text>
      <rect x="116" y="38" width="8" height="2.4" fill={ART.red.base} /><rect x="4" y="38" width="8" height="2.4" fill={ART.red.base} />
      <Toggles x={14} y={58} cols={20} rows={1} gap={4.6} seed={9} /><Lights x={110} y={58.4} n={3} gap={4} seed={2} />
      <Seams x={2} y={38} w={124} h={22} cols={7} op={0.3} />
      <Grime x={4} y={6} w={120} h={54} seed={770} n={6} op={0.2} /><Scuff x={6} y={40} w={116} h={18} seed={771} n={8} c="hull" /><Rivets x={10} y={5.6} w={108} n={13} />
    </svg>
  );
}

// ===== CORUSCANT PROPS END =====
// ===== BADOR BASE PROPS BEGIN =====
// Kuati Forward Directorate Hub: alabaster ceramic, slate blue trim, burnished gold, cyan holo light.
const bez = (p0, p1, p2, p3, k) => { const u = 1 - k; return [u * u * u * p0[0] + 3 * u * u * k * p1[0] + 3 * u * k * k * p2[0] + k * k * k * p3[0], u * u * u * p0[1] + 3 * u * u * k * p1[1] + 3 * u * k * k * p2[1] + k * k * k * p3[1]]; };

function PropLandingPad() {
  const S = ART.signal;
  const ring = Array.from({ length: 8 }, (_, i) => [192 + Math.cos((i / 8) * Math.PI * 2 + 0.39) * 150, 128 + Math.sin((i / 8) * Math.PI * 2 + 0.39) * 92]);
  const diodes = Array.from({ length: 28 }, (_, i) => [192 + Math.cos((i / 28) * Math.PI * 2) * 176, 128 + Math.sin((i / 28) * Math.PI * 2) * 114]);
  return (
    <svg viewBox="0 0 384 256" width="384" height="256" style={PROP_STYLE}>
      <ellipse cx="192" cy="130" rx="188" ry="122" fill={ART.ink} opacity="0.55" />
      <ellipse cx="192" cy="128" rx="186" ry="120" fill={ART.dark.shade} />
      <ellipse cx="192" cy="128" rx="178" ry="113" fill={ART.dark.base} />
      <ellipse cx="192" cy="128" rx="162" ry="100" fill={ART.steel.shade} opacity="0.8" />
      <ellipse cx="192" cy="128" rx="160" ry="98" fill="none" stroke={ART.amber.base} strokeWidth="5" strokeDasharray="14 10" />
      <ellipse cx="192" cy="128" rx="160" ry="98" fill="none" stroke={ART.ink} strokeWidth="5" strokeDasharray="10 14" strokeDashoffset="12" opacity="0.85" />
      <ellipse cx="192" cy="128" rx="120" ry="72" fill="none" stroke={ART.steel.base} strokeWidth="1.4" opacity="0.7" />
      <ellipse cx="192" cy="128" rx="60" ry="34" fill="none" stroke={ART.steel.base} strokeWidth="1.2" opacity="0.6" />
      <line x1="30" y1="128" x2="354" y2="128" stroke={ART.steel.base} strokeWidth="1" opacity="0.5" /><line x1="192" y1="16" x2="192" y2="240" stroke={ART.steel.base} strokeWidth="1" opacity="0.5" />
      {/* thruster scorch */}
      {[[130, 104, 46, 22], [214, 150, 52, 24], [172, 130, 36, 16], [250, 100, 34, 14], [110, 150, 30, 14]].map(([x, y, rx, ry], i) => <ellipse key={i} cx={x} cy={y} rx={rx} ry={ry} fill={ART.ink} opacity={0.34} />)}
      {/* magnetic tie down clamps */}
      {ring.map(([x, y], i) => (
        <g key={i}>
          <Bev t="polygon" points={pts(ngon(x, y, 9, 6.4, 6))} c="brass" o={0.9} />
          <polygon points={pts(ngon(x, y, 4, 2.8, 6))} fill={ART.ink} />
          <circle cx={x} cy={y} r="1.2" fill={i % 2 ? S.green : S.amber} style={{ animation:`ship-blink ${1.6 + (i % 3) * 0.4}s steps(2) infinite` }} />
        </g>
      ))}
      {/* fuel supply conduits */}
      <Cable d="M354 30 C300 36, 280 60, 250 92" w={5} />
      <Cable d="M30 226 C80 214, 110 190, 140 168" w={5} />
      <Slab x={244} y={86} w={18} h={12} k={2} c="steel" /><Lights x={247} y={92} n={3} gap={4} seed={5} />
      <Slab x={130} y={162} w={18} h={12} k={2} c="steel" /><Lights x={133} y={168} n={3} gap={4} seed={8} />
      {diodes.map(([x, y], i) => <circle key={i} cx={x} cy={y} r="2.2" fill={S.red} opacity={0.5} style={i % 4 === 0 ? { animation:`ship-blink ${1.8 + (i % 5) * 0.3}s steps(2) infinite` } : undefined} />)}
      <text x="56" y="214" fontSize="16" fill={ART.amber.hi} fontWeight="700" letterSpacing="2" opacity="0.5" {...PT}>PAD ALPHA</text>
      <text x="290" y="48" fontSize="9" fill={ART.beige.hi} opacity="0.55" {...PT}>KDY BADOR</text>
      <Seams x={30} y={30} w={324} h={196} cols={6} rows={3} op={0.18} />
      <Grime x={20} y={24} w={344} h={210} seed={601} n={14} op={0.2} /><Scuff x={26} y={30} w={332} h={196} seed={602} n={16} c="steel" />
    </svg>
  );
}

function PropThoroughfare() {
  const S = ART.signal;
  const A = [[0, 62], [140, 0], [300, 14], [440, 100]], B = [[0, 130], [140, 76], [290, 90], [430, 152]];
  const path = (P) => `M${P[0][0]} ${P[0][1]} C${P[1][0]} ${P[1][1]} ${P[2][0]} ${P[2][1]} ${P[3][0]} ${P[3][1]}`;
  const mids = [0.08, 0.2, 0.32, 0.44, 0.56, 0.68, 0.8, 0.92].map((k) => { const a = bez(...A, k), b = bez(...B, k), m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2], n = bez(...A, k + 0.03), n2 = bez(...B, k + 0.03); return { m, d: [(n[0] + n2[0]) / 2 - m[0], (n[1] + n2[1]) / 2 - m[1]], a, b }; });
  return (
    <svg viewBox="0 0 448 192" width="448" height="192" style={PROP_STYLE}>
      <path d={`${path(A)} L${B[3][0]} ${B[3][1]} C${B[2][0]} ${B[2][1]} ${B[1][0]} ${B[1][1]} ${B[0][0]} ${B[0][1]} Z`} fill={ART.steel.shade} opacity="0.55" />
      <path d={path(A)} fill="none" stroke={ART.amber.hi} strokeWidth="3.4" /><path d={path(B)} fill="none" stroke={ART.amber.hi} strokeWidth="3.4" />
      <path d={path(A)} fill="none" stroke={S.orange} strokeWidth="1.2" strokeDasharray="4 8" /><path d={path(B)} fill="none" stroke={S.orange} strokeWidth="1.2" strokeDasharray="4 8" />
      {mids.map(({ m, d, a, b }, i) => {
        const L = Math.hypot(d[0], d[1]) || 1, ux = d[0] / L, uy = d[1] / L;
        return (
          <g key={i}>
            <polyline points={pts([[m[0] - ux * 6 - uy * 7, m[1] - uy * 6 + ux * 7], [m[0] + ux * 4, m[1] + uy * 4], [m[0] - ux * 6 + uy * 7, m[1] - uy * 6 - ux * 7]])} fill="none" stroke={ART.amber.hi} strokeWidth="1.8" opacity="0.8" />
            <Bev t="polygon" points={pts(ngon(a[0], a[1], 4, 3, 6))} c="steel" o={0.5} /><circle cx={a[0]} cy={a[1]} r="1.4" fill={i % 2 ? S.amberHi : S.orange} style={{ animation:`ship-blink ${1.6 + (i % 4) * 0.3}s ease-in-out infinite` }} />
            <Bev t="polygon" points={pts(ngon(b[0], b[1], 4, 3, 6))} c="steel" o={0.5} /><circle cx={b[0]} cy={b[1]} r="1.4" fill={i % 2 ? S.orange : S.amberHi} />
          </g>
        );
      })}
      <Slab x={16} y={40} w={34} h={9} k={1} c="dark" o={0.8} /><text x="33" y="46.6" textAnchor="middle" fontSize="4.2" fill={S.amberHi} fontWeight="700" {...PT}>PAD A</text>
      <Slab x={372} y={106} w={50} h={9} k={1} c="dark" o={0.8} /><text x="397" y="112.6" textAnchor="middle" fontSize="4.2" fill={S.amberHi} fontWeight="700" {...PT}>HQ 400M</text>
      <Rivets x={140} y={96} w={140} n={8} /><Grime x={10} y={20} w={420} h={150} seed={611} n={10} op={0.18} /><Scuff x={14} y={24} w={410} h={140} seed={612} n={12} c="steel" />
    </svg>
  );
}

function PropHqDome() {
  const S = ART.signal;
  const ribs = Array.from({ length: 11 }, (_, i) => { const a = -Math.PI / 2 + (i / 10) * Math.PI; return [262 + Math.cos(a) * 128, 176 + Math.sin(a) * 144]; });
  return (
    <svg viewBox="0 0 448 352" width="448" height="352" style={PROP_STYLE}>
      <PropShadow cx={240} cy={340} rx={200} ry={8} />
      {/* wing blocks */}
      <Bev t="polygon" points={pts([[104, 8], [440, 8], [440, 70], [350, 76], [104, 76]])} c="pearl" o={1.2} />
      <Bev t="polygon" points={pts([[104, 346], [440, 346], [440, 282], [350, 276], [104, 276]])} c="pearl" o={1.2} />
      <Slab x={116} y={14} w={150} h={14} k={1.4} c="deepglass" o={0.9} /><text x="191" y="24.4" textAnchor="middle" fontSize="8" fill={ART.brass.hi} fontWeight="700" letterSpacing="1.4" {...PT}>KDY MAIN HQ BADOR</text>
      {Array.from({ length: 12 }, (_, i) => <polygon key={i} points={pts([[124 + i * 25, 44], [140 + i * 25, 44], [136 + i * 25, 66], [120 + i * 25, 66]])} fill={ART.screen.cyan} />)}
      {Array.from({ length: 12 }, (_, i) => <polygon key={i} points={pts([[124 + i * 25, 290], [140 + i * 25, 290], [136 + i * 25, 312], [120 + i * 25, 312]])} fill={ART.screen.cyan} />)}
      {Array.from({ length: 12 }, (_, i) => <line key={i} x1={128 + i * 25} y1="44" x2={124 + i * 25} y2="66" stroke={S.cyan} strokeWidth="0.8" opacity="0.7" />)}
      <rect x="104" y="72" width="336" height="3" fill={ART.brass.base} /><rect x="104" y="276" width="336" height="3" fill={ART.brass.base} />
      {/* central semi dome */}
      <Bev t="polygon" points={pts([[96, 96], [200, 52], [330, 44], [440, 80], [440, 272], [330, 308], [200, 300], [96, 256]])} c="pearl" o={1.6} />
      <Bev t="polygon" points={pts(ngon(262, 176, 150, 128, 28))} c="pearl" o={1.2} />
      {[128, 100, 72, 44].map((r, i) => <ellipse key={r} cx="262" cy="176" rx={r * 1.12} ry={r} fill="none" stroke={i % 2 ? ART.deepglass.base : ART.brass.base} strokeWidth={i % 2 ? 4 : 2.4} opacity="0.9" />)}
      {ribs.map(([x, y], i) => <line key={i} x1="262" y1="176" x2={x} y2={y} stroke={ART.deepglass.base} strokeWidth="1.6" opacity="0.7" />)}
      <polygon points={pts(ngon(262, 176, 34, 30, 14))} fill={ART.deepglass.shade} /><polygon points={pts(ngon(262, 176, 26, 22, 14))} fill={ART.glass.base} opacity="0.85" />
      <Glow cx={262} cy={176} r={24} c={S.cyan} opacity={0.5} style={{ animation:'ship-engine 3.4s ease-in-out infinite' }} />
      {/* solar slates and masts */}
      {[0, 1, 2].map((i) => <g key={i}><Slab x={376} y={96 + i * 54} w={50} h={44} k={2} c="deepglass" o={0.9} /><Seams x={376} y={96 + i * 54} w={50} h={44} cols={3} rows={2} op={0.4} /></g>)}
      {[[420, 20], [420, 330], [330, 36]].map(([x, y], i) => <g key={i}><line x1={x} y1={y} x2={x} y2={y - 18} stroke={ART.steel.hi} strokeWidth="1.6" /><circle cx={x} cy={y - 19} r="2" fill={S.red} style={{ animation:`ship-blink ${1.5 + i * 0.4}s steps(2) infinite` }} /></g>)}
      {/* recess apron and the blast doors */}
      <polygon points={pts([[32, 96], [96, 96], [96, 224], [32, 224]])} fill={ART.steel.shade} opacity="0.6" />
      <Hazard x={32} y={96} w={64} h={5} /><Hazard x={32} y={219} w={64} h={5} />
      <Bev t="polygon" points={pts([[96, 112], [124, 112], [128, 128], [128, 192], [124, 208], [96, 208]])} c="deepglass" o={1} />
      <Bev t="polygon" points={pts([[100, 128], [126, 128], [126, 158], [100, 158]])} c="pearl" o={0.9} /><Bev t="polygon" points={pts([[100, 162], [126, 162], [126, 192], [100, 192]])} c="pearl" o={0.9} />
      <rect x="100" y="158.6" width="26" height="2.6" fill={ART.brass.base} />
      <circle cx="113" cy="160" r="8" fill="none" stroke={ART.brass.hi} strokeWidth="1.8" /><circle cx="113" cy="160" r="2" fill={ART.brass.hi} />
      {[0, 1, 2, 3, 4, 5].map((i) => <line key={i} x1="113" y1="160" x2={113 + Math.cos(i * 1.047) * 8} y2={160 + Math.sin(i * 1.047) * 8} stroke={ART.brass.hi} strokeWidth="1" />)}
      {[[100, 114], [100, 204]].map(([x, y], i) => <g key={i}><Bev t="polygon" points={pts(ngon(x + 8, y, 6, 6, 6))} c="brass" o={0.8} /><Glow cx={x + 8} cy={y} r={7} c={S.cyan} opacity={0.6} /></g>)}
      <Slab x={36} y={140} w={52} h={9} k={1} c="dark" o={0.85} /><text x="62" y="146.6" textAnchor="middle" fontSize="4.6" fill={S.cyanHi} fontWeight="700" {...PT}>PASS REQUIRED</text>
      <Rivets x={110} y={86} w={320} n={20} c="brass" /><Rivets x={110} y={266} w={320} n={20} c="brass" />
      <Vent x={300} y={56} w={30} h={12} n={3} /><Vent x={300} y={284} w={30} h={12} n={3} />
      <Greeble x={140} y={80} w={200} h={14} seed={621} n={9} c="hull" /><Greeble x={140} y={262} w={200} h={14} seed={622} n={9} c="hull" />
      <Grime x={32} y={8} w={410} h={340} seed={623} n={16} op={0.16} /><Scuff x={36} y={12} w={400} h={330} seed={624} n={18} c="pearl" />
    </svg>
  );
}

function PropBactaPod() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={92} rx={26} ry={3} />
      <Bev t="polygon" points={pts(ngon(32, 84, 26, 8, 8, Math.PI / 8))} c="brass" o={1.2} />
      <Slab x={14} y={74} w={36} h={12} k={2} c="dark" /><Vent x={18} y={77} w={28} h={6} n={3} /><Lights x={20} y={72} n={4} gap={7} seed={13} />
      <Bev t="polygon" points={pts([[16, 74], [16, 14], [48, 14], [48, 74]])} c="glass" o={0.7} />
      <rect x="19" y="34" width="26" height="38" fill={S.green} opacity="0.5" /><rect x="19" y="34" width="26" height="4" fill={S.greenHi} opacity="0.55" />
      {[[26, 62], [34, 54], [40, 66], [30, 46], [38, 42]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r={1 + (i % 2) * 0.6} fill={S.greenHi} style={{ animation:`ship-blink ${1.2 + i * 0.3}s ease-in-out infinite` }} />)}
      <polygon points={pts([[22, 20], [26, 20], [26, 72], [22, 72]])} fill={S.white} opacity="0.3" />
      <ellipse cx="32" cy="48" rx="15" ry="3.4" fill="none" stroke={S.cyan} strokeWidth="1.2" opacity="0.8" />
      <Slab x={12} y={6} w={40} h={10} k={2} c="steel" /><Seams x={12} y={6} w={40} h={10} cols={3} op={0.4} />
      <Cable d="M24 8 C14 2, 8 10, 8 24" w={2} /><Cable d="M40 8 C50 2, 56 10, 56 22" w={2} />
      <Slab x={44} y={28} w={14} h={9} k={1} c="dark" o={0.9} /><text x="51" y="34" textAnchor="middle" fontSize="3" fill={S.greenHi} fontWeight="700" {...PT}>BACTA</text>
      <Rivets x={16} y={14} w={32} n={5} /><Grime x={10} y={10} w={44} h={80} seed={631} n={4} op={0.18} /><Scuff x={14} y={78} w={36} h={10} seed={632} n={4} c="brass" />
    </svg>
  );
}

function PropRepulsorCrane() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 160 96" width="160" height="96" style={PROP_STYLE}>
      <PropShadow cx={80} cy={92} rx={74} ry={3} />
      <Bev t="rect" x="4" y="84" width="152" height="4" c="steel" o={0.8} /><Hazard x={4} y={87} w={152} h={2.4} />
      {[6, 140].map((x) => <g key={x}><Bev t="polygon" points={pts([[x, 86], [x, 14], [x + 12, 14], [x + 12, 86]])} c="amber" o={1.1} /><Seams x={x} y={14} w={12} h={72} rows={5} op={0.4} />{[26, 44, 62].map((y) => <line key={y} x1={x + 1} y1={y} x2={x + 11} y2={y + 8} stroke={ART.dark.shade} strokeWidth="1.2" />)}</g>)}
      <Bev t="polygon" points={pts([[2, 24], [2, 6], [158, 6], [158, 24]])} c="amber" o={1.2} />
      {Array.from({ length: 16 }, (_, i) => <line key={i} x1={6 + i * 9.6} y1="22" x2={14 + i * 9.6} y2="8" stroke={ART.dark.shade} strokeWidth="1.2" />)}
      <Hazard x={2} y={21} w={156} h={3} />
      <Slab x={58} y={22} w={44} h={22} k={2} c="amber" o={1} /><Hazard x={58} y={40} w={44} h={4} /><Vent x={64} y={26} w={14} h={8} n={3} /><Lights x={84} y={29} n={3} gap={4} seed={7} />
      <polygon points={pts([[66, 44], [94, 44], [100, 54], [60, 54]])} fill={ART.dark.base} />
      <polygon points={pts([[60, 54], [100, 54], [118, 82], [42, 82]])} fill={S.violet} opacity="0.2" style={{ animation:'ship-blink 2.6s ease-in-out infinite' }} />
      <Glow cx={80} cy={56} rx={22} ry={5} c={S.violet} opacity={0.8} style={{ animation:'ship-engine 2.6s ease-in-out infinite' }} />
      <CrateBox x={56} y={62} w={48} h={20} c="hull" hz={false} cols={3} rows={0} />
      {[58, 100].map((x) => <polygon key={x} points={pts([[x, 56], [x + 4, 56], [x + 4, 66], [x, 66]])} fill={ART.steel.hi} />)}
      <Slab x={122} y={58} w={32} h={28} k={2} c="dark" /><Slab x={126} y={62} w={24} h={9} k={1} c="steel" o={0.8} />
      <circle cx="132" cy="78" r="2.4" fill={ART.steel.hi} /><circle cx="144" cy="78" r="2.4" fill={ART.steel.hi} /><line x1="132" y1="78" x2="131" y2="72" stroke={ART.steel.hi} strokeWidth="1.2" /><line x1="144" y1="78" x2="145" y2="72" stroke={ART.steel.hi} strokeWidth="1.2" />
      <circle cx="138" cy="66" r="2.6" fill={S.red} /><text x="138" y="84" textAnchor="middle" fontSize="2.6" fill={S.amberHi} {...PT}>CRANE 2</text>
      <Rivets x={8} y={7} w={146} n={16} /><Grime x={4} y={8} w={152} h={80} seed={641} n={7} op={0.22} /><Scuff x={6} y={10} w={148} h={74} seed={642} n={9} c="amber" />
    </svg>
  );
}

function PropRoboticsBench() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {[8, 82].map((x) => <Bev key={x} t="polygon" points={pts([[x, 60], [x, 40], [x + 6, 40], [x + 6, 60]])} c="pearl" o={0.6} />)}
      <Bev t="polygon" points={pts([[2, 44], [94, 44], [90, 34], [6, 34]])} c="dark" o={1.2} /><Slab x={2} y={44} w={92} h={7} k={1} c="steel" o={0.8} /><Seams x={2} y={44} w={92} h={7} cols={7} op={0.4} />
      {/* overhead articulated arm */}
      <Bev t="polygon" points={pts([[44, 34], [44, 8], [50, 8], [50, 34]])} c="steel" o={0.9} />
      <polyline points={pts([[47, 10], [66, 6], [74, 18], [70, 30]])} fill="none" stroke={ART.steel.hi} strokeWidth="3" /><polyline points={pts([[47, 10], [66, 6], [74, 18], [70, 30]])} fill="none" stroke={ART.dark.base} strokeWidth="1" />
      {[[47, 10], [66, 6], [74, 18]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="2.4" fill={ART.brass.base} />)}
      <polygon points={pts([[68, 30], [72, 30], [70, 36]])} fill={ART.steel.hi} /><Glow cx={70} cy={36} r={5} c={S.cyanHi} opacity={0.9} style={{ animation:'ship-blink 1.4s steps(2) infinite' }} />
      {/* diagnostic screens */}
      {[0, 1].map((i) => <g key={i}><Slab x={6 + i * 17} y={14 + i * 3} w={15} h={13} k={1.2} c="dark" /><rect x={7.6 + i * 17} y={15.6 + i * 3} width="11.8" height="9.8" fill={ART.screen.amber} /><polyline points={pts([[9, 22 + i * 3], [12, 18 + i * 3], [14, 22 + i * 3], [17, 17 + i * 3]])} fill="none" stroke={S.amber} strokeWidth="0.8" /></g>)}
      {/* pit droid on the bench */}
      <Bev t="polygon" points={pts([[28, 34], [28, 26], [40, 26], [40, 34]])} c="tan" o={0.7} /><Bev t="polygon" points={pts(ngon(34, 22, 6, 5, 8))} c="tan" o={0.6} /><circle cx="32" cy="22" r="1.6" fill={S.amber} /><circle cx="36" cy="22" r="1.6" fill={S.amber} />
      <line x1="30" y1="34" x2="26" y2="40" stroke={ART.steel.base} strokeWidth="1.4" /><line x1="38" y1="34" x2="42" y2="40" stroke={ART.steel.base} strokeWidth="1.4" />
      {/* solder pot and parts */}
      <Slab x={78} y={28} w={10} h={8} k={1.2} c="copper" o={0.8} /><circle cx="83" cy="27" r="1.6" fill={S.orange} opacity="0.9" /><Cable d="M83 24 C80 18, 86 14, 82 8" w={1} />
      {[[10, 42], [18, 43], [60, 42]].map(([x, y], i) => <Slab key={i} x={x} y={y - 4} w={6} h={4} k={0.8} c={i % 2 ? 'brass' : 'steel'} o={0.7} />)}
      <Rivets x={6} y={46} w={84} n={9} /><Grime x={2} y={30} w={92} h={30} seed={651} n={5} op={0.25} /><Scuff x={4} y={32} w={88} h={26} seed={652} n={7} c="steel" />
    </svg>
  );
}

function PropWeaponBench() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Slab x={3} y={3} w={90} h={34} k={3} c="dark" o={1} /><Seams x={3} y={3} w={90} h={34} cols={5} op={0.3} />
      {Array.from({ length: 6 }, (_, i) => <g key={i}><Slab x={8 + i * 14.4} y={7} w={11} h={5} k={1} c="steel" o={0.9} /><polygon points={pts([[10 + i * 14.4, 12], [17 + i * 14.4, 12], [15 + i * 14.4, 30], [12 + i * 14.4, 30]])} fill={ART.steel.shade} /><rect x={11 + i * 14.4} y="14" width="4.6" height="3" fill={ART.brass.base} /></g>)}
      <Bev t="polygon" points={pts([[2, 50], [94, 50], [90, 40], [6, 40]])} c="steel" o={1.1} /><Slab x={2} y={50} w={92} h={8} k={1} c="dark" o={0.8} />
      <Slab x={36} y={36} w={22} h={8} k={1.4} c="dark" /><rect x="40" y="38" width="14" height="2.4" fill={S.orange} /><Glow cx={47} cy={39} rx={10} ry={4} c={S.orange} opacity={0.5} style={{ animation:'ship-blink 2.2s ease-in-out infinite' }} />
      <polygon points={pts([[40, 44], [54, 44], [58, 36], [36, 36]])} fill={S.orange} opacity="0.16" />
      <Slab x={72} y={34} w={16} h={14} k={1.4} c="amber" o={0.9} /><Hazard x={72} y={45} w={16} h={3} /><circle cx="80" cy="40" r="2.6" fill={ART.ink} /><circle cx="80" cy="40" r="1.2" fill={S.red} />
      {[[10, 44], [18, 45], [26, 44]].map(([x, y], i) => <Slab key={i} x={x} y={y - 3} w={6} h={5} k={0.8} c="brass" o={0.8} />)}
      <Lights x={64} y={46} n={3} gap={3} seed={4} /><Rivets x={6} y={52} w={84} n={9} />
      <Grime x={2} y={34} w={92} h={26} seed={661} n={5} op={0.25} /><Scuff x={4} y={6} w={88} h={50} seed={662} n={8} c="steel" />
    </svg>
  );
}

function PropTargetRange() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      <Slab x={2} y={50} w={92} h={9} k={1.2} c="dark" /><Hazard x={2} y={56} w={92} h={3} />
      <line x1="8" y1="46" x2="88" y2="46" stroke={ART.steel.base} strokeWidth="0.8" strokeDasharray="3 3" />
      {[18, 48, 78].map((x, i) => (
        <g key={x}>
          <Bev t="polygon" points={pts([[x - 3, 46], [x - 3, 20], [x + 3, 20], [x + 3, 46]])} c="steel" o={0.5} />
          <Bev t="polygon" points={pts(ngon(x, 12, 7, 7, 10))} c="pearl" o={0.6} />
          <polygon points={pts(ngon(x, 12, 4.6, 4.6, 10))} fill={ART.red.base} /><polygon points={pts(ngon(x, 12, 2.4, 2.4, 10))} fill={ART.pearl.hi} />
          <Bev t="polygon" points={pts([[x - 8, 40], [x - 6, 22], [x + 6, 22], [x + 8, 40]])} c="dark" o={0.7} />
          <circle cx={x + (i - 1) * 2} cy={30 + i * 2} r="1.2" fill={S.amberHi} />
        </g>
      ))}
      <Slab x={36} y={52} w={24} h={9} k={1.2} c="steel" /><Slab x={38} y={53.6} w={20} h={5} k={0.6} c="dark" /><rect x="39.4" y="54.6" width="17" height="3" fill={ART.screen.green} /><text x="48" y="57" textAnchor="middle" fontSize="2.6" fill={S.greenHi} fontWeight="700" {...PT}>SIM 03</text>
      <Lights x={6} y={5} n={3} gap={4} seed={2} /><Rivets x={4} y={48} w={88} n={10} />
      <Grime x={2} y={8} w={92} h={52} seed={671} n={5} op={0.2} /><Scuff x={4} y={10} w={88} h={46} seed={672} n={7} c="steel" />
    </svg>
  );
}

function PropFuelDiagTerminal() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={26} ry={3.4} />
      <Cable d="M52 50 C60 56, 56 62, 44 62" w={3} /><Cable d="M52 44 C62 50, 60 60, 46 60" w={3} />
      <Slab x={10} y={46} w={38} h={11} k={1.4} c="pearl" o={1} /><Slab x={10} y={46} w={7} h={11} k={1} c="deepglass" o={0.9} /><Slab x={41} y={46} w={7} h={11} k={1} c="deepglass" o={0.9} />
      <Bev t="polygon" points={pts([[8, 10], [50, 10], [54, 14], [54, 44], [8, 44]])} c="pearl" o={1.2} />
      <Bev t="polygon" points={pts([[8, 10], [14, 10], [14, 44], [8, 44]])} c="deepglass" o={1} />
      <polygon points={pts([[16, 14], [48, 14], [50, 40], [18, 40]])} fill={ART.screen.amber} />
      <text x="33" y="19.6" textAnchor="middle" fontSize="3.2" fill={S.amberHi} fontWeight="700" {...PT}>SHUTTLE K-14</text>
      {[0, 1, 2].map((i) => <g key={i}><rect x="20" y={23 + i * 5.4} width="26" height="2.6" fill={ART.dark.base} /><rect x="20" y={23 + i * 5.4} width={[22, 17, 24][i]} height="2.6" fill={[S.amber, S.green, S.cyan][i]} /></g>)}
      <text x="33" y="39" textAnchor="middle" fontSize="2.6" fill={S.amberHi} {...PT}>FUEL 92 HULL 100</text>
      <Rivets x={10} y={12} w={42} n={6} c="brass" /><Lights x={44} y={44} n={3} gap={3} seed={6} />
      <Seams x={8} y={10} w={46} h={34} cols={2} op={0.3} /><Grime x={6} y={10} w={50} h={50} seed={681} n={4} op={0.18} /><Scuff x={10} y={14} w={42} h={40} seed={682} n={5} c="pearl" />
    </svg>
  );
}

function PropSanctumHolo() {
  const S = ART.signal;
  const top = ngon(48, 74, 44, 14, 28);
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={90} rx={44} ry={4.4} />
      <Bev t="polygon" points={pts(ngon(48, 82, 40, 10, 28))} c="pearl" o={1.4} /><polygon points={pts(ngon(48, 82, 34, 7.4, 28))} fill="none" />
      <ellipse cx="48" cy="81" rx="34" ry="7" fill="none" stroke={ART.brass.hi} strokeWidth="1.6" />
      <Bev t="polygon" points={pts(top)} c="deepglass" o={1.4} />
      <ellipse cx="48" cy="73" rx="44" ry="14" fill="none" stroke={ART.brass.base} strokeWidth="2.4" />
      {top.filter((_, i) => i % 4 === 0).map(([x, y], i) => <circle key={i} cx={x} cy={y - 1} r="1.2" fill={ART.brass.hi} />)}
      <polygon points={pts(ngon(48, 72, 36, 11, 28))} fill={ART.screen.cyan} />
      {Array.from({ length: 6 }, (_, i) => <polygon key={i} points={pts([[48, 72], [48 + Math.cos(i * 1.047) * 9, 72 + Math.sin(i * 1.047) * 3], [48 + Math.cos((i + 1) * 1.047) * 9, 72 + Math.sin((i + 1) * 1.047) * 3]])} fill={ART.brass.base} />)}
      <polygon points={pts([[16, 72], [80, 72], [66, 16], [30, 16]])} fill={S.cyan} opacity="0.1" />
      <g style={{ animation:'holo-flicker 3s ease-in-out infinite' }}>
        {[0, 1, 2, 3].map((i) => <polygon key={i} points={pts(ngon(48, 66 - i * 9, 28 - i * 5, 7 - i, 24))} fill="none" stroke={S.cyanHi} strokeWidth="0.6" opacity={0.7 - i * 0.12} />)}
        {Array.from({ length: 6 }, (_, i) => <line key={i} x1={24 + i * 9.6} y1="68" x2={34 + i * 5.6} y2="36" stroke={S.cyan} strokeWidth="0.4" opacity="0.5" />)}
        <polyline points={pts([[28, 62], [38, 52], [48, 56], [60, 44], [68, 50]])} fill="none" stroke={S.cyanHi} strokeWidth="0.9" />
        {[[40, 54], [56, 48], [62, 60], [34, 46]].map(([x, y], i) => <polygon key={i} points={pts([[x - 3, y + 3], [x, y - 3], [x + 3, y + 3]])} fill={S.orange} style={{ animation:`ship-blink ${1.4 + i * 0.4}s ease-in-out infinite` }} />)}
        <text x="48" y="14" textAnchor="middle" fontSize="3" fill={S.cyanHi} fontWeight="700" {...PT}>BADOR 2 RPM</text>
      </g>
      <Glow cx={48} cy={72} rx={30} ry={7} c={S.cyan} opacity={0.5} />
      <Rivets x={14} y={86} w={68} n={10} c="brass" /><Seams x={10} y={78} w={76} h={10} cols={7} op={0.3} />
      <Grime x={8} y={64} w={80} h={26} seed={691} n={4} op={0.16} /><Scuff x={10} y={66} w={76} h={22} seed={692} n={5} c="pearl" />
    </svg>
  );
}

function PropCommandBoard() {
  const S = ART.signal;
  const slips = [[10, 12, 'note', 'yellow'], [28, 11, 'note', 'pink'], [46, 12, 'note', 'yellow'], [66, 11, 'note', 'yellow'], [10, 30, 'note', 'pink'], [28, 31, 'note', 'yellow'], [46, 30, 'note', 'yellow']];
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={42} ry={3} />
      <Slab x={2} y={2} w={92} h={56} k={3} c="deepglass" o={1.1} />
      <rect x="6" y="6" width="84" height="48" fill={ART.pearl.shade} />
      <rect x="6" y="6" width="84" height="48" fill={ART.dark.base} opacity="0.35" />
      <rect x="4" y="4" width="88" height="2" fill={ART.brass.base} /><rect x="4" y="54" width="88" height="2" fill={ART.brass.base} />
      <text x="48" y="12" textAnchor="middle" fontSize="4.4" fill={ART.brass.hi} fontWeight="700" letterSpacing="1" {...PT}>COMMAND NOTICES</text>
      {slips.map(([x, y, , c], i) => <g key={i} transform={`rotate(${(i % 3) - 1} ${x + 8} ${y + 7})`}><rect x={x + 4} y={y + 6} width="15" height="12" fill={ART.note[c]} /><rect x={x + 6} y={y + 9} width="11" height="1" fill={ART.note.ink} opacity="0.7" /><rect x={x + 6} y={y + 12} width="8" height="1" fill={ART.note.ink} opacity="0.6" /><circle cx={x + 11.5} cy={y + 7.2} r="1" fill={S.red} /></g>)}
      <Slab x={62} y={28} w={26} h={20} k={1.4} c="dark" o={0.9} /><rect x="64" y="30" width="22" height="16" fill={ART.screen.red} />
      <text x="75" y="35.6" textAnchor="middle" fontSize="3.4" fill={S.redHi} fontWeight="700" {...PT}>BOUNTIES</text><text x="75" y="41" textAnchor="middle" fontSize="2.6" fill={S.amberHi} {...PT}>3 OPEN</text><text x="75" y="45" textAnchor="middle" fontSize="2.2" fill={S.amberHi} {...PT}>ENGINE WASTES</text>
      <Slab x={36} y={56} w={24} h={4} k={0.8} c="brass" o={0.6} />
      <Rivets x={8} y={8} w={80} n={9} c="brass" /><Rivets x={8} y={52} w={80} n={9} c="brass" />
      <Grime x={2} y={2} w={92} h={58} seed={701} n={4} op={0.16} /><Scuff x={4} y={4} w={88} h={52} seed={702} n={5} c="steel" />
    </svg>
  );
}

function PropKuatiTerminal({ variant }) {
  const S = ART.signal;
  const cfg = {
    security: { title: 'HQ SECURITY', rows: ['CLEARANCE: PASS', 'BASE LOGS LOCKED', 'YELLOW ALERT'], scr: ART.screen.cyan, ink: S.cyanHi, trim: S.cyan },
    manifest: { title: 'SUPPLY MANIFEST', rows: ['SKIFF KDY-9: OVERDUE', 'ALLOYS 412 T', 'BUY / SELL OPEN'], scr: ART.screen.amber, ink: S.amberHi, trim: S.amber },
    ledger: { title: 'QUARTERMASTER', rows: ['ISSUE LEDGER', 'ARMORY SEALED', 'AUDIT: PENDING'], scr: ART.screen.green, ink: S.greenHi, trim: S.green },
  }[variant] || { title: 'KUATI NODE', rows: ['STANDBY'], scr: ART.screen.cyan, ink: S.cyanHi, trim: S.cyan };
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={24} ry={3.4} />
      <Slab x={20} y={44} w={24} h={13} k={1.4} c="deepglass" /><Slab x={12} y={54} w={40} h={5} k={1.2} c="pearl" o={0.9} />
      <Bev t="polygon" points={pts([[8, 5], [56, 5], [60, 10], [60, 42], [56, 46], [8, 46], [4, 42], [4, 10]])} c="pearl" o={1.2} />
      <rect x="4" y="22" width="56" height="2" fill={ART.brass.base} />
      <rect x="9" y="9" width="46" height="26" fill={cfg.scr} /><rect x="9" y="9" width="46" height="4.6" fill={cfg.trim} opacity="0.3" />
      <text x="32" y="12.8" textAnchor="middle" fontSize="3.2" fill={cfg.ink} fontWeight="700" {...PT}>{cfg.title}</text>
      {cfg.rows.map((r, i) => <text key={i} x="12" y={19.6 + i * 4.4} fontSize="2.8" fill={cfg.ink} opacity="0.9" {...PT}>{r}</text>)}
      <Slab x={9} y={37} w={46} h={7} k={1} c="deepglass" o={0.9} />
      <Toggles x={12} y={39} cols={8} rows={1} gap={4.6} seed={9} /><Lights x={44} y={41} n={3} gap={3.4} seed={11} />
      <Rivets x={8} y={7} w={48} n={7} c="brass" /><Seams x={4} y={5} w={56} h={41} cols={1} op={0.2} />
      <Grime x={4} y={5} w={56} h={52} seed={711} n={4} op={0.18} /><Scuff x={8} y={9} w={48} h={34} seed={712} n={5} c="pearl" />
    </svg>
  );
}

function PropFacilitySign({ variant }) {
  const S = ART.signal;
  const cfg = {
    medic: { t: 'MEDIC TENT', s: 'TRIAGE · BACTA', c: 'teal', f: 11 },
    goods: { t: 'GOODS & MATERIALS', s: 'DEPOT · SUPPLY SKIFFS', c: 'amber', f: 8.6 },
    trike: { t: "TRIKE'S ROBOTICS", s: 'REPAIR · UPGRADES', c: 'copper', f: 9.4 },
    barracks: { t: 'WEAPONS BARRACKS', s: 'ARMORY · RANGE', c: 'red', f: 9 },
    hq: { t: 'KDY MAIN HQ', s: 'PASS REQUIRED', c: 'deepglass', f: 11 },
    mfg: { t: 'SECTOR 4 FABRICATION', s: 'SHIFT QUOTA 114 PCT', c: 'rust', f: 7.6 },
    slag: { t: 'SLAG LINE FREIGHT LOCK', s: 'TUNNEL DEAD', c: 'dark', f: 7 },
  }[variant] || { t: 'KDY BADOR', s: 'FORWARD HUB', c: 'steel' };
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={54} ry={2.6} />
      {[14, 108].map((x) => <Bev key={x} t="polygon" points={pts([[x, 60], [x, 30], [x + 5, 30], [x + 5, 60]])} c="steel" o={0.7} />)}
      <Bev t="polygon" points={pts([[6, 8], [122, 8], [124, 10], [124, 44], [122, 46], [6, 46], [4, 44], [4, 10]])} c={cfg.c} o={1.2} />
      <Seams x={4} y={8} w={120} h={38} cols={3} op={0.25} />
      <rect x="8" y="12" width="112" height="30" fill={ART.screen.off} opacity="0.55" />
      <text x="64" y="27" textAnchor="middle" fontSize={cfg.f || 11} fill={ART.pearl.hi} fontWeight="700" letterSpacing="0.6" {...PT}>{cfg.t}</text>
      <text x="64" y="37" textAnchor="middle" fontSize="4" fill={S.amberHi} {...PT}>{cfg.s}</text>
      <Hazard x={8} y={40} w={112} h={2.4} />
      {[10, 118].map((x) => <circle key={x} cx={x} cy="14" r="1.6" fill={S.amber} style={{ animation:'ship-blink 2s steps(2) infinite' }} />)}
      <Rivets x={10} y={10} w={108} n={13} /><Grime x={4} y={8} w={120} h={52} seed={721} n={5} op={0.2} /><Scuff x={6} y={10} w={116} h={34} seed={722} n={6} c="steel" />
    </svg>
  );
}

function PropKuatiBanner() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={92} rx={22} ry={2.6} />
      <Bev t="polygon" points={pts([[8, 10], [56, 10], [56, 14], [8, 14]])} c="brass" o={1} />
      <circle cx="8" cy="12" r="2.2" fill={ART.brass.hi} /><circle cx="56" cy="12" r="2.2" fill={ART.brass.hi} />
      <Bev t="polygon" points={pts([[12, 14], [52, 14], [52, 78], [32, 90], [12, 78]])} c="deepglass" o={1.2} />
      <polygon points={pts([[12, 14], [16, 14], [16, 76], [12, 78]])} fill={ART.pearl.base} /><polygon points={pts([[48, 14], [52, 14], [52, 78], [48, 76]])} fill={ART.pearl.base} />
      <polygon points={pts([[16, 74], [48, 74], [32, 85]])} fill={ART.brass.base} opacity="0.8" />
      <circle cx="32" cy="42" r="14" fill="none" stroke={ART.brass.hi} strokeWidth="2.4" /><circle cx="32" cy="42" r="3.4" fill={ART.brass.hi} />
      {Array.from({ length: 8 }, (_, i) => <line key={i} x1="32" y1="42" x2={32 + Math.cos(i * Math.PI / 4) * 14} y2={42 + Math.sin(i * Math.PI / 4) * 14} stroke={ART.brass.hi} strokeWidth="1.4" />)}
      <text x="32" y="68" textAnchor="middle" fontSize="4" fill={ART.brass.hi} fontWeight="700" letterSpacing="1" {...PT}>KUAT</text>
      <Seams x={12} y={14} w={40} h={62} rows={3} op={0.2} /><Rivets x={14} y={20} w={36} n={5} c="brass" />
      <Grime x={10} y={12} w={44} h={78} seed={731} n={3} op={0.14} /><Scuff x={12} y={16} w={40} h={70} seed={732} n={4} c="pearl" />
    </svg>
  );
}

function PropTerminalBank() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={61} rx={58} ry={2.8} />
      <Slab x={2} y={30} w={124} h={28} k={2} c="pearl" o={1.2} /><rect x="2" y="30" width="124" height="2" fill={ART.brass.base} />
      <Slab x={6} y={34} w={116} h={20} k={1.6} c="deepglass" o={0.9} />
      {[0, 1, 2, 3].map((i) => <g key={i}>
        <Bev t="polygon" points={pts([[10 + i * 28, 36], [32 + i * 28, 36], [34 + i * 28, 14], [8 + i * 28, 14]])} c="dark" o={1} />
        <polygon points={pts([[12 + i * 28, 34], [30 + i * 28, 34], [31 + i * 28, 17], [11 + i * 28, 17]])} fill={ART.screen.cyan} />
        <polyline points={pts([[14 + i * 28, 30], [18 + i * 28, 24], [22 + i * 28, 28], [28 + i * 28, 20]])} fill="none" stroke={S.cyanHi} strokeWidth="0.9" />
        <rect x={14 + i * 28} y="19" width={6 + i * 2} height="1.4" fill={S.cyan} opacity="0.7" />
        <Toggles x={13 + i * 28} y={42} cols={5} rows={2} gap={3.8} seed={20 + i} />
        <circle cx={31 + i * 28} cy="38" r="1" fill={i % 2 ? S.green : S.amber} />
      </g>)}
      <Rivets x={8} y={55} w={112} n={14} c="brass" /><Vent x={52} y={4} w={24} h={6} n={2} />
      <Grime x={2} y={10} w={124} h={50} seed={741} n={4} op={0.16} /><Scuff x={4} y={32} w={120} h={24} seed={742} n={6} c="pearl" />
    </svg>
  );
}

function PropFloorInlay() {
  return (
    <svg viewBox="0 0 160 160" width="160" height="160" style={PROP_STYLE}>
      <Bev t="polygon" points={pts(ngon(80, 80, 76, 76, 32))} c="pearl" o={0.9} />
      {[70, 54, 40].map((r, i) => <circle key={r} cx="80" cy="80" r={r} fill="none" stroke={ART.brass.base} strokeWidth={i === 0 ? 3 : 1.6} opacity="0.9" />)}
      <circle cx="80" cy="80" r="22" fill={ART.deepglass.base} opacity="0.7" />
      <circle cx="80" cy="80" r="22" fill="none" stroke={ART.brass.hi} strokeWidth="2.6" /><circle cx="80" cy="80" r="5" fill={ART.brass.hi} />
      {Array.from({ length: 12 }, (_, i) => <line key={i} x1="80" y1="80" x2={80 + Math.cos(i * Math.PI / 6) * 22} y2={80 + Math.sin(i * Math.PI / 6) * 22} stroke={ART.brass.hi} strokeWidth="1.6" />)}
      {Array.from({ length: 8 }, (_, i) => <polygon key={i} points={pts([[80 + Math.cos(i * Math.PI / 4) * 70, 80 + Math.sin(i * Math.PI / 4) * 70], [80 + Math.cos(i * Math.PI / 4 + 0.12) * 56, 80 + Math.sin(i * Math.PI / 4 + 0.12) * 56], [80 + Math.cos(i * Math.PI / 4 - 0.12) * 56, 80 + Math.sin(i * Math.PI / 4 - 0.12) * 56]])} fill={ART.brass.base} />)}
      <Seams x={6} y={6} w={148} h={148} cols={4} rows={4} op={0.12} /><Rivets x={20} y={20} w={120} n={6} c="brass" />
      <Grime x={8} y={8} w={144} h={144} seed={751} n={6} op={0.1} /><Scuff x={10} y={10} w={140} h={140} seed={752} n={8} c="pearl" />
    </svg>
  );
}
// ===== BADOR BASE PROPS END =====
// ===== SENATE LOUNGE PROPS BEGIN =====
// Senatorial Sky Lounges: deep garnet velvet, electrum and gold leaf, obsidian, royal azure, sandstone, and the dusk skyline.
function PropSkylineVista({ variant }) {
  const S = ART.signal, K = ART.sky;
  const night = variant === 'night' || variant === 'nighthud';
  const hud = variant === 'hud' || variant === 'nighthud';
  const r = rng(77);
  const bands = night ? [[0, 24, ART.ink], [24, 46, K.indigo], [46, 64, K.violet], [64, 78, K.plum], [78, 88, K.rose], [88, 100, K.haze]] : [[0, 16, K.indigo], [16, 32, K.violet], [32, 46, K.plum], [46, 58, K.rose], [58, 70, K.ember], [70, 80, K.amber], [80, 100, K.haze]];
  const stars = Array.from({ length: 70 }, () => ({ x: r() * 1216, y: r() * 70, s: 0.6 + r() * 0.9, d: r() * 4 }));
  const pyramids = [[40, 100, 180, 62], [260, 100, 150, 74], [520, 100, 220, 56], [800, 100, 180, 70], [1010, 100, 190, 58]];
  const spires = Array.from({ length: 30 }, (_, i) => { const x = 8 + i * 40 + r() * 10, w = 16 + r() * 18, h = 30 + r() * 60; return { x, w, h, tier: r() < 0.5, i }; });
  const far = Array.from({ length: 26 }, (_, i) => ({ x: 20 + i * 46 + r() * 14, w: 14 + r() * 14, h: 14 + r() * 26 }));
  const windows = (sx, sy, w, h, seed, c) => { const rr = rng(seed), out = []; for (let yy = sy + 4; yy < sy + h - 3; yy += 5) for (let xx = sx + 3; xx < sx + w - 3; xx += 5) if (rr() < (night ? 0.64 : 0.42)) out.push(<rect key={xx + '_' + yy} x={xx} y={yy} width="2.4" height="2.4" fill={c} opacity={0.55 + rr() * 0.4} />); return out; };
  const saucer = (cx, base, sc, key) => (
    <g key={key}>
      <polygon points={pts([[cx - 120 * sc, base - 100 * sc], [cx - 60 * sc, base - 120 * sc], [cx + 60 * sc, base - 120 * sc], [cx + 120 * sc, base - 100 * sc]])} fill={ART.bronze.shade} />
      {[0, 1, 2].map((t) => <Bev key={t} t="polygon" points={pts([[cx - (150 - t * 24) * sc, base - (20 + t * 26) * sc], [cx - (120 - t * 24) * sc, base - (42 + t * 26) * sc], [cx + (120 - t * 24) * sc, base - (42 + t * 26) * sc], [cx + (150 - t * 24) * sc, base - (20 + t * 26) * sc]])} c="bronze" o={1} />)}
      <polygon points={pts([[cx - 56 * sc, base - 98 * sc], [cx, base - 128 * sc], [cx + 56 * sc, base - 98 * sc]])} fill={ART.bronze.hi} />
      <rect x={cx - 150 * sc} y={base - 20 * sc} width={300 * sc} height={20 * sc} fill={ART.dark.shade} />
      {[0, 1, 2].map((t) => Array.from({ length: 14 }, (_, k) => <rect key={t + '_' + k} x={cx - (112 - t * 24) * sc + k * (16 - t * 3.2) * sc} y={base - (36 + t * 26) * sc} width={4 * sc} height={5 * sc} fill={S.amberHi} opacity={0.7} />))}
      <line x1={cx} y1={base - 128 * sc} x2={cx} y2={base - 146 * sc} stroke={ART.steel.hi} strokeWidth="1.6" /><circle cx={cx} cy={base - 148 * sc} r="2" fill={S.red} style={{ animation: 'ship-blink 1.8s steps(2) infinite' }} />
    </g>
  );
  return (
    <svg viewBox="0 0 1216 160" width="1216" height="160" style={PROP_STYLE}>
      {bands.map(([a, b, c], i) => <rect key={i} x="0" y={a} width="1216" height={b - a + 1} fill={c} />)}
      {!night && <g><circle cx="930" cy="82" r="44" fill={K.amber} opacity="0.2" /><circle cx="930" cy="82" r="30" fill={K.amber} opacity="0.4" /><circle cx="930" cy="82" r="20" fill={S.amberHi} /></g>}
      {night && stars.map((st, i) => <circle key={i} cx={st.x} cy={st.y} r={st.s} fill={S.kyber} opacity="0.7" style={{ animation: `ship-blink ${2 + st.d}s ease-in-out ${st.d}s infinite` }} />)}
      {night && <g><circle cx="150" cy="30" r="14" fill={ART.pearl.base} opacity="0.85" /><circle cx="144" cy="26" r="3" fill={ART.pearl.shade} opacity="0.5" /><circle cx="156" cy="35" r="2" fill={ART.pearl.shade} opacity="0.4" /></g>}
      {[[120, 24, 70], [420, 40, 90], [700, 18, 120], [1060, 34, 80]].map(([x, y, w], i) => <rect key={i} x={x} y={y} width={w} height="2.6" fill={K.rose} opacity="0.35" style={{ animation: `cast-sweep ${40 + i * 9}s linear ${i * 4}s infinite` }} />)}
      {/* distant step pyramids and far spires, fading into haze */}
      {pyramids.map(([x, y, w, h], i) => <polygon key={i} points={pts([[x, y], [x + w * 0.1, y - h * 0.6], [x + w * 0.22, y - h * 0.6], [x + w * 0.28, y - h * 0.86], [x + w * 0.42, y - h * 0.86], [x + w * 0.5, y - h], [x + w * 0.6, y - h * 0.86], [x + w * 0.76, y - h * 0.6], [x + w, y]])} fill={K.spire} opacity="0.45" />)}
      {far.map((f, i) => <rect key={i} x={f.x} y={100 - f.h} width={f.w} height={f.h + 1} fill={K.spire} opacity="0.5" />)}
      <rect x="0" y="70" width="1216" height="30" fill={K.haze} opacity="0.2" />
      {saucer(180, 100, 0.34, 'sa')}{saucer(1030, 100, 0.4, 'sb')}
      {/* spires, art deco crowns, amber window grids */}
      {spires.map((s) => (
        <g key={s.i}>
          <rect x={s.x} y={100 - s.h} width={s.w} height={s.h + 40} fill={K.spire} />
          <rect x={s.x} y={100 - s.h} width={s.w * 0.28} height={s.h + 40} fill={K.spireHi} opacity="0.45" />
          {s.tier && <rect x={s.x + s.w * 0.18} y={100 - s.h - 10} width={s.w * 0.64} height="11" fill={K.spire} />}
          {s.tier && <polygon points={pts([[s.x + s.w * 0.3, 100 - s.h - 10], [s.x + s.w * 0.5, 100 - s.h - 22], [s.x + s.w * 0.7, 100 - s.h - 10]])} fill={K.spireHi} />}
          {Array.from({ length: 3 }, (_, k) => <line key={k} x1={s.x + (s.w * (k + 1)) / 4} y1={100 - s.h} x2={s.x + (s.w * (k + 1)) / 4} y2={100} stroke={ART.ink} strokeWidth="0.5" opacity="0.4" />)}
          {windows(s.x, 100 - s.h, s.w, s.h, 200 + s.i, S.amberHi)}
          {s.i % 5 === 0 && <circle cx={s.x + s.w / 2} cy={100 - s.h - (s.tier ? 24 : 4)} r="1.6" fill={S.red} style={{ animation: `ship-blink ${1.4 + (s.i % 4) * 0.4}s steps(2) infinite` }} />}
        </g>
      ))}
      {/* Galactic Senate Rotunda: bronze domed saucer with spotlight beams */}
      <g>
        <polygon points={pts([[500, 70], [560, 38], [656, 38], [716, 70]])} fill={ART.bronze.shade} />
        <Bev t="polygon" points={pts([[470, 100], [520, 72], [696, 72], [746, 100]])} c="bronze" o={1.6} />
        <Bev t="polygon" points={pts([[520, 72], [556, 46], [660, 46], [696, 72]])} c="bronze" o={1.2} />
        <polygon points={pts([[556, 46], [608, 14], [660, 46]])} fill={ART.bronze.hi} /><polygon points={pts([[608, 14], [660, 46], [632, 46]])} fill={ART.brass.hi} opacity="0.6" />
        {Array.from({ length: 26 }, (_, k) => <rect key={k} x={486 + k * 9.6} y={90} width="4" height="4" fill={S.amberHi} opacity="0.8" />)}
        {Array.from({ length: 18 }, (_, k) => <rect key={k} x={532 + k * 9} y={64} width="3.4" height="4" fill={S.amberHi} opacity="0.7" />)}
        <line x1="608" y1="14" x2="608" y2="2" stroke={ART.steel.hi} strokeWidth="1.4" /><circle cx="608" cy="1.6" r="1.8" fill={S.red} style={{ animation: 'ship-blink 2s steps(2) infinite' }} />
        {[[560, 600], [656, 700]].map(([bx, tx], i) => <polygon key={i} points={pts([[bx - 6, 100], [bx + 6, 100], [tx + 24, 0], [tx - 24, 0]])} fill={S.kyber} opacity={night ? 0.12 : 0.06} style={{ animation: `holo-flicker ${3 + i}s ease-in-out infinite` }} />)}
      </g>
      <rect x="0" y="94" width="1216" height="46" fill={K.indigo} opacity="0.55" />
      {/* skyway traffic: orange northbound, blue white southbound */}
      {[[96, S.orange, 'skyline-lane', 0], [104, S.orange, 'skyline-lane', 1], [112, S.kyber, 'skyline-lane-rev', 2], [120, S.kyber, 'skyline-lane-rev', 3]].map(([y, c, anim, i]) => (
        <g key={i}>
          <line x1="0" y1={y} x2="1216" y2={y} stroke={c} strokeWidth="1.6" strokeDasharray="6 10" opacity="0.85" style={{ animation: `${anim} ${5 + i * 1.3}s linear infinite` }} />
          <line x1="0" y1={y} x2="1216" y2={y} stroke={c} strokeWidth="4" strokeDasharray="1 40" opacity="0.4" style={{ animation: `${anim} ${8 + i}s linear infinite` }} />
        </g>
      ))}
      {[[92, 0, S.amberHi, 7], [100, 3, S.orange, 9], [108, 1, S.kyber, 8], [116, 5, S.kyber, 11], [88, 7, S.amberHi, 12]].map(([y, d, c, t], i) => (
        <g key={i} style={{ animation: `${i % 2 ? 'skyline-fly-rev' : 'skyline-fly'} ${t}s linear ${d}s infinite` }}>
          <polygon points={pts([[0, y], [10, y - 2], [14, y], [10, y + 2]])} fill={c} /><rect x="-18" y={y - 0.6} width="18" height="1.4" fill={c} opacity="0.5" />
        </g>
      ))}
      {/* lower canyon glow */}
      <rect x="0" y="124" width="1216" height="16" fill={ART.ink} opacity="0.55" />
      {Array.from({ length: 12 }, (_, i) => <rect key={i} x={30 + i * 102} y={126 + (i % 3) * 4} width={50 + (i % 4) * 14} height="1.8" fill={[S.cyan, S.magenta, S.blue][i % 3]} opacity="0.7" />)}
      {/* window mullions with reflected light streaks */}
      {Array.from({ length: 9 }, (_, i) => <g key={i}><Bev t="polygon" points={pts([[i * 152 - 3, 0], [i * 152 + 3, 0], [i * 152 + 3, 140], [i * 152 - 3, 140]])} c="dark" o={0.8} /></g>)}
      {[[130, 0.14], [470, 0.1], [840, 0.12]].map(([x, o], i) => <polygon key={i} points={pts([[x, 0], [x + 26, 0], [x - 40, 138], [x - 66, 138]])} fill={S.white} opacity={o * 0.6} />)}
      {/* obsidian sill, brass inlay, repulsor safety field */}
      <rect x="0" y="138" width="1216" height="22" fill={ART.dark.shade} /><rect x="0" y="138" width="1216" height="3" fill={ART.brass.base} />
      {Array.from({ length: 38 }, (_, i) => <rect key={i} x={i * 32 + 8} y="146" width="16" height="2" fill={ART.brass.hi} opacity="0.5" />)}
      <line x1="0" y1="133" x2="1216" y2="133" stroke={S.cyan} strokeWidth="1.2" strokeDasharray="3 5" opacity="0.8" style={{ animation: 'skyline-lane 2.4s linear infinite' }} />
      {Array.from({ length: 20 }, (_, i) => <Bev key={i} t="polygon" points={pts(ngon(i * 64 + 24, 134, 3, 3, 6))} c="brass" o={0.6} />)}
      <Seams x={0} y={138} w={1216} h={22} cols={37} op={0.25} /><Rivets x={4} y={157} w={1208} n={76} c="brass" />
      <Grime x={0} y={138} w={1216} h={22} seed={801} n={10} op={0.2} /><Scuff x={0} y={140} w={1216} h={18} seed={802} n={14} c="dark" />
      {hud && (
        <g style={{ animation: 'holo-flicker 3s ease-in-out infinite' }}>
          {[[608, 14, 'GALACTIC SENATE ROTUNDA', 640, 4], [180, 40, 'EXECUTIVE SAUCER 4', 120, 26], [1030, 36, 'EXECUTIVE SAUCER 7', 960, 18], [330, 56, 'JEDI TEMPLE SPIRES', 270, 44], [880, 92, 'LANE 5120 NORTH', 800, 78], [380, 114, 'LANE 5120 SOUTH', 300, 128], [1100, 130, 'LOWER CANYON 5000 M', 1010, 122]].map(([x, y, t, tx, ty], i) => (
            <g key={i}>
              <line x1={x} y1={y} x2={tx} y2={ty} stroke={S.cyanHi} strokeWidth="0.7" />
              <circle cx={x} cy={y} r="2" fill="none" stroke={S.cyanHi} strokeWidth="0.8" />
              <rect x={tx - 2} y={ty - 6} width={t.length * 4.6 + 8} height="9" fill={ART.screen.cyan} opacity="0.9" /><text x={tx + 2} y={ty + 0.8} fontSize="5.6" fill={S.cyanHi} fontWeight="700" {...PT}>{t}</text>
            </g>
          ))}
        </g>
      )}
    </svg>
  );
}

function PropCrestInlay() {
  return (
    <svg viewBox="0 0 160 160" width="160" height="160" style={PROP_STYLE}>
      <Bev t="polygon" points={pts(ngon(80, 80, 76, 76, 40))} c="dark" o={1.2} />
      <circle cx="80" cy="80" r="70" fill="none" stroke={ART.brass.base} strokeWidth="3" />
      <circle cx="80" cy="80" r="60" fill="none" stroke={ART.brass.hi} strokeWidth="1.2" opacity="0.8" />
      {Array.from({ length: 24 }, (_, i) => { const a = (i / 24) * Math.PI * 2; return <rect key={i} x={80 + Math.cos(a) * 65 - 1.4} y={80 + Math.sin(a) * 65 - 2} width="2.8" height="4" fill={ART.brass.hi} opacity="0.85" transform={`rotate(${(a * 180) / Math.PI + 90} ${80 + Math.cos(a) * 65} ${80 + Math.sin(a) * 65})`} />; })}
      <Bev t="polygon" points={pts(Array.from({ length: 24 }, (_, i) => { const a = (i / 24) * Math.PI * 2, rr = i % 2 ? 28 : 38; return [+(80 + Math.cos(a) * rr).toFixed(2), +(80 + Math.sin(a) * rr).toFixed(2)]; }))} c="red" o={1} />
      <polygon points={pts(ngon(80, 80, 22, 22, 24))} fill={ART.dark.base} /><polygon points={pts(ngon(80, 80, 12, 12, 24))} fill={ART.brass.hi} /><circle cx="80" cy="80" r="4" fill={ART.dark.shade} />
      {Array.from({ length: 8 }, (_, i) => <line key={i} x1="80" y1="80" x2={80 + Math.cos(i * Math.PI / 4) * 12} y2={80 + Math.sin(i * Math.PI / 4) * 12} stroke={ART.dark.shade} strokeWidth="1.2" />)}
      <Seams x={6} y={6} w={148} h={148} cols={4} rows={4} op={0.14} /><Rivets x={20} y={146} w={120} n={7} c="brass" />
      <Grime x={8} y={8} w={144} h={144} seed={811} n={5} op={0.12} /><Scuff x={10} y={10} w={140} h={140} seed={812} n={8} c="dark" />
    </svg>
  );
}

function PropRepulsorChandelier() {
  const S = ART.signal;
  const ring = (cx, cy, rx, ry, k, n) => (
    <g key={k}>
      <ellipse cx={cx} cy={cy} rx={rx} ry={ry} fill="none" stroke={ART.bronze.base} strokeWidth="4" />
      <ellipse cx={cx} cy={cy - 1} rx={rx} ry={ry} fill="none" stroke={ART.brass.hi} strokeWidth="1.2" />
      {Array.from({ length: n }, (_, i) => { const a = (i / n) * Math.PI * 2; return <polygon key={i} points={pts([[cx + Math.cos(a) * rx - 3, cy + Math.sin(a) * ry], [cx + Math.cos(a) * rx, cy + Math.sin(a) * ry - 7], [cx + Math.cos(a) * rx + 3, cy + Math.sin(a) * ry]])} fill={S.amberHi} opacity="0.85" />; })}
    </g>
  );
  return (
    <svg viewBox="0 0 128 128" width="128" height="128" style={PROP_STYLE}>
      <g style={{ animation: 'drift 7s ease-in-out infinite alternate' }}>
        <ellipse cx="64" cy="104" rx="46" ry="12" fill={ART.ink} opacity="0.3" />
        <Glow cx={64} cy={100} rx={44} ry={14} c={S.amberHi} opacity={0.35} style={{ animation: 'holo-flicker 4s ease-in-out infinite' }} />
        {[[64, 74, 52, 14, 'a', 14], [64, 58, 36, 10, 'b', 10], [64, 44, 20, 6, 'c', 6]].map(([cx, cy, rx, ry, k, n]) => ring(cx, cy, rx, ry, k, n))}
        <Bev t="polygon" points={pts(ngon(64, 58, 6, 4, 8))} c="brass" o={0.8} />
        <line x1="64" y1="12" x2="64" y2="50" stroke={ART.steel.base} strokeWidth="1" strokeDasharray="2 3" />
        {[0, 1, 2, 3].map((i) => <polygon key={i} points={pts([[36 + i * 18, 78], [40 + i * 18, 78], [38 + i * 18, 94]])} fill={S.amberHi} opacity="0.16" />)}
      </g>
      <Rivets x={46} y={120} w={36} n={5} c="brass" /><Grime x={20} y={30} w={90} h={80} seed={821} n={3} op={0.1} /><Scuff x={24} y={34} w={80} h={70} seed={822} n={4} c="brass" />
    </svg>
  );
}

function PropConversationPit() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <ellipse cx="64" cy="52" rx="60" ry="38" fill={ART.ink} opacity="0.45" />
      <Bev t="polygon" points={pts(ngon(64, 48, 58, 36, 28))} c="dark" o={1.3} />
      <ellipse cx="64" cy="48" rx="56" ry="34" fill="none" stroke={S.blue} strokeWidth="2" strokeDasharray="4 4" opacity="0.8" style={{ animation: 'ship-engine 3s ease-in-out infinite' }} />
      <polygon points={pts(ngon(64, 50, 48, 28, 28))} fill={ART.fabric.shade} />
      <Bev t="polygon" points={pts([[16, 50], [26, 30], [102, 30], [112, 50], [104, 62], [24, 62]])} c="red" o={1.4} />
      {[0, 1, 2, 3, 4, 5].map((i) => <ellipse key={i} cx={30 + i * 14} cy={42 + (i % 2) * 2} rx="6" ry="3.6" fill={ART.red.hi} opacity="0.5" />)}
      <polyline points={pts([[16, 50], [26, 30], [102, 30], [112, 50], [104, 62], [24, 62], [16, 50]])} fill="none" stroke={ART.ink} strokeWidth="0.9" opacity="0.7" />
      <polygon points={pts(ngon(64, 52, 20, 11, 20))} fill={ART.dark.base} /><polygon points={pts(ngon(64, 51, 18, 9.4, 20))} fill={ART.wood.base} /><polygon points={pts(ngon(64, 50, 12, 6, 16))} fill={ART.wood.hi} opacity="0.7" />
      <rect x="58" y="46" width="12" height="3.6" fill={ART.brass.hi} /><circle cx="52" cy="52" r="2.2" fill={S.cyanHi} opacity="0.8" /><circle cx="74" cy="51" r="1.8" fill={S.amberHi} />
      <Glow cx={64} cy={52} rx={52} ry={26} c={S.blue} opacity={0.25} />
      <Seams x={16} y={30} w={96} h={32} cols={5} op={0.2} /><Rivets x={20} y={64} w={88} n={9} c="brass" />
      <Grime x={8} y={20} w={112} h={64} seed={831} n={4} op={0.14} /><Scuff x={12} y={24} w={104} h={56} seed={832} n={6} c="fabric" />
    </svg>
  );
}

function PropNewsColumn() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={92} rx={24} ry={3} />
      <Slab x={10} y={80} w={44} h={10} k={2} c="sand" /><Slab x={14} y={74} w={36} h={8} k={1.6} c="sand" o={0.9} />
      <Bev t="polygon" points={pts([[16, 74], [16, 14], [48, 14], [48, 74]])} c="sand" o={1.4} />
      {[0, 1, 2, 3, 4].map((i) => <line key={i} x1={20 + i * 6} y1="14" x2={20 + i * 6} y2="74" stroke={ART.sand.shade} strokeWidth="1" opacity="0.7" />)}
      <Slab x={12} y={8} w={40} h={10} k={2} c="brass" /><Slab x={14} y={20} w={36} h={4} k={1} c="dark" o={0.8} />
      <rect x="18" y="28" width="28" height="34" fill={ART.screen.amber} opacity="0.95" />
      <g style={{ animation: 'holo-flicker 2.4s ease-in-out infinite' }}>
        {[['SENATE VOTE 61 TO 38', 34], ['CZERKA +2.1  KDY -0.4', 41], ['RENDILI  +0.9', 48], ['PIRACY RISING', 55]].map(([t, y], i) => <text key={i} x="20" y={y} fontSize="3" fill={i === 3 ? S.redHi : S.amberHi} fontWeight="700" {...PT}>{t}</text>)}
        <rect x="19" y="30" width="26" height="1.2" fill={S.amber} opacity="0.7" /><rect x="19" y="59" width="26" height="1.2" fill={S.amber} opacity="0.5" />
      </g>
      <Glow cx={32} cy={46} rx={22} ry={22} c={S.amber} opacity={0.25} />
      <Rivets x={16} y={76} w={32} n={5} c="brass" /><Vent x={24} y={84} w={16} h={4} n={2} />
      <Grime x={10} y={14} w={44} h={74} seed={841} n={3} op={0.14} /><Scuff x={14} y={18} w={36} h={60} seed={842} n={4} c="sand" />
    </svg>
  );
}

function PropObsidianBar() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 192 64" width="192" height="64" style={PROP_STYLE}>
      <PropShadow cx={96} cy={60} rx={90} ry={3} />
      {/* back bar shelves */}
      <Slab x={10} y={2} w={172} h={22} k={3} c="wood" o={1} /><Seams x={10} y={2} w={172} h={22} cols={9} op={0.3} />
      {Array.from({ length: 16 }, (_, i) => <g key={i}><polygon points={pts([[16 + i * 10.4, 22], [18 + i * 10.4, 10], [21 + i * 10.4, 10], [23 + i * 10.4, 22]])} fill={[ART.glass.base, ART.amber.base, ART.red.hi, ART.teal.hi][i % 4]} opacity="0.9" /><rect x={18 + i * 10.4} y="7" width="3" height="3" fill={ART.brass.hi} /></g>)}
      <rect x="10" y="12" width="172" height="1.4" fill={ART.brass.base} />
      {/* obsidian slab with electrum river */}
      <Bev t="polygon" points={pts([[2, 34], [8, 26], [184, 26], [190, 34], [190, 52], [184, 58], [8, 58], [2, 52]])} c="dark" o={1.6} />
      <path d="M12 40 C40 32, 60 50, 96 42 S150 34, 180 46" fill="none" stroke={ART.brass.hi} strokeWidth="2.6" /><path d="M12 40 C40 32, 60 50, 96 42 S150 34, 180 46" fill="none" stroke={S.cyanHi} strokeWidth="0.9" opacity="0.8" style={{ animation: 'holo-flicker 3s ease-in-out infinite' }} />
      <path d="M12 48 C50 44, 70 54, 110 50 S160 44, 180 52" fill="none" stroke={ART.brass.base} strokeWidth="1.4" opacity="0.8" />
      <Glow cx={96} cy={44} rx={80} ry={10} c={S.cyan} opacity={0.22} />
      {[28, 60, 120, 152].map((x, i) => <g key={i}><polygon points={pts([[x, 38], [x + 5, 38], [x + 4, 31], [x + 1, 31]])} fill={ART.glass.hi} opacity="0.8" /><circle cx={x + 2.5} cy={39} r="1" fill={[S.amber, S.cyan, S.magenta, S.amber][i]} /></g>)}
      <Slab x={8} y={52} w={176} h={8} k={1.4} c="brass" o={0.9} /><Rivets x={14} y={56} w={164} n={20} c="brass" />
      <Vent x={86} y={14} w={20} h={6} n={2} />
      <Grime x={2} y={26} w={188} h={34} seed={851} n={5} op={0.16} /><Scuff x={6} y={28} w={180} h={30} seed={852} n={7} c="dark" />
    </svg>
  );
}

function PropHoloFountain() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={90} rx={40} ry={4} />
      <Slab x={6} y={76} w={84} h={12} k={3} c="stone" o={1.2} /><Seams x={6} y={76} w={84} h={12} cols={8} op={0.3} />
      <polygon points={pts([[12, 78], [84, 78], [78, 70], [18, 70]])} fill={ART.teal.shade} />
      <Bev t="polygon" points={pts([[20, 72], [20, 8], [76, 8], [76, 72]])} c="stone" o={1.4} />
      <rect x="26" y="14" width="44" height="54" fill={ART.screen.cyan} />
      {Array.from({ length: 14 }, (_, i) => <rect key={i} x={28 + i * 3} y="14" width="1.6" height="54" fill={S.cyanHi} opacity={0.2 + (i % 3) * 0.1} style={{ animation: `prop-scan ${1.6 + (i % 4) * 0.3}s linear ${i * 0.1}s infinite` }} />)}
      <g style={{ animation: 'holo-flicker 3.4s ease-in-out infinite' }}>
        {['PEACE', 'ORDER', 'PROSPERITY'].map((t, i) => <text key={i} x="48" y={28 + i * 16} textAnchor="middle" fontSize="7" fill={S.cyanHi} fontWeight="700" letterSpacing="1" {...PT}>{t}</text>)}
        {[34, 50, 66].map((y) => <text key={y} x="48" y={y} textAnchor="middle" fontSize="3.2" fill={S.amberHi} opacity="0.9" {...PT}>aurek besh cresh</text>)}
      </g>
      <Slab x={24} y={4} w={48} h={6} k={1.4} c="brass" /><Slab x={26} y={66} w={44} h={6} k={1.4} c="dark" o={0.8} />
      <Glow cx={48} cy={74} rx={34} ry={7} c={S.cyan} opacity={0.4} />
      {[34, 48, 62].map((x, i) => <circle key={i} cx={x} cy={76} r="1.6" fill={S.white} opacity="0.7" style={{ animation: `ship-blink ${1.4 + i * 0.4}s ease-in-out infinite` }} />)}
      <Rivets x={24} y={84} w={48} n={7} c="brass" /><Grime x={8} y={8} w={80} h={80} seed={861} n={4} op={0.14} /><Scuff x={12} y={12} w={72} h={72} seed={862} n={5} c="stone" />
    </svg>
  );
}

function PropDecanterPedestal() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={24} ry={3} />
      {[14, 40].map((x, i) => (
        <g key={i}>
          <Slab x={x} y={46} w={14} h={12} k={1.6} c="dark" o={1} /><Slab x={x - 2} y={54} w={18} h={5} k={1} c="brass" o={0.8} />
          <Bev t="polygon" points={pts([[x + 1, 46], [x + 3, 22], [x + 11, 22], [x + 13, 46]])} c="glass" o={0.8} />
          <rect x={x + 2.4} y={32 + i * 4} width="9.2" height={14 - i * 4} fill={[ART.amber.base, ART.red.base][i]} opacity="0.85" />
          <polygon points={pts([[x + 5, 22], [x + 5.6, 14], [x + 8.4, 14], [x + 9, 22]])} fill={ART.glass.hi} opacity="0.8" /><rect x={x + 4} y="11" width="6" height="4" fill={ART.brass.hi} />
          <rect x={x + 3} y="48" width="8" height="5" fill={ART.screen.cyan} /><circle cx={x + 7} cy="50.4" r="1.4" fill={i ? S.red : S.green} style={{ animation: `ship-blink ${2 + i}s steps(2) infinite` }} />
        </g>
      ))}
      <Rivets x={14} y={57} w={36} n={6} c="brass" /><Grime x={10} y={12} w={44} h={50} seed={871} n={3} op={0.14} /><Scuff x={12} y={16} w={40} h={44} seed={872} n={4} c="glass" />
    </svg>
  );
}

function PropTerrarium() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <ellipse cx="32" cy="58" rx="16" ry="3" fill={ART.ink} opacity="0.35" />
      <g style={{ animation: 'drift 5s ease-in-out infinite alternate' }}>
        <Bev t="polygon" points={pts(ngon(32, 30, 22, 22, 18))} c="glass" o={0.8} />
        <polygon points={pts(ngon(32, 32, 18, 18, 18))} fill={ART.screen.green} opacity="0.7" />
        {[[26, 38, 18], [34, 38, 22], [30, 38, 14], [38, 38, 12]].map(([x, y, h], i) => <g key={i}><path d={`M${x} ${y} C${x - 4} ${y - h / 2}, ${x + 3} ${y - h * 0.8}, ${x + (i % 2 ? 6 : -6)} ${y - h}`} fill="none" stroke={ART.moss.hi} strokeWidth="1.8" /><circle cx={x + (i % 2 ? 6 : -6)} cy={y - h} r="2.4" fill={[S.violet, S.magenta, S.lime, S.cyanHi][i]} /></g>)}
        <polygon points={pts([[22, 40], [42, 40], [38, 48], [26, 48]])} fill={ART.moss.shade} />
        <polygon points={pts([[20, 18], [30, 14], [24, 26]])} fill={S.white} opacity="0.35" />
        <Slab x={24} y={46} w={16} h={5} k={1.2} c="brass" /><Glow cx={32} cy={34} r={16} c={S.lime} opacity={0.22} style={{ animation: 'holo-flicker 4s ease-in-out infinite' }} />
      </g>
      <Rivets x={22} y={56} w={20} n={4} c="brass" /><Grime x={12} y={10} w={40} h={44} seed={881} n={2} op={0.1} /><Scuff x={14} y={14} w={36} h={36} seed={882} n={3} c="glass" />
    </svg>
  );
}

function PropBrassRegister() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <Slab x={6} y={26} w={52} h={30} k={3} c="dark" o={1} />
      <Bev t="polygon" points={pts([[10, 30], [54, 30], [52, 52], [12, 52]])} c="brass" o={1.2} />
      {Array.from({ length: 7 }, (_, i) => <rect key={i} x="14" y={33 + i * 2.8} width="36" height="1.4" fill={ART.ink} opacity="0.85" />)}
      {[18, 30, 42].map((x, i) => <g key={i}><ellipse cx={x} cy="24" rx="5" ry="2.4" fill={S.white} opacity="0.12" style={{ animation: `steam-rise ${2.2 + i * 0.5}s ease-out ${i * 0.4}s infinite` }} /><ellipse cx={x + 1} cy="16" rx="3.6" ry="1.8" fill={S.amberHi} opacity="0.1" style={{ animation: `steam-rise ${3 + i * 0.4}s ease-out ${i * 0.7}s infinite` }} /></g>)}
      <Rivets x={10} y={54} w={44} n={6} c="brass" />
      <Grime x={6} y={26} w={52} h={30} seed={891} n={3} op={0.2} /><Scuff x={8} y={28} w={48} h={26} seed={892} n={4} c="brass" />
    </svg>
  );
}

function PropMacroBinocular() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={22} ry={3} />
      <Slab x={20} y={46} w={24} h={12} k={2} c="brass" o={1} /><Bev t="polygon" points={pts([[28, 46], [28, 22], [36, 22], [36, 46]])} c="brass" o={0.9} />
      <Bev t="polygon" points={pts([[10, 24], [16, 12], [48, 12], [54, 24], [48, 28], [16, 28]])} c="dark" o={1.2} />
      {[20, 38].map((x, i) => <g key={i}><Bev t="polygon" points={pts(ngon(x + 3, 20, 7, 7, 10))} c="steel" o={0.8} /><circle cx={x + 3} cy="20" r="4.6" fill={ART.deepglass.shade} /><circle cx={x + 3} cy="20" r="3" fill={ART.glass.base} opacity="0.7" /><circle cx={x + 1.6} cy="18.6" r="1" fill={S.white} opacity="0.7" /></g>)}
      <rect x="12" y="25" width="40" height="2" fill={S.cyan} opacity="0.7" style={{ animation: 'ship-blink 2.4s ease-in-out infinite' }} />
      <Slab x={22} y={48} w={20} h={6} k={1} c="dark" o={0.9} /><text x="32" y="52.6" textAnchor="middle" fontSize="3" fill={S.cyanHi} fontWeight="700" {...PT}>SCAN</text>
      <Rivets x={22} y={56} w={20} n={4} c="brass" /><Grime x={10} y={12} w={44} h={46} seed={901} n={3} op={0.16} /><Scuff x={12} y={14} w={40} h={40} seed={902} n={4} c="brass" />
    </svg>
  );
}

function PropGlassOverhang() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <Bev t="polygon" points={pts([[6, 12], [122, 12], [124, 54], [4, 54]])} c="deepglass" o={1} />
      <polygon points={pts([[10, 16], [118, 16], [120, 50], [8, 50]])} fill={ART.screen.space} />
      {Array.from({ length: 9 }, (_, i) => <line key={i} x1={14 + i * 12.4} y1="16" x2={14 + i * 12.4} y2="50" stroke={S.cyan} strokeWidth="0.5" opacity="0.4" />)}
      {Array.from({ length: 3 }, (_, i) => <line key={i} x1="10" y1={26 + i * 10} x2="118" y2={26 + i * 10} stroke={S.cyan} strokeWidth="0.5" opacity="0.4" />)}
      {Array.from({ length: 10 }, (_, i) => <rect key={i} x={16 + i * 11} y={20 + (i % 3) * 10} width={5 + (i % 3) * 3} height="1.6" fill={[S.magenta, S.cyan, S.amber][i % 3]} opacity="0.7" />)}
      <polygon points={pts([[26, 14], [44, 14], [34, 50], [16, 50]])} fill={S.white} opacity="0.16" />
      <rect x="6" y="12" width="116" height="2.4" fill={ART.brass.base} /><rect x="4" y="52" width="120" height="2.4" fill={ART.brass.base} />
      <Slab x={44} y={2} w={40} h={8} k={1.4} c="dark" o={0.9} /><text x="64" y="8" textAnchor="middle" fontSize="4" fill={S.amberHi} fontWeight="700" {...PT}>DEPTH 5000 M</text>
      <Rivets x={10} y={56} w={108} n={13} c="brass" /><Seams x={6} y={12} w={116} h={42} cols={5} op={0.2} />
      <Grime x={4} y={12} w={120} h={44} seed={911} n={4} op={0.12} /><Scuff x={6} y={14} w={116} h={38} seed={912} n={5} c="glass" />
    </svg>
  );
}

function PropTintConsole() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={24} ry={3} />
      <Slab x={20} y={44} w={24} h={13} k={1.4} c="brass" /><Slab x={12} y={54} w={40} h={5} k={1.2} c="dark" o={0.9} />
      <Bev t="polygon" points={pts([[8, 6], [56, 6], [60, 10], [60, 42], [56, 46], [8, 46], [4, 42], [4, 10]])} c="dark" o={1.2} />
      <rect x="9" y="10" width="46" height="24" fill={ART.screen.cyan} />
      <text x="32" y="16" textAnchor="middle" fontSize="3.2" fill={S.cyanHi} fontWeight="700" {...PT}>VIEWPORT TINT</text>
      <rect x="14" y="20" width="36" height="4" fill={ART.dark.base} /><rect x="14" y="20" width="22" height="4" fill={S.cyan} opacity="0.8" /><rect x="35" y="19" width="3" height="6" fill={S.white} />
      <text x="14" y="30" fontSize="2.6" fill={S.amberHi} {...PT}>CLEAR</text><text x="40" y="30" fontSize="2.6" fill={S.amberHi} {...PT}>OBSIDIAN</text>
      <circle cx="16" cy="40" r="3" fill={ART.brass.hi} /><circle cx="26" cy="40" r="3" fill={ART.brass.base} /><Lights x={38} y={40} n={4} gap={4.4} seed={9} />
      <Rivets x={8} y={8} w={48} n={7} c="brass" /><Seams x={4} y={6} w={56} h={40} cols={2} op={0.2} />
      <Grime x={4} y={6} w={56} h={52} seed={921} n={3} op={0.16} /><Scuff x={8} y={10} w={48} h={34} seed={922} n={4} c="dark" />
    </svg>
  );
}
// ===== SENATE LOUNGE PROPS END =====
// ===== PENTHOUSE PROPS BEGIN =====
function PropObservatoryTelescope() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={90} rx={34} ry={4} />
      {[[26, 90], [48, 94], [70, 90]].map(([x, y], i) => <polygon key={i} points={pts([[48, 56], [x - 2, y], [x + 2, y], [49, 56]])} fill={ART.brass.shade} />)}
      <Bev t="polygon" points={pts(ngon(48, 88, 26, 5, 10))} c="dark" o={0.8} />
      <Bev t="polygon" points={pts([[38, 60], [38, 46], [58, 46], [58, 60]])} c="brass" o={1} />
      <Slab x={36} y={56} w={24} h={8} k={1.6} c="dark" /><Lights x={40} y={60} n={4} gap={5} seed={3} />
      <g transform="rotate(-38 48 46)">
        <Bev t="polygon" points={pts([[16, 38], [16, 54], [80, 52], [80, 40]])} c="pearl" o={1.2} />
        <Bev t="polygon" points={pts([[8, 36], [8, 56], [20, 54], [20, 38]])} c="brass" o={1} />
        <polygon points={pts([[10, 38], [10, 54], [18, 52], [18, 40]])} fill={ART.deepglass.base} /><polygon points={pts([[11, 40], [15, 40], [13, 46]])} fill={S.kyber} opacity="0.7" />
        <rect x="30" y="38" width="3" height="16" fill={ART.brass.base} /><rect x="52" y="38" width="3" height="16" fill={ART.brass.base} />
        <Bev t="polygon" points={pts([[78, 42], [92, 44], [92, 50], [78, 50]])} c="dark" o={0.9} /><circle cx="90" cy="47" r="2" fill={S.cyanHi} opacity="0.8" />
      </g>
      <Bev t="polygon" points={pts(ngon(72, 70, 12, 12, 16))} c="deepglass" o={0.9} />
      <polygon points={pts(ngon(72, 70, 9, 9, 16))} fill={ART.screen.space} />
      {[[68, 66], [74, 72], [70, 74], [77, 65], [66, 72]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="0.9" fill={S.kyber} style={{ animation: `ship-blink ${1.6 + i * 0.3}s ease-in-out infinite` }} />)}
      <polyline points={pts([[64, 70], [70, 66], [76, 72]])} fill="none" stroke={S.cyan} strokeWidth="0.6" opacity="0.8" />
      <Rivets x={40} y={63} w={20} n={4} c="brass" /><Seams x={36} y={46} w={24} h={18} rows={1} op={0.3} />
      <Grime x={10} y={30} w={76} h={60} seed={931} n={3} op={0.14} /><Scuff x={14} y={34} w={68} h={52} seed={932} n={4} c="brass" />
    </svg>
  );
}

function PropGardenPlanter() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3} />
      <Bev t="polygon" points={pts([[6, 56], [6, 34], [90, 34], [90, 56]])} c="sand" o={1.4} />
      <Seams x={6} y={34} w={84} h={22} cols={5} op={0.3} /><Slab x={4} y={30} w={88} h={7} k={1.6} c="sand" o={0.9} />
      <rect x="10" y="36" width="76" height="5" fill={ART.moss.shade} />
      {[[16, 40], [28, 40], [40, 40], [54, 40], [66, 40], [78, 40]].map(([x, y], i) => (
        <g key={i}>
          <path d={`M${x} ${y} C${x - 6} ${y - 10}, ${x + 5} ${y - 18}, ${x + (i % 2 ? 9 : -9)} ${y - 24}`} fill="none" stroke={ART.moss.hi} strokeWidth="2.4" />
          <path d={`M${x} ${y} C${x + 5} ${y - 8}, ${x - 4} ${y - 14}, ${x + (i % 2 ? -6 : 6)} ${y - 18}`} fill="none" stroke={ART.moss.base} strokeWidth="2" />
          <circle cx={x + (i % 2 ? 9 : -9)} cy={y - 25} r="3" fill={[S.violet, S.magenta, S.lime, S.cyanHi, S.amberHi, S.violet][i]} />
          <Glow cx={x + (i % 2 ? 9 : -9)} cy={y - 25} r={7} c={[S.violet, S.magenta, S.lime, S.cyan, S.amber, S.violet][i]} opacity={0.35} style={{ animation: `holo-flicker ${3 + i * 0.4}s ease-in-out infinite` }} />
        </g>
      ))}
      <rect x="10" y="50" width="76" height="2" fill={ART.brass.base} /><Lights x={14} y={53} n={8} gap={10} seed={4} />
      <Rivets x={10} y={32} w={76} n={10} c="brass" /><Grime x={6} y={34} w={84} h={24} seed={941} n={3} op={0.16} /><Scuff x={8} y={34} w={80} h={20} seed={942} n={4} c="sand" />
    </svg>
  );
}

function PropGalleryPedestal() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={20} ry={3} />
      <polygon points={pts([[14, 6], [50, 6], [58, 56], [6, 56]])} fill={S.amberHi} opacity="0.08" />
      <Bev t="polygon" points={pts([[16, 58], [16, 38], [48, 38], [48, 58]])} c="dark" o={1.4} /><Slab x={12} y={54} w={40} h={6} k={1.4} c="brass" o={0.8} />
      <rect x="16" y="38" width="32" height="2.4" fill={ART.brass.hi} />
      <path d="M32 36 C20 34, 22 24, 32 24 S44 14, 32 12 C26 12, 24 18, 30 20" fill="none" stroke={ART.brass.hi} strokeWidth="4" />
      <path d="M32 36 C20 34, 22 24, 32 24 S44 14, 32 12 C26 12, 24 18, 30 20" fill="none" stroke={ART.brass.base} strokeWidth="1.4" />
      <circle cx="30" cy="20" r="2.4" fill={S.amberHi} /><Glow cx={32} cy={24} r={16} c={S.amberHi} opacity={0.25} />
      <Slab x={22} y={44} w={20} h={7} k={1} c="brass" o={0.5} /><text x="32" y="49.4" textAnchor="middle" fontSize="2.8" fill={ART.ink} fontWeight="700" {...PT}>ASCENT I</text>
      <Rivets x={18} y={57} w={28} n={5} c="brass" /><Grime x={10} y={30} w={44} h={30} seed={951} n={2} op={0.14} /><Scuff x={14} y={34} w={36} h={22} seed={952} n={3} c="dark" />
    </svg>
  );
}

function PropWallArt({ variant }) {
  const S = ART.signal, K = ART.sky;
  const sky = variant === 'skyline';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={42} ry={2.4} />
      <Bev t="polygon" points={pts([[4, 4], [92, 4], [92, 56], [4, 56]])} c="brass" o={1.4} />
      <rect x="9" y="9" width="78" height="42" fill={ART.dark.shade} />
      {sky ? (
        <g>
          {[[9, 20, K.indigo], [29, 12, K.violet], [41, 10, K.plum], [51, 8, K.rose], [59, 8, K.ember], [67, 8, K.amber]].map(([y, h, c], i) => <rect key={i} x="9" y={y} width="78" height={h + 1} fill={c} />)}
          {[[12, 20, 30], [20, 14, 38], [30, 22, 28], [40, 12, 40], [50, 18, 32], [60, 10, 42], [68, 16, 34], [76, 12, 38]].map(([x, w, h], i) => <rect key={i} x={x} y={51 - h} width={w * 0.5} height={h} fill={K.spire} />)}
          <circle cx="66" cy="42" r="8" fill={S.amberHi} opacity="0.8" />
          {Array.from({ length: 16 }, (_, i) => <rect key={i} x={13 + i * 5} y={34 + (i % 4) * 4} width="1.6" height="1.6" fill={S.amberHi} opacity="0.8" />)}
          <rect x="9" y="46" width="78" height="1.4" fill={S.orange} opacity="0.8" /><rect x="9" y="48" width="78" height="1.2" fill={S.kyber} opacity="0.7" />
        </g>
      ) : (
        <g>
          <polygon points={pts([[9, 51], [30, 9], [44, 51]])} fill={ART.red.base} /><polygon points={pts([[30, 51], [56, 9], [70, 51]])} fill={ART.brass.base} />
          <polygon points={pts([[56, 51], [78, 9], [87, 9], [87, 51]])} fill={ART.deepglass.base} /><circle cx="48" cy="28" r="10" fill={ART.brass.hi} opacity="0.9" /><circle cx="48" cy="28" r="5" fill={ART.red.shade} />
          <polyline points={pts([[9, 40], [30, 30], [50, 44], [70, 24], [87, 34]])} fill="none" stroke={ART.pearl.hi} strokeWidth="1" opacity="0.8" />
        </g>
      )}
      <rect x="9" y="9" width="78" height="42" fill="none" stroke={ART.brass.hi} strokeWidth="0.8" opacity="0.8" />
      <Slab x={36} y={54} w={24} h={6} k={1} c="brass" o={0.8} /><text x="48" y="58.6" textAnchor="middle" fontSize="2.8" fill={ART.ink} fontWeight="700" {...PT}>{sky ? 'DUSK OVER 5120' : 'COMPOSITION IN GARNET'}</text>
      <Rivets x={8} y={6} w={80} n={9} c="brass" /><Grime x={4} y={4} w={88} h={54} seed={961} n={3} op={0.12} /><Scuff x={6} y={6} w={84} h={48} seed={962} n={4} c="brass" />
    </svg>
  );
}

function PropHoloTheater() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={92} rx={58} ry={3} />
      <Slab x={8} y={6} w={112} h={46} k={3} c="dark" o={1.2} />
      <rect x="14" y="12" width="100" height="34" fill={ART.screen.cyan} />
      <g style={{ animation: 'holo-flicker 2.8s ease-in-out infinite' }}>
        <polygon points={pts([[14, 46], [114, 46], [114, 34], [90, 26], [70, 36], [50, 22], [30, 34], [14, 30]])} fill={S.blue} opacity="0.45" />
        <circle cx="96" cy="22" r="7" fill={S.amberHi} opacity="0.85" />
        <polygon points={pts([[44, 46], [44, 30], [52, 30], [52, 46]])} fill={S.cyanHi} opacity="0.6" /><polygon points={pts([[62, 46], [62, 26], [72, 26], [72, 46]])} fill={S.cyan} opacity="0.5" />
        <rect x="14" y="12" width="100" height="1.4" fill={S.cyanHi} opacity="0.7" />
        <text x="20" y="20" fontSize="4.4" fill={S.amberHi} fontWeight="700" {...PT}>NOW SHOWING</text>
      </g>
      <Glow cx={64} cy={30} rx={50} ry={14} c={S.cyan} opacity={0.3} />
      {[10, 112].map((x) => <Bev key={x} t="polygon" points={pts([[x, 52], [x, 12], [x + 6, 12], [x + 6, 52]])} c="brass" o={0.8} />)}
      <Bev t="polygon" points={pts([[14, 82], [14, 62], [114, 62], [114, 82], [102, 90], [26, 90]])} c="red" o={1.4} />
      {[0, 1, 2, 3, 4, 5].map((i) => <ellipse key={i} cx={28 + i * 14.4} cy={72} rx="6" ry="3.4" fill={ART.red.hi} opacity="0.5" />)}
      <Slab x={46} y={56} w={36} h={7} k={1.2} c="dark" o={0.9} /><Lights x={50} y={60} n={5} gap={6} seed={6} />
      <Rivets x={16} y={86} w={96} n={12} c="brass" /><Seams x={14} y={62} w={100} h={28} cols={5} op={0.2} />
      <Grime x={8} y={8} w={112} h={84} seed={971} n={4} op={0.14} /><Scuff x={12} y={12} w={104} h={76} seed={972} n={5} c="dark" />
    </svg>
  );
}

function PropLoungeRug() {
  return (
    <svg viewBox="0 0 160 96" width="160" height="96" style={PROP_STYLE}>
      <Bev t="polygon" points={pts([[6, 10], [154, 10], [154, 86], [6, 86]])} c="red" o={1} />
      <rect x="14" y="18" width="132" height="60" fill={ART.red.shade} /><rect x="14" y="18" width="132" height="60" fill="none" stroke={ART.brass.hi} strokeWidth="1.6" />
      <rect x="22" y="26" width="116" height="44" fill="none" stroke={ART.brass.base} strokeWidth="1" />
      {Array.from({ length: 11 }, (_, i) => <polygon key={i} points={pts([[22 + i * 11.6, 26], [28 + i * 11.6, 32], [22 + i * 11.6, 38], [16 + i * 11.6, 32]])} fill={ART.brass.base} opacity="0.85" transform="translate(6 0)" />)}
      <polygon points={pts([[80, 30], [112, 48], [80, 66], [48, 48]])} fill={ART.brass.base} opacity="0.5" />
      <polygon points={pts([[80, 36], [100, 48], [80, 60], [60, 48]])} fill={ART.red.base} /><circle cx="80" cy="48" r="5" fill={ART.brass.hi} />
      {Array.from({ length: 24 }, (_, i) => <rect key={i} x={6 + i * 6.2} y="86" width="1.4" height="6" fill={ART.brass.hi} opacity="0.7" />)}
      {Array.from({ length: 24 }, (_, i) => <rect key={i} x={6 + i * 6.2} y="4" width="1.4" height="6" fill={ART.brass.hi} opacity="0.7" />)}
      <Seams x={14} y={18} w={132} h={60} cols={7} rows={3} op={0.12} /><Grime x={6} y={10} w={148} h={76} seed={981} n={4} op={0.1} /><Scuff x={8} y={12} w={144} h={72} seed={982} n={6} c="red" />
    </svg>
  );
}

function PropSoakPool() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={92} rx={58} ry={3} />
      <Bev t="polygon" points={pts([[6, 90], [6, 26], [122, 26], [122, 90]])} c="stone" o={1.4} /><Seams x={6} y={26} w={116} h={64} cols={6} rows={3} op={0.22} />
      <polygon points={pts([[16, 80], [16, 36], [112, 36], [112, 80]])} fill={ART.teal.shade} />
      <polygon points={pts([[20, 76], [20, 40], [108, 40], [108, 76]])} fill={ART.teal.base} />
      {[[30, 50, 24], [60, 58, 30], [86, 48, 20], [46, 68, 26], [92, 66, 18]].map(([x, y, w], i) => <ellipse key={i} cx={x} cy={y} rx={w * 0.5} ry="2" fill={S.cyanHi} opacity="0.22" style={{ animation: `holo-flicker ${2.6 + i * 0.5}s ease-in-out infinite` }} />)}
      {[34, 62, 90].map((x, i) => <g key={i}><ellipse cx={x} cy="30" rx="8" ry="3" fill={S.white} opacity="0.14" style={{ animation: `steam-rise ${2.4 + i * 0.5}s ease-out ${i * 0.5}s infinite` }} /><ellipse cx={x + 3} cy="22" rx="6" ry="2.4" fill={S.white} opacity="0.1" style={{ animation: `steam-rise ${3.2 + i * 0.4}s ease-out ${i * 0.9}s infinite` }} /></g>)}
      <Slab x={8} y={28} w={112} h={7} k={1.4} c="brass" o={0.8} />
      {[24, 104].map((x) => <g key={x}><Bev t="polygon" points={pts([[x - 4, 44], [x + 4, 44], [x + 4, 54], [x - 4, 54]])} c="brass" o={0.8} /><circle cx={x} cy="49" r="1.6" fill={ART.ink} /></g>)}
      <polygon points={pts([[100, 84], [118, 84], [118, 92], [100, 92]])} fill={ART.pearl.base} /><polygon points={pts([[102, 86], [116, 86], [116, 90], [102, 90]])} fill={ART.pearl.hi} />
      <Rivets x={10} y={88} w={108} n={13} c="brass" /><Grime x={6} y={26} w={116} h={66} seed={991} n={3} op={0.14} /><Scuff x={8} y={28} w={112} h={60} seed={992} n={4} c="stone" />
    </svg>
  );
}

function PropDesignConsole() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={59} rx={24} ry={3} />
      <Slab x={20} y={44} w={24} h={13} k={1.4} c="brass" /><Slab x={12} y={54} w={40} h={5} k={1.2} c="dark" o={0.9} />
      <Bev t="polygon" points={pts([[8, 6], [56, 6], [60, 10], [60, 42], [56, 46], [8, 46], [4, 42], [4, 10]])} c="dark" o={1.2} />
      <rect x="9" y="10" width="46" height="24" fill={ART.screen.cyan} />
      <g style={{ animation: 'holo-flicker 3s ease-in-out infinite' }}>
        <polygon points={pts([[14, 30], [32, 14], [50, 30]])} fill="none" stroke={S.cyanHi} strokeWidth="0.8" /><rect x="22" y="22" width="20" height="8" fill="none" stroke={S.cyan} strokeWidth="0.7" />
        <line x1="32" y1="14" x2="32" y2="30" stroke={S.cyan} strokeWidth="0.5" opacity="0.7" /><rect x="29" y="25" width="6" height="5" fill={S.amberHi} opacity="0.7" />
      </g>
      <text x="32" y="17" textAnchor="middle" fontSize="2.8" fill={S.cyanHi} fontWeight="700" {...PT}>INTERIOR DESIGN</text>
      {[ART.red.base, ART.brass.base, ART.deepglass.base, ART.sand.base, ART.moss.base].map((c, i) => <rect key={i} x={10 + i * 8.4} y="37" width="6" height="6" fill={c} />)}
      <Rivets x={8} y={8} w={48} n={7} c="brass" /><Seams x={4} y={6} w={56} h={40} cols={2} op={0.2} />
      <Grime x={4} y={6} w={56} h={52} seed={1001} n={3} op={0.16} /><Scuff x={8} y={10} w={48} h={34} seed={1002} n={4} c="dark" />
    </svg>
  );
}
// ===== PENTHOUSE PROPS END =====
// ===== BADOR WAR PROPS BEGIN =====
// Wide basalt, wall and furnace backdrops for the Bador surface zones. One 24 by 5 tile strip, tiled across the zone top.
function PropBasaltVista({ variant = 'ravine_a' }) {
  const S = ART.signal;
  const fam = variant.split('_')[0];
  const seed = variant.endsWith('b') ? 7 : variant.endsWith('c') ? 13 : 3;
  const r = rng(seed);
  const W = 768;
  const bands = fam === 'ravine' ? [[0, 40, ART.ink], [40, 80, ART.dark.shade], [80, 112, ART.teal.shade], [112, 160, ART.teal.base]]
    : fam === 'outskirts' ? [[0, 36, ART.ink], [36, 72, ART.dark.shade], [72, 104, ART.bronze.shade], [104, 160, ART.rust.shade]]
      : [[0, 34, ART.ink], [34, 70, ART.dark.shade], [70, 100, ART.rust.shade], [100, 160, ART.copper.shade]];
  const spires = Array.from({ length: fam === 'ravine' ? 15 : 8 }, () => ({ x: r() * W, w: 14 + r() * 34, h: 38 + r() * 92, lean: (r() - 0.5) * 8 }));
  const stars = Array.from({ length: 40 }, () => ({ x: r() * W, y: r() * 52, s: 0.5 + r() * 0.8, d: r() * 3 }));
  return (
    <svg viewBox="0 0 768 160" width="768" height="160" style={PROP_STYLE}>
      {bands.map(([y0, y1, c], i) => <rect key={i} x="0" y={y0} width={W} height={y1 - y0 + 1} fill={c} />)}
      {fam !== 'wastes' && stars.map((st, i) => <circle key={i} cx={st.x} cy={st.y} r={st.s} fill={S.kyber} opacity="0.6" style={{ animation: `ship-blink ${2 + st.d}s ease-in-out ${st.d}s infinite` }} />)}
      {fam === 'ravine' && (
        <g>
          <circle cx="560" cy="44" r="26" fill={ART.pearl.base} opacity="0.9" /><circle cx="574" cy="38" r="24" fill={ART.dark.shade} />
          <circle cx="552" cy="48" r="2.4" fill={ART.pearl.shade} opacity="0.5" />
          {spires.map((p, i) => (
            <g key={i}>
              <polygon points={pts([[p.x - p.w / 2, 140], [p.x - p.w * 0.28 + p.lean, 140 - p.h], [p.x + p.w * 0.1 + p.lean, 140 - p.h - 12], [p.x + p.w * 0.34 + p.lean, 140 - p.h * 0.7], [p.x + p.w / 2, 140]])} fill={ART.dark.shade} />
              <polygon points={pts([[p.x + p.w * 0.1 + p.lean, 140 - p.h - 12], [p.x + p.w * 0.34 + p.lean, 140 - p.h * 0.7], [p.x + p.w / 2, 140], [p.x + p.w * 0.1, 140]])} fill={ART.dark.base} />
              <polyline points={pts([[p.x - p.w * 0.28 + p.lean, 140 - p.h], [p.x - p.w / 2, 140]])} fill="none" stroke={ART.teal.hi} strokeWidth="0.8" opacity="0.5" />
            </g>
          ))}
          {[[60, 96, 220], [330, 110, 260], [560, 92, 180]].map(([x, y, w], i) => <ellipse key={i} cx={x} cy={y} rx={w / 2} ry="9" fill={S.kyber} opacity="0.1" style={{ animation: `steam-rise ${6 + i * 1.5}s ease-out ${i}s infinite` }} />)}
        </g>
      )}
      {fam === 'outskirts' && (
        <g>
          <circle cx="620" cy="118" r="96" fill={ART.teal.base} opacity="0.4" /><circle cx="618" cy="120" r="92" fill={ART.copper.shade} /><circle cx="590" cy="100" r="60" fill={ART.bronze.shade} opacity="0.8" />
          <polygon points={pts([[560, 70], [610, 58], [650, 72], [700, 90], [640, 96]])} fill={ART.rust.shade} opacity="0.7" />
          {[0, 1, 2, 3, 4, 5].map((i) => <rect key={i} x={20 + i * 128} y={92 - (i % 2) * 14} width={112} height={68 + (i % 2) * 14} fill={ART.hull.shade} />)}
          {[0, 1, 2, 3, 4, 5].map((i) => <rect key={'c' + i} x={20 + i * 128} y={92 - (i % 2) * 14} width={112} height="5" fill={ART.hull.base} />)}
          {[0, 1, 2, 3, 4, 5].map((i) => <polygon key={'t' + i} points={pts([[40 + i * 128, 92 - (i % 2) * 14], [48 + i * 128, 66 - (i % 2) * 14], [88 + i * 128, 66 - (i % 2) * 14], [96 + i * 128, 92 - (i % 2) * 14]])} fill={ART.hull.base} />)}
          {Array.from({ length: 24 }, (_, i) => <rect key={'w' + i} x={30 + i * 32} y={108 + (i % 3) * 10} width="5" height="2.4" fill={S.cyan} opacity="0.6" />)}
          <polygon points={pts([[436, 160], [448, 96], [512, 96], [524, 160]])} fill={ART.ink} /><polygon points={pts([[452, 160], [460, 118], [500, 118], [508, 160]])} fill={S.orange} opacity="0.7" style={{ animation: 'ship-engine 2.2s ease-in-out infinite' }} />
          {[[120, 38], [300, 30], [480, 44]].map(([x, y], i) => <ellipse key={i} cx={x} cy={y} rx="46" ry="14" fill={ART.dark.base} opacity="0.7" style={{ animation: `steam-rise ${5 + i}s ease-out ${i * 1.3}s infinite` }} />)}
          {[[60, 54, 380, 80, S.redHi], [700, 40, 420, 84, S.cyanHi], [200, 30, 560, 74, S.redHi]].map(([x1, y1, x2, y2, c], i) => <line key={i} x1={x1} y1={y1} x2={x2} y2={y2} stroke={c} strokeWidth="0.9" opacity="0.6" style={{ animation: `holo-flicker ${0.9 + i * 0.4}s ease-in-out infinite` }} />)}
        </g>
      )}
      {fam === 'wastes' && (
        <g>
          <rect x="0" y="96" width={W} height="64" fill={ART.dark.shade} />
          {[[40, 30], [200, 46], [360, 26], [520, 50], [680, 34]].map(([x, h], i) => (
            <g key={i}>
              <rect x={x} y={96 - h * 2} width="26" height={h * 2 + 20} fill={ART.dark.base} /><rect x={x} y={96 - h * 2} width="6" height={h * 2 + 20} fill={ART.dark.hi} opacity="0.5" />
              <rect x={x - 5} y={96 - h * 2 - 6} width="36" height="8" fill={ART.copper.shade} /><rect x={x - 5} y={96 - h * 2 - 6} width="36" height="2" fill={ART.copper.base} />
              <rect x={x + 8} y={96 - h * 2 + 12} width="10" height="3" fill={S.orange} opacity="0.8" style={{ animation: `ship-engine ${1.5 + i * 0.3}s ease-in-out infinite` }} />
            </g>
          ))}
          {[[110, 14], [440, 18], [610, 12]].map(([x, rr], i) => <g key={i}><circle cx={x} cy="80" r={rr * 2} fill={ART.brass.shade} /><circle cx={x} cy="80" r={rr * 2 - 5} fill={ART.dark.shade} /><circle cx={x} cy="80" r="4" fill={ART.brass.base} /></g>)}
          {seed === 7 && <g><polygon points={pts([[0, 160], [0, 90], [70, 90], [70, 160]])} fill={ART.ink} /><polygon points={pts([[6, 160], [6, 104], [64, 104], [64, 160]])} fill={S.orange} opacity="0.5" style={{ animation: 'ship-engine 2.4s ease-in-out infinite' }} /></g>}
          <polygon points={pts([[0, 150], [120, 138], [260, 148], [420, 136], [600, 150], [768, 140], [768, 160], [0, 160]])} fill={S.orange} opacity="0.55" />
          {[[80, 40], [330, 34], [590, 44]].map(([x, y], i) => <ellipse key={i} cx={x} cy={y} rx="52" ry="16" fill={ART.ink} opacity="0.5" style={{ animation: `steam-rise ${5 + i}s ease-out ${i * 1.2}s infinite` }} />)}
        </g>
      )}
      <rect x="0" y="146" width={W} height="14" fill={ART.dark.shade} /><rect x="0" y="146" width={W} height="2" fill={fam === 'ravine' ? ART.teal.shade : ART.rust.shade} />
      <Rivets x={6} y={152} w={756} n={48} c="dark" /><Seams x={0} y={146} w={W} h={14} cols={24} op={0.3} />
      <Grime x={0} y={100} w={W} h={60} seed={2101} n={10} op={0.12} /><Scuff x={0} y={146} w={W} h={14} seed={2102} n={12} c="dark" />
    </svg>
  );
}

// BADOR VISTA BEGIN
// Backdrop panoramas for the Bador zones, one drawing routine per family, one thin wrapper per footprint (the art linter wants a literal viewBox).
// Families: moonbase, foundry, cavern, hullbay, ring (orbital array), hqwindow (panoramic glass). Variants end in _a, _b or _c to vary the layout.
function bvistaArt(variant, cols, rows) {
  const S = ART.signal, K = ART.sky;
  const fam = variant.split('_')[0];
  const W = cols * 32, H = rows * 32;
  const seed = { a: 3, b: 7, c: 13 }[variant.split('_')[1]] || 3;
  const r = rng(seed + cols);
  const band = (list) => list.map(([y0, y1, c], i) => <rect key={'bd' + i} x="0" y={y0} width={W} height={y1 - y0 + 1} fill={c} />);
  const stars = (n, maxY, c) => Array.from({ length: n }, (_, i) => <circle key={'st' + i} cx={r() * W} cy={r() * maxY} r={0.5 + r() * 0.8} fill={c || S.kyber} opacity="0.6" style={{ animation: `ship-blink ${2 + r() * 3}s ease-in-out ${r() * 3}s infinite` }} />);
  const steam = (list, c) => list.map(([x, y, rx, ry], i) => <ellipse key={'sm' + i} cx={x} cy={y} rx={rx} ry={ry} fill={c} opacity="0.45" style={{ animation: `steam-rise ${5 + i * 1.3}s ease-out ${i * 1.1}s infinite` }} />);
  const out = [];
  if (fam === 'moonbase') {
    out.push(...band([[0, 30, ART.ink], [30, 58, K.indigo], [58, 84, K.violet], [84, 104, K.plum], [104, 120, K.rose], [120, 134, K.haze]]));
    out.push(...stars(Math.round(cols * 2.2), 70));
    const px = W * (seed === 7 ? 0.28 : 0.74), py = 92;
    out.push(<g key="planet">
      <circle cx={px} cy={py} r="76" fill={ART.deepglass.shade} />
      <clipPath id={'bvp' + cols + seed}><circle cx={px} cy={py} r="74" /></clipPath>
      <g clipPath={`url(#bvp${cols}${seed})`}>
        <circle cx={px} cy={py} r="74" fill={ART.deepglass.base} />
        {[[-52, 12, ART.deepglass.hi], [-28, 9, ART.teal.base], [-6, 14, ART.deepglass.hi], [20, 10, ART.teal.shade], [42, 12, ART.deepglass.shade]].map(([dy, h, c], i) => <rect key={i} x={px - 80} y={py + dy} width="160" height={h} fill={c} opacity="0.75" />)}
        <circle cx={px + 34} cy={py + 10} r="74" fill={ART.dark.shade} opacity="0.62" />
        {Array.from({ length: 16 }, (_, i) => <rect key={'cl' + i} x={px + 8 + r() * 56} y={py - 40 + r() * 100} width="1.6" height="1.6" fill={S.amber} opacity="0.8" />)}
      </g>
      <ellipse cx={px} cy={py + 4} rx="122" ry="15" fill="none" stroke={ART.pearl.base} strokeWidth="2.2" opacity="0.75" transform={`rotate(-16 ${px} ${py})`} />
      {Array.from({ length: 9 }, (_, i) => { const a = (i / 9) * Math.PI * 2; return <rect key={'rg' + i} x={px + Math.cos(a) * 122 - 1.5} y={py + 4 + Math.sin(a) * 15 - 1.5} width="3" height="3" fill={S.gold} opacity="0.85" style={{ animation: `ship-blink ${2 + (i % 3)}s ease-in-out ${i * 0.3}s infinite` }} />; })}
    </g>);
    out.push(<polygon key="rim" points={pts([[0, 134], [60, 118], [150, 126], [260, 112], [380, 124], [500, 114], [640, 126], [760, 116], [W, 122], [W, 150], [0, 150]])} fill={ART.dark.shade} />);
    out.push(<polygon key="rim2" points={pts([[0, 146], [90, 136], [210, 142], [340, 134], [470, 142], [600, 136], [W, 140], [W, 150], [0, 150]])} fill={ART.dark.base} />);
    const masts = [0.1, 0.26, 0.52, 0.86].map((f) => f * W);
    masts.forEach((x, i) => out.push(<g key={'ms' + i}><rect x={x} y={70 - (i % 2) * 14} width="3" height={80 + (i % 2) * 14} fill={ART.dark.base} /><circle cx={x + 1.5} cy={70 - (i % 2) * 14} r="3" fill={S.red} style={{ animation: `ship-blink ${1.6 + i * 0.4}s ease-in-out infinite` }} /><polygon points={pts([[x + 1.5, 76 - (i % 2) * 14], [x - 22, 146], [x + 25, 146]])} fill={S.cyanHi} opacity="0.1" /></g>));
    const dx = W * (seed === 7 ? 0.62 : 0.34);
    out.push(<g key="dome"><ellipse cx={dx} cy="140" rx="74" ry="34" fill={ART.hull.shade} /><ellipse cx={dx} cy="140" rx="74" ry="6" fill={ART.brass.base} /><ellipse cx={dx} cy="124" rx="40" ry="14" fill={ART.hull.base} opacity="0.55" />{Array.from({ length: 9 }, (_, i) => <rect key={i} x={dx - 62 + i * 14} y="132" width="5" height="2.4" fill={S.cyan} opacity="0.7" />)}<rect x={dx - 2} y="96" width="4" height="22" fill={ART.dark.base} /><circle cx={dx} cy="95" r="2.6" fill={S.amber} style={{ animation: 'ship-blink 2s ease-in-out infinite' }} /></g>);
    out.push(<g key="shuttle"><polygon points={pts([[W * 0.5, 60], [W * 0.5 + 22, 56], [W * 0.5 + 28, 61], [W * 0.5 + 22, 65]])} fill={ART.hull.base} /><polygon points={pts([[W * 0.5 - 140, 112], [W * 0.5, 61], [W * 0.5 - 4, 66], [W * 0.5 - 140, 118]])} fill={ART.pearl.hi} opacity="0.22" /><circle cx={W * 0.5 - 2} cy="62" r="3" fill={S.orange} opacity="0.9" style={{ animation: 'ship-engine 1.4s ease-in-out infinite' }} /></g>);
    out.push(...steam([[W * 0.18, 100, 70, 10], [W * 0.62, 108, 90, 12]], K.haze));
  } else if (fam === 'foundry') {
    out.push(...band([[0, 26, ART.ink], [26, 58, ART.dark.shade], [58, 92, ART.rust.shade], [92, 120, ART.copper.shade], [120, 146, ART.rust.base]]));
    out.push(...steam([[W * 0.12, 40, 80, 20], [W * 0.4, 30, 100, 22], [W * 0.7, 44, 90, 20], [W * 0.92, 34, 70, 18]], ART.ink));
    const towers = Math.round(cols / 4);
    for (let i = 0; i < towers; i++) {
      const x = 18 + i * (W / towers) + r() * 22, h = 52 + r() * 54, w = 26 + r() * 16, tall = i % 2 === 0;
      if (tall) out.push(<g key={'sk' + i}><rect x={x} y={146 - h - 20} width={w * 0.5} height={h + 22} fill={ART.dark.base} /><rect x={x} y={146 - h - 20} width="3" height={h + 22} fill={ART.dark.hi} opacity="0.5" />{[0, 1, 2].map((b) => <rect key={b} x={x} y={146 - h - 14 + b * 12} width={w * 0.5} height="5" fill={b % 2 ? ART.pearl.shade : ART.red.base} opacity="0.85" />)}<polygon points={pts([[x + w * 0.25 - 5, 146 - h - 20], [x + w * 0.25, 146 - h - 38], [x + w * 0.25 + 5, 146 - h - 20]])} fill={S.orange} style={{ animation: `ship-engine ${1.3 + i * 0.2}s ease-in-out infinite` }} /></g>);
      else out.push(<g key={'ct' + i}><polygon points={pts([[x, 146], [x + 6, 146 - h], [x + w - 6, 146 - h], [x + w, 146]])} fill={ART.hull.shade} /><polygon points={pts([[x + w * 0.5, 146], [x + w * 0.5 + 4, 146 - h], [x + w - 6, 146 - h], [x + w, 146]])} fill={ART.dark.base} opacity="0.7" /><rect x={x + 4} y={146 - h} width={w - 8} height="4" fill={ART.hull.base} />{[0, 1, 2, 3].map((b) => <rect key={b} x={x + 8} y={146 - h + 12 + b * 12} width={w - 16} height="2.6" fill={S.amber} opacity="0.7" style={{ animation: `ship-blink ${2 + b * 0.5}s ease-in-out ${i * 0.2}s infinite` }} />)}</g>);
    }
    for (let i = 0; i < Math.round(cols / 8); i++) { const gx = 40 + i * (W / Math.max(1, Math.round(cols / 8))) + 60; out.push(<g key={'gc' + i}><polygon points={pts([[gx, 146], [gx + 8, 60], [gx + 14, 60], [gx + 22, 146]])} fill={ART.dark.base} opacity="0.85" /><rect x={gx - 30} y="58" width="110" height="6" fill={ART.brass.shade} /><rect x={gx + 40} y="64" width="3" height="40" fill={ART.dark.hi} opacity="0.6" /><rect x={gx + 36} y="104" width="11" height="8" fill={ART.hull.shade} /></g>); }
    out.push(<polygon key="slag" points={pts([[0, 150], [W * 0.12, 138], [W * 0.3, 148], [W * 0.5, 136], [W * 0.72, 148], [W * 0.9, 138], [W, 146], [W, 160], [0, 160]])} fill={S.orange} opacity="0.55" />);
  } else if (fam === 'cavern') {
    out.push(...band([[0, 36, ART.ink], [36, 76, ART.dark.shade], [76, 112, K.indigo], [112, 146, ART.teal.shade]]));
    out.push(...stars(Math.round(cols * 1.6), 100, S.anomaly));
    for (let i = 0; i < Math.round(cols * 0.9); i++) { const x = r() * W, w = 10 + r() * 30, h = 20 + r() * 70; out.push(<g key={'sl' + i}><polygon points={pts([[x - w / 2, 0], [x + w / 2, 0], [x + r() * 4 - 2, h]])} fill={ART.dark.base} /><polygon points={pts([[x + w * 0.1, 0], [x + w / 2, 0], [x + r() * 4 - 2, h]])} fill={ART.ink} opacity="0.6" /></g>); }
    out.push(<polygon key="shaft" points={pts([[W * 0.46, 0], [W * 0.54, 0], [W * 0.62, 146], [W * 0.38, 146]])} fill={ART.pearl.hi} opacity="0.07" />);
    for (let i = 0; i < Math.round(cols / 5); i++) { const x = 20 + i * (W / Math.round(cols / 5)) + r() * 30, h = 20 + r() * 40; out.push(<g key={'cr' + i}><polygon points={pts([[x, 146], [x + 5, 146 - h], [x + 11, 146 - h * 0.55], [x + 16, 146 - h * 0.9], [x + 22, 146]])} fill={ART.fabric.base} /><polygon points={pts([[x + 5, 146 - h], [x + 11, 146 - h * 0.55], [x + 16, 146 - h * 0.9], [x + 13, 146]])} fill={S.violet} opacity="0.85" style={{ animation: `holo-flicker ${2.4 + i * 0.3}s ease-in-out ${i * 0.4}s infinite` }} /></g>); }
    for (let i = 0; i < 4; i++) { const y = 44 + i * 18; out.push(<polyline key={'vn' + i} points={pts([[r() * W * 0.3, y], [W * 0.3 + r() * 80, y + 10], [W * 0.6, y - 6 + r() * 12], [W, y + 8]])} fill="none" stroke={S.anomaly} strokeWidth="1.4" opacity="0.5" style={{ animation: `holo-flicker ${3 + i}s ease-in-out ${i * 0.7}s infinite` }} />); }
    out.push(<rect key="lake" x="0" y="124" width={W} height="22" fill={ART.deepglass.shade} opacity="0.85" />);
    out.push(...Array.from({ length: Math.round(cols / 2) }, (_, i) => <rect key={'lk' + i} x={r() * W} y={128 + r() * 14} width={10 + r() * 22} height="1.6" fill={S.cyan} opacity="0.35" style={{ animation: `ship-blink ${3 + r() * 3}s ease-in-out ${r() * 3}s infinite` }} />));
    out.push(...steam([[W * 0.2, 118, 120, 8], [W * 0.7, 120, 140, 9]], ART.teal.base));
  } else if (fam === 'hullbay') {
    out.push(...band([[0, 40, ART.ink], [40, 100, ART.dark.shade], [100, 146, ART.steel.shade]]));
    out.push(...Array.from({ length: Math.round(cols * 1.5) }, (_, i) => <rect key={'pn' + i} x={i * (W / Math.round(cols * 1.5))} y="44" width="2" height="102" fill={ART.ink} opacity="0.35" />));
    const fx = W * (seed === 7 ? 0.5 : 0.06), fl = W * 0.46;
    out.push(<g key="keel"><polygon points={pts([[fx, 136], [fx + fl * 0.12, 96], [fx + fl * 0.7, 82], [fx + fl, 100], [fx + fl * 0.9, 126], [fx + fl * 0.5, 138]])} fill={ART.dark.base} opacity="0.9" />{Array.from({ length: 11 }, (_, i) => <polygon key={i} points={pts([[fx + 10 + i * fl * 0.085, 134], [fx + 22 + i * fl * 0.085, 90 + Math.abs(i - 5) * 3], [fx + 30 + i * fl * 0.085, 90 + Math.abs(i - 5) * 3], [fx + 20 + i * fl * 0.085, 134]])} fill={ART.steel.shade} opacity="0.8" />)}<polygon points={pts([[fx + fl * 0.12, 96], [fx + fl * 0.7, 82], [fx + fl * 0.7, 88], [fx + fl * 0.14, 102]])} fill={ART.steel.base} opacity="0.7" />{Array.from({ length: 6 }, (_, i) => <rect key={'wl' + i} x={fx + 24 + i * fl * 0.14} y={86 + (i % 2) * 4} width="4" height="3" fill={i % 2 ? S.cyan : S.amber} opacity="0.9" style={{ animation: `ship-blink ${1.8 + i * 0.4}s ease-in-out ${i * 0.3}s infinite` }} />)}</g>);
    const vx = W * (seed === 7 ? 0.2 : 0.78);
    out.push(<g key="vp"><rect x={vx - 60} y="46" width="120" height="56" fill={ART.screen.space} /><rect x={vx - 60} y="46" width="120" height="4" fill={ART.steel.hi} />{Array.from({ length: 22 }, (_, i) => <circle key={'vs' + i} cx={vx - 56 + r() * 112} cy={52 + r() * 46} r="0.9" fill={S.kyber} opacity="0.7" />)}<polygon points={pts([[vx - 56, 102], [vx - 26, 80], [vx + 2, 90], [vx + 28, 72], [vx + 60, 102]])} fill={ART.fabric.shade} opacity="0.85" /><rect x={vx - 1} y="46" width="3" height="56" fill={ART.steel.shade} /></g>);
    for (let i = 0; i < Math.round(cols / 6); i++) { const x = 10 + i * (W / Math.round(cols / 6)) + 130; out.push(<g key={'rb' + i}><polygon points={pts([[x, 146], [x + 6, 30], [x + 34, 14], [x + 68, 30], [x + 74, 146], [x + 60, 146], [x + 56, 40], [x + 34, 28], [x + 18, 40], [x + 14, 146]])} fill={ART.steel.base} /><polygon points={pts([[x + 56, 40], [x + 34, 28], [x + 34, 34], [x + 52, 44]])} fill={ART.steel.hi} opacity="0.5" /><rect x={x + 62} y="60" width="5" height="3" fill={S.red} opacity="0.8" style={{ animation: `ship-blink ${1.4 + i * 0.3}s ease-in-out ${i * 0.2}s infinite` }} /></g>); }
    out.push(<rect key="g1" x="0" y="22" width={W} height="6" fill={ART.dark.hi} opacity="0.8" />, <rect key="g2" x="0" y="106" width={W} height="4" fill={ART.dark.hi} opacity="0.55" />);
    out.push(...Array.from({ length: Math.round(cols / 3) }, (_, i) => <rect key={'lp' + i} x={8 + i * (W / Math.round(cols / 3))} y="28" width="6" height="3" fill={S.red} opacity="0.85" style={{ animation: `ship-blink ${2 + (i % 3)}s ease-in-out ${i * 0.25}s infinite` }} />));
    out.push(...Array.from({ length: Math.round(cols / 3) }, (_, i) => <line key={'cb' + i} x1={r() * W} y1="28" x2={r() * W * 0.04 + (i * W) / Math.round(cols / 3)} y2={34 + r() * 60} stroke={ART.dark.hi} strokeWidth="1.2" opacity="0.6" />));
    out.push(...Array.from({ length: Math.round(cols / 6) }, (_, i) => <circle key={'sp' + i} cx={r() * W} cy={40 + r() * 60} r="2.4" fill={S.amberHi} opacity="0.9" style={{ animation: `ship-spark ${1.6 + r() * 2}s ease-in-out ${r() * 2}s infinite` }} />));
  } else if (fam === 'ring') {
    out.push(...band([[0, 150, ART.ink]]));
    out.push(...stars(Math.round(cols * 3.2), 110));
    out.push(<g key="limb"><circle cx={W * 0.5} cy={H + 360} r="440" fill={ART.deepglass.shade} /><circle cx={W * 0.5} cy={H + 360} r="430" fill={ART.deepglass.base} /><circle cx={W * 0.5} cy={H + 372} r="420" fill={ART.teal.shade} opacity="0.7" />{Array.from({ length: Math.round(cols * 1.4) }, (_, i) => <rect key={'ct' + i} x={r() * W} y={118 + r() * 34} width="1.8" height="1.8" fill={S.amber} opacity="0.85" />)}</g>);
    const hulls = Math.max(2, Math.round(cols / 10));
    for (let i = 0; i < hulls; i++) { const x = 30 + i * (W / hulls) + r() * 40, sc = 0.8 + r() * 0.5; out.push(<g key={'hl' + i}><polygon points={pts([[x, 96], [x + 170 * sc, 78], [x + 210 * sc, 92], [x + 170 * sc, 106]])} fill={ART.dark.base} /><polygon points={pts([[x, 96], [x + 170 * sc, 78], [x + 120 * sc, 94]])} fill={ART.steel.shade} />{Array.from({ length: 7 }, (_, k) => <rect key={k} x={x + 20 + k * 22 * sc} y={92 + (k % 2) * 4} width="3" height="2" fill={k % 3 ? S.cyan : S.amber} opacity="0.85" style={{ animation: `ship-blink ${2 + k * 0.4}s ease-in-out ${k * 0.3}s infinite` }} />)}<line x1={x + 30} y1="60" x2={x + 120 * sc} y2="84" stroke={ART.steel.base} strokeWidth="1.2" opacity="0.7" /><line x1={x + 60} y1="56" x2={x + 140 * sc} y2="82" stroke={ART.steel.base} strokeWidth="1.2" opacity="0.7" /></g>); }
    out.push(<g key="arr"><polygon points={pts([[0, 128], [W, 112], [W, 117], [0, 133]])} fill={ART.steel.base} opacity="0.85" />{Array.from({ length: Math.round(cols * 1.2) }, (_, i) => <rect key={'al' + i} x={6 + i * (W / Math.round(cols * 1.2))} y={128 - (i / Math.round(cols * 1.2)) * 16} width="4" height="2.4" fill={S.gold} opacity="0.85" style={{ animation: `ship-blink ${2 + (i % 4)}s ease-in-out ${i * 0.2}s infinite` }} />)}</g>);
    out.push(<g key="frt"><polygon points={pts([[W * 0.78, 40], [W * 0.78 + 34, 36], [W * 0.78 + 42, 41], [W * 0.78 + 34, 46]])} fill={ART.hull.base} /><circle cx={W * 0.78 - 1} cy="41" r="3" fill={S.cyan} opacity="0.9" style={{ animation: 'ship-engine 1.6s ease-in-out infinite' }} /></g>);
  } else if (fam === 'hqwindow') {
    out.push(...band([[0, H * 0.22, K.indigo], [H * 0.22, H * 0.44, K.violet], [H * 0.44, H * 0.62, K.plum], [H * 0.62, H * 0.78, K.rose], [H * 0.78, H, K.haze]]));
    out.push(...stars(Math.round(cols * 1.2), H * 0.3));
    out.push(<polygon key="hz" points={pts([[0, H - 4], [W * 0.1, H * 0.66], [W * 0.24, H * 0.76], [W * 0.4, H * 0.62], [W * 0.58, H * 0.74], [W * 0.78, H * 0.6], [W, H * 0.7], [W, H], [0, H]])} fill={ART.dark.shade} />);
    out.push(<ellipse key="dm" cx={W * 0.7} cy={H * 0.8} rx="60" ry="22" fill={ART.hull.shade} />);
    out.push(...[0.18, 0.46, 0.84].map((f, i) => <g key={'mk' + i}><rect x={W * f} y={H * 0.44} width="3" height={H * 0.4} fill={ART.dark.base} /><circle cx={W * f + 1.5} cy={H * 0.44} r="2.4" fill={S.red} style={{ animation: `ship-blink ${1.6 + i * 0.5}s ease-in-out infinite` }} /></g>));
    out.push(...Array.from({ length: Math.round(cols / 4) }, (_, i) => <rect key={'ml' + i} x={20 + i * (W / Math.round(cols / 4))} y={H - 14} width="8" height="2.4" fill={S.cyan} opacity="0.6" />));
    for (let i = 0; i <= Math.round(cols / 5); i++) { const x = i * (W / Math.round(cols / 5)); out.push(<g key={'mu' + i}><rect x={x - 4} y="0" width="8" height={H} fill={ART.steel.shade} /><rect x={x - 4} y="0" width="3" height={H} fill={ART.steel.base} /><rect x={x - 5} y={H * 0.5} width="10" height="3" fill={ART.brass.base} /></g>); }
    out.push(<rect key="hd" x="0" y="0" width={W} height="6" fill={ART.brass.shade} />);
  }
  return out;
}

const bvistaBase = (cols, rows, W, H, variant, extra) => (
  <>
    {bvistaArt(variant, cols, rows)}
    {extra}
  </>
);

function PropBvistaA({ variant = 'moonbase_a' }) {
  return (
    <svg viewBox="0 0 640 160" width="640" height="160" style={PROP_STYLE}>
      {bvistaBase(20, 5, 640, 160, variant)}
      <rect x="0" y="146" width="640" height="14" fill={ART.dark.shade} /><rect x="0" y="146" width="640" height="2" fill={ART.steel.shade} />
      <Rivets x={6} y={152} w={628} n={40} c="dark" /><Seams x={0} y={146} w={640} h={14} cols={20} op={0.3} />
      <Grime x={0} y={100} w={640} h={60} seed={2301} n={9} op={0.12} /><Scuff x={0} y={146} w={640} h={14} seed={2302} n={10} c="dark" />
    </svg>
  );
}
function PropBvistaB({ variant = 'moonbase_a' }) {
  return (
    <svg viewBox="0 0 768 160" width="768" height="160" style={PROP_STYLE}>
      {bvistaBase(24, 5, 768, 160, variant)}
      <rect x="0" y="146" width="768" height="14" fill={ART.dark.shade} /><rect x="0" y="146" width="768" height="2" fill={ART.steel.shade} />
      <Rivets x={6} y={152} w={756} n={48} c="dark" /><Seams x={0} y={146} w={768} h={14} cols={24} op={0.3} />
      <Grime x={0} y={100} w={768} h={60} seed={2311} n={10} op={0.12} /><Scuff x={0} y={146} w={768} h={14} seed={2312} n={12} c="dark" />
    </svg>
  );
}
function PropBvistaC({ variant = 'moonbase_a' }) {
  return (
    <svg viewBox="0 0 1024 160" width="1024" height="160" style={PROP_STYLE}>
      {bvistaBase(32, 5, 1024, 160, variant)}
      <rect x="0" y="146" width="1024" height="14" fill={ART.dark.shade} /><rect x="0" y="146" width="1024" height="2" fill={ART.steel.shade} />
      <Rivets x={6} y={152} w={1012} n={64} c="dark" /><Seams x={0} y={146} w={1024} h={14} cols={32} op={0.3} />
      <Grime x={0} y={100} w={1024} h={60} seed={2321} n={12} op={0.12} /><Scuff x={0} y={146} w={1024} h={14} seed={2322} n={14} c="dark" />
    </svg>
  );
}
function PropBvistaD({ variant = 'moonbase_a' }) {
  return (
    <svg viewBox="0 0 1152 160" width="1152" height="160" style={PROP_STYLE}>
      {bvistaBase(36, 5, 1152, 160, variant)}
      <rect x="0" y="146" width="1152" height="14" fill={ART.dark.shade} /><rect x="0" y="146" width="1152" height="2" fill={ART.steel.shade} />
      <Rivets x={6} y={152} w={1140} n={72} c="dark" /><Seams x={0} y={146} w={1152} h={14} cols={36} op={0.3} />
      <Grime x={0} y={100} w={1152} h={60} seed={2331} n={14} op={0.12} /><Scuff x={0} y={146} w={1152} h={14} seed={2332} n={16} c="dark" />
    </svg>
  );
}
function PropBvistaE({ variant = 'moonbase_a' }) {
  return (
    <svg viewBox="0 0 1408 160" width="1408" height="160" style={PROP_STYLE}>
      {bvistaBase(44, 5, 1408, 160, variant)}
      <rect x="0" y="146" width="1408" height="14" fill={ART.dark.shade} /><rect x="0" y="146" width="1408" height="2" fill={ART.steel.shade} />
      <Rivets x={6} y={152} w={1396} n={88} c="dark" /><Seams x={0} y={146} w={1408} h={14} cols={44} op={0.3} />
      <Grime x={0} y={100} w={1408} h={60} seed={2341} n={16} op={0.12} /><Scuff x={0} y={146} w={1408} h={14} seed={2342} n={18} c="dark" />
    </svg>
  );
}
function PropBvistaF({ variant = 'moonbase_a' }) {
  return (
    <svg viewBox="0 0 1536 160" width="1536" height="160" style={PROP_STYLE}>
      {bvistaBase(48, 5, 1536, 160, variant)}
      <rect x="0" y="146" width="1536" height="14" fill={ART.dark.shade} /><rect x="0" y="146" width="1536" height="2" fill={ART.steel.shade} />
      <Rivets x={6} y={152} w={1524} n={96} c="dark" /><Seams x={0} y={146} w={1536} h={14} cols={48} op={0.3} />
      <Grime x={0} y={100} w={1536} h={60} seed={2351} n={18} op={0.12} /><Scuff x={0} y={146} w={1536} h={14} seed={2352} n={20} c="dark" />
    </svg>
  );
}
function PropBvistaG({ variant = 'hqwindow_a' }) {
  return (
    <svg viewBox="0 0 960 96" width="960" height="96" style={PROP_STYLE}>
      {bvistaBase(30, 3, 960, 96, variant)}
      <rect x="0" y="86" width="960" height="10" fill={ART.dark.shade} /><rect x="0" y="86" width="960" height="2" fill={ART.brass.base} />
      <Rivets x={6} y={91} w={948} n={60} c="brass" /><Seams x={0} y={86} w={960} h={10} cols={30} op={0.3} />
      <Grime x={0} y={50} w={960} h={46} seed={2361} n={8} op={0.1} /><Scuff x={0} y={86} w={960} h={10} seed={2362} n={8} c="dark" />
    </svg>
  );
}
// BADOR VISTA END

// ZORA PROPS BEGIN
// The Ghost Keel frame in its drydock cradle, the Warden Prime defence droid, and the Cherit memorial plinth.
function PropKeelFrame() {
  const S = ART.signal;
  const ribs = Array.from({ length: 12 }, (_, i) => ({ x: 34 + i * 18, top: 84 + Math.abs(i - 4) * 1.8 }));
  return (
    <svg viewBox="0 0 288 160" width="288" height="160" style={PROP_STYLE}>
      <PropShadow cx={144} cy={154} rx={134} ry={4} />
      <Bev t="polygon" points={pts([[6, 154], [6, 140], [282, 140], [282, 154]])} c="steel" o={1.2} /><Hazard x={10} y={146} w={268} h={4} />
      {[26, 98, 170, 242].map((x, i) => <g key={i}><Bev t="polygon" points={pts([[x, 140], [x + 2, 66], [x + 14, 66], [x + 16, 140]])} c="dark" o={1} /><Seams x={x} y={70} w={16} h={68} cols={1} rows={4} op={0.4} /><rect x={x + 3} y={60 - (i % 2) * 6} width="10" height="6" fill={ART.brass.base} /></g>)}
      <Bev t="polygon" points={pts([[18, 124], [30, 100], [246, 86], [282, 102], [250, 126], [64, 134]])} c="dark" o={1.3} />
      {ribs.map((rb, i) => <polygon key={'rb' + i} points={pts([[rb.x, 130], [rb.x + 3, rb.top], [rb.x + 11, rb.top - 2], [rb.x + 13, 128]])} fill={ART.steel.shade} />)}
      {ribs.map((rb, i) => <polygon key={'rh' + i} points={pts([[rb.x + 3, rb.top], [rb.x + 6, rb.top - 1], [rb.x + 6, 128], [rb.x + 3, 130]])} fill={ART.steel.base} opacity="0.7" />)}
      <Bev t="polygon" points={pts([[196, 90], [246, 86], [282, 102], [250, 126], [196, 126]])} c="hull" o={1.1} />
      <Seams x={196} y={88} w={86} h={38} cols={4} rows={2} op={0.5} />
      <polygon points={pts([[196, 90], [246, 86], [250, 92], [198, 96]])} fill={ART.pearl.hi} opacity="0.8" />
      <Bev t="polygon" points={pts([[96, 86], [104, 40], [150, 30], [160, 84]])} c="steel" o={0.9} />
      <g stroke={ART.hull.shade} strokeWidth="1.4" fill="none"><line x1="98" y1="84" x2="150" y2="34" /><line x1="104" y1="42" x2="158" y2="84" /><line x1="100" y1="62" x2="156" y2="58" /></g>
      <polygon points={pts([[104, 40], [150, 30], [152, 36], [106, 46]])} fill={ART.steel.hi} opacity="0.7" />
      <polygon points={pts(ngon(124, 110, 10, 10, 10))} fill={ART.ink} /><polygon points={pts(ngon(124, 110, 7, 7, 10))} fill={S.cyan} style={{ animation: 'ship-engine 2.4s ease-in-out infinite' }} /><polygon points={pts(ngon(124, 110, 3, 3, 6))} fill={S.cyanHi} />
      <line x1="60" y1="0" x2="60" y2="70" stroke={ART.dark.hi} strokeWidth="1.4" /><line x1="200" y1="0" x2="206" y2="84" stroke={ART.dark.hi} strokeWidth="1.4" />
      <Bev t="polygon" points={pts([[54, 70], [66, 70], [64, 80], [56, 80]])} c="amber" o={0.7} /><Bev t="polygon" points={pts([[198, 82], [212, 82], [210, 90], [200, 90]])} c="amber" o={0.7} />
      {[[40, 112], [112, 94], [176, 92], [220, 108], [262, 112]].map(([x, y], i) => <circle key={'sp' + i} cx={x} cy={y} r="2.2" fill={S.amberHi} style={{ animation: `ship-spark ${1.3 + i * 0.4}s ease-in-out ${i * 0.3}s infinite` }} />)}
      {[[44, 92, 'cyan'], [80, 100, 'amber'], [168, 88, 'cyan'], [228, 96, 'amber']].map(([x, y, k], i) => <rect key={'wl' + i} x={x} y={y} width="5" height="3" fill={S[k]} style={{ animation: `ship-blink ${1.8 + i * 0.5}s ease-in-out ${i * 0.3}s infinite` }} />)}
      <Slab x={96} y={142} w={96} h={9} k={1} c="dark" o={0.8} /><text x="144" y="148.8" textAnchor="middle" fontSize="4" fill={S.amberHi} fontWeight="700" {...PT}>GHOST KEEL HULL 001</text>
      <Rivets x={10} y={142} w={268} n={30} /><Lights x={14} y={150} n={6} gap={6} seed={11} />
      <Grime x={8} y={60} w={272} h={92} seed={880} n={10} op={0.22} /><Scuff x={10} y={84} w={266} h={56} seed={881} n={12} c="steel" />
    </svg>
  );
}

function PropWardenPrime() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={91} rx={40} ry={3.4} />
      <Slab x={10} y={80} w={76} h={9} k={2} c="dark" o={1} /><Hazard x={14} y={84} w={68} h={3} />
      {[[16, 80, 4, 52], [26, 80, 14, 62], [70, 80, 82, 52], [60, 80, 72, 62]].map(([x1, y1, x2, y2], i) => <g key={i}><line x1={x1 + 6} y1={y1} x2={x2 + 6} y2={y2} stroke={ART.ink} strokeWidth="6" /><line x1={x1 + 6} y1={y1} x2={x2 + 6} y2={y2} stroke={ART.steel.base} strokeWidth="3.4" /><polygon points={pts(ngon(x2 + 6, y2, 4, 4, 6))} fill={ART.steel.hi} /></g>)}
      <Bev t="polygon" points={pts([[24, 70], [20, 40], [34, 24], [62, 24], [76, 40], [72, 70]])} c="dark" o={1.2} />
      <Bev t="polygon" points={pts([[30, 66], [28, 44], [38, 32], [58, 32], [68, 44], [66, 66]])} c="steel" o={0.9} />
      <Seams x={28} y={34} w={40} h={32} cols={2} rows={2} op={0.45} /><Vent x={38} y={54} w={20} h={8} n={3} />
      <Bev t="polygon" points={pts([[34, 24], [38, 10], [58, 10], [62, 24]])} c="dark" o={1} />
      <rect x="40" y="14" width="16" height="6" fill={ART.ink} /><rect x="42" y="15" width="12" height="4" fill={S.red} style={{ animation: 'ship-engine 1.6s ease-in-out infinite' }} /><rect x="46" y="15" width="4" height="4" fill={S.redHi} />
      <Bev t="polygon" points={pts([[6, 44], [6, 28], [20, 28], [22, 46]])} c="steel" o={0.9} /><Bev t="polygon" points={pts([[74, 46], [76, 28], [90, 28], [90, 44]])} c="steel" o={0.9} />
      <rect x="2" y="30" width="6" height="14" fill={ART.dark.base} /><rect x="88" y="30" width="6" height="14" fill={ART.dark.base} />
      <Glyph k="cog" x={48} y={46} s={4} c={S.amber} />
      <Rivets x={26} y={28} w={44} n={7} /><Lights x={30} y={72} n={5} gap={8} seed={3} />
      <Grime x={8} y={8} w={80} h={82} seed={890} n={7} op={0.22} /><Scuff x={10} y={12} w={76} h={72} seed={891} n={9} c="steel" />
    </svg>
  );
}

function PropMemorialPlinth() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={60} rx={26} ry={2.6} />
      <Slab x={8} y={42} w={48} h={16} k={2} c="dark" o={1} /><Slab x={14} y={30} w={36} h={14} k={2} c="steel" o={0.9} />
      <Hazard x={10} y={54} w={44} h={2.4} />
      <polygon points={pts([[18, 32], [30, 28], [46, 30], [44, 38], [20, 38]])} fill={ART.fabric.base} /><polygon points={pts([[18, 32], [30, 28], [34, 32], [22, 36]])} fill={ART.fabric.hi} opacity="0.8" />
      <rect x="38" y="31" width="9" height="6" fill={ART.paper.base} /><rect x="39.4" y="32.4" width="6.2" height="1.2" fill={S.green} />
      <polygon points={pts([[28, 28], [30, 18], [34, 18], [36, 28]])} fill={ART.brass.base} /><polygon points={pts([[29, 18], [32, 8], [35, 18]])} fill={S.cyanHi} style={{ animation: 'holo-flicker 1.8s ease-in-out infinite' }} /><polygon points={pts([[31, 16], [32, 11], [33, 16]])} fill={S.white} />
      <Glow cx={32} cy={14} r={9} c={S.cyan} opacity={0.35} style={{ animation: 'ship-engine 2.4s ease-in-out infinite' }} />
      <Slab x={20} y={46} w={24} h={7} k={1} c="brass" o={0.8} /><text x="32" y="51.2" textAnchor="middle" fontSize="3" fill={ART.ink} fontWeight="700" {...PT}>CHERIT</text>
      <Rivets x={11} y={44} w={42} n={6} /><Lights x={16} y={57} n={3} gap={4} seed={4} />
      <Grime x={8} y={28} w={48} h={30} seed={895} n={4} op={0.22} /><Scuff x={10} y={30} w={44} h={26} seed={896} n={5} c="steel" />
    </svg>
  );
}
// ZORA PROPS END

function PropKdyBarricade({ variant }) {
  const S = ART.signal, scrap = variant === 'scrap';
  const body = scrap ? 'rust' : 'hull';
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3} />
      <Bev t="polygon" points={pts([[6, 56], [6, 24], [16, 14], [80, 14], [90, 24], [90, 56]])} c={body} o={1.4} />
      <rect x="10" y="26" width="76" height="3" fill={scrap ? ART.rust.shade : ART.hull.shade} opacity="0.7" />
      {!scrap && <Hazard x={10} y={44} w={76} h={8} opacity={0.9} />}
      {scrap && [14, 36, 58].map((x, i) => <polygon key={i} points={pts([[x, 52], [x + 4, 30], [x + 20, 34], [x + 18, 52]])} fill={ART.dark.base} opacity="0.7" />)}
      {[[22, 20], [52, 18], [70, 22]].map(([x, y], i) => <g key={i}><circle cx={x} cy={y + 12} r="4" fill={ART.ink} opacity="0.7" /><circle cx={x} cy={y + 12} r="2.2" fill={S.orange} opacity="0.75" style={{ animation: `ship-engine ${1.6 + i * 0.4}s ease-in-out infinite` }} /></g>)}
      <polygon points={pts([[40, 14], [46, 4], [56, 14]])} fill={ART.dark.base} opacity="0.5" />
      <Greeble x={12} y={18} w={70} h={10} seed={2111} n={6} c={scrap ? 'rust' : 'hull'} /><Rivets x={10} y={20} w={76} n={9} c={body} /><Seams x={6} y={14} w={84} h={42} cols={4} rows={1} op={0.3} />
      <Grime x={6} y={14} w={84} h={46} seed={2112} n={6} op={0.2} /><Scuff x={8} y={16} w={80} h={40} seed={2113} n={6} c={body} />
    </svg>
  );
}

function PropAutoTurret({ variant }) {
  const S = ART.signal, ok = variant === 'friendly', lamp = ok ? S.cyanHi : S.red, glow = ok ? S.cyan : S.red;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={24} ry={3} />
      <Slab x={10} y={42} w={44} h={14} k={2} c="dark" o={1.2} /><Hazard x={13} y={50} w={38} h={4} opacity={0.85} />
      <Bev t="polygon" points={pts([[18, 42], [22, 22], [42, 22], [46, 42]])} c="steel" o={1} />
      <Bev t="polygon" points={pts([[22, 24], [26, 12], [38, 12], [42, 24]])} c="dark" o={0.8} />
      <rect x="40" y="14" width="22" height="4" fill={ART.dark.shade} /><rect x="40" y="14" width="22" height="1.2" fill={ART.steel.hi} />
      <rect x="40" y="19" width="22" height="3" fill={ART.dark.shade} />
      <circle cx="32" cy="18" r="4" fill={ART.ink} /><circle cx="32" cy="18" r="2.6" fill={lamp} style={{ animation: 'holo-flicker 1.3s ease-in-out infinite' }} /><Glow cx={32} cy={18} r={11} c={glow} opacity={0.4} />
      <rect x="14" y="30" width="6" height="12" fill={ART.ink} /><rect x="15" y={42 - (ok ? 4 : 10)} width="4" height={ok ? 4 : 10} fill={ok ? S.cyan : S.orange} />
      <Vent x={44} y={30} w={8} h={8} n={3} /><Lights x={24} y={46} n={4} gap={5} seed={5} />
      <Rivets x={13} y={44} w={38} n={7} c="dark" /><Seams x={10} y={42} w={44} h={14} cols={3} op={0.3} />
      <Grime x={10} y={12} w={44} h={44} seed={2121} n={4} op={0.2} /><Scuff x={14} y={14} w={36} h={36} seed={2122} n={5} c="steel" />
    </svg>
  );
}

function PropShantyHovel({ variant = 'a' }) {
  const S = ART.signal;
  const sign = { a: [S.magenta, 'CANTINA'], b: [S.cyanHi, 'REPAIRS'], c: [S.amberHi, 'NOODLES'] }[variant] || [S.magenta, 'OPEN'];
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={92} rx={42} ry={3} />
      <Bev t="polygon" points={pts([[6, 90], [6, 34], [90, 34], [90, 90]])} c="rust" o={1.4} />
      {[0, 1, 2, 3, 4, 5, 6, 7].map((i) => <line key={i} x1={10 + i * 10.4} y1="36" x2={10 + i * 10.4} y2="88" stroke={ART.rust.shade} strokeWidth="1.2" opacity="0.7" />)}
      <Bev t="polygon" points={pts([[2, 36], [14, 20], [82, 18], [94, 36]])} c="copper" o={1} />
      {[0, 1, 2, 3, 4, 5, 6].map((i) => <line key={i} x1={14 + i * 10} y1="22" x2={10 + i * 11} y2="35" stroke={ART.copper.shade} strokeWidth="1" opacity="0.8" />)}
      <rect x="12" y="46" width="22" height="18" fill={ART.screen.amber} /><rect x="14" y="48" width="18" height="14" fill={S.amber} opacity="0.35" style={{ animation: 'holo-flicker 3.4s ease-in-out infinite' }} />
      <Bev t="polygon" points={pts([[58, 90], [58, 52], [78, 52], [78, 90]])} c="dark" o={0.8} /><rect x="60" y="54" width="16" height="34" fill={ART.ink} opacity="0.8" />
      <Slab x={16} y={24} w={62} h={9} k={1.2} c="dark" o={0.8} /><rect x="18" y="26" width="58" height="5" fill={ART.screen.off} />
      <text x="47" y="30.4" textAnchor="middle" fontSize="4.6" fill={sign[0]} fontWeight="700" {...PT} style={{ animation: 'holo-flicker 2s ease-in-out infinite' }}>{sign[1]}</text>
      <Cable d="M80 36 C86 44, 88 52, 84 60" c={ART.dark.base} w={1.6} />
      <Greeble x={10} y={68} w={46} h={20} seed={2131} n={7} c="rust" /><Lights x={14} y={80} n={4} gap={6} seed={6} /><Rivets x={10} y={38} w={76} n={9} c="rust" /><Seams x={6} y={34} w={84} h={56} cols={3} op={0.2} />
      <Grime x={6} y={34} w={84} h={56} seed={2132} n={7} op={0.22} /><Scuff x={8} y={36} w={80} h={50} seed={2133} n={6} c="rust" />
    </svg>
  );
}

function PropBurningWreck() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3} />
      <Bev t="polygon" points={pts([[6, 54], [10, 34], [30, 26], [64, 24], [88, 40], [90, 54]])} c="dark" o={1.2} />
      <polygon points={pts([[16, 36], [32, 28], [60, 26], [60, 40], [20, 42]])} fill={ART.rust.base} /><polygon points={pts([[20, 38], [34, 30], [58, 28], [58, 36]])} fill={ART.rust.hi} opacity="0.6" />
      <circle cx="24" cy="54" r="7" fill={ART.ink} /><circle cx="24" cy="54" r="3.6" fill={ART.dark.hi} /><circle cx="74" cy="54" r="7" fill={ART.ink} /><circle cx="74" cy="54" r="3.6" fill={ART.dark.hi} />
      <polygon points={pts([[40, 28], [46, 8], [52, 20], [58, 4], [64, 24], [60, 30]])} fill={S.orange} style={{ animation: 'holo-flicker 0.9s ease-in-out infinite' }} /><polygon points={pts([[46, 28], [50, 16], [54, 24], [58, 14], [60, 28]])} fill={S.amberHi} style={{ animation: 'holo-flicker 0.6s ease-in-out infinite' }} />
      <Glow cx={52} cy={24} r={26} c={S.orange} opacity={0.5} style={{ animation: 'ship-engine 1.2s ease-in-out infinite' }} />
      <ellipse cx="56" cy="8" rx="14" ry="5" fill={ART.ink} opacity="0.5" style={{ animation: 'steam-rise 3.4s ease-out infinite' }} />
      <Vent x={68} y={36} w={10} h={8} n={3} /><Greeble x={12} y={36} w={20} h={14} seed={2141} n={4} c="dark" /><Rivets x={10} y={50} w={76} n={9} c="dark" /><Seams x={6} y={34} w={84} h={20} cols={3} op={0.3} />
      <Grime x={6} y={26} w={84} h={30} seed={2142} n={6} op={0.3} /><Scuff x={8} y={30} w={80} h={24} seed={2143} n={5} c="rust" />
    </svg>
  );
}

function PropFuelConduit({ variant }) {
  const S = ART.signal, burst = variant === 'burst';
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={60} rx={56} ry={3} />
      <Bev t="polygon" points={pts([[2, 44], [2, 26], [126, 26], [126, 44]])} c="copper" o={1.2} />
      {[24, 54, 84, 108].map((x) => <Bev key={x} t="polygon" points={pts([[x, 48], [x, 22], [x + 6, 22], [x + 6, 48]])} c="dark" o={0.8} />)}
      <Hazard x={4} y={38} w={120} h={5} opacity={0.9} />
      <circle cx="40" cy="34" r="6" fill={ART.ink} /><circle cx="40" cy="34" r="4.4" fill={S.red} /><Bev t="polygon" points={pts([[36, 30], [44, 30], [44, 38], [36, 38]])} c="brass" o={0.5} />
      <circle cx="96" cy="34" r="6" fill={ART.ink} /><circle cx="96" cy="34" r="4.4" fill={burst ? S.orange : S.cyan} style={{ animation: 'holo-flicker 1.4s ease-in-out infinite' }} />
      {burst && <g><polygon points={pts([[66, 28], [60, 8], [70, 18], [74, 2], [80, 24], [72, 30]])} fill={S.orange} style={{ animation: 'holo-flicker 0.8s ease-in-out infinite' }} /><Glow cx={70} cy={20} r={24} c={S.orange} opacity={0.45} /></g>}
      {[10, 70].map((x) => <rect key={x} x={x} y="46" width="20" height="10" fill={ART.dark.base} />)}
      <Rivets x={6} y={29} w={116} n={14} c="copper" /><Seams x={2} y={26} w={124} h={18} cols={8} op={0.35} /><Lights x={12} y={49} n={3} gap={5} seed={7} /><Greeble x={4} y={46} w={120} h={10} seed={2151} n={5} c="dark" />
      <Grime x={2} y={26} w={124} h={30} seed={2152} n={6} op={0.25} /><Scuff x={4} y={28} w={118} h={20} seed={2153} n={6} c="copper" />
    </svg>
  );
}

function PropMortarPit() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={90} rx={42} ry={4} />
      <polygon points={pts(ngon(48, 66, 42, 22, 10))} fill={ART.dark.shade} />
      <polygon points={pts(ngon(48, 62, 38, 19, 10))} fill={ART.rust.shade} />
      {Array.from({ length: 10 }, (_, i) => { const a = (i / 10) * Math.PI * 2; return <Bev key={i} t="polygon" points={pts([[48 + Math.cos(a) * 36 - 6, 62 + Math.sin(a) * 18], [48 + Math.cos(a) * 36 - 6, 52 + Math.sin(a) * 18], [48 + Math.cos(a) * 36 + 6, 52 + Math.sin(a) * 18], [48 + Math.cos(a) * 36 + 6, 62 + Math.sin(a) * 18]])} c="concrete" o={0.6} />; })}
      <Bev t="polygon" points={pts([[40, 64], [44, 30], [52, 30], [56, 64]])} c="dark" o={1} />
      <polygon points={pts([[46, 30], [48, 8], [52, 8], [54, 30]])} fill={ART.steel.base} /><rect x="44" y="26" width="12" height="4" fill={ART.brass.base} />
      <rect x="30" y="68" width="14" height="10" fill={ART.wood.base} /><rect x="30" y="68" width="14" height="2" fill={ART.wood.hi} /><Hazard x={52} y={70} w={16} h={7} opacity={0.9} />
      <circle cx="48" cy="9" r="3" fill={S.orange} style={{ animation: 'ship-blink 1.1s ease-in-out infinite' }} /><Glow cx={48} cy={9} r={12} c={S.orange} opacity={0.4} />
      <Greeble x={34} y={50} w={28} h={14} seed={2161} n={5} c="dark" /><Rivets x={16} y={72} w={64} n={8} c="rust" /><Seams x={14} y={50} w={68} h={30} cols={4} op={0.25} /><Lights x={40} y={74} n={4} gap={4} seed={8} />
      <Grime x={8} y={30} w={80} h={56} seed={2162} n={6} op={0.25} /><Scuff x={14} y={34} w={68} h={46} seed={2163} n={6} c="rust" />
    </svg>
  );
}

function PropCatwalkPost() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={92} rx={40} ry={3} />
      {[12, 80].map((x) => <Bev key={x} t="polygon" points={pts([[x, 92], [x, 34], [x + 5, 34], [x + 5, 92]])} c="rust" o={0.8} />)}
      {[44, 60, 76].map((y) => <line key={y} x1="14" y1={y} x2="82" y2={y + 10} stroke={ART.rust.base} strokeWidth="1.4" opacity="0.7" />)}
      <Bev t="polygon" points={pts([[4, 38], [4, 28], [92, 28], [92, 38]])} c="steel" o={1.2} />
      {Array.from({ length: 12 }, (_, i) => <rect key={i} x={8 + i * 7.2} y="30" width="4" height="6" fill={ART.dark.shade} opacity="0.7" />)}
      <line x1="4" y1="14" x2="92" y2="14" stroke={ART.rust.hi} strokeWidth="1.4" /><line x1="4" y1="20" x2="92" y2="20" stroke={ART.rust.base} strokeWidth="1.2" />
      {[4, 28, 52, 76, 92].map((x) => <rect key={x} x={x} y="14" width="2.2" height="16" fill={ART.rust.base} />)}
      <polygon points={pts([[56, 28], [60, 12], [72, 12], [76, 28]])} fill={ART.dark.base} /><rect x="70" y="14" width="22" height="3" fill={ART.dark.shade} /><circle cx="64" cy="18" r="2" fill={S.orange} style={{ animation: 'ship-blink 1.6s ease-in-out infinite' }} />
      <rect x="22" y="38" width="4" height="54" fill={ART.dark.base} />{[44, 54, 64, 74, 84].map((y) => <rect key={y} x="18" y={y} width="12" height="1.6" fill={ART.steel.base} />)}
      <Vent x={36} y={40} w={14} h={8} n={3} /><Lights x={36} y={52} n={5} gap={4} seed={9} /><Greeble x={6} y={30} w={40} h={6} seed={2171} n={4} c="steel" /><Rivets x={6} y={33} w={84} n={11} c="steel" /><Seams x={4} y={28} w={88} h={10} cols={6} op={0.3} />
      <Grime x={4} y={28} w={88} h={64} seed={2172} n={7} op={0.22} /><Scuff x={6} y={30} w={84} h={58} seed={2173} n={6} c="rust" />
    </svg>
  );
}

function PropLandspeederDepot() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 160 96" width="160" height="96" style={PROP_STYLE}>
      <PropShadow cx={80} cy={90} rx={72} ry={4} />
      <Bev t="polygon" points={pts([[4, 88], [4, 40], [156, 40], [156, 88]])} c="hull" o={1.4} />
      <Hazard x={4} y={40} w={152} h={5} opacity={0.9} /><Slab x={40} y={12} w={80} h={20} k={2} c="dark" o={1.2} />
      <rect x="44" y="15" width="72" height="14" fill={ART.screen.off} /><text x="80" y="25" textAnchor="middle" fontSize="7" fill={S.amberHi} fontWeight="700" {...PT}>KDY LANDSPEEDER DEPOT</text>
      {[16, 64, 112].map((x, i) => (
        <g key={x}>
          <Bev t="polygon" points={pts([[x, 82], [x + 4, 56], [x + 30, 54], [x + 34, 70], [x + 30, 82]])} c="steel" o={0.9} />
          <polygon points={pts([[x + 20, 56], [x + 38, 50], [x + 40, 58], [x + 24, 62]])} fill={ART.deepglass.base} /><rect x={x + 2} y="76" width="30" height="3" fill={ART.dark.shade} />
          <circle cx={x + 8} cy="82" r="3" fill={S.cyan} style={{ animation: `ship-engine ${1.4 + i * 0.3}s ease-in-out infinite` }} /><Glow cx={x + 8} cy={84} r={7} c={S.cyan} opacity={0.4} />
        </g>
      ))}
      <Bev t="polygon" points={pts([[60, 88], [60, 62], [100, 62], [100, 88]])} c="dark" o={0.9} /><Lights x={66} y={66} n={6} gap={5} seed={10} />
      <Greeble x={8} y={46} w={146} h={8} seed={2181} n={10} c="hull" /><Rivets x={8} y={44} w={144} n={18} c="hull" /><Seams x={4} y={40} w={152} h={48} cols={7} op={0.25} /><Vent x={130} y={56} w={20} h={14} n={4} />
      <Grime x={4} y={40} w={152} h={52} seed={2182} n={8} op={0.2} /><Scuff x={6} y={44} w={148} h={44} seed={2183} n={8} c="hull" />
    </svg>
  );
}

function PropEwebTurret({ variant }) {
  const S = ART.signal, linked = variant === 'linked';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={24} ry={3} />
      <Slab x={12} y={44} w={40} h={12} k={2} c="dark" o={1} />
      {[14, 46].map((x) => <polygon key={x} points={pts([[32, 40], [x, 58], [x + 4, 58], [34, 40]])} fill={ART.steel.base} />)}
      <Bev t="polygon" points={pts([[20, 40], [22, 24], [42, 24], [44, 40]])} c="steel" o={1} />
      <rect x="40" y="26" width="22" height="5" fill={ART.dark.shade} /><rect x="40" y="26" width="22" height="1.4" fill={ART.steel.hi} /><rect x="40" y="32" width="22" height="3" fill={ART.dark.shade} />
      {linked && <Glow cx={62} cy={29} r={8} c={S.cyan} opacity={0.7} />}
      <rect x="14" y="26" width="8" height="14" fill={ART.ink} /><rect x="15" y={40 - (linked ? 12 : 5)} width="6" height={linked ? 12 : 5} fill={linked ? S.cyan : S.amber} />
      <rect x="24" y="28" width="14" height="6" fill={ART.screen.cyan} /><text x="31" y="32.6" textAnchor="middle" fontSize="3.2" fill={linked ? S.cyanHi : S.amberHi} fontWeight="700" {...PT}>{linked ? 'x4 LINKED' : 'HEAT 12'}</text>
      <Vent x={24} y={36} w={14} h={5} n={2} /><Lights x={22} y={46} n={5} gap={4.5} seed={11} /><Rivets x={14} y={52} w={36} n={6} c="dark" /><Seams x={12} y={44} w={40} h={12} cols={3} op={0.3} /><Greeble x={22} y={26} w={18} h={4} seed={2191} n={3} c="steel" />
      <Grime x={12} y={24} w={40} h={32} seed={2192} n={4} op={0.22} /><Scuff x={14} y={26} w={36} h={26} seed={2193} n={5} c="steel" />
    </svg>
  );
}

function PropExoSocket() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={22} ry={3} />
      <Slab x={14} y={44} w={36} h={12} k={2} c="dark" o={1} /><Hazard x={16} y={52} w={32} h={3} opacity={0.85} />
      <Bev t="polygon" points={pts([[20, 46], [20, 14], [44, 14], [44, 46]])} c="steel" o={1.2} />
      <rect x="24" y="18" width="16" height="16" fill={ART.ink} /><circle cx="32" cy="26" r="6" fill={ART.dark.base} /><circle cx="32" cy="26" r="3.6" fill={ART.dark.shade} /><circle cx="32" cy="26" r="1.6" fill={S.cyanHi} style={{ animation: 'holo-flicker 1.2s ease-in-out infinite' }} />
      <Glow cx={32} cy={26} r={12} c={S.cyan} opacity={0.35} />
      <rect x="24" y="36" width="16" height="6" fill={ART.screen.cyan} /><text x="32" y="40.8" textAnchor="middle" fontSize="3" fill={S.cyanHi} fontWeight="700" {...PT}>POWER LINK</text>
      <Cable d="M44 30 C56 30, 58 44, 52 54" c={ART.dark.base} w={2.4} /><Vent x={22} y={16} w={6} h={3} n={2} /><Lights x={24} y={48} n={5} gap={4} seed={12} /><Rivets x={22} y={44} w={20} n={4} c="steel" /><Seams x={20} y={14} w={24} h={32} rows={2} op={0.3} /><Greeble x={22} y={34} w={20} h={2} seed={2201} n={2} c="steel" />
      <Grime x={14} y={14} w={36} h={42} seed={2202} n={4} op={0.22} /><Scuff x={18} y={16} w={28} h={34} seed={2203} n={5} c="steel" />
    </svg>
  );
}

function PropRepulsorBarricade() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3} />
      <Bev t="polygon" points={pts([[8, 50], [8, 20], [88, 20], [88, 50]])} c="dark" o={1.4} />
      {[12, 36, 60].map((x, i) => <g key={x}><rect x={x} y="24" width="22" height="22" fill={ART.dark.base} /><rect x={x} y="24" width="22" height="2" fill={ART.dark.hi} /><circle cx={x + 11} cy="36" r="2" fill={ART.dark.shade} /></g>)}
      {[16, 40, 64].map((x, i) => <g key={x}><rect x={x} y="50" width="14" height="5" fill={ART.dark.shade} /><rect x={x + 1} y="53" width="12" height="2.4" fill={S.cyan} style={{ animation: `ship-engine ${1.6 + i * 0.3}s ease-in-out infinite` }} /><Glow cx={x + 7} cy={56} rx={14} ry={3} c={S.cyan} opacity={0.4} /></g>)}
      <Hazard x={8} y={18} w={80} h={4} opacity={0.85} />
      <Lights x={14} y={28} n={3} gap={24} seed={13} /><Greeble x={10} y={24} w={76} h={20} seed={2211} n={6} c="dark" /><Rivets x={10} y={46} w={76} n={9} c="dark" /><Seams x={8} y={20} w={80} h={30} cols={3} op={0.35} /><Vent x={78} y={26} w={8} h={8} n={3} />
      <Grime x={8} y={18} w={80} h={36} seed={2212} n={6} op={0.25} /><Scuff x={10} y={22} w={76} h={26} seed={2213} n={6} c="steel" />
    </svg>
  );
}

function PropDropSpeeder() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={90} rx={56} ry={4} />
      <Bev t="polygon" points={pts([[8, 74], [16, 46], [48, 34], [100, 34], [120, 56], [118, 74]])} c="hull" o={1.4} />
      <polygon points={pts([[26, 46], [50, 38], [70, 38], [70, 54], [28, 56]])} fill={ART.deepglass.base} /><polygon points={pts([[30, 48], [50, 42], [62, 42], [62, 48]])} fill={ART.deepglass.hi} opacity="0.5" />
      <polygon points={pts([[76, 40], [98, 40], [112, 56], [76, 56]])} fill={ART.dark.shade} /><Vent x={80} y={44} w={26} h={10} n={4} />
      <rect x="74" y="12" width="40" height="24" fill={ART.dark.base} /><rect x="74" y="12" width="40" height="3" fill={ART.dark.hi} /><circle cx="94" cy="28" r="7" fill={ART.ink} /><circle cx="94" cy="28" r="4" fill={S.orange} style={{ animation: 'ship-engine 1.8s ease-in-out infinite' }} />
      <Hazard x={10} y={66} w={104} h={5} opacity={0.9} />
      {[30, 90].map((x, i) => <g key={x}><Slab x={x - 12} y={72} w={24} h={8} k={1.4} c="dark" o={0.8} /><rect x={x - 10} y="78" width="20" height="2.4" fill={S.cyan} style={{ animation: `ship-engine ${1.4 + i * 0.3}s ease-in-out infinite` }} /><Glow cx={x} cy={84} rx={18} ry={3} c={S.cyan} opacity={0.4} /></g>)}
      <text x="40" y="64" fontSize="4.4" fill={ART.ink} fontWeight="700" {...PT}>KDY V-19 DROP</text>
      <Greeble x={14} y={50} w={60} h={14} seed={2221} n={6} c="hull" /><Rivets x={12} y={60} w={100} n={14} c="hull" /><Seams x={8} y={34} w={112} h={40} cols={6} rows={1} op={0.25} /><Lights x={80} y={60} n={6} gap={5} seed={14} />
      <Grime x={8} y={34} w={112} h={46} seed={2222} n={7} op={0.22} /><Scuff x={10} y={38} w={108} h={38} seed={2223} n={8} c="hull" />
    </svg>
  );
}

function PropCamoNetting() {
  return (
    <svg viewBox="0 0 256 128" width="256" height="128" style={PROP_STYLE}>
      <polygon points={pts([[8, 8], [248, 12], [250, 70], [220, 88], [180, 84], [150, 120], [100, 110], [70, 118], [30, 96], [6, 64]])} fill={ART.canvas.shade} opacity="0.72" />
      <polygon points={pts([[20, 14], [120, 12], [110, 54], [60, 66], [26, 52]])} fill={ART.canvas.base} opacity="0.55" />
      <polygon points={pts([[130, 20], [240, 24], [236, 66], [190, 74], [140, 56]])} fill={ART.dark.base} opacity="0.55" />
      {Array.from({ length: 18 }, (_, i) => <line key={i} x1={10 + i * 14} y1="8" x2={10 + i * 14 + (i % 3) * 6} y2={90 + (i % 4) * 8} stroke={ART.dark.hi} strokeWidth="0.7" opacity="0.45" />)}
      {[22, 44, 66, 88].map((y) => <line key={y} x1="6" y1={y} x2="250" y2={y + 4} stroke={ART.dark.hi} strokeWidth="0.7" opacity="0.45" />)}
      <Rivets x={10} y={10} w={236} n={16} c="steel" /><Seams x={6} y={8} w={244} h={90} cols={8} rows={2} op={0.2} /><Lights x={30} y={96} n={5} gap={40} seed={15} />
      <Grime x={6} y={8} w={244} h={110} seed={2231} n={8} op={0.15} /><Scuff x={10} y={12} w={236} h={100} seed={2232} n={8} c="canvas" />
    </svg>
  );
}

function PropSteamGrate() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={56} rx={24} ry={3} />
      <Bev t="polygon" points={pts([[8, 54], [8, 24], [56, 24], [56, 54]])} c="dark" o={1.2} />
      <Hazard x={8} y={22} w={48} h={4} opacity={0.9} />
      {[0, 1, 2, 3, 4, 5].map((i) => <rect key={i} x="14" y={30 + i * 3.8} width="36" height="1.8" fill={ART.ink} />)}
      {[22, 32, 42].map((x, i) => <g key={x}><ellipse cx={x} cy="22" rx="8" ry="4" fill={ART.pearl.base} opacity="0.4" style={{ animation: `steam-rise ${2.2 + i * 0.5}s ease-out ${i * 0.6}s infinite` }} /><ellipse cx={x + 3} cy="12" rx="6" ry="3" fill={ART.pearl.base} opacity="0.3" style={{ animation: `steam-rise ${3 + i * 0.4}s ease-out ${i * 0.9}s infinite` }} /></g>)}
      <circle cx="12" cy="50" r="1.8" fill={S.orange} style={{ animation: 'ship-blink 1.4s ease-in-out infinite' }} />
      <Vent x={44} y={44} w={8} h={7} n={2} /><Lights x={16} y={50} n={4} gap={5} seed={16} /><Rivets x={10} y={28} w={44} n={6} c="dark" /><Seams x={8} y={24} w={48} h={30} cols={3} op={0.3} /><Greeble x={10} y={26} w={44} h={3} seed={2241} n={3} c="dark" />
      <Grime x={8} y={24} w={48} h={30} seed={2242} n={4} op={0.3} /><Scuff x={10} y={26} w={44} h={26} seed={2243} n={4} c="dark" />
    </svg>
  );
}

function PropFusionLever({ variant }) {
  const S = ART.signal, blue = variant === 'blue';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={22} ry={3} />
      <Slab x={12} y={38} w={40} h={18} k={2} c="steel" o={1.2} /><Hazard x={14} y={52} w={36} h={3} opacity={0.85} />
      <Bev t="polygon" points={pts([[16, 40], [16, 20], [48, 20], [48, 40]])} c="dark" o={1} />
      <rect x="20" y="24" width="24" height="8" fill={ART.screen.off} /><rect x="22" y="26" width="20" height="4" fill={blue ? S.blue : S.amber} opacity="0.7" style={{ animation: 'holo-flicker 1.6s ease-in-out infinite' }} />
      <text x="32" y="22.6" textAnchor="middle" fontSize="2.6" fill={S.cyanHi} fontWeight="700" {...PT}>FUSION DIVERTER</text>
      <polygon points={pts([[30, 40], [34, 40], [40, 12], [36, 12]])} fill={ART.steel.base} /><circle cx="38" cy="10" r="4" fill={S.red} /><circle cx="38" cy="10" r="2" fill={S.redHi} />
      <polyline points={pts([[40, 14], [44, 8], [42, 4], [48, 0]])} fill="none" stroke={S.amberHi} strokeWidth="0.9" opacity="0.8" style={{ animation: 'ship-spark 1.2s ease-in-out infinite' }} />
      <Lights x={18} y={44} n={5} gap={6} seed={17} colors={blue ? ['cyan'] : ['amber', 'red']} /><Vent x={42} y={42} w={8} h={6} n={2} /><Rivets x={14} y={40} w={36} n={6} c="steel" /><Seams x={12} y={38} w={40} h={18} cols={3} op={0.3} /><Greeble x={14} y={34} w={14} h={3} seed={2251} n={2} c="steel" />
      <Grime x={12} y={20} w={40} h={36} seed={2252} n={4} op={0.22} /><Scuff x={14} y={22} w={36} h={30} seed={2253} n={5} c="steel" />
    </svg>
  );
}

function PropBrigCell({ variant }) {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={90} rx={40} ry={3} />
      <Slab x={6} y={20} w={84} h={68} k={2.5} c="dark" o={1.4} />
      <rect x="12" y="26" width="72" height="56" fill={ART.screen.cyan} />
      {[0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => <rect key={i} x={16 + i * 7.6} y="26" width="2.2" height="56" fill={S.cyan} opacity="0.7" style={{ animation: `holo-flicker ${1.2 + (i % 3) * 0.3}s ease-in-out infinite` }} />)}
      <Glow cx={48} cy={54} rx={40} ry={28} c={S.cyan} opacity={0.3} />
      <Bev t="polygon" points={pts([[16, 76], [16, 66], [46, 66], [46, 76]])} c="steel" o={0.6} /><rect x="16" y="64" width="30" height="3" fill={ART.fabric.base} />
      {variant !== 'empty' && <g><circle cx="68" cy="58" r="5" fill={ART.skin.dark} opacity="0.7" /><rect x="63" y="62" width="10" height="14" fill={ART.rust.base} opacity="0.8" /><rect x="62" y="70" width="12" height="3" fill={S.red} style={{ animation: 'ship-blink 1.2s ease-in-out infinite' }} /></g>}
      <Slab x={36} y={10} w={24} h={10} k={1.4} c="steel" o={0.8} /><rect x="40" y="13" width="16" height="4" fill={ART.screen.off} /><text x="48" y="16.4" textAnchor="middle" fontSize="3" fill={S.cyanHi} fontWeight="700" {...PT}>CELL 3</text>
      <Vent x={76} y={22} w={10} h={6} n={2} /><Lights x={12} y={84} n={6} gap={6} seed={18} /><Rivets x={10} y={22} w={76} n={9} c="dark" /><Seams x={6} y={20} w={84} h={68} cols={3} op={0.3} /><Greeble x={8} y={84} w={80} h={3} seed={2261} n={4} c="dark" />
      <Grime x={6} y={20} w={84} h={68} seed={2262} n={6} op={0.25} /><Scuff x={8} y={22} w={80} h={62} seed={2263} n={6} c="dark" />
    </svg>
  );
}

function PropEncArray() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3} />
      {[8, 34, 60].map((x, i) => (
        <g key={x}>
          <Slab x={x} y={8} w={26} h={48} k={2} c="dark" o={1.2} /><rect x={x + 3} y="12" width="20" height="40" fill={ART.screen.cyan} />
          {Array.from({ length: 7 }, (_, j) => <rect key={j} x={x + 5} y={15 + j * 5.2} width={10 + ((i + j) % 3) * 3} height="2.4" fill={j % 3 ? S.cyan : S.green} opacity="0.8" style={{ animation: `ship-blink ${1.2 + ((i + j) % 4) * 0.4}s ease-in-out infinite` }} />)}
          <Cable d={`M${x + 13} 52 C${x + 13} 58, ${x + 24} 60, ${x + 30} 58`} c={ART.dark.base} w={1.6} />
        </g>
      ))}
      <Vent x={84} y={14} w={8} h={10} n={3} /><Lights x={12} y={54} n={9} gap={9} seed={19} /><Rivets x={10} y={10} w={72} n={9} c="dark" /><Seams x={8} y={8} w={80} h={48} cols={2} op={0.35} /><Greeble x={10} y={50} w={76} h={4} seed={2271} n={4} c="dark" />
      <Grime x={8} y={8} w={80} h={48} seed={2272} n={5} op={0.22} /><Scuff x={10} y={10} w={76} h={44} seed={2273} n={5} c="dark" />
    </svg>
  );
}

function PropFieldDesk() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={58} rx={40} ry={3} />
      <Bev t="polygon" points={pts([[6, 56], [6, 24], [90, 24], [90, 56]])} c="steel" o={1.4} />
      <Slab x={4} y={20} w={88} h={8} k={1.4} c="dark" o={1} />
      <rect x="14" y="14" width="18" height="8" fill={ART.paper.base} /><rect x="14" y="14" width="18" height="1.4" fill={ART.paper.hi} /><rect x="36" y="12" width="14" height="10" fill={ART.screen.cyan} /><rect x="38" y="14" width="10" height="3" fill={S.cyan} opacity="0.8" style={{ animation: 'holo-flicker 2.4s ease-in-out infinite' }} />
      <path d="M58 22 C58 10, 76 10, 76 22 Z" fill={ART.dark.base} /><path d="M60 20 C60 13, 74 13, 74 20 Z" fill={ART.dark.shade} /><polygon points={pts([[62, 14], [70, 14], [68, 12]])} fill={ART.rust.base} opacity="0.7" />
      <rect x="78" y="16" width="10" height="5" fill={ART.dark.shade} /><rect x="80" y="14" width="4" height="3" fill={ART.steel.base} />
      <Bev t="polygon" points={pts([[12, 56], [12, 34], [40, 34], [40, 56]])} c="dark" o={0.8} /><rect x="14" y="42" width="24" height="2" fill={ART.steel.base} /><circle cx="26" cy="38" r="1.4" fill={S.amberHi} />
      <Hazard x={46} y={46} w={40} h={5} opacity={0.85} />
      <Lights x={48} y={32} n={5} gap={8} seed={20} /><Vent x={78} y={34} w={8} h={8} n={3} /><Rivets x={10} y={26} w={76} n={9} c="steel" /><Seams x={6} y={24} w={84} h={32} cols={4} op={0.3} /><Greeble x={46} y={34} w={30} h={8} seed={2281} n={4} c="steel" />
      <Grime x={6} y={24} w={84} h={32} seed={2282} n={5} op={0.25} /><Scuff x={8} y={26} w={80} h={28} seed={2283} n={5} c="steel" />
    </svg>
  );
}

function PropScoutWalker() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 128" width="128" height="128" style={PROP_STYLE}>
      <PropShadow cx={64} cy={122} rx={52} ry={4} />
      {[30, 90].map((x) => <g key={x}><Bev t="polygon" points={pts([[x - 6, 110], [x - 4, 70], [x + 4, 70], [x + 8, 110]])} c="dark" o={0.9} /><Bev t="polygon" points={pts([[x - 16, 122], [x - 14, 108], [x + 16, 108], [x + 18, 122]])} c="steel" o={0.9} /><circle cx={x} cy="70" r="5" fill={ART.brass.base} /></g>)}
      <Bev t="polygon" points={pts([[24, 70], [30, 38], [56, 22], [92, 26], [104, 50], [100, 72]])} c="hull" o={1.4} />
      <polygon points={pts([[36, 54], [48, 36], [70, 34], [74, 52]])} fill={ART.deepglass.base} /><polygon points={pts([[40, 50], [50, 40], [64, 38], [64, 46]])} fill={ART.deepglass.hi} opacity="0.5" />
      <rect x="86" y="44" width="28" height="5" fill={ART.dark.shade} /><rect x="86" y="44" width="28" height="1.4" fill={ART.steel.hi} /><rect x="86" y="52" width="24" height="4" fill={ART.dark.shade} />
      <Hazard x={26} y={64} w={70} h={5} opacity={0.9} /><rect x="78" y="30" width="12" height="10" fill={ART.dark.base} /><circle cx="84" cy="35" r="3" fill={S.orange} style={{ animation: 'ship-blink 1.4s ease-in-out infinite' }} />
      <text x="40" y="62" fontSize="4.4" fill={ART.ink} fontWeight="700" {...PT}>KDY SCOUT 07</text>
      <Vent x={58} y={44} w={16} h={12} n={4} /><Greeble x={26} y={44} w={30} h={16} seed={2291} n={6} c="hull" /><Rivets x={26} y={66} w={72} n={10} c="hull" /><Seams x={24} y={22} w={80} h={50} cols={5} rows={1} op={0.25} /><Lights x={90} y={60} n={4} gap={5} seed={21} />
      <Grime x={20} y={22} w={90} h={100} seed={2292} n={8} op={0.22} /><Scuff x={24} y={26} w={80} h={92} seed={2293} n={8} c="hull" />
    </svg>
  );
}

function PropPistonTower() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 160" width="96" height="160" style={PROP_STYLE}>
      <PropShadow cx={48} cy={154} rx={40} ry={4} />
      <Slab x={10} y={134} w={76} h={22} k={3} c="dark" o={1.4} /><Hazard x={12} y={148} w={72} h={5} opacity={0.9} />
      <Bev t="polygon" points={pts([[24, 136], [24, 18], [72, 18], [72, 136]])} c="copper" o={1.6} />
      <rect x="30" y="24" width="10" height="108" fill={ART.copper.shade} opacity="0.6" />
      <g style={{ animation: 'prop-scan 3.4s ease-in-out infinite alternate' }}>
        <Bev t="polygon" points={pts([[38, 70], [38, 30], [58, 30], [58, 70]])} c="steel" o={1} /><rect x="44" y="8" width="8" height="26" fill={ART.steel.base} /><rect x="44" y="8" width="3" height="26" fill={ART.steel.hi} />
        <Slab x={30} y={66} w={36} h={12} k={1.6} c="brass" o={0.9} /><Rivets x={34} y={72} w={28} n={5} c="brass" />
      </g>
      {[40, 96, 120].map((y) => <Slab key={y} x={20} y={y} w={56} h={8} k={1.4} c="brass" o={0.8} />)}
      {[34, 62].map((x, i) => <g key={x}><circle cx={x} cy="108" r="6" fill={ART.ink} /><circle cx={x} cy="108" r="4" fill={S.orange} style={{ animation: `ship-engine ${1.4 + i * 0.5}s ease-in-out infinite` }} /><Glow cx={x} cy={108} r={11} c={S.orange} opacity={0.4} /></g>)}
      <ellipse cx="50" cy="6" rx="16" ry="5" fill={ART.pearl.base} opacity="0.4" style={{ animation: 'steam-rise 3s ease-out infinite' }} />
      <Vent x={54} y={84} w={16} h={10} n={4} /><Greeble x={26} y={26} w={44} h={8} seed={2301} n={6} c="copper" /><Rivets x={26} y={132} w={44} n={7} c="copper" /><Seams x={24} y={18} w={48} h={118} cols={2} rows={4} op={0.3} /><Lights x={30} y={140} n={6} gap={7} seed={22} />
      <Grime x={10} y={18} w={76} h={138} seed={2302} n={9} op={0.28} /><Scuff x={22} y={20} w={52} h={116} seed={2303} n={8} c="copper" />
    </svg>
  );
}

function PropValveWheel({ variant }) {
  const S = ART.signal, open = variant === 'open';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={22} ry={3} />
      <Slab x={14} y={44} w={36} h={12} k={2} c="dark" o={1} /><Bev t="polygon" points={pts([[26, 46], [26, 24], [38, 24], [38, 46]])} c="copper" o={0.9} />
      <g transform={open ? 'rotate(45 32 22)' : 'rotate(0 32 22)'}>
        <circle cx="32" cy="22" r="17" fill={ART.brass.shade} /><circle cx="32" cy="22" r="14.5" fill={ART.brass.base} /><circle cx="32" cy="22" r="10" fill={ART.dark.shade} />
        {[0, 60, 120].map((a) => <rect key={a} x="29.4" y="6" width="5.2" height="32" fill={ART.brass.hi} transform={`rotate(${a} 32 22)`} />)}
        <circle cx="32" cy="22" r="3.6" fill={ART.brass.hi} />
      </g>
      {[[20, 12], [44, 12], [32, 36]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="2.2" fill={open ? S.cyan : S.amberHi} style={{ animation: `ship-blink ${1.4 + i * 0.4}s ease-in-out infinite` }} />)}
      <Lights x={20} y={50} n={4} gap={6} seed={23} colors={open ? ['cyan'] : ['amber', 'red']} /><Vent x={42} y={46} w={6} h={6} n={2} /><Rivets x={16} y={46} w={32} n={5} c="dark" /><Seams x={14} y={44} w={36} h={12} cols={3} op={0.3} /><Greeble x={16} y={52} w={30} h={3} seed={2311} n={3} c="dark" />
      <Grime x={14} y={6} w={36} h={50} seed={2312} n={4} op={0.22} /><Scuff x={18} y={8} w={28} h={44} seed={2313} n={4} c="brass" />
    </svg>
  );
}

function PropSteamVentPipe() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={22} ry={3} />
      <Slab x={14} y={44} w={36} h={12} k={2} c="dark" o={1} />
      <Bev t="polygon" points={pts([[22, 46], [22, 22], [42, 22], [42, 46]])} c="copper" o={1.2} />
      <Slab x={18} y={18} w={28} h={7} k={1.2} c="brass" o={0.9} /><Slab x={18} y={34} w={28} h={5} k={1} c="brass" o={0.7} />
      <circle cx="32" cy="30" r="5" fill={ART.ink} /><circle cx="32" cy="30" r="3.4" fill={S.red} style={{ animation: 'ship-blink 1s ease-in-out infinite' }} />
      {[24, 32, 40].map((x, i) => <g key={x}><ellipse cx={x} cy="14" rx="7" ry="4" fill={ART.pearl.base} opacity="0.45" style={{ animation: `steam-rise ${1.8 + i * 0.4}s ease-out ${i * 0.5}s infinite` }} /><ellipse cx={x + 2} cy="5" rx="6" ry="3" fill={ART.pearl.base} opacity="0.3" style={{ animation: `steam-rise ${2.4 + i * 0.4}s ease-out ${i * 0.7}s infinite` }} /></g>)}
      <Hazard x={16} y={50} w={32} h={4} opacity={0.9} /><Vent x={36} y={38} w={8} h={5} n={2} /><Lights x={18} y={47} n={5} gap={6} seed={24} /><Rivets x={22} y={24} w={20} n={4} c="copper" /><Seams x={22} y={22} w={20} h={24} rows={2} op={0.3} /><Greeble x={24} y={38} w={14} h={3} seed={2321} n={2} c="copper" />
      <Grime x={14} y={18} w={36} h={38} seed={2322} n={4} op={0.28} /><Scuff x={20} y={20} w={24} h={26} seed={2323} n={4} c="copper" />
    </svg>
  );
}

function PropFoundryHatch() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 160 128" width="160" height="128" style={PROP_STYLE}>
      <PropShadow cx={80} cy={122} rx={72} ry={4} />
      <Bev t="polygon" points={pts([[6, 118], [6, 40], [30, 14], [130, 14], [154, 40], [154, 118]])} c="bronze" o={1.6} />
      <polygon points={pts([[22, 112], [22, 46], [40, 28], [120, 28], [138, 46], [138, 112]])} fill={ART.dark.shade} />
      <polygon points={pts([[40, 112], [40, 50], [54, 38], [106, 38], [120, 50], [120, 112]])} fill={ART.ink} />
      <polygon points={pts([[44, 112], [44, 54], [56, 42], [104, 42], [116, 54], [116, 112]])} fill={S.cyan} opacity="0.2" style={{ animation: 'holo-flicker 2.6s ease-in-out infinite' }} />
      {[48, 62, 76, 90, 104].map((x) => <rect key={x} x={x} y="48" width="2" height="64" fill={ART.steel.base} opacity="0.7" />)}
      <Slab x={52} y={4} w={56} h={14} k={2} c="dark" o={1} /><rect x="56" y="7" width="48" height="8" fill={ART.screen.cyan} /><text x="80" y="13" textAnchor="middle" fontSize="5.2" fill={S.cyanHi} fontWeight="700" {...PT}>FOUNDRY 09</text>
      {[16, 136].map((x, i) => <g key={x}><circle cx={x} cy="58" r="6" fill={ART.ink} /><circle cx={x} cy="58" r="4" fill={S.cyan} style={{ animation: `ship-engine ${1.5 + i * 0.4}s ease-in-out infinite` }} /><Glow cx={x} cy={58} r={14} c={S.cyan} opacity={0.4} /></g>)}
      <Hazard x={8} y={108} w={144} h={8} opacity={0.9} />
      <Greeble x={10} y={20} w={140} h={14} seed={2331} n={9} c="bronze" /><Rivets x={10} y={44} w={140} n={18} c="bronze" /><Seams x={6} y={14} w={148} h={104} cols={6} rows={1} op={0.25} /><Vent x={126} y={68} w={18} h={22} n={5} /><Lights x={20} y={100} n={8} gap={8} seed={25} />
      <Grime x={6} y={14} w={148} h={108} seed={2332} n={9} op={0.25} /><Scuff x={10} y={18} w={140} h={98} seed={2333} n={9} c="bronze" />
    </svg>
  );
}

function PropExhaustCathedral() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 256 224" width="256" height="224" style={PROP_STYLE}>
      <PropShadow cx={128} cy={216} rx={116} ry={5} />
      <Bev t="polygon" points={pts([[28, 214], [40, 90], [74, 60], [182, 60], [216, 90], [228, 214]])} c="dark" o={2} />
      <polygon points={pts([[60, 210], [66, 94], [88, 74], [168, 74], [190, 94], [196, 210]])} fill={ART.rust.shade} />
      {[78, 106, 134, 162].map((x, i) => <g key={x}><rect x={x} y="40" width="18" height="170" fill={ART.copper.base} /><rect x={x} y="40" width="5" height="170" fill={ART.copper.hi} opacity="0.7" /><rect x={x - 3} y={74 + i * 8} width="24" height="10" fill={ART.brass.base} /><rect x={x - 3} y={74 + i * 8} width="24" height="2.4" fill={ART.brass.hi} /></g>)}
      <polygon points={pts([[100, 42], [128, 4], [156, 42]])} fill={ART.dark.base} /><polygon points={pts([[112, 42], [128, 14], [144, 42]])} fill={ART.dark.hi} opacity="0.5" />
      <ellipse cx="128" cy="8" rx="34" ry="8" fill={ART.ink} opacity="0.5" style={{ animation: 'steam-rise 4s ease-out infinite' }} />
      <polygon points={pts([[96, 214], [96, 150], [112, 130], [144, 130], [160, 150], [160, 214]])} fill={ART.ink} /><polygon points={pts([[104, 214], [104, 154], [116, 138], [140, 138], [152, 154], [152, 214]])} fill={S.orange} opacity="0.35" style={{ animation: 'ship-engine 2.4s ease-in-out infinite' }} />
      {[[46, 150], [210, 150], [46, 110], [210, 110]].map(([x, y], i) => <g key={i}><circle cx={x} cy={y} r="6" fill={ART.ink} /><circle cx={x} cy={y} r="4" fill={S.orange} style={{ animation: `ship-blink ${1.3 + i * 0.3}s ease-in-out infinite` }} /><Glow cx={x} cy={y} r={15} c={S.orange} opacity={0.4} /></g>)}
      <Slab x={84} y={92} w={88} h={14} k={2} c="dark" o={1} /><text x="128" y="102.6" textAnchor="middle" fontSize="6" fill={S.amberHi} fontWeight="700" {...PT}>EXHAUST CATHEDRAL</text>
      <Hazard x={30} y={204} w={196} h={8} opacity={0.9} />
      <Greeble x={40} y={64} w={176} h={22} seed={2341} n={12} c="dark" /><Rivets x={40} y={196} w={176} n={22} c="dark" /><Seams x={28} y={60} w={200} h={154} cols={7} rows={3} op={0.2} /><Vent x={170} y={116} w={26} h={30} n={6} /><Lights x={60} y={190} n={10} gap={14} seed={26} />
      <Grime x={28} y={40} w={200} h={174} seed={2342} n={12} op={0.28} /><Scuff x={32} y={44} w={192} h={166} seed={2343} n={12} c="dark" />
    </svg>
  );
}

function PropBoilerHull() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={90} rx={56} ry={4} />
      <Bev t="polygon" points={pts([[8, 84], [8, 36], [24, 18], [104, 18], [120, 36], [120, 84]])} c="copper" o={1.6} />
      {[26, 52, 78, 100].map((x) => <rect key={x} x={x} y="18" width="4" height="66" fill={ART.copper.shade} opacity="0.7" />)}
      <circle cx="64" cy="48" r="16" fill={ART.dark.shade} /><circle cx="64" cy="48" r="12" fill={ART.ink} /><circle cx="64" cy="48" r="8" fill={S.orange} opacity="0.4" style={{ animation: 'ship-engine 2s ease-in-out infinite' }} />
      <Glow cx={64} cy={48} r={26} c={S.orange} opacity={0.35} />
      <circle cx="26" cy="64" r="6" fill={ART.ink} /><circle cx="26" cy="64" r="3.6" fill={S.cyan} style={{ animation: 'holo-flicker 1.6s ease-in-out infinite' }} /><circle cx="102" cy="64" r="6" fill={ART.ink} /><circle cx="102" cy="64" r="3.6" fill={S.red} style={{ animation: 'ship-blink 1.2s ease-in-out infinite' }} />
      <Slab x={44} y={72} w={40} h={10} k={1.4} c="brass" o={0.8} /><rect x="48" y="74" width="32" height="6" fill={ART.screen.amber} /><text x="64" y="79" textAnchor="middle" fontSize="3.6" fill={S.amberHi} fontWeight="700" {...PT}>DEAD BOILER</text>
      <Hazard x={10} y={82} w={108} h={4} opacity={0.9} />
      <Vent x={96} y={26} w={16} h={12} n={4} /><Greeble x={12} y={22} w={80} h={10} seed={2351} n={7} c="copper" /><Rivets x={12} y={34} w={104} n={14} c="copper" /><Seams x={8} y={18} w={112} h={66} cols={5} op={0.3} /><Lights x={14} y={78} n={4} gap={6} seed={27} />
      <Grime x={8} y={18} w={112} h={70} seed={2352} n={8} op={0.28} /><Scuff x={12} y={22} w={104} h={60} seed={2353} n={8} c="copper" />
    </svg>
  );
}

function PropGovernorSocket({ variant }) {
  const S = ART.signal, on = variant === 'installed';
  return (
    <svg viewBox="0 0 64 64" width="64" height="64" style={PROP_STYLE}>
      <PropShadow cx={32} cy={58} rx={22} ry={3} />
      <Slab x={12} y={42} w={40} h={14} k={2} c="dark" o={1.2} /><Hazard x={14} y={52} w={36} h={3} opacity={0.85} />
      <Bev t="polygon" points={pts([[20, 44], [20, 16], [44, 16], [44, 44]])} c="brass" o={1.2} />
      <circle cx="32" cy="28" r="12" fill={ART.dark.shade} />
      {on ? (
        <g><circle cx="32" cy="28" r="11" fill={ART.brass.base} />{[0, 60, 120].map((a) => <rect key={a} x="29.8" y="18" width="4.4" height="20" fill={ART.brass.hi} transform={`rotate(${a} 32 28)`} />)}{[[26, 24], [38, 24], [32, 36]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="2.4" fill={S.cyan} style={{ animation: `holo-flicker ${1.2 + i * 0.3}s ease-in-out infinite` }} />)}<Glow cx={32} cy={28} r={20} c={S.cyan} opacity={0.45} /></g>
      ) : (
        <g><circle cx="32" cy="28" r="8" fill={ART.ink} /><circle cx="32" cy="28" r="3" fill={S.red} style={{ animation: 'ship-blink 1.1s ease-in-out infinite' }} /></g>
      )}
      <Slab x={22} y={46} w={20} h={6} k={1} c="steel" o={0.6} /><text x="32" y="50.6" textAnchor="middle" fontSize="2.8" fill={ART.ink} fontWeight="700" {...PT}>{on ? 'GOVERNOR OK' : 'GOVERNOR OFF'}</text>
      <Vent x={46} y={22} w={5} h={8} n={3} /><Lights x={14} y={40} n={5} gap={6} seed={28} /><Rivets x={22} y={18} w={20} n={4} c="brass" /><Seams x={20} y={16} w={24} h={28} cols={1} op={0.3} /><Greeble x={22} y={40} w={20} h={3} seed={2361} n={3} c="brass" />
      <Grime x={12} y={16} w={40} h={40} seed={2362} n={4} op={0.25} /><Scuff x={16} y={18} w={32} h={34} seed={2363} n={4} c="brass" />
    </svg>
  );
}

// Wastes finale set piece: the central blast doors to Zone Five. Closed: heat locked, red lamps. Open: leaves retracted, a lit descent.
function PropWstBlastDoors({ variant }) {
  const S = ART.signal, open = variant === 'open';
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={92} rx={44} ry={3} />
      <Slab x={2} y={14} w={92} h={76} k={4} c="dark" o={1.4} />
      <Slab x={8} y={22} w={80} h={68} k={3} c="steel" o={1} />
      <rect x="16" y="30" width="64" height="60" fill={open ? ART.screen.amber : ART.dark.shade} />
      {open ? (
        <g>
          <polygon points={pts([[20, 90], [28, 40], [68, 40], [76, 90]])} fill={ART.ink} />
          {[48, 60, 72, 84].map((y, i) => <rect key={y} x={30 - i * 2.4} y={y} width={36 + i * 4.8} height="3" fill={ART.dark.base} />)}
          {[0, 1, 2, 3].map((i) => <circle key={i} cx={36 + i * 8} cy={46 + i * 0} r="1.2" fill={S.amberHi} style={{ animation: `ship-blink ${1.2 + i * 0.3}s ease-in-out infinite` }} />)}
          <Glow cx={48} cy={64} rx={20} ry={26} c={S.orange} opacity={0.55} style={{ animation: 'ship-engine 2.4s ease-in-out infinite' }} />
          <Bev t="polygon" points={pts([[16, 30], [26, 30], [26, 90], [16, 90]])} c="hull" o={1} />
          <Bev t="polygon" points={pts([[70, 30], [80, 30], [80, 90], [70, 90]])} c="hull" o={1} />
        </g>
      ) : (
        <g>
          <Bev t="polygon" points={pts([[16, 30], [47, 30], [47, 90], [16, 90]])} c="hull" o={1.4} />
          <Bev t="polygon" points={pts([[49, 30], [80, 30], [80, 90], [49, 90]])} c="hull" o={1.4} />
          <rect x="46.4" y="30" width="3.2" height="60" fill={S.orange} style={{ animation: 'ship-engine 1.8s ease-in-out infinite' }} />
          {[40, 56, 72].map((y) => <g key={y}><rect x="20" y={y} width="22" height="2.4" fill={ART.hull.shade} /><rect x="54" y={y} width="22" height="2.4" fill={ART.hull.shade} /></g>)}
          <polygon points={pts([[20, 34], [40, 34], [38, 38], [22, 38]])} fill={ART.rust.base} opacity="0.7" /><polygon points={pts([[56, 34], [76, 34], [74, 38], [58, 38]])} fill={ART.rust.base} opacity="0.7" />
        </g>
      )}
      <Slab x={26} y={16} w={44} h={10} k={2} c="dark" o={0.9} />
      <text x="48" y="23.6" textAnchor="middle" fontSize="4.4" fill={open ? S.greenHi : S.redHi} fontWeight="700" {...PT}>{open ? 'ZONE 05 OPEN' : 'ZONE 05 HEAT LOCKED'}</text>
      {[14, 82].map((x) => <g key={x}><circle cx={x} cy="20" r="3" fill={ART.ink} /><circle cx={x} cy="20" r="1.8" fill={open ? S.green : S.red} style={{ animation: 'ship-blink 1.1s ease-in-out infinite' }} /><Glow cx={x} cy={20} r={7} c={open ? S.green : S.red} opacity={0.4} /></g>)}
      <Hazard x={10} y={82} w={76} h={6} opacity={0.9} />
      <Vent x={4} y={42} w={8} h={12} n={4} /><Vent x={84} y={42} w={8} h={12} n={4} />
      <Rivets x={12} y={26} w={72} n={10} c="steel" /><Seams x={8} y={22} w={80} h={68} cols={open ? 0 : 3} rows={0} op={0.3} />
      <Greeble x={10} y={60} w={4} h={20} seed={3411} n={3} c="steel" /><Greeble x={82} y={60} w={4} h={20} seed={3412} n={3} c="steel" />
      <Grime x={2} y={14} w={92} h={76} seed={3413} n={7} op={0.24} /><Scuff x={10} y={24} w={76} h={62} seed={3414} n={8} c="steel" />
    </svg>
  );
}
// ===== BADOR WAR PROPS END =====
// ===== BASE PROPS BEGIN =====
function PropBaseCureBench() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={61} rx={44} ry={2.8} />
      {[10, 78].map((x) => <Bev key={x} t="polygon" points={pts([[x, 60], [x, 38], [x + 8, 38], [x + 8, 60]])} c="dark" o={0.7} />)}
      <Bev t="polygon" points={pts([[4, 42], [92, 42], [88, 34], [8, 34]])} c="steel" o={1} />
      <Slab x={4} y={42} w={88} h={7} k={1} c="dark" o={0.7} />
      <Seams x={4} y={42} w={88} h={7} cols={7} op={0.4} />
      {/* brass centrifuge */}
      <Slab x={10} y={22} w={26} h={12} k={2} c="brass" o={1} />
      <polygon points={pts(ngon(23, 20, 12, 4.4, 10))} fill={ART.brass.shade} />
      <Bev t="polygon" points={pts(ngon(23, 18, 11, 4, 10))} c="brass" o={1} />
      <polygon points={pts(ngon(23, 18, 6.4, 2.2, 10))} fill={ART.dark.shade} />
      <circle cx="23" cy="18" r="1.4" fill={S.cyan} style={{ animation: 'ship-blink 1.6s steps(2) infinite' }} />
      <rect x="14" y="26" width="18" height="4" fill={ART.screen.cyan} /><rect x="15.4" y="27.2" width="8" height="1.6" fill={S.cyan} />
      {/* mortar of blue crystal dust */}
      <Slab x={42} y={26} w={16} h={9} k={1.6} c="dark" o={0.9} />
      {[[44, 26, 48, 20, 50, 26], [49, 26, 53, 18, 55, 26], [52, 26, 56, 22, 58, 26]].map((p, i) => <polygon key={i} points={pts([[p[0], p[1]], [p[2], p[3]], [p[4], p[5]]])} fill={i === 1 ? S.cyanHi : S.cyan} opacity="0.95" />)}
      <Glow cx={52} cy={26} rx={9} ry={3} c={S.cyan} opacity={0.4} />
      {/* rack of amber vials */}
      <Slab x={64} y={20} w={26} h={14} k={1.6} c="dark" o={0.8} />
      {[0, 1, 2, 3, 4].map((i) => <g key={i}><rect x={66 + i * 4.6} y="22" width="3" height="9" fill={ART.amber.base} /><rect x={66.4 + i * 4.6} y="22.4" width="1.2" height="8" fill={ART.amber.hi} /><rect x={66 + i * 4.6} y="20.6" width="3" height="1.6" fill={ART.steel.hi} /></g>)}
      <Slab x={38} y={44} w={22} h={5} k={1} c="paper" o={0.6} /><rect x="40" y="45.4" width="18" height="1" fill={ART.note.ink} />
      <Vent x={14} y={51} w={20} h={7} n={3} /><Vent x={56} y={51} w={20} h={7} n={3} /><Rivets x={7} y={44} w={82} n={10} />
      <Lights x={64} y={36} n={5} gap={4} r={0.7} seed={14} />
      <Grime x={4} y={34} w={88} h={25} seed={931} n={6} op={0.22} /><Scuff x={6} y={36} w={84} h={18} seed={932} n={8} c="steel" />
    </svg>
  );
}
// ===== BASE PROPS END =====

// ===== MFG PROPS BEGIN =====
function PropMfgConveyor({ variant }) {
  const S = ART.signal, hot = variant === 'ingots';
  return (
    <svg viewBox="0 0 128 64" width="128" height="64" style={PROP_STYLE}>
      <PropShadow cx={64} cy={60} rx={58} ry={3} />
      {[10, 112].map((x) => <Bev key={x} t="polygon" points={pts([[x, 58], [x, 40], [x + 6, 40], [x + 6, 58]])} c="steel" o={0.7} />)}
      <Slab x={2} y={30} w={124} h={22} k={3} c="dark" o={1.2} />
      <rect x="6" y="34" width="116" height="12" fill={ART.ink} />
      {Array.from({ length: 14 }, (_, i) => <polygon key={i} points={pts([[10 + i * 8, 35], [14 + i * 8, 35], [18 + i * 8, 40], [14 + i * 8, 45], [10 + i * 8, 45], [14 + i * 8, 40]])} fill={ART.amber.base} opacity="0.8" style={{ animation: `ship-blink ${2.4 + (i % 3) * 0.5}s steps(2) infinite` }} />)}
      <Slab x={2} y={26} w={124} h={6} k={1.4} c="steel" o={0.9} />
      {hot ? (
        <g>
          {[16, 52, 88].map((x, i) => <g key={x}><Slab x={x} y={14} w={24} h={14} k={2} c="rust" o={1} /><rect x={x + 3} y="16" width="18" height="4" fill={S.orange} /><Glow cx={x + 12} cy={20} r={18} c={S.orange} opacity={0.45} style={{ animation: `ship-engine ${1.8 + i * 0.4}s ease-in-out infinite` }} /></g>)}
        </g>
      ) : (
        <g>
          {[[14, 12, 30], [48, 8, 34], [86, 14, 28]].map(([x, y, w]) => <g key={x}><Slab x={x} y={y} w={w} h={16} k={2} c="hull" o={1} /><Slab x={x + 3} y={y - 5} w={w - 8} h={7} k={1.4} c="steel" o={0.8} /><Seams x={x} y={y} w={w} h={16} cols={2} op={0.4} /></g>)}
        </g>
      )}
      <Hazard x={6} y={50} w={116} h={4} opacity={0.9} />
      <Vent x={104} y={32} w={14} h={8} n={3} /><Lights x={10} y={56} n={8} gap={14} seed={31} /><Rivets x={6} y={29} w={116} n={16} c="steel" /><Seams x={2} y={30} w={124} h={22} cols={7} op={0.3} /><Greeble x={8} y={46} w={90} h={4} seed={3101} n={5} c="dark" />
      <Grime x={2} y={14} w={124} h={44} seed={3102} n={7} op={0.26} /><Scuff x={4} y={16} w={118} h={36} seed={3103} n={7} c="steel" />
    </svg>
  );
}

function PropMfgCradle({ variant }) {
  const S = ART.signal, younger = variant === 'b';
  const ribs = [30, 44, 58, 72, 86, 100, 114, 128];
  const top = (x) => 34 + Math.abs(x - 80) * 0.45;
  return (
    <svg viewBox="0 0 160 128" width="160" height="128" style={PROP_STYLE}>
      <PropShadow cx={80} cy={122} rx={72} ry={4} />
      <Slab x={6} y={106} w={148} h={14} k={3} c="dark" o={1.2} /><Hazard x={10} y={114} w={140} h={4} opacity={0.9} />
      {[6, 142].map((x) => <g key={x}><Bev t="polygon" points={pts([[x, 108], [x, 16], [x + 12, 16], [x + 12, 108]])} c="amber" o={1.2} /><Seams x={x} y={16} w={12} h={92} rows={7} op={0.4} /></g>)}
      <Slab x={2} y={8} w={156} h={10} k={1.6} c="amber" o={1} />
      <Bev t="polygon" points={pts([[24, 106], [24, 62], [46, 36], [114, 36], [136, 62], [136, 106]])} c="dark" o={1.4} />
      <polygon points={pts([[30, 104], [30, 64], [48, 42], [112, 42], [130, 64], [130, 104]])} fill={ART.ink} />
      {ribs.map((x) => <rect key={x} x={x - 2.5} y={top(x) + 4} width="5" height={104 - top(x) - 4} fill={ART.steel.base} />)}
      {ribs.map((x) => <rect key={'h' + x} x={x - 2.5} y={top(x) + 4} width="1.8" height={104 - top(x) - 4} fill={ART.steel.hi} opacity="0.7" />)}
      {(younger ? [[26, 86, 22, 18], [50, 84, 24, 20]] : [[26, 66, 22, 18], [26, 86, 22, 18], [50, 42, 24, 18], [50, 62, 24, 20], [50, 84, 24, 20], [76, 40, 20, 20], [76, 62, 20, 16]]).map(([x, y, w, h], i) => <g key={i}><Slab x={x} y={y} w={w} h={h} k={1.6} c="hull" o={0.9} /><Seams x={x} y={y} w={w} h={h} rows={1} op={0.35} /></g>)}
      <circle cx="102" cy="58" r="3.4" fill={S.amberHi} style={{ animation: 'ship-blink 0.9s steps(2) infinite' }} /><Glow cx={102} cy={58} r={16} c={S.orange} opacity={0.5} />
      <Slab x={52} y={88} w={56} h={14} k={1.4} c="steel" o={0.9} />
      <text x="80" y="95" textAnchor="middle" fontSize="5.6" fill={ART.ink} fontWeight="700" {...PT}>{younger ? 'SECTION 12' : 'SECTION 11'}</text>
      <text x="80" y="100" textAnchor="middle" fontSize="2.8" fill={ART.red.base} fontWeight="700" {...PT}>NO REGISTRY</text>
      <Vent x={118} y={72} w={10} h={14} n={4} /><Lights x={14} y={110} n={10} gap={14} seed={32} /><Rivets x={24} y={64} w={112} n={14} c="steel" /><Greeble x={26} y={20} w={110} h={12} seed={3201} n={8} c="amber" /><Seams x={24} y={36} w={112} h={70} cols={6} op={0.2} />
      <Grime x={6} y={8} w={148} h={112} seed={3202} n={10} op={0.28} /><Scuff x={26} y={40} w={108} h={64} seed={3203} n={9} c="steel" />
    </svg>
  );
}

function PropMfgForge() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={90} rx={56} ry={4} />
      <Bev t="polygon" points={pts([[86, 64], [90, 8], [112, 8], [116, 64]])} c="dark" o={1.4} />
      <ellipse cx="102" cy="6" rx="14" ry="4" fill={ART.pearl.base} opacity="0.4" style={{ animation: 'steam-rise 3.2s ease-out infinite' }} />
      <Bev t="polygon" points={pts([[8, 86], [8, 42], [26, 24], [102, 24], [120, 42], [120, 86]])} c="rust" o={1.6} />
      <polygon points={pts([[28, 82], [28, 52], [40, 40], [88, 40], [100, 52], [100, 82]])} fill={ART.dark.shade} />
      <polygon points={pts([[34, 82], [34, 54], [44, 44], [84, 44], [94, 54], [94, 82]])} fill={ART.ink} />
      <polygon points={pts([[38, 82], [38, 56], [46, 48], [82, 48], [90, 56], [90, 82]])} fill={S.orange} opacity="0.55" style={{ animation: 'ship-engine 2.2s ease-in-out infinite' }} />
      <polygon points={pts([[46, 82], [46, 62], [52, 56], [76, 56], [82, 62], [82, 82]])} fill={S.amberHi} opacity="0.5" />
      <Glow cx={64} cy={62} r={38} c={S.orange} opacity={0.4} />
      <Bev t="polygon" points={pts([[100, 56], [124, 56], [124, 66], [100, 66]])} c="copper" o={0.9} />
      <rect x="118" y="66" width="4" height="18" fill={S.orange} opacity="0.9" style={{ animation: 'ship-engine 1.4s ease-in-out infinite' }} />
      <Slab x={104} y={82} w={20} h={6} k={1.2} c="rust" o={0.8} />
      <Slab x={38} y={26} w={52} h={11} k={1.6} c="dark" o={1} />
      <text x="64" y="34" textAnchor="middle" fontSize="5" fill={S.amberHi} fontWeight="700" {...PT}>FOUNDRY 4</text>
      <Hazard x={10} y={84} w={90} h={4} opacity={0.9} />
      <Vent x={10} y={48} w={14} h={16} n={4} /><Lights x={14} y={78} n={4} gap={6} seed={33} /><Rivets x={12} y={40} w={104} n={14} c="rust" /><Seams x={8} y={24} w={112} h={62} cols={5} op={0.28} /><Greeble x={12} y={28} w={22} h={10} seed={3301} n={5} c="rust" />
      <Grime x={8} y={8} w={112} h={80} seed={3302} n={9} op={0.3} /><Scuff x={12} y={26} w={104} h={56} seed={3303} n={8} c="rust" />
    </svg>
  );
}

function PropMfgGlass() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 160 64" width="160" height="64" style={PROP_STYLE}>
      <PropShadow cx={80} cy={61} rx={72} ry={2.6} />
      <Bev t="polygon" points={pts([[2, 30], [2, 0], [158, 0], [158, 30]])} c="dark" o={1} />
      {[[6, 4, 52], [130, 4, 24]].map(([x, y, w], i) => <g key={i}><rect x={x} y={y} width={w} height="24" fill={ART.glass.shade} /><rect x={x + 1.5} y={y + 1.5} width={w - 3} height="21" fill={ART.amber.base} opacity="0.55" /><rect x={x + 1.5} y={y + 1.5} width={w - 3} height="5" fill={ART.glass.hi} opacity="0.5" />{Array.from({ length: Math.floor(w / 8) }, (_, k) => <rect key={k} x={x + 4 + k * 8} y={y + 8} width="4" height="14" fill={ART.dark.base} opacity="0.7" />)}</g>)}
      <Bev t="polygon" points={pts([[62, 2], [130, 2], [130, 16], [62, 16]])} c="dark" o={0.8} />
      <text x="96" y="9.6" textAnchor="middle" fontSize="4.4" fill={S.amberHi} fontWeight="700" {...PT}>OVERSEER SECTOR 4</text>
      <text x="96" y="14" textAnchor="middle" fontSize="2.6" fill={S.redHi} {...PT}>APPOINTMENTS ONLY</text>
      <Hazard x={62} y={20} w={68} h={3} opacity={0.9} />
      <Slab x={4} y={36} w={56} h={22} k={2.4} c="brass" o={1} /><Slab x={128} y={36} w={28} h={22} k={2.4} c="brass" o={1} />
      <rect x="8" y="40" width="48" height="4" fill={ART.brass.hi} opacity="0.7" /><rect x="132" y="40" width="20" height="4" fill={ART.brass.hi} opacity="0.7" />
      <rect x="14" y="46" width="14" height="7" fill={ART.screen.amber} /><rect x="16" y="48" width="10" height="1.4" fill={S.amberHi} /><rect x="16" y="50.4" width="7" height="1.4" fill={S.amberHi} />
      <Lights x={36} y={50} n={4} gap={5} seed={34} /><Vent x={136} y={46} w={12} h={7} n={3} />
      <Rivets x={6} y={33} w={150} n={18} c="brass" /><Seams x={4} y={36} w={152} h={22} cols={5} op={0.25} /><Greeble x={6} y={4} w={150} h={3} seed={3401} n={5} c="dark" />
      <Grime x={2} y={0} w={156} h={60} seed={3402} n={7} op={0.26} /><Scuff x={4} y={36} w={152} h={22} seed={3403} n={6} c="brass" />
    </svg>
  );
}

function PropMfgKeel() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 160 96" width="160" height="96" style={PROP_STYLE}>
      <PropShadow cx={80} cy={90} rx={72} ry={4} />
      <Slab x={6} y={74} w={148} h={14} k={3} c="dark" o={1.2} /><Hazard x={10} y={82} w={140} h={4} opacity={0.9} />
      {[20, 50, 80, 110, 136].map((x) => <Slab key={x} x={x} y={62} w={8} h={16} k={1.2} c="steel" o={0.8} />)}
      <Bev t="polygon" points={pts([[8, 70], [24, 40], [62, 24], [138, 24], [154, 46], [150, 70]])} c="hull" o={1.6} />
      <polygon points={pts([[28, 62], [40, 44], [66, 34], [132, 34], [142, 48], [138, 62]])} fill={ART.steel.base} />
      <polygon points={pts([[28, 62], [40, 44], [66, 34], [74, 34], [66, 62]])} fill={ART.steel.hi} opacity="0.6" />
      <text x="88" y="53" textAnchor="middle" fontSize="8" fill={ART.red.base} fontWeight="700" {...PT}>GK-01 KEEL</text>
      <text x="88" y="61" textAnchor="middle" fontSize="3.2" fill={ART.ink} fontWeight="700" {...PT}>NO COMMISSION NUMBER</text>
      {[[18, 66], [146, 62]].map(([x, y], i) => <g key={i}><circle cx={x} cy={y} r="2.4" fill={S.amberHi} style={{ animation: `ship-blink ${1.2 + i * 0.4}s steps(2) infinite` }} /><Glow cx={x} cy={y} r={9} c={S.amber} opacity={0.4} /></g>)}
      <Vent x={118} y={26} w={14} h={7} n={3} /><Lights x={30} y={78} n={9} gap={12} seed={35} /><Rivets x={20} y={32} w={120} n={14} c="hull" /><Greeble x={66} y={26} w={50} h={6} seed={3501} n={6} c="hull" /><Seams x={8} y={24} w={146} h={46} cols={8} rows={1} op={0.3} />
      <Grime x={6} y={24} w={148} h={64} seed={3502} n={9} op={0.28} /><Scuff x={10} y={28} w={140} h={42} seed={3503} n={9} c="hull" />
    </svg>
  );
}
// ===== MFG PROPS END =====



















const PROP_DEFS = {
  fuel_rig:           { w: 2, h: 2, ax: 1, ay: 1 },
  manifest_terminal:  { w: 2, h: 2, ax: 1, ay: 1 },
  vectis:             { w: 2, h: 3, ax: 0, ay: 2 },
  impound_door:       { w: 3, h: 2, ax: 1, ay: 1 },
  orbital_viewport:   { w: 3, h: 2, ax: 1, ay: 1 },
  tether_lockers:     { w: 3, h: 2, ax: 0, ay: 0 },
  transit_pod:        { w: 3, h: 2, ax: 1, ay: 1 },
  welding_rig:        { w: 2, h: 2, ax: 0, ay: 1 },
  wayfinding_plaque:  { w: 2, h: 2, ax: 0, ay: 1 },
  blast_panel:        { w: 2, h: 2, ax: 0, ay: 1 },
  holo_projector:     { w: 3, h: 3, ax: 1, ay: 2 },
  secure_locker:      { w: 3, h: 2, ax: 1, ay: 1 },
  intercept_wall:     { w: 4, h: 2, ax: 1, ay: 1 },
  terminal:           { w: 2, h: 2, ax: 0, ay: 1 },
  holo_table:         { w: 3, h: 2, ax: 1, ay: 1 },
  ordnance_crates:    { w: 3, h: 2, ax: 1, ay: 1 },
  armory_locker:      { w: 2, h: 2, ax: 0, ay: 1 },
  cable_truss:        { w: 4, h: 2, ax: 1, ay: 1 },
  coolant_pipe:       { w: 3, h: 2, ax: 1, ay: 1 },
  sabacc_table:       { w: 3, h: 3, ax: 1, ay: 1 },
  market_stall:       { w: 3, h: 2, ax: 1, ay: 1 },
  shift_board:        { w: 3, h: 2, ax: 1, ay: 1 },
  union_bulletin:     { w: 2, h: 2, ax: 0, ay: 1 },
  surveillance_node:  { w: 2, h: 2, ax: 0, ay: 0 },
  forge_bar:          { w: 4, h: 2, ax: 1, ay: 1 },
  czerka_terminal:    { w: 2, h: 2, ax: 0, ay: 0 },
  fuel_rack:          { w: 3, h: 2, ax: 1, ay: 1 },
  drive_cradle:       { w: 3, h: 3, ax: 1, ay: 2 },
  crawler_port:       { w: 2, h: 2, ax: 0, ay: 0 },
  rivet_hopper:       { w: 3, h: 3, ax: 1, ay: 2 },
  republic_terminal:  { w: 2, h: 2, ax: 0, ay: 1 },
  shipyard_viewport:  { w: 3, h: 2, ax: 1, ay: 1 },
  executive_safe:     { w: 2, h: 2, ax: 0, ay: 1 },
  model_case:         { w: 4, h: 2, ax: 1, ay: 1 },
  bronze_mural:       { w: 3, h: 2, ax: 0, ay: 1 },
  panoramic_viewport: { w: 4, h: 2, ax: 1, ay: 1 },
  protocol_droid:     { w: 1, h: 2, ax: 0, ay: 1 },
  // CORUSCANT DEFS BEGIN
  console:              { w: 2, h: 2, ax: 0, ay: 1 },
  kiosk:                { w: 3, h: 2, ax: 1, ay: 1 },
  airtaxi:              { w: 2, h: 2, ax: 0, ay: 1 },
  comm_relay:           { w: 2, h: 2, ax: 0, ay: 1 },
  server_stack:         { w: 2, h: 3, ax: 0, ay: 2 },
  board:                { w: 3, h: 2, ax: 1, ay: 1 },
  sign_array:           { w: 4, h: 2, ax: 1, ay: 1 },
  shopfront:            { w: 4, h: 2, ax: 1, ay: 1 },
  apt_door:             { w: 2, h: 2, ax: 0, ay: 1 },
  wall_marks:           { w: 3, h: 2, ax: 1, ay: 1 },
  skyline:              { w: 4, h: 2, ax: 1, ay: 1 },
  elevator_door:        { w: 2, h: 2, ax: 0, ay: 0 },
  datapad_table:        { w: 2, h: 2, ax: 0, ay: 1 },
  crate_stack:          { w: 3, h: 2, ax: 1, ay: 1 },
  container_stack:      { w: 4, h: 2, ax: 1, ay: 1 },
  cargo_container:      { w: 3, h: 2, ax: 1, ay: 1 },
  drum_array:           { w: 3, h: 2, ax: 1, ay: 1 },
  hab_block:            { w: 4, h: 3, ax: 1, ay: 2 },
  evidence_locker:      { w: 3, h: 2, ax: 1, ay: 1 },
  weapon_rack:          { w: 3, h: 2, ax: 1, ay: 1 },
  medic_crate:          { w: 2, h: 2, ax: 0, ay: 1 },
  scrap_bin:            { w: 2, h: 2, ax: 0, ay: 1 },
  fuel_hose:            { w: 2, h: 2, ax: 0, ay: 1 },
  storage_alcoves:      { w: 3, h: 2, ax: 1, ay: 1 },
  archive_cabinet:      { w: 2, h: 2, ax: 0, ay: 1 },
  workbench:            { w: 3, h: 2, ax: 1, ay: 1 },
  cargo_lift:           { w: 3, h: 2, ax: 1, ay: 1 },
  vault_door:           { w: 3, h: 3, ax: 1, ay: 2 },
  security_gate:        { w: 3, h: 2, ax: 1, ay: 1 },
  junction_box:         { w: 2, h: 2, ax: 0, ay: 1 },
  leaking_pipe:         { w: 3, h: 2, ax: 1, ay: 1 },
  pressure_valve:       { w: 2, h: 2, ax: 0, ay: 1 },
  steam_vent:           { w: 2, h: 2, ax: 0, ay: 1 },
  access_hatch:         { w: 2, h: 2, ax: 0, ay: 1 },
  scanner_arch:         { w: 3, h: 2, ax: 1, ay: 1 },
  damper_array:         { w: 3, h: 2, ax: 1, ay: 1 },
  power_tap:            { w: 2, h: 2, ax: 0, ay: 1 },
  drop_shaft:           { w: 3, h: 2, ax: 1, ay: 1 },
  catwalk_junction:     { w: 3, h: 3, ax: 1, ay: 2 },
  floor_grate:          { w: 2, h: 2, ax: 0, ay: 1 },
  drain_channel:        { w: 3, h: 2, ax: 1, ay: 1 },
  support_pillar:       { w: 2, h: 3, ax: 0, ay: 2 },
  radiator:             { w: 3, h: 2, ax: 1, ay: 1 },
  fluid_slick:          { w: 3, h: 2, ax: 1, ay: 1 },
  crane_arm:            { w: 4, h: 3, ax: 1, ay: 2 },
  landing_beacon:       { w: 2, h: 2, ax: 0, ay: 1 },
  ceiling_tap:          { w: 2, h: 3, ax: 0, ay: 2 },
  tactical_table:       { w: 3, h: 3, ax: 1, ay: 2 },
  vendor_stall:         { w: 3, h: 2, ax: 1, ay: 1 },
  gorg_spit:            { w: 3, h: 2, ax: 1, ay: 1 },
  booth:                { w: 3, h: 2, ax: 1, ay: 1 },
  guard_post:           { w: 3, h: 2, ax: 1, ay: 1 },
  interrogation_rig:    { w: 3, h: 2, ax: 1, ay: 1 },
  ceremony_dais:        { w: 3, h: 2, ax: 1, ay: 1 },
  monitor_wall:         { w: 4, h: 2, ax: 1, ay: 1 },
  roster_wall:          { w: 3, h: 2, ax: 1, ay: 1 },
  trophy_wall:          { w: 4, h: 2, ax: 1, ay: 1 },
  duel_ring:            { w: 3, h: 3, ax: 1, ay: 2 },
  arena_ring:           { w: 5, h: 4, ax: 2, ay: 3 },
  pit_gate:             { w: 3, h: 2, ax: 1, ay: 1 },
  rail_run:             { w: 4, h: 2, ax: 1, ay: 1 },
  fighter_altar:        { w: 2, h: 2, ax: 0, ay: 1 },
  kyber_cluster:        { w: 3, h: 2, ax: 1, ay: 1 },
  survey_marker:        { w: 2, h: 2, ax: 0, ay: 1 },
  containment_seal:     { w: 3, h: 2, ax: 1, ay: 1 },
  ruin_stone:           { w: 3, h: 2, ax: 1, ay: 1 },
  outcast_camp:         { w: 4, h: 3, ax: 1, ay: 2 },
  lux_skiff:            { w: 4, h: 3, ax: 2, ay: 2 },
  speeder_wreck:        { w: 4, h: 2, ax: 1, ay: 1 },
  speeder_bay:          { w: 3, h: 2, ax: 1, ay: 1 },
  fountain:             { w: 3, h: 2, ax: 1, ay: 0 },
  scorch_wall:          { w: 3, h: 2, ax: 1, ay: 1 },
  sentry_post:          { w: 3, h: 2, ax: 1, ay: 1 },
  maglev_crane:         { w: 3, h: 3, ax: 1, ay: 2 },
  capacitor_bank:       { w: 3, h: 3, ax: 1, ay: 2 },
  scan_wreck:           { w: 4, h: 2, ax: 1, ay: 1 },
  crater_glass:         { w: 4, h: 3, ax: 1, ay: 2 },
  rebreather_rack:      { w: 3, h: 2, ax: 1, ay: 1 },
  sentinel_droid:       { w: 2, h: 3, ax: 0, ay: 2 },
  hypercore:            { w: 3, h: 3, ax: 1, ay: 2 },
  bridge_console:       { w: 4, h: 2, ax: 1, ay: 1 },
  // CORUSCANT DEFS END
  // BADOR DEFS BEGIN
  landing_pad:          { w: 12, h: 8, ax: 6, ay: 1, backdrop: true },
  thoroughfare:         { w: 14, h: 6, ax: 5, ay: 2, backdrop: true },
  hq_dome:              { w: 14, h: 11, ax: 1, ay: 4, backdrop: true },
  bacta_pod:            { w: 2, h: 3, ax: 0, ay: 2 },
  repulsor_crane:       { w: 5, h: 3, ax: 4, ay: 2 },
  robotics_bench:       { w: 3, h: 2, ax: 1, ay: 1 },
  weapon_bench:         { w: 3, h: 2, ax: 1, ay: 1 },
  target_range:         { w: 3, h: 2, ax: 1, ay: 1 },
  fuel_diag_terminal:   { w: 2, h: 2, ax: 0, ay: 1 },
  sanctum_holo:         { w: 3, h: 3, ax: 1, ay: 2 },
  command_board:        { w: 3, h: 2, ax: 1, ay: 1 },
  kuati_terminal:       { w: 2, h: 2, ax: 0, ay: 1 },
  facility_sign:        { w: 4, h: 2, ax: 1, ay: 1 },
  kuati_banner:         { w: 2, h: 3, ax: 0, ay: 2 },
  terminal_bank:        { w: 4, h: 2, ax: 1, ay: 1 },
  floor_inlay:          { w: 5, h: 5, ax: 2, ay: 2, backdrop: true },
  // BADOR DEFS END
  // SENATE DEFS BEGIN
  skyline_vista:        { w: 38, h: 5, ax: 19, ay: 4, backdrop: true },
  crest_inlay:          { w: 5, h: 5, ax: 2, ay: 2, backdrop: true },
  repulsor_chandelier:  { w: 4, h: 4, ax: 2, ay: 3, backdrop: true },
  conversation_pit:     { w: 4, h: 3, ax: 2, ay: 1 },
  news_column:          { w: 2, h: 3, ax: 0, ay: 2 },
  obsidian_bar:         { w: 6, h: 2, ax: 2, ay: 1 },
  holo_fountain:        { w: 3, h: 3, ax: 1, ay: 2 },
  decanter_pedestal:    { w: 2, h: 2, ax: 0, ay: 1 },
  terrarium:            { w: 2, h: 2, ax: 0, ay: 1 },
  brass_register:       { w: 2, h: 2, ax: 0, ay: 1 },
  macro_binocular:      { w: 2, h: 2, ax: 0, ay: 1 },
  glass_overhang:       { w: 4, h: 2, ax: 1, ay: 1 },
  tint_console:         { w: 2, h: 2, ax: 0, ay: 1 },
  // SENATE DEFS END
  // PENTHOUSE DEFS BEGIN
  observatory_telescope: { w: 3, h: 3, ax: 1, ay: 2 },
  garden_planter:        { w: 3, h: 2, ax: 1, ay: 1 },
  art_pedestal:          { w: 2, h: 2, ax: 0, ay: 1 },
  wall_art:              { w: 3, h: 2, ax: 1, ay: 1 },
  holo_theater:          { w: 4, h: 3, ax: 1, ay: 2 },
  lounge_rug:            { w: 5, h: 3, ax: 2, ay: 1, backdrop: true },
  soak_pool:             { w: 4, h: 3, ax: 1, ay: 1 },
  design_console:        { w: 2, h: 2, ax: 0, ay: 1 },
  // PENTHOUSE DEFS END
  // BADOR WAR DEFS BEGIN
  basalt_vista:          { w: 24, h: 5, ax: 12, ay: 4, backdrop: true },
  // BADOR VISTA DEFS BEGIN
  bvista_20x5:           { w: 20, h: 5, ax: 10, ay: 4, backdrop: true },
  bvista_24x5:           { w: 24, h: 5, ax: 12, ay: 4, backdrop: true },
  bvista_32x5:           { w: 32, h: 5, ax: 16, ay: 4, backdrop: true },
  bvista_36x5:           { w: 36, h: 5, ax: 18, ay: 4, backdrop: true },
  bvista_44x5:           { w: 44, h: 5, ax: 22, ay: 4, backdrop: true },
  bvista_48x5:           { w: 48, h: 5, ax: 24, ay: 4, backdrop: true },
  bvista_30x3:           { w: 30, h: 3, ax: 15, ay: 2, backdrop: true },
  keel_frame:            { w: 9, h: 5, ax: 4, ay: 4 },
  warden_prime:          { w: 3, h: 3, ax: 1, ay: 2 },
  memorial_plinth:       { w: 2, h: 2, ax: 0, ay: 1 },
  // BADOR VISTA DEFS END
  kdy_barricade:         { w: 3, h: 2, ax: 1, ay: 1 },
  auto_turret:           { w: 2, h: 2, ax: 0, ay: 1 },
  shanty_hovel:          { w: 3, h: 3, ax: 1, ay: 2 },
  burning_wreck:         { w: 3, h: 2, ax: 1, ay: 1 },
  fuel_conduit:          { w: 4, h: 2, ax: 1, ay: 1 },
  mortar_pit:            { w: 3, h: 3, ax: 1, ay: 2 },
  catwalk_post:          { w: 3, h: 3, ax: 1, ay: 2 },
  landspeeder_depot:     { w: 5, h: 3, ax: 2, ay: 2 },
  eweb_turret:           { w: 2, h: 2, ax: 0, ay: 1 },
  exo_socket:            { w: 2, h: 2, ax: 0, ay: 1 },
  repulsor_barricade:    { w: 3, h: 2, ax: 1, ay: 1 },
  drop_speeder:          { w: 4, h: 3, ax: 1, ay: 2 },
  camo_netting:          { w: 8, h: 4, ax: 4, ay: 3, backdrop: true },
  steam_grate:           { w: 2, h: 2, ax: 0, ay: 1 },
  fusion_lever:          { w: 2, h: 2, ax: 0, ay: 1 },
  brig_cell:             { w: 3, h: 3, ax: 1, ay: 2 },
  enc_array:             { w: 3, h: 2, ax: 1, ay: 1 },
  field_desk:            { w: 3, h: 2, ax: 1, ay: 1 },
  scout_walker:          { w: 4, h: 4, ax: 1, ay: 3 },
  piston_tower:          { w: 3, h: 5, ax: 1, ay: 4 },
  valve_wheel:           { w: 2, h: 2, ax: 0, ay: 1 },
  steam_vent_pipe:       { w: 2, h: 2, ax: 0, ay: 1 },
  foundry_hatch:         { w: 5, h: 4, ax: 2, ay: 3 },
  exhaust_cathedral:     { w: 8, h: 7, ax: 4, ay: 6, backdrop: true },
  boiler_hull:           { w: 4, h: 3, ax: 1, ay: 2 },
  governor_socket:       { w: 2, h: 2, ax: 0, ay: 1 },
  wst_blast_doors:       { w: 3, h: 3, ax: 2, ay: 2 },
  // BADOR WAR DEFS END
  // CAVERN DEFS BEGIN
  cav_stalactite:        { w: 2, h: 3, ax: 0, ay: 2 },
  cav_spore_pod:         { w: 3, h: 2, ax: 1, ay: 1 },
  cav_survey_beacon:     { w: 2, h: 3, ax: 0, ay: 2 },
  cav_mining_rig:        { w: 4, h: 3, ax: 1, ay: 2 },
  cav_crystal_pylon:     { w: 2, h: 3, ax: 0, ay: 2 },
  cav_guardian_chassis:  { w: 3, h: 3, ax: 1, ay: 2 },
  cav_gate_shutter:      { w: 2, h: 4, ax: 0, ay: 3, backdrop: true },
  // CAVERN DEFS END
  // MFG DEFS BEGIN
  mfg_conveyor:          { w: 4, h: 2, ax: 1, ay: 1 },
  mfg_cradle:            { w: 5, h: 4, ax: 2, ay: 3 },
  mfg_forge:             { w: 4, h: 3, ax: 1, ay: 2 },
  mfg_glass:             { w: 5, h: 2, ax: 2, ay: 1 },
  mfg_keel:              { w: 5, h: 3, ax: 2, ay: 2 },
  // MFG DEFS END
  // BASE DEFS BEGIN
  base_cure_bench:       { w: 3, h: 2, ax: 1, ay: 1 },
  // BASE DEFS END
};

const PropArt = React.memo(function PropArt({ kind, variant, active }) {
  switch (kind) {
    case 'fuel_rig': return <PropFuelRig />;
    case 'manifest_terminal': return <PropManifestTerminal />;
    case 'vectis': return <PropVectis active={!!active} />;
    case 'impound_door': return <PropImpoundDoor />;
    case 'orbital_viewport': return <PropOrbitalViewport />;
    case 'tether_lockers': return <PropTetherLockers />;
    case 'transit_pod': return <PropTransitPod />;
    case 'welding_rig': return <PropWeldingRig />;
    case 'wayfinding_plaque': return <PropWayfindingPlaque />;
    case 'blast_panel': return <PropBlastPanel />;
    case 'holo_projector': return <PropHoloProjector />;
    case 'secure_locker': return <PropSecureLocker />;
    case 'intercept_wall': return <PropInterceptWall />;
    case 'terminal': return <PropTerminal variant={variant || 'engineering'} />;
    case 'holo_table': return <PropHoloTable variant={variant || 'fleet'} />;
    case 'ordnance_crates': return <PropOrdnanceCrates />;
    case 'armory_locker': return <PropArmoryLocker />;
    case 'cable_truss': return <PropCableTruss />;
    case 'coolant_pipe': return <PropCoolantPipe />;
    case 'sabacc_table': return <PropSabaccTable />;
    case 'market_stall': return <PropMarketStall />;
    case 'shift_board': return <PropShiftBoard />;
    case 'union_bulletin': return <PropUnionBulletin />;
    case 'surveillance_node': return <PropSurveillanceNode />;
    case 'forge_bar': return <PropForgeBar />;
    case 'czerka_terminal': return <PropCzerkaTerminal />;
    case 'fuel_rack': return <PropFuelRack />;
    case 'drive_cradle': return <PropDriveCradle />;
    case 'crawler_port': return <PropCrawlerPort />;
    case 'rivet_hopper': return <PropRivetHopper />;
    case 'republic_terminal': return <PropRepublicTerminal />;
    case 'shipyard_viewport': return <PropShipyardViewport />;
    case 'executive_safe': return <PropExecutiveSafe />;
    case 'model_case': return <PropModelCase />;
    case 'bronze_mural': return <PropBronzeMural />;
    case 'panoramic_viewport': return <PropPanoramicViewport />;
    case 'protocol_droid': return <PropProtocolDroid />;
    // CORUSCANT CASES BEGIN
    case 'console': return <PropConsole variant={variant} active={!!active} />;
    case 'kiosk': return <PropKiosk variant={variant} active={!!active} />;
    case 'airtaxi': return <PropAirTaxi variant={variant} active={!!active} />;
    case 'comm_relay': return <PropCommRelay variant={variant} active={!!active} />;
    case 'server_stack': return <PropServerStack variant={variant} active={!!active} />;
    case 'board': return <PropBoard variant={variant} active={!!active} />;
    case 'sign_array': return <PropSignArray variant={variant} active={!!active} />;
    case 'shopfront': return <PropShopfront variant={variant} active={!!active} />;
    case 'apt_door': return <PropAptDoor variant={variant} active={!!active} />;
    case 'wall_marks': return <PropWallMarks variant={variant} active={!!active} />;
    case 'skyline': return <PropSkyline variant={variant} active={!!active} />;
    case 'elevator_door': return <PropElevatorDoor variant={variant} active={!!active} />;
    case 'datapad_table': return <PropDatapadTable variant={variant} active={!!active} />;
    case 'crate_stack': return <PropCrateStack variant={variant} active={!!active} />;
    case 'container_stack': return <PropContainerStack variant={variant} active={!!active} />;
    case 'cargo_container': return <PropCargoContainer variant={variant} active={!!active} />;
    case 'drum_array': return <PropDrumArray variant={variant} active={!!active} />;
    case 'hab_block': return <PropHabBlock variant={variant} active={!!active} />;
    case 'evidence_locker': return <PropEvidenceLocker variant={variant} active={!!active} />;
    case 'weapon_rack': return <PropWeaponRack variant={variant} active={!!active} />;
    case 'medic_crate': return <PropMedicCrate variant={variant} active={!!active} />;
    case 'scrap_bin': return <PropScrapBin variant={variant} active={!!active} />;
    case 'fuel_hose': return <PropFuelHose variant={variant} active={!!active} />;
    case 'storage_alcoves': return <PropStorageAlcoves variant={variant} active={!!active} />;
    case 'archive_cabinet': return <PropArchiveCabinet variant={variant} active={!!active} />;
    case 'workbench': return <PropWorkbench variant={variant} active={!!active} />;
    case 'cargo_lift': return <PropCargoLift variant={variant} active={!!active} />;
    case 'vault_door': return <PropVaultDoor variant={variant} active={!!active} />;
    case 'security_gate': return <PropSecurityGate variant={variant} active={!!active} />;
    case 'junction_box': return <PropJunctionBox variant={variant} active={!!active} />;
    case 'leaking_pipe': return <PropLeakingPipe variant={variant} active={!!active} />;
    case 'pressure_valve': return <PropPressureValve variant={variant} active={!!active} />;
    case 'steam_vent': return <PropSteamVent variant={variant} active={!!active} />;
    case 'access_hatch': return <PropAccessHatch variant={variant} active={!!active} />;
    case 'scanner_arch': return <PropScannerArch variant={variant} active={!!active} />;
    case 'damper_array': return <PropDamperArray variant={variant} active={!!active} />;
    case 'power_tap': return <PropPowerTap variant={variant} active={!!active} />;
    case 'drop_shaft': return <PropDropShaft variant={variant} active={!!active} />;
    case 'catwalk_junction': return <PropCatwalkJunction variant={variant} active={!!active} />;
    case 'floor_grate': return <PropFloorGrate variant={variant} active={!!active} />;
    case 'drain_channel': return <PropDrainChannel variant={variant} active={!!active} />;
    case 'support_pillar': return <PropSupportPillar variant={variant} active={!!active} />;
    case 'radiator': return <PropRadiator variant={variant} active={!!active} />;
    case 'fluid_slick': return <PropFluidSlick variant={variant} active={!!active} />;
    case 'crane_arm': return <PropCraneArm variant={variant} active={!!active} />;
    case 'landing_beacon': return <PropLandingBeacon variant={variant} active={!!active} />;
    case 'ceiling_tap': return <PropCeilingTap variant={variant} active={!!active} />;
    case 'tactical_table': return <PropTacticalTable variant={variant} active={!!active} />;
    case 'vendor_stall': return <PropVendorStall variant={variant} active={!!active} />;
    case 'gorg_spit': return <PropGorgSpit variant={variant} active={!!active} />;
    case 'booth': return <PropBooth variant={variant} active={!!active} />;
    case 'guard_post': return <PropGuardPost variant={variant} active={!!active} />;
    case 'interrogation_rig': return <PropInterrogationRig variant={variant} active={!!active} />;
    case 'ceremony_dais': return <PropCeremonyDais variant={variant} active={!!active} />;
    case 'monitor_wall': return <PropMonitorWall variant={variant} active={!!active} />;
    case 'roster_wall': return <PropRosterWall variant={variant} active={!!active} />;
    case 'trophy_wall': return <PropTrophyWall variant={variant} active={!!active} />;
    case 'duel_ring': return <PropDuelRing variant={variant} active={!!active} />;
    case 'arena_ring': return <PropArenaRing variant={variant} active={!!active} />;
    case 'pit_gate': return <PropPitGate variant={variant} active={!!active} />;
    case 'rail_run': return <PropRailRun variant={variant} active={!!active} />;
    case 'fighter_altar': return <PropFighterAltar variant={variant} active={!!active} />;
    case 'kyber_cluster': return <PropKyberCluster variant={variant} active={!!active} />;
    case 'survey_marker': return <PropSurveyMarker variant={variant} active={!!active} />;
    case 'containment_seal': return <PropContainmentSeal variant={variant} active={!!active} />;
    case 'ruin_stone': return <PropRuinStone variant={variant} active={!!active} />;
    case 'outcast_camp': return <PropOutcastCamp variant={variant} active={!!active} />;
    case 'lux_skiff': return <PropLuxSkiff variant={variant} active={!!active} />;
    case 'speeder_wreck': return <PropSpeederWreck variant={variant} active={!!active} />;
    case 'speeder_bay': return <PropSpeederBay variant={variant} active={!!active} />;
    case 'fountain': return <PropFountain variant={variant} active={!!active} />;
    case 'scorch_wall': return <PropScorchWall variant={variant} active={!!active} />;
    case 'sentry_post': return <PropSentryPost variant={variant} active={!!active} />;
    case 'maglev_crane': return <PropMaglevCrane variant={variant} active={!!active} />;
    case 'capacitor_bank': return <PropCapacitorBank variant={variant} />;
    case 'scan_wreck': return <PropScanWreck variant={variant} active={!!active} />;
    case 'crater_glass': return <PropCraterGlass variant={variant} active={!!active} />;
    case 'rebreather_rack': return <PropRebreatherRack variant={variant} active={!!active} />;
    case 'sentinel_droid': return <PropSentinelDroid variant={variant} active={!!active} />;
    case 'hypercore': return <PropHypercore variant={variant} active={!!active} />;
    case 'bridge_console': return <PropBridgeConsole variant={variant} active={!!active} />;
    // CORUSCANT CASES END
    // BADOR CASES BEGIN
    case 'landing_pad': return <PropLandingPad />;
    case 'thoroughfare': return <PropThoroughfare />;
    case 'hq_dome': return <PropHqDome />;
    case 'bacta_pod': return <PropBactaPod />;
    case 'repulsor_crane': return <PropRepulsorCrane />;
    case 'robotics_bench': return <PropRoboticsBench />;
    case 'weapon_bench': return <PropWeaponBench />;
    case 'target_range': return <PropTargetRange />;
    case 'fuel_diag_terminal': return <PropFuelDiagTerminal />;
    case 'sanctum_holo': return <PropSanctumHolo />;
    case 'command_board': return <PropCommandBoard />;
    case 'kuati_terminal': return <PropKuatiTerminal variant={variant} />;
    case 'facility_sign': return <PropFacilitySign variant={variant} />;
    case 'kuati_banner': return <PropKuatiBanner />;
    case 'terminal_bank': return <PropTerminalBank />;
    case 'floor_inlay': return <PropFloorInlay />;
    // BADOR CASES END
    // SENATE CASES BEGIN
    case 'skyline_vista': return <PropSkylineVista variant={variant} />;
    case 'crest_inlay': return <PropCrestInlay />;
    case 'repulsor_chandelier': return <PropRepulsorChandelier />;
    case 'conversation_pit': return <PropConversationPit />;
    case 'news_column': return <PropNewsColumn />;
    case 'obsidian_bar': return <PropObsidianBar />;
    case 'holo_fountain': return <PropHoloFountain />;
    case 'decanter_pedestal': return <PropDecanterPedestal />;
    case 'terrarium': return <PropTerrarium />;
    case 'brass_register': return <PropBrassRegister />;
    case 'macro_binocular': return <PropMacroBinocular />;
    case 'glass_overhang': return <PropGlassOverhang />;
    case 'tint_console': return <PropTintConsole />;
    // SENATE CASES END
    // PENTHOUSE CASES BEGIN
    case 'observatory_telescope': return <PropObservatoryTelescope />;
    case 'garden_planter': return <PropGardenPlanter />;
    case 'art_pedestal': return <PropGalleryPedestal />;
    case 'wall_art': return <PropWallArt variant={variant} />;
    case 'holo_theater': return <PropHoloTheater />;
    case 'lounge_rug': return <PropLoungeRug />;
    case 'soak_pool': return <PropSoakPool />;
    case 'design_console': return <PropDesignConsole />;
    // PENTHOUSE CASES END
    // BADOR WAR CASES BEGIN
    case 'basalt_vista': return <PropBasaltVista variant={variant} />;
    // BADOR VISTA CASES BEGIN
    case 'bvista_20x5': return <PropBvistaA variant={variant} />;
    case 'bvista_24x5': return <PropBvistaB variant={variant} />;
    case 'bvista_32x5': return <PropBvistaC variant={variant} />;
    case 'bvista_36x5': return <PropBvistaD variant={variant} />;
    case 'bvista_44x5': return <PropBvistaE variant={variant} />;
    case 'bvista_48x5': return <PropBvistaF variant={variant} />;
    case 'bvista_30x3': return <PropBvistaG variant={variant} />;
    case 'keel_frame': return <PropKeelFrame />;
    case 'warden_prime': return <PropWardenPrime />;
    case 'memorial_plinth': return <PropMemorialPlinth />;
    // BADOR VISTA CASES END
    case 'kdy_barricade': return <PropKdyBarricade variant={variant} />;
    case 'auto_turret': return <PropAutoTurret variant={variant} />;
    case 'shanty_hovel': return <PropShantyHovel variant={variant} />;
    case 'burning_wreck': return <PropBurningWreck />;
    case 'fuel_conduit': return <PropFuelConduit variant={variant} />;
    case 'mortar_pit': return <PropMortarPit />;
    case 'catwalk_post': return <PropCatwalkPost />;
    case 'landspeeder_depot': return <PropLandspeederDepot />;
    case 'eweb_turret': return <PropEwebTurret variant={variant} />;
    case 'exo_socket': return <PropExoSocket />;
    case 'repulsor_barricade': return <PropRepulsorBarricade />;
    case 'drop_speeder': return <PropDropSpeeder />;
    case 'camo_netting': return <PropCamoNetting />;
    case 'steam_grate': return <PropSteamGrate />;
    case 'fusion_lever': return <PropFusionLever variant={variant} />;
    case 'brig_cell': return <PropBrigCell variant={variant} />;
    case 'enc_array': return <PropEncArray />;
    case 'field_desk': return <PropFieldDesk />;
    case 'scout_walker': return <PropScoutWalker />;
    case 'piston_tower': return <PropPistonTower />;
    case 'valve_wheel': return <PropValveWheel variant={variant} />;
    case 'steam_vent_pipe': return <PropSteamVentPipe />;
    case 'foundry_hatch': return <PropFoundryHatch />;
    case 'exhaust_cathedral': return <PropExhaustCathedral />;
    case 'boiler_hull': return <PropBoilerHull />;
    case 'governor_socket': return <PropGovernorSocket variant={variant} />;
    case 'base_cure_bench': return <PropBaseCureBench />;
    case 'wst_blast_doors': return <PropWstBlastDoors variant={variant} />;
    // BADOR WAR CASES END
    // CAVERN CASES BEGIN
    case 'cav_stalactite': return <PropCavStalactite />;
    case 'cav_spore_pod': return <PropCavSporePod />;
    case 'cav_survey_beacon': return <PropCavBeacon variant={variant} />;
    case 'cav_mining_rig': return <PropCavRig />;
    case 'cav_crystal_pylon': return <PropCavPylon />;
    case 'cav_guardian_chassis': return <PropCavChassis variant={variant} />;
    case 'cav_gate_shutter': return <PropCavShutter variant={variant} />;
    // CAVERN CASES END
    // MFG CASES BEGIN
    case 'mfg_conveyor': return <PropMfgConveyor variant={variant} />;
    case 'mfg_cradle': return <PropMfgCradle variant={variant} />;
    case 'mfg_forge': return <PropMfgForge />;
    case 'mfg_glass': return <PropMfgGlass />;
    case 'mfg_keel': return <PropMfgKeel />;
    // MFG CASES END
    default: return null;
  }
});

function visiblePropObjects(zone, questFlags, completed) {
  const byTile = {};
  (zone.worldObjects || []).forEach((wo) => {
    if (!wo.propArt || !PROP_DEFS[wo.propArt]) return;
    if (wo.once && completed.has(wo.id)) return;
    if ((wo.requiresNoneFlags || []).some((f) => questFlags[f])) return;
    const k = wo.x + ',' + wo.y;
    const cur = byTile[k];
    const unlocked = (!wo.requiresFlag || questFlags[wo.requiresFlag]) && (wo.requiresAllFlags || []).every((f) => questFlags[f]);
    if (wo.invisibleLocked && !unlocked) return;
    if (!cur || (unlocked && (wo.requiresFlag || wo.requiresAllFlags))) byTile[k] = wo;
  });
  return Object.values(byTile);
}

const SHIP_SVG_STYLE = { position:'absolute', inset:0, pointerEvents:'none' };

function ShipFreeHorizon({ accent }) {
  const S = ART.signal;
  const disc = ngon(96, 92, 64, 56, 22, Math.PI / 22);
  const ring1 = ngon(96, 92, 46, 40, 22, Math.PI / 22);
  const ring2 = ngon(96, 92, 27, 23, 22, Math.PI / 22);
  return (
    <svg viewBox="0 0 192 160" width="192" height="160" style={SHIP_SVG_STYLE}>
      <rect x="3" y="3" width="186" height="154" fill={ART.screen.off} opacity="0.55" />
      <Hazard x={3} y={3} w={186} h={3} opacity={0.8} /><Hazard x={3} y={154} w={186} h={3} opacity={0.8} />
      <Hazard x={3} y={3} w={3} h={154} opacity={0.8} /><Hazard x={186} y={3} w={3} h={154} opacity={0.8} />
      <PropShadow cx={101} cy={99} rx={70} ry={53} />
      {/* port prong with sensor dish */}
      <Bev t="polygon" points={pts([[46, 76], [34, 24], [38, 12], [54, 12], [86, 58]])} c="steel" />
      <polygon points={pts([[38, 20], [52, 18], [54, 24], [40, 28]])} fill={ART.amber.base} />
      <Seams x={36} y={16} w={44} h={52} cols={1} rows={3} op={0.35} />
      <Greeble x={40} y={30} w={22} h={22} seed={7} n={6} c="steel" />
      <line x1="46" y1="12" x2="46" y2="6" stroke={ART.steel.hi} strokeWidth="1" />
      <Bev t="polygon" points={pts(ngon(46, 8, 5, 3.6, 8))} c="hull" />
      <polygon points={pts(ngon(46, 8, 2.2, 1.6, 8))} fill={ART.dark.shade} />
      {/* starboard cockpit tube */}
      <Bev t="polygon" points={pts([[134, 74], [138, 34], [160, 28], [164, 68]])} c="steel" />
      <Bev t="polygon" points={pts([[139, 36], [159, 30], [156, 46], [141, 50]])} c="glass" o={0.5} />
      <polyline points={pts([[148, 33], [149, 48]])} stroke={ART.dark.shade} strokeWidth="0.8" fill="none" />
      <polygon points={pts([[141, 36], [150, 34], [148, 38], [142, 40]])} fill={ART.signal.white} opacity="0.5" />
      <Seams x={136} y={52} w={26} h={20} cols={1} rows={1} op={0.4} />
      <Lights x={142} y={62} n={4} gap={4} colors={['amber', 'green']} seed={4} />
      {/* bridge between prongs */}
      <polygon points={pts([[84, 58], [106, 50], [124, 60], [108, 70]])} fill={ART.dark.base} />
      <rect x="92" y="55" width="16" height="2.4" fill={S.cyan} opacity="0.55" />
      {/* main disc */}
      <Bev t="polygon" points={pts(disc)} c="steel" o={1.1} />
      <polygon points={pts(ring1)} fill="none" stroke={ART.ink} strokeWidth="0.6" opacity="0.5" />
      <polygon points={pts(ring2)} fill="none" stroke={ART.ink} strokeWidth="0.6" opacity="0.5" />
      {ring1.map((p, i) => <line key={i} x1={ring2[i][0]} y1={ring2[i][1]} x2={p[0]} y2={p[1]} stroke={ART.ink} strokeWidth="0.5" opacity="0.4" />)}
      {ring1.map((p, i) => <line key={'o' + i} x1={p[0]} y1={p[1]} x2={disc[i][0]} y2={disc[i][1]} stroke={ART.ink} strokeWidth="0.5" opacity="0.4" />)}
      <polygon points={pts([[58, 74], [72, 64], [78, 76], [64, 84]])} fill={ART.hull.base} opacity="0.85" />
      <polygon points={pts([[118, 104], [134, 98], [138, 112], [122, 118]])} fill={ART.hull.base} opacity="0.85" />
      <polygon points={pts([[76, 112], [92, 108], [94, 120], [78, 124]])} fill={ART.hull.shade} opacity="0.9" />
      <Greeble x={60} y={100} w={26} h={14} seed={11} n={8} c="steel" />
      <Greeble x={114} y={70} w={26} h={16} seed={13} n={9} c="steel" />
      <Vent x={118} y={88} w={16} h={8} n={4} /><Vent x={52} y={96} w={12} h={7} n={3} />
      <polygon points={pts([[68, 116], [92, 110], [104, 122], [82, 130]])} fill={ART.ink} opacity="0.3" />
      <Grime x={44} y={50} w={104} h={80} seed={21} n={9} op={0.16} />
      <Scuff x={50} y={56} w={90} h={70} seed={5} n={14} />
      {/* dorsal quad turret */}
      <Bev t="polygon" points={pts(ngon(96, 72, 9, 8, 8))} c="dark" />
      <Bev t="rect" c="dark" x={92} y={52} width={2.6} height={20} /><Bev t="rect" c="dark" x={98} y={52} width={2.6} height={20} />
      <polygon points={pts(ngon(96, 72, 4, 3.6, 6))} fill={ART.steel.base} />
      {/* docking ring */}
      <Bev t="polygon" points={pts(ngon(96, 110, 13, 11, 8))} c="dark" />
      <polygon points={pts(ngon(96, 110, 8, 6.6, 8))} fill={ART.ink} />
      <path d="M96 103 L96 117 M89 110 L103 110" stroke={ART.dark.hi} strokeWidth="0.6" />
      {/* hold seal and stencils */}
      <rect x="74" y="84" width="44" height="9" fill={ART.red.base} />
      <rect x="74" y="84" width="44" height="1.4" fill={ART.red.hi} />
      <text x="96" y="91" textAnchor="middle" fontSize="4.4" fill={ART.paper.hi} fontWeight="700" {...PT}>RING-SEC HOLD</text>
      <text x="96" y="132" textAnchor="middle" fontSize="6.4" fill={ART.hull.hi} opacity="0.75" letterSpacing="1.2" {...PT}>FREE HORIZON</text>
      <text x="96" y="139" textAnchor="middle" fontSize="3.6" fill={ART.hull.hi} opacity="0.6" letterSpacing="1" {...PT}>CR-7714 CORELLIA</text>
      <path d="M100 141.4 L112 140" stroke={ART.signal.white} strokeWidth="0.7" opacity="0.8" />
      <path d="M58 112 L70 122 L78 110" fill="none" stroke={ART.ink} strokeWidth="1.2" opacity="0.8" />
      {[[60, 113], [64, 117], [68, 121], [72, 120], [75, 115], [77, 111]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="0.9" fill={S.orange} />)}
      {/* engine block */}
      <Bev t="polygon" points={pts([[50, 140], [142, 140], [152, 154], [40, 154]])} c="dark" />
      {[68, 96, 124].map((x, i) => (
        <g key={x}>
          <Bev t="polygon" points={pts(ngon(x, i === 1 ? 150 : 148, 10, 7, 6))} c="dark" />
          <polygon points={pts(ngon(x, i === 1 ? 150 : 148, 6, 4.2, 6))} fill={ART.ink} />
          <Glow cx={x} cy={i === 1 ? 150 : 148} r={7} c={S.cyan} opacity={0.35} style={{ animation:`ship-engine ${4 + i}s ease-in-out ${i * 0.6}s infinite` }} />
        </g>
      ))}
      <Vent x={56} y={141} w={80} h={3} n={2} />
      {/* impound clamps */}
      {[0, 1].map((side) => {
        const m = side ? -1 : 1, bx = side ? 184 : 8;
        return (
          <g key={side}>
            <Bev t="polygon" points={pts([[bx, 80], [bx + m * 22, 80], [bx + m * 22, 106], [bx, 106]])} c="dark" />
            <Hazard x={side ? bx - 22 : bx} y={80} w={22} h={4} />
            <Bev t="polygon" points={pts([[bx + m * 22, 86], [bx + m * 36, 92], [bx + m * 36, 98], [bx + m * 22, 102]])} c="red" />
            <circle cx={bx + m * 11} cy={94} r="2.6" fill={S.red} style={{ animation:`ship-blink 1.1s steps(2) ${side * 0.55}s infinite` }} />
            <Glow cx={bx + m * 11} cy={94} r={7} c={S.red} opacity={0.5} style={{ animation:`ship-blink 1.1s steps(2) ${side * 0.55}s infinite` }} />
          </g>
        );
      })}
      <Cable d="M10 106 Q8 130 20 150" c={ART.amber.shade} w={1.6} /><Cable d="M182 106 Q184 130 172 150" c={ART.amber.shade} w={1.6} />
    </svg>
  );
}

function ShipRepublicShuttle({ accent }) {
  const S = ART.signal;
  const cog = Array.from({ length: 24 }, (_, i) => { const a = (i / 24) * Math.PI * 2, r = i % 2 ? 8.4 : 11; return [+(100 + Math.cos(a) * r).toFixed(2), +(64 + Math.sin(a) * r).toFixed(2)]; });
  return (
    <svg viewBox="0 0 192 128" width="192" height="128" style={SHIP_SVG_STYLE}>
      <rect x="3" y="3" width="186" height="122" fill={ART.screen.off} opacity="0.5" />
      <rect x="3" y="3" width="186" height="122" fill="none" stroke={accent} strokeWidth="0.8" strokeDasharray="6 4" opacity="0.5" />
      <PropShadow cx={98} cy={71} rx={78} ry={38} />
      {/* swept wings folded back */}
      {[0, 1].map((m) => {
        const f = (y) => (m ? 128 - y : y);
        return (
          <g key={m}>
            <Bev t="polygon" points={pts([[98, f(44)], [124, f(20)], [180, f(13)], [172, f(44)]])} c="hull" />
            <polygon points={pts([[118, f(26)], [180, f(13)], [178, f(18)], [122, f(31)]])} fill={ART.red.base} />
            <Seams x={104} y={m ? 90 : 18} w={70} h={26} cols={3} rows={1} op={0.3} />
            <Greeble x={130} y={m ? 96 : 24} w={36} h={14} seed={31 + m} n={5} c="hull" />
            <circle cx="179" cy={f(14)} r="1.8" fill={m ? S.green : S.red} style={{ animation:`ship-blink 1.4s steps(2) ${m * 0.7}s infinite` }} />
          </g>
        );
      })}
      {/* fuselage */}
      <Bev t="polygon" points={pts([[6, 64], [22, 56], [50, 48], [88, 43], [140, 42], [172, 46], [186, 56], [186, 72], [172, 82], [140, 86], [88, 85], [50, 80], [22, 72]])} c="pearl" o={1} />
      <Bev t="polygon" points={pts([[62, 52], [150, 50], [162, 57], [162, 71], [150, 78], [62, 76]])} c="hull" o={0.7} />
      <Seams x={62} y={50} w={100} h={28} cols={6} rows={1} op={0.4} />
      <Greeble x={66} y={53} w={90} h={7} seed={41} n={9} c="hull" /><Greeble x={66} y={69} w={90} h={7} seed={43} n={9} c="hull" />
      <Vent x={128} y={58} w={14} h={5} n={3} /><Vent x={128} y={66} w={14} h={5} n={3} />
      <polygon points={pts([[28, 58], [176, 56], [176, 61], [28, 63]])} fill={ART.red.base} />
      <polygon points={pts([[28, 65], [176, 67], [176, 72], [28, 70]])} fill={ART.red.base} opacity="0.0" />
      <Seams x={20} y={44} w={160} h={40} cols={9} op={0.18} />
      <Grime x={20} y={44} w={160} h={40} seed={9} n={7} op={0.12} />
      <Scuff x={24} y={46} w={150} h={36} seed={3} n={10} c="pearl" />
      {/* republic cog */}
      <Bev t="polygon" points={pts(ngon(100, 64, 13, 13, 24))} c="dark" o={0.6} />
      <Bev t="polygon" points={pts(cog)} c="red" o={0.6} />
      <polygon points={pts(ngon(100, 64, 4.2, 4.2, 12))} fill={ART.pearl.base} />
      <circle cx="100" cy="64" r="1.6" fill={ART.red.base} />
      <text x="138" y="68" textAnchor="middle" fontSize="5" fill={ART.paper.hi} fontWeight="700" letterSpacing="0.6" {...PT}>RN-4471</text>
      {/* canopy */}
      <Bev t="polygon" points={pts([[8, 64], [24, 57], [46, 52], [60, 58], [60, 70], [46, 76], [24, 71]])} c="glass" o={0.7} />
      <path d="M26 58 L26 70 M38 54 L38 74 M50 54 L52 74 M10 64 L58 64" stroke={ART.dark.base} strokeWidth="0.9" opacity="0.85" />
      <polygon points={pts([[22, 59], [40, 55], [52, 57], [30, 61]])} fill={S.white} opacity="0.4" />
      {/* engines */}
      {[0, 1].map((m) => {
        const f = (y) => (m ? 128 - y : y);
        return (
          <g key={m}>
            <Bev t="polygon" points={pts([[154, f(40)], [188, f(38)], [192, f(46)], [192, f(52)], [154, f(54)]])} c="dark" />
            <polygon points={pts([[186, f(40)], [192, f(42)], [192, f(50)], [186, f(52)]])} fill={ART.ink} />
            <Glow cx={191} cy={f(46)} r={6.5} c={S.cyan} opacity={0.7} style={{ animation:`ship-engine 3.4s ease-in-out ${m * 0.8}s infinite` }} />
            <Seams x={156} y={m ? 76 : 40} w={30} h={12} cols={2} op={0.4} />
          </g>
        );
      })}
      <rect x="164" y="58" width="12" height="12" fill="none" stroke={ART.ink} strokeWidth="0.6" opacity="0.6" />
      <line x1="170" y1="58" x2="170" y2="70" stroke={ART.ink} strokeWidth="0.5" opacity="0.5" />
      <line x1="88" y1="43" x2="88" y2="34" stroke={ART.steel.hi} strokeWidth="0.9" /><circle cx="88" cy="33" r="1.1" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      {[[52, 40], [52, 88], [146, 38], [146, 90]].map(([x, y], i) => <Bev key={i} t="polygon" points={pts(ngon(x, y, 3.6, 3.2, 6))} c="dark" o={0.5} />)}
    </svg>
  );
}

function ShipValorFrame({ accent }) {
  const S = ART.signal;
  const U = [[36, 50], [44, 42], [84, 36], [130, 30], [176, 24], [228, 24], [270, 28], [310, 36], [360, 48], [410, 60], [432, 64]];
  const yTop = (x) => { for (let i = 0; i < U.length - 1; i++) { const [x0, y0] = U[i], [x1, y1] = U[i + 1]; if (x >= x0 && x <= x1) return y0 + ((y1 - y0) * (x - x0)) / (x1 - x0); } return 64; };
  const mir = (a) => a.map(([x, y]) => [x, 128 - y]);
  const plated = U.filter((p) => p[0] <= 270).concat([[300, 33]]);
  const platedPoly = plated.concat(mir(plated).reverse());
  const skel = [[300, 33], [310, 36], [360, 48], [410, 60], [432, 64], [410, 68], [360, 80], [310, 92], [300, 95]];
  const ribs = [308, 326, 344, 362, 380, 398];
  const turret = (cx, cy, rot = 0, k = 'T', hl = false) => (
    <g key={k} transform={`rotate(${rot} ${cx} ${cy})`}>
      <Bev t="polygon" points={pts(ngon(cx, cy, 6, 5.4, 8))} c="dark" o={0.6} />
      <rect x={cx + 2} y={cy - 2.6} width="15" height="1.8" fill={ART.dark.base} /><rect x={cx + 2} y={cy + 0.8} width="15" height="1.8" fill={ART.dark.base} />
      <rect x={cx + 2} y={cy - 2.6} width="15" height="0.5" fill={ART.dark.hi} />
      <circle cx={cx - 1} cy={cy - 1} r="1.6" fill={hl ? S.amber : ART.steel.base} />
    </g>
  );
  return (
    <svg viewBox="0 0 448 128" width="448" height="128" style={SHIP_SVG_STYLE}>
      <rect x="2" y="2" width="444" height="124" fill={ART.screen.off} opacity="0.55" />
      <PropShadow cx={234} cy={71} rx={204} ry={36} />
      {/* gantry rails, braces, hazard ends */}
      <Slab x={8} y={4} w={432} h={8} k={1} c="steel" /><Slab x={8} y={116} w={432} h={8} k={1} c="steel" />
      <Hazard x={8} y={4} w={72} h={8} opacity={0.9} /><Hazard x={376} y={116} w={64} h={8} opacity={0.9} />
      {[8, 56, 104, 152, 200, 248, 296, 344, 392, 436].map((x) => (
        <g key={x}>
          <Slab x={x - 2.5} y={12} w={5} h={12} k={0.8} c="steel" /><Slab x={x - 2.5} y={104} w={5} h={12} k={0.8} c="steel" />
        </g>
      ))}
      {[8, 56, 104, 152, 200, 248, 296, 344, 392].map((x) => (
        <g key={'b' + x} stroke={ART.steel.hi} strokeWidth="0.8" opacity="0.7">
          <line x1={x} y1="12" x2={x + 48} y2="24" /><line x1={x + 48} y1="12" x2={x} y2="24" />
          <line x1={x} y1="116" x2={x + 48} y2="104" /><line x1={x + 48} y1="116" x2={x} y2="104" />
        </g>
      ))}
      {/* plated hull: bulbous middle, republic white */}
      <Bev t="polygon" points={pts(platedPoly)} c="hull" o={1.2} />
      <polygon points={pts([[36, 64], [300, 64]].concat(mir(plated).reverse()))} fill={ART.ink} opacity="0.15" />
      <polyline points={pts(plated.map(([x, y]) => [x, y + 7]).filter((p) => p[0] >= 60))} fill="none" stroke={ART.red.base} strokeWidth="2.4" />
      <polyline points={pts(mir(plated).map(([x, y]) => [x, y - 7]).filter((p) => p[0] >= 60))} fill="none" stroke={ART.red.base} strokeWidth="2.4" />
      {[60, 84, 108, 132, 156, 180, 204, 228, 252, 276, 296].map((x) => <line key={x} x1={x} y1={yTop(x) + 1} x2={x} y2={127 - yTop(x)} stroke={ART.ink} strokeWidth="0.5" opacity="0.32" />)}
      <line x1="40" y1="64" x2="298" y2="64" stroke={ART.ink} strokeWidth="0.6" opacity="0.4" />
      <Greeble x={56} y={40} w={92} h={14} seed={51} n={14} c="hull" /><Greeble x={56} y={74} w={92} h={14} seed={52} n={14} c="hull" />
      <Greeble x={216} y={32} w={74} h={16} seed={53} n={12} c="hull" /><Greeble x={216} y={80} w={74} h={16} seed={54} n={12} c="hull" />
      <Grime x={44} y={30} w={250} h={70} seed={6} n={14} op={0.12} />
      <Scuff x={50} y={34} w={240} h={60} seed={8} n={22} c="hull" />
      {/* side hangars */}
      {[0, 1].map((m) => {
        const y = m ? 128 - 42 : 34;
        return (
          <g key={m}>
            <rect x="92" y={y} width="48" height="8" fill={ART.ink} />
            <rect x="92" y={m ? y : y + 6} width="48" height="2" fill={ART.dark.base} />
            <rect x="96" y={m ? y + 2 : y + 3} width="40" height="1" fill={S.cyan} opacity="0.55" />
            <Lights x={94} y={m ? y + 6.4 : y + 1.6} n={9} gap={5.4} r={0.7} colors={['amber']} blink={false} />
          </g>
        );
      })}
      {/* command tower */}
      <Slab x={148} y={44} w={66} h={40} k={2} c="hull" o={1} />
      <Slab x={158} y={50} w={46} h={28} k={2} c="steel" o={0.8} />
      <Slab x={170} y={56} w={22} h={16} k={1.5} c="dark" o={0.6} />
      {[154, 162, 170, 178, 186, 194, 202].map((x, i) => <rect key={x} x={x} y="47" width="5" height="1.8" fill={S.amber} opacity={0.5 + (i % 3) * 0.2} />)}
      {[172, 178, 184].map((x) => <rect key={x} x={x} y="60" width="4" height="2" fill={S.cyan} opacity="0.8" />)}
      <line x1="181" y1="56" x2="181" y2="36" stroke={ART.steel.hi} strokeWidth="1" />
      <Bev t="polygon" points={pts(ngon(181, 34, 4, 2.6, 8))} c="hull" o={0.5} />
      <circle cx="181" cy="31" r="1.2" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      <Seams x={148} y={44} w={66} h={40} cols={5} rows={2} op={0.3} />
      {/* gun batteries */}
      {[100, 128, 232, 262].map((x, i) => turret(x, 40, i === 2 ? 6 : 0, 'a' + x, i === 2))}
      {[100, 128, 232, 262].map((x, i) => turret(x, 88, 0, 'b' + x))}
      <polygon points={pts([[226, 54], [232, 47], [238, 54]])} fill={S.amber} />
      <text x="266" y="56" textAnchor="end" fontSize="4.2" fill={S.amber} {...PT}>6 DEG OFF AXIS</text>
      {/* seven stern thrusters */}
      <Bev t="polygon" points={pts([[14, 46], [38, 40], [38, 88], [14, 82]])} c="dark" o={1} />
      {[...ngon(26, 64, 10.5, 10.5, 6, 0), [26, 64]].map(([x, y], i) => (
        <g key={i}>
          <Bev t="polygon" points={pts(ngon(x, y, 4.6, 4.6, 6))} c="steel" o={0.4} />
          <polygon points={pts(ngon(x, y, 2.8, 2.8, 6))} fill={ART.ink} />
          <Glow cx={x} cy={y} r={4.8} c={S.cyan} opacity={0.4} style={{ animation:`ship-engine ${4 + (i % 3)}s ease-in-out ${i * 0.5}s infinite` }} />
        </g>
      ))}
      {/* skeletal bow: construction */}
      <polygon points={pts(skel)} fill={ART.screen.off} opacity="0.55" />
      <polyline points={pts(skel)} fill="none" stroke={ART.steel.hi} strokeWidth="1.2" />
      <line x1="300" y1="64" x2="432" y2="64" stroke={ART.dark.base} strokeWidth="3.2" /><line x1="300" y1="64" x2="432" y2="64" stroke={ART.steel.hi} strokeWidth="1" />
      <line x1="300" y1="48" x2="414" y2="60" stroke={ART.steel.base} strokeWidth="1" /><line x1="300" y1="80" x2="414" y2="68" stroke={ART.steel.base} strokeWidth="1" />
      {ribs.map((x) => <g key={x}><line x1={x} y1={yTop(x)} x2={x} y2={128 - yTop(x)} stroke={ART.dark.base} strokeWidth="2.4" /><line x1={x} y1={yTop(x)} x2={x} y2={128 - yTop(x)} stroke={ART.steel.hi} strokeWidth="0.9" /></g>)}
      <Bev t="polygon" points={pts([[302, 34], [324, 37], [324, 62], [302, 62]])} c="hull" o={0.8} />
      <Bev t="polygon" points={pts([[328, 39], [346, 42], [346, 62], [328, 62]])} c="hull" o={0.8} />
      <Bev t="polygon" points={pts([[302, 66], [324, 66], [324, 91], [302, 94]])} c="hull" o={0.8} />
      <Bev t="polygon" points={pts([[348, 66], [366, 66], [366, 82], [348, 85]])} c="hull" o={0.8} />
      <Seams x={302} y={34} w={44} h={28} cols={1} rows={1} op={0.4} />
      {/* crane with a lifted armor plate */}
      <Slab x={336} y={13} w={30} h={5} k={1} c="steel" />
      <line x1="351" y1="18" x2="340" y2="26" stroke={ART.steel.hi} strokeWidth="0.8" /><line x1="351" y1="18" x2="362" y2="26" stroke={ART.steel.hi} strokeWidth="0.8" />
      <Bev t="polygon" points={pts([[338, 26], [364, 26], [362, 34], [340, 34]])} c="hull" o={0.7} />
      <Hazard x={340} y={33} w={22} h={2} />
      {/* suited workers with headlamps */}
      {[[318, 52], [338, 56], [372, 66], [356, 74], [312, 74], [392, 62]].map(([x, y], i) => (
        <g key={i}><circle cx={x} cy={y} r="1.5" fill={ART.hull.hi} /><circle cx={x + 1.2} cy={y - 0.2} r="0.7" fill={S.amberHi} /></g>
      ))}
      {/* welding sparks and floodlights */}
      {[[290, 66, 0], [322, 44, 0.4], [372, 72, 0.8]].map(([x, y, d], i) => <Glow key={i} cx={x} cy={y} r={11} c={S.orange} opacity={0.9} style={{ animation:`ship-spark 1.1s ease-in-out ${d}s infinite`, transformBox:'fill-box', transformOrigin:'center' }} />)}
      {[104, 248, 392].map((x) => <g key={x}><Glow cx={x} cy={18} r={16} c={S.amberHi} opacity={0.5} /><circle cx={x} cy="14" r="2.4" fill={S.amberHi} /></g>)}
      <text x="150" y="122.4" fontSize="5.4" fill={ART.hull.hi} opacity="0.9" letterSpacing="1" {...PT}>VALOR CLASS CRUISER · HULL 0417</text>
      <text x="300" y="10.2" fontSize="5" fill={ART.ink} fontWeight="700" letterSpacing="1" {...PT}>KDY RING DRYDOCK 2</text>
    </svg>
  );
}

function ShipCzerkaHauler({ accent }) {
  const S = ART.signal;
  const cols = [52, 82, 112, 142];
  const top = ['amber', 'rust', 'tan', 'steel'], bot = ['teal', 'amber', null, 'tan'];
  const box = (x, y, c, k, haz) => (
    <g key={k}>
      {haz ? <Hazard x={x} y={y} w={26} h={24} /> : null}
      <Slab x={haz ? x + 2 : x} y={haz ? y + 2 : y} w={haz ? 22 : 26} h={haz ? 20 : 24} k={1.4} c={c} o={0.9} />
      <Seams x={x} y={y} w={26} h={24} cols={3} op={0.35} />
      <rect x={x + 1} y={y + 1} width="2" height="2" fill={ART.dark.shade} /><rect x={x + 23} y={y + 1} width="2" height="2" fill={ART.dark.shade} />
      <rect x={x + 1} y={y + 21} width="2" height="2" fill={ART.dark.shade} /><rect x={x + 23} y={y + 21} width="2" height="2" fill={ART.dark.shade} />
      <rect x={x + 9} y={y + 9} width="8" height="5" fill={ART.paper.base} opacity="0.85" /><rect x={x + 10} y={y + 10.4} width="4" height="1" fill={ART.red.base} />
      <Grime x={x} y={y} w={26} h={24} seed={x + y} n={3} op={0.16} />
    </g>
  );
  return (
    <svg viewBox="0 0 224 160" width="224" height="160" style={SHIP_SVG_STYLE}>
      <rect x="3" y="3" width="218" height="154" fill={ART.screen.off} opacity="0.55" />
      <path d="M8 22 L8 8 L22 8 M216 8 L216 22 M8 138 L8 152 L22 152 M216 138 L216 152 L202 152" fill="none" stroke={ART.amber.base} strokeWidth="1" opacity="0.7" />
      <PropShadow cx={114} cy={89} rx={96} ry={52} />
      {/* truss spine */}
      <Slab x={40} y={75} w={146} h={10} k={1} c="dark" />
      {Array.from({ length: 14 }, (_, i) => <line key={i} x1={44 + i * 10} y1={75} x2={54 + i * 10} y2={85} stroke={ART.steel.hi} strokeWidth="0.6" opacity="0.6" />)}
      <text x="112" y="82.4" textAnchor="middle" fontSize="5" fill={ART.amber.hi} letterSpacing="1.2" {...PT}>CZERKA CORP · BULK HAULER</text>
      {/* cargo containers */}
      {cols.map((x, i) => box(x, 50, top[i], 't' + x, i === 1))}
      {cols.map((x, i) => bot[i] ? box(x, 90, bot[i], 'b' + x, false) : (
        <g key={'e' + x}>
          <rect x={x} y="90" width="26" height="24" fill={ART.ink} opacity="0.7" />
          <polyline points={pts([[x, 90], [x + 26, 90], [x + 26, 114], [x, 114], [x, 90]])} fill="none" stroke={ART.amber.base} strokeWidth="0.9" strokeDasharray="3 2" />
          <line x1={x} y1="90" x2={x + 26} y2="114" stroke={ART.amber.base} strokeWidth="0.5" opacity="0.5" /><line x1={x + 26} y1="90" x2={x} y2="114" stroke={ART.amber.base} strokeWidth="0.5" opacity="0.5" />
          {[[x, 90], [x + 26, 90], [x, 114], [x + 26, 114]].map(([a, b], j) => <rect key={j} x={a - 1.6} y={b - 1.6} width="3.2" height="3.2" fill={ART.steel.hi} />)}
        </g>
      ))}
      {/* bridge module */}
      <Bev t="polygon" points={pts([[8, 80], [18, 64], [36, 58], [48, 58], [48, 102], [36, 102], [18, 96]])} c="steel" o={1} />
      <Bev t="polygon" points={pts([[13, 80], [21, 70], [36, 66], [36, 94], [21, 90]])} c="glass" o={0.6} />
      <line x1="28" y1="66" x2="28" y2="94" stroke={ART.dark.base} strokeWidth="0.9" />
      <polygon points={pts([[16, 74], [30, 68], [24, 76]])} fill={S.white} opacity="0.35" />
      <Seams x={36} y={58} w={12} h={44} rows={4} op={0.4} /><Greeble x={38} y={60} w={9} h={40} seed={61} n={5} c="steel" />
      <line x1="40" y1="58" x2="40" y2="46" stroke={ART.steel.hi} strokeWidth="0.9" /><circle cx="40" cy="45" r="1.2" fill={S.amber} style={{ animation:'ship-blink 2s steps(2) infinite' }} />
      {/* engine block under repair */}
      <Bev t="polygon" points={pts([[184, 54], [210, 48], [222, 60], [222, 100], [210, 112], [184, 106]])} c="dark" o={1} />
      <Bev t="polygon" points={pts([[188, 54], [202, 36], [218, 40], [216, 54]])} c="steel" o={0.7} /><Hazard x={192} y={52} w={24} h={3} />
      <Bev t="polygon" points={pts([[188, 106], [202, 124], [218, 120], [216, 106]])} c="steel" o={0.7} /><Hazard x={192} y={105} w={24} h={3} />
      <Bev t="polygon" points={pts(ngon(201, 80, 21, 21, 16))} c="dark" o={0.8} />
      <polygon points={pts(ngon(201, 80, 16, 16, 16))} fill="none" stroke={ART.amber.base} strokeWidth="2" strokeDasharray="4 2.6" />
      <polygon points={pts(ngon(201, 80, 11, 11, 8))} fill={ART.ink} />
      <Glow cx={201} cy={80} r={11} c={S.cyan} opacity={0.95} style={{ animation:'ship-engine 2.2s ease-in-out infinite' }} />
      <path d="M194 76 L199 80 L195 84 M208 76 L203 80 L207 84" fill="none" stroke={S.cyanHi} strokeWidth="0.8" />
      {[[186, 46, 220, 46], [186, 114, 220, 114]].map(([a, b, c, d], i) => <line key={i} x1={a} y1={b} x2={c} y2={d} stroke={ART.signal.warn} strokeWidth="1.6" />)}
      {/* conduit crawler working in the core */}
      {[0, 1, 2].map((i) => <Bev key={i} t="polygon" points={pts(ngon(190 + i * 4.4, 98, 2.6, 2.2, 6))} c="copper" o={0.4} />)}
      <Cable d="M222 80 Q224 94 220 124" c={ART.copper.base} w={2} hi="copper" />
      <circle cx="210" cy="52" r="1" fill={S.amberHi} /><Glow cx={210} cy={52} r={9} c={S.orange} opacity={0.8} style={{ animation:'ship-spark 1.3s ease-in-out infinite', transformBox:'fill-box', transformOrigin:'center' }} />
      {[[50, 40], [172, 40], [50, 122], [172, 122]].map(([x, y], i) => <Bev key={i} t="polygon" points={pts(ngon(x, y, 4.2, 3.6, 6))} c="dark" o={0.5} />)}
      <Slab x={150} y={124} w={16} h={10} k={1} c="amber" /><line x1="150" y1="128" x2="166" y2="128" stroke={ART.ink} strokeWidth="0.6" />
      <g><Glow cx={178} cy={138} r={13} c={S.amberHi} opacity={0.5} /><circle cx="178" cy="138" r="2.4" fill={S.amberHi} /></g>
    </svg>
  );
}

function ShipKuatYacht({ accent }) {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 160 128" width="160" height="128" style={SHIP_SVG_STYLE}>
      <rect x="3" y="3" width="154" height="122" fill={ART.screen.off} opacity="0.55" />
      <rect x="3" y="3" width="154" height="122" fill="none" stroke={accent} strokeWidth="0.8" strokeDasharray="5 4" opacity="0.5" />
      {[[10, 10], [150, 10], [10, 118], [150, 118]].map(([x, y], i) => <circle key={i} cx={x} cy={y} r="2.2" fill={accent} style={{ animation:`ship-blink 2.4s ease-in-out ${i * 0.4}s infinite` }} />)}
      <PropShadow cx={84} cy={71} rx={66} ry={32} />
      {/* slender swept wing blades */}
      {[0, 1].map((m) => {
        const f = (y) => (m ? 128 - y : y);
        return (
          <g key={m}>
            <Bev t="polygon" points={pts([[84, f(56)], [118, f(20)], [148, f(10)], [142, f(48)]])} c="pearl" o={0.9} />
            <polyline points={pts([[84, f(56)], [118, f(20)], [148, f(10)]])} fill="none" stroke={ART.brass.base} strokeWidth="0.9" />
            <polygon points={pts([[100, f(52)], [124, f(26)], [128, f(28)], [108, f(54)]])} fill={ART.pearl.hi} opacity="0.5" />
            <Seams x={92} y={m ? 78 : 14} w={50} h={36} cols={2} op={0.25} />
            <circle cx="147" cy={f(11)} r="1.8" fill={m ? S.green : S.red} style={{ animation:`ship-blink 1.5s steps(2) ${m * 0.75}s infinite` }} />
          </g>
        );
      })}
      {/* needle fuselage */}
      <Bev t="polygon" points={pts([[4, 64], [22, 60], [52, 56], [96, 52], [136, 50], [152, 56], [156, 64], [152, 72], [136, 78], [96, 76], [52, 72], [22, 68]])} c="pearl" o={1} />
      <polyline points={pts([[8, 63], [24, 59], [54, 55], [96, 51], [136, 49]])} fill="none" stroke={ART.brass.base} strokeWidth="0.9" />
      <polyline points={pts([[8, 65], [24, 69], [54, 73], [96, 77], [136, 79]])} fill="none" stroke={ART.brass.base} strokeWidth="0.9" opacity="0.8" />
      <rect x="20" y="63" width="130" height="2" fill={ART.brand.kdy} /><rect x="20" y="63" width="130" height="0.6" fill={S.cyanHi} opacity="0.8" />
      <Seams x={20} y={52} w={132} h={24} cols={8} op={0.2} />
      <Scuff x={24} y={54} w={120} h={20} seed={12} n={8} c="pearl" />
      {/* dorsal ridge and crest */}
      <Bev t="polygon" points={pts([[90, 57], [130, 55], [134, 60], [134, 68], [130, 73], [90, 71]])} c="hull" o={0.6} />
      <Vent x={118} y={58} w={10} h={4} n={2} /><Vent x={118} y={66} w={10} h={4} n={2} />
      <Bev t="polygon" points={pts(ngon(106, 64, 6, 6, 12))} c="dark" o={0.5} />
      <polygon points={pts([[102.4, 66.4], [106, 60.4], [109.6, 66.4], [106, 64.8]])} fill={ART.brass.hi} />
      {[94, 102, 110, 118, 126].map((x) => <g key={x}><circle cx={x} cy="56" r="0.9" fill={S.amberHi} /><circle cx={x} cy="72" r="0.9" fill={S.amberHi} /></g>)}
      {/* canopy */}
      <Bev t="polygon" points={pts([[24, 64], [40, 59], [72, 56], [88, 64], [72, 72], [40, 69]])} c="deepglass" o={0.7} />
      <path d="M44 59 L44 69 M58 57 L58 71 M72 56 L72 72" stroke={ART.brass.base} strokeWidth="0.7" opacity="0.8" />
      <polygon points={pts([[32, 61], [52, 58], [66, 58], [40, 62]])} fill={S.white} opacity="0.45" />
      {/* twin engine nacelles */}
      {[0, 1].map((m) => {
        const f = (y) => (m ? 128 - y : y);
        return (
          <g key={m}>
            <Bev t="polygon" points={pts([[132, f(42)], [156, f(44)], [160, f(50)], [160, f(54)], [134, f(54)]])} c="dark" o={0.7} />
            <rect x="152" y={m ? 74 : 46} width="6" height="3" fill={ART.ink} />
            <Glow cx={158} cy={m ? 75.5 : 52.5} r={6} c={S.cyan} opacity={0.8} style={{ animation:`ship-engine 3s ease-in-out ${m * 0.9}s infinite` }} />
          </g>
        );
      })}
      <Slab x={134} y={61} w={14} h={5} k={1} c="brass" o={0.5} />
      <text x="141" y="65" textAnchor="middle" fontSize="2.6" fill={ART.ink} fontWeight="700" {...PT}>LK-001</text>
      {[[50, 48], [50, 80], [118, 46], [118, 82]].map(([x, y], i) => <Bev key={i} t="polygon" points={pts(ngon(x, y, 2.6, 2.4, 6))} c="brass" o={0.3} />)}
    </svg>
  );
}

function ShipKdyShuttle({ accent }) {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 192 128" width="192" height="128" style={SHIP_SVG_STYLE}>
      <rect x="3" y="3" width="186" height="122" fill={ART.screen.off} opacity="0.55" />
      <rect x="3" y="3" width="186" height="122" fill="none" stroke={accent} strokeWidth="0.8" strokeDasharray="6 4" opacity="0.5" />
      <Hazard x={6} y={6} w={180} h={3} />
      <Hazard x={6} y={119} w={180} h={3} />
      <PropShadow cx={98} cy={67} rx={80} ry={40} />
      {/* broad swept wings */}
      {[0, 1].map((m) => {
        const f = (y) => (m ? 128 - y : y);
        return (
          <g key={m}>
            <Bev t="polygon" points={pts([[70, f(46)], [104, f(14)], [150, f(10)], [150, f(44)]])} c="hull" />
            <polygon points={pts([[104, f(14)], [150, f(10)], [150, f(15)], [110, f(19)]])} fill={ART.brand.kdy} />
            <Seams x={78} y={m ? 86 : 18} w={68} h={28} cols={3} rows={1} op={0.3} />
            <Greeble x={112} y={m ? 90 : 22} w={34} h={12} seed={61 + m} n={5} c="hull" />
            <circle cx="149" cy={f(12)} r="1.8" fill={m ? S.green : S.red} style={{ animation:`ship-blink 1.5s steps(2) ${m * 0.7}s infinite` }} />
          </g>
        );
      })}
      {/* fuselage */}
      <Bev t="polygon" points={pts([[8, 64], [26, 56], [58, 48], [100, 44], [150, 44], [176, 52], [184, 64], [176, 76], [150, 84], [100, 84], [58, 80], [26, 72]])} c="pearl" o={1} />
      <rect x="22" y="62" width="150" height="4" fill={ART.brand.kdy} /><rect x="22" y="62" width="150" height="1" fill={S.cyanHi} opacity="0.7" />
      <Seams x={24} y={48} w={150} h={32} cols={8} op={0.2} />
      <Grime x={24} y={48} w={150} h={32} seed={14} n={6} op={0.12} />
      <Scuff x={28} y={50} w={140} h={28} seed={5} n={9} c="pearl" />
      {/* dorsal cargo hatch and crest */}
      <Bev t="polygon" points={pts([[100, 52], [150, 50], [158, 58], [158, 70], [150, 78], [100, 76]])} c="hull" o={0.7} />
      <Seams x={102} y={52} w={54} h={24} cols={4} rows={2} op={0.35} />
      <Vent x={132} y={54} w={14} h={4} n={3} /><Vent x={132} y={70} w={14} h={4} n={3} />
      <Bev t="polygon" points={pts(ngon(84, 64, 8, 8, 12))} c="dark" o={0.5} />
      <polygon points={pts([[79.6, 67], [84, 59.4], [88.4, 67], [84, 65]])} fill={ART.brass.hi} />
      <text x="130" y="67" textAnchor="middle" fontSize="5" fill={ART.ink} fontWeight="700" letterSpacing="0.6" {...PT}>KDY K-14</text>
      {/* canopy */}
      <Bev t="polygon" points={pts([[12, 64], [28, 58], [52, 54], [66, 64], [52, 74], [28, 70]])} c="deepglass" o={0.75} />
      <path d="M32 58 L32 70 M44 55 L44 73 M56 55 L56 73" stroke={ART.dark.base} strokeWidth="0.9" opacity="0.85" />
      <polygon points={pts([[20, 60], [40, 56], [52, 57], [28, 62]])} fill={S.white} opacity="0.4" />
      {/* twin engines */}
      {[0, 1].map((m) => {
        const f = (y) => (m ? 128 - y : y);
        return (
          <g key={m}>
            <Bev t="polygon" points={pts([[140, f(44)], [176, f(42)], [184, f(48)], [184, f(54)], [140, f(56)]])} c="dark" />
            <polygon points={pts([[178, f(43)], [184, f(46)], [184, f(52)], [178, f(54)]])} fill={ART.ink} />
            <Glow cx={184} cy={m ? 79 : 49} r={7} c={S.cyan} opacity={0.8} style={{ animation:`ship-engine 3s ease-in-out ${m * 0.8}s infinite` }} />
            <Seams x={142} y={m ? 74 : 44} w={32} h={10} cols={3} op={0.4} />
          </g>
        );
      })}
      <line x1="58" y1="48" x2="58" y2="38" stroke={ART.steel.hi} strokeWidth="0.9" /><circle cx="58" cy="37" r="1.1" fill={S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      {[[48, 44], [48, 84], [120, 40], [120, 88]].map(([x, y], i) => <Bev key={i} t="polygon" points={pts(ngon(x, y, 3.4, 3, 6))} c="dark" o={0.5} />)}
    </svg>
  );
}

const ShipSprite = React.memo(function ShipSprite({ kind, accent }) {
  if (kind === 'free_horizon') return <ShipFreeHorizon accent={accent} />;
  if (kind === 'republic_shuttle') return <ShipRepublicShuttle accent={accent} />;
  if (kind === 'valor_frame') return <ShipValorFrame accent={accent} />;
  if (kind === 'czerka_hauler') return <ShipCzerkaHauler accent={accent} />;
  if (kind === 'kuat_yacht') return <ShipKuatYacht accent={accent} />;
  if (kind === 'kdy_shuttle') return <ShipKdyShuttle accent={accent} />;
  return null;
});

function FloorTilePattern({ kind }) {
  if (kind === 'marble') return (
    <div style={{ position:'absolute',inset:0,background:'linear-gradient(135deg, transparent 30%, rgba(255,255,255,0.06) 50%, transparent 70%)',pointerEvents:'none',zIndex:1 }} />
  );
  if (kind === 'grate') return (
    <div style={{ position:'absolute',inset:0,backgroundImage:'repeating-linear-gradient(0deg,rgba(0,0,0,0.18) 0px,rgba(0,0,0,0.18) 1px,transparent 1px,transparent 8px),repeating-linear-gradient(90deg,rgba(0,0,0,0.18) 0px,rgba(0,0,0,0.18) 1px,transparent 1px,transparent 8px)',pointerEvents:'none',zIndex:1 }} />
  );
  if (kind === 'polished') return (
    <div style={{ position:'absolute',inset:0,background:'linear-gradient(180deg, rgba(255,255,255,0.04) 0%, transparent 60%)',pointerEvents:'none',zIndex:1 }} />
  );
  if (kind === 'rough') return (
    <div style={{ position:'absolute',inset:0,backgroundImage:'repeating-linear-gradient(45deg, rgba(0,0,0,0.12) 0px, rgba(0,0,0,0.12) 2px, transparent 2px, transparent 6px)',pointerEvents:'none',zIndex:1 }} />
  );
  return null;
}

function VendorOverlay({ npc, inventory, credits, alignment, onBuy, onSell, onClose, flags = {}, zoneId = '' }) {
  const [activeTab, setActiveTab] = React.useState('buy');
  const [selectedItem, setSelectedItem] = React.useState(null);
  const [feedback, setFeedback] = React.useState('');

  React.useEffect(() => {
    const handler = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [onClose]);

  const faction = npc.vendorFaction || 'underworld';
  const rep = alignment?.loyalty?.[faction] || 0;
  const tier = rep >= 41 ? 'honored' : rep >= 11 ? 'favored' : rep <= -51 ? 'hostile' : 'neutral';
  const worldDisc = vendorWorldDiscount(flags, zoneId);
  const discount = (tier === 'honored' ? 0.70 : tier === 'favored' ? 0.85 : 1.0) * worldDisc;

  const stock = (npc.vendorStock || []).map(id => typeof id === 'string' ? ITEMS[id] : id).filter(Boolean);

  const handleBuy = (item) => {
    if (tier === 'hostile') { setFeedback('They refuse to deal with you.'); return; }
    const price = Math.floor(item.value * discount);
    if (credits < price) { setFeedback('Insufficient credits.'); return; }
    onBuy(item, price);
    setFeedback(`Purchased ${item.name} for ${price} cr.`);
  };

  const handleSell = (item) => {
    const sellPrice = Math.floor((item.value || 0) * 0.5);
    onSell(item, sellPrice);
    setFeedback(`Sold ${item.name} for ${sellPrice} cr.`);
  };

  const tierColor = { honored: '#E8C97A', favored: '#8FA6FF', neutral: '#888', hostile: '#FF4444' };

  return (
    <div style={{ position:'absolute', inset:0, background:'rgba(0,0,0,0.88)', zIndex:28, display:'flex', flexDirection:'column', padding:'24px', fontFamily:'monospace' }}>
      <div style={{ display:'flex', justifyContent:'space-between', marginBottom:'16px' }}>
        <div>
          <div style={{ color:'#E8C97A', fontSize:'18px', fontWeight:'bold' }}>{npc.label}</div>
          <div style={{ color: tierColor[tier], fontSize:'12px', marginTop:'4px' }}>
            Reputation: {tier.toUpperCase()} {tier !== 'hostile' && discount < 1 && `(${Math.round((1 - discount) * 100)}% discount${worldDisc < 1 ? ', world effects included' : ''})`}
          </div>
        </div>
        <div style={{ color:'#E8C97A' }}>Credits: {credits}</div>
      </div>
      {npc.vendorGreeting && <div style={{ color:'#9A9070', fontSize:'12px', fontStyle:'italic', marginBottom:'12px' }}>{npc.vendorGreeting}</div>}

      <div style={{ display:'flex', gap:'8px', marginBottom:'16px' }}>
        {['buy', 'sell'].map(tab => (
          <div key={tab} onClick={() => { setActiveTab(tab); setSelectedItem(null); setFeedback(''); }}
            style={{ padding:'6px 18px', background: activeTab === tab ? '#1A2A1A' : 'transparent',
              border: `1px solid ${activeTab === tab ? '#4CAF50' : '#333'}`, color: activeTab === tab ? '#4CAF50' : '#666',
              cursor:'pointer', borderRadius:'4px', textTransform:'uppercase', fontSize:'12px' }}>
            {tab}
          </div>
        ))}
        <div onClick={onClose} style={{ marginLeft:'auto', padding:'6px 18px', background:'transparent',
          border:'1px solid #333', color:'#666', cursor:'pointer', borderRadius:'4px', fontSize:'12px' }}>
          [ESC] Close
        </div>
      </div>

      {tier === 'hostile' && (
        <div style={{ color:'#FF4444', padding:'12px', border:'1px solid #FF4444', marginBottom:'12px', textAlign:'center' }}>
          They refuse to deal with you. Your reputation here is too low.
        </div>
      )}

      <div style={{ display:'flex', gap:'16px', flex:1, overflow:'hidden' }}>
        <div style={{ flex:1, overflowY:'auto', display:'flex', flexDirection:'column', gap:'4px' }}>
          {activeTab === 'buy' && stock.map((item, i) => {
            const price = Math.floor(item.value * discount);
            return (
              <div key={i} onClick={() => setSelectedItem(item)}
                style={{ padding:'8px 12px', background: selectedItem?.id === item.id ? '#1A2A1A' : 'transparent',
                  border: `1px solid ${selectedItem?.id === item.id ? '#4CAF50' : '#222'}`,
                  cursor:'pointer', display:'flex', justifyContent:'space-between', borderRadius:'4px' }}>
                <span style={{ color:'#CCC' }}>{item.name}</span>
                <span style={{ color:'#E8C97A' }}>{price} cr</span>
              </div>
            );
          })}
          {activeTab === 'sell' && inventory.filter(i => i.type !== 'quest').map((item, idx) => (
            <div key={idx} onClick={() => setSelectedItem(item)}
              style={{ padding:'8px 12px', background: selectedItem?.id === item.id ? '#1A1A2A' : 'transparent',
                border: `1px solid ${selectedItem?.id === item.id ? '#8FA6FF' : '#222'}`,
                cursor:'pointer', display:'flex', justifyContent:'space-between', borderRadius:'4px' }}>
              <span style={{ color:'#CCC' }}>{item.name}</span>
              <span style={{ color:'#8FA6FF' }}>{Math.floor((item.value || 0) * 0.5)} cr</span>
            </div>
          ))}
        </div>

        <div style={{ width:'200px', borderLeft:'1px solid #222', paddingLeft:'16px' }}>
          {selectedItem && (
            <div>
              <div style={{ color:'#E8C97A', marginBottom:'8px', fontWeight:'bold' }}>{selectedItem.name}</div>
              <div style={{ color:'#888', fontSize:'12px', marginBottom:'12px', lineHeight:'1.5' }}>{selectedItem.description}</div>
              <button onClick={() => activeTab === 'buy' ? handleBuy(selectedItem) : handleSell(selectedItem)}
                disabled={tier === 'hostile'}
                style={{ width:'100%', padding:'8px', background: tier === 'hostile' ? '#333' : activeTab === 'buy' ? '#1A2A1A' : '#1A1A2A',
                  border: `1px solid ${tier === 'hostile' ? '#444' : activeTab === 'buy' ? '#4CAF50' : '#8FA6FF'}`,
                  color: tier === 'hostile' ? '#444' : activeTab === 'buy' ? '#4CAF50' : '#8FA6FF',
                  cursor: tier === 'hostile' ? 'not-allowed' : 'pointer', borderRadius:'4px', fontFamily:'monospace' }}>
                {activeTab === 'buy' ? `Buy ${Math.floor(selectedItem.value * discount)} cr` : `Sell ${Math.floor((selectedItem.value || 0) * 0.5)} cr`}
              </button>
            </div>
          )}
          {feedback && <div style={{ color:'#E8C97A', marginTop:'12px', fontSize:'12px', padding:'8px', border:'1px solid #333', borderRadius:'4px' }}>{feedback}</div>}
        </div>
      </div>
    </div>
  );
}

function resolveDialoguePhase(npc, questFlags) {
  if (!npc.phases || npc.phases.length === 0) return npc;
  let active = npc.phases[0];
  for (const phase of npc.phases) {
    const allMet = (phase.requiresAllFlags || []).every(f => questFlags[f]);
    const noneMet = !(phase.requiresNoneFlags || []).some(f => questFlags[f]);
    const anyMet = !phase.requiresAnyFlag || phase.requiresAnyFlag.some(f => questFlags[f]);
    if (allMet && noneMet && anyMet) active = phase;
  }
  return {
    ...npc,
    prompt: active.prompt ?? npc.prompt,
    choices: active.choices ?? npc.choices,
    repeatPrompt: active.repeatPrompt ?? npc.repeatPrompt,
    vendorGreeting: active.vendorGreeting ?? npc.vendorGreeting,
    _activePhaseId: active.id,
  };
}








// ===== CONQUEST ENGINE BEGIN =====
// Coruscant Conquest v2: a pure, testable strategy engine. No React in here. State is plain JSON.
// Campaign layer: sectors, garrisons, buildings, economy, enemy factions.
// Tactical layer (CQ.tb): themed battle maps, deployment, unit abilities, AI, auto-resolve.
const CQ = (() => {
  const UNIT_ORDER = ['inf', 'gam', 'snp', 'med', 'spc', 'drd', 'tnk', 'wlk'];
  const UNITS = {
    inf: { id: 'inf', ucr: 3, name: 'Enforcer Infantry', short: 'Enforcer', role: 'Line trooper', cr: 150, pwr: 0, upkeep: 0, req: null, hp: 12, mov: 4, rng: 4, atk: 4, armor: 0, col: '#8FB4D8',
      ab: { id: 'suppress', name: 'Suppressing Fire', cd: 2, desc: 'Pins a target: -2 move and -2 attack next turn. Armor reduces the damage it deals.' },
      passive: 'Cheap and numerous. Holds cover well.', lore: 'Street muscle in matched jackets. Not elite, not afraid.' },
    gam: { id: 'gam', ucr: 6, name: 'Gamorrean Bruiser', short: 'Bruiser', role: 'Melee brawler', cr: 400, pwr: 0, upkeep: 0, req: 'barracks', hp: 24, mov: 3, rng: 1.5, atk: 8, armor: 2, col: '#7FB26A',
      ab: { id: 'smash', name: 'Skull Smash', cd: 2, desc: 'Hits every enemy next to it for full damage.' },
      passive: 'Shrugs off blaster fire. Must close to melee.', lore: 'Hired for volume and enthusiasm. Needs a Barracks.' },
    snp: { id: 'snp', ucr: 6, name: 'Covert Marksman', short: 'Marksman', role: 'Long range sniper', cr: 350, pwr: 1, upkeep: 0, req: null, hp: 8, mov: 3, rng: 8, atk: 8, armor: 0, col: '#C8A0E0',
      ab: null, passive: 'Steady Aim: +4 damage if it has not moved this turn.', lore: 'One shot, one invoice.' },
    med: { id: 'med', ucr: 4, name: 'Field Medic', short: 'Medic', role: 'Support healer', cr: 250, pwr: 0, upkeep: 0, req: null, hp: 10, mov: 4, rng: 3, atk: 2, armor: 0, col: '#7FE0A8',
      ab: { id: 'heal', name: 'Stim Injection', cd: 0, desc: 'Heals an ally within 4 tiles for 8 hit points.' },
      passive: 'Keeps the line alive. Priority target.', lore: 'Black bag, steady hands, no questions.' },
    spc: { id: 'spc', ucr: 8, name: 'Speeder Cavalry', short: 'Speeder', role: 'Fast flanker', cr: 500, pwr: 1, upkeep: 0, req: 'motor_pool', hp: 14, mov: 8, rng: 1.5, atk: 6, armor: 0, col: '#F0B060',
      ab: null, passive: 'Charge: +4 damage if it moved 4 or more tiles first.', lore: 'Swoop riders in syndicate colors. Needs a Motor Pool.' },
    drd: { id: 'drd', ucr: 12, name: 'Combat Droid', short: 'Droid', role: 'Armored shooter', cr: 700, pwr: 2, upkeep: 1, req: 'droid_foundry', hp: 18, mov: 4, rng: 5, atk: 6, armor: 2, col: '#E07070',
      ab: { id: 'overcharge', name: 'Overcharge', cd: 2, desc: 'Next shot deals +4 damage. The droid takes 4 damage.' },
      passive: 'Immune to suppression and hazards. Needs a Droid Foundry.', lore: 'Battle chassis with a security license and no sense of fear.' },
    tnk: { id: 'tnk', ucr: 18, name: 'Assault Tank', short: 'Tank', role: 'Armored fire support', cr: 1200, pwr: 5, upkeep: 1, req: 'motor_pool', hp: 36, mov: 3, rng: 6, atk: 10, armor: 4, col: '#F08A4A',
      ab: { id: 'shell', name: 'Siege Shell', cd: 3, desc: 'Fires an explosive shell: the target and every enemy within 1 tile of it take damage.' },
      passive: 'Heavy armor shrugs off rifles. Walls do not stop shells. Needs a Motor Pool.', lore: 'A repulsor tank with a gang boss painted on the turret.' },
    wlk: { id: 'wlk', ucr: 40, name: 'Scout Walker', short: 'Walker', role: 'Heavy walker', cr: 2200, pwr: 6, upkeep: 2, req: 'war_college', hp: 48, mov: 4, rng: 7, atk: 10, armor: 6, col: '#E0D070',
      ab: { id: 'barrage', name: 'Twin Barrage', cd: 2, desc: 'Fires twice at one target in a single action.' },
      passive: 'The biggest thing on the street. Needs a War College.', lore: 'Two legs, two guns, one very expensive tantrum.' },
  };
  // Turret emplacement (spawned by Auto Turret Nests). Not recruitable.
  const TURRET = { id: 'trt', name: 'Auto Turret', short: 'Turret', role: 'Emplacement', hp: 16, mov: 0, rng: 6, atk: 6, armor: 2, col: '#D0D0D0', ab: null, passive: 'Immobile.', immobile: true };
  UNITS.trt = TURRET;
  const unitPower = (u) => u.hp * (u.atk + (u.ab ? 0.6 : 0)) * (1 + 0.18 * u.armor) * (0.65 + Math.min(u.rng, 8) / 14);

  // Buildings: 4 slots per sector (+1 Command Center). Each one is a real tactical or economic decision.
  const BUILDINGS = [
    // economy
    { id: 'relay', cat: 'Economy', name: 'Black Market Relay', cost: 1200, pwr: 1, req: [], income: 130, desc: '+130 CR per turn.' },
    { id: 'slicehub', cat: 'Economy', name: 'Slicing Hub', cost: 800, pwr: 2, req: [], income: 150, heatDecay: 1, desc: '+150 CR per turn. Heat -1 per turn.' },
    { id: 'casino', cat: 'Economy', name: 'Syndicate Casino', cost: 2200, pwr: 1, req: ['relay'], income: 280, desc: '+280 CR per turn. Needs a Relay.' },
    { id: 'spice_lab', cat: 'Economy', name: 'Spice Refinery', cost: 1800, pwr: 1, req: ['relay'], income: 360, heatGen: 2, desc: '+360 CR per turn. Heat +2 per turn. Needs a Relay.' },
    { id: 'exchange_tower', cat: 'Economy', name: 'Syndicate Exchange Tower', cost: 5200, pwr: 3, req: ['relay', 'slicehub'], income: 620, heatGen: 1, desc: '+620 CR per turn. Needs a Relay and a Slicing Hub.' },
    { id: 'safehouse', cat: 'Economy', name: 'Safehouse Network', cost: 900, pwr: 0, req: [], heatDecay: 3, desc: 'Heat -3 per turn.' },
    // power
    { id: 'substat', cat: 'Power', name: 'Power Sub-Station', cost: 500, pwr: 0, req: [], pwrGen: 3, desc: '+3 PWR per turn.' },
    { id: 'fusion', cat: 'Power', name: 'Fusion Reactor', cost: 2800, pwr: 0, req: ['substat'], pwrGen: 9, desc: '+9 PWR per turn. Needs a Sub-Station.' },
    // military
    { id: 'barracks', cat: 'Military', name: 'Enforcer Barracks', cost: 900, pwr: 1, req: [], cap: 8, discount: { inf: 0.75 }, unlock: ['gam'], desc: 'Garrison cap +8. Enforcers cost 25% less. Unlocks Gamorrean Bruisers.' },
    { id: 'motor_pool', cat: 'Military', name: 'Motor Pool', cost: 1500, pwr: 2, req: [], discount: { tnk: 0.85, spc: 0.85, wlk: 0.85 }, unlock: ['tnk', 'spc'], desc: 'Unlocks Assault Tanks and Speeder Cavalry. Vehicles cost 15% less.' },
    { id: 'droid_foundry', cat: 'Military', name: 'Droid Foundry', cost: 1700, pwr: 3, req: [], discount: { drd: 0.8 }, unlock: ['drd'], desc: 'Unlocks Combat Droids. Droids cost 20% less.' },
    { id: 'war_college', cat: 'Military', name: 'War College', cost: 2600, pwr: 2, req: ['barracks', 'motor_pool'], unlock: ['wlk'], battle: 'vets', desc: 'Unlocks Scout Walkers. Units from this sector start battles with +2 hit points. Needs Barracks and Motor Pool.' },
    { id: 'command_center', cat: 'Military', name: 'Command Center', cost: 3600, pwr: 3, req: ['barracks'], slots: 1, cap: 4, depCap: 3, desc: '+1 building slot, garrison cap +4, deploy 3 more units per battle. Needs a Barracks.' },
    // battle support
    { id: 'rally', cat: 'Battle', name: 'War Rally Point', cost: 700, pwr: 1, req: [], battle: 'rally', desc: 'Attackers from here get +2 move on the first turn.' },
    { id: 'armory', cat: 'Battle', name: 'Weapons Armory', cost: 900, pwr: 2, req: [], battle: 'armory', desc: 'Units sent from here deal +2 damage.' },
    { id: 'bunker', cat: 'Battle', name: 'Reinforced Bunker', cost: 600, pwr: 2, req: [], battle: 'bunker', desc: 'Defending here: a ring of hard cover around the objective.' },
    { id: 'turret', cat: 'Battle', name: 'Auto Turret Nest', cost: 850, pwr: 4, req: [], battle: 'turret', desc: 'Defending here: two auto turrets guard the objective.' },
    { id: 'shield_gen', cat: 'Battle', name: 'Shield Generator', cost: 1600, pwr: 4, req: ['substat'], battle: 'shield', desc: 'Defending here: every defender starts with a 6 point shield. Needs a Sub-Station.' },
    { id: 'medbay', cat: 'Battle', name: 'Field Medical Bay', cost: 600, pwr: 1, req: [], battle: 'medbay', desc: 'Defenders heal 2 per turn. After any battle involving this sector 40% of the fallen are revived.' },
    { id: 'intel_hub', cat: 'Battle', name: 'Intelligence Hub', cost: 1100, pwr: 2, req: [], battle: 'intel', desc: 'Reveals enemy garrisons on adjacent sectors and makes battle forecasts accurate.' },
    { id: 'orbital', cat: 'Battle', name: 'Orbital Battery', cost: 5000, pwr: 6, req: ['fusion'], battle: 'orbital', desc: 'One orbital strike per battle: 10 damage in a 3 by 3 area. Needs a Fusion Reactor.' },
  ];
  const BLD = {}; BUILDINGS.forEach((b) => { BLD[b.id] = b; });
  const BASE_SLOTS = 4, BASE_CAP = 12;

  const FACTIONS = {
    player: { id: 'player', name: 'Your Syndicate', color: '#2FC8FF', aggr: 0 },
    black_sun: { id: 'black_sun', name: 'The Black Sun', color: '#FF5468', aggr: 70, rel: -20, mix: { inf: 5, gam: 3, snp: 2, tnk: 2, spc: 1, med: 1 }, rivals: ['csf'] },
    exchange: { id: 'exchange', name: 'The Exchange', color: '#34D86A', aggr: 45, rel: 25, mix: { inf: 5, drd: 3, snp: 1, tnk: 2, med: 1, wlk: 0.3 }, rivals: [] },
    csf: { id: 'csf', name: 'Coruscant CSF', color: '#5AA8FF', aggr: 55, rel: -70, mix: { inf: 6, snp: 3, tnk: 2, med: 2, spc: 1, drd: 1 }, rivals: ['black_sun'] },
    neutral: { id: 'neutral', name: 'Neutral', color: '#8A8F9C', aggr: 0 },
  };

  // x,y are positions on a 1060 by 740 map. theme drives both the map card art and the battle map.
  const T = (id, name, tier, x, y, adj, income, pwr, theme, owner, gar, extra, lore) => ({ id, name, tier, x, y, adj, income, pwr, theme, owner0: owner, gar0: gar, ...extra, lore });
  const G = (inf, snp = 0, tnk = 0, med = 0, drd = 0, spc = 0, gam = 0, wlk = 0) => ({ inf, snp, tnk, med, drd, spc, gam, wlk });
  const SECTOR_LIST = [
    T('sky_lounges', 'Senatorial Sky-Lounges', 'Apex Tier', 313, 70, ['senate_district', 'upper_levels'], 1500, 0, 'lounge', 'black_sun', G(15, 6, 3, 2, 0, 0, 2), { isHQ: 'black_sun' }, 'Gold-lit terraces above the clouds. Black Sun runs the guest list and the knives.'),
    T('senate_district', 'Senate District', 'Upper Core', 578, 70, ['sky_lounges', 'senate_precinct', 'upper_levels'], 1200, 0, 'senate', 'black_sun', G(12, 5, 2, 1, 0, 0, 1), {}, 'Marble domes and bought votes. Every column hides a camera.'),
    T('upper_levels', 'Upper Levels', 'Upper Core', 842, 70, ['sky_lounges', 'senate_district', 'lower_promenade'], 850, 0, 'upper', 'neutral', G(5, 1), {}, 'Skyline towers and speeder lanes. Rich, nervous, lightly guarded.'),
    T('senate_precinct', 'Senate Precinct L.1900', 'Legislature', 451, 185, ['senate_district', 'lower_promenade', 'slicer_alley', 'csf_hub'], 1100, 0, 'precinct', 'csf', G(20, 6, 4, 2, 1), {}, 'The CSF keeps a heavy hand and a heavier garrison here.'),
    T('lower_promenade', 'Lower Promenade L.1100', 'Commerce Belt', 715, 185, ['upper_levels', 'senate_precinct', 'slicer_alley', 'rep_midlevels'], 700, 0, 'promenade', 'neutral', G(6, 2), {}, 'Awnings, lamps and shopkeepers who pay whoever asks nicest.'),
    T('slicer_alley', 'Slicer Alleyway L.1150', 'Data Nexus', 280, 300, ['senate_precinct', 'lower_promenade', 'rep_midlevels'], 600, 1, 'alley', 'neutral', G(4, 3), {}, 'Cyan terminals, tangled wire, kids who can open any door.'),
    T('rep_midlevels', 'Republic Mid-Levels', 'Mid-Layers', 578, 300, ['lower_promenade', 'slicer_alley', 'csf_hub', 'ind_midlevels'], 750, 1, 'midlevels', 'neutral', G(7, 2, 1), {}, 'Stacked apartments, clotheslines and civil servants with long memories.'),
    T('csf_hub', 'CSF Training Hub L.1222', 'Enforcement', 842, 300, ['senate_precinct', 'rep_midlevels', 'the_works'], 1000, 0, 'csfhub', 'csf', G(18, 8, 3, 2, 0, 1), { isHQ: 'csf' }, 'Drill yards and a crest bright enough to read from orbit.'),
    T('the_works', 'The Works L.005', 'Industrial', 448, 415, ['csf_hub', 'rep_midlevels', 'ind_midlevels', 'shadow_town', 'sub_l2_west'], 950, 4, 'works', 'exchange', G(12, 3, 3, 0, 2), {}, 'Furnace glow, smokestacks and pipes that never stop hissing.'),
    T('ind_midlevels', 'Industrial Mid-Levels', 'Factory Belt', 715, 415, ['rep_midlevels', 'the_works', 'sub_spaceport', 'sub_l2_west'], 800, 3, 'industrial', 'neutral', G(8, 1, 2), {}, 'Gears, gantries and cranes the size of cathedrals.'),
    T('sub_spaceport', 'Sub-Surface Spaceport', 'Docking Ring', 186, 530, ['ind_midlevels', 'sub_l2_west', 'freight_hub'], 700, 0, 'spaceport', 'neutral', G(5, 1, 1), {}, 'Landing lights in the gloom. Anything can arrive, anything can leave.'),
    T('shadow_town', 'Shadow Town L.1312', 'Underworld', 448, 530, ['the_works', 'sub_l2_west', 'undercity'], 600, 2, 'shadow', 'player', G(6, 1), { isHQ: 'player' }, 'Your home turf. Hooded eyes, neon signs, loyal knives.'),
    T('sub_l2_west', 'Sub-Surface L2 West Market', 'Black Market', 715, 530, ['the_works', 'ind_midlevels', 'sub_spaceport', 'shadow_town', 'freight_hub', 'undercity_out'], 650, 1, 'market', 'neutral', G(5, 2), {}, 'Lantern stalls, contraband and a very flexible definition of weights.'),
    T('freight_hub', 'Sector 4 Freight Hub L.088', 'Logistics', 934, 530, ['sub_spaceport', 'sub_l2_west', 'undercity_out'], 850, 0, 'freight', 'exchange', G(14, 4, 2, 0, 1), { isHQ: 'exchange' }, 'Container canyons and loader cranes. The Exchange counts every crate.'),
    T('undercity', 'Undercity', 'Depths', 313, 645, ['shadow_town', 'undercity_out'], 500, 2, 'undercity', 'neutral', G(3), {}, 'Wet stone, glowing moss and things that stay in the dark.'),
    T('undercity_out', 'Undercity Outskirts', 'Depths', 715, 645, ['freight_hub', 'sub_l2_west', 'undercity'], 450, 1, 'outskirts', 'neutral', G(3), {}, 'Rubble, campfires and people who owe nobody.'),
  ];
  const SECTORS = {}; SECTOR_LIST.forEach((s) => { SECTORS[s.id] = s; });
  return { UNIT_ORDER, UNITS, BUILDINGS, BLD, BASE_SLOTS, BASE_CAP, WIN_INCOME: 10000, FACTIONS, SECTORS, SECTOR_LIST, unitPower, __parts: {} };
})();

// ---------------------------------------------------------------------------------------------
// Themes: used by the campaign map cards and by the procedural battle maps.
// recipe: pillars | blocks | market | containers | pad | river | maze
CQ.THEMES = {
  lounge:     { name: 'Sky-Lounge Terrace',  recipe: 'pillars',    floor: ['#1E1A28', '#271F33'], wall: ['#6A5A8A', '#2E2542'], cover: '#C9A24A', river: '#10101C', hazard: 'fire',   accent: '#E8C860', sky: ['#120C22', '#3A2050'], decor: ['lamp', 'plant', 'banner'] },
  senate:     { name: 'Senate Concourse',    recipe: 'pillars',    floor: ['#22242E', '#2B2E3A'], wall: ['#9AA0B8', '#40445A'], cover: '#C8CCE0', river: '#10121C', hazard: 'sparks', accent: '#A8B4FF', sky: ['#141830', '#38406A'], decor: ['banner', 'lamp', 'plant'] },
  upper:      { name: 'Upper Levels Rooftops', recipe: 'blocks',   floor: ['#1C2230', '#232B3C'], wall: ['#5A6C8C', '#28324A'], cover: '#7C8CAA', river: '#080C16', hazard: 'sparks', accent: '#7FD0FF', sky: ['#0C1428', '#2A4070'], decor: ['vent', 'lamp', 'crate'], voidEdges: true },
  precinct:   { name: 'CSF Precinct Plaza',  recipe: 'blocks',     floor: ['#1E2430', '#262E3C'], wall: ['#7C94C0', '#2C3A5C'], cover: '#9CB4E0', river: '#0A0E18', hazard: 'sparks', accent: '#5AA8FF', sky: ['#0E1630', '#28407A'], decor: ['lamp', 'banner', 'crate'], checkpoints: true },
  promenade:  { name: 'Lower Promenade',     recipe: 'market',     floor: ['#262028', '#2E2630'], wall: ['#8C6A5A', '#3A2A2A'], cover: '#D49A60', river: '#100A10', hazard: 'fire',   accent: '#FFB070', sky: ['#1C1020', '#4A2840'], decor: ['lamp', 'crate', 'plant'] },
  alley:      { name: 'Slicer Alleyway',     recipe: 'maze',       floor: ['#14181E', '#1A2028'], wall: ['#3A4A5C', '#161C26'], cover: '#4ADCE0', river: '#080A10', hazard: 'sparks', accent: '#39E8E8', sky: ['#060A12', '#102030'], decor: ['neon', 'crate', 'vent'] },
  midlevels:  { name: 'Mid-Level Blocks',    recipe: 'blocks',     floor: ['#1E2024', '#25272C'], wall: ['#6A7080', '#2A2E38'], cover: '#A0A8B8', river: '#0A0C10', hazard: 'steam',  accent: '#B0C0D8', sky: ['#101420', '#2A3448'], decor: ['vent', 'crate', 'lamp'] },
  csfhub:     { name: 'CSF Drill Yard',      recipe: 'blocks',     floor: ['#1A2028', '#222A34'], wall: ['#6C8CB8', '#243450'], cover: '#7CA4E8', river: '#080C14', hazard: 'sparks', accent: '#4A9FFF', sky: ['#0A1228', '#203C70'], decor: ['banner', 'lamp', 'crate'], checkpoints: true },
  works:      { name: 'The Works Foundry',   recipe: 'river',      floor: ['#1C1612', '#241C16'], wall: ['#6A5A4A', '#2A1E16'], cover: '#B07A4A', river: '#FF6A1A', hazard: 'steam',  accent: '#FF8A3A', sky: ['#1A0A04', '#5A2A0A'], decor: ['pipe', 'vent', 'barrel'] },
  industrial: { name: 'Factory Gantries',    recipe: 'containers', floor: ['#1C1C20', '#232329'], wall: ['#7A7A88', '#2C2C34'], cover: '#C8A040', river: '#0A0A0E', hazard: 'sparks', accent: '#E8B84A', sky: ['#0E0E14', '#34343E'], decor: ['pipe', 'crate', 'barrel'] },
  spaceport:  { name: 'Landing Pad Ring',    recipe: 'pad',        floor: ['#1A1E24', '#21262E'], wall: ['#8A94A8', '#2E3644'], cover: '#6ABCE8', river: '#080A10', hazard: 'fire',   accent: '#6ABCE8', sky: ['#080C18', '#1C2E50'], decor: ['lamp', 'crate', 'vent'] },
  shadow:     { name: 'Shadow Town Streets', recipe: 'maze',       floor: ['#16141C', '#1D1A26'], wall: ['#4A3A62', '#1A1424'], cover: '#9A6AE8', river: '#08060E', hazard: 'fire',   accent: '#B07CFF', sky: ['#08060E', '#241838'], decor: ['neon', 'barrel', 'crate'] },
  market:     { name: 'Black Market Stalls', recipe: 'market',     floor: ['#221C1C', '#2A2222'], wall: ['#7A5A4A', '#32221E'], cover: '#E8A040', river: '#0E0808', hazard: 'fire',   accent: '#FFB040', sky: ['#14080A', '#442018'], decor: ['lamp', 'crate', 'barrel'] },
  freight:    { name: 'Freight Canyons',     recipe: 'containers', floor: ['#1A1E1C', '#212624'], wall: ['#5A7A62', '#243228'], cover: '#6AC880', river: '#080C0A', hazard: 'sparks', accent: '#34D86A', sky: ['#080E0A', '#1C3424'], decor: ['crate', 'pipe', 'lamp'] },
  undercity:  { name: 'Undercity Canals',    recipe: 'river',      floor: ['#121A16', '#18221C'], wall: ['#3A5A48', '#142018'], cover: '#5AD890', river: '#0E4A6A', hazard: 'acid',   accent: '#6AE8A0', sky: ['#040A08', '#0C2418'], decor: ['plant', 'pipe', 'barrel'] },
  outskirts:  { name: 'Outskirts Rubble',    recipe: 'blocks',     floor: ['#1C1814', '#241E18'], wall: ['#6A5A48', '#2A2218'], cover: '#A08A60', river: '#0A0806', hazard: 'fire',   accent: '#E8A860', sky: ['#100A06', '#3A2410'], decor: ['barrel', 'crate', 'lamp'] },
};
CQ.tb = (() => {
  const W = 30, H = 18, MAX_ROUNDS = 22;
  const U = CQ.UNITS;
  const key = (x, y) => x + ',' + y;
  const mb32 = (a) => () => { a |= 0; a = a + 0x6D2B79F5 | 0; let t = Math.imul(a ^ a >>> 15, 1 | a); t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t; return ((t ^ t >>> 14) >>> 0) / 4294967296; };
  const hash = (s) => { let h = 2166136261; for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619); } return h >>> 0; };
  const hyp = (ax, ay, bx, by) => Math.hypot(ax - bx, ay - by);
  const other = (team) => (team === 'p' ? 'e' : 'p');

  // ----------------------------------------------------------------------------- map generation
  function genMap(themeId, seed) {
    const th = CQ.THEMES[themeId] || CQ.THEMES.shadow;
    const rnd = mb32(seed);
    const R = (a, b) => a + Math.floor(rnd() * (b - a + 1));
    const t = Array.from({ length: H }, (_, y) => Array.from({ length: W }, (_, x) => (x === 0 || y === 0 || x === W - 1 || y === H - 1 ? (th.voidEdges && (y === 0 || y === H - 1) ? 'x' : 'w') : 'f')));
    const node = { x: Math.floor(W / 2), y: Math.floor(H / 2) };
    const inZone = (x, y) => (x >= 1 && x <= 4 && y >= 2 && y <= H - 3) || (x >= W - 5 && x <= W - 2 && y >= 2 && y <= H - 3);
    const nearNode = (x, y, r) => Math.abs(x - node.x) <= r && Math.abs(y - node.y) <= r;
    const free = (x, y) => x >= 1 && y >= 1 && x <= W - 2 && y <= H - 2 && !inZone(x, y) && !nearNode(x, y, 2);
    const hasWall = (x0, y0, w, h) => { for (let y = y0 - 1; y <= y0 + h; y++) for (let x = x0 - 1; x <= x0 + w; x++) if (t[y] && t[y][x] === 'w' && x > 0 && y > 0 && x < W - 1 && y < H - 1) return true; return false; };
    const rect = (x0, y0, w, h, tile) => { for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) if (free(x, y)) t[y][x] = tile; };
    const canBlock = (x0, y0, w, h) => { for (let y = y0; y < y0 + h; y++) for (let x = x0; x < x0 + w; x++) if (!free(x, y)) return false; return !hasWall(x0, y0, w, h); };
    const blocks = (n, minW, maxW, minH, maxH) => { for (let i = 0, tries = 0; i < n && tries < n * 30; tries++) { const w = R(minW, maxW), h = R(minH, maxH), x = R(5, W - 6 - w), y = R(1, H - 2 - h); if (canBlock(x, y, w, h)) { rect(x, y, w, h, 'w'); i++; } } };
    const recipe = th.recipe;
    if (recipe === 'pillars') {
      for (let x = 6; x <= W - 7; x += 3) for (let y = 2; y <= H - 3; y += 3) { if ((x + y) % 2 === 0 || rnd() < 0.2) { if (free(x, y) && Math.abs(y - node.y) > 1) t[y][x] = 'w'; } }
      // symmetric bars / counters
      [[9, 4], [9, H - 6], [W - 12, 4], [W - 12, H - 6], [13, 6], [W - 16, H - 8]].forEach(([x, y]) => rect(x, y, 3, 1, 'c'));
    } else if (recipe === 'market') {
      for (let y = 2; y <= H - 3; y += 3) for (let x = 6; x <= W - 9; x += 4) { if (rnd() < 0.78 && free(x, y) && free(x + 1, y)) { rect(x, y, 2, 1, 'c'); if (rnd() < 0.5) rect(x, y - 1, 1, 1, 'w'); } }
      blocks(5, 2, 3, 2, 3);
    } else if (recipe === 'containers') {
      for (let x = 6; x <= W - 10; x += 5) for (let y = 2; y <= H - 5; y += 4) { if (rnd() < 0.82 && canBlock(x, y, 3, 2)) rect(x, y, 3, 2, 'w'); }
      blocks(4, 2, 2, 2, 3);
    } else if (recipe === 'pad') {
      rect(8, 4, 4, 2, 'w'); rect(W - 12, H - 6, 4, 2, 'w'); rect(8, H - 7, 4, 2, 'w'); rect(W - 12, 5, 4, 2, 'w'); blocks(6, 2, 3, 2, 3);
    } else if (recipe === 'river') {
      const rx = Math.floor(W / 2) - 1;
      for (let y = 1; y <= H - 2; y++) { if ((y >= 3 && y <= 4) || (y >= H - 5 && y <= H - 4)) continue; t[y][rx] = 'x'; t[y][rx + 1] = 'x'; }
      blocks(9, 2, 4, 2, 3);
    } else if (recipe === 'maze') {
      blocks(24, 2, 4, 2, 4);
    } else { // blocks
      blocks(14, 2, 5, 2, 4);
      if (th.checkpoints) { for (const x of [8, W - 9]) for (let y = 3; y <= H - 4; y += 2) if (free(x, y) && t[y][x] === 'f') t[y][x] = 'c'; }
    }
    // cover clusters and hazard patches
    for (let i = 0; i < 30; i++) { const x = R(5, W - 6), y = R(1, H - 2); if (free(x, y) && t[y][x] === 'f') { t[y][x] = 'c'; if (rnd() < 0.5 && free(x + 1, y) && t[y][x + 1] === 'f') t[y][x + 1] = 'c'; } }
    for (let i = 0; i < 7; i++) { const x = R(6, W - 8), y = R(2, H - 4); for (let dy = 0; dy < 2; dy++) for (let dx = 0; dx < 2; dx++) if (free(x + dx, y + dy) && t[y + dy][x + dx] === 'f') t[y + dy][x + dx] = 'h'; }
    // objective area clear
    for (let y = node.y - 1; y <= node.y + 1; y++) for (let x = node.x - 1; x <= node.x + 1; x++) t[y][x] = 'f';
    // deployment zones are always open
    const zoneL = [], zoneR = [];
    for (let y = 2; y <= H - 3; y++) { for (let x = 1; x <= 3; x++) { t[y][x] = 'f'; zoneL.push({ x, y }); } for (let x = W - 4; x <= W - 2; x++) { t[y][x] = 'f'; zoneR.push({ x, y }); } }
    // connectivity: carve straight corridors from any isolated area to the node
    const passable = (x, y) => t[y] && (t[y][x] === 'f' || t[y][x] === 'c' || t[y][x] === 'h');
    const flood = (sx, sy) => { const seen = new Set([key(sx, sy)]); const q = [[sx, sy]]; while (q.length) { const [x, y] = q.pop(); for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { const nx = x + dx, ny = y + dy; if (passable(nx, ny) && !seen.has(key(nx, ny))) { seen.add(key(nx, ny)); q.push([nx, ny]); } } } return seen; };
    for (let pass = 0; pass < 6; pass++) {
      const seen = flood(node.x, node.y);
      const lost = zoneL.concat(zoneR).find((z) => !seen.has(key(z.x, z.y)));
      if (!lost) break;
      let x = lost.x, y = lost.y;
      while (x !== node.x || y !== node.y) { if (x !== node.x) x += x < node.x ? 1 : -1; else y += y < node.y ? 1 : -1; if (x > 0 && y > 0 && x < W - 1 && y < H - 1 && !passable(x, y)) t[y][x] = 'f'; }
    }
    // decor (visual only)
    const decor = [];
    for (let i = 0; i < 150; i++) { const x = R(1, W - 2), y = R(1, H - 2); if (t[y][x] === 'f' && !(x === node.x && y === node.y)) decor.push({ x, y, k: th.decor[R(0, th.decor.length - 1)], r: rnd() }); }
    return { w: W, h: H, tiles: t, decor, node, zoneL, zoneR, theme: themeId };
  }

  // ----------------------------------------------------------------------------- battle setup
  const sortedTypes = (army) => CQ.UNIT_ORDER.filter((k) => (army[k] || 0) > 0);
  function newUnit(b, type, team, cfg) {
    const d = U[type];
    const c = (cfg && cfg[team]) || {};
    const hp = d.hp + (c.hp || 0);
    return { id: b.nextId++, type, team, x: -1, y: -1, hp, max: hp, atk: d.atk + (c.atk || 0), armor: d.armor + (c.armor || 0), mov: d.mov, rng: d.rng, moved: false, acted: false, movedDist: 0, cd: {}, sup: 0, shield: c.shield || 0, od: 0, immobile: !!d.immobile };
  }
  // setup: { sectorId, theme, seed, armies:{p,e}, attacker:'p'|'e', cfg:{p,e}, depCap, intel }
  function makeBattle(setup) {
    const map = genMap(setup.theme, setup.seed);
    const baseCfg = { p: { ...((setup.cfg && setup.cfg.p) || {}) }, e: { ...((setup.cfg && setup.cfg.e) || {}) } };
    setup = { ...setup, cfg: baseCfg };
    const b = { sectorId: setup.sectorId, theme: setup.theme, seed: setup.seed, ...map, units: [], dead: [], nextId: 1, round: 1, turn: setup.attacker, attacker: setup.attacker,
      phase: 'deploy', result: null, hold: 0, maxRounds: MAX_ROUNDS, cfg: setup.cfg || { p: {}, e: {} }, armies: { p: { ...setup.armies.p }, e: { ...setup.armies.e } },
      orbital: { p: (setup.cfg && setup.cfg.p && setup.cfg.p.orbital) || 0, e: (setup.cfg && setup.cfg.e && setup.cfg.e.orbital) || 0 },
      reserve: { p: [], e: [] }, events: [], log: [], depCap: setup.depCap || 14, intel: !!setup.intel, mines: [] };
    const defender = other(setup.attacker);
    // reserves: anything above the on-field cap waits and arrives in waves (defender only; attackers are capped at commit time)
    ['p', 'e'].forEach((team) => {
      const list = []; sortedTypes(b.armies[team]).forEach((k) => { for (let i = 0; i < b.armies[team][k]; i++) list.push(k); });
      const cap = team === setup.attacker ? 99 : 14;
      list.slice(0, cap).forEach((k) => b.units.push(newUnit(b, k, team, b.cfg)));
      if (list.length > cap) b.reserve[team] = list.slice(cap);
    });
    // defender fortifications
    const dc = b.cfg[defender] || {};
    if (dc.bunker) { for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) { const x = b.node.x + dx, y = b.node.y + dy; if (Math.max(Math.abs(dx), Math.abs(dy)) === 2 && b.tiles[y][x] === 'f') b.tiles[y][x] = 'c'; } }
    if (dc.turrets) { const spots = [[-3, -2], [-3, 2], [3, -2], [3, 2], [0, -3], [0, 3]]; let n = 0; for (const [dx, dy] of spots) { if (n >= dc.turrets) break; const x = b.node.x + dx, y = b.node.y + dy; if (b.tiles[y] && b.tiles[y][x] === 'f' && !unitAtSetup(b, x, y)) { const u = newUnit(b, 'trt', defender, b.cfg); u.x = x; u.y = y; u.hp = u.max = U.trt.hp; b.units.push(u); n++; } } }
    placeAll(b, 'e'); // the AI side places itself; the player deploys by hand (or autoDeploy)
    return b;
  }
  const unitAtSetup = (b, x, y) => b.units.find((u) => u.hp > 0 && u.x === x && u.y === y);
  const zoneOf = (b, team) => (team === b.attacker ? b.zoneL : b.zoneR);
  function placeAll(b, team) {
    const zone = zoneOf(b, team).slice().sort((a, c) => hyp(a.x, a.y, b.node.x, b.node.y) - hyp(c.x, c.y, b.node.x, b.node.y));
    const mine = b.units.filter((u) => u.team === team && u.x < 0 && !u.immobile);
    // frontline units first (closest tiles), support and ranged behind
    const rank = (u) => ({ gam: 0, tnk: 1, wlk: 1, spc: 2, drd: 3, inf: 3, med: 5, snp: 6 }[u.type] ?? 4);
    mine.sort((a, c) => rank(a) - rank(c));
    const used = new Set(b.units.filter((u) => u.x >= 0).map((u) => key(u.x, u.y)));
    const spots = zone.filter((z) => !used.has(key(z.x, z.y)));
    // spread the formation across the zone height instead of packing one column
    const bands = [[], [], []]; spots.forEach((z, i) => bands[Math.floor((i / spots.length) * 3)].push(z));
    let bi = 0; mine.forEach((u) => { let tries = 0; while (tries < 3 && !bands[bi % 3].length) { bi++; tries++; } const s = bands[bi % 3].shift() || spots.shift(); if (s) { u.x = s.x; u.y = s.y; } bi++; });
  }
  function autoDeploy(b, team) { placeAll(b, team); }
  function deployRemaining(b, team) { return b.units.filter((u) => u.team === team && u.x < 0 && !u.immobile); }
  function deployAt(b, team, uid, x, y) {
    const u = b.units.find((q) => q.id === uid && q.team === team); if (!u || b.phase !== 'deploy') return false;
    if (!zoneOf(b, team).some((z) => z.x === x && z.y === y)) return false;
    if (unitAtSetup(b, x, y) && unitAtSetup(b, x, y).id !== uid) return false;
    u.x = x; u.y = y; return true;
  }
  function undeploy(b, uid) { const u = b.units.find((q) => q.id === uid); if (u && b.phase === 'deploy') { u.x = -1; u.y = -1; } }
  function begin(b) {
    if (b.phase !== 'deploy') return false;
    const undeployed = b.units.filter((u) => u.team === 'p' && u.x < 0 && !u.immobile);
    undeployed.forEach((u) => { /* anything still unplaced is auto placed */ });
    if (undeployed.length) placeAll(b, 'p');
    b.phase = 'battle'; b.round = 1; b.turn = b.attacker; startTurn(b, b.turn, null);
    b.log.push('Battle begins. Round 1.');
    return true;
  }

  // ----------------------------------------------------------------------------- queries
  const tileAt = (b, x, y) => (b.tiles[y] ? b.tiles[y][x] : undefined);
  const unitAt = (b, x, y) => b.units.find((u) => u.hp > 0 && u.x === x && u.y === y);
  const passable = (b, x, y) => { const t = tileAt(b, x, y); return t === 'f' || t === 'c' || t === 'h'; };
  const isDroid = (u) => u.type === 'drd' || u.type === 'wlk' || u.type === 'tnk' || u.type === 'trt';
  function moveOf(b, u) {
    if (u.immobile || u.moved) return 0;
    const c = b.cfg[u.team] || {};
    const bonus = c.rally && u.team === b.attacker && b.round === 1 ? 2 : 0;
    return Math.max(1, u.mov + bonus - (u.sup > 0 && u.type !== 'drd' ? 2 : 0));
  }
  function reach(b, u) {
    const m = moveOf(b, u); const best = new Map(); best.set(key(u.x, u.y), { c: 0, px: null, py: null, x: u.x, y: u.y });
    if (m <= 0) return best;
    const open = [[u.x, u.y, 0]];
    while (open.length) {
      let bi = 0; for (let i = 1; i < open.length; i++) if (open[i][2] < open[bi][2]) bi = i;
      const [x, y, c] = open.splice(bi, 1)[0];
      if (c > (best.get(key(x, y)) || { c }).c) continue;
      for (let dy = -1; dy <= 1; dy++) for (let dx = -1; dx <= 1; dx++) {
        if (!dx && !dy) continue; const nx = x + dx, ny = y + dy;
        if (!passable(b, nx, ny) || unitAt(b, nx, ny)) continue;
        if (dx && dy && tileAt(b, x + dx, y) === 'w' && tileAt(b, x, y + dy) === 'w') continue;
        const step = tileAt(b, nx, ny) === 'h' && u.type !== 'drd' ? 2 : 1; const nc = c + step;
        if (nc > m) continue; const old = best.get(key(nx, ny));
        if (!old || nc < old.c) { best.set(key(nx, ny), { c: nc, px: x, py: y, x: nx, y: ny }); open.push([nx, ny, nc]); }
      }
    }
    return best;
  }
  function pathTo(r, x, y) { const out = []; let cur = r.get(key(x, y)); while (cur && cur.px !== null) { out.unshift({ x: cur.x, y: cur.y }); cur = r.get(key(cur.px, cur.py)); } return out; }
  function los(b, x0, y0, x1, y1) {
    let dx = Math.abs(x1 - x0), dy = Math.abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1, err = dx - dy, x = x0, y = y0;
    while (!(x === x1 && y === y1)) { const e2 = 2 * err; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } if (!(x === x1 && y === y1) && tileAt(b, x, y) === 'w') return false; }
    return true;
  }
  const abilityReady = (u) => !!(U[u.type].ab) && !(u.cd[U[u.type].ab.id] > 0);
  function canHit(b, u, t, fromX, fromY, ignoreLos) {
    const x = fromX ?? u.x, y = fromY ?? u.y; const d = hyp(x, y, t.x, t.y);
    if (d > u.rng + 0.001) return false; return ignoreLos || los(b, x, y, t.x, t.y);
  }
  function targetsFrom(b, u, x, y) { return b.units.filter((t) => t.hp > 0 && t.team !== u.team && canHit(b, u, t, x, y, false)); }
  function dmgVs(b, u, t, o) {
    o = o || {}; let atk = u.atk + (o.bonus || 0);
    if (u.type === 'snp' && !u.moved) atk += 4;
    if (u.type === 'spc' && u.movedDist >= 4) atk += 4;
    if (u.sup > 0 && u.type !== 'drd') atk -= 2;
    const dist = hyp(u.x, u.y, t.x, t.y); const cover = tileAt(b, t.x, t.y) === 'c' && dist > 1.5 && t.type !== 'tnk' && t.type !== 'wlk' && !o.ignoreCover ? 2 : 0;
    return atk - t.armor - cover; // raw: <= 0 means the armor stops it (chip damage rule in chip())
  }
  const chip = (raw, rnd) => { if (raw <= 0) return rnd && rnd() < 0.5 ? 1 : 0; if (!rnd) return raw; const v = rnd(); return raw + (v < 0.2 ? -1 : v > 0.8 ? 1 : 0) < 1 ? 1 : raw + (v < 0.2 ? -1 : v > 0.8 ? 1 : 0); };
  const alive = (b, team) => b.units.filter((u) => u.hp > 0 && u.team === team && !u.immobile);

  // ----------------------------------------------------------------------------- actions
  function ev(b, e) { e.n = (b.evN = (b.evN || 0) + 1); b.events.push(e); if (b.events.length > 80) b.events.shift(); }
  function hurt(b, t, dmg, src) {
    if (dmg <= 0) { ev(b, { k: 'miss', x: t.x, y: t.y, id: t.id }); return; }
    let d = dmg; if (t.shield > 0) { const a = Math.min(t.shield, d); t.shield -= a; d -= a; }
    t.hp -= d; ev(b, { k: 'hit', x: t.x, y: t.y, v: dmg, id: t.id });
    if (t.hp <= 0) { t.hp = 0; b.dead.push({ id: t.id, type: t.type, team: t.team, x: t.x, y: t.y }); ev(b, { k: 'kill', x: t.x, y: t.y, id: t.id }); b.log.push(`${U[t.type].short} (${t.team === 'p' ? 'yours' : 'enemy'}) falls.`); }
  }
  function sweepDead(b) { b.units = b.units.filter((u) => u.hp > 0); }
  function doMove(b, u, x, y) {
    const r = reach(b, u); const node = r.get(key(x, y)); if (!node || (x === u.x && y === u.y)) return false;
    const path = pathTo(r, x, y); u.movedDist = node.c; u.x = x; u.y = y; u.moved = true; ev(b, { k: 'move', id: u.id, path }); return true;
  }
  function doAttack(b, u, t, rnd, o) {
    if (u.acted || !canHit(b, u, t, null, null, false) || t.hp <= 0) return false;
    let d = chip(dmgVs(b, u, t, o), rnd); if (rnd && rnd() < 0.1) d += 2;
    ev(b, { k: 'shot', from: [u.x, u.y], to: [t.x, t.y], team: u.team, type: u.type });
    hurt(b, t, d, u); u.acted = true; u.moved = true; if (u.od) u.od = 0; return d;
  }
  function doAbility(b, u, tx, ty, rnd) {
    const ab = U[u.type].ab; if (!ab || u.acted || !abilityReady(u)) return false;
    const t = unitAt(b, tx, ty);
    const setcd = () => { u.cd[ab.id] = ab.cd; u.acted = true; u.moved = true; };
    if (ab.id === 'suppress') { if (!t || t.team === u.team || !canHit(b, u, t, null, null, false)) return false; ev(b, { k: 'shot', from: [u.x, u.y], to: [t.x, t.y], team: u.team, type: u.type }); hurt(b, t, chip(dmgVs(b, u, t) - 2, rnd), u); if (t.hp > 0 && t.type !== 'drd') t.sup = 2; setcd(); return true; }
    if (ab.id === 'smash') { const adj = b.units.filter((q) => q.hp > 0 && q.team !== u.team && hyp(u.x, u.y, q.x, q.y) <= 1.5); if (!adj.length) return false; adj.forEach((q) => hurt(b, q, chip(dmgVs(b, u, q, { ignoreCover: true }), rnd), u)); ev(b, { k: 'smash', x: u.x, y: u.y }); setcd(); return true; }
    if (ab.id === 'heal') { if (!t || t.team !== u.team || t.id === u.id || t.type === 'trt' || hyp(u.x, u.y, t.x, t.y) > 4.001 || t.hp >= t.max) return false; const h = Math.min(8, t.max - t.hp); t.hp += h; ev(b, { k: 'heal', x: t.x, y: t.y, v: h, from: [u.x, u.y] }); setcd(); return true; }
    if (ab.id === 'overcharge') { if (!t || t.team === u.team || !canHit(b, u, t, null, null, false)) return false; ev(b, { k: 'shot', from: [u.x, u.y], to: [t.x, t.y], team: u.team, type: u.type, big: true }); hurt(b, t, chip(dmgVs(b, u, t, { bonus: 4 }), rnd), u); u.hp = Math.max(1, u.hp - 4); setcd(); return true; }
    if (ab.id === 'shell') { if (!t || t.team === u.team || hyp(u.x, u.y, t.x, t.y) > u.rng + 0.001) return false; ev(b, { k: 'shell', from: [u.x, u.y], to: [t.x, t.y] }); const splash = b.units.filter((q) => q.hp > 0 && q.team !== u.team && q.id !== t.id && hyp(t.x, t.y, q.x, q.y) <= 1.5); hurt(b, t, chip(dmgVs(b, u, t, { ignoreCover: true }), rnd), u); splash.forEach((q) => hurt(b, q, chip(u.atk - 2 - q.armor, rnd), u)); setcd(); return true; }
    if (ab.id === 'barrage') { if (!t || t.team === u.team || !canHit(b, u, t, null, null, false)) return false; ev(b, { k: 'shot', from: [u.x, u.y], to: [t.x, t.y], team: u.team, type: u.type, big: true }); hurt(b, t, chip(dmgVs(b, u, t), rnd), u); if (t.hp > 0) hurt(b, t, chip(dmgVs(b, u, t), rnd), u); setcd(); return true; }
    return false;
  }
  function orbital(b, team, tx, ty) {
    if (b.orbital[team] <= 0 || b.phase !== 'battle') return false; b.orbital[team]--; ev(b, { k: 'orbital', x: tx, y: ty });
    b.units.filter((q) => q.hp > 0 && q.team !== team && Math.abs(q.x - tx) <= 1 && Math.abs(q.y - ty) <= 1).forEach((q) => hurt(b, q, 10, null));
    b.log.push('Orbital strike!'); sweepDead(b); checkEnd(b); return true;
  }
  function startTurn(b, team, rnd) {
    b.units.forEach((u) => { if (u.team === team) { u.moved = false; u.acted = false; u.movedDist = 0; Object.keys(u.cd).forEach((k) => { if (u.cd[k] > 0) u.cd[k]--; }); } });
    // reserves arrive at the start of the defender turn from round 3
    if (team !== b.attacker && b.round >= 3 && b.round % 2 === 1 && b.reserve[team].length) {
      const zone = zoneOf(b, team).filter((z) => !unitAt(b, z.x, z.y)); let n = 0;
      while (n < 4 && b.reserve[team].length && zone.length) { const z = zone.splice(Math.floor((rnd ? rnd() : 0.5) * zone.length), 1)[0]; const u = newUnit(b, b.reserve[team].shift(), team, b.cfg); u.x = z.x; u.y = z.y; b.units.push(u); n++; }
      if (n) b.log.push(`Reinforcements arrive (${n}).`);
    }
  }
  function endTurn(b, rnd) {
    if (b.phase !== 'battle') return;
    const team = b.turn; const c = b.cfg[team] || {};
    // turrets fire on their own
    b.units.filter((u) => u.hp > 0 && u.team === team && u.type === 'trt').forEach((u) => { const ts = targetsFrom(b, u, u.x, u.y); if (ts.length) { ts.sort((a, z) => a.hp - z.hp); hurt(b, ts[0], chip(dmgVs(b, u, ts[0]), rnd), u); ev(b, { k: 'shot', from: [u.x, u.y], to: [ts[0].x, ts[0].y], team, type: 'trt' }); } });
    sweepDead(b); if (checkEnd(b)) return;
    b.units.filter((u) => u.team === team && u.hp > 0).forEach((u) => {
      if (tileAt(b, u.x, u.y) === 'h' && u.type !== 'trt') hurt(b, u, u.type === 'drd' ? 2 : 4, null);
      if (u.hp > 0) { if (c.medbay && !u.immobile) u.hp = Math.min(u.max, u.hp + c.medbay); if (u.sup > 0) u.sup--; }
    });
    sweepDead(b); if (checkEnd(b)) return;
    // objective: hold the node for two of your own turns with no enemy within 2 tiles
    if (team === b.attacker) {
      const on = b.units.find((u) => u.hp > 0 && u.team === team && !u.immobile && u.x === b.node.x && u.y === b.node.y);
      const contested = b.units.some((u) => u.hp > 0 && u.team !== team && !u.immobile && hyp(u.x, u.y, b.node.x, b.node.y) <= 2.01);
      b.hold = on && !contested ? b.hold + 1 : 0; if (b.hold >= 2) { finish(b, team, 'node'); return; }
    }
    const next = other(team); if (next === b.attacker) b.round++;
    if (b.round > b.maxRounds) { finish(b, other(b.attacker), 'timeout'); return; }
    b.turn = next; ev(b, { k: 'turn', team: next, round: b.round }); startTurn(b, next, rnd);
  }
  function finish(b, winner, reason) {
    b.phase = 'done';
    const surv = { p: {}, e: {} }, lost = { p: {}, e: {} };
    ['p', 'e'].forEach((team) => { const dead = {}; b.dead.filter((d) => d.team === team && d.type !== 'trt').forEach((d) => { dead[d.type] = (dead[d.type] || 0) + 1; }); Object.keys(b.armies[team]).forEach((k) => { const n = b.armies[team][k] || 0; const l = Math.min(n, dead[k] || 0); lost[team][k] = l; surv[team][k] = n - l; }); });
    b.result = { winner, reason, rounds: b.round, survivors: surv, lost };
    b.log.push(winner === 'p' ? 'VICTORY.' : winner === 'e' ? 'DEFEAT.' : 'Withdrawn.');
  }
  function checkEnd(b) {
    if (b.phase !== 'battle') return true;
    if (!alive(b, 'p').length && !b.reserve.p.length) { finish(b, 'e', 'wiped'); return true; }
    if (!alive(b, 'e').length && !b.reserve.e.length) { finish(b, 'p', 'wiped'); return true; }
    return false;
  }
  function retreat(b) { if (b.phase !== 'battle') return; const surv = { p: {}, e: {} }, lost = { p: {}, e: {} }; finish(b, 'e', 'retreat'); b.result.reason = 'retreat'; }

  // ----------------------------------------------------------------------------- AI
  function valueOfShot(b, u, t, d) {
    let v = Math.min(d, t.hp) + (d >= t.hp ? 8 : 0);
    v += { med: 4, snp: 3, tnk: 3, wlk: 3, drd: 1, trt: 1 }[t.type] || 0; v += (1 - t.hp / t.max) * 3; return v;
  }
  function aiGoal(b, u) {
    const foes = b.units.filter((q) => q.hp > 0 && q.team !== u.team && !q.immobile);
    let near = null, nd = 1e9; foes.forEach((q) => { const d = hyp(u.x, u.y, q.x, q.y); if (d < nd) { nd = d; near = q; } });
    return { near, nd };
  }
  function aiPlayUnit(b, u, rnd) {
    const out = { id: u.id, from: [u.x, u.y], to: [u.x, u.y], did: null };
    if (u.immobile || u.hp <= 0) { u.acted = true; return out; }
    const defending = u.team !== b.attacker;
    const ab = U[u.type].ab;
    // medics heal the most wounded ally they can reach
    if (u.type === 'med' && abilityReady(u)) {
      const hurtAllies = b.units.filter((q) => q.hp > 0 && q.team === u.team && q.id !== u.id && q.hp < q.max && q.type !== 'trt').sort((a, c) => a.hp / a.max - c.hp / c.max);
      if (hurtAllies.length) {
        const tgt = hurtAllies[0]; const r = reach(b, u); let bestSpot = null, bd = 1e9;
        r.forEach((n) => { const d = hyp(n.x, n.y, tgt.x, tgt.y); if (d <= 4 && d < bd) { bd = d; bestSpot = n; } });
        if (bestSpot) { if (bestSpot.x !== u.x || bestSpot.y !== u.y) doMove(b, u, bestSpot.x, bestSpot.y); if (doAbility(b, u, tgt.x, tgt.y, rnd)) { out.to = [u.x, u.y]; out.did = 'heal'; u.acted = true; return out; } }
      }
    }
    const r = reach(b, u);
    const { near, nd } = aiGoal(b, u);
    // defenders hold position until a threat is close
    const threatNear = near && nd <= u.mov + u.rng + 2.5;
    let best = null, bestScore = -1e9;
    r.forEach((n) => {
      if (n.x !== u.x || n.y !== u.y) { if (unitAt(b, n.x, n.y)) return; }
      const ts = targetsFrom(b, u, n.x, n.y); let score = 0;
      const tileT = tileAt(b, n.x, n.y);
      if (ts.length) {
        let bv = -1; ts.forEach((t) => { const d = Math.max(1, u.atk + (u.type === 'snp' && n.x === u.x && n.y === u.y ? 4 : 0) - t.armor - (tileAt(b, t.x, t.y) === 'c' ? 2 : 0)); const v = valueOfShot(b, u, t, d); if (v > bv) bv = v; });
        score = 50 + bv;
      } else if (defending && !threatNear) {
        score = -hyp(n.x, n.y, b.node.x, b.node.y) * 1.2 - (n.x === u.x && n.y === u.y ? 0 : 0.5);
      } else {
        const g = near ? hyp(n.x, n.y, near.x, near.y) : hyp(n.x, n.y, b.node.x, b.node.y);
        const gn = hyp(n.x, n.y, b.node.x, b.node.y);
        score = -Math.min(g, defending ? g : gn + 3) * 2;
      }
      if (tileT === 'c') score += 3; if (tileT === 'h' && u.type !== 'drd') score -= 7;
      // ranged units avoid standing next to enemies when they can shoot from afar
      if (u.rng > 2) { const adj = b.units.filter((q) => q.hp > 0 && q.team !== u.team && hyp(n.x, n.y, q.x, q.y) <= 1.5).length; score -= adj * 4; if (u.type === 'snp') score += hyp(n.x, n.y, near ? near.x : n.x, near ? near.y : n.y) * 0.3; }
      // exposure: count enemies that could probably hit this tile next turn
      let exp = 0; b.units.forEach((q) => { if (q.hp > 0 && q.team !== u.team && !q.immobile && hyp(n.x, n.y, q.x, q.y) <= q.mov + q.rng) exp++; }); score -= exp * 0.6;
      score += rnd() * 0.5;
      if (score > bestScore) { bestScore = score; best = n; }
    });
    if (best && (best.x !== u.x || best.y !== u.y)) { doMove(b, u, best.x, best.y); out.to = [u.x, u.y]; }
    // choose the action
    const ts = targetsFrom(b, u, u.x, u.y);
    if (u.type === 'tnk' && abilityReady(u)) {
      let bt = null, bs = 1; b.units.forEach((t) => { if (t.hp > 0 && t.team !== u.team && hyp(u.x, u.y, t.x, t.y) <= u.rng + 0.001) { const s = b.units.filter((q) => q.hp > 0 && q.team !== u.team && hyp(t.x, t.y, q.x, q.y) <= 1.5).length; if (s > bs || (s === bs && bs > 1 && bt && t.hp < bt.hp)) { bs = s; bt = t; } } });
      if (bt && bs >= 2 && doAbility(b, u, bt.x, bt.y, rnd)) { out.did = 'shell'; return out; }
    }
    if (u.type === 'gam' && abilityReady(u)) { const adj = b.units.filter((q) => q.hp > 0 && q.team !== u.team && hyp(u.x, u.y, q.x, q.y) <= 1.5).length; if (adj >= 2 && doAbility(b, u, u.x, u.y, rnd)) { out.did = 'smash'; return out; } }
    if (ts.length) {
      ts.sort((a, c) => valueOfShot(b, u, c, Math.max(1, dmgVs(b, u, c))) - valueOfShot(b, u, a, Math.max(1, dmgVs(b, u, a))));
      const t = ts[0];
      if (u.type === 'wlk' && abilityReady(u) && t.hp >= 4 && doAbility(b, u, t.x, t.y, rnd)) { out.did = 'barrage'; return out; }
      if (u.type === 'drd' && abilityReady(u) && u.hp >= 6 && t.hp <= 5 && doAbility(b, u, t.x, t.y, rnd)) { out.did = 'overcharge'; return out; }
      if (u.type === 'inf' && abilityReady(u) && (t.type === 'tnk' || t.type === 'gam' || t.type === 'spc' || t.type === 'wlk') && doAbility(b, u, t.x, t.y, rnd)) { out.did = 'suppress'; return out; }
      if (doAttack(b, u, t, rnd)) { out.did = 'attack'; return out; }
    }
    u.acted = true; return out;
  }
  function aiOrbital(b, team) {
    if (b.orbital[team] <= 0 || b.round < 2) return false;
    let best = null, bs = 1;
    for (let y = 1; y < H - 1; y++) for (let x = 1; x < W - 1; x++) { const n = b.units.filter((q) => q.hp > 0 && q.team !== team && Math.abs(q.x - x) <= 1 && Math.abs(q.y - y) <= 1).length; const own = b.units.filter((q) => q.hp > 0 && q.team === team && Math.abs(q.x - x) <= 1 && Math.abs(q.y - y) <= 1).length; if (n - own > bs) { bs = n - own; best = [x, y]; } }
    return best ? orbital(b, team, best[0], best[1]) : false;
  }
  // plays one unit for the team and returns its event, or null if everyone has acted
  function aiStep(b, team, rnd) {
    if (b.phase !== 'battle' || b.turn !== team) return null;
    if (aiOrbital(b, team)) return { id: 0, did: 'orbital', from: [0, 0], to: [0, 0] };
    const todo = b.units.filter((u) => u.hp > 0 && u.team === team && !u.immobile && !u.acted);
    if (!todo.length) return null;
    // act in order: frontline first so ranged units can see what is left standing
    const rank = (u) => ({ gam: 0, tnk: 1, wlk: 1, spc: 2, drd: 3, inf: 3, med: 4, snp: 5 }[u.type] ?? 4);
    todo.sort((a, c) => rank(a) - rank(c));
    const e = aiPlayUnit(b, todo[0], rnd); sweepDead(b); checkEnd(b); return e;
  }
  function aiTeamTurn(b, team, rnd) { let guard = 0; while (b.phase === 'battle' && b.turn === team && guard++ < 80) { const e = aiStep(b, team, rnd); if (!e) break; } if (b.phase === 'battle' && b.turn === team) endTurn(b, rnd); }
  // headless battle: both sides AI
  function autoBattle(b, rnd) {
    if (b.phase === 'deploy') { autoDeploy(b, 'p'); autoDeploy(b, 'e'); begin(b); }
    let guard = 0; while (b.phase === 'battle' && guard++ < 200) aiTeamTurn(b, b.turn, rnd);
    if (b.phase === 'battle') finish(b, other(b.attacker), 'timeout');
    return b.result;
  }
  return { W, H, MAX_ROUNDS, genMap, makeBattle, autoDeploy, deployRemaining, deployAt, undeploy, begin, reach, pathTo, targetsFrom, canHit, los, dmgVs, doMove, doAttack, doAbility, orbital, endTurn, aiStep, aiTeamTurn, autoBattle, abilityReady, retreat, unitAt, tileAt, moveOf, hash, mb32, finish, sweepDead, checkEnd, alive };
})();

// ---------------------------------------------------------------------------------------------
// Campaign layer
CQ.camp = (() => {
  const U = CQ.UNITS, BLD = CQ.BLD, ORDER = CQ.UNIT_ORDER;
  const AIF = ['black_sun', 'exchange', 'csf'];
  const clone = (s) => JSON.parse(JSON.stringify(s));
  const total = (g) => ORDER.reduce((n, k) => n + (g[k] || 0), 0);
  const emptyG = () => ({ inf: 0, snp: 0, tnk: 0, med: 0, drd: 0, spc: 0, gam: 0, wlk: 0 });
  const addG = (a, b) => { const o = { ...a }; ORDER.forEach((k) => { o[k] = (o[k] || 0) + (b[k] || 0); }); return o; };
  const power = (g) => ORDER.reduce((n, k) => n + (g[k] || 0) * CQ.unitPower(U[k]), 0);
  const hasB = (sec, id) => sec.bld.includes(id);

  function newCampaign(startCredits) {
    const sec = {};
    CQ.SECTOR_LIST.forEach((s) => { sec[s.id] = { owner: s.owner0, gar: { ...emptyG(), ...s.gar0 }, bld: [], tired: {} }; });
    // the AI factions start with a couple of buildings in their home sectors so assaults are not free
    sec.sky_lounges.bld = ['bunker', 'turret']; sec.senate_precinct.bld = ['bunker']; sec.csf_hub.bld = ['bunker', 'turret', 'medbay']; sec.freight_hub.bld = ['bunker', 'shield_gen']; sec.the_works.bld = ['bunker'];
    sec.shadow_town.bld = ['bunker'];
    return { v: 2, turn: 1, cr: startCredits || 3000, pwr: 10, heat: 30, rel: { black_sun: -20, exchange: 25, csf: -70 }, sec,
      log: ['Turn 1: Shadow Town is yours. Recruit, build, and take the sectors around you.'], crisis: null, discount: false, pending: null, over: null, brownout: false,
      stats: { won: 0, lost: 0, captured: 0, defended: 0, fallen: 0 }, seq: 1 };
  }
  function fx(sec) {
    const e = { income: 0, pwrGen: 0, heatDecay: 0, heatGen: 0, cap: CQ.BASE_CAP, slots: CQ.BASE_SLOTS, depCap: 0, discount: {} };
    sec.bld.forEach((id) => { const b = BLD[id]; if (!b) return; e.income += b.income || 0; e.pwrGen += b.pwrGen || 0; e.heatDecay += b.heatDecay || 0; e.heatGen += b.heatGen || 0; e.cap += b.cap || 0; e.slots += b.slots || 0; e.depCap += b.depCap || 0; if (b.discount) Object.keys(b.discount).forEach((k) => { e.discount[k] = Math.min(e.discount[k] || 1, b.discount[k]); }); });
    return e;
  }
  const secIncome = (def, sec) => def.income + fx(sec).income;
  function economy(st) {
    let cr = 0, pwrGen = 0, heatDecay = 0, heatGen = 0, upkeep = 0, owned = 0, upkeepCR = 0;
    CQ.SECTOR_LIST.forEach((d) => { const s = st.sec[d.id]; if (s.owner !== 'player') return; owned++; const e = fx(s); cr += d.income + e.income; pwrGen += d.pwr + e.pwrGen; heatDecay += e.heatDecay; heatGen += e.heatGen; ORDER.forEach((k) => { upkeep += (s.gar[k] || 0) * U[k].upkeep; upkeepCR += (s.gar[k] || 0) * U[k].ucr; }); });
    return { cr, pwrGen, heatDecay, heatGen, upkeep, upkeepCR, owned, net: pwrGen - upkeep, netCR: cr - upkeepCR };
  }
  const garCap = (sec) => fx(sec).cap;
  const slots = (sec) => fx(sec).slots;
  const avail = (sec) => { const g = {}; ORDER.forEach((k) => { g[k] = Math.max(0, (sec.gar[k] || 0) - ((sec.tired && sec.tired[k]) || 0)); }); return g; };
  const unitCost = (st, secId, k) => { const e = fx(st.sec[secId]); return Math.round(U[k].cr * (e.discount[k] || 1) * (st.discount ? 0.7 : 1)); };
  const logTo = (st, m) => { st.log.unshift(m); if (st.log.length > 40) st.log.length = 40; };
  const R = (ok, st, msg) => ({ ok, state: st, msg });

  function recruit(st0, secId, k, n) {
    n = n || 1; const st = clone(st0); const sec = st.sec[secId], u = U[k]; if (!sec || sec.owner !== 'player' || !u || k === 'trt') return R(false, st0, 'Not available.');
    if (u.req && !hasB(sec, u.req)) return R(false, st0, `${u.name} needs a ${BLD[u.req].name} in this sector.`);
    { const free = garCap(sec) - total(sec.gar); if (n > free && free > 0) n = free; }
    if (total(sec.gar) + n > garCap(sec)) return R(false, st0, `Garrison full (${garCap(sec)}). Build Barracks or a Command Center, or move units out.`);
    const cost = unitCost(st, secId, k) * n, pw = u.pwr * n;
    if (st.cr < cost) return R(false, st0, `Need ${cost} CR.`); if (st.pwr < pw) return R(false, st0, `Need ${pw} PWR.`);
    st.cr -= cost; st.pwr -= pw; sec.gar[k] = (sec.gar[k] || 0) + n; logTo(st, `Recruited ${n} ${u.name} in ${CQ.SECTORS[secId].name}.`);
    return R(true, st, 'Recruited.');
  }
  function buildCheck(st, secId, bId) {
    const sec = st.sec[secId], b = BLD[bId]; if (!sec || !b) return 'Unknown.'; if (sec.owner !== 'player') return 'You do not own this sector.';
    if (hasB(sec, bId)) return 'Already built here.'; if (sec.bld.length >= slots(sec)) return `No free slots (${slots(sec)}). Demolish something or build a Command Center.`;
    const miss = b.req.filter((r) => !hasB(sec, r)); if (miss.length) return `Needs ${miss.map((r) => BLD[r].name).join(' and ')} first.`;
    if (st.cr < b.cost) return `Need ${b.cost} CR.`; if (st.pwr < b.pwr) return `Need ${b.pwr} PWR.`; return null;
  }
  function build(st0, secId, bId) { const why = buildCheck(st0, secId, bId); if (why) return R(false, st0, why); const st = clone(st0), b = BLD[bId]; st.cr -= b.cost; st.pwr -= b.pwr; st.sec[secId].bld.push(bId); logTo(st, `Built ${b.name} in ${CQ.SECTORS[secId].name}.`); return R(true, st, 'Built.'); }
  function demolish(st0, secId, bId) {
    const st = clone(st0), sec = st.sec[secId]; if (!sec || sec.owner !== 'player' || !hasB(sec, bId)) return R(false, st0, 'Nothing to demolish.');
    const dependants = sec.bld.filter((o) => BLD[o].req.includes(bId)); if (dependants.length) return R(false, st0, `${dependants.map((d) => BLD[d].name).join(', ')} depends on it.`);
    if (bId === 'command_center' && sec.bld.length > CQ.BASE_SLOTS) return R(false, st0, 'Free a slot first: the Command Center provides one.');
    sec.bld = sec.bld.filter((o) => o !== bId); const back = Math.round(BLD[bId].cost * 0.4); st.cr += back; logTo(st, `Demolished ${BLD[bId].name} (+${back} CR).`); return R(true, st, 'Demolished.');
  }
  function move(st0, from, to, units) {
    const st = clone(st0), a = st.sec[from], b = st.sec[to]; if (!a || !b || a.owner !== 'player' || b.owner !== 'player') return R(false, st0, 'Both sectors must be yours.');
    if (!CQ.SECTORS[from].adj.includes(to)) return R(false, st0, 'Sectors are not adjacent.');
    const av = avail(a); let n = 0; for (const k of ORDER) { const c = Math.max(0, Math.floor(units[k] || 0)); if (c > av[k]) return R(false, st0, `Only ${av[k]} ${U[k].short} can move (units that fought or moved this turn are spent).`); n += c; }
    if (!n) return R(false, st0, 'Choose units to move.'); if (total(b.gar) + n > garCap(b)) return R(false, st0, `Destination garrison cap is ${garCap(b)}.`);
    const cost = n * 20; if (st.cr < cost) return R(false, st0, `Logistics cost ${cost} CR.`);
    st.cr -= cost; ORDER.forEach((k) => { const c = Math.max(0, Math.floor(units[k] || 0)); a.gar[k] -= c; b.gar[k] = (b.gar[k] || 0) + c; b.tired[k] = (b.tired[k] || 0) + c; });
    logTo(st, `Moved ${n} units from ${CQ.SECTORS[from].name} to ${CQ.SECTORS[to].name} (${cost} CR).`); return R(true, st, 'Moved.');
  }
  function attackSources(st, targetId) {
    const t = st.sec[targetId]; if (!t || t.owner === 'player') return [];
    return CQ.SECTORS[targetId].adj.filter((id) => st.sec[id].owner === 'player').map((id) => ({ id, avail: avail(st.sec[id]), total: total(avail(st.sec[id])) })).filter((s) => s.total > 0);
  }
  const canAttack = (st, targetId) => { const t = st.sec[targetId]; if (!t || t.owner === 'player') return 'Not an enemy sector.'; if (CQ.SECTORS[targetId].isHQ) return 'Faction headquarters cannot be captured.'; if (!CQ.SECTORS[targetId].adj.some((id) => st.sec[id].owner === 'player')) return 'You must own an adjacent sector to attack.'; return null; };
  const depCapFor = (st, srcIds) => Math.min(22, 14 + srcIds.reduce((n, id) => n + fx(st.sec[id]).depCap, 0));
  const intelLevel = (st, id) => st.sec[id].owner === 'player' || CQ.SECTORS[id].adj.some((a) => st.sec[a].owner === 'player' && hasB(st.sec[a], 'intel_hub'));

  function cfgFromSectors(secs, role) {
    const c = { atk: 0, hp: 0, armor: 0, rally: 0, shield: 0, medbay: 0, turrets: 0, bunker: 0, orbital: 0 };
    secs.forEach((s) => s.bld.forEach((id) => { const f = BLD[id].battle;
      if (role === 'att') { if (f === 'armory') c.atk = 2; if (f === 'rally') c.rally = 1; if (f === 'vets') c.hp = 2; if (f === 'orbital') c.orbital = 1; if (f === 'medbay') c.medic = 1; }
      else { if (f === 'bunker') { c.bunker = 1; } if (f === 'turret') c.turrets = 2; if (f === 'shield') c.shield = 6; if (f === 'medbay') c.medbay = 2; if (f === 'orbital') c.orbital = 1; if (f === 'armory') c.atk = 2; if (f === 'vets') c.hp = 2; } }));
    return c;
  }
  const trimCommit = (commit, cap) => { const out = {}; let left = cap; ORDER.slice().sort((a, b) => U[b].cr - U[a].cr).forEach((k) => { const n = Math.min(commit[k] || 0, left); if (n > 0) { out[k] = n; left -= n; } }); return out; };
  // meta travels with the battle so the result can be applied later
  function makeAttack(st, targetId, commit, seqSeed) {
    const t = st.sec[targetId], srcIds = Object.keys(commit.bySrc);
    const merged = {}; srcIds.forEach((id) => ORDER.forEach((k) => { merged[k] = (merged[k] || 0) + (commit.bySrc[id][k] || 0); }));
    const cap = depCapFor(st, srcIds); const fielded = trimCommit(merged, cap);
    const cfgP = cfgFromSectors(srcIds.map((id) => st.sec[id]), 'att'); const cfgE = cfgFromSectors([t], 'def');
    const seed = CQ.tb.hash(targetId + ':' + st.seq + ':' + st.turn + ':' + (seqSeed || 0));
    const battle = CQ.tb.makeBattle({ sectorId: targetId, theme: CQ.SECTORS[targetId].theme, seed, armies: { p: fielded, e: { ...t.gar } }, attacker: 'p', cfg: { p: cfgP, e: cfgE }, depCap: cap });
    return { battle, meta: { kind: 'attack', targetId, bySrc: commit.bySrc, fielded, cap, defOwner: t.owner } };
  }
  function makeDefense(st, pending, seqSeed) {
    const t = st.sec[pending.targetId]; const cfgP = cfgFromSectors([t], 'def'); const cfgE = { atk: 0, hp: 0, armor: 0, rally: 0, shield: 0, medbay: 0, turrets: 0, bunker: 0, orbital: 0 };
    const seed = CQ.tb.hash('D' + pending.targetId + ':' + st.seq + ':' + st.turn + ':' + (seqSeed || 0));
    const battle = CQ.tb.makeBattle({ sectorId: pending.targetId, theme: CQ.SECTORS[pending.targetId].theme, seed, armies: { p: { ...t.gar }, e: { ...pending.force } }, attacker: 'e', cfg: { p: cfgP, e: cfgE }, depCap: 14 });
    return { battle, meta: { kind: 'defense', targetId: pending.targetId, srcId: pending.srcId, faction: pending.faction, force: pending.force } };
  }
  const subG = (a, b) => { const o = {}; ORDER.forEach((k) => { o[k] = Math.max(0, (a[k] || 0) - (b[k] || 0)); }); return o; };
  const scaleG = (g, f) => { const o = {}; ORDER.forEach((k) => { o[k] = Math.floor((g[k] || 0) * f); }); return o; };
  const revive = (lost, pct, rnd) => { const back = {}; ORDER.forEach((k) => { let n = 0; for (let i = 0; i < (lost[k] || 0); i++) if (rnd() < pct) n++; back[k] = n; }); return back; };
  // spread casualties over sources in proportion to what each committed
  function distribute(bySrc, lost) {
    const rem = {}; Object.keys(bySrc).forEach((id) => { rem[id] = { ...emptyG(), ...bySrc[id] }; });
    ORDER.forEach((k) => { let toRemove = lost[k] || 0; const ids = Object.keys(rem).filter((id) => rem[id][k] > 0); let i = 0; while (toRemove > 0 && ids.length) { const id = ids[i % ids.length]; if (rem[id][k] > 0) { rem[id][k]--; toRemove--; } if (!ids.some((x) => rem[x][k] > 0)) break; i++; } });
    return rem; // survivors by source
  }
  function applyResult(st0, meta, result, rnd) {
    const st = clone(st0); const res = result; rnd = rnd || Math.random; const out = { st, summary: '' };
    if (meta.kind === 'attack') {
      const tId = meta.targetId, t = st.sec[tId], def = CQ.SECTORS[tId], ids = Object.keys(meta.bySrc);
      const medic = ids.some((id) => hasB(st.sec[id], 'medbay')) || hasB(t, 'medbay');
      const lostP = { ...res.lost.p }; let survP = { ...res.survivors.p };
      if (res.reason === 'retreat') { let n = Math.ceil(total(survP) * 0.25); ORDER.slice().sort((a, b) => U[a].cr - U[b].cr).forEach((k) => { const c = Math.min(survP[k] || 0, n); survP[k] -= c; lostP[k] = (lostP[k] || 0) + c; n -= c; }); }
      const back = medic ? revive(lostP, 0.4, rnd) : emptyG(); survP = addG(survP, back);
      const netLost = total(lostP) - total(back); st.stats.fallen += netLost;
      // everything committed leaves its home sector first
      ids.forEach((id) => { ORDER.forEach((k) => { st.sec[id].gar[k] = Math.max(0, (st.sec[id].gar[k] || 0) - (meta.bySrc[id][k] || 0)); }); });
      // committed minus casualties, per source; units that were never fielded (over the deployment cap) go straight home
      const r = distribute(meta.bySrc, subG(meta.fielded, survP));
      ORDER.forEach((k) => { let bench = 0; ids.forEach((id) => { bench += meta.bySrc[id][k] || 0; }); bench -= meta.fielded[k] || 0; ids.forEach((id) => { const c = Math.min(Math.max(0, bench), r[id][k] || 0); if (c > 0) { st.sec[id].gar[k] = (st.sec[id].gar[k] || 0) + c; r[id][k] -= c; bench -= c; } }); });
      if (res.winner === 'p') {
        const occ = emptyG(); ids.forEach((id) => { ORDER.forEach((k) => { occ[k] += r[id][k] || 0; }); });
        const old = t.owner; t.owner = 'player'; t.gar = occ; t.tired = { ...occ };
        t.bld = t.bld.filter(() => rnd() < 0.5).slice(0, CQ.BASE_SLOTS);
        const loot = Math.round(def.income * 1.2); st.cr += loot; st.heat = Math.min(100, st.heat + 4); st.stats.won++; st.stats.captured++;
        if (st.rel[old] !== undefined) st.rel[old] = Math.max(-100, st.rel[old] - 12);
        out.summary = `VICTORY: ${def.name} is yours. +${loot} CR loot. Lost ${netLost} units${total(back) ? ` (${total(back)} revived by medics)` : ''}.`;
      } else {
        ids.forEach((id) => { ORDER.forEach((k) => { const n = r[id][k] || 0; if (n > 0) { st.sec[id].gar[k] = (st.sec[id].gar[k] || 0) + n; st.sec[id].tired[k] = (st.sec[id].tired[k] || 0) + n; } }); });
        t.gar = { ...emptyG(), ...res.survivors.e }; if (total(t.gar) === 0) t.gar = { ...emptyG(), inf: 1 };
        st.heat = Math.min(100, st.heat + 6); st.stats.lost++;
        out.summary = res.reason === 'retreat' ? `RETREAT from ${def.name}. Lost ${netLost} units in the fighting and the withdrawal.` : `DEFEAT at ${def.name}. Lost ${netLost} units; the survivors fall back.`;
      }
    } else { // defense of a player sector
      const tId = meta.targetId, t = st.sec[tId], def = CQ.SECTORS[tId], src = st.sec[meta.srcId];
      const back = hasB(t, 'medbay') ? revive(res.lost.p, 0.4, rnd) : emptyG(); const netLost = total(res.lost.p) - total(back); st.stats.fallen += netLost;
      const surv = addG(res.survivors.p, back);
      if (res.winner === 'p') { t.gar = surv; st.stats.defended++; st.heat = Math.max(0, st.heat - 2); ORDER.forEach((k) => { src.gar[k] = (src.gar[k] || 0) + (res.survivors.e[k] || 0); }); out.summary = `HELD: your garrison in ${def.name} beat back ${CQ.FACTIONS[meta.faction].name}. Lost ${netLost} units.`; }
      else if (def.isHQ) { t.gar = { ...emptyG(), ...surv, inf: (surv.inf || 0) + 3 }; ORDER.forEach((k) => { src.gar[k] = (src.gar[k] || 0) + (res.survivors.e[k] || 0); }); st.stats.lost++; out.summary = `${def.name} was battered but headquarters cannot fall: militia rallies. Lost ${netLost} units.`; }
      else { t.owner = meta.faction; t.gar = { ...emptyG(), ...res.survivors.e }; t.tired = {}; t.bld = t.bld.filter(() => rnd() < 0.4); st.stats.lost++; out.summary = `LOST: ${CQ.FACTIONS[meta.faction].name} took ${def.name}. Lost ${netLost} units.`; }
      st.pending = null;
    }
    logTo(st, out.summary); st.seq++; return out;
  }
  function autoResolveAttack(st, targetId, commit, rnd) {
    const { battle, meta } = makeAttack(st, targetId, commit, 0); const result = CQ.tb.autoBattle(battle, CQ.tb.mb32(Math.floor(rnd() * 1e9))); return { ...applyResult(st, meta, result, rnd), result };
  }
  function autoResolveDefense(st, rnd) {
    const { battle, meta } = makeDefense(st, st.pending, 0); const result = CQ.tb.autoBattle(battle, CQ.tb.mb32(Math.floor(rnd() * 1e9))); const out = applyResult(st, meta, result, rnd); return { ...out, result };
  }
  function forecast(st, targetId, commit, n, rnd) {
    n = n || 12; let win = 0, lossPct = 0, rounds = 0; const base = total(Object.values(commit.bySrc).reduce((a, g) => addG(a, g), emptyG())) || 1;
    for (let i = 0; i < n; i++) { const { battle } = makeAttack(st, targetId, commit, i + 1); const r = CQ.tb.autoBattle(battle, CQ.tb.mb32(Math.floor(rnd() * 1e9))); if (r.winner === 'p') win++; lossPct += total(r.lost.p) / Math.max(1, total(r.lost.p) + total(r.survivors.p)); rounds += r.rounds; }
    return { winPct: Math.round((win / n) * 100), lossPct: Math.round((lossPct / n) * 100), rounds: Math.round(rounds / n) };
  }
  const band = (p) => { const eq = p / 45; return eq < 6 ? 'Light' : eq < 14 ? 'Moderate' : eq < 28 ? 'Heavy' : eq < 55 ? 'Fortified' : 'Overwhelming'; }; // 45 = one Enforcer

  // ------------------------------------------------------------------- enemy factions
  const UNIT_VALUE = { inf: 0.5, gam: 1.3, snp: 1.2, med: 0.8, spc: 1.6, drd: 2.3, tnk: 4, wlk: 7 };
  const pickWeighted = (mix, rnd, maxVal) => { const keys = Object.keys(mix).filter((k) => UNIT_VALUE[k] <= maxVal); let tot = keys.reduce((n, k) => n + mix[k], 0), r = rnd() * tot; for (const k of keys) { r -= mix[k]; if (r <= 0) return k; } return 'inf'; };
  function factionEconomy(st, f, rnd) {
    const mine = CQ.SECTOR_LIST.filter((d) => st.sec[d.id].owner === f); if (!mine.length) return;
    const inc = mine.reduce((n, d) => n + d.income, 0); let pts = inc / 300 + 0.5; const mix = CQ.FACTIONS[f].mix; let guard = 0;
    while (pts > 0.4 && guard++ < 40) {
      const d = mine.slice().sort((a, b) => power(st.sec[a.id].gar) - power(st.sec[b.id].gar))[Math.floor(rnd() * Math.min(3, mine.length))]; const s = st.sec[d.id];
      if (total(s.gar) >= CQ.BASE_CAP + (CQ.SECTORS[d.id].isHQ ? 6 : 2)) { pts -= 0.3; continue; }
      const k = pickWeighted(mix, rnd, Math.min(pts + 0.2, d.income >= 900 ? 7 : 4)); s.gar[k] = (s.gar[k] || 0) + 1; pts -= UNIT_VALUE[k];
    }
    // buildings: a defensive structure every few turns on frontier sectors
    if (st.turn % 4 === 0) { const d = mine[Math.floor(rnd() * mine.length)]; const s = st.sec[d.id]; const opts = ['bunker', 'turret', 'medbay', 'shield_gen'].filter((b) => !s.bld.includes(b) && BLD[b].req.every((r) => s.bld.includes(r))); if (opts.length && s.bld.length < 3) s.bld.push(opts[Math.floor(rnd() * opts.length)]); }
  }
  function aiAttack(st, f, rnd, events) {
    const F = CQ.FACTIONS[f]; const rel = st.rel[f] || 0;
    let chance = (F.aggr / 100) * 0.5 + st.turn * 0.004; if (f === 'csf') chance += st.heat / 400; if (st.turn < 3) chance *= 0.4;
    if (rnd() > chance) return;
    const cands = [];
    CQ.SECTOR_LIST.forEach((d) => { const s = st.sec[d.id]; if (s.owner !== f) return; const sp = power(scaleG(s.gar, 0.65)); if (total(s.gar) < 5) return;
      d.adj.forEach((aid) => { const t = st.sec[aid], td = CQ.SECTORS[aid]; if (t.owner === f || td.isHQ) return; const tp = power(t.gar) * (1 + 0.12 * t.bld.length);
        let w = 0; if (t.owner === 'neutral') w = 3; else if (t.owner === 'player') { if (rel >= 80) return; w = rel <= -50 ? 4 : rel < 0 ? 2.5 : rel < 40 ? 1.2 : 0.4; w *= 1 + st.turn * 0.02; } else w = F.rivals.includes(t.owner) ? 2 : 0.6;
        const need = t.owner === 'player' ? 0.85 : 1.1; if (sp / Math.max(1, tp) < need) return; cands.push({ src: d.id, target: aid, w: w * Math.min(2, sp / Math.max(1, tp)) }); }); });
    if (!cands.length) return;
    let tot = cands.reduce((n, c) => n + c.w, 0), r = rnd() * tot, pick = cands[0]; for (const c of cands) { r -= c.w; if (r <= 0) { pick = c; break; } }
    const s = st.sec[pick.src], t = st.sec[pick.target];
    // march 65% of the garrison, keep a few at home
    const force = emptyG(); ORDER.forEach((k) => { force[k] = Math.floor((s.gar[k] || 0) * 0.65); }); if (total(force) < 3) return;
    ORDER.forEach((k) => { s.gar[k] -= force[k]; });
    if (t.owner === 'player') { if (st.pending) { ORDER.forEach((k) => { s.gar[k] += force[k]; }); return; } st.pending = { faction: f, srcId: pick.src, targetId: pick.target, force }; events.push(`ALERT: ${F.name} is assaulting ${CQ.SECTORS[pick.target].name} from ${CQ.SECTORS[pick.src].name}!`); return; }
    // AI versus AI or neutral: resolve headless
    const cfgE = cfgFromSectors([t], 'def'); const seed = CQ.tb.hash('A' + pick.target + ':' + st.turn + ':' + f);
    const b = CQ.tb.makeBattle({ sectorId: pick.target, theme: CQ.SECTORS[pick.target].theme, seed, armies: { p: force, e: { ...t.gar } }, attacker: 'p', cfg: { p: {}, e: cfgE }, depCap: 14 });
    const res = CQ.tb.autoBattle(b, CQ.tb.mb32(Math.floor(rnd() * 1e9))); const oldOwner = t.owner;
    if (res.winner === 'p') { t.owner = f; t.gar = { ...emptyG(), ...res.survivors.p }; t.tired = {}; t.bld = t.bld.filter(() => rnd() < 0.4); events.push(`${F.name} captured ${CQ.SECTORS[pick.target].name} from ${(CQ.FACTIONS[oldOwner] || {}).name}.`); }
    else { t.gar = { ...emptyG(), ...res.survivors.e }; ORDER.forEach((k) => { s.gar[k] += res.survivors.p[k] || 0; }); if (total(t.gar) === 0) t.gar = { ...emptyG(), inf: 1 }; }
  }
  function neutralRegen(st) { CQ.SECTOR_LIST.forEach((d) => { const s = st.sec[d.id]; if (s.owner === 'neutral') { ORDER.forEach((k) => { const base = d.gar0[k] || 0; if ((s.gar[k] || 0) < base && total(s.gar) < total(d.gar0) + 3) s.gar[k] = (s.gar[k] || 0) + 1; }); } }); }

  const CRISES = [
    { title: 'CSF Sector Sweep', desc: 'Heat +5 and income cut 30% this turn.', fx: (st, e) => { st.heat = Math.min(100, st.heat + 5); e.crMul = 0.7; } },
    { title: 'Power Conduit Rupture', desc: 'Power reserve -8.', fx: (st) => { st.pwr = Math.max(0, st.pwr - 8); } },
    { title: 'Underworld Cartel War', desc: 'Recruits are 30% cheaper next turn.', fx: (st) => { st.discount = true; } },
    { title: 'Black Market Windfall', desc: '+500 CR shipment.', fx: (st, e) => { e.crAdd = 500; } },
    { title: 'Informant Tip-off', desc: 'The CSF raids a weakly held sector of yours. It loses 2 units.', fx: (st, e, rnd) => { const mine = CQ.SECTOR_LIST.filter((d) => st.sec[d.id].owner === 'player' && !d.isHQ && total(st.sec[d.id].gar) > 2); if (mine.length) { const d = mine[Math.floor(rnd() * mine.length)]; const g = st.sec[d.id].gar; let n = 2; ORDER.forEach((k) => { while (n > 0 && g[k] > 0) { g[k]--; n--; } }); } } },
  ];
  function endTurn(st0, rnd) {
    if (st0.pending) return R(false, st0, 'Resolve the enemy assault first.'); if (st0.over) return R(false, st0, 'The campaign is over.');
    rnd = rnd || Math.random; const st = clone(st0); const events = [];
    const eco = economy(st); let cr = eco.cr; const e = { crMul: 1, crAdd: 0 };
    st.turn++; st.discount = false; st.crisis = null;
    if (st.turn % 3 === 0) { const c = CRISES[Math.floor(rnd() * CRISES.length)]; c.fx(st, e, rnd); st.crisis = { title: c.title, desc: c.desc }; events.push(`CRISIS: ${c.title}. ${c.desc}`); }
    cr = Math.round(cr * e.crMul) + e.crAdd;
    // power: upkeep comes out of generation; a deficit browns the grid out
    const net = eco.pwrGen - eco.upkeep; st.brownout = false;
    if (st.pwr + net < 0) { st.pwr = 0; st.brownout = true; cr = Math.round(cr * 0.75); events.push('BROWNOUT: not enough PWR for your upkeep. Income cut 25%. Build power or release machines.'); } else st.pwr += net;
    cr -= eco.upkeepCR; st.cr += cr;
    if (st.cr < 0) { st.cr = 0; let n = 2; const big = CQ.SECTOR_LIST.filter((d) => st.sec[d.id].owner === 'player').sort((a, b) => total(st.sec[b.id].gar) - total(st.sec[a.id].gar))[0]; if (big) { const g = st.sec[big.id].gar; ORDER.slice().sort((a, b) => U[a].cr - U[b].cr).forEach((k) => { while (n > 0 && g[k] > 0) { g[k]--; n--; } }); } events.push('UNPAID TROOPS: you could not cover the payroll and some units deserted.'); }
    st.heat = Math.min(100, Math.max(0, st.heat + 2 + eco.heatGen - eco.heatDecay));
    if (st.heat >= 100) { const loss = Math.round(st.cr * 0.15); st.cr -= loss; st.heat = 60; const mine = CQ.SECTOR_LIST.filter((d) => st.sec[d.id].owner === 'player' && st.sec[d.id].bld.length && !d.isHQ); if (mine.length) { const d = mine[Math.floor(rnd() * mine.length)]; const s = st.sec[d.id]; const gone = s.bld.filter((b) => !s.bld.some((o) => BLD[o].req.includes(b)))[0] || s.bld[s.bld.length - 1]; s.bld = s.bld.filter((b) => b !== gone); events.push(`CSF RAID: -${loss} CR and the ${BLD[gone].name} in ${d.name} was seized. Heat drops to 60.`); } else events.push(`CSF RAID: -${loss} CR. Heat drops to 60.`); }
    CQ.SECTOR_LIST.forEach((d) => { st.sec[d.id].tired = {}; });
    // enemy turns
    neutralRegen(st); AIF.slice().sort(() => rnd() - 0.5).forEach((f) => { factionEconomy(st, f, rnd); aiAttack(st, f, rnd, events); });
    events.forEach((m) => logTo(st, m)); logTo(st, `Turn ${st.turn}: ${cr >= 0 ? '+' : ''}${cr} CR (after ${eco.upkeepCR} payroll), ${net >= 0 ? '+' : ''}${net} PWR, Heat ${st.heat}.`);
    if (!st.pending && !st.won && economy(st).cr >= CQ.WIN_INCOME) st.over = 'win';
    return { ...R(true, st, 'Turn ended.'), events };
  }
  function adjustRelation(st0, f, delta) {
    const st = clone(st0); if (delta > 0) { if (st.cr < 400) return R(false, st0, 'Bribes cost 400 CR.'); st.cr -= 400; }
    st.rel[f] = Math.max(-100, Math.min(100, (st.rel[f] || 0) + delta)); logTo(st, delta > 0 ? `Relations with ${CQ.FACTIONS[f].name} improved.` : `Tensions with ${CQ.FACTIONS[f].name} rose.`); return R(true, st, 'ok');
  }
  return { newCampaign, clone, total, power, addG, subG, emptyG, avail, fx, economy, garCap, slots, unitCost, recruit, buildCheck, build, demolish, move, attackSources, canAttack, depCapFor, intelLevel, makeAttack, makeDefense, applyResult, autoResolveAttack, autoResolveDefense, forecast, band, endTurn, adjustRelation, secIncome, logTo, cfgFromSectors, trimCommit };
})();
// ===== CONQUEST ENGINE END =====


function PitFightOverlay({ onSuccess, onFailure, opponentName, opponentHp, accent }) {
  const gameRef = React.useRef({ playerHp: 3, opponentHp: opponentHp, phase: 'ready', telegraph: null, tickCount: 0, running: true });
  const [display, setDisplay] = React.useState({ playerHp: 3, opponentHp: opponentHp, telegraph: null, message: 'The pit is watching. Press A (dodge), D (counter), or S (guard).', outcome: null });
  const intervalRef = React.useRef(null);
  const TELEGRAPHS = [
    { name: 'OVERHEAD', counter: 'S', label: 'Guard (S)' },
    { name: 'SWEEP',    counter: 'A', label: 'Dodge (A)' },
    { name: 'LUNGE',    counter: 'D', label: 'Counter (D)' },
  ];
  React.useEffect(() => {
    const ref = gameRef.current;
    intervalRef.current = setInterval(() => {
      if (!ref.running) return;
      ref.tickCount++;
      if (ref.phase === 'ready' && ref.tickCount % 4 === 0) {
        const t = TELEGRAPHS[Math.floor(Math.random() * TELEGRAPHS.length)];
        ref.telegraph = t;
        ref.phase = 'telegraphed';
        ref.tickCount = 0;
        setDisplay(d => ({ ...d, telegraph: t, message: `${opponentName} telegraphs: ${t.name}! Respond with ${t.label}.` }));
      } else if (ref.phase === 'telegraphed' && ref.tickCount >= 3) {
        ref.playerHp = Math.max(0, ref.playerHp - 1);
        ref.telegraph = null;
        ref.phase = 'ready';
        ref.tickCount = 0;
        if (ref.playerHp <= 0) {
          ref.running = false;
          clearInterval(intervalRef.current);
          setDisplay(d => ({ ...d, playerHp: 0, telegraph: null, message: 'You fall back. The pit is unforgiving.', outcome: 'fail' }));
          setTimeout(() => onFailure && onFailure(), 1800);
        } else {
          setDisplay(d => ({ ...d, playerHp: ref.playerHp, telegraph: null, message: 'You took a hit. Stay focused.' }));
        }
      }
    }, 600);
    const handleKey = (e) => {
      const ref = gameRef.current;
      if (!ref.running || ref.phase !== 'telegraphed') return;
      const key = e.key.toUpperCase();
      if (!['A','D','S'].includes(key)) return;
      if (key === ref.telegraph.counter) {
        ref.opponentHp = Math.max(0, ref.opponentHp - 1);
        ref.telegraph = null;
        ref.phase = 'ready';
        ref.tickCount = 0;
        if (ref.opponentHp <= 0) {
          ref.running = false;
          clearInterval(intervalRef.current);
          window.removeEventListener('keydown', handleKey);
          setDisplay(d => ({ ...d, opponentHp: 0, telegraph: null, message: `${opponentName} goes down. The pit made its decision.`, outcome: 'success' }));
          setTimeout(() => onSuccess && onSuccess(), 1800);
        } else {
          setDisplay(d => ({ ...d, opponentHp: ref.opponentHp, telegraph: null, message: 'Clean counter. Keep going.' }));
        }
      } else {
        ref.playerHp = Math.max(0, ref.playerHp - 1);
        ref.telegraph = null;
        ref.phase = 'ready';
        ref.tickCount = 0;
        if (ref.playerHp <= 0) {
          ref.running = false;
          clearInterval(intervalRef.current);
          window.removeEventListener('keydown', handleKey);
          setDisplay(d => ({ ...d, playerHp: 0, telegraph: null, message: 'Wrong call. You are down.', outcome: 'fail' }));
          setTimeout(() => onFailure && onFailure(), 1800);
        } else {
          setDisplay(d => ({ ...d, playerHp: ref.playerHp, telegraph: null, message: 'Wrong counter. You took damage.' }));
        }
      }
    };
    window.addEventListener('keydown', handleKey);
    return () => { ref.running = false; clearInterval(intervalRef.current); window.removeEventListener('keydown', handleKey); };
  }, []);
  const barColor = accent || '#C03030';
  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.92)', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', zIndex:200, fontFamily:'monospace', color:'#EEE' }}>
      <div style={{ fontSize:'1.1rem', color: barColor, marginBottom:'1.2rem', letterSpacing:2 }}>PIT ARENA — {opponentName.toUpperCase()}</div>
      <div style={{ display:'flex', gap:'3rem', marginBottom:'1.2rem' }}>
        <div style={{ textAlign:'center' }}>
          <div style={{ fontSize:'0.75rem', color:'#888', marginBottom:'0.3rem' }}>YOU</div>
          <div style={{ display:'flex', gap:'4px' }}>
            {Array.from({ length: 3 }).map((_, i) => (
              <div key={i} style={{ width:18, height:18, background: i < display.playerHp ? '#4A9FFF' : '#222', border:'1px solid #444' }} />
            ))}
          </div>
        </div>
        <div style={{ textAlign:'center' }}>
          <div style={{ fontSize:'0.75rem', color:'#888', marginBottom:'0.3rem' }}>{opponentName.toUpperCase()}</div>
          <div style={{ display:'flex', gap:'4px' }}>
            {Array.from({ length: opponentHp }).map((_, i) => (
              <div key={i} style={{ width:18, height:18, background: i < display.opponentHp ? barColor : '#222', border:'1px solid #444' }} />
            ))}
          </div>
        </div>
      </div>
      {display.telegraph && (
        <div style={{ fontSize:'1.4rem', color: barColor, fontWeight:'bold', marginBottom:'0.8rem', letterSpacing:3 }}>
          {display.telegraph.name}
        </div>
      )}
      <div style={{ maxWidth:400, textAlign:'center', color:'#CCC', fontSize:'0.85rem', lineHeight:1.6, marginBottom:'1rem' }}>{display.message}</div>
      {display.outcome === 'fail' && <div style={{ color:'#888', fontSize:'0.8rem' }}>Walk away and try again later.</div>}
      <div style={{ marginTop:'1rem', fontSize:'0.75rem', color:'#555' }}>A: Dodge &nbsp; D: Counter &nbsp; S: Guard</div>
    </div>
  );
}

// ===== CAVERN PROPS BEGIN =====
// Deep Caverns of Bador (tag cav): stalactite column, spore pods, survey beacon, KDY core drill, tuned crystal pylon,
// the guardian cradle (plinth, wreck, stripped, dormant) and the sealed or open gate shutter.
function PropCavStalactite() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={90} rx={28} ry={4} />
      <Glow cx={32} cy={86} rx={27} ry={6} c={S.violet} opacity={0.4} style={{ animation:'ship-engine 3.2s ease-in-out infinite' }} />
      <Bev t="polygon" points={pts([[8, 2], [56, 2], [45, 30], [41, 52], [50, 76], [61, 92], [3, 92], [15, 76], [23, 52], [19, 28]])} c="stone" o={1.1} />
      <polygon points={pts([[8, 2], [26, 2], [25, 32], [24, 54], [17, 76], [3, 92], [12, 92], [20, 76], [26, 54], [27, 32]])} fill={ART.stone.hi} opacity="0.35" />
      <polygon points={pts([[44, 6], [56, 2], [45, 30], [41, 52], [50, 76], [61, 92], [52, 92], [44, 74], [37, 54], [39, 30]])} fill={ART.stone.shade} opacity="0.5" />
      <Seams x={14} y={4} w={36} h={86} cols={1} rows={4} op={0.3} />
      <g fill="none" stroke={S.violet} strokeWidth="1" opacity="0.85">
        <polyline points={pts([[22, 10], [28, 26], [26, 44], [33, 62], [30, 80]])} />
        <polyline points={pts([[40, 12], [37, 30], [40, 50], [36, 70]])} opacity="0.6" />
      </g>
      <g>
        <polygon points={pts([[10, 92], [13, 80], [17, 92]])} fill={ART.fabric.hi} />
        <polygon points={pts([[14, 92], [18, 74], [22, 92]])} fill={S.violet} opacity="0.9" />
        <polygon points={pts([[44, 92], [48, 78], [52, 92]])} fill={ART.fabric.hi} />
        <polygon points={pts([[50, 92], [53, 82], [56, 92]])} fill={S.violet} opacity="0.9" />
      </g>
      <Greeble x={18} y={60} w={28} h={28} seed={640} n={6} c="stone" />
      <Grime x={10} y={8} w={44} h={80} seed={641} n={7} op={0.3} /><Scuff x={14} y={10} w={34} h={76} seed={642} n={8} c="stone" />
    </svg>
  );
}

function PropCavSporePod() {
  const S = ART.signal;
  const pod = (x, y, r, k, d) => (
    <g key={k}>
      <line x1={x} y1={y + r} x2={x + (k % 2 ? 2 : -2)} y2={58} stroke={ART.moss.shade} strokeWidth="1.6" />
      <Bev t="polygon" points={pts(ngon(x, y, r, r * 1.15, 7, 0.2))} c="moss" o={0.9} />
      <polygon points={pts(ngon(x, y, r * 0.55, r * 0.62, 6, 0.4))} fill={S.lime} opacity="0.8" style={{ animation:`ship-engine ${3 + d}s ease-in-out ${d}s infinite` }} />
      <polygon points={pts([[x - r * 0.5, y - r * 0.6], [x - r * 0.1, y - r * 0.9], [x - r * 0.2, y - r * 0.4]])} fill={ART.moss.hi} />
    </g>
  );
  return (
    <svg viewBox="0 0 96 64" width="96" height="64" style={PROP_STYLE}>
      <PropShadow cx={48} cy={60} rx={42} ry={3.6} />
      <Glow cx={48} cy={38} rx={44} ry={20} c={S.lime} opacity={0.3} style={{ animation:'ship-engine 4s ease-in-out infinite' }} />
      <polygon points={pts([[6, 61], [10, 54], [26, 50], [48, 52], [70, 50], [86, 54], [90, 61]])} fill={ART.dark.shade} />
      <Bev t="polygon" points={pts([[8, 61], [13, 55], [28, 53], [48, 54], [68, 53], [83, 55], [88, 61]])} c="stone" o={0.9} />
      {pod(20, 40, 9, 1, 0)}{pod(36, 32, 11, 2, 0.6)}{pod(54, 38, 12, 3, 1.2)}{pod(70, 42, 8, 4, 0.3)}{pod(80, 48, 5, 5, 0.9)}
      <g fill={S.greenHi} opacity="0.7" style={{ animation:'steam-rise 5s ease-out infinite' }}>
        <circle cx="30" cy="18" r="1" /><circle cx="50" cy="12" r="1.2" /><circle cx="66" cy="20" r="0.9" /><circle cx="44" cy="22" r="0.8" />
      </g>
      <Seams x={10} y={53} w={76} h={8} cols={5} op={0.3} />
      <Greeble x={12} y={55} w={72} h={6} seed={650} n={7} c="stone" />
      <Grime x={8} y={52} w={80} h={9} seed={651} n={5} op={0.3} /><Scuff x={10} y={53} w={74} h={7} seed={652} n={5} c="stone" />
    </svg>
  );
}

function PropCavBeacon({ variant }) {
  const S = ART.signal;
  const memorial = variant === 'memorial';
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={90} rx={26} ry={4} />
      <Slab x={8} y={80} w={48} h={9} k={1.6} c="dark" />
      <Hazard x={11} y={85} w={42} h={2.6} />
      {memorial ? (
        <g>
          <Bev t="polygon" points={pts([[20, 80], [22, 26], [28, 14], [36, 14], [42, 26], [44, 80]])} c="pearl" o={1} />
          <polygon points={pts([[28, 14], [32, 8], [36, 14]])} fill={ART.pearl.hi} />
          <rect x="26" y="32" width="12" height="20" fill={ART.screen.cyan} />
          <text x="32" y="38" textAnchor="middle" fontSize="2.6" fill={S.cyanHi} fontWeight="700" {...PT}>TEAM NINE</text>
          <text x="32" y="43" textAnchor="middle" fontSize="1.8" fill={S.cyan} {...PT}>DUG BY ORDER</text>
          <text x="32" y="47" textAnchor="middle" fontSize="1.8" fill={S.cyan} {...PT}>OF I. CHERIT</text>
          <Glow cx={32} cy={10} r={9} c={S.cyanHi} opacity={0.5} style={{ animation:'ship-engine 3.6s ease-in-out infinite' }} />
          <g>
            <line x1="46" y1="84" x2="50" y2="74" stroke={ART.moss.base} strokeWidth="1.4" />
            <polygon points={pts([[48, 76], [52, 72], [50, 78]])} fill={S.lime} />
          </g>
          <Seams x={21} y={16} w={22} h={62} cols={0} rows={5} op={0.3} />
        </g>
      ) : (
        <g>
          <polygon points={pts([[10, 82], [30, 34], [33, 34], [24, 82]])} fill={ART.steel.shade} />
          <polygon points={pts([[54, 82], [36, 34], [33, 34], [42, 82]])} fill={ART.steel.shade} />
          <Bev t="rect" c="steel" x={29} y={18} width={6} height={62} />
          {[24, 36, 48, 60].map((y, i) => <rect key={y} x="29" y={y} width="6" height="6" fill={i % 2 ? ART.hull.hi : ART.red.base} />)}
          <Slab x={22} y={8} w={20} h={12} k={2} c="dark" />
          <rect x="26" y="11" width="12" height="6" fill={S.amber} style={{ animation:'ship-blink 2.4s steps(2) infinite' }} />
          <Glow cx={32} cy={14} r={12} c={S.amber} opacity={0.45} style={{ animation:'ship-engine 2.4s ease-in-out infinite' }} />
          <polygon points={pts([[38, 28], [52, 22], [50, 34]])} fill={ART.hull.base} />
          <rect x="22" y="64" width="20" height="9" fill={ART.screen.amber} />
          <text x="32" y="70" textAnchor="middle" fontSize="2.8" fill={S.amberHi} fontWeight="700" {...PT}>GRID 9-1</text>
          <Lights x={25} y={76} n={4} gap={4.4} seed={660} />
        </g>
      )}
      <Rivets x={12} y={82} w={40} n={7} c="dark" />
      <Grime x={10} y={12} w={44} h={72} seed={661} n={6} op={0.26} /><Scuff x={14} y={14} w={36} h={66} seed={662} n={7} c={memorial ? 'pearl' : 'steel'} />
    </svg>
  );
}

function PropCavRig() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 128 96" width="128" height="96" style={PROP_STYLE}>
      <PropShadow cx={64} cy={90} rx={58} ry={4.4} />
      <Slab x={6} y={74} w={116} h={14} k={2} c="dark" />
      <Hazard x={10} y={82} w={108} h={3.4} />
      <polygon points={pts([[40, 78], [52, 12], [60, 12], [62, 78]])} fill={ART.steel.shade} />
      <polygon points={pts([[74, 78], [68, 12], [76, 12], [90, 78]])} fill={ART.steel.shade} />
      <Bev t="polygon" points={pts([[44, 76], [53, 16], [58, 16], [58, 76]])} c="steel" o={0.9} />
      <Bev t="polygon" points={pts([[76, 76], [70, 16], [75, 16], [86, 76]])} c="steel" o={0.9} />
      <g stroke={ART.dark.base} strokeWidth="1.2" fill="none">
        <polyline points={pts([[48, 70], [72, 54], [50, 40], [70, 26]])} />
        <polyline points={pts([[72, 70], [50, 54], [70, 40], [54, 26]])} />
      </g>
      <Slab x={44} y={4} w={36} h={14} k={2.2} c="deepglass" />
      <Slab x={48} y={7} w={28} h={8} k={1.4} c="dark" />
      <rect x="52" y="9" width="20" height="3" fill={S.cyan} style={{ animation:'holo-flicker 3s steps(3) infinite' }} />
      <Bev t="rect" c="steel" x={58} y={40} width={8} height={30} />
      <polygon points={pts([[56, 70], [68, 70], [62, 86]])} fill={ART.dark.base} />
      <polygon points={pts([[58, 78], [66, 78], [62, 90]])} fill={ART.steel.hi} opacity="0.7" />
      <Bev t="polygon" points={pts(ngon(62, 88, 12, 3.4, 8))} c="dark" o={0.6} />
      <Slab x={8} y={50} w={30} h={24} k={2} c="deepglass" />
      <Vent x={11} y={53} w={24} h={8} n={3} />
      <rect x="11" y="64" width="24" height="7" fill={ART.screen.amber} />
      <text x="23" y="69.4" textAnchor="middle" fontSize="3.2" fill={S.amberHi} fontWeight="700" {...PT}>KDY-7 CORE DRILL</text>
      <Bev t="polygon" points={pts(ngon(104, 62, 12, 12, 6))} c="steel" o={0.9} />
      <Bev t="polygon" points={pts(ngon(104, 62, 7, 7, 6))} c="dark" o={0.6} />
      <rect x="92" y="66" width="24" height="4" fill={ART.amber.base} />
      <Cable d="M38 64 Q60 90 92 70" c={ART.amber.shade} w={1.8} />
      <Lights x={100} y={46} n={4} gap={4} seed={670} />
      <Rivets x={8} y={76} w={110} n={14} c="dark" />
      <Seams x={8} y={50} w={30} h={24} cols={1} rows={0} op={0.4} />
      <Greeble x={90} y={30} w={26} h={14} seed={671} n={7} c="steel" />
      <Grime x={8} y={20} w={112} h={66} seed={672} n={9} op={0.28} /><Scuff x={10} y={24} w={108} h={60} seed={673} n={10} c="steel" />
    </svg>
  );
}

function PropCavPylon() {
  const S = ART.signal;
  return (
    <svg viewBox="0 0 64 96" width="64" height="96" style={PROP_STYLE}>
      <PropShadow cx={32} cy={90} rx={25} ry={4} />
      <Glow cx={32} cy={36} rx={26} ry={32} c={S.violet} opacity={0.35} style={{ animation:'ship-engine 3.4s ease-in-out infinite' }} />
      <Bev t="polygon" points={pts([[10, 90], [14, 72], [50, 72], [54, 90]])} c="stone" o={1} />
      <Slab x={16} y={64} w={32} h={10} k={2} c="brass" />
      <Slab x={20} y={54} w={24} h={10} k={1.8} c="bronze" />
      <polygon points={pts([[32, 6], [44, 22], [42, 52], [22, 52], [20, 22]])} fill={ART.glass.shade} transform="translate(1 1.4)" />
      <polygon points={pts([[32, 6], [44, 22], [42, 52], [22, 52], [20, 22]])} fill={ART.fabric.hi} />
      <polygon points={pts([[32, 6], [44, 22], [42, 52], [32, 52]])} fill={ART.glass.base} opacity="0.65" />
      <polyline points={pts([[27, 48], [30, 30], [34, 40], [36, 20]])} fill="none" stroke={S.cyanHi} strokeWidth="0.9" opacity="0.9" />
      <rect x="29" y="66" width="6" height="5" fill={S.cyan} style={{ animation:'ship-blink 2.2s steps(2) infinite' }} />
      <g stroke={ART.brass.hi} strokeWidth="0.7" fill="none"><polyline points={pts([[20, 60], [24, 57], [28, 60]])} /><polyline points={pts([[36, 60], [40, 57], [44, 60]])} /></g>
      <Rivets x={19} y={74} w={26} n={5} c="brass" />
      <Seams x={14} y={72} w={36} h={16} cols={3} op={0.35} />
      <Greeble x={14} y={76} w={36} h={12} seed={680} n={5} c="stone" />
      <Grime x={12} y={56} w={40} h={32} seed={681} n={5} op={0.26} /><Scuff x={14} y={58} w={36} h={28} seed={682} n={6} c="brass" />
    </svg>
  );
}

function PropCavChassis({ variant }) {
  const S = ART.signal;
  const v = variant || 'plinth';
  const wreck = v === 'wreck' || v === 'stripped';
  return (
    <svg viewBox="0 0 96 96" width="96" height="96" style={PROP_STYLE}>
      <PropShadow cx={48} cy={88} rx={44} ry={5} />
      <Bev t="polygon" points={pts(ngon(48, 78, 42, 13, 8, 0.39))} c="dark" o={1.1} />
      <polygon points={pts(ngon(48, 77, 32, 9.4, 8, 0.39))} fill={ART.steel.shade} />
      <polygon points={pts(ngon(48, 76, 26, 7.4, 8, 0.39))} fill={ART.dark.base} />
      {[0, 1, 2, 3].map((i) => <rect key={i} x={14 + i * 20} y="86" width="8" height="2.4" fill={i % 2 ? ART.hull.hi : ART.amber.base} />)}
      <polygon points={pts(ngon(48, 76, 22, 6, 8, 0.39))} fill="none" stroke={S.amber} strokeWidth="0.8" opacity={v === 'ally' ? 0.2 : 0.8} style={{ animation:'ship-engine 3s ease-in-out infinite' }} />
      <Cable d="M8 80 Q-2 70 4 56" c={ART.amber.shade} w={1.8} /><Cable d="M88 80 Q98 70 92 56" c={ART.amber.shade} w={1.8} />
      {v === 'plinth' && (
        <g>
          <Slab x={14} y={56} w={10} h={22} k={2} c="steel" /><Slab x={72} y={56} w={10} h={22} k={2} c="steel" />
          <rect x="16" y="60" width="6" height="2.4" fill={S.amber} /><rect x="74" y="60" width="6" height="2.4" fill={S.amber} />
          <Lights x={36} y={80} n={5} gap={6} seed={690} />
        </g>
      )}
      {v === 'dormant' && (
        <g>
          <Bev t="polygon" points={pts([[28, 70], [32, 48], [64, 48], [68, 70]])} c="deepglass" o={1} />
          <Slab x={36} y={30} w={24} h={20} k={3} c="steel" />
          <rect x="40" y="36" width="16" height="5" fill={ART.screen.off} />
          <rect x="46" y="37.4" width="4" height="2.2" fill={S.amber} style={{ animation:'ship-blink 3s steps(2) infinite' }} />
          <Slab x={20} y={50} w={10} h={22} k={2} c="steel" /><Slab x={66} y={50} w={10} h={22} k={2} c="steel" />
          <Hazard x={32} y={58} w={32} h={4} />
          <Vent x={38} y={52} w={20} h={5} n={2} />
        </g>
      )}
      {wreck && (
        <g>
          <Bev t="polygon" points={pts([[24, 70], [36, 54], [66, 60], [72, 72], [44, 76]])} c="deepglass" o={1} />
          <polygon points={pts([[38, 60], [60, 64], [56, 72], [40, 70]])} fill={ART.ink} />
          {v === 'wreck' && (
            <g>
              <polygon points={pts(ngon(49, 66, 6, 5, 6))} fill={S.cyan} style={{ animation:'ship-engine 2.6s ease-in-out infinite' }} />
              <Glow cx={49} cy={66} r={13} c={S.cyanHi} opacity={0.5} style={{ animation:'ship-engine 2.6s ease-in-out infinite' }} />
            </g>
          )}
          <Slab x={64} y={50} w={18} h={12} k={2.4} c="steel" transform="rotate(24 72 56)" />
          <rect x="70" y="53" width="5" height="3" fill={ART.screen.off} transform="rotate(24 72 56)" />
          <Slab x={8} y={64} w={20} h={7} k={1.6} c="steel" transform="rotate(-18 18 68)" />
          <Bev t="polygon" points={pts(ngon(78, 80, 5, 3, 6))} c="brass" o={0.6} /><Bev t="polygon" points={pts(ngon(20, 82, 4, 2.6, 6))} c="brass" o={0.6} />
          <path d="M40 72 L44 56 L50 62" fill="none" stroke={ART.ink} strokeWidth="1.2" opacity="0.8" />
          <circle cx="30" cy="56" r="1" fill={S.orange} style={{ animation:'ship-spark 1.8s steps(3) infinite' }} />
        </g>
      )}
      <Seams x={20} y={68} w={56} h={18} cols={4} op={0.3} />
      <Rivets x={20} y={84} w={56} n={9} c="dark" />
      <Grime x={14} y={46} w={68} h={40} seed={691} n={8} op={wreck ? 0.4 : 0.26} /><Scuff x={16} y={48} w={64} h={36} seed={692} n={9} c="steel" />
    </svg>
  );
}

function PropCavShutter({ variant }) {
  const S = ART.signal;
  const open = variant === 'open';
  return (
    <svg viewBox="0 0 64 128" width="64" height="128" style={PROP_STYLE}>
      <Slab x={2} y={6} w={14} h={116} k={3} c="dark" />
      <Slab x={5} y={14} w={8} h={20} k={1.6} c="steel" />
      <rect x="7" y="18" width="4" height="3" fill={open ? S.green : S.red} style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
      <rect x="7" y="25" width="4" height="3" fill={open ? S.green : S.amber} />
      <Bev t="polygon" points={pts([[16, 6], [62, 6], [62, 122], [16, 122]])} c="dark" o={1} />
      <rect x="22" y="14" width="38" height="104" fill={ART.ink} />
      {open ? (
        <g>
          <Slab x={22} y={14} w={38} h={14} k={1.8} c="steel" />
          <Hazard x={22} y={28} w={38} h={3} />
          <polygon points={pts([[30, 60], [46, 66], [30, 72]])} fill={S.green} opacity="0.85" />
          <polygon points={pts([[40, 80], [54, 86], [40, 92]])} fill={S.green} opacity="0.55" />
          {[44, 56, 68, 80, 92, 104].map((y, i) => <rect key={y} x="58" y={y} width="2" height="4" fill={S.green} style={{ animation:`ship-blink ${1.6 + i * 0.3}s steps(2) infinite` }} />)}
          <Glow cx={44} cy={66} rx={18} ry={26} c={S.greenHi} opacity={0.3} style={{ animation:'ship-engine 3s ease-in-out infinite' }} />
        </g>
      ) : (
        <g>
          {[14, 34, 54, 74, 94].map((y, i) => (
            <g key={y}>
              <Slab x={22} y={y} w={38} h={19} k={1.6} c={i % 2 ? 'steel' : 'dark'} />
              <Seams x={22} y={y} w={38} h={19} cols={2} op={0.4} />
            </g>
          ))}
          <Hazard x={22} y={108} w={38} h={10} />
          <rect x="26" y="52" width="30" height="9" fill={ART.red.base} />
          <rect x="26" y="52" width="30" height="1.4" fill={ART.red.hi} />
          <text x="41" y="58.8" textAnchor="middle" fontSize="3" fill={ART.paper.hi} fontWeight="700" {...PT}>ZORA IV SECTOR</text>
          <text x="41" y="46" textAnchor="middle" fontSize="2.2" fill={S.amberHi} {...PT}>LOCKOUT ACTIVE</text>
          <rect x="24" y="64" width="34" height="2" fill={S.red} opacity="0.8" style={{ animation:'ship-blink 1.6s steps(2) infinite' }} />
        </g>
      )}
      <Rivets x={18} y={9} w={42} n={8} c="steel" />
      <Vent x={4} y={40} w={10} h={12} n={4} />
      <Grime x={4} y={10} w={56} h={110} seed={700} n={9} op={0.28} /><Scuff x={6} y={14} w={52} h={104} seed={701} n={10} c="steel" />
    </svg>
  );
}
// ===== CAVERN PROPS END =====

// ===== PENTHOUSE DESIGN OVERLAY BEGIN =====
function PenthouseDesignOverlay({ inventory, flags, onInstall, onRemove, onClose }) {
  const [sel, setSel] = React.useState(0);
  React.useEffect(() => {
    const h = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [onClose]);
  const has = (p) => inventory.some((i) => i.id === p.itemId);
  const state = (p) => (flags[p.flag] ? 'installed' : has(p) ? 'cargo' : 'unowned');
  const installed = PENTHOUSE_PACKAGES.filter((p) => flags[p.flag]);
  const income = installed.reduce((s, p) => s + p.income, 0);
  const decay = installed.reduce((s, p) => s + p.heatDecay, 0);
  const p = PENTHOUSE_PACKAGES[sel];
  const st = state(p);
  const stColor = { installed: '#4CAF50', cargo: '#E8C97A', unowned: '#666' };
  const stLabel = { installed: 'INSTALLED', cargo: 'IN CARGO, READY TO INSTALL', unowned: 'NOT OWNED' };
  return (
    <div style={{ position: 'absolute', inset: 0, background: 'rgba(4,3,0,0.93)', zIndex: 29, display: 'flex', flexDirection: 'column', padding: '22px', fontFamily: 'monospace' }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '12px' }}>
        <div>
          <div style={{ color: '#C8A000', fontSize: '18px', fontWeight: 'bold', letterSpacing: '0.12em' }}>PENTHOUSE INTERIOR DESIGN</div>
          <div style={{ color: '#8A7A3A', fontSize: '12px', marginTop: '4px' }}>{installed.length} of {PENTHOUSE_PACKAGES.length} packages installed. Bonus income {income} cr and extra heat decay {decay} per Advance Time.</div>
        </div>
        <div onClick={onClose} style={{ alignSelf: 'flex-start', padding: '6px 18px', border: '1px solid #444', color: '#999', cursor: 'pointer', borderRadius: '4px', fontSize: '12px' }}>[ESC] Close</div>
      </div>
      <div style={{ display: 'flex', gap: '16px', flex: 1, overflow: 'hidden' }}>
        <div style={{ width: '46%', overflowY: 'auto', display: 'flex', flexDirection: 'column', gap: '4px' }}>
          {PENTHOUSE_PACKAGES.map((q, i) => {
            const s = state(q);
            return (
              <div key={q.id} onClick={() => setSel(i)} style={{ padding: '8px 12px', border: `1px solid ${sel === i ? '#C8A000' : '#2A2410'}`, background: sel === i ? '#1E1A08' : 'transparent', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', borderRadius: '4px' }}>
                <span style={{ color: '#DDD' }}>{q.name}</span>
                <span style={{ color: stColor[s], fontSize: '11px' }}>{s === 'installed' ? 'INSTALLED' : s === 'cargo' ? 'READY' : 'LOCKED'}</span>
              </div>
            );
          })}
        </div>
        <div style={{ flex: 1, borderLeft: '1px solid #2A2410', paddingLeft: '16px', overflowY: 'auto' }}>
          <div style={{ color: '#E8C97A', fontSize: '16px', marginBottom: '6px' }}>{p.name}</div>
          <div style={{ color: stColor[st], fontSize: '11px', marginBottom: '10px' }}>{stLabel[st]}</div>
          <div style={{ color: '#BBB', fontSize: '12px', lineHeight: 1.5, marginBottom: '10px' }}>{p.blurb}</div>
          <div style={{ color: '#8FD0C8', fontSize: '12px', lineHeight: 1.5, marginBottom: '10px' }}>Effect: {p.perk}</div>
          <div style={{ color: '#888', fontSize: '11px', marginBottom: '14px' }}>Sold by {p.where} for {p.price} cr.</div>
          {st === 'cargo' && <div onClick={() => onInstall(p)} style={{ display: 'inline-block', padding: '8px 22px', border: '1px solid #4CAF50', color: '#4CAF50', cursor: 'pointer', borderRadius: '4px', fontSize: '12px' }}>INSTALL PACKAGE</div>}
          {st === 'installed' && <div onClick={() => onRemove(p)} style={{ display: 'inline-block', padding: '8px 22px', border: '1px solid #A66', color: '#C88', cursor: 'pointer', borderRadius: '4px', fontSize: '12px' }}>REMOVE AND RETURN TO CARGO</div>}
          {st === 'unowned' && <div style={{ color: '#666', fontSize: '12px' }}>Buy this package from the merchant named above, then return here to install it.</div>}
        </div>
      </div>
    </div>
  );
}
// ===== PENTHOUSE DESIGN OVERLAY END =====

function SyndicateManagementOverlay({ onClose, roster, setRoster, contracts, activeContracts, setActiveContracts, heat, setHeat, territories, setTerritories, credits, setCredits, incomeBonus = 0, heatDecayBonus = 0 }) {
  const [panel, setPanel] = React.useState('contracts');
  const [log, setLog] = React.useState([]);
  const addLog = (msg) => setLog(l => [`> ${msg}`, ...l].slice(0, 20));
  const passiveIncome = territories.length * 120 + incomeBonus;
  const availableAgents = roster.filter(a => a.status === 'available');

  const advanceTime = () => {
    const resolved = [];
    const updated = activeContracts.map(ac => {
      const t = ac.turnsRemaining - 1;
      if (t <= 0) { resolved.push(ac); return null; }
      return { ...ac, turnsRemaining: t };
    }).filter(Boolean);
    setActiveContracts(updated);
    resolved.forEach(ac => {
      const roll = Math.random();
      if (roll > 0.3) {
        setCredits(c => c + ac.reward);
        addLog(`${ac.title}: SUCCESS. +${ac.reward} credits.`);
      } else {
        setRoster(r => r.map(a => ac.agents.includes(a.id) ? { ...a, status: 'injured' } : a));
        addLog(`${ac.title}: FAILED. Agents injured.`);
      }
      setHeat(h => Math.min(100, h + ac.heatGenerated));
    });
    setRoster(r => r.map(a => a.status === 'injured' && Math.random() > 0.5 ? { ...a, status: 'available' } : a));
    setCredits(c => c + passiveIncome);
    setHeat(h => Math.max(0, h - 2 - heatDecayBonus));
    addLog(`Passive income: +${passiveIncome} credits. Heat: ${Math.max(0, heat - 2 - heatDecayBonus + resolved.reduce((s, ac) => s + ac.heatGenerated, 0))}.`);

    // Dynamic Heat Event pipeline
    const currentHeat = heat;
    if (currentHeat >= 100) {
      // Critical: Full CSF Raid
      setTerritories([]);
      setCredits(c => Math.max(0, c - 1200));
      setHeat(50);
      addLog('CRITICAL RAID: CSF executed a full district sweep. All territory seized. -1200 credits. Heat reset to 50. Rebuild from Shadow Town.');
    } else if (currentHeat >= 70 && Math.random() < 0.30) {
      // High heat: pick one of three events
      const pick = Math.floor(Math.random() * 3);
      if (pick === 0) {
        setHeat(h => Math.min(100, h + 8));
        addLog('HEAT EVENT: Courier Ambush. A supply run was intercepted. Heat +8. Consider cooling operations.');
      } else if (pick === 1) {
        const loss = 500 + (Math.random() * 400 | 0);
        setCredits(c => Math.max(0, c - loss));
        addLog(`HEAT EVENT: Warehouse Fire Bombing. One of your holdings torched. -${loss} credits.`);
      } else {
        const avail = roster.filter(a => a.status === 'available');
        if (avail.length === 0) {
          addLog('HEAT EVENT: CSF attempted an agent extraction but found no exposed operatives.');
        } else {
          const target = avail[Math.floor(Math.random() * avail.length)];
          addLog(`HEAT EVENT: Agent Extradition. ${target.name} is in CSF custody. Status: detained. Extract via slicing mission.`);
          setRoster(r => r.map(a => a.id === target.id ? { ...a, status: 'injured' } : a));
        }
      }
    } else if (currentHeat >= 40 && Math.random() < 0.18) {
      // Mid heat: Shakedown or Turf War
      if (Math.random() < 0.5) {
        const cut = Math.round(passiveIncome * 0.30);
        setCredits(c => Math.max(0, c - cut));
        addLog(`HEAT EVENT: CSF Customs Shakedown. Income routes disrupted this cycle. -${cut} credits.`);
      } else {
        setHeat(h => Math.min(100, h + 6));
        addLog('HEAT EVENT: Turf War. Rival faction muscle moved into one of your districts. Heat +6. Hold the line.');
      }
    }
  };

  const dispatch = (contract) => {
    if (availableAgents.length === 0) { addLog('No available agents.'); return; }
    const agent = availableAgents[0];
    setRoster(r => r.map(a => a.id === agent.id ? { ...a, status: 'active' } : a));
    setActiveContracts(ac => [...ac, { ...contract, agents: [agent.id], turnsRemaining: contract.duration }]);
    setHeat(h => Math.min(100, h + Math.floor(contract.heatGenerated * 0.3)));
    addLog(`${agent.name} dispatched: ${contract.title}.`);
  };

  const heatColor = heat < 40 ? '#4A9FFF' : heat < 70 ? '#E8A030' : '#C03030';

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.94)', zIndex:200, display:'flex', flexDirection:'column', fontFamily:'monospace', color:'#DDD', padding:'1.5rem' }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'1rem' }}>
        <div style={{ color:'#C8A000', fontSize:'1.1rem', letterSpacing:2 }}>WAR TABLE — SYNDICATE OPERATIONS</div>
        <div style={{ display:'flex', gap:'0.8rem', alignItems:'center' }}>
          <div style={{ fontSize:'0.8rem', color: heatColor }}>HEAT: {heat}/100</div>
          <div style={{ fontSize:'0.8rem', color:'#888' }}>INCOME: {passiveIncome}cr/turn</div>
          <button onClick={advanceTime} style={{ background:'#C8A000', color:'#000', border:'none', padding:'0.3rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Advance Time</button>
          <button onClick={onClose} style={{ background:'#333', color:'#EEE', border:'none', padding:'0.3rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Close</button>
        </div>
      </div>
      <div style={{ display:'flex', gap:'0.5rem', marginBottom:'1rem' }}>
        {['contracts','roster','log'].map(p => (
          <button key={p} onClick={() => setPanel(p)} style={{ background: panel===p ? '#C8A000' : '#222', color: panel===p ? '#000' : '#888', border:'1px solid #444', padding:'0.2rem 0.7rem', cursor:'pointer', fontSize:'0.8rem', textTransform:'uppercase' }}>{p}</button>
        ))}
      </div>
      <div style={{ flex:1, overflowY:'auto' }}>
        {panel === 'contracts' && (
          <div>
            <div style={{ color:'#888', fontSize:'0.75rem', marginBottom:'0.5rem' }}>AVAILABLE CONTRACTS</div>
            {contracts.map(c => (
              <div key={c.id} style={{ background:'#111', border:'1px solid #333', padding:'0.6rem', marginBottom:'0.4rem', display:'flex', justifyContent:'space-between', alignItems:'center' }}>
                <div>
                  <div style={{ color:'#EEE', fontSize:'0.85rem' }}>{c.title}</div>
                  <div style={{ color:'#666', fontSize:'0.75rem' }}>{c.contractType} · Difficulty {c.difficulty} · {c.duration} turns · +{c.heatGenerated} heat · {c.primaryClass}</div>
                </div>
                <div style={{ display:'flex', alignItems:'center', gap:'0.5rem' }}>
                  <div style={{ color:'#C8A000', fontSize:'0.85rem' }}>{c.reward}cr</div>
                  <button onClick={() => dispatch(c)} disabled={availableAgents.length === 0} style={{ background: availableAgents.length > 0 ? '#C8A000' : '#333', color: availableAgents.length > 0 ? '#000' : '#666', border:'none', padding:'0.2rem 0.6rem', cursor: availableAgents.length > 0 ? 'pointer' : 'default', fontSize:'0.75rem' }}>Dispatch</button>
                </div>
              </div>
            ))}
            {activeContracts.length > 0 && (
              <div style={{ marginTop:'1rem' }}>
                <div style={{ color:'#888', fontSize:'0.75rem', marginBottom:'0.5rem' }}>ACTIVE OPERATIONS</div>
                {activeContracts.map(ac => (
                  <div key={ac.id + ac.turnsRemaining} style={{ background:'#0A1008', border:'1px solid #2A4020', padding:'0.5rem', marginBottom:'0.3rem', display:'flex', justifyContent:'space-between' }}>
                    <div style={{ color:'#CCC', fontSize:'0.8rem' }}>{ac.title}</div>
                    <div style={{ color:'#4A9FFF', fontSize:'0.8rem' }}>{ac.turnsRemaining} turn{ac.turnsRemaining !== 1 ? 's' : ''} remaining</div>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
        {panel === 'roster' && (
          <div>
            <div style={{ color:'#888', fontSize:'0.75rem', marginBottom:'0.5rem' }}>AGENT ROSTER</div>
            {roster.map(a => (
              <div key={a.id} style={{ background:'#111', border:'1px solid #333', padding:'0.6rem', marginBottom:'0.4rem', display:'flex', justifyContent:'space-between', alignItems:'center' }}>
                <div>
                  <div style={{ color:'#EEE', fontSize:'0.85rem' }}>{a.name} <span style={{ color:'#888', fontSize:'0.75rem' }}>— {a.agentClass}</span></div>
                  <div style={{ color:'#666', fontSize:'0.75rem' }}>Skill {a.skill} · Loyalty {a.loyalty} · {a.traits.join(', ')}</div>
                </div>
                <div style={{ color: a.status === 'available' ? '#4A9FFF' : a.status === 'active' ? '#C8A000' : '#C03030', fontSize:'0.8rem', textTransform:'uppercase' }}>{a.status}</div>
              </div>
            ))}
          </div>
        )}
        {panel === 'log' && (
          <div>
            <div style={{ color:'#888', fontSize:'0.75rem', marginBottom:'0.5rem' }}>OPERATIONS LOG</div>
            {log.length === 0 && <div style={{ color:'#444', fontSize:'0.8rem' }}>No operations yet. Advance time to process contracts.</div>}
            {log.map((l, i) => <div key={i} style={{ color:'#AAA', fontSize:'0.8rem', marginBottom:'0.2rem' }}>{l}</div>)}
          </div>
        )}
      </div>
      <div style={{ marginTop:'0.5rem', fontSize:'0.75rem', color:'#555' }}>
        Territories: {territories.length > 0 ? territories.join(', ') : 'none'} · Available agents: {availableAgents.length}
      </div>
    </div>
  );
}

// ── Sabacc Card Game ──────────────────────────────────────────────────────────
const SABACC_DECK_DEF = (() => {
  const suits = ['Sabers','Staves','Flasks','Coins'];
  const cards = [];
  suits.forEach(s => {
    for (let v = 1; v <= 11; v++) {
      const names = { 11:'Ace', 10:'Master', 9:'Mistress', 8:'Commander' };
      cards.push({ id: `${s}_${v}`, name: names[v] || String(v), value: v, suit: s });
    }
  });
  [['Idiot',0],['Fair One',-2],['Demise',-8],['Balance',-11],['Endurance',-13],['Queen',-14],['Eclipse',-15],['The Star',-17]].forEach(([n,v]) => {
    cards.push({ id:`spec_${n}1`, name:n, value:v, suit:null });
    cards.push({ id:`spec_${n}2`, name:n, value:v, suit:null });
  });
  return cards;
})();

function shuffleDeck() { return [...SABACC_DECK_DEF].sort(() => Math.random() - 0.5); }

function evaluateHand(hand) {
  const total = hand.reduce((s,c) => s + c.value, 0);
  const vals = hand.map(c => c.value).sort((a,b) => a-b);
  if (vals.length === 3 && vals[0]===0 && vals[1]===2 && vals[2]===3) return { rank:'IDIOTS_ARRAY', label:"Idiot's Array", total:0 };
  if (total === 24 || total === -24) return { rank:'PURE_SABACC', label:'Pure Sabacc', total };
  if (total > 24 || total < -24) return { rank:'BOMBOUT', label:'Bomb-Out', total };
  return { rank:'STANDARD', label:`Sum: ${total}`, total };
}

function SabaccOverlay({ onSuccess, onFailure }) {
  const [deck, setDeck] = React.useState(() => shuffleDeck());
  const [playerHand, setPlayerHand] = React.useState([]);
  const [npcHand, setNpcHand] = React.useState([]);
  const [heldCards, setHeldCards] = React.useState(new Set());
  const [handPot, setHandPot] = React.useState(0);
  const [sabaccPot, setSabaccPot] = React.useState(0);
  const [phase, setPhase] = React.useState('ante');
  const [message, setMessage] = React.useState('Place your ante to begin the match.');
  const [result, setResult] = React.useState(null);

  const ante = 500;

  const deal = (d) => {
    const p = [d.pop(), d.pop()];
    const n = [d.pop(), d.pop()];
    setPlayerHand(p); setNpcHand(n); setDeck([...d]);
    setHandPot(ante * 1.4 | 0); setSabaccPot(ante * 0.6 | 0);
    setPhase('betting'); setMessage('Cards dealt. Lock a card to protect it from the Sabacc Shift, then Shift or Reveal.');
  };

  const handleAnte = () => {
    const d = shuffleDeck();
    deal(d);
  };

  const toggleHold = (idx) => {
    setHeldCards(prev => { const s = new Set(prev); s.has(idx) ? s.delete(idx) : s.add(idx); return s; });
  };

  const doShift = () => {
    const d1 = Math.ceil(Math.random()*6), d2 = Math.ceil(Math.random()*6);
    const shifted = d1 === d2;
    if (shifted) {
      const d = [...deck];
      const newHand = playerHand.map((c, i) => heldCards.has(i) ? c : d.pop());
      const newNpc = npcHand.map((c, i) => d.pop());
      setPlayerHand(newHand); setNpcHand(newNpc); setDeck(d);
      setMessage(`⚡ Sabacc Shift! Dice: [${d1}][${d2}]. Unlocked cards reshuffled.`);
    } else {
      setMessage(`Dice: [${d1}][${d2}]. No shift. Hands stable.`);
    }
  };

  const cheat = () => {
    const caught = Math.random() < 0.25;
    if (caught) {
      setPhase('done'); setResult('loss');
      setMessage('🚨 Caught cheating! The match is forfeit and the table erupts. Heat +10.');
      return;
    }
    const d = [...deck];
    const best = d.splice(Math.floor(Math.random()*d.length), 1)[0];
    const worst = playerHand.reduce((wi, c, i) => Math.abs(c.value) > Math.abs(playerHand[wi].value) ? i : wi, 0);
    const newHand = [...playerHand]; newHand[worst] = best;
    setPlayerHand(newHand); setDeck(d);
    setMessage('You slipped a card from your sleeve. The table does not notice.');
  };

  const reveal = () => {
    const pe = evaluateHand(playerHand);
    const ne = evaluateHand(npcHand);
    if (pe.rank === 'BOMBOUT' && ne.rank === 'BOMBOUT') {
      setSabaccPot(s => s + handPot); setHandPot(0);
      setMessage(`Both bombed out! Hand pot rolls into Sabacc pot (${sabaccPot + handPot} cr). All hands bust.`);
      setPhase('done'); setResult('draw');
    } else if (pe.rank === 'BOMBOUT') {
      setMessage(`You bombed out (${pe.total}). Vex takes the hand pot.`);
      setPhase('done'); setResult('loss');
    } else if (ne.rank === 'BOMBOUT') {
      setMessage(`Vex bombed out! You win the hand pot (+${handPot} cr).`);
      setPhase('done'); setResult('win');
    } else {
      const rankOrder = { IDIOTS_ARRAY:3, PURE_SABACC:2, STANDARD:1 };
      const pr = rankOrder[pe.rank], nr = rankOrder[ne.rank];
      if (pr > nr) {
        const prize = handPot + (pe.rank !== 'STANDARD' ? sabaccPot : 0);
        setMessage(`${pe.label} beats ${ne.label}! You win ${prize} cr.`);
        setPhase('done'); setResult('win');
      } else if (nr > pr) {
        setMessage(`${ne.label} beats ${pe.label}. Vex takes the pot.`);
        setPhase('done'); setResult('loss');
      } else {
        const pd = Math.min(Math.abs(pe.total - 24), Math.abs(pe.total + 24));
        const nd = Math.min(Math.abs(ne.total - 24), Math.abs(ne.total + 24));
        if (pd < nd) { setMessage(`Closer to 24: ${pe.total} vs ${ne.total}. You win the hand pot (+${handPot} cr).`); setResult('win'); }
        else if (nd < pd) { setMessage(`Vex is closer: ${ne.total} vs ${pe.total}. You lose.`); setResult('loss'); }
        else { setSabaccPot(s => s + handPot); setHandPot(0); setMessage(`Tie! Hand pot rolls. Sabacc pot grows.`); setResult('draw'); }
        setPhase('done');
      }
    }
  };

  const cardStyle = (locked) => ({ display:'inline-block', background: locked ? '#1A3A1A' : '#111', border:`1px solid ${locked ? '#4A8' : '#444'}`, borderRadius:4, padding:'0.25rem 0.4rem', margin:'0.2rem', fontSize:'0.8rem', cursor: phase==='betting'?'pointer':'default', minWidth:60, textAlign:'center' });

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.96)', zIndex:200, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', fontFamily:'monospace', color:'#DDD', padding:'2rem' }}>
      <div style={{ color:'#C8A000', fontSize:'1.1rem', letterSpacing:2, marginBottom:'0.5rem' }}>HIGH-STAKES SABACC</div>
      <div style={{ fontSize:'0.75rem', color:'#888', marginBottom:'1rem' }}>Hand Pot: {handPot} cr &nbsp;|&nbsp; Sabacc Pot: {sabaccPot} cr</div>
      {phase === 'ante' && <button onClick={handleAnte} style={{ background:'#C8A000', color:'#000', border:'none', padding:'0.5rem 1.5rem', cursor:'pointer', fontSize:'0.9rem' }}>Ante Up ({ante} cr)</button>}
      {phase !== 'ante' && phase !== 'done' && (
        <>
          <div style={{ marginBottom:'0.5rem', fontSize:'0.75rem', color:'#888' }}>YOUR HAND (click to lock/unlock)</div>
          <div style={{ marginBottom:'1rem' }}>
            {playerHand.map((c,i) => (
              <span key={c.id+i} style={cardStyle(heldCards.has(i))} onClick={() => toggleHold(i)}>
                {c.name}{c.suit ? ` / ${c.suit[0]}` : ''} ({c.value > 0 ? '+' : ''}{c.value}) {heldCards.has(i) ? '🔒' : ''}
              </span>
            ))}
          </div>
          <div style={{ display:'flex', gap:'0.5rem', marginBottom:'1rem' }}>
            <button onClick={doShift} style={{ background:'#333', color:'#EEE', border:'1px solid #555', padding:'0.3rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Roll Shift Dice</button>
            <button onClick={cheat} style={{ background:'#2A1A00', color:'#C8A000', border:'1px solid #604800', padding:'0.3rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Sleeve Swap (Risk)</button>
            <button onClick={reveal} style={{ background:'#C8A000', color:'#000', border:'none', padding:'0.3rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Reveal Hands</button>
          </div>
        </>
      )}
      {phase === 'done' && (
        <div style={{ marginBottom:'1rem' }}>
          <div style={{ fontSize:'0.8rem', color:'#888', marginBottom:'0.4rem' }}>OPPONENT HAND:</div>
          <div>{npcHand.map((c,i) => <span key={c.id+i} style={cardStyle(false)}>{c.name}{c.suit ? ` / ${c.suit[0]}` : ''} ({c.value > 0 ? '+' : ''}{c.value})</span>)}</div>
        </div>
      )}
      <div style={{ maxWidth:420, textAlign:'center', color:'#CCC', fontSize:'0.85rem', marginBottom:'1rem', lineHeight:1.6 }}>{message}</div>
      {phase === 'done' && (
        <div style={{ display:'flex', gap:'0.8rem' }}>
          {result === 'win' && <button onClick={onSuccess} style={{ background:'#C8A000', color:'#000', border:'none', padding:'0.4rem 1rem', cursor:'pointer', fontSize:'0.85rem' }}>Collect Winnings</button>}
          {result !== 'win' && <button onClick={onFailure} style={{ background:'#333', color:'#EEE', border:'none', padding:'0.4rem 1rem', cursor:'pointer', fontSize:'0.85rem' }}>Leave Table</button>}
          {result === 'win' && <button onClick={onFailure} style={{ background:'#222', color:'#888', border:'none', padding:'0.4rem 0.8rem', cursor:'pointer', fontSize:'0.75rem' }}>Decline winnings</button>}
        </div>
      )}
    </div>
  );
}

// ── Contraband Market ─────────────────────────────────────────────────────────
const CONTRABAND_GOODS = [
  { id:'glitterstim', name:'Glitterstim Spice', base:1200, vol:0.35 },
  { id:'mil_blasters', name:'Military Blasters', base:2500, vol:0.20 },
  { id:'slicer_chips', name:'Slicer Chips', base:800, vol:0.15 },
  { id:'bacta_vials', name:'Bacta Vials', base:450, vol:0.10 },
  { id:'cybernetics', name:'Black-Market Cybernetics', base:3200, vol:0.25 },
];

function ContrabandMarketOverlay({ onSuccess, onFailure, credits, setCredits, heat, setHeat }) {
  const [prices, setPrices] = React.useState(() => Object.fromEntries(CONTRABAND_GOODS.map(g => [g.id, g.base])));
  const [prev, setPrev] = React.useState(() => Object.fromEntries(CONTRABAND_GOODS.map(g => [g.id, g.base])));
  const [portfolio, setPortfolio] = React.useState(() => Object.fromEntries(CONTRABAND_GOODS.map(g => [g.id, 0])));
  const [pnl, setPnl] = React.useState(0);
  const [msg, setMsg] = React.useState('');

  const tick = () => {
    setPrev({ ...prices });
    setPrices(p => {
      const n = {};
      CONTRABAND_GOODS.forEach(g => {
        const chg = p[g.id] * (Math.random() * g.vol * 2 - g.vol);
        n[g.id] = Math.round(Math.max(g.base * 0.2, Math.min(g.base * 4, p[g.id] + chg)));
      });
      return n;
    });
    setMsg('Market shifted. Prices updated.');
  };

  const buy = (id) => {
    const price = prices[id];
    if (credits < price) { setMsg('Insufficient credits.'); return; }
    if (Object.values(portfolio).reduce((s,v) => s+v, 0) >= 20) { setMsg('Inventory full. Max 20 units.'); return; }
    setCredits(c => c - price);
    setPortfolio(p => ({ ...p, [id]: p[id]+1 }));
    setPnl(n => n - price);
    setMsg(`Bought 1 unit. Paid ${price} cr.`);
  };

  const sell = (id) => {
    if (portfolio[id] <= 0) { setMsg('None to sell.'); return; }
    const price = prices[id];
    setCredits(c => c + price);
    setPortfolio(p => ({ ...p, [id]: p[id]-1 }));
    setPnl(n => n + price);
    if (heat >= 40) setHeat(h => Math.min(100, h + 2));
    setMsg(`Sold 1 unit for ${price} cr.${heat >= 40 ? ' Heat +2 (CSF watching).' : ''}`);
  };

  const dump = (id) => {
    if (portfolio[id] < 5) { setMsg('Need 5 units to dump.'); return; }
    const price = Math.max(prices[id] - 20, 50);
    const total = price * 5;
    setCredits(c => c + total);
    setPortfolio(p => ({ ...p, [id]: p[id]-5 }));
    setPnl(n => n + total);
    setPrices(p => ({ ...p, [id]: Math.max(50, p[id] - 20) }));
    setMsg(`Dumped 5 units. Crashed price by 20. Received ${total} cr.`);
  };

  const trendArrow = (id) => {
    if (prices[id] > prev[id]) return '▲';
    if (prices[id] < prev[id]) return '▼';
    return '▬';
  };

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.96)', zIndex:200, display:'flex', flexDirection:'column', fontFamily:'monospace', color:'#DDD', padding:'1.5rem' }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:'1rem' }}>
        <div style={{ color:'#9B59B6', fontSize:'1.0rem', letterSpacing:2 }}>CONTRABAND MARKET INDEX</div>
        <div style={{ display:'flex', gap:'1rem', alignItems:'center', fontSize:'0.8rem' }}>
          <span style={{ color: pnl >= 0 ? '#4A8' : '#C33' }}>P&L: {pnl >= 0 ? '+' : ''}{pnl} cr</span>
          <span style={{ color:'#888' }}>Holdings: {Object.values(portfolio).reduce((s,v) => s+v,0)}/20</span>
          <button onClick={tick} style={{ background:'#9B59B6', color:'#FFF', border:'none', padding:'0.3rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Market Tick</button>
          <button onClick={onSuccess} style={{ background:'#333', color:'#EEE', border:'none', padding:'0.3rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Exit</button>
        </div>
      </div>
      <div style={{ overflowY:'auto', flex:1 }}>
        {CONTRABAND_GOODS.map(g => (
          <div key={g.id} style={{ background:'#111', border:'1px solid #333', padding:'0.6rem', marginBottom:'0.4rem', display:'flex', alignItems:'center', gap:'1rem' }}>
            <div style={{ flex:1 }}>
              <div style={{ fontSize:'0.85rem', color:'#EEE' }}>{g.name}</div>
              <div style={{ fontSize:'0.75rem', color:'#666' }}>Base: {g.base} cr · Volatile: {(g.vol*100|0)}%</div>
            </div>
            <div style={{ color: prices[g.id] > prev[g.id] ? '#4A8' : prices[g.id] < prev[g.id] ? '#C33' : '#888', minWidth:100, textAlign:'right' }}>
              {trendArrow(g.id)} {prices[g.id]} cr
            </div>
            <div style={{ fontSize:'0.8rem', color:'#888' }}>Held: {portfolio[g.id]}</div>
            <button onClick={() => buy(g.id)} style={{ background:'#1A3A1A', color:'#4A8', border:'1px solid #2A5A2A', padding:'0.2rem 0.6rem', cursor:'pointer', fontSize:'0.75rem' }}>Buy</button>
            <button onClick={() => sell(g.id)} disabled={portfolio[g.id]===0} style={{ background: portfolio[g.id]>0?'#3A1A1A':'#1A1A1A', color: portfolio[g.id]>0?'#C33':'#555', border:'1px solid #5A2A2A', padding:'0.2rem 0.6rem', cursor: portfolio[g.id]>0?'pointer':'default', fontSize:'0.75rem' }}>Sell</button>
            <button onClick={() => dump(g.id)} disabled={portfolio[g.id]<5} style={{ background:'#2A1A00', color:'#C8A000', border:'1px solid #604800', padding:'0.2rem 0.5rem', cursor: portfolio[g.id]>=5?'pointer':'default', fontSize:'0.7rem' }}>Dump×5</button>
          </div>
        ))}
      </div>
      {msg && <div style={{ color:'#AAA', fontSize:'0.8rem', marginTop:'0.5rem', borderTop:'1px solid #333', paddingTop:'0.5rem' }}>{msg}</div>}
    </div>
  );
}

// ── Interrogation Matrix ──────────────────────────────────────────────────────
const SUSPECT_TRAITS = ['Greedy','Fanatical','Cowardly','Deceptive','Loyal'];

function InterrogationMatrixOverlay({ onSuccess, onFailure }) {
  const [target] = React.useState(() => ({
    name: ['Vex Informant','CSF Double Agent','Exchange Courier','Black Sun Runner'][Math.floor(Math.random()*4)],
    trait: SUSPECT_TRAITS[Math.floor(Math.random()*SUSPECT_TRAITS.length)],
    fear: 0,
    resistance: 80 + Math.floor(Math.random()*20),
    stress: 0,
  }));
  const [state, setState] = React.useState({ fear:0, resistance: target.resistance, stress:0 });
  const [round, setRound] = React.useState(1);
  const [msg, setMsg] = React.useState(`${target.name} sits across from you. Trait detected on profile: ${target.trait}. 8 rounds before they lawyer up.`);
  const [done, setDone] = React.useState(false);
  const [success, setSuccess] = React.useState(false);
  const [bribeCount, setBribeCount] = React.useState(0);

  const MAX_ROUNDS = 8;

  const applyTactic = (tactic) => {
    if (done) return;
    let fearGain = 0, resLoss = 0, stressGain = 0;
    let narrative = '';

    switch(tactic) {
      case 'INTIMIDATE':
        if (target.trait === 'Cowardly') { fearGain=30+Math.random()*15|0; resLoss=15+Math.random()*10|0; stressGain=20; }
        else if (target.trait === 'Fanatical') { fearGain=5; resLoss=5; stressGain=35; }
        else { fearGain=15+Math.random()*10|0; resLoss=10; stressGain=20; }
        narrative = 'You lean forward. The temperature in the room drops.';
        break;
      case 'BRIBE':
        setBribeCount(b => b+1);
        if (target.trait === 'Greedy') { fearGain=5; resLoss=35+Math.random()*15|0; stressGain=5; }
        else { fearGain=0; resLoss=10; stressGain=5; }
        narrative = 'You slide credits across the table.';
        break;
      case 'LOGIC':
        if (target.trait === 'Fanatical' || target.trait === 'Deceptive') { fearGain=5; resLoss=30+Math.random()*15|0; stressGain=10; }
        else { fearGain=5; resLoss=15; stressGain=10; }
        narrative = 'You lay out the evidence. Each piece is irrefutable.';
        break;
      case 'SILENCE':
        stressGain=10+Math.random()*10|0; fearGain=5;
        narrative = 'You say nothing. The silence is its own kind of pressure.';
        break;
      case 'EVIDENCE':
        resLoss=20+Math.random()*10|0; fearGain=10; stressGain=5;
        narrative = 'You drop a data chip on the table. Transaction logs. Their name is on every one.';
        break;
    }

    const newState = {
      fear: Math.min(100, state.fear + fearGain),
      resistance: Math.max(0, state.resistance - resLoss),
      stress: Math.min(100, state.stress + stressGain),
    };
    setState(newState);

    let result = `${narrative} [Fear: ${newState.fear} / Resistance: ${newState.resistance} / Stress: ${newState.stress}]`;

    if (newState.stress >= 100) {
      setDone(true); setSuccess(false);
      setMsg('The target shut down completely under pressure. No intel recovered. Medical incident report filed.');
      return;
    }
    if (newState.resistance <= 0) {
      if (target.trait === 'Greedy' && bribeCount >= 2) {
        result += `\n\n${target.name} breaks and immediately offers to become a double agent inside the rival faction. You have a source.`;
      } else {
        result += `\n\n${target.name} breaks. Vault codes, contact names, drop schedules. All of it. You have what you came for.`;
      }
      setDone(true); setSuccess(true);
      setMsg(result);
      return;
    }
    if (round >= MAX_ROUNDS) {
      setDone(true); setSuccess(false);
      setMsg(result + '\n\nRound limit reached. The target lawyered up. Zero intel recovered.');
      return;
    }
    setRound(r => r+1);
    setMsg(result);
  };

  const barStyle = (val, color) => ({ width: `${val}%`, background: color, height:8, borderRadius:4, transition:'width 0.3s' });

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.96)', zIndex:200, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', fontFamily:'monospace', color:'#DDD', padding:'2rem' }}>
      <div style={{ color:'#E8A030', fontSize:'1.0rem', letterSpacing:2, marginBottom:'0.3rem' }}>INTERROGATION MATRIX</div>
      <div style={{ color:'#888', fontSize:'0.8rem', marginBottom:'1rem' }}>{target.name} · Round {round}/{MAX_ROUNDS} · Trait: {target.trait}</div>
      <div style={{ width:'100%', maxWidth:420, marginBottom:'1rem' }}>
        {[['Fear', state.fear,'#C03030'],['Resistance',state.resistance,'#4A7FBF'],['Stress',state.stress,'#9B59B6']].map(([label,val,color]) => (
          <div key={label} style={{ marginBottom:'0.4rem' }}>
            <div style={{ display:'flex', justifyContent:'space-between', fontSize:'0.75rem', marginBottom:2 }}><span>{label}</span><span>{val}/100</span></div>
            <div style={{ background:'#222', borderRadius:4, overflow:'hidden' }}><div style={barStyle(val, color)}/></div>
          </div>
        ))}
      </div>
      {!done && (
        <div style={{ display:'flex', gap:'0.5rem', flexWrap:'wrap', justifyContent:'center', marginBottom:'1rem' }}>
          {[['INTIMIDATE','Intimidate'],['BRIBE','Bribe'],['LOGIC','Logic'],['SILENCE','Silence'],['EVIDENCE','Present Evidence']].map(([t,l]) => (
            <button key={t} onClick={() => applyTactic(t)} style={{ background:'#1A1A1A', color:'#CCC', border:'1px solid #444', padding:'0.3rem 0.7rem', cursor:'pointer', fontSize:'0.8rem' }}>{l}</button>
          ))}
        </div>
      )}
      <div style={{ maxWidth:420, textAlign:'center', color:'#AAA', fontSize:'0.82rem', lineHeight:1.6, marginBottom:'1rem', whiteSpace:'pre-line' }}>{msg}</div>
      {done && (
        <button onClick={success ? onSuccess : onFailure} style={{ background: success ? '#C8A000' : '#333', color: success ? '#000' : '#EEE', border:'none', padding:'0.4rem 1.2rem', cursor:'pointer', fontSize:'0.85rem' }}>
          {success ? 'Extract the Intel' : 'End Session'}
        </button>
      )}
    </div>
  );
}

// ── Droid Arena ───────────────────────────────────────────────────────────────
const DROID_FRAMES = [
  { id:'bx', name:'BX Commando', hp:100, dmg:20, armor:3, spd:18, desc:'High speed and damage.' },
  { id:'hk', name:'HK Assassin', hp:90, dmg:35, armor:1, spd:12, desc:'Maximum firepower, light armor.' },
  { id:'ig', name:'IG War Droid', hp:140, dmg:15, armor:9, spd:6, desc:'Heavy armor, low speed.' },
];
const DROID_MODS = [
  { id:'cannon', name:'Repeating Cannon', slot:'WEAPON', dmg:20, armor:0, spd:-2, cost:600 },
  { id:'plating', name:'Durasteel Plating', slot:'ARMOR', dmg:0, armor:7, spd:-1, cost:500 },
  { id:'overclock', name:'Overclock Actuators', slot:'OVERCLOCK', dmg:5, armor:0, spd:4, cost:400 },
];

function DroidArenaOverlay({ onSuccess, onFailure, credits, setCredits }) {
  const [frame, setFrame] = React.useState(DROID_FRAMES[0]);
  const [installedMod, setInstalledMod] = React.useState(null);
  const [bet, setBet] = React.useState(200);
  const [sabotaged, setSabotaged] = React.useState(false);
  const [phase, setPhase] = React.useState('prep');
  const [log, setLog] = React.useState([]);
  const [result, setResult] = React.useState(null);

  const opponentFrames = DROID_FRAMES.filter(f => f.id !== frame.id);
  const opponent = opponentFrames[Math.floor(Math.random()*opponentFrames.length)];

  const playerStats = () => {
    const m = installedMod || { dmg:0, armor:0, spd:0 };
    return { hp:frame.hp, dmg:frame.dmg+m.dmg, armor:frame.armor+m.armor, spd:frame.spd+m.spd };
  };

  const simulateFight = () => {
    if (credits < bet) { setLog(['Insufficient credits for bet.']); return; }
    setCredits(c => c - bet);
    const ps = playerStats();
    const os = { hp: sabotaged ? opponent.hp * 0.75 | 0 : opponent.hp, dmg:opponent.dmg, armor:opponent.armor, spd:opponent.spd };
    let pHp = ps.hp, oHp = os.hp;
    const newLog = [];
    if (sabotaged) newLog.push('Sabotage successful. Opponent starts at 75% HP.');
    for (let t = 1; t <= 20 && pHp > 0 && oHp > 0; t++) {
      const pFirst = ps.spd >= os.spd;
      if (pFirst) {
        const d = Math.max(1, ps.dmg - os.armor + (Math.random()*6|0) - 3);
        oHp -= d;
        if (oHp <= 0) break;
        const d2 = Math.max(1, os.dmg - ps.armor + (Math.random()*6|0) - 3);
        pHp -= d2;
      } else {
        const d = Math.max(1, os.dmg - ps.armor + (Math.random()*6|0) - 3);
        pHp -= d;
        if (pHp <= 0) break;
        const d2 = Math.max(1, ps.dmg - os.armor + (Math.random()*6|0) - 3);
        oHp -= d2;
      }
      if (t <= 5 || pHp <= 0 || oHp <= 0) newLog.push(`Turn ${t}: Your droid ${pHp} HP / Opponent ${Math.max(0,oHp)} HP`);
    }
    const win = oHp <= 0;
    newLog.push(win ? `Your ${frame.name} wins! Payout: ${bet * 2} cr.` : `Opponent wins. ${frame.name} defeated.`);
    if (win) setCredits(c => c + bet * 2);
    setLog(newLog);
    setResult(win ? 'win' : 'loss');
    setPhase('done');
  };

  const installMod = (mod) => {
    if (credits < mod.cost) { setLog([`Need ${mod.cost} cr to install ${mod.name}.`]); return; }
    setCredits(c => c - mod.cost);
    setInstalledMod(mod);
    setLog([`Installed ${mod.name}. Cost: ${mod.cost} cr.`]);
  };

  const doSabotage = () => {
    const success = Math.random() > 0.4;
    if (success) { setSabotaged(true); setLog(['Sabotage planted. Opponent starts at 75% HP.']); }
    else { setLog(['Sabotage detected! CSF notified. Heat +5.']); }
  };

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.96)', zIndex:200, display:'flex', flexDirection:'column', fontFamily:'monospace', color:'#DDD', padding:'1.5rem' }}>
      <div style={{ color:'#C03030', fontSize:'1.0rem', letterSpacing:2, marginBottom:'1rem' }}>DROID PIT-FIGHT ARENA</div>
      {phase === 'prep' && (
        <>
          <div style={{ marginBottom:'0.8rem' }}>
            <div style={{ color:'#888', fontSize:'0.75rem', marginBottom:'0.3rem' }}>SELECT FRAME</div>
            <div style={{ display:'flex', gap:'0.5rem' }}>
              {DROID_FRAMES.map(f => (
                <button key={f.id} onClick={() => setFrame(f)} style={{ background: frame.id===f.id ? '#3A0000' : '#111', border:`1px solid ${frame.id===f.id ? '#C03030' : '#444'}`, color:'#DDD', padding:'0.4rem 0.6rem', cursor:'pointer', fontSize:'0.75rem' }}>
                  {f.name}<br/><span style={{ color:'#888', fontSize:'0.7rem' }}>{f.desc}</span>
                </button>
              ))}
            </div>
          </div>
          <div style={{ marginBottom:'0.8rem' }}>
            <div style={{ color:'#888', fontSize:'0.75rem', marginBottom:'0.3rem' }}>INSTALL MOD</div>
            <div style={{ display:'flex', gap:'0.5rem' }}>
              {DROID_MODS.map(m => (
                <button key={m.id} onClick={() => installMod(m)} disabled={installedMod?.id===m.id} style={{ background: installedMod?.id===m.id ? '#1A3A1A' : '#111', border:`1px solid ${installedMod?.id===m.id ? '#4A8' : '#444'}`, color:'#DDD', padding:'0.3rem 0.5rem', cursor:'pointer', fontSize:'0.75rem' }}>
                  {m.name} ({m.cost}cr)
                </button>
              ))}
            </div>
          </div>
          <div style={{ display:'flex', gap:'1rem', alignItems:'center', marginBottom:'0.8rem' }}>
            <div style={{ fontSize:'0.8rem', color:'#888' }}>Bet: <input type="number" value={bet} onChange={e=>setBet(+e.target.value)} min={100} max={2000} style={{ background:'#111', color:'#EEE', border:'1px solid #444', padding:'0.2rem 0.4rem', width:80, fontFamily:'monospace' }}/> cr</div>
            <button onClick={doSabotage} disabled={sabotaged} style={{ background:'#2A1A00', color:'#C8A000', border:'1px solid #604800', padding:'0.2rem 0.6rem', cursor:'pointer', fontSize:'0.8rem' }}>Sabotage Opponent (40% risk)</button>
            <button onClick={simulateFight} style={{ background:'#C03030', color:'#FFF', border:'none', padding:'0.3rem 1rem', cursor:'pointer', fontSize:'0.85rem' }}>FIGHT</button>
          </div>
        </>
      )}
      <div style={{ flex:1, overflowY:'auto', fontSize:'0.8rem', color:'#AAA', lineHeight:1.8 }}>
        {log.map((l,i) => <div key={i}>{l}</div>)}
      </div>
      {phase === 'done' && (
        <div style={{ display:'flex', gap:'0.8rem', marginTop:'0.8rem' }}>
          <button onClick={result==='win' ? onSuccess : onFailure} style={{ background: result==='win' ? '#C8A000' : '#333', color: result==='win' ? '#000' : '#EEE', border:'none', padding:'0.4rem 1rem', cursor:'pointer', fontSize:'0.85rem' }}>
            {result === 'win' ? 'Collect Winnings' : 'Exit Arena'}
          </button>
        </div>
      )}
    </div>
  );
}

// ── Arms Bench ────────────────────────────────────────────────────────────────
const ARMS_BLUEPRINTS = [
  { id:'pistol', name:'Heavy DL-44 Variant', dmg:35, heatRisk:15, cost:1200, itemId:'crafted_blaster_pistol' },
  { id:'rifle', name:'Czerka Disruptor', dmg:75, heatRisk:40, cost:3500, itemId:'crafted_disruptor_rifle' },
  { id:'repeater', name:'Mando Heavy Repeater', dmg:55, heatRisk:30, cost:2200, itemId:'crafted_repeater' },
];
const ARMS_MODS = [
  { id:'heatsink', name:'Heat Sink', heatBonus:-20, dmgBonus:0, valueMult:1.0, cost:300 },
  { id:'powercell', name:'Power Cell', heatBonus:10, dmgBonus:10, valueMult:1.3, cost:400 },
  { id:'grip', name:'Cortosis Grip', heatBonus:0, dmgBonus:5, valueMult:1.5, cost:500 },
];

function ArmsBenchOverlay({ onSuccess, onFailure, credits, setCredits, addItem }) {
  const [bp, setBp] = React.useState(null);
  const [mod, setMod] = React.useState(null);
  const [msg, setMsg] = React.useState('Select a blueprint and optional mod, then craft.');
  const [done, setDone] = React.useState(false);

  const craft = () => {
    if (!bp) { setMsg('Select a blueprint first.'); return; }
    const totalCost = bp.cost + (mod ? mod.cost : 0);
    if (credits < totalCost) { setMsg(`Need ${totalCost} cr to craft.`); return; }
    setCredits(c => c - totalCost);
    const totalHeat = bp.heatRisk + (mod ? mod.heatBonus : 0);
    if (totalHeat > 50 && Math.random() < (totalHeat - 30) / 100) {
      setMsg('💥 Thermal chamber ruptured! Components destroyed. Heat risk was too high.');
      setDone(true); return;
    }
    const finalDmg = bp.dmg + (mod ? mod.dmgBonus : 0);
    const resale = Math.round(totalCost * 1.8 * (mod ? mod.valueMult : 1.0) * (1 + finalDmg / 100));
    setMsg(`✨ ${bp.name}${mod ? ` (+${mod.name})` : ''} fabricated. Damage: ${finalDmg}. Estimated resale: ${resale} cr. Weapon added to inventory.`);
    setDone(true);
  };

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.96)', zIndex:200, display:'flex', flexDirection:'column', fontFamily:'monospace', color:'#DDD', padding:'1.5rem' }}>
      <div style={{ color:'#888', fontSize:'1.0rem', letterSpacing:2, marginBottom:'1rem' }}>BLACK-MARKET ARMS BENCH</div>
      <div style={{ marginBottom:'0.8rem' }}>
        <div style={{ color:'#666', fontSize:'0.75rem', marginBottom:'0.3rem' }}>BLUEPRINTS</div>
        <div style={{ display:'flex', gap:'0.5rem' }}>
          {ARMS_BLUEPRINTS.map(b => (
            <button key={b.id} onClick={() => setBp(b)} style={{ background: bp?.id===b.id ? '#1A1A2A' : '#111', border:`1px solid ${bp?.id===b.id ? '#4A7FBF' : '#444'}`, color:'#DDD', padding:'0.4rem 0.6rem', cursor:'pointer', fontSize:'0.75rem', textAlign:'left' }}>
              {b.name}<br/><span style={{ color:'#888', fontSize:'0.7rem' }}>DMG:{b.dmg} Heat:{b.heatRisk}% Cost:{b.cost}cr</span>
            </button>
          ))}
        </div>
      </div>
      <div style={{ marginBottom:'0.8rem' }}>
        <div style={{ color:'#666', fontSize:'0.75rem', marginBottom:'0.3rem' }}>MODS (optional)</div>
        <div style={{ display:'flex', gap:'0.5rem' }}>
          {ARMS_MODS.map(m => (
            <button key={m.id} onClick={() => setMod(prev => prev?.id===m.id ? null : m)} style={{ background: mod?.id===m.id ? '#1A2A1A' : '#111', border:`1px solid ${mod?.id===m.id ? '#4A8' : '#444'}`, color:'#DDD', padding:'0.3rem 0.5rem', cursor:'pointer', fontSize:'0.75rem' }}>
              {m.name}<br/><span style={{ color:'#888', fontSize:'0.7rem' }}>Heat:{m.heatBonus>0?'+':''}{m.heatBonus}% Value:×{m.valueMult}</span>
            </button>
          ))}
        </div>
      </div>
      {bp && <div style={{ fontSize:'0.8rem', color:'#888', marginBottom:'0.8rem' }}>Total cost: {bp.cost + (mod ? mod.cost : 0)} cr · Heat risk: {bp.heatRisk + (mod ? mod.heatBonus : 0)}%{bp.heatRisk + (mod ? mod.heatBonus : 0) > 50 ? ' ⚠️ HIGH' : ''}</div>}
      <div style={{ maxWidth:420, color:'#AAA', fontSize:'0.82rem', lineHeight:1.6, marginBottom:'1rem' }}>{msg}</div>
      <div style={{ display:'flex', gap:'0.8rem' }}>
        {!done && <button onClick={craft} style={{ background:'#C8A000', color:'#000', border:'none', padding:'0.4rem 1.2rem', cursor:'pointer', fontSize:'0.85rem' }}>Fabricate</button>}
        <button onClick={done ? onSuccess : onFailure} style={{ background:'#333', color:'#EEE', border:'none', padding:'0.4rem 1rem', cursor:'pointer', fontSize:'0.85rem' }}>{done ? 'Done' : 'Cancel'}</button>
      </div>
    </div>
  );
}

// ── Protection Shakedown ──────────────────────────────────────────────────────
function ProtectionShakedownOverlay({ onSuccess, onFailure }) {
  const targets = [
    { name:'The Onyx Cantina', type:'Cantina', tribute:1500 },
    { name:'Level 1313 Cyber-Lab', type:'Clinic', tribute:2500 },
    { name:'Czerka Pawn & Arms', type:'Pawn Shop', tribute:1200 },
  ];
  const [target] = React.useState(() => targets[Math.floor(Math.random()*targets.length)]);
  const [fear, setFear] = React.useState(10+Math.floor(Math.random()*20));
  const [resistance, setResistance] = React.useState(50+Math.floor(Math.random()*30));
  const [csfNotices, setCsfNotices] = React.useState(0);
  const [msg, setMsg] = React.useState(`${target.name} (${target.type}). Weekly tribute: ${target.tribute} cr. Break their resistance before they call CSF.`);
  const [done, setDone] = React.useState(false);
  const [success, setSuccess] = React.useState(false);

  const act = (action) => {
    if (done) return;
    let fearGain = 0, resLoss = 0, csfRisk = 0;
    let narrative = '';
    switch(action) {
      case 'VERBAL':
        fearGain=10+Math.random()*10|0; resLoss=5; csfRisk=0.1;
        narrative='You remind them of what happens to establishments that lack adequate protection.';
        break;
      case 'SMASH':
        fearGain=25+Math.random()*15|0; resLoss=20; csfRisk=0.35;
        narrative='Your enforcers break two tables and a display case. The owner goes pale.';
        break;
      case 'OFFER':
        fearGain=8; resLoss=25+Math.random()*10|0; csfRisk=0.05;
        narrative='You explain the value you provide. Guaranteed security. Peace of mind.';
        break;
    }
    const newFear = Math.min(100, fear+fearGain);
    const newRes = Math.max(0, resistance-resLoss);
    setFear(newFear); setResistance(newRes);
    if (Math.random() < csfRisk) {
      setCsfNotices(n => {
        const nc = n+1;
        if (nc >= 3) { setDone(true); setSuccess(false); setMsg(`${narrative} — but CSF arrives before the deal is sealed. Three reports. You are made. Retreat.`); }
        else setMsg(`${narrative} [CSF Alert ${nc}/3]`);
        return nc;
      });
    } else {
      setMsg(`${narrative} [Fear: ${newFear} | Resistance: ${newRes}]`);
    }
    if (newRes <= 0 && !done) {
      setDone(true); setSuccess(true);
      setMsg(`${target.name} capitulates. Weekly tribute: ${target.tribute} cr. The establishment is now under Syndicate protection.`);
    }
  };

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.96)', zIndex:200, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', fontFamily:'monospace', color:'#DDD', padding:'2rem' }}>
      <div style={{ color:'#C8A000', fontSize:'1.0rem', letterSpacing:2, marginBottom:'0.3rem' }}>DISTRICT PROTECTION SHAKEDOWN</div>
      <div style={{ color:'#888', fontSize:'0.8rem', marginBottom:'1rem' }}>CSF Notices: {csfNotices}/3</div>
      <div style={{ width:'100%', maxWidth:360, marginBottom:'1rem' }}>
        {[['Fear',fear,'#C03030'],['Resistance',resistance,'#4A7FBF']].map(([label,val,color]) => (
          <div key={label} style={{ marginBottom:'0.5rem' }}>
            <div style={{ display:'flex', justifyContent:'space-between', fontSize:'0.75rem', marginBottom:2 }}><span>{label}</span><span>{val}/100</span></div>
            <div style={{ background:'#222', borderRadius:4, overflow:'hidden' }}><div style={{ width:`${val}%`, background:color, height:10, borderRadius:4, transition:'width 0.3s' }}/></div>
          </div>
        ))}
      </div>
      {!done && (
        <div style={{ display:'flex', gap:'0.5rem', marginBottom:'1rem' }}>
          <button onClick={() => act('VERBAL')} style={{ background:'#1A1A1A', color:'#CCC', border:'1px solid #444', padding:'0.4rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Verbal Threat</button>
          <button onClick={() => act('SMASH')} style={{ background:'#2A0000', color:'#C03030', border:'1px solid #600', padding:'0.4rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Smash Property</button>
          <button onClick={() => act('OFFER')} style={{ background:'#1A2A1A', color:'#4A8', border:'1px solid #2A5A2A', padding:'0.4rem 0.8rem', cursor:'pointer', fontSize:'0.8rem' }}>Offer Protection</button>
        </div>
      )}
      <div style={{ maxWidth:420, textAlign:'center', color:'#AAA', fontSize:'0.82rem', lineHeight:1.6, marginBottom:'1rem' }}>{msg}</div>
      {done && <button onClick={success ? onSuccess : onFailure} style={{ background: success ? '#C8A000' : '#333', color: success ? '#000' : '#EEE', border:'none', padding:'0.4rem 1.2rem', cursor:'pointer', fontSize:'0.85rem' }}>{success ? 'Seal the Deal' : 'Withdraw'}</button>}
    </div>
  );
}

// ── Sky-Lane Evasion ──────────────────────────────────────────────────────────
function SkyLaneEvasionOverlay({ onSuccess, onFailure }) {
  const LANES = 5;
  const [lane, setLane] = React.useState(2);
  const [csfLane, setCsfLane] = React.useState(0);
  const [tick, setTick] = React.useState(0);
  const [distance, setDistance] = React.useState(20);
  const [hull, setHull] = React.useState(5);
  const [cargo, setCargo] = React.useState(3);
  const [msg, setMsg] = React.useState('CSF interceptors on your tail. Dodge traffic. Drop cargo to create distance.');
  const [done, setDone] = React.useState(false);
  const [success, setSuccess] = React.useState(false);
  const intervalRef = React.useRef(null);

  React.useEffect(() => {
    if (done) { clearInterval(intervalRef.current); return; }
    intervalRef.current = setInterval(() => {
      setTick(t => t + 1);
      setCsfLane(l => {
        const opts = [];
        for (let i = 0; i < LANES; i++) opts.push(i);
        return opts[Math.floor(Math.random() * opts.length)];
      });
      setDistance(d => {
        const nd = d - 1;
        if (nd <= 0) { setDone(true); setSuccess(false); setMsg('CSF interceptors achieved weapons lock. You are boarded. Run failed.'); return 0; }
        return nd;
      });
    }, 900);
    return () => clearInterval(intervalRef.current);
  }, [done]);

  React.useEffect(() => {
    if (done) return;
    if (hull <= 0) { setDone(true); setSuccess(false); setMsg('Hull breached. Speeder goes down in the mid-levels. Capture imminent.'); return; }
    if (distance >= 20) { setDone(true); setSuccess(true); setMsg('Lost them in the undercity traffic. CSF interceptors broke off pursuit. You are clear.'); return; }
    if (lane === csfLane && tick > 0) {
      setHull(h => h - 1);
      setMsg(`Collision! Hull integrity: ${hull - 1}/5`);
    }
  }, [tick, csfLane, lane]);

  const dodge = (dir) => {
    if (done) return;
    setLane(l => Math.max(0, Math.min(LANES - 1, l + dir)));
  };

  const dropCargo = () => {
    if (cargo <= 0) { setMsg('No cargo remaining to drop.'); return; }
    setCargo(c => c - 1);
    setDistance(d => Math.min(20, d + 5));
    setMsg(`Cargo container deployed. CSF slows to investigate. Distance gained.`);
  };

  const laneBar = (n) => n === lane ? '[YOU]' : n === csfLane ? '[CSF]' : '[ · ]';

  return (
    <div style={{ position:'fixed', inset:0, background:'rgba(0,0,0,0.96)', zIndex:200, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', fontFamily:'monospace', color:'#DDD', padding:'2rem' }}>
      <div style={{ color:'#4A7FBF', fontSize:'1.0rem', letterSpacing:2, marginBottom:'0.5rem' }}>SKY-LANE EVASION</div>
      <div style={{ fontSize:'0.8rem', color:'#888', marginBottom:'1rem' }}>
        Distance to clear: {distance}/20 &nbsp;|&nbsp; Hull: {hull}/5 &nbsp;|&nbsp; Cargo: {cargo}
      </div>
      <div style={{ display:'flex', gap:'0.3rem', marginBottom:'1rem' }}>
        {Array.from({ length: LANES }, (_, i) => (
          <div key={i} style={{ width:60, height:32, background: i===lane ? '#1A3A6A' : i===csfLane ? '#3A0000' : '#111', border:`1px solid ${i===lane ? '#4A7FBF' : i===csfLane ? '#C03030' : '#333'}`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:'0.7rem', color: i===lane ? '#4A7FBF' : i===csfLane ? '#C03030' : '#555' }}>
            {laneBar(i)}
          </div>
        ))}
      </div>
      {!done && (
        <div style={{ display:'flex', gap:'0.8rem', marginBottom:'1rem' }}>
          <button onClick={() => dodge(-1)} style={{ background:'#111', color:'#EEE', border:'1px solid #444', padding:'0.4rem 0.8rem', cursor:'pointer', fontSize:'0.85rem' }}>◀ Left</button>
          <button onClick={dropCargo} disabled={cargo<=0} style={{ background: cargo>0 ? '#2A1A00' : '#111', color: cargo>0 ? '#C8A000' : '#555', border:`1px solid ${cargo>0 ? '#604800' : '#333'}`, padding:'0.4rem 0.8rem', cursor: cargo>0 ? 'pointer' : 'default', fontSize:'0.85rem' }}>Drop Cargo ({cargo})</button>
          <button onClick={() => dodge(1)} style={{ background:'#111', color:'#EEE', border:'1px solid #444', padding:'0.4rem 0.8rem', cursor:'pointer', fontSize:'0.85rem' }}>Right ▶</button>
        </div>
      )}
      <div style={{ maxWidth:400, textAlign:'center', color:'#AAA', fontSize:'0.82rem', lineHeight:1.6, marginBottom:'1rem' }}>{msg}</div>
      {done && <button onClick={success ? onSuccess : onFailure} style={{ background: success ? '#C8A000' : '#333', color: success ? '#000' : '#EEE', border:'none', padding:'0.4rem 1.2rem', cursor:'pointer', fontSize:'0.85rem' }}>{success ? 'Disappear into the city' : 'Accept Capture'}</button>}
    </div>
  );
}

const AI_COMBAT_PROFILES = {
  csf_swat:             { aggression:30, cover:90, flank:30, overwatch:85, optRange:4, name:'CSF SWAT Operative',   hp:6, shield:3, accent:'#4A9FFF' },
  csf_scout:            { aggression:55, cover:55, flank:70, overwatch:50, optRange:3, name:'CSF Scout',            hp:4, shield:2, accent:'#4A9FFF' },
  black_sun_striker:    { aggression:85, cover:30, flank:75, overwatch:20, optRange:2, name:'Black Sun Striker',    hp:5, shield:1, accent:'#C03030' },
  black_sun_vigo_guard: { aggression:60, cover:65, flank:55, overwatch:55, optRange:3, name:'Black Sun Guard',      hp:7, shield:2, accent:'#C03030' },
  exchange_bounty_hunter:{ aggression:60, cover:65, flank:85, overwatch:70, optRange:3, name:'Exchange Bounty Hunter',hp:6,shield:2, accent:'#9B59B6' },
  anzati_assassin:      { aggression:90, cover:20, flank:95, overwatch:10, optRange:1, name:'Anzati Assassin',      hp:5, shield:0, accent:'#C8A000' },
  malak_enforcer:       { aggression:70, cover:50, flank:60, overwatch:40, optRange:2, name:'Malak',                hp:8, shield:2, accent:'#C03030' },
  jon_vane:             { aggression:55, cover:70, flank:65, overwatch:60, optRange:3, name:'Jon Vane',             hp:10, shield:3, accent:'#C8A000' },
  syndicate_thug:       { aggression:75, cover:25, flank:40, overwatch:15, optRange:2, name:'Syndicate Thug',       hp:4, shield:0, accent:'#808080' },
  kesh_sith:            { aggression:80, cover:30, flank:70, overwatch:30, optRange:2, name:'Kesh',                 hp:8, shield:0, accent:'#8B0000' },
  vigo_vanguard:        { aggression:50, cover:80, flank:40, overwatch:80, optRange:4, name:'Black Sun Vigo Vanguard',  hp:8,  shield:3, accent:'#2A4A8A' },
  falleen_taskmaster:   { aggression:35, cover:70, flank:30, overwatch:60, optRange:3, name:'Falleen Taskmaster',        hp:5,  shield:1, accent:'#2ECC71' },
  cyborg_splicer:       { aggression:80, cover:20, flank:90, overwatch:10, optRange:2, name:'Cyborg Splicer',            hp:3,  shield:0, accent:'#00FF80' },
  repulsor_drone:       { aggression:90, cover:10, flank:85, overwatch:15, optRange:2, name:'Repulsor-Blade Drone',      hp:2,  shield:0, accent:'#7B00FF' },
  riot_controller:      { aggression:40, cover:95, flank:20, overwatch:70, optRange:2, name:'CSF Riot Controller',       hp:7,  shield:4, accent:'#4A9FFF' },
  swat_marksman:        { aggression:30, cover:90, flank:15, overwatch:95, optRange:6, name:'SWAT Marksman',             hp:5,  shield:2, accent:'#C83030' },
  pyke_sentinel:        { aggression:55, cover:60, flank:75, overwatch:50, optRange:3, name:'Pyke Sentinel',             hp:5,  shield:1, accent:'#4AB8A8' },
  spice_brute:          { aggression:100,cover:5,  flank:50, overwatch:0,  optRange:1, name:'Spice-Gorged Brute',        hp:7,  shield:0, accent:'#FF40CC' },
  gamorrean_ironclad:   { aggression:45, cover:85, flank:15, overwatch:50, optRange:1, name:'Gamorrean Ironclad',        hp:10, shield:2, accent:'#7A5A3A' },
  rodian_saboteur:      { aggression:65, cover:75, flank:80, overwatch:30, optRange:4, name:'Rodian Saboteur',           hp:4,  shield:0, accent:'#5ABF5A' },
  mandalorian_tracker:  { aggression:70, cover:55, flank:90, overwatch:75, optRange:4, name:'Mandalorian Tracker',       hp:9,  shield:3, accent:'#8A9AB0' },
  ig_assassin_droid:    { aggression:80, cover:20, flank:70, overwatch:90, optRange:5, name:'IG Assassin Droid',         hp:8,  shield:0, accent:'#CC3030' },
  sewer_crawler:        { aggression:95, cover:0,  flank:30, overwatch:0,  optRange:1, name:'Sewer Crawler',             hp:6,  shield:0, accent:'#80FF40' },
  desperate_scrapper:   { aggression:80, cover:30, flank:40, overwatch:10, optRange:2, name:'Desperate Scrapper',        hp:3,  shield:0, accent:'#B08030' },
  unit_8x:              { aggression:65, cover:50, flank:40, overwatch:90, optRange:5, name:'Unit 8-X',                  hp:10, shield:4, accent:'#B8D4E8' },
  kdy_security_guard:   { aggression:45, cover:75, flank:35, overwatch:65, optRange:4, name:'KDY Security Guard',          hp:6,  shield:3, accent:'#4A7A9B' },
  trandoshan_hunter:    { aggression:70, cover:55, flank:85, overwatch:70, optRange:5, name:'Trandoshan Hunter',            hp:7,  shield:1, accent:'#5A7050' },
  // BADOR PROFILES BEGIN
  kef_shock_breacher:   { aggression:85, cover:25, flank:45, overwatch:15, optRange:2, name:'KEF Shock-Breacher',        hp:6,  shield:2, accent:'#FF5500' },
  kef_breach_wave:      { aggression:85, cover:30, flank:60, overwatch:20, optRange:2, name:'KEF Breach Wave',            hp:9,  shield:3, accent:'#FF5500' },
  kef_jump_trooper:     { aggression:75, cover:20, flank:95, overwatch:10, optRange:2, name:'KEF Jump-Trooper',          hp:4,  shield:0, accent:'#FF8A2A' },
  kef_mortar_specialist:{ aggression:40, cover:85, flank:10, overwatch:90, optRange:6, name:'KEF Mortar Specialist',     hp:4,  shield:1, accent:'#E04A1A' },
  kef_hunter_killer:    { aggression:70, cover:35, flank:80, overwatch:60, optRange:4, name:'KEF Hunter-Killer Droid',   hp:5,  shield:2, accent:'#FF3030' },
  kef_convoy_guard:     { aggression:55, cover:70, flank:40, overwatch:60, optRange:4, name:'KEF Convoy Guard',          hp:6,  shield:2, accent:'#FF5500' },
  kef_gate_guards:      { aggression:60, cover:80, flank:30, overwatch:70, optRange:4, name:'KEF Gate Guards',           hp:7,  shield:3, accent:'#FF5500' },
  kef_gate_guards_flanked:{ aggression:40, cover:30, flank:10, overwatch:20, optRange:3, name:'KEF Gate Guards (Ambushed)', hp:4, shield:1, accent:'#FF5500' },
  kef_sledge_tank:      { aggression:70, cover:10, flank:20, overwatch:85, optRange:5, name:'KEF Repulsor-Sledge Tank',  hp:14, shield:6, accent:'#FF3300' },
  kef_sledge_tank_crippled:{ aggression:60, cover:10, flank:10, overwatch:70, optRange:4, name:'KEF Repulsor-Sledge Tank (Crippled)', hp:8, shield:2, accent:'#FF3300' },
  varek_korr:           { aggression:65, cover:55, flank:50, overwatch:65, optRange:4, name:'Varek Korr and the War-Dredge', hp:14, shield:5, accent:'#FF5500', regen:1, enrageAt:0.4, enrageShots:1 },
  furnace_husk:         { aggression:90, cover:5,  flank:20, overwatch:0,  optRange:2, name:'Furnace Husk',               hp:8,  shield:2, accent:'#FF7A1A' },
  piston_sentinel:      { aggression:50, cover:70, flank:15, overwatch:80, optRange:2, name:'Piston Sentinel',            hp:11, shield:5, accent:'#C4923E' },
  unchained_overseer:   { aggression:75, cover:40, flank:30, overwatch:80, optRange:4, name:'The Unchained Overseer',     hp:16, shield:6, accent:'#2B7895', regen:1, enrageAt:0.4, enrageShots:1 },
  cav_unit_8x_guard:    { aggression:60, cover:85, flank:45, overwatch:80, optRange:4, name:'Gate Sentinel Escort',       hp:12, shield:5, accent:'#7A5CC8', dmg:1, regen:1 },
  cav_unit_8x_boss:     { aggression:72, cover:60, flank:35, overwatch:95, optRange:5, name:'Unit 8-X, Gate Warden',      hp:16, shield:6, accent:'#B8D4E8', regen:1, enrageAt:0.4, enrageShots:1 },
  zora_hull_wardens:    { aggression:70, cover:75, flank:50, overwatch:85, optRange:4, name:'Zora Hull Sentinels',        hp:14, shield:5, accent:'#B8D4E8', dmg:1, regen:1 },
  ghost_keel_warden:    { aggression:80, cover:55, flank:60, overwatch:90, optRange:5, name:'Warden Prime',               hp:24, shield:8, accent:'#FF4040', regen:1, enrageAt:0.3, enrageShots:1 },
  wst_gharza:           { aggression:45, cover:85, flank:60, overwatch:90, optRange:6, name:'Gharza, Trandoshan Sniper',  hp:12,  shield:2, accent:'#5A7050', dmg:1 },
  // BADOR PROFILES END
  // MFG PROFILES BEGIN
  mfg_enforcers:        { aggression:75, cover:45, flank:40, overwatch:55, optRange:3, name:'Krax\'s Loader Enforcers',     hp:12, shield:4, accent:'#D4762C', dmg:1, regen:1 },
  mfg_krax:             { aggression:70, cover:55, flank:55, overwatch:70, optRange:3, name:'Overseer Krax and the Loader Frame', hp:16, shield:6, accent:'#D4762C', regen:1, enrageAt:0.4, enrageShots:1 },
  // MFG PROFILES END
  // RING PROFILES BEGIN
  ring_vaelen_sentinel: { aggression:40, cover:85, flank:15, overwatch:85, optRange:3, name:'Captain Vaelen',                 hp:12, shield:5, accent:'#E8D090', dmg:1, regen:1 },
  ring_sec_riot_squad:  { aggression:60, cover:75, flank:35, overwatch:55, optRange:3, name:'Ring-Sec Riot Squad',            hp:12,  shield:3, accent:'#4A7A9B', dmg:1 },
  ring_isb_krennis:     { aggression:55, cover:80, flank:55, overwatch:80, optRange:4, name:'Commander Krennis and ISB Aides', hp:13, shield:4, accent:'#7A8A68', dmg:1, regen:1 },
  // RING PROFILES END
};

const ENCOUNTER_TABLE = {
  shadow_town:         ['black_sun_striker','black_sun_striker','syndicate_thug','exchange_bounty_hunter','black_sun_vigo_guard'],
  slicer_alleyway:     ['csf_scout','black_sun_striker','syndicate_thug','exchange_bounty_hunter'],
  freight_hub:         ['csf_swat','csf_swat','syndicate_thug','csf_scout','exchange_bounty_hunter'],
  the_works:           ['syndicate_thug','black_sun_striker','anzati_assassin'],
  undercity_outskirts: ['syndicate_thug','black_sun_striker','anzati_assassin','kesh_sith'],
  spice_refining_vaults:['pyke_sentinel','pyke_sentinel','spice_brute','cyborg_splicer','rodian_saboteur'],
  senatorial_lounges:  ['vigo_vanguard','falleen_taskmaster','black_sun_striker','csf_swat'],
  csf_academy:         ['riot_controller','swat_marksman','csf_swat','csf_scout'],
};

// Bador Moon active combat zones. Hostiles here are raiders, scavengers and rogue machines only:
// KDY troops and personnel never attack the player. Each zone needs `need` victories to be cleared for good.
const BADOR_COMBAT_ZONES = {
  bador_manufacturing: { need: 2, rate: 7, pool: ['syndicate_thug', 'trandoshan_hunter', 'desperate_scrapper'],
    flavor: ['Raiders have slipped through a cut fence in the Sector 4 gantries. They are stripping the assembly line for parts, and you are in the way.', 'A looter crew drops from the catwalk above the fabrication floor, weapons already up.'] },
  city_outskirts_bador: { need: 3, rate: 8, pool: ['kef_shock_breacher', 'desperate_scrapper', 'kef_jump_trooper'],
    flavor: ['A KEF breach squad comes through the smoke between two burning hovels, orange suits glowing in the firelight.', 'A jump trooper drops from a rooftop and lands in a crouch with a plasma shotgun already leveled.', 'Mortar rounds walk down the street and a KEF fire team uses the confusion to move on you.'] },
  engine_wastes: { need: 3, rate: 9, pool: ['furnace_husk', 'piston_sentinel', 'trandoshan_hunter', 'desperate_scrapper'],
    flavor: ['A furnace husk drags itself out of the slag, chest door glowing white, and aims straight at you.', 'A piston sentinel steps off its plinth. Steam hisses from its back stacks as the pile drivers come up.', 'Engine slag crunches under boots that are not yours. Hunters rise from behind a dead thruster bell.'] },
  deep_caverns: { need: 3, rate: 9, pool: ['unit_8x', 'trandoshan_hunter', 'desperate_scrapper'],
    flavor: ['A red optic blinks awake in the cavern dark. The old defense droid has not stopped patrolling.', 'Echoes carry a clatter of armor from the tunnel mouth. You are not alone down here.'] },
  zora_iv_wreckage: { need: 2, rate: 8, pool: ['unit_8x', 'trandoshan_hunter', 'desperate_scrapper'],
    flavor: ['The wreck groans and a salvage crew steps out of the broken hull, rifles leveled.', 'A drone from the dead freighter wakes and locks onto you.'] },
};
const BADOR_ENCOUNTER_MIN_STEPS = 12;

const ENCOUNTER_RATES = {
  undercity_outskirts: 22,
  the_works:           20,
  freight_hub:         18,
  shadow_town:         15,
  slicer_alleyway:     15,
  spice_refining_vaults: 25,
  senatorial_lounges:    12,
  csf_academy:           30,
};

const ENCOUNTER_FLAVOR = {
  shadow_town: {
    black_sun_striker: ["A Black Sun enforcer clocks your face on their wanted holo. Blaster drawn before you can step back.","You round a corner into a patrol. One of them recognizes you from Malak's description."],
    syndicate_thug: ["A street enforcer has been watching you for three blocks. The loading bay has no other exit.","Somebody put a price on your head. Word travels fast down here."],
    exchange_bounty_hunter: ["A tracker in Exchange contract gear drops from a maintenance gantry. Professional. Prepared."],
  },
  slicer_alleyway: {
    csf_scout: ["A CSF scout drone flags your position. The handler is two intersections away and closing.","You trip a micro-surveillance wire. A plainclothes unit converges from both ends of the alley."],
    black_sun_striker: ["A Black Sun data thief does not appreciate the competition. They make that clear with a raised blaster."],
    syndicate_thug: ["Two enforcers spot your slicer kit. In their logic, you are cutting into their territory."],
  },
  freight_hub: {
    csf_swat: ["Red strobes. A CSF tactical team seals the freight corridor. Someone burned your route.","The manifest you were carrying matches a flagged shipment. A SWAT unit comes through the service door."],
    syndicate_thug: ["A dockworkers crew has been skimming this bay for months. They are not interested in witnesses."],
  },
  the_works: {
    syndicate_thug: ["The pipe network goes quiet. Then three shapes detach from the scaffolding above you.","Maintenance drones scatter. The crew behind them was using them as mobile cover."],
    black_sun_striker: ["Black Sun runs a distribution node somewhere in this industrial maze. You just crossed their perimeter."],
  },
  sky_market: {
    csf_scout: ["A CSF plainclothes agent flags your ID at a market terminal. They signal their partner across the promenade.","You made a purchase that triggered a pattern-match in the surveillance net. The scout is already moving."],
    exchange_bounty_hunter: ["An Exchange contractor hands your dossier to a hunter across the promenade. The hunter reads it and walks toward you."],
  },
  market: {
    syndicate_thug: ["A protection crew decides your credits belong to them. The market clears fast when weapons come out."],
    csf_scout: ["Your face matches a warrant flag in the precinct system. The officer clocks the alert and reaches for their comm."],
  },
  lower_sky_market: {
    black_sun_striker: ["Black Sun has been expanding into this level. You stepped into their newest block without realizing it.","A Black Sun runner made you for an underworld rival. The striker behind them does not negotiate."],
    exchange_bounty_hunter: ["The Exchange posted a bounty on anyone disrupting their lower-market routes. You qualify."],
  },
  undercity_outskirts: {
    syndicate_thug: ["Three shapes drop from the overhead pipes. This far down, there are no witnesses. They know it.","A salvage crew decides your equipment is worth more than the finder's fee they are owed. Old math."],
    black_sun_striker: ["A Black Sun patrol has pushed its territory all the way to the Undercity edge. You are standing in their newest claim.","Word of your operations reached the Black Sun network. A striker intercepted your route before you knew you were being tracked."],
    anzati_assassin: ["Something moved in the dark ahead of you. Then stopped. Then was somewhere else entirely. You have been stalked since you entered this level.","An Anzati has been hired. You are the contract."],
    kesh_sith: ["The Force stirs here. Something ancient and wrong. A figure emerges from the ruin entrance, pale eyes burning in the dark. 'You should not be here.'","The Sith acolyte felt you before you saw them. They have been waiting at the ruin threshold. They do not intend to let you pass."],
  },
  spice_refining_vaults: {
    pyke_sentinel: ["A Pyke sentry clocks your face against their watch-list before you reach the first refinery tank. The carbine comes up in one fluid motion.","The yellow vapor trail catches your eye too late. Three Pyke Sentinels have flanked the catwalk on both sides."],
    spice_brute: ["Something huge staggers out of the vapor clouds. Its eyes are solid magenta. It is not interested in negotiation.","The low growling you heard was the warning sign. Now it charges. The raw spice has burned every instinct but violence."],
    cyborg_splicer: ["A crackle of sparks down the corridor. Then a wire-thin figure with neon-green optical ports drops from the duct above you.","The turret sweeps toward you. Behind it, a Splicer has already jacked into the panel and is smiling."],
  },
  senatorial_lounges: {
    vigo_vanguard: ["The guard's ceremonial cape does not slow the draw at all. A Vigo's detail does not accept witnesses.","Two Black Sun Vanguard step out of the private elevator. Indigo armor, gold trim. Professional. They were expecting you."],
    falleen_taskmaster: ["The room temperature shifts. Something chemical and sweet. Then your allies are moving wrong. The Falleen Taskmaster just deployed their pheromones.","A composed reptilian figure in burgundy silk is already dictating orders into a wrist-comm. The Vanguard behind it draws."],
    black_sun_striker: ["Gang insignia on the lapel. A Black Sun runner was tailing you from the promenade. The striker at the service door is their backup."],
    csf_swat: ["Red strobes at the corridor junction. A SWAT element was tipped off. Someone in the lounges burned your route."],
  },
  csf_academy: {
    riot_controller: ["A shield wall clicks into formation across the corridor. The Riot Controller at center does not blink. The stun baton is already live.","The corridor strobes alternate red and blue. A Riot Controller steps into your path, shield raised, cutting the hall in half."],
    swat_marksman: ["A red dot walks across the far wall and settles on your chest. Somewhere in the dark, a SWAT Marksman has a firing solution.","You hear the stabilizer harness click. The red laser line is already crossing the room. The Marksman is stationary and entrenched."],
    csf_swat: ["The Academy breach alarm is live. A SWAT unit sweeps the annex from the far end."],
  },
};

function getEncounterFlavor(zoneId, profileKey) {
  const zoneFlavors = ENCOUNTER_FLAVOR[zoneId] || {};
  const options = zoneFlavors[profileKey] || Object.values(zoneFlavors).flat();
  const pool = options.length > 0 ? options : ['A hostile contact corners you. There is no talking your way out of this.'];
  return pool[Math.floor(Math.random() * pool.length)];
}

const COMBAT_LOOT = {
  // BADOR LOOT BEGIN
  kef_shock_breacher: { credits:[60,120], itemChance:0.4, items:[{id:'loot_kef_plasma_shotgun',name:'KEF Plasma Shotgun',type:'weapon',iconKind:'gear',value:380,description:'A scavenged industrial shotgun with the safety clip cut off.'}] },
  kef_breach_wave: { credits:[90,160], itemChance:0.45, items:[{id:'loot_breach_shield',name:'Scorched Thermal Shield',type:'gear',iconKind:'gear',value:420,description:'A battered thermal shield plate with fresh scoring.'}] },
  kef_jump_trooper: { credits:[50,100], itemChance:0.35, items:[{id:'loot_kef_jump_pack',name:'Spent Jump Pack',type:'gear',iconKind:'gear',value:260,description:'A rocket pack, half empty and fully loud.'}] },
  kef_mortar_specialist: { credits:[70,130], itemChance:0.4, items:[{id:'loot_kef_mortar_sight',name:'Mortar Spotting Optic',type:'gear',iconKind:'gear',value:320,description:'A calibrated optic still glowing with a target solution.'}] },
  kef_hunter_killer: { credits:[80,140], itemChance:0.4, items:[{id:'loot_hk_servo',name:'Hunter-Killer Servo Core',type:'gear',iconKind:'gear',value:340,description:'A tracking servo that would make a very good alarm clock.'}] },
  kef_convoy_guard: { credits:[70,130], itemChance:0.4, items:[{id:'loot_convoy_manifest',name:'KEF Convoy Manifest',type:'quest',iconKind:'datapad',value:220,description:'A cargo list for the convoy you just ruined.'}] },
  kef_gate_guards: { credits:[80,150], itemChance:0.4, items:[{id:'loot_gate_codes',name:'KEF Gate Codes',type:'quest',iconKind:'keycard',value:260,description:'Handwritten gate codes in a trembling hand.'}] },
  kef_gate_guards_flanked: { credits:[50,100], itemChance:0.3, items:[{id:'loot_gate_rations',name:'Dropped Field Rations',type:'consumable',iconKind:'supply',value:40,description:'They never saw you coming.'}] },
  kef_sledge_tank: { credits:[180,320], itemChance:0.7, items:[{id:'loot_sledge_core',name:'Repulsor Sledge Core',type:'gear',iconKind:'gear',value:900,description:'A burnt out repulsor coil, still warm.'}] },
  kef_sledge_tank_crippled: { credits:[140,260], itemChance:0.6, items:[{id:'loot_sledge_plate',name:'Sledge Armor Plate',type:'gear',iconKind:'gear',value:600,description:'A thick plate scored by a dozen hits.'}] },
  varek_korr: { credits:[250,420], itemChance:0.85, items:[{id:'loot_war_dredge_core',name:'War-Dredge Core Coil',type:'gear',iconKind:'gear',value:700,description:'The overcharged core coil from the War-Dredge. Still warm, still dangerous.'}] },
  furnace_husk: { credits:[70,130], itemChance:0.4, items:[{id:'loot_slag_residue',name:'Cooled Slag Residue',type:'consumable',iconKind:'supply',value:150,description:'Dense, warm and unexpectedly valuable to a forge.'}] },
  piston_sentinel: { credits:[120,220], itemChance:0.55, items:[{id:'loot_brass_plate',name:'Sentinel Brass Plate',type:'gear',iconKind:'gear',value:520,description:'Tarnished brass from a construct that was never meant to retire.'}] },
  unchained_overseer: { credits:[260,440], itemChance:0.9, items:[{id:'item_titan_cylinder_ring',name:'Titan Cylinder Ring',type:'gear',iconKind:'gear',value:800,description:'An ancient signet ring proving engineering rank.'}] },
  cav_unit_8x_guard: { credits:[180,300], itemChance:0.8, items:[{id:'loot_sentinel_servo',name:'Sentinel Servo Pack',type:'gear',iconKind:'gear',value:650,description:'A matched servo pack torn from an escort sentinel. Still twitching.'}] },
  cav_unit_8x_boss: { credits:[360,560], itemChance:1.0, items:[{id:'loot_warden_actuator',name:'Warden Pile Driver Actuator',type:'weapon',iconKind:'gear',value:1100,description:'The pneumatic actuator from the warden\'s arm. Hits like a falling door.'}] },
  zora_hull_wardens: { credits:[300,500], itemChance:0.8, items:[{id:'loot_zora_sentinel_core',name:'Sentinel Logic Core',type:'gear',iconKind:'gear',value:900,description:'A hardened logic core from a hull sentinel. Fenced for a month of rent in any yard.'}] },
  ghost_keel_warden: { credits:[500,900], itemChance:1, items:[{id:'loot_warden_prime_actuator',name:'Warden Prime Actuator',type:'gear',iconKind:'gear',value:2000,description:'The pressure actuator from the Warden Prime\'s arm. Dense, Republic grade, still warm.'}] },
  wst_gharza: { credits:[300,520], itemChance:0.9, items:[{id:'loot_wst_trandoshan_rifle',name:'Trandoshan Long Rifle',type:'weapon',iconKind:'gear',value:650,description:'A long barreled hunting rifle with a notched stock. Fifteen notches. The newest is fresh.'}] },
  // BADOR LOOT END
  // MFG LOOT BEGIN
  mfg_enforcers: { credits:[120,200], itemChance:0.5, items:[{id:'loot_mfg_clamp_gauntlet',name:'Loader Clamp Gauntlet',type:'gear',iconKind:'gear',value:480,description:'A hydraulic clamp gauntlet, built for pallets and used on people.'}] },
  mfg_krax: { credits:[320,520], itemChance:0.9, items:[{id:'loot_mfg_overseer_keycard',name:'Overseer Master Keycard',type:'gear',iconKind:'keycard',value:900,description:'Krax\'s master card for every door in Sector 4, scorched at one corner.'}] },
  // MFG LOOT END
  csf_swat:             { credits:[90,160],  itemChance:0.45, items:[{id:'loot_csf_stun_baton',name:'CSF Stun Baton',type:'weapon',iconKind:'gear',value:200,description:'Regulation CSF close-quarters weapon. Still carries a charge.'},{id:'loot_csf_access_chip',name:'CSF Access Chip',type:'gear',iconKind:'keycard',value:350,description:'Biometric-coded to a SWAT officer. Crackable.'},{id:'loot_tactical_stim',name:'Tactical Stim',type:'consumable',iconKind:'supply',value:120,description:'Military-grade stim. Restores combat readiness fast.'}] },
  csf_scout:            { credits:[60,110],  itemChance:0.35, items:[{id:'loot_scout_earpiece',name:'CSF Scout Earpiece',type:'gear',iconKind:'gear',value:180,description:'Encrypted channel. 48-hour window before they rotate the key.'},{id:'loot_det_round',name:'Det Round x2',type:'consumable',iconKind:'supply',value:90,description:'Compact explosive rounds. Useful.'}] },
  black_sun_striker:    { credits:[70,140],  itemChance:0.40, items:[{id:'loot_vibro_knife',name:'Vibro-Knife',type:'weapon',iconKind:'gear',value:250,description:'Black Sun manufacture. Serrated edge still carries a charge.'},{id:'loot_black_sun_sigil',name:'Black Sun Sigil',type:'quest',iconKind:'keycard',value:150,description:'Proof of gang membership. Useful for bluffing at checkpoints.'}] },
  black_sun_vigo_guard: { credits:[100,180], itemChance:0.45, items:[{id:'loot_heavy_pistol',name:'Heavy Blaster Pistol',type:'weapon',iconKind:'gear',value:400,description:'Modified for stopping power. Black Sun guard-issue.'},{id:'loot_vigo_access_card',name:'Vigo Access Card',type:'quest',iconKind:'keycard',value:500,description:'Opens restricted Black Sun safehouses.'}] },
  exchange_bounty_hunter:{credits:[110,200], itemChance:0.50, items:[{id:'loot_bounty_contract',name:'Blank Bounty Contract',type:'quest',iconKind:'keycard',value:300,description:'An Exchange authorization seal, unsigned. Could issue counter-contracts.'},{id:'loot_tracking_device',name:'Tracking Device',type:'gear',iconKind:'gear',value:200,description:'Designed to tag targets through crowded levels. One target loaded.'},{id:'loot_dura_weave_vest',name:'Dura-Weave Vest',type:'gear',iconKind:'supply',value:450,description:'Exchange-issue light armor. Absorbs one hit cleanly.'}] },
  anzati_assassin:      { credits:[80,160],  itemChance:0.50, items:[{id:'loot_memory_toxin',name:'Memory Toxin Vial',type:'consumable',iconKind:'supply',value:600,description:'Anzati-brewed. Disorients target utterly. Handle with care.'},{id:'loot_cipher_thread',name:'Cipher Thread',type:'gear',iconKind:'keycard',value:350,description:'Encrypted data line the Anzati was using. Someone hired them.'}] },
  malak_enforcer:       { credits:[150,250], itemChance:0.65, items:[{id:'loot_malak_comlink',name:"Malak's Comlink",type:'gear',iconKind:'gear',value:500,description:"Malak's encrypted channel. Other contacts are stored in it."},{id:'loot_cartel_ledger',name:'Cartel Ledger Chip',type:'quest',iconKind:'keycard',value:700,description:'Records of dealings. Names the Black Sun principals.'}] },
  jon_vane:             { credits:[200,350], itemChance:0.80, items:[{id:'loot_vane_blaster',name:"Jon's Custom Blaster",type:'weapon',iconKind:'gear',value:800,description:"Jon Vane's personal sidearm. Modified barrel and grip. Unmistakable."},{id:'loot_senate_account',name:'Senate Account Key',type:'quest',iconKind:'keycard',value:1000,description:'Access to a laundered Senate credit account. This is what he was protecting.'}] },
  syndicate_thug:       { credits:[40,90],   itemChance:0.30, items:[{id:'loot_street_cred_chip',name:'Street Cred Chip',type:'quest',iconKind:'keycard',value:100,description:'Gang token. Buys passage through one checkpoint without questions.'},{id:'loot_contraband_stash',name:'Contraband Sample',type:'consumable',iconKind:'supply',value:150,description:'Whatever they were carrying. Sell it quietly.'}] },
  kesh_sith:            { credits:[120,220], itemChance:0.60, items:[{id:'loot_sith_amulet',name:'Sith Amulet',type:'gear',iconKind:'gear',value:600,description:'Dark side artifact. Force-sensitive users report unease. Sells for a premium.'},{id:'loot_kesh_holocron',name:"Kesh's Holocron Fragment",type:'quest',iconKind:'keycard',value:900,description:'Shattered piece of a Sith teaching device. Dangerous intel.'}] },
  vigo_vanguard:        { credits:[120,210], itemChance:0.50, items:[{id:'loot_vigo_cape',name:'Falepian-Leather Half-Cape',type:'gear',iconKind:'supply',value:550,description:'High-end Black Sun ceremonial piece. Sells to collectors or worn as status marker.'},{id:'loot_indigo_cipher',name:'Indigo Cipher Disk',type:'quest',iconKind:'keycard',value:400,description:'Encoded disk carrying Black Sun internal comm frequencies.'}] },
  falleen_taskmaster:   { credits:[90,160],  itemChance:0.45, items:[{id:'loot_stim_injector',name:'Wrist Stim-Injector',type:'consumable',iconKind:'supply',value:300,description:'Combat-grade stimulant array. One charge remaining. Highly illegal above-level.'},{id:'loot_pheromone_vial',name:'Falleen Pheromone Vial',type:'consumable',iconKind:'supply',value:500,description:'Distilled. Even synthetic application clouds judgment. Single dose.'}] },
  cyborg_splicer:       { credits:[60,110],  itemChance:0.50, items:[{id:'loot_cranial_port',name:'Cranial Data Port',type:'gear',iconKind:'gear',value:250,description:'Aftermarket neural interface. Exchange manufacture. Still has data cached.'},{id:'loot_hack_spike',name:'Encrypted Hack Spike',type:'gear',iconKind:'keycard',value:350,description:'Single-use intrusion tool. Bypasses standard CSF access locks.'}] },
  repulsor_drone:       { credits:[20,50],   itemChance:0.60, items:[{id:'loot_plasma_blade_frag',name:'Plasma Blade Fragment',type:'weapon',iconKind:'gear',value:200,description:'Intact spinning blade from the drone chassis. Violet plasma edge still holds charge.'},{id:'loot_repulsor_coil',name:'Repulsor Coil',type:'gear',iconKind:'gear',value:180,description:'Recovery component. Tech shops pay well for working units.'}] },
  riot_controller:      { credits:[80,140],  itemChance:0.45, items:[{id:'loot_riot_shield',name:'Transparent Riot Shield',type:'gear',iconKind:'supply',value:400,description:'CSF-issue. Reinforced transparisteel. Still projects a half-meter wider than its handle.'},{id:'loot_stun_baton_heavy',name:'Heavy Stun Baton',type:'weapon',iconKind:'gear',value:300,description:'Shock output turned past legal civilian settings.'}] },
  swat_marksman:        { credits:[90,170],  itemChance:0.55, items:[{id:'loot_sniper_harness',name:'Stabilizing Sniper Harness',type:'gear',iconKind:'supply',value:600,description:'Hydraulic arm-mount. Reduces long-range deviation to near-zero.'},{id:'loot_laser_designator',name:'Laser Designator Module',type:'gear',iconKind:'gear',value:450,description:'Projects the red targeting line across six tiles. Intimidation alone pays for it.'}] },
  pyke_sentinel:        { credits:[70,130],  itemChance:0.45, items:[{id:'loot_pyke_vapor_mask',name:'Pyke Vapor Mask',type:'gear',iconKind:'supply',value:320,description:'Sealed breathing unit. Filters raw spice atmosphere. Vents the sickly yellow indicator gas.'},{id:'loot_spice_sample',name:'Raw Spice Sample',type:'consumable',iconKind:'supply',value:400,description:'Unrefined. Highly illegal. The smell alone is distinctive.'}] },
  spice_brute:          { credits:[30,70],   itemChance:0.35, items:[{id:'loot_vibro_cleaver',name:'Jagged Vibro-Cleaver',type:'weapon',iconKind:'gear',value:280,description:'Crude. Heavy. The vibro-edge is still functional despite the damage it took being used as a bludgeon.'},{id:'loot_raw_spice_vial',name:'Raw Spice Vial',type:'consumable',iconKind:'supply',value:500,description:'Distilled directly from the refinery vats. Effects: temporary pain suppression, enhanced aggression, lasting cellular damage.'}] },
  gamorrean_ironclad:   { credits:[100,180], itemChance:0.50, items:[{id:'loot_vibro_axe_head',name:'Vibro-Axe Head',type:'weapon',iconKind:'gear',value:450,description:'Detached from the shaft on impact. The vibro-edge still functions at 40% power.'},{id:'loot_hutt_cartel_seal',name:'Hutt Cartel Seal',type:'quest',iconKind:'keycard',value:600,description:'Authentication token. Grants passage through Hutt-controlled docking bays.'}] },
  rodian_saboteur:      { credits:[80,150],  itemChance:0.55, items:[{id:'loot_det_pack',name:'Thermal Detonator Pack',type:'consumable',iconKind:'supply',value:350,description:'Three live charges in a linked housing. The Rodian had more on order.'},{id:'loot_blast_goggles',name:'Blast-Rated Goggles',type:'gear',iconKind:'gear',value:200,description:'Rated for point-blank thermal bursts. Also useful for dark level navigation.'}] },
  mandalorian_tracker:  { credits:[150,280], itemChance:0.70, items:[{id:'loot_beskar_fragment',name:'Beskar Plate Fragment',type:'gear',iconKind:'supply',value:1200,description:'A section of genuine Mandalorian iron. Almost impossibly valuable for resale or re-forging.'},{id:'loot_jetpack_fuel_cell',name:'Jetpack Fuel Cell',type:'gear',iconKind:'gear',value:500,description:'One full burn charge. Compatible with WESTAR-series propulsion units.'},{id:'loot_westar_blaster',name:'WESTAR Blaster Pistol',type:'weapon',iconKind:'gear',value:700,description:'Mandalorian-issue sidearm. Dual-wield configuration, custom grip worn smooth by long use.'}] },
  ig_assassin_droid:    { credits:[120,240], itemChance:0.65, items:[{id:'loot_ig_targeting_matrix',name:'IG Targeting Matrix',type:'gear',iconKind:'gear',value:800,description:'360-degree optical tracking core. Interfaces with any ranged weapon system. One of the most sought targeting components in the sector.'},{id:'loot_pulse_rifle_barrel',name:'IG Pulse Rifle Barrel',type:'weapon',iconKind:'gear',value:650,description:'Precision-bore barrel from the droid\'s primary weapon. Still rated for sustained fire.'}] },
  sewer_crawler:        { credits:[0,20],    itemChance:0.40, items:[{id:'loot_bioluminescent_fungi',name:'Bioluminescent Fungi Sample',type:'consumable',iconKind:'supply',value:150,description:'Harvested from the creature\'s back. Alchemists and certain Jedi healers pay premium.'},{id:'loot_acid_gland',name:'Acid Gland',type:'consumable',iconKind:'supply',value:220,description:'The sac that produces the corrosive burst on death. Handle with insulated gloves only.'}] },
  desperate_scrapper:   { credits:[25,65],   itemChance:0.30, items:[{id:'loot_mining_laser_core',name:'Mining Laser Core',type:'weapon',iconKind:'gear',value:180,description:'Industrial mining laser, jury-rigged for personal use. Overheats after two shots.'},{id:'loot_scrap_armor_piece',name:'Salvaged Stormtrooper Chestplate',type:'gear',iconKind:'supply',value:120,description:'Cracked white plasteel. Still stops a glancing hit. Better than nothing.'}] },
  // RING LOOT BEGIN
  ring_vaelen_sentinel: { credits:[220,340], itemChance:0.8, items:[{id:'loot_sentinel_gauntlet',name:'Sentinel Gauntlet',type:'gear',iconKind:'gear',value:700,description:'An ivory and gold gauntlet from the old Sentinel plate, older than the contract and in better repair than the man who wore it.'}] },
  ring_sec_riot_squad:  { credits:[90,170], itemChance:0.4, items:[{id:'loot_ring_sec_baton',name:'Ring-Sec Riot Baton',type:'weapon',iconKind:'gear',value:260,description:'A humming riot baton in blue and white Ring Security colors. The charge indicator is set to polite.'}] },
  ring_isb_krennis:     { credits:[320,520], itemChance:0.85, items:[{id:'loot_isb_cipher_gloves',name:'ISB Cipher Gloves',type:'gear',iconKind:'gear',value:900,description:'Fine leather gloves with a code cylinder sewn into the cuff. Krennis will notice they are missing.'}] },
  // RING LOOT END
};

function rollLoot(profileKey) {
  const table = COMBAT_LOOT[profileKey] || COMBAT_LOOT.syndicate_thug;
  const credits = table.credits[0] + Math.floor(Math.random() * (table.credits[1] - table.credits[0]));
  const item = Math.random() < table.itemChance ? { ...table.items[Math.floor(Math.random() * table.items.length)], qty: 1 } : null;
  return { credits, item };
}

function genCombatMap() {
  const GW = 8, GH = 6;
  const cover = Array.from({ length: GH }, () => Array(GW).fill(0));
  const objs = [];
  const taken = new Set(['2,1', '2,6', '1,1', '3,1', '1,6', '3,6']);
  function tryPlace(r, c, val) {
    const k = `${r},${c}`;
    if (r < 0 || r >= GH || c < 0 || c >= GW || taken.has(k)) return false;
    cover[r][c] = val; taken.add(k); return true;
  }
  const fullTarget = 2 + Math.floor(Math.random() * 2);
  for (let a = 0; a < 40 && cover.flat().filter(v => v === 2).length < fullTarget; a++) tryPlace(Math.floor(Math.random() * GH), 2 + Math.floor(Math.random() * 4), 2);
  const halfTarget = 3 + Math.floor(Math.random() * 3);
  for (let a = 0; a < 50 && cover.flat().filter(v => v === 1).length < halfTarget; a++) tryPlace(Math.floor(Math.random() * GH), 1 + Math.floor(Math.random() * 6), 1);
  const candidates = [];
  for (let r = 0; r < GH; r++) for (let c = 1; c < GW - 1; c++) { if (!taken.has(`${r},${c}`) && cover[r][c] === 0) candidates.push([r, c]); }
  candidates.sort(() => Math.random() - 0.5);
  const plan = ['barrel'];
  if (Math.random() > 0.35) plan.push('barrel');
  if (Math.random() > 0.45) plan.push('steam');
  if (Math.random() > 0.5)  plan.push('turret');
  plan.forEach((type, i) => {
    if (i < candidates.length) { const [r, c] = candidates[i]; taken.add(`${r},${c}`); objs.push({ id: `o${i}`, type, row: r, col: c, active: true, ...(type === 'turret' ? { hacked: false } : {}) }); }
  });
  return { cover, objs };
}

function PlayerSprite({ w = 48, h = 64 }) {
  return (
    <svg width={w} height={h} viewBox="0 0 20 32" style={{ imageRendering: 'pixelated', display: 'block' }}>
      {/* Hair */}
      <rect x="6" y="0" width="8" height="2" fill="#1A0E05"/>
      <rect x="5" y="1" width="10" height="1" fill="#1A0E05"/>
      {/* Face */}
      <rect x="6" y="2" width="8" height="4" fill="#C8956A"/>
      {/* Eyes */}
      <rect x="7" y="3" width="2" height="1" fill="#2A2040"/>
      <rect x="11" y="3" width="2" height="1" fill="#2A2040"/>
      {/* Nose shadow */}
      <rect x="9" y="4" width="2" height="1" fill="#B07848"/>
      {/* Mouth / jaw */}
      <rect x="8" y="5" width="4" height="1" fill="#9A6040"/>
      {/* Neck */}
      <rect x="8" y="6" width="4" height="2" fill="#C8956A"/>
      {/* Collar */}
      <rect x="6" y="6" width="2" height="1" fill="#0E0E1A"/>
      <rect x="12" y="6" width="2" height="1" fill="#0E0E1A"/>
      {/* Tactical vest / torso */}
      <rect x="3" y="8" width="14" height="11" fill="#0E0E1A"/>
      {/* Chest inner panel */}
      <rect x="6" y="9" width="8" height="7" fill="#1A1A2A"/>
      {/* Chest rib lines */}
      <rect x="7" y="10" width="6" height="1" fill="#252535"/>
      <rect x="7" y="12" width="6" height="1" fill="#252535"/>
      {/* Badge left breast */}
      <rect x="4" y="10" width="2" height="1" fill="#4A9FFF"/>
      <rect x="4" y="11" width="2" height="1" fill="#2A7ADF"/>
      {/* Utility pouches on sides */}
      <rect x="3" y="14" width="2" height="3" fill="#252535"/>
      <rect x="15" y="14" width="2" height="3" fill="#252535"/>
      {/* Belt */}
      <rect x="3" y="19" width="14" height="2" fill="#3A2A1A"/>
      <rect x="9" y="19" width="2" height="2" fill="#5A4A2A"/>
      {/* Left arm */}
      <rect x="1" y="8" width="3" height="8" fill="#0E0E1A"/>
      {/* Left hand */}
      <rect x="0" y="15" width="3" height="3" fill="#C8956A"/>
      {/* Right arm */}
      <rect x="16" y="8" width="3" height="8" fill="#0E0E1A"/>
      {/* Blaster (right hand) */}
      <rect x="17" y="13" width="3" height="2" fill="#888"/>
      <rect x="19" y="11" width="1" height="4" fill="#666"/>
      {/* Left leg */}
      <rect x="3" y="21" width="6" height="7" fill="#1A1A2A"/>
      {/* Right leg */}
      <rect x="11" y="21" width="6" height="7" fill="#1A1A2A"/>
      {/* Left boot */}
      <rect x="3" y="28" width="6" height="4" fill="#0A0808"/>
      <rect x="3" y="28" width="6" height="1" fill="#1A1510"/>
      {/* Right boot */}
      <rect x="11" y="28" width="6" height="4" fill="#0A0808"/>
      <rect x="11" y="28" width="6" height="1" fill="#1A1510"/>
    </svg>
  );
}

function EnemySprite({ kind, w = 48, h = 64 }) {
  const vb = '0 0 20 32';
  const px = { imageRendering: 'pixelated', display: 'block' };

  if (kind === 'csf_swat') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Helmet shell */}
      <rect x="5" y="0" width="10" height="7" fill="#1E3A6A"/>
      {/* Full visor glow */}
      <rect x="6" y="2" width="8" height="3" fill="#4A9FFF"/>
      <rect x="7" y="3" width="6" height="1" fill="#8ACFFF"/>
      <rect x="5" y="6" width="10" height="1" fill="#0A1428"/>
      {/* Neck collar */}
      <rect x="7" y="7" width="6" height="1" fill="#1E3A6A"/>
      {/* Shoulder pauldrons */}
      <rect x="1" y="8" width="5" height="3" fill="#2A4A7A"/>
      <rect x="14" y="8" width="5" height="3" fill="#2A4A7A"/>
      {/* Torso plate armor */}
      <rect x="3" y="8" width="14" height="12" fill="#1E3A6A"/>
      <rect x="5" y="9" width="10" height="8" fill="#0A1428"/>
      <rect x="6" y="10" width="8" height="3" fill="#1A2A4A"/>
      <rect x="6" y="14" width="8" height="2" fill="#1A2A4A"/>
      {/* CSF badge */}
      <rect x="7" y="11" width="2" height="1" fill="#4A9FFF"/>
      {/* Belt */}
      <rect x="3" y="20" width="14" height="2" fill="#0A1020"/>
      {/* Arms */}
      <rect x="1" y="11" width="3" height="7" fill="#1E3A6A"/>
      <rect x="16" y="11" width="3" height="7" fill="#1E3A6A"/>
      {/* Gloves */}
      <rect x="1" y="18" width="3" height="2" fill="#0A1020"/>
      <rect x="16" y="18" width="3" height="2" fill="#0A1020"/>
      {/* Blaster rifle */}
      <rect x="17" y="14" width="3" height="2" fill="#777"/>
      <rect x="19" y="12" width="1" height="4" fill="#555"/>
      {/* Legs */}
      <rect x="3" y="22" width="6" height="6" fill="#1E3A6A"/>
      <rect x="11" y="22" width="6" height="6" fill="#1E3A6A"/>
      {/* Kneepads */}
      <rect x="3" y="24" width="6" height="2" fill="#2A4A7A"/>
      <rect x="11" y="24" width="6" height="2" fill="#2A4A7A"/>
      {/* Boots */}
      <rect x="3" y="28" width="6" height="4" fill="#0A1020"/>
      <rect x="11" y="28" width="6" height="4" fill="#0A1020"/>
    </svg>;
  }

  if (kind === 'csf_scout') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Scout cap */}
      <rect x="6" y="0" width="8" height="2" fill="#1E3A6A"/>
      {/* Half visor */}
      <rect x="6" y="1" width="8" height="2" fill="#2A5A8A"/>
      <rect x="7" y="2" width="6" height="1" fill="#6ABFFF"/>
      {/* Face exposed lower half */}
      <rect x="7" y="3" width="6" height="3" fill="#C8956A"/>
      <rect x="8" y="3" width="1" height="1" fill="#2A2040"/>
      <rect x="11" y="3" width="1" height="1" fill="#2A2040"/>
      {/* Comm unit on jaw */}
      <rect x="12" y="5" width="2" height="1" fill="#6ABFFF"/>
      {/* Neck */}
      <rect x="8" y="6" width="4" height="2" fill="#C8956A"/>
      {/* Light armor vest */}
      <rect x="4" y="8" width="12" height="10" fill="#2A5A8A"/>
      <rect x="6" y="9" width="8" height="7" fill="#1E3A6A"/>
      <rect x="7" y="10" width="6" height="1" fill="#2A5A8A"/>
      <rect x="7" y="12" width="6" height="1" fill="#2A5A8A"/>
      {/* Lighter shoulder pads */}
      <rect x="2" y="8" width="3" height="2" fill="#2A5A8A"/>
      <rect x="15" y="8" width="3" height="2" fill="#2A5A8A"/>
      {/* Belt */}
      <rect x="4" y="18" width="12" height="2" fill="#0A1020"/>
      {/* Arms */}
      <rect x="2" y="9" width="3" height="7" fill="#2A5A8A"/>
      <rect x="15" y="9" width="3" height="7" fill="#2A5A8A"/>
      {/* Pistol (lighter scout loadout) */}
      <rect x="17" y="12" width="3" height="2" fill="#888"/>
      {/* Legs */}
      <rect x="4" y="20" width="5" height="8" fill="#1E3A6A"/>
      <rect x="11" y="20" width="5" height="8" fill="#1E3A6A"/>
      {/* Boots */}
      <rect x="4" y="28" width="5" height="4" fill="#0A1020"/>
      <rect x="11" y="28" width="5" height="4" fill="#0A1020"/>
    </svg>;
  }

  if (kind === 'black_sun_striker') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Balaclava head, partial face exposed */}
      <rect x="6" y="0" width="8" height="7" fill="#080808"/>
      {/* Face */}
      <rect x="7" y="2" width="6" height="3" fill="#7A5040"/>
      <rect x="8" y="3" width="2" height="1" fill="#1A1030"/>
      <rect x="11" y="3" width="2" height="1" fill="#1A1030"/>
      {/* Red Black Sun tattoo lines at jaw */}
      <rect x="7" y="6" width="2" height="1" fill="#C03030"/>
      <rect x="11" y="6" width="2" height="1" fill="#C03030"/>
      {/* Black combat jacket */}
      <rect x="3" y="7" width="14" height="12" fill="#080808"/>
      {/* Red gang marking on chest */}
      <rect x="9" y="8" width="2" height="8" fill="#8B0000"/>
      <rect x="7" y="11" width="6" height="1" fill="#8B0000"/>
      {/* Weapon side holster */}
      <rect x="3" y="16" width="3" height="3" fill="#333"/>
      {/* Belt */}
      <rect x="3" y="19" width="14" height="2" fill="#1A0808"/>
      {/* Arms */}
      <rect x="1" y="7" width="3" height="9" fill="#080808"/>
      <rect x="16" y="7" width="3" height="9" fill="#080808"/>
      {/* Blaster raised right hand */}
      <rect x="17" y="12" width="3" height="2" fill="#666"/>
      <rect x="19" y="10" width="1" height="4" fill="#444"/>
      {/* Legs */}
      <rect x="3" y="21" width="6" height="7" fill="#080808"/>
      <rect x="11" y="21" width="6" height="7" fill="#080808"/>
      {/* Red stripe boots */}
      <rect x="3" y="28" width="6" height="4" fill="#050505"/>
      <rect x="3" y="28" width="6" height="1" fill="#8B0000"/>
      <rect x="11" y="28" width="6" height="4" fill="#050505"/>
      <rect x="11" y="28" width="6" height="1" fill="#8B0000"/>
    </svg>;
  }

  if (kind === 'black_sun_vigo_guard') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Heavy armored helmet */}
      <rect x="5" y="0" width="10" height="7" fill="#0A0808"/>
      <rect x="6" y="2" width="8" height="2" fill="#4A0808"/>
      <rect x="7" y="3" width="6" height="1" fill="#8B0000"/>
      {/* Face plate cutouts (eyes) */}
      <rect x="7" y="4" width="2" height="2" fill="#1A0808"/>
      <rect x="11" y="4" width="2" height="2" fill="#1A0808"/>
      {/* Wide armored shoulders */}
      <rect x="0" y="7" width="5" height="5" fill="#0A0808"/>
      <rect x="15" y="7" width="5" height="5" fill="#0A0808"/>
      <rect x="1" y="7" width="3" height="2" fill="#1A0808"/>
      <rect x="16" y="7" width="3" height="2" fill="#1A0808"/>
      {/* Gold trim on shoulder tops */}
      <rect x="0" y="7" width="5" height="1" fill="#C8A000"/>
      <rect x="15" y="7" width="5" height="1" fill="#C8A000"/>
      {/* Heavy chest plate */}
      <rect x="3" y="7" width="14" height="13" fill="#0A0808"/>
      <rect x="5" y="8" width="10" height="9" fill="#111"/>
      {/* Black Sun faction sigil */}
      <rect x="9" y="9" width="2" height="6" fill="#8B0000"/>
      <rect x="7" y="12" width="6" height="1" fill="#8B0000"/>
      {/* Gold trim stripe on armor */}
      <rect x="3" y="7" width="14" height="1" fill="#C8A000"/>
      <rect x="0" y="12" width="20" height="1" fill="#C8A000"/>
      {/* Belt */}
      <rect x="3" y="20" width="14" height="2" fill="#1A0808"/>
      {/* Arms */}
      <rect x="1" y="12" width="3" height="6" fill="#0A0808"/>
      <rect x="16" y="12" width="3" height="6" fill="#0A0808"/>
      {/* Blaster */}
      <rect x="17" y="14" width="3" height="2" fill="#666"/>
      {/* Legs greaves */}
      <rect x="3" y="22" width="6" height="6" fill="#0A0808"/>
      <rect x="11" y="22" width="6" height="6" fill="#0A0808"/>
      {/* Gold boot trim */}
      <rect x="3" y="28" width="6" height="4" fill="#050505"/>
      <rect x="3" y="28" width="6" height="1" fill="#C8A000"/>
      <rect x="11" y="28" width="6" height="4" fill="#050505"/>
      <rect x="11" y="28" width="6" height="1" fill="#C8A000"/>
    </svg>;
  }

  if (kind === 'exchange_bounty_hunter') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Bounty hunter helmet, T-visor */}
      <rect x="5" y="0" width="10" height="7" fill="#2A2A3A"/>
      <rect x="6" y="2" width="8" height="2" fill="#9B59B6"/>
      <rect x="7" y="3" width="6" height="1" fill="#C07AE6"/>
      <rect x="5" y="5" width="10" height="1" fill="#1A1A2A"/>
      {/* Rangefinder */}
      <rect x="14" y="0" width="2" height="3" fill="#555"/>
      <rect x="15" y="2" width="1" height="1" fill="#9B59B6"/>
      {/* Neck */}
      <rect x="7" y="7" width="6" height="1" fill="#2A2A3A"/>
      {/* Beskar chest plate */}
      <rect x="4" y="8" width="12" height="11" fill="#2A2A3A"/>
      <rect x="6" y="9" width="8" height="8" fill="#1A1A2A"/>
      {/* Exchange purple cross markings */}
      <rect x="9" y="9" width="2" height="7" fill="#9B59B6"/>
      <rect x="7" y="12" width="6" height="1" fill="#9B59B6"/>
      {/* Shoulder pauldrons with purple marks */}
      <rect x="1" y="8" width="4" height="4" fill="#2A2A3A"/>
      <rect x="15" y="8" width="4" height="4" fill="#2A2A3A"/>
      <rect x="2" y="8" width="2" height="1" fill="#9B59B6"/>
      <rect x="16" y="8" width="2" height="1" fill="#9B59B6"/>
      {/* Belt / ammo pack */}
      <rect x="4" y="19" width="12" height="2" fill="#1A1A2A"/>
      <rect x="8" y="19" width="4" height="2" fill="#555"/>
      {/* Arms */}
      <rect x="1" y="12" width="3" height="6" fill="#2A2A3A"/>
      <rect x="16" y="12" width="3" height="6" fill="#2A2A3A"/>
      {/* Large blaster carbine */}
      <rect x="17" y="13" width="3" height="2" fill="#888"/>
      <rect x="19" y="11" width="1" height="5" fill="#666"/>
      {/* Legs */}
      <rect x="4" y="21" width="5" height="7" fill="#2A2A3A"/>
      <rect x="11" y="21" width="5" height="7" fill="#2A2A3A"/>
      {/* Purple stripe boots */}
      <rect x="4" y="28" width="5" height="4" fill="#1A1A2A"/>
      <rect x="4" y="28" width="5" height="1" fill="#9B59B6"/>
      <rect x="11" y="28" width="5" height="4" fill="#1A1A2A"/>
      <rect x="11" y="28" width="5" height="1" fill="#9B59B6"/>
    </svg>;
  }

  if (kind === 'anzati_assassin') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Wide enveloping hood */}
      <rect x="3" y="0" width="14" height="8" fill="#080808"/>
      <rect x="5" y="1" width="10" height="5" fill="#101010"/>
      {/* Pallid near-human face */}
      <rect x="6" y="3" width="8" height="5" fill="#D4D0CC"/>
      {/* Amber eyes - the only color */}
      <rect x="7" y="4" width="2" height="2" fill="#C8A000"/>
      <rect x="8" y="4" width="1" height="1" fill="#FFD040"/>
      <rect x="11" y="4" width="2" height="2" fill="#C8A000"/>
      <rect x="11" y="4" width="1" height="1" fill="#FFD040"/>
      {/* Featureless lower face, proboscides tucked */}
      <rect x="7" y="6" width="6" height="2" fill="#C0BCA8"/>
      <rect x="8" y="7" width="1" height="1" fill="#A8A498"/>
      <rect x="11" y="7" width="1" height="1" fill="#A8A498"/>
      {/* Dark flowing cloak body */}
      <rect x="2" y="8" width="16" height="14" fill="#080808"/>
      <rect x="4" y="9" width="12" height="11" fill="#101010"/>
      {/* Cloak fold shadow lines */}
      <rect x="6" y="9" width="1" height="10" fill="#080808"/>
      <rect x="10" y="9" width="1" height="10" fill="#080808"/>
      <rect x="14" y="9" width="1" height="10" fill="#080808"/>
      {/* Hidden blades barely visible at sides */}
      <rect x="2" y="14" width="2" height="6" fill="#555"/>
      <rect x="16" y="14" width="2" height="6" fill="#555"/>
      {/* Legs hidden under cloak */}
      <rect x="2" y="22" width="16" height="10" fill="#080808"/>
      <rect x="5" y="22" width="4" height="10" fill="#0A0A0A"/>
      <rect x="11" y="22" width="4" height="10" fill="#0A0A0A"/>
      {/* Foot shadows */}
      <rect x="5" y="30" width="4" height="2" fill="#050505"/>
      <rect x="11" y="30" width="4" height="2" fill="#050505"/>
    </svg>;
  }

  if (kind === 'malak_enforcer') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Bald skull */}
      <rect x="6" y="0" width="8" height="4" fill="#8A6A58"/>
      <rect x="5" y="1" width="10" height="3" fill="#8A6A58"/>
      {/* Sith-touched red eyes */}
      <rect x="7" y="2" width="2" height="1" fill="#FF2020"/>
      <rect x="8" y="2" width="1" height="1" fill="#FF6040"/>
      <rect x="11" y="2" width="2" height="1" fill="#FF2020"/>
      <rect x="12" y="2" width="1" height="1" fill="#FF6040"/>
      {/* Metal jaw plate - the iconic Malak detail */}
      <rect x="5" y="4" width="10" height="3" fill="#7A8A9A"/>
      <rect x="6" y="5" width="8" height="2" fill="#8A9AAA"/>
      <rect x="6" y="5" width="1" height="1" fill="#5A6A7A"/>
      <rect x="9" y="5" width="1" height="1" fill="#5A6A7A"/>
      <rect x="13" y="5" width="1" height="1" fill="#5A6A7A"/>
      <rect x="7" y="4" width="6" height="1" fill="#6A7A8A"/>
      {/* Massive shoulder armor */}
      <rect x="0" y="7" width="6" height="6" fill="#0A0A0A"/>
      <rect x="14" y="7" width="6" height="6" fill="#0A0A0A"/>
      <rect x="1" y="8" width="4" height="1" fill="#1A0808"/>
      <rect x="15" y="8" width="4" height="1" fill="#1A0808"/>
      {/* Heavy Sith chest armor */}
      <rect x="3" y="7" width="14" height="13" fill="#0A0A0A"/>
      <rect x="5" y="8" width="10" height="10" fill="#111"/>
      {/* Red energy vein lines */}
      <rect x="9" y="8" width="2" height="10" fill="#C03030"/>
      <rect x="9" y="9" width="2" height="1" fill="#FF4040"/>
      <rect x="9" y="12" width="2" height="1" fill="#FF4040"/>
      <rect x="5" y="11" width="10" height="1" fill="#8B0000"/>
      <rect x="5" y="14" width="10" height="1" fill="#8B0000"/>
      {/* Sternum node glow */}
      <rect x="9" y="11" width="2" height="1" fill="#FF6060"/>
      {/* Belt */}
      <rect x="3" y="20" width="14" height="2" fill="#0A0A0A"/>
      <rect x="7" y="20" width="6" height="2" fill="#8B0000"/>
      {/* Gauntlets */}
      <rect x="1" y="13" width="3" height="5" fill="#0A0A0A"/>
      <rect x="16" y="13" width="3" height="5" fill="#0A0A0A"/>
      <rect x="0" y="18" width="4" height="3" fill="#555"/>
      <rect x="16" y="18" width="4" height="3" fill="#555"/>
      {/* Legs wide stance */}
      <rect x="2" y="22" width="7" height="6" fill="#0A0A0A"/>
      <rect x="11" y="22" width="7" height="6" fill="#0A0A0A"/>
      {/* Red energy vein on legs */}
      <rect x="5" y="22" width="1" height="6" fill="#8B0000"/>
      <rect x="14" y="22" width="1" height="6" fill="#8B0000"/>
      {/* Boots */}
      <rect x="2" y="28" width="7" height="4" fill="#050508"/>
      <rect x="11" y="28" width="7" height="4" fill="#050508"/>
    </svg>;
  }

  if (kind === 'jon_vane') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Slicked hair, grey temples */}
      <rect x="6" y="0" width="8" height="2" fill="#1A0E05"/>
      <rect x="5" y="1" width="10" height="1" fill="#1A0E05"/>
      <rect x="14" y="1" width="2" height="2" fill="#8A8088"/>
      {/* Face - sharp, calculating */}
      <rect x="6" y="2" width="8" height="5" fill="#C8956A"/>
      <rect x="7" y="3" width="2" height="1" fill="#1A1030"/>
      <rect x="11" y="3" width="2" height="1" fill="#1A1030"/>
      {/* Strong jaw, stubble shadow */}
      <rect x="7" y="6" width="6" height="1" fill="#A07040"/>
      <rect x="8" y="6" width="4" height="1" fill="#9A6A38"/>
      {/* Neck */}
      <rect x="8" y="7" width="4" height="1" fill="#C8956A"/>
      {/* Gold-trimmed dark suit jacket */}
      <rect x="3" y="8" width="14" height="12" fill="#080808"/>
      {/* Suit lapels */}
      <rect x="8" y="8" width="4" height="10" fill="#1A1A1A"/>
      {/* Gold trim on lapels */}
      <rect x="7" y="8" width="1" height="10" fill="#C8A000"/>
      <rect x="12" y="8" width="1" height="10" fill="#C8A000"/>
      {/* White shirt */}
      <rect x="9" y="9" width="2" height="7" fill="#E8E0CC"/>
      {/* Gold buttons */}
      <rect x="9" y="10" width="2" height="1" fill="#C8A000"/>
      <rect x="9" y="13" width="2" height="1" fill="#C8A000"/>
      {/* Gold shoulder trim */}
      <rect x="3" y="8" width="14" height="1" fill="#C8A000"/>
      {/* Belt - fine leather */}
      <rect x="3" y="20" width="14" height="2" fill="#4A3010"/>
      <rect x="9" y="20" width="2" height="2" fill="#C8A000"/>
      {/* Arms */}
      <rect x="1" y="8" width="3" height="9" fill="#080808"/>
      <rect x="16" y="8" width="3" height="9" fill="#080808"/>
      {/* Gold cufflinks */}
      <rect x="1" y="17" width="3" height="1" fill="#C8A000"/>
      <rect x="16" y="17" width="3" height="1" fill="#C8A000"/>
      {/* Merr-Sonn blaster with cortosis grip */}
      <rect x="17" y="12" width="3" height="2" fill="#8A8878"/>
      <rect x="19" y="10" width="1" height="4" fill="#6A6858"/>
      <rect x="18" y="13" width="2" height="1" fill="#C8A000"/>
      {/* Trousers with gold stripe */}
      <rect x="3" y="22" width="6" height="6" fill="#1A1010"/>
      <rect x="11" y="22" width="6" height="6" fill="#1A1010"/>
      <rect x="6" y="22" width="1" height="6" fill="#C8A000"/>
      <rect x="13" y="22" width="1" height="6" fill="#C8A000"/>
      {/* Polished boots */}
      <rect x="3" y="28" width="6" height="4" fill="#1A0808"/>
      <rect x="3" y="28" width="6" height="1" fill="#3A2818"/>
      <rect x="11" y="28" width="6" height="4" fill="#1A0808"/>
      <rect x="11" y="28" width="6" height="1" fill="#3A2818"/>
    </svg>;
  }

  if (kind === 'kesh_sith') {
    return <svg width={w} height={h} viewBox={vb} style={px}>
      {/* Sith hood, partially concealing */}
      <rect x="4" y="0" width="12" height="5" fill="#0A0808"/>
      <rect x="5" y="1" width="10" height="3" fill="#111"/>
      {/* Pallid Sith-corrupted face */}
      <rect x="6" y="2" width="8" height="5" fill="#D0C8BC"/>
      {/* Bright red Sith eyes */}
      <rect x="7" y="3" width="2" height="2" fill="#FF2020"/>
      <rect x="8" y="3" width="1" height="1" fill="#FF6060"/>
      <rect x="11" y="3" width="2" height="2" fill="#FF2020"/>
      <rect x="11" y="3" width="1" height="1" fill="#FF6060"/>
      {/* Dark side vein corruption on face */}
      <rect x="7" y="5" width="1" height="2" fill="#5A1010"/>
      <rect x="12" y="5" width="1" height="2" fill="#5A1010"/>
      {/* Layered Sith robes */}
      <rect x="2" y="7" width="16" height="14" fill="#0A0808"/>
      <rect x="4" y="8" width="12" height="12" fill="#111"/>
      <rect x="5" y="9" width="10" height="10" fill="#0D0808"/>
      {/* Robe fold lines */}
      <rect x="7" y="9" width="1" height="10" fill="#0A0808"/>
      <rect x="12" y="9" width="1" height="10" fill="#0A0808"/>
      {/* Red sash belt */}
      <rect x="4" y="16" width="12" height="2" fill="#8B0000"/>
      <rect x="4" y="17" width="12" height="1" fill="#C03030"/>
      {/* Arms in robes */}
      <rect x="1" y="8" width="4" height="9" fill="#0A0808"/>
      <rect x="15" y="8" width="4" height="9" fill="#0A0808"/>
      {/* Red lightsaber blade */}
      <rect x="17" y="3" width="2" height="5" fill="#5A4040"/>
      <rect x="17" y="8" width="2" height="8" fill="#C03030"/>
      <rect x="18" y="8" width="1" height="7" fill="#FF4040"/>
      {/* Legs under robes */}
      <rect x="4" y="21" width="5" height="11" fill="#0A0808"/>
      <rect x="11" y="21" width="5" height="11" fill="#0A0808"/>
      {/* Boot tips */}
      <rect x="4" y="29" width="5" height="3" fill="#050508"/>
      <rect x="11" y="29" width="5" height="3" fill="#050508"/>
    </svg>;
  }


  if (kind === 'vigo_vanguard') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Heavy ceremonial helmet - deep indigo, gold filigree band */}
      <rect x="5" y="0" width="10" height="7" fill="#0D1433"/>
      <rect x="6" y="1" width="8" height="5" fill="#131A3D"/>
      {/* Gold filigree trim band across helmet brow */}
      <rect x="5" y="1" width="10" height="1" fill="#C8A000"/>
      <rect x="6" y="2" width="8" height="1" fill="#FFD040"/>
      {/* Visor slit - bright blue */}
      <rect x="7" y="3" width="6" height="1" fill="#4A9FFF"/>
      <rect x="8" y="3" width="4" height="1" fill="#8ACFFF"/>
      {/* Cheek plates */}
      <rect x="5" y="5" width="2" height="2" fill="#0D1433"/>
      <rect x="13" y="5" width="2" height="2" fill="#0D1433"/>
      {/* Neck gorget - gold trimmed */}
      <rect x="7" y="7" width="6" height="2" fill="#131A3D"/>
      <rect x="7" y="7" width="6" height="1" fill="#C8A000"/>
      {/* Wide shoulder pauldrons - asymmetric (cape side wider) */}
      <rect x="0" y="7" width="5" height="4" fill="#0D1433"/>
      <rect x="15" y="7" width="5" height="4" fill="#0D1433"/>
      <rect x="0" y="7" width="5" height="1" fill="#C8A000"/>
      <rect x="15" y="7" width="5" height="1" fill="#C8A000"/>
      {/* Gold filigree lines on pauldrons */}
      <rect x="1" y="9" width="3" height="1" fill="#C8A000"/>
      <rect x="16" y="9" width="3" height="1" fill="#C8A000"/>
      {/* Ceremonial chest plate - indigo with gold cross motif */}
      <rect x="3" y="8" width="14" height="12" fill="#0D1433"/>
      <rect x="5" y="9" width="10" height="9" fill="#131A3D"/>
      {/* Gold filigree cross on chest */}
      <rect x="9" y="9" width="2" height="9" fill="#C8A000"/>
      <rect x="5" y="12" width="10" height="2" fill="#C8A000"/>
      <rect x="9" y="10" width="2" height="1" fill="#FFD040"/>
      <rect x="9" y="13" width="2" height="1" fill="#FFD040"/>
      {/* Gold trim on chest top */}
      <rect x="3" y="8" width="14" height="1" fill="#C8A000"/>
      {/* The half-cape (left shoulder only) - deep indigo, Falepian-leather */}
      <rect x="0" y="7" width="3" height="20" fill="#090F26"/>
      <rect x="0" y="8" width="2" height="18" fill="#0D1433"/>
      <rect x="0" y="7" width="1" height="1" fill="#C8A000"/>
      {/* Belt - gold clasp */}
      <rect x="3" y="20" width="14" height="2" fill="#090F26"/>
      <rect x="8" y="20" width="4" height="2" fill="#C8A000"/>
      {/* Arms - right arm exposed, left under cape */}
      <rect x="1" y="11" width="2" height="6" fill="#090F26"/>
      <rect x="16" y="11" width="3" height="6" fill="#0D1433"/>
      {/* Gauntlets */}
      <rect x="1" y="17" width="2" height="2" fill="#131A3D"/>
      <rect x="16" y="17" width="3" height="2" fill="#131A3D"/>
      {/* Hold-out blaster (right hand) */}
      <rect x="17" y="15" width="3" height="2" fill="#555"/>
      <rect x="19" y="13" width="1" height="4" fill="#444"/>
      {/* Greaves - leg armor */}
      <rect x="3" y="22" width="6" height="6" fill="#0D1433"/>
      <rect x="11" y="22" width="6" height="6" fill="#0D1433"/>
      <rect x="3" y="22" width="6" height="1" fill="#C8A000"/>
      <rect x="11" y="22" width="6" height="1" fill="#C8A000"/>
      {/* Sabatons - polished boots */}
      <rect x="3" y="28" width="6" height="4" fill="#080808"/>
      <rect x="3" y="28" width="6" height="1" fill="#C8A000"/>
      <rect x="11" y="28" width="6" height="4" fill="#080808"/>
      <rect x="11" y="28" width="6" height="1" fill="#C8A000"/>
    </svg>;
  }

  if (kind === 'falleen_taskmaster') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Falleen head - elongated, slightly ridged scalp */}
      <rect x="6" y="0" width="8" height="2" fill="#1E5C38"/>
      <rect x="5" y="1" width="10" height="6" fill="#2ECC71"/>
      <rect x="6" y="1" width="8" height="5" fill="#27AE60"/>
      {/* Brow ridge */}
      <rect x="5" y="2" width="2" height="1" fill="#1E5C38"/>
      <rect x="13" y="2" width="2" height="1" fill="#1E5C38"/>
      {/* Dark eyes - calculating */}
      <rect x="7" y="3" width="2" height="1" fill="#111"/>
      <rect x="11" y="3" width="2" height="1" fill="#111"/>
      {/* Faint scale pattern - highlight dots */}
      <rect x="8" y="2" width="1" height="1" fill="#3EE080"/>
      <rect x="11" y="2" width="1" height="1" fill="#3EE080"/>
      <rect x="7" y="5" width="1" height="1" fill="#3EE080"/>
      <rect x="12" y="5" width="1" height="1" fill="#3EE080"/>
      {/* Narrow mouth, reptilian */}
      <rect x="8" y="5" width="4" height="1" fill="#165C38"/>
      {/* High-collared tunic - rich burgundy */}
      <rect x="5" y="7" width="10" height="3" fill="#6B0F1A"/>
      <rect x="6" y="7" width="8" height="2" fill="#8B1A2A"/>
      {/* Collar stand */}
      <rect x="7" y="6" width="6" height="1" fill="#6B0F1A"/>
      {/* Torso - elegant layered robes */}
      <rect x="4" y="10" width="12" height="10" fill="#8B1A2A"/>
      <rect x="5" y="10" width="10" height="9" fill="#6B0F1A"/>
      <rect x="7" y="11" width="6" height="7" fill="#5A0A16"/>
      {/* Gold trim on robe edges */}
      <rect x="4" y="10" width="1" height="9" fill="#C8A000"/>
      <rect x="15" y="10" width="1" height="9" fill="#C8A000"/>
      {/* Hold-out blaster (left hand, subtle) */}
      <rect x="3" y="14" width="2" height="2" fill="#666"/>
      <rect x="2" y="13" width="1" height="3" fill="#444"/>
      {/* Wrist stim-injector (right wrist) - chrome, blue vial */}
      <rect x="16" y="16" width="3" height="2" fill="#888"/>
      <rect x="17" y="15" width="2" height="1" fill="#4A9FFF"/>
      <rect x="17" y="15" width="1" height="1" fill="#8ACFFF"/>
      {/* Pheromone vent slots at collar (Emission Layer sim) */}
      <rect x="7" y="8" width="2" height="1" fill="#3EE080"/>
      <rect x="11" y="8" width="2" height="1" fill="#3EE080"/>
      {/* Belt - dark, understated */}
      <rect x="4" y="19" width="12" height="2" fill="#3A0808"/>
      {/* Flowing robe lower half */}
      <rect x="4" y="21" width="12" height="7" fill="#5A0A16"/>
      <rect x="5" y="21" width="10" height="6" fill="#4A0810"/>
      {/* Boot tips beneath robe */}
      <rect x="5" y="28" width="4" height="4" fill="#1A0808"/>
      <rect x="11" y="28" width="4" height="4" fill="#1A0808"/>
    </svg>;
  }

  if (kind === 'cyborg_splicer') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Emaciated skull - barely any flesh, rusted copper cyber-skull cap */}
      <rect x="6" y="0" width="8" height="3" fill="#7A5A3A"/>
      <rect x="5" y="1" width="10" height="5" fill="#C8956A"/>
      {/* Bulky cranial port array - rust copper plates */}
      <rect x="4" y="0" width="3" height="4" fill="#8A5A30"/>
      <rect x="13" y="0" width="3" height="4" fill="#8A5A30"/>
      <rect x="4" y="1" width="2" height="2" fill="#C87840"/>
      <rect x="14" y="1" width="2" height="2" fill="#C87840"/>
      {/* Neon-green optical sensors (Emission Layer) */}
      <rect x="7" y="2" width="2" height="2" fill="#00FF80"/>
      <rect x="8" y="2" width="1" height="1" fill="#80FFB0"/>
      <rect x="11" y="2" width="2" height="2" fill="#00FF80"/>
      <rect x="11" y="2" width="1" height="1" fill="#80FFB0"/>
      {/* Dangling wire from left port */}
      <rect x="4" y="4" width="1" height="6" fill="#555"/>
      <rect x="4" y="8" width="1" height="1" fill="#00FF80"/>
      {/* Gaunt neck with visible cybernetic collar ring */}
      <rect x="8" y="6" width="4" height="2" fill="#C8956A"/>
      <rect x="7" y="7" width="6" height="1" fill="#8A5A30"/>
      {/* Patchwork jacket - torn, copper/grey */}
      <rect x="3" y="8" width="14" height="10" fill="#3A3228"/>
      <rect x="5" y="9" width="10" height="8" fill="#2A2218"/>
      {/* Exposed ribs on left side through torn jacket */}
      <rect x="3" y="10" width="3" height="5" fill="#1A1208"/>
      <rect x="4" y="11" width="1" height="1" fill="#C8956A"/>
      <rect x="4" y="13" width="1" height="1" fill="#C8956A"/>
      {/* Crude cybernetic left arm - bolted on */}
      <rect x="0" y="8" width="4" height="10" fill="#8A5A30"/>
      <rect x="1" y="9" width="2" height="8" fill="#C87840"/>
      {/* Bolt detail on cyber arm */}
      <rect x="1" y="10" width="1" height="1" fill="#E8A060"/>
      <rect x="1" y="13" width="1" height="1" fill="#E8A060"/>
      <rect x="1" y="16" width="1" height="1" fill="#E8A060"/>
      {/* Cable wrist - extends for hacking */}
      <rect x="0" y="18" width="3" height="2" fill="#555"/>
      <rect x="0" y="19" width="1" height="1" fill="#00FF80"/>
      {/* Normal right arm, thin */}
      <rect x="16" y="8" width="3" height="8" fill="#3A3228"/>
      <rect x="16" y="16" width="3" height="2" fill="#C8956A"/>
      {/* Small blade weapon in right hand */}
      <rect x="18" y="14" width="2" height="3" fill="#666"/>
      <rect x="19" y="12" width="1" height="4" fill="#00FF80"/>
      {/* Belt - pouches for tools */}
      <rect x="3" y="18" width="14" height="2" fill="#2A2218"/>
      <rect x="5" y="18" width="3" height="2" fill="#3A3228"/>
      <rect x="10" y="18" width="3" height="2" fill="#3A3228"/>
      {/* Legs - worn trousers, wires taped along them */}
      <rect x="3" y="20" width="6" height="8" fill="#2A2218"/>
      <rect x="11" y="20" width="6" height="8" fill="#2A2218"/>
      <rect x="6" y="20" width="1" height="8" fill="#555"/>
      <rect x="13" y="20" width="1" height="8" fill="#555"/>
      {/* Boots */}
      <rect x="3" y="28" width="6" height="4" fill="#1A1208"/>
      <rect x="11" y="28" width="6" height="4" fill="#1A1208"/>
    </svg>;
  }

  if (kind === 'repulsor_drone') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Floating chassis - spherical, bare scratched metal */}
      <rect x="5" y="6" width="10" height="10" fill="#5A5A5A"/>
      <rect x="6" y="7" width="8" height="8" fill="#6A6A6A"/>
      <rect x="7" y="7" width="2" height="1" fill="#888"/>
      <rect x="11" y="7" width="2" height="1" fill="#888"/>
      {/* Sensor ring - central optical band */}
      <rect x="5" y="10" width="10" height="2" fill="#3A3A3A"/>
      <rect x="6" y="10" width="8" height="2" fill="#1A1A1A"/>
      {/* Three optical nodes on the band */}
      <rect x="7" y="10" width="1" height="2" fill="#7B00FF"/>
      <rect x="7" y="10" width="1" height="1" fill="#BB60FF"/>
      <rect x="10" y="10" width="1" height="2" fill="#7B00FF"/>
      <rect x="13" y="10" width="1" height="2" fill="#7B00FF"/>
      {/* Repulsor emitter bottom */}
      <rect x="7" y="16" width="6" height="2" fill="#3A3A3A"/>
      <rect x="8" y="16" width="4" height="1" fill="#7B00FF"/>
      {/* Repulsor glow effect below */}
      <rect x="7" y="18" width="6" height="2" fill="#7B00FF"/>
      <rect x="8" y="18" width="4" height="1" fill="#BB60FF"/>
      <rect x="8" y="20" width="4" height="2" fill="#4A0088"/>
      {/* Spinning plasma blade arm 1 (Emission Layer - left horizontal) */}
      <rect x="0" y="10" width="5" height="2" fill="#333"/>
      <rect x="1" y="10" width="3" height="1" fill="#444"/>
      {/* Blade tip left */}
      <rect x="0" y="9" width="2" height="4" fill="#7B00FF"/>
      <rect x="0" y="10" width="2" height="2" fill="#BB60FF"/>
      {/* Spinning plasma blade arm 2 (right horizontal) */}
      <rect x="15" y="10" width="5" height="2" fill="#333"/>
      <rect x="16" y="10" width="3" height="1" fill="#444"/>
      {/* Blade tip right */}
      <rect x="18" y="9" width="2" height="4" fill="#7B00FF"/>
      <rect x="18" y="10" width="2" height="2" fill="#BB60FF"/>
      {/* Spinning plasma blade arm 3 (diagonal top) */}
      <rect x="3" y="4" width="3" height="3" fill="#333"/>
      <rect x="2" y="2" width="3" height="3" fill="#7B00FF"/>
      <rect x="2" y="3" width="2" height="2" fill="#BB60FF"/>
      {/* Damage scoring on chassis */}
      <rect x="8" y="8" width="1" height="2" fill="#444"/>
      <rect x="13" y="12" width="1" height="2" fill="#444"/>
      {/* No legs - floating unit */}
    </svg>;
  }

  if (kind === 'riot_controller') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Glossy full helmet - pure white, zero features */}
      <rect x="5" y="0" width="10" height="8" fill="#E0E8F0"/>
      <rect x="6" y="1" width="8" height="6" fill="#EEF4FC"/>
      {/* No face cutouts - featureless faceplate */}
      <rect x="5" y="3" width="10" height="3" fill="#D8E4F0"/>
      {/* Police blue stripe on helmet */}
      <rect x="5" y="1" width="10" height="1" fill="#2A5A9A"/>
      <rect x="5" y="6" width="10" height="1" fill="#2A5A9A"/>
      {/* Shoulder siren (left) - red/blue alternating implied by layers */}
      <rect x="3" y="7" width="2" height="2" fill="#C03030"/>
      <rect x="3" y="9" width="2" height="1" fill="#4A9FFF"/>
      {/* Shoulder siren (right) */}
      <rect x="15" y="7" width="2" height="2" fill="#4A9FFF"/>
      <rect x="15" y="9" width="2" height="1" fill="#C03030"/>
      {/* Heavy shoulder pauldrons */}
      <rect x="1" y="8" width="5" height="5" fill="#D8E4F0"/>
      <rect x="14" y="8" width="5" height="5" fill="#D8E4F0"/>
      <rect x="1" y="8" width="5" height="1" fill="#2A5A9A"/>
      <rect x="14" y="8" width="5" height="1" fill="#2A5A9A"/>
      {/* Chest plate - layered armor panels */}
      <rect x="3" y="8" width="14" height="12" fill="#D0DCE8"/>
      <rect x="5" y="9" width="10" height="10" fill="#BCC8D8"/>
      <rect x="6" y="10" width="8" height="4" fill="#D0DCE8"/>
      <rect x="6" y="15" width="8" height="3" fill="#D0DCE8"/>
      {/* CSF badge on breast */}
      <rect x="7" y="11" width="2" height="1" fill="#2A5A9A"/>
      <rect x="7" y="12" width="2" height="1" fill="#4A9FFF"/>
      {/* Belt */}
      <rect x="3" y="20" width="14" height="2" fill="#0A1020"/>
      {/* Giant transparent riot shield (occupies half tile) */}
      <rect x="0" y="8" width="4" height="16" fill="#4A9FFF"/>
      <rect x="1" y="9" width="2" height="14" fill="#8ACFFF"/>
      <rect x="0" y="8" width="1" height="16" fill="#2A7ADF"/>
      {/* Shield edge grip */}
      <rect x="3" y="13" width="2" height="4" fill="#0A1020"/>
      {/* Stun baton (right hand) - crackling bright blue tip */}
      <rect x="16" y="10" width="2" height="10" fill="#555"/>
      <rect x="16" y="9" width="2" height="2" fill="#4A9FFF"/>
      <rect x="16" y="8" width="2" height="2" fill="#8ACFFF"/>
      {/* Legs - matching white armor greaves */}
      <rect x="3" y="22" width="6" height="6" fill="#D0DCE8"/>
      <rect x="11" y="22" width="6" height="6" fill="#D0DCE8"/>
      <rect x="3" y="24" width="6" height="2" fill="#BCC8D8"/>
      <rect x="11" y="24" width="6" height="2" fill="#BCC8D8"/>
      {/* Boots */}
      <rect x="3" y="28" width="6" height="4" fill="#0A1020"/>
      <rect x="11" y="28" width="6" height="4" fill="#0A1020"/>
    </svg>;
  }

  if (kind === 'swat_marksman') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Matte black stealth helmet - zero shine */}
      <rect x="5" y="0" width="10" height="7" fill="#080808"/>
      <rect x="6" y="1" width="8" height="5" fill="#0D0D0D"/>
      {/* Sniper scope attachment on helmet right */}
      <rect x="14" y="1" width="3" height="2" fill="#444"/>
      <rect x="16" y="1" width="1" height="2" fill="#C83030"/>
      {/* Half-balaclava - lower face only */}
      <rect x="6" y="5" width="8" height="3" fill="#0D0D0D"/>
      {/* Exposed eyes - cold, steady */}
      <rect x="7" y="3" width="2" height="1" fill="#1A1020"/>
      <rect x="11" y="3" width="2" height="1" fill="#1A1020"/>
      {/* Red targeting laser beam - Emission Layer  */}
      <rect x="17" y="4" width="3" height="1" fill="#C83030"/>
      <rect x="18" y="4" width="2" height="1" fill="#FF5050"/>
      {/* Ghillie-textured stealth jacket */}
      <rect x="3" y="8" width="14" height="11" fill="#111"/>
      <rect x="5" y="9" width="10" height="9" fill="#0A0A0A"/>
      {/* Texture patches - ghillie effect */}
      <rect x="5" y="10" width="2" height="2" fill="#111"/>
      <rect x="9" y="9" width="2" height="2" fill="#111"/>
      <rect x="13" y="11" width="2" height="2" fill="#111"/>
      <rect x="7" y="14" width="2" height="2" fill="#0D0D0D"/>
      <rect x="12" y="13" width="2" height="2" fill="#0D0D0D"/>
      {/* Stabilizing harness across chest */}
      <rect x="4" y="12" width="12" height="1" fill="#222"/>
      <rect x="4" y="15" width="12" height="1" fill="#222"/>
      {/* Arms in stealth suit */}
      <rect x="1" y="8" width="3" height="9" fill="#0D0D0D"/>
      <rect x="16" y="8" width="3" height="9" fill="#0D0D0D"/>
      {/* Gloves - matte black */}
      <rect x="1" y="17" width="3" height="2" fill="#080808"/>
      <rect x="16" y="17" width="3" height="2" fill="#080808"/>
      {/* Heavy long-barrel sniper rifle - extended past edge */}
      <rect x="16" y="12" width="4" height="2" fill="#2A2A2A"/>
      <rect x="17" y="11" width="3" height="1" fill="#333"/>
      <rect x="19" y="10" width="1" height="4" fill="#222"/>
      {/* Rifle barrel - long */}
      <rect x="18" y="12" width="2" height="1" fill="#444"/>
      {/* Belt */}
      <rect x="3" y="19" width="14" height="2" fill="#0D0D0D"/>
      {/* Legs - stealth suit */}
      <rect x="3" y="21" width="6" height="7" fill="#0D0D0D"/>
      <rect x="11" y="21" width="6" height="7" fill="#0D0D0D"/>
      {/* Kneepads */}
      <rect x="3" y="23" width="6" height="2" fill="#111"/>
      <rect x="11" y="23" width="6" height="2" fill="#111"/>
      {/* Boots */}
      <rect x="3" y="28" width="6" height="4" fill="#050508"/>
      <rect x="11" y="28" width="6" height="4" fill="#050508"/>
    </svg>;
  }

  if (kind === 'pyke_sentinel') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Bulbous fish-like helmet - the iconic Pyke silhouette */}
      <rect x="4" y="0" width="12" height="8" fill="#1A4040"/>
      <rect x="5" y="0" width="10" height="7" fill="#1F4E4E"/>
      {/* Helmet dome - wider at top */}
      <rect x="3" y="1" width="14" height="5" fill="#254F4F"/>
      <rect x="4" y="0" width="12" height="2" fill="#1F4040"/>
      {/* Breathing mask section - distinctive Pyke gear */}
      <rect x="6" y="5" width="8" height="4" fill="#1A3A3A"/>
      {/* Yellow spice-vapor vents (Emission Layer) */}
      <rect x="7" y="6" width="2" height="2" fill="#B8D000"/>
      <rect x="7" y="6" width="1" height="1" fill="#E0FF00"/>
      <rect x="11" y="6" width="2" height="2" fill="#B8D000"/>
      <rect x="12" y="6" width="1" height="1" fill="#E0FF00"/>
      {/* Brass armor trim on helmet */}
      <rect x="4" y="4" width="12" height="1" fill="#8A6A00"/>
      <rect x="3" y="2" width="1" height="4" fill="#8A6A00"/>
      <rect x="16" y="2" width="1" height="4" fill="#8A6A00"/>
      {/* Neck - taller than human */}
      <rect x="7" y="8" width="6" height="3" fill="#1A4040"/>
      {/* Layered robes over mesh armor - sea-foam pattern */}
      <rect x="3" y="11" width="14" height="10" fill="#2E8080"/>
      <rect x="4" y="12" width="12" height="8" fill="#3A9090"/>
      {/* Robe fold lines */}
      <rect x="6" y="12" width="1" height="8" fill="#2E8080"/>
      <rect x="10" y="12" width="1" height="8" fill="#2E8080"/>
      <rect x="14" y="12" width="1" height="8" fill="#2E8080"/>
      {/* Brass trim on robe hem */}
      <rect x="3" y="11" width="14" height="1" fill="#8A6A00"/>
      <rect x="3" y="18" width="14" height="1" fill="#8A6A00"/>
      {/* Lightweight mesh armor glint on shoulders */}
      <rect x="1" y="11" width="4" height="3" fill="#2A7070"/>
      <rect x="15" y="11" width="4" height="3" fill="#2A7070"/>
      {/* Arms - robes over slim frame */}
      <rect x="1" y="11" width="3" height="8" fill="#2E8080"/>
      <rect x="16" y="11" width="3" height="8" fill="#2E8080"/>
      {/* Long-barrel precision carbine */}
      <rect x="17" y="13" width="3" height="2" fill="#555"/>
      <rect x="17" y="12" width="2" height="1" fill="#444"/>
      <rect x="19" y="11" width="1" height="5" fill="#333"/>
      {/* Robe lower half */}
      <rect x="3" y="19" width="14" height="9" fill="#2E8080"/>
      <rect x="5" y="19" width="10" height="8" fill="#3A9090"/>
      {/* Boot tips */}
      <rect x="5" y="28" width="4" height="4" fill="#1A3030"/>
      <rect x="11" y="28" width="4" height="4" fill="#1A3030"/>
    </svg>;
  }

  if (kind === 'spice_brute') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Trandoshan head - wide, scaled, no hair */}
      <rect x="5" y="0" width="10" height="6" fill="#8A6A4A"/>
      <rect x="4" y="1" width="12" height="5" fill="#9A7A5A"/>
      {/* Feverish brow ridge */}
      <rect x="4" y="1" width="12" height="1" fill="#6A4A2A"/>
      {/* Spice-maddened magenta eyes (Emission Layer) */}
      <rect x="6" y="2" width="3" height="2" fill="#FF40CC"/>
      <rect x="7" y="2" width="2" height="1" fill="#FF80E8"/>
      <rect x="11" y="2" width="3" height="2" fill="#FF40CC"/>
      <rect x="11" y="2" width="2" height="1" fill="#FF80E8"/>
      {/* Veins on neck/face pulsing with raw spice (Emission Layer) */}
      <rect x="5" y="3" width="1" height="3" fill="#FF40CC"/>
      <rect x="14" y="3" width="1" height="3" fill="#FF40CC"/>
      <rect x="9" y="4" width="1" height="2" fill="#CC20A0"/>
      {/* Massive shirtless torso */}
      <rect x="2" y="6" width="16" height="14" fill="#8A6A4A"/>
      <rect x="4" y="7" width="12" height="12" fill="#9A7A5A"/>
      {/* Pulsing vein network across chest (Emission Layer) */}
      <rect x="9" y="7" width="2" height="12" fill="#FF40CC"/>
      <rect x="9" y="8" width="2" height="2" fill="#FF80E8"/>
      <rect x="9" y="12" width="2" height="2" fill="#FF80E8"/>
      <rect x="4" y="10" width="12" height="1" fill="#CC20A0"/>
      <rect x="4" y="14" width="12" height="1" fill="#CC20A0"/>
      {/* Swollen musculature highlights */}
      <rect x="4" y="7" width="4" height="5" fill="#A08060"/>
      <rect x="12" y="7" width="4" height="5" fill="#A08060"/>
      {/* Crude vibro-cleavers in each hand */}
      <rect x="0" y="8" width="3" height="10" fill="#8A6A4A"/>
      {/* Left cleaver blade */}
      <rect x="0" y="7" width="3" height="4" fill="#666"/>
      <rect x="0" y="7" width="2" height="3" fill="#888"/>
      <rect x="0" y="6" width="1" height="2" fill="#8A6A4A"/>
      {/* Right arm */}
      <rect x="17" y="8" width="3" height="10" fill="#8A6A4A"/>
      {/* Right cleaver blade */}
      <rect x="17" y="7" width="3" height="4" fill="#666"/>
      <rect x="18" y="7" width="2" height="3" fill="#888"/>
      <rect x="19" y="6" width="1" height="2" fill="#8A6A4A"/>
      {/* Simple torn-cloth belt - barely covering lower half */}
      <rect x="2" y="20" width="16" height="2" fill="#5A4A2A"/>
      <rect x="5" y="20" width="10" height="1" fill="#3A2A10"/>
      {/* Legs - massive, scale-covered */}
      <rect x="2" y="22" width="7" height="6" fill="#8A6A4A"/>
      <rect x="11" y="22" width="7" height="6" fill="#8A6A4A"/>
      {/* Vein on legs (Emission) */}
      <rect x="5" y="22" width="1" height="6" fill="#FF40CC"/>
      <rect x="14" y="22" width="1" height="6" fill="#FF40CC"/>
      {/* Bare feet / clawed */}
      <rect x="2" y="28" width="7" height="4" fill="#6A5030"/>
      <rect x="11" y="28" width="7" height="4" fill="#6A5030"/>
      <rect x="2" y="31" width="2" height="1" fill="#4A3020"/>
      <rect x="6" y="31" width="2" height="1" fill="#4A3020"/>
      <rect x="11" y="31" width="2" height="1" fill="#4A3020"/>
      <rect x="15" y="31" width="2" height="1" fill="#4A3020"/>
    </svg>;
  }

  if (kind === 'gamorrean_ironclad') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Huge pig head */}
      <rect x="4" y="0" width="12" height="7" fill="#5A8840"/>
      <rect x="5" y="0" width="10" height="6" fill="#6A9848"/>
      {/* Tusks */}
      <rect x="5" y="5" width="2" height="3" fill="#E8D8A0"/>
      <rect x="13" y="5" width="2" height="3" fill="#E8D8A0"/>
      {/* Small deep-set eyes */}
      <rect x="7" y="2" width="2" height="2" fill="#1A1A1A"/>
      <rect x="11" y="2" width="2" height="2" fill="#1A1A1A"/>
      {/* Snout */}
      <rect x="8" y="4" width="4" height="3" fill="#5A7A38"/>
      <rect x="9" y="5" width="1" height="1" fill="#3A5020"/>
      <rect x="11" y="5" width="1" height="1" fill="#3A5020"/>
      {/* Overlapping scrap-iron armor plates - dark, rusted */}
      <rect x="0" y="7" width="7" height="8" fill="#2A2020"/>
      <rect x="13" y="7" width="7" height="8" fill="#2A2020"/>
      <rect x="1" y="8" width="5" height="6" fill="#3A2A2A"/>
      <rect x="14" y="8" width="5" height="6" fill="#3A2A2A"/>
      {/* Chest plate - massive, bolted on */}
      <rect x="2" y="7" width="16" height="13" fill="#2A2020"/>
      <rect x="4" y="8" width="12" height="11" fill="#3A2828"/>
      {/* Red Cartel insignia painted on chest */}
      <rect x="8" y="9" width="4" height="7" fill="#8B0000"/>
      <rect x="6" y="12" width="8" height="2" fill="#8B0000"/>
      <rect x="9" y="10" width="2" height="1" fill="#C03030"/>
      {/* Rust streaks on armor */}
      <rect x="4" y="9" width="1" height="5" fill="#5A3010"/>
      <rect x="15" y="11" width="1" height="4" fill="#5A3010"/>
      {/* Belt - wide leather, iron clasps */}
      <rect x="2" y="20" width="16" height="3" fill="#3A2010"/>
      <rect x="8" y="20" width="4" height="3" fill="#5A3A20"/>
      {/* Massive arms */}
      <rect x="0" y="8" width="3" height="12" fill="#2A2020"/>
      <rect x="17" y="8" width="3" height="12" fill="#2A2020"/>
      {/* Gauntlets */}
      <rect x="0" y="18" width="4" height="3" fill="#3A2A2A"/>
      <rect x="16" y="18" width="4" height="3" fill="#3A2A2A"/>
      {/* Two-handed vibro-axe (drags on ground) */}
      <rect x="16" y="14" width="2" height="18" fill="#5A4A3A"/>
      <rect x="14" y="12" width="5" height="4" fill="#8A7A6A"/>
      <rect x="14" y="12" width="5" height="2" fill="#AAA"/>
      <rect x="14" y="12" width="1" height="3" fill="#C8A000"/>
      {/* Legs - thick, stumpy, armored */}
      <rect x="2" y="23" width="7" height="5" fill="#2A2020"/>
      <rect x="11" y="23" width="7" height="5" fill="#2A2020"/>
      {/* Knee guards */}
      <rect x="2" y="24" width="7" height="2" fill="#3A2828"/>
      <rect x="11" y="24" width="7" height="2" fill="#3A2828"/>
      {/* Boots */}
      <rect x="2" y="28" width="7" height="4" fill="#1A1010"/>
      <rect x="11" y="28" width="7" height="4" fill="#1A1010"/>
    </svg>;
  }

  if (kind === 'rodian_saboteur') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Rodian head - round, insectoid antennae */}
      <rect x="6" y="0" width="8" height="6" fill="#4A8A5A"/>
      <rect x="5" y="1" width="10" height="5" fill="#5A9A6A"/>
      {/* Twin antennae */}
      <rect x="8" y="0" width="1" height="3" fill="#3A6A4A"/>
      <rect x="11" y="0" width="1" height="3" fill="#3A6A4A"/>
      <rect x="7" y="0" width="1" height="1" fill="#3A6A4A"/>
      <rect x="12" y="0" width="1" height="1" fill="#3A6A4A"/>
      {/* Large compound bug-eyes */}
      <rect x="7" y="2" width="2" height="2" fill="#111"/>
      <rect x="11" y="2" width="2" height="2" fill="#111"/>
      <rect x="7" y="2" width="1" height="1" fill="#3A6A4A"/>
      <rect x="12" y="2" width="1" height="1" fill="#3A6A4A"/>
      {/* Snout */}
      <rect x="9" y="4" width="2" height="2" fill="#3A6A4A"/>
      {/* Blast goggles pushed up on forehead */}
      <rect x="7" y="1" width="6" height="2" fill="#C8A000"/>
      <rect x="8" y="1" width="4" height="1" fill="#FFD040"/>
      {/* Heavy bandoliers - packed with explosives */}
      <rect x="5" y="6" width="10" height="12" fill="#6A5030"/>
      <rect x="6" y="7" width="8" height="10" fill="#5A4020"/>
      {/* Bandolier straps */}
      <rect x="5" y="6" width="2" height="12" fill="#7A5A30"/>
      <rect x="13" y="6" width="2" height="12" fill="#7A5A30"/>
      {/* Thermal detonator belt pouches (blinking lights) */}
      <rect x="6" y="9" width="3" height="2" fill="#333"/>
      <rect x="7" y="9" width="1" height="1" fill="#FF4040"/>
      <rect x="11" y="9" width="3" height="2" fill="#333"/>
      <rect x="12" y="9" width="1" height="1" fill="#FF4040"/>
      <rect x="6" y="13" width="3" height="2" fill="#333"/>
      <rect x="7" y="13" width="1" height="1" fill="#FF4040"/>
      <rect x="11" y="13" width="3" height="2" fill="#333"/>
      <rect x="12" y="13" width="1" height="1" fill="#FF4040"/>
      {/* Arms - hunched posture */}
      <rect x="2" y="6" width="4" height="10" fill="#5A9A6A"/>
      <rect x="14" y="6" width="4" height="10" fill="#5A9A6A"/>
      {/* Live thermal detonator in left hand (tossing) */}
      <rect x="2" y="9" width="3" height="3" fill="#333"/>
      <rect x="2" y="10" width="3" height="1" fill="#FF4040"/>
      <rect x="3" y="9" width="1" height="1" fill="#FF8080"/>
      {/* Small hold-out blaster */}
      <rect x="15" y="11" width="3" height="2" fill="#666"/>
      {/* Belt */}
      <rect x="5" y="18" width="10" height="2" fill="#4A3020"/>
      {/* Legs - hunched, crouching stance */}
      <rect x="5" y="20" width="4" height="8" fill="#5A9A6A"/>
      <rect x="11" y="20" width="4" height="8" fill="#5A9A6A"/>
      {/* Boots */}
      <rect x="5" y="28" width="4" height="4" fill="#2A4A2A"/>
      <rect x="11" y="28" width="4" height="4" fill="#2A4A2A"/>
    </svg>;
  }

  if (kind === 'mandalorian_tracker') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* T-visor helmet - pristine slate blue Beskar */}
      <rect x="5" y="0" width="10" height="8" fill="#6A7A8A"/>
      <rect x="6" y="1" width="8" height="6" fill="#7A8A9A"/>
      {/* T-visor slit - pure black, absorbs all light */}
      <rect x="6" y="3" width="8" height="2" fill="#080808"/>
      <rect x="7" y="3" width="6" height="1" fill="#111"/>
      {/* Rangefinder - right side */}
      <rect x="14" y="0" width="2" height="4" fill="#5A6A7A"/>
      <rect x="15" y="2" width="1" height="2" fill="#8A9AB0"/>
      <rect x="15" y="3" width="1" height="1" fill="#C0D0E0"/>
      {/* Jetpack - prominent, twin exhausts */}
      <rect x="8" y="0" width="4" height="6" fill="#5A6A7A"/>
      <rect x="7" y="1" width="2" height="5" fill="#6A7A8A"/>
      <rect x="11" y="1" width="2" height="5" fill="#6A7A8A"/>
      {/* Jetpack exhaust ports */}
      <rect x="7" y="5" width="2" height="2" fill="#C8A000"/>
      <rect x="11" y="5" width="2" height="2" fill="#C8A000"/>
      {/* Full Beskar body armor - slate blue/silver */}
      <rect x="3" y="8" width="14" height="12" fill="#6A7A8A"/>
      <rect x="4" y="9" width="12" height="10" fill="#7A8A9A"/>
      {/* Chest plate highlight */}
      <rect x="6" y="10" width="8" height="6" fill="#8A9AB0"/>
      {/* Sigil on chest - Mandalorian skull motif */}
      <rect x="9" y="11" width="2" height="4" fill="#5A6A7A"/>
      <rect x="8" y="12" width="4" height="1" fill="#5A6A7A"/>
      <rect x="8" y="14" width="1" height="1" fill="#5A6A7A"/>
      <rect x="11" y="14" width="1" height="1" fill="#5A6A7A"/>
      {/* Shoulder pauldrons */}
      <rect x="0" y="8" width="5" height="4" fill="#7A8A9A"/>
      <rect x="15" y="8" width="5" height="4" fill="#7A8A9A"/>
      <rect x="1" y="9" width="3" height="2" fill="#8A9AB0"/>
      <rect x="16" y="9" width="3" height="2" fill="#8A9AB0"/>
      {/* Arms - full Beskar */}
      <rect x="1" y="12" width="3" height="7" fill="#6A7A8A"/>
      <rect x="16" y="12" width="3" height="7" fill="#6A7A8A"/>
      {/* Gauntlets */}
      <rect x="1" y="18" width="3" height="2" fill="#5A6A7A"/>
      <rect x="16" y="18" width="3" height="2" fill="#5A6A7A"/>
      {/* Dual WESTAR blasters - one in each hand */}
      <rect x="0" y="16" width="2" height="2" fill="#888"/>
      <rect x="0" y="15" width="1" height="3" fill="#666"/>
      <rect x="17" y="16" width="3" height="2" fill="#888"/>
      <rect x="19" y="14" width="1" height="4" fill="#666"/>
      {/* Belt with pouches */}
      <rect x="3" y="20" width="14" height="2" fill="#4A5A6A"/>
      <rect x="5" y="20" width="3" height="2" fill="#5A6A7A"/>
      <rect x="12" y="20" width="3" height="2" fill="#5A6A7A"/>
      {/* Leg armor */}
      <rect x="3" y="22" width="6" height="6" fill="#6A7A8A"/>
      <rect x="11" y="22" width="6" height="6" fill="#6A7A8A"/>
      <rect x="3" y="24" width="6" height="2" fill="#7A8A9A"/>
      <rect x="11" y="24" width="6" height="2" fill="#7A8A9A"/>
      {/* Boots */}
      <rect x="3" y="28" width="6" height="4" fill="#3A4A5A"/>
      <rect x="11" y="28" width="6" height="4" fill="#3A4A5A"/>
    </svg>;
  }

  if (kind === 'ig_assassin_droid') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Elongated cylindrical head - no reflectivity */}
      <rect x="7" y="0" width="6" height="6" fill="#3A3A3A"/>
      <rect x="8" y="0" width="4" height="5" fill="#2A2A2A"/>
      {/* Multiple red optical sensors ringing the cylinder */}
      <rect x="7" y="1" width="1" height="1" fill="#CC3030"/>
      <rect x="9" y="0" width="1" height="1" fill="#CC3030"/>
      <rect x="11" y="0" width="1" height="1" fill="#CC3030"/>
      <rect x="12" y="1" width="1" height="1" fill="#CC3030"/>
      <rect x="12" y="3" width="1" height="1" fill="#CC3030"/>
      <rect x="11" y="4" width="1" height="1" fill="#CC3030"/>
      <rect x="9" y="5" width="1" height="1" fill="#CC3030"/>
      <rect x="7" y="4" width="1" height="1" fill="#CC3030"/>
      <rect x="7" y="3" width="1" height="1" fill="#CC3030"/>
      {/* Primary sensor highlight */}
      <rect x="8" y="2" width="4" height="2" fill="#CC3030"/>
      <rect x="9" y="2" width="2" height="2" fill="#FF5050"/>
      {/* Skeletal-thin neck */}
      <rect x="9" y="6" width="2" height="3" fill="#2A2A2A"/>
      {/* Impossibly thin torso - geometric, rigid */}
      <rect x="7" y="9" width="6" height="11" fill="#2A2A2A"/>
      <rect x="8" y="10" width="4" height="9" fill="#1A1A1A"/>
      {/* Spinal cable visible */}
      <rect x="9" y="10" width="2" height="9" fill="#222"/>
      {/* Torso strut details */}
      <rect x="7" y="11" width="6" height="1" fill="#333"/>
      <rect x="7" y="14" width="6" height="1" fill="#333"/>
      <rect x="7" y="17" width="6" height="1" fill="#333"/>
      {/* Torso rotates independently - belt junction */}
      <rect x="6" y="19" width="8" height="2" fill="#444"/>
      <rect x="7" y="20" width="6" height="1" fill="#555"/>
      {/* Skeletal arms - one pulse rifle, one heavy blaster */}
      <rect x="2" y="9" width="5" height="2" fill="#2A2A2A"/>
      {/* Left arm */}
      <rect x="2" y="9" width="2" height="10" fill="#2A2A2A"/>
      <rect x="3" y="9" width="1" height="9" fill="#1A1A1A"/>
      <rect x="2" y="18" width="2" height="2" fill="#333"/>
      {/* Heavy blaster (left hand) */}
      <rect x="0" y="15" width="3" height="2" fill="#666"/>
      <rect x="0" y="14" width="1" height="4" fill="#444"/>
      {/* Right arm */}
      <rect x="15" y="9" width="3" height="2" fill="#2A2A2A"/>
      <rect x="16" y="9" width="2" height="10" fill="#2A2A2A"/>
      <rect x="16" y="18" width="2" height="2" fill="#333"/>
      {/* Pulse rifle barrel (right hand) - long */}
      <rect x="17" y="13" width="3" height="2" fill="#555"/>
      <rect x="19" y="10" width="1" height="7" fill="#333"/>
      {/* Legs - stop-motion rigid geometry */}
      <rect x="7" y="21" width="2" height="9" fill="#2A2A2A"/>
      <rect x="11" y="21" width="2" height="9" fill="#2A2A2A"/>
      {/* Knee joints - visible mechanical */}
      <rect x="6" y="24" width="4" height="2" fill="#333"/>
      <rect x="10" y="24" width="4" height="2" fill="#333"/>
      {/* Feet - flat, angular */}
      <rect x="5" y="29" width="5" height="3" fill="#222"/>
      <rect x="10" y="29" width="5" height="3" fill="#222"/>
    </svg>;
  }

  if (kind === 'sewer_crawler') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Massive multi-limbed rodent body */}
      <rect x="2" y="10" width="16" height="12" fill="#D4D0CC"/>
      <rect x="3" y="11" width="14" height="10" fill="#C8C4BE"/>
      {/* Translucent flesh highlights */}
      <rect x="5" y="12" width="10" height="7" fill="#DCDAD4"/>
      {/* Ugly snout/head */}
      <rect x="1" y="8" width="8" height="7" fill="#C8C4BE"/>
      <rect x="2" y="8" width="7" height="6" fill="#D4D0C8"/>
      {/* Beady black eyes */}
      <rect x="2" y="9" width="2" height="2" fill="#111"/>
      <rect x="2" y="9" width="1" height="1" fill="#333"/>
      <rect x="5" y="9" width="2" height="2" fill="#111"/>
      {/* Exposed incisors */}
      <rect x="2" y="13" width="1" height="2" fill="#E8E0C0"/>
      <rect x="4" y="13" width="1" height="2" fill="#E8E0C0"/>
      {/* Bioluminescent fungal growths on back (Emission Layer - cyan/yellow) */}
      <rect x="7" y="8" width="3" height="4" fill="#00CCCC"/>
      <rect x="8" y="8" width="2" height="3" fill="#40FFFF"/>
      <rect x="11" y="9" width="3" height="3" fill="#B8FF00"/>
      <rect x="12" y="9" width="2" height="2" fill="#E0FF40"/>
      <rect x="14" y="8" width="2" height="4" fill="#00CCCC"/>
      <rect x="15" y="8" width="1" height="3" fill="#40FFFF"/>
      <rect x="6" y="10" width="2" height="3" fill="#B8FF00"/>
      {/* Toxic pustules */}
      <rect x="9" y="10" width="2" height="2" fill="#80FF40"/>
      <rect x="13" y="11" width="2" height="2" fill="#80FF40"/>
      {/* Slime trail on belly */}
      <rect x="3" y="18" width="14" height="2" fill="#60D060"/>
      <rect x="4" y="18" width="12" height="1" fill="#80F080"/>
      {/* Six legs - multi-limbed horror */}
      <rect x="3" y="20" width="2" height="5" fill="#B8B4AC"/>
      <rect x="8" y="20" width="2" height="6" fill="#B8B4AC"/>
      <rect x="13" y="20" width="2" height="5" fill="#B8B4AC"/>
      <rect x="3" y="24" width="3" height="2" fill="#A0A09A"/>
      <rect x="8" y="25" width="3" height="2" fill="#A0A09A"/>
      <rect x="13" y="24" width="3" height="2" fill="#A0A09A"/>
      {/* Back legs */}
      <rect x="15" y="20" width="2" height="4" fill="#B8B4AC"/>
      <rect x="15" y="23" width="3" height="2" fill="#A0A09A"/>
      {/* Long naked tail */}
      <rect x="17" y="12" width="3" height="1" fill="#C8C4BE"/>
      <rect x="18" y="13" width="2" height="1" fill="#B8B4AE"/>
      <rect x="19" y="14" width="1" height="2" fill="#A8A4A0"/>
    </svg>;
  }

  if (kind === 'desperate_scrapper') {
    return <svg width={w} height={h} viewBox="0 0 20 32" style={px}>
      {/* Mismatched patchwork helmet - half stormtrooper, half droid */}
      <rect x="5" y="0" width="8" height="5" fill="#E8E8E0"/>
      <rect x="6" y="1" width="6" height="4" fill="#F0F0E8"/>
      {/* Cracked visor - white plasteel, broken line */}
      <rect x="6" y="2" width="6" height="2" fill="#D0D0C8"/>
      <rect x="8" y="2" width="4" height="1" fill="#E0E0D8"/>
      <rect x="9" y="2" width="1" height="2" fill="#B0B0A8"/>
      {/* Right side - scavenged droid plate, different color */}
      <rect x="13" y="0" width="4" height="6" fill="#7A7A6A"/>
      <rect x="14" y="1" width="2" height="4" fill="#8A8A7A"/>
      {/* Exposed jaw/lower face */}
      <rect x="7" y="4" width="6" height="3" fill="#C8956A"/>
      <rect x="8" y="5" width="1" height="1" fill="#2A2020"/>
      <rect x="11" y="5" width="1" height="1" fill="#2A2020"/>
      {/* Neck */}
      <rect x="8" y="6" width="4" height="2" fill="#C8956A"/>
      {/* Mismatched shoulder armor - one heavy, one light */}
      {/* Left: scavenged heavy trooper plate */}
      <rect x="0" y="7" width="6" height="5" fill="#E8E8E0"/>
      <rect x="1" y="8" width="4" height="3" fill="#D8D8D0"/>
      {/* Right: rusty droid panel */}
      <rect x="14" y="7" width="6" height="5" fill="#5A3010"/>
      <rect x="15" y="8" width="4" height="3" fill="#7A5A30"/>
      <rect x="15" y="8" width="1" height="3" fill="#C87840"/>
      {/* Torso - layered mismatched pieces */}
      <rect x="3" y="8" width="14" height="12" fill="#5A4A3A"/>
      <rect x="4" y="9" width="12" height="10" fill="#4A3828"/>
      {/* White plasteel chestpiece (salvaged) */}
      <rect x="5" y="9" width="6" height="5" fill="#D8D8D0"/>
      <rect x="5" y="13" width="6" height="1" fill="#B8B8B0"/>
      {/* Yellow hazard paint (faded) */}
      <rect x="11" y="9" width="5" height="5" fill="#4A4028"/>
      <rect x="12" y="10" width="3" height="1" fill="#8A8000"/>
      <rect x="12" y="12" width="3" height="1" fill="#8A8000"/>
      {/* Arms - one in stormtrooper bracer, one bare */}
      <rect x="1" y="12" width="3" height="7" fill="#E8E8E0"/>
      <rect x="16" y="12" width="3" height="7" fill="#C8956A"/>
      {/* Mining laser (left hand) - industrial */}
      <rect x="0" y="15" width="3" height="2" fill="#8A6A3A"/>
      <rect x="0" y="13" width="1" height="5" fill="#6A4A20"/>
      <rect x="0" y="14" width="1" height="1" fill="#FF8000"/>
      {/* Gloves */}
      <rect x="1" y="19" width="3" height="2" fill="#4A3020"/>
      <rect x="16" y="19" width="3" height="2" fill="#4A3020"/>
      {/* Belt - multiple buckles, mismatched */}
      <rect x="3" y="20" width="14" height="2" fill="#3A2A18"/>
      <rect x="5" y="20" width="2" height="2" fill="#888"/>
      <rect x="10" y="20" width="2" height="2" fill="#C87840"/>
      {/* Legs - torn camo and white trooper greave */}
      <rect x="3" y="22" width="6" height="6" fill="#4A4A3A"/>
      <rect x="11" y="22" width="6" height="6" fill="#E8E8E0"/>
      <rect x="11" y="24" width="6" height="2" fill="#D8D8D0"/>
      {/* Boots - different types */}
      <rect x="3" y="28" width="6" height="4" fill="#2A2010"/>
      <rect x="11" y="28" width="6" height="4" fill="#C8C8C0"/>
    </svg>;
  }

  return <svg width={w} height={h} viewBox={vb} style={px}>
    {/* Syndicate thug - worn street clothes, scar, basic blaster */}
    <rect x="6" y="0" width="8" height="2" fill="#2A1A0A"/>
    <rect x="6" y="1" width="8" height="5" fill="#B07850"/>
    <rect x="8" y="3" width="1" height="1" fill="#2A2020"/>
    <rect x="11" y="3" width="1" height="1" fill="#2A2020"/>
    {/* Scar on face */}
    <rect x="12" y="3" width="1" height="3" fill="#8A5030"/>
    {/* Neck */}
    <rect x="8" y="6" width="4" height="2" fill="#B07850"/>
    {/* Worn jacket */}
    <rect x="3" y="8" width="14" height="12" fill="#333"/>
    <rect x="8" y="8" width="4" height="10" fill="#1A1A1A"/>
    <rect x="4" y="9" width="3" height="8" fill="#2A2A2A"/>
    <rect x="13" y="9" width="3" height="8" fill="#2A2A2A"/>
    {/* Belt and holster */}
    <rect x="3" y="20" width="14" height="2" fill="#2A1A08"/>
    <rect x="4" y="18" width="3" height="4" fill="#1A1A1A"/>
    {/* Arms */}
    <rect x="1" y="8" width="3" height="10" fill="#333"/>
    <rect x="16" y="8" width="3" height="10" fill="#333"/>
    {/* Blaster raised */}
    <rect x="17" y="13" width="3" height="2" fill="#666"/>
    <rect x="19" y="11" width="1" height="4" fill="#444"/>
    {/* Worn trousers */}
    <rect x="3" y="22" width="6" height="6" fill="#2A2A2A"/>
    <rect x="11" y="22" width="6" height="6" fill="#2A2A2A"/>
    {/* Boots */}
    <rect x="3" y="28" width="6" height="4" fill="#111"/>
    <rect x="11" y="28" width="6" height="4" fill="#111"/>
  </svg>;
}

function TacticalGridCombatOverlay({ onSuccess, onFailure, opponentProfile, flavorText }) {
  const GW = 8, GH = 6, CELL = 52;
  const profile = AI_COMBAT_PROFILES[opponentProfile] || AI_COMBAT_PROFILES.syndicate_thug;

  const { cover: _initCover, objs: _initObjs } = genCombatMap();

  const INIT = {
    pRow: 2, pCol: 1, eRow: 2, eCol: 6,
    pHp: 8, pSh: 3, eHp: profile.hp, eSh: profile.shield,
    ap: 3, phase: 'intro', outcome: null, loot: null,
    pOW: false, eOW: false, eBlind: 0,
    gadgets: { thermal: 2, flash: 2, shield: 1, glitch: 2 },
    selGadget: null,
    log: ['Tactical engagement! WASD=Move  E=Attack/Gadget  Tab=Overwatch  Q=Cycle Gadget  Space=End Turn'],
    turn: 1,
    cover: _initCover,
    objs: _initObjs,
    steam: {},
  };

  const [gs, setGs] = React.useState(INIT);
  const gRef = React.useRef(INIT);
  const canvasRef = React.useRef(null);
  const [hoverCell, setHoverCell] = React.useState(null);

  function upd(patch) {
    const next = { ...gRef.current, ...patch };
    gRef.current = next;
    setGs(next);
  }

  function cdist(r1, c1, r2, c2) { return Math.max(Math.abs(r1 - r2), Math.abs(c1 - c2)); }

  function hasLoS(ar, ac, tr, tc) {
    const steps = Math.max(Math.abs(tr - ar), Math.abs(tc - ac));
    for (let i = 1; i < steps; i++) {
      const r = Math.round(ar + (tr - ar) * i / steps);
      const c = Math.round(ac + (tc - ac) * i / steps);
      if (gRef.current.steam[`${r},${c}`]) return false;
    }
    return true;
  }

  function isFlank(ar, ac, tr, tc) {
    const d = cdist(ar, ac, tr, tc);
    if (d <= 1) return true;
    return (ar === tr || ac === tc) && d <= 2;
  }

  function getCv(r, c) { return (gRef.current.cover[r] || [])[c] || 0; }

  function calcHit(ar, ac, tr, tc, blind) {
    const cv = getCv(tr, tc);
    const fl = isFlank(ar, ac, tr, tc);
    let base = 75;
    if (!fl) { if (cv === 1) base -= 25; else if (cv === 2) base -= 50; }
    const d = cdist(ar, ac, tr, tc);
    if (d > 4) base -= (d - 4) * 8;
    if (blind) base = Math.round(base * 0.35);
    return Math.max(10, Math.min(95, base));
  }

  function applyDmg(sh, hp, dmg) {
    let s = sh, d = dmg;
    if (s > 0) { const a = Math.min(s, d); s -= a; d -= a; }
    return { sh: s, hp: Math.max(0, hp - d) };
  }

  function playerAttack() {
    const g = gRef.current;
    if (g.phase !== 'player' || g.ap < 1) return;
    if (!hasLoS(g.pRow, g.pCol, g.eRow, g.eCol)) {
      upd({ log: [...g.log.slice(-4), 'No line of sight through the steam cloud.'] });
      return;
    }
    const fl = isFlank(g.pRow, g.pCol, g.eRow, g.eCol);
    const hit = Math.random() * 100 < calcHit(g.pRow, g.pCol, g.eRow, g.eCol, false);
    let nESh = g.eSh, nEHp = g.eHp, nPSh = g.pSh, nPHp = g.pHp;
    let nCov = g.cover, nObjs = g.objs;
    let msg = '', nPhase = g.phase, nOut = g.outcome, nLoot = null;

    if (hit) {
      const dmg = fl ? 3 : 2;
      const r = applyDmg(nESh, nEHp, dmg);
      nESh = r.sh; nEHp = r.hp;
      msg = `Hit! ${fl ? 'Flanking shot' : getCv(g.eRow, g.eCol) > 0 ? 'Through cover' : 'Clean shot'} — ${dmg} dmg.`;
      const brl = g.objs.find(o => o.type === 'barrel' && o.active && cdist(o.row, o.col, g.eRow, g.eCol) <= 1);
      if (brl) {
        nObjs = g.objs.map(o => o.id === brl.id ? { ...o, active: false } : o);
        nCov = g.cover.map(row => [...row]);
        for (let dr = -1; dr <= 1; dr++) for (let dc = -1; dc <= 1; dc++) {
          const br = brl.row + dr, bc = brl.col + dc;
          if (br >= 0 && br < GH && bc >= 0 && bc < GW) nCov[br][bc] = 0;
        }
        if (cdist(g.eRow, g.eCol, brl.row, brl.col) <= 1) { const r2 = applyDmg(nESh, nEHp, 4); nESh = r2.sh; nEHp = r2.hp; }
        if (cdist(g.pRow, g.pCol, brl.row, brl.col) <= 1) { const r3 = applyDmg(nPSh, nPHp, 3); nPSh = r3.sh; nPHp = r3.hp; }
        msg += ' BARREL EXPLODES!';
      }
      if (nEHp <= 0) { nPhase = 'outcome'; nOut = 'win'; nLoot = rollLoot(opponentProfile); msg += ` ${profile.name} eliminated!`; }
      if (nPHp <= 0 && nPhase !== 'outcome') { nPhase = 'outcome'; nOut = 'loss'; msg += ' You are critically hit!'; }
    } else {
      msg = `Miss! ${getCv(g.eRow, g.eCol) > 0 ? 'Cover held.' : 'Shot went wide.'}`;
    }
    upd({ ap: g.ap - 1, eSh: nESh, eHp: nEHp, pSh: nPSh, pHp: nPHp, cover: nCov, objs: nObjs, log: [...g.log.slice(-4), msg], phase: nPhase, outcome: nOut, loot: nLoot });
  }

  function useGadget() {
    const g = gRef.current;
    if (g.phase !== 'player' || !g.selGadget || g.gadgets[g.selGadget] <= 0) return;
    const sel = g.selGadget;
    const apCost = (sel === 'thermal' || sel === 'shield') ? 2 : 1;
    if (g.ap < apCost) { upd({ log: [...g.log.slice(-4), `Need ${apCost} AP for that gadget.`] }); return; }
    const nGad = { ...g.gadgets, [sel]: g.gadgets[sel] - 1 };
    let patch = { ap: g.ap - apCost, gadgets: nGad };
    let msg = '';
    if (sel === 'thermal') {
      if (cdist(g.pRow, g.pCol, g.eRow, g.eCol) > 4) { upd({ log: [...g.log.slice(-4), 'Enemy too far for Thermal Detonator.'] }); return; }
      const r = applyDmg(g.eSh, g.eHp, 5); patch.eSh = r.sh; patch.eHp = r.hp;
      const nC = g.cover.map(row => [...row]);
      for (let dr = -1; dr <= 1; dr++) for (let dc = -1; dc <= 1; dc++) { const rr = g.eRow + dr, cc = g.eCol + dc; if (rr >= 0 && rr < GH && cc >= 0 && cc < GW) nC[rr][cc] = 0; }
      patch.cover = nC;
      msg = 'Thermal Detonator! 5 area damage, cover destroyed in blast radius.';
      if (r.hp <= 0) { patch.phase = 'outcome'; patch.outcome = 'win'; patch.loot = rollLoot(opponentProfile); msg += ` ${profile.name} eliminated!`; }
    } else if (sel === 'flash') {
      if (cdist(g.pRow, g.pCol, g.eRow, g.eCol) > 3) { upd({ log: [...g.log.slice(-4), 'Enemy too far for Flashbang.'] }); return; }
      patch.eBlind = 2; patch.eOW = false;
      msg = 'Flashbang detonates! Enemy blinded for 2 turns.';
    } else if (sel === 'shield') {
      patch.pSh = Math.min(g.pSh + 4, 8);
      msg = 'Shield Emitter active! +4 shield capacity restored.';
    } else if (sel === 'glitch') {
      const turret = g.objs.find(o => o.type === 'turret' && o.active && !o.hacked && cdist(o.row, o.col, g.eRow, g.eCol) <= 2);
      if (!turret) { upd({ log: [...g.log.slice(-4), 'No hackable turret within range of the enemy.'] }); return; }
      patch.objs = g.objs.map(o => o.id === turret.id ? { ...o, hacked: true } : o);
      msg = 'Cyber-Glitch! Turret compromised and now targeting the enemy.';
    }
    upd({ ...patch, log: [...g.log.slice(-4), msg] });
  }

  function movePlayer(dr, dc) {
    const g = gRef.current;
    if (g.phase !== 'player' || g.ap < 1) return;
    const nr = g.pRow + dr, nc = g.pCol + dc;
    if (nr < 0 || nr >= GH || nc < 0 || nc >= GW || (nr === g.eRow && nc === g.eCol)) return;
    let patch = { pRow: nr, pCol: nc, ap: g.ap - 1, pOW: false };
    let msg = `Advanced to position [${nc},${nr}].`;
    if (g.eOW && hasLoS(g.eRow, g.eCol, nr, nc)) {
      const r = applyDmg(g.pSh, g.pHp, 2);
      patch.pSh = r.sh; patch.pHp = r.hp; patch.eOW = false;
      msg = 'Enemy overwatch reaction fire! 2 damage taken moving into the open.';
      if (r.hp <= 0) { patch.phase = 'outcome'; patch.outcome = 'loss'; }
    }
    upd({ ...patch, log: [...g.log.slice(-4), msg] });
  }

  function toggleOW() {
    const g = gRef.current;
    if (g.phase !== 'player' || g.ap < 1) return;
    const next = !g.pOW;
    upd({ pOW: next, ap: g.ap - 1, log: [...g.log.slice(-4), next ? 'Overwatch set. Reaction fire triggers on enemy movement.' : 'Overwatch cancelled.'] });
  }

  function endTurn() {
    const g = gRef.current;
    if (g.phase !== 'player') return;
    upd({ phase: 'enemy', log: [...g.log.slice(-4), 'Turn ended. Enemy acting...'] });
    setTimeout(runEnemy, 420);
  }

  function runEnemy() {
    const g = gRef.current;
    if (g.phase !== 'enemy') return;
    let eRow = g.eRow, eCol = g.eCol, eHp = g.eHp, eSh = g.eSh;
    let pHp = g.pHp, pSh = g.pSh;
    const eBlind = Math.max(0, g.eBlind - 1);
    const newSteam = {};
    Object.entries(g.steam).forEach(([k, v]) => { if (v > 1) newSteam[k] = v - 1; });
    const logs = [];

    const ht = g.objs.find(o => o.type === 'turret' && o.active && o.hacked);
    if (ht && cdist(ht.row, ht.col, eRow, eCol) <= 4) {
      const r = applyDmg(eSh, eHp, 2); eSh = r.sh; eHp = r.hp;
      logs.push('Allied turret fires on the enemy — 2 damage!');
      if (eHp <= 0) {
        upd({ eRow, eCol, eHp, eSh, pHp, pSh, eBlind, eOW: false, steam: newSteam, phase: 'outcome', outcome: 'win', loot: rollLoot(opponentProfile), turn: g.turn + 1, ap: 3, log: [...g.log.slice(-2), ...logs, `${profile.name} destroyed by the hacked turret!`] });
        return;
      }
    }

    let bestScore = -Infinity, bestR = eRow, bestC = eCol;
    for (let dr = -2; dr <= 2; dr++) {
      for (let dc = -2; dc <= 2; dc++) {
        const tr = eRow + dr, tc = eCol + dc;
        if (tr < 0 || tr >= GH || tc < 0 || tc >= GW) continue;
        if (tr === g.pRow && tc === g.pCol) continue;
        let score = 0;
        const d = cdist(tr, tc, g.pRow, g.pCol);
        score -= Math.abs(d - profile.optRange) * 10;
        const cv = (g.cover[tr] || [])[tc] || 0;
        score += cv * (profile.cover / 10);
        if (isFlank(tr, tc, g.pRow, g.pCol)) score += profile.flank * 0.5;
        if (d < cdist(eRow, eCol, g.pRow, g.pCol)) score += profile.aggression * 0.2;
        if (score > bestScore) { bestScore = score; bestR = tr; bestC = tc; }
      }
    }

    let eOW = false;
    if (bestR !== eRow || bestC !== eCol) {
      const stepR = eRow + Math.sign(bestR - eRow), stepC = eCol + Math.sign(bestC - eCol);
      if (stepR !== g.pRow || stepC !== g.pCol) {
        if (g.pOW && hasLoS(g.pRow, g.pCol, stepR, stepC)) {
          const r = applyDmg(eSh, eHp, 2); eSh = r.sh; eHp = r.hp;
          logs.push(`Overwatch reaction! Hit ${profile.name} for 2 damage.`);
          if (eHp <= 0) {
            upd({ eRow, eCol, eHp, eSh, pHp, pSh, eBlind, eOW, steam: newSteam, pOW: false, phase: 'outcome', outcome: 'win', loot: rollLoot(opponentProfile), turn: g.turn + 1, ap: 3, log: [...g.log.slice(-2), ...logs, 'Enemy eliminated!'] });
            return;
          }
        }
        eRow = stepR; eCol = stepC;
        logs.push(`${profile.name} moves to [${eCol},${eRow}].`);
      }
    } else if (Math.random() * 100 < profile.overwatch && !eBlind) {
      eOW = true;
      logs.push(`${profile.name} takes overwatch position.`);
    }

    // Boss traits: extra shots per turn, bonus damage, shield regeneration and an enrage threshold.
    if (profile.regen && eSh < profile.shield) { eSh = Math.min(profile.shield, eSh + profile.regen); logs.push(`${profile.name} shields recharge (+${profile.regen}).`); }
    const enraged = !!(profile.enrageAt && eHp <= profile.hp * profile.enrageAt);
    if (enraged && !g.eEnraged) logs.push(`${profile.name} is damaged and goes to full power!`);
    const shots = (profile.shots || 1) + (enraged ? (profile.enrageShots || 1) : 0);
    if (hasLoS(eRow, eCol, g.pRow, g.pCol)) {
      for (let sh = 0; sh < shots; sh++) {
        const hit = Math.random() * 100 < calcHit(eRow, eCol, g.pRow, g.pCol, eBlind > 0);
        if (hit) {
          const fl = isFlank(eRow, eCol, g.pRow, g.pCol);
          const dmg = (fl ? 3 : 2) + (profile.dmg || 0);
          const r = applyDmg(pSh, pHp, dmg); pSh = r.sh; pHp = r.hp;
          logs.push(`${profile.name} fires${fl ? ' (flanking)' : ''} — ${dmg} damage!`);
          if (pHp <= 0) {
            upd({ eRow, eCol, eHp, eSh, pHp, pSh, eBlind, eOW, eEnraged: enraged, steam: newSteam, phase: 'outcome', outcome: 'loss', turn: g.turn + 1, ap: 3, log: [...g.log.slice(-2), ...logs, 'You are down. Retreat!'] });
            return;
          }
        } else {
          logs.push(`${profile.name} fires and misses.`);
        }
      }
    } else {
      logs.push(`${profile.name} advances — no clear line of sight.`);
    }

    upd({ eRow, eCol, eHp, eSh, pHp, pSh, eBlind, eOW, eEnraged: enraged, steam: newSteam, phase: 'player', turn: g.turn + 1, ap: 3, log: [...g.log.slice(-2), ...logs] });
  }

  React.useEffect(() => {
    function handle(e) {
      const g = gRef.current;
      if (g.phase === 'outcome') return;
      if (g.phase === 'intro') {
        const k2 = e.key.toLowerCase();
        if (k2 === 'f' || k2 === 'enter' || k2 === ' ') { e.preventDefault(); upd({ phase: 'player' }); }
        else if (k2 === 'escape') { e.preventDefault(); onFailure(); }
        return;
      }
      const k = e.key.toLowerCase();
      if (k === 'arrowup' || k === 'w') { e.preventDefault(); movePlayer(-1, 0); }
      else if (k === 'arrowdown' || k === 's') { e.preventDefault(); movePlayer(1, 0); }
      else if (k === 'arrowleft' || k === 'a') { e.preventDefault(); movePlayer(0, -1); }
      else if (k === 'arrowright' || k === 'd') { e.preventDefault(); movePlayer(0, 1); }
      else if (k === 'e') { e.preventDefault(); if (g.selGadget && g.gadgets[g.selGadget] > 0) useGadget(); else playerAttack(); }
      else if (k === 'tab') { e.preventDefault(); toggleOW(); }
      else if (k === 'q') {
        e.preventDefault();
        const ord = [null, 'thermal', 'flash', 'shield', 'glitch'];
        const i = ord.indexOf(g.selGadget);
        upd({ selGadget: ord[(i + 1) % ord.length] });
      }
      else if (k === ' ' || k === 'enter') { e.preventDefault(); endTurn(); }
    }
    window.addEventListener('keydown', handle);
    return () => window.removeEventListener('keydown', handle);
  }, []);

  React.useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || gs.phase === 'intro' || gs.phase === 'outcome') return;
    const ctx = canvas.getContext('2d');
    const s = gs;
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    for (let r = 0; r < GH; r++) {
      for (let c = 0; c < GW; c++) {
        const px = c * CELL, py = r * CELL;
        const cv = (s.cover[r] || [])[c] || 0;
        const hasSteam = !!s.steam[r + ',' + c];
        ctx.fillStyle = hasSteam ? '#0E1A0E' : cv === 2 ? '#13121E' : cv === 1 ? '#0F0F1A' : '#0A0A14';
        ctx.fillRect(px, py, CELL, CELL);
        ctx.strokeStyle = 'rgba(0,180,230,0.1)'; ctx.lineWidth = 1;
        ctx.strokeRect(px, py, CELL, CELL);
        if (hoverCell && hoverCell[0] === r && hoverCell[1] === c && s.phase === 'player') {
          ctx.fillStyle = 'rgba(0,200,255,0.14)'; ctx.fillRect(px, py, CELL, CELL);
        }
        if (cv === 1) {
          ctx.fillStyle = '#5A3A00'; ctx.fillRect(px + 8, py + CELL - 12, CELL - 16, 7);
          ctx.fillStyle = '#8A6000'; ctx.fillRect(px + 8, py + CELL - 13, CELL - 16, 2);
        } else if (cv === 2) {
          ctx.fillStyle = '#1A0A0A'; ctx.fillRect(px + 6, py + 6, CELL - 12, CELL - 12);
          ctx.strokeStyle = '#3A0000'; ctx.lineWidth = 1; ctx.strokeRect(px + 6, py + 6, CELL - 12, CELL - 12);
        }
        if (hasSteam) {
          ctx.fillStyle = 'rgba(70,110,70,0.22)'; ctx.fillRect(px, py, CELL, CELL);
          ctx.fillStyle = 'rgba(90,160,90,0.55)';
          ctx.font = Math.round(CELL * 0.52) + 'px monospace';
          ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
          ctx.fillText('≈', px + CELL / 2, py + CELL / 2);
          ctx.textBaseline = 'alphabetic';
        }
      }
    }
    s.objs.forEach(function(obj) {
      if (!obj.active) return;
      const px = obj.col * CELL + CELL / 2, py = obj.row * CELL + CELL / 2;
      const rad = Math.round(CELL * 0.29);
      if (obj.type === 'barrel') {
        ctx.beginPath(); ctx.arc(px, py, rad, 0, Math.PI * 2);
        ctx.fillStyle = '#2A1800'; ctx.fill();
        ctx.strokeStyle = '#C8A000'; ctx.lineWidth = 2; ctx.stroke();
        ctx.fillStyle = '#C8A000';
        ctx.font = Math.round(CELL * 0.42) + 'px monospace';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        ctx.fillText('⚡', px, py); ctx.textBaseline = 'alphabetic';
      } else if (obj.type === 'turret') {
        ctx.beginPath(); ctx.arc(px, py, rad, 0, Math.PI * 2);
        ctx.fillStyle = obj.hacked ? '#00182A' : '#001400'; ctx.fill();
        ctx.strokeStyle = obj.hacked ? '#4A9FFF' : '#22C55E'; ctx.lineWidth = 2; ctx.stroke();
        ctx.fillStyle = obj.hacked ? '#4A9FFF' : '#22C55E';
        ctx.font = Math.round(CELL * 0.42) + 'px monospace';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        ctx.fillText(obj.hacked ? '★' : '⊙', px, py); ctx.textBaseline = 'alphabetic';
      } else if (obj.type === 'steam') {
        ctx.beginPath(); ctx.arc(px, py, rad, 0, Math.PI * 2);
        ctx.fillStyle = '#1A2A1A'; ctx.fill();
        ctx.strokeStyle = '#4A7A4A'; ctx.lineWidth = 1.5; ctx.stroke();
        ctx.fillStyle = '#7AAA7A';
        ctx.font = Math.round(CELL * 0.42) + 'px monospace';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        ctx.fillText('≈', px, py); ctx.textBaseline = 'alphabetic';
      } else if (obj.type === 'neon_sign') {
        const nsColors = ['#FF40CC','#00CCFF','#FF8C00','#40FF80'];
        const nsC = nsColors[obj.colorIdx || 0];
        ctx.fillStyle = '#1A1A2A'; ctx.fillRect(px - rad, py - rad, rad*2, rad*2);
        ctx.strokeStyle = nsC; ctx.lineWidth = 2; ctx.strokeRect(px - rad + 2, py - rad + 2, rad*2 - 4, rad*2 - 4);
        ctx.fillStyle = nsC; ctx.globalAlpha = 0.7;
        ctx.fillRect(px - rad + 4, py - rad + 4, rad*2 - 8, rad*2 - 8);
        ctx.globalAlpha = 1.0;
        ctx.fillStyle = '#FFF'; ctx.font = Math.round(CELL * 0.28) + 'px monospace';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('★', px, py); ctx.textBaseline = 'alphabetic';
      } else if (obj.type === 'coaxium_barrel') {
        ctx.beginPath(); ctx.arc(px, py, rad, 0, Math.PI * 2);
        ctx.fillStyle = '#1A0A18'; ctx.fill();
        ctx.strokeStyle = '#FF00CC'; ctx.lineWidth = 2; ctx.stroke();
        ctx.fillStyle = '#FF00CC'; ctx.font = Math.round(CELL * 0.38) + 'px monospace';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⬡', px, py); ctx.textBaseline = 'alphabetic';
        ctx.fillStyle = '#C8A000'; ctx.font = Math.round(CELL * 0.22) + 'px monospace';
        ctx.textAlign = 'center'; ctx.textBaseline = 'top'; ctx.fillText(obj.temp || '78°', px, py - rad + 3); ctx.textBaseline = 'alphabetic';
      } else if (obj.type === 'plasma_grid') {
        ctx.fillStyle = '#0A0A1A'; ctx.fillRect(px - 4, py - rad, 8, rad*2);
        ctx.strokeStyle = '#4A9FFF'; ctx.lineWidth = 1; ctx.strokeRect(px - 4, py - rad, 8, rad*2);
        ctx.fillStyle = '#4A9FFF'; ctx.globalAlpha = 0.6;
        for (let el = 0; el < 4; el++) { ctx.fillRect(px - 2, py - rad + el * (rad/2), 4, 2); }
        ctx.globalAlpha = 1.0;
        ctx.fillStyle = '#8ACFFF'; ctx.font = Math.round(CELL * 0.34) + 'px monospace';
        ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('⚡', px, py); ctx.textBaseline = 'alphabetic';
      }
    });
    var drawF = function(row, col, isP, ow, blind) {
      var fpx = col * CELL + CELL / 2, fpy = row * CELL + CELL / 2;
      var rad = Math.round(CELL * 0.32);
      if (ow) {
        ctx.beginPath(); ctx.arc(fpx, fpy, rad + 7, 0, Math.PI * 2);
        ctx.setLineDash([5, 4]); ctx.strokeStyle = '#C8A000'; ctx.lineWidth = 2; ctx.stroke(); ctx.setLineDash([]);
      }
      if (blind) {
        ctx.beginPath(); ctx.arc(fpx, fpy, rad + 3, 0, Math.PI * 2);
        ctx.strokeStyle = '#9B59B6'; ctx.lineWidth = 1.5; ctx.stroke();
      }
      /* sprite rendered via SVG overlay */
      var barW = CELL - 8, barH = 4, barX = col * CELL + 4, barY = row * CELL + CELL - 7;
      var maxHp = isP ? 8 : profile.hp, curHp = isP ? s.pHp : s.eHp;
      ctx.fillStyle = '#111'; ctx.fillRect(barX, barY, barW, barH);
      ctx.fillStyle = isP ? (curHp > 4 ? '#4A9FFF' : '#C8A000') : profile.accent;
      ctx.fillRect(barX, barY, barW * Math.max(0, curHp / maxHp), barH);
      var curSh = isP ? s.pSh : s.eSh, maxSh = isP ? 8 : profile.shield;
      if (maxSh > 0 && curSh > 0) {
        ctx.fillStyle = '#0A1A0A'; ctx.fillRect(barX, barY - 5, barW, 4);
        ctx.fillStyle = '#22C55E'; ctx.fillRect(barX, barY - 5, barW * Math.max(0, curSh / maxSh), 4);
      }
    };
    drawF(s.pRow, s.pCol, true, s.pOW, false);
    drawF(s.eRow, s.eCol, false, s.eOW, s.eBlind > 0);
  }, [gs, hoverCell]);

  function handleCvMove(e) {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const rect = canvas.getBoundingClientRect();
    const col = Math.floor((e.clientX - rect.left) / CELL);
    const row = Math.floor((e.clientY - rect.top) / CELL);
    if (row >= 0 && row < GH && col >= 0 && col < GW) setHoverCell([row, col]);
    else setHoverCell(null);
  }

  function handleCvClick() {
    if (!hoverCell) return;
    const tr = hoverCell[0], tc = hoverCell[1];
    const g2 = gRef.current;
    if (g2.phase !== 'player' || g2.ap < 1) return;
    const dr = tr - g2.pRow, dc = tc - g2.pCol;
    if (dr === 0 && dc === 0) return;
    movePlayer(Math.sign(dr), Math.sign(dc));
  }

  const g = gs;
  const gdLabels = { thermal: 'Thermal(2AP)', flash: 'Flash(1AP)', shield: 'Shield(2AP)', glitch: 'Glitch(1AP)' };

  if (g.phase === 'intro') {
    return (
      <div style={{ position: 'fixed', inset: 0, background: '#000', zIndex: 200, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontFamily: "'IBM Plex Mono',monospace", gap: 20, padding: 24 }}>
        <div style={{ color: '#C8A000', fontSize: '0.6rem', letterSpacing: '0.3em' }}>TACTICAL ENCOUNTER</div>
        <div style={{ display: 'flex', gap: 56, alignItems: 'flex-end', width: '100%', maxWidth: 480, justifyContent: 'center' }}>
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
            <PlayerSprite w={48} h={64} />
            <div style={{ color: '#4A9FFF', fontSize: '0.5rem' }}>YOU</div>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
            <EnemySprite kind={opponentProfile} w={96} h={128} />
            <div style={{ color: profile.accent, fontSize: '0.5rem' }}>{profile.name}</div>
          </div>
        </div>
        <div style={{ color: '#AAA', fontSize: '0.65rem', maxWidth: 440, textAlign: 'center', lineHeight: 1.9, border: '1px solid #1A1A2A', background: '#06060F', padding: '12px 16px' }}>
          {flavorText || 'A hostile contact has you cornered. There is no talking your way out of this.'}
        </div>
        {(profile.shots > 1 || profile.dmg || profile.regen || profile.enrageAt) && <div style={{ color: profile.accent, fontSize: '0.55rem', maxWidth: 440, textAlign: 'center' }}>THREAT: {[profile.shots > 1 ? `${profile.shots} shots a turn` : '', profile.dmg ? `+${profile.dmg} damage` : '', profile.regen ? `shields recharge ${profile.regen} a turn` : '', profile.enrageAt ? 'goes to full power when badly hurt' : ''].filter(Boolean).join(', ')}</div>}
        <div style={{ display: 'flex', gap: 16 }}>
          <button onClick={() => upd({ phase: 'player' })} style={{ background: '#C8A000', color: '#000', border: 'none', padding: '0.5rem 2.5rem', cursor: 'pointer', fontSize: '0.8rem', fontFamily: "'IBM Plex Mono',monospace", fontWeight: 'bold' }}>FIGHT!</button>
          <button onClick={onFailure} style={{ background: '#1A0A0A', color: '#888', border: '1px solid #333', padding: '0.5rem 2rem', cursor: 'pointer', fontSize: '0.8rem', fontFamily: "'IBM Plex Mono',monospace" }}>Run</button>
        </div>
        <div style={{ color: '#333', fontSize: '0.5rem' }}>F / Enter = Fight  |  Esc = Run</div>
      </div>
    );
  }

  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.96)', zIndex: 200, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontFamily: "'IBM Plex Mono',monospace", gap: 4, padding: 8, overflowY: 'auto' }}>
      <div style={{ color: '#C8A000', fontSize: '0.65rem', letterSpacing: '0.25em', marginBottom: 4 }}>TACTICAL ENGAGEMENT</div>
      <div style={{ display: 'flex', gap: 32, width: GW * CELL, marginBottom: 4 }}>
        <div style={{ flex: 1 }}>
          <div style={{ color: '#4A9FFF', fontSize: '0.6rem', marginBottom: 2 }}>YOU  HP:{g.pHp}/8  SH:{g.pSh}</div>
          <div style={{ height: 5, background: '#111', borderRadius: 2 }}><div style={{ height: '100%', width: `${Math.max(0, g.pHp / 8) * 100}%`, background: g.pHp > 4 ? '#4A9FFF' : '#C8A000', borderRadius: 2, transition: 'width 0.2s' }} /></div>
        </div>
        <div style={{ flex: 1, textAlign: 'right' }}>
          <div style={{ color: profile.accent, fontSize: '0.6rem', marginBottom: 2 }}>{profile.name}  HP:{g.eHp}/{profile.hp}  SH:{g.eSh}</div>
          <div style={{ height: 5, background: '#111', borderRadius: 2, display: 'flex', justifyContent: 'flex-end' }}><div style={{ height: '100%', width: `${Math.max(0, g.eHp / profile.hp) * 100}%`, background: profile.accent, borderRadius: 2, transition: 'width 0.2s' }} /></div>
        </div>
      </div>
      <div style={{ position: 'relative', width: GW * CELL, height: GH * CELL }}>
      <canvas ref={canvasRef} width={GW * CELL} height={GH * CELL} onMouseMove={handleCvMove} onMouseLeave={() => setHoverCell(null)} onClick={handleCvClick} style={{ display: 'block', border: '1px solid #1A1A2A', cursor: g.phase === 'player' ? 'crosshair' : 'default' }} />
      {/* SVG sprite overlay - detailed pixel-art units on grid */}
      <div style={{ position: 'absolute', top: 0, left: 0, width: GW * CELL, height: GH * CELL, pointerEvents: 'none' }}>
        {g.phase !== 'intro' && g.phase !== 'outcome' || g.outcome !== 'loss' ? (
          <div style={{ position: 'absolute', left: g.pCol * CELL + 4, top: g.pRow * CELL + 2, width: CELL - 8, height: CELL - 10, display: 'flex', alignItems: 'flex-end', justifyContent: 'center' }}>
            <PlayerSprite w={CELL - 14} h={CELL - 10} />
          </div>
        ) : null}
        {(g.phase !== 'outcome' || g.eHp > 0) ? (
          <div style={{ position: 'absolute', left: g.eCol * CELL + 4, top: g.eRow * CELL + 2, width: CELL - 8, height: CELL - 10, display: 'flex', alignItems: 'flex-end', justifyContent: 'center', filter: g.eBlind > 0 ? 'brightness(0.4) saturate(0.2)' : undefined }}>
            <EnemySprite kind={opponentProfile} w={CELL - 14} h={CELL - 10} />
          </div>
        ) : null}
      </div>
      </div>
      <div style={{ display: 'flex', gap: 12, width: GW * CELL, marginTop: 4, alignItems: 'center' }}>
        <div style={{ color: '#555', fontSize: '0.6rem' }}>Turn {g.turn}</div>
        <div style={{ display: 'flex', gap: 3 }}>{[0, 1, 2].map(i => <div key={i} style={{ width: 11, height: 11, borderRadius: '50%', background: i < g.ap ? '#C8A000' : '#1A1A14', border: '1px solid #333' }} />)}</div>
        {g.pOW && <div style={{ color: '#C8A000', fontSize: '0.58rem' }}>OW</div>}
        {g.eBlind > 0 && <div style={{ color: '#9B59B6', fontSize: '0.58rem' }}>BLINDED({g.eBlind})</div>}
        <div style={{ flex: 1, textAlign: 'right', fontSize: '0.6rem', color: g.phase === 'player' ? '#4ACDFF' : g.phase === 'enemy' ? '#C03030' : '#C8A000' }}>
          {g.phase === 'player' ? 'YOUR TURN' : g.phase === 'enemy' ? 'ENEMY ACTING...' : g.outcome === 'win' ? 'VICTORY' : 'DEFEAT'}
        </div>
      </div>
      <div style={{ display: 'flex', gap: 4, width: GW * CELL, marginTop: 2 }}>
        {['thermal', 'flash', 'shield', 'glitch'].map(gk => (
          <button key={gk} onClick={() => upd({ selGadget: g.selGadget === gk ? null : gk })}
            style={{ flex: 1, padding: '3px 2px', fontSize: '0.5rem', background: g.selGadget === gk ? '#2A2600' : '#0C0C14', color: g.gadgets[gk] > 0 ? '#C8A000' : '#333', border: g.selGadget === gk ? '1px solid #C8A000' : '1px solid #1A1A2A', cursor: 'pointer' }}>
            {gdLabels[gk]}[{g.gadgets[gk]}]
          </button>
        ))}
      </div>
      <div style={{ display: 'flex', gap: 6, width: GW * CELL, marginTop: 2 }}>
        <button onClick={playerAttack} disabled={g.phase !== 'player' || g.ap < 1} style={{ flex: 1, padding: '4px', fontSize: '0.6rem', background: '#0E0808', color: '#C03030', border: '1px solid #C03030', cursor: 'pointer', opacity: g.phase === 'player' && g.ap >= 1 ? 1 : 0.35 }}>[E] Attack</button>
        <button onClick={useGadget} disabled={!g.selGadget || g.phase !== 'player'} style={{ flex: 1, padding: '4px', fontSize: '0.6rem', background: '#080E08', color: '#5ACD5A', border: '1px solid #5ACD5A', cursor: 'pointer', opacity: g.selGadget && g.phase === 'player' ? 1 : 0.35 }}>[E] Gadget</button>
        <button onClick={toggleOW} disabled={g.phase !== 'player' || g.ap < 1} style={{ flex: 1, padding: '4px', fontSize: '0.6rem', background: g.pOW ? '#1A1600' : '#080814', color: '#C8A000', border: '1px solid #C8A000', cursor: 'pointer', opacity: g.phase === 'player' && g.ap >= 1 ? 1 : 0.35 }}>[Tab] {g.pOW ? 'Cancel OW' : 'Overwatch'}</button>
        <button onClick={endTurn} disabled={g.phase !== 'player'} style={{ flex: 1, padding: '4px', fontSize: '0.6rem', background: '#0C0C0C', color: '#888', border: '1px solid #333', cursor: 'pointer', opacity: g.phase === 'player' ? 1 : 0.35 }}>[Space] End Turn</button>
      </div>
      <div style={{ width: GW * CELL, background: '#060610', border: '1px solid #111', padding: '5px 8px', marginTop: 2, minHeight: 52 }}>
        {g.log.slice(-4).map((l, i) => <div key={i} style={{ color: i === g.log.slice(-4).length - 1 ? '#BBB' : '#555', fontSize: '0.58rem', lineHeight: 1.6 }}>{l}</div>)}
      </div>
      <div style={{ color: '#333', fontSize: '0.52rem', marginTop: 2 }}>WASD/Arrows: Move  |  E: Attack or use selected Gadget  |  Tab: Overwatch  |  Q: Cycle Gadget  |  Space: End Turn</div>
      {g.phase === 'outcome' && g.outcome === 'win' && g.loot && (
        <div style={{ marginTop: 10, border: '1px solid #C8A000', background: '#080800', padding: '10px 20px', textAlign: 'center', minWidth: 260 }}>
          <div style={{ color: '#C8A000', fontSize: '0.65rem', letterSpacing: '0.2em', marginBottom: 6 }}>FIELD SECURED</div>
          <div style={{ color: '#EEE', fontSize: '0.7rem', marginBottom: 4 }}>Credits recovered: <span style={{ color: '#C8A000' }}>{g.loot.credits}</span></div>
          {g.loot.item && <div style={{ color: '#9B59B6', fontSize: '0.65rem', marginBottom: 2 }}>Item found: {g.loot.item.name}</div>}
          {g.loot.item && <div style={{ color: '#555', fontSize: '0.55rem', marginBottom: 6 }}>{g.loot.item.description}</div>}
          <button onClick={() => onSuccess(g.loot)} style={{ marginTop: 6, background: '#C8A000', color: '#000', border: 'none', padding: '0.4rem 1.8rem', cursor: 'pointer', fontSize: '0.75rem', fontFamily: "'IBM Plex Mono',monospace", fontWeight: 'bold' }}>Collect</button>
        </div>
      )}
      {g.phase === 'outcome' && g.outcome === 'loss' && (
        <button onClick={onFailure} style={{ marginTop: 10, background: '#1A0A0A', color: '#EEE', border: '1px solid #333', padding: '0.5rem 2rem', cursor: 'pointer', fontSize: '0.8rem', fontFamily: "'IBM Plex Mono',monospace" }}>Fall Back</button>
      )}
    </div>
  );
}









// ===== CONQUEST UI ART BEGIN =====
const CQ_INK = '#0B1018', CQ_LIGHT = '#E8EEF8';
// Unit glyphs: one flat silhouette per unit type. Used on the map, in panels and as battle tokens.
function CQUnitGlyph({ type, size = 24, color, x, y }) {
  const c = color || (CQ.UNITS[type] && CQ.UNITS[type].col) || '#CCC', d = CQ_INK, k = CQ_LIGHT;
  let g = null;
  if (type === 'inf') g = <g><circle cx="14" cy="8" r="4.4" fill={c} /><rect x="9.5" y="12" width="9" height="10" rx="2" fill={c} /><rect x="9.5" y="22" width="3.6" height="7" fill={c} /><rect x="15" y="22" width="3.6" height="7" fill={c} /><rect x="17" y="13.5" width="12" height="2.6" fill={k} /><rect x="11" y="6.4" width="6" height="2" fill={d} /></g>;
  else if (type === 'gam') g = <g><circle cx="14" cy="8" r="5" fill={c} /><rect x="7" y="12" width="14" height="11" rx="3" fill={c} /><rect x="8" y="23" width="5" height="6" fill={c} /><rect x="15" y="23" width="5" height="6" fill={c} /><rect x="22" y="5" width="3.4" height="18" fill={k} /><rect x="20" y="3" width="7.4" height="5" rx="2" fill={k} /><rect x="9" y="10" width="2" height="3" fill={d} /><rect x="16" y="10" width="2" height="3" fill={d} /></g>;
  else if (type === 'snp') g = <g><circle cx="12" cy="8" r="4" fill={c} /><rect x="8.5" y="12" width="7" height="10" rx="2" fill={c} /><rect x="8.5" y="22" width="3" height="7" fill={c} /><rect x="12.5" y="22" width="3" height="7" fill={c} /><rect x="14" y="13" width="16" height="2" fill={k} /><rect x="22" y="10.5" width="4" height="2.4" fill={d} /><rect x="9.5" y="6.4" width="6" height="2" fill={d} /></g>;
  else if (type === 'med') g = <g><circle cx="14" cy="8" r="4.4" fill={c} /><rect x="9" y="12" width="10" height="11" rx="2" fill={c} /><rect x="9.5" y="23" width="3.6" height="6" fill={c} /><rect x="15" y="23" width="3.6" height="6" fill={c} /><rect x="12.4" y="14" width="3.2" height="8" fill={k} /><rect x="10" y="16.4" width="8" height="3.2" fill={k} /><rect x="19" y="14" width="8" height="8" rx="1" fill={d} /><rect x="22" y="15.4" width="2" height="5" fill={c} /></g>;
  else if (type === 'spc') g = <g><polygon points="2,22 12,14 28,14 31,19 24,24 8,24" fill={c} /><circle cx="9" cy="26" r="3.4" fill={d} /><circle cx="25" cy="26" r="3.4" fill={d} /><circle cx="16" cy="9" r="3.6" fill={k} /><rect x="14" y="11" width="5" height="5" fill={k} /><rect x="22" y="15" width="8" height="2" fill={d} /></g>;
  else if (type === 'drd') g = <g><circle cx="16" cy="12" r="8.4" fill={c} /><circle cx="16" cy="12" r="4.4" fill={d} /><circle cx="16" cy="12" r="2.2" fill="#FF5A4A" /><rect x="15" y="1" width="2" height="4" fill={k} /><rect x="9" y="20" width="14" height="5" rx="2" fill={c} /><rect x="9" y="25" width="4" height="5" fill={c} /><rect x="19" y="25" width="4" height="5" fill={c} /><rect x="22" y="14" width="8" height="2.6" fill={k} /></g>;
  else if (type === 'tnk') g = <g><rect x="2" y="19" width="28" height="9" rx="3" fill={d} /><rect x="3.5" y="20.4" width="25" height="6" rx="2" fill={c} /><rect x="8" y="12" width="15" height="8" rx="2" fill={c} /><rect x="21" y="14" width="11" height="2.6" fill={k} /><circle cx="8" cy="24" r="2.2" fill={d} /><circle cx="16" cy="24" r="2.2" fill={d} /><circle cx="24" cy="24" r="2.2" fill={d} /><rect x="11" y="9" width="5" height="3.4" fill={k} /></g>;
  else if (type === 'wlk') g = <g><rect x="8" y="8" width="16" height="10" rx="3" fill={c} /><rect x="11" y="4" width="10" height="5" rx="2" fill={k} /><rect x="9" y="17" width="3.6" height="12" fill={c} /><rect x="19.4" y="17" width="3.6" height="12" fill={c} /><rect x="6" y="28" width="8" height="3" fill={d} /><rect x="18" y="28" width="8" height="3" fill={d} /><rect x="23" y="9" width="8" height="2.4" fill={k} /><rect x="23" y="13" width="8" height="2.4" fill={k} /><rect x="13" y="11" width="6" height="2.4" fill={d} /></g>;
  else g = <g><rect x="8" y="20" width="16" height="8" rx="2" fill={c} /><rect x="12" y="13" width="8" height="8" rx="2" fill={c} /><rect x="18" y="14" width="12" height="2.6" fill={k} /></g>;
  return <svg viewBox="0 0 32 32" width={size} height={size} x={x} y={y} style={{ display: 'block', flex: 'none' }}>{g}</svg>;
}

// Sector scenes: 132 by 50 illustrations, one per theme, used on map cards and the sector panel banner.
function CQScene({ theme, id }) {
  const th = CQ.THEMES[theme] || CQ.THEMES.shadow; const sky = th.sky, acc = th.accent, W = th.wall[0], Ws = th.wall[1];
  const r = rng((id || theme).length * 31 + (id || theme).charCodeAt(0));
  const gid = 'sg_' + (id || theme);
  const stars = Array.from({ length: 14 }, () => [r() * 132, r() * 20, 0.4 + r() * 0.7]);
  const towers = (n, base, minH, maxH, col, win) => Array.from({ length: n }, (_, i) => { const w = 7 + r() * 9, h = minH + r() * (maxH - minH), x = (i / n) * 140 - 4 + r() * 4; return <g key={i}><rect x={x} y={base - h} width={w} height={h} fill={col} />{win && Array.from({ length: Math.floor(h / 6) }, (_, j) => (r() < 0.55 ? <rect key={j} x={x + 1.5 + (j % 2) * 3} y={base - h + 3 + j * 6} width="1.6" height="2" fill={win} opacity="0.85" /> : null))}</g>; });
  let art = null;
  switch (theme) {
    case 'lounge': art = <g>{[18, 52, 86, 116].map((x, i) => <path key={i} d={`M${x - 14} 50 L${x - 14} 28 Q${x} 8 ${x + 14} 28 L${x + 14} 50 Z`} fill="#241838" stroke={acc} strokeWidth="1.2" />)}{[18, 52, 86, 116].map((x, i) => <path key={'a' + i} d={`M${x - 8} 50 L${x - 8} 32 Q${x} 20 ${x + 8} 32 L${x + 8} 50 Z`} fill="#FFB84A" opacity="0.5" />)}{Array.from({ length: 10 }, (_, i) => <circle key={'l' + i} cx={6 + i * 13} cy={24 + Math.sin(i) * 3} r="1.2" fill="#FFE9B0" />)}</g>; break;
    case 'senate': art = <g><path d="M30 50 L30 30 Q66 4 102 30 L102 50 Z" fill="#D8DCEF" /><path d="M30 30 Q66 4 102 30" fill="none" stroke="#7C86C8" strokeWidth="1.5" />{Array.from({ length: 9 }, (_, i) => <rect key={i} x={34 + i * 7.6} y="30" width="3" height="20" fill="#9AA2D0" />)}<rect x="26" y="46" width="80" height="4" fill="#B0B8E0" /><rect x="65" y="2" width="1.6" height="9" fill="#EEE" /><polygon points="66.6,2 76,5 66.6,8" fill="#A8B4FF" /></g>; break;
    case 'upper': art = <g>{towers(9, 50, 14, 40, '#16203A', '#7FD0FF')}<rect x="0" y="22" width="132" height="1" fill="#FFB070" opacity="0.7" /><rect x="0" y="30" width="132" height="1" fill="#7FD0FF" opacity="0.6" /></g>; break;
    case 'precinct': art = <g>{towers(5, 50, 18, 34, '#E4ECFA', '#3A5AA8')}<polygon points="106,50 96,0 118,0" fill="#CFE4FF" opacity="0.2" /><path d="M63 12 L73 16 L73 26 Q68 32 63 28 Q58 32 53 26 L53 16 Z" fill="#4A9FFF" transform="translate(3,0)" /></g>; break;
    case 'promenade': art = <g>{Array.from({ length: 6 }, (_, i) => <g key={i}><polygon points={`${i * 22 + 2},26 ${i * 22 + 22},26 ${i * 22 + 24},32 ${i * 22},32`} fill={i % 2 ? '#E8744A' : '#F4E0B0'} /><rect x={i * 22 + 3} y="32" width="18" height="18" fill="#2A1C24" /><rect x={i * 22 + 6} y="36" width="12" height="10" fill="#FFB070" opacity="0.5" /></g>)}{Array.from({ length: 7 }, (_, i) => <circle key={'c' + i} cx={8 + i * 19} cy="20" r="2.4" fill="#FFD890" />)}<path d="M0 20 Q66 28 132 20" fill="none" stroke="#6A4A3A" /></g>; break;
    case 'alley': art = <g><rect x="0" y="0" width="30" height="50" fill="#0E131C" /><rect x="102" y="0" width="30" height="50" fill="#0E131C" /><rect x="6" y="8" width="18" height="5" fill="#39E8E8" opacity="0.9" /><rect x="108" y="16" width="18" height="5" fill="#FF4FC8" opacity="0.9" /><rect x="10" y="28" width="12" height="4" fill="#FF4FC8" opacity="0.8" /><rect x="110" y="34" width="12" height="4" fill="#39E8E8" opacity="0.8" />{[0, 1, 2].map((i) => <path key={i} d={`M30 ${6 + i * 6} Q66 ${20 + i * 5} 102 ${6 + i * 6}`} fill="none" stroke="#2A3A4A" strokeWidth="1.2" />)}<rect x="56" y="30" width="20" height="20" fill="#0A1018" /><rect x="60" y="34" width="12" height="8" fill="#39E8E8" opacity="0.4" /></g>; break;
    case 'midlevels': art = <g>{[0, 1, 2].map((i) => <g key={i}><rect x={i * 46 + 2} y="8" width="40" height="42" fill="#2A2E38" />{Array.from({ length: 12 }, (_, j) => <rect key={j} x={i * 46 + 6 + (j % 4) * 9} y={12 + Math.floor(j / 4) * 12} width="5" height="7" fill={r() < 0.6 ? '#FFD890' : '#161A22'} />)}</g>)}<path d="M4 20 L44 22" stroke="#B0A090" /><rect x="30" y="22" width="6" height="4" fill="#D84A4A" /></g>; break;
    case 'csfhub': art = <g>{towers(3, 50, 16, 30, '#C8D8F0', '#2A4A8A')}<rect x="84" y="14" width="40" height="36" fill="#E0EAFA" /><path d="M104 18 L116 23 L116 34 Q110 42 104 38 Q98 42 92 34 L92 23 Z" fill="#4A9FFF" /><path d="M104 22 L110 25 L110 33 Q107 37 104 35 Z" fill="#E8F2FF" /><rect x="6" y="40" width="70" height="2" fill="#4A9FFF" opacity="0.6" /></g>; break;
    case 'works': art = <g>{[16, 46, 76, 104].map((x, i) => <g key={i}><rect x={x - 6} y={22 - (i % 2) * 6} width="12" height={28 + (i % 2) * 6} fill="#3A2A22" /><rect x={x - 7} y={20 - (i % 2) * 6} width="14" height="4" fill="#5A3A2A" /><ellipse cx={x} cy={14 - (i % 2) * 6} rx="9" ry="5" fill="#1A1210" opacity="0.7" /></g>)}<rect x="0" y="42" width="132" height="8" fill="#FF6A1A" opacity="0.85" /><rect x="0" y="44" width="132" height="2" fill="#FFD070" /><path d="M0 34 L132 34" stroke="#6A4A3A" strokeWidth="2.4" /></g>; break;
    case 'industrial': art = <g>{[26, 66, 106].map((x, i) => <g key={i}><circle cx={x} cy="32" r="14" fill="#2A2A32" stroke="#C8A040" strokeWidth="2" strokeDasharray="4 3" /><circle cx={x} cy="32" r="5" fill="#C8A040" /></g>)}<path d="M4 50 L4 8 L70 8" fill="none" stroke="#C8A040" strokeWidth="2.4" /><rect x="66" y="8" width="3" height="14" fill="#C8A040" /><rect x="62" y="22" width="11" height="6" fill="#E8B84A" /></g>; break;
    case 'spaceport': art = <g><ellipse cx="66" cy="44" rx="56" ry="8" fill="#10161E" /><ellipse cx="66" cy="44" rx="40" ry="5" fill="none" stroke="#6ABCE8" strokeWidth="1" strokeDasharray="3 3" /><polygon points="40,38 86,30 96,36 50,44" fill="#9AA6B8" /><polygon points="86,30 100,26 100,36 96,36" fill="#C8D2E0" /><rect x="52" y="39" width="26" height="2" fill="#6ABCE8" /><circle cx="20" cy="40" r="2" fill="#FF5A4A" /><circle cx="112" cy="40" r="2" fill="#34D86A" /><rect x="14" y="14" width="2" height="26" fill="#6ABCE8" opacity="0.5" /></g>; break;
    case 'shadow': art = <g>{towers(6, 50, 12, 32, '#1A1424', '#B07CFF')}<ellipse cx="60" cy="34" rx="5" ry="3.4" fill="#B07CFF" /><ellipse cx="74" cy="34" rx="5" ry="3.4" fill="#B07CFF" /><ellipse cx="60" cy="34" rx="2" ry="3" fill="#0A0610" /><ellipse cx="74" cy="34" rx="2" ry="3" fill="#0A0610" /><rect x="14" y="14" width="26" height="6" fill="#FF4FC8" opacity="0.85" /></g>; break;
    case 'market': art = <g>{Array.from({ length: 5 }, (_, i) => <g key={i}><polygon points={`${i * 27 + 2},24 ${i * 27 + 26},24 ${i * 27 + 30},30 ${i * 27 - 2},30`} fill={['#E8A040', '#C84A4A', '#40A8C8', '#E8A040', '#C84A4A'][i]} /><rect x={i * 27 + 4} y="30" width="22" height="20" fill="#2A1A14" /><circle cx={i * 27 + 15} cy="20" r="2.6" fill="#FFD070" /></g>)}<rect x="0" y="46" width="132" height="4" fill="#3A2A1E" /></g>; break;
    case 'freight': art = <g>{Array.from({ length: 12 }, (_, i) => <rect key={i} x={(i % 6) * 22 + 2} y={26 + Math.floor(i / 6) * 12} width="20" height="11" fill={['#34A860', '#C8A040', '#3A7AB8', '#C84A4A', '#8AA84A', '#6A5AB8'][(i * 5) % 6]} />)}<path d="M104 50 L104 6 L128 6" fill="none" stroke="#E8B84A" strokeWidth="2.4" /><rect x="124" y="6" width="2" height="14" fill="#E8B84A" /></g>; break;
    case 'undercity': art = <g><path d="M0 50 L0 26 Q20 14 34 28 Q50 8 70 24 Q90 10 110 26 Q124 18 132 28 L132 50 Z" fill="#0C1C16" />{Array.from({ length: 12 }, (_, i) => <circle key={i} cx={6 + i * 11} cy={30 + (i * 7) % 14} r="1.6" fill="#6AE8A0" opacity="0.9" />)}<rect x="40" y="0" width="5" height="22" fill="#2A3A34" /><circle cx="42" cy="26" r="1.4" fill="#6AE8A0" /><rect x="0" y="44" width="132" height="6" fill="#0E4A6A" opacity="0.7" /></g>; break;
    default: art = <g>{[12, 40, 72, 104].map((x, i) => <polygon key={i} points={`${x},50 ${x + 6},${30 - (i % 2) * 6} ${x + 14},50`} fill="#2A2218" />)}<ellipse cx="66" cy="44" rx="6" ry="3" fill="#E8A860" opacity="0.9" /><polygon points="62,44 66,32 70,44" fill="#FF8A3A" /><rect x="86" y="36" width="22" height="14" fill="#3A2E22" /></g>;
  }
  return (
    <g>
      <defs><linearGradient id={gid} x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor={sky[0]} /><stop offset="1" stopColor={sky[1]} /></linearGradient></defs>
      <rect width="132" height="50" fill={`url(#${gid})`} />
      {theme !== 'works' && theme !== 'industrial' && stars.map((s, i) => <circle key={i} cx={s[0]} cy={s[1]} r={s[2]} fill="#FFF" opacity="0.5" />)}
      {art}
    </g>
  );
}
// ===== CONQUEST UI ART END =====

// ===== CONQUEST UI MAP BEGIN =====
const CQ_TIERS = [
  { y: 70, name: 'APEX', lvl: 'L.5100', a: '#2A1A4A', b: '#4A2A5A', glow: '#E8C860' },
  { y: 185, name: 'LEGISLATURE', lvl: 'L.1900', a: '#18243E', b: '#2A3A62', glow: '#8CA8FF' },
  { y: 300, name: 'MID LAYERS', lvl: 'L.1200', a: '#1A2028', b: '#2A323E', glow: '#B0C0D8' },
  { y: 415, name: 'INDUSTRIAL', lvl: 'L.005', a: '#2A1A12', b: '#3E2418', glow: '#FF8A3A' },
  { y: 530, name: 'SUB-SURFACE', lvl: 'L.1300', a: '#1A1428', b: '#2A1E3E', glow: '#B07CFF' },
  { y: 645, name: 'UNDERCITY', lvl: 'DEPTHS', a: '#0E1C16', b: '#14281E', glow: '#6AE8A0' },
];
function CQUnitStrip({ gar, x, y, max = 5 }) {
  const items = CQ.UNIT_ORDER.filter((k) => (gar[k] || 0) > 0).slice(0, max);
  const more = CQ.UNIT_ORDER.filter((k) => (gar[k] || 0) > 0).length - items.length;
  return (
    <g>
      {items.map((k, i) => (
        <g key={k} transform={`translate(${x + i * 22},${y})`}>
          <CQUnitGlyph type={k} size={13} />
          <text x="13" y="10" fontSize="8" fill="#E8EEF8" fontFamily="monospace" fontWeight="700">{gar[k]}</text>
        </g>
      ))}
      {more > 0 && <text x={x + items.length * 22} y={y + 10} fontSize="8" fill="#8A93A8" fontFamily="monospace">+{more}</text>}
    </g>
  );
}
function CQMap({ st, sel, onSelect, intelOf, vw = 1060 }) {
  const [hover, setHover] = React.useState(null);
  const sx = vw / 1060;
  const list = CQ.SECTOR_LIST;
  const selSec = sel && CQ.SECTORS[sel];
  const attackable = (id) => !CQ.camp.canAttack(st, id);
  const sources = sel && st.sec[sel] && st.sec[sel].owner !== 'player' && attackable(sel) ? list.filter((d) => CQ.SECTORS[sel].adj.includes(d.id) && st.sec[d.id].owner === 'player') : [];
  const edges = []; const seen = new Set();
  list.forEach((d) => d.adj.forEach((a) => { const k = [d.id, a].sort().join('|'); if (!seen.has(k)) { seen.add(k); edges.push([d, CQ.SECTORS[a]]); } }));
  const col = (o) => (CQ.FACTIONS[o] || CQ.FACTIONS.neutral).color;
  return (
    <svg viewBox={`0 0 ${vw} 740`} preserveAspectRatio="xMidYMid meet" style={{ width: '100%', height: '100%', display: 'block', background: '#050810', borderRadius: 8 }}>
      <defs>
        <linearGradient id="cqSky" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stopColor="#1E1236" /><stop offset="0.45" stopColor="#0C1220" /><stop offset="1" stopColor="#050A08" /></linearGradient>
        <pattern id="cqWin" width="14" height="12" patternUnits="userSpaceOnUse"><rect x="2" y="2" width="4" height="5" fill="#FFD890" opacity="0.13" /><rect x="8" y="5" width="3" height="4" fill="#7FD0FF" opacity="0.09" /></pattern>
        <pattern id="cqPipe" width="22" height="22" patternUnits="userSpaceOnUse"><path d="M0 11 H22 M11 0 V22" stroke="#FF8A3A" strokeWidth="1" opacity="0.07" /></pattern>
        <filter id="cqGlow" x="-30%" y="-30%" width="160%" height="160%"><feGaussianBlur stdDeviation="3" /></filter>
        <marker id="cqArrow" markerWidth="8" markerHeight="8" refX="6" refY="4" orient="auto"><path d="M0 0 L8 4 L0 8 Z" fill="#FF5468" /></marker>
      </defs>
      <rect width={vw} height="740" fill="url(#cqSky)" />
      {/* distant towers */}
      {Array.from({ length: Math.round(26 * sx) }, (_, i) => { const x = i * 42 - 10, h = 40 + ((i * 53) % 90); return <rect key={i} x={x} y={0} width={28 + (i % 3) * 8} height={h} fill="#10162A" opacity="0.8" />; })}
      {CQ_TIERS.map((t, i) => (
        <g key={t.name}>
          <rect x="0" y={t.y - 56} width={vw} height="112" fill={t.a} opacity="0.55" />
          <rect x="0" y={t.y - 56} width={vw} height="112" fill={i < 3 ? 'url(#cqWin)' : 'url(#cqPipe)'} />
          <rect x="0" y={t.y - 56} width={vw} height="1.4" fill={t.glow} opacity="0.35" />
          <text x="12" y={t.y - 40} fontSize="10" fontFamily="monospace" fontWeight="700" fill={t.glow} opacity="0.9" letterSpacing="2">{t.name}</text>
          <text x="12" y={t.y - 28} fontSize="8" fontFamily="monospace" fill={t.glow} opacity="0.5">{t.lvl}</text>
        </g>
      ))}
      {/* lanes between adjacent sectors */}
      {edges.map(([a, b]) => {
        const oa = st.sec[a.id].owner, ob = st.sec[b.id].owner; const same = oa === ob && oa !== 'neutral'; const front = (oa === 'player') !== (ob === 'player') && (oa === 'player' || ob === 'player');
        return <line key={a.id + b.id} x1={a.x * sx} y1={a.y} x2={b.x * sx} y2={b.y} stroke={same ? col(oa) : front ? '#FF5468' : '#2A3448'} strokeWidth={same ? 2.4 : 1.6} opacity={same ? 0.7 : front ? 0.75 : 0.9} strokeDasharray={same ? '' : '6 5'}>{front && <animate attributeName="stroke-dashoffset" from="22" to="0" dur="1.4s" repeatCount="indefinite" />}</line>;
      })}
      {/* attack arrows from your adjacent sectors to the selected target */}
      {sources.map((s0) => { const s = { ...s0, x: s0.x * sx }; const t0 = CQ.SECTORS[sel]; const t = { ...t0, x: t0.x * sx }; const dx = t.x - s.x, dy = t.y - s.y, d = Math.hypot(dx, dy) || 1; const x1 = s.x + (dx / d) * 54, y1 = s.y + (dy / d) * 36, x2 = t.x - (dx / d) * 62, y2 = t.y - (dy / d) * 42; return <path key={s.id} d={`M${x1} ${y1} L${x2} ${y2}`} stroke="#FF5468" strokeWidth="3" strokeDasharray="9 6" markerEnd="url(#cqArrow)" fill="none">{<animate attributeName="stroke-dashoffset" from="30" to="0" dur="0.9s" repeatCount="indefinite" />}</path>; })}
      {list.map((d) => {
        const s = st.sec[d.id]; const own = s.owner, c = col(own); const isSel = d.id === sel, canAtk = own !== 'player' && attackable(d.id), seeing = intelOf(d.id);
        const x0 = d.x * sx - 66, y0 = d.y - 42; const fx = CQ.camp.fx(s); const pw = CQ.camp.power(s.gar);
        return (
          <g key={d.id} onClick={() => onSelect(d.id)} onMouseEnter={() => setHover(d.id)} onMouseLeave={() => setHover(null)} style={{ cursor: 'pointer' }}>
            {(isSel || hover === d.id) && <rect x={x0 - 5} y={y0 - 5} width="142" height="94" rx="10" fill="none" stroke={isSel ? '#00FFC8' : '#FFFFFF'} strokeWidth={isSel ? 2.4 : 1.2} strokeDasharray={isSel ? '7 4' : ''} opacity={isSel ? 1 : 0.5}>{isSel && <animate attributeName="stroke-dashoffset" from="22" to="0" dur="1.6s" repeatCount="indefinite" />}</rect>}
            {canAtk && <rect x={x0 - 3} y={y0 - 3} width="138" height="90" rx="9" fill="none" stroke="#FF5468" strokeWidth="1.8"><animate attributeName="opacity" values="0.25;1;0.25" dur="1.5s" repeatCount="indefinite" /></rect>}
            <rect x={x0 - 1} y={y0 - 1} width="134" height="86" rx="7" fill={c} opacity="0.25" filter="url(#cqGlow)" />
            <rect x={x0} y={y0} width="132" height="84" rx="6" fill="#0A1020" stroke={c} strokeWidth={d.isHQ ? 3 : 1.6} />
            <svg x={x0 + 1} y={y0 + 1} width="130" height="50" viewBox="0 0 132 50" preserveAspectRatio="xMidYMid slice"><CQScene theme={d.theme} id={d.id} /></svg>
            <rect x={x0 + 1} y={y0 + 1} width="130" height="49" rx="5" fill={c} opacity={own === 'neutral' ? 0.0 : 0.16} />
            {/* pennant */}
            <polygon points={`${x0 + 6},${y0 + 2} ${x0 + 24},${y0 + 2} ${x0 + 20},${y0 + 9} ${x0 + 24},${y0 + 16} ${x0 + 6},${y0 + 16}`} fill={c} opacity="0.95" />
            <text x={x0 + 14} y={y0 + 12} fontSize="8" textAnchor="middle" fontFamily="monospace" fontWeight="700" fill="#06101A">{own === 'player' ? 'YOU' : own === 'neutral' ? '' : own === 'black_sun' ? 'BS' : own === 'csf' ? 'CSF' : 'EX'}</text>
            {d.isHQ && <text x={x0 + 124} y={y0 + 14} fontSize="12" textAnchor="end" fill="#FFD36A">★</text>}
            <rect x={x0 + 82} y={y0 + 34} width="48" height="13" rx="3" fill="#060A12" opacity="0.8" />
            <text x={x0 + 106} y={y0 + 44} fontSize="9" textAnchor="middle" fontFamily="monospace" fontWeight="700" fill="#FFD36A">{d.income + fx.income}¢</text>
            <text x={x0 + 6} y={y0 + 61} fontSize="8.6" fontFamily="monospace" fontWeight="700" fill="#EAF0FF" textLength={d.name.length * 5.2 > 120 ? 120 : undefined} lengthAdjust="spacingAndGlyphs">{d.name}</text>
            {own === 'player' || seeing ? <CQUnitStrip gar={s.gar} x={x0 + 6} y={y0 + 68} /> : <text x={x0 + 6} y={y0 + 77} fontSize="8" fontFamily="monospace" fill={pw / 45 > 28 ? '#FF8A8A' : '#B8C0D4'}>? {CQ.camp.band(pw).toUpperCase()}</text>}
            {Array.from({ length: CQ.camp.slots(s) }, (_, i) => <rect key={i} x={x0 + 100 + i * 6.4} y={y0 + 70} width="5" height="8" rx="1" fill={s.bld[i] ? '#FFD36A' : '#1C2638'} stroke="#0A1020" strokeWidth="0.5" />)}
            {s.tired && CQ.camp.total(s.tired) > 0 && own === 'player' && <text x={x0 + 126} y={y0 + 61} fontSize="7" textAnchor="end" fontFamily="monospace" fill="#FF9A5A">SPENT {CQ.camp.total(s.tired)}</text>}
          </g>
        );
      })}
    </svg>
  );
}
// ===== CONQUEST UI MAP END =====

// ===== CONQUEST UI BATTLE BEGIN =====
const CQ_TS = 40;
function CQHpBar({ hp, max, w = 30, shield }) {
  const f = Math.max(0, Math.min(1, hp / max));
  return <g><rect x="0" y="0" width={w} height="4" rx="1.5" fill="#05080C" opacity="0.85" /><rect x="0.6" y="0.6" width={(w - 1.2) * f} height="2.8" rx="1" fill={f > 0.6 ? '#3AE87A' : f > 0.3 ? '#FFC24A' : '#FF4A5A'} />{shield > 0 && <rect x="0" y="-2.4" width={Math.min(w, shield * 2.5)} height="1.8" fill="#7FD0FF" />}</g>;
}
function CQDecor({ k, x, y, r, acc }) {
  const cx = x * CQ_TS + 8 + r * 24, cy = y * CQ_TS + 10 + r * 20;
  if (k === 'lamp') return <g><rect x={cx} y={cy} width="2" height="12" fill="#2A3040" /><circle cx={cx + 1} cy={cy} r="3" fill={acc} opacity="0.9" /><circle cx={cx + 1} cy={cy} r="9" fill={acc} opacity="0.12" /></g>;
  if (k === 'plant') return <g><rect x={cx} y={cy + 6} width="8" height="5" fill="#4A3A2A" /><circle cx={cx + 4} cy={cy + 3} r="5" fill="#2E7A48" /><circle cx={cx + 2} cy={cy + 1} r="3" fill="#48A868" /></g>;
  if (k === 'banner') return <g><rect x={cx} y={cy} width="2" height="14" fill="#6A6A7A" /><polygon points={`${cx + 2},${cy} ${cx + 12},${cy + 2} ${cx + 12},${cy + 9} ${cx + 2},${cy + 11}`} fill={acc} opacity="0.85" /></g>;
  if (k === 'crate') return <g><rect x={cx} y={cy + 2} width="10" height="9" fill="#5A4A38" /><rect x={cx} y={cy + 2} width="10" height="2" fill="#7A6A52" /><line x1={cx} y1={cy + 2} x2={cx + 10} y2={cy + 11} stroke="#3A2E22" strokeWidth="0.8" /></g>;
  if (k === 'vent') return <g><rect x={cx} y={cy + 3} width="12" height="8" fill="#1A2028" /><rect x={cx + 1} y={cy + 4} width="10" height="1.4" fill="#0A0E14" /><rect x={cx + 1} y={cy + 7} width="10" height="1.4" fill="#0A0E14" /></g>;
  if (k === 'neon') return <g><rect x={cx} y={cy + 2} width="14" height="5" fill={acc} opacity="0.9" /><rect x={cx - 3} y={cy - 1} width="20" height="11" fill={acc} opacity="0.12" /></g>;
  if (k === 'pipe') return <g><rect x={cx} y={cy + 4} width="18" height="4" fill="#4A4038" /><rect x={cx} y={cy + 4} width="18" height="1.2" fill="#7A6A58" /><rect x={cx + 6} y={cy + 2} width="3" height="8" fill="#6A5A48" /></g>;
  if (k === 'barrel') return <g><rect x={cx} y={cy + 1} width="8" height="11" rx="2" fill="#7A4A2A" /><rect x={cx} y={cy + 4} width="8" height="1.4" fill="#2A1A10" /><rect x={cx} y={cy + 8} width="8" height="1.4" fill="#2A1A10" /></g>;
  return null;
}
function CQBoardTile({ th, x, y, t }) {
  const px = x * CQ_TS, py = y * CQ_TS; const h = (x * 73 + y * 151) % 7;
  return <rect x={px} y={py} width={CQ_TS} height={CQ_TS} fill={th.floor[(x + y) % 2]} opacity={0.92 + h * 0.01} />;
}
function CQRoof({ th, px, py, k }) {
  const a = th.accent;
  if (k === 0) return <g><rect x={px + 6} y={py - 14} width="6" height="10" fill={th.wall[1]} /><rect x={px + 5} y={py - 16} width="8" height="3" fill={a} opacity="0.8" /></g>;
  if (k === 1) return <g><rect x={px + 24} y={py - 6} width="12" height="8" fill={th.wall[1]} /><rect x={px + 26} y={py - 4} width="8" height="1.6" fill="#000" opacity="0.5" /><rect x={px + 26} y={py - 1} width="8" height="1.6" fill="#000" opacity="0.5" /></g>;
  if (k === 2) return <g><line x1={px + 30} y1={py - 8} x2={px + 30} y2={py - 22} stroke={th.wall[1]} strokeWidth="2" /><circle cx={px + 30} cy={py - 23} r="2.4" fill={a}><animate attributeName="opacity" values="1;0.2;1" dur="1.6s" repeatCount="indefinite" /></circle></g>;
  if (k === 3) return <g><rect x={px + 4} y={py - 4} width="16" height="9" fill={a} opacity="0.35" /><rect x={px + 6} y={py - 2} width="12" height="5" fill="#FFFFFF" opacity="0.12" /></g>;
  if (k === 4) return <g><circle cx={px + 14} cy={py - 2} r="7" fill={th.wall[1]} /><circle cx={px + 14} cy={py - 2} r="4" fill={a} opacity="0.45" /></g>;
  return null;
}
function CQParticles({ th, w, h }) {
  const kind = th.hazard; const n = 22; const colr = kind === 'fire' ? '#FF9A3A' : kind === 'steam' ? '#DCE8EE' : kind === 'acid' ? '#7AF0A0' : '#FFE890';
  return <g pointerEvents="none">{Array.from({ length: n }, (_, i) => { const x = (i * 97) % w, y0 = (i * 53) % h; return <circle key={i} cx={x} cy={y0} r={1.4 + (i % 3) * 0.7} fill={colr} opacity="0.5"><animate attributeName="cy" values={`${y0};${y0 - 120};${y0}`} dur={6 + (i % 5) * 1.6 + 's'} repeatCount="indefinite" /><animate attributeName="opacity" values="0;0.65;0" dur={6 + (i % 5) * 1.6 + 's'} repeatCount="indefinite" /></circle>; })}</g>;
}
function CQBoardLayers({ b, th }) {
  const tiles = [];
  for (let y = 0; y < b.h; y++) for (let x = 0; x < b.w; x++) tiles.push(<CQBoardTile key={x + ',' + y} th={th} x={x} y={y} t={b.tiles[y][x]} />);
  const walls = [], covers = [], hazards = [], voids = [];
  for (let y = 0; y < b.h; y++) for (let x = 0; x < b.w; x++) {
    const t = b.tiles[y][x], px = x * CQ_TS, py = y * CQ_TS; const below = b.tiles[y + 1] && b.tiles[y + 1][x] === 'w';
    if (t === 'w') {
      const lit = ((x * 31 + y * 17) % 5) < 2;
      walls.push(<g key={'w' + x + ',' + y}>
        {!below && <rect x={px} y={py + CQ_TS - 14} width={CQ_TS} height="14" fill={th.wall[1]} />}
        <rect x={px} y={py - 8} width={CQ_TS} height={CQ_TS - (below ? -8 : 6)} fill={th.wall[0]} opacity="0.92" />
        <rect x={px} y={py - 8} width={CQ_TS} height="3" fill="#FFFFFF" opacity="0.14" />
        {!below && <rect x={px + 5} y={py + CQ_TS - 10} width="6" height="4" fill={th.accent} opacity={lit ? 0.9 : 0.25} />}
        {!below && <rect x={px + 18} y={py + CQ_TS - 10} width="6" height="4" fill={th.accent} opacity={lit ? 0.3 : 0.8} />}
        {(x + y) % 3 === 0 && <rect x={px + 12} y={py + 2} width="14" height="9" fill={th.wall[1]} opacity="0.7" />}
        {CQRoof({ th, px, py, k: (x * 7 + y * 13) % 6 })}
      </g>);
    } else if (t === 'c') {
      covers.push(<g key={'c' + x + ',' + y}><rect x={px + 5} y={py + 12} width={CQ_TS - 10} height="18" rx="2" fill={th.cover} opacity="0.95" /><rect x={px + 5} y={py + 12} width={CQ_TS - 10} height="4" fill="#FFFFFF" opacity="0.28" /><rect x={px + 5} y={py + 26} width={CQ_TS - 10} height="4" fill="#000" opacity="0.28" /><line x1={px + 9} y1={py + 18} x2={px + CQ_TS - 9} y2={py + 24} stroke="#000" strokeWidth="1" opacity="0.2" /></g>);
    } else if (t === 'h') {
      const kind = th.hazard; const colr = kind === 'fire' ? '#FF7A1A' : kind === 'steam' ? '#CFE0E8' : kind === 'acid' ? '#6AE87A' : '#FFE070';
      hazards.push(<g key={'h' + x + ',' + y}><rect x={px + 2} y={py + 2} width={CQ_TS - 4} height={CQ_TS - 4} rx="6" fill={colr} opacity="0.18"><animate attributeName="opacity" values="0.1;0.3;0.1" dur={1.2 + (x % 3) * 0.3 + 's'} repeatCount="indefinite" /></rect>
        {kind === 'fire' ? <polygon points={`${px + 12},${py + 32} ${px + 17},${py + 12} ${px + 22},${py + 24} ${px + 27},${py + 10} ${px + 30},${py + 32}`} fill={colr} opacity="0.85"><animate attributeName="opacity" values="0.5;1;0.5" dur="0.7s" repeatCount="indefinite" /></polygon>
          : kind === 'sparks' ? <polyline points={`${px + 8},${py + 28} ${px + 16},${py + 14} ${px + 22},${py + 24} ${px + 32},${py + 8}`} fill="none" stroke={colr} strokeWidth="2"><animate attributeName="opacity" values="0.2;1;0.2" dur="0.5s" repeatCount="indefinite" /></polyline>
            : <g><circle cx={px + 14} cy={py + 24} r="6" fill={colr} opacity="0.5"><animate attributeName="cy" values={`${py + 28};${py + 10}`} dur="1.8s" repeatCount="indefinite" /></circle><circle cx={px + 26} cy={py + 28} r="5" fill={colr} opacity="0.4"><animate attributeName="cy" values={`${py + 30};${py + 12}`} dur="2.2s" repeatCount="indefinite" /></circle></g>}</g>);
    } else if (t === 'x') {
      voids.push(<g key={'x' + x + ',' + y}><rect x={px} y={py} width={CQ_TS} height={CQ_TS} fill={th.river} opacity={th.voidEdges ? 0.98 : 0.8} /><rect x={px} y={py + 10} width={CQ_TS} height="2" fill="#FFFFFF" opacity="0.18"><animate attributeName="opacity" values="0.05;0.3;0.05" dur={1.6 + (y % 3) * 0.4 + 's'} repeatCount="indefinite" /></rect><rect x={px + 6} y={py + 26} width="22" height="2" fill="#FFFFFF" opacity="0.12" /></g>);
    }
  }
  return <g>{tiles}{voids}{hazards}{b.decor.map((d, i) => <CQDecor key={i} k={d.k} x={d.x} y={d.y} r={d.r} acc={th.accent} />)}{covers}{walls}</g>;
}
function CQBattleBoard({ b, tick, selId, reach, targets, abTargets, mode, fx, hover, onTile, onHover }) {
  const th = CQ.THEMES[b.theme] || CQ.THEMES.shadow; const W = b.w * CQ_TS, H = b.h * CQ_TS;
  const layers = React.useMemo(() => <CQBoardLayers b={b} th={th} />, [b.seed, b.theme, b.tiles.length, b.nodeKey]);
  const click = (e) => { const r = e.currentTarget.getBoundingClientRect(); const x = Math.floor(((e.clientX - r.left) / r.width) * b.w), y = Math.floor(((e.clientY - r.top) / r.height) * b.h); onTile(x, y); };
  const move = (e) => { const r = e.currentTarget.getBoundingClientRect(); const x = Math.floor(((e.clientX - r.left) / r.width) * b.w), y = Math.floor(((e.clientY - r.top) / r.height) * b.h); onHover(x, y); };
  const pz = b.attacker === 'p' ? b.zoneL : b.zoneR, ez = b.attacker === 'p' ? b.zoneR : b.zoneL;
  const units = b.units.slice().sort((a, c) => a.y - c.y);
  const pad = 6;
  return (
    <svg viewBox={`-${pad} -${pad + 8} ${W + pad * 2} ${H + pad * 2 + 8}`} style={{ width: '100%', display: 'block', background: th.sky[0], borderRadius: 8, cursor: 'crosshair' }}>
      <defs><radialGradient id="cqVig" cx="0.5" cy="0.5" r="0.75"><stop offset="0.6" stopColor="#000" stopOpacity="0" /><stop offset="1" stopColor="#000" stopOpacity="0.55" /></radialGradient></defs>
      {layers}
      {/* deployment zones */}
      {pz.map((z, i) => <rect key={'pz' + i} x={z.x * CQ_TS} y={z.y * CQ_TS} width={CQ_TS} height={CQ_TS} fill="#2FC8FF" opacity={b.phase === 'deploy' ? 0.2 : 0.07} stroke="#2FC8FF" strokeOpacity={b.phase === 'deploy' ? 0.55 : 0.15} strokeWidth="1" />)}
      {ez.map((z, i) => <rect key={'ez' + i} x={z.x * CQ_TS} y={z.y * CQ_TS} width={CQ_TS} height={CQ_TS} fill="#FF5468" opacity={b.phase === 'deploy' ? 0.16 : 0.06} stroke="#FF5468" strokeOpacity={b.phase === 'deploy' ? 0.4 : 0.12} strokeWidth="1" />)}
      {/* objective node */}
      <g transform={`translate(${b.node.x * CQ_TS + CQ_TS / 2},${b.node.y * CQ_TS + CQ_TS / 2})`}>
        <circle r="18" fill="none" stroke="#FFD36A" strokeWidth="2" strokeDasharray="5 4"><animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="12s" repeatCount="indefinite" /></circle>
        <circle r="9" fill="#FFD36A" opacity="0.25"><animate attributeName="r" values="7;12;7" dur="2s" repeatCount="indefinite" /></circle>
        <polygon points="0,-8 7,0 0,8 -7,0" fill="#FFD36A" />
        <text y="30" textAnchor="middle" fontSize="8" fontFamily="monospace" fill="#FFD36A" fontWeight="700">COMMAND NODE</text>
      </g>
      {/* highlights */}
      {reach && Array.from(reach.values()).map((n) => (n.px === null ? null : <rect key={'r' + n.x + ',' + n.y} x={n.x * CQ_TS + 2} y={n.y * CQ_TS + 2} width={CQ_TS - 4} height={CQ_TS - 4} rx="5" fill="#2FC8FF" opacity="0.28" stroke="#2FC8FF" strokeOpacity="0.8" />))}
      {targets && targets.map((t) => <g key={'t' + t.id}><circle cx={t.x * CQ_TS + CQ_TS / 2} cy={t.y * CQ_TS + CQ_TS / 2} r="19" fill="none" stroke="#FF5468" strokeWidth="2.4"><animate attributeName="r" values="17;21;17" dur="1s" repeatCount="indefinite" /></circle></g>)}
      {abTargets && abTargets.map((t) => <circle key={'a' + t.id} cx={t.x * CQ_TS + CQ_TS / 2} cy={t.y * CQ_TS + CQ_TS / 2} r="19" fill="none" stroke={mode === 'heal' ? '#3AE87A' : '#FFA03A'} strokeWidth="2.6" strokeDasharray="4 3" />)}
      {hover && b.phase === 'battle' && mode === 'orbital' && <rect x={(hover.x - 1) * CQ_TS} y={(hover.y - 1) * CQ_TS} width={CQ_TS * 3} height={CQ_TS * 3} fill="#FF5468" opacity="0.28" stroke="#FF5468" strokeWidth="2" />}
      {/* units */}
      {units.map((u) => {
        if (u.x < 0) return null; const mine = u.team === 'p'; const c = mine ? '#2FC8FF' : '#FF5468'; const d = CQ.UNITS[u.type]; const spent = mine && b.phase === 'battle' && b.turn === 'p' && u.acted;
        return (
          <g key={u.id} style={{ transform: `translate(${u.x * CQ_TS}px,${u.y * CQ_TS}px)`, transition: 'transform 0.3s ease-out' }}>
            <ellipse cx={CQ_TS / 2} cy={CQ_TS - 6} rx="14" ry="5" fill="#000" opacity="0.4" />
            {u.id === selId && <circle cx={CQ_TS / 2} cy={CQ_TS / 2} r="19" fill="none" stroke="#FFFFFF" strokeWidth="2.4"><animate attributeName="opacity" values="1;0.4;1" dur="1.1s" repeatCount="indefinite" /></circle>}
            <circle cx={CQ_TS / 2} cy={CQ_TS / 2} r="16" fill={mine ? '#0C2234' : '#341018'} stroke={c} strokeWidth="2.2" opacity={spent ? 0.55 : 1} />
            <g transform={`translate(${CQ_TS / 2 - 12},${CQ_TS / 2 - 12})`} opacity={spent ? 0.55 : 1}><CQUnitGlyph type={u.type} size={24} color={mine ? d.col : '#FF9AA6'} /></g>
            <g transform="translate(5,2)"><CQHpBar hp={u.hp} max={u.max} w={30} shield={u.shield} /></g>
            {u.sup > 0 && <text x="4" y="14" fontSize="9" fill="#FFC24A">⇩</text>}
            {u.shield > 0 && <text x={CQ_TS - 8} y="14" fontSize="9" fill="#7FD0FF">◈</text>}
            {tick >= 0 && u.cd && d.ab && u.cd[d.ab.id] > 0 && <text x={CQ_TS - 4} y={CQ_TS - 3} fontSize="9" textAnchor="end" fill="#FFC24A" fontFamily="monospace">{u.cd[d.ab.id]}</text>}
          </g>
        );
      })}
      {/* effects */}
      {fx.map((f) => {
        const cx = (x) => x * CQ_TS + CQ_TS / 2;
        if (f.k === 'shot') { const x1 = cx(f.from[0]), y1 = cx(f.from[1]), x2 = cx(f.to[0]), y2 = cx(f.to[1]); const col = f.team === 'p' ? '#7FE8FF' : '#FF8A9A'; return <g key={f.id}><line x1={x1} y1={y1} x2={x2} y2={y2} stroke={col} strokeWidth={f.big ? 4 : 2} strokeLinecap="round" opacity="0.9"><animate attributeName="opacity" from="1" to="0" dur="0.45s" fill="freeze" /></line><circle r={f.big ? 5 : 3} fill="#FFF"><animate attributeName="cx" from={x1} to={x2} dur="0.22s" fill="freeze" /><animate attributeName="cy" from={y1} to={y2} dur="0.22s" fill="freeze" /><animate attributeName="opacity" from="1" to="0" dur="0.3s" fill="freeze" /></circle></g>; }
        if (f.k === 'hit') return <text key={f.id} x={cx(f.x)} y={f.y * CQ_TS + 10} textAnchor="middle" fontSize="15" fontWeight="800" fontFamily="monospace" fill="#FFE070" stroke="#000" strokeWidth="0.8">-{f.v}<animate attributeName="y" from={f.y * CQ_TS + 10} to={f.y * CQ_TS - 16} dur="0.9s" fill="freeze" /><animate attributeName="opacity" from="1" to="0" dur="1s" fill="freeze" /></text>;
        if (f.k === 'miss') return <text key={f.id} x={cx(f.x)} y={f.y * CQ_TS + 10} textAnchor="middle" fontSize="11" fontWeight="700" fontFamily="monospace" fill="#AAB">blocked<animate attributeName="y" from={f.y * CQ_TS + 10} to={f.y * CQ_TS - 10} dur="0.9s" fill="freeze" /><animate attributeName="opacity" from="1" to="0" dur="1s" fill="freeze" /></text>;
        if (f.k === 'heal') return <text key={f.id} x={cx(f.x)} y={f.y * CQ_TS + 10} textAnchor="middle" fontSize="15" fontWeight="800" fontFamily="monospace" fill="#3AE87A" stroke="#000" strokeWidth="0.8">+{f.v}<animate attributeName="y" from={f.y * CQ_TS + 10} to={f.y * CQ_TS - 16} dur="0.9s" fill="freeze" /><animate attributeName="opacity" from="1" to="0" dur="1s" fill="freeze" /></text>;
        if (f.k === 'kill') return <circle key={f.id} cx={cx(f.x)} cy={cx(f.y)} r="6" fill="#FFB04A" opacity="0.9"><animate attributeName="r" from="6" to="30" dur="0.5s" fill="freeze" /><animate attributeName="opacity" from="0.9" to="0" dur="0.5s" fill="freeze" /></circle>;
        if (f.k === 'smash') return <circle key={f.id} cx={cx(f.x)} cy={cx(f.y)} r="10" fill="none" stroke="#FFC24A" strokeWidth="4"><animate attributeName="r" from="10" to="46" dur="0.4s" fill="freeze" /><animate attributeName="opacity" from="1" to="0" dur="0.45s" fill="freeze" /></circle>;
        if (f.k === 'shell') { const x2 = cx(f.to[0]), y2 = cx(f.to[1]); return <circle key={f.id} cx={x2} cy={y2} r="8" fill="#FFA03A" opacity="0.8"><animate attributeName="r" from="8" to="50" dur="0.55s" fill="freeze" /><animate attributeName="opacity" from="0.9" to="0" dur="0.55s" fill="freeze" /></circle>; }
        if (f.k === 'orbital') return <g key={f.id}><rect x={cx(f.x) - 4} y="-8" width="8" height={cx(f.y) + 8} fill="#FF5468" opacity="0.8"><animate attributeName="opacity" from="0.9" to="0" dur="0.7s" fill="freeze" /></rect><rect x={(f.x - 1) * CQ_TS} y={(f.y - 1) * CQ_TS} width={CQ_TS * 3} height={CQ_TS * 3} fill="#FFA03A" opacity="0.7"><animate attributeName="opacity" from="0.8" to="0" dur="0.9s" fill="freeze" /></rect></g>;
        return null;
      })}
      <CQParticles th={th} w={W} h={H} />
      <rect x="-6" y="-14" width={W + 12} height={H + 20} fill="url(#cqVig)" pointerEvents="none" />
      <rect x="0" y="0" width={W} height={H} fill="transparent" onClick={click} onMouseMove={move} onMouseLeave={() => onHover(-1, -1)} />
    </svg>
  );
}
// ===== CONQUEST UI BATTLE END =====

// ===== CONQUEST UI BATTLE CONTROLLER BEGIN =====
function CQBattle({ battle, meta, title, onDone }) {
  const tb = CQ.tb, U = CQ.UNITS;
  const bRef = React.useRef(battle); const b = bRef.current;
  const [tick, setTick] = React.useState(0);
  const [selId, setSelId] = React.useState(null);
  const [mode, setMode] = React.useState('idle');
  const [hover, setHover] = React.useState(null);
  const [fx, setFx] = React.useState([]);
  const [busy, setBusy] = React.useState(false);
  const [placeId, setPlaceId] = React.useState(null);
  const [msg, setMsg] = React.useState('');
  const lastN = React.useRef(0); const fxId = React.useRef(1);
  const rndRef = React.useRef(tb.mb32((Date.now() % 1000003) + 17)); const rnd = () => rndRef.current();
  const alive = React.useRef(true);
  React.useEffect(() => () => { alive.current = false; }, []);
  React.useEffect(() => { window.__cqBattle = b; return () => { if (window.__cqBattle === b) window.__cqBattle = null; }; }); // test hook for automated playtests
  const bump = () => setTick((t) => t + 1);
  const pull = () => {
    const fresh = b.events.filter((e) => e.n > lastN.current); if (!fresh.length) return; lastN.current = fresh[fresh.length - 1].n;
    const items = fresh.filter((e) => ['shot', 'hit', 'miss', 'heal', 'kill', 'smash', 'shell', 'orbital'].includes(e.k)).map((e) => ({ ...e, id: fxId.current++ }));
    if (!items.length) return; setFx((f) => f.concat(items).slice(-30));
    setTimeout(() => { if (alive.current) setFx((f) => f.filter((q) => !items.some((i) => i.id === q.id))); }, 1100);
  };
  const after = () => { tb.sweepDead(b); tb.checkEnd(b); pull(); bump(); };
  const sel = b.units.find((u) => u.id === selId && u.hp > 0);
  const mine = b.turn === 'p' && b.phase === 'battle' && !busy;
  const reach = sel && mine && sel.team === 'p' && !sel.moved ? tb.reach(b, sel) : null;
  const targets = sel && mine && sel.team === 'p' && !sel.acted && mode === 'idle' ? tb.targetsFrom(b, sel, sel.x, sel.y) : null;
  const abOf = (u) => U[u.type].ab;
  let abTargets = null;
  if (sel && mine && mode === 'ability' && abOf(sel)) {
    const id = abOf(sel).id;
    if (id === 'heal') abTargets = b.units.filter((q) => q.hp > 0 && q.team === 'p' && q.id !== sel.id && q.type !== 'trt' && q.hp < q.max && Math.hypot(q.x - sel.x, q.y - sel.y) <= 4.001);
    else if (id === 'shell') abTargets = b.units.filter((q) => q.hp > 0 && q.team === 'e' && Math.hypot(q.x - sel.x, q.y - sel.y) <= sel.rng + 0.001);
    else abTargets = tb.targetsFrom(b, sel, sel.x, sel.y);
  }
  const unplaced = b.units.filter((u) => u.team === 'p' && u.x < 0 && !u.immobile);
  const placed = b.units.filter((u) => u.team === 'p' && u.x >= 0 && !u.immobile);
  const cap = b.depCap;

  const runEnemy = () => {
    setBusy(true);
    const step = () => {
      if (!alive.current) return;
      if (b.phase !== 'battle' || b.turn !== 'e') { setBusy(false); bump(); return; }
      const e = tb.aiStep(b, 'e', rnd); pull(); bump();
      if (e) { setTimeout(step, 430); }
      else { tb.endTurn(b, rnd); pull(); bump(); if (b.phase === 'battle' && b.turn === 'e') setTimeout(step, 430); else setBusy(false); }
    };
    setTimeout(step, 350);
  };
  const begin = () => {
    if (b.phase !== 'deploy') return; tb.begin(b); pull(); bump(); setMsg('');
    if (b.turn === 'e') runEnemy();
  };
  const endMyTurn = () => {
    if (!mine) return; tb.endTurn(b, rnd); pull(); setSelId(null); setMode('idle'); bump();
    if (b.phase === 'battle' && b.turn === 'e') runEnemy();
  };
  const autoRest = () => { if (b.phase !== 'battle' || busy) return; let g = 0; while (b.phase === 'battle' && g++ < 80) tb.aiTeamTurn(b, b.turn, rnd); pull(); bump(); };
  const doRetreat = () => { if (b.phase !== 'battle' || b.attacker !== 'p' || busy) return; if (window.confirm('Retreat? Survivors fall back, but about a quarter of them are lost in the withdrawal.')) { tb.retreat(b); bump(); } };
  const onHover = (x, y) => setHover(x < 0 ? null : { x, y });
  const onTile = (x, y) => {
    if (x < 0 || y < 0 || x >= b.w || y >= b.h) return;
    if (b.phase === 'deploy') {
      const here = tb.unitAt(b, x, y);
      if (here && here.team === 'p') { tb.undeploy(b, here.id); setPlaceId(here.id); bump(); return; }
      if (placeId == null) { setMsg('Pick a unit from the roster first.'); return; }
      const u = b.units.find((q) => q.id === placeId);
      if (u && tb.deployAt(b, 'p', placeId, x, y)) { const nxt = b.units.find((q) => q.team === 'p' && q.x < 0 && !q.immobile && q.type === u.type); setPlaceId(nxt ? nxt.id : null); setMsg(''); bump(); } else setMsg('Place units inside the cyan deployment zone on a free tile.');
      return;
    }
    if (!mine) return;
    const u = tb.unitAt(b, x, y);
    if (mode === 'orbital') { if (tb.orbital(b, 'p', x, y)) { setMode('idle'); after(); } return; }
    if (mode === 'ability' && sel) { const ab = abOf(sel); const px = ab.id === 'smash' ? sel.x : x, py = ab.id === 'smash' ? sel.y : y; if (tb.doAbility(b, sel, px, py, rnd)) { setMode('idle'); setMsg(''); after(); } else setMsg('Not a valid target for that ability.'); return; }
    if (u && u.team === 'p') { setSelId(u.id); setMode('idle'); setMsg(''); return; }
    if (!sel) return;
    if (u && u.team === 'e') { if (sel.acted) { setMsg('That unit has already acted.'); return; } if (tb.canHit(b, sel, u, null, null, false)) { tb.doAttack(b, sel, u, rnd); setMsg(''); after(); } else setMsg('Out of range or no line of sight. Move closer first.'); return; }
    if (!u && reach && !sel.moved && reach.has(x + ',' + y) && (x !== sel.x || y !== sel.y)) { tb.doMove(b, sel, x, y); setMsg(''); pull(); bump(); }
  };
  const useAbility = () => {
    if (!sel || sel.acted) return; const ab = abOf(sel); if (!ab || !tb.abilityReady(sel)) { setMsg('Ability on cooldown.'); return; }
    if (ab.id === 'smash') { if (tb.doAbility(b, sel, sel.x, sel.y, rnd)) { setMode('idle'); setMsg(''); after(); } else setMsg('No enemy adjacent.'); return; }
    setMode(mode === 'ability' ? 'idle' : 'ability'); setMsg(mode === 'ability' ? '' : `${ab.name}: pick a target.`);
  };
  React.useEffect(() => {
    const k = (e) => { if (b.phase === 'battle') { if (e.key === 'e' || e.key === 'E' || e.key === 'Enter') { e.preventDefault(); endMyTurn(); } if (e.key === 'Escape') { setSelId(null); setMode('idle'); } if ((e.key === 'a' || e.key === 'A') && sel) useAbility(); } };
    window.addEventListener('keydown', k); return () => window.removeEventListener('keydown', k);
  });
  const hoverUnit = hover ? tb.unitAt(b, hover.x, hover.y) : null;
  let preview = '';
  if (hoverUnit && sel && sel.team === 'p' && hoverUnit.team === 'e' && mine) { const raw = tb.dmgVs(b, sel, hoverUnit); preview = `${U[sel.type].short} vs ${U[hoverUnit.type].short}: ~${Math.max(0, raw)} damage${raw <= 0 ? ' (armor stops it, chip chance only)' : ''}${tb.canHit(b, sel, hoverUnit, null, null, false) ? '' : ' (out of range or blocked)'}`; }
  const counts = (team) => { const o = {}; b.units.filter((u) => u.team === team && u.hp > 0 && !u.immobile).forEach((u) => { o[u.type] = (o[u.type] || 0) + 1; }); (b.reserve[team] || []).forEach((k) => { o[k] = (o[k] || 0) + 1; }); return o; };
  const done = b.phase === 'done'; const res = b.result;
  const won = done && res.winner === 'p'; const th = CQ.THEMES[b.theme] || {};
  const panel = { background: '#0A101C', border: '1px solid #1B2A40', borderRadius: 8, padding: 10 };
  const btn = (c, dis) => ({ padding: '7px 12px', background: dis ? '#121826' : c + '22', color: dis ? '#4A5468' : c, border: '1px solid ' + (dis ? '#1B2536' : c), borderRadius: 5, cursor: dis ? 'default' : 'pointer', fontFamily: 'monospace', fontSize: 12, fontWeight: 700, letterSpacing: '0.04em' });
  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 80, background: 'radial-gradient(circle at 50% 0%, #101A2E 0%, #05080F 70%)', color: '#E0E8F8', fontFamily: 'monospace', display: 'flex', flexDirection: 'column', overflow: 'auto' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '8px 16px', borderBottom: '1px solid #1B2A40', background: '#070C16', flexWrap: 'wrap' }}>
        <div style={{ color: th.accent || '#2FC8FF', fontWeight: 800, letterSpacing: '0.1em' }}>{title}</div>
        <div style={{ color: '#7A88A8', fontSize: 11 }}>{th.name}</div>
        <div style={{ marginLeft: 'auto', display: 'flex', gap: 14, alignItems: 'center', fontSize: 12 }}>
          {b.phase === 'battle' && <span>ROUND <b style={{ color: '#FFD36A' }}>{b.round}</b>/{b.maxRounds}</span>}
          {b.phase === 'battle' && <span style={{ color: b.turn === 'p' ? '#2FC8FF' : '#FF5468', fontWeight: 800 }}>{b.turn === 'p' ? 'YOUR TURN' : 'ENEMY TURN'}</span>}
          {b.phase === 'battle' && <span style={{ color: '#FFD36A' }} title="The attacker wins by holding the Command Node for two of its turns with no enemy within two tiles.">NODE HOLD {b.hold}/2</span>}
          {b.phase === 'deploy' && <span style={{ color: '#FFD36A' }}>DEPLOYMENT {placed.length}/{placed.length + unplaced.length} placed (cap {cap})</span>}
        </div>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(220px,260px) minmax(0,1fr) minmax(220px,260px)', gap: 10, padding: 10, flex: 1, alignItems: 'start', maxWidth: 1700, width: '100%', margin: '0 auto', boxSizing: 'border-box' }}>
        {/* left: roster or unit panel */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {b.phase === 'deploy' && (
            <div style={panel}>
              <div style={{ color: '#2FC8FF', fontWeight: 800, marginBottom: 6, letterSpacing: '0.08em' }}>DEPLOY YOUR FORCE</div>
              <div style={{ fontSize: 11, color: '#8A98B8', marginBottom: 8 }}>Pick a unit type, then click a tile inside the cyan zone. Click a placed unit to pick it up again. Tanks and heavy units go in front, snipers and medics behind.</div>
              {CQ.UNIT_ORDER.concat(['trt']).map((k) => { const n = unplaced.filter((u) => u.type === k).length; const tot = b.units.filter((u) => u.team === 'p' && u.type === k && !u.immobile).length; if (!tot) return null; const on = placeId != null && (b.units.find((q) => q.id === placeId) || {}).type === k;
                return <div key={k} onClick={() => n && setPlaceId(unplaced.find((u) => u.type === k).id)} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '5px 6px', marginBottom: 3, borderRadius: 5, border: '1px solid ' + (on ? '#2FC8FF' : '#1B2A40'), background: on ? '#0E2A40' : '#0C1422', cursor: n ? 'pointer' : 'default', opacity: n ? 1 : 0.45 }}><CQUnitGlyph type={k} size={24} /><div style={{ flex: 1, fontSize: 11 }}>{U[k].name}<div style={{ color: '#6A7898', fontSize: 9 }}>{U[k].role}</div></div><b style={{ color: '#FFD36A' }}>{n}/{tot}</b></div>; })}
              <div style={{ display: 'flex', gap: 6, marginTop: 8, flexWrap: 'wrap' }}>
                <button style={btn('#34D86A')} onClick={() => { tb.autoDeploy(b, 'p'); setPlaceId(null); bump(); }}>AUTO DEPLOY</button>
                <button style={btn('#FF9A5A')} onClick={() => { b.units.filter((u) => u.team === 'p' && !u.immobile).forEach((u) => tb.undeploy(b, u.id)); setPlaceId(null); bump(); }}>CLEAR</button>
              </div>
              <button style={{ ...btn('#2FC8FF'), width: '100%', marginTop: 8, padding: 11, fontSize: 13 }} onClick={begin}>BEGIN BATTLE ▶</button>
              {b.units.some((u) => u.team === 'p' && u.type !== 'trt') && b.reserve.p.length > 0 && <div style={{ fontSize: 10, color: '#FFC24A', marginTop: 6 }}>{b.reserve.p.length} reserve units arrive in waves.</div>}
            </div>
          )}
          {b.phase !== 'deploy' && (
            <div style={panel}>
              {sel ? (
                <div>
                  <div style={{ display: 'flex', gap: 8, alignItems: 'center', marginBottom: 6 }}><CQUnitGlyph type={sel.type} size={34} color={sel.team === 'p' ? U[sel.type].col : '#FF9AA6'} /><div><div style={{ fontWeight: 800, color: sel.team === 'p' ? '#2FC8FF' : '#FF5468' }}>{U[sel.type].name}</div><div style={{ fontSize: 10, color: '#7A88A8' }}>{U[sel.type].role}{sel.team === 'e' ? ' (enemy)' : ''}</div></div></div>
                  <div style={{ background: '#05080C', borderRadius: 3, height: 8, marginBottom: 6 }}><div style={{ width: (100 * sel.hp / sel.max) + '%', height: '100%', background: '#3AE87A', borderRadius: 3 }} /></div>
                  <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 3, fontSize: 11, marginBottom: 6 }}><span>HP {sel.hp}/{sel.max}{sel.shield > 0 ? ` +${sel.shield}◈` : ''}</span><span>MOVE {sel.mov}</span><span>ATK {sel.atk}</span><span>ARMOR {sel.armor}</span><span>RANGE {sel.rng < 2 ? 'melee' : sel.rng}</span><span>{sel.sup > 0 ? 'SUPPRESSED' : ''}</span></div>
                  <div style={{ fontSize: 10, color: '#9AA8C8', marginBottom: 6 }}>{U[sel.type].passive}</div>
                  {sel.team === 'p' && abOf(sel) && <div><button style={{ ...btn('#FFC24A', !mine || sel.acted || !tb.abilityReady(sel)), width: '100%', background: mode === 'ability' ? '#FFC24A33' : undefined }} onClick={useAbility}>[A] {abOf(sel).name.toUpperCase()}{sel.cd[abOf(sel).id] > 0 ? ` (cooldown ${sel.cd[abOf(sel).id]})` : ''}</button><div style={{ fontSize: 10, color: '#8A98B8', marginTop: 4 }}>{abOf(sel).desc}</div></div>}
                  {sel.team === 'p' && mine && <div style={{ fontSize: 10, color: '#6A7898', marginTop: 6 }}>{sel.moved ? 'Moved. ' : 'Click a blue tile to move. '}{sel.acted ? 'Acted.' : 'Click a red-ringed enemy to shoot.'}</div>}
                </div>
              ) : <div style={{ fontSize: 11, color: '#8A98B8' }}>Click one of your units to select it. Blue tiles show where it can move; red rings show targets in range and line of sight.<div style={{ marginTop: 8, color: '#6A7898' }}>Hotkeys: E end turn · A ability · Esc deselect.</div></div>}
            </div>
          )}
          <div style={panel}>
            <div style={{ fontSize: 11, color: '#2FC8FF', fontWeight: 800, marginBottom: 4 }}>YOUR FORCES</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>{Object.entries(counts('p')).map(([k, n]) => <span key={k} style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: 11 }}><CQUnitGlyph type={k} size={16} />{n}</span>)}</div>
            <div style={{ fontSize: 11, color: '#FF5468', fontWeight: 800, margin: '8px 0 4px' }}>ENEMY FORCES</div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>{Object.entries(counts('e')).map(([k, n]) => <span key={k} style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: 11 }}><CQUnitGlyph type={k} size={16} color="#FF9AA6" />{n}</span>)}</div>
          </div>
        </div>
        {/* centre: board */}
        <div>
          <CQBattleBoard b={b} tick={tick} selId={selId} reach={reach} targets={targets} abTargets={abTargets} mode={mode === 'ability' && sel && abOf(sel) ? (abOf(sel).id === 'heal' ? 'heal' : 'ability') : mode} fx={fx} hover={hover} onTile={onTile} onHover={onHover} />
          <div style={{ minHeight: 18, fontSize: 11, color: msg ? '#FFC24A' : '#8A98B8', marginTop: 6 }}>{msg || preview || (b.phase === 'deploy' ? 'Deployment: place your units, then begin.' : b.turn === 'p' ? 'Your move.' : 'The enemy is moving…')}</div>
        </div>
        {/* right: controls and log */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {b.phase === 'battle' && (
            <div style={panel}>
              <button style={{ ...btn('#2FC8FF', !mine), width: '100%', padding: 11, fontSize: 13 }} onClick={endMyTurn}>END TURN [E] ▶</button>
              {b.orbital.p > 0 && <button style={{ ...btn('#FF5468', !mine), width: '100%', marginTop: 6, background: mode === 'orbital' ? '#FF546833' : undefined }} onClick={() => setMode(mode === 'orbital' ? 'idle' : 'orbital')}>ORBITAL STRIKE ({b.orbital.p})</button>}
              <button style={{ ...btn('#34D86A', busy), width: '100%', marginTop: 6 }} onClick={autoRest}>AUTO-PLAY THE REST</button>
              {b.attacker === 'p' && <button style={{ ...btn('#FF9A5A', busy), width: '100%', marginTop: 6 }} onClick={doRetreat}>RETREAT</button>}
            </div>
          )}
          <div style={panel}>
            <div style={{ fontSize: 11, color: '#FFD36A', fontWeight: 800, marginBottom: 4 }}>BATTLE LOG</div>
            <div style={{ maxHeight: 190, overflowY: 'auto', fontSize: 10, color: '#9AA8C8', lineHeight: 1.5 }}>{b.log.slice().reverse().slice(0, 18).map((l, i) => <div key={i} style={{ color: i === 0 ? '#E8F0FF' : undefined }}>{l}</div>)}</div>
          </div>
          <div style={{ ...panel, fontSize: 10, color: '#8A98B8', lineHeight: 1.5 }}>
            <div style={{ color: '#2FC8FF', fontWeight: 800, marginBottom: 3 }}>FIELD RULES</div>
            Dark blocks block movement and sight. Crates give cover (-2 damage at range). Glowing patches burn at end of turn. Your side moves first when attacking. {b.attacker === 'p' ? 'Hold the Command Node to win early.' : 'Hold out for 22 rounds or destroy the attackers.'}
          </div>
        </div>
      </div>
      {done && (
        <div style={{ position: 'fixed', inset: 0, zIndex: 90, background: 'rgba(3,6,12,0.82)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <div style={{ ...panel, width: 'min(560px,92vw)', padding: 22, border: '2px solid ' + (won ? '#34D86A' : '#FF5468'), boxShadow: '0 0 40px ' + (won ? '#34D86A55' : '#FF546855') }}>
            <div style={{ fontSize: 26, fontWeight: 800, letterSpacing: '0.12em', color: won ? '#34D86A' : '#FF5468' }}>{res.reason === 'retreat' ? 'RETREAT' : won ? (meta.kind === 'defense' ? 'SECTOR HELD' : 'VICTORY') : (meta.kind === 'defense' ? 'SECTOR LOST' : 'DEFEAT')}</div>
            <div style={{ fontSize: 12, color: '#9AA8C8', margin: '4px 0 12px' }}>{res.reason === 'node' ? 'The Command Node was seized.' : res.reason === 'wiped' ? 'One side was wiped out.' : res.reason === 'timeout' ? 'The defenders held out until time ran out.' : 'You pulled your units out.'} Rounds: {res.rounds}.</div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
              {[['p', 'YOUR LOSSES', '#2FC8FF'], ['e', 'ENEMY LOSSES', '#FF5468']].map(([t, lbl, c]) => <div key={t}><div style={{ color: c, fontWeight: 800, fontSize: 11, marginBottom: 4 }}>{lbl}</div>{CQ.UNIT_ORDER.filter((k) => res.lost[t][k] > 0).map((k) => <div key={k} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12 }}><CQUnitGlyph type={k} size={18} />{res.lost[t][k]} {U[k].short}</div>)}{!CQ.UNIT_ORDER.some((k) => res.lost[t][k] > 0) && <div style={{ fontSize: 11, color: '#6A7898' }}>none</div>}</div>)}
            </div>
            <button style={{ ...btn(won ? '#34D86A' : '#FF9A5A'), width: '100%', marginTop: 16, padding: 12, fontSize: 14 }} onClick={() => onDone(res)}>CONTINUE</button>
          </div>
        </div>
      )}
    </div>
  );
}
// ===== CONQUEST UI BATTLE CONTROLLER END =====

// ===== CONQUEST UI MAIN BEGIN =====
const CQ_PANEL = { background: '#0A101C', border: '1px solid #1B2A40', borderRadius: 8, padding: 12 };
const cqBtn = (c, dis, extra) => ({ padding: '6px 12px', background: dis ? '#101624' : c + '22', color: dis ? '#4A5468' : c, border: '1px solid ' + (dis ? '#1B2536' : c), borderRadius: 5, cursor: dis ? 'not-allowed' : 'pointer', fontFamily: 'monospace', fontSize: 11, fontWeight: 700, letterSpacing: '0.04em', ...(extra || {}) });
function CQStepper({ n, max, onChange, step = 1 }) {
  return <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}><button style={cqBtn('#8A98B8', n <= 0, { padding: '1px 7px' })} onClick={() => n > 0 && onChange(Math.max(0, n - step))}>-</button><b style={{ minWidth: 20, textAlign: 'center', color: '#FFD36A', fontSize: 12 }}>{n}</b><button style={cqBtn('#34D86A', n >= max, { padding: '1px 7px' })} onClick={() => n < max && onChange(Math.min(max, n + step))}>+</button></span>;
}
function CQUnitStats({ k }) { const u = CQ.UNITS[k]; return <span style={{ fontSize: 10, color: '#8A98B8' }}>HP {u.hp} · ATK {u.atk} · ARM {u.armor} · RNG {u.rng < 2 ? 'melee' : u.rng} · MOV {u.mov}</span>; }
function CQGarList({ gar, tired }) { const ks = CQ.UNIT_ORDER.filter((k) => (gar[k] || 0) > 0); if (!ks.length) return <div style={{ fontSize: 11, color: '#6A7898' }}>No units stationed.</div>; return <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>{ks.map((k) => <span key={k} title={CQ.UNITS[k].name} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, background: '#0E1626', border: '1px solid #1B2A40', borderRadius: 5, padding: '2px 6px', fontSize: 12 }}><CQUnitGlyph type={k} size={18} />{gar[k]}{tired && tired[k] > 0 ? <i style={{ color: '#FF9A5A', fontSize: 9 }}> ({tired[k]} spent)</i> : null}</span>)}</div>; }

function CoruscantConquestOverlay({ onSuccess, onFailure, startCredits }) {
  const SAVE_KEY = 'swtor5_conquest_v2';
  const [st, setSt] = React.useState(() => { try { const r = JSON.parse(localStorage.getItem(SAVE_KEY) || 'null'); if (r && r.v === 2 && r.sec) return r; } catch (e) { /* new campaign */ } return CQ.camp.newCampaign(startCredits); });
  const [sel, setSel] = React.useState('shadow_town');
  const [tab, setTab] = React.useState('map');
  const [ptab, setPtab] = React.useState('overview');
  const [planner, setPlanner] = React.useState(null);
  const [battleCtx, setBattleCtx] = React.useState(null);
  const [report, setReport] = React.useState(null);
  const [toast, setToast] = React.useState('');
  const [moveTo, setMoveTo] = React.useState(null);
  const [moveUnits, setMoveUnits] = React.useState({});
  const [codexTab, setCodexTab] = React.useState('howto');
  const toastT = React.useRef(null);
  const mapBox = React.useRef(null);
  const [vw, setVw] = React.useState(1060);
  React.useEffect(() => { const el = mapBox.current; if (!el || !window.ResizeObserver) return undefined; const measure = () => { const w = el.clientWidth, h = el.clientHeight; if (h > 80 && w > 80) setVw(Math.max(1060, Math.min(1560, Math.round(740 * w / h)))); }; measure(); const ro = new window.ResizeObserver(measure); ro.observe(el); return () => ro.disconnect(); }, [tab]);
  React.useEffect(() => { window.__cqState = st; }); // test hook for automated playtests
  React.useEffect(() => { try { localStorage.setItem(SAVE_KEY, JSON.stringify(st)); } catch (e) { /* storage blocked */ } }, [st]);
  const flash = (m) => { setToast(m); clearTimeout(toastT.current); toastT.current = setTimeout(() => setToast(''), 3200); };
  const run = (res) => { if (res.ok) { setSt(res.state); } else flash(res.msg); return res.ok; };
  const sd = CQ.SECTORS[sel] || CQ.SECTORS.shadow_town, ss = st.sec[sel];
  const eco = CQ.camp.economy(st);
  const intelOf = (id) => CQ.camp.intelLevel(st, id);
  const col = (o) => (CQ.FACTIONS[o] || CQ.FACTIONS.neutral).color;
  const U = CQ.UNITS;
  const resetCampaign = () => { if (window.confirm('Reset the whole Conquest campaign?')) { try { localStorage.removeItem(SAVE_KEY); } catch (e) { /* ignore */ } setSt(CQ.camp.newCampaign(startCredits)); setSel('shadow_town'); setPlanner(null); setReport(null); } };

  const endTurn = () => {
    const r = CQ.camp.endTurn(st, Math.random); if (!r.ok) { flash(r.msg); return; }
    setSt(r.state); if (r.events.length) setReport({ title: `TURN ${r.state.turn} REPORT`, lines: r.events.concat([`Treasury ${r.state.cr} CR.`]) });
  };
  // ---- attack planner
  const openPlanner = (targetId) => {
    const why = CQ.camp.canAttack(st, targetId); if (why) { flash(why); return; }
    const srcs = CQ.camp.attackSources(st, targetId); if (!srcs.length) { flash('No rested units next to that sector. Units that moved or fought this turn are spent.'); return; }
    const bySrc = {}; srcs.forEach((s) => { const g = {}; CQ.UNIT_ORDER.forEach((k) => { const n = Math.floor(s.avail[k] * 0.8 + (s.avail[k] > 0 && s.avail[k] < 3 ? 0.99 : 0)); if (n > 0) g[k] = Math.min(s.avail[k], n); }); if (Object.keys(g).length) bySrc[s.id] = g; });
    setPlanner({ targetId, bySrc, forecast: null });
  };
  const planTotal = (p) => Object.values(p.bySrc).reduce((n, g) => n + CQ.camp.total(g), 0);
  const setCommit = (srcId, k, n) => setPlanner((p) => { const bySrc = { ...p.bySrc, [srcId]: { ...(p.bySrc[srcId] || {}) } }; if (n > 0) bySrc[srcId][k] = n; else delete bySrc[srcId][k]; if (!Object.keys(bySrc[srcId]).length) delete bySrc[srcId]; return { ...p, bySrc, forecast: null }; });
  const finishBattle = (meta, result) => { const out = CQ.camp.applyResult(st, meta, result, Math.random); setSt(out.st); setBattleCtx(null); setReport({ title: meta.kind === 'defense' ? 'DEFENSE REPORT' : 'ASSAULT REPORT', lines: [out.summary] }); };
  const startTactical = () => {
    const p = planner; if (!p || planTotal(p) === 0) { flash('Commit at least one unit.'); return; }
    const { battle, meta } = CQ.camp.makeAttack(st, p.targetId, { bySrc: p.bySrc }, 0);
    setBattleCtx({ battle, meta, title: `ASSAULT ON ${CQ.SECTORS[p.targetId].name.toUpperCase()}` }); setPlanner(null);
  };
  const startAuto = () => {
    const p = planner; if (!p || planTotal(p) === 0) { flash('Commit at least one unit.'); return; }
    const out = CQ.camp.autoResolveAttack(st, p.targetId, { bySrc: p.bySrc }, Math.random); setSt(out.st); setPlanner(null);
    setReport({ title: 'ASSAULT REPORT (AUTO-RESOLVED)', lines: [out.summary, `Fought ${out.result.rounds} rounds.`] });
  };
  const runForecast = () => {
    const p = planner; if (!p) return; if (planTotal(p) === 0) { flash('Commit at least one unit first.'); return; } const f = CQ.camp.forecast(st, p.targetId, { bySrc: p.bySrc }, 12, Math.random);
    const known = intelOf(p.targetId); const noise = known ? 0 : Math.round((Math.random() - 0.5) * 36); setPlanner({ ...p, forecast: { ...f, winPct: Math.max(0, Math.min(100, f.winPct + noise)), rough: !known } });
  };
  // ---- defense
  const startDefense = () => { const { battle, meta } = CQ.camp.makeDefense(st, st.pending, 0); setBattleCtx({ battle, meta, title: `DEFENDING ${CQ.SECTORS[st.pending.targetId].name.toUpperCase()}` }); };
  const autoDefense = () => { const out = CQ.camp.autoResolveDefense(st, Math.random); setSt(out.st); setReport({ title: 'DEFENSE REPORT (AUTO-RESOLVED)', lines: [out.summary] }); };

  // ---- derived UI helpers
  const canAtk = sel && !CQ.camp.canAttack(st, sel);
  const slotsUsed = ss ? ss.bld.length : 0;
  const incPct = Math.min(100, Math.round((eco.cr / CQ.WIN_INCOME) * 100));
  const tabBtn = (t, l) => <button key={t} style={cqBtn(tab === t ? '#2FC8FF' : '#6A7898', false, { background: tab === t ? '#0E2A40' : '#0A101C', padding: '7px 16px', fontSize: 12 })} onClick={() => setTab(t)}>{l}</button>;
  const ptBtn = (t, l) => <button key={t} style={cqBtn(ptab === t ? '#FFD36A' : '#6A7898', false, { background: ptab === t ? '#2A2410' : '#0A101C', padding: '4px 10px' })} onClick={() => setPtab(t)}>{l}</button>;

  const sectorPanel = () => {
    if (!ss) return null;
    const own = ss.owner === 'player'; const fxs = CQ.camp.fx(ss); const seeing = intelOf(sel);
    return (
      <div style={{ ...CQ_PANEL, padding: 0, overflow: 'hidden', border: '1px solid ' + col(ss.owner) }}>
        <div style={{ position: 'relative' }}>
          <svg viewBox="0 0 132 50" preserveAspectRatio="xMidYMid slice" style={{ width: '100%', height: 120, display: 'block' }}><CQScene theme={sd.theme} id={sd.id} /></svg>
          <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(180deg, transparent 40%, #0A101C 100%)' }} />
          <div style={{ position: 'absolute', left: 12, bottom: 6 }}><div style={{ fontSize: 15, fontWeight: 800, color: '#F0F4FF', textShadow: '0 1px 4px #000' }}>{sd.name}{sd.isHQ ? ' ★' : ''}</div><div style={{ fontSize: 10, color: col(ss.owner), letterSpacing: '0.1em' }}>{(CQ.FACTIONS[ss.owner] || {}).name.toUpperCase()} · {sd.tier.toUpperCase()}</div></div>
        </div>
        <div style={{ padding: 12 }}>
          <div style={{ fontSize: 11, color: '#8A98B8', fontStyle: 'italic', marginBottom: 8 }}>{sd.lore}</div>
          <div style={{ display: 'flex', gap: 14, fontSize: 12, marginBottom: 8, flexWrap: 'wrap' }}><span style={{ color: '#FFD36A' }}>¢ {sd.income + fxs.income}/turn</span><span style={{ color: '#34D86A' }}>⚡ {sd.pwr + fxs.pwrGen} PWR</span><span style={{ color: '#7FD0FF' }}>Cap {CQ.camp.total(ss.gar)}/{Math.max(fxs.cap, CQ.camp.total(ss.gar))}</span></div>
          <div style={{ display: 'flex', gap: 4, marginBottom: 10, flexWrap: 'wrap' }}>{own && ptBtn('overview', 'Overview')}{own && ptBtn('recruit', 'Recruit')}{own && ptBtn('build', 'Build')}{own && ptBtn('move', 'Move')}</div>
          {(!own || ptab === 'overview') && (
            <div>
              <div style={{ fontSize: 11, color: '#7A88A8', fontWeight: 800, marginBottom: 4 }}>GARRISON</div>
              {own || seeing ? <CQGarList gar={ss.gar} tired={own ? ss.tired : null} /> : <div style={{ fontSize: 12, color: '#FFC24A' }}>Unknown force. Estimated strength: <b>{CQ.camp.band(CQ.camp.power(ss.gar)).toUpperCase()}</b>. Build an Intelligence Hub next to it for exact numbers.</div>}
              <div style={{ fontSize: 11, color: '#7A88A8', fontWeight: 800, margin: '10px 0 4px' }}>BUILDINGS ({slotsUsed}/{CQ.camp.slots(ss)})</div>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>{ss.bld.map((b) => <span key={b} title={CQ.BLD[b].desc} style={{ fontSize: 10, background: '#2A2410', border: '1px solid #6A5A20', borderRadius: 4, padding: '2px 6px', color: '#FFD36A' }}>{CQ.BLD[b].name}</span>)}{!ss.bld.length && <span style={{ fontSize: 11, color: '#6A7898' }}>None</span>}</div>
              {!own && (
                <div style={{ marginTop: 12 }}>
                  {canAtk ? <button style={cqBtn('#FF5468', false, { width: '100%', padding: 11, fontSize: 13 })} onClick={() => openPlanner(sel)}>⚔ PLAN ATTACK ON {sd.name.toUpperCase()}</button> : <div style={{ fontSize: 11, color: '#FFA03A' }}>{CQ.camp.canAttack(st, sel)}</div>}
                </div>
              )}
              {own && <div style={{ fontSize: 10, color: '#6A7898', marginTop: 10 }}>Select an enemy sector on the map to attack it. You can send units from every sector of yours that borders it.</div>}
            </div>
          )}
          {own && ptab === 'recruit' && (
            <div style={{ maxHeight: 360, overflowY: 'auto' }}>
              {CQ.UNIT_ORDER.map((k) => { const u = U[k]; const locked = u.req && !ss.bld.includes(u.req); const cost = CQ.camp.unitCost(st, sel, k); return (
                <div key={k} style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '6px 4px', borderBottom: '1px solid #131C2C', opacity: locked ? 0.55 : 1 }}>
                  <CQUnitGlyph type={k} size={30} />
                  <div style={{ flex: 1 }}><div style={{ fontSize: 12, fontWeight: 800, color: '#E8F0FF' }}>{u.name}</div><CQUnitStats k={k} /><div style={{ fontSize: 10, color: '#6A7898' }}>{locked ? `Needs: ${CQ.BLD[u.req].name}` : u.role}</div></div>
                  <div style={{ textAlign: 'right' }}><div style={{ fontSize: 11, color: '#FFD36A' }}>{cost}¢{u.pwr ? ` +${u.pwr}⚡` : ''}</div><div style={{ display: 'flex', gap: 3, marginTop: 3 }}><button style={cqBtn('#34D86A', locked, { padding: '3px 8px' })} onClick={() => run(CQ.camp.recruit(st, sel, k, 1))}>+1</button><button style={cqBtn('#34D86A', locked, { padding: '3px 8px' })} onClick={() => run(CQ.camp.recruit(st, sel, k, 5))}>+5</button></div></div>
                </div>); })}
            </div>
          )}
          {own && ptab === 'build' && (
            <div style={{ maxHeight: 400, overflowY: 'auto' }}>
              <div style={{ display: 'flex', gap: 4, marginBottom: 8 }}>{Array.from({ length: CQ.camp.slots(ss) }, (_, i) => <div key={i} style={{ flex: 1, height: 26, borderRadius: 4, border: '1px solid ' + (ss.bld[i] ? '#6A5A20' : '#1B2A40'), background: ss.bld[i] ? '#2A2410' : '#0C1422', fontSize: 9, color: ss.bld[i] ? '#FFD36A' : '#4A5468', display: 'flex', alignItems: 'center', justifyContent: 'center', textAlign: 'center', lineHeight: 1 }}>{ss.bld[i] ? CQ.BLD[ss.bld[i]].name : 'empty slot'}</div>)}</div>
              {['Economy', 'Power', 'Military', 'Battle'].map((cat) => <div key={cat}><div style={{ fontSize: 10, color: '#7A88A8', fontWeight: 800, margin: '8px 0 3px', letterSpacing: '0.1em' }}>{cat.toUpperCase()}</div>{CQ.BUILDINGS.filter((b) => b.cat === cat).map((b) => { const built = ss.bld.includes(b.id); const why = built ? null : CQ.camp.buildCheck(st, sel, b.id); return (
                <div key={b.id} style={{ padding: '5px 4px', borderBottom: '1px solid #131C2C' }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', gap: 6 }}><span style={{ fontSize: 12, fontWeight: 800, color: built ? '#34D86A' : '#E8F0FF' }}>{built ? '✓ ' : ''}{b.name}</span><span style={{ fontSize: 11, color: '#FFD36A', whiteSpace: 'nowrap' }}>{b.cost}¢{b.pwr ? ` +${b.pwr}⚡` : ''}</span></div>
                  <div style={{ fontSize: 10, color: '#8A98B8' }}>{b.desc}</div>
                  <div style={{ marginTop: 3, display: 'flex', gap: 6, alignItems: 'center' }}>{built ? <button style={cqBtn('#FF9A5A', false, { padding: '2px 8px' })} onClick={() => run(CQ.camp.demolish(st, sel, b.id))}>Demolish (+{Math.round(b.cost * 0.4)}¢)</button> : <button style={cqBtn('#FFD36A', !!why, { padding: '2px 10px' })} onClick={() => run(CQ.camp.build(st, sel, b.id))}>Build</button>}{why && <span style={{ fontSize: 9, color: '#FFA03A' }}>{why}</span>}</div>
                </div>); })}</div>)}
            </div>
          )}
          {own && ptab === 'move' && (() => {
            const dests = sd.adj.filter((id) => st.sec[id].owner === 'player'); const to = moveTo && dests.includes(moveTo) ? moveTo : dests[0]; const av = CQ.camp.avail(ss); const n = Object.values(moveUnits).reduce((a, b) => a + b, 0);
            if (!dests.length) return <div style={{ fontSize: 11, color: '#8A98B8' }}>No adjacent sector of yours to move units into. Capture a neighbour first.</div>;
            return (
              <div>
                <div style={{ fontSize: 11, color: '#8A98B8', marginBottom: 6 }}>Move units to an adjacent sector of yours (20 CR per unit). Units that moved this turn cannot attack.</div>
                <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap', marginBottom: 8 }}>{dests.map((id) => <button key={id} style={cqBtn(to === id ? '#2FC8FF' : '#6A7898', false, { background: to === id ? '#0E2A40' : undefined })} onClick={() => setMoveTo(id)}>{CQ.SECTORS[id].name}</button>)}</div>
                {CQ.UNIT_ORDER.filter((k) => av[k] > 0).map((k) => <div key={k} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}><CQUnitGlyph type={k} size={22} /><span style={{ flex: 1, fontSize: 12 }}>{U[k].name} <i style={{ color: '#6A7898' }}>({av[k]} ready)</i></span><CQStepper n={moveUnits[k] || 0} max={av[k]} onChange={(v) => setMoveUnits({ ...moveUnits, [k]: v })} /></div>)}
                {!CQ.UNIT_ORDER.some((k) => av[k] > 0) && <div style={{ fontSize: 11, color: '#FFA03A' }}>All units here are spent this turn.</div>}
                <button style={cqBtn('#34D86A', n === 0, { width: '100%', marginTop: 8, padding: 9 })} onClick={() => { if (n && run(CQ.camp.move(st, sel, to, moveUnits))) setMoveUnits({}); }}>MOVE {n} UNITS ({n * 20}¢)</button>
              </div>
            );
          })()}
        </div>
      </div>
    );
  };

  const plannerView = () => {
    const p = planner; const t = st.sec[p.targetId], td = CQ.SECTORS[p.targetId]; const srcs = CQ.camp.attackSources(st, p.targetId); const total = planTotal(p); const cap = CQ.camp.depCapFor(st, Object.keys(p.bySrc)); const seeing = intelOf(p.targetId);
    const efx = CQ.camp.cfgFromSectors([t], 'def');
    return (
      <div style={{ position: 'fixed', inset: 0, zIndex: 85, background: 'rgba(3,6,12,0.86)', display: 'flex', alignItems: 'center', justifyContent: 'center', overflow: 'auto', padding: 12 }}>
        <div style={{ ...CQ_PANEL, width: 'min(1040px,96vw)', border: '2px solid #FF5468', boxShadow: '0 0 40px #FF546855' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}><div style={{ fontSize: 18, fontWeight: 800, color: '#FF5468', letterSpacing: '0.1em' }}>⚔ ATTACK PLANNER: {td.name.toUpperCase()}</div><button style={cqBtn('#8A98B8')} onClick={() => setPlanner(null)}>✕ CANCEL</button></div>
          <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.3fr) minmax(0,1fr)', gap: 14 }}>
            <div>
              <div style={{ fontSize: 11, color: '#2FC8FF', fontWeight: 800, marginBottom: 6 }}>YOUR ASSAULT FORCE ({total} committed, {Math.min(total, cap)} fielded of {cap} max)</div>
              {srcs.map((s) => <div key={s.id} style={{ ...CQ_PANEL, marginBottom: 8, background: '#0C1422' }}><div style={{ fontSize: 12, fontWeight: 800, color: '#E8F0FF', marginBottom: 4 }}>From {CQ.SECTORS[s.id].name}</div>{CQ.UNIT_ORDER.filter((k) => s.avail[k] > 0).map((k) => <div key={k} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}><CQUnitGlyph type={k} size={22} /><span style={{ flex: 1, fontSize: 12 }}>{U[k].short} <i style={{ color: '#6A7898' }}>({s.avail[k]})</i></span><CQStepper n={(p.bySrc[s.id] || {})[k] || 0} max={s.avail[k]} onChange={(v) => setCommit(s.id, k, v)} /></div>)}</div>)}
              <div style={{ display: 'flex', gap: 6 }}><button style={cqBtn('#34D86A')} onClick={() => { const bySrc = {}; srcs.forEach((s) => { const g = {}; CQ.UNIT_ORDER.forEach((k) => { if (s.avail[k] > 0) g[k] = s.avail[k]; }); bySrc[s.id] = g; }); setPlanner({ ...p, bySrc, forecast: null }); }}>SEND EVERYTHING</button><button style={cqBtn('#FF9A5A')} onClick={() => setPlanner({ ...p, bySrc: {}, forecast: null })}>CLEAR</button></div>
              {total > cap && <div style={{ fontSize: 10, color: '#FFC24A', marginTop: 6 }}>Only {cap} units fit in the battle. The most expensive are fielded first, the rest stay home. A Command Center raises the cap.</div>}
            </div>
            <div>
              <svg viewBox="0 0 132 50" preserveAspectRatio="xMidYMid slice" style={{ width: '100%', height: 90, borderRadius: 6, display: 'block' }}><CQScene theme={td.theme} id={td.id} /></svg>
              <div style={{ fontSize: 11, color: col(t.owner), fontWeight: 800, margin: '8px 0 4px' }}>DEFENDERS: {(CQ.FACTIONS[t.owner] || {}).name}</div>
              {seeing ? <CQGarList gar={t.gar} /> : <div style={{ fontSize: 12, color: '#FFC24A' }}>Unknown. Estimated strength: <b>{CQ.camp.band(CQ.camp.power(t.gar)).toUpperCase()}</b>.</div>}
              <div style={{ fontSize: 11, color: '#7A88A8', fontWeight: 800, margin: '8px 0 3px' }}>DEFENSES</div>
              <div style={{ fontSize: 11, color: '#9AA8C8', lineHeight: 1.5 }}>{t.bld.length ? t.bld.map((b) => `${CQ.BLD[b].name}: ${CQ.BLD[b].desc}`).join(' · ') : 'No fortifications.'}{efx.turrets ? ' Expect auto turrets.' : ''}</div>
              <div style={{ ...CQ_PANEL, background: '#0C1422', marginTop: 10 }}>
                <button style={cqBtn('#FFD36A', total === 0, { width: '100%' })} onClick={runForecast}>RUN BATTLE FORECAST (12 simulated battles)</button>
                {p.forecast && <div style={{ marginTop: 8, fontSize: 12 }}>Win chance <b style={{ color: p.forecast.winPct >= 60 ? '#34D86A' : p.forecast.winPct >= 35 ? '#FFC24A' : '#FF5468', fontSize: 18 }}>{p.forecast.winPct}%</b> · expect to lose ~{p.forecast.lossPct}% of the force · ~{p.forecast.rounds} rounds{p.forecast.rough ? <div style={{ color: '#FFA03A', fontSize: 10 }}>Rough estimate. An Intelligence Hub next door makes forecasts exact.</div> : null}</div>}
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 12 }}>
                <button style={cqBtn('#2FC8FF', total === 0, { padding: 12, fontSize: 13 })} onClick={startTactical}>⚔ TACTICAL BATTLE</button>
                <button style={cqBtn('#34D86A', total === 0, { padding: 12, fontSize: 13 })} onClick={startAuto}>⚡ AUTO-RESOLVE</button>
              </div>
              <div style={{ fontSize: 10, color: '#6A7898', marginTop: 6 }}>Tactical lets you deploy and command every unit. Auto-resolve simulates the same battle instantly with both sides on AI.</div>
            </div>
          </div>
        </div>
      </div>
    );
  };

  const defenseView = () => {
    const pd = st.pending, f = CQ.FACTIONS[pd.faction], td = CQ.SECTORS[pd.targetId];
    return (
      <div style={{ position: 'fixed', inset: 0, zIndex: 86, background: 'rgba(30,4,8,0.88)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
        <div style={{ ...CQ_PANEL, width: 'min(560px,94vw)', border: '2px solid ' + f.color, boxShadow: '0 0 50px ' + f.color + '88', padding: 22 }}>
          <div style={{ fontSize: 22, fontWeight: 800, color: '#FF5468', letterSpacing: '0.12em' }}>⚠ ENEMY ASSAULT</div>
          <div style={{ fontSize: 13, margin: '8px 0', color: '#E8F0FF' }}><b style={{ color: f.color }}>{f.name}</b> is attacking <b>{td.name}</b> from {CQ.SECTORS[pd.srcId].name}.</div>
          <div style={{ fontSize: 11, color: '#8A98B8', marginBottom: 4 }}>Attacking force</div><CQGarList gar={pd.force} />
          <div style={{ fontSize: 11, color: '#8A98B8', margin: '10px 0 4px' }}>Your garrison</div><CQGarList gar={st.sec[pd.targetId].gar} />
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, marginTop: 16 }}><button style={cqBtn('#2FC8FF', false, { padding: 12, fontSize: 13 })} onClick={startDefense}>🛡 DEFEND TACTICALLY</button><button style={cqBtn('#34D86A', false, { padding: 12, fontSize: 13 })} onClick={autoDefense}>⚡ AUTO-RESOLVE</button></div>
          <div style={{ fontSize: 10, color: '#6A7898', marginTop: 8 }}>{td.isHQ ? 'Your headquarters cannot be captured, but a loss will shatter its garrison.' : 'If your garrison falls the sector is lost to the attacker.'}</div>
        </div>
      </div>
    );
  };

  const empireTab = () => {
    const mine = CQ.SECTOR_LIST.filter((d) => st.sec[d.id].owner === 'player');
    const fstat = ['black_sun', 'exchange', 'csf'].map((f) => { const ds = CQ.SECTOR_LIST.filter((d) => st.sec[d.id].owner === f); return { f, n: ds.length, inc: ds.reduce((a, d) => a + d.income, 0), pw: Math.round(ds.reduce((a, d) => a + CQ.camp.power(st.sec[d.id].gar), 0)) }; });
    return (
      <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1.5fr) minmax(0,1fr)', gap: 12 }}>
        <div style={CQ_PANEL}>
          <div style={{ fontSize: 12, fontWeight: 800, color: '#2FC8FF', marginBottom: 8, letterSpacing: '0.1em' }}>YOUR SECTORS ({mine.length})</div>
          {mine.map((d) => { const s = st.sec[d.id]; return <div key={d.id} onClick={() => { setSel(d.id); setTab('map'); }} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 4px', borderBottom: '1px solid #131C2C', cursor: 'pointer' }}><span style={{ flex: 1, fontSize: 12, fontWeight: 700 }}>{d.name}{d.isHQ ? ' ★' : ''}</span><CQGarList gar={s.gar} /><span style={{ fontSize: 11, color: '#FFD36A', minWidth: 64, textAlign: 'right' }}>{d.income + CQ.camp.fx(s).income}¢</span><span style={{ fontSize: 10, color: '#8A98B8' }}>{s.bld.length}/{CQ.camp.slots(s)} bld</span></div>; })}
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div style={CQ_PANEL}><div style={{ fontSize: 12, fontWeight: 800, color: '#FFD36A', marginBottom: 8, letterSpacing: '0.1em' }}>ECONOMY</div><div style={{ fontSize: 12, lineHeight: 1.8 }}>Income <b style={{ color: '#FFD36A' }}>{eco.cr}¢</b> / turn, payroll <b style={{ color: '#FF9A5A' }}>{eco.upkeepCR}¢</b>, net <b style={{ color: eco.netCR >= 0 ? '#34D86A' : '#FF5468' }}>{eco.netCR}¢</b><br />Power generation <b style={{ color: '#34D86A' }}>{eco.pwrGen}</b>, upkeep <b style={{ color: '#FF9A5A' }}>{eco.upkeep}</b>, net <b style={{ color: eco.net >= 0 ? '#34D86A' : '#FF5468' }}>{eco.net}</b><br />Heat {st.heat} ({eco.heatGen - eco.heatDecay >= 0 ? '+' : ''}{2 + eco.heatGen - eco.heatDecay} per turn)<br />Victory at {CQ.WIN_INCOME} income: <b>{incPct}%</b></div></div>
          <div style={CQ_PANEL}><div style={{ fontSize: 12, fontWeight: 800, color: '#FF9A5A', marginBottom: 8, letterSpacing: '0.1em' }}>CAMPAIGN RECORD</div><div style={{ fontSize: 12, lineHeight: 1.8 }}>Assaults won {st.stats.won} · lost {st.stats.lost}<br />Sectors captured {st.stats.captured} · defended {st.stats.defended}<br />Units fallen {st.stats.fallen}</div></div>
          <div style={CQ_PANEL}><div style={{ fontSize: 12, fontWeight: 800, color: '#FF5468', marginBottom: 8, letterSpacing: '0.1em' }}>RIVAL POWERS</div>{fstat.map((x) => <div key={x.f} style={{ display: 'flex', justifyContent: 'space-between', fontSize: 12, color: CQ.FACTIONS[x.f].color, marginBottom: 3 }}><span>{CQ.FACTIONS[x.f].name}</span><span>{x.n} sectors · {x.inc}¢ · power {x.pw}</span></div>)}</div>
        </div>
      </div>
    );
  };
  const diplomacyTab = () => (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3,minmax(0,1fr))', gap: 12 }}>
      {['black_sun', 'exchange', 'csf'].map((f) => { const F = CQ.FACTIONS[f]; const rel = st.rel[f] || 0; const n = CQ.SECTOR_LIST.filter((d) => st.sec[d.id].owner === f).length; return (
        <div key={f} style={{ ...CQ_PANEL, border: '1px solid ' + F.color + '88' }}>
          <div style={{ fontSize: 15, fontWeight: 800, color: F.color }}>{F.name}</div><div style={{ fontSize: 11, color: '#8A98B8', marginBottom: 8 }}>{n} sectors · aggression {F.aggr}%</div>
          <div style={{ fontSize: 22, fontWeight: 800, color: rel >= 80 ? '#34D86A' : rel > 0 ? '#FFC24A' : '#FF5468' }}>{rel > 0 ? '+' : ''}{rel}</div>
          <div style={{ background: '#05080C', borderRadius: 3, height: 8, margin: '6px 0 10px' }}><div style={{ width: ((rel + 100) / 2) + '%', height: '100%', background: F.color, borderRadius: 3 }} /></div>
          <div style={{ fontSize: 11, color: '#9AA8C8', marginBottom: 8, lineHeight: 1.5 }}>{rel >= 80 ? 'Non-aggression pact: they will not attack you.' : rel <= -50 ? 'Hostile: they target your sectors first.' : 'They will attack you if they see weakness.'} Attacking their sectors costs you 12 standing each time.</div>
          <div style={{ display: 'flex', gap: 6 }}><button style={cqBtn('#34D86A', st.cr < 400)} onClick={() => run(CQ.camp.adjustRelation(st, f, 10))}>BRIBE +10 (400¢)</button><button style={cqBtn('#FF9A5A')} onClick={() => run(CQ.camp.adjustRelation(st, f, -15))}>PROVOKE -15</button></div>
        </div>); })}
    </div>
  );
  const codexView = () => (
    <div>
      <div style={{ display: 'flex', gap: 6, marginBottom: 10 }}>{[['howto', 'How to play'], ['units', 'Units'], ['buildings', 'Buildings']].map(([t, l]) => <button key={t} style={cqBtn(codexTab === t ? '#FFD36A' : '#6A7898', false, { background: codexTab === t ? '#2A2410' : undefined })} onClick={() => setCodexTab(t)}>{l}</button>)}</div>
      {codexTab === 'howto' && <div style={{ ...CQ_PANEL, fontSize: 12, lineHeight: 1.7, color: '#B8C4DC', maxHeight: 520, overflowY: 'auto' }}>
        {[['GOAL', 'Reach ' + CQ.WIN_INCOME + ' credits of income per turn, then end the turn. Income comes from the sectors you hold and the buildings in them.'], ['THE MAP', 'Coruscant is a vertical cross-section. Each card is a sector with its owner (colored border and pennant), income, garrison and building slots. A red pulsing border means you can attack it; the red arrows show which of your sectors will join the assault.'], ['RECRUIT AND BUILD', 'Select a sector you own. Recruit units into that sector (the Recruit tab) and build in its slots (the Build tab). Advanced units need the right building in the same sector: Tanks and Speeders need a Motor Pool, Droids a Droid Foundry, Bruisers a Barracks, Walkers a War College.'], ['ATTACK', 'Select an enemy sector next to one of yours and press PLAN ATTACK. Choose units from every adjacent sector of yours, run a battle forecast, then fight a Tactical Battle yourself or Auto-Resolve. Units that fought or moved this turn are spent until next turn. Faction HQs (★) cannot be captured.'], ['TACTICAL BATTLES', 'Each sector has its own battlefield. Deploy your units inside the cyan zone, then take turns. Move, shoot, use abilities, use cover, avoid burning tiles. Win by wiping out the defenders or by holding the Command Node for two of your turns. Defenders win by surviving 22 rounds.'], ['BUILDINGS IN BATTLE', 'Buildings change fights. Bunkers give the defenders hard cover, Turret Nests add auto turrets, Shield Generators shield defenders, Armories add damage, Rally Points add first turn speed, Orbital Batteries give you one big strike, Medical Bays revive 40% of the fallen.'], ['ENEMIES FIGHT BACK', 'The Black Sun, Exchange and CSF recruit, build and attack neutral sectors, each other and you. When they attack you, you get to defend in a tactical battle or auto-resolve. You can bribe factions to a pact (+80 standing) or provoke them.'], ['POWER AND HEAT', 'Tanks, Droids and Walkers cost PWR to build and PWR upkeep every turn. A power shortfall browns out the grid and cuts income. Heat rises every turn and with every attack; at 100 the CSF raids you for credits and, outside your HQ sector, a building.']].map(([h, t]) => <div key={h} style={{ marginBottom: 10 }}><div style={{ color: '#FFD36A', fontWeight: 800, letterSpacing: '0.08em' }}>{h}</div>{t}</div>)}
      </div>}
      {codexTab === 'units' && <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: 10 }}>{CQ.UNIT_ORDER.map((k) => { const u = U[k]; return <div key={k} style={{ ...CQ_PANEL, display: 'flex', gap: 10 }}><CQUnitGlyph type={k} size={46} /><div><div style={{ fontWeight: 800, color: '#E8F0FF' }}>{u.name}</div><div style={{ fontSize: 10, color: '#7A88A8' }}>{u.role} · {u.cr}¢{u.pwr ? ` + ${u.pwr}⚡` : ''} · payroll {u.ucr}¢/turn{u.upkeep ? ` · upkeep ${u.upkeep}⚡` : ''}{u.req ? ` · needs ${CQ.BLD[u.req].name}` : ''}</div><CQUnitStats k={k} /><div style={{ fontSize: 11, color: '#9AA8C8', marginTop: 3 }}>{u.passive}</div>{u.ab && <div style={{ fontSize: 11, color: '#FFC24A', marginTop: 3 }}><b>{u.ab.name}</b> (cooldown {u.ab.cd}): {u.ab.desc}</div>}<div style={{ fontSize: 10, color: '#5A6888', fontStyle: 'italic', marginTop: 3 }}>{u.lore}</div></div></div>; })}</div>}
      {codexTab === 'buildings' && <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: 8 }}>{CQ.BUILDINGS.map((b) => <div key={b.id} style={CQ_PANEL}><div style={{ display: 'flex', justifyContent: 'space-between' }}><b style={{ color: '#E8F0FF', fontSize: 12 }}>{b.name}</b><span style={{ fontSize: 11, color: '#FFD36A' }}>{b.cost}¢{b.pwr ? ` +${b.pwr}⚡` : ''}</span></div><div style={{ fontSize: 10, color: '#7A88A8' }}>{b.cat}</div><div style={{ fontSize: 11, color: '#9AA8C8', marginTop: 2 }}>{b.desc}</div></div>)}</div>}
    </div>
  );

  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 70, background: 'radial-gradient(circle at 50% -10%, #14203A 0%, #070A12 55%, #04060B 100%)', color: '#E0E8F8', fontFamily: 'monospace', overflow: 'hidden' }}>
      <div style={{ maxWidth: 2000, height: '100%', display: 'flex', flexDirection: 'column', margin: '0 auto', padding: '8px 14px 10px' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 16, flexWrap: 'wrap', marginBottom: 8 }}>
          <div><div style={{ fontSize: 20, fontWeight: 800, letterSpacing: '0.18em', color: '#2FC8FF' }}>CORUSCANT CONQUEST</div><div style={{ fontSize: 10, color: '#6A7898', letterSpacing: '0.2em' }}>TURN {st.turn} · LEVELS 5100 TO DEPTHS</div></div>
          <div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', marginLeft: 'auto', fontSize: 13 }}>
            <span style={{ color: '#FFD36A' }}>¢ {st.cr.toLocaleString()}</span><span style={{ color: '#34D86A' }}>⚡ {st.pwr} <i style={{ fontSize: 10, color: eco.net >= 0 ? '#34D86A' : '#FF5468' }}>({eco.net >= 0 ? '+' : ''}{eco.net})</i></span>
            <span style={{ color: st.heat > 70 ? '#FF5468' : st.heat > 40 ? '#FFC24A' : '#8A98B8' }}>HEAT {st.heat}</span>
            <span title={'Reach ' + CQ.WIN_INCOME + ' income per turn and end the turn to win'} style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: eco.cr >= CQ.WIN_INCOME ? '#34D86A' : '#B8C4DC' }}>INCOME {eco.cr}/{CQ.WIN_INCOME}<span style={{ width: 90, height: 8, background: '#05080C', borderRadius: 4, display: 'inline-block' }}><span style={{ display: 'block', width: incPct + '%', height: '100%', background: eco.cr >= CQ.WIN_INCOME ? '#34D86A' : '#2FC8FF', borderRadius: 4 }} /></span></span>
            <button style={cqBtn('#FF5468', false, { padding: '3px 8px' })} onClick={resetCampaign}>Reset</button><button style={cqBtn('#2FC8FF', false, { padding: '3px 8px' })} onClick={onSuccess}>Exit to Game</button>
          </div>
        </div>
        {st.crisis && <div style={{ background: '#2A1A00', border: '1px solid #FF8C00', borderRadius: 5, padding: '6px 10px', marginBottom: 8, fontSize: 12, color: '#FFD36A' }}>⚠ CRISIS: {st.crisis.title}. {st.crisis.desc}</div>}
        {st.brownout && <div style={{ background: '#2A0A0A', border: '1px solid #FF5468', borderRadius: 5, padding: '6px 10px', marginBottom: 8, fontSize: 12, color: '#FF9A9A' }}>⚡ BROWNOUT: your upkeep exceeds power generation. Build power, or income stays cut.</div>}
        <div style={{ display: 'flex', gap: 6, marginBottom: 10, alignItems: 'center', flexWrap: 'wrap' }}>
          {tabBtn('map', 'Map')}{tabBtn('empire', 'Empire')}{tabBtn('diplomacy', 'Diplomacy')}{tabBtn('codex', 'Codex')}
          <button style={cqBtn(st.pending ? '#FF5468' : '#34D86A', false, { marginLeft: 'auto', padding: '9px 22px', fontSize: 13 })} onClick={endTurn}>{st.pending ? '⚠ RESOLVE ASSAULT' : `END TURN ${st.turn} ▶`}</button>
        </div>
        {tab === 'map' && <div style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 360px', gap: 12, flex: 1, minHeight: 0 }}><div ref={mapBox} style={{ minHeight: 0, minWidth: 0, height: '100%' }}><CQMap st={st} sel={sel} vw={vw} onSelect={(id) => { setSel(id); setPtab('overview'); }} intelOf={intelOf} /></div><div style={{ minHeight: 0, overflowY: 'auto' }}>{sectorPanel()}        <div style={{ ...CQ_PANEL, marginTop: 10, maxHeight: 150, overflowY: 'auto', padding: 8 }}>{st.log.slice(0, 12).map((l, i) => <div key={i} style={{ fontSize: 11, color: i === 0 ? '#2FC8FF' : '#6A7898', borderBottom: '1px solid #0E1626', padding: '2px 0' }}>{l}</div>)}</div></div></div>}
        {tab !== 'map' && <div style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>{tab === 'empire' && empireTab()}{tab === 'diplomacy' && diplomacyTab()}{tab === 'codex' && codexView()}        <div style={{ ...CQ_PANEL, marginTop: 10, maxHeight: 110, overflowY: 'auto', padding: 8 }}>{st.log.slice(0, 12).map((l, i) => <div key={i} style={{ fontSize: 11, color: i === 0 ? '#2FC8FF' : '#6A7898', borderBottom: '1px solid #0E1626', padding: '2px 0' }}>{l}</div>)}</div></div>}
      </div>
      {toast && <div style={{ position: 'fixed', left: '50%', bottom: 26, transform: 'translateX(-50%)', zIndex: 120, background: '#2A1A08', border: '1px solid #FFA03A', color: '#FFD890', padding: '9px 18px', borderRadius: 6, fontSize: 13, maxWidth: '90vw' }}>{toast}</div>}
      {planner && !battleCtx && plannerView()}
      {st.pending && !battleCtx && !report && defenseView()}
      {report && <div style={{ position: 'fixed', inset: 0, zIndex: 95, background: 'rgba(3,6,12,0.82)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><div style={{ ...CQ_PANEL, width: 'min(520px,92vw)', padding: 20, border: '2px solid #FFD36A' }}><div style={{ fontSize: 18, fontWeight: 800, color: '#FFD36A', letterSpacing: '0.1em', marginBottom: 10 }}>{report.title}</div>{report.lines.map((l, i) => <div key={i} style={{ fontSize: 13, color: '#E0E8F8', marginBottom: 6, lineHeight: 1.5 }}>{l}</div>)}<button style={cqBtn('#FFD36A', false, { width: '100%', marginTop: 10, padding: 10 })} onClick={() => setReport(null)}>OK</button></div></div>}
      {st.over === 'win' && <div style={{ position: 'fixed', inset: 0, zIndex: 110, background: 'rgba(0,16,8,0.9)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><div style={{ ...CQ_PANEL, width: 'min(560px,92vw)', padding: 28, border: '2px solid #34D86A', textAlign: 'center', boxShadow: '0 0 60px #34D86A66' }}><div style={{ fontSize: 30, fontWeight: 800, color: '#34D86A', letterSpacing: '0.15em' }}>CORUSCANT IS YOURS</div><div style={{ fontSize: 13, color: '#B8E8C8', margin: '12px 0 18px', lineHeight: 1.6 }}>Your syndicate pulls in {eco.cr} credits a turn. The Black Sun, the Exchange and the CSF all answer to you now, whether they admit it or not.</div><button style={cqBtn('#34D86A', false, { padding: '12px 28px', fontSize: 14 })} onClick={() => { setSt({ ...st, over: null, won: true }); onSuccess(); }}>CLAIM VICTORY</button><button style={cqBtn('#7FD0FF', false, { padding: '12px 22px', fontSize: 14, marginLeft: 12 })} onClick={() => setSt({ ...st, over: null, won: true })}>KEEP PLAYING</button></div></div>}
      {battleCtx && <CQBattle battle={battleCtx.battle} meta={battleCtx.meta} title={battleCtx.title} onDone={(res) => finishBattle(battleCtx.meta, res)} />}
    </div>
  );
}
// ===== CONQUEST UI MAIN END =====


function SignalSiphonOverlay({ onSuccess, onFailure }) {
  const [stage, setStage] = React.useState(1);
  const [selectedFreq, setSelectedFreq] = React.useState(null);
  const [gridPos, setGridPos] = React.useState({ x: 0, y: 0 });
  const [timeLeft, setTimeLeft] = React.useState(20);
  const [message, setMessage] = React.useState('');
  const timerRef = React.useRef(null);
  const FREQS = ['108.4 MHz', '114.8 MHz', '121.3 MHz'];
  const CORRECT_FREQ = '114.8 MHz';
  const BLOCKED = [[0,2],[2,0],[2,3]];
  const isBlocked = (x, y) => BLOCKED.some(([bx, by]) => bx === x && by === y);

  React.useEffect(() => {
    if (stage !== 2) return;
    timerRef.current = setInterval(() => {
      setTimeLeft(t => {
        if (t <= 1) { clearInterval(timerRef.current); onFailure(); return 0; }
        return t - 1;
      });
    }, 1000);
    return () => clearInterval(timerRef.current);
  }, [stage]);

  const handleFreqSelect = (freq) => {
    if (selectedFreq) return;
    setSelectedFreq(freq);
    if (freq === CORRECT_FREQ) {
      setMessage('Signal locked. Bypass grid initializing...');
      setTimeout(() => setStage(2), 900);
    } else {
      setMessage('Frequency mismatch. Security ping detected.');
      setTimeout(onFailure, 1200);
    }
  };

  React.useEffect(() => {
    if (stage !== 2) return;
    const handler = (e) => {
      e.preventDefault();
      setGridPos(prev => {
        let { x, y } = prev;
        if (e.key === 'ArrowUp' && y > 0 && !isBlocked(x, y - 1)) y -= 1;
        else if (e.key === 'ArrowDown' && y < 3 && !isBlocked(x, y + 1)) y += 1;
        else if (e.key === 'ArrowLeft' && x > 0 && !isBlocked(x - 1, y)) x -= 1;
        else if (e.key === 'ArrowRight' && x < 3 && !isBlocked(x + 1, y)) x += 1;
        if (x === 3 && y === 3) { clearInterval(timerRef.current); setTimeout(onSuccess, 200); }
        return { x, y };
      });
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, [stage]);

  const ov = { position:'fixed', inset:0, background:'rgba(0,0,0,0.93)', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', zIndex:50 };
  const pan = { background:'#080C14', border:'1px solid #4A9FFF', borderRadius:6, padding:28, minWidth:360, color:'#C9C5BE', fontFamily:"'IBM Plex Mono',monospace" };

  if (stage === 1) return (
    <div style={ov}>
      <div style={pan}>
        <div style={{ color:'#4A9FFF', fontSize:13, letterSpacing:'0.15em', marginBottom:18 }}>SIGNAL SIPHON — FREQUENCY LOCK</div>
        <div style={{ fontSize:11, color:'#8A8F9E', marginBottom:18 }}>Select the active customs uplink frequency.</div>
        {FREQS.map(f => (
          <div key={f} onClick={() => handleFreqSelect(f)} style={{ padding:'10px 16px', margin:'6px 0', background:'#101828', border:`1px solid ${selectedFreq===f?'#4A9FFF':'#2A3050'}`, borderRadius:4, cursor:'pointer', color: selectedFreq===f?'#4A9FFF':'#C9C5BE', transition:'border-color 0.2s' }}>
            {f}
          </div>
        ))}
        {message && <div style={{ marginTop:14, color:'#FF6060', fontSize:11 }}>{message}</div>}
        <div style={{ marginTop:18, fontSize:10, color:'#404858' }}>Click to select</div>
      </div>
    </div>
  );

  return (
    <div style={ov}>
      <div style={pan}>
        <div style={{ color:'#4A9FFF', fontSize:13, letterSpacing:'0.15em', marginBottom:8 }}>BYPASS GRID — REACH TARGET</div>
        <div style={{ fontSize:11, color:'#8A8F9E', marginBottom:6 }}>Arrow keys to move. Avoid [X] nodes. Reach [T].</div>
        <div style={{ fontSize:11, color:'#E8C97A', marginBottom:14 }}>TIME: {timeLeft}s</div>
        {[0,1,2,3].map(row => (
          <div key={row} style={{ display:'flex', gap:6, marginBottom:6 }}>
            {[0,1,2,3].map(col => {
              const isPlayer = gridPos.x === col && gridPos.y === row;
              const isTarget = col === 3 && row === 3;
              const blocked = isBlocked(col, row);
              return (
                <div key={col} style={{ width:52, height:52, border:'1px solid', display:'flex', alignItems:'center', justifyContent:'center', fontSize:13, borderColor: blocked?'#442020':isTarget?'#E8C97A':'#2A3050', background: isPlayer?'#1A3A5A':blocked?'#1A0808':isTarget?'#2A2408':'#0E1220', color: isPlayer?'#4A9FFF':isTarget?'#E8C97A':blocked?'#442020':'#6A7090', fontWeight: isPlayer||isTarget?700:400 }}>
                  {isPlayer ? 'S' : isTarget ? 'T' : blocked ? 'X' : col===0&&row===0&&!isPlayer ? '·' : '·'}
                </div>
              );
            })}
          </div>
        ))}
        <div style={{ marginTop:10, fontSize:10, color:'#404858' }}>Arrow keys · Reach T to complete</div>
      </div>
    </div>
  );
}

function SpeederPursuitOverlay({ onSuccess, onFailure }) {
  const gameRef = React.useRef({ lane: 1, distance: 300, shield: 3, turbo: 100, heat: 0, phase: 1, obstacles: [], running: true, tickCount: 0 });
  const [display, setDisplay] = React.useState({ lane: 1, distance: 300, shield: 3, turbo: 100, phase: 1, obstacles: [], message: '' });
  const intervalRef = React.useRef(null);
  const LANE_NAMES = ['LEFT', 'CENTER', 'RIGHT'];
  const OBS_TYPES = ['freighter', 'laser_gate', 'swoop'];

  React.useEffect(() => {
    intervalRef.current = setInterval(() => {
      const g = gameRef.current;
      if (!g.running) return;
      g.tickCount++;
      g.distance = Math.max(0, g.distance - 2);
      if (g.tickCount % (g.phase === 3 ? 12 : g.phase === 2 ? 15 : 20) === 0) {
        const type = OBS_TYPES[Math.floor(Math.random() * OBS_TYPES.length)];
        g.obstacles = [...g.obstacles, { type, lane: Math.floor(Math.random() * 3), pos: 12 }];
      }
      g.obstacles = g.obstacles.map(o => ({ ...o, pos: o.pos - 1 }));
      let message = '';
      const colliders = g.obstacles.filter(o => o.pos <= 1 && o.lane === g.lane);
      colliders.forEach(o => {
        if (o.type === 'freighter') { g.turbo = Math.min(100, g.turbo + 20); message = 'Drafting freighter — turbo charge!'; }
        else if (o.type === 'laser_gate') { g.shield--; message = 'Laser gate hit! Shield down.'; }
        else if (o.type === 'swoop') { g.shield--; message = 'Swoop impact! Shield down.'; }
      });
      g.obstacles = g.obstacles.filter(o => o.pos > 1 || o.lane !== g.lane);
      if (g.distance <= 200 && g.phase < 2) { g.phase = 2; message = 'PHASE 2: pursuit intensifying.'; }
      if (g.distance <= 100 && g.phase < 3) { g.phase = 3; message = 'PHASE 3: final stretch!'; }
      if (g.shield <= 0) { g.running = false; clearInterval(intervalRef.current); setDisplay(d => ({ ...d, shield: 0, message: 'Shield depleted. Pursuit failed.' })); setTimeout(onFailure, 1400); return; }
      if (g.distance <= 0) { g.running = false; clearInterval(intervalRef.current); setDisplay(d => ({ ...d, distance: 0, message: 'Target distance reached!' })); setTimeout(onSuccess, 900); return; }
      setDisplay({ lane: g.lane, distance: g.distance, shield: g.shield, turbo: Math.round(g.turbo), phase: g.phase, obstacles: [...g.obstacles], message });
    }, 60);
    return () => clearInterval(intervalRef.current);
  }, []);

  React.useEffect(() => {
    const handler = (e) => {
      const g = gameRef.current;
      if (!g.running) return;
      if (e.key === 'a' || e.key === 'A' || e.key === 'ArrowLeft') { g.lane = Math.max(0, g.lane - 1); e.preventDefault(); }
      else if (e.key === 'd' || e.key === 'D' || e.key === 'ArrowRight') { g.lane = Math.min(2, g.lane + 1); e.preventDefault(); }
      else if (e.key === ' ') { if (g.turbo >= 50) { g.turbo -= 50; g.distance = Math.max(0, g.distance - 30); } e.preventDefault(); }
      else if (e.key === 'e' || e.key === 'E') { g.obstacles = g.obstacles.filter(o => !(o.type === 'swoop' && o.lane === g.lane)); e.preventDefault(); }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  const ov = { position:'fixed', inset:0, background:'rgba(0,0,0,0.93)', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', zIndex:50 };
  const pan = { background:'#080604', border:'1px solid #FF6030', borderRadius:6, padding:24, minWidth:420, color:'#C9C5BE', fontFamily:"'IBM Plex Mono',monospace" };

  const shieldBar = display.shield > 0 ? '█'.repeat(display.shield) + '░'.repeat(Math.max(0, 3 - display.shield)) : '░░░';

  return (
    <div style={ov}>
      <div style={pan}>
        <div style={{ color:'#FF6030', fontSize:13, letterSpacing:'0.15em', marginBottom:8 }}>SPEEDER PURSUIT — PHASE {display.phase}</div>
        <div style={{ display:'flex', justifyContent:'space-between', fontSize:11, color:'#8A8F9E', marginBottom:14 }}>
          <span>DIST: <span style={{ color:'#E8C97A' }}>{display.distance}</span></span>
          <span>SHIELD: [{shieldBar}]</span>
          <span>TURBO: {display.turbo}%</span>
        </div>
        <div style={{ display:'flex', gap:8, marginBottom:12 }}>
          {LANE_NAMES.map((lname, i) => {
            const obsInLane = display.obstacles.filter(o => o.lane === i);
            const isPlayer = display.lane === i;
            return (
              <div key={i} style={{ flex:1, height:110, background: isPlayer?'#1A1006':'#080604', border:`1px solid ${isPlayer?'#FF6030':'#2A1A10'}`, borderRadius:4, display:'flex', flexDirection:'column', justifyContent:'flex-end', padding:'4px 6px', position:'relative', overflow:'hidden' }}>
                {obsInLane.map((o, idx) => (
                  <div key={idx} style={{ position:'absolute', top:`${Math.max(2, Math.min(85, (12-o.pos)/12*100))}%`, left:4, right:4, height:14, background: o.type==='freighter'?'#303030':o.type==='laser_gate'?'#6000C0':'#C02020', borderRadius:2, fontSize:9, color:'#DDD', display:'flex', alignItems:'center', justifyContent:'center' }}>
                    {o.type==='freighter'?'■':o.type==='laser_gate'?'⚡':'◆'}
                  </div>
                ))}
                <div style={{ fontSize:16, color: isPlayer?'#FF6030':'#3A2010', textAlign:'center' }}>▲</div>
                <div style={{ fontSize:8, color:'#5A3020', textAlign:'center', marginTop:1 }}>{lname}</div>
              </div>
            );
          })}
        </div>
        <div style={{ fontSize:11, color:'#E8C97A', minHeight:16, marginBottom:8 }}>{display.message}</div>
        <div style={{ fontSize:10, color:'#4A3020' }}>A/D: lane · Space: turbo burst · E: EMP swoop</div>
      </div>
    </div>
  );
}

function TerminalSlicingOverlay({ onSuccess, onFailure, difficulty }) {
  var nodeCount = (difficulty || 4) + 3;
  var canvasRef = React.useRef(null);
  var initData = React.useMemo(function() {
    var cols = [60, 140, 220, 300, 360];
    var rows = [55, 130, 205];
    var nodes = [];
    var usedKeys = {};
    for (var i = 0; i < nodeCount; i++) {
      var px, py, key, tries = 0;
      do { px = cols[Math.floor(Math.random() * cols.length)]; py = rows[Math.floor(Math.random() * rows.length)]; key = px + '|' + py; tries++; }
      while (usedKeys[key] && tries < 30);
      usedKeys[key] = true;
      nodes.push({ id: i, x: px, y: py, type: i === 0 ? 'entry' : i === nodeCount - 1 ? 'exit' : 'node' });
    }
    var conns = [];
    for (var a = 0; a < nodeCount - 1; a++) conns.push([a, a + 1]);
    for (var b = 0; b < nodeCount; b++) for (var c = b + 2; c < nodeCount; c++) if (Math.random() < 0.28) conns.push([b, c]);
    return { nodes: nodes, connections: conns };
  }, []);
  var [path, setPath] = React.useState([]);
  var [timeLeft, setTimeLeft] = React.useState(20);
  var [status, setStatus] = React.useState('active');

  React.useEffect(function() {
    if (status !== 'active') return;
    var t = setInterval(function() {
      setTimeLeft(function(tl) {
        if (tl <= 1) { clearInterval(t); setStatus('fail'); setTimeout(onFailure, 900); return 0; }
        return tl - 1;
      });
    }, 1000);
    return function() { clearInterval(t); };
  }, [status]);

  React.useEffect(function() {
    var canvas = canvasRef.current;
    if (!canvas) return;
    var ctx = canvas.getContext('2d');
    var nodes = initData.nodes, conns = initData.connections;
    ctx.fillStyle = '#020810'; ctx.fillRect(0, 0, canvas.width, canvas.height);
    conns.forEach(function(pair) {
      var na = nodes[pair[0]], nb = nodes[pair[1]];
      var iInPath = path.indexOf(pair[0]) !== -1 && path.indexOf(pair[1]) !== -1 && Math.abs(path.indexOf(pair[0]) - path.indexOf(pair[1])) === 1;
      ctx.strokeStyle = iInPath ? 'rgba(0,255,128,0.55)' : 'rgba(0,150,80,0.18)';
      ctx.lineWidth = iInPath ? 2 : 1;
      if (iInPath) ctx.setLineDash([4, 3]);
      ctx.beginPath(); ctx.moveTo(na.x, na.y); ctx.lineTo(nb.x, nb.y); ctx.stroke();
      ctx.setLineDash([]);
    });
    nodes.forEach(function(n) {
      var inPath = path.indexOf(n.id) !== -1;
      var isLast = path.length > 0 && path[path.length - 1] === n.id;
      ctx.beginPath(); ctx.arc(n.x, n.y, 22, 0, Math.PI * 2);
      ctx.fillStyle = n.type === 'exit' ? '#001A0A' : n.type === 'entry' ? '#00080F' : inPath ? '#002210' : '#030C08';
      ctx.fill();
      ctx.strokeStyle = isLast ? '#00FF80' : n.type === 'exit' ? '#22C55E' : n.type === 'entry' ? '#4A9FFF' : inPath ? '#00C060' : '#0A3020';
      ctx.lineWidth = isLast ? 2.5 : 1.5; ctx.stroke();
      ctx.fillStyle = n.type === 'entry' ? '#4A9FFF' : n.type === 'exit' ? '#22C55E' : inPath ? '#00FF80' : '#1A6040';
      ctx.font = '11px monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
      ctx.fillText(n.type === 'entry' ? 'IN' : n.type === 'exit' ? 'OUT' : n.id.toString(16).toUpperCase().padStart(2, '0'), n.x, n.y);
      ctx.textBaseline = 'alphabetic';
    });
  }, [initData, path]);

  function handleClick(e) {
    if (status !== 'active') return;
    var canvas = canvasRef.current;
    var rect = canvas.getBoundingClientRect();
    var mx = e.clientX - rect.left, my = e.clientY - rect.top;
    var clicked = null;
    initData.nodes.forEach(function(n) { if (Math.sqrt((n.x - mx) * (n.x - mx) + (n.y - my) * (n.y - my)) < 26) clicked = n; });
    if (!clicked) return;
    if (path.length === 0) {
      if (clicked.type !== 'entry') return;
      setPath([clicked.id]);
    } else {
      if (path.indexOf(clicked.id) !== -1) return;
      var newPath = path.concat([clicked.id]);
      setPath(newPath);
      if (clicked.type === 'exit') { setStatus('success'); setTimeout(onSuccess, 700); }
    }
  }

  var timeRatio = timeLeft / 20;
  var msg = status === 'active' ? (path.length === 0 ? 'Click IN to start. Trace a bypass route to OUT.' : 'Continue tracing the path to OUT.') : status === 'success' ? 'BYPASS COMPLETE' : 'SECURITY TRIGGERED';

  return (
    <div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.95)', zIndex: 200, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', fontFamily: "'IBM Plex Mono',monospace" }}>
      <div style={{ background: '#020810', border: '1px solid #0A4030', borderRadius: 6, padding: 22, maxWidth: 440, width: '100%' }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
          <div style={{ color: '#22C55E', fontSize: '0.65rem', letterSpacing: '0.2em' }}>TERMINAL SLICING OVERRIDE</div>
          <div style={{ color: timeLeft <= 5 ? '#C03030' : '#555', fontSize: '0.65rem' }}>T:{timeLeft}s</div>
        </div>
        <div style={{ height: 3, background: '#111', borderRadius: 2, marginBottom: 14 }}>
          <div style={{ height: '100%', width: (timeRatio * 100) + '%', background: timeRatio > 0.5 ? '#22C55E' : timeRatio > 0.25 ? '#C8A000' : '#C03030', borderRadius: 2, transition: 'width 1s linear' }} />
        </div>
        <canvas ref={canvasRef} width={420} height={280} onClick={handleClick} style={{ display: 'block', cursor: 'crosshair', width: '100%', borderRadius: 4, border: '1px solid #0A2018' }} />
        <div style={{ marginTop: 10, color: status === 'success' ? '#22C55E' : status === 'fail' ? '#C03030' : '#555', fontSize: '0.55rem', textAlign: 'center' }}>{msg}</div>
        {status === 'fail' && <button onClick={onFailure} style={{ marginTop: 8, width: '100%', background: '#1A0808', color: '#888', border: '1px solid #333', padding: '6px', cursor: 'pointer', fontSize: '0.65rem', fontFamily: "'IBM Plex Mono',monospace" }}>Abort</button>}
      </div>
    </div>
  );
}

function ValveOverrideOverlay({ onSuccess, onFailure }) {
  const [pressure, setPressure] = React.useState(50);
  const [target] = React.useState(() => 65 + Math.floor(Math.random() * 20));
  const [timeLeft, setTimeLeft] = React.useState(8);
  const [locked, setLocked] = React.useState(false);
  const [message, setMessage] = React.useState('');
  const lockedRef = React.useRef(false);

  React.useEffect(() => {
    const timer = setInterval(() => {
      setTimeLeft(t => {
        if (t <= 1) { clearInterval(timer); if (!lockedRef.current) onFailure(); return 0; }
        return t - 1;
      });
    }, 1000);
    return () => clearInterval(timer);
  }, []);

  React.useEffect(() => {
    const handler = (e) => {
      if (lockedRef.current) return;
      if (e.key === 'a' || e.key === 'A' || e.key === 'ArrowLeft') { setPressure(p => Math.max(0, p - 5)); e.preventDefault(); }
      else if (e.key === 'd' || e.key === 'D' || e.key === 'ArrowRight') { setPressure(p => Math.min(100, p + 5)); e.preventDefault(); }
      else if (e.key === ' ' || e.key === 'Enter') {
        if (lockedRef.current) return;
        lockedRef.current = true;
        setLocked(true);
        e.preventDefault();
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  React.useEffect(() => {
    if (!locked) return;
    const diff = Math.abs(pressure - target);
    if (diff <= 8) { setMessage('Pressure stabilized. Valve locked.'); setTimeout(onSuccess, 1000); }
    else { setMessage(`Off by ${diff} units. Pressure spike detected.`); setTimeout(onFailure, 1200); }
  }, [locked]);

  const ov = { position:'fixed', inset:0, background:'rgba(0,0,0,0.93)', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', zIndex:50 };
  const pan = { background:'#080402', border:'1px solid #FF4444', borderRadius:6, padding:28, minWidth:360, color:'#C9C5BE', fontFamily:"'IBM Plex Mono',monospace" };
  const barPct = pressure;
  const tStart = Math.max(0, target - 8);
  const inZone = Math.abs(pressure - target) <= 8;

  return (
    <div style={ov}>
      <div style={pan}>
        <div style={{ color:'#FF4444', fontSize:13, letterSpacing:'0.15em', marginBottom:8 }}>PRESSURE VALVE OVERRIDE</div>
        <div style={{ fontSize:11, color:'#8A8F9E', marginBottom:16 }}>Match pressure to the green target zone, then lock. Timer: {timeLeft}s</div>
        <div style={{ position:'relative', height:36, background:'#160604', borderRadius:4, marginBottom:8, overflow:'hidden', border:'1px solid #3A1010' }}>
          <div style={{ position:'absolute', left:`${tStart}%`, width:'16%', height:'100%', background:'rgba(60,180,60,0.2)', borderLeft:'2px solid #40C040', borderRight:'2px solid #40C040' }} />
          <div style={{ position:'absolute', left:`${barPct}%`, top:0, bottom:0, width:4, background: inZone?'#40C040':'#FF4444', transform:'translateX(-50%)', transition:'left 0.1s, background 0.2s', borderRadius:2 }} />
        </div>
        <div style={{ display:'flex', justifyContent:'space-between', fontSize:11, marginBottom:16 }}>
          <span style={{ color:'#604040' }}>0</span>
          <span>PRESSURE: <span style={{ color: inZone?'#40C040':'#FF6060', fontWeight:700 }}>{pressure}</span></span>
          <span style={{ color:'#604040' }}>100</span>
        </div>
        {message && <div style={{ fontSize:11, color: message.includes('stabilized')?'#40C040':'#FF6060', marginBottom:10 }}>{message}</div>}
        <div style={{ fontSize:10, color:'#503030' }}>A/D: adjust · Space or Enter: lock</div>
      </div>
    </div>
  );
}

function WillpowerOverlay({ onSuccess, onFailure }) {
  const TOTAL = 12;
  const [willpower, setWillpower] = React.useState(TOTAL);
  const [switches, setSwitches] = React.useState([false, false, false]);
  const [done, setDone] = React.useState(false);
  const [message, setMessage] = React.useState('');
  const doneRef = React.useRef(false);

  React.useEffect(() => {
    const drain = setInterval(() => {
      setWillpower(w => {
        if (doneRef.current) { clearInterval(drain); return w; }
        if (w <= 1) {
          clearInterval(drain);
          if (!doneRef.current) { doneRef.current = true; setDone(true); setMessage('Neural integrity lost. Echo-7 takes hold.'); setTimeout(onFailure, 1400); }
          return 0;
        }
        return w - 1;
      });
    }, 900);
    return () => clearInterval(drain);
  }, []);

  React.useEffect(() => {
    const handler = (e) => {
      if (doneRef.current) return;
      let idx = -1;
      if (e.key === 'q' || e.key === 'Q') idx = 0;
      else if (e.key === 'w' || e.key === 'W') idx = 1;
      else if (e.key === 'e' || e.key === 'E') idx = 2;
      if (idx !== -1) {
        e.preventDefault();
        setSwitches(prev => {
          const next = [...prev];
          next[idx] = true;
          if (next.every(Boolean) && !doneRef.current) {
            doneRef.current = true;
            setDone(true);
            setMessage('Neural dampeners active. Consciousness secured.');
            setTimeout(onSuccess, 1200);
          }
          return next;
        });
      }
    };
    window.addEventListener('keydown', handler);
    return () => window.removeEventListener('keydown', handler);
  }, []);

  const ov = { position:'fixed', inset:0, background:'rgba(0,0,10,0.96)', display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', zIndex:50 };
  const pan = { background:'#050510', border:'1px solid #9966FF', borderRadius:6, padding:28, minWidth:380, color:'#C0BBDD', fontFamily:"'IBM Plex Mono',monospace" };
  const fillPct = (willpower / TOTAL) * 100;
  const barColor = fillPct > 50 ? '#9966FF' : fillPct > 25 ? '#E8A030' : '#FF4422';

  return (
    <div style={ov}>
      <div style={pan}>
        <div style={{ color:'#9966FF', fontSize:13, letterSpacing:'0.15em', marginBottom:4 }}>NEURAL INTEGRITY OVERRIDE</div>
        <div style={{ fontSize:11, color:'#6A6890', marginBottom:16 }}>Activate all three dampeners before willpower is consumed. Q / W / E</div>
        <div style={{ fontSize:10, color:'#504880', marginBottom:10 }}>Echo-7 is pushing through. Hold the mind.</div>
        <div style={{ position:'relative', height:20, background:'#0A0815', borderRadius:4, marginBottom:16, overflow:'hidden', border:'1px solid #2A1A4A' }}>
          <div style={{ width:`${fillPct}%`, height:'100%', background:barColor, transition:'width 0.4s, background 0.4s', borderRadius:4 }} />
        </div>
        <div style={{ display:'flex', gap:16, marginBottom:20, justifyContent:'center' }}>
          {['Q','W','E'].map((key, i) => (
            <div key={i} style={{
              width:72, height:56, borderRadius:6, border: switches[i] ? '2px solid #9966FF' : '2px solid #2A1A4A',
              background: switches[i] ? 'rgba(153,102,255,0.18)' : '#0A0815',
              display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center',
              color: switches[i] ? '#9966FF' : '#403860', fontSize:18, fontWeight:700,
              transition:'all 0.2s'
            }}>
              <div>{key}</div>
              <div style={{ fontSize:9, marginTop:2 }}>{switches[i] ? 'LOCKED' : 'PRESS'}</div>
            </div>
          ))}
        </div>
        {message && <div style={{ fontSize:11, color: message.includes('secured') ? '#9966FF' : '#FF4422', marginBottom:8 }}>{message}</div>}
        <div style={{ fontSize:10, color:'#302848' }}>Each dampener buys time. All three must lock.</div>
      </div>
    </div>
  );
}

function isNpcVisible(npc, questFlags) {
  if (npc.hideAfterFlags && npc.hideAfterFlags.some(f => questFlags[f])) return false;
  if (npc.requiresFlag && !questFlags[npc.requiresFlag]) return false;
  return true;
}

const ZONE_ARCHETYPE_PROFILES = {
  exterior: {
    ambient: 'traffic',
    decor: ['cargo_crate', 'pipe', 'neon_sign', 'girder'],
    floorColor: '#1A1C2A', floorAlt: '#222438',
    wallDark: '#0C0D16', wallLight: '#181A28',
    accent: '#8FA6FF', accentGlow: 'rgba(143,166,255,0.18)', accentDim: '#3A4880',
    bg: 'radial-gradient(circle at 50% 10%, #141628 0%, #080A14 70%)',
  },
  interior_cantina: {
    ambient: 'neon_haze',
    decor: ['neon_sign', 'brazier', 'pillar', 'neon_sign'],
    floorColor: '#1A0C14', floorAlt: '#22101C',
    wallDark: '#0C0608', wallLight: '#180C10',
    accent: '#FF0055', accentGlow: 'rgba(255,0,85,0.22)', accentDim: '#660022',
    bg: 'radial-gradient(circle at 50% 40%, #1A080E 0%, #080406 70%)',
  },
  interior_slicer: {
    ambient: 'datastream',
    decor: ['archive', 'pipe', 'neon_sign', 'cable_bundle'],
    floorColor: '#080E0E', floorAlt: '#0C1414',
    wallDark: '#040808', wallLight: '#0C1010',
    accent: '#00F0FF', accentGlow: 'rgba(0,240,255,0.20)', accentDim: '#005060',
    bg: 'radial-gradient(circle at 30% 60%, #060E0E 0%, #040808 70%)',
  },
  interior_csf: {
    ambient: 'traffic',
    decor: ['pillar', 'archive', 'scan_arch'],
    floorColor: '#181C28', floorAlt: '#1E2430',
    wallDark: '#0A0C14', wallLight: '#141820',
    accent: '#4A9FFF', accentGlow: 'rgba(74,159,255,0.20)', accentDim: '#1A4A80',
    bg: 'radial-gradient(circle at 50% 30%, #10182A 0%, #080C14 70%)',
  },
  interior_warehouse: {
    ambient: 'steam',
    decor: ['cargo_crate', 'girder', 'pipe', 'warning_beacon', 'slag'],
    floorColor: '#1C1A14', floorAlt: '#24221A',
    wallDark: '#0A0902', wallLight: '#161408',
    accent: '#FF9900', accentGlow: 'rgba(255,153,0,0.18)', accentDim: '#804800',
    bg: 'radial-gradient(circle at 30% 70%, #181410 0%, #08070A 70%)',
  },
  interior_generic: {
    ambient: 'mist',
    decor: ['archive', 'pipe', 'cargo_crate'],
    floorColor: '#191E30', floorAlt: '#1E2438',
    wallDark: '#0A0C14', wallLight: '#141820',
    accent: '#7AB8E0', accentGlow: 'rgba(122,184,224,0.18)', accentDim: '#2A5870',
    bg: 'radial-gradient(circle at 50% 50%, #141828 0%, #080A14 70%)',
  },
};

const STORY_CHECKPOINTS = [
  {
    flag: 'chapter1_active',
    entry: {
      id: 'story-chapter1-start', title: 'Chapter 1: Bay 14', category: 'story',
      summary: 'Jon has pointed you toward Docking Bay 14.',
      body: ['Jon briefed you on the Bay 14 incident. Phrik alloy moved under false Senate credentials. Your first stop is Sector 4 Freight Hub, Level 088. Find out who walked that cargo out.'],
    },
  },
  {
    flag: 'freight_hub_investigated',
    entry: {
      id: 'story-bay14-investigated', title: 'Bay 14: Evidence Found', category: 'story',
      summary: 'The dock investigation is complete.',
      body: ['You gathered testimony and physical evidence from Docking Bay 14. Two unmarked lifters, Republic security codes that checked out clean, and blast marks that were not from any fuel fire. Someone with Senate access walked the Phrik alloy out in broad daylight.'],
    },
  },
  {
    flag: 'marlo_sky_talked',
    entry: {
      id: 'story-marlo-contacted', title: 'Contact: Slick Marlo', category: 'story',
      summary: "The broker has given you the buyer's identity.",
      body: ['Marlo confirmed the alloy is bound for The Works, Level 005. He handed over the Buyer\'s Encrypted ID and a warning: the Iron Syndicate is the real operation. Everything else is noise.'],
    },
  },
  {
    flag: 'vane_sky_cooperated',
    entry: {
      id: 'story-vane-allied', title: 'Contact: Officer Vane', category: 'story',
      summary: 'You are operating as a CSF Auxiliary.',
      body: ['Officer Vane accepted your evidence and issued a provisional Auxiliary Corps commission. You are now operating inside the Republic law enforcement structure. Your designation is AX-7. Supervisor: Vane, T.'],
    },
  },
  {
    flag: 'csf_training_complete',
    entry: {
      id: 'story-training-complete', title: 'CSF Training: Certified', category: 'story',
      summary: 'All three training modules passed. Sector 4 deployment cleared.',
      body: ['You completed the non-lethal combat module, the forensic slicing simulation, and the high-stress interrogation exercise. Sergeant Torren has cleared you for Sector 4 deployment. You have been issued an EMP grenade kit and Level 088 priority transit clearance.'],
    },
  },
  {
    flag: 'sector4_raid_complete',
    entry: {
      id: 'story-raid-complete', title: 'Sector 4: Raid Resolved', category: 'story',
      summary: 'The Docking Bay 14 operation has been shut down.',
      body: ['The Sector 4 raid is complete. The Iron Syndicate staging operation at Docking Bay 14 has been disrupted. The thread leads deeper: the alloy is already en route to The Works. The Senate connection is now confirmed.'],
    },
  },
  {
    flag: 'senate_line_secured',
    entry: {
      id: 'story-senate-line-secured', title: 'Senate Line: Secured', category: 'story',
      summary: 'The transit bomb has been neutralised.',
      body: ['The seismic charge aboard the Senate transit car has been jettisoned into the incinerator shaft. The Senate Plaza is intact. Whatever path you took to reach this point, the alloy is gone and the Senator\'s operation is exposed.'],
    },
  },
  {
    flag: 'jon_endgame_known',
    entry: {
      id: 'story-jon-backstory-revealed', title: 'Jon: The Scylla Route', category: 'story',
      summary: 'Jon revealed his connection to the Bay 14 operation.',
      body: ['Jon ran the Scylla route three years ago before it became a cover operation. He did not know what was in the sealed containers. When he found out, he shut it down. Someone later used his old transit authentication codes to reopen the route. The Bay 14 strike team built their operation from what Jon left behind.'],
    },
  },
  {
    flag: 'inheritance_active',
    entry: {
      id: 'story-inheritance-begins', title: 'The Inheritance', category: 'story',
      summary: "Jon has offered you the other half of his operation. The question is what you plan to do with it.",
      body: ["Jon's Scylla route is gone and the Senate connection burned. But the network beneath it still exists: pit income, data brokerage, warehouse agreements, a bribe schedule covering nine CSF officers. Fifteen years of infrastructure. He is asking you to stand beside him and run half of it. You said yes."],
    },
  },
  {
    flag: 'vault_heist_complete',
    entry: {
      id: 'story-vault-heist', title: 'The Vault', category: 'story',
      summary: "Jon's financial architecture is yours. His guards stopped receiving hazard pay at midnight.",
      body: ["You accessed the penthouse vault terminal using Reya's keycard and rerouted the Iron Syndicate's payroll credit stream. The bribe schedule that kept nine CSF officers looking the other way was cut off at midnight. Jon's operational security dissolved in forty-eight hours. He is now alone in the penthouse with no guards, no allies, and no leverage."],
    },
  },
  {
    flag: 'jon_status_dead',
    entry: {
      id: 'story-inheritance-path-a', title: 'The Betrayal', category: 'story',
      summary: "Jon is dead. The Iron Syndicate has a new director. The city does not know it yet.",
      body: ["Jon is gone. His custom blaster is in your hand and his operation belongs to you entirely. The Iron Syndicate's Coruscant network, the pit arena, the warehouse agreements, the bribe schedule, the financial architecture, all of it. The city does not know it yet. That window is yours to use."],
    },
  },
  {
    flag: 'jon_status_subjugated',
    entry: {
      id: 'story-inheritance-path-b', title: 'The Subjugation', category: 'story',
      summary: "Jon runs your operations. He answers to you. He has not yet decided if that is worse than death.",
      body: ["Jon accepted the COO role with the flat competence of someone who knows when to cut their losses. He manages the day-to-day. He handles the contacts he spent fifteen years building. He reports to you. The Syndicate is intact and twice as efficient now that one person holds both ends of it."],
    },
  },
  {
    flag: 'jon_status_rival',
    entry: {
      id: 'story-inheritance-path-c', title: 'The Schism', category: 'story',
      summary: "Two organisations. Two territories. Jon's cantinas against your freight network. The war is quiet for now.",
      body: ["The Iron Syndicate split along the seam you created. You took Bay 14, Shadow Town, and the Slicer Alleyway. Jon kept the Sky-Market cantinas and the Senate adjacencies. You are neighbours now. The border between your territories is clear. Neither side is shooting yet. Both sides are counting."],
    },
  },
];

const INITIAL_ROSTER = [
  { id: 'agent_mira',  name: 'Mira',  agentClass: 'Enforcer', skill: 2, loyalty: 70, traits: ['street_fighter'], status: 'available', turnsRemaining: 0 },
  { id: 'agent_tak',   name: 'Tak',   agentClass: 'Slicer',   skill: 3, loyalty: 60, traits: ['clean_breach'],   status: 'available', turnsRemaining: 0 },
  { id: 'agent_vonna', name: 'Vonna', agentClass: 'Smuggler', skill: 2, loyalty: 80, traits: ['cargo_runner'],   status: 'available', turnsRemaining: 0 },
  { id: 'agent_dresh', name: 'Dresh', agentClass: 'Fixer',    skill: 1, loyalty: 55, traits: ['fast_talker'],    status: 'available', turnsRemaining: 0 },
];

const BASE_CONTRACTS = [
  { id: 'contract_spice_run',  title: 'Spice Run: Level 800',     contractType: 'Smuggling',  difficulty: 2, reward: 600,  duration: 2, heatGenerated: 5,  primaryClass: 'Smuggler' },
  { id: 'contract_data_heist', title: 'Data Heist: Senate Annex', contractType: 'Data Heist', difficulty: 4, reward: 1800, duration: 3, heatGenerated: 20, primaryClass: 'Slicer'   },
  { id: 'contract_protection', title: 'Protection: Bay 14',       contractType: 'Extortion',  difficulty: 2, reward: 400,  duration: 1, heatGenerated: 8,  primaryClass: 'Enforcer' },
  { id: 'contract_bribe_run',  title: 'Bribe: Precinct 22',       contractType: 'Negotiation',difficulty: 3, reward: 900,  duration: 2, heatGenerated: 3,  primaryClass: 'Fixer'    },
];

function StarWarsRPG() {
  const [planetId, setPlanetId] = useState('coruscant');
  const [zoneId, setZoneId] = useState('spaceport');

  const zone = PLANETS[planetId].zones[zoneId];

  const [map, setMap] = useState(() => PLANETS.coruscant.zones.spaceport.buildMap());
  const [pos, setPos] = useState({ x: 14, y: 10 });
  const [facing, setFacing] = useState(1);
  const [credits, setCredits] = useState(340);
  const [inventory, setInventory] = useState([
    { id: 'comlink',       name: 'Comlink',       type: 'gear',       iconKind: 'tool',   qty: 1, value: 0,  description: "Standard-issue encrypted comlink. Jon's frequency is already stored." },
    { id: 'field_rations', name: 'Field Rations', type: 'consumable', iconKind: 'supply', qty: 2, value: 5,  description: 'Compressed nutrient bars. Tasteless. Effective.' },
  ]);
  const [codex, setCodex] = useState([]);
  const [showInventory, setShowInventory] = useState(false);
  const [showCodex, setShowCodex] = useState(false);
  const [collectedItems, setCollectedItems] = useState(() => new Set());
  const [completedInteractions, setCompletedInteractions] = useState(() => new Set());
  const [alignment, setAlignment] = useState({ morality: 0, loyalty: { republic: 0, sithEmpire: 0, underworld: 0 } });
  const [showTravel, setShowTravel] = useState(false);
  const [activeDialogue, setActiveDialogue] = useState(null);
  const [actionLog, setActionLog] = useState([{ text: 'Docked at Coruscant Spaceport, Subsurface Level 2. Your contact Jon is supposed to be waiting near Docking Bay 14.', zone: 'spaceport' }]);
  const [transitioning, setTransitioning] = useState(false);
  const [questFlags, setQuestFlags] = useState({});
  const [showSpeeder, setShowSpeeder] = useState(false);
  const [shuttlePilot, setShuttlePilot] = useState(null);
  const [cinematic, setCinematic] = useState(null);
  const encStepsRef = React.useRef(0);
  const patrolRef = React.useRef({});
  const [npcPositions, setNpcPositions] = useState({});
  const [suspicionMeter, setSuspicionMeter] = useState(0);
  const [choiceFeedback, setChoiceFeedback] = useState(null);
  const [activeMinigame, setActiveMinigame] = useState(null);
  const [showVendor, setShowVendor] = useState(false);
  const [activeVendorNpc, setActiveVendorNpc] = useState(null);
  const [syndicateRoster, setSyndicateRoster] = useState([]);
  const [syndicateContracts] = useState(BASE_CONTRACTS);
  const [syndicateActiveContracts, setSyndicateActiveContracts] = useState([]);
  const [syndicateHeat, setSyndicateHeat] = useState(0);
  const [syndicateTerritories, setSyndicateTerritories] = useState([]);
  const [showDebug, setShowDebug] = useState(false);
  const posRef = React.useRef(pos);
  const questFlagsRef = React.useRef(questFlags);

  const worldState = React.useMemo(() => {
    const repTotal = (questFlags.csf_duty_stance ? 1 : 0) + (questFlags.jaxxon_arrested ? 1 : 0)
      + (questFlags.vane_record_commend ? 1 : 0) + (questFlags.jon_gone_straight_warned ? 1 : 0)
      + (questFlags.marro_recruited ? 1 : 0) + (questFlags.ambush_survived ? 1 : 0);
    const uwTotal = (questFlags.inside_man_path ? 1 : 0) + (questFlags.jaxxon_deal ? 1 : 0)
      + (questFlags.rook_eliminated ? 1 : 0) + (questFlags.marlo_sky_talked ? 1 : 0)
      + (questFlags.jon_status_dead ? 2 : 0) + (questFlags.syndicate_1313_founded ? 2 : 0)
      + (questFlags.syndicateManagement_active ? 1 : 0)
      + (questFlags.sith_contact ? 1 : 0) + (questFlags.grix_recruited ? 1 : 0)
      + (questFlags.vael_recruited ? 1 : 0) + (questFlags.black_sun_allied ? 2 : 0);
    if (repTotal > uwTotal) return 'lawful';
    if (uwTotal > repTotal) return 'underworld';
    return 'neutral';
  }, [questFlags]);

  const currentObjective = React.useMemo(() => {
    if (BADOR_SURFACE_ZONES.includes(zoneId)) {
      if (!questFlags.bador_pass_issued) return '[BADOR] Check in with Lieutenant Vane on the landing deck and collect your Base Access Pass.';
      if (!questFlags.vael_briefed) return '[BADOR] Yellow Alert. Report to Commander Vael in the Tactical Sanctum inside Main HQ.';
      if (zoneId === 'city_outskirts_bador') {
        if (!questFlags.outskirts_started) return '[OUTSKIRTS] Firestorm at the Perimeter: reach Squad Leader Kaelin at the breach.';
        if (!questFlags.outskirts_mortar_down) return '[OUTSKIRTS] Get Captain Bevin\'s plan at the redoubt, then silence the KEF mortar line (push, flank or slice the turret).';
        if (!questFlags.kef_prisoner_interrogated) return '[OUTSKIRTS] Question the wounded KEF insurgent in the alley behind the cantina.';
        if (!questFlags.outskirts_complete) return '[OUTSKIRTS] Tell Kaelin your decision: overcharge the gate grid or flank through the alleys.';
        if (!questFlags.varek_resolved) return '[OUTSKIRTS] Silence the Iron Thunder: open the Mortar Fortress vault in the southeast and face Varek Korr.';
        if (!questFlags.front_outskirts_closed) return '[OUTSKIRTS] The depot gate is open. Report to Squad Leader Kaelin at the breach, then Captain Bevin at the redoubt.';
        return '[OUTSKIRTS] The depot gate is open. Take the landspeeder south to the Engine Wastes.';
      }
      if (zoneId === 'engine_wastes') {
        if (!questFlags.vance_met) return '[WASTES] Find Combustion Foundry 09 and speak to Vance Crankshaft.';
        if (!questFlags.governor_valve_acquired) return '[WASTES] Drain the Dead Boiler Matrix (three valve wheels) and take the Catalyst Governor Valve.';
        if (!questFlags.overseer_defeated) return '[WASTES] Defeat the Unchained Overseer at the base of the Exhaust Cathedral.';
        if (!questFlags.manifest_restarted) return '[WASTES] Seat the Governor Valve in the apex socket to restart the manifold.';
        if (!questFlags.zone05_gate_open) return '[WASTES] The manifold runs. Open the central blast doors on the east wall to unseal the Zone Five Descent.';
        return '[WASTES] The Zone Five Descent is open on the east wall. The Deep Caverns lie below.';
      }
      if (zoneId === 'deep_caverns') {
        if (!(questFlags.unit8x_destroyed || questFlags.unit8x_reprogrammed || questFlags.unit8x_stood_down || questFlags.sentinel_cleared)) return '[CAVERNS] Get past Unit 8-X at the Guardian Gate in the east.';
        return '[CAVERNS] The shutter is open. Enter the wreck of the Zora IV.';
      }
      if (zoneId === 'zora_iv_wreckage') {
        if (questFlags.ghost_keel_complete) return '[GHOST KEEL] Complete. Report to Commander Vael at HQ and Tanner in the Private Suites.';
        if (!questFlags.hypercore_vault_open) return '[ZORA IV] Cross the reactor corridor and open the hypercore vault lock.';
        if (!questFlags.warden_guards_down) return '[ZORA IV] Destroy the hull sentinels guarding the vault.';
        if (!questFlags.warden_prime_down) return '[ZORA IV] Defeat the Warden Prime.';
        if (!questFlags.hypercore_visible) return '[ZORA IV] Enter the hypercore chamber.';
        return '[ZORA IV] Decide the fate of the Ghost Keel with the Architect.';
      }
      if (zoneId === 'outpost_7') {
        if (!questFlags.o7_coop_briefed) return '[OUTPOST 7] Report to Major Coop in the command bunker.';
        if (!questFlags.sundown_started) return '[OUTPOST 7] Prepare. Raid the convoy, siphon the geothermal vents, question the prisoner, keep your thermal signature low. Then tell Coop to begin Operation Sundown.';
        if (!questFlags.sundown_complete) return '[SUNDOWN] Hold the Iron Trench, destroy the sledge tank at LZ Delta, then breach the gateway.';
        if (!questFlags.front_sundown_closed) return '[OUTPOST 7] Sundown is complete. Report to Major Coop in the command bunker.';
        return '[OUTPOST 7] Sundown is complete. The gateway south to the Engine Wastes is open.';
      }
      if (!questFlags.scan_team_fate_known) return '[BADOR] Find the lost scan team and the overdue supply skiff. Sweep Sector 4 and the Engine Wastes.';
    }
    if (questFlags.syndicateManagement_active && !questFlags.syndicate_first_contract_run) return '[SYNDICATE] Open the War Table at the Penthouse and run your first contract.';
    if (questFlags.syndicateManagement_active) return '[SYNDICATE] Run the network from the Penthouse War Table. Expand your territory, recruit lieutenants, and take the Sector Control campaign to 10,000 credits per turn.';
    if (questFlags.vault_heist_complete && !questFlags.jon_status_dead && !questFlags.jon_status_subjugated && !questFlags.jon_status_rival) return '[INHERITANCE] Go to the Penthouse. Your lieutenants are ready. Jon has nowhere to run.';
    if ((questFlags.malak_turned || questFlags.malak_dead) && (questFlags.reya_loyal || questFlags.reya_blackmailed) && !questFlags.vault_heist_complete) return '[INHERITANCE] Use Reya\'s keycard at the Penthouse vault terminal. Freeze the payroll.';
    if ((questFlags.malak_turned || questFlags.malak_dead) && !questFlags.reya_loyal && !questFlags.reya_blackmailed && !questFlags.reya_warrant_mission_active) return '[INHERITANCE] Speak with Reya in Shadow Town. She has a condition.';
    if (questFlags.reya_warrant_cleared && !questFlags.reya_loyal && !questFlags.reya_blackmailed) return '[INHERITANCE] The warrant is gone. Return to Reya in Shadow Town for the vault keycard.';
    if (questFlags.reya_warrant_mission_active && !questFlags.reya_warrant_cleared) return '[INHERITANCE] Slice Precinct 42 at the CSF Academy and clear Reya\'s warrant.';
    if (questFlags.cargo_skimmed && !questFlags.shadow_cache_confirmed && !questFlags.malak_turned && !questFlags.malak_dead) return '[INHERITANCE] Stash the ten skimmed crates. Use the cache key at the arms cache in the northeast alcove of Shadow Town, Level 1312.';
    if ((questFlags.cargo_skimmed || questFlags.cargo_delivered_full) && !questFlags.malak_turned && !questFlags.malak_dead) return '[INHERITANCE] Find Malak at Shadow Town, Level 1312. Turn him or remove him.';
    if (questFlags.inheritance_active && !questFlags.weapons_manifest_acquired && !questFlags.cargo_skimmed && !questFlags.cargo_delivered_full) return '[INHERITANCE] Go to the Sector 4 Freight Hub. Take the Bay 14 weapons manifest from the cache in the northeast bay.';
    if (questFlags.inheritance_active && questFlags.weapons_manifest_acquired && !questFlags.cargo_skimmed && !questFlags.cargo_delivered_full) return '[INHERITANCE] Bring the Bay 14 weapons manifest back to Jon.';
    if (questFlags.senate_line_secured && !questFlags.inheritance_active) return 'Jon has a new proposition. Return to his apartment.';
    if (questFlags.bomb_reached && !questFlags.senate_line_secured) return 'Neutralize the weapon on the Senate transit line. Car 12, east cargo bay of the mag-rail corridor. Bring the drive from the Syndicate command terminal.';
    if (questFlags.sector4_raid_complete && !questFlags.jon_endgame_known) return 'Debrief Jon on the raid. He has intel on the Senate connection you need.';
    if (questFlags.sector4_raid_complete) return 'Pursue the Iron Syndicate to Level 005. Find Vex.';
    if (questFlags.csf_briefed && !questFlags.jon_confrontation_done) return 'Jon needs to see your CSF badge. Return to his apartment now.';
    if (questFlags.bay14_reported && !questFlags.sector4_raid_complete) return '[CSF] Breach the sealed cargo door at Platform 09 in the Sector 4 Freight Hub. Vane\'s team is waiting.';
    if (questFlags.csf_training_complete && questFlags.freight_hub_investigated && !questFlags.bay14_reported) return 'Return to Vane at CSF Academy. Report your Bay 14 findings.';
    if (questFlags.csf_training_complete) return 'Report to Vane at Sector 4 Freight Hub. Investigate Bay 14.';
    if (questFlags.module_a_complete && questFlags.module_b_complete && questFlags.module_c_complete && !questFlags.csf_training_complete) return 'All three modules are cleared. Report to Sergeant Torren in the drill yard for certification.';
    if (questFlags.csf_briefed) return 'Complete all three training modules at the CSF Academy.';
    if ((questFlags.marlo_sky_talked || questFlags.vane_sky_cooperated) && !questFlags.jon_sky_market_debriefed) return 'Check in with Jon. He will want to know which side you picked at the Sky-Market.';
    if (questFlags.republic_path_open) return 'Travel to CSF Tactical Command, Level 1222.';
    if (questFlags.marlo_sky_talked) return 'Follow the Phrik trail to Level 005. Reach The Works.';
    if (questFlags.marlo_sky_intro || questFlags.vane_sky_intro) return 'Present the Scylla manifest to your Sky-Market contact. If you do not have it, return to Docking Bay 14.';
    if (questFlags.sky_market_direction_given) return 'Travel to Sky-Market District, Level 1450. Find Marlo at the Aurebesh Lounge or Officer Vane at the precinct.';
    if (questFlags.freight_hub_investigated && !questFlags.jon_bay14_briefed) return 'Report to Jon at his apartment. He needs to know what you found at Bay 14.';
    if (questFlags.met_jon_spaceport) return 'Locate the Scylla Freight manifest. Start at Docking Bay 14.';
    if (questFlags.echo7_vault_approach_active && !questFlags.echo7_climax_reached) return '[SIDE QUEST] Reach the Republic Archive Core in Sub-Level 1313. Echo-7 is waiting.';
    if (questFlags.echo7_clearance_acquired && !questFlags.echo7_vault_approach_active) return '[SIDE QUEST] Use the CSF clearance pass to enter Sub-Level 1313 via the Slicer Alleyway.';
    if (questFlags.echo7_vane_leverage_offered && !questFlags.echo7_clearance_acquired) return '[SIDE QUEST] Confront Officer Vane at the CSF Academy about Transfer Code 99-Delta.';
    if (questFlags.echo7_node2_extracted && !questFlags.echo7_vane_leverage_offered) return '[SIDE QUEST] Return to Echo-7 in the Slicer Alleyway. Both memory fragments are recovered.';
    if (questFlags.echo7_node1_extracted && !questFlags.echo7_node2_extracted) return '[SIDE QUEST] Extract the second Echo-7 memory fragment from the Sky-Market penthouse relay tap.';
    if (questFlags.echo7_found && !questFlags.echo7_node1_extracted) return '[SIDE QUEST] Extract Echo-7 memory fragment from the overrun server stack in the Slicer Alleyway.';
    if (questFlags.syndicate_ledger_found && !questFlags.echo7_clearance_acquired) return '[SIDE QUEST] The Syndicate ledger gives you leverage over Vane. Visit the CSF Academy.';
    if (questFlags.sabacc_won && !questFlags.syndicate_ledger_found) return '[SIDE QUEST] Use the keycard from Madame Vex to access Bay 14 restricted storage alcove.';
    if (questFlags.vex_sabacc_offered && !questFlags.sabacc_won) return '[SIDE QUEST] Return to Madame Vex in Sky-Market for the high-stakes Sabacc match.';
    if (questFlags.garrus_coordinates_sold && !questFlags.sublevels_entrance_found) return '[SIDE QUEST] Head to Sub-Level 1313 to investigate the pre-Republic vault Garrus described.';
    return 'Find your contact Jon at Coruscant Spaceport, Docking Bay 14.';
  }, [questFlags, zoneId]);

  const pushActionLog = useCallback((msg, zoneLabel) => {
    setActionLog((prev) => [{ text: msg, zone: zoneLabel || '' }, ...prev.slice(0, 49)]);
  }, []);

  const setFlag = useCallback((key) => {
    if (KUAT_WORLD_EFFECTS[key] && !questFlagsRef.current[key]) pushActionLog(`[WORLD EFFECT] ${KUAT_WORLD_EFFECTS[key].label}. ${KUAT_WORLD_EFFECTS[key].text}`, 'kuat');
    setQuestFlags((prev) => ({ ...prev, [key]: true }));
  }, []);

  // Thermal Signature (0 to 100) on Bador: how loud the player is. Stored in questFlags so it saves with everything else.
  const addThermal = useCallback((d) => {
    setQuestFlags((prev) => {
      const cur = prev.thermal_index || 0; const n = Math.max(0, Math.min(100, cur + d));
      if (n === cur) return prev;
      return { ...prev, thermal_index: n, thermal_30: n >= 30, thermal_60: n >= 60 };
    });
    pushActionLog(`[THERMAL SIGNATURE] ${d > 0 ? 'Rising' : 'Falling'} (${d > 0 ? '+' : ''}${d}).`, 'bador');
  }, [pushActionLog]);


  const addItem = useCallback((itemDef) => setInventory(prev => {
    const ex = prev.find(i => i.id === itemDef.id);
    if (ex) return prev.map(i => i.id === itemDef.id ? { ...i, qty: (i.qty || 1) + 1 } : i);
    return [...prev, { ...itemDef, qty: 1 }];
  }), []);

  const unlockCodex = useCallback((entry) => setCodex(prev => {
    if (prev.find(e => e.id === entry.id)) return prev;
    return [...prev, { ...entry, unread: true }];
  }), []);

  const phPerks = React.useMemo(() => { const base = PENTHOUSE_PACKAGES.reduce((a, p) => (questFlags[p.flag] ? { income: a.income + p.income, decay: a.decay + p.heatDecay } : a), { income: (questFlags.mfg_tithe_rich ? 250 : questFlags.mfg_tithe ? 150 : 0), decay: 0 }); const withFlagship = questFlags.syndicate_flagship_active ? { income: base.income + 400, decay: base.decay + 1 } : base; return worldEffectsOf(questFlags).reduce((a, e) => ({ income: a.income + (e.income || 0), decay: a.decay + (e.decay || 0) }), withFlagship); }, [questFlags]);
  const installPenthousePackage = useCallback((pkg) => {
    setInventory(prev => prev.flatMap(i => i.id !== pkg.itemId ? [i] : (i.qty > 1 ? [{ ...i, qty: i.qty - 1 }] : [])));
    setFlag(pkg.flag);
    if (PENTHOUSE_PACKAGES.every((q) => q.id === pkg.id || questFlagsRef.current[q.flag])) setFlag('ph_complete');
    pushActionLog(`[Interior Design] ${pkg.name} installed. ${pkg.perk}`, 'penthouse');
    if (pkg.id === 'skyview') unlockCodex(CODEX_ENTRIES['codex-ph-observatory']);
  }, []);
  const removePenthousePackage = useCallback((pkg) => {
    setQuestFlags(prev => { const n = { ...prev }; delete n[pkg.flag]; return n; });
    addItem(ITEMS[pkg.itemId]);
    pushActionLog(`[Interior Design] ${pkg.name} removed and returned to your cargo.`, 'penthouse');
  }, []);

  const travelToZone = useCallback((targetZoneId, targetPos) => {
    if (targetZoneId === 'penthouse' && !questFlagsRef.current.inheritance_active) {
      pushActionLog('The elevator requires authorization. Come back when you have it.', zoneId);
      return;
    }
    setTransitioning(true);
    setTimeout(() => {
      const newZone = PLANETS[planetId].zones[targetZoneId];
      setZoneId(targetZoneId);
      setMap(newZone.buildMap());
      setPos(targetPos);
      setTransitioning(false);
      if (BADOR_SURFACE_ZONES.includes(targetZoneId)) setQuestFlags((prev) => { const c = prev.thermal_index || 0; if (!c) return prev; const n = Math.max(0, c - 5 - worldEffectsOf(prev).reduce((a, e) => a + (e.cool || 0), 0)); return { ...prev, thermal_index: n, thermal_30: n >= 30, thermal_60: n >= 60 }; });
      pushActionLog(`Entered ${newZone.name}.`, targetZoneId);
    }, 400);
  }, [planetId, pushActionLog]);

  const travelToPlanet = useCallback((destPlanetId) => {
    const destPlanet = PLANETS[destPlanetId];
    const destZone = destPlanet.zones[destPlanet.startZoneId];
    setTransitioning(true);
    pushActionLog(`Jumping to hyperspace: ${destPlanet.name}...`, zoneId);
    setCredits((c) => c - destPlanet.travelCost);
    setTimeout(() => {
      setPlanetId(destPlanetId);
      setZoneId(destPlanet.startZoneId);
      setMap(destZone.buildMap());
      setPos(destZone.spawnPos);
      setShowTravel(false);
      setTransitioning(false);
      pushActionLog(`Arrived at ${destPlanet.name}. ${destPlanet.description}`, destPlanet.startZoneId);
    }, 650);
  }, [zoneId, pushActionLog]);

  const arriveShuttle = useCallback((dest) => {
    const newZone = PLANETS[planetId].zones[dest.targetZone];
    setZoneId(dest.targetZone);
    setMap(newZone.buildMap());
    setPos(dest.targetPos);
    pushActionLog(dest.quiet ? `Entered ${newZone.name}.` : `Shuttle K-14 touches down. Entered ${newZone.name}.`, dest.targetZone);
    if (dest.targetZone === 'bador_main_base' && !questFlagsRef.current.bador_landed) {
      setFlag('bador_landed');
      pushActionLog(SHUTTLE_ARRIVAL_TEXT, dest.targetZone);
      unlockCodex({ ...CODEX_ENTRIES['codex-bador-arrival'], unread: true });
    }
  }, [planetId, pushActionLog, unlockCodex, setFlag]);

  useEffect(() => { posRef.current = pos; }, [pos]);
  useEffect(() => { questFlagsRef.current = questFlags; }, [questFlags]);
  useEffect(() => { setNpcPositions({}); encStepsRef.current = 0; patrolRef.current = {}; }, [zoneId]);
  useEffect(() => {
    if (zoneId === 'senatorial_lounges' || zoneId === 'penthouse') return;
    setQuestFlags((prev) => { if (!Object.keys(prev).some((k) => k.startsWith('buff_'))) return prev; const next = { ...prev }; Object.keys(next).forEach((k) => { if (k.startsWith('buff_')) delete next[k]; }); return next; });
  }, [zoneId]);
  useEffect(() => {
    const lines = zone.paAnnouncements;
    if (!lines || !lines.length) return;
    let i = Math.floor(Math.random() * lines.length);
    const id = setInterval(() => { pushActionLog(`[BASE PA] ${lines[i % lines.length]}`, zoneId); i += 1; }, 34000);
    return () => clearInterval(id);
  }, [zoneId]);
  useEffect(() => {
    const bz = BADOR_COMBAT_ZONES[zoneId];
    if (!bz) return;
    const f = questFlagsRef.current || {};
    if (f[`bador_cleared_${zoneId}`]) return;
    let wins = 0;
    for (let i = 1; i <= bz.need; i++) if (f[`bador_won_${zoneId}_${i}`]) wins = i;
    pushActionLog(`ACTIVE COMBAT ZONE: hostile raiders and scavengers operate here. ${bz.need - wins} contact${bz.need - wins === 1 ? '' : 's'} left to clear. KDY personnel will not engage you.`, zoneId);
  }, [zoneId]);
  useEffect(() => {
    const tickId = setInterval(() => {
      setNpcPositions((prev) => {
        const next = { ...prev };
        const playerPos = posRef.current;
        zone.npcs?.filter(npc => isNpcVisible(npc, questFlagsRef.current || {})).forEach((npc) => {
          if (!npc.mobile && !npc.wander && !npc.patrol) return;
          if (npc.wander && Math.random() < 0.4) return;
          const cur = prev[npc.id] || { x: npc.x, y: npc.y };
          if (npc.patrol) {
            const idx = patrolRef.current[npc.id] || 0;
            const tgt = npc.patrol[idx % npc.patrol.length];
            if (cur.x === tgt[0] && cur.y === tgt[1]) { patrolRef.current[npc.id] = (idx + 1) % npc.patrol.length; return; }
            const dx = Math.sign(tgt[0] - cur.x), dy = Math.sign(tgt[1] - cur.y);
            const horizFirst = Math.abs(tgt[0] - cur.x) >= Math.abs(tgt[1] - cur.y);
            const tries = (horizFirst ? [[dx, 0], [0, dy]] : [[0, dy], [dx, 0]]).filter(([a, b]) => a || b);
            for (const [mx, my] of tries) {
              const nx = cur.x + mx, ny = cur.y + my;
              const tile = map[ny]?.[nx];
              if (!tile || tile.type !== 'floor') continue;
              if (nx === playerPos.x && ny === playerPos.y) continue;
              if (zone.npcs.some((n) => { if (n.id === npc.id) return false; const np = prev[n.id] || { x: n.x, y: n.y }; return np.x === nx && np.y === ny; })) continue;
              if ((zone.worldObjects || []).some(o => o.x === nx && o.y === ny)) continue;
              next[npc.id] = { x: nx, y: ny };
              break;
            }
            return;
          }
          const dirs = [{ dx: 0, dy: -1 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 }, { dx: 1, dy: 0 }];
          dirs.sort(() => Math.random() - 0.5);
          for (const { dx, dy } of dirs) {
            const nx = cur.x + dx, ny = cur.y + dy;
            const tile = map[ny]?.[nx];
            if (!tile || tile.type !== 'floor') continue;
            if (nx === playerPos.x && ny === playerPos.y) continue;
            if (npc.wander && (Math.abs(nx - npc.x) > npc.wander || Math.abs(ny - npc.y) > npc.wander)) continue;
            if ((zone.worldObjects || []).some(o => o.x === nx && o.y === ny) || (zone.collectibles || []).some(c => c.x === nx && c.y === ny)) continue;
            const occupied = zone.npcs.some((n) => {
              if (n.id === npc.id) return false;
              const np = prev[n.id] || { x: n.x, y: n.y };
              return np.x === nx && np.y === ny;
            });
            if (occupied) continue;
            next[npc.id] = { x: nx, y: ny };
            break;
          }
        });
        return next;
      });
    }, 1500);
    return () => clearInterval(tickId);
  }, [zone, map]);

  useEffect(() => {
    if (questFlags.echo7_fused && !questFlags.trait_neural_vessel) setFlag('trait_neural_vessel');
    if (questFlags.echo7_purged && !questFlags.trait_iron_will) setFlag('trait_iron_will');
    if (questFlags.echo7_dominant && !questFlags.trait_ai_overlord) setFlag('trait_ai_overlord');
    if (questFlags.sabacc_won && questFlags.ledger_leaked && !questFlags.trait_extortionist) setFlag('trait_extortionist');
    if (questFlags.spore_chamber_entered && inventory.some(i => i.id === 'kyber_spore_crystal') && !questFlags.trait_mutated_scavenger) setFlag('trait_mutated_scavenger');
    if (questFlags.jon_status_dead && !questFlags.trait_crime_lord) setFlag('trait_crime_lord');
  }, [questFlags, inventory]);

  useEffect(() => {
    if (questFlags.syndicateManagement_active && syndicateRoster.length === 0) {
      setSyndicateRoster(INITIAL_ROSTER);
      const territories = [];
      if (questFlags.jon_status_dead || questFlags.syndicate_1313_founded) {
        territories.push('bay_14', 'shadow_town', 'slicer_alleyway');
      } else if (questFlags.jon_status_subjugated) {
        territories.push('shadow_town', 'slicer_alleyway');
      }
      setSyndicateTerritories(territories);
    }
  }, [questFlags, syndicateRoster.length]);

  useEffect(() => {
    const additions = [];
    if (questFlags.grix_recruited && !syndicateRoster.find(a => a.id === 'agent_grix'))
      additions.push({ id:'agent_grix', name:'Grix', agentClass:'Smuggler', skill:4, loyalty:65, traits:['devaronian_contacts'], status:'available', turnsRemaining:0 });
    if (questFlags.vael_recruited && !syndicateRoster.find(a => a.id === 'agent_vael'))
      additions.push({ id:'agent_vael', name:'Vael', agentClass:'Slicer', skill:4, loyalty:70, traits:['sis_training'], status:'available', turnsRemaining:0 });
    if (questFlags.gharza_recruited && !syndicateRoster.find(a => a.id === 'agent_gharza'))
      additions.push({ id:'agent_gharza', name:'Gharza', agentClass:'Enforcer', skill:5, loyalty:60, traits:['trandoshan_hunter','long_range'], status:'available', turnsRemaining:0 });
    if (questFlags.marro_recruited && !syndicateRoster.find(a => a.id === 'agent_marro'))
      additions.push({ id:'agent_marro', name:'Marro', agentClass:'Fixer', skill:3, loyalty:75, traits:['csf_contacts','heat_reduction'], status:'available', turnsRemaining:0 });
    if (additions.length > 0) setSyndicateRoster(r => [...r, ...additions]);
  }, [questFlags, syndicateRoster]);

  useEffect(() => {
    if (questFlags.syndicateManagement_active && syndicateActiveContracts.length > 0 && !questFlags.syndicate_first_contract_run) setFlag('syndicate_first_contract_run');
    if (syndicateHeat >= 70 && !questFlags.heat_high_active) setFlag('heat_high_active');
    else if (syndicateHeat >= 40 && !questFlags.heat_mid_active) setFlag('heat_mid_active');
  }, [syndicateHeat, questFlags, syndicateActiveContracts]);

  useEffect(() => {
    STORY_CHECKPOINTS.forEach(({ flag, entry }) => {
      if (questFlags[flag] && !codex.find(e => e.id === entry.id)) {
        unlockCodex(entry);
      }
    });
  }, [questFlags, codex, unlockCodex]);

  const resolveChoice = useCallback((choice, npcId) => {
    setCompletedInteractions((prev) => new Set([...prev, npcId]));
    const moralityDelta = choice.morality || 0;
    const loyaltyDeltas = choice.loyalty || {};
    setAlignment((prev) => ({
      morality: Math.max(-100, Math.min(100, prev.morality + moralityDelta)),
      loyalty: {
        republic: Math.max(0, Math.min(100, prev.loyalty.republic + (loyaltyDeltas.republic || 0))),
        sithEmpire: Math.max(0, Math.min(100, prev.loyalty.sithEmpire + (loyaltyDeltas.sithEmpire || 0))),
        underworld: Math.max(0, Math.min(100, prev.loyalty.underworld + (loyaltyDeltas.underworld || 0))),
      },
    }));
    const feedbackParts = [];
    if (moralityDelta > 0) feedbackParts.push(`+${moralityDelta} LIGHT`);
    else if (moralityDelta < 0) feedbackParts.push(`${moralityDelta} DARK`);
    if (loyaltyDeltas.republic) feedbackParts.push(`${loyaltyDeltas.republic > 0 ? '+' : ''}${loyaltyDeltas.republic} REPUBLIC`);
    if (loyaltyDeltas.underworld) feedbackParts.push(`${loyaltyDeltas.underworld > 0 ? '+' : ''}${loyaltyDeltas.underworld} UNDERWORLD`);
    if (feedbackParts.length > 0) {
      setChoiceFeedback(feedbackParts.join('  '));
      setTimeout(() => setChoiceFeedback(null), 2800);
    }
    if (choice.requires?.credits) setCredits((c) => c - choice.requires.credits);
    if (choice.grants?.credits) setCredits((c) => c + choice.grants.credits);
    if (choice.grants?.thermal) addThermal(choice.grants.thermal);
    [].concat(choice.grants?.questFlags || []).forEach((f) => setFlag(f));
    // A choice that grants nothing (a deferral, a "not yet") must not burn a phase that has a real progression choice.
    const grantsAny = (c) => c.grants && ((c.grants.flags || []).length || (c.grants.items || []).length || (c.grants.codex || []).length || c.grants.credits || c.grants.questFlags);
    if (activeDialogue && activeDialogue._activePhaseId && (choice.defer || (!grantsAny(choice) && (activeDialogue.choices || []).some(grantsAny)))) {
      const phaseKey = activeDialogue.id + ':' + activeDialogue._activePhaseId;
      setCompletedInteractions((prev) => { const s = new Set(prev); s.delete(phaseKey); return s; });
    }
    if (choice.grants?.flags) {
      choice.grants.flags.forEach((f) => {
        setFlag(f);
        if (f === 'deceiver_path') setSuspicionMeter(prev => Math.min(100, prev + 25));
        if (f === 'vane_suspicious_raised') setSuspicionMeter(prev => Math.min(100, prev + 20));
        if (f === 'jaxxon_deal') setSuspicionMeter(prev => Math.min(100, prev + 30));
      });
    }
    if (choice.grants?.items) choice.grants.items.forEach(id => { if (ITEMS[id]) addItem(ITEMS[id]); });
    if (choice.grants?.codex) choice.grants.codex.forEach(id => { if (CODEX_ENTRIES[id]) unlockCodex(CODEX_ENTRIES[id]); });
    pushActionLog(choice.result, zoneId);
    setActiveDialogue(null);
    if (choice.triggersMinigame) {
      setActiveMinigame({ type: choice.triggersMinigame, ...(choice.minigameConfig ?? {}), context: choice,
        onSuccess: (loot) => { [].concat(choice.onSuccessFlags || []).forEach((f) => setFlag(f)); if (loot && loot.credits) { setCredits((c) => c + loot.credits); pushActionLog(`+${loot.credits} credits recovered from the engagement.`, zoneId); } if (loot && loot.item) addItem(loot.item); pushActionLog('Engagement concluded.', zoneId); setActiveMinigame(null); },
        onFailure: () => { pushActionLog('You fell back. The attempt failed.', zoneId); setActiveMinigame(null); } });
    }
  }, [zoneId, pushActionLog, setFlag, addItem, unlockCodex, activeDialogue, addThermal]);

  useEffect(() => {
    const handleKey = (e) => {
      if (e.key === '`') { setShowDebug(v => !v); return; }
      if (showDebug) {
        const stage = DEBUG_STAGES.find(s => s.key === e.key);
        if (stage) { setQuestFlags(stage.flags); setCredits(stage.credits); travelToZone(stage.zone, stage.pos); setShowDebug(false); return; }
        if (e.key === 'C' && e.shiftKey) { setCredits(c => c + 5000); setShowDebug(false); return; }
        if (e.key === 'Z' && e.shiftKey) { setQuestFlags({}); setShowDebug(false); return; }
        setShowDebug(false); return;
      }
      if (showTravel || activeDialogue || transitioning || showSpeeder || shuttlePilot || cinematic || showInventory || showCodex || activeMinigame || showVendor) return;
      if (e.key === 'i' || e.key === 'I') { setShowInventory(v => !v); return; }
      if (e.key === 'c' || e.key === 'C') { setShowCodex(v => !v); return; }
      let { x, y } = pos;
      let newFacing = facing;
      if (e.key === 'w' || e.key === 'ArrowUp') y -= 1;
      else if (e.key === 's' || e.key === 'ArrowDown') y += 1;
      else if (e.key === 'a' || e.key === 'ArrowLeft') { x -= 1; newFacing = -1; }
      else if (e.key === 'd' || e.key === 'ArrowRight') { x += 1; newFacing = 1; }
      else return;

      e.preventDefault();
      if (newFacing !== facing) setFacing(newFacing);

      const tile = map[y]?.[x];
      if (!tile || tile.type === 'wall') { pushActionLog('Blocked.', zoneId); return; }
      if (tile.type === 'ship_hull') {
        const ship = shipAtTile(zone, x, y);
        if (ship) {
          pushActionLog(ship.description, zoneId);
          if (ship.grantsFlag && !questFlags[ship.grantsFlag]) setFlag(ship.grantsFlag);
          return;
        }
        pushActionLog('The hull plating is solid. No way through.', zoneId);
        return;
      }
      if (tile.type === 'lava') { pushActionLog('The lava channels are impassable. You can feel the heat from here.', zoneId); return; }
      if (tile.type === 'water') { pushActionLog('The water runs too deep to wade through.', zoneId); return; }
      if (tile.type === 'ship_ramp') { setShowTravel(true); return; }
      if (tile.type === 'door') {
        const door = zone.doors.find(d => d.x === x && d.y === y);
        if (door) {
          if ((door.requiresFlag && !questFlags[door.requiresFlag]) || (door.requiresAnyFlag && !door.requiresAnyFlag.some(f => questFlags[f]))) { pushActionLog(door.lockedMessage || 'The door is sealed.', zoneId); return; }
          if (door.cinematic) { setCinematic({ mode: door.cinematic, dest: { targetZone: door.targetZone, targetPos: door.targetPos, name: door.label, quiet: true } }); return; }
          travelToZone(door.targetZone, door.targetPos); return;
        }
      }

      const npcHere = zone.npcs?.filter(n => isNpcVisible(n, questFlags)).find((n) => {
        const p = npcPositions[n.id] || { x: n.x, y: n.y };
        return p.x === x && p.y === y;
      });
      if (npcHere) {
        if (npcHere.triggersOverlay === 'shuttle') {
          setShuttlePilot(npcHere); setPos({ x, y }); return;
        }
        if (npcHere.triggersOverlay === 'speeder') {
          if (!questFlags.speeder_transit_unlocked) { pushActionLog('RESTRICTED TRANSIT: Sector clearance pass required.', zoneId); setPos({ x, y }); return; }
          setShowSpeeder(true); setPos({ x, y }); return;
        }
        const resolvedNpc = resolveDialoguePhase(npcHere, questFlags);
        const interactionKey = resolvedNpc._activePhaseId
          ? resolvedNpc.id + ':' + resolvedNpc._activePhaseId
          : resolvedNpc.id;
        if (completedInteractions.has(interactionKey) && !resolvedNpc.repeatable) {
          pushActionLog(resolvedNpc.repeatPrompt || `${resolvedNpc.label} nods but says nothing new.`, zoneId);
          return;
        }
        setCompletedInteractions((prev) => new Set([...prev, interactionKey]));
        if (resolvedNpc.isVendor) {
          setActiveVendorNpc(resolvedNpc);
          setShowVendor(true);
          setPos({ x, y });
          return;
        }
        setActiveDialogue(resolvedNpc);
        return;
      }

      const worldObjHere = zone.worldObjects?.find(wo => wo.x === x && wo.y === y && !(wo.requiresNoneFlags || []).some(f => questFlags[f]) && !(wo.invisibleLocked && ((wo.requiresFlag && !questFlags[wo.requiresFlag]) || (wo.requiresAllFlags || []).some(f => !questFlags[f]))));
      if (worldObjHere) {
        if (worldObjHere.id.startsWith('airtaxi_')) {
          if (!questFlags.speeder_transit_unlocked) { pushActionLog('RESTRICTED TRANSIT: Sector clearance pass required.', zoneId); setPos({ x, y }); return; }
          setShowSpeeder(true); setPos({ x, y }); return;
        }
        if ((worldObjHere.requiresFlag && !questFlags[worldObjHere.requiresFlag]) || (worldObjHere.requiresAllFlags || []).some(f => !questFlags[f])) {
          pushActionLog(`[${worldObjHere.label}] ${worldObjHere.lockedMessage || 'Access restricted. Required condition not met.'}`, zoneId);
          setPos({ x, y }); return;
        }
        if (worldObjHere.requires?.item && !inventory.some(i => i.id === worldObjHere.requires.item)) {
          const itemName = ITEMS[worldObjHere.requires.item]?.name || worldObjHere.requires.item;
          pushActionLog(`[${worldObjHere.label}] Locked. Requires: ${itemName}.`, zoneId);
          setPos({ x, y }); return;
        }
        const alreadySeen = worldObjHere.once && completedInteractions.has(worldObjHere.id);
        if (!alreadySeen) {
          if (worldObjHere.autoCodex) {
            unlockCodex({ ...worldObjHere.autoCodex, unread: true });
            pushActionLog(`[DISCOVERED] ${worldObjHere.label} — logged to Codex.`, zoneId);
          } else {
            const woDesc = worldObjHere.worldStateVariant?.[worldState] ?? worldObjHere.description;
            pushActionLog(`[${worldObjHere.label}] ${woDesc}`, zoneId);
          }
          if (worldObjHere.healsRoster) setSyndicateRoster(r => r.map(a => a.status === 'injured' ? { ...a, status: 'available' } : a));
          if (worldObjHere.thermalDelta) addThermal(worldObjHere.thermalDelta);
          if (worldObjHere.heatDelta) setSyndicateHeat(h => Math.max(0, Math.min(100, h + worldObjHere.heatDelta)));
          if (worldObjHere.grantsCredits && !worldObjHere.triggersMinigame) setCredits(c => c + worldObjHere.grantsCredits);
          if (worldObjHere.grantsItem && ITEMS[worldObjHere.grantsItem] && !worldObjHere.triggersMinigame) {
            addItem(ITEMS[worldObjHere.grantsItem]);
            pushActionLog(`Acquired: ${ITEMS[worldObjHere.grantsItem].name}`, zoneId);
          }
          if (worldObjHere.triggersMinigame && !completedInteractions.has(worldObjHere.id)) {
            const successCb = (loot) => {
              [worldObjHere.grantsFlag, ...(worldObjHere.grantsFlags || [])].filter(Boolean).forEach(setFlag);
              if (worldObjHere.grantsItem && ITEMS[worldObjHere.grantsItem]) { addItem(ITEMS[worldObjHere.grantsItem]); pushActionLog(`Acquired: ${ITEMS[worldObjHere.grantsItem].name}`, zoneId); }
              if (worldObjHere.grantsCodex && CODEX_ENTRIES[worldObjHere.grantsCodex]) unlockCodex(CODEX_ENTRIES[worldObjHere.grantsCodex]);
              if (worldObjHere.grantsCredits) { setCredits(c => c + worldObjHere.grantsCredits); pushActionLog(`+${worldObjHere.grantsCredits} credits.`, zoneId); }
              if (worldObjHere.once) setCompletedInteractions(prev => new Set([...prev, worldObjHere.id]));
              if (worldObjHere.heatDeltaOnSuccess) setSyndicateHeat(h => Math.max(0, Math.min(100, h + worldObjHere.heatDeltaOnSuccess)));
              if (worldObjHere.thermalDeltaOnSuccess) addThermal(worldObjHere.thermalDeltaOnSuccess);
              if (loot && loot.credits) { setCredits(c => c + loot.credits); pushActionLog(`+${loot.credits} credits recovered from the engagement.`, zoneId); }
              if (loot && loot.item) { addItem(loot.item); pushActionLog(`Salvaged: ${loot.item.name}`, zoneId); }
              pushActionLog(`[${worldObjHere.label}] Engagement concluded.`, zoneId);
              setActiveMinigame(null);
            };
            const failCb = () => {
              if (worldObjHere.failGrantsFlags) {
                worldObjHere.failGrantsFlags.forEach(setFlag);
                if (worldObjHere.once) setCompletedInteractions(prev => new Set([...prev, worldObjHere.id]));
                pushActionLog(`[${worldObjHere.label}] ${worldObjHere.failMessage || 'The attempt failed, and it cost you.'}`, zoneId);
                setActiveMinigame(null);
                return;
              }
              pushActionLog(`[${worldObjHere.label}] Attempt failed. Security alert triggered.`, zoneId);
              setActiveMinigame(null);
            };
            setActiveMinigame({ type: worldObjHere.triggersMinigame, ...(worldObjHere.minigameConfig ?? {}), context: worldObjHere, onSuccess: successCb, onFailure: failCb });
            setPos({ x, y });
            return;
          }
          if (worldObjHere.grantsCodex && CODEX_ENTRIES[worldObjHere.grantsCodex]) unlockCodex(CODEX_ENTRIES[worldObjHere.grantsCodex]);
          [worldObjHere.grantsFlag, ...(worldObjHere.grantsFlags || [])].filter(Boolean).forEach(setFlag);
          if (worldObjHere.once) setCompletedInteractions((prev) => new Set([...prev, worldObjHere.id]));
        }
        setPos({ x, y });
        return;
      }

      const collectible = zone.collectibles?.find(c => c.x === x && c.y === y && !collectedItems.has(c.id));
      if (collectible) {
        if (collectible.reward > 0) setCredits((c) => c + collectible.reward);
        setCollectedItems((prev) => new Set([...prev, collectible.id]));
        if (collectible.grantsItem && ITEMS[collectible.grantsItem]) { addItem(ITEMS[collectible.grantsItem]); pushActionLog(`${collectible.label}. Acquired: ${ITEMS[collectible.grantsItem].name}`, zoneId); }
        else pushActionLog(`${collectible.label}.${collectible.reward > 0 ? ` (+${collectible.reward} credits)` : ''}`, zoneId);
      }

      const _bz = BADOR_COMBAT_ZONES[zoneId];
      if (_bz && !questFlags[`bador_cleared_${zoneId}`] && !activeMinigame) {
        encStepsRef.current += 1;
        const _th = questFlags.thermal_index || 0;
        const _rate = _bz.rate * (_th >= 60 ? 2 : _th >= 30 ? 1.5 : 1);
        if (encStepsRef.current >= BADOR_ENCOUNTER_MIN_STEPS && Math.random() * 100 < _rate) {
          encStepsRef.current = 0;
          const _pool = _th >= 60 ? [..._bz.pool, 'kef_shock_breacher', 'kef_mortar_specialist'] : _th >= 30 ? [..._bz.pool, 'kef_hunter_killer'] : _bz.pool;
          const _bKey = _pool[Math.floor(Math.random() * _pool.length)];
          setActiveMinigame({
            type: 'tactical_combat',
            opponentProfile: _bKey,
            flavorText: _bz.flavor[Math.floor(Math.random() * _bz.flavor.length)],
            onSuccess: (loot) => {
              const _cr = loot ? loot.credits : 100;
              setCredits(c => c + _cr);
              addThermal(4);
              if (loot && loot.item) addItem(loot.item);
              let wins = 0;
              for (let i = 1; i <= _bz.need; i++) if (questFlagsRef.current[`bador_won_${zoneId}_${i}`]) wins = i;
              wins += 1;
              setFlag(`bador_won_${zoneId}_${wins}`);
              if (wins >= _bz.need) {
                setFlag(`bador_cleared_${zoneId}`);
                pushActionLog(`You held the field. +${_cr} credits. ZONE CLEARED: ${zone.name} is quiet again. No further contact expected here.`, zoneId);
              } else {
                pushActionLog(`You held the field. +${_cr} credits. Hostile contacts remaining in this zone: ${_bz.need - wins}.`, zoneId);
              }
              setActiveMinigame(null);
            },
            onFailure: () => { pushActionLog('You fell back under fire. The hostiles are still out there.', zoneId); setActiveMinigame(null); },
          });
          setPos({ x, y });
          return;
        }
      }

      const _ePool = ENCOUNTER_TABLE[zoneId];
      if (_ePool && !questFlags[`enc_cd_${zoneId}`] && !activeMinigame) {
        const _hBonus = Math.floor((syndicateHeat || 0) * 0.4);
        const _baseRate = ENCOUNTER_RATES[zoneId] || 15;
        if (Math.random() * 100 < (_baseRate + _hBonus)) {
          const _pKey = _ePool[Math.floor(Math.random() * _ePool.length)];
          const _flavor = getEncounterFlavor(zoneId, _pKey);
          setFlag(`enc_cd_${zoneId}`);
          setActiveMinigame({
            type: 'tactical_combat',
            opponentProfile: _pKey,
            flavorText: _flavor,
            onSuccess: (loot) => {
              const _cr = loot ? loot.credits : 100;
              pushActionLog(`You held the field. +${_cr} credits recovered.`, zoneId);
              setCredits(c => c + _cr);
              if (loot && loot.item) addItem(loot.item);
              setActiveMinigame(null);
            },
            onFailure: () => { pushActionLog('You fell back under fire. CSF heat spikes.', zoneId); setSyndicateHeat(h => Math.min(100, h + 10)); setActiveMinigame(null); },
          });
          setPos({ x, y });
          return;
        }
      }

      setPos({ x, y });
    };
    window.addEventListener('keydown', handleKey);
    return () => window.removeEventListener('keydown', handleKey);
  }, [pos, map, zone, zoneId, facing, showTravel, activeDialogue, transitioning, showSpeeder, shuttlePilot, cinematic, showInventory, showCodex, activeMinigame, showVendor, questFlags, npcPositions, collectedItems, completedInteractions, pushActionLog, travelToZone, addItem, unlockCodex, setFlag]);

  const camX = Math.max(0, Math.min(zone.width - VIEWPORT_COLS, pos.x - Math.floor(VIEWPORT_COLS / 2)));
  const camY = Math.max(0, Math.min(zone.height - VIEWPORT_ROWS, pos.y - Math.floor(VIEWPORT_ROWS / 2)));

  return (
    <div style={{ minHeight:'100vh',background:zone.bg,color:'#C9C5BE',fontFamily:"'IBM Plex Mono',ui-monospace,monospace",display:'flex',flexDirection:'column',gap:10,padding:'16px 16px 20px',position:'relative',overflow:'hidden',transition:'background 0.6s ease' }}>
      <GlobalAnimations />
      {zone.textureId === 'coruscant' && <CoruscantBackdrop accent={zone.accent} accentDim={zone.accentDim} />}
      <AmbientLayer kind={zone.ambient} accent={zone.accent} />

      <div style={{ display:'flex',justifyContent:'space-between',alignItems:'baseline',position:'relative',zIndex:2 }}>
        <div>
          <span style={{ fontSize:16,fontWeight:600,color:zone.accent,textShadow:`0 0 12px ${zone.accentGlow}` }}>{zone.name}</span>
          <span style={{ fontSize:11,color:'#7A7F94',marginLeft:10 }}>{zone.subtitle}</span>
          {BADOR_SURFACE_ZONES.includes(zoneId) && (questFlags.thermal_index || 0) > 0 && <span style={{ fontSize:10,marginLeft:10,letterSpacing:'0.12em',color: questFlags.thermal_60 ? '#FF4A2A' : questFlags.thermal_30 ? '#FFB844' : '#7FD0FF' }}>THERMAL {questFlags.thermal_index}%</span>}
          {BADOR_COMBAT_ZONES[zoneId] && (questFlags[`bador_cleared_${zoneId}`]
            ? <span style={{ fontSize:10,color:'#3AFF8A',marginLeft:10,letterSpacing:'0.12em' }}>ZONE CLEARED</span>
            : <span style={{ fontSize:10,color:'#FF6A4A',marginLeft:10,letterSpacing:'0.12em' }}>ACTIVE COMBAT ZONE</span>)}
        </div>
        <div style={{ fontSize:12,color:'#E8C97A' }}>{credits} cr &nbsp;&middot;&nbsp; <span style={{color:'#6A7090'}}>{collectedItems.size} items</span></div>
      </div>

      <div style={{ position:'relative',display:'inline-block',alignSelf:'flex-start',zIndex:2 }}>
        <div style={{ position:'relative',overflow:'hidden',border:`1px solid ${zone.accentDim}`,background:zone.wallDark,boxShadow:`0 0 24px ${zone.accentGlow}` }}>
          {Array.from({ length: VIEWPORT_ROWS }, (_, vy) => {
            const ty = camY + vy;
            return (
              <div key={ty} style={{ display:'flex' }}>
                {Array.from({ length: VIEWPORT_COLS }, (_, vx) => {
                  const tx = camX + vx;
                  const tile = map[ty]?.[tx] || { type: 'wall' };
                  const isPlayer = pos.x === tx && pos.y === ty;
                  const npcHere = zone.npcs?.filter(n => isNpcVisible(n, questFlags)).find((n) => { const p = npcPositions[n.id] || { x: n.x, y: n.y }; return p.x === tx && p.y === ty; });
                  const collectibleHere = zone.collectibles?.find(c => c.x === tx && c.y === ty && !collectedItems.has(c.id));
                  const doorHere = zone.doors?.find(d => d.x === tx && d.y === ty);
                  const worldObjHere = zone.worldObjects?.find(wo => wo.x === tx && wo.y === ty && !(wo.once && completedInteractions.has(wo.id)));
                  const npcDone = npcHere && completedInteractions.has(npcHere.id);

                  let bg = zone.wallDark;
                  if (tile.type === 'floor') bg = floorBackground(zone, tx, ty);
                  if (tile.type === 'wall') bg = wallBackground(zone, tx, ty);
                  const inShip = tile.type === 'ship_hull' && !!shipAtTile(zone, tx, ty);
                  if (tile.type === 'ship_hull') bg = inShip ? floorBackground(zone, tx, ty) : 'linear-gradient(135deg, #2A2A3A, #1A1A26)';
                  if (tile.type === 'ship_ramp') bg = 'repeating-linear-gradient(45deg, #2C2C3C, #2C2C3C 4px, #383848 4px, #383848 8px)';
                  if (tile.type === 'door') bg = `radial-gradient(circle, ${zone.accentDim}88, #0D0E16)`;
                  if (tile.type === 'lava') bg = 'radial-gradient(circle at 40% 40%, #FF5500, #AA2000)';
                  if (tile.type === 'water') bg = 'radial-gradient(circle at 60% 60%, #1A4A8A, #0A1E3A)';
                  if (npcHere || collectibleHere) bg = floorBackground(zone, tx, ty);

                  return (
                    <div key={tx} style={{ width:TILE,height:TILE,position:'relative',background:bg,display:'flex',alignItems:'center',justifyContent:'center',boxShadow:isPlayer?`inset 0 0 0 1.5px ${zone.accent}`:'none',flexShrink:0 }}>
                      {tile.type === 'wall' && wallDecorFor(zone, tx, ty) && <DecorIcon kind={wallDecorFor(zone, tx, ty)} accent={zone.accent} />}
                      {tile.type === 'floor' && zone.floorPattern && <FloorTilePattern kind={zone.floorPattern} />}
                      {tile.type === 'floor' && !isPlayer && !npcHere && !collectibleHere && decorFor(zone, tx, ty, map) && <DecorIcon kind={decorFor(zone, tx, ty, map)} accent={zone.accent} />}
                      {tile.type === 'ship_hull' && !inShip && (
                        <svg viewBox="0 0 32 32" width={TILE} height={TILE} style={{ position:'absolute',inset:0,pointerEvents:'none' }}>
                          <line x1="0" y1="8" x2="32" y2="8" stroke="#FFFFFF" strokeWidth="0.4" opacity="0.12" />
                          <line x1="0" y1="16" x2="32" y2="16" stroke="#FFFFFF" strokeWidth="0.4" opacity="0.12" />
                          <line x1="0" y1="24" x2="32" y2="24" stroke="#FFFFFF" strokeWidth="0.4" opacity="0.12" />
                          <line x1="8" y1="0" x2="8" y2="32" stroke="#FFFFFF" strokeWidth="0.4" opacity="0.08" />
                          <line x1="16" y1="0" x2="16" y2="32" stroke="#FFFFFF" strokeWidth="0.4" opacity="0.08" />
                        </svg>
                      )}
                      {tile.type === 'ship_ramp' && (
                        <div style={{ fontSize:8,color:zone.accent,opacity:0.7,textAlign:'center',lineHeight:1.2 }}>▼<br/>EXIT</div>
                      )}
                      {tile.type === 'door' && (
                        <div style={{ position:'absolute',top:0,left:0,right:0,height:'3px',background:zone.accent,opacity:0.7,borderRadius:'1px 1px 0 0' }} />
                      )}
                      {doorHere && !isPlayer && (
                        <div style={{ animation:'door-pulse 2s ease-in-out infinite',fontSize:9,color:zone.accent,textAlign:'center' }}>▶<br/><span style={{fontSize:7}}>{doorHere.label}</span></div>
                      )}
                      {tile.type === 'lava' && (
                        <svg viewBox="0 0 32 32" width={TILE} height={TILE} style={{ position:'absolute',inset:0,pointerEvents:'none' }}>
                          <ellipse cx="10" cy="16" rx="6" ry="3" fill="#FF7722" opacity="0.4" style={{ animation:'door-pulse 1.8s ease-in-out infinite' }} />
                          <ellipse cx="22" cy="20" rx="5" ry="2.5" fill="#FF9944" opacity="0.35" style={{ animation:'door-pulse 2.2s ease-in-out 0.4s infinite' }} />
                        </svg>
                      )}
                      {tile.type === 'water' && (
                        <svg viewBox="0 0 32 32" width={TILE} height={TILE} style={{ position:'absolute',inset:0,pointerEvents:'none' }}>
                          <path d="M4 16 Q8 12 12 16 Q16 20 20 16 Q24 12 28 16" stroke="#4488CC" strokeWidth="1" fill="none" opacity="0.5" style={{ animation:'mist-drift 3s ease-in-out infinite' }} />
                        </svg>
                      )}
                      {worldObjHere && !worldObjHere.propArt && !isPlayer && !npcHere && !collectibleHere && (
                        <div style={{ position:'absolute',inset:0,display:'flex',alignItems:'center',justifyContent:'center',pointerEvents:'none',zIndex:2,filter:spriteFx(zone.accent,'icon') }}>
                          <WorldObjectSprite kind={worldObjHere.iconKind ?? getWorldObjIconKind(worldObjHere.id)} accent={zone.accent} id={worldObjHere.id} />
                        </div>
                      )}
                      {npcHere && !isPlayer && (
                        <React.Fragment>
                          <div style={ACTOR_SHADOW} />
                          <div style={{ position:'absolute',bottom:0,left:'50%',zIndex:5,animation:'npc-sway 4.2s ease-in-out infinite',filter:(npcDone?'grayscale(0.6) brightness(0.7) ':'') + spriteFx(zone.accent) }}>
                            <NpcPortrait kind={npcHere.kind} accent={zone.accent} />
                          </div>
                        </React.Fragment>
                      )}
                      {collectibleHere && !isPlayer && (
                        <div style={{ animation:'collectible-bob 2.3s ease-in-out infinite' }}>
                          <CollectibleIcon />
                        </div>
                      )}
                      {isPlayer && (
                        <React.Fragment>
                          <div style={{ ...ACTOR_SHADOW, zIndex:5 }} />
                          <div style={{ position:'absolute',bottom:0,left:'50%',transform:'translateX(-50%)',zIndex:6,filter:spriteFx(zone.accent) }}>
                            <PlayerMarker accent={zone.accent} facing={facing} />
                          </div>
                        </React.Fragment>
                      )}
                    </div>
                  );
                })}
              </div>
            );
          })}
          {visiblePropObjects(zone, questFlags, completedInteractions).map((wo) => {
            const d = PROP_DEFS[wo.propArt];
            return (
              <div key={'prop_' + wo.id} style={{ position:'absolute',left:(wo.x - d.ax - camX) * TILE,top:(wo.y - d.ay - camY) * TILE,width:d.w * TILE,height:d.h * TILE,zIndex:d.backdrop ? 1 : 2,pointerEvents:'none',filter:spriteFx(zone.accent,'prop') }}>
                <PropArt kind={wo.propArt} variant={wo.propVariant} active={wo.id.endsWith('_active')} />
              </div>
            );
          })}
          {zone.ships?.map((sh) => {
            const d = SHIP_DEFS[sh.kind];
            if (!d) return null;
            return (
              <div key={sh.id} style={{ position:'absolute',left:(sh.x - camX) * TILE,top:(sh.y - camY) * TILE,width:d.w * TILE,height:d.h * TILE,zIndex:3,pointerEvents:'none',filter:spriteFx(zone.accent,'ship') }}>
                <ShipSprite kind={sh.kind} accent={zone.accent} />
              </div>
            );
          })}
        </div>
        <div style={{ position:'absolute',bottom:6,right:6,zIndex:10,background:'rgba(4,4,8,0.80)',padding:4,border:`1px solid ${zone.accentDim}55` }}>
          <div style={{ fontSize:8,color:'#5A5F74',marginBottom:2 }}>minimap</div>
          <Minimap zone={zone} map={map} pos={pos} camX={camX} camY={camY} npcPositions={npcPositions} completedInteractions={completedInteractions} questFlags={questFlags} />
        </div>
      </div>

      {choiceFeedback && (
        <div style={{ position:'fixed',top:24,left:'50%',transform:'translateX(-50%)',zIndex:50,background:'rgba(4,4,8,0.92)',border:`1px solid ${zone.accentDim}`,padding:'6px 18px',fontSize:11,letterSpacing:'0.12em',color:zone.accent,pointerEvents:'none',animation:'door-pulse 0.4s ease-out' }}>
          {choiceFeedback}
        </div>
      )}

      <div style={{ display:'flex',gap:10,flexWrap:'wrap',position:'relative',zIndex:2 }}>
        <div style={{ flex:'0 0 170px',border:'1px solid #24242E',padding:10,fontSize:11 }}>
          <div style={{ color:'#5A5F74',marginBottom:8,fontSize:10 }}>navigation</div>
          <div style={{ color:'#A8ADC0',marginBottom:4 }}>WASD / arrows to move</div>
          <div style={{ color:'#6A6F84',fontSize:10,marginBottom:2 }}>walk ramp to open travel</div>
          <div style={{ color:'#6A6F84',fontSize:10,marginBottom:8 }}>walk door to change zone</div>
          <div style={{ color:'#7A7F94',borderTop:'1px solid #1C1C26',paddingTop:8,fontSize:10 }}>
            <div>zone: <span style={{color:zone.accent}}>{zone.id}</span></div>
            <div>pos: {pos.x},{pos.y}</div>
          </div>
          {suspicionMeter > 0 && (
            <div style={{ marginTop:8,borderTop:'1px solid #1C1C26',paddingTop:8 }}>
              <div style={{ fontSize:9,color:'#E8A020',marginBottom:3 }}>SUSPICION</div>
              <div style={{ height:4,background:'#1C1A12',borderRadius:2 }}>
                <div style={{ height:'100%',width:`${suspicionMeter}%`,background: suspicionMeter > 60 ? '#FF4422' : '#E8A020',borderRadius:2,transition:'width 0.6s ease' }} />
              </div>
            </div>
          )}
        </div>
        <div style={{ flex:'1 1 200px',border:'1px solid #24242E',padding:10 }}>
          <AlignmentPanel alignment={alignment} />
        </div>
        <div style={{ flex:'1 1 150px',border:'1px solid #24242E',padding:10 }}>
          <div style={{ display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8 }}>
            <div style={{ fontSize:10,color:'#5A5F74' }}>inventory</div>
            <div style={{ display:'flex',gap:6 }}>
              <div onClick={() => setShowInventory(true)} style={{ fontSize:9,padding:'2px 6px',border:'1px solid #4ACDFF44',color:'#4ACDFF',cursor:'pointer' }}>[I]</div>
              <div onClick={() => setShowCodex(true)} style={{ fontSize:9,padding:'2px 6px',border:`1px solid ${codex.some(e => e.unread)?'#E8C97A':'#44443A'}`,color:codex.some(e => e.unread)?'#E8C97A':'#6A6F84',cursor:'pointer',position:'relative' }}>
                [C]{codex.filter(e => e.unread).length > 0 && <span style={{ position:'absolute',top:-4,right:-4,background:'#E8C97A',color:'#0A0A12',fontSize:8,borderRadius:'50%',width:12,height:12,display:'flex',alignItems:'center',justifyContent:'center',fontWeight:700 }}>{codex.filter(e => e.unread).length}</span>}
              </div>
            </div>
          </div>
          {inventory.map((item, i) => (
            <div key={item.id} style={{ fontSize:11,padding:'3px 0',borderBottom:i<inventory.length-1?'1px solid #1C1C26':'none',color:'#A8ADC0',display:'flex',justifyContent:'space-between' }}>
              <span>{item.name}</span>
              {item.qty > 1 && <span style={{color:'#E8C97A',fontSize:10}}>x{item.qty}</span>}
            </div>
          ))}
        </div>
        <div style={{ flex:'2 1 260px',border:'1px solid #24242E',padding:10,maxHeight:160,overflowY:'auto' }}>
          <div style={{ fontSize:10,color:'#5A5F74',marginBottom:8 }}>action log</div>
          {actionLog.map((entry, i) => (
            <div key={i} style={{ fontSize:11,color:i===0?zone.accent:'#6A6F84',padding:'2px 0',lineHeight:1.5 }}>{entry.text}</div>
          ))}
        </div>
        <div style={{ flex:'1 1 200px',border:`1px solid ${zone.accentDim}55`,padding:10,alignSelf:'flex-start' }}>
          <div style={{ fontSize:9,color:'#5A5F74',marginBottom:6,letterSpacing:'0.08em' }}>ACTIVE OBJECTIVE</div>
          <div style={{ fontSize:11,color:zone.accent,lineHeight:1.6 }}>{currentObjective}</div>
          <div style={{ marginTop:8,fontSize:9,color:'#3A3F54' }}>world: <span style={{color: worldState==='lawful'?'#4A9FFF':worldState==='underworld'?'#FF3366':'#7A7F94'}}>{worldState}</span></div>
        </div>
      </div>

      {transitioning && (
        <div style={{ position:'absolute',inset:0,background:'rgba(4,4,8,0.75)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:30,animation:'door-pulse 0.5s ease-in-out infinite' }}>
          <div style={{ color:zone.accent,fontSize:14,letterSpacing:'0.2em' }}>...</div>
        </div>
      )}
      {showTravel && <TravelOverlay currentPlanetId={planetId} credits={credits} questFlags={questFlags} onTravel={travelToPlanet} onClose={() => setShowTravel(false)} />}
      {activeDialogue && <DialogueOverlay npc={activeDialogue} onChoose={resolveChoice} inventory={inventory} questFlags={questFlags} credits={credits} />}
      {shuttlePilot && <ShuttleOverlay pilot={shuttlePilot} questFlags={questFlags} currentZoneId={zoneId} onTravel={(dest) => { setCredits((c) => c - dest.cost); const mode = shuttleModeFor(zoneId, dest); setShuttlePilot(null); if (mode) setCinematic({ mode, dest }); else travelToZone(dest.targetZone, dest.targetPos); }} onClose={() => setShuttlePilot(null)} />}
      {cinematic && <ShuttleCinematic mode={cinematic.mode} destName={cinematic.dest.name} onSwap={() => arriveShuttle(cinematic.dest)} onDone={() => setCinematic(null)} />}
      {showSpeeder && <SpeederOverlay credits={credits} questFlags={questFlags} currentZoneId={zoneId} onTravel={(dest) => { setCredits((c) => c - dest.cost); setShowSpeeder(false); travelToZone(dest.targetZone, dest.targetPos); }} onClose={() => setShowSpeeder(false)} />}
      {showInventory && <InventoryOverlay inventory={inventory} onClose={() => setShowInventory(false)} />}
      {showCodex && <CodexOverlay codex={codex} setCodex={setCodex} onClose={() => setShowCodex(false)} />}
      {showVendor && activeVendorNpc && (
        <VendorOverlay
          npc={activeVendorNpc}
          inventory={inventory}
          credits={credits}
          alignment={alignment}
          flags={questFlags}
          zoneId={zoneId}
          onBuy={(item, price) => { setCredits(c => c - price); addItem(item); }}
          onSell={(item, price) => { setCredits(c => c + price); setInventory(prev => { const idx = prev.findIndex(i => i.id === item.id); if (idx === -1) return prev; const updated = [...prev]; if (updated[idx].qty > 1) { updated[idx] = { ...updated[idx], qty: updated[idx].qty - 1 }; } else { updated.splice(idx, 1); } return updated; }); }}
          onClose={() => { setShowVendor(false); setActiveVendorNpc(null); }}
        />
      )}
      {activeMinigame && activeMinigame.type === 'signal_siphon' && <SignalSiphonOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'speeder_pursuit' && <SpeederPursuitOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'valve_override' && <ValveOverrideOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'willpower_override' && <WillpowerOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'pit_fight' && <PitFightOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} opponentName={activeMinigame.opponentName ?? 'Opponent'} opponentHp={activeMinigame.opponentHp ?? 5} accent={activeMinigame.accent ?? '#C03030'} />}
      {activeMinigame && activeMinigame.type === 'syndicate_management' && <SyndicateManagementOverlay onClose={activeMinigame.onSuccess} roster={syndicateRoster} setRoster={setSyndicateRoster} contracts={syndicateContracts} activeContracts={syndicateActiveContracts} setActiveContracts={setSyndicateActiveContracts} heat={syndicateHeat} setHeat={setSyndicateHeat} territories={syndicateTerritories} setTerritories={setSyndicateTerritories} credits={credits} setCredits={setCredits} incomeBonus={phPerks.income} heatDecayBonus={phPerks.decay} />}
      {activeMinigame && activeMinigame.type === 'penthouse_design' && <PenthouseDesignOverlay inventory={inventory} flags={questFlags} onInstall={installPenthousePackage} onRemove={removePenthousePackage} onClose={activeMinigame.onSuccess} />}
      {activeMinigame && activeMinigame.type === 'sabacc' && <SabaccOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'contraband_market' && <ContrabandMarketOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} heat={syndicateHeat} setHeat={setSyndicateHeat} credits={credits} setCredits={setCredits} />}
      {activeMinigame && activeMinigame.type === 'interrogation' && <InterrogationMatrixOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'droid_arena' && <DroidArenaOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} credits={credits} setCredits={setCredits} />}
      {activeMinigame && activeMinigame.type === 'arms_bench' && <ArmsBenchOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} credits={credits} setCredits={setCredits} />}
      {activeMinigame && activeMinigame.type === 'shakedown' && <ProtectionShakedownOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'sky_evasion' && <SkyLaneEvasionOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} />}
      {activeMinigame && activeMinigame.type === 'tactical_combat' && <TacticalGridCombatOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} opponentProfile={activeMinigame.opponentProfile ?? 'syndicate_thug'} flavorText={activeMinigame.flavorText} />}
      {activeMinigame && activeMinigame.type === 'terminal_slicing' && <TerminalSlicingOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} difficulty={activeMinigame.difficulty ?? 4} />}
      {activeMinigame && activeMinigame.type === 'coruscant_conquest' && <CoruscantConquestOverlay onSuccess={activeMinigame.onSuccess} onFailure={activeMinigame.onFailure} startCredits={activeMinigame.startCredits} />}
      {showDebug && (
        <div style={{ position:'fixed',top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.82)',zIndex:200,display:'flex',alignItems:'center',justifyContent:'center',fontFamily:'monospace' }}>
          <div style={{ background:'#0A0A12',border:'1px solid #4ACDFF44',padding:24,minWidth:480,maxWidth:620 }}>
            <div style={{ color:'#4ACDFF',fontSize:13,letterSpacing:'0.15em',marginBottom:4 }}>DEBUG STAGE SELECT</div>
            <div style={{ color:'#3A3F54',fontSize:10,marginBottom:16 }}>Press a number to jump to that checkpoint. Any other key closes this panel.</div>
            {DEBUG_STAGES.map(s => (
              <div key={s.key} style={{ display:'flex',gap:12,alignItems:'baseline',padding:'4px 0',borderBottom:'1px solid #14141E' }}>
                <div style={{ color:'#E8C97A',fontSize:13,width:16,textAlign:'right',flexShrink:0 }}>[{s.key}]</div>
                <div style={{ color:'#C8CDD8',fontSize:11,flex:1 }}>{s.label}</div>
                <div style={{ color:'#3A3F54',fontSize:10 }}>{s.zone}</div>
                <div style={{ color:'#5A9F6A',fontSize:10 }}>{s.credits}cr</div>
              </div>
            ))}
            <div style={{ marginTop:16,display:'flex',gap:24 }}>
              <div style={{ fontSize:10,color:'#5A5F74' }}>[Shift+C] +5000 credits</div>
              <div style={{ fontSize:10,color:'#5A5F74' }}>[Shift+Z] reset all flags</div>
              <div style={{ fontSize:10,color:'#5A5F74' }}>[` ] close panel</div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<StarWarsRPG />);
