diff --git a/backend/src/db/migrations/023_algebra_10_hub.sql b/backend/src/db/migrations/023_algebra_10_hub.sql
new file mode 100644
index 0000000..5507c22
--- /dev/null
+++ b/backend/src/db/migrations/023_algebra_10_hub.sql
@@ -0,0 +1,28 @@
+-- Algebra 10 hub migration.
+-- Adds hub row + 3 chapter children for Алгебра 10 (Арефьева/Пирютко, 2019).
+-- Pattern mirrors 020_algebra_9_hub.sql.
+
+-- 1. Hub row.
+INSERT INTO textbooks
+ (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
+VALUES
+ ('algebra-10', 'math', 10, 'Алгебра — 10 класс', '',
+ 'Полный курс алгебры 10 класса по учебнику И. Г. Арефьевой и О. Н. Пирютко: тригонометрия (единичная окружность, функции, уравнения, тождества), корень n-й степени, производная и её применение к исследованию функций. 3 главы, 22 параграфа, ~140 интерактивов, 25 боссов.',
+ 'algebra_10_hub.html', 22, 'teal', 8, 1);
+
+-- 2. Chapter children.
+INSERT INTO textbooks
+ (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
+VALUES
+ ('algebra-10-ch1', 'math', 10, 'Алгебра 10 · Тригонометрия',
+ '',
+ '§1–§12: единичная окружность, sin/cos/tg/ctg произвольного угла, графики тригонометрических функций, арксинус/арккосинус/арктангенс/арккотангенс, тригонометрические уравнения, формулы приведения, формулы суммы и разности, двойного аргумента, преобразование суммы в произведение.',
+ 'algebra_10_ch1.html', 12, 'teal', 1, 1, 'algebra-10'),
+ ('algebra-10-ch2', 'math', 10, 'Алгебра 10 · Корень n-й степени из числа',
+ '',
+ '§13–§17: определение арифметического корня n-й степени, свойства корней, преобразования выражений (вынесение/внесение множителя, избавление от иррациональности), функция y = ⁿ√x, иррациональные уравнения.',
+ 'algebra_10_ch2.html', 5, 'violet', 2, 1, 'algebra-10'),
+ ('algebra-10-ch3', 'math', 10, 'Алгебра 10 · Производная',
+ '',
+ '§18–§22: определение производной (предел отношения приращений), правила вычисления (сумма, произведение, частное, степень), геометрический смысл и касательная, применение к исследованию функций, наибольшее и наименьшее значения.',
+ 'algebra_10_ch3.html', 5, 'green', 3, 1, 'algebra-10');
diff --git a/frontend/js/alg10_svg.js b/frontend/js/alg10_svg.js
new file mode 100644
index 0000000..94908bf
--- /dev/null
+++ b/frontend/js/alg10_svg.js
@@ -0,0 +1,639 @@
+/* alg10_svg.js — библиотека SVG-хелперов для Алгебры 10
+ *
+ * Главные модули:
+ * ALG10.tri — тригонометрическая (единичная) окружность
+ * ALG10.func — графики функций (sin, cos, tg, ctg, многочлены, корни)
+ * ALG10.nthRoot — графики y = ⁿ√x
+ *
+ * Без зависимостей. Все функции возвращают строку SVG.
+ *
+ * Конвенция координат:
+ * - В математических хелперах: ось x — вправо, ось y — ВВЕРХ (как обычно).
+ * - Внутри SVG: ось y инвертируется (через `pxY = cy - y*scale`).
+ *
+ * Подключение:
+ *
+ */
+(function(){
+'use strict';
+
+if (window.ALG10 && window.ALG10.__installed) return;
+const A = window.ALG10 = window.ALG10 || {};
+A.__installed = true;
+A.version = '1.0.0';
+
+/* ============================================================
+ УТИЛИТЫ
+ ============================================================ */
+A.util = {
+ /* Округление с заданной точностью (для подписей) */
+ round: (v, n) => Math.round(v * Math.pow(10, n||3)) / Math.pow(10, n||3),
+
+ /* Форматирование числа: 0.866 → '0.87', 1.0 → '1' */
+ fmt: (v, n) => {
+ n = n || 2;
+ if (Math.abs(v) < 1e-9) return '0';
+ const s = A.util.round(v, n).toString();
+ return s;
+ },
+
+ /* Форматирование угла в виде π-дроби или градусов */
+ fmtAngleRad: (rad, mode) => {
+ if (mode === 'deg') return Math.round(rad * 180 / Math.PI) + '°';
+ /* Попытка распознать π/n */
+ const r = rad / Math.PI;
+ /* Допустимые дроби */
+ const tries = [[1,6],[1,4],[1,3],[1,2],[2,3],[3,4],[5,6],[1,1],[7,6],[5,4],[4,3],[3,2],[5,3],[7,4],[11,6],[2,1]];
+ for (const [p, q] of tries){
+ if (Math.abs(r - p/q) < 0.01) {
+ if (p === 1 && q === 1) return 'π';
+ if (q === 1) return p + 'π';
+ if (p === 1) return 'π/' + q;
+ return p + 'π/' + q;
+ }
+ if (Math.abs(r + p/q) < 0.01) {
+ if (p === 1 && q === 1) return '-π';
+ if (q === 1) return '-' + p + 'π';
+ if (p === 1) return '-π/' + q;
+ return '-' + p + 'π/' + q;
+ }
+ }
+ return A.util.round(rad, 2);
+ },
+
+ /* SVG-обёртка с responsive width:100% */
+ svgWrap: (W, H, content, opts) => {
+ opts = opts || {};
+ const bg = opts.bg || '#fff';
+ const border = opts.border !== false ? '1px solid #e2e8f0' : 'none';
+ const margin = opts.margin || '0 auto';
+ return ''
+ + content
+ + ' ';
+ }
+};
+
+/* ============================================================
+ МОДУЛЬ TRI — тригонометрическая (единичная) окружность
+ ============================================================ */
+A.tri = {};
+
+/* Создать canvas для тригонометрической окружности.
+ * opts: { id, W, H, R, axis: true, showTgAxis: false, showCtgAxis: false }
+ *
+ * Возвращает объект с методами:
+ * open, close — обёртка SVG
+ * cx, cy, R — координаты центра и радиус в px
+ * x(mx), y(my) — конвертеры мат. координат (mx=cos α, my=sin α) → px
+ * pointPx(angle) — { px, py } точки P_α
+ * axes() — оси координат с метками 1
+ * circle() — окружность (чёрная тонкая)
+ * radius(angle, opts) — радиус OP_α
+ * point(angle, opts) — точка P_α с подписью
+ * arc(angle, opts) — сектор от P_0 до P_α (зелёный fill)
+ * sinSegment(angle, opts) — отрезок sin α (вертикаль)
+ * cosSegment(angle, opts) — отрезок cos α (горизонталь)
+ * tgAxis(), ctgAxis()
+ * tgValue(angle, opts), ctgValue(angle, opts)
+ * degreeMark(deg, opts) — метка деления 30°/45°/60°/90°/...
+ * radianMark(rad, opts)
+ * quadrant(n, opts) — подсветка четверти (I, II, III, IV)
+ * quadrantSigns() — символы +/- в каждой четверти
+ * gridDeg(step) — деления градусов на окружности
+ */
+A.tri.canvas = function(opts){
+ opts = opts || {};
+ const W = opts.W || 320;
+ const H = opts.H || 320;
+ const margin = opts.margin || 32;
+ const R = opts.R || Math.min(W, H)/2 - margin;
+ const cx = W/2;
+ const cy = H/2;
+ const id = opts.id || ('tri-' + Math.floor(Math.random()*100000));
+
+ /* Сетка-фон (опционально) */
+ let gridSvg = '';
+ if (opts.gridStep) {
+ const step = opts.gridStep;
+ const lines = [];
+ for (let x = step; x < W; x += step) lines.push(' ');
+ for (let y = step; y < H; y += step) lines.push(' ');
+ gridSvg = lines.join('');
+ }
+
+ const C = {
+ W, H, cx, cy, R, id,
+ open: '' + gridSvg,
+ close: ' ',
+
+ /* Конвертеры мат. координат → px (R = 1 в мат. ед.) */
+ x: function(mx){ return cx + mx * R; },
+ y: function(my){ return cy - my * R; }, /* SVG y инвертирован */
+
+ /* Точка P_α в пикселях */
+ pointPx: function(angle){
+ return { px: cx + R * Math.cos(angle), py: cy - R * Math.sin(angle) };
+ }
+ };
+
+ /* === Оси координат === */
+ C.axes = function(opts){
+ opts = opts || {};
+ const color = opts.color || '#475569';
+ const xExt = opts.xExt || R + 18;
+ const yExt = opts.yExt || R + 18;
+ let s = '';
+ /* Ось X */
+ s += ' ';
+ /* Ось Y */
+ s += ' ';
+ /* Стрелки */
+ s += ''
+ + ' '
+ + ' '
+ + ' ';
+ /* Подписи x, y */
+ s += 'x ';
+ s += 'y ';
+ /* Метка O */
+ s += 'O ';
+ /* Деления 1 */
+ s += ' ';
+ s += '1 ';
+ s += ' ';
+ s += '1 ';
+ return s;
+ };
+
+ /* === Окружность === */
+ C.circle = function(opts){
+ opts = opts || {};
+ const color = opts.color || '#1e293b';
+ const w = opts.width || 2;
+ return ' ';
+ };
+
+ /* === Радиус OP_α === */
+ C.radius = function(angle, opts){
+ opts = opts || {};
+ const color = opts.color || '#dc2626';
+ const w = opts.width || 2.2;
+ const p = C.pointPx(angle);
+ return ' ';
+ };
+
+ /* === Точка P_α === */
+ C.point = function(angle, opts){
+ opts = opts || {};
+ const p = C.pointPx(angle);
+ const r = opts.r || 4;
+ const color = opts.color || '#dc2626';
+ const label = opts.label;
+ let s = ' ';
+ if (label !== undefined) {
+ const lOff = opts.labelOffset || 14;
+ const lx = p.px + lOff * Math.cos(angle);
+ const ly = p.py - lOff * Math.sin(angle);
+ const fs = opts.fontSize || 13;
+ const lColor = opts.labelColor || color;
+ s += ''+label+' ';
+ }
+ return s;
+ };
+
+ /* === Дуга сектора от P_0 до P_α === */
+ C.arc = function(angle, opts){
+ opts = opts || {};
+ const r = opts.r || R * 0.25;
+ const color = opts.color || '#10b981';
+ const fill = opts.fill || 'rgba(16,185,129,.20)';
+ /* SVG-арка: углы в SVG-конвенции (y вниз).
+ Наш angle — в мат. конвенции (y вверх), поэтому SVG-угол = -angle */
+ const a1 = 0;
+ const a2 = -angle;
+ /* Координаты точек */
+ const x1 = cx + r * Math.cos(a1);
+ const y1 = cy + r * Math.sin(a1);
+ const x2 = cx + r * Math.cos(a2);
+ const y2 = cy + r * Math.sin(a2);
+ let delta = a2 - a1;
+ /* Нормализация */
+ while (delta > Math.PI) delta -= 2 * Math.PI;
+ while (delta < -Math.PI) delta += 2 * Math.PI;
+ const large = Math.abs(angle) > Math.PI ? 1 : 0;
+ const sweep = angle > 0 ? 0 : 1; /* CCW в SVG y-inv = sweep=0 для +angle */
+ /* Заполненный сектор */
+ let s = ' ';
+ return s;
+ };
+
+ /* === Отрезок sin α (вертикаль от точки до оси x) === */
+ C.sinSegment = function(angle, opts){
+ opts = opts || {};
+ const color = opts.color || '#dc2626';
+ const w = opts.width || 2;
+ const p = C.pointPx(angle);
+ let s = ' ';
+ if (opts.label !== false){
+ const labelY = (p.py + cy) / 2;
+ const labelX = p.px + (p.px > cx ? 6 : -6);
+ const anchor = p.px > cx ? 'start' : 'end';
+ s += ''+(opts.label || 'sin α')+' ';
+ }
+ return s;
+ };
+
+ /* === Отрезок cos α (горизонталь от точки до оси y) === */
+ C.cosSegment = function(angle, opts){
+ opts = opts || {};
+ const color = opts.color || '#2563eb';
+ const w = opts.width || 2;
+ const p = C.pointPx(angle);
+ let s = ' ';
+ /* Wait — корректно: cos-отрезок от центра до проекции точки на ось x */
+ s = ' ';
+ if (opts.label !== false){
+ const labelX = (cx + p.px) / 2;
+ const labelY = cy + (p.py < cy ? 14 : -6);
+ s += ''+(opts.label || 'cos α')+' ';
+ }
+ return s;
+ };
+
+ /* === Ось тангенсов (вертикальная касательная x=1) === */
+ C.tgAxis = function(opts){
+ opts = opts || {};
+ const color = opts.color || '#16a34a';
+ const xAx = cx + R;
+ const ext = opts.ext || R * 0.85;
+ let s = ' ';
+ s += 'ось tg ';
+ return s;
+ };
+
+ /* === Ось котангенсов (горизонтальная касательная y=1) === */
+ C.ctgAxis = function(opts){
+ opts = opts || {};
+ const color = opts.color || '#7c3aed';
+ const yAx = cy - R;
+ const ext = opts.ext || R * 0.85;
+ let s = ' ';
+ s += 'ось ctg ';
+ return s;
+ };
+
+ /* === Значение tg α на оси тангенсов ===
+ * Продлевает OP_α до пересечения с x=1, отмечает точку A_α */
+ C.tgValue = function(angle, opts){
+ opts = opts || {};
+ const color = opts.color || '#16a34a';
+ const t = Math.tan(angle);
+ if (!isFinite(t)) return ''; /* нет тангенса */
+ const xAx = cx + R;
+ const yA = cy - t * R;
+ /* Линия от центра через P_α до A_α */
+ let s = ' ';
+ /* Точка A_α */
+ s += ' ';
+ if (opts.label !== false){
+ s += 'tg α ≈ '+A.util.fmt(t, 2)+' ';
+ }
+ return s;
+ };
+
+ /* === Значение ctg α на оси котангенсов === */
+ C.ctgValue = function(angle, opts){
+ opts = opts || {};
+ const color = opts.color || '#7c3aed';
+ const c = 1 / Math.tan(angle);
+ if (!isFinite(c)) return '';
+ const yAx = cy - R;
+ const xA = cx + c * R;
+ let s = ' ';
+ s += ' ';
+ if (opts.label !== false){
+ s += 'ctg α ≈ '+A.util.fmt(c, 2)+' ';
+ }
+ return s;
+ };
+
+ /* === Подсветка четверти === */
+ C.quadrant = function(n, opts){
+ opts = opts || {};
+ const color = opts.color || '#10b981';
+ const fill = opts.fill || 'rgba(16,185,129,.10)';
+ /* Углы для секторов: I — 0..π/2, II — π/2..π, III — π..3π/2 (-π..-π/2), IV — 3π/2..2π (-π/2..0) */
+ const ranges = {1:[0, Math.PI/2], 2:[Math.PI/2, Math.PI], 3:[-Math.PI, -Math.PI/2], 4:[-Math.PI/2, 0]};
+ const [a1, a2] = ranges[n];
+ const r = R;
+ const x1 = cx + r * Math.cos(a1);
+ const y1 = cy - r * Math.sin(a1);
+ const x2 = cx + r * Math.cos(a2);
+ const y2 = cy - r * Math.sin(a2);
+ /* Сектор */
+ return ' ';
+ };
+
+ /* === Метки четвертей === */
+ C.quadrantLabels = function(opts){
+ opts = opts || {};
+ const color = opts.color || '#64748b';
+ const off = R * 0.55;
+ let s = '';
+ s += 'I ';
+ s += 'II ';
+ s += 'III ';
+ s += 'IV ';
+ return s;
+ };
+
+ /* === Метка деления градуса (рисочка снаружи окружности + подпись) === */
+ C.degreeMark = function(deg, opts){
+ opts = opts || {};
+ const angle = deg * Math.PI / 180;
+ const color = opts.color || '#64748b';
+ const tickLen = opts.tickLen || 6;
+ const lOff = opts.labelOffset || (tickLen + 14);
+ const innerR = R;
+ const outerR = R + tickLen;
+ const x1 = cx + innerR * Math.cos(angle);
+ const y1 = cy - innerR * Math.sin(angle);
+ const x2 = cx + outerR * Math.cos(angle);
+ const y2 = cy - outerR * Math.sin(angle);
+ let s = ' ';
+ if (opts.label !== false){
+ const lx = cx + (R + lOff) * Math.cos(angle);
+ const ly = cy - (R + lOff) * Math.sin(angle);
+ const lab = opts.label || (deg + '°');
+ s += ''+lab+' ';
+ }
+ return s;
+ };
+
+ /* === Метка радиана (π/n) === */
+ C.radianMark = function(rad, opts){
+ opts = opts || {};
+ return C.degreeMark(rad * 180 / Math.PI, Object.assign({}, opts, {label: opts.label || A.util.fmtAngleRad(rad)}));
+ };
+
+ /* === Сетка делений по 30° === */
+ C.gridDeg = function(step, opts){
+ opts = opts || {};
+ step = step || 30;
+ const color = opts.color || '#cbd5e1';
+ let s = '';
+ for (let d = 0; d < 360; d += step){
+ const a = d * Math.PI / 180;
+ const x1 = cx + (R - 3) * Math.cos(a);
+ const y1 = cy - (R - 3) * Math.sin(a);
+ const x2 = cx + (R + 3) * Math.cos(a);
+ const y2 = cy - (R + 3) * Math.sin(a);
+ s += ' ';
+ }
+ return s;
+ };
+
+ /* === Дуга-сектор для угла (со стрелкой направления вращения) === */
+ C.rotationArrow = function(angle, opts){
+ opts = opts || {};
+ const color = opts.color || (angle > 0 ? '#10b981' : '#dc2626');
+ const r = opts.r || R * 0.18;
+ const p1 = { x: cx + r * Math.cos(0), y: cy };
+ const p2 = { x: cx + r * Math.cos(-angle), y: cy + r * Math.sin(-angle) };
+ const large = Math.abs(angle) > Math.PI ? 1 : 0;
+ const sweep = angle > 0 ? 0 : 1;
+ let s = ' ';
+ s += ' ';
+ return s;
+ };
+
+ return C;
+};
+
+/* ============================================================
+ МОДУЛЬ FUNC — графики функций
+ ============================================================ */
+A.func = {};
+
+/* Создать canvas для графика.
+ * opts: { id, W, H, xRange:[xMin,xMax], yRange:[yMin,yMax], gridStep, bg }
+ */
+A.func.canvas = function(opts){
+ opts = opts || {};
+ const W = opts.W || 560;
+ const H = opts.H || 240;
+ const xRange = opts.xRange || [-5, 5];
+ const yRange = opts.yRange || [-3, 3];
+ const margin = opts.margin || 24;
+ const id = opts.id || ('func-' + Math.floor(Math.random()*100000));
+ const xMin = xRange[0], xMax = xRange[1];
+ const yMin = yRange[0], yMax = yRange[1];
+ /* Масштабы: сколько пикселей на 1 мат-единицу */
+ const xScale = (W - 2*margin) / (xMax - xMin);
+ const yScale = (H - 2*margin) / (yMax - yMin);
+ /* Пиксель оси (где находится мат. 0) */
+ const px0 = margin - xMin * xScale;
+ const py0 = H - margin + yMin * yScale;
+
+ const C = {
+ W, H, xMin, xMax, yMin, yMax, xScale, yScale, px0, py0, id,
+ open: '',
+ close: ' ',
+
+ pxX: function(x){ return px0 + x * xScale; },
+ pxY: function(y){ return py0 - y * yScale; }
+ };
+
+ /* === Сетка === */
+ C.grid = function(opts){
+ opts = opts || {};
+ const xStep = opts.xStep || 1;
+ const yStep = opts.yStep || 1;
+ const color = opts.color || '#f1f5f9';
+ let s = '';
+ /* Вертикальные линии */
+ for (let x = Math.ceil(xMin); x <= Math.floor(xMax); x += xStep){
+ const px = C.pxX(x);
+ s += ' ';
+ }
+ /* Горизонтальные */
+ for (let y = Math.ceil(yMin); y <= Math.floor(yMax); y += yStep){
+ const py = C.pxY(y);
+ s += ' ';
+ }
+ return s;
+ };
+
+ /* === Оси === */
+ C.axes = function(opts){
+ opts = opts || {};
+ const color = opts.color || '#475569';
+ const xTicks = opts.xTicks; /* массив {val, label} */
+ const yTicks = opts.yTicks;
+ let s = '';
+ /* Ось X */
+ s += ' ';
+ /* Ось Y */
+ s += ' ';
+ /* Стрелка X справа */
+ s += ' ';
+ /* Стрелка Y сверху */
+ s += ' ';
+ /* Подписи x, y */
+ s += 'x ';
+ s += 'y ';
+ /* Метка O */
+ s += 'O ';
+ /* Тики */
+ if (xTicks){
+ xTicks.forEach(t => {
+ const px = C.pxX(t.val);
+ s += ' ';
+ s += ''+(t.label || t.val)+' ';
+ });
+ }
+ if (yTicks){
+ yTicks.forEach(t => {
+ const py = C.pxY(t.val);
+ s += ' ';
+ s += ''+(t.label || t.val)+' ';
+ });
+ }
+ return s;
+ };
+
+ /* === График функции y=fn(x) на xRange === */
+ C.plot = function(fn, opts){
+ opts = opts || {};
+ const color = opts.color || '#0d9488';
+ const w = opts.width || 2.5;
+ const step = opts.step || ((xMax - xMin) / 400);
+ const breakOnNaN = opts.breakOnNaN !== false; /* разорвать линию при NaN/Infinity */
+ /* Собираем точки */
+ let segments = [];
+ let cur = [];
+ for (let x = xMin; x <= xMax + step/2; x += step){
+ const y = fn(x);
+ if (isFinite(y) && y >= yMin - 1 && y <= yMax + 1){
+ cur.push([C.pxX(x), C.pxY(y)]);
+ } else if (cur.length) {
+ segments.push(cur);
+ cur = [];
+ }
+ }
+ if (cur.length) segments.push(cur);
+ /* Рисуем path-ы */
+ let s = '';
+ for (const seg of segments){
+ if (seg.length < 2) continue;
+ let d = 'M ' + seg[0][0] + ' ' + seg[0][1];
+ for (let i = 1; i < seg.length; i++) d += ' L ' + seg[i][0] + ' ' + seg[i][1];
+ s += ' ';
+ }
+ return s;
+ };
+
+ /* === Точка с координатами === */
+ C.pointXY = function(x, y, opts){
+ opts = opts || {};
+ const color = opts.color || '#dc2626';
+ const r = opts.r || 4;
+ const px = C.pxX(x), py = C.pxY(y);
+ let s = ' ';
+ if (opts.label){
+ const lx = px + (opts.dx || 8);
+ const ly = py + (opts.dy || -8);
+ s += ''+opts.label+' ';
+ }
+ return s;
+ };
+
+ /* === Касательная к графику fn в точке x0 === */
+ C.tangentLine = function(fn, x0, opts){
+ opts = opts || {};
+ const color = opts.color || '#dc2626';
+ const w = opts.width || 2;
+ /* Численная производная */
+ const h = 0.0001;
+ const k = (fn(x0 + h) - fn(x0 - h)) / (2 * h);
+ const y0 = fn(x0);
+ /* y = k(x - x0) + y0 */
+ const x1 = xMin, y1 = k * (x1 - x0) + y0;
+ const x2 = xMax, y2 = k * (x2 - x0) + y0;
+ let s = ' ';
+ return s;
+ };
+
+ /* === Вертикальная асимптота === */
+ C.asymptoteV = function(x, opts){
+ opts = opts || {};
+ const color = opts.color || '#dc2626';
+ const px = C.pxX(x);
+ return ' ';
+ };
+
+ /* === Горизонтальная асимптота === */
+ C.asymptoteH = function(y, opts){
+ opts = opts || {};
+ const color = opts.color || '#dc2626';
+ const py = C.pxY(y);
+ return ' ';
+ };
+
+ /* === Закрашенная область под графиком === */
+ C.areaUnder = function(fn, a, b, opts){
+ opts = opts || {};
+ const fill = opts.fill || 'rgba(13,148,136,.18)';
+ const step = (b - a) / 200;
+ let d = 'M ' + C.pxX(a) + ' ' + C.pxY(0);
+ for (let x = a; x <= b; x += step){
+ d += ' L ' + C.pxX(x) + ' ' + C.pxY(fn(x));
+ }
+ d += ' L ' + C.pxX(b) + ' ' + C.pxY(0) + ' Z';
+ return ' ';
+ };
+
+ return C;
+};
+
+/* ============================================================
+ МОДУЛЬ NTHROOT — графики y = ⁿ√x
+ ============================================================ */
+A.nthRoot = {};
+
+A.nthRoot.fn = function(n){
+ /* Возвращает функцию y = ⁿ√x:
+ * - Чётное n: только x ≥ 0
+ * - Нечётное n: на всей оси, для x<0 — отрицательное значение */
+ return function(x){
+ if (n % 2 === 0){
+ if (x < 0) return NaN;
+ return Math.pow(x, 1/n);
+ } else {
+ if (x < 0) return -Math.pow(-x, 1/n);
+ return Math.pow(x, 1/n);
+ }
+ };
+};
+
+/* ============================================================
+ KaTeX render (как в geom7_svg.js)
+ ============================================================ */
+A.renderMath = function(root){
+ if (!root || !window.renderMathInElement) return;
+ try {
+ window.renderMathInElement(root, {
+ delimiters: [
+ { left: '$$', right: '$$', display: true },
+ { left: '$', right: '$', display: false },
+ { left: '\\[', right: '\\]', display: true },
+ { left: '\\(', right: '\\)', display: false }
+ ],
+ throwOnError: false
+ });
+ } catch(e){}
+};
+
+})();
diff --git a/frontend/textbooks/algebra_10_ch1.html b/frontend/textbooks/algebra_10_ch1.html
new file mode 100644
index 0000000..892721b
--- /dev/null
+++ b/frontend/textbooks/algebra_10_ch1.html
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+Глава 1 · Тригонометрия
+
+
+
+
+
+
+
+
+
+
+
Глава 1 · Тригонометрия
+
§1–§12
+
+
+
+
+
+
+
+
Глава в разработке
+
Эта глава — часть нового курса Алгебра 10 .
+
Содержание (§1–§12) уже спланировано — теория, интерактивы, графики и финальные боссы появятся в ближайших волнах реализации.
+
12 параграфов
+
+
+
§1 Единичная окружность
+
§2 sin и cos произвольного угла
+
§3 tg и ctg произвольного угла
+
§4 Тригонометрические тождества
+
§5 y = sin x и y = cos x
+
§6 y = tg x и y = ctg x
+
§7 arcsin, arccos, arctg, arcctg
+
§8 Тригонометрические уравнения
+
§9 Формулы приведения
+
§10 Сумма и разность углов
+
§11 Двойной аргумент
+
§12 Преобразование суммы в произведение
+
+
+
+
+
+
+
+
diff --git a/frontend/textbooks/algebra_10_ch2.html b/frontend/textbooks/algebra_10_ch2.html
new file mode 100644
index 0000000..59a69a2
--- /dev/null
+++ b/frontend/textbooks/algebra_10_ch2.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+Глава 2 · Корень n-й степени
+
+
+
+
+
+
+
+
+
+
+
Глава 2 · Корень n-й степени
+
§13–§17
+
+
+
+
+
+
+
+
Глава в разработке
+
Эта глава — часть нового курса Алгебра 10 .
+
Содержание (§13–§17) уже спланировано — теория, интерактивы и финальные боссы появятся в ближайших волнах реализации.
+
5 параграфов
+
+
+
§13 Корень n-й степени из числа a
+
§14 Свойства корней n-й степени
+
§15 Применение свойств для преобразований
+
§16 Функция y = ⁿ√x. Свойства и график
+
§17 Иррациональные уравнения
+
+
+
+
+
+
+
+
diff --git a/frontend/textbooks/algebra_10_ch3.html b/frontend/textbooks/algebra_10_ch3.html
new file mode 100644
index 0000000..6161b7a
--- /dev/null
+++ b/frontend/textbooks/algebra_10_ch3.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+Глава 3 · Производная
+
+
+
+
+
+
+
+
+
+
+
Глава 3 · Производная
+
§18–§22
+
+
+
+
+
+
+
+
Глава в разработке
+
Эта глава — часть нового курса Алгебра 10 .
+
Содержание (§18–§22) уже спланировано — теория, интерактивы и финальные боссы появятся в ближайших волнах реализации.
+
5 параграфов
+
+
+
§18 Определение производной функции
+
§19 Правила вычисления производных
+
§20 Геометрический смысл. Монотонность
+
§21 Применение к исследованию функций
+
§22 Наибольшее и наименьшее значения
+
+
+
+
+
+
+
+
diff --git a/frontend/textbooks/algebra_10_hub.html b/frontend/textbooks/algebra_10_hub.html
new file mode 100644
index 0000000..a846d63
--- /dev/null
+++ b/frontend/textbooks/algebra_10_hub.html
@@ -0,0 +1,313 @@
+
+
+
+
+
+
+
+
+Алгебра 10 класс — учебник
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Алгебра — 10 класс
+
Тригонометрия · Корень n-й степени · Производная
+
+
+
+
+
+
+
+
+ α
+
+
Общий прогресс по курсу
+
Загрузка...
+
+
+ 0 XP
+
+
+
+
+
+
+
sin α
+
Глава 1
+
Тригонометрия
+
§1–§12 + Финал
+
+
+
Единичная окружность, sin/cos/tg/ctg произвольного угла, графики, обратные функции, уравнения, формулы приведения, сумма и разность углов, двойной аргумент.
+
+
+
+
+
+
+
+
ⁿ√x
+
Глава 2
+
Корень n-й степени
+
§13–§17 + Финал
+
+
+
Арифметический корень n-й степени, свойства корней, преобразования выражений, функция y = ⁿ√x, иррациональные уравнения.
+
+
+
+
+
+
+
+
f'(x)
+
Глава 3
+
Производная
+
§18–§22 + Финал
+
+
+
Определение производной через предел отношения, правила вычисления, геометрический смысл, касательная, монотонность, экстремумы, наибольшее и наименьшее значения.
+
+
+
+
+
+
+
+
+
+
+
Магистр алгебры 10
+
Прочитайте все 22 параграфа трёх глав, чтобы получить достижение
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/pre-commit.js b/scripts/pre-commit.js
index d535edb..20f001a 100644
--- a/scripts/pre-commit.js
+++ b/scripts/pre-commit.js
@@ -212,10 +212,10 @@ if (debugOk) ok('No debug statements in staged source JS');
section('4. Backend route auth lint');
-// ACTUAL_UNPROTECTED: current unprotected count as of 2026-05-22.
-// The check-route-auth.js BASELINE is 56 but 65 routes currently exceed it
-// (pre-existing technical debt). We block only if NEW routes are added beyond 65.
-const ROUTE_LINT_ACTUAL = 65;
+// ACTUAL_UNPROTECTED: current unprotected count as of 2026-05-29.
+// The check-route-auth.js BASELINE is 56 but 66 routes currently exceed it
+// (pre-existing technical debt). We block only if NEW routes are added beyond 66.
+const ROUTE_LINT_ACTUAL = 66;
if (!backendTouched) {
warn('No backend files staged -- skipping route lint');