diff --git a/frontend/textbooks/algebra_8_ch3.html b/frontend/textbooks/algebra_8_ch3.html
index 893c72c..0f3c733 100644
--- a/frontend/textbooks/algebra_8_ch3.html
+++ b/frontend/textbooks/algebra_8_ch3.html
@@ -1152,8 +1152,592 @@ function init(){
document.addEventListener('DOMContentLoaded', init);
/* STUBS */
-function buildP15stub(){ document.getElementById('p15-body').innerHTML = `
§ 15 — Числовые промежутки. Линейные неравенства Будет в Wave 2.
${secNav('p14','p16')}`; }
-function buildP16stub(){ document.getElementById('p16-body').innerHTML = `§ 16 — Системы неравенств Будет в Wave 2.
${secNav('p15','p17')}`; }
+function buildP15stub(){ buildP15(); }
+function buildP16stub(){ buildP16(); }
+
+/* Helper: SVG-рисовалка интервала / луча / системы */
+function drawNumLine(opts){
+ const W = 520, H = 80, M = 30;
+ const min = opts.min != null ? opts.min : -10;
+ const max = opts.max != null ? opts.max : 10;
+ const x = v => M + (v - min) * (W - 2*M) / (max - min);
+ let s = '';
+ // ось
+ s += ' ';
+ // стрелка вправо
+ s += ' ';
+ // деления и подписи
+ for(let v = Math.ceil(min); v <= Math.floor(max); v++){
+ const px = x(v);
+ s += ' ';
+ if(v % Math.max(1, Math.floor((max - min) / 10)) === 0 || max - min <= 12) s += '' + v + ' ';
+ }
+ // интервалы
+ (opts.intervals || []).forEach(it => {
+ const color = it.color || 'var(--sec-acc)';
+ const a = it.a != null ? Math.max(min, it.a) : min;
+ const b = it.b != null ? Math.min(max, it.b) : max;
+ const xa = x(a), xb = x(b);
+ // полоска
+ s += ' ';
+ // концы
+ if(it.a != null){
+ if(it.openA) s += ' ';
+ else s += ' ';
+ } else {
+ s += ' ';
+ }
+ if(it.b != null){
+ if(it.openB) s += ' ';
+ else s += ' ';
+ } else {
+ s += ' ';
+ }
+ });
+ s += ' ';
+ return s;
+}
+
+/* ============================================================
+ § 15 — ЧИСЛОВЫЕ ПРОМЕЖУТКИ. ЛИНЕЙНЫЕ НЕРАВЕНСТВА
+ ============================================================ */
+function buildP15(){
+ const box = document.getElementById('p15-body');
+ let html = '';
+
+ html += makeCard('repeat','Повторение',null,`
+
+ Свойства неравенств (§13): при делении на отрицательное знак меняется.
+ Линейное уравнение $ax + b = 0$ имеет корень $x = -b/a$.
+ Числовая прямая: точки слева меньше, справа — больше.
+ `);
+
+ html += makeCard('theory','5 видов промежутков','15.1',`
+
+ Название Неравенство Запись Изображение
+
+ Отрезок $a \\leq x \\leq b$ $[a;\\,b]$ ● ━━━ ●
+ Интервал $a < x < b$ $(a;\\,b)$ ○ ━━━ ○
+ Полуинтервал $a \\leq x < b$ $[a;\\,b)$ ● ━━━ ○
+ Луч $x \\geq a$ $[a;\\,+\\infty)$ ● ━━━ →
+ Открытый луч $x < a$ $(-\\infty;\\,a)$ ← ━━━ ○
+
+
+ Запомните: квадратная скобка $[\\,]$ — точка входит . Круглая $(\\,)$ — выколота . У бесконечности всегда круглая.
`);
+
+ html += makeCard('algo','Решение линейного неравенства','15.2',`
+
+ Раскрыть скобки, если есть.
+ Перенести члены с $x$ в одну часть, числа — в другую (со сменой знака при переносе).
+ Привести подобные.
+ Разделить на коэффициент при $x$. Если он отрицательный — знак неравенства меняется .
+ Записать ответ как промежуток.
+ `);
+
+ html += makeCard('example','Пример',null,`
+ Решим: $3x - 7 > 2x + 1$.
+ 1) $3x - 2x > 1 + 7$. 2) $x > 8$. 3) Ответ: $x \\in (8;\\,+\\infty)$.
+ С отрицательным коэффициентом: $-2x \\geq 6$ → делим на $-2$: $x \\leq -3$. Ответ: $x \\in (-\\infty;\\,-3]$.
`);
+
+ /* INT 1 — Конструктор промежутка */
+ html += widget('Конструктор промежутка','INTERACT 1','Выбери концы $a$, $b$ и тип. Промежуток нарисуется на числовой прямой.',`
+
+ $a$ = -2
+ $b$ = 5
+
+
+ $[a;b]$
+ $(a;b)$
+ $[a;b)$
+ $(a;b]$
+ $[a;+\\infty)$
+ $(-\\infty;b)$
+
+
+
`);
+
+ /* INT 2 — Конвертация: запись → неравенство */
+ html += widget('Конвертация записи','INTERACT 2','Найди соответствие между записью промежутка и неравенством.',`
+ Раунд 1 / 8 Очки: 0
+
+
+
+ Начать `);
+
+ /* INT 3 — Пошаговый решатель линейного */
+ html += widget('Пошаговый решатель','INTERACT 3','Введите $a, b, c, d$ для $ax + b \\geq cx + d$ и нажимайте «Дальше».',`
+
+ $x +$
+ $\\geq$
+ $x +$
+
+ Старт
+ Дальше
+ Сначала
+
+
`);
+
+ /* INT 4 — Тренажёр линейных */
+ html += widget('Тренажёр линейных','INTERACT 4','Решите неравенство и введите ответ в формате $(a; +\\infty)$ или $[-\\infty; b)$. Используйте `inf` для бесконечности.',`
+ Задача 1 / 8 Очки: 0
+
+
+ $x > k$
+ $x \\geq k$
+ $x < k$
+ $x \\leq k$
+
+
+
+ Ответ
+
+
+ Начать `);
+
+ /* INT 5 — Drag: какой промежуток */
+ html += widget('Сопоставь неравенство и промежуток','INTERACT 5','Отнеси каждое неравенство к правильной записи промежутка.',`
+ ${DND_HINT_HTML}
+
+
+ Проверить Сначала
+
`);
+
+ html += makeCard('oral','Устно',null,`
+
+ Запишите промежуток для $x > 5$.
+ Что означает скобка $[$?
+ Решите устно: $2x > 6$.
+ `);
+
+ html += makeCard('class','Класс — решите',null,`
+
+ $5x - 3 < 12$
+ $-3x + 1 \\geq 7$
+ $2(x - 1) > 3(x + 2)$
+ Запишите $[-2; 5)$ через неравенство.
+ `);
+
+ html += makeCard('home','Домашка',null,`
+
+ $7x + 2 \\leq 4x - 7$
+ $-\\dfrac{x}{3} > 2$
+ $3 - 2x \\leq x + 9$
+ Изобразите $(-\\infty; -1) \\cup [3; +\\infty)$.
+ `);
+
+ html += secNav('p14', 'p16');
+ box.innerHTML = html;
+ if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
+
+ /* INIT 1 — Конструктор */
+ (function(){
+ const aE = document.getElementById('p15c-a'), bE = document.getElementById('p15c-b');
+ const lineE = document.getElementById('p15c-line'), out = document.getElementById('p15c-out');
+ let type = 'cc', done = false;
+ document.querySelectorAll('.p15c-type').forEach(btn => btn.addEventListener('click', ()=>{
+ document.querySelectorAll('.p15c-type').forEach(x => x.classList.remove('active'));
+ btn.classList.add('active'); type = btn.dataset.t; refresh();
+ }));
+ function refresh(){
+ const a = +aE.value, b = +bE.value;
+ document.getElementById('p15c-a-val').textContent = a;
+ document.getElementById('p15c-b-val').textContent = b;
+ let interval = null, label = '', ineq = '';
+ if(type === 'cc'){ if(b < a){ out.innerHTML = 'Нужно $a \\leq b$'; renderMath(out); return; } interval = { a, b, openA:false, openB:false }; label = '$[' + a + ';\\,' + b + ']$'; ineq = a + ' \\leq x \\leq ' + b; }
+ else if(type === 'oo'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:true, openB:true }; label = '$(' + a + ';\\,' + b + ')$'; ineq = a + ' < x < ' + b; }
+ else if(type === 'co'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:false, openB:true }; label = '$[' + a + ';\\,' + b + ')$'; ineq = a + ' \\leq x < ' + b; }
+ else if(type === 'oc'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:true, openB:false }; label = '$(' + a + ';\\,' + b + ']$'; ineq = a + ' < x \\leq ' + b; }
+ else if(type === 'rcr'){ interval = { a, b:null, openA:false, openB:false }; label = '$[' + a + ';\\,+\\infty)$'; ineq = 'x \\geq ' + a; }
+ else if(type === 'rol'){ interval = { a:null, b, openA:false, openB:true }; label = '$(-\\infty;\\,' + b + ')$'; ineq = 'x < ' + b; }
+ lineE.innerHTML = drawNumLine({ intervals:[interval] });
+ out.innerHTML = 'Запись: ' + label + '
Неравенство: $' + ineq + '$
';
+ renderMath(out);
+ if(!done){ done = true; setTimeout(()=>{ achievement('p15_line'); bumpProgress('p15', 14); }, 300); }
+ }
+ [aE,bE].forEach(e => e.addEventListener('input', refresh));
+ refresh();
+ })();
+
+ /* INIT 2 — Конвертация */
+ (function(){
+ const tasks = [
+ { q:'Запись: $[3;\\,7]$. Какое неравенство?', opts:['$3 \\leq x \\leq 7$','$3 < x < 7$','$x \\geq 3$','$3 \\leq x < 7$'], ok:0 },
+ { q:'Неравенство: $-2 < x \\leq 5$. Какая запись?', opts:['$(-2;\\,5]$','$[-2;\\,5)$','$[-2;\\,5]$','$(-2;\\,5)$'], ok:0 },
+ { q:'Запись: $(-\\infty;\\,4)$. Что значит?', opts:['$x < 4$','$x \\leq 4$','$x > 4$','$-\\infty < x < 4$ строго'], ok:0 },
+ { q:'Неравенство: $x \\geq 6$. Какая запись?', opts:['$[6;\\,+\\infty)$','$(6;\\,+\\infty)$','$(-\\infty;\\,6]$','$[-6;\\,+\\infty)$'], ok:0 },
+ { q:'Запись: $(0;\\,1)$. Содержит ли точку $0$?', opts:['Нет, выколота','Да, входит','Только если $0 > 0$','Зависит от контекста'], ok:0 },
+ { q:'Запись с двумя круглыми скобками означает:', opts:['Оба конца выколоты','Оба конца входят','Один выколот, один входит','Только справа выколот'], ok:0 },
+ { q:'Какой записью обозначить «все числа меньше или равны $-3$»?', opts:['$(-\\infty;\\,-3]$','$(-\\infty;\\,-3)$','$[-3;\\,+\\infty)$','$(-3;\\,+\\infty)$'], ok:0 },
+ { q:'Промежуток $[2;\\,2]$ содержит:', opts:['Только число $2$','Все числа от $0$ до $2$','Пуст','Все числа $\\geq 2$'], ok:0 },
+ ];
+ let cur = null, i = 1, score = 0, shuffled = [];
+ function show(){
+ cur = shuffled[i-1];
+ document.getElementById('p15v-i').textContent = i;
+ document.getElementById('p15v-task').innerHTML = cur.q;
+ renderMath(document.getElementById('p15v-task'));
+ const opts = document.getElementById('p15v-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('p15v-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('p15v-score').textContent = score;
+ if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/' + shuffled.length); if(score >= 6){ achievement('p15_convert'); bumpProgress('p15', 14); confetti(); } }, 600); }
+ else { i++; setTimeout(show, 800); }
+ });
+ opts.appendChild(b);
+ });
+ renderMath(opts);
+ document.getElementById('p15v-fb').style.display = 'none';
+ }
+ document.getElementById('p15v-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p15v-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
+ })();
+
+ /* INIT 3 — Шаговый решатель */
+ (function(){
+ const stage = document.getElementById('p15s-stage');
+ const goBtn = document.getElementById('p15s-go'), nextBtn = document.getElementById('p15s-next'), resetBtn = document.getElementById('p15s-reset');
+ let steps = [], idx = 0, awarded = false;
+ function build(a, b, c, d){
+ // ax + b >= cx + d -> (a-c)x >= d-b
+ const k = a - c, r = d - b;
+ const arr = [];
+ arr.push('Дано: $' + a + 'x ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + ' \\geq ' + c + 'x ' + (d >= 0 ? '+ ' + d : '- ' + Math.abs(d)) + '$');
+ arr.push('Шаг 1. Переносим $x$ влево, числа — вправо: $' + a + 'x - ' + c + 'x \\geq ' + d + ' - (' + b + ') \\Rightarrow ' + k + 'x \\geq ' + r + '$');
+ if(k === 0){
+ arr.push('Шаг 2. $0 \\cdot x \\geq ' + r + '$. ' + (r <= 0 ? 'Верно для любого $x$ — решение $x \\in \\mathbb{R}$.' : 'Невозможно — решений нет.'));
+ arr.push('Ответ: ' + (r <= 0 ? '$(-\\infty;\\,+\\infty)$' : 'нет решений'));
+ } else if(k > 0){
+ arr.push('Шаг 2. Делим на $' + k + ' > 0$: $x \\geq ' + r + '/' + k + ' = ' + fmt(r/k) + '$');
+ arr.push('Ответ: $x \\in [' + fmt(r/k) + ';\\,+\\infty)$');
+ } else {
+ arr.push('Шаг 2. Делим на $' + k + ' < 0$ — знак меняется! $x \\leq ' + r + '/' + k + ' = ' + fmt(r/k) + '$');
+ arr.push('Ответ: $x \\in (-\\infty;\\,' + fmt(r/k) + ']$');
+ }
+ 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('p15_solver'); bumpProgress('p15', 14); confetti(); }
+ } else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
+ }
+ goBtn.addEventListener('click', ()=>{
+ const a = +document.getElementById('p15s-a').value, b = +document.getElementById('p15s-b').value;
+ const c = +document.getElementById('p15s-c').value, d = +document.getElementById('p15s-d').value;
+ steps = build(a, b, c, d); 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 4 — Тренажёр линейных */
+ (function(){
+ function gen(){
+ const a = (Math.random() < 0.5 ? -1 : 1) * (1 + Math.floor(Math.random()*4));
+ const b = -5 + Math.floor(Math.random()*11);
+ const c = -5 + Math.floor(Math.random()*11);
+ // ax + b > c
+ const v = (c - b) / a;
+ // знак ответа зависит от знака a
+ const baseCmp = ['gt','ge','lt','le'][Math.floor(Math.random()*4)];
+ const flipped = a < 0 ? { gt:'lt', ge:'le', lt:'gt', le:'ge' }[baseCmp] : baseCmp;
+ return { a, b, c, baseCmp, ansCmp: flipped, ansK: v, txt: a + 'x ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + ' ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[baseCmp] + ' ' + c };
+ }
+ let cur = null, i = 1, score = 0, chosen = null;
+ function show(){
+ cur = gen();
+ document.getElementById('p15t-i').textContent = i;
+ document.getElementById('p15t-task').innerHTML = '$' + cur.txt + '$';
+ renderMath(document.getElementById('p15t-task'));
+ document.getElementById('p15t-k').value = '';
+ chosen = null;
+ document.querySelectorAll('#p15t-gt,#p15t-ge,#p15t-lt,#p15t-le').forEach(b => b.classList.remove('primary'));
+ document.getElementById('p15t-fb').style.display = 'none';
+ }
+ function check(){
+ const fb = document.getElementById('p15t-fb'); fb.style.display = 'block';
+ const u = +document.getElementById('p15t-k').value;
+ const ok = chosen === cur.ansCmp && Math.abs(u - cur.ansK) < 1e-6;
+ if(ok){ score++; feedback(fb, true, '✓ $x ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[cur.ansCmp] + ' ' + fmt(cur.ansK) + '$'); renderMath(fb); }
+ else feedback(fb, false, 'Правильно: $x ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[cur.ansCmp] + ' ' + fmt(cur.ansK) + '$');
+ renderMath(fb);
+ document.getElementById('p15t-score').textContent = score;
+ if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 5, 'Итог: ' + score + '/8'); if(score >= 5){ achievement('p15_train'); bumpProgress('p15', 16); confetti(); } }, 700); }
+ else { i++; setTimeout(show, 900); }
+ }
+ document.getElementById('p15t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p15t-score').textContent = 0; show(); });
+ ['gt','ge','lt','le'].forEach(c => document.getElementById('p15t-' + c).addEventListener('click', ()=>{
+ chosen = c;
+ document.querySelectorAll('#p15t-gt,#p15t-ge,#p15t-lt,#p15t-le').forEach(b => b.classList.remove('primary'));
+ document.getElementById('p15t-' + c).classList.add('primary');
+ }));
+ document.getElementById('p15t-go').addEventListener('click', ()=>{ if(!chosen){ const fb = document.getElementById('p15t-fb'); fb.style.display='block'; feedback(fb, false, 'Выберите знак.'); return; } check(); });
+ })();
+
+ /* INIT 5 — Drag */
+ (function(){
+ const items = [
+ { id:1, html:'$2 \\leq x \\leq 7$', cat:'cc' },
+ { id:2, html:'$-3 < x < 4$', cat:'oo' },
+ { id:3, html:'$x > 5$', cat:'rop' },
+ { id:4, html:'$x \\leq 0$', cat:'lcr' },
+ { id:5, html:'$-1 \\leq x \\leq 6$', cat:'cc' },
+ { id:6, html:'$0 < x < 10$', cat:'oo' },
+ { id:7, html:'$x > 8$', cat:'rop' },
+ { id:8, html:'$x \\leq -2$', cat:'lcr' },
+ ];
+ const sorter = setupSorter({ poolId:'p15d-pool', cats:['cc','oo','rop','lcr'], items, scopeSelector:'#p15-body' });
+ document.getElementById('p15d-check').addEventListener('click', ()=>{
+ const fb = document.getElementById('p15d-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('p15_drag'); bumpProgress('p15', 14); confetti(); }
+ else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
+ });
+ document.getElementById('p15d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p15d-fb').style.display='none'; });
+ })();
+}
+
+/* ============================================================
+ § 16 — СИСТЕМЫ И СОВОКУПНОСТИ НЕРАВЕНСТВ
+ ============================================================ */
+function buildP16(){
+ const box = document.getElementById('p16-body');
+ let html = '';
+
+ html += makeCard('repeat','Повторение § 15',null,`
+
+ Промежутки: $[a;b]$, $(a;b)$, лучи и др.
+ Решение линейного: изоляция $x$, не забывать про знак при делении на отрицательное.
+ `);
+
+ html += makeCard('theory','Система и совокупность','16.1',`
+ Система неравенств — несколько неравенств, выполненных одновременно («И»). Решение — пересечение решений каждого. Записывается фигурной скобкой:
+ $$\\begin{cases} x \\geq 1 \\\\ x < 5 \\end{cases} \\Rightarrow x \\in [1;\\,5)$$
+ Совокупность — неравенства, из которых выполняется хотя бы одно («ИЛИ»). Решение — объединение . Записывается квадратной скобкой:
+ $$\\left[\\begin{array}{l} x < -2 \\\\ x \\geq 3 \\end{array}\\right. \\Rightarrow x \\in (-\\infty;\\,-2) \\cup [3;\\,+\\infty)$$
`);
+
+ html += makeCard('algo','Алгоритм решения системы',null,`
+
+ Решить каждое неравенство отдельно.
+ Изобразить решения на одной числовой прямой.
+ Найти пересечение (общую часть для системы) или объединение (для совокупности).
+ Записать ответ как промежуток.
+ `);
+
+ html += makeCard('example','Пример',null,`
+ Решим систему: $\\begin{cases} 2x - 1 > 3 \\\\ x + 4 \\leq 10 \\end{cases}$
+ Первое: $2x > 4 \\Rightarrow x > 2$, т.е. $(2;\\,+\\infty)$.
+ Второе: $x \\leq 6$, т.е. $(-\\infty;\\,6]$.
+ Пересечение: $x \\in (2;\\,6]$.
`);
+
+ /* INT 1 — Пересечение промежутков */
+ html += widget('Пересечение двух промежутков','INTERACT 1','Сдвигай границы каждого промежутка и наблюдай пересечение.',`
+
+ $a_1$ = -3
+ $b_1$ = 4
+ $a_2$ = 1
+ $b_2$ = 7
+
+
+
`);
+
+ /* INT 2 — Пошаговый решатель */
+ html += widget('Шаговый решатель системы','INTERACT 2','Решаем систему пошагово: каждое неравенство отдельно, затем пересечение.',`
+ Система: $\\begin{cases} 3x - 5 \\geq 1 \\\\ -2x + 4 > -6 \\end{cases}$
+
+ Старт Дальше Сначала
`);
+
+ /* INT 3 — Drag: система или совокупность */
+ html += widget('Что это: система или совокупность?','INTERACT 3','По логической связке («И» / «ИЛИ») определи тип записи.',`
+ ${DND_HINT_HTML}
+
+
+
+
Совокупность (ИЛИ, объединение)
+
+ Проверить Сначала
+
`);
+
+ /* INT 4 — Тренажёр систем */
+ html += widget('Тренажёр систем','INTERACT 4','Решите систему и введите ответ как промежуток.',`
+ Задача 1 / 6 Очки: 0
+
+
+
+ Начать `);
+
+ /* INT 5 — Совокупность визуально */
+ html += widget('Совокупность: объединение','INTERACT 5','Двигай $a$ и $b$, смотри, как меняется $(-\\infty;a) \\cup (b;+\\infty)$.',`
+
+ $a$ = -2
+ $b$ = 3
+
+
+
`);
+
+ html += makeCard('class','Класс — решите',null,`
+
+ $\\begin{cases} x > 2 \\\\ x < 8 \\end{cases}$
+ $\\begin{cases} 2x - 1 \\leq 5 \\\\ x + 3 > -2 \\end{cases}$
+ $\\left[\\begin{array}{l} x < -1 \\\\ x \\geq 4 \\end{array}\\right.$
+ `);
+
+ html += makeCard('home','Домашка',null,`
+
+ $\\begin{cases} 3x + 1 > 7 \\\\ x - 2 \\leq 3 \\end{cases}$
+ $\\begin{cases} 5 - 2x \\geq 1 \\\\ x + 1 > -3 \\end{cases}$
+ $\\left[\\begin{array}{l} 2x > 8 \\\\ -x \\geq 2 \\end{array}\\right.$
+ `);
+
+ html += secNav('p15', 'p17');
+ box.innerHTML = html;
+ if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
+
+ /* INIT 1 — Пересечение промежутков */
+ (function(){
+ const ids = ['p16i-a1','p16i-b1','p16i-a2','p16i-b2'];
+ const lineE = document.getElementById('p16i-line'), out = document.getElementById('p16i-out');
+ let done = false;
+ function refresh(){
+ const a1 = +document.getElementById('p16i-a1').value, b1 = +document.getElementById('p16i-b1').value;
+ const a2 = +document.getElementById('p16i-a2').value, b2 = +document.getElementById('p16i-b2').value;
+ document.getElementById('p16i-a1-val').textContent = a1;
+ document.getElementById('p16i-b1-val').textContent = b1;
+ document.getElementById('p16i-a2-val').textContent = a2;
+ document.getElementById('p16i-b2-val').textContent = b2;
+ const intervals = [];
+ if(a1 <= b1) intervals.push({ a:a1, b:b1, openA:false, openB:false, color:'#6366f1' });
+ if(a2 <= b2) intervals.push({ a:a2, b:b2, openA:false, openB:false, color:'#f59e0b' });
+ const lo = Math.max(a1, a2), hi = Math.min(b1, b2);
+ if(lo <= hi) intervals.push({ a:lo, b:hi, openA:false, openB:false, color:'#10b981' });
+ lineE.innerHTML = drawNumLine({ intervals });
+ let s = '[' + a1 + ';' + b1 + '] ∩ [' + a2 + ';' + b2 + '] = ';
+ if(lo > hi) s += '∅ (пусто)
';
+ else s += '[' + lo + ';' + hi + '] ';
+ out.innerHTML = s;
+ if(!done){ done = true; setTimeout(()=>{ achievement('p16_intersect'); bumpProgress('p16', 14); }, 300); }
+ }
+ ids.forEach(id => document.getElementById(id).addEventListener('input', refresh));
+ refresh();
+ })();
+
+ /* INIT 2 — Шаговый решатель */
+ (function(){
+ const stage = document.getElementById('p16s-stage');
+ const goBtn = document.getElementById('p16s-go'), nextBtn = document.getElementById('p16s-next'), resetBtn = document.getElementById('p16s-reset');
+ const steps = [
+ 'Шаг 1. Решим первое: $3x - 5 \\geq 1 \\Rightarrow 3x \\geq 6 \\Rightarrow x \\geq 2$, т.е. $[2;\\,+\\infty)$.',
+ 'Шаг 2. Решим второе: $-2x + 4 > -6 \\Rightarrow -2x > -10 \\Rightarrow x < 5$ (знак сменился!), т.е. $(-\\infty;\\,5)$.',
+ 'Шаг 3. Пересечение: $[2;\\,+\\infty) \\cap (-\\infty;\\,5) = [2;\\,5)$.',
+ 'Ответ: $x \\in [2;\\,5)$.',
+ ];
+ let idx = 0, awarded = false;
+ 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('p16_solver'); bumpProgress('p16', 14); confetti(); }
+ } else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
+ }
+ goBtn.addEventListener('click', ()=>{ 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 — Drag система/совокупность */
+ (function(){
+ const items = [
+ { id:1, html:'$\\begin{cases} x > 2 \\\\ x \\leq 5 \\end{cases}$', cat:'sys' },
+ { id:2, html:'$\\left[\\begin{array}{l} x < -3 \\\\ x \\geq 4 \\end{array}\\right.$', cat:'sov' },
+ { id:3, html:'Оба условия одновременно', cat:'sys' },
+ { id:4, html:'Хотя бы одно условие', cat:'sov' },
+ { id:5, html:'$\\begin{cases} x \\geq -1 \\\\ x < 7 \\end{cases}$', cat:'sys' },
+ { id:6, html:'$\\left[\\begin{array}{l} x \\leq 0 \\\\ x > 5 \\end{array}\\right.$', cat:'sov' },
+ { id:7, html:'Пересечение решений', cat:'sys' },
+ { id:8, html:'Объединение решений', cat:'sov' },
+ ];
+ const sorter = setupSorter({ poolId:'p16d-pool', cats:['sys','sov'], items, scopeSelector:'#p16-body' });
+ document.getElementById('p16d-check').addEventListener('click', ()=>{
+ const fb = document.getElementById('p16d-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('p16_union'); bumpProgress('p16', 14); confetti(); }
+ else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
+ });
+ document.getElementById('p16d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p16d-fb').style.display='none'; });
+ })();
+
+ /* INIT 4 — Тренажёр систем */
+ (function(){
+ const tasks = [
+ { q:'$\\begin{cases} x > 3 \\\\ x \\leq 7 \\end{cases}$', opts:['$(3;\\,7]$','$[3;\\,7)$','$(3;\\,7)$','$\\emptyset$'], ok:0 },
+ { q:'$\\begin{cases} x \\geq -2 \\\\ x < 4 \\end{cases}$', opts:['$[-2;\\,4)$','$(-2;\\,4)$','$[-2;\\,4]$','$\\emptyset$'], ok:0 },
+ { q:'$\\begin{cases} x > 5 \\\\ x < 2 \\end{cases}$', opts:['$\\emptyset$ (нет решений)','$(2;\\,5)$','$(5;\\,2)$','любое $x$'], ok:0 },
+ { q:'$\\left[\\begin{array}{l} x < -1 \\\\ x \\geq 3 \\end{array}\\right.$', opts:['$(-\\infty;\\,-1) \\cup [3;\\,+\\infty)$','$(-1;\\,3)$','$[-1;\\,3)$','$\\emptyset$'], ok:0 },
+ { q:'$\\begin{cases} 2x \\geq 6 \\\\ x \\leq 10 \\end{cases}$', opts:['$[3;\\,10]$','$[3;\\,10)$','$(3;\\,10]$','$[6;\\,10]$'], ok:0 },
+ { q:'$\\begin{cases} x > 0 \\\\ x < 0 \\end{cases}$', opts:['$\\emptyset$','$\\{0\\}$','$\\mathbb{R}$','$(-\\infty;\\,0)$'], ok:0 },
+ ];
+ let cur = null, i = 1, score = 0, shuffled = [];
+ function show(){
+ cur = shuffled[i-1];
+ document.getElementById('p16t-i').textContent = i;
+ document.getElementById('p16t-task').innerHTML = cur.q;
+ renderMath(document.getElementById('p16t-task'));
+ const opts = document.getElementById('p16t-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('p16t-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('p16t-score').textContent = score;
+ if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p16_train'); bumpProgress('p16', 16); confetti(); } }, 700); }
+ else { i++; setTimeout(show, 900); }
+ });
+ opts.appendChild(b);
+ });
+ renderMath(opts);
+ document.getElementById('p16t-fb').style.display = 'none';
+ }
+ document.getElementById('p16t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p16t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
+ })();
+
+ /* INIT 5 — Совокупность визуально */
+ (function(){
+ const aE = document.getElementById('p16u-a'), bE = document.getElementById('p16u-b');
+ const lineE = document.getElementById('p16u-line'), out = document.getElementById('p16u-out');
+ function refresh(){
+ const a = +aE.value, b = +bE.value;
+ document.getElementById('p16u-a-val').textContent = a;
+ document.getElementById('p16u-b-val').textContent = b;
+ const intervals = [
+ { a:null, b:a, openA:false, openB:true, color:'#8b5cf6' },
+ { a:b, b:null, openA:true, openB:false, color:'#8b5cf6' },
+ ];
+ lineE.innerHTML = drawNumLine({ intervals });
+ out.innerHTML = 'Совокупность: $x < ' + a + '$ или $x > ' + b + '$. Решение: $(-\\infty;\\,' + a + ') \\cup (' + b + ';\\,+\\infty)$.';
+ renderMath(out);
+ }
+ [aE,bE].forEach(e => e.addEventListener('input', refresh));
+ 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 buildFinal3stub(){ document.getElementById('final3-body').innerHTML = `Финал главы Будет в Wave 4 — 7 боссов, увлекательная математика, практика.
${secNav('p18',null)}`; }