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

1771 lines
118 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>Геометрия 9 · Глава 3 · Теоремы синусов и косинусов</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800;900&family=Unbounded:wght@700;800;900&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#fafafa; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --ink:#0f172a; --muted:#64748b;
--border:#e2e8f0; --sh:0 1px 3px rgba(0,0,0,.06); --sh2:0 4px 14px rgba(0,0,0,.08);
--pri:#7c3aed; --pri2:#6d28d9; --pri-soft:#ede9fe;
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#f5f3ff;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#160b29; --card:#1d1238; --card-soft:#241646; --text:#ede9fe; --ink:#ede9fe; --muted:#a08fbf; --border:#352160}
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
html,body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.55;font-size:15px}
button,input,select,textarea{font-family:inherit;font-size:inherit}
button{cursor:pointer;border:0;background:transparent;color:inherit}
a{color:inherit;text-decoration:none}
.ic{width:16px;height:16px;display:inline-block;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle}
.hdr{position:relative;background:linear-gradient(110deg,#3b0764 0%,#7c3aed 55%,#a78bfa 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(237,233,254,.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(237,233,254,.12);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero::before{content:'∠';position:absolute;right:0;top:-30px;font-size:clamp(2rem,12vw,8rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(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,#fff5e1,#fef3c7)}
.psel-card.final .psel-num{color:var(--warn)}
.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}
.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}
.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}
.sliders{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-bottom:10px}
.sliders label{display:block;font-size:.92rem;color:var(--muted);background:var(--card);padding:8px 12px;border-radius:8px;border:1px solid var(--border);line-height:1.5}
.sliders label b{font-family:'JetBrains Mono',monospace;font-size:1.05rem;color:var(--pri2);margin-left:4px}
.sliders label input[type="range"]{display:block;width:100%;margin-top:6px;accent-color:var(--pri)}
.score-display{display:flex;gap:14px;flex-wrap:wrap;align-items:center;padding:10px 14px;background:var(--pri-soft);border-radius:10px;margin-bottom:12px}
.score-display b{color:var(--pri2);font-size:1.15rem}
.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}
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
.sidecard-row:last-child{margin-bottom:0}
@media(max-width:980px){.col-side{position:static;max-height:none}}
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(0,0,0,.10);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}
.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(--acc));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}
}
.search-modal{position:fixed;inset:0;background:rgba(15,23,42,.55);backdrop-filter:blur(4px);z-index:9993;display:none;align-items:flex-start;justify-content:center;padding-top:14vh}
.search-modal.show{display:flex}
.search-box{background:var(--bg);border:1px solid var(--border);border-radius:14px;width:560px;max-width:92vw;max-height:70vh;display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 64px rgba(0,0,0,.4)}
.search-input{padding:14px 16px;font-size:1rem;border:0;border-bottom:1px solid var(--border);background:transparent;color:var(--text);outline:none}
.search-results{flex:1;overflow-y:auto;padding:6px 0}
.search-row{display:block;padding:8px 16px;cursor:pointer;border-bottom:1px solid var(--border);text-align:left;background:transparent;border:0;width:100%;color:var(--text)}
.search-row:hover,.search-row.active{background:var(--pri-soft)}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
/* === GEOM9 POLISH (visual + micro-interactions) === */
@keyframes wgFadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec.active .wg{animation:wgFadeIn .35s cubic-bezier(.16,1,.3,1) backwards}
.sec.active .wg:nth-of-type(1){animation-delay:.02s}
.sec.active .wg:nth-of-type(2){animation-delay:.08s}
.sec.active .wg:nth-of-type(3){animation-delay:.14s}
.sec.active .wg:nth-of-type(4){animation-delay:.20s}
.sec.active .wg:nth-of-type(5){animation-delay:.26s}
.sec.active .wg:nth-of-type(6){animation-delay:.32s}
.wg svg{transition:filter .25s ease}
.wg:hover svg{filter:drop-shadow(0 4px 16px rgba(0,0,0,.10))}
input[type=range]:active{box-shadow:0 0 0 4px var(--pri-soft);border-radius:8px}
.wg input[type=range]{cursor:ew-resize}
.score-display b{transition:transform .22s cubic-bezier(.16,1,.3,1),color .22s;display:inline-block;transform-origin:center}
.score-display b.bump{transform:scale(1.28);color:var(--pri)}
.katex{transition:color .2s}
.wg-help .katex:hover,.card-body .katex:hover{color:var(--pri2,var(--pri));cursor:help}
.hp-fill,.psel-prog-fill,.xp-fill,[id$="-overall-fill"]{transition:width .6s cubic-bezier(.16,1,.3,1)!important}
.boss-card,.btn.primary,.btn-primary{position:relative;overflow:hidden}
.btn.primary::after,.btn-primary::after{content:'';position:absolute;inset:0;background:radial-gradient(circle at center,rgba(255,255,255,.42) 0%,transparent 60%);opacity:0;transition:opacity .25s;pointer-events:none}
.btn.primary:hover::after,.btn-primary:hover::after{opacity:1}
.psel-card{position:relative}
.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}
.boss-card{transition:border-color .35s,box-shadow .6s,background .3s,transform .2s}
.boss-card.glow{box-shadow:0 0 24px rgba(16,185,129,.6),0 0 0 2px rgba(16,185,129,.45)!important}
@keyframes bossPulse{0%{box-shadow:0 0 0 0 rgba(16,185,129,.55)}70%{box-shadow:0 0 0 14px rgba(16,185,129,0)}100%{box-shadow:0 0 0 0 rgba(16,185,129,0)}}
.boss-card.pulse{animation:bossPulse .8s ease-out}
.psel-card{transition:transform .2s,box-shadow .2s,border-color .2s,background .25s}
.sec{transition:opacity .25s}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Геометрия 9 · Глава 3</h1>
<div class="hdr-sub">Произвольный треугольник · формула Герона</div>
</div>
<div class="hdr-side">
<a href="/textbook/geometry-9" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К геометрии 9</a>
<button id="search-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4-4"/></svg> Поиск</button>
<button id="sidebar-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><line x1="4" y1="6" x2="20" y2="6"/><line x1="4" y1="12" x2="20" y2="12"/><line x1="4" y1="18" x2="14" y2="18"/></svg> Шпаргалка</button>
<button id="theme-btn" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg><span id="theme-lab">Тёмная</span></button>
</div>
</div>
</header>
<main class="main">
<div class="col-main">
<section class="hero">
<h2>Теоремы синусов и косинусов</h2>
<p>Здесь мы выводим <b>теорему синусов</b> $\tfrac{a}{\sin A} = 2R$, <b>теорему косинусов</b> $a^2 = b^2 + c^2 - 2bc\cos A$ и <b>формулу Герона</b> $S = \sqrt{p(p-a)(p-b)(p-c)}$. С их помощью решается любой треугольник.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p10')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 10</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-p10" class="sec" data-watermark="sin"><div class="sec-header"><span class="sec-num">§ 10</span><h2 class="sec-h">Теорема синусов</h2></div><div id="p10-body"></div></section>
<section id="sec-p11" class="sec" data-watermark="cos"><div class="sec-header"><span class="sec-num">§ 11</span><h2 class="sec-h">Теорема косинусов</h2></div><div id="p11-body"></div></section>
<section id="sec-p12" class="sec" data-watermark="√"><div class="sec-header"><span class="sec-num">§ 12</span><h2 class="sec-h">Формула Герона. Решение треугольников</h2></div><div id="p12-body"></div></section>
<section id="sec-final3" class="sec" data-watermark="★"><div class="sec-header"><span class="sec-num" style="background:linear-gradient(135deg,#7c3aed,#a78bfa)"></span><h2 class="sec-h">Итоги главы 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">Интерактивный учебник «Геометрия 9» · Глава 3 · Теоремы синусов и косинусов · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<div id="search-modal" class="search-modal" role="dialog">
<div class="search-box">
<input type="text" id="search-input" class="search-input" placeholder="Поиск…" autocomplete="off">
<div id="search-results" class="search-results"></div>
<div class="search-foot"><span><kbd>↑↓</kbd> навигация</span><span><kbd>Enter</kbd> открыть</span><span><kbd>Esc</kbd> закрыть</span></div>
</div>
</div>
<script>
'use strict';
const STATE = { current:'p10', progress:{p10:0,p11:0,p12:0,final3:0}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 4;
const _TB_SLUG = 'geometry-9-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!',
p10_done:'Теорема синусов освоена!',
p11_done:'Теорема косинусов освоена!',
p12_done:'Формула Герона освоена!',
ch3_done:'Глава 3 пройдена! Теоремы синусов и косинусов — финал!'
};
function loadProgress(){
try{
const s=localStorage.getItem('geometry9_ch3_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('geometry9_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('geometry9_xp')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('geometry9_ch3_progress', JSON.stringify(STATE.progress));
localStorage.setItem('geometry9_ch3_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('geometry9_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 loadServerReadState(){
const tok=(window.LS&&LS.getToken)?LS.getToken():''; if(!tok) return;
fetch('/api/textbooks/'+_TB_SLUG,{headers:{'Authorization':'Bearer '+tok}}).then(r=>r.ok?r.json():null).then(d=>{
if(!d||!d.progress) return;
(d.progress.read||[]).forEach(k=>{_markedRead.add(k); if((STATE.progress[k]||0)<50) STATE.progress[k]=100;});
saveProgress(); refreshProgressUI();
}).catch(()=>{});
}
function addXp(n,src){
if(!n) return;
const prev=STATE.level; STATE.xp=Math.max(0,(STATE.xp||0)+n); STATE.level=calcLevel(STATE.xp);
saveProgress(); refreshProgressUI();
if(window.LS&&window.LS.xp) window.LS.xp.add(n,'geometry9-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:'p10', num:'§ 10', name:'Теорема синусов', sub:'$\\tfrac{a}{\\sin A} = 2R$' },
{ id:'p11', num:'§ 11', name:'Теорема косинусов', sub:'$a^2 = b^2 + c^2 - 2bc\\cos A$' },
{ id:'p12', num:'§ 12', name:'Формула Герона. Решение треугольников', sub:'$S = \\sqrt{p(p-a)(p-b)(p-c)}$' },
{ id:'final3', num:'★', name:'Финал главы', sub:'Итоги главы 3', final:true }
];
function buildParaSelector(){
const g=document.getElementById('psel-grid'); g.innerHTML='';
PARAS.forEach(p=>{
const card=document.createElement('div');
card.className='psel-card'+(p.final?' final':'');
card.dataset.id=p.id; card.dataset.progCard=p.id;
card.innerHTML='<div class="psel-num">'+p.num+'</div><div class="psel-name">'+p.name+'</div><div class="psel-prog"><div class="psel-prog-fill"></div></div>';
card.addEventListener('click', ()=>goTo(p.id));
g.appendChild(card);
});
}
const BUILT=new Set();
const BUILDERS = { p10:()=>buildP10(), p11:()=>buildP11(), p12:()=>buildP12(), 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 = {
p10:{title:'Шпаргалка \xA710',rows:[['Теорема','$\\tfrac{a}{\\sin A} = \\tfrac{b}{\\sin B} = \\tfrac{c}{\\sin C} = 2R$'],['Применение','две стороны и угол напротив']]},
p11:{title:'Шпаргалка \xA711',rows:[['Теорема','$a^2 = b^2 + c^2 - 2bc\\cos A$'],['Применение','три стороны или две стороны и угол']]},
p12:{title:'Шпаргалка \xA712',rows:[['Полупериметр','$p = \\tfrac{a+b+c}{2}$'],['Площадь','$S = \\sqrt{p(p-a)(p-b)(p-c)}$']]},
final3:{title:'Финал главы',rows:[['§§1012','теория главы 3'],['Дальше','глава 4 — правильные многоугольники']]}
};
const TIPS=[
{sec:'p10',html:'Теорема синусов: $\\dfrac{a}{\\sin A} = 2R$, где $R$ — радиус описанной окружности.'},
{sec:'p11',html:'Теорема косинусов обобщает теорему Пифагора: при $A = 90^\\circ$ получаем $a^2 = b^2 + c^2$.'},
{sec:'p12',html:'Формула Герона позволяет найти площадь треугольника, зная только три его стороны.'},
{sec:'final3',html:'Главные результаты главы 3: теоремы синусов и косинусов, формула Герона.'}
];
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS['p10'];
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,#fef3c7),var(--pri-soft));border-color:var(--warn,#f59e0b)"><h4 style="color:#92400e;display:flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px"><polygon points="12,2 22,20 2,20"/></svg>Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem;line-height:1.55">'+tip.html+'</div></div>';
}
if(STATE.achievements.size>0){
html+='<div class="sidecard"><h4>Достижения <span style="color:var(--warn);float:right">'+STATE.achievements.size+'</span></h4>';
[...STATE.achievements.values()].slice(-4).forEach(text=>{ html+='<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; '+text+'</div>'; });
html+='</div>';
}
box.innerHTML=html;
if(window.renderMathInElement) try{ renderMath(box); }catch(e){}
}
function initTheme(){
const t=localStorage.getItem('geometry9_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('geometry9_ch3_theme', dark?'dark':'light');
document.getElementById('theme-lab').textContent=dark?'Светлая':'Тёмная';
});
}
function renderMath(root){ if(window.renderMathInElement){ try{ renderMathInElement(root, {delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false}); }catch(e){} } }
function feedback(elm, ok, text){ if(!elm) return; elm.className='feedback '+(ok?'ok':'fail'); elm.innerHTML=text||(ok?'&#10003; Верно!':'&#10007; Неверно'); elm.style.display='block'; try{renderMath(elm);}catch(e){} }
function fmt(n){ if(!isFinite(n)) return '?'; if(Number.isInteger(n)) return String(n); return Math.abs(n-Math.round(n))<1e-9?String(Math.round(n)):(+n.toFixed(6)).toString(); }
const ICONS = {
repeat:'<svg class="ic" viewBox="0 0 24 24"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>',
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
algo:'<svg class="ic" viewBox="0 0 24 24"><polyline points="17 11 21 7 17 3"/><line x1="21" y1="7" x2="9" y2="7"/><polyline points="7 13 3 17 7 21"/><line x1="3" y1="17" x2="15" y2="17"/></svg>',
rule:'<svg class="ic" viewBox="0 0 24 24"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>',
example:'<svg class="ic" viewBox="0 0 24 24"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M12 2a7 7 0 0 0-4 13c1 1 2 2 2 4h4c0-2 1-3 2-4a7 7 0 0 0-4-13z"/></svg>',
oral:'<svg class="ic" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
};
function makeCard(kind, title, num, body){
const labels = {repeat:'Повторение',theory:'Теория',algo:'Алгоритм',rule:'Правило',example:'Пример',oral:'Устно'};
return '<div class="card"><div class="card-header"><div class="card-icon '+kind+'">'+ICONS[kind]+'</div><div class="card-title">'+(labels[kind]||'')+(title&&title!==labels[kind]?' \xb7 '+title:'')+'</div>'+(num?'<div class="card-num">'+num+'</div>':'')+'</div><div class="card-body">'+body+'</div></div>';
}
function secNav(prev, next){
const NAMES={p10:'\xA710',p11:'\xA711',p12:'\xA712',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 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>'
+' Я прочитал — '+(paraId.startsWith('final')?'финал':'\xA7'+paraId.replace('p',''))+' (+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, 100);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
if(paraId==='final3') achievement('ch3_done');
});
}
/* ===== SVG helpers ===== */
function rightAngleMark(V, uIn, wIn, s){
s = s || 9;
const p1 = {x: V.x + s*uIn.x, y: V.y + s*uIn.y};
const c = {x: p1.x + s*wIn.x, y: p1.y + s*wIn.y};
const p2 = {x: V.x + s*wIn.x, y: V.y + s*wIn.y};
return p1.x+','+p1.y+' '+c.x+','+c.y+' '+p2.x+','+p2.y;
}
function angleArcAuto(V, uA, uB, R){
const sA = {x: V.x + R*uA.x, y: V.y + R*uA.y};
const eB = {x: V.x + R*uB.x, y: V.y + R*uB.y};
const cross = uA.x*uB.y - uA.y*uB.x;
const sweep = cross > 0 ? 1 : 0;
return 'M'+sA.x+','+sA.y+' A'+R+','+R+' 0 0,'+sweep+' '+eB.x+','+eB.y;
}
function unitVec(p1, p2){
const dx = p2.x - p1.x, dy = p2.y - p1.y;
const len = Math.sqrt(dx*dx + dy*dy) || 1;
return {x: dx/len, y: dy/len};
}
function deg2rad(d){ return d * Math.PI / 180; }
/* ===== STUB BUILDERS — наполнение в Phase 7+ ===== */
function _stubBuilder(paraId, num, name, prev, next){
const body = document.getElementById(paraId+'-body');
let html = '';
html += makeCard('theory', 'В разработке', num, `
<p>Содержание параграфа <b>«${name}»</b> будет добавлено в следующих обновлениях.</p>
<p style="color:var(--muted);font-size:.9rem">Раздел Phase 7.</p>`);
html += readButton(paraId);
html += secNav(prev, next);
body.innerHTML = html;
wireReadBtn(paraId);
if(window.renderMathInElement) renderMath(body);
}
/* ===== §10 — Теорема синусов ===== */
function buildP10(){
const box = document.getElementById('p10-body');
let html = '';
html += makeCard('theory', 'Формулировка теоремы', '10.1', `
<p>В любом треугольнике стороны пропорциональны синусам противолежащих углов:</p>
$$\\dfrac{a}{\\sin A} = \\dfrac{b}{\\sin B} = \\dfrac{c}{\\sin C} = 2R$$
<p>где $a, b, c$ — стороны треугольника, $A, B, C$ — противолежащие им углы, а $R$ — радиус описанной окружности.</p>
<details class="spoiler"><summary>Почему именно $2R$?</summary><div class="spoiler-body">
Идея доказательства: в треугольнике, вписанном в окружность радиуса $R$, сторона $a$ — это хорда, стягиваемая центральным углом $2A$ (по теореме о вписанном угле). Длина такой хорды равна $2R \\sin A$, откуда $\\dfrac{a}{\\sin A} = 2R$. То же для $b$ и $c$.
</div></details>`);
html += makeCard('rule', 'Радиус описанной окружности', '10.2', `
<p>Из теоремы синусов сразу получаем формулы для радиуса описанной окружности:</p>
$$R = \\dfrac{a}{2 \\sin A} = \\dfrac{b}{2 \\sin B} = \\dfrac{c}{2 \\sin C} = \\dfrac{abc}{4S}$$
<p>А также — выражение стороны через радиус и противолежащий угол:</p>
$$a = 2R \\sin A, \\qquad b = 2R \\sin B, \\qquad c = 2R \\sin C$$
<p><b>Это очень удобно:</b> зная всего один угол и противолежащую ему сторону, можно сразу найти $R$ — а через него и любую другую сторону.</p>`);
html += makeCard('example', 'Применение', '10.3', `
<p><b>Что можно вычислить?</b></p>
<ul style="padding-left:22px;line-height:1.95">
<li>Найти $b$, зная $a, A, B$: $b = a \\cdot \\dfrac{\\sin B}{\\sin A}$.</li>
<li>Найти $R$, зная $a$ и $A$: $R = \\dfrac{a}{2 \\sin A}$.</li>
<li>Найти угол $B$, зная $a, b, A$: $\\sin B = \\dfrac{b \\sin A}{a}$.</li>
</ul>
<p><b>Пример.</b> $a = 10$, $A = 30°$, $B = 45°$.</p>
$$b = a \\cdot \\dfrac{\\sin B}{\\sin A} = 10 \\cdot \\dfrac{\\sin 45°}{\\sin 30°} = 10 \\cdot \\dfrac{\\sqrt{2}/2}{1/2} = 10\\sqrt{2} \\approx 14{,}14$$
<p>Радиус описанной окружности: $R = \\dfrac{a}{2 \\sin A} = \\dfrac{10}{2 \\cdot 0{,}5} = 10$.</p>`);
/* IV1 — Треугольник с описанной окружностью */
html += `<div class="wg" id="p10-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Треугольник в описанной окружности</div></div>
<div class="wg-help">Меняй углы $A$ и $B$ — программа найдёт $C = 180° - A - B$ и стороны $a = 2R\\sin A$, $b = 2R\\sin B$, $c = 2R\\sin C$. Радиус $R = 130$ фиксирован.</div>
<div class="sliders">
<label>$A$ (°)<b id="p10-iv1-aval">60</b><input type="range" id="p10-iv1-a" min="20" max="140" step="1" value="60"></label>
<label>$B$ (°)<b id="p10-iv1-bval">50</b><input type="range" id="p10-iv1-b" min="20" max="140" step="1" value="50"></label>
</div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p10-iv1-svg" viewBox="0 0 420 400" style="width:100%;min-width:340px;height:auto;display:block"></svg>
</div>
<div id="p10-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--pri-soft);border-radius:9px;font-size:.95rem;text-align:center;line-height:1.9"></div>
</div>`;
/* IV2 — Калькулятор */
html += `<div class="wg" id="p10-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор $R$ и сторон</div></div>
<div class="wg-help">Введи сторону $a$ и два угла $A$, $B$ — программа найдёт $C$, $R$, $b$ и $c$ по теореме синусов.</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:10px">
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $a$<input type="number" id="p10-iv2-a" class="tinp" style="width:100%;margin-top:4px" value="10" step="0.1" min="0.1"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">угол $A$ (°)<input type="number" id="p10-iv2-A" class="tinp" style="width:100%;margin-top:4px" value="30" step="1" min="1" max="178"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">угол $B$ (°)<input type="number" id="p10-iv2-B" class="tinp" style="width:100%;margin-top:4px" value="45" step="1" min="1" max="178"></label>
</div>
<div style="text-align:center;margin-bottom:10px"><button class="btn primary" id="p10-iv2-go">Вычислить</button></div>
<div id="p10-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;font-size:.95rem;min-height:50px;line-height:1.9"></div>
<div class="feedback" id="p10-iv2-fb"></div>
</div>`;
/* IV3 — Что вычислить? */
html += `<div class="wg" id="p10-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Какую формулу применить?</div></div>
<div class="wg-help">Дано условие — выбери формулу теоремы синусов, которая поможет найти искомое.</div>
<div class="score-display"><span>Задача <b id="p10-iv3-i">1</b> / 6</span><span>Очки: <b id="p10-iv3-s">0</b> / 6</span></div>
<div id="p10-iv3-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:8px">
<button class="btn primary" data-ans="R" id="p10-iv3-R">$\\dfrac{a}{\\sin A} = 2R$</button>
<button class="btn primary" data-ans="side" id="p10-iv3-side">$a = 2R\\sin A$</button>
<button class="btn primary" data-ans="prop" id="p10-iv3-prop">$\\dfrac{a}{\\sin A} = \\dfrac{b}{\\sin B}$</button>
</div>
<div class="feedback" id="p10-iv3-fb"></div>
</div>`;
/* IV4 — Тренажёр */
html += `<div class="wg" id="p10-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр теоремы синусов</div></div>
<div class="wg-help">Реши задачу и введи число (округляй до 2 знаков после запятой).</div>
<div class="score-display"><span>Задача <b id="p10-iv4-i">1</b> / 6</span><span>Очки: <b id="p10-iv4-s">0</b> / 6</span></div>
<div id="p10-iv4-q" style="padding:14px;background:var(--pri-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">
<span style="font-family:'JetBrains Mono',monospace">ответ =</span>
<input type="number" id="p10-iv4-ans" class="tinp" style="width:120px;text-align:center" step="0.01">
<button class="btn primary" id="p10-iv4-go">Проверить</button>
<button class="btn" id="p10-iv4-start">Заново</button>
</div>
<div class="feedback" id="p10-iv4-fb"></div>
</div>`;
html += readButton('p10');
html += secNav(null, 'p11');
box.innerHTML = html;
renderMath(box);
/* IV1 — Треугольник в описанной окружности */
(function(){
const sA=document.getElementById('p10-iv1-a');
const sB=document.getElementById('p10-iv1-b');
const lA=document.getElementById('p10-iv1-aval');
const lB=document.getElementById('p10-iv1-bval');
const svg=document.getElementById('p10-iv1-svg');
const out=document.getElementById('p10-iv1-out');
const seen=new Set();
const cx=210, cy=200, R=130;
function draw(){
let A=+sA.value, B=+sB.value;
// ограничение чтобы сумма не превышала 170
if(A+B>170){ if(A>B) A=170-B; else B=170-A; }
sA.value=A; sB.value=B;
const C=180-A-B;
lA.textContent=A; lB.textContent=B;
// строим треугольник, вписанный в окружность с углами A, B, C
// выбираем для удобства: вершина CA на угле 210° (центральном) от центра
// Используем: положение вершин на окружности — углы относительно центра 2A, 2B, 2C
// Простой способ: разместим точку A_v внизу слева, B_v внизу справа, C_v вверху
// Углы при центре: arc BC = 2A, arc CA = 2B, arc AB = 2C
// Начнём с A_v на угле theta0=210°, пойдём по окружности
const tA=deg2rad(210);
const tB=tA + deg2rad(2*C); // от A через дугу AB (=2C) до B
const tC=tB + deg2rad(2*A); // от B через дугу BC (=2A) до C
const Av={x:cx+R*Math.cos(tA), y:cy+R*Math.sin(tA)};
const Bv={x:cx+R*Math.cos(tB), y:cy+R*Math.sin(tB)};
const Cv={x:cx+R*Math.cos(tC), y:cy+R*Math.sin(tC)};
const a=Math.hypot(Bv.x-Cv.x,Bv.y-Cv.y);
const b=Math.hypot(Av.x-Cv.x,Av.y-Cv.y);
const c=Math.hypot(Av.x-Bv.x,Av.y-Bv.y);
let s='';
s += '<rect x="0" y="0" width="420" height="400" fill="none"/>';
s += '<circle cx="'+cx+'" cy="'+cy+'" r="'+R+'" fill="rgba(124,58,237,.06)" stroke="#7c3aed" stroke-width="2" stroke-dasharray="6 4"/>';
s += '<circle cx="'+cx+'" cy="'+cy+'" r="3" fill="#7c3aed"/>';
s += '<text x="'+(cx+8)+'" y="'+(cy-6)+'" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#5b21b6">O</text>';
// радиус к одной вершине (показать R)
s += '<line x1="'+cx+'" y1="'+cy+'" x2="'+Av.x.toFixed(2)+'" y2="'+Av.y.toFixed(2)+'" stroke="#7c3aed" stroke-width="1.4" stroke-dasharray="3 3"/>';
const mx=(cx+Av.x)/2, my=(cy+Av.y)/2;
s += '<text x="'+mx.toFixed(2)+'" y="'+(my-4).toFixed(2)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="#5b21b6">R</text>';
// треугольник
s += '<polygon points="'+Av.x.toFixed(2)+','+Av.y.toFixed(2)+' '+Bv.x.toFixed(2)+','+Bv.y.toFixed(2)+' '+Cv.x.toFixed(2)+','+Cv.y.toFixed(2)+'" fill="rgba(16,185,129,.10)" stroke="#059669" stroke-width="2.2" stroke-linejoin="round"/>';
// вершины и подписи
function lab(P, name, color){
const ux=(P.x-cx)/R, uy=(P.y-cy)/R;
const lx=P.x+ux*18, ly=P.y+uy*18;
return '<circle cx="'+P.x.toFixed(2)+'" cy="'+P.y.toFixed(2)+'" r="4" fill="#0f172a"/>'
+'<text x="'+lx.toFixed(2)+'" y="'+(ly+5).toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="15" font-weight="700" fill="'+color+'">'+name+'</text>';
}
s += lab(Av,'A','#0f172a');
s += lab(Bv,'B','#0f172a');
s += lab(Cv,'C','#0f172a');
svg.innerHTML=s;
const sinA=Math.sin(deg2rad(A))||1e-9;
// R = 130 px ↔ R = 5 ед., коэффициент K = 26 px / ед.
const K = 26;
const Ru = R / K; // = 5
const au = a / K, bu = b / K, cu = c / K;
out.innerHTML = '$A = '+A+'°$, $B = '+B+'°$, $C = '+C+'°$ &nbsp;·&nbsp; $R = '+Ru.toFixed(2)+'$<br>'
+ '$\\dfrac{a}{\\sin A} = \\dfrac{b}{\\sin B} = \\dfrac{c}{\\sin C} = 2R = '+(2*Ru).toFixed(2)+'$<br>'
+ '$a \\approx '+au.toFixed(2)+'$, $b \\approx '+bu.toFixed(2)+'$, $c \\approx '+cu.toFixed(2)+'$ &nbsp;·&nbsp; $a/\\sin A \\approx '+(au/sinA).toFixed(2)+'$';
renderMath(out);
seen.add(A+'|'+B);
if(seen.size>=4 && !seen.has('done')){ addXp(10,'p10-iv1'); bumpProgress('p10',15); seen.add('done'); }
}
[sA,sB].forEach(s=>s.addEventListener('input', draw));
draw();
})();
/* IV2 — Калькулятор */
(function(){
const aI=document.getElementById('p10-iv2-a');
const AI=document.getElementById('p10-iv2-A');
const BI=document.getElementById('p10-iv2-B');
const go=document.getElementById('p10-iv2-go');
const out=document.getElementById('p10-iv2-out');
const fb=document.getElementById('p10-iv2-fb');
let solved=0;
function calc(){
const a=parseFloat(aI.value), A=parseFloat(AI.value), B=parseFloat(BI.value);
if(!isFinite(a)||!isFinite(A)||!isFinite(B)){ feedback(fb,false,'&#10007; Введи все значения.'); return; }
if(a<=0){ feedback(fb,false,'&#10007; Сторона должна быть положительной.'); return; }
if(A<=0||B<=0||A+B>=180){ feedback(fb,false,'&#10007; Углы должны быть положительными, сумма $A + B < 180°$.'); return; }
const C=180-A-B;
const sinA=Math.sin(deg2rad(A)), sinB=Math.sin(deg2rad(B)), sinC=Math.sin(deg2rad(C));
const R=a/(2*sinA);
const b=2*R*sinB;
const c=2*R*sinC;
out.innerHTML = '$C = 180° - A - B = '+C.toFixed(1)+'°$<br>'
+ '$R = \\dfrac{a}{2 \\sin A} = \\dfrac{'+a+'}{2 \\sin '+A+'°} \\approx '+R.toFixed(3)+'$<br>'
+ '$b = 2R \\sin B = 2 \\cdot '+R.toFixed(3)+' \\cdot \\sin '+B+'° \\approx '+b.toFixed(3)+'$<br>'
+ '$c = 2R \\sin C = 2 \\cdot '+R.toFixed(3)+' \\cdot \\sin '+C.toFixed(1)+'° \\approx '+c.toFixed(3)+'$';
renderMath(out);
feedback(fb,true,'&#10003; Готово!');
solved++;
if(solved===1){ addXp(10,'p10-iv2'); bumpProgress('p10',10); }
}
go.addEventListener('click', calc);
})();
/* IV3 — «Какую формулу применить?» */
(function(){
const Q=[
{t:'Известны $a$ и $A$. Найти радиус $R$ описанной окружности.', a:'R'},
{t:'Известны $R$ и угол $A$. Найти сторону $a$.', a:'side'},
{t:'Известны $a$, $A$ и угол $B$. Найти сторону $b$.', a:'prop'},
{t:'Известны $R$ и угол $B$. Найти сторону $b$.', a:'side'},
{t:'Известны $b$, $B$ и угол $C$. Найти сторону $c$.', a:'prop'},
{t:'Известны сторона $c$ и угол $C$. Найти радиус $R$.', a:'R'}
];
const explain={
R:'Прямо $R = \\dfrac{a}{2\\sin A}$ — сторону пропускаем через формулу $2R$.',
side:'Сторона через радиус: $a = 2R \\sin A$.',
prop:'Связываем две стороны через их углы: $\\dfrac{a}{\\sin A} = \\dfrac{b}{\\sin B}$.'
};
const labels={R:'$\\dfrac{a}{\\sin A} = 2R$', side:'$a = 2R\\sin A$', prop:'$\\dfrac{a}{\\sin A} = \\dfrac{b}{\\sin B}$'};
const qBox=document.getElementById('p10-iv3-q');
const iEl=document.getElementById('p10-iv3-i');
const sEl=document.getElementById('p10-iv3-s');
const fb=document.getElementById('p10-iv3-fb');
const btns=document.querySelectorAll('#p10-iv3 button[data-ans]');
let i=0, score=0, done=false;
function show(){
if(i>=Q.length){
qBox.innerHTML='Завершено! Очки: <b>'+score+'</b> / '+Q.length;
if(!done){ done=true; addXp(15,'p10-iv3'); bumpProgress('p10',25); if(score===Q.length) achievement('p10_done'); }
btns.forEach(b=>b.disabled=true);
return;
}
qBox.innerHTML=Q[i].t; renderMath(qBox);
iEl.textContent=(i+1); sEl.textContent=score;
fb.style.display='none';
}
btns.forEach(b=>b.addEventListener('click', ()=>{
const ok = b.dataset.ans===Q[i].a;
if(ok) score++;
feedback(fb, ok, ok?('&#10003; Верно! '+explain[Q[i].a]):('&#10007; Правильная формула: '+labels[Q[i].a]+'. '+explain[Q[i].a]));
i++;
setTimeout(show, 1100);
}));
show();
})();
/* IV4 — Тренажёр */
(function(){
const Q=[
{t:'$a = 10$, $A = 30°$. Найди радиус $R$ описанной окружности.', a:10, tol:0.02},
{t:'$R = 5$, $A = 90°$. Найди сторону $a$.', a:10, tol:0.02},
{t:'$a = 8$, $A = 45°$, $B = 60°$. Найди сторону $b$.', a:9.80, tol:0.05},
{t:'В прямоугольном треугольнике гипотенуза $c = 10$, угол $C = 90°$. Найди $R$.', a:5, tol:0.02},
{t:'$a = 6$, $A = 30°$, $B = 60°$. Найди сторону $b$.', a:10.39, tol:0.05},
{t:'$b = 12$, $B = 60°$. Найди радиус $R$.', a:6.93, tol:0.05}
];
const qBox=document.getElementById('p10-iv4-q');
const ans=document.getElementById('p10-iv4-ans');
const go=document.getElementById('p10-iv4-go');
const reset=document.getElementById('p10-iv4-start');
const iEl=document.getElementById('p10-iv4-i');
const sEl=document.getElementById('p10-iv4-s');
const fb=document.getElementById('p10-iv4-fb');
let i=0, score=0, done=false;
function show(){
if(i>=Q.length){
qBox.innerHTML='Финиш! Очки: <b>'+score+'</b> / '+Q.length;
if(!done){ done=true; addXp(15,'p10-iv4'); bumpProgress('p10',25); }
go.disabled=true; ans.disabled=true;
return;
}
qBox.innerHTML=Q[i].t; renderMath(qBox);
iEl.textContent=(i+1); sEl.textContent=score;
ans.value=''; fb.style.display='none'; go.disabled=false; ans.disabled=false;
}
go.addEventListener('click', ()=>{
const v=parseFloat((ans.value||'').replace(',', '.'));
if(!isFinite(v)){ feedback(fb,false,'&#10007; Введи число.'); return; }
const tol=Q[i].tol||0.05;
const ok=Math.abs(v-Q[i].a)<=tol;
if(ok) score++;
feedback(fb, ok, ok?'&#10003; Верно!':'&#10007; Правильный ответ: $'+Q[i].a+'$');
i++;
setTimeout(show, 1000);
});
reset.addEventListener('click', ()=>{ i=0; score=0; done=false; show(); });
show();
})();
wireReadBtn('p10');
}
/* ===== §11 — Теорема косинусов ===== */
function buildP11(){
const box = document.getElementById('p11-body');
let html = '';
html += makeCard('theory', 'Формулировка теоремы', '11.1', `
<p>В любом треугольнике квадрат стороны равен сумме квадратов двух других сторон минус удвоенное произведение этих сторон на косинус угла между ними:</p>
$$a^2 = b^2 + c^2 - 2bc \\cos A$$
<p>Аналогично — для двух других сторон:</p>
$$b^2 = a^2 + c^2 - 2ac \\cos B, \\qquad c^2 = a^2 + b^2 - 2ab \\cos C$$
<p>Это <b>обобщение теоремы Пифагора</b>: при $A = 90°$ имеем $\\cos A = 0$, и формула превращается в $a^2 = b^2 + c^2$.</p>
<details class="spoiler"><summary>Когда нужна теорема косинусов?</summary><div class="spoiler-body">
Если известны две стороны и угол <b>между ними</b> — третья сторона находится теоремой косинусов. Если известны все три стороны — любой угол находится из неё же. В этих двух случаях теорема синусов <i>не помогает</i> (нет пары «сторона + противолежащий угол»).
</div></details>`);
html += makeCard('rule', 'Применение для нахождения сторон и углов', '11.2', `
<p><b>Случай 1:</b> известны две стороны $b, c$ и угол $A$ между ними. Тогда</p>
$$a = \\sqrt{b^2 + c^2 - 2bc \\cos A}$$
<p><b>Случай 2:</b> известны все три стороны $a, b, c$. Тогда любой угол находится по формуле:</p>
$$\\cos A = \\dfrac{b^2 + c^2 - a^2}{2bc}, \\qquad \\cos B = \\dfrac{a^2 + c^2 - b^2}{2ac}, \\qquad \\cos C = \\dfrac{a^2 + b^2 - c^2}{2ab}$$
<p>Затем $A = \\arccos(\\cos A)$.</p>
<p><b>Удобно знать:</b> если $\\cos A > 0$ — угол $A$ острый, если $\\cos A < 0$ — тупой, если $\\cos A = 0$ — прямой.</p>`);
html += makeCard('example', 'Примеры', '11.3', `
<p><b>Пример 1.</b> Две стороны $b = 5$, $c = 7$, угол между ними $A = 60°$. Найти третью сторону.</p>
$$a^2 = 25 + 49 - 2 \\cdot 5 \\cdot 7 \\cdot \\cos 60° = 74 - 70 \\cdot 0{,}5 = 39$$
$$a = \\sqrt{39} \\approx 6{,}24$$
<p><b>Пример 2.</b> Стороны $3, 4, 5$. Найти угол между сторонами $3$ и $4$ (т.е. противолежащий стороне $5$).</p>
$$\\cos C = \\dfrac{9 + 16 - 25}{2 \\cdot 3 \\cdot 4} = 0 \\Rightarrow C = 90°$$
<p>Это знакомый прямоугольный треугольник 3–4–5.</p>
<p><b>Пример 3.</b> Стороны $7, 8, 13$. Найти наибольший угол (он лежит против $13$).</p>
$$\\cos A = \\dfrac{49 + 64 - 169}{2 \\cdot 7 \\cdot 8} = \\dfrac{-56}{112} = -0{,}5 \\Rightarrow A = 120°$$`);
/* IV1 — Slider угла → одна сторона */
html += `<div class="wg" id="p11-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Угол меняется — сторона тоже</div></div>
<div class="wg-help">Стороны $b = 100$ и $c = 150$ зафиксированы. Меняй угол $A$ между ними — третья сторона $a$ вычисляется по теореме косинусов.</div>
<div class="sliders">
<label>$A$ (°)<b id="p11-iv1-aval">60</b><input type="range" id="p11-iv1-a" min="30" max="150" step="1" value="60"></label>
</div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p11-iv1-svg" viewBox="0 0 400 320" style="width:100%;min-width:320px;height:auto;display:block"></svg>
</div>
<div id="p11-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--pri-soft);border-radius:9px;font-size:.95rem;text-align:center;line-height:1.9"></div>
</div>`;
/* IV2 — Калькулятор */
html += `<div class="wg" id="p11-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор стороны и угла</div></div>
<div class="wg-help">Две формы: одна находит сторону по двум сторонам и углу, другая — угол по трём сторонам.</div>
<div style="font-weight:700;color:var(--pri2);margin-bottom:6px;font-size:.92rem">Найти сторону $a$</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:8px">
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $b$<input type="number" id="p11-iv2-b" class="tinp" style="width:100%;margin-top:4px" value="5" step="0.1" min="0.1"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $c$<input type="number" id="p11-iv2-c" class="tinp" style="width:100%;margin-top:4px" value="8" step="0.1" min="0.1"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">угол $A$ (°)<input type="number" id="p11-iv2-A" class="tinp" style="width:100%;margin-top:4px" value="60" step="1" min="1" max="179"></label>
</div>
<div style="text-align:center;margin-bottom:8px"><button class="btn primary" id="p11-iv2-goS">Найти $a$</button></div>
<div id="p11-iv2-outS" style="padding:12px 14px;background:var(--card);border-radius:9px;font-size:.95rem;min-height:40px;line-height:1.9;margin-bottom:14px"></div>
<div style="font-weight:700;color:var(--pri2);margin-bottom:6px;font-size:.92rem">Найти угол $A$</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:8px">
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $a$<input type="number" id="p11-iv2-a2" class="tinp" style="width:100%;margin-top:4px" value="7" step="0.1" min="0.1"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $b$<input type="number" id="p11-iv2-b2" class="tinp" style="width:100%;margin-top:4px" value="5" step="0.1" min="0.1"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $c$<input type="number" id="p11-iv2-c2" class="tinp" style="width:100%;margin-top:4px" value="8" step="0.1" min="0.1"></label>
</div>
<div style="text-align:center;margin-bottom:8px"><button class="btn primary" id="p11-iv2-goA">Найти $A$</button></div>
<div id="p11-iv2-outA" style="padding:12px 14px;background:var(--card);border-radius:9px;font-size:.95rem;min-height:40px;line-height:1.9"></div>
<div class="feedback" id="p11-iv2-fb"></div>
</div>`;
/* IV3 — Что найти? */
html += `<div class="wg" id="p11-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Что искать — сторону или угол?</div></div>
<div class="wg-help">Дано условие — выбери, в каком виде применить теорему косинусов: для нахождения стороны или для нахождения угла.</div>
<div class="score-display"><span>Задача <b id="p11-iv3-i">1</b> / 6</span><span>Очки: <b id="p11-iv3-s">0</b> / 6</span></div>
<div id="p11-iv3-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:8px">
<button class="btn primary" data-ans="side" id="p11-iv3-side">Для стороны<br><span style="font-size:.78rem;opacity:.85">$a^2 = b^2+c^2-2bc\\cos A$</span></button>
<button class="btn primary" data-ans="angle" id="p11-iv3-angle">Для угла<br><span style="font-size:.78rem;opacity:.85">$\\cos A = \\dfrac{b^2+c^2-a^2}{2bc}$</span></button>
</div>
<div class="feedback" id="p11-iv3-fb"></div>
</div>`;
/* IV4 — Тренажёр */
html += `<div class="wg" id="p11-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр теоремы косинусов</div></div>
<div class="wg-help">Реши задачу и введи число (округляй до 2 знаков после запятой).</div>
<div class="score-display"><span>Задача <b id="p11-iv4-i">1</b> / 6</span><span>Очки: <b id="p11-iv4-s">0</b> / 6</span></div>
<div id="p11-iv4-q" style="padding:14px;background:var(--pri-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">
<span style="font-family:'JetBrains Mono',monospace">ответ =</span>
<input type="number" id="p11-iv4-ans" class="tinp" style="width:120px;text-align:center" step="0.01">
<button class="btn primary" id="p11-iv4-go">Проверить</button>
<button class="btn" id="p11-iv4-start">Заново</button>
</div>
<div class="feedback" id="p11-iv4-fb"></div>
</div>`;
html += readButton('p11');
html += secNav('p10', 'p12');
box.innerHTML = html;
renderMath(box);
/* IV1 — Slider угла A */
(function(){
const sA=document.getElementById('p11-iv1-a');
const lA=document.getElementById('p11-iv1-aval');
const svg=document.getElementById('p11-iv1-svg');
const out=document.getElementById('p11-iv1-out');
const seen=new Set();
// Стороны зафиксированы: b = 4 ед., c = 6 ед. Масштаб K = 25 px/ед.
const bU = 4, cU = 6, K = 25;
const bPx = bU * K, cPx = cU * K;
function draw(){
const A=+sA.value; lA.textContent=A;
// A_v — вершина с углом A (низ-лево), B_v — на расстоянии c (низ-право), C_v — под углом A от A_v на расстояние b.
const Av={x:90, y:240};
const Bv={x:Av.x+cPx, y:Av.y};
const Cv={x:Av.x+bPx*Math.cos(deg2rad(A)), y:Av.y-bPx*Math.sin(deg2rad(A))};
const aPx=Math.hypot(Bv.x-Cv.x, Bv.y-Cv.y);
const aU=aPx/K;
let s='';
s += '<rect x="0" y="0" width="400" height="320" fill="none"/>';
// треугольник
s += '<polygon points="'+Av.x.toFixed(2)+','+Av.y.toFixed(2)+' '+Bv.x.toFixed(2)+','+Bv.y.toFixed(2)+' '+Cv.x.toFixed(2)+','+Cv.y.toFixed(2)+'" fill="rgba(124,58,237,.10)" stroke="#7c3aed" stroke-width="2.2" stroke-linejoin="round"/>';
// дуга угла A
const uAB=unitVec(Av,Bv), uAC=unitVec(Av,Cv);
s += '<path d="'+angleArcAuto(Av, uAB, uAC, 24)+'" fill="none" stroke="#dc2626" stroke-width="2"/>';
// прямой угол, если A=90
if(Math.abs(A-90)<0.5){
s += '<polyline points="'+rightAngleMark(Av, uAB, uAC, 14)+'" fill="none" stroke="#dc2626" stroke-width="2"/>';
}
// подписи сторон в единицах
const midAB={x:(Av.x+Bv.x)/2, y:(Av.y+Bv.y)/2};
const midAC={x:(Av.x+Cv.x)/2, y:(Av.y+Cv.y)/2};
const midBC={x:(Bv.x+Cv.x)/2, y:(Bv.y+Cv.y)/2};
s += '<text x="'+midAB.x.toFixed(2)+'" y="'+(midAB.y+18).toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#5b21b6">c = '+cU+'</text>';
s += '<text x="'+(midAC.x-18).toFixed(2)+'" y="'+midAC.y.toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#5b21b6">b = '+bU+'</text>';
s += '<text x="'+(midBC.x+14).toFixed(2)+'" y="'+midBC.y.toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#dc2626">a ≈ '+aU.toFixed(2)+'</text>';
// подпись угла A
s += '<text x="'+(Av.x+34).toFixed(2)+'" y="'+(Av.y-10).toFixed(2)+'" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#dc2626">A = '+A+'°</text>';
// вершины
[['A',Av],['B',Bv],['C',Cv]].forEach(([n,P])=>{
s += '<circle cx="'+P.x.toFixed(2)+'" cy="'+P.y.toFixed(2)+'" r="4" fill="#0f172a"/>';
const dx=(n==='A'?-12:(n==='B'?12:0)), dy=(n==='C'?-10:18);
s += '<text x="'+(P.x+dx).toFixed(2)+'" y="'+(P.y+dy).toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="14" font-weight="700" fill="#0f172a">'+n+'</text>';
});
svg.innerHTML=s;
const cosA=Math.cos(deg2rad(A));
const a2u = bU*bU + cU*cU - 2*bU*cU*cosA;
let info = '$a^2 = b^2 + c^2 - 2bc \\cos A = '+(bU*bU)+' + '+(cU*cU)+' - '+(2*bU*cU)+' \\cdot \\cos '+A+'° \\approx '+a2u.toFixed(2)+'$<br>'
+ '$a \\approx '+Math.sqrt(Math.max(0,a2u)).toFixed(2)+'$';
if(Math.abs(A-90)<0.5) info += '<br><b style="color:#dc2626">&#10003; Прямой угол — это теорема Пифагора!</b>';
out.innerHTML=info;
renderMath(out);
seen.add(A);
if(seen.size>=4 && !seen.has('done')){ addXp(10,'p11-iv1'); bumpProgress('p11',15); seen.add('done'); }
}
sA.addEventListener('input', draw);
draw();
})();
/* IV2 — Калькулятор */
(function(){
const bI=document.getElementById('p11-iv2-b');
const cI=document.getElementById('p11-iv2-c');
const AI=document.getElementById('p11-iv2-A');
const goS=document.getElementById('p11-iv2-goS');
const outS=document.getElementById('p11-iv2-outS');
const aI=document.getElementById('p11-iv2-a2');
const b2I=document.getElementById('p11-iv2-b2');
const c2I=document.getElementById('p11-iv2-c2');
const goA=document.getElementById('p11-iv2-goA');
const outA=document.getElementById('p11-iv2-outA');
const fb=document.getElementById('p11-iv2-fb');
let solvedS=0, solvedA=0;
goS.addEventListener('click', ()=>{
const b=parseFloat(bI.value), c=parseFloat(cI.value), A=parseFloat(AI.value);
if(!isFinite(b)||!isFinite(c)||!isFinite(A)){ feedback(fb,false,'&#10007; Введи все значения.'); return; }
if(b<=0||c<=0){ feedback(fb,false,'&#10007; Стороны должны быть положительными.'); return; }
if(A<=0||A>=180){ feedback(fb,false,'&#10007; Угол $A$ от $0°$ до $180°$.'); return; }
const a2 = b*b + c*c - 2*b*c*Math.cos(deg2rad(A));
const a = Math.sqrt(a2);
outS.innerHTML = '$a^2 = b^2 + c^2 - 2bc \\cos A = '+(b*b).toFixed(2)+' + '+(c*c).toFixed(2)+' - '+(2*b*c).toFixed(2)+' \\cdot \\cos '+A+'° \\approx '+a2.toFixed(3)+'$<br>'
+ '$a \\approx '+a.toFixed(3)+'$';
renderMath(outS);
feedback(fb,true,'&#10003; Сторона найдена.');
solvedS++; if(solvedS+solvedA===1){ addXp(10,'p11-iv2'); bumpProgress('p11',10); }
});
goA.addEventListener('click', ()=>{
const a=parseFloat(aI.value), b=parseFloat(b2I.value), c=parseFloat(c2I.value);
if(!isFinite(a)||!isFinite(b)||!isFinite(c)){ feedback(fb,false,'&#10007; Введи все значения.'); return; }
if(a<=0||b<=0||c<=0){ feedback(fb,false,'&#10007; Стороны должны быть положительными.'); return; }
// проверка неравенства треугольника
if(a+b<=c || a+c<=b || b+c<=a){ feedback(fb,false,'&#10007; Неравенство треугольника не выполняется.'); return; }
let cosA = (b*b + c*c - a*a)/(2*b*c);
if(cosA>1) cosA=1; if(cosA<-1) cosA=-1;
const A = Math.acos(cosA) * 180 / Math.PI;
outA.innerHTML = '$\\cos A = \\dfrac{b^2 + c^2 - a^2}{2bc} = \\dfrac{'+(b*b).toFixed(2)+' + '+(c*c).toFixed(2)+' - '+(a*a).toFixed(2)+'}{'+(2*b*c).toFixed(2)+'} \\approx '+cosA.toFixed(4)+'$<br>'
+ '$A = \\arccos('+cosA.toFixed(4)+') \\approx '+A.toFixed(2)+'°$';
renderMath(outA);
feedback(fb,true,'&#10003; Угол найден.');
solvedA++; if(solvedS+solvedA===1){ addXp(10,'p11-iv2'); bumpProgress('p11',10); }
});
})();
/* IV3 — «Что искать?» */
(function(){
const Q=[
{t:'Известны 2 стороны и угол между ними. Найти третью сторону.', a:'side'},
{t:'Известны 3 стороны. Найти один из углов.', a:'angle'},
{t:'Стороны $b = 5$, $c = 7$, угол $A = 60°$. Найти сторону $a$.', a:'side'},
{t:'Стороны $a = 13$, $b = 14$, $c = 15$. Найти угол $B$.', a:'angle'},
{t:'Стороны $b = 8$, $c = 10$, угол $A = 120°$. Найти сторону $a$.', a:'side'},
{t:'Стороны треугольника $5, 12, 13$. Найти наибольший угол.', a:'angle'}
];
const explain={
side:'$a^2 = b^2 + c^2 - 2bc \\cos A$ — даёт квадрат искомой стороны.',
angle:'$\\cos A = \\dfrac{b^2 + c^2 - a^2}{2bc}$ — даёт косинус искомого угла.'
};
const labels={side:'для стороны', angle:'для угла'};
const qBox=document.getElementById('p11-iv3-q');
const iEl=document.getElementById('p11-iv3-i');
const sEl=document.getElementById('p11-iv3-s');
const fb=document.getElementById('p11-iv3-fb');
const btns=document.querySelectorAll('#p11-iv3 button[data-ans]');
let i=0, score=0, done=false;
function show(){
if(i>=Q.length){
qBox.innerHTML='Завершено! Очки: <b>'+score+'</b> / '+Q.length;
if(!done){ done=true; addXp(15,'p11-iv3'); bumpProgress('p11',25); if(score===Q.length) achievement('p11_done'); }
btns.forEach(b=>b.disabled=true);
return;
}
qBox.innerHTML=Q[i].t; renderMath(qBox);
iEl.textContent=(i+1); sEl.textContent=score;
fb.style.display='none';
}
btns.forEach(b=>b.addEventListener('click', ()=>{
const ok = b.dataset.ans===Q[i].a;
if(ok) score++;
feedback(fb, ok, ok?('&#10003; Верно! '+explain[Q[i].a]):('&#10007; Правильно: '+labels[Q[i].a]+'. '+explain[Q[i].a]));
i++;
setTimeout(show, 1100);
}));
show();
})();
/* IV4 — Тренажёр */
(function(){
const Q=[
{t:'Стороны $b = 5$, $c = 8$, угол $A = 60°$. Найди $a$.', a:7, tol:0.02},
{t:'Стороны $b = 3$, $c = 4$, угол $A = 90°$. Найди $a$.', a:5, tol:0.02},
{t:'Стороны треугольника $7, 8, 13$. Найди наибольший угол (в градусах).', a:120, tol:0.5},
{t:'Стороны $b = 6$, $c = 10$, угол $A = 120°$. Найди $a$.', a:14, tol:0.02},
{t:'Стороны $3, 5, 7$. Найди угол против стороны $7$ (в градусах).', a:120, tol:0.5},
{t:'Стороны $b = 4$, $c = 6$, угол $A = 45°$. Найди $a^2$.', a:18.06, tol:0.1}
];
const qBox=document.getElementById('p11-iv4-q');
const ans=document.getElementById('p11-iv4-ans');
const go=document.getElementById('p11-iv4-go');
const reset=document.getElementById('p11-iv4-start');
const iEl=document.getElementById('p11-iv4-i');
const sEl=document.getElementById('p11-iv4-s');
const fb=document.getElementById('p11-iv4-fb');
let i=0, score=0, done=false;
function show(){
if(i>=Q.length){
qBox.innerHTML='Финиш! Очки: <b>'+score+'</b> / '+Q.length;
if(!done){ done=true; addXp(15,'p11-iv4'); bumpProgress('p11',25); }
go.disabled=true; ans.disabled=true;
return;
}
qBox.innerHTML=Q[i].t; renderMath(qBox);
iEl.textContent=(i+1); sEl.textContent=score;
ans.value=''; fb.style.display='none'; go.disabled=false; ans.disabled=false;
}
go.addEventListener('click', ()=>{
const v=parseFloat((ans.value||'').replace(',', '.'));
if(!isFinite(v)){ feedback(fb,false,'&#10007; Введи число.'); return; }
const tol=Q[i].tol||0.05;
const ok=Math.abs(v-Q[i].a)<=tol;
if(ok) score++;
feedback(fb, ok, ok?'&#10003; Верно!':'&#10007; Правильный ответ: $'+Q[i].a+'$');
i++;
setTimeout(show, 1000);
});
reset.addEventListener('click', ()=>{ i=0; score=0; done=false; show(); });
show();
})();
wireReadBtn('p11');
}
function buildP12(){
const box = document.getElementById('p12-body');
let html = '';
html += makeCard('theory', 'Формула Герона', '12.1', `
<p>Площадь треугольника по трём сторонам $a, b, c$ находится по <b>формуле Герона</b>:</p>
$$S = \\sqrt{p(p-a)(p-b)(p-c)}$$
<p>где $p = \\dfrac{a+b+c}{2}$ — <b>полупериметр</b> треугольника.</p>
<p><b>В чём ценность?</b> Не нужны ни высоты, ни углы — достаточно знать только длины трёх сторон.</p>
<p><b>Пример.</b> Треугольник со сторонами $5, 7, 8$.</p>
$$p = \\dfrac{5 + 7 + 8}{2} = 10$$
$$S = \\sqrt{10 \\cdot (10-5) \\cdot (10-7) \\cdot (10-8)} = \\sqrt{10 \\cdot 5 \\cdot 3 \\cdot 2} = \\sqrt{300} = 10\\sqrt{3} \\approx 17{,}32$$
<details class="spoiler"><summary>Откуда берётся формула?</summary><div class="spoiler-body">
Идея: $S = \\tfrac{1}{2}ab\\sin C$, $\\cos C = \\tfrac{a^2+b^2-c^2}{2ab}$ (теорема косинусов), $\\sin^2 C = 1 - \\cos^2 C$. После алгебраических преобразований через разность квадратов выражение упрощается до формулы Герона.
</div></details>`);
html += makeCard('rule', 'Медиана треугольника', '12.2', `
<p>Длина медианы $m_c$, проведённой к стороне $c$:</p>
$$m_c = \\dfrac{1}{2}\\sqrt{2a^2 + 2b^2 - c^2}$$
<p>Аналогично для остальных медиан:</p>
$$m_a = \\dfrac{1}{2}\\sqrt{2b^2 + 2c^2 - a^2}, \\qquad m_b = \\dfrac{1}{2}\\sqrt{2a^2 + 2c^2 - b^2}$$
<p><b>Удобно:</b> три медианы пересекаются в одной точке — <b>центроиде</b>, делящей каждую медиану в отношении $2:1$ от вершины.</p>`);
html += makeCard('example', 'Решение произвольного треугольника', '12.3', `
<p><b>«Решить треугольник»</b> — значит найти все его стороны и углы. Какие случаи бывают:</p>
<ul style="padding-left:22px;line-height:1.95">
<li><b>3 стороны $(a,b,c)$:</b> углы — по теореме косинусов; площадь — по Герону.</li>
<li><b>2 стороны и угол между ними $(a, b, C)$:</b> 3-я сторона — по теореме косинусов; остальные углы — по теореме синусов; площадь $S = \\tfrac{1}{2}ab\\sin C$.</li>
<li><b>1 сторона и 2 угла $(a, A, B)$:</b> 3-й угол $C = 180° - A - B$; остальные стороны — по теореме синусов.</li>
<li><b>2 стороны и угол не между ними:</b> может быть 0, 1 или 2 решения (<i>неоднозначный случай</i>).</li>
</ul>
<p><b>Пример.</b> $a = 5$, $b = 7$, угол между ними $C = 60°$.</p>
$$c^2 = 25 + 49 - 2 \\cdot 5 \\cdot 7 \\cdot \\cos 60° = 74 - 35 = 39 \\Rightarrow c \\approx 6{,}24$$
$$S = \\dfrac{1}{2} \\cdot 5 \\cdot 7 \\cdot \\sin 60° = \\dfrac{35\\sqrt{3}}{4} \\approx 15{,}16$$`);
/* IV1 — Slider 3-х сторон */
html += `<div class="wg" id="p12-iv1">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 1</span><div class="wg-title">Слайдер трёх сторон</div></div>
<div class="wg-help">Задай стороны $a, b, c$ ползунками. Если они удовлетворяют неравенству треугольника, увидишь треугольник; площадь, углы и радиусы вычисляются автоматически.</div>
<div class="sliders">
<label>$a$<b id="p12-iv1-aval">5</b><input type="range" id="p12-iv1-a" min="1" max="15" step="1" value="5"></label>
<label>$b$<b id="p12-iv1-bval">7</b><input type="range" id="p12-iv1-b" min="1" max="15" step="1" value="7"></label>
<label>$c$<b id="p12-iv1-cval">8</b><input type="range" id="p12-iv1-c" min="1" max="15" step="1" value="8"></label>
</div>
<div style="background:var(--card);border-radius:10px;padding:10px;overflow-x:auto">
<svg id="p12-iv1-svg" viewBox="0 0 420 320" style="width:100%;min-width:340px;height:auto;display:block"></svg>
</div>
<div id="p12-iv1-out" style="margin-top:10px;padding:10px 14px;background:var(--pri-soft);border-radius:9px;font-size:.95rem;line-height:1.9"></div>
</div>`;
/* IV2 — Калькулятор Герона */
html += `<div class="wg" id="p12-iv2">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 2</span><div class="wg-title">Калькулятор Герона</div></div>
<div class="wg-help">Введи длины трёх сторон — программа найдёт площадь по формуле Герона.</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin-bottom:10px">
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $a$<input type="number" id="p12-iv2-a" class="tinp" style="width:100%;margin-top:4px" value="5" step="0.1" min="0.1"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $b$<input type="number" id="p12-iv2-b" class="tinp" style="width:100%;margin-top:4px" value="7" step="0.1" min="0.1"></label>
<label style="display:block;font-size:.85rem;color:var(--muted)">сторона $c$<input type="number" id="p12-iv2-c" class="tinp" style="width:100%;margin-top:4px" value="8" step="0.1" min="0.1"></label>
</div>
<div style="text-align:center;margin-bottom:10px"><button class="btn primary" id="p12-iv2-go">Найти площадь</button></div>
<div id="p12-iv2-out" style="padding:12px 14px;background:var(--card);border-radius:9px;font-size:.95rem;min-height:50px;line-height:1.9"></div>
<div class="feedback" id="p12-iv2-fb"></div>
</div>`;
/* IV3 — Какой случай / какой метод? */
html += `<div class="wg" id="p12-iv3">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 3</span><div class="wg-title">Какой метод применим?</div></div>
<div class="wg-help">Дано — какой инструмент быстрее всего приведёт к ответу? Выбери из четырёх.</div>
<div class="score-display"><span>Задача <b id="p12-iv3-i">1</b> / 6</span><span>Очки: <b id="p12-iv3-s">0</b> / 6</span></div>
<div id="p12-iv3-q" style="padding:14px;background:var(--pri-soft);border-radius:10px;font-size:1.05rem;text-align:center;margin-bottom:10px"></div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:8px">
<button class="btn primary" data-ans="heron" id="p12-iv3-heron">Формула Герона<br><span style="font-size:.78rem;opacity:.85">$\\sqrt{p(p-a)(p-b)(p-c)}$</span></button>
<button class="btn primary" data-ans="cos" id="p12-iv3-cos">Теорема косинусов<br><span style="font-size:.78rem;opacity:.85">$a^2 = b^2+c^2-2bc\\cos A$</span></button>
<button class="btn primary" data-ans="sin" id="p12-iv3-sin">Теорема синусов<br><span style="font-size:.78rem;opacity:.85">$\\dfrac{a}{\\sin A} = 2R$</span></button>
<button class="btn primary" data-ans="sab" id="p12-iv3-sab">$S = \\tfrac{1}{2}ab\\sin C$<br><span style="font-size:.78rem;opacity:.85">площадь через 2 стороны и угол</span></button>
</div>
<div class="feedback" id="p12-iv3-fb"></div>
</div>`;
/* IV4 — Тренажёр */
html += `<div class="wg" id="p12-iv4">
<div class="wg-header"><span class="wg-badge">ИНТЕРАКТИВ 4</span><div class="wg-title">Тренажёр Герона</div></div>
<div class="wg-help">Реши задачу и введи площадь (округляй до 2 знаков после запятой).</div>
<div class="score-display"><span>Задача <b id="p12-iv4-i">1</b> / 6</span><span>Очки: <b id="p12-iv4-s">0</b> / 6</span></div>
<div id="p12-iv4-q" style="padding:14px;background:var(--pri-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">
<span style="font-family:'JetBrains Mono',monospace">$S =$</span>
<input type="number" id="p12-iv4-ans" class="tinp" style="width:120px;text-align:center" step="0.01">
<button class="btn primary" id="p12-iv4-go">Проверить</button>
<button class="btn" id="p12-iv4-start">Заново</button>
</div>
<div class="feedback" id="p12-iv4-fb"></div>
</div>`;
html += readButton('p12');
html += secNav('p11', 'final3');
box.innerHTML = html;
renderMath(box);
/* IV1 — Slider 3-х сторон */
(function(){
const sA=document.getElementById('p12-iv1-a');
const sB=document.getElementById('p12-iv1-b');
const sC=document.getElementById('p12-iv1-c');
const lA=document.getElementById('p12-iv1-aval');
const lB=document.getElementById('p12-iv1-bval');
const lC=document.getElementById('p12-iv1-cval');
const svg=document.getElementById('p12-iv1-svg');
const out=document.getElementById('p12-iv1-out');
const seen=new Set();
function draw(){
const a=+sA.value, b=+sB.value, c=+sC.value;
lA.textContent=a; lB.textContent=b; lC.textContent=c;
// Неравенство треугольника
if(a+b<=c || a+c<=b || b+c<=a){
svg.innerHTML='<rect x="0" y="0" width="420" height="320" fill="none"/>'
+'<text x="210" y="160" text-anchor="middle" font-family="Inter,sans-serif" font-size="18" font-weight="700" fill="#dc2626">Невозможный треугольник</text>'
+'<text x="210" y="186" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" fill="#7f1d1d">Сумма двух сторон должна быть больше третьей</text>';
out.innerHTML='<b style="color:#dc2626">Неравенство треугольника не выполнено:</b> $a + b > c$, $a + c > b$, $b + c > a$.';
renderMath(out);
return;
}
// Построение треугольника: A в начале, B = (c, 0); C находим по теореме косинусов через cos A
const cosA=(b*b+c*c-a*a)/(2*b*c);
const sinA=Math.sqrt(Math.max(0,1-cosA*cosA));
// в локальных координатах: A=(0,0), B=(c,0), C=(b*cosA, b*sinA)
const Ax=0, Ay=0, Bx=c, By=0, Cx=b*cosA, Cy=b*sinA;
// масштабируем в SVG 420×320 с полем 40
const minX=Math.min(Ax,Bx,Cx), maxX=Math.max(Ax,Bx,Cx);
const minY=Math.min(Ay,Cy), maxY=Math.max(By,Cy);
const W=420, H=320, pad=46;
const sx=(W-2*pad)/(maxX-minX||1), sy=(H-2*pad)/(maxY-minY||1);
const s_=Math.min(sx,sy);
function T(X,Y){ return {x: pad+(X-minX)*s_, y: H-pad-(Y-minY)*s_}; }
const Av=T(Ax,Ay), Bv=T(Bx,By), Cv=T(Cx,Cy);
// углы
const cosB_=(a*a+c*c-b*b)/(2*a*c); const cosC_=(a*a+b*b-c*c)/(2*a*b);
const Aang=Math.acos(Math.max(-1,Math.min(1,cosA)))*180/Math.PI;
const Bang=Math.acos(Math.max(-1,Math.min(1,cosB_)))*180/Math.PI;
const Cang=Math.acos(Math.max(-1,Math.min(1,cosC_)))*180/Math.PI;
const p=(a+b+c)/2;
const S=Math.sqrt(Math.max(0,p*(p-a)*(p-b)*(p-c)));
const R=(a*b*c)/(4*S||1e-9);
const r=S/(p||1e-9);
const mA=0.5*Math.sqrt(2*b*b+2*c*c-a*a);
const mB=0.5*Math.sqrt(2*a*a+2*c*c-b*b);
const mC=0.5*Math.sqrt(2*a*a+2*b*b-c*c);
let s='';
s += '<rect x="0" y="0" width="420" height="320" fill="none"/>';
s += '<polygon points="'+Av.x.toFixed(2)+','+Av.y.toFixed(2)+' '+Bv.x.toFixed(2)+','+Bv.y.toFixed(2)+' '+Cv.x.toFixed(2)+','+Cv.y.toFixed(2)+'" fill="rgba(124,58,237,.12)" stroke="#7c3aed" stroke-width="2.2" stroke-linejoin="round"/>';
// подписи сторон
const midAB={x:(Av.x+Bv.x)/2, y:(Av.y+Bv.y)/2};
const midAC={x:(Av.x+Cv.x)/2, y:(Av.y+Cv.y)/2};
const midBC={x:(Bv.x+Cv.x)/2, y:(Bv.y+Cv.y)/2};
s += '<text x="'+midAB.x.toFixed(2)+'" y="'+(midAB.y+18).toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#5b21b6">c = '+c+'</text>';
s += '<text x="'+(midAC.x-16).toFixed(2)+'" y="'+midAC.y.toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#5b21b6">b = '+b+'</text>';
s += '<text x="'+(midBC.x+16).toFixed(2)+'" y="'+midBC.y.toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="13" font-weight="700" fill="#dc2626">a = '+a+'</text>';
// вершины с подписями
[['A',Av,-14,18],['B',Bv,14,18],['C',Cv,0,-10]].forEach(([n,P,dx,dy])=>{
s += '<circle cx="'+P.x.toFixed(2)+'" cy="'+P.y.toFixed(2)+'" r="4" fill="#0f172a"/>';
s += '<text x="'+(P.x+dx).toFixed(2)+'" y="'+(P.y+dy).toFixed(2)+'" text-anchor="middle" font-family="Inter,sans-serif" font-size="14" font-weight="800" fill="#0f172a">'+n+'</text>';
});
svg.innerHTML=s;
out.innerHTML = '<b>Полупериметр:</b> $p = '+p.toFixed(2)+'$ &nbsp;·&nbsp; <b>Площадь (Герон):</b> $S \\approx '+S.toFixed(2)+'$<br>'
+ '<b>Углы:</b> $A \\approx '+Aang.toFixed(1)+'°$, $B \\approx '+Bang.toFixed(1)+'°$, $C \\approx '+Cang.toFixed(1)+'°$<br>'
+ '<b>Радиусы:</b> $R \\approx '+R.toFixed(2)+'$, $r \\approx '+r.toFixed(2)+'$<br>'
+ '<b>Медианы:</b> $m_a \\approx '+mA.toFixed(2)+'$, $m_b \\approx '+mB.toFixed(2)+'$, $m_c \\approx '+mC.toFixed(2)+'$';
renderMath(out);
seen.add(a+'|'+b+'|'+c);
if(seen.size>=4 && !seen.has('done')){ addXp(10,'p12-iv1'); bumpProgress('p12',15); seen.add('done'); }
}
[sA,sB,sC].forEach(s=>s.addEventListener('input', draw));
draw();
})();
/* IV2 — Калькулятор Герона */
(function(){
const aI=document.getElementById('p12-iv2-a');
const bI=document.getElementById('p12-iv2-b');
const cI=document.getElementById('p12-iv2-c');
const go=document.getElementById('p12-iv2-go');
const out=document.getElementById('p12-iv2-out');
const fb=document.getElementById('p12-iv2-fb');
let solved=0;
go.addEventListener('click', ()=>{
const a=parseFloat(aI.value), b=parseFloat(bI.value), c=parseFloat(cI.value);
if(!isFinite(a)||!isFinite(b)||!isFinite(c)){ feedback(fb,false,'&#10007; Введи все значения.'); return; }
if(a<=0||b<=0||c<=0){ feedback(fb,false,'&#10007; Стороны должны быть положительными.'); return; }
if(a+b<=c||a+c<=b||b+c<=a){ feedback(fb,false,'&#10007; Неравенство треугольника не выполняется.'); return; }
const p=(a+b+c)/2;
const inner=p*(p-a)*(p-b)*(p-c);
const S=Math.sqrt(Math.max(0,inner));
out.innerHTML = '$p = \\dfrac{a+b+c}{2} = \\dfrac{'+a+'+'+b+'+'+c+'}{2} = '+p.toFixed(2)+'$<br>'
+ '$S = \\sqrt{p(p-a)(p-b)(p-c)} = \\sqrt{'+p.toFixed(2)+' \\cdot '+(p-a).toFixed(2)+' \\cdot '+(p-b).toFixed(2)+' \\cdot '+(p-c).toFixed(2)+'}$<br>'
+ '$S = \\sqrt{'+inner.toFixed(3)+'} \\approx '+S.toFixed(3)+'$';
renderMath(out);
feedback(fb,true,'&#10003; Площадь найдена.');
solved++;
if(solved===1){ addXp(10,'p12-iv2'); bumpProgress('p12',10); }
});
})();
/* IV3 — Какой метод? */
(function(){
const Q=[
{t:'Известны три стороны треугольника. Найти его площадь.', a:'heron'},
{t:'Известны две стороны и угол между ними. Найти площадь.', a:'sab'},
{t:'Известны три стороны. Найти один из углов.', a:'cos'},
{t:'Известны сторона и противолежащий ей угол. Найти радиус описанной окружности.', a:'sin'},
{t:'Известны два угла и одна сторона. Найти другую сторону.', a:'sin'},
{t:'Известны две стороны и угол между ними. Найти третью сторону.', a:'cos'}
];
const explain={
heron:'$S = \\sqrt{p(p-a)(p-b)(p-c)}$ — площадь без углов и высот.',
cos:'$a^2 = b^2+c^2-2bc\\cos A$ — связывает 3 стороны и угол.',
sin:'$\\dfrac{a}{\\sin A} = 2R$ — пара (сторона; противолежащий угол).',
sab:'$S = \\tfrac{1}{2}ab\\sin C$ — площадь через две стороны и угол между ними.'
};
const labels={heron:'формула Герона', cos:'теорема косинусов', sin:'теорема синусов', sab:'$S = \\tfrac{1}{2}ab\\sin C$'};
const qBox=document.getElementById('p12-iv3-q');
const iEl=document.getElementById('p12-iv3-i');
const sEl=document.getElementById('p12-iv3-s');
const fb=document.getElementById('p12-iv3-fb');
const btns=document.querySelectorAll('#p12-iv3 button[data-ans]');
let i=0, score=0, done=false;
function show(){
if(i>=Q.length){
qBox.innerHTML='Завершено! Очки: <b>'+score+'</b> / '+Q.length;
if(!done){ done=true; addXp(15,'p12-iv3'); bumpProgress('p12',25); if(score===Q.length) achievement('p12_done'); }
btns.forEach(b=>b.disabled=true);
return;
}
qBox.innerHTML=Q[i].t; renderMath(qBox);
iEl.textContent=(i+1); sEl.textContent=score;
fb.style.display='none';
}
btns.forEach(b=>b.addEventListener('click', ()=>{
const ok = b.dataset.ans===Q[i].a;
if(ok) score++;
feedback(fb, ok, ok?('&#10003; Верно! '+explain[Q[i].a]):('&#10007; Правильно: '+labels[Q[i].a]+'. '+explain[Q[i].a]));
i++;
setTimeout(show, 1100);
}));
show();
})();
/* IV4 — Тренажёр Герона */
(function(){
const Q=[
{t:'Треугольник со сторонами $3, 4, 5$. Найди площадь $S$.', a:6, tol:0.02},
{t:'Треугольник со сторонами $5, 5, 6$. Найди площадь $S$.', a:12, tol:0.02},
{t:'Треугольник со сторонами $7, 8, 9$. Найди площадь $S$.', a:26.83, tol:0.05},
{t:'Треугольник со сторонами $6, 8, 10$. Найди площадь $S$.', a:24, tol:0.02},
{t:'Треугольник со сторонами $13, 14, 15$. Найди площадь $S$.', a:84, tol:0.05},
{t:'Стороны $a = 4$, $b = 6$, угол между ними $C = 90°$. Найди $S$.', a:12, tol:0.02}
];
const qBox=document.getElementById('p12-iv4-q');
const ans=document.getElementById('p12-iv4-ans');
const go=document.getElementById('p12-iv4-go');
const reset=document.getElementById('p12-iv4-start');
const iEl=document.getElementById('p12-iv4-i');
const sEl=document.getElementById('p12-iv4-s');
const fb=document.getElementById('p12-iv4-fb');
let i=0, score=0, done=false;
function show(){
if(i>=Q.length){
qBox.innerHTML='Финиш! Очки: <b>'+score+'</b> / '+Q.length;
if(!done){ done=true; addXp(15,'p12-iv4'); bumpProgress('p12',25); }
go.disabled=true; ans.disabled=true;
return;
}
qBox.innerHTML=Q[i].t; renderMath(qBox);
iEl.textContent=(i+1); sEl.textContent=score;
ans.value=''; fb.style.display='none'; go.disabled=false; ans.disabled=false;
}
go.addEventListener('click', ()=>{
const v=parseFloat((ans.value||'').replace(',', '.'));
if(!isFinite(v)){ feedback(fb,false,'&#10007; Введи число.'); return; }
const tol=Q[i].tol||0.05;
const ok=Math.abs(v-Q[i].a)<=tol;
if(ok) score++;
feedback(fb, ok, ok?'&#10003; Верно!':'&#10007; Правильный ответ: $'+Q[i].a+'$');
i++;
setTimeout(show, 1000);
});
reset.addEventListener('click', ()=>{ i=0; score=0; done=false; show(); });
show();
})();
wireReadBtn('p12');
}
function buildFinal3(){
const box = document.getElementById('final3-body');
let html = '';
/* Часть А — Шпаргалка главы (3 mini-карточки) */
html += `<div class="card">
<div class="card-header">
<div class="card-icon theory">${ICONS.theory}</div>
<div class="card-title">Шпаргалка главы 3</div>
<div class="card-num">Итог</div>
</div>
<div class="card-body">
<p>Главные формулы главы «Теоремы синусов и косинусов» — в одном месте. Просмотри перед боссами!</p>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;margin-top:10px">
<div style="padding:12px 14px;background:var(--acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--pri2)" stroke-width="2" style="width:18px;height:18px"><circle cx="12" cy="12" r="9"/><polygon points="6 7 17 7 12 18"/></svg>
<span style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 10 · Теорема синусов</span>
</div>
<div style="font-size:.92rem">$\\dfrac{a}{\\sin A} = \\dfrac{b}{\\sin B} = \\dfrac{c}{\\sin C} = 2R$. Пара «сторона + противолежащий угол» $\\Rightarrow$ радиус $R$ и любая сторона.</div>
</div>
<div style="padding:12px 14px;background:var(--acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--pri2)" stroke-width="2" style="width:18px;height:18px"><polygon points="4 20 20 20 12 4"/><line x1="4" y1="20" x2="20" y2="20"/></svg>
<span style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 11 · Теорема косинусов</span>
</div>
<div style="font-size:.92rem">$a^2 = b^2 + c^2 - 2bc\\cos A$ — обобщение Пифагора. По 3 сторонам — любой угол; по 2 сторонам и углу — 3-я сторона.</div>
</div>
<div style="padding:12px 14px;background:var(--acc-soft);border-radius:11px;border-left:3px solid var(--pri)">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--pri2)" stroke-width="2" style="width:18px;height:18px"><path d="M4 17 L12 5 L20 17 Z"/><line x1="8" y1="17" x2="16" y2="17"/></svg>
<span style="font-family:'Unbounded',sans-serif;font-weight:700;color:var(--pri2);font-size:.92rem">§ 12 · Формула Герона</span>
</div>
<div style="font-size:.92rem">$S = \\sqrt{p(p-a)(p-b)(p-c)}$, где $p = \\dfrac{a+b+c}{2}$ — полупериметр. Площадь по трём сторонам.</div>
</div>
</div>
</div>
</div>`;
/* Часть Б — 5 боссов */
html += `<div class="card">
<div class="card-header">
<div class="card-icon rule">${ICONS.rule}</div>
<div class="card-title">Боссы главы 3</div>
<div class="card-num">5</div>
</div>
<div class="card-body">
<p>5 интегрированных задач — каждая комбинирует темы §§10–12. За каждого побеждённого босса — <b>+10 XP</b> и +18% к прогрессу. Победишь всех — <b>+50 XP бонус</b> и ачивка «Магистр треугольников»!</p>
</div>
</div>`;
html += '<div id="ch3G-bosses-container"></div>';
html += `<div style="margin-top:18px;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" id="ch3G-final-summary">
<div style="font-family:'Unbounded',sans-serif;font-weight:800;color:var(--pri2);font-size:1.1rem;margin-bottom:6px">Прогресс по боссам</div>
<div id="ch3G-boss-overall" style="font-size:.95rem;color:var(--text);margin-bottom:10px">0 / 5 боссов побеждено</div>
<div style="height:12px;background:var(--card);border-radius:8px;overflow:hidden;border:1px solid var(--border)">
<div id="ch3G-boss-overall-fill" style="height:100%;width:0%;background:linear-gradient(90deg,var(--pri),var(--acc));transition:width .35s"></div>
</div>
<div id="ch3G-final-reward" style="margin-top:14px;display:none;padding:14px;background:var(--card);border-radius:11px;border:2px solid var(--warn)">
<div style="display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:6px">
<svg viewBox="0 0 24 24" fill="none" stroke="var(--warn)" stroke-width="2.2" style="width:22px;height:22px"><polygon points="12 2 15 9 22 9 17 14 19 22 12 18 5 22 7 14 2 9 9 9"/></svg>
<div style="font-family:'Unbounded',sans-serif;font-weight:800;color:var(--pri2);font-size:1.05rem">Магистр треугольников</div>
</div>
<div style="font-size:.92rem;margin-bottom:10px">Глава 3 пройдена! Все 5 боссов повержены. +50 XP бонус.</div>
<a class="btn primary" href="/textbook/geometry-9-ch4" style="text-decoration:none">Дальше: Глава 4 <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></a>
</div>
</div>`;
html += secNav('p12', null);
box.innerHTML = html;
renderMath(box);
/* Боссы */
const BOSSES = [
{
n:1, color:'#10b981',
title:'Циклоп Синусов',
tag:'§ 10',
q:'В треугольнике сторона $a = 12$ и противолежащий угол $A = 30°$. Найди радиус описанной окружности $R$.',
ans:12, decimal:false,
hint:'$R = \\dfrac{a}{2\\sin A} = \\dfrac{12}{2 \\cdot 0{,}5} = \\dfrac{12}{1} = 12$.'
},
{
n:2, color:'#0891b2',
title:'Минотавр Косинусов',
tag:'§ 11',
q:'В треугольнике стороны $b = 5$ и $c = 8$, угол между ними $A = 60°$. Найди сторону $a$.',
ans:7, decimal:false,
hint:'$a^2 = 25 + 64 - 2 \\cdot 5 \\cdot 8 \\cdot \\cos 60° = 89 - 40 = 49 \\Rightarrow a = 7$.'
},
{
n:3, color:'#7c3aed',
title:'Гарпия Герона',
tag:'§ 12',
q:'Треугольник со сторонами $5, 12, 13$. Найди его площадь $S$.',
ans:30, decimal:false,
hint:'$p = 15$. $S = \\sqrt{15 \\cdot 10 \\cdot 3 \\cdot 2} = \\sqrt{900} = 30$. Заметь: это прямоугольный треугольник, и $S = \\tfrac{1}{2}\\cdot 5 \\cdot 12 = 30$.'
},
{
n:4, color:'#dc2626',
title:'Дракон Решения',
tag:'§§ 11 + 12',
q:'В треугольнике стороны $7, 8, 9$. Найди наибольший угол (в градусах, округли до целого).',
ans:73, decimal:false,
hint:'Наибольший угол лежит против стороны $9$. $\\cos C = \\dfrac{49+64-81}{2\\cdot 7\\cdot 8} = \\dfrac{32}{112} \\approx 0{,}2857$. $C = \\arccos(0{,}2857) \\approx 73°$.'
},
{
n:5, color:'#f59e0b',
title:'Мастер Треугольников',
tag:'§§ 1012 — синтез',
q:'В треугольнике стороны $a = 10$, $b = 6$, угол между ними $C = 60°$. Найди площадь $S$ (округли до 2 знаков).',
ans:25.98, decimal:true,
hint:'$S = \\tfrac{1}{2}ab\\sin C = \\tfrac{1}{2}\\cdot 10 \\cdot 6 \\cdot \\sin 60° = 30 \\cdot \\dfrac{\\sqrt{3}}{2} = 15\\sqrt{3} \\approx 25{,}98$.'
}
];
const cont = document.getElementById('ch3G-bosses-container');
const STATE_KEY = 'geometry9_ch3_bosses';
const BOSS_STATE = (function(){
try{ const s = localStorage.getItem(STATE_KEY); if(s){ const p = JSON.parse(s); if(Array.isArray(p) && p.length === BOSSES.length) return p; } }catch(e){}
return BOSSES.map(()=>({defeated:false}));
})();
function saveBosses(){ try{ localStorage.setItem(STATE_KEY, JSON.stringify(BOSS_STATE)); }catch(e){} }
cont.innerHTML = BOSSES.map((b)=>{
const stepAttr = b.decimal ? 'step="0.01"' : 'step="1"';
const ph = b.decimal ? 'число (можно с запятой)' : 'целое число';
return '<div class="boss-card" id="bossG3-'+b.n+'-card" style="padding:16px;background:var(--card);border-radius:12px;border:2px solid '+b.color+';margin-bottom:14px;transition:background .3s,box-shadow .3s">'
+'<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap">'
+'<svg viewBox="0 0 24 24" fill="none" stroke="'+b.color+'" stroke-width="2.2" style="width:28px;height:28px;flex-shrink:0"><polygon points="12 4 20 18 4 18"/><circle cx="12" cy="13" r="2.4"/></svg>'
+'<div style="font-family:\'Unbounded\',sans-serif;font-weight:800;color:'+b.color+';font-size:1.05rem">Босс '+b.n+': '+b.title+'</div>'
+'<div style="margin-left:auto;font-size:.78rem;color:var(--muted);padding:3px 8px;background:var(--acc-soft);border-radius:6px">'+b.tag+'</div>'
+'</div>'
+'<div id="bossG3-'+b.n+'-q" class="boss-q" style="padding:12px 14px;background:var(--acc-soft);border-radius:9px;font-size:1rem;line-height:1.55;margin-bottom:10px">'+b.q+'</div>'
+'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">'
+'<span style="font-family:\'JetBrains Mono\',monospace;font-size:.92rem">ответ =</span>'
+'<input type="number" '+stepAttr+' id="bossG3-'+b.n+'-ans" class="tinp" style="width:150px;text-align:center" placeholder="'+ph+'">'
+'<button class="btn primary" id="bossG3-'+b.n+'-go" style="background:'+b.color+';border-color:'+b.color+'">Атаковать</button>'
+'<button class="btn" id="bossG3-'+b.n+'-hint">Подсказка</button>'
+'</div>'
+'<div class="feedback" id="bossG3-'+b.n+'-fb"></div>'
+'</div>';
}).join('');
renderMath(cont);
function markDefeatedUI(b){
const card = document.getElementById('bossG3-'+b.n+'-card');
const goBtn = document.getElementById('bossG3-'+b.n+'-go');
const ansInp = document.getElementById('bossG3-'+b.n+'-ans');
if(!card || !goBtn || !ansInp) return;
card.style.background = 'linear-gradient(135deg,var(--acc-soft),var(--pri-soft))';
card.style.boxShadow = '0 0 0 2px '+b.color+'33, 0 8px 24px rgba(16,185,129,.12)';
goBtn.disabled = true; goBtn.style.opacity = .55;
goBtn.innerHTML = '<svg class="ic" viewBox="0 0 24 24" style="margin-right:4px"><polyline points="20 6 9 17 4 12"/></svg> Повержен';
ansInp.disabled = true;
}
function refreshOverall(){
const won = BOSS_STATE.filter(s => s.defeated).length;
const txt = document.getElementById('ch3G-boss-overall');
const fill = document.getElementById('ch3G-boss-overall-fill');
if(txt) txt.textContent = won + ' / ' + BOSSES.length + ' боссов побеждено';
if(fill) fill.style.width = (won * 100 / BOSSES.length) + '%';
if(won >= BOSSES.length){
const reward = document.getElementById('ch3G-final-reward');
if(reward && reward.style.display === 'none'){
reward.style.display = 'block';
if(!STATE.achievements.has('ch3_done')){
achievement('ch3_done','Магистр треугольников');
addXp(50, 'ch3-bonus');
bumpProgress('final3', 10);
if(window.confetti){ try{ window.confetti(); }catch(e){} }
}
}
}
}
BOSSES.forEach((b, idx)=>{
const goBtn = document.getElementById('bossG3-'+b.n+'-go');
const hintBtn = document.getElementById('bossG3-'+b.n+'-hint');
const ansInp = document.getElementById('bossG3-'+b.n+'-ans');
if(BOSS_STATE[idx].defeated) markDefeatedUI(b);
goBtn.addEventListener('click', ()=>{
if(BOSS_STATE[idx].defeated) return;
const fb = document.getElementById('bossG3-'+b.n+'-fb');
const raw = (ansInp.value||'').replace(',', '.').trim();
const val = parseFloat(raw);
if(isNaN(val) || raw === ''){ feedback(fb, false, '&#10007; Введи число.'); return; }
const ok = Math.abs(val - b.ans) < 0.05;
if(ok){
BOSS_STATE[idx].defeated = true; saveBosses();
feedback(fb, true, '&#10003; Босс '+b.n+' повержен! +10 XP. '+b.hint);
addXp(10, 'boss-ch3-'+b.n);
bumpProgress('final3', 18);
markDefeatedUI(b);
refreshOverall();
} else {
feedback(fb, false, '&#10007; Промах. Попробуй ещё. Подсказка доступна.');
}
});
hintBtn.addEventListener('click', ()=>{
const fb = document.getElementById('bossG3-'+b.n+'-fb');
fb.className = 'feedback ok';
fb.innerHTML = '<b>Подсказка:</b> '+b.hint;
fb.style.display = 'block';
fb.style.background = 'var(--warn-bg)';
fb.style.color = '#92400e';
fb.style.borderLeftColor = 'var(--warn)';
renderMath(fb);
});
ansInp.addEventListener('keydown', e=>{ if(e.key === 'Enter') goBtn.click(); });
});
refreshOverall();
}
/* ===== Search ===== */
const SEARCH_INDEX = (function(){
const arr=[];
PARAS.forEach(p=>arr.push({kind:'Параграф',title:p.num+' '+p.name,desc:p.sub||'',sec:p.id}));
return arr;
})();
function initSearch(){
const modal=document.getElementById('search-modal'),inp=document.getElementById('search-input'),out=document.getElementById('search-results'),btn=document.getElementById('search-btn');
if(!modal||!inp||!out) return;
let cur=0,rows=[];
function score(q,it){ const t=(it.title+' '+it.desc).toLowerCase(); if(t.includes(q)) return 100+(it.title.toLowerCase().startsWith(q)?50:0); let s=0; q.split(/\s+/).forEach(w=>{if(w&&t.includes(w))s+=10;}); return s; }
function rank(q){ q=q.trim().toLowerCase(); if(!q) return SEARCH_INDEX.slice(0,12); return SEARCH_INDEX.map(it=>({it,s:score(q,it)})).filter(x=>x.s>0).sort((a,b)=>b.s-a.s).slice(0,20).map(x=>x.it); }
function render(){ cur=0; if(!rows.length){out.innerHTML='<div class="search-empty">Ничего не найдено</div>';return;} out.innerHTML=rows.map((r,i)=>'<button class="search-row'+(i===0?' active':'')+'" data-i="'+i+'"><div class="sr-kind">'+r.kind+'</div><div class="sr-title">'+r.title+'</div>'+(r.desc?'<div class="sr-desc">'+(r.desc.length>90?r.desc.slice(0,90)+'…':r.desc)+'</div>':'')+'</button>').join(''); out.querySelectorAll('.search-row').forEach(b=>b.addEventListener('click',()=>{cur=+b.dataset.i;pick();})); }
function pick(){ const r=rows[cur]; if(!r) return; close(); goTo(r.sec); }
function move(d){ const items=out.querySelectorAll('.search-row'); if(!items.length) return; items[cur]&&items[cur].classList.remove('active'); cur=(cur+d+items.length)%items.length; items[cur].classList.add('active'); items[cur].scrollIntoView({block:'nearest'}); }
function open(){ modal.classList.add('show'); inp.value=''; rows=rank(''); render(); setTimeout(()=>inp.focus(),50); }
function close(){ modal.classList.remove('show'); }
btn&&btn.addEventListener('click',open);
modal.addEventListener('click',e=>{if(e.target===modal)close();});
inp.addEventListener('input',()=>{rows=rank(inp.value);render();});
inp.addEventListener('keydown',e=>{ if(e.key==='ArrowDown'){e.preventDefault();move(1);}else if(e.key==='ArrowUp'){e.preventDefault();move(-1);}else if(e.key==='Enter'){e.preventDefault();pick();}else if(e.key==='Escape'){e.preventDefault();close();} });
document.addEventListener('keydown',e=>{ if((e.ctrlKey||e.metaKey)&&(e.key==='k'||e.key==='K')){ e.preventDefault(); if(modal.classList.contains('show')) close(); else open(); } });
}
function initSidebarToggle(){
const side=document.getElementById('col-side'),back=document.getElementById('col-side-backdrop'),btn=document.getElementById('sidebar-btn');
if(!side||!btn) return;
function open(){ side.classList.add('open'); back.classList.add('show'); }
function close(){ side.classList.remove('open'); back.classList.remove('show'); }
btn.addEventListener('click',()=>{ if(side.classList.contains('open')) close(); else open(); });
back.addEventListener('click',close);
document.addEventListener('keydown',e=>{ if(e.key==='Escape') close(); });
}
function init(){
loadProgress(); initTheme(); initSidebarToggle(); initSearch();
buildParaSelector(); refreshProgressUI(); loadServerReadState(); goTo('p10');
setTimeout(()=>achievement('start'), 600);
if(window.LS&&window.LS.xp){
window.LS.xp.load().then(function(s){ if(s&&s.xp>STATE.xp){ STATE.xp=s.xp; STATE.level=calcLevel(STATE.xp); saveProgress(); refreshProgressUI(); if(STATE.current) buildSidebar(STATE.current); } });
}
}
document.addEventListener('DOMContentLoaded', init);
/* === GEOM9 POLISH JS === */
(function(){
function bumpScore(el){
if(!el) return;
el.classList.remove('bump');
void el.offsetWidth;
el.classList.add('bump');
setTimeout(function(){ try{ el.classList.remove('bump'); }catch(e){} }, 270);
}
window.__geom9BumpScore = bumpScore;
function observeScores(root){
root = root || document;
var nodes = root.querySelectorAll('.score-display b');
nodes.forEach(function(b){
if(b.__scoreObs) return;
b.__scoreObs = true;
var last = b.textContent;
try{
var mo = new MutationObserver(function(){
var nv = b.textContent;
if(nv !== last){ last = nv; bumpScore(b); }
});
mo.observe(b, {childList:true, characterData:true, subtree:true});
}catch(e){}
});
}
function rescanScores(){ try{ observeScores(document); }catch(e){} }
if(document.readyState === 'loading') document.addEventListener('DOMContentLoaded', rescanScores);
else rescanScores();
try{
var rootObs = new MutationObserver(function(muts){
var need = false;
for(var i=0;i<muts.length && !need;i++){
var m = muts[i];
for(var j=0;j<m.addedNodes.length;j++){
var n = m.addedNodes[j];
if(n.nodeType===1){
if(n.classList && n.classList.contains('score-display')) { need = true; break; }
if(n.querySelector && n.querySelector('.score-display b')) { need = true; break; }
}
}
}
if(need) rescanScores();
});
rootObs.observe(document.body, {childList:true, subtree:true});
}catch(e){}
function refreshDoneMarks(){
try{
if(typeof STATE === 'undefined' || !STATE.progress) return;
document.querySelectorAll('.psel-card').forEach(function(c){
var id = c.dataset.id || c.dataset.progCard;
if(!id) return;
var pct = +STATE.progress[id] || 0;
if(!c.querySelector('.psel-done')){
var s = document.createElement('span');
s.className = 'psel-done';
s.setAttribute('title','Прочитано');
s.innerHTML = '<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>';
c.appendChild(s);
}
c.classList.toggle('done', pct >= 50);
});
}catch(e){}
}
try{
if(typeof window.refreshProgressUI === 'function'){
var _origRP = window.refreshProgressUI;
window.refreshProgressUI = function(){ var r = _origRP.apply(this, arguments); setTimeout(refreshDoneMarks, 0); return r; };
}
}catch(e){}
setTimeout(refreshDoneMarks, 600);
setTimeout(refreshDoneMarks, 1800);
window.addEventListener('focus', function(){ setTimeout(refreshDoneMarks, 200); });
document.addEventListener('click', function(e){
var card = e.target.closest && e.target.closest('.psel-card');
if(!card) return;
var id = card.dataset.id;
if(!id) return;
setTimeout(function(){
var sec = document.getElementById('sec-' + id);
if(sec) try{ sec.scrollIntoView({behavior:'smooth', block:'start'}); }catch(e){}
}, 60);
});
function observeBossGlow(){
var cards = document.querySelectorAll('.boss-card');
cards.forEach(function(card){
if(card.__bossObs) return;
card.__bossObs = true;
try{
var mo = new MutationObserver(function(){
if(card.__glowed) return;
var go = card.querySelector('button[id*="-go"]');
var fb = card.querySelector('.feedback.ok, .boss-fb.ok');
var defeatedByBtn = go && go.disabled;
var defeatedByFb = fb && fb.textContent && /Побед|повержен/i.test(fb.textContent);
if(defeatedByBtn || defeatedByFb){
card.__glowed = true;
card.classList.add('glow','pulse');
setTimeout(function(){ try{ card.classList.remove('pulse'); }catch(e){} }, 900);
setTimeout(function(){ try{ card.classList.remove('glow'); }catch(e){} }, 1400);
}
});
mo.observe(card, {childList:true, subtree:true, attributes:true, attributeFilter:['class','style','disabled']});
var goNow = card.querySelector('button[id*="-go"]');
if(goNow && goNow.disabled) card.__glowed = true;
}catch(e){}
});
}
var bossRescan = function(){ try{ observeBossGlow(); }catch(e){} };
setTimeout(bossRescan, 800);
var bossPoll = setInterval(bossRescan, 2500);
setTimeout(function(){ try{ clearInterval(bossPoll); }catch(e){} }, 90000);
})();
</script>
</body>
</html>