var SigSelect = (function () {
    // Polarity → three roles in fixed left/mid/right cursor order
    var POLARITY_ROLES = {
        levity:  ['PC', 'NC', 'SC'],
        gravity: ['BC', 'EC', 'AC'],
    };

    var overlay, deckGrid, stage, stageCard, statBlock;
    var cautionEl, cautionEffect, cautionTitle, cautionTypeEl, cautionPrev, cautionNext, cautionIndexEl;
    var _flipBtn, _cautionBtn, _flipOrigLabel, _cautionOrigLabel;
    var reserveUrl, readyUrl, userRole, userPolarity;

    var _isReady = false;
    var _takeSigBtn = null;
    var _glowTimer = null;
    var _glowPeak = false;
    var _countdownTimer = null;
    var _countdownSecondsLeft = 0;

    var _cautionData = [];
    var _cautionIdx  = 0;

    var _focusedCardEl = null;   // card currently shown in stage
    var _reservedCardId = null;  // card with active reservation
    var _stageFrozen = false;    // true after OK — stage locks on reserved card
    var _requestInFlight = false;

    var _floatingCursors = {};   // key: cardId+posClass → portal <i> element (hover)
    var _reservedFloats  = {};   // key: role → portal <i> element (thumbs-up, frozen)
    var _cursorPortal = null;

    // CAST SKY click → page reload. Reassignable via setReload for tests so
    // they can spy without actually reloading the Jasmine runner.
    var _reload = function () { window.location.reload(); };

    function getCsrf() {
        var m = document.cookie.match(/csrftoken=([^;]+)/);
        return m ? m[1] : '';
    }

    // ── Stage ──────────────────────────────────────────────────────────────

    // _populateKeywordList removed — sig now delegates to StageCard.populateKeywords

    // ── Caution tooltip ───────────────────────────────────────────────────

    function _renderCaution() {
        StageCard.renderFyi(stage.querySelector('.sig-info'), _cautionData, _cautionIdx);
        // Sig disables PRV/NXT when there's only one entry (sea/fan don't bother).
        if (cautionPrev) cautionPrev.disabled = (_cautionData.length <= 1);
        if (cautionNext) cautionNext.disabled = (_cautionData.length <= 1);
    }

    function _openCaution() {
        if (!_focusedCardEl) return;
        var card = StageCard.fromDataset(_focusedCardEl);
        _cautionData = StageCard.buildInfoData(card);
        _cautionIdx = 0;
        _renderCaution();
        _flipBtn.classList.add('btn-disabled');
        _cautionBtn.classList.add('btn-disabled');
        _flipBtn.textContent = '\u00D7';
        _cautionBtn.textContent = '\u00D7';
        statBlock.classList.add('fyi-open');
    }

    function _closeCaution() {
        if (statBlock) statBlock.classList.remove('fyi-open');
        if (_flipBtn) {
            _flipBtn.classList.remove('btn-disabled');
            _cautionBtn.classList.remove('btn-disabled');
            _flipBtn.textContent = _flipOrigLabel;
            _cautionBtn.textContent = _cautionOrigLabel;
        }
    }

    function updateStage(cardEl) {
        if (_stageFrozen) return;
        _closeCaution();
        if (!cardEl) {
            stageCard.style.display = 'none';
            stage.classList.remove('sig-stage--active');
            _focusedCardEl = null;
            return;
        }
        _focusedCardEl = cardEl;

        var card = StageCard.fromDataset(cardEl);
        StageCard.populateCard(stageCard, card, userPolarity);
        // Sig hides the correspondence in the stage card (shown in game-kit only)
        var corrEl = stageCard.querySelector('.fan-card-correspondence');
        if (corrEl) corrEl.textContent = '';

        // Reset SPIN state, then populate keyword lists
        statBlock.classList.remove('is-reversed');
        stageCard.classList.remove('stage-card--reversed');
        StageCard.populateKeywords(statBlock, card.keywords_upright, card.keywords_reversed, {
            uprightSel:  '#id_stat_keywords_upright',
            reversedSel: '#id_stat_keywords_reversed',
        });

        stageCard.style.display = '';
        stage.classList.add('sig-stage--active');
    }

    // ── Focus a card (click/tap) — shows OK overlay on the card ──────────

    function focusCard(cardEl) {
        deckGrid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
            if (c !== cardEl) c.classList.remove('sig-focused');
        });
        cardEl.classList.add('sig-focused');
        updateStage(cardEl);
    }

    // ── Hover events ──────────────────────────────────────────────────────

    function onCardEnter(e) {
        var card = e.currentTarget;
        if (!_stageFrozen) updateStage(card);
        sendHover(card.dataset.cardId, true);
    }

    function onCardLeave(e) {
        if (!_stageFrozen) updateStage(null);
        sendHover(e.currentTarget.dataset.cardId, false);
    }

    // ── Reserve / release ─────────────────────────────────────────────────

    function doReserve(cardEl) {
        if (_requestInFlight) return;
        var cardId = cardEl.dataset.cardId;
        _requestInFlight = true;
        fetch(reserveUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrf() },
            body: 'action=reserve&card_id=' + encodeURIComponent(cardId),
        }).then(function (res) {
            _requestInFlight = false;
            if (res.ok) applyReservation(cardId, userRole, true);
        }).catch(function () { _requestInFlight = false; });
    }

    function doRelease() {
        if (_requestInFlight || !_reservedCardId) return;
        var cardId = _reservedCardId;
        _requestInFlight = true;
        fetch(reserveUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrf() },
            body: 'action=release&card_id=' + encodeURIComponent(cardId),
        }).then(function (res) {
            _requestInFlight = false;
            if (res.ok) applyReservation(cardId, userRole, false);
        }).catch(function () { _requestInFlight = false; });
    }

    // ── Apply reservation state (local + from WS) ─────────────────────────

    function _placeReservedFloat(cardId, cardEl, role) {
        // Remove any pre-existing reserved float for this role (e.g. page-load replay)
        if (_reservedFloats[role]) { _reservedFloats[role].remove(); }

        // Retire ALL hover floats for this role — may be on a different card than reserved
        var roles = POLARITY_ROLES[userPolarity] || [];
        var idx   = roles.indexOf(role);
        var posClass = ['--left', '--mid', '--right'][idx] || '--left';
        Object.keys(_floatingCursors).forEach(function (key) {
            if (key.slice(-posClass.length) === posClass) {
                _floatingCursors[key].remove();
                var hCid = key.slice(0, key.length - posClass.length);
                var hEl = deckGrid.querySelector('.sig-card[data-card-id="' + hCid + '"]');
                if (hEl) {
                    var a = hEl.querySelector('.sig-cursor' + posClass);
                    if (a) a.classList.remove('active');
                }
                delete _floatingCursors[key];
            }
        });

        var rect = cardEl.getBoundingClientRect();
        var xFractions = [0.15, 0.5, 0.85];
        var fc = document.createElement('i');
        fc.className = 'fa-solid fa-thumbs-up sig-cursor-float sig-cursor-float--reserved';
        fc.dataset.role = role;
        fc.style.left = (rect.left + rect.width * xFractions[idx < 0 ? 1 : idx]) + 'px';
        fc.style.top  = rect.bottom + 'px';
        _ensureCursorPortal().appendChild(fc);
        _reservedFloats[role] = fc;
    }

    function applyReservation(cardId, role, reserved) {
        var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
        if (!cardEl) return;

        if (reserved) {
            cardEl.dataset.reservedBy = role;
            cardEl.classList.add('sig-reserved');
            if (role === userRole) {
                _reservedCardId = cardId;
                cardEl.classList.add('sig-reserved--own');
                cardEl.classList.remove('sig-focused');
                // Freeze stage on this card (temporarily unfreeze to populate it)
                _stageFrozen = false;
                updateStage(cardEl);
                _stageFrozen = true;
                stage.classList.add('sig-stage--frozen');
                _showTakeSigBtn();
            }
            // Thumbs-up float for all reservations — own role sees their own indicator too
            _placeReservedFloat(cardId, cardEl, role);
        } else {
            delete cardEl.dataset.reservedBy;
            cardEl.classList.remove('sig-reserved', 'sig-reserved--own');
            if (role === userRole) {
                _reservedCardId = null;
                _stageFrozen = false;
                stage.classList.remove('sig-stage--frozen');
                _hideTakeSigBtn();
            }
            // Remove thumbs-up float for all releases — own role included
            if (_reservedFloats[role]) {
                _reservedFloats[role].remove();
                delete _reservedFloats[role];
            }
        }
    }

    // ── Apply hover cursor (WS only — own hover is CSS :hover) ────────────
    //
    // Cursor icons are portaled to document root so they escape overflow/clip
    // contexts in the deck grid. The in-card anchor elements only carry the
    // .active class (for test assertions and the :has() z-index rule).

    function _ensureCursorPortal() {
        if (!_cursorPortal || !document.body.contains(_cursorPortal)) {
            _cursorPortal = document.getElementById('id_sig_cursor_portal');
            if (!_cursorPortal) {
                _cursorPortal = document.createElement('div');
                _cursorPortal.id = 'id_sig_cursor_portal';
                document.body.appendChild(_cursorPortal);
            }
        }
        return _cursorPortal;
    }

    function applyHover(cardId, role, active) {
        if (role === userRole) return;
        if (_reservedFloats[role]) return;  // role already has thumbs-up — ignore hover
        var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
        if (!cardEl) return;

        var roles = POLARITY_ROLES[userPolarity] || [];
        var idx = roles.indexOf(role);
        var posClass = ['--left', '--mid', '--right'][idx] || '--left';
        var anchor = cardEl.querySelector('.sig-cursor' + posClass);
        if (!anchor) return;

        var key = cardId + posClass;

        if (active) {
            anchor.classList.add('active');  // kept for test assertions + :has() z-index

            // Place a fixed-position clone in the portal, positioned from card bounds
            var rect = cardEl.getBoundingClientRect();
            var xFractions = [0.15, 0.5, 0.85];
            var fc = document.createElement('i');
            fc.className = 'fa-solid fa-hand-pointer sig-cursor-float';
            fc.dataset.role = role;
            fc.style.left = (rect.left + rect.width * xFractions[idx]) + 'px';
            fc.style.top  = rect.bottom + 'px';
            _ensureCursorPortal().appendChild(fc);
            _floatingCursors[key] = fc;
        } else {
            anchor.classList.remove('active');
            if (_floatingCursors[key]) {
                _floatingCursors[key].remove();
                delete _floatingCursors[key];
            }
        }
    }

    // ── Reposition floats after resize ────────────────────────────────────
    //
    // Both reserved (thumbs-up) and hover (hand-pointer) floats are stamped with
    // fixed pixel coords at placement time.  When the viewport changes size the
    // cards reflow but the icons stay put.  Re-measure from the card's current
    // bounding rect and update left/top in-place.

    var _posClasses = ['--left', '--mid', '--right'];
    var _xFractions = [0.15, 0.5, 0.85];

    function _repositionFloats() {
        if (!deckGrid) return;
        var roles = POLARITY_ROLES[userPolarity] || [];

        Object.keys(_reservedFloats).forEach(function (role) {
            var cardEl = deckGrid.querySelector('.sig-card[data-reserved-by="' + role + '"]');
            if (!cardEl) return;
            var rect = cardEl.getBoundingClientRect();
            var idx  = roles.indexOf(role);
            var fc   = _reservedFloats[role];
            fc.style.left = (rect.left + rect.width * _xFractions[idx < 0 ? 1 : idx]) + 'px';
            fc.style.top  = rect.bottom + 'px';
        });

        Object.keys(_floatingCursors).forEach(function (key) {
            var posClass = _posClasses.find(function (p) {
                return key.slice(-p.length) === p;
            });
            if (!posClass) return;
            var cardId = key.slice(0, key.length - posClass.length);
            var cardEl = deckGrid.querySelector('.sig-card[data-card-id="' + cardId + '"]');
            if (!cardEl) return;
            var rect = cardEl.getBoundingClientRect();
            var idx  = _posClasses.indexOf(posClass);
            var fc   = _floatingCursors[key];
            fc.style.left = (rect.left + rect.width * _xFractions[idx]) + 'px';
            fc.style.top  = rect.bottom + 'px';
        });
    }

    // ── SAVE SIG / WAIT NVM button ─────────────────────────────────────────

    function _onTakeSigClick() {
        if (_isReady) {
            var body = 'action=unready';
            if (_countdownTimer !== null) {
                body += '&seconds_remaining=' + _countdownSecondsLeft;
            }
            fetch(readyUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrf() },
                body: body,
            }).then(function (res) {
                if (res.ok) {
                    _isReady = false;
                    if (_countdownTimer !== null) {
                        clearInterval(_countdownTimer);
                        _countdownTimer = null;
                        if (_takeSigBtn) _takeSigBtn.style.fontSize = '';
                    }
                    if (_takeSigBtn) _takeSigBtn.textContent = 'SAVE SIG';
                    _stopWaitNoGlow();
                    _stopCountdownGlow();
                }
            });
        } else {
            fetch(readyUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRFToken': getCsrf() },
                body: 'action=ready',
            }).then(function (res) {
                if (res.ok) {
                    _isReady = true;
                    // countdown_start WS may arrive before this response for the
                    // gamer who triggered the countdown — don't clobber the numeral.
                    if (_countdownTimer === null) {
                        if (_takeSigBtn) _takeSigBtn.textContent = 'WAIT NVM';
                        _startWaitNoGlow();
                    }
                }
            });
        }
    }

    function _showTakeSigBtn() {
        if (_takeSigBtn || !stage) return;
        _takeSigBtn = document.createElement('button');
        _takeSigBtn.id = 'id_take_sig_btn';
        _takeSigBtn.className = 'btn btn-primary sig-take-sig-btn';
        _takeSigBtn.type = 'button';
        _takeSigBtn.textContent = 'SAVE SIG';
        _takeSigBtn.addEventListener('click', _onTakeSigClick);
        stage.appendChild(_takeSigBtn);
    }

    function _startWaitNoGlow() {
        if (_glowTimer !== null) return;
        _glowPeak = false;
        _glowTimer = setInterval(function () {
            if (!_takeSigBtn) { _stopWaitNoGlow(); return; }
            _glowPeak = !_glowPeak;
            if (_glowPeak) {
                _takeSigBtn.classList.add('btn-cancel');
                _takeSigBtn.style.boxShadow =
                    '0 0 0.8rem 0.2rem rgba(var(--terOr), 0.75), ' +
                    '0 0 2rem 0.4rem rgba(var(--terOr), 0.35)';
            } else {
                _takeSigBtn.classList.remove('btn-cancel');
                _takeSigBtn.style.boxShadow = '';
            }
        }, 600);
    }

    function _stopWaitNoGlow() {
        if (_glowTimer !== null) { clearInterval(_glowTimer); _glowTimer = null; }
        if (_takeSigBtn) {
            _takeSigBtn.classList.remove('btn-cancel');
            _takeSigBtn.style.boxShadow = '';
        }
        _glowPeak = false;
    }

    function _startCountdownGlow() {
        if (_glowTimer !== null) return;
        _glowPeak = false;
        _glowTimer = setInterval(function () {
            if (!_takeSigBtn) { _stopCountdownGlow(); return; }
            _glowPeak = !_glowPeak;
            if (_glowPeak) {
                _takeSigBtn.classList.add('btn-danger');
                _takeSigBtn.style.boxShadow =
                    '0 0 0.8rem 0.2rem rgba(var(--terRd), 0.75), ' +
                    '0 0 2rem 0.4rem rgba(var(--terRd), 0.35)';
            } else {
                _takeSigBtn.classList.remove('btn-danger');
                _takeSigBtn.style.boxShadow = '';
            }
        }, 600);
    }

    function _stopCountdownGlow() {
        if (_glowTimer !== null) { clearInterval(_glowTimer); _glowTimer = null; }
        if (_takeSigBtn) {
            _takeSigBtn.classList.remove('btn-danger');
            _takeSigBtn.style.boxShadow = '';
        }
        _glowPeak = false;
    }

    function _hideTakeSigBtn() {
        if (!_takeSigBtn) return;
        _stopWaitNoGlow();
        _takeSigBtn.removeEventListener('click', _onTakeSigClick);
        _takeSigBtn.remove();
        _takeSigBtn = null;
        _isReady = false;
    }

    // ── Polarity countdown ────────────────────────────────────────────────

    function _showCountdown(seconds) {
        _countdownSecondsLeft = seconds;
        _stopWaitNoGlow();
        if (_takeSigBtn) {
            _takeSigBtn.textContent = _countdownSecondsLeft;
            _takeSigBtn.style.fontSize = '2em';
        }
        _startCountdownGlow();
        if (_countdownTimer !== null) clearInterval(_countdownTimer);
        _countdownTimer = setInterval(function () {
            _countdownSecondsLeft -= 1;
            if (_takeSigBtn) _takeSigBtn.textContent = _countdownSecondsLeft;
            if (_countdownSecondsLeft <= 0) {
                clearInterval(_countdownTimer);
                _countdownTimer = null;
                _stopCountdownGlow();  // server drives the transition via Celery task
            }
        }, 1000);
    }

    function _hideCountdown() {
        if (_countdownTimer !== null) {
            clearInterval(_countdownTimer);
            _countdownTimer = null;
        }
        _stopCountdownGlow();
        if (_takeSigBtn) {
            _takeSigBtn.style.fontSize = '';
            if (_isReady) {
                // Countdown cancelled by another gamer — restore WAIT NVM state
                _takeSigBtn.textContent = 'WAIT NVM';
                _startWaitNoGlow();
            }
        }
    }

    // ── Overlay dismiss + waiting message ─────────────────────────────────

    function _dismissSigOverlay() {
        _hideCountdown();
        _hideTakeSigBtn();
        var backdrop = document.querySelector('.sig-backdrop');
        if (backdrop) backdrop.remove();
        if (overlay) { overlay.remove(); overlay = null; }
        // Remove all floating cursors (hover + thumbs-up) from the portal
        Object.keys(_reservedFloats).forEach(function (role) {
            _reservedFloats[role].remove();
        });
        _reservedFloats = {};
        Object.keys(_floatingCursors).forEach(function (key) {
            _floatingCursors[key].remove();
        });
        _floatingCursors = {};
    }

    function _showWaitingMsg(pendingPolarity) {
        if (document.getElementById('id_hex_waiting_msg')) return;
        // If the OTHER polarity finished before our tray sequence completed,
        // pick_sky_available will already have fired and revealed CAST SKY.
        // In that case skip the waiting msg so the two don't co-exist.
        var pickSkyBtn = document.getElementById('id_pick_sky_btn');
        if (pickSkyBtn && pickSkyBtn.style.display !== 'none') return;
        var msg = document.createElement('p');
        msg.id = 'id_hex_waiting_msg';
        msg.textContent = pendingPolarity === 'gravity'
            ? 'Gravity settling . . .'
            : 'Levity appraising . . .';
        var center = document.querySelector('.table-center');
        if (center) center.appendChild(msg);
    }

    // ── WS events ─────────────────────────────────────────────────────────

    window.addEventListener('room:sig_reserved', function (e) {
        if (!deckGrid) return;
        applyReservation(String(e.detail.card_id), e.detail.role, e.detail.reserved);
    });

    window.addEventListener('room:sig_hover', function (e) {
        if (!deckGrid) return;
        applyHover(String(e.detail.card_id), e.detail.role, e.detail.active);
    });

    window.addEventListener('room:countdown_start', function (e) {
        if (!overlay) return;
        _showCountdown(e.detail.seconds);
    });

    window.addEventListener('room:countdown_cancel', function (e) {
        _hideCountdown();
        _countdownSecondsLeft = e.detail.seconds_remaining;
    });

    window.addEventListener('room:polarity_room_done', function (e) {
        if (!overlay) return;
        if (e.detail.polarity !== userPolarity) return;
        var pendingPolarity = userPolarity === 'levity' ? 'gravity' : 'levity';
        // Tray-place sequence first (visually anchors the sig stage's exit),
        // then a 2s hang so the user can register the tray closing before
        // the overlay vanishes; overlay dismissal + waiting msg run last.
        // User sees: stage card → tray slides in → sig fades into the tray
        // cell → tray slides out → 2s pause → overlay dismisses → table hex
        // w. waiting msg (or CAST SKY btn if both polarities are done).
        function _settle() {
            _dismissSigOverlay();
            _showWaitingMsg(pendingPolarity);
        }
        if (typeof Tray !== "undefined" && Tray.placeSig) {
            Tray.placeSig(stageCard, function () {
                setTimeout(_settle, 2000);
            });
        } else {
            _settle();
        }
    });

    window.addEventListener('room:pick_sky_available', function () {
        var msg = document.getElementById('id_hex_waiting_msg');
        if (msg) msg.remove();
        var btn = document.getElementById('id_pick_sky_btn');
        if (btn) btn.style.display = '';
    });

    window.addEventListener('resize:end', _repositionFloats);

    // ── WS send ───────────────────────────────────────────────────────────

    function sendHover(cardId, active) {
        if (!window._roomSocket || window._roomSocket.readyState !== WebSocket.OPEN) return;
        window._roomSocket.send(JSON.stringify({
            type: 'sig_hover', card_id: cardId, role: userRole, active: active,
        }));
    }

    // ── Init ──────────────────────────────────────────────────────────────

    function init() {
        overlay = document.querySelector('.sig-overlay');
        if (!overlay) return;

        deckGrid  = overlay.querySelector('.sig-deck-grid');
        stage     = overlay.querySelector('.sig-stage');
        stageCard = stage.querySelector('.sig-stage-card');
        statBlock = stage.querySelector('.sig-stat-block');

        _flipBtn = statBlock.querySelector('.spin-btn');
        _cautionBtn = statBlock.querySelector('.fyi-btn');
        _flipOrigLabel = _flipBtn.textContent;
        _cautionOrigLabel = _cautionBtn.textContent;

        _flipBtn.addEventListener('click', function () {
            if (_flipBtn.classList.contains('btn-disabled')) return;
            statBlock.classList.toggle('is-reversed');
            stageCard.classList.toggle('stage-card--reversed');
        });

        cautionEl       = stage.querySelector('.sig-info');
        cautionEffect   = cautionEl.querySelector('.sig-info-effect');
        cautionTitle    = cautionEl.querySelector('.sig-info-title');
        cautionTypeEl   = cautionEl.querySelector('.sig-info-type');
        cautionPrev     = statBlock.querySelector('.fyi-prev');
        cautionNext     = statBlock.querySelector('.fyi-next');
        cautionIndexEl  = cautionEl.querySelector('.sig-info-index');

        // Clicking the tooltip (not nav buttons) dismisses it
        cautionEl.addEventListener('click', function () {
            _closeCaution();
        });

        _cautionBtn.addEventListener('click', function () {
            if (_cautionBtn.classList.contains('btn-disabled')) return;
            statBlock.classList.contains('fyi-open') ? _closeCaution() : _openCaution();
        });
        cautionPrev.addEventListener('click', function () {
            _cautionIdx = (_cautionIdx - 1 + _cautionData.length) % _cautionData.length;
            _renderCaution();
        });
        cautionNext.addEventListener('click', function () {
            _cautionIdx = (_cautionIdx + 1) % _cautionData.length;
            _renderCaution();
        });

        reserveUrl  = overlay.dataset.reserveUrl;
        readyUrl    = overlay.dataset.readyUrl;

        userRole    = overlay.dataset.userRole;
        userPolarity= overlay.dataset.polarity;

        // CAST SKY btn is rendered hidden during SIG_SELECT; revealed by
        // room:pick_sky_available once both polarity rooms are done. Clicking
        // it must reload the page — the _sky_overlay.html partial is only
        // included server-side once room.table_status == "SKY_SELECT", so
        // reloading is the only path that brings the modal + its openSky
        // handler into the DOM. (Tray.placeSig already played during the
        // polarity_room_done sequence; do NOT re-open the tray here.)
        var pickSkyBtn = document.getElementById('id_pick_sky_btn');
        if (pickSkyBtn) {
            pickSkyBtn.addEventListener('click', function () { _reload(); });
        }

        // Restore reservations from server-rendered JSON (page-load state).
        // Deferred to 'load' so sizeSigModal() (also a 'load' listener, registered
        // in room.js before this script) has already applied paddingBottom and
        // --sig-card-w before _placeReservedFloat calls getBoundingClientRect().
        try {
            var existing = JSON.parse(overlay.dataset.reservations || '{}');
            if (Object.keys(existing).length) {
                var _replayReservations = function () {
                    Object.keys(existing).forEach(function (cardId) {
                        applyReservation(cardId, existing[cardId], true);
                    });
                    // Restore WAIT NVM state if gamer was already ready before page load
                    if (overlay.dataset.ready === 'true' && _takeSigBtn) {
                        _isReady = true;
                        _takeSigBtn.textContent = 'WAIT NVM';
                        _startWaitNoGlow();
                    }
                };
                if (document.readyState === 'complete') {
                    _replayReservations();
                } else {
                    window.addEventListener('load', _replayReservations, { once: true });
                }
            }
        } catch (e) { /* malformed JSON — ignore */ }

        // Hover: update stage preview + broadcast cursor
        deckGrid.querySelectorAll('.sig-card').forEach(function (card) {
            card.addEventListener('mouseenter', onCardEnter);
            card.addEventListener('mouseleave', onCardLeave);
            card.addEventListener('touchstart', function (e) {
                var card = e.currentTarget;
                if (_reservedCardId) return;  // locked until NVM — no preventDefault either
                var reservedByOther = card.dataset.reservedBy && card.dataset.reservedBy !== userRole;
                var isOwnReserved = card.classList.contains('sig-reserved--own');
                if (reservedByOther || isOwnReserved) return;
                // If the tap is on the OK button, let the synthetic click fire normally
                if (e.target.closest('.sig-ok-btn')) return;
                focusCard(card);
                e.preventDefault(); // prevent ghost click on card body
            }, { passive: false });
        });

        // Touch outside the grid — dismiss stage preview (unfocused state only).
        // Card touchstart doesn't stop propagation, so we guard with closest().
        overlay.addEventListener('touchstart', function (e) {
            if (_stageFrozen || !_focusedCardEl) return;
            if (e.target.closest('.sig-deck-grid')) return;
            deckGrid.querySelectorAll('.sig-card.sig-focused').forEach(function (c) {
                c.classList.remove('sig-focused');
            });
            updateStage(null);
        }, { passive: true });

        // Click delegation: card body → focus (shows OK); OK/NVM buttons → reserve/release
        deckGrid.addEventListener('click', function (e) {
            if (e.target.closest('.sig-ok-btn')) {
                if (_reservedCardId) return;  // already holding — must NVM first
                var card = e.target.closest('.sig-card');
                if (card) doReserve(card);
                return;
            }
            if (e.target.closest('.sig-nvm-btn')) {
                doRelease();
                return;
            }
            var card = e.target.closest('.sig-card');
            if (!card) return;
            if (_reservedCardId) return;  // locked until NVM
            var reservedByOther = card.dataset.reservedBy && card.dataset.reservedBy !== userRole;
            var isOwnReserved = card.classList.contains('sig-reserved--own');
            if (reservedByOther || isOwnReserved) return;
            focusCard(card);
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    // ── Test API ──────────────────────────────────────────────────────────
    return {
        _testInit: function () {
            _focusedCardEl = null;
            _reservedCardId = null;
            _stageFrozen = false;
            _requestInFlight = false;
            _cautionData = [];
            _cautionIdx = 0;
            Object.keys(_floatingCursors).forEach(function (k) { _floatingCursors[k].remove(); });
            _floatingCursors = {};
            Object.keys(_reservedFloats).forEach(function (k) { _reservedFloats[k].remove(); });
            _reservedFloats = {};
            _cursorPortal = null;
            _isReady = false;
            _stopWaitNoGlow();
            _hideTakeSigBtn();
            _hideCountdown();
            _countdownSecondsLeft = 0;
            init();
        },
        _setFrozen: function (v) { _stageFrozen = v; },
        _setReservedCardId: function (id) { _reservedCardId = id; },
        setReload:  function (fn) { _reload = fn; },
    };
}());
