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.4.3
// @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 || '');

window._lmXHRPatched = false;
if (LM_IS_LABYRINTH_PAGE) {
(function() {
  var origOpen = XMLHttpRequest.prototype.open;
  var origSend = XMLHttpRequest.prototype.send;
  window._lmXHRPatched = true;

  var _lmPendingQuiz = null;
  function _lmQuizCapture(resp, reqBody) {
    if (!resp) return;
    if (resp.event === 'quiz' && resp.quiz && resp.quiz.answers && resp.text) {
      _lmPendingQuiz = { quiz_id: resp.quiz.id || null, text: String(resp.text), answers: resp.quiz.answers };
    } else if (resp.event === 'quiz_result' && resp.correct_answer != null && _lmPendingQuiz) {
      var pq = _lmPendingQuiz; _lmPendingQuiz = null;
      var correctText = pq.answers[String(resp.correct_answer)];
      if (!correctText) return;
      var selected = null;
      try { var k = new URLSearchParams(reqBody).get('answer'); if (k) selected = pq.answers[k]; } catch(e) {}
      if (window._lmReportQuiz) window._lmReportQuiz({
        quiz_id: pq.quiz_id, question_text: pq.text, answers: pq.answers,
        correct_answer_text: correctText, selected_answer: selected, is_correct: !!resp.is_correct
      });
    }
  }

  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;
            }

            try { _lmQuizCapture(resp, body); } catch(_e) {}

            try { if (window._lmScanUnknown) window._lmScanUnknown(resp); } 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);
          }
        });
      }

      if (body.indexOf('action=answer_fate_room') !== -1) {
        var _fxhr = this;

        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) {}
        });
      }

      if (body.indexOf('action=answer_quiz') !== -1) {
        var _qxhr = this, _qbody = body;
        _qxhr.addEventListener('load', function() {
          try { _lmQuizCapture(JSON.parse(_qxhr.responseText), _qbody); } 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.4.3';
  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 = 6;
  var FULL_VIEW_MAX_CELL = 12;
  var FULL_FOCUS_SCALE   = 0.42;
  var FOCUS_ZOOM_NEAR    = 1.1;

  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
        });
        if (window.labyrinthData && window.labyrinthData.emission && window.labyrinthData.emission.active) {
          var _em = window.labyrinthData.emission;
          copy._dat = Object.assign({}, copy._dat || copy.dat || {}, {
            during_emission: true,
            emission: { active: true, last_start_at: _em.last_start_at, last_end_at: _em.last_end_at, seconds_left: _em.seconds_left }
          });
        }
        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;
  }

  window._lmReportQuiz = function(payload) {
    try {
      if (!payload || !payload.question_text || !payload.correct_answer_text) return;
      window.lmFetch(VPS_URL + '/quiz/report', {
        method: 'POST',
        headers: vpsAuthHeaders(true),
        body: JSON.stringify(payload)
      }).then(function(r){ if (r && r.ok) lmDebug('[LabMap] вікторину збережено'); }).catch(function() {});
    } catch(e) {}
  };

  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'};
  var LM_RESULT_EV = {trap_back:1, echo_room_result:1, fate_room_result:1, room_trap_result:1};

  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', resonance_room:'#062028',
    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:'🧵',
    resonance_room:'📡',
    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',
    resonance_room:'#5fe0ff',
    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:'Выбор судьбы✓', resonance_room:'Резонанс',
    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:'Выбор судьбы уже сделан',
    resonance_room:'Комната резонанса — можно вызвать событие с других координат',
    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','Выбор судьбы'],['resonance_room','Резонанс'],['__variable__','🔀 Переменные']
  ];

  var cloudMap       = {};
  var _cloudMapCount = 0;
  var LS_BLOCKED = 'lm_blocked' + _lmUserSuffix;
  function _lmIsBlocked(){ return window.lmLS.get(LS_BLOCKED) === '1'; }
  function _lmSetBlocked(on){ try { if (on) window.lmLS.set(LS_BLOCKED, '1'); else window.lmLS.remove(LS_BLOCKED); } catch(e) {} }
  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 _lmPencil      = 'off';
  var _lmRoute       = [];
  var LS_ROUTE       = 'lm_route' + _lmUserSuffix;
  function _routeLoad(){ try { var a = JSON.parse(window.lmLS.get(LS_ROUTE) || '[]'); _lmRoute = Array.isArray(a) ? a : []; } catch(e){ _lmRoute = []; } }
  function _routeSave(){ try { window.lmLS.set(LS_ROUTE, JSON.stringify(_lmRoute)); } catch(e){} }
  var _routeSyncTimer = null;
  function _routeSyncToServer(){
    if (_routeSyncTimer) clearTimeout(_routeSyncTimer);
    _routeSyncTimer = setTimeout(function(){
      try { window.lmFetch(VPS_URL + '/route', { method:'POST', headers: vpsAuthHeaders(true), body: JSON.stringify({ route: _lmRoute }) }).catch(function(){}); } catch(e){}
    }, 1500);
  }
  function _routeRestoreFromServer(){
    if (_lmRoute.length) return;
    try {
      window.lmFetch(VPS_URL + '/route', { headers: vpsAuthHeaders(false) })
        .then(function(r){ return r.ok ? r.json() : null; })
        .then(function(d){ if (d && Array.isArray(d.route) && d.route.length && !_lmRoute.length) { _lmRoute = d.route; _routeSave(); if (fmOpen) drawFull(); patchSiteMap(); } })
        .catch(function(){});
    } catch(e){}
  }
  function _routeIdx(x,y){ for (var i=0;i<_lmRoute.length;i++) if (_lmRoute[i].x===x && _lmRoute[i].y===y) return i; return -1; }
  function _routeTrimToCurrent(){
    if (!_lmRoute.length) return;
    var c = curPos();
    if (!c || !Number.isFinite(Number(c.x))) return;
    var idx = _routeIdx(Number(c.x), Number(c.y));
    if (idx >= 0) {
      _lmRoute = _lmRoute.slice(idx + 1);
      _routeSave(); _routeSyncToServer();
      if (fmOpen) drawFull();
      if (window._lmPencilCount) window._lmPencilCount();
    }
  }
  function _adjacent(a,b){ return Math.abs(a.x-b.x) + Math.abs(a.y-b.y) === 1; }
  function _lmMsg(title, sub){ try { showToast(title, sub); hideToast(2200); } catch(e){} }

  function _esc(s){ return String(s==null?'':s).replace(/[&<>"]/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }
  var LS_CONFIRMS = 'lm_confirms' + _lmUserSuffix;
  function _cfLoad(){ try { var o=JSON.parse(window.lmLS.get(LS_CONFIRMS)||'{}'); return (o&&typeof o==='object')?o:{}; } catch(e){ return {}; } }
  function _cfSave(o){ try { window.lmLS.set(LS_CONFIRMS, JSON.stringify(o)); } catch(e){} }
  function _cfEnabled(key){ var o=_cfLoad(); return o[key]!==false; }
  var LM_CONFIRM_LIST = [
    { key:'place_gift',   ico:'🎁', label:'Оставить подарок' },
    { key:'place_trap',   ico:'🕸', label:'Оставить ловушку' },
    { key:'rob_mine',     ico:'⚔',  label:'Ограбить шахту' },
    { key:'help_mine',    ico:'🤝', label:'Помочь со сбором' },
    { key:'echo_repeat',  ico:'👣', label:'Повторить путь' },
    { key:'raid_soft',    ico:'🏴', label:'Лёгкий рейд' },
    { key:'raid_normal',  ico:'⚔',  label:'Обычный штурм' },
    { key:'raid_full',    ico:'💥', label:'Полное вторжение' },
    { key:'collect_mine', ico:'⛏', label:'Забрать добычу из шахты' },
    { key:'upgrade_mine', ico:'⬆', label:'Улучшить шахту' },
    { key:'buy_attempt',  ico:'👣', label:'Купить дополнительный ход' },
    { key:'boost',        ico:'✨', label:'Активировать бусты' }
  ];
  function _renderSettings(){
    var body = document.getElementById('lm-set-body'); if (!body) return;
    var rows = LM_CONFIRM_LIST.map(function(it){
      var on = _cfEnabled(it.key);
      return '<div class="lm-set-row"><span class="lm-set-lbl">'+it.ico+' '+_esc(it.label)+'</span><label class="lm-set-sw"><input type="checkbox" data-cf="'+_esc(it.key)+'"'+(on?' checked':'')+'><span></span></label></div>';
    }).join('');
    var layout = '<div class="lm-set-sec-title">🧭 Расположение кнопок</div>'+
      '<div class="lm-set-hint">По умолчанию кнопки «Вид», «Карта» и таймер — на самой карте. Можно вернуть их как было раньше: кнопки — в шапку блока карты, таймер — в строку статистики.</div>'+
      '<div class="lm-set-row"><span class="lm-set-lbl">🔀 «Вид», «Карта» и таймер — как раньше</span><label class="lm-set-sw"><input type="checkbox" id="lm-set-btnhdr"'+(_lmBtnsInHeader()?' checked':'')+'><span></span></label></div>';
    body.innerHTML = layout+'<div class="lm-set-sec-title">✅ Окна подтверждения</div><div class="lm-set-hint">Включённое действие сначала спросит подтверждение. Выключи то, где вопрос не нужен — действие выполнится сразу, одним кликом.</div>'+rows;
    var _bh = document.getElementById('lm-set-btnhdr');
    if (_bh) _bh.addEventListener('change', function(){ window.lmLS.set(LS_BTNS_HEADER, this.checked ? '1' : '0'); _lmApplyBtnLayout(); });
    Array.prototype.forEach.call(body.querySelectorAll('input[data-cf]'), function(inp){
      inp.addEventListener('change', function(){ var c=_cfLoad(); c[this.getAttribute('data-cf')]=this.checked; _cfSave(c); });
    });
  }
  function showSettings(){
    var back = document.getElementById('lm-set-back');
    if (!back) {
      back = document.createElement('div'); back.id='lm-set-back';
      back.innerHTML = '<div id="lm-set-box"><div id="lm-set-head"><b>⚙ Настройки скрипта</b><button id="lm-set-close">✕</button></div><div id="lm-set-body"></div></div>';
      document.body.appendChild(back);
      back.addEventListener('click', function(e){ if (e.target===back) back.classList.remove('on'); });
      back.querySelector('#lm-set-close').addEventListener('click', function(){ back.classList.remove('on'); });
    }
    _renderSettings();
    back.classList.add('on');
  }
  window._lmShowSettings = showSettings;
  var _pDownX = 0, _pDownY = 0, _touchMoved = false;
  function _lmPencilAt(cx, cy) {
    if (!Number.isFinite(cx) || !Number.isFinite(cy)) return;
    if (_lmPencil === 'remove') {
      var ri = _routeIdx(cx, cy);
      if (ri >= 0) { _lmRoute = _lmRoute.slice(0, ri); _routeSave(); _routeSyncToServer(); if (fmOpen) drawFull(); patchSiteMap(); if (window._lmPencilCount) window._lmPencilCount(); }
      return;
    }
    if (_lmPencil !== 'add') return;
    if (!_lmRoute.length) {
      var cur = curPos();
      if (!cur || !Number.isFinite(Number(cur.x))) { _lmMsg('Позиция неизвестна', ''); return; }
      if (cx === cur.x && cy === cur.y) return;
      if (!_adjacent({x:cx,y:cy}, {x:Number(cur.x),y:Number(cur.y)})) { _lmMsg('Начните с соседней клетки', 'рядом с вашей позицией'); return; }
      if (cy === Number(cur.y) + 1) { _lmMsg('Вниз ходить нельзя', 'только вверх / влево / вправо'); return; }
    } else {
      var last = _lmRoute[_lmRoute.length - 1];
      if (cx === last.x && cy === last.y) return;
      if (_routeIdx(cx, cy) >= 0) { _lmMsg('Клетка уже в маршруте', ''); return; }
      if (!_adjacent({x:cx,y:cy}, last)) { _lmMsg('Только соседняя клетка', 'вверх / влево / вправо'); return; }
      if (cy === last.y + 1) { _lmMsg('Вниз ходить нельзя', 'только вверх / влево / вправо'); return; }
    }
    _lmRoute.push({ x: cx, y: cy });
    _routeSave(); _routeSyncToServer(); if (fmOpen) drawFull(); patchSiteMap(); if (window._lmPencilCount) window._lmPencilCount();
  }
  function _lmPencilFromPoint(clientX, clientY) {
    if (!elMbody) return;
    var rect = elMbody.getBoundingClientRect();
    var cs = CELL_SIZE * fmScale;
    _lmPencilAt(Math.floor((clientX - rect.left - fmX) / cs), Math.floor((clientY - rect.top - fmY) / cs));
  }
  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);
      cell.classList.remove('lm-emission-cell');
      var emi = cell.querySelector('.lm-emission-ico');
      if (emi) emi.parentNode.removeChild(emi);
      cell.classList.remove('lm-route-cell');
      var rar = cell.querySelector('.lm-route-tunnel');
      if (rar) rar.parentNode.removeChild(rar);
    });
  }

  function removeCleanView() { patchSiteMapDelayed(); if (window._lmApplyEmissionCells) window._lmApplyEmissionCells(); }

  function patchSiteMap() {
    _routeTrimToCurrent();
    if (cleanViewMode) return;
    if (_lmIsBlocked()) { try { applyCleanView(); } catch(e) {} 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) {
      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 _rIdx = _routeIdx(lx, ly);
      if (_rIdx >= 0) {
        cell.classList.add('lm-route-cell');
        var _dirs = {}, _nb = [];
        if (_rIdx > 0) _nb.push(_lmRoute[_rIdx - 1]);
        else if (cur && Number.isFinite(Number(cur.x)) && _adjacent({x:lx,y:ly}, {x:Number(cur.x),y:Number(cur.y)})) _nb.push({x:Number(cur.x),y:Number(cur.y)});
        if (_lmRoute[_rIdx + 1]) _nb.push(_lmRoute[_rIdx + 1]);
        _nb.forEach(function(n){ if (n.y < ly) _dirs.up=1; else if (n.y > ly) _dirs.down=1; else if (n.x > lx) _dirs.right=1; else if (n.x < lx) _dirs.left=1; });
        var _tun = cell.querySelector('.lm-route-tunnel');
        if (!_tun) { _tun = document.createElement('span'); _tun.className = 'lm-route-tunnel'; cell.appendChild(_tun); }
        var _h = '';
        ['up','down','left','right'].forEach(function(dd){ if (_dirs[dd]) _h += '<i class="lm-tb lm-tb-'+dd+'"></i>'; });
        _h += '<i class="lm-tb-c'+(_lmRoute[_rIdx+1]?'':' end')+'"></i>';
        _tun.innerHTML = _h;
      } else {
        cell.classList.remove('lm-route-cell');
        var _ot = cell.querySelector('.lm-route-tunnel');
        if (_ot) _ot.parentNode.removeChild(_ot);
      }
      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 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;
  var _emissionCheckSnap = 0;
  var _emActiveEndAt = 0;
  var _emActiveSnap = 0;

  function _startEmissionTimer() {
    if (_emissionTimer) return;
    _emissionTimer = setInterval(function() { updateEmissionInfo(); _lmSyncArenaTimer(); }, 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 sl = Number(em.seconds_left);
      if (!Number.isFinite(sl) || sl <= 0) {
        sl = (em.active_until ? em.active_until : (em.last_start_at + EMISSION_DURATION_SEC)) - now;
      }
      if (!_emActiveEndAt || Math.abs(sl - _emActiveSnap) > 5) { _emActiveEndAt = now + sl; _emActiveSnap = sl; }
      var left = Math.round(_emActiveEndAt - 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 = '🌩';
      _emActiveEndAt = 0; _emActiveSnap = 0;

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

      if (nextCheck > 0) {

        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; }
      else if (LM_RESULT_EV[ev] && room.base_type && room.base_type !== 'unknown') { ev = room.base_type; }
      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() {
    if (_lmIsBlocked()) { cloudMap = {}; _cloudMapCount = 0; invalidateRoomsCache(); try { idbReplaceRooms([], 0); } catch(e) {} return; }
    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++;
        if(d&&d.blocked)return {rooms:[],version:0,pages:page,blocked:true};
        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',headers:{'X-Player':window.visitor_name||''}}).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.blocked)return idbReplaceRooms([],0).then(function(){return {mode:'blocked',version:0,count:0,pages:d.pages};});
        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]||{};
      if (remote.blocked) { _lmSetBlocked(true); return idbReplaceRooms([],0).then(function(){ return {mode:'blocked',version:0,count:0}; }); }
      _lmSetBlocked(false);
      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');
          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*.07); ctx.globalAlpha=.75;
      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.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.08);
      ctx.setLineDash([cs*0.15, cs*0.1]);
      ctx.globalAlpha = 0.85;
      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.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(200,150,255,0.98)' : 'rgba(255,220,120,0.95)';
      ctx.textAlign = 'left'; ctx.textBaseline = 'bottom';
      ctx.fillText(isVarRoom ? '~' : '?', px + 2, py + cs - 1);
    }
  }

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

  var LS_FMVIEW = 'lm_fullmap_view' + _lmUserSuffix;
  var _lmFmView = null;
  function _lmSaveFmView() {
    if (!Number.isFinite(fmX) || !Number.isFinite(fmY) || !Number.isFinite(fmScale)) return;
    _lmFmView = { x: fmX, y: fmY, s: fmScale };
    try { window.lmLS.set(LS_FMVIEW, JSON.stringify(_lmFmView)); } catch (e) {}
  }
  function _lmLoadFmView() {
    if (_lmFmView) return _lmFmView;
    try {
      var o = JSON.parse(window.lmLS.get(LS_FMVIEW) || 'null');
      if (o && Number.isFinite(o.x) && Number.isFinite(o.y) && Number.isFinite(o.s)) { _lmFmView = o; return o; }
    } catch (e) {}
    return null;
  }
  function openFull(skipCenter) {
    fmOpen=true;
    document.getElementById('lm-modal').classList.add('on');
    updateEmissionInfo();
    if (!skipCenter) {
      var sv = _lmLoadFmView();
      requestAnimationFrame(function(){
        if (!fmOpen) return;
        if (sv) { fmScale = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, sv.s)); fmX = sv.x; fmY = sv.y; drawFull(); }
        else fitReadableMap();
      });
    }
  }
  function closeFull() { _lmSaveFmView(); 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;
    ctx.beginPath();
    for (var gx=((fmX%cs)+cs)%cs; gx<elFc.width; gx+=cs) { ctx.moveTo(gx,0); ctx.lineTo(gx,elFc.height); }
    for (var gy=((fmY%cs)+cs)%cs; gy<elFc.height; gy+=cs) { ctx.moveTo(0,gy); ctx.lineTo(elFc.width,gy); }
    ctx.stroke();
    var myPathSet = {};
    if (showMyPath) {
      if (window._lmMyPathData) {
        myPathSet = window._lmMyPathData;
      }

      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]);

      var _emMark = (room.emissionEv && room.emissionEv !== room.event) || room.emissionVisited || _emCell;
      if (room.event === 'unknown' && (room.emissionEv || room.emissionVisited || _emCell) && cs >= 4 && !dimmed) {
        ctx.save();
        ctx.fillStyle = 'rgba(180,40,40,0.2)';
        ctx.fillRect(px, py, cs, cs);
        ctx.restore();
      }
      if (_emMark && cs >= 12 && !dimmed) {
        ctx.save();
        ctx.font = (cs * 0.26) + 'px sans-serif';
        ctx.fillStyle = 'rgba(255,215,70,0.95)';
        ctx.textAlign = 'right';
        ctx.textBaseline = 'bottom';
        ctx.fillText('⚡', px + cs - cs * 0.08, py + cs - cs * 0.06);
        ctx.restore();
      }
    }
    if (showMyPath) {

      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 (_lmRoute.length) {
      var _rc = curPos();
      ctx.save();
      ctx.strokeStyle = 'rgba(60,240,190,.85)';
      ctx.lineWidth = Math.max(2, cs * 0.14);
      ctx.lineJoin = 'round'; ctx.lineCap = 'round';
      ctx.beginPath();
      if (_rc && Number.isFinite(Number(_rc.x)) && _adjacent({x:Number(_rc.x),y:Number(_rc.y)}, _lmRoute[0])) ctx.moveTo(fmX + _rc.x * cs + cs / 2, fmY + _rc.y * cs + cs / 2);
      else ctx.moveTo(fmX + _lmRoute[0].x * cs + cs / 2, fmY + _lmRoute[0].y * cs + cs / 2);
      for (var _ri = 0; _ri < _lmRoute.length; _ri++) ctx.lineTo(fmX + _lmRoute[_ri].x * cs + cs / 2, fmY + _lmRoute[_ri].y * cs + cs / 2);
      ctx.stroke();
      ctx.strokeStyle = 'rgba(60,240,190,.95)';
      ctx.lineWidth = Math.max(1, cs * 0.08);
      for (var _rj = 0; _rj < _lmRoute.length; _rj++) {
        var _rp = _lmRoute[_rj], _rpx = fmX + _rp.x * cs, _rpy = fmY + _rp.y * cs;
        if (_rpx < -cs || _rpx > elFc.width + cs || _rpy < -cs || _rpy > elFc.height + cs) continue;
        ctx.strokeRect(_rpx + 1, _rpy + 1, cs - 2, cs - 2);
      }
      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:6px;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-setbtn{min-height:36px;padding:8px 12px;border:1px solid rgba(255,255,255,.12);border-radius:8px;background:rgba(255,255,255,.08);color:rgba(255,255,255,.72);font-size:15px;line-height:1;cursor:pointer;transition:.15s;}',
      '#lm-setbtn:hover{background:rgba(255,255,255,.16);color:#fff;transform:translateY(-1px);}',
      '#lm-set-back{position:fixed;inset:0;z-index:2000000;background:rgba(3,6,14,.72);display:none;align-items:center;justify-content:center;padding:16px;}',
      '#lm-set-back.on{display:flex;}',
      '#lm-set-box{width:min(520px,96vw);max-height:88vh;overflow:auto;background:#0d1322;border:1px solid #233;border-radius:16px;box-shadow:0 24px 70px rgba(0,0,0,.6);color:#dfe8f5;font:14px system-ui,sans-serif;}',
      '#lm-set-head{display:flex;align-items:center;gap:10px;padding:16px 18px;border-bottom:1px solid #1c2740;position:sticky;top:0;background:#0d1322;}',
      '#lm-set-head b{font-size:16px;color:#9fc5ff;flex:1;}',
      '#lm-set-close{width:30px;height:30px;border:none;border-radius:8px;background:#c0392b;color:#fff;font-size:17px;cursor:pointer;}',
      '#lm-set-body{padding:16px 18px;}',
      '.lm-set-sec-title{font-size:13px;font-weight:800;color:#cfe;margin:4px 0 6px;}',
      '.lm-set-hint{font-size:12px;color:rgba(226,232,240,.6);margin-bottom:12px;}',
      '.lm-set-row{display:flex;align-items:center;gap:10px;justify-content:space-between;padding:9px 11px;background:#111a2e;border:1px solid #1c2740;border-radius:10px;margin-bottom:7px;}',
      '.lm-set-row .lm-set-lbl{flex:1;font-size:13px;line-height:1.35;}',
      '.lm-set-sw{position:relative;width:44px;height:24px;flex-shrink:0;cursor:pointer;}',
      '.lm-set-sw input{opacity:0;width:0;height:0;}',
      '.lm-set-sw span{position:absolute;inset:0;background:rgba(255,255,255,.15);border-radius:24px;transition:.2s;}',
      '.lm-set-sw span:before{content:"";position:absolute;height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.2s;}',
      '.lm-set-sw input:checked + span{background:#12b891;}',
      '.lm-set-sw input:checked + span:before{transform:translateX(20px);}',
      '#lm-anbtn{min-height:36px;padding:8px 14px;border:1px solid rgba(120,180,255,.3);border-radius:8px;background:rgba(90,150,255,.12);color:#9fc5ff;font-size:11px;font-weight:800;cursor:pointer;transition:.15s;white-space:nowrap;}',
      '#lm-anbtn:hover{background:rgba(90,150,255,.22);color:#fff;transform:translateY(-1px);}',
      '#lm-msgbtn{position:relative;min-height:36px;padding:8px 12px;border:1px solid rgba(120,180,255,.3);border-radius:8px;background:rgba(90,150,255,.12);color:#9fc5ff;font-size:15px;line-height:1;font-weight:800;cursor:pointer;transition:.15s;white-space:nowrap;}',
      '#lm-msgbtn:hover{background:rgba(90,150,255,.22);color:#fff;transform:translateY(-1px);}',
      '#lm-msgbtn.has-unread{border-color:#ff3b3b;color:#fff;background:rgba(255,59,59,.18);}',
      '#lm-msgbadge{display:none;position:absolute;top:-7px;right:-7px;min-width:18px;height:18px;padding:0 4px;border-radius:9px;background:#ff2d2d;color:#fff;font-size:10px;font-weight:900;line-height:18px;text-align:center;box-shadow:0 0 8px rgba(255,45,45,.9);}',
      '#lm-msgbadge.show{display:block;}',
      '#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:radial-gradient(circle at 50% 30%,#12141f 0%,#0a0b11 75%);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:radial-gradient(circle at 50% 22%,rgba(74,90,199,.07),transparent 50%);opacity:.9;}',
      '#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:linear-gradient(180deg,rgba(30,33,52,.92),rgba(22,24,40,.88));border-bottom:1px solid rgba(255,255,255,.08);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:#4a5ac7;box-shadow:0 0 14px rgba(74,90,199,.85);flex:0 0 auto;animation:lm-pulse 2s infinite;}',
      '#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-bpencil,.lm-pctl{min-height:36px;padding:8px 12px;border-radius:8px;font-size:11px;font-weight:800;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:6px;white-space:nowrap;border:1px solid rgba(60,240,190,.4);background:rgba(60,240,190,.12);color:#7ff0d0;transition:.14s;}',
      '#lm-bpencil:hover,.lm-pctl:hover{background:rgba(60,240,190,.24);color:#fff;transform:translateY(-1px);}',
      '#lm-bpencil.active,.lm-pctl.active{background:#12b891;color:#fff;border-color:#3ef0c0;}',
      '.lm-pctl{padding:9px 12px;}',
      '#lm-pencil-panel{display:none;position:relative;z-index:4;align-items:center;flex-wrap:wrap;gap:8px;padding:10px 14px;background:linear-gradient(180deg,rgba(24,40,44,.96),rgba(18,30,34,.96));border-bottom:1px solid rgba(60,240,190,.3);flex-shrink:0;}',
      '#lm-pencil-panel.on{display:flex;}',
      '.lm-pp-title{font-size:13px;font-weight:900;color:#7ff0d0;white-space:nowrap;}',
      '.lm-pp-hint{font-size:11px;color:rgba(226,232,240,.55);}',
      '.lm-pp-count{font-size:12px;font-weight:800;color:#cfe;background:rgba(60,240,190,.15);border:1px solid rgba(60,240,190,.3);border-radius:8px;padding:4px 9px;white-space:nowrap;}',
      '.lm-pctl.danger{border-color:rgba(255,90,90,.5);background:rgba(255,90,90,.14);color:#ff9b9b;}',
      '.lm-pctl.danger:hover{background:rgba(255,90,90,.26);color:#fff;}',
      '.lm-pctl.done{border-color:rgba(120,160,255,.5);background:rgba(90,150,255,.16);color:#bcd3ff;}',
      '.lm-pctl.done:hover{background:rgba(90,150,255,.3);color:#fff;}',
      '.labyrinth-cell.lm-route-cell{position:relative!important;}',
      '.lm-route-tunnel{position:absolute;inset:0;pointer-events:none;z-index:2;}',
      '.lm-route-tunnel .lm-tb{position:absolute;background:rgba(60,240,190,.9);border-radius:3px;box-shadow:0 0 5px rgba(60,240,190,.75);}',
      '.lm-tb-up{left:50%;top:-2px;width:22%;height:56%;transform:translateX(-50%);}',
      '.lm-tb-down{left:50%;bottom:-2px;width:22%;height:56%;transform:translateX(-50%);}',
      '.lm-tb-left{top:50%;left:-2px;height:22%;width:56%;transform:translateY(-50%);}',
      '.lm-tb-right{top:50%;right:-2px;height:22%;width:56%;transform:translateY(-50%);}',
      '.lm-route-tunnel .lm-tb-c{position:absolute;left:50%;top:50%;width:22%;height:22%;transform:translate(-50%,-50%);background:rgba(60,240,190,.95);border-radius:50%;box-shadow:0 0 5px rgba(60,240,190,.8);}',
      '.lm-route-tunnel .lm-tb-c.end{width:40%;height:40%;background:#7cffdd;box-shadow:0 0 9px rgba(60,240,190,.95);}',
      '.labyrinth__personal-mine.lm-mine-slim{background:#171a28!important;border:1px solid rgba(255,255,255,.06)!important;border-radius:16px!important;}',
      '.labyrinth__personal-mine.lm-mine-slim .labyrinth__personal-mine-text{display:none!important;}',
      '.labyrinth__personal-mine.lm-mine-slim .labyrinth__personal-mine-head{margin-bottom:8px!important;}',
      '.labyrinth__personal-mine.lm-mine-slim .labyrinth__personal-mine-stats{margin:8px 0!important;}',
      '.labyrinth__personal-mine.lm-mine-slim .labyrinth__personal-mine-progress{margin:8px 0!important;}',
      '.lm-mine-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;align-items:start;}',
      '.lm-mine-col{display:flex;flex-direction:column;gap:5px;min-width:0;}',
      '.lm-mine-col .button{width:100%!important;margin:0!important;}',
      '.lm-mine-col .labyrinth__personal-mine-upgrade,.lm-mine-col #labyrinthPersonalMineProgressTimer{font-size:11px!important;line-height:1.35!important;color:#d4e0f2!important;opacity:1!important;text-align:center;margin:0!important;background:rgba(255,255,255,.07)!important;border:1px solid rgba(255,255,255,.11)!important;border-radius:8px!important;padding:6px 8px!important;}',
      '.labyrinth__personal-mine.lm-mine-slim #labyrinthPersonalMineTitle{display:none!important;}',
      '.labyrinth__personal-mine.lm-mine-slim .labyrinth__personal-mine-label{font-size:21px!important;font-weight:900!important;color:#fff!important;text-transform:none!important;letter-spacing:.01em!important;opacity:1!important;}',
      '.labyrinth__personal-mine.lm-mine-slim .labyrinth__personal-mine-icon{display:none!important;}',
      '#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 15px;gap:7px;border-radius:8px;background:linear-gradient(135deg,#4a5ac7,#3a4ab0);color:#fff;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:none;white-space:nowrap;box-shadow:0 2px 12px rgba(74,90,199,.4);transition:.15s;}',
      '#lm-brefresh:hover{background:linear-gradient(135deg,#5b6dd8,#4a5ac7);transform:translateY(-1px);}',
      '#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:linear-gradient(0deg,rgba(30,33,52,.94),rgba(22,24,40,.9));border-top:1px solid rgba(255,255,255,.08);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:14px;right:10px;flex-direction:row;}#lm-info{left:10px;right:108px;bottom:14px;transform:none;max-width:none;text-align:left;line-height:44px;padding-top:0;padding-bottom:0;height:44px;box-sizing:border-box;}.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;}',
      '#lm-gamestats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:0 0 4px 0;padding:14px;background:#171a28;border:1px solid rgba(255,255,255,.06);border-radius:16px;}',
      '.labyrinth__top-tabs{background:#171a28!important;border:1px solid rgba(255,255,255,.06)!important;border-radius:16px!important;padding:8px!important;margin-bottom:10px!important;}',
      '.labyrinth__top-tabs .ncard__tabs{width:100%!important;justify-content:stretch!important;}',
      '.labyrinth__top-tabs .ncard__tabs-btns{width:100%!important;flex-wrap:nowrap!important;gap:6px!important;}',
      '.labyrinth__top-tabs .ncard__tabs-btn{flex:1 1 0!important;min-width:0!important;justify-content:center!important;text-align:center!important;padding:9px 10px!important;background:linear-gradient(135deg,rgba(34,46,82,.5),rgba(16,22,42,.55))!important;border:1px solid rgba(120,160,255,.16)!important;border-radius:10px!important;color:#cdd9ee!important;font-weight:700!important;font-size:12px!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:border-color .15s,transform .15s,background .15s!important;}',
      '.labyrinth__top-tabs .ncard__tabs-btn:hover{border-color:rgba(120,160,255,.42)!important;color:#fff!important;transform:translateY(-1px);}',
      '.labyrinth__top-tabs .ncard__tabs-btn.is-active{background:linear-gradient(135deg,#4a5ac7,#3a4ab0)!important;border-color:#5b6dd8!important;color:#fff!important;box-shadow:0 2px 12px rgba(74,90,199,.35)!important;}',
      '.labyrinth__top-tabs .labyrinth-music-switch{order:-1!important;flex:0 0 auto!important;width:42px;height:38px;border-radius:10px!important;background:rgba(34,46,82,.55)!important;border:2px solid rgba(120,160,255,.55)!important;color:#cdd9ee!important;font-size:15px!important;}',
      '.labyrinth__top-tabs .labyrinth-music-switch.is-off{border-color:#ff4d4d!important;color:#ff5b5b!important;background:rgba(255,60,60,.14)!important;}',
      '@media(max-width:760px){.labyrinth__top-tabs .ncard__tabs-btns{flex-wrap:wrap!important;}.labyrinth__top-tabs .ncard__tabs-btn{flex:1 1 auto!important;font-size:11px!important;padding:8px!important;}}',
      '#labyrinthBoosts{background:#171a28!important;border:1px solid rgba(255,255,255,.06)!important;border-radius:16px!important;padding:14px 16px!important;margin-bottom:10px!important;}',
      '#labyrinthBoosts .labyrinth__boosts-title{display:none!important;}',
      '.labyrinth__body{margin-top:0!important;}',
      '#labyrinthBoosts .labyrinth__boost-btn{background:linear-gradient(135deg,rgba(34,46,82,.5),rgba(16,22,42,.55))!important;border:1px solid rgba(120,160,255,.16)!important;border-radius:14px!important;box-shadow:0 2px 10px rgba(0,0,0,.2)!important;transition:border-color .15s,transform .15s!important;}',
      '#labyrinthBoosts .labyrinth__boost-btn:not([disabled]):hover{border-color:rgba(120,160,255,.42)!important;transform:translateY(-1px);}',
      '#lm-gamestats .labyrinth__stat{background:linear-gradient(135deg,rgba(34,46,82,.55),rgba(16,22,42,.6))!important;border:1px solid rgba(120,160,255,.18)!important;border-radius:14px!important;padding:13px 16px!important;display:flex!important;flex-direction:column!important;gap:5px!important;box-shadow:0 3px 14px rgba(0,0,0,.28)!important;margin:0!important;min-width:0!important;cursor:default!important;transform:none!important;}',
      '#lm-gamestats .labyrinth__stat:hover{border-color:rgba(120,160,255,.18)!important;transform:none!important;background:linear-gradient(135deg,rgba(34,46,82,.55),rgba(16,22,42,.6))!important;box-shadow:0 3px 14px rgba(0,0,0,.28)!important;}',
      '#lm-gamestats .labyrinth__stat-label{font-size:10px!important;letter-spacing:.07em!important;text-transform:uppercase!important;color:#8093b6!important;font-weight:800!important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}',
      '#lm-gamestats .labyrinth__stat-value{font-size:20px!important;font-weight:800!important;color:#eaf1ff!important;line-height:1.15!important;}',
      '.labyrinth__header.lm-hdr-stretched .labyrinth__title-block{flex:1 1 auto!important;max-width:none!important;}',
      '.labyrinth__header.lm-hdr-stretched .labyrinth__subtitle{max-width:none!important;}',
      '.labyrinth__header.lm-hdr-stretched{display:none!important;}',
      '@media (max-width:640px){#lm-gamestats{grid-template-columns:repeat(2,minmax(0,1fr));}#lm-gamestats .labyrinth__stat-value{font-size:18px!important;}}',
      '#lm-topdeck{display:grid;grid-template-columns:minmax(0,1fr) 420px;gap:20px;align-items:stretch;margin:24px 24px 20px;padding:18px;border:1px solid rgba(255,255,255,.1);border-radius:22px;background:linear-gradient(135deg,rgba(24,27,44,.55),rgba(14,16,27,.6));box-shadow:0 10px 34px rgba(0,0,0,.28);}',
      '#lm-topdeck .lm-td-left{display:flex;flex-direction:column;gap:18px;min-width:0;}',
      '#lm-topdeck .lm-td-right{display:flex;flex-direction:column;min-width:0;}',
      '#lm-topdeck .lm-td-left>#lm-wrap{margin-bottom:0;}',
      '#lm-topdeck .lm-td-left>#lm-gamestats{margin:0;flex:1 1 auto;align-content:start;}',
      '#lm-topdeck .lm-td-right>#labyrinthPersonalMine{height:100%;box-sizing:border-box;margin:0;border-radius:16px;}',
      '#lm-topdeck .lm-td-left>#lm-wrap{border-radius:16px;}',
      '#lm-mine-ph{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:14px;height:100%;min-height:220px;box-sizing:border-box;padding:30px 24px;border-radius:16px;background:radial-gradient(circle at top right,rgba(255,196,87,.10),transparent 46%),linear-gradient(135deg,rgba(30,33,52,.9),rgba(16,18,31,.92));border:1px dashed rgba(255,255,255,.16);}',
      '#lm-mine-ph .lm-mph-ico{width:66px;height:66px;border-radius:20px;display:flex;align-items:center;justify-content:center;font-size:32px;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.1);box-shadow:0 8px 22px rgba(0,0,0,.25);}',
      '#lm-mine-ph .lm-mph-title{font-size:18px;font-weight:800;color:#fff;}',
      '#lm-mine-ph .lm-mph-text{font-size:13px;line-height:1.55;color:rgba(255,255,255,.6);max-width:280px;}',
      '@media (max-width:1280px){#lm-topdeck{grid-template-columns:minmax(0,1fr) 360px;}}',
      '.animesss-labyrinth .labyrinth__body.lm-body-deck{margin:0 24px;padding:20px;gap:20px;border:1px solid rgba(255,255,255,.1);border-radius:22px;background:linear-gradient(135deg,rgba(24,27,44,.55),rgba(14,16,27,.6));box-shadow:0 10px 34px rgba(0,0,0,.28);}',
      '.animesss-labyrinth .labyrinth__body.lm-body-deck .labyrinth__tips{display:none!important;}',
      '#lm-botdeck{margin:20px 24px 0;padding:20px;border:1px solid rgba(255,255,255,.1);border-radius:22px;background:linear-gradient(135deg,rgba(24,27,44,.55),rgba(14,16,27,.6));box-shadow:0 10px 34px rgba(0,0,0,.28);}',
      '#lm-botdeck>.labyrinth-global-stats{margin:0!important;padding:0!important;border:none!important;background:transparent!important;box-shadow:none!important;}',
      '.animesss-labyrinth .animesss-chat.animesss-chat--labyrinth{margin:20px 24px 0!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:20px!important;background:linear-gradient(135deg,rgba(24,27,44,.72),rgba(14,16,27,.8))!important;box-shadow:0 10px 34px rgba(0,0,0,.28)!important;}',
      '.animesss-labyrinth .labyrinth-legend-full{margin:20px 24px 24px!important;padding:20px!important;border:1px solid rgba(255,255,255,.1)!important;border-radius:20px!important;background:linear-gradient(135deg,rgba(24,27,44,.55),rgba(14,16,27,.6))!important;box-shadow:0 10px 34px rgba(0,0,0,.28)!important;}',
      '.animesss-labyrinth .labyrinth-legend-full__title{color:#fff!important;font-weight:800!important;}',
      '.animesss-labyrinth .labyrinth-legend-full__grid>div{padding:7px 10px!important;border-radius:10px!important;background:rgba(255,255,255,.04)!important;border:1px solid rgba(255,255,255,.06)!important;color:rgba(255,255,255,.82)!important;}',
      '@media(max-width:640px){.animesss-labyrinth .animesss-chat.animesss-chat--labyrinth{margin-left:14px!important;margin-right:14px!important;}.animesss-labyrinth .labyrinth-legend-full{margin-left:14px!important;margin-right:14px!important;padding:14px!important;}}',
      '.labyrinth__arena.lm-arena-styled{background:linear-gradient(135deg,rgba(30,33,52,.96),rgba(16,18,31,.98))!important;padding-top:56px!important;}',
      '#lm-arena-bar{position:absolute;top:0;left:0;right:0;z-index:6;display:flex;align-items:center;justify-content:space-between;gap:10px;padding:11px 14px;box-sizing:border-box;}',
      '#lm-arena-bar .lm-ab-right{display:flex;align-items:center;gap:10px;}',
      '#lm-cleanbtn,#lm-pbtn{display:none;}',
      'html.lm-btns-header #lm-cleanbtn,html.lm-btns-header #lm-pbtn{display:inline-flex;align-items:center;}',
      'html.lm-btns-header #lm-arena-bar{display:none;}',
      'html.lm-btns-header .labyrinth__arena.lm-arena-styled{padding-top:14px!important;}',
      '#lm-arena-bar button{min-height:38px;padding:8px 15px;border-radius:10px;border:none;cursor:pointer;font-size:11px;font-weight:800;white-space:nowrap;transition:.15s;line-height:1;}',
      '#lm-ab-view{background:rgba(255,255,255,.08);color:rgba(255,255,255,.72);border:1px solid rgba(255,255,255,.14)!important;box-shadow:0 4px 16px rgba(0,0,0,.22);}',
      '#lm-ab-view:hover{background:rgba(255,255,255,.16);color:#fff;transform:translateY(-1px);}',
      '#lm-ab-view.active{background:rgba(255,214,107,.18);color:#ffd66b;border-color:rgba(255,214,107,.45)!important;}',
      '#lm-ab-map{background:linear-gradient(135deg,#b02a59,#8a1f45);color:#fff;box-shadow:0 4px 16px rgba(176,42,89,.4);}',
      '#lm-ab-map:hover{background:linear-gradient(135deg,#c03468,#b02a59);transform:translateY(-1px);}',
      '#lm-ab-timer{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:38px;min-width:82px;padding:6px 14px;border-radius:10px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);box-sizing:border-box;box-shadow:0 4px 16px rgba(0,0,0,.22);}',
      '#lm-ab-timer .lm-abt-val{font-size:15px;font-weight:800;color:#fff;line-height:1;white-space:nowrap;}',
      '#lm-ab-timer .lm-abt-lbl{font-size:8px;letter-spacing:.05em;text-transform:uppercase;color:rgba(255,255,255,.5);margin-top:3px;line-height:1;white-space:nowrap;}',
      '@media (max-width:640px){#lm-arena-bar{padding:9px 10px;}#lm-ab-timer{min-width:64px;padding:5px 9px;}.labyrinth__arena.lm-arena-styled{padding-top:52px!important;}}',
    ].join('\n');
    document.head.appendChild(style);
  }

  function _lmMoveGameStats(wrap, arenaEl) {
    try {
      var ids = ['labyrinthTodaySteps', 'labyrinthRemainingSteps', 'labyrinthTotalSteps', 'labyrinthBank'];
      var _st = document.getElementById('labyrinthStatusText');
      if (_st && _st.closest) { var _sc = _st.closest('.labyrinth__stat'); if (_sc) _sc.style.display = 'none'; }
      var bar = document.getElementById('lm-gamestats');
      if (!bar) {
        bar = document.createElement('div'); bar.id = 'lm-gamestats';
        if (arenaEl && arenaEl.parentNode) arenaEl.parentNode.insertBefore(bar, arenaEl);
        else if (wrap && wrap.parentNode) wrap.parentNode.insertBefore(bar, wrap.nextSibling);
        else return;
      }
      ids.forEach(function(id) {
        var span = document.getElementById(id);
        if (!span || !span.closest) return;
        var card = span.closest('.labyrinth__stat');
        if (card && card.parentNode !== bar) bar.appendChild(card);
      });
      var hdr = document.querySelector('.labyrinth__header');
      if (hdr) hdr.classList.add('lm-hdr-stretched');
      _lmPlaceTimerStat();
    } catch(e) {}
  }

  function _lmDeckDesktop(){ return window.innerWidth > 980; }
  function _lmMineVisible(){
    var m = document.getElementById('labyrinthPersonalMine');
    if (!m || m.style.display === 'none') return false;
    try { return window.getComputedStyle(m).display !== 'none'; } catch(e) { return true; }
  }
  function _lmDeckMineToggle(){
    var deck = document.getElementById('lm-topdeck'); if (!deck) return;
    var ph = document.getElementById('lm-mine-ph'); if (!ph) return;
    ph.style.display = _lmMineVisible() ? 'none' : 'flex';
  }
  function _lmBuildTopDeck() {
    try {
      var body = document.querySelector('.labyrinth__body');
      var left = document.querySelector('.labyrinth__left');
      var right = document.querySelector('.labyrinth__right');
      if (!body || !left || !right) return;
      var wrap = document.getElementById('lm-wrap');
      var stats = document.getElementById('lm-gamestats');
      var mine = document.getElementById('labyrinthPersonalMine');
      var deck = document.getElementById('lm-topdeck');
      if (!_lmDeckDesktop()) {
        if (deck) {
          var arena = left.querySelector('.labyrinth__arena');
          if (wrap) left.insertBefore(wrap, left.firstChild);
          if (stats) { if (wrap) left.insertBefore(stats, wrap.nextSibling); else if (arena) left.insertBefore(stats, arena); else left.appendChild(stats); }
          if (mine) right.insertBefore(mine, right.firstChild);
          if (deck.parentNode) deck.parentNode.removeChild(deck);
        }
        return;
      }
      if (!deck) {
        deck = document.createElement('div'); deck.id = 'lm-topdeck';
        deck.innerHTML = '<div class="lm-td-left"></div><div class="lm-td-right"></div>';
        if (body.parentNode) body.parentNode.insertBefore(deck, body);
      }
      var dl = deck.querySelector('.lm-td-left');
      var dr = deck.querySelector('.lm-td-right');
      if (wrap && wrap.parentNode !== dl) dl.appendChild(wrap);
      if (stats && stats.parentNode !== dl) dl.appendChild(stats);
      if (mine && mine.parentNode !== dr) dr.appendChild(mine);
      var ph = document.getElementById('lm-mine-ph');
      if (!ph) {
        ph = document.createElement('div'); ph.id = 'lm-mine-ph';
        ph.innerHTML = '<div class="lm-mph-ico">⛏</div><div class="lm-mph-title">Персональная шахта</div><div class="lm-mph-text">У тебя пока нет шахты. Найди её в лабиринте — и она появится здесь.</div>';
      }
      if (ph.parentNode !== dr) dr.appendChild(ph);
      _lmDeckMineToggle();
    } catch (e) {}
  }

  function _lmBuildBotDeck() {
    try {
      var body = document.querySelector('.labyrinth__body');
      var left = document.querySelector('.labyrinth__left');
      var right = document.querySelector('.labyrinth__right');
      var stats = document.querySelector('.labyrinth-global-stats');
      if (!body || !stats) return;
      var deck = document.getElementById('lm-botdeck');
      if (!_lmDeckDesktop()) {
        body.classList.remove('lm-body-deck');
        if (deck) {
          var tips = right && right.querySelector('.labyrinth__tips');
          if (right && tips) right.insertBefore(stats, tips.nextSibling);
          else if (right) right.appendChild(stats);
          else if (left) left.appendChild(stats);
          if (deck.parentNode) deck.parentNode.removeChild(deck);
        }
        return;
      }
      if (!deck) {
        deck = document.createElement('div'); deck.id = 'lm-botdeck';
        if (body.parentNode) body.parentNode.insertBefore(deck, body.nextSibling);
      }
      if (stats.parentNode !== deck) deck.appendChild(stats);
      body.classList.add('lm-body-deck');
    } catch (e) {}
  }

  var LS_BTNS_HEADER = 'lm_btns_header' + _lmUserSuffix;
  function _lmBtnsInHeader() { return window.lmLS.get(LS_BTNS_HEADER) === '1'; }
  function _lmPlaceTimerStat() {
    try {
      var span = document.getElementById('labyrinthTimerText');
      if (!span || !span.closest) return;
      var card = span.closest('.labyrinth__stat'); if (!card) return;
      var bar = document.getElementById('lm-gamestats');
      if (_lmBtnsInHeader()) {
        if (bar && card.parentNode !== bar) bar.appendChild(card);
      } else {
        var stats = document.querySelector('.labyrinth__header .labyrinth__stats');
        if (bar && card.parentNode === bar && stats) stats.appendChild(card);
      }
    } catch (e) {}
  }
  function _lmApplyBtnLayout() {
    try {
      document.documentElement.classList.toggle('lm-btns-header', _lmBtnsInHeader());
      _lmPlaceTimerStat();
      var h = document.getElementById('lm-cleanbtn'), a = document.getElementById('lm-ab-view');
      if (h) h.classList.toggle('active', cleanViewMode);
      if (a) a.classList.toggle('active', cleanViewMode);
    } catch (e) {}
  }

  function _lmSyncArenaTimer() {
    var t = document.getElementById('lm-ab-timer'); if (!t) return;
    var src = document.getElementById('labyrinthTimerText');
    var vv = t.querySelector('.lm-abt-val');
    if (vv) vv.textContent = (src && (src.textContent || '').trim()) || '00:00';
  }
  function _lmBuildArenaBar() {
    try {
      var arena = document.querySelector('.labyrinth__arena');
      if (!arena) return;
      arena.classList.add('lm-arena-styled');
      if (!document.getElementById('lm-arena-bar')) {
        var bar = document.createElement('div'); bar.id = 'lm-arena-bar';
        bar.innerHTML = '<button id="lm-ab-view" title="Скрыть/показать иконки скрипта на карте">👁 Вид</button>'+
          '<div class="lm-ab-right">'+
            '<div id="lm-ab-timer"><span class="lm-abt-val">00:00</span><span class="lm-abt-lbl">таймер</span></div>'+
            '<button id="lm-ab-map" title="Открыть полную карту">⛶ Карта</button>'+
          '</div>';
        arena.insertBefore(bar, arena.firstChild);
        var vbtn = document.getElementById('lm-ab-view');
        vbtn.classList.toggle('active', cleanViewMode);
        vbtn.addEventListener('click', function() {
          cleanViewMode = !cleanViewMode;
          if (cleanViewMode) applyCleanView(); else removeCleanView();
          _lmApplyBtnLayout();
        });
        document.getElementById('lm-ab-map').addEventListener('click', function() { openFull(); });
      }
      var tEl = document.getElementById('labyrinthTimerText');
      if (tEl && !window._lmTimerObs) {
        window._lmTimerObs = new MutationObserver(function() { _lmSyncArenaTimer(); });
        window._lmTimerObs.observe(tEl, { childList: true, characterData: true, subtree: true });
      }
      _lmSyncArenaTimer();
      _lmApplyBtnLayout();
    } catch (e) {}
  }

  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-msgbtn" title="Сообщения от админа лабиринта">💬<span id="lm-msgbadge"></span></button>'+
          '<button id="lm-anbtn" title="Статус скрипта и отправка отчёта о поломке">📊 Статус</button>'+
          '<button id="lm-cleanbtn" title="Скрыть/показать иконки скрипта на карте">👁 Вид</button>'+
          '<button id="lm-rbtn">🔄 Обновить</button>'+
          '<button id="lm-pbtn">⛶ Карта</button>'+
          '<button id="lm-setbtn" title="Настройки скрипта">⚙</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);

    _lmMoveGameStats(wrap, arenaEl);
    _lmBuildTopDeck(); _lmBuildArenaBar(); _lmBuildBotDeck();
    setTimeout(function(){ _lmMoveGameStats(wrap, arenaEl); _lmBuildTopDeck(); _lmBuildArenaBar(); _lmBuildBotDeck(); }, 800);
    setTimeout(function(){ _lmMoveGameStats(wrap, arenaEl); _lmBuildTopDeck(); _lmBuildArenaBar(); _lmBuildBotDeck(); }, 2500);
    try {
      var hdrEl = document.querySelector('.labyrinth__header');
      if (hdrEl && !window._lmStatsObs) {
        window._lmStatsObs = new MutationObserver(function(){ _lmMoveGameStats(wrap, arenaEl); _lmBuildTopDeck(); _lmBuildArenaBar(); _lmBuildBotDeck(); });
        window._lmStatsObs.observe(hdrEl, { childList: true, subtree: false });
      }
    } catch(e) {}
    try {
      if (!window._lmDeckResize) {
        window._lmDeckResize = true;
        window.addEventListener('resize', function(){ clearTimeout(window._lmDeckRT); window._lmDeckRT = setTimeout(function(){ _lmBuildTopDeck(); _lmBuildBotDeck(); }, 150); });
      }
    } catch(e) {}

    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-bpencil" title="Нарисовать свой маршрут">✏️ Путь</button>'+
          '<button id="lm-bf" title="Фильтр комнат">⊞ Фильтр</button>'+
        '</div>'+
      '</div>'+
      '<div id="lm-pencil-panel">'+
        '<span class="lm-pp-title">✏️ Маршрут:</span>'+
        '<button id="lm-padd" class="lm-pctl active">➕ Добавлять</button>'+
        '<button id="lm-prem" class="lm-pctl">➖ Убирать</button>'+
        '<span class="lm-pp-count" id="lm-pp-count">0</span>'+
        '<span style="flex:1;min-width:8px"></span>'+
        '<button id="lm-pclr" class="lm-pctl danger">🗑 Стереть</button>'+
        '<button id="lm-pdone" class="lm-pctl done">✓ Готово</button>'+
        '<div class="lm-pp-hint" style="flex-basis:100%">Кликай клетки на карте: сначала рядом с собой, потом соседние (вверх/влево/вправо)</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();
    });

    var _pBtn = document.getElementById('lm-pbtn');
    if (_pBtn) _pBtn.addEventListener('click', function(){ openFull(); });
    var _anBtn = document.getElementById('lm-anbtn');
    if (_anBtn) _anBtn.addEventListener('click', function(){ if (window._lmOpenReport) window._lmOpenReport('', null); });
    var _msgBtn = document.getElementById('lm-msgbtn');
    if (_msgBtn) _msgBtn.addEventListener('click', function(){ if (window._lmOpenMessages) window._lmOpenMessages(); });
    if (window._lmMsgPoll) window._lmMsgPoll();

    var _cleanBtn = document.getElementById('lm-cleanbtn');
    if (_cleanBtn) _cleanBtn.addEventListener('click', function() {
      cleanViewMode = !cleanViewMode;
      if (cleanViewMode) applyCleanView(); else removeCleanView();
      _lmApplyBtnLayout();
    });
    var _setBtn = document.getElementById('lm-setbtn');
    if (_setBtn) _setBtn.addEventListener('click', showSettings);
    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,FOCUS_ZOOM_NEAR); }, 50); }
      else { centerOn(0,0,FOCUS_ZOOM_NEAR); }
    });
    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,FOCUS_ZOOM_NEAR); }, 50); }
      else { centerOn(c.x,c.y,FOCUS_ZOOM_NEAR); }
    });
    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;
              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();
    });

    function _pencilCount(){ var el=document.getElementById('lm-pp-count'); if(el) el.textContent='Клеток: '+_lmRoute.length; }
    window._lmPencilCount = _pencilCount;
    function _pencilSub(mode){
      _lmPencil=mode;
      var a=document.getElementById('lm-padd'), r=document.getElementById('lm-prem');
      if(a) a.classList.toggle('active',mode==='add');
      if(r) r.classList.toggle('active',mode==='remove');
    }
    function _pencilOpen(open){
      var p=document.getElementById('lm-pencil-panel'), b=document.getElementById('lm-bpencil');
      if(p) p.classList.toggle('on',open);
      if(b) b.classList.toggle('active',open);
      if(open){ _pencilSub('add'); _pencilCount(); } else { _lmPencil='off'; }
      if(fmOpen) requestAnimationFrame(drawFull);
    }
    document.getElementById('lm-bpencil').addEventListener('click', function(){ _pencilOpen(_lmPencil==='off'); });
    document.getElementById('lm-padd').addEventListener('click', function(){ _pencilSub('add'); });
    document.getElementById('lm-prem').addEventListener('click', function(){ _pencilSub('remove'); });
    document.getElementById('lm-pdone').addEventListener('click', function(){ _pencilOpen(false); });
    document.getElementById('lm-pclr').addEventListener('click', function(){
      if(!_lmRoute.length){ _lmMsg('Маршрут пуст',''); return; }
      if(confirm('Стереть весь нарисованный маршрут?')){ _lmRoute=[]; _routeSave(); _routeSyncToServer(); if(fmOpen)drawFull(); patchSiteMap(); _pencilCount(); _lmMsg('Маршрут стёрт',''); }
    });
    var _ppEl = document.getElementById('lm-pencil-panel');
    if (_ppEl) ['wheel','touchmove'].forEach(function(ev){ _ppEl.addEventListener(ev, function(e){ e.stopPropagation(); }, { passive: true }); });
    function _inPencilPanel(t){ return !!(t && t.closest && t.closest('#lm-pencil-panel')); }

    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'); _pDownX=e.clientX; _pDownY=e.clientY; });
    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(e){ var _clk = drag && _lmPencil!=='off' && !_inPencilPanel(e.target) && Math.abs(e.clientX-_pDownX)<6 && Math.abs(e.clientY-_pDownY)<6; drag=false; elMbody.classList.remove('dr'); if(rafId){ cancelAnimationFrame(rafId); rafId=null; } if(_clk) _lmPencilFromPoint(e.clientX, e.clientY); });

    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; _touchMoved = false;
      } 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];
        if (Math.abs(t.clientX - tapStartX) > 6 || Math.abs(t.clientY - tapStartY) > 6) _touchMoved = true;
        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) {
        if (drag && !_touchMoved && _lmPencil !== 'off' && !_inPencilPanel(e.target)) _lmPencilFromPoint(tapStartX, tapStartY);
        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;
      if (!rafId) rafId=requestAnimationFrame(function(){ rafId=null; 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'&&t!==room.emissionEv; });
          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>';
          var mmEmLine = '';
          if (!isCur && room.emissionVisited && room.emissionEv && room.emissionEv !== room.event) {
            mmEmLine = '<div class="tt-alt" style="color:#ffcf4d">⚡ Во время выброса: '+ico(room.emissionEv)+' '+roomName(room.emissionEv)+'</div>';
          }
          var knownType = room.event && room.event !== 'unknown';
          var mmRealHdr = isCur ? '⚔ Ваша позиция' : (knownType ? ico(room.event)+' '+roomName(room.event) : '❔ Неизвестная комната');
          var mmRealLine = isCur ? '' : '<div class="tt-desc" style="color:#9fd6ff">Реальный тип: '+(knownType ? roomName(room.event) : 'не найдено')+'</div>';
          elTooltip.innerHTML='<b>'+mmRealHdr+'</b>'+
            mmRealLine+mmObjLine+mmEmLine+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, key, ico2, name, desc) {
      if (!btn) return;
      btn.addEventListener('click', function(e) {
        if (_roomActionConfirmed) return;
        if (!_cfEnabled(key)) 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'), 'place_trap', '🕸', 'Оставить ловушку', 'Ловушка остановит первого игрока, который войдёт сюда. Стоимость — 1 ход.');
    interceptRoomAction(document.getElementById('labyrinthPlaceGiftBtn'), 'place_gift', '🎁', 'Оставить подарок', 'Подарок сразу достанется следующему игроку. Стоимость — 1 ход.');
    interceptRoomAction(document.getElementById('labyrinthRobMineBtn'),   'rob_mine', '⚔', 'Ограбить шахту',   'Вы заберёте добычу из шахты другого игрока.');
    interceptRoomAction(document.getElementById('labyrinthEchoRepeatBtn'), 'echo_repeat', '👣', 'Повторить путь', 'Ты повторишь путь другого игрока в этой комнате.');
    interceptRoomAction(document.getElementById('labyrinthClubWarSoftBtn'),   'raid_soft', '🏴', 'Лёгкий рейд',       'Небольшая атака на вражеский клуб.');
    interceptRoomAction(document.getElementById('labyrinthClubWarNormalBtn'), 'raid_normal', '⚔',  'Обычный штурм',     'Стандартная атака на вражеский клуб.');
    interceptRoomAction(document.getElementById('labyrinthClubWarFullBtn'),   'raid_full', '💥', 'Полное вторжение',  'Мощная атака на вражеский клуб. Требует больше ресурсов.');
    interceptRoomAction(document.getElementById('labyrinthHelpMineBtn'),  'help_mine', '🤝', 'Помочь со сбором', 'Вы поможете другому игроку собрать добычу быстрее.');

    var _mineConfirmed = false;
    var collectMineBtn = document.getElementById('labyrinthCollectMineBtn');
    if (collectMineBtn) {
      collectMineBtn.addEventListener('click', function(e) {
        if (_mineConfirmed) return;
        if (!_cfEnabled('collect_mine')) 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;
        if (!_cfEnabled('upgrade_mine')) 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;
        if (!_cfEnabled('buy_attempt')) 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;
        if (!_cfEnabled('boost')) 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'&&t!==room.emissionEv; });
        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 knownType2 = room.event && room.event !== 'unknown';
        var realHdr2 = isCur ? '⚔ Ваша позиция' : (knownType2 ? ico(room.event)+' '+roomName(room.event) : '❔ Неизвестная комната');
        var realLine2 = isCur ? '' : '<div class="tt-desc" style="color:#9fd6ff">Реальный тип: '+(knownType2 ? roomName(room.event) : 'не найдено')+'</div>';
        var emLine2 = (!isCur && room.emissionVisited && room.emissionEv && room.emissionEv !== room.event)
          ? '<div class="tt-alt" style="color:#ffcf4d">⚡ Во время выброса: '+ico(room.emissionEv)+' '+roomName(room.emissionEv)+'</div>' : '';
        elTooltip.innerHTML = '<b>'+realHdr2+'</b>'+
          realLine2+objLine2+emLine2+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) { if (_lmPencil !== 'off') return; showTooltipAt(e.clientX, e.clientY); });
    elMbody.addEventListener('touchend', function(e) {
      if (_lmPencil !== 'off') return;
      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;
              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(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('Карта включена');
    _routeLoad();
    _routeRestoreFromServer();
    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;
    var LS_MINE_FILL_AT   = 'lm_mine_fill_at'        + _lmUserSuffix;
    var LS_MINE_RETRY_AT  = 'lm_mine_retry_at'       + _lmUserSuffix;
    var LS_MINE_FAILS     = 'lm_mine_fails'          + _lmUserSuffix;
    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; }

      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 && !d.error) {
          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 && d.error ? d.error : d); }
      })
      .catch(function(e) { onDone(false, e.message); });
    }

    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;

      var _retryAt = parseInt(window.lmLS.get(LS_MINE_RETRY_AT) || '0', 10);
      if (_retryAt && Date.now() < _retryAt) return;

      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;

      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);
        if (!mine) return;
        var threshold = getThreshold();
        if (mine.can_collect && (mine.storage_progress >= threshold || mine.is_storage_full)) {
          collectMine(function(ok, data) {
            if (ok) {

              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 {

              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 {

          window.lmLS.remove(LS_MINE_FAILS);

          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() {

      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;

      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;

      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());
      }

      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));
      }

      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 _fmtRuTime(s) {
      var h = 0, m = 0, sec = 0, mm;
      mm = s.match(/(\d+)\s*ч/);   if (mm) h = +mm[1];
      mm = s.match(/(\d+)\s*мин/); if (mm) m = +mm[1];
      mm = s.match(/(\d+)\s*сек/); if (mm) sec = +mm[1];
      if (!h && !m && !sec) return null;
      function p(n){ return (n < 10 ? '0' : '') + n; }
      var d = Math.floor(h / 24), hh = h % 24;
      return d > 0 ? (d + ' дн ' + p(hh) + ':' + p(m) + ':' + p(sec)) : (p(h) + ':' + p(m) + ':' + p(sec));
    }
    function _lmMineShorten() {
      var pt = document.getElementById('labyrinthPersonalMineProgressTimer');
      if (pt) { var tm = _fmtRuTime(pt.textContent); if (tm) { var w = 'Заполнение: ' + tm; if (pt.textContent !== w) pt.textContent = w; } }
      var ui = document.getElementById('labyrinthPersonalMineUpgradeInfo');
      if (ui) {
        var um = _fmtRuTime(ui.textContent);
        if (um) { var w2 = 'Улучшение: ' + um; if (ui.textContent !== w2) ui.textContent = w2; }
        else { var m2 = ui.textContent.replace(/^\s*Следующее улучшение доступно\.\s*Цена:\s*/, 'Улучшить за '); if (m2 !== ui.textContent) ui.textContent = m2; }
      }
    }
    function _lmMineRestyle() {
      try {
        var mine = document.getElementById('labyrinthPersonalMine');
        if (!mine) return;
        mine.classList.add('lm-mine-slim');
        var _rp = mine.parentElement;
        if (_rp && /labyrinth__right/.test(_rp.className || '')) {
          if (window.innerWidth >= 900) {
            if (_rp.firstElementChild !== mine) _rp.insertBefore(mine, _rp.firstElementChild);
          } else if (_rp.firstElementChild === mine && _rp.children.length > 1) {
            _rp.appendChild(mine);
          }
        }
        var actions = mine.querySelector('.labyrinth__personal-mine-actions');
        var collect = document.getElementById('labyrinthCollectMineBtn');
        var upgrade = document.getElementById('labyrinthUpgradeMineBtn');
        var ptimer = document.getElementById('labyrinthPersonalMineProgressTimer');
        var uinfo = document.getElementById('labyrinthPersonalMineUpgradeInfo');
        if (actions && collect && upgrade && !mine.querySelector('.lm-mine-grid')) {
          var grid = document.createElement('div'); grid.className = 'lm-mine-grid';
          var L = document.createElement('div'); L.className = 'lm-mine-col';
          var R = document.createElement('div'); R.className = 'lm-mine-col';
          L.appendChild(collect); if (ptimer) L.appendChild(ptimer);
          R.appendChild(upgrade); if (uinfo) R.appendChild(uinfo);
          grid.appendChild(L); grid.appendChild(R);
          actions.insertBefore(grid, actions.firstChild);
        }
        _lmMineShorten();
      } catch(e) {}
    }
    function injectMineSettingsBtn() {
      function tryInject() {
        var mineEl = document.getElementById('labyrinthPersonalMine');
        if (!mineEl || mineEl.style.display === 'none' || window.getComputedStyle(mineEl).display === 'none') return;
        _lmMineRestyle();
        _lmBuildTopDeck();
        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) {
        try { _lmMineShorten(); } catch(e) {}
        try { _lmDeckMineToggle(); } catch(e) {}
        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, characterData: 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() {});
  }

  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 (cleanViewMode) return;
    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;

    _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;

          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;
        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);

  if (IS_LABYRINTH) {
    setTimeout(sendAccHistoryToVPS, 9000);
    setInterval(sendAccHistoryToVPS, 15 * 60 * 1000);
  }

  (function loadMineSettingsFromServer() {
    var u = window.visitor_name || '';
    if (!u) return;

    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;
    }
  };

})();

(function() {
  'use strict';
  if (window._lmReportInit) return; window._lmReportInit = true;
  if (!/animesss\.(com|tv)$/.test(location.hostname)) return;

  var REPORT_API = 'https://labmap.duckdns.org/api';
  var VER = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? GM_info.script.version : '8.4.3';
  function nick() { return window.visitor_name || 'unknown'; }
  function onLab() { return /\/labyrinth(\/|$)/.test(location.pathname || ''); }

  var KNOWN = {};
  ('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 ' +
   'puzzle_result room_trap_result room_gift_result mini_boss_hit mini_boss_killed mini_boss_result ' +
   'hard_boss_hit hard_boss_killed boss_hit boss_killed personal_mine_upgrade resonance_room ' +
   'rl mx mr success error ok').split(' ').forEach(function(k){ KNOWN[k] = 1; });

  function postReport(payload, timeoutMs) {
    var ctrl = new AbortController();
    var t = setTimeout(function(){ ctrl.abort(); }, timeoutMs || 30000);

    return fetch(REPORT_API + '/report', {
      method: 'POST', cache: 'no-store', credentials: 'omit', signal: ctrl.signal,
      body: JSON.stringify(payload)
    }).then(function(r){ clearTimeout(t); if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .catch(function(e){ clearTimeout(t); throw e; });
  }

  function _slim(resp) {
    try {
      var c = {}; for (var k in resp) { if (k === 'mapData') continue;
        var v = resp[k];
        if (v && typeof v === 'object') { try { if (JSON.stringify(v).length > 4000) continue; } catch(e) { continue; } }
        c[k] = v;
      }
      if (resp.mapData && resp.mapData.steps) c._lastStep = resp.mapData.steps[resp.mapData.steps.length - 1] || null;
      return c;
    } catch(e) { return null; }
  }

  var _unknownSent = null;
  function _loadSent() {
    if (_unknownSent) return _unknownSent;
    _unknownSent = {};
    try { JSON.parse(window.lmLS.get('lm_unknown_reported') || '[]').forEach(function(t){ _unknownSent[t] = 1; }); } catch(e) {}
    return _unknownSent;
  }
  window._lmScanUnknown = function(resp) {
    if (!resp) return;
    var types = [];
    if (typeof resp.event === 'string') types.push(resp.event);
    try {
      var steps = resp.mapData && resp.mapData.steps;
      if (steps && steps.length) for (var i = Math.max(0, steps.length - 6); i < steps.length; i++) {
        if (steps[i] && typeof steps[i].event === 'string') types.push(steps[i].event);
      }
    } catch(e) {}
    var sent = _loadSent();
    var fresh = [];
    types.forEach(function(t){
      if (!t || KNOWN[t] || sent[t]) return;
      if (!/^[a-z][a-z0-9_]{1,39}$/.test(t)) return;
      if (/(_result|_hit|_killed|_escape|_reward|_back|_upgrade|_created|_collect)$/.test(t)) return;
      sent[t] = 1; fresh.push(t);
    });
    if (!fresh.length) return;
    try { window.lmLS.set('lm_unknown_reported', JSON.stringify(Object.keys(sent).slice(-200))); } catch(e) {}

    postReport({
      trigger: 'unknown_room', unknown_room: true,
      username: nick(), script_version: VER, url: location.href,
      x: (resp.x != null ? resp.x : null), y: (resp.y != null ? resp.y : null),
      user_message: 'AUTO: новые типы комнат: ' + fresh.join(', '),
      labyrinth_data: { new_types: fresh, event: resp.event || null, sample: _slim(resp) }
    }, 15000).catch(function(){});
  };

  var _lmLatestVer = null;
  function _vcmp(a, b) {
    var pa = String(a||'').split('.').map(function(n){ return parseInt(n,10)||0; });
    var pb = String(b||'').split('.').map(function(n){ return parseInt(n,10)||0; });
    for (var i = 0; i < Math.max(pa.length, pb.length); i++) { var d = (pa[i]||0) - (pb[i]||0); if (d) return d > 0 ? 1 : -1; }
    return 0;
  }
  function checkServer() {
    var ctrl = new AbortController();
    var t = setTimeout(function(){ ctrl.abort(); }, 8000);
    var t0 = Date.now();
    return fetch(REPORT_API + '/map/version', { cache: 'no-store', credentials: 'omit', signal: ctrl.signal })
      .then(function(r){ clearTimeout(t);
        if (r.ok) return r.json().then(function(d){ if (d && d.latest_script) _lmLatestVer = d.latest_script; return { ok: true, label: 'Связь с сервером', detail: 'OK (' + (Date.now()-t0) + ' мс)' }; })
                                  .catch(function(){ return { ok: true, label: 'Связь с сервером', detail: 'OK (' + (Date.now()-t0) + ' мс)' }; });
        if (r.status === 520 || r.status === 521 || r.status === 522)
          return { ok: false, label: 'Связь с сервером', detail: 'Ошибка ' + r.status, advice: 'Сервер перегружен (Cloudflare). Подождите 1-2 минуты и попробуйте снова.' };
        if (r.status === 429)
          return { ok: false, label: 'Связь с сервером', detail: '429', advice: 'Слишком много запросов. Подождите минуту.' };
        return { ok: false, label: 'Связь с сервером', detail: 'HTTP ' + r.status, advice: 'Сервер ответил ошибкой. Сообщите разработчику - отправьте отчёт ниже.' };
      })
      .catch(function(e){ clearTimeout(t);
        return { ok: false, label: 'Связь с сервером', detail: (e.name === 'AbortError' ? 'таймаут' : 'нет связи'),
          advice: 'Соединение блокируется. Отключите AdBlock / приватные расширения для animesss и проверьте интернет.' };
      });
  }
  function diagnose() {
    var checks = [];
    var lsOk = false; try { window.localStorage.setItem('__lm_t','1'); window.localStorage.removeItem('__lm_t'); lsOk = true; } catch(e) {}
    checks.push(lsOk ? { ok: true, label: 'Сохранение данных', detail: 'работает' }
                     : { ok: false, label: 'Сохранение данных', detail: 'не работает', advice: 'Выключите режим Инкогнито — карта не сможет сохранять данные.' });
    checks.push({ ok: true, label: 'Скрипты Tampermonkey', detail: 'включены' });
    checks.push(window._lmXHRPatched ? { ok: true, label: 'Перехват данных лабиринта', detail: 'подключено' }
      : { ok: false, label: 'Перехват данных лабиринта', detail: 'не подключено',
          advice: onLab() ? 'Другое расширение мешает. Отключите лишние скрипты и обновите страницу.' : 'Откройте страницу лабиринта — перехват включается там.' });
    checks.push((window.visitor_name) ? { ok: true, label: 'Авторизация на сайте', detail: 'успешна' }
      : { ok: false, label: 'Авторизация на сайте', detail: 'не успешна', advice: 'Войдите в аккаунт animesss.' });
    checks.push(onLab() ? { ok: true, label: 'Страница лабиринта', detail: 'открыта' }
      : { ok: false, label: 'Страница лабиринта', detail: 'не открыта', advice: 'Откройте лабиринт для полной проверки.' });
    return checkServer().then(function(srv){
      checks.splice(0, 0, srv);
      var outdated = _lmLatestVer && _vcmp(VER, _lmLatestVer) < 0;
      checks.push(outdated
        ? { ok: false, label: 'Версия скрипта', detail: VER + ' — устарела!', advice: 'Доступна новая версия ' + _lmLatestVer + '. Обновите скрипт на Greasyfork (в Tampermonkey → проверить обновления).' }
        : { ok: true, label: 'Версия скрипта', detail: VER + (_lmLatestVer ? ' (последняя)' : '') });
      return checks;
    });
  }

  function lsDump() {
    var out = {};
    try { var L = window.localStorage; for (var i = 0; i < L.length; i++) { var k = L.key(i);
      if (k && k.indexOf('lm') === 0) { var v = L.getItem(k); out[k] = (v && v.length > 8000) ? v.slice(0,8000)+'…[обрезано]' : v; } } } catch(e) {}
    return out;
  }
  function collect(trigger, userMsg, diagArr, errorInfo, includeHtml) {
    var ld = null; try { if (window.labyrinthData) { var s = JSON.stringify(window.labyrinthData);
      ld = (s.length > 600000) ? { _note: 'labyrinthData слишком большой', size: s.length } : JSON.parse(s); } } catch(e) {}
    var _cur = null; try { _cur = window.labyrinthData && window.labyrinthData.mapData && window.labyrinthData.mapData.current; } catch(e) {}
    var p = {
      trigger: trigger, username: nick(), script_version: VER, url: location.href,
      user_agent: navigator.userAgent, user_message: userMsg || '',
      x: (_cur && _cur.x != null) ? _cur.x : null,
      y: (_cur && _cur.y != null) ? _cur.y : null,
      diagnostics: diagArr || null, labyrinth_data: ld, ls_dump: lsDump(), error: errorInfo || null
    };
    if (includeHtml) { try { p.page_html = document.documentElement.outerHTML; } catch(e) { p.page_html = ''; } }
    return p;
  }

  function el(tag, css, html) { var e = document.createElement(tag); if (css) e.style.cssText = css; if (html != null) e.innerHTML = html; return e; }
  var Z = 2147483600;

  var CARD_CSS = 'background:linear-gradient(180deg,#141d33,#0d1322);border:1px solid #263655;border-radius:18px;box-shadow:0 28px 80px rgba(0,0,0,.65);padding:32px 38px;min-width:300px;max-width:90vw;text-align:center;color:#e6eefb;font:600 16px system-ui,sans-serif';
  function lockOverlay() {
    var o = el('div', 'position:fixed;inset:0;z-index:'+(Z+5)+';background:rgba(3,6,14,.88);backdrop-filter:blur(5px);display:flex;align-items:center;justify-content:center;cursor:wait');
    o.innerHTML = '<div id="lmLockCard" style="'+CARD_CSS+'">'
      + '<div style="width:54px;height:54px;margin:0 auto 20px;border:4px solid rgba(120,170,255,.18);border-top-color:#6ea8ff;border-radius:50%;animation:lmspin .9s linear infinite"></div>'
      + '<div style="font-size:17px">Отправка отчёта…</div>'
      + '<div style="font-weight:400;color:#8ea3c4;font-size:13px;margin-top:8px;line-height:1.5">Пожалуйста, подождите.<br>Ничего не нажимайте на странице.</div></div>';
    document.body.appendChild(o); return o;
  }
  function lockCard(lock, iconBg, icon, title, sub, buttonsHtml) {
    lock.style.cursor = 'default';
    var card = lock.querySelector('#lmLockCard'); if (!card) return;
    card.innerHTML =
      '<div style="width:66px;height:66px;margin:0 auto 18px;border-radius:50%;background:'+iconBg+';display:flex;align-items:center;justify-content:center;font-size:36px">'+icon+'</div>'
      + '<div style="font-size:19px">'+title+'</div>'
      + (sub ? '<div style="font-weight:400;color:#8ea3c4;font-size:13px;margin-top:10px;line-height:1.5">'+sub+'</div>' : '')
      + '<div style="margin-top:22px">'+buttonsHtml+'</div>';
  }
  var BTN_PRIMARY = 'background:linear-gradient(135deg,#3b82f6,#22b8cf);border:none;color:#fff;padding:11px 30px;border-radius:11px;cursor:pointer;font-weight:700;font-size:14px';
  var BTN_GHOST = 'background:#26324c;border:none;color:#cfdcf2;padding:11px 22px;border-radius:11px;cursor:pointer;font-size:14px;margin-left:10px';

  function openModal(prefillMsg, errorInfo) {
    var back = el('div', 'position:fixed;inset:0;z-index:'+Z+';background:rgba(3,6,14,.7);display:flex;align-items:center;justify-content:center;padding:16px');
    var box = el('div', 'width:min(560px,96vw);max-height:92vh;overflow:auto;background:#0d1322;border:1px solid #233;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.6);color:#dfe8f5;font:14px system-ui,sans-serif');
    box.innerHTML =
      '<div style="padding:16px 18px;border-bottom:1px solid #1c2740;display:flex;align-items:center;gap:10px">'
      + '<b style="font-size:15px;color:#8fd0ff">🛠 Сообщить о поломке</b>'
      + '<span style="flex:1"></span><button id="lmRClose" style="background:none;border:none;color:#8aa;font-size:20px;cursor:pointer">✕</button></div>'
      + '<div style="padding:16px 18px">'
      + '<div style="font-size:12px;color:#8aa;margin-bottom:6px">Проверка системы:</div>'
      + '<div id="lmDiag" style="background:#0a1020;border:1px solid #1c2740;border-radius:9px;padding:10px;font-size:13px;min-height:40px">Проверяю…</div>'
      + '<div style="font-size:12px;color:#8aa;margin:14px 0 6px">Опишите проблему (необязательно):</div>'
      + '<textarea id="lmRMsg" placeholder="Например: карта не открывается, шахта не собирается…" '
      + 'style="width:100%;box-sizing:border-box;min-height:78px;background:#0a1020;border:1px solid #233;border-radius:9px;color:#dfe8f5;padding:10px;font:13px system-ui;resize:vertical"></textarea>'
      + '<div style="font-size:11px;color:#6b7a93;margin-top:8px">При отправке передаётся полное состояние страницы (для диагностики). Данные видит только администратор.</div>'
      + '<div style="display:flex;gap:10px;margin-top:16px">'
      + '<button id="lmRSend" style="flex:1;background:linear-gradient(135deg,#3b82f6,#22b8cf);border:none;color:#fff;font-weight:700;padding:11px;border-radius:9px;cursor:pointer;font-size:14px">📤 Отправить отчёт</button>'
      + '<button id="lmRCancel" style="background:#1a2236;border:1px solid #2a3550;color:#bcd;padding:11px 16px;border-radius:9px;cursor:pointer">Отмена</button></div>'
      + '<div id="lmRResult" style="margin-top:12px"></div></div>';
    back.appendChild(box); document.body.appendChild(back);
    if (prefillMsg) box.querySelector('#lmRMsg').value = prefillMsg;
    function close() { try { document.body.removeChild(back); } catch(e) {} }
    box.querySelector('#lmRClose').onclick = close;
    box.querySelector('#lmRCancel').onclick = close;
    back.onclick = function(e){ if (e.target === back) close(); };

    var diagArr = null;
    diagnose().then(function(arr){ diagArr = arr;
      box.querySelector('#lmDiag').innerHTML = arr.map(function(c){
        return '<div style="display:flex;gap:8px;padding:3px 0;align-items:flex-start">'
          + '<span>' + (c.ok ? '✅' : '❌') + '</span>'
          + '<div><b>' + c.label + ':</b> ' + (c.detail || '')
          + (c.advice ? '<div style="color:#ffb454;font-size:12px;margin-top:2px">→ ' + c.advice + '</div>' : '') + '</div></div>';
      }).join('');
    }).catch(function(){ box.querySelector('#lmDiag').textContent = 'Не удалось выполнить проверку.'; });

    box.querySelector('#lmRSend').onclick = function() {
      var msg = box.querySelector('#lmRMsg').value || '';
      close();
      var lock = lockOverlay();
      var payload = collect('manual', msg, diagArr, errorInfo, true);
      postReport(payload, 60000).then(function(res){
        lockCard(lock, 'rgba(46,204,113,.16)', '✅', 'Отчёт отправлен!',
          'Спасибо! Номер обращения: <b style="color:#9fc5ff">#' + (res && res.id || '?') + '</b>',
          '<button id="lmLockOk" style="'+BTN_PRIMARY+'">Готово</button>');
        var ok = document.getElementById('lmLockOk');
        if (ok) ok.onclick = function(){ try { document.body.removeChild(lock); } catch(e) {} };
      }).catch(function(e){
        lockCard(lock, 'rgba(255,170,50,.16)', '⚠️', 'Не удалось отправить',
          (e.message || 'ошибка связи'),
          '<button id="lmLockRetry" style="'+BTN_PRIMARY+'">Повторить</button>'
          + '<button id="lmLockClose" style="'+BTN_GHOST+'">Закрыть</button>');
        var rt = document.getElementById('lmLockRetry'), cl = document.getElementById('lmLockClose');
        if (rt) rt.onclick = function(){ try { document.body.removeChild(lock); } catch(e) {} openModal(msg, errorInfo); };
        if (cl) cl.onclick = function(){ try { document.body.removeChild(lock); } catch(e) {} };
      });
    };
  }
  window._lmOpenReport = openModal;

  function addButton() {
    if (!onLab()) return;
    if (document.getElementById('lmReportBtn')) return;
    if (document.getElementById('lm-anbtn')) return;
    var b = el('button', 'position:fixed;left:14px;bottom:14px;z-index:'+(Z-10)+';background:#16203a;border:1px solid #2c3b5e;color:#9fc5ff;font:600 12px system-ui;padding:8px 12px;border-radius:10px;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.4);opacity:.85');
    b.id = 'lmReportBtn'; b.textContent = '🛠 Сообщить о поломке';
    b.title = 'Проверка и отправка отчёта разработчику';
    b.onmouseenter = function(){ b.style.opacity = '1'; };
    b.onmouseleave = function(){ b.style.opacity = '.85'; };
    b.onclick = function(){ openModal('', null); };
    document.body.appendChild(b);
  }

  var _errShown = false;
  function looksOurs(stack, src) {

    var s = (stack || '') + ' ' + (src || '');
    return /labyrinth_map|_lm[A-Z]|lmFetch|lmLS|Animesss Labyrinth/.test(s);
  }
  function offerError(info) {
    if (_errShown || !onLab()) return; _errShown = true;
    var tip = el('div', 'position:fixed;left:14px;bottom:58px;z-index:'+(Z-9)+';background:#2a1414;border:1px solid #5a2b2b;color:#ffd9d9;font:13px system-ui;padding:12px 14px;border-radius:11px;max-width:300px;box-shadow:0 6px 18px rgba(0,0,0,.5)');
    tip.innerHTML = '⚠️ Возможна ошибка в работе карты.<br>Отправить отчёт разработчику?'
      + '<div style="display:flex;gap:8px;margin-top:10px"><button id="lmEY" style="flex:1;background:#d13b3b;border:none;color:#fff;padding:7px;border-radius:7px;cursor:pointer;font-weight:600">Да</button>'
      + '<button id="lmEN" style="background:#3a2a2a;border:none;color:#eebcbc;padding:7px 14px;border-radius:7px;cursor:pointer">Нет</button></div>';
    document.body.appendChild(tip);
    var to = setTimeout(function(){ try { document.body.removeChild(tip); } catch(e) {} }, 15000);
    tip.querySelector('#lmEY').onclick = function(){ clearTimeout(to); try { document.body.removeChild(tip); } catch(e) {} openModal('АВТО: обнаружена ошибка скрипта.', info); };
    tip.querySelector('#lmEN').onclick = function(){ clearTimeout(to); try { document.body.removeChild(tip); } catch(e) {} };
  }
  window.addEventListener('error', function(e){
    try { if (!e || !e.error) return; if (!looksOurs(e.error && e.error.stack, e.filename)) return;
      offerError({ type: 'error', message: String(e.message||''), stack: String((e.error&&e.error.stack)||'').slice(0,2000), src: e.filename||'', line: e.lineno||0 });
    } catch(_) {}
  });
  window.addEventListener('unhandledrejection', function(e){
    try { var r = e && e.reason; var stack = (r && r.stack) || ''; if (!looksOurs(stack, '')) return;
      offerError({ type: 'unhandledrejection', message: String((r&&r.message)||r||''), stack: String(stack).slice(0,2000) });
    } catch(_) {}
  });

  function init() { setTimeout(function(){ try { addButton(); } catch(e) {} }, 4000); }
  if (document.body) init(); else document.addEventListener('DOMContentLoaded', init);

  try { var st = document.createElement('style'); st.textContent = '@keyframes lmspin{to{transform:rotate(360deg)}}'; document.head.appendChild(st); } catch(e) {}
})();

(function() {
  'use strict';
  if (window._lmMsgInit) return; window._lmMsgInit = true;
  if (!/animesss\.(com|tv)$/.test(location.hostname)) return;

  var MSG_API = 'https://labmap.duckdns.org/api';
  function nick() { return window.visitor_name || ''; }
  var _msgs = [];
  var Z = 2147483600;

  function lastSeen() { try { return parseInt(window.lmLS.get('lm_msg_lastseen') || '0', 10) || 0; } catch(e) { return 0; } }
  function setLastSeen(id) { try { window.lmLS.set('lm_msg_lastseen', String(id)); } catch(e) {} }
  function unreadCount() { var ls = lastSeen(); return _msgs.filter(function(m){ return Number(m.id) > ls; }).length; }

  function updateBadge() {
    var b = document.getElementById('lm-msgbadge'), btn = document.getElementById('lm-msgbtn');
    if (!b || !btn) return;
    var n = unreadCount();
    if (n > 0) { b.textContent = n > 99 ? '99+' : String(n); b.classList.add('show'); btn.classList.add('has-unread'); }
    else { b.classList.remove('show'); btn.classList.remove('has-unread'); }
  }

  function poll() {
    var u = nick(); if (!u) return;
    var ctrl = new AbortController();
    var t = setTimeout(function(){ ctrl.abort(); }, 10000);
    fetch(MSG_API + '/messages?username=' + encodeURIComponent(u), { cache: 'no-store', credentials: 'omit', signal: ctrl.signal })
      .then(function(r){ clearTimeout(t); return r.ok ? r.json() : null; })
      .then(function(d){ if (d && Array.isArray(d.messages)) { _msgs = d.messages; updateBadge(); } })
      .catch(function(){ clearTimeout(t); });
  }
  window._lmMsgPoll = poll;

  function fmtTime(iso) {
    try { var d = new Date(iso); return ('0'+d.getDate()).slice(-2)+'.'+('0'+(d.getMonth()+1)).slice(-2)+' '+('0'+d.getHours()).slice(-2)+':'+('0'+d.getMinutes()).slice(-2); } catch(e) { return ''; }
  }
  function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]; }); }

  window._lmOpenMessages = function() {
    var back = document.createElement('div');
    back.style.cssText = 'position:fixed;inset:0;z-index:'+Z+';background:rgba(3,6,14,.7);display:flex;align-items:center;justify-content:center;padding:16px';
    var box = document.createElement('div');
    box.style.cssText = 'width:min(480px,96vw);max-height:88vh;display:flex;flex-direction:column;background:#0d1322;border:1px solid #233;border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.6);color:#dfe8f5;font:14px system-ui,sans-serif';
    var rows = _msgs.slice().sort(function(a,b){ return Number(a.id)-Number(b.id); }).map(function(m){
      var upd = (m.msg_type === 'update' && m.action_url)
        ? '<div style="margin-top:8px"><a href="'+esc(m.action_url)+'" target="_blank" rel="noopener" style="display:inline-block;background:linear-gradient(135deg,#3b82f6,#22b8cf);color:#fff;text-decoration:none;padding:8px 16px;border-radius:8px;font-weight:700;font-size:13px">⬆ Обновить скрипт</a></div>'
        : '';
      var rep = m.allow_reply
        ? '<div class="lm-reply" data-mid="'+m.id+'" style="margin-top:10px;border-top:1px solid #1c2740;padding-top:8px">'
          + '<div style="font-size:11px;color:#8fd0ff;margin-bottom:5px">✍ Можно ответить админу:</div>'
          + '<textarea class="lm-reply-txt" placeholder="Ваш ответ..." style="width:100%;box-sizing:border-box;min-height:44px;background:#0a1020;border:1px solid #233;border-radius:8px;color:#dfe8f5;padding:8px;font:13px system-ui;resize:vertical"></textarea>'
          + '<div style="margin-top:6px"><button class="lm-reply-btn" style="background:linear-gradient(135deg,#3b82f6,#22b8cf);border:none;color:#fff;font-weight:700;padding:7px 16px;border-radius:8px;cursor:pointer;font-size:13px">Ответить</button><span class="lm-reply-res" style="margin-left:8px;font-size:12px"></span></div>'
          + '</div>'
        : '';
      return '<div style="background:#111a2e;border:1px solid #1c2740;border-radius:10px;padding:10px 12px;margin-bottom:10px">'
        + '<div style="font-size:11px;color:#8fd0ff;font-weight:700;margin-bottom:4px">👑 Админ лабиринта <span style="color:#5b6b85;font-weight:400;float:right">'+fmtTime(m.created_at)+'</span></div>'
        + '<div style="white-space:pre-wrap;line-height:1.5">'+esc(m.text)+'</div>' + upd + rep + '</div>';
    }).join('');
    box.innerHTML =
      '<div style="padding:14px 16px;border-bottom:1px solid #1c2740;display:flex;align-items:center;gap:10px">'
      + '<b style="font-size:15px;color:#8fd0ff">💬 Сообщения от админа</b><span style="flex:1"></span>'
      + '<button id="lmMsgClose" style="background:none;border:none;color:#8aa;font-size:20px;cursor:pointer">✕</button></div>'
      + '<div style="padding:14px 16px;overflow:auto">' + (rows || '<div style="text-align:center;color:#6b7a93;padding:30px 0">📭 У вас нет сообщений</div>') + '</div>';
    back.appendChild(box); document.body.appendChild(back);
    Array.prototype.forEach.call(box.querySelectorAll('.lm-reply-btn'), function(btn){
      btn.addEventListener('click', function(){
        var wrap = btn.closest ? btn.closest('.lm-reply') : null; if (!wrap) return;
        var mid = wrap.getAttribute('data-mid');
        var txtEl = wrap.querySelector('.lm-reply-txt'), res = wrap.querySelector('.lm-reply-res');
        var t = (txtEl.value || '').trim();
        if (!t) { res.style.color = '#ff9b9b'; res.textContent = 'Введите текст'; return; }
        btn.disabled = true; res.style.color = '#8aa'; res.textContent = 'Отправка...';
        fetch(MSG_API + '/message-reply', { method:'POST', cache:'no-store', credentials:'omit', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ username: nick(), message_id: Number(mid), text: t }) })
          .then(function(r){ return r.ok ? r.json() : null; })
          .then(function(d){ if (d && d.ok) { res.style.color='#6ee786'; res.textContent='✓ Ответ отправлен'; txtEl.value=''; txtEl.disabled=true; btn.style.display='none'; } else { res.style.color='#ff9b9b'; res.textContent='Ошибка'; btn.disabled=false; } })
          .catch(function(){ res.style.color='#ff9b9b'; res.textContent='Ошибка сети'; btn.disabled=false; });
      });
    });
    function close(){ try { document.body.removeChild(back); } catch(e) {} }
    box.querySelector('#lmMsgClose').onclick = close;
    back.onclick = function(e){ if (e.target === back) close(); };
    var maxId = _msgs.reduce(function(m,x){ return Math.max(m, Number(x.id)||0); }, 0);
    if (maxId > lastSeen()) setLastSeen(maxId);
    updateBadge();
  };

  poll();
  setInterval(poll, 90000);
  setInterval(updateBadge, 5000);
})();