'use strict';
  const { user, isTeacher, isAdmin } = LS.initPage();
  window._simQuizAllowed = true; // default; overridden after permission fetch for students
  LS.showBoardIfAllowed();


  /* ════════════════════════════════
     SIM CATALOGUE  (defined after P_* consts below)
  ════════════════════════════════ */

  let _catFilter = 'all';
  var _disabledSimIds  = new Set();
  let _simModuleDisabled = false;

  function filterSims(cat, btn) {
    _catFilter = cat;
    document.querySelectorAll('.lab-filter').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    renderSims();
  }

  function renderSims() {
    // Контент-движок: мёрж код-реестра поверх legacy SIMS.
    // Порядок берём из SIMS; для мигрированных id используем манифест реестра;
    // registry-only записи добавляем в конец.
    const _reg = (window.LabRegistry ? window.LabRegistry.all() : []);
    const _regById = {};
    _reg.forEach(m => { _regById[m.id] = m; });
    const _seen = {};
    const _merged = [];
    SIMS.forEach(s => {
      _merged.push(s.id && _regById[s.id] ? _regById[s.id] : s);
      if (s.id) _seen[s.id] = 1;
    });
    _reg.forEach(m => { if (!_seen[m.id]) _merged.push(m); });

    const base = _catFilter === 'all' ? _merged : _merged.filter(s => s.cat === _catFilter);
    const list = base.filter(s => !s.id || !_disabledSimIds.has(s.id));
    document.getElementById('sim-grid').innerHTML = list.map(s => `
      <div class="sim-card ${s.id ? '' : 'soon'}" ${s.id ? `onclick="openSim('${s.id}')"` : ''}>
        ${window.LabRegistry ? window.LabRegistry.resolvePreview(s) : s.preview}
        <div class="sim-body">
          <div class="sim-cat ${s.cat}">${s.cat === 'math' ? '∑ Математика' : s.cat === 'chem' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M9 3h6m-4.5 0v5.5l-4 7.5a1 1 0 0 0 .9 1.5h8.2a1 1 0 0 0 .9-1.5l-4-7.5V3"/></svg> Химия' : s.cat === 'bio' ? '<svg class="ic" viewBox="0 0 24 24"><path d="M2 15c6.667-6 13.333 0 20-6"/><path d="M9 22c1.798-2 2.518-4 2.807-6"/><path d="M15 2c-1.798 2-2.518 4-2.807 6"/><path d="m17 6-2.5-2.5M14 8 13 7M7 18l2.5 2.5M3.5 14.5l.5.5M20 9l.5.5M6.5 12.5l1 1M16.5 10.5l1 1M10 16l1.5 1.5"/></svg> Биология' : s.cat === 'game' ? '<svg class="ic" viewBox="0 0 24 24"><line x1="6" y1="12" x2="10" y2="12"/><line x1="8" y1="10" x2="8" y2="14"/><line x1="15" y1="13" x2="15.01" y2="13"/><line x1="18" y1="11" x2="18.01" y2="11"/><rect x="2" y="6" width="20" height="12" rx="2"/></svg> Игры' : LS.icon('zap',14) + ' Физика'}</div>
          <div class="sim-title">${s.title}</div>
          <div class="sim-desc">${s.desc}</div>
        </div>
        ${!s.id ? '<div class="sim-soon-badge">Скоро</div>' : ''}
      </div>`).join('');
    if (window.lucide) lucide.createIcons();
  }

    /* ════════════════════════════════
     CARD PREVIEW SVGs — вынесены в /js/lab-previews.js (единый источник).
     Берём их по имени из window.__LabP; SIMS ниже ссылается на эти алиасы.
  ════════════════════════════════ */
  var __LP = window.__LabP || {};
  var P_GRAPH = __LP.P_GRAPH, P_TRANSFORM = __LP.P_TRANSFORM, P_TRIANGLE = __LP.P_TRIANGLE, P_CIRCLES = __LP.P_CIRCLES, P_QUADRATIC = __LP.P_QUADRATIC, P_3D = __LP.P_3D, P_PROB = __LP.P_PROB, P_NORMAL = __LP.P_NORMAL, P_TRIGCIRCLE = __LP.P_TRIGCIRCLE, P_PROJECTILE = __LP.P_PROJECTILE, P_PENDULUM = __LP.P_PENDULUM, P_COLLISION = __LP.P_COLLISION, P_CIRCUIT = __LP.P_CIRCUIT, P_MAGNETIC = __LP.P_MAGNETIC, P_FIELD = __LP.P_FIELD, P_LENS = __LP.P_LENS, P_REFRACTION = __LP.P_REFRACTION, P_MIRROR = __LP.P_MIRROR, P_ISOPROCESS = __LP.P_ISOPROCESS, P_GAS = __LP.P_GAS, P_NEWTON = __LP.P_NEWTON, P_SANDBOX = __LP.P_SANDBOX, P_HYDRO = __LP.P_HYDRO, P_KINETICS = __LP.P_KINETICS, P_EQUILIBRIUM = __LP.P_EQUILIBRIUM, P_ELECTROLYSIS = __LP.P_ELECTROLYSIS, P_BOHR = __LP.P_BOHR, P_ORBITALS = __LP.P_ORBITALS, P_PH = __LP.P_PH, P_CHEMSANDBOX = __LP.P_CHEMSANDBOX, P_STOICHIOMETRY = __LP.P_STOICHIOMETRY, P_PERIODIC = __LP.P_PERIODIC, P_CRYSTAL = __LP.P_CRYSTAL, P_CELLDIVISION = __LP.P_CELLDIVISION, P_PHOTOSYNTHESIS = __LP.P_PHOTOSYNTHESIS, P_ANGRYBIRDS = __LP.P_ANGRYBIRDS, P_WAVES = __LP.P_WAVES, P_RADIOACTIVE = __LP.P_RADIOACTIVE, P_HEATENGINE = __LP.P_HEATENGINE, P_GEOMETRY = __LP.P_GEOMETRY, P_RACE = __LP.P_RACE, P_LOGIC = __LP.P_LOGIC, P_QUALANALYSIS = __LP.P_QUALANALYSIS, P_ORGANIC = __LP.P_ORGANIC, P_SOLUTIONS = __LP.P_SOLUTIONS;

const SIMS = [
    /* ── Математика ── */
    { id: 'graph', cat: 'math',
      title: 'График функции',
      desc: 'Строй графики функций y = f(x) с параметрами, зумом и курсором координат.',
      preview: P_GRAPH },
    { id: 'graphtransform', cat: 'math',
      title: 'Трансформации графиков',
      desc: 'Наблюдай, как сдвиги, растяжения и отражения меняют вид функции y = a·f(kx+b)+c.',
      preview: P_TRANSFORM },
    { id: 'geometry', cat: 'math',
      title: 'Планиметрия',
      desc: 'Интерактивная среда построений: точки, отрезки, прямые, окружности, многоугольники. Полноценный чертёж с привязкой и измерениями.',
      preview: P_GEOMETRY },
    { id: 'triangle', cat: 'math',
      title: 'Геометрия треугольника',
      desc: 'Интерактивный треугольник: медианы, высоты, биссектрисы, вписанная и описанная окружности.',
      preview: P_TRIANGLE },
    { id: 'quadratic', cat: 'math',
      title: 'Корни квадратного уравнения',
      desc: 'Задай a, b, c ползунками — смотри дискриминант и корни анимированно на числовой оси.',
      preview: P_QUADRATIC },
    { id: 'stereo', cat: 'math',
      title: 'Стереометрия 3D',
      desc: 'Вращаемые объёмные фигуры: куб, пирамида, цилиндр, конус с формулами объёма и площади. Сечения, развёртка, вписанные/описанные сферы.',
      preview: P_3D },
    { id: 'probability', cat: 'math',
      title: 'Теория вероятностей',
      desc: 'Подброс монеты/кубика N раз — гистограмма частот и закон больших чисел в действии.',
      preview: P_PROB },
    { id: 'trigcircle', cat: 'math',
      title: 'Тригонометрическая окружность',
      desc: 'Единичная окружность с sin, cos, tg, ctg. Перетаскивай точку — все функции обновляются мгновенно. График синхронизирован.',
      preview: P_TRIGCIRCLE },
    { id: 'normaldist', cat: 'math',
      title: 'Нормальное распределение',
      desc: 'Двигай μ и σ ползунками — колокол Гаусса и площадь под кривой обновляются мгновенно.',
      preview: P_NORMAL },
    /* ── Физика ── */
    { id: 'projectile', cat: 'phys',
      title: 'Бросок тела',
      desc: 'Задай начальную скорость и угол — симулируй траекторию, дальность и высоту полёта.',
      preview: P_PROJECTILE },
    { id: 'pendulum', cat: 'phys',
      title: 'Маятник',
      desc: 'Регулируй длину и угол отклонения — изучай период колебаний и затухание.',
      preview: P_PENDULUM },
    { id: 'collision', cat: 'phys',
      title: 'Столкновение шаров',
      desc: 'Упругий и неупругий удар двух тел: законы сохранения импульса и энергии.',
      preview: P_COLLISION },
    { id: 'emfield', cat: 'phys',
      title: 'Электромагнитные поля',
      desc: 'Электрическое и магнитное поля в одной симуляции: заряды, токи, силовые линии, эквипотенциали, частица Лоренца.',
      preview: P_MAGNETIC },
    { id: 'circuit', cat: 'phys',
      title: 'Электрические цепи',
      desc: 'Конструктор цепей из резисторов и конденсаторов. Законы Ома и Кирхгофа наглядно.',
      preview: P_CIRCUIT },
    { id: 'hydrostatics', cat: 'phys',
      title: 'Гидростатика',
      desc: 'Давление жидкости P=ρgh, закон Архимеда, сообщающиеся сосуды, поверхностное натяжение и капиллярность.',
      preview: P_HYDRO },
    { id: 'dynamics', cat: 'phys',
      title: 'Динамика',
      desc: 'Законы Ньютона, песочница сил, наклонная плоскость — всё в одном интерактивном модуле.',
      preview: P_SANDBOX },
    { id: 'opticsbench', cat: 'phys',
      title: 'Оптическая скамья',
      desc: 'Линза, зеркала и преломление в одной симуляции: формула линзы, зеркальное отражение, закон Снеллиуса, ПВО, дисперсия.',
      preview: P_LENS },
    { id: 'isoprocess', cat: 'phys',
      title: 'Изопроцессы',
      desc: 'PV-диаграмма для четырёх изопроцессов идеального газа. Расчёт работы, теплоты и внутренней энергии.',
      preview: P_ISOPROCESS },
    { id: 'waves', cat: 'phys',
      title: 'Волны и звук',
      desc: 'Поперечные и продольные волны, суперпозиция, стоячие волны. Частота, амплитуда, фаза, гармоники.',
      preview: P_WAVES },
    { id: 'radioactive', cat: 'phys',
      title: 'Радиоактивный распад',
      desc: 'Период полураспада, цепочки распадов, активность. Визуализация ядер + кривая N(t). Радиоуглеродное датирование.',
      preview: P_RADIOACTIVE },
    { id: 'race', cat: 'phys',
      title: 'Гонка с задачами',
      desc: 'Кинематика 1D: встреча, догон, кто первый. Реши задачу — проверь анимацией и графиком x(t).',
      preview: P_RACE },
    { id: 'heatengine', cat: 'phys',
      title: 'Тепловые двигатели',
      desc: 'Циклы Карно, Отто, Дизеля, Брайтона. PV-диаграмма, поршень, КПД.',
      preview: P_HEATENGINE },
    { id: 'logic', cat: 'phys',
      title: 'Логические схемы',
      desc: 'Конструктор цифровых схем: И/ИЛИ/НЕ/XOR, триггеры, сумматоры. Авто-таблица истинности.',
      preview: P_LOGIC },
    /* ── Химия / Молекулярная физика ── */
    { id: 'molphys', cat: 'chem',
      title: 'Молекулярная физика',
      desc: 'Идеальный газ, броуновское движение, агрегатные состояния и диффузия — всё в одном модуле.',
      preview: P_GAS },
    { id: 'chemistry', cat: 'chem',
      title: 'Химические реакции',
      desc: 'Кинетика реакций, металл + кислота в колбе, ОВР с переносом электронов, ионный обмен — всё в одном модуле.',
      preview: P_KINETICS },
    { id: 'equilibrium', cat: 'chem',
      title: 'Химическое равновесие',
      desc: 'Прямая и обратная реакция, принцип Ле Шателье: изменяй T, P, концентрацию и наблюдай сдвиг.',
      preview: P_EQUILIBRIUM },
    { id: 'electrolysis', cat: 'chem',
      title: 'Электролиз',
      desc: 'Катод и анод в растворе электролита: движение ионов, выделение газа, закон Фарадея.',
      preview: P_ELECTROLYSIS },
    /* ── Скоро: Атомная структура ── */
    { id: 'bohratom', cat: 'chem',
      title: 'Атом Бора',
      desc: 'Электроны на орбитах, квантование энергии, эмиссия и поглощение фотонов при переходах.',
      preview: P_BOHR },
    { id: 'orbitals', cat: 'chem',
      title: 'Молекулярные орбитали',
      desc: 'H₂, H₂O — ковалентная связь, перекрывание орбиталей, 3D-визуализация электронных облаков.',
      preview: P_ORBITALS },
    /* ── Скоро: Визуальная химия ── */
    { id: 'titration', cat: 'chem',
      title: 'pH и кривая титрования',
      desc: 'Добавляй кислоту или щёлочь — наблюдай изменение pH, цвет раствора и кривую нейтрализации.',
      preview: P_PH },
    { id: 'chemsandbox', cat: 'chem',
      title: 'Химическая песочница',
      desc: 'Смешивай реагенты, наблюдай реакции: осадки, газы, изменение цвета. Свободное экспериментирование.',
      preview: P_CHEMSANDBOX },
    { id: 'stoichiometry', cat: 'chem',
      title: 'Стехиометрия',
      desc: 'Расчёты по уравнениям: масса, моль, объём. Лимитирующий реагент, выход. 10 реакций.',
      preview: P_STOICHIOMETRY },
    { id: 'crystal', cat: 'chem',
      title: 'Кристаллическая решётка',
      desc: 'NaCl, алмаз, металл — интерактивная 3D-решётка, типы связей, вращение структуры.',
      preview: P_CRYSTAL },
    { id: 'qualanalysis', cat: 'chem',
      title: 'Качественный анализ',
      desc: 'Определяй катионы и анионы качественными реакциями: осадки, газы, пламя. Два режима: guided и свободный эксперимент.',
      preview: P_QUALANALYSIS },
    { id: 'periodic', cat: 'chem',
      title: 'Периодическая таблица',
      desc: '118 элементов: подсветка по типу/блоку, карточка элемента, боровские оболочки, графики свойств.',
      preview: P_PERIODIC },
    { id: 'organic', cat: 'chem',
      title: 'Органическая химия',
      desc: 'Конструктор молекул с проверкой валентности, гомологические ряды с таблицей свойств, качественные реакции (бромная вода, KMnO₄, зеркало Толленса, Cu(OH)₂, FeCl₃, Na).',
      preview: P_ORGANIC },
    { id: 'solutions', cat: 'chem',
      title: 'Растворы',
      desc: 'Калькулятор раствора: ω, ν, C_M, плотность. Разбавление и смешивание с визуализацией. Кривые растворимости S(T) для 8 веществ + задача на перекристаллизацию.',
      preview: P_SOLUTIONS },
    /* ── Биология ── */
    { id: 'celldivision', cat: 'bio',
      title: 'Деление клетки',
      desc: 'Митоз и мейоз: анимированные фазы, хромосомы, веретено деления, ядерная оболочка.',
      preview: P_CELLDIVISION },
    { id: 'photosynthesis', cat: 'bio',
      title: 'Фотосинтез и дыхание',
      desc: 'Световые реакции в тилакоидах, цикл Кальвина, митохондриальное дыхание — молекулярная анимация.',
      preview: P_PHOTOSYNTHESIS },

    /* ── Игры ── */
    { id: 'angrybirds', cat: 'game',
      title: 'Angry Birds Physics',
      desc: 'Запускай птиц из рогатки, разрушай блоки, побеждай свиней. Реальная физика: гравитация, ветер, импульс. 6 уровней.',
      preview: P_ANGRYBIRDS },
  ];

  var _theoryOpen = false;
  function toggleTheory() {
    _theoryOpen = !_theoryOpen;
    document.getElementById('theory-panel').classList.toggle('open', _theoryOpen);
    const btn = document.getElementById('theory-toggle');
    btn.style.background = _theoryOpen ? 'rgba(155,93,229,0.15)' : '';
    btn.style.borderColor = _theoryOpen ? 'var(--violet)' : '';
    btn.style.color = _theoryOpen ? 'var(--violet)' : '';
  }

  function loadTheory(simId) {
    // Контент-движок: теория мигрированных симуляций берётся из манифеста реестра.
    const _rm = window.LabRegistry ? window.LabRegistry.get(simId) : null;
    const t = (_rm && _rm.theory) ? _rm.theory : THEORY[simId];
    const el = document.getElementById('theory-content');
    if (!t) { el.innerHTML = '<div class="tp-text" style="text-align:center;padding:40px 0;color:var(--text-3)">Теория для этой симуляции пока не добавлена</div>'; return; }
    let html = `<div class="tp-title">${LS.icon('book-open',16)} ${t.title}</div>`;
    for (const s of t.sections) {
      html += '<div class="tp-section">';
      if (s.head)    html += `<div class="tp-section-head">${s.head}</div>`;
      if (s.formula) html += `<div class="tp-formula" data-formula="${s.formula.replace(/"/g,'&quot;')}"></div>`;
      if (s.text)    html += `<div class="tp-text">${s.text}</div>`;
      if (s.vars)    html += `<div class="tp-var-list">${s.vars.map(([v,d]) => `<div class="tp-var"><b>${v}</b> — ${d}</div>`).join('')}</div>`;
      html += '</div>';
    }
    el.innerHTML = html;
    // render KaTeX formulas
    el.querySelectorAll('.tp-formula[data-formula]').forEach(div => {
      try { katex.render(div.dataset.formula, div, { displayMode: true, throwOnError: false }); }
      catch(e) { div.textContent = div.dataset.formula; }
    });
  }

  /* ── Контент-движок, Фаза 5: чип «Связано с программой» ──────────────────
     Подтягивает курикулумные связи симуляции (GET /api/lab/sims/:id/related) и
     рендерит чипы-ссылки рядом с заголовком симуляции. Самодостаточно: создаёт
     контейнер #sim-related динамически (без правок lab.html/CSS — меньше риск
     конфликта с параллельными сессиями). Тихо прячется, если связей нет/ошибка. */
  var _LAB_LINK_ICON = '<svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>';
  function _labRelEsc(s) {
    return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }
  function _ensureRelatedHost() {
    var host = document.getElementById('sim-related');
    if (host) return host;
    host = document.createElement('div');
    host.id = 'sim-related';
    host.style.cssText = 'display:none;align-items:center;gap:6px;flex-wrap:wrap;margin-left:14px;min-width:0';
    var title = document.getElementById('sim-topbar-title');
    if (title && title.parentNode) title.parentNode.insertBefore(host, title.nextSibling);
    return host;
  }
  function _loadRelated(simId) {
    var host = _ensureRelatedHost();
    host.style.display = 'none';
    host.innerHTML = '';
    if (!window.LS || !LS.api) return;
    LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related')
      .then(function (data) {
        var links = (data && data.links) || {};
        var all = [].concat(links.textbook || [], links.topic || [], links.kmap || [], links.question || []);
        if (!all.length) return;
        var chipBase = 'display:inline-flex;align-items:center;gap:4px;font-size:.72rem;padding:3px 9px;border-radius:999px;';
        var html = '<span style="font-size:.68rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:.05em">'
          + _LAB_LINK_ICON + ' Связано с программой</span>';
        all.forEach(function (l) {
          var label = _labRelEsc(l.label || (l.kind + ':' + l.ref_id));
          if (l.href) {
            html += '<a href="' + _labRelEsc(l.href) + '" title="Открыть в учебнике" style="' + chipBase
              + 'background:rgba(155,93,229,.14);color:var(--violet);text-decoration:none;border:1px solid rgba(155,93,229,.32)">' + label + '</a>';
          } else {
            html += '<span style="' + chipBase
              + 'background:rgba(255,255,255,.06);color:var(--text-2);border:1px solid rgba(255,255,255,.12)">' + label + '</span>';
          }
        });
        host.innerHTML = html;
        host.style.display = 'flex';
        if (window.lucide) lucide.createIcons();
      })
      .catch(function () { /* нет связей или ошибка — чип просто не показываем */ });
  }
  window._loadRelated = _loadRelated;

  /* ── embed mode + auto-open from ?sim= ── */
  const _qp = new URLSearchParams(location.search);
  var _embedMode = _qp.get('embed') === '1';
  var _autoSim   = _qp.get('sim');

  /* ── Sim state relay (embed mode only) ──────────────────────────────── */
  // Map simId → { getState, applyState } registered by openSim handlers
  const _simStateRegistry = {};

  function _registerSimState(simId, getState, applyState) {
    _simStateRegistry[simId] = { getState, applyState };
  }

  let _lastEmittedState = null;
  let _stateEmitInterval = null;

  function _startStateEmit(simId) {
    if (_stateEmitInterval) clearInterval(_stateEmitInterval);
    _lastEmittedState = null;
    _stateEmitInterval = setInterval(() => {
      const reg = _simStateRegistry[simId];
      if (!reg) return;
      try {
        const state = reg.getState();
        const json  = JSON.stringify(state);
        if (json === _lastEmittedState) return;
        _lastEmittedState = json;
        window.parent.postMessage({ type: 'sim_state', simId, state }, '*');
      } catch {}
    }, 400);
  }

  function _stopStateEmit() {
    if (_stateEmitInterval) { clearInterval(_stateEmitInterval); _stateEmitInterval = null; }
    _lastEmittedState = null;
  }

  // Receive apply_sim_state from parent (students)
  window.addEventListener('message', e => {
    if (!_embedMode) return;
    const d = e.data;
    if (!d || d.type !== 'apply_sim_state') return;
    const reg = _simStateRegistry[_autoSim];
    if (!reg) return;
    try {
      reg.applyState(d.state);
      _lastEmittedState = JSON.stringify(d.state); // suppress echo
    } catch {}
  });

  if (_embedMode) {
    document.querySelector('.sidebar').style.display = 'none';
    document.querySelector('.sb-content').style.marginLeft = '0';
    document.querySelector('.app-layout').classList.add('embed-mode');
    document.getElementById('lab-home').style.display = 'none';
    document.getElementById('theory-toggle').style.display = 'none';
    if (_autoSim) {
      document.getElementById('lab-sim').classList.add('open');
      document.querySelector('.sim-topbar').style.display = 'none';
      // defer until all external scripts are loaded
      window.addEventListener('load', () => openSim(_autoSim));
    }
  } else {
    /* init — fetch sim settings + permissions in parallel, then render */
    const _permFetch = (!isTeacher && !isAdmin)
      ? LS.api('/api/permissions/me').catch(() => null)
      : Promise.resolve(null);

    Promise.all([
      LS.api('/api/settings/sims').catch(() => ({})),
      _permFetch,
    ]).then(([cfg, permData]) => {
      _simModuleDisabled = cfg.module_disabled || false;
      _disabledSimIds    = new Set(cfg.disabled_ids || []);

      // check simulations.access for students
      if (!isTeacher && !isAdmin && permData) {
        const p = permData.permissions?.find(p => p.key === 'simulations.access');
        if (p && p.effective === false) {
          document.getElementById('sim-grid').innerHTML =
            `<div style="grid-column:1/-1;padding:60px 0;text-align:center;color:var(--text-3)">
              <div style="font-size:2rem;margin-bottom:12px"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
              <div style="font-family:'Unbounded',sans-serif;font-size:1rem;font-weight:800;color:var(--text);margin-bottom:6px">Доступ к симуляциям закрыт</div>
              <div style="font-size:.88rem">Администратор ограничил доступ к лаборатории</div>
            </div>`;
          return;
        }
        // store quiz permission for later use
        const qp = permData.permissions?.find(p => p.key === 'simulations.quiz');
        window._simQuizAllowed = !qp || qp.effective !== false;
      } else {
        window._simQuizAllowed = true;
      }

      if (_simModuleDisabled) {
        document.getElementById('sim-grid').innerHTML =
          `<div style="grid-column:1/-1;padding:60px 0;text-align:center;color:var(--text-3)">
            <div style="font-size:2rem;margin-bottom:12px"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
            <div style="font-family:'Unbounded',sans-serif;font-size:1rem;font-weight:800;color:var(--text);margin-bottom:6px">Модуль симуляций отключён</div>
            <div style="font-size:.88rem">Администратор временно отключил лабораторию</div>
          </div>`;
      } else {
        renderSims();
        if (_autoSim) openSim(_autoSim);
        // hash-router: activate sim from URL fragment after catalogue renders
        else _activateFromHash();
      }
    });
    lucide.createIcons();
    LS.notif.init();
  }

  /* ─── Hash router for sim deep-links ─────────────────────────────────────
     URL pattern: /lab#sim/<name>
     <name> matches SIMS[i].id (e.g. 'projectile', 'graph', 'chemsandbox').
     F5 restores sim. Browser back/forward switches between sims.
     Click on sim-card updates URL via wrapped openSim.
  ──────────────────────────────────────────────────────────────────────── */

  // Build valid-id set from SIMS catalogue (filters out "coming soon" entries)
  const _SIM_HASH_MAP = {};
  SIMS.forEach(function(s) { if (s.id) { _SIM_HASH_MAP[s.id] = s.id; } });
  // backward-compat aliases: old URLs redirect to unified emfield sim
  _SIM_HASH_MAP['magnetic']   = 'magnetic';
  _SIM_HASH_MAP['coulomb']    = 'coulomb';
  // backward-compat aliases: old optics sims redirect to opticsbench
  _SIM_HASH_MAP['thinlens']   = 'opticsbench';
  _SIM_HASH_MAP['mirrors']    = 'opticsbench';
  _SIM_HASH_MAP['refraction'] = 'opticsbench';

  var _routerNavigating = false;

  function _activateFromHash() {
    var m = (location.hash || '').match(/^#sim\/([\w-]+)/);
    if (!m) return false;
    var simName = m[1];
    if (!_SIM_HASH_MAP[simName]) {
      // eslint-disable-next-line no-console
      window.console && window.console.warn('lab-router: unknown sim', simName);
      return false;
    }
    openSim(simName);
    return true;
  }

  // Intercept openSim to push URL hash on user-initiated navigation
  var _origOpenSim = openSim;
  openSim = function(id) {
    _origOpenSim(id);
    if (!_routerNavigating && !_embedMode) {
      var baseId = id.includes(':') ? id.split(':')[0] : id;
      if (_SIM_HASH_MAP[baseId]) {
        _routerNavigating = true;
        location.hash = '#sim/' + baseId;
        // use setTimeout so hashchange fires after flag is set
        setTimeout(function() { _routerNavigating = false; }, 0);
      }
    }
  };

  /* ─── Sim Fade Transition + View Transitions API ─────────────────────────
     Wraps openSim with a fade-out (150ms) → swap → fade-in (200ms) sequence.
     If document.startViewTransition is available it is used for GPU-composited
     cross-fade; otherwise the manual .sim-fading CSS class is toggled.
     The hash-router wrap above runs synchronously during the transition so URL
     updates are not delayed.
  ──────────────────────────────────────────────────────────────────────── */
  var _hashRouterOpenSim = openSim; // reference after hash-router wrap
  openSim = function(id) {
    var labSim = document.getElementById('lab-sim');
    if (!labSim) { _hashRouterOpenSim(id); return; }

    function _doSwitch() {
      labSim.classList.add('sim-fading');
      setTimeout(function() {
        _hashRouterOpenSim(id);
        labSim.classList.remove('sim-fading');
      }, 150);
    }

    if (typeof document.startViewTransition === 'function' && !_embedMode) {
      document.startViewTransition(function() {
        _hashRouterOpenSim(id);
      });
    } else {
      _doSwitch();
    }
  };

  // Intercept closeSim to clear hash when returning to home grid
  var _origCloseSim = closeSim;
  closeSim = function() {
    _origCloseSim();
    if (!_embedMode) {
      _routerNavigating = true;
      history.pushState(null, '', location.pathname + location.search);
      setTimeout(function() { _routerNavigating = false; }, 0);
    }
  };

  // Browser back/forward navigation
  window.addEventListener('hashchange', function() {
    if (_routerNavigating) return;
    var hasHash = _activateFromHash();
    if (!hasHash && document.getElementById('lab-sim').classList.contains('open')) {
      _origCloseSim();
    }
  });
