/**
 * Galaxy Strike Online — Starbase World  v3
 * Isometric Habbo-style walkable environment.
 *
 * Features:
 *  - JSON-driven grid size, walls, doors, rooms
 *  - Per-segment wall colour overrides
 *  - Animated sliding doors (open when player is adjacent)
 *  - Locked rooms (filled with sealed tiles, show LOCKED badge)
 *    → unlock via room_unlock decoration items from shop/dungeons/quests
 *  - Wallpapers: global or per-room, purchasable/earnable
 *    → active wallpaper overrides style.floorColor* + style.wall* + style.door*
 *  - Wallpaper selector panel (bottom toolbar)
 *  - Click-to-walk BFS pathfinding
 *  - Pixel-art astronaut with walk animation
 */

const TILE = { VOID: 0, FLOOR: 1, WALL: 2, DOOR: 3, LOCKED: 4 };

class StarbaseWorld {
    constructor(canvasId, layoutJson, starbasePlayerData, decorationCatalog) {
        this.canvas  = document.getElementById(canvasId);
        this.ctx     = this.canvas.getContext('2d');

        this.TW = 64;
        this.TH = 32;

        this.layout = layoutJson || StarbaseWorld.defaultLayout();
        this.COLS   = this.layout.grid.cols;
        this.ROWS   = this.layout.grid.rows;

        // Player's starbase save data { wallpaper, ownedWallpapers, unlockedRooms, roomWallpapers }
        this.sbData  = starbasePlayerData || { wallpaper: null, ownedWallpapers: [], unlockedRooms: [] };
        // Full decoration catalog from server
        this.catalog = decorationCatalog  || [];

        this.originX = 0;
        this.originY = 0;

        const ps = this.layout.playerStart || { col: Math.floor(this.COLS/2), row: 2 };
        this.player = {
            col: ps.col, row: ps.row, x: 0, y: 0,
            path: [], moving: false, frame: 0, frameTimer: 0,
            dir: 'se', name: 'Commander'
        };

        this.tileMap = this._buildMapFromLayout();

        this.doorStates = {};
        this.doorDefs   = {};
        for (const d of (this.layout.doors || [])) {
            const k = `${d.col},${d.row}`;
            this.doorStates[k] = { open: false, anim: 0 };
            this.doorDefs[k]   = d;
        }

        this.wallDefs = {};
        for (const w of (this.layout.walls || [])) {
            for (let i = 0; i < (w.span || 1); i++) {
                const c = w.dir === 'h' ? w.col + i : w.col;
                const r = w.dir === 'v' ? w.row + i : w.row;
                this.wallDefs[`${c},${r}`] = w;
            }
        }

        // Which room each floor tile belongs to (for per-room wallpaper)
        this.tileRoom = this._buildRoomMap();

        this.objects = this._buildObjects();

        this.hoveredTile      = null;
        this._pendingInteract = null;
        this._stars           = null;
        this.lastTime = 0;
        this.running  = false;
        this.animId   = null;

        // Wallpaper UI state
        this._wpPanelOpen = false;
        this._wpSelected  = null;   // wallpaperId being previewed
        this._wpRoomTarget= null;   // roomId to apply to (null = global)

        this._recenterCamera();
        this._bindInput();
        this._snapPlayerPos();
        this._buildWallpaperPanel();
    }

    // ─── Default layout ──────────────────────────────────────────────────────
    static defaultLayout() {
        return {
            name: 'Starbase Alpha-7', grid: { cols: 20, rows: 16 },
            style: {
                wallColor: '#00d4ff', wallColorLeft: '#0c1626',
                wallColorRight: '#0a1220', wallColorTop: '#1a2840',
                floorColorEven: '#151c2e', floorColorOdd: '#111827',
                doorColor: '#00ffcc', doorFrameColor: '#00d4ff'
            },
            walls: [
                { col:0,  row:0,  span:20, dir:'h' }, { col:0,  row:15, span:20, dir:'h' },
                { col:0,  row:0,  span:16, dir:'v' }, { col:19, row:0,  span:16, dir:'v' },
            ],
            doors: [], rooms: [], playerStart: { col:10, row:2 }
        };
    }

    // ─── Map builder ─────────────────────────────────────────────────────────
    _buildMapFromLayout() {
        const C = this.COLS, R = this.ROWS;
        const map = Array.from({ length: R }, () => Array(C).fill(TILE.FLOOR));

        for (const w of (this.layout.walls || [])) {
            for (let i = 0; i < (w.span || 1); i++) {
                const c = w.dir === 'h' ? w.col + i : w.col;
                const r = w.dir === 'v' ? w.row + i : w.row;
                if (r >= 0 && r < R && c >= 0 && c < C) map[r][c] = TILE.WALL;
            }
        }
        for (const d of (this.layout.doors || [])) {
            if (d.row >= 0 && d.row < R && d.col >= 0 && d.col < C)
                map[d.row][d.col] = TILE.DOOR;
        }

        // Seal locked rooms
        for (const room of (this.layout.rooms || [])) {
            if (!room.unlock) continue;
            const owned = this.sbData.unlockedRooms || [];
            if (owned.includes(room.id)) continue;  // player has unlocked it

            const b = room.bounds;
            for (let r = b.row; r < b.row + b.rows; r++) {
                for (let c = b.col; c < b.col + b.cols; c++) {
                    if (r >= 0 && r < R && c >= 0 && c < C && map[r][c] === TILE.FLOOR)
                        map[r][c] = TILE.LOCKED;
                }
            }
        }
        return map;
    }

    _buildRoomMap() {
        const C = this.COLS, R = this.ROWS;
        const rm = Array.from({ length: R }, () => Array(C).fill(null));
        for (const room of (this.layout.rooms || [])) {
            const b = room.bounds;
            for (let r = b.row; r < b.row + b.rows; r++)
                for (let c = b.col; c < b.col + b.cols; c++)
                    if (r >= 0 && r < R && c >= 0 && c < C) rm[r][c] = room.id;
        }
        return rm;
    }

    // ─── Style resolver ───────────────────────────────────────────────────────
    /**
     * Returns the effective style for a given tile position.
     * Priority: per-room wallpaper → global wallpaper → layout.style defaults
     */
    _styleFor(col, row) {
        const base = this.layout.style || {};
        const globalWpId   = this.sbData.wallpaper;
        const roomId       = this.tileRoom[row]?.[col];
        const roomWpId     = roomId && this.sbData.roomWallpapers?.[roomId];
        const activeWpId   = this._wpSelected || roomWpId || globalWpId;

        if (!activeWpId) return base;
        const wp = this.catalog.find(i => i.id === activeWpId && i.subtype === 'wallpaper');
        if (!wp?.preview) return base;

        // Merge: preview overrides base, but let per-wall-segment defs still win later
        return { ...base, ...wp.preview };
    }

    // ─── Interactive objects ──────────────────────────────────────────────────
    _buildObjects() {
        const sc = c => Math.min(c, this.COLS - 2);
        const sr = r => Math.min(r, this.ROWS - 2);
        const mk = (id, col, row, icon, label, color, glow, action) =>
            ({ id, col: sc(col), row: sr(row), icon, label, color, glowColor: glow, action });
        const modal = (title, icon, desc, color, btn) =>
            () => this._showInteractModal(title, icon, desc, color, btn ? [btn] : []);

        return [
            mk('shop',     3,  2,  '🛒','Trade Post',       '#00d4ff','rgba(0,212,255,.6)',
                modal('Trade Post','🛒','Access the galactic marketplace to buy ships, weapons, armour, wallpapers, and room unlocks.','#00d4ff',{label:'Open Shop',action:'shop'})),
            mk('crafting', 21, 2,  '⚗️','Forge',            '#ff8800','rgba(255,136,0,.6)',
                modal('Forge','⚗️','Smelt ores, combine alloys, build circuits and hull sections.','#ff8800',{label:'Open Crafting',action:'crafting'})),
            mk('quests',   3,  16, '📋','Mission Board',    '#ffdd00','rgba(255,221,0,.6)',
                modal('Mission Board','📋','Daily missions, weekly challenges, and story campaign.','#ffdd00',{label:'View Quests',action:'quests'})),
            mk('hangar',   21, 16, '🚀','Hangar',           '#aa44ff','rgba(170,68,255,.6)',
                modal('Hangar','🚀','Equip your active ship and prepare for dungeon runs.','#aa44ff',{label:'View Ships',action:'ships'})),
            mk('skills',   12, 3,  '🎓','Academy',          '#00ff88','rgba(0,255,136,.6)',
                modal('Academy','🎓','Improve combat, science, and crafting proficiencies.','#00ff88',{label:'Train Skills',action:'skills'})),
            mk('terminal', 12, 10, '💻','Command Terminal', '#ff00ff','rgba(255,0,255,.6)',
                modal('Command Terminal','💻','Monitor stats, server status, and your empire.','#ff00ff',{label:'Base Overview',action:'base'})),
            mk('wallpaper',12, 17, '🎨','Wallpaper Desk',   '#ffaa00','rgba(255,170,0,.6)',
                () => this._toggleWallpaperPanel()),
        ];
    }

    // ─── Coordinate helpers ───────────────────────────────────────────────────
    _isoProject(col, row) {
        return {
            x: this.originX + (col - row) * (this.TW / 2),
            y: this.originY + (col + row) * (this.TH / 2)
        };
    }
    _screenToGrid(sx, sy) {
        const tx = sx - this.originX, ty = sy - this.originY;
        return {
            col: Math.floor((tx / (this.TW/2) + ty / (this.TH/2)) / 2),
            row: Math.floor((ty / (this.TH/2) - tx / (this.TW/2)) / 2)
        };
    }
    _recenterCamera() {
        this.originX = this.canvas.width  / 2;
        this.originY = this.canvas.height * 0.13;
    }
    _snapPlayerPos() {
        const iso = this._isoProject(this.player.col + 0.5, this.player.row + 0.5);
        this.player.x = iso.x;
        this.player.y = iso.y;
    }

    // ─── Pathfinding ──────────────────────────────────────────────────────────
    _walkable(col, row) {
        if (col < 0 || row < 0 || col >= this.COLS || row >= this.ROWS) return false;
        const t = this.tileMap[row][col];
        return t === TILE.FLOOR || t === TILE.DOOR;
    }
    _findPath(sc, sr, tc, tr) {
        if (!this._walkable(tc, tr)) return [];
        const key = (c, r) => `${c},${r}`;
        const q = [{ c: sc, r: sr, path: [] }];
        const vis = new Set([key(sc, sr)]);
        const dirs = [[1,0],[-1,0],[0,1],[0,-1]];
        while (q.length) {
            const { c, r, path } = q.shift();
            if (c === tc && r === tr) return path;
            for (const [dc, dr] of dirs) {
                const nc = c+dc, nr = r+dr, k = key(nc, nr);
                if (!vis.has(k) && this._walkable(nc, nr)) {
                    vis.add(k);
                    q.push({ c: nc, r: nr, path: [...path, { col: nc, row: nr }] });
                }
            }
        }
        return [];
    }
    _findAdjacentWalkable(col, row) {
        for (const [dc, dr] of [[0,-1],[0,1],[-1,0],[1,0]]) {
            const nc = col+dc, nr = row+dr;
            if (this._walkable(nc, nr)) return { col: nc, row: nr };
        }
        return null;
    }

    // ─── Input ────────────────────────────────────────────────────────────────
    _bindInput() {
        this.canvas.addEventListener('click',     e => this._onClick(e));
        this.canvas.addEventListener('mousemove', e => this._onMouseMove(e));
        this.canvas.addEventListener('mouseleave',() => { this.hoveredTile = null; });
    }
    _canvasPos(e) {
        const r = this.canvas.getBoundingClientRect();
        return {
            x: (e.clientX - r.left) * (this.canvas.width  / r.width),
            y: (e.clientY - r.top)  * (this.canvas.height / r.height)
        };
    }
    _onClick(e) {
        const { x, y } = this._canvasPos(e);
        const { col, row } = this._screenToGrid(x, y);
        if (col < 0 || row < 0 || col >= this.COLS || row >= this.ROWS) return;

        // Clicking a locked tile → show unlock info
        if (this.tileMap[row][col] === TILE.LOCKED) {
            const roomId = this.tileRoom[row]?.[col];
            const room   = this.layout.rooms?.find(rm => rm.id === roomId);
            if (room) this._showLockedRoomModal(room);
            return;
        }

        const obj = this.objects.find(o => o.col === col && o.row === row);
        if (obj) {
            const adj = this._findAdjacentWalkable(obj.col, obj.row);
            if (adj) {
                this.player.path   = this._findPath(this.player.col, this.player.row, adj.col, adj.row);
                this.player.moving = this.player.path.length > 0;
                this._pendingInteract = obj;
            } else { obj.action(); }
            return;
        }

        this.player.path   = this._findPath(this.player.col, this.player.row, col, row);
        this.player.moving = this.player.path.length > 0;
        this._pendingInteract = null;
    }
    _onMouseMove(e) {
        const { x, y } = this._canvasPos(e);
        const { col, row } = this._screenToGrid(x, y);
        this.hoveredTile = (col>=0&&row>=0&&col<this.COLS&&row<this.ROWS) ? {col,row} : null;
    }

    // ─── Game loop ────────────────────────────────────────────────────────────
    start() {
        if (this.running) return;
        this.running  = true;
        this.lastTime = performance.now();
        const loop = ts => {
            if (!this.running) return;
            const dt = Math.min((ts - this.lastTime) / 1000, 0.1);
            this.lastTime = ts;
            this._update(dt);
            this._render();
            this.animId = requestAnimationFrame(loop);
        };
        this.animId = requestAnimationFrame(loop);
    }
    stop() {
        this.running = false;
        if (this.animId) cancelAnimationFrame(this.animId);
    }
    resize(w, h) {
        this.canvas.width  = w;
        this.canvas.height = h;
        this._recenterCamera();
        this._snapPlayerPos();
        this._stars = null;
    }

    // ─── Update ───────────────────────────────────────────────────────────────
    _update(dt) {
        this._updatePlayer(dt);
        this._updateDoors(dt);
    }
    _updatePlayer(dt) {
        const p = this.player;
        if (!p.moving || !p.path.length) return;
        const SPEED = 4.5;
        const next  = p.path[0];
        const tPos  = this._isoProject(next.col + 0.5, next.row + 0.5);
        const dx = tPos.x - p.x, dy = tPos.y - p.y;
        const dist = Math.sqrt(dx*dx + dy*dy);
        const step = SPEED * this.TW * dt;

        const dC = next.col - p.col, dR = next.row - p.row;
        if      (dC>0&&dR===0) p.dir='se';
        else if (dC<0&&dR===0) p.dir='nw';
        else if (dR>0&&dC===0) p.dir='sw';
        else if (dR<0&&dC===0) p.dir='ne';

        if (dist <= step) {
            p.x = tPos.x; p.y = tPos.y;
            p.col = next.col; p.row = next.row;
            p.path.shift();
            if (!p.path.length) {
                p.moving = false;
                if (this._pendingInteract) {
                    const obj = this._pendingInteract;
                    this._pendingInteract = null;
                    setTimeout(() => obj.action(), 100);
                }
            }
        } else {
            p.x += (dx/dist)*step;
            p.y += (dy/dist)*step;
        }
        p.frameTimer += dt;
        if (p.frameTimer > 0.15) { p.frame = (p.frame+1)%4; p.frameTimer = 0; }
    }
    _updateDoors(dt) {
        const p = this.player;
        const SPEED = 3.5;
        for (const key of Object.keys(this.doorStates)) {
            const [dc, dr] = key.split(',').map(Number);
            const state = this.doorStates[key];
            state.open  = (Math.abs(p.col - dc) + Math.abs(p.row - dr)) <= 1;
            const target = state.open ? 1 : 0;
            if (state.anim < target)      state.anim = Math.min(state.anim + SPEED*dt, 1);
            else if (state.anim > target) state.anim = Math.max(state.anim - SPEED*dt, 0);
        }
    }

    // ─── Render ───────────────────────────────────────────────────────────────
    _render() {
        const ctx = this.ctx;
        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
        ctx.fillStyle = '#07091a';
        ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
        this._drawStarfield();

        for (let r = 0; r < this.ROWS; r++) {
            for (let c = 0; c < this.COLS; c++) {
                const tile = this.tileMap[r][c];
                const iso  = this._isoProject(c, r);
                const hov  = this.hoveredTile?.col===c && this.hoveredTile?.row===r;
                const st   = this._styleFor(c, r);

                if      (tile === TILE.FLOOR)  this._drawFloorTile(iso.x, iso.y, c, r, hov, st);
                else if (tile === TILE.WALL) {
                    this._drawFloorTile(iso.x, iso.y, c, r, false, st);
                    this._drawWallTile(iso.x, iso.y, this.wallDefs[`${c},${r}`], st);
                }
                else if (tile === TILE.DOOR) {
                    this._drawFloorTile(iso.x, iso.y, c, r, hov, st);
                    this._drawDoorTile(iso.x, iso.y, c, r, this.doorDefs[`${c},${r}`], st);
                }
                else if (tile === TILE.LOCKED) this._drawLockedTile(iso.x, iso.y, c, r, hov);
            }
        }

        this._drawRoomLabels();

        const sorted = [...this.objects].sort((a,b) => (a.col+a.row)-(b.col+b.row));
        for (const obj of sorted) {
            const iso = this._isoProject(obj.col+0.5, obj.row+0.5);
            this._drawObject(obj, iso.x, iso.y);
        }

        this._drawPlayer();
        this._drawHUD();
    }

    // ─── Tile drawers ─────────────────────────────────────────────────────────
    _drawStarfield() {
        const ctx = this.ctx;
        if (!this._stars) {
            this._stars = [];
            const rng = mulberry32(0xdeadbeef);
            for (let i=0; i<120; i++) this._stars.push({
                x: rng()*this.canvas.width, y: rng()*(this.canvas.height*0.25),
                r: rng()*1.5+0.3, a: rng()*0.6+0.2
            });
        }
        for (const s of this._stars) {
            ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI*2);
            ctx.fillStyle = `rgba(180,210,255,${s.a})`; ctx.fill();
        }
    }

    _drawFloorTile(x, y, col, row, hovered, st) {
        const ctx = this.ctx, tw = this.TW, th = this.TH;
        const even = (col+row)%2===0;
        const base = even ? (st.floorColorEven||'#151c2e') : (st.floorColorOdd||'#111827');
        const fill = hovered ? this._lighten(base, 0.35) : base;

        ctx.beginPath();
        ctx.moveTo(x, y); ctx.lineTo(x+tw/2, y+th/2);
        ctx.lineTo(x, y+th); ctx.lineTo(x-tw/2, y+th/2);
        ctx.closePath();
        ctx.fillStyle   = fill; ctx.fill();
        ctx.strokeStyle = hovered ? 'rgba(0,212,255,.5)' : 'rgba(0,212,255,.08)';
        ctx.lineWidth   = hovered ? 1.5 : 0.8; ctx.stroke();
    }

    _drawWallTile(x, y, def, st) {
        const ctx = this.ctx, tw = this.TW, th = this.TH, h = th*1.8;
        const accent = (def?.color)      || st.wallColor      || '#00d4ff';
        const left   = (def?.colorLeft)  || st.wallColorLeft  || '#0c1626';
        const right  = (def?.colorRight) || st.wallColorRight || '#0a1220';
        const top    = (def?.colorTop)   || st.wallColorTop   || '#1a2840';

        ctx.beginPath();
        ctx.moveTo(x-tw/2, y+th/2  ); ctx.lineTo(x,      y+th    );
        ctx.lineTo(x,      y+th+h  ); ctx.lineTo(x-tw/2, y+th/2+h);
        ctx.closePath(); ctx.fillStyle=left; ctx.fill();
        ctx.strokeStyle=this._alpha(accent,.15); ctx.lineWidth=0.8; ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(x,      y+th    ); ctx.lineTo(x+tw/2, y+th/2  );
        ctx.lineTo(x+tw/2, y+th/2+h); ctx.lineTo(x,      y+th+h  );
        ctx.closePath(); ctx.fillStyle=right; ctx.fill();
        ctx.strokeStyle=this._alpha(accent,.10); ctx.stroke();

        ctx.beginPath();
        ctx.moveTo(x,      y      ); ctx.lineTo(x+tw/2, y+th/2);
        ctx.lineTo(x,      y+th   ); ctx.lineTo(x-tw/2, y+th/2);
        ctx.closePath(); ctx.fillStyle=top; ctx.fill();
        ctx.strokeStyle=this._alpha(accent,.35); ctx.lineWidth=1; ctx.stroke();
    }

    _drawDoorTile(x, y, col, row, def, st) {
        const ctx = this.ctx, tw = this.TW, th = this.TH, h = th*1.8;
        const key  = `${col},${row}`;
        const anim = (this.doorStates[key]||{anim:0}).anim;
        const fc   = (def?.frameColor) || st.doorFrameColor || '#00d4ff';
        const dc   = (def?.color)      || st.doorColor      || '#00ffcc';
        const fw   = tw*0.12;
        const slide= anim*h*0.93;

        // Left pillar
        ctx.beginPath();
        ctx.moveTo(x-tw/2,    y+th/2        ); ctx.lineTo(x-tw/2+fw, y+th/2+fw*.5  );
        ctx.lineTo(x-tw/2+fw, y+th/2+h+fw*.5); ctx.lineTo(x-tw/2,    y+th/2+h      );
        ctx.closePath(); ctx.fillStyle=this._darken(fc,.25); ctx.fill();
        ctx.strokeStyle=this._alpha(fc,.6); ctx.lineWidth=0.8; ctx.stroke();

        // Right pillar
        ctx.beginPath();
        ctx.moveTo(x+tw/2,    y+th/2        ); ctx.lineTo(x+tw/2-fw, y+th/2+fw*.5  );
        ctx.lineTo(x+tw/2-fw, y+th/2+h+fw*.5); ctx.lineTo(x+tw/2,    y+th/2+h      );
        ctx.closePath(); ctx.fillStyle=this._darken(fc,.3); ctx.fill();
        ctx.strokeStyle=this._alpha(fc,.5); ctx.stroke();

        // Top lintel
        ctx.beginPath();
        ctx.moveTo(x-tw/2+fw, y+th/2+fw*.5 ); ctx.lineTo(x+tw/2-fw, y+th/2+fw*.5 );
        ctx.lineTo(x+tw/2-fw, y+th/2+fw*1.5); ctx.lineTo(x-tw/2+fw, y+th/2+fw*1.5);
        ctx.closePath(); ctx.fillStyle=this._darken(fc,.2); ctx.fill();
        ctx.strokeStyle=this._alpha(fc,.6); ctx.stroke();

        // Sliding panel
        const pTop    = y+th/2+fw*1.5 - slide;
        const pBottom = y+th/2+h;
        const pw      = tw - fw*2;
        if (pBottom - pTop > 1) {
            ctx.save();
            ctx.beginPath(); ctx.rect(x-tw/2, y+th/2+fw*1.5-h, tw, h*2); ctx.clip();
            ctx.beginPath();
            ctx.moveTo(x-pw/2, pTop           ); ctx.lineTo(x+pw/2, pTop  -th*.25);
            ctx.lineTo(x+pw/2, pBottom-th*.25  ); ctx.lineTo(x-pw/2, pBottom      );
            ctx.closePath();
            const pg = ctx.createLinearGradient(x, pTop, x, pBottom);
            pg.addColorStop(0, this._alpha(dc,.9)); pg.addColorStop(1, this._alpha(dc,.35));
            ctx.fillStyle=pg; ctx.fill();
            ctx.strokeStyle=this._alpha(dc,.85); ctx.lineWidth=1; ctx.stroke();
            ctx.strokeStyle=this._alpha(dc,.18); ctx.lineWidth=0.5;
            for (let ly=pTop+5; ly<pBottom; ly+=5) {
                ctx.beginPath(); ctx.moveTo(x-pw/2, ly); ctx.lineTo(x+pw/2, ly-th*.25); ctx.stroke();
            }
            if (anim>0.05) {
                ctx.fillStyle=this._alpha(dc, anim*.7);
                ctx.beginPath(); ctx.ellipse(x, pTop, pw/2, 4, 0, 0, Math.PI*2); ctx.fill();
            }
            ctx.restore();
        }

        const pulse = anim<0.1 ? 0.25+Math.sin(performance.now()/400)*.12 : anim*.4;
        const fg = ctx.createRadialGradient(x, y+th, 0, x, y+th, 18);
        fg.addColorStop(0, this._alpha(dc, pulse)); fg.addColorStop(1,'transparent');
        ctx.fillStyle=fg;
        ctx.beginPath(); ctx.ellipse(x, y+th, 18, 9, 0, 0, Math.PI*2); ctx.fill();
    }

    _drawLockedTile(x, y, col, row, hovered) {
        const ctx = this.ctx, tw = this.TW, th = this.TH, h = th*1.8;
        const now = performance.now()/1000;
        const pulse = 0.3 + Math.sin(now*1.5)*0.1;

        // Sealed wall face — dark hatched
        const fill = hovered ? '#1a1a2a' : '#0e0e1a';
        ctx.beginPath();
        ctx.moveTo(x, y); ctx.lineTo(x+tw/2, y+th/2);
        ctx.lineTo(x, y+th); ctx.lineTo(x-tw/2, y+th/2);
        ctx.closePath(); ctx.fillStyle=fill; ctx.fill();

        // Hatch pattern
        ctx.save();
        ctx.beginPath();
        ctx.moveTo(x, y); ctx.lineTo(x+tw/2, y+th/2);
        ctx.lineTo(x, y+th); ctx.lineTo(x-tw/2, y+th/2);
        ctx.closePath(); ctx.clip();
        ctx.strokeStyle=`rgba(80,80,120,${0.15+pulse*0.1})`; ctx.lineWidth=1;
        for (let i=-tw; i<tw*2; i+=8) {
            ctx.beginPath(); ctx.moveTo(x-tw/2+i, y); ctx.lineTo(x-tw/2+i+tw, y+th); ctx.stroke();
        }
        ctx.restore();

        // Lock icon in centre
        ctx.font='14px serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
        ctx.fillStyle=`rgba(100,100,160,${pulse})`;
        ctx.fillText('🔒', x, y+th/2);

        // Glowing border
        ctx.beginPath();
        ctx.moveTo(x, y); ctx.lineTo(x+tw/2, y+th/2);
        ctx.lineTo(x, y+th); ctx.lineTo(x-tw/2, y+th/2);
        ctx.closePath();
        ctx.strokeStyle=`rgba(80,80,200,${pulse})`; ctx.lineWidth=1.5; ctx.stroke();
    }

    _drawRoomLabels() {
        if (!this.layout.rooms?.length) return;
        const ctx = this.ctx;
        for (const room of this.layout.rooms) {
            const b   = room.bounds;
            const iso = this._isoProject(b.col + b.cols/2, b.row + b.rows/2 + 0.5);
            const locked = room.unlock && !(this.sbData.unlockedRooms||[]).includes(room.id);

            ctx.save();
            ctx.font = '700 9px "Orbitron",monospace';
            ctx.textAlign = 'center'; ctx.textBaseline = 'middle';

            if (locked) {
                ctx.fillStyle = 'rgba(80,80,160,.55)';
                ctx.fillText('[ LOCKED ]', iso.x+1, iso.y+1);
                ctx.fillStyle = 'rgba(120,120,220,.45)';
                ctx.fillText('[ LOCKED ]', iso.x, iso.y);
            } else {
                ctx.fillStyle = 'rgba(0,0,0,.55)';
                ctx.fillText(room.label.toUpperCase(), iso.x+1, iso.y+1);
                ctx.fillStyle = 'rgba(180,210,255,.22)';
                ctx.fillText(room.label.toUpperCase(), iso.x, iso.y);
            }
            ctx.restore();
        }
    }

    // ─── Object drawing ───────────────────────────────────────────────────────
    _drawObject(obj, cx, cy) {
        const ctx = this.ctx, now = performance.now()/1000;
        const bob = Math.sin(now*2+obj.col)*3;
        const cy2 = cy - 28 - bob;

        const grad = ctx.createRadialGradient(cx, cy+this.TH/2, 0, cx, cy+this.TH/2, 30);
        grad.addColorStop(0, obj.glowColor.replace('.6','.35'));
        grad.addColorStop(1,'transparent');
        ctx.fillStyle=grad;
        ctx.beginPath(); ctx.ellipse(cx, cy+this.TH/2, 28, 14, 0, 0, Math.PI*2); ctx.fill();

        this._drawIsoBox(cx, cy, 36, 36, 20, obj.color);

        ctx.font='22px serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
        ctx.fillText(obj.icon, cx, cy2);
        ctx.font='600 9px "Orbitron",monospace'; ctx.textAlign='center'; ctx.textBaseline='top';
        ctx.fillStyle='rgba(0,0,0,.8)'; ctx.fillText(obj.label, cx+1, cy2+20);
        ctx.fillStyle=obj.color;         ctx.fillText(obj.label, cx,   cy2+19);
    }

    _drawIsoBox(cx, cy, w, d, h, color) {
        const ctx=this.ctx, hw=w/2, hd=d/4;
        ctx.beginPath();
        ctx.moveTo(cx, cy-h); ctx.lineTo(cx+hw, cy-h+hd); ctx.lineTo(cx, cy-h+hd*2); ctx.lineTo(cx-hw, cy-h+hd);
        ctx.closePath(); ctx.fillStyle=color; ctx.globalAlpha=0.9; ctx.fill(); ctx.globalAlpha=1;

        ctx.beginPath();
        ctx.moveTo(cx-hw, cy-h+hd); ctx.lineTo(cx, cy-h+hd*2); ctx.lineTo(cx, cy+hd*2); ctx.lineTo(cx-hw, cy+hd);
        ctx.closePath(); ctx.fillStyle=this._darken(color,.4); ctx.fill();

        ctx.beginPath();
        ctx.moveTo(cx+hw, cy-h+hd); ctx.lineTo(cx, cy-h+hd*2); ctx.lineTo(cx, cy+hd*2); ctx.lineTo(cx+hw, cy+hd);
        ctx.closePath(); ctx.fillStyle=this._darken(color,.25); ctx.fill();

        ctx.strokeStyle=color; ctx.lineWidth=0.8; ctx.globalAlpha=0.5;
        ctx.beginPath();
        ctx.moveTo(cx, cy-h); ctx.lineTo(cx+hw, cy-h+hd); ctx.lineTo(cx+hw, cy+hd);
        ctx.lineTo(cx, cy+hd*2); ctx.lineTo(cx-hw, cy+hd); ctx.lineTo(cx-hw, cy-h+hd);
        ctx.closePath(); ctx.stroke(); ctx.globalAlpha=1;
    }

    // ─── Player drawing ───────────────────────────────────────────────────────
    _drawPlayer() {
        const ctx=this.ctx, p=this.player, cx=p.x, cy=p.y, now=performance.now()/1000;
        ctx.fillStyle='rgba(0,0,0,.4)';
        ctx.beginPath(); ctx.ellipse(cx,cy+2,10,5,0,0,Math.PI*2); ctx.fill();
        const bobY=p.moving?Math.sin(now*12)*2:0, bodyY=cy-28+bobY;
        const legSw=p.moving?Math.sin(now*10)*5:0, armSw=p.moving?Math.sin(now*10)*6:0;
        ctx.strokeStyle='#00d4ff'; ctx.lineWidth=3; ctx.lineCap='round';
        ctx.beginPath(); ctx.moveTo(cx-3,bodyY+18); ctx.lineTo(cx-3+legSw,bodyY+26); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(cx+3,bodyY+18); ctx.lineTo(cx+3-legSw,bodyY+26); ctx.stroke();
        ctx.strokeStyle='#0099cc'; ctx.lineWidth=2.5;
        ctx.beginPath(); ctx.moveTo(cx-7,bodyY+7); ctx.lineTo(cx-9-armSw,bodyY+14); ctx.stroke();
        ctx.beginPath(); ctx.moveTo(cx+7,bodyY+7); ctx.lineTo(cx+9+armSw,bodyY+14); ctx.stroke();
        const bg=ctx.createLinearGradient(cx-8,bodyY,cx+8,bodyY+18);
        bg.addColorStop(0,'#1a6a8a'); bg.addColorStop(1,'#0a3a5a');
        ctx.fillStyle=bg;
        ctx.beginPath(); ctx.roundRect(cx-7,bodyY+3,14,16,3); ctx.fill();
        ctx.strokeStyle='#00d4ff'; ctx.lineWidth=1; ctx.stroke();
        ctx.strokeStyle='rgba(0,212,255,.6)'; ctx.lineWidth=1;
        ctx.beginPath(); ctx.moveTo(cx,bodyY+5); ctx.lineTo(cx,bodyY+17); ctx.stroke();
        const hg=ctx.createRadialGradient(cx-2,bodyY-3,1,cx,bodyY,9);
        hg.addColorStop(0,'#2a8aaa'); hg.addColorStop(1,'#0a2540');
        ctx.fillStyle=hg;
        ctx.beginPath(); ctx.arc(cx,bodyY,9,0,Math.PI*2); ctx.fill();
        ctx.strokeStyle='#00d4ff'; ctx.lineWidth=1.5; ctx.stroke();
        ctx.fillStyle='rgba(0,212,255,.35)';
        ctx.beginPath(); ctx.ellipse(cx,bodyY+1,5,4,0,0,Math.PI*2); ctx.fill();
        ctx.strokeStyle='rgba(0,212,255,.7)'; ctx.lineWidth=0.8; ctx.stroke();
        ctx.fillStyle='rgba(255,255,255,.3)';
        ctx.beginPath(); ctx.ellipse(cx-1.5,bodyY-.5,1.5,1,-.5,0,Math.PI*2); ctx.fill();
        ctx.font='700 8px "Orbitron",monospace'; ctx.textAlign='center'; ctx.textBaseline='bottom';
        ctx.fillStyle='rgba(0,0,0,.7)'; ctx.fillText(p.name,cx+1,bodyY-12);
        ctx.fillStyle='#00d4ff';        ctx.fillText(p.name,cx,  bodyY-13);
    }

    // ─── HUD ──────────────────────────────────────────────────────────────────
    _drawHUD() {
        const ctx=this.ctx, W=this.canvas.width, H=this.canvas.height;
        const accent=this.layout.style?.wallColor||'#00d4ff';
        ctx.fillStyle='rgba(7,9,18,.88)';
        ctx.fillRect(0, H-32, W, 32);
        ctx.font='10px "Orbitron",monospace';
        ctx.fillStyle=this._alpha(accent,.65);
        ctx.textAlign='center'; ctx.textBaseline='middle';
        ctx.fillText('[ CLICK FLOOR TO WALK  •  CLICK STATION TO INTERACT  •  CLICK 🔒 TO UNLOCK ROOM ]', W/2, H-16);
        ctx.font='700 11px "Orbitron",monospace'; ctx.fillStyle=accent;
        ctx.textAlign='left'; ctx.fillText('◈  '+(this.layout.name||'STARBASE').toUpperCase(), 14, 20);
        ctx.font='700 9px "Orbitron",monospace'; ctx.fillStyle=this._alpha(accent,.35);
        ctx.textAlign='right'; ctx.fillText(`${this.COLS}×${this.ROWS}`, W-14, 20);

        // Active wallpaper badge
        const wpId = this.sbData.wallpaper;
        if (wpId) {
            const wp = this.catalog.find(i => i.id === wpId);
            if (wp) {
                ctx.font='700 9px "Orbitron",monospace';
                ctx.fillStyle=this._alpha(accent,.5);
                ctx.textAlign='right';
                ctx.fillText(`🎨 ${wp.name}`, W-14, 36);
            }
        }
    }

    // ─── Wallpaper panel ──────────────────────────────────────────────────────
    _buildWallpaperPanel() {
        document.getElementById('sb-wp-panel')?.remove();
        const wallpapers = this.catalog.filter(i => i.subtype === 'wallpaper');
        const owned      = this.sbData.ownedWallpapers || [];
        const rooms      = (this.layout.rooms || []).filter(r =>
            !r.unlock || (this.sbData.unlockedRooms||[]).includes(r.id));

        const panel = document.createElement('div');
        panel.id = 'sb-wp-panel';
        panel.style.cssText = `
            display:none; position:absolute; bottom:36px; left:0; right:0;
            background:linear-gradient(180deg,transparent,rgba(7,9,24,.96));
            border-top:1px solid rgba(0,212,255,.25);
            padding:12px 16px 10px; z-index:500; font-family:'Orbitron',monospace;
            pointer-events:all;
        `;

        // Room selector row
        const roomTabs = rooms.map(r =>
            `<button data-room="${r.id}" onclick="starbaseWorld._wpSetRoom('${r.id}')"
                style="background:rgba(0,212,255,.12);border:1px solid rgba(0,212,255,.3);
                color:#00d4ff;padding:4px 10px;border-radius:4px;cursor:pointer;
                font-family:'Orbitron',monospace;font-size:9px;margin-right:6px">
                ${r.label}
            </button>`
        ).join('');

        // Wallpaper swatches
        const swatches = wallpapers.map(wp => {
            const isOwned = owned.includes(wp.id);
            const p = wp.preview || {};
            const bg = p.wallColorTop || p.floorColorEven || '#1a2840';
            const fg = p.doorColor    || p.wallColor      || '#00ffcc';
            return `
            <div onclick="starbaseWorld._wpPreview('${wp.id}')"
                 title="${wp.name}${isOwned ? '' : ' (not owned)'}"
                 style="display:inline-flex;flex-direction:column;align-items:center;
                        gap:3px;cursor:${isOwned?'pointer':'default'};opacity:${isOwned?1:0.38};
                        margin-right:8px;vertical-align:top">
                <div style="width:36px;height:36px;border-radius:4px;border:2px solid ${fg};
                            background:${bg};position:relative;overflow:hidden">
                    ${isOwned ? '' : '<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;font-size:14px">🔒</div>'}
                    <div style="position:absolute;bottom:0;left:0;right:0;height:12px;
                                background:${p.wallColor||'#00d4ff'}33"></div>
                </div>
                <span style="font-size:7px;color:${isOwned?fg:'#555'};max-width:40px;
                             text-align:center;line-height:1.2">${wp.name}</span>
            </div>`;
        }).join('');

        panel.innerHTML = `
            <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
                <span style="font-size:10px;color:#00d4ff;font-weight:700">WALLPAPER</span>
                <span style="font-size:9px;color:rgba(0,212,255,.5)">Apply to:</span>
                <button onclick="starbaseWorld._wpSetRoom(null)"
                    style="background:rgba(0,212,255,.2);border:1px solid rgba(0,212,255,.5);
                    color:#00d4ff;padding:4px 10px;border-radius:4px;cursor:pointer;
                    font-family:'Orbitron',monospace;font-size:9px">All Rooms</button>
                ${roomTabs}
                <button onclick="starbaseWorld._wpApply(null)"
                    style="margin-left:auto;background:rgba(255,80,80,.15);border:1px solid rgba(255,80,80,.4);
                    color:#ff6060;padding:4px 12px;border-radius:4px;cursor:pointer;
                    font-family:'Orbitron',monospace;font-size:9px">Reset</button>
                <button onclick="starbaseWorld._toggleWallpaperPanel()"
                    style="background:none;border:none;color:rgba(255,255,255,.4);cursor:pointer;font-size:16px">✕</button>
            </div>
            <div style="overflow-x:auto;white-space:nowrap;padding-bottom:4px">${swatches}</div>
            <div style="margin-top:8px;display:flex;align-items:center;gap:10px">
                <span id="sb-wp-preview-name" style="font-size:9px;color:rgba(0,212,255,.6)">Hover a swatch to preview</span>
                <button id="sb-wp-apply-btn" onclick="starbaseWorld._wpApply(starbaseWorld._wpSelected)"
                    style="display:none;background:rgba(0,212,255,.2);border:1px solid #00d4ff;
                    color:#00d4ff;padding:5px 16px;border-radius:5px;cursor:pointer;
                    font-family:'Orbitron',monospace;font-size:9px;font-weight:700">Apply</button>
            </div>
        `;

        const container = this.canvas.parentElement;
        container.style.position = 'relative';
        container.appendChild(panel);
        this._wpPanel = panel;
    }

    _toggleWallpaperPanel() {
        if (!this._wpPanel) this._buildWallpaperPanel();
        this._wpPanelOpen = !this._wpPanelOpen;
        this._wpPanel.style.display = this._wpPanelOpen ? 'block' : 'none';
        if (this._wpPanelOpen) {
            this._wpSelected  = null;
            this._wpRoomTarget = null;
        }
    }

    _wpSetRoom(roomId) {
        this._wpRoomTarget = roomId;
        // Highlight active tab
        this._wpPanel.querySelectorAll('button[data-room]').forEach(b => {
            b.style.background = b.dataset.room === roomId
                ? 'rgba(0,212,255,.35)' : 'rgba(0,212,255,.12)';
        });
    }

    _wpPreview(wpId) {
        const owned = this.sbData.ownedWallpapers || [];
        if (!owned.includes(wpId)) return;
        this._wpSelected = wpId;
        const wp = this.catalog.find(i => i.id === wpId);
        const nameEl = document.getElementById('sb-wp-preview-name');
        const applyBtn = document.getElementById('sb-wp-apply-btn');
        if (nameEl && wp)  nameEl.textContent = wp.name + ' — ' + (wp.description || '');
        if (applyBtn)      applyBtn.style.display = 'inline-block';
    }

    _wpApply(wpId) {
        this._wpSelected = null;
        const socket = window.game?.socket || window.gameInitializer?.socket;
        if (!socket) {
            // Offline: apply locally only
            if (this._wpRoomTarget) {
                if (!this.sbData.roomWallpapers) this.sbData.roomWallpapers = {};
                if (wpId) this.sbData.roomWallpapers[this._wpRoomTarget] = wpId;
                else delete this.sbData.roomWallpapers[this._wpRoomTarget];
            } else {
                this.sbData.wallpaper = wpId;
            }
            return;
        }
        socket.emit('set_wallpaper', { wallpaperId: wpId, roomId: this._wpRoomTarget || null });
        // Optimistic update
        if (this._wpRoomTarget) {
            if (!this.sbData.roomWallpapers) this.sbData.roomWallpapers = {};
            if (wpId) this.sbData.roomWallpapers[this._wpRoomTarget] = wpId;
            else delete this.sbData.roomWallpapers[this._wpRoomTarget];
        } else {
            this.sbData.wallpaper = wpId;
        }
        const applyBtn = document.getElementById('sb-wp-apply-btn');
        if (applyBtn) applyBtn.style.display = 'none';
    }

    // ─── Interaction modals ───────────────────────────────────────────────────
    _showInteractModal(title, icon, desc, color, buttons) {
        document.getElementById('sb-interact-modal')?.remove();
        const modal = document.createElement('div');
        modal.id = 'sb-interact-modal';
        modal.style.cssText = `
            position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
            background:linear-gradient(135deg,#0d1525,#111c30);border:2px solid ${color};
            border-radius:12px;padding:24px 28px;min-width:280px;max-width:360px;z-index:1000;
            box-shadow:0 0 40px ${color}55,0 8px 32px rgba(0,0,0,.8);
            font-family:'Orbitron',monospace;color:#fff;animation:sbModalIn .2s ease;pointer-events:all;
        `;
        const btnHtml = buttons.map(b=>`
            <button onclick="starbaseWorld._handleModalAction('${b.action}');document.getElementById('sb-interact-modal').remove();"
                style="background:${color}22;border:1px solid ${color};color:${color};
                       padding:8px 18px;border-radius:6px;cursor:pointer;
                       font-family:'Orbitron',monospace;font-size:10px;font-weight:700;transition:background .2s;"
                onmouseover="this.style.background='${color}44'" onmouseout="this.style.background='${color}22'">${b.label}</button>
        `).join('');
        modal.innerHTML=`
            <div style="display:flex;align-items:center;gap:12px;margin-bottom:14px">
                <span style="font-size:2em">${icon}</span>
                <div>
                    <div style="font-size:13px;font-weight:700;color:${color};letter-spacing:1px">${title}</div>
                    <div style="font-size:9px;color:rgba(255,255,255,.4);margin-top:2px">STARBASE STATION</div>
                </div>
                <button onclick="document.getElementById('sb-interact-modal').remove()"
                    style="margin-left:auto;background:none;border:none;color:rgba(255,255,255,.4);cursor:pointer;font-size:18px">&times;</button>
            </div>
            <div style="font-size:11px;color:rgba(184,197,214,.9);line-height:1.6;margin-bottom:18px">${desc}</div>
            <div style="display:flex;flex-wrap:wrap;gap:8px">${btnHtml}</div>
        `;
        this.canvas.parentElement.appendChild(modal);
        setTimeout(() => modal.remove(), 8000);
    }

    _showLockedRoomModal(room) {
        const unlock = this.catalog.find(i => i.id === room.unlock);
        const name   = unlock?.name || room.unlock;
        const price  = unlock ? `${unlock.price} ${unlock.currency}` : '?';
        const rarity = unlock?.rarity || 'unknown';
        const color  = '#ff8844';
        this._showInteractModal(
            `${room.label} — Locked`, '🔒',
            `This wing requires the <strong style="color:${color}">${name}</strong> (${rarity}) to unlock.<br><br>` +
            (unlock?.categories?.includes('shop') ? `Purchase for ${price} in the Trade Post, or earn it as a dungeon drop.` :
             `Earn this by clearing dungeons or completing quests.`),
            color,
            unlock?.categories?.includes('shop') ? [{ label: 'Visit Trade Post', action: 'shop' }] : []
        );
    }

    _handleModalAction(action) {
        const map={shop:'[data-tab="shop"]',crafting:'[data-tab="crafting"]',
                   quests:'[data-tab="quests"]',skills:'[data-tab="skills"]',base:'[data-tab="base"]'};
        if (map[action]) { const b=document.querySelector(map[action]); if(b){b.click();return;} }
        for (const b of document.querySelectorAll('[onclick]')) {
            if ((b.getAttribute('onclick')||'').toLowerCase().includes(action)) { b.click(); return; }
        }
    }

    // ─── Color utilities ──────────────────────────────────────────────────────
    _hexToRgb(hex) {
        hex=hex.replace('#','');
        if (hex.length===3) hex=hex.split('').map(c=>c+c).join('');
        return { r:parseInt(hex.slice(0,2),16), g:parseInt(hex.slice(2,4),16), b:parseInt(hex.slice(4,6),16) };
    }
    _darken(hex, f) {
        const {r,g,b}=this._hexToRgb(hex);
        return `rgb(${Math.round(r*f)},${Math.round(g*f)},${Math.round(b*f)})`;
    }
    _lighten(hex, f) {
        const {r,g,b}=this._hexToRgb(hex);
        return `rgb(${Math.min(255,Math.round(r+(255-r)*f))},${Math.min(255,Math.round(g+(255-g)*f))},${Math.min(255,Math.round(b+(255-b)*f))})`;
    }
    _alpha(hex, a) {
        if (hex.startsWith('rgba')||hex.startsWith('rgb(')) return hex.replace(/[\d.]+\)$/,`${a})`);
        const {r,g,b}=this._hexToRgb(hex);
        return `rgba(${r},${g},${b},${a})`;
    }
}

// ─── JSON loader ──────────────────────────────────────────────────────────────
async function loadStarbaseWorld(canvasId) {
    let layout;
    try {
        const resp = await fetch(`data/starbase-layout.json?v=${Date.now()}`);
        if (!resp.ok) throw new Error(resp.status);
        layout = await resp.json();
    } catch(e) {
        console.warn('[STARBASE] Using default layout:', e.message);
        layout = StarbaseWorld.defaultLayout();
    }

    // Fetch player starbase data from server
    let sbData = { wallpaper: null, ownedWallpapers: [], unlockedRooms: [] };
    let catalog = [];

    const socket = window.game?.socket || window.gameInitializer?.socket;
    if (socket) {
        await new Promise(resolve => {
            const timer = setTimeout(resolve, 3000);
            socket.once('starbase_data', data => {
                clearTimeout(timer);
                if (data.success) { sbData = data.starbase; catalog = data.catalog || []; }
                resolve();
            });
            socket.emit('get_starbase_data');
        });
    }

    if (window.starbaseWorld) { window.starbaseWorld.stop(); window.starbaseWorld = null; }
    window.starbaseWorld = new StarbaseWorld(canvasId, layout, sbData, catalog);

    // Listen for server wallpaper confirmations
    if (socket) {
        socket.off('wallpaper_set');
        socket.on('wallpaper_set', data => {
            if (data.success && window.starbaseWorld) {
                window.starbaseWorld.sbData = data.starbase;
            }
        });
    }

    return window.starbaseWorld;
}

// ─── Helpers ──────────────────────────────────────────────────────────────────
function mulberry32(a) {
    return function() {
        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;
    };
}

(function(){
    if (document.getElementById('sb-anim-style')) return;
    const s=document.createElement('style'); s.id='sb-anim-style';
    s.textContent=`
        @keyframes sbModalIn {
            from{opacity:0;transform:translate(-50%,-46%)}
            to  {opacity:1;transform:translate(-50%,-50%)}
        }
    `;
    document.head.appendChild(s);
})();
