Files
Learn_System/frontend/textbooks/algebra_8_ch3.html
T
Maxim Dolgolyov 699fdcc7fb feat(catalog): хаб-страница для Алгебры 8 (3 главы под единым слагом)
- migration 014: parent_slug column + algebra-8 hub row +
  rename old algebra-8 → algebra-8-ch1 (progress сохраняется
  через стабильный textbook_id=3)
- backend/routes/textbooks.js: GET / фильтрует parent_slug IS NULL;
  aggregated progress для хабов; новый GET /:slug/children
- algebra_8_hub.html: новая хаб-страница с 3 карточками глав,
  hero с общим прогрессом, XP-бейдж, ссылки на главы
- algebra_8/ch2/ch3: кнопки cross-chapter заменены на
  одну «К алгебре 8» в шапке

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 16:49:20 +03:00

3178 lines
208 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="К Алгебре 8 — все главы">
<svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
К алгебре 8
</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(){ buildP15(); }
function buildP16stub(){ buildP16(); }
/* Helper: SVG-рисовалка интервала / луча / системы */
function drawNumLine(opts){
const W = 520, H = 80, M = 30;
const min = opts.min != null ? opts.min : -10;
const max = opts.max != null ? opts.max : 10;
const x = v => M + (v - min) * (W - 2*M) / (max - min);
let s = '<svg viewBox="0 0 ' + W + ' ' + H + '">';
// ось
s += '<line x1="' + M + '" y1="40" x2="' + (W - M) + '" y2="40" stroke="#94a3b8" stroke-width="1.5"/>';
// стрелка вправо
s += '<polygon points="' + (W - M) + ',35 ' + (W - M + 10) + ',40 ' + (W - M) + ',45" fill="#94a3b8"/>';
// деления и подписи
for(let v = Math.ceil(min); v <= Math.floor(max); v++){
const px = x(v);
s += '<line x1="' + px + '" y1="36" x2="' + px + '" y2="44" stroke="#94a3b8"/>';
if(v % Math.max(1, Math.floor((max - min) / 10)) === 0 || max - min <= 12) s += '<text x="' + px + '" y="62" text-anchor="middle" font-size="11" fill="#64748b">' + v + '</text>';
}
// интервалы
(opts.intervals || []).forEach(it => {
const color = it.color || 'var(--sec-acc)';
const a = it.a != null ? Math.max(min, it.a) : min;
const b = it.b != null ? Math.min(max, it.b) : max;
const xa = x(a), xb = x(b);
// полоска
s += '<line x1="' + xa + '" y1="40" x2="' + xb + '" y2="40" stroke="' + color + '" stroke-width="6" opacity="0.55"/>';
// концы
if(it.a != null){
if(it.openA) s += '<circle cx="' + xa + '" cy="40" r="6" fill="white" stroke="' + color + '" stroke-width="2.5"/>';
else s += '<circle cx="' + xa + '" cy="40" r="6" fill="' + color + '"/>';
} else {
s += '<polygon points="' + (M-2) + ',32 ' + (M-12) + ',40 ' + (M-2) + ',48" fill="' + color + '"/>';
}
if(it.b != null){
if(it.openB) s += '<circle cx="' + xb + '" cy="40" r="6" fill="white" stroke="' + color + '" stroke-width="2.5"/>';
else s += '<circle cx="' + xb + '" cy="40" r="6" fill="' + color + '"/>';
} else {
s += '<polygon points="' + (W-M+2) + ',32 ' + (W-M+12) + ',40 ' + (W-M+2) + ',48" fill="' + color + '"/>';
}
});
s += '</svg>';
return s;
}
/* ============================================================
§ 15 — ЧИСЛОВЫЕ ПРОМЕЖУТКИ. ЛИНЕЙНЫЕ НЕРАВЕНСТВА
============================================================ */
function buildP15(){
const box = document.getElementById('p15-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Свойства неравенств (§13): при делении на отрицательное знак меняется.</li>
<li>Линейное уравнение $ax + b = 0$ имеет корень $x = -b/a$.</li>
<li>Числовая прямая: точки слева меньше, справа — больше.</li>
</ul>`);
html += makeCard('theory','5 видов промежутков','15.1',`
<table class="tbl" style="margin:8px 0">
<thead><tr><th>Название</th><th>Неравенство</th><th>Запись</th><th>Изображение</th></tr></thead>
<tbody>
<tr><td>Отрезок</td><td>$a \\leq x \\leq b$</td><td>$[a;\\,b]$</td><td>● ━━━ ●</td></tr>
<tr><td>Интервал</td><td>$a < x < b$</td><td>$(a;\\,b)$</td><td>○ ━━━ ○</td></tr>
<tr><td>Полуинтервал</td><td>$a \\leq x < b$</td><td>$[a;\\,b)$</td><td>● ━━━ ○</td></tr>
<tr><td>Луч</td><td>$x \\geq a$</td><td>$[a;\\,+\\infty)$</td><td>● ━━━ →</td></tr>
<tr><td>Открытый луч</td><td>$x < a$</td><td>$(-\\infty;\\,a)$</td><td>← ━━━ ○</td></tr>
</tbody>
</table>
<p style="font-size:.88rem;color:var(--muted)">Запомните: <b>квадратная</b> скобка $[\\,]$ — точка <b>входит</b>. <b>Круглая</b> $(\\,)$ — <b>выколота</b>. У бесконечности всегда круглая.</p>`);
html += makeCard('algo','Решение линейного неравенства','15.2',`
<ol style="margin-left:18px;line-height:1.9">
<li>Раскрыть скобки, если есть.</li>
<li>Перенести члены с $x$ в одну часть, числа — в другую (со сменой знака при переносе).</li>
<li>Привести подобные.</li>
<li>Разделить на коэффициент при $x$. Если он <b>отрицательный — знак неравенства меняется</b>.</li>
<li>Записать ответ как промежуток.</li>
</ol>`);
html += makeCard('example','Пример',null,`
<p><b>Решим:</b> $3x - 7 > 2x + 1$.</p>
<p>1) $3x - 2x > 1 + 7$. 2) $x > 8$. 3) Ответ: $x \\in (8;\\,+\\infty)$.</p>
<p style="margin-top:6px"><b>С отрицательным коэффициентом:</b> $-2x \\geq 6$ &rarr; делим на $-2$: $x \\leq -3$. Ответ: $x \\in (-\\infty;\\,-3]$.</p>`);
/* INT 1 — Конструктор промежутка */
html += widget('Конструктор промежутка','INTERACT 1','Выбери концы $a$, $b$ и тип. Промежуток нарисуется на числовой прямой.',`
<div class="sliders">
<label>$a$ = <b id="p15c-a-val">-2</b><input type="range" min="-9" max="9" step="1" value="-2" id="p15c-a"></label>
<label>$b$ = <b id="p15c-b-val">5</b><input type="range" min="-9" max="9" step="1" value="5" id="p15c-b"></label>
</div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin:8px 0">
<button class="btn small p15c-type active" data-t="cc">$[a;b]$</button>
<button class="btn small p15c-type" data-t="oo">$(a;b)$</button>
<button class="btn small p15c-type" data-t="co">$[a;b)$</button>
<button class="btn small p15c-type" data-t="oc">$(a;b]$</button>
<button class="btn small p15c-type" data-t="rcr">$[a;+\\infty)$</button>
<button class="btn small p15c-type" data-t="rol">$(-\\infty;b)$</button>
</div>
<div class="numline" id="p15c-line"></div>
<div id="p15c-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;text-align:center;font-size:1.1rem"></div>`);
/* INT 2 — Конвертация: запись → неравенство */
html += widget('Конвертация записи','INTERACT 2','Найди соответствие между записью промежутка и неравенством.',`
<div class="score-display"><span>Раунд <b id="p15v-i">1</b> / 8</span><span>Очки: <b id="p15v-score">0</b></span></div>
<div id="p15v-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>
<div id="p15v-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p15v-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p15v-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Пошаговый решатель линейного */
html += widget('Пошаговый решатель','INTERACT 3','Введите $a, b, c, d$ для $ax + b \\geq cx + d$ и нажимайте «Дальше».',`
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px;font-size:1.02rem">
<input type="number" id="p15s-a" value="3" class="tinp" style="width:55px">$x +$
<input type="number" id="p15s-b" value="-7" class="tinp" style="width:55px">$\\geq$
<input type="number" id="p15s-c" value="2" class="tinp" style="width:55px">$x +$
<input type="number" id="p15s-d" value="1" class="tinp" style="width:55px">
<button class="btn primary" id="p15s-go">Старт</button>
<button class="btn" id="p15s-next" style="display:none">Дальше</button>
<button class="btn" id="p15s-reset" style="display:none">Сначала</button>
</div>
<div id="p15s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:80px"></div>`);
/* INT 4 — Тренажёр линейных */
html += widget('Тренажёр линейных','INTERACT 4','Решите неравенство и введите ответ в формате $(a; +\\infty)$ или $[-\\infty; b)$. Используйте `inf` для бесконечности.',`
<div class="score-display"><span>Задача <b id="p15t-i">1</b> / 8</span><span>Очки: <b id="p15t-score">0</b></span></div>
<div id="p15t-task" style="font-size:1.3rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap">
<button class="btn" data-cmp="gt" id="p15t-gt">$x > k$</button>
<button class="btn" data-cmp="ge" id="p15t-ge">$x \\geq k$</button>
<button class="btn" data-cmp="lt" id="p15t-lt">$x < k$</button>
<button class="btn" data-cmp="le" id="p15t-le">$x \\leq k$</button>
</div>
<div style="display:flex;gap:6px;justify-content:center;flex-wrap:wrap;margin-top:6px">
<input type="number" id="p15t-k" placeholder="k =" class="tinp" style="width:90px">
<button class="btn primary" id="p15t-go">Ответ</button>
</div>
<div class="feedback" id="p15t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p15t-start" style="margin-top:10px">Начать</button>`);
/* INT 5 — Drag: какой промежуток */
html += widget('Сопоставь неравенство и промежуток','INTERACT 5','Отнеси каждое неравенство к правильной записи промежутка.',`
${DND_HINT_HTML}
<div id="p15d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px">
<div class="drop-box"><h5>$[a;b]$</h5><div class="drop-items" data-cat="cc"></div></div>
<div class="drop-box"><h5>$(a;b)$</h5><div class="drop-items" data-cat="oo"></div></div>
<div class="drop-box"><h5>$(a;+\\infty)$</h5><div class="drop-items" data-cat="rop"></div></div>
<div class="drop-box"><h5>$(-\\infty;b]$</h5><div class="drop-items" data-cat="lcr"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p15d-check">Проверить</button><button class="btn" id="p15d-reset">Сначала</button></div>
<div class="feedback" id="p15d-fb" style="display:none"></div>`);
html += makeCard('oral','Устно',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>Запишите промежуток для $x > 5$.</li>
<li>Что означает скобка $[$?</li>
<li>Решите устно: $2x > 6$.</li>
</ol>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$5x - 3 < 12$</li>
<li>$-3x + 1 \\geq 7$</li>
<li>$2(x - 1) > 3(x + 2)$</li>
<li>Запишите $[-2; 5)$ через неравенство.</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$7x + 2 \\leq 4x - 7$</li>
<li>$-\\dfrac{x}{3} > 2$</li>
<li>$3 - 2x \\leq x + 9$</li>
<li>Изобразите $(-\\infty; -1) \\cup [3; +\\infty)$.</li>
</ol>`);
html += secNav('p14', 'p16');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Конструктор */
(function(){
const aE = document.getElementById('p15c-a'), bE = document.getElementById('p15c-b');
const lineE = document.getElementById('p15c-line'), out = document.getElementById('p15c-out');
let type = 'cc', done = false;
document.querySelectorAll('.p15c-type').forEach(btn => btn.addEventListener('click', ()=>{
document.querySelectorAll('.p15c-type').forEach(x => x.classList.remove('active'));
btn.classList.add('active'); type = btn.dataset.t; refresh();
}));
function refresh(){
const a = +aE.value, b = +bE.value;
document.getElementById('p15c-a-val').textContent = a;
document.getElementById('p15c-b-val').textContent = b;
let interval = null, label = '', ineq = '';
if(type === 'cc'){ if(b < a){ out.innerHTML = 'Нужно $a \\leq b$'; renderMath(out); return; } interval = { a, b, openA:false, openB:false }; label = '$[' + a + ';\\,' + b + ']$'; ineq = a + ' \\leq x \\leq ' + b; }
else if(type === 'oo'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:true, openB:true }; label = '$(' + a + ';\\,' + b + ')$'; ineq = a + ' < x < ' + b; }
else if(type === 'co'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:false, openB:true }; label = '$[' + a + ';\\,' + b + ')$'; ineq = a + ' \\leq x < ' + b; }
else if(type === 'oc'){ if(b <= a){ out.innerHTML = 'Нужно $a < b$'; renderMath(out); return; } interval = { a, b, openA:true, openB:false }; label = '$(' + a + ';\\,' + b + ']$'; ineq = a + ' < x \\leq ' + b; }
else if(type === 'rcr'){ interval = { a, b:null, openA:false, openB:false }; label = '$[' + a + ';\\,+\\infty)$'; ineq = 'x \\geq ' + a; }
else if(type === 'rol'){ interval = { a:null, b, openA:false, openB:true }; label = '$(-\\infty;\\,' + b + ')$'; ineq = 'x < ' + b; }
lineE.innerHTML = drawNumLine({ intervals:[interval] });
out.innerHTML = '<div><b>Запись:</b> ' + label + '</div><div><b>Неравенство:</b> $' + ineq + '$</div>';
renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p15_line'); bumpProgress('p15', 14); }, 300); }
}
[aE,bE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — Конвертация */
(function(){
const tasks = [
{ q:'Запись: $[3;\\,7]$. Какое неравенство?', opts:['$3 \\leq x \\leq 7$','$3 < x < 7$','$x \\geq 3$','$3 \\leq x < 7$'], ok:0 },
{ q:'Неравенство: $-2 < x \\leq 5$. Какая запись?', opts:['$(-2;\\,5]$','$[-2;\\,5)$','$[-2;\\,5]$','$(-2;\\,5)$'], ok:0 },
{ q:'Запись: $(-\\infty;\\,4)$. Что значит?', opts:['$x < 4$','$x \\leq 4$','$x > 4$','$-\\infty < x < 4$ строго'], ok:0 },
{ q:'Неравенство: $x \\geq 6$. Какая запись?', opts:['$[6;\\,+\\infty)$','$(6;\\,+\\infty)$','$(-\\infty;\\,6]$','$[-6;\\,+\\infty)$'], ok:0 },
{ q:'Запись: $(0;\\,1)$. Содержит ли точку $0$?', opts:['Нет, выколота','Да, входит','Только если $0 > 0$','Зависит от контекста'], ok:0 },
{ q:'Запись с двумя круглыми скобками означает:', opts:['Оба конца выколоты','Оба конца входят','Один выколот, один входит','Только справа выколот'], ok:0 },
{ q:'Какой записью обозначить «все числа меньше или равны $-3$»?', opts:['$(-\\infty;\\,-3]$','$(-\\infty;\\,-3)$','$[-3;\\,+\\infty)$','$(-3;\\,+\\infty)$'], ok:0 },
{ q:'Промежуток $[2;\\,2]$ содержит:', opts:['Только число $2$','Все числа от $0$ до $2$','Пуст','Все числа $\\geq 2$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p15v-i').textContent = i;
document.getElementById('p15v-task').innerHTML = cur.q;
renderMath(document.getElementById('p15v-task'));
const opts = document.getElementById('p15v-opts'); opts.innerHTML = '';
cur.opts.forEach((o, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p15v-fb'); fb.style.display = 'block';
if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
document.getElementById('p15v-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 6, 'Итог: ' + score + '/' + shuffled.length); if(score >= 6){ achievement('p15_convert'); bumpProgress('p15', 14); confetti(); } }, 600); }
else { i++; setTimeout(show, 800); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p15v-fb').style.display = 'none';
}
document.getElementById('p15v-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p15v-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 3 — Шаговый решатель */
(function(){
const stage = document.getElementById('p15s-stage');
const goBtn = document.getElementById('p15s-go'), nextBtn = document.getElementById('p15s-next'), resetBtn = document.getElementById('p15s-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b, c, d){
// ax + b >= cx + d -> (a-c)x >= d-b
const k = a - c, r = d - b;
const arr = [];
arr.push('<b>Дано:</b> $' + a + 'x ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + ' \\geq ' + c + 'x ' + (d >= 0 ? '+ ' + d : '- ' + Math.abs(d)) + '$');
arr.push('<b>Шаг 1.</b> Переносим $x$ влево, числа — вправо: $' + a + 'x - ' + c + 'x \\geq ' + d + ' - (' + b + ') \\Rightarrow ' + k + 'x \\geq ' + r + '$');
if(k === 0){
arr.push('<b>Шаг 2.</b> $0 \\cdot x \\geq ' + r + '$. ' + (r <= 0 ? 'Верно для любого $x$ — решение $x \\in \\mathbb{R}$.' : 'Невозможно — решений нет.'));
arr.push('<b>Ответ:</b> ' + (r <= 0 ? '$(-\\infty;\\,+\\infty)$' : 'нет решений'));
} else if(k > 0){
arr.push('<b>Шаг 2.</b> Делим на $' + k + ' > 0$: $x \\geq ' + r + '/' + k + ' = ' + fmt(r/k) + '$');
arr.push('<b>Ответ:</b> $x \\in [' + fmt(r/k) + ';\\,+\\infty)$');
} else {
arr.push('<b>Шаг 2.</b> Делим на $' + k + ' < 0$ — <span style="color:var(--bad);font-weight:700">знак меняется!</span> $x \\leq ' + r + '/' + k + ' = ' + fmt(r/k) + '$');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + fmt(r/k) + ']$');
}
return arr;
}
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('p15_solver'); bumpProgress('p15', 14); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p15s-a').value, b = +document.getElementById('p15s-b').value;
const c = +document.getElementById('p15s-c').value, d = +document.getElementById('p15s-d').value;
steps = build(a, b, c, d); idx = 0; awarded = false;
goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
render();
});
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
})();
/* INIT 4 — Тренажёр линейных */
(function(){
function gen(){
const a = (Math.random() < 0.5 ? -1 : 1) * (1 + Math.floor(Math.random()*4));
const b = -5 + Math.floor(Math.random()*11);
const c = -5 + Math.floor(Math.random()*11);
// ax + b > c
const v = (c - b) / a;
// знак ответа зависит от знака a
const baseCmp = ['gt','ge','lt','le'][Math.floor(Math.random()*4)];
const flipped = a < 0 ? { gt:'lt', ge:'le', lt:'gt', le:'ge' }[baseCmp] : baseCmp;
return { a, b, c, baseCmp, ansCmp: flipped, ansK: v, txt: a + 'x ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + ' ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[baseCmp] + ' ' + c };
}
let cur = null, i = 1, score = 0, chosen = null;
function show(){
cur = gen();
document.getElementById('p15t-i').textContent = i;
document.getElementById('p15t-task').innerHTML = '$' + cur.txt + '$';
renderMath(document.getElementById('p15t-task'));
document.getElementById('p15t-k').value = '';
chosen = null;
document.querySelectorAll('#p15t-gt,#p15t-ge,#p15t-lt,#p15t-le').forEach(b => b.classList.remove('primary'));
document.getElementById('p15t-fb').style.display = 'none';
}
function check(){
const fb = document.getElementById('p15t-fb'); fb.style.display = 'block';
const u = +document.getElementById('p15t-k').value;
const ok = chosen === cur.ansCmp && Math.abs(u - cur.ansK) < 1e-6;
if(ok){ score++; feedback(fb, true, '&#10003; $x ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[cur.ansCmp] + ' ' + fmt(cur.ansK) + '$'); renderMath(fb); }
else feedback(fb, false, 'Правильно: $x ' + ({gt:'>',ge:'\\geq',lt:'<',le:'\\leq'})[cur.ansCmp] + ' ' + fmt(cur.ansK) + '$');
renderMath(fb);
document.getElementById('p15t-score').textContent = score;
if(i >= 8){ setTimeout(()=>{ feedback(fb, score >= 5, 'Итог: ' + score + '/8'); if(score >= 5){ achievement('p15_train'); bumpProgress('p15', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
}
document.getElementById('p15t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p15t-score').textContent = 0; show(); });
['gt','ge','lt','le'].forEach(c => document.getElementById('p15t-' + c).addEventListener('click', ()=>{
chosen = c;
document.querySelectorAll('#p15t-gt,#p15t-ge,#p15t-lt,#p15t-le').forEach(b => b.classList.remove('primary'));
document.getElementById('p15t-' + c).classList.add('primary');
}));
document.getElementById('p15t-go').addEventListener('click', ()=>{ if(!chosen){ const fb = document.getElementById('p15t-fb'); fb.style.display='block'; feedback(fb, false, 'Выберите знак.'); return; } check(); });
})();
/* INIT 5 — Drag */
(function(){
const items = [
{ id:1, html:'$2 \\leq x \\leq 7$', cat:'cc' },
{ id:2, html:'$-3 < x < 4$', cat:'oo' },
{ id:3, html:'$x > 5$', cat:'rop' },
{ id:4, html:'$x \\leq 0$', cat:'lcr' },
{ id:5, html:'$-1 \\leq x \\leq 6$', cat:'cc' },
{ id:6, html:'$0 < x < 10$', cat:'oo' },
{ id:7, html:'$x > 8$', cat:'rop' },
{ id:8, html:'$x \\leq -2$', cat:'lcr' },
];
const sorter = setupSorter({ poolId:'p15d-pool', cats:['cc','oo','rop','lcr'], items, scopeSelector:'#p15-body' });
document.getElementById('p15d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p15d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p15_drag'); bumpProgress('p15', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p15d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p15d-fb').style.display='none'; });
})();
}
/* ============================================================
§ 16 — СИСТЕМЫ И СОВОКУПНОСТИ НЕРАВЕНСТВ
============================================================ */
function buildP16(){
const box = document.getElementById('p16-body');
let html = '';
html += makeCard('repeat','Повторение § 15',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Промежутки: $[a;b]$, $(a;b)$, лучи и др.</li>
<li>Решение линейного: изоляция $x$, не забывать про знак при делении на отрицательное.</li>
</ul>`);
html += makeCard('theory','Система и совокупность','16.1',`
<p><b>Система</b> неравенств — несколько неравенств, выполненных <b>одновременно</b> («И»). Решение — <b>пересечение</b> решений каждого. Записывается фигурной скобкой:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:10px;margin:8px 0;text-align:center;font-size:1.05rem">$$\\begin{cases} x \\geq 1 \\\\ x < 5 \\end{cases} \\Rightarrow x \\in [1;\\,5)$$</div>
<p><b>Совокупность</b> — неравенства, из которых выполняется <b>хотя бы одно</b> («ИЛИ»). Решение — <b>объединение</b>. Записывается квадратной скобкой:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:10px;margin:8px 0;text-align:center;font-size:1.05rem">$$\\left[\\begin{array}{l} x < -2 \\\\ x \\geq 3 \\end{array}\\right. \\Rightarrow x \\in (-\\infty;\\,-2) \\cup [3;\\,+\\infty)$$</div>`);
html += makeCard('algo','Алгоритм решения системы',null,`
<ol style="margin-left:18px;line-height:1.9">
<li>Решить каждое неравенство отдельно.</li>
<li>Изобразить решения на одной числовой прямой.</li>
<li>Найти пересечение (общую часть для системы) или объединение (для совокупности).</li>
<li>Записать ответ как промежуток.</li>
</ol>`);
html += makeCard('example','Пример',null,`
<p><b>Решим систему:</b> $\\begin{cases} 2x - 1 > 3 \\\\ x + 4 \\leq 10 \\end{cases}$</p>
<p>Первое: $2x > 4 \\Rightarrow x > 2$, т.е. $(2;\\,+\\infty)$.</p>
<p>Второе: $x \\leq 6$, т.е. $(-\\infty;\\,6]$.</p>
<p>Пересечение: $x \\in (2;\\,6]$.</p>`);
/* INT 1 — Пересечение промежутков */
html += widget('Пересечение двух промежутков','INTERACT 1','Сдвигай границы каждого промежутка и наблюдай пересечение.',`
<div class="sliders">
<label>$a_1$ = <b id="p16i-a1-val">-3</b><input type="range" min="-9" max="9" step="1" value="-3" id="p16i-a1"></label>
<label>$b_1$ = <b id="p16i-b1-val">4</b><input type="range" min="-9" max="9" step="1" value="4" id="p16i-b1"></label>
<label>$a_2$ = <b id="p16i-a2-val">1</b><input type="range" min="-9" max="9" step="1" value="1" id="p16i-a2"></label>
<label>$b_2$ = <b id="p16i-b2-val">7</b><input type="range" min="-9" max="9" step="1" value="7" id="p16i-b2"></label>
</div>
<div class="numline" id="p16i-line"></div>
<div id="p16i-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;text-align:center;font-size:1.05rem;line-height:1.8"></div>`);
/* INT 2 — Пошаговый решатель */
html += widget('Шаговый решатель системы','INTERACT 2','Решаем систему пошагово: каждое неравенство отдельно, затем пересечение.',`
<p style="margin-bottom:10px"><b>Система:</b> $\\begin{cases} 3x - 5 \\geq 1 \\\\ -2x + 4 > -6 \\end{cases}$</p>
<div id="p16s-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="p16s-go">Старт</button><button class="btn" id="p16s-next" style="display:none">Дальше</button><button class="btn" id="p16s-reset" style="display:none">Сначала</button></div>`);
/* INT 3 — Drag: система или совокупность */
html += widget('Что это: система или совокупность?','INTERACT 3','По логической связке («И» / «ИЛИ») определи тип записи.',`
${DND_HINT_HTML}
<div id="p16d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="drop-box"><h5>Система (И, пересечение)</h5><div class="drop-items" data-cat="sys"></div></div>
<div class="drop-box"><h5>Совокупность (ИЛИ, объединение)</h5><div class="drop-items" data-cat="sov"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p16d-check">Проверить</button><button class="btn" id="p16d-reset">Сначала</button></div>
<div class="feedback" id="p16d-fb" style="display:none"></div>`);
/* INT 4 — Тренажёр систем */
html += widget('Тренажёр систем','INTERACT 4','Решите систему и введите ответ как промежуток.',`
<div class="score-display"><span>Задача <b id="p16t-i">1</b> / 6</span><span>Очки: <b id="p16t-score">0</b></span></div>
<div id="p16t-task" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>
<div id="p16t-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p16t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p16t-start" style="margin-top:10px">Начать</button>`);
/* INT 5 — Совокупность визуально */
html += widget('Совокупность: объединение','INTERACT 5','Двигай $a$ и $b$, смотри, как меняется $(-\\infty;a) \\cup (b;+\\infty)$.',`
<div class="sliders">
<label>$a$ = <b id="p16u-a-val">-2</b><input type="range" min="-9" max="9" step="1" value="-2" id="p16u-a"></label>
<label>$b$ = <b id="p16u-b-val">3</b><input type="range" min="-9" max="9" step="1" value="3" id="p16u-b"></label>
</div>
<div class="numline" id="p16u-line"></div>
<div id="p16u-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;text-align:center;font-size:1.05rem"></div>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\begin{cases} x > 2 \\\\ x < 8 \\end{cases}$</li>
<li>$\\begin{cases} 2x - 1 \\leq 5 \\\\ x + 3 > -2 \\end{cases}$</li>
<li>$\\left[\\begin{array}{l} x < -1 \\\\ x \\geq 4 \\end{array}\\right.$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\begin{cases} 3x + 1 > 7 \\\\ x - 2 \\leq 3 \\end{cases}$</li>
<li>$\\begin{cases} 5 - 2x \\geq 1 \\\\ x + 1 > -3 \\end{cases}$</li>
<li>$\\left[\\begin{array}{l} 2x > 8 \\\\ -x \\geq 2 \\end{array}\\right.$</li>
</ol>`);
html += secNav('p15', 'p17');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Пересечение промежутков */
(function(){
const ids = ['p16i-a1','p16i-b1','p16i-a2','p16i-b2'];
const lineE = document.getElementById('p16i-line'), out = document.getElementById('p16i-out');
let done = false;
function refresh(){
const a1 = +document.getElementById('p16i-a1').value, b1 = +document.getElementById('p16i-b1').value;
const a2 = +document.getElementById('p16i-a2').value, b2 = +document.getElementById('p16i-b2').value;
document.getElementById('p16i-a1-val').textContent = a1;
document.getElementById('p16i-b1-val').textContent = b1;
document.getElementById('p16i-a2-val').textContent = a2;
document.getElementById('p16i-b2-val').textContent = b2;
const intervals = [];
if(a1 <= b1) intervals.push({ a:a1, b:b1, openA:false, openB:false, color:'#6366f1' });
if(a2 <= b2) intervals.push({ a:a2, b:b2, openA:false, openB:false, color:'#f59e0b' });
const lo = Math.max(a1, a2), hi = Math.min(b1, b2);
if(lo <= hi) intervals.push({ a:lo, b:hi, openA:false, openB:false, color:'#10b981' });
lineE.innerHTML = drawNumLine({ intervals });
let s = '<div><b style="color:#6366f1">[' + a1 + ';' + b1 + ']</b> ∩ <b style="color:#f59e0b">[' + a2 + ';' + b2 + ']</b> = ';
if(lo > hi) s += '<span style="color:var(--bad)">∅ (пусто)</span></div>';
else s += '<b style="color:#10b981">[' + lo + ';' + hi + ']</b></div>';
out.innerHTML = s;
if(!done){ done = true; setTimeout(()=>{ achievement('p16_intersect'); bumpProgress('p16', 14); }, 300); }
}
ids.forEach(id => document.getElementById(id).addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — Шаговый решатель */
(function(){
const stage = document.getElementById('p16s-stage');
const goBtn = document.getElementById('p16s-go'), nextBtn = document.getElementById('p16s-next'), resetBtn = document.getElementById('p16s-reset');
const steps = [
'<b>Шаг 1.</b> Решим первое: $3x - 5 \\geq 1 \\Rightarrow 3x \\geq 6 \\Rightarrow x \\geq 2$, т.е. $[2;\\,+\\infty)$.',
'<b>Шаг 2.</b> Решим второе: $-2x + 4 > -6 \\Rightarrow -2x > -10 \\Rightarrow x < 5$ (знак сменился!), т.е. $(-\\infty;\\,5)$.',
'<b>Шаг 3.</b> Пересечение: $[2;\\,+\\infty) \\cap (-\\infty;\\,5) = [2;\\,5)$.',
'<b>Ответ:</b> $x \\in [2;\\,5)$.',
];
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('p16_solver'); bumpProgress('p16', 14); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{ idx = 0; awarded = false; goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = ''; render(); });
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
})();
/* INIT 3 — Drag система/совокупность */
(function(){
const items = [
{ id:1, html:'$\\begin{cases} x > 2 \\\\ x \\leq 5 \\end{cases}$', cat:'sys' },
{ id:2, html:'$\\left[\\begin{array}{l} x < -3 \\\\ x \\geq 4 \\end{array}\\right.$', cat:'sov' },
{ id:3, html:'Оба условия одновременно', cat:'sys' },
{ id:4, html:'Хотя бы одно условие', cat:'sov' },
{ id:5, html:'$\\begin{cases} x \\geq -1 \\\\ x < 7 \\end{cases}$', cat:'sys' },
{ id:6, html:'$\\left[\\begin{array}{l} x \\leq 0 \\\\ x > 5 \\end{array}\\right.$', cat:'sov' },
{ id:7, html:'Пересечение решений', cat:'sys' },
{ id:8, html:'Объединение решений', cat:'sov' },
];
const sorter = setupSorter({ poolId:'p16d-pool', cats:['sys','sov'], items, scopeSelector:'#p16-body' });
document.getElementById('p16d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p16d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p16_union'); bumpProgress('p16', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p16d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p16d-fb').style.display='none'; });
})();
/* INIT 4 — Тренажёр систем */
(function(){
const tasks = [
{ q:'$\\begin{cases} x > 3 \\\\ x \\leq 7 \\end{cases}$', opts:['$(3;\\,7]$','$[3;\\,7)$','$(3;\\,7)$','$\\emptyset$'], ok:0 },
{ q:'$\\begin{cases} x \\geq -2 \\\\ x < 4 \\end{cases}$', opts:['$[-2;\\,4)$','$(-2;\\,4)$','$[-2;\\,4]$','$\\emptyset$'], ok:0 },
{ q:'$\\begin{cases} x > 5 \\\\ x < 2 \\end{cases}$', opts:['$\\emptyset$ (нет решений)','$(2;\\,5)$','$(5;\\,2)$','любое $x$'], ok:0 },
{ q:'$\\left[\\begin{array}{l} x < -1 \\\\ x \\geq 3 \\end{array}\\right.$', opts:['$(-\\infty;\\,-1) \\cup [3;\\,+\\infty)$','$(-1;\\,3)$','$[-1;\\,3)$','$\\emptyset$'], ok:0 },
{ q:'$\\begin{cases} 2x \\geq 6 \\\\ x \\leq 10 \\end{cases}$', opts:['$[3;\\,10]$','$[3;\\,10)$','$(3;\\,10]$','$[6;\\,10]$'], ok:0 },
{ q:'$\\begin{cases} x > 0 \\\\ x < 0 \\end{cases}$', opts:['$\\emptyset$','$\\{0\\}$','$\\mathbb{R}$','$(-\\infty;\\,0)$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p16t-i').textContent = i;
document.getElementById('p16t-task').innerHTML = cur.q;
renderMath(document.getElementById('p16t-task'));
const opts = document.getElementById('p16t-opts'); opts.innerHTML = '';
cur.opts.forEach((o, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p16t-fb'); fb.style.display = 'block';
if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
document.getElementById('p16t-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p16_train'); bumpProgress('p16', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p16t-fb').style.display = 'none';
}
document.getElementById('p16t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p16t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 5 — Совокупность визуально */
(function(){
const aE = document.getElementById('p16u-a'), bE = document.getElementById('p16u-b');
const lineE = document.getElementById('p16u-line'), out = document.getElementById('p16u-out');
function refresh(){
const a = +aE.value, b = +bE.value;
document.getElementById('p16u-a-val').textContent = a;
document.getElementById('p16u-b-val').textContent = b;
const intervals = [
{ a:null, b:a, openA:false, openB:true, color:'#8b5cf6' },
{ a:b, b:null, openA:true, openB:false, color:'#8b5cf6' },
];
lineE.innerHTML = drawNumLine({ intervals });
out.innerHTML = '<b>Совокупность:</b> $x < ' + a + '$ или $x > ' + b + '$. Решение: $(-\\infty;\\,' + a + ') \\cup (' + b + ';\\,+\\infty)$.';
renderMath(out);
}
[aE,bE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
}
function buildP17stub(){ buildP17(); }
function buildP18stub(){ buildP18(); }
/* ============================================================
§ 17 — КВАДРАТНЫЕ НЕРАВЕНСТВА. МЕТОД ИНТЕРВАЛОВ
============================================================ */
function buildP17(){
const box = document.getElementById('p17-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Квадратное уравнение $ax^2 + bx + c = 0$ (Глава 2).</li>
<li>Дискриминант $D = b^2 - 4ac$, корни $x_{1,2} = \\dfrac{-b \\pm \\sqrt{D}}{2a}$.</li>
<li>Парабола: при $a > 0$ ветви вверх, при $a < 0$ — вниз.</li>
</ul>`);
html += makeCard('theory','Что такое квадратное неравенство','17.1',`
<p><b>Определение.</b> Неравенство вида $ax^2 + bx + c \\gtrless 0$, $a \\neq 0$.</p>
<p style="margin-top:6px"><b>Геометрический смысл:</b> найти, где парабола $y = ax^2 + bx + c$ <i>выше</i> или <i>ниже</i> оси OX (в зависимости от знака неравенства).</p>`);
html += makeCard('algo','Метод интервалов','17.2',`
<ol style="margin-left:18px;line-height:1.9">
<li>Найти корни уравнения $ax^2 + bx + c = 0$.</li>
<li>Если $D < 0$ — знак выражения постоянен (совпадает со знаком $a$).</li>
<li>Если $D \\geq 0$ — отметить корни на числовой прямой.</li>
<li>Определить знак выражения на каждом интервале (можно подставить пробную точку).</li>
<li>Выбрать интервалы, удовлетворяющие неравенству.</li>
</ol>
<p style="margin-top:8px"><b>Правило знаков для квадратного:</b> при $a > 0$ — между корнями знак минус, вне — плюс. При $a < 0$ — наоборот.</p>`);
html += makeCard('example','Пример',null,`
<p><b>Решить</b> $x^2 - 5x + 6 > 0$.</p>
<p>Корни: $x_1 = 2$, $x_2 = 3$. $a = 1 > 0$ — парабола вверх.</p>
<p>Знаки: $+$ при $x<2$, $-$ при $2<x<3$, $+$ при $x>3$.</p>
<p><b>Ответ:</b> $x \\in (-\\infty;\\,2) \\cup (3;\\,+\\infty)$.</p>`);
/* INT 1 — Парабола + закраска */
html += widget('Парабола и знак','INTERACT 1','Двигай $a, b, c$. Парабола показывает, где выражение положительно (зелёное) и отрицательно (красное).',`
<div class="sliders">
<label>$a$ = <b id="p17p-a-val">1</b><input type="range" min="-3" max="3" step="0.5" value="1" id="p17p-a"></label>
<label>$b$ = <b id="p17p-b-val">-1</b><input type="range" min="-6" max="6" step="0.5" value="-1" id="p17p-b"></label>
<label>$c$ = <b id="p17p-c-val">-6</b><input type="range" min="-8" max="8" step="0.5" value="-6" id="p17p-c"></label>
</div>
<svg id="p17p-svg" viewBox="-10 -10 240 180" style="width:100%;max-width:520px;display:block;margin:10px auto;background:#fafafa;border:1px solid var(--border);border-radius:10px"></svg>
<div id="p17p-out" style="padding:12px;background:var(--sec-acc-soft);border-radius:10px;line-height:1.7"></div>`);
/* INT 2 — Шаговый решатель */
html += widget('Метод интервалов: шаг за шагом','INTERACT 2','Введите $a, b, c$ для $ax^2 + bx + c \\geq 0$ и нажимайте «Дальше».',`
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px">
<label>$a$ = <input type="number" id="p17s-a" value="1" class="tinp" style="width:60px"></label>
<label>$b$ = <input type="number" id="p17s-b" value="-5" class="tinp" style="width:60px"></label>
<label>$c$ = <input type="number" id="p17s-c" value="6" class="tinp" style="width:60px"></label>
<button class="btn primary" id="p17s-go">Старт</button>
<button class="btn" id="p17s-next" style="display:none">Дальше</button>
<button class="btn" id="p17s-reset" style="display:none">Сначала</button>
</div>
<div id="p17s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:80px"></div>`);
/* INT 3 — Тренажёр */
html += widget('Тренажёр квадратных неравенств','INTERACT 3','Решите неравенство и выберите правильный промежуток-ответ.',`
<div class="score-display"><span>Задача <b id="p17t-i">1</b> / 6</span><span>Очки: <b id="p17t-score">0</b></span></div>
<div id="p17t-task" style="font-size:1.2rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div id="p17t-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p17t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p17t-start" style="margin-top:10px">Начать</button>`);
/* INT 4 — Drag: парабола ↔ ответ */
html += widget('Сопоставь параболу и неравенство','INTERACT 4','По описанию ситуации (направление ветвей + расположение корней) подбери решение неравенства $\\geq 0$.',`
${DND_HINT_HTML}
<div id="p17d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:10px">
<div class="drop-box"><h5>$(-\\infty; x_1] \\cup [x_2; +\\infty)$</h5><div class="drop-items" data-cat="out"></div></div>
<div class="drop-box"><h5>$[x_1; x_2]$</h5><div class="drop-items" data-cat="in"></div></div>
<div class="drop-box"><h5>$\\mathbb{R}$ (все числа)</h5><div class="drop-items" data-cat="all"></div></div>
<div class="drop-box"><h5>$\\emptyset$ (нет решений)</h5><div class="drop-items" data-cat="none"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p17d-check">Проверить</button><button class="btn" id="p17d-reset">Сначала</button></div>
<div class="feedback" id="p17d-fb" style="display:none"></div>`);
/* INT 5 — Знак между / вне корней */
html += widget('Где плюс, где минус?','INTERACT 5','Дана парабола. Кликни на интервал — раскрась знак.',`
<p style="margin-bottom:10px">Пусть $y = x^2 - 4x + 3$. Корни: $x_1 = 1$, $x_2 = 3$. Кликни по каждому интервалу и поставь знак.</p>
<div id="p17z-line" style="background:var(--card);border:1px solid var(--border);border-radius:8px;padding:12px"></div>
<div class="feedback" id="p17z-fb" style="display:none;margin-top:10px"></div>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 6x + 5 \\leq 0$</li>
<li>$-x^2 + 4x - 3 > 0$</li>
<li>$x^2 + 2x + 5 > 0$ (без корней — особый случай)</li>
<li>$4x^2 - 9 \\geq 0$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$x^2 - 7x + 10 < 0$</li>
<li>$3x^2 + 2x - 1 \\geq 0$</li>
<li>$x^2 + 1 \\leq 0$ (особый случай)</li>
<li>При каких $m$ неравенство $x^2 + 6x + m > 0$ верно для всех $x$?</li>
</ol>`);
html += secNav('p16', 'p18');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Парабола */
(function(){
const aE = document.getElementById('p17p-a'), bE = document.getElementById('p17p-b'), cE = document.getElementById('p17p-c');
const svg = document.getElementById('p17p-svg'), out = document.getElementById('p17p-out');
let done = false;
const W = 220, H = 160, x0 = W/2, y0 = H/2, sx = 14, sy = 14;
function refresh(){
const a = +aE.value, b = +bE.value, c = +cE.value;
document.getElementById('p17p-a-val').textContent = a;
document.getElementById('p17p-b-val').textContent = b;
document.getElementById('p17p-c-val').textContent = c;
let path = '';
for(let i = 0; i <= 240; i++){
const x = -W/(2*sx) + i * (W/sx) / 240;
const y = a*x*x + b*x + c;
const cx = x0 + x*sx, cy = y0 - y*sy;
path += (i === 0 ? 'M' : 'L') + cx.toFixed(2) + ',' + cy.toFixed(2) + ' ';
}
const D = b*b - 4*a*c;
const xs = D >= 0 ? [(-b - Math.sqrt(D))/(2*a), (-b + Math.sqrt(D))/(2*a)].sort((p,q)=>p-q) : [];
// фон зон
let s = '';
if(a !== 0){
if(D > 0){
// полосы зелёный/красный на оси
const x1 = x0 + xs[0]*sx, x2 = x0 + xs[1]*sx;
if(a > 0){
s += '<rect x="0" y="' + (y0-3) + '" width="' + x1 + '" height="6" fill="#10b981" opacity="0.35"/>';
s += '<rect x="' + x1 + '" y="' + (y0-3) + '" width="' + (x2-x1) + '" height="6" fill="#ef4444" opacity="0.35"/>';
s += '<rect x="' + x2 + '" y="' + (y0-3) + '" width="' + (W-x2) + '" height="6" fill="#10b981" opacity="0.35"/>';
} else {
s += '<rect x="0" y="' + (y0-3) + '" width="' + x1 + '" height="6" fill="#ef4444" opacity="0.35"/>';
s += '<rect x="' + x1 + '" y="' + (y0-3) + '" width="' + (x2-x1) + '" height="6" fill="#10b981" opacity="0.35"/>';
s += '<rect x="' + x2 + '" y="' + (y0-3) + '" width="' + (W-x2) + '" height="6" fill="#ef4444" opacity="0.35"/>';
}
} else {
s += '<rect x="0" y="' + (y0-3) + '" width="' + W + '" height="6" fill="' + (a*c >= 0 ? '#10b981' : '#ef4444') + '" opacity="0.35"/>';
}
}
s += '<line x1="0" y1="' + y0 + '" x2="' + W + '" y2="' + y0 + '" stroke="#94a3b8" stroke-width="0.8"/>';
s += '<line x1="' + x0 + '" y1="0" x2="' + x0 + '" y2="' + H + '" stroke="#94a3b8" stroke-width="0.8"/>';
s += '<path d="' + path + '" fill="none" stroke="#6366f1" stroke-width="2"/>';
xs.forEach(r => { s += '<circle cx="' + (x0 + r*sx).toFixed(2) + '" cy="' + y0 + '" r="4" fill="#6366f1"/>'; });
svg.innerHTML = s;
let info = '<div><b>$D$ = ' + D.toFixed(2) + '</b></div>';
if(D > 0){
info += '<div>Корни: $x_1 = ' + fmt(xs[0]) + ',\\ x_2 = ' + fmt(xs[1]) + '$</div>';
if(a > 0) info += '<div><b>$ax^2+bx+c > 0$:</b> $x < ' + fmt(xs[0]) + '$ или $x > ' + fmt(xs[1]) + '$</div><div><b>$ax^2+bx+c < 0$:</b> $' + fmt(xs[0]) + ' < x < ' + fmt(xs[1]) + '$</div>';
else info += '<div><b>$ax^2+bx+c > 0$:</b> $' + fmt(xs[0]) + ' < x < ' + fmt(xs[1]) + '$</div><div><b>$ax^2+bx+c < 0$:</b> $x < ' + fmt(xs[0]) + '$ или $x > ' + fmt(xs[1]) + '$</div>';
} else if(D === 0){
const r = -b/(2*a);
info += '<div>Один корень: $x = ' + fmt(r) + '$</div>';
info += '<div>Парабола касается оси. Знак — везде ' + (a > 0 ? 'положителен (кроме точки $x = ' + fmt(r) + '$)' : 'отрицателен') + '.</div>';
} else {
info += '<div>Корней нет. Парабола ' + (a > 0 ? 'выше' : 'ниже') + ' оси.</div>';
info += '<div>Знак выражения везде ' + (a > 0 ? 'положителен' : 'отрицателен') + '.</div>';
}
out.innerHTML = info; renderMath(out);
if(!done){ done = true; setTimeout(()=>{ achievement('p17_parab'); bumpProgress('p17', 14); }, 300); }
}
[aE,bE,cE].forEach(e => e.addEventListener('input', refresh));
refresh();
})();
/* INIT 2 — Метод интервалов шаговый */
(function(){
const stage = document.getElementById('p17s-stage');
const goBtn = document.getElementById('p17s-go'), nextBtn = document.getElementById('p17s-next'), resetBtn = document.getElementById('p17s-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b, c){
const arr = [];
arr.push('<b>Дано:</b> $' + a + 'x^2 ' + (b >= 0 ? '+ ' + b : '- ' + Math.abs(b)) + 'x ' + (c >= 0 ? '+ ' + c : '- ' + Math.abs(c)) + ' \\geq 0$');
const D = b*b - 4*a*c;
arr.push('<b>Шаг 1.</b> $D = b^2 - 4ac = ' + (b*b) + ' - ' + (4*a*c) + ' = ' + D + '$');
if(D < 0){
arr.push('<b>Шаг 2.</b> $D < 0$ — корней нет. Знак совпадает со знаком $a = ' + a + '$.');
if(a > 0) arr.push('<b>Шаг 3.</b> $a > 0$ — выражение всегда $> 0$, тем более $\\geq 0$. Ответ: $\\mathbb{R}$.');
else arr.push('<b>Шаг 3.</b> $a < 0$ — выражение всегда $< 0$, ни одна точка не удовлетворяет $\\geq 0$. Ответ: $\\emptyset$.');
} else if(D === 0){
const r = -b/(2*a);
arr.push('<b>Шаг 2.</b> $D = 0$ — один корень: $x = ' + fmt(r) + '$.');
if(a > 0) arr.push('<b>Шаг 3.</b> $a > 0$ — выражение всюду $\\geq 0$, равно нулю только в $x = ' + fmt(r) + '$. Ответ: $\\mathbb{R}$.');
else arr.push('<b>Шаг 3.</b> $a < 0$ — выражение всюду $\\leq 0$, равно нулю только в $x = ' + fmt(r) + '$. Ответ: $\\{' + fmt(r) + '\\}$.');
} else {
const x1 = (-b - Math.sqrt(D))/(2*a), x2 = (-b + Math.sqrt(D))/(2*a);
const lo = Math.min(x1, x2), hi = Math.max(x1, x2);
arr.push('<b>Шаг 2.</b> Корни: $x_1 = ' + fmt(lo) + ',\\ x_2 = ' + fmt(hi) + '$');
if(a > 0){
arr.push('<b>Шаг 3.</b> $a > 0$ — парабола вверх. Знак: $+$ при $x < x_1$, $-$ между корнями, $+$ при $x > x_2$.');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + fmt(lo) + '] \\cup [' + fmt(hi) + ';\\,+\\infty)$');
} else {
arr.push('<b>Шаг 3.</b> $a < 0$ — парабола вниз. Знак: $-$ вне, $+$ между корнями.');
arr.push('<b>Ответ:</b> $x \\in [' + fmt(lo) + ';\\,' + fmt(hi) + ']$');
}
}
return arr;
}
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('p17_solver'); bumpProgress('p17', 16); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p17s-a').value, b = +document.getElementById('p17s-b').value, c = +document.getElementById('p17s-c').value;
if(!a){ stage.innerHTML = '<p>$a \\neq 0$</p>'; renderMath(stage); return; }
steps = build(a, b, c); idx = 0; awarded = false;
goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
render();
});
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
})();
/* INIT 3 — Тренажёр */
(function(){
const tasks = [
{ q:'$x^2 - 5x + 6 > 0$', opts:['$(-\\infty;\\,2) \\cup (3;\\,+\\infty)$','$[2;\\,3]$','$(2;\\,3)$','$\\mathbb{R}$'], ok:0 },
{ q:'$x^2 - 4x + 3 \\leq 0$', opts:['$[1;\\,3]$','$(-\\infty;\\,1] \\cup [3;\\,+\\infty)$','$(1;\\,3)$','$\\emptyset$'], ok:0 },
{ q:'$x^2 + 1 > 0$', opts:['$\\mathbb{R}$','$\\emptyset$','$x \\neq 0$','$x > 0$'], ok:0 },
{ q:'$x^2 + 2x + 5 < 0$', opts:['$\\emptyset$','$\\mathbb{R}$','$(-1;\\,1)$','$x < -1$'], ok:0 },
{ q:'$-x^2 + 4 \\geq 0$', opts:['$[-2;\\,2]$','$(-\\infty;\\,-2] \\cup [2;\\,+\\infty)$','$(-2;\\,2)$','$\\mathbb{R}$'], ok:0 },
{ q:'$x^2 - 9 < 0$', opts:['$(-3;\\,3)$','$[-3;\\,3]$','$(-\\infty;\\,-3) \\cup (3;\\,+\\infty)$','$\\emptyset$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p17t-i').textContent = i;
document.getElementById('p17t-task').innerHTML = '$' + cur.q + '$';
renderMath(document.getElementById('p17t-task'));
const opts = document.getElementById('p17t-opts'); opts.innerHTML = '';
cur.opts.forEach((o, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p17t-fb'); fb.style.display = 'block';
if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
document.getElementById('p17t-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p17_train'); bumpProgress('p17', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p17t-fb').style.display = 'none';
}
document.getElementById('p17t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p17t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 4 — Drag парабола ↔ ответ */
(function(){
const items = [
{ id:1, html:'$a > 0$, есть 2 корня, неравенство $\\geq 0$', cat:'out' },
{ id:2, html:'$a > 0$, есть 2 корня, неравенство $\\leq 0$', cat:'in' },
{ id:3, html:'$a < 0$, есть 2 корня, неравенство $\\geq 0$', cat:'in' },
{ id:4, html:'$a > 0$, $D < 0$, неравенство $\\geq 0$', cat:'all' },
{ id:5, html:'$a > 0$, $D < 0$, неравенство $\\leq 0$', cat:'none' },
{ id:6, html:'$a < 0$, $D < 0$, неравенство $\\geq 0$', cat:'none' },
{ id:7, html:'$a < 0$, $D < 0$, неравенство $\\leq 0$', cat:'all' },
{ id:8, html:'$a < 0$, есть 2 корня, неравенство $\\leq 0$', cat:'out' },
];
const sorter = setupSorter({ poolId:'p17d-pool', cats:['out','in','all','none'], items, scopeSelector:'#p17-body', columnLayout:true });
document.getElementById('p17d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p17d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p17_drag'); bumpProgress('p17', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p17d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p17d-fb').style.display='none'; });
})();
/* INIT 5 — Знаки на интервалах (клик) */
(function(){
// 3 интервала: (-inf, 1), (1, 3), (3, inf). Должны быть: +, -, +.
const correct = ['+','-','+'];
const labels = ['$x < 1$', '$1 < x < 3$', '$x > 3$'];
const lineE = document.getElementById('p17z-line');
function build(){
let s = '<div style="display:flex;gap:8px;flex-wrap:wrap;justify-content:center;align-items:center;font-size:1.1rem">';
s += '<span>$-\\infty$</span>';
[0, 1, 2].forEach(i => {
s += '<button class="btn p17z-int" data-i="' + i + '" data-sign="" style="min-width:90px">' + labels[i] + '<br><span class="sg">?</span></button>';
if(i < 2) s += '<span style="font-family:JetBrains Mono,monospace;font-size:1.2rem;color:var(--sec-acc-d)">●' + (i === 0 ? ' 1 ●' : ' 3 ●').replace('●','') + '</span>';
});
s += '<span>$+\\infty$</span></div>';
lineE.innerHTML = s;
renderMath(lineE);
document.querySelectorAll('.p17z-int').forEach(btn => {
btn.addEventListener('click', ()=>{
const cur = btn.dataset.sign;
const next = cur === '' ? '+' : cur === '+' ? '-' : '';
btn.dataset.sign = next;
btn.querySelector('.sg').textContent = next || '?';
btn.style.background = next === '+' ? 'rgba(16,185,129,.2)' : next === '-' ? 'rgba(239,68,68,.2)' : '';
checkAll();
});
});
}
function checkAll(){
const all = [...document.querySelectorAll('.p17z-int')];
const got = all.map(b => b.dataset.sign);
if(got.every((s, i) => s === correct[i])){
const fb = document.getElementById('p17z-fb'); fb.style.display = 'block';
feedback(fb, true, '&#10003; Точно! Между корнями знак минус (так как $a > 0$).');
achievement('p17_intervals'); bumpProgress('p17', 14); confetti();
}
}
build();
})();
}
/* ============================================================
§ 18 — ДРОБНО-РАЦИОНАЛЬНЫЕ НЕРАВЕНСТВА
============================================================ */
function buildP18(){
const box = document.getElementById('p18-body');
let html = '';
html += makeCard('repeat','Повторение',null,`
<ul style="margin-left:18px;line-height:1.7">
<li>Метод интервалов из § 17 — корни, знаки на интервалах.</li>
<li>ОДЗ (Глава 2 § 12): знаменатель $\\neq 0$.</li>
<li>$\\dfrac{a}{b}$ имеет тот же знак, что и $a \\cdot b$.</li>
</ul>`);
html += makeCard('theory','Что такое дробно-рациональное неравенство','18.1',`
<p><b>Дробно-рациональное</b> — неравенство, в котором есть дроби с переменной в знаменателе:</p>
<div style="background:var(--sec-acc-soft);border-radius:10px;padding:10px;margin:8px 0;text-align:center;font-size:1.1rem">$$\\dfrac{f(x)}{g(x)} \\gtrless 0$$</div>
<p><b>Ключевая идея:</b> знак дроби определяется произведением знаков числителя и знаменателя. Метод интервалов работает, но точки, где знаменатель $= 0$, всегда <b>выколотые</b> (не входят в ОДЗ).</p>`);
html += makeCard('algo','Алгоритм','18.2',`
<ol style="margin-left:18px;line-height:1.9">
<li>Привести к виду $\\dfrac{f(x)}{g(x)} \\gtrless 0$ (всё в одну часть, общий знаменатель).</li>
<li>Найти нули числителя $f(x) = 0$ и знаменателя $g(x) = 0$.</li>
<li>Отметить точки на прямой: нули $f$ — закрашены (если знак $\\geq, \\leq$), нули $g$ — всегда выколотые.</li>
<li>Определить знак выражения на каждом интервале.</li>
<li>Выбрать интервалы, удовлетворяющие неравенству.</li>
</ol>`);
html += makeCard('example','Пример',null,`
<p><b>Решим:</b> $\\dfrac{x - 1}{x + 2} \\geq 0$.</p>
<p>Нули: числитель $x = 1$ (входит), знаменатель $x = -2$ (выколот).</p>
<p>Знаки на интервалах: $(-\\infty;\\,-2)$ — $+$ (минус на минус), $(-2;\\,1)$ — $-$ (плюс на минус), $(1;\\,+\\infty)$ — $+$.</p>
<p><b>Ответ:</b> $x \\in (-\\infty;\\,-2) \\cup [1;\\,+\\infty)$.</p>`);
/* INT 1 — Пошаговый решатель */
html += widget('Пошаговый решатель дроби','INTERACT 1','Решаем $\\dfrac{x-a}{x-b} \\geq 0$ пошагово.',`
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center;margin-bottom:10px;font-size:1.05rem">
<span>$\\dfrac{x -$</span><input type="number" id="p18s-a" value="1" class="tinp" style="width:55px"><span>$}{x -$</span><input type="number" id="p18s-b" value="-2" class="tinp" style="width:55px"><span>$} \\geq 0$</span>
<button class="btn primary" id="p18s-go">Старт</button>
<button class="btn" id="p18s-next" style="display:none">Дальше</button>
<button class="btn" id="p18s-reset" style="display:none">Сначала</button>
</div>
<div id="p18s-stage" style="padding:14px;background:var(--card-soft);border-radius:10px;min-height:80px"></div>`);
/* INT 2 — Тренажёр */
html += widget('Тренажёр дробно-рациональных','INTERACT 2','Выбери правильный ответ.',`
<div class="score-display"><span>Задача <b id="p18t-i">1</b> / 6</span><span>Очки: <b id="p18t-score">0</b></span></div>
<div id="p18t-task" style="font-size:1.2rem;text-align:center;padding:16px;background:var(--sec-acc-soft);border-radius:10px;margin-bottom:10px"></div>
<div id="p18t-opts" style="display:flex;flex-direction:column;gap:6px"></div>
<div class="feedback" id="p18t-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p18t-start" style="margin-top:10px">Начать</button>`);
/* INT 3 — Найди ОДЗ */
html += widget('Найди ОДЗ','INTERACT 3','По выражению определи запрещённые точки (где знаменатель = 0).',`
<div class="score-display"><span>Раунд <b id="p18o-i">1</b> / 5</span><span>Очки: <b id="p18o-score">0</b></span></div>
<div id="p18o-task" style="font-size:1.15rem;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">
<input type="text" id="p18o-inp" placeholder="x = ... ; x = ..." class="tinp" style="width:220px">
<button class="btn primary" id="p18o-go">Ответ</button>
</div>
<div class="feedback" id="p18o-fb" style="display:none;margin-top:10px"></div>
<button class="btn primary" id="p18o-start" style="margin-top:10px">Начать</button>`);
/* INT 4 — Drag: закрашена/выколота */
html += widget('Закрашена или выколота?','INTERACT 4','Отнеси каждую точку к нужной категории.',`
${DND_HINT_HTML}
<div id="p18d-pool"></div>
<div class="drop-row" style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div class="drop-box"><h5>Закрашена (входит)</h5><div class="drop-items" data-cat="full"></div></div>
<div class="drop-box"><h5>Выколота (не входит)</h5><div class="drop-items" data-cat="open"></div></div>
</div>
<div class="actions"><button class="btn primary" id="p18d-check">Проверить</button><button class="btn" id="p18d-reset">Сначала</button></div>
<div class="feedback" id="p18d-fb" style="display:none"></div>`);
html += makeCard('class','Класс — решите',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\dfrac{x - 2}{x + 3} > 0$</li>
<li>$\\dfrac{x + 1}{x - 4} \\leq 0$</li>
<li>$\\dfrac{x^2 - 4}{x - 1} \\geq 0$</li>
</ol>`);
html += makeCard('home','Домашка',null,`
<ol style="margin-left:18px;line-height:1.8">
<li>$\\dfrac{x - 3}{x + 1} \\geq 0$</li>
<li>$\\dfrac{x + 5}{x^2 - 4} > 0$</li>
<li>$\\dfrac{x^2 - 9}{x^2 + 2x - 8} \\leq 0$</li>
</ol>`);
html += secNav('p17', 'final3');
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* INIT 1 — Пошаговый */
(function(){
const stage = document.getElementById('p18s-stage');
const goBtn = document.getElementById('p18s-go'), nextBtn = document.getElementById('p18s-next'), resetBtn = document.getElementById('p18s-reset');
let steps = [], idx = 0, awarded = false;
function build(a, b){
const arr = [];
arr.push('<b>Дано:</b> $\\dfrac{x - (' + a + ')}{x - (' + b + ')} \\geq 0$');
arr.push('<b>Шаг 1.</b> Нули числителя: $x = ' + a + '$ (входит, $\\geq$). Нули знаменателя: $x = ' + b + '$ (всегда выколот).');
const lo = Math.min(a, b), hi = Math.max(a, b);
arr.push('<b>Шаг 2.</b> Отметим на прямой: ' + (a < b ? ('$' + a + '$ (закрашена), $' + b + '$ (выколота)') : ('$' + b + '$ (выколота), $' + a + '$ (закрашена)')) + '.');
arr.push('<b>Шаг 3.</b> Знаки: подставим $x = ' + (hi + 1) + '$ — числитель $' + (hi + 1 - a) + ' > 0$, знаменатель $' + (hi + 1 - b) + ' > 0$, дробь $> 0$. Чередуем знаки от правого края: $+,\\ -,\\ +$.');
if(a < b){
arr.push('<b>Шаг 4.</b> $\\geq 0$ — берём $+$. Это $(-\\infty;\\,' + a + ']$ и $(' + b + ';\\,+\\infty)$.');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + a + '] \\cup (' + b + ';\\,+\\infty)$');
} else if(a > b){
arr.push('<b>Шаг 4.</b> $\\geq 0$ — берём $+$. Это $(-\\infty;\\,' + b + ')$ и $[' + a + ';\\,+\\infty)$.');
arr.push('<b>Ответ:</b> $x \\in (-\\infty;\\,' + b + ') \\cup [' + a + ';\\,+\\infty)$');
} else {
arr.push('<b>Шаг 4.</b> $a = b$ — дробь равна 1 везде, кроме $x = ' + a + '$ (выколота). $1 \\geq 0$. Ответ: $x \\neq ' + a + '$.');
}
return arr;
}
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('p18_solver'); bumpProgress('p18', 16); confetti(); }
} else { nextBtn.disabled = false; nextBtn.textContent = 'Дальше (' + (idx + 1) + '/' + steps.length + ')'; }
}
goBtn.addEventListener('click', ()=>{
const a = +document.getElementById('p18s-a').value, b = +document.getElementById('p18s-b').value;
steps = build(a, b); idx = 0; awarded = false;
goBtn.style.display = 'none'; nextBtn.style.display = ''; resetBtn.style.display = '';
render();
});
nextBtn.addEventListener('click', ()=>{ if(idx < steps.length - 1){ idx++; render(); } });
resetBtn.addEventListener('click', ()=>{ idx = 0; stage.innerHTML = ''; goBtn.style.display = ''; nextBtn.style.display = 'none'; resetBtn.style.display = 'none'; });
})();
/* INIT 2 — Тренажёр */
(function(){
const tasks = [
{ q:'$\\dfrac{x - 3}{x + 1} > 0$', opts:['$(-\\infty;\\,-1) \\cup (3;\\,+\\infty)$','$(-1;\\,3)$','$[-1;\\,3]$','$\\emptyset$'], ok:0 },
{ q:'$\\dfrac{x + 2}{x - 5} \\leq 0$', opts:['$[-2;\\,5)$','$(-2;\\,5)$','$[-2;\\,5]$','$(-\\infty;\\,-2] \\cup (5;\\,+\\infty)$'], ok:0 },
{ q:'$\\dfrac{1}{x - 4} > 0$', opts:['$(4;\\,+\\infty)$','$(-\\infty;\\,4)$','$\\mathbb{R} \\setminus \\{4\\}$','$[4;\\,+\\infty)$'], ok:0 },
{ q:'$\\dfrac{x - 1}{x + 3} \\geq 0$', opts:['$(-\\infty;\\,-3) \\cup [1;\\,+\\infty)$','$[-3;\\,1]$','$(-3;\\,1)$','$\\mathbb{R}$'], ok:0 },
{ q:'$\\dfrac{x + 6}{x} < 0$', opts:['$(-6;\\,0)$','$[-6;\\,0)$','$(-\\infty;\\,-6)$','$(0;\\,+\\infty)$'], ok:0 },
{ q:'$\\dfrac{x - 2}{x - 2} > 0$', opts:['$\\mathbb{R} \\setminus \\{2\\}$','$\\mathbb{R}$','$\\{2\\}$','$\\emptyset$'], ok:0 },
];
let cur = null, i = 1, score = 0, shuffled = [];
function show(){
cur = shuffled[i-1];
document.getElementById('p18t-i').textContent = i;
document.getElementById('p18t-task').innerHTML = '$' + cur.q.replace(/^\$|\$$/g,'') + '$';
renderMath(document.getElementById('p18t-task'));
const opts = document.getElementById('p18t-opts'); opts.innerHTML = '';
cur.opts.forEach((o, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>{
const fb = document.getElementById('p18t-fb'); fb.style.display = 'block';
if(k === cur.ok){ score++; b.classList.add('ok'); feedback(fb, true, '&#10003;'); }
else { b.classList.add('fail'); feedback(fb, false, 'Не то.'); }
document.getElementById('p18t-score').textContent = score;
if(i >= shuffled.length){ setTimeout(()=>{ feedback(fb, score >= 4, 'Итог: ' + score + '/' + shuffled.length); if(score >= 4){ achievement('p18_intervals'); bumpProgress('p18', 16); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
opts.appendChild(b);
});
renderMath(opts);
document.getElementById('p18t-fb').style.display = 'none';
}
document.getElementById('p18t-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p18t-score').textContent = 0; shuffled = [...tasks].sort(()=>Math.random()-0.5); show(); });
})();
/* INIT 3 — ОДЗ */
(function(){
const tasks = [
{ q:'$\\dfrac{x + 1}{x - 3}$', ans:[3] },
{ q:'$\\dfrac{x^2 + 1}{x(x - 5)}$', ans:[0, 5] },
{ q:'$\\dfrac{1}{x^2 - 4}$', ans:[-2, 2] },
{ q:'$\\dfrac{x + 3}{x^2 - 6x + 9}$', ans:[3] },
{ q:'$\\dfrac{1}{x} + \\dfrac{1}{x + 1}$', ans:[-1, 0] },
];
let cur = null, i = 1, score = 0;
function show(){
cur = tasks[i-1];
document.getElementById('p18o-i').textContent = i;
document.getElementById('p18o-task').innerHTML = 'Запрещённые точки для ' + cur.q;
renderMath(document.getElementById('p18o-task'));
document.getElementById('p18o-inp').value = '';
document.getElementById('p18o-fb').style.display = 'none';
}
document.getElementById('p18o-go').addEventListener('click', ()=>{
const fb = document.getElementById('p18o-fb'); fb.style.display = 'block';
const u = document.getElementById('p18o-inp').value.replace(/[xX\s=]/g, '').split(/[;,]+/).filter(Boolean).map(Number).sort((a,b)=>a-b);
const a = [...cur.ans].sort((p,q)=>p-q);
const ok = u.length === a.length && a.every((v, k) => v === u[k]);
if(ok){ score++; feedback(fb, true, '&#10003;'); }
else feedback(fb, false, 'Правильно: ' + a.join(', '));
document.getElementById('p18o-score').textContent = score;
if(i >= tasks.length){ setTimeout(()=>{ feedback(fb, score >= 3, 'Итог: ' + score + '/' + tasks.length); if(score >= 3){ achievement('p18_odz'); bumpProgress('p18', 14); confetti(); } }, 700); }
else { i++; setTimeout(show, 900); }
});
document.getElementById('p18o-start').addEventListener('click', ()=>{ i=1; score=0; document.getElementById('p18o-score').textContent = 0; show(); });
})();
/* INIT 4 — Drag закрашена/выколота */
(function(){
const items = [
{ id:1, html:'нуль числителя при $\\geq 0$', cat:'full' },
{ id:2, html:'нуль знаменателя', cat:'open' },
{ id:3, html:'нуль числителя при $> 0$ строго', cat:'open' },
{ id:4, html:'нуль числителя при $\\leq 0$', cat:'full' },
{ id:5, html:'точка вне ОДЗ', cat:'open' },
{ id:6, html:'граница в системе с $\\leq$', cat:'full' },
{ id:7, html:'граница в системе с $<$', cat:'open' },
{ id:8, html:'нуль знаменателя — всегда', cat:'open' },
];
const sorter = setupSorter({ poolId:'p18d-pool', cats:['full','open'], items, scopeSelector:'#p18-body', columnLayout:true });
document.getElementById('p18d-check').addEventListener('click', ()=>{
const fb = document.getElementById('p18d-fb'); fb.style.display = 'block';
if(Object.keys(sorter.placed).length < items.length){ feedback(fb, false, '&#9888; Разложите все.'); return; }
let ok = 0; items.forEach(it=>{ if(sorter.placed[it.id] === it.cat) ok++; });
if(ok === items.length){ feedback(fb, true, '&#10003; Все верно!'); achievement('p18_odz'); bumpProgress('p18', 14); confetti(); }
else feedback(fb, false, 'Верно ' + ok + ' из ' + items.length);
});
document.getElementById('p18d-reset').addEventListener('click', ()=>{ sorter.reset(); document.getElementById('p18d-fb').style.display='none'; });
})();
}
function buildFinal3stub(){ buildFinal3(); }
function buildFinal3(){
const box = document.getElementById('final3-body');
let html = '';
html += `<div class="card"><div class="card-header"><div class="card-icon theory">${ICONS.theory}</div><div class="card-title">Поздравляем!</div></div><div class="card-body">
<p>Вы прошли 6 параграфов главы «Неравенства с одной переменной». В финале вас ждут <b>7 боссов</b> — по одному на каждый параграф и один общий. Победите всех — получите титул «Чемпион неравенств».</p>
</div></div>`;
html += widget('Боссы главы 3','BOSS ARENA','Каждый босс — 5 заданий. Победите всех 7, чтобы открыть финальный титул.',`
<div id="boss-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px"></div>
<div id="boss-arena" style="margin-top:14px;display:none">
<div id="boss-head" style="display:flex;align-items:center;gap:14px;padding:14px;background:linear-gradient(135deg,#1f2937,#0f172a);border-radius:10px;color:#fff;margin-bottom:10px">
<div id="boss-emoji" style="font-size:2.4rem;line-height:1">★</div>
<div style="flex:1">
<div id="boss-name" style="font-family:'Unbounded',sans-serif;font-weight:800;font-size:1.05rem"></div>
<div id="boss-subname" style="font-size:.85rem;opacity:.75"></div>
<div class="hp-bar" style="margin-top:6px;background:rgba(255,255,255,.15);height:8px;border-radius:4px;overflow:hidden"><div id="boss-hp" style="height:100%;background:linear-gradient(90deg,#10b981,#fbbf24,#ef4444);width:100%;transition:width .5s"></div></div>
</div>
<button class="btn" id="boss-quit" style="background:rgba(255,255,255,.15);color:#fff;border-color:transparent">Выйти</button>
</div>
<div id="boss-task" style="padding:16px;background:var(--card);border:1.5px solid var(--sec-acc);border-radius:10px;min-height:120px"></div>
<div id="boss-opts" style="display:flex;flex-direction:column;gap:6px;margin-top:10px"></div>
<div id="boss-fb" class="feedback" style="display:none;margin-top:10px"></div>
</div>`);
html += widget('Увлекательная математика','BONUS','3 факта про неравенства.',`
<details class="spoiler" style="margin-bottom:8px"><summary>Почему меняется знак при умножении на отрицательное?</summary>
<div class="spoiler-body">Геометрически: умножение на $-1$ — это отражение числовой прямой относительно нуля. То, что было правее (больше), теперь оказалось левее (меньше). Поэтому «больше» становится «меньше».</div>
</details>
<details class="spoiler" style="margin-bottom:8px"><summary>Кто придумал знаки $<$ и $>$?</summary>
<div class="spoiler-body">Английский математик Томас Хэрриот в 1631 году предложил эти знаки. Левый конец — узкий, правый — широкий: с маленькой стороны меньшее, с широкой — большее. Знак <i>точка слева, точка справа</i> наглядно показывает направление сравнения.</div>
</details>
<details class="spoiler"><summary>Неравенство Коши</summary>
<div class="spoiler-body">Знаменитое неравенство: $\\dfrac{a + b}{2} \\geq \\sqrt{ab}$ для $a, b \\geq 0$ — среднее арифметическое не меньше среднего геометрического. Равенство — когда $a = b$. Это краеугольный камень многих доказательств в высшей математике.</div>
</details>`);
html += widget('Финальная практика','PRACTICE','Случайные задачи из всей главы.',`
<div class="score-display"><span>Решено: <b id="prac-i">0</b></span><span>Правильно: <b id="prac-score">0</b></span><span>Серия: <b id="prac-streak">0</b></span></div>
<div id="prac-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 style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;justify-content:center">
<input type="text" id="prac-inp" placeholder="ответ" class="tinp" style="width:200px">
<button class="btn primary" id="prac-go">Ответ</button>
<button class="btn" id="prac-next">Следующая</button>
</div>
<div class="feedback" id="prac-fb" style="display:none;margin-top:10px"></div>`);
html += `<div class="card"><div class="card-header"><div class="card-icon home">${ICONS.home}</div><div class="card-title">Сертификат прохождения</div></div><div class="card-body">
<p>Как вы оцените своё знание неравенств?</p>
<div id="cert-state" style="margin-top:10px;font-size:.9rem;color:var(--muted)"></div>
</div></div>`;
html += secNav('p18', null);
box.innerHTML = html;
if(window.renderMathInElement) setTimeout(()=>renderMath(box), 0);
/* BOSSES */
const BOSSES = [
{ id:'b1', name:'Хранитель сравнения', sub:'§ 13 · свойства неравенств', icon:'<>', hp:5, tasks:[
{ t:'select', q:'Что больше: $-7$ или $-3$?', opts:['$-3$','$-7$','равны','зависит'], ok:0 },
{ t:'yesno', q:'При умножении на $-2$ знак неравенства меняется.', ok:true },
{ t:'input', q:'Дано $a > b$. Найдите $c$ такое, что $a + c$ и $b + c$ остаются в том же порядке. (введите любое число, например 5)', ans:[5] },
{ t:'select', q:'$a > b$, $b > c$. Тогда:', opts:['$a > c$','$a < c$','$a = c$','зависит'], ok:0 },
{ t:'yesno', q:'Из $a > b > 0$ следует $a^2 > b^2$.', ok:true },
]},
{ id:'b2', name:'Алхимик границ', sub:'§ 14 · оценка значений', icon:'±', hp:5, tasks:[
{ t:'input', q:'$1 \\leq x \\leq 3$, $2 \\leq y \\leq 5$. Найдите верхнюю границу $x + y$.', ans:[8] },
{ t:'input', q:'Те же границы. Найдите нижнюю границу $x - y$.', ans:[-4] },
{ t:'input', q:'Те же границы (все $>0$). Найдите верхнюю границу $xy$.', ans:[15] },
{ t:'yesno', q:'Можно сложить $a > b$ и $c < d$, чтобы получить $a + c > b + d$.', ok:false },
{ t:'select', q:'Если $0 < a < b$ и $0 < c < d$, то:', opts:['$ac < bd$','$ac > bd$','$ac = bd$','неизвестно'], ok:0 },
]},
{ id:'b3', name:'Архитектор промежутков', sub:'§ 15 · линейные', icon:'[]', hp:5, tasks:[
{ t:'select', q:'Решение $2x - 4 > 0$:', opts:['$(2;\\,+\\infty)$','$[2;\\,+\\infty)$','$(-\\infty;\\,2)$','$\\{2\\}$'], ok:0 },
{ t:'select', q:'Решение $-3x \\geq 9$:', opts:['$(-\\infty;\\,-3]$','$[-3;\\,+\\infty)$','$(-\\infty;\\,3]$','$[3;\\,+\\infty)$'], ok:0 },
{ t:'yesno', q:'Промежуток $[2;\\,5)$ включает точку $5$.', ok:false },
{ t:'select', q:'Запись $x > 4$ соответствует:', opts:['$(4;\\,+\\infty)$','$[4;\\,+\\infty)$','$(-\\infty;\\,4)$','$(-\\infty;\\,4]$'], ok:0 },
{ t:'input', q:'Корень неравенства $5x = 25$ (граница). Чему равен $x$?', ans:[5] },
]},
{ id:'b4', name:'Дирижёр пересечений', sub:'§ 16 · системы', icon:'∩', hp:5, tasks:[
{ t:'select', q:'$\\begin{cases} x > 2 \\\\ x \\leq 5 \\end{cases}$:', opts:['$(2;\\,5]$','$[2;\\,5)$','$(2;\\,5)$','$\\emptyset$'], ok:0 },
{ t:'yesno', q:'Система $\\begin{cases} x > 5 \\\\ x < 3 \\end{cases}$ имеет решение.', ok:false },
{ t:'select', q:'Совокупность $\\left[\\begin{array}{l} x < 0 \\\\ x \\geq 4 \\end{array}\\right.$:', opts:['$(-\\infty;\\,0) \\cup [4;\\,+\\infty)$','$[0;\\,4)$','$\\emptyset$','$\\mathbb{R}$'], ok:0 },
{ t:'yesno', q:'Система использует логическое «И», совокупность — «ИЛИ».', ok:true },
{ t:'select', q:'Пересечение $[1;\\,5]$ и $[3;\\,8]$:', opts:['$[3;\\,5]$','$[1;\\,8]$','$\\emptyset$','$[1;\\,3]$'], ok:0 },
]},
{ id:'b5', name:'Мастер параболы', sub:'§ 17 · квадратные', icon:'', hp:5, tasks:[
{ t:'select', q:'Решение $x^2 - 4 > 0$:', opts:['$(-\\infty;\\,-2) \\cup (2;\\,+\\infty)$','$(-2;\\,2)$','$[-2;\\,2]$','$\\emptyset$'], ok:0 },
{ t:'select', q:'$x^2 + 1 > 0$:', opts:['$\\mathbb{R}$','$\\emptyset$','$x \\neq 0$','$x > 0$'], ok:0 },
{ t:'yesno', q:'При $D < 0$ и $a > 0$ выражение $ax^2 + bx + c$ всегда положительно.', ok:true },
{ t:'select', q:'$x^2 - 5x + 6 \\leq 0$ имеет решение:', opts:['$[2;\\,3]$','$(2;\\,3)$','$(-\\infty;\\,2) \\cup (3;\\,+\\infty)$','$\\emptyset$'], ok:0 },
{ t:'input', q:'Сколько решений у $x^2 + 9 < 0$ (количество, 0 если нет)?', ans:[0] },
]},
{ id:'b6', name:'Властелин ОДЗ', sub:'§ 18 · дробно-рац.', icon:'1/x', hp:5, tasks:[
{ t:'input', q:'Запрещённая точка для $\\dfrac{1}{x - 5}$:', ans:[5] },
{ t:'select', q:'$\\dfrac{x - 1}{x + 2} > 0$:', opts:['$(-\\infty;\\,-2) \\cup (1;\\,+\\infty)$','$(-2;\\,1)$','$[-2;\\,1]$','$\\emptyset$'], ok:0 },
{ t:'yesno', q:'Точка, где знаменатель равен нулю, ВСЕГДА выколотая.', ok:true },
{ t:'select', q:'$\\dfrac{x + 3}{x - 4} \\leq 0$:', opts:['$[-3;\\,4)$','$(-3;\\,4)$','$[-3;\\,4]$','$\\emptyset$'], ok:0 },
{ t:'yesno', q:'$\\dfrac{1}{x} > 0$ верно при любом $x \\neq 0$.', ok:false },
]},
{ id:'b7', name:'Чемпион неравенств', sub:'Финал · вся глава', icon:'★', hp:7, tasks:[
{ t:'input', q:'$4x + 3 > 11$. Граничное значение $x$:', ans:[2] },
{ t:'select', q:'Знак $a > b$ после умножения на $-5$:', opts:['Меняется','Не меняется','Зависит','Исчезает'], ok:0 },
{ t:'select', q:'$1 \\leq x \\leq 4$, $2 \\leq y \\leq 6$. Верхняя граница $xy$:', opts:['$24$','$8$','$12$','$10$'], ok:0 },
{ t:'select', q:'$\\begin{cases} x \\geq 0 \\\\ x \\leq 10 \\end{cases}$:', opts:['$[0;\\,10]$','$(0;\\,10)$','$\\mathbb{R}$','$\\emptyset$'], ok:0 },
{ t:'select', q:'$x^2 - 9 < 0$:', opts:['$(-3;\\,3)$','$[-3;\\,3]$','$(-\\infty;\\,-3) \\cup (3;\\,+\\infty)$','$\\emptyset$'], ok:0 },
{ t:'input', q:'Запрещённая точка для $\\dfrac{x + 1}{x - 7}$:', ans:[7] },
{ t:'yesno', q:'Метод интервалов работает только для квадратных неравенств.', ok:false },
]},
];
const BOSS_STATE = (function(){
try { return JSON.parse(localStorage.getItem('algebra8_ch3_bosses') || '{}'); } catch(e){ return {}; }
})();
function saveBosses(){ try{ localStorage.setItem('algebra8_ch3_bosses', JSON.stringify(BOSS_STATE)); }catch(e){} }
function renderBossGrid(){
const g = document.getElementById('boss-grid');
g.innerHTML = '';
BOSSES.forEach(b=>{
const won = BOSS_STATE[b.id];
const c = document.createElement('div');
c.style.cssText = 'background:' + (won ? 'linear-gradient(135deg,var(--ok-bg),#d1fae5)' : 'var(--card)') + ';border:1.5px solid ' + (won ? 'var(--ok)' : 'var(--border)') + ';border-radius:10px;padding:10px 12px;cursor:pointer;text-align:center;transition:transform .15s,box-shadow .15s';
c.innerHTML = '<div style="font-size:1.5rem;line-height:1;margin-bottom:4px;font-family:JetBrains Mono,monospace">' + b.icon + '</div><div style="font-family:Unbounded,sans-serif;font-size:.78rem;font-weight:800;color:var(--sec-acc-d)">' + b.name + '</div><div style="font-size:.7rem;color:var(--muted);margin-top:2px">' + b.sub + '</div>' + (won ? '<div style="color:var(--ok);font-weight:700;font-size:.8rem;margin-top:4px">&#10003; Побеждён</div>' : '<div style="margin-top:4px"><button class="btn small" style="font-size:.72rem">В бой</button></div>');
c.addEventListener('click', ()=>startBoss(b.id));
c.addEventListener('mouseover', ()=>{ c.style.transform='translateY(-2px)'; c.style.boxShadow='var(--sh2)'; });
c.addEventListener('mouseout', ()=>{ c.style.transform=''; c.style.boxShadow=''; });
g.appendChild(c);
});
if(window.renderMathInElement) renderMath(g);
}
let currentBoss = null, taskIdx = 0, hpLeft = 0;
function startBoss(id){
const b = BOSSES.find(x => x.id === id);
if(!b) return;
currentBoss = b; taskIdx = 0; hpLeft = b.hp;
document.getElementById('boss-arena').style.display = 'block';
document.getElementById('boss-name').textContent = b.name;
document.getElementById('boss-subname').textContent = b.sub;
document.getElementById('boss-emoji').textContent = b.icon;
showTask();
}
function showTask(){
const t = currentBoss.tasks[taskIdx];
if(!t){ winBoss(); return; }
document.getElementById('boss-hp').style.width = (hpLeft / currentBoss.hp * 100) + '%';
const tEl = document.getElementById('boss-task');
tEl.innerHTML = '<div style="font-size:.75rem;color:var(--muted);margin-bottom:6px">Задание ' + (taskIdx + 1) + ' / ' + currentBoss.tasks.length + '</div><div style="font-size:1.05rem">' + t.q + '</div>';
renderMath(tEl);
const opts = document.getElementById('boss-opts'); opts.innerHTML = '';
document.getElementById('boss-fb').style.display = 'none';
if(t.t === 'select'){
t.opts.forEach((o, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.innerHTML = o; b.style.cssText = 'text-align:left';
b.addEventListener('click', ()=>checkAnswer(k === t.ok, b));
opts.appendChild(b);
});
renderMath(opts);
} else if(t.t === 'yesno'){
['Да','Нет'].forEach((lab, k)=>{
const b = document.createElement('button');
b.className = 'btn'; b.textContent = lab;
b.addEventListener('click', ()=>checkAnswer((k === 0) === t.ok, b));
opts.appendChild(b);
});
} else if(t.t === 'input'){
const wrap = document.createElement('div');
wrap.style.cssText = 'display:flex;gap:8px;align-items:center';
const inp = document.createElement('input');
inp.type = 'text'; inp.placeholder = 'ваш ответ'; inp.className = 'tinp'; inp.style.flex = '1';
const go = document.createElement('button'); go.className = 'btn primary'; go.textContent = 'Ответ';
go.addEventListener('click', ()=>{
const u = parseFloat(inp.value.replace(',','.'));
const ok = t.ans.some(a => Math.abs(u - a) < 1e-6);
checkAnswer(ok, go);
});
inp.addEventListener('keyup', e=>{ if(e.key === 'Enter') go.click(); });
wrap.appendChild(inp); wrap.appendChild(go); opts.appendChild(wrap);
}
}
function checkAnswer(ok, btn){
const fb = document.getElementById('boss-fb'); fb.style.display = 'block';
if(ok){
if(btn) btn.classList.add('ok');
feedback(fb, true, '&#10003; Точное попадание!');
taskIdx++;
setTimeout(showTask, 700);
} else {
if(btn) btn.classList.add('fail');
feedback(fb, false, '&#10007; Промах! Попробуйте ещё.');
}
}
function winBoss(){
BOSS_STATE[currentBoss.id] = 1; saveBosses();
achievement('boss_' + currentBoss.id, 'Победил: ' + currentBoss.name);
bumpProgress('final3', 14); confetti();
document.getElementById('boss-task').innerHTML = '<div style="text-align:center;padding:20px"><div style="font-size:3rem;font-family:JetBrains Mono,monospace">' + currentBoss.icon + '</div><h3 style="color:var(--ok);margin:10px 0">Победа!</h3><p>Босс <b>«' + currentBoss.name + '»</b> повержен.</p></div>';
document.getElementById('boss-opts').innerHTML = '<button class="btn primary" id="boss-back">К списку боссов</button>';
document.getElementById('boss-fb').style.display = 'none';
document.getElementById('boss-back').addEventListener('click', closeBoss);
renderBossGrid();
if(BOSSES.every(b => BOSS_STATE[b.id])){
setTimeout(()=>{ achievement('all_bosses', 'Чемпион неравенств!'); confetti(); refreshCert(); }, 800);
}
}
function closeBoss(){ document.getElementById('boss-arena').style.display = 'none'; currentBoss = null; }
document.getElementById('boss-quit').addEventListener('click', closeBoss);
renderBossGrid();
/* PRACTICE */
(function(){
let cur = null, total = 0, score = 0, streak = 0;
function gen(){
const t = Math.floor(Math.random()*5);
if(t === 0){
// линейное
const a = (Math.random()<0.5?-1:1)*(1+Math.floor(Math.random()*4));
const b = -5+Math.floor(Math.random()*11);
const c = -5+Math.floor(Math.random()*11);
const v = (c - b) / a;
const flip = a < 0;
return { q:'Решите $' + a + 'x ' + (b>=0?'+ '+b:'- '+Math.abs(b)) + ' > ' + c + '$. Введите граничное значение $x$.', ans:[v] };
}
if(t === 1){
// оценка
const a = 1+Math.floor(Math.random()*3), b = a+1+Math.floor(Math.random()*3);
const c = 1+Math.floor(Math.random()*3), d = c+1+Math.floor(Math.random()*3);
return { q:'Дано: $' + a + ' \\leq x \\leq ' + b + '$, $' + c + ' \\leq y \\leq ' + d + '$. Верхняя граница $x + y$:', ans:[b + d] };
}
if(t === 2){
// система
const a = -5+Math.floor(Math.random()*5), b = a+2+Math.floor(Math.random()*5);
return { q:'$\\begin{cases} x > ' + a + ' \\\\ x \\leq ' + b + ' \\end{cases}$. Введите верхнюю границу.', ans:[b] };
}
if(t === 3){
// квадратное
const r = 1+Math.floor(Math.random()*5);
return { q:'Сколько корней у $x^2 - ' + (r*r) + ' = 0$?', ans:[2] };
}
// дробное
const r = (Math.random()<0.5?-1:1)*(1+Math.floor(Math.random()*8));
return { q:'Запрещённая точка для $\\dfrac{1}{x - ' + r + '}$:', ans:[r] };
}
function show(){
cur = gen();
document.getElementById('prac-task').innerHTML = cur.q;
renderMath(document.getElementById('prac-task'));
document.getElementById('prac-inp').value = '';
document.getElementById('prac-fb').style.display = 'none';
}
document.getElementById('prac-go').addEventListener('click', ()=>{
const fb = document.getElementById('prac-fb'); fb.style.display = 'block';
const u = parseFloat(document.getElementById('prac-inp').value.replace(',','.'));
const ok = cur.ans.some(a => Math.abs(u - a) < 1e-6);
total++;
if(ok){ score++; streak++; feedback(fb, true, '&#10003;'); if(streak === 5){ achievement('prac_streak', 'Серия из 5!'); confetti(); } }
else { streak = 0; feedback(fb, false, 'Правильно: ' + cur.ans.join(', ')); }
document.getElementById('prac-i').textContent = total;
document.getElementById('prac-score').textContent = score;
document.getElementById('prac-streak').textContent = streak;
if(total >= 5) bumpProgress('final3', 4);
});
document.getElementById('prac-next').addEventListener('click', show);
document.getElementById('prac-inp').addEventListener('keyup', e=>{ if(e.key === 'Enter') document.getElementById('prac-go').click(); });
show();
})();
function refreshCert(){
const won = BOSSES.filter(b => BOSS_STATE[b.id]).length;
const cs = document.getElementById('cert-state');
if(!cs) return;
if(won === BOSSES.length){
cs.innerHTML = '<div style="padding:18px;background:linear-gradient(135deg,#fef3c7,#ede9fe);border:2px solid #8b5cf6;border-radius:12px;text-align:center"><div style="font-family:Unbounded,sans-serif;font-size:1.2rem;font-weight:900;color:#6d28d9">ЧЕМПИОН НЕРАВЕНСТВ</div><div style="font-size:.9rem;color:#5b21b6;margin-top:4px">Все 7 боссов главы 3 повержены &mdash; вы освоили §§ 1318.</div></div>';
} else {
cs.innerHTML = 'Побеждено боссов: <b>' + won + ' / ' + BOSSES.length + '</b>. Победите всех, чтобы получить титул.';
}
}
refreshCert();
}
</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>