Files
Learn_System/frontend/admin.html
T
Maxim Dolgolyov 471171b77c feat(access): доступ к учебникам и экзаменам по классам/ученикам из админ-панели
Модель allowlist (закрыто по умолчанию), правило ученика важнее класса.
Управляют админ (все) и учителя (свои классы/ученики).

- миграция 040: таблица content_access + непрерывный переход
  (всем существующим классам открыт текущий контент)
- сервис contentAccess: резолвинг доступа, главы наследуют хаб
- API /api/access (catalog/targets/rules) для admin+teacher
- гейты: каталог учебников, router.param slug/examKey, фильтр tracks
- клиентские редиректы на /403 (textbook-tracker, exam-prep boot)
- раздел админки «Доступ к учебникам»: классы + ученики (tri-state)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 12:33:05 +03:00

2049 lines
127 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Панель управления — LearnSpace</title>
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/css/ls.css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" crossorigin="anonymous" />
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js" crossorigin="anonymous"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js" crossorigin="anonymous"
onload="window._katexReady=true; if(window._katexCb){window._katexCb(); window._katexCb=null;}"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
.container { padding: 32px 32px 80px; }
.page-title { font-family: 'Unbounded', sans-serif; font-size: 1.65rem; font-weight: 800; margin-bottom: 6px; }
.page-sub { font-size: 1rem; color: var(--text-2); margin-bottom: 40px; }
.section-title { font-family: 'Unbounded', sans-serif; font-size: 0.9rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 20px; }
/* stats */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 20px; margin-bottom: 48px; }
.stat-card { background: var(--surface); backdrop-filter: var(--blur); border: 1px solid var(--border); border-radius: var(--r-lg); padding: 28px 24px; }
.stat-val { font-family: 'Unbounded', sans-serif; font-size: 2.4rem; font-weight: 800; margin-bottom: 6px; }
.stat-label { font-size: 0.9rem; color: var(--text-3); font-weight: 600; }
.stat-val.violet { color: var(--violet); }
.stat-val.cyan { color: var(--cyan); }
.stat-val.green { color: var(--green); }
.subj-stats { display: flex; gap: 14px; flex-wrap: wrap; margin-bottom: 48px; }
.subj-stat { background: var(--surface); backdrop-filter: var(--blur); border: 1px solid var(--border); border-radius: 16px; padding: 18px 22px; display: flex; align-items: center; gap: 14px; }
.subj-stat-name { font-size: 0.95rem; font-weight: 700; }
.subj-stat-info { font-size: 0.84rem; color: var(--text-3); }
.subj-stat-pct { font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 800; color: var(--violet); min-width: 48px; text-align: right; }
/* subject config cards */
.sc-list { display: flex; flex-direction: column; gap: 8px; }
.sc-card {
background: #fff; border: 1.5px solid var(--border); border-radius: 16px;
overflow: hidden; transition: border-color 0.18s, box-shadow 0.18s;
}
.sc-card.open { border-color: rgba(155,93,229,0.2); box-shadow: 0 6px 24px rgba(15,23,42,0.07); }
/* Collapsed row — always visible */
.sc-row-top {
display: flex; align-items: center; gap: 14px; padding: 14px 20px;
cursor: pointer; user-select: none; transition: background 0.12s;
}
.sc-row-top:hover { background: rgba(155,93,229,0.02); }
.sc-icon {
width: 42px; height: 42px; border-radius: 12px;
display: flex; align-items: center; justify-content: center; flex-shrink: 0; color: #fff;
}
.sc-icon svg { width: 20px; height: 20px; }
.sc-info { flex: 1; min-width: 0; }
.sc-name { font-family: 'Unbounded', sans-serif; font-size: 0.88rem; font-weight: 800; color: #0F172A; }
.sc-summary {
display: flex; gap: 6px; margin-top: 4px; flex-wrap: wrap; align-items: center;
}
.sc-tag {
font-size: 0.68rem; font-weight: 700; padding: 2px 8px; border-radius: 6px;
background: rgba(15,23,42,0.05); color: #64748B;
}
.sc-tag-mode { background: rgba(155,93,229,0.08); color: var(--violet); }
.sc-qcount { font-size: 0.72rem; color: var(--text-3); font-weight: 600; }
.sc-chevron {
width: 20px; height: 20px; color: #cbd5e1; transition: transform 0.2s; flex-shrink: 0;
}
.sc-card.open .sc-chevron { transform: rotate(180deg); color: var(--violet); }
/* Expanded body */
.sc-body {
display: none; padding: 0 20px 20px; flex-direction: column; gap: 14px;
border-top: 1px solid rgba(15,23,42,0.06);
}
.sc-card.open .sc-body { display: flex; }
/* Presets row */
.sc-presets {
display: flex; gap: 6px; flex-wrap: wrap; padding-top: 14px;
}
.sc-preset {
padding: 6px 14px; border: 1.5px solid var(--border); border-radius: 99px;
background: #fff; font-family: 'Manrope', sans-serif; font-size: 0.76rem;
font-weight: 600; color: #64748B; cursor: pointer; transition: all 0.15s;
}
.sc-preset:hover { border-color: rgba(155,93,229,0.35); color: var(--violet); background: rgba(155,93,229,0.04); }
.sc-preset.active { border-color: var(--violet); background: rgba(155,93,229,0.08); color: var(--violet); }
/* Fields */
.sc-fields { display: flex; flex-direction: column; gap: 12px; }
.sc-field { display: flex; align-items: center; gap: 10px; }
.sc-label {
font-size: 0.72rem; color: var(--text-3); font-weight: 700; white-space: nowrap;
text-transform: uppercase; letter-spacing: 0.04em; min-width: 68px;
}
.sc-select {
flex: 1; padding: 8px 12px; border: 1.5px solid var(--border); border-radius: 10px;
background: #f8f9fc; font-family: 'Manrope', sans-serif; font-size: 0.82rem;
font-weight: 600; color: var(--text); cursor: pointer; transition: border-color 0.15s;
}
.sc-select:focus { border-color: var(--violet); outline: none; }
.sc-input {
width: 68px; padding: 8px 10px; border: 1.5px solid var(--border); border-radius: 10px;
background: #f8f9fc; font-family: 'Manrope', sans-serif; font-size: 0.82rem;
font-weight: 600; color: var(--text); text-align: center; transition: border-color 0.15s;
}
.sc-input:focus { border-color: var(--violet); outline: none; }
.sc-src-toggle {
display: flex; gap: 0; background: rgba(15,23,42,0.04); border-radius: 10px; padding: 3px; flex: 1;
}
.sc-src-btn {
flex: 1; padding: 6px 12px; border: none; border-radius: 8px; background: transparent;
font-family: 'Manrope', sans-serif; font-size: 0.76rem; font-weight: 600;
color: var(--text-3); cursor: pointer; transition: all 0.15s; text-align: center;
}
.sc-src-btn.active { background: #fff; color: var(--violet); box-shadow: 0 1px 4px rgba(15,23,42,0.08); }
.sc-test-pick { display: none; flex-direction: column; gap: 10px; }
.sc-test-pick.open { display: flex; }
/* Footer */
.sc-footer {
display: flex; gap: 8px; align-items: center; padding-top: 14px;
border-top: 1px solid rgba(15,23,42,0.05);
}
.sc-save {
padding: 8px 22px; border: none; border-radius: 999px; background: #0F172A; color: #fff;
font-family: 'Manrope', sans-serif; font-size: 0.8rem; font-weight: 700;
cursor: pointer; transition: all 0.15s;
}
.sc-save:hover { background: #1E293B; }
.sc-save.saved { background: #059652; pointer-events: none; }
.sc-save-add {
padding: 8px 16px; border: 1.5px solid rgba(155,93,229,0.25); border-radius: 999px;
background: rgba(155,93,229,0.06); color: var(--violet);
font-family: 'Manrope', sans-serif; font-size: 0.8rem; font-weight: 700;
cursor: pointer; transition: all 0.15s;
}
.sc-save-add:hover { background: rgba(155,93,229,0.12); border-color: var(--violet); }
/* tables */
.table-wrap { background: var(--surface); backdrop-filter: var(--blur); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; margin-bottom: 48px; box-shadow: var(--shadow); }
table { width: 100%; border-collapse: collapse; }
/* Sticky header: stays visible while body scrolls (in .sb-content). Background opaque (not transparent) so rows scroll cleanly behind. */
th { padding: 14px 20px; text-align: left; font-size: 0.82rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; border-bottom: 1px solid var(--border); background: #E5EAF7; position: sticky; top: 0; z-index: 5; }
td { padding: 15px 20px; font-size: 0.94rem; border-bottom: 1px solid var(--border); vertical-align: middle; }
tr:last-child td { border-bottom: none; }
tr.clickable { cursor: pointer; transition: background var(--tr); }
tr.clickable:hover td { background: rgba(155,93,229,0.04); }
tr.selected td { background: rgba(155,93,229,0.07); }
.role-badge { display: inline-block; padding: 4px 12px; border-radius: var(--r-pill); font-size: 0.78rem; font-weight: 700; }
.role-badge.student { background: rgba(15,23,42,0.07); color: var(--text-3); }
.role-badge.free_student { background: rgba(16,185,129,0.12); color: #059669; }
.role-badge.teacher { background: rgba(6,214,224,0.12); color: #05aab3; }
.role-badge.admin { background: rgba(155,93,229,0.12); color: var(--violet); }
.role-select { padding: 6px 10px; border: 1.5px solid var(--border-h); border-radius: 8px; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 600; background: transparent; color: var(--text); cursor: pointer; }
.role-select:focus { outline: none; border-color: var(--violet); }
.role-select:disabled { opacity: 0.4; cursor: default; }
.pct-cell { font-family: 'Unbounded', sans-serif; font-size: 0.92rem; font-weight: 700; }
.pct-hi { color: var(--green); }
.pct-mid { color: var(--amber); }
.pct-lo { color: var(--pink); }
/* Legacy .user-panel overlay was removed in Phase 6 — the deep page
(#users/:id) replaces it. .btn-close kept for use elsewhere if any. */
.btn-close { padding: 8px 18px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all var(--tr); }
.btn-close:hover { border-color: var(--pink); color: var(--pink); }
.sess-list { display: flex; flex-direction: column; gap: 12px; }
.sess-item { display: flex; align-items: center; gap: 16px; padding: 14px 20px; border: 1px solid var(--border); border-radius: 14px; }
.sess-pct { font-family: 'Unbounded', sans-serif; font-size: 1.1rem; font-weight: 800; min-width: 52px; text-align: center; }
.sess-info { flex: 1; }
.sess-subj { font-size: 0.94rem; font-weight: 700; margin-bottom: 3px; }
.sess-meta { font-size: 0.83rem; color: var(--text-3); }
.sess-score{ font-size: 0.9rem; font-weight: 600; color: var(--text-2); white-space: nowrap; }
.spinner { width: 36px; height: 36px; border: 3px solid var(--border); border-top-color: var(--violet); border-radius: 50%; animation: spin 0.8s linear infinite; margin: 40px auto; display: block; }
.empty { text-align: center; padding: 40px; color: var(--text-3); font-size: 0.95rem; }
.error { color: var(--pink); font-size: 0.92rem; padding: 14px 0; }
/* ── admin two-column layout ── */
.admin-layout { display: flex; gap: 0; align-items: flex-start; }
.admin-nav {
width: 200px; flex-shrink: 0;
position: sticky; top: 24px;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--r-lg); padding: 12px 10px;
box-shadow: var(--shadow);
display: flex; flex-direction: column; gap: 2px;
}
.admin-nav-label {
font-size: .68rem; font-weight: 800; text-transform: uppercase;
letter-spacing: .09em; color: var(--text-3);
padding: 12px 12px 5px; margin-top: 4px;
}
.admin-nav-label:first-child { margin-top: 0; }
.admin-nav-sep { height: 1px; background: var(--border); margin: 8px 6px; }
/* Collapsible nav groups */
.admin-nav-group { margin-bottom: 6px; }
.admin-nav-ghdr {
width: 100%; padding: 6px 10px 4px; border: none; background: none;
display: flex; align-items: center; justify-content: space-between;
font-family: 'Manrope', system-ui, sans-serif;
font-size: 0.68rem; font-weight: 800; letter-spacing: 0.08em;
color: var(--text-3); text-transform: uppercase;
cursor: pointer; transition: color .12s, opacity .12s; opacity: .72;
}
.admin-nav-ghdr:hover { color: var(--violet); opacity: 1; }
.admin-nav-ghdr .adm-chev { width: 12px; height: 12px; transition: transform .18s; }
.admin-nav-group.collapsed .adm-chev { transform: rotate(-90deg); }
.admin-nav-body { display: flex; flex-direction: column; max-height: 1500px; overflow: hidden; transition: max-height .25s ease, opacity .18s; opacity: 1; }
.admin-nav-group.collapsed .admin-nav-body { max-height: 0; opacity: 0; pointer-events: none; }
.admin-nav-group .admin-nav-item { padding-left: 10px; }
.admin-nav-item {
display: flex; align-items: center; gap: 10px;
padding: 10px 12px; border-radius: 10px; border: none;
background: transparent; width: 100%; text-align: left;
font-family: 'Manrope', sans-serif; font-size: .93rem; font-weight: 600;
color: var(--text-2); cursor: pointer; transition: all .14s;
white-space: nowrap;
}
.admin-nav-item svg { flex-shrink: 0; opacity: .7; }
.admin-nav-item:hover { background: rgba(155,93,229,.07); color: var(--text); }
.admin-nav-item:hover svg { opacity: 1; }
.admin-nav-item.active {
background: rgba(155,93,229,.1); color: var(--violet);
font-weight: 700;
}
.admin-nav-item.active svg { opacity: 1; color: var(--violet); }
.admin-nav-item.locked { opacity: .42; cursor: not-allowed; }
.admin-nav-item.locked:hover { background: transparent; color: var(--text-3); }
.admin-nav-item.locked svg { opacity: .55; }
.admin-nav-item.locked .adm-lock {
margin-left: auto; width: 11px; height: 11px; opacity: .8; flex-shrink: 0;
}
.admin-main { flex: 1; min-width: 0; padding-left: 28px; }
/* tab panes */
.tab-pane { display: none; }
.tab-pane.active { display: block; }
/* permissions tab */
.perm-header { margin-bottom: 24px; }
.perm-role-block { background: var(--surface); border: 1.5px solid var(--border-h); border-radius: var(--r-xl); padding: 20px 24px; margin-bottom: 20px; }
.perm-role-title { margin-bottom: 16px; }
.perm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.perm-card { display: flex; align-items: flex-start; gap: 16px; padding: 16px 20px; border: 1.5px solid var(--border); border-radius: var(--r-lg); background: var(--surface-2, #f8fafc); transition: border-color .2s, box-shadow .2s; }
.perm-card:hover { border-color: var(--violet); box-shadow: 0 2px 12px rgba(109,40,217,.08); }
.perm-card.enabled { border-color: rgba(6,214,160,.5); background: rgba(6,214,160,.04); }
.perm-info { flex: 1; min-width: 0; }
.perm-label { font-size: 15px; font-weight: 700; color: var(--text); line-height: 1.3; }
.perm-desc { font-size: 13px; color: var(--muted); margin-top: 4px; line-height: 1.45; }
/* toggle switch */
.perm-toggle { flex-shrink: 0; position: relative; width: 46px; height: 26px; cursor: pointer; }
.perm-toggle input { opacity: 0; width: 0; height: 0; }
.perm-track { position: absolute; inset: 0; border-radius: 26px; background: var(--border-h); transition: background .2s; }
.perm-thumb { position: absolute; top: 3px; left: 3px; width: 20px; height: 20px; border-radius: 50%; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.2); transition: transform .2s; }
.perm-toggle input:checked ~ .perm-track { background: var(--green, #06d6a0); }
.perm-toggle input:checked ~ .perm-thumb { transform: translateX(20px); }
.perm-toggle input:focus-visible ~ .perm-track { outline: 2px solid var(--violet); }
/* dot shown when a role-level perm differs from its registry default */
.perm-modified-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: var(--amber, #FFB347); flex-shrink: 0;
vertical-align: middle; margin-left: 6px;
}
/* toolbar */
.t-toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 24px; }
.t-select { padding: 10px 16px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); font-family: 'Manrope', sans-serif; font-size: 0.92rem; font-weight: 600; background: var(--surface); color: var(--text); cursor: pointer; }
.t-select:focus { outline: none; border-color: var(--violet); }
.t-input { padding: 10px 16px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); font-family: 'Manrope', sans-serif; font-size: 0.92rem; background: var(--surface); color: var(--text); width: 220px; }
.t-input:focus { outline: none; border-color: var(--violet); }
.t-count { font-size: 0.9rem; color: var(--text-3); margin-left: auto; }
.btn-add { padding: 10px 24px; border: none; border-radius: var(--r-pill); background: var(--grad-1); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.92rem; font-weight: 700; cursor: pointer; white-space: nowrap; transition: transform var(--tr), box-shadow var(--tr); }
.btn-add:hover { transform: translateY(-1px); box-shadow: 0 6px 20px rgba(6,214,224,0.35); }
.mode-badge { display: inline-block; padding: 3px 10px; border-radius: var(--r-pill); font-size: 0.76rem; font-weight: 700; }
.mode-exam { background: rgba(155,93,229,0.1); color: var(--violet); }
.mode-practice { background: rgba(6,214,224,0.1); color: #05aab3; }
.mode-repeat { background: rgba(6,214,224,0.1); color: #05aab3; }
.mode-ct { background: rgba(255,179,71,0.12); color: var(--amber); }
.mode-topic { background: rgba(255,179,71,0.12); color: var(--amber); }
.mode-random { background: rgba(15,23,42,0.07); color: var(--text-3); }
/* ── assignment card rows ── */
.a-summary { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 18px; }
.a-sum-chip { padding: 6px 16px; border-radius: var(--r-pill); font-size: 0.85rem; font-weight: 700; font-family: 'Manrope', sans-serif; display: flex; align-items: center; gap: 7px; }
.a-sum-chip.s-all { background: rgba(15,23,42,0.06); color: var(--text-3); }
.a-sum-chip.s-active { background: rgba(155,93,229,0.1); color: var(--violet); }
.a-sum-chip.s-overdue { background: rgba(241,91,181,0.12); color: var(--pink); }
.a-sum-chip.s-done { background: rgba(6,214,100,0.12); color: #059652; }
.a-filter-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 18px; }
.a-f-chip { padding: 6px 18px; border-radius: var(--r-pill); border: 1.5px solid var(--border-h); background: var(--surface); color: var(--text-3); font-family: 'Manrope', sans-serif; font-size: 0.85rem; font-weight: 700; cursor: pointer; transition: all var(--tr); }
.a-f-chip:hover { border-color: var(--violet); color: var(--violet); }
.a-f-chip.active { background: #0F172A; color: #fff; border-color: #0F172A; }
.a-filter-sep { width: 1px; height: 22px; background: var(--border-h); margin: 0 4px; }
.a-sort-sel { padding: 6px 14px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); background: var(--surface); color: var(--text-3); font-family: 'Manrope', sans-serif; font-size: 0.85rem; font-weight: 600; cursor: pointer; }
.a-sort-sel:focus { outline: none; border-color: var(--violet); }
.a-rows { display: flex; flex-direction: column; gap: 6px; }
.a-row {
display: flex; align-items: center; gap: 14px;
background: var(--surface);
border: 1.5px solid var(--border);
border-left: 4px solid var(--ac, var(--violet));
border-radius: 14px;
padding: 0 16px 0 12px;
height: 68px;
transition: box-shadow 0.15s, transform 0.15s;
}
.a-row:hover { box-shadow: 0 4px 18px rgba(15,23,42,0.1); transform: translateX(2px); }
.a-row.a-overdue { --ac: var(--pink); background: rgba(241,91,181,0.02); }
.a-row.a-done { --ac: var(--green); opacity: 0.76; }
.a-row.a-urgent { --ac: #FF4C29; }
.a-icon { width: 36px; height: 36px; border-radius: 9px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 1.05rem; }
.a-main { flex: 1; min-width: 0; }
.a-title { font-size: 0.94rem; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text); line-height: 1.2; }
.a-meta { font-size: 0.78rem; color: var(--text-3); margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.a-meta .a-tag-over { color: var(--pink); font-weight: 700; }
.a-meta .a-tag-urgent { color: #E83A1E; font-weight: 700; }
.a-prog { flex-shrink: 0; width: 120px; }
.a-prog-nums { display: flex; justify-content: space-between; font-size: 0.76rem; color: var(--text-3); font-weight: 600; margin-bottom: 5px; }
.a-prog-pct { font-family: 'Unbounded', sans-serif; font-size: 0.84rem; font-weight: 900; }
.a-prog-bar { height: 6px; border-radius: 99px; background: rgba(15,23,42,0.08); overflow: hidden; }
.a-prog-fill { height: 100%; border-radius: 99px; transition: width 0.3s; }
.a-actions { display: flex; gap: 6px; flex-shrink: 0; }
/* session drawer */
.sess-drawer { overflow: hidden; max-height: 0; transition: max-height 0.35s ease; }
.sess-drawer.open { max-height: 4000px; }
.sess-drawer-inner { padding: 28px 32px; background: rgba(238,242,255,0.6); border-top: 1px solid var(--border); }
.drawer-header { display: flex; align-items: center; gap: 18px; margin-bottom: 24px; flex-wrap: wrap; }
.drawer-meta { font-size: 0.9rem; color: var(--text-3); }
.drawer-score { font-family: 'Unbounded', sans-serif; font-size: 1.8rem; font-weight: 900; }
.qb-list { display: flex; flex-direction: column; gap: 12px; }
.qb-item { border-radius: 14px; padding: 16px 20px; border: 1px solid var(--border); background: var(--surface); }
.qb-item.correct { border-left: 3px solid var(--green); }
.qb-item.wrong { border-left: 3px solid var(--pink); }
.qb-item.skipped { border-left: 3px solid var(--text-3); }
.qb-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.qb-badge { padding: 3px 10px; border-radius: var(--r-pill); font-size: 0.74rem; font-weight: 700; }
.qb-badge.correct { background: rgba(6,214,100,0.1); color: var(--green); }
.qb-badge.wrong { background: rgba(241,91,181,0.1); color: var(--pink); }
.qb-badge.skipped { background: rgba(15,23,42,0.06); color: var(--text-3); }
.qb-qnum { font-size: 0.82rem; color: var(--text-3); font-weight: 600; }
.qb-time { margin-left: auto; font-size: 0.79rem; color: var(--text-3); }
.qb-text { font-size: 0.96rem; line-height: 1.6; margin-bottom: 12px; }
.qb-opts { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
.qb-opt { display: flex; align-items: center; gap: 9px; padding: 8px 12px; border-radius: 9px; font-size: 0.9rem; }
.qb-opt.correct-opt { background: rgba(6,214,100,0.08); color: var(--green); font-weight: 600; }
.qb-opt.chosen-wrong { background: rgba(241,91,181,0.08); color: var(--pink); }
.qb-opt-icon { font-size: 0.88rem; width: 18px; text-align: center; flex-shrink: 0; }
.qb-expl { font-size: 0.88rem; color: var(--text-2); background: rgba(155,93,229,0.05); border: 1px solid rgba(155,93,229,0.12); border-radius: 9px; padding: 10px 14px; line-height: 1.6; }
/* ── E: upgraded stat cards ── */
.stat-card { position: relative; overflow: hidden; }
.stat-card::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--stat-top, var(--violet)); opacity: 0.7; }
.stat-card-icon { width: 48px; height: 48px; border-radius: 14px; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; margin-bottom: 14px; }
/* ── F: performance bar in users table ── */
.perf-bar { height: 4px; background: rgba(15,23,42,0.06); border-radius: 99px; margin-top: 6px; min-width: 70px; overflow: hidden; }
.perf-fill { height: 100%; border-radius: 99px; }
.perf-fill.pct-hi { background: var(--green); }
.perf-fill.pct-mid { background: var(--amber); }
.perf-fill.pct-lo { background: var(--pink); }
/* ── H: session timeline ── */
.sess-tl-day { font-family: 'Unbounded', sans-serif; font-size: 0.76rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; margin: 24px 0 12px; display: flex; align-items: center; gap: 10px; }
.sess-tl-day::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.sess-tl-day:first-child { margin-top: 0; }
.sess-tl-item { display: flex; align-items: center; gap: 16px; padding: 15px 20px; border: 1px solid var(--border); border-radius: 14px; margin-bottom: 10px; transition: all var(--tr); cursor: pointer; background: var(--surface); }
.sess-tl-item:hover { border-color: var(--border-h); transform: translateX(3px); box-shadow: 0 4px 20px rgba(15,23,42,0.07); }
.sess-tl-item.open { border-color: var(--violet); background: rgba(155,93,229,0.04); }
.sess-tl-ring { flex-shrink: 0; }
.sess-tl-user { flex: 1; min-width: 0; }
.sess-tl-name { font-size: 0.94rem; font-weight: 700; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sess-tl-meta { font-size: 0.81rem; color: var(--text-3); }
.sess-tl-score { font-size: 0.8rem; font-weight: 600; color: var(--text-2); white-space: nowrap; }
.sess-tl-time { font-size: 0.75rem; color: var(--text-3); white-space: nowrap; }
.sess-tl-drawer { overflow: hidden; max-height: 0; transition: max-height 0.35s ease; margin-bottom: 0; }
.sess-tl-drawer.open { max-height: 4000px; margin-bottom: 8px; }
.sess-tl-wrap { display: flex; flex-direction: column; }
/* ═══ QUESTION EDITOR ═══════════════════════════════════════════════ */
.q-list { display: flex; flex-direction: column; gap: 12px; }
.q-card { background: var(--surface); backdrop-filter: var(--blur); border: 1px solid var(--border); border-radius: var(--r-lg); overflow: hidden; transition: border-color var(--tr), box-shadow var(--tr); }
.q-card:hover { border-color: var(--border-h); box-shadow: 0 4px 20px rgba(15,23,42,0.07); }
.q-card-head { display: flex; align-items: flex-start; gap: 14px; padding: 18px 20px; }
.q-card-num { font-family: 'Unbounded', sans-serif; font-size: 0.7rem; font-weight: 700; color: var(--text-3); min-width: 36px; padding-top: 3px; flex-shrink: 0; }
.q-card-body { flex: 1; min-width: 0; cursor: pointer; }
.q-card-text { font-size: 0.9rem; font-weight: 600; line-height: 1.5; margin-bottom: 8px; }
.q-card-meta { display: flex; gap: 8px; flex-wrap: wrap; }
.q-badge { display: inline-block; padding: 2px 8px; border-radius: var(--r-pill); font-size: 0.68rem; font-weight: 700; }
.q-badge-subj { background: rgba(155,93,229,0.1); color: var(--violet); }
.q-badge-topic { background: rgba(6,214,224,0.1); color: #05aab3; }
.diff-1 { background: rgba(6,214,100,0.1); color: var(--green); }
.diff-2 { background: rgba(255,179,71,0.1); color: var(--amber); }
.diff-3 { background: rgba(241,91,181,0.1); color: var(--pink); }
.q-card-actions { display: flex; gap: 6px; flex-shrink: 0; align-items: flex-start; padding-top: 2px; }
.btn-edit-q { padding: 5px 12px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all var(--tr); }
.btn-edit-q:hover { border-color: var(--violet); color: var(--violet); }
.btn-dup-q { padding: 5px 10px; border: 1.5px solid transparent; border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all var(--tr); }
.btn-dup-q:hover { border-color: var(--cyan); color: #05aab3; }
.btn-del-q { padding: 5px 10px; border: 1.5px solid transparent; border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.75rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all var(--tr); }
.btn-del-q:hover { border-color: var(--pink); color: var(--pink); }
.q-card-detail { display: none; padding: 0 20px 16px 70px; border-top: 1px solid var(--border); margin-top: -1px; }
.q-card-detail.open { display: block; }
.q-opt-row { display: flex; align-items: center; gap: 10px; padding: 7px 0; font-size: 0.85rem; border-bottom: 1px solid var(--border); }
.q-opt-row:last-of-type { border-bottom: none; }
.q-opt-row.correct { color: var(--green); font-weight: 700; }
.q-opt-icon { font-size: 0.85rem; width: 18px; flex-shrink: 0; }
.q-expl { margin-top: 12px; font-size: 0.8rem; color: var(--text-2); background: rgba(155,93,229,0.05); border: 1px solid rgba(155,93,229,0.12); border-radius: 10px; padding: 10px 14px; line-height: 1.6; }
/* modal */
.q-modal { position: fixed; inset: 0; z-index: 400; display: none; align-items: flex-start; justify-content: center; padding: 32px 20px 60px; background: rgba(15,23,42,0.5); backdrop-filter: blur(10px); overflow-y: auto; }
.q-modal.open { display: flex; }
.q-modal-box { background: #fff; border-radius: 24px; padding: 36px 40px; width: 100%; max-width: 780px; box-shadow: 0 40px 100px rgba(15,23,42,0.26); }
.q-modal-title { font-family: 'Unbounded', sans-serif; font-size: 1.05rem; font-weight: 800; margin-bottom: 28px; }
.form-row { margin-bottom: 18px; }
.form-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 18px; }
.form-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; margin-bottom: 18px; }
.form-label { display: block; font-size: 0.72rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 6px; }
.form-hint { font-size: 0.72rem; color: var(--text-3); margin-top: 4px; }
.form-ctrl { width: 100%; padding: 10px 14px; border: 1.5px solid rgba(15,23,42,0.16); border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: 0.9rem; color: var(--text); background: #f8f9ff; resize: vertical; transition: border-color 0.2s; }
.form-ctrl:focus { outline: none; border-color: var(--violet); background: #fff; }
.char-counter { font-size: 0.72rem; color: var(--text-3); text-align: right; margin-top: 3px; }
.char-counter.warn { color: var(--amber); }
.char-counter.over { color: var(--pink); }
/* options in modal */
.opts-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.opts-label { font-size: 0.72rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; }
.opts-grid { display: flex; flex-direction: column; gap: 8px; margin-bottom: 10px; }
.opt-row { display: flex; align-items: center; gap: 10px; padding: 8px 12px; border-radius: 12px; border: 1.5px solid rgba(15,23,42,0.12); background: #f8f9ff; transition: border-color 0.2s, background 0.2s; }
.opt-row.opt-correct { border-color: var(--green); background: rgba(6,214,100,0.06); }
.opt-letter { font-size: 0.78rem; font-weight: 800; color: var(--text-3); min-width: 18px; font-family: 'Unbounded', sans-serif; }
.opt-row.opt-correct .opt-letter { color: var(--green); }
.opt-radio { width: 18px; height: 18px; accent-color: var(--green); flex-shrink: 0; cursor: pointer; }
.opt-input { flex: 1; border: none; background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.9rem; color: var(--text); outline: none; }
.opt-row.opt-correct .opt-input { font-weight: 600; color: #1a7a3e; }
.btn-rem-opt { width: 24px; height: 24px; border: none; border-radius: 50%; background: rgba(15,23,42,0.07); color: var(--text-3); font-size: 1rem; line-height: 1; cursor: pointer; flex-shrink: 0; transition: background 0.2s, color 0.2s; display: flex; align-items: center; justify-content: center; }
.btn-rem-opt:hover { background: rgba(241,91,181,0.12); color: var(--pink); }
.btn-add-opt { padding: 7px 16px; border: 1.5px dashed rgba(15,23,42,0.2); border-radius: 10px; background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600; color: var(--text-3); cursor: pointer; width: 100%; margin-bottom: 18px; transition: border-color 0.2s, color 0.2s; }
.btn-add-opt:hover { border-color: var(--violet); color: var(--violet); }
.img-upload-row { display: flex; gap: 8px; align-items: center; }
.btn-img-upload { display: inline-flex; align-items: center; gap: 6px; padding: 0 16px; height: 40px; border: 1.5px solid rgba(15,23,42,0.18); border-radius: 10px; background: var(--surface); font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600; color: var(--text-2); cursor: pointer; white-space: nowrap; transition: border-color 0.2s, color 0.2s; flex-shrink: 0; }
.btn-img-upload:hover { border-color: var(--violet); color: var(--violet); }
.btn-img-upload:disabled { opacity: 0.5; cursor: not-allowed; }
#qf-image-preview { display: none; }
#qf-image-preview.visible { display: inline-block !important; }
.modal-footer { display: flex; gap: 10px; justify-content: flex-end; margin-top: 24px; border-top: 1px solid var(--border); padding-top: 20px; }
.btn-cancel2 { padding: 10px 22px; border: 1.5px solid rgba(15,23,42,0.2); border-radius: 999px; background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 600; color: var(--text-3); cursor: pointer; }
.btn-save { padding: 10px 28px; border: none; border-radius: 999px; background: var(--grad-1); color: #fff; font-family: 'Manrope', sans-serif; font-size: 0.88rem; font-weight: 700; cursor: pointer; min-width: 130px; transition: opacity 0.2s; }
.btn-save:disabled { opacity: 0.5; cursor: not-allowed; }
.form-error { font-size: 0.82rem; color: var(--pink); margin-top: 8px; min-height: 20px; }
/* question type buttons */
.type-btn { padding: 6px 14px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); background: transparent; font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 600; color: var(--text-3); cursor: pointer; transition: all var(--tr); }
.type-btn.active { background: var(--violet); border-color: var(--violet); color: #fff; }
/* ═══ TESTS TAB ══════════════════════════════════════════════════════ */
.tst-drawer { border-top: 1px solid var(--border); background: rgba(238,242,255,0.5); }
.tst-drawer-inner { padding: 20px 24px; }
.tst-cols { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
@media(max-width:700px){ .tst-cols { grid-template-columns: 1fr; } }
.tst-panel-title { font-size: 0.72rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 10px; }
.tst-q-list { display: flex; flex-direction: column; gap: 2px; max-height: 400px; overflow-y: auto; border: 1px solid var(--border); border-radius: 10px; background: #fff; padding: 4px; }
.tst-q-item { display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px; border-radius: 7px; font-size: 0.83rem; transition: background var(--tr); }
.tst-q-item:hover { background: rgba(15,23,42,0.03); }
.tst-q-num { font-size: 0.68rem; font-weight: 700; color: var(--text-3); min-width: 22px; padding-top: 2px; }
.tst-q-body { flex: 1; min-width: 0; }
.tst-q-text { display: block; line-height: 1.45; word-break: break-word; margin-bottom: 4px; }
.tst-q-meta { display: flex; gap: 4px; flex-wrap: wrap; }
.tst-q-badge { font-size: 0.62rem; font-weight: 700; padding: 1px 6px; border-radius: 999px; flex-shrink: 0; }
.tst-q-opts { font-size: 0.68rem; color: var(--text-3); padding-top: 2px; }
.btn-tst-rem { width: 22px; height: 22px; border: none; border-radius: 50%; background: rgba(241,91,181,0.1); color: var(--pink); font-size: 0.85rem; cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; transition: background var(--tr); }
.btn-tst-rem:hover { background: rgba(241,91,181,0.22); }
.btn-tst-add { width: 22px; height: 22px; border: none; border-radius: 50%; background: rgba(6,214,100,0.12); color: var(--green); font-size: 1rem; line-height: 1; cursor: pointer; flex-shrink: 0; display: flex; align-items: center; justify-content: center; font-weight: 700; transition: background var(--tr); }
.btn-tst-add:hover { background: rgba(6,214,100,0.25); }
.btn-tst-add.added { background: rgba(6,214,100,0.25); cursor: default; opacity: 0.6; }
.tst-search { width: 100%; padding: 7px 12px; border: 1.5px solid var(--border-h); border-radius: 8px; font-family: 'Manrope', sans-serif; font-size: 0.83rem; background: #fff; color: var(--text); margin-bottom: 8px; outline: none; }
.tst-search:focus { border-color: var(--violet); }
.tst-empty { text-align: center; padding: 20px; color: var(--text-3); font-size: 0.82rem; }
.src-toggle { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
/* formula bar */
/* Formula bar: hidden by default, toggled via #qf-fml-toggle */
.formula-bar { display: none; gap: 4px; align-items: center; flex-wrap: wrap; padding: 8px 12px; background: #f0f2ff; border-radius: 10px; margin-bottom: 8px; }
.formula-bar.visible { display: flex; }
.fml-toggle {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 10px; margin-bottom: 6px;
border: 1px solid var(--border); border-radius: 999px;
background: transparent; color: var(--text-3); cursor: pointer;
font-family: 'Manrope', sans-serif; font-size: 0.74rem; font-weight: 700;
transition: all .12s;
}
.fml-toggle:hover { border-color: var(--violet); color: var(--violet); }
.fml-toggle.open { background: #f0f2ff; color: var(--violet); border-color: rgba(155,93,229,.3); }
.fml-toggle .fml-chev { width: 11px; height: 11px; transition: transform .18s; }
.fml-toggle.open .fml-chev { transform: rotate(180deg); }
/* Preview: hidden by default, visible when textarea has content */
.q-preview-wrap.hidden { display: none; }
.fml { padding: 4px 9px; border: 1px solid rgba(155,93,229,0.2); border-radius: 6px; background: #fff; font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600; color: var(--violet); cursor: pointer; transition: background 0.15s; }
.fml:hover { background: rgba(155,93,229,0.08); }
/* preview */
.q-preview-wrap { background: rgba(155,93,229,0.04); border: 1px solid rgba(155,93,229,0.15); border-radius: 10px; padding: 12px 16px; margin-top: 8px; margin-bottom: 18px; min-height: 40px; }
.q-preview-text { font-size: 0.95rem; line-height: 1.6; color: var(--text); }
.form-hint { font-size: 0.72rem; color: var(--text-3); margin-top: 4px; }
/* ── Mobile responsive ── */
@media (max-width: 768px) {
.container { padding: 16px 12px 80px; }
.page-title { font-size: 1.1rem; }
.page-sub { font-size: 0.83rem; margin-bottom: 24px; }
/* Stats */
.stats-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; }
.stat-val { font-size: 1.5rem; }
/* Admin nav — collapse to horizontal scroll strip on mobile */
.admin-layout { flex-direction: column; gap: 0; align-items: stretch; }
.admin-nav {
width: 100%; position: static;
flex-direction: row; overflow-x: auto; overflow-y: hidden;
flex-wrap: nowrap; padding: 6px; gap: 2px;
scrollbar-width: none; border-radius: 14px;
margin-bottom: 20px;
}
.admin-nav::-webkit-scrollbar { display: none; }
.admin-nav-label { display: none; }
.admin-nav-sep { display: none !important; }
.admin-nav-item { padding: 7px 12px; font-size: .78rem; white-space: nowrap; border-radius: 8px; }
.admin-nav-item svg { display: none; }
/* admin-main must fill full width when stacked */
.admin-main { padding-left: 0; width: 100%; min-width: 0; box-sizing: border-box; }
/* Toolbar: selects wrap and fill available space */
.t-toolbar { gap: 7px; flex-wrap: wrap; }
.t-select { flex: 1 1 120px; min-width: 0; max-width: 100%; font-size: 0.82rem; padding: 8px 10px; }
.t-input { width: 100%; flex: 1 1 140px; min-width: 0; }
.t-count { margin-left: 0; width: 100%; }
.btn-add { width: 100%; justify-content: center; }
/* Tables: horizontal scroll container */
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { min-width: 540px; }
th, td { padding: 10px 12px; }
/* Assignment rows */
.a-row { height: auto; min-height: 56px; padding: 8px 10px; gap: 8px; flex-wrap: wrap; }
.a-prog { display: none; } /* hide progress column on mobile */
.a-meta { white-space: normal; }
/* Subject config cards */
.sc-row-top { padding: 12px 14px; gap: 10px; }
.sc-icon { width: 36px; height: 36px; border-radius: 10px; }
.sc-icon svg { width: 16px; height: 16px; }
.sc-name { font-size: 0.82rem; }
.sc-body { padding: 0 14px 16px; gap: 10px; }
.sc-presets { gap: 4px; padding-top: 10px; }
.sc-preset { font-size: 0.7rem; padding: 5px 10px; }
.sc-field { flex-wrap: wrap; gap: 6px; }
.sc-label { min-width: 56px; font-size: 0.68rem; }
.sc-input { width: 60px; }
.sc-footer { gap: 6px; }
/* Question editor modal */
.q-modal { padding: 0; align-items: flex-end; overflow-y: hidden; }
.q-modal-box {
border-radius: 22px 22px 0 0;
padding: 24px 16px 32px;
max-height: 92vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.q-modal-title { font-size: 0.9rem; margin-bottom: 20px; }
.form-row-2, .form-row-3 { grid-template-columns: 1fr; }
/* Session drawer */
.sess-drawer-inner { padding: 16px 12px; }
.drawer-header { gap: 10px; }
.drawer-score { font-size: 1.2rem; }
/* Session timeline */
.sess-tl-item { padding: 10px 12px; gap: 10px; }
.sess-tl-time { display: none; }
/* Subj stats: stretch to fill row */
.subj-stats { gap: 8px; }
.subj-stat { padding: 10px 12px; flex: 1 1 calc(50% - 8px); min-width: 0; box-sizing: border-box; }
.subj-stat-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.subj-stat-info { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* adm-panel (shop/gam/perms tabs): reduce padding */
.adm-panel { padding: 18px 14px; }
/* q-card-detail: reduce deep left indent */
.q-card-detail { padding-left: 20px; }
/* modal footer: wrap buttons on narrow screens */
.q-modal-box .modal-footer { flex-wrap: wrap; }
.q-modal-box .modal-footer .btn-cancel2,
.q-modal-box .modal-footer .btn-save { flex: 1; min-width: 0; text-align: center; justify-content: center; }
/* subj-stat: don't let % value push items off screen */
.subj-stat-pct { min-width: 36px; font-size: 0.95rem; }
}
@media (max-width: 480px) {
.container { padding: 14px 10px 80px; }
.stats-grid { grid-template-columns: 1fr; gap: 8px; }
.stat-val { font-size: 1.2rem; }
.stat-card { padding: 16px 14px; }
.admin-nav-item { padding: 6px 11px; font-size: 0.75rem; }
.a-actions { gap: 4px; }
.a-icon { display: none; }
/* subj-stat: 1 column on very narrow */
.subj-stat { flex: 1 1 100%; }
/* perm-grid: single column at 480px */
.perm-grid { grid-template-columns: 1fr; }
/* adm-form-row: full-width inputs */
.adm-form-group input,
.adm-form-group select,
.adm-form-group textarea { width: 100%; box-sizing: border-box; }
/* toolbar: each select full row */
.t-select { flex: 1 1 100%; }
}
/* ═══ SHOP / GAM / TPL tabs ═══ */
.shop-items-table td, .gam-top-table td, .gam-log-table td, .tpl-table td { font-size: 0.92rem; }
.adm-panel { background: var(--surface); backdrop-filter: var(--blur); border: 1.5px solid var(--border); border-radius: var(--r-lg); padding: 28px 32px; margin-bottom: 28px; }
.adm-panel-title { font-family: 'Unbounded', sans-serif; font-size: 0.88rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 20px; }
.adm-form-row { display: flex; gap: 14px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 16px; }
.adm-form-group { display: flex; flex-direction: column; gap: 5px; }
.adm-form-group label { font-size: 0.76rem; font-weight: 700; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.04em; }
.adm-form-group input, .adm-form-group select, .adm-form-group textarea { padding: 10px 14px; border: 1.5px solid var(--border-h); border-radius: 10px; font-family: 'Manrope', sans-serif; font-size: 0.92rem; background: var(--surface); color: var(--text); }
.adm-form-group input:focus, .adm-form-group select:focus, .adm-form-group textarea:focus { outline: none; border-color: var(--violet); }
.adm-form-group textarea { resize: vertical; min-height: 70px; }
.adm-btn { padding: 10px 24px; border: none; border-radius: var(--r-pill); font-family: 'Manrope', sans-serif; font-size: 0.92rem; font-weight: 700; cursor: pointer; transition: opacity .2s; }
.adm-btn-primary { background: var(--grad-1); color: #fff; }
.adm-btn-danger { background: var(--pink); color: #fff; }
.adm-btn-small { padding: 6px 14px; font-size: 0.84rem; }
.adm-btn:hover { opacity: 0.88; }
.adm-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.adm-toggle { position: relative; width: 42px; height: 24px; cursor: pointer; display: inline-block; vertical-align: middle; }
.adm-toggle input { opacity: 0; width: 0; height: 0; }
.adm-toggle .track { position: absolute; inset: 0; border-radius: 24px; background: var(--border-h); transition: background .2s; }
.adm-toggle .thumb { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 1px 4px rgba(0,0,0,.15); transition: transform .2s; }
.adm-toggle input:checked ~ .track { background: var(--green, #06d6a0); }
.adm-toggle input:checked ~ .thumb { transform: translateX(18px); }
.adm-user-search { position: relative; }
.adm-user-search .us-results { position: absolute; top: 100%; left: 0; right: 0; z-index: 50; background: #fff; border: 1.5px solid var(--border-h); border-radius: 12px; max-height: 240px; overflow-y: auto; box-shadow: 0 8px 24px rgba(15,23,42,0.12); display: none; }
.adm-user-search .us-results.open { display: block; }
.adm-user-search .us-item { padding: 10px 16px; font-size: 0.92rem; cursor: pointer; transition: background .15s; display: flex; justify-content: space-between; align-items: center; }
.adm-user-search .us-item:hover { background: rgba(155,93,229,0.06); }
.adm-user-search .us-item .us-role { font-size: 0.76rem; color: var(--text-3); }
/* ── Submission log ── */
.sl-wrap {
overflow-x: auto; border: 1.5px solid var(--border); border-radius: 16px;
background: #fff; box-shadow: 0 2px 12px rgba(15,23,42,0.05);
}
.sl-table { width: 100%; border-collapse: collapse; min-width: 800px; }
.sl-table th {
padding: 12px 14px; text-align: left; font-size: 0.68rem; font-weight: 800;
color: var(--violet); text-transform: uppercase; letter-spacing: 0.07em;
border-bottom: 2px solid rgba(155,93,229,0.12);
background: linear-gradient(180deg, rgba(155,93,229,0.04) 0%, rgba(155,93,229,0.01) 100%);
white-space: nowrap;
}
.sl-table td {
padding: 12px 14px; font-size: 0.82rem; color: #1E293B;
border-bottom: 1px solid rgba(15,23,42,0.06); vertical-align: middle;
}
.sl-table tr:last-child td { border-bottom: none; }
.sl-table tbody tr { transition: background 0.12s; }
.sl-table tbody tr:hover td { background: rgba(155,93,229,0.03); }
.sl-table tbody tr:nth-child(even) td { background: rgba(15,23,42,0.015); }
.sl-table tbody tr:nth-child(even):hover td { background: rgba(155,93,229,0.04); }
.sl-date { font-size: 0.78rem; color: #64748B; white-space: nowrap; }
.sl-student {
font-weight: 700; color: #0F172A; display: flex; align-items: center; gap: 8px;
}
.sl-student-avatar {
width: 28px; height: 28px; border-radius: 8px; flex-shrink: 0;
background: linear-gradient(135deg, rgba(155,93,229,0.15), rgba(6,214,224,0.1));
display: flex; align-items: center; justify-content: center;
font-size: 0.65rem; font-weight: 800; color: var(--violet);
}
.sl-file {
max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-size: 0.78rem; color: #64748B; font-family: monospace;
}
.sl-assignment { font-weight: 600; color: #3D4F6B; }
.sl-class { font-size: 0.78rem; color: var(--text-3); }
.sl-status {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 99px; font-size: 0.7rem; font-weight: 700;
white-space: nowrap;
}
.sl-status-new { background: rgba(6,214,224,0.1); color: #06aab3; }
.sl-status-reviewed { background: rgba(5,150,82,0.08); color: #059652; }
.sl-status-accepted { background: rgba(5,150,82,0.12); color: #047857; }
.sl-status-revision { background: rgba(241,91,181,0.08); color: #c0306a; }
.sl-status-resubmitted { background: rgba(59,130,246,0.08); color: #3B82F6; }
.sl-grade {
font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 800;
min-width: 32px; text-align: center;
}
.sl-grade-hi { color: #059652; }
.sl-grade-mid { color: #c07c00; }
.sl-grade-lo { color: #c0306a; }
.sl-grade-none { color: #cbd5e1; font-weight: 400; font-family: 'Manrope', sans-serif; font-size: 0.78rem; }
.sl-deleted-by {
display: flex; align-items: center; gap: 6px;
font-size: 0.78rem; color: #64748B;
}
.sl-role-badge {
font-size: 0.62rem; font-weight: 700; padding: 1px 6px; border-radius: 4px;
text-transform: uppercase; letter-spacing: 0.04em;
}
.sl-role-admin { background: rgba(241,91,181,0.1); color: #c0306a; }
.sl-role-teacher { background: rgba(155,93,229,0.1); color: var(--violet); }
.sl-role-student { background: rgba(6,214,224,0.1); color: #06aab3; }
.sl-empty {
padding: 48px 24px; text-align: center; color: var(--text-3); font-size: 0.88rem;
}
.sl-empty-icon { margin-bottom: 12px; opacity: 0.3; }
.sl-filter-row { display: flex; align-items: center; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.sl-filter-select {
padding: 8px 14px; border: 1.5px solid var(--border); border-radius: 10px;
font-family: 'Manrope', sans-serif; font-size: 0.82rem; font-weight: 600;
color: #3D4F6B; background: #fff; cursor: pointer; min-width: 200px;
transition: border-color 0.15s;
}
.sl-filter-select:focus { border-color: var(--violet); outline: none; }
.sl-count { font-size: 0.78rem; color: var(--text-3); font-weight: 600; }
/* ══════════ CLASSROOM ADMIN TAB ══════════ */
.cr-admin-section { margin-bottom: 40px; }
.cr-admin-section-title {
font-family: 'Unbounded', sans-serif; font-size: 0.82rem; font-weight: 800;
color: var(--text-3); text-transform: uppercase; letter-spacing: 0.07em;
margin-bottom: 16px; display: flex; align-items: center; gap: 10px;
}
.cr-admin-section-title::after { content: ''; flex: 1; height: 1px; background: var(--border); }
/* Active session card */
.cr-live-list { display: flex; flex-direction: column; gap: 10px; }
.cr-live-card {
display: flex; align-items: center; gap: 14px;
background: var(--surface); border: 1.5px solid var(--border);
border-left: 4px solid #EF4444; border-radius: 16px;
padding: 14px 18px; transition: box-shadow 0.15s, transform 0.15s;
}
.cr-live-card:hover { box-shadow: 0 4px 20px rgba(15,23,42,0.1); transform: translateX(2px); }
.cr-live-pulse {
width: 10px; height: 10px; border-radius: 50%; background: #EF4444; flex-shrink: 0;
animation: pulse-live 1.4s ease-in-out infinite;
}
@keyframes pulse-live {
0%,100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.5); }
50% { box-shadow: 0 0 0 6px rgba(239,68,68,0); }
}
.cr-live-info { flex: 1; min-width: 0; }
.cr-live-title { font-size: 0.96rem; font-weight: 700; margin-bottom: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cr-live-meta { font-size: 0.81rem; color: var(--text-3); }
.cr-live-badges { display: flex; gap: 6px; flex-shrink: 0; align-items: center; }
.cr-badge {
display: inline-flex; align-items: center; gap: 5px;
padding: 4px 10px; border-radius: 99px; font-size: 0.76rem; font-weight: 700;
}
.cr-badge-online { background: rgba(6,214,100,0.12); color: #059652; }
.cr-badge-msgs { background: rgba(6,214,224,0.12); color: #05aab3; }
.cr-badge-dur { background: rgba(155,93,229,0.1); color: var(--violet); }
.cr-live-actions { display: flex; gap: 6px; flex-shrink: 0; }
/* History session row */
.cr-hist-list { display: flex; flex-direction: column; gap: 8px; }
.cr-hist-row {
display: flex; align-items: center; gap: 14px;
background: var(--surface); border: 1px solid var(--border);
border-radius: 14px; padding: 12px 16px; cursor: pointer;
transition: border-color 0.15s, box-shadow 0.15s;
}
.cr-hist-row:hover { border-color: var(--violet); box-shadow: 0 2px 12px rgba(109,40,217,0.07); }
.cr-hist-row.open { border-color: var(--violet); background: rgba(155,93,229,0.03); border-radius: 14px 14px 0 0; border-bottom: none; }
.cr-hist-icon { width: 38px; height: 38px; border-radius: 10px; background: rgba(155,93,229,0.1); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.cr-hist-main { flex: 1; min-width: 0; }
.cr-hist-title { font-size: 0.94rem; font-weight: 700; margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cr-hist-meta { font-size: 0.79rem; color: var(--text-3); }
.cr-hist-chips { display: flex; gap: 6px; flex-shrink: 0; flex-wrap: wrap; }
.cr-hist-chevron { width: 18px; height: 18px; color: var(--text-3); transition: transform 0.2s; flex-shrink: 0; }
.cr-hist-row.open .cr-hist-chevron { transform: rotate(180deg); color: var(--violet); }
/* Session detail drawer */
.cr-detail-drawer {
overflow: hidden; max-height: 0; transition: max-height 0.35s ease;
border: 1px solid var(--violet); border-top: none;
border-radius: 0 0 14px 14px; background: rgba(238,242,255,0.5);
margin-bottom: 0;
}
.cr-detail-drawer.open { max-height: 3000px; margin-bottom: 8px; }
.cr-detail-inner { padding: 20px 24px; }
.cr-detail-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
@media(max-width:700px) { .cr-detail-grid { grid-template-columns: repeat(2, 1fr); } }
.cr-detail-stat {
background: #fff; border: 1px solid var(--border); border-radius: 12px;
padding: 14px 16px; text-align: center;
}
.cr-detail-val { font-family: 'Unbounded', sans-serif; font-size: 1.3rem; font-weight: 800; color: var(--violet); margin-bottom: 4px; }
.cr-detail-label { font-size: 0.72rem; color: var(--text-3); font-weight: 700; text-transform: uppercase; }
.cr-attend-list { display: flex; flex-direction: column; gap: 6px; margin-top: 12px; }
.cr-attend-row {
display: flex; align-items: center; gap: 12px; padding: 8px 12px;
border: 1px solid var(--border); border-radius: 10px; background: #fff;
font-size: 0.88rem;
}
.cr-attend-name { flex: 1; font-weight: 600; }
.cr-attend-time { color: var(--text-3); font-size: 0.8rem; }
.cr-attend-dur { color: var(--cyan); font-weight: 700; font-size: 0.8rem; min-width: 60px; text-align: right; }
.cr-pages-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px,1fr)); gap: 8px; margin-top: 10px; }
.cr-page-chip {
display: flex; align-items: center; justify-content: space-between;
background: #fff; border: 1px solid var(--border); border-radius: 8px;
padding: 8px 12px; font-size: 0.82rem;
}
.cr-page-num { font-weight: 700; color: var(--violet); }
.cr-page-cnt { color: var(--text-3); font-size: 0.76rem; }
.cr-detail-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--border); }
.btn-cr-export { padding: 8px 18px; border: 1.5px solid var(--cyan); border-radius: 99px; background: rgba(6,214,224,0.06); color: #05aab3; font-family:'Manrope',sans-serif; font-size:0.82rem; font-weight:700; cursor:pointer; transition: all 0.15s; }
.btn-cr-export:hover { background: rgba(6,214,224,0.15); }
.btn-cr-del { padding: 8px 18px; border: 1.5px solid rgba(241,91,181,0.4); border-radius: 99px; background: transparent; color: var(--pink); font-family:'Manrope',sans-serif; font-size:0.82rem; font-weight:700; cursor:pointer; transition: all 0.15s; }
.btn-cr-del:hover { background: rgba(241,91,181,0.08); border-color: var(--pink); }
.btn-cr-end { padding: 8px 18px; border: none; border-radius: 99px; background: #EF4444; color: #fff; font-family:'Manrope',sans-serif; font-size:0.82rem; font-weight:700; cursor:pointer; transition: opacity 0.15s; }
.btn-cr-end:hover { opacity: 0.85; }
/* Pagination */
.cr-pagination { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 24px; flex-wrap: wrap; }
.cr-page-btn {
min-width: 36px; height: 36px; padding: 0 12px; border: 1.5px solid var(--border);
border-radius: 10px; background: var(--surface); font-family:'Manrope',sans-serif;
font-size:0.85rem; font-weight:700; color:var(--text-2); cursor:pointer; transition:all 0.14s;
display:flex; align-items:center; justify-content:center;
}
.cr-page-btn:hover:not(:disabled) { border-color:var(--violet); color:var(--violet); }
.cr-page-btn.active { background:var(--violet); border-color:var(--violet); color:#fff; }
.cr-page-btn:disabled { opacity:0.4; cursor:default; }
.cr-page-info { font-size:0.82rem; color:var(--text-3); font-weight:600; }
/* toolbar for classroom history */
.cr-hist-toolbar { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
.cr-hist-search { flex: 1; min-width: 180px; padding: 9px 14px; border: 1.5px solid var(--border-h); border-radius: var(--r-pill); font-family:'Manrope',sans-serif; font-size:0.88rem; background:var(--surface); color:var(--text); }
.cr-hist-search:focus { outline:none; border-color:var(--violet); }
.cr-hist-count { font-size:0.85rem; color:var(--text-3); font-weight:600; white-space:nowrap; }
/* ── Avatar moderation ── */
.av-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); gap: 16px; }
.av-card {
background: var(--surface); border: 1.5px solid var(--border-h);
border-radius: 14px; padding: 16px; display: flex; flex-direction: column; gap: 12px;
}
.av-card-top { display: flex; align-items: center; gap: 12px; }
.av-imgs { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
.av-img-wrap { text-align: center; }
.av-img-wrap span { font-size: 0.62rem; color: var(--text-3); font-weight: 600; display: block; margin-bottom: 4px; }
.av-img {
width: 64px; height: 64px; border-radius: 50%; object-fit: cover;
border: 2px solid var(--border-h); background: var(--surface-2);
display: flex; align-items: center; justify-content: center;
font-family: 'Unbounded', sans-serif; font-size: 1rem; font-weight: 800;
color: var(--text-2); overflow: hidden;
}
.av-img img { width: 100%; height: 100%; object-fit: cover; }
.av-arrow { color: var(--text-3); flex-shrink: 0; }
.av-user-name { font-size: 0.82rem; font-weight: 700; color: var(--text); }
.av-date { font-size: 0.7rem; color: var(--text-3); margin-top: 2px; }
.av-actions { display: flex; gap: 8px; }
.av-approve { flex: 1; padding: 7px; border-radius: 8px; border: none; cursor: pointer; font-size: 0.8rem; font-weight: 700; background: rgba(6,214,96,0.12); color: #06d660; transition: background .15s; }
.av-approve:hover { background: rgba(6,214,96,0.22); }
.av-reject { flex: 1; padding: 7px; border-radius: 8px; border: none; cursor: pointer; font-size: 0.8rem; font-weight: 700; background: rgba(241,91,181,0.12); color: #F15BB5; transition: background .15s; }
.av-reject:hover { background: rgba(241,91,181,0.22); }
.av-empty { text-align: center; padding: 60px 0; color: var(--text-3); font-size: 0.85rem; }
.admin-badge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 18px; height: 18px; border-radius: 99px; padding: 0 5px;
background: #F15BB5; color: #fff; font-size: 0.65rem; font-weight: 800;
margin-left: 4px; vertical-align: middle;
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<div class="sb-content">
<div class="container">
<div class="page-title">Панель управления</div>
<div class="page-sub" id="page-sub">Загрузка…</div>
<div class="admin-layout">
<nav class="admin-nav" id="admin-nav">
<div class="admin-nav-group" data-ng="analytics">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('analytics')">
<span>Аналитика</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item active" data-tab="overview" onclick="switchTab(this)">
<i data-lucide="layout-dashboard" style="width:15px;height:15px"></i> Обзор
</button>
<button class="admin-nav-item" data-tab="stats" onclick="switchTab(this)">
<i data-lucide="bar-chart-2" style="width:15px;height:15px"></i> Статистика
</button>
<button class="admin-nav-item" data-tab="sessions" onclick="switchTab(this)">
<i data-lucide="clock" style="width:15px;height:15px"></i> История сессий
</button>
<button class="admin-nav-item" data-tab="classroom" onclick="switchTab(this)">
<i data-lucide="video" style="width:15px;height:15px"></i> Онлайн-уроки
</button>
</div>
</div>
<div class="admin-nav-group" data-ng="content">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('content')">
<span>Контент</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item" data-tab="questions" onclick="switchTab(this)">
<i data-lucide="help-circle" style="width:15px;height:15px"></i> Вопросы
</button>
<button class="admin-nav-item" data-tab="tests" onclick="switchTab(this)">
<i data-lucide="clipboard-list" style="width:15px;height:15px"></i> Тесты
</button>
<button class="admin-nav-item" data-tab="assignments" onclick="switchTab(this)">
<i data-lucide="file-check" style="width:15px;height:15px"></i> Задания
</button>
<button class="admin-nav-item" data-tab="subjects" onclick="switchTab(this)" id="btn-tab-subjects" style="display:none">
<i data-lucide="book-marked" style="width:15px;height:15px"></i> Доступные тесты
</button>
<button class="admin-nav-item" data-tab="tpl" onclick="switchTab(this)" id="btn-tab-tpl" style="display:none">
<i data-lucide="copy" style="width:15px;height:15px"></i> Шаблоны
</button>
<button class="admin-nav-item" data-tab="topics" onclick="switchTab(this)">
<i data-lucide="list-tree" style="width:15px;height:15px"></i> Темы
</button>
<button class="admin-nav-item" data-tab="access" onclick="switchTab(this)">
<i data-lucide="book-lock" style="width:15px;height:15px"></i> Доступ к учебникам
</button>
</div>
</div>
<div class="admin-nav-group" data-ng="users">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('users')">
<span>Пользователи</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item" data-tab="users" onclick="switchTab(this)">
<i data-lucide="users" style="width:15px;height:15px"></i> Пользователи
</button>
<button class="admin-nav-item" data-tab="permissions" onclick="switchTab(this)" id="btn-tab-permissions" style="display:none">
<i data-lucide="shield" style="width:15px;height:15px"></i> Права доступа
</button>
<button class="admin-nav-item" data-tab="avatars" onclick="switchTab(this);loadAvatarRequests()">
<i data-lucide="image" style="width:15px;height:15px"></i> Аватары
<span class="admin-badge" id="av-badge" style="display:none"></span>
</button>
<button class="admin-nav-item" data-tab="sublog" onclick="switchTab(this)">
<i data-lucide="file-x" style="width:15px;height:15px"></i> Журнал работ
</button>
<button class="admin-nav-item" data-tab="broadcast" onclick="switchTab(this)">
<i data-lucide="megaphone" style="width:15px;height:15px"></i> Рассылка
</button>
</div>
</div>
<div class="admin-nav-group" data-ng="system" id="admin-nav-system-group" style="display:none">
<button class="admin-nav-ghdr" onclick="toggleAdminGroup('system')">
<span>Система</span>
<svg class="adm-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
<div class="admin-nav-body">
<button class="admin-nav-item" data-tab="shop" onclick="switchTab(this)" id="btn-tab-shop" style="display:none">
<i data-lucide="shopping-bag" style="width:15px;height:15px"></i> Магазин
</button>
<button class="admin-nav-item" data-tab="gam" onclick="switchTab(this)" id="btn-tab-gam" style="display:none">
<i data-lucide="trophy" style="width:15px;height:15px"></i> Геймификация
</button>
<button class="admin-nav-item" data-tab="sims" onclick="switchTab(this)" id="btn-tab-sims" style="display:none">
<i data-lucide="atom" style="width:15px;height:15px"></i> Симуляции
</button>
<button class="admin-nav-item" data-tab="games" onclick="switchTab(this)" id="btn-tab-games" style="display:none">
<i data-lucide="gamepad-2" style="width:15px;height:15px"></i> Игры
</button>
<button class="admin-nav-item" data-tab="audit" onclick="switchTab(this)">
<i data-lucide="scroll-text" style="width:15px;height:15px"></i> Аудит-лог
</button>
<button class="admin-nav-item" data-tab="errors" onclick="switchTab(this)">
<i data-lucide="bug" style="width:15px;height:15px"></i> Ошибки
</button>
<button class="admin-nav-item" data-tab="health" onclick="switchTab(this)">
<i data-lucide="activity" style="width:15px;height:15px"></i> Здоровье
</button>
</div>
</div>
</nav>
<div class="admin-main">
<!-- ── Обзор (Phase 3) ── -->
<div class="tab-pane active" id="tab-overview">
<div id="overview-content"><div class="spinner"></div></div>
</div>
<!-- ── Статистика ── -->
<div class="tab-pane" id="tab-stats">
<div class="section-title">Общая статистика</div>
<div class="stats-grid" id="stats-grid"><div class="spinner"></div></div>
<div class="section-title">По предметам</div>
<div class="subj-stats" id="subj-stats"><div class="spinner"></div></div>
</div>
<!-- ── Вопросы ── -->
<div class="tab-pane" id="tab-questions">
<div class="t-toolbar">
<select class="t-select" id="q-subject" onchange="onQSubjectChange()">
<option value="">Все предметы</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
<select class="t-select" id="q-topic" onchange="loadQuestions()">
<option value="">Все темы</option>
</select>
<select class="t-select" id="q-sort" onchange="loadQuestions()">
<option value="date_desc">Новые сначала</option>
<option value="date_asc">Старые сначала</option>
<option value="diff_asc">Сложность <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></option>
<option value="diff_desc">Сложность <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></option>
</select>
<input class="t-input" id="q-search" type="text" placeholder="Поиск по тексту…" oninput="renderQuestions()" />
<span class="t-count" id="q-count"></span>
<button class="btn-add" onclick="openQModal()">+ Добавить вопрос</button>
<button class="btn-add" style="background:var(--grad-1)" onclick="document.getElementById('csv-file-input').click()"><i data-lucide="upload" style="width:13px;height:13px;vertical-align:-2px"></i> Импорт CSV</button>
<input type="file" id="csv-file-input" accept=".csv,.txt" style="display:none" onchange="importCSVFile(this)" />
<a id="csv-template-link" href="#" onclick="downloadCSVTemplate(event)" style="font-size:0.78rem;color:var(--violet);text-decoration:none;margin-left:4px"><i data-lucide="file-text" style="width:12px;height:12px;vertical-align:-2px"></i> Шаблон</a>
</div>
<div id="q-list-wrap"><div class="empty">Выберите предмет или загрузите все вопросы</div></div>
</div>
<!-- ── Тесты (шаблоны) ── -->
<div class="tab-pane" id="tab-tests">
<div class="t-toolbar">
<select class="t-select" id="tst-subj" onchange="loadTests()">
<option value="">Все предметы</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
<input class="t-input" id="tst-search" type="text" placeholder="Поиск по названию…" oninput="renderTests()" />
<span class="t-count" id="tst-count"></span>
<button class="btn-add" onclick="openTstModal()">+ Создать тест</button>
</div>
<div id="tst-list-wrap"><div class="empty">Загрузка…</div></div>
</div>
<!-- ── Задания ── -->
<div class="tab-pane" id="tab-assignments">
<div class="t-toolbar" style="margin-bottom:12px">
<input class="t-input" id="a-search" type="text" placeholder="Поиск по названию…" oninput="renderAssignments()" style="flex:1;max-width:280px" />
<select class="t-select" id="a-subject" onchange="renderAssignments()">
<option value="">Все предметы</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
<span class="t-count" id="a-count" style="margin-left:0"></span>
</div>
<div class="a-summary" id="a-summary"></div>
<div class="a-filter-row">
<button class="a-f-chip active" onclick="setAFilter('all')">Все</button>
<button class="a-f-chip" onclick="setAFilter('active')">Активные</button>
<button class="a-f-chip" onclick="setAFilter('overdue')">Просрочены</button>
<button class="a-f-chip" onclick="setAFilter('done')">Завершены</button>
<div class="a-filter-sep"></div>
<select class="a-sort-sel" id="a-sort" onchange="renderAssignments()">
<option value="date">По дате <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></option>
<option value="deadline">По дедлайну <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></option>
<option value="progress_asc">Прогресс <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg></option>
<option value="progress_desc">Прогресс <svg class="ic" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></svg></option>
</select>
<a class="btn-add" href="/classes" style="text-decoration:none;margin-left:auto">Перейти в классы <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></a>
</div>
<div class="a-rows" id="a-body"><div class="spinner"></div></div>
</div>
<!-- ── Доступные тесты (настройки предметов) ── -->
<div class="tab-pane" id="tab-subjects">
<div class="section-title" style="margin-bottom:6px">Настройка доступных тестов</div>
<div class="perm-desc" style="margin-bottom:20px">Настройте что увидят ученики на дашборде: режим, количество вопросов, источник.</div>
<div class="sc-list" id="subj-config-list"></div>
</div>
<!-- ── Пользователи ── -->
<div class="tab-pane" id="tab-users">
<div class="section-title">Пользователи</div>
<div class="table-wrap">
<table>
<thead><tr><th>Пользователь</th><th>Роль</th><th>Тестов</th><th>Средний %</th><th>Регистрация</th><th>Посл. вход</th><th></th></tr></thead>
<tbody id="users-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div id="users-pagination" class="pgn-bar" style="display:none"></div>
<!-- Phase 6: legacy .user-panel overlay removed; deep page renders into #tab-user-detail above. -->
</div>
<!-- ── Deep page: user detail (#users/:id) — populated by user-detail.js ── -->
<div class="tab-pane" id="tab-user-detail">
<div id="user-detail-content"><div class="spinner"></div></div>
</div>
<!-- ── Deep page: session detail (#sessions/:id) — populated by session-detail.js ── -->
<div class="tab-pane" id="tab-session-detail">
<div id="session-detail-content"><div class="spinner"></div></div>
</div>
<!-- ── Тесты (сессии) ── -->
<div class="tab-pane" id="tab-sessions">
<div class="t-toolbar">
<select class="t-select" id="t-subject" onchange="loadSessions()">
<option value="">Все предметы</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
<select class="t-select" id="t-mode" onchange="renderSessions()">
<option value="">Все режимы</option>
<option value="exam">Экзамен</option>
<option value="practice">Тренировка</option>
<option value="repeat">Обычный</option>
<option value="ct">ЦТ/ЦЭ</option>
</select>
<input class="t-input" id="t-search" type="text" placeholder="Поиск по имени…" oninput="renderSessions()" />
<span class="t-count" id="t-count"></span>
</div>
<div id="t-body"><div class="spinner"></div></div>
</div>
<!-- ── Онлайн-уроки ── -->
<div class="tab-pane" id="tab-classroom">
<!-- Module master toggle -->
<div style="display:flex;align-items:center;justify-content:space-between;background:var(--surface);border:1.5px solid var(--border-h);border-radius:var(--r-lg);padding:20px 24px;margin-bottom:32px">
<div>
<div style="font-size:0.97rem;font-weight:700;margin-bottom:4px">Модуль онлайн-уроков</div>
<div class="perm-desc" style="margin:0">Если отключить, учителя не смогут создавать новые уроки. Уже активные сессии продолжат работу до завершения.</div>
</div>
<label class="perm-toggle" id="cr-master-lbl" title="Включить / выключить модуль" style="margin-left:24px;flex-shrink:0">
<input type="checkbox" id="cr-master-chk" onchange="crMasterToggle(this.checked)" checked />
<span class="perm-track"></span>
<span class="perm-thumb"></span>
</label>
</div>
<!-- Active sessions -->
<div class="cr-admin-section">
<div class="cr-admin-section-title">
<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
Активные уроки
<span id="cr-live-refresh-btn" style="font-size:0.76rem;font-weight:600;color:var(--violet);cursor:pointer;text-transform:none;letter-spacing:0;margin-left:-4px" onclick="loadCrActiveSessions()">
<svg class="ic" viewBox="0 0 24 24" style="width:12px;height:12px;vertical-align:-2px"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>
Обновить
</span>
</div>
<div id="cr-live-list"><div class="spinner"></div></div>
</div>
<!-- Session history -->
<div class="cr-admin-section">
<div class="cr-admin-section-title">
<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
История уроков
</div>
<div class="cr-hist-toolbar">
<input class="cr-hist-search" id="cr-hist-q" type="text" placeholder="Поиск по теме или учителю…" oninput="crHistDebounce()">
<span class="cr-hist-count" id="cr-hist-count"></span>
</div>
<div id="cr-hist-list"><div class="spinner"></div></div>
<div id="cr-hist-pagination"></div>
</div>
</div>
<!-- ── Права доступа ── -->
<!-- ── Avatars moderation tab ── -->
<div class="tab-pane" id="tab-avatars">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:20px;flex-wrap:wrap;gap:10px">
<div class="section-title" style="margin:0">Аватары на модерации</div>
<button class="btn-outline" onclick="loadAvatarRequests()" style="padding:6px 14px;font-size:0.8rem">
<i data-lucide="refresh-cw" style="width:13px;height:13px;vertical-align:-2px"></i> Обновить
</button>
</div>
<div id="av-list">
<div style="color:var(--muted);text-align:center;padding:40px 0;font-size:0.85rem">Загрузка...</div>
</div>
</div>
<div class="tab-pane" id="tab-permissions">
<div class="perm-header">
<div class="section-title" style="margin:0">Права доступа по ролям</div>
<p style="color:var(--muted);font-size:13px;margin:4px 0 0">Настройте, что могут делать учителя и ученики. Администраторы имеют все права всегда.</p>
</div>
<div class="perm-search-wrap" style="margin: 16px 0 20px">
<input type="search" id="perm-search-input" placeholder="Поиск по правам..."
style="width:100%;max-width:420px;padding:9px 14px;border:1.5px solid var(--border);border-radius:10px;font-family:inherit;font-size:0.9rem"
oninput="filterPermissions(this.value)">
</div>
<div class="perm-role-block">
<div class="perm-role-title">
<span class="badge badge-warn" style="font-size:13px;padding:4px 12px">Учитель</span>
</div>
<div class="perm-grid" id="perm-teacher"></div>
</div>
<div class="perm-role-block">
<div class="perm-role-title">
<span class="badge badge-info" style="font-size:13px;padding:4px 12px">Ученик</span>
</div>
<div class="perm-grid" id="perm-student"></div>
</div>
</div>
<!-- ── Магазин ── -->
<div class="tab-pane" id="tab-shop">
<div class="section-title">Магазин</div>
<div class="stats-grid" id="shop-stats-grid"><div class="spinner"></div></div>
<div class="section-title" style="margin-top:32px">Товары</div>
<div style="margin-bottom:14px">
<button class="btn-add" onclick="shopAdminCreateItem()">+ Добавить товар</button>
</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>ID</th><th>Название</th><th>Тип</th><th>Цена</th><th>Продано</th><th>Активен</th><th>Действия</th>
</tr></thead>
<tbody id="shop-items-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div class="adm-panel" id="shop-item-form" style="display:none">
<div class="adm-panel-title" id="shop-form-title">Добавить товар</div>
<div class="adm-form-row">
<div class="adm-form-group" style="flex:1">
<label>Название</label>
<input type="text" id="shop-f-name" placeholder="Название товара" />
</div>
<div class="adm-form-group" style="flex:1">
<label>Тип</label>
<select id="shop-f-type">
<option value="frame">Рамка</option>
<option value="title">Титул</option>
<option value="theme">Тема</option>
<option value="effect">Эффект</option>
</select>
</div>
<div class="adm-form-group" style="width:100px">
<label>Цена</label>
<input type="number" id="shop-f-price" min="0" value="100" />
</div>
</div>
<div class="adm-form-row">
<div class="adm-form-group" style="flex:1">
<label>Описание</label>
<textarea id="shop-f-desc" rows="2" placeholder="Описание товара"></textarea>
</div>
</div>
<div class="adm-form-row">
<div class="adm-form-group" style="flex:1">
<label>Иконка (emoji/код)</label>
<input type="text" id="shop-f-icon" placeholder="SVG-код или эмодзи" />
</div>
<div class="adm-form-group" style="flex:1">
<label>Данные (JSON)</label>
<input type="text" id="shop-f-data" placeholder='{"key":"value"}' />
</div>
<div class="adm-form-group">
<label>Активен</label>
<label class="adm-toggle">
<input type="checkbox" id="shop-f-active" checked />
<span class="track"></span><span class="thumb"></span>
</label>
</div>
</div>
<div style="display:flex;gap:10px">
<button class="adm-btn adm-btn-primary" onclick="shopAdminSaveItem()">Сохранить</button>
<button class="adm-btn" style="background:var(--border-h);color:var(--text-3)" onclick="shopAdminCancelForm()">Отмена</button>
</div>
</div>
<div class="section-title" style="margin-top:32px">Начислить монеты</div>
<div class="adm-panel">
<div class="adm-form-row">
<div class="adm-form-group adm-user-search" style="flex:1">
<label>Пользователь</label>
<input type="text" id="shop-award-user" placeholder="Поиск по имени…" autocomplete="off" oninput="shopSearchUser(this.value)" />
<div class="us-results" id="shop-award-results"></div>
<input type="hidden" id="shop-award-uid" />
</div>
<div class="adm-form-group" style="width:120px">
<label>Кол-во монет</label>
<input type="number" id="shop-award-amount" min="1" value="10" />
</div>
<div class="adm-form-group" style="flex:1">
<label>Причина</label>
<input type="text" id="shop-award-reason" placeholder="За активность" />
</div>
<button class="adm-btn adm-btn-primary" onclick="shopAdminAwardCoins()" style="align-self:flex-end">Начислить</button>
</div>
</div>
</div>
<!-- ── Геймификация ── -->
<div class="tab-pane" id="tab-gam">
<div class="section-title">Геймификация</div>
<div class="stats-grid" id="gam-stats-grid"><div class="spinner"></div></div>
<div class="section-title" style="margin-top:32px">Топ-10 по XP</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>#</th><th>Имя</th><th>XP</th><th>Уровень</th><th>Монеты</th>
</tr></thead>
<tbody id="gam-top-body"><tr><td colspan="5"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div class="section-title" style="margin-top:32px">Покупки в магазине</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>Время</th><th>Пользователь</th><th>Предмет</th><th>Тип</th><th>Цена</th>
</tr></thead>
<tbody id="gam-purchases-body"><tr><td colspan="5"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div class="section-title" style="margin-top:32px">Последние начисления XP</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>Время</th><th>Имя</th><th>XP</th><th>Причина</th>
</tr></thead>
<tbody id="gam-log-body"><tr><td colspan="4"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div class="section-title" style="margin-top:32px">Начислить XP / Монеты</div>
<div class="adm-panel">
<div class="adm-form-row">
<div class="adm-form-group adm-user-search" style="flex:1">
<label>Пользователь</label>
<input type="text" id="gam-award-user" placeholder="Поиск по имени…" autocomplete="off" oninput="gamSearchUser(this.value,'gam-award')" />
<div class="us-results" id="gam-award-results"></div>
<input type="hidden" id="gam-award-uid" />
</div>
<div class="adm-form-group" style="width:100px">
<label>XP</label>
<input type="number" id="gam-award-xp" min="0" value="10" />
</div>
<div class="adm-form-group" style="width:100px">
<label>Монеты</label>
<input type="number" id="gam-award-coins" min="0" value="0" />
</div>
<div class="adm-form-group" style="flex:1">
<label>Причина</label>
<input type="text" id="gam-award-reason" placeholder="За участие" />
</div>
<button class="adm-btn adm-btn-primary" onclick="gamAdminAward()" style="align-self:flex-end">Начислить</button>
</div>
</div>
<div class="section-title" style="margin-top:32px">Сбросить прогресс пользователя</div>
<div class="adm-panel">
<div class="adm-form-row">
<div class="adm-form-group adm-user-search" style="flex:1">
<label>Пользователь</label>
<input type="text" id="gam-reset-user" placeholder="Поиск по имени…" autocomplete="off" oninput="gamSearchUser(this.value,'gam-reset')" />
<div class="us-results" id="gam-reset-results"></div>
<input type="hidden" id="gam-reset-uid" />
</div>
<button class="adm-btn adm-btn-danger" onclick="gamAdminReset()" style="align-self:flex-end">Сбросить прогресс</button>
</div>
</div>
</div>
<!-- ── Шаблоны ── -->
<div class="tab-pane" id="tab-tpl">
<div class="section-title">Шаблоны курсов</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>ID</th><th>Название</th><th>Предмет</th><th>Категория</th><th>Автор</th><th>Публичный</th><th>Действия</th>
</tr></thead>
<tbody id="tpl-course-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
<div class="section-title" style="margin-top:32px">Шаблоны уроков</div>
<div class="table-wrap">
<table>
<thead><tr>
<th>ID</th><th>Название</th><th>Предмет</th><th>Категория</th><th>Автор</th><th>Публичный</th><th>Действия</th>
</tr></thead>
<tbody id="tpl-lesson-body"><tr><td colspan="7"><div class="spinner"></div></td></tr></tbody>
</table>
</div>
</div>
<div class="tab-pane" id="tab-sims">
<div class="section-title">Управление симуляциями</div>
<!-- Master toggle -->
<div class="perm-role-block" style="margin-bottom:24px">
<div style="display:flex;align-items:center;justify-content:space-between;gap:16px">
<div>
<div class="perm-label" style="font-size:15px">Модуль симуляций</div>
<div class="perm-desc">Отключить полностью — страница «Лаборатория» станет недоступна для всех пользователей</div>
</div>
<label class="perm-toggle" id="sims-master-lbl" title="Включить / выключить весь модуль">
<input type="checkbox" id="sims-master-chk" onchange="simsMasterToggle(this.checked)" />
<span class="perm-track"></span>
<span class="perm-thumb"></span>
</label>
</div>
</div>
<!-- Per-sim grid -->
<div class="perm-desc" style="margin-bottom:16px">Отключённые симуляции не отображаются в лаборатории. Симуляции в статусе «скоро» не показываются независимо от этой настройки.</div>
<div class="perm-grid" id="sims-grid">
<div style="color:var(--muted);font-size:0.84rem">Загрузка…</div>
</div>
</div>
<!-- ── Игры ── -->
<div class="tab-pane" id="tab-games">
<div class="section-title">Управление играми</div>
<div class="perm-desc" style="margin-bottom:20px">Отключённые игры скрываются из бокового меню и становятся недоступны для всех пользователей.</div>
<div class="perm-grid" id="games-features-grid">
<div style="color:var(--muted);font-size:0.84rem">Загрузка…</div>
</div>
<div class="section-title" style="margin-top:32px">Модули для «Своб. ученика»</div>
<div class="perm-desc" style="margin-bottom:20px">Отключённые модули скрываются только для пользователей с ролью <b>Своб. ученик</b>. Глобальные настройки выше применяются поверх этих.</div>
<div class="perm-grid" id="fs-features-grid">
<div style="color:var(--muted);font-size:0.84rem">Загрузка…</div>
</div>
</div>
<!-- ── Журнал удалённых работ ── -->
<div class="tab-pane" id="tab-sublog">
<div class="section-title">Журнал удалённых работ</div>
<div class="perm-desc" style="margin-bottom:16px">Все удалённые работы учеников записываются сюда. Данные сохраняются даже после удаления файлов.</div>
<div class="sl-filter-row">
<select class="sl-filter-select" id="sublog-class-filter" onchange="loadSubmissionLog()">
<option value="">Все классы</option>
</select>
<span class="sl-count" id="sublog-count"></span>
<button class="btn-del-q" id="btn-clear-sublog" style="display:none;margin-left:auto" onclick="clearSubmissionLog()">
<i data-lucide="trash-2" style="width:13px;height:13px;vertical-align:-2px"></i> Очистить журнал
</button>
</div>
<div id="sublog-list"></div>
</div>
<!-- ── Темы ── -->
<div class="tab-pane" id="tab-topics">
<div class="section-title">Управление темами</div>
<div style="display:flex;gap:12px;margin-bottom:18px;flex-wrap:wrap;align-items:center">
<select class="t-select" id="topics-subj-filter" onchange="loadTopics()" style="max-width:220px"></select>
<button class="adm-btn adm-btn-primary adm-btn-small" onclick="showAddTopic()">+ Добавить тему</button>
<span class="sl-count" id="topics-count"></span>
</div>
<div id="topics-add-row" style="display:none;margin-bottom:16px">
<div class="adm-panel" style="padding:16px 20px">
<div class="adm-form-row" style="margin:0">
<div class="adm-form-group" style="flex:1"><label>Название</label><input type="text" id="topics-new-name" placeholder="Название темы" /></div>
<button class="adm-btn adm-btn-primary adm-btn-small" onclick="createTopic()" style="align-self:flex-end">Добавить</button>
<button class="adm-btn adm-btn-small" style="background:var(--border-h);color:var(--text-3);align-self:flex-end" onclick="document.getElementById('topics-add-row').style.display='none'">Отмена</button>
</div>
</div>
</div>
<div id="topics-list"></div>
</div>
<!-- ── Доступ к учебникам / экзаменам ── -->
<div class="tab-pane" id="tab-access">
<div class="section-title">Доступ к учебникам и экзаменам</div>
<p style="color:var(--muted);font-size:13px;margin:4px 0 16px;max-width:720px">
По умолчанию доступ <b>закрыт</b>. Откройте учебник или экзамен-модуль нужным классам.
Внутри класса можно сделать точечное исключение для отдельного ученика —
индивидуальное правило важнее правила класса.
</p>
<div class="acc-layout" style="display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap">
<div class="acc-list adm-panel" style="flex:0 0 280px;max-width:320px;padding:10px">
<div class="acc-list-head" style="font-weight:600;font-size:13px;color:var(--text-3);padding:6px 8px">Учебники</div>
<div id="acc-textbooks"></div>
<div class="acc-list-head" style="font-weight:600;font-size:13px;color:var(--text-3);padding:12px 8px 6px">Экзамены</div>
<div id="acc-exams"></div>
</div>
<div class="acc-detail adm-panel" style="flex:1;min-width:340px;padding:18px">
<div id="acc-detail-empty" style="color:var(--muted);font-size:14px">
Выберите учебник или экзамен слева, чтобы настроить доступ.
</div>
<div id="acc-detail" style="display:none"></div>
</div>
</div>
</div>
<!-- ── Рассылка ── -->
<div class="tab-pane" id="tab-broadcast">
<div class="section-title">Рассылка уведомлений</div>
<div class="adm-panel">
<div class="adm-form-row">
<div class="adm-form-group" style="flex:1">
<label>Сообщение</label>
<textarea id="bc-message" rows="3" placeholder="Текст уведомления (макс. 500 символов)" maxlength="500"></textarea>
</div>
</div>
<div class="adm-form-row">
<div class="adm-form-group" style="width:200px">
<label>Кому</label>
<select id="bc-role">
<option value="all">Всем пользователям</option>
<option value="student">Только ученикам</option>
<option value="teacher">Только учителям</option>
<option value="free_student">Свободным ученикам</option>
</select>
</div>
<div class="adm-form-group" style="flex:1">
<label>Ссылка (необязательно)</label>
<input type="text" id="bc-link" placeholder="/dashboard" />
</div>
<button class="adm-btn adm-btn-primary" onclick="sendBroadcast()" style="align-self:flex-end">Отправить</button>
</div>
<div id="bc-result" style="font-size:0.85rem;color:var(--green);margin-top:8px"></div>
</div>
</div>
<!-- ── Аудит-лог ── -->
<div class="tab-pane" id="tab-audit">
<div class="section-title" style="display:flex;align-items:center;justify-content:space-between">
Журнал действий администраторов
<button class="adm-btn adm-btn-danger adm-btn-small" onclick="clearAuditLog()">Очистить</button>
</div>
<div id="audit-list"></div>
</div>
<!-- ── Ошибки ── -->
<div class="tab-pane" id="tab-errors">
<div class="section-title" style="display:flex;align-items:center;justify-content:space-between">
Журнал ошибок сервера
<button class="adm-btn adm-btn-danger adm-btn-small" onclick="clearErrorLog()">Очистить</button>
</div>
<div id="errors-list"></div>
</div>
<!-- ── Здоровье системы ── -->
<div class="tab-pane" id="tab-health">
<div class="section-title">Здоровье системы</div>
<div id="health-content"></div>
</div>
</div><!-- /admin-main -->
</div><!-- /admin-layout -->
</div><!-- /container -->
<!-- ═══ MODAL ПРАВ ПОЛЬЗОВАТЕЛЯ ════════════════════════════════════ -->
<div class="q-modal" id="up-modal" onclick="if(event.target===this)closeUserPermsModal()">
<div class="q-modal-box" style="max-width:520px">
<div class="q-modal-title" id="up-modal-title">Права пользователя</div>
<p style="font-size:12.5px;color:var(--muted);margin:-8px 0 16px">Индивидуальные настройки переопределяют права роли для этого пользователя.</p>
<div id="up-modal-list" style="display:flex;flex-direction:column;gap:8px;max-height:420px;overflow-y:auto;padding-right:4px"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:20px;gap:12px">
<button class="btn-del-q" onclick="doResetAllUserPerms()" id="up-modal-reset-btn"
title="Удалить индивидуальные настройки — пользователь будет иметь права как у его роли">
<i data-lucide="rotate-ccw" style="width:13px;height:13px;vertical-align:-2px"></i> Сбросить всё по умолчанию
</button>
<button class="btn-close" onclick="closeUserPermsModal()">Закрыть</button>
</div>
</div>
</div>
<!-- ═══ MODAL РЕДАКТИРОВАНИЯ ПОЛЬЗОВАТЕЛЯ ═══════════════════════════ -->
<div class="q-modal" id="eu-modal" onclick="if(event.target===this)closeEditUserModal()">
<div class="q-modal-box" style="max-width:460px">
<div class="q-modal-title">Редактировать пользователя</div>
<div class="form-row">
<label class="form-label">Имя *</label>
<input type="text" class="form-ctrl" id="eu-name" placeholder="Имя пользователя" />
</div>
<div class="form-row">
<label class="form-label">Email *</label>
<input type="email" class="form-ctrl" id="eu-email" placeholder="email@example.com" />
</div>
<div class="form-row">
<label class="form-label">Новый пароль</label>
<input type="password" class="form-ctrl" id="eu-password" placeholder="Оставьте пустым, чтобы не менять (мин. 6 символов)" />
</div>
<div class="form-error" id="eu-error"></div>
<div class="modal-footer">
<button class="btn-cancel2" onclick="closeEditUserModal()">Отмена</button>
<button class="btn-save" id="eu-save" onclick="saveEditUser()">Сохранить</button>
</div>
</div>
</div>
<!-- ═══ MODAL РЕДАКТОРА ВОПРОСА ══════════════════════════════════════ -->
<div class="q-modal" id="q-modal" onclick="if(event.target===this)closeQModal()">
<div class="q-modal-box">
<div class="q-modal-title" id="q-modal-title">Добавить вопрос</div>
<div class="form-row">
<label class="form-label">Тип вопроса</label>
<div style="display:flex;gap:8px;flex-wrap:wrap" id="qf-type-btns">
<button type="button" class="type-btn active" data-type="single" onclick="setQType('single')"><i data-lucide="circle-dot" style="width:12px;height:12px;vertical-align:-1px"></i> Один ответ</button>
<button type="button" class="type-btn" data-type="multi" onclick="setQType('multi')"><i data-lucide="list-checks" style="width:12px;height:12px;vertical-align:-1px"></i> Несколько ответов</button>
<button type="button" class="type-btn" data-type="true_false" onclick="setQType('true_false')"><i data-lucide="check-circle-2" style="width:12px;height:12px;vertical-align:-1px"></i> Верно/Неверно</button>
<button type="button" class="type-btn" data-type="short_answer" onclick="setQType('short_answer')"><i data-lucide="pencil" style="width:12px;height:12px;vertical-align:-1px"></i> Краткий ответ</button>
<button type="button" class="type-btn" data-type="matching" onclick="setQType('matching')"><i data-lucide="git-compare" style="width:12px;height:12px;vertical-align:-1px"></i> Сопоставление</button>
</div>
</div>
<div class="form-row-3">
<div>
<label class="form-label">Предмет *</label>
<select class="form-ctrl" id="qf-subject" onchange="loadQModalTopics()">
<option value="">— выберите —</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
</div>
<div>
<label class="form-label">Тема</label>
<input type="text" class="form-ctrl" id="qf-topic-text" placeholder="Введите или выберите…" list="qf-topic-list" autocomplete="off" />
<datalist id="qf-topic-list"></datalist>
<div class="form-hint">Введите новую — создастся автоматически</div>
</div>
<div>
<label class="form-label">Сложность</label>
<select class="form-ctrl" id="qf-difficulty">
<option value="1">1 — Лёгкий</option>
<option value="2" selected>2 — Средний</option>
<option value="3">3 — Сложный</option>
</select>
</div>
</div>
<button type="button" class="fml-toggle" id="qf-fml-toggle" onclick="toggleFormulaBar()">
<svg class="fml-chev" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
<span>Вставить формулу</span>
</button>
<div class="formula-bar" id="formula-bar">
<span style="font-size:0.7rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em">Формулы:</span>
<button type="button" class="fml" onclick="ins('\\frac{a}{b}')" title="Дробь">ᵃ⁄ᵦ</button>
<button type="button" class="fml" onclick="ins('\\sqrt{x}')" title="Корень"></button>
<button type="button" class="fml" onclick="ins('x^{n}')" title="Степень">xⁿ</button>
<button type="button" class="fml" onclick="ins('x_{n}')" title="Индекс">xₙ</button>
<button type="button" class="fml" onclick="ins('\\pi')" title="Пи">π</button>
<button type="button" class="fml" onclick="ins('\\alpha')" title="Альфа">α</button>
<button type="button" class="fml" onclick="ins('\\beta')" title="Бета">β</button>
<button type="button" class="fml" onclick="ins('\\theta')" title="Тета">θ</button>
<button type="button" class="fml" onclick="ins('\\infty')" title="Бесконечность"></button>
<button type="button" class="fml" onclick="ins('\\leq')" title="≤"></button>
<button type="button" class="fml" onclick="ins('\\geq')" title="≥"></button>
<button type="button" class="fml" onclick="ins('\\neq')" title="≠"></button>
<button type="button" class="fml" onclick="ins('\\pm')" title="±">±</button>
<button type="button" class="fml" onclick="ins('\\cdot')" title="·">·</button>
<button type="button" class="fml" onclick="ins('\\log_{a}{x}')" title="Логарифм">log</button>
<button type="button" class="fml" onclick="ins('\\sin')" title="Синус">sin</button>
<button type="button" class="fml" onclick="ins('\\cos')" title="Косинус">cos</button>
<button type="button" class="fml" onclick="wrapMath()" title="Обернуть в \(...\)" style="font-weight:800">\( \)</button>
</div>
<div class="form-row">
<label class="form-label">Текст вопроса *</label>
<textarea class="form-ctrl" id="qf-text" rows="3" placeholder="Введите текст вопроса…" oninput="updateCharCounter(this, 'qf-text-cnt', 500)" maxlength="500"></textarea>
<div class="char-counter" id="qf-text-cnt">0 / 500</div>
</div>
<div class="q-preview-wrap hidden" id="q-preview-wrap">
<div style="font-size:0.7rem;font-weight:700;color:var(--text-3);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:6px">Предпросмотр</div>
<div class="q-preview-text" id="q-preview-text"></div>
</div>
<div class="opts-header" id="qf-opts-header">
<span class="opts-label">Варианты ответов — отметьте правильный <svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="8" fill="currentColor" stroke="none"/></svg></span>
</div>
<div class="opts-grid" id="qf-opts"></div>
<!-- short_answer input -->
<div id="qf-short-wrap" style="display:none;margin-bottom:18px">
<label class="form-label">Правильный ответ *</label>
<input type="text" class="form-ctrl" id="qf-correct-text" placeholder="Введите точный правильный ответ (регистр не важен)…" />
<div class="form-hint">Сравнение без учёта регистра и лишних пробелов</div>
</div>
<!-- matching pairs UI -->
<div id="qf-match-wrap" style="display:none;margin-bottom:18px">
<div style="display:grid;grid-template-columns:1fr 1fr auto;gap:8px;align-items:center;margin-bottom:8px">
<span class="form-label" style="margin:0">Левая часть</span>
<span class="form-label" style="margin:0">Правая часть (пара)</span>
<span></span>
</div>
<div id="qf-match-rows"></div>
<button type="button" class="btn-add-opt" onclick="addMatchPair()">+ Добавить пару</button>
</div>
<button type="button" class="btn-add-opt" id="btn-add-opt" onclick="addOpt()">+ Добавить вариант</button>
<div class="form-row">
<label class="form-label">Изображение к вопросу</label>
<div class="img-upload-row">
<button type="button" class="btn-img-upload" id="btn-img-upload" onclick="document.getElementById('qf-image-file').click()">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<span id="btn-img-upload-lbl">Загрузить</span>
</button>
<input type="file" id="qf-image-file" accept="image/*" style="display:none" onchange="handleImageFileSelect(this)">
<input type="text" class="form-ctrl" id="qf-image" placeholder="или вставьте URL" style="flex:1;min-width:0" />
</div>
<div id="qf-image-preview" style="display:none;margin-top:8px;position:relative;display:inline-block">
<img id="qf-image-img" style="max-width:100%;max-height:180px;border-radius:8px;border:1px solid var(--border);display:block" />
<button type="button" onclick="clearQuestionImage()" style="position:absolute;top:6px;right:6px;background:rgba(0,0,0,.6);color:#fff;border:none;border-radius:50%;width:26px;height:26px;cursor:pointer;font-size:15px;line-height:26px;text-align:center"><svg class="ic" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></button>
</div>
</div>
<div class="form-row">
<label class="form-label">Пояснение к правильному ответу</label>
<textarea class="form-ctrl" id="qf-explanation" rows="2" placeholder="Необязательно. Появится после ответа в режиме тренировки…"></textarea>
</div>
<div class="form-error" id="qf-error"></div>
<div class="modal-footer">
<button class="btn-cancel2" onclick="closeQModal()">Отмена</button>
<button class="btn-save" id="qf-save" onclick="saveQuestion()">Сохранить</button>
</div>
</div>
</div>
<!-- ═══ MODAL СОЗДАНИЯ ЗАДАНИЯ ═══════════════════════════════════════ -->
<div class="q-modal" id="ac-modal" onclick="if(event.target===this)closeCreateAModal()">
<div class="q-modal-box">
<div class="q-modal-title">Создать задание</div>
<div class="form-row">
<label class="form-label">Назначить</label>
<div class="src-toggle">
<button type="button" class="type-btn active" data-actgt="class" onclick="setAcTarget('class')"><i data-lucide="users" style="width:12px;height:12px;vertical-align:-1px"></i> Классу</button>
<button type="button" class="type-btn" data-actgt="user" onclick="setAcTarget('user')"><i data-lucide="user" style="width:12px;height:12px;vertical-align:-1px"></i> Ученику</button>
</div>
</div>
<div id="acf-class-field">
<div class="form-row">
<label class="form-label">Класс *</label>
<select class="form-ctrl" id="acf-class">
<option value="">— загрузка… —</option>
</select>
</div>
</div>
<div id="acf-user-field" style="display:none">
<div class="form-row">
<label class="form-label">Ученик *</label>
<div style="position:relative">
<input type="text" class="form-ctrl" id="acf-student-search" placeholder="Поиск по имени или email…" autocomplete="off"
oninput="filterAcStudents(this.value)" onfocus="openAcStudentDrop()" onblur="setTimeout(closeAcStudentDrop,180)" />
<div id="acf-student-drop" style="display:none;position:absolute;top:100%;left:0;right:0;background:#fff;border:1px solid #d1d5db;border-radius:6px;max-height:180px;overflow-y:auto;z-index:200;box-shadow:0 4px 12px rgba(0,0,0,.12)"></div>
</div>
<div id="acf-student-selected" style="display:none;margin-top:6px;padding:6px 10px;background:#f0fdf4;border-radius:6px;font-size:13px;color:#166534"></div>
</div>
</div>
<div class="form-row">
<label class="form-label">Название задания *</label>
<input type="text" class="form-ctrl" id="acf-title" placeholder="Например: Контрольная работа по биологии" />
</div>
<div class="form-row">
<label class="form-label">Источник</label>
<div class="src-toggle">
<button type="button" class="type-btn active" data-src="random" onclick="setAcSrc('random')"><i data-lucide="shuffle" style="width:12px;height:12px;vertical-align:-1px"></i> Случайные</button>
<button type="button" class="type-btn" data-src="test" onclick="setAcSrc('test')"><i data-lucide="clipboard-list" style="width:12px;height:12px;vertical-align:-1px"></i> Тест</button>
<button type="button" class="type-btn" data-src="file" onclick="setAcSrc('file')"><i data-lucide="paperclip" style="width:12px;height:12px;vertical-align:-1px"></i> Файл</button>
</div>
</div>
<!-- Random source fields -->
<div id="acf-random-fields">
<div class="form-row-3">
<div>
<label class="form-label">Предмет *</label>
<select class="form-ctrl" id="acf-subject">
<option value="">— выберите —</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
</div>
<div>
<label class="form-label">Режим</label>
<select class="form-ctrl" id="acf-mode">
<option value="exam">Экзамен (1 раз, таймер)</option>
<option value="repeat">Обычный (многократно)</option>
<option value="ct">ЦТ/ЦЭ (A+Б части)</option>
</select>
</div>
<div>
<label class="form-label">Кол-во вопросов</label>
<input type="number" class="form-ctrl" id="acf-count" min="5" max="100" value="25" />
</div>
</div>
</div>
<!-- Test source fields -->
<div id="acf-test-fields" style="display:none">
<div class="form-row-2">
<div>
<label class="form-label">Тест *</label>
<select class="form-ctrl" id="acf-test">
<option value="">— загрузка тестов… —</option>
</select>
<div class="form-hint">Вопросы берутся из теста в заданном порядке</div>
</div>
<div>
<label class="form-label">Режим</label>
<select class="form-ctrl" id="acf-mode-test">
<option value="exam">Экзамен (1 раз, таймер)</option>
<option value="repeat">Обычный (многократно)</option>
<option value="ct">ЦТ/ЦЭ (A+Б части)</option>
</select>
</div>
</div>
</div>
<!-- File source fields -->
<div id="acf-file-fields" style="display:none">
<div class="form-row">
<label class="form-label">Файл из библиотеки *</label>
<input type="text" class="form-ctrl" id="acf-file-search" placeholder="Поиск по названию…" oninput="filterAcFiles(this.value)" />
<div id="acf-file-list" style="max-height:180px;overflow-y:auto;border:1.5px solid rgba(15,23,42,0.15);border-radius:8px;margin-top:6px"></div>
<div id="acf-file-selected" style="display:none;margin-top:6px;font-size:0.82rem;color:var(--violet);font-weight:600"></div>
</div>
</div>
<div class="form-row">
<label class="form-label">Дедлайн (необязательно)</label>
<input type="date" class="form-ctrl" id="acf-deadline" style="max-width:220px" />
</div>
<div class="form-error" id="acf-error"></div>
<div class="modal-footer">
<button class="btn-cancel2" onclick="closeCreateAModal()">Отмена</button>
<button class="btn-save" id="acf-save" onclick="saveNewAssignment()">Создать</button>
</div>
</div>
</div>
<!-- ═══ MODAL РЕДАКТОРА ЗАДАНИЯ ══════════════════════════════════════ -->
<div class="q-modal" id="a-modal" onclick="if(event.target===this)closeAModal()">
<div class="q-modal-box">
<div class="q-modal-title" id="a-modal-title">Редактировать задание</div>
<div class="form-row">
<label class="form-label">Название задания *</label>
<input type="text" class="form-ctrl" id="af-title" placeholder="Например: Итоговый тест по биологии" />
</div>
<div class="form-row">
<label class="form-label">Источник вопросов</label>
<div class="src-toggle">
<button type="button" class="type-btn active" data-afsrc="random" onclick="setAfSrc('random')"><i data-lucide="shuffle" style="width:12px;height:12px;vertical-align:-1px"></i> Случайные из предмета</button>
<button type="button" class="type-btn" data-afsrc="test" onclick="setAfSrc('test')"><i data-lucide="clipboard-list" style="width:12px;height:12px;vertical-align:-1px"></i> Готовый тест</button>
</div>
</div>
<!-- Random source fields -->
<div id="af-random-fields">
<div class="form-row-2">
<div>
<label class="form-label">Предмет *</label>
<select class="form-ctrl" id="af-subject">
<option value="">— выберите —</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
</div>
<div>
<label class="form-label">Режим</label>
<select class="form-ctrl" id="af-mode">
<option value="exam">Экзамен (1 раз, таймер)</option>
<option value="repeat">Обычный (многократно)</option>
<option value="ct">ЦТ/ЦЭ (A+Б части)</option>
</select>
</div>
</div>
<div class="form-row">
<label class="form-label">Количество вопросов</label>
<input type="number" class="form-ctrl" id="af-count" min="5" max="100" value="25" style="max-width:160px" />
</div>
</div>
<!-- Test source fields -->
<div id="af-test-fields" style="display:none">
<div class="form-row-2">
<div>
<label class="form-label">Тест *</label>
<select class="form-ctrl" id="af-test">
<option value="">— загрузка… —</option>
</select>
<div class="form-hint">Вопросы берутся из теста в заданном порядке</div>
</div>
<div>
<label class="form-label">Режим</label>
<select class="form-ctrl" id="af-mode-test">
<option value="exam">Экзамен (1 раз, таймер)</option>
<option value="repeat">Обычный (многократно)</option>
<option value="ct">ЦТ/ЦЭ (A+Б части)</option>
</select>
</div>
</div>
</div>
<div class="form-row">
<label class="form-label">Дедлайн (необязательно)</label>
<input type="date" class="form-ctrl" id="af-deadline" style="max-width:220px" />
</div>
<div class="form-error" id="af-error"></div>
<div class="modal-footer">
<button class="btn-cancel2" onclick="closeAModal()">Отмена</button>
<button class="btn-save" id="af-save" onclick="saveAssignment()">Сохранить</button>
</div>
</div>
</div>
<!-- ═══ MODAL СОЗДАНИЯ/РЕДАКТИРОВАНИЯ ТЕСТА ════════════════════════ -->
<div class="q-modal" id="tst-modal" onclick="if(event.target===this)closeTstModal()">
<div class="q-modal-box" style="max-width:560px">
<div class="q-modal-title" id="tst-modal-title">Создать тест</div>
<div class="form-row-2">
<div>
<label class="form-label">Название *</label>
<input type="text" class="form-ctrl" id="tstf-title" placeholder="Название теста…" />
</div>
<div>
<label class="form-label">Предмет *</label>
<select class="form-ctrl" id="tstf-subject">
<option value="">— выберите —</option>
<option value="bio">Биология</option>
<option value="chem">Химия</option>
<option value="math">Математика</option>
<option value="phys">Физика</option>
</select>
</div>
</div>
<div class="form-row">
<label class="form-label">Описание (необязательно)</label>
<textarea class="form-ctrl" id="tstf-desc" rows="2" placeholder="Краткое описание теста…"></textarea>
</div>
<div class="form-row">
<label class="form-label">Лимит времени (мин)</label>
<input type="number" class="form-ctrl" id="tstf-time" min="1" max="600" placeholder="по умолчанию: кол-во вопросов × 1.5 мин" style="max-width:320px" />
<div class="form-hint">Оставьте пустым — время рассчитывается автоматически</div>
</div>
<div class="form-row">
<label class="form-label">После завершения</label>
<div style="display:flex;gap:8px;flex-wrap:wrap">
<button type="button" class="type-btn active" id="tstf-show-yes" onclick="setTstShowAnswers(true)"><i data-lucide="eye" style="width:12px;height:12px;vertical-align:-1px"></i> Показать ответы и решения</button>
<button type="button" class="type-btn" id="tstf-show-no" onclick="setTstShowAnswers(false)"><i data-lucide="eye-off" style="width:12px;height:12px;vertical-align:-1px"></i> Скрыть ответы</button>
</div>
<div class="form-hint">При «Скрыть» ученики видят только итоговый балл</div>
</div>
<div class="form-error" id="tstf-error"></div>
<div class="modal-footer">
<button class="btn-cancel2" onclick="closeTstModal()">Отмена</button>
<button class="btn-save" id="tstf-save" onclick="saveTst()">Сохранить</button>
</div>
</div>
</div>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/admin/router.js"></script>
<script src="/js/admin/_shared.js"></script>
<script src="/js/admin/sections/overview.js"></script>
<script src="/js/admin/sections/stats.js"></script>
<script src="/js/admin/sections/sublog.js"></script>
<script src="/js/admin/sections/sims.js"></script>
<script src="/js/admin/sections/games.js"></script>
<script src="/js/admin/sections/tpl.js"></script>
<script src="/js/admin/sections/subjects.js"></script>
<script src="/js/admin/sections/permissions.js"></script>
<script src="/js/admin/sections/shop.js"></script>
<script src="/js/admin/sections/gam.js"></script>
<script src="/js/admin/sections/assignments.js"></script>
<script src="/js/admin/sections/tests.js"></script>
<script src="/js/admin/sections/questions.js"></script>
<script src="/js/admin/sections/users.js"></script>
<script src="/js/admin/sections/sessions.js"></script>
<script src="/js/admin/sections/user-detail.js"></script>
<script src="/js/admin/sections/session-detail.js"></script>
<script src="/js/admin/sections/access.js"></script>
<script src="/js/admin/palette.js"></script>
<script src="/js/admin/admin.js"></script>
</div>
</div>
<script src="/js/notifications.js"></script>
<script src="/js/search.js"></script>
<script src="/js/mobile.js"></script>
</body>
</html>