diff --git a/frontend/textbooks/algebra_8_ch3.html b/frontend/textbooks/algebra_8_ch3.html
index 0f3c733..1adf3b1 100644
--- a/frontend/textbooks/algebra_8_ch3.html
+++ b/frontend/textbooks/algebra_8_ch3.html
@@ -1738,8 +1738,551 @@ function buildP16(){
refresh();
})();
}
-function buildP17stub(){ document.getElementById('p17-body').innerHTML = `
§ 17 — Квадратные неравенства. Метод интервалов Будет в Wave 3.
${secNav('p16','p18')}`; }
-function buildP18stub(){ document.getElementById('p18-body').innerHTML = `§ 18 — Дробно-рациональные неравенства Будет в Wave 3.
${secNav('p17','final3')}`; }
+function buildP17stub(){ buildP17(); }
+function buildP18stub(){ buildP18(); }
+
+/* ============================================================
+ § 17 — КВАДРАТНЫЕ НЕРАВЕНСТВА. МЕТОД ИНТЕРВАЛОВ
+ ============================================================ */
+function buildP17(){
+ const box = document.getElementById('p17-body');
+ let html = '';
+
+ html += makeCard('repeat','Повторение',null,`
+
+ Квадратное уравнение $ax^2 + bx + c = 0$ (Глава 2).
+ Дискриминант $D = b^2 - 4ac$, корни $x_{1,2} = \\dfrac{-b \\pm \\sqrt{D}}{2a}$.
+ Парабола: при $a > 0$ ветви вверх, при $a < 0$ — вниз.
+ `);
+
+ html += makeCard('theory','Что такое квадратное неравенство','17.1',`
+ Определение. Неравенство вида $ax^2 + bx + c \\gtrless 0$, $a \\neq 0$.
+ Геометрический смысл: найти, где парабола $y = ax^2 + bx + c$ выше или ниже оси OX (в зависимости от знака неравенства).
`);
+
+ html += makeCard('algo','Метод интервалов','17.2',`
+
+ Найти корни уравнения $ax^2 + bx + c = 0$.
+ Если $D < 0$ — знак выражения постоянен (совпадает со знаком $a$).
+ Если $D \\geq 0$ — отметить корни на числовой прямой.
+ Определить знак выражения на каждом интервале (можно подставить пробную точку).
+ Выбрать интервалы, удовлетворяющие неравенству.
+
+ Правило знаков для квадратного: при $a > 0$ — между корнями знак минус, вне — плюс. При $a < 0$ — наоборот.
`);
+
+ html += makeCard('example','Пример',null,`
+ Решить $x^2 - 5x + 6 > 0$.
+ Корни: $x_1 = 2$, $x_2 = 3$. $a = 1 > 0$ — парабола вверх.
+ Знаки: $+$ при $x<2$, $-$ при $23$.
+ Ответ: $x \\in (-\\infty;\\,2) \\cup (3;\\,+\\infty)$.
`);
+
+ /* INT 1 — Парабола + закраска */
+ html += widget('Парабола и знак','INTERACT 1','Двигай $a, b, c$. Парабола показывает, где выражение положительно (зелёное) и отрицательно (красное).',`
+
+ $a$ = 1
+ $b$ = -1
+ $c$ = -6
+
+
+
`);
+
+ /* INT 2 — Шаговый решатель */
+ html += widget('Метод интервалов: шаг за шагом','INTERACT 2','Введите $a, b, c$ для $ax^2 + bx + c \\geq 0$ и нажимайте «Дальше».',`
+
+ $a$ =
+ $b$ =
+ $c$ =
+ Старт
+ Дальше
+ Сначала
+
+
`);
+
+ /* INT 3 — Тренажёр */
+ html += widget('Тренажёр квадратных неравенств','INTERACT 3','Решите неравенство и выберите правильный промежуток-ответ.',`
+ Задача 1 / 6 Очки: 0
+
+
+
+ Начать `);
+
+ /* INT 4 — Drag: парабола ↔ ответ */
+ html += widget('Сопоставь параболу и неравенство','INTERACT 4','По описанию ситуации (направление ветвей + расположение корней) подбери решение неравенства $\\geq 0$.',`
+ ${DND_HINT_HTML}
+
+
+
$(-\\infty; x_1] \\cup [x_2; +\\infty)$
+
+
$\\mathbb{R}$ (все числа)
+
$\\emptyset$ (нет решений)
+
+ Проверить Сначала
+
`);
+
+ /* INT 5 — Знак между / вне корней */
+ html += widget('Где плюс, где минус?','INTERACT 5','Дана парабола. Кликни на интервал — раскрась знак.',`
+ Пусть $y = x^2 - 4x + 3$. Корни: $x_1 = 1$, $x_2 = 3$. Кликни по каждому интервалу и поставь знак.
+
+
`);
+
+ html += makeCard('class','Класс — решите',null,`
+
+ $x^2 - 6x + 5 \\leq 0$
+ $-x^2 + 4x - 3 > 0$
+ $x^2 + 2x + 5 > 0$ (без корней — особый случай)
+ $4x^2 - 9 \\geq 0$
+ `);
+
+ html += makeCard('home','Домашка',null,`
+
+ $x^2 - 7x + 10 < 0$
+ $3x^2 + 2x - 1 \\geq 0$
+ $x^2 + 1 \\leq 0$ (особый случай)
+ При каких $m$ неравенство $x^2 + 6x + m > 0$ верно для всех $x$?
+ `);
+
+ html += secNav('p16', 'p18');
+ box.innerHTML = html;
+ if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
+
+ /* INIT 1 — Парабола */
+ (function(){
+ const aE = document.getElementById('p17p-a'), bE = document.getElementById('p17p-b'), cE = document.getElementById('p17p-c');
+ const svg = document.getElementById('p17p-svg'), out = document.getElementById('p17p-out');
+ let done = false;
+ const W = 220, H = 160, x0 = W/2, y0 = H/2, sx = 14, sy = 14;
+ function refresh(){
+ const a = +aE.value, b = +bE.value, c = +cE.value;
+ document.getElementById('p17p-a-val').textContent = a;
+ document.getElementById('p17p-b-val').textContent = b;
+ document.getElementById('p17p-c-val').textContent = c;
+ let path = '';
+ for(let i = 0; i <= 240; i++){
+ const x = -W/(2*sx) + i * (W/sx) / 240;
+ const y = a*x*x + b*x + c;
+ const cx = x0 + x*sx, cy = y0 - y*sy;
+ path += (i === 0 ? 'M' : 'L') + cx.toFixed(2) + ',' + cy.toFixed(2) + ' ';
+ }
+ const D = b*b - 4*a*c;
+ const xs = D >= 0 ? [(-b - Math.sqrt(D))/(2*a), (-b + Math.sqrt(D))/(2*a)].sort((p,q)=>p-q) : [];
+ // фон зон
+ let s = '';
+ if(a !== 0){
+ if(D > 0){
+ // полосы зелёный/красный на оси
+ const x1 = x0 + xs[0]*sx, x2 = x0 + xs[1]*sx;
+ if(a > 0){
+ s += ' ';
+ s += ' ';
+ s += ' ';
+ } else {
+ s += ' ';
+ s += ' ';
+ s += ' ';
+ }
+ } else {
+ s += ' ';
+ }
+ }
+ s += ' ';
+ s += ' ';
+ s += ' ';
+ xs.forEach(r => { s += ' '; });
+ svg.innerHTML = s;
+ let info = '$D$ = ' + D.toFixed(2) + '
';
+ if(D > 0){
+ info += 'Корни: $x_1 = ' + fmt(xs[0]) + ',\\ x_2 = ' + fmt(xs[1]) + '$
';
+ if(a > 0) info += '$ax^2+bx+c > 0$: $x < ' + fmt(xs[0]) + '$ или $x > ' + fmt(xs[1]) + '$
$ax^2+bx+c < 0$: $' + fmt(xs[0]) + ' < x < ' + fmt(xs[1]) + '$
';
+ else info += '$ax^2+bx+c > 0$: $' + fmt(xs[0]) + ' < x < ' + fmt(xs[1]) + '$
$ax^2+bx+c < 0$: $x < ' + fmt(xs[0]) + '$ или $x > ' + fmt(xs[1]) + '$
';
+ } else if(D === 0){
+ const r = -b/(2*a);
+ info += 'Один корень: $x = ' + fmt(r) + '$
';
+ info += 'Парабола касается оси. Знак — везде ' + (a > 0 ? 'положителен (кроме точки $x = ' + fmt(r) + '$)' : 'отрицателен') + '.
';
+ } else {
+ info += 'Корней нет. Парабола ' + (a > 0 ? 'выше' : 'ниже') + ' оси.
';
+ info += 'Знак выражения везде ' + (a > 0 ? 'положителен' : 'отрицателен') + '.
';
+ }
+ out.innerHTML = info; renderMath(out);
+ if(!done){ done = true; setTimeout(()=>{ achievement('p17_parab'); bumpProgress('p17', 14); }, 300); }
+ }
+ [aE,bE,cE].forEach(e => e.addEventListener('input', refresh));
+ refresh();
+ })();
+
+ /* INIT 2 — Метод интервалов шаговый */
+ (function(){
+ const stage = document.getElementById('p17s-stage');
+ const goBtn = document.getElementById('p17s-go'), nextBtn = document.getElementById('p17s-next'), resetBtn = document.getElementById('p17s-reset');
+ let steps = [], idx = 0, awarded = false;
+ function build(a, b, c){
+ const arr = [];
+ arr.push('Дано: $' + a + 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' \\geq 0$');
+ const D = b*b - 4*a*c;
+ arr.push('Шаг 1. $D = b^2 - 4ac = ' + (b*b) + ' - ' + (4*a*c) + ' = ' + D + '$');
+ if(D < 0){
+ arr.push('Шаг 2. $D < 0$ — корней нет. Знак совпадает со знаком $a = ' + a + '$.');
+ if(a > 0) arr.push('Шаг 3. $a > 0$ — выражение всегда $> 0$, тем более $\\geq 0$. Ответ: $\\mathbb{R}$.');
+ else arr.push('Шаг 3. $a < 0$ — выражение всегда $< 0$, ни одна точка не удовлетворяет $\\geq 0$. Ответ: $\\emptyset$.');
+ } else if(D === 0){
+ const r = -b/(2*a);
+ arr.push('Шаг 2. $D = 0$ — один корень: $x = ' + fmt(r) + '$.');
+ if(a > 0) arr.push('Шаг 3. $a > 0$ — выражение всюду $\\geq 0$, равно нулю только в $x = ' + fmt(r) + '$. Ответ: $\\mathbb{R}$.');
+ else arr.push('Шаг 3. $a < 0$ — выражение всюду $\\leq 0$, равно нулю только в $x = ' + fmt(r) + '$. Ответ: $\\{' + fmt(r) + '\\}$.');
+ } else {
+ const x1 = (-b - Math.sqrt(D))/(2*a), x2 = (-b + Math.sqrt(D))/(2*a);
+ const lo = Math.min(x1, x2), hi = Math.max(x1, x2);
+ arr.push('Шаг 2. Корни: $x_1 = ' + fmt(lo) + ',\\ x_2 = ' + fmt(hi) + '$');
+ if(a > 0){
+ arr.push('Шаг 3. $a > 0$ — парабола вверх. Знак: $+$ при $x < x_1$, $-$ между корнями, $+$ при $x > x_2$.');
+ arr.push('Ответ: $x \\in (-\\infty;\\,' + fmt(lo) + '] \\cup [' + fmt(hi) + ';\\,+\\infty)$');
+ } else {
+ arr.push('Шаг 3. $a < 0$ — парабола вниз. Знак: $-$ вне, $+$ между корнями.');
+ arr.push('Ответ: $x \\in [' + fmt(lo) + ';\\,' + fmt(hi) + ']$');
+ }
+ }
+ return arr;
+ }
+ function render(){
+ stage.innerHTML = steps.slice(0, idx + 1).map(s => `${s}
`).join('');
+ renderMath(stage);
+ if(idx >= steps.length - 1){
+ nextBtn.disabled = true; nextBtn.textContent = 'Готово';
+ if(!awarded){ awarded = true; achievement('p17_solver'); bumpProgress('p17', 16); confetti(); }
+ } else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
+ }
+ goBtn.addEventListener('click', ()=>{
+ const a = +document.getElementById('p17s-a').value, b = +document.getElementById('p17s-b').value, c = +document.getElementById('p17s-c').value;
+ if(!a){ stage.innerHTML = '$a \\neq 0$
'; renderMath(stage); return; }
+ steps = build(a, b, c); idx = 0; awarded = false;
+ goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
+ render();
+ });
+ nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
+ resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
+ })();
+
+ /* INIT 3 — Тренажёр */
+ (function(){
+ const tasks = [
+ { q:'$x^2 - 5x + 6 > 0$', opts:['$(-\\infty;\\,2) \\cup (3;\\,+\\infty)$','$[2;\\,3]$','$(2;\\,3)$','$\\mathbb{R}$'], ok:0 },
+ { q:'$x^2 - 4x + 3 \\leq 0$', opts:['$[1;\\,3]$','$(-\\infty;\\,1] \\cup [3;\\,+\\infty)$','$(1;\\,3)$','$\\emptyset$'], ok:0 },
+ { q:'$x^2 + 1 > 0$', opts:['$\\mathbb{R}$','$\\emptyset$','$x \\neq 0$','$x > 0$'], ok:0 },
+ { q:'$x^2 + 2x + 5 < 0$', opts:['$\\emptyset$','$\\mathbb{R}$','$(-1;\\,1)$','$x < -1$'], ok:0 },
+ { q:'$-x^2 + 4 \\geq 0$', opts:['$[-2;\\,2]$','$(-\\infty;\\,-2] \\cup [2;\\,+\\infty)$','$(-2;\\,2)$','$\\mathbb{R}$'], ok:0 },
+ { q:'$x^2 - 9 < 0$', opts:['$(-3;\\,3)$','$[-3;\\,3]$','$(-\\infty;\\,-3) \\cup (3;\\,+\\infty)$','$\\emptyset$'], ok:0 },
+ ];
+ let cur = null, i = 1, score = 0, shuffled = [];
+ function show(){
+ cur = shuffled[i-1];
+ document.getElementById('p17t-i').textContent = i;
+ document.getElementById('p17t-task').innerHTML = '$' + cur.q + '$';
+ renderMath(document.getElementById('p17t-task'));
+ const opts = document.getElementById('p17t-opts'); opts.innerHTML = '';
+ cur.opts.forEach((o, k)=>{
+ const b = document.createElement('button');
+ b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
+ b.addEventListener('click', ()=>{
+ const fb = document.getElementById('p17t-fb'); fb.style.display = 'block';
+ if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '✓'); }
+ else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
+ document.getElementById('p17t-score').textContent = score;
+ if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p17_train'); bumpProgress('p17', 16); confetti(); } }, 700); }
+ else { i++; setTimeout(show, 900); }
+ });
+ opts.appendChild(b);
+ });
+ renderMath(opts);
+ document.getElementById('p17t-fb').style.display = 'none';
+ }
+ document.getElementById('p17t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p17t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
+ })();
+
+ /* INIT 4 — Drag парабола ↔ ответ */
+ (function(){
+ const items = [
+ { id:1, html:'$a > 0$, есть 2 корня, неравенство $\\geq 0$', cat:'out' },
+ { id:2, html:'$a > 0$, есть 2 корня, неравенство $\\leq 0$', cat:'in' },
+ { id:3, html:'$a < 0$, есть 2 корня, неравенство $\\geq 0$', cat:'in' },
+ { id:4, html:'$a > 0$, $D < 0$, неравенство $\\geq 0$', cat:'all' },
+ { id:5, html:'$a > 0$, $D < 0$, неравенство $\\leq 0$', cat:'none' },
+ { id:6, html:'$a < 0$, $D < 0$, неравенство $\\geq 0$', cat:'none' },
+ { id:7, html:'$a < 0$, $D < 0$, неравенство $\\leq 0$', cat:'all' },
+ { id:8, html:'$a < 0$, есть 2 корня, неравенство $\\leq 0$', cat:'out' },
+ ];
+ const sorter = setupSorter({ poolId:'p17d-pool', cats:['out','in','all','none'], items, scopeSelector:'#p17-body', columnLayout:true });
+ document.getElementById('p17d-check').addEventListener('click', ()=>{
+ const fb = document.getElementById('p17d-fb'); fb.style.display = 'block';
+ if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '⚠ Разложите все.'); return; }
+ let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
+ if(ok === items.length){ feedback(fb, true, '✓ Все верно!'); achievement('p17_drag'); bumpProgress('p17', 14); confetti(); }
+ else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
+ });
+ document.getElementById('p17d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p17d-fb').style.display='none'; });
+ })();
+
+ /* INIT 5 — Знаки на интервалах (клик) */
+ (function(){
+ // 3 интервала: (-inf, 1), (1, 3), (3, inf). Должны быть: +, -, +.
+ const correct = ['+','-','+'];
+ const labels = ['$x < 1$', '$1 < x < 3$', '$x > 3$'];
+ const lineE = document.getElementById('p17z-line');
+ function build(){
+ let s = '';
+ s += '$-\\infty$ ';
+ [0, 1, 2].forEach(i => {
+ s += '' + labels[i] + '? ';
+ if(i < 2) s += '●' + (i === 0 ? ' 1 ●' : ' 3 ●').replace('●','') + ' ';
+ });
+ s += '$+\\infty$
';
+ lineE.innerHTML = s;
+ renderMath(lineE);
+ document.querySelectorAll('.p17z-int').forEach(btn => {
+ btn.addEventListener('click', ()=>{
+ const cur = btn.dataset.sign;
+ const next = cur === '' ? '+' : cur === '+' ? '-' : '';
+ btn.dataset.sign = next;
+ btn.querySelector('.sg').textContent = next || '?';
+ btn.style.background = next === '+' ? 'rgba(16,185,129,.2)' : next === '-' ? 'rgba(239,68,68,.2)' : '';
+ checkAll();
+ });
+ });
+ }
+ function checkAll(){
+ const all = [...document.querySelectorAll('.p17z-int')];
+ const got = all.map(b => b.dataset.sign);
+ if(got.every((s, i) => s === correct[i])){
+ const fb = document.getElementById('p17z-fb'); fb.style.display = 'block';
+ feedback(fb, true, '✓ Точно! Между корнями знак минус (так как $a > 0$).');
+ achievement('p17_intervals'); bumpProgress('p17', 14); confetti();
+ }
+ }
+ build();
+ })();
+}
+
+/* ============================================================
+ § 18 — ДРОБНО-РАЦИОНАЛЬНЫЕ НЕРАВЕНСТВА
+ ============================================================ */
+function buildP18(){
+ const box = document.getElementById('p18-body');
+ let html = '';
+
+ html += makeCard('repeat','Повторение',null,`
+
+ Метод интервалов из § 17 — корни, знаки на интервалах.
+ ОДЗ (Глава 2 § 12): знаменатель $\\neq 0$.
+ $\\dfrac{a}{b}$ имеет тот же знак, что и $a \\cdot b$.
+ `);
+
+ html += makeCard('theory','Что такое дробно-рациональное неравенство','18.1',`
+ Дробно-рациональное — неравенство, в котором есть дроби с переменной в знаменателе:
+ $$\\dfrac{f(x)}{g(x)} \\gtrless 0$$
+ Ключевая идея: знак дроби определяется произведением знаков числителя и знаменателя. Метод интервалов работает, но точки, где знаменатель $= 0$, всегда выколотые (не входят в ОДЗ).
`);
+
+ html += makeCard('algo','Алгоритм','18.2',`
+
+ Привести к виду $\\dfrac{f(x)}{g(x)} \\gtrless 0$ (всё в одну часть, общий знаменатель).
+ Найти нули числителя $f(x) = 0$ и знаменателя $g(x) = 0$.
+ Отметить точки на прямой: нули $f$ — закрашены (если знак $\\geq, \\leq$), нули $g$ — всегда выколотые.
+ Определить знак выражения на каждом интервале.
+ Выбрать интервалы, удовлетворяющие неравенству.
+ `);
+
+ html += makeCard('example','Пример',null,`
+ Решим: $\\dfrac{x - 1}{x + 2} \\geq 0$.
+ Нули: числитель $x = 1$ (входит), знаменатель $x = -2$ (выколот).
+ Знаки на интервалах: $(-\\infty;\\,-2)$ — $+$ (минус на минус), $(-2;\\,1)$ — $-$ (плюс на минус), $(1;\\,+\\infty)$ — $+$.
+ Ответ: $x \\in (-\\infty;\\,-2) \\cup [1;\\,+\\infty)$.
`);
+
+ /* INT 1 — Пошаговый решатель */
+ html += widget('Пошаговый решатель дроби','INTERACT 1','Решаем $\\dfrac{x-a}{x-b} \\geq 0$ пошагово.',`
+
+ $\\dfrac{x -$ $}{x -$ $} \\geq 0$
+ Старт
+ Дальше
+ Сначала
+
+
`);
+
+ /* INT 2 — Тренажёр */
+ html += widget('Тренажёр дробно-рациональных','INTERACT 2','Выбери правильный ответ.',`
+ Задача 1 / 6 Очки: 0
+
+
+
+ Начать `);
+
+ /* INT 3 — Найди ОДЗ */
+ html += widget('Найди ОДЗ','INTERACT 3','По выражению определи запрещённые точки (где знаменатель = 0).',`
+ Раунд 1 / 5 Очки: 0
+
+
+
+ Ответ
+
+
+ Начать `);
+
+ /* INT 4 — Drag: закрашена/выколота */
+ html += widget('Закрашена или выколота?','INTERACT 4','Отнеси каждую точку к нужной категории.',`
+ ${DND_HINT_HTML}
+
+
+ Проверить Сначала
+
`);
+
+ html += makeCard('class','Класс — решите',null,`
+
+ $\\dfrac{x - 2}{x + 3} > 0$
+ $\\dfrac{x + 1}{x - 4} \\leq 0$
+ $\\dfrac{x^2 - 4}{x - 1} \\geq 0$
+ `);
+
+ html += makeCard('home','Домашка',null,`
+
+ $\\dfrac{x - 3}{x + 1} \\geq 0$
+ $\\dfrac{x + 5}{x^2 - 4} > 0$
+ $\\dfrac{x^2 - 9}{x^2 + 2x - 8} \\leq 0$
+ `);
+
+ html += secNav('p17', 'final3');
+ box.innerHTML = html;
+ if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
+
+ /* INIT 1 — Пошаговый */
+ (function(){
+ const stage = document.getElementById('p18s-stage');
+ const goBtn = document.getElementById('p18s-go'), nextBtn = document.getElementById('p18s-next'), resetBtn = document.getElementById('p18s-reset');
+ let steps = [], idx = 0, awarded = false;
+ function build(a, b){
+ const arr = [];
+ arr.push('Дано: $\\dfrac{x - (' + a + ')}{x - (' + b + ')} \\geq 0$');
+ arr.push('Шаг 1. Нули числителя: $x = ' + a + '$ (входит, $\\geq$). Нули знаменателя: $x = ' + b + '$ (всегда выколот).');
+ const lo = Math.min(a, b), hi = Math.max(a, b);
+ arr.push('Шаг 2. Отметим на прямой: ' + (a < b ? ('$' + a + '$ (закрашена), $' + b + '$ (выколота)') : ('$' + b + '$ (выколота), $' + a + '$ (закрашена)')) + '.');
+ arr.push('Шаг 3. Знаки: подставим $x = ' + (hi + 1) + '$ — числитель $' + (hi + 1 - a) + ' > 0$, знаменатель $' + (hi + 1 - b) + ' > 0$, дробь $> 0$. Чередуем знаки от правого края: $+,\\ -,\\ +$.');
+ if(a < b){
+ arr.push('Шаг 4. $\\geq 0$ — берём $+$. Это $(-\\infty;\\,' + a + ']$ и $(' + b + ';\\,+\\infty)$.');
+ arr.push('Ответ: $x \\in (-\\infty;\\,' + a + '] \\cup (' + b + ';\\,+\\infty)$');
+ } else if(a > b){
+ arr.push('Шаг 4. $\\geq 0$ — берём $+$. Это $(-\\infty;\\,' + b + ')$ и $[' + a + ';\\,+\\infty)$.');
+ arr.push('Ответ: $x \\in (-\\infty;\\,' + b + ') \\cup [' + a + ';\\,+\\infty)$');
+ } else {
+ arr.push('Шаг 4. $a = b$ — дробь равна 1 везде, кроме $x = ' + a + '$ (выколота). $1 \\geq 0$. Ответ: $x \\neq ' + a + '$.');
+ }
+ return arr;
+ }
+ function render(){
+ stage.innerHTML = steps.slice(0, idx + 1).map(s => `${s}
`).join('');
+ renderMath(stage);
+ if(idx >= steps.length - 1){
+ nextBtn.disabled = true; nextBtn.textContent = 'Готово';
+ if(!awarded){ awarded = true; achievement('p18_solver'); bumpProgress('p18', 16); confetti(); }
+ } else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
+ }
+ goBtn.addEventListener('click', ()=>{
+ const a = +document.getElementById('p18s-a').value, b = +document.getElementById('p18s-b').value;
+ steps = build(a, b); idx = 0; awarded = false;
+ goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
+ render();
+ });
+ nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
+ resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
+ })();
+
+ /* INIT 2 — Тренажёр */
+ (function(){
+ const tasks = [
+ { q:'$\\dfrac{x - 3}{x + 1} > 0$', opts:['$(-\\infty;\\,-1) \\cup (3;\\,+\\infty)$','$(-1;\\,3)$','$[-1;\\,3]$','$\\emptyset$'], ok:0 },
+ { q:'$\\dfrac{x + 2}{x - 5} \\leq 0$', opts:['$[-2;\\,5)$','$(-2;\\,5)$','$[-2;\\,5]$','$(-\\infty;\\,-2] \\cup (5;\\,+\\infty)$'], ok:0 },
+ { q:'$\\dfrac{1}{x - 4} > 0$', opts:['$(4;\\,+\\infty)$','$(-\\infty;\\,4)$','$\\mathbb{R} \\setminus \\{4\\}$','$[4;\\,+\\infty)$'], ok:0 },
+ { q:'$\\dfrac{x - 1}{x + 3} \\geq 0$', opts:['$(-\\infty;\\,-3) \\cup [1;\\,+\\infty)$','$[-3;\\,1]$','$(-3;\\,1)$','$\\mathbb{R}$'], ok:0 },
+ { q:'$\\dfrac{x + 6}{x} < 0$', opts:['$(-6;\\,0)$','$[-6;\\,0)$','$(-\\infty;\\,-6)$','$(0;\\,+\\infty)$'], ok:0 },
+ { q:'$\\dfrac{x - 2}{x - 2} > 0$', opts:['$\\mathbb{R} \\setminus \\{2\\}$','$\\mathbb{R}$','$\\{2\\}$','$\\emptyset$'], ok:0 },
+ ];
+ let cur = null, i = 1, score = 0, shuffled = [];
+ function show(){
+ cur = shuffled[i-1];
+ document.getElementById('p18t-i').textContent = i;
+ document.getElementById('p18t-task').innerHTML = '$' + cur.q.replace(/^\$|\$$/g,'') + '$';
+ renderMath(document.getElementById('p18t-task'));
+ const opts = document.getElementById('p18t-opts'); opts.innerHTML = '';
+ cur.opts.forEach((o, k)=>{
+ const b = document.createElement('button');
+ b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
+ b.addEventListener('click', ()=>{
+ const fb = document.getElementById('p18t-fb'); fb.style.display = 'block';
+ if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '✓'); }
+ else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
+ document.getElementById('p18t-score').textContent = score;
+ if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p18_intervals'); bumpProgress('p18', 16); confetti(); } }, 700); }
+ else { i++; setTimeout(show, 900); }
+ });
+ opts.appendChild(b);
+ });
+ renderMath(opts);
+ document.getElementById('p18t-fb').style.display = 'none';
+ }
+ document.getElementById('p18t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p18t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
+ })();
+
+ /* INIT 3 — ОДЗ */
+ (function(){
+ const tasks = [
+ { q:'$\\dfrac{x + 1}{x - 3}$', ans:[3] },
+ { q:'$\\dfrac{x^2 + 1}{x(x - 5)}$', ans:[0, 5] },
+ { q:'$\\dfrac{1}{x^2 - 4}$', ans:[-2, 2] },
+ { q:'$\\dfrac{x + 3}{x^2 - 6x + 9}$', ans:[3] },
+ { q:'$\\dfrac{1}{x} + \\dfrac{1}{x + 1}$', ans:[-1, 0] },
+ ];
+ let cur = null, i = 1, score = 0;
+ function show(){
+ cur = tasks[i-1];
+ document.getElementById('p18o-i').textContent = i;
+ document.getElementById('p18o-task').innerHTML = 'Запрещённые точки для ' + cur.q;
+ renderMath(document.getElementById('p18o-task'));
+ document.getElementById('p18o-inp').value = '';
+ document.getElementById('p18o-fb').style.display = 'none';
+ }
+ document.getElementById('p18o-go').addEventListener('click', ()=>{
+ const fb = document.getElementById('p18o-fb'); fb.style.display = 'block';
+ const u = document.getElementById('p18o-inp').value.replace(/[xX\s=]/g, '').split(/[;,]+/).filter(Boolean).map(Number).sort((a,b)=>a-b);
+ const a = [...cur.ans].sort((p,q)=>p-q);
+ const ok = u.length === a.length && a.every((v, k) => v === u[k]);
+ if(ok){ score++; feedback(fb, true, '✓'); }
+ else feedback(fb, false, 'Правильно: ' + a.join(', '));
+ document.getElementById('p18o-score').textContent = score;
+ if(i >= tasks.length){ setTimeout(()=>{ feedback(fb, score >= 3, 'Итог: ' + score + '/' + tasks.length); if(score >= 3){ achievement('p18_odz'); bumpProgress('p18', 14); confetti(); } }, 700); }
+ else { i++; setTimeout(show, 900); }
+ });
+ document.getElementById('p18o-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p18o-score').textContent = 0; show(); });
+ })();
+
+ /* INIT 4 — Drag закрашена/выколота */
+ (function(){
+ const items = [
+ { id:1, html:'нуль числителя при $\\geq 0$', cat:'full' },
+ { id:2, html:'нуль знаменателя', cat:'open' },
+ { id:3, html:'нуль числителя при $> 0$ строго', cat:'open' },
+ { id:4, html:'нуль числителя при $\\leq 0$', cat:'full' },
+ { id:5, html:'точка вне ОДЗ', cat:'open' },
+ { id:6, html:'граница в системе с $\\leq$', cat:'full' },
+ { id:7, html:'граница в системе с $<$', cat:'open' },
+ { id:8, html:'нуль знаменателя — всегда', cat:'open' },
+ ];
+ const sorter = setupSorter({ poolId:'p18d-pool', cats:['full','open'], items, scopeSelector:'#p18-body', columnLayout:true });
+ document.getElementById('p18d-check').addEventListener('click', ()=>{
+ const fb = document.getElementById('p18d-fb'); fb.style.display = 'block';
+ if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '⚠ Разложите все.'); return; }
+ let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
+ if(ok === items.length){ feedback(fb, true, '✓ Все верно!'); achievement('p18_odz'); bumpProgress('p18', 14); confetti(); }
+ else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
+ });
+ document.getElementById('p18d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p18d-fb').style.display='none'; });
+ })();
+}
function buildFinal3stub(){ document.getElementById('final3-body').innerHTML = `Финал главы Будет в Wave 4 — 7 боссов, увлекательная математика, практика.
${secNav('p18',null)}`; }