Animesss Labyrinth Map

Интерактивная карта лабиринта (совместное прохождение)

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

Advertisement:

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

Advertisement:

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Animesss Labyrinth Map
// @namespace    https://animesss.com/
// @version      8.1.0
// @description  Интерактивная карта лабиринта (совместное прохождение)
// @author       VladLIO
// @license      MIT
// @match        https://animesss.com/*
// @match        https://animesss.tv/*
// @grant        none
// @connect      labmap.duckdns.org
// @run-at       document-end
// ==/UserScript==

var LM_IS_CLUB_PAGE = /^\/clubs\/\d+\/?$/.test(location.pathname || '');
var LM_IS_LABYRINTH_PAGE = /\/labyrinth(\/|$)/.test(location.pathname || '');
var _QSC_API_BASE  = 'https://labmap.duckdns.org/qsc';
var _QSC_API_KEY   = 'qzPub_animesss_2026';
var _labLastStepsCount = 0;
// Черга для /lab/events — щоб не спамити при швидкому клікані
var _qscEvQueue = [];
var _qscEvTimer2 = null;
function _qscProcessQueue() {
  if (_qscEvQueue.length === 0) { _qscEvTimer2 = null; return; }
  var item = _qscEvQueue.shift();
  fetch(item.url, item.opts).catch(function() {});
  if (_qscEvQueue.length > 0) {
    _qscEvTimer2 = setTimeout(_qscProcessQueue, 400);
  } else {
    _qscEvTimer2 = null;
  }
}

if (LM_IS_LABYRINTH_PAGE) {
(function() {
  var origOpen = XMLHttpRequest.prototype.open;
  var origSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function(method, url) {
    this._lmUrl = url;
    return origOpen.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function(body) {
    if (this._lmUrl && this._lmUrl.indexOf('mod=animesss_game') !== -1 &&
        typeof body === 'string') {
      if (body.indexOf('action=step') !== -1) {
        var xhr = this;
        xhr.addEventListener('load', function() {
          try {
            var resp = JSON.parse(xhr.responseText);
            if (!resp) return;
            if (!window.labyrinthData) window.labyrinthData = {};
            if (resp.mapData) window.labyrinthData.mapData = resp.mapData;
            if (typeof resp.fate_room !== 'undefined') window.labyrinthData.fateRoom = resp.fate_room || null;
            if (typeof resp.echo_room !== 'undefined') window.labyrinthData.echoRoom = resp.echo_room || null;
            if (typeof resp.can_place_room_object !== 'undefined') {
              window.labyrinthData.canPlaceObject = resp.can_place_room_object;
              window.labyrinthData.canPlaceRoomObject = resp.can_place_room_object;
            }
            var _username = window.visitor_name || '';
            var _sv = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? GM_info.script.version : '8.1.0';
            var _sn = encodeURIComponent('Animesss Labyrinth Map');
            try {
              if (_username) {
                var _evName = (typeof resp.event === 'string' && resp.event) ? resp.event : 'unknown';
                var _d = new Date();
                var _pad = function(n) { return String(n).padStart(2, '0'); };
                var _stamp = _d.getFullYear() + '-' + _pad(_d.getMonth()+1) + '-' + _pad(_d.getDate()) + '_' + _pad(_d.getHours()) + '-' + _pad(_d.getMinutes()) + '-' + _pad(_d.getSeconds());
                _qscEvQueue.push({
                  url: _QSC_API_BASE + '/lab/events?key=' + _QSC_API_KEY + '&sv=' + _sv + '&sn=' + _sn,
                  opts: {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                      username: _username,
                      event_name: _evName,
                      filename: _username + '_' + _evName + '_' + _stamp + '.json',
                      content: xhr.responseText
                    })
                  }
                });
                if (!_qscEvTimer2) _qscProcessQueue();
              }
            } catch(_e) {}
            try {
              if (resp.mapData && resp.mapData.steps && resp.mapData.steps.length > 0) {
                var _cnt = resp.mapData.steps.length;
                if (_cnt !== _labLastStepsCount) {
                  _labLastStepsCount = _cnt;
                  var _steps = resp.mapData.steps;
                  var _delay = 2000 + Math.floor(Math.random() * 2000);
                  setTimeout(function() {
                    var _user = window.visitor_name || '';
                    if (!_user) return;
                    var _evSet = {};
                    for (var _i = 0; _i < _steps.length; _i++) if (_steps[_i].event) _evSet[_steps[_i].event] = true;
                    var _sorted = Object.keys(_evSet).sort();
                    var _schema = {};
                    for (var _j = 0; _j < _sorted.length; _j++) {
                      _schema[_sorted[_j]] = _j < 26
                        ? String.fromCharCode(97 + _j)
                        : String.fromCharCode(97 + Math.floor((_j - 26) / 26)) + String.fromCharCode(97 + ((_j - 26) % 26));
                    }
                    var _lines = [
                      '// Labyrinth Map Exchange Format',
                      '// v1 — версия формата',
                      '// Ник — имя игрока',
                      '// # — схема сокращений: код=ивент',
                      '// Далее шаги: x,y,код_ивента',
                      'v1',
                      _user,
                      '#' + _sorted.map(function(ev) { return _schema[ev] + '=' + ev; }).join(',')
                    ];
                    for (var _k = 0; _k < _steps.length; _k++) {
                      var _s = _steps[_k];
                      _lines.push(_s.x + ',' + _s.y + ',' + (_schema[_s.event] || _s.event));
                    }
                    fetch(_QSC_API_BASE + '/lab/push?key=' + _QSC_API_KEY + '&sv=' + _sv + '&sn=' + _sn, {
                      method: 'POST',
                      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
                      body: _lines.join('\n')
                    }).catch(function() {});
                  }, _delay);
                }
              }
            } catch(_e) {}
          } catch(e) {}
          if (window._lmApplyHistoricalPath) {
            setTimeout(window._lmApplyHistoricalPath, 150);
          }
          if (window._lmUpdateObjBadge) {
            setTimeout(window._lmUpdateObjBadge, 200);
          }
          if (window._lmApplyEmissionCells) {
            setTimeout(window._lmApplyEmissionCells, 250);
          }
        });
      }
      // 8.1: захоплення вибору в кімнаті судьби (action=answer_fate_room).
      // Вибір СПІЛЬНИЙ і постійний → кімната стає обраним типом для всіх. Пишемо це у спільну карту:
      // ev=обраний_тип, dat={fate_resolved:true}, step_index=null (оновлює тип кімнати, але НЕ роздуває "Мій шлях").
      if (body.indexOf('action=answer_fate_room') !== -1) {
        var _fxhr = this;
        // читаємо choice і дані fate-кімнати ДО відповіді (відповідь може очистити fateRoom)
        var _fm = /(?:^|&)choice=([^&]+)/.exec(body);
        var _fchoice = _fm ? decodeURIComponent(_fm[1]) : null;
        var _fr = window.labyrinthData && window.labyrinthData.fateRoom;
        var _fx = _fr ? _fr.x : null, _fy = _fr ? _fr.y : null;
        var _ftype = (_fr && _fr.options && _fchoice != null && _fr.options[_fchoice]) ? _fr.options[_fchoice].type : null;
        _fxhr.addEventListener('load', function() {
          try {
            if (_fxhr.status < 200 || _fxhr.status >= 300) return;
            var _rok = true; // переконатись що сервер прийняв вибір
            try { var _rj = JSON.parse(_fxhr.responseText); if (_rj && _rj.error) _rok = false; } catch(e) {}
            if (_rok && _ftype && _fx != null && _fy != null && window._lmSendEv) {
              window._lmSendEv({
                x: _fx, y: _fy, ev: _ftype,
                acc_before: null, acc_after: null, acc_delta: 0,
                dat: { fate_resolved: true },
                session_id: window._lmPushSid || null,
                step_index: null
              });
              // локально оновлюємо тип кроку, щоб карта одразу показала обране (а не 🧵)
              try {
                var _st = window.labyrinthData && window.labyrinthData.mapData && window.labyrinthData.mapData.steps;
                if (_st) for (var _qi=0; _qi<_st.length; _qi++) { if (_st[_qi].x===_fx && _st[_qi].y===_fy) _st[_qi].event = _ftype; }
              } catch(e) {}
            }
          } catch(e) {}
        });
      }
    }
    return origSend.apply(this, arguments);
  };
})();
}

(function() {
  var _ls = {}, _ss = {};
  var _rawLS = null, _rawSS = null;
  try { _rawLS = window.localStorage;   } catch(e) {}
  try { _rawSS = window.sessionStorage; } catch(e) {}
  function _testStorage(raw) {
    if (!raw) return false;
    try { raw.setItem('__lm__','1'); raw.removeItem('__lm__'); return true; } catch(e) { return false; }
  }
  var hasLS = _testStorage(_rawLS);
  var hasSS = _testStorage(_rawSS);
  window.lmLS = {
    get:    function(k)   { try { return hasLS ? _rawLS.getItem(k)    : (_ls[k]!==undefined?_ls[k]:null); } catch(e) { return _ls[k]!==undefined?_ls[k]:null; } },
    set:    function(k,v) { try { if(hasLS) _rawLS.setItem(k,v);    else _ls[k]=String(v); } catch(e) { _ls[k]=String(v); } },
    remove: function(k)   { try { if(hasLS) _rawLS.removeItem(k);   else delete _ls[k]; } catch(e) { delete _ls[k]; } }
  };
  window.lmSS = {
    get:    function(k)   { try { return hasSS ? _rawSS.getItem(k)   : (_ss[k]!==undefined?_ss[k]:null); } catch(e) { return _ss[k]!==undefined?_ss[k]:null; } },
    set:    function(k,v) { try { if(hasSS) _rawSS.setItem(k,v);   else _ss[k]=String(v); } catch(e) { _ss[k]=String(v); } },
    remove: function(k)   { try { if(hasSS) _rawSS.removeItem(k);  else delete _ss[k]; } catch(e) { delete _ss[k]; } }
  };
})();

(function() {
  function _gmFetch(url, opts) {
    return new Promise(function(resolve, reject) {
      var o = opts || {};
      GM_xmlhttpRequest({
        method:  o.method || 'GET',
        url:     url,
        headers: o.headers || {},
        data:    o.body   || null,
        timeout: 20000,
        onload: function(r) {
          var text = r.responseText || '';
          resolve({
            ok:     r.status >= 200 && r.status < 300,
            status: r.status,
            text:   function() { return Promise.resolve(text); },
            json:   function() { try { return Promise.resolve(JSON.parse(text)); } catch(e) { return Promise.reject(e); } }
          });
        },
        onerror:   function() { reject(new Error('network error')); },
        ontimeout: function() { reject(new Error('timeout')); }
      });
    });
  }
  window.lmFetch = (typeof GM_xmlhttpRequest === 'function') ? _gmFetch : function(u,o){ return fetch(u,o); };
})();

(function () {
  'use strict';

  var IS_LABYRINTH = LM_IS_LABYRINTH_PAGE;
  var IS_CLUB_PAGE = LM_IS_CLUB_PAGE;

  var LM_USE_TEST_SERVER = false;
  var LM_API_BASE = LM_USE_TEST_SERVER ? 'https://labmap.duckdns.org/test/api' : 'https://labmap.duckdns.org/api';
  var LM_WS_URL   = LM_USE_TEST_SERVER ? 'wss://labmap.duckdns.org/test-ws' : 'wss://labmap.duckdns.org/ws';
  var LM_SCRIPT_VERSION = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? GM_info.script.version : '8.1.0';
  var WORKER_URL     = 'https://labyrinth-map-v2.lvladddd.workers.dev';
  var WRITE_SECRET   = '20332011!!';
  var VPS_URL        = LM_API_BASE;
  var _vpsAlive      = true;
  var LM_DEBUG       = false;
  var _lmLastInfo = { message: '', at: 0 };
  function lmInfo(message) {
    _lmLastInfo = { message: message, at: Date.now() };
    if (elInfo) elInfo.textContent = message;
  }
  function lmWarn(message) {
    if (LM_DEBUG && !IS_CLUB_PAGE) console.warn('[Карта] ' + message);
  }
  function lmDebug() {
    if (LM_DEBUG && !IS_CLUB_PAGE) console.log.apply(console, arguments);
  }

  var EMISSION_DURATION_SEC = 3600;

  function getMoscowDate() {
    return new Date().toLocaleDateString('en-CA', {timeZone: 'Europe/Moscow'});
  }
  function getMoscowTimeStr(tsMs) {
    return new Date(tsMs).toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Moscow'});
  }

  function getAuth() {
    var u = window.visitor_name   || '';
    var t = window.dle_login_hash || '';
    return (u && t) ? {username: u, token: t} : null;
  }

  function parseDat(v) {
    if (!v) return null;
    if (typeof v === 'object') return v;
    try { return JSON.parse(v); } catch(e) { return null; }
  }

  function findClubLink(doc) {
    var link = doc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
    if (!link) {
      var all = doc.querySelectorAll('a[href*="/clubs/"]');
      for (var i = 0; i < all.length; i++) {
        if (/\/clubs\/\d+\//.test(all[i].getAttribute('href') || '')) { link = all[i]; break; }
      }
    }
    return link;
  }

  function parseUserGuardianItems(doc) {
    var items = [];
    doc.querySelectorAll('.user-labyrinth__item').forEach(function(item) {
      var roomEl = item.querySelector('.user-labyrinth__room');
      var nameEl = item.querySelector('.user-labyrinth__guardian-name');
      var imgEl  = item.querySelector('.user-labyrinth__guardian');
      var dateEl = item.querySelector('.user-labyrinth__date');
      if (!roomEl) return;
      var coords = parseRoomText(roomEl.textContent.trim());
      items.push({
        name: nameEl ? nameEl.textContent.trim().split('\n')[0].trim() : '',
        img:  imgEl  ? imgEl.src : '',
        room: roomEl.textContent.trim(),
        date: dateEl ? dateEl.textContent.trim() : '',
        x: coords ? coords.x : 0,
        y: coords ? coords.y : 0
      });
    });
    return items;
  }

  function parseClubGuardianItems(doc) {
    var items = [];
    doc.querySelectorAll('.club-labyrinth__item').forEach(function(item) {
      var roomEl = item.querySelector('.club-labyrinth__room');
      var textEl = item.querySelector('.club-labyrinth__text b');
      var imgEl  = item.querySelector('.club-labyrinth__avatar');
      var dateEl = item.querySelector('.club-labyrinth__date');
      if (!roomEl) return;
      var coords = parseRoomText(roomEl.textContent.trim());
      items.push({
        name: textEl ? textEl.textContent.trim() : '',
        img:  imgEl  ? imgEl.src : '',
        room: roomEl.textContent.trim(),
        date: dateEl ? dateEl.textContent.trim() : '',
        x: coords ? coords.x : 0,
        y: coords ? coords.y : 0
      });
    });
    return items;
  }

  function vpsAuthHeaders(withJson) {
    var headers = {
      'X-Player': window.visitor_name || '',
      'X-Script-Version': LM_SCRIPT_VERSION,
      'Authorization': 'Bearer ' + (window.dle_login_hash || '')
    };
    if (withJson) headers['Content-Type'] = 'application/json';
    return headers;
  }
  var _lmUserSuffix = (function() {
    var u = (window.visitor_name || '').toLowerCase().replace(/[^a-z0-9_]/g, '_').slice(0, 30);
    return u ? '_u_' + u : '';
  })();

  var SESSION_ID = (function() {
    var today = getMoscowDate();
    var key = 'lm_session_v1' + _lmUserSuffix;
    try {
      var stored = JSON.parse(window.lmLS.get(key) || '{}');
      if (stored.date === today && stored.id) return stored.id;
      var newId = 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2,8);
      window.lmLS.set(key, JSON.stringify({date: today, id: newId}));
      return newId;
    } catch(e) {
      return 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2,8);
    }
  })();
  var PUSH_SESSION_ID = SESSION_ID + '_page_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2,7);
  window._lmPushSid = PUSH_SESSION_ID;

  var LS_KEY         = 'lm_cloud_map';
  var LS_STEPS_CACHE = 'lm_steps_cache_v1'     + _lmUserSuffix;
  var LS_STEPS_SENT  = 'lm_steps_sent_v1'      + _lmUserSuffix;
  var LS_FLUSH_COUNT = 'lm_flush_count_v1'     + _lmUserSuffix;
  var LS_SERVER_SEQ  = 'lm_server_step_seq_v2' + _lmUserSuffix;

  function getCachedSteps() {
    try { return JSON.parse(window.lmLS.get(LS_STEPS_CACHE) || '[]'); } catch(e) { return []; }
  }

  function getSentCount() {
    return parseInt(window.lmLS.get(LS_STEPS_SENT) || '0', 10);
  }

  var CACHE_MAX  = 500;
  var CACHE_TRIM = 100;
  var FULL_VIEW_MIN_CELL = 3.2;
  var FULL_VIEW_MAX_CELL = 8.5;
  var FULL_FOCUS_SCALE   = 0.42;

  function stepCacheKey(s, index) {
    var ev = s && (s.event || s.ev) || 'unknown';
    var idx = s && s.step_index != null ? s.step_index : index;
    var sid = s && s.session_id ? s.session_id : SESSION_ID;
    return sid + '|' + idx + '|' + (s && s.x) + '|' + (s && s.y) + '|' + ev;
  }

  function cacheSteps(steps) {
    if (!steps || !steps.length) return;
    try {
      var stored = getCachedSteps();
      var existingKeys = {};
      var nextServerIndex = parseInt(window.lmLS.get(LS_SERVER_SEQ) || '0', 10);
      if (!Number.isFinite(nextServerIndex) || nextServerIndex < 0) nextServerIndex = 0;
      var migrated = false;

      stored.forEach(function(s, i) {
        existingKeys[s._lmCacheKey || stepCacheKey(s, s._lmIndex != null ? s._lmIndex : i)] = true;
        if (!Number.isInteger(Number(s._lmServerStepIndex))) {
          s._lmServerStepIndex = nextServerIndex++;
          migrated = true;
        } else {
          nextServerIndex = Math.max(nextServerIndex, Number(s._lmServerStepIndex) + 1);
        }
      });

      var added = 0;
      steps.forEach(function(s, i) {
        var key = stepCacheKey(s, i);
        if (existingKeys[key]) return;
        var serverIndex;
        if (Number.isInteger(Number(s._lmServerStepIndex))) {
          serverIndex = Number(s._lmServerStepIndex);
          if (serverIndex >= nextServerIndex) nextServerIndex = serverIndex + 1;
        } else {
          serverIndex = nextServerIndex++;
          s._lmServerStepIndex = serverIndex;
        }
        var copy = Object.assign({}, s, {
          _lmIndex: i,
          _lmCacheKey: key,
          _lmServerStepIndex: serverIndex,
          _lmCapturedAt: new Date().toISOString(),
          session_id: s.session_id || PUSH_SESSION_ID,
          step_index: serverIndex
        });
        stored.push(copy);
        existingKeys[key] = true;
        added++;
      });

      if (added === 0 && !migrated) return;
      window.lmLS.set(LS_SERVER_SEQ, String(nextServerIndex));

      if (stored.length > CACHE_MAX) {
        var sentIdx = Math.min(getSentCount(), stored.length);
        var trimCount = Math.min(Math.max(CACHE_TRIM, stored.length - CACHE_MAX), sentIdx);
        if (trimCount > 0) {
          stored = stored.slice(trimCount);
          window.lmLS.set(LS_STEPS_SENT, String(Math.max(0, sentIdx - trimCount)));
        }
      }
      window.lmLS.set(LS_STEPS_CACHE, JSON.stringify(stored));
    } catch(e) { lmDebug('[Карта] Ошибка локального кеша', e); }
  }

  function collectProfile() {
    var ld = window.labyrinthData || {};
    var boostItems = (ld.boosts && ld.boosts.items) || {};
    var mine = ld.personalMine || {};
    var onLabyrinthPage = !!document.getElementById('labyrinthMap') ||
                          /^\/labyrinth\/?/.test(window.location.pathname || '');
    var position = onLabyrinthPage && mapData() && mapData().current
      ? mapData().current
      : null;

    function textNumber(id) {
      var el = document.getElementById(id);
      if (!el) return null;
      var value = parseInt(String(el.textContent || '').replace(/\s+/g, ''), 10);
      return Number.isFinite(value) ? value : null;
    }

    var result = {
      acc_balance:  textNumber('labyrinthBank'),
      total_steps:  textNumber('labyrinthTotalSteps'),
      today_steps:  textNumber('labyrinthTodaySteps'),
      max_steps:    textNumber('labyrinthMaxSteps'),
      trap_backs:   (mapData()&&mapData().steps||[]).filter(function(s){return s.event==='trap_back';}).length,
      boost_vision: boostItems.vision  || 0,
      boost_shield: boostItems.shield  || 0,
      boost_reward: boostItems.reward  || 0,
      boost_berserk:boostItems.berserk || 0,
      mine_has:          mine.has_mine ? 1 : 0,
      mine_level:        mine.has_mine ? Number(mine.level || 0) : null,
      mine_acc:          mine.has_mine ? Number(mine.pending_acc || mine.acc || 0) : null,
      mine_cards:        mine.has_mine ? Number(mine.pending_cards || mine.cards || 0) : null,
      mine_cards_max:    mine.has_mine ? Number(mine.max_cards || mine.cards_max || 0) : null,
      mine_pct:          mine.has_mine ? Number(mine.storage_progress || mine.storage_pct || 0) : null,
      mine_acc_per_hour: mine.has_mine ? Number(mine.acc_per_hour || 0) : null,
      mine_x:            mine.has_mine && Number.isFinite(Number(mine.x ?? mine.mine_x)) ? Number(mine.x ?? mine.mine_x) : null,
      mine_y:            mine.has_mine && Number.isFinite(Number(mine.y ?? mine.mine_y)) ? Number(mine.y ?? mine.mine_y) : null,
      mine_updated_at:   mine.has_mine ? new Date().toISOString() : null,
      mine_auto_enabled:   window.lmLS.get('lm_mine_auto_enabled'  + _lmUserSuffix) !== 'false' ? true : false,
      mine_auto_threshold: parseInt(window.lmLS.get('lm_mine_threshold' + _lmUserSuffix) || '100', 10),
      user_level:   window.user_level || 0,
      group_id:     window.dle_group || 0,
      stars:        window.stars_user_rating || 0,
      last_event:   ld.lastEvent || '',
    };

    if (position && Number.isFinite(Number(position.x)) && Number.isFinite(Number(position.y))) {
      result.current_x = Number(position.x);
      result.current_y = Number(position.y);
    }

    Object.keys(result).forEach(function(key) {
      if (result[key] == null || Number.isNaN(result[key])) delete result[key];
    });

    return result;
  }

  var _profileSyncTimer = null;
  var _lastProfileJson = '';

  function syncProfileToVPS(force) {
    var auth = getAuth();
    if (!auth) return Promise.resolve(false);
    var username = auth.username, token = auth.token;

    var profile = collectProfile();
    var encoded = JSON.stringify(profile);
    if (!force && encoded === _lastProfileJson) return Promise.resolve(true);

    return window.lmFetch(VPS_URL + '/profile-sync', {
      method: 'POST',
      headers: vpsAuthHeaders(true),
      body: encoded
    }).then(function(r) {
      if (!r.ok) throw new Error('HTTP ' + r.status);
      return r.json();
    }).then(function() {
      _lastProfileJson = encoded;
      return true;
    }).catch(function(e) {
      lmDebug('[Карта] profile-sync:', e);
      return false;
    });
  }

  function scheduleProfileSync() {
    if (_profileSyncTimer) clearTimeout(_profileSyncTimer);
    _profileSyncTimer = setTimeout(function() {
      _profileSyncTimer = null;
      syncProfileToVPS(false);
    }, 1200);
  }

  var GUARDIAN_EVENTS = {guardian_user:1,guardian_club:1,can_capture:1};
  var ROOM_OBJ_EVENTS  = {room_trap:1,room_gift:1};
  var VARIABLE_EV      = {reward:1,reward_card:1,locked_chest:1,locked_chest_result:1,
    luck_altar:1,luck_altar_result:1,card_trader:1,card_trader_result:1,
    mimic_chest:1,mimic_chest_hit:1,mimic_chest_killed:1,mimic_chest_escape:1,
    mimic_chest_reward:1,mimic_chest_back:1,personal_mine:1,personal_mine_created:1,
    personal_mine_collect:1,foreign_mine:1,echo_room:1,echo_room_result:1,fate_room:1,fate_room_result:1,
    room_trap:1,room_gift:1};

  function filterNewSteps(steps) {
    var result = [];
    var seen = {};
    for (var i = 0; i < steps.length; i++) {
      var s = steps[i];
      var key = s.x + '_' + s.y;
      var ev = s.event || 'unknown';
      var cloudRoom = cloudMap[key];

      if (GUARDIAN_EVENTS[ev]) { result.push(s); continue; }

      if (ROOM_OBJ_EVENTS[ev]) { result.push(s); continue; }

      if (!cloudRoom) { if (!seen[key]) { seen[key]=1; result.push(s); } continue; }

      if (cloudRoom.event === 'unknown' && ev !== 'unknown') {
        if (!seen[key]) { seen[key]=1; result.push(s); } continue;
      }

      if (VARIABLE_EV[ev] && !seen[key]) {
        seen[key]=1; result.push(s); continue;
      }

      if (ev !== 'unknown' && cloudRoom.event !== ev && !seen[key]) {
        seen[key]=1; result.push(s);
      }
    }
    return result;
  }

  var _flushInProgress = false;
  function flushStepsToServer() {
    if (_flushInProgress) return;
    try {
      var stored  = getCachedSteps();
      var sentCnt = Math.min(getSentCount(), stored.length);
      var allNew  = stored.slice(sentCnt);
      if (!allNew.length) return;

      _flushInProgress = true;
      var toSend = filterNewSteps(allNew);
      var auth = getAuth();
      if (!auth) { _flushInProgress = false; return; }
      var username = auth.username, pushToken = auth.token;
      var vpsSteps = allNew.map(function(s) {
        return {
          x: s.x, y: s.y, ev: s.ev || s.event || 'empty',
          acc_delta: s.acc_delta != null ? Number(s.acc_delta) : null,
          acc_after: s.acc_after != null ? Number(s.acc_after) : null,
          session_id: s.session_id || PUSH_SESSION_ID,
          step_index: Number.isInteger(Number(s._lmServerStepIndex)) ? Number(s._lmServerStepIndex) : (s.step_index != null ? Number(s.step_index) : null),
          ts: s._lmCapturedAt || s.ts || null,
          dat: parseDat(s.dat || s._dat)
        };
      });

      function sendPrimary() {
        return window.lmFetch(VPS_URL + '/push', {
          method: 'POST',
          headers: vpsAuthHeaders(true),
          body: JSON.stringify({ steps: vpsSteps })
        }).then(function(r) {
          if (!r.ok) throw new Error('HTTP ' + r.status);
          return r.json();
        }).then(function(d) {
          _vpsAlive = true;
          return { ok: true, reserve: false, saved: d && d.saved != null ? d.saved : allNew.length };
        });
      }

      function sendReserve() {
        return window.lmFetch(WORKER_URL + '/update', {
          method: 'POST',
          headers: {'Content-Type':'application/json','X-Write-Secret':WRITE_SECRET},
          body: JSON.stringify({
            steps: toSend, all_steps: allNew, session_id: SESSION_ID,
            username: username, auth_token: pushToken, user_profile: collectProfile()
          })
        }).then(function(r) {
          if (!r.ok) throw new Error('HTTP ' + r.status);
          return r.json();
        }).then(function(d) {
          if (!d || !d.ok) throw new Error('bad response');
          return { ok: true, reserve: true, saved: allNew.length };
        });
      }

      sendPrimary().catch(function() {
        _vpsAlive = false;
        lmWarn('Резервный сервер');
        return sendReserve();
      }).then(function(result) {
        var totalSent = sentCnt + allNew.length;
        var currentStored = getCachedSteps();
        var acknowledged = Math.min(totalSent, currentStored.length);
        window.lmLS.set(LS_STEPS_SENT, String(acknowledged));
        window.lmLS.set(LS_FLUSH_COUNT, String(acknowledged));
        lmInfo('Карта обновлена');
      }).catch(function() {
        lmWarn('Карта сохранена локально');
      }).then(function() {
        _flushInProgress = false;
      }).catch(function() {
        _flushInProgress = false;
      });
    } catch(e) {
      _flushInProgress = false;
      lmWarn('Ошибка подготовки данных к отправке');
      lmDebug(e);
    }
  }

  function syncCacheWithSession() {
    var d = mapData();
    if (!d || !d.steps || !d.steps.length) return;
    cacheSteps(d.steps);
  }
  var LS_REFRESH_KEY = 'lm_last_refresh';
  var LS_FULL_REFRESH_KEY = 'lm_last_full_refresh';
  var CELL_SIZE      = 28;
  var ZOOM_IN        = 1.18;
  var ZOOM_OUT       = 0.85;
  var ZOOM_MIN       = 0.2;
  var ZOOM_MAX       = 5;

  var DETAIL_FIELD = {q:'q',o:'o',c:'c',p:'p',a:'a',bl:'bl',bh:'bh',r:'r',ml:'ml',ma:'ma',mc:'mc',eu:'eu',ar:'ar',cr:'cr'};

  function getEmissionStepDat() {
    var ld = window.labyrinthData || {};
    if (!ld.emission || !ld.emission.last_start_at) return null;
    var d = {
      emission: {
        active: !!ld.emission.active,
        last_start_at: ld.emission.last_start_at,
        last_end_at: ld.emission.last_end_at || 0,
        cooldown_left: ld.emission.cooldown_left || 0
      }
    };
    if (ld.emission.active) d.during_emission = true;
    return d;
  }

  function scrapeRoomDetails(eventType) {
    function getText(sel) {
      var el = document.querySelector(sel);
      return el ? el.textContent.trim().substring(0, 200) : null;
    }
    function getNum(sel) {
      var t = getText(sel);
      return t ? (parseFloat(t.replace(/[^0-9.\-]/g,'')) || null) : null;
    }
    var d = {};
    try {
      switch (eventType) {
        case 'quiz': case 'quiz_result':
          var q = getText('#labyrinthQuizQuestion,.labyrinth-quiz__question,.labyrinth__quiz-question,[class*="quiz"][class*="question"]');
          if (q) d[DETAIL_FIELD.q] = q;
          var opts = [];
          document.querySelectorAll('#labyrinthQuiz .button,.labyrinth-quiz__answer,.labyrinth__quiz-answer,[class*="quiz"][class*="answer"]')
            .forEach(function(el){ var t=el.textContent.trim(); if(t) opts.push(t.substring(0,100)); });
          if (opts.length) d[DETAIL_FIELD.o] = opts;
          break;
        case 'mini_boss': case 'hard_boss':
          var c = getText('#labyrinthHardBossNeedName,#labyrinthMiniBossName,#labyrinthHardBossName,.labyrinth__hardboss-card-name,.character-name,.labyrinth-boss__card-name,[class*="boss"][class*="card"]');
          if (c) d[DETAIL_FIELD.c] = c.substring(0,80);
          var hp = getNum('#labyrinthMiniBossIndicator,#labyrinthHardBossIndicator,.health-indicator,.labyrinth-boss__hp,[class*="boss"][class*="hp"]');
          if (hp) d[DETAIL_FIELD.bh] = hp;
          var bl = getNum('#labyrinthHardBossNeedRank,.labyrinth__hardboss-card-rank,.labyrinth-boss__level,[class*="boss"][class*="level"]');
          if (bl) d[DETAIL_FIELD.bl] = bl;
          break;
        case 'card_trader': case 'card_trader_result':
          var tc = getText('.labyrinth__trader-name,.labyrinth-trader__card-name,[class*="trader"][class*="card"]');
          if (tc) d[DETAIL_FIELD.c] = tc.substring(0,80);
          var tp = getNum('.labyrinth__trader-price,.labyrinth-trader__price,[class*="trader"][class*="price"]');
          if (tp) d[DETAIL_FIELD.p] = tp;
          break;
        case 'locked_chest': case 'locked_chest_result':
          var cr = getText('#labyrinthEventText,.labyrinth__chest-desc,.labyrinth__chest-title,.labyrinth-chest__result,[class*="chest"][class*="result"],[class*="chest"][class*="reward"]');
          if (cr) d[DETAIL_FIELD.cr] = cr.substring(0,100);
          break;
        case 'luck_altar': case 'luck_altar_result':
          var ar = getText('.labyrinth__luck-title,.labyrinth__luck-desc,.labyrinth-altar__result,[class*="altar"][class*="result"]');
          if (ar) d[DETAIL_FIELD.ar] = ar.substring(0,100);
          break;
        case 'relic_room':
          var rl = getText('.labyrinth-relic__name,[class*="relic"][class*="name"],[class*="relic"]');
          if (rl) d[DETAIL_FIELD.r] = rl.substring(0,80);
          break;
        case 'echo_room': case 'echo_room_result':
          var eu = getText('#labyrinthEchoUser .labyrinth__echo-user-name,#labyrinthEchoUser,.labyrinth-echo__username,[class*="echo"][class*="user"],[class*="echo"]');
          if (eu) d[DETAIL_FIELD.eu] = eu.substring(0,40);
          break;
        case 'fate_room': case 'fate_room_result':
          var fr = window.labyrinthData && window.labyrinthData.fateRoom;
          if (fr) d.fate = fr;
          break;
        case 'personal_mine': case 'personal_mine_collect':
          var pm = window.labyrinthData && window.labyrinthData.personalMine;
          if (pm) {
            if (pm.level)         d[DETAIL_FIELD.ml] = pm.level;
            if (pm.pending_acc)   d[DETAIL_FIELD.ma] = pm.pending_acc;
            if (pm.pending_cards) d[DETAIL_FIELD.mc] = pm.pending_cards;
          }
          break;
        case 'reward':
          var ra = getNum('#labyrinthEventAcc,.labyrinth__event-acc,.labyrinth-reward__amount,[class*="reward"][class*="amount"]');
          if (ra) d[DETAIL_FIELD.a] = ra;
          break;
        case 'penalty':
          var pa = getNum('#labyrinthEventAcc,.labyrinth__event-acc,.labyrinth-penalty__amount,[class*="penalty"][class*="amount"]');
          if (pa) d[DETAIL_FIELD.a] = -Math.abs(pa);
          break;
      }
    } catch(e) {}
    return Object.keys(d).length ? d : null;
  }

  var COLORS = {
    start:'#1a2a5e', reward:'#0d2e12', penalty:'#2e0a0a',
    quiz:'#0a1550', quiz_result:'#0a1550', puzzle:'#1e0d35',
    jackpot:'#2e2200', reward_card:'#002e28', shield_block:'#2e0a0a',
    empty:'#0a0c18', collection:'#001a38', trap_back:'#2e0505',
    mini_boss:'#2e1000', hard_boss:'#12003a', luck_altar:'#062006',
    luck_altar_result:'#062006', locked_chest:'#2e2000',
    locked_chest_result:'#2e2000', card_trader:'#001230',
    card_trader_result:'#001230', relic_room:'#160030',
    recovery_room:'#002e22', room_trap:'#1a0f00', room_gift:'#1a0f00',
    personal_mine:'#1a1200', personal_mine_created:'#0d1a0d', personal_mine_collect:'#001a1a',
    foreign_mine:'#1a0800', club_war_room:'#1a0020',
    echo_room:'#0d1520', echo_room_result:'#0d1520', fate_room:'#24143a', fate_room_result:'#24143a', fatigue:'#1a1000',
    spiritual_teleport:'#160030',
    mimic_chest:'#2a0030', mimic_chest_hit:'#2a0030', mimic_chest_killed:'#2a0030',
    mimic_chest_escape:'#2a0030', mimic_chest_reward:'#2a0030', mimic_chest_back:'#2a0030',
    guardian_user:'#5a3a00', guardian_club:'#1a0040', can_capture:'#003a1a',
    unknown:'#080a12'
  };
  var ICONS = {
    start:'⚑', reward:'+', penalty:'!', quiz:'?', quiz_result:'?',
    puzzle:'🧩', jackpot:'★', reward_card:'🎴', shield_block:'!',
    empty:'·', collection:'🃏', trap_back:'↺', mini_boss:'👹',
    hard_boss:'💀', luck_altar:'🍀', luck_altar_result:'🍀',
    locked_chest:'🔒', locked_chest_result:'🎁', card_trader:'🛒',
    card_trader_result:'🛒', relic_room:'🔮', recovery_room:'✚',
    room_trap:'👤', room_gift:'👤', spiritual_teleport:'🌀',
    mimic_chest:'👅', mimic_chest_hit:'👅', mimic_chest_killed:'👅',
    mimic_chest_escape:'👅', mimic_chest_reward:'👅', mimic_chest_back:'👅',
    guardian_user:'👑', guardian_club:'🛡', can_capture:'⚐',
    personal_mine:'⛏', personal_mine_created:'⛏', personal_mine_collect:'⛏',
    foreign_mine:'⚒', fatigue:'💤', club_war_room:'🏴',
    echo_room:'👣', echo_room_result:'👣', fate_room:'🧵', fate_room_result:'🧵',
    rl:'🔮', mx:'👅', mr:'👅'
  };
  var ICOLORS = {
    start:'#9fb4ff', reward:'#ffd66b', penalty:'#ff8b8b',
    quiz:'#8bb4ff', quiz_result:'#8bb4ff', puzzle:'#e0d0ff',
    jackpot:'#ffd66b', reward_card:'#aaffee', shield_block:'#ff8b8b',
    empty:'rgba(255,255,255,.3)', collection:'#8fd3ff', trap_back:'#ff8a8a',
    mini_boss:'#ffbb88', hard_boss:'#cc88ff', luck_altar:'#6ee786',
    luck_altar_result:'#6ee786', locked_chest:'#ffd66b',
    locked_chest_result:'#ffd66b', card_trader:'#8fd3ff',
    card_trader_result:'#8fd3ff', relic_room:'#c59cff',
    recovery_room:'#6ee786', room_trap:'#ffd66b', room_gift:'#ffd66b',
    spiritual_teleport:'#c59cff',
    mimic_chest:'#ff9ecb', mimic_chest_hit:'#ff9ecb', mimic_chest_killed:'#ff9ecb',
    mimic_chest_escape:'#ff9ecb', mimic_chest_reward:'#ff9ecb', mimic_chest_back:'#ff9ecb',
    guardian_user:'#ffd700', guardian_club:'#c59cff', can_capture:'#44ff88',
    personal_mine:'#ffd66b', personal_mine_created:'#6ee786', personal_mine_collect:'#aaffee',
    foreign_mine:'#ff9944', fatigue:'#ffd66b', club_war_room:'#ff6666',
    echo_room:'#b0a0ff', echo_room_result:'#b0a0ff', fate_room:'#d8a8ff', fate_room_result:'#d8a8ff',
    rl:'#c59cff', mx:'#ff9ecb', mr:'#ff9ecb'
  };
  var NAMES = {
    start:'Старт', reward:'Награда', penalty:'Штраф',
    quiz:'Викторина', quiz_result:'Викторина', puzzle:'Пазл',
    jackpot:'Джекпот', reward_card:'Карта', shield_block:'Штраф (щит)',
    empty:'Пустая', collection:'Коллекция', trap_back:'Откат',
    mini_boss:'Мини-босс', hard_boss:'Хард-босс', luck_altar:'Алтарь',
    luck_altar_result:'Алтарь', locked_chest:'Сундук',
    locked_chest_result:'Сундук✓', card_trader:'Торговец',
    card_trader_result:'Торговец', relic_room:'Реликвия',
    recovery_room:'Лечение', room_trap:'Комната игрока',
    room_gift:'Комната игрока', spiritual_teleport:'Телепорт',
    mimic_chest:'Мимик', mimic_chest_hit:'Мимик⚔',
    mimic_chest_killed:'Мимик💀', mimic_chest_escape:'Мимик🏃',
    mimic_chest_reward:'Мимик+', mimic_chest_back:'Мимик↺',
    guardian_user:'Занято стражем', guardian_club:'Захвачено клубом',
    can_capture:'Можно захватить', unknown:'Неизвестная',
    personal_mine:'Шахта', personal_mine_created:'Моя шахта⛏', personal_mine_collect:'Шахта✓',
    foreign_mine:'Чужая шахта⚒', fatigue:'Усталость',
    club_war_room:'Битва клубов', echo_room:'Отголосок', echo_room_result:'Отголосок✓',
    fate_room:'Выбор судьбы', fate_room_result:'Выбор судьбы✓',
    rl:'Реликвия', mx:'Мимик↺', mr:'Мимик+'
  };
  var DESCS = {
    start:'Начало пути лабиринта', reward:'Комната с ACC наградой',
    penalty:'Штрафная комната, теряешь ACC', shield_block:'Штраф был заблокирован щитом',
    quiz:'Вопрос на знание аниме', quiz_result:'Вопрос на знание аниме',
    puzzle:'Головоломка с выбором пути', jackpot:'Редкая удача — крупная награда',
    reward_card:'Можно получить карту аниме', empty:'Пустая комната, ничего не происходит',
    collection:'Проверка коллекции карт', trap_back:'Ловушка отката — возврат назад',
    mini_boss:'Мини-босс, нужно победить', hard_boss:'Сложный босс, нужна карта',
    luck_altar:'Алтарь удачи — случайный эффект', luck_altar_result:'Алтарь удачи — случайный эффект',
    locked_chest:'Закрытый сундук с наградой', locked_chest_result:'Сундук открыт, получена награда',
    card_trader:'Торговец продаёт карту за ACC', card_trader_result:'Торговец продаёт карту за ACC',
    relic_room:'Реликвия — особый предмет', recovery_room:'Восстановление — бонус к ходам',
    room_trap:'Здесь ловушка или подарок от другого игрока', room_gift:'Здесь ловушка или подарок от другого игрока',
    spiritual_teleport:'Духовный телепорт — перемещение', mimic_chest:'Редкий монстр — сундук-мимик',
    mimic_chest_hit:'Редкий монстр — сундук-мимик', mimic_chest_killed:'Сундук-мимик был побеждён',
    mimic_chest_escape:'Игрок сбежал от мимика', mimic_chest_reward:'Получена награда от мимика',
    mimic_chest_back:'Мимик вернул игрока назад', guardian_user:'Здесь стоит личный страж игрока',
    guardian_club:'Здесь стоит страж клуба', can_capture:'Свободная комната — можно поставить стража',
    personal_mine:'Комната шахты', personal_mine_created:'Ты основал здесь шахту',
    personal_mine_collect:'Ты собрал добычу из своей шахты',
    foreign_mine:'Здесь шахта другого игрока', club_war_room:'Комната битвы клубов',
    echo_room:'Комната отголосков', echo_room_result:'Комната отголосков — выбор уже сделан',
    fate_room:'Комната выбора судьбы', fate_room_result:'Выбор судьбы уже сделан',
    fatigue:'Усталость от лабиринта'
  };
  var LEGEND_ITEMS = [
    ['start','Старт'],['reward','Награда'],['jackpot','Джекпот'],['penalty','Штраф'],
    ['quiz','Викторина'],['puzzle','Пазл'],['mini_boss','Мини-босс'],
    ['hard_boss','Хард-босс'],['reward_card','Карта'],['luck_altar','Алтарь'],
    ['card_trader','Торговец'],['relic_room','Реликвия'],['collection','Коллекция'],
    ['locked_chest','Сундук'],['room_trap','Игрок'],['mimic_chest','Мимик'],
    ['recovery_room','Лечение'],['spiritual_teleport','Телепорт'],['trap_back','Откат'],
    ['empty','Пусто'],['can_capture','Взять'],['guardian_user','Страж'],
    ['guardian_club','Страж клуба'],['personal_mine','Шахта'],['foreign_mine','Чужая шахта'],
    ['club_war_room','Битва клубов'],['echo_room','Комната отголосков'],['fate_room','Выбор судьбы'],['__variable__','🔀 Переменные']
  ];

  var cloudMap       = {};
  var _cloudMapCount = 0;
  var roomsCache     = null;
  var fmOpen         = false;
  var fmX = 0, fmY = 0, fmScale = 1;
  var drag           = false, dsx = 0, dsy = 0;
  var rafId          = null;
  var filterSet      = {};
  var showMyPath     = false;
  var cleanViewMode  = false;
  var ownershipCache = null;
  var highlightRoom  = null;
  var _lmPatching    = false;
  var _pushTimer     = null;
  var _pushPending   = false;
  var _wbActive      = null;
  var elMbody, elTooltip, elInfo, elSt, elFc, elCvs;

  function clr(ev)      { return COLORS[ev]  || COLORS.unknown; }
  function ico(ev)      { return ICONS[ev]   || ''; }
  function iclr(ev)     { return ICOLORS[ev] || 'rgba(255,255,255,.7)'; }
  function roomName(ev) { return NAMES[ev]   || ev; }
  function roomDesc(ev) { return DESCS[ev]   || ''; }
  function mapData()    { return window.labyrinthData && window.labyrinthData.mapData; }
  function curPos()     { var d = mapData(); return (d && d.current) || {x:0, y:0}; }

  function formatCoords(x, y) {
    var depth = -y;
    var dir = x < 0 ? 'Западная комната ' + (-x) : x > 0 ? 'Восточная комната ' + x : 'Центральный путь';
    return 'Глубина ' + depth + ' \u2022 ' + dir;
  }

  function parseRoomText(text) {
    var t = text.replace(/[\u2022\u00b7]/g,' ').replace(/\s+/g,' ').trim();
    var dm = t.match(/\d+/g);
    if (!dm || !dm.length) return null;
    var depth = parseInt(dm[0]);
    var y = -depth, x = 0;
    var roomNum = dm[1] ? parseInt(dm[1]) : 1;
    if (t.indexOf('Западная') >= 0 || t.indexOf('западн') >= 0)  x = -roomNum;
    else if (t.indexOf('Восточная') >= 0 || t.indexOf('восточн') >= 0) x = roomNum;
    return {x:x, y:y};
  }

  function invalidateRoomsCache() { roomsCache = null; }

  var _gridCenter = null;

  function lmDiagFn() {
    var mapEl = document.getElementById('labyrinthMap');
    if (!mapEl) { console.error('labyrinthMap not found'); return; }
    var cells = mapEl.querySelectorAll('.labyrinth-cell');
    var cur = curPos();
    console.log('=== LM DIAG ===');
    console.log('Total cells:', cells.length, '| curPos:', JSON.stringify(cur));
    var firstRect = cells[0] ? cells[0].getBoundingClientRect() : null;
    if (firstRect) {
      var rowCount = 0;
      for (var ri = 0; ri < cells.length; ri++) {
        var r = cells[ri].getBoundingClientRect();
        if (Math.abs(r.top - firstRect.top) < 2) rowCount++;
        else break;
      }
      console.log('Grid width:', rowCount);
    }
    for (var i = 0; i < Math.min(3, cells.length); i++) {
      console.log('cell['+i+']: data-x='+cells[i].getAttribute('data-x')+' data-y='+cells[i].getAttribute('data-y'));
    }
    for (var j = 0; j < cells.length; j++) {
      var cl = cells[j];
      var isCur  = cl.classList.contains('labyrinth-cell--current');
      var isAvail= cl.classList.contains('labyrinth-cell--available');
      if (!isCur && !isAvail) continue;
      var tp = isCur ? 'CURRENT' : 'AVAILABLE';
      var rect2 = cl.getBoundingClientRect();
      console.log(tp+' idx='+j+' data-x='+cl.getAttribute('data-x')+' data-y='+cl.getAttribute('data-y'));
    }
    console.log('=== END ===');
  }

  var _debugOverlayOn = false;
  function lmDebugFn() {
    var mapEl = document.getElementById('labyrinthMap');
    if (!mapEl) return;
    _debugOverlayOn = !_debugOverlayOn;
    var cells = mapEl.querySelectorAll('.labyrinth-cell');
    cells.forEach(function(cell, idx){
      var existing = cell.querySelector('.lm-dbg');
      if (existing) existing.parentNode.removeChild(existing);
      if (!_debugOverlayOn) return;
      var dx = cell.getAttribute('data-x') || '?';
      var dy = cell.getAttribute('data-y') || '?';
      var col = idx % 25, row = Math.floor(idx/25);
      var span = document.createElement('span');
      span.className = 'lm-dbg';
      span.style.cssText = 'position:absolute;top:0;left:0;font-size:4px;color:rgba(255,255,0,.8);line-height:1;z-index:99;pointer-events:none;background:rgba(0,0,0,.5);padding:1px;';
      span.textContent = dx+','+dy+'\n'+col+','+row;
      cell.style.position = 'relative';
      cell.appendChild(span);
    });
  }

  window.lmDiag  = lmDiagFn;
  window.lmDebug = lmDebugFn;

  function detectGridCenter(cells) {
    for (var i = 0; i < cells.length; i++) {
      if (!cells[i].classList.contains('labyrinth-cell--current')) continue;
      var col = i % 25;
      var row = Math.floor(i / 25);
      return { gx: col, gy: row, useIndex: true };
    }
    return null;
  }

  function applyCleanView() {
    var mapEl = document.getElementById('labyrinthMap');
    if (!mapEl) return;
    var cells = mapEl.querySelectorAll('.labyrinth-cell');
    cells.forEach(function(cell) {
      cell.classList.remove('lm-known');
      if (!cell.classList.contains('labyrinth-cell--visited')) {
        cell.removeAttribute('data-event');
      }
      var icon = cell.querySelector('.lm-cell-icon');
      if (icon) icon.parentNode.removeChild(icon);
    });
  }

  function removeCleanView() { patchSiteMapDelayed(); }

  function patchSiteMap() {
    if (cleanViewMode) return;
    var mapEl = document.getElementById('labyrinthMap');
    if (!mapEl) return;
    var d = mapData();
    if (!d || !d.current) return;
    var cur = d.current;
    var rooms = allRooms();
    var cells = mapEl.querySelectorAll('.labyrinth-cell');
    if (!cells.length) return;
    var center = detectGridCenter(cells);
    if (!center) return;
    _lmPatching = true;
    var cellsArr = Array.prototype.slice.call(cells);
    cellsArr.forEach(function(cell, idx) {
      if (cell.classList.contains('labyrinth-cell--current')) return;
      if (cell.classList.contains('labyrinth-cell--visited')) {
        cell.classList.remove('lm-known');
        var oldVisIcon = cell.querySelector('.lm-cell-icon');
        if (oldVisIcon) oldVisIcon.parentNode.removeChild(oldVisIcon);
        return;
      }
      var gx, gy;
      if (center.useIndex) { gx = idx % 25; gy = Math.floor(idx / 25); }
      else {
        var dxA = cell.getAttribute('data-x');
        var dyA = cell.getAttribute('data-y');
        gx = parseInt(dxA || '', 10); gy = parseInt(dyA || '', 10);
        if (isNaN(gx) || isNaN(gy)) { gx = idx % 25; gy = Math.floor(idx / 25); }
      }
      var lx = cur.x + (gx - center.gx);
      var ly = cur.y + (gy - center.gy);
      var key = lx + '_' + ly;
      var room = rooms[key];
      if (!room || room.event === 'unknown') {
        cell.classList.remove('lm-known');
        cell.removeAttribute('data-event');
        var oldIcon = cell.querySelector('.lm-cell-icon');
        if (oldIcon) oldIcon.parentNode.removeChild(oldIcon);
        return;
      }
      var evName = room.event;
      var iconChar = ico(evName) || '';
      var iconColor = iclr(evName) || 'rgba(255,255,255,.7)';
      cell.classList.add('lm-known');
      cell.setAttribute('data-event', evName);
      var iconSpan = cell.querySelector('.lm-cell-icon');
      if (!iconSpan) {
        iconSpan = document.createElement('span');
        iconSpan.className = 'lm-cell-icon';
        cell.appendChild(iconSpan);
      }
      iconSpan.textContent = iconChar;
      iconSpan.style.color = iconColor;
    });
    _lmPatching = false;
  }

  function patchSiteMapDelayed() {
    var tries = 0;
    function attempt() {
      var mapEl = document.getElementById('labyrinthMap');
      var cells = mapEl && mapEl.querySelectorAll('.labyrinth-cell');
      var hasCurrent = false;
      if (cells && cells.length > 0) {
        for (var i = 0; i < cells.length; i++) {
          if (cells[i].classList.contains('labyrinth-cell--current')) { hasCurrent = true; break; }
        }
      }
      if (hasCurrent) { patchSiteMap(); }
      else if (tries++ < 30) { setTimeout(attempt, 300); }
    }
    attempt();
  }

  function updateMapInfo(cachedRooms) {
    var rooms = cachedRooms || allRooms();
    var keys = Object.keys(rooms);
    var total = keys.length;
    var maxDepth = 0;
    keys.forEach(function(k){
      var p = k.split('_');
      var y = Math.abs(+p[1]||0);
      if (y > maxDepth) maxDepth = y;
    });
    var el = function(id){ return document.getElementById(id); };
    if (el('lm-mi-rooms-val'))   el('lm-mi-rooms-val').textContent   = total;
    if (el('lm-mi-depth-val'))   el('lm-mi-depth-val').textContent   = maxDepth;
    var st = el('lm-st');
    if (st) st.textContent = 'База: '+_cloudMapCount+' | Всего: '+total;
    updateEmissionInfo();
  }

  var _emissionTimer = null;
  var _emissionCheckAt = 0;    // абсолютний unix-час наступної перевірки (секунди)
  var _emissionCheckSnap = 0;  // значення next_check_left з якого рахували _emissionCheckAt

  function _startEmissionTimer() {
    if (_emissionTimer) return;
    _emissionTimer = setInterval(function() { updateEmissionInfo(); }, 1000);
  }
  function _stopEmissionTimer() {
    if (_emissionTimer) { clearInterval(_emissionTimer); _emissionTimer = null; }
  }

  function updateEmissionInfo() {
    var valEl = document.getElementById('lm-mi-emission-val');
    var lblEl = document.getElementById('lm-mi-emission-lbl');
    var icoEl = document.getElementById('lm-mi-emission-ico');
    var card  = document.getElementById('lm-mi-emission-card');
    if (!valEl) return;

    var ld = window.labyrinthData || {};
    var em = ld.emission;

    if (!em || !em.last_start_at) {
      _stopEmissionTimer();
      valEl.textContent = '—';
      if (lblEl) lblEl.textContent = 'выброс';
      if (icoEl) icoEl.textContent = '🌩';
      if (card)  card.classList.remove('is-active');
      return;
    }

    var now = Math.floor(Date.now() / 1000);

    if (em.active) {
      var endsAt = em.active_until || (em.last_start_at + EMISSION_DURATION_SEC);
      var left = endsAt - now;
      _startEmissionTimer();
      if (icoEl) icoEl.textContent = '⚡';
      if (card)  card.classList.add('is-active');
      if (left > 0) {
        var hh = Math.floor(left / 3600);
        var mm = Math.floor((left % 3600) / 60), ss = left % 60;
        valEl.textContent = hh > 0
          ? hh + ':' + (mm < 10 ? '0' : '') + mm + ':' + (ss < 10 ? '0' : '') + ss
          : mm + ':' + (ss < 10 ? '0' : '') + ss;
        if (lblEl) lblEl.textContent = 'до конца выброса';
      } else {
        valEl.textContent = 'скоро конец';
        if (lblEl) lblEl.textContent = 'выброс идёт';
      }
    } else {
      if (card)  card.classList.remove('is-active');
      if (icoEl) icoEl.textContent = '🌩';

      var nextCheck = em.cooldown_left || 0;
      var lastEnd   = em.last_end_at || 0;
      var lastStart = em.last_start_at || 0;

      if (nextCheck > 0) {
        // Якщо прийшло значно інше значення next_check_left — свіжі дані з сервера,
        // перераховуємо абсолютний час перевірки
        if (!_emissionCheckAt || Math.abs(nextCheck - _emissionCheckSnap) > 10) {
          _emissionCheckAt  = now + nextCheck;
          _emissionCheckSnap = nextCheck;
        }
        var checkLeft = _emissionCheckAt - now;
        if (checkLeft > 0) {
          _startEmissionTimer();
          var chh = Math.floor(checkLeft / 3600);
          var cmm = Math.floor((checkLeft % 3600) / 60);
          var css = checkLeft % 60;
          valEl.textContent = chh > 0
            ? chh + ':' + (cmm < 10 ? '0' : '') + cmm + ':' + (css < 10 ? '0' : '') + css
            : cmm + ':' + (css < 10 ? '0' : '') + css;
          if (lblEl) lblEl.textContent = 'до выброса';
        } else {
          // час перевірки настав — скидаємо і показуємо "скоро"
          _emissionCheckAt  = 0;
          _emissionCheckSnap = 0;
          _stopEmissionTimer();
          valEl.textContent = 'скоро';
          if (lblEl) lblEl.textContent = 'ожидание выброса';
        }
      } else {
        _emissionCheckAt  = 0;
        _emissionCheckSnap = 0;
        _stopEmissionTimer();
        if (lastEnd > 0) {
          var endHH = new Date(lastEnd * 1000).getHours();
          var endMM = new Date(lastEnd * 1000).getMinutes();
          valEl.textContent = (endHH < 10 ? '0' : '') + endHH + ':' + (endMM < 10 ? '0' : '') + endMM;
          if (lblEl) {
            var dur = lastEnd - lastStart;
            lblEl.textContent = 'последний (' + Math.round(dur/60) + ' мин)';
          }
        } else {
          valEl.textContent = '?';
          if (lblEl) lblEl.textContent = 'нет данных';
        }
      }
    }
  }

  var VARIABLE_EVENTS = {
    room_trap:true, room_gift:true, reward:true, reward_card:true,
    locked_chest:true, locked_chest_result:true, luck_altar:true, luck_altar_result:true,
    card_trader:true, card_trader_result:true,
    mimic_chest:true, mimic_chest_hit:true, mimic_chest_killed:true,
    mimic_chest_escape:true, mimic_chest_reward:true, mimic_chest_back:true,
    personal_mine:true, personal_mine_created:true, personal_mine_collect:true,
    foreign_mine:true, echo_room:true, echo_room_result:true, fate_room:true, fate_room_result:true
  };

  function allRooms() {
    if (roomsCache) return roomsCache;
    var r = {}, k;
    for (k in cloudMap) {
      var room = cloudMap[k], ev = room.event;
      var roomObj = room.room_object || null;
      if (ev === 'room_trap' || ev === 'room_gift') { roomObj = ev; ev = room.base_type || ev; }
      var altTypes = (room.alt_types || []).filter(function(t){
        return t !== 'room_trap' && t !== 'room_gift' && t !== 'room_player' && t !== 'shield_block';
      });
      if (ev === 'unknown' && altTypes.length > 0) { ev = altTypes[0]; altTypes = altTypes.slice(1); }
      var rDat = room.dat || {};
      r[k] = {event:ev, guardian:room.guardian||null, altTypes:altTypes, roomObject:roomObj,
              emissionVisited:!!(rDat.emission_visited), emissionEv:rDat.emission_ev||null};
    }
    var d = mapData();
    if (d && d.steps) {
      for (var i = 0; i < d.steps.length; i++) {
        var s = d.steps[i], key = s.x+'_'+s.y;
        var isRoomEv = (s.event==='room_trap'||s.event==='room_gift');
        if (!r[key]) {
          r[key] = { event: s.event, guardian: null, visits: 1, altTypes: [], roomObject: isRoomEv ? s.event : null };
        } else {
          var cur = r[key];
          if (isRoomEv) { cur.roomObject = s.event; }
          else if (cur.event === 'unknown' && s.event && s.event !== 'unknown') { cur.event = s.event; }
          else if (cur.event !== s.event && s.event && s.event !== 'unknown') {
            if (VARIABLE_EVENTS[cur.event] || VARIABLE_EVENTS[s.event]) {
              if (cur.altTypes.indexOf(s.event) === -1) cur.altTypes.push(s.event);
            }
          }
        }
      }
    }
    roomsCache = r;
    return r;
  }

  var MAP_DB_NAME='animesss-labyrinth-map';
  var MAP_DB_VERSION=1;
  function openMapDb(){
    return new Promise(function(resolve,reject){
      if(!window.indexedDB){reject(new Error('indexeddb_unavailable'));return;}
      var request=indexedDB.open(MAP_DB_NAME,MAP_DB_VERSION);
      request.onupgradeneeded=function(){
        var db=request.result;
        if(!db.objectStoreNames.contains('rooms'))db.createObjectStore('rooms',{keyPath:'key'});
        if(!db.objectStoreNames.contains('meta'))db.createObjectStore('meta',{keyPath:'name'});
      };
      request.onsuccess=function(){resolve(request.result);};
      request.onerror=function(){reject(request.error);};
    });
  }
  function idbGetAllRooms(){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction('rooms','readonly'),r=tx.objectStore('rooms').getAll();r.onsuccess=function(){resolve(r.result||[])};r.onerror=function(){reject(r.error)}})});}
  function idbCountRooms(){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction('rooms','readonly'),r=tx.objectStore('rooms').count();r.onsuccess=function(){resolve(Number(r.result||0))};r.onerror=function(){reject(r.error)}})});}
  function idbGetMeta(name){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var r=db.transaction('meta','readonly').objectStore('meta').get(name);r.onsuccess=function(){resolve(r.result?r.result.value:null)};r.onerror=function(){reject(r.error)}})});}
  function idbPutMeta(name,value){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction('meta','readwrite');tx.objectStore('meta').put({name:name,value:value});tx.oncomplete=function(){resolve()};tx.onerror=function(){reject(tx.error)}})});}
  function idbReplaceRooms(rooms,version){
    rooms=Array.isArray(rooms)?rooms:[];
    return openMapDb().then(function(db){
      return new Promise(function(resolve,reject){
        var clearTx=db.transaction('rooms','readwrite');
        clearTx.objectStore('rooms').clear();
        clearTx.oncomplete=function(){resolve(db);};
        clearTx.onerror=function(){reject(clearTx.error);};
      });
    }).then(function(db){
      var index=0;
      var batchSize=1500;
      function writeNext(){
        if(index>=rooms.length){
          return new Promise(function(resolve,reject){
            var tx=db.transaction('meta','readwrite');
            var meta=tx.objectStore('meta');
            meta.put({name:'map_version',value:Number(version||0)});
            meta.put({name:'map_complete',value:true});
            meta.put({name:'room_count',value:rooms.length});
            tx.oncomplete=function(){resolve();};
            tx.onerror=function(){reject(tx.error);};
          });
        }
        var end=Math.min(index+batchSize,rooms.length);
        return new Promise(function(resolve,reject){
          var tx=db.transaction('rooms','readwrite');
          var store=tx.objectStore('rooms');
          for(var i=index;i<end;i++){
            var room=rooms[i];
            if(room&&room.x!=null&&room.y!=null){
              store.put(Object.assign({key:room.x+'_'+room.y},room));
            }
          }
          tx.oncomplete=function(){index=end;resolve();};
          tx.onerror=function(){reject(tx.error);};
        }).then(writeNext);
      }
      return writeNext();
    });
  }
  function idbApplyChanges(changes,version){return openMapDb().then(function(db){return new Promise(function(resolve,reject){var tx=db.transaction(['rooms','meta'],'readwrite'),store=tx.objectStore('rooms');(changes||[]).forEach(function(room){if(room&&room.x!=null&&room.y!=null)store.put(Object.assign({key:room.x+'_'+room.y},room));});var meta=tx.objectStore('meta');meta.put({name:'map_version',value:Number(version||0)});meta.put({name:'map_complete',value:true});tx.oncomplete=function(){resolve()};tx.onerror=function(){reject(tx.error)}})});}
  function roomsArrayToCloud(rows){var converted={};(rows||[]).forEach(function(room){if(room&&room.x!=null&&room.y!=null)converted[room.x+'_'+room.y]={event:room.ev||room.event||'unknown',base_type:room.base_type||null,current_event:room.current_event||null,guardian:room.guardian||null,alt_types:room.alt_types||[],room_object:room.current_event||room.room_object||null,first_user:room.first_user||null,first_at:room.first_at||null,last_user:room.last_user||null,last_at:room.last_at||null,visits:room.visits||0,dat:room.dat||null,state_version:room.state_version||0};});return converted;}

  function idbStreamRooms(onBatch, onDone, onError) {
    openMapDb().then(function(db) {
      var tx = db.transaction('rooms', 'readonly');
      var store = tx.objectStore('rooms');
      var batch = [];
      var BATCH = 3000;
      var req = store.openCursor();
      req.onsuccess = function(e) {
        var cursor = e.target.result;
        if (cursor) {
          batch.push(cursor.value);
          if (batch.length >= BATCH) {
            onBatch(batch.splice(0, BATCH));
          }
          cursor.continue();
        } else {
          if (batch.length) onBatch(batch);
          if (onDone) onDone();
        }
      };
      req.onerror = function() { if (onError) onError(req.error); };
    }).catch(function(e) { if (onError) onError(e); });
  }

  function loadFromCache() {
    var rendered = false;
    idbStreamRooms(
      function(batch) {
        if (!cloudMap) cloudMap = {};
        batch.forEach(function(room) {
          if (!room || room.x == null || room.y == null) return;
          if (Math.abs(room.y) > 2000 || Math.abs(room.x) > 2000) return;
          cloudMap[room.x + '_' + room.y] = {
            event: room.ev || room.event || 'unknown',
            base_type: room.base_type || null,
            current_event: room.current_event || null,
            guardian: room.guardian || null,
            alt_types: room.alt_types || [],
            room_object: room.current_event || room.room_object || null,
            first_user: room.first_user || null,
            first_at: room.first_at || null,
            last_user: room.last_user || null,
            last_at: room.last_at || null,
            visits: room.visits || 0,
            dat: room.dat || null,
            state_version: room.state_version || 0
          };
        });
        _cloudMapCount = Object.keys(cloudMap).length;
        invalidateRoomsCache();
        drawMini();
        if (!rendered) { rendered = true; if (fmOpen) drawFull(); }
      },
      function() {
        if (!rendered) {
          try {
            var raw = window.lmLS.get(LS_KEY);
            if (raw) {
              var parsed = JSON.parse(raw);
              Object.keys(parsed).forEach(function(k) {
                var p = k.split('_');
                if (Math.abs(+p[0]) > 2000 || Math.abs(+p[1]) > 2000) delete parsed[k];
              });
              cloudMap = parsed;
              _cloudMapCount = Object.keys(parsed).length;
              invalidateRoomsCache();
              var migrate = Object.keys(parsed).map(function(key) {
                var p = key.split('_');
                return Object.assign({x: Number(p[0]), y: Number(p[1]), ev: parsed[key].event}, parsed[key]);
              });
              idbReplaceRooms(migrate, 0).catch(function() {});
              drawMini();
              if (fmOpen) drawFull();
            }
          } catch(e) {}
        } else {
          patchSiteMapDelayed();
        }
      },
      function() {
        try {
          var raw = window.lmLS.get(LS_KEY);
          if (raw) {
            var _p = JSON.parse(raw);
            Object.keys(_p).forEach(function(k){ var _c=k.split('_'); if(Math.abs(+_c[0])>2000||Math.abs(+_c[1])>2000) delete _p[k]; });
            cloudMap = _p; _cloudMapCount = Object.keys(_p).length; invalidateRoomsCache(); drawMini();
          }
        } catch(e) {}
      }
    );
    return true;
  }

  function fetchFullMapPaged(request, onProgress){
    var byKey=Object.create(null);
    var ordered=[];
    var version=0;
    var cursor=null;
    var lastCursorKey='';
    var page=0;
    var pageSize=2000;
    var maxPages=100;

    function addRooms(rows){
      (rows||[]).forEach(function(room){
        if(!room||room.x==null||room.y==null)return;
        var key=room.x+'_'+room.y;
        if(!byKey[key])ordered.push(key);
        byKey[key]=room;
      });
    }

    function next(){
      if(page>=maxPages)throw new Error('map_pages_limit');
      var url=VPS_URL+'/map/full?limit='+pageSize;
      if(cursor)url+='&after_y='+encodeURIComponent(cursor.y)+'&after_x='+encodeURIComponent(cursor.x);
      return request(url).then(function(d){
        page++;
        version=Number(d&&d.version||version||0);
        var rows=Array.isArray(d&&d.rooms)?d.rooms:[];
        addRooms(rows);

        if(onProgress) onProgress(ordered.length, page);

        var nextCursor=d&&d.next_cursor;
        if(!nextCursor&&rows.length>=pageSize){
          var last=rows[rows.length-1];
          if(last&&last.x!=null&&last.y!=null)nextCursor={y:last.y,x:last.x};
        }

        var shouldContinue=Boolean((d&&d.has_more)||rows.length>=pageSize);
        if(!shouldContinue||!nextCursor){
          return {rooms:ordered.map(function(k){return byKey[k];}),version:version,pages:page};
        }

        var cursorKey=String(nextCursor.y)+'_'+String(nextCursor.x);
        if(cursorKey===lastCursorKey)throw new Error('map_cursor_stuck:'+cursorKey);
        lastCursorKey=cursorKey;
        cursor={y:Number(nextCursor.y),x:Number(nextCursor.x)};
        return next();
      });
    }

    return next();
  }

  var _cloudLoading = false;
  function loadCloud(onDone, onProgress) {
    if(_cloudLoading){if(onDone)onDone();return;}
    _cloudLoading=true;

    function finish(){_cloudLoading=false;if(onDone)onDone();}
    function request(url){return window.lmFetch(url,{cache:'no-store'}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.json();});}
    function rebuildFull(){
      return fetchFullMapPaged(request, onProgress).then(function(d){
        if(!d.rooms||d.rooms.length<2)throw new Error('full_map_too_small:'+((d.rooms&&d.rooms.length)||0));
        return idbReplaceRooms(d.rooms,d.version).then(function(){return {mode:'full',version:d.version,count:d.rooms.length,pages:d.pages};});
      });
    }

    function rebuildFromD1(){
      lmWarn('VPS недоступен — загрузка карты из резерва (D1)');
      return window.lmFetch(WORKER_URL+'/map',{
        cache:'no-store',
        headers:{Origin:'https://animesss.com',Referer:'https://animesss.com/'}
      }).then(function(r){
        if(!r.ok)throw new Error('D1 HTTP '+r.status);
        return r.json();
      }).then(function(d){
        var raw=d.rooms||d;
        if(typeof raw!=='object'||Array.isArray(raw))throw new Error('d1_bad_format');
        var EV_FIX={'rl':'relic_room','mx':'mimic_chest_back','mr':'mimic_chest_reward'};
        var rows=[];
        Object.keys(raw).forEach(function(k){
          var parts=k.split('_');
          var x=parseInt(parts[0],10),y=parseInt(parts[1],10);
          if(isNaN(x)||isNaN(y))return;
          var room=raw[k]||{};
          var ev=String(room.event||'unknown');
          ev=EV_FIX[ev]||ev;
          rows.push({x:x,y:y,ev:ev,base_type:ev==='unknown'?null:ev,
            dat:{alt_types:room.alt_types||[],room_object:room.room_object||null,
                 guardian:room.guardian||null,source:'d1_fallback'}});
        });
        if(rows.length<100)throw new Error('d1_too_small:'+rows.length);
        if(onProgress)onProgress(rows.length,1);
        return idbReplaceRooms(rows,0).then(function(){
          return {mode:'d1_fallback',version:0,count:rows.length};
        });
      });
    }

    Promise.all([
      idbGetMeta('map_version').catch(function(){return 0;}),
      idbGetMeta('map_complete').catch(function(){return false;}),
      idbCountRooms().catch(function(){return 0;}),
      request(VPS_URL+'/map/version')
    ]).then(function(values){
      var localVersion=Number(values[0]||0);
      var complete=values[1]===true;
      var localCount=Number(values[2]||0);
      var remote=values[3]||{};
      var remoteVersion=Number(remote.version||0);
      var remoteCount=Number(remote.rooms_count||0);
      var countMatches=!remoteCount||localCount===remoteCount;

      if(complete&&localCount>0&&countMatches&&localVersion===remoteVersion){
        return {mode:'same',version:remoteVersion,count:localCount};
      }

      if(complete&&localCount>0&&countMatches&&localVersion&&remoteVersion>localVersion){
        return request(VPS_URL+'/map/changes?after='+localVersion+'&limit=10000').then(function(d){
          if(d.has_more)throw new Error('too_many_changes');
          return idbApplyChanges(d.changes||[],d.version).then(function(){return {mode:'changes',version:d.version,count:localCount};});
        });
      }

      return rebuildFull();
    }).catch(function(error){
      lmDebug('[Карта] Полная перестройка кеша:',error);
      return rebuildFull().catch(function(e2){
        lmDebug('[Карта] VPS недоступен, пробуем D1:',e2);
        return idbCountRooms().catch(function(){return 0;}).then(function(cnt){
          if(cnt>100)throw new Error('use_cache'); // є кеш — не треба D1
          return rebuildFromD1();
        });
      });
    }).then(function(){return idbGetAllRooms();})
      .then(function(rows){
        cloudMap=roomsArrayToCloud(rows);
        _cloudMapCount = Object.keys(cloudMap).length;
        invalidateRoomsCache();
        updateMapInfo();
        drawMini();
        if(fmOpen)drawFull();
        patchSiteMapDelayed();
        lmInfo('Карта обновлена');
        finish();
      }).catch(function(e){
        lmWarn('Используется кеш карты');
        lmDebug(e);
        finish();
      });
  }

  var _stepsPushing = false;

  var FLUSH_EVERY_STEPS = 1;

  function schedulePush(steps, sid) {
    cacheSteps(steps);

    var stored = getCachedSteps();
    var sentCount = Math.min(getSentCount(), stored.length);
    var pendingCount = Math.max(0, stored.length - sentCount);

    if (pendingCount < FLUSH_EVERY_STEPS) return;

    if (_pushTimer) clearTimeout(_pushTimer);
    _pushPending = true;
    _pushTimer = setTimeout(function() {
      _pushPending = false;
      _pushTimer = null;
      syncCacheWithSession();
      flushStepsToServer();
    }, 700);
  }

  function pushSteps(steps, sid) {
    if (_stepsPushing) return Promise.resolve(null);
    _stepsPushing = true;
    var auth = getAuth();
    if (!auth) { _stepsPushing = false; return Promise.resolve(null); }
    var username = auth.username, token = auth.token;
    var vpsSteps = steps.map(function(s, i) {
      var rawStepIndex = s.step_index != null ? Number(s.step_index) : i;
      var stableStepIndex = Number.isInteger(Number(s._lmServerStepIndex))
        ? Number(s._lmServerStepIndex)
        : (Number.isInteger(rawStepIndex) ? rawStepIndex : i);
      return { x:s.x, y:s.y, ev:s.ev||s.event||'empty',
        acc_delta:s.acc_delta != null ? Number(s.acc_delta) : null,
        acc_after:s.acc_after != null ? Number(s.acc_after) : null,
        session_id:sid||SESSION_ID, step_index:stableStepIndex,
        dat: parseDat(s.dat || s._dat) };
    });

    function primary() {
      return window.lmFetch(VPS_URL + '/push', {
        method:'POST', headers:vpsAuthHeaders(true),
        body:JSON.stringify({steps:vpsSteps})
      }).then(function(r) {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.json();
      }).then(function(d) {
        _vpsAlive = true;
        return { data:d, reserve:false };
      });
    }

    function reserve() {
      return window.lmFetch(WORKER_URL+'/update', {
        method:'POST',
        headers:{'Content-Type':'application/json','X-Write-Secret':WRITE_SECRET},
        body:JSON.stringify({steps:steps,session_id:sid||SESSION_ID,username:username,auth_token:token})
      }).then(function(r) {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.json();
      }).then(function(d) { return { data:d, reserve:true }; });
    }

    return primary().catch(function() {
      _vpsAlive = false;
      lmWarn('Резервный сервер');
      return reserve();
    }).then(function(result) {
      lmInfo('Карта обновлена');
      return result.data;
    }).catch(function(e) {
      lmWarn('Карта сохранена локально');
      lmDebug(e);
      return null;
    }).then(function(result) {
      _stepsPushing = false;
      return result;
    });
  }

  function getCurrentRoomOwnership() {
    var cur = curPos();
    var owDiv = document.getElementById('labyrinthOwnership');
    if (!owDiv || window.getComputedStyle(owDiv).display==='none') return [];
    var captUser = document.getElementById('labyrinthCaptureUserBtn');
    var captClub = document.getElementById('labyrinthCaptureClubBtn');
    var tribute  = document.getElementById('labyrinthPayTributeBtn');
    function vis(el) { return el && window.getComputedStyle(el).display!=='none'; }
    var guardian = null;
    if (vis(tribute)) guardian = vis(captClub) ? 'guardian_club' : 'guardian_user';
    else if (vis(captUser)||vis(captClub)) guardian = 'can_capture';
    if (!guardian) return [];
    var d = mapData(), realEvent = 'unknown';
    if (d && d.steps) {
      for (var i = d.steps.length-1; i >= 0; i--) {
        if (d.steps[i].x===cur.x && d.steps[i].y===cur.y) { realEvent=d.steps[i].event; break; }
      }
    }
    return [{x:cur.x, y:cur.y, event:realEvent, guardian:guardian}];
  }

  function fetchOwnership(callback) {
    if (ownershipCache !== null) { callback(ownershipCache); return; }
    var username = window.visitor_name || '';
    if (!username) { callback([]); return; }
    var parser = new DOMParser(), rooms = [];
    fetch('/user/' + encodeURIComponent(username) + '/')
      .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
      .then(function(html) {
        var doc = parser.parseFromString(html, 'text/html');
        doc.querySelectorAll('.user-labyrinth__item').forEach(function(item) {
          var el = item.querySelector('.user-labyrinth__room');
          if (el) { var c = parseRoomText(el.textContent.trim()); if (c) rooms.push({x:c.x,y:c.y,event:'unknown',guardian:'guardian_user'}); }
        });
        var clubLink = findClubLink(doc);
        if (clubLink) {
          return fetch(clubLink.getAttribute('href')).then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
            .then(function(ch) {
              var cd = parser.parseFromString(ch, 'text/html');
              cd.querySelectorAll('.club-labyrinth__item').forEach(function(item) {
                var el = item.querySelector('.club-labyrinth__room');
                if (el) { var c = parseRoomText(el.textContent.trim()); if (c) rooms.push({x:c.x,y:c.y,event:'unknown',guardian:'guardian_club'}); }
              });
              ownershipCache = rooms; callback(rooms);
            });
        } else { ownershipCache = rooms; callback(rooms); }
      })
      .catch(function(e) { lmDebug('[LabMap] fetchOwnership:', e); ownershipCache = []; callback([]); });
  }

  function drawCell(ctx, px, py, cs, event, isCurrent, guardian, dimmed, hasAlt, roomObject) {
    var r = Math.max(1, cs * 0.12);
    ctx.fillStyle = isCurrent ? '#2a1800' : dimmed ? '#0d0f1a' : clr(event);
    ctx.beginPath();
    if (ctx.roundRect) ctx.roundRect(px+.5, py+.5, cs-1, cs-1, r);
    else ctx.rect(px+.5, py+.5, cs-1, cs-1);
    ctx.fill();
    ctx.strokeStyle = isCurrent ? 'rgba(255,214,107,.6)' : 'rgba(255,255,255,.07)';
    ctx.lineWidth = .5; ctx.stroke();
    if (isCurrent) {
      ctx.shadowColor='#ffd66b'; ctx.shadowBlur=cs*.8;
      ctx.strokeStyle='rgba(255,214,107,.9)'; ctx.lineWidth=1; ctx.stroke();
      ctx.shadowBlur=0;
    }
    if (cs < 8 || dimmed) return;
    var iconTxt = isCurrent ? '⚔' : ico(event);
    if (iconTxt) {
      ctx.font = Math.max(8, cs*.58) + 'px sans-serif';
      ctx.fillStyle = isCurrent ? '#ffd66b' : iclr(event);
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.fillText(iconTxt, px+cs/2, py+cs/2);
    }
    if (guardian && cs >= 8) {
      var gc = guardian==='guardian_user' ? '#ffd700' : guardian==='guardian_club' ? '#c59cff' : '#44ff88';
      ctx.strokeStyle=gc; ctx.lineWidth=Math.max(1,cs*.06);
      ctx.shadowColor=gc; ctx.shadowBlur=cs*.12; ctx.globalAlpha=.55;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(px+.5,py+.5,cs-1,cs-1,r);
      else ctx.rect(px+.5,py+.5,cs-1,cs-1);
      ctx.stroke(); ctx.shadowBlur=0; ctx.globalAlpha=1;
      if (cs >= 14) {
        var gico = guardian==='guardian_user'?'👑':guardian==='guardian_club'?'🛡':'⚐';
        ctx.font=Math.max(7,cs*.3)+'px sans-serif';
        ctx.fillStyle=gc; ctx.textAlign='right'; ctx.textBaseline='top';
        ctx.fillText(gico, px+cs-1, py+1);
      }
    }
    if (roomObject && !dimmed && cs >= 6) {
      var oc = roomObject==='room_trap' ? '#ff6b6b' : '#ffd66b';
      ctx.strokeStyle = oc; ctx.lineWidth = Math.max(1, cs * 0.07);
      ctx.setLineDash([cs*0.15, cs*0.1]);
      ctx.shadowColor = oc; ctx.shadowBlur = cs * 0.1; ctx.globalAlpha = 0.7;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(px+.5, py+.5, cs-1, cs-1, r);
      else ctx.rect(px+.5, py+.5, cs-1, cs-1);
      ctx.stroke(); ctx.setLineDash([]); ctx.shadowBlur = 0; ctx.globalAlpha = 1;
    }
    if (hasAlt && !dimmed && cs >= 10) {
      var asz = Math.max(6, cs * 0.28);
      ctx.font = 'bold ' + asz + 'px sans-serif';
      var isVarRoom = event !== 'unknown' && VARIABLE_EVENTS[event];
      ctx.fillStyle = isVarRoom ? 'rgba(192,132,252,0.95)' : 'rgba(255,214,107,0.9)';
      ctx.textAlign = 'left'; ctx.textBaseline = 'bottom';
      ctx.shadowColor = isVarRoom ? 'rgba(160,80,255,0.6)' : 'rgba(255,160,0,0.6)';
      ctx.shadowBlur = cs * 0.15;
      ctx.fillText(isVarRoom ? '~' : '?', px + 2, py + cs - 1);
      ctx.shadowBlur = 0;
    }
  }

  function drawMini() {
    var rooms = allRooms(), keys = Object.keys(rooms);
    if (elSt) elSt.textContent = 'База: '+_cloudMapCount+' | Всего: '+keys.length;
    updateMapInfo(rooms);
  }

  function openFull(skipCenter) {
    fmOpen=true;
    document.getElementById('lm-modal').classList.add('on');
    updateEmissionInfo();
    if (!skipCenter) {
      requestAnimationFrame(function(){
        if (!fmOpen) return;
        fitReadableMap();
      });
    }
  }
  function closeFull() { fmOpen=false; document.getElementById('lm-modal').classList.remove('on'); }

  function fullMapBounds() {
    var rooms = allRooms(), keys = Object.keys(rooms);
    if (!keys.length) return null;
    var minX=Infinity, maxX=-Infinity, minY=Infinity, maxY=-Infinity;
    for (var i=0; i<keys.length; i++) {
      var p=keys[i].split('_'), rx=+p[0], ry=+p[1];
      if (!Number.isFinite(rx) || !Number.isFinite(ry)) continue;
      if (rx<minX) minX=rx; if (rx>maxX) maxX=rx;
      if (ry<minY) minY=ry; if (ry>maxY) maxY=ry;
    }
    return minX===Infinity ? null : {minX:minX,maxX:maxX,minY:minY,maxY:maxY};
  }

  function setMapView(x, y, cellSize) {
    if (!elMbody) return;
    var cs = Math.max(1, Math.min(CELL_SIZE, cellSize || CELL_SIZE * fmScale));
    fmScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, cs / CELL_SIZE));
    cs = CELL_SIZE * fmScale;
    fmX = elMbody.offsetWidth / 2 - Number(x || 0) * cs;
    fmY = elMbody.offsetHeight / 2 - Number(y || 0) * cs;
    drawFull();
  }

  function fitReadableMap() {
    if (!elMbody) return;
    var b = fullMapBounds();
    if (!b) { centerOn(0, 0, FULL_FOCUS_SCALE); return; }
    var c = curPos();
    var cx = Number.isFinite(Number(c.x)) ? Number(c.x) : (b.minX + b.maxX) / 2;
    var cy = Number.isFinite(Number(c.y)) ? Number(c.y) : (b.minY + b.maxY) / 2;
    var W = Math.max(100, elMbody.offsetWidth - 72);
    var H = Math.max(100, elMbody.offsetHeight - 96);
    var visibleX = Math.max(160, Math.min(360, W / FULL_VIEW_MIN_CELL));
    var visibleY = Math.max(95, Math.min(210, H / FULL_VIEW_MIN_CELL));
    var cs = Math.min(W / visibleX, H / visibleY, FULL_VIEW_MAX_CELL);
    cs = Math.max(FULL_VIEW_MIN_CELL, cs);
    setMapView(cx, cy, cs);
  }

  function fitAllRooms() {
    if (!elMbody) return;
    var b = fullMapBounds();
    if (!b) { centerOn(0, 0); return; }
    var W=elMbody.offsetWidth, H=elMbody.offsetHeight;
    var spanX=Math.max(1, b.maxX-b.minX+1), spanY=Math.max(1, b.maxY-b.minY+1);
    var csX = W / spanX, csY = H / spanY;
    var cs  = Math.min(csX, csY, CELL_SIZE);
    cs = Math.max(0.5, cs);
    fmScale = cs / CELL_SIZE;
    var midX=(b.minX+b.maxX)/2, midY=(b.minY+b.maxY)/2;
    fmX=W/2-midX*cs; fmY=H/2-midY*cs;
    drawFull();
  }

  function centerOn(x,y,scale) {
    if (!elMbody) return;
    if (Number.isFinite(Number(scale))) fmScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Number(scale)));
    setMapView(x, y, CELL_SIZE * fmScale);
  }

  function drawFull() {
    if (!elFc || !elMbody) return;
    window._lmFmX = fmX; window._lmFmY = fmY; window._lmFmScale = fmScale;
    elFc.width=elMbody.offsetWidth; elFc.height=elMbody.offsetHeight;
    var ctx=elFc.getContext('2d');
    ctx.clearRect(0,0,elFc.width,elFc.height);
    var rooms=allRooms(), keys=Object.keys(rooms);
    if (!keys.length) return;
    var cs=CELL_SIZE*fmScale, cur=curPos(), hasFilter=Object.keys(filterSet).length>0;
    ctx.strokeStyle='rgba(255,255,255,.04)'; ctx.lineWidth=.5;
    for (var gx=((fmX%cs)+cs)%cs; gx<elFc.width; gx+=cs) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,elFc.height); ctx.stroke(); }
    for (var gy=((fmY%cs)+cs)%cs; gy<elFc.height; gy+=cs) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(elFc.width,gy); ctx.stroke(); }
    var myPathSet = {};
    if (showMyPath) {
      if (window._lmMyPathData) {
        myPathSet = window._lmMyPathData;
      }
      // 8.0.4: emission-кімнати гравця теж частина шляху (їх нема в step-історії /my/path) → підсвічуємо, щоб не були затемнені
      if (window._lmEmissionCells) window._lmEmissionCells.forEach(function(k){ myPathSet[k]=true; });
      var dm2=mapData();
      if (dm2&&dm2.steps) { for (var si=0;si<dm2.steps.length;si++) { var ss=dm2.steps[si]; myPathSet[ss.x+'_'+ss.y]=true; } }
    }
    for (var i=0; i<keys.length; i++) {
      var p=keys[i].split('_'),x=+p[0],y=+p[1],room=rooms[keys[i]];
      var px=fmX+x*cs, py=fmY+y*cs;
      if (px<-cs||px>elFc.width+cs||py<-cs||py>elFc.height+cs) continue;
      var isCur=x===cur.x&&y===cur.y;
      var _altClean=(room.altTypes||[]).filter(function(t){return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift';}),
          _isVar=_altClean.length>0&&room.event!=='unknown'&&VARIABLE_EVENTS[room.event],
          matchesFilter=filterSet[room.event]||(room.guardian&&filterSet[room.guardian])||(filterSet['__has_alt__']&&_altClean.length>0&&!_isVar)||(filterSet['__variable__']&&_isVar)||(filterSet['room_trap']&&room.roomObject&&(room.roomObject==='room_trap'||room.roomObject==='room_gift'));
      var dimmed=(hasFilter&&!matchesFilter&&!isCur)||(showMyPath&&!myPathSet[keys[i]]&&!isCur);
      drawCell(ctx,px,py,cs,room.event,isCur,room.guardian,dimmed,room.altTypes&&room.altTypes.length>0,room.roomObject);
      var _emCell = window._lmEmissionCells && window._lmEmissionCells.has(keys[i]);
      // Червоний фон: кімната unknown, але відвідана під час викиду.
      // 8.0.4: не малюємо на затемнених кімнатах (фільтр/«Мой путь») — інакше всі emission-кімнати засмічують вид шляху.
      if (room.event === 'unknown' && (room.emissionEv || room.emissionVisited || _emCell) && cs >= 4 && !dimmed) {
        ctx.save();
        ctx.fillStyle = 'rgba(180,30,30,0.55)';
        ctx.fillRect(px, py, cs, cs);
        ctx.restore();
      }
      if (((room.emissionEv && room.emissionEv !== room.event) || room.emissionVisited || _emCell) && cs >= 4 && !dimmed) {
        // рамка
        ctx.save();
        ctx.strokeStyle = 'rgba(255,200,0,0.7)';
        ctx.lineWidth = Math.max(1, cs * 0.06);
        ctx.strokeRect(px + ctx.lineWidth * 0.5, py + ctx.lineWidth * 0.5, cs - ctx.lineWidth, cs - ctx.lineWidth);
        ctx.restore();
        // значок
        var _efs = Math.max(8, Math.min(cs * 0.55, 18));
        ctx.font = _efs + 'px sans-serif';
        ctx.fillStyle = 'rgba(255,200,0,0.9)';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText('⚡', px + cs * 0.5, py + cs * 0.5);
        ctx.textAlign = 'left';
        ctx.textBaseline = 'alphabetic';
      }
    }
    if (showMyPath) {
      // 8.0.4: лінія по ВСЬОМУ шляху (історія + поточна сесія), але З'ЄДНУЄМО ЛИШЕ СУСІДНІ кроки.
      // Так лінія точно трасує твій маршрут (і бриджить ділянки, де кімнати ще не в спільній карті — "кусок шляху"),
      // але БЕЗ діагональних стрибків між сесіями/телепортами, що раніше робили клубок-«простирадло».
      var pathSteps = [];
      if (window._lmHistSteps && window._lmHistSteps.length) pathSteps = window._lmHistSteps;
      var dm3=mapData();
      if (dm3&&dm3.steps&&dm3.steps.length) pathSteps = pathSteps.concat(dm3.steps);
      if (pathSteps.length > 1) {
        ctx.save();
        ctx.strokeStyle='rgba(255,214,107,.5)'; ctx.lineWidth=Math.max(1,cs*.12); ctx.lineJoin='round'; ctx.lineCap='round';
        ctx.beginPath();
        for (var li=1;li<pathSteps.length;li++) {
          var pa=pathSteps[li-1], pb=pathSteps[li];
          var pdx=Math.abs((+pa.x)-(+pb.x)), pdy=Math.abs((+pa.y)-(+pb.y));
          if ((pdx>1||pdy>1) || (pdx===0&&pdy===0)) continue; // не сусідні (стрибок/телепорт) або та сама клітина → не з'єднуємо
          ctx.moveTo(fmX+pa.x*cs+cs/2,fmY+pa.y*cs+cs/2);
          ctx.lineTo(fmX+pb.x*cs+cs/2,fmY+pb.y*cs+cs/2);
        }
        ctx.stroke();
        ctx.restore();
      }
    }
    if (highlightRoom) {
      if (Date.now() < highlightRoom.until) {
        var hx=fmX+highlightRoom.x*cs, hy=fmY+highlightRoom.y*cs;
        if (hx>-cs&&hx<elFc.width+cs&&hy>-cs&&hy<elFc.height+cs) {
          var pulse=0.4+0.6*Math.abs(Math.sin(Date.now()/200));
          ctx.save();
          ctx.shadowColor='#ffd66b'; ctx.shadowBlur=cs*1.2*pulse;
          ctx.strokeStyle='rgba(255,220,80,'+(0.7+0.3*pulse)+')';
          ctx.lineWidth=Math.max(2,cs*.18);
          ctx.beginPath();
          var hr=Math.max(2,cs*.15);
          if(ctx.roundRect) ctx.roundRect(hx+.5,hy+.5,cs-1,cs-1,hr);
          else ctx.rect(hx+.5,hy+.5,cs-1,cs-1);
          ctx.stroke(); ctx.restore();
          requestAnimationFrame(drawFull);
        }
      } else { highlightRoom=null; }
    }
    if (elInfo) {
      var cx=Math.round((elFc.width/2-fmX)/cs),cy=Math.round((elFc.height/2-fmY)/cs);
      elInfo.textContent=formatCoords(cx,cy)+' · База '+_cloudMapCount+' · Всего '+keys.length;
    }
  }

  function injectTopBtn() {
    var tabsBtns = document.querySelector('.ncard__tabs-btns');
    if (!tabsBtns || tabsBtns.querySelector('.lm-top-btn')) return;
    var topUrl = 'https://' + location.hostname + '/users_top/';
    var btn = document.createElement('a');
    btn.href = topUrl; btn.className = 'ncard__tabs-btn btn c-gap-10 lm-top-btn';
    btn.innerHTML = '<span class="fal fa-trophy"></span>Топ лабиринта';
    tabsBtns.appendChild(btn);
  }

  var HISTORY_KEYWORDS = ['лабиринт','лабиринта','испытание дао','алтаря удачи','небесный кирпич'];
  var HISTORY_ICONS = [
    { re:/мини.?босс/i, icon:'👹', color:'#ffbb88' },
    { re:/хард.?босс/i, icon:'💀', color:'#cc88ff' },
    { re:/викторин/i, icon:'?', color:'#8bb4ff' },
    { re:/джекпот/i, icon:'★', color:'#ffd66b' },
    { re:/сундук.мимик/i, icon:'👅', color:'#ff9ecb' },
    { re:/сундук/i, icon:'🔒', color:'#ffd66b' },
    { re:/реликвия/i, icon:'🔮', color:'#c59cff' },
    { re:/телепорт/i, icon:'🌀', color:'#c59cff' },
    { re:/торговец/i, icon:'🛒', color:'#8fd3ff' },
    { re:/алтарь/i, icon:'🍀', color:'#6ee786' },
    { re:/страж/i, icon:'👑', color:'#ffd700' },
    { re:/коллекци/i, icon:'🃏', color:'#8fd3ff' },
    { re:/пазл/i, icon:'🧩', color:'#e0d0ff' },
    { re:/небесный кирпич/i, icon:'🧱', color:'#8fd3ff' },
    { re:/испытание дао/i, icon:'☯', color:'#c59cff' },
    { re:/откат|ловушка/i, icon:'↺', color:'#ff8a8a' },
    { re:/штраф.*лабиринт/i, icon:'!', color:'#ff8b8b' },
    { re:/штраф.*дао/i, icon:'!', color:'#ff8b8b' },
    { re:/награда.*лабиринт/i, icon:'+', color:'#ffd66b' },
    { re:/награда.*босс/i, icon:'+', color:'#ffd66b' },
    { re:/лабиринт/i, icon:'⚔', color:'rgba(255,255,255,.45)' }
  ];

  function histIcon(desc) {
    for (var i=0;i<HISTORY_ICONS.length;i++) {
      if (HISTORY_ICONS[i].re.test(desc)) return HISTORY_ICONS[i];
    }
    return { icon:'⚔', color:'rgba(255,255,255,.35)' };
  }

  function isLabRow(desc) {
    var d = desc.toLowerCase();
    for (var i=0;i<HISTORY_KEYWORDS.length;i++) { if (d.indexOf(HISTORY_KEYWORDS[i])!==-1) return true; }
    return false;
  }

  var LS_HIST_KEY      = 'lm_hist_cache_v2'   + _lmUserSuffix;
  var LS_HIST_DATE_KEY = 'lm_hist_cache_date' + _lmUserSuffix;
  var histCache = {};
  var histTotal = null;
  var histCurPage = 1;
  try {
    var _raw = window.lmLS.get(LS_HIST_KEY);
    if (_raw) { histCache = JSON.parse(_raw); histTotal = Object.keys(histCache).length || null; }
  } catch(e) {}

  function parseHistoryHtml(html, pageNum) {
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');
    var rows = [];
    var trs = doc.querySelectorAll('.ncard-transactions__table tbody tr');
    if (!trs.length) trs = doc.querySelectorAll('table tbody tr');
    var allAmounts = [];
    trs.forEach(function(tr) {
      var tds = tr.querySelectorAll('td');
      if (tds.length < 4) return;
      var amount  = tds[0].textContent.trim();
      var balance = tds[1].textContent.trim();
      var date    = tds[2].textContent.trim();
      var descEl  = tds[3];
      var descClone = descEl.cloneNode(true);
      var cardDiv = descClone.querySelector('.acc-history__card');
      if (cardDiv) cardDiv.parentNode.removeChild(cardDiv);
      var desc = descClone.textContent.replace(/\s+/g,' ').trim();
      var amt = parseFloat(amount.replace(/[^0-9.\-]/g,''));
      if (isNaN(amt)) return;
      var dl = desc.toLowerCase();
      if (amt > 0) {
        if (isLabRow(desc)) allAmounts.push({ amt: amt, isLost: false });
      } else {
        var isShutraf = dl.indexOf('штраф') !== -1;
        var isPokupka = dl.indexOf('за покупку') !== -1;
        var isVitrina = dl.indexOf('витрин') !== -1;
        if (isShutraf && !isPokupka && !isVitrina) allAmounts.push({ amt: amt, isLost: true });
      }
      if (!desc || !isLabRow(desc)) return;
      rows.push({ amount: amount, balance: balance, date: date, desc: desc });
    });
    var totalPages = pageNum;
    var pLinks = doc.querySelectorAll('.acc-history__page');
    pLinks.forEach(function(a) {
      var n = parseInt(a.textContent.trim(), 10);
      if (!isNaN(n) && n > totalPages) totalPages = n;
    });
    var trapBackCount = 0;
    var roomCounts = {};
    var DESC_MAP = [
      { re: /мини.?босс/i,          ev: 'mini_boss' },
      { re: /сложн\w* босс/i,        ev: 'hard_boss' },
      { re: /сундук.мимик/i,         ev: 'mimic_chest' },
      { re: /открытие сундука/i,      ev: null },
      { re: /сундука в лабиринте/i,   ev: 'locked_chest' },
      { re: /алтар\w* удачи/i,        ev: 'luck_altar' },
      { re: /викторин/i,              ev: 'quiz' },
      { re: /испытание дао/i,         ev: 'puzzle' },
      { re: /персональной шахты/i,    ev: 'personal_mine' },
      { re: /штраф в лабиринте/i,     ev: 'penalty' },
      { re: /награда в лабиринте/i,   ev: 'reward' },
      { re: /награда за победу над/i, ev: null },
      { re: /откат|ловушка отката/i,  ev: 'trap_back' },
      { re: /коллекц/i,               ev: 'collection' },
      { re: /торговец/i,              ev: 'card_trader' },
      { re: /реликв/i,                ev: 'relic_room' },
      { re: /восстановление|хода/i,   ev: 'recovery_room' },
    ];
    var _seenKeys = {};
    for (var _ri = 0; _ri < rows.length; _ri++) {
      var _desc = rows[_ri].desc;
      if (/откат/i.test(_desc)) trapBackCount++;
      var _matched = false;
      for (var _di = 0; _di < DESC_MAP.length; _di++) {
        if (!DESC_MAP[_di].re.test(_desc)) continue;
        _matched = true;
        var _ev = DESC_MAP[_di].ev;
        if (_ev === null) break;
        var _deduKey = _ev + '_' + (rows[_ri].date || '').replace(/[^0-9]/g, '').slice(0, 12);
        if (_ev === 'locked_chest' && _seenKeys[_deduKey]) break;
        _seenKeys[_deduKey] = true;
        roomCounts[_ev] = (roomCounts[_ev] || 0) + 1;
        break;
      }
    }
    return { rows: rows, totalPages: totalPages, allAmounts: allAmounts, trapBackCount: trapBackCount, roomCounts: roomCounts };
  }

  function loadHistPage(pageNum, onDone) {
    if (histCache[pageNum]) { onDone(histCache[pageNum]); return; }
    var url = pageNum > 1 ? '/acchistory/page/' + pageNum + '/' : '/acchistory/';
    fetch(url, { credentials: 'same-origin' })
      .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
      .then(function(html) {
        var result = parseHistoryHtml(html, pageNum);
        histCache[pageNum] = result;
        if (histTotal === null || result.totalPages > histTotal) histTotal = result.totalPages;
        try { window.lmLS.set(LS_HIST_KEY, JSON.stringify(histCache)); } catch(e) {}
        onDone(result);
      })
      .catch(function(e) {
        lmDebug('[LabMap] history err:', e);
        onDone({ rows: [], totalPages: pageNum });
      });
  }

  function buildAccTotals() {
    var earned = 0, lost = 0;
    Object.keys(histCache).forEach(function(p) {
      var amounts = histCache[p].allAmounts || [];
      amounts.forEach(function(item) {
        if (item.isLost) lost += Math.abs(item.amt);
        else earned += item.amt;
      });
    });
    return { earned: earned, lost: lost };
  }

  function buildAllTimeTrapCount() {
    var total = 0;
    Object.keys(histCache).forEach(function(p) {
      total += (histCache[p].trapBackCount || 0);
    });
    return total;
  }

  function buildAllTimeRoomCounts() {
    var merged = {};
    Object.keys(histCache).forEach(function(p) {
      var rc = histCache[p].roomCounts || {};
      Object.keys(rc).forEach(function(ev) {
        merged[ev] = (merged[ev] || 0) + rc[ev];
      });
    });
    return merged;
  }

  function buildStepsStats() {
    var d = mapData();
    if (!d || !d.steps || !d.steps.length) return null;
    var counts = {};
    d.steps.forEach(function(s) {
      if (s.event==='room_trap'||s.event==='room_gift') return;
      counts[s.event] = (counts[s.event]||0) + 1;
    });
    delete counts['start'];
    var total = d.steps.length - 1;
    var histRooms = buildAllTimeRoomCounts();
    var hasHistData = Object.keys(histRooms).length > 0;
    if (hasHistData) {
      Object.keys(histRooms).forEach(function(ev) {
        var sessionCount = counts[ev] || 0;
        var histCount    = histRooms[ev] || 0;
        counts[ev] = Math.max(sessionCount, histCount);
      });
      total = 0;
      Object.keys(counts).forEach(function(ev) { total += counts[ev]; });
    }
    var todayTrap = counts['trap_back'] || 0;
    var allTimeTotalEl = document.getElementById('labyrinthTotalSteps');
    var domTotal = allTimeTotalEl
      ? (parseInt(allTimeTotalEl.textContent.replace(/\D/g,''), 10) || total)
      : total;
    var allTimeTotal = window._lmAllTimeTotal || domTotal;
    var dbLoaded    = window._lmAllTimeTrapBacks !== undefined;
    var allTimeTrap = dbLoaded ? window._lmAllTimeTrapBacks : buildAllTimeTrapCount();
    var histLoaded  = dbLoaded || allTimeTrap > 0;
    var trapCount   = histLoaded ? allTimeTrap : todayTrap;
    var lostRooms   = trapCount * 5;
    var sorted = Object.keys(counts).sort(function(a,b){ return counts[b]-counts[a]; });
    return {
      total: total,
      allTimeTotal: allTimeTotal,
      counts: counts,
      top: sorted,
      trapCount: trapCount,
      lostRooms: lostRooms,
      historyLoaded: histLoaded
    };
  }

  function renderHistPopup(result, page) {
    var pop = document.getElementById('lm-hpop');
    if (!pop) return;
    var listEl = pop.querySelector('.lm-hist-body');
    if (!listEl) return;
    var totalPages = histTotal || result.totalPages || 1;
    var rows = result.rows;
    var freshHtml = '';
    var lastDate = window.lmLS.get(LS_HIST_DATE_KEY);
    var pagesLoaded = Object.keys(histCache).length;
    if (lastDate && pagesLoaded > 0) {
      var d = new Date(lastDate);
      var pad = function(n){ return n < 10 ? '0'+n : ''+n; };
      var dateStr = pad(d.getDate())+'.'+pad(d.getMonth()+1)+'.'+d.getFullYear()+' '+pad(d.getHours())+':'+pad(d.getMinutes());
      freshHtml = '<div class="lm-hist-fresh">🕐 Актуально на: <b>'+dateStr+'</b> &bull; Стр.: <b>'+pagesLoaded+' из '+(histTotal||totalPages||'?')+'</b></div>';
    } else {
      freshHtml = '<div class="lm-hist-fresh lm-hist-fresh--warn">⚠ Нажмите 🔄 Обновить инфу для загрузки всех данных.</div>';
    }
    var stats = buildStepsStats();
    var statsHtml = '';
    if (stats) {
      var itemsHtml = stats.top.map(function(ev) {
        var pct = Math.round(stats.counts[ev] / stats.total * 100);
        var barColor = ev === 'empty' ? 'rgba(255,255,255,0.25)' : iclr(ev)+'44';
        return '<div class="lm-hs-item"><span class="lm-hs-ico" style="background:'+clr(ev)+';color:'+iclr(ev)+'">'+ico(ev)+'</span><span class="lm-hs-name">'+roomName(ev)+'</span><div class="lm-hs-bar-wrap"><div class="lm-hs-bar" style="width:'+pct+'%;background:'+barColor+'"></div></div><span class="lm-hs-cnt">'+stats.counts[ev]+' <span class="lm-hs-pct">('+pct+'%)</span></span></div>';
      }).join('');
      var acc = buildAccTotals();
      var trapSub = stats.historyLoaded ? 'за всё время' : 'сегодня';
      var cardsHtml = '<div class="lm-sc-grid">' +
        '<div class="lm-sc-card"><div class="lm-sc-label">Всего комнат</div><div class="lm-sc-val">'+stats.allTimeTotal+'</div><div class="lm-sc-sub">комнат пройдено</div></div>' +
        '<div class="lm-sc-card lm-sc-trap">' +
        '<div class="lm-sc-label">Откаты</div>' +
        '<div class="lm-sc-val lm-sc-bad">' + (stats.historyLoaded ? (stats.trapCount > 0 ? '−'+stats.trapCount : '0') : '?') + '</div>' +
        '<div class="lm-sc-trap-row">' +
          (stats.historyLoaded && stats.lostRooms > 0
            ? '<span class="lm-sc-rooms">−'+stats.lostRooms+' комнат &bull; '+trapSub+'</span>'
            : '<span class="lm-sc-hint">'+(stats.historyLoaded ? '0 откатов' : 'нажми Обновить')+'</span>') +
          (window._lmRollbackPlace ? '<span class="lm-sc-badge">Топ #'+window._lmRollbackPlace+'</span>' : '') +
        '</div>' +
      '</div>' +
        '<div class="lm-sc-card"><div class="lm-sc-label">Заработано</div><div class="lm-sc-val lm-sc-good">+'+acc.earned+'</div><div class="lm-sc-sub">АСС за всё время</div></div>' +
        '<div class="lm-sc-card"><div class="lm-sc-label">Потеряно</div><div class="lm-sc-val lm-sc-bad">−'+acc.lost+'</div><div class="lm-sc-sub">АСС на штрафах</div></div>' +
        '</div>';
      statsHtml = cardsHtml+'<div class="lm-hist-stats"><div class="lm-hist-stats-ttl">📊 Статистика по комнатам</div><div class="lm-hist-stats-list">'+itemsHtml+'</div></div>';
    }
    var rowsHtml = '';
    if (rows.length) {
      rowsHtml = rows.map(function(row) {
        var amt = parseFloat(row.amount.replace(/[^0-9.\-+]/g,''));
        var isPlus  = (!isNaN(amt) && amt > 0) || row.amount.indexOf('+') !== -1;
        var isMinus = (!isNaN(amt) && amt < 0) || row.amount.indexOf('-') !== -1;
        var amtCls  = isPlus ? 'lm-ha-plus' : isMinus ? 'lm-ha-minus' : '';
        var hi = histIcon(row.desc);
        return '<div class="lm-hr"><div class="lm-hr-ico" style="background:'+hi.color+'18;border-color:'+hi.color+'44"><span style="color:'+hi.color+'">'+hi.icon+'</span></div><div class="lm-hr-main"><div class="lm-hr-desc">'+row.desc+'</div><div class="lm-hr-meta">'+row.date+(row.balance?' &bull; Баланс: <b>'+row.balance+'</b> ACC':'')+'</div></div><div class="lm-hr-amt '+amtCls+'">'+row.amount+(row.amount.indexOf('ACC')===-1?' ACC':'')+'</div></div>';
      }).join('');
    } else {
      rowsHtml = '<div class="lm-hist-empty">Записей лабиринта не найдено на странице '+page+'.</div>';
    }
    var pagHtml = '';
    if (totalPages > 1) {
      var start = Math.max(1, page-2), end = Math.min(totalPages, page+2);
      var nums = '';
      if (start > 1) nums += '<button class="lm-hp-btn" data-p="1">1</button>'+(start>2?'<span class="lm-hp-dots">…</span>':'');
      for (var pi=start; pi<=end; pi++) {
        nums += '<button class="lm-hp-btn'+(pi===page?' lm-hp-cur':'')+'" data-p="'+pi+'">'+pi+'</button>';
      }
      if (end < totalPages) nums += (end<totalPages-1?'<span class="lm-hp-dots">…</span>':'')+'<button class="lm-hp-btn" data-p="'+totalPages+'">'+totalPages+'</button>';
      pagHtml = '<div class="lm-hist-pag"><button class="lm-hp-nav" id="lm-hprev" '+(page<=1?'disabled':'')+'>‹ Новее</button>'+nums+'<button class="lm-hp-nav" id="lm-hnext" '+(page>=totalPages?'disabled':'')+'>Старее ›</button></div>';
    }
    listEl.innerHTML = freshHtml + statsHtml + '<div class="lm-hist-rows-head">📜 Записи из лабиринта (страница '+page+')</div><div class="lm-hist-rows">'+rowsHtml+'</div>'+pagHtml;
    listEl.querySelectorAll('.lm-hp-btn[data-p]').forEach(function(btn) {
      btn.addEventListener('click', function() { loadAndRenderHist(parseInt(this.dataset.p, 10)); });
    });
    var prev = listEl.querySelector('#lm-hprev');
    var next = listEl.querySelector('#lm-hnext');
    if (prev) prev.addEventListener('click', function() { if (page>1) loadAndRenderHist(page-1); });
    if (next) next.addEventListener('click', function() { if (page<totalPages) loadAndRenderHist(page+1); });
  }

  function loadAndRenderHist(page) {
    histCurPage = page;
    var listEl = document.querySelector('#lm-hpop .lm-hist-body');
    if (listEl) listEl.innerHTML = '<div class="lm-spop-loading">Загрузка страницы '+page+'...</div>';
    loadHistPage(page, function(result) { renderHistPopup(result, page); });
  }

  function injectStyles() {
    var style = document.createElement('style');
    style.textContent = [
      '#lm-wrap{width:100%;max-width:100%;min-width:0;margin-bottom:18px;border-radius:16px;background:linear-gradient(135deg,rgba(30,33,52,.96) 0%,rgba(16,18,31,.98) 100%);border:1px solid rgba(255,255,255,.1);color:#fff;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.35);container-type:inline-size;}',
      '#lm-hdr{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;border-bottom:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.025);min-width:0;}',
      '#lm-ttl{display:flex;align-items:center;gap:8px;min-width:0;font-size:12px;font-weight:900;text-transform:uppercase;letter-spacing:.04em;color:rgba(226,232,240,.72);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '#lm-ttl-dot{width:8px;height:8px;border-radius:50%;background:#4a5ac7;box-shadow:0 0 8px rgba(74,90,199,.7);animation:lm-pulse 2s infinite;}',
      '@keyframes lm-pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.8)}}',
      '#lm-btns{display:flex;align-items:center;justify-content:flex-end;gap:8px;min-width:0;}',
      '#lm-rbtn{min-height:36px;padding:8px 14px;border:none;border-radius:8px;background:linear-gradient(135deg,#4a5ac7,#3a4ab0);color:#fff;font-size:11px;font-weight:800;cursor:pointer;box-shadow:0 2px 12px rgba(74,90,199,.4);transition:.15s;white-space:nowrap;}',
      '#lm-rbtn:hover{background:linear-gradient(135deg,#5b6dd8,#4a5ac7);transform:translateY(-1px);}',
      '#lm-rbtn.loading{opacity:.6;cursor:not-allowed;transform:none;}',
      '#lm-pbtn{min-height:36px;padding:8px 14px;border:none;border-radius:8px;background:linear-gradient(135deg,#b02a59,#8a1f45);color:#fff;font-size:11px;font-weight:800;cursor:pointer;box-shadow:0 2px 12px rgba(176,42,89,.4);transition:.15s;white-space:nowrap;}',
      '#lm-pbtn:hover{background:linear-gradient(135deg,#c03468,#b02a59);transform:translateY(-1px);}',
      '#lm-cleanbtn{min-height:36px;padding:8px 14px;border:none;border-radius:8px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.65);font-size:11px;font-weight:800;cursor:pointer;border:1px solid rgba(255,255,255,.12);transition:.15s;white-space:nowrap;}',
      '#lm-cleanbtn:hover{background:rgba(255,255,255,.14);color:#fff;transform:translateY(-1px);}',
      '#lm-cleanbtn.active{background:rgba(255,214,107,.18);color:#ffd66b;border-color:rgba(255,214,107,.45);}',
      '#lm-map-info{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0;border-bottom:1px solid rgba(255,255,255,.07);}',
      '.lm-mi-card{display:flex;align-items:center;gap:10px;min-width:0;padding:11px 16px;}',
      '.lm-mi-card:first-child{border-right:1px solid rgba(255,255,255,.07);}',
      '.lm-mi-ico{font-size:22px;flex-shrink:0;}',
      '.lm-mi-val{font-size:20px;font-weight:850;line-height:1;color:#fff;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-mi-val-sm{font-size:15px;font-weight:700;line-height:1;color:rgba(255,255,255,.78);margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-mi-sep{height:1px;background:rgba(255,255,255,.08);margin:5px 0;}',
      '.lm-mi-lbl{font-size:10px;color:rgba(255,255,255,.46);text-transform:uppercase;letter-spacing:.04em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '#lm-mi-emission-card.is-active .lm-mi-ico{animation:lmEmissionPulse 1.2s ease-in-out infinite;}',
      '@keyframes lmEmissionPulse{0%,100%{opacity:1;}50%{opacity:.4;}}',
      '#lm-action-bar{display:flex;gap:8px;padding:11px 14px;border-bottom:1px solid rgba(255,255,255,.07);background:rgba(0,0,0,.15);min-width:0;}',
      '.labyrinth-cell.lm-emission-cell{position:relative!important;}',
      '.lm-emission-ico{position:absolute;bottom:1px;right:2px;font-size:7px;line-height:1;pointer-events:none;z-index:5;filter:drop-shadow(0 0 2px rgba(255,220,0,.8));}',
      '.lm-abar-btn{flex:1 1 0;min-width:0;display:flex;align-items:center;justify-content:center;gap:7px;padding:9px 10px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:rgba(255,255,255,.68);font-size:11px;font-weight:800;cursor:pointer;border-radius:10px;transition:background .15s,border-color .15s,color .15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-abar-btn:hover{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.9);}',
      '.lm-abar-ico{font-size:15px;line-height:1;flex-shrink:0;}',
      '#lm-wb-guard.active{background:rgba(255,215,0,.15);border-color:rgba(255,215,0,.5);color:#ffd700;}',
      '#lm-wb-club.active{background:rgba(197,156,255,.15);border-color:rgba(197,156,255,.5);color:#c59cff;}',
      '#lm-wb-hist.active{background:rgba(103,232,249,.15);border-color:rgba(103,232,249,.5);color:#67e8f9;}',
      '#lm-inline-panel{display:none;border-top:0;}',
      '#lm-inline-panel.on{display:block;}',
      '#lm-wi-guard,#lm-wi-club{max-height:280px;overflow-y:auto;}',
      '.lm-wi-item{display:flex;align-items:center;gap:10px;padding:9px 16px;cursor:pointer;transition:.12s;border-bottom:1px solid rgba(255,255,255,.04);}',
      '.lm-wi-item:last-child{border-bottom:none;}',
      '.lm-wi-item:hover{background:rgba(255,255,255,.05);}',
      '.lm-wi-img{width:34px;height:48px;border-radius:6px;object-fit:cover;flex-shrink:0;}',
      '.lm-wi-info{flex:1;min-width:0;}',
      '.lm-wi-name{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-wi-room{font-size:11px;color:rgba(255,255,255,.55);margin-top:2px;}',
      '.lm-wi-date{font-size:10px;color:rgba(255,255,255,.35);margin-top:1px;}',
      '.lm-wi-nav{font-size:14px;color:rgba(255,255,255,.3);flex-shrink:0;}',
      '.lm-wi-empty{padding:20px;text-align:center;font-size:12px;color:rgba(255,255,255,.35);}',
      '.lm-wi-loading{padding:16px;text-align:center;font-size:12px;color:rgba(255,255,255,.4);}',
      '#lm-wi-hist{max-height:420px;overflow-y:auto;}',
      '#lm-cvs-wrap{display:none!important;}#lm-cvs{display:none!important;}#lm-st{display:none!important;}',
      '@container (max-width:620px){#lm-hdr{flex-direction:column;align-items:stretch;padding:11px;}#lm-btns{justify-content:flex-start;overflow-x:auto;overflow-y:hidden;margin:0 -11px;padding:0 11px 2px;scrollbar-width:none;}#lm-btns::-webkit-scrollbar{display:none;}#lm-cleanbtn,#lm-rbtn,#lm-pbtn{flex:0 0 auto;min-height:38px;padding:8px 12px;}#lm-map-info{grid-template-columns:repeat(2,minmax(0,1fr));}.lm-mi-card{padding:12px 11px;gap:8px;}.lm-mi-val{font-size:20px;}.lm-mi-lbl{font-size:9px;}#lm-action-bar{overflow-x:auto;overflow-y:hidden;flex-wrap:nowrap;padding:10px 11px;scrollbar-width:none;}#lm-action-bar::-webkit-scrollbar{display:none;}.lm-abar-btn{flex:0 0 auto;min-width:134px;font-size:10px;padding:10px 12px;}.lm-sc-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:7px!important;}.lm-sc-card{min-width:0;padding:9px 10px;}.lm-sc-val{font-size:18px;}.lm-hist-body{padding:8px;}.lm-hr{gap:7px;padding:7px 8px;}.lm-hr-amt{font-size:12px;}}',
      '@container (max-width:420px){#lm-wrap{border-radius:12px;}#lm-ttl{font-size:11px;}#lm-map-info{grid-template-columns:1fr;}.lm-mi-card:first-child{border-right:0;border-bottom:1px solid rgba(255,255,255,.07);}.lm-mi-ico{width:24px;font-size:20px;text-align:center;}.lm-mi-val{font-size:19px;}#lm-cleanbtn,#lm-rbtn,#lm-pbtn{font-size:10px;padding:8px 10px;}.lm-sc-grid{grid-template-columns:1fr!important;}.lm-hr-desc{white-space:normal;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;}.lm-hr-amt{align-self:flex-start;white-space:nowrap;}}',
      '@media(max-width:640px){#lm-map-info{grid-template-columns:1fr 1fr;}.lm-mi-val{font-size:18px;}.lm-abar-btn{font-size:10px;padding:9px 8px;}}',
      '#lm-modal{display:none;position:fixed;inset:0;z-index:999999;background:#060912;flex-direction:column;touch-action:none;overflow:hidden;color:#f8fbff;height:100vh;height:100dvh;}',
      '#lm-modal:before{content:"";position:absolute;inset:0;pointer-events:none;background:linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px),radial-gradient(circle at 50% 20%,rgba(56,189,248,.09),transparent 42%);background-size:24px 24px,24px 24px,100% 100%;opacity:.8;}',
      '#lm-modal.on{display:flex;}',
      '#lm-mhdr{position:relative;z-index:4;display:flex;flex-direction:column;gap:8px;padding:calc(10px + env(safe-area-inset-top)) 14px 10px;background:rgba(12,16,28,.88);border-bottom:1px solid rgba(148,163,184,.18);box-shadow:0 12px 32px rgba(0,0,0,.32);backdrop-filter:blur(14px);flex-shrink:0;}',
      '#lm-mhdr-top{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%;min-width:0;}',
      '#lm-mhdr-btns{display:flex;align-items:center;gap:8px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;scrollbar-width:none;margin:0 -14px;padding:0 14px 2px;}',
      '#lm-mhdr-btns::-webkit-scrollbar{display:none;}',
      '#lm-mttl{display:flex;align-items:center;gap:9px;min-width:0;font-size:14px;font-weight:900;letter-spacing:.02em;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '#lm-mttl:before{content:"";width:9px;height:9px;border-radius:50%;background:#22d3ee;box-shadow:0 0 16px rgba(34,211,238,.85);flex:0 0 auto;}',
      '#lm-b0,#lm-b1,#lm-bf,#lm-bpath,#lm-bguard,#lm-bclub,#lm-bhist{min-height:36px;padding:8px 12px;border-radius:8px;font-size:11px;font-weight:800;line-height:1;cursor:pointer;position:relative;display:inline-flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap;box-shadow:inset 0 1px 0 rgba(255,255,255,.08);transition:background .14s,border-color .14s,color .14s,transform .14s;}',
      '#lm-b0{background:rgba(99,102,241,.16);color:#b9c4ff;border:1px solid rgba(129,140,248,.38);}#lm-b0:hover{background:rgba(99,102,241,.28);color:#fff;transform:translateY(-1px);}',
      '#lm-b1{background:rgba(245,158,11,.16);color:#ffd66b;border:1px solid rgba(245,158,11,.42);}#lm-b1:hover{background:rgba(245,158,11,.3);color:#fff;transform:translateY(-1px);}',
      '#lm-bf{background:rgba(139,92,246,.16);color:#d8c4ff;border:1px solid rgba(139,92,246,.4);}#lm-bf:hover{background:rgba(139,92,246,.3);color:#fff;transform:translateY(-1px);}#lm-bf.active{background:#7c3aed;color:#fff;border-color:#a78bfa;}',
      '#lm-bpath{background:rgba(16,185,129,.14);color:#7cf4a2;border:1px solid rgba(16,185,129,.38);}#lm-bpath:hover{background:rgba(16,185,129,.28);color:#fff;transform:translateY(-1px);}#lm-bpath.active{background:#059669;color:#fff;border-color:#34d399;}',
      '#lm-bguard{background:rgba(234,179,8,.14);color:#ffe16d;border:1px solid rgba(234,179,8,.34);}#lm-bguard:hover{background:rgba(234,179,8,.27);color:#fff;transform:translateY(-1px);}#lm-bguard.active{background:#a16207;color:#fff;border-color:#eab308;}',
      '#lm-bclub{background:rgba(168,85,247,.14);color:#d8b4fe;border:1px solid rgba(168,85,247,.34);}#lm-bclub:hover{background:rgba(168,85,247,.27);color:#fff;transform:translateY(-1px);}#lm-bclub.active{background:#6b21a8;color:#fff;border-color:#c084fc;}',
      '#lm-bhist{background:rgba(6,182,212,.14);color:#67e8f9;border:1px solid rgba(6,182,212,.34);}#lm-bhist:hover{background:rgba(6,182,212,.27);color:#fff;transform:translateY(-1px);}#lm-bhist.active{background:#0e7490;color:#fff;border-color:#22d3ee;}',
      '#lm-bc{width:40px;height:40px;border:none;border-radius:8px;padding:0;background:rgba(239,68,68,.95);color:#fff;font-size:22px;font-weight:800;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;box-shadow:0 8px 22px rgba(239,68,68,.22);}#lm-bc:hover{background:#f87171;}',
      '#lm-brefresh{height:40px;padding:0 13px;gap:7px;border-radius:8px;background:rgba(59,130,246,.18);color:#b9d2ff;font-size:11px;font-weight:800;letter-spacing:.04em;text-transform:uppercase;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(96,165,250,.42);white-space:nowrap;box-shadow:inset 0 1px 0 rgba(255,255,255,.08);}',
      '#lm-brefresh:hover{background:rgba(59,130,246,.32);color:#fff;}',
      '#lm-brefresh.loading{opacity:.6;cursor:not-allowed;}',
      '#lm-brefresh.loading .lm-ref-ico{display:inline-block;animation:lm-spin 1s linear infinite;}',
      '@keyframes lm-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}',
      '#lm-hdr-right{display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:auto;}',
      '.lm-pop{display:none;position:fixed;top:0;left:0;z-index:9999999;background:rgba(15,23,42,.96);border:1px solid rgba(148,163,184,.22);border-radius:12px;padding:0;box-shadow:0 18px 52px rgba(0,0,0,.58);overflow:hidden;max-width:calc(100vw - 16px);backdrop-filter:blur(14px);}',
      '.lm-pop.on{display:flex;flex-direction:column;}',
      '#lm-gpop,#lm-cpop{width:min(380px,calc(100vw - 24px));max-height:460px;}',
      '#lm-fpop{width:min(360px,calc(100vw - 24px));padding:12px;}',
      '#lm-hpop{width:min(520px,calc(100vw - 24px));max-height:min(560px,80vh);}',
      '#lm-hpop .lm-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);flex-shrink:0;}',
      '#lm-hpop .lm-pop-title{font-size:12px;font-weight:800;color:#fff;text-transform:uppercase;letter-spacing:.04em;}',
      '#lm-hpop .lm-pop-link{font-size:10px;color:rgba(255,255,255,.4);text-decoration:none;}',
      '@media(max-width:600px){.lm-pop.on{left:8px!important;right:8px!important;width:auto!important;max-width:none!important;max-height:calc(100dvh - 130px);}}',
      '.lm-hist-body{overflow-y:auto;padding:10px;display:flex;flex-direction:column;gap:8px;}',
      '.lm-sc-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:10px;}',
      '.lm-sc-card{padding:10px 12px;border-radius:12px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.08);}',
      '.lm-sc-label{font-size:10px;text-transform:uppercase;letter-spacing:.04em;color:rgba(255,255,255,.45);margin-bottom:4px;}',
      '.lm-sc-val{font-size:20px;font-weight:800;color:#fff;line-height:1.1;margin-bottom:3px;}',
      '.lm-sc-good{color:#6ee786;}.lm-sc-bad{color:#ff8b8b;}.lm-sc-trap .lm-sc-val{margin-bottom:3px}.lm-sc-trap-row{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-top:3px}.lm-sc-rooms{font-size:10px;color:rgba(255,133,133,.75);line-height:1.3}.lm-sc-hint{font-size:10px;color:rgba(255,255,255,.4)}.lm-sc-badge{display:inline-flex;align-items:center;padding:1px 6px;border-radius:999px;font-size:10px;font-weight:800;line-height:1.6;background:linear-gradient(135deg,rgba(255,100,100,.22),rgba(255,60,60,.12));border:1px solid rgba(255,100,100,.35);color:#ff9e9e;white-space:nowrap}',
      '.lm-sc-sub{font-size:10px;color:rgba(255,255,255,.4);line-height:1.3;}',
      '.lm-hist-stats{padding:10px;border-radius:12px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);}',
      '.lm-hist-stats-ttl{font-size:11px;color:rgba(255,255,255,.55);text-transform:uppercase;letter-spacing:.04em;margin-bottom:8px;}',
      '.lm-hist-stats-list{display:flex;flex-direction:column;gap:4px;}',
      '.lm-hs-item{display:flex;align-items:center;gap:8px;padding:5px 0;}',
      '.lm-hs-ico{width:24px;height:24px;border-radius:5px;display:flex;align-items:center;justify-content:center;font-size:13px;flex-shrink:0;}',
      '.lm-hs-name{font-size:12px;color:rgba(255,255,255,.85);width:90px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-hs-bar-wrap{flex:1;min-width:30px;height:6px;border-radius:3px;background:rgba(255,255,255,.08);overflow:hidden;}',
      '.lm-hs-bar{height:6px;border-radius:3px;transition:width .3s;}',
      '.lm-hs-cnt{font-size:12px;font-weight:600;color:#fff;min-width:58px;text-align:right;white-space:nowrap;}',
      '.lm-hs-pct{font-size:10px;font-weight:400;color:rgba(255,255,255,.45);}',
      '.lm-hist-fresh{font-size:11px;color:rgba(255,255,255,.45);padding:6px 10px;border-radius:8px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.07);margin-bottom:8px;line-height:1.5;}',
      '.lm-hist-fresh b{color:rgba(255,255,255,.8);}',
      '.lm-hist-fresh--warn{color:#ffd66b;border-color:rgba(255,214,107,.25);background:rgba(255,214,107,.07);}',
      '.lm-hist-rows-head{font-size:11px;font-weight:700;color:rgba(255,255,255,.5);text-transform:uppercase;letter-spacing:.04em;padding:2px 0;}',
      '.lm-hist-rows{display:flex;flex-direction:column;gap:5px;}',
      '.lm-hr{display:flex;align-items:center;gap:8px;padding:7px 9px;border-radius:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.06);}',
      '.lm-hr-ico{width:32px;height:32px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:15px;flex-shrink:0;border:1px solid transparent;}',
      '.lm-hr-main{flex:1;min-width:0;}',
      '.lm-hr-desc{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-hr-meta{font-size:10px;color:rgba(255,255,255,.42);margin-top:1px;}',
      '.lm-hr-meta b{color:rgba(255,255,255,.7);}',
      '.lm-hr-amt{font-size:12px;font-weight:800;flex-shrink:0;white-space:nowrap;}',
      '.lm-ha-plus{color:#6ee786;}.lm-ha-minus{color:#ff8b8b;}',
      '.lm-hist-empty{padding:18px;text-align:center;font-size:12px;color:rgba(255,255,255,.4);}',
      '.lm-hist-pag{display:flex;align-items:center;justify-content:center;gap:4px;padding:8px 0 2px;border-top:1px solid rgba(255,255,255,.07);flex-wrap:wrap;}',
      '.lm-hp-nav{padding:4px 10px;border:none;border-radius:7px;background:rgba(6,182,212,.2);color:#67e8f9;font-size:11px;font-weight:700;cursor:pointer;}',
      '.lm-hp-nav:disabled{opacity:.3;cursor:not-allowed;}',
      '.lm-hp-btn{width:28px;height:28px;border:none;border-radius:6px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.7);font-size:11px;font-weight:700;cursor:pointer;}',
      '.lm-hp-btn:hover{background:rgba(255,255,255,.18);color:#fff;}',
      '.lm-hp-cur{background:rgba(6,182,212,.35)!important;color:#fff!important;}',
      '.lm-hp-dots{font-size:11px;color:rgba(255,255,255,.3);padding:0 3px;}',
      '.lm-spop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid rgba(255,255,255,.08);flex-shrink:0;}',
      '.lm-spop-title{font-size:12px;font-weight:800;color:#fff;text-transform:uppercase;letter-spacing:.04em;}',
      '.lm-spop-count{font-size:11px;color:rgba(255,255,255,.5);margin-left:6px;}',
      '.lm-spop-list{overflow-y:auto;padding:8px;}',
      '.lm-si{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:10px;cursor:pointer;transition:background .12s;border:1px solid transparent;}',
      '.lm-si:hover{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.08);}',
      '.lm-si-img{width:38px;height:54px;border-radius:6px;object-fit:cover;flex-shrink:0;}',
      '.lm-si-info{min-width:0;flex:1;}',
      '.lm-si-name{font-size:12px;font-weight:700;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '.lm-si-room{font-size:11px;color:rgba(255,255,255,.65);margin-top:2px;}',
      '.lm-si-date{font-size:10px;color:rgba(255,255,255,.4);margin-top:2px;}',
      '.lm-si-nav{font-size:16px;color:rgba(255,255,255,.4);flex-shrink:0;}',
      '.lm-spop-loading{padding:20px;text-align:center;color:rgba(255,255,255,.5);font-size:12px;}',
      '#lm-fpop-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;}',
      '#lm-fpop-title{font-size:12px;font-weight:800;color:#fff;text-transform:uppercase;letter-spacing:.04em;}',
      '#lm-fpop-clear{padding:3px 8px;border:none;border-radius:6px;background:#b02a59;color:#fff;font-size:10px;font-weight:700;cursor:pointer;}',
      '#lm-fpop-grid{display:flex;flex-wrap:wrap;gap:5px;}',
      '.lm-fi{display:flex;align-items:center;gap:4px;font-size:11px;color:rgba(255,255,255,.7);cursor:pointer;padding:4px 8px;border-radius:8px;border:1px solid rgba(255,255,255,.08);background:rgba(255,255,255,.04);transition:all .12s;}',
      '.lm-fi:hover{background:rgba(255,255,255,.1);color:#fff;}',
      '.lm-fi.sel{border-color:rgba(255,255,255,.45);background:rgba(255,255,255,.16);color:#fff;font-weight:700;}',
      '.lm-fi-alt{border-color:rgba(255,214,107,.25)!important;background:rgba(255,214,107,.08)!important;}',
      '.lm-fi-alt.sel{border-color:rgba(255,214,107,.6)!important;background:rgba(255,214,107,.22)!important;}',
      '.lm-fid{width:14px;height:14px;border-radius:3px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:10px;}',
      '#lm-mbody{flex:1;min-height:0;overflow:hidden;position:relative;cursor:grab;background:transparent;touch-action:none;}',
      '#lm-mbody.dr{cursor:grabbing;}#lm-fc{position:absolute;top:0;left:0;}',
      '#lm-info{position:absolute;z-index:3;bottom:14px;left:50%;transform:translateX(-50%);max-width:calc(100% - 148px);padding:7px 14px;border-radius:999px;background:rgba(7,11,22,.82);border:1px solid rgba(148,163,184,.22);color:rgba(226,232,240,.82);font-size:11px;font-weight:700;pointer-events:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;box-shadow:0 10px 24px rgba(0,0,0,.28);backdrop-filter:blur(10px);}',
      '#lm-zbtns{position:absolute;z-index:3;right:14px;top:14px;display:flex;flex-direction:column;gap:8px;}',
      '.lm-zb{width:42px;height:42px;border:1px solid rgba(148,163,184,.26);border-radius:8px;background:rgba(15,23,42,.78);color:#fff;font-size:20px;font-weight:800;cursor:pointer;box-shadow:0 10px 26px rgba(0,0,0,.26);backdrop-filter:blur(10px);transition:background .14s,transform .14s,border-color .14s;}',
      '.lm-zb:hover{background:rgba(37,99,235,.42);border-color:rgba(147,197,253,.5);transform:translateY(-1px);}',
      '#lm-legend-bar{position:relative;z-index:4;display:flex;flex-wrap:wrap;gap:5px 7px;padding:8px 12px calc(8px + env(safe-area-inset-bottom));background:rgba(12,16,28,.9);border-top:1px solid rgba(148,163,184,.16);box-shadow:0 -12px 32px rgba(0,0,0,.28);flex-shrink:0;align-items:flex-start;overflow:hidden;backdrop-filter:blur(14px);}',
      '.lm-lb-item{display:flex;align-items:center;gap:5px;min-height:24px;padding:3px 7px;border-radius:8px;background:rgba(255,255,255,.035);border:1px solid rgba(255,255,255,.06);font-size:10px;color:rgba(226,232,240,.72);white-space:nowrap;flex:0 0 auto;}',
      '.lm-lb-ico{width:16px;height:16px;border-radius:4px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:10px;}',
      '@media(max-width:860px){#lm-mhdr{padding-left:10px;padding-right:10px;}#lm-mhdr-btns{margin:0 -10px;padding:0 10px 2px;}#lm-b0,#lm-b1,#lm-bf,#lm-bpath,#lm-bguard,#lm-bclub,#lm-bhist{min-height:42px;padding:9px 12px;font-size:11px;flex:0 0 auto;}#lm-brefresh{height:40px;}#lm-zbtns{right:10px;top:10px;}.lm-zb{width:44px;height:44px;}#lm-info{bottom:12px;max-width:calc(100% - 116px);font-size:10px;padding:7px 11px;}#lm-legend-bar{gap:4px 6px;padding-left:10px;padding-right:10px;}.lm-lb-item{font-size:9px;padding:3px 6px;}}',
      '@media(max-width:520px){#lm-mttl{font-size:13px;}#lm-brefresh{width:42px;padding:0;font-size:0;}#lm-brefresh .lm-ref-ico{font-size:15px;}#lm-bc{width:42px;height:42px;}#lm-zbtns{top:auto;bottom:58px;right:10px;flex-direction:row;}#lm-info{left:10px;right:108px;bottom:58px;transform:none;max-width:none;text-align:left;}.lm-lb-item span{max-width:72px;overflow:hidden;text-overflow:ellipsis;}}',
      '#lm-tooltip{position:fixed;z-index:9999999;pointer-events:none;background:rgba(10,12,24,.95);border:1px solid rgba(255,255,255,.15);border-radius:10px;padding:8px 12px;color:#fff;font-size:12px;line-height:1.5;display:none;min-width:210px;max-width:260px;box-shadow:0 4px 16px rgba(0,0,0,.5);}',
      '#lm-tooltip b{display:block;font-size:13px;margin-bottom:2px;}',
      '#lm-tooltip .tt-desc{color:rgba(255,255,255,.75);font-size:11px;margin-bottom:3px;}',
      '#lm-tooltip .tt-guard{color:#ffd700;font-size:11px;margin-bottom:2px;font-weight:600;}',
      '#lm-tooltip .tt-coord{color:rgba(255,255,255,.5);font-size:11px;white-space:nowrap;}',
      '#lm-tooltip .tt-alt{color:#ffd66b;font-size:11px;margin-bottom:2px;}',
      '#lm-tooltip .tt-obj{font-size:11px;font-weight:700;margin-bottom:3px;padding:2px 6px;border-radius:4px;}',
      '#lm-tooltip .tt-trap{color:#ff8b8b;background:rgba(255,100,100,.12);}',
      '#lm-tooltip .tt-gift{color:#ffd66b;background:rgba(255,214,107,.12);}',
      '#lm-tooltip .tt-var{color:#c084fc;background:rgba(192,132,252,.1);}',
      '#lm-confirm{display:none;position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,.7);align-items:center;justify-content:center;}',
      '#lm-confirm.on{display:flex;}',
      '#lm-cbox{background:#1a1d2a;border:1px solid rgba(255,255,255,.15);border-radius:18px;padding:24px 28px;max-width:340px;width:90%;color:#fff;}',
      '#lm-cbox h3{margin:0 0 10px;font-size:16px;font-weight:800;}',
      '#lm-cbox p{margin:0 0 8px;font-size:13px;color:rgba(255,255,255,.7);line-height:1.5;}',
      '#lm-cbox .warn{color:#ffd66b;font-size:12px;margin-bottom:8px;}',
      '#lm-cbox .time{font-size:12px;color:rgba(255,255,255,.5);margin-bottom:16px;}',
      '#lm-cbtns{display:flex;gap:10px;}',
      '#lm-cyes{flex:1;padding:10px;border:none;border-radius:10px;background:#4a5ac7;color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
      '#lm-cno{flex:1;padding:10px;border:none;border-radius:10px;background:rgba(255,255,255,.08);color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
      '#lm-boost-confirm{display:none;position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,.75);align-items:center;justify-content:center;}',
      '#lm-boost-confirm.on{display:flex;}',
      '#lm-boost-box{background:linear-gradient(135deg,#1a1d2a,#0f1220);border:1px solid rgba(255,255,255,.15);border-radius:20px;padding:28px;max-width:320px;width:90%;color:#fff;text-align:center;}',
      '#lm-boost-box .lm-boost-conf-ico{font-size:42px;margin-bottom:12px;line-height:1;}',
      '#lm-boost-box .lm-boost-conf-title{font-size:18px;font-weight:800;margin-bottom:8px;}',
      '#lm-boost-box .lm-boost-conf-desc{font-size:13px;color:rgba(255,255,255,.65);margin-bottom:20px;line-height:1.5;}',
      '#lm-boost-box .lm-boost-conf-btns{display:flex;gap:10px;}',
      '#lm-boost-conf-yes{flex:1;padding:12px;border:none;border-radius:12px;background:#4a5ac7;color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
      '#lm-boost-conf-no{flex:1;padding:12px;border:none;border-radius:12px;background:rgba(255,255,255,.08);color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
      '#lm-toast{position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:99999999;pointer-events:none;display:flex;flex-direction:column;align-items:center;gap:6px;min-width:280px;max-width:min(480px,90vw);}',
      '.lm-toast-msg{width:100%;padding:10px 16px;border-radius:12px;background:linear-gradient(135deg,#1a2a4a,#0d1a30);border:1px solid rgba(74,144,226,.4);box-shadow:0 4px 20px rgba(0,0,0,.5);color:#fff;font-size:13px;font-weight:600;line-height:1.4;opacity:0;transform:translateY(-12px);transition:opacity .25s,transform .25s;}',
      '.lm-toast-msg.on{opacity:1;transform:translateY(0);}',
      '.lm-toast-msg b{color:#67e8f9;display:block;margin-bottom:2px;}',
      '#lm-confirm2{display:none;position:fixed;inset:0;z-index:9999999;background:rgba(0,0,0,.7);align-items:center;justify-content:center;}',
      '#lm-confirm2.on{display:flex;}',
      '#lm-cbox2{background:#1a1d2a;border:1px solid rgba(255,255,255,.15);border-radius:18px;padding:24px 28px;max-width:340px;width:90%;color:#fff;}',
      '#lm-cbox2 h3{margin:0 0 10px;font-size:16px;font-weight:800;}',
      '#lm-cbox2 p{margin:0 0 8px;font-size:13px;color:rgba(255,255,255,.7);line-height:1.5;}',
      '#lm-cbox2 .warn{color:#ffd66b;font-size:12px;margin-bottom:8px;}',
      '#lm-cbox2 .time{font-size:12px;color:rgba(255,255,255,.5);margin-bottom:16px;}',
      '#lm-cbtns2{display:flex;gap:10px;}',
      '#lm-cyes2{flex:1;padding:10px;border:none;border-radius:10px;background:#4a5ac7;color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
      '#lm-cno2{flex:1;padding:10px;border:none;border-radius:10px;background:rgba(255,255,255,.08);color:#fff;font-size:14px;font-weight:700;cursor:pointer;}',
      '.labyrinth-cell.lm-known{position:relative!important;}',
      '.lm-cell-icon{position:absolute!important;inset:0!important;display:flex!important;align-items:center!important;justify-content:center!important;font-size:clamp(5px,1.3vw,11px)!important;line-height:1!important;pointer-events:none!important;z-index:3!important;}',
      '.labyrinth-cell.lm-known:not(.labyrinth-cell--visited):not(.labyrinth-cell--available):not(.labyrinth-cell--current){background:rgba(255,255,255,.07)!important;opacity:0.85!important;}',
    ].join('\n');
    document.head.appendChild(style);
  }

  function buildDOM() {
    var wrap = document.createElement('div'); wrap.id='lm-wrap';
    wrap.innerHTML=
      '<div id="lm-hdr">'+
        '<div id="lm-ttl"><div id="lm-ttl-dot"></div>Карта лабиринта</div>'+
        '<div id="lm-btns">'+
          '<button id="lm-cleanbtn" title="Скрыть иконки скрипта с карты">👁 Чистый вид</button>'+
          '<button id="lm-rbtn">🔄 Обновить</button>'+
          '<button id="lm-pbtn">⛶ Полная карта</button>'+
        '</div>'+
      '</div>'+
      '<div id="lm-map-info">'+
        '<div class="lm-mi-card">'+
          '<div class="lm-mi-ico">🗺</div>'+
          '<div style="min-width:0;flex:1">'+
            '<div class="lm-mi-val" id="lm-mi-rooms-val">—</div>'+
            '<div class="lm-mi-lbl">комнат в базе</div>'+
            '<div class="lm-mi-sep"></div>'+
            '<div class="lm-mi-val-sm" id="lm-mi-depth-val">—</div>'+
            '<div class="lm-mi-lbl">макс. глубина</div>'+
          '</div>'+
        '</div>'+
        '<div class="lm-mi-card" id="lm-mi-emission-card">'+
          '<div class="lm-mi-ico" id="lm-mi-emission-ico">🌩</div>'+
          '<div style="min-width:0;flex:1">'+
            '<div class="lm-mi-val-sm" id="lm-mi-emission-val">—</div>'+
            '<div class="lm-mi-lbl" id="lm-mi-emission-lbl">выброс</div>'+
          '</div>'+
        '</div>'+
      '</div>'+
      '<div id="lm-action-bar">'+
        '<button class="lm-abar-btn" id="lm-wb-guard"><span class="lm-abar-ico">👑</span>Стражи</button>'+
        '<button class="lm-abar-btn" id="lm-wb-club"><span class="lm-abar-ico">🛡</span>Клуб</button>'+
        '<button class="lm-abar-btn" id="lm-wb-hist"><span class="lm-abar-ico">📜</span>История</button>'+
      '</div>'+
      '<div id="lm-inline-panel">'+
        '<div id="lm-wi-guard" style="display:none"><div class="lm-wi-loading">Загрузка...</div></div>'+
        '<div id="lm-wi-club"  style="display:none"><div class="lm-wi-loading">Загрузка...</div></div>'+
        '<div id="lm-wi-hist"  style="display:none"><div class="lm-hist-body"><div class="lm-wi-loading">Загрузка...</div></div></div>'+
      '</div>'+
      '<canvas id="lm-cvs" style="display:none"></canvas>'+
      '<div id="lm-st" style="display:none"></div>';

    var arenaEl = document.querySelector('.labyrinth__arena');
    var placed = false;
    if (arenaEl) { arenaEl.parentNode.insertBefore(wrap, arenaEl); placed = true; }
    if (!placed) {
      var fallbacks = ['.labyrinth__left', '.animesss-labyrinth'];
      for (var ti=0;ti<fallbacks.length;ti++) { var el=document.querySelector(fallbacks[ti]); if(el){el.parentNode.insertBefore(wrap,el.nextSibling);placed=true;break;} }
    }
    if (!placed) document.body.appendChild(wrap);

    var modal = document.createElement('div'); modal.id='lm-modal';
    modal.innerHTML=
      '<div id="lm-mhdr">'+
        '<div id="lm-mhdr-top">'+
          '<div id="lm-mttl">Полная карта лабиринта</div>'+
          '<div id="lm-hdr-right">'+
            '<button id="lm-brefresh" title="Обновить карту"><span class="lm-ref-ico">🔄</span> Обновить</button>'+
            '<button id="lm-bc">×</button>'+
          '</div>'+
        '</div>'+
        '<div id="lm-mhdr-btns">'+
          '<button id="lm-b1" title="Перейти к моей позиции">⚔ Моя позиция</button>'+
          '<button id="lm-b0" title="Центрировать старт">⚑ Старт</button>'+
          '<button id="lm-bguard" title="Показать моих стражей">👑 Мои стражи</button>'+
          '<button id="lm-bclub" title="Показать стражей клуба">🛡 Стражи клуба</button>'+
          '<button id="lm-bhist" title="История лабиринта">📜 История</button>'+
          '<button id="lm-bpath" title="Показать мой путь">👣 Мой путь</button>'+
          '<button id="lm-bf" title="Фильтр комнат">⊞ Фильтр</button>'+
        '</div>'+
      '</div>'+
      '<div id="lm-mbody"><canvas id="lm-fc"></canvas>'+
        '<div id="lm-info">Карта готова</div>'+
        '<div id="lm-zbtns"><button class="lm-zb" id="lm-zi">+</button><button class="lm-zb" id="lm-zo">−</button></div>'+
      '</div>'+
      '<div id="lm-legend-bar">'+
        LEGEND_ITEMS.map(function(i){ return '<div class="lm-lb-item"><div class="lm-lb-ico" style="background:'+clr(i[0])+';color:'+iclr(i[0])+'">'+ico(i[0])+'</div><span>'+i[1]+'</span></div>'; }).join('')+
      '</div>';
    document.body.appendChild(modal);

    var fpop = document.createElement('div'); fpop.id='lm-fpop'; fpop.className='lm-pop';
    fpop.innerHTML='<div id="lm-fpop-head"><div id="lm-fpop-title">⊞ Фильтр комнат</div><button id="lm-fpop-clear">× Сбросить</button></div><div id="lm-fpop-grid">'+
      LEGEND_ITEMS.map(function(i){ return '<div class="lm-fi" data-ev="'+i[0]+'"><div class="lm-fid" style="background:'+clr(i[0])+';color:'+iclr(i[0])+'">'+ico(i[0])+'</div><span>'+i[1]+'</span></div>'; }).join('')+
      '<div class="lm-fi lm-fi-alt" data-ev="__has_alt__"><div class="lm-fid" style="background:#2a1a4a;color:#ffd66b">👁</div><span>Разные типы</span></div>'+
      '</div>';
    modal.appendChild(fpop);

    var gpop = document.createElement('div'); gpop.id='lm-gpop'; gpop.className='lm-pop';
    gpop.innerHTML='<div class="lm-spop-head"><div><span class="lm-spop-title">👑 Мои стражи</span><span class="lm-spop-count" id="lm-gcount"></span></div></div><div class="lm-spop-list lm-spop-loading" id="lm-glist">Загрузка...</div>';
    modal.appendChild(gpop);

    var cpop = document.createElement('div'); cpop.id='lm-cpop'; cpop.className='lm-pop';
    cpop.innerHTML='<div class="lm-spop-head"><div><span class="lm-spop-title">🛡 Стражи клуба</span><span class="lm-spop-count" id="lm-ccount"></span></div></div><div class="lm-spop-list lm-spop-loading" id="lm-clist">Загрузка...</div>';
    modal.appendChild(cpop);

    var hpop = document.createElement('div'); hpop.id='lm-hpop'; hpop.className='lm-pop';
    hpop.innerHTML='<div class="lm-pop-head"><span class="lm-pop-title">📜 История лабиринта</span><a class="lm-pop-link" href="/acchistory/" target="_blank">↗ Полная история</a></div><div class="lm-hist-body"><div class="lm-spop-loading">Загрузка...</div></div>';
    modal.appendChild(hpop);

    var tt = document.createElement('div'); tt.id='lm-tooltip'; document.body.appendChild(tt);

    var boostConf = document.createElement('div'); boostConf.id='lm-boost-confirm';
    boostConf.innerHTML='<div id="lm-boost-box"><div class="lm-boost-conf-ico" id="lm-boost-conf-ico">👁</div><div class="lm-boost-conf-title" id="lm-boost-conf-title">Активировать буст?</div><div class="lm-boost-conf-desc" id="lm-boost-conf-desc"></div><div class="lm-boost-conf-btns"><button id="lm-boost-conf-no">Отмена</button><button id="lm-boost-conf-yes">Активировать</button></div></div>';
    document.body.appendChild(boostConf);

    var conf = document.createElement('div'); conf.id='lm-confirm';
    conf.innerHTML='<div id="lm-cbox"><h3>🔄 Обновить карту?</h3><p>Скрипт отправит ваши данные в облако и загрузит свежую карту.</p><div class="time" id="lm-ctime"></div><div id="lm-cbtns"><button id="lm-cno">Отмена</button><button id="lm-cyes">Обновить</button></div></div>';
    document.body.appendChild(conf);

    var conf2 = document.createElement('div'); conf2.id='lm-confirm2';
    conf2.innerHTML='<div id="lm-cbox2"><h3>🔄 Обновить карту?</h3><p>Карта будет полностью обновлена. Может занять до 2 минут.</p><div class="time" id="lm-ctime2"></div><div id="lm-cbtns2"><button id="lm-cno2">Отмена</button><button id="lm-cyes2">Обновить</button></div></div>';
    document.body.appendChild(conf2);

    var toast = document.createElement('div'); toast.id='lm-toast';
    document.body.appendChild(toast);
  }

  function closeAllPopups() {
    ['lm-gpop','lm-cpop','lm-fpop','lm-hpop'].forEach(function(id) {
      var el = document.getElementById(id); if (el) el.classList.remove('on');
    });
    ['lm-bguard','lm-bclub','lm-bf','lm-bhist'].forEach(function(id) {
      var el = document.getElementById(id); if (el) el.classList.remove('active');
    });
  }

  function fixPopupPos(popEl, btnEl) {
    var btn = btnEl || popEl.parentElement;
    var bRect = btn.getBoundingClientRect();
    popEl.style.top  = (bRect.bottom + 8) + 'px';
    popEl.style.left = bRect.left + 'px';
    popEl.style.right = 'auto';
    var pRect = popEl.getBoundingClientRect();
    var over = pRect.right - (window.innerWidth - 8);
    if (over > 0) popEl.style.left = Math.max(8, bRect.left - over) + 'px';
    if (pRect.bottom > window.innerHeight - 8) {
      popEl.style.top = Math.max(8, bRect.top - pRect.height - 8) + 'px';
    }
  }

  function bindEvents() {
    var conf = document.getElementById('lm-confirm');
    var fpop = document.getElementById('lm-fpop');

    elMbody   = document.getElementById('lm-mbody');
    elTooltip = document.getElementById('lm-tooltip');
    elInfo    = document.getElementById('lm-info');
    elSt      = document.getElementById('lm-st');
    elFc      = document.getElementById('lm-fc');
    elCvs     = document.getElementById('lm-cvs');

    function wbClose() {
      _wbActive = null;
      document.getElementById('lm-inline-panel').classList.remove('on');
      ['lm-wb-guard','lm-wb-club','lm-wb-hist'].forEach(function(id){
        var b = document.getElementById(id); if(b) b.classList.remove('active');
      });
      ['lm-wi-guard','lm-wi-club','lm-wi-hist'].forEach(function(id){
        var el = document.getElementById(id); if(el) el.style.display = 'none';
      });
    }

    function wbOpen(tab) {
      if (_wbActive === tab) { wbClose(); return; }
      wbClose();
      _wbActive = tab;
      document.getElementById('lm-inline-panel').classList.add('on');
      document.getElementById('lm-wb-'+tab).classList.add('active');
      var panel = document.getElementById('lm-wi-'+tab);
      if (panel) panel.style.display = 'block';
      if (tab === 'guard') {
        loadWbGuardian('user', document.getElementById('lm-wi-guard'));
      } else if (tab === 'club') {
        loadWbGuardian('club', document.getElementById('lm-wi-club'));
      } else if (tab === 'hist') {
        var hBody = document.querySelector('#lm-wi-hist .lm-hist-body');
        if (hBody) {
          if (Object.keys(histCache).length === 0) {
            histTotal = null; histCurPage = 1;
            hBody.innerHTML = '<div class="lm-wi-loading">Загрузка...</div>';
            loadHistPage(1, function(result) { renderHistInline(result, 1); });
          } else {
            renderHistInline(histCache[histCurPage] || histCache[1] || {rows:[],totalPages:1}, histCurPage || 1);
          }
        }
      }
    }

    document.getElementById('lm-wb-guard').addEventListener('click', function(){ wbOpen('guard'); });
    document.getElementById('lm-wb-club').addEventListener('click',  function(){ wbOpen('club');  });
    document.getElementById('lm-wb-hist').addEventListener('click',  function(){ wbOpen('hist');  });

    var inlinePanel = document.getElementById('lm-inline-panel');
    if (inlinePanel) inlinePanel.addEventListener('click', function(e){ e.stopPropagation(); });

    document.getElementById('lm-rbtn').addEventListener('click', function() {
      var conf2 = document.getElementById('lm-confirm2');
      var timeEl2 = document.getElementById('lm-ctime2');
      if (conf2) {
        if (timeEl2) {
          var lastT2 = window.lmLS.get(LS_FULL_REFRESH_KEY);
          if (lastT2) { var diff2=Math.floor((Date.now()-parseInt(lastT2))/60000); timeEl2.textContent=diff2<1?'Последнее: только что':'Последнее: '+diff2+' мин. назад'; }
          else timeEl2.textContent='Ещё не обновляли.';
        }
        conf2.classList.add('on');
      }
    });
    document.getElementById('lm-cno').addEventListener('click', function(){ conf.classList.remove('on'); });
    conf.addEventListener('click', function(e){ if(e.target===conf) conf.classList.remove('on'); });
    document.getElementById('lm-cyes').addEventListener('click', function() {
      conf.classList.remove('on');
      window.lmLS.set(LS_REFRESH_KEY, String(Date.now()));
      doRefresh();
    });

    document.getElementById('lm-pbtn').addEventListener('click', function(){ openFull(); });

    document.getElementById('lm-cleanbtn').addEventListener('click', function() {
      cleanViewMode = !cleanViewMode;
      this.classList.toggle('active', cleanViewMode);
      this.textContent = cleanViewMode ? '👁 Карта скрипта' : '👁 Чистый вид';
      if (cleanViewMode) applyCleanView(); else removeCleanView();
    });
    document.getElementById('lm-bc').addEventListener('click', closeFull);

    document.getElementById('lm-brefresh').addEventListener('click', function() {
      var conf2 = document.getElementById('lm-confirm2');
      var timeEl2 = document.getElementById('lm-ctime2');
      if (timeEl2) {
        var lastT = window.lmLS.get(LS_FULL_REFRESH_KEY);
        if (lastT) {
          var diff = Math.floor((Date.now() - parseInt(lastT)) / 60000);
          timeEl2.textContent = diff < 1 ? 'Последнее обновление: только что' : 'Последнее обновление: ' + diff + ' мин. назад';
        } else { timeEl2.textContent = 'Вы ещё не обновляли информацию.'; }
      }
      conf2.classList.add('on');
    });

    var conf2El = document.getElementById('lm-confirm2');
    document.getElementById('lm-cno2').addEventListener('click', function() { conf2El.classList.remove('on'); });
    conf2El.addEventListener('click', function(e) { if (e.target === conf2El) conf2El.classList.remove('on'); });
    document.getElementById('lm-cyes2').addEventListener('click', function() {
      conf2El.classList.remove('on');
      window.lmLS.set(LS_FULL_REFRESH_KEY, String(Date.now()));
      doFullRefresh();
    });

    document.addEventListener('keydown', function(e){ if(e.key==='Escape') closeFull(); });
    document.getElementById('lm-b0').addEventListener('click', function(){
      highlightRoom={x:0,y:0,until:Date.now()+2000};
      if(!fmOpen) { openFull(true); setTimeout(function(){ centerOn(0,0,FULL_FOCUS_SCALE); }, 50); }
      else { centerOn(0,0,FULL_FOCUS_SCALE); }
    });
    document.getElementById('lm-b1').addEventListener('click', function(){
      var c=curPos();
      highlightRoom={x:c.x,y:c.y,until:Date.now()+2000};
      if(!fmOpen) { openFull(true); setTimeout(function(){ centerOn(c.x,c.y,FULL_FOCUS_SCALE); }, 50); }
      else { centerOn(c.x,c.y,FULL_FOCUS_SCALE); }
    });
    document.getElementById('lm-zi').addEventListener('click', function(){ fmScale=Math.min(fmScale*ZOOM_IN,ZOOM_MAX); drawFull(); });
    document.getElementById('lm-zo').addEventListener('click', function(){ fmScale=Math.max(fmScale*ZOOM_OUT,ZOOM_MIN); drawFull(); });

    document.getElementById('lm-bpath').addEventListener('click', function(){
      showMyPath=!showMyPath;
      this.classList.toggle('active',showMyPath);
      this.textContent=showMyPath?'👣 Все игроки':'👣 Мой путь';
      if (showMyPath && !window._lmMyPathData) {
        window.lmFetch(VPS_URL+'/my/path', { headers: vpsAuthHeaders(false) })
          .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
          .then(function(d){
            if (d && Array.isArray(d.steps)) {
              window._lmMyPathData = {};
              d.steps.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
              window._lmHistSteps = d.steps; // 8.0.4: лінія історичного шляху на повній карті (раніше не заповнювалось)
              if (Array.isArray(d.emission_cells)) window._lmEmissionCells = new Set(d.emission_cells.map(function(c){ return c.x+'_'+c.y; }));
              drawFull();
            }
          }).catch(function(){});
        return;
      }
      drawFull();
    });

    document.getElementById('lm-bf').addEventListener('click', function(e){
      e.stopPropagation();
      var wasOpen = fpop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) { fpop.classList.add('on'); this.classList.add('active'); fixPopupPos(fpop, this); }
    });
    fpop.addEventListener('click', function(e){
      e.stopPropagation();
      var fi = e.target.closest('.lm-fi[data-ev]');
      if (fi) {
        var ev = fi.dataset.ev;
        if(filterSet[ev]) delete filterSet[ev]; else filterSet[ev]=true;
        fi.classList.toggle('sel',!!filterSet[ev]);
        var n=Object.keys(filterSet).length, bf=document.getElementById('lm-bf');
        bf.childNodes[0].textContent=n>0?'⊞ Фильтр ('+n+')':'⊞ Фильтр';
        drawFull(); return;
      }
      if (e.target.id==='lm-fpop-clear') {
        filterSet={};
        fpop.querySelectorAll('.lm-fi').forEach(function(x){ x.classList.remove('sel'); });
        var bf2=document.getElementById('lm-bf');
        bf2.childNodes[0].textContent='⊞ Фильтр'; bf2.classList.remove('active');
        fpop.classList.remove('on'); drawFull();
      }
    });

    document.getElementById('lm-bguard').addEventListener('click', function(e){
      var pop = document.getElementById('lm-gpop'), wasOpen = pop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) { pop.classList.add('on'); this.classList.add('active'); loadGuardianPopup('user'); fixPopupPos(pop, this); }
    });
    document.getElementById('lm-bclub').addEventListener('click', function(e){
      var pop = document.getElementById('lm-cpop'), wasOpen = pop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) { pop.classList.add('on'); this.classList.add('active'); loadGuardianPopup('club'); fixPopupPos(pop, this); }
    });

    document.getElementById('lm-bhist').addEventListener('click', function(e){
      e.stopPropagation();
      var pop = document.getElementById('lm-hpop'), wasOpen = pop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) {
        pop.classList.add('on'); this.classList.add('active');
        if (Object.keys(histCache).length === 0) {
          histTotal = null; histCurPage = 1;
          loadAndRenderHist(1);
        } else {
          renderHistPopup(histCache[histCurPage] || histCache[1] || {rows:[],totalPages:1}, histCurPage || 1);
        }
        fixPopupPos(pop, document.getElementById('lm-bhist'));
      }
    });

    ['lm-gpop','lm-cpop','lm-fpop','lm-hpop'].forEach(function(id) {
      var el = document.getElementById(id);
      if (el) el.addEventListener('click', function(e){ e.stopPropagation(); });
    });

    document.addEventListener('click', function(e){
      var modalPopupIds = ['lm-bguard','lm-bclub','lm-bf','lm-bhist'];
      var insideModal = modalPopupIds.some(function(id){
        var b = document.getElementById(id);
        return b && b.contains(e.target);
      });
      if (!insideModal) closeAllPopups();
      var wrap2 = document.getElementById('lm-wrap');
      if (wrap2 && !wrap2.contains(e.target)) {
        var panel = document.getElementById('lm-inline-panel');
        if (panel && panel.classList.contains('on')) {
          document.getElementById('lm-inline-panel').classList.remove('on');
          ['lm-wb-guard','lm-wb-club','lm-wb-hist'].forEach(function(id){
            var b=document.getElementById(id); if(b) b.classList.remove('active');
          });
          ['lm-wi-guard','lm-wi-club','lm-wi-hist'].forEach(function(id){
            var el=document.getElementById(id); if(el) el.style.display='none';
          });
          _wbActive = null;
        }
      }
    });

    elMbody.addEventListener('mousedown', function(e){ drag=true; dsx=e.clientX-fmX; dsy=e.clientY-fmY; elMbody.classList.add('dr'); });
    document.addEventListener('mousemove', function(e){
      if (!drag) return;
      fmX=e.clientX-dsx; fmY=e.clientY-dsy;
      if (rafId) return;
      rafId=requestAnimationFrame(function(){ rafId=null; drawFull(); });
    });
    document.addEventListener('mouseup', function(){ drag=false; elMbody.classList.remove('dr'); if(rafId){ cancelAnimationFrame(rafId); rafId=null; } });

    var pinchDist0 = 0, pinchScale0 = 1, pinchFmX0 = 0, pinchFmY0 = 0, pinchCx = 0, pinchCy = 0;
    var tapStartX = 0, tapStartY = 0;

    function getTouchDist(e) {
      var dx = e.touches[0].clientX - e.touches[1].clientX;
      var dy = e.touches[0].clientY - e.touches[1].clientY;
      return Math.sqrt(dx*dx + dy*dy);
    }

    elMbody.addEventListener('touchstart', function(e) {
      e.preventDefault();
      if (e.touches.length === 1) {
        pinchDist0 = 0;
        var t = e.touches[0];
        drag = true; dsx = t.clientX - fmX; dsy = t.clientY - fmY;
        tapStartX = t.clientX; tapStartY = t.clientY;
      } else if (e.touches.length === 2) {
        drag = false; pinchDist0 = getTouchDist(e); pinchScale0 = fmScale;
        pinchFmX0 = fmX; pinchFmY0 = fmY;
        var rect = elMbody.getBoundingClientRect();
        pinchCx = ((e.touches[0].clientX + e.touches[1].clientX) / 2) - rect.left;
        pinchCy = ((e.touches[0].clientY + e.touches[1].clientY) / 2) - rect.top;
      }
    }, { passive: false });

    elMbody.addEventListener('touchmove', function(e) {
      e.preventDefault();
      if (e.touches.length === 2 && pinchDist0 > 0) {
        var dist = getTouchDist(e);
        var ns = Math.max(ZOOM_MIN, Math.min(pinchScale0 * (dist / pinchDist0), ZOOM_MAX));
        fmX = pinchCx - (pinchCx - pinchFmX0) * (ns / pinchScale0);
        fmY = pinchCy - (pinchCy - pinchFmY0) * (ns / pinchScale0);
        fmScale = ns;
        if (rafId) return;
        rafId = requestAnimationFrame(function() { rafId = null; drawFull(); });
      } else if (e.touches.length === 1 && drag) {
        var t = e.touches[0]; fmX = t.clientX - dsx; fmY = t.clientY - dsy;
        if (rafId) return;
        rafId = requestAnimationFrame(function() { rafId = null; drawFull(); });
      }
    }, { passive: false });

    elMbody.addEventListener('touchend', function(e) {
      e.preventDefault(); pinchDist0 = 0;
      if (e.touches.length === 0) { drag = false; }
      else if (e.touches.length === 1) {
        var t = e.touches[0]; dsx = t.clientX - fmX; dsy = t.clientY - fmY; drag = true;
      }
    }, { passive: false });

    elMbody.addEventListener('wheel',function(e){
      e.preventDefault();
      var rect=elMbody.getBoundingClientRect(), mx=e.clientX-rect.left, my=e.clientY-rect.top;
      var factor=e.deltaY>0?ZOOM_OUT:ZOOM_IN, ns=Math.max(ZOOM_MIN,Math.min(fmScale*factor,ZOOM_MAX));
      fmX=mx-(mx-fmX)*(ns/fmScale); fmY=my-(my-fmY)*(ns/fmScale); fmScale=ns; drawFull();
    },{passive:false});

    var ttRafId=null;
    elMbody.addEventListener('mousemove',function(e){
      if (drag) { elTooltip.style.display='none'; return; }
      if (ttRafId) return;
      ttRafId=requestAnimationFrame(function(){
        ttRafId=null;
        var rect=elMbody.getBoundingClientRect(), cs=CELL_SIZE*fmScale;
        var cx=Math.floor((e.clientX-rect.left-fmX)/cs), cy=Math.floor((e.clientY-rect.top-fmY)/cs);
        var rooms=allRooms(), room=rooms[cx+'_'+cy];
        if (room) {
          var cur2=curPos(), isCur=cx===cur2.x&&cy===cur2.y;
          var gLine=room.guardian?'<div class="tt-guard">'+(room.guardian==='guardian_user'?'👑 Личный страж':room.guardian==='guardian_club'?'🛡 Страж клуба':'⚐ Можно захватить')+'</div>':'';
          var mmAltFiltered = (room.altTypes||[]).filter(function(t){ return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift'&&t!=='shield_block'; });
          var mmAltLine = '';
          if (mmAltFiltered.length) {
            var mmIsVar = room.event!=='unknown' && VARIABLE_EVENTS[room.event];
            if (mmIsVar) {
              var mmAll = [ico(room.event)+' '+roomName(room.event)].concat(mmAltFiltered.map(function(t){ return ico(t)+' '+roomName(t); }));
              mmAltLine = '<div class="tt-alt tt-var">🔀 Варианты: '+mmAll.join(' / ')+'</div>';
            } else {
              mmAltLine = '<div class="tt-alt">Также видели: '+mmAltFiltered.map(function(t){ return ico(t)+' '+roomName(t); }).join(', ')+'</div>';
            }
          }
          var mmObjLine = '';
          if (room.roomObject==='room_trap') mmObjLine='<div class="tt-obj tt-trap">⚠ Здесь установлена ловушка</div>';
          else if (room.roomObject==='room_gift') mmObjLine='<div class="tt-obj tt-gift">🎁 Здесь оставлен подарок</div>';
          elTooltip.innerHTML='<b>'+(isCur?'⚔ Ваша позиция':ico(room.event)+' '+roomName(room.event))+'</b>'+
            (!isCur&&roomDesc(room.event)?'<div class="tt-desc">'+roomDesc(room.event)+'</div>':'')+mmObjLine+mmAltLine+gLine+
            '<div class="tt-coord">'+formatCoords(cx,cy)+'</div>';
          elTooltip.style.display='block';
          var tx=e.clientX+14, ty=e.clientY-10;
          if(tx+270>window.innerWidth) tx=e.clientX-274;
          elTooltip.style.left=tx+'px'; elTooltip.style.top=ty+'px';
        } else elTooltip.style.display='none';
      });
    });
    elMbody.addEventListener('mouseleave',function(){ elTooltip.style.display='none'; });

    var BOOST_INFO = {
      'labyrinthBoostVisionBtn':  { ico:'👁',  name:'Всевидящее oko',          desc:'Откроет тип следующей комнаты перед входом' },
      'labyrinthBoostShieldBtn':  { ico:'🛡',  name:'Щит',                     desc:'Защитит от следующего штрафа в лабиринте' },
      'labyrinthBoostRewardBtn':  { ico:'🎯',  name:'Гарантированная награда', desc:'Гарантирует награду в следующей комнате' },
      'labyrinthBoostBerserkBtn': { ico:'⚔',  name:'Режим берсерка',           desc:'Усиливает урон по боссам' }
    };
    var _pendingBoostBtn = null, _pendingIsRoomAction = false, _pendingIsBuy = false;

    function interceptRoomAction(btn, ico2, name, desc) {
      if (!btn) return;
      btn.addEventListener('click', function(e) {
        if (_roomActionConfirmed) return;
        e.stopImmediatePropagation(); e.preventDefault();
        document.getElementById('lm-boost-conf-ico').textContent   = ico2;
        document.getElementById('lm-boost-conf-title').textContent = name + '?';
        document.getElementById('lm-boost-conf-desc').textContent  = desc;
        _pendingBoostBtn = btn; _pendingIsRoomAction = true;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    }
    var _roomActionConfirmed = false;

    interceptRoomAction(document.getElementById('labyrinthPlaceTrapBtn'), '🕸', 'Оставить ловушку', 'Ловушка остановит первого игрока, который войдёт сюда. Стоимость — 1 ход.');
    interceptRoomAction(document.getElementById('labyrinthPlaceGiftBtn'), '🎁', 'Оставить подарок', 'Подарок сразу достанется следующему игроку. Стоимость — 1 ход.');
    interceptRoomAction(document.getElementById('labyrinthRobMineBtn'),   '⚔', 'Ограбить шахту',   'Вы заберёте добычу из шахты другого игрока.');
    interceptRoomAction(document.getElementById('labyrinthEchoRepeatBtn'), '👣', 'Повторить путь', 'Ты повторишь путь другого игрока в этой комнате.');
    interceptRoomAction(document.getElementById('labyrinthClubWarSoftBtn'),   '🏴', 'Лёгкий рейд',       'Небольшая атака на вражеский клуб.');
    interceptRoomAction(document.getElementById('labyrinthClubWarNormalBtn'), '⚔',  'Обычный штурм',     'Стандартная атака на вражеский клуб.');
    interceptRoomAction(document.getElementById('labyrinthClubWarFullBtn'),   '💥', 'Полное вторжение',  'Мощная атака на вражеский клуб. Требует больше ресурсов.');
    interceptRoomAction(document.getElementById('labyrinthHelpMineBtn'),  '🤝', 'Помочь со сбором', 'Вы поможете другому игроку собрать добычу быстрее.');

    var _mineConfirmed = false;
    var collectMineBtn = document.getElementById('labyrinthCollectMineBtn');
    if (collectMineBtn) {
      collectMineBtn.addEventListener('click', function(e) {
        if (_mineConfirmed) return;
        e.stopImmediatePropagation(); e.preventDefault();
        var pm = window.labyrinthData && window.labyrinthData.personalMine;
        var desc = pm ? 'Забрать ' + (pm.pending_acc || 0) + ' АСС и ' + (pm.pending_cards || 0) + ' карт из шахты ур. ' + (pm.level || 1) + '.' : 'Забрать накопленную добычу из персональной шахты.';
        document.getElementById('lm-boost-conf-ico').textContent   = '⛏';
        document.getElementById('lm-boost-conf-title').textContent = 'Забрать добычу?';
        document.getElementById('lm-boost-conf-desc').textContent  = desc;
        _pendingBoostBtn = collectMineBtn; _pendingIsRoomAction = true;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    }

    var upgradeMineBtn = document.getElementById('labyrinthUpgradeMineBtn');
    if (upgradeMineBtn) {
      upgradeMineBtn.addEventListener('click', function(e) {
        if (_mineConfirmed) return;
        e.stopImmediatePropagation(); e.preventDefault();
        var pm = window.labyrinthData && window.labyrinthData.personalMine;
        var price = pm ? (pm.next_upgrade_price || pm.upgrade_price || 1000) : 1000;
        var lvl   = pm ? (pm.level || 1) : 1;
        document.getElementById('lm-boost-conf-ico').textContent   = '⬆';
        document.getElementById('lm-boost-conf-title').textContent = 'Улучшить шахту?';
        document.getElementById('lm-boost-conf-desc').textContent  = 'Улучшение с уровня ' + lvl + ' до ' + (lvl + 1) + ' стоит ' + price + ' АСС.';
        _pendingBoostBtn = upgradeMineBtn; _pendingIsRoomAction = true;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    }

    var _buyConfirmed = false;
    var buyBtn = document.getElementById('labyrinthBuyAttemptBtn');
    if (buyBtn) {
      buyBtn.addEventListener('click', function(e) {
        if (_buyConfirmed) return;
        e.stopImmediatePropagation(); e.preventDefault();
        var buyText    = document.getElementById('labyrinthBuyAttemptText');
        var buySubtext = document.getElementById('labyrinthBuyAttemptSubtext');
        var descText   = (buyText ? buyText.textContent.trim() : '') + (buySubtext ? ' ' + buySubtext.textContent.trim() : '');
        document.getElementById('lm-boost-conf-ico').textContent   = '👣';
        document.getElementById('lm-boost-conf-title').textContent = 'Купить +1 ход?';
        document.getElementById('lm-boost-conf-desc').textContent  = descText || 'Подтвердите покупку дополнительного хода';
        _pendingBoostBtn = buyBtn; _pendingIsBuy = true;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    }

    var _boostConfirmed = false;
    Object.keys(BOOST_INFO).forEach(function(btnId) {
      var btn = document.getElementById(btnId);
      if (!btn) return;
      btn.addEventListener('click', function(e) {
        if (_boostConfirmed) return;
        if (btn.disabled) return;
        e.stopImmediatePropagation(); e.preventDefault();
        var info = BOOST_INFO[btnId];
        document.getElementById('lm-boost-conf-ico').textContent   = info.ico;
        document.getElementById('lm-boost-conf-title').textContent = 'Активировать «' + info.name + '»?';
        document.getElementById('lm-boost-conf-desc').textContent  = info.desc;
        _pendingBoostBtn = btn;
        document.getElementById('lm-boost-confirm').classList.add('on');
      }, true);
    });

    document.getElementById('lm-boost-conf-no').addEventListener('click', function() {
      document.getElementById('lm-boost-confirm').classList.remove('on');
      _pendingBoostBtn = null; _pendingIsBuy = false; _pendingIsRoomAction = false;
    });
    document.getElementById('lm-boost-confirm').addEventListener('click', function(e) {
      if (e.target === this) { this.classList.remove('on'); _pendingBoostBtn = null; _pendingIsBuy = false; _pendingIsRoomAction = false; }
    });
    document.getElementById('lm-boost-conf-yes').addEventListener('click', function() {
      document.getElementById('lm-boost-confirm').classList.remove('on');
      if (_pendingBoostBtn) {
        var btn = _pendingBoostBtn;
        var isBuy = _pendingIsBuy, isRoom = _pendingIsRoomAction;
        _pendingBoostBtn = null; _pendingIsBuy = false; _pendingIsRoomAction = false;
        if (isBuy)       { _buyConfirmed = true; }
        else if (isRoom) { _roomActionConfirmed = true; _mineConfirmed = true; }
        else             { _boostConfirmed = true; }
        btn.click();
        _boostConfirmed = false; _buyConfirmed = false; _roomActionConfirmed = false; _mineConfirmed = false;
      }
    });

    var tooltipPinned = false;
    function showTooltipAt(clientX, clientY) {
      var rect = elMbody.getBoundingClientRect(), cs = CELL_SIZE * fmScale;
      var cx = Math.floor((clientX - rect.left - fmX) / cs);
      var cy = Math.floor((clientY - rect.top  - fmY) / cs);
      var rooms = allRooms(), room = rooms[cx+'_'+cy];
      if (room) {
        var cur2 = curPos(), isCur = cx===cur2.x && cy===cur2.y;
        var gLine = room.guardian ? '<div class="tt-guard">'+(room.guardian==='guardian_user'?'👑 Личный страж':room.guardian==='guardian_club'?'🛡 Страж клуба':'⚐ Можно захватить')+'</div>' : '';
        var altFiltered2 = (room.altTypes||[]).filter(function(t){ return t!=='room_player'&&t!=='room_trap'&&t!=='room_gift'&&t!=='shield_block'; });
        var altLine2 = '';
        if (altFiltered2.length) {
          var isVar2 = room.event!=='unknown' && VARIABLE_EVENTS[room.event];
          if (isVar2) {
            var allVar2 = [ico(room.event)+' '+roomName(room.event)].concat(altFiltered2.map(function(t){ return ico(t)+' '+roomName(t); }));
            altLine2 = '<div class="tt-alt tt-var">🔀 Варианты: '+allVar2.join(' / ')+'</div>';
          } else {
            altLine2 = '<div class="tt-alt">Также видели: '+altFiltered2.map(function(t){ return ico(t)+' '+roomName(t); }).join(', ')+'</div>';
          }
        }
        var objLine2 = '';
        if (room.roomObject==='room_trap') objLine2='<div class="tt-obj tt-trap">⚠ Здесь установлена ловушка</div>';
        else if (room.roomObject==='room_gift') objLine2='<div class="tt-obj tt-gift">🎁 Здесь оставлен подарок</div>';
        var _emKey = cx+'_'+cy;
        var _emLine = (room.emissionVisited || (window._lmEmissionCells && window._lmEmissionCells.has(_emKey)))
          ? '<div style="color:#ffd700;font-size:11px;margin-bottom:2px;">⚡ Посещено во время выброса</div>' : '';
        var _dispEv = (room.event === 'unknown' && room.emissionEv) ? room.emissionEv : room.event;
        var _emOnlyStyle = (room.event === 'unknown' && room.emissionEv)
          ? ' style="border:1px solid rgba(255,60,60,0.7);padding:1px 5px;border-radius:3px;"' : '';
        elTooltip.innerHTML = '<b'+_emOnlyStyle+'>'+(isCur?'⚔ Ваша позиция':ico(_dispEv)+' '+roomName(_dispEv))+'</b>'+
          (!isCur&&roomDesc(room.event)?'<div class="tt-desc">'+roomDesc(room.event)+'</div>':'')+_emLine+objLine2+altLine2+gLine+
          '<div class="tt-coord">'+formatCoords(cx,cy)+'</div>';
        var tw = Math.min(260, window.innerWidth - 16);
        var tx = clientX - tw / 2;
        if (tx < 8) tx = 8;
        if (tx + tw + 8 > window.innerWidth) tx = window.innerWidth - tw - 8;
        var ty = clientY - 130;
        if (ty < 60) ty = clientY + 20;
        elTooltip.style.left = tx + 'px'; elTooltip.style.top = ty + 'px';
        elTooltip.style.display = 'block'; tooltipPinned = true;
        return true;
      }
      elTooltip.style.display = 'none'; tooltipPinned = false; return false;
    }

    elMbody.addEventListener('click', function(e) { showTooltipAt(e.clientX, e.clientY); });
    elMbody.addEventListener('touchend', function(e) {
      if (pinchDist0 > 0) return;
      var t = e.changedTouches[0];
      var dx = t.clientX - tapStartX, dy = t.clientY - tapStartY;
      if (Math.sqrt(dx*dx + dy*dy) < 10) showTooltipAt(t.clientX, t.clientY);
    }, { passive: false });

    document.addEventListener('click', function(e) {
      if (tooltipPinned && !elMbody.contains(e.target)) {
        elTooltip.style.display = 'none'; tooltipPinned = false;
      }
    });
  }

  var _toastEl = null, _toastTimer = null;

  function showToast(title, sub, pct) {
    if (!_toastEl) _toastEl = document.getElementById('lm-toast');
    if (!_toastEl) return;
    var pctNum = (pct === undefined) ? -1 : pct;
    var barHtml = pctNum >= 0 ? '<div style="height:3px;border-radius:999px;background:rgba(255,255,255,.15);margin-top:4px;overflow:hidden"><div style="height:3px;border-radius:999px;background:linear-gradient(90deg,#4a90e2,#67e8f9);width:'+pctNum+'%"></div></div>' : '';
    _toastEl.innerHTML = '<div class="lm-toast-msg on"><b>'+title+'</b>'+(sub ? sub : '')+barHtml+'</div>';
    if (_toastTimer) clearTimeout(_toastTimer);
  }

  function hideToast(delay) {
    if (_toastTimer) clearTimeout(_toastTimer);
    _toastTimer = setTimeout(function() {
      if (!_toastEl) _toastEl = document.getElementById('lm-toast');
      if (_toastEl) _toastEl.innerHTML = '';
    }, delay || 2500);
  }

  function doFullRefresh() {
    var btn = document.getElementById('lm-brefresh');
    if (!btn || btn.classList.contains('loading')) return;
    btn.classList.add('loading'); btn.disabled = true;
    var d = mapData();
    var steps = d && d.steps && d.steps.length ? d.steps : [];
    var domOwn = getCurrentRoomOwnership();
    showToast('Обновление карты...', '', 10);
    var p = steps.length > 0 ? pushSteps(steps, SESSION_ID) : Promise.resolve();
    p.then(function() {
      showToast('Обновление карты...', '', 30);
      return new Promise(function(resolve) {
        ownershipCache = null;
        guardianDataCache = {user: null, club: null};
        fetchOwnership(function(hist) {
          var allOwn = domOwn.concat(hist);
          if (allOwn.length) pushSteps(allOwn, SESSION_ID+'_own').then(resolve).catch(resolve);
          else resolve();
        });
      });
    }).then(function() {
      showToast('Обновление карты...', '', 55);
      return new Promise(function(resolve) { loadCloud(resolve, function(count){ showToast('Обновление карты...', '', Math.min(88,55+Math.floor(count/500))); }); });
    }).then(function() {
      histCache = {}; histTotal = null; histCurPage = 1;
      window.lmLS.set(LS_HIST_DATE_KEY, new Date().toISOString());
      function loadPageSeq(pageNum) {
        return new Promise(function(resolve) {
          setTimeout(function() {
            loadHistPage(pageNum, function(result) {
              var total = histTotal || result.totalPages || 1;
              var pct = Math.round(60 + (pageNum / Math.max(total,1)) * 35);
              showToast('Обновление карты...', '', pct);
              if (pageNum < total) loadPageSeq(pageNum + 1).then(resolve);
              else resolve();
            });
          }, pageNum === 1 ? 0 : 700);
        });
      }
      showToast('Обновление карты...', '', 60);
      return loadPageSeq(1);
    }).then(function() {
      var pop = document.getElementById('lm-hpop');
      if (pop && pop.classList.contains('on')) renderHistPopup(histCache[1] || {rows:[], totalPages:1}, 1);
      checkAndSyncClub(true);
      showToast('Карта обновлена!', '', 100);
      hideToast(3000);
      btn.classList.remove('loading'); btn.disabled = false;
    }).catch(function(e) {
      showToast('Ошибка обновления', 'Попробуйте ещё раз');
      hideToast(4000);
      btn.classList.remove('loading'); btn.disabled = false;
    });
  }

  function fetchTrapBacksFromRollbackPage(callback) {
    var username = window.visitor_name || '';
    if (!username) { callback(null); return; }
    fetch('/rollback/', { credentials: 'same-origin' })
      .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
      .then(function(html){
        var parser = new DOMParser();
        var doc = parser.parseFromString(html, 'text/html');
        var found = null;
        var userLink = doc.querySelector('a[href="/user/' + username + '/"].card-inline');
        if (userLink) {
          var valEl = userLink.querySelector('.users-top__value');
          if (valEl) {
            var m = valEl.textContent.replace(/[^0-9]/g, '');
            if (m) found = parseInt(m, 10);
          }
        }
        if (found === null) {
          var allCards = doc.querySelectorAll('.ncard__users-toplist .card-inline');
          allCards.forEach(function(card) {
            if (found !== null) return;
            var nameEl = card.querySelector('.card-inline__name');
            if (!nameEl) return;
            var name = nameEl.textContent.trim();
            if (name.toLowerCase() !== username.toLowerCase()) return;
            var valEl2 = card.querySelector('.users-top__value');
            if (valEl2) {
              var m2 = valEl2.textContent.replace(/[^0-9]/g, '');
              if (m2) found = parseInt(m2, 10);
            }
          });
        }
        var place = null;
        if (userLink) {
          var p = parseInt(userLink.getAttribute('data-place') || '', 10);
          if (!isNaN(p)) place = p;
        } else if (found !== null) {
          var allCards2 = doc.querySelectorAll('.ncard__users-toplist .card-inline');
          allCards2.forEach(function(card) {
            if (place !== null) return;
            var nameEl = card.querySelector('.card-inline__name');
            if (!nameEl || nameEl.textContent.trim().toLowerCase() !== username.toLowerCase()) return;
            var p2 = parseInt(card.getAttribute('data-place') || '', 10);
            if (!isNaN(p2)) place = p2;
          });
        }
        if (place !== null) window._lmRollbackPlace = place;
        lmDebug('[LabMap] /rollback/ откатов для', username, ':', found, '| Топ: №' + place);
        callback(found);
      })
      .catch(function(e){
        lmDebug('[LabMap] fetchTrapBacksFromRollbackPage:', e);
        callback(null);
      });
  }

  function doRefresh() {
    var btn=document.getElementById('lm-rbtn');
    btn.classList.add('loading'); btn.disabled=true;
    showToast('Обновление карты...', '', 10);
    setTimeout(function() {
      lmDebug('[Синхронизация] Автосинхронизация после обновления...');
      var tb = window._lmAllTimeTrapBacks;
      var tp = window._lmRollbackPlace;
      if (tb != null && tb > 0) {
        lmDebug('[Синхронизация] Отправляем откаты:', tb);
        sendRollbackToVPS(tb, tp || null);
      }
      sendAccHistoryToVPS();
      collectAndSendGuardians([]);
      setTimeout(function(){ checkAndSyncClub(true); }, 1000);
    }, 5000);
    syncCacheWithSession();
    flushStepsToServer();
    var d=mapData(), steps=d&&d.steps&&d.steps.length?d.steps:[];
    var domOwn=getCurrentRoomOwnership();
    var p=steps.length>0 ? pushSteps(steps,SESSION_ID) : Promise.resolve();
    p.then(function(){
      showToast('Обновление карты...', '', 30);
      return new Promise(function(resolve){
        ownershipCache = null;
        guardianDataCache = {user: null, club: null};
        fetchOwnership(function(hist){
          var allOwn=domOwn.concat(hist);
          var p1 = allOwn.length ? pushSteps(allOwn,SESSION_ID+'_own').catch(function(){}) : Promise.resolve();
          p1.then(function(){
            collectAndSendGuardians(allOwn);
          });
          p1.then(resolve).catch(resolve);
        });
      });
    }).then(function(){
      showToast('Обновление карты...', '', 55);
      return new Promise(function(resolve){ loadCloud(resolve, function(count){ showToast('Загрузка карты...', 'Карта: '+(count>=1000?Math.floor(count/1000)+'k ':count+' ')+'комнат', Math.min(88,55+Math.floor(count/500))); }); });
    }).then(function(){
      showToast('Обновление карты...', '', 75);
      var _ra = getAuth() || {};
      return Promise.all([
        window.lmFetch(VPS_URL+'/my/path', { headers: vpsAuthHeaders(false) })
          .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
          .then(function(d){
            if (d && Array.isArray(d.steps)) {
              window._lmMyPathData = {};
              d.steps.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
              window._lmHistSteps = d.steps; // 8.0.4: лінія історичного шляху на повній карті
              if (Array.isArray(d.emission_cells)) window._lmEmissionCells = new Set(d.emission_cells.map(function(c){ return c.x+'_'+c.y; }));
              lmDebug('[LabMap] My path:', d.steps.length, 'комнат из базы');
            }
          }).catch(function(){}),
        new Promise(function(resolveStats){
          fetchTrapBacksFromRollbackPage(function(rollbackCount) {
            if (rollbackCount !== null && rollbackCount > 0) {
              window._lmAllTimeTrapBacks = rollbackCount;
              lmDebug('[LabMap] /rollback/ откатов найдено:', rollbackCount);
              lmDebug('[Синхронизация] Вызываем sendRollbackToVPS...', typeof sendRollbackToVPS);
              sendRollbackToVPS(rollbackCount, window._lmRollbackPlace || null);
              resolveStats();
            } else {
              window.lmFetch(WORKER_URL+'/my-stats?username='+encodeURIComponent(_ra.username||'')+'&token='+(_ra.token||''))
                .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
                .then(function(d){
                  if (d.trap_backs !== undefined) {
                    var fromDb = d.trap_backs;
                    var prev = window._lmAllTimeTrapBacks || 0;
                    window._lmAllTimeTrapBacks = Math.max(fromDb, rollbackCount || 0, prev);
                    window._lmAllTimeTotal = d.profile && d.profile.total_steps || 0;
                    lmDebug('[LabMap] D1 откатов:', fromDb, '→', window._lmAllTimeTrapBacks);
                  }
                  resolveStats();
                }).catch(resolveStats);
            }
          });
        }).then(function(){
          var hpop = document.getElementById('lm-hpop');
          if (hpop && hpop.classList.contains('on')) {
            renderHistPopup(histCache[histCurPage||1] || {rows:[],totalPages:1}, histCurPage||1);
          }
          var whist = document.getElementById('lm-wi-hist');
          if (whist && whist.style.display !== 'none') {
            renderHistInline(histCache[histCurPage||1] || {rows:[],totalPages:1}, histCurPage||1);
          }
        })
      ]);
    }).then(function(){
      lmDebug('[Синхронизация] Отложенная отправка ACC-истории запущена');
      setTimeout(function() { sendAccHistoryToVPS(); }, 500);
      btn.classList.remove('loading'); btn.disabled=false;
      invalidateRoomsCache(); drawMini();
      if (fmOpen && showMyPath) drawFull();
      showToast('Карта обновлена!', '', 100);
      hideToast(2500);
      scheduleProfileSync();
      var currentMine = window.labyrinthData && window.labyrinthData.personalMine;
      if (currentMine && currentMine.has_mine) {
        var currentPosition = curPos();
        sendMineData(currentPosition.x, currentPosition.y, {
          level: Number(currentMine.level || 0),
          storage_pct: Number(currentMine.storage_progress || 0),
          acc_inside: Number(currentMine.pending_acc || currentMine.acc || 0),
          cards_current: Number(currentMine.pending_cards || currentMine.cards || 0),
          cards_max: Number(currentMine.max_cards || currentMine.cards_max || 0)
        });
      }
      setTimeout(function(){ checkAndSyncClub(true); }, 1000);
    }).catch(function(e){
      lmDebug('[LabMap] refresh err:',e);
      btn.classList.remove('loading'); btn.disabled=false;
    });
  }

  var guardianDataCache = {user:null, club:null};

  function collectAndSendGuardians(ownershipData) {
    var VPS_API  = LM_API_BASE;
    var auth = getAuth();
    if (!auth) return;
    var username = auth.username, token = auth.token;

    var guardians = [];

    if (ownershipData && ownershipData.length) {
      ownershipData.forEach(function(item) {
        if (!item || item.x == null || item.y == null) return;
        var ev = item.event_type || item.ev || item.guardian || '';
        var isClub = ev === 'guardian_club' || item.guardian_type === 'club';
        var isPersonal = ev === 'guardian_user' || item.guardian_type === 'personal' ||
                         ev.includes('own') || ev === 'protected';
        if (!isClub && !isPersonal) return;

        guardians.push({
          x: Number(item.x),
          y: Number(item.y),
          owner: item.owner || item.username || (isPersonal ? username : null),
          card_name: item.card_name || item.cardName || null,
          card_rank: item.card_rank || item.cardRank || null,
          tribute_count: item.tribute_count != null ? Number(item.tribute_count) : null,
          acc_per_pass: item.acc != null ? Number(item.acc) :
                        item.accPerPass != null ? Number(item.accPerPass) : null,
          guardian_type: isClub ? 'club' : 'personal',
          club_id: item.club_id || item.clubId || null,
          club_name: item.club_name || item.clubName || null,
          captured_by: item.captured_by || item.capturedBy || item.owner || item.username ||
                       (isPersonal ? username : null),
          captured_at: item.captured_at || item.capturedAt || null,
          confirmed: true,
          source: 'ownership'
        });
      });
    }

    var guardianRows = document.querySelectorAll(
      '.labyrinth-guardian-item, .guardian-row, [data-guardian], .ncard__guard-item'
    );
    guardianRows.forEach(function(el) {
      var xEl = el.querySelector('[data-x], .coord-x');
      var yEl = el.querySelector('[data-y], .coord-y');
      var ownerEl = el.querySelector('.guardian-owner, .owner-name, .card-inline__name');
      var cardEl  = el.querySelector('.card-name, .guardian-card');

      var x = xEl ? parseInt(xEl.getAttribute('data-x') || xEl.textContent) : null;
      var y = yEl ? parseInt(yEl.getAttribute('data-y') || yEl.textContent) : null;

      if (x == null || y == null) {
        var text = el.textContent || '';
        var m = text.match(/\(?\s*(-?\d+)\s*,\s*(-?\d+)\s*\)?/);
        if (m) { x = parseInt(m[1]); y = parseInt(m[2]); }
      }

      if (x == null || y == null) return;

      var rowText = (el.textContent || '').toLowerCase();
      var isClubGuardian = el.matches('[data-guardian="club"],.club-guardian') ||
                           rowText.indexOf('клуб') >= 0;
      var parsedOwner = ownerEl ? ownerEl.textContent.trim() : null;
      guardians.push({
        x: x,
        y: y,
        owner: parsedOwner || (!isClubGuardian ? username : null),
        card_name: cardEl ? cardEl.textContent.trim() : null,
        guardian_type: isClubGuardian ? 'club' : 'personal',
        captured_by: parsedOwner || (!isClubGuardian ? username : null),
        captured_at: null,
        confirmed: true,
        source: 'labyrinth_dom'
      });
    });

    var labData = window.labyrinthData;
    if (labData && labData.guardians && labData.guardians.length) {
      labData.guardians.forEach(function(g) {
        if (g.x == null || g.y == null) return;
        guardians.push({
          x:         g.x,
          y:         g.y,
          owner:     g.owner || g.username || username,
          card_name: g.card_name || g.cardName || null,
          card_rank: g.card_rank || g.cardRank || null,
          tribute_count: g.tribute_count != null ? Number(g.tribute_count) : null,
          acc_per_pass: g.acc_per_pass != null ? Number(g.acc_per_pass) :
                        g.acc != null ? Number(g.acc) : null,
          guardian_type: (g.guardian_type === 'club' || g.guardian === 'guardian_club') ? 'club' : 'personal',
          club_id: g.club_id || null,
          club_name: g.club_name || null,
          captured_by: g.captured_by || g.capturedBy || username,
          captured_at: g.captured_at || g.capturedAt || null,
          confirmed: true,
          source: 'labyrinth_data'
        });
      });
    }

    if (!guardians.length) {
      lmDebug('[LGuard] Стражи не найдены');
      return;
    }

    var guardianByRoom = {};
    guardians.forEach(function(g) {
      if (!Number.isInteger(Number(g.x)) || !Number.isInteger(Number(g.y))) return;
      var key = Number(g.x) + '_' + Number(g.y);
      var score = (g.confirmed ? 8 : 0) + (g.owner ? 4 : 0) +
                  (g.card_name ? 2 : 0) + (g.club_id || g.club_name ? 2 : 0) +
                  (g.card_rank ? 1 : 0);
      if (!guardianByRoom[key] || score > guardianByRoom[key]._score) {
        guardianByRoom[key] = Object.assign({}, g, {_score: score});
      }
    });
    guardians = Object.keys(guardianByRoom).map(function(key) {
      var g = guardianByRoom[key];
      delete g._score;
      return g;
    }).filter(function(g) {
      return g.confirmed && (g.owner || g.card_name || g.club_id || g.club_name);
    });

    window.lmFetch(VPS_API + '/guardians', {
      method: 'POST',
      headers: vpsAuthHeaders(true),
      body: JSON.stringify({ guardians: guardians }),
    })
    .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
    .then(function(res){
      lmDebug('[LGuard] Стражи отправлены:', res.saved, '/', guardians.length);
    })
    .catch(function(e){
      lmDebug('[LGuard] Ошибка отправки стражей:', e.message);
    });
  }

  var _clubSyncPromise = null;
  var _clubSyncLastAt = 0;

  function checkAndSyncClub(force) {
    var now = Date.now();

    if (_clubSyncPromise) return _clubSyncPromise;
    if (!force && now - _clubSyncLastAt < 15000) return Promise.resolve(false);

    _clubSyncLastAt = now;
    _clubSyncPromise = Promise.resolve()
      .then(collectAndSendClubRooms)
      .catch(function() { return false; })
      .then(function(result) {
        _clubSyncPromise = null;
        return result;
      }, function() {
        _clubSyncPromise = null;
        return false;
      });

    return _clubSyncPromise;
  }

  function collectAndSendClubRooms() {
    var auth = getAuth();
    if (!auth) return Promise.resolve(false);
    var username = auth.username, token = auth.token;

    function parseClubDate(raw) {
      var text = String(raw || '').replace(/\s+/g, ' ').trim();
      var m = text.match(/(\d{2})\.(\d{2})\.(\d{4})\s*(?:в|,)?\s*(\d{1,2}):(\d{2})/i);
      if (!m) return null;
      var d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]), Number(m[4]), Number(m[5]), 0);
      return Number.isNaN(d.getTime()) ? null : d.toISOString();
    }

    function parseMember(item) {
      var nameEl = item.querySelector('.club__member-name');
      if (!nameEl) return null;
      var name = nameEl.textContent.trim();
      if (!name) return null;

      var statusEl = item.querySelector('.club__member-status');
      var contributionEl = item.querySelector('.club__member-contribution');
      var imageEl = item.querySelector('.club__member-image img');
      var status = statusEl ? statusEl.textContent.trim() : '';
      var role = 'member';

      if (/^Гильдмастер$/i.test(status)) role = 'leader';
      else if (/Зам\.?\s*Гильдмастера/i.test(status)) role = 'deputy';

      return {
        username: name,
        role: role,
        status: status || null,
        contribution: contributionEl ? Number((contributionEl.textContent || '').replace(/\D+/g, '')) || 0 : 0,
        is_online: item.classList.contains('club__member--online'),
        avatar: imageEl ? (imageEl.getAttribute('src') || null) : null
      };
    }

    function parseClubDocument(doc, clubUrl) {
      var root = doc.querySelector('.nclub.nclub-enter[data-id], .nclub-enter[data-id]');
      var rooms = [];
      var members = [];

      var dataId = root && root.getAttribute('data-id');
      var urlId = String(clubUrl || '').match(/\/clubs\/(\d+)(?:\/|$)/);
      var clubId = dataId && /^\d+$/.test(dataId) ? Number(dataId) :
                   urlId ? Number(urlId[1]) : null;

      var nameEl = doc.querySelector('.nclub-enter__main-name > div:first-child');
      var membersTextEl = doc.querySelector('.nclub-enter__main-members');
      var levelInfoEl = doc.querySelector('.nclub-enter__lvl-info > div:first-child');
      var activityEls = doc.querySelectorAll('.nclub-enter__activity-item');

      doc.querySelectorAll('.nclub-enter__members-list .club__member').forEach(function(item) {
        var member = parseMember(item);
        if (member) members.push(member);
      });

      var leaderMember = members.find(function(m) { return m.role === 'leader'; });
      var memberCount = members.length;
      if (membersTextEl) {
        var countMatch = membersTextEl.textContent.match(/(\d+)\s+участник/i);
        if (countMatch) memberCount = Number(countMatch[1]);
      }

      var enlightenment = null;
      if (membersTextEl) {
        var lightMatch = membersTextEl.textContent.match(/просветлен\w*\s+(\d+)/i);
        if (lightMatch) enlightenment = Number(lightMatch[1]);
      }
      if (enlightenment == null && activityEls.length > 1) {
        var n = Number((activityEls[1].querySelector('div') || {}).textContent || '');
        if (Number.isFinite(n)) enlightenment = n;
      }

      var level = null;
      if (levelInfoEl) {
        var levelMatch = levelInfoEl.textContent.match(/(\d+)\s+уровень/i);
        if (levelMatch) level = Number(levelMatch[1]);
      }

      var info = {
        id: clubId,
        name: nameEl ? nameEl.textContent.trim() : null,
        leader: leaderMember ? leaderMember.username : null,
        member_count: memberCount || 0,
        enlightenment: enlightenment,
        level: level
      };

      doc.querySelectorAll('.club-labyrinth__item').forEach(function(item) {
        var roomEl = item.querySelector('.club-labyrinth__room');
        if (!roomEl) return;

        var coords = parseRoomText(roomEl.textContent || '');
        if (!coords || !Number.isInteger(Number(coords.x)) || !Number.isInteger(Number(coords.y))) return;

        var ownerEl = item.querySelector('.club-labyrinth__text b');
        var dateEl = item.querySelector('.club-labyrinth__date');
        var badgeEl = item.querySelector('.club-labyrinth__badge');
        var avatarEl = item.querySelector('.club-labyrinth__avatar');

        var capturedBy = ownerEl ? ownerEl.textContent.trim() : null;

        rooms.push({
          x: Number(coords.x),
          y: Number(coords.y),
          club_id: info.id,
          club_name: info.name,
          club_owner: capturedBy,
          guardian_type: 'club',
          card_name: null,
          card_rank: null,
          captured_by: capturedBy,
          captured_at: dateEl ? parseClubDate(dateEl.textContent) : null,
          is_new: !!badgeEl,
          avatar: avatarEl ? (avatarEl.getAttribute('src') || null) : null,
          confirmed: true,
          source: 'club_page'
        });
      });

      return {club: info, rooms: rooms, members: members};
    }

    function send(payload) {
      var hasClub = payload && payload.club && (payload.club.id || payload.club.name);
      var hasRooms = payload && Array.isArray(payload.rooms) && payload.rooms.length;
      var hasMembers = payload && Array.isArray(payload.members) && payload.members.length;
      if (!hasClub && !hasRooms && !hasMembers) return Promise.resolve(false);

      return window.lmFetch(VPS_URL + '/club-rooms', {
        method: 'POST',
        headers: vpsAuthHeaders(true),
        body: JSON.stringify(payload)
      }).then(function(r) {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.json();
      }).then(function(res) {
        return true;
      }).catch(function(e) {
        lmDebug('[Карта] club sync send:', e);
        return false;
      });
    }

    function fetchText(url) {
      return fetch(url, {credentials: 'same-origin'}).then(function(r) {
        if (!r.ok) throw new Error('HTTP ' + r.status);
        return r.text();
      });
    }

    var parser = new DOMParser();
    var currentPath = window.location.pathname || '';

    if (/^\/clubs\/\d+\/?$/.test(currentPath) && document.querySelector('.nclub-enter[data-id]')) {
      return send(parseClubDocument(document, currentPath));
    }

    return fetchText('/user/' + encodeURIComponent(username) + '/')
      .then(function(html) {
        var profileDoc = parser.parseFromString(html, 'text/html');
        var clubLink = profileDoc.querySelector('.usn__club-item-top a[href*="/clubs/"]');

        if (!clubLink) {
          var links = profileDoc.querySelectorAll('a[href*="/clubs/"]');
          for (var i = 0; i < links.length; i++) {
            var href = links[i].getAttribute('href') || '';
            if (/\/clubs\/\d+\/?/.test(href)) {
              clubLink = links[i];
              break;
            }
          }
        }

        if (!clubLink) throw new Error('club_link_not_found');

        var href = clubLink.getAttribute('href');
        return fetchText(href).then(function(clubHtml) {
          return send(parseClubDocument(parser.parseFromString(clubHtml, 'text/html'), href));
        });
      })
      .catch(function(e) {
        lmDebug('[Карта] club sync:', e);
        return false;
      });
  }

  function loadGuardianPopup(type) {
    var listEl=document.getElementById(type==='user'?'lm-glist':'lm-clist');
    var countEl=document.getElementById(type==='user'?'lm-gcount':'lm-ccount');
    if(!listEl) return;
    if(guardianDataCache[type]){ renderGuardianList(type,guardianDataCache[type],listEl,countEl); return; }
    listEl.className='lm-spop-list lm-spop-loading'; listEl.textContent='Загрузка...';
    var username=window.visitor_name||'';
    if(!username){ listEl.textContent='Не удалось определить пользователя.'; return; }
    var parser=new DOMParser();
    fetch('/user/'+encodeURIComponent(username)+'/')
    .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
    .then(function(html){
      var doc=parser.parseFromString(html,'text/html');
      if(type==='user'){
        var items=parseUserGuardianItems(doc);
        guardianDataCache.user=items; renderGuardianList(type,items,listEl,countEl);
      } else {
        var clubLink=findClubLink(doc);
        if(!clubLink){ listEl.textContent='Клуб не найден.'; return; }
        return fetch(clubLink.getAttribute('href')).then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
        .then(function(ch){
          var items=parseClubGuardianItems(parser.parseFromString(ch,'text/html'));
          guardianDataCache.club=items; renderGuardianList(type,items,listEl,countEl);
        });
      }
    }).catch(function(){ listEl.textContent='Ошибка загрузки.'; });
  }

  function renderGuardianList(type, items, listEl, countEl) {
    if(countEl) countEl.textContent='('+items.length+')';
    if(!items.length){ listEl.className='lm-spop-list lm-spop-loading'; listEl.textContent='Нет данных.'; return; }
    listEl.className='lm-spop-list'; listEl.innerHTML='';
    items.forEach(function(it){
      var div=document.createElement('div'); div.className='lm-si'; div.title='Перейти на карте';
      div.innerHTML=(it.img?'<img class="lm-si-img" src="'+it.img+'" alt="">':'')+
        '<div class="lm-si-info"><div class="lm-si-name">'+it.name+'</div><div class="lm-si-room">'+it.room+'</div><div class="lm-si-date">'+it.date+'</div></div>'+
        '<div class="lm-si-nav">→</div>';
      div.addEventListener('click', function(){
        closeAllPopups();
        highlightRoom = {x: it.x, y: it.y, until: Date.now() + 3000};
        openFull(true);
        setTimeout(function() { centerOn(it.x, it.y, FULL_FOCUS_SCALE); }, 50);
      });
      listEl.appendChild(div);
    });
  }

  function loadWbGuardian(type, container) {
    if (!container) return;
    if (guardianDataCache[type]) { renderWbGuardianList(type, guardianDataCache[type], container); return; }
    container.innerHTML = '<div class="lm-wi-loading">Загрузка...</div>';
    var username = window.visitor_name || '';
    if (!username) { container.innerHTML = '<div class="lm-wi-empty">Пользователь не определён</div>'; return; }
    var parser = new DOMParser();
    fetch('/user/'+encodeURIComponent(username)+'/')
      .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
      .then(function(html){
        var doc = parser.parseFromString(html, 'text/html');
        if (type === 'user') {
          var items = parseUserGuardianItems(doc);
          guardianDataCache.user = items; renderWbGuardianList('user', items, container);
        } else {
          var clubLink = findClubLink(doc);
          if (!clubLink) { container.innerHTML = '<div class="lm-wi-empty">Клуб не найден</div>'; return; }
          return fetch(clubLink.getAttribute('href')).then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
            .then(function(ch){
              var items = parseClubGuardianItems(parser.parseFromString(ch, 'text/html'));
              guardianDataCache.club = items; renderWbGuardianList('club', items, container);
            });
        }
      }).catch(function(){ container.innerHTML = '<div class="lm-wi-empty">Ошибка загрузки</div>'; });
  }

  function renderWbGuardianList(type, items, container) {
    if (!items.length) { container.innerHTML = '<div class="lm-wi-empty">Нет данных</div>'; return; }
    var html = items.map(function(it){
      return '<div class="lm-wi-item" data-x="'+it.x+'" data-y="'+it.y+'">'+
        (it.img ? '<img class="lm-wi-img" src="'+it.img+'" alt="">' : '')+
        '<div class="lm-wi-info"><div class="lm-wi-name">'+it.name+'</div><div class="lm-wi-room">'+it.room+'</div><div class="lm-wi-date">'+it.date+'</div></div>'+
        '<div class="lm-wi-nav">→</div></div>';
    }).join('');
    container.innerHTML = html;
    container.querySelectorAll('.lm-wi-item').forEach(function(el){
      el.addEventListener('click', function(){
        var x = parseInt(this.getAttribute('data-x'), 10);
        var y = parseInt(this.getAttribute('data-y'), 10);
        highlightRoom = {x: x, y: y, until: Date.now() + 3000};
        openFull(true); setTimeout(function(){ centerOn(x, y, FULL_FOCUS_SCALE); }, 50);
      });
    });
  }

  function renderHistInline(result, page) {
    var container = document.querySelector('#lm-wi-hist .lm-hist-body');
    if (!container) return;
    var totalPages = histTotal || result.totalPages || 1;
    var rows = result.rows;
    var stats = buildStepsStats();
    var statsHtml = '';
    if (stats) {
      var acc = buildAccTotals();
      statsHtml =
        '<div class="lm-sc-grid" style="margin:10px;">'+
          '<div class="lm-sc-card"><div class="lm-sc-label">Всего комнат</div><div class="lm-sc-val">'+stats.allTimeTotal+'</div></div>'+
          '<div class="lm-sc-card lm-sc-trap">' +
            '<div class="lm-sc-label">Откаты</div>' +
            '<div class="lm-sc-val lm-sc-bad">' + (stats.historyLoaded ? (stats.trapCount > 0 ? '−'+stats.trapCount : '0') : '?') + '</div>' +
            '<div class="lm-sc-trap-row">' +
              (stats.historyLoaded && stats.lostRooms > 0
                ? '<span class="lm-sc-rooms">−'+stats.lostRooms+' комнат</span>'
                : '<span class="lm-sc-hint">'+(stats.historyLoaded?'нет откатов':'нажми Обновить')+'</span>') +
              (window._lmRollbackPlace ? '<span class="lm-sc-badge">Топ #'+window._lmRollbackPlace+'</span>' : '') +
            '</div>' +
          '</div>'+
          '<div class="lm-sc-card"><div class="lm-sc-label">Заработано</div><div class="lm-sc-val lm-sc-good">+'+acc.earned+'</div></div>'+
          '<div class="lm-sc-card"><div class="lm-sc-label">Потеряно</div><div class="lm-sc-val lm-sc-bad">−'+acc.lost+'</div></div>'+
        '</div>';
    }
    var rowsHtml = rows.length
      ? rows.map(function(row){
          var amt = parseFloat(row.amount.replace(/[^0-9.\-+]/g,''));
          var isPlus = (!isNaN(amt)&&amt>0)||row.amount.indexOf('+')!==-1;
          var isMinus= (!isNaN(amt)&&amt<0)||row.amount.indexOf('-')!==-1;
          var amtCls = isPlus?'lm-ha-plus':isMinus?'lm-ha-minus':'';
          var hi = histIcon(row.desc);
          return '<div class="lm-hr">'+
            '<div class="lm-hr-ico" style="background:'+hi.color+'18;border-color:'+hi.color+'44"><span style="color:'+hi.color+'">'+hi.icon+'</span></div>'+
            '<div class="lm-hr-main"><div class="lm-hr-desc">'+row.desc+'</div><div class="lm-hr-meta">'+row.date+'</div></div>'+
            '<div class="lm-hr-amt '+amtCls+'">'+row.amount+(row.amount.indexOf('ACC')===-1?' ACC':'')+'</div>'+
          '</div>';
        }).join('')
      : '<div class="lm-hist-empty">Записей не найдено (стр. '+page+')</div>';
    var pagHtml = '';
    if (totalPages > 1) {
      var start = Math.max(1,page-2), end = Math.min(totalPages,page+2), nums='';
      if(start>1) nums+='<button class="lm-hp-btn" data-p="1">1</button>'+(start>2?'<span class="lm-hp-dots">…</span>':'');
      for(var pi=start;pi<=end;pi++) nums+='<button class="lm-hp-btn'+(pi===page?' lm-hp-cur':'')+'" data-p="'+pi+'">'+pi+'</button>';
      if(end<totalPages) nums+=(end<totalPages-1?'<span class="lm-hp-dots">…</span>':'')+'<button class="lm-hp-btn" data-p="'+totalPages+'">'+totalPages+'</button>';
      pagHtml='<div class="lm-hist-pag">'+
        '<button class="lm-hp-nav" id="lm-wi-hprev" '+(page<=1?'disabled':'')+'>‹</button>'+nums+
        '<button class="lm-hp-nav" id="lm-wi-hnext" '+(page>=totalPages?'disabled':'')+'>›</button>'+
      '</div>';
    }
    container.innerHTML = statsHtml +
      '<div class="lm-hist-rows-head" style="padding:4px 10px;">📜 Страница '+page+'</div>'+
      '<div class="lm-hist-rows" style="padding:0 10px 8px;">'+rowsHtml+'</div>'+pagHtml;
    container.querySelectorAll('.lm-hp-btn[data-p]').forEach(function(btn){
      btn.addEventListener('click', function(e){
        e.stopPropagation();
        var p = parseInt(this.dataset.p,10);
        histCurPage = p;
        container.innerHTML = '<div class="lm-wi-loading">Загрузка стр. '+p+'...</div>';
        loadHistPage(p, function(r){ renderHistInline(r, p); });
      });
    });
    var prev = container.querySelector('#lm-wi-hprev');
    var next = container.querySelector('#lm-wi-hnext');
    if(prev) prev.addEventListener('click', function(e){
      e.stopPropagation();
      if(page>1){ histCurPage=page-1; container.innerHTML='<div class="lm-wi-loading">Загрузка...</div>'; loadHistPage(page-1,function(r){renderHistInline(r,page-1);}); }
    });
    if(next) next.addEventListener('click', function(e){
      e.stopPropagation();
      if(page<totalPages){ histCurPage=page+1; container.innerHTML='<div class="lm-wi-loading">Загрузка...</div>'; loadHistPage(page+1,function(r){renderHistInline(r,page+1);}); }
    });
  }

  function buildUI() {
    if (document.getElementById('lm-wrap')) return;
    injectStyles(); buildDOM(); bindEvents();
    lmInfo('Карта готова');
  }

  var LS_AUTO_SYNC_KEY = 'lm_last_auto_sync' + _lmUserSuffix;
  var AUTO_SYNC_INTERVAL = 24 * 60 * 60 * 1000;

  function autoSyncIfNeeded() {
    var lastSync = parseInt(window.lmLS.get(LS_AUTO_SYNC_KEY) || '0', 10);
    var now = Date.now();
    if (now - lastSync < AUTO_SYNC_INTERVAL) {
      var cacheLoaded = loadFromCache();
      var LS_CLOUD_UPDATED = 'lm_cloud_last_updated';
      var lastCloud = parseInt(window.lmLS.get(LS_CLOUD_UPDATED) || '0', 10);

      // Завжди перевіряємо версію на сервері — loadCloud сам вирішить чи потрібне оновлення IDB
      loadCloud(function() {
        window.lmLS.set(LS_CLOUD_UPDATED, String(Date.now()));
        drawMini();
        if (fmOpen) drawFull();
      });
      return;
    }
    setTimeout(function() {
      showToast('Обновление карты...', '', 5);
      var d = mapData(), steps = d && d.steps && d.steps.length ? d.steps : [];
      var domOwn = getCurrentRoomOwnership();
      var p = steps.length > 0 ? pushSteps(steps, SESSION_ID) : Promise.resolve();
      p.then(function() {
        showToast('Обновление карты...', '', 25);
        return new Promise(function(resolve) {
          ownershipCache = null; guardianDataCache = {user: null, club: null};
          fetchOwnership(function(hist) {
            var allOwn = domOwn.concat(hist);
            if (allOwn.length) pushSteps(allOwn, SESSION_ID+'_own').then(resolve).catch(resolve);
            else resolve();
          });
        });
      }).then(function() {
        showToast('Обновление карты...', '', 50);
        return new Promise(function(resolve) {
          loadCloud(resolve, function(count) {
            var pct = Math.min(88, 50 + Math.floor(count / 500));
            showToast('Обновление карты...', '', pct);
          });
        });
      }).then(function() {
        histCache = {}; histTotal = null; histCurPage = 1;
        window.lmLS.set(LS_HIST_DATE_KEY, new Date().toISOString());
        function autoLoadPage(pageNum) {
          return new Promise(function(resolve) {
            setTimeout(function() {
              loadHistPage(pageNum, function(result) {
                var total = histTotal || result.totalPages || 1;
                showToast('Авто-синхронизация...', 'История: стр. '+pageNum+' из '+total, Math.round(55 + (pageNum / Math.max(total,1)) * 40));
                if (pageNum < total && pageNum < 20) autoLoadPage(pageNum + 1).then(resolve);
                else resolve();
              });
            }, pageNum === 1 ? 0 : 700);
          });
        }
        return autoLoadPage(1);
      }).then(function() {
        window.lmLS.set(LS_AUTO_SYNC_KEY, String(Date.now()));
        showToast('Авто-синхронизация завершена!', 'Следующая через 24 часа', 100);
        hideToast(3000); drawMini();
      }).catch(function(e) { lmDebug('[LabMap] Auto-sync error:', e); hideToast(1000); });
    }, 3000);
  }

  function init() {
    lmInfo('Карта включена');
    syncCacheWithSession();
    var _lastFlushCount = parseInt(window.lmLS.get(LS_FLUSH_COUNT) || '0');
    var _currentSteps   = mapData() && mapData().steps ? mapData().steps.length : 0;
    var _cachedSteps    = getCachedSteps();
    var _sentIdx        = getSentCount();
    var _hasPending     = _cachedSteps.length > _sentIdx;
    if (_sentIdx >= _cachedSteps.length && _currentSteps <= _lastFlushCount) {
      _hasPending = false;
    }
    if (_currentSteps > _lastFlushCount || _hasPending) {
      lmDebug('[LabMap] Отправка при запуске: шагов=' + _currentSteps + ' lastFlush=' + _lastFlushCount + ' pending=' + _hasPending);
      flushStepsToServer();
    } else {
      lmDebug('[LabMap] Новых данных нет — отправка пропущена');
    }
    window.addEventListener('beforeunload', function() {
      var d = mapData();
      if (d && d.steps && d.steps.length) cacheSteps(d.steps);
    });
    buildUI(); drawMini(); injectTopBtn();
    autoSyncIfNeeded();
    patchSiteMapDelayed();
    setTimeout(function() {
      fetchTrapBacksFromRollbackPage(function(count) {
        if (count !== null) {
          window._lmAllTimeTrapBacks = count;
          lmDebug('[LabMap] /rollback/ откатов при старте:', count);
        }
      });
    }, 1500);

    var lastN   = mapData()&&mapData().steps ? mapData().steps.length : 0;
    var c0      = curPos();
    var lastPos = c0.x+'_'+c0.y;

    var _stepProcessing = false;
    var _pendingStep = null;

    (function() {
      var mapEl = document.getElementById('labyrinthMap');
      if (!mapEl) { lmDebug('[LabMap] labyrinthMap not found'); return; }

      mapEl.addEventListener('click', function(e) {
        if (_lmPatching || _stepProcessing) return;
        var cell = e.target.closest
          ? e.target.closest('.labyrinth-cell--available')
          : (e.target.classList.contains('labyrinth-cell--available') ? e.target : null);
        if (!cell) return;
        var cur = curPos();
        var mapElLocal = document.getElementById('labyrinthMap');
        var cells = mapElLocal.querySelectorAll('.labyrinth-cell');
        var center = detectGridCenter(cells);
        if (!center) return;
        var cellsArr = Array.prototype.slice.call(cells);
        var idx = cellsArr.indexOf(cell);
        if (idx < 0) return;
        var gx, gy;
        if (center.useIndex) { gx = idx % 25; gy = Math.floor(idx / 25); }
        else {
          gx = parseInt(cell.getAttribute('data-x') || '', 10);
          gy = parseInt(cell.getAttribute('data-y') || '', 10);
          if (isNaN(gx) || isNaN(gy)) { gx = idx % 25; gy = Math.floor(idx / 25); }
        }
        var targetX = cur.x + (gx - center.gx);
        var targetY = cur.y + (gy - center.gy);
        lmDebug('[LabMap] cell click target:', targetX, targetY);
        _pendingStep = {cx: targetX, cy: targetY};
        _stepProcessing = true;
        waitForStep(targetX, targetY);
      }, true);

      var obs = new MutationObserver(function(mutations) {
        if (_lmPatching) return;
        var hasCurrentChange = false;
        for (var i = 0; i < mutations.length; i++) {
          var m = mutations[i];
          if (m.type === 'attributes' && m.attributeName === 'class') {
            if (m.target.classList &&
               (m.target.classList.contains('labyrinth-cell--current') ||
               (m.oldValue && m.oldValue.indexOf('labyrinth-cell--current') >= 0))) {
              hasCurrentChange = true; break;
            }
          }
          if (m.type === 'childList' && m.addedNodes.length > 10) { hasCurrentChange = true; break; }
        }
        if (!hasCurrentChange) return;
        var d = window.labyrinthData;
        if (!d || !d.mapData || !d.mapData.current) return;
        var cx = d.mapData.current.x, cy = d.mapData.current.y;
        var pos = cx + '_' + cy;
        if (_stepProcessing && _pendingStep && _pendingStep.cx === cx && _pendingStep.cy === cy) {
          lastPos = pos; return;
        }
        if (pos === lastPos) return;
        if (_stepProcessing) return;
        lastPos = pos; _stepProcessing = true;
        lmDebug('[LabMap] step (observer):', cx, cy);
        waitForStep(cx, cy);
      });

      obs.observe(mapEl, {
        subtree: true, attributes: true,
        attributeFilter: ['class'], attributeOldValue: true, childList: true
      });
    })();

    setInterval(function() {
      if (_stepProcessing) return;
      var d = mapData(), n = d && d.steps ? d.steps.length : 0;
      var c = curPos(), pos = c.x + '_' + c.y;
      if (n !== lastN || pos !== lastPos) {
        lastN = n; lastPos = pos; _stepProcessing = true;
        lmDebug('[LabMap] fallback detector:', pos);
        waitForStep(c.x, c.y);
      }
    }, 5000);

    function inferCurrentEventFromDom(fallbackEvent) {
      var ev = fallbackEvent || 'unknown';
      if (ev && ev !== 'unknown' && ev !== 'empty') return ev;

      var text = '';
      var selectors = [
        '.labyrinth-last-event',
        '.labyrinth-event',
        '.labyrinth-info',
        '.labyrinth-sidebar',
        '#labyrinth',
        'main'
      ];
      for (var i = 0; i < selectors.length; i++) {
        var el = document.querySelector(selectors[i]);
        if (el && el.textContent) text += ' ' + el.textContent;
      }
      text = text.toLowerCase();

      var rules = [
        [/печать коллекционера|проверка одинаковых карт|коллекц/i, 'collection'],
        [/чужой подарок|подарок/i, 'room_gift'],
        [/замкненн(?:ый|ого) сундук|закрыт(?:ый|ого) сундук/i, 'locked_chest'],
        [/мини.?босс/i, 'mini_boss'],
        [/хард.?босс|сложн\w* босс/i, 'hard_boss'],
        [/викторин/i, 'quiz'],
        [/пазл|испытание дао/i, 'puzzle'],
        [/откат|ловушка отката/i, 'trap_back'],
        [/реликв/i, 'relic_room'],
        [/эхо|відгомін/i, 'echo_room'],
        [/пусто|обычная комната|центральная комната/i, 'empty']
      ];
      for (var r = 0; r < rules.length; r++) {
        if (rules[r][0].test(text)) return rules[r][1];
      }
      return ev || 'unknown';
    }

    var _syntheticStepNonce = 0;
    var _lastSyntheticKey = '';

    function sendSyntheticCurrentStep(cx, cy) {
      var key = cx + '_' + cy;
      if (_lastSyntheticKey === key) return;
      _lastSyntheticKey = key;

      setTimeout(function() {
        var ev = inferCurrentEventFromDom('unknown');
        var bankEl = document.getElementById('labyrinthBank');
        var accAfter = bankEl
          ? parseInt(String(bankEl.textContent || '').replace(/\s+/g, ''), 10)
          : null;

        var synthetic = {
          x: Number(cx),
          y: Number(cy),
          event: ev || 'unknown',
          ev: ev || 'unknown',
          acc_after: Number.isFinite(accAfter) ? accAfter : null,
          session_id: PUSH_SESSION_ID,
          step_index: 'synthetic_' + Date.now() + '_' + (++_syntheticStepNonce),
          ts: new Date().toISOString(),
          _lmSynthetic: true
        };

        var det = scrapeRoomDetails(synthetic.event);
        var emDat = getEmissionStepDat();
        synthetic._dat = det ? Object.assign({}, emDat, det) : (emDat || undefined);

        schedulePush([synthetic], PUSH_SESSION_ID);
        syncProfileToVPS(true);
      }, 850);
    }

    function waitForStep(cx, cy) {
      var targetKey = cx + '_' + cy;
      var maxTries = 20, tryCount = 0;
      function tryUpdate() {
        tryCount++;
        var curD = mapData();
        var steps = curD && curD.steps ? curD.steps : [];
        var lastStep = steps.length > 0 ? steps[steps.length - 1] : null;
        var stepReady = lastStep && (lastStep.x + '_' + lastStep.y === targetKey);
        var mapEl = document.getElementById('labyrinthMap');
        var curCell = mapEl && mapEl.querySelector('.labyrinth-cell--current');
        if (tryCount >= maxTries && !(stepReady && curCell)) {
          _pendingStep = null;
          _stepProcessing = false;

          var current = curD && curD.current ? curD.current : null;
          if (current &&
              Number(current.x) === Number(cx) &&
              Number(current.y) === Number(cy)) {
            sendSyntheticCurrentStep(cx, cy);
          }
          return;
        }
        if (stepReady && curCell) {
          lastN = steps.length; lastPos = targetKey; _pendingStep = null;
          invalidateRoomsCache(); updateMapInfo(); patchSiteMap();
          if (fmOpen) { drawFull(); centerOn(cx, cy); }
          if (steps.length > 0) {
            var lastS = steps[steps.length-1];
            setTimeout(function() {
              lastS.event = inferCurrentEventFromDom(lastS.event);
              lastS.ev = lastS.event;
              lastS.session_id = PUSH_SESSION_ID;
              var det = scrapeRoomDetails(lastS.event);
              var emDat = getEmissionStepDat();
              lastS._dat = det ? Object.assign({}, emDat, det) : (emDat || undefined);
              schedulePush(steps, PUSH_SESSION_ID);
            }, 650);
          }

          if (window._lmMyPathData) {
            window._lmMyPathData[cx + '_' + cy] = true;
          }

          _stepProcessing = false;
          lmDebug('[Карта] Комната обработана:', cx, cy);
        } else {
          setTimeout(tryUpdate, 150);
        }
      }
      setTimeout(tryUpdate, 100);
    }
  }

  function wait() {
    var MAX_TRIES=100, tries=0;
    function check() {
      tries++;
      var d2=mapData();
      if (d2&&d2.steps&&d2.steps.length>0) { lmDebug('[LabMap] Найдено! steps:',d2.steps.length); init(); }
      else if (tries<MAX_TRIES) setTimeout(check,300);
      else lmDebug('[LabMap] Data not found after',MAX_TRIES,'tries');
    }
    check();
  }

  if (IS_LABYRINTH) { wait(); }

  (function mineAutoCollect() {
    var LS_MINE_ENABLED   = 'lm_mine_auto_enabled'   + _lmUserSuffix;
    var LS_MINE_INTERVAL  = 'lm_mine_check_interval' + _lmUserSuffix;
    var LS_MINE_LAST      = 'lm_mine_last_check'     + _lmUserSuffix;
    var LS_MINE_THRESHOLD = 'lm_mine_threshold'      + _lmUserSuffix;
    var LS_MINE_HAS       = 'lm_mine_has'            + _lmUserSuffix; // 8.0.3: кеш "чи є шахта" → без неї бекоф до 1 год
    var LS_MINE_FILL_AT   = 'lm_mine_fill_at'        + _lmUserSuffix; // 8.0.3: абсолютний ts (мс) досягнення порогу → точний збір поза сторінкою лабіринту
    var LS_MINE_RETRY_AT  = 'lm_mine_retry_at'       + _lmUserSuffix; // 8.0.3: не повторювати збір до цього ts (бекоф при невдачі — щоб НЕ спамити)
    var LS_MINE_FAILS     = 'lm_mine_fails'          + _lmUserSuffix; // 8.0.3: к-сть невдалих зборів підряд → прогресивний бекоф
    var NO_MINE_BACKOFF_MS = 60 * 60 * 1000; // немає шахти → перевіряти не частіше ніж раз на годину
    var DEFAULT_INTERVAL  = 5;
    var DEFAULT_THRESHOLD = 100;

    function isEnabled()    { return window.lmLS.get(LS_MINE_ENABLED) !== 'false'; }
    function getInterval()  { return parseInt(window.lmLS.get(LS_MINE_INTERVAL)  || DEFAULT_INTERVAL,  10); }
    function getThreshold() { return parseInt(window.lmLS.get(LS_MINE_THRESHOLD) || DEFAULT_THRESHOLD, 10); }
    function getUserHash()  { return window.dle_login_hash || ''; }

    function checkMine(callback, forceFresh) {
      var hash = getUserHash();
      if (!hash) { callback(null); return; }
      // 8.0.3: forceFresh=true → завжди тягнемо свіжі дані (коли за часовою оцінкою шахта мала б заповнитись,
      // але снапшот сторінки лабіринту міг застаріти). Інакше на сторінці лабіринту беремо живий снапшот без мережі.
      if (!forceFresh && IS_LABYRINTH && window.labyrinthData && window.labyrinthData.personalMine) {
        callback(window.labyrinthData.personalMine); return;
      }
      fetch('/labyrinth/?_=' + Date.now(), { credentials: 'same-origin', cache: 'no-store' })
        .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.text(); })
        .then(function(html) {
          var marker = 'personalMine:';
          var idx = html.indexOf(marker);
          if (idx === -1) { callback(null); return; }
          var afterColon = html.substring(idx + marker.length).replace(/^\s+/, '');
          if (afterColon.indexOf('null') === 0) { callback(null); return; }
          if (afterColon[0] !== '{') { callback(null); return; }
          var depth = 0, pos = 0, len = afterColon.length;
          while (pos < len) {
            var ch = afterColon[pos];
            if (ch === '{') { depth++; }
            else if (ch === '}') { depth--; if (depth === 0) { pos++; break; } }
            else if (ch === '"') {
              pos++;
              while (pos < len) {
                if (afterColon[pos] === '\\' && pos + 1 < len) { pos += 2; continue; }
                if (afterColon[pos] === '"') break;
                pos++;
              }
            }
            pos++;
          }
          try {
            var mine = JSON.parse(afterColon.substring(0, pos));
            if (mine && mine.has_mine) callback(mine); else callback(null);
          } catch(e) { callback(null); }
        })
        .catch(function() { callback(null); });
    }

    function collectMine(onDone) {
      var hash = getUserHash();
      if (!hash) { onDone(false, 'No hash'); return; }
      fetch('/index.php?controller=ajax&mod=animesss_game', {
        method: 'POST', credentials: 'same-origin',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 'X-Requested-With': 'XMLHttpRequest' },
        body: 'action=collect_personal_mine&user_hash=' + encodeURIComponent(hash)
      })
      .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(function(d) {
        if (d.success) {
          onDone(true, d);
          try {
            if (window.DLEPush && DLEPush.success) {
              DLEPush.success('⛏ Шахта: собрано ' + (d.acc || 0) + ' АСС' + (d.cards_awarded && d.cards_awarded.length ? ' + ' + d.cards_awarded.length + ' карт' : ''));
            }
          } catch(e) {}
        } else { onDone(false, d); }
      })
      .catch(function(e) { onDone(false, e.message); });
    }

    // 8.0.3: кеш стану шахти для інших вкладок — чи є шахта (LS_MINE_HAS) і КОЛИ досягне порогу (LS_MINE_FILL_AT, без +30с буфера).
    function setMineFillCache(m) {
      if (!m || !m.has_mine) { window.lmLS.set(LS_MINE_HAS, '0'); return; }
      window.lmLS.set(LS_MINE_HAS, '1');
      var thr = getThreshold();
      var pct = Number(m.storage_progress || 0);
      if (m.is_storage_full || pct >= thr) { window.lmLS.set(LS_MINE_FILL_AT, String(Date.now())); return; }
      var sl = Number(m.storage_seconds_left || 0);
      var toThr = sl * ((thr - pct) / Math.max(1, 100 - pct));
      window.lmLS.set(LS_MINE_FILL_AT, String(Date.now() + Math.max(0, toThr) * 1000));
    }

    function runCheck() {
      if (!isEnabled() || !getUserHash()) return;
      // 8.0.3: бекоф після невдалого збору — НЕ спамимо, чекаємо до LS_MINE_RETRY_AT (10→20→40→60 хв).
      var _retryAt = parseInt(window.lmLS.get(LS_MINE_RETRY_AT) || '0', 10);
      if (_retryAt && Date.now() < _retryAt) return;

      // За часовою оцінкою шахта мала б заповнитись? (LS_MINE_FILL_AT у минулому). Якщо так — тягнемо СВІЖІ дані
      // (forceFresh), бо снапшот сторінки лабіринту міг застаріти. Поза сторінкою лабіринту завжди свіжий fetch.
      var _fillAt = parseInt(window.lmLS.get(LS_MINE_FILL_AT) || '0', 10);
      var _believeReady = _fillAt > 0 && Date.now() >= _fillAt;
      var _haveSnap = IS_LABYRINTH && window.labyrinthData && window.labyrinthData.personalMine;
      var _willFetch = _believeReady || !_haveSnap;

      // 8.0.3 (ФІКС флуду 520): анти-стампед дедуп ЛИШЕ коли реально йде мережевий fetch('/labyrinth/').
      // Перша вкладка робить запит, решта в межах 10с виходять і перепланують себе. Снапшот на сторінці лабіринту — без мережі.
      // "🔍 Проверить сейчас" / collectMine цей дедуп НЕ зачіпає.
      if (_willFetch) {
        var _lastChk = parseInt(window.lmLS.get(LS_MINE_LAST) || '0', 10);
        if (_lastChk && (Date.now() - _lastChk) < 10000) return;
        window.lmLS.set(LS_MINE_LAST, String(Date.now()));
      }

      checkMine(function(mine) {
        setMineFillCache(mine); // оновлює LS_MINE_HAS + LS_MINE_FILL_AT за свіжими/живими даними
        if (!mine) return;
        var threshold = getThreshold();
        if (mine.can_collect && (mine.storage_progress >= threshold || mine.is_storage_full)) {
          collectMine(function(ok, data) {
            if (ok) {
              // 8.0.3: збір вдався → сховище обнулилось. Перераховуємо НАСТУПНИЙ час заповнення з max_storage_seconds,
              // чистимо бекоф/лічильник невдач і оновлюємо живий снапшот (щоб не перезібрати зі старих даних і не зациклитись).
              window.lmLS.remove(LS_MINE_RETRY_AT);
              window.lmLS.remove(LS_MINE_FAILS);
              var fullSecs = Number(mine.max_storage_seconds || 0);
              var toThrSec = fullSecs * (getThreshold() / 100);
              if (fullSecs > 0) window.lmLS.set(LS_MINE_FILL_AT, String(Date.now() + toThrSec * 1000));
              var _pml = window.labyrinthData && window.labyrinthData.personalMine;
              if (_pml) {
                _pml.storage_progress = 0; _pml.is_storage_full = false; _pml.can_collect = false;
                _pml.storage_seconds_passed = 0; _pml.storage_seconds_left = fullSecs;
                _pml.pending_acc = 0; _pml.pending_cards = 0;
              }
              lmDebug('[LabMap Mine] собрано:', data.acc, 'АСС; наступний збір ~через', Math.round(toThrSec / 60), 'хв');
            } else {
              // 8.0.3: збір НЕ вдався → прогресивний бекоф 10→20→40→60 хв (без спаму запитами).
              var _fails = parseInt(window.lmLS.get(LS_MINE_FAILS) || '0', 10) + 1;
              window.lmLS.set(LS_MINE_FAILS, String(_fails));
              var _backoff = Math.min(60, 10 * Math.pow(2, _fails - 1)) * 60 * 1000;
              window.lmLS.set(LS_MINE_RETRY_AT, String(Date.now() + _backoff));
              lmDebug('[LabMap Mine] збір не вдався (#' + _fails + '), повтор через', Math.round(_backoff / 60000), 'хв');
            }
          });
        } else {
          // не готово / не можна збирати → це НЕ невдача збору: чистимо лічильник; fill_at вже оновлено setMineFillCache.
          window.lmLS.remove(LS_MINE_FAILS);
          // Якщо за часом думали що готово і сховище справді повне, але can_collect=false — кулдаун 30 хв проти зайвих fetch.
          if (_believeReady && (mine.storage_progress >= threshold || mine.is_storage_full)) {
            window.lmLS.set(LS_MINE_RETRY_AT, String(Date.now() + 30 * 60 * 1000));
          }
        }
      }, _believeReady);
    }

    var _mineTimer = null;
    function getSmartDelay() {
      // 8.0.3: у бекофі після невдалого збору — плануємо рівно на час повтору (не раніше), щоб не спамити.
      var retryAt = parseInt(window.lmLS.get(LS_MINE_RETRY_AT) || '0', 10);
      if (retryAt && Date.now() < retryAt) return retryAt - Date.now();

      var pm = window.labyrinthData && window.labyrinthData.personalMine;

      // 1) Живі дані на сторінці лабіринту — найточніше. Заодно оновлюємо кеш стану/часу для інших вкладок.
      if (pm && pm.has_mine && pm.max_storage_seconds > 0) {
        setMineFillCache(pm);
        var pct = Number(pm.storage_progress || 0);
        if (pct >= getThreshold() || pm.is_storage_full) return 0;
        var fa = parseInt(window.lmLS.get(LS_MINE_FILL_AT) || '0', 10);
        return Math.max(15000, (fa + 30000) - Date.now());
      }

      var last   = parseInt(window.lmLS.get(LS_MINE_LAST) || '0', 10);
      var baseMs = getInterval() * 60 * 1000;

      // 2) Шахти нема (живі дані АБО кеш LS_MINE_HAS) → бекоф до 1 год (раніше дарма фетчив /labyrinth/ кожні 5 хв).
      //    last===0 (ще не перевіряли) → негайно, щоб один раз дізнатись стан, далі вже бекоф.
      if ((pm && !pm.has_mine) || (!pm && window.lmLS.get(LS_MINE_HAS) === '0')) {
        return last === 0 ? 0 : Math.max(0, (last + Math.max(NO_MINE_BACKOFF_MS, baseMs)) - Date.now());
      }

      // 3) Поза сторінкою лабіринту, але шахта Є → плануємо ТОЧНО на час заповнення (кеш LS_MINE_FILL_AT + 30с буфер),
      //    а не сліпим 5-хв опитуванням. Збір спрацює за ~30с після заповнення навіть на іншій сторінці.
      //    Стеля 1 год — перестрахування: якщо до заповнення довго, раз на годину освіжимо оцінку.
      var fillAt = parseInt(window.lmLS.get(LS_MINE_FILL_AT) || '0', 10);
      if (fillAt) {
        var untilFull = (fillAt + 30000) - Date.now();
        if (untilFull <= 0) return 0;
        return Math.max(30000, Math.min(untilFull, NO_MINE_BACKOFF_MS));
      }

      // 4) Ще нічого не виміряли → перевірити раз, щоб дізнатись стан шахти.
      return last === 0 ? 0 : Math.max(0, (last + baseMs) - Date.now());
    }
    function scheduleNext() {
      if (_mineTimer) clearTimeout(_mineTimer);
      if (!isEnabled()) return;
      var delay = getSmartDelay();
      _mineTimer = setTimeout(function() {
        _mineTimer = null;
        if (!isEnabled()) { scheduleNext(); return; }
        runCheck(); scheduleNext();
      }, delay);
    }

    function injectMineSettingsBtn() {
      function tryInject() {
        var mineEl = document.getElementById('labyrinthPersonalMine');
        if (!mineEl || mineEl.style.display === 'none' || window.getComputedStyle(mineEl).display === 'none') return;
        if (mineEl.querySelector('#lm-mine-settings-btn')) return;
        var actionsEl = mineEl.querySelector('.labyrinth__personal-mine-actions');
        if (!actionsEl) return;
        var btn = document.createElement('button');
        btn.id = 'lm-mine-settings-btn'; btn.type = 'button'; btn.className = 'button';
        btn.innerHTML = '⚙ Авто-сбор';
        btn.style.cssText = 'width:100%;margin-top:8px;background:linear-gradient(135deg,#4a5ac7,#3a4ab0);border:none;color:#fff;font-weight:700;';
        btn.addEventListener('click', function(e) { e.stopImmediatePropagation(); e.preventDefault(); showMineSettings(); });
        actionsEl.appendChild(btn);
      }
      tryInject();
      [200, 500, 1000, 2000, 3000].forEach(function(ms) { setTimeout(tryInject, ms); });
      var _injectScheduled = false;
      var obs = new MutationObserver(function(mutations) {
        if (_injectScheduled) return;
        for (var i = 0; i < mutations.length; i++) {
          var t = mutations[i].target;
          if (t.id === 'labyrinthPersonalMine' || (t.classList && t.classList.contains('labyrinth__personal-mine-actions'))) {
            _injectScheduled = true;
            setTimeout(function() { _injectScheduled = false; tryInject(); }, 100);
            return;
          }
        }
      });
      var mineEl = document.getElementById('labyrinthPersonalMine');
      if (mineEl) obs.observe(mineEl, { attributes: true, attributeFilter: ['style'], childList: true, subtree: true });
      var rightEl = document.querySelector('.labyrinth__right');
      if (rightEl) obs.observe(rightEl, { childList: true, subtree: false, attributes: true, attributeFilter: ['style'] });
    }

    function showMineSettings() {
      var existing = document.getElementById('lm-mine-modal');
      if (existing) { existing.classList.add('on'); return; }
      var modal = document.createElement('div'); modal.id = 'lm-mine-modal';
      var enabled   = isEnabled();
      var interval  = getInterval();
      var threshold = getThreshold();
      var last = parseInt(window.lmLS.get(LS_MINE_LAST) || '0', 10);
      var lastStr = last ? getMoscowTimeStr(last) : 'ещё не проверялась';
      modal.innerHTML =
        '<div id="lm-mine-overlay"></div>' +
        '<div id="lm-mine-box">' +
          '<div id="lm-mine-head"><div id="lm-mine-title">⛏ Авто-сбор шахты</div><button id="lm-mine-close">×</button></div>' +
          '<div id="lm-mine-body">' +
            '<div class="lm-mine-row">' +
              '<div class="lm-mine-label">Авто-сбор включён</div>' +
              '<label class="lm-mine-toggle"><input type="checkbox" id="lm-mine-enabled" ' + (enabled ? 'checked' : '') + '><span class="lm-mine-slider"></span></label>' +
            '</div>' +
            '<div class="lm-mine-row">' +
              '<div class="lm-mine-label">Собирать при заполненности</div>' +
              '<div class="lm-mine-input-wrap"><input type="number" id="lm-mine-threshold" value="' + threshold + '" min="1" max="100" step="1"><span class="lm-mine-pct-label">%</span></div>' +
            '</div>' +
            '<div class="lm-mine-row">' +
              '<div class="lm-mine-label">Резервный интервал (мин)</div>' +
              '<input type="number" id="lm-mine-interval" value="' + interval + '" min="1" max="1440" step="1">' +
            '</div>' +
            '<div class="lm-mine-info">Последняя проверка: <b>' + lastStr + '</b></div>' +
            '<div id="lm-mine-actions">' +
              '<button id="lm-mine-check-now" class="button button--primary">🔍 Проверить сейчас</button>' +
              '<button id="lm-mine-save" class="button">💾 Сохранить</button>' +
            '</div>' +
            '<div id="lm-mine-status"></div>' +
          '</div>' +
        '</div>';
      var style = document.createElement('style');
      style.textContent = '#lm-mine-modal{display:none;position:fixed;inset:0;z-index:9999999;align-items:center;justify-content:center;}#lm-mine-modal.on{display:flex;}#lm-mine-overlay{position:absolute;inset:0;background:rgba(0,0,0,.7);}#lm-mine-box{position:relative;z-index:2;background:#1a1d2a;border:1px solid rgba(255,255,255,.12);border-radius:20px;padding:0;width:min(380px,calc(100vw - 24px));color:#fff;overflow:hidden;}#lm-mine-head{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;border-bottom:1px solid rgba(255,255,255,.08);}#lm-mine-title{font-size:16px;font-weight:800;}#lm-mine-close{width:32px;height:32px;border:none;border-radius:8px;background:#c0392b;color:#fff;font-size:18px;cursor:pointer;}#lm-mine-body{padding:18px 20px;display:flex;flex-direction:column;gap:14px;}.lm-mine-row{display:flex;align-items:center;justify-content:space-between;gap:10px;}.lm-mine-label{font-size:14px;color:rgba(255,255,255,.8);flex:1;}.lm-mine-toggle{position:relative;display:inline-block;width:44px;height:24px;}.lm-mine-toggle input{opacity:0;width:0;height:0;}.lm-mine-slider{position:absolute;inset:0;background:rgba(255,255,255,.15);border-radius:24px;cursor:pointer;transition:.2s;}.lm-mine-slider:before{content:"";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s;}.lm-mine-toggle input:checked + .lm-mine-slider{background:#4a5ac7;}.lm-mine-toggle input:checked + .lm-mine-slider:before{transform:translateX(20px);}.lm-mine-input-wrap{display:flex;align-items:stretch;flex-shrink:0;}#lm-mine-threshold{width:52px;padding:6px 8px;border:1px solid rgba(255,255,255,.15);border-radius:10px 0 0 10px;border-right:none;background:rgba(255,255,255,.06);color:#fff;font-size:14px;text-align:center;}.lm-mine-pct-label{font-size:13px;color:rgba(255,255,255,.65);padding:0 10px;border:1px solid rgba(255,255,255,.15);border-radius:0 10px 10px 0;background:rgba(255,255,255,.04);display:flex;align-items:center;}#lm-mine-interval{width:70px;padding:6px 10px;border:1px solid rgba(255,255,255,.15);border-radius:10px;background:rgba(255,255,255,.06);color:#fff;font-size:14px;text-align:center;}.lm-mine-info{font-size:12px;color:rgba(255,255,255,.45);padding:8px 12px;border-radius:10px;background:rgba(255,255,255,.04);}.lm-mine-info b{color:rgba(255,255,255,.75);}#lm-mine-actions{display:flex;flex-direction:column;gap:8px;}#lm-mine-actions .button{width:100%;justify-content:center;}#lm-mine-status{font-size:13px;font-weight:700;text-align:center;min-height:20px;color:#6ee786;}';
      document.head.appendChild(style);
      document.body.appendChild(modal);
      modal.classList.add('on');
      document.getElementById('lm-mine-close').addEventListener('click', function() { modal.classList.remove('on'); });
      document.getElementById('lm-mine-overlay').addEventListener('click', function() { modal.classList.remove('on'); });
      document.getElementById('lm-mine-save').addEventListener('click', function() {
        var en   = document.getElementById('lm-mine-enabled').checked;
        var inv  = parseInt(document.getElementById('lm-mine-interval').value, 10)  || DEFAULT_INTERVAL;
        var thr  = parseInt(document.getElementById('lm-mine-threshold').value, 10) || DEFAULT_THRESHOLD;
        inv = Math.max(1, Math.min(1440, inv)); thr = Math.max(1, Math.min(100, thr));
        window.lmLS.set(LS_MINE_ENABLED, String(en));
        window.lmLS.set(LS_MINE_INTERVAL, String(inv));
        window.lmLS.set(LS_MINE_THRESHOLD, String(thr));
        var st = document.getElementById('lm-mine-status');
        st.textContent = 'Сохранено!'; st.style.color = '#6ee786';
        scheduleNext();
        if (en) setTimeout(function() { runCheck(); }, 500);
        setTimeout(function() { st.textContent = ''; modal.classList.remove('on'); }, 1200);
      });
      document.getElementById('lm-mine-check-now').addEventListener('click', function() {
        var st = document.getElementById('lm-mine-status');
        st.style.color = 'rgba(255,255,255,.6)'; st.textContent = 'Проверяем...';
        checkMine(function(mine) {
          if (!mine) { st.style.color = '#ff8b8b'; st.textContent = 'Не удалось получить данные'; return; }
          var thr2 = parseInt((document.getElementById('lm-mine-threshold')||{}).value || getThreshold(), 10) || getThreshold();
          if (mine.storage_progress >= thr2 || mine.is_storage_full) {
            st.style.color = '#ffd66b'; st.textContent = mine.storage_progress + '%! Собираем...';
            collectMine(function(ok, data) {
              if (ok) { st.style.color = '#6ee786'; st.textContent = 'Собрано ' + (data.acc || 0) + ' АСС'; }
              else { st.style.color = '#ff8b8b'; st.textContent = 'Ошибка сбора'; }
            });
          } else {
            st.style.color = 'rgba(255,255,255,.6)'; st.textContent = 'Сейчас ' + mine.storage_progress + '% — порог ' + thr2 + '%';
          }
        });
      });
    }

    scheduleNext();
    if (IS_LABYRINTH) {
      if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', injectMineSettingsBtn); }
      else { injectMineSettingsBtn(); }
    }
    lmDebug('[LabMap Mine] авто-сбор инициализирован, интервал:', getInterval(), 'min');
  })();


function collectRoomExtra() {
  var result = {};
  var ld = window.labyrinthData || {};

  var ro = ld.roomOpener;
  if (ro && ro.name) {
    result.first_opener = String(ro.name).trim();
    if (ro.date) {
      var dm = String(ro.date).match(/(\d{2})\.(\d{2})\.(\d{4})[^\d]*(\d{2}:\d{2})/);
      if (dm) result.first_opened_at = dm[3]+'-'+dm[2]+'-'+dm[1]+'T'+dm[4]+':00';
    }
  }

  if (!result.first_opener) {
    var openerEl = document.querySelector('.labyrinth__opener-name, .room-opener .username, [class*="opener"] .name');
    if (openerEl) {
      result.first_opener = openerEl.textContent.trim();
      var openerBlock = openerEl.closest('[class*="opener"], .labyrinth__opener');
      if (openerBlock) {
        var dateMatch = openerBlock.textContent.match(/(\d{2})\.(\d{2})\.(\d{4})[^\d]*(\d{2}:\d{2})/);
        if (dateMatch) result.first_opened_at = dateMatch[3]+'-'+dateMatch[2]+'-'+dateMatch[1]+'T'+dateMatch[4]+':00';
      }
    }
  }

  var pm = ld.personalMine;
  if (pm && pm.has_mine) {
    result.mine = {
      is_owner: true,
      level: Number(pm.level || 0),
      storage_pct: Number(pm.storage_progress || 0),
      acc_inside: Number(pm.pending_acc || 0),
      cards_current: Number(pm.pending_cards || 0),
      cards_max: Number(pm.max_cards || 0),
      acc_per_hour: Number(pm.acc_per_hour || 0),
      storage_seconds_left: Number(pm.storage_seconds_left || 0),
      is_storage_full: !!pm.is_storage_full
    };
  } else if (ld.foreignMine) {
    var fm = ld.foreignMine;
    result.mine = {
      is_owner: false,
      owner: fm.owner || fm.username || fm.user || null,
      level: Number(fm.level || 0),
      storage_pct: Number(fm.storage_progress || 0),
      acc_inside: Number(fm.pending_acc || fm.acc || 0),
      cards_current: Number(fm.pending_cards || fm.cards || 0),
      cards_max: Number(fm.max_cards || 0)
    };
  }

  if (ld.echoRoom) result.echo = ld.echoRoom;
  if (ld.fateRoom) result.fate = ld.fateRoom;
  if (ld.clubWarRoom) result.club_war = ld.clubWarRoom;
  if (typeof ld.canPlaceObject !== 'undefined') result.can_place_object = !!ld.canPlaceObject;
  if (ld.emission && ld.emission.last_start_at) {
    result.emission = {
      active: !!ld.emission.active,
      last_start_at: ld.emission.last_start_at,
      last_end_at: ld.emission.last_end_at || 0,
      cooldown_left: ld.emission.cooldown_left || 0
    };
    if (ld.emission.active) result.during_emission = true;
  }

  return result;
}


function sendRollbackToVPS(count, place) {
  lmDebug('[Синхронизация] sendRollbackToVPS:', count, place);
  var VPS_API  = LM_API_BASE;
  var auth = getAuth();
  if (!auth || count == null) return;
  var username = auth.username;

  window.lmFetch(VPS_API + '/sync/rollbacks', {
    method: 'POST',
    headers: vpsAuthHeaders(true),
    body: JSON.stringify({ trap_backs: count, rollback_place: place }),
  })
  .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
  .then(function(res){
    lmDebug('[Синхронизация] Откаты отправлены на VPS:', count, '| Топ:', place);
  })
  .catch(function() {});
}

function sendAccHistoryToVPS() {
  if (Date.now() - (window._lmLastAccSync || 0) < 20000) { lmDebug('[Синхронизация] acc-sync throttled (<20с)'); return; }
  window._lmLastAccSync = Date.now();
  lmDebug('[Синхронизация] sendAccHistoryToVPS викликано, histCache pages:', Object.keys(histCache).length);
  var VPS_API  = LM_API_BASE;
  var auth = getAuth();
  if (!auth) { lmDebug('[Синхронизация] Нет имени пользователя или токена'); return; }
  var username = auth.username;

  function doSend() {
    if (!histCache || !Object.keys(histCache).length) return;
    var rows = [];
    lmDebug('[Синхронизация] doSend histCache keys:', Object.keys(histCache));
    Object.keys(histCache).forEach(function(page) {
      var pageData = histCache[page];
      if (!pageData || !pageData.rows) { lmDebug('[Синхронизация] page',page,'нет строк'); return; }
      lmDebug('[Синхронизация] page',page,'rows count:', pageData.rows.length);
      pageData.rows.forEach(function(row) {
        var desc    = row.desc || row.description || '';
        var date    = row.date || row.ts || null;
        var amount  = row.amount  !== undefined ? row.amount  : row.acc_delta;
        var balance = row.balance !== undefined ? row.balance : row.acc_after;
        if (!desc) return;

        var isoDate = null;
        if (date) {
          var dm = String(date).match(/(\d{2})\.(\d{2})\.(\d{4})\s+(\d{2}:\d{2}:\d{2})/);
          if (dm) isoDate = dm[3]+'-'+dm[2]+'-'+dm[1]+'T'+dm[4];
          else isoDate = date;
        }

        var amtStr = String(amount||'0');
        var isNeg  = amtStr.indexOf('-') >= 0;
        var delta  = parseInt(amtStr.replace(/[^0-9]/g,'')) * (isNeg ? -1 : 1);
        var bal    = balance !== null && balance !== undefined
                     ? parseInt(String(balance).replace(/[^0-9]/g,'')) : null;

        rows.push({
          ts:          isoDate,
          description: desc,
          acc_delta:   isNaN(delta) ? null : delta,
          acc_after:   bal,
        });
      });
    });
    lmDebug('[Синхронизация] Строк для отправки собрано:', rows.length, '| перший:', rows[0] ? JSON.stringify(rows[0]) : 'нет');
    if (!rows.length) return;
    window.lmFetch(VPS_API + '/sync/acc-history', {
      method: 'POST',
      headers: vpsAuthHeaders(true),
      body: JSON.stringify({ rows: rows }),
    })
    .then(function(r){ if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
    .then(function(res){
      lmDebug('[Синхронизация] ACC-история отправлена:', res.saved, 'строк');
    })
    .catch(function() {});
  }

  // ФІКС 8.0.1+8.0.2: рефетчимо ЛИШЕ найновішу сторінку (нові транзакції завжди на стор.1)
  // — 1 запит замість 13, щоб НЕ бомбити animesss.com (причина 520 "забагато запитів").
  // Старі сторінки лишаються в кеші (вони не міняються), сервер дедуплікує.
  // Це водночас виправляє staleness: стор.1 завжди свіжа, нові транзакції доходять.
  delete histCache[1];
  loadHistPage(1, function() { doSend(); });
}

function parseRoomDetails(ev, accDelta) {
  var labData = window.labyrinthData;
  var evData  = labData && labData.lastEventData;
  var details = {};

  switch(ev) {
    case 'quiz': case 'quiz_result':
      var qEl = document.querySelector('#labyrinthQuizQuestion, .labyrinth-quiz__question, .labyrinth__quiz-question, .quiz-question');
      var aEl = document.querySelector('#labyrinthQuiz .button.is-active, .quiz-answer.selected, .labyrinth-quiz__answer.active, .labyrinth__quiz-answer.active, .answer-btn.active');
      if (qEl) details.question     = qEl.textContent.trim().slice(0,300);
      if (aEl) details.answer_given = aEl.textContent.trim().slice(0,100);
      details.is_correct = accDelta > 0;
      break;

    case 'personal_mine':
    case 'personal_mine_created':
    case 'personal_mine_collect':
      var mEl = document.querySelector('#labyrinthPersonalMine, .labyrinth__personal-mine, .mine-info, #labyrinthMineInfo, .labyrinth-mine');
      if (mEl) {
        var t = mEl.textContent;
        var lv = t.match(/уровень[^\d]*(\d+)/i)||t.match(/level[^\d]*(\d+)/i);
        var fl = t.match(/(\d+)\s*%/);
        var ac = t.match(/(\d+)\s*ACC/i);
        if (lv) details.level     = parseInt(lv[1]);
        if (fl) details.fill_pct  = parseInt(fl[1]);
        if (ac) details.acc_inside= parseInt(ac[1]);
      }
      if (evData) {
        details.level     = details.level    ||evData.level    ||null;
        details.fill_pct  = details.fill_pct ||evData.fillPct  ||null;
        details.acc_inside= details.acc_inside||evData.accInside||null;
      }
      details.action = accDelta > 0 ? 'collected' : 'viewed';
      break;

    case 'foreign_mine':
      var fmEl = document.querySelector('#labyrinthForeignMine .card-inline__name, #labyrinthForeignMine, .labyrinth__foreign-mine, .mine-owner, .foreign-mine .owner, .card-inline__name');
      if (fmEl) details.owner = fmEl.textContent.trim();
      else if (evData&&evData.owner) details.owner = evData.owner;
      details.action = accDelta !== 0 ? 'collected_tribute' : 'viewed';
      break;

    case 'card_trader': case 'card_trader_result':
      var cEl = document.querySelector('.labyrinth__trader-name, .trader-card .card-inline__name, .trade-card-name');
      var pEl = document.querySelector('.labyrinth__trader-price, .trader-price, .trade-cost');
      if (cEl) details.card_name = cEl.textContent.trim();
      if (pEl) details.price_acc = parseInt(pEl.textContent)||null;
      details.action = accDelta < 0 ? 'bought' : 'declined';
      break;

    case 'spiritual_teleport':
      if (evData) {
        details.teleport_to_x  = evData.toX   ||null;
        details.teleport_to_y  = evData.toY   ||null;
        details.guardian_owner = evData.owner ||null;
      }
      details.action = accDelta < 0 ? 'paid' : (accDelta > 0 ? 'won' : 'viewed');
      break;

    case 'mimic_chest': case 'mimic_chest_hit': case 'mimic_chest_killed':
    case 'mimic_chest_escape': case 'mimic_chest_reward': case 'mimic_chest_back':
      details.result = accDelta > 0 ? 'real_chest' : accDelta < 0 ? 'mimic_trap' : 'alive_chest';
      var mhpEl = document.querySelector('#labyrinthMimicHpText, .labyrinth__mimic-hp-text');
      if (mhpEl) details.hp = mhpEl.textContent.trim().slice(0,40);
      break;

    case 'room_gift':
      details.action = 'triggered';
      if (accDelta > 0) details.acc_inside = accDelta;
      break;

    case 'room_trap':
      details.action = 'triggered';
      if (accDelta < 0) details.acc_penalty = accDelta;
      break;

    case 'club_war_room':
      var cwEl = document.querySelector('#labyrinthClubWarClubs .is-active, .labyrinth__club-war-club.is-active, .club-war-target, .attack-club');
      if (cwEl) details.attacked_club = cwEl.textContent.trim();
      details.won = accDelta >= 0;
      details._admin_only = true;
      break;

    case 'dao_test': case 'dao_result':
      details.result = accDelta > 0 ? 'acc' : accDelta < 0 ? 'trap' : 'nothing';
      if (accDelta !== 0) details.acc_delta = accDelta;
      break;

    case 'relic_room':
      details.piece_or_full = accDelta >= 60 ? 'full' : 'piece';
      details.acc_value = accDelta;
      break;

    case 'fate_room': case 'fate_room_result':
      var frd = window.labyrinthData && window.labyrinthData.fateRoom;
      if (frd && frd.options) {
        details.has_options = true;
        details.option_count = Object.keys(frd.options).length;
      }
      details.choice_made = accDelta !== 0;
      break;
  }

  return Object.keys(details).length > 0 ? details : null;
}

function sendFirstOpener(x, y, opener, openedAt) {
  var VPS_API  = LM_API_BASE;
  if (!getAuth() || !opener) return;

  window.lmFetch(VPS_API + '/room/first-opener', {
    method: 'POST',
    headers: vpsAuthHeaders(true),
    body: JSON.stringify({ x: x, y: y, first_user: opener, first_at: openedAt }),
  }).catch(function(){});
}

function sendMineData(x, y, mine) {
  var VPS_API  = LM_API_BASE;
  if (!getAuth() || !mine) return;

  window.lmFetch(VPS_API + '/room/mine', {
    method: 'POST',
    headers: vpsAuthHeaders(true),
    body: JSON.stringify({ x: x, y: y, ...mine }),
  }).catch(function(){});
}

(function() {
  'use strict';

  const VPS_WS  = LM_WS_URL;
  const VPS_API = LM_API_BASE;

  const USERNAME = window.visitor_name   || '';
  const TOKEN    = window.dle_login_hash || '';
  if (!USERNAME || !TOKEN) return;
  if (!window.location.pathname.startsWith('/labyrinth')) return;

  const SS_KEY   = 'lm_step_' + USERNAME;
  const SS_BAL   = 'lm_bal_'  + USERNAME;

  let ws      = null;
  let authed  = false;
  let queue   = [];
  let pingTimer = null;
  let reconnectTimer = null;
  let reconnectDelay = 2000;
  let connecting = false;
  let registrationPromise = null;

  function ensureRegistered() {
    if (registrationPromise) return registrationPromise;

    registrationPromise = window.lmFetch(VPS_API + '/register', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Script-Version': LM_SCRIPT_VERSION },
      body: JSON.stringify({ username: USERNAME, token: TOKEN, script_version: LM_SCRIPT_VERSION })
    }).then(function(response) {
      if (response.ok) return true;
      if (response.status === 409) { registrationPromise = null; return false; }
      if (response.status === 429) throw new Error('register_rate_limited');
      throw new Error('register_http_' + response.status);
    }).catch(function(error) {
      registrationPromise = null;
      lmDebug('[Карта] Регистрация на VPS:', error && error.message);
      return false;
    });

    return registrationPromise;
  }

  function startPing() {
    if (pingTimer) return;
    pingTimer = setInterval(function() {
      if (ws && ws.readyState === WebSocket.OPEN && authed) {
        try { ws.send(JSON.stringify({ type: 'ping' })); } catch(e) {}
      }
    }, 25000);
  }

  function scheduleReconnect() {
    if (reconnectTimer) return;
    reconnectTimer = setTimeout(function() {
      reconnectTimer = null;
      connect();
    }, reconnectDelay);
    reconnectDelay = Math.min(reconnectDelay * 2, 30000);
  }

  function connect() {
    if (connecting) return;
    if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) return;
    connecting = true;

    ensureRegistered().then(function(registered) {
      if (!registered) {
        connecting = false;
        scheduleReconnect();
        return;
      }
      try {
        ws = new WebSocket(VPS_WS);
      } catch(e) {
        connecting = false;
        scheduleReconnect();
        return;
      }

      ws.onopen = function() {
        connecting = false;
        reconnectDelay = 2000;
        startPing();
        ws.send(JSON.stringify({ type: 'auth', user: USERNAME, token: TOKEN, script_version: LM_SCRIPT_VERSION }));
      };

      ws.onmessage = function(e) {
        try {
          var msg = JSON.parse(e.data);
          if (msg.type === 'auth_ok') {
            authed = true;
            lmDebug('[Карта] Синхронизация включена');
            var statusEl = document.getElementById('lm-status');
            if (statusEl) statusEl.textContent = '🟢 Карта подключена';
            var pending = queue.slice();
            queue = [];
            pending.forEach(function(ev){ sendEv(ev); });
          }
        } catch(err) {}
      };

      ws.onclose = function(event) {
        connecting = false;
        authed = false;
        var statusEl = document.getElementById('lm-status');
        if (event && event.code === 4001) {
          if (statusEl) statusEl.textContent = '🟡 Активна другая вкладка';
          reconnectDelay = 30000;
        } else {
          if (statusEl) statusEl.textContent = '🔴 Нет соединения';
        }
        scheduleReconnect();
      };

      ws.onerror = function() {
        try { ws.close(); } catch(e) {}
      };
    });
  }

  function sendEv(ev) {
    if (!ws || ws.readyState !== WebSocket.OPEN || !authed) {
      if (queue.length < 1000) queue.push(ev);
      return;
    }
    try { ws.send(JSON.stringify(Object.assign({ type: 'event' }, ev))); } catch(e) {}
  }

  function getBalance() {
    var el = document.getElementById('labyrinthBank');
    if (!el) return null;
    return parseInt(el.textContent.replace(/[^0-9]/g, '')) || 0;
  }
  window._lmGetBalance = getBalance;
  window._lmSendEv     = sendEv;

  function startPolling() {
    var sessionId = PUSH_SESSION_ID;

    var savedState = null;
    try {
      var raw = window.lmSS.get(SS_KEY);
      if (raw) savedState = JSON.parse(raw);
    } catch(e) {}

    var lastStepCount = savedState ? savedState.count : -1;
    var sentStepKeys  = savedState ? new Set(savedState.keys || []) : new Set();

    var prevBalance = null;
    try {
      var savedBal = window.lmSS.get(SS_BAL);
      if (savedBal) prevBalance = parseInt(savedBal);
    } catch(e) {}

    function saveState() {
      try {
        var keysArr = Array.from(sentStepKeys).slice(-200);
        window.lmSS.set(SS_KEY, JSON.stringify({
          count: lastStepCount,
          keys: keysArr
        }));
      } catch(e) {}
    }

    var initDone = false;
    var initTimer = setInterval(function() {
      var labData = window.labyrinthData;
      if (!labData) return;
      var steps = labData.mapData && labData.mapData.steps;
      if (!steps) return;

      var today = getMoscowDate();
      var savedDay = null;
      try { savedDay = window.lmSS.get('lm_day_' + USERNAME); } catch(e) {}
      if (savedDay !== today) {
        sentStepKeys.clear();
        lastStepCount = -1;
        try { window.lmSS.set('lm_day_' + USERNAME, today); } catch(e) {}
      }

      if (lastStepCount === -1) {
        lastStepCount = steps.length;
        steps.forEach(function(s, i) {
          sentStepKeys.add(i + '_' + (s.event||'') + '_' + (s.x||0) + '_' + (s.y||0));
        });
        saveState();
      }

      prevBalance = getBalance();
      try { window.lmSS.set(SS_BAL, String(prevBalance)); } catch(e) {}

      clearInterval(initTimer);
      initDone = true;
    }, 500);

    setInterval(function() {
      if (!initDone) return;

      var labData = window.labyrinthData;
      if (!labData) return;
      var cur   = labData.mapData && labData.mapData.current;
      var steps = labData.mapData && labData.mapData.steps;
      var ev    = labData.lastEvent;
      if (!cur || !steps) return;

      var stepCount = steps.length;

      if (stepCount < lastStepCount && lastStepCount > 0) {
        lastStepCount = stepCount;
        saveState();
      }

      for (var i = Math.max(0, lastStepCount); i < stepCount; i++) {
        var step = steps[i];
        if (!step) continue;

        var stepKey = i + '_' + (step.event||ev||'') + '_' + (step.x||cur.x||0) + '_' + (step.y||cur.y||0);
        if (sentStepKeys.has(stepKey)) continue;

        sentStepKeys.add(stepKey);

        var stableIdx;
        if (Number.isInteger(Number(step._lmServerStepIndex))) {
          stableIdx = Number(step._lmServerStepIndex);
        } else {
          var seq = parseInt(window.lmLS.get(LS_SERVER_SEQ) || '0', 10);
          if (!Number.isFinite(seq) || seq < 0) seq = 0;
          step._lmServerStepIndex = seq;
          window.lmLS.set(LS_SERVER_SEQ, String(seq + 1));
          stableIdx = seq;
        }

        (function(stepIdx, stepData, isLast) {
          var delay = isLast ? 800 : 0;
          setTimeout(function() {
            var balAfter = isLast ? getBalance() : null;
            if (balAfter !== null) {
              try { window.lmSS.set(SS_BAL, String(balAfter)); } catch(e) {}
            }
            var delta = (prevBalance !== null && balAfter !== null)
              ? balAfter - prevBalance : 0;

            var sx = stepData.x != null ? stepData.x : cur.x;
            var sy = stepData.y != null ? stepData.y : cur.y;
            var sev = stepData.event || ev || 'empty';

            var isCurrentRoom = isLast && sx === cur.x && sy === cur.y;
            var _extra = isCurrentRoom && window._lmCollectExtra
              ? window._lmCollectExtra()
              : {};

            var details = parseRoomDetails(sev, delta) || {};
            if (_extra.echo) details.echo = _extra.echo;
            if (_extra.fate) details.fate = _extra.fate;
            if (_extra.club_war) details.club_war = _extra.club_war;
            sendEv({
              x:          sx,
              y:          sy,
              ev:         sev,
              acc_before: prevBalance,
              acc_after:  balAfter != null ? balAfter : null,
              acc_delta:  delta,
              dat:        Object.keys(details).length ? details : null,
              session_id: sessionId,
              step_index: stepIdx
            });
            if (isCurrentRoom && _extra.first_opener && window._lmSendOpener) {
              window._lmSendOpener(sx, sy, _extra.first_opener, _extra.first_opened_at || null);
            }
            if (isCurrentRoom && _extra.mine && window._lmSendMine) {
              window._lmSendMine(sx, sy, _extra.mine);
            }

            if (isLast && balAfter !== null) prevBalance = balAfter;
          }, delay);
        })(stableIdx, step, i === stepCount - 1);
      }

      if (stepCount > lastStepCount) {
        lastStepCount = stepCount;
        saveState();
      }

    }, 1000);
  }

  connect();
  if (document.readyState === 'complete') {
    startPolling();
  } else {
    window.addEventListener('load', startPolling);
  }

})();

  window._lmCollectExtra = collectRoomExtra;
  window._lmSendOpener   = sendFirstOpener;
  window._lmSendMine     = sendMineData;

  var _lmHistSteps = null;
  var _lmEmissionCells = null;

  function lmApplyEmissionCells() {
    if (!_lmEmissionCells || !_lmEmissionCells.size) return;
    var mapEl = document.getElementById('labyrinthMap');
    if (!mapEl) return;
    var labData = window.labyrinthData;
    var cur = labData && labData.mapData && labData.mapData.current;
    if (!cur) return;
    var curX = parseInt(cur.x, 10) || 0;
    var curY = parseInt(cur.y, 10) || 0;
    var C = 12;
    _lmEmissionCells.forEach(function(key) {
      var parts = key.split('_');
      var sx = parseInt(parts[0], 10);
      var sy = parseInt(parts[1], 10);
      var gx = C + (sx - curX);
      var gy = C + (sy - curY);
      if (gx < 0 || gx >= 25 || gy < 0 || gy >= 25) return;
      var cell = mapEl.querySelector('[data-x="' + gx + '"][data-y="' + gy + '"]');
      if (!cell) return;
      cell.classList.add('lm-emission-cell');
      if (!cell.querySelector('.lm-emission-ico')) {
        var ico = document.createElement('span');
        ico.className = 'lm-emission-ico';
        ico.textContent = '⚡';
        ico.title = 'Посещено во время выброса';
        cell.appendChild(ico);
      }
    });
  }
  window._lmApplyEmissionCells = lmApplyEmissionCells;

  function lmApplyHistoricalPath() {
    if (!_lmHistSteps || !_lmHistSteps.length) return;
    var mapEl = document.getElementById('labyrinthMap');
    if (!mapEl) return;
    var labData = window.labyrinthData;
    var cur = labData && labData.mapData && labData.mapData.current;
    if (!cur) return;
    var curX = parseInt(cur.x, 10) || 0;
    var curY = parseInt(cur.y, 10) || 0;
    var C = 12; // center of 25x25 grid

    _lmHistSteps.forEach(function(s) {
      var sx = parseInt(s.x, 10);
      var sy = parseInt(s.y, 10);
      var ev = s.event || '';
      var gx = C + (sx - curX);
      var gy = C + (sy - curY);
      if (gx < 0 || gx >= 25 || gy < 0 || gy >= 25) return;
      var cell = mapEl.querySelector('[data-x="' + gx + '"][data-y="' + gy + '"]');
      if (!cell || !cell.classList.contains('labyrinth-cell--unknown')) return;
      cell.classList.remove('labyrinth-cell--unknown');
      cell.classList.add('labyrinth-cell--visited', 'lm-hist');
      if (ev) {
        cell.classList.add('labyrinth-cell--event-' + ev);
        cell.setAttribute('data-event', ev);
      }
    });
  }
  window._lmApplyHistoricalPath = lmApplyHistoricalPath;

  function lmFetchHistoricalPath() {
    if (!IS_LABYRINTH) return;
    var u = window.visitor_name || '';
    if (!u) return;
    var LS_HIST    = 'lm_hist_' + u;
    var LS_HIST_TS = 'lm_hist_ts_' + u;
    var LS_EM      = 'lm_em_cells_' + u;
    try {
      var ts = parseInt(window.lmLS.get(LS_HIST_TS) || '0', 10);
      if (Date.now() - ts < 5 * 60 * 1000) {
        var cached = JSON.parse(window.lmLS.get(LS_HIST) || 'null');
        var cachedEm = JSON.parse(window.lmLS.get(LS_EM) || 'null');
        if (cached && cached.length && cachedEm !== null) {
          _lmHistSteps = cached;
          window._lmHistSteps = cached; // 8.0.4: повна карта читає window-версію (лінія шляху)
          // Кеш-гілка теж має наповнити _lmMyPathData (канвас "Мой путь"),
          // інакше шлях на повній карті схлопується в поточну сесію (тонка лінія)
          if (!window._lmMyPathData) window._lmMyPathData = {};
          cached.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
          if (cachedEm && cachedEm.length) {
            _lmEmissionCells = new Set(cachedEm);
            window._lmEmissionCells = _lmEmissionCells;
          }
          setTimeout(function(){ lmApplyHistoricalPath(); lmApplyEmissionCells(); }, 100);
          return;
        }
      }
    } catch(e) {}

    window.lmFetch(VPS_URL + '/my/path', { headers: vpsAuthHeaders(false) })
      .then(function(r) { return r.ok ? r.json() : null; })
      .then(function(data) {
        if (!data || !Array.isArray(data.steps)) return;
        _lmHistSteps = data.steps;
        window._lmHistSteps = data.steps; // 8.0.4: повна карта читає window-версію (лінія шляху)
        if (!window._lmMyPathData) window._lmMyPathData = {};
        data.steps.forEach(function(s){ window._lmMyPathData[s.x+'_'+s.y]=true; });
        var emKeys = [];
        if (Array.isArray(data.emission_cells)) {
          emKeys = data.emission_cells.map(function(c){ return c.x + '_' + c.y; });
          _lmEmissionCells = new Set(emKeys);
          window._lmEmissionCells = _lmEmissionCells;
        }
        try {
          window.lmLS.set(LS_HIST, JSON.stringify(data.steps));
          window.lmLS.set(LS_HIST_TS, String(Date.now()));
          window.lmLS.set(LS_EM, JSON.stringify(emKeys));
        } catch(e) {}
        lmApplyHistoricalPath();
        lmApplyEmissionCells();
      })
      .catch(function() {});
  }

  if (IS_LABYRINTH) setTimeout(lmFetchHistoricalPath, 1500);

  // ФІКС 8.0.1: авто-синк ACC-історії — щоб не залежати від кнопки "Обновить".
  // Раніше синк був лише по кнопці + кеш застрягав → у 41 з 42 активних гравців 0 ACC-історії.
  if (IS_LABYRINTH) {
    setTimeout(sendAccHistoryToVPS, 9000);
    setInterval(sendAccHistoryToVPS, 15 * 60 * 1000);
  }

  (function loadMineSettingsFromServer() {
    var u = window.visitor_name || '';
    if (!u) return;
    // Якщо обидва значення вже є в localStorage — не робимо зайвий запит
    var lsEnabled   = window.lmLS.get('lm_mine_auto_enabled'  + _lmUserSuffix);
    var lsThreshold = window.lmLS.get('lm_mine_threshold'     + _lmUserSuffix);
    if (lsEnabled !== null && lsThreshold !== null) return;
    window.lmFetch(VPS_URL + '/my/profile', { headers: vpsAuthHeaders(false) })
      .then(function(r) { return r.ok ? r.json() : null; })
      .then(function(p) {
        if (!p) return;
        if (lsEnabled === null && p.mine_auto_enabled != null) {
          window.lmLS.set('lm_mine_auto_enabled' + _lmUserSuffix, p.mine_auto_enabled ? 'true' : 'false');
        }
        if (lsThreshold === null && p.mine_auto_threshold != null) {
          window.lmLS.set('lm_mine_threshold' + _lmUserSuffix, String(p.mine_auto_threshold));
        }
      })
      .catch(function() {});
  })();

  scheduleProfileSync();
  setInterval(function(){ syncProfileToVPS(false); }, 60000);
  if (IS_CLUB_PAGE) {
    setTimeout(function(){ checkAndSyncClub(true); }, 1200);

    setInterval(function(){ checkAndSyncClub(false); }, 10 * 60 * 1000);
  }

  window._lmVPS = {
    syncRollback: function() {
      var tb = window._lmAllTimeTrapBacks;
      var tp = window._lmRollbackPlace;
      lmDebug('[Синхронизация] Manual sync rollback:', tb, tp);
      if (tb) sendRollbackToVPS(tb, tp||null);
    },
    syncHistory: sendAccHistoryToVPS,
    syncAll: function() {
      window._lmVPS.syncRollback();
      sendAccHistoryToVPS();
    },
    getHistCache: function() { return histCache; },
    getHistSample: function() {
      var keys = Object.keys(histCache);
      if (!keys.length) return 'Кеш истории пуст';
      var first = histCache[keys[0]];
      console.log('pages:', keys.length, '| rows[0]:', JSON.stringify(first));
      return first;
    }
  };

})();