Files
Learn_System/frontend/textbooks/physics_8_ch2.html
T
Maxim Dolgolyov 33a91900a8 feat(phys8): Phase 0 — skeleton hub + 3 chapters + lab + phys.js/optics.js
Полная инфраструктура курса «Физика 8» (Исаченкова, 2018):
- physics_8_hub.html: палитра violet/indigo, 3 главы + ЛР + финал курса
  с 10 интегрированными боссами и ачивкой «Магистр физики 8» (+150 XP)
- physics_8_ch1.html (Тепловые, §§1–11): красный акцент
- physics_8_ch2.html (Электромагнитные, §§12–31): янтарный акцент
- physics_8_ch3.html (Световые, §§32–40): голубой акцент
- physics_8_lab.html (7 ЛР): зелёный акцент
- Расширение phys.js: tempColor, thermometer, calorimeter, createHeatBar,
  phaseGraphTT, Rseries, Rparallel
- Новый модуль optics.js: ray, refractRay, reflectRay, mirrorPlane,
  mirrorSpherical, thinLens, buildLensImage, goldenRays, eyeDiagram,
  lightObject, shadowTriangle
- Миграция 037: replace legacy children (thermal/electro/optics) на
  physics-8-ch1/ch2/ch3 + physics-8-lab; обновлён hub до 47 пунктов

BUILDERS всех § рендерят stub с указанием Phase/Wave из PLAN_PHYSICS_8.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 22:41:37 +03:00

732 lines
65 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Физика 8 · Глава 2 · «Электромагнитные явления»</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"
onload="renderMathInElement(document.body,{delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false},{left:'\\[',right:'\\]',display:true},{left:'\\(',right:'\\)',display:false}],throwOnError:false})"></script>
<script src="/js/api.js" defer></script>
<script src="/js/xp.js" defer></script>
<script src="/js/g3d.js" defer></script>
<script src="/js/phys.js" defer></script>
<script src="/js/optics.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:#fffbeb; --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:#5b21b6; --pri-soft:#ede9fe;
--acc:#a78bfa; --acc2:#7c3aed; --acc-soft:#ede9fe;
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0a0e; --card:#13120a; --card-soft:#18160a; --text:#fef9e7; --ink:#fef9e7; --muted:#a39070; --border:#2a2512}
*{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,#78350f 0%,#d97706 55%,#fcd34d 100%);color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.2);min-height:130px}
.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 h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
.sec[id="sec-p12"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p13"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p14"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p15"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p16"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p17"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p18"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p19"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p20"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p21"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p22"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p23"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p24"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p25"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p26"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p27"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p28"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p29"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p30"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-p31"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.sec[id="sec-final2"]{ --sec-acc:#d97706; --sec-acc-d:#92400e; --sec-acc-soft:#fef3c7; }
.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-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.6rem;font-weight:800;color:var(--sec-acc-d,var(--pri2));letter-spacing:-.01em;line-height:1.25}
.card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:18px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(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(--sec-acc-soft,var(--pri-soft));padding:3px 7px;border-radius:5px}
.card-body{font-size:.94rem;line-height:1.65}
.card-body p{margin-bottom:8px}
.card-body p:last-child{margin-bottom:0}
.btn{padding:8px 16px;border-radius:8px;background:var(--card);color:var(--text);border:1.5px solid var(--border);font-weight:600;font-size:.88rem;transition:background .15s,border-color .15s,transform .1s}
.btn:hover{background:var(--sec-acc-soft,var(--pri-soft));border-color:var(--sec-acc,var(--pri))}
.btn:active{transform:scale(.96)}
.btn.primary{background:var(--sec-acc,var(--pri));color:#fff;border-color:var(--sec-acc,var(--pri))}
.btn.primary:hover{background:var(--sec-acc-d,var(--pri2));border-color:var(--sec-acc-d,var(--pri2))}
.feedback{padding:10px 14px;border-radius:9px;font-weight:600;font-size:.88rem;margin-top:8px;display:none}
.feedback.ok{display:block;background:var(--ok-bg);color:#065f46;border-left:4px solid var(--ok)}
.feedback.fail{display:block;background:var(--fail-bg);color:#7f1d1d;border-left:4px solid var(--fail)}
.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
.sidecard-row:last-child{margin-bottom:0}
@media(max-width:980px){.col-side{position:static;max-height:none}}
.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:8px;display:flex;align-items:center;justify-content:space-between}
.xp-level{font-size:1.1rem;font-weight:900;color:var(--acc2);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(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(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(--sec-acc-soft,var(--pri-soft))}
.search-row .sr-kind{font-size:.7rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;margin-bottom:2px}
.search-row .sr-title{font-weight:700;font-size:.92rem;color:var(--text)}
.search-row .sr-desc{font-size:.8rem;color:var(--muted);margin-top:2px}
.search-empty{padding:20px;text-align:center;color:var(--muted);font-size:.88rem}
.search-foot{padding:8px 14px;border-top:1px solid var(--border);font-size:.74rem;color:var(--muted);display:flex;gap:14px}
.search-foot kbd{padding:2px 6px;background:var(--card);border:1px solid var(--border);border-radius:4px;font-family:'JetBrains Mono',monospace;font-size:.72rem}
.sec{transition:opacity .25s}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Физика 8 · Глава 2</h1>
<div class="hdr-sub">Электризация · ток · закон Ома · магнитное поле</div>
</div>
<div class="hdr-side">
<a href="/textbook/physics-8" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 8</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>Заряды притягиваются и отталкиваются, образуют электрическое поле. По проводнику течёт ток, и закон Ома связывает $U$, $I$, $R$. У постоянных магнитов и проводников с током есть магнитное поле.</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('p12')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать § 12</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-p12" class="sec"><div class="sec-header"><span class="sec-num">&sect; 12</span><h2 class="sec-h">Электризация тел. Взаимодействие зарядов</h2></div><div id="p12-body"></div></section>
<section id="sec-p13" class="sec"><div class="sec-header"><span class="sec-num">&sect; 13</span><h2 class="sec-h">Проводники и диэлектрики</h2></div><div id="p13-body"></div></section>
<section id="sec-p14" class="sec"><div class="sec-header"><span class="sec-num">&sect; 14</span><h2 class="sec-h">Электризация через влияние</h2></div><div id="p14-body"></div></section>
<section id="sec-p15" class="sec"><div class="sec-header"><span class="sec-num">&sect; 15</span><h2 class="sec-h">Электрический заряд. Элементарный заряд</h2></div><div id="p15-body"></div></section>
<section id="sec-p16" class="sec"><div class="sec-header"><span class="sec-num">&sect; 16</span><h2 class="sec-h">Строение атома. Ионы</h2></div><div id="p16-body"></div></section>
<section id="sec-p17" class="sec"><div class="sec-header"><span class="sec-num">&sect; 17</span><h2 class="sec-h">Электрическое поле. Электрическое напряжение</h2></div><div id="p17-body"></div></section>
<section id="sec-p18" class="sec"><div class="sec-header"><span class="sec-num">&sect; 18</span><h2 class="sec-h">Единица электрического напряжения. Расчёт работы в электрическом поле</h2></div><div id="p18-body"></div></section>
<section id="sec-p19" class="sec"><div class="sec-header"><span class="sec-num">&sect; 19</span><h2 class="sec-h">Электрический ток. Источники тока</h2></div><div id="p19-body"></div></section>
<section id="sec-p20" class="sec"><div class="sec-header"><span class="sec-num">&sect; 20</span><h2 class="sec-h">Сила и направление электрического тока</h2></div><div id="p20-body"></div></section>
<section id="sec-p21" class="sec"><div class="sec-header"><span class="sec-num">&sect; 21</span><h2 class="sec-h">Электрическая цепь. Измерение силы тока и напряжения</h2></div><div id="p21-body"></div></section>
<section id="sec-p22" class="sec"><div class="sec-header"><span class="sec-num">&sect; 22</span><h2 class="sec-h">Связь силы тока и напряжения. Закон Ома для участка электрической цепи</h2></div><div id="p22-body"></div></section>
<section id="sec-p23" class="sec"><div class="sec-header"><span class="sec-num">&sect; 23</span><h2 class="sec-h">Единица сопротивления. Расчёт сопротивления</h2></div><div id="p23-body"></div></section>
<section id="sec-p24" class="sec"><div class="sec-header"><span class="sec-num">&sect; 24</span><h2 class="sec-h">Последовательное соединение проводников. Реостат</h2></div><div id="p24-body"></div></section>
<section id="sec-p25" class="sec"><div class="sec-header"><span class="sec-num">&sect; 25</span><h2 class="sec-h">Параллельное соединение проводников</h2></div><div id="p25-body"></div></section>
<section id="sec-p26" class="sec"><div class="sec-header"><span class="sec-num">&sect; 26</span><h2 class="sec-h">Работа и мощность электрического тока. Закон Джоуля — Ленца</h2></div><div id="p26-body"></div></section>
<section id="sec-p27" class="sec"><div class="sec-header"><span class="sec-num">&sect; 27</span><h2 class="sec-h">Использование и экономия электроэнергии. Безопасность</h2></div><div id="p27-body"></div></section>
<section id="sec-p28" class="sec"><div class="sec-header"><span class="sec-num">&sect; 28</span><h2 class="sec-h">Постоянные магниты</h2></div><div id="p28-body"></div></section>
<section id="sec-p29" class="sec"><div class="sec-header"><span class="sec-num">&sect; 29</span><h2 class="sec-h">Магнитное поле</h2></div><div id="p29-body"></div></section>
<section id="sec-p30" class="sec"><div class="sec-header"><span class="sec-num">&sect; 30</span><h2 class="sec-h">Магнитное поле тока</h2></div><div id="p30-body"></div></section>
<section id="sec-p31" class="sec"><div class="sec-header"><span class="sec-num">&sect; 31</span><h2 class="sec-h">Магнитное поле прямого проводника и катушки с током. Электромагнит</h2></div><div id="p31-body"></div></section>
<section id="sec-final2" class="sec"><div class="sec-header"><span class="sec-num">&#9733;</span><h2 class="sec-h">Финал главы</h2></div><div id="final2-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">Интерактивный учебник «Физика 8» · Глава 2 · «Электромагнитные явления» · LearnSpace</footer>
<div id="ach-popup" class="ach-popup"><svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><polygon points="12,2 22,20 2,20"/></svg><span id="ach-text">Достижение!</span></div>
<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:'p12', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = 21;
const _TB_SLUG = 'physics-8-ch2';
const LS_PREFIX = 'physics8_ch2';
const LS_XP = 'physics8_xp';
const PARAS = [
{ id:'p12', num:'\u00a7 12', name:'Электризация тел. Взаимодействие зарядов', sub:'Два рода зарядов' },
{ id:'p13', num:'\u00a7 13', name:'Проводники и диэлектрики', sub:'Свободные носители' },
{ id:'p14', num:'\u00a7 14', name:'Электризация через влияние', sub:'Индукция' },
{ id:'p15', num:'\u00a7 15', name:'Электрический заряд. Элементарный заряд', sub:'$e = 1{,}6 \\cdot 10^{-19}$ Кл' },
{ id:'p16', num:'\u00a7 16', name:'Строение атома. Ионы', sub:'Ядро + электроны' },
{ id:'p17', num:'\u00a7 17', name:'Электрическое поле. Электрическое напряжение', sub:'$U$ как работа поля' },
{ id:'p18', num:'\u00a7 18', name:'Единица электрического напряжения. Расчёт работы в электрическом поле', sub:'$A = qU$' },
{ id:'p19', num:'\u00a7 19', name:'Электрический ток. Источники тока', sub:'Упорядоченное движение' },
{ id:'p20', num:'\u00a7 20', name:'Сила и направление электрического тока', sub:'$I = q/t$' },
{ id:'p21', num:'\u00a7 21', name:'Электрическая цепь. Измерение силы тока и напряжения', sub:'A — последов., V — паралл.' },
{ id:'p22', num:'\u00a7 22', name:'Связь силы тока и напряжения. Закон Ома для участка электрической цепи', sub:'$I = U/R$' },
{ id:'p23', num:'\u00a7 23', name:'Единица сопротивления. Расчёт сопротивления', sub:'$R = \\rho l/S$' },
{ id:'p24', num:'\u00a7 24', name:'Последовательное соединение проводников. Реостат', sub:'$R = R_1 + R_2$' },
{ id:'p25', num:'\u00a7 25', name:'Параллельное соединение проводников', sub:'$1/R = 1/R_1 + 1/R_2$' },
{ id:'p26', num:'\u00a7 26', name:'Работа и мощность электрического тока. Закон Джоуля — Ленца', sub:'$P = UI$, $Q = I^2 Rt$' },
{ id:'p27', num:'\u00a7 27', name:'Использование и экономия электроэнергии. Безопасность', sub:'кВт·ч, ТБ' },
{ id:'p28', num:'\u00a7 28', name:'Постоянные магниты', sub:'N, S, поле Земли' },
{ id:'p29', num:'\u00a7 29', name:'Магнитное поле', sub:'$\\vec{B}$, линии' },
{ id:'p30', num:'\u00a7 30', name:'Магнитное поле тока', sub:'Опыт Эрстеда' },
{ id:'p31', num:'\u00a7 31', name:'Магнитное поле прямого проводника и катушки с током. Электромагнит', sub:'Правило правой руки' },
{ id:'final2', num:'\u2605', name:'Финал главы', sub:'Итоги · 10 боссов', final:true }
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
const ACH_LABELS = {
start:"Начало главы 2!",
p12_done:"Электризация тел. Взаимодействие зарядов освоен!",
p13_done:"Проводники и диэлектрики освоен!",
p14_done:"Электризация через влияние освоен!",
p15_done:"Электрический заряд. Элементарный заряд освоен!",
p16_done:"Строение атома. Ионы освоен!",
p17_done:"Электрическое поле. Электрическое напряжение освоен!",
p18_done:"Единица электрического напряжения. Расчёт работы в электрическом поле освоен!",
p19_done:"Электрический ток. Источники тока освоен!",
p20_done:"Сила и направление электрического тока освоен!",
p21_done:"Электрическая цепь. Измерение силы тока и напряжения освоен!",
p22_done:"Связь силы тока и напряжения. Закон Ома для участка электрической цепи освоен!",
p23_done:"Единица сопротивления. Расчёт сопротивления освоен!",
p24_done:"Последовательное соединение проводников. Реостат освоен!",
p25_done:"Параллельное соединение проводников освоен!",
p26_done:"Работа и мощность электрического тока. Закон Джоуля — Ленца освоен!",
p27_done:"Использование и экономия электроэнергии. Безопасность освоен!",
p28_done:"Постоянные магниты освоен!",
p29_done:"Магнитное поле освоен!",
p30_done:"Магнитное поле тока освоен!",
p31_done:"Магнитное поле прямого проводника и катушки с током. Электромагнит освоен!",
ch2_done:"Глава 2 пройдена!"
};
const SIDEBARS = {
p12:{title:"Шпаргалка § 12",rows:[["В разработке","Phase 2 Wave 1"]]},
p13:{title:"Шпаргалка § 13",rows:[["В разработке","Phase 2 Wave 1"]]},
p14:{title:"Шпаргалка § 14",rows:[["В разработке","Phase 2 Wave 1"]]},
p15:{title:"Шпаргалка § 15",rows:[["В разработке","Phase 2 Wave 2"]]},
p16:{title:"Шпаргалка § 16",rows:[["В разработке","Phase 2 Wave 2"]]},
p17:{title:"Шпаргалка § 17",rows:[["В разработке","Phase 2 Wave 3"]]},
p18:{title:"Шпаргалка § 18",rows:[["В разработке","Phase 2 Wave 3"]]},
p19:{title:"Шпаргалка § 19",rows:[["В разработке","Phase 3 Wave 1"]]},
p20:{title:"Шпаргалка § 20",rows:[["В разработке","Phase 3 Wave 1"]]},
p21:{title:"Шпаргалка § 21",rows:[["В разработке","Phase 3 Wave 2"]]},
p22:{title:"Шпаргалка § 22",rows:[["В разработке","Phase 3 Wave 2"]]},
p23:{title:"Шпаргалка § 23",rows:[["В разработке","Phase 3 Wave 3"]]},
p24:{title:"Шпаргалка § 24",rows:[["В разработке","Phase 3 Wave 3"]]},
p25:{title:"Шпаргалка § 25",rows:[["В разработке","Phase 3 Wave 3"]]},
p26:{title:"Шпаргалка § 26",rows:[["В разработке","Phase 3 Wave 4"]]},
p27:{title:"Шпаргалка § 27",rows:[["В разработке","Phase 3 Wave 4"]]},
p28:{title:"Шпаргалка § 28",rows:[["В разработке","Phase 4 Wave 1"]]},
p29:{title:"Шпаргалка § 29",rows:[["В разработке","Phase 4 Wave 1"]]},
p30:{title:"Шпаргалка § 30",rows:[["В разработке","Phase 4 Wave 2"]]},
p31:{title:"Шпаргалка § 31",rows:[["В разработке","Phase 4 Wave 2"]]},
final2:{title:"Шпаргалка ★",rows:[["В разработке","Phase 4 Wave 2"]]}
};
const TIPS=[
{sec:'p12',html:"Параграф § 12 будет реализован в Phase 2 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p13',html:"Параграф § 13 будет реализован в Phase 2 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p14',html:"Параграф § 14 будет реализован в Phase 2 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p15',html:"Параграф § 15 будет реализован в Phase 2 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p16',html:"Параграф § 16 будет реализован в Phase 2 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p17',html:"Параграф § 17 будет реализован в Phase 2 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p18',html:"Параграф § 18 будет реализован в Phase 2 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p19',html:"Параграф § 19 будет реализован в Phase 3 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p20',html:"Параграф § 20 будет реализован в Phase 3 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p21',html:"Параграф § 21 будет реализован в Phase 3 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p22',html:"Параграф § 22 будет реализован в Phase 3 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p23',html:"Параграф § 23 будет реализован в Phase 3 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p24',html:"Параграф § 24 будет реализован в Phase 3 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p25',html:"Параграф § 25 будет реализован в Phase 3 Wave 3. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p26',html:"Параграф § 26 будет реализован в Phase 3 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p27',html:"Параграф § 27 будет реализован в Phase 3 Wave 4. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p28',html:"Параграф § 28 будет реализован в Phase 4 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p29',html:"Параграф § 29 будет реализован в Phase 4 Wave 1. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p30',html:"Параграф § 30 будет реализован в Phase 4 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'p31',html:"Параграф § 31 будет реализован в Phase 4 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."},
{sec:'final2',html:"Параграф ★ будет реализован в Phase 4 Wave 2. Используем хелперы из <code>phys.js</code> и <code>optics.js</code>."}
];
const BUILDERS = {
p12: ()=>{ const box=document.getElementById('p12-body'); box.innerHTML = buildStub('p12', 'Электризация тел. Взаимодействие зарядов', 'Phase 2 Wave 1') + secNavFor('p12') + readButton('p12'); renderMath(box); wireReadBtn('p12'); },
p13: ()=>{ const box=document.getElementById('p13-body'); box.innerHTML = buildStub('p13', 'Проводники и диэлектрики', 'Phase 2 Wave 1') + secNavFor('p13') + readButton('p13'); renderMath(box); wireReadBtn('p13'); },
p14: ()=>{ const box=document.getElementById('p14-body'); box.innerHTML = buildStub('p14', 'Электризация через влияние', 'Phase 2 Wave 1') + secNavFor('p14') + readButton('p14'); renderMath(box); wireReadBtn('p14'); },
p15: ()=>{ const box=document.getElementById('p15-body'); box.innerHTML = buildStub('p15', 'Электрический заряд. Элементарный заряд', 'Phase 2 Wave 2') + secNavFor('p15') + readButton('p15'); renderMath(box); wireReadBtn('p15'); },
p16: ()=>{ const box=document.getElementById('p16-body'); box.innerHTML = buildStub('p16', 'Строение атома. Ионы', 'Phase 2 Wave 2') + secNavFor('p16') + readButton('p16'); renderMath(box); wireReadBtn('p16'); },
p17: ()=>{ const box=document.getElementById('p17-body'); box.innerHTML = buildStub('p17', 'Электрическое поле. Электрическое напряжение', 'Phase 2 Wave 3') + secNavFor('p17') + readButton('p17'); renderMath(box); wireReadBtn('p17'); },
p18: ()=>{ const box=document.getElementById('p18-body'); box.innerHTML = buildStub('p18', 'Единица электрического напряжения. Расчёт работы в электрическом поле', 'Phase 2 Wave 3') + secNavFor('p18') + readButton('p18'); renderMath(box); wireReadBtn('p18'); },
p19: ()=>{ const box=document.getElementById('p19-body'); box.innerHTML = buildStub('p19', 'Электрический ток. Источники тока', 'Phase 3 Wave 1') + secNavFor('p19') + readButton('p19'); renderMath(box); wireReadBtn('p19'); },
p20: ()=>{ const box=document.getElementById('p20-body'); box.innerHTML = buildStub('p20', 'Сила и направление электрического тока', 'Phase 3 Wave 1') + secNavFor('p20') + readButton('p20'); renderMath(box); wireReadBtn('p20'); },
p21: ()=>{ const box=document.getElementById('p21-body'); box.innerHTML = buildStub('p21', 'Электрическая цепь. Измерение силы тока и напряжения', 'Phase 3 Wave 2') + secNavFor('p21') + readButton('p21'); renderMath(box); wireReadBtn('p21'); },
p22: ()=>{ const box=document.getElementById('p22-body'); box.innerHTML = buildStub('p22', 'Связь силы тока и напряжения. Закон Ома для участка электрической цепи', 'Phase 3 Wave 2') + secNavFor('p22') + readButton('p22'); renderMath(box); wireReadBtn('p22'); },
p23: ()=>{ const box=document.getElementById('p23-body'); box.innerHTML = buildStub('p23', 'Единица сопротивления. Расчёт сопротивления', 'Phase 3 Wave 3') + secNavFor('p23') + readButton('p23'); renderMath(box); wireReadBtn('p23'); },
p24: ()=>{ const box=document.getElementById('p24-body'); box.innerHTML = buildStub('p24', 'Последовательное соединение проводников. Реостат', 'Phase 3 Wave 3') + secNavFor('p24') + readButton('p24'); renderMath(box); wireReadBtn('p24'); },
p25: ()=>{ const box=document.getElementById('p25-body'); box.innerHTML = buildStub('p25', 'Параллельное соединение проводников', 'Phase 3 Wave 3') + secNavFor('p25') + readButton('p25'); renderMath(box); wireReadBtn('p25'); },
p26: ()=>{ const box=document.getElementById('p26-body'); box.innerHTML = buildStub('p26', 'Работа и мощность электрического тока. Закон Джоуля — Ленца', 'Phase 3 Wave 4') + secNavFor('p26') + readButton('p26'); renderMath(box); wireReadBtn('p26'); },
p27: ()=>{ const box=document.getElementById('p27-body'); box.innerHTML = buildStub('p27', 'Использование и экономия электроэнергии. Безопасность', 'Phase 3 Wave 4') + secNavFor('p27') + readButton('p27'); renderMath(box); wireReadBtn('p27'); },
p28: ()=>{ const box=document.getElementById('p28-body'); box.innerHTML = buildStub('p28', 'Постоянные магниты', 'Phase 4 Wave 1') + secNavFor('p28') + readButton('p28'); renderMath(box); wireReadBtn('p28'); },
p29: ()=>{ const box=document.getElementById('p29-body'); box.innerHTML = buildStub('p29', 'Магнитное поле', 'Phase 4 Wave 1') + secNavFor('p29') + readButton('p29'); renderMath(box); wireReadBtn('p29'); },
p30: ()=>{ const box=document.getElementById('p30-body'); box.innerHTML = buildStub('p30', 'Магнитное поле тока', 'Phase 4 Wave 2') + secNavFor('p30') + readButton('p30'); renderMath(box); wireReadBtn('p30'); },
p31: ()=>{ const box=document.getElementById('p31-body'); box.innerHTML = buildStub('p31', 'Магнитное поле прямого проводника и катушки с током. Электромагнит', 'Phase 4 Wave 2') + secNavFor('p31') + readButton('p31'); renderMath(box); wireReadBtn('p31'); },
final2: ()=>{ const box=document.getElementById('final2-body'); box.innerHTML = buildStub('final2', 'Финал главы', 'Phase 4 Wave 2') + secNavFor('final2') + readButton('final2'); renderMath(box); wireReadBtn('final2'); }
};
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
function loadProgress(){
try{
const s=localStorage.getItem(LS_PREFIX+'_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem(LS_PREFIX+'_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(LS_XP)||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem(LS_PREFIX+'_progress', JSON.stringify(STATE.progress));
localStorage.setItem(LS_PREFIX+'_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem(LS_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, LS_PREFIX+'-'+(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);
}
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();
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);
}
function buildSidebar(id){
const box=document.getElementById('sidebar-content');
const sb=SIDEBARS[id]||SIDEBARS[PARAS[0].id];
let html='';
const xpForLv=_xpForLevel(STATE.level), xpNext=_xpForLevel(STATE.level+1);
const xpInLv=STATE.xp-xpForLv, xpRange=xpNext-xpForLv;
const xpPct=xpRange>0?Math.round(xpInLv/xpRange*100):100;
html+='<div class="xp-card" data-gamified><div class="xp-card-title" data-gamified><span>XP-прогресс</span><span class="xp-level">Ур. '+STATE.level+'</span></div><div class="xp-bar"><div class="xp-fill" style="width:'+xpPct+'%"></div></div><div class="xp-nums"><span>'+STATE.xp+' XP</span><span>'+xpNext+' XP</span></div></div>';
html+='<div class="sidecard"><h4>'+sb.title+'</h4>';
sb.rows.forEach(([k,v])=>{ html+='<div class="sidecard-row"><b>'+k+'</b>'+(v?' \u2014 '+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(LS_PREFIX+'_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(LS_PREFIX+'_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(); }
function ipow(base, exp){ let r=1; for(let i=0;i<Math.abs(exp);i++) r*=base; return exp<0 ? 1/r : r; }
function gcd(a,b){ a=Math.abs(a|0); b=Math.abs(b|0); while(b){ const t=b; b=a%b; a=t; } return a||1; }
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>';
}
/* === SVG-хелперы === */
function axes2D(W, H, pad, xmin, xmax, ymin, ymax){
const ux = (W - 2*pad) / (xmax - xmin);
const uy = (H - 2*pad) / (ymax - ymin);
const toX = v => pad + (v - xmin) * ux;
const toY = v => H - pad - (v - ymin) * uy;
let g = '';
g += '<g stroke="#e5e7eb" stroke-width="1">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
g += '<line x1="'+toX(x)+'" y1="'+pad+'" x2="'+toX(x)+'" y2="'+(H-pad)+'"/>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
g += '<line x1="'+pad+'" y1="'+toY(y)+'" x2="'+(W-pad)+'" y2="'+toY(y)+'"/>';
}
g += '</g>';
const y0 = toY(0), x0 = toX(0);
g += '<line x1="'+pad+'" y1="'+y0+'" x2="'+(W-pad)+'" y2="'+y0+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<line x1="'+x0+'" y1="'+pad+'" x2="'+x0+'" y2="'+(H-pad)+'" stroke="#0f172a" stroke-width="1.5"/>';
g += '<text x="'+(W-pad+2)+'" y="'+(y0-4)+'" font-size="11" fill="#0f172a">x</text>';
g += '<text x="'+(x0+4)+'" y="'+(pad-2)+'" font-size="11" fill="#0f172a">y</text>';
g += '<g font-size="10" fill="#64748b">';
for (let x = Math.ceil(xmin); x <= xmax; x++){
if (x !== 0) g += '<text x="'+(toX(x)-3)+'" y="'+(y0+12)+'">'+x+'</text>';
}
for (let y = Math.ceil(ymin); y <= ymax; y++){
if (y !== 0) g += '<text x="'+(x0+4)+'" y="'+(toY(y)+3)+'">'+y+'</text>';
}
g += '<text x="'+(x0+4)+'" y="'+(y0+12)+'">0</text>';
g += '</g>';
return { content: g, toX, toY, ux, uy };
}
function plotFunc(f, xmin, xmax, toX, toY, color, N){
N = N || 200;
let d = '';
let prevValid = false;
for (let i = 0; i <= N; i++){
const x = xmin + (xmax - xmin) * i / N;
let y;
try { y = f(x); } catch(e){ y = NaN; }
if (!isFinite(y) || isNaN(y) || y < -1e4 || y > 1e4){ prevValid = false; continue; }
d += (prevValid ? ' L' : ' M') + toX(x).toFixed(2) + ',' + toY(y).toFixed(2);
prevValid = true;
}
return '<path d="'+d+'" stroke="'+color+'" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>';
}
function pointWithDrop(x, fx, toX, toY, color, label){
const px = toX(x), py = toY(fx);
let s = '';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+px+'" y2="'+toY(0)+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<line x1="'+px+'" y1="'+py+'" x2="'+toX(0)+'" y2="'+py+'" stroke="'+color+'" stroke-width="1.2" stroke-dasharray="3 3" opacity=".7"/>';
s += '<circle cx="'+px+'" cy="'+py+'" r="4.5" fill="'+color+'" stroke="#fff" stroke-width="2"/>';
if (label){
s += '<text x="'+(px+8)+'" y="'+(py-8)+'" font-family="Inter,sans-serif" font-size="12" font-weight="700" fill="'+color+'">'+label+'</text>';
}
return s;
}
function asymptote(orientation, value, toX, toY, xmin, xmax, ymin, ymax, color){
color = color || '#94a3b8';
if (orientation === 'h'){
const y = toY(value);
return '<line x1="'+toX(xmin)+'" y1="'+y+'" x2="'+toX(xmax)+'" y2="'+y+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
} else {
const x = toX(value);
return '<line x1="'+x+'" y1="'+toY(ymin)+'" x2="'+x+'" y2="'+toY(ymax)+'" stroke="'+color+'" stroke-width="1.3" stroke-dasharray="6 4"/>';
}
}
function snapToValue(value, snapPoints, tolerance){
tolerance = tolerance || 0.1;
for (const sp of snapPoints){
if (Math.abs(value - sp) < tolerance) return sp;
}
return value;
}
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; }
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 secNavFor(curId){
const idx = PARAS.findIndex(p => p.id === curId);
const prev = idx > 0 ? PARAS[idx-1].id : null;
const next = idx < PARAS.length - 1 ? PARAS[idx+1].id : null;
return secNav(prev, next);
}
function secNav(prev, next){
function lbl(id){ if(!id) return ''; const p=PARAS.find(x=>x.id===id); return p?p.num:id; }
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> '+lbl(prev)+'</button>':'<span></span>';
h+=next?'<button class="btn primary" onclick="goTo(\''+next+'\')">'+lbl(next)+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
h+='</div>'; return h;
}
function readButton(paraId){
const p = PARAS.find(x => x.id === paraId);
const labelTail = p && p.final ? 'финал' : (p ? p.num : '?');
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>'
+' Я прочитал \u2014 '+labelTail+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
const aId = paraId+'_done';
if(ACH_LABELS[aId]) achievement(aId);
});
}
function setupSorter(cfg){
const placed = {}; const pool = document.getElementById(cfg.poolId); const scope = document.querySelector(cfg.scopeSelector);
if(!pool||!scope) return {placed,render:()=>{},reset:()=>{}};
pool.classList.add('dnd-pool'); if(cfg.columnLayout) pool.classList.add('col');
let armed = null;
function buildChip(it,isPlaced){ const e=document.createElement('div'); e.className='dnd-chip'+(isPlaced?' placed':''); e.dataset.id=it.id; e.innerHTML='<span class="dnd-txt">'+it.html+'</span><span class="dnd-x" title="Убрать">\xd7</span>'; attach(e,it.id); return e; }
function attach(elm,itId){ let ghost=null,dragging=false,sx=0,sy=0; elm.addEventListener('pointerdown',ev=>{ if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault(); if(ev.target.classList&&ev.target.classList.contains('dnd-x')){ ev.stopPropagation(); if(placed[itId]){delete placed[itId];render();}else if(armed===itId){armed=null;render();} return; } sx=ev.clientX;sy=ev.clientY; const r=elm.getBoundingClientRect(); const ox=ev.clientX-r.left,oy=ev.clientY-r.top; try{elm.setPointerCapture(ev.pointerId);}catch(e){} function onMove(e){ const dx=e.clientX-sx,dy=e.clientY-sy; if(!dragging&&Math.hypot(dx,dy)>8){ dragging=true; ghost=elm.cloneNode(true); ghost.classList.remove('armed'); ghost.style.cssText='position:fixed;z-index:9999;pointer-events:none;opacity:.9;transform:rotate(-2.5deg);box-shadow:0 14px 36px rgba(0,0,0,.32);width:'+r.width+'px;left:'+(e.clientX-ox)+'px;top:'+(e.clientY-oy)+'px'; document.body.appendChild(ghost); elm.classList.add('dragging'); } if(dragging&&ghost){ ghost.style.left=(e.clientX-ox)+'px';ghost.style.top=(e.clientY-oy)+'px'; const under=document.elementsFromPoint(e.clientX,e.clientY); scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); const tgt=under.find(n=>n.classList&&(n.classList.contains('drop-box')||n.classList.contains('dnd-pool'))); if(tgt)tgt.classList.add('over'); } } function onUp(e){ elm.removeEventListener('pointermove',onMove);elm.removeEventListener('pointerup',onUp);elm.removeEventListener('pointercancel',onUp);elm.classList.remove('dragging'); if(ghost){ghost.remove();ghost=null;} scope.querySelectorAll('.drop-box.over,.dnd-pool.over').forEach(n=>n.classList.remove('over')); if(dragging){ const under=document.elementsFromPoint(e.clientX,e.clientY); const box=under.find(n=>n.classList&&n.classList.contains('drop-box')); const pl=under.find(n=>n.classList&&n.classList.contains('dnd-pool')); if(box){const di=box.querySelector('[data-cat]');if(di){placed[itId]=di.dataset.cat;armed=null;render();return;}}else if(pl){delete placed[itId];armed=null;render();return;} }else{ if(placed[itId]){delete placed[itId];armed=null;render();}else{armed=(armed===itId)?null:itId;render();} } dragging=false; } elm.addEventListener('pointermove',onMove);elm.addEventListener('pointerup',onUp);elm.addEventListener('pointercancel',onUp); }); }
function attachBoxTaps(){ scope.querySelectorAll('.drop-box').forEach(box=>{ box.addEventListener('click',ev=>{ if(!armed)return; if(ev.target.closest('.dnd-chip'))return; const di=box.querySelector('[data-cat]'); if(di){placed[armed]=di.dataset.cat;armed=null;render();} }); }); }
function render(){ pool.innerHTML=''; cfg.items.forEach(it=>{if(placed[it.id])return;const c=buildChip(it,false);if(armed===it.id)c.classList.add('armed');pool.appendChild(c);}); cfg.cats.forEach(cat=>{const box=scope.querySelector('.drop-items[data-cat="'+cat+'"]');if(!box)return;box.innerHTML='';cfg.items.forEach(it=>{if(placed[it.id]!==cat)return;box.appendChild(buildChip(it,true));});}); if(window.renderMathInElement)try{renderMath(scope);}catch(_){} }
attachBoxTaps(); render();
return {placed,render,reset(){ for(const k in placed)delete placed[k];armed=null;render(); }};
}
function buildStub(id, name, phase){
return '<div class="card" style="background:linear-gradient(135deg,var(--sec-acc-soft),var(--card));border:1.5px dashed var(--sec-acc)">'
+ '<div class="card-header"><div class="card-icon theory">'+ICONS.theory+'</div><div class="card-title">В разработке</div></div>'
+ '<div class="card-body"><p>Контент <b>'+name+'</b> будет реализован в <b>'+phase+'</b> по плану <code>PLAN_PHYSICS_8.md</code>.</p>'
+ '<p style="margin-top:8px;color:var(--muted);font-size:.9rem">Phase 0 \u2014 это каркас (skeleton). Все 4 интерактива, 3 теоретические карточки и тренажёр задач будут добавлены в волне.</p>'
+ '</div></div>';
}
/* ===== 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)+'\u2026':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(PARAS[0].id);
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);
</script>
</body>
</html>