diff --git a/frontend/js/labs/_loader.js b/frontend/js/labs/_loader.js
new file mode 100644
index 0000000..f8435a1
--- /dev/null
+++ b/frontend/js/labs/_loader.js
@@ -0,0 +1,76 @@
+'use strict';
+/*
+ * LabLoader — ленивый загрузчик кода симуляций (контент-движок, Фаза 3).
+ *
+ * Тяжёлый код симуляций (~2.5 МБ) и three.js (~600 КБ) больше НЕ грузятся на старте.
+ * При открытии симуляции LabLoader.ensure(id) подгружает её файлы (по манифесту
+ * window.SIM_DEPS из _sim_deps.js) и, при необходимости, three.js — затем резолвит.
+ *
+ * Гарантия корректности (самовосстановление): если после загрузки указанных файлов
+ * глобальная open-функция (SIM_DEPS[id].open, напр. "_openPendulum") всё ещё не
+ * определена, грузятся ВСЕ ленивые файлы (window.LAB_LAZY_FILES). Поэтому ошибка в
+ * манифесте не может «сломать» симуляцию — в худшем случае грузится больше файлов
+ * (поведение как до Фазы 3). Манифест лишь оптимизирует объём загрузки.
+ *
+ * Все загрузки кешируются (по URL) и дедуплицируются.
+ */
+(function () {
+ var BASE = '/js/labs/';
+ var THREE_URL = 'https://cdn.jsdelivr.net/npm/three@0.149.0/build/three.min.js';
+ var _cache = {}; // url -> Promise
+ var _allLoaded = false;
+
+ function loadScript(url) {
+ if (_cache[url]) return _cache[url];
+ _cache[url] = new Promise(function (resolve, reject) {
+ var s = document.createElement('script');
+ s.src = url;
+ s.async = false; // сохранить порядок при добавлении нескольких сразу
+ s.onload = function () { resolve(url); };
+ s.onerror = function () { delete _cache[url]; reject(new Error('LabLoader: не удалось загрузить ' + url)); };
+ document.head.appendChild(s);
+ });
+ return _cache[url];
+ }
+
+ function ensureThree() {
+ if (typeof window.THREE !== 'undefined') return Promise.resolve();
+ return loadScript(THREE_URL);
+ }
+
+ function loadFiles(files) {
+ return Promise.all((files || []).map(function (f) { return loadScript(BASE + f); }));
+ }
+
+ function loadAllLazy() {
+ if (_allLoaded) return Promise.resolve();
+ var list = window.LAB_LAZY_FILES || [];
+ return loadFiles(list).then(function () { _allLoaded = true; });
+ }
+
+ // ensure(id): загрузить всё необходимое для симуляции id, вернуть Promise.
+ function ensure(id) {
+ var dep = (window.SIM_DEPS && window.SIM_DEPS[id]) || null;
+ if (!dep) {
+ // нет манифеста для id — безопасно грузим всё
+ return loadAllLazy();
+ }
+ var p = dep.three ? ensureThree() : Promise.resolve();
+ return p
+ .then(function () { return loadFiles(dep.files); })
+ .then(function () {
+ var openName = dep.open;
+ if (openName && typeof window[openName] !== 'function') {
+ if (window.console) console.warn('[LabLoader] самовосстановление для "' + id + '": ' + openName + ' не найдена после загрузки ' + JSON.stringify(dep.files) + ' — гружу все ленивые файлы');
+ return loadAllLazy();
+ }
+ });
+ }
+
+ window.LabLoader = {
+ ensure: ensure,
+ ensureThree: ensureThree,
+ loadScript: loadScript,
+ loadAllLazy: loadAllLazy
+ };
+})();
diff --git a/frontend/js/labs/_sim_deps.js b/frontend/js/labs/_sim_deps.js
new file mode 100644
index 0000000..114a8b9
--- /dev/null
+++ b/frontend/js/labs/_sim_deps.js
@@ -0,0 +1,300 @@
+'use strict';
+/* Контент-движок, Фаза 3 — манифест зависимостей симуляций (СГЕНЕРИРОВАН).
+ id -> { open: имя глобальной _openX, files: [ленивые файлы], three: нужен ли three.js }.
+ Файлы загружаются лениво по клику (см. _loader.js). three.js — только для 3D-симуляций.
+ Самовосстановление в _loader: если после загрузки open-функция не определена,
+ грузятся ВСЕ ленивые файлы -> корректность не зависит от точности манифеста.
+ Регенерация: node tools/gen-sim-deps.js (см. CONTEXT). НЕ редактировать вручную. */
+window.SIM_DEPS = {
+ "graph": {
+ "open": "_openGraph",
+ "files": [],
+ "three": false
+ },
+ "projectile": {
+ "open": "_openProjectile",
+ "files": [
+ "projectile.js"
+ ],
+ "three": false
+ },
+ "collision": {
+ "open": "_openCollision",
+ "files": [
+ "collision.js"
+ ],
+ "three": false
+ },
+ "triangle": {
+ "open": "_openTriangle",
+ "files": [
+ "triangle.js"
+ ],
+ "three": false
+ },
+ "trigcircle": {
+ "open": "_openTrigCircle",
+ "files": [
+ "trigcircle.js"
+ ],
+ "three": false
+ },
+ "emfield": {
+ "open": "_openEMField",
+ "files": [
+ "emfield.js",
+ "logic.js"
+ ],
+ "three": false
+ },
+ "molphys": {
+ "open": "_openMolPhys",
+ "files": [
+ "brownian.js",
+ "diffusion.js",
+ "gas.js",
+ "states.js"
+ ],
+ "three": false
+ },
+ "circuit": {
+ "open": "_openCircuit",
+ "files": [
+ "circuit.js"
+ ],
+ "three": false
+ },
+ "chemistry": {
+ "open": "_openChemistry",
+ "files": [
+ "circuit.js",
+ "flask.js",
+ "ionexchange.js",
+ "reactions.js",
+ "redox.js"
+ ],
+ "three": false
+ },
+ "dynamics": {
+ "open": "_openDynamics",
+ "files": [
+ "forcesandbox.js",
+ "newton.js"
+ ],
+ "three": false
+ },
+ "crystal": {
+ "open": "_openCrystal",
+ "files": [
+ "crystal.js"
+ ],
+ "three": true
+ },
+ "orbitals": {
+ "open": "_openOrbitals",
+ "files": [
+ "orbitals.js"
+ ],
+ "three": true
+ },
+ "stereo": {
+ "open": "_openStereo",
+ "files": [
+ "stereo.js"
+ ],
+ "three": true
+ },
+ "chemsandbox": {
+ "open": "_openChemSandbox",
+ "files": [
+ "chemsandbox.js",
+ "collision.js"
+ ],
+ "three": false
+ },
+ "celldivision": {
+ "open": "_openCellDivision",
+ "files": [
+ "celldivision.js"
+ ],
+ "three": false
+ },
+ "photosynthesis": {
+ "open": "_openPhotosynthesis",
+ "files": [
+ "photosynthesis.js"
+ ],
+ "three": false
+ },
+ "angrybirds": {
+ "open": "_openAngryBirds",
+ "files": [
+ "angrybirds.js"
+ ],
+ "three": false
+ },
+ "quadratic": {
+ "open": "_openQuadratic",
+ "files": [
+ "quadratic.js"
+ ],
+ "three": false
+ },
+ "normaldist": {
+ "open": "_openNormalDist",
+ "files": [
+ "normaldist.js"
+ ],
+ "three": false
+ },
+ "graphtransform": {
+ "open": "_openGraphTransform",
+ "files": [
+ "graphtransform.js"
+ ],
+ "three": false
+ },
+ "pendulum": {
+ "open": "_openPendulum",
+ "files": [
+ "pendulum.js"
+ ],
+ "three": false
+ },
+ "equilibrium": {
+ "open": "_openEquilibrium",
+ "files": [
+ "equilibrium.js"
+ ],
+ "three": false
+ },
+ "opticsbench": {
+ "open": "_openOpticsBench",
+ "files": [
+ "opticsbench.js"
+ ],
+ "three": false
+ },
+ "isoprocess": {
+ "open": "_openIsoprocess",
+ "files": [
+ "isoprocess.js"
+ ],
+ "three": false
+ },
+ "titration": {
+ "open": "_openTitration",
+ "files": [
+ "titration.js"
+ ],
+ "three": false
+ },
+ "probability": {
+ "open": "_openProbability",
+ "files": [
+ "probability.js"
+ ],
+ "three": false
+ },
+ "bohratom": {
+ "open": "_openBohrAtom",
+ "files": [
+ "bohratom.js"
+ ],
+ "three": false
+ },
+ "electrolysis": {
+ "open": "_openElectrolysis",
+ "files": [
+ "electrolysis.js"
+ ],
+ "three": false
+ },
+ "race": {
+ "open": "_openRace",
+ "files": [
+ "race.js"
+ ],
+ "three": false
+ },
+ "waves": {
+ "open": "_openWaves",
+ "files": [
+ "waves.js"
+ ],
+ "three": false
+ },
+ "hydrostatics": {
+ "open": "_openHydro",
+ "files": [
+ "hydrostatics.js"
+ ],
+ "three": false
+ },
+ "radioactive": {
+ "open": "_openRadioactive",
+ "files": [
+ "radioactive.js"
+ ],
+ "three": false
+ },
+ "geometry": {
+ "open": "_openGeometry",
+ "files": [
+ "geometry.js",
+ "triangle.js"
+ ],
+ "three": false
+ },
+ "logic": {
+ "open": "_openLogic",
+ "files": [
+ "logic.js"
+ ],
+ "three": false
+ },
+ "heatengine": {
+ "open": "_openHeatEngine",
+ "files": [
+ "heatengine.js"
+ ],
+ "three": false
+ },
+ "stoichiometry": {
+ "open": "_openStoich",
+ "files": [
+ "stoichiometry.js"
+ ],
+ "three": false
+ },
+ "qualanalysis": {
+ "open": "_openQualAnalysis",
+ "files": [
+ "qualanalysis.js"
+ ],
+ "three": false
+ },
+ "periodic": {
+ "open": "_openPeriodic",
+ "files": [
+ "_periodic_data.js",
+ "periodic.js"
+ ],
+ "three": true
+ },
+ "organic": {
+ "open": "_openOrganic",
+ "files": [
+ "organic.js"
+ ],
+ "three": false
+ },
+ "solutions": {
+ "open": "_openSolutions",
+ "files": [
+ "solutions.js"
+ ],
+ "three": false
+ }
+};
+window.LAB_LAZY_FILES = ["angrybirds.js","bohratom.js","brownian.js","celldivision.js","chemsandbox.js","circuit.js","collision.js","crystal.js","diffusion.js","electrolysis.js","emfield.js","equilibrium.js","flask.js","forcesandbox.js","gas.js","geometry.js","graphtransform.js","heatengine.js","hydrostatics.js","ionexchange.js","isoprocess.js","logic.js","newton.js","normaldist.js","opticsbench.js","orbitals.js","organic.js","pendulum.js","periodic.js","photosynthesis.js","probability.js","projectile.js","quadratic.js","qualanalysis.js","race.js","radioactive.js","reactions.js","redox.js","solutions.js","states.js","stereo.js","stoichiometry.js","titration.js","triangle.js","trigcircle.js","waves.js","_periodic_data.js"];
diff --git a/frontend/lab.html b/frontend/lab.html
index eae2f81..abc74da 100644
--- a/frontend/lab.html
+++ b/frontend/lab.html
@@ -400,71 +400,33 @@
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+