Files
Learn_System/frontend/textbooks/algebra_7_ch4.html
T
Maxim Dolgolyov 660e7e2747 feat(gamification): Phase 1 — full kill-switch + textbook XP wrapping
Until now the 'gamification' feature flag did nothing: it had no row in
app_settings, the admin couldn't toggle it, awardXP/awardCoins ignored
it, and the CSS only hid three dashboard widgets — XP bars in textbooks
stayed visible regardless.

Phase 1 closes every hole.

Backend (source of truth):
  • migration 029 seeds feature_gamification_enabled=1
  • new isGamificationEnabled() helper in gamification/_shared.js with a
    30s cache + invalidateGamificationCache() for instant admin toggles
  • awardXP / awardCoins / updateStreak / unlockAchievement /
    checkAchievements all bail out when the flag is off
  • /api/gamification/* and /api/shop/* (user routes) return 404 when
    disabled; admin routes remain open so the switch itself is reachable
  • adminController.updateFeatures gains 'gamification' in the allow-list
    and invalidates the cache on flip

Frontend:
  • LS.isGamificationEnabled() (synchronous, populated by loadFeatures)
    so xp.js + applyCosmetics can bail without a round-trip
  • xp.js load/add/flush become no-ops when the flag is off
  • applyCosmetics skips the round-trip when off
  • CSS .no-gamification rule expanded to cover .hero-xp-badge, .po-xp,
    .xp-card, .xp-bar, #frames-section, and a universal [data-gamified]
    hook for future blocks

Textbooks (Variant 2 of the plan):
  • backend/scripts/wrap_textbook_xp.py — idempotent script that adds
    data-gamified to 167 XP tags across 63 textbook files (chapters +
    hubs, all subjects/grades). Single CSS rule now hides everything.

Verified end-to-end: with the flag off, awardXP/awardCoins write nothing;
flipping back restores normal behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 19:43:24 +03:00

1523 lines
121 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Алгебра 7 · Глава 4 · Системы линейных уравнений</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:#0891b2; --pri2:#0e7490; --pri-soft:#cffafe;
--acc:#22d3ee; --acc2:#0891b2; --acc-soft:#ecfeff;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#04141a; --card:#0a1b22; --card-soft:#0d2229; --text:#e0fcff; --ink:#e0fcff; --muted:#7aa8b3; --border:#163842}
*{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,#164e63 0%,#0891b2 55%,#22d3ee 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(165,243,252,.2);min-height:130px}
.hdr::before{content:'ГЛАВА 4';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(209,250,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:'{';position:absolute;right:0;top:-30px;font-size:clamp(2rem,12vw,8rem);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(8,145,178,.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(8,145,178,.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(8,145,178,.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(180px,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(8,145,178,.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-p21"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.sec[id="sec-p22"]{ --sec-acc:#06b6d4; --sec-acc-d:#0891b2; --sec-acc-soft:#cffafe; }
.sec[id="sec-p23"]{ --sec-acc:#2563eb; --sec-acc-d:#1d4ed8; --sec-acc-soft:#dbeafe; }
.sec[id="sec-p24"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p25"]{ --sec-acc:#db2777; --sec-acc-d:#9d174d; --sec-acc-soft:#fce7f3; }
.sec[id="sec-final4"]{ --sec-acc:#0891b2; --sec-acc-d:#0e7490; --sec-acc-soft:#cffafe; }
.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(8,145,178,.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(8,145,178,.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(34,211,238,.20);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,#0891b2,#22d3ee);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(8,145,178,.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(8,145,178,.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(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)}
.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 · Глава 4</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>. Финал — <b>текстовые задачи</b>, которые удобно решаются именно системой двух уравнений.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p21')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 21</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-p21" class="sec" data-watermark="ax+by=c"><div class="sec-header"><span class="sec-num">§ 21</span><h2 class="sec-h">Линейное уравнение с двумя переменными</h2></div><div id="p21-body"></div></section>
<section id="sec-p22" class="sec" data-watermark=""><div class="sec-header"><span class="sec-num">§ 22</span><h2 class="sec-h">График линейного уравнения $ax + by = c$</h2></div><div id="p22-body"></div></section>
<section id="sec-p23" class="sec" data-watermark="{"><div class="sec-header"><span class="sec-num">§ 23</span><h2 class="sec-h">Система линейных уравнений с двумя переменными</h2></div><div id="p23-body"></div></section>
<section id="sec-p24" class="sec" data-watermark="±"><div class="sec-header"><span class="sec-num">§ 24</span><h2 class="sec-h">Способы решения системы (подстановка, сложение)</h2></div><div id="p24-body"></div></section>
<section id="sec-p25" class="sec" data-watermark="?"><div class="sec-header"><span class="sec-num">§ 25</span><h2 class="sec-h">Решение текстовых задач с помощью системы</h2></div><div id="p25-body"></div></section>
<section id="sec-final4" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#0891b2,#22d3ee)">Финал главы</span><h2 class="sec-h">Итоги. 5 боссов главы 4</h2></div><div id="final4-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» · Глава 4 · Системы линейных уравнений · 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:'p21', progress:{p21:0,p22:0,p23:0,p24:0,p25:0,final4:0}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 6;
const _TB_SLUG = 'algebra-7-ch4';
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:'Начало главы 4!',
p22_done:'Графики линейных уравнений!',
p24_done:'Подстановка и сложение освоены!',
p25_done:'Текстовые задачи на системы!',
ch4_done:'Глава 4 пройдена! Алгебра 7 — финал!',
};
function loadProgress(){
try{
const s=localStorage.getItem('algebra7_ch4_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('algebra7_ch4_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_ch4_progress', JSON.stringify(STATE.progress));
localStorage.setItem('algebra7_ch4_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==='p22') achievement('p22_done');
else if(key==='p24') achievement('p24_done');
else if(key==='p25') achievement('p25_done');
else if(key==='final4') achievement('ch4_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-ch4-'+(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:'p21', num:'§ 21', name:'Линейное уравнение с 2 переменными', sub:'$ax + by = c$' },
{ id:'p22', num:'§ 22', name:'График уравнения', sub:'Прямая на плоскости' },
{ id:'p23', num:'§ 23', name:'Система уравнений', sub:'$\\{a_1x+b_1y=c_1$' },
{ id:'p24', num:'§ 24', name:'Подстановка и сложение', sub:'Два алгоритма' },
{ id:'p25', num:'§ 25', name:'Текстовые задачи', sub:'Через систему' },
{ id:'final4', 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 = { p21:()=>buildP21(), p22:()=>buildP22(), p23:()=>buildP23(), p24:()=>buildP24(), p25:()=>buildP25(), final4:()=>buildFinal4() };
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 = {
p21:{title:'Шпаргалка \xA721',rows:[
['Уравнение','$ax + by = c$ — линейное с двумя переменными'],
['Решение','пара чисел $(x_0; y_0)$, при которой равенство верно'],
['Решений','бесконечно много'],
['Выразить $y$','$y = \\dfrac{c - ax}{b}$ (при $b \\ne 0$)'],
]},
p22:{title:'Шпаргалка \xA722',rows:[
['График','прямая на координатной плоскости'],
['$b \\ne 0$','график — прямая $y = -\\dfrac{a}{b}x + \\dfrac{c}{b}$'],
['$b = 0, a \\ne 0$','прямая, параллельная оси $Oy$ ($x = c/a$)'],
['$a = 0, b \\ne 0$','прямая, параллельная оси $Ox$ ($y = c/b$)'],
['Построение','две точки → прямая'],
]},
p23:{title:'Шпаргалка \xA723',rows:[
['Система','две прямые на одной плоскости'],
['Решение','пара $(x_0; y_0)$ — общая точка'],
['Пересекаются','одно решение'],
['Параллельны','решений нет'],
['Совпадают','бесконечно много решений'],
]},
p24:{title:'Шпаргалка \xA724',rows:[
['Подстановка','выразил из 1-го → подставил во 2-е'],
['Сложение','уравнял коэф. → сложил/вычел → одна переменная'],
['Когда что','подстановка — если легко выразить; сложение — когда есть противопол. коэф.'],
]},
p25:{title:'Шпаргалка \xA725',rows:[
['Шаг 1','что неизвестно? — это $x$ и $y$'],
['Шаг 2','составь 2 уравнения по условию'],
['Шаг 3','реши систему любым способом'],
['Шаг 4','проверь смысл и запиши ответ'],
]},
final4:{title:'Финал главы',rows:[
['\xA721\xA725','теория главы 4'],
['Боссов','5'],
['Награда','+100 XP за полное прохождение'],
['Алгебра 7','полностью пройдена!'],
]},
};
const TIPS=[
{sec:'p21',html:'Линейное уравнение $ax + by = c$ имеет <b>бесконечно много</b> решений. Каждое решение — это <b>пара</b> чисел $(x; y)$.'},
{sec:'p22',html:'График проходит через любые две его точки. Удобно искать точки пересечения с осями: при $x = 0$ найти $y$, при $y = 0$ найти $x$.'},
{sec:'p23',html:'Графически система — это <b>две прямые</b>. Решение — координаты <b>общей точки</b>. Если прямые параллельны — решений нет.'},
{sec:'p24',html:'Подстановка лучше, если в одном уравнении легко выразить переменную ($x = ...$). Сложение — когда сразу видны противоположные коэф.'},
{sec:'p25',html:'В задачах на системы — <b>две</b> неизвестные. Внимательно подбирай, что обозначить $x$, что $y$, и составь <b>два</b> уравнения.'},
{sec:'final4',html:'5 боссов. После их прохождения вся Алгебра 7 будет в твоём арсенале!'},
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS.p21;
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_ch4_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_ch4_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={p21:'\xA721',p22:'\xA722',p23:'\xA723',p24:'\xA724',p25:'\xA725',final4:'Финал'};
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=['#0891b2','#22d3ee','#67e8f9','#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:'Уравнение вида $ax + by = c$, где $a, b, c$ — числа.', sec:'p21', aliases:['линейное уравнение с двумя переменными','линейного уравнения с двумя переменными','линейных уравнений с двумя переменными'] },
{ term:'решение уравнения', def:'Пара чисел $(x_0; y_0)$, при подстановке которой уравнение становится верным равенством.', sec:'p21', aliases:['решение уравнения','решения уравнения','решением уравнения'] },
{ term:'график уравнения', def:'Множество всех точек $(x; y)$, координаты которых удовлетворяют уравнению. У линейного уравнения — прямая.', sec:'p22', aliases:['график уравнения','графика уравнения','графики уравнений'] },
{ term:'система уравнений', def:'Несколько уравнений, для которых ищут общие решения.', sec:'p23', aliases:['система уравнений','системы уравнений','систему уравнений','систем уравнений','системой уравнений'] },
{ term:'решение системы', def:'Пара чисел $(x_0; y_0)$, удовлетворяющая каждому уравнению системы.', sec:'p23', aliases:['решение системы','решения системы','решением системы'] },
{ term:'способ подстановки', def:'Из одного уравнения выражают переменную и подставляют в другое.', sec:'p24', aliases:['способ подстановки','способом подстановки','подстановки','подстановка'] },
{ term:'способ сложения', def:'Сложение или вычитание уравнений системы (часто после умножения на множители).', sec:'p24', 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}));
[
['Формула','ax + by = c — линейное уравнение','\xA721','p21'],
['Формула','система: a1x+b1y=c1, a2x+b2y=c2','\xA723','p23'],
['Способ','Подстановка: x=... из 1-го → во 2-е','\xA724','p24'],
['Способ','Сложение: уравнять коэф → сложить','\xA724','p24'],
].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('p21');
setTimeout(()=>achievement('start','Начало главы 4!'), 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 — supports multiple lines */
function coordSVG(opts){
const w=opts.width||320, h=opts.height||280;
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+'">';
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"/>';
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"/>';
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"/>';
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>';
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>'; }
/* lines: each is {a,b,c, color, label} for ax+by=c, or {k,b, color, label} for y=kx+b */
(opts.lines||[]).forEach(L=>{
let k, B;
if('a' in L){
if(L.b===0){ /* vertical x = c/a */
const xv = L.c/L.a;
svg += '<line x1="'+sx(xv)+'" y1="'+padT+'" x2="'+sx(xv)+'" y2="'+(padT+H)+'" stroke="'+(L.color||'#0891b2')+'" stroke-width="2.4"/>';
if(L.label) svg += '<text x="'+(sx(xv)+4)+'" y="'+(padT+12)+'" font-size="10" fill="'+(L.color||'#0891b2')+'" font-weight="700" font-family="JetBrains Mono,monospace">'+L.label+'</text>';
return;
}
k = -L.a/L.b; B = L.c/L.b;
} else { k = L.k; B = L.b; }
const x1=xMin, y1=k*x1+B, x2=xMax, y2=k*x2+B;
function clip(x1,y1,x2,y2){
const pts=[];
[yMax,yMin].forEach(yE=>{ if((y1-yE)*(y2-yE)<=0 && y1!==y2){ const t=(yE-y1)/(y2-y1); const x=x1+t*(x2-x1); if(x>=xMin-.01 && x<=xMax+.01) pts.push([x,yE]); } });
[xMax,xMin].forEach(xE=>{ if((x1-xE)*(x2-xE)<=0 && x1!==x2){ const t=(xE-x1)/(x2-x1); const y=y1+t*(y2-y1); if(y>=yMin-.01 && y<=yMax+.01) pts.push([xE,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||'#0891b2')+'" stroke-width="2.4"/>';
if(L.label) svg += '<text x="'+(sx(pts[0][0])+6)+'" y="'+(sy(pts[0][1])-6)+'" font-size="10" fill="'+(L.color||'#0891b2')+'" font-weight="700" font-family="JetBrains Mono,monospace">'+L.label+'</text>';
}
});
(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 buildP21(){
const box = document.getElementById('p21-body');
let html = '';
html += makeCard('theory', 'Линейное уравнение с двумя переменными', '21.1', `
<p><b>Линейным уравнением</b> с двумя переменными называется уравнение вида:</p>
\\[ax + by = c\\]
<p>где $a, b, c$ — числа, а $x, y$ — переменные.</p>
<p><b>Решением</b> уравнения называется упорядоченная <b>пара чисел</b> $(x_0; y_0)$, при подстановке которой уравнение становится верным равенством.</p>
<p>Например, для $x - y = 4$: пары $(5; 1)$, $(0; -4)$, $(\\tfrac{16}{7}; -\\tfrac{12}{7})$ — все являются решениями. Решений у линейного уравнения с двумя переменными — <b>бесконечно много</b>.</p>`);
html += makeCard('rule', 'Выражение одной переменной через другую', '21.2', `
<p>Чтобы найти много решений, удобно выразить одну переменную через другую. Если $b \\ne 0$:</p>
\\[y = \\dfrac{c - ax}{b}\\]
<p>Подставляя любое значение $x$, получаем соответствующее $y$.</p>
<p><b>Пример:</b> $x + 4y = 7 \\Rightarrow y = \\dfrac{7 - x}{4}$. При $x = 3$: $y = 1$. При $x = -1$: $y = 2$. При $x = 0$: $y = 1{,}75$.</p>`);
html += makeCard('example', 'Применение', '21.3', `
<p><b>Проверь:</b> является ли пара $(3; -20)$ решением $10x + y = 12$?</p>
<p>Подставим: $10 \\cdot 3 + (-20) = 30 - 20 = 10 \\ne 12$. <b>Нет</b>, не является.</p>
<p>А пара $(1; 2)$: $10 \\cdot 1 + 2 = 12$. <b>Да</b>, является.</p>
<p><b>Задача:</b> 2 пакета молока и пакет кефира стоят 5 р. 25 к. Записать уравнением: пусть пакет молока стоит $x$ р., кефира — $y$ р. Тогда $2x + y = 5{,}25$.</p>`);
html += '<div class="wg" id="p21-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="p21-iv1-i">1</b> / 6</span><span>Очки: <b id="p21-iv1-s">0</b> / 6</span></div>'
+'<div id="p21-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:10px;justify-content:center"><button class="btn primary" id="p21-iv1-yes" style="background:#10b981;border-color:#10b981">Да</button><button class="btn primary" id="p21-iv1-no" style="background:#dc2626;border-color:#dc2626">Нет</button></div>'
+'<div class="feedback" id="p21-iv1-fb"></div></div>';
html += '<div class="wg" id="p21-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Выразить $y$ через $x$</div></div>'
+'<div class="wg-help">По уравнению $ax + by = c$ найди коэффициенты в формуле $y = px + q$. Введи через запятую: <b>p, q</b>.</div>'
+trainerHTML('p21-iv2', 5, 'p, q')
+'</div>';
html += '<div class="wg" id="p21-iv3">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Найди значение по уравнению</div></div>'
+'<div class="wg-help">Дано уравнение и одно значение. Найди второе.</div>'
+trainerHTML('p21-iv3', 5, 'число')
+'</div>';
html += secNav(null, 'p22') + readButton('p21');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$x - y = 4$, пара $(5; 1)$', isOK:true },
{ e:'$10x + y = 12$, пара $(3; -20)$', isOK:false },
{ e:'$10x + y = 12$, пара $(1; 2)$', isOK:true },
{ e:'$10x + y = 12$, пара $(0{,}1; 11)$', isOK:true },
{ e:'$x + 4y = 7$, пара $(-1; 2)$', isOK:true },
{ e:'$3x + 5y = 18$, пара $(1; 3)$', isOK:true },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p21-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p21-iv1');bumpProgress('p21',25);} else if(score>=4){addXp(8,'p21-iv1');bumpProgress('p21',15);} return; }
document.getElementById('p21-iv1-i').textContent=(i+1);
document.getElementById('p21-iv1-s').textContent=score;
document.getElementById('p21-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p21-iv1-q'));
document.getElementById('p21-iv1-fb').style.display='none';
}
function ans(isYes){
if(i>=Q.length) return;
const fb=document.getElementById('p21-iv1-fb');
if(isYes===Q[i].isOK){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; Правильно: '+(Q[i].isOK?'<b>да</b>':'<b>нет</b>'));
document.getElementById('p21-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p21-iv1-yes').addEventListener('click',()=>ans(true));
document.getElementById('p21-iv1-no').addEventListener('click',()=>ans(false));
show();
})();
function parsePair(v){ return String(v).trim().replace(',','.').split(/[,;\s]+/).filter(Boolean); }
makeTrainer({
idPrefix:'p21-iv2',
parser:(v)=>v,
questions:[
{ q:'$x + y = 5 \\Rightarrow y = px + q$. Введи p, q', a:(v)=>{const m=parsePair(v); return +m[0]===-1 && +m[1]===5;}, show:'-1, 5' },
{ q:'$2x + y = 7 \\Rightarrow y = ?$', a:(v)=>{const m=parsePair(v); return +m[0]===-2 && +m[1]===7;}, show:'-2, 7' },
{ q:'$3x - y = -5 \\Rightarrow y = ?$', a:(v)=>{const m=parsePair(v); return +m[0]===3 && +m[1]===5;}, show:'3, 5' },
{ q:'$x + 4y = 8 \\Rightarrow y = ?$', a:(v)=>{const m=parsePair(v); return Math.abs(+m[0]-(-0.25))<1e-6 && +m[1]===2;}, show:'-0.25, 2' },
{ q:'$-2x + 3y = 12 \\Rightarrow y = ?$', a:(v)=>{const m=parsePair(v); return Math.abs(+m[0]-(2/3))<1e-3 && +m[1]===4;}, show:'2/3, 4' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p21-iv2');bumpProgress('p21',25);} else if(s>=3){addXp(8,'p21-iv2');bumpProgress('p21',15);} }
});
makeTrainer({
idPrefix:'p21-iv3',
questions:[
{ q:'$x + y = 5$, $x = 3$. Найди $y$.', a:2 },
{ q:'$2x + y = 7$, $x = 1$. Найди $y$.', a:5 },
{ q:'$x - 3y = 6$, $y = 0$. Найди $x$.', a:6 },
{ q:'$5x + 2y = 14$, $x = 2$. Найди $y$.', a:2 },
{ q:'$3x - 4y = 1$, $y = 2$. Найди $x$.', a:3 },
],
onComplete:(s,n)=>{ if(s===n){addXp(12,'p21-iv3');bumpProgress('p21',22);} else if(s>=3){addXp(6,'p21-iv3');bumpProgress('p21',12);} }
});
wireReadBtn('p21');
}
function buildP22(){
const box = document.getElementById('p22-body');
let html = '';
html += makeCard('theory', 'График — это прямая', '22.1', `
<p><b>Графиком</b> линейного уравнения $ax + by = c$ называется множество всех точек координатной плоскости, координаты которых $(x; y)$ удовлетворяют уравнению.</p>
<p>Если $b \\ne 0$, выразим $y$: $y = -\\dfrac{a}{b}x + \\dfrac{c}{b}$. Это уже знакомая <b>линейная функция</b>, графиком которой является <b>прямая</b>.</p>
<p><b>Графиком линейного уравнения $ax + by = c$ всегда является прямая.</b></p>
<p>Например, $3x + 2y = 6 \\Rightarrow y = -1{,}5x + 3$.</p>
<div style="display:flex;justify-content:center;margin-top:10px">${coordSVG({xMin:-3,xMax:5,yMin:-3,yMax:5, lines:[{a:3,b:2,c:6,color:'#0891b2',label:'y=-1.5x+3'}]})}</div>`);
html += makeCard('rule', 'Особые случаи', '22.2', `
<p><b>1.</b> Если $b = 0, a \\ne 0$ — уравнение $ax = c$, т. е. $x = \\dfrac{c}{a}$. График — прямая, <b>параллельная оси $Oy$</b>.</p>
<div style="display:flex;justify-content:center;margin:10px 0">${coordSVG({xMin:-3,xMax:5,yMin:-3,yMax:5, lines:[{a:1,b:0,c:4,color:'#0891b2',label:'x=4'}]})}</div>
<p><b>2.</b> Если $a = 0, b \\ne 0$ — уравнение $by = c$, т. е. $y = \\dfrac{c}{b}$. График — прямая, <b>параллельная оси $Ox$</b>.</p>
<div style="display:flex;justify-content:center;margin:10px 0">${coordSVG({xMin:-3,xMax:5,yMin:-3,yMax:5, lines:[{k:0,b:2,color:'#0891b2',label:'y=2'}]})}</div>`);
html += makeCard('algo', 'Алгоритм построения', '22.3', `
<p>Чтобы построить график линейного уравнения, нужно:</p>
<ol style="padding-left:22px;line-height:1.85">
<li>выразить $y$ через $x$ (или $x$ через $y$);</li>
<li>найти координаты двух точек (например, при $x = 0$ и при $y = 0$);</li>
<li>отметить точки и провести через них прямую.</li>
</ol>
<p><b>Пример:</b> $2x + 3y = -6$. При $x = 3$: $y = -4$. При $x = -6$: $y = 2$. Соединяем точки $(3; -4)$ и $(-6; 2)$.</p>`);
html += '<div class="wg" id="p22-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">График уравнения $ax + by = c$ — слайдеры</div></div>'
+'<div class="wg-help">Подвигай ползунки $a, b, c$ и наблюдай, как меняется прямая.</div>'
+'<div class="sliders">'
+'<label>$a$ = <b id="p22-a-val">1</b><input type="range" id="p22-a-sl" min="-4" max="4" step="1" value="1"></label>'
+'<label>$b$ = <b id="p22-b-val">1</b><input type="range" id="p22-b-sl" min="-4" max="4" step="1" value="1"></label>'
+'<label>$c$ = <b id="p22-c-val">4</b><input type="range" id="p22-c-sl" min="-6" max="6" step="1" value="4"></label>'
+'</div>'
+'<div id="p22-iv1-svg" style="display:flex;justify-content:center"></div>'
+'<div id="p22-iv1-info" style="text-align:center;font-size:.92rem;margin-top:8px"></div></div>';
html += '<div class="wg" id="p22-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Принадлежит ли точка графику?</div></div>'
+'<div class="wg-help">Подставь координаты в уравнение и проверь.</div>'
+'<div class="score-display"><span>Задача <b id="p22-iv2-i">1</b> / 6</span><span>Очки: <b id="p22-iv2-s">0</b> / 6</span></div>'
+'<div id="p22-iv2-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.1rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:10px;justify-content:center"><button class="btn primary" id="p22-iv2-yes" style="background:#10b981;border-color:#10b981">Да</button><button class="btn primary" id="p22-iv2-no" style="background:#dc2626;border-color:#dc2626">Нет</button></div>'
+'<div class="feedback" id="p22-iv2-fb"></div></div>';
html += '<div class="wg" id="p22-iv3">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Точки пересечения с осями</div></div>'
+'<div class="wg-help">Найди точки пересечения графика с осями координат. Введи через запятую: <b>точка_на_Ox, точка_на_Oy</b>.</div>'
+trainerHTML('p22-iv3', 4, 'x₀, y₀')
+'</div>';
html += secNav('p21', 'p23') + readButton('p22');
box.innerHTML = html; renderMath(box);
/* IV1 */
(function(){
const aSl=document.getElementById('p22-a-sl'), bSl=document.getElementById('p22-b-sl'), cSl=document.getElementById('p22-c-sl');
const aV=document.getElementById('p22-a-val'), bV=document.getElementById('p22-b-val'), cV=document.getElementById('p22-c-val');
const out=document.getElementById('p22-iv1-svg'), info=document.getElementById('p22-iv1-info');
function update(){
const a=+aSl.value, b=+bSl.value, c=+cSl.value;
aV.textContent=a; bV.textContent=b; cV.textContent=c;
let infoText;
if(a===0 && b===0){
out.innerHTML = coordSVG({xMin:-5,xMax:5,yMin:-5,yMax:5, lines:[]});
infoText = c===0 ? 'Уравнение $0 = 0$ — выполняется при любых $x, y$ (вся плоскость).' : 'Уравнение $0 = '+c+'$ — невозможно, графика нет.';
} else if(a===0){
out.innerHTML = coordSVG({xMin:-5,xMax:5,yMin:-5,yMax:5, lines:[{k:0,b:c/b,color:'#0891b2'}]});
infoText = 'Прямая параллельна оси $Ox$: $y = '+(c/b).toFixed(2)+'$';
} else if(b===0){
out.innerHTML = coordSVG({xMin:-5,xMax:5,yMin:-5,yMax:5, lines:[{a:a,b:0,c:c,color:'#0891b2'}]});
infoText = 'Прямая параллельна оси $Oy$: $x = '+(c/a).toFixed(2)+'$';
} else {
out.innerHTML = coordSVG({xMin:-5,xMax:5,yMin:-5,yMax:5, lines:[{a:a,b:b,c:c,color:'#0891b2'}]});
infoText = 'Прямая: $y = '+(-a/b).toFixed(2)+'x + '+(c/b).toFixed(2)+'$';
}
info.innerHTML = '<b>'+a+'x + '+b+'y = '+c+'</b><br><span style="font-size:.85rem;color:var(--muted)">'+infoText+'</span>';
renderMath(info);
}
aSl.addEventListener('input', update); bSl.addEventListener('input', update); cSl.addEventListener('input', update); update();
})();
/* IV2 */
(function(){
const Q=[
{ e:'$A(4; -1)$ принадлежит $3x - 4y = 12$?', isOK:true },
{ e:'$B(4; 0)$ принадлежит $3x - 4y = 12$?', isOK:true },
{ e:'$C(2; -1{,}5)$ принадлежит $3x - 4y = 12$?', isOK:true },
{ e:'$D(0; -3)$ принадлежит $3x - 4y = 12$?', isOK:true },
{ e:'$M(1; 1)$ принадлежит $2x - y = 3$?', isOK:false },
{ e:'$K(0; 4)$ принадлежит $x + y = 4$?', isOK:true },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p22-iv2-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p22-iv2');bumpProgress('p22',22);} else if(score>=4){addXp(6,'p22-iv2');bumpProgress('p22',12);} return; }
document.getElementById('p22-iv2-i').textContent=(i+1);
document.getElementById('p22-iv2-s').textContent=score;
document.getElementById('p22-iv2-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p22-iv2-q'));
document.getElementById('p22-iv2-fb').style.display='none';
}
function ans(isYes){
if(i>=Q.length) return;
const fb=document.getElementById('p22-iv2-fb');
if(isYes===Q[i].isOK){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; Правильно: '+(Q[i].isOK?'<b>да</b>':'<b>нет</b>'));
document.getElementById('p22-iv2-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p22-iv2-yes').addEventListener('click',()=>ans(true));
document.getElementById('p22-iv2-no').addEventListener('click',()=>ans(false));
show();
})();
/* IV3 */
function parsePair(v){ return String(v).trim().replace(',','.').split(/[,;\s]+/).filter(Boolean); }
makeTrainer({
idPrefix:'p22-iv3',
parser:(v)=>v,
questions:[
{ q:'$x - y = 7$. Введи $x_0$ (при $y=0$), $y_0$ (при $x=0$)', a:(v)=>{const m=parsePair(v); return +m[0]===7 && +m[1]===-7;}, show:'7, -7' },
{ q:'$3x + y = 1$. Введи $x_0, y_0$', a:(v)=>{const m=parsePair(v); return Math.abs(+m[0]-(1/3))<1e-3 && +m[1]===1;}, show:'1/3, 1' },
{ q:'$2x - 4 = 0$ (т. е. $2x + 0y = 4$). Введи $x_0, y_0$ или «нет» если оси не пересекаются', a:(v)=>{const t=String(v).trim().toLowerCase(); if(t.includes('нет')||t==='—') return false; const m=parsePair(v); return +m[0]===2;}, show:'$x_0 = 2$, ось $Oy$ не пересекает' },
{ q:'$x + y = 5$. Введи $x_0, y_0$', a:(v)=>{const m=parsePair(v); return +m[0]===5 && +m[1]===5;}, show:'5, 5' },
],
onComplete:(s,n)=>{ if(s===n){addXp(12,'p22-iv3');bumpProgress('p22',22);} else if(s>=2){addXp(6,'p22-iv3');bumpProgress('p22',10);} }
});
wireReadBtn('p22');
}
function buildP23(){
const box = document.getElementById('p23-body');
let html = '';
html += makeCard('theory', 'Что такое система', '23.1', `
<p>Иногда нужно найти такие пары $(x; y)$, которые являются решением <b>одновременно двух</b> уравнений. Тогда говорят, что нужно решить <b>систему</b> уравнений.</p>
<p><b>Системой двух линейных уравнений с двумя переменными</b> называется запись вида:</p>
\\[\\begin{cases} a_1 x + b_1 y = c_1 \\\\ a_2 x + b_2 y = c_2 \\end{cases}\\]
<p>где $a_1, b_1, c_1, a_2, b_2, c_2$ — числа.</p>
<p><b>Решением системы</b> называется упорядоченная пара чисел $(x_0; y_0)$, являющаяся решением и первого, и второго уравнений.</p>
<p><b>Решить систему</b> — значит найти все её решения или доказать, что их нет.</p>`);
html += makeCard('example', 'Проверка решения', '23.2', `
<p>Проверим, является ли пара $(3; 2)$ решением системы:</p>
\\[\\begin{cases} 2x + y = 8 \\\\ 3x - 2y = 5 \\end{cases}\\]
<p>Подставим в каждое уравнение:</p>
<p>$\\;2 \\cdot 3 + 2 = 8$ — <b>верно</b>;</p>
<p>$\\;3 \\cdot 3 - 2 \\cdot 2 = 5$ — <b>верно</b>.</p>
<p>Значит, $(3; 2)$ — решение системы.</p>`);
html += makeCard('theory', 'Графический смысл', '23.3', `
<p>Графиком каждого уравнения системы является <b>прямая</b>. Решение системы — это координаты <b>общих точек</b> этих двух прямых.</p>
<p>Возможны <b>три случая</b>:</p>
<ul style="padding-left:22px;line-height:1.85">
<li><b>Прямые пересекаются</b> — одно решение (общая точка).</li>
<li><b>Прямые параллельны</b> — общих точек нет, <b>решений нет</b>.</li>
<li><b>Прямые совпадают</b> — <b>бесконечно много</b> решений.</li>
</ul>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:10px;margin-top:12px">
<div style="text-align:center"><div style="font-size:.8rem;color:var(--muted);margin-bottom:4px">Пересекаются</div>${coordSVG({width:260,height:220,xMin:-4,xMax:6,yMin:-3,yMax:5,lines:[{a:1,b:1,c:3,color:'#0891b2'},{a:2,b:-1,c:2,color:'#dc2626'}],points:[{x:5/3,y:4/3,label:'(...)',color:'#10b981'}]})}<div style="font-size:.78rem;margin-top:4px">Одно решение</div></div>
<div style="text-align:center"><div style="font-size:.8rem;color:var(--muted);margin-bottom:4px">Параллельны</div>${coordSVG({width:260,height:220,xMin:-4,xMax:6,yMin:-3,yMax:5,lines:[{a:1,b:1,c:3,color:'#0891b2'},{a:1,b:1,c:0,color:'#dc2626'}]})}<div style="font-size:.78rem;margin-top:4px">Нет решений</div></div>
<div style="text-align:center"><div style="font-size:.8rem;color:var(--muted);margin-bottom:4px">Совпадают</div>${coordSVG({width:260,height:220,xMin:-4,xMax:6,yMin:-3,yMax:5,lines:[{a:1,b:1,c:3,color:'#0891b2'},{a:2,b:2,c:6,color:'#dc2626'}]})}<div style="font-size:.78rem;margin-top:4px">$\\infty$ решений</div></div>
</div>`);
html += '<div class="wg" id="p23-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="p23-iv1-i">1</b> / 5</span><span>Очки: <b id="p23-iv1-s">0</b> / 5</span></div>'
+'<div id="p23-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="p23-iv1-yes" style="background:#10b981;border-color:#10b981">Да</button><button class="btn primary" id="p23-iv1-no" style="background:#dc2626;border-color:#dc2626">Нет</button></div>'
+'<div class="feedback" id="p23-iv1-fb"></div></div>';
html += '<div class="wg" id="p23-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Сколько решений у системы?</div></div>'
+'<div class="wg-help">Сравни прямые: пересекаются, параллельны или совпадают.</div>'
+'<div class="score-display"><span>Задача <b id="p23-iv2-i">1</b> / 6</span><span>Очки: <b id="p23-iv2-s">0</b> / 6</span></div>'
+'<div id="p23-iv2-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="p23-iv2-one" style="background:#10b981;border-color:#10b981">Одно</button>'
+'<button class="btn primary" id="p23-iv2-none" style="background:#dc2626;border-color:#dc2626">Нет</button>'
+'<button class="btn primary" id="p23-iv2-inf" style="background:#7c3aed;border-color:#7c3aed">Бесконечно</button>'
+'</div><div class="feedback" id="p23-iv2-fb"></div></div>';
html += secNav('p22', 'p24') + readButton('p23');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$\\begin{cases}2x+y=8\\\\3x-2y=5\\end{cases}$, пара $(3; 2)$', isOK:true },
{ e:'$\\begin{cases}6x+2y=12\\\\8x-y=5\\end{cases}$, пара $(1; 3)$', isOK:true },
{ e:'$\\begin{cases}6x+2y=12\\\\8x-y=5\\end{cases}$, пара $(-2; 6)$', isOK:false },
{ e:'$\\begin{cases}x+y=60\\\\x-y=20\\end{cases}$, пара $(40; 20)$', isOK:true },
{ e:'$\\begin{cases}x+y=3\\\\2x-y=5\\end{cases}$, пара $(2; 1)$', isOK:false },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p23-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p23-iv1');bumpProgress('p23',22);} else if(score>=3){addXp(6,'p23-iv1');bumpProgress('p23',12);} return; }
document.getElementById('p23-iv1-i').textContent=(i+1);
document.getElementById('p23-iv1-s').textContent=score;
document.getElementById('p23-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p23-iv1-q'));
document.getElementById('p23-iv1-fb').style.display='none';
}
function ans(isYes){
if(i>=Q.length) return;
const fb=document.getElementById('p23-iv1-fb');
if(isYes===Q[i].isOK){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; Правильно: '+(Q[i].isOK?'<b>да</b>':'<b>нет</b>'));
document.getElementById('p23-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p23-iv1-yes').addEventListener('click',()=>ans(true));
document.getElementById('p23-iv1-no').addEventListener('click',()=>ans(false));
show();
})();
(function(){
/* Compare k1, k2, b1, b2 для y=kx+b */
const Q=[
{ e:'$\\begin{cases}x+y=4\\\\x-y=6\\end{cases}$', ans:'one' },
{ e:'$\\begin{cases}3x-y=2\\\\-6x+2y=3\\end{cases}$', ans:'none' },
{ e:'$\\begin{cases}x-3y=5\\\\2x+y=4\\end{cases}$', ans:'one' },
{ e:'$\\begin{cases}2x-0{,}5y=4\\\\-x+0{,}25y=-2\\end{cases}$', ans:'inf' },
{ e:'$\\begin{cases}x+2y=5\\\\-x-2y=5\\end{cases}$', ans:'none' },
{ e:'$\\begin{cases}x+2y=5\\\\2x+4y=10\\end{cases}$', ans:'inf' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p23-iv2-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p23-iv2');bumpProgress('p23',28);} else if(score>=4){addXp(8,'p23-iv2');bumpProgress('p23',16);} return; }
document.getElementById('p23-iv2-i').textContent=(i+1);
document.getElementById('p23-iv2-s').textContent=score;
document.getElementById('p23-iv2-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p23-iv2-q'));
document.getElementById('p23-iv2-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p23-iv2-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={one:'одно решение',none:'нет решений',inf:'бесконечно много'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p23-iv2-s').textContent=score;
i++; setTimeout(show,1200);
}
document.getElementById('p23-iv2-one').addEventListener('click',()=>ans('one'));
document.getElementById('p23-iv2-none').addEventListener('click',()=>ans('none'));
document.getElementById('p23-iv2-inf').addEventListener('click',()=>ans('inf'));
show();
})();
wireReadBtn('p23');
}
function buildP24(){
const box = document.getElementById('p24-body');
let html = '';
html += makeCard('algo', 'Способ подстановки', '24.1', `
<p>Чтобы решить систему способом подстановки:</p>
<ol style="padding-left:22px;line-height:1.85">
<li>из одного уравнения выразить одну переменную через другую;</li>
<li>подставить это выражение во второе уравнение;</li>
<li>решить полученное уравнение с одной переменной;</li>
<li>найденное значение подставить в выражение из шага 1;</li>
<li>записать ответ — упорядоченную пару $(x_0; y_0)$.</li>
</ol>`);
html += makeCard('example', 'Подстановка — пример', '24.2', `
<p>Решить $\\begin{cases} 2x + y = 7 \\\\ 3x - 4y = 5 \\end{cases}$ способом подстановки.</p>
<p><b>1.</b> Из первого: $y = 7 - 2x$.</p>
<p><b>2.</b> Подставим во второе: $3x - 4(7 - 2x) = 5$.</p>
<p><b>3.</b> Решим: $3x - 28 + 8x = 5 \\Rightarrow 11x = 33 \\Rightarrow x = 3$.</p>
<p><b>4.</b> $y = 7 - 2 \\cdot 3 = 1$.</p>
<p><b>Ответ:</b> $(3; 1)$.</p>`);
html += makeCard('algo', 'Способ сложения', '24.3', `
<p>Чтобы решить систему способом сложения:</p>
<ol style="padding-left:22px;line-height:1.85">
<li>если коэффициенты при одной из переменных <b>не противоположны</b>, умножить уравнения на множители так, чтобы они стали противоположными;</li>
<li>сложить уравнения почленно — одна переменная исчезает;</li>
<li>решить получившееся уравнение;</li>
<li>подставить значение в одно из исходных уравнений и найти вторую переменную;</li>
<li>записать ответ.</li>
</ol>`);
html += makeCard('example', 'Сложение — пример', '24.4', `
<p>Решить $\\begin{cases} 2x + 5y = 16 \\\\ 3x - 2y = 5 \\end{cases}$ способом сложения.</p>
<p>Умножим 1-е на $2$, 2-е на $5$:</p>
<p>$\\begin{cases} 4x + 10y = 32 \\\\ 15x - 10y = 25 \\end{cases}$</p>
<p>Сложим: $19x = 57 \\Rightarrow x = 3$.</p>
<p>Подставим $x = 3$ в первое: $2 \\cdot 3 + 5y = 16 \\Rightarrow 5y = 10 \\Rightarrow y = 2$.</p>
<p><b>Ответ:</b> $(3; 2)$.</p>`);
html += '<div class="wg" id="p24-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Тренажёр способа подстановки</div></div>'
+'<div class="wg-help">Реши систему и введи через запятую: <b>x, y</b>.</div>'
+trainerHTML('p24-iv1', 5, 'x, y')
+'</div>';
html += '<div class="wg" id="p24-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Тренажёр способа сложения</div></div>'
+'<div class="wg-help">Реши систему и введи через запятую: <b>x, y</b>.</div>'
+trainerHTML('p24-iv2', 5, 'x, y')
+'</div>';
html += '<div class="wg" id="p24-iv3">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Какой способ удобнее?</div></div>'
+'<div class="wg-help">Какой способ — подстановка или сложение — быстрее приведёт к ответу?</div>'
+'<div class="score-display"><span>Задача <b id="p24-iv3-i">1</b> / 5</span><span>Очки: <b id="p24-iv3-s">0</b> / 5</span></div>'
+'<div id="p24-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:10px;justify-content:center"><button class="btn primary" id="p24-iv3-sub" style="background:#0891b2;border-color:#0891b2">Подстановка</button><button class="btn primary" id="p24-iv3-add" style="background:#7c3aed;border-color:#7c3aed">Сложение</button></div>'
+'<div class="feedback" id="p24-iv3-fb"></div></div>';
html += secNav('p23', 'p25') + readButton('p24');
box.innerHTML = html; renderMath(box);
function parsePair(v){ return String(v).trim().replace(/,/g,' ').split(/[;\s]+/).filter(Boolean).map(s=>parseFloat(s.replace(',','.'))); }
makeTrainer({
idPrefix:'p24-iv1',
parser:(v)=>v,
questions:[
{ q:'$\\begin{cases}x = 4 + 5y \\\\ 4x - 3y = -1\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===-1 && m[1]===-1;}, show:'(-1; -1)' },
{ q:'$\\begin{cases}y = 3x - 3 \\\\ 3x - 2y = 0\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===2 && m[1]===3;}, show:'(2; 3)' },
{ q:'$\\begin{cases}x + y = 7 \\\\ 5x - 3y = 11\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===4 && m[1]===3;}, show:'(4; 3)' },
{ q:'$\\begin{cases}x - 3y = 6 \\\\ 2x - 5y = -4\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===-42 && m[1]===-16;}, show:'(-42; -16)' },
{ q:'$\\begin{cases}3x + y = 7 \\\\ 9x - 4y = -7\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===1 && m[1]===4;}, show:'(1; 4)' },
],
onComplete:(s,n)=>{ if(s===n){addXp(20,'p24-iv1');bumpProgress('p24',30);} else if(s>=3){addXp(10,'p24-iv1');bumpProgress('p24',18);} }
});
makeTrainer({
idPrefix:'p24-iv2',
parser:(v)=>v,
questions:[
{ q:'$\\begin{cases}3x - 2y = 5 \\\\ 2x + 5y = 16\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===3 && m[1]===2;}, show:'(3; 2)' },
{ q:'$\\begin{cases}2x + y = 13 \\\\ 3x - y = 2\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===3 && m[1]===7;}, show:'(3; 7)' },
{ q:'$\\begin{cases}4x + 7y = 40 \\\\ -4x + 9y = 24\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===3 && m[1]===4;}, show:'(3; 4)' },
{ q:'$\\begin{cases}5x - 2y = 3 \\\\ 7x + 2y = 9\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===1 && m[1]===1;}, show:'(1; 1)' },
{ q:'$\\begin{cases}9x + 4y = 8 \\\\ 5x + 2y = 3\\end{cases}$', a:(v)=>{const m=parsePair(v); return m[0]===-2 && Math.abs(m[1]-6.5)<1e-3;}, show:'(-2; 6,5)' },
],
onComplete:(s,n)=>{ if(s===n){addXp(20,'p24-iv2');bumpProgress('p24',30);} else if(s>=3){addXp(10,'p24-iv2');bumpProgress('p24',18);} }
});
(function(){
const Q=[
{ e:'$\\begin{cases}x = 4 + 5y \\\\ 4x - 3y = -1\\end{cases}$', best:'sub', why:'$x$ уже выражен' },
{ e:'$\\begin{cases}3x - 2y = 5 \\\\ 2x + 5y = 16\\end{cases}$', best:'add', why:'нет легко выражаемой переменной' },
{ e:'$\\begin{cases}y = 3x - 3 \\\\ 3x - 2y = 0\\end{cases}$', best:'sub', why:'$y$ уже выражен' },
{ e:'$\\begin{cases}2x + y = 13 \\\\ 3x - y = 2\\end{cases}$', best:'add', why:'противоположные коэф. при $y$ — сразу сложить' },
{ e:'$\\begin{cases}9x + 4y = 8 \\\\ 5x + 2y = 3\\end{cases}$', best:'add', why:'умножь 2-е на $-2$ — получишь противоположные коэф.' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p24-iv3-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p24-iv3');bumpProgress('p24',20);} else if(score>=3){addXp(6,'p24-iv3');bumpProgress('p24',10);} return; }
document.getElementById('p24-iv3-i').textContent=(i+1);
document.getElementById('p24-iv3-s').textContent=score;
document.getElementById('p24-iv3-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p24-iv3-q'));
document.getElementById('p24-iv3-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p24-iv3-fb');
if(a===Q[i].best){ score++; feedback(fb,true,'&#10003; Верно! ('+Q[i].why+')'); }
else feedback(fb,false,'&#10007; Лучше — <b>'+(Q[i].best==='sub'?'подстановка':'сложение')+'</b>: '+Q[i].why);
document.getElementById('p24-iv3-s').textContent=score;
i++; setTimeout(show,1400);
}
document.getElementById('p24-iv3-sub').addEventListener('click',()=>ans('sub'));
document.getElementById('p24-iv3-add').addEventListener('click',()=>ans('add'));
show();
})();
wireReadBtn('p24');
}
function buildP25(){
const box = document.getElementById('p25-body');
let html = '';
html += makeCard('algo', 'Алгоритм решения задачи через систему', '25.1', `
<p>Чтобы решить задачу с помощью системы двух уравнений:</p>
<ol style="padding-left:22px;line-height:1.85">
<li>выяснить, о каких величинах идёт речь — что известно, что найти;</li>
<li>обозначить <b>две</b> неизвестные величины через $x$ и $y$;</li>
<li>составить <b>два уравнения</b> по условию задачи;</li>
<li>решить полученную систему;</li>
<li>записать ответ в соответствии с условием задачи (проверь смысл!).</li>
</ol>`);
html += makeCard('example', 'Задача про кофе и чай', '25.2', `
<p><b>Условие:</b> чашка кофе стоит 5 р., чашка чая — 3 р. За день продали 120 чашек кофе и чая и получили 500 р. Сколько чашек чего продали?</p>
<p><b>Решение:</b> пусть продали $x$ чашек кофе и $y$ чашек чая. Получим систему:</p>
<p>$\\begin{cases} x + y = 120 \\\\ 5x + 3y = 500 \\end{cases}$</p>
<p>Из первого: $y = 120 - x$. Подставим: $5x + 3(120 - x) = 500 \\Rightarrow 2x = 140 \\Rightarrow x = 70$. Тогда $y = 50$.</p>
<p><b>Ответ:</b> 70 чашек кофе и 50 чашек чая.</p>`);
html += makeCard('example', 'Задача про растворы', '25.3', `
<p><b>Условие:</b> сколько граммов 10%-го и 15%-го растворов соли нужно смешать, чтобы получить 100 г 12%-го раствора?</p>
<p>Пусть нужно $x$ г 10%-го и $y$ г 15%-го раствора. По массе: $x + y = 100$. По массе соли: $0{,}1x + 0{,}15y = 12$.</p>
<p>$\\begin{cases} x + y = 100 \\\\ 0{,}1x + 0{,}15y = 12 \\end{cases}$</p>
<p>Из 1-го $y = 100 - x$, подставим во 2-е: $0{,}1x + 0{,}15(100 - x) = 12 \\Rightarrow -0{,}05x = -3 \\Rightarrow x = 60$, $y = 40$.</p>
<p><b>Ответ:</b> 60 г 10%-го раствора и 40 г 15%-го раствора.</p>`);
html += makeCard('example', 'Задача про монеты', '25.4', `
<p><b>Условие:</b> кассир разменял 50-руб. купюру на 5-руб. купюры и 1-руб. монеты, всего 22 знака. Сколько купюр и монет?</p>
<p>Пусть $x$ — число 5-руб. купюр, $y$ — число 1-руб. монет.</p>
<p>$\\begin{cases} x + y = 22 \\\\ 5x + y = 50 \\end{cases}$</p>
<p>Вычтем из 2-го 1-е: $4x = 28 \\Rightarrow x = 7$, $y = 15$.</p>
<p><b>Ответ:</b> 7 купюр и 15 монет.</p>`);
html += '<div class="wg" id="p25-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Тренажёр текстовых задач</div></div>'
+'<div class="wg-help">Составь систему в уме и реши. Введи только ответ — через запятую <b>x, y</b> (или одно число, если в задаче спросили одно).</div>'
+trainerHTML('p25-iv1', 6, 'x, y или число')
+'</div>';
html += '<div class="wg" id="p25-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Какая система описывает задачу?</div></div>'
+'<div class="wg-help">Выбери систему, которая правильно моделирует условие.</div>'
+'<div class="score-display"><span>Задача <b id="p25-iv2-i">1</b> / 4</span><span>Очки: <b id="p25-iv2-s">0</b> / 4</span></div>'
+'<div id="p25-iv2-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1rem;margin-bottom:10px"></div>'
+'<div id="p25-iv2-opts" style="display:grid;gap:8px"></div>'
+'<div class="feedback" id="p25-iv2-fb"></div></div>';
html += secNav('p24', 'final4') + readButton('p25');
box.innerHTML = html; renderMath(box);
function parsePair(v){ return String(v).trim().replace(/,/g,' ').split(/[;\s]+/).filter(Boolean).map(s=>parseFloat(s.replace(',','.'))); }
makeTrainer({
idPrefix:'p25-iv1',
parser:(v)=>v,
questions:[
{ q:'2 кг груш и 1 кг яблок — 10 р. 5 кг груш дороже 3 кг яблок на 14 р. Введи: <b>цена груш, цена яблок</b>',
a:(v)=>{const m=parsePair(v); return m[0]===4 && m[1]===2;}, show:'4 р., 2 р.' },
{ q:'В копилке двухрублёвые и пятирублёвые. 5-рубл. на 32 меньше двухр., всего 120 р. Сколько двухрублёвых?',
a:(v)=>{const m=parsePair(v); return m[0]===40;}, show:'40 двухрублёвых' },
{ q:'2 карандаша и 3 тетради — 1 р. 40 к. 3 карандаша и 2 тетради — 1 р. 35 к. Сколько (коп.) стоят 5 карандашей и 6 тетрадей?',
a:(v)=>{const m=parsePair(v); return m[0]===325;}, show:'325 к.' },
{ q:'15 школьников купили 40 сувениров: девочки по 2, мальчики по 3. Сколько мальчиков, сколько девочек? Введи <b>дев., мал.</b>',
a:(v)=>{const m=parsePair(v); return m[0]===5 && m[1]===10;}, show:'5 дев., 10 мал.' },
{ q:'На двух полках 210 книг. Если с верхней убрать половину, а на нижнюю удвоить, станет 180. Сколько было на нижней?',
a:(v)=>{const m=parsePair(v); return m[0]===60;}, show:'60 на нижней (150 на верхней)' },
{ q:'Сумма двух чисел 80,5; 40% одного равны 75% другого. Найди меньшее.',
a:(v)=>{const m=parsePair(v); return Math.abs(m[0]-28)<.1;}, show:'28' },
],
onComplete:(s,n)=>{ if(s===n){addXp(20,'p25-iv1');bumpProgress('p25',35);} else if(s>=3){addXp(10,'p25-iv1');bumpProgress('p25',18);} }
});
(function(){
const Q=[
{ task:'На двух улицах 117 домов. На первой в 2 раза меньше, чем на второй. Пусть $x$ — число домов на первой улице. Какое уравнение составить?',
opts:[
{ e:'$2x - x = 117$', ok:false },
{ e:'$2x + x = 117$', ok:true },
{ e:'$x + \\dfrac{x}{2} = 117$', ok:false },
{ e:'$2x = 117$', ok:false },
]
},
{ task:'Два карандаша и три тетради — 1 р. 40 к. Два тетради и три карандаша — 1 р. 35 к. Пусть карандаш — $x$ к., тетрадь — $y$ к. Какая система?',
opts:[
{ e:'$\\begin{cases}2x+3y=140\\\\3x+2y=135\\end{cases}$', ok:true },
{ e:'$\\begin{cases}3x+2y=140\\\\2x+3y=135\\end{cases}$', ok:false },
{ e:'$\\begin{cases}2x+3y=1{,}40\\\\3x+2y=1{,}35\\end{cases}$', ok:false },
]
},
{ task:'В клетках 20 кроликов и цыплят, у них 58 ног. Пусть $x$ — кролики, $y$ — цыплята. Какая система?',
opts:[
{ e:'$\\begin{cases}x+y=20\\\\2x+4y=58\\end{cases}$', ok:false },
{ e:'$\\begin{cases}x+y=20\\\\4x+2y=58\\end{cases}$', ok:true },
{ e:'$\\begin{cases}x-y=20\\\\4x+2y=58\\end{cases}$', ok:false },
]
},
{ task:'В двух коробках 300 карандашей. Если в первой уменьшить вдвое, во второй удвоить — станет 240. Пусть $x, y$ — карандаши. Какая система?',
opts:[
{ e:'$\\begin{cases}x+y=300\\\\\\tfrac{x}{2}+2y=240\\end{cases}$', ok:true },
{ e:'$\\begin{cases}x-y=300\\\\2x+\\tfrac{y}{2}=240\\end{cases}$', ok:false },
{ e:'$\\begin{cases}x+y=300\\\\2x+\\tfrac{y}{2}=240\\end{cases}$', ok:false },
]
},
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p25-iv2-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p25-iv2');bumpProgress('p25',22);} else if(score>=2){addXp(6,'p25-iv2');bumpProgress('p25',12);} document.getElementById('p25-iv2-opts').innerHTML=''; return; }
document.getElementById('p25-iv2-i').textContent=(i+1);
document.getElementById('p25-iv2-s').textContent=score;
document.getElementById('p25-iv2-q').innerHTML=Q[i].task;
const opts=document.getElementById('p25-iv2-opts');
opts.innerHTML='';
Q[i].opts.forEach((o,j)=>{
const b=document.createElement('button');
b.className='btn';
b.style.cssText='padding:10px;text-align:left;display:block;width:100%';
b.innerHTML='<span style="color:var(--muted);margin-right:8px">'+(j+1)+')</span>'+o.e;
b.addEventListener('click', ()=>{
const fb=document.getElementById('p25-iv2-fb');
if(o.ok){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; Неверно. Правильный — другой.');
document.getElementById('p25-iv2-s').textContent=score;
i++; setTimeout(show,1300);
});
opts.appendChild(b);
});
renderMath(document.getElementById('p25-iv2-q'));
renderMath(opts);
document.getElementById('p25-iv2-fb').style.display='none';
}
show();
})();
wireReadBtn('p25');
}
const BOSSES_CH4 = [
{
n:1, title:'Босс \xA721 — Линейное уравнение с 2 переменными', color:'#0891b2',
steps:[
{ q:'Является ли пара $(1; 2)$ решением $10x + y = 12$? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'$10 + 2 = 12$.' },
{ q:'Из $x + 4y = 8$ выразить $y$ через $x$: $y = ?x + ?$. Введи свободный член (без $x$).', verify:(v)=>+v===2, hint:'$y = (8 - x)/4 = -x/4 + 2$.' },
{ q:'Сколько решений у $ax + by = c$ (при $a, b \\ne 0$)?', verify:(v)=>{const t=String(v).trim().toLowerCase(); return t.includes('бесконеч')||t.includes('много');}, hint:'Бесконечно много.' },
{ q:'$2x + y = 7$, $x = 1$. Найди $y$.', verify:(v)=>+v===5, hint:'$y = 7 - 2 = 5$.' },
{ q:'Пара $(3; -20)$ — решение $10x + y = 12$? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('н'), hint:'$30 - 20 = 10 \\ne 12$.' },
]
},
{
n:2, title:'Босс \xA722 — График уравнения', color:'#06b6d4',
steps:[
{ q:'График $ax + by = c$ — это всегда что? «прямая», «парабола» или «окружность»', verify:(v)=>String(v).trim().toLowerCase().startsWith('прям'), hint:'Линейное уравнение — линейная функция.' },
{ q:'$0 \\cdot x + 3y = 6$ — какова прямая? Введи «гориз» или «верт»', verify:(v)=>String(v).trim().toLowerCase().startsWith('гор'), hint:'$y = 2$ — параллельна оси $Ox$.' },
{ q:'$5x + 0 \\cdot y = 20$ — какова прямая? «гориз» или «верт»', verify:(v)=>String(v).trim().toLowerCase().startsWith('верт'), hint:'$x = 4$ — параллельна оси $Oy$.' },
{ q:'$x - y = 7$. Найди $x_0$ (точка на $Ox$).', verify:(v)=>+v===7, hint:'$y = 0 \\Rightarrow x = 7$.' },
{ q:'$3x + y = 1$. Найди $y_0$ (точка на $Oy$).', verify:(v)=>+v===1, hint:'$x = 0 \\Rightarrow y = 1$.' },
]
},
{
n:3, title:'Босс \xA723 — Система уравнений', color:'#2563eb',
steps:[
{ q:'Решение системы — это пара чисел или одно число? «пара» или «число»', verify:(v)=>String(v).trim().toLowerCase().startsWith('пар'), hint:'$(x_0; y_0)$ — два числа.' },
{ q:'Прямые параллельны. Сколько решений у системы? («одно», «нет», «бесконечно»)', verify:(v)=>String(v).trim().toLowerCase().startsWith('нет'), hint:'У параллельных прямых нет общих точек.' },
{ q:'Прямые пересекаются. Сколько решений?', verify:(v)=>String(v).trim().toLowerCase().startsWith('одн'), hint:'Одно — точка пересечения.' },
{ q:'Прямые совпадают. Сколько решений?', verify:(v)=>{const t=String(v).trim().toLowerCase(); return t.includes('бесконеч')||t.includes('много');}, hint:'Бесконечно много.' },
{ q:'Является ли $(1; 3)$ решением $\\begin{cases}6x+2y=12\\\\8x-y=5\\end{cases}$? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'$6+6=12$, $8-3=5$.' },
]
},
{
n:4, title:'Босс \xA724 — Способы решения', color:'#7c3aed',
steps:[
{ q:'$\\begin{cases}x + y = 7 \\\\ 5x - 3y = 11\\end{cases}$ — найди $x$', verify:(v)=>+v===4, hint:'Из 1-го $y = 7 - x$, подставь.' },
{ q:'(та же система) — найди $y$', verify:(v)=>+v===3, hint:'$y = 7 - 4 = 3$.' },
{ q:'$\\begin{cases}2x + y = 13 \\\\ 3x - y = 2\\end{cases}$ — найди $x$ (способ сложения)', verify:(v)=>+v===3, hint:'Сложи: $5x = 15$.' },
{ q:'$\\begin{cases}2x + y = 13 \\\\ 3x - y = 2\\end{cases}$ — найди $y$', verify:(v)=>+v===7, hint:'$y = 13 - 2 \\cdot 3 = 7$.' },
{ q:'$\\begin{cases}5x - 2y = 3 \\\\ 7x + 2y = 9\\end{cases}$ — найди $x$', verify:(v)=>+v===1, hint:'Сложи: $12x = 12$.' },
]
},
{
n:5, title:'Финальный босс — Текстовые задачи (системы)', color:'#db2777',
steps:[
{ q:'$x + y = 120$, $5x + 3y = 500$ — найди $x$ (число чашек кофе)', verify:(v)=>+v===70, hint:'Подстановка $y = 120 - x$.' },
{ q:'(та же) — найди $y$ (число чашек чая)', verify:(v)=>+v===50, hint:'$y = 120 - 70 = 50$.' },
{ q:'Сумма двух чисел 80, разность 20. Найди большее.', verify:(v)=>+v===50, hint:'$x + y = 80$, $x - y = 20$, сложить.' },
{ q:'В клетках 20 кроликов и цыплят, у них 58 ног. Сколько кроликов?', verify:(v)=>+v===9, hint:'Кролики 4 ноги, цыплята 2. Система $x+y=20$, $4x+2y=58$.' },
{ q:'Купили 50-руб. на 5-руб. купюры и 1-руб. монеты, всего 22 знака. Сколько 5-рублёвок?', verify:(v)=>+v===7, hint:'$x+y=22$, $5x+y=50$. Вычти.' },
]
},
];
function buildFinal4(){
const box = document.getElementById('final4-body');
let html = '';
html += makeCard('theory', 'Что мы изучили', 'Итог', `
<p>В этой главе мы:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>познакомились с <b>линейным уравнением с двумя переменными</b> $ax + by = c$ — у него <b>бесконечно много</b> решений;</li>
<li>научились строить <b>график</b> такого уравнения — это <b>прямая</b>;</li>
<li>узнали, что <b>система</b> двух уравнений — это <b>две прямые</b>: одно решение, нет решений или бесконечно много;</li>
<li>освоили <b>два способа решения</b>: подстановка и сложение;</li>
<li>научились <b>составлять системы</b> для решения текстовых задач.</li>
</ul>
<p>Это <b>финальная глава Алгебры 7</b>! Пройди 5 боссов — и весь курс будет в твоём арсенале.</p>`);
html += '<div id="bosses-container-ch4"></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-ch4" 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-ch4" style="width:0%;background:linear-gradient(90deg,#0891b2,#22d3ee)"></div></div>'
+'</div>';
html += secNav('p25', null);
box.innerHTML = html; renderMath(box);
const cont = document.getElementById('bosses-container-ch4');
const BOSS_STATE = (function(){
try{ const s=localStorage.getItem('algebra7_ch4_bosses'); if(s) return JSON.parse(s); }catch(e){}
return BOSSES_CH4.map(()=>({stage:0,defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem('algebra7_ch4_bosses', JSON.stringify(BOSS_STATE)); }catch(e){} }
function refreshOverall(){
const won=BOSS_STATE.filter(b=>b.defeated).length;
const txt=document.getElementById('boss-overall-ch4'); if(txt) txt.textContent=won+' / '+BOSSES_CH4.length+' боссов побеждено';
const fill=document.getElementById('boss-overall-fill-ch4'); if(fill) fill.style.width=(won*100/BOSSES_CH4.length)+'%';
if(won>=BOSSES_CH4.length){ bumpProgress('final4',60); achievement('ch4_done','Алгебра 7 — пройдена полностью!'); }
}
cont.innerHTML = BOSSES_CH4.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_CH4.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('final4',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>