'use strict';
// admin.html — main script (extracted from inline <script> for cleaner HTML caching)
// Order of operation preserved: loads after api.js + sidebar.js, before notifications/search/mobile

    const { user, isTeacher, isAdmin } = LS.initPage();
    if (!isTeacher) { window.location.href = '/dashboard'; throw new Error(); }
    document.getElementById('page-sub').textContent =
      isAdmin ? 'Администратор · полный доступ' : 'Учитель · просмотр статистики';

    /* Admin-only tabs: show to everyone for discoverability, but lock for non-admins */
    const ADMIN_ONLY_TABS = ['btn-tab-subjects','btn-tab-permissions','btn-tab-shop','btn-tab-gam','btn-tab-tpl','btn-tab-sims','btn-tab-games'];
    const lockSvg = '<svg class="adm-lock" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
    ADMIN_ONLY_TABS.forEach(id => {
      const el = document.getElementById(id);
      if (!el) return;
      el.style.display = '';               // always visible now
      if (!isAdmin) {
        el.classList.add('locked');
        el.title = 'Только для администраторов';
        el.insertAdjacentHTML('beforeend', lockSvg);
      }
    });
    // Система group: visible to everyone too
    const sysGroup = document.getElementById('admin-nav-system-group');
    if (sysGroup) sysGroup.style.display = '';

    /* Collapsible nav groups — state persisted in localStorage */
    window.toggleAdminGroup = function (slug) {
      const g = document.querySelector(`.admin-nav-group[data-ng="${slug}"]`);
      if (!g) return;
      const collapsed = g.classList.toggle('collapsed');
      try { localStorage.setItem('ls_adm_g_' + slug, collapsed ? '1' : '0'); } catch {}
    };
    // Restore collapsed state on page load
    document.querySelectorAll('.admin-nav-group').forEach(g => {
      const slug = g.dataset.ng;
      try {
        if (localStorage.getItem('ls_adm_g_' + slug) === '1') g.classList.add('collapsed');
      } catch {}
    });
    LS.showBoardIfAllowed();
    LS.hideDisabledFeatures?.();
    LS.notif?.init();
    const MODES   = { exam:'Экзамен', practice:'Тренировка', repeat:'Обычный', ct:'ЦТ/ЦЭ', topic:'По теме', random:'Случайный' };
    const DIFFS   = { 1:'Лёгкий', 2:'Средний', 3:'Сложный' };

    function pctClass(p) { return p === null ? '' : p >= 75 ? 'pct-hi' : p >= 50 ? 'pct-mid' : 'pct-lo'; }
    function fmtDate(d) { return new Date(d).toLocaleDateString('ru',{day:'numeric',month:'short',year:'numeric'}); }
    function fmtTime(sec) {
      if (!sec || sec < 0) return '—';
      const m = Math.floor(sec / 60), s = sec % 60;
      return m ? `${m} мин ${s} сек` : `${s} сек`;
    }

    /* ─── Tabs ─── */
    let questionsInited = false, testsInited = false, assignmentsInited = false, usersInited = false, sessionsInited = false, subjectsInited = false, permissionsInited = false, shopInited = false, gamInited = false, tplInited = false, simsInited = false, gamesInited = false, sublogInited = false;
    function switchTab(btn) {
      if (btn.classList.contains('locked')) {
        LS.toast('Этот раздел доступен только администраторам', 'warn');
        return;
      }
      document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
      document.querySelectorAll('.admin-nav-item').forEach(b => b.classList.remove('active'));
      const name = btn.dataset.tab;
      document.getElementById('tab-' + name).classList.add('active');
      btn.classList.add('active');
      if (name === 'questions'   && !questionsInited)   { questionsInited = true;   loadQuestions(); }
      if (name === 'tests'       && !testsInited)       { testsInited = true;       loadTests(); }
      if (name === 'assignments' && !assignmentsInited) { assignmentsInited = true; loadAssignments(); }
      if (name === 'subjects'    && !subjectsInited)    { subjectsInited = true;    loadSubjectConfig(); }
      if (name === 'users'       && !usersInited)       { usersInited = true;       loadUsers(); }
      if (name === 'sessions'    && !sessionsInited)    { sessionsInited = true;    loadSessions(); }
      if (name === 'permissions' && !permissionsInited) { permissionsInited = true; loadPermissions(); }
      if (name === 'shop'        && !shopInited)        { shopInited = true;        loadShopAdmin(); }
      if (name === 'gam'         && !gamInited)         { gamInited = true;         loadGamAdmin(); }
      if (name === 'tpl'         && !tplInited)         { tplInited = true;         loadTplAdmin(); }
      if (name === 'sims'        && !simsInited)        { simsInited = true;         loadSimsAdmin(); }
      if (name === 'games'       && !gamesInited)       { gamesInited = true;        loadGamesAdmin(); loadFsFeatures(); }
      if (name === 'sublog'      && !sublogInited)      { sublogInited = true;       loadSubmissionLog(); }
    }

    /* Переход к вопросам конкретного предмета с открытием формы */
    async function goAddQuestion(slug) {
      // переключаем на вкладку Вопросы
      const qBtn = document.querySelector('[data-tab="questions"]');
      switchTab(qBtn);
      // выставляем предмет в фильтре и обновляем список
      document.getElementById('q-subject').value = slug;
      if (!questionsInited) { questionsInited = true; }
      await loadQuestions();
      // открываем форму с предзаполненным предметом
      openQModal();
      document.getElementById('qf-subject').value = slug;
      await loadQModalTopics();
    }

    /* ════════════════════════════════════════════════
       СТАТИСТИКА
    ════════════════════════════════════════════════ */
    async function loadStats() {
      try {
        const s = await LS.adminGetStats();
        document.getElementById('stats-grid').innerHTML = `
          <div class="stat-card" style="--stat-top:var(--violet)">
            <div class="stat-card-icon" style="background:rgba(155,93,229,0.1)"><i data-lucide="users" class="stat-icon"></i></div>
            <div class="stat-val violet">${s.totalUsers}</div>
            <div class="stat-label">Пользователей</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--cyan)">
            <div class="stat-card-icon" style="background:rgba(6,214,224,0.1)"><i data-lucide="file-text" class="stat-icon"></i></div>
            <div class="stat-val cyan">${s.totalTests}</div>
            <div class="stat-label">Тестов пройдено</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--green)">
            <div class="stat-card-icon" style="background:rgba(6,214,100,0.1)"><i data-lucide="target" class="stat-icon"></i></div>
            <div class="stat-val green">${s.avgScore ?? '—'}%</div>
            <div class="stat-label">Средний результат</div>
          </div>`;
        if (window.lucide) lucide.createIcons();
        const subjEl = document.getElementById('subj-stats');
        if (!s.bySubject?.length) { subjEl.innerHTML = '<div class="empty">Нет данных</div>'; return; }
        subjEl.innerHTML = s.bySubject.map(b => {
          const pct = b.avg_pct ?? 0;
          const barColor = pct >= 75 ? 'var(--green)' : pct >= 50 ? 'var(--amber)' : 'var(--pink)';
          return `<div class="subj-stat">
            <div><div class="subj-stat-name">${esc(b.name)}</div><div class="subj-stat-info">${b.tests} тестов</div></div>
            <div>
              <div class="subj-stat-pct">${b.avg_pct ?? '—'}%</div>
              <div style="width:60px;height:3px;background:rgba(15,23,42,0.06);border-radius:99px;margin-top:5px;overflow:hidden"><div style="width:${pct}%;height:100%;background:${barColor};border-radius:99px"></div></div>
            </div>
          </div>`;
        }).join('');
      } catch (e) {
        LS.state.error(document.getElementById('stats-grid'), e, loadStats);
      }
    }

    /* ════════════════════════════════════════════════
       РЕДАКТОР ВОПРОСОВ
    ════════════════════════════════════════════════ */
    let allQuestions = [];
    let editingQId   = null;
    let openQId      = null;
    let _topicMap    = {};   // topic name (lower) <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> id, for current subject

    /* ─── KaTeX rendering ─── */
    const KATEX_OPTS = {
      delimiters: [
        { left: '\\(', right: '\\)', display: false },
        { left: '\\[', right: '\\]', display: true  },
      ],
      throwOnError: false,
    };
    function renderMath(el) {
      if (!el) return;
      const run = () => { if (window.renderMathInElement) renderMathInElement(el, KATEX_OPTS); };
      if (window._katexReady) run(); else window._katexCb = run;
    }

    function updateCharCounter(el, cntId, max) {
      const n = el.value.length;
      const cnt = document.getElementById(cntId);
      if (!cnt) return;
      cnt.textContent = `${n} / ${max}`;
      cnt.className = 'char-counter' + (n > max * 0.9 ? ' warn' : '') + (n >= max ? ' over' : '');
    }

    async function onQSubjectChange() {
      const slug = document.getElementById('q-subject').value;
      const sel  = document.getElementById('q-topic');
      sel.innerHTML = '<option value="">Все темы</option>';
      if (slug) {
        try {
          const topics = await LS.getTopics(slug);
          topics.forEach(t => sel.appendChild(new Option(t.name, t.id)));
        } catch {}
      }
      loadQuestions();
    }

    async function loadQuestions() {
      const subject  = document.getElementById('q-subject').value;
      const topic_id = document.getElementById('q-topic').value;
      const sort     = document.getElementById('q-sort').value;
      const wrap     = document.getElementById('q-list-wrap');
      wrap.innerHTML = LS.skeleton(5);
      try {
        allQuestions = await LS.getQuestions(subject || null, topic_id || null, sort);
        renderQuestions();
      } catch (e) {
        wrap.innerHTML = `<div class="error">Ошибка загрузки: ${esc(e.message)}</div>`;
      }
    }

    function renderQuestions() {
      const search   = document.getElementById('q-search').value.toLowerCase();
      const filtered = search
        ? allQuestions.filter(q => q.text.toLowerCase().includes(search) || (q.topic||'').toLowerCase().includes(search))
        : allQuestions;

      document.getElementById('q-count').textContent = `${filtered.length} вопросов`;

      if (!filtered.length) {
        document.getElementById('q-list-wrap').innerHTML = '<div class="empty">Вопросов не найдено</div>';
        return;
      }

      const wrap = document.getElementById('q-list-wrap');
      wrap.innerHTML =
        `<div class="q-list">${filtered.map(q => {
          const diffCls = `diff-${q.difficulty}`;
          const optsHtml = (q.options || []).map(o =>
            `<div class="q-opt-row ${o.is_correct ? 'correct' : ''}">
               <span class="q-opt-icon">${o.is_correct ? '<i data-lucide="check" style="width:13px;height:13px"></i>' : '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg>'}</span>${esc(o.text)}
             </div>`).join('');
          const explHtml = q.explanation
            ? `<div class="q-expl"><strong>Пояснение:</strong> ${esc(q.explanation)}</div>` : '';
          return `<div class="q-card" id="qcard-${q.id}">
            <div class="q-card-head">
              <span class="q-card-num">#${q.id}</span>
              <div class="q-card-body" onclick="toggleQDetail(${q.id})">
                <div class="q-card-text">${esc(q.text)}</div>
                <div class="q-card-meta">
                  ${q.subject_name ? `<span class="q-badge q-badge-subj">${esc(q.subject_name)}</span>` : ''}
                  ${q.topic ? `<span class="q-badge q-badge-topic">${esc(q.topic)}</span>` : ''}
                  <span class="q-badge ${diffCls}">${DIFFS[q.difficulty]||q.difficulty}</span>
                  <span style="font-size:0.72rem;color:var(--text-3);background:rgba(15,23,42,0.05);padding:2px 7px;border-radius:999px">${{single:'Один',multi:'Несколько',true_false:'Верно/Неверно',short_answer:'Краткий',matching:'Сопост.'}[q.type]||q.type||'Один'}</span>
                  <span style="font-size:0.75rem;color:var(--text-3)">${q.options?.length||0} вар.</span>
                </div>
              </div>
              <div class="q-card-actions">
                <button class="btn-edit-q" onclick="editQ(${q.id})">Изменить</button>
                <button class="btn-dup-q"  onclick="dupQ(${q.id})" title="Дублировать">⧉</button>
                <button class="btn-del-q"  onclick="deleteQ(${q.id})" title="Удалить"><i data-lucide="x" style="width:14px;height:14px"></i></button>
              </div>
            </div>
            <div class="q-card-detail" id="qdetail-${q.id}">
              ${optsHtml}${explHtml}
            </div>
          </div>`;
        }).join('')}</div>`;
      renderMath(wrap);
      if (window.lucide) lucide.createIcons();
    }

    function toggleQDetail(id) {
      if (openQId === id) {
        document.getElementById('qdetail-' + id)?.classList.remove('open');
        openQId = null; return;
      }
      if (openQId) document.getElementById('qdetail-' + openQId)?.classList.remove('open');
      document.getElementById('qdetail-' + id)?.classList.add('open');
      openQId = id;
    }

    async function dupQ(id) {
      try {
        const { id: newId } = await LS.duplicateQuestion(id);
        await loadQuestions();
        // scroll to new card
        setTimeout(() => document.getElementById('qcard-' + newId)?.scrollIntoView({ behavior:'smooth', block:'center' }), 300);
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    async function deleteQ(id) {
      if (!await LS.confirm(`Удалить вопрос #${id}?`, { title: 'Удалить вопрос', confirmText: 'Удалить' })) return;
      try {
        await LS.deleteQuestion(id);
        allQuestions = allQuestions.filter(q => q.id !== id);
        renderQuestions();
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ─── Question type ─── */
    let _currentType = 'single';
    let _matchPairs = []; // [{left:'', right:''}]

    function setQType(type) {
      _currentType = type;
      document.querySelectorAll('[data-type]').forEach(b => b.classList.toggle('active', b.dataset.type === type));
      const isMatching   = type === 'matching';
      const isShort      = type === 'short_answer';
      const showOpts     = !isShort && !isMatching;
      const optsHeader = document.getElementById('qf-opts-header');
      if (optsHeader) optsHeader.style.display = showOpts ? '' : 'none';
      document.getElementById('qf-opts').style.display       = showOpts ? '' : 'none';
      document.getElementById('qf-short-wrap').style.display = isShort    ? '' : 'none';
      document.getElementById('qf-match-wrap').style.display = isMatching ? '' : 'none';
      document.getElementById('btn-add-opt').style.display   = showOpts && type !== 'true_false' ? '' : 'none';

      if (type === 'true_false') {
        initOpts([{ text:'Верно', is_correct:false }, { text:'Неверно', is_correct:false }]);
      } else if (isShort) {
        _opts = [];
      } else if (isMatching) {
        _opts = [];
        if (_matchPairs.length === 0) _matchPairs = [{left:'',right:''},{left:'',right:''},{left:'',right:''}];
        renderMatchRows();
      } else {
        if (_opts.length === 0 || _opts[0]?.text === 'Верно') initOpts([{},{},{},{}]);
        else renderOptRows(_opts);
      }
    }

    function renderMatchRows() {
      const cont = document.getElementById('qf-match-rows');
      cont.innerHTML = _matchPairs.map((p, i) => `
        <div style="display:grid;grid-template-columns:1fr 1fr auto;gap:8px;margin-bottom:8px" data-mi="${i}">
          <input type="text" class="form-ctrl" placeholder="Элемент…" value="${esc(p.left)}"
            oninput="_matchPairs[${i}].left=this.value" style="margin:0" />
          <input type="text" class="form-ctrl" placeholder="Пара к нему…" value="${esc(p.right)}"
            oninput="_matchPairs[${i}].right=this.value" style="margin:0" />
          <button type="button" onclick="removeMatchPair(${i})" style="border:none;background:none;color:var(--text-3);cursor:pointer;padding:0 6px;display:flex;align-items:center" title="Удалить"><i data-lucide="x" style="width:15px;height:15px"></i></button>
        </div>`).join('');
      if (window.lucide) lucide.createIcons();
    }

    function addMatchPair() {
      _matchPairs.push({left:'',right:''});
      renderMatchRows();
    }

    function removeMatchPair(i) {
      _matchPairs.splice(i, 1);
      renderMatchRows();
    }

    /* ─── Formula bar ─── */
    let _focusedInput = null;
    document.addEventListener('focusin', e => {
      if (e.target.closest && e.target.closest('#q-modal') &&
          (e.target.tagName === 'TEXTAREA' || (e.target.tagName === 'INPUT' && e.target.type === 'text'))) {
        _focusedInput = e.target;
      }
    });

    function ins(latex) {
      const el = _focusedInput || document.getElementById('qf-text');
      if (!el) return;
      const s = el.selectionStart ?? el.value.length;
      const e2= el.selectionEnd   ?? el.value.length;
      const before = el.value.slice(0, s), after = el.value.slice(e2);
      const opens  = (before.match(/\\\(/g)||[]).length;
      const closes = (before.match(/\\\)/g)||[]).length;
      const insert = opens > closes ? latex : `\\(${latex}\\)`;
      el.value = before + insert + after;
      el.setSelectionRange(s + insert.length, s + insert.length);
      el.focus();
      updateQPreview();
    }

    function wrapMath() {
      const el = _focusedInput || document.getElementById('qf-text');
      if (!el) return;
      const s = el.selectionStart, e2 = el.selectionEnd;
      const sel = el.value.slice(s, e2) || 'x';
      el.value = el.value.slice(0, s) + `\\(${sel}\\)` + el.value.slice(e2);
      el.focus();
      updateQPreview();
    }

    /* ─── Live preview ─── */
    let _prevTimer = null;
    function updateQPreview() {
      clearTimeout(_prevTimer);
      _prevTimer = setTimeout(() => {
        const text = (document.getElementById('qf-text').value || '').trim();
        const el   = document.getElementById('q-preview-text');
        const wrap = document.getElementById('q-preview-wrap');
        // Hide preview entirely when text is empty — saves vertical space in modal
        wrap.classList.toggle('hidden', !text);
        if (!text) return;
        el.textContent = text;
        renderMath(el);
      }, 150);
    }
    // Formula bar toggle (default collapsed)
    window.toggleFormulaBar = function () {
      const bar = document.getElementById('formula-bar');
      const btn = document.getElementById('qf-fml-toggle');
      const open = bar.classList.toggle('visible');
      btn.classList.toggle('open', open);
    };
    // Wire textarea input to preview
    setTimeout(() => {
      const ta = document.getElementById('qf-text');
      if (ta) ta.addEventListener('input', updateQPreview);
    }, 0);

    /* ─── Dynamic options ─── */
    const OPT_LETTERS = 'АБВГДЕ';

    function renderOptRows(opts) {
      const grid    = document.getElementById('qf-opts');
      const isMulti = _currentType === 'multi';
      grid.innerHTML = opts.map((o, i) => `
        <div class="opt-row${o.is_correct ? ' opt-correct' : ''}" data-i="${i}">
          <span class="opt-letter">${OPT_LETTERS[i]}</span>
          ${isMulti
            ? `<input type="checkbox" class="opt-radio" value="${i}" ${o.is_correct ? 'checked' : ''}
                   onchange="onCheckChange(${i}, this.checked)" />`
            : `<input type="radio" name="qf-correct" class="opt-radio" value="${i}" ${o.is_correct ? 'checked' : ''}
                   onchange="onRadioChange(${i})" />`}
          <input type="text" class="opt-input" placeholder="Вариант ${OPT_LETTERS[i]}"
                 value="${esc(o.text||'')}" oninput="syncOptText(${i}, this.value)" />
          ${opts.length > 2
            ? `<button type="button" class="btn-rem-opt" onclick="removeOpt(${i})" title="Удалить">−</button>`
            : '<span style="width:24px;flex-shrink:0"></span>'}
        </div>`).join('');
      document.getElementById('btn-add-opt').style.display = opts.length >= 6 ? 'none' : '';
    }

    function onCheckChange(idx, checked) {
      _opts[idx].is_correct = checked;
      document.querySelector(`#qf-opts .opt-row[data-i="${idx}"]`)?.classList.toggle('opt-correct', checked);
    }

    let _opts = [];  // current options state

    function initOpts(opts) {
      _opts = opts.length ? opts.map(o => ({ text: o.text||'', is_correct: !!o.is_correct }))
                          : [{text:'',is_correct:false},{text:'',is_correct:false},{text:'',is_correct:false},{text:'',is_correct:false}];
      renderOptRows(_opts);
    }

    function onRadioChange(idx) {
      _opts.forEach((o, i) => o.is_correct = (i === idx));
      renderOptRows(_opts);
    }

    function syncOptText(idx, val) { _opts[idx].text = val; }

    function addOpt() {
      if (_opts.length >= 6) return;
      _opts.push({ text: '', is_correct: false });
      renderOptRows(_opts);
      // focus new input
      const rows = document.querySelectorAll('#qf-opts .opt-row');
      rows[rows.length - 1]?.querySelector('input[type=text]')?.focus();
    }

    function removeOpt(idx) {
      if (_opts.length <= 2) return;
      const wasCorrect = _opts[idx].is_correct;
      _opts.splice(idx, 1);
      if (wasCorrect && _opts.length > 0) _opts[0].is_correct = true;
      renderOptRows(_opts);
    }

    /* ─── Modal ─── */
    function openQModal(q = null) {
      editingQId = q ? q.id : null;
      document.getElementById('q-modal-title').textContent = q ? `Редактировать вопрос #${q.id}` : 'Добавить вопрос';
      const textEl = document.getElementById('qf-text');
      textEl.value = q?.text || '';
      updateCharCounter(textEl, 'qf-text-cnt', 500);
      document.getElementById('qf-explanation').value   = q?.explanation || '';
      document.getElementById('qf-difficulty').value    = q?.difficulty ?? 2;
      document.getElementById('qf-subject').value       = q?.subject_slug || '';
      document.getElementById('qf-topic-text').value    = q?.topic || '';
      document.getElementById('qf-correct-text').value  = q?.correct_text || '';
      document.getElementById('qf-error').textContent   = '';
      const imgVal = q?.image || '';
      document.getElementById('qf-image').value = imgVal;
      updateImagePreview(imgVal);

      if (q?.type === 'matching') {
        _matchPairs = (q.options || []).map(o => ({ left: o.text, right: o.match_pair || '' }));
        if (!_matchPairs.length) _matchPairs = [{left:'',right:''},{left:'',right:''},{left:'',right:''}];
      } else {
        _matchPairs = [];
      }
      setQType(q?.type || 'single');
      if (q?.type !== 'matching') initOpts(q?.options || []);
      updateQPreview();
      loadQModalTopics();
      document.getElementById('q-modal').classList.add('open');
      setTimeout(() => textEl.focus(), 80);
    }

    function editQ(id) {
      const q = allQuestions.find(x => x.id === id);
      if (q) openQModal(q);
    }

    function closeQModal() {
      document.getElementById('q-modal').classList.remove('open');
      editingQId = null;
    }

    async function loadQModalTopics() {
      const slug = document.getElementById('qf-subject').value;
      const dl   = document.getElementById('qf-topic-list');
      dl.innerHTML = '';
      _topicMap = {};
      if (!slug) return;
      try {
        const topics = await LS.getTopics(slug);
        topics.forEach(t => {
          dl.appendChild(new Option(t.name));
          _topicMap[t.name.toLowerCase()] = t.id;
        });
      } catch {}
    }

    async function saveQuestion() {
      const text         = document.getElementById('qf-text').value.trim();
      const explanation  = document.getElementById('qf-explanation').value.trim();
      const difficulty   = Number(document.getElementById('qf-difficulty').value);
      const subject_slug = document.getElementById('qf-subject').value;
      const topicText    = document.getElementById('qf-topic-text').value.trim();
      const type         = _currentType;
      const errEl        = document.getElementById('qf-error');
      errEl.textContent  = '';

      if (!subject_slug) { errEl.textContent = 'Выберите предмет'; return; }
      if (!text)         { errEl.textContent = 'Введите текст вопроса'; return; }

      let options      = null;
      let correct_text = null;

      if (type === 'short_answer') {
        correct_text = document.getElementById('qf-correct-text').value.trim();
        if (!correct_text) { errEl.textContent = 'Введите правильный ответ'; return; }
      } else if (type === 'matching') {
        // sync text from DOM inputs
        document.querySelectorAll('#qf-match-rows [data-mi]').forEach((row, i) => {
          const [l, r] = row.querySelectorAll('input');
          if (_matchPairs[i]) { _matchPairs[i].left = l.value.trim(); _matchPairs[i].right = r.value.trim(); }
        });
        if (_matchPairs.length < 2) { errEl.textContent = 'Нужно минимум 2 пары'; return; }
        if (_matchPairs.some(p => !p.left || !p.right)) { errEl.textContent = 'Заполните все пары'; return; }
        options = _matchPairs.map(p => ({ text: p.left, match_pair: p.right, is_correct: 0 }));
      } else {
        // sync any unsaved text from DOM inputs
        document.querySelectorAll('#qf-opts .opt-row').forEach((row, i) => {
          if (_opts[i]) _opts[i].text = row.querySelector('input[type=text]').value.trim();
        });
        options = _opts.map(o => ({ text: o.text, is_correct: o.is_correct }));
        if (options.length < 2)           { errEl.textContent = 'Нужно минимум 2 варианта ответа'; return; }
        if (options.some(o => !o.text))   { errEl.textContent = 'Заполните все варианты ответов'; return; }
        if (!options.some(o => o.is_correct)) { errEl.textContent = 'Отметьте правильный ответ'; return; }
      }

      // resolve topic: use id if known, else send topic_name for find-or-create
      const knownId    = _topicMap[topicText.toLowerCase()];
      const topic_id   = knownId || null;
      const topic_name = !knownId && topicText ? topicText : null;

      const image = document.getElementById('qf-image').value.trim() || null;

      const btn = document.getElementById('qf-save');
      btn.disabled = true; btn.textContent = 'Сохранение…';
      try {
        if (editingQId) {
          await LS.updateQuestion(editingQId, { text, type, correct_text, difficulty, explanation: explanation||null, topic_id, topic_name, options, image });
        } else {
          await LS.createQuestion({ subject_slug, topic_id, topic_name, text, type, correct_text, difficulty, explanation: explanation||null, options, image });
        }
        closeQModal();
        loadQuestions();
      } catch (e) {
        errEl.textContent = 'Ошибка: ' + e.message;
      } finally {
        btn.disabled = false; btn.textContent = 'Сохранить';
      }
    }

    /* ── Image upload & preview ── */
    function updateImagePreview(url) {
      const wrap = document.getElementById('qf-image-preview');
      const img  = document.getElementById('qf-image-img');
      if (url) { img.src = url; wrap.classList.add('visible'); }
      else     { wrap.classList.remove('visible'); img.src = ''; }
    }

    function clearQuestionImage() {
      document.getElementById('qf-image').value = '';
      updateImagePreview('');
    }

    async function handleImageFileSelect(input) {
      const file = input.files[0];
      if (!file) return;
      input.value = '';
      const btn = document.getElementById('btn-img-upload');
      const lbl = document.getElementById('btn-img-upload-lbl');
      btn.disabled = true;
      lbl.textContent = 'Загрузка…';
      try {
        const fd = new FormData();
        fd.append('file', file);
        fd.append('title', 'Question image: ' + file.name);
        fd.append('is_public', '1');
        const res = await fetch('/api/files', {
          method: 'POST',
          headers: { Authorization: 'Bearer ' + localStorage.getItem('ls_token') },
          body: fd
        });
        if (!res.ok) throw new Error((await res.json()).error || res.statusText);
        const { id } = await res.json();
        const url = `/api/files/${id}/download`;
        document.getElementById('qf-image').value = url;
        updateImagePreview(url);
      } catch (e) {
        document.getElementById('qf-error').textContent = 'Ошибка загрузки: ' + e.message;
      } finally {
        btn.disabled = false;
        lbl.textContent = 'Загрузить';
      }
    }

    document.addEventListener('DOMContentLoaded', () => {
      const imgInput = document.getElementById('qf-image');
      if (imgInput) imgInput.addEventListener('input', e => updateImagePreview(e.target.value.trim()));
    });

    /* ── CSV Import ── */
    async function importCSVFile(input) {
      const file = input.files[0];
      if (!file) return;
      input.value = '';
      const fd = new FormData();
      fd.append('file', file);
      const btn = document.querySelector('[onclick="document.getElementById(\'csv-file-input\').click()"]');
      if (btn) { btn.disabled = true; btn.textContent = 'Импорт…'; }
      try {
        const { imported, errors } = await LS.importQuestions(fd);
        LS.toast(`Импортировано: ${imported} вопросов${errors.length ? ` (${errors.length} ошибок)` : ''}`, imported > 0 ? 'success' : 'warn', 5000);
        loadQuestions();
      } catch (e) {
        LS.toast('Ошибка импорта: ' + e.message, 'error');
      } finally {
        if (btn) { btn.disabled = false; btn.innerHTML = '<i data-lucide="upload" style="width:14px;height:14px;vertical-align:-2px"></i> Импорт CSV'; if(window.lucide)lucide.createIcons(); }
      }
    }

    function downloadCSVTemplate(e) {
      e.preventDefault();
      const header = 'subject_slug;topic;text;difficulty;type;opt1;c1;opt2;c2;opt3;c3;opt4;c4;correct_text;explanation;year';
      const example = [
        'bio;Клетки;Что является «электростанцией» клетки?;2;single;Митохондрия;1;Рибосома;0;Лизосома;0;Ядро;0;;Митохондрии синтезируют АТФ;2024',
        'bio;Клетки;Какие органоиды участвуют в синтезе белка?;2;multi;Рибосома;1;Митохондрия;0;Эндоплазматическая сеть;1;Лизосома;0;;',
        'chem;Кислоты;Формула серной кислоты;1;short_answer;;;;;;;;H2SO4;;',
      ].join('\n');
      const blob = new Blob(['\ufeff' + header + '\n' + example], { type: 'text/csv;charset=utf-8' });
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'questions_template.csv';
      a.click();
    }

    /* ════════════════════════════════════════════════
       ПОЛЬЗОВАТЕЛИ
    ════════════════════════════════════════════════ */
    async function loadUsers() {
      try {
        const users = await LS.adminGetUsers();
        const tbody = document.getElementById('users-body');
        if (!users.length) { tbody.innerHTML = '<tr><td colspan="7"><div class="empty">Пользователей нет</div></td></tr>'; return; }
        tbody.innerHTML = users.map(u => {
          const pc = pctClass(u.avg_pct);
          const initials = (u.name||'?').split(' ').slice(0,2).map(w=>w[0]?.toUpperCase()||'').join('')||'?';
          const avatarBg = u.role==='admin' ? 'linear-gradient(135deg,#9B5DE5,#c084fc)' : u.role==='teacher' ? 'linear-gradient(135deg,#06D6E0,#9B5DE5)' : u.role==='free_student' ? 'linear-gradient(135deg,#10B981,#059669)' : 'linear-gradient(135deg,#8898AA,#3D4F6B)';
          const roleCell = isAdmin && u.id !== user.id
            ? `<select class="role-select" data-uid="${u.id}" onchange="changeRole(this)">
                <option value="student"      ${u.role==='student'      ?'selected':''}>Ученик</option>
                <option value="free_student" ${u.role==='free_student' ?'selected':''}>Своб. ученик</option>
                <option value="teacher"      ${u.role==='teacher'      ?'selected':''}>Учитель</option>
                <option value="admin"        ${u.role==='admin'        ?'selected':''}>Админ</option>
               </select>`
            : `<span class="role-badge ${u.role}">${{student:'Ученик',free_student:'Своб. ученик',teacher:'Учитель',admin:'Админ'}[u.role]||u.role}</span>`;
          return `<tr class="clickable${u.is_banned ? ' banned-row' : ''}" onclick="openUserPanel(event,${u.id},'${u.role}')">
            <td>
              <div style="display:flex;align-items:center;gap:12px">
                <div style="width:36px;height:36px;border-radius:10px;background:${avatarBg};display:flex;align-items:center;justify-content:center;font-family:'Unbounded',sans-serif;font-size:0.62rem;font-weight:800;color:#fff;flex-shrink:0;${u.is_banned?'filter:grayscale(1);opacity:.5':''}">${initials}</div>
                <div>
                  <div style="font-weight:700;font-size:0.88rem;color:var(--text)">${esc(u.name)}${u.is_banned ? ' <span style="font-size:0.7rem;background:rgba(239,68,68,.12);color:#EF4444;border-radius:4px;padding:1px 5px;font-weight:600;vertical-align:middle">заблокирован</span>' : ''}</div>
                  <div style="color:var(--text-3);font-size:0.76rem">${esc(u.email)}</div>
                </div>
              </div>
            </td>
            <td onclick="event.stopPropagation()">${roleCell}</td>
            <td style="font-weight:700">${u.tests_count}</td>
            <td>
              <span class="pct-cell ${pc}">${u.avg_pct !== null ? u.avg_pct+'%' : '—'}</span>
              ${u.avg_pct !== null ? `<div class="perf-bar"><div class="perf-fill ${pc}" style="width:${u.avg_pct}%"></div></div>` : ''}
            </td>
            <td style="color:var(--text-3);font-size:0.8rem">${fmtDate(u.created_at)}</td>
            <td style="color:var(--text-3);font-size:0.8rem">${u.last_login ? new Date(u.last_login).toLocaleDateString('ru',{day:'numeric',month:'short'}) : '—'}</td>
            <td style="text-align:right;color:var(--text-3);font-size:0.85rem;opacity:0.4">›</td>
          </tr>`;
        }).join('');
      } catch (e) {
        document.getElementById('users-body').innerHTML = `<tr><td colspan="7"></td></tr>`;
        LS.state.error(document.getElementById('users-body').querySelector('td'), e, loadUsers);
      }
    }

    async function changeRole(select) {
      select.disabled = true;
      try { await LS.adminUpdateRole(select.dataset.uid, select.value); LS.toast('Роль изменена', 'success', 2000); }
      catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
      finally { select.disabled = false; }
    }

    let activeTr = null;
    let activeUid = null;
    let activeUserRole = null;

    async function openUserPanel(e, uid, role) {
      if (activeTr) activeTr.classList.remove('selected');
      activeTr = e.currentTarget; activeTr.classList.add('selected');
      activeUid = uid;
      activeUserRole = role;
      const panel = document.getElementById('user-panel');
      panel.classList.add('visible');
      panel.scrollIntoView({ behavior:'smooth', block:'nearest' });
      document.getElementById('up-sessions').innerHTML = LS.skeleton(3, 'row');
      document.getElementById('up-name').textContent   = '…';
      document.getElementById('up-email').textContent  = '';
      if (isAdmin) {
        document.getElementById('up-edit-btn').style.display   = '';
        document.getElementById('up-clear-btn').style.display  = '';
        document.getElementById('up-perms-btn').style.display  = role === 'teacher' ? '' : 'none';
        document.getElementById('up-ban-btn').style.display    = '';
        document.getElementById('up-delete-btn').style.display = '';
      }
      await reloadUserPanel(uid);
    }

    async function reloadUserPanel(uid) {
      try {
        const { user: u, sessions } = await LS.adminGetUserSessions(uid);
        activeUserRole = u.role;
        document.getElementById('up-name').innerHTML  = LS.esc(u.name) + (u.is_banned ? ' <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>' : '');
        document.getElementById('up-email').textContent = u.email;
        // Sync button in case role changed after panel was opened
        if (isAdmin) {
          document.getElementById('up-perms-btn').style.display = u.role === 'teacher' ? '' : 'none';
          const banBtn = document.getElementById('up-ban-btn');
          const banLbl = document.getElementById('up-ban-label');
          if (u.is_banned) {
            banBtn.style.background     = 'rgba(34,197,94,.12)';
            banBtn.style.color          = '#22C55E';
            banBtn.style.borderColor    = 'rgba(34,197,94,.25)';
            banLbl.textContent          = 'Разблокировать';
          } else {
            banBtn.style.background = '';
            banBtn.style.color      = '';
            banBtn.style.borderColor = '';
            banLbl.textContent      = 'Заблокировать';
          }
        }
        const el = document.getElementById('up-sessions');
        if (!sessions.length) { el.innerHTML = '<div class="empty">Тестов нет</div>'; return; }
        el.innerHTML = '<div class="sess-list">' + sessions.map(s => {
          const pct = s.score !== null ? Math.round((s.score/s.total)*100) : null;
          return `<div class="sess-item">
            <div class="sess-pct ${pctClass(pct)}">${pct !== null ? pct+'%' : '—'}</div>
            <div class="sess-info"><div class="sess-subj">${s.subject_name||'Тест'}</div><div class="sess-meta">${fmtDate(s.started_at)} · ${MODES[s.mode]||s.mode}</div></div>
            <div class="sess-score">${s.score??'—'} / ${s.total}</div>
          </div>`;
        }).join('') + '</div>';
      } catch (e) { LS.state.error(document.getElementById('up-sessions'), e); }
    }

    function closeUserPanel() {
      document.getElementById('user-panel').classList.remove('visible');
      if (activeTr) { activeTr.classList.remove('selected'); activeTr = null; }
      activeUid = null;
    }

    async function clearUserHistory() {
      const name = document.getElementById('up-name').textContent;
      if (!await LS.confirm(`Удалить всю историю тестов пользователя «${name}»?\nЭто действие нельзя отменить.`, { title: 'Очистить историю', confirmText: 'Удалить историю' })) return;
      try {
        await LS.adminClearUserSessions(activeUid);
        await reloadUserPanel(activeUid);
        loadUsers();
      } catch (e) { LS.toast('Ошибка очистки истории: ' + e.message, 'error'); }
    }

    async function toggleBanUser() {
      const banLbl = document.getElementById('up-ban-label');
      const isBanning = banLbl.textContent === 'Заблокировать';
      const name = document.getElementById('up-name').innerHTML.replace(' <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>','');
      const msg = isBanning
        ? `Заблокировать пользователя «${name}»?\nОн не сможет войти в систему.`
        : `Разблокировать пользователя «${name}»?`;
      if (!await LS.confirm(msg, { title: isBanning ? 'Блокировка' : 'Разблокировка', confirmText: isBanning ? 'Заблокировать' : 'Разблокировать' })) return;
      try {
        await LS.adminBanUser(activeUid, isBanning);
        LS.toast(isBanning ? 'Пользователь заблокирован' : 'Пользователь разблокирован', isBanning ? 'warning' : 'success');
        await reloadUserPanel(activeUid);
        loadUsers();
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    async function confirmDeleteUser() {
      const name = document.getElementById('up-name').innerHTML.replace(' <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>','');
      if (!await LS.confirm(`Удалить пользователя «${name}» навсегда?\nВсе его данные, тесты и прогресс будут удалены. Это действие нельзя отменить.`, { title: 'Удалить пользователя', confirmText: 'Удалить навсегда' })) return;
      try {
        await LS.adminDeleteUser(activeUid);
        LS.toast('Пользователь удалён', 'success');
        closeUserPanel();
        loadUsers();
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    let _editUid = null;
    function closeEditUserModal() {
      document.getElementById('eu-modal').classList.remove('open');
      _editUid = null;
    }
    function openEditUserModal() {
      _editUid = activeUid;
      document.getElementById('eu-name').value     = document.getElementById('up-name').textContent;
      document.getElementById('eu-email').value    = document.getElementById('up-email').textContent;
      document.getElementById('eu-password').value = '';
      document.getElementById('eu-error').textContent = '';
      document.getElementById('eu-modal').classList.add('open');
      setTimeout(() => document.getElementById('eu-name').focus(), 80);
    }
    async function saveEditUser() {
      const name     = document.getElementById('eu-name').value.trim();
      const email    = document.getElementById('eu-email').value.trim();
      const password = document.getElementById('eu-password').value;
      const errEl    = document.getElementById('eu-error');
      errEl.textContent = '';
      if (!name)  { errEl.textContent = 'Введите имя'; return; }
      if (!email) { errEl.textContent = 'Введите email'; return; }
      if (password && password.length < 6) { errEl.textContent = 'Пароль должен быть не менее 6 символов'; return; }
      const payload = { name, email };
      if (password) payload.password = password;
      const btn = document.getElementById('eu-save');
      btn.disabled = true; btn.textContent = 'Сохранение…';
      try {
        await LS.adminUpdateUser(_editUid, payload);
        closeEditUserModal();
        await reloadUserPanel(activeUid);
        loadUsers();
      } catch (e) {
        errEl.textContent = 'Ошибка: ' + e.message;
      } finally {
        btn.disabled = false; btn.textContent = 'Сохранить';
      }
    }

    /* ════════════════════════════════════════════════
       СЕССИИ
    ════════════════════════════════════════════════ */
    let allSessions  = [];
    let openDrawerId = null;

    async function loadSessions() {
      const subject = document.getElementById('t-subject').value;
      document.getElementById('t-body').innerHTML = '<div class="spinner"></div>';
      openDrawerId = null;
      try {
        allSessions = await LS.adminGetSessions({ subject: subject || undefined });
        renderSessions();
      } catch (e) {
        document.getElementById('t-body').innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    function sessPctRing(pct) {
      const pc  = pctClass(pct);
      const colorMap = {'pct-hi':'var(--green)','pct-mid':'var(--amber)','pct-lo':'var(--pink)'};
      const color = colorMap[pc] || 'var(--text-3)';
      const circ = 106.8;
      const dash = (pct / 100 * circ).toFixed(1);
      return `<svg class="sess-tl-ring" width="48" height="48" viewBox="0 0 48 48">
        <circle cx="24" cy="24" r="17" fill="none" stroke="rgba(15,23,42,0.08)" stroke-width="4"/>
        <circle cx="24" cy="24" r="17" fill="none" stroke="${color}" stroke-width="4"
          stroke-dasharray="${dash} ${circ}" stroke-dashoffset="26.7" stroke-linecap="round"
          transform="rotate(-90 24 24)"/>
        <text x="24" y="28" text-anchor="middle" font-family="Unbounded,sans-serif" font-size="8" font-weight="800" fill="${color}">${pct}%</text>
      </svg>`;
    }

    function renderSessions() {
      const modeF   = document.getElementById('t-mode').value;
      const searchF = document.getElementById('t-search').value.toLowerCase();
      const filtered = allSessions.filter(s => {
        if (modeF && s.mode !== modeF) return false;
        if (searchF && !s.user_name.toLowerCase().includes(searchF) && !s.user_email.toLowerCase().includes(searchF)) return false;
        return true;
      });
      document.getElementById('t-count').textContent = `${filtered.length} тестов`;
      if (!filtered.length) {
        document.getElementById('t-body').innerHTML = '<div class="empty">Нет тестов</div>';
        return;
      }
      // Group by date
      const groups = {};
      filtered.forEach(s => {
        const key = fmtDate(s.started_at);
        (groups[key] = groups[key] || []).push(s);
      });
      document.getElementById('t-body').innerHTML = Object.entries(groups).map(([date, sessions]) =>
        `<div class="sess-tl-day">${date}</div>
         <div class="sess-tl-wrap">${sessions.map(s => {
           const ring = s.percent !== null
             ? sessPctRing(s.percent)
             : `<div style="width:48px;height:48px;display:flex;align-items:center;justify-content:center;font-family:'Unbounded',sans-serif;font-size:0.85rem;font-weight:800;color:var(--text-3)">—</div>`;
           return `<div class="sess-tl-item" id="trow-${s.id}" onclick="toggleDrawer(${s.id})">
             ${ring}
             <div class="sess-tl-user">
               <div class="sess-tl-name">${esc(s.user_name)}</div>
               <div class="sess-tl-meta">${esc(s.subject_name||'?')} · <span class="mode-badge mode-${s.mode}">${MODES[s.mode]||s.mode}</span></div>
             </div>
             <div class="sess-tl-score">${s.score??'—'} / ${s.total}</div>
             <div class="sess-tl-time">${fmtTime(s.duration_sec)}</div>
           </div>
           <div class="sess-tl-drawer" id="tdrawer-${s.id}">
             <div class="sess-drawer" id="drawer-${s.id}">
               <div class="sess-drawer-inner" id="drawer-inner-${s.id}"><div class="spinner"></div></div>
             </div>
           </div>`;
         }).join('')}</div>`
      ).join('');
    }

    async function toggleDrawer(id) {
      const drawerEl = document.getElementById('tdrawer-' + id);
      const drawer   = document.getElementById('drawer-'  + id);
      const trow     = document.getElementById('trow-'    + id);
      if (openDrawerId && openDrawerId !== id) {
        document.getElementById('tdrawer-' + openDrawerId)?.classList.remove('open');
        document.getElementById('drawer-'  + openDrawerId)?.classList.remove('open');
        document.getElementById('trow-'    + openDrawerId)?.classList.remove('open');
      }
      if (openDrawerId === id) {
        drawerEl.classList.remove('open'); drawer.classList.remove('open'); trow.classList.remove('open');
        openDrawerId = null; return;
      }
      openDrawerId = id; trow.classList.add('open');
      drawerEl.classList.add('open');
      requestAnimationFrame(() => drawer.classList.add('open'));
      const inner = document.getElementById('drawer-inner-' + id);
      if (inner.dataset.loaded) return;
      inner.dataset.loaded = '1';
      try {
        const d = await LS.adminGetSessionDetail(id);
        renderDrawer(inner, d);
      } catch (e) { inner.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`; }
    }

    function renderDrawer(el, d) {
      const pct     = d.score !== null && d.total ? Math.round((d.score/d.total)*100) : null;
      const pc      = pctClass(pct);
      const correct = d.questions.filter(q => q.is_correct).length;
      const wrong   = d.questions.filter(q => !q.is_correct && q.chosen_option_id).length;
      const skipped = d.questions.filter(q => !q.chosen_option_id).length;
      const qHtml   = d.questions.map((q,i) => {
        const status   = !q.chosen_option_id ? 'skipped' : q.is_correct ? 'correct' : 'wrong';
        const badgeTxt = { correct:'Верно', wrong:'Неверно', skipped:'Пропущено' }[status];
        const opts = q.options.map(o => {
          const isCor = o.is_correct, isCho = o.id === q.chosen_option_id;
          let cls='', icon='<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8"/></svg>';
          if (isCor) { cls='correct-opt'; icon='<i data-lucide="check" style="width:13px;height:13px"></i>'; }
          else if (isCho && !isCor) { cls='chosen-wrong'; icon='<i data-lucide="x" style="width:13px;height:13px"></i>'; }
          return `<div class="qb-opt ${cls}"><span class="qb-opt-icon">${icon}</span>${esc(o.text)}</div>`;
        }).join('');
        const expl = q.explanation ? `<div class="qb-expl"><strong>Пояснение:</strong> ${esc(q.explanation)}</div>` : '';
        return `<div class="qb-item ${status}">
          <div class="qb-header"><span class="qb-qnum">Вопрос ${i+1}</span><span class="qb-badge ${status}">${badgeTxt}</span><span class="qb-time">${q.time_spent_sec?q.time_spent_sec+' сек':''}</span></div>
          <div class="qb-text">${esc(q.text)}</div>
          <div class="qb-opts">${opts}</div>${expl}
        </div>`;
      }).join('');
      el.innerHTML = `
        <div class="drawer-header">
          <div>
            <div style="font-family:'Unbounded',sans-serif;font-weight:800;font-size:0.95rem">${esc(d.user_name)}</div>
            <div class="drawer-meta">${esc(d.user_email)} · ${d.subject_name||'?'} · ${MODES[d.mode]||d.mode} · ${fmtDate(d.started_at)}</div>
          </div>
          <div class="drawer-score ${pc}">${pct !== null ? pct+'%' : '—'}</div>
          <div style="display:flex;gap:20px;margin-left:auto;text-align:center">
            <div><div style="font-family:'Unbounded',sans-serif;color:var(--green);font-weight:700">${correct}</div><div style="font-size:0.72rem;color:var(--text-3)">Верно</div></div>
            <div><div style="font-family:'Unbounded',sans-serif;color:var(--pink);font-weight:700">${wrong}</div><div style="font-size:0.72rem;color:var(--text-3)">Неверно</div></div>
            <div><div style="font-family:'Unbounded',sans-serif;color:var(--text-3);font-weight:700">${skipped}</div><div style="font-size:0.72rem;color:var(--text-3)">Пропущено</div></div>
            <div><div style="font-family:'Unbounded',sans-serif;color:var(--text-2);font-weight:700">${fmtTime(d.duration_sec)}</div><div style="font-size:0.72rem;color:var(--text-3)">Время</div></div>
          </div>
        </div>
        <div class="qb-list">${qHtml||'<div class="empty">Вопросы не найдены</div>'}</div>`;
      renderMath(el);
      if (window.lucide) lucide.createIcons();
    }

    /* ════════════════════════════════════════════════
       ТЕСТЫ (ШАБЛОНЫ)
    ════════════════════════════════════════════════ */
    let allTests = [];
    let openTstId = null;
    let editingTstId = null;
    const DIFF_LABELS = { 1:'Лёгкий', 2:'Средний', 3:'Сложный' };
    const TYPE_LABELS = { single:'Один', multi:'Несколько', true_false:'Верно/Нет', short_answer:'Краткий', matching:'Сопоставление' };

    async function loadTests() {
      const subj = document.getElementById('tst-subj').value;
      const wrap = document.getElementById('tst-list-wrap');
      wrap.innerHTML = '<div class="spinner"></div>';
      try {
        allTests = await LS.getTests(subj || null);
        renderTests();
      } catch (e) {
        wrap.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    function renderTests() {
      const search = document.getElementById('tst-search').value.toLowerCase();
      const filtered = search ? allTests.filter(t => t.title.toLowerCase().includes(search)) : allTests;
      document.getElementById('tst-count').textContent = `${filtered.length} тестов`;
      const wrap = document.getElementById('tst-list-wrap');
      if (!filtered.length) { wrap.innerHTML = '<div class="empty">Тестов не найдено</div>'; return; }
      const SUBJ_N = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика' };
      wrap.innerHTML = `<div class="q-list">${filtered.map(t => `
        <div class="q-card" id="tstcard-${t.id}">
          <div class="q-card-head">
            <span class="q-card-num">#${t.id}</span>
            <div class="q-card-body" onclick="toggleTstDrawer(${t.id})">
              <div class="q-card-text">${esc(t.title)}</div>
              <div class="q-card-meta">
                <span class="q-badge q-badge-subj">${SUBJ_N[t.subject_slug]||t.subject_slug}</span>
                <span style="font-size:0.75rem;color:var(--text-3)">${t.question_count} вопросов</span>
                <span style="font-size:0.75rem;color:var(--text-3)">${fmtDate(t.created_at)}</span>
                ${t.description ? `<span style="font-size:0.75rem;color:var(--text-2)">${esc(t.description)}</span>` : ''}
              </div>
            </div>
            <div class="q-card-actions">
              <button class="btn-edit-q" onclick="editTst(${t.id})">Изменить</button>
              <button class="btn-del-q"  onclick="deleteTst(${t.id})" title="Удалить"><i data-lucide="x" style="width:14px;height:14px"></i></button>
            </div>
          </div>
          <div class="tst-drawer" id="tstdrawer-${t.id}" style="display:none">
            <div class="tst-drawer-inner" id="tstdinner-${t.id}">
              <div class="spinner"></div>
            </div>
          </div>
        </div>`).join('')}</div>`;
      if (window.lucide) lucide.createIcons();
    }

    async function toggleTstDrawer(id) {
      const drawer = document.getElementById('tstdrawer-' + id);
      if (!drawer) return;
      if (openTstId && openTstId !== id) {
        const old = document.getElementById('tstdrawer-' + openTstId);
        if (old) old.style.display = 'none';
      }
      if (openTstId === id) {
        drawer.style.display = 'none'; openTstId = null; return;
      }
      openTstId = id;
      drawer.style.display = '';
      await renderTstDrawer(id);
    }

    async function renderTstDrawer(id) {
      const inner = document.getElementById('tstdinner-' + id);
      if (!inner) return;
      inner.innerHTML = '<div class="spinner"></div>';
      try {
        const [t, subjectQs] = await Promise.all([
          LS.getTest(id),
          LS.getQuestions(
            (_tstPickerCache[id]?.subject_slug) || allTests.find(x => x.id === id)?.subject_slug || '',
            null, 'date_asc'
          ).catch(() => []),
        ]);

        const inIds = new Set(t.questions.map(q => q.id));
        // Update cache so filterTstPicker can filter locally
        _tstPickerCache[id] = { subjectQs, inIds, subject_slug: t.subject_slug };

        inner.innerHTML = `
          <div class="tst-cols">
            <div>
              <div class="tst-panel-title">Вопросы в тесте (${t.questions.length})</div>
              <div class="tst-q-list" id="tstql-${id}">${renderTstQList(t.questions, id)}</div>
            </div>
            <div>
              <div class="tst-panel-title">Добавить вопросы</div>
              <input class="tst-search" id="tstps-${id}" placeholder="Поиск вопросов…" oninput="filterTstPicker(${id})" />
              <div class="tst-q-list" id="tstpicker-${id}">${renderTstPicker(subjectQs, inIds, id)}</div>
            </div>
          </div>`;
        renderMath(inner);
        if (window.lucide) lucide.createIcons();
      } catch (e) {
        inner.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    function qTypeBadge(type) {
      const MAP = { single:'Один', multi:'Несколько', true_false:'Верно/Нет', short_answer:'Ответ', matching:'Сопост.' };
      const CLR = { single:'rgba(155,93,229,0.12)', multi:'rgba(6,214,224,0.12)', true_false:'rgba(255,179,71,0.14)', short_answer:'rgba(6,214,100,0.12)', matching:'rgba(241,91,181,0.10)' };
      const TXT = { single:'var(--violet)', multi:'#05aab3', true_false:'var(--amber)', short_answer:'var(--green)', matching:'var(--pink)' };
      return `<span class="tst-q-badge" style="background:${CLR[type]||'rgba(15,23,42,0.06)'};color:${TXT[type]||'var(--text-3)'}">${MAP[type]||type}</span>`;
    }

    function qOptsPreview(q) {
      if (q.type === 'short_answer') return q.correct_text ? `<span class="tst-q-opts">Ответ: ${esc(q.correct_text)}</span>` : '';
      if (!q.options?.length) return '';
      const correct = q.options.filter(o => o.is_correct).map(o => esc(o.text)).join(', ');
      return `<span class="tst-q-opts"><i data-lucide="check" style="width:12px;height:12px;vertical-align:-2px"></i> ${correct}</span>`;
    }

    function renderTstQList(questions, tid) {
      if (!questions.length) return '<div class="tst-empty">Вопросов нет. Добавьте справа <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></div>';
      return questions.map((q, i) => `
        <div class="tst-q-item" id="tstqitem-${tid}-${q.id}">
          <span class="tst-q-num">${i+1}.</span>
          <div class="tst-q-body">
            <span class="tst-q-text">${esc(q.text)}</span>
            <div class="tst-q-meta">
              <span class="tst-q-badge diff-${q.difficulty}">${DIFF_LABELS[q.difficulty]||q.difficulty}</span>
              ${qTypeBadge(q.type)}
              ${qOptsPreview(q)}
            </div>
          </div>
          <button class="btn-tst-rem" onclick="tstRemoveQ(${tid},${q.id})" title="Убрать">−</button>
        </div>`).join('');
    }

    function renderTstPicker(questions, inIds, tid) {
      if (!questions.length) return '<div class="tst-empty">Вопросов нет в этом предмете</div>';
      return questions.map(q => {
        const added = inIds.has(q.id);
        return `<div class="tst-q-item" id="tstpick-${tid}-${q.id}">
          <div class="tst-q-body">
            <span class="tst-q-text">${esc(q.text)}</span>
            <div class="tst-q-meta">
              <span class="tst-q-badge diff-${q.difficulty}">${DIFF_LABELS[q.difficulty]||''}</span>
              ${qTypeBadge(q.type)}
              ${qOptsPreview(q)}
            </div>
          </div>
          <button class="btn-tst-add${added?' added':''}" id="tstbtn-${tid}-${q.id}"
                  title="${added?'Уже в тесте':'Добавить'}" ${added?'disabled':'onclick="tstAddQ('+tid+','+q.id+')"'}>${added?'<i data-lucide="check" style="width:14px;height:14px"></i>':'+'}</button>
        </div>`;
      }).join('');
    }

    // Cache for picker: { tid: { subjectQs:[], inIds: Set } }
    const _tstPickerCache = {};

    async function filterTstPicker(tid) {
      const search = document.getElementById('tstps-'+tid)?.value.toLowerCase() || '';
      const cache  = _tstPickerCache[tid];
      if (!cache) return; // not loaded yet
      const filtered = search
        ? cache.subjectQs.filter(q => q.text.toLowerCase().includes(search))
        : cache.subjectQs;
      const picker = document.getElementById('tstpicker-'+tid);
      if (picker) { picker.innerHTML = renderTstPicker(filtered, cache.inIds, tid); renderMath(picker); if(window.lucide)lucide.createIcons(); }
    }

    async function tstAddQ(tid, qid) {
      const btn = document.getElementById(`tstbtn-${tid}-${qid}`);
      if (btn) { btn.disabled = true; btn.textContent = '…'; }
      try {
        await LS.addQuestionsToTest(tid, [qid]);
        const t = allTests.find(x => x.id === tid);
        if (t) t.question_count++;
        renderTests();
        openTstId = tid;
        document.getElementById('tstdrawer-' + tid).style.display = '';
        await renderTstDrawer(tid);
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); if (btn) { btn.disabled=false; btn.textContent='+'; } }
    }

    async function tstRemoveQ(tid, qid) {
      try {
        await LS.removeQFromTest(tid, qid);
        const t = allTests.find(x => x.id === tid);
        if (t) t.question_count = Math.max(0, t.question_count - 1);
        renderTests();
        openTstId = tid;
        document.getElementById('tstdrawer-' + tid).style.display = '';
        await renderTstDrawer(tid);
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ── Test modal ── */
    let _tstShowAnswers = true;
    function setTstShowAnswers(val) {
      _tstShowAnswers = val;
      document.getElementById('tstf-show-yes').classList.toggle('active',  val);
      document.getElementById('tstf-show-no').classList.toggle('active', !val);
    }

    function openTstModal(t = null) {
      editingTstId = t ? t.id : null;
      document.getElementById('tst-modal-title').textContent = t ? `Редактировать: ${t.title}` : 'Создать тест';
      document.getElementById('tstf-title').value   = t?.title || '';
      document.getElementById('tstf-subject').value = t?.subject_slug || '';
      document.getElementById('tstf-desc').value    = t?.description || '';
      document.getElementById('tstf-time').value    = t?.time_limit || '';
      document.getElementById('tstf-error').textContent = '';
      setTstShowAnswers(t ? (t.show_answers !== 0) : true);
      document.getElementById('tst-modal').classList.add('open');
      setTimeout(() => document.getElementById('tstf-title').focus(), 80);
    }

    function editTst(id) {
      const t = allTests.find(x => x.id === id);
      if (t) openTstModal(t);
    }

    function closeTstModal() {
      document.getElementById('tst-modal').classList.remove('open');
      editingTstId = null;
    }

    async function saveTst() {
      const title       = document.getElementById('tstf-title').value.trim();
      const subject_slug= document.getElementById('tstf-subject').value;
      const description = document.getElementById('tstf-desc').value.trim();
      const errEl       = document.getElementById('tstf-error');
      errEl.textContent = '';
      if (!title)        { errEl.textContent = 'Введите название'; return; }
      if (!subject_slug) { errEl.textContent = 'Выберите предмет'; return; }

      const btn = document.getElementById('tstf-save');
      btn.disabled = true; btn.textContent = 'Сохранение…';
      const show_answers = _tstShowAnswers ? 1 : 0;
      const timeVal = parseInt(document.getElementById('tstf-time').value, 10);
      const time_limit = timeVal >= 1 ? Math.min(600, timeVal) : null;
      try {
        if (editingTstId) {
          await LS.updateTest(editingTstId, { title, subject_slug, description: description||null, show_answers, time_limit });
          const idx = allTests.findIndex(x => x.id === editingTstId);
          if (idx !== -1) Object.assign(allTests[idx], { title, subject_slug, description, show_answers, time_limit });
        } else {
          const { id } = await LS.createTest({ title, subject_slug, description: description||null, show_answers, time_limit });
          allTests.unshift({ id, title, subject_slug, description, question_count: 0, created_at: new Date().toISOString() });
          closeTstModal();
          renderTests();
          // Auto-open drawer so teacher can immediately add questions
          openTstId = id;
          document.getElementById('tstdrawer-' + id).style.display = '';
          await renderTstDrawer(id);
          return;
        }
        closeTstModal();
        renderTests();
      } catch (e) {
        errEl.textContent = 'Ошибка: ' + e.message;
      } finally {
        btn.disabled = false; btn.textContent = 'Сохранить';
      }
    }

    async function deleteTst(id) {
      const t = allTests.find(x => x.id === id);
      if (!await LS.confirm(`Удалить тест «${t?.title}»?`, { title: 'Удалить тест', confirmText: 'Удалить' })) return;
      try {
        await LS.deleteTest(id);
        allTests = allTests.filter(x => x.id !== id);
        if (openTstId === id) openTstId = null;
        renderTests();
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ════════════════════════════════════════════════
       ЗАДАНИЯ
    ════════════════════════════════════════════════ */
    let allAssignments  = [];
    let editingAId      = null;
    const SUBJ_NAMES    = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика' };

    async function loadAssignments() {
      document.getElementById('a-body').innerHTML = '<div class="spinner"></div>';
      try {
        allAssignments = await LS.teacherAssignments();
        renderAssignments();
      } catch (e) {
        document.getElementById('a-body').innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    const SUBJ_COLORS_A = { bio:'#9B5DE5', chem:'#06D6A0', math:'#06B6D4', phys:'#F59E0B' };
    const SUBJ_ICONS_A  = { bio:'dna', chem:'flask-conical', math:'calculator', phys:'zap' };
    let _aFilter = 'all';

    function setAFilter(f) {
      _aFilter = f;
      document.querySelectorAll('.a-f-chip').forEach(c =>
        c.classList.toggle('active', c.textContent.trim() === {all:'Все',active:'Активные',overdue:'Просрочены',done:'Завершены'}[f])
      );
      renderAssignments();
    }

    function aClassify(a) {
      const pct = a.total_members ? Math.round(a.completed_count / a.total_members * 100) : null;
      if (pct === 100) return 'done';
      if (a.deadline && new Date(a.deadline) < new Date()) return 'overdue';
      return 'active';
    }

    function renderAssignments() {
      const subjF   = document.getElementById('a-subject').value;
      const searchF = document.getElementById('a-search').value.toLowerCase();
      const sortF   = document.getElementById('a-sort')?.value || 'date';

      let list = allAssignments.filter(a => {
        if (subjF   && a.subject_slug !== subjF) return false;
        if (searchF && !a.title.toLowerCase().includes(searchF)) return false;
        if (_aFilter === 'active'  && aClassify(a) !== 'active')  return false;
        if (_aFilter === 'overdue' && aClassify(a) !== 'overdue') return false;
        if (_aFilter === 'done'    && aClassify(a) !== 'done')    return false;
        return true;
      });

      // Sort
      list = [...list].sort((a, b) => {
        if (sortF === 'deadline') {
          const da = a.deadline ? new Date(a.deadline) : new Date(9e15);
          const db = b.deadline ? new Date(b.deadline) : new Date(9e15);
          return da - db;
        }
        if (sortF === 'progress_asc') {
          const pa = a.total_members ? a.completed_count / a.total_members : 0;
          const pb = b.total_members ? b.completed_count / b.total_members : 0;
          return pa - pb;
        }
        if (sortF === 'progress_desc') {
          const pa = a.total_members ? a.completed_count / a.total_members : 0;
          const pb = b.total_members ? b.completed_count / b.total_members : 0;
          return pb - pa;
        }
        return 0; // date: keep server order
      });

      // Summary chips
      const all     = allAssignments;
      const nActive  = all.filter(a => aClassify(a) === 'active').length;
      const nOverdue = all.filter(a => aClassify(a) === 'overdue').length;
      const nDone    = all.filter(a => aClassify(a) === 'done').length;
      document.getElementById('a-summary').innerHTML = [
        `<span class="a-sum-chip s-all">Всего: ${all.length}</span>`,
        nActive  ? `<span class="a-sum-chip s-active">Активных: ${nActive}</span>`    : '',
        nOverdue ? `<span class="a-sum-chip s-overdue">Просрочено: ${nOverdue}</span>` : '',
        nDone    ? `<span class="a-sum-chip s-done">Завершено: ${nDone}</span>`        : '',
      ].join('');

      document.getElementById('a-count').textContent = `${list.length} заданий`;
      const container = document.getElementById('a-body');

      if (!list.length) {
        container.innerHTML = '<div class="empty">Заданий нет</div>';
        return;
      }

      const now = new Date();
      container.innerHTML = list.map(a => {
        const pct      = a.total_members ? Math.round(a.completed_count / a.total_members * 100) : null;
        const cls      = aClassify(a);
        const rowCls   = cls === 'overdue' ? 'a-overdue' : cls === 'done' ? 'a-done' : '';
        const sColor   = SUBJ_COLORS_A[a.subject_slug] || '#9B5DE5';
        const dlMs     = a.deadline ? new Date(a.deadline) - now : Infinity;
        const isUrgent = cls === 'active' && dlMs > 0 && dlMs < 24 * 3600 * 1000;

        const dl = a.deadline
          ? new Date(a.deadline).toLocaleDateString('ru', {day:'numeric', month:'short'})
          : null;

        const targetStr = a.target_user_id
          ? esc(a.target_user_name || 'Ученик')
          : esc(a.class_name || '—');

        const metaParts = [
          targetStr,
          SUBJ_NAMES[a.subject_slug] || a.subject_slug,
          `<span class="mode-badge mode-${a.mode}">${MODES[a.mode]||a.mode}</span>`,
          a.count + ' вопр.',
          dl ? `до ${dl}` : null,
          isUrgent  ? `<span class="a-tag-urgent"><i data-lucide="zap" style="width:10px;height:10px;vertical-align:-1px"></i> срочно</span>` : null,
          cls === 'overdue' ? `<span class="a-tag-over">просрочено</span>` : null,
        ].filter(Boolean);

        const barColor = pct >= 75 ? '#06D6A0' : pct >= 40 ? '#F59E0B' : '#F15BB5';
        const pctLabel = pct !== null ? `${pct}%` : '—';

        return `<div class="a-row ${rowCls}${isUrgent ? ' a-urgent' : ''}" style="--ac:${sColor}">
          <div class="a-icon" style="background:${sColor}18;color:${sColor}"><i data-lucide="${SUBJ_ICONS_A[a.subject_slug]||'file-text'}" style="width:18px;height:18px"></i></div>
          <div class="a-main">
            <div class="a-title">${esc(a.title)}</div>
            <div class="a-meta">${metaParts.join(' · ')}</div>
          </div>
          <div class="a-prog">
            <div class="a-prog-nums">
              <span>${a.completed_count} / ${a.total_members} сдали</span>
              <span class="a-prog-pct ${pctClass(pct)}">${pctLabel}</span>
            </div>
            <div class="a-prog-bar">
              <div class="a-prog-fill" style="width:${pct||0}%;background:${barColor}"></div>
            </div>
          </div>
          <div class="a-actions">
            <button class="btn-edit-q" onclick="openAModal(${a.id})">Изменить</button>
            <button class="btn-del-q"  onclick="deleteAsgn(${a.id})"><i data-lucide="x" style="width:14px;height:14px"></i></button>
          </div>
        </div>`;
      }).join('');
      if (window.lucide) lucide.createIcons();
    }

    let _afSrc = 'random';
    let _afLoadedTests = [];

    function setAfSrc(src) {
      _afSrc = src;
      document.querySelectorAll('[data-afsrc]').forEach(b => b.classList.toggle('active', b.dataset.afsrc === src));
      document.getElementById('af-random-fields').style.display = src === 'random' ? '' : 'none';
      document.getElementById('af-test-fields').style.display   = src === 'test'   ? '' : 'none';
    }

    async function openAModal(id) {
      const a = allAssignments.find(x => x.id === id);
      if (!a) return;
      editingAId = id;
      document.getElementById('a-modal-title').textContent = `Редактировать: ${a.title}`;
      document.getElementById('af-title').value    = a.title;
      document.getElementById('af-deadline').value = a.deadline ? a.deadline.split('T')[0] : '';
      document.getElementById('af-error').textContent = '';

      // Load tests for the dropdown
      const testSel = document.getElementById('af-test');
      testSel.innerHTML = '<option value="">Загрузка…</option>';
      try {
        _afLoadedTests = await LS.getTests();
        testSel.innerHTML = _afLoadedTests.length
          ? '<option value="">— выберите тест —</option>' + _afLoadedTests.map(t => `<option value="${t.id}">${esc(t.title)} (${t.question_count} вопр.)</option>`).join('')
          : '<option value="">Нет тестов</option>';
      } catch {
        testSel.innerHTML = '<option value="">Ошибка загрузки</option>';
        _afLoadedTests = [];
      }

      if (a.test_id) {
        setAfSrc('test');
        testSel.value = a.test_id;
        document.getElementById('af-mode-test').value = a.mode;
      } else {
        setAfSrc('random');
        document.getElementById('af-subject').value = a.subject_slug;
        document.getElementById('af-mode').value    = a.mode;
        document.getElementById('af-count').value   = a.count;
      }

      document.getElementById('a-modal').classList.add('open');
      setTimeout(() => document.getElementById('af-title').focus(), 80);
    }

    function closeAModal() {
      document.getElementById('a-modal').classList.remove('open');
      editingAId = null;
    }

    async function saveAssignment() {
      const title    = document.getElementById('af-title').value.trim();
      const deadline = document.getElementById('af-deadline').value || null;
      const errEl    = document.getElementById('af-error');
      errEl.textContent = '';
      if (!title) { errEl.textContent = 'Введите название'; return; }

      let payload = { title, deadline };

      if (_afSrc === 'test') {
        const test_id = document.getElementById('af-test').value;
        const mode    = document.getElementById('af-mode-test').value;
        if (!test_id) { errEl.textContent = 'Выберите тест'; return; }
        const testObj = _afLoadedTests.find(t => t.id === Number(test_id));
        if (testObj && testObj.question_count === 0) { errEl.textContent = 'В выбранном тесте нет вопросов'; return; }
        payload = { ...payload, test_id: Number(test_id), mode };
      } else {
        const subject_slug = document.getElementById('af-subject').value;
        const mode         = document.getElementById('af-mode').value;
        const count        = Number(document.getElementById('af-count').value);
        if (!subject_slug)       { errEl.textContent = 'Выберите предмет'; return; }
        if (!count || count < 1) { errEl.textContent = 'Введите количество вопросов'; return; }
        payload = { ...payload, subject_slug, mode, count, test_id: null };
      }

      const btn = document.getElementById('af-save');
      btn.disabled = true; btn.textContent = 'Сохранение…';
      try {
        await LS.updateAssignment(editingAId, payload);
        const idx = allAssignments.findIndex(x => x.id === editingAId);
        if (idx !== -1) Object.assign(allAssignments[idx], payload);
        closeAModal();
        renderAssignments();
      } catch (e) {
        errEl.textContent = 'Ошибка: ' + e.message;
      } finally {
        btn.disabled = false; btn.textContent = 'Сохранить';
      }
    }

    /* ─── Create assignment modal ─── */
    let _acSrc    = 'random'; // 'random' | 'test'
    let _acTarget = 'class';  // 'class' | 'user'

    let _acFileId = null, _acAllFiles = null;
    let _acStudentId = null, _acAllStudents = null;

    function setAcTarget(t) {
      _acTarget = t;
      document.querySelectorAll('[data-actgt]').forEach(b => b.classList.toggle('active', b.dataset.actgt === t));
      document.getElementById('acf-class-field').style.display = t === 'class' ? '' : 'none';
      document.getElementById('acf-user-field').style.display  = t === 'user'  ? '' : 'none';
      if (t === 'user' && !_acAllStudents) loadAcStudents();
    }

    async function loadAcStudents() {
      const drop = document.getElementById('acf-student-drop');
      drop.innerHTML = '<div style="padding:8px 12px;font-size:13px;color:#9ca3af">Загрузка…</div>';
      drop.style.display = '';
      try {
        _acAllStudents = await LS.getStudentsList();
        openAcStudentDrop();
      } catch(e) {
        _acAllStudents = [];
        drop.innerHTML = `<div style="padding:8px 12px;font-size:13px;color:#ef4444">Ошибка загрузки: ${e.message}</div>`;
      }
    }

    function filterAcStudents(q) {
      openAcStudentDrop(q);
    }

    function openAcStudentDrop(q) {
      const drop = document.getElementById('acf-student-drop');
      if (_acAllStudents === null) { loadAcStudents(); return; }
      const list = _acAllStudents;
      const term = (q !== undefined ? q : document.getElementById('acf-student-search').value).toLowerCase().trim();
      const filtered = term ? list.filter(s => s.name.toLowerCase().includes(term) || s.email.toLowerCase().includes(term)) : list;
      if (!filtered.length) {
        drop.innerHTML = '<div style="padding:8px 12px;font-size:13px;color:#9ca3af">Нет учеников</div>';
        drop.style.display = '';
        return;
      }
      drop.innerHTML = filtered.slice(0, 50).map(s =>
        `<div style="padding:8px 12px;cursor:pointer;border-bottom:1px solid #f3f4f6;font-size:13px" data-id="${s.id}" data-name="${esc(s.name)}" data-email="${esc(s.email)}" onmousedown="selectAcStudent(+this.dataset.id,this.dataset.name,this.dataset.email)" onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background=''">${esc(s.name)} <span style="color:#9ca3af">${esc(s.email)}</span></div>`
      ).join('');
      drop.style.display = '';
    }

    function closeAcStudentDrop() {
      document.getElementById('acf-student-drop').style.display = 'none';
    }

    function selectAcStudent(id, name, email) {
      _acStudentId = id;
      document.getElementById('acf-student-search').value = name;
      document.getElementById('acf-student-selected').textContent = `${name} (${email})`;
      document.getElementById('acf-student-selected').style.display = '';
      closeAcStudentDrop();
    }

    function setAcSrc(src) {
      _acSrc = src;
      document.querySelectorAll('[data-src]').forEach(b => b.classList.toggle('active', b.dataset.src === src));
      document.getElementById('acf-random-fields').style.display = src === 'random' ? '' : 'none';
      document.getElementById('acf-test-fields').style.display   = src === 'test'   ? '' : 'none';
      document.getElementById('acf-file-fields').style.display   = src === 'file'   ? '' : 'none';
      if (src === 'file' && !_acAllFiles) loadAcFiles();
    }

    async function loadAcFiles() {
      try {
        _acAllFiles = await LS.getFiles();
        renderAcFiles('');
      } catch { _acAllFiles = []; }
    }

    function renderAcFiles(q) {
      const el = document.getElementById('acf-file-list');
      if (!_acAllFiles) { el.innerHTML = '<div style="padding:10px;color:var(--text-3);font-size:.82rem;text-align:center">Загрузка…</div>'; return; }
      const lq = q.toLowerCase();
      const items = q ? _acAllFiles.filter(f => (f.title||'').toLowerCase().includes(lq)) : _acAllFiles;
      const SUBJ = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика' };
      if (!items.length) { el.innerHTML = '<div style="padding:10px;color:var(--text-3);font-size:.82rem;text-align:center">Нет файлов</div>'; return; }
      el.innerHTML = items.map(f => `
        <div onclick="selectAcFile(${f.id},'${esc(f.title||'Файл')}','${f.subject_slug||''}')"
             style="padding:9px 12px;cursor:pointer;border-bottom:1px solid rgba(15,23,42,0.07);display:flex;align-items:center;gap:8px;${_acFileId===f.id?'background:rgba(155,93,229,0.08);':''} transition:background .15s">
          <div style="flex:1">
            <div style="font-size:.84rem;font-weight:600">${esc(f.title||'Файл')}</div>
            <div style="font-size:.74rem;color:var(--text-3)">${SUBJ[f.subject_slug]||f.subject_slug||''}</div>
          </div>
          ${_acFileId===f.id ? '<span style="color:var(--violet)"><i data-lucide="check" style="width:15px;height:15px"></i></span>' : ''}
        </div>`).join('');
      if (window.lucide) lucide.createIcons();
    }

    function filterAcFiles(q) { renderAcFiles(q); }

    function selectAcFile(id, title, subject_slug) {
      _acFileId = id;
      renderAcFiles(document.getElementById('acf-file-search').value);
      const sel = document.getElementById('acf-file-selected');
      sel.textContent = 'Выбран: ' + title;
      sel.style.display = '';
    }

    async function openCreateAModal() {
      _acSrc = 'random'; _acTarget = 'class'; _acFileId = null; _acStudentId = null; _acAllStudents = null;
      setAcSrc('random');
      setAcTarget('class');
      loadAcStudents(); // preload students so search is ready
      document.getElementById('acf-title').value          = '';
      document.getElementById('acf-subject').value         = '';
      document.getElementById('acf-mode').value            = 'exam';
      document.getElementById('acf-mode-test').value       = 'exam';
      document.getElementById('acf-count').value           = '25';
      document.getElementById('acf-deadline').value        = '';
      document.getElementById('acf-student-search').value   = '';
      document.getElementById('acf-student-selected').style.display = 'none';
      _acStudentId = null;
      document.getElementById('acf-error').textContent     = '';
      document.getElementById('acf-file-search').value     = '';
      document.getElementById('acf-file-selected').style.display = 'none';

      // load classes and tests in parallel
      const [clsSel, testSel] = [document.getElementById('acf-class'), document.getElementById('acf-test')];
      clsSel.innerHTML = '<option value="">Загрузка…</option>';
      testSel.innerHTML = '<option value="">Загрузка…</option>';

      const [classesP, testsP] = await Promise.allSettled([LS.getClasses(), LS.getTests()]);

      if (classesP.status === 'fulfilled') {
        const classes = classesP.value;
        clsSel.innerHTML = classes.length
          ? '<option value="">— выберите класс —</option>' + classes.map(c => `<option value="${c.id}">${esc(c.name)} (${c.member_count} уч.)</option>`).join('')
          : '<option value="">Нет классов — создайте класс</option>';
      } else {
        clsSel.innerHTML = `<option value="">Ошибка загрузки классов</option>`;
      }

      if (testsP.status === 'fulfilled') {
        const tests = testsP.value;
        testSel.innerHTML = tests.length
          ? '<option value="">— выберите тест —</option>' + tests.map(t => `<option value="${t.id}">${esc(t.title)} (${t.question_count} вопр.)</option>`).join('')
          : '<option value="">Нет тестов — создайте тест</option>';
      } else {
        testSel.innerHTML = `<option value="">Ошибка загрузки тестов</option>`;
      }

      document.getElementById('ac-modal').classList.add('open');
      setTimeout(() => document.getElementById('acf-title').focus(), 80);
    }

    function closeCreateAModal() {
      document.getElementById('ac-modal').classList.remove('open');
    }

    async function saveNewAssignment() {
      const title    = document.getElementById('acf-title').value.trim();
      const deadline = document.getElementById('acf-deadline').value || null;
      const errEl    = document.getElementById('acf-error');
      errEl.textContent = '';
      if (!title) { errEl.textContent = 'Введите название'; return; }

      let payload = { title, deadline };

      if (_acSrc === 'file') {
        if (!_acFileId) { errEl.textContent = 'Выберите файл из библиотеки'; return; }
        const f = _acAllFiles.find(x => x.id === _acFileId);
        payload = { ...payload, file_id: _acFileId, subject_slug: f?.subject_slug || 'bio', mode: 'exam', count: 1 };
      } else if (_acSrc === 'test') {
        const test_id = document.getElementById('acf-test').value;
        const mode    = document.getElementById('acf-mode-test').value;
        if (!test_id) { errEl.textContent = 'Выберите тест'; return; }
        const selOpt = document.querySelector(`#acf-test option[value="${test_id}"]`);
        if (selOpt && selOpt.textContent.includes('(0 вопр.)')) { errEl.textContent = 'В выбранном тесте нет вопросов. Добавьте вопросы во вкладке «Тесты».'; return; }
        payload = { ...payload, test_id: Number(test_id), mode };
      } else {
        const subject_slug = document.getElementById('acf-subject').value;
        const mode         = document.getElementById('acf-mode').value;
        const count        = Number(document.getElementById('acf-count').value);
        if (!subject_slug) { errEl.textContent = 'Выберите предмет'; return; }
        if (!count || count < 1) { errEl.textContent = 'Укажите количество вопросов'; return; }
        payload = { ...payload, subject_slug, mode, count };
      }

      const btn = document.getElementById('acf-save');
      btn.disabled = true; btn.textContent = 'Создание…';
      try {
        if (_acTarget === 'user') {
          if (!_acStudentId) { errEl.textContent = 'Выберите ученика из списка'; btn.disabled=false; btn.textContent='Создать'; return; }
          await LS.createDirectAssignment({ ...payload, student_id: _acStudentId });
        } else {
          const class_id = document.getElementById('acf-class').value;
          if (!class_id) { errEl.textContent = 'Выберите класс'; btn.disabled=false; btn.textContent='Создать'; return; }
          await LS.createAssignment(class_id, payload);
        }
        closeCreateAModal();
        loadAssignments();
      } catch (e) {
        errEl.textContent = 'Ошибка: ' + e.message;
      } finally {
        btn.disabled = false; btn.textContent = 'Создать';
      }
    }

    async function deleteAsgn(id) {
      const a = allAssignments.find(x => x.id === id);
      if (!await LS.confirm(`Удалить задание «${a?.title}»?\nВсе связанные сессии будут удалены.`, { title: 'Удалить задание', confirmText: 'Удалить' })) return;
      try {
        await LS.deleteAssignment(id);
        allAssignments = allAssignments.filter(x => x.id !== id);
        renderAssignments();
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ════════════════════════════════════════════════
       ДОСТУПНЫЕ ТЕСТЫ — настройка предметов
    ════════════════════════════════════════════════ */
    let _subjConfigInited = false;
    const SC_MODES = { exam: 'Экзамен', practice: 'Пробный тест', topic: 'По теме', random: 'Случайный' };
    const SC_ICONS  = { bio:'dna', chem:'flask-conical', math:'calculator', phys:'zap' };
    const SC_COLORS = { bio:'#9B5DE5', chem:'#06D6A0', math:'#06B6D4', phys:'#F59E0B' };

    // кэш тестов по предмету для селектора
    const _scTests = {};
    async function loadScTests(slug) {
      if (_scTests[slug]) return _scTests[slug];
      const tests = await LS.getTests(slug);
      _scTests[slug] = tests;
      return tests;
    }

    function setSrcMode(slug, src) {
      const rndBtn  = document.getElementById(`sc-src-rnd-${slug}`);
      const fixBtn  = document.getElementById(`sc-src-fix-${slug}`);
      const pick    = document.getElementById(`sc-test-pick-${slug}`);
      const cntWrap = document.getElementById(`sc-count-wrap-${slug}`);
      rndBtn.classList.toggle('active', src === 'random');
      fixBtn.classList.toggle('active', src === 'fixed');
      pick.classList.toggle('open', src === 'fixed');
      cntWrap.style.display = src === 'random' ? '' : 'none';
      if (src === 'fixed') {
        loadAndRenderTestPick(slug);
      } else {
        // скрыть drawer вопросов при переключении на случайный
        const dr = document.getElementById(`sc-qdr-${slug}`);
        if (dr) { dr.style.display = 'none'; }
      }
    }

    async function loadAndRenderTestPick(slug) {
      const sel = document.getElementById(`sc-test-sel-${slug}`);
      if (sel.dataset.loaded) return;
      sel.innerHTML = '<option value="">Загрузка…</option>';
      try {
        const tests = await loadScTests(slug);
        const cur   = document.getElementById(`sc-card-${slug}`)?.dataset.testId || '';
        sel.innerHTML = `<option value="">— случайные вопросы —</option>` +
          tests.map(t => `<option value="${t.id}"${String(t.id) === cur ? ' selected' : ''}>${esc(t.title)} (${t.question_count ?? '?'} вопр.)</option>`).join('');
        sel.dataset.loaded = '1';
      } catch(e) {
        sel.innerHTML = '<option value="">Ошибка загрузки</option>';
      }
    }

    async function loadSubjectConfig() {
      const wrap = document.getElementById('subj-config-list');
      wrap.innerHTML = LS.skeleton(4);
      try {
        const subjects = await LS.getSubjects();
        wrap.innerHTML = subjects.map(s => {
          const hasFix = !!s.default_test_id;
          const color = SC_COLORS[s.slug] || '#9B5DE5';
          const mode = s.default_mode || 'exam';
          const count = s.default_count || 25;
          const srcLabel = hasFix ? 'Фикс. тест' : `${count} вопросов`;
          return `
          <div class="sc-card" id="sc-card-${s.slug}" data-test-id="${s.default_test_id || ''}">
            <div class="sc-row-top" onclick="toggleScCard('${s.slug}')">
              <div class="sc-icon" style="background:${color}"><i data-lucide="${SC_ICONS[s.slug]||'book'}"></i></div>
              <div class="sc-info">
                <div class="sc-name">${esc(s.name)}</div>
                <div class="sc-summary" id="sc-sum-${s.slug}">
                  <span class="sc-tag sc-tag-mode">${SC_MODES[mode]}</span>
                  <span class="sc-tag">${srcLabel}</span>
                  <span class="sc-qcount">${s.question_count ?? 0} в базе</span>
                </div>
              </div>
              <i data-lucide="chevron-down" class="sc-chevron"></i>
            </div>
            <div class="sc-body">
              <!-- Quick presets -->
              <div class="sc-presets">
                <button class="sc-preset${mode==='exam'&&count===25&&!hasFix?' active':''}" onclick="applyPreset('${s.slug}','exam',25)">Экзамен 25</button>
                <button class="sc-preset${mode==='exam'&&count===40&&!hasFix?' active':''}" onclick="applyPreset('${s.slug}','exam',40)">Экзамен 40</button>
                <button class="sc-preset${mode==='practice'&&count===15&&!hasFix?' active':''}" onclick="applyPreset('${s.slug}','practice',15)">Практика 15</button>
                <button class="sc-preset${mode==='practice'&&count===25&&!hasFix?' active':''}" onclick="applyPreset('${s.slug}','practice',25)">Практика 25</button>
              </div>
              <!-- Detailed fields -->
              <div class="sc-fields">
                <div class="sc-field">
                  <span class="sc-label">Режим</span>
                  <select class="sc-select" id="sc-mode-${s.slug}">
                    ${Object.entries(SC_MODES).map(([v, l]) =>
                      `<option value="${v}"${mode === v ? ' selected' : ''}>${l}</option>`
                    ).join('')}
                  </select>
                </div>
                <div class="sc-field">
                  <span class="sc-label">Источник</span>
                  <div class="sc-src-toggle">
                    <button class="sc-src-btn${hasFix ? '' : ' active'}" id="sc-src-rnd-${s.slug}" onclick="setSrcMode('${s.slug}','random')">Случайные</button>
                    <button class="sc-src-btn${hasFix ? ' active' : ''}" id="sc-src-fix-${s.slug}" onclick="setSrcMode('${s.slug}','fixed')">Из теста</button>
                  </div>
                </div>
                <div class="sc-field" id="sc-count-wrap-${s.slug}" style="${hasFix ? 'display:none' : ''}">
                  <span class="sc-label">Вопросов</span>
                  <input class="sc-input" type="number" id="sc-count-${s.slug}" min="5" max="100" value="${count}" />
                </div>
                <div class="sc-test-pick${hasFix ? ' open' : ''}" id="sc-test-pick-${s.slug}">
                  <div class="sc-field">
                    <span class="sc-label">Тест</span>
                    <select class="sc-select" id="sc-test-sel-${s.slug}" onchange="onScTestChange('${s.slug}')">
                      <option value="${s.default_test_id || ''}" selected>Загрузка...</option>
                    </select>
                  </div>
                  <button class="sc-save-add" id="sc-qdr-btn-${s.slug}" style="display:${hasFix?'':'none'};align-self:flex-start"
                    onclick="toggleScDrawer('${s.slug}')"><i data-lucide="list" style="width:13px;height:13px;vertical-align:-2px"></i> Вопросы</button>
                </div>
              </div>
              <!-- Footer -->
              <div class="sc-footer">
                <button class="sc-save" id="sc-save-btn-${s.slug}" onclick="saveSubjectConfig('${s.slug}')">Сохранить</button>
                <button class="sc-save-add" onclick="goAddQuestion('${s.slug}')"><i data-lucide="plus" style="width:13px;height:13px;vertical-align:-2px"></i> Вопрос</button>
              </div>
            </div>
          </div>
          <div id="sc-qdr-${s.slug}" style="display:none;border-top:1px solid var(--border);padding:20px 24px;background:rgba(238,242,255,0.5)">
            <div id="sc-qdr-inner-${s.slug}"></div>
          </div>`;
        }).join('');
        if (window.lucide) lucide.createIcons();
        // pre-load test selectors and show Вопросы button for subjects already using a fixed test
        subjects.filter(s => s.default_test_id).forEach(s => {
          loadAndRenderTestPick(s.slug);
          const btn = document.getElementById(`sc-qdr-btn-${s.slug}`);
          if (btn) btn.style.display = '';
        });
      } catch (e) {
        wrap.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    function toggleScCard(slug) {
      const card = document.getElementById('sc-card-' + slug);
      if (!card) return;
      const wasOpen = card.classList.contains('open');
      // Close all
      document.querySelectorAll('.sc-card.open').forEach(c => c.classList.remove('open'));
      if (!wasOpen) {
        card.classList.add('open');
        if (window.lucide) lucide.createIcons({ nodes: [card] });
      }
    }

    function applyPreset(slug, mode, count) {
      document.getElementById('sc-mode-' + slug).value = mode;
      document.getElementById('sc-count-' + slug).value = count;
      setSrcMode(slug, 'random');
      // Highlight active preset
      const card = document.getElementById('sc-card-' + slug);
      card.querySelectorAll('.sc-preset').forEach(p => p.classList.remove('active'));
      const isFix = document.getElementById('sc-src-fix-' + slug).classList.contains('active');
      card.querySelectorAll('.sc-preset').forEach(p => {
        const txt = p.textContent.trim();
        const mLabel = SC_MODES[mode];
        if (txt === mLabel + ' ' + count && !isFix) p.classList.add('active');
      });
      // Auto-save
      saveSubjectConfig(slug);
    }

    function updateScSummary(slug) {
      const el = document.getElementById('sc-sum-' + slug);
      if (!el) return;
      const mode = document.getElementById('sc-mode-' + slug).value;
      const isFix = document.getElementById('sc-src-fix-' + slug).classList.contains('active');
      const count = document.getElementById('sc-count-' + slug).value;
      const srcLabel = isFix ? 'Фикс. тест' : count + ' вопросов';
      el.innerHTML = `<span class="sc-tag sc-tag-mode">${SC_MODES[mode]}</span><span class="sc-tag">${srcLabel}</span>`;
    }

    function scPreviewText(s) {
      if (s.default_test_id) return `На дашборде: «${SC_MODES[s.default_mode || 'exam']}», фиксированный тест`;
      return `На дашборде: «${SC_MODES[s.default_mode || 'exam']}», ${s.default_count || 25} вопросов (случайные)`;
    }

    async function saveSubjectConfig(slug) {
      const btn = document.getElementById(`sc-save-btn-${slug}`);
      const mode   = document.getElementById(`sc-mode-${slug}`).value;
      const isFix  = document.getElementById(`sc-src-fix-${slug}`).classList.contains('active');
      const count  = Number(document.getElementById(`sc-count-${slug}`)?.value || 25);
      const testId = isFix ? (document.getElementById(`sc-test-sel-${slug}`).value || null) : null;

      if (btn) { btn.disabled = true; btn.textContent = '...'; }
      const payload = { default_mode: mode, default_count: count, default_test_id: testId ? Number(testId) : null };
      try {
        await LS.updateSubject(slug, payload);
        document.getElementById(`sc-card-${slug}`).dataset.testId = testId || '';
        if (isFix) document.getElementById(`sc-test-sel-${slug}`).dataset.loaded = '';
        updateScSummary(slug);
        // Visual feedback
        if (btn) { btn.classList.add('saved'); btn.textContent = 'Сохранено'; }
        setTimeout(() => { if (btn) { btn.classList.remove('saved'); btn.textContent = 'Сохранить'; btn.disabled = false; } }, 1500);
      } catch (e) {
        LS.toast('Ошибка: ' + e.message, 'error');
        if (btn) { btn.disabled = false; btn.textContent = 'Сохранить'; }
      }
    }

    /* ── Subject drawer: управление вопросами теста ── */
    function onScTestChange(slug) {
      const tid = document.getElementById(`sc-test-sel-${slug}`).value;
      const btn = document.getElementById(`sc-qdr-btn-${slug}`);
      btn.style.display = tid ? '' : 'none';
      // скрыть drawer если тест сменился
      const dr = document.getElementById(`sc-qdr-${slug}`);
      dr.style.display = 'none';
      document.getElementById(`sc-qdr-inner-${slug}`).innerHTML = '';
    }

    const _scDrOpen = {}; // slug <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> bool
    async function toggleScDrawer(slug) {
      const dr  = document.getElementById(`sc-qdr-${slug}`);
      const tid = Number(document.getElementById(`sc-test-sel-${slug}`).value);
      if (!tid) return;
      if (dr.style.display !== 'none') { dr.style.display = 'none'; return; }
      dr.style.display = '';
      await renderScDrawer(slug, tid);
    }

    const _scCache = {}; // tid <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg> { test, subjectQs }
    async function renderScDrawer(slug, tid) {
      const inner = document.getElementById(`sc-qdr-inner-${slug}`);
      inner.innerHTML = LS.skeleton(3, 'row');
      try {
        const [t, subjectQs] = await Promise.all([
          LS.getTest(tid),
          LS.getQuestions(slug, null, 'date_asc').catch(() => []),
        ]);
        _scCache[tid] = { test: t, subjectQs };
        inner.innerHTML = `
          <div class="tst-cols">
            <div>
              <div class="tst-panel-title">Вопросы в тесте (<span id="sc-qcnt-${tid}">${t.questions.length}</span>)</div>
              <div class="tst-q-list" id="sc-ql-${tid}">${renderScQList(t.questions, tid, slug)}</div>
            </div>
            <div>
              <div class="tst-panel-title">Добавить из базы</div>
              <input class="tst-search" placeholder="Поиск…" oninput="filterScPicker(${tid},'${slug}',this.value)" />
              <div class="tst-q-list" id="sc-pick-${tid}">${renderScPicker(subjectQs, new Set(t.questions.map(q=>q.id)), tid, slug)}</div>
            </div>
          </div>`;
        renderMath(inner);
      } catch(e) {
        inner.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    function renderScQList(questions, tid, slug) {
      if (!questions.length) return '<div class="tst-empty">Пусто. Добавьте вопросы справа <svg class="ic" viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg></div>';
      return questions.map((q,i) => `
        <div class="tst-q-item" id="sc-qi-${tid}-${q.id}">
          <span class="tst-q-num">${i+1}.</span>
          <div class="tst-q-body">
            <span class="tst-q-text">${esc(q.text)}</span>
            <div class="tst-q-meta">
              <span class="tst-q-badge diff-${q.difficulty}">${DIFF_LABELS[q.difficulty]||q.difficulty}</span>
              ${qTypeBadge(q.type)}
              ${qOptsPreview(q)}
            </div>
          </div>
          <button class="btn-tst-rem" onclick="scRemoveQ(${tid},'${slug}',${q.id})" title="Убрать">−</button>
        </div>`).join('');
    }

    function renderScPicker(questions, inIds, tid, slug) {
      if (!questions.length) return '<div class="tst-empty">Вопросов нет в этом предмете</div>';
      return questions.map(q => {
        const added = inIds.has(q.id);
        return `
          <div class="tst-q-item" id="sc-pick-item-${tid}-${q.id}" style="${added?'opacity:0.4;pointer-events:none':''}">
            <div class="tst-q-body" style="flex:1">
              <span class="tst-q-text">${esc(q.text)}</span>
              <div class="tst-q-meta">
                <span class="tst-q-badge diff-${q.difficulty}">${DIFF_LABELS[q.difficulty]||q.difficulty}</span>
                ${qTypeBadge(q.type)}
                ${q.topic ? `<span class="tst-q-badge" style="background:rgba(6,214,224,0.1);color:#05aab3">${esc(q.topic)}</span>` : ''}
              </div>
            </div>
            <button class="btn-tst-add" id="sc-add-btn-${tid}-${q.id}" onclick="scAddQ(${tid},'${slug}',${q.id},this)" title="Добавить">${added?'<i data-lucide="check" style="width:14px;height:14px"></i>':'+' }</button>
          </div>`;
      }).join('');
    }

    function filterScPicker(tid, slug, q) {
      const cache = _scCache[tid];
      if (!cache) return;
      const lq  = q.toLowerCase();
      const filtered = lq.length < 1
        ? cache.subjectQs
        : cache.subjectQs.filter(x => x.text.toLowerCase().includes(lq) || (x.topic||'').toLowerCase().includes(lq));
      const inIds = new Set(cache.test.questions.map(x=>x.id));
      document.getElementById(`sc-pick-${tid}`).innerHTML = renderScPicker(filtered, inIds, tid, slug);
    }

    async function scAddQ(tid, slug, qid, btn) {
      btn.disabled = true; btn.textContent = '…';
      try {
        await LS.addQuestionsToTest(tid, [qid]);
        // обновить кэш и списки
        const t = await LS.getTest(tid);
        _scCache[tid].test = t;
        const inIds = new Set(t.questions.map(q=>q.id));
        document.getElementById(`sc-ql-${tid}`).innerHTML   = renderScQList(t.questions, tid, slug);
        document.getElementById(`sc-qcnt-${tid}`).textContent = t.questions.length;
        // пометить кнопку в пикере
        const item = document.getElementById(`sc-pick-item-${tid}-${qid}`);
        if (item) { item.style.opacity='0.4'; item.style.pointerEvents='none'; }
        const addBtn = document.getElementById(`sc-add-btn-${tid}-${qid}`);
        if (addBtn) { addBtn.innerHTML = '<i data-lucide="check" style="width:14px;height:14px"></i>'; if(window.lucide)lucide.createIcons(); }
        renderMath(document.getElementById(`sc-ql-${tid}`));
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); btn.disabled=false; btn.textContent='+'; }
    }

    async function scRemoveQ(tid, slug, qid) {
      try {
        await LS.removeQFromTest(tid, qid);
        const t = await LS.getTest(tid);
        _scCache[tid].test = t;
        const inIds = new Set(t.questions.map(q=>q.id));
        document.getElementById(`sc-ql-${tid}`).innerHTML    = renderScQList(t.questions, tid, slug);
        document.getElementById(`sc-qcnt-${tid}`).textContent = t.questions.length;
        // разблокировать в пикере
        const item = document.getElementById(`sc-pick-item-${tid}-${qid}`);
        if (item) { item.style.opacity=''; item.style.pointerEvents=''; }
        const addBtn = document.getElementById(`sc-add-btn-${tid}-${qid}`);
        if (addBtn) { addBtn.textContent='+'; addBtn.disabled=false; }
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ─── User permissions modal ───────────────────────────────────────── */
    let _upPermsData = null;

    function closeUserPermsModal() {
      document.getElementById('up-modal').classList.remove('open');
      _upPermsData = null;
    }

    async function openUserPermsModal() {
      if (!activeUid) return;
      const name = document.getElementById('up-name').textContent;
      document.getElementById('up-modal-title').textContent = `Права: ${name}`;
      document.getElementById('up-modal-list').innerHTML = LS.skeleton(5, 'row');
      document.getElementById('up-modal').classList.add('open');
      try {
        _upPermsData = await LS.getUserPermissions(activeUid);
        renderUserPerms();
      } catch(e) {
        document.getElementById('up-modal-list').innerHTML = `<p style="color:var(--danger);font-size:13px">Ошибка: ${esc(e.message)}</p>`;
      }
    }

    function renderUserPerms() {
      if (!_upPermsData) return;
      const list = document.getElementById('up-modal-list');
      list.innerHTML = _upPermsData.permissions.map(p => {
        const hasOverride = p.userVal !== undefined;
        const checked = p.effective;
        const badge = hasOverride
          ? `<span style="font-size:10px;padding:2px 7px;border-radius:var(--r-pill);background:rgba(155,93,229,0.12);color:var(--violet);font-weight:700">Инд.</span>`
          : `<span style="font-size:10px;padding:2px 7px;border-radius:var(--r-pill);background:rgba(136,152,170,0.12);color:var(--text-3);font-weight:700">По роли</span>`;
        const resetBtn = hasOverride
          ? `<button style="background:none;border:none;cursor:pointer;color:var(--text-3);padding:3px 6px;border-radius:6px;font-size:11px;font-weight:700;transition:color .2s"
               onmouseover="this.style.color='var(--danger)'" onmouseout="this.style.color='var(--text-3)'"
               onclick="doResetOneUserPerm('${esc(p.key)}')" title="Сбросить к роли">×</button>`
          : '';
        return `
          <div class="perm-card${checked ? ' enabled' : ''}" id="up-perm-card-${p.key.replace('.','_')}">
            <div class="perm-info">
              <div style="display:flex;align-items:center;gap:7px">
                <span class="perm-label">${esc(p.label)}</span>
                ${badge}
                ${resetBtn}
              </div>
              <div class="perm-desc">${esc(p.desc)}</div>
            </div>
            <label class="perm-toggle">
              <input type="checkbox" ${checked ? 'checked' : ''}
                onchange="doSetUserPerm('${esc(p.key)}', this.checked, this)">
              <span class="perm-track"></span>
              <span class="perm-thumb"></span>
            </label>
          </div>`;
      }).join('');
      // update reset-all btn visibility
      const hasAny = _upPermsData.permissions.some(p => p.userVal !== undefined);
      document.getElementById('up-modal-reset-btn').style.opacity = hasAny ? '1' : '0.4';
    }

    async function doSetUserPerm(key, enabled, checkbox) {
      checkbox.disabled = true;
      try {
        await LS.setUserPermission(activeUid, key, enabled);
        _upPermsData = await LS.getUserPermissions(activeUid);
        renderUserPerms();
        LS.toast(enabled ? 'Право включено' : 'Право отключено', 'success');
      } catch(e) {
        checkbox.checked = !enabled;
        LS.toast('Ошибка: ' + e.message, 'error');
      } finally {
        checkbox.disabled = false;
      }
    }

    async function doResetOneUserPerm(key) {
      try {
        await LS.resetUserPermissions(activeUid, key);
        _upPermsData = await LS.getUserPermissions(activeUid);
        renderUserPerms();
        LS.toast('Сброшено к значению роли', 'success');
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    async function doResetAllUserPerms() {
      const name = document.getElementById('up-name').textContent;
      if (!await LS.confirm(`Сбросить все индивидуальные права «${name}»?\nБудут применены права роли.`, { title: 'Сбросить права', confirmText: 'Сбросить' })) return;
      try {
        await LS.resetUserPermissions(activeUid);
        _upPermsData = await LS.getUserPermissions(activeUid);
        renderUserPerms();
        LS.toast('Права сброшены к роли', 'success');
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ─── Permissions tab ──────────────────────────────────────────────── */
    let _permData = null;

    async function loadPermissions() {
      try {
        _permData = await LS.getPermissions();
        renderPermissions();
      } catch(e) {
        document.getElementById('perm-teacher').innerHTML =
          `<p style="color:var(--danger);font-size:13px">Ошибка загрузки: ${esc(e.message)}</p>`;
      }
    }

    function renderPermissions() {
      if (!_permData) return;
      const { permissions, definitions } = _permData;
      ['teacher', 'student'].forEach(role => {
        const container = document.getElementById('perm-' + role);
        const defs = definitions.filter(d => d.role === role);
        container.innerHTML = defs.map(def => {
          const enabled = permissions[role]?.[def.key] ?? def.default;
          return `
            <div class="perm-card${enabled ? ' enabled' : ''}" id="perm-card-${role}-${def.key.replace('.','_')}">
              <div class="perm-info">
                <div class="perm-label">${esc(def.label)}</div>
                <div class="perm-desc">${esc(def.desc)}</div>
              </div>
              <label class="perm-toggle" title="${enabled ? 'Выключить' : 'Включить'}">
                <input type="checkbox" ${enabled ? 'checked' : ''}
                  onchange="togglePermission('${esc(role)}','${esc(def.key)}',this.checked,this)">
                <span class="perm-track"></span>
                <span class="perm-thumb"></span>
              </label>
            </div>`;
        }).join('');
      });
    }

    async function togglePermission(role, key, enabled, checkbox) {
      checkbox.disabled = true;
      try {
        await LS.setPermission(role, key, enabled);
        // update local cache
        if (!_permData.permissions[role]) _permData.permissions[role] = {};
        _permData.permissions[role][key] = enabled;
        // update card style
        const safeKey = key.replace('.', '_');
        const card = document.getElementById(`perm-card-${role}-${safeKey}`);
        if (card) card.classList.toggle('enabled', enabled);
        LS.toast(enabled ? 'Право включено' : 'Право отключено', 'success');
      } catch(e) {
        checkbox.checked = !enabled; // revert
        LS.toast('Ошибка: ' + e.message, 'error');
      } finally {
        checkbox.disabled = false;
      }
    }

    /* ════════════════════════════════════════════════
       МАГАЗИН (Shop)
    ════════════════════════════════════════════════ */
    let _shopItems = [];
    let _shopEditId = null;

    async function loadShopAdmin() {
      try {
        const [stats, items] = await Promise.all([
          LS.adminShopStats(),
          LS.adminShopGetItems()
        ]);
        const topName = stats.topItems?.[0]?.name || '—';
        document.getElementById('shop-stats-grid').innerHTML = `
          <div class="stat-card" style="--stat-top:var(--violet)">
            <div class="stat-card-icon" style="background:rgba(155,93,229,0.1)"><i data-lucide="shopping-bag" class="stat-icon"></i></div>
            <div class="stat-val violet">${stats.activeItems}/${stats.totalItems}</div>
            <div class="stat-label">Товаров</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--cyan)">
            <div class="stat-card-icon" style="background:rgba(6,214,224,0.1)"><i data-lucide="receipt" class="stat-icon"></i></div>
            <div class="stat-val cyan">${stats.totalPurchases}</div>
            <div class="stat-label">Покупок</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--green)">
            <div class="stat-card-icon" style="background:rgba(6,214,100,0.1)"><i data-lucide="coins" class="stat-icon"></i></div>
            <div class="stat-val green">${stats.totalCoinsInCirculation}</div>
            <div class="stat-label">Монет в обороте</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--amber, #FFB347)">
            <div class="stat-card-icon" style="background:rgba(255,179,71,0.1)"><i data-lucide="star" class="stat-icon"></i></div>
            <div class="stat-val" style="color:var(--amber, #FFB347);font-size:1.1rem">${esc(topName)}</div>
            <div class="stat-label">Топ товар</div>
          </div>`;
        _shopItems = items;
        renderShopItems();
        if (window.lucide) lucide.createIcons();
      } catch(e) {
        document.getElementById('shop-stats-grid').innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    function renderShopItems() {
      const body = document.getElementById('shop-items-body');
      if (!_shopItems.length) { body.innerHTML = '<tr><td colspan="7" class="empty">Нет товаров</td></tr>'; return; }
      const typeLabels = { frame:'Рамка', title:'Титул', theme:'Тема', effect:'Эффект' };
      body.innerHTML = _shopItems.map(it => `<tr>
        <td>${it.id}</td>
        <td><strong>${esc(it.name)}</strong></td>
        <td><span class="mode-badge mode-practice">${typeLabels[it.type] || esc(it.type)}</span></td>
        <td>${it.price} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px;color:var(--amber, #FFB347)"></i></td>
        <td>${it.sold_count || 0}</td>
        <td>
          <label class="adm-toggle">
            <input type="checkbox" ${it.is_active ? 'checked' : ''} onchange="shopAdminToggleActive(${it.id}, this.checked)" />
            <span class="track"></span><span class="thumb"></span>
          </label>
        </td>
        <td>
          <button class="btn-edit-q" onclick="shopAdminEditItem(${it.id})">Ред.</button>
          <button class="btn-del-q" onclick="shopAdminDeleteItem(${it.id})">Удалить</button>
        </td>
      </tr>`).join('');
      if (window.lucide) lucide.createIcons();
    }

    function shopAdminCreateItem() {
      _shopEditId = null;
      document.getElementById('shop-form-title').textContent = 'Добавить товар';
      document.getElementById('shop-f-name').value = '';
      document.getElementById('shop-f-type').value = 'frame';
      document.getElementById('shop-f-price').value = '100';
      document.getElementById('shop-f-desc').value = '';
      document.getElementById('shop-f-icon').value = '';
      document.getElementById('shop-f-data').value = '';
      document.getElementById('shop-f-active').checked = true;
      document.getElementById('shop-item-form').style.display = '';
    }

    function shopAdminEditItem(id) {
      const it = _shopItems.find(i => i.id === id);
      if (!it) return;
      _shopEditId = id;
      document.getElementById('shop-form-title').textContent = 'Редактировать товар #' + id;
      document.getElementById('shop-f-name').value = it.name || '';
      document.getElementById('shop-f-type').value = it.type || 'frame';
      document.getElementById('shop-f-price').value = it.price ?? 100;
      document.getElementById('shop-f-desc').value = it.description || '';
      document.getElementById('shop-f-icon').value = it.icon || '';
      document.getElementById('shop-f-data').value = it.data ? (typeof it.data === 'string' ? it.data : JSON.stringify(it.data)) : '';
      document.getElementById('shop-f-active').checked = !!it.is_active;
      document.getElementById('shop-item-form').style.display = '';
    }

    function shopAdminCancelForm() {
      document.getElementById('shop-item-form').style.display = 'none';
      _shopEditId = null;
    }

    let _shopSaving = false;
    async function shopAdminSaveItem() {
      if (_shopSaving) return;
      _shopSaving = true;
      const data = {
        name: document.getElementById('shop-f-name').value.trim(),
        type: document.getElementById('shop-f-type').value,
        price: parseInt(document.getElementById('shop-f-price').value) || 0,
        description: document.getElementById('shop-f-desc').value.trim(),
        icon: document.getElementById('shop-f-icon').value.trim(),
        data: document.getElementById('shop-f-data').value.trim() || null,
        is_active: document.getElementById('shop-f-active').checked ? 1 : 0
      };
      if (!data.name) { LS.toast('Введите название', 'error'); return; }
      try {
        if (_shopEditId) {
          await LS.adminShopUpdateItem(_shopEditId, data);
          LS.toast('Товар обновлён', 'success');
        } else {
          await LS.adminShopCreateItem(data);
          LS.toast('Товар создан', 'success');
        }
        shopAdminCancelForm();
        shopInited = false;
        loadShopAdmin();
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
      finally { _shopSaving = false; }
    }

    async function shopAdminDeleteItem(id) {
      if (!await LS.confirm('Все покупки этого товара будут удалены.', { title: 'Удалить товар?', confirmText: 'Удалить', danger: true })) return;
      try {
        await LS.adminShopDeleteItem(id);
        LS.toast('Товар удалён', 'success');
        shopInited = false;
        loadShopAdmin();
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    async function shopAdminToggleActive(id, active) {
      try {
        await LS.adminShopUpdateItem(id, { is_active: active ? 1 : 0 });
        LS.toast(active ? 'Товар активирован' : 'Товар деактивирован', 'success');
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    let _shopSearchTimer = null;
    async function shopSearchUser(q) {
      clearTimeout(_shopSearchTimer);
      const box = document.getElementById('shop-award-results');
      if (q.length < 2) { box.classList.remove('open'); return; }
      _shopSearchTimer = setTimeout(async () => {
        try {
          const r = await LS.adminGetUsers({ q, limit: 8 });
          box.innerHTML = (r.users || []).map(u => `<div class="us-item" onclick="shopPickUser(${u.id}, '${esc(u.name || u.email)}')">
            <span>${esc(u.name || u.email)}</span><span class="us-role">${u.role}</span>
          </div>`).join('') || '<div class="us-item" style="color:var(--text-3)">Не найдено</div>';
          box.classList.add('open');
        } catch(e) { box.classList.remove('open'); }
      }, 300);
    }
    function shopPickUser(id, name) {
      document.getElementById('shop-award-uid').value = id;
      document.getElementById('shop-award-user').value = name;
      document.getElementById('shop-award-results').classList.remove('open');
    }

    let _coinsAwarding = false;
    async function shopAdminAwardCoins() {
      if (_coinsAwarding) return;
      const userId = parseInt(document.getElementById('shop-award-uid').value);
      const amount = parseInt(document.getElementById('shop-award-amount').value);
      const reason = document.getElementById('shop-award-reason').value.trim();
      if (!userId) { LS.toast('Выберите пользователя', 'error'); return; }
      if (!amount || amount <= 0) { LS.toast('Введите количество монет', 'error'); return; }
      _coinsAwarding = true;
      try {
        const r = await LS.adminShopAwardCoins({ userId, amount, reason });
        LS.toast(`Начислено ${amount} монет. Баланс: ${r.coins}`, 'success');
        document.getElementById('shop-award-uid').value = '';
        document.getElementById('shop-award-user').value = '';
        document.getElementById('shop-award-reason').value = '';
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
      finally { _coinsAwarding = false; }
    }

    /* ════════════════════════════════════════════════
       ГЕЙМИФИКАЦИЯ (Gamification)
    ════════════════════════════════════════════════ */
    async function loadGamAdmin() {
      try {
        const stats = await LS.adminGamStats();
        document.getElementById('gam-stats-grid').innerHTML = `
          <div class="stat-card" style="--stat-top:var(--violet)">
            <div class="stat-card-icon" style="background:rgba(155,93,229,0.1)"><i data-lucide="zap" class="stat-icon"></i></div>
            <div class="stat-val violet">${stats.totalXP}</div>
            <div class="stat-label">Суммарный XP</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--cyan)">
            <div class="stat-card-icon" style="background:rgba(6,214,224,0.1)"><i data-lucide="coins" class="stat-icon"></i></div>
            <div class="stat-val cyan">${stats.totalCoins}</div>
            <div class="stat-label">Суммарные монеты</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--green)">
            <div class="stat-card-icon" style="background:rgba(6,214,100,0.1)"><i data-lucide="bar-chart-3" class="stat-icon"></i></div>
            <div class="stat-val green">${(stats.avgLevel ?? 0).toFixed(1)}</div>
            <div class="stat-label">Средний уровень</div>
          </div>
          <div class="stat-card" style="--stat-top:var(--amber, #FFB347)">
            <div class="stat-card-icon" style="background:rgba(255,179,71,0.1)"><i data-lucide="trophy" class="stat-icon"></i></div>
            <div class="stat-val" style="color:var(--amber, #FFB347)">${stats.achievementCount}</div>
            <div class="stat-label">Достижений выдано</div>
          </div>
          <div class="stat-card" style="--stat-top:#FF9F1C">
            <div class="stat-card-icon" style="background:rgba(255,159,28,0.1)"><i data-lucide="shopping-bag" class="stat-icon"></i></div>
            <div class="stat-val" style="color:#FF9F1C">${stats.totalPurchases || 0}</div>
            <div class="stat-label">Покупок</div>
          </div>`;

        // Top-10
        const topBody = document.getElementById('gam-top-body');
        if (stats.topByXP?.length) {
          topBody.innerHTML = stats.topByXP.slice(0, 10).map((u, i) => `<tr>
            <td><strong>${i + 1}</strong></td>
            <td>${esc(u.name || u.email || 'ID:' + (u.id || u.user_id))}</td>
            <td><span style="color:var(--violet);font-weight:700">${u.xp}</span></td>
            <td>${u.level}</td>
            <td>${u.coins} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px;color:var(--amber, #FFB347)"></i></td>
          </tr>`).join('');
        } else {
          topBody.innerHTML = '<tr><td colspan="5" class="empty">Нет данных</td></tr>';
        }

        // Recent XP
        const XP_REASONS = {
          'daily_activity': ['sun',          '#F59E0B', 'Ежедневная активность'],
          'correct_answers':['check-circle', '#10B981', 'Правильные ответы'],
          'test_complete':  ['file-text',    '#06B6D4', 'Тест завершён'],
          'test_90+':       ['zap',          '#9B5DE5', 'Тест на 90%+'],
          'test_perfect':   ['trophy',       '#F59E0B', 'Идеальный тест (100%)'],
          'lab_experiment': ['atom',         '#06D6A0', 'Лабораторный эксперимент'],
          'daily_goal':     ['target',       '#EF476F', 'Ежедневная цель выполнена'],
          'Admin award':    ['crown',        '#9B5DE5', 'Начисление администратором'],
        };
        function fmtXPReason(reason) {
          if (!reason) return '—';
          const entry = XP_REASONS[reason];
          if (entry) {
            const [icon, color, label] = entry;
            return `<span style="display:inline-flex;align-items:center;gap:5px"><span style="color:${color};display:inline-flex">${lsIcon(icon,14)}</span>${label}</span>`;
          }
          if (reason.startsWith('achievement:')) {
            return `<span style="display:inline-flex;align-items:center;gap:5px"><span style="color:#F59E0B;display:inline-flex">${lsIcon('award',14)}</span>Достижение: ${esc(reason.slice(12))}</span>`;
          }
          if (reason.startsWith('Испытание:')) {
            return `<span style="display:inline-flex;align-items:center;gap:5px"><span style="color:#EF476F;display:inline-flex">${lsIcon('swords',14)}</span>${esc(reason)}</span>`;
          }
          return esc(reason);
        }

        const logBody = document.getElementById('gam-log-body');
        if (stats.recentXP?.length) {
          logBody.innerHTML = stats.recentXP.slice(0, 20).map(e => `<tr>
            <td style="font-size:0.78rem;color:var(--text-3)">${fmtDate(e.created_at || e.date)}</td>
            <td>${esc(e.name || e.user_name || '—')}</td>
            <td><span style="color:var(--violet);font-weight:700">+${e.amount}</span></td>
            <td style="font-size:0.82rem;color:var(--text-2)">${fmtXPReason(e.reason)}</td>
          </tr>`).join('');
        } else {
          logBody.innerHTML = '<tr><td colspan="4" class="empty">Нет данных</td></tr>';
        }

        // Purchases
        const purchBody = document.getElementById('gam-purchases-body');
        if (stats.recentPurchases?.length) {
          purchBody.innerHTML = stats.recentPurchases.slice(0, 20).map(p => `<tr>
            <td style="font-size:0.78rem;color:var(--text-3)">${fmtDate(p.purchased_at)}</td>
            <td>${esc(p.user_name || '—')}</td>
            <td style="font-weight:600">${esc(p.item_name || '—')}</td>
            <td><span class="badge" style="font-size:0.7rem">${esc(p.type || '—')}</span></td>
            <td style="color:var(--amber,#FFB347);font-weight:700">${p.price} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px"></i></td>
          </tr>`).join('');
        } else {
          purchBody.innerHTML = '<tr><td colspan="5" class="empty">Нет покупок</td></tr>';
        }

        if (window.lucide) lucide.createIcons();
      } catch(e) {
        document.getElementById('gam-stats-grid').innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    let _gamSearchTimer = null;
    async function gamSearchUser(q, prefix) {
      clearTimeout(_gamSearchTimer);
      const box = document.getElementById(prefix + '-results');
      if (q.length < 2) { box.classList.remove('open'); return; }
      _gamSearchTimer = setTimeout(async () => {
        try {
          const r = await LS.adminGetUsers({ q, limit: 8 });
          box.innerHTML = (r.users || []).map(u => `<div class="us-item" onclick="gamPickUser(${u.id}, '${esc(u.name || u.email)}', '${prefix}')">
            <span>${esc(u.name || u.email)}</span><span class="us-role">${u.role}</span>
          </div>`).join('') || '<div class="us-item" style="color:var(--text-3)">Не найдено</div>';
          box.classList.add('open');
        } catch(e) { box.classList.remove('open'); }
      }, 300);
    }
    function gamPickUser(id, name, prefix) {
      document.getElementById(prefix + '-uid').value = id;
      document.getElementById(prefix + '-user').value = name;
      document.getElementById(prefix + '-results').classList.remove('open');
    }

    let _gamAwarding = false;
    async function gamAdminAward() {
      if (_gamAwarding) return;
      const userId = parseInt(document.getElementById('gam-award-uid').value);
      const xp = parseInt(document.getElementById('gam-award-xp').value) || 0;
      const coins = parseInt(document.getElementById('gam-award-coins').value) || 0;
      const reason = document.getElementById('gam-award-reason').value.trim();
      if (!userId) { LS.toast('Выберите пользователя', 'error'); return; }
      if (!xp && !coins) { LS.toast('Введите XP или монеты', 'error'); return; }
      _gamAwarding = true;
      try {
        const r = await LS.adminGamAward({ userId, xp, coins, reason });
        LS.toast(`Начислено! XP: ${r.xp}, Уровень: ${r.level}, Монеты: ${r.coins}`, 'success');
        document.getElementById('gam-award-uid').value = '';
        document.getElementById('gam-award-user').value = '';
        document.getElementById('gam-award-reason').value = '';
        gamInited = false;
        loadGamAdmin();
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
      finally { _gamAwarding = false; }
    }

    async function gamAdminReset() {
      const userId = parseInt(document.getElementById('gam-reset-uid').value);
      const userName = document.getElementById('gam-reset-user').value;
      if (!userId) { LS.toast('Выберите пользователя', 'error'); return; }
      if (!await LS.confirm(`ВСЕ XP, монеты и достижения «${userName}» будут удалены безвозвратно.`, { title: 'Сбросить прогресс?', confirmText: 'Сбросить', danger: true })) return;
      try {
        await LS.adminGamReset({ userId });
        LS.toast('Прогресс сброшен', 'success');
        document.getElementById('gam-reset-uid').value = '';
        document.getElementById('gam-reset-user').value = '';
        gamInited = false;
        loadGamAdmin();
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ════════════════════════════════════════════════
       ШАБЛОНЫ (Templates)
    ════════════════════════════════════════════════ */
    async function loadTplAdmin() {
      try {
        const [courses, lessons] = await Promise.all([
          LS.getCourseTemplates().catch(() => []),
          LS.getLessonTemplates().catch(() => [])
        ]);
        renderTplTable('tpl-course-body', courses, 'courses');
        renderTplTable('tpl-lesson-body', lessons, 'lessons');
        if (window.lucide) lucide.createIcons();
      } catch(e) {
        document.getElementById('tpl-course-body').innerHTML = `<tr><td colspan="7" class="error">Ошибка: ${esc(e.message)}</td></tr>`;
      }
    }

    function renderTplTable(bodyId, items, type) {
      const body = document.getElementById(bodyId);
      if (!items || !items.length) {
        body.innerHTML = '<tr><td colspan="7" class="empty">Нет шаблонов</td></tr>';
        return;
      }
      body.innerHTML = items.map(t => `<tr>
        <td>${t.id}</td>
        <td><strong>${esc(t.name || t.title || '—')}</strong></td>
        <td>${esc(t.subject || '—')}</td>
        <td>${esc(t.category || '—')}</td>
        <td>${esc(t.author_name || t.author || '—')}</td>
        <td>
          <label class="adm-toggle">
            <input type="checkbox" ${t.is_public ? 'checked' : ''} onchange="tplTogglePublic('${type}', ${t.id}, this.checked)" />
            <span class="track"></span><span class="thumb"></span>
          </label>
        </td>
        <td>
          <button class="btn-del-q" onclick="tplDelete('${type}', ${t.id})">Удалить</button>
        </td>
      </tr>`).join('');
    }

    async function tplTogglePublic(type, id, isPublic) {
      try {
        const endpoint = type === 'courses' ? '/api/templates/courses/' : '/api/templates/lessons/';
        await LS.api(endpoint + id, { method: 'PUT', body: JSON.stringify({ is_public: isPublic ? 1 : 0 }) });
        LS.toast(isPublic ? 'Шаблон опубликован' : 'Шаблон скрыт', 'success');
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    async function tplDelete(type, id) {
      if (!confirm('Удалить шаблон #' + id + '?')) return;
      try {
        if (type === 'courses') await LS.deleteCourseTemplate(id);
        else await LS.deleteLessonTemplate(id);
        LS.toast('Шаблон удалён', 'success');
        tplInited = false;
        loadTplAdmin();
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ════════════════════════════════════════════════
       СИМУЛЯЦИИ
    ════════════════════════════════════════════════ */

    // Full list of available (non-null id) sims mirrored from /lab
    const ADMIN_SIMS = [
      { id: 'graph',          cat: 'Математика', title: 'График функции' },
      { id: 'graphtransform', cat: 'Математика', title: 'Трансформации графиков' },
      { id: 'geometry',       cat: 'Математика', title: 'Планиметрия' },
      { id: 'triangle',       cat: 'Математика', title: 'Геометрия треугольника' },
      { id: 'quadratic',      cat: 'Математика', title: 'Корни квадратного уравнения' },
      { id: 'stereo',         cat: 'Математика', title: 'Стереометрия 3D' },
      { id: 'probability',    cat: 'Математика', title: 'Теория вероятностей' },
      { id: 'trigcircle',     cat: 'Математика', title: 'Тригонометрическая окружность' },
      { id: 'normaldist',     cat: 'Математика', title: 'Нормальное распределение' },
      { id: 'projectile',     cat: 'Физика',     title: 'Бросок тела' },
      { id: 'pendulum',       cat: 'Физика',     title: 'Маятник' },
      { id: 'collision',      cat: 'Физика',     title: 'Столкновение шаров' },
      { id: 'magnetic',       cat: 'Физика',     title: 'Магнитное поле токов' },
      { id: 'circuit',        cat: 'Физика',     title: 'Электрические цепи' },
      { id: 'coulomb',        cat: 'Физика',     title: 'Закон Кулона' },
      { id: 'hydrostatics',   cat: 'Физика',     title: 'Гидростатика' },
      { id: 'dynamics',       cat: 'Физика',     title: 'Динамика' },
      { id: 'thinlens',       cat: 'Физика',     title: 'Тонкая линза' },
      { id: 'refraction',     cat: 'Физика',     title: 'Преломление света' },
      { id: 'mirrors',        cat: 'Физика',     title: 'Зеркала' },
      { id: 'isoprocess',     cat: 'Физика',     title: 'Изопроцессы' },
      { id: 'waves',          cat: 'Физика',     title: 'Волны и звук' },
      { id: 'molphys',        cat: 'Химия',      title: 'Молекулярная физика' },
      { id: 'chemistry',      cat: 'Химия',      title: 'Химические реакции' },
      { id: 'equilibrium',    cat: 'Химия',      title: 'Химическое равновесие' },
      { id: 'electrolysis',   cat: 'Химия',      title: 'Электролиз' },
      { id: 'bohratom',       cat: 'Химия',      title: 'Атом Бора' },
      { id: 'orbitals',       cat: 'Химия',      title: 'Молекулярные орбитали' },
      { id: 'titration',      cat: 'Химия',      title: 'pH и кривая титрования' },
      { id: 'chemsandbox',    cat: 'Химия',      title: 'Химическая песочница' },
      { id: 'crystal',        cat: 'Химия',      title: 'Кристаллическая решётка' },
      { id: 'celldivision',   cat: 'Биология',   title: 'Деление клетки' },
      { id: 'photosynthesis', cat: 'Биология',   title: 'Фотосинтез и дыхание' },
      { id: 'angrybirds',     cat: 'Игры',       title: 'Angry Birds Physics' },
    ];

    let _simsSettings = { module_disabled: false, disabled_ids: [] };

    async function loadSimsAdmin() {
      try {
        const data = await LS.api('/api/settings/sims');
        _simsSettings = data;
        _renderSimsAdmin();
      } catch(e) { LS.toast('Ошибка загрузки настроек: ' + e.message, 'error'); }
    }

    function _renderSimsAdmin() {
      // master toggle
      const masterChk = document.getElementById('sims-master-chk');
      if (masterChk) masterChk.checked = !_simsSettings.module_disabled;

      // per-sim cards
      const grid = document.getElementById('sims-grid');
      const dis  = new Set(_simsSettings.disabled_ids || []);
      // group by category
      const byCat = {};
      ADMIN_SIMS.forEach(s => { (byCat[s.cat] = byCat[s.cat] || []).push(s); });

      let html = '';
      Object.entries(byCat).forEach(([cat, sims]) => {
        html += `<div style="grid-column:1/-1;font-size:.72rem;font-weight:800;text-transform:uppercase;letter-spacing:.07em;color:var(--text-3);margin-top:12px;margin-bottom:2px">${esc(cat)}</div>`;
        sims.forEach(s => {
          const enabled = !dis.has(s.id);
          html += `<div class="perm-card${enabled ? ' enabled' : ''}" id="simcard-${s.id}">
            <div class="perm-info">
              <div class="perm-label">${esc(s.title)}</div>
              <div class="perm-desc" style="font-size:11px;margin-top:2px;opacity:.7">${esc(s.id)}</div>
            </div>
            <label class="perm-toggle" title="${enabled ? 'Отключить' : 'Включить'}">
              <input type="checkbox" ${enabled ? 'checked' : ''} onchange="simToggleOne('${s.id}', this.checked)" />
              <span class="perm-track"></span>
              <span class="perm-thumb"></span>
            </label>
          </div>`;
        });
      });
      grid.innerHTML = html;
      if (window.lucide) lucide.createIcons();
    }

    async function simsMasterToggle(checked) {
      // checked = module enabled; disabled = !checked
      try {
        await LS.api('/api/settings/sims', { method: 'PUT', body: JSON.stringify({ module_disabled: !checked }) });
        _simsSettings.module_disabled = !checked;
        LS.toast(checked ? 'Модуль симуляций включён' : 'Модуль симуляций отключён', checked ? 'success' : 'warning');
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    async function simToggleOne(simId, enabled) {
      const dis = new Set(_simsSettings.disabled_ids || []);
      if (enabled) dis.delete(simId); else dis.add(simId);
      const disabled_ids = [...dis];
      try {
        await LS.api('/api/settings/sims', { method: 'PUT', body: JSON.stringify({ disabled_ids }) });
        _simsSettings.disabled_ids = disabled_ids;
        // update card style
        const card = document.getElementById('simcard-' + simId);
        if (card) card.classList.toggle('enabled', enabled);
        LS.toast(enabled ? `«${simId}» включена` : `«${simId}» отключена`, enabled ? 'success' : 'warning');
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ─── Games features admin ─── */
    const GAME_FEATURES = [
      { key: 'hangman',       label: 'Виселица',        desc: 'Игра «Угадай слово» — отгадывание терминов по буквам',              icon: 'gamepad-2'      },
      { key: 'crossword',     label: 'Кроссворд',       desc: 'Кроссворд из терминов — генерируется автоматически по темам',       icon: 'grid-3x3'       },
      { key: 'pet',           label: 'Питомец',          desc: 'Виртуальный питомец, отражающий активность ученика',                icon: 'heart'          },
      { key: 'red_book',      label: 'Красная книга',   desc: 'Интерактивная Красная книга РБ: виды, биомы, пищевые сети, квесты', icon: 'leaf'           },
      { key: 'collection',    label: 'Коллекция',        desc: 'Коллекция карточек и достижений — игровой прогресс ученика',        icon: 'layers'         },
      { key: 'flashcards',    label: 'Флеш-карточки',   desc: 'Карточки для запоминания терминов и понятий методом интервальных повторений', icon: 'square-stack'   },
      { key: 'knowledge_map', label: 'Карта знаний',    desc: 'Визуальная карта тем и связей между биологическими понятиями',      icon: 'share-2'        },
      { key: 'board',         label: 'Доска',             desc: 'Классная доска с объявлениями, постами и обсуждениями',             icon: 'layout-dashboard'},
      { key: 'biochem',       label: 'Биохимия',          desc: 'Молекулярный редактор, задачи на построение молекул и реакции',     icon: 'flask-conical'  },
      { key: 'live_quiz',     label: 'Живая викторина',  desc: 'Синхронная викторина в реальном времени для всего класса',          icon: 'radio'          },
    ];

    async function loadGamesAdmin() {
      const grid = document.getElementById('games-features-grid');
      try {
        const features = await LS.api('/api/admin/features');
        grid.innerHTML = '';
        for (const f of GAME_FEATURES) {
          const enabled = features[f.key] !== false;
          const card = document.createElement('div');
          card.className = 'perm-card' + (enabled ? ' enabled' : '');
          card.innerHTML = `
            <div class="perm-info">
              <div class="perm-label"><i data-lucide="${f.icon}" style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"></i>${f.label}</div>
              <div class="perm-desc">${f.desc}</div>
            </div>
            <label class="perm-toggle">
              <input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleGameFeature('${f.key}', this.checked, this)" />
              <span class="perm-track"></span>
              <span class="perm-thumb"></span>
            </label>`;
          grid.appendChild(card);
        }
        if (window.lucide) lucide.createIcons();
      } catch(e) {
        grid.innerHTML = '<div class="error">Ошибка загрузки</div>';
      }
    }

    async function toggleGameFeature(key, enabled, checkbox) {
      try {
        await LS.api('/api/admin/features', {
          method: 'PATCH',
          body: JSON.stringify({ [key]: enabled }),
        });
        const card = checkbox.closest('.perm-card');
        if (card) card.classList.toggle('enabled', enabled);
        LS.toast(enabled ? 'Функция включена' : 'Функция отключена', 'success');
      } catch(e) {
        checkbox.checked = !enabled;
        LS.toast('Ошибка: ' + e.message, 'error');
      }
    }

    /* ─── Free-student module features ─── */
    const FS_FEATURES = [
      { key: 'gamification', label: 'Геймификация',   desc: 'XP, уровни, достижения, монеты, стрики, магазин', icon: 'trophy' },
      { key: 'hangman',      label: 'Виселица',        desc: 'Игра «Угадай слово» — отгадывание терминов по буквам', icon: 'gamepad-2' },
      { key: 'crossword',    label: 'Кроссворд',       desc: 'Кроссворд из терминов — генерируется автоматически', icon: 'grid-3x3' },
      { key: 'pet',          label: 'Питомец',          desc: 'Виртуальный питомец, отражающий активность ученика', icon: 'heart' },
      { key: 'red_book',     label: 'Красная книга',   desc: 'Интерактивная Красная книга РБ: виды, биомы, квесты', icon: 'leaf' },
      { key: 'collection',   label: 'Коллекция',        desc: 'Коллекция карточек и игровой прогресс ученика', icon: 'layers' },
      { key: 'lab',          label: 'Лаборатория',      desc: 'Виртуальные симуляции и интерактивные опыты', icon: 'flask-conical' },
      { key: 'knowledge_map',label: 'Карта знаний',    desc: 'Визуальная карта тем и связей между понятиями', icon: 'map' },
      { key: 'flashcards',   label: 'Флеш-карточки',   desc: 'Карточки для повторения терминов и понятий', icon: 'square-stack' },
      { key: 'board',        label: 'Доска',             desc: 'Классная доска с объявлениями и постами', icon: 'layout-dashboard' },
      { key: 'biochem',      label: 'Биохимия',          desc: 'Молекулярный редактор, задачи на построение молекул и реакции', icon: 'flask-conical' },
      { key: 'live_quiz',    label: 'Живая викторина',  desc: 'Синхронная викторина в реальном времени для всего класса', icon: 'radio' },
    ];

    async function loadFsFeatures() {
      const grid = document.getElementById('fs-features-grid');
      try {
        const features = await LS.api('/api/admin/free-student-features');
        grid.innerHTML = '';
        for (const f of FS_FEATURES) {
          const enabled = features[f.key] !== false;
          const card = document.createElement('div');
          card.className = 'perm-card' + (enabled ? ' enabled' : '');
          card.innerHTML = `
            <div class="perm-info">
              <div class="perm-label"><i data-lucide="${f.icon}" style="width:14px;height:14px;vertical-align:-2px;margin-right:6px"></i>${f.label}</div>
              <div class="perm-desc">${f.desc}</div>
            </div>
            <label class="perm-toggle">
              <input type="checkbox" ${enabled ? 'checked' : ''} onchange="toggleFsFeature('${f.key}', this.checked, this)" />
              <span class="perm-track"></span>
              <span class="perm-thumb"></span>
            </label>`;
          grid.appendChild(card);
        }
        if (window.lucide) lucide.createIcons();
      } catch(e) {
        grid.innerHTML = '<div class="error">Ошибка загрузки</div>';
      }
    }

    async function toggleFsFeature(key, enabled, checkbox) {
      try {
        await LS.api('/api/admin/free-student-features', {
          method: 'PATCH',
          body: JSON.stringify({ [key]: enabled }),
        });
        const card = checkbox.closest('.perm-card');
        if (card) card.classList.toggle('enabled', enabled);
        LS.toast(enabled ? 'Модуль включён' : 'Модуль отключён', 'success');
      } catch(e) {
        checkbox.checked = !enabled;
        LS.toast('Ошибка: ' + e.message, 'error');
      }
    }

    /* ─── Submission log ─── */
    const SL_STATUSES = { new:'На проверке', reviewed:'Проверено', accepted:'Принято', revision:'На доработке', resubmitted:'Повторно' };

    async function loadSubmissionLog() {
      const el = document.getElementById('sublog-list');
      const countEl = document.getElementById('sublog-count');
      const classId = document.getElementById('sublog-class-filter').value;
      el.innerHTML = '<div class="spinner"></div>';
      countEl.textContent = '';
      try {
        const url = classId ? `/api/submissions/log?class_id=${classId}` : '/api/submissions/log';
        const rows = await LS.api(url);

        // Populate class filter on first load
        const sel = document.getElementById('sublog-class-filter');
        if (sel.options.length <= 1 && rows.length) {
          const classMap = new Map();
          rows.forEach(r => { if (r.class_id && r.class_name) classMap.set(r.class_id, r.class_name); });
          classMap.forEach((name, id) => {
            const opt = document.createElement('option');
            opt.value = id; opt.textContent = name;
            sel.appendChild(opt);
          });
        }

        countEl.textContent = rows.length ? `${rows.length} записей` : '';

        if (!rows.length) {
          el.innerHTML = `<div class="sl-empty">
            <div class="sl-empty-icon"><i data-lucide="inbox" style="width:48px;height:48px"></i></div>
            Удалённых работ нет
          </div>`;
          if (window.lucide) lucide.createIcons({ nodes: [el] });
          return;
        }

        const ROLE_LABELS = { admin: 'Админ', teacher: 'Учитель', student: 'Ученик' };

        el.innerHTML = `<div class="sl-wrap"><table class="sl-table">
          <thead><tr>
            <th>Дата</th>
            <th>Ученик</th>
            <th>Файл</th>
            <th>Задание</th>
            <th>Класс</th>
            <th>Статус</th>
            <th>Оценка</th>
            <th>Удалил</th>
          </tr></thead>
          <tbody>${rows.map(r => {
            const dt = r.deleted_at ? new Date(r.deleted_at.includes('T') ? r.deleted_at : r.deleted_at.replace(' ','T')+'Z') : null;
            const dateStr = dt ? dt.toLocaleDateString('ru',{day:'numeric',month:'short'}) + ' ' + dt.toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'}) : '—';
            const initials = (r.student_name || '?').split(' ').slice(0,2).map(w => w[0]?.toUpperCase() || '').join('');
            const st = r.status || 'new';
            const gradeVal = r.grade != null ? r.grade : null;
            const gradeCls = gradeVal != null ? (gradeVal >= 80 ? 'sl-grade-hi' : gradeVal >= 50 ? 'sl-grade-mid' : 'sl-grade-lo') : 'sl-grade-none';
            const roleCls = 'sl-role-' + (r.deleted_by_role || 'student');
            return `<tr>
              <td><span class="sl-date">${dateStr}</span></td>
              <td><span class="sl-student"><span class="sl-student-avatar">${initials}</span>${esc(r.student_name || '—')}</span></td>
              <td><span class="sl-file" title="${esc(r.original_name || '')}">${esc(r.original_name || '—')}</span></td>
              <td><span class="sl-assignment">${esc(r.assignment_title || '—')}</span></td>
              <td><span class="sl-class">${esc(r.class_name || '—')}</span></td>
              <td><span class="sl-status sl-status-${st}">${SL_STATUSES[st] || st}</span></td>
              <td><span class="sl-grade ${gradeCls}">${gradeVal != null ? gradeVal : '—'}</span></td>
              <td><span class="sl-deleted-by">${esc(r.deleted_by_name || '—')} <span class="sl-role-badge ${roleCls}">${ROLE_LABELS[r.deleted_by_role] || r.deleted_by_role || '?'}</span></span></td>
            </tr>`;
          }).join('')}</tbody>
        </table></div>`;
        document.getElementById('btn-clear-sublog').style.display = '';
      } catch (e) {
        el.innerHTML = `<div class="sl-empty" style="color:#c0306a">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    async function clearSubmissionLog() {
      if (!await LS.confirm('Очистить весь журнал удалённых работ? Это действие необратимо.', { title: 'Очистка журнала', confirmText: 'Очистить', danger: true })) return;
      try {
        await LS.api('/api/submissions/log', { method: 'DELETE' });
        document.getElementById('btn-clear-sublog').style.display = 'none';
        document.getElementById('sublog-count').textContent = '';
        document.getElementById('sublog-list').innerHTML = `<div class="sl-empty">
          <div class="sl-empty-icon"><i data-lucide="inbox" style="width:48px;height:48px"></i></div>
          Журнал очищен
        </div>`;
        if (window.lucide) lucide.createIcons({ nodes: [document.getElementById('sublog-list')] });
        LS.toast('Журнал очищен', 'success');
      } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ═══ TOPICS ═══════════════════════════════════════════════════════ */
    let _topicsSubjects = [];
    async function loadTopicSubjects() {
      if (_topicsSubjects.length) return;
      try {
        _topicsSubjects = await LS.getSubjects();
        const sel = document.getElementById('topics-subj-filter');
        sel.innerHTML = _topicsSubjects.map(s => `<option value="${s.id}">${esc(s.name)}</option>`).join('');
      } catch {}
    }
    async function loadTopics() {
      await loadTopicSubjects();
      const subjId = document.getElementById('topics-subj-filter').value;
      const el = document.getElementById('topics-list');
      el.innerHTML = LS.skeleton(4, 'row');
      try {
        const rows = await LS.api(`/api/admin/topics?subject_id=${subjId}`);
        document.getElementById('topics-count').textContent = rows.length + ' тем';
        if (!rows.length) { el.innerHTML = '<div style="padding:32px;text-align:center;color:var(--text-3)">Тем нет</div>'; return; }
        el.innerHTML = '<div style="display:flex;flex-direction:column;gap:6px">' + rows.map(t => `
          <div class="adm-panel" style="padding:12px 18px;margin:0;display:flex;align-items:center;gap:14px">
            <span style="font-size:0.75rem;color:var(--text-3);font-weight:700;min-width:28px">#${t.order_index}</span>
            <span style="flex:1;font-weight:600">${esc(t.name)}</span>
            <span style="font-size:0.78rem;color:var(--text-3)">${t.question_count} вопр.</span>
            <button class="adm-btn adm-btn-small" style="background:var(--border-h);color:var(--text-2);padding:5px 12px" onclick="renameTopic(${t.id},'${esc(t.name).replace(/'/g,"\\'")}')">
              <i data-lucide="pencil" style="width:12px;height:12px;vertical-align:-1px"></i>
            </button>
            <button class="adm-btn adm-btn-small" style="background:rgba(241,91,181,0.1);color:var(--pink);padding:5px 12px" onclick="deleteTopic(${t.id},'${esc(t.name).replace(/'/g,"\\'")}',${t.question_count})">
              <i data-lucide="trash-2" style="width:12px;height:12px;vertical-align:-1px"></i>
            </button>
          </div>`).join('') + '</div>';
        if (window.lucide) lucide.createIcons({ nodes: [el] });
      } catch (e) { el.innerHTML = `<div style="color:var(--pink)">${esc(e.message)}</div>`; }
    }
    function showAddTopic() { document.getElementById('topics-add-row').style.display = ''; document.getElementById('topics-new-name').focus(); }
    async function createTopic() {
      const name = document.getElementById('topics-new-name').value.trim();
      if (!name) return;
      const subjId = document.getElementById('topics-subj-filter').value;
      try {
        await LS.api('/api/admin/topics', { method:'POST', body: JSON.stringify({ subject_id: subjId, name }) });
        document.getElementById('topics-new-name').value = '';
        document.getElementById('topics-add-row').style.display = 'none';
        LS.toast('Тема создана', 'success');
        loadTopics();
      } catch (e) { LS.toast(e.message, 'error'); }
    }
    async function renameTopic(id, oldName) {
      const name = prompt('Новое название темы:', oldName);
      if (!name || name === oldName) return;
      try {
        await LS.api(`/api/admin/topics/${id}`, { method:'PATCH', body: JSON.stringify({ name }) });
        LS.toast('Тема переименована', 'success');
        loadTopics();
      } catch (e) { LS.toast(e.message, 'error'); }
    }
    async function deleteTopic(id, name, qcount) {
      if (qcount > 0) { LS.toast(`Нельзя удалить тему с ${qcount} вопросами`, 'warn'); return; }
      if (!await LS.confirm(`Удалить тему "${name}"?`, { danger: true })) return;
      try {
        await LS.api(`/api/admin/topics/${id}`, { method:'DELETE' });
        LS.toast('Тема удалена', 'success');
        loadTopics();
      } catch (e) { LS.toast(e.message, 'error'); }
    }

    /* ═══ BROADCAST ═════════════════════════════════════════════════════ */
    async function sendBroadcast() {
      const message = document.getElementById('bc-message').value.trim();
      if (!message) { LS.toast('Введите сообщение', 'warn'); return; }
      const role = document.getElementById('bc-role').value;
      const link = document.getElementById('bc-link').value.trim() || null;
      try {
        const r = await LS.api('/api/admin/broadcast', { method:'POST', body: JSON.stringify({ message, role, link }) });
        document.getElementById('bc-result').textContent = `Отправлено ${r.sent} пользователям`;
        document.getElementById('bc-message').value = '';
        LS.toast(`Уведомление отправлено ${r.sent} пользователям`, 'success');
      } catch (e) { LS.toast(e.message, 'error'); }
    }

    /* ═══ AUDIT LOG ════════════════════════════════════════════════════ */
    async function loadAuditLog() {
      const el = document.getElementById('audit-list');
      el.innerHTML = LS.skeleton(5, 'row');
      try {
        const rows = await LS.api('/api/admin/audit-log?limit=200');
        if (!rows.length) { el.innerHTML = '<div style="padding:32px;text-align:center;color:var(--text-3)">Журнал пуст</div>'; return; }
        const ACTION_LABELS = {
          'user.role_change': 'Смена роли', 'user.edit': 'Редактирование', 'user.ban': 'Блокировка',
          'user.unban': 'Разблокировка', 'user.delete': 'Удаление', 'user.clear_sessions': 'Очистка истории',
          'features.update': 'Фичи обновлены', 'topic.create': 'Создание темы', 'topic.update': 'Редакт. темы',
          'topic.delete': 'Удаление темы', 'broadcast': 'Рассылка',
        };
        const ACTION_COLORS = {
          'user.delete': 'var(--pink)', 'user.ban': 'var(--pink)', 'user.clear_sessions': 'var(--amber)',
        };
        el.innerHTML = `<div class="sl-wrap"><table class="sl-table">
          <thead><tr><th>Дата</th><th>Админ</th><th>Действие</th><th>Цель</th><th>Детали</th><th>IP</th></tr></thead>
          <tbody>${rows.map(r => {
            const dt = new Date(r.created_at);
            const ds = dt.toLocaleDateString('ru',{day:'numeric',month:'short'}) + ' ' + dt.toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'});
            const acol = ACTION_COLORS[r.action] || 'var(--violet)';
            return `<tr>
              <td><span class="sl-date">${ds}</span></td>
              <td>${esc(r.admin_name || '—')}</td>
              <td><span style="color:${acol};font-weight:700;font-size:0.82rem">${ACTION_LABELS[r.action] || r.action}</span></td>
              <td style="font-size:0.82rem;color:var(--text-3)">${esc(r.target || '')}</td>
              <td style="font-size:0.82rem;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(r.detail || '')}">${esc(r.detail || '')}</td>
              <td style="font-size:0.78rem;color:var(--text-3);font-family:monospace">${esc(r.ip || '')}</td>
            </tr>`;
          }).join('')}</tbody></table></div>`;
      } catch (e) { el.innerHTML = `<div style="color:var(--pink)">${esc(e.message)}</div>`; }
    }
    async function clearAuditLog() {
      if (!await LS.confirm('Очистить весь аудит-лог?', { danger: true })) return;
      try {
        await LS.api('/api/admin/audit-log', { method:'DELETE' });
        document.getElementById('audit-list').innerHTML = '<div style="padding:32px;text-align:center;color:var(--text-3)">Журнал очищен</div>';
        LS.toast('Журнал очищен', 'success');
      } catch (e) { LS.toast(e.message, 'error'); }
    }

    /* ═══ ERROR LOG ════════════════════════════════════════════════════ */
    async function loadErrorLog() {
      const el = document.getElementById('errors-list');
      el.innerHTML = LS.skeleton(3, 'row');
      try {
        const rows = await LS.api('/api/admin/error-log?limit=200');
        if (!rows.length) { el.innerHTML = '<div style="padding:32px;text-align:center;color:var(--text-3);font-size:0.88rem">Ошибок нет</div>'; return; }
        el.innerHTML = rows.map(r => {
          const dt = new Date(r.created_at);
          const ds = dt.toLocaleDateString('ru',{day:'numeric',month:'short'}) + ' ' + dt.toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'});
          return `<div class="adm-panel" style="padding:14px 18px;margin-bottom:8px;border-left:3px solid var(--pink)">
            <div style="display:flex;align-items:center;gap:10px;margin-bottom:6px">
              <span style="font-size:0.78rem;color:var(--pink);font-weight:700">${r.method || ''} ${esc(r.route || '')}</span>
              <span style="font-size:0.72rem;color:var(--text-3);margin-left:auto">${ds}</span>
              ${r.user_id ? `<span style="font-size:0.72rem;color:var(--text-3)">user:${r.user_id}</span>` : ''}
            </div>
            <div style="font-size:0.88rem;font-weight:600;color:var(--text);margin-bottom:4px">${esc(r.message)}</div>
            ${r.stack ? `<details><summary style="font-size:0.75rem;color:var(--text-3);cursor:pointer">Stack trace</summary><pre style="font-size:0.72rem;color:var(--text-3);white-space:pre-wrap;max-height:200px;overflow:auto;margin-top:6px;padding:8px;background:rgba(0,0,0,0.02);border-radius:8px">${esc(r.stack)}</pre></details>` : ''}
          </div>`;
        }).join('');
      } catch (e) { el.innerHTML = `<div style="color:var(--pink)">${esc(e.message)}</div>`; }
    }
    async function clearErrorLog() {
      if (!await LS.confirm('Очистить журнал ошибок?', { danger: true })) return;
      try {
        await LS.api('/api/admin/error-log', { method:'DELETE' });
        document.getElementById('errors-list').innerHTML = '<div style="padding:32px;text-align:center;color:var(--text-3)">Журнал очищен</div>';
        LS.toast('Журнал очищен', 'success');
      } catch (e) { LS.toast(e.message, 'error'); }
    }

    /* ═══ SYSTEM HEALTH ════════════════════════════════════════════════ */
    async function loadHealth() {
      const el = document.getElementById('health-content');
      el.innerHTML = LS.skeleton(3, 'row');
      try {
        const h = await LS.api('/api/admin/health');
        const fmtBytes = b => b > 1e9 ? (b/1e9).toFixed(1)+' GB' : b > 1e6 ? (b/1e6).toFixed(1)+' MB' : (b/1e3).toFixed(0)+' KB';
        const fmtUp = s => { const d=Math.floor(s/86400), hr=Math.floor(s%86400/3600), m=Math.floor(s%3600/60); return d>0?`${d}d ${hr}h`:hr>0?`${hr}h ${m}m`:`${m}m`; };
        el.innerHTML = `
          <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:14px;margin-bottom:24px">
            <div class="adm-panel" style="padding:18px;margin:0;text-align:center">
              <div style="font-size:1.3rem;font-weight:800;font-family:'Unbounded',sans-serif;color:var(--green)">${fmtUp(h.uptime)}</div>
              <div style="font-size:0.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-top:4px">Uptime</div>
            </div>
            <div class="adm-panel" style="padding:18px;margin:0;text-align:center">
              <div style="font-size:1.3rem;font-weight:800;font-family:'Unbounded',sans-serif;color:var(--violet)">${fmtBytes(h.db.sizeBytes)}</div>
              <div style="font-size:0.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-top:4px">База данных</div>
            </div>
            <div class="adm-panel" style="padding:18px;margin:0;text-align:center">
              <div style="font-size:1.3rem;font-weight:800;font-family:'Unbounded',sans-serif">${fmtBytes(h.uploads.sizeBytes)}</div>
              <div style="font-size:0.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-top:4px">Файлы</div>
            </div>
            <div class="adm-panel" style="padding:18px;margin:0;text-align:center">
              <div style="font-size:1.3rem;font-weight:800;font-family:'Unbounded',sans-serif;color:${h.recentErrors>0?'var(--pink)':'var(--green)'}">${h.recentErrors}</div>
              <div style="font-size:0.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-top:4px">Ошибок за 24ч</div>
            </div>
          </div>
          <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px">
            <div class="adm-panel" style="margin:0">
              <div class="adm-panel-title">Платформа</div>
              <table style="width:100%;font-size:0.88rem">
                <tr><td style="color:var(--text-3);padding:4px 0">Node.js</td><td style="font-weight:600">${h.node}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">OS</td><td style="font-weight:600">${h.platform}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">CPU ядра</td><td style="font-weight:600">${h.cpus}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">RAM использовано</td><td style="font-weight:600">${fmtBytes(h.memory.rss)}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">RAM heap</td><td style="font-weight:600">${fmtBytes(h.memory.heapUsed)}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">RAM свободно</td><td style="font-weight:600">${fmtBytes(h.freeMem)} / ${fmtBytes(h.totalMem)}</td></tr>
              </table>
            </div>
            <div class="adm-panel" style="margin:0">
              <div class="adm-panel-title">Данные</div>
              <table style="width:100%;font-size:0.88rem">
                <tr><td style="color:var(--text-3);padding:4px 0">Пользователей</td><td style="font-weight:600">${h.db.totalUsers}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">Всего сессий</td><td style="font-weight:600">${h.db.totalSessions}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">Сессий сегодня</td><td style="font-weight:600;color:var(--violet)">${h.db.todaySessions}</td></tr>
                <tr><td style="color:var(--text-3);padding:4px 0">Вопросов в базе</td><td style="font-weight:600">${h.db.totalQuestions}</td></tr>
              </table>
            </div>
          </div>`;
      } catch (e) { el.innerHTML = `<div style="color:var(--pink)">${esc(e.message)}</div>`; }
    }

    /* ════════════════════════════════════════════════
       ОНЛАЙН-УРОКИ (classroom admin)
    ════════════════════════════════════════════════ */
    let _crHistPage = 1, _crHistTotal = 0, _crHistPages = 0, _crHistSearch = '';
    let _crOpenDetailId = null, _crHistDebTimer = null;

    async function loadCrModuleState() {
      try {
        const features = await LS.api('/api/admin/features');
        const chk = document.getElementById('cr-master-chk');
        if (chk) chk.checked = features.classroom !== false;
      } catch(e) { /* silent */ }
    }

    async function crMasterToggle(enabled) {
      try {
        await LS.api('/api/admin/features', { method: 'PATCH', body: JSON.stringify({ classroom: enabled }) });
        LS.toast(enabled ? 'Модуль онлайн-уроков включён' : 'Модуль онлайн-уроков отключён', enabled ? 'success' : 'warning', 3000);
      } catch(e) {
        LS.toast('Ошибка: ' + e.message, 'error');
        // revert checkbox
        const chk = document.getElementById('cr-master-chk');
        if (chk) chk.checked = !enabled;
      }
    }

    function fmtDuration(sec) {
      if (!sec || sec < 0) return '—';
      const h = Math.floor(sec / 3600), m = Math.floor((sec % 3600) / 60), s = sec % 60;
      if (h) return `${h}ч ${m}м`;
      if (m) return `${m} мин ${s} сек`;
      return `${s} сек`;
    }
    function fmtLiveDuration(createdAt) {
      const sec = Math.round((Date.now() - new Date(createdAt).getTime()) / 1000);
      return fmtDuration(sec);
    }

    async function loadCrActiveSessions() {
      const el = document.getElementById('cr-live-list');
      try {
        const { sessions } = await LS.api('/api/classroom/admin/active');
        if (!sessions.length) {
          el.innerHTML = '<div class="empty">Нет активных уроков</div>';
          return;
        }
        el.innerHTML = sessions.map(s => {
          const dur = fmtLiveDuration(s.created_at);
          const title = s.title || `Урок #${s.id}`;
          const cls   = s.class_name ? `Класс: ${esc(s.class_name)}` : 'Личный урок';
          return `<div class="cr-live-card">
            <div class="cr-live-pulse"></div>
            <div class="cr-live-info">
              <div class="cr-live-title">${esc(title)}</div>
              <div class="cr-live-meta">${esc(s.teacher_name)} · ${cls}</div>
            </div>
            <div class="cr-live-badges">
              <span class="cr-badge cr-badge-online">
                <svg class="ic" viewBox="0 0 24 24" style="width:11px;height:11px"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
                ${s.online_count}
              </span>
              <span class="cr-badge cr-badge-msgs">
                <svg class="ic" viewBox="0 0 24 24" style="width:11px;height:11px"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
                ${s.message_count}
              </span>
              <span class="cr-badge cr-badge-dur">
                <svg class="ic" viewBox="0 0 24 24" style="width:11px;height:11px"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
                ${dur}
              </span>
            </div>
            <div class="cr-live-actions">
              <button class="btn-cr-end" onclick="adminEndSession(${s.id})">Завершить</button>
            </div>
          </div>`;
        }).join('');
      } catch(e) {
        el.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
      if (window.lucide) lucide.createIcons();
    }

    async function adminEndSession(id) {
      if (!await LS.confirm(`Завершить урок #${id}? Все участники будут отключены.`, { title: 'Завершить урок', confirmText: 'Завершить' })) return;
      try {
        await LS.api(`/api/classroom/${id}`, { method: 'DELETE' });
        LS.toast('Урок завершён', 'success', 2500);
        loadCrActiveSessions();
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    function crHistDebounce() {
      clearTimeout(_crHistDebTimer);
      _crHistDebTimer = setTimeout(() => { _crHistPage = 1; loadCrHistory(); }, 350);
    }

    async function loadCrHistory(page) {
      if (page) _crHistPage = page;
      _crHistSearch = (document.getElementById('cr-hist-q')?.value || '').trim();
      const el = document.getElementById('cr-hist-list');
      el.innerHTML = '<div class="spinner"></div>';
      try {
        const params = new URLSearchParams({ page: _crHistPage, limit: 20 });
        if (_crHistSearch) params.set('search', _crHistSearch);
        const { sessions, total, pages } = await LS.api('/api/classroom/admin/sessions?' + params);
        _crHistTotal = total; _crHistPages = pages;
        document.getElementById('cr-hist-count').textContent = `${total} уроков`;
        if (!sessions.length) {
          el.innerHTML = '<div class="empty">Нет завершённых уроков</div>';
          renderCrPagination();
          return;
        }
        el.innerHTML = sessions.map(s => {
          const title = s.title || `Урок #${s.id}`;
          const cls   = s.class_name ? `Класс: ${esc(s.class_name)}` : 'Личный урок';
          const dur   = fmtDuration(s.ended_at ? Math.round((new Date(s.ended_at)-new Date(s.created_at))/1000) : null);
          return `<div>
            <div class="cr-hist-row${_crOpenDetailId===s.id?' open':''}" onclick="toggleCrDetail(${s.id},this)">
              <div class="cr-hist-icon">
                <svg class="ic" viewBox="0 0 24 24" style="width:18px;height:18px;color:var(--violet)"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
              </div>
              <div class="cr-hist-main">
                <div class="cr-hist-title">${esc(title)}</div>
                <div class="cr-hist-meta">${esc(s.teacher_name)} · ${cls} · ${fmtDate(s.ended_at || s.created_at)}</div>
              </div>
              <div class="cr-hist-chips">
                <span class="cr-badge cr-badge-online">${s.participant_count} уч.</span>
                <span class="cr-badge cr-badge-msgs">${s.message_count} сообщ.</span>
                <span class="cr-badge cr-badge-dur">${dur}</span>
              </div>
              <svg class="cr-hist-chevron ic" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></svg>
            </div>
            <div class="cr-detail-drawer${_crOpenDetailId===s.id?' open':''}" id="cr-detail-${s.id}">
              <div class="cr-detail-inner" id="cr-detail-inner-${s.id}">
                <div class="spinner"></div>
              </div>
            </div>
          </div>`;
        }).join('');
        if (_crOpenDetailId) {
          const dr = document.getElementById(`cr-detail-${_crOpenDetailId}`);
          if (dr) loadCrSessionDetail(_crOpenDetailId);
        }
        renderCrPagination();
      } catch(e) {
        el.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
      if (window.lucide) lucide.createIcons();
    }

    function renderCrPagination() {
      const el = document.getElementById('cr-hist-pagination');
      if (_crHistPages <= 1) { el.innerHTML = ''; return; }
      const p = _crHistPage, total = _crHistPages;
      let html = '<div class="cr-pagination">';
      html += `<button class="cr-page-btn" onclick="loadCrHistory(${p-1})" ${p<=1?'disabled':''}>
        <svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><polyline points="15 18 9 12 15 6"/></svg>
      </button>`;
      const range = [];
      for (let i=1;i<=total;i++) {
        if (i===1||i===total||Math.abs(i-p)<=1) range.push(i);
        else if (range[range.length-1]!=='…') range.push('…');
      }
      range.forEach(r => {
        if (r==='…') html += `<span class="cr-page-info">…</span>`;
        else html += `<button class="cr-page-btn${r===p?' active':''}" onclick="loadCrHistory(${r})">${r}</button>`;
      });
      html += `<button class="cr-page-btn" onclick="loadCrHistory(${p+1})" ${p>=total?'disabled':''}>
        <svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><polyline points="9 18 15 12 9 6"/></svg>
      </button></div>`;
      el.innerHTML = html;
    }

    async function toggleCrDetail(id, rowEl) {
      const wasOpen = _crOpenDetailId === id;
      // close all
      document.querySelectorAll('.cr-hist-row.open').forEach(r => r.classList.remove('open'));
      document.querySelectorAll('.cr-detail-drawer.open').forEach(d => { d.classList.remove('open'); d.style.maxHeight=''; });
      _crOpenDetailId = null;
      if (wasOpen) return;
      // open this one
      rowEl.classList.add('open');
      const dr = document.getElementById(`cr-detail-${id}`);
      if (dr) { dr.classList.add('open'); }
      _crOpenDetailId = id;
      await loadCrSessionDetail(id);
    }

    async function loadCrSessionDetail(id) {
      const inner = document.getElementById(`cr-detail-inner-${id}`);
      if (!inner) return;
      inner.innerHTML = '<div class="spinner"></div>';
      try {
        const { session, stats, attendance, pages } = await LS.api(`/api/classroom/${id}/summary`);
        const dur = fmtDuration(stats.duration_sec);
        inner.innerHTML = `
          <div class="cr-detail-grid">
            <div class="cr-detail-stat"><div class="cr-detail-val">${stats.participant_count}</div><div class="cr-detail-label">Участников</div></div>
            <div class="cr-detail-stat"><div class="cr-detail-val">${stats.message_count}</div><div class="cr-detail-label">Сообщений</div></div>
            <div class="cr-detail-stat"><div class="cr-detail-val">${stats.page_count}</div><div class="cr-detail-label">Страниц</div></div>
            <div class="cr-detail-stat"><div class="cr-detail-val" style="font-size:1rem">${dur}</div><div class="cr-detail-label">Длительность</div></div>
          </div>
          ${attendance.length ? `
            <div class="section-title" style="font-size:0.72rem;margin-bottom:8px">Посещаемость</div>
            <div class="cr-attend-list">
              ${attendance.map(a => `
                <div class="cr-attend-row">
                  <svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px;flex-shrink:0;color:var(--violet)"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
                  <span class="cr-attend-name">${esc(a.user_name)}</span>
                  <span class="cr-attend-time">${a.joined_at ? new Date(a.joined_at).toLocaleTimeString('ru-RU',{hour:'2-digit',minute:'2-digit'}) : '—'}</span>
                  <span class="cr-attend-dur">${a.duration_sec ? fmtDuration(a.duration_sec) : (a.left_at ? '—' : '<span style="color:var(--green)">онлайн</span>')}</span>
                </div>
              `).join('')}
            </div>
          ` : ''}
          ${pages.length > 1 ? `
            <div class="section-title" style="font-size:0.72rem;margin:16px 0 8px">Страницы доски</div>
            <div class="cr-pages-list">
              ${pages.map(p => `
                <div class="cr-page-chip">
                  <span class="cr-page-num">Стр. ${p.page_num}</span>
                  <span class="cr-page-cnt">${p.stroke_count} штр.</span>
                </div>
              `).join('')}
            </div>
          ` : ''}
          <div class="cr-detail-actions">
            <button class="btn-cr-export" onclick="adminExportChat(${id})">
              <svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
              Экспорт чата
            </button>
            <button class="btn-cr-del" onclick="adminDeleteSession(${id})">
              <svg class="ic" viewBox="0 0 24 24" style="width:13px;height:13px;vertical-align:-2px"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Удалить запись
            </button>
          </div>`;
      } catch(e) {
        inner.innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
      }
    }

    function adminExportChat(id) {
      window.open(`/api/classroom/${id}/chat/export`, '_blank');
    }

    async function adminDeleteSession(id) {
      if (!await LS.confirm('Удалить всю запись об этом уроке? Данные нельзя восстановить.', { title: 'Удалить урок', confirmText: 'Удалить', dangerous: true })) return;
      try {
        await LS.api(`/api/classroom/${id}/history`, { method: 'DELETE' });
        LS.toast('Урок удалён', 'success', 2500);
        _crOpenDetailId = null;
        loadCrHistory();
      } catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
    }

    /* ─── wire tab loading ─── */
    const _origSwitchTab = window.switchTab;
    window.switchTab = function(btn) {
      _origSwitchTab(btn);
      const tab = btn.dataset.tab;
      if (tab === 'topics') loadTopics();
      else if (tab === 'audit') loadAuditLog();
      else if (tab === 'errors') loadErrorLog();
      else if (tab === 'health') loadHealth();
      else if (tab === 'classroom') { loadCrModuleState(); loadCrActiveSessions(); loadCrHistory(); }
      else if (tab === 'avatars')   { loadAvatarRequests(); }
    };

    /* ── Avatar moderation ─────────────────────────────────────────────── */
    async function loadAvatarRequests() {
      const list = document.getElementById('av-list');
      list.innerHTML = '<div style="color:var(--muted);text-align:center;padding:40px 0;font-size:0.85rem">Загрузка...</div>';
      try {
        const rows = await LS.get('/api/avatar/pending');
        const badge = document.getElementById('av-badge');
        if (rows.length) {
          badge.textContent = rows.length;
          badge.style.display = 'inline-flex';
        } else {
          badge.style.display = 'none';
        }
        if (!rows.length) {
          list.innerHTML = '<div class="av-empty"><i data-lucide="check-circle" style="width:36px;height:36px;opacity:.3;display:block;margin:0 auto 10px"></i>Нет заявок на модерацию</div>';
          if (window.lucide) lucide.createIcons();
          return;
        }
        list.innerHTML = `<div class="av-grid">${rows.map(r => {
          const initials = (r.user_name||'LS').split(' ').slice(0,2).map(w=>(w[0]||'').toUpperCase()).join('') || 'LS';
          const curAvatar = r.current_avatar
            ? `<img src="/avatars/${esc(r.current_avatar)}" alt="">`
            : initials;
          const newAvatar = `<img src="/avatars/${esc(r.filename)}" alt="" onerror="this.parentElement.textContent='?'">`;
          const d = new Date(r.created_at).toLocaleString('ru', { day:'numeric', month:'short', hour:'2-digit', minute:'2-digit' });
          return `<div class="av-card" id="av-card-${r.id}">
            <div class="av-card-top">
              <div class="av-imgs">
                <div class="av-img-wrap">
                  <span>Сейчас</span>
                  <div class="av-img">${curAvatar}</div>
                </div>
                <svg class="av-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
                <div class="av-img-wrap">
                  <span>Новый</span>
                  <div class="av-img">${newAvatar}</div>
                </div>
              </div>
            </div>
            <div>
              <div class="av-user-name">${esc(r.user_name||r.user_email)}</div>
              <div class="av-date">${esc(r.user_email)} · ${d}</div>
            </div>
            <div class="av-actions">
              <button class="av-approve" onclick="avatarApprove(${r.id})">Одобрить</button>
              <button class="av-reject"  onclick="avatarRejectPrompt(${r.id})">Отклонить</button>
            </div>
          </div>`;
        }).join('')}</div>`;
        if (window.lucide) lucide.createIcons();
      } catch {
        list.innerHTML = '<div class="av-empty">Ошибка загрузки</div>';
      }
    }

    async function avatarApprove(id) {
      const card = document.getElementById('av-card-' + id);
      if (card) card.style.opacity = '0.5';
      try {
        await LS.post('/api/avatar/' + id + '/approve', {});
        LS.toast('Аватар одобрен', 'success');
        loadAvatarRequests();
      } catch { LS.toast('Ошибка', 'error'); if (card) card.style.opacity = ''; }
    }

    function avatarRejectPrompt(id) {
      const reason = prompt('Причина отклонения (необязательно):') ?? null;
      if (reason === null) return; // cancelled
      avatarReject(id, reason);
    }

    async function avatarReject(id, reason) {
      const card = document.getElementById('av-card-' + id);
      if (card) card.style.opacity = '0.5';
      try {
        await LS.patch('/api/avatar/' + id + '/reject', { reason });
        LS.toast('Аватар отклонён', 'info');
        loadAvatarRequests();
      } catch { LS.toast('Ошибка', 'error'); if (card) card.style.opacity = ''; }
    }

    /* ─── init ─── */
    loadStats();
    loadAvatarRequests(); // load badge count on page open
    if (window.lucide) lucide.createIcons();
