# LearnSpace — Полный UX-аудит фронтенда

**Дата проверки:** 2026-03-16
**Проверено файлов:** 40
**Методология:** ручной просмотр каждого HTML-файла, оценка по 10 категориям

---

## Сводная таблица критических проблем

| # | Файл | Проблема | Серьёзность |
|---|------|----------|-------------|
| 1 | board.html | Кнопка «Перейти» ведёт на `/dashboard`, а не запускает тест | HIGH |
| 2 | login.html | После регистрации всегда редирект на `/dashboard` — учителя попадают не туда | HIGH |
| 3 | homework.html | `catch {}` — пустой обработчик ошибок, студент не знает о сбое | HIGH |
| 4 | test-result.html | Обе кнопки «К тестам» и «В кабинет» ведут на `/dashboard` — дублирование без смысла | HIGH |
| 5 | live-quiz.html | Кнопка «Завершить сессию» без подтверждения — деструктивное действие | HIGH |
| 6 | admin.html | Смена роли пользователя без подтверждения (inline select) | HIGH |
| 7 | board.html | Ссылка `href="/red-book.html"` — с расширением `.html`, все остальные роуты без расширения | MEDIUM |
| 8 | Все страницы | Analytics, Gradebook, Question Bank, Live Quiz, Lesson Editor — нет в сайдбаре | MEDIUM |
| 9 | profile.html | `overflow: hidden` на `body` — может сломаться при виртуальной клавиатуре на мобиле | MEDIUM |
| 10 | profile.html | Форма смены пароля без поля «текущий пароль» | MEDIUM |

---

## Детальный разбор по файлам

---

### login.html

#### [HIGH] Flow: редирект после регистрации всегда на `/dashboard`
Учителя и администраторы после создания аккаунта попадают на страницу студента. Им нужны `/classes` или `/admin`.
**Фикс:** после успешной регистрации получать роль из ответа API и редиректить по роли: `student → /dashboard`, `teacher → /classes`, `admin → /admin`.

#### [MEDIUM] Flow: нет выбора роли при регистрации
Непонятно, как учитель или администратор создаёт аккаунт. Нет поля «роль» в форме регистрации.
**Фикс:** добавить `<select>` с вариантами «Студент» / «Учитель». Роль «Администратор» создаётся только через admin-панель.

#### [MEDIUM] Accessibility: нет ссылки «Забыли пароль?»
Стандартный элемент auth-страниц полностью отсутствует.
**Фикс:** добавить ссылку под полем пароля на форме входа.

#### [LOW] Consistency: `showBtnSuccess` вставляет строку `'✓ Сохранено'` через конкатенацию текста
Использование символа `✓` в коде (эмодзи-подобный символ) противоречит правилу проекта «только SVG-иконки».
**Фикс:** заменить `'✓ '` на inline SVG с классом `.ic`.

---

### dashboard.html

#### [MEDIUM] Loading: у виджетов есть скелетоны, но нет fallback при сетевой ошибке
Если API возвращает 500 — скелетон не убирается и данные не появляются. Пользователь не понимает, что произошло.
**Фикс:** в каждом `catch` блоке заменять скелетон на rich-empty с текстом ошибки и кнопкой «Повторить».

#### [MEDIUM] Navigation: кнопки быстрых действий (`adm-actions`) видны для teacher/admin, но не имеют состояния загрузки
После нажатия на «Создать тест» или «Добавить класс» кнопка не блокируется — возможно двойное нажатие.
**Фикс:** `btn.disabled = true` сразу при клике.

#### [LOW] Feedback: таймер дедлайна предупреждает только за 2 минуты (`secondsLeft <= 120`)
Для длинных тестов (1 час+) единственное предупреждение придёт слишком поздно, если пользователь отвлёкся.
**Фикс:** добавить промежуточное предупреждение при 25% оставшегося времени и за 5 минут.

---

### board.html

#### [HIGH] Flow: кнопка «Перейти» на карточке задания ведёт на `/dashboard`, а не на тест
```html
<a href="/dashboard" class="btn-action">Перейти</a>
```
Студент не может начать задание прямо с борда — он попадает на главную страницу.
**Фикс:** ссылка должна вести на `/test-run?session=...&assignment=...` (с нужными параметрами задания).

#### [MEDIUM] Consistency: ссылка на Red Book в сайдбаре содержит `.html`-расширение
```html
<a href="/red-book.html" ...>
```
Все остальные ссылки: `/dashboard`, `/board`, `/classes` — без расширения.
**Фикс:** добавить Express-роут `/red-book` → отдавать `red-book.html`, изменить ссылку на `/red-book`. Это же исправление нужно во **всех** файлах с сайдбаром.

#### [MEDIUM] Navigation: студент без класса видит пустой бord без способа вступить по инвайт-коду
Пустое состояние есть, но нет поля ввода инвайт-кода.
**Фикс:** добавить в empty-state форму «Введи код класса» с полем и кнопкой.

#### [MEDIUM] Feedback: бейдж «LIVE» рендерится всегда, независимо от статуса
```html
<span class="live-badge">LIVE</span>
```
JS должен скрывать/показывать его по реальному статусу сессии.
**Фикс:** `liveBadge.style.display = session.is_live ? '' : 'none'`.

#### [LOW] Data integrity: реакции сохраняются только в `localStorage`
Данные `ls_reactions` пропадут на другом браузере/устройстве и не видны другим пользователям.
**Фикс:** сохранять реакцию через POST `/api/posts/:id/react`, localStorage использовать только как оптимистичный кэш.

---

### classes.html

#### [MEDIUM] Forms: кнопка «Создать класс» видна до выполнения JS role-check
В HTML `btn-new-cl` отрисовывается сразу. Студент, быстро открыв страницу, видит её до скрытия.
**Фикс:** добавить `style="display:none"` в HTML, показывать через JS только для teacher/admin.

#### [MEDIUM] Navigation: ссылка на Gradebook скрыта в маленькой иконке в заголовке сайдбара
Новый пользователь никогда не найдёт журнал оценок.
**Фикс:** вынести «Журнал» как отдельный пункт сайдбара (или добавить явную кнопку в шапку детали класса).

#### [MEDIUM] Confirmation: кнопка `btn-delete-class` — неясно, есть ли подтверждение
В HTML диалог подтверждения не виден. Если он реализован только в JS-коде в виде `confirm()` браузера — это слабое решение.
**Фикс:** использовать кастомный modal с явным текстом «Удалить класс X? Это действие необратимо», кнопками «Отмена» / «Удалить».

#### [LOW] Accessibility: все элементы управления классами доступны только после выбора класса из списка
Состояние «ничего не выбрано» показывает заглушку «Выберите класс» без инструкции о том, что нужно сделать слева.
**Фикс:** добавить стрелку-подсказку или анимацию, указывающую на список классов.

---

### admin.html

#### [HIGH] Confirmation: смена роли пользователя через inline `<select>` без подтверждения
```html
<select class="role-select">...</select>
```
Неосторожный клик меняет роль немедленно. Это деструктивное действие: учитель превращается в студента без возможности отмены.
**Фикс:** при `change` события показывать confirmation modal «Изменить роль пользователя X с Y на Z?», только после подтверждения отправлять PATCH-запрос.

#### [HIGH] Confirmation: кнопка `btn-del-q` (удалить вопрос) без видимого диалога подтверждения
Удаление вопроса из банка — необратимое действие.
**Фикс:** confirmation modal с предупреждением об необратимости.

#### [MEDIUM] Error handling: нет отображения ошибок при провале admin-операций
Если PATCH /api/users/:id/role вернул 500, пользователь не узнает об этом.
**Фикс:** показывать toast/alert с текстом ошибки из ответа API.

#### [LOW] UX: `max-height: 0` → `max-height: 4000px` в session drawer
Анимация открытия непредсказуема по скорости — CSS считает переход от 0 до 4000px, хотя реальная высота 200px.
**Фикс:** использовать JavaScript для установки точной `max-height: element.scrollHeight + 'px'`.

---

### test-run.html

#### [MEDIUM] Feedback: кнопка «Завершить тест» доступна с первого вопроса без минимального порога
Студент может нечаянно нажать и завершить тест, ответив на 0 вопросов.
**Фикс:** показывать предупреждение в confirmation modal: «Ты ответил на X из Y вопросов. Уверен, что хочешь завершить?»

#### [MEDIUM] Accessibility: `<img src="..." alt="">` — пустой атрибут alt у изображений в вопросах
Студенты с нарушениями зрения не получат описание изображения.
**Фикс:** заполнять `alt` из поля описания вопроса в БД (например, `alt="Изображение к вопросу: ..."`).

#### [MEDIUM] Loading: нет серверного сохранения прогресса теста в процессе
SessionStorage работает только в рамках одной вкладки. При сбое браузера прогресс теряется.
**Фикс:** периодически (каждые 30 сек) отправлять автосохранение через PATCH `/api/sessions/:id/autosave` с текущими ответами.

#### [LOW] Feedback: предупреждение таймера только за 120 секунд — мало для коротких тестов
**Фикс:** добавить предупреждение при достижении 25% оставшегося времени (независимо от абсолютного значения).

---

### test-result.html

#### [HIGH] Flow: обе кнопки «К тестам» и «В кабинет» ведут на `/dashboard`
```html
<a href="/dashboard" class="btn-primary">К тестам</a>
<a href="/dashboard" class="btn-ghost">В кабинет</a>
```
Дублирование кнопок с одинаковым назначением сбивает с толку.
**Фикс:** «К тестам» → `/board` (список заданий), «В кабинет» → `/dashboard`, добавить третью кнопку «Пройти ещё раз» → `/test-run?...` с теми же параметрами.

#### [MEDIUM] Navigation: нет возврата к заданию на борде, если тест был из assignment
Если студент пришёл с `/board?class=5`, после теста нет ссылки «Вернуться к классу».
**Фикс:** передавать `returnUrl` параметром и использовать его в кнопке «Назад».

---

### homework.html

#### [HIGH] Error handling: пустой `catch {}` при загрузке данных класса в `initStudent()`
```js
catch {}
```
Студент видит пустую форму без объяснения причины.
**Фикс:** заменить на `catch(e) { showError('Не удалось загрузить задания. ' + e.message); }`.

#### [HIGH] Forms: при нескольких классах у студента задание всегда подаётся в `classes[0].id`
Студент в 2+ классах может случайно отправить работу не в тот класс.
**Фикс:** добавить `<select>` для выбора класса, если `classes.length > 1`.

#### [MEDIUM] Forms: нет клиентской валидации размера файла перед отправкой
Подсказка «до 50 МБ» есть, но проверки нет — пользователь узнаёт об ошибке только после долгой загрузки.
**Фикс:**
```js
if (file.size > 50 * 1024 * 1024) { showError('Файл слишком большой (максимум 50 МБ)'); return; }
```

#### [MEDIUM] Error handling: при ошибке загрузки списка работ нет кнопки «Повторить»
Показывается текст «Ошибка загрузки», но нет способа повторить запрос.
**Фикс:** добавить `<button onclick="loadHomeworks()">Повторить</button>` в блок с ошибкой.

---

### profile.html

#### [MEDIUM] Mobile: `html, body { height: 100%; overflow: hidden; }` сломается при виртуальной клавиатуре
На iOS при фокусе на input виртуальная клавиатура поднимает область просмотра, overflow hidden блокирует прокрутку к полю.
**Фикс:** убрать `overflow: hidden` с `body`, использовать `height: 100dvh` на layout-контейнере или flex с `min-height: 100svh`.

#### [MEDIUM] Security/Forms: форма смены пароля не требует ввода текущего пароля
Любой получивший доступ к разблокированному устройству может сменить пароль.
**Фикс:** добавить поле «Текущий пароль» (`type="password"`) перед полями нового пароля.

#### [LOW] Feedback: `form-msg` появляется, но нет auto-hide через N секунд
Сообщение «Сохранено» остаётся на экране indefinitely.
**Фикс:** `setTimeout(() => formMsg.classList.remove('ok','err'), 4000)`.

---

### library.html

#### [MEDIUM] Accessibility/Mobile: кнопки редактирования/удаления папок появляются только при hover
```css
.folder-card-acts { opacity: 0; }
.folder-card:hover .folder-card-acts { opacity: 1; }
```
На touch-устройствах hover недоступен — кнопки принципиально не видны.
**Фикс:** на мобиле заменить hover на long-press или показывать кнопки через иконку «⋮» (три точки) при тапе на карточку.

#### [MEDIUM] Confirmation: `btn-del` (удалить файл/папку) без видимого confirmation modal в HTML
**Фикс:** добавить modal «Удалить файл/папку X? Это действие необратимо».

#### [LOW] Navigation: breadcrumb `.lib-bc` показывает путь, но нет клавиатурной поддержки
Ссылки в breadcrumb должны быть `<a href="...">`, а не `<span onclick="...">`.
**Фикс:** использовать семантические `<a>` элементы для каждого уровня пути.

---

### analytics.html

#### [MEDIUM] Loading: нет состояния загрузки для диаграмм Chart.js
Пока данные грузятся — область диаграммы пустая/белая без индикации.
**Фикс:** показывать skeleton-placeholder над canvas до получения данных.

#### [LOW] Navigation: кнопка «Назад» (`an-header-back`) ведёт `history.back()` — может вернуть в сломанное состояние
**Фикс:** задать конкретный href (`/classes` или `/dashboard`) вместо `history.back()`.

---

### gradebook.html

#### [MEDIUM] Accessibility: при большом числе студентов таблица горизонтально прокручивается, но нет подсказки
Пользователь не знает, что таблица шире экрана.
**Фикс:** добавить fade-gradient на правом краю таблицы, пока есть горизонтальный скролл.

#### [LOW] Navigation: нет ссылки на профиль студента при клике на имя в таблице
**Фикс:** сделать имена студентов в первом столбце кликабельными ссылками на `/profile?id=...`.

---

### question-bank.html

#### [MEDIUM] Confirmation: удаление вопроса (если есть кнопка) без confirmation modal
**Фикс:** modal с предупреждением «Вопрос может быть использован в активных тестах».

#### [LOW] Forms: фильтры не сохраняются при переходе к редактированию и обратно
Пользователь теряет выбранные фильтры при навигации.
**Фикс:** хранить состояние фильтров в URL query params (`?subject=bio&type=mc`).

---

### live-quiz.html

#### [HIGH] Confirmation: кнопка `btn-end` (завершить сессию) без подтверждения
Нажатие заканчивает живую викторину для всех участников мгновенно.
**Фикс:** modal «Завершить сессию? Все участники будут автоматически переведены на экран результатов».

#### [MEDIUM] Feedback: нет отображения числа подключённых участников в реальном времени
Учитель не видит, сколько студентов уже подключились перед стартом.
**Фикс:** показывать счётчик «X участников подключились» с обновлением через polling/WebSocket.

---

### course.html

#### [LOW] Navigation: кнопка «Записаться» / «Продолжить» не показывает состояние записи до загрузки
Кнопка мерцает при загрузке (меняет текст после JS-инициализации).
**Фикс:** рендерить кнопку в скрытом/skeleton-состоянии до получения данных о статусе записи.

---

### lesson.html

#### [MEDIUM] Content: inline quiz не имеет кнопки «Попробовать снова»
После неверного ответа опция подсвечивается красным, но повторить нельзя.
**Фикс:** добавить кнопку «Ещё раз» после показа правильного ответа.

#### [MEDIUM] Layout: `max-height: 600px` для accordion-блоков — длинный контент обрежется
```css
.accordion-body { max-height: 600px; overflow: hidden; }
```
**Фикс:** убрать `max-height`, использовать JS `element.style.maxHeight = element.scrollHeight + 'px'` для анимации.

#### [LOW] Navigation: оглавление (TOC) скрыто при ширине < 1100px — нет альтернативного доступа
На планшетах нет способа перейти к нужному разделу без ручной прокрутки.
**Фикс:** добавить кнопку «Оглавление» в topbar, открывающую TOC как drawer/dropdown на мобиле.

---

### pet.html

#### [LOW] Accessibility: интерактивная анимация дыхания без возможности приостановки
Постоянно движущийся элемент может вызывать дискомфорт у пользователей с вестибулярными нарушениями.
**Фикс:** соблюдать `prefers-reduced-motion`: `@media (prefers-reduced-motion: reduce) { .breath-anim { animation: none; } }`.

---

### red-book.html

#### [MEDIUM] Consistency: доступ через `href="/red-book.html"` с расширением
Все остальные страницы доступны без `.html`. Эта единственная исключение.
**Фикс:** добавить роут `GET /red-book` → serve `red-book.html`. Исправить ссылку в сайдбарах всех страниц.

#### [LOW] Navigation: пункты подменю Red Book (`red-book-biomes`, `red-book-ecosystem`, `red-book-games`) видны в сайдбаре только на страницах Red Book
Пользователь не знает о подразделах, пока не зайдёт в Red Book.
**Фикс:** допустимо (паттерн контекстного сайдбара), но нужен `<title>` в виде «Красная книга — Биомы» для ориентации (уже есть).

---

### theory.html

#### [MEDIUM] Navigation/Roles: кнопка «Создать курс» (`btn-new-course`) видна в заголовке для всех
Студент видит кнопку действия, недоступного ему по роли.
**Фикс:** скрывать кнопку до JS role-check (`style="display:none"` в HTML, показывать только для teacher/admin).

---

### 404.html

#### [LOW] Navigation: кнопка «Назад» использует `history.back()`
Если 404 открыт по прямой ссылке или из внешнего источника — history.back() закроет вкладку или уведёт на внешний сайт.
**Фикс:** добавить `if (history.length > 1) history.back(); else location.href = '/dashboard';`.

---

### 403.html

#### [LOW] UX: три кнопки подряд — «На главную», «Назад», «Войти» — могут запутать незалогиненного пользователя
Если пользователь не залогинен, «На главную» тоже приведёт к редиректу на login.
**Фикс:** проверять авторизацию через `LS.isLoggedIn()` и показывать только релевантные кнопки: для залогиненных — «На главную» + «Назад», для незалогиненных — только «Войти».

---

### 500.html

#### [LOW] Feedback: «Мы уже в курсе» — вводящий в заблуждение текст
На статических страницах ошибок нет механизма автоматической отправки отчёта. Фраза ложная.
**Фикс:** заменить на «Попробуй обновить страницу или вернись позже».

---

### biochem.html и семейство (biochem-library, biochem-pathways, biochem-reactions, biochem-properties)

#### [MEDIUM] Mobile: `body { overflow: hidden; }` + canvas-интерфейс непригоден для мобиле
Молекулярный конструктор и аналогичные страницы требуют мышь для взаимодействия с canvas.
**Фикс:** добавить предупреждение «Для работы рекомендуется компьютер» при ширине < 768px, или реализовать touch-управление (pinch-to-zoom, tap-to-place).

#### [MEDIUM] Navigation: нет кнопки «Сохранить молекулу» с очевидным расположением
Действия в правой панели (`bp-btn`) мелкие и не имеют подписей при сжатии панели.
**Фикс:** добавить явную primary-кнопку «Сохранить в библиотеку» в тулбар.

#### [LOW] Accessibility: canvas-элементы недоступны для скринридеров
`<canvas id="mol-canvas">` без `aria-label` и без fallback-контента.
**Фикс:** `<canvas aria-label="Молекулярный конструктор" role="img">Интерактивный редактор молекул</canvas>`.

---

### lesson-editor.html

#### [MEDIUM] Forms: нет автосохранения (видна статусная строка `.etb-status`, но нет таймера автосохранения)
Потеря введённого контента при случайном закрытии вкладки.
**Фикс:** `setInterval(() => saveDraft(), 30000)` + обработчик `beforeunload` при несохранённых изменениях.

#### [MEDIUM] Navigation/Confirmation: кнопка «Назад» (`etb-back`) ведёт без проверки несохранённых изменений
**Фикс:** перехватить клик, если `isDirty`, показать: «Есть несохранённые изменения. Уйти?».

#### [LOW] Accessibility: редактор использует `contenteditable` (предположительно) без ARIA-ролей
**Фикс:** `role="textbox" aria-multiline="true" aria-label="Содержимое урока"`.

---

### collection.html / collection-rb.html

#### [LOW] Empty state: нет состояния пустой коллекции
Если у пользователя нет элементов в коллекции, нет поясняющего empty-state.
**Фикс:** добавить `.rich-empty` с иллюстрацией и текстом «Коллекция пуста — открывай карточки видов, чтобы пополнять её».

---

### red-book-biomes.html / red-book-ecosystem.html / red-book-games.html

#### [LOW] Mobile: canvas-страницы с `height: 100vh; overflow: hidden` некорректно ведут себя на мобиле
**Фикс:** использовать `height: 100dvh` для корректного учёта браузерной панели на мобиле.

#### [LOW] Navigation: кнопка «Назад» присутствует в topbar как `eco-back` — хорошо, но нет breadcrumb
Пользователь не видит, где он в иерархии Red Book.
**Фикс:** добавить breadcrumb «Красная книга > Экосистемы».

---

## Кросс-страничные (системные) проблемы

### [HIGH] Navigation: ссылка `/red-book.html` с расширением во всех сайдбарах
Затрагивает: board.html, classes.html, homework.html, library.html, profile.html, dashboard.html и все остальные страницы с общим сайдбаром.
**Фикс:** добавить роут в Express: `app.get('/red-book', (req,res) => res.sendFile('red-book.html', {root: 'frontend'}))`, изменить все ссылки.

### [MEDIUM] Navigation: «скрытые» инструментальные страницы недостижимы из навигации
Следующие страницы **не имеют ссылок в основном сайдбаре**:
- `/analytics` — аналитика успеваемости
- `/gradebook` — журнал оценок
- `/question-bank` — банк вопросов
- `/live-quiz` — живая викторина
- `/lesson-editor` — редактор уроков
- `/flashcards` — карточки SRS
- `/crossword`, `/hangman` — игры
- `/knowledge-map` — карта знаний
- `/lab` — лаборатория симуляций
- `/pet` — виртуальный питомец

Пользователи находят их только если знают прямой URL.
**Фикс:** сгруппировать в сайдбаре или добавить «хаб» страницу с плитками всех инструментов (например, `/tools`).

### [MEDIUM] Consistency: CDN Lucide загружается с разных источников
- Большинство файлов: `https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js`
- `biochem-pathways.html`, `biochem-reactions.html`: `https://unpkg.com/lucide@0.469.0/...`

**Фикс:** унифицировать на один CDN (jsDelivr предпочтительнее по производительности).

### [MEDIUM] Accessibility: нет `aria-current="page"` на активных пунктах сайдбара
`.sb-link.active` ставится через JS, но без ARIA-атрибута скринридеры не объявляют текущую страницу.
**Фикс:** при активации добавлять `link.setAttribute('aria-current', 'page')`.

### [LOW] Mobile: `js/mobile.js` не подключён к инструментальным страницам
Страницы analytics, gradebook, question-bank, live-quiz и др. не имеют моб-бара, их нельзя нормально открыть на мобиле.
**Фикс:** добавить `<script src="/js/mobile.js"></script>` перед `</body>` во всех страницах с `.app-layout`.

### [LOW] Consistency: страницы biochem-семейства используют `data-lucide` без вызова `lucide.createIcons()`
Если JS lucide загружен defer, иконки не инициализируются до вызова функции.
**Фикс:** убедиться, что `lucide.createIcons()` вызывается после загрузки DOM во всех таких страницах.

### [LOW] Accessibility: большинство модальных окон (`confirm-overlay`, `modal`) не управляют фокусом
При открытии modal фокус остаётся на кнопке открытия, а не перемещается внутрь. Нет `aria-modal="true"`, нет `role="dialog"`, нет `aria-labelledby`.
**Фикс:** при открытии modal: `modal.setAttribute('aria-modal', 'true'); modal.setAttribute('role', 'dialog'); firstFocusable.focus();`. Trap focus внутри пока открыт.

---

## Статистика проблем

| Серьёзность | Количество |
|-------------|------------|
| HIGH        | 10         |
| MEDIUM      | 32         |
| LOW         | 22         |
| **Итого**   | **64**     |

---

## Приоритетный план исправлений

### Спринт 1 (критические, 1–2 дня)
1. `board.html`: исправить href кнопки «Перейти» → `/test-run?...`
2. `login.html`: редирект после регистрации по роли
3. `homework.html`: убрать пустой `catch {}`, добавить обработку ошибок
4. `test-result.html`: разделить кнопки на разные destination
5. `admin.html`: добавить confirmation modal на смену роли и удаление вопроса
6. `live-quiz.html`: добавить confirmation на завершение сессии

### Спринт 2 (важные, 3–5 дней)
7. Express-роут `/red-book` + исправить ссылки во всех сайдбарах
8. Добавить `/js/mobile.js` на все инструментальные страницы
9. `profile.html`: поле «текущий пароль» + убрать `overflow:hidden` с body
10. `homework.html`: select класса при нескольких классах + валидация размера файла
11. `lesson-editor.html`: автосохранение + beforeunload guard
12. Скрыть учительские кнопки (`btn-new-cl`, `btn-new-course`) до JS role-check

### Спринт 3 (улучшения, 1–2 недели)
13. Добавить `aria-current="page"` в сайдбаре
14. ARIA-атрибуты на всех modal/dialog
15. `prefers-reduced-motion` на анимации
16. Хаб-страница `/tools` или группировка инструментов в сайдбаре
17. Унификация CDN Lucide на jsDelivr
18. Сохранение реакций на сервере (board.html)
19. Автосохранение прогресса теста на сервере (test-run.html)
20. Контекстные empty-state для всех ошибок загрузки с кнопкой «Повторить»
