Animesss Labyrinth Map

Інтерактивна карта лабіринту (совместное прохождение)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Animesss Labyrinth Map
// @namespace    https://animesss.com/
// @version      5.0.0
// @description  Інтерактивна карта лабіринту (совместное прохождение)
// @author       VladLIO
// @license      MIT
// @match        https://animesss.com/labyrinth/
// @match        https://animesss.com/labyrinth/*
// @match        https://animesss.tv/labyrinth/
// @match        https://animesss.tv/labyrinth/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

console.log('[LabMap] v3.8.0 старт');

// ============================================================
// ПЕРЕХВАТ AJAX — оновлення mapData після кожного ходу
// (фікс від друга — перехоплює mod=animesss_game&action=step)
// ============================================================
(function() {
  var origOpen = XMLHttpRequest.prototype.open;
  var origSend = XMLHttpRequest.prototype.send;
  XMLHttpRequest.prototype.open = function(method, url) {
    this._lmUrl = url;
    return origOpen.apply(this, arguments);
  };
  XMLHttpRequest.prototype.send = function(body) {
    if (this._lmUrl && this._lmUrl.indexOf('mod=animesss_game') !== -1 &&
        typeof body === 'string' && body.indexOf('action=step') !== -1) {
      var xhr = this;
      xhr.addEventListener('load', function() {
        try {
          var resp = JSON.parse(xhr.responseText);
          if (resp && resp.mapData) {
            if (!window.labyrinthData) window.labyrinthData = {};
            window.labyrinthData.mapData = resp.mapData;
            console.log('[LabMap] mapData з AJAX, current:', JSON.stringify(resp.mapData.current));
          }
        } catch(e) {}
      });
    }
    return origSend.apply(this, arguments);
  };
})();


(function () {
  'use strict';

  // ============================================================
  // КОНСТАНТИ
  // ============================================================
  var WORKER_URL     = 'https://labyrinth-map.lvladddd.workers.dev';
  var SESSION_ID     = 'sess_' + Date.now() + '_' + Math.random().toString(36).slice(2,8);
  var LS_KEY         = 'lm_cloud_map';
  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 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:'#1a1200', personal_mine_collect:'#1a1200',
    foreign_mine:'#1a0a00', fatigue:'#1a1000',
    spiritual_teleport:'#160030',
    mimic_chest:'#2a0030', mimic_chest_hit:'#2a0030', mimic_chest_killed:'#2a0030',
    mimic_chest_escape:'#2a0030', mimic_chest_reward:'#2a0030', mimic_chest_back:'#2a0030',
    guardian_user:'#5a3a00', guardian_club:'#1a0040', can_capture:'#003a1a',
    unknown:'#080a12'
  };
  var ICONS = {
    start:'⚑', reward:'+', penalty:'!', quiz:'?', quiz_result:'?',
    puzzle:'🧩', jackpot:'★', reward_card:'🎴', shield_block:'!',
    empty:'·', collection:'🃏', trap_back:'↺', mini_boss:'👹',
    hard_boss:'💀', luck_altar:'🍀', luck_altar_result:'🍀',
    locked_chest:'🔒', locked_chest_result:'🎁', card_trader:'🛒',
    card_trader_result:'🛒', relic_room:'🔮', recovery_room:'✚',
    room_trap:'👤', room_gift:'👤', spiritual_teleport:'🌀',
    mimic_chest:'👅', mimic_chest_hit:'👅', mimic_chest_killed:'👅',
    mimic_chest_escape:'👅', mimic_chest_reward:'👅', mimic_chest_back:'👅',
    guardian_user:'👑', guardian_club:'🛡', can_capture:'⚐',
    personal_mine:'⛏', personal_mine_created:'⛏', personal_mine_collect:'⛏',
    foreign_mine:'⛏', fatigue:'💤'
  };
  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:'#6ee786',
    foreign_mine:'#ff8b8b', fatigue:'#ffd66b'
  };
  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:'Усталость'
  };
  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:'Персональная шахта — добывает ACC',
    personal_mine_created:'Шахта только что основана',
    personal_mine_collect:'Добыча собрана из шахты',
    foreign_mine:'Шахта другого игрока',
    fatigue:'Усталость от лабиринта'
  };
  var LEGEND_ITEMS = [
    ['start','Старт'],['reward','Награда'],['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','Чужая шахта'],
    ['__variable__','🔀 Переменные']
  ];

  // ============================================================
  // СТАН
  // ============================================================
  var cloudMap       = {};
  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 ownershipCache = null;
  var highlightRoom  = null;

  // FIX: одна глобальна змінна для патчингу (була подвійна декларація)
  var _lmPatching    = false;

  // FIX: push debounce — уникаємо подвійного push при одному кроці
  var _pushTimer     = null;
  var _pushPending   = false;

  // Inline action bar — поточна активна вкладка
  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; }

  // ============================================================
  // PATCH SITE MAP — показує іконки на міні-карті сайту
  // ============================================================
  // Кешуємо знайдений центр сітки щоб не шукати кожен раз
  var _gridCenter = null; // {gx, gy, useIndex}

  // ============================================================
  // ДІАГНОСТИКА — викликати з консолі: window.lmDiag()
  // ============================================================
  // ============================================================
  // ДІАГНОСТИКА — викликати з консолі: lmDiag() або lmDebug()
  // ============================================================
  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));

    // Визначаємо реальну ширину grid (скільки колонок)
    // Беремо першу клітинку і дивимось скільки клітинок в тому ж рядку по top
    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 (cells per row by DOM position):', rowCount);
    }

    // data-x/data-y перших 3 і current
    console.log('--- Cells attr ---');
    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')+' cls='+cells[i].className.split(' ').filter(function(x){return x.indexOf('labyrinth')>=0;}).join(' '));
    }

    // Current і available
    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')+
        ' idx%25='+j%25+' Math.floor(idx/25)='+Math.floor(j/25)+
        ' top='+Math.round(rect2.top)+' left='+Math.round(rect2.left));
    }
    console.log('=== END ===');
  }

  // Показує data-x/data-y/index прямо на клітинках (toggle)
  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);
    });
    console.log('[LabMap] debug overlay:', _debugOverlayOn ? 'ON' : 'OFF');
  }

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

  function detectGridCenter(cells) {
    // Знаходимо current клітинку по INDEX в NodeList (надійніше ніж data-x/data-y)
    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);
      console.log('[LabMap] center by index='+i+': col='+col+' row='+row);
      return { gx: col, gy: row, useIndex: true };
    }
    console.warn('[LabMap] current cell not found!');
    return null;
  }

  function patchSiteMap() {
    var mapEl = document.getElementById('labyrinthMap');
    if (!mapEl) return;
    var d = mapData();
    if (!d || !d.current) return;
    var cur = d.current;
    var rooms = allRooms();
    var cells = mapEl.querySelectorAll('.labyrinth-cell');
    if (!cells.length) return;

    // Знаходимо де поточна клітинка в сітці — скидаємо кеш після кожного ходу
    var center = detectGridCenter(cells);
    if (!center) return; // не знайшли поточну клітинку — не патчимо

    _lmPatching = true;

    var cellsArr = Array.prototype.slice.call(cells);
    cellsArr.forEach(function(cell, idx) {
      if (cell.classList.contains('labyrinth-cell--current')) return;

      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);
      // Пробуємо обидва напрямки — якщо карта відзеркалена по Y,
      // row=0 може бути внизу (CSS grid може йти знизу вгору)
      var ly = cur.y + (gy - center.gy); // + замість - : row↑ = глибше = менший y
      var key = lx + '_' + ly;
      var room = rooms[key];

      if (!room || room.event === 'unknown') {
        cell.classList.remove('lm-known');
        cell.removeAttribute('data-event');
        // Видаляємо span іконку якщо була
        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);

      // Вставляємо або оновлюємо span з іконкою
      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;
    console.log('[LabMap] patchSiteMap done, center:', center.gx, center.gy);
  }

  function patchSiteMapDelayed() {
    var tries = 0;
    function attempt() {
      var mapEl = document.getElementById('labyrinthMap');
      var cells = mapEl && mapEl.querySelectorAll('.labyrinth-cell');
      // Чекаємо не просто наявності клітинок, а і наявності поточної (current)
      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);
      } else {
        console.warn('[LabMap] patchSiteMap: current cell not found after 30 tries');
      }
    }
    attempt();
  }

  function updateMapInfo() {
    var rooms = 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 = 'Облако: '+Object.keys(cloudMap).length+' | Всего: '+total;
  }

  // Змінні кімнати
  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
  };

  function allRooms() {
    if (roomsCache) return roomsCache;
    var r = {}, k;

    // 1. Дані з хмари
    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 = 'unknown';
      }
      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);
      }
      r[k] = {event:ev, guardian:room.guardian||null, visits:room.visits||1, altTypes:altTypes, roomObject:roomObj};
    }

    // 2. Локальні кроки гравця — ЗАВЖДИ мають пріоритет
    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: isRoomEv ? 'unknown' : 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;
  }

  // ============================================================
  // МЕРЕЖА
  // ============================================================
  function loadFromCache() {
    try {
      var raw = localStorage.getItem(LS_KEY);
      if (raw) {
        cloudMap = JSON.parse(raw);
        invalidateRoomsCache();
        drawMini();
        if (fmOpen) drawFull();
        // Патчимо карту сайту з кешованих даних
        patchSiteMapDelayed();
      }
    } catch(e) { console.warn('[LabMap] кэш:', e); }
  }

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

    // FIX: timeout 15с щоб не зависнути назавжди
    var timedOut = false;
    var timeoutId = setTimeout(function() {
      timedOut = true;
      _cloudLoading = false;
      console.warn('[LabMap] loadCloud timeout');
      if (onDone) onDone();
    }, 15000);

    fetch(WORKER_URL+'/map')
      .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
      .then(function(d) {
        if (timedOut) return;
        clearTimeout(timeoutId);
        cloudMap = d.rooms || {};
        invalidateRoomsCache();
        try { localStorage.setItem(LS_KEY, JSON.stringify(cloudMap)); } catch(e) {}
        console.log('[LabMap] Облако:', Object.keys(cloudMap).length, 'комнат');
        _cloudLoading = false;
        updateMapInfo();
        drawMini();
        if (fmOpen) drawFull();
        // Патчимо карту сайту з новими даними
        patchSiteMapDelayed();
        if (onDone) onDone();
      })
      .catch(function(e) {
        if (timedOut) return;
        clearTimeout(timeoutId);
        _cloudLoading = false;
        console.warn('[LabMap] loadCloud:', e);
        if (onDone) onDone();
      });
  }

  var _stepsPushing = false;

  // FIX: debounced push — чекаємо 800мс після останньої зміни
  function schedulePush(steps, sid) {
    if (_pushTimer) clearTimeout(_pushTimer);
    _pushPending = true;
    _pushTimer = setTimeout(function() {
      _pushPending = false;
      _pushTimer = null;
      pushSteps(steps, sid);
    }, 800);
  }

  function pushSteps(steps, sid) {
    if (_stepsPushing) return Promise.resolve(null);
    _stepsPushing = true;
    return fetch(WORKER_URL+'/update', {
      method:'POST',
      headers:{'Content-Type':'application/json'},
      body:JSON.stringify({steps:steps, session_id:sid||SESSION_ID})
    })
    .then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
    .then(function(d) {
      _stepsPushing = false;
      console.log('[LabMap] push', steps.length, 'total:', d.rooms);
      return d;
    })
    .catch(function(e) {
      _stepsPushing = false;
      console.warn('[LabMap] push err:', e);
    });
  }

  // ============================================================
  // OWNERSHIP
  // ============================================================
  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) { 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 = doc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
        if (!clubLink) {
          var all = doc.querySelectorAll('a[href*="/clubs/"]');
          for (var i = 0; i < all.length; i++) {
            if (/\/clubs\/\d+\//.test(all[i].getAttribute('href')||'')) { clubLink=all[i]; break; }
          }
        }
        if (clubLink) {
          return fetch(clubLink.getAttribute('href')).then(function(r){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) { console.warn('[LabMap] fetchOwnership:', e); ownershipCache = []; callback([]); });
  }

  // ============================================================
  // МАЛЮВАННЯ
  // ============================================================
  function drawCell(ctx, px, py, cs, event, isCurrent, guardian, dimmed, hasAlt, roomObject) {
    var r = Math.max(1, cs * 0.12);
    ctx.fillStyle = isCurrent ? '#2a1800' : dimmed ? '#0d0f1a' : clr(event);
    ctx.beginPath();
    if (ctx.roundRect) ctx.roundRect(px+.5, py+.5, cs-1, cs-1, r);
    else ctx.rect(px+.5, py+.5, cs-1, cs-1);
    ctx.fill();
    ctx.strokeStyle = isCurrent ? 'rgba(255,214,107,.6)' : 'rgba(255,255,255,.07)';
    ctx.lineWidth = .5; ctx.stroke();

    if (isCurrent) {
      ctx.shadowColor='#ffd66b'; ctx.shadowBlur=cs*.8;
      ctx.strokeStyle='rgba(255,214,107,.9)'; ctx.lineWidth=1; ctx.stroke();
      ctx.shadowBlur=0;
    }
    if (cs < 8 || dimmed) return;

    var iconTxt = isCurrent ? '⚔' : ico(event);
    if (iconTxt) {
      ctx.font = Math.max(8, cs*.58) + 'px sans-serif';
      ctx.fillStyle = isCurrent ? '#ffd66b' : iclr(event);
      ctx.textAlign='center'; ctx.textBaseline='middle';
      ctx.fillText(iconTxt, px+cs/2, py+cs/2);
    }

    if (guardian && cs >= 8) {
      var gc = guardian==='guardian_user' ? '#ffd700' : guardian==='guardian_club' ? '#c59cff' : '#44ff88';
      ctx.strokeStyle=gc; ctx.lineWidth=Math.max(1,cs*.06);
      ctx.shadowColor=gc; ctx.shadowBlur=cs*.12; ctx.globalAlpha=.55;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(px+.5,py+.5,cs-1,cs-1,r);
      else ctx.rect(px+.5,py+.5,cs-1,cs-1);
      ctx.stroke(); ctx.shadowBlur=0; ctx.globalAlpha=1;
      if (cs >= 14) {
        var gico = guardian==='guardian_user'?'👑':guardian==='guardian_club'?'🛡':'⚐';
        ctx.font=Math.max(7,cs*.3)+'px sans-serif';
        ctx.fillStyle=gc; ctx.textAlign='right'; ctx.textBaseline='top';
        ctx.fillText(gico, px+cs-1, py+1);
      }
    }

    if (roomObject && !dimmed && cs >= 6) {
      var oc = roomObject==='room_trap' ? '#ff6b6b' : '#ffd66b';
      ctx.strokeStyle = oc;
      ctx.lineWidth = Math.max(1, cs * 0.07);
      ctx.setLineDash([cs*0.15, cs*0.1]);
      ctx.shadowColor = oc; ctx.shadowBlur = cs * 0.1; ctx.globalAlpha = 0.7;
      ctx.beginPath();
      if (ctx.roundRect) ctx.roundRect(px+.5, py+.5, cs-1, cs-1, r);
      else ctx.rect(px+.5, py+.5, cs-1, cs-1);
      ctx.stroke();
      ctx.setLineDash([]); ctx.shadowBlur = 0; ctx.globalAlpha = 1;
    }

    if (hasAlt && !dimmed && cs >= 10) {
      var asz = Math.max(6, cs * 0.28);
      ctx.font = 'bold ' + asz + 'px sans-serif';
      var isVarRoom = event !== 'unknown' && VARIABLE_EVENTS[event];
      ctx.fillStyle = isVarRoom ? 'rgba(192,132,252,0.95)' : 'rgba(255,214,107,0.9)';
      ctx.textAlign = 'left';
      ctx.textBaseline = 'bottom';
      ctx.shadowColor = isVarRoom ? 'rgba(160,80,255,0.6)' : 'rgba(255,160,0,0.6)';
      ctx.shadowBlur = cs * 0.15;
      ctx.fillText(isVarRoom ? '~' : '?', px + 2, py + cs - 1);
      ctx.shadowBlur = 0;
    }
  }

  // drawMini — міні-карта прибрана, але функція залишена щоб не ламати виклики
  // оновлює тільки лічильник комнат
  function drawMini() {
    var rooms = allRooms(), keys = Object.keys(rooms);
    if (elSt) elSt.textContent = 'Облако: '+Object.keys(cloudMap).length+' | Всего: '+keys.length;
    updateMapInfo();
  }

  function openFull(skipCenter) {
    fmOpen=true;
    document.getElementById('lm-modal').classList.add('on');
    if (!skipCenter) {
      requestAnimationFrame(function(){
        if (!fmOpen) return;
        var c = curPos();
        centerOn(c.x, c.y);
      });
    }
  }
  function closeFull() { fmOpen=false; document.getElementById('lm-modal').classList.remove('on'); }
  function centerOn(x,y) {
    if (!elMbody) return;
    var cs=CELL_SIZE*fmScale;
    fmX=elMbody.offsetWidth/2-x*cs; fmY=elMbody.offsetHeight/2-y*cs; drawFull();
  }

  function drawFull() {
    if (!elFc || !elMbody) return;
    elFc.width=elMbody.offsetWidth; elFc.height=elMbody.offsetHeight;
    var ctx=elFc.getContext('2d');
    ctx.clearRect(0,0,elFc.width,elFc.height);
    var rooms=allRooms(), keys=Object.keys(rooms);
    if (!keys.length) return;
    var cs=CELL_SIZE*fmScale, cur=curPos(), hasFilter=Object.keys(filterSet).length>0;

    ctx.strokeStyle='rgba(255,255,255,.04)'; ctx.lineWidth=.5;
    for (var gx=fmX%cs; gx<elFc.width; gx+=cs) { ctx.beginPath(); ctx.moveTo(gx,0); ctx.lineTo(gx,elFc.height); ctx.stroke(); }
    for (var gy=fmY%cs; gy<elFc.height; gy+=cs) { ctx.beginPath(); ctx.moveTo(0,gy); ctx.lineTo(elFc.width,gy); ctx.stroke(); }

    var myPathSet = {};
    if (showMyPath) {
      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);
    }

    if (showMyPath) {
      var dm3=mapData();
      if (dm3&&dm3.steps&&dm3.steps.length>1) {
        ctx.strokeStyle='rgba(255,214,107,.35)'; ctx.lineWidth=Math.max(1,cs*.18); ctx.lineJoin='round';
        ctx.beginPath();
        var f=dm3.steps[0]; ctx.moveTo(fmX+f.x*cs+cs/2,fmY+f.y*cs+cs/2);
        for (var li=1;li<dm3.steps.length;li++) { var ls=dm3.steps[li]; ctx.lineTo(fmX+ls.x*cs+cs/2,fmY+ls.y*cs+cs/2); }
        ctx.stroke();
      }
    }

    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)+' | Облако: '+Object.keys(cloudMap).length+' | Всего: '+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';
  var LS_HIST_DATE_KEY = 'lm_hist_cache_date';
  var histCache = {};
  var histTotal = null;
  var histCurPage = 1;
  try {
    var _raw = localStorage.getItem(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;
    });
    return { rows: rows, totalPages: totalPages, allAmounts: allAmounts };
  }

  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) { return r.text(); })
      .then(function(html) {
        var result = parseHistoryHtml(html, pageNum);
        histCache[pageNum] = result;
        if (histTotal === null || result.totalPages > histTotal) histTotal = result.totalPages;
        try { localStorage.setItem(LS_HIST_KEY, JSON.stringify(histCache)); } catch(e) {}
        onDone(result);
      })
      .catch(function(e) {
        console.warn('[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 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 trapCount = counts['trap_back'] || 0;
    var lostRooms = trapCount * 5;
    var sorted = Object.keys(counts).sort(function(a,b){ return counts[b]-counts[a]; });
    return { total: total, counts: counts, top: sorted, trapCount: trapCount, lostRooms: lostRooms };
  }

  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 = localStorage.getItem(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 cardsHtml = '<div class="lm-sc-grid"><div class="lm-sc-card"><div class="lm-sc-label">Всего комнат</div><div class="lm-sc-val">'+stats.total+'</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">−'+stats.lostRooms+'</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-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{margin-bottom:18px;border-radius:20px;background:linear-gradient(135deg,rgba(26,29,50,.95) 0%,rgba(15,18,32,.98) 100%);border:1px solid rgba(255,255,255,.09);color:#fff;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,.35);}',

      /* Шапка */
      '#lm-hdr{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;border-bottom:1px solid rgba(255,255,255,.07);background:rgba(255,255,255,.02);}',
      '#lm-ttl{display:flex;align-items:center;gap:8px;font-size:13px;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:rgba(255,255,255,.6);}',
      '#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;gap:8px;}',
      '#lm-rbtn{padding:7px 16px;border:none;border-radius:10px;background:linear-gradient(135deg,#4a5ac7,#3a4ab0);color:#fff;font-size:12px;font-weight:700;cursor:pointer;box-shadow:0 2px 12px rgba(74,90,199,.4);transition:.15s;}',
      '#lm-rbtn:hover{background:linear-gradient(135deg,#5b6dd8,#4a5ac7);transform:translateY(-1px);box-shadow:0 4px 16px rgba(74,90,199,.5);}',
      '#lm-rbtn.loading{opacity:.6;cursor:not-allowed;transform:none;}',
      '#lm-pbtn{padding:7px 16px;border:none;border-radius:10px;background:linear-gradient(135deg,#b02a59,#8a1f45);color:#fff;font-size:12px;font-weight:700;cursor:pointer;box-shadow:0 2px 12px rgba(176,42,89,.4);transition:.15s;}',
      '#lm-pbtn:hover{background:linear-gradient(135deg,#c03468,#b02a59);transform:translateY(-1px);box-shadow:0 4px 16px rgba(176,42,89,.5);}',

      /* Статистичні картки — БЕЗ hover */
      '#lm-map-info{display:grid;grid-template-columns:1fr 1fr;gap:0;border-bottom:1px solid rgba(255,255,255,.07);}',
      '.lm-mi-card{display:flex;align-items:center;gap:12px;padding:14px 18px;position:relative;}',
      '.lm-mi-card:first-child{border-right:1px solid rgba(255,255,255,.07);}',
      '.lm-mi-ico{font-size:24px;flex-shrink:0;filter:drop-shadow(0 0 6px rgba(255,255,255,.2));}',
      '.lm-mi-val{font-size:22px;font-weight:800;line-height:1;color:#fff;margin-bottom:3px;}',
      '.lm-mi-lbl{font-size:11px;color:rgba(255,255,255,.45);text-transform:uppercase;letter-spacing:.04em;}',

      /* Action bar — таби у вигляді пілюль */
      '#lm-action-bar{display:flex;gap:8px;padding:12px 16px;border-bottom:1px solid rgba(255,255,255,.07);background:rgba(0,0,0,.15);}',
      '.lm-abar-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:7px;padding:9px 12px;border:1px solid rgba(255,255,255,.1);background:rgba(255,255,255,.05);color:rgba(255,255,255,.6);font-size:12px;font-weight:700;cursor:pointer;border-radius:12px;transition:background .15s,border-color .15s,color .15s;white-space:nowrap;}',
      '.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;}',

      /* Inline панель попапів */
      '#lm-inline-panel{display:none;border-top:0;}',
      '#lm-inline-panel.on{display:block;}',

      /* Inline стражі/клуб */
      '#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);}',

      /* Inline історія */
      '#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;}',

      '@media(max-width:640px){#lm-map-info{grid-template-columns:1fr 1fr;}.lm-mi-val{font-size:18px;}.lm-abar-btn{font-size:9px;padding:9px 4px;}}',
      '#lm-modal{display:none;position:fixed;inset:0;z-index:999999;background:rgba(0,0,0,.93);flex-direction:column;touch-action:none;overflow:hidden;}',
      '#lm-modal.on{display:flex;}',
      '#lm-mhdr{display:flex;flex-direction:column;gap:6px;padding:10px 16px;background:#13151f;border-bottom:1px solid rgba(255,255,255,.1);flex-shrink:0;}',
      '#lm-mhdr-top{display:flex;align-items:center;justify-content:space-between;width:100%;}',
      '#lm-mhdr-btns{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-left:auto;}',
      '#lm-mttl{font-size:15px;font-weight:800;flex-grow:1;color:#fff;}',
      '#lm-b0,#lm-b1,#lm-bf,#lm-bpath,#lm-bguard,#lm-bclub,#lm-bhist{padding:6px 12px;border-radius:8px;font-size:11px;font-weight:700;cursor:pointer;position:relative;}',
      '#lm-b0{border:none;background:rgba(74,90,199,.25);color:#9fb4ff;border:1px solid rgba(74,90,199,.4);}#lm-b0:hover{background:rgba(74,90,199,.4);color:#fff;}',
      '#lm-b1{border:none;background:rgba(249,168,37,.2);color:#ffd66b;border:1px solid rgba(249,168,37,.4);}#lm-b1:hover{background:rgba(249,168,37,.35);color:#fff;}',
      '#lm-bf{border:none;background:rgba(124,58,237,.2);color:#c59cff;border:1px solid rgba(124,58,237,.4);}#lm-bf:hover{background:rgba(124,58,237,.35);color:#fff;}#lm-bf.active{background:#7c3aed;color:#fff;border-color:#7c3aed;}',
      '#lm-bpath{border:none;background:rgba(5,150,105,.2);color:#6ee786;border:1px solid rgba(5,150,105,.4);}#lm-bpath:hover{background:rgba(5,150,105,.35);color:#fff;}#lm-bpath.active{background:#059669;color:#fff;border-color:#059669;}',
      '#lm-bguard{border:none;background:rgba(255,215,0,.15);color:#ffd700;border:1px solid rgba(255,215,0,.35);}#lm-bguard:hover{background:rgba(255,215,0,.28);color:#fff;}#lm-bguard.active{background:#b8860b;color:#fff;border-color:#b8860b;}',
      '#lm-bclub{border:none;background:rgba(197,156,255,.15);color:#c59cff;border:1px solid rgba(197,156,255,.35);}#lm-bclub:hover{background:rgba(197,156,255,.28);color:#fff;}#lm-bclub.active{background:#6b21a8;color:#fff;border-color:#6b21a8;}',
      '#lm-bhist{border:none;background:rgba(6,182,212,.15);color:#67e8f9;border:1px solid rgba(6,182,212,.35);}#lm-bhist:hover{background:rgba(6,182,212,.28);color:#fff;}#lm-bhist.active{background:#0e7490;color:#fff;border-color:#0e7490;}',
      '#lm-bc{width:36px;height:36px;border:none;border-radius:8px;padding:0;background:#c0392b;color:#fff;font-size:18px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;}#lm-bc:hover{background:#e74c3c;}',
      '#lm-brefresh{height:36px;padding:0 12px;gap:5px;border:none;border-radius:8px;background:rgba(74,90,199,.3);color:#9fb4ff;font-size:12px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;border:1px solid rgba(74,90,199,.5);white-space:nowrap;}',
      '#lm-brefresh:hover{background:rgba(74,90,199,.5);color:#fff;}',
      '#lm-brefresh.loading{opacity:.6;cursor:not-allowed;}',
      '#lm-brefresh.loading .lm-ref-ico{display:inline-block;animation:lm-spin 1s linear infinite;}',
      '@keyframes lm-spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}',
      '#lm-hdr-right{display:flex;align-items:center;gap:6px;}',
      '.lm-pop{display:none;position:absolute;top:calc(100% + 8px);left:0;z-index:100;background:#1a1d2a;border:1px solid rgba(255,255,255,.15);border-radius:14px;padding:0;box-shadow:0 8px 32px rgba(0,0,0,.6);overflow:hidden;max-width:calc(100vw - 16px);}',
      '.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;}#lm-hpop .lm-pop-link:hover{color:#67e8f9;}',
      '@media(max-width:600px){.lm-pop.on{position:fixed;top:160px;left:8px;right:8px;width:auto;max-width:none;}}',
      '.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-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-nav:hover:not(:disabled){background:rgba(6,182,212,.38);}',
      '.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;overflow:hidden;position:relative;cursor:grab;background:#06080f;touch-action:none;}',
      '#lm-mbody.dr{cursor:grabbing;}#lm-fc{position:absolute;top:0;left:0;}',
      '#lm-info{position:absolute;bottom:12px;left:50%;transform:translateX(-50%);padding:5px 14px;border-radius:999px;background:rgba(0,0,0,.8);color:rgba(255,255,255,.65);font-size:11px;pointer-events:none;white-space:nowrap;}',
      '#lm-zbtns{position:absolute;right:12px;top:12px;display:flex;flex-direction:column;gap:5px;}',
      '.lm-zb{width:34px;height:34px;border:none;border-radius:8px;background:rgba(255,255,255,.12);color:#fff;font-size:18px;cursor:pointer;}',
      '#lm-legend-bar{display:flex;flex-wrap:wrap;gap:3px 8px;padding:6px 14px;background:#13151f;border-top:1px solid rgba(255,255,255,.08);flex-shrink:0;align-items:center;}',
      '.lm-lb-item{display:flex;align-items:center;gap:3px;font-size:10px;color:rgba(255,255,255,.6);}',
      '.lm-lb-ico{width:14px;height:14px;border-radius:3px;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:9px;}',
      '#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-yes:hover{background:#5b6dd8;}',
      '#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-boost-conf-no:hover{background:rgba(255,255,255,.15);}',
      '#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-toast-bar{height:3px;border-radius:999px;background:rgba(255,255,255,.15);margin-top:4px;overflow:hidden;}',
      '.lm-toast-bar-fill{height:3px;border-radius:999px;background:linear-gradient(90deg,#4a90e2,#67e8f9);width:0%;transition:width .4s ease;}',
      '#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;}',
      /* Span з іконкою всередині клітинки */
      '.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;}',
      /* Visited — легка рамка щоб було видно */
      '.labyrinth-cell.lm-known.labyrinth-cell--visited .lm-cell-icon{opacity:0.9!important;}',
      /* Невідвідані відомі кімнати — підсвітка фону */
      '.labyrinth-cell.lm-known:not(.labyrinth-cell--visited):not(.labyrinth-cell--available):not(.labyrinth-cell--current){background:rgba(255,255,255,.07)!important;opacity:0.85!important;}',
    ].join('\n');
    document.head.appendChild(style);
  }

  // ============================================================
  // DOM
  // ============================================================
  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-rbtn">🔄 Обновить</button>'+
          '<button id="lm-pbtn">⛶ Полная карта</button>'+
        '</div>'+
      '</div>'+
      /* Статистичні картки */
      '<div id="lm-map-info">'+
        '<div class="lm-mi-card">'+
          '<div class="lm-mi-ico">🗺</div>'+
          '<div><div class="lm-mi-val" id="lm-mi-rooms-val">—</div><div class="lm-mi-lbl">комнат в базе</div></div>'+
        '</div>'+
        '<div class="lm-mi-card">'+
          '<div class="lm-mi-ico">⬇</div>'+
          '<div><div class="lm-mi-val" id="lm-mi-depth-val">—</div><div class="lm-mi-lbl">макс. глубина</div></div>'+
        '</div>'+
      '</div>'+
      /* Action bar — кнопки дій */
      '<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>'+
      /* Inline панель — відкривається під кнопками */
      '<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 (потрібен щоб уникнути null) */
      '<canvas id="lm-cvs" style="display:none"></canvas>'+
      '<div id="lm-map-info-hidden" style="display:none"><div id="lm-mi-rooms-val-h"></div><div id="lm-mi-depth-val-h"></div></div>'+
      '<div id="lm-st" style="display:none"></div>';

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

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

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

    // Тулпіт
    var tt = document.createElement('div'); tt.id='lm-tooltip'; document.body.appendChild(tt);

    // Boost confirm
    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);

    // Confirm 1
    var conf = document.createElement('div'); conf.id='lm-confirm';
    conf.innerHTML='<div id="lm-cbox"><h3>🔄 Обновить карту?</h3><p>Скрипт отправит ваши данные в облако и загрузит свежую карту.</p><p class="warn">⚠ Не обновляйте чаще чем раз в 10–15 минут.</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);

    // Confirm 2
    var conf2 = document.createElement('div'); conf2.id='lm-confirm2';
    conf2.innerHTML='<div id="lm-cbox2"><h3>🔄 Обновить всю информацию?</h3><p>Скрипт отправит ваш маршрут и стражей в облако, затем загрузит все страницы истории для подсчёта АСС.</p><p class="warn">⚠ Не нажимайте чаще чем раз в 10–15 минут.</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);

    // Toast
    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) {
    popEl.style.left = '0'; popEl.style.right = 'auto';
    var rect = popEl.getBoundingClientRect();
    var overflow = rect.right - (window.innerWidth - 8);
    if (overflow > 0) popEl.style.left = (-overflow) + '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'); // прихований, але нехай буде

    // ============================================================
    // INLINE ACTION BAR — стражі / клуб / історія в блоці
    // ============================================================
    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');  });

    // FIX: кліки всередині inline панелі НЕ спливають до document
    // інакше пагінація в історії закриває всю панель
    var inlinePanel = document.getElementById('lm-inline-panel');
    if (inlinePanel) {
      inlinePanel.addEventListener('click', function(e){ e.stopPropagation(); });
    }

    // Refresh (міні-карта)
    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 = localStorage.getItem(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');
      localStorage.setItem(LS_REFRESH_KEY, String(Date.now()));
      doRefresh();
    });

    document.getElementById('lm-pbtn').addEventListener('click', function(){ openFull(); });
    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 = localStorage.getItem(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');
      localStorage.setItem(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(){ fmScale=1; centerOn(0,0); drawFull(); }, 50); }
      else { fmScale=1; centerOn(0,0); drawFull(); }
    });
    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(){ fmScale=1; centerOn(c.x,c.y); drawFull(); }, 50); }
      else { fmScale=1; centerOn(c.x,c.y); drawFull(); }
    });
    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?'👣 Все игроки':'👣 Мой путь';
      drawFull();
    });

    // Фільтр
    document.getElementById('lm-bf').addEventListener('click', function(e){
      e.stopPropagation();
      var wasOpen = fpop.classList.contains('on');
      closeAllPopups();
      if (!wasOpen) { fpop.classList.add('on'); this.classList.add('active'); fixPopupPos(fpop); }
    });
    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();
      }
    });

    // Стражі і Клуб в модалці (кнопки в modal header)
    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); }
    });
    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); }
    });

    // Історія в модалці
    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);
      }
    });

    ['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();
      // Закриваємо inline панель при кліку поза wrap
      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;
        }
      }
    });

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  // ============================================================
  // ТОСТЕР
  // ============================================================
  var _toastEl = null, _toastTimer = null;

  function showToast(title, sub, pct) {
    if (!_toastEl) _toastEl = document.getElementById('lm-toast');
    if (!_toastEl) return;
    var pctNum = (pct === undefined) ? -1 : pct;
    var barHtml = pctNum >= 0 ? '<div class="lm-toast-bar"><div class="lm-toast-bar-fill" style="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('🗺 Отправка маршрута...', 'Шаг 1 из 4: загрузка вашего пути в облако', 10);
    var p = steps.length > 0 ? pushSteps(steps, SESSION_ID) : Promise.resolve();

    p.then(function() {
      showToast('👑 Отправка стражей...', 'Шаг 2 из 4: синхронизация стражей', 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('🌐 Обновление карты...', 'Шаг 3 из 4: загрузка облачной карты', 55);
      return new Promise(function(resolve) { loadCloud(resolve); });
    }).then(function() {
      // FIX: не очищаємо histCache щоб не скидати сторінку яку дивились
      // Скидаємо лише дату щоб примусово перезавантажити
      histCache = {};
      histTotal = null;
      // FIX: скидаємо на першу сторінку
      histCurPage = 1;
      localStorage.setItem(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('📜 Загрузка истории... стр. '+pageNum+' из '+total, 'Шаг 4 из 4: подсчёт АСС', pct);
              if (pageNum < total) loadPageSeq(pageNum + 1).then(resolve);
              else resolve();
            });
          }, pageNum === 1 ? 0 : 700);
        });
      }

      showToast('📜 Загрузка истории...', 'Шаг 4 из 4: сканирование страниц', 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);
      }
      var gpop = document.getElementById('lm-gpop');
      if (gpop && gpop.classList.contains('on')) loadGuardianPopup('user');
      var cpop = document.getElementById('lm-cpop');
      if (cpop && cpop.classList.contains('on')) loadGuardianPopup('club');

      showToast('✅ Обновление завершено!', 'Загружено стр.: '+Object.keys(histCache).length+' | Данные актуальны', 100);
      hideToast(3000);
      btn.classList.remove('loading'); btn.disabled = false;
    }).catch(function(e) {
      showToast('❌ Ошибка обновления', e.message || 'Попробуйте ещё раз');
      hideToast(4000);
      console.warn('[LabMap] fullRefresh err:', e);
      btn.classList.remove('loading'); btn.disabled = false;
    });
  }

  function doRefresh() {
    var btn=document.getElementById('lm-rbtn');
    btn.classList.add('loading'); btn.disabled=true;
    showToast('🗺 Отправка данных...', 'Загрузка маршрута в облако', 15);
    if (elSt) elSt.textContent='Отправка данных...';
    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('👑 Отправка стражей...', 'Синхронизация стражей', 45);
      if (elSt) elSt.textContent='Отправка стражей...';
      return new Promise(function(resolve){
        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('🌐 Загрузка карты...', 'Обновление облачной карты', 75);
      if (elSt) elSt.textContent='Загрузка карты...';
      loadCloud(function(){
        btn.classList.remove('loading'); btn.disabled=false;
        showToast('✅ Карта обновлена!', 'Комнат в облаке: '+Object.keys(cloudMap).length, 100);
        hideToast(2500);
        if(elSt) elSt.textContent='Облако: '+Object.keys(cloudMap).length+' | Всего: '+Object.keys(allRooms()).length;
      });
    }).catch(function(e){
      console.warn('[LabMap] refresh err:',e);
      btn.classList.remove('loading'); btn.disabled=false;
    });
  }

  // ============================================================
  // СТРАЖІ
  // ============================================================
  var guardianDataCache = {user:null, club:null};

  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();
    if(type==='user'){
      fetch('/user/'+encodeURIComponent(username)+'/')
      .then(function(r){ return r.text(); })
      .then(function(html){
        var doc=parser.parseFromString(html,'text/html');
        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(String.fromCharCode(10))[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 });
        });
        guardianDataCache.user=items; renderGuardianList(type,items,listEl,countEl);
      }).catch(function(){ listEl.textContent='Ошибка загрузки.'; });
    } else {
      fetch('/user/'+encodeURIComponent(username)+'/')
      .then(function(r){ return r.text(); })
      .then(function(html){
        var doc=parser.parseFromString(html,'text/html');
        var clubLink=doc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
        if(!clubLink){ var all=doc.querySelectorAll('a[href*="/clubs/"]'); for(var i=0;i<all.length;i++){ if(/\/clubs\/\d+\//.test(all[i].getAttribute('href')||'')){ clubLink=all[i]; break; } } }
        if(!clubLink){ listEl.textContent='Клуб не найден.'; return; }
        return fetch(clubLink.getAttribute('href')).then(function(r){ return r.text(); })
        .then(function(ch){
          var cd=parser.parseFromString(ch,'text/html');
          var items=[];
          cd.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 });
          });
          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();
        fmScale = 1;
        highlightRoom = {x: it.x, y: it.y, until: Date.now() + 3000};
        // FIX: openFull(true) — не центруємо на curPos, одразу centerOn стража
        openFull(true);
        // невелика затримка щоб modal встиг відрендеритись і offsetWidth був правильним
        setTimeout(function() { centerOn(it.x, it.y); }, 50);
      });
      listEl.appendChild(div);
    });
  }

  // ============================================================
  // INLINE ПАНЕЛЬ — завантаження стражів
  // ============================================================
  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();

    if (type === 'user') {
      fetch('/user/'+encodeURIComponent(username)+'/')
        .then(function(r){ return r.text(); })
        .then(function(html){
          var doc = parser.parseFromString(html, 'text/html');
          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
            });
          });
          guardianDataCache.user = items;
          renderWbGuardianList('user', items, container);
        }).catch(function(){ container.innerHTML = '<div class="lm-wi-empty">Ошибка загрузки</div>'; });
    } else {
      fetch('/user/'+encodeURIComponent(username)+'/')
        .then(function(r){ return r.text(); })
        .then(function(html){
          var doc = parser.parseFromString(html, 'text/html');
          var clubLink = doc.querySelector('.usn__club-item-top a[href*="/clubs/"]');
          if (!clubLink) {
            var all = doc.querySelectorAll('a[href*="/clubs/"]');
            for (var i=0;i<all.length;i++){ if(/\/clubs\/\d+\//.test(all[i].getAttribute('href')||'')){ clubLink=all[i]; break; } }
          }
          if (!clubLink) { container.innerHTML = '<div class="lm-wi-empty">Клуб не найден</div>'; return; }
          return fetch(clubLink.getAttribute('href')).then(function(r){ return r.text(); })
            .then(function(ch){
              var cd = parser.parseFromString(ch, 'text/html');
              var items = [];
              cd.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
                });
              });
              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);
        fmScale = 1;
        highlightRoom = {x: x, y: y, until: Date.now() + 3000};
        openFull(true);
        setTimeout(function(){ centerOn(x, y); }, 50);
      });
    });
  }

  // Render inline історія (скорочена версія renderHistPopup для inline блоку)
  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.total+'</div></div>'+
          '<div class="lm-sc-card"><div class="lm-sc-label">Откаты</div><div class="lm-sc-val lm-sc-bad">−'+stats.lostRooms+'</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);}); }
    });
  }

  // ============================================================
  // BUILD UI
  // ============================================================
  function buildUI() {
    if (document.getElementById('lm-wrap')) return;
    injectStyles(); buildDOM(); bindEvents();
    console.log('[LabMap] UI готово');
  }

  // ============================================================
  // АВТО-СИНК
  // ============================================================
  var LS_AUTO_SYNC_KEY = 'lm_last_auto_sync';
  var AUTO_SYNC_INTERVAL = 24 * 60 * 60 * 1000;

  function autoSyncIfNeeded() {
    var lastSync = parseInt(localStorage.getItem(LS_AUTO_SYNC_KEY) || '0', 10);
    var now = Date.now();
    if (now - lastSync < AUTO_SYNC_INTERVAL) {
      loadFromCache();
      drawMini();
      loadCloud(function() { drawMini(); if (fmOpen) drawFull(); });
      return;
    }
    setTimeout(function() {
      showToast('🔄 Авто-синхронизация...', 'Обновление данных (раз в 24ч)', 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); });
      }).then(function() {
        histCache = {}; histTotal = null; histCurPage = 1;
        localStorage.setItem(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) autoLoadPage(pageNum + 1).then(resolve);
                else resolve();
              });
            }, pageNum === 1 ? 0 : 700);
          });
        }
        return autoLoadPage(1);
      }).then(function() {
        localStorage.setItem(LS_AUTO_SYNC_KEY, String(Date.now()));
        showToast('✅ Авто-синхронизация завершена!', 'Следующая через 24 часа', 100);
        hideToast(3000); drawMini();
      }).catch(function(e) { console.warn('[LabMap] Авто-синк помилка:', e); hideToast(1000); });
    }, 3000);
  }

  // ============================================================
  // INIT — головний спостерігач за ходами
  // ============================================================
  function init() {
    console.log('[LabMap] init(), шагов:', mapData()&&mapData().steps?mapData().steps.length:0);
    buildUI(); drawMini(); injectTopBtn();
    autoSyncIfNeeded();
    // Патчимо карту одразу при старті (з retry якщо клітинки ще не з'явились)
    patchSiteMapDelayed();

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

    // ============================================================
    // ВІДСЛІДКОВУВАННЯ ХОДІВ — перехоплення кліку на клітинку
    // ============================================================
    var _stepProcessing = false;
    var _pendingStep = null; // {cx, cy} — позиція куди натиснули

    // Перехоплюємо клік на .labyrinth-cell--available
    // Це найнадійніший спосіб — ми знаємо ХОД ще до того як сайт відповів
    (function() {
      var mapEl = document.getElementById('labyrinthMap');
      if (!mapEl) { console.warn('[LabMap] labyrinthMap не знайдено'); return; }

      // Делегований обробник на весь #labyrinthMap
      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');

        // Знаходимо центр (current клітинку)
        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);

        console.log('[LabMap] клік на клітинку! target:', targetX, targetY);
        _pendingStep = {cx: targetX, cy: targetY};
        _stepProcessing = true;

        // Чекаємо поки сайт обробить хід і запише в labyrinthData
        waitForStep(targetX, targetY);
      }, true); // capture: true — перехоплюємо раніше ніж game.js

      // Також Observer як fallback для інших способів ходу (напр. клавіатура/touch)
      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;

        // Якщо вже обробляємо цей крок через клік — просто оновлюємо lastPos
        if (_stepProcessing && _pendingStep &&
            _pendingStep.cx === cx && _pendingStep.cy === cy) {
          lastPos = pos;
          return;
        }

        // Нова позиція без кліку (fallback)
        if (pos === lastPos) return;
        if (_stepProcessing) return;
        lastPos = pos;
        _stepProcessing = true;
        console.log('[LabMap] хід (observer fallback):', cx, cy);
        waitForStep(cx, cy);
      });

      obs.observe(mapEl, {
        subtree: true, attributes: true,
        attributeFilter: ['class'], attributeOldValue: true, childList: true
      });
      console.log('[LabMap] Click interceptor + Observer OK');
    })();

    // Polling — резервний для нестандартних ходів
    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;
        console.log('[LabMap] хід (poll fallback):', pos);
        waitForStep(c.x, c.y);
      }
    }, 1000);

    // Чекаємо поки сайт запише хід в labyrinthData, тоді оновлюємо все
    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 : [];

        // Перевіряємо: новий крок є в steps?
        var lastStep = steps.length > 0 ? steps[steps.length - 1] : null;
        var stepReady = lastStep && (lastStep.x + '_' + lastStep.y === targetKey);

        // DOM: current клітинка існує?
        var mapEl = document.getElementById('labyrinthMap');
        var curCell = mapEl && mapEl.querySelector('.labyrinth-cell--current');

        if ((stepReady && curCell) || tryCount >= maxTries) {
          // Дані готові — оновлюємо все
          lastN = steps.length;
          lastPos = targetKey;
          _pendingStep = null;

          invalidateRoomsCache();
          updateMapInfo();
          patchSiteMap();

          if (fmOpen) {
            drawFull();
            centerOn(cx, cy);
          }

          if (steps.length > 0) schedulePush(steps, SESSION_ID);

          _stepProcessing = false;
          console.log('[LabMap] ✓ хід записано:', cx, cy,
            '| steps:', steps.length, '| tries:', tryCount,
            stepReady ? '' : '(timeout)');
        } else {
          // Ще не готово — чекаємо
          setTimeout(tryUpdate, 150);
        }
      }

      // Перша перевірка через 100мс після кліку
      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) { console.log('[LabMap] Найдено! steps:',d2.steps.length); init(); }
      else if (tries<MAX_TRIES) setTimeout(check,300);
      else console.warn('[LabMap] Дані не знайдено після',MAX_TRIES,'спроб');
    }
    check();
  }
  wait();
})();