Files
Learn_System/backend/scripts/gen_phys9_ch.js
T
Maxim Dolgolyov 66bd7ac1f4 fix(textbooks): Физика 9 — STATE collision, KaTeX escape, авто-init симуляций
Три бага из жалобы пользователя:

1) phys9_legacy.js упал с 'Identifier STATE has already been declared' —
   const STATE в монолите конфликтовал с const STATE в chapter inline JS.
   Скрипт extract_phys9_legacy.cjs теперь оборачивает извлечённый код в IIFE
   и явно экспортит через window 70 функций (upd*/draw*/init*/start*/lab*/
   check*/toggle*/render*/show*/...) + 7 const-массивов (TASKS_PN, PUZ_PN).

2) В боковой панели формулы рендерились как 'Delta vecr' вместо Δr⃗ —
   мой переход на JSON.stringify в gen_phys9_ch.js добавил лишний слой
   escape backslash. Уменьшил \\ → \ в SIDEBAR_ROWS, TIPS_HTML,
   PARA_SUBS, LR_SUBS (90 строк). Цепочка теперь: source \Delta → string
   \Delta → JSON "\\Delta" → HTML JS \Delta → runtime \Delta →
   KaTeX \Delta ✓.

3) 'не работают симуляции' — функции из legacy.js были доступны, но
   chapter goTo(id) их не вызывал. Добавлен авто-вызов upd<N>(),
   startAnim<N>(), init<N>(), draw<N>() при переключении на параграф,
   и updLab<N>(), drawLab<N>() — для ЛР.
2026-05-30 09:06:20 +03:00

942 lines
68 KiB
JavaScript
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.
// Генератор physics_9_ch{1..5}.html — Phase 0 skeleton со STUB-builder'ами.
// По образцу gen_phys10_ch.js. Главы: ch1..ch4 — параграфы §1..§36, ch5 — ЛР1..ЛР12.
'use strict';
const fs = require('fs');
const path = require('path');
const TBOOKS = path.join(__dirname, '..', '..', 'frontend', 'textbooks');
// === Данные параграфов (§1..§36) ===
const PARA_NAMES = {
p1:'Механическое движение',
p2:'Относительность движения. Система отсчёта',
p3:'Скалярные и векторные величины. Действия над векторами',
p4:'Проекция вектора на ось',
p5:'Путь и перемещение',
p6:'Равномерное прямолинейное движение. Скорость',
p7:'Графическое представление равномерного движения',
p8:'Неравномерное движение. Средняя и мгновенная скорость',
p9:'Сложение скоростей',
p10:'Ускорение',
p11:'Скорость при равноускоренном движении',
p12:'Перемещение, координата и путь при равноускоренном движении',
p13:'Линейная и угловая скорости',
p14:'Ускорение точки при движении по окружности',
p15:'Взаимодействие тел. Сила. ИСО. 1-й закон Ньютона',
p16:'Масса',
p17:'Второй закон Ньютона',
p18:'Третий закон Ньютона. Принцип относительности Галилея',
p19:'Деформация тел. Сила упругости. Закон Гука',
p20:'Силы трения. Силы сопротивления среды',
p21:'Движение тела под действием силы тяжести',
p22:'Движение тела, брошенного под углом к горизонту',
p23:'Закон всемирного тяготения',
p24:'Вес. Невесомость и перегрузки',
p25:'Условия равновесия тел. Момент силы',
p26:'Простые механизмы. Рычаги. Блоки',
p27:'Наклонная плоскость. «Золотое правило» механики. КПД',
p28:'Центр тяжести. Виды равновесия',
p29:'Закон Архимеда. Выталкивающая сила',
p30:'Плавание судов. Воздухоплавание',
p31:'Импульс тела. Импульс системы тел',
p32:'Закон сохранения импульса. Реактивное движение',
p33:'Механическая работа. Мощность',
p34:'Потенциальная энергия',
p35:'Кинетическая энергия. Полная энергия системы тел',
p36:'Закон сохранения энергии',
};
const PARA_SUBS = {
p1:'материальная точка',
p2:'СО · относительность',
p3:'$\\vec a + \\vec b$',
p4:'$a_x = a\\cos\\alpha$',
p5:'$s$ vs $\\Delta\\vec r$',
p6:'$\\Delta\\vec r = \\vec v t$',
p7:'графики $v(t)$, $x(t)$',
p8:'$\\langle v\\rangle = s/t$',
p9:'$\\vec v_{1,3} = \\vec v_{1,2} + \\vec v_{2,3}$',
p10:'$\\vec a = \\Delta\\vec v/\\Delta t$',
p11:'$\\vec v = \\vec v_0 + \\vec a t$',
p12:'$x = x_0 + v_0 t + at^2/2$',
p13:'$v = \\omega R$',
p14:'$a_n = v^2/R$',
p15:'1-й закон Ньютона',
p16:'$m_1/m_2 = a_2/a_1$',
p17:'$\\vec F = m\\vec a$',
p18:'$\\vec F_{12} = -\\vec F_{21}$',
p19:'$F = -kx$',
p20:'$F_{тр} = \\mu N$',
p21:'$h = gt^2/2$',
p22:'$L = v_0^2\\sin 2\\alpha/g$',
p23:'$F = Gm_1m_2/r^2$',
p24:'$P = m(g \\pm a)$',
p25:'$M = Fl$',
p26:'$F_1 l_1 = F_2 l_2$',
p27:'$\\eta = A_{пол}/A_{сов}$',
p28:'ЦТ · равновесие',
p29:'$F_A = \\rho g V$',
p30:'$\\rho_т \\le \\rho_ж$',
p31:'$\\vec p = m\\vec v$',
p32:'$\\sum\\vec p = \\text{const}$',
p33:'$A = F\\Delta r\\cos\\alpha$',
p34:'$E_п = mgh$',
p35:'$E_к = mv^2/2$',
p36:'$E_к + E_п = \\text{const}$',
};
const PARA_WM = {
p1:'движ.', p2:'СО', p3:'&vec;a', p4:'a_x', p5:'&Delta;r', p6:'v&middot;t', p7:'v(t)',
p8:'&lang;v&rang;', p9:'v_1+v_2', p10:'a', p11:'v_0+at', p12:'at&sup2;/2', p13:'&omega;R', p14:'v&sup2;/R',
p15:'ma=F', p16:'m', p17:'F=ma', p18:'F_12=-F_21', p19:'kx', p20:'&mu;N',
p21:'g', p22:'&part;', p23:'G', p24:'P=mg',
p25:'M', p26:'l_1F_1', p27:'&eta;', p28:'ЦТ', p29:'F_A', p30:'&rho;',
p31:'p=mv', p32:'&sum;p', p33:'A', p34:'mgh', p35:'mv&sup2;/2', p36:'E=const',
final1:'&#9733;', final2:'&#9733;', final3:'&#9733;', final4:'&#9733;', final5:'&#9733;',
};
// === Данные ЛР1..ЛР12 (для Ch5) ===
const LR_NAMES = {
lr1:'Определение абсолютной и относительной погрешностей прямых измерений',
lr2:'Измерение ускорения при равноускоренном движении',
lr3:'Изучение движения тела по окружности',
lr4:'Проверка закона Гука',
lr5:'Измерение коэффициента трения скольжения',
lr6:'Изучение движения тела, брошенного горизонтально',
lr7:'Проверка условия равновесия рычага',
lr8:'Изучение неподвижного и подвижного блоков',
lr9:'Изучение наклонной плоскости и измерение её КПД',
lr10:'Изучение выталкивающей силы',
lr11:'Проверка закона сохранения импульса',
lr12:'Проверка закона сохранения механической энергии',
};
const LR_SUBS = {
lr1:'$\\Delta t$, $\\varepsilon_t$', lr2:'$a = 2l/t^2$', lr3:'$a_n = 4\\pi^2 R/T^2$',
lr4:'$k = F/x$', lr5:'$\\mu = F_{тр}/P$', lr6:'$v_0 = l\\sqrt{g/(2h)}$',
lr7:'$F_1 l_1 = F_2 l_2$', lr8:'$P h_1 = F h_2$', lr9:'$\\eta = mgh/A_{сов}$',
lr10:'$F_A = F_1 - F_2$', lr11:'$m_1 l_1 = m_1 l_1\' + m_2 l_2\'$',
lr12:'$F|x| = ml^2 g/(2h)$',
};
const LR_WM = {
lr1:'&Delta;', lr2:'a', lr3:'&omega;', lr4:'k', lr5:'&mu;', lr6:'v_0',
lr7:'l_1F_1', lr8:'F=P/2', lr9:'&eta;', lr10:'F_A', lr11:'&sum;p', lr12:'E',
};
// === Главы ===
const CHAPTERS = {
ch1: {
paras: ['p1','p2','p3','p4','p5','p6','p7','p8','p9','p10','p11','p12','p13','p14'], final: 'final1',
title: 'Основы кинематики',
headerSub: 'Механическое движение · векторы · путь и перемещение · равноускоренное движение · движение по окружности',
hero: { h:'Кинематика — как описывать движение', p:'Раздел физики, изучающий движение тел без выяснения причин, его вызывающих. Изучаем векторы, скорость, ускорение и графики движения.' },
pri:'#2563eb', priD:'#1d4ed8', priSoft:'#dbeafe', priLight:'#60a5fa',
headerGrad:'linear-gradient(110deg,#1e3a8a 0%,#2563eb 55%,#60a5fa 100%)',
chNum:1, watermarkHero:'v',
},
ch2: {
paras: ['p15','p16','p17','p18','p19','p20','p21','p22','p23','p24'], final: 'final2',
title: 'Основы динамики',
headerSub: 'Законы Ньютона · масса · сила Гука · трение · гравитация · вес и невесомость',
hero: { h:'Динамика — почему тела движутся', p:'Динамика выясняет причины движения: силы и массы. Три закона Ньютона, закон всемирного тяготения, силы упругости и трения.' },
pri:'#059669', priD:'#047857', priSoft:'#d1fae5', priLight:'#34d399',
headerGrad:'linear-gradient(110deg,#064e3b 0%,#059669 55%,#34d399 100%)',
chNum:2, watermarkHero:'F',
},
ch3: {
paras: ['p25','p26','p27','p28','p29','p30'], final: 'final3',
title: 'Основы статики',
headerSub: 'Момент силы · рычаги · блоки · наклонная плоскость · КПД · центр тяжести · закон Архимеда',
hero: { h:'Статика — равновесие тел', p:'Статика изучает условия покоя тел. Момент силы, простые механизмы, центр тяжести, закон Архимеда — основа техники.' },
pri:'#7c3aed', priD:'#6d28d9', priSoft:'#ede9fe', priLight:'#a78bfa',
headerGrad:'linear-gradient(110deg,#3b0764 0%,#7c3aed 55%,#a78bfa 100%)',
chNum:3, watermarkHero:'M',
},
ch4: {
paras: ['p31','p32','p33','p34','p35','p36'], final: 'final4',
title: 'Законы сохранения',
headerSub: 'Импульс · реактивное движение · работа · мощность · кинетическая и потенциальная энергия · закон сохранения энергии',
hero: { h:'Законы сохранения — фундамент физики', p:'Импульс и энергия сохраняются в замкнутых системах. Эти законы лежат в основе всего, от движения ракет до колебаний маятников.' },
pri:'#db2777', priD:'#be185d', priSoft:'#fce7f3', priLight:'#f472b6',
headerGrad:'linear-gradient(110deg,#831843 0%,#db2777 55%,#f472b6 100%)',
chNum:4, watermarkHero:'p&middot;E',
},
ch5: {
paras: ['lr1','lr2','lr3','lr4','lr5','lr6','lr7','lr8','lr9','lr10','lr11','lr12'], final: 'final5',
title: 'Лабораторный практикум',
headerSub: '12 лабораторных работ: погрешности · ускорение · окружность · Гук · трение · бросок · рычаг · блоки · наклонная плоскость · Архимед · импульс · энергия',
hero: { h:'Лабораторный практикум — физика руками', p:'12 классических лабораторных работ. Каждая: цель, оборудование, вывод формул, ход работы, таблица измерений, контрольные вопросы и суперзадание.' },
pri:'#0891b2', priD:'#0e7490', priSoft:'#cffafe', priLight:'#22d3ee',
headerGrad:'linear-gradient(110deg,#164e63 0%,#0891b2 55%,#22d3ee 100%)',
chNum:5, watermarkHero:'&Delta;t',
},
};
// === Краткие подсказки в боковой панели (минимальный набор; расширяется в Phase 5) ===
const SIDEBAR_ROWS = {
p1: [['Кинематика','описывает движение без причин'],['Мат. точка','тело с пренебр. размерами'],['Поступательное','все точки движутся одинаково']],
p2: [['СО','тело отсчёта + оси + часы'],['Относ.','скорость, путь, траектория'],['Земля','чаще всего тело отсчёта']],
p3: [['Скаляр','число'],['Вектор','число + направление'],['$\\vec a + \\vec b$','правило треугольника / параллелограмма']],
p4: [['Проекция','$a_x = a\\cos\\alpha$'],['Знак','зависит от $\\alpha$'],['Сумма','$(\\vec a + \\vec b)_x = a_x + b_x$']],
p5: [['Путь','скаляр $s \\ge 0$'],['Перемещ.','вектор $\\Delta\\vec r$'],['$s \\ge |\\Delta\\vec r|$','']],
p6: [['$\\vec v = \\text{const}$','равномерное'],['$\\Delta\\vec r = \\vec v t$',''],['$x = x_0 + v_x t$','координата']],
p7: [['$v(t)$','прямая'],['$x(t)$','наклонная прямая'],['Площадь','под $v(t)$ = путь']],
p8: [['Средняя','$\\langle v\\rangle = s/t$'],['Мгновенная','предел $\\Delta s/\\Delta t$'],['Спидометр','показывает мгн. $v$']],
p9: [['$\\vec v_{1,3} = \\vec v_{1,2} + \\vec v_{2,3}$',''],['Лодка/река','$\\vec v_{л,б} = \\vec v_{л,в} + \\vec v_{в,б}$'],['По теч.','скорости складываются']],
p10: [['$\\vec a = \\Delta\\vec v/\\Delta t$',''],['Ед.','м/с²'],['Знак','совпадает с $\\Delta\\vec v$']],
p11: [['$\\vec v = \\vec v_0 + \\vec a t$',''],['Проекция','$v_x = v_{0x} + a_x t$'],['','']],
p12: [['$\\Delta\\vec r = \\vec v_0 t + \\vec a t^2/2$',''],['$v^2 - v_0^2 = 2a_x\\Delta x$','без $t$'],['','']],
p13: [['$\\omega = 2\\pi/T$',''],['$v = \\omega R$',''],['$\\omega$','рад/с']],
p14: [['$a_n = v^2/R$',''],['$a_n = \\omega^2 R$',''],['К центру','направление']],
p15: [['ИСО','системы, в которых выполняется 1-й закон'],['1-й Н.','$\\sum\\vec F = 0 \\Rightarrow \\vec v = \\text{const}$'],['Инерция','свойство сохранять скорость']],
p16: [['Масса','мера инертности'],['$m_1/m_2 = a_2/a_1$',''],['Ед.','кг (эталон)']],
p17: [['$\\vec a = \\vec F/m$',''],['$\\vec F = m\\vec a$',''],['Принцип суперп.','$\\vec F = \\sum\\vec F_i$']],
p18: [['3-й Н.','$\\vec F_{12} = -\\vec F_{21}$'],['Разные тела','силы действуют на разные тела'],['Галилей','законы одинаковы во всех ИСО']],
p19: [['Закон Гука','$F = -kx$'],['Жёсткость','$k$, ед. Н/м'],['Лин. упр.','при малых деформациях']],
p20: [['Покоя','до начала движения'],['Скольж.','$F_{тр} = \\mu N$'],['$\\mu$','коэф. трения']],
p21: [['$g \\approx 9{,}81$ м/с²',''],['$h = gt^2/2$','свободное падение'],['$v = gt$','']],
p22: [['$L = v_0^2 \\sin 2\\alpha / g$','дальность'],['$H = v_0^2\\sin^2\\alpha/(2g)$','высота'],['$\\alpha = 45°$','макс. дальность']],
p23: [['$F = G m_1 m_2 / r^2$',''],['$G = 6{,}67\\cdot 10^{-11}$ Н·м²/кг²',''],['$g = GM/R^2$','на поверх. Земли']],
p24: [['Вес $P$','сила на опору/подвес'],['$P = m(g \\pm a)$',''],['$P = 0$','невесомость']],
p25: [['$M = Fl$','момент силы'],['$\\sum\\vec F = 0$ и $\\sum M = 0$',''],['Плечо','$l$ — расст. от оси до линии действия']],
p26: [['Рычаг','$F_1 l_1 = F_2 l_2$'],['Неподв. блок','без выигрыша'],['Подв. блок','выигрыш в силе в 2 раза']],
p27: [['Накл. пл.','выигрыш = $l/h$'],['«Золотое правило»','выигр. в силе = проигр. в пути'],['$\\eta = A_{пол}/A_{сов}$','КПД']],
p28: [['ЦТ','точка прилож. силы тяжести'],['Устойч.','ЦТ при отклонении поднимается'],['Безразл.','ЦТ не меняется']],
p29: [['$F_A = \\rho g V_{погр}$',''],['Вверх','направление'],['Архимед','выталкивающая сила']],
p30: [['Плав.','$\\rho_т \\le \\rho_ж$'],['Ватерлиния','граница погружения'],['Воздухопл.','подъёмная сила']],
p31: [['$\\vec p = m\\vec v$','импульс тела'],['Ед.','кг·м/с'],['Сумма','$\\vec p_{сист} = \\sum \\vec p_i$']],
p32: [['ЗСИ','$\\sum\\vec p_{до} = \\sum\\vec p_{после}$'],['Замкн. сист.','без внеш. сил'],['Ракета','$m_р\\vec v_р + m_г\\vec v_г = 0$']],
p33: [['$A = F\\Delta r\\cos\\alpha$',''],['Ед.','Дж'],['Мощность','$P = A/\\Delta t$, Вт']],
p34: [['$E_п = mgh$','тяжести'],['$E_п = kx^2/2$','упругости'],['$A = -\\Delta E_п$','']],
p35: [['$E_к = mv^2/2$',''],['Теорема','$A = \\Delta E_к$'],['$E = E_к + E_п$','полная']],
p36: [['ЗСЭ','$E = \\text{const}$ в замкн. консервативной сист.'],['Превращ.','один вид → другой'],['Трение','диссипация $\\to$ тепло']],
// ЛР sidebars — краткие
lr1: [['Цель','$\\Delta t$, $\\varepsilon_t$'],['Обор.','мерная лента, шарик, секундомер'],['Формула','$\\varepsilon_t = \\Delta t/\\langle t\\rangle \\cdot 100\\%$']],
lr2: [['Цель','измерить $a$ при равноускор.'],['Обор.','жёлоб, шарик, секундомер'],['Формула','$a = 2l/t^2$']],
lr3: [['Цель','$T$, $a_n$, $\\omega$, $v$'],['Обор.','штатив, нить, шарик'],['Формула','$a_n = 4\\pi^2 R/T^2$']],
lr4: [['Цель','$k$ пружины'],['Обор.','штатив, динамометр, грузы'],['Формула','$k = mg/|x|$']],
lr5: [['Цель','$\\mu$ дерево/дерево'],['Обор.','брусок, доска, динамометр'],['Формула','$\\mu = F_{упр}/P$']],
lr6: [['Цель','$v_0$ гориз. бросок'],['Обор.','лоток, шарик, копир. бумага'],['Формула','$v_0 = l\\sqrt{g/(2h)}$']],
lr7: [['Цель','правило рычага'],['Обор.','рычаг, грузы'],['Формула','$F_1 l_1 = F_2 l_2$']],
lr8: [['Цель','выигр. подв. блока'],['Обор.','блоки, динамометр'],['Формула','$P h_1 = F h_2$']],
lr9: [['Цель','КПД накл. плоскости'],['Обор.','доска, брусок, динамометр'],['Формула','$\\eta = mgh/(F_{упр}l)\\cdot 100\\%$']],
lr10: [['Цель','$F_A$ для разных жидк.'],['Обор.','цилиндры, динамометр, вода, соль'],['Формула','$F_A = F_{упр1} - F_{упр2}$']],
lr11: [['Цель','проверить ЗСИ'],['Обор.','лоток, два шара, бумага'],['Формула','$m_1 l_1 = m_1 l_1\' + m_2 l_2\'$']],
lr12: [['Цель','проверить ЗСЭ'],['Обор.','лоток, шар, пружина, бумага'],['Формула','$F|x| = ml^2g/(2h)$']],
};
const TIPS_HTML = {
p1: 'Кинематика — раздел физики о движении без причин. Мат. точка — тело, размерами которого можно пренебречь.',
p2: 'СО = тело отсчёта + система координат + часы. Скорость, путь и траектория зависят от выбора СО.',
p3: 'Скаляры — число (масса, путь). Векторы — число + направление (сила, скорость). Сумма векторов: правило треугольника или параллелограмма.',
p4: 'Проекция вектора $\\vec a$ на ось: $a_x = a\\cos\\alpha$. Знак зависит от угла $\\alpha$. Сумма проекций = проекция суммы.',
p5: 'Путь $s$ — скаляр $\\ge 0$. Перемещение $\\Delta\\vec r$ — вектор. Всегда $s \\ge |\\Delta\\vec r|$.',
p6: 'Равномерное движение: $\\vec v = \\text{const}$. $\\Delta\\vec r = \\vec v t$, координата $x = x_0 + v_x t$.',
p7: 'График $v(t)$ — прямая параллельная оси $t$. График $x(t)$ — наклонная прямая. Площадь под $v(t)$ = пройденный путь.',
p8: 'Средняя скорость: $\\langle v\\rangle = s/t$. Мгновенная — предел $\\Delta s/\\Delta t$ при $\\Delta t \\to 0$. Спидометр показывает мгновенную.',
p9: 'Закон сложения скоростей: $\\vec v_{1,3} = \\vec v_{1,2} + \\vec v_{2,3}$. По течению — скорости складываются, против — вычитаются.',
p10: 'Ускорение: $\\vec a = \\Delta\\vec v / \\Delta t$. Единица м/с². Направление совпадает с $\\Delta\\vec v$.',
p11: 'При равноускоренном движении: $\\vec v = \\vec v_0 + \\vec a t$. В проекциях: $v_x = v_{0x} + a_x t$.',
p12: 'Перемещение: $\\Delta\\vec r = \\vec v_0 t + \\vec a t^2/2$. Без времени: $v^2 - v_0^2 = 2a_x\\Delta x$.',
p13: 'Угловая скорость $\\omega = 2\\pi/T = 2\\pi\\nu$ (рад/с). Связь с линейной: $v = \\omega R$.',
p14: 'Центростремит. ускорение: $a_n = v^2/R = \\omega^2 R$. Направлено к центру окружности.',
p15: 'ИСО — система, в которой выполняется 1-й закон Ньютона. В отсутствие сил тело сохраняет скорость (инерция).',
p16: 'Масса — мера инертности. $m_1/m_2 = a_2/a_1$. Единица — килограмм, эталонная.',
p17: '2-й закон Ньютона: $\\vec a = \\vec F/m$. Или $\\vec F = m\\vec a$. Принцип суперпозиции: $\\vec F = \\sum \\vec F_i$.',
p18: '3-й закон Ньютона: $\\vec F_{12} = -\\vec F_{21}$. Силы приложены к разным телам! Принцип относ. Галилея: законы одинаковы во всех ИСО.',
p19: 'Закон Гука: $F_{упр} = -kx$, где $k$ — жёсткость пружины (Н/м). Линейность только при малых деформациях.',
p20: 'Сила трения скольжения: $F_{тр} = \\mu N$, где $\\mu$ — коэф. трения. Сила сопротивления среды растёт со скоростью.',
p21: 'Свободное падение: $g \\approx 9{,}81$ м/с² у поверхности Земли. $h = gt^2/2$, $v = gt$.',
p22: 'Тело, брошенное под углом: $L = v_0^2 \\sin 2\\alpha/g$ — дальность; $H = v_0^2\\sin^2\\alpha/(2g)$ — высота. Макс. $L$ при $\\alpha = 45°$.',
p23: 'Закон всемирного тяготения: $F = G m_1 m_2/r^2$. $G = 6{,}67\\cdot 10^{-11}$ Н·м²/кг². У поверхности: $g = GM/R^2$.',
p24: 'Вес $P$ — сила, с которой тело давит на опору / тянет подвес. $P = m(g \\pm a)$. При свободном падении $P = 0$ — невесомость.',
p25: 'Условия равновесия: $\\sum\\vec F = 0$ И $\\sum M = 0$. Момент силы $M = F \\cdot l$, где $l$ — плечо.',
p26: 'Рычаг в равновесии: $F_1 l_1 = F_2 l_2$. Неподвижный блок выигрыша не даёт. Подвижный — выигрыш в силе в 2 раза.',
p27: 'Накл. плоскость: выигрыш в силе = $l/h$. «Золотое правило»: выигрываем в силе — проигрываем в пути. КПД: $\\eta = A_{пол}/A_{сов}$.',
p28: 'Центр тяжести — точка приложения равнодействующей сил тяжести. Устойчивое равновесие: ЦТ при отклонении поднимается.',
p29: 'Закон Архимеда: $F_A = \\rho_ж g V_{погр}$. Направлен вверх. Не зависит от глубины, формы тела или плотности тела.',
p30: 'Условие плавания: $\\rho_т \\le \\rho_ж$. Подъёмная сила воздухоплавательного аппарата — разность веса вытесненного воздуха и веса аппарата.',
p31: 'Импульс тела: $\\vec p = m\\vec v$ (кг·м/с). Импульс системы — сумма импульсов всех тел.',
p32: 'ЗСИ: в замкнутой системе $\\sum\\vec p = \\text{const}$. Реактивное движение: $m_р\\vec v_р + m_г\\vec v_г = 0$.',
p33: 'Работа силы: $A = F \\Delta r \\cos\\alpha$ (Дж). Мощность: $P = A/\\Delta t = Fv\\cos\\alpha$ (Вт).',
p34: 'Потенц. энергия тяжести: $E_п = mgh$. Упругости: $E_п = kx^2/2$. Работа консерват. силы: $A = -\\Delta E_п$.',
p35: 'Кинет. энергия: $E_к = mv^2/2$. Теорема: $A = \\Delta E_к$. Полная мех. энергия: $E = E_к + E_п$.',
p36: 'ЗСЭ: в замкнутой консервативной системе $E_к + E_п = \\text{const}$. При трении мех. энергия превращается в тепло.',
// ЛР tips
lr1: 'ЛР1: погрешности прямых измерений. $\\Delta t = \\Delta t_{сист} + \\Delta t_{случ}$. Результат в интервальной форме: $t = \\langle t\\rangle \\pm \\Delta t$.',
lr2: 'ЛР2: ускорение шарика по наклонному жёлобу. $a = 2l/t^2$ (из $s = at^2/2$ при $v_0 = 0$).',
lr3: 'ЛР3: движение по окружности. Измеряем $T$, считаем $a_n = 4\\pi^2 R/T^2$, $\\omega = 2\\pi/T$, $v = \\omega R$.',
lr4: 'ЛР4: закон Гука. Подвешиваем грузы, строим график $F_{упр}(x)$. Жёсткость $k = mg/|x|$.',
lr5: 'ЛР5: коэффициент трения скольжения дерева по дереву. $\\mu = F_{упр}/P$.',
lr6: 'ЛР6: тело, брошенное горизонтально. Измеряем дальность $l$ и высоту $h$. $v_0 = l\\sqrt{g/(2h)}$.',
lr7: 'ЛР7: условие равновесия рычага. Проверяем $F_1 l_1 = F_2 l_2$.',
lr8: 'ЛР8: блоки. Неподв. — без выигрыша; подвижный — выигрыш в силе в 2 раза, проигрыш в пути в 2 раза.',
lr9: 'ЛР9: КПД наклонной плоскости. $\\eta = A_{пол}/A_{сов} = mgh/(F_{упр}l)\\cdot 100\\%$. Сравниваем при 30° и 45°.',
lr10: 'ЛР10: выталкивающая сила Архимеда. $F_A = F_{упр1} - F_{упр2}$ (вес в воздухе минус вес в жидкости).',
lr11: 'ЛР11: ЗСИ. Шар $m_1$ скатывается, сталкивается с покоящимся шаром $m_2$. Проверяем $m_1 l_1 = m_1 l_1\' + m_2 l_2\'$.',
lr12: 'ЛР12: ЗСЭ. Сжатая пружина → шар → дальность полёта. $F_{упр}|x| = ml^2 g/(2h)$.',
final1: 'Финал главы 1 — интегрированные задачи по §§1–14. В разработке (Phase 1+).',
final2: 'Финал главы 2 — интегрированные задачи по §§15–24. В разработке (Phase 2+).',
final3: 'Финал главы 3 — интегрированные задачи по §§25–30. В разработке (Phase 3+).',
final4: 'Финал главы 4 — интегрированные задачи по §§31–36. В разработке (Phase 4+).',
final5: 'Финал главы 5 — итоговый отчёт по 12 ЛР. В разработке (Phase 5+).',
};
// Helper: prefix для номера секции (§ или ЛР или ★ для финала)
function numLabel(pid){
if (pid.startsWith('final')) return '★';
if (pid.startsWith('lr')) return 'ЛР ' + pid.slice(2);
return '§ ' + pid.slice(1);
}
function nameOf(pid){
if (pid.startsWith('final')) return 'Финал главы';
if (pid.startsWith('lr')) return LR_NAMES[pid];
return PARA_NAMES[pid];
}
function subOf(pid){
if (pid.startsWith('final')) return '';
if (pid.startsWith('lr')) return LR_SUBS[pid] || '';
return PARA_SUBS[pid] || '';
}
function wmOf(pid){
if (pid.startsWith('lr')) return LR_WM[pid] || '?';
return PARA_WM[pid] || '?';
}
// === Билд одного ch ===
function buildCh(chKey) {
const C = CHAPTERS[chKey];
const slug = 'physics-9-' + chKey;
const lsPrefix = 'physics9_' + chKey;
const xpKey = 'physics9_xp';
const allParas = [...C.paras, C.final];
// PARAS JS literal
const parasArr = allParas.map(pid => {
if (pid.startsWith('final')) {
return ` { id:${JSON.stringify(pid)}, num:'\\u2605', name:'Финал главы', sub:${JSON.stringify('Итоги · боссы главы ' + C.chNum)}, final:true }`;
}
const sub = subOf(pid);
const num = pid.startsWith('lr') ? `ЛР ${pid.slice(2)}` : ${pid.slice(1)}`;
return ` { id:${JSON.stringify(pid)}, num:${JSON.stringify(num)}, name:${JSON.stringify(nameOf(pid))}, sub:${JSON.stringify(sub)} }`;
}).join(',\n');
const total = allParas.length;
// ACH_LABELS
const achLabels = [
` start:"Начало главы ${C.chNum}!"`,
...C.paras.map(pid => ` ${pid}_done:${JSON.stringify(nameOf(pid) + ' освоен!')}`),
` ${chKey}_done:"Глава ${C.chNum} пройдена!"`,
].join(',\n');
// SIDEBARS
const sidebarObj = allParas.map(pid => {
const rows = pid.startsWith('final')
? [[`§§${C.paras[0].replace(/^[pl]r?/,'')}${C.paras[C.paras.length-1].replace(/^[pl]r?/,'')}`, `теория главы ${C.chNum}`],['Награда','+50 XP']]
: (SIDEBAR_ROWS[pid] || [['В разработке',`шпаргалка ${pid}`]]);
const titleStr = pid.startsWith('final')
? `Финал главы ${C.chNum}`
: (pid.startsWith('lr') ? `Шпаргалка ЛР ${pid.slice(2)}` : `Шпаргалка §${pid.slice(1)}`);
const rowsLit = rows.map(([k,v]) => `[${JSON.stringify(k)},${JSON.stringify(v)}]`).join(',');
return ` ${pid}:{title:${JSON.stringify(titleStr)},rows:[${rowsLit}]}`;
}).join(',\n');
// TIPS
const tipsArr = allParas.map(pid => {
const html = TIPS_HTML[pid] || `Подсказка к ${pid} — в разработке.`;
return ` {sec:${JSON.stringify(pid)},html:${JSON.stringify(html)}}`;
}).join(',\n');
// STUB-builder для каждого
const builders = allParas.map(pid => {
const isFinal = pid.startsWith('final');
const isLR = pid.startsWith('lr');
const name = isFinal ? `Финал главы ${C.chNum}` : nameOf(pid);
const num = isFinal ? '★' : (isLR ? `ЛР ${pid.slice(2)}` : ${pid.slice(1)}`);
const idx = allParas.indexOf(pid);
const prev = idx > 0 ? allParas[idx-1] : null;
const next = idx < allParas.length - 1 ? allParas[idx+1] : null;
const prevStr = prev ? `'${prev}'` : 'null';
const nextStr = next ? `'${next}'` : 'null';
const bodyHtml = isLR
? `<p><b>${name}</b> — лабораторная работа в разработке (Phase 5+).</p>
<p>Здесь появятся: <b>Цель · Оборудование · Проверьте себя · Вывод расчётных формул · Ход работы · Таблица измерений · Контрольные вопросы · Суперзадание</b> — по канве учебника Исаченковой 2019.</p>
<p style="margin-top:10px;padding:10px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:.92rem">
<b>Phase 0:</b> создан скелет. <b>Phase 5:</b> наполнение ЛР пошаговой работой с интерактивной таблицей измерений.
</p>`
: `<p><b>${name}</b> — этот параграф в разработке (Phase ${C.chNum}+).</p>
<p>Здесь появятся: теория, формулы, разобранные примеры и 3–4 интерактива в стиле «физики 10» — векторные диаграммы, графики движения, ползунки и автопроверяемые тренажёры.</p>
<p style="margin-top:10px;padding:10px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:.92rem">
<b>Phase 0:</b> создан скелет. <b>Phase 5:</b> наполнение по учебнику «Физика 9» (Исаченкова, Сокольский, Захаревич, 2019).
</p>`;
return `function build_${pid}(){
const box = document.getElementById('${pid}-body');
let html = '';
html += makeCard('theory', ${JSON.stringify(name)}, ${JSON.stringify(num)}, \`
${bodyHtml}
\`);
html += secNav(${prevStr}, ${nextStr});
html += readButton('${pid}');
box.innerHTML = html;
renderMath(box);
wireReadBtn('${pid}');
}`;
}).join('\n\n');
const buildersMap = allParas.map(pid => `${pid}:()=>build_${pid}()`).join(', ');
// sec node HTML
const secNodes = allParas.map(pid => {
const isFinal = pid.startsWith('final');
const isLR = pid.startsWith('lr');
const num = isFinal ? '★' : (isLR ? `ЛР ${pid.slice(2)}` : ${pid.slice(1)}`);
const titleHtml = isFinal ? 'Финал главы' : nameOf(pid);
const wm = wmOf(pid);
const numHtml = isFinal
? `<span class="sec-num" style="background:linear-gradient(135deg,${C.pri},${C.priLight})">★</span>`
: `<span class="sec-num">${num}</span>`;
return ` <section id="sec-${pid}" class="sec" data-watermark="${wm}"><div class="sec-header">${numHtml}<h2 class="sec-h">${titleHtml}</h2></div><div id="${pid}-body"></div></section>`;
}).join('\n');
const secCss = allParas.map(pid =>
`.sec[id="sec-${pid}"]{ --sec-acc:${C.pri}; --sec-acc-d:${C.priD}; --sec-acc-soft:${C.priSoft}; }`
).join('\n');
// Names for secNav
const namesObj = allParas.map(pid => {
if (pid.startsWith('final')) return `${pid}:'Финал'`;
if (pid.startsWith('lr')) return `${pid}:'ЛР${pid.slice(2)}'`;
return `${pid}:'\\xA7${pid.slice(1)}'`;
}).join(',');
const firstParaLabel = C.paras[0].startsWith('lr') ? `ЛР ${C.paras[0].slice(2)}` : ${C.paras[0].slice(1)}`;
// === Финальный HTML ===
const html = `<!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 · Глава ${C.chNum} · «${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">
<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" 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:${C.pri}; --pri2:${C.priD}; --pri-soft:${C.priSoft};
--acc:${C.priLight}; --acc2:${C.pri}; --acc-soft:${C.priSoft};
--ok:#10b981; --ok-bg:#d1fae5; --warn:#f59e0b; --warn-bg:#fef3c7;
--bad:#ef4444; --fail:#dc2626; --fail-bg:#fee2e2;
}
.dark{--bg:#0a0e1a; --card:#0f1727; --card-soft:#13192a; --text:#dbeafe; --ink:#dbeafe; --muted:#7c8fab; --border:#1e2a44}
*{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:${C.headerGrad};color:#fff;padding:46px 22px 30px;overflow:hidden;border-bottom:2px solid rgba(255,255,255,.2);min-height:130px}
.hdr::before{content:'ГЛАВА ${C.chNum}';position:absolute;right:-12px;top:50%;transform:translateY(-50%);font-family:'Unbounded',sans-serif;font-size:clamp(5rem,15vw,11rem);font-weight:900;letter-spacing:-.04em;color:transparent;-webkit-text-stroke:1.5px rgba(255,255,255,.12);line-height:1;pointer-events:none;user-select:none;z-index:0}
.hdr-row{position:relative;z-index:1;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
.hdr h1{font-family:'Unbounded',sans-serif;font-size:1.5rem;font-weight:900;letter-spacing:-.01em;line-height:1.3;padding-top:4px}
.hdr-sub{font-size:.85rem;opacity:.88;margin-top:6px;font-weight:500;line-height:1.4}
.hdr-side{margin-left:auto;display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.hdr-btn{padding:7px 12px;border-radius:9px;background:rgba(255,255,255,.14);color:#fff;font-weight:600;font-size:.82rem;display:inline-flex;align-items:center;gap:6px;transition:background .15s;text-decoration:none}
.hdr-btn:hover{background:rgba(255,255,255,.24)}
.main{max-width:1240px;margin:0 auto;padding:22px;width:100%;display:grid;grid-template-columns:1fr 280px;gap:24px}
@media(max-width:980px){.main{grid-template-columns:1fr;padding:14px}}
.col-main{min-width:0}
.hero{background:linear-gradient(135deg,var(--pri-soft) 0%,var(--acc-soft) 50%,var(--pri-soft) 100%);background-size:200% 200%;animation:heroShift 12s ease-in-out infinite;border:1px solid var(--border);border-radius:18px;padding:24px 22px;margin-bottom:24px;position:relative;overflow:hidden}
@keyframes heroShift{0%,100%{background-position:0% 50%}50%{background-position:100% 50%}}
.hero::before{content:'${C.watermarkHero}';position:absolute;right:0;top:-30px;font-size:clamp(2rem,12vw,8rem);font-weight:900;color:var(--pri);opacity:.10;line-height:1;pointer-events:none;font-family:'Unbounded',sans-serif}
.hero h2{font-family:'Unbounded',sans-serif;font-size:1.55rem;font-weight:800;color:var(--pri2);margin-bottom:10px;letter-spacing:-.01em}
.hero p{font-size:.95rem;color:var(--text);opacity:.88;margin-bottom:14px;max-width:640px}
.hero-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center}
.btn-primary{padding:11px 22px;background:linear-gradient(135deg,var(--pri),var(--pri2));color:#fff;border-radius:11px;font-weight:700;font-size:.92rem;display:inline-flex;align-items:center;gap:8px;box-shadow:var(--sh2);transition:transform .15s,box-shadow .15s}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 28px rgba(0,0,0,.18)}
.hero-progress{flex:1;min-width:200px;max-width:280px}
.hp-label{font-size:.74rem;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.06em;display:block;margin-bottom:5px}
.hp-bar{height:8px;background:rgba(0,0,0,.12);border-radius:5px;overflow:hidden}
.hp-fill{height:100%;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:5px;width:0%;transition:width .6s cubic-bezier(.16,1,.3,1)}
.hp-text{font-size:.78rem;color:var(--muted);font-weight:700;margin-top:4px;display:block}
.hero-xp-badge{display:inline-flex;align-items:center;gap:6px;padding:6px 12px;background:linear-gradient(135deg,var(--warn,#f59e0b),var(--pri));color:#fff;border-radius:99px;font-size:.82rem;font-weight:800;letter-spacing:.02em;box-shadow:0 4px 12px rgba(0,0,0,.18);font-family:'Unbounded',sans-serif}
.psel{margin-bottom:24px}
.psel-title{font-size:.72rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;margin-bottom:10px}
.psel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px}
.psel-card{background:var(--card);border:1.5px solid var(--border);border-radius:13px;padding:14px;cursor:pointer;transition:transform .2s,box-shadow .2s,border-color .2s;text-align:left;position:relative}
.psel-card:hover{transform:translateY(-3px);box-shadow:var(--sh2);border-color:var(--pri)}
.psel-card.active{border-color:var(--pri);background:linear-gradient(135deg,var(--pri-soft),var(--card));box-shadow:var(--sh2)}
.psel-card.active::after{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--pri),var(--acc));border-radius:13px 13px 0 0}
.psel-num{font-family:'Unbounded',sans-serif;font-size:.72rem;font-weight:800;color:var(--pri);text-transform:uppercase;letter-spacing:.08em;margin-bottom:5px}
.psel-name{font-size:.86rem;font-weight:700;color:var(--text);line-height:1.3;margin-bottom:8px}
.psel-prog{height:4px;background:rgba(0,0,0,.10);border-radius:3px;overflow:hidden}
.psel-prog-fill{height:100%;background:var(--pri);width:0%;transition:width .4s}
.psel-card.final{background:linear-gradient(135deg,var(--acc-soft),var(--pri-soft))}
.psel-card.final .psel-num{color:var(--warn)}
${secCss}
.sec{display:none;position:relative;animation:fadeIn .35s ease}
.sec.active{display:block}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.sec::before{content:attr(data-watermark);position:absolute;right:-20px;top:10%;font-family:'Unbounded',sans-serif;font-size:clamp(6rem,18vw,14rem);font-weight:900;color:transparent;-webkit-text-stroke:1.5px var(--sec-acc-soft,var(--pri-soft));line-height:1;pointer-events:none;user-select:none;z-index:0;opacity:.35}
.sec-header{margin-bottom:22px;padding-bottom:14px;border-bottom:2px solid var(--sec-acc-soft,var(--pri-soft));position:relative;z-index:1}
.sec-num{display:inline-block;padding:4px 10px;background:linear-gradient(135deg,var(--sec-acc,var(--pri)),var(--sec-acc-d,var(--pri2)));color:#fff;border-radius:7px;font-family:'Unbounded',sans-serif;font-size:.78rem;font-weight:800;letter-spacing:.04em;margin-bottom:8px}
.sec-h{font-family:'Unbounded',sans-serif;font-size:1.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.theory{background:#8b5cf6}.card-icon.example{background:#10b981}.card-icon.lab{background:#0891b2}.card-icon.rule{background:#ec4899}
.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))}
.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}
.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}
</style>
</head>
<body>
<header class="hdr">
<div class="hdr-row">
<div>
<h1>Физика 9 · Глава ${C.chNum}</h1>
<div class="hdr-sub">${C.headerSub}</div>
</div>
<div class="hdr-side">
<a href="/textbook/physics-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>${C.hero.h}</h2>
<p>${C.hero.p}</p>
<div class="hero-row">
<button class="btn-primary" onclick="goTo('${C.paras[0]}')"><svg class="ic" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4" fill="currentColor" stroke="none"/></svg> Начать ${firstParaLabel}</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"></div>
</div>
</section>
<section class="psel">
<div class="psel-title">${chKey === 'ch5' ? 'Лабораторные работы' : 'Параграфы главы'}</div>
<div id="psel-grid" class="psel-grid"></div>
</section>
${secNodes}
</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» · Глава ${C.chNum} · «${C.title}» · 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:'${C.paras[0]}', progress:{}, achievements:new Map(), xp:0, level:1 };
const TOTAL_PARAS = ${total};
const _TB_SLUG = '${slug}';
const PARAS = [
${parasArr}
];
PARAS.forEach(p => { STATE.progress[p.id] = 0; });
function calcLevel(xp){ return Math.floor(Math.sqrt((xp||0)/100))+1; }
function _xpForLevel(lv){ return (lv-1)*(lv-1)*100; }
const ACH_LABELS = {
${achLabels}
};
function loadProgress(){
try{
const s=localStorage.getItem('${lsPrefix}_progress'); if(s) Object.assign(STATE.progress, JSON.parse(s));
const a=localStorage.getItem('${lsPrefix}_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('${xpKey}')||0); STATE.level=calcLevel(STATE.xp);
}catch(e){}
}
function saveProgress(){
try{
localStorage.setItem('${lsPrefix}_progress', JSON.stringify(STATE.progress));
localStorage.setItem('${lsPrefix}_achievements', JSON.stringify(Object.fromEntries(STATE.achievements)));
localStorage.setItem('${xpKey}', 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,'physics9-${chKey}-'+(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){} }
refreshDoneMarks();
}
function refreshDoneMarks(){
try{
document.querySelectorAll('.psel-card').forEach(c=>{
const id = c.dataset.id || c.dataset.progCard;
if(!id) return;
const pct = +STATE.progress[id] || 0;
if(!c.querySelector('.psel-done')){
const 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){}
}
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();
const BUILDERS = { ${buildersMap} };
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);
// Auto-init legacy simulations: call upd<N>() / startAnim<N>() / draw<N>() if defined in phys9_legacy.js.
if(id.startsWith('p')){
const n = id.slice(1);
setTimeout(()=>{
['upd','startAnim','init','draw'].forEach(prefix=>{
const fn = window[prefix + n];
if(typeof fn === 'function'){ try{ fn(); }catch(e){ console.warn(prefix + n + ' init:', e.message); } }
});
}, 50);
} else if(id.startsWith('lr')){
const n = id.slice(2);
setTimeout(()=>{
['updLab','drawLab'].forEach(prefix=>{
const fn = window[prefix + n];
if(typeof fn === 'function'){ try{ fn(); }catch(e){} }
});
}, 50);
}
markLastPara(id);
}
const SIDEBARS = {
${sidebarObj}
};
const TIPS=[
${tipsArr}
];
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"><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?' \\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('${lsPrefix}_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('${lsPrefix}_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){} } }
const ICONS = {
theory:'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
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>',
lab:'<svg class="ic" viewBox="0 0 24 24"><path d="M10 2v7.5L4.5 19a2 2 0 0 0 1.7 3h11.6a2 2 0 0 0 1.7-3L14 9.5V2"/><line x1="9" y1="2" x2="15" y2="2"/></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>',
};
function makeCard(kind, title, num, body){
const labels = {theory:'Теория',example:'Пример',lab:'Лабораторная работа',rule:'Правило'};
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 = {${namesObj}};
let h='<div class="sec-nav">';
h+=prev?'<button class="btn" onclick="goTo(\\''+prev+'\\')"><svg class="ic" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg> '+NAMES[prev]+'</button>':'<span></span>';
h+=next?'<button class="btn primary" onclick="goTo(\\''+next+'\\')">'+NAMES[next]+' <svg class="ic" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>':'<span></span>';
h+='</div>'; return h;
}
function readButton(paraId){
const p = PARAS.find(x => x.id === paraId);
const labelTail = p && p.final ? 'финал' : (p ? p.num : '?');
return '<div style="margin-top:18px;display:flex;justify-content:center">'
+'<button class="btn primary" id="'+paraId+'-read-btn">'
+'<svg class="ic" viewBox="0 0 24 24"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>'
+' Я прочитал — '+labelTail+' (+10 XP)'
+'</button></div>';
}
function wireReadBtn(paraId){
const btn = document.getElementById(paraId+'-read-btn'); if(!btn) return;
btn.addEventListener('click', ()=>{
addXp(10, paraId+'-read'); bumpProgress(paraId, 30);
btn.textContent='Прочитано! +10 XP'; btn.disabled=true; btn.style.opacity=.6;
const aId = paraId+'_done';
if(ACH_LABELS[aId]) achievement(aId);
});
}
/* ===== STUB BUILDERS — наполнение в Phase 5 ===== */
${builders}
/* ===== Search ===== */
const SEARCH_INDEX = (function(){
const arr=[];
PARAS.forEach(p=>arr.push({kind: p.id.startsWith('lr')?'Лабораторная':(p.final?'Финал':'Параграф'),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(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>
`;
return html;
}
// === Run ===
for (const chKey of ['ch1','ch2','ch3','ch4','ch5']) {
const dst = path.join(TBOOKS, `physics_9_${chKey}.html`);
const html = buildCh(chKey);
fs.writeFileSync(dst, html);
const scriptMatches = [...html.matchAll(/<script>([\s\S]*?)<\/script>/g)];
for (const m of scriptMatches) {
try { new Function(m[1]); }
catch(e) {
console.error(`JS PARSE FAIL in ${chKey}:`, e.message);
process.exit(1);
}
}
console.log(`OK ${chKey}${dst} bytes: ${html.length}`);
}
console.log('All 5 chapters generated.');