Files
Learn_System/frontend/textbooks/algebra_8_ch3.html
T
Maxim Dolgolyov dc201f28ff feat(algebra-8): Глава 3 Wave 1 — скелет + §13 + §14
Глава 3 «Неравенства с одной переменной» по программе Арефьевой/Пирютко.
Палитра: индиго → фиолетовый → бирюза. 6 параграфов + финал.

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

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

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

DB: миграция 013 — slug 'algebra-8-ch3', sort_order=5, бамп physics-8 на 6.
Главы 1 и 2 теперь имеют кнопку «Глава 3 →» в шапке.
2026-05-27 16:14:15 +03:00

1775 lines
111 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Алгебра 8 · Глава 3 · Неравенства</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#fafafa; --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);
--pri:#6366f1; --pri2:#4338ca; --pri-soft:#e0e7ff;
--acc:#06b6d4; --acc2:#0e7490; --acc-soft:#cffafe;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0e1a; --card:#111827; --card-soft:#0f1729; --text:#e2e8f0; --ink:#e2e8f0; --muted:#94a3b8; --border:#1e293b}
*{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,#4338ca 0%,#6366f1 50%,#8b5cf6 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(199,180,255,.2);min-height:130px}
.hdr::before{content:'ГЛАВА 3';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(220,210,255,.1);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:.88;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,.14);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,.24)}
/* 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:'x > a';position:absolute;right:-10px;top:-20px;font-family:'JetBrains Mono',monospace;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.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(99,102,241,.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(99,102,241,.18);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(99,102,241,.22);font-family:'Unbounded',sans-serif}
.hero-xp-badge svg{flex-shrink:0}
/* 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(170px,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:.88rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(99,102,241,.10);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,#fff5e1,#e0e7ff)}
.psel-card.final .psel-num{color:var(--warn)}
/* SECTION COLORS */
.sec[id="sec-p13"] { --sec-acc:#6366f1; --sec-acc-d:#4338ca; --sec-acc-soft:#e0e7ff; }
.sec[id="sec-p14"] { --sec-acc:#0ea5e9; --sec-acc-d:#0369a1; --sec-acc-soft:#e0f2fe; }
.sec[id="sec-p15"] { --sec-acc:#10b981; --sec-acc-d:#047857; --sec-acc-soft:#d1fae5; }
.sec[id="sec-p16"] { --sec-acc:#f59e0b; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p17"] { --sec-acc:#8b5cf6; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p18"] { --sec-acc:#e11d48; --sec-acc-d:#9f1239; --sec-acc-soft:#ffe4e6; }
.sec[id="sec-final3"] { --sec-acc:#d97706; --sec-acc-d:#b45309; --sec-acc-soft:#fef3c7; }
.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.7rem;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(99,102,241,.06);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(99,102,241,.12)}
.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;outline:2px solid var(--sec-acc-soft,transparent);outline-offset:1px}
.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}
/* 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;transition:box-shadow .25s}
.wg:hover{box-shadow:0 4px 12px rgba(0,0,0,.08),0 16px 40px rgba(99,102,241,.18)}
.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;position:relative}
.wg-help::before{content:'?';position:absolute;left:-13px;top:50%;transform:translateY(-50%);width:22px;height:22px;border-radius:50%;background:var(--warn,#f59e0b);color:#fff;display:flex;align-items:center;justify-content:center;font-weight:900;font-size:.78rem;box-shadow:0 2px 6px rgba(0,0,0,.18)}
.dark .wg-help{background:linear-gradient(135deg,rgba(245,158,11,.18),rgba(99,102,241,.10))}
/* 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.acc{background:var(--acc);color:#fff;border-color:var(--acc)}
.btn.ok{background:var(--ok);color:#fff;border-color:var(--ok)}
.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,box-shadow .15s}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
/* CHIPS / FEEDBACK */
.chip{display:inline-flex;align-items:center;gap:5px;padding:5px 10px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:7px;font-size:.84rem;font-weight:600;color:var(--sec-acc-d,var(--pri2))}
.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)}
/* 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(3,169,244,.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:''}
.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}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
/* 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;font-family:'JetBrains Mono',monospace}
.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,#10b981,#06b6d4);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(16,185,129,.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}
@keyframes achIn{from{opacity:0;transform:translateX(40px)}to{opacity:1;transform:none}}
/* SCORE */
.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}
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
/* SLIDERS / DROP / ACTIONS */
.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 .katex{font-size:1em}
.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))}
.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-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
/* DRAG & DROP — sortable chips */
.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(99,102,241,.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.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.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}
/* SIDEBAR DRAWER for narrow viewports */
.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(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}
}
/* NUMBER LINE — спец-виджет для §15-§17 */
.numline{background:var(--card);border:1px solid var(--border);border-radius:10px;padding:14px 6px;margin:10px 0;overflow-x:auto}
.numline svg{width:100%;min-width:520px;height:88px;display:block}
/* GLOSSARY tooltip */
.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 MODAL */
.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}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Алгебра 8 · Глава 3</h1>
<div class="hdr-sub">Неравенства с одной переменной</div>
</div>
<div class="hdr-side">
<a href="/textbook/algebra-8" class="hdr-btn" title="К Главе 1">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
Глава 1
</a>
<a href="/textbook/algebra-8-ch2" class="hdr-btn" title="К Главе 2">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
Глава 2
</a>
<button id="search-btn" class="hdr-btn" title="Поиск (Ctrl+K)">
<svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg>
<span>Поиск</span>
</button>
<button id="sidebar-btn" class="hdr-btn" title="Шпаргалка">
<svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg>
Шпаргалка
</button>
<button id="theme-btn" class="hdr-btn" title="Сменить тему">
<svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>
<span id="theme-lab">Тёмная</span>
</button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero">
<h2>Когда математика говорит «больше или меньше»</h2>
<p>Равенства — это точечный ответ: $x = 5$. Неравенства — это <b>множество</b> ответов: $x > 3$ означает «3, 4, 5, и так далее, бесконечно». Эта глава научит сравнивать числа, решать неравенства всех типов и понимать <b>метод интервалов</b> — универсальный инструмент.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p13')">
<svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg>
Начать § 13
</button>
<div class="hero-progress">
<span class="hp-label">Прогресс по главе</span>
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
<span id="hero-hp-text" class="hp-text">0%</span>
</div>
<div id="hero-xp-badge" class="hero-xp-badge" title="Опыт"></div>
</div>
</section>
<section class="psel">
<div class="psel-title">Параграфы главы</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
<section id="sec-p13" class="sec" data-watermark="<>">
<div class="sec-header"><span class="sec-num">§ 13</span><h2 class="sec-h">Числовые неравенства и их свойства</h2></div>
<div id="p13-body"></div>
</section>
<section id="sec-p14" class="sec" data-watermark="±">
<div class="sec-header"><span class="sec-num">§ 14</span><h2 class="sec-h">Сложение и умножение числовых неравенств. Оценка значений</h2></div>
<div id="p14-body"></div>
</section>
<section id="sec-p15" class="sec" data-watermark="[;]">
<div class="sec-header"><span class="sec-num">§ 15</span><h2 class="sec-h">Числовые промежутки. Линейные неравенства</h2></div>
<div id="p15-body"></div>
</section>
<section id="sec-p16" class="sec" data-watermark="∩">
<div class="sec-header"><span class="sec-num">§ 16</span><h2 class="sec-h">Системы и совокупности линейных неравенств</h2></div>
<div id="p16-body"></div>
</section>
<section id="sec-p17" class="sec" data-watermark="x²">
<div class="sec-header"><span class="sec-num">§ 17</span><h2 class="sec-h">Квадратные неравенства. Метод интервалов</h2></div>
<div id="p17-body"></div>
</section>
<section id="sec-p18" class="sec" data-watermark="1/x">
<div class="sec-header"><span class="sec-num">§ 18</span><h2 class="sec-h">Дробно-рациональные неравенства</h2></div>
<div id="p18-body"></div>
</section>
<section id="sec-final3" class="sec" data-watermark="★">
<div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#f59e0b,#8b5cf6)">Финал главы</span><h2 class="sec-h">Итоги. Практическая и увлекательная математика</h2></div>
<div id="final3-body"></div>
</section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot">Интерактивный учебник «Алгебра 8» · Глава 3 · LearnSpace · версия 1.0</footer>
<div id="ach-popup" class="ach-popup">
<svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><circle cx="12" cy="8" r="5"/><path d="M8 13l-2 8 6-4 6 4-2-8"/></svg>
<span id="ach-text">Достижение!</span>
</div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog" aria-label="Поиск по главе">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск: понятие, формула, параграф…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot">
<span><kbd>↑↓</kbd> навигация</span>
<span><kbd>Enter</kbd> открыть</span>
<span><kbd>Esc</kbd> закрыть</span>
</div>
</div>
</div>
<script>
'use strict';
/* STATE & PROGRESS */
const STATE = {
current: 'p13',
progress: { p13:0, p14:0, p15:0, p16:0, p17:0, p18:0, final3:0 },
achievements: new Map(),
xp: 0,
level: 1,
};
const XP_LEVELS = null;
function calcLevel(xp){ return Math.floor(Math.sqrt((xp || 0) / 100)) + 1; }
function _xpForLevel(lv){ return (lv - 1) * (lv - 1) * 100; }
const ACH_LABELS = {
start: 'Начало главы 3!',
p13_compare: 'Сравнил числа',
p13_props: 'Свойства неравенств',
p13_sign: 'Знак не меняется',
p13_flip: 'Знак меняется',
p13_chain: 'Цепочка свойств',
p14_estimate: 'Оценил выражение',
p14_add: 'Сложил неравенства',
p14_mul: 'Перемножил неравенства',
p14_drag: 'Сортировка операций',
p14_train: 'Тренажёр оценки',
p15_line: 'Промежутки на прямой',
p15_convert: 'Конвертация записи',
p15_solver: 'Линейное решено',
p15_train: 'Тренажёр линейных',
p15_drag: 'Промежуток к неравенству',
p16_intersect: 'Пересечение промежутков',
p16_solver: 'Система решена',
p16_union: 'Совокупность',
p16_train: 'Тренажёр систем',
p17_parab: 'Парабола и знак',
p17_intervals: 'Метод интервалов',
p17_solver: 'Квадратное неравенство',
p17_train: 'Тренажёр квадратных',
p17_drag: 'График к решению',
p18_intervals: 'Интервалы для дробей',
p18_solver: 'Дробно-рациональное',
p18_odz: 'ОДЗ учтена',
boss_b1: 'Босс §13 повержен',
boss_b2: 'Босс §14 повержен',
boss_b3: 'Босс §15 повержен',
boss_b4: 'Босс §16 повержен',
boss_b5: 'Босс §17 повержен',
boss_b6: 'Босс §18 повержен',
boss_b7: 'Чемпион неравенств',
all_bosses: 'Все 7 боссов побеждены!',
prac_streak: 'Серия из 5 верных',
};
function loadProgress(){
try{
const s = localStorage.getItem('algebra8_ch3_progress');
if(s) Object.assign(STATE.progress, JSON.parse(s));
const a = localStorage.getItem('algebra8_ch3_achievements');
if(a){
const p = JSON.parse(a);
if(Array.isArray(p)) p.forEach(id => STATE.achievements.set(id, ACH_LABELS[id] || id));
else if(p && typeof p === 'object'){
for(const [id, t] of Object.entries(p)) STATE.achievements.set(id, (t && t !== id) ? t : (ACH_LABELS[id] || id));
}
}
// Общий XP для всех глав
let xp = localStorage.getItem('algebra8_xp');
if(xp === null){
const c1 = +(localStorage.getItem('algebra8_ch1_xp') || 0);
const c2 = +(localStorage.getItem('algebra8_ch2_xp') || 0);
xp = c1 + c2;
try { localStorage.setItem('algebra8_xp', String(xp)); } catch(e){}
}
STATE.xp = +xp || 0; STATE.level = calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('algebra8_ch3_progress', JSON.stringify(STATE.progress));
localStorage.setItem('algebra8_ch3_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('algebra8_xp', String(STATE.xp));
}catch(e){}
}
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);
}
/* Server sync прогресса */
const _TB_SLUG = 'algebra-8-ch3';
const _markedRead = new Set();
let _pendingProgressBody = null, _progressTimer = null;
function _flushProgress(){
const body = _pendingProgressBody;
_pendingProgressBody = null;
if(!body) return;
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
if(!tok) return;
fetch('/api/textbooks/' + _TB_SLUG + '/progress', {
method:'POST',
headers:{ 'Content-Type':'application/json', 'Authorization':'Bearer ' + tok },
body: JSON.stringify(body),
keepalive: true,
}).catch(()=>{});
}
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(){
const tok = (window.LS && LS.getToken) ? LS.getToken() : '';
if(!tok) return;
fetch('/api/textbooks/' + _TB_SLUG, { headers:{ 'Authorization':'Bearer ' + tok } })
.then(r => r.ok ? r.json() : null)
.then(d => {
if(!d || !d.progress) return;
(d.progress.read || []).forEach(k => {
_markedRead.add(k);
if((STATE.progress[k] || 0) < 50) STATE.progress[k] = 100;
});
saveProgress(); refreshProgressUI();
})
.catch(()=>{});
}
function addXp(n, src){
if(!n) return;
const 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, 'algebra8-ch3-' + (src || 'misc'));
if(STATE.level > prev){
const pop = document.getElementById('ach-popup');
if(pop){
document.getElementById('ach-text').textContent = 'Уровень ' + STATE.level + '!';
pop.classList.add('show');
setTimeout(()=>pop.classList.remove('show'), 2600);
}
if(window.confetti) try { confetti(); } catch(e){}
}
}
function refreshProgressUI(){
const total = Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0) / 7);
const f = document.getElementById('hero-hp-fill');
if(f) f.style.width = total + '%';
const t = document.getElementById('hero-hp-text');
if(t) t.textContent = total + '% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(el=>{
const k = el.dataset.progCard;
const fl = el.querySelector('.psel-prog-fill');
if(fl) fl.style.width = (STATE.progress[k]||0) + '%';
});
const xpBadge = document.getElementById('hero-xp-badge');
if(xpBadge){
xpBadge.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg> Ур. ' + STATE.level + ' · ' + (STATE.xp || 0) + ' XP';
}
if(STATE.current && document.getElementById('sidebar-content')){
try { buildSidebar(STATE.current); } catch(e){}
}
}
function achievement(id, text){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, text || ACH_LABELS[id] || id);
saveProgress();
const pop = document.getElementById('ach-popup');
if(pop){
document.getElementById('ach-text').textContent = text || ACH_LABELS[id] || id;
pop.classList.add('show');
setTimeout(()=>pop.classList.remove('show'), 3300);
}
addXp(20, 'ach-' + id);
}
/* PARAGRAPH LIST */
const PARAS = [
{ id:'p13', num:'§ 13', name:'Числовые неравенства', sub:'Свойства' },
{ id:'p14', num:'§ 14', name:'Сложение и умножение', sub:'Оценка значений' },
{ id:'p15', num:'§ 15', name:'Промежутки и линейные', sub:'Графика и решение' },
{ id:'p16', num:'§ 16', name:'Системы неравенств', sub:'Пересечение и объединение' },
{ id:'p17', num:'§ 17', name:'Квадратные неравенства', sub:'Метод интервалов' },
{ id:'p18', num:'§ 18', name:'Дробно-рациональные', sub:'Интервалы и ОДЗ' },
{ id:'final3', num:'★', name:'Финал главы', sub:'Итоги · Практика', final:true },
];
function buildParaSelector(){
const g = document.getElementById('psel-grid');
g.innerHTML = '';
PARAS.forEach(p=>{
const card = document.createElement('div');
card.className = 'psel-card' + (p.final ? ' final' : '');
card.dataset.id = p.id;
card.dataset.progCard = p.id;
card.innerHTML = `
<div class="psel-num">${p.num}</div>
<div class="psel-name">${p.name}</div>
<div class="psel-prog"><div class="psel-prog-fill"></div></div>`;
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
}
const BUILT = new Set();
const BUILDERS = {
p13:()=>buildP13(), p14:()=>buildP14(),
p15:()=>buildP15stub(), p16:()=>buildP16stub(),
p17:()=>buildP17stub(), p18:()=>buildP18stub(),
final3:()=>buildFinal3stub(),
};
function ensureBuilt(id){
if(BUILT.has(id)) return;
const fn = BUILDERS[id];
if(fn){ fn(); BUILT.add(id); }
}
function goTo(id){
STATE.current = id;
ensureBuilt(id);
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
const el = document.getElementById('sec-' + id);
if(el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id === id));
buildSidebar(id);
window.scrollTo({top:0, behavior:'smooth'});
if((STATE.progress[id]||0) < 10) bumpProgress(id, 10);
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
setTimeout(()=>{ try { wrapGlossary(el); } catch(e){} }, 60);
markLastPara(id);
}
/* SIDEBAR */
const SIDEBARS = {
p13: { title:'Шпаргалка § 13', rows:[
['$a > b$','значит $a - b > 0$'],
['Транзитивность','$a > b,\\ b > c \\Rightarrow a > c$'],
['$+c$ к обеим','знак не меняется'],
['$\\times k > 0$','знак не меняется'],
['$\\times k < 0$','знак МЕНЯЕТСЯ'],
]},
p14: { title:'Шпаргалка § 14', rows:[
['Сложение','$a < b,\\ c < d \\Rightarrow a + c < b + d$'],
['Умножение (+)','$0<a<b,\\ 0<c<d \\Rightarrow ac<bd$'],
['Оценка','если $a \\leq x \\leq b$ и $c \\leq y \\leq d$, то $a+c \\leq x+y \\leq b+d$'],
['Разность','$x - y$: $a - d \\leq x-y \\leq b - c$'],
]},
p15: { title:'Шпаргалка § 15', rows:[
['$[a;b]$','отрезок: $a \\leq x \\leq b$'],
['$(a;b)$','интервал: $a < x < b$'],
['$[a;b)$','полуинтервал'],
['$(-\\infty;a)$','луч $x < a$'],
['Линейное','$ax + b > 0 \\Rightarrow$ изоляция $x$'],
]},
p16: { title:'Шпаргалка § 16', rows:[
['Система','И · пересечение решений'],
['Совокупность','ИЛИ · объединение решений'],
['Открытая точка','знак $<$ или $>$ без равенства'],
]},
p17: { title:'Шпаргалка § 17', rows:[
['Метод интервалов','найти корни → отметить → знаки'],
['$a > 0$','парабола вверх'],
['$a < 0$','парабола вниз'],
['$D < 0$','знак не меняется'],
]},
p18: { title:'Шпаргалка § 18', rows:[
['$\\dfrac{f(x)}{g(x)} \\geq 0$','знаки числителя и знаменателя'],
['ОДЗ','знаменатель $\\neq 0$'],
['Полюс','выколотая точка на прямой'],
]},
final3: { title:'Финал главы', rows:[
['7 боссов','один на каждый § + общий'],
['Награда','«Чемпион неравенств»'],
['Практика','случайные задачи всей главы'],
]},
};
const TIPS = [
{ sec:'p13', html:'При умножении на $-1$ <b>знак неравенства меняется</b>. Это самое частое место ошибок.' },
{ sec:'p14', html:'Складывать можно <b>одного знака</b>: $a<b$ и $c<d$, но НЕ $a<b$ и $c>d$.' },
{ sec:'p15', html:'Квадратная скобка $[$ — точка <b>входит</b> в множество. Круглая $($ — <b>выколота</b>.' },
{ sec:'p16', html:'Система — это «И», пересечение. Совокупность — «ИЛИ», объединение. Не путайте.' },
{ sec:'p17', html:'Знак между корнями определяется коэффициентом $a$: если $a>0$, между корнями знак минус.' },
{ sec:'p18', html:'Точки, где знаменатель $=0$, всегда <b>выколотые</b>, даже при нестрогом неравенстве.' },
{ sec:'final3', html:'Метод интервалов — универсальный. Освойте его на §17, и §18 пойдёт легко.' },
];
function buildSidebar(id){
const box = document.getElementById('sidebar-content');
const sb = SIDEBARS[id] || SIDEBARS.p13;
let html = '';
// XP card
const xpForLv = _xpForLevel(STATE.level);
const xpNext = _xpForLevel(STATE.level + 1);
const xpInLv = STATE.xp - xpForLv;
const xpRange = xpNext - xpForLv;
const xpPct = xpRange > 0 ? Math.round(xpInLv / xpRange * 100) : 100;
html += `<div class="xp-card">
<div class="xp-card-title">
<span>XP-прогресс</span>
<span class="xp-level">Ур. ${STATE.level}</span>
</div>
<div class="xp-bar"><div class="xp-fill" style="width:${xpPct}%"></div></div>
<div class="xp-nums"><span>${STATE.xp} XP</span><span>${xpNext} XP</span></div>
</div>`;
// Шпаргалка
html += `<div class="sidecard"><h4>${sb.title}</h4>`;
sb.rows.forEach(([k,v])=>{
html += `<div class="sidecard-row"><b>${k}</b> ${v ? '— ' + v : ''}</div>`;
});
html += '</div>';
// Совет дня
const tip = TIPS.find(t => t.sec === id) || TIPS[0];
html += `<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)">
<h4 style="color:#92400e;display:flex;align-items:center;gap:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/><circle cx="12" cy="12" r="4"/></svg>
Подсказка
</h4>
<div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">${tip.html}</div>
</div>`;
// Достижения
if(STATE.achievements.size > 0){
html += `<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">${STATE.achievements.size}</span></h4>`;
[...STATE.achievements.values()].slice(-4).forEach(text=>{
html += `<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">✓ ${text}</div>`;
});
html += '</div>';
}
box.innerHTML = html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
/* THEME */
function initTheme(){
const t = localStorage.getItem('algebra8_ch3_theme') || 'light';
if(t === 'dark') document.documentElement.classList.add('dark');
document.getElementById('theme-lab').textContent = t === 'dark' ? 'Светлая' : 'Тёмная';
document.getElementById('theme-btn').addEventListener('click', ()=>{
document.documentElement.classList.toggle('dark');
const dark = document.documentElement.classList.contains('dark');
localStorage.setItem('algebra8_ch3_theme', dark ? 'dark' : 'light');
document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
});
}
/* HELPERS */
function $(sel, root){ return (root||document).querySelector(sel); }
function $$(sel, root){ return [...(root||document).querySelectorAll(sel)]; }
function el(tag, attrs, html){
const e = document.createElement(tag);
if(attrs) Object.entries(attrs).forEach(([k,v])=>{
if(k === 'class') e.className = v;
else if(k === 'style') e.style.cssText = v;
else if(k.startsWith('on') && typeof v === 'function') e.addEventListener(k.slice(2), v);
else e.setAttribute(k, v);
});
if(html != null) e.innerHTML = html;
return e;
}
function renderMath(root){
if(window.renderMathInElement){
try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){}
}
}
function feedback(elm, ok, text){
elm.className = 'feedback ' + (ok ? 'ok' : 'fail');
elm.innerHTML = text || (ok ? '&#10003; Верно!' : '&#10007; Неверно');
}
function sleep(ms){ return new Promise(r => setTimeout(r, ms)); }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n - Math.round(n)) < 1e-9 ? String(Math.round(n)) : (+n.toFixed(4)).toString(); }
const ICONS = {
repeat: '<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory: '<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo: '<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule: '<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example: '<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral: '<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>',
class: '<svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="14" rx="1"/><line x1="3" y1="21" x2="21" y2="21"/><polyline points="7 14 10 11 13 14 17 10"/></svg>',
home: '<svg class="ic" viewBox="0 0 24 24"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>',
};
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно',class:'Класс',home:'Домашка'};
return `<div class="card">
<div class="card-header">
<div class="card-icon ${kind}">${ICONS[kind]}</div>
<div class="card-title">${labels[kind] || ''} ${title && title !== labels[kind] ? '· ' + title : ''}</div>
${num ? `<div class="card-num">${num}</div>` : ''}
</div>
<div class="card-body">${body}</div>
</div>`;
}
function widget(title, badge, helpText, body){
return `<div class="wg">
<div class="wg-header"><span class="wg-badge">${badge||'INTERACT'}</span><div class="wg-title">${title}</div></div>
${helpText ? `<div class="wg-help">${helpText}</div>` : ''}
${body}
</div>`;
}
function secNav(prev, next){
const NAMES = {p13:'§13',p14:'§14',p15:'§15',p16:'§16',p17:'§17',p18:'§18',final3:'Финал'};
let h = '<div class="sec-nav">';
h += prev ? `<button class="btn" onclick="goTo('${prev}')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> ${NAMES[prev]}</button>` : '<span></span>';
h += next ? `<button class="btn primary" onclick="goTo('${next}')">${NAMES[next]} <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>` : '<span></span>';
h += '</div>';
return h;
}
/* CONFETTI */
let _confettiCanvas = null, _confettiParticles = [], _confettiRaf = null;
function confetti(){
if(!_confettiCanvas){
_confettiCanvas = document.createElement('canvas');
_confettiCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:9999';
document.body.appendChild(_confettiCanvas);
}
const c = _confettiCanvas;
c.width = window.innerWidth; c.height = window.innerHeight;
const ctx = c.getContext('2d');
const colors = ['#6366f1','#8b5cf6','#06b6d4','#10b981','#f59e0b'];
for(let i = 0; i < 80; i++){
_confettiParticles.push({
x: window.innerWidth/2 + (Math.random()-0.5)*200,
y: window.innerHeight/2,
vx: (Math.random()-0.5)*14,
vy: -10 - Math.random()*10,
g: 0.4, life: 100, color: colors[i%colors.length], r: 4+Math.random()*4, rot: 0, vRot: (Math.random()-0.5)*0.3,
});
}
if(_confettiRaf) cancelAnimationFrame(_confettiRaf);
function frame(){
ctx.clearRect(0,0,c.width,c.height);
_confettiParticles = _confettiParticles.filter(p=>{
p.x += p.vx; p.y += p.vy; p.vy += p.g; p.life--; p.rot += p.vRot;
ctx.save(); ctx.translate(p.x, p.y); ctx.rotate(p.rot);
ctx.fillStyle = p.color;
ctx.fillRect(-p.r, -p.r/2, p.r*2, p.r);
ctx.restore();
return p.life > 0 && p.y < c.height + 50;
});
if(_confettiParticles.length > 0) _confettiRaf = requestAnimationFrame(frame);
else { ctx.clearRect(0,0,c.width,c.height); _confettiRaf = null; }
}
frame();
}
/* DnD shared sorter */
function setupSorter(cfg){
const placed = {};
const pool = document.getElementById(cfg.poolId);
const scope = document.querySelector(cfg.scopeSelector);
if(!pool || !scope) return { placed, render: ()=>{}, reset: ()=>{} };
pool.classList.add('dnd-pool');
if(cfg.columnLayout) pool.classList.add('col');
let armed = null;
function buildChip(it, isPlaced){
const e = document.createElement('div');
e.className = 'dnd-chip' + (isPlaced ? ' placed' : '');
e.dataset.id = it.id;
e.innerHTML = '<span class="dnd-txt">' + it.html + '</span><span class="dnd-x" title="Убрать">×</span>';
attach(e, it.id);
return e;
}
function attach(elm, itId){
let ghost = null, dragging = false, sx = 0, sy = 0;
elm.addEventListener('pointerdown', ev => {
if(ev.button !== undefined && ev.button !== 0) return;
if(ev.target.classList && ev.target.classList.contains('dnd-x')){
ev.stopPropagation();
if(placed[itId]){ delete placed[itId]; render(); }
else if(armed === itId){ armed = null; render(); }
return;
}
sx = ev.clientX; sy = ev.clientY;
const r = elm.getBoundingClientRect();
const ox = ev.clientX - r.left, oy = ev.clientY - r.top;
try { elm.setPointerCapture(ev.pointerId); } catch(e){}
function onMove(e){
const dx = e.clientX - sx, dy = e.clientY - sy;
if(!dragging && Math.hypot(dx, dy) > 8){
dragging = true;
ghost = elm.cloneNode(true);
ghost.classList.remove('armed');
ghost.style.cssText = 'position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:' + r.width + 'px;left:' + (e.clientX - ox) + 'px;top:' + (e.clientY - oy) + 'px';
document.body.appendChild(ghost);
elm.classList.add('dragging');
}
if(dragging && ghost){
ghost.style.left = (e.clientX - ox) + 'px';
ghost.style.top = (e.clientY - oy) + 'px';
const under = document.elementsFromPoint(e.clientX, e.clientY);
scope.querySelectorAll('.drop-box.over, .dnd-pool.over').forEach(n => n.classList.remove('over'));
const tgt = under.find(n => n.classList && (n.classList.contains('drop-box') || n.classList.contains('dnd-pool')));
if(tgt) tgt.classList.add('over');
}
}
function onUp(e){
elm.removeEventListener('pointermove', onMove);
elm.removeEventListener('pointerup', onUp);
elm.removeEventListener('pointercancel', onUp);
elm.classList.remove('dragging');
if(ghost){ ghost.remove(); ghost = null; }
scope.querySelectorAll('.drop-box.over, .dnd-pool.over').forEach(n => n.classList.remove('over'));
if(dragging){
const under = document.elementsFromPoint(e.clientX, e.clientY);
const box = under.find(n => n.classList && n.classList.contains('drop-box'));
const pl = under.find(n => n.classList && n.classList.contains('dnd-pool'));
if(box){
const di = box.querySelector('[data-cat]');
if(di){ placed[itId] = di.dataset.cat; armed = null; render(); return; }
} else if(pl){ delete placed[itId]; armed = null; render(); return; }
} else {
if(placed[itId]){ delete placed[itId]; armed = null; render(); }
else { armed = (armed === itId) ? null : itId; render(); }
}
dragging = false;
}
elm.addEventListener('pointermove', onMove);
elm.addEventListener('pointerup', onUp);
elm.addEventListener('pointercancel', onUp);
});
}
function attachBoxTaps(){
scope.querySelectorAll('.drop-box').forEach(box => {
box.addEventListener('click', ev => {
if(!armed) return;
if(ev.target.closest('.dnd-chip')) return;
const di = box.querySelector('[data-cat]');
if(di){ placed[armed] = di.dataset.cat; armed = null; render(); }
});
});
}
function render(){
pool.innerHTML = '';
cfg.items.forEach(it => { if(placed[it.id]) return; const c = buildChip(it, false); if(armed === it.id) c.classList.add('armed'); pool.appendChild(c); });
cfg.cats.forEach(cat => {
const box = scope.querySelector('.drop-items[data-cat="' + cat + '"]');
if(!box) return;
box.innerHTML = '';
cfg.items.forEach(it => { if(placed[it.id] !== cat) return; box.appendChild(buildChip(it, true)); });
});
if(window.renderMathInElement) try { renderMath(scope); } catch(_){}
}
attachBoxTaps();
render();
return { placed, render, reset(){ for(const k in placed) delete placed[k]; armed = null; render(); } };
}
const DND_HINT_HTML = '<div class="dnd-hint"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 11V6a3 3 0 0 1 6 0v5"/><path d="M9 11h6v8a4 4 0 0 1-8 0z"/></svg> Перетащите карточку или нажмите её, затем — на нужный ящик.</div>';
/* GLOSSARY */
const GLOSSARY = [
{ term:'числовое неравенство', def:'Запись вида $a > b$ или $a < b$, где $a, b$ — числа.', sec:'p13', aliases:['числовое неравенство','числовых неравенств','числового неравенства'] },
{ term:'свойства неравенств', def:'Транзитивность, прибавление, умножение на положительное/отрицательное (смена знака).', sec:'p13', aliases:['свойства неравенств','свойств неравенств'] },
{ term:'оценка значения', def:'Если $a \\leq x \\leq b$, найти границы для $x+y$, $xy$, $x/y$, и т.п.', sec:'p14', aliases:['оценка значения','оценка значений'] },
{ term:'числовой промежуток', def:'Отрезок, интервал, полуинтервал или луч на числовой прямой.', sec:'p15', aliases:['числовой промежуток','числового промежутка','числовые промежутки','числовых промежутков','промежуток','промежутка','промежутки','промежутков'] },
{ term:'линейное неравенство', def:'Неравенство вида $ax + b > 0$ (или с другим знаком), $a \\neq 0$.', sec:'p15', aliases:['линейное неравенство','линейных неравенств','линейные неравенства','линейного неравенства'] },
{ term:'система неравенств', def:'Несколько неравенств, выполненных одновременно (И). Решение — пересечение.', sec:'p16', aliases:['система неравенств','системы неравенств','систем неравенств','системе неравенств'] },
{ term:'совокупность неравенств', def:'Несколько неравенств, из которых выполняется хотя бы одно (ИЛИ). Решение — объединение.', sec:'p16', aliases:['совокупность неравенств','совокупности неравенств','совокупностей неравенств'] },
{ term:'квадратное неравенство', def:'Неравенство $ax^2 + bx + c \\gtrless 0$, $a \\neq 0$.', sec:'p17', aliases:['квадратное неравенство','квадратных неравенств','квадратные неравенства','квадратного неравенства'] },
{ term:'метод интервалов', def:'Найти корни → отметить на прямой → определить знаки на интервалах.', sec:'p17', aliases:['метод интервалов','методом интервалов','методу интервалов'] },
{ term:'дробно-рациональное неравенство', def:'Неравенство, содержащее дробь $\\dfrac{f(x)}{g(x)}$. Решается методом интервалов с учётом ОДЗ.', sec:'p18', aliases:['дробно-рациональное','дробно-рациональных','дробно-рациональные','дробно-рациональным'] },
{ term:'ОДЗ', def:'Область допустимых значений. Для дробей — знаменатель $\\neq 0$.', sec:'p18', aliases:['ОДЗ','область допустимых значений'] },
{ term:'выколотая точка', def:'Точка на прямой, не входящая в множество (например, при строгом $<$ или знаменатель $=0$).', sec:'p15', aliases:['выколотая точка','выколотой точки','выколотых точек','выколотые точки'] },
];
function wrapGlossary(root){
if(!root || root.__glossDone) return;
const allAliases = [];
GLOSSARY.forEach((g, i) => g.aliases.forEach(a => allAliases.push({ a, i })));
allAliases.sort((x, y) => y.a.length - x.a.length);
const re = new RegExp('(?<![\\w-])(' + allAliases.map(x => x.a.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\$&')).join('|') + ')(?![\\w-])', 'iu');
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
acceptNode(node){
const p = node.parentElement;
if(!p) return NodeFilter.FILTER_REJECT;
if(p.closest('.katex, .gloss-term, button, input, select, .wg-badge, .card-icon, .sec-num, .psel-num, .hdr, .ach-popup, script, style, .search-modal, .sidecard, .gloss-tip')) return NodeFilter.FILTER_REJECT;
if(!re.test(node.nodeValue)) return NodeFilter.FILTER_REJECT;
return NodeFilter.FILTER_ACCEPT;
}
});
const nodes = [];
let n; while((n = walker.nextNode())) nodes.push(n);
nodes.forEach(node => {
const text = node.nodeValue;
const out = document.createDocumentFragment();
let cursor = 0;
const global = new RegExp(re.source, 'giu');
let m;
while((m = global.exec(text)) !== null){
if(m.index > cursor) out.appendChild(document.createTextNode(text.slice(cursor, m.index)));
const found = m[0].toLowerCase();
const hit = allAliases.find(x => x.a.toLowerCase() === found);
const g = hit ? GLOSSARY[hit.i] : null;
const sp = document.createElement('span');
sp.className = 'gloss-term';
sp.dataset.gloss = g ? g.term : '';
sp.textContent = m[0];
out.appendChild(sp);
cursor = m.index + m[0].length;
}
if(cursor < text.length) out.appendChild(document.createTextNode(text.slice(cursor)));
node.parentNode.replaceChild(out, node);
});
root.__glossDone = true;
}
function initGlossaryTip(){
const tip = document.getElementById('gloss-tip');
if(!tip) return;
let lockOpen = null;
function show(elm){
const g = GLOSSARY.find(x => x.term === elm.dataset.gloss);
if(!g) return;
tip.innerHTML = '<b>' + g.term[0].toUpperCase() + g.term.slice(1) + '</b><div style="margin-top:4px">' + g.def + '</div><div style="margin-top:6px;font-size:.72rem;color:var(--muted);text-transform:uppercase;letter-spacing:.06em">См. §&nbsp;' + g.sec.replace('p','') + '</div>';
if(window.renderMathInElement) renderMath(tip);
const r = elm.getBoundingClientRect();
tip.classList.add('show');
const tw = tip.offsetWidth, th = tip.offsetHeight;
let left = r.left, top = r.bottom + 8;
if(left + tw > window.innerWidth - 12) left = window.innerWidth - tw - 12;
if(top + th > window.innerHeight - 12) top = r.top - th - 8;
tip.style.left = Math.max(8, left) + 'px';
tip.style.top = Math.max(8, top) + 'px';
}
function hide(){ tip.classList.remove('show'); }
document.addEventListener('mouseover', e => {
const elm = e.target.closest && e.target.closest('.gloss-term');
if(elm && !lockOpen) show(elm);
});
document.addEventListener('mouseout', e => {
const elm = e.target.closest && e.target.closest('.gloss-term');
if(elm && !lockOpen) hide();
});
document.addEventListener('click', e => {
const elm = e.target.closest && e.target.closest('.gloss-term');
if(elm){
if(lockOpen === elm){ lockOpen = null; hide(); }
else { lockOpen = elm; show(elm); }
} else if(lockOpen && !e.target.closest('.gloss-tip')){
lockOpen = null; hide();
}
});
}
/* SEARCH */
const SEARCH_INDEX = (function(){
const arr = [];
PARAS.forEach(p => arr.push({ kind:'Параграф', title:p.num + ' ' + p.name, desc:p.sub || '', sec:p.id }));
GLOSSARY.forEach(g => arr.push({ kind:'Понятие', title:g.term, desc:g.def.replace(/\$/g,''), sec:g.sec, gloss:g.term }));
[
['Формула','a > b ⟺ a b > 0','§13 — определение','p13'],
['Формула','×k<0 ⟹ знак МЕНЯЕТСЯ','§13 — главное правило','p13'],
['Формула','a<b, c<d ⟹ a+c<b+d','§14 — сложение неравенств','p14'],
['Формула','[a;b], (a;b), [a;b)','§15 — обозначения промежутков','p15'],
['Формула','Метод интервалов: корни → знаки','§17 — алгоритм','p17'],
].forEach(([k,t,d,s]) => arr.push({ kind:k, title:t, desc:d, sec:s }));
arr.push({ kind:'Финал', title:'Боссы главы', desc:'7 проверочных боссов', sec:'final3' });
return arr;
})();
function initSearch(){
const modal = document.getElementById('search-modal');
const inp = document.getElementById('search-input');
const out = document.getElementById('search-results');
const btn = document.getElementById('search-btn');
if(!modal || !inp || !out) return;
let cur = 0, rows = [];
function score(q, it){
const t = (it.title + ' ' + it.desc).toLowerCase();
if(t.includes(q)) return 100 + (it.title.toLowerCase().startsWith(q) ? 50 : 0);
let s = 0; q.split(/\s+/).forEach(w => { if(w && t.includes(w)) s += 10; });
return s;
}
function rank(q){
q = q.trim().toLowerCase();
if(!q) return SEARCH_INDEX.slice(0, 12);
return SEARCH_INDEX.map(it => ({ it, s: score(q, it) })).filter(x => x.s > 0).sort((a,b) => b.s - a.s).slice(0, 20).map(x => x.it);
}
function render(){
cur = 0;
if(!rows.length){ out.innerHTML = '<div class="search-empty">Ничего не найдено</div>'; return; }
out.innerHTML = rows.map((r, i) => `<button class="search-row${i === 0 ? ' active' : ''}" data-i="${i}"><div class="sr-kind">${r.kind}</div><div class="sr-title">${r.title}</div>${r.desc ? `<div class="sr-desc">${r.desc.length > 90 ? r.desc.slice(0, 90) + '…' : r.desc}</div>` : ''}</button>`).join('');
out.querySelectorAll('.search-row').forEach(b => b.addEventListener('click', ()=>{ cur = +b.dataset.i; pick(); }));
}
function pick(){
const r = rows[cur]; if(!r) return;
close();
goTo(r.sec);
if(r.gloss){
setTimeout(()=>{
const sec = document.getElementById('sec-' + r.sec);
const elm = sec && sec.querySelector('[data-gloss="' + r.gloss + '"]');
if(elm){ elm.scrollIntoView({ behavior:'smooth', block:'center' }); elm.style.transition = 'background .3s'; elm.style.background = 'var(--warn,#f59e0b)'; setTimeout(()=>{ elm.style.background = ''; }, 1400); }
}, 400);
}
}
function move(d){
const items = out.querySelectorAll('.search-row'); if(!items.length) return;
items[cur] && items[cur].classList.remove('active');
cur = (cur + d + items.length) % items.length;
items[cur].classList.add('active');
items[cur].scrollIntoView({ block:'nearest' });
}
function open(){ modal.classList.add('show'); inp.value = ''; rows = rank(''); render(); setTimeout(()=>inp.focus(), 50); }
function close(){ modal.classList.remove('show'); }
btn && btn.addEventListener('click', open);
modal.addEventListener('click', e => { if(e.target === modal) close(); });
inp.addEventListener('input', ()=>{ rows = rank(inp.value); render(); });
inp.addEventListener('keydown', e => {
if(e.key === 'ArrowDown'){ e.preventDefault(); move(1); }
else if(e.key === 'ArrowUp'){ e.preventDefault(); move(-1); }
else if(e.key === 'Enter'){ e.preventDefault(); pick(); }
else if(e.key === 'Escape'){ e.preventDefault(); close(); }
});
document.addEventListener('keydown', e => {
if((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')){
e.preventDefault();
if(modal.classList.contains('show')) close(); else open();
}
});
}
/* SIDEBAR TOGGLE */
function initSidebarToggle(){
const side = document.getElementById('col-side');
const back = document.getElementById('col-side-backdrop');
const btn = document.getElementById('sidebar-btn');
if(!side || !btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click', ()=>{
if(side.classList.contains('open')) close();
else open();
});
back.addEventListener('click', close);
document.addEventListener('keydown', e=>{ if(e.key === 'Escape') close(); });
}
/* INIT */
function init(){
loadProgress();
initTheme();
initSidebarToggle();
initGlossaryTip();
initSearch();
buildParaSelector();
refreshProgressUI();
loadServerReadState();
goTo('p13');
setTimeout(()=>achievement('start','Начало главы 3!'), 600);
if(window.LS && window.LS.xp){
window.LS.xp.load().then(function(s){
if(s && s.xp > STATE.xp){
STATE.xp = s.xp;
STATE.level = calcLevel(STATE.xp);
saveProgress();
refreshProgressUI();
if(STATE.current) buildSidebar(STATE.current);
}
});
}
}
document.addEventListener('DOMContentLoaded', init);
/* STUBS */
function buildP15stub(){ document.getElementById('p15-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 15 — Числовые промежутки. Линейные неравенства</b><br><br>Будет в Wave 2.</p></div></div>${secNav('p14','p16')}`; }
function buildP16stub(){ document.getElementById('p16-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 16 — Системы неравенств</b><br><br>Будет в Wave 2.</p></div></div>${secNav('p15','p17')}`; }
function buildP17stub(){ document.getElementById('p17-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 17 — Квадратные неравенства. Метод интервалов</b><br><br>Будет в Wave 3.</p></div></div>${secNav('p16','p18')}`; }
function buildP18stub(){ document.getElementById('p18-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>§ 18 — Дробно-рациональные неравенства</b><br><br>Будет в Wave 3.</p></div></div>${secNav('p17','final3')}`; }
function buildFinal3stub(){ document.getElementById('final3-body').innerHTML = `<div class="card"><div class="card-body"><p style="text-align:center;padding:20px"><b>Финал главы</b><br><br>Будет в Wave 4 — 7 боссов, увлекательная математика, практика.</p></div></div>${secNav('p18',null)}`; }
</script>
<script>
/* ============================================================
§ 13 — ЧИСЛОВЫЕ НЕРАВЕНСТВА И ИХ СВОЙСТВА
============================================================ */
function buildP13(){
const box = document.getElementById('p13-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Действительные числа на числовой прямой: левее — меньше, правее — больше.</li>
<li>Знаки: $<$ (меньше), $>$ (больше), $\\leq$, $\\geq$, $\\neq$.</li>
<li>$|a|$ — модуль: расстояние от точки $a$ до нуля.</li>
</ul>`);
html += makeCard('theory','Что значит «$a > b$»','13.1',`
<p><b>Определение.</b> Число $a$ больше числа $b$, если разность $a - b$ — положительное число.</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:14px;margin:10px 0;text-align:center;font-size:1.15rem">$$a > b \\iff a - b > 0,\\qquad a < b \\iff a - b < 0$$</div>
<p>Это удобно, потому что любое сравнение сводится к проверке знака разности.</p>`);
html += makeCard('rule','5 главных свойств','13.2',`
<ol style="margin-left:18px;line-height:1.9">
<li><b>Транзитивность:</b> $a > b$ и $b > c \\Rightarrow a > c$.</li>
<li><b>Прибавление:</b> $a > b \\Rightarrow a + c > b + c$ (знак не меняется).</li>
<li><b>Умножение/деление на положительное:</b> $a > b,\\ k > 0 \\Rightarrow ak > bk$.</li>
<li><b>Умножение/деление на отрицательное:</b> $a > b,\\ k < 0 \\Rightarrow ak < bk$ — <span style="color:var(--bad);font-weight:700">знак меняется!</span></li>
<li><b>Возведение в квадрат для положительных:</b> $a > b > 0 \\Rightarrow a^2 > b^2$.</li>
</ol>`);
html += makeCard('example','Примеры',null,`
<p><b>1)</b> Сравним $\\sqrt{5}$ и $2{,}2$. Возведём в квадрат: $5$ и $4{,}84$. Поскольку $5 > 4{,}84$ и обе величины положительные, $\\sqrt{5} > 2{,}2$.</p>
<p><b>2)</b> Из $a > b$ умножим на $-3$: получим $-3a < -3b$. Знак ПОМЕНЯЛСЯ.</p>
<p><b>3)</b> Дано $a > 0$ и $b < 0$. Тогда $a - b > 0$, то есть $a > b$ (естественно: положительное больше отрицательного).</p>`);
/* INT 1 — Сравни числа (drag-сортировка) */
html += widget('Расставь по возрастанию','INTERACT 1','Перетащи числа в правильном порядке — слева направо. Проверь нажатием.',`
${DND_HINT_HTML}
<div id="p13a-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(5, 1fr);gap:6px">
<div class="drop-box" style="min-height:60px"><h5>1-й</h5><div class="drop-items" data-cat="1"></div></div>
<div class="drop-box" style="min-height:60px"><h5>2-й</h5><div class="drop-items" data-cat="2"></div></div>
<div class="drop-box" style="min-height:60px"><h5>3-й</h5><div class="drop-items" data-cat="3"></div></div>
<div class="drop-box" style="min-height:60px"><h5>4-й</h5><div class="drop-items" data-cat="4"></div></div>
<div class="drop-box" style="min-height:60px"><h5>5-й</h5><div class="drop-items" data-cat="5"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p13a-check">Проверить</button><button class="btn" id="p13a-new">Новая партия</button></div>
<div class="feedback" id="p13a-fb" style="display:none"></div>`);
/* INT 2 — Свойства: знак меняется или нет */
html += widget('Знак меняется или нет?','INTERACT 2','Дано $a > b$. Что произойдёт со знаком после операции?',`
<div class="score-display"><span>Раунд <b id="p13s-i">1</b> / 8</span><span>Очки: <b id="p13s-score">0</b></span></div>
<div id="p13s-task" style="font-size:1.2rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<button class="btn" id="p13s-keep">Не меняется</button>
<button class="btn" id="p13s-flip">Меняется</button>
</div>
<div class="feedback" id="p13s-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p13s-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Конструктор: a > b + операция */
html += widget('Конструктор операций','INTERACT 3','Выбери $a$, $b$, операцию и число. Смотри, как меняется неравенство.',`
<div class="sliders">
<label>$a$ = <b id="p13c-a-val">8</b><input type="range" min="-10" max="10" step="1" value="8" id="p13c-a"></label>
<label>$b$ = <b id="p13c-b-val">3</b><input type="range" min="-10" max="10" step="1" value="3" id="p13c-b"></label>
<label>$k$ = <b id="p13c-k-val">2</b><input type="range" min="-5" max="5" step="1" value="2" id="p13c-k"></label>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin:10px 0">
<button class="btn small p13c-op active" data-op="add">$+ k$</button>
<button class="btn small p13c-op" data-op="sub">$- k$</button>
<button class="btn small p13c-op" data-op="mul">$\\times k$</button>
<button class="btn small p13c-op" data-op="div">$\\div k$</button>
</div>
<div id="p13c-out" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;line-height:1.8;text-align:center"></div>`);
/* INT 4 — Цепочка свойств: пошаговое доказательство */
html += widget('Цепочка свойств','INTERACT 4','Дано: $a > 5$. Подбери правильный шаг для каждого следующего вывода.',`
<div id="p13ch-stage" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.8;min-height:60px"></div>
<div id="p13ch-opts" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
<div class="feedback" id="p13ch-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p13ch-start" style="margin-top:10px">Начать</button>`);
/* INT 5 — Drag классификация: какое свойство */
html += widget('Какое свойство применили?','INTERACT 5','Отнеси каждый переход к нужному свойству.',`
${DND_HINT_HTML}
<div id="p13d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px">
<div class="drop-box"><h5>+/ число</h5><div class="drop-items" data-cat="add"></div></div>
<div class="drop-box"><h5>×/÷ на +</h5><div class="drop-items" data-cat="mulpos"></div></div>
<div class="drop-box"><h5>×/÷ на </h5><div class="drop-items" data-cat="mulneg"></div></div>
<div class="drop-box"><h5>Транзитивность</h5><div class="drop-items" data-cat="trans"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p13d-check">Проверить</button><button class="btn" id="p13d-reset">Сначала</button></div>
<div class="feedback" id="p13d-fb" style="display:none"></div>`);
/* INT 6 — Тренажёр «Что больше?» */
html += widget('Что больше?','INTERACT 6','Сравни два выражения за 30 секунд каждое.',`
<div class="score-display"><span>Задача <b id="p13t-i">1</b> / 10</span><span>Очки: <b id="p13t-score">0</b></span></div>
<div id="p13t-task" style="display:flex;align-items:center;gap:14px;justify-content:center;padding:18px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px">
<span id="p13t-left" style="font-size:1.5rem;font-weight:700"></span>
<span style="font-size:1.3rem;color:var(--muted)">vs</span>
<span id="p13t-right" style="font-size:1.5rem;font-weight:700"></span>
</div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap">
<button class="btn" id="p13t-lt">$<$</button>
<button class="btn" id="p13t-eq">$=$</button>
<button class="btn" id="p13t-gt">$>$</button>
</div>
<div class="feedback" id="p13t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p13t-start" style="margin-top:10px">Начать</button>`);
html += makeCard('oral','Устные вопросы',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Что означает запись $a > b$ через разность?</li>
<li>Можно ли прибавить $-7$ к обеим частям $a > b$?</li>
<li>Что произойдёт со знаком при умножении на $-2$?</li>
<li>Сравните $-5$ и $-3$. Какое больше?</li>
</ol>`);
html += makeCard('class','Класс — выполните',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Сравните $\\sqrt{7}$ и $2{,}6$.</li>
<li>Дано $a > b$. Сравните: а) $a + 5$ и $b + 5$; б) $-a$ и $-b$; в) $a/3$ и $b/3$.</li>
<li>Докажите: если $a > b > 0$, то $\\dfrac{1}{a} < \\dfrac{1}{b}$.</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Сравните $\\sqrt{10}$ и $3{,}2$.</li>
<li>Дано $x < y$. Что больше: $7 - 3x$ или $7 - 3y$?</li>
<li>Известно, что $-2 < a < 3$. Найдите границы для $5a + 1$.</li>
<li>Докажите: $(a-b)^2 + (b-c)^2 + (a-c)^2 \\geq 0$ для любых $a, b, c$.</li>
</ol>`);
html += secNav(null, 'p14');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Сортировка по возрастанию */
(function(){
const sets = [
[{v:-3},{v:0},{v:2},{v:5},{v:7}],
[{v:-5,html:'$-5$'},{v:-2,html:'$-2$'},{v:0,html:'$0$'},{v:3,html:'$3$'},{v:6,html:'$6$'}],
[{v:Math.sqrt(2),html:'$\\sqrt{2}$'},{v:1.5,html:'$1{,}5$'},{v:Math.sqrt(3),html:'$\\sqrt{3}$'},{v:2,html:'$2$'},{v:Math.sqrt(5),html:'$\\sqrt{5}$'}],
[{v:-0.5,html:'$-\\dfrac{1}{2}$'},{v:0,html:'$0$'},{v:0.3,html:'$0{,}3$'},{v:0.5,html:'$\\dfrac{1}{2}$'},{v:1,html:'$1$'}],
[{v:-3.14,html:'$-\\pi$'},{v:-3,html:'$-3$'},{v:0,html:'$0$'},{v:Math.sqrt(2),html:'$\\sqrt{2}$'},{v:Math.PI,html:'$\\pi$'}],
];
let setIdx = 0, items = [], sorter;
function build(){
const cur = sets[setIdx];
items = cur.map((c, i) => ({ id: i + 1, html: c.html || ('$' + c.v + '$'), v: c.v }));
const shuffled = [...items].sort(()=>Math.random()-0.5);
sorter = setupSorter({ poolId:'p13a-pool', cats:['1','2','3','4','5'], items: shuffled, scopeSelector:'#p13-body' });
document.getElementById('p13a-fb').style.display = 'none';
}
document.getElementById('p13a-check').addEventListener('click', ()=>{
const fb = document.getElementById('p13a-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Расставьте все 5 чисел.'); return; }
const sorted = [...items].sort((a,b)=>a.v-b.v);
let ok = true;
for(let i = 0; i < sorted.length; i++){
if(sorter.placed[sorted[i].id] !== String(i + 1)){ ok = false; break; }
}
if(ok){ feedback(fb, true, '&#10003; Правильно!'); achievement('p13_compare'); bumpProgress('p13', 14); confetti(); }
else feedback(fb, false, 'Не совсем. Проверьте знаки и сравнение корней.');
});
document.getElementById('p13a-new').addEventListener('click', ()=>{ setIdx = (setIdx + 1) % sets.length; build(); });
build();
})();
/* INIT 2 — Знак меняется или нет */
(function(){
const tasks = [
{ txt:'Прибавим $+5$ к обеим частям', flip:false },
{ txt:'Умножим обе части на $-3$', flip:true },
{ txt:'Разделим обе части на $2$', flip:false },
{ txt:'Прибавим $-7$', flip:false },
{ txt:'Умножим на $-1$', flip:true },
{ txt:'Разделим на $-4$', flip:true },
{ txt:'Возведём положительные части в квадрат', flip:false },
{ txt:'Умножим на $\\dfrac{1}{2}$ (положительное)', flip:false },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p13s-i').textContent = i;
document.getElementById('p13s-task').innerHTML = '<b>Дано:</b> $a > b$. ' + cur.txt + '.';
renderMath(document.getElementById('p13s-task'));
document.getElementById('p13s-fb').style.display = 'none';
}
function answer(flip){
const fb = document.getElementById('p13s-fb'); fb.style.display = 'block';
if(flip === cur.flip){ score++; feedback(fb, true, '&#10003;'); }
else feedback(fb, false, 'Не то. Правильно: ' + (cur.flip ? 'знак МЕНЯЕТСЯ' : 'знак не меняется'));
document.getElementById('p13s-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/8'); if(score >= 6){ achievement(score === 8 ? 'p13_flip' : 'p13_sign'); bumpProgress('p13', 14); confetti(); } }, 600); }
else { i++; setTimeout(show, 800); }
}
document.getElementById('p13s-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p13s-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
document.getElementById('p13s-keep').addEventListener('click', ()=>answer(false));
document.getElementById('p13s-flip').addEventListener('click', ()=>answer(true));
})();
/* INIT 3 — Конструктор */
(function(){
const aE = document.getElementById('p13c-a'), bE = document.getElementById('p13c-b'), kE = document.getElementById('p13c-k');
const out = document.getElementById('p13c-out');
let op = 'add', done = false;
document.querySelectorAll('.p13c-op').forEach(btn => btn.addEventListener('click', ()=>{
document.querySelectorAll('.p13c-op').forEach(x=>x.classList.remove('active'));
btn.classList.add('active'); op = btn.dataset.op; refresh();
}));
function refresh(){
const a = +aE.value, b = +bE.value, k = +kE.value;
document.getElementById('p13c-a-val').textContent = a;
document.getElementById('p13c-b-val').textContent = b;
document.getElementById('p13c-k-val').textContent = k;
const sign = a > b ? '>' : a < b ? '<' : '=';
let na, nb, sym;
const flip = (op === 'mul' || op === 'div') && k < 0;
if(op === 'add'){ na = a + k; nb = b + k; sym = sign; }
else if(op === 'sub'){ na = a - k; nb = b - k; sym = sign; }
else if(op === 'mul'){ if(k === 0){ na = 0; nb = 0; sym = '='; } else { na = a * k; nb = b * k; sym = flip ? (sign === '>' ? '<' : sign === '<' ? '>' : '=') : sign; } }
else { if(k === 0){ out.innerHTML = 'Деление на $0$ не определено.'; renderMath(out); return; } na = a / k; nb = b / k; sym = flip ? (sign === '>' ? '<' : sign === '<' ? '>' : '=') : sign; }
let s = '<div>Исходно: $' + a + ' ' + sign + ' ' + b + '$</div>';
s += '<div>После операции: $' + fmt(na) + ' ' + sym + ' ' + fmt(nb) + '$</div>';
if(flip && sign !== '=') s += '<div style="color:var(--bad);font-weight:700;margin-top:4px">Знак изменился!</div>';
out.innerHTML = s; renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p13_props'); bumpProgress('p13', 12); }, 300); }
}
[aE,bE,kE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT 4 — Цепочка */
(function(){
const steps = [
{ q:'Дано $a > 5$. Прибавим $3$ к обеим частям. Что получим?', opts:['$a + 3 > 8$','$a + 3 > 5$','$a + 3 < 8$','$3a > 15$'], ok:0 },
{ q:'Из $a + 3 > 8$ умножим на $2$. Что получим?', opts:['$2a + 6 > 16$','$2a + 3 > 8$','$2a + 6 < 16$','$a + 6 > 16$'], ok:0 },
{ q:'Из $2a + 6 > 16$ вычтем $6$. Что получим?', opts:['$2a > 10$','$2a > 22$','$2a < 10$','$a > 10$'], ok:0 },
{ q:'Из $2a > 10$ разделим на $-2$. Что получим?', opts:['$-a < -5$','$-a > -5$','$-a < 5$','$a < 5$'], ok:0 },
{ q:'Какой исходный шаг мы могли бы сделать одной операцией: «из $a > 5$ к $-a < -5$»?', opts:['Умножить на $-1$','Прибавить $-5$','Возвести в квадрат','Прибавить $5$'], ok:0 },
];
let i = 0;
function show(){
const s = steps[i];
document.getElementById('p13ch-stage').innerHTML = '<b>Шаг ' + (i + 1) + ' / ' + steps.length + '.</b> ' + s.q;
renderMath(document.getElementById('p13ch-stage'));
const opts = document.getElementById('p13ch-opts'); opts.innerHTML = '';
s.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('p13ch-fb'); fb.style.display = 'block';
if(k === s.ok){ b.classList.add('ok'); feedback(fb, true, '&#10003;'); if(i >= steps.length - 1){ feedback(fb, true, '&#10003; Цепочка пройдена!'); achievement('p13_chain'); bumpProgress('p13', 14); confetti(); } else setTimeout(()=>{ i++; show(); }, 700); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то. Подумайте.'); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p13ch-fb').style.display = 'none';
}
document.getElementById('p13ch-start').addEventListener('click', ()=>{ i = 0; show(); });
})();
/* INIT 5 — Drag классификация */
(function(){
const items = [
{ id:1, html:'$a > b \\Rightarrow a + 7 > b + 7$', cat:'add' },
{ id:2, html:'$a > b \\Rightarrow 5a > 5b$', cat:'mulpos' },
{ id:3, html:'$a > b \\Rightarrow -2a < -2b$', cat:'mulneg' },
{ id:4, html:'$a > b,\\ b > c \\Rightarrow a > c$', cat:'trans' },
{ id:5, html:'$a > b \\Rightarrow a - 4 > b - 4$', cat:'add' },
{ id:6, html:'$a > b \\Rightarrow \\dfrac{a}{3} > \\dfrac{b}{3}$', cat:'mulpos' },
{ id:7, html:'$a > b \\Rightarrow \\dfrac{a}{-2} < \\dfrac{b}{-2}$', cat:'mulneg' },
{ id:8, html:'$x > 3,\\ 3 > y \\Rightarrow x > y$', cat:'trans' },
];
const sorter = setupSorter({ poolId:'p13d-pool', cats:['add','mulpos','mulneg','trans'], items, scopeSelector:'#p13-body' });
document.getElementById('p13d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p13d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все ' + items.length + '.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Идеально!'); bumpProgress('p13', 12); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p13d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p13d-fb').style.display='none'; });
})();
/* INIT 6 — Что больше? */
(function(){
function gen(){
const t = Math.floor(Math.random()*4);
if(t === 0){
const a = -5 + Math.floor(Math.random()*11);
const b = -5 + Math.floor(Math.random()*11);
return { L: '$' + a + '$', R: '$' + b + '$', cmp: a < b ? '<' : a > b ? '>' : '=' };
}
if(t === 1){
const a = 1 + Math.floor(Math.random()*8);
const b = (2 + Math.random() * 4).toFixed(1);
return { L: '$\\sqrt{' + a + '}$', R: '$' + b + '$', cmp: a < +b * +b ? '<' : a > +b * +b ? '>' : '=' };
}
if(t === 2){
const a = 1 + Math.floor(Math.random()*5), b = 1 + Math.floor(Math.random()*5);
const c = 1 + Math.floor(Math.random()*5), d = 1 + Math.floor(Math.random()*5);
const L = a/b, R = c/d;
return { L: '$\\dfrac{' + a + '}{' + b + '}$', R: '$\\dfrac{' + c + '}{' + d + '}$', cmp: L < R ? '<' : L > R ? '>' : '=' };
}
const sign = Math.random() < 0.5 ? -1 : 1;
const a = 1 + Math.floor(Math.random()*5);
const b = 1 + Math.floor(Math.random()*5);
return { L: '$' + (sign * a) + '$', R: '$' + (sign * b) + '$', cmp: sign*a < sign*b ? '<' : sign*a > sign*b ? '>' : '=' };
}
let cur = null, i = 1, score = 0;
function show(){
cur = gen();
document.getElementById('p13t-i').textContent = i;
document.getElementById('p13t-left').innerHTML = cur.L;
document.getElementById('p13t-right').innerHTML = cur.R;
renderMath(document.getElementById('p13t-task'));
document.getElementById('p13t-fb').style.display = 'none';
}
function answer(sym){
const fb = document.getElementById('p13t-fb'); fb.style.display = 'block';
if(sym === cur.cmp){ score++; feedback(fb, true, '&#10003;'); }
else feedback(fb, false, 'Не то. Правильно: ' + cur.cmp);
document.getElementById('p13t-score').textContent = score;
if(i >= 10){ setTimeout(()=>{ feedback(fb, score >= 7, 'Итог: ' + score + '/10'); if(score >= 7){ achievement('p13_compare'); bumpProgress('p13', 16); confetti(); } }, 600); }
else { i++; setTimeout(show, 700); }
}
document.getElementById('p13t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p13t-score').textContent = 0; show(); });
document.getElementById('p13t-lt').addEventListener('click', ()=>answer('<'));
document.getElementById('p13t-eq').addEventListener('click', ()=>answer('='));
document.getElementById('p13t-gt').addEventListener('click', ()=>answer('>'));
})();
}
</script>
<script>
/* ============================================================
§ 14 — СЛОЖЕНИЕ И УМНОЖЕНИЕ НЕРАВЕНСТВ. ОЦЕНКА
============================================================ */
function buildP14(){
const box = document.getElementById('p14-body');
let html = '';
html += makeCard('repeat','Повторение § 13',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Свойства неравенств: прибавление, умножение/деление на положительное (знак сохраняется), на отрицательное (меняется).</li>
<li>$a > b \\iff a - b > 0$.</li>
</ul>`);
html += makeCard('theory','Сложение неравенств','14.1',`
<p><b>Правило.</b> Если $a < b$ и $c < d$, то $a + c < b + d$.</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:12px;margin:8px 0;text-align:center;font-size:1.1rem">$$\\begin{aligned} a &< b \\\\ c &< d \\\\ \\hline a + c &< b + d \\end{aligned}$$</div>
<p><b>Важно:</b> неравенства должны быть одного направления. Складывать $a < b$ и $c > d$ напрямую нельзя.</p>`);
html += makeCard('rule','Умножение неравенств','14.2',`
<p>Для <b>положительных</b> чисел: если $0 < a < b$ и $0 < c < d$, то $ac < bd$.</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:12px;margin:8px 0;text-align:center;font-size:1.1rem">$$0 < a < b,\\quad 0 < c < d \\Rightarrow ac < bd$$</div>
<p><b>Внимание:</b> требование «положительные» обязательно. Например, $-3 < 2$ и $-3 < 2$, но $(-3)(-3) = 9 > 2 \\cdot 2 = 4$ — формула не работает для отрицательных.</p>`);
html += makeCard('algo','Оценка значений','14.3',`
<p>Если $a \\leq x \\leq b$ и $c \\leq y \\leq d$, то:</p>
<table class="tbl" style="margin:8px 0">
<thead><tr><th>Выражение</th><th>Нижняя граница</th><th>Верхняя граница</th></tr></thead>
<tbody>
<tr><td>$x + y$</td><td>$a + c$</td><td>$b + d$</td></tr>
<tr><td>$x - y$</td><td>$a - d$</td><td>$b - c$</td></tr>
<tr><td>$xy$ (все $\\geq 0$)</td><td>$ac$</td><td>$bd$</td></tr>
<tr><td>$\\dfrac{x}{y}$ (все $> 0$)</td><td>$\\dfrac{a}{d}$</td><td>$\\dfrac{b}{c}$</td></tr>
</tbody>
</table>
<p style="font-size:.88rem;color:var(--muted)">Запомни: для разности и деления — у второй переменной границы «крест-накрест».</p>`);
html += makeCard('example','Пример',null,`
<p><b>Дано:</b> $2 \\leq x \\leq 5$, $1 \\leq y \\leq 3$. <b>Найдите границы для $x + y$, $x - y$, $xy$.</b></p>
<p><b>$x + y$:</b> $2 + 1 \\leq x + y \\leq 5 + 3 \\Rightarrow 3 \\leq x+y \\leq 8$.</p>
<p><b>$x - y$:</b> $2 - 3 \\leq x - y \\leq 5 - 1 \\Rightarrow -1 \\leq x-y \\leq 4$.</p>
<p><b>$xy$ (все $> 0$):</b> $2 \\cdot 1 \\leq xy \\leq 5 \\cdot 3 \\Rightarrow 2 \\leq xy \\leq 15$.</p>`);
/* INT 1 — Калькулятор оценки */
html += widget('Калькулятор оценок','INTERACT 1','Введите границы для $x$ и $y$ — увидите границы для всех 4 выражений сразу.',`
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin-bottom:10px">
<label class="tinp" style="display:flex;align-items:center;gap:6px;background:var(--card)">$x \\geq$ <input type="number" id="p14e-a" value="2" class="tinp" style="width:60px;padding:4px;border:0"></label>
<label class="tinp" style="display:flex;align-items:center;gap:6px;background:var(--card)">$x \\leq$ <input type="number" id="p14e-b" value="5" class="tinp" style="width:60px;padding:4px;border:0"></label>
<label class="tinp" style="display:flex;align-items:center;gap:6px;background:var(--card)">$y \\geq$ <input type="number" id="p14e-c" value="1" class="tinp" style="width:60px;padding:4px;border:0"></label>
<label class="tinp" style="display:flex;align-items:center;gap:6px;background:var(--card)">$y \\leq$ <input type="number" id="p14e-d" value="3" class="tinp" style="width:60px;padding:4px;border:0"></label>
</div>
<div id="p14e-out" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.8;font-size:1rem"></div>`);
/* INT 2 — Тренажёр оценки */
html += widget('Тренажёр: найдите границу','INTERACT 2','По заданным границам $x$ и $y$ найдите верхнюю или нижнюю границу выражения.',`
<div class="score-display"><span>Задача <b id="p14t-i">1</b> / 8</span><span>Очки: <b id="p14t-score">0</b></span></div>
<div id="p14t-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.04rem;line-height:1.7;margin-bottom:10px"></div>
<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">
<input type="number" id="p14t-inp" placeholder="ваш ответ" class="tinp" style="width:140px">
<button class="btn primary" id="p14t-go">Ответ</button>
</div>
<div class="feedback" id="p14t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p14t-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Drag: можно/нельзя складывать */
html += widget('Можно ли применить правило?','INTERACT 3','Отнеси каждую пару неравенств к одной из категорий.',`
${DND_HINT_HTML}
<div id="p14d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px">
<div class="drop-box"><h5>Можно сложить</h5><div class="drop-items" data-cat="add"></div></div>
<div class="drop-box"><h5>Можно перемножить</h5><div class="drop-items" data-cat="mul"></div></div>
<div class="drop-box"><h5>Ни того, ни другого</h5><div class="drop-items" data-cat="none"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p14d-check">Проверить</button><button class="btn" id="p14d-reset">Сначала</button></div>
<div class="feedback" id="p14d-fb" style="display:none"></div>`);
/* INT 4 — Пошаговое сложение */
html += widget('Пошаговое сложение и оценка','INTERACT 4','По заданным $x, y$ — пройди 4 шага: сложение, разность, произведение, итог.',`
<p style="margin-bottom:10px"><b>Дано:</b> $1 \\leq x \\leq 4$, $2 \\leq y \\leq 6$.</p>
<div id="p14p-stage" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.8;min-height:80px"></div>
<div class="actions" style="margin-top:10px"><button class="btn primary" id="p14p-go">Старт</button><button class="btn" id="p14p-next" style="display:none">Дальше</button><button class="btn" id="p14p-reset" style="display:none">Сначала</button></div>`);
/* INT 5 — Складываем неравенства (упражнение) */
html += widget('Сложи неравенства','INTERACT 5','Дано: $a < b$ и $c < d$. Подбери, что получится после сложения.',`
<div class="score-display"><span>Раунд <b id="p14a-i">1</b> / 6</span><span>Очки: <b id="p14a-score">0</b></span></div>
<div id="p14a-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;line-height:1.6;margin-bottom:10px"></div>
<div id="p14a-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p14a-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p14a-start" style="margin-top:10px">Начать</button>`);
html += makeCard('oral','Устные вопросы',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Можно ли складывать $a > b$ и $c < d$? Почему?</li>
<li>Какое условие нужно для перемножения неравенств?</li>
<li>Если $1 \\leq x \\leq 3$, какие границы у $-x$?</li>
</ol>`);
html += makeCard('class','Класс — выполните',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$3 \\leq x \\leq 7$, $2 \\leq y \\leq 5$. Найдите границы для $x + y$, $x - y$, $xy$.</li>
<li>Сложите неравенства $a < b$ и $-c < -d$ (где $d < c$).</li>
<li>$0{,}5 \\leq a \\leq 2$, $1 \\leq b \\leq 4$. Найдите границы $\\dfrac{a}{b}$.</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$-2 \\leq x \\leq 1$, $0 \\leq y \\leq 4$. Границы для $x + y$ и $x - y$.</li>
<li>$1 \\leq a \\leq 5$, $2 \\leq b \\leq 6$. Границы $ab$ и $\\dfrac{a}{b}$.</li>
<li>Известно: периметр треугольника $P$ удовлетворяет $12 \\leq P \\leq 18$. Найти границы половины периметра $\\dfrac{P}{2}$.</li>
</ol>`);
html += secNav('p13', 'p15');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Калькулятор оценки */
(function(){
const ids = ['p14e-a','p14e-b','p14e-c','p14e-d'];
const out = document.getElementById('p14e-out');
let done = false;
function refresh(){
const a = +document.getElementById('p14e-a').value;
const b = +document.getElementById('p14e-b').value;
const c = +document.getElementById('p14e-c').value;
const d = +document.getElementById('p14e-d').value;
if(b < a || d < c){ out.innerHTML = '<span style="color:var(--bad)">Неверные границы: нижняя должна быть $\\leq$ верхней.</span>'; renderMath(out); return; }
const sumLo = a + c, sumHi = b + d;
const subLo = a - d, subHi = b - c;
const prods = [a*c, a*d, b*c, b*d];
const prdLo = Math.min(...prods), prdHi = Math.max(...prods);
const positives = a > 0 && c > 0;
let s = '<div><b>$x + y$:</b> $' + fmt(sumLo) + ' \\leq x+y \\leq ' + fmt(sumHi) + '$</div>';
s += '<div><b>$x - y$:</b> $' + fmt(subLo) + ' \\leq x-y \\leq ' + fmt(subHi) + '$</div>';
s += '<div><b>$xy$:</b> $' + fmt(prdLo) + ' \\leq xy \\leq ' + fmt(prdHi) + '$</div>';
if(positives){
s += '<div><b>$\\dfrac{x}{y}$:</b> $' + fmt(a/d) + ' \\leq \\dfrac{x}{y} \\leq ' + fmt(b/c) + '$ (все положительные)</div>';
} else {
s += '<div style="color:var(--muted)">Для $\\dfrac{x}{y}$ нужны все положительные — иначе формула сложнее.</div>';
}
out.innerHTML = s; renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p14_estimate'); bumpProgress('p14', 14); }, 300); }
}
ids.forEach(id => document.getElementById(id).addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — Тренажёр оценки */
(function(){
function gen(){
const a = 1 + Math.floor(Math.random()*4);
const b = a + 1 + Math.floor(Math.random()*4);
const c = 1 + Math.floor(Math.random()*4);
const d = c + 1 + Math.floor(Math.random()*4);
const ops = [
{ txt:'верхняя граница $x + y$', ans: b + d },
{ txt:'нижняя граница $x + y$', ans: a + c },
{ txt:'верхняя граница $x - y$', ans: b - c },
{ txt:'нижняя граница $x - y$', ans: a - d },
{ txt:'верхняя граница $xy$', ans: b * d },
{ txt:'нижняя граница $xy$', ans: a * c },
];
const op = ops[Math.floor(Math.random()*ops.length)];
return { a, b, c, d, op };
}
let cur = null, i = 1, score = 0;
function show(){
cur = gen();
document.getElementById('p14t-i').textContent = i;
document.getElementById('p14t-task').innerHTML = '<b>Дано:</b> $' + cur.a + ' \\leq x \\leq ' + cur.b + '$, $' + cur.c + ' \\leq y \\leq ' + cur.d + '$.<br><b>Найдите:</b> ' + cur.op.txt + '.';
renderMath(document.getElementById('p14t-task'));
document.getElementById('p14t-inp').value = '';
document.getElementById('p14t-fb').style.display = 'none';
}
function check(){
const fb = document.getElementById('p14t-fb'); fb.style.display = 'block';
const u = +document.getElementById('p14t-inp').value;
if(u === cur.op.ans){ score++; feedback(fb, true, '&#10003; ' + cur.op.ans); }
else feedback(fb, false, 'Не то. Правильно: ' + cur.op.ans);
document.getElementById('p14t-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/8'); if(score >= 6){ achievement('p14_train'); bumpProgress('p14', 16); confetti(); } }, 600); }
else { i++; setTimeout(show, 800); }
}
document.getElementById('p14t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p14t-score').textContent = 0; show(); });
document.getElementById('p14t-go').addEventListener('click', check);
document.getElementById('p14t-inp').addEventListener('keyup', e=>{ if(e.key === 'Enter') check(); });
})();
/* INIT 3 — Drag складывать/перемножать */
(function(){
const items = [
{ id:1, html:'$2 < 5$ и $3 < 7$', cat:'mul' },
{ id:2, html:'$a < b$ и $c < d$', cat:'add' },
{ id:3, html:'$a > b$ и $c < d$', cat:'none' },
{ id:4, html:'$-3 < 1$ и $-2 < 4$', cat:'add' },
{ id:5, html:'$5 > 2$ и $4 > 1$', cat:'mul' },
{ id:6, html:'$a < b$ и $c > d$', cat:'none' },
{ id:7, html:'$0 < x < 3$ и $1 < y < 5$', cat:'mul' },
{ id:8, html:'$-2 < a < 1$ и $-1 < b < 4$', cat:'add' },
];
const sorter = setupSorter({ poolId:'p14d-pool', cats:['add','mul','none'], items, scopeSelector:'#p14-body' });
document.getElementById('p14d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p14d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все ' + items.length + ' верно!'); achievement('p14_drag'); bumpProgress('p14', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p14d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p14d-fb').style.display='none'; });
})();
/* INIT 4 — Пошаговое */
(function(){
const stage = document.getElementById('p14p-stage');
const goBtn = document.getElementById('p14p-go');
const nextBtn = document.getElementById('p14p-next');
const resetBtn = document.getElementById('p14p-reset');
const steps = [
'<b>Шаг 1.</b> $x + y$: складываем границы соответствующих частей. $1 + 2 = 3$ — низ, $4 + 6 = 10$ — верх. Итог: $3 \\leq x+y \\leq 10$.',
'<b>Шаг 2.</b> $x - y$: внимание на «крест-накрест». Низ: $1 - 6 = -5$. Верх: $4 - 2 = 2$. Итог: $-5 \\leq x-y \\leq 2$.',
'<b>Шаг 3.</b> $xy$ (все положительные): низ — произведение нижних, $1 \\cdot 2 = 2$. Верх — произведение верхних, $4 \\cdot 6 = 24$. Итог: $2 \\leq xy \\leq 24$.',
'<b>Шаг 4.</b> $\\dfrac{x}{y}$ (все $> 0$): низ — $\\dfrac{\\text{мин}\\ x}{\\text{макс}\\ y} = \\dfrac{1}{6}$. Верх — $\\dfrac{\\text{макс}\\ x}{\\text{мин}\\ y} = \\dfrac{4}{2} = 2$. Итог: $\\dfrac{1}{6} \\leq \\dfrac{x}{y} \\leq 2$.',
'<b>Ответ:</b> все 4 границы найдены. Заметьте — для разности и деления у $y$ границы поменялись местами.',
];
let idx = 0, awarded = false;
function render(){
stage.innerHTML = steps.slice(0, idx + 1).map(s => `<div style="margin:6px 0;padding:9px 12px;background:var(--card);border-left:3px solid var(--sec-acc);border-radius:7px;animation:fadeIn .3s ease">${s}</div>`).join('');
renderMath(stage);
if(idx >= steps.length - 1){
nextBtn.disabled = true; nextBtn.textContent = 'Готово';
if(!awarded){ awarded = true; achievement('p14_add'); bumpProgress('p14', 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 5 — Сложи неравенства */
(function(){
const tasks = [
{ q:'$3 < 5$ и $2 < 4$. Результат сложения:', opts:['$5 < 9$','$3 < 9$','$5 < 4$','$1 < 1$'], ok:0 },
{ q:'$-1 < 4$ и $0 < 7$. Результат сложения:', opts:['$-1 < 11$','$0 < 11$','$-1 < 7$','$0 < 4$'], ok:0 },
{ q:'$a < b$ и $c < d$, всегда верно:', opts:['$a + c < b + d$','$a + c > b + d$','$ac < bd$','ничего из перечисленного'], ok:0 },
{ q:'$2 < 6$ и $3 < 4$. Перемножение (все положительные):', opts:['$6 < 24$','$5 < 10$','$2 < 24$','$6 < 4$'], ok:0 },
{ q:'$-3 < 2$ и $-1 < 5$. Можно сложить?', opts:['Да: $-4 < 7$','Нет, неравенства разных знаков','Только если они положительные','Нет, потому что $-1 > -3$'], ok:0 },
{ q:'$a > b$ и $c > d$, тогда $a + c$ ? $b + d$:', opts:['$>$','$<$','$=$','непредсказуемо'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p14a-i').textContent = i;
document.getElementById('p14a-task').innerHTML = '<b>Задача ' + i + '.</b> ' + cur.q;
renderMath(document.getElementById('p14a-task'));
const opts = document.getElementById('p14a-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('p14a-fb'); fb.style.display = 'block';
if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
document.getElementById('p14a-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p14_mul'); bumpProgress('p14', 14); confetti(); } }, 700); }
else { i++; setTimeout(show, 800); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p14a-fb').style.display = 'none';
}
document.getElementById('p14a-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p14a-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
}
</script>
</body>
</html>