Commit Graph

230 Commits

Author SHA1 Message Date
Maxim Dolgolyov 427874ee54 feat(textbook): add inline SVG visualizations to all 48 theory cards in geometry_8_ch1
Added labeled SVG diagrams (280x148–170px) to every makeCard() call across
all 16 paragraphs (§1–§16). Each section gets 3 theory cards × 1 SVG each,
showing pentagons, hexagons, triangulations, parallelograms, rectangles,
rhombuses, trapezoids, Thales construction, medians/centroid and more.
Total: +1069 LOC, 48 SVGs inserted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 09:51:40 +03:00
Maxim Dolgolyov cb1559439c feat(geom8): Wave 2 Главы 2 — §5-§8 (трапеция, ромб, прямоуг.тр-к, высота к гипотенузе)
§5 Трапеция: draggable SVG (b, h), 4-шаговое доказательство через 2 трапеции
→ параллелограмм, калькулятор 3 режима, DnD, тренажёр, босс.
§6 Ромб: draggable концов диагоналей (AC⊥BD), доказательство 4 тр-ка
→ прямоугольник d₁×d₂/2, тройной калькулятор (диагонали/a·h/a²sinα), DnD,
тренажёр, босс.
§7 Прямоугольный треугольник: draggable катетов, доказательство дублированием
→ прямоугольник, калькулятор (a,b→S,c; S,a→b; c,h_c→S), тренажёр, босс.
§8 Высота к гипотенузе: 3 подобных треугольника подсвечены цветом,
доказательство h_c=ab/c через равенство площадей, калькулятор полный
(a,b→c,h_c,a_c,b_c), DnD, тренажёр, босс.

File: 1675 → 3167 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:30:47 +03:00
Maxim Dolgolyov d20f0f933e feat(geom8): Wave 1 Главы 2 — §1-§4 (квадрат, прямоугольник, параллелограмм, треугольник)
§1 Площадь квадрата: SVG-сетка со слайдером a=1..10, калькулятор двусторонний
(a→S, S→√S), конвертер единиц (мм²/см²/дм²/м²/км²), тренажёр, босс.
§2 Прямоугольник: draggable угол (a,b,S=a·b в реалтайме), калькулятор прямой
и обратный, DnD-сортер по S=24, тренажёр, босс.
§3 Параллелограмм: draggable верхнее основание — S=a·h не меняется
(равноплощадные!), 4-шаговая анимация 'разрезаем и переставляем
в прямоугольник', калькулятор, тренажёр, босс.
§4 Треугольник: draggable C по горизонтальной прямой — S=½·a·h постоянна,
анимация 'достраиваем поворотом на 180° в параллелограмм', калькулятор тройной
(a,h→S; S,a→h; S,h→a), тренажёр, босс.

File: 503 → 1675 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:08:28 +03:00
Maxim Dolgolyov e22405516b fix(geom8): §3 внешние углы — корректная геометрия визуализации
Было: продолжение рисовалось от next-vertex назад через v, дуга центрировалась
у next-vertex с углом из произвольного направления — углы отображались
неправильно (не у тех вершин, не в тех направлениях).

Стало: для каждой вершины v вычисляются prev/next, направления u=(v-prev)/|·|
(входящая сторона), w=(next-v)/|·| (исходящая). Продолжение u рисуется от v
дальше. Дуга — сектор у v от u-направления до w-направления, sweep
определяется через знак векторного произведения (u×w). Подпись угла —
по биссектрисе дуги на радиусе Rlabel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:56:35 +03:00
Maxim Dolgolyov 640ca245ee fix(geom8): drag-интерактивы — pointermove/up на window + §7 индикатор равенства диагоналей
Drag (12 SVG-интерактивов): pointermove/pointerup/pointercancel слушались на
самом vertex-элементе. При выходе курсора за пределы маленького круга drag
обрывался — отсюда эффект 'нажал, чуть-чуть потянулось, и всё'. Перенесены
на window — теперь работают как нативный drag.

§7 (Прямоугольник): info-карточка показывала 'AC = BD' с одним значением.
Теперь две отдельные карточки AC и BD + индикатор равенства (зелёная плашка
'Диагонали равны' / красная 'Не равны' с Δ).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:53:57 +03:00
Maxim Dolgolyov 2e37360dac fix(geom8): §4 — определение cy в drawProof доказательства
В функции drawProof пошагового доказательства §4 использовалась переменная
cy без определения (была только cx). Это приводило к ReferenceError при
вызове buildP4, и из-за throw в ensureBuilt секция §4 не открывалась
при клике на карточку в селекторе параграфов.

Проверено: все 17 параграфов главы (p1-p16, final1) теперь строятся без ошибок.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:49:16 +03:00
Maxim Dolgolyov 22bd60cf0f feat(geom8): Wave 4 Главы 1 — финал главы (шпаргалка, карта связей, 7 боссов)
Часть 1: 9 mini-cards с формулами всех 16 параграфов (KaTeX).
Часть 2: интерактивная SVG-карта иерархии четырёхугольников
(клик по узлу — подсветка свойств).
Часть 3: 7 интегрированных боссов (по 10 XP):
  - Босс 1: многоугольник из суммы углов 1620°
  - Босс 2: параллелограмм через треугольник ABD
  - Босс 3: средние линии прямоугольника → ромб
  - Босс 4: ромб 60° → диагонали (Пифагор)
  - Босс 5: теорема Фалеса, 3 подзадачи
  - Босс 6: треугольник 12-16-20 — средняя линия + медиана + центроид
  - Босс 7: равнобедренная трапеция 20/8/10
Часть 4: при победе над всеми — achievement 'Мастер многоугольников Главы 1',
+50 XP бонус, confetti, кнопка перехода к Главе 2.

File: 5194 → 5558 LOC. Глава 1 полностью наполнена.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:38:19 +03:00
Maxim Dolgolyov ecda85e8ef feat(geom8): Wave 3 Главы 1 — §11-§16 (Фалес, медианы, средние линии, трапеция)
§11 Теорема Фалеса: SVG-угол с параллелями, конструктор деления отрезка
на n частей, тренажёр, DnD, босс.
§12 Медианы: SVG-треугольник drag + центроид G, доказательство 2:1
через среднюю линию, калькулятор, тренажёр, босс.
§13 Средняя линия треугольника: SVG со срединным треугольником,
доказательство, mini-quiz, DnD, тренажёр, босс.
§14 Трапеция: SVG drag (сохраняет параллельность оснований), конструктор
типов, доказательство m=(a+b)/2, калькулятор, тренажёр, босс.
§15 Равнобедренная трапеция: SVG с симметрией, 2 доказательства
(углы, диагонали), DnD свойств, тренажёр, босс.
§16 Признаки равнобедренной: 2 SVG-индикатора, доказательство признака,
mini-quiz, тренажёр, босс.

GLOSSARY: +центроид, +основания трапеции.
File: 3373 → 5194 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:18:47 +03:00
Maxim Dolgolyov 76eff24732 feat(geom8): Wave 2 Главы 1 — §5-§10 (параллелограмм, прямоугольник, ромб, квадрат)
§5 Свойства параллелограмма: SVG drag B/D, 2 пошаговых доказательства,
DnD-сортер, тренажёр, босс.
§6 Признаки: 3 SVG-демо, квиз выбора, DnD, доказательство признака 1, босс.
§7 Прямоугольник: SVG, доказательство AC=BD, калькулятор d=√(a²+b²),
тренажёр, DnD, босс.
§8 Признак прямоугольника: SVG с двойным индикатором, доказательство,
mini-quiz, тренажёр, босс.
§9 Ромб: SVG drag, доказательство AC⊥BD, калькулятор S=d₁d₂/2, DnD, босс.
§10 Квадрат: SVG со слайдером, иерархия фигур, калькулятор, DnD, тренажёр, босс.

File: 1910 → 3373 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:32:59 +03:00
Maxim Dolgolyov 99d7bf3d03 feat(geom8): Wave 1 Главы 1 — §1-§4 с интерактивами
§1 Многоугольники: SVG-конструктор с drag-вершинами, калькулятор диагоналей,
DnD-сортер фигур, тренажёр периметра, босс (4 задачи).
§2 Сумма углов: анимация триангуляции, калькулятор, обратная задача, DnD
правильные ↔ углы, босс.
§3 Внешние углы: SVG свёртка в точку (360°), калькулятор, тренажёр, mini-quiz, босс.
§4 Параллелограмм: SVG-конструктор (drag B/D), DnD, пошаговое доказательство,
тренажёр углы/периметр, босс.

File: 766 → 1910 LOC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 19:07:22 +03:00
Maxim Dolgolyov 03d567e953 feat(catalog): Геометрия 8 (Казаков) — Phase 0 hub + 4 skeleton
- migration 017: geometry-8 hub + 4 children (Многоугольники, Площади,
  Подобие, Окружности) с parent_slug. sort_order=4, physics-8 → 5.
- geometry_8_hub.html (~380 LOC): blue/cyan hub в стиле algebra-8-hub,
  4 цветные карточки глав (amber/emerald/purple/cyan), агрегированный
  прогресс, ачивка «Мастер геометрии 8» при 56/56.
- 4 skeleton-файла chapter (geometry_8_ch1..ch4.html): полная
  инфраструктура (CSS, STATE, XP-карта, glossary, search Ctrl+K,
  sidebar, DnD, server-sync), 16/15/9/16 параграфов как stub'ы.
  Реальный контент — в последующих волнах.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 18:47:40 +03:00
Maxim Dolgolyov 08d259bfa2 chore(tracker): убрать отладку — console.log, debug-бейдж, server-лог
Прогресс работает, отладочная обвязка больше не нужна:
- tracker.js: удалены все console.log/console.warn (boot, click,
  POST, HTTP-ответ, patch-успех), удалены ensureDebugBadge и
  updateDebugBadge (визуальный бейдж в правом нижнем углу),
  recordParaVisit больше не вызывает updateDebugBadge
- 5 хуков (bubble, capture, setParaTab-patch, .tab[refN] sidebar,
  polling .active) сохранены в production-виде — без логов, но
  с теми же действиями
- backend/routes/textbooks.js: убран '[progress]' console.log из
  POST /:slug/progress

Pre-commit hook теперь проходит без --no-verify.
2026-05-27 18:12:12 +03:00
Maxim Dolgolyov 908e7f3f1c fix(tracker): хук на боковую панель-справочник (.tab[data-tab=refN])
Chemistry-9 и physics-9 имеют ДВА навигатора:
1. .para-pill[data-para=pN] — верхние пилюли с большими карточками
2. .tab[data-tab=refN]      — sidebar-справочник, тонкие строки слева

Ученик кликал именно по второму (§46 Mg и ЩЗМ), но tracker
ловил только первый. Маппинг ref<N> → p<N> по регексу.

Capture-фаза, чтобы не зависеть от bubble.
2026-05-27 17:56:54 +03:00
Maxim Dolgolyov 1b07f086b4 debug(tracker): визуальный бейдж в правом нижнем углу + серверный лог POST'ов 2026-05-27 17:53:38 +03:00
Maxim Dolgolyov dd7daa7d7a fix(tracker): 4-й хук — polling по .para-pill.active
Если ни bubble, ни capture, ни setParaTab-patch не сработали (например,
страница использует другой механизм навигации), наблюдаем DOM раз в
500мс на изменение класса .active у пилюли. Когда активная пилюля
меняется — фиксируем визит.

Это самый robust способ: работает независимо от событий, функций и
библиотек страницы. Стоит копейки — один querySelector в 500мс.
2026-05-27 17:47:33 +03:00
Maxim Dolgolyov 1e1c0e95f7 fix(tracker): тройной хук — bubble, capture, monkey-patch setParaTab
Юзер докладывает, что клик по пилюле не вызывает body click handler
(никаких логов после клика). Возможные причины: capture-listener
расширения браузера со stopPropagation, CSS overlay, что-то ещё.

Чтобы гарантированно ловить клики ВНЕ зависимости от bubble-цепочки:
1) Bubble click на body (как было)
2) Capture click на document (фаза до bubble)
3) Monkey-patch window.setParaTab — функцию, которую chemistry-9 и
   physics-9 зовут inline через onclick. Перехват на уровне JS-функции
   работает даже если event-стек сломан.

Защита от двойного срабатывания: pill.__tbVisited флаг на 100мс.

Если setParaTab определяется позже tracker'а — короткий poll 20*100мс.
2026-05-27 17:44:29 +03:00
Maxim Dolgolyov 5e49fd5835 debug(tracker): логировать ВСЕ клики на body, чтобы найти потерянный bubbling 2026-05-27 17:38:28 +03:00
Maxim Dolgolyov edeb442846 fix(tracker): hash-вход (chemistry-9#p6) тоже шлёт mark_read
Из каталога кнопка 'Продолжить' ведёт на /textbook/<slug>#<last_para>.
handleHashNav при загрузке делала setLastPara(p6) — POST с last_para
БЕЗ mark_read. Поэтому каталог менял last_para, но 'прочитано'
оставалось без изменений.

Сейчас handleHashNav объединяет оба обновления (как wirePillTracking)
в один POST с mark_read=key.

Из лога user 2: '[tracker] chemistry-9 → POST {"last_para":"p6"}'
теперь будет '...{"last_para":"p6","mark_read":"p6"}'.
2026-05-27 17:33:54 +03:00
Maxim Dolgolyov 43f5edbbc3 debug(tracker): временные console.log для диагностики молчащего sync
Пользователь видит '1 из N' (от моих тестовых POST через API) но
клики в браузере не увеличивают счётчик. Добавлены логи:
- на boot: slug, есть ли LS, есть ли токен
- на клик по пилюле: ключ
- на каждый POST: тело + HTTP-статус ответа
- на ошибку: response.text или fetch-exception

Цель — собрать сигнал из DevTools-консоли пользователя.
Уберём после диагностики (одобрено как временное).
2026-05-27 17:31:55 +03:00
Maxim Dolgolyov dfe26a4771 fix(physics8): добавить /js/api.js в head — без него tracker молча отключается
Tracker проверяет 'LS.getToken()' перед каждым POST'ом. Без api.js
объект LS undefined, и tracker возвращает из syncToServer ничего не
делая. Поэтому в physics8_thermal/electro/optics прогресс не писался
вообще (ни last_para, ни mark_read).

Добавил <script src="/js/api.js" defer> перед xp.js во все 3 файла.

Chemistry-9 и physics-9 не затронуты — у них api.js уже подключён в
конце body перед tracker'ом.
2026-05-27 17:21:06 +03:00
Maxim Dolgolyov 25c0bb2a79 fix(tracker): mark_read шлётся на КАЖДЫЙ клик пилюли (идемпотентно)
Старый syncPending-баг успел залить локальный localState.read данными,
которых нет на сервере. После фиксов firstTime=false для всех ключей в
localState.read, и mark_read иначе никогда не уходил → каталог показывал
0 даже после реальных кликов.

Решение: убрать оптимизацию firstTime. Слать mark_read КАЖДЫЙ раз —
серверный код  if(!arr.includes(mark_read)) arr.push(...)  не добавит
дубликат. Лишний POST стоит копейки, зато система самовосстанавливается
без зависимости от загрузочного backfill.
2026-05-27 17:17:00 +03:00
Maxim Dolgolyov 89ddc4f68f fix(tracker): backfill — local-only mark_read'ы досылаются на сервер при загрузке
Старый syncPending-баг (теперь починен в коммите dacc0eb) оставил у
учеников локальное состояние с прочитанными параграфами, но сервер
ничего не знал. После фикса firstTime=false для всех уже-кликнутых
пилюль, и mark_read не уходил на сервер при повторном клике.

Решение: loadServerProgress теперь вычисляет diff между local.read
и server.read; для каждого ключа, которого нет на сервере, дёргает
syncToServer({mark_read: k}). Coalesce в pendingExtra гарантирует,
что все запросы упорядочатся.

Эффект: при следующей загрузке учебника каталог автоматически догоняется.
2026-05-27 17:10:33 +03:00
Maxim Dolgolyov dacc0eb4ac fix(tracker): mark_read больше не дропается из-за syncPending
Раньше: клик по .para-pill вызывал setLastPara() → POST с last_para
→ syncPending=true. Тут же вызывался markRead() → второй POST с
mark_read → guard 'if (syncPending) return' молча отбрасывал его.
Результат: каталог показывал 'Продолжить' (last_para пришёл),
но '0 из N прочитано' (paragraphs_read остался пуст).

Два уровня фикса:
1) wirePillTracking объединяет last_para + mark_read в ОДИН POST
   через коалесцирующий syncToServer(firstTime ? {mark_read:key} : {})
2) syncToServer теперь не дропает патчи: если предыдущий POST в
   полёте, новые поля сохраняются в pendingExtra и отправляются
   после .finally() — гарантия 'ни один mark_read не теряется'.

Затрагивает chemistry-9, physics-9, physics8_thermal/electro/optics —
у них теперь '0/N прочитано' начнёт расти при кликах по пилюлям.
2026-05-27 17:08:49 +03:00
Maxim Dolgolyov dad34dc1d6 fix(algebra-8 ch1): прогресс пишется под правильный slug + миграция 016
После переименования slug algebra-8 → algebra-8-ch1 (миграция 014) Глава 1
продолжала POSTить прогресс под старым именем 'algebra-8', который теперь
указывает на hub-строку. Эффект: paragraphs_read и last_para уходили в
hub-row, а каталог хабов их игнорировал (агрегирует только children).

Фикс:
- algebra_8.html: _TB_SLUG = 'algebra-8-ch1'
- migration 016: union перенос ошибочно записанного прогресса из hub в
  ch1; очистка hub-row. Идемпотентно (NOT EXISTS guard).

Проверено: после миграции у user 2 paragraphs_read='["p1"]' живёт в
ch1-row, hub-row пуста.

Другие учебники проверены — корректно:
- ch2/ch3 уже использовали правильные slug
- chemistry-9, physics-9, physics8_* подключены через textbook-tracker
- algebra_8_hub.html и physics_8.html — хабы без tracker (правильно)
2026-05-27 17:03:59 +03:00
Maxim Dolgolyov 1a347650f4 feat(catalog): авто-mark-as-read + Физика 8 как полноценный хаб
A. textbook-tracker.js: первый клик по .para-pill теперь автоматически
   помечает параграф как прочитанный. «Прочитано» = «открыто». Сразу
   даёт осмысленный счётчик для chemistry-9 и physics-9 в каталоге.
   Slug fallback: physics8_* → physics-8-* (корректный слаг).

B. Физика 8 — миграция 015:
   - 3 children: physics-8-thermal / electro / optics с parent_slug
   - parent физики-8 обновлён: para_count=40, описание трёх разделов
   - sub-файлы получили textbook-tracker.js + правильный слаг
   - physics_8.html переписана в стиле algebra_8_hub: 3 цветные
     карточки, агрегированный прогресс, ачивка «Эксперт физики 8»

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 17:00:36 +03:00
Maxim Dolgolyov c806a5137a fix(algebra-8 hub): убран intro-блок с упоминанием авторов и дублирующая статистика 2026-05-27 16:53:29 +03:00
Maxim Dolgolyov 699fdcc7fb feat(catalog): хаб-страница для Алгебры 8 (3 главы под единым слагом)
- migration 014: parent_slug column + algebra-8 hub row +
  rename old algebra-8 → algebra-8-ch1 (progress сохраняется
  через стабильный textbook_id=3)
- backend/routes/textbooks.js: GET / фильтрует parent_slug IS NULL;
  aggregated progress для хабов; новый GET /:slug/children
- algebra_8_hub.html: новая хаб-страница с 3 карточками глав,
  hero с общим прогрессом, XP-бейдж, ссылки на главы
- algebra_8/ch2/ch3: кнопки cross-chapter заменены на
  одну «К алгебре 8» в шапке

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 16:49:20 +03:00
Maxim Dolgolyov 033c941b02 feat(xp): physics8 + chem9 + phys9 синхронизируют XP с системной геймификацией
- js/textbook-xp-widget.js: shared модуль (monkey-patch addXp +
  para-pill auto-award для учебников без addXp)
- physics8_thermal/electro/optics: добавлены теги /js/xp.js и
  /js/textbook-xp-widget.js — теперь все 74 addXp-хука пробрасываются
  в глобальный gamification (через self-award endpoint с дебаунсом)
- chemistry_9 + physics_9: те же теги. Каждый первый клик по
  .para-pill даёт +5 XP в систему (без правок 23000 LOC)
- Изначальный XP в учебниках не теряется — localStorage остаётся
  кешем, сервер — источник правды
2026-05-27 16:36:43 +03:00
Maxim Dolgolyov c2ef4f4898 feat(algebra-8 ch3): Wave 4 — финал, 7 боссов, практика
§ Финал главы 3:
- BOSS ARENA: 7 боссов (5–7 заданий каждый, иконки <>, ±, [], ∩, ∪, 1/x, ★):
  · §13 Хранитель сравнения — свойства, транзитивность, смена знака
  · §14 Алхимик границ — оценки x+y, x-y, xy
  · §15 Архитектор промежутков — линейные неравенства, запись
  · §16 Дирижёр пересечений — системы и совокупности
  · §17 Мастер параболы — квадратные, D, корни
  · §18 Властелин ОДЗ — дробно-рац., выколотые точки
  · ★ Чемпион неравенств (финал) — 7 заданий из всей главы
- Универсальный движок select / yesno / input, HP-бар, состояние в localStorage
- Сертификат «Чемпион неравенств» при всех 7 победах

Увлекательная математика (3 факта):
- Почему меняется знак при умножении на отрицательное
- Кто придумал знаки $<$ и $>$ (Хэрриот, 1631)
- Неравенство Коши (a+b)/2 ≥ √(ab)

Финальная практика — генератор 5 типов задач (линейные, оценка,
системы, квадратные, ОДЗ). Серия из 5 = достижение.

algebra_8.html: добавлена ссылка «Глава 3 →» в шапке.
2026-05-27 16:24:12 +03:00
Maxim Dolgolyov b540c1b3a0 feat(algebra-8 ch3): Wave 3 — §17 (метод интервалов) + §18 (дробно-рац.)
§ 17 «Квадратные неравенства. Метод интервалов»:
- Теория: парабола, метод интервалов, правило знаков
- INTERACT 1: SVG-парабола + слайдеры a, b, c с цветовой раскраской
  на оси: зелёные зоны = выражение > 0, красные = < 0. Корни как
  точки. Внизу — текстовый анализ (D, корни, решение для >0 и <0).
- INTERACT 2: Пошаговый решатель — D, корни, знаки, ответ
  (4-5 шагов с обработкой D<0 и D=0)
- INTERACT 3: Тренажёр 6 квадратных (multiple-choice)
- INTERACT 4: Drag-сопоставление (a, D, направление неравенства) →
  тип ответа: вне корней / между / R / пусто
- INTERACT 5: «Где плюс, где минус?» — кликаем по 3 интервалам
  параболы x²−4x+3, ставим знаки. Победа = +, -, +.

§ 18 «Дробно-рациональные»:
- Теория: f/g ≷ 0, выколотые точки знаменателя, алгоритм
- INTERACT 1: Пошаговый решатель (x-a)/(x-b) ≥ 0 с учётом a vs b
  (включая случай a == b)
- INTERACT 2: Тренажёр 6 неравенств (multiple-choice)
- INTERACT 3: Найди ОДЗ — 5 выражений, вводим запрещённые точки
- INTERACT 4: Drag «закрашена/выколота» — 8 ситуаций
2026-05-27 16:21:49 +03:00
Maxim Dolgolyov a508b6a4da feat(algebra-8 ch3): Wave 2 — §15 (промежутки + линейные) + §16 (системы)
Новый общий хелпер drawNumLine(opts) — SVG числовая прямая с делениями,
стрелкой и подписями, поддерживает несколько интервалов разных цветов.

§ 15 «Числовые промежутки. Линейные неравенства»:
- Теория: таблица 5 видов промежутков, алгоритм решения линейного
- INTERACT 1: Конструктор промежутка (a, b + 6 типов) → SVG-визуализация
- INTERACT 2: Конвертация записи (8 multiple-choice)
- INTERACT 3: Пошаговый решатель (вводишь a,b,c,d → 4 шага решения
  ax+b ≥ cx+d с обработкой смены знака)
- INTERACT 4: Тренажёр линейных (8 случайных, выбор знака + ввод k)
- INTERACT 5: Drag-сопоставление неравенство ↔ запись промежутка

§ 16 «Системы и совокупности»:
- Теория: система (И, пересечение) и совокупность (ИЛИ, объединение)
- INTERACT 1: Пересечение двух промежутков на SVG-прямой
  (4 слайдера → пересечение зелёным, оригиналы индиго/янтарный)
- INTERACT 2: Пошаговый решатель системы (3 шага + ответ)
- INTERACT 3: Drag «система или совокупность» (8 примеров)
- INTERACT 4: Тренажёр систем (6 случайных)
- INTERACT 5: Совокупность визуально — (-∞;a) ∪ (b;+∞) с слайдерами
2026-05-27 16:18:05 +03:00
Maxim Dolgolyov dc201f28ff feat(algebra-8): Глава 3 Wave 1 — скелет + §13 + §14
Глава 3 «Неравенства с одной переменной» по программе Арефьевой/Пирютко.
Палитра: индиго → фиолетовый → бирюза. 6 параграфов + финал.

Скелет (общая инфраструктура, копия паттернов из ch2):
- 7 параграфов: §13–§18 + final3
- LocalStorage 'algebra8_ch3_*', shared XP 'algebra8_xp'
- DnD-хелпер setupSorter, glossary с 12 терминами, поиск Ctrl+K
- XP-карта + бейдж + 7 контекстных подсказок + ачивки
- Server sync прогресса (markLastPara/markParaRead, debounce 600мс)

§ 13 «Числовые неравенства и их свойства»:
- Теория, 5 главных свойств, примеры
- INTERACT 1: Drag-сортировка 5 чисел по возрастанию (5 наборов)
- INTERACT 2: «Знак меняется или нет» (8 операций)
- INTERACT 3: Конструктор a, b, k + операция → live-сравнение
- INTERACT 4: Цепочка свойств (5 шагов выбора)
- INTERACT 5: Drag-классификация (8 переходов по 4 свойствам)
- INTERACT 6: Тренажёр «Что больше?» (10 случайных задач)

§ 14 «Сложение, умножение, оценка»:
- Теория, таблица 4 операций для оценки, пример
- INTERACT 1: Калькулятор оценок (live x+y, x-y, xy, x/y)
- INTERACT 2: Тренажёр границ (8 задач)
- INTERACT 3: Drag «Можно сложить / перемножить / нельзя»
- INTERACT 4: Пошаговое сложение (5 шагов)
- INTERACT 5: Сложи неравенства (6 multiple-choice)

DB: миграция 013 — slug 'algebra-8-ch3', sort_order=5, бамп physics-8 на 6.
Главы 1 и 2 теперь имеют кнопку «Глава 3 →» в шапке.
2026-05-27 16:14:15 +03:00
Maxim Dolgolyov 66166f6294 feat(algebra-8): синхронизация прогресса учебника с каталогом
Раньше: алгебра 1 и 2 главы хранили прогресс только в localStorage,
поэтому каталог /textbooks показывал 0/N прочитано и кнопку 'Открыть'
даже после активной работы с учебником.

Теперь обе главы шлют POST /api/textbooks/:slug/progress:
- markLastPara(id) — при каждом goTo(); сервер запоминает last_para,
  каталог показывает кнопку 'Продолжить'.
- markParaRead(id) — когда STATE.progress[key] первый раз ≥ 50%
  (внутрипараграфный прогресс достаточен); сервер добавляет id в
  paragraphs_read[], каталог показывает '1/7 прочитано'.
- Дебаунс 600мс — несколько быстрых переходов схлопываются в один POST.
- keepalive:true + beforeunload-flush, чтобы последний переход не
  потерялся при закрытии вкладки.
- loadServerReadState() при init() — если на другом устройстве уже
  прочитаны параграфы, локальный STATE.progress поднимается до 100%
  для них (визуально совпадает с каталогом).

Slug: 'algebra-8' для ch1, 'algebra-8-ch2' для ch2.
2026-05-27 16:01:26 +03:00
Maxim Dolgolyov 64bd44088d feat(xp): textbook XP синхронизируется с системной геймификацией
- backend: POST /api/gamification/self-award (rate-limited, validated)
- frontend/js/xp.js: load/add/flush/on клиент, ~150 LOC, дебаунс 300мс,
  keepalive fetch на unload/visibilitychange hidden
- algebra_8.html и algebra_8_ch2.html: XP_LEVELS заменён на единую
  формулу с сервером; addXp/loadProgress подключены к window.LS.xp
- При первой загрузке: merge max(local, server); далее сервер — источник
  правды
2026-05-27 15:56:36 +03:00
Maxim Dolgolyov 9199427dfd feat(algebra-8): общая система опыта для главы 1 и главы 2
Раньше: каждая глава хранила XP отдельно (algebra8_ch1_xp +
algebra8_ch2_xp), формулы уровня были разные (дискретная таблица в
ch1, формула sqrt в ch2), визуально XP-карты различались.

Теперь:
- Один ключ localStorage: 'algebra8_xp' для обеих глав.
- При первой загрузке (в любой главе) — single-shot миграция:
  если новый ключ отсутствует, суммирует старые ch1 + ch2 и
  сохраняет под единый ключ. Старые ключи не удаляются (на всякий).
- Единая таблица уровней XP_LEVELS = [0, 50, 120, 220, 350, 520,
  740, 1000, 1300, 1700, 2200] (11 уровней, MAX = Ур. 11).
- Единые функции calcLevel(xp) и _xpForLevel(lv).
- XP-карта в сайдбаре главы 2 теперь идентична главе 1:
  градиент acc→pri-soft, .xp-card-title, .xp-bar, .xp-fill, .xp-nums.
- Hero badge «★ Ур. N · NN XP» добавлен в hero обоих глав.
- addXp в ch2: при повышении уровня — popup с номером уровня + confetti.
- addXp в ch1: refreshProgressUI вызывается, чтобы обновлять hero
  badge сразу после начисления.
2026-05-27 15:41:54 +03:00
Maxim Dolgolyov 58998a59c0 feat(algebra-8 ch2): XP-карта, бейдж в hero, совет дня, фикс sidebar 'Финал'
- SIDEBARS.final2: убрал stub 'будет в Wave 4', добавил 5 строк по
  финалу (7 боссов, типы заданий, награда, практика, серия).
- XP card в сайдбаре: уровень (Lv N), текущий XP, прогресс-бар до
  следующего уровня, остаток XP. Формула: Lv = floor(sqrt(xp/50)).
- XP badge в hero (рядом с прогрессом): жёлто-розовая пилюля
  «★ Lv N · NN XP», обновляется при каждом addXp.
- TIPS: 7 советов (по одному на каждый §+финал). В сайдбаре отдельная
  карточка «Подсказка» с жёлтым градиентом — контекстная под текущий
  параграф.
- refreshProgressUI: после изменения XP пересобирает сайдбар, чтобы
  карточки опыта/совета оставались актуальными.
2026-05-27 15:36:11 +03:00
Maxim Dolgolyov e21b12a7ce feat(algebra-8 ch2): Wave 5 — глоссарий-тултипы + поиск Ctrl+K
GLOSSARY: 13 ключевых терминов (квадратное уравнение, дискриминант,
теорема Виета, биквадратное, ОДЗ, посторонний корень и др.) с
определениями в KaTeX и привязкой к параграфу.

- wrapGlossary(root): обходит текстовые узлы секции, оборачивает
  совпадения регулярным выражением по всем алиасам. Игнорирует
  KaTeX-узлы, кнопки, инпуты, сайдбары, шапку, поп-апы.
- Падеж-алиасы для каждого термина (дискриминант / дискриминанта /
  дискриминантом / дискриминанте).
- Подчёркнутый пунктиром термин при ховере / клике показывает
  плавающий tooltip с определением и ссылкой на параграф.
- Запускается после goTo() с задержкой 60мс.

SEARCH (Ctrl+K):
- Кнопка «Поиск» в шапке + хоткей Ctrl+K (cmd+K на Mac).
- Индекс: 7 параграфов + 13 терминов глоссария + 5 ключевых формул
  + Финал главы.
- Скоринг: title contains > startsWith bonus > word match.
- Стрелки ↑↓ / Enter / Esc / клик мышью.
- При выборе термина — переход в его параграф + scrollIntoView
  + жёлтая подсветка 1.4с.

Стили: .gloss-term пунктирное подчёркивание, .gloss-tip floating card,
.search-modal с blur backdrop, .search-row с hover/active.
2026-05-27 15:31:35 +03:00
Maxim Dolgolyov 0cd187b693 feat(algebra-8 ch2): 3 сортировки переведены на drag-and-drop
Универсальный хелпер setupSorter(cfg) с pointer-events:
- desktop: тащим карточку → подсветка целевого ящика → отпускаем = поставлено
- touch / mobile: тап по карточке (становится "armed") → тап по ящику = поставлено
- × кнопка на placed-чипе → возврат в pool
- drop за пределы ящика на сам pool тоже возвращает чип
- threshold 8px — клик не превращается в drag случайно

Стили: .dnd-chip с cursor:grab/active grabbing, .armed shadow,
.dragging opacity, .drop-box.over подсветка с лёгким scale.

Применено к:
- § 7 INT 2 (полное / неполное / не квадратное) — 8 уравнений
- § 10 INT 5 (раскладывается / не раскладывается) — 8 трёхчленов
- § 11 INT 5 (движение / работа / числа / геометрия) — 8 задач,
  columnLayout:true для длинных текстов

Старые «лесенки кнопок Полн./Неполн./Не квадр.» удалены — теперь
один-клик-затем-один-клик или drag. § 12 INT 4 оставлен как
<select> (другой паттерн: одна метка для нескольких уравнений).
2026-05-27 15:27:44 +03:00
Maxim Dolgolyov 75792c93aa fix(algebra-8 ch2): шаговые решатели — теперь действительно по одному шагу
Три «пошаговых» решателя дампили все шаги сразу при первом клике.
Переписаны на прогрессивное раскрытие:
- § 8 INT 5 «Пошаговый решатель» (квадратное)
- § 10 INT 2 «Пошаговый разлагатель»
- § 12 INT 1 «Решатель биквадратного»

Паттерн: Старт → шаги собираются в массив, idx=0 → Дальше (1/N) →
каждый шаг — отдельный блок с border-left и fadeIn. По окончании —
кнопка «Готово», начисление достижения и confetti. Кнопка «Сначала»
сбрасывает к Старту.

Ещё: § 8 INT 4 — $D = b^2 - 4ac$ показывался буквально с долларами,
потому что использовался textContent + renderMath на чужом элементе.
Заменено на innerHTML + renderMath на правильный узел.
2026-05-27 15:21:45 +03:00
Maxim Dolgolyov 7a85007777 fix(algebra-8 ch2): сломанная вёрстка слайдеров, прокачаны подсказки и шпаргалка
- Слайдеры (.sliders label): убран flex-direction:column, который раскладывал
  KaTeX-span / '=' / <b> / <input> на 4 строки. Теперь label = block,
  всё на одной строке, slider — на следующей.
- .wg-help: вместо мелкого курсива — полноценный hint-box с жёлтым
  градиентом, левой полосой и круглым «?» слева. Совпадает по визуалу
  с главой 1.
- Шпаргалка: добавлена кнопка «Шпаргалка» в шапке, на узких экранах
  (≤980px) col-side превращается в выезжающий справа drawer с
  backdrop'ом, открывается по кнопке/закрывается по клику вне или Esc.
- initSidebarToggle() вызывается из init().
2026-05-27 15:18:47 +03:00
Maxim Dolgolyov 26510ff712 fix(migration 012): bump physics-8 to sort_order=5 so algebra-8-ch2 sits next to ch1 2026-05-27 15:11:25 +03:00
Maxim Dolgolyov c2f66b1e97 feat(algebra-8 ch2): Wave 4 — финал + 7 боссов + DB migration
frontend/textbooks/algebra_8_ch2.html · final2:
- Boss Arena с 7 боссами:
  · §7 «Хранитель неполных» — 5 заданий
  · §8 «Дискриминатор» — 5 заданий
  · §9 «Дух Виета» — 5 заданий
  · §10 «Разложитель» — 5 заданий
  · §11 «Архивариус задач» — 5 заданий
  · §12 «Мастер замены» — 5 заданий
  · ★ «Магистр алгебры» (финал) — 7 заданий
- Универсальный движок: select / yesno / input
- HP-бар, иконки боссов, состояние в localStorage
- Сертификат «Магистр квадратных уравнений» при всех 7 победах

Дополнительно:
- Увлекательная математика (3 spoiler-факта: история, a≠0, парадокс)
- Финальная практика: генератор случайных задач со всей главы (5 типов)
- Серия из 5 верных = достижение
- ACH_LABELS для всех boss_*, all_bosses, prac_streak

algebra_8.html: добавлена ссылка «Глава 2 →» в шапке.
migrations/012_algebra_8_ch2.sql: регистрация slug 'algebra-8-ch2'.
2026-05-27 14:54:01 +03:00
Maxim Dolgolyov cfca88c3e0 feat(algebra-8 ch2): Wave 3 — §11 (Текст. задачи) + §12 (Сводящиеся)
§ 11 «Текстовые задачи»:
- Теория «4 шага», типы (движение/работа/числа/геометрия)
- INTERACT 1: Шаблон 4 шага с пошаговым выбором (6 шагов)
- INTERACT 2: Тренажёр 5 задач с подсказками
- INTERACT 3: Движение по реке (vл, vр, S — live-расчёт)
- INTERACT 4: Задача про двузначное число (54)
- INTERACT 5: Drag-классификатор 8 задач по 4 типам

§ 12 «Сводящиеся к квадратным»:
- Биквадратные через замену t=x², дробные уравнения, ОДЗ
- INTERACT 1: Решатель биквадратного (вводишь a, b, c — полное решение)
- INTERACT 2: Тренажёр 6 биквадратных уравнений
- INTERACT 3: Пошаговое решение дробного уравнения (6 шагов)
- INTERACT 4: Выбор подходящей замены (4 уравнения)
- INTERACT 5: «Найди посторонний корень» (3 уравнения с ОДЗ)

ACH_LABELS для p11_*/p12_*, шпаргалки §11/§12 заполнены.
2026-05-27 14:50:51 +03:00
Maxim Dolgolyov 90d0c41fd0 feat(algebra-8 ch2): Wave 2 — §9 (Виета) + §10 (Разложение)
§ 9 «Теорема Виета»:
- Теория, обратная теорема, общий случай (a≠1), знаки корней
- INTERACT 1: Тренажёр устного подбора (10 уравнений)
- INTERACT 2: Конструктор «корни → уравнение»
- INTERACT 3: Знаки корней (8 раундов: оба+, оба-, разные, нет)
- INTERACT 4: Быстрая проверка корней через Виета
- INTERACT 5: Виета для непривед. (сумма −b/a, произведение c/a)

§ 10 «Квадратный трёхчлен. Разложение»:
- Теория, алгоритм, формула ax²+bx+c = a(x−x₁)(x−x₂)
- INTERACT 1: Конструктор разложения (a, x1, x2 → трёхчлен)
- INTERACT 2: Пошаговый разлагатель (D, корни, разложение)
- INTERACT 3: Тренажёр (8 трёхчленов → корни)
- INTERACT 4: Сокращение дробей (5 задач, выбор из 4 вариантов)
- INTERACT 5: Разложимо или нет (8 трёхчленов по D)

ACH_LABELS добавлены для p9_* и p10_*.
Сайдбары для §9 и §10 заполнены формулами.
2026-05-27 14:46:03 +03:00
Maxim Dolgolyov 2c8eb84c65 feat(algebra-8): Chapter 2 Wave 1 — skeleton + §7 + §8
§7 «Квадратные уравнения. Неполные»:
- Теория, правила, алгоритм, примеры
- INTERACT 1: Конструктор уравнения (3 слайдера a/b/c, live-расчёт типа и корней)
- INTERACT 2: Сортировка 8 уравнений по 3 типам (полное/неполное/не квадратное)
- INTERACT 3: Пошаговый решатель неполных (2 типа: bc=0, ac=0)
- INTERACT 4: Задача про страницу книги (165 см²)
- INTERACT 5: Тренажёр 10 неполных с таймером
- INTERACT 6: «Имеет ли корни?» — 8 раундов на знаки

§8 «Формулы корней. Дискриминант»:
- Вывод формулы, таблица 3 случаев, алгоритм 5 шагов
- INTERACT 1: Калькулятор дискриминанта (пошагово)
- INTERACT 2: SVG-парабола (a, b, c слайдеры) + точки пересечения с OX
- INTERACT 3: 3 случая D (>0, =0, <0) с мини-графиками
- INTERACT 4: Тренажёр «сколько корней?» 10 уравнений
- INTERACT 5: Пошаговый решатель полного квадратного
- INTERACT 6: «Угадай знак D» только по графику параболы

Скелет: 6 параграфов (§7-§12) + финал, цвета по секциям, LocalStorage,
12 достижений, hero-прогресс, KaTeX, тёмная тема.

§§9-12 + final — stubs до следующих волн.
2026-05-27 14:42:07 +03:00
Maxim Dolgolyov 31fb5d7ab0 feat(textbooks): Wave Bosses — 7 битв-проверок (+971 строка)
В конце каждого § перед secNav добавлена карточка 'Босс §N: <тема>' с битвой из 5-7 задач.

7 битв:
- §1 «Знаток корней» (7 задач): √121, √50 vs 7, √(−9), (√5)², √(a²), √0.81, число корней из 100
- §2 «Эксперт по числам» (6): множество для 1/3, √7 рацион/иррац, поиск иррац., 0.(3)=1/3, ℕ⊂ℝ, целые между √51
- §3 «Свойства корней» (7): √(9·25), √a·√b формула, √(64/16), √(a²)=a (нет), √100·√4, √81/√9, √(36a²)
- §4 «Преобразования» (6): √72=?, 5√3=√?, освобождение 1/√3, 3√2 vs 2√3, √200=?, (√7+√7)²
- §5 «Числовые промежутки» (6): запись x>5, (2;6)∩[4;10], 3∈(2;5], (-∞;0)∪(0;+∞), [1;4)∪[4;8], целые в [-3;4]
- §6 «Системы» (6): {x>2;x≤5}, [x≤1;x>4], -2<3x+1≤7, целые {x≥0;x<4}, {x≥5;x≤3}, {x²>0}
- Финальный босс (7 комбинированных): √(15²+8²), √75−√12, x²=49 число корней, D(√(x-3)+√(7-x)), √(10−2√21), 0.5≤x/3<2, √(0.04·49)

Движок (универсальный):
- 3 типа: select (кнопки), yesno, input (числовой с Enter)
- Полоса прогресса 'N / total'
- 2 попытки → объяснение → опционально пропуск (-5 XP)
- Подсказка -3 XP
- Медали: golden 7/7 без ошибок и подсказок | silver ≥5 | bronze прошёл
- XP: 30 / 50 / 80
- Perfect → доп. ачивка boss_pN_perfect
- 3D-flip анимация медали при награде
- Confetti при ≥4 правильных
- Интеграция с streak, sounds, achievement
- STATE.bossResults сохранён в LocalStorage algebra8_ch1_bossResults
- После прохождения в заголовке карточки отображается медаль + счёт + 'Повторить'

CSS: 52 строки новых стилей через --sec-acc для цветового разделения

Итог: 6829 строк, 11/11 JS-блоков валидны
2026-05-27 13:49:12 +03:00
Maxim Dolgolyov beebdadca0 feat(textbooks): Wave Depth — 4 прокачанных интерактива (+474 строки)
1. §1 «Извлечение в столбик» — пошаговая анимация
   - Поле ввода числа + пресеты 1296/2916/7744
   - Async-функция clStart() рендерит классическое 'деление в столбик'
   - JetBrains Mono шрифт, подсветка текущей грани цветом секции
   - Поясняющий текст для каждого шага рядом
   - При остатке 0: confetti + 15 XP + ачивка 'col-root'
   - Для нецелых корней — корректно показывает остаток

2. §4 «Сравнение через квадраты» — визуальное доказательство
   - 5 пар: 3√2 vs 2√3, 4√3 vs 3√5, √17 vs 4, ...
   - SVG с двумя анимированно растущими квадратами (transform scale 0→1, spring)
   - Победитель — бейдж в верхней части
   - Под квадратами: (3√2)² = 18 > 12 = (2√3)²

3. §5 «Эйлеровы диаграммы» — альтернатива линейной визуализации
   - 4 слайдера для границ A и B
   - Два эллипса (pink/blue) с пересечением
   - Режимы: 'Показать ∪' (золотой контур), '∩' (зелёная штриховка), 'Оба'
   - Дополняет существующую линейную визуализацию

4. §6 «Решатель систем 3+ неравенств» — расширен с 2 до 5
   - Динамический контейнер #sys-list с массивом _sysRows
   - Кнопка '+ Добавить неравенство' (до 5)
   - Кнопка '×' удаляет (кроме первой)
   - SVG-прямая динамически масштабируется под N строк
   - Совместимость с sysMode/solveLin сохранена
2026-05-27 13:39:11 +03:00
Maxim Dolgolyov aed820c2d1 feat(textbooks): красивая анимация доказательства √(ab)=√a·√b
Старая версия: два статичных прямоугольника бок о бок (синий a×b и розовый √(ab)×√(ab)) с текстовым описанием. Зритель не видел РАВЕНСТВА площадей.

Новая версия — настоящее визуальное доказательство:
- Один большой SVG-канвас (600×280) с двумя зонами и стрелкой между ними
- Слева: прямоугольник a×b из единичных клеток (синих). Каждая клетка отдельный <rect> (всего a·b штук)
- Справа: пунктирная рамка квадрата √(ab)×√(ab) (заполнится анимацией)
- При нажатии 'Анимировать':
  * Шаг 1: волна подсветки клеток жёлтым по очереди (20мс задержка)
  * Шаг 2: клетки 'летят' (CSS transition 550мс на x/y) к новой позиции в квадрате,
    меняя цвет с синего на розовый
  * Шаг 3: финальная пульсация + KaTeX-формула с числами и бейдж 'Доказано!'
- KaTeX-формула под канвасом обновляется живо: $\sqrt{a·b}$ = ... + $\sqrt{a}·\sqrt{b}$ = ...
- 'Сбросить' возвращает в исходное положение

Бонус: для непрямого квадрата (a·b не точный квадрат) анимация всё равно работает, клетки плотно укладываются в столбцы по ceil(√ab), визуально показывая что суммарная площадь одинакова.
2026-05-27 13:18:41 +03:00
Maxim Dolgolyov aebdc47e4f fix(textbooks): KaTeX распознаёт \[…\] + переделать «Упрости √» в пошаговую игру
1. KaTeX: в config delimiters добавлены '\['/'\]' (display) и '\('/'\)' (inline) во всех 6 местах вызова renderMathInElement. Раньше initFracIrr использовал \[…\] в template literal — выводилось raw LaTeX. Теперь рендерится математически.

2. «Упрости √» переделан с нуля:
   Было: непонятный drag-and-drop с пустой drop-zone и техническим хинтом
   Стало: явный вопрос 'Выберите точный квадрат, который делит подкоренное'
   - Карточки кандидатов крупные (с подписью "= N²" под числом)
   - Не делит → красная тряска + объяснение
   - Делит но не максимальный → жёлтое предупреждение
   - Максимальный квадрат → зелёная анимация pop + пошаговый вывод KaTeX:
     √72 = √(36·2) = √36·√2 = 6√2
   - confetti + XP +8
   - Кнопка 'Подсказка' даёт намёк
   - На правильном ответе остальные карточки блокируются
2026-05-27 13:14:54 +03:00
Maxim Dolgolyov 6864db5b94 fix(textbooks): подписи на числовой прямой §2 больше не перекрываются
Было: 3 уровня (i%3) × 12px — близко стоящие √2 √3 √5 π √15 наложились друг на друга.

Стало:
- Точки сортируются по координате
- Для каждой подписи ищется минимальный уровень БЕЗ перекрытия с уже размещёнными (с учётом ширины метки ~44px и шкалы в пикселях)
- До 9 уровней по 20px вверх от оси
- От подписи к точке идёт тонкая линия-выноска (0.45 opacity)
- Box-shadow на метках для разделения если плотно

Также: ось перемещена с y=60 на y=100 — больше места сверху для уровней. Контейнер 120 → 140px высоты.
2026-05-27 13:10:20 +03:00