Files
Learn_System/frontend/textbooks/geometry_10_r2.html
T
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

1070 lines
79 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>Геометрия 10 · Раздел 2 · «Параллельность»</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/stereo3d.js"></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:#059669; --pri2:#047857; --pri-soft:#d1fae5;
--acc:#10b981; --acc2:#059669; --acc-soft:#a7f3d0;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a140e; --card:#0d1c14; --card-soft:#10261c; --text:#d1fae5; --ink:#d1fae5; --muted:#86efac; --border:#14532d}
*{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,#064e3b 0%,#059669 55%,#86efac 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(134,239,172,.2);min-height:130px}
.hdr::before{content:'РАЗДЕЛ 2';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(255,255,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:'\2225';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(0,0,0,.18)}
.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(0,0,0,.12);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(0,0,0,.18);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(0,0,0,.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,var(--warn-bg),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
.psel-card .psel-done{position:absolute;top:6px;right:6px;width:18px;height:18px;border-radius:50%;background:#10b981;display:none;align-items:center;justify-content:center;box-shadow:0 2px 6px rgba(16,185,129,.45);z-index:2}
.psel-card .psel-done svg{width:11px;height:11px;stroke:#fff;fill:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:round}
.psel-card.done .psel-done{display:flex}
.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(--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(--pri-soft);position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--pri),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(--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(0,0,0,.04);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(0,0,0,.08)}
.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(--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}
.card-body ul{margin:6px 0 6px 22px;line-height:1.7}
.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(--pri-soft);border-color:var(--pri)}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--pri);color:#fff;border-color:var(--pri)}
.btn.primary:hover{background:var(--pri2);border-color:var(--pri2)}
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.wg{background:linear-gradient(135deg,var(--card),var(--pri-soft));border:1.5px solid 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(--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(--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),var(--pri-soft));border-left:4px solid var(--warn);padding:9px 14px;border-radius:9px}
.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;width:140px}
.tinp:focus{outline:0;border-color:var(--pri);box-shadow:0 0 0 3px var(--pri-soft)}
.actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;align-items:center}
.spoiler{border:1px solid var(--border);border-radius:10px;background:var(--card);margin:10px 0;overflow:hidden}
.spoiler summary{padding:8px 14px;background:var(--pri-soft);font-weight:700;cursor:pointer;font-size:.88rem;color: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(--pri);width:18px}
.spoiler[open] summary::before{content:'\2212'}
.spoiler-body{padding:10px 14px;font-size:.92rem;line-height:1.6}
.opts-row{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}
.opt-btn{padding:8px 14px;background:var(--card);border:1.5px solid var(--border);border-radius:9px;font-weight:700;font-size:.88rem;color:var(--text);cursor:pointer;transition:all .15s}
.opt-btn:hover{background:var(--pri-soft);border-color:var(--pri)}
.opt-btn.correct{background:var(--ok-bg);border-color:var(--ok);color:#065f46}
.opt-btn.wrong{background:var(--fail-bg);border-color:var(--fail);color:#991b1b}
.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(--warn-bg),var(--pri-soft));border:1.5px solid var(--warn);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--warn);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:#92400e;font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(0,0,0,.10);border-radius:6px;overflow:hidden;margin:7px 0}
.xp-fill{height:100%;background:linear-gradient(90deg,var(--warn),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}
.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,var(--pri),var(--warn));color:#fff;padding:12px 18px;border-radius:11px;font-weight:700;font-size:.9rem;box-shadow:0 8px 28px rgba(0,0,0,.32);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}
.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}
}
.viz3d{background:var(--card);border:1px solid var(--border);border-radius:11px;padding:12px;text-align:center;margin:10px 0}
.viz3d-row{display:flex;gap:12px;flex-wrap:wrap;justify-content:center;margin:10px 0}
.viz3d-cell{flex:1 1 180px;max-width:220px;text-align:center}
.viz3d-label{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:6px}
.boss-card{background:var(--card);border:2px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;transition:border-color .35s,box-shadow .35s,transform .2s}
.boss-card.solved{border-color:#10b981;box-shadow:0 0 0 3px rgba(16,185,129,.18)}
.boss-head{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap}
.boss-tag{font-size:.7rem;font-weight:800;padding:3px 9px;border-radius:99px;background:var(--pri-soft);color:var(--pri2);letter-spacing:.04em;text-transform:uppercase}
.boss-title{font-family:'Unbounded',sans-serif;font-weight:800;color:var(--text);font-size:1.02rem;flex:1;min-width:0}
.boss-q{padding:12px 14px;background:var(--pri-soft);border-radius:10px;font-size:.96rem;line-height:1.55;margin-bottom:10px;color:var(--text)}
.boss-input{padding:8px 12px;border:1.5px solid var(--border);border-radius:8px;background:var(--card);color:var(--text);font-family:'JetBrains Mono',monospace;width:130px;text-align:center;font-size:.95rem}
.boss-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:6px}
.boss-fb{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none;line-height:1.45}
.boss-fb.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.boss-fb.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.stub-note{padding:18px 22px;background:linear-gradient(135deg,var(--pri-soft),var(--warn-bg));border:1.5px dashed var(--pri);border-radius:13px;text-align:center;color:var(--text);margin-bottom:14px}
.stub-note h3{font-family:'Unbounded',sans-serif;color:var(--pri2);margin-bottom:8px;font-size:1.05rem}
.stub-note p{color:var(--muted);font-size:.9rem;line-height:1.55}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 10 · Раздел 2</h1>
<div class="hdr-sub">Параллельность · прямые в пространстве, прямая и плоскость, две плоскости</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-10" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 10</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>3 случая взаимного расположения прямых (включая <b>скрещивающиеся</b>!), признаки параллельности прямой и плоскости, признак параллельности плоскостей.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p1')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 4</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-p1" class="sec" data-watermark="&#8741;"><div class="sec-header"><span class="sec-num">§ 4</span><h2 class="sec-h">Расположение прямых в пространстве</h2></div><div id="p1-body"></div></section>
<section id="sec-p2" class="sec" data-watermark="&#8869;"><div class="sec-header"><span class="sec-num">§ 5</span><h2 class="sec-h">Прямая и плоскость</h2></div><div id="p2-body"></div></section>
<section id="sec-p3" class="sec" data-watermark="&#8742;"><div class="sec-header"><span class="sec-num">§ 6</span><h2 class="sec-h">Две плоскости</h2></div><div id="p3-body"></div></section>
<section id="sec-final" class="sec" data-watermark="&#9733;"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#d97706,#f59e0b)">&#9733;</span><h2 class="sec-h">Финал раздела</h2></div><div id="final-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">Интерактивный учебник «Геометрия 10» · Раздел 2 · «Параллельность» · 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:'p1', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 3;
const _TB_SLUG = 'geometry-10-r2';
const PARAS = [
{ id:'p1', num:'§ 4', name:'Расположение прямых', sub:'Пересек. · парал. · скрещ.' },
{ id:'p2', num:'§ 5', name:'Прямая и плоскость', sub:'3 случая · признак $a \\parallel \\alpha$' },
{ id:'p3', num:'§ 6', name:'Две плоскости', sub:'Признак $\\alpha \\parallel \\beta$' },
{ id:'final', num:'&#9733;', name:'Финал раздела', sub:'4 интегрированных босса', final:true }
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
const ACH_LABELS = {
p1_done:'§4 — прямые в пространстве освоены',
p2_done:'§5 — прямая и плоскость освоены',
p3_done:'§6 — две плоскости освоены',
start:'Начало раздела 2!',
r2_done:'Параллельность освоена!'
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry10_r2_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry10_r2_achievements');
if(a){ const p=JSON.parse(a); 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('geometry10_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry10_r2_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry10_r2_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry10_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);
}
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,'geometry10-r2-'+(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)+'%'; if((STATE.progress[k]||0)>=100) el.classList.add('done'); });
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);
}
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><div class="psel-done"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></div>';
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
if(window.renderMathInElement) try{ renderMath(g); }catch(e){}
}
const BUILT=new Set();
const BUILDERS = { p1:()=>buildP1(), p2:()=>buildP2(), p3:()=>buildP3(), final:()=>buildFinal() };
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 = {
p1:{title:'Шпаргалка § 4', rows:[['Случаев','3 (пересек./парал./скрещ.)'],['Скрещ. прямые','Не лежат в одной плоскости'],['Транзит.','$a \\parallel b, b \\parallel c \\Rightarrow a \\parallel c$'],['Угол скрещ.','Через парал. перенос'],['Расстояние','Общий $\\perp$']]},
p2:{title:'Шпаргалка § 5', rows:[['Случаев','3 ($a \\subset \\alpha$, $a \\cap \\alpha$, $a \\parallel \\alpha$)'],['Признак $a \\parallel \\alpha$','$a \\parallel b \\subset \\alpha, a \\not\\subset \\alpha$'],['Свойство','$a \\parallel \\alpha, a \\subset \\beta, \\alpha \\cap \\beta = c \\Rightarrow a \\parallel c$']]},
p3:{title:'Шпаргалка § 6', rows:[['Случаев','2 (пересек./парал.)'],['Признак $\\alpha \\parallel \\beta$','2 пересек. прямые в $\\alpha$, обе $\\parallel \\beta$'],['Свойство','Парал. плоскости + сек. ⇒ парал. линии'],['Через точку','Единственная парал. плоскость']]},
final:{title:'Финал раздела 2', rows:[['§ 4','Прямые'],['§ 5','Прямая и плоскость'],['§ 6','Две плоскости'],['Боссов','4'],['Награда','+100 XP + ачивка']]}
};
const TIPS=[
{sec:'p1',html:'§ 4 — главное: <b>скрещивающиеся</b> прямые не лежат в одной плоскости (это и есть определение).'},
{sec:'p2',html:'§ 5 — признак $a \\parallel \\alpha$: достаточно найти в плоскости $\\alpha$ прямую, параллельную $a$.'},
{sec:'p3',html:'§ 6 — для $\\alpha \\parallel \\beta$ нужно <b>две пересекающиеся</b> прямые в $\\alpha$, парал. $\\beta$. Двух параллельных мало!'},
{sec:'final',html:'4 босса на интегральные задачи. После победы — ачивка «Параллельность освоена».'}
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
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];
if(tip){
html+='<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><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)">&#10003; '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem('geometry10_theme')||localStorage.getItem('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('geometry10_theme', dark?'dark':'light');
localStorage.setItem('theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
document.getElementById('sidebar-btn').addEventListener('click', ()=>{
document.getElementById('col-side').classList.toggle('open');
document.getElementById('col-side-backdrop').classList.toggle('show');
});
document.getElementById('col-side-backdrop').addEventListener('click', ()=>{
document.getElementById('col-side').classList.remove('open');
document.getElementById('col-side-backdrop').classList.remove('show');
});
}
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 = {
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 secNavFor(curId){
const idx = PARAS.findIndex(p => p.id === curId);
const prev = idx > 0 ? PARAS[idx-1].id : null;
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
const NAMES = {p1:'\xA74',p2:'\xA75',p3:'\xA76',final:'Финал'};
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 readButton(paraId){
const p = PARAS.find(x => x.id === paraId);
const labelTail = p && p.final ? 'финал' : (p ? p.num : '\xA7?');
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>'
+' Я прочитал — '+labelTail+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
const aId = paraId+'_done';
if(ACH_LABELS[aId]) achievement(aId);
});
}
function normalizeAns(s){
return String(s||'').toLowerCase()
.replace(/\s+/g,'').replace(/°/g,'')
.replace(/sqrt/g,'√').replace(/корень/g,'√')
.replace(/,/g,'.');
}
function makeBoss(id, def){
const stage = (typeof def.stage === 'number') ? def.stage : 0;
const solved = !!def.solved;
const total = def.stages.length;
const stageObj = def.stages[Math.min(stage, total-1)];
let optsHtml = '';
if(solved){
return '<div class="boss-card solved" id="boss-'+id+'"><div class="boss-head"><span class="boss-tag">'+(def.tag||'Босс')+'</span><span class="boss-title">'+def.title+'</span></div><div class="boss-q">Побеждён! +'+def.xp+' XP получены.</div></div>';
}
if(stageObj.type === 'mc'){
optsHtml = '<div class="opts-row">';
stageObj.opts.forEach((o,i)=>{ optsHtml += '<button class="opt-btn" data-i="'+i+'">'+o+'</button>'; });
optsHtml += '</div>';
} else {
optsHtml = '<div class="boss-row"><input class="boss-input" id="boss-'+id+'-inp" placeholder="ответ"><button class="btn primary" id="boss-'+id+'-go">Атака</button></div>';
}
return '<div class="boss-card" id="boss-'+id+'"><div class="boss-head"><span class="boss-tag">'+(def.tag||'Босс')+'</span><span class="boss-title">'+def.title+' — этап '+(stage+1)+' / '+total+'</span></div><div class="boss-q">'+stageObj.q+'</div>'+optsHtml+'<div class="boss-fb" id="boss-'+id+'-fb"></div></div>';
}
function bindBoss(id, def, state, save, onWin){
const card = document.getElementById('boss-'+id);
if(!card || state.solved) return;
const stageObj = def.stages[state.stage];
const fb = document.getElementById('boss-'+id+'-fb');
function advance(){
state.stage++;
if(state.stage >= def.stages.length){
state.solved = true; save(); addXp(def.xp, 'boss-'+id);
if(onWin) onWin();
} else { save(); }
rebuildBoss(id, def, state, save, onWin);
}
if(stageObj.type === 'mc'){
card.querySelectorAll('.opt-btn').forEach(btn=>{
btn.addEventListener('click', ()=>{
const i = +btn.dataset.i;
if(i === stageObj.correct){
btn.classList.add('correct');
fb.className='feedback ok'; fb.innerHTML='&#10003; Верно. '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
setTimeout(advance, 700);
} else {
btn.classList.add('wrong');
fb.className='feedback fail'; fb.innerHTML='&#10007; Не так. '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
}
});
});
} else {
const inp = document.getElementById('boss-'+id+'-inp');
const go = document.getElementById('boss-'+id+'-go');
function attack(){
const v = normalizeAns(inp.value);
const ans = Array.isArray(stageObj.a) ? stageObj.a.map(normalizeAns) : [normalizeAns(stageObj.a)];
if(ans.indexOf(v) >= 0){
fb.className='feedback ok'; fb.innerHTML='&#10003; Верно! '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
setTimeout(advance, 600);
} else {
fb.className='feedback fail'; fb.innerHTML='&#10007; Не то. '+(stageObj.explain||''); fb.style.display='block'; renderMath(fb);
}
}
go.addEventListener('click', attack);
inp.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); attack(); } });
}
}
function rebuildBoss(id, def, state, save, onWin){
const card = document.getElementById('boss-'+id);
if(!card) return;
card.outerHTML = makeBoss(id, Object.assign({}, def, state));
bindBoss(id, def, state, save, onWin);
renderMath(document.getElementById('boss-'+id));
}
function makeAndBindBoss(slotId, id, def, state, save, onWin){
const slot = document.getElementById(slotId); if(!slot) return;
slot.innerHTML = makeBoss(id, Object.assign({}, def, state));
bindBoss(id, def, state, save, onWin);
renderMath(slot);
}
function ensureStereo(cb){ if(window.STEREO3D) return cb(); setTimeout(()=>ensureStereo(cb), 60); }
/* ===== §4 (p1) ===== */
function buildP1(){
const box = document.getElementById('p1-body'); if(!box) return;
let html = '';
html += makeCard('theory', '3 случая взаимного расположения прямых', '§ 4.1',
'<p>В пространстве две прямые могут быть:</p>'
+ '<ul>'
+ '<li><b>Пересекающимися</b> — лежат в одной плоскости, имеют общую точку.</li>'
+ '<li><b>Параллельными</b> — лежат в одной плоскости, не пересекаются. Обозначение: $a \\parallel b$.</li>'
+ '<li><b>Скрещивающимися</b> — <b>не лежат в одной плоскости</b>.</li>'
+ '</ul>'
+ '<div class="viz3d-row" style="margin-top:10px"><div class="viz3d-cell"><div id="viz4-cross"></div><div class="viz3d-label">Пересек.</div></div><div class="viz3d-cell"><div id="viz4-par"></div><div class="viz3d-label">Парал.</div></div><div class="viz3d-cell"><div id="viz4-skew"></div><div class="viz3d-label">Скрещ.</div></div></div>');
html += makeCard('example', 'Куб: все 3 типа на одном теле', '§ 4.2',
'<p>В кубе $ABCDA_1B_1C_1D_1$:</p>'
+ '<ul>'
+ '<li><span style="color:#dc2626;font-weight:800">$AB$ и $AD$</span> — пересекающиеся (общая точка $A$).</li>'
+ '<li><span style="color:#16a34a;font-weight:800">$AA_1$ и $BB_1$</span> — параллельные (соседние боковые рёбра).</li>'
+ '<li><span style="color:#d97706;font-weight:800">$BC$ и $DD_1$</span> — скрещивающиеся (не пересекаются, не парал.).</li>'
+ '</ul>'
+ '<div class="viz3d" id="viz4-cube"></div>');
html += makeCard('rule', 'Теоремы о параллельных прямых', '§ 4.3',
'<p><b>Через точку $M$, не лежащую на прямой $a$, проходит единственная прямая, параллельная $a$.</b></p>'
+ '<p><b>Транзитивность:</b> если $a \\parallel b$ и $b \\parallel c$, то $a \\parallel c$.</p>'
+ '<p><b>Признак скрещивающихся:</b> если прямая $a \\subset \\alpha$, а $b$ пересекает $\\alpha$ в точке вне $a$, то $a$ и $b$ скрещиваются.</p>');
html += makeCard('theory', 'Угол и расстояние', '§ 4.4',
'<p><b>Угол между скрещивающимися прямыми</b> $a$ и $b$ — угол между двумя пересекающимися прямыми $a\' \\parallel a$ и $b\' \\parallel b$ (выбираются в общей точке).</p>'
+ '<p><b>Расстояние между скрещивающимися прямыми</b> — длина их <b>общего перпендикуляра</b>. Общий перпендикуляр существует и единствен.</p>');
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 1</span><span class="wg-title">Тип пары рёбер в кубе</span></div>'
+ '<div class="wg-help">Решено: <b id="i1-type-score">0</b> / 7.</div>'
+ '<div id="i1-type-q" style="margin:8px 0"></div><div class="opts-row" id="i1-type-opts"></div><div class="feedback" id="i1-type-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 2</span><span class="wg-title">Угол между рёбрами куба</span></div>'
+ '<div class="wg-help">Используй параллельный перенос. Решено: <b id="i1-ang-score">0</b> / 5.</div>'
+ '<div id="i1-ang-q" style="margin:8px 0"></div><div class="actions"><input class="tinp" id="i1-ang-inp" placeholder="°"><button class="btn primary" id="i1-ang-go">Проверить</button></div><div class="feedback" id="i1-ang-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 3</span><span class="wg-title">Верно или неверно</span></div>'
+ '<div class="wg-help">Решено: <b id="i1-tf-score">0</b> / 5.</div>'
+ '<div id="i1-tf-q" style="margin:8px 0"></div><div class="opts-row" id="i1-tf-opts"></div><div class="feedback" id="i1-tf-fb"></div></div>';
html += '<div id="boss-1-slot"></div>';
html += readButton('p1');
html += secNavFor('p1');
box.innerHTML = html;
ensureStereo(()=>buildLineCases());
ensureStereo(()=>buildCubePairs());
runQuizMC('i1-type', I1_TYPE_ITEMS, 14, 'тип пары');
runQuizInput('i1-ang', I1_ANG_ITEMS, 14, 'угол');
runQuizMC('i1-tf', I1_TF_ITEMS, 10, 'верно/неверно');
const bs = loadBossState('boss-1') || { stage:0, solved:false };
makeAndBindBoss('boss-1-slot', '1', BOSS_DEFS.b1, bs,
()=>saveBossState('boss-1', bs),
()=>{ bumpProgress('p1', 40); achievement('p1_done'); });
wireReadBtn('p1');
renderMath(box);
}
/* ===== §5 (p2) ===== */
function buildP2(){
const box = document.getElementById('p2-body'); if(!box) return;
let html = '';
html += makeCard('theory', '3 случая для прямой и плоскости', '§ 5.1',
'<p>Прямая $a$ и плоскость $\\alpha$ могут располагаться:</p>'
+ '<ul>'
+ '<li>$a \\subset \\alpha$ — <b>лежит в плоскости</b> (бесконечно общих точек).</li>'
+ '<li>$a \\cap \\alpha = M$ — <b>пересекает</b> в одной точке.</li>'
+ '<li>$a \\parallel \\alpha$ — <b>параллельна</b> (0 общих точек).</li>'
+ '</ul>'
+ '<div class="viz3d-row"><div class="viz3d-cell"><div id="viz5-in"></div><div class="viz3d-label">$a \\subset \\alpha$</div></div><div class="viz3d-cell"><div id="viz5-cross"></div><div class="viz3d-label">$a \\cap \\alpha = M$</div></div><div class="viz3d-cell"><div id="viz5-par"></div><div class="viz3d-label">$a \\parallel \\alpha$</div></div></div>');
html += makeCard('rule', 'Признак параллельности прямой и плоскости', '§ 5.2',
'<p>Если прямая $a$ <b>параллельна</b> некоторой прямой $b \\subset \\alpha$ и сама $a$ не лежит в $\\alpha$, то $a \\parallel \\alpha$.</p>'
+ '<p style="text-align:center;margin:10px 0">$$a \\parallel b \\subset \\alpha,\\ a \\not\\subset \\alpha \\Rightarrow a \\parallel \\alpha$$</p>'
+ '<div class="viz3d" id="viz5-sign"></div>'
+ '<p style="font-size:.86rem;color:var(--muted)">Найди в плоскости прямую, параллельную $a$ — этого достаточно.</p>');
html += makeCard('example', 'Свойство параллельной прямой', '§ 5.3',
'<p>Если $a \\parallel \\alpha$ и плоскость $\\beta \\supset a$ пересекает $\\alpha$ по $c$, то $a \\parallel c$.</p>'
+ '<p>Транзитивность: $a \\parallel \\alpha, b \\parallel a \\Rightarrow b \\parallel \\alpha$ или $b \\subset \\alpha$.</p>');
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 1</span><span class="wg-title">Какой случай взаимного расположения?</span></div>'
+ '<div class="wg-help">Решено: <b id="i2-case-score">0</b> / 6.</div>'
+ '<div id="i2-case-q" style="margin:8px 0"></div><div class="opts-row" id="i2-case-opts"></div><div class="feedback" id="i2-case-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 2</span><span class="wg-title">Применение признака</span></div>'
+ '<div class="wg-help">Решено: <b id="i2-sign-score">0</b> / 5.</div>'
+ '<div id="i2-sign-q" style="margin:8px 0"></div><div class="opts-row" id="i2-sign-opts"></div><div class="feedback" id="i2-sign-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 3</span><span class="wg-title">Параллельность в кубе</span></div>'
+ '<div class="wg-help">Решено: <b id="i2-cube-score">0</b> / 5.</div>'
+ '<div id="i2-cube-q" style="margin:8px 0"></div><div class="opts-row" id="i2-cube-opts"></div><div class="feedback" id="i2-cube-fb"></div></div>';
html += '<div id="boss-2-slot"></div>';
html += readButton('p2');
html += secNavFor('p2');
box.innerHTML = html;
ensureStereo(()=>buildPlaneLineCases());
ensureStereo(()=>buildSignLineToPlane());
runQuizMC('i2-case', I2_CASE_ITEMS, 12, 'случаи');
runQuizMC('i2-sign', I2_SIGN_ITEMS, 12, 'признак');
runQuizMC('i2-cube', I2_CUBE_ITEMS, 12, 'куб');
const bs = loadBossState('boss-2') || { stage:0, solved:false };
makeAndBindBoss('boss-2-slot', '2', BOSS_DEFS.b2, bs,
()=>saveBossState('boss-2', bs),
()=>{ bumpProgress('p2', 40); achievement('p2_done'); });
wireReadBtn('p2');
renderMath(box);
}
/* ===== §6 (p3) ===== */
function buildP3(){
const box = document.getElementById('p3-body'); if(!box) return;
let html = '';
html += makeCard('theory', '2 случая для двух плоскостей', '§ 6.1',
'<p>Две различные плоскости либо <b>пересекаются по прямой</b>, либо <b>параллельны</b> (не имеют общих точек). Третьего не дано.</p>'
+ '<div class="viz3d-row"><div class="viz3d-cell"><div id="viz6-cross"></div><div class="viz3d-label">Пересек. по прямой</div></div><div class="viz3d-cell"><div id="viz6-par"></div><div class="viz3d-label">Параллельны</div></div></div>');
html += makeCard('rule', 'Признак параллельности плоскостей', '§ 6.2',
'<p>Если плоскость $\\alpha$ содержит <b>две пересекающиеся</b> прямые $a, b$, каждая из которых параллельна $\\beta$ — то $\\alpha \\parallel \\beta$.</p>'
+ '<p style="text-align:center;margin:10px 0">$$a, b \\subset \\alpha;\\ a \\cap b \\neq \\varnothing;\\ a \\parallel \\beta;\\ b \\parallel \\beta \\Rightarrow \\alpha \\parallel \\beta$$</p>'
+ '<div class="viz3d" id="viz6-sign"></div>'
+ '<p style="font-size:.86rem;color:var(--muted)"><b>Одной</b> параллельной прямой недостаточно. <b>Двух параллельных</b> — тоже мало (могут лежать в плоскости, пересекающей $\\beta$). Нужны именно <b>две пересекающиеся</b>.</p>');
html += makeCard('example', 'Свойства параллельных плоскостей', '§ 6.3',
'<ul>'
+ '<li>Через точку $M \\notin \\alpha$ — единственная плоскость, параллельная $\\alpha$.</li>'
+ '<li>Если $\\alpha \\parallel \\beta$ и $\\gamma$ пересекает $\\alpha$ по $a$, то $\\gamma$ пересекает $\\beta$ по $b \\parallel a$.</li>'
+ '<li>Параллельные плоскости отсекают на параллельных прямых <b>равные отрезки</b>.</li>'
+ '<li>Транзитивность: $\\alpha \\parallel \\gamma, \\beta \\parallel \\gamma \\Rightarrow \\alpha \\parallel \\beta$.</li>'
+ '</ul>');
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 1</span><span class="wg-title">Параллельны или пересекаются?</span></div>'
+ '<div class="wg-help">Решено: <b id="i3-case-score">0</b> / 5.</div>'
+ '<div id="i3-case-q" style="margin:8px 0"></div><div class="opts-row" id="i3-case-opts"></div><div class="feedback" id="i3-case-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 2</span><span class="wg-title">Достаточно ли условий?</span></div>'
+ '<div class="wg-help">Решено: <b id="i3-sign-score">0</b> / 5.</div>'
+ '<div id="i3-sign-q" style="margin:8px 0"></div><div class="opts-row" id="i3-sign-opts"></div><div class="feedback" id="i3-sign-fb"></div></div>';
html += '<div class="wg"><div class="wg-header"><span class="wg-badge">Инт. 3</span><span class="wg-title">Свойства параллельных плоскостей</span></div>'
+ '<div class="wg-help">Решено: <b id="i3-prop-score">0</b> / 5.</div>'
+ '<div id="i3-prop-q" style="margin:8px 0"></div><div class="opts-row" id="i3-prop-opts"></div><div class="feedback" id="i3-prop-fb"></div></div>';
html += '<div id="boss-3-slot"></div>';
html += readButton('p3');
html += secNavFor('p3');
box.innerHTML = html;
ensureStereo(()=>buildPlanePlaneCases());
ensureStereo(()=>buildSignPlaneToPlane());
runQuizMC('i3-case', I3_CASE_ITEMS, 12, 'случаи');
runQuizMC('i3-sign', I3_SIGN_ITEMS, 12, 'признак');
runQuizMC('i3-prop', I3_PROP_ITEMS, 12, 'свойства');
const bs = loadBossState('boss-3') || { stage:0, solved:false };
makeAndBindBoss('boss-3-slot', '3', BOSS_DEFS.b3, bs,
()=>saveBossState('boss-3', bs),
()=>{ bumpProgress('p3', 40); achievement('p3_done'); });
wireReadBtn('p3');
renderMath(box);
}
/* ===== Final ===== */
function buildFinal(){
const box = document.getElementById('final-body'); if(!box) return;
let html = '';
html += '<div class="stub-note"><h3>Финальное испытание</h3><p>Победи 4 интегрированных боссов (прямые, прямая и плоскость, две плоскости, сборная). После — ачивка «Параллельность освоена» + 100 XP бонус.</p></div>';
html += '<div id="boss-f1-slot"></div>';
html += '<div id="boss-f2-slot"></div>';
html += '<div id="boss-f3-slot"></div>';
html += '<div id="boss-f4-slot"></div>';
html += '<div id="celebration" style="display:none;margin-top:18px"></div>';
html += secNavFor('final');
box.innerHTML = html;
['f1','f2','f3','f4'].forEach(id=>{
const def = FINAL_BOSS_DEFS[id];
const st = loadBossState('boss-'+id) || { stage:0, solved:false };
makeAndBindBoss('boss-'+id+'-slot', id, def, st,
()=>saveBossState('boss-'+id, st),
()=>{ checkFinalComplete(); });
});
checkFinalComplete();
renderMath(box);
}
function checkFinalComplete(){
const allBeat = ['f1','f2','f3','f4'].every(k=>{
const st = loadBossState('boss-'+k); return st && st.solved;
});
if(!allBeat) return;
const cel = document.getElementById('celebration');
if(!cel || cel.dataset.shown === '1') return;
cel.dataset.shown = '1'; cel.style.display = 'block';
cel.innerHTML = '<div class="boss-card solved" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));text-align:center"><div style="font-family:Unbounded,sans-serif;font-size:1.4rem;font-weight:900;color:#92400e;margin-bottom:8px">&#9733; Раздел 2 пройден! &#9733;</div><div style="color:var(--text);margin-bottom:14px">Все 4 финальных босса побеждены. Параллельность — освоена.</div><div style="display:inline-block;padding:8px 18px;background:linear-gradient(135deg,#f59e0b,#dc2626);color:#fff;border-radius:99px;font-family:Unbounded,sans-serif;font-weight:800;font-size:.9rem">+ 100 XP бонус + ачивка stereo10_r2_master</div></div>';
const ach = JSON.parse(localStorage.getItem('geometry10_achievements')||'[]');
if(ach.indexOf('stereo10_r2_master') < 0){
ach.push('stereo10_r2_master');
localStorage.setItem('geometry10_achievements', JSON.stringify(ach));
addXp(100, 'r2-master');
achievement('r2_done', 'Параллельность освоена!');
}
bumpProgress('final', 100);
}
function loadBossState(key){ try{ return JSON.parse(localStorage.getItem('geometry10_r2_'+key)||'null'); }catch(e){ return null; } }
function saveBossState(key, state){ try{ localStorage.setItem('geometry10_r2_'+key, JSON.stringify(state)); }catch(e){} }
function runQuizMC(id, items, xpReward){
const state = JSON.parse(localStorage.getItem('geometry10_r2_quiz_'+id)||'null') || { idx:0, solved:0, awarded:false };
const qEl = document.getElementById(id+'-q');
const optsEl = document.getElementById(id+'-opts');
const fbEl = document.getElementById(id+'-fb');
const scoreEl = document.getElementById(id+'-score');
function save(){ localStorage.setItem('geometry10_r2_quiz_'+id, JSON.stringify(state)); }
function render(){
if(state.solved >= items.length){
qEl.innerHTML = '<b style="color:var(--ok)">Все задания решены!</b> +'+xpReward+' XP.';
optsEl.innerHTML = ''; fbEl.style.display='none';
if(scoreEl) scoreEl.textContent = state.solved;
if(!state.awarded){ state.awarded = true; save(); addXp(xpReward, 'quiz-'+id); }
return;
}
const it = items[state.idx % items.length];
qEl.innerHTML = it.q; optsEl.innerHTML = ''; fbEl.style.display='none';
if(scoreEl) scoreEl.textContent = state.solved;
it.opts.forEach((o,i)=>{
const b = document.createElement('button'); b.className = 'opt-btn'; b.innerHTML = o;
b.addEventListener('click', ()=>{
if(i === it.correct){
b.classList.add('correct'); state.solved++; state.idx++; save();
if(scoreEl) scoreEl.textContent = state.solved;
fbEl.className='feedback ok'; fbEl.innerHTML='&#10003; Верно. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
setTimeout(render, 850);
} else {
b.classList.add('wrong');
fbEl.className='feedback fail'; fbEl.innerHTML='&#10007; Не так. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
}
});
optsEl.appendChild(b);
});
renderMath(qEl);
}
render();
}
function runQuizInput(id, items, xpReward){
const state = JSON.parse(localStorage.getItem('geometry10_r2_quiz_'+id)||'null') || { idx:0, solved:0, awarded:false };
const qEl = document.getElementById(id+'-q');
const inp = document.getElementById(id+'-inp');
const go = document.getElementById(id+'-go');
const fbEl = document.getElementById(id+'-fb');
const scoreEl = document.getElementById(id+'-score');
function save(){ localStorage.setItem('geometry10_r2_quiz_'+id, JSON.stringify(state)); }
function render(){
if(state.solved >= items.length){
qEl.innerHTML = '<b style="color:var(--ok)">Все задания решены!</b> +'+xpReward+' XP.';
inp.value=''; inp.disabled=true; go.disabled=true;
if(scoreEl) scoreEl.textContent = state.solved;
if(!state.awarded){ state.awarded = true; save(); addXp(xpReward, 'quiz-'+id); }
return;
}
const it = items[state.idx % items.length];
qEl.innerHTML = it.q; inp.value=''; inp.disabled=false; go.disabled=false; fbEl.style.display='none';
if(scoreEl) scoreEl.textContent = state.solved;
renderMath(qEl);
}
function check(){
const it = items[state.idx % items.length];
const v = normalizeAns(inp.value);
const ans = Array.isArray(it.answer) ? it.answer.map(normalizeAns) : [normalizeAns(it.answer)];
if(ans.indexOf(v) >= 0){
state.solved++; state.idx++; save();
if(scoreEl) scoreEl.textContent = state.solved;
fbEl.className='feedback ok'; fbEl.innerHTML='&#10003; Верно. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
setTimeout(render, 850);
} else {
fbEl.className='feedback fail'; fbEl.innerHTML='&#10007; Не так. '+(it.explain||''); fbEl.style.display='block'; renderMath(fbEl);
}
}
go.addEventListener('click', check);
inp.addEventListener('keydown', e=>{ if(e.key==='Enter'){ e.preventDefault(); check(); } });
render();
}
/* ===== Stereo3D vizes ===== */
function buildLineCases(){
const S = window.STEREO3D; if(!S) return;
(()=>{ const sc = new S.Scene(200,180,{view:'CABINET',scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.6,opacity:0.12});
sc.addEdge([-1.3,-0.5,0],[1.3,0.5,0],{stroke:'#dc2626',width:2.4});
sc.addEdge([-0.9,0.8,0],[1.0,-0.9,0],{stroke:'#1e3a8a',width:2.4});
sc.addVertex([0.05,0.05,0],'O',{dx:8,dy:-8,color:'#0b1d33'});
const el=document.getElementById('viz4-cross'); if(el) el.innerHTML=sc.render(); })();
(()=>{ const sc = new S.Scene(200,180,{view:'CABINET',scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.6,opacity:0.12});
sc.addEdge([-1.3,-0.5,0],[1.3,-0.5,0],{stroke:'#16a34a',width:2.4});
sc.addEdge([-1.3,0.5,0],[1.3,0.5,0],{stroke:'#16a34a',width:2.4});
const el=document.getElementById('viz4-par'); if(el) el.innerHTML=sc.render(); })();
(()=>{ const sc = new S.Scene(200,180,{view:'CABINET',scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.4,opacity:0.10,fill:'#86efac'});
sc.addEdge([-1.3,-0.4,0],[1.3,0.4,0],{stroke:'#dc2626',width:2.4});
sc.addEdge([-1.3,0.4,0.9],[1.3,-0.4,0.9],{stroke:'#d97706',width:2.4});
const el=document.getElementById('viz4-skew'); if(el) el.innerHTML=sc.render(); })();
}
function buildCubePairs(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(380, 280, {view:'CABINET', scale:50});
sc.addCube({center:[0,0,0], size:2.0, labels:true, color:'#d1fae5', opacity:0.20});
sc.addEdge([-1,-1,-1],[1,-1,-1],{stroke:'#dc2626',width:3});
sc.addEdge([-1,-1,-1],[-1,1,-1],{stroke:'#dc2626',width:3});
sc.addEdge([-1,-1,-1],[-1,-1,1],{stroke:'#16a34a',width:3});
sc.addEdge([1,-1,-1],[1,-1,1],{stroke:'#16a34a',width:3});
sc.addEdge([1,-1,-1],[1,1,-1],{stroke:'#d97706',width:3});
sc.addEdge([-1,1,-1],[-1,1,1],{stroke:'#d97706',width:3});
const el = document.getElementById('viz4-cube'); if(el) el.innerHTML = sc.render();
}
function buildPlaneLineCases(){
const S = window.STEREO3D; if(!S) return;
(()=>{ const sc = new S.Scene(200,180,{view:'CABINET',scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.6,label:'α'});
sc.addEdge([-1.3,-0.4,0],[1.3,0.4,0],{stroke:'#1e3a8a',width:2.4});
sc.addLabel('a',[1.3,0.4,0],{dx:12,dy:-4,color:'#1e3a8a',fontSize:14,anchor:'start'});
const el=document.getElementById('viz5-in'); if(el) el.innerHTML=sc.render(); })();
(()=>{ const sc = new S.Scene(200,180,{view:'CABINET',scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.6,label:'α'});
sc.addEdge([-0.4,-0.5,1.2],[0.5,0.5,-0.7],{stroke:'#dc2626',width:2.4});
sc.addVertex([0.12,0.05,0],'M',{dx:8,dy:-8,color:'#dc2626'});
const el=document.getElementById('viz5-cross'); if(el) el.innerHTML=sc.render(); })();
(()=>{ const sc = new S.Scene(200,180,{view:'CABINET',scale:38});
sc.addPlane([0,0,0],[0,0,1],{size:1.6,label:'α'});
sc.addEdge([-1.3,-0.3,0.9],[1.3,0.3,0.9],{stroke:'#16a34a',width:2.4});
sc.addLabel('a',[1.3,0.3,0.9],{dx:12,dy:-4,color:'#16a34a',fontSize:14,anchor:'start'});
const el=document.getElementById('viz5-par'); if(el) el.innerHTML=sc.render(); })();
}
function buildSignLineToPlane(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(380,240,{view:'CABINET',scale:46});
sc.addPlane([0,0,0],[0,0,1],{size:2.0,label:'α'});
sc.addEdge([-1.5,-0.5,0],[1.5,0.5,0],{stroke:'#1e3a8a',width:2.4});
sc.addLabel('b',[1.5,0.5,0],{dx:12,dy:-4,color:'#1e3a8a',fontSize:14,anchor:'start'});
sc.addEdge([-1.5,-0.5,1.0],[1.5,0.5,1.0],{stroke:'#16a34a',width:2.6});
sc.addLabel('a',[1.5,0.5,1.0],{dx:12,dy:-4,color:'#16a34a',fontSize:14,anchor:'start'});
sc.addEdge([-0.8,-0.2,1.0],[-0.8,-0.2,0],{stroke:'#94a3b8',width:1,dash:'3 3'});
const el = document.getElementById('viz5-sign'); if(el) el.innerHTML = sc.render();
}
function buildPlanePlaneCases(){
const S = window.STEREO3D; if(!S) return;
(()=>{ const sc = new S.Scene(240,200,{view:'CABINET',scale:42});
sc.addPlane([0,0,0],[0,0,1],{size:1.4,label:'α',opacity:0.22});
sc.addPlane([0,0,0],[Math.cos(Math.PI/4),0,Math.sin(Math.PI/4)],{size:1.4,label:'β',opacity:0.24,fill:'#fde047'});
sc.addEdge([-1.2,0,0],[1.2,0,0],{stroke:'#dc2626',width:2.6});
sc.addLabel('c',[1.2,0,0],{dx:14,dy:-4,color:'#dc2626',fontSize:14,anchor:'start'});
const el=document.getElementById('viz6-cross'); if(el) el.innerHTML=sc.render(); })();
(()=>{ const sc = new S.Scene(240,200,{view:'CABINET',scale:42});
sc.addPlane([0,0,-0.5],[0,0,1],{size:1.4,label:'α'});
sc.addPlane([0,0,+0.5],[0,0,1],{size:1.4,label:'β',fill:'#86efac',opacity:0.30});
const el=document.getElementById('viz6-par'); if(el) el.innerHTML=sc.render(); })();
}
function buildSignPlaneToPlane(){
const S = window.STEREO3D; if(!S) return;
const sc = new S.Scene(400,260,{view:'CABINET',scale:46});
sc.addPlane([0,0,-0.5],[0,0,1],{size:2.0,label:'β'});
sc.addPlane([0,0,+0.5],[0,0,1],{size:2.0,label:'α',fill:'#86efac',opacity:0.30});
sc.addEdge([-1.5,-0.6,0.5],[1.5,0.6,0.5],{stroke:'#dc2626',width:2.4});
sc.addEdge([-0.9,0.8,0.5],[1.0,-0.9,0.5],{stroke:'#1e3a8a',width:2.4});
sc.addVertex([0.05,0.05,0.5],'O',{dx:8,dy:-8,color:'#0b1d33'});
const el = document.getElementById('viz6-sign'); if(el) el.innerHTML = sc.render();
}
/* ===== Quiz items ===== */
const I1_TYPE_ITEMS = [
{ q:'Куб: пара $AB$ и $AD$ — это…', opts:['Парал.','Пересек.','Скрещ.'], correct:1, explain:'Общая точка $A$.' },
{ q:'Куб: пара $AA_1$ и $CC_1$ — это…', opts:['Парал.','Пересек.','Скрещ.'], correct:0, explain:'Все бок. рёбра прямой призмы парал.' },
{ q:'Куб: пара $AB$ и $CC_1$ — это…', opts:['Парал.','Пересек.','Скрещ.'], correct:2, explain:'Скрещиваются: не пересек., не парал.' },
{ q:'Куб: пара $AB$ и $A_1B_1$ — это…', opts:['Парал.','Пересек.','Скрещ.'], correct:0, explain:'Противоп. стороны прямоугольника.' },
{ q:'Куб: пара $A_1B_1$ и $C_1D_1$ — это…', opts:['Парал.','Пересек.','Скрещ.'], correct:0, explain:'Парал. стороны верхней грани.' },
{ q:'Куб: пара $AB$ и $A_1D_1$ — это…', opts:['Парал.','Пересек.','Скрещ.'], correct:2, explain:'Скрещиваются.' },
{ q:'Куб: $AC$ (диагональ нижней грани) и $BB_1$ — это…', opts:['Парал.','Пересек.','Скрещ.'], correct:2, explain:'Не пересек., не парал.' }
];
const I1_ANG_ITEMS = [
{ q:'Угол между $AB$ и $AD$ куба?', answer:'90', explain:'Стороны квадрата.' },
{ q:'Угол между скрещ. $AB$ и $CC_1$ куба?', answer:'90', explain:'$CC_1 \\to BB_1 \\perp AB$.' },
{ q:'Угол между диагоналями смежных граней из одной вершины (например $AB_1$ и $AD_1$)?', answer:'60', explain:'Стороны равностороннего $\\triangle AB_1D_1$.' },
{ q:'Угол между параллельными прямыми всегда равен … °.', answer:'0', explain:'$0°$.' },
{ q:'Угол между скрещ. диагоналями $AC$ и $B_1D_1$ куба?', answer:'90', explain:'$AC \\parallel A_1C_1 \\perp B_1D_1$.' }
];
const I1_TF_ITEMS = [
{ q:'Если две прямые не пересекаются, то они параллельны.', opts:['Верно','Неверно'], correct:1, explain:'Могут скрещиваться.' },
{ q:'Скрещ. прямые могут лежать в одной плоскости.', opts:['Верно','Неверно'], correct:1, explain:'По определению — нет.' },
{ q:'Через точку, не лежащую на прямой — единственная парал.', opts:['Верно','Неверно'], correct:0, explain:'Теорема.' },
{ q:'$a \\parallel b, b \\parallel c \\Rightarrow a \\parallel c$', opts:['Верно','Неверно'], correct:0, explain:'Транзитивность.' },
{ q:'Через любые 2 прямые в пространстве можно провести плоскость.', opts:['Верно','Неверно'], correct:1, explain:'Через скрещ. — нельзя.' }
];
const I2_CASE_ITEMS = [
{ q:'Прямая $a$ имеет 1 общую точку с $\\alpha$. Это…', opts:['$a \\subset \\alpha$','$a \\cap \\alpha = M$','$a \\parallel \\alpha$'], correct:1, explain:'Пересекает.' },
{ q:'Прямая не имеет общих точек с $\\alpha$. Это…', opts:['$a \\subset \\alpha$','$a \\cap \\alpha = M$','$a \\parallel \\alpha$'], correct:2, explain:'Парал.' },
{ q:'Прямая имеет 2 точки в $\\alpha$. Это…', opts:['$a \\subset \\alpha$','$a \\cap \\alpha = M$','$a \\parallel \\alpha$'], correct:0, explain:'A2.' },
{ q:'Сколько общих точек у $a \\parallel \\alpha$?', opts:['0','1','Бесконечно'], correct:0, explain:'Нуль.' },
{ q:'Прямая лежит в $\\alpha$. Сколько общих точек?', opts:['1','2','Бесконечно'], correct:2, explain:'Все.' },
{ q:'$a \\cap \\alpha = M$. Сколько общих точек?', opts:['1','2','Бесконечно'], correct:0, explain:'Только $M$.' }
];
const I2_SIGN_ITEMS = [
{ q:'$a \\parallel b \\subset \\alpha, a \\not\\subset \\alpha$. Вывод?', opts:['$a \\parallel \\alpha$','$a \\subset \\alpha$','$a \\cap \\alpha$'], correct:0, explain:'Признак.' },
{ q:'$a \\parallel b \\subset \\alpha$, но $a \\subset \\alpha$. Вывод?', opts:['$a \\parallel \\alpha$','$a \\subset \\alpha$','Невозм.'], correct:1, explain:'В плоскости — значит лежит в ней.' },
{ q:'Достаточно ли для $a \\parallel \\alpha$ просто не пересекать плоскость?', opts:['Да','Нет','Только в плоскости'], correct:0, explain:'По определению.' },
{ q:'$a \\parallel \\alpha, \\beta \\supset a, \\alpha \\cap \\beta = c$. Тогда $a$ и $c$:', opts:['Скрещ.','Парал.','Пересек.'], correct:1, explain:'$a \\parallel c$.' },
{ q:'$a \\parallel \\alpha, b \\parallel a$. Тогда $b$:', opts:['$\\parallel \\alpha$ или $\\subset \\alpha$','Пересек.','Скрещ.'], correct:0, explain:'Транзитивность.' }
];
const I2_CUBE_ITEMS = [
{ q:'Куб: прямая $AB$ и плоскость верхней грани $A_1B_1C_1D_1$:', opts:['Лежит','Пересек.','Парал.'], correct:2, explain:'$AB \\parallel A_1B_1$.' },
{ q:'Куб: $AA_1$ и плоскость $ABCD$:', opts:['Лежит','Пересек. в $A$','Парал.'], correct:1, explain:'Боковое ребро.' },
{ q:'Куб: $AC$ (диагональ грани $ABCD$) и плоскость $ABCD$:', opts:['Лежит','Пересек.','Парал.'], correct:0, explain:'Лежит.' },
{ q:'Куб: $A_1B_1$ и плоскость $ABCD$:', opts:['Лежит','Пересек.','Парал.'], correct:2, explain:'Парал.' },
{ q:'Сколько рёбер куба парал. одной заданной грани (не лежат в ней)?', opts:['2','4','6','8'], correct:1, explain:'4 ребра противоположной грани.' }
];
const I3_CASE_ITEMS = [
{ q:'Две плоскости имеют общую точку. Они:', opts:['Парал.','Пересек.','Совпадают или пересек.'], correct:2, explain:'A3 + могут совпадать.' },
{ q:'Плоскости без общих точек:', opts:['Парал.','Пересек.','Скрещ.'], correct:0, explain:'Парал.' },
{ q:'Сколько типов располож. 2 различных плоскостей?', opts:['1','2','3','4'], correct:1, explain:'2: парал./пересек.' },
{ q:'Куб: верх. и нижн. грани:', opts:['Парал.','Пересек.'], correct:0, explain:'Парал.' },
{ q:'Куб: передн. ($ABB_1A_1$) и правая ($BCC_1B_1$) грани:', opts:['Парал.','Пересек. по $BB_1$'], correct:1, explain:'Общее ребро.' }
];
const I3_SIGN_ITEMS = [
{ q:'$\\alpha$ содержит 2 парал. прямые, обе $\\parallel \\beta$. $\\alpha \\parallel \\beta$?', opts:['Да','Не всегда','Никогда'], correct:1, explain:'Нужны пересек.' },
{ q:'$\\alpha$ содержит 2 пересек., обе $\\parallel \\beta$. $\\alpha \\parallel \\beta$?', opts:['Да','Не всегда','Нет'], correct:0, explain:'Признак.' },
{ q:'1 прямая $a \\parallel \\beta$ в $\\alpha$. Достаточно?', opts:['Да','Нет'], correct:1, explain:'Мало.' },
{ q:'Все рёбра одной грани куба парал. другой грани. Грани парал.?', opts:['Да','Нет','Не всегда'], correct:0, explain:'4 ребра включают 2 пересек.' },
{ q:'$\\alpha$ содержит прямую $\\perp$ всем прямым $\\beta$. $\\alpha \\parallel \\beta$?', opts:['Да','Нет'], correct:1, explain:'Это $\\perp$, а не $\\parallel$.' }
];
const I3_PROP_ITEMS = [
{ q:'$\\alpha \\parallel \\beta$, $\\gamma \\cap \\alpha = a$. Тогда $\\gamma \\cap \\beta$ — это $b$ относительно $a$:', opts:['Парал.','Перп.','Скрещ.'], correct:0, explain:'Парал.' },
{ q:'Через $M \\notin \\alpha$ — сколько плоскостей $\\parallel \\alpha$?', opts:['1','2','Бесконечно'], correct:0, explain:'Единственная.' },
{ q:'$\\alpha \\parallel \\gamma, \\beta \\parallel \\gamma$. $\\alpha$ и $\\beta$:', opts:['Парал. или совпадают','Пересек.','Скрещ.'], correct:0, explain:'Транзитивность.' },
{ q:'Парал. плоскости отсекают на парал. прямых:', opts:['Равные отрезки','$\\perp$','Разные'], correct:0, explain:'Равные.' },
{ q:'$\\alpha \\parallel \\beta, a \\parallel \\alpha$. Тогда $a$:', opts:['$\\parallel \\beta$ или $\\subset \\beta$','Пересек. $\\beta$','$\\perp \\beta$'], correct:0, explain:'Парал.' }
];
/* ===== Bosses ===== */
const BOSS_DEFS = {
b1: { title:'Босс §4 — Прямые в пространстве', tag:'§4', xp:65, stages:[
{ q:'Куб: пара $AB$ и $D_1C_1$ — это…', type:'mc', opts:['Парал.','Пересек.','Скрещ.'], correct:0, explain:'$AB \\parallel DC \\parallel D_1C_1$.' },
{ q:'Угол между скрещ. $AB$ и $CC_1$ куба?', type:'input', a:'90', explain:'$90°$.' },
{ q:'Через 2 скрещ. прямые можно провести плоскость?', type:'mc', opts:['Да','Нет','Иногда'], correct:1, explain:'По определению.' },
{ q:'$a \\parallel b, b \\parallel c \\Rightarrow$ для $a$ и $c$:', type:'mc', opts:['$\\parallel$','Скрещ.','Пересек.'], correct:0, explain:'Транзит.' },
{ q:'Угол между парал. прямыми = ? °', type:'input', a:'0', explain:'$0°$.' }
]},
b2: { title:'Босс §5 — Прямая и плоскость', tag:'§5', xp:65, stages:[
{ q:'Сколько случаев для прямой и плоскости?', type:'input', a:'3', explain:'3.' },
{ q:'$a \\parallel b \\subset \\alpha, a \\not\\subset \\alpha \\Rightarrow$', type:'mc', opts:['$a \\subset \\alpha$','$a \\parallel \\alpha$','$a \\cap \\alpha$'], correct:1, explain:'Признак.' },
{ q:'$a \\parallel \\alpha$. Общих точек:', type:'input', a:'0', explain:'0.' },
{ q:'Куб: $AB$ и плоскость верхней грани:', type:'mc', opts:['Лежит','Пересек.','Парал.'], correct:2, explain:'Парал.' },
{ q:'$a \\parallel \\alpha, \\beta \\supset a \\cap \\alpha = c$. $a$ и $c$:', type:'mc', opts:['Скрещ.','Парал.','Пересек.'], correct:1, explain:'$a \\parallel c$.' }
]},
b3: { title:'Босс §6 — Две плоскости', tag:'§6', xp:65, stages:[
{ q:'Случаев для 2 различных плоскостей?', type:'input', a:'2', explain:'2.' },
{ q:'$\\alpha$ содержит 2 пересек. $a, b \\parallel \\beta \\Rightarrow$', type:'mc', opts:['$\\alpha \\parallel \\beta$','$\\alpha=\\beta$','$\\alpha \\cap \\beta=c$'], correct:0, explain:'Признак.' },
{ q:'$\\alpha \\parallel \\beta, \\gamma \\cap \\alpha=a, \\gamma \\cap \\beta=b$. $a$ и $b$:', type:'mc', opts:['Парал.','Скрещ.','$\\perp$'], correct:0, explain:'Парал.' },
{ q:'Через $M \\notin \\alpha$ — сколько $\\parallel \\alpha$?', type:'input', a:'1', explain:'1.' },
{ q:'$\\alpha \\parallel \\gamma, \\beta \\parallel \\gamma \\Rightarrow$', type:'mc', opts:['Парал. или совпадают','Пересек.','Скрещ.'], correct:0, explain:'Транзит.' }
]}
};
const FINAL_BOSS_DEFS = {
f1: { title:'Финал · Прямые', tag:'Финал R2', xp:35, stages:[
{ q:'Куб: $A_1B_1$ и $D_1C_1$ — это…', type:'mc', opts:['Парал.','Пересек.','Скрещ.'], correct:0, explain:'Парал.' },
{ q:'Куб: $AD$ и $B_1C_1$ — это…', type:'mc', opts:['Парал.','Пересек.','Скрещ.'], correct:0, explain:'Парал.' },
{ q:'Угол $AB$ и $A_1D_1$ куба?', type:'input', a:'90', explain:'$90°$.' },
{ q:'Скрещ. пар из 3 рёбер $A$ куба?', type:'mc', opts:['0','1','2','3'], correct:0, explain:'Все пересек. в $A$.' }
]},
f2: { title:'Финал · Прямая+плоск.', tag:'Финал R2', xp:35, stages:[
{ q:'Случаев располож.?', type:'input', a:'3', explain:'3.' },
{ q:'$a \\parallel \\alpha$. Общих точек?', type:'input', a:'0', explain:'0.' },
{ q:'Куб: $AC$ и пл. $A_1B_1C_1D_1$:', type:'mc', opts:['Лежит','Пересек.','Парал.'], correct:2, explain:'$AC \\parallel A_1C_1$.' },
{ q:'$a \\parallel b \\subset \\alpha, a \\not\\subset \\alpha$:', type:'mc', opts:['$a \\cap \\alpha$','$a \\parallel \\alpha$','$a \\subset \\alpha$'], correct:1, explain:'Признак.' }
]},
f3: { title:'Финал · 2 плоскости', tag:'Финал R2', xp:35, stages:[
{ q:'Случаев?', type:'input', a:'2', explain:'2.' },
{ q:'$\\alpha$ содержит 2 пересек. $a, b \\parallel \\beta \\Rightarrow$', type:'mc', opts:['$\\alpha=\\beta$','$\\alpha \\parallel \\beta$','$\\alpha \\cap \\beta=c$'], correct:1, explain:'Признак.' },
{ q:'Куб: верх. и нижн. грани:', type:'mc', opts:['Парал.','Пересек.','Совпадают'], correct:0, explain:'Парал.' },
{ q:'$\\alpha \\parallel \\beta, \\gamma$ сечёт обе по $a, b$. $a, b$:', type:'mc', opts:['Скрещ.','Парал.','$\\perp$'], correct:1, explain:'$a \\parallel b$.' }
]},
f4: { title:'Финал · Сборная', tag:'Финал R2', xp:45, stages:[
{ q:'Через 2 парал. прямые проходит ровно … плоскостей.', type:'input', a:'1', explain:'1.' },
{ q:'$a \\parallel \\alpha, b \\parallel a$. $b$ относительно $\\alpha$:', type:'mc', opts:['$\\parallel \\alpha$ или $\\subset \\alpha$','Пересек.','Скрещ.'], correct:0, explain:'Транзит.' },
{ q:'Куб: сколько рёбер $\\parallel$ грани $ABCD$ (не лежат в ней)?', type:'input', a:'4', explain:'4.' },
{ q:'Скрещ. прямые в одной плоскости?', type:'mc', opts:['Да','Нет','Иногда'], correct:1, explain:'Нет.' },
{ q:'Сколько прямых $\\subset \\alpha$, парал. данной $a \\parallel \\alpha$?', type:'mc', opts:['1','2','Бесконечно'], correct:2, explain:'Через каждую точку.' }
]}
};
function init(){
loadProgress(); initTheme(); buildParaSelector(); goTo('p1'); refreshProgressUI();
if(!STATE.achievements.has('start')) achievement('start');
}
if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
</script>
</body>
</html>