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

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

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

1318 lines
97 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Геометрия 7 · Глава 3 · Параллельность прямых</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/geom7_svg.js?v=6" 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; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
--pri:#7c3aed; --pri2:#6d28d9; --pri-soft:#ede9fe;
--acc:#a855f7; --acc2:#9333ea; --acc-soft:#f3e8ff;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0c0a14; --card:#120e1c; --card-soft:#171125; --text:#f3e8ff; --muted:#9d8db5; --border:#2a1f3d}
*{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,#4c1d95 0%,#7c3aed 55%,#a855f7 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(196,181,253,.2);min-height:130px}
.hdr::before{content:'ГЛАВА 3';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(232,220,255,.12);line-height:1;pointer-events: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:'\2225';position:absolute;right:-10px;top:-20px;font-size:clamp(2rem,8vw,5.5rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px}
.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)}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(124,58,237,.32)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(124,58,237,.18);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;font-family:'Unbounded',sans-serif}
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(124,58,237,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,#fff5e1,#fef3c7)}
.sec[id="sec-p15"] { --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec[id="sec-p16"] { --sec-acc:#a855f7; --sec-acc-d:#9333ea; --sec-acc-soft:#f3e8ff; }
.sec[id="sec-p17"] { --sec-acc:#c026d3; --sec-acc-d:#a21caf; --sec-acc-soft:#fae8ff; }
.sec[id="sec-p18"] { --sec-acc:#db2777; --sec-acc-d:#9d174d; --sec-acc-soft:#fce7f3; }
.sec[id="sec-final3"]{ --sec-acc:#7c3aed; --sec-acc-d:#6d28d9; --sec-acc-soft:#ede9fe; }
.sec{display:none;position:relative;animation:fadeIn .35s ease}
.sec.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;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.55rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));line-height:1.25}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(124,58,237,.06);position:relative;z-index:1}
.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.theory{background:#8b5cf6}.card-icon.rule{background:#ec4899}.card-icon.algo{background:#f59e0b}.card-icon.example{background:#10b981}
.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}
.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}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.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);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}
@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(124,58,237,.18);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--pri));border-radius:6px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.xp-nums{font-size:.74rem;color:var(--muted);display:flex;justify-content:space-between}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--sec-acc-soft,var(--pri-soft));font-weight:700;cursor:pointer;font-size:.88rem;color:var(--sec-acc-d,var(--pri2));list-style:none;display:flex;align-items:center;gap:8px}
.spoiler summary::-webkit-details-marker{display:none}
.spoiler summary::before{content:'+';font-size:1.2rem;font-weight:900;color:var(--sec-acc,var(--pri));width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.sec-nav{display:flex;gap:10px;margin-top:24px;padding-top:20px;border-top:1px solid var(--border);justify-content:space-between;flex-wrap:wrap}
.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}
.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,#7c3aed,#a855f7);color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(124,58,237,.45);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.dnd-pool{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px;padding:10px;border:1.5px dashed var(--border);border-radius:10px;min-height:54px}
.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:pointer;user-select:none;font-size:.92rem}
.dnd-chip:hover{border-color:var(--sec-acc,var(--pri))}
.dnd-chip.armed{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));box-shadow:0 0 0 3px rgba(124,58,237,.22)}
.dnd-chip.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;cursor:pointer}
.drop-box{background:var(--card);border:1.5px dashed var(--border);border-radius:10px;padding:10px;min-height:90px}
.drop-box.over{border-color:var(--sec-acc,var(--pri));background:var(--sec-acc-soft,var(--pri-soft));border-style:solid}
.drop-box h5{font-family:'Unbounded',sans-serif;font-size:.78rem;color:var(--sec-acc-d,var(--pri2));margin-bottom:8px;text-transform:uppercase}
.drop-items{display:flex;flex-wrap:wrap;gap:6px;min-height:32px}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--sec-acc-soft,var(--pri-soft));border-radius:10px;margin-bottom:12px}
.score-display b{color:var(--sec-acc-d,var(--pri2));font-size:1.15rem}
.hp-boss{height:14px;background:rgba(220,38,38,.12);border-radius:9px;overflow:hidden;border:1px solid #fecaca;margin:8px 0}
.hp-boss-fill{height:100%;background:linear-gradient(90deg,#dc2626,#f59e0b);border-radius:9px;transition:width .5s cubic-bezier(.4,0,.2,1)}
.col-side-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.42);z-index:9990;display:none}
.col-side-backdrop.show{display:block}
@media(min-width:981px){#sidebar-btn{display:none}.col-side-backdrop.show{display:none}}
@media(max-width:980px){
.col-side{position:fixed;top:0;right:0;height:100vh;width:300px;max-width:88vw;background:var(--bg);box-shadow:-12px 0 24px rgba(0,0,0,.18);padding:18px 16px;overflow-y:auto;transform:translateX(100%);transition:transform .25s ease;z-index:9991;max-height:none}
.col-side.open{transform:none}
}
.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)}
.svg-host{display:flex;justify-content:center;margin:12px 0}
.svg-host-row{display:flex;gap:8px;flex-wrap:wrap;justify-content:center;margin:12px 0}
.angle-legend{display:flex;flex-wrap:wrap;gap:8px;justify-content:center;font-size:.82rem;margin-top:8px}
.angle-legend span{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;background:var(--card);border-radius:99px;border:1px solid var(--border)}
.angle-legend span::before{content:'';width:10px;height:10px;border-radius:50%;background:var(--c,#7c3aed)}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 7 · Глава 3</h1>
<div class="hdr-sub">Параллельность прямых на плоскости · признаки, аксиома, свойства</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-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="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>секущую и 8 углов</b>, узнаем <b>аксиому параллельных</b> (через точку — только одна!), разберём <b>обратные свойства</b> и угрозы при пересечении пары параллельных, а в финале разберёмся с <b>углами, у которых стороны соответственно параллельны или перпендикулярны</b>.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p15')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 15</button>
<div class="hero-progress">
<span class="hp-label">Прогресс по главе</span>
<div class="hp-bar"><div id="hero-hp-fill" class="hp-fill"></div></div>
<span id="hero-hp-text" class="hp-text">0%</span>
</div>
<div id="hero-xp-badge" class="hero-xp-badge" data-gamified></div>
</div>
</section>
<section class="psel">
<div class="psel-title">Параграфы главы</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
<section id="sec-p15" class="sec" data-watermark="&#8741;"><div class="sec-header"><span class="sec-num">§ 15</span><h2 class="sec-h">Признаки параллельности прямых</h2></div><div id="p15-body"></div></section>
<section id="sec-p16" class="sec" data-watermark="A"><div class="sec-header"><span class="sec-num">§ 16</span><h2 class="sec-h">Аксиома параллельных прямых</h2></div><div id="p16-body"></div></section>
<section id="sec-p17" class="sec" data-watermark="↔"><div class="sec-header"><span class="sec-num">§ 17</span><h2 class="sec-h">Свойства параллельных прямых</h2></div><div id="p17-body"></div></section>
<section id="sec-p18" class="sec" data-watermark="∠"><div class="sec-header"><span class="sec-num">§ 18</span><h2 class="sec-h">Углы со сторонами, соответственно параллельными или перпендикулярными</h2></div><div id="p18-body"></div></section>
<section id="sec-final3" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#7c3aed,#a855f7)">Финал главы</span><h2 class="sec-h">Итоги. 5 боссов главы 3</h2></div><div id="final3-body"></div></section>
</div>
<aside class="col-side" id="col-side"><div id="sidebar-content"></div></aside>
<div class="col-side-backdrop" id="col-side-backdrop"></div>
</main>
<footer class="foot">Интерактивный учебник «Геометрия 7» · Глава 3 · Параллельность прямых · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<script>
'use strict';
const STATE = { current:'p15', progress:{p15:0,p16:0,p17:0,p18:0,final3:0}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 5;
const _TB_SLUG = 'geometry-7-ch3';
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
const ACH_LABELS = {
start:'Начало главы 3!',
p15_done:'Признаки параллельности — освоены!',
p16_done:'Аксиома Евклида — твоя!',
p17_done:'Свойства параллельных — знаешь!',
p18_done:'Углы с парал./перп. сторонами — мастер!',
ch3_done:'Глава 3 пройдена!',
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry7_ch3_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry7_ch3_achievements');
if(a){ const p=JSON.parse(a); if(Array.isArray(p)) p.forEach(id=>STATE.achievements.set(id, ACH_LABELS[id]||id)); else if(p&&typeof p==='object'){ for(const[id,t] of Object.entries(p)) STATE.achievements.set(id,(t&&t!==id)?t:(ACH_LABELS[id]||id)); } }
STATE.xp=+(localStorage.getItem('geometry7_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry7_ch3_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry7_ch3_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry7_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==='p15') achievement('p15_done');
else if(key==='p16') achievement('p16_done');
else if(key==='p17') achievement('p17_done');
else if(key==='p18') achievement('p18_done');
else if(key==='final3') achievement('ch3_done');
}
}
const _markedRead=new Set();
let _pendingProgressBody=null, _progressTimer=null;
function _flushProgress(){
const body=_pendingProgressBody; _pendingProgressBody=null; if(!body) return;
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG+'/progress',{method:'POST',headers:{'Content-Type':'application/json','Authorization':'Bearer '+tok},body:JSON.stringify(body),keepalive:true}).catch(()=>{});
}
function _queueProgress(patch){ _pendingProgressBody=Object.assign(_pendingProgressBody||{},patch); if(_progressTimer) clearTimeout(_progressTimer); _progressTimer=setTimeout(_flushProgress, 600); }
function markLastPara(id){ _queueProgress({last_para:id}); }
function markParaRead(id){ if(_markedRead.has(id)) return; _markedRead.add(id); _queueProgress({mark_read:id}); }
window.addEventListener('beforeunload', _flushProgress);
function 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,'geometry7-ch3-'+(src||'misc'));
if(STATE.level>prev){
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent='Уровень '+STATE.level+'!'; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),2600); }
}
}
function refreshProgressUI(){
const total=Math.round(Object.values(STATE.progress).reduce((a,b)=>a+b,0)/TOTAL_PARAS);
const f=document.getElementById('hero-hp-fill'); if(f) f.style.width=total+'%';
const t=document.getElementById('hero-hp-text'); if(t) t.textContent=total+'% пройдено';
document.querySelectorAll('[data-prog-card]').forEach(el=>{ const k=el.dataset.progCard; const fl=el.querySelector('.psel-prog-fill'); if(fl) fl.style.width=(STATE.progress[k]||0)+'%'; });
const xpBadge=document.getElementById('hero-xp-badge');
if(xpBadge){ xpBadge.innerHTML='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:13px;height:13px"><polygon points="12 2 22 20 2 20"/></svg> Ур. '+STATE.level+' \xb7 '+(STATE.xp||0)+' XP'; }
if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
}
function achievement(id,text){
if(STATE.achievements.has(id)) return;
STATE.achievements.set(id, text||ACH_LABELS[id]||id); saveProgress();
const pop=document.getElementById('ach-popup');
if(pop){ document.getElementById('ach-text').textContent=text||ACH_LABELS[id]||id; pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'),3300); }
addXp(20,'ach-'+id);
}
const PARAS = [
{ id:'p15', num:'§ 15', name:'Признаки параллельности', sub:'Секущая и 8 углов' },
{ id:'p16', num:'§ 16', name:'Аксиома параллельных', sub:'Через точку — одна' },
{ id:'p17', num:'§ 17', name:'Свойства параллельных', sub:'Обратные теоремы' },
{ id:'p18', num:'§ 18', name:'Углы со сторонами ∥ / ⊥', sub:'Равны или 180°' },
{ id:'final3', num:'★', name:'Финал главы', sub:'Итоги \xB7 5 боссов', final:true },
];
function buildParaSelector(){
const g=document.getElementById('psel-grid'); g.innerHTML='';
PARAS.forEach(p=>{
const card=document.createElement('div');
card.className='psel-card'+(p.final?' final':'');
card.dataset.id=p.id; card.dataset.progCard=p.id;
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
if(window.renderMathInElement) try{ renderMath(g); }catch(e){}
}
const BUILT=new Set();
const BUILDERS = { p15:()=>buildP15(), p16:()=>buildP16(), p17:()=>buildP17(), p18:()=>buildP18(), final3:()=>buildFinal3() };
function ensureBuilt(id){ if(BUILT.has(id)) return; const fn=BUILDERS[id]; if(fn){ fn(); BUILT.add(id); } }
function goTo(id){
STATE.current=id; ensureBuilt(id);
document.querySelectorAll('.sec').forEach(s=>s.classList.remove('active'));
const el=document.getElementById('sec-'+id); if(el) el.classList.add('active');
document.querySelectorAll('.psel-card').forEach(c=>c.classList.toggle('active', c.dataset.id===id));
buildSidebar(id);
window.scrollTo({top:0,behavior:'smooth'});
if((STATE.progress[id]||0)<10) bumpProgress(id, 10);
if(window.renderMathInElement) setTimeout(()=>renderMath(el), 0);
markLastPara(id);
}
const SIDEBARS = {
p15:{title:'Шпаргалка \xA715',rows:[
['$a \\parallel b$','прямые не пересекаются'],
['Секущая','прямая, пересекающая обе'],
['8 углов','4 у каждой точки пересечения'],
['Соотв. равны','$\\Rightarrow a \\parallel b$'],
['Накрест лежащие $=$','$\\Rightarrow a \\parallel b$'],
['Односторонние $= 180°$','$\\Rightarrow a \\parallel b$'],
]},
p16:{title:'Шпаргалка \xA716',rows:[
['Аксиома','через точку вне $a$ — ровно <b>одна</b> прямая $\\parallel a$'],
['5-й постулат','Евклид, ~300 до н.э.'],
['Следствие 1','если $a \\parallel b$, $c \\cap a$ $\\Rightarrow$ $c \\cap b$'],
['Следствие 2','$a \\parallel c$, $b \\parallel c$ $\\Rightarrow$ $a \\parallel b$'],
['Геометрия Лобачев.','параллельных можно больше'],
]},
p17:{title:'Шпаргалка \xA717',rows:[
['Если $a \\parallel b$','то секущая даёт:'],
['Соотв. углы','равны'],
['Накрест леж.','равны'],
['Односторонние','$= 180°$'],
['Это обратные','к признакам \xA715'],
]},
p18:{title:'Шпаргалка \xA718',rows:[
['Стороны \xA0\xA0 $\\parallel$','углы равны ИЛИ $= 180°$'],
['Стороны \xA0\xA0 $\\perp$','углы равны ИЛИ $= 180°$'],
['Острый+острый','равны'],
['Острый+тупой','$= 180°$'],
]},
final3:{title:'Финал главы',rows:[
['\xA715\xA718','теория главы 3'],
['Боссов','5'],
['Награда','+100 XP за полное прохождение'],
]},
};
const TIPS=[
{sec:'p15',html:'<b>Признак</b> работает в одну сторону: <b>угол равен углу $\\Rightarrow$ прямые параллельны</b>. Используй для <b>доказательства</b> параллельности.'},
{sec:'p16',html:'Аксиома — недоказуемое утверждение. Через точку $M$ вне прямой $a$ можно провести <b>ровно одну</b> прямую, параллельную $a$.'},
{sec:'p17',html:'<b>Свойство</b> работает в другую сторону: <b>прямые параллельны $\\Rightarrow$ угол равен углу</b>. Используй для <b>вычисления</b> углов.'},
{sec:'p18',html:'Запомни просто: <b>оба острых</b> или <b>оба тупых</b> $\\Rightarrow$ равны. Один острый, другой тупой $\\Rightarrow$ дают $180°$.'},
{sec:'final3',html:'5 боссов — проверка всей теории. Помни: <b>признак</b> даёт параллельность, <b>свойство</b> — равенство углов.'},
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS.p15;
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' — '+v:'')+'</div>'; });
html+='</div>';
const tip=TIPS.find(t=>t.sec===id)||TIPS[0];
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения</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('geometry7_ch3_theme')||'light';
if(t==='dark') document.documentElement.classList.add('dark');
document.getElementById('theme-lab').textContent=t==='dark'?'Светлая':'Тёмная';
document.getElementById('theme-btn').addEventListener('click', ()=>{
document.documentElement.classList.toggle('dark');
const dark=document.documentElement.classList.contains('dark');
localStorage.setItem('geometry7_ch3_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
const ICONS = {
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>',
};
function makeCard(kind, title, num, body){
const labels={theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример'};
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
function secNav(prev, next){
const NAMES={p15:'\xA715',p16:'\xA716',p17:'\xA717',p18:'\xA718',final3:'Финал'};
let h='<div class="sec-nav">';
h+=prev?'<button class="btn" onclick="goTo(\''+prev+'\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+NAMES[prev]+'</button>':'<span></span>';
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+NAMES[next]+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
h+='</div>'; return h;
}
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;
});
}
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();
buildParaSelector(); refreshProgressUI(); goTo('p15');
setTimeout(()=>achievement('start','Начало главы 3!'), 600);
}
document.addEventListener('DOMContentLoaded', init);
/* ============================================================
Хелпер: рисунок 2 прямые + секущая + 8 углов (с подсветкой пары)
highlight: { pair: 'corresp'|'alt'|'cons', n1, n2 } — какие 2 угла подсветить
============================================================ */
function drawParallelSecant(opts){
const G = window.GEOM7; if(!G) return '';
opts = opts || {};
const W = opts.W || 360, H = opts.H || 230;
const b = G.svgBox(W, H, { id: opts.id || 'par-sec', cell: 20 });
// Две горизонтальные прямые
const aY = 70, bY = 170;
const A1 = {x:20,y:aY}, A2 = {x:W-20,y:aY};
const B1 = {x:20,y:bY}, B2 = {x:W-20,y:bY};
// Секущая (наклонная)
const tX1 = 90, tX2 = W - 80;
// Точки пересечения секущей с a и b
// секущая y = aY при x=tX1; y = bY при x=tX2
// P = (tX1, aY), Q = (tX2, bY) — направляющие
const P = { x:tX1, y:aY }, Q = { x:tX2, y:bY };
// Продолжаем секущую за P и Q для красоты
const v = G.unit(G.vec(P, Q));
const T1 = { x: P.x - v.x * 60, y: P.y - v.y * 60 };
const T2 = { x: Q.x + v.x * 60, y: Q.y + v.y * 60 };
const aColor = opts.parallel ? '#7c3aed' : '#475569';
let s = b.open;
s += G.segment(A1, A2, { color: aColor, width:2.5 });
s += G.segment(B1, B2, { color: aColor, width:2.5 });
if(opts.parallel){
s += G.parallelMark({x:A1.x+60,y:aY}, {x:A1.x+120,y:aY}, { color:'#7c3aed', count:1 });
s += G.parallelMark({x:B1.x+60,y:bY}, {x:B1.x+120,y:bY}, { color:'#7c3aed', count:1 });
}
s += G.segment(T1, T2, { color:'#dc2626', width:2.5 });
// подписи прямых
s += '<text x="'+(W-18)+'" y="'+(aY-6)+'" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+aColor+'">a</text>';
s += '<text x="'+(W-18)+'" y="'+(bY-6)+'" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="'+aColor+'">b</text>';
s += '<text x="'+(T2.x+4)+'" y="'+(T2.y+4)+'" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#dc2626">c</text>';
// 8 углов — рисуем мелкие дуги вокруг P и Q
// Направление вдоль a: вправо (1,0)
const aDir = {x:1,y:0};
// Направление вдоль c (от P к Q)
const cDirP = v; // вниз-вправо
const cDirN = {x:-v.x,y:-v.y};
const aDirN = {x:-1,y:0};
// углы у P: 1 (между +a и +c справа сверху), 2 (между -a и +c слева сверху), 3 (между -a и -c снизу слева), 4 (между +a и -c снизу справа)
// Стандартная схема: 1 — правый верх, 2 — лев. верх, 3 — лев. низ, 4 — прав. низ
function placeNum(P, dir1, dir2, n, color){
// позиция метки — внутри дуги (биссектриса направлений)
const u1 = G.unit(dir1), u2 = G.unit(dir2);
const bx = u1.x + u2.x, by = u1.y + u2.y;
const L = Math.hypot(bx,by)||1;
const ux = bx/L, uy = by/L;
const r = 22;
return '<text x="'+(P.x+ux*r)+'" y="'+(P.y+uy*r+4)+'" text-anchor="middle" font-size="12" font-family="Unbounded,Inter,sans-serif" font-weight="800" fill="'+color+'">'+n+'</text>';
}
// У P (на прямой a): между лучами вдоль a (+/-) и c (+/-)
// 1 — между +a и -c (правый верх, c уходит вниз — значит верхний правый угол между прямой a (вправо) и c вверх (–c))
// На самом деле визуально: секущая идёт сверху-слева (T1) вниз-направо (T2).
// У P: вверх по секущей = -v; вправо по a = +x.
// Угол 1 (правый верх) — между +x и -v
// Угол 2 (левый верх) — между -x и -v
// Угол 3 (левый низ) — между -x и +v
// Угол 4 (правый низ) — между +x и +v
const hi = opts.highlight || null;
function isHL(n){ return hi && (hi.n1===n || hi.n2===n); }
function arcAt(P, dirA, dirB, n, baseColor){
const r = 18;
const a1 = Math.atan2(dirA.y, dirA.x);
const a2 = Math.atan2(dirB.y, dirB.x);
const color = isHL(n) ? (hi.color || '#f59e0b') : baseColor;
const w = isHL(n) ? 3 : 1.6;
return G.arc(P, r, a1, a2, { color:color, width:w });
}
// У P:
s += arcAt(P, aDir, cDirN, 1, '#94a3b8');
s += arcAt(P, cDirN, aDirN, 2, '#94a3b8');
s += arcAt(P, aDirN, cDirP, 3, '#94a3b8');
s += arcAt(P, cDirP, aDir, 4, '#94a3b8');
// У Q:
s += arcAt(Q, aDir, cDirN, 5, '#94a3b8');
s += arcAt(Q, cDirN, aDirN, 6, '#94a3b8');
s += arcAt(Q, aDirN, cDirP, 7, '#94a3b8');
s += arcAt(Q, cDirP, aDir, 8, '#94a3b8');
// подписи цифр
function lblColor(n){ return isHL(n) ? (hi.color||'#f59e0b') : '#475569'; }
s += placeNum(P, aDir, cDirN, 1, lblColor(1));
s += placeNum(P, cDirN, aDirN, 2, lblColor(2));
s += placeNum(P, aDirN, cDirP, 3, lblColor(3));
s += placeNum(P, cDirP, aDir, 4, lblColor(4));
s += placeNum(Q, aDir, cDirN, 5, lblColor(5));
s += placeNum(Q, cDirN, aDirN, 6, lblColor(6));
s += placeNum(Q, aDirN, cDirP, 7, lblColor(7));
s += placeNum(Q, cDirP, aDir, 8, lblColor(8));
// точки пересечения
s += G.point(P.x, P.y, '', { r:3, color:'#dc2626' });
s += G.point(Q.x, Q.y, '', { r:3, color:'#dc2626' });
s += b.close;
return s;
}
/* ============================================================
\xA7 15 — Признаки параллельности прямых
============================================================ */
function buildP15(){
const box = document.getElementById('p15-body');
const G = window.GEOM7;
let html = '';
/* Базовая картинка: 2 прямые + секущая с 8 углами */
const svgBase = drawParallelSecant({ id:'p15-base', parallel:true });
/* Подсветки */
const svgCorresp = drawParallelSecant({ id:'p15-corresp', parallel:true, highlight:{n1:1,n2:5,color:'#7c3aed'} });
const svgAlt = drawParallelSecant({ id:'p15-alt', parallel:true, highlight:{n1:3,n2:5,color:'#0891b2'} });
const svgCons = drawParallelSecant({ id:'p15-cons', parallel:true, highlight:{n1:4,n2:5,color:'#f59e0b'} });
html += makeCard('theory', 'Параллельные прямые и секущая', '15.1', `
<p>Две прямые на плоскости называются <b>параллельными</b>, если они не пересекаются. Обозначение: $a \\parallel b$.</p>
<p><b>Секущая</b> — прямая, пересекающая обе прямые $a$ и $b$ в двух разных точках. В каждой точке пересечения возникает по 4 угла. Итого <b>8 углов</b>, которые принято нумеровать $\\angle 1$, $\\angle 2$, ..., $\\angle 8$.</p>
<div class="svg-host">`+svgBase+`</div>
<div class="angle-legend">
<span style="--c:#94a3b8">8 углов, 4+4</span>
<span style="--c:#7c3aed">a ∥ b</span>
<span style="--c:#dc2626">c — секущая</span>
</div>`);
html += makeCard('rule', 'Три пары углов', '15.2', `
<p>Среди 8 углов выделяют три типа особых пар:</p>
<ul style="padding-left:22px;line-height:1.85">
<li><b style="color:#7c3aed">Соответственные углы</b> — лежат по одну сторону от секущей и по одну сторону от прямых (один над $a$, другой над $b$). Пары: $\\angle 1, \\angle 5$; $\\angle 2, \\angle 6$; $\\angle 3, \\angle 7$; $\\angle 4, \\angle 8$.</li>
<li><b style="color:#0891b2">Накрест лежащие (внутренние)</b> — лежат между прямыми $a$ и $b$, по <b>разные</b> стороны от секущей. Пары: $\\angle 3, \\angle 5$; $\\angle 4, \\angle 6$.</li>
<li><b style="color:#f59e0b">Односторонние (внутренние)</b> — лежат между прямыми $a$ и $b$, по <b>одну</b> сторону от секущей. Пары: $\\angle 4, \\angle 5$; $\\angle 3, \\angle 6$.</li>
</ul>
<div class="svg-host-row">`+svgCorresp+svgAlt+svgCons+`</div>
<div class="angle-legend">
<span style="--c:#7c3aed">Соответственные ∠ 1, ∠ 5</span>
<span style="--c:#0891b2">Накрест лежащие ∠ 3, ∠ 5</span>
<span style="--c:#f59e0b">Односторонние ∠ 4, ∠ 5</span>
</div>`);
html += makeCard('rule', 'Признаки параллельности прямых', '15.3', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема (3 признака).</b> Если при пересечении двух прямых секущей выполняется хотя бы одно из условий:</p>
<ol style="padding-left:22px;line-height:1.9">
<li>пара <b>соответственных углов</b> равна;</li>
<li>пара <b>накрест лежащих углов</b> равна;</li>
<li>пара <b>односторонних углов</b> в сумме даёт $180°$,</li>
</ol>
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px">то <b>прямые параллельны</b>.</p>
<details class="spoiler"><summary>Идея доказательства (1 признак $\\Rightarrow$ 2 и 3)</summary>
<div class="spoiler-body">
<p>Накрест лежащие $\\angle 3$ и $\\angle 5$ равны $\\Leftrightarrow$ соответственные $\\angle 1$ и $\\angle 5$ равны, потому что $\\angle 1 = \\angle 3$ как <b>вертикальные</b>.</p>
<p>Односторонние $\\angle 4 + \\angle 5 = 180°$ $\\Leftrightarrow$ накрест лежащие $\\angle 3 = \\angle 5$, потому что $\\angle 3 + \\angle 4 = 180°$ как <b>смежные</b>.</p>
<p>Поэтому достаточно доказать <b>один</b> из признаков — остальные следуют автоматически.</p>
</div></details>`);
html += makeCard('example', 'Пример. Угол $40°$', '15.4', `
<p><b>Задача.</b> При пересечении прямых $a$ и $b$ секущей $c$ одна пара накрест лежащих углов равна по $40°$. Параллельны ли прямые?</p>
<p><b>Решение.</b> По <b>2-му признаку</b> (накрест лежащие равны) $\\Rightarrow$ $a \\parallel b$. Да, прямые параллельны.</p>
<p style="background:var(--warn-bg);padding:10px 14px;border-radius:8px;border-left:4px solid var(--warn)"><b>Важно:</b> признак — это условие, при котором мы можем <b>сделать вывод</b> о параллельности. Сами углы задают параллельность.</p>`);
/* ИНТЕРАКТИВ 1 — найди тип пары углов */
html += '<div class="wg" id="p15-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Определи тип пары углов</div></div>'
+'<div class="wg-help">Назови, какой <b>тип</b> образуют эти углы при пересечении двух прямых секущей.</div>'
+'<div class="score-display"><span>Задача <b id="p15-iv1-i">1</b> / 6</span><span>Очки: <b id="p15-iv1-s">0</b> / 6</span></div>'
+'<div id="p15-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:8px;justify-content:center;flex-wrap:wrap"><button class="btn primary" id="p15-iv1-c1" style="background:#7c3aed;border-color:#7c3aed">Соответственные</button><button class="btn primary" id="p15-iv1-c2" style="background:#0891b2;border-color:#0891b2">Накрест лежащие</button><button class="btn primary" id="p15-iv1-c3" style="background:#f59e0b;border-color:#f59e0b">Односторонние</button></div>'
+'<div class="feedback" id="p15-iv1-fb"></div></div>';
/* ИНТЕРАКТИВ 2 — параллельны ли прямые? */
html += '<div class="wg" id="p15-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Параллельны ли прямые?</div></div>'
+'<div class="wg-help">По данным об углах при секущей реши, гарантирует ли это $a \\parallel b$.</div>'
+'<div class="score-display"><span>Задача <b id="p15-iv2-i">1</b> / 6</span><span>Очки: <b id="p15-iv2-s">0</b> / 6</span></div>'
+'<div id="p15-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"><button class="btn primary" id="p15-iv2-y" style="background:#10b981;border-color:#10b981">Да, $\\parallel$</button><button class="btn primary" id="p15-iv2-n" style="background:#dc2626;border-color:#dc2626">Нет, не $\\parallel$</button></div>'
+'<div class="feedback" id="p15-iv2-fb"></div></div>';
html += secNav(null, 'p16') + readButton('p15');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'$\\angle 1$ и $\\angle 5$ (одна позиция, по одну сторону секущей)', ans:'c1' },
{ e:'$\\angle 3$ и $\\angle 5$ (между прямыми, по разные стороны)', ans:'c2' },
{ e:'$\\angle 4$ и $\\angle 5$ (между прямыми, по одну сторону)', ans:'c3' },
{ e:'$\\angle 2$ и $\\angle 6$ (по одну сторону, в одинаковой позиции)', ans:'c1' },
{ e:'$\\angle 4$ и $\\angle 6$ (между прямыми, по разные стороны)', ans:'c2' },
{ e:'$\\angle 3$ и $\\angle 6$ (между прямыми, по одну сторону)', ans:'c3' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p15-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p15-iv1');bumpProgress('p15',25);} else if(score>=4){addXp(8,'p15-iv1');bumpProgress('p15',12);} return; }
document.getElementById('p15-iv1-i').textContent=(i+1);
document.getElementById('p15-iv1-s').textContent=score;
document.getElementById('p15-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p15-iv1-q'));
document.getElementById('p15-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p15-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={c1:'соответственные',c2:'накрест лежащие',c3:'односторонние'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p15-iv1-s').textContent=score;
i++; setTimeout(show,1100);
}
document.getElementById('p15-iv1-c1').addEventListener('click',()=>ans('c1'));
document.getElementById('p15-iv1-c2').addEventListener('click',()=>ans('c2'));
document.getElementById('p15-iv1-c3').addEventListener('click',()=>ans('c3'));
show();
})();
(function(){
const Q=[
{ e:'$\\angle 1 = \\angle 5 = 60°$ (соответственные)', ok:true, why:'1 признак.' },
{ e:'$\\angle 3 = \\angle 5 = 70°$ (накрест лежащие)', ok:true, why:'2 признак.' },
{ e:'$\\angle 4 + \\angle 5 = 180°$ (односторонние)', ok:true, why:'3 признак.' },
{ e:'$\\angle 1 = 60°$, $\\angle 5 = 70°$ (соответственные)', ok:false, why:'Не равны, признак не применим.' },
{ e:'$\\angle 4 + \\angle 5 = 200°$ (односторонние)', ok:false, why:'Сумма $\\ne 180°$.' },
{ e:'$\\angle 3 = \\angle 6 = 90°$ (односторонние)', ok:true, why:'$90+90=180$, 3 признак.' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p15-iv2-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p15-iv2');bumpProgress('p15',25);} else if(score>=4){addXp(8,'p15-iv2');bumpProgress('p15',12);} return; }
document.getElementById('p15-iv2-i').textContent=(i+1);
document.getElementById('p15-iv2-s').textContent=score;
document.getElementById('p15-iv2-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p15-iv2-q'));
document.getElementById('p15-iv2-fb').style.display='none';
}
function ans(yes){
if(i>=Q.length) return;
const fb=document.getElementById('p15-iv2-fb');
if(yes===Q[i].ok){ score++; feedback(fb,true,'&#10003; Верно! '+Q[i].why); }
else feedback(fb,false,'&#10007; '+(Q[i].ok?'Это <b>верно</b>: ':'Это <b>неверно</b>: ')+Q[i].why);
document.getElementById('p15-iv2-s').textContent=score;
i++; setTimeout(show,1400);
}
document.getElementById('p15-iv2-y').addEventListener('click',()=>ans(true));
document.getElementById('p15-iv2-n').addEventListener('click',()=>ans(false));
show();
})();
wireReadBtn('p15');
}
/* ============================================================
\xA7 16 — Аксиома параллельных прямых
============================================================ */
function buildP16(){
const box = document.getElementById('p16-body');
const G = window.GEOM7;
let html = '';
/* SVG: точка M и прямая a, через M — единственная b ∥ a */
let svgAxiom='';
if(G){
const b=G.svgBox(320,200,{id:'p16-axiom',cell:20});
const A1={x:20,y:140}, A2={x:300,y:140};
const B1={x:20,y:70}, B2={x:300,y:70};
const M={x:160,y:70};
svgAxiom = b.open
+ G.segment(A1,A2,{color:'#475569',width:2.5})
+ G.segment(B1,B2,{color:'#7c3aed',width:2.5,dash:'6 3'})
+ G.parallelMark({x:80,y:140},{x:140,y:140},{color:'#7c3aed'})
+ G.parallelMark({x:80,y:70},{x:140,y:70},{color:'#7c3aed'})
+ G.point(M.x,M.y,'M',{color:'#dc2626',dx:8,dy:-8})
+ '<text x="295" y="155" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#475569">a</text>'
+ '<text x="295" y="62" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">b</text>'
+ b.close;
}
/* SVG: следствие 1 — c пересекает a, тогда c пересекает b */
let svgCons1='';
if(G){
const b=G.svgBox(320,200,{id:'p16-cons1',cell:20});
const A1={x:20,y:150}, A2={x:300,y:150};
const B1={x:20,y:60}, B2={x:300,y:60};
// секущая
const T1={x:80,y:30}, T2={x:250,y:180};
svgCons1 = b.open
+ G.segment(A1,A2,{color:'#7c3aed',width:2.5})
+ G.segment(B1,B2,{color:'#7c3aed',width:2.5})
+ G.parallelMark({x:80,y:150},{x:140,y:150},{color:'#7c3aed'})
+ G.parallelMark({x:80,y:60},{x:140,y:60},{color:'#7c3aed'})
+ G.segment(T1,T2,{color:'#dc2626',width:2.5})
+ '<text x="295" y="60-5" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">b</text>'.replace('60-5','55')
+ '<text x="295" y="165" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#7c3aed">a</text>'
+ '<text x="260" y="190" font-size="13" font-family="Unbounded,Inter,sans-serif" font-weight="700" fill="#dc2626">c</text>'
+ b.close;
}
html += makeCard('theory', 'Аксиома Евклида', '16.1', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Аксиома параллельных.</b> Через любую точку, не лежащую на данной прямой, можно провести <b>только одну</b> прямую, параллельную данной.</p>
<div class="svg-host">`+svgAxiom+`</div>
<p>На рисунке: $M \\notin a$. Существует <b>ровно одна</b> прямая $b$, проходящая через $M$ и параллельная $a$.</p>
<p>Это <b>аксиома</b> — утверждение, принимаемое без доказательства. В геометрии Евклида (~300 до н.э.) она называется <b>5-м постулатом</b>.</p>
<details class="spoiler"><summary>Историческая справка</summary>
<div class="spoiler-body">
<p>На протяжении более 2000 лет математики пытались <b>вывести</b> 5-й постулат из остальных аксиом — безуспешно. В XIX веке <b>Н.&nbsp;И.&nbsp;Лобачевский</b> и независимо Я.&nbsp;Бойяи и К.&nbsp;Гаусс построили <b>неевклидову геометрию</b>, в которой через точку проходит <b>бесконечно много</b> параллельных прямых. Это было настоящей научной революцией!</p>
</div></details>`);
html += makeCard('rule', 'Следствие 1 — пересечение наследуется', '16.2', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Следствие 1.</b> Если прямая $c$ пересекает одну из двух параллельных прямых, то она пересекает и вторую.</p>
<div class="svg-host">`+svgCons1+`</div>
<p>Логика: если бы $c$ не пересекла $b$, то $c \\parallel b$. А так как $a \\parallel b$ и $c$ пересекает $a$, то через точку пересечения проходят <b>две</b> прямые ($a$ и $c$), параллельные $b$ — противоречие с аксиомой.</p>`);
html += makeCard('rule', 'Следствие 2 — транзитивность', '16.3', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Следствие 2.</b> Если две прямые $a$ и $b$ параллельны третьей прямой $c$, то они параллельны друг другу: $a \\parallel c$ и $b \\parallel c$ $\\Rightarrow$ $a \\parallel b$.</p>
<p>Логика: если бы $a$ и $b$ пересекались в точке $K$, то через точку $K$ проходили бы две прямые, параллельные $c$ — противоречие с аксиомой.</p>`);
html += makeCard('example', 'Пример. Расположение прямых', '16.4', `
<p><b>Задача.</b> Известно, что $a \\parallel b$, $b \\parallel d$. Что можно сказать о прямых $a$ и $d$?</p>
<p><b>Решение.</b> По <b>следствию 2</b>: $a \\parallel b$ и $b \\parallel d$ $\\Rightarrow$ $a \\parallel d$.</p>
<p><b>Задача 2.</b> $a \\parallel b$, прямая $c$ пересекает $a$. Пересечёт ли $c$ прямую $b$?</p>
<p><b>Решение.</b> Да, по <b>следствию 1</b>.</p>`);
/* ИНТЕРАКТИВ 1 — да/нет по аксиоме и следствиям */
html += '<div class="wg" id="p16-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Верное утверждение?</div></div>'
+'<div class="wg-help">Опирайся на аксиому параллельных и её следствия.</div>'
+'<div class="score-display"><span>Задача <b id="p16-iv1-i">1</b> / 6</span><span>Очки: <b id="p16-iv1-s">0</b> / 6</span></div>'
+'<div id="p16-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.02rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center"><button class="btn primary" id="p16-iv1-y" style="background:#10b981;border-color:#10b981">Да</button><button class="btn primary" id="p16-iv1-n" style="background:#dc2626;border-color:#dc2626">Нет</button></div>'
+'<div class="feedback" id="p16-iv1-fb"></div></div>';
/* ИНТЕРАКТИВ 2 — что можно вывести */
html += '<div class="wg" id="p16-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Сделай вывод</div></div>'
+'<div class="wg-help">Введи <b>«параллельны»</b> или <b>«пересекаются»</b> для двух прямых.</div>'
+trainerHTML('p16-iv2', 5, 'парал. / пересек.')
+'</div>';
html += secNav('p15', 'p17') + readButton('p16');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'Через точку вне прямой можно провести только одну $\\parallel$ ей прямую.', ok:true },
{ e:'Через точку вне прямой можно провести две разных прямых, $\\parallel$ ей.', ok:false },
{ e:'Если $a \\parallel b$, $c$ пересекает $a$, то $c$ пересекает $b$.', ok:true },
{ e:'Если $a \\parallel c$, $b \\parallel c$, то $a$ может пересечь $b$.', ok:false },
{ e:'Аксиомы доказываются.', ok:false },
{ e:'5-й постулат Евклида — это аксиома параллельных.', ok:true },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p16-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(12,'p16-iv1');bumpProgress('p16',25);} else if(score>=4){addXp(6,'p16-iv1');bumpProgress('p16',12);} return; }
document.getElementById('p16-iv1-i').textContent=(i+1);
document.getElementById('p16-iv1-s').textContent=score;
document.getElementById('p16-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p16-iv1-q'));
document.getElementById('p16-iv1-fb').style.display='none';
}
function ans(yes){
if(i>=Q.length) return;
const fb=document.getElementById('p16-iv1-fb');
if(yes===Q[i].ok){ score++; feedback(fb,true,'&#10003; Верно!'); }
else feedback(fb,false,'&#10007; '+(Q[i].ok?'Утверждение <b>верно</b>.':'Утверждение <b>ложно</b>.'));
document.getElementById('p16-iv1-s').textContent=score;
i++; setTimeout(show,1300);
}
document.getElementById('p16-iv1-y').addEventListener('click',()=>ans(true));
document.getElementById('p16-iv1-n').addEventListener('click',()=>ans(false));
show();
})();
makeTrainer({
idPrefix:'p16-iv2',
parser:(v)=>v,
questions:[
{ q:'$a \\parallel b$, $b \\parallel c$. Какое отношение между $a$ и $c$?', a:(v)=>String(v).trim().toLowerCase().startsWith('парал'), show:'параллельны' },
{ q:'$a \\parallel b$, $c$ пересекает $a$. Какое отношение между $c$ и $b$?', a:(v)=>String(v).trim().toLowerCase().startsWith('пересек'), show:'пересекаются' },
{ q:'$a \\parallel c$, $b \\parallel c$, $a \\ne b$. Какое отношение между $a$ и $b$?', a:(v)=>String(v).trim().toLowerCase().startsWith('парал'), show:'параллельны' },
{ q:'$a$ и $b$ обе пересекают $c$ в одной и той же точке $K$. Они...', a:(v)=>String(v).trim().toLowerCase().startsWith('пересек'), show:'пересекаются (в точке $K$)' },
{ q:'$a \\parallel b$, прямая $d$ пересекает $b$. Какое отношение между $d$ и $a$?', a:(v)=>String(v).trim().toLowerCase().startsWith('пересек'), show:'пересекаются' },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p16-iv2');bumpProgress('p16',25);} else if(s>=3){addXp(8,'p16-iv2');bumpProgress('p16',12);} }
});
wireReadBtn('p16');
}
/* ============================================================
\xA7 17 — Свойства параллельных прямых
============================================================ */
function buildP17(){
const box = document.getElementById('p17-body');
const G = window.GEOM7;
let html = '';
const svg17 = drawParallelSecant({ id:'p17-prop', parallel:true });
html += makeCard('theory', 'Обратные теоремы', '17.1', `
<p>В §15 мы изучили <b>признаки</b>: <i>«если углы равны $\\Rightarrow$ прямые $\\parallel$»</i>.</p>
<p>Теперь — <b>обратные теоремы</b>, или <b>свойства параллельных прямых</b>: <i>«если прямые $\\parallel \\Rightarrow$ углы равны»</i>.</p>
<div class="svg-host">`+svg17+`</div>`);
html += makeCard('rule', 'Свойство 1 — соответственные углы', '17.2', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема.</b> Если две параллельные прямые пересечены секущей, то <b>соответственные углы равны</b>.</p>
<p>На рисунке: $a \\parallel b$ $\\Rightarrow$ $\\angle 1 = \\angle 5$, $\\angle 2 = \\angle 6$, $\\angle 3 = \\angle 7$, $\\angle 4 = \\angle 8$.</p>
<details class="spoiler"><summary>Доказательство «от противного»</summary>
<div class="spoiler-body">
<p>Пусть $a \\parallel b$. Допустим, $\\angle 1 \\ne \\angle 5$.</p>
<p>Через точку $P$ (где $c$ пересекает $a$) проведём прямую $b'$ так, чтобы соответственные углы стали равны.</p>
<p>По <b>признаку</b> $b' \\parallel a$. Но через одну точку проходит только одна прямая $\\parallel a$ — значит $b' = b$. Тогда $\\angle 1 = \\angle 5$ — противоречие. ■</p>
</div></details>`);
html += makeCard('rule', 'Свойства 2 и 3 — накрест лежащие и односторонние', '17.3', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема.</b> Если две параллельные прямые пересечены секущей, то:</p>
<ul style="padding-left:22px;line-height:1.9">
<li><b>накрест лежащие углы равны</b>: $\\angle 3 = \\angle 5$, $\\angle 4 = \\angle 6$;</li>
<li><b>сумма односторонних углов</b> равна $180°$: $\\angle 3 + \\angle 6 = 180°$, $\\angle 4 + \\angle 5 = 180°$.</li>
</ul>
<p>Доказательство: $\\angle 3 = \\angle 1$ (вертикальные), $\\angle 1 = \\angle 5$ (соотв.) $\\Rightarrow$ $\\angle 3 = \\angle 5$. Для односторонних: $\\angle 3 + \\angle 4 = 180°$ (смежные), $\\angle 3 = \\angle 5$ $\\Rightarrow$ $\\angle 4 + \\angle 5 = 180°$.</p>`);
html += makeCard('example', 'Пример. Один угол → все остальные', '17.4', `
<p><b>Задача.</b> $a \\parallel b$, секущая $c$. Известно, что $\\angle 1 = 65°$. Найди $\\angle 2$, $\\angle 5$, $\\angle 6$.</p>
<p><b>Решение.</b></p>
<ul style="padding-left:22px;line-height:1.9">
<li>$\\angle 2 = 180° - 65° = 115°$ (смежный к $\\angle 1$);</li>
<li>$\\angle 5 = \\angle 1 = 65°$ (соответственные);</li>
<li>$\\angle 6 = \\angle 2 = 115°$ (соответственные) или $180° - 65° = 115°$ (односторонний к $\\angle 1$ — нет, по схеме это $\\angle 6$ соотв. $\\angle 2$).</li>
</ul>
<p>Получаем правило: при $a \\parallel b$ <b>все 8 углов</b> разбиваются на <b>2 группы</b> — равных между собой. Если один из углов $= \\alpha$, то остальные либо $\\alpha$, либо $180° - \\alpha$.</p>`);
/* ИНТЕРАКТИВ 1 — найти угол */
html += '<div class="wg" id="p17-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Найди угол по свойствам</div></div>'
+'<div class="wg-help">При $a \\parallel b$ используй: соответственные равны, накрест лежащие равны, односторонние дают $180°$.</div>'
+trainerHTML('p17-iv1', 6, 'градусы')
+'</div>';
/* ИНТЕРАКТИВ 2 — найди все углы */
html += '<div class="wg" id="p17-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Сколько углов равно?</div></div>'
+'<div class="wg-help">При $a \\parallel b$ и секущей все 8 углов распадаются на 2 группы. Найди число углов, равных $\\angle 1$.</div>'
+trainerHTML('p17-iv2', 4, 'число')
+'</div>';
html += secNav('p16', 'p18') + readButton('p17');
box.innerHTML = html; renderMath(box);
makeTrainer({
idPrefix:'p17-iv1',
questions:[
{ q:'$a \\parallel b$. $\\angle 1 = 70°$. Найди соответственный $\\angle 5$.', a:70 },
{ q:'$a \\parallel b$. $\\angle 3 = 55°$. Найди накрест лежащий $\\angle 5$.', a:55 },
{ q:'$a \\parallel b$. $\\angle 4 = 110°$. Найди односторонний $\\angle 5$.', a:70 },
{ q:'$a \\parallel b$. $\\angle 1 = 80°$. Найди смежный с ним $\\angle 2$.', a:100 },
{ q:'$a \\parallel b$. $\\angle 2 = 130°$. Найди $\\angle 6$ (соответственный).', a:130 },
{ q:'$a \\parallel b$. $\\angle 5 + \\angle 4 = 180°$. $\\angle 5 = 75°$. Найди $\\angle 4$.', a:105 },
],
onComplete:(s,n)=>{ if(s===n){addXp(18,'p17-iv1');bumpProgress('p17',30);} else if(s>=4){addXp(9,'p17-iv1');bumpProgress('p17',15);} }
});
makeTrainer({
idPrefix:'p17-iv2',
questions:[
{ q:'$a \\parallel b$, секущая. Сколько всего углов вокруг 2 точек пересечения?', a:8 },
{ q:'$a \\parallel b$. Сколько углов равно $\\angle 1$ (включая сам $\\angle 1$)?', a:4 },
{ q:'$a \\parallel b$, $\\angle 1 = 60°$. Сколько углов равно $120°$?', a:4 },
{ q:'$a \\parallel b$. Сколько <b>разных</b> по величине углов в схеме (если $\\angle 1 \\ne 90°$)?', a:2 },
],
onComplete:(s,n)=>{ if(s===n){addXp(15,'p17-iv2');bumpProgress('p17',25);} else if(s>=2){addXp(7,'p17-iv2');bumpProgress('p17',10);} }
});
wireReadBtn('p17');
}
/* ============================================================
\xA7 18 — Углы с соответственно параллельными или перпендикулярными сторонами
============================================================ */
function buildP18(){
const box = document.getElementById('p18-body');
const G = window.GEOM7;
let html = '';
/* SVG: два угла с параллельными сторонами (оба острых — равны) */
let svgPar='';
if(G){
const b=G.svgBox(320,200,{id:'p18-par',cell:20});
const V1={x:60,y:150};
const A1={x:160,y:150}, B1={x:130,y:60};
const V2={x:200,y:80};
const A2={x:300,y:80}, B2={x:270,y:-10};
svgPar = b.open
+ G.segment(V1,A1,{color:'#7c3aed',width:2.5})
+ G.segment(V1,B1,{color:'#0891b2',width:2.5})
+ G.angle(V1,A1,B1,{color:'#dc2626',r:30,label:'∠ 1',fontSize:12,labelOffset:14})
+ G.point(V1.x,V1.y,'',{r:3,color:'#7c3aed'})
+ G.segment(V2,A2,{color:'#7c3aed',width:2.5,dash:'4 3'})
+ G.segment(V2,{x:270,y:30},{color:'#0891b2',width:2.5,dash:'4 3'})
+ G.angle(V2,A2,{x:270,y:30},{color:'#dc2626',r:30,label:'∠ 2',fontSize:12,labelOffset:14})
+ G.point(V2.x,V2.y,'',{r:3,color:'#7c3aed'})
+ b.close;
}
html += makeCard('theory', 'Сравнение углов через стороны', '18.1', `
<p>Если у двух углов <b>стороны одного соответственно параллельны сторонам другого</b>, то возможны два случая:</p>
<ul style="padding-left:22px;line-height:1.85">
<li><b>оба угла равны</b> (если оба острые или оба тупые);</li>
<li><b>в сумме дают $180°$</b> (если один острый, другой тупой).</li>
</ul>
<div class="svg-host">`+svgPar+`</div>
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема.</b> Если стороны одного угла соответственно параллельны сторонам другого, то такие углы либо <b>равны</b>, либо в сумме дают $180°$.</p>`);
html += makeCard('rule', 'Перпендикулярные стороны — тот же закон', '18.2', `
<p style="background:var(--sec-acc-soft);padding:10px 14px;border-radius:8px"><b>Теорема.</b> Если стороны одного угла соответственно <b>перпендикулярны</b> сторонам другого, то такие углы либо <b>равны</b>, либо в сумме дают $180°$.</p>
<p>Это работает по тому же принципу: оба острых или оба тупых $\\Rightarrow$ равны; разные типы $\\Rightarrow$ дополняют до $180°$.</p>
<details class="spoiler"><summary>Идея доказательства (параллельные стороны)</summary>
<div class="spoiler-body">
<p>Пусть у $\\angle AOB$ и $\\angle A_1O_1B_1$: $OA \\parallel O_1A_1$, $OB \\parallel O_1B_1$.</p>
<p>Проведём прямую через $O$ и $O_1$. Она будет <b>секущей</b> для пар параллельных прямых. Применяя свойство соответственных (накрест лежащих) углов параллельных прямых для каждой пары сторон, получаем нужное равенство углов.</p>
</div></details>`);
html += makeCard('algo', 'Как пользоваться правилом', '18.3', `
<p>При решении задач:</p>
<ol style="padding-left:22px;line-height:1.9">
<li>Определи тип каждого угла: острый или тупой.</li>
<li>Если <b>оба острые</b> или <b>оба тупые</b> — углы <b>равны</b>.</li>
<li>Если <b>один острый</b>, а <b>другой тупой</b> — их сумма $= 180°$.</li>
<li>Прямые углы (по $90°$) <b>всегда равны</b>.</li>
</ol>`);
html += makeCard('example', 'Пример. Острый + острый', '18.4', `
<p><b>Задача.</b> Стороны $\\angle A$ параллельны сторонам $\\angle B$. $\\angle A = 40°$, $\\angle B$ — острый. Найди $\\angle B$.</p>
<p><b>Решение.</b> Оба угла острые $\\Rightarrow$ они равны. $\\angle B = 40°$.</p>
<p><b>Задача 2.</b> Стороны $\\angle A$ перпендикулярны сторонам $\\angle B$. $\\angle A = 70°$, $\\angle B$ тупой. Найди $\\angle B$.</p>
<p><b>Решение.</b> Один острый ($70°$), другой тупой $\\Rightarrow$ $\\angle B = 180° - 70° = 110°$.</p>`);
/* ИНТЕРАКТИВ 1 — выбор по типам углов */
html += '<div class="wg" id="p18-iv1">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Равны или в сумме $180°$?</div></div>'
+'<div class="wg-help">Стороны одного $\\parallel$ или $\\perp$ сторонам другого. Реши: равны или сумма $= 180°$.</div>'
+'<div class="score-display"><span>Задача <b id="p18-iv1-i">1</b> / 6</span><span>Очки: <b id="p18-iv1-s">0</b> / 6</span></div>'
+'<div id="p18-iv1-q" style="padding:14px;background:var(--sec-acc-soft);border-radius:10px;font-size:1.02rem;text-align:center;margin-bottom:10px"></div>'
+'<div style="display:flex;gap:8px;justify-content:center;flex-wrap:wrap"><button class="btn primary" id="p18-iv1-eq" style="background:#10b981;border-color:#10b981">Равны</button><button class="btn primary" id="p18-iv1-sum" style="background:#f59e0b;border-color:#f59e0b">Сумма $180°$</button></div>'
+'<div class="feedback" id="p18-iv1-fb"></div></div>';
/* ИНТЕРАКТИВ 2 — найди величину угла */
html += '<div class="wg" id="p18-iv2">'
+'<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Найди величину угла</div></div>'
+'<div class="wg-help">Используй правило: одинаковые типы $\\Rightarrow$ равны; разные $\\Rightarrow$ сумма $180°$.</div>'
+trainerHTML('p18-iv2', 6, 'градусы')
+'</div>';
html += secNav('p17', 'final3') + readButton('p18');
box.innerHTML = html; renderMath(box);
(function(){
const Q=[
{ e:'Стороны $\\parallel$. $\\angle A = 40°$ (острый), $\\angle B$ — острый.', ans:'eq' },
{ e:'Стороны $\\parallel$. $\\angle A = 130°$ (тупой), $\\angle B$ — острый.', ans:'sum' },
{ e:'Стороны $\\parallel$. $\\angle A = 100°$ (тупой), $\\angle B$ — тупой.', ans:'eq' },
{ e:'Стороны $\\perp$. $\\angle A = 30°$ (острый), $\\angle B$ — тупой.', ans:'sum' },
{ e:'Стороны $\\perp$. $\\angle A = 55°$ (острый), $\\angle B$ — острый.', ans:'eq' },
{ e:'Стороны $\\parallel$. $\\angle A = 90°$, $\\angle B = 90°$.', ans:'eq' },
];
let i=0,score=0;
function show(){
if(i>=Q.length){ document.getElementById('p18-iv1-q').innerHTML='<b>Готово!</b> '+score+' / '+Q.length; if(score===Q.length){addXp(15,'p18-iv1');bumpProgress('p18',25);} else if(score>=4){addXp(8,'p18-iv1');bumpProgress('p18',12);} return; }
document.getElementById('p18-iv1-i').textContent=(i+1);
document.getElementById('p18-iv1-s').textContent=score;
document.getElementById('p18-iv1-q').innerHTML=Q[i].e;
renderMath(document.getElementById('p18-iv1-q'));
document.getElementById('p18-iv1-fb').style.display='none';
}
function ans(a){
if(i>=Q.length) return;
const fb=document.getElementById('p18-iv1-fb');
if(a===Q[i].ans){ score++; feedback(fb,true,'&#10003; Верно!'); }
else{ const lab={eq:'равны',sum:'сумма $= 180°$'}; feedback(fb,false,'&#10007; Правильно: <b>'+lab[Q[i].ans]+'</b>'); }
document.getElementById('p18-iv1-s').textContent=score;
i++; setTimeout(show,1200);
}
document.getElementById('p18-iv1-eq').addEventListener('click',()=>ans('eq'));
document.getElementById('p18-iv1-sum').addEventListener('click',()=>ans('sum'));
show();
})();
makeTrainer({
idPrefix:'p18-iv2',
questions:[
{ q:'Стороны $\\parallel$. $\\angle A = 35°$, $\\angle B$ острый. $\\angle B = ?$', a:35 },
{ q:'Стороны $\\parallel$. $\\angle A = 120°$, $\\angle B$ острый. $\\angle B = ?$', a:60 },
{ q:'Стороны $\\perp$. $\\angle A = 50°$, $\\angle B$ острый. $\\angle B = ?$', a:50 },
{ q:'Стороны $\\perp$. $\\angle A = 70°$, $\\angle B$ тупой. $\\angle B = ?$', a:110 },
{ q:'Стороны $\\parallel$. $\\angle A = 90°$. $\\angle B = ?$', a:90 },
{ q:'Стороны $\\parallel$. $\\angle A = 145°$, $\\angle B$ тупой. $\\angle B = ?$', a:145 },
],
onComplete:(s,n)=>{ if(s===n){addXp(18,'p18-iv2');bumpProgress('p18',30);} else if(s>=4){addXp(9,'p18-iv2');bumpProgress('p18',15);} }
});
wireReadBtn('p18');
}
/* ============================================================
FINAL 3 — 5 БОССОВ
============================================================ */
const BOSSES = [
{
n:1, title:'Босс \xA715 — Признаки параллельности', color:'#7c3aed',
steps:[
{ q:'Сколько признаков параллельности изучено? (число)', verify:(v)=>+v===3, hint:'Соотв., накрест леж., односторон.' },
{ q:'$\\angle 1 = \\angle 5 = 60°$ (соответственные). Параллельны? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'1-й признак.' },
{ q:'$\\angle 4 + \\angle 5 = 180°$ (односторонние). Параллельны? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'3-й признак.' },
{ q:'$\\angle 3 = 50°$, $\\angle 5 = 60°$ (накрест лежащие). Параллельны? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('н'), hint:'Не равны — нет.' },
{ q:'Сколько углов всего возникает при 2 прямых и секущей?', verify:(v)=>+v===8, hint:'4 + 4.' },
]
},
{
n:2, title:'Босс \xA716 — Аксиома параллельных', color:'#a855f7',
steps:[
{ q:'Через точку вне прямой можно провести $?$ прямых, $\\parallel$ ей. (число)', verify:(v)=>+v===1, hint:'Только одна.' },
{ q:'Аксиома: можно/нельзя её доказать? Введи «можно» или «нельзя».', verify:(v)=>String(v).trim().toLowerCase().startsWith('нельз'), hint:'Принимается без доказательства.' },
{ q:'$a \\parallel b$, $c$ пересекает $a$. Пересечёт ли $c$ прямую $b$? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'Следствие 1.' },
{ q:'$a \\parallel c$ и $b \\parallel c$. Тогда $a$ и $b$ ... ? Введи «параллельны» или «пересекаются».', verify:(v)=>String(v).trim().toLowerCase().startsWith('парал'), hint:'Транзитивность.' },
{ q:'Какой постулат Евклида — это аксиома параллельных? (число)', verify:(v)=>+v===5, hint:'Пятый.' },
]
},
{
n:3, title:'Босс \xA717 — Свойства параллельных', color:'#c026d3',
steps:[
{ q:'$a \\parallel b$, $\\angle 1 = 65°$. Найди соотв. $\\angle 5$.', verify:(v)=>+v===65, hint:'Соотв. равны.' },
{ q:'$a \\parallel b$, $\\angle 3 = 80°$. Найди накрест лежащий $\\angle 5$.', verify:(v)=>+v===80, hint:'Накрест леж. равны.' },
{ q:'$a \\parallel b$, $\\angle 4 = 110°$. Найди односторонний $\\angle 5$.', verify:(v)=>+v===70, hint:'Сумма $= 180°$.' },
{ q:'$a \\parallel b$, $\\angle 1 = 70°$. Сколько углов из 8 равны $70°$? (число)', verify:(v)=>+v===4, hint:'Каждые 4 равны.' },
{ q:'$a \\parallel b$, $\\angle 1 = \\angle 5 = ?$ Это свойство называется «соответственные ...» — введи слово.', verify:(v)=>String(v).trim().toLowerCase().startsWith('равн'), hint:'Равны.' },
]
},
{
n:4, title:'Босс \xA718 — Углы со сторонами ∥ / ⊥', color:'#db2777',
steps:[
{ q:'Стороны $\\parallel$, оба острые. Равны или 180°? Введи «равны» или «180».', verify:(v)=>String(v).trim().toLowerCase().startsWith('равн'), hint:'Оба острые — равны.' },
{ q:'Стороны $\\parallel$. $\\angle A = 50°$, $\\angle B$ острый. $\\angle B = ?$', verify:(v)=>+v===50, hint:'Равны.' },
{ q:'Стороны $\\parallel$. $\\angle A = 120°$, $\\angle B$ острый. $\\angle B = ?$', verify:(v)=>+v===60, hint:'$180 - 120$.' },
{ q:'Стороны $\\perp$. $\\angle A = 35°$, $\\angle B$ тупой. $\\angle B = ?$', verify:(v)=>+v===145, hint:'$180 - 35$.' },
{ q:'Стороны $\\parallel$, $\\angle A = 90°$. $\\angle B = ?$', verify:(v)=>+v===90, hint:'Прямые углы всегда равны.' },
]
},
{
n:5, title:'Финальный босс — Параллельность от и до', color:'#dc2626',
steps:[
{ q:'$a \\parallel b$, секущая. Один из углов $= 70°$. Сколько различных значений у 8 углов? (число)', verify:(v)=>+v===2, hint:'$70°$ и $180°-70°=110°$.' },
{ q:'$a \\parallel b$, односторонние = $x$ и $2x$. Найди меньший $x$ (градусы).', verify:(v)=>+v===60, hint:'Сумма $= 180°$: $x + 2x = 180 \\Rightarrow x = 60°$.' },
{ q:'Две прямые $\\parallel$ третьей. Параллельны ли они между собой? «да»/«нет»', verify:(v)=>String(v).trim().toLowerCase().startsWith('д'), hint:'Следствие 2.' },
{ q:'Стороны $\\angle A$ перпендикулярны сторонам $\\angle B$, оба тупых. $\\angle A = 100°$. $\\angle B = ?$', verify:(v)=>+v===100, hint:'Оба тупых — равны.' },
{ q:'$a \\parallel b$, $\\angle 1 + \\angle 2 = ?$ (смежные при одной точке).', verify:(v)=>+v===180, hint:'Смежные дают $180°$.' },
]
},
];
function buildFinal3(){
const box = document.getElementById('final3-body');
let html = '';
html += makeCard('theory', 'Что мы изучили', 'Итог', `
<p>Глава 3 — основы параллельности на плоскости:</p>
<ul style="padding-left:22px;line-height:1.85">
<li>узнали, как <b>секущая</b> создаёт <b>8 углов</b> и три типа особых пар;</li>
<li>освоили <b>3 признака параллельности</b> (соотв., накрест леж., односторон.);</li>
<li>познакомились с <b>аксиомой параллельных</b> и её следствиями;</li>
<li>разобрали <b>обратные теоремы — свойства</b> параллельных прямых;</li>
<li>освоили правило для углов со <b>сторонами $\\parallel$ или $\\perp$</b>.</li>
</ul>
<p>5 боссов проверяют всю эту теорию.</p>`);
html += '<div id="bosses-container"></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" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>'
+'<div style="height:14px;background:rgba(124,58,237,.12);border-radius:9px;overflow:hidden"><div id="boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,#7c3aed,#a855f7);transition:width .5s"></div></div>'
+'</div>';
html += secNav('p18', null);
box.innerHTML = html; renderMath(box);
const cont = document.getElementById('bosses-container');
const BOSS_STATE = (function(){
try{ const s=localStorage.getItem('geometry7_ch3_bosses'); if(s) return JSON.parse(s); }catch(e){}
return BOSSES.map(()=>({stage:0,defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem('geometry7_ch3_bosses', JSON.stringify(BOSS_STATE)); }catch(e){} }
function refreshOverall(){
const won=BOSS_STATE.filter(b=>b.defeated).length;
const txt=document.getElementById('boss-overall'); if(txt) txt.textContent=won+' / '+BOSSES.length+' боссов побеждено';
const fill=document.getElementById('boss-overall-fill'); if(fill) fill.style.width=(won*100/BOSSES.length)+'%';
if(won>=BOSSES.length){ bumpProgress('final3',60); achievement('ch3_done','Глава 3 пройдена!'); }
}
cont.innerHTML = BOSSES.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('');
if(window.renderMathInElement) try{ renderMath(cont); }catch(e){}
BOSSES.forEach((b,idx)=>{
function show(){
const st=BOSS_STATE[idx];
const stageEl=document.getElementById('boss-'+idx+'-stage');
const fill=document.getElementById('boss-'+idx+'-fill');
const q=document.getElementById('boss-'+idx+'-q');
const fb=document.getElementById('boss-'+idx+'-fb');
if(st.defeated){
stageEl.textContent='✓ Побеждён'; fill.style.width='100%';
q.innerHTML='<b style="color:'+b.color+'">Босс повержен!</b>';
document.getElementById('boss-'+idx+'-go').disabled=true;
document.getElementById('boss-'+idx+'-go').style.opacity=.5;
return;
}
stageEl.textContent='Этап '+(st.stage+1)+' / '+b.steps.length;
fill.style.width=(st.stage*100/b.steps.length)+'%';
q.innerHTML=b.steps[st.stage].q;
document.getElementById('boss-'+idx+'-input').value='';
fb.style.display='none';
renderMath(q);
}
document.getElementById('boss-'+idx+'-go').addEventListener('click',()=>{
const st=BOSS_STATE[idx]; if(st.defeated) return;
const step=b.steps[st.stage];
const val=document.getElementById('boss-'+idx+'-input').value;
const fb=document.getElementById('boss-'+idx+'-fb');
if(!val.trim()){ feedback(fb,false,'&#10007; Введи ответ.'); return; }
if(step.verify(val)){
st.stage++;
if(st.stage>=b.steps.length){
st.defeated=true; saveBosses();
feedback(fb,true,'&#10003; Босс '+b.n+' побеждён! +20 XP');
addXp(20,'boss-'+b.n); bumpProgress('final3',18); refreshOverall();
setTimeout(show,1400);
}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>