Animesss Labyrinth Map

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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