#!/usr/bin/env node
// Генератор скелетов глав Физики 7. Создаёт physics_7_ch1..ch5.html из единого шаблона.
// Phase 0: скелет с инфраструктурой (header, navigator, sidebar, KaTeX, прогресс/XP, goTo),
// без §-контента — наполняется в Phase 1+.

const fs = require('fs');
const path = require('path');

const VER = '20260530';
const OUT = path.join(__dirname, '..', '..', 'frontend', 'textbooks');

const CHAPTERS = [
  {
    n: 1, slug: 'physics-7-ch1',
    title: 'Физические методы познания природы',
    range: '§§1–7',
    accent: '#4f46e5', accentD: '#3730a3', accentSoft: '#e0e7ff',
    coverGrad: 'linear-gradient(135deg,#312e81,#4f46e5 60%,#a5b4fc)',
    paras: [
      { id:'p1', num:'§ 1', title:'Физика — наука о природе', wm:'?' },
      { id:'p2', num:'§ 2', title:'Тело, явление, величина', wm:'×' },
      { id:'p3', num:'§ 3', title:'Методы исследования в физике', wm:'⚙' },
      { id:'p4', num:'§ 4', title:'Прямые и косвенные измерения', wm:'=' },
      { id:'p5', num:'§ 5', title:'Единицы измерения. СИ', wm:'м' },
      { id:'p6', num:'§ 6', title:'Действия над физическими величинами', wm:'±' },
      { id:'p7', num:'§ 7', title:'Цена деления. Погрешность', wm:'∇' },
      { id:'final1', num:'Финал', title:'Итоги главы 1', wm:'★' },
    ],
    achTitle: 'Юный физик',
  },
  {
    n: 2, slug: 'physics-7-ch2',
    title: 'Строение вещества',
    range: '§§8–13',
    accent: '#7c3aed', accentD: '#5b21b6', accentSoft: '#ede9fe',
    coverGrad: 'linear-gradient(135deg,#4c1d95,#7c3aed 60%,#c4b5fd)',
    paras: [
      { id:'p8',  num:'§ 8',  title:'Дискретное строение вещества', wm:'•' },
      { id:'p9',  num:'§ 9',  title:'Тепловое движение частиц',     wm:'~' },
      { id:'p10', num:'§ 10', title:'Взаимодействие частиц',         wm:'⇌' },
      { id:'p11', num:'§ 11', title:'Газ, жидкость, твёрдое',        wm:'△' },
      { id:'p12', num:'§ 12', title:'Тепловое расширение',           wm:'↔' },
      { id:'p13', num:'§ 13', title:'Температура. Термометры',       wm:'°' },
      { id:'final2', num:'Финал', title:'Итоги главы 2', wm:'★' },
    ],
    achTitle: 'Знаток вещества',
  },
  {
    n: 3, slug: 'physics-7-ch3',
    title: 'Движение и силы',
    range: '§§14–27',
    accent: '#dc2626', accentD: '#991b1b', accentSoft: '#fee2e2',
    coverGrad: 'linear-gradient(135deg,#7f1d1d,#dc2626 60%,#f87171)',
    paras: [
      { id:'p14', num:'§ 14', title:'Механическое движение. Относительность', wm:'→' },
      { id:'p15', num:'§ 15', title:'Траектория, путь, время',                wm:'s' },
      { id:'p16', num:'§ 16', title:'Равномерное движение. Скорость',         wm:'v' },
      { id:'p17', num:'§ 17', title:'Графики s(t) и v(t)',                    wm:'∠' },
      { id:'p18', num:'§ 18', title:'Средняя скорость',                       wm:'⟨⟩' },
      { id:'p19', num:'§ 19', title:'Инерция',                                wm:'∞' },
      { id:'p20', num:'§ 20', title:'Масса. Плотность',                       wm:'ρ' },
      { id:'p21', num:'§ 21', title:'Сила',                                   wm:'F' },
      { id:'p22', num:'§ 22', title:'Сила тяжести',                           wm:'↓' },
      { id:'p23', num:'§ 23', title:'Сила упругости',                         wm:'≈' },
      { id:'p24', num:'§ 24', title:'Вес тела',                               wm:'P' },
      { id:'p25', num:'§ 25', title:'Динамометр',                             wm:'⊥' },
      { id:'p26', num:'§ 26', title:'Сложение сил',                           wm:'+' },
      { id:'p27', num:'§ 27', title:'Сила трения',                            wm:'~' },
      { id:'final3', num:'Финал', title:'Итоги главы 3', wm:'★' },
    ],
    achTitle: 'Мастер движения',
  },
  {
    n: 4, slug: 'physics-7-ch4',
    title: 'Давление',
    range: '§§28–35',
    accent: '#d97706', accentD: '#92400e', accentSoft: '#fef3c7',
    coverGrad: 'linear-gradient(135deg,#78350f,#d97706 60%,#fbbf24)',
    paras: [
      { id:'p28', num:'§ 28', title:'Давление. Единицы давления', wm:'p' },
      { id:'p29', num:'§ 29', title:'Давление газа',              wm:'∴' },
      { id:'p30', num:'§ 30', title:'Закон Паскаля',              wm:'⊕' },
      { id:'p31', num:'§ 31', title:'Гидростатическое давление',  wm:'≡' },
      { id:'p32', num:'§ 32', title:'Сообщающиеся сосуды',        wm:'U' },
      { id:'p33', num:'§ 33', title:'Газы и их вес',              wm:'⌒' },
      { id:'p34', num:'§ 34', title:'Атмосферное давление',       wm:'∼' },
      { id:'p35', num:'§ 35', title:'Барометры и манометры',      wm:'⏚' },
      { id:'final4', num:'Финал', title:'Итоги главы 4', wm:'★' },
    ],
    achTitle: 'Властелин давления',
  },
  {
    n: 5, slug: 'physics-7-ch5',
    title: 'Работа. Мощность. Энергия',
    range: '§§36–42',
    accent: '#10b981', accentD: '#047857', accentSoft: '#d1fae5',
    coverGrad: 'linear-gradient(135deg,#064e3b,#10b981 60%,#6ee7b7)',
    paras: [
      { id:'p36', num:'§ 36', title:'Механическая работа', wm:'A' },
      { id:'p37', num:'§ 37', title:'КПД',                 wm:'η' },
      { id:'p38', num:'§ 38', title:'Мощность',            wm:'P' },
      { id:'p39', num:'§ 39', title:'Кинетическая энергия',wm:'Eк' },
      { id:'p40', num:'§ 40', title:'Потенциальная энергия',wm:'Eп' },
      { id:'p41', num:'§ 41', title:'Расчёт Eп = mgh',     wm:'h' },
      { id:'p42', num:'§ 42', title:'Закон сохранения энергии',wm:'∑' },
      { id:'final5', num:'Финал', title:'Итоги главы 5', wm:'★' },
    ],
    achTitle: 'Энергетик',
  },
];

function makeHTML(C) {
  const parasJs = C.paras.map(p => `{id:'${p.id}',num:'${p.num}',title:${JSON.stringify(p.title)},wm:'${p.wm}'}`).join(',');
  const sections = C.paras.map(p =>
    `    <section id="sec-${p.id}" class="sec" data-watermark="${p.wm}">
      <div class="sec-header"><span class="sec-num">${p.num}</span><h2 class="sec-h">${p.title}</h2></div>
      <div id="${p.id}-body"><div class="placeholder">Содержимое параграфа появится в одной из ближайших фаз разработки.</div></div>
    </section>`).join('\n');

  return `<!doctype html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title>Физика 7 · Глава ${C.n} · ${C.title}</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">
<link rel="stylesheet" href="/css/phys-textbook-widgets.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/phys.js?v=${VER}" defer></script>
<script src="/js/phys7_ch${C.n}_widgets.js?v=${VER}" 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:#f0f9ff; --card:#fff; --card-soft:#f8fafc; --text:#0f172a; --muted:#475569;
  --border:#bae6fd; --pri:#0284c7; --pri2:#0c4a6e; --pri-soft:#e0f2fe;
  --acc:${C.accent}; --acc-d:${C.accentD}; --acc-soft:${C.accentSoft};
  --ok:#10b981; --ok-bg:#d1fae5; --fail:#dc2626; --fail-bg:#fee2e2; --warn:#f59e0b; --warn-bg:#fef3c7;
  --sh:0 4px 16px rgba(2,132,199,.08); --sh-h:0 12px 36px rgba(2,132,199,.16);
}
html.dark{--bg:#0c1e2e;--card:#0e2436;--card-soft:#0b1a28;--text:#e0f2fe;--muted:#7dd3fc;--border:#1e3a5f;--pri-soft:rgba(2,132,199,.18)}
*{margin:0;padding:0;box-sizing:border-box}
html,body{min-height:100vh}
body{font-family:'Inter',system-ui,sans-serif;background:var(--bg);color:var(--text);line-height:1.6;transition:background .25s,color .25s}

.hdr{position:relative;background:${C.coverGrad};color:#fff;padding:24px 22px 22px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.18)}
.hdr-inner{position:relative;z-index:1;max-width:1240px;margin:0 auto;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;letter-spacing:-.01em}
.hdr-sub{font-size:.88rem;opacity:.9;margin-top:3px}
.hdr-side{margin-left:auto;display:flex;gap:8px;flex-wrap:wrap}
.hdr-btn{padding:8px 12px;background:rgba(255,255,255,.16);border:none;color:#fff;border-radius:9px;cursor:pointer;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;font-family:inherit;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.26)}
.ic{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

.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}
.psel{background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:16px;margin-bottom:18px;box-shadow:var(--sh)}
.psel-head{font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:10px}
.psel-card{padding:12px;background:var(--card-soft);border:1.5px solid var(--border);border-radius:10px;cursor:pointer;transition:transform .15s,border-color .15s,box-shadow .15s;text-align:left}
.psel-card:hover{border-color:var(--acc);transform:translateY(-2px);box-shadow:0 4px 14px rgba(0,0,0,.06)}
.psel-card.active{border-color:var(--acc);background:var(--acc-soft)}
.psel-num{font-size:.7rem;font-weight:800;color:var(--acc-d);letter-spacing:.04em;text-transform:uppercase;margin-bottom:3px}
.psel-title{font-size:.86rem;font-weight:700;line-height:1.35}
.psel-prog{height:4px;background:rgba(0,0,0,.07);border-radius:3px;overflow:hidden;margin-top:7px}
.psel-prog-fill{height:100%;background:linear-gradient(90deg,var(--acc),var(--acc-d));border-radius:3px;transition:width .4s}

.sec{display:none;background:var(--card);border:1.5px solid var(--border);border-radius:14px;padding:22px;box-shadow:var(--sh);position:relative}
.sec.active{display:block}
.sec[data-watermark]::before{content:attr(data-watermark);position:absolute;right:18px;top:-12px;font-family:'Unbounded',sans-serif;font-size:5.2rem;font-weight:900;color:var(--acc-soft);pointer-events:none;line-height:1;user-select:none}
.sec-header{display:flex;align-items:baseline;gap:14px;margin-bottom:18px;padding-bottom:14px;border-bottom:1.5px solid var(--border);position:relative;z-index:1}
.sec-num{background:linear-gradient(135deg,var(--acc),var(--acc-d));color:#fff;padding:5px 12px;border-radius:9px;font-family:'Unbounded',sans-serif;font-weight:800;font-size:.86rem;letter-spacing:.04em}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.35rem;font-weight:800;color:var(--text)}
.placeholder{padding:32px 20px;text-align:center;color:var(--muted);font-size:.95rem;background:var(--card-soft);border:1.5px dashed var(--border);border-radius:10px}

.col-side{position:sticky;top:14px;align-self:start;height:fit-content;max-height:calc(100vh - 28px);overflow-y:auto}
.sidecard{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;margin-bottom:14px;box-shadow:var(--sh)}
.sidecard h4{font-family:'Unbounded',sans-serif;font-size:.74rem;font-weight:800;color:var(--pri2);text-transform:uppercase;letter-spacing:.07em;margin-bottom:10px;padding-bottom:8px;border-bottom:1px solid var(--border)}
.sidecard-row{margin-bottom:8px;font-size:.86rem;line-height:1.6}
.sidecard-row b{color:var(--pri);font-weight:700}
@media(max-width:980px){.col-side{position:static;max-height:none}}

.xp-card{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft));border:1.5px solid var(--acc);border-radius:12px;padding:14px;margin-bottom:14px}
.xp-card-title{font-size:.68rem;font-weight:800;color:var(--acc-d);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(--acc-d);font-family:'Unbounded',sans-serif}
.xp-bar{height:9px;background:rgba(245,158,11,.15);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}

.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}
}

.ach-popup{position:fixed;top:80px;right:18px;background:linear-gradient(135deg,var(--acc-d),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,.25);z-index:1002;display:none;align-items:center;gap:8px;max-width:340px}
.ach-popup.show{display:flex}

.foot{text-align:center;padding:30px 16px;color:var(--muted);font-size:.78rem;border-top:1px solid var(--border);margin-top:30px}

/* Search modal */
.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:80px}
.search-modal.show{display:flex}
.search-box{background:var(--card);border-radius:14px;width:520px;max-width:92vw;padding:14px;box-shadow:0 24px 64px rgba(0,0,0,.35);border:1.5px solid var(--border)}
.search-inp{width:100%;padding:11px 14px;background:var(--card-soft);border:1.5px solid var(--border);border-radius:9px;color:var(--text);font-size:.95rem;font-family:inherit;outline:0}
.search-inp:focus{border-color:var(--acc)}
.search-list{margin-top:12px;max-height:320px;overflow-y:auto}
.search-item{padding:10px 12px;border-radius:9px;cursor:pointer;border:1px solid transparent;font-size:.9rem}
.search-item:hover,.search-item.cur{background:var(--acc-soft);border-color:var(--acc)}
.search-item .num{display:inline-block;padding:2px 8px;background:var(--acc);color:#fff;border-radius:99px;font-size:.7rem;font-weight:700;margin-right:8px}
</style>
</head>
<body>

<header class="hdr">
  <div class="hdr-inner">
    <div>
      <a href="/textbook/physics-7" class="hdr-btn"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> К физике 7</a>
    </div>
    <div>
      <h1>Физика 7 &middot; Глава ${C.n}</h1>
      <div class="hdr-sub">${C.title} &middot; ${C.range}</div>
    </div>
    <div class="hdr-side">
      <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">
    <div class="psel">
      <div class="psel-head">Параграфы главы ${C.n}</div>
      <div class="psel-grid" id="psel-grid"></div>
    </div>

${sections}
  </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>

<div class="ach-popup" id="ach-popup"><svg class="ic" viewBox="0 0 24 24"><polygon points="12,2 15,9 22,9.3 17,14 18.5,21 12,17 5.5,21 7,14 2,9.3 9,9"/></svg><span id="ach-text"></span></div>

<div class="search-modal" id="search-modal"><div class="search-box">
  <input type="text" class="search-inp" id="search-inp" placeholder="Поиск по параграфам... (Esc — закрыть, Ctrl+K)">
  <div class="search-list" id="search-list"></div>
</div></div>

<footer class="foot">Интерактивный учебник «Физика 7 класс» &middot; Глава ${C.n} &middot; LearnSpace</footer>

<script>
'use strict';

const LS_PREFIX = 'physics7_ch${C.n}';
const _TB_SLUG = '${C.slug}';

const PARAS = [${parasJs}];
const TOTAL_PARAS = PARAS.length;

const SIDEBARS = {};
PARAS.forEach(p => { SIDEBARS[p.id] = { title: 'Шпаргалка ' + p.num, rows: [['В разработке','контент появится с волной соответствующего §']] }; });
const TIPS = [{ sec: PARAS[0].id, html: 'Скелет главы готов. Контент параграфов выйдет в одной из ближайших фаз.' }];
const ACH_LABELS = { start: 'Начало главы ${C.n}', ch_done: '${C.achTitle}' };

const STATE = { current: null, progress: {}, xp: 0, level: 1, achievements: new Map(), _built: new Set() };

function _xpForLevel(lv){ return Math.round(100 * Math.pow(lv-1, 1.6)); }
function calcLevel(xp){ let lv = 1; while(_xpForLevel(lv+1) <= xp) lv++; return lv; }

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(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('physics7_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('physics7_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] >= 100 && key === PARAS[PARAS.length-1].id) achievement('ch_done', '${C.achTitle}');
}
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, 'physics7-ch${C.n}-' + (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 achievement(id, label){
  if(STATE.achievements.has(id)) return;
  STATE.achievements.set(id, label || ACH_LABELS[id] || id);
  saveProgress();
  const pop = document.getElementById('ach-popup');
  if(pop){ document.getElementById('ach-text').textContent = 'Ачивка: ' + (label || ACH_LABELS[id] || id); pop.classList.add('show'); setTimeout(()=>pop.classList.remove('show'), 3000); }
  addXp(20, 'ach-' + id);
}

function refreshProgressUI(){
  document.querySelectorAll('[data-prog-card]').forEach(el => {
    const k = el.dataset.progCard;
    const fl = el.querySelector('.psel-prog-fill');
    if(fl) fl.style.width = (STATE.progress[k]||0) + '%';
  });
  if(STATE.current && document.getElementById('sidebar-content')){ try{ buildSidebar(STATE.current); }catch(e){} }
}

function buildParaSelector(){
  const grid = document.getElementById('psel-grid');
  if(!grid) return;
  grid.innerHTML = PARAS.map(p =>
    '<button class="psel-card" data-id="' + p.id + '" data-prog-card="' + p.id + '">'
    + '<div class="psel-num">' + p.num + '</div>'
    + '<div class="psel-title">' + p.title + '</div>'
    + '<div class="psel-prog"><div class="psel-prog-fill" style="width:' + (STATE.progress[p.id]||0) + '%"></div></div>'
    + '</button>'
  ).join('');
  grid.querySelectorAll('.psel-card').forEach(c => c.addEventListener('click', () => goTo(c.dataset.id)));
}

function ensureBuilt(id){
  if(STATE._built.has(id)) return;
  STATE._built.add(id);
  const W = window['PHYS7_CH${C.n}_WIDGETS'];
  if(W && typeof W[id] === 'function'){
    const body = document.getElementById(id + '-body');
    if(body){
      const ph = body.querySelector('.placeholder');
      if(ph) ph.remove();
    }
    try{ W[id](); }catch(e){ console.warn('phys7 widget ' + id + ':', e.message); }
  }
}

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 && el){
    setTimeout(() => {
      try{ renderMathInElement(el, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
    }, 0);
  }
}

function buildSidebar(id){
  const box = document.getElementById('sidebar-content');
  if(!box) return;
  const sb = SIDEBARS[id] || SIDEBARS[PARAS[0].id];
  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;
  let html = '';
  html += '<div class="xp-card"><div class="xp-card-title"><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 ? ' &mdash; ' + v : '') + '</div>'; });
  html += '</div>';
  const tip = TIPS.find(t => t.sec === id) || TIPS[0];
  if(tip){
    html += '<div class="sidecard" style="background:linear-gradient(135deg,var(--warn-bg),var(--pri-soft));border-color:var(--warn)"><h4 style="color:#92400e">Подсказка</h4><div class="sidecard-row" style="margin-bottom:0;font-size:.84rem">' + 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(t => { html += '<div class="sidecard-row" style="font-size:.78rem;color:var(--ok)">&#10003; ' + t + '</div>'; });
    html += '</div>';
  }
  box.innerHTML = html;
  if(window.renderMathInElement){
    try{ renderMathInElement(box, { delimiters:[{left:'$$',right:'$$',display:true},{left:'$',right:'$',display:false}], throwOnError:false }); }catch(e){}
  }
}

function initTheme(){
  const t = localStorage.getItem(LS_PREFIX + '_theme') || localStorage.getItem('physics7_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');
    localStorage.setItem('physics7_theme', dark ? 'dark' : 'light');
    document.getElementById('theme-lab').textContent = dark ? 'Светлая' : 'Тёмная';
  });
}

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 initSearch(){
  const btn = document.getElementById('search-btn'), modal = document.getElementById('search-modal'), inp = document.getElementById('search-inp'), list = document.getElementById('search-list');
  if(!btn || !modal) return;
  let cur = 0;
  function render(q){
    const ql = (q||'').toLowerCase().trim();
    const items = PARAS.filter(p => !ql || p.title.toLowerCase().includes(ql) || p.num.toLowerCase().includes(ql));
    list.innerHTML = items.map((p,i) => '<div class="search-item' + (i === cur ? ' cur' : '') + '" data-id="' + p.id + '"><span class="num">' + p.num + '</span>' + p.title + '</div>').join('');
    list.querySelectorAll('.search-item').forEach(el => el.addEventListener('click', () => { goTo(el.dataset.id); close(); }));
  }
  function open(){ modal.classList.add('show'); inp.value = ''; cur = 0; render(''); setTimeout(() => inp.focus(), 50); }
  function close(){ modal.classList.remove('show'); }
  btn.addEventListener('click', open);
  modal.addEventListener('click', e => { if(e.target === modal) close(); });
  inp.addEventListener('input', () => { cur = 0; render(inp.value); });
  inp.addEventListener('keydown', e => {
    const items = list.querySelectorAll('.search-item');
    if(e.key === 'ArrowDown'){ e.preventDefault(); cur = Math.min(items.length-1, cur+1); render(inp.value); }
    else if(e.key === 'ArrowUp'){ e.preventDefault(); cur = Math.max(0, cur-1); render(inp.value); }
    else if(e.key === 'Enter'){ e.preventDefault(); const sel = items[cur]; if(sel){ goTo(sel.dataset.id); close(); } }
    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 init(){
  loadProgress(); initTheme(); initSidebarToggle(); initSearch();
  buildParaSelector(); refreshProgressUI(); goTo(PARAS[0].id);
  setTimeout(() => achievement('start'), 600);
}
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
`;
}

CHAPTERS.forEach(C => {
  const html = makeHTML(C);
  const file = path.join(OUT, `physics_7_ch${C.n}.html`);
  fs.writeFileSync(file, html, 'utf8');
  console.log(`[gen_phys7_ch] ${file} — ${html.split('\n').length} lines`);
});
console.log('Done.');
