diff --git a/backend/src/db/migrations/049_math6_hub.sql b/backend/src/db/migrations/049_math6_hub.sql
new file mode 100644
index 0000000..b976cb6
--- /dev/null
+++ b/backend/src/db/migrations/049_math6_hub.sql
@@ -0,0 +1,51 @@
+-- Math 6 hub migration.
+-- Creates math-6 as a full hub textbook (6 chapters) in the style of chemistry-7 / algebra-7:
+-- math-6 (hub, html_path = math_6_hub.html)
+-- math-6-ch1 (Десятичные дроби, §§1–12) → math_6_ch1.html
+-- math-6-ch2 (Проценты и пропорции, §§1–9) → math_6_ch2.html
+-- math-6-ch3 (Множество, §§1–5) → math_6_ch3.html
+-- math-6-ch4 (Рациональные числа, §§1–11) → math_6_ch4.html
+-- math-6-ch5 (Координатная плоскость, §§1–5) → math_6_ch5.html
+-- math-6-ch6 (Наглядная геометрия, §§1–5) → math_6_ch6.html
+--
+-- Source: Герасимов В. Д., Пирютко О. Н., «Математика. 6 класс»,
+-- Минск: Адукацыя і выхаванне, 2022 (2-е изд.). Контент авторский (наш).
+-- Author left empty per project policy.
+
+-- 1. Parent hub row.
+INSERT OR IGNORE INTO textbooks
+ (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
+VALUES
+ ('math-6', 'math', 6, 'Математика — 6 класс',
+ '',
+ 'Полный курс математики 6 класса: десятичные дроби, проценты и пропорции, множества, рациональные (положительные и отрицательные) числа, координатная плоскость и наглядная геометрия. 6 глав, 38 параграфов, интерактивные тренажёры и финалы-боссы.',
+ 'math_6_hub.html', 48, 'indigo', 6, 1, NULL);
+
+-- 2. Six chapters.
+INSERT OR IGNORE INTO textbooks
+ (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
+VALUES
+ ('math-6-ch1', 'math', 6, 'Математика 6 · Десятичные дроби',
+ '',
+ '§§1–12: десятичная запись и разряды, сравнение и округление, координатный луч, сложение и вычитание, умножение и деление (в т. ч. на 10, 100, 1000), деление на десятичную дробь, конечные и бесконечные дроби, преобразования выражений.',
+ 'math_6_ch1.html', 12, 'indigo', 1, 1, 'math-6'),
+ ('math-6-ch2', 'math', 6, 'Математика 6 · Проценты и пропорции',
+ '',
+ '§§1–9: проценты, три основные задачи на проценты, пропорция и её основное свойство, прямая и обратная пропорциональные зависимости, решение задач пропорцией, масштаб, круговые диаграммы.',
+ 'math_6_ch2.html', 9, 'cyan', 2, 1, 'math-6'),
+ ('math-6-ch3', 'math', 6, 'Математика 6 · Множество',
+ '',
+ '§§1–5: множество и его элементы, пустое множество, способы задания множеств, операции (пересечение и объединение), круги Эйлера и решение задач с их помощью.',
+ 'math_6_ch3.html', 5, 'violet', 3, 1, 'math-6'),
+ ('math-6-ch4', 'math', 6, 'Математика 6 · Рациональные числа',
+ '',
+ '§§1–11: положительные и отрицательные числа, координатная прямая, модуль и противоположные числа, множества Z и Q, сравнение, сложение, вычитание, умножение и деление рациональных чисел, законы сложения.',
+ 'math_6_ch4.html', 11, 'rose', 4, 1, 'math-6'),
+ ('math-6-ch5', 'math', 6, 'Математика 6 · Координатная плоскость',
+ '',
+ '§§1–5: прямоугольная (декартова) система координат, графики реальных процессов, графики прямой и обратной пропорциональной зависимости.',
+ 'math_6_ch5.html', 5, 'emerald', 5, 1, 'math-6'),
+ ('math-6-ch6', 'math', 6, 'Математика 6 · Наглядная геометрия',
+ '',
+ '§§1–5: наглядные представления тел и их развёртки, окружность и круг (длина окружности и площадь круга), виды треугольников, центральная и осевая симметрия.',
+ 'math_6_ch6.html', 6, 'amber', 6, 1, 'math-6');
diff --git a/backend/tests/math6-page.test.js b/backend/tests/math6-page.test.js
new file mode 100644
index 0000000..34d200e
--- /dev/null
+++ b/backend/tests/math6-page.test.js
@@ -0,0 +1,88 @@
+'use strict';
+/*
+ * Phase 0 jsdom-каркас «Математика 6»: хаб и 6 глав выполняются на движке
+ * math6_engine.js без ошибок скриптов; para-selector строится с нужным числом
+ * карточек; активен первый §; движок рендерит секции и заглушку/контент с кнопкой
+ * прочтения. Содержание §§ наполняется по главам — здесь проверяется каркас.
+ */
+const test = require('node:test');
+const assert = require('node:assert');
+const fs = require('node:fs');
+const path = require('node:path');
+const { JSDOM, VirtualConsole } = require('jsdom');
+
+const ROOT = path.join(__dirname, '..', '..');
+const readF = p => fs.readFileSync(path.join(ROOT, p), 'utf8');
+const wait = ms => new Promise(r => setTimeout(r, ms));
+
+/* Инлайним внешние скрипты (CDN убираем, api/xp заменяем заглушками). */
+function buildPage(file) {
+ let html = readF('frontend/textbooks/' + file);
+ const inl = {
+ '/js/math6_svg.js': readF('frontend/js/math6_svg.js'),
+ '/js/math6_engine.js': readF('frontend/js/math6_engine.js')
+ };
+ html = html
+ .replace(/')
+ .replace(/');
+ });
+ return html;
+}
+
+async function loadDom(file) {
+ const errors = [];
+ const vc = new VirtualConsole();
+ vc.on('jsdomError', e => errors.push(e.message));
+ const dom = new JSDOM(buildPage(file), {
+ runScripts: 'dangerously', pretendToBeVisual: true, virtualConsole: vc, url: 'http://localhost/',
+ beforeParse(w) { w.scrollTo = function () {}; }
+ });
+ await wait(160);
+ return { dom, errors, doc: dom.window.document };
+}
+
+const CHAPTERS = [
+ { file: 'math_6_ch1.html', cards: 12 },
+ { file: 'math_6_ch2.html', cards: 9 },
+ { file: 'math_6_ch3.html', cards: 5 },
+ { file: 'math_6_ch4.html', cards: 11 },
+ { file: 'math_6_ch5.html', cards: 5 },
+ { file: 'math_6_ch6.html', cards: 6 }
+];
+
+for (const ch of CHAPTERS) {
+ test(`${ch.file}: SPA без ошибок, ${ch.cards} карточек, активен § 1`, async () => {
+ const { doc, errors } = await loadDom(ch.file);
+ assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
+ assert.equal(doc.querySelectorAll('#psel-grid .psel-card').length, ch.cards, ch.cards + ' карточек');
+ const active = doc.querySelector('.sec.active');
+ assert.ok(active && active.id === 'sec-p1', 'активен sec-p1');
+ const body = doc.querySelector('#p1-body');
+ assert.ok(body && body.children.length > 0, 'тело § 1 заполнено');
+ assert.ok(doc.querySelector('#p1-body [data-read]'), 'кнопка прочтения § 1');
+ /* финал помечен и присутствует */
+ assert.ok(doc.querySelector('#psel-grid .psel-card.final'), 'есть карточка финала');
+ });
+}
+
+test('hub: 6 карточек глав', async () => {
+ const { doc, errors } = await loadDom('math_6_hub.html');
+ assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
+ assert.equal(doc.querySelectorAll('.ch-grid .ch-card').length, 6, '6 глав');
+});
+
+test('навигация и прогресс: переход на § и отметка прочтения', async () => {
+ const { doc, errors } = await loadDom('math_6_ch1.html');
+ const win = doc.defaultView;
+ win.goTo('p4'); await wait(60);
+ assert.ok(doc.querySelector('#sec-p4.active'), 'перешли на § 4');
+ /* отметка прочтения начисляет XP */
+ const btn = doc.querySelector('#p4-body [data-read]');
+ assert.ok(btn, 'кнопка прочтения § 4');
+ btn.click(); await wait(20);
+ assert.ok((win.M6STATE.progress.p4 || 0) >= 30, 'прогресс § 4 вырос после прочтения');
+ assert.deepEqual(errors, [], 'нет ошибок: ' + errors.join(' | '));
+});
diff --git a/frontend/css/math6.css b/frontend/css/math6.css
new file mode 100644
index 0000000..74eade5
--- /dev/null
+++ b/frontend/css/math6.css
@@ -0,0 +1,243 @@
+/* math6.css — общий фреймворк интерактивного учебника «Математика 6».
+ * Подключается всеми 6 главами; палитра задаётся на странице через :root
+ * (--pri / --pri2 / --pri-soft / --acc / --acc2 / --acc-soft).
+ * Базируется на проверенном дизайне Алгебры 7, обобщён под переменные.
+ * Иконки — только inline SVG .ic (эмодзи запрещены).
+ */
+
+:root{
+ --bg:#f7f8fc; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
+ --border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
+ /* Палитра по умолчанию (indigo) — страницы переопределяют */
+ --pri:#4f46e5; --pri2:#3730a3; --pri-soft:#e0e7ff;
+ --acc:#6366f1; --acc2:#4f46e5; --acc-soft:#eef2ff;
+ --ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
+ --bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
+ /* Акцент секции = цвет главы (можно переопределить на .sec) */
+ --sec-acc:var(--pri); --sec-acc-d:var(--pri2); --sec-acc-soft:var(--pri-soft);
+}
+html.dark,.dark{
+ --bg:#0a0a12; --card:#13131f; --card-soft:#18182a; --text:#e8eaf6; --ink:#e8eaf6; --muted:#9aa0c0;
+ --border:#26263a; --pri-soft:rgba(99,102,241,.16); --acc-soft:rgba(99,102,241,.14);
+ --warn-bg:rgba(245,158,11,.16); --ok-bg:rgba(16,185,129,.16); --fail-bg:rgba(220,38,38,.18);
+}
+
+*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
+html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
+button,input,select,textarea{font-family:inherit;font-size:inherit}
+button{cursor:pointer;border:0;background:transparent;color:inherit}
+a{color:inherit;text-decoration:none}
+.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
+
+/* HEADER */
+.hdr{position:relative;background:linear-gradient(110deg,var(--pri2) 0%,var(--pri) 55%,var(--acc) 100%);color:#fff;padding:42px 22px 28px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.16);min-height:124px}
+.hdr::before{content:attr(data-wm);position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(4.5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.13);line-height:1;pointer-events:none;user-select:none;z-index:0}
+.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
+.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
+.hdr-sub{font-size:.85rem;opacity:.9;margin-top:6px;font-weight:500;line-height:1.4}
+.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
+.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.16);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
+.hdr-btn:hover{background:rgba(255,255,255,.28)}
+
+/* MAIN GRID */
+.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
+@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
+.col-main{min-width:0}
+
+/* HERO */
+.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
+@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
+.hero::before{content:attr(data-wm);position:absolute;right:-10px;top:-20px;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
+.hero h2{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
+.hero p{font-size:.95rem;color:var(--text);opacity:.9;margin-bottom:14px;max-width:660px}
+.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
+.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
+.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(79,70,229,.32)}
+.hero-progress{flex:1;min-width:200px;max-width:280px}
+.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
+.hp-bar{height:8px;background:rgba(79,70,229,.16);border-radius:5px;overflow:hidden}
+.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
+.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
+.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(79,70,229,.22);font-family:'Unbounded',sans-serif}
+
+/* PARA SELECTOR */
+.psel{margin-bottom:24px}
+.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
+.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
+.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
+.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
+.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
+.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
+.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
+.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
+.psel-prog{height:4px;background:rgba(79,70,229,.12);border-radius:3px;overflow:hidden}
+.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
+.psel-card.final{background:linear-gradient(135deg,var(--warn-bg,#fff5e1),var(--pri-soft))}
+.psel-card.final .psel-num{color:var(--warn)}
+.psel-card.applied .psel-num{color:var(--ok)}
+
+/* SECTIONS */
+.sec{display:none;position:relative;animation:fadeIn .35s ease}
+.sec.active{display:block}
+@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
+.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
+.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
+.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
+.sec-h{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
+
+/* CARDS */
+.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(79,70,229,.05);position:relative;z-index:1;transition:transform .25s cubic-bezier(.16,1,.3,1),box-shadow .25s}
+.card:hover{transform:translateY(-2px);box-shadow:0 4px 10px rgba(0,0,0,.06),0 16px 36px rgba(79,70,229,.10)}
+.card-header{display:flex;align-items:center;gap:10px;margin-bottom:12px;padding-bottom:10px;border-bottom:1px dashed var(--border)}
+.card-icon{width:32px;height:32px;border-radius:9px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:#fff}
+.card-icon.repeat{background:#0ea5e9}.card-icon.theory{background:#8b5cf6}.card-icon.algo{background:#f59e0b}.card-icon.rule{background:#ec4899}.card-icon.example{background:#10b981}.card-icon.oral{background:#06b6d4}.card-icon.class{background:#3b82f6}.card-icon.home{background:#f97316}
+.card-icon .ic{width:18px;height:18px}
+.card-title{font-family:'Unbounded',sans-serif;font-size:.82rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);flex:1}
+.card-num{font-size:.74rem;font-weight:700;color:var(--muted);background:var(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
+.card-body{font-size:.94rem;line-height:1.65}
+.card-body p{margin-bottom:8px}
+.card-body p:last-child{margin-bottom:0}
+.card-body ul,.card-body ol{margin:6px 0}
+
+/* WIDGET */
+.wg{background:linear-gradient(135deg,var(--card),var(--sec-acc-soft,var(--pri-soft)));border:1.5px solid var(--sec-acc,var(--pri));border-radius:14px;padding:18px 20px;margin-bottom:18px;box-shadow:var(--sh2);position:relative;z-index:1}
+.wg-header{display:flex;align-items:center;gap:8px;margin-bottom:14px}
+.wg-badge{padding:4px 9px;background:var(--sec-acc,var(--pri));color:#fff;border-radius:6px;font-family:'Unbounded',sans-serif;font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.06em}
+.wg-title{font-family:'Unbounded',sans-serif;font-size:1.05rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));flex:1}
+.wg-help{font-size:.88rem;color:var(--text);margin-bottom:12px;line-height:1.55;background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--sec-acc-soft,var(--pri-soft)));border-left:4px solid var(--warn,#f59e0b);padding:9px 14px;border-radius:9px}
+
+/* BUTTONS */
+.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
+.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
+.btn:active{transform:scale(.96)}
+.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
+.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
+.btn.small{padding:5px 11px;font-size:.78rem}
+
+/* INPUTS */
+.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace}
+.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
+
+/* FEEDBACK */
+.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
+.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
+.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
+.dark .feedback.ok{color:#86efac}.dark .feedback.fail{color:#fca5a5}
+
+/* SIDEBAR */
+.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
+.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
+.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
+.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
+.sidecard-row b{color:var(--pri);font-weight:700}
+.sidecard-row:last-child{margin-bottom:0}
+@media(max-width:980px){.col-side{position:static;max-height:none}}
+
+/* XP card */
+.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
+.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
+.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
+.xp-bar{height:9px;background:rgba(99,102,241,.15);border-radius:6px;overflow:hidden;margin:7px 0}
+.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
+.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
+
+/* SPOILER */
+.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
+.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,var(--pri2));list-style:none;display:flex;align-items:center;gap:8px}
+.spoiler summary::-webkit-details-marker{display:none}
+.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--sec-acc,var(--pri));width:18px}
+.spoiler[open] summary::before{content:'\2212'}
+.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
+
+.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
+.read-wrap{margin-top:18px;display:flex;justify-content:center}
+.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
+
+/* PLACEHOLDER (для ненаполненных §) */
+.m6-placeholder{padding:26px 20px;text-align:center;background:var(--card);border:1.5px dashed var(--border);border-radius:14px;color:var(--muted);font-size:.95rem;margin-bottom:16px}
+.m6-placeholder svg{width:34px;height:34px;stroke:var(--pri);opacity:.6;margin-bottom:10px}
+
+/* TABLES */
+.tbl{width:100%;border-collapse:collapse;margin:12px 0;font-size:.88rem}
+.tbl th,.tbl td{padding:7px 10px;border:1px solid var(--border);text-align:center}
+.tbl th{background:var(--sec-acc-soft,var(--pri-soft));color:var(--sec-acc-d,var(--pri2));font-weight:700}
+
+/* ACH popup */
+.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--pri),var(--acc));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(79,70,229,.45);z-index:1002;display:none;align-items:center;gap:8px;animation:achIn .45s cubic-bezier(.34,1.56,.64,1) forwards;max-width:340px}
+.ach-popup.show{display:flex}
+.ach-popup svg{stroke:#fff;fill:none}
+@keyframes achIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
+
+/* DRAG & DROP */
+.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px;transition:border-color .18s,background .18s}
+.dnd-pool.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
+.dnd-pool.col{flex-direction:column;align-items:stretch}
+.dnd-pool.col .dnd-chip{width:auto}
+.dnd-chip{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:var(--card);border:1.5px solid var(--border);border-radius:10px;cursor:grab;user-select:none;font-size:.92rem;line-height:1.4;transition:transform .12s,box-shadow .12s,border-color .12s;touch-action:none;max-width:100%}
+.dnd-chip:hover{transform:translateY(-1px);border-color:var(--sec-acc,var(--pri));box-shadow:var(--sh)}
+.dnd-chip:active{cursor:grabbing}
+.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(79,70,229,.22);transform:translateY(-1px)}
+.dnd-chip.dragging{opacity:.28}
+.dnd-chip.placed{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
+.dnd-chip .dnd-x{padding:0 5px;color:var(--muted);font-weight:700;font-size:1.05rem;border-radius:4px;cursor:pointer}
+.dnd-chip .dnd-x:hover{color:var(--bad,var(--fail));background:var(--fail-bg)}
+.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px;transition:border-color .15s,background .15s}
+.drop-box:hover{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft))}
+.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase;letter-spacing:.05em}
+.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
+.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
+.dnd-hint{font-size:.78rem;color:var(--muted);margin-bottom:8px;display:flex;align-items:center;gap:6px}
+.dnd-hint svg{width:14px;height:14px;flex-shrink:0}
+.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
+.sliders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
+.sliders label{display:block;font-size:.92rem;color:var(--muted);background:var(--card);padding:8px 12px;border-radius:8px;border:1px solid var(--border);line-height:1.5}
+.sliders label b{font-family:'JetBrains Mono',monospace;font-size:1.05rem;color:var(--sec-acc-d,var(--pri2));margin-left:4px}
+.sliders label input[type="range"]{display:block;width:100%;margin-top:6px;accent-color:var(--sec-acc,var(--pri))}
+.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px;font-size:.92rem}
+.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
+.qbox{padding:14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center;min-height:54px}
+
+/* HP-BAR for bosses */
+.hp-boss{height:14px;background:rgba(220,38,38,.12);border-radius:9px;overflow:hidden;border:1px solid #fecaca;margin:8px 0}
+.hp-boss-fill{height:100%;background:linear-gradient(90deg,#dc2626,#f59e0b);border-radius:9px;transition:width .5s cubic-bezier(.4,0,.2,1)}
+.boss-card{padding:16px;background:var(--card);border-radius:12px;border:2px solid var(--bad,#dc2626);margin-bottom:14px}
+.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
+.boss-title{font-family:'Unbounded',sans-serif;font-weight:800;color:#b91c1c;font-size:1.04rem;flex:1}
+.boss-stage{font-size:.85rem;color:var(--muted)}
+.boss-q{font-size:1rem;line-height:1.55;padding:11px 13px;background:var(--card-soft);border-radius:8px;margin-bottom:9px;border-left:3px solid var(--bad,#dc2626)}
+
+/* SIDEBAR DRAWER (mobile) */
+.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none;animation:fadeIn .18s ease}
+.col-side-backdrop.show{display:block}
+@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
+@media(max-width:980px){
+ .col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
+ .col-side.open{transform:none}
+}
+
+/* GLOSSARY */
+.gloss-term{border-bottom:1.5px dotted var(--sec-acc,var(--pri));cursor:help;color:var(--sec-acc-d,var(--pri2));font-weight:600;padding:0 1px}
+.gloss-term:hover{background:var(--sec-acc-soft,var(--pri-soft));border-radius:3px}
+.gloss-tip{position:fixed;max-width:320px;padding:11px 14px;background:var(--card);border:1.5px solid var(--sec-acc,var(--pri));border-radius:11px;font-size:.84rem;line-height:1.55;box-shadow:0 12px 32px rgba(0,0,0,.18);z-index:9994;display:none;pointer-events:none;color:var(--text)}
+.gloss-tip.show{display:block;animation:tipIn .15s ease}
+.gloss-tip b{color:var(--sec-acc-d,var(--pri2));font-size:.92rem}
+@keyframes tipIn{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
+
+/* SEARCH */
+.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
+.search-modal.show{display:flex;animation:fadeIn .15s ease}
+.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
+.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
+.search-results{flex:1;overflow-y:auto;padding:6px 0}
+.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border-left:0;border-right:0;border-top:0;width:100%;color:var(--text)}
+.search-row:hover,.search-row.active{background:var(--sec-acc-soft,var(--pri-soft))}
+.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
+.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
+.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
+.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
+.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px;background:var(--card-soft,transparent)}
+.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
+
+/* SVG figure container */
+.m6-fig{max-width:520px;margin:10px auto;border-radius:10px}
+.m6-fig svg{width:100%;height:auto;display:block}
diff --git a/frontend/js/math6_engine.js b/frontend/js/math6_engine.js
new file mode 100644
index 0000000..1d82f37
--- /dev/null
+++ b/frontend/js/math6_engine.js
@@ -0,0 +1,389 @@
+/* math6_engine.js — общий движок-плумбинг интерактивного учебника «Математика 6».
+ *
+ * Каждая страница-глава объявляет конфиг `window.M6` и (опционально) функции-билдеры
+ * buildXX(), затем подключает этот файл. Движок строит para-selector, навигацию,
+ * прогресс/XP/достижения, сайдбар (шпаргалка + подсказка), поиск, глоссарий, тему.
+ *
+ * § без билдера автоматически получает заглушку (.m6-placeholder + кнопка прочтения),
+ * поэтому каркас новой главы = только M6.paras. Кастомные интерактивы § пишутся
+ * inline-билдерами на странице и пользуются глобальными хелперами этого движка:
+ * makeCard, secNav, readBtn, feedback, renderMath, fmt, num, addXp, bumpProgress,
+ * achievement, setupSorter, confetti, goTo, M6icon.
+ *
+ * Конфиг window.M6 = {
+ * slug, lsPrefix, xpKey, paras:[{id,num,name,sub,final?,applied?}],
+ * achLabels:{}, startAch:[id,text], finalAch:[id,text],
+ * sidebars:{id:{title,rows:[[k,v]]}}, tips:[{sec,html}],
+ * glossary:[{term,def,sec,aliases:[]}], searchRows:[[kind,title,desc,sec]],
+ * builders:{id:fn}, footer
+ * }
+ */
+(function () {
+'use strict';
+if (window.__M6_ENGINE) return;
+window.__M6_ENGINE = true;
+
+var M6 = window.M6 || (window.M6 = {});
+var LSPRE = function () { return M6.lsPrefix || 'math6'; };
+var XPKEY = function () { return M6.xpKey || 'math6_xp'; };
+
+/* ============================================================ STATE */
+var STATE = { current: null, progress: {}, achievements: new Map(), xp: 0, level: 1 };
+window.M6STATE = STATE;
+function paras() { return M6.paras || []; }
+function total() { return paras().length || 1; }
+
+function calcLevel(xp) { return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
+function _xpForLevel(lv) { return (lv - 1) * (lv - 1) * 100; }
+
+function loadProgress() {
+ paras().forEach(function (p) { if (STATE.progress[p.id] == null) STATE.progress[p.id] = 0; });
+ try {
+ var s = localStorage.getItem(LSPRE() + '_progress');
+ if (s) Object.assign(STATE.progress, JSON.parse(s));
+ var a = localStorage.getItem(LSPRE() + '_achievements');
+ if (a) {
+ var p = JSON.parse(a);
+ if (Array.isArray(p)) p.forEach(function (id) { STATE.achievements.set(id, achLabel(id)); });
+ else if (p && typeof p === 'object') Object.keys(p).forEach(function (id) { STATE.achievements.set(id, (p[id] && p[id] !== id) ? p[id] : achLabel(id)); });
+ }
+ STATE.xp = +(localStorage.getItem(XPKEY()) || 0);
+ STATE.level = calcLevel(STATE.xp);
+ } catch (e) {}
+}
+function saveProgress() {
+ try {
+ localStorage.setItem(LSPRE() + '_progress', JSON.stringify(STATE.progress));
+ localStorage.setItem(LSPRE() + '_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
+ localStorage.setItem(XPKEY(), String(STATE.xp));
+ } catch (e) {}
+}
+function achLabel(id) { return (M6.achLabels && M6.achLabels[id]) || id; }
+
+function bumpProgress(key, delta) {
+ STATE.progress[key] = Math.max(0, Math.min(100, (STATE.progress[key] || 0) + delta));
+ saveProgress(); refreshProgressUI();
+ if (STATE.progress[key] >= 50) markParaRead(key);
+ if (STATE.progress[key] >= 100) {
+ achievement(key + '_done');
+ var fin = paras().filter(function (p) { return p.final; }).map(function (p) { return p.id; });
+ if (fin.indexOf(key) >= 0 && M6.finalAch) achievement(M6.finalAch[0], M6.finalAch[1]);
+ }
+}
+
+/* ====================================================== SERVER SYNC */
+var _markedRead = new Set();
+var _pendingProgressBody = null, _progressTimer = null;
+function _flushProgress() {
+ var body = _pendingProgressBody; _pendingProgressBody = null; if (!body) return;
+ var tok = (window.LS && LS.getToken) ? LS.getToken() : ''; if (!tok) return;
+ fetch('/api/textbooks/' + M6.slug + '/progress', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + tok }, body: JSON.stringify(body), keepalive: true }).catch(function () {});
+}
+function _queueProgress(patch) {
+ _pendingProgressBody = Object.assign(_pendingProgressBody || {}, patch);
+ if (_progressTimer) clearTimeout(_progressTimer);
+ _progressTimer = setTimeout(_flushProgress, 600);
+}
+function markLastPara(id) { _queueProgress({ last_para: id }); }
+function markParaRead(id) { if (_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({ mark_read: id }); }
+window.addEventListener('beforeunload', _flushProgress);
+function loadServerReadState() {
+ var tok = (window.LS && LS.getToken) ? LS.getToken() : ''; if (!tok) return;
+ fetch('/api/textbooks/' + M6.slug, { headers: { 'Authorization': 'Bearer ' + tok } })
+ .then(function (r) { return r.ok ? r.json() : null; })
+ .then(function (d) {
+ if (!d || !d.progress) return;
+ (d.progress.read || []).forEach(function (k) { _markedRead.add(k); if ((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100; });
+ saveProgress(); refreshProgressUI();
+ }).catch(function () {});
+}
+
+/* ============================================================ XP */
+function addXp(n, src) {
+ if (!n) return;
+ var prev = STATE.level;
+ STATE.xp = Math.max(0, (STATE.xp || 0) + n);
+ STATE.level = calcLevel(STATE.xp);
+ saveProgress(); refreshProgressUI();
+ if (window.LS && window.LS.xp) window.LS.xp.add(n, (M6.slug || 'math6') + '-' + (src || 'misc'));
+ if (STATE.level > prev) {
+ popup('Уровень ' + STATE.level + '!');
+ if (window.confetti) try { confetti(); } catch (e) {}
+ }
+}
+function refreshProgressUI() {
+ var sum = 0; paras().forEach(function (p) { sum += (STATE.progress[p.id] || 0); });
+ var tot = Math.round(sum / total());
+ var f = document.getElementById('hero-hp-fill'); if (f) f.style.width = tot + '%';
+ var t = document.getElementById('hero-hp-text'); if (t) t.textContent = tot + '% пройдено';
+ document.querySelectorAll('[data-prog-card]').forEach(function (el) {
+ var k = el.dataset.progCard; var fl = el.querySelector('.psel-prog-fill'); if (fl) fl.style.width = (STATE.progress[k] || 0) + '%';
+ });
+ var xpBadge = document.getElementById('hero-xp-badge');
+ if (xpBadge) xpBadge.innerHTML = ' Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP';
+ if (STATE.current && document.getElementById('sidebar-content')) { try { buildSidebar(STATE.current); } catch (e) {} }
+}
+function popup(text, gold) {
+ var pop = document.getElementById('ach-popup'); if (!pop) return;
+ document.getElementById('ach-text').textContent = text;
+ pop.classList.add('show'); setTimeout(function () { pop.classList.remove('show'); }, gold ? 3300 : 2600);
+}
+function achievement(id, text) {
+ if (STATE.achievements.has(id)) return;
+ if (!text && !(M6.achLabels && M6.achLabels[id])) return; /* неизвестные id игнорируем */
+ STATE.achievements.set(id, text || achLabel(id));
+ saveProgress(); popup(text || achLabel(id), true); addXp(20, 'ach-' + id);
+}
+
+/* ================================================ SECTIONS */
+function buildSections() {
+ var host = document.getElementById('sections'); if (!host) return; /* статичные секции уже в HTML */
+ if (host.dataset.built) return; host.dataset.built = '1';
+ var html = '';
+ paras().forEach(function (p) {
+ var wm = p.wm || (p.final ? '★' : (M6.wm || ''));
+ var numCls = p.final ? ' style="background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri))"' : '';
+ html += '' + p.name + '