Files
Maxim Dolgolyov 9d5a2959e1 fix(textbooks): кнопка «Шпаргалка» не открывала контент на desktop
На десктопе (>980px) .col-side уже видна как sticky-колонка справа в grid 1fr 280px.
Клик по кнопке #sidebar-btn добавлял .col-side-backdrop.show — backdrop с
z-index:9990 затемнял всю страницу, перекрывая sticky-aside. Со стороны
выглядело как «ничего не открылось» — на самом деле появлялась чёрная вуаль.

Фикс: @media(min-width:981px) скрывает #sidebar-btn и подавляет показ backdrop.
На мобайле (≤980px) кнопка и overlay работают как раньше.

Применено в 51 файле: physics 8/9/10 chN, algebra 7/9/10/11 chN + 8 ch2-3,
geometry 7/8/9/11 chN, geometry_10 r1-4.
2026-05-30 09:51:04 +03:00

1601 lines
125 KiB
HTML
Raw Permalink 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>Алгебра 7 · Глава 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="stylesheet" href="/css/alg7-fx.css">
<script src="/js/alg7-fx.js" defer></script>
<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:#7c3aed; --pri2:#6d28d9; --pri-soft:#ede9fe;
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#f5f3ff;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0a14; --card:#13102a; --card-soft:#1a1430; --text:#f0e6ff; --ink:#f0e6ff; --muted:#9080b0; --border:#2a1f48}
*{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}
.hdr{position:relative;background:linear-gradient(110deg,#3b0764 0%,#7c3aed 55%,#a78bfa 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(196,181,253,.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(232,219,255,.12);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{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{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:'y=kx+b';position:absolute;right:-10px;top:-20px;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.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(124,58,237,.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(124,58,237,.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(124,58,237,.22);font-family:'Unbounded',sans-serif}
.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:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(124,58,237,.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,#fef3c7)}
.psel-card.final .psel-num{color:var(--warn)}
.sec[id="sec-p15"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p16"]{ --sec-acc:#9333ea; --sec-acc-d:#7e22ce; --sec-acc-soft:#f3e8ff; }
.sec[id="sec-p17"]{ --sec-acc:#c026d3; --sec-acc-d:#a21caf; --sec-acc-soft:#fae8ff; }
.sec[id="sec-p18"]{ --sec-acc:#db2777; --sec-acc-d:#9d174d; --sec-acc-soft:#fce7f3; }
.sec[id="sec-p19"]{ --sec-acc:#2563eb; --sec-acc-d:#1d4ed8; --sec-acc-soft:#dbeafe; }
.sec[id="sec-p20"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-final3"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.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.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
.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(124,58,237,.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(124,58,237,.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}
.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 .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}
.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}
.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}
.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))}
.tinp{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);transition:border-color .15s;font-family:'JetBrains Mono',monospace}
.tinp:focus{outline:0;border-color:var(--sec-acc,var(--pri));box-shadow:0 0 0 3px var(--sec-acc-soft,var(--pri-soft))}
.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)}
.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{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(167,139,250,.22);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{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:'\2212'}
.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}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,#7c3aed,#a78bfa);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(124,58,237,.45);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.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-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.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(124,58,237,.22)}
.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{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px}
.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-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid;transform:scale(1.015)}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.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 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))}
.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}
.hp-boss{height:14px;background:rgba(220,38,38,.12);border-radius:9px;overflow:hidden;border:1px solid #fecaca;margin:8px 0}
.hp-boss-fill{height:100%;background:linear-gradient(90deg,#dc2626,#f59e0b);border-radius:9px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
.col-side-backdrop.show{display:block}
@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
@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}
}
.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}
.gloss-tip b{color:var(--sec-acc-d,var(--pri2));font-size:.92rem}
.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}
.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: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}
.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}
.boss-card{padding:16px;background:var(--card);border-radius:12px;border:2px solid var(--bad,#dc2626);margin-bottom:14px}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px}
.boss-title{font-family:'Unbounded',sans-serif;font-weight:800;color:#7f1d1d;font-size:1.04rem;flex:1}
.boss-stage{font-size:.85rem;color:var(--muted)}
.boss-q{font-size:1rem;line-height:1.55;padding:11px 13px;background:var(--card-soft);border-radius:8px;margin-bottom:9px;border-left:3px solid var(--bad,#dc2626)}
.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}
.tbl th{background:var(--sec-acc-soft,var(--pri-soft));color:var(--sec-acc-d,var(--pri2));font-weight:700}
.coord{background:#fafafa;border:1px solid var(--border);border-radius:10px;display:block;max-width:100%;margin:0 auto}
.dark .coord{background:#0f172a}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Алгебра 7 · Глава 3</h1>
<div class="hdr-sub">Линейные уравнения · Неравенства · Линейная функция</div>
</div>
<div class="hdr-side">
<a href="/textbook/algebra-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К алгебре 7</a>
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
<button id="sidebar-btn" class="hdr-btn"><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"><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>В этой главе мы изучаем <b>линейные уравнения с одной переменной</b> и применяем их к текстовым задачам. Знакомимся со <b>свойствами числовых неравенств</b> и решаем <b>линейные неравенства</b>. Узнаём, что такое <b>функция</b> и как устроена <b>линейная функция</b> $y = kx + b$ — фундамент всей школьной алгебры.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p15')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 15</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" data-gamified></div>
</div>
</section>
<section class="psel">
<div class="psel-title">Параграфы главы</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
<section id="sec-p15" class="sec" data-watermark="ax=b"><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="&gt;&lt;"><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="ax&gt;b"><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-p19" class="sec" data-watermark="f(x)"><div class="sec-header"><span class="sec-num">§ 19</span><h2 class="sec-h">Функция</h2></div><div id="p19-body"></div></section>
<section id="sec-p20" class="sec" data-watermark="y=kx+b"><div class="sec-header"><span class="sec-num">§ 20</span><h2 class="sec-h">Линейная функция и её свойства</h2></div><div id="p20-body"></div></section>
<section id="sec-final3" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#7c3aed,#a78bfa)">Финал главы</span><h2 class="sec-h">Итоги. 5 боссов главы 3</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">Интерактивный учебник «Алгебра 7» · Глава 3 · Уравнения, неравенства, функция · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="gloss-tip" class="gloss-tip"></div>
<div id="search-modal" class="search-modal" role="dialog">
<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';
const STATE = { current:'p15', progress:{p15:0,p16:0,p17:0,p18:0,p19:0,p20:0,final3:0}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 7;
const _TB_SLUG = 'algebra-7-ch3';
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!',
p16_done:'Текстовые задачи покорены!',
p18_done:'Неравенства решаешь уверенно!',
p20_done:'Линейная функция — твой друг!',
ch3_done:'Глава 3 пройдена!',
};
function loadProgress(){
try{
const s=localStorage.getItem('algebra7_ch3_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('algebra7_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)); } }
STATE.xp=+(localStorage.getItem('algebra7_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('algebra7_ch3_progress', JSON.stringify(STATE.progress));
localStorage.setItem('algebra7_ch3_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('algebra7_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);
if(STATE.progress[key]>=100){
if(key==='p16') achievement('p16_done');
else if(key==='p18') achievement('p18_done');
else if(key==='p20') achievement('p20_done');
else if(key==='final3') achievement('ch3_done');
}
}
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,'algebra7-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)/TOTAL_PARAS);
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 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(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);
}
const PARAS = [
{ id:'p15', num:'§ 15', name:'Линейные уравнения', sub:'$ax = b$ — три случая' },
{ id:'p16', num:'§ 16', name:'Текстовые задачи', sub:'Составление уравнений' },
{ id:'p17', num:'§ 17', name:'Числовые неравенства', sub:'3 свойства + сложение/умножение' },
{ id:'p18', num:'§ 18', name:'Линейные неравенства', sub:'$ax > b$ и алгоритм' },
{ id:'p19', num:'§ 19', name:'Функция', sub:'Аргумент, $f(x)$, нули' },
{ id:'p20', num:'§ 20', name:'Линейная функция', sub:'$y = kx + b$' },
{ id:'final3', num:'★', name:'Финал главы', sub:'Итоги \xB7 5 боссов', 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 = { p15:()=>buildP15(), p16:()=>buildP16(), p17:()=>buildP17(), p18:()=>buildP18(), p19:()=>buildP19(), p20:()=>buildP20(), final3:()=>buildFinal3() };
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);
}
const SIDEBARS = {
p15:{title:'Шпаргалка \xA7 15',rows:[
['Уравнение','равенство с переменной'],
['Корень','значение переменной, при котором уравнение верно'],
['Линейное','вида $ax = b$'],
['$a \\ne 0$','единственный корень $x = b/a$'],
['$a = 0, b = 0$','любое число — корень'],
['$a = 0, b \\ne 0$','корней нет'],
]},
p16:{title:'Шпаргалка \xA7 16',rows:[
['Шаг 1','прочитай задачу, найди неизвестное'],
['Шаг 2','обозначь неизвестное буквой $x$'],
['Шаг 3','составь уравнение из условия'],
['Шаг 4','реши уравнение, проверь смысл'],
]},
p17:{title:'Шпаргалка \xA7 17',rows:[
['$a > b \\Leftrightarrow$','$a - b > 0$'],
['Свойство 1','если $a > b$ и $b > c$, то $a > c$'],
['Свойство 2','к обеим частям можно прибавить число'],
['Свойство 3','умножая на положительное — знак сохр., на отрицательное — меняется'],
['Сложение','$a > b$ и $c > d \\Rightarrow a + c > b + d$'],
]},
p18:{title:'Шпаргалка \xA7 18',rows:[
['Равносильные','имеют одинаковые решения'],
['Перенос','знак меняется'],
['Умножение на $>0$','знак неравенства сохраняется'],
['Умножение на $<0$','знак <b>меняется</b>'],
['$0 \\cdot x < 0$','корней нет'],
['$0 \\cdot x > -1$','любое число'],
]},
p19:{title:'Шпаргалка \xA7 19',rows:[
['Функция','каждому $x$ — единственное $y$'],
['Аргумент','независимая переменная $x$'],
['$D(f)$','область определения'],
['$E(f)$','множество значений'],
['Нуль функции','$x$, при котором $f(x) = 0$'],
['График','множество точек $(x; f(x))$'],
]},
p20:{title:'Шпаргалка \xA7 20',rows:[
['Линейная','$y = kx + b$'],
['$k$','угловой коэффициент'],
['$b$','ордината точки $(0; b)$'],
['$k > 0$','острый угол с осью $x$'],
['$k < 0$','тупой угол с осью $x$'],
['$k = 0$','прямая параллельна оси $x$'],
['$k_1 = k_2$','прямые параллельны'],
['$k_1 \\ne k_2$','прямые пересекаются'],
]},
final3:{title:'Финал главы',rows:[
['\xA715\xA720','теория главы 3'],
['Боссов','5 итоговых задач'],
['Награда','+100 XP за полное прохождение'],
]},
};
const TIPS=[
{sec:'p15',html:'Уравнение $ax = b$ имеет: <b>один</b> корень ($a \\ne 0$), <b>бесконечно</b> много корней ($a = b = 0$) или <b>не имеет</b> корней ($a = 0, b \\ne 0$).'},
{sec:'p16',html:'Чтобы составить уравнение, обозначь неизвестное буквой и «переведи» условие задачи на язык математики.'},
{sec:'p17',html:'При умножении неравенства на <b>отрицательное число</b> знак неравенства <b>меняется</b>!'},
{sec:'p18',html:'Когда после преобразований получается $0 \\cdot x < $ что-то — посмотри: верно неравенство $0 < $ что-то? Если да — любое число; если нет — корней нет.'},
{sec:'p19',html:'График — это <b>множество точек</b>. Точка $(x_0; y_0)$ принадлежит графику $y = f(x)$, если $y_0 = f(x_0)$.'},
{sec:'p20',html:'Чтобы построить $y = kx + b$, найди две точки: подставь два значения $x$. Через две точки проведи прямую.'},
{sec:'final3',html:'5 боссов проверяют все темы главы 3.'},
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS.p15;
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><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"><polygon points="12,2 22,20 2,20"/></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){}
}
function initTheme(){
const t=localStorage.getItem('algebra7_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('algebra7_ch3_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
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){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
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(6)).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>',
};
function makeCard(kind, title, num, body){
const labels={repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
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]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
function secNav(prev, next){
const NAMES={p15:'\xA715',p16:'\xA716',p17:'\xA717',p18:'\xA718',p19:'\xA719',p20:'\xA720',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;
}
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=['#7c3aed','#a78bfa','#c084fc','#f59e0b','#10b981'];
for(let i=0;i<80;i++){ _confettiParticles.push({x:window.innerWidth/2+(Math.random()-.5)*200,y:window.innerHeight/2,vx:(Math.random()-.5)*14,vy:-10-Math.random()*10,g:.4,life:100,color:colors[i%colors.length],r:4+Math.random()*4,rot:0,vRot:(Math.random()-.5)*.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();
}
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="Убрать">\xd7</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;
ev.preventDefault(); 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 GLOSSARY = [
{ term:'уравнение', def:'Равенство с переменной (буквой), для которой ищут все значения, при которых равенство верно.', sec:'p15', aliases:['уравнение','уравнения','уравнении','уравнений','уравнениями','уравнением'] },
{ term:'корень уравнения', def:'Значение переменной, при котором уравнение превращается в верное равенство.', sec:'p15', aliases:['корень уравнения','корни уравнения','корнем уравнения','корней уравнения'] },
{ term:'линейное уравнение', def:'Уравнение вида $ax = b$ с одной переменной.', sec:'p15', aliases:['линейное уравнение','линейного уравнения','линейные уравнения','линейному уравнению','линейным уравнением','линейных уравнений'] },
{ term:'равносильные уравнения', def:'Уравнения, имеющие одинаковые корни (или оба не имеющие корней).', sec:'p15', aliases:['равносильные уравнения','равносильных уравнений','равносильные'] },
{ term:'числовое неравенство', def:'Сравнение двух чисел с помощью знаков $>, <, \\ge, \\le$.', sec:'p17', aliases:['числовое неравенство','числовые неравенства','числового неравенства','числовых неравенств'] },
{ term:'линейное неравенство', def:'Неравенство вида $ax > b$ (или $\\ge, <, \\le$) с одной переменной.', sec:'p18', aliases:['линейное неравенство','линейные неравенства','линейного неравенства','линейных неравенств','линейным неравенством'] },
{ term:'равносильные неравенства', def:'Неравенства, имеющие одно и то же множество решений.', sec:'p18', aliases:['равносильные неравенства','равносильных неравенств'] },
{ term:'функция', def:'Зависимость, при которой каждому значению переменной $x$ соответствует ровно одно значение $y$.', sec:'p19', aliases:['функция','функции','функцию','функцией','функций'] },
{ term:'аргумент', def:'Независимая переменная функции (обычно обозначается $x$).', sec:'p19', aliases:['аргумент','аргумента','аргументе','аргументы','аргументов','аргументом'] },
{ term:'область определения', def:'Множество всех значений аргумента, при которых функция определена.', sec:'p19', aliases:['область определения','области определения','областью определения'] },
{ term:'множество значений', def:'Все значения, которые принимает функция при $x$ из области определения.', sec:'p19', aliases:['множество значений','множества значений','множестве значений'] },
{ term:'нуль функции', def:'Значение аргумента $x$, при котором $f(x) = 0$.', sec:'p19', aliases:['нуль функции','нули функции','нулём функции','нулей функции'] },
{ term:'график функции', def:'Множество всех точек $(x; f(x))$ координатной плоскости.', sec:'p19', aliases:['график функции','графика функции','графику функции','графики функций','графики'] },
{ term:'линейная функция', def:'Функция вида $y = kx + b$, где $k$ и $b$ — числа.', sec:'p20', aliases:['линейная функция','линейные функции','линейной функции','линейную функцию','линейных функций'] },
{ term:'угловой коэффициент', def:'Число $k$ в формуле $y = kx + b$. Определяет наклон прямой.', sec:'p20', 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\\u0400-\\u04ff-])('+allAliases.map(x=>x.a.replace(/[.*+?^${}()|[\]\\]/g,'\\$&')).join('|')+')(?![\\w\\u0400-\\u04ff-])', '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">См. \xA7 '+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();} });
}
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}));
[
['Формула','y = kx + b — линейная функция','\xA720','p20'],
['Формула','ax = b — линейное уравнение','\xA715','p15'],
['Формула','k > 0 → острый угол; k < 0 → тупой угол','\xA720','p20'],
['Формула','k1 = k2 → прямые параллельны','\xA720','p20'],
].forEach(([k,t,d,s])=>arr.push({kind:k,title:t,desc:d,sec:s}));
return arr;
})();
function initSearch(){
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),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); }
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(); } });
}
function initSidebarToggle(){
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),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(); });
}
function init(){
loadProgress(); initTheme(); initSidebarToggle(); initGlossaryTip(); initSearch();
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo('p15');
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);
function makeTrainer(opts){
let i=0, score=0;
const Q=opts.questions;
const parser = opts.parser || (v => parseFloat(String(v).replace(',','.')));
function show(){
if(i >= Q.length){
document.getElementById(opts.idPrefix+'-q').innerHTML = '<b>Готово!</b> Результат: '+score+' / '+Q.length;
if(opts.onComplete) opts.onComplete(score, Q.length);
return;
}
document.getElementById(opts.idPrefix+'-i').textContent = (i+1);
document.getElementById(opts.idPrefix+'-s').textContent = score;
document.getElementById(opts.idPrefix+'-q').innerHTML = Q[i].q;
document.getElementById(opts.idPrefix+'-ans').value = '';
renderMath(document.getElementById(opts.idPrefix+'-q'));
document.getElementById(opts.idPrefix+'-fb').style.display = 'none';
}
function go(){
if(i >= Q.length) return;
const fb = document.getElementById(opts.idPrefix+'-fb');
const raw = document.getElementById(opts.idPrefix+'-ans').value.trim();
if(raw === ''){ feedback(fb, false, '&#10007; Введи ответ.'); return; }
const expected = Q[i].a;
let ok = false;
if(typeof expected === 'function') ok = expected(raw);
else { const got = parser(raw); ok = !isNaN(got) && Math.abs(got - expected) < 1e-6; }
if(ok){ score++; feedback(fb, true, '&#10003; Верно! Дальше ▶'); }
else feedback(fb, false, '&#10007; Неверно. Правильно: <b>'+(Q[i].show||expected)+'</b>. Дальше ▶');
document.getElementById(opts.idPrefix+'-s').textContent = score;
i++; setTimeout(show, 1100);
}
document.getElementById(opts.idPrefix+'-go').addEventListener('click', go);
document.getElementById(opts.idPrefix+'-ans').addEventListener('keydown', e=>{ if(e.key==='Enter') go(); });
const restart = document.getElementById(opts.idPrefix+'-start');
if(restart) restart.addEventListener('click', ()=>{ i=0; score=0; show(); });
show();
}
function trainerHTML(idPrefix, total, placeholder){
return '<div class="score-display"><span>Задача <b id="'+idPrefix+'-i">1</b> / '+total+'</span><span>Очки: <b id="'+idPrefix+'-s">0</b> / '+total+'</span></div>'
+'<div id="'+idPrefix+'-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;margin-bottom:10px;text-align:center"></div>'
+'<div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;justify-content:center">'
+'<input type="text" id="'+idPrefix+'-ans" class="tinp" placeholder="'+(placeholder||'Ответ')+'" style="width:140px;text-align:center">'
+'<button class="btn primary" id="'+idPrefix+'-go">Проверить</button>'
+'<button class="btn" id="'+idPrefix+'-start">Заново</button>'
+'</div><div class="feedback" id="'+idPrefix+'-fb"></div>';
}
function readButton(paraId){
return '<div style="margin-top:18px;display:flex;justify-content:center">'
+'<button class="btn primary" id="'+paraId+'-read-btn">'
+'<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>'
+' Я прочитал \xA7'+paraId.replace('p','')+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
document.getElementById(paraId+'-read-btn').addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
const b=document.getElementById(paraId+'-read-btn'); b.textContent='Прочитано! +10 XP'; b.disabled=true; b.style.opacity=.6;
});
}
/* Coordinate plane SVG helper for §19-§20 */
function coordSVG(opts){
/* opts: { width=300, height=260, xMin=-5, xMax=5, yMin=-5, yMax=5, lines=[{k,b,color}], points=[{x,y,label,color}] } */
const w=opts.width||300, h=opts.height||260;
const xMin=opts.xMin||-5, xMax=opts.xMax||5, yMin=opts.yMin||-5, yMax=opts.yMax||5;
const padL=24, padR=14, padT=14, padB=24;
const W=w-padL-padR, H=h-padT-padB;
const sx = x => padL + (x - xMin) / (xMax - xMin) * W;
const sy = y => padT + (yMax - y) / (yMax - yMin) * H;
let svg = '<svg class="coord" viewBox="0 0 '+w+' '+h+'">';
/* grid */
for(let i=xMin;i<=xMax;i++){
svg += '<line x1="'+sx(i)+'" y1="'+padT+'" x2="'+sx(i)+'" y2="'+(padT+H)+'" stroke="#e2e8f0" stroke-width="0.6"/>';
}
for(let i=yMin;i<=yMax;i++){
svg += '<line x1="'+padL+'" y1="'+sy(i)+'" x2="'+(padL+W)+'" y2="'+sy(i)+'" stroke="#e2e8f0" stroke-width="0.6"/>';
}
/* axes */
svg += '<line x1="'+padL+'" y1="'+sy(0)+'" x2="'+(padL+W)+'" y2="'+sy(0)+'" stroke="#64748b" stroke-width="1.5"/>';
svg += '<line x1="'+sx(0)+'" y1="'+padT+'" x2="'+sx(0)+'" y2="'+(padT+H)+'" stroke="#64748b" stroke-width="1.5"/>';
/* axis arrows */
svg += '<polyline points="'+(padL+W-6)+','+(sy(0)-4)+' '+(padL+W)+','+sy(0)+' '+(padL+W-6)+','+(sy(0)+4)+'" stroke="#64748b" stroke-width="1.5" fill="none"/>';
svg += '<polyline points="'+(sx(0)-4)+','+(padT+6)+' '+sx(0)+','+padT+' '+(sx(0)+4)+','+(padT+6)+'" stroke="#64748b" stroke-width="1.5" fill="none"/>';
/* labels */
svg += '<text x="'+(padL+W-4)+'" y="'+(sy(0)+14)+'" font-size="10" fill="#64748b" font-family="JetBrains Mono,monospace">x</text>';
svg += '<text x="'+(sx(0)+6)+'" y="'+(padT+8)+'" font-size="10" fill="#64748b" font-family="JetBrains Mono,monospace">y</text>';
/* tick numbers on x */
for(let i=xMin;i<=xMax;i++){ if(i===0) continue; svg += '<text x="'+sx(i)+'" y="'+(sy(0)+13)+'" font-size="8" fill="#64748b" font-family="JetBrains Mono,monospace" text-anchor="middle">'+i+'</text>'; }
for(let i=yMin;i<=yMax;i++){ if(i===0) continue; svg += '<text x="'+(sx(0)-5)+'" y="'+(sy(i)+3)+'" font-size="8" fill="#64748b" font-family="JetBrains Mono,monospace" text-anchor="end">'+i+'</text>'; }
/* O */
svg += '<text x="'+(sx(0)-5)+'" y="'+(sy(0)+13)+'" font-size="9" fill="#64748b" font-family="JetBrains Mono,monospace" text-anchor="end">O</text>';
/* lines */
(opts.lines||[]).forEach(L=>{
const x1=xMin, y1=L.k*x1+L.b, x2=xMax, y2=L.k*x2+L.b;
/* clip to box */
function clip(x1,y1,x2,y2){
const pts = [];
[yMax,yMin].forEach(yEdge=>{
if((y1-yEdge)*(y2-yEdge)<=0 && (y1!==y2)){
const t=(yEdge-y1)/(y2-y1);
const x=x1+t*(x2-x1);
if(x>=xMin-.01 && x<=xMax+.01) pts.push([x,yEdge]);
}
});
[xMax,xMin].forEach(xEdge=>{
if((x1-xEdge)*(x2-xEdge)<=0 && (x1!==x2)){
const t=(xEdge-x1)/(x2-x1);
const y=y1+t*(y2-y1);
if(y>=yMin-.01 && y<=yMax+.01) pts.push([xEdge,y]);
}
});
return pts.slice(0,2);
}
const pts=clip(x1,y1,x2,y2);
if(pts.length>=2){
svg += '<line x1="'+sx(pts[0][0])+'" y1="'+sy(pts[0][1])+'" x2="'+sx(pts[1][0])+'" y2="'+sy(pts[1][1])+'" stroke="'+(L.color||'#7c3aed')+'" stroke-width="2.4"/>';
}
/* y-intercept dot */
if(L.b>=yMin && L.b<=yMax){
svg += '<circle cx="'+sx(0)+'" cy="'+sy(L.b)+'" r="3.5" fill="'+(L.color||'#7c3aed')+'" stroke="#fff" stroke-width="1.5"/>';
}
});
/* points */
(opts.points||[]).forEach(p=>{
svg += '<circle cx="'+sx(p.x)+'" cy="'+sy(p.y)+'" r="4" fill="'+(p.color||'#ef4444')+'" stroke="#fff" stroke-width="1.5"/>';
if(p.label) svg += '<text x="'+(sx(p.x)+6)+'" y="'+(sy(p.y)-6)+'" font-size="9" fill="'+(p.color||'#ef4444')+'" font-family="JetBrains Mono,monospace" font-weight="700">'+p.label+'</text>';
});
svg += '</svg>';
return svg;
}
/* BUILDERS stubs filled below */
function buildP15(){
const box = document.getElementById('p15-body');
let html = '';
html += makeCard('theory', 'Уравнение. Корень уравнения', '15.1', `
<p><b>Уравнением</b> с одной переменной называется равенство, содержащее эту переменную.</p>
<p>Например: $-3x = 1$, $\\;2x - 7 = -13$, $\\;5(x - 2) + 1 = 4x$.</p>
<p><b>Корнем</b> уравнения называется значение переменной, при котором уравнение превращается в верное равенство.</p>
<p><b>Решить уравнение</b> — значит найти все его корни или доказать, что их нет.</p>
<details class="spoiler"><summary>Пример проверки</summary><div class="spoiler-body">
Является ли $-3$ корнем $2x - 7 = -13$? Подставим: $2 \\cdot (-3) - 7 = -13$, т. е. $-13 = -13$ — верно. Да, $-3$ — корень.
</div></details>`);
html += makeCard('rule', 'Равносильные уравнения', '15.2', `
<p>Два уравнения называются <b>равносильными</b>, если они имеют одно и то же множество корней (или оба не имеют корней).</p>
<p>Свойства равносильных преобразований:</p>
<ol style="padding-left:22px;line-height:1.85">
<li>к обеим частям можно прибавить (или вычесть) одно и то же число;</li>
<li>обе части можно умножить (или разделить) на одно и то же число, <b>не равное нулю</b>;</li>
<li>можно <b>переносить</b> члены из одной части в другую, меняя знак.</li>
</ol>`);
html += makeCard('theory', 'Линейное уравнение $ax = b$', '15.3', `
<p><b>Линейным уравнением</b> с одной переменной называется уравнение вида $ax = b$, где $a$ и $b$ — числа.</p>
<p>Возможны <b>три случая</b>:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>$a \\ne 0$ — <b>единственный корень</b>: $x = \\dfrac{b}{a}$.</li>
<li>$a = 0$ и $b = 0$ — <b>любое число</b> является корнем (бесконечно много корней).</li>
<li>$a = 0$ и $b \\ne 0$ — <b>корней нет</b>.</li>
</ul>`);
html += makeCard('algo', 'Алгоритм решения линейного уравнения', '15.4', `
<ol style="padding-left:22px;line-height:1.9">
<li>раскрыть скобки;</li>
<li>перенести члены с переменной в левую часть, числа — в правую (с заменой знака);</li>
<li>привести подобные слагаемые;</li>
<li>получить $ax = b$;</li>
<li>разделить обе части на $a$ (если $a \\ne 0$).</li>
</ol>
<p><b>Пример:</b> $5(x - 2) + 1 = 4x \\;\\Rightarrow\\; 5x - 10 + 1 = 4x \\;\\Rightarrow\\; x = 9$.</p>`);
html += '<div class="wg" id="p15-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Сколько корней?</div></div>'
+'<div class="wg-help">Для каждого уравнения определи: один корень / нет корней / любое число.</div>'
+'<div class="score-display"><span>Задача <b id="p15-iv1-i">1</b> / 7</span><span>Очки: <b id="p15-iv1-s">0</b> / 7</span></div>'
+'<div id="p15-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.15rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">'
+'<button class="btn primary" id="p15-iv1-one" style="background:#10b981;border-color:#10b981">Один корень</button>'
+'<button class="btn primary" id="p15-iv1-none" style="background:#dc2626;border-color:#dc2626">Корней нет</button>'
+'<button class="btn primary" id="p15-iv1-any" style="background:#7c3aed;border-color:#7c3aed">Любое число</button>'
+'</div><div class="feedback" id="p15-iv1-fb"></div></div>';
html += '<div class="wg" id="p15-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Тренажёр: реши уравнение</div></div>'
+'<div class="wg-help">Реши линейное уравнение и введи корень (число — десятичная дробь через запятую или точку).</div>'
+trainerHTML('p15-iv2', 6, 'корень $x$')
+'</div>';
html += '<div class="wg" id="p15-iv3">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Уравнения со скобками</div></div>'
+'<div class="wg-help">Раскрой скобки, приведи подобные, реши.</div>'
+trainerHTML('p15-iv3', 5, 'корень $x$')
+'</div>';
html += secNav(null, 'p16') + readButton('p15');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$0 \\cdot x = 12$', ans:'none' },
{ e:'$3x = 0$', ans:'one' },
{ e:'$0 \\cdot x = 0$', ans:'any' },
{ e:'$-5x = 35$', ans:'one' },
{ e:'$0{,}1 \\cdot x = 9$', ans:'one' },
{ e:'$0 \\cdot x = \\dfrac{1}{3}$', ans:'none' },
{ e:'$5(x-2)+1 = 4x$', ans:'one' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p15-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p15-iv1');bumpProgress('p15',25);} else if(score>=5){addXp(7,'p15-iv1');bumpProgress('p15',15);} return; }
document.getElementById('p15-iv1-i').textContent=(i+1);
document.getElementById('p15-iv1-s').textContent=score;
document.getElementById('p15-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p15-iv1-q'));
document.getElementById('p15-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p15-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={one:'один корень',none:'корней нет',any:'любое число'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>.'); }
document.getElementById('p15-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p15-iv1-one').addEventListener('click',()=>ans('one'));
document.getElementById('p15-iv1-none').addEventListener('click',()=>ans('none'));
document.getElementById('p15-iv1-any').addEventListener('click',()=>ans('any'));
show();
})();
makeTrainer({
idPrefix:'p15-iv2',
questions:[
{ q:'$-5x = 45$', a:-9 },
{ q:'$24x = 8$', a:1/3, show:'$1/3 \\approx 0{,}333$' },
{ q:'$-x = 2{,}8$', a:-2.8 },
{ q:'$0{,}5x = -9$', a:-18 },
{ q:'$\\tfrac{x}{7} = 5$', a:35 },
{ q:'$3{,}5x = 7$', a:2 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p15-iv2');bumpProgress('p15',25);} else if(s>=4){addXp(8,'p15-iv2');bumpProgress('p15',15);} }
});
makeTrainer({
idPrefix:'p15-iv3',
questions:[
{ q:'$3x - (x - 14) = 5$', a:-4.5, show:'$-4{,}5$' },
{ q:'$18 - (6x + 5) = 4 - 7x$', a:-9 },
{ q:'$4x + 5 = 6 + 5(x - 3)$', a:14 },
{ q:'$2(x - 1) - 4 = 6(x + 2)$', a:-4.5, show:'$-4{,}5$' },
{ q:'$3(x - 2) - 5(x + 1) = -8x$', a:11/6, show:'$11/6$' },
],
onComplete:(s,n)=>{ if(s===n){addXp(18,'p15-iv3');bumpProgress('p15',30);} else if(s>=3){addXp(8,'p15-iv3');bumpProgress('p15',15);} }
});
wireReadBtn('p15');
}
function buildP16(){
const box = document.getElementById('p16-body');
let html = '';
html += makeCard('algo', 'Решение текстовой задачи', '16.1', `
<p>Чтобы решить задачу с помощью уравнения, нужно:</p>
<ol style="padding-left:22px;line-height:1.85">
<li><b>прочитать</b> условие и понять, <i>что</i> искать;</li>
<li><b>обозначить</b> неизвестное буквой $x$ (или другой);</li>
<li>выразить остальные величины через $x$;</li>
<li>составить <b>уравнение</b> по условию задачи;</li>
<li>решить уравнение;</li>
<li>проверить смысл ответа (например, нельзя «отрицательное число тетрадей»);</li>
<li>записать ответ.</li>
</ol>`);
html += makeCard('example', 'Задача про возраст', '16.2', `
<p><b>Условие:</b> Сейчас отцу 36 лет, а сыну 10. Через сколько лет отец будет старше сына в 2 раза?</p>
<p><b>Решение:</b> пусть это произойдёт через $x$ лет. Тогда отцу будет $36 + x$ лет, сыну — $10 + x$. По условию $36 + x = 2(10 + x)$.</p>
<p>$36 + x = 20 + 2x \\;\\Rightarrow\\; -x = -16 \\;\\Rightarrow\\; x = 16$.</p>
<p><b>Ответ:</b> через 16 лет.</p>`);
html += makeCard('example', 'Задача про движение', '16.3', `
<p><b>Условие:</b> Из двух городов навстречу друг другу выехали два велосипедиста. Скорости — 12 и 15 км/ч. Расстояние — 81 км. Через сколько часов они встретятся?</p>
<p><b>Решение:</b> пусть встреча через $x$ ч. Первый проехал $12x$ км, второй — $15x$ км. Сумма путей равна расстоянию:</p>
<p>$12x + 15x = 81 \\;\\Rightarrow\\; 27x = 81 \\;\\Rightarrow\\; x = 3$.</p>
<p><b>Ответ:</b> через 3 часа.</p>`);
html += makeCard('example', 'Задача про покупку', '16.4', `
<p><b>Условие:</b> Купили 7 тетрадей и 5 ручек, заплатили 110 коп. Тетрадь стоит на 5 коп. дороже ручки. Сколько стоит каждая?</p>
<p><b>Решение:</b> пусть ручка стоит $x$ коп. Тогда тетрадь — $(x+5)$ коп. По условию:</p>
<p>$7(x+5) + 5x = 110 \\;\\Rightarrow\\; 7x + 35 + 5x = 110 \\;\\Rightarrow\\; 12x = 75 \\;\\Rightarrow\\; x = 6{,}25$.</p>
<p><b>Ответ:</b> ручка — 6,25 к., тетрадь — 11,25 к.</p>`);
html += '<div class="wg" id="p16-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Тренажёр текстовых задач</div></div>'
+'<div class="wg-help">Составь уравнение в уме и реши задачу. Введи только число.</div>'
+trainerHTML('p16-iv1', 6, 'ответ')
+'</div>';
html += secNav('p15', 'p17') + readButton('p16');
box.innerHTML = html; renderMath(box);
makeTrainer({
idPrefix:'p16-iv1',
questions:[
{ q:'Сумма двух чисел 50. Одно в 4 раза больше другого. Найди меньшее.', a:10 },
{ q:'Из города A и B выехали навстречу друг другу два автомобиля. Скорости 60 и 80 км/ч. Расстояние 280 км. Через сколько часов встретятся?', a:2 },
{ q:'У Маши в 3 раза больше марок, чем у Пети. Всего у них 48 марок. Сколько у Пети?', a:12 },
{ q:'Велосипедист ехал 2 ч со скоростью 12 км/ч и ещё $x$ ч со скоростью 18 км/ч. Всего проехал 78 км. Сколько часов ехал с большей скоростью?', a:3 },
{ q:'Прямоугольник: длина на 4 см больше ширины, периметр 48 см. Найди ширину (см).', a:10 },
{ q:'Купили 10 ручек по $a$ коп. и 5 тетрадей по 8 коп. Всего на 90 коп. Сколько стоит ручка?', a:5 },
],
onComplete:(s,n)=>{ if(s===n){addXp(18,'p16-iv1');bumpProgress('p16',40);} else if(s>=4){addXp(10,'p16-iv1');bumpProgress('p16',20);} }
});
wireReadBtn('p16');
}
function buildP17(){
const box = document.getElementById('p17-body');
let html = '';
html += makeCard('theory', 'Сравнение двух чисел', '17.1', `
<p>Число $a$ <b>больше</b> числа $b$, если разность $a - b$ — положительное число:</p>
\\[a > b \\;\\Leftrightarrow\\; a - b > 0\\]
<p>Аналогично: $a < b \\Leftrightarrow a - b < 0$; $a = b \\Leftrightarrow a - b = 0$.</p>
<p>Знаки $\\le$ и $\\ge$ — <b>нестрогие</b> неравенства (могут быть равны). Знаки $<$ и $>$ — <b>строгие</b>.</p>`);
html += makeCard('rule', 'Три основных свойства', '17.2', `
<p><b>Свойство 1 (транзитивность):</b> если $a > b$ и $b > c$, то $a > c$.</p>
<p><b>Свойство 2 (прибавление числа):</b> к обеим частям неравенства можно прибавить (вычесть) одно и то же число — знак сохраняется.</p>
<p>Из $a > b$ следует $a + c > b + c$.</p>
<p><b>Свойство 3 (умножение на число):</b></p>
<ul style="padding-left:22px;line-height:1.85">
<li>умножая на <b>положительное</b> число, знак сохраняется: $a > b \\Rightarrow ac > bc$ при $c > 0$;</li>
<li>умножая на <b>отрицательное</b> — знак <b>меняется на противоположный</b>: $a > b \\Rightarrow ac < bc$ при $c < 0$.</li>
</ul>`);
html += makeCard('rule', 'Сложение и умножение неравенств', '17.3', `
<p><b>Сложение неравенств одного знака:</b> $\\;a > b,\\;\\; c > d \\;\\Rightarrow\\; a + c > b + d$.</p>
<p><b>Умножение неравенств с положительными частями:</b> $\\;a > b > 0,\\;\\; c > d > 0 \\;\\Rightarrow\\; ac > bd$.</p>
<p>Этим пользуются при <b>оценке</b> выражений.</p>
<p>Например, если $3 < a < 5$ и $2 < b < 4$, то $3 + 2 < a + b < 5 + 4$, т. е. $5 < a + b < 9$.</p>`);
html += '<div class="wg" id="p17-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Изменится ли знак?</div></div>'
+'<div class="wg-help">Для каждого действия определи: остаётся ли знак неравенства прежним.</div>'
+'<div class="score-display"><span>Задача <b id="p17-iv1-i">1</b> / 7</span><span>Очки: <b id="p17-iv1-s">0</b> / 7</span></div>'
+'<div id="p17-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:10px;justify-content:center">'
+'<button class="btn primary" id="p17-iv1-yes" style="background:#10b981;border-color:#10b981">Знак сохраняется</button>'
+'<button class="btn primary" id="p17-iv1-no" style="background:#dc2626;border-color:#dc2626">Знак меняется</button>'
+'</div><div class="feedback" id="p17-iv1-fb"></div></div>';
html += '<div class="wg" id="p17-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Оценка выражений</div></div>'
+'<div class="wg-help">По данным двойным неравенствам найди границы. Введи через запятую: <b>нижняя, верхняя</b>.</div>'
+trainerHTML('p17-iv2', 5, 'низ, верх')
+'</div>';
html += secNav('p16', 'p18') + readButton('p17');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'Из $a > b$ прибавили к обеим частям $3$', keep:true },
{ e:'Из $a > b$ умножили обе части на $-5$', keep:false },
{ e:'Из $a < b$ умножили обе части на $0{,}1$', keep:true },
{ e:'Из $-7 < -2$ разделили обе части на $-1$', keep:false },
{ e:'Из $a \\ge b$ вычли $-7$', keep:true },
{ e:'Из $a > b$ умножили обе части на $-\\dfrac{1}{2}$', keep:false },
{ e:'Из $-3 < 5$ умножили обе части на $4$', keep:true },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p17-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p17-iv1');bumpProgress('p17',25);} else if(score>=5){addXp(7,'p17-iv1');bumpProgress('p17',15);} return; }
document.getElementById('p17-iv1-i').textContent=(i+1);
document.getElementById('p17-iv1-s').textContent=score;
document.getElementById('p17-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p17-iv1-q'));
document.getElementById('p17-iv1-fb').style.display='none';
}
function ans(isYes){
if(i>=Q.length) return;
const fb=document.getElementById('p17-iv1-fb');
if(isYes===Q[i].keep){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; Правильно: знак '+(Q[i].keep?'<b>сохраняется</b>':'<b>меняется</b>'));
document.getElementById('p17-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p17-iv1-yes').addEventListener('click',()=>ans(true));
document.getElementById('p17-iv1-no').addEventListener('click',()=>ans(false));
show();
})();
function parsePair(v){ return String(v).trim().replace(',','.').split(/[,;\s]+/).filter(Boolean); }
makeTrainer({
idPrefix:'p17-iv2',
parser:(v)=>v,
questions:[
{ q:'Если $3 < a < 5$ и $2 < b < 4$, то $a + b \\in (?, ?)$', a:(v)=>{const m=parsePair(v); return +m[0]===5 && +m[1]===9;}, show:'5, 9' },
{ q:'Если $5 < a < 9$, то $2a \\in (?, ?)$', a:(v)=>{const m=parsePair(v); return +m[0]===10 && +m[1]===18;}, show:'10, 18' },
{ q:'Если $-3 \\le b < 8$, то $b + 2 \\in [?, ?)$ (введи $b+2$ снизу/сверху)', a:(v)=>{const m=parsePair(v); return +m[0]===-1 && +m[1]===10;}, show:'-1, 10' },
{ q:'Если $3{,}9 < a < 5$ и $2 < b < 3$, то $ab \\in (?, ?)$', a:(v)=>{const m=parsePair(v); return Math.abs(+m[0]-7.8)<1e-3 && +m[1]===15;}, show:'7,8 и 15' },
{ q:'Если $5 < a < 9$, то $-3a \\in (?, ?)$', a:(v)=>{const m=parsePair(v); return +m[0]===-27 && +m[1]===-15;}, show:'-27, -15' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p17-iv2');bumpProgress('p17',25);} else if(s>=3){addXp(8,'p17-iv2');bumpProgress('p17',15);} }
});
wireReadBtn('p17');
}
function buildP18(){
const box = document.getElementById('p18-body');
let html = '';
html += makeCard('theory', 'Линейное неравенство', '18.1', `
<p><b>Линейным неравенством</b> с одной переменной называется неравенство вида $ax > b$ (а также $ax < b$, $ax \\ge b$, $ax \\le b$).</p>
<p><b>Решением</b> неравенства называется значение переменной, при котором неравенство верно.</p>
<p>Например, для $x > 5$ решениями являются 6, 7, 100 и т. д.; для $x < -3$ — все числа меньше $-3$.</p>`);
html += makeCard('rule', 'Равносильные преобразования', '18.2', `
<p>Два неравенства называются <b>равносильными</b>, если они имеют одно и то же множество решений.</p>
<p>Равносильные преобразования (как у уравнений + одно важное!):</p>
<ol style="padding-left:22px;line-height:1.85">
<li>прибавить (вычесть) одно и то же число — знак сохраняется;</li>
<li>умножить (разделить) на <b>положительное</b> число — знак сохраняется;</li>
<li>умножить (разделить) на <b>отрицательное</b> — знак <b>меняется на противоположный</b>!</li>
</ol>
<p><b>Пример:</b> $-2x > 6 \\;\\Rightarrow\\; x < -3$ (поделили на $-2$, знак $>$ стал $<$).</p>`);
html += makeCard('algo', 'Алгоритм решения', '18.3', `
<ol style="padding-left:22px;line-height:1.85">
<li>раскрыть скобки;</li>
<li>перенести члены с переменной — налево, числа — направо;</li>
<li>привести подобные слагаемые;</li>
<li>разделить обе части на коэффициент при переменной (знак неравенства может измениться!).</li>
</ol>
<p><b>Особые случаи:</b></p>
<ul style="padding-left:22px;line-height:1.85">
<li>$0 \\cdot x < 3$ — верно при <b>любом</b> $x$ (получили $0 < 3$);</li>
<li>$0 \\cdot x > 5$ — <b>решений нет</b> ($0 > 5$ — ложь);</li>
<li>$0 \\cdot x \\ge 0$ — любое $x$;</li>
<li>$0 \\cdot x < -1$ — решений нет.</li>
</ul>`);
html += makeCard('example', 'Применение', '18.4', `
<p><b>а)</b> $7x < 21 \\;\\Rightarrow\\; x < 3$.</p>
<p><b>б)</b> $-4x \\ge 16 \\;\\Rightarrow\\; x \\le -4$ (поделили на $-4$, знак сменился).</p>
<p><b>в)</b> $3(x + 2) > 4 - x \\;\\Rightarrow\\; 3x + 6 > 4 - x \\;\\Rightarrow\\; 4x > -2 \\;\\Rightarrow\\; x > -0{,}5$.</p>`);
html += '<div class="wg" id="p18-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Реши неравенство $ax > b$</div></div>'
+'<div class="wg-help">Введи правую часть после деления на коэффициент. Запиши знак отдельно после числа: например, для $x > 3$ — введи <code>3</code>, выбрав знак ниже.</div>'
+trainerHTML('p18-iv1', 5, 'число')
+'</div>';
html += '<div class="wg" id="p18-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Со скобками</div></div>'
+'<div class="wg-help">Раскрой, упрости, реши. Введи число (правую часть после деления). Знак можно проверить сразу.</div>'
+trainerHTML('p18-iv2', 5, 'число')
+'</div>';
html += '<div class="wg" id="p18-iv3">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Особые случаи</div></div>'
+'<div class="wg-help">Реши неравенство. Если решений нет — введи <code>нет</code>. Если решением является любое число — введи <code>любое</code>.</div>'
+trainerHTML('p18-iv3', 4, 'число / «нет» / «любое»')
+'</div>';
html += secNav('p17', 'p19') + readButton('p18');
box.innerHTML = html; renderMath(box);
makeTrainer({
idPrefix:'p18-iv1',
questions:[
{ q:'$7x < 21$. Введи число справа (получится $x < ?$)', a:3 },
{ q:'$-4x \\ge 16$. После деления на $-4$ получится $x \\le ?$', a:-4 },
{ q:'$2x \\le -9$. $x \\le ?$', a:-4.5, show:'$-4{,}5$' },
{ q:'$-5x > -12$. $x < ?$', a:2.4, show:'$2{,}4$' },
{ q:'$0{,}5x < 6$. $x < ?$', a:12 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p18-iv1');bumpProgress('p18',28);} else if(s>=3){addXp(7,'p18-iv1');bumpProgress('p18',15);} }
});
makeTrainer({
idPrefix:'p18-iv2',
questions:[
{ q:'$3(x + 2) > 4 - x$. $x > ?$', a:-0.5, show:'$-0{,}5$' },
{ q:'$4x - 11 < 2x + 13$. $x < ?$', a:12 },
{ q:'$2x - 3 > 1$. $x > ?$', a:2 },
{ q:'$1 - 5x < 6$. $x > ?$', a:-1 },
{ q:'$3 - 8x \\le 1$. $x \\ge ?$', a:0.25, show:'$0{,}25$' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p18-iv2');bumpProgress('p18',28);} else if(s>=3){addXp(7,'p18-iv2');bumpProgress('p18',15);} }
});
makeTrainer({
idPrefix:'p18-iv3',
parser:(v)=>{ const t=String(v).trim().toLowerCase(); if(t==='нет'||t==='no') return 'нет'; if(t==='любое'||t==='all'||t==='any') return 'любое'; return parseFloat(t.replace(',','.')); },
questions:[
{ q:'$0 \\cdot x > 5$', a:(v)=>String(v).trim().toLowerCase()==='нет', show:'нет решений' },
{ q:'$0 \\cdot x < 3$', a:(v)=>String(v).trim().toLowerCase()==='любое', show:'любое число' },
{ q:'$0 \\cdot x \\ge -2$', a:(v)=>String(v).trim().toLowerCase()==='любое', show:'любое число' },
{ q:'$0 \\cdot x \\le -7$', a:(v)=>String(v).trim().toLowerCase()==='нет', show:'нет решений' },
],
onComplete:(s,n)=>{ if(s===n){addXp(12,'p18-iv3');bumpProgress('p18',22);} else if(s>=2){addXp(5,'p18-iv3');bumpProgress('p18',10);} }
});
wireReadBtn('p18');
}
function buildP19(){
const box = document.getElementById('p19-body');
let html = '';
html += makeCard('theory', 'Функция', '19.1', `
<p><b>Функцией</b> называется такая зависимость между двумя переменными, при которой каждому значению одной переменной соответствует <b>ровно одно</b> значение другой.</p>
<p>Независимую переменную называют <b>аргументом</b> (обычно $x$), зависимую — <b>значением функции</b> (обычно $y$ или $f(x)$).</p>
<p>Если каждому $x$ соответствует <b>несколько</b> или <b>ноль</b> значений $y$ — это <b>не функция</b>.</p>
<p><b>Пример:</b> зависимость стоимости покупки от количества тетрадей — функция (каждому числу — одна цена). Зависимость роста учеников от их оценки — <b>не</b> функция (одну и ту же оценку могут получить ученики разного роста).</p>`);
html += makeCard('rule', 'Область определения и множество значений', '19.2', `
<p><b>Область определения</b> $D(f)$ — все значения аргумента, при которых функция определена (имеет смысл).</p>
<p><b>Множество значений</b> $E(f)$ — все значения, которые принимает функция.</p>
<p>Например, для $y = x^2 + 5$: $D(f)$ — все числа, $E(f)$: $y \\ge 5$ (так как $x^2 \\ge 0$).</p>
<p>Способы задания функции: <b>формулой</b>, <b>таблицей</b>, <b>графиком</b>, <b>словесно</b>.</p>`);
html += makeCard('rule', 'Нули функции', '19.3', `
<p><b>Нулями функции</b> называются те значения аргумента $x$, при которых $f(x) = 0$.</p>
<p>На графике это точки пересечения с осью $Ox$.</p>
<p><b>Пример:</b> найти нули $f(x) = 2x - 2{,}8$. Решаем $2x - 2{,}8 = 0$, получаем $x = 1{,}4$. Это нуль функции.</p>`);
html += makeCard('theory', 'График функции', '19.4', `
<p><b>Графиком функции</b> называется множество всех точек координатной плоскости, абсциссы которых равны значениям аргумента, а ординаты — соответствующим значениям функции.</p>
<p>По графику можно определить:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>значение функции $f(x_0)$ — по точке с абсциссой $x_0$;</li>
<li>нули функции — точки пересечения с осью $Ox$;</li>
<li>где $f(x) > 0$ — график выше оси $Ox$;</li>
<li>где $f(x) < 0$ — график ниже.</li>
</ul>`);
html += '<div class="wg" id="p19-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Функция или не функция?</div></div>'
+'<div class="wg-help">Является ли зависимость функцией? Да — каждому $x$ ровно одно $y$.</div>'
+'<div class="score-display"><span>Задача <b id="p19-iv1-i">1</b> / 6</span><span>Очки: <b id="p19-iv1-s">0</b> / 6</span></div>'
+'<div id="p19-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:10px;justify-content:center"><button class="btn primary" id="p19-iv1-yes" style="background:#10b981;border-color:#10b981">Функция</button><button class="btn primary" id="p19-iv1-no" style="background:#dc2626;border-color:#dc2626">Не функция</button></div>'
+'<div class="feedback" id="p19-iv1-fb"></div></div>';
html += '<div class="wg" id="p19-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Найди значение $f(x_0)$</div></div>'
+'<div class="wg-help">Подставь $x_0$ в формулу и вычисли.</div>'
+trainerHTML('p19-iv2', 6, '$f(x_0)$')
+'</div>';
html += '<div class="wg" id="p19-iv3">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Найди нуль функции</div></div>'
+'<div class="wg-help">Реши уравнение $f(x) = 0$.</div>'
+trainerHTML('p19-iv3', 5, 'нуль $x$')
+'</div>';
html += secNav('p18', 'p20') + readButton('p19');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'Стоимость покупки $S$ от числа купленных тетрадей $n$', isF:true },
{ e:'Оценка за к/р и номер ученика в журнале', isF:false },
{ e:'Длина стороны квадрата и его площадь', isF:true },
{ e:'Периметр треугольника и его наибольшая сторона', isF:false },
{ e:'Время суток $t$ и температура воздуха $T$ в этот момент', isF:true },
{ e:'Объём куба $V$ и длина его ребра $a$', isF:true },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p19-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p19-iv1');bumpProgress('p19',22);} else if(score>=4){addXp(6,'p19-iv1');bumpProgress('p19',12);} return; }
document.getElementById('p19-iv1-i').textContent=(i+1);
document.getElementById('p19-iv1-s').textContent=score;
document.getElementById('p19-iv1-q').innerHTML=Q[i].e;
document.getElementById('p19-iv1-fb').style.display='none';
}
function ans(yes){
if(i>=Q.length) return;
const fb=document.getElementById('p19-iv1-fb');
if(yes===Q[i].isF){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; Это '+(Q[i].isF?'<b>функция</b>':'<b>не функция</b>'));
document.getElementById('p19-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p19-iv1-yes').addEventListener('click',()=>ans(true));
document.getElementById('p19-iv1-no').addEventListener('click',()=>ans(false));
show();
})();
makeTrainer({
idPrefix:'p19-iv2',
questions:[
{ q:'$f(x) = 5x - 1$, найди $f(0)$', a:-1 },
{ q:'$f(x) = 5x - 1$, найди $f(3)$', a:14 },
{ q:'$f(x) = -3x + 2$, найди $f(-1)$', a:5 },
{ q:'$f(x) = x^2 - 4$, найди $f(3)$', a:5 },
{ q:'$f(x) = \\dfrac{x}{x+5}$, найди $f(0)$', a:0 },
{ q:'$f(x) = 2x + 7$, найди $f(-3)$', a:1 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p19-iv2');bumpProgress('p19',28);} else if(s>=4){addXp(8,'p19-iv2');bumpProgress('p19',15);} }
});
makeTrainer({
idPrefix:'p19-iv3',
questions:[
{ q:'$f(x) = 9x - 1$', a:1/9, show:'$1/9$' },
{ q:'$f(x) = -6x$', a:0 },
{ q:'$f(x) = 0{,}1 - 2x$', a:0.05, show:'$0{,}05$' },
{ q:'$f(x) = 2x - 15$', a:7.5, show:'$7{,}5$' },
{ q:'$f(x) = 7 - 8x$', a:7/8, show:'$7/8$' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p19-iv3');bumpProgress('p19',28);} else if(s>=3){addXp(8,'p19-iv3');bumpProgress('p19',15);} }
});
wireReadBtn('p19');
}
function buildP20(){
const box = document.getElementById('p20-body');
let html = '';
html += makeCard('theory', 'Линейная функция', '20.1', `
<p><b>Линейной функцией</b> называется функция, заданная формулой $y = kx + b$, где $k$ и $b$ — некоторые числа.</p>
<p>Примеры: $y = 2x + 5$ ($k = 2, b = 5$); $y = -x$ ($k = -1, b = 0$); $y = 12$ ($k = 0, b = 12$); $y = 16x$ ($k = 16, b = 0$).</p>
<p><b>Не</b> линейные: $y = \\dfrac{2}{x} - 6$ (деление на $x$), $y = 12x^2 + 7$ ($x$ во второй степени).</p>`);
html += makeCard('theory', 'Свойства', '20.2', `
<p>Область определения линейной функции — <b>все числа</b>: $D(y) = \\mathbb{R}$.</p>
<p>Множество значений:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>если $k \\ne 0$ — все числа: $E(y) = \\mathbb{R}$;</li>
<li>если $k = 0$ — единственное значение $b$: $E(y) = \\{b\\}$.</li>
</ul>
<p><b>Нуль функции:</b> $y = 0 \\Rightarrow kx + b = 0 \\Rightarrow x = -\\dfrac{b}{k}$ (при $k \\ne 0$).</p>`);
html += makeCard('theory', 'График линейной функции', '20.3', `
<p>Графиком линейной функции $y = kx + b$ является <b>прямая</b>.</p>
<p>Чтобы построить прямую, нужны <b>две точки</b>. Выбираем два значения $x$, вычисляем $y$, отмечаем точки и проводим прямую.</p>
<p><b>Пример:</b> $y = 2x - 3$. При $x = 0$: $y = -3$ — точка $(0; -3)$. При $x = 2$: $y = 1$ — точка $(2; 1)$. Соединяем.</p>
<div style="display:flex;justify-content:center;margin-top:10px">${coordSVG({xMin:-3,xMax:5,yMin:-5,yMax:4, lines:[{k:2,b:-3,color:'#7c3aed'}], points:[{x:0,y:-3,label:'(0;-3)',color:'#dc2626'},{x:2,y:1,label:'(2;1)',color:'#dc2626'}]})}</div>`);
html += makeCard('rule', 'Геометрический смысл $k$ и $b$', '20.4', `
<p>Число $k$ называется <b>угловым коэффициентом</b> прямой.</p>
<ul style="padding-left:22px;line-height:1.85">
<li>$k > 0$ — прямая образует с положительным направлением оси $Ox$ <b>острый</b> угол;</li>
<li>$k < 0$ — <b>тупой</b> угол;</li>
<li>$k = 0$ — прямая <b>параллельна</b> оси $Ox$ (горизонталь).</li>
</ul>
<p>Число $b$ — <b>ордината точки пересечения</b> прямой с осью $Oy$ (то есть точка $(0; b)$).</p>`);
html += makeCard('rule', 'Взаимное расположение прямых', '20.5', `
<p>Для двух функций $y = k_1 x + b_1$ и $y = k_2 x + b_2$:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>$k_1 = k_2$ и $b_1 \\ne b_2$ — прямые <b>параллельны</b>;</li>
<li>$k_1 \\ne k_2$ — прямые <b>пересекаются</b>;</li>
<li>$k_1 = k_2$ и $b_1 = b_2$ — прямые <b>совпадают</b>.</li>
</ul>`);
html += '<div class="wg" id="p20-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">График $y = kx + b$ — слайдеры</div></div>'
+'<div class="wg-help">Двигай ползунки и смотри, как меняется прямая. $k$ задаёт <b>наклон</b>, $b$ — <b>сдвиг</b> по вертикали.</div>'
+'<div class="sliders"><label>$k$ = <b id="p20-k-val">2</b><input type="range" id="p20-k-sl" min="-5" max="5" step="0.5" value="2"></label>'
+'<label>$b$ = <b id="p20-b-val">-3</b><input type="range" id="p20-b-sl" min="-5" max="5" step="0.5" value="-3"></label></div>'
+'<div id="p20-iv1-svg" style="display:flex;justify-content:center"></div>'
+'<div id="p20-iv1-info" style="text-align:center;font-size:.92rem;margin-top:8px"></div></div>';
html += '<div class="wg" id="p20-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Угол и нуль</div></div>'
+'<div class="wg-help">По формуле $y = kx + b$ определи: знак $k$ → угол с осью $Ox$; нуль функции.</div>'
+trainerHTML('p20-iv2', 6, 'число / «остр»/«тупой»')
+'</div>';
html += '<div class="wg" id="p20-iv3">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Параллельны или пересекаются?</div></div>'
+'<div class="wg-help">Сравни две линейные функции. Определи их взаимное расположение по $k$ и $b$.</div>'
+'<div class="score-display"><span>Задача <b id="p20-iv3-i">1</b> / 6</span><span>Очки: <b id="p20-iv3-s">0</b> / 6</span></div>'
+'<div id="p20-iv3-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap">'
+'<button class="btn primary" id="p20-iv3-par" style="background:#10b981;border-color:#10b981">Параллельны</button>'
+'<button class="btn primary" id="p20-iv3-cross" style="background:#7c3aed;border-color:#7c3aed">Пересекаются</button>'
+'<button class="btn primary" id="p20-iv3-same" style="background:#0891b2;border-color:#0891b2">Совпадают</button>'
+'</div><div class="feedback" id="p20-iv3-fb"></div></div>';
html += '<div class="wg" id="p20-iv4">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Найди $k$ и $b$ по точкам</div></div>'
+'<div class="wg-help">Принадлежит ли точка $(x_0; y_0)$ графику $y = kx + b$? Подставь $x_0$ и проверь равенство с $y_0$.</div>'
+trainerHTML('p20-iv4', 5, 'да / нет')
+'</div>';
html += secNav('p19', 'final3') + readButton('p20');
box.innerHTML = html; renderMath(box);
/* IV1 slider */
(function(){
const kSl=document.getElementById('p20-k-sl'), bSl=document.getElementById('p20-b-sl');
const kV=document.getElementById('p20-k-val'), bV=document.getElementById('p20-b-val');
const out=document.getElementById('p20-iv1-svg'), info=document.getElementById('p20-iv1-info');
function update(){
const k=+kSl.value, b=+bSl.value;
kV.textContent=k; bV.textContent=b;
out.innerHTML = coordSVG({xMin:-5,xMax:5,yMin:-5,yMax:5, lines:[{k:k,b:b,color:'#7c3aed'}], points:b>=-5&&b<=5?[{x:0,y:b,label:'(0;'+b+')',color:'#dc2626'}]:[]});
let angle = k>0 ? 'острый угол с осью $Ox$' : (k<0 ? 'тупой угол с осью $Ox$' : 'параллельна оси $Ox$');
let zero = k!==0 ? 'нуль функции: $x = '+(-b/k).toFixed(2)+'$' : (b===0 ? 'все числа — нули' : 'нулей нет');
info.innerHTML = '<b>$y = '+(k===0?'':(k===1?'':k===-1?'-':k))+(k===0?'':'x')+(b>0?' + '+b:b<0?' - '+(-b):'')+(k===0?b:'')+'$</b> — '+angle+'; '+zero;
renderMath(info);
}
kSl.addEventListener('input', update); bSl.addEventListener('input', update); update();
})();
/* IV2 — angle/null */
makeTrainer({
idPrefix:'p20-iv2',
parser:(v)=>v,
questions:[
{ q:'$y = 3x - 6$ — какой угол с осью $Ox$? («острый» или «тупой»)', a:(v)=>String(v).trim().toLowerCase().startsWith('остр'), show:'острый ($k>0$)' },
{ q:'$y = -4x + 2$ — угол?', a:(v)=>String(v).trim().toLowerCase().startsWith('туп'), show:'тупой ($k<0$)' },
{ q:'$y = 2x - 10$ — нуль функции (число)', a:(v)=>parseFloat(String(v).replace(',','.'))===5, show:'5' },
{ q:'$y = -3x + 12$ — нуль функции (число)', a:(v)=>parseFloat(String(v).replace(',','.'))===4, show:'4' },
{ q:'$y = 9x$ — нуль функции', a:(v)=>parseFloat(String(v).replace(',','.'))===0, show:'0' },
{ q:'$y = 7$ — какой угол с $Ox$? («парал», «острый» или «тупой»)', a:(v)=>String(v).trim().toLowerCase().startsWith('парал'), show:'параллельна ($k=0$)' },
],
onComplete:(s,n)=>{ if(s===n){addXp(18,'p20-iv2');bumpProgress('p20',30);} else if(s>=4){addXp(10,'p20-iv2');bumpProgress('p20',18);} }
});
/* IV3 — взаимное расположение */
(function(){
const Q=[
{ e:'$y = x - 6$ и $y = 49x$', ans:'cross' },
{ e:'$y = x$ и $y = x + 8$', ans:'par' },
{ e:'$y = 1{,}5x + 5$ и $y = 9$', ans:'cross' },
{ e:'$y = 0{,}1x$ и $y = 0{,}2x + 0{,}1$', ans:'cross' },
{ e:'$y = -7x$ и $y = 7x$', ans:'cross' },
{ e:'$y = 2x + 3$ и $y = 2x + 3$', ans:'same' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p20-iv3-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p20-iv3');bumpProgress('p20',25);} else if(score>=4){addXp(7,'p20-iv3');bumpProgress('p20',12);} return; }
document.getElementById('p20-iv3-i').textContent=(i+1);
document.getElementById('p20-iv3-s').textContent=score;
document.getElementById('p20-iv3-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p20-iv3-q'));
document.getElementById('p20-iv3-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p20-iv3-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={par:'параллельны',cross:'пересекаются',same:'совпадают'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p20-iv3-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p20-iv3-par').addEventListener('click',()=>ans('par'));
document.getElementById('p20-iv3-cross').addEventListener('click',()=>ans('cross'));
document.getElementById('p20-iv3-same').addEventListener('click',()=>ans('same'));
show();
})();
/* IV4 — принадлежит ли точка */
makeTrainer({
idPrefix:'p20-iv4',
parser:(v)=>v,
questions:[
{ q:'Точка $M(-1; 5)$ и $y = 2x - 3$. Принадлежит? «да»/«нет»', a:(v)=>String(v).trim().toLowerCase().startsWith('н'), show:'нет ($2(-1)-3 = -5 \\ne 5$)' },
{ q:'Точка $A(2; 7)$ и $y = 3x + 1$. Принадлежит?', a:(v)=>String(v).trim().toLowerCase().startsWith('д'), show:'да ($3\\cdot2+1 = 7$)' },
{ q:'Точка $B(0; -4)$ и $y = -x - 4$. Принадлежит?', a:(v)=>String(v).trim().toLowerCase().startsWith('д'), show:'да' },
{ q:'Точка $K(-2; 11)$ и $y = -4x + 3$. Принадлежит?', a:(v)=>String(v).trim().toLowerCase().startsWith('д'), show:'да ($-4(-2)+3 = 11$)' },
{ q:'Точка $C(3; 0)$ и $y = 2x - 5$. Принадлежит?', a:(v)=>String(v).trim().toLowerCase().startsWith('н'), show:'нет ($2\\cdot3 - 5 = 1 \\ne 0$)' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p20-iv4');bumpProgress('p20',25);} else if(s>=3){addXp(7,'p20-iv4');bumpProgress('p20',12);} }
});
wireReadBtn('p20');
}
const BOSSES_CH3 = [
{
n:1, title:'Босс \xA715-16 — Уравнения и текстовые задачи', color:'#7c3aed',
steps:[
{ q:'Реши $-5x = 35$. Введи $x$.', verify:(v)=>+v===-7, hint:'$x = 35 : (-5)$.' },
{ q:'Реши $5(x-2)+1 = 4x$. Введи $x$.', verify:(v)=>+v===9, hint:'$5x-10+1=4x \\Rightarrow x = 9$.' },
{ q:'Сумма двух чисел 60, одно в 5 раз больше другого. Меньшее = ?', verify:(v)=>+v===10, hint:'$x+5x=60$.' },
{ q:'Из городов навстречу, скорости 50 и 60 км/ч, расстояние 220 км. Через сколько часов встретятся?', verify:(v)=>+v===2, hint:'$50t+60t=220$.' },
{ q:'$0 \\cdot x = 7$ — есть ли корни? Введи «нет» или «есть».', verify:(v)=>['нет','no','-'].includes(String(v).trim().toLowerCase()), hint:'$0 \\ne 7$.' },
]
},
{
n:2, title:'Босс \xA717 — Числовые неравенства', color:'#c026d3',
steps:[
{ q:'Из $a > b$ умножили обе части на $-3$. Знак сохранился? «да»/«нет»', verify:(v)=>['нет','no','-'].includes(String(v).trim().toLowerCase()), hint:'При умножении на отрицательное число знак меняется.' },
{ q:'Если $3 < a < 5$ и $2 < b < 4$, то $a+b$ больше какого числа?', verify:(v)=>+v===5, hint:'$3+2=5$.' },
{ q:'Если $5 < a < 9$, то $2a$ меньше какого числа?', verify:(v)=>+v===18, hint:'$2 \\cdot 9 = 18$.' },
{ q:'Если $a < b$, то $-3a$ и $-3b$ как соотносятся? «больше» или «меньше» — введи знак сравнения $-3a$ и $-3b$ (например, &gt;)', verify:(v)=>String(v).trim()==='>', hint:'Умножили на $-3$ — знак сменился.' },
{ q:'Сложи $5 < x < 9$ и $-1 < y < 7$. Введи нижнюю границу $x+y$.', verify:(v)=>+v===4, hint:'$5+(-1)=4$.' },
]
},
{
n:3, title:'Босс \xA718 — Линейные неравенства', color:'#db2777',
steps:[
{ q:'Реши $7x < 21$. Введи правую часть в ответе $x < ?$', verify:(v)=>+v===3, hint:'$21:7=3$.' },
{ q:'Реши $-4x \\ge 16$. Введи правую часть в $x \\le ?$', verify:(v)=>+v===-4, hint:'Делим на $-4$, знак меняется.' },
{ q:'Реши $3(x+2) > 4 - x$. Введи правую часть в $x > ?$', verify:(v)=>Math.abs(parseFloat(String(v).replace(',','.'))-(-0.5))<1e-6, hint:'$3x+6 > 4-x \\Rightarrow 4x > -2$.' },
{ q:'$0 \\cdot x < 5$ — введи решение: «нет», «любое» или число.', verify:(v)=>String(v).trim().toLowerCase()==='любое', hint:'$0 < 5$ — всегда верно.' },
{ q:'$0 \\cdot x > 1$ — введи решение: «нет», «любое» или число.', verify:(v)=>String(v).trim().toLowerCase()==='нет', hint:'$0 > 1$ — ложь.' },
]
},
{
n:4, title:'Босс \xA719 — Функция', color:'#2563eb',
steps:[
{ q:'$f(x) = 5x - 1$. Найди $f(3)$.', verify:(v)=>+v===14, hint:'$5 \\cdot 3 - 1$.' },
{ q:'$f(x) = -3x + 2$. Найди $f(-1)$.', verify:(v)=>+v===5, hint:'$-3(-1)+2=5$.' },
{ q:'$f(x) = 9x - 1$ — найди нуль функции (дробь $a/b$).', verify:(v)=>{const m=String(v).match(/^(-?\d+)\s*\/\s*(-?\d+)$/); if(!m) return Math.abs(parseFloat(String(v).replace(',','.'))-1/9)<1e-4; return +m[1]===1 && +m[2]===9;}, hint:'$9x = 1$.' },
{ q:'$y = x^2 + 5$ — минимальное значение функции?', verify:(v)=>+v===5, hint:'$x^2 \\ge 0$, поэтому $y \\ge 5$.' },
{ q:'Зависимость стоимости покупки от числа купленных тетрадей — это функция? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'Каждому $n$ — одна цена.' },
]
},
{
n:5, title:'Финальный босс — Линейная функция $y = kx + b$', color:'#dc2626',
steps:[
{ q:'У $y = -3x + 4$: введи $k$', verify:(v)=>+v===-3, hint:'Число при $x$.' },
{ q:'У $y = -3x + 4$: введи $b$', verify:(v)=>+v===4, hint:'Свободный член.' },
{ q:'$y = 2x + 5$ и $y = 2x - 7$. Что общее? «параллельны» или «пересекаются»', verify:(v)=>String(v).trim().toLowerCase().startsWith('парал'), hint:'$k_1 = k_2 = 2$, $b_1 \\ne b_2$.' },
{ q:'$y = 4x + 5$ — найди нуль функции (число)', verify:(v)=>Math.abs(parseFloat(String(v).replace(',','.'))-(-1.25))<1e-6, hint:'$4x = -5$.' },
{ q:'Принадлежит ли точка $A(2; 7)$ графику $y = 3x + 1$? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'$3 \\cdot 2 + 1 = 7$.' },
]
},
];
function buildFinal3(){
const box = document.getElementById('final3-body');
let html = '';
html += makeCard('theory', 'Что мы изучили', 'Итог', `
<p>В этой главе мы:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>научились решать <b>линейные уравнения</b> $ax = b$ — три случая по корням;</li>
<li>освоили <b>составление уравнений</b> для текстовых задач (возраст, движение, покупки);</li>
<li>выучили <b>3 свойства числовых неравенств</b> и сложение/умножение неравенств;</li>
<li>научились решать <b>линейные неравенства</b>, помня про смену знака при умножении на отрицательное;</li>
<li>узнали, что такое <b>функция</b>, аргумент, значение, нуль, область определения;</li>
<li>освоили <b>линейную функцию</b> $y = kx + b$ — наклон по $k$, сдвиг по $b$.</li>
</ul>
<p>Теперь — <b>5 боссов</b>. Каждый — серия из 5 этапов.</p>`);
html += '<div id="bosses-container-ch3"></div>';
html += '<div style="margin-top:22px;padding:18px 20px;background:linear-gradient(135deg,var(--pri-soft),var(--acc-soft));border-radius:14px;border:1.5px solid var(--pri);text-align:center">'
+'<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>'
+'<div id="boss-overall-ch3" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>'
+'<div class="hp-boss"><div class="hp-boss-fill" id="boss-overall-fill-ch3" style="width:0%;background:linear-gradient(90deg,#7c3aed,#a78bfa)"></div></div>'
+'</div>';
html += secNav('p20', null);
box.innerHTML = html; renderMath(box);
const cont = document.getElementById('bosses-container-ch3');
const BOSS_STATE = (function(){
try{ const s=localStorage.getItem('algebra7_ch3_bosses'); if(s) return JSON.parse(s); }catch(e){}
return BOSSES_CH3.map(()=>({stage:0,defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem('algebra7_ch3_bosses', JSON.stringify(BOSS_STATE)); }catch(e){} }
function refreshOverall(){
const won=BOSS_STATE.filter(b=>b.defeated).length;
const txt=document.getElementById('boss-overall-ch3'); if(txt) txt.textContent=won+' / '+BOSSES_CH3.length+' боссов побеждено';
const fill=document.getElementById('boss-overall-fill-ch3'); if(fill) fill.style.width=(won*100/BOSSES_CH3.length)+'%';
if(won>=BOSSES_CH3.length){ bumpProgress('final3',60); achievement('ch3_done','Глава 3 пройдена!'); }
}
cont.innerHTML = BOSSES_CH3.map((b,idx)=>{
return '<div class="boss-card" id="boss-card-'+idx+'" style="border-color:'+b.color+'">'
+'<div class="boss-head">'
+'<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px"><polygon points="12,2 22,20 2,20"/></svg>'
+'<div class="boss-title" style="color:'+b.color+'">'+b.title+'</div>'
+'<div class="boss-stage" id="boss-'+idx+'-stage">Этап 1 / '+b.steps.length+'</div>'
+'</div>'
+'<div class="hp-boss" style="border-color:'+b.color+'66;background:'+b.color+'1a"><div class="hp-boss-fill" id="boss-'+idx+'-fill" style="width:0%;background:linear-gradient(90deg,'+b.color+',#f59e0b)"></div></div>'
+'<div class="boss-q" id="boss-'+idx+'-q" style="border-color:'+b.color+'"></div>'
+'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
+'<input type="text" id="boss-'+idx+'-input" class="tinp" placeholder="Ответ" style="width:160px;text-align:center">'
+'<button class="btn primary" id="boss-'+idx+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атака</button>'
+'<button class="btn" id="boss-'+idx+'-hint">Подсказка</button>'
+'<button class="btn" id="boss-'+idx+'-restart">↻</button>'
+'</div>'
+'<div class="feedback" id="boss-'+idx+'-fb"></div>'
+'</div>';
}).join('');
BOSSES_CH3.forEach((b,idx)=>{
function show(){
const st=BOSS_STATE[idx];
const stageEl=document.getElementById('boss-'+idx+'-stage');
const fill=document.getElementById('boss-'+idx+'-fill');
const q=document.getElementById('boss-'+idx+'-q');
const fb=document.getElementById('boss-'+idx+'-fb');
if(st.defeated){
stageEl.textContent='✓ Побеждён'; fill.style.width='100%';
q.innerHTML='<b style="color:'+b.color+'">Босс повержен!</b>';
document.getElementById('boss-'+idx+'-go').disabled=true;
document.getElementById('boss-'+idx+'-go').style.opacity=.5;
return;
}
stageEl.textContent='Этап '+(st.stage+1)+' / '+b.steps.length;
fill.style.width=(st.stage*100/b.steps.length)+'%';
q.innerHTML=b.steps[st.stage].q;
document.getElementById('boss-'+idx+'-input').value='';
fb.style.display='none';
renderMath(q);
}
document.getElementById('boss-'+idx+'-go').addEventListener('click',()=>{
const st=BOSS_STATE[idx]; if(st.defeated) return;
const step=b.steps[st.stage];
const val=document.getElementById('boss-'+idx+'-input').value;
const fb=document.getElementById('boss-'+idx+'-fb');
if(!val.trim()){ feedback(fb,false,'&#10007; Введи ответ.'); return; }
if(step.verify(val)){
st.stage++;
if(st.stage>=b.steps.length){
st.defeated=true; saveBosses();
feedback(fb,true,'&#10003; Босс '+b.n+' побеждён! +20 XP');
addXp(20,'boss-'+b.n); bumpProgress('final3',18); refreshOverall();
setTimeout(show,1400);
if(window.confetti) try{confetti();}catch(e){}
}else{
saveBosses(); feedback(fb,true,'&#10003; Верно! +3 XP'); addXp(3,'boss-step'); setTimeout(show,1100);
}
}else{ feedback(fb,false,'&#10007; Промах.'); }
});
document.getElementById('boss-'+idx+'-hint').addEventListener('click',()=>{
const st=BOSS_STATE[idx]; if(st.defeated) return;
const fb=document.getElementById('boss-'+idx+'-fb');
fb.className='feedback ok';
fb.innerHTML='<span style="color:#92400e">\u{1F4A1} Подсказка:</span> '+b.steps[st.stage].hint;
fb.style.display='block';
fb.style.background='var(--warn-bg)'; fb.style.color='#92400e'; fb.style.borderLeftColor='var(--warn)';
renderMath(fb);
});
document.getElementById('boss-'+idx+'-restart').addEventListener('click',()=>{
BOSS_STATE[idx]={stage:0,defeated:false}; saveBosses();
document.getElementById('boss-'+idx+'-go').disabled=false;
document.getElementById('boss-'+idx+'-go').style.opacity=1;
show(); refreshOverall();
});
show();
});
refreshOverall();
}
</script>
</body>
</html>