Quellcode durchsuchen

3.1.100/002_s4_2

ypv vor 2 Tagen
Ursprung
Commit
543aeeaf6b
2 geänderte Dateien mit 1511 neuen und 7 gelöschten Zeilen
  1. 1476 7
      ПМ.03.01/3.1.100/002/index.html
  2. 35 0
      ПМ.03.01/3.1.100/002/specification.md

+ 1476 - 7
ПМ.03.01/3.1.100/002/index.html

@@ -1,7 +1,1476 @@
-<tr valign="top" textalign="left">
-<td> 12 </td><td> Сцена 4 · модуляция </td><td> Число циклов огибающей зашито константой, ползунок F_м меняет только спектр, но не осциллограмму. Не обоснована необходимость модуляции </td><td> Окно времени фиксировано (4 мс), число циклов = F_м × T. Добавить график модулирующего сигнала. Добавить текст: антенна эффективна при λ/4, для звука 1 кГц — 75 км, для несущей 1 МГц — 75 м </td><td> 3.1.500/001_s4_3 </td>
-</tr><tr valign="top" textalign="left">
-<td colspan=5>
-<b>Что изменилось в сцене 4:</b> <code>const cycles = 4</code> заменено на <code>nCycles = Fm * T_WINDOW_MS</code> при <code>T_WINDOW_MS = 4</code>. Число циклов модулирующего сигнала и огибающей в окне теперь 4 при F_м=1 кГц и 80 при F_м=20 кГц. Число отсчётов адаптивное: <code>max(300, ceil(nCycles * 20))</code> — при 80 циклах 1600 отсчётов, шаг штриховки несущей 8, чтобы не рисовать 1600 линий. Добавлен верхний график «Модулирующий сигнал (звук)» — синус с частотой F_м, зелёный. Средний график «АМ-сигнал» — штриховка несущей (постоянная плотность) между огибающими (красные). Нижний — спектр. Перед canvas — блок с обоснованием: антенна эффективна при длине ~ λ/4, звук 1 кГц требует 75 км, несущая 1 МГц — 75 м. Canvas вырос с 260 до 420 px. Обратная связь дополнена пояснением про F_м: низкая — редкая огибающая, узкая полоса; высокая — плотная, широкая, нужна более высокая несущая.
-</td>
-</tr>
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Эфир-1: Принципы радиосвязи</title>
+<style>
+:root {
+  --paper: #f4ecd8;
+  --paper-dark: #e8dcc0;
+  --ink: #1a2a4a;
+  --ink-light: #3a4a6a;
+  --red: #b22222;
+  --green: #2a6a3a;
+  --blue: #1a4a7a;
+  --mono: 'Roboto Mono', 'Courier New', monospace;
+  --serif: 'PT Serif', Georgia, serif;
+  --sans: 'Oswald', 'Arial Narrow', sans-serif;
+}
+* { box-sizing: border-box; }
+body {
+  margin: 0;
+  background: var(--paper);
+  color: var(--ink);
+  font-family: var(--serif);
+  line-height: 1.5;
+}
+header {
+  background: var(--ink);
+  color: var(--paper);
+  padding: 12px 20px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+  gap: 8px;
+}
+header h1 {
+  font-family: var(--sans);
+  font-size: 22px;
+  margin: 0;
+  letter-spacing: 2px;
+  text-transform: uppercase;
+}
+header .sub {
+  font-family: var(--mono);
+  font-size: 12px;
+  opacity: 0.7;
+}
+#progress-bar { height: 4px; background: var(--paper-dark); }
+#progress-fill { height: 100%; background: var(--red); width: 11.1%; transition: width 0.3s; }
+main { max-width: 900px; margin: 0 auto; padding: 20px; }
+.scene { display: none; }
+.scene.active { display: block; animation: fadeIn 0.25s; }
+@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
+h2 {
+  font-family: var(--sans);
+  color: var(--red);
+  border-bottom: 2px solid var(--red);
+  padding-bottom: 6px;
+  letter-spacing: 1px;
+  text-transform: uppercase;
+  font-size: 22px;
+}
+p { margin: 8px 0; }
+.lead { font-size: 17px; color: var(--ink-light); }
+ul { padding-left: 22px; }
+canvas { display: block; width: 100%; background: var(--paper-dark); border: 1px solid var(--ink); margin: 12px 0; }
+.controls { background: var(--paper-dark); border: 1px solid var(--ink); padding: 12px 16px; margin: 12px 0; }
+.control-row { display: flex; align-items: center; gap: 12px; margin: 8px 0; flex-wrap: wrap; }
+.control-row label { font-family: var(--mono); font-size: 13px; min-width: 240px; }
+input[type=range] { flex: 1; min-width: 140px; accent-color: var(--red); }
+.value-box {
+  font-family: var(--mono);
+  background: var(--ink);
+  color: var(--paper);
+  padding: 2px 8px;
+  font-size: 13px;
+  min-width: 90px;
+  text-align: center;
+}
+.readout {
+  font-family: var(--mono);
+  font-size: 14px;
+  background: var(--ink);
+  color: var(--paper);
+  padding: 10px 14px;
+  margin: 12px 0;
+  line-height: 1.7;
+}
+.readout .hl { color: #ffd700; }
+.feedback {
+  border-left: 4px solid var(--red);
+  background: rgba(178,34,34,0.08);
+  padding: 10px 14px;
+  margin: 12px 0;
+  font-size: 15px;
+}
+.notebook {
+  background: #fffef5;
+  border: 2px solid var(--ink);
+  border-left: 6px solid var(--red);
+  padding: 16px 20px;
+  margin: 16px 0;
+}
+.notebook .nb-title {
+  font-family: var(--sans);
+  color: var(--red);
+  font-size: 16px;
+  letter-spacing: 2px;
+  margin-bottom: 8px;
+  text-transform: uppercase;
+}
+.notebook dt { font-family: var(--mono); font-weight: bold; margin-top: 8px; }
+.notebook dd { margin: 0; color: var(--ink-light); font-family: var(--mono); font-size: 13px; }
+.btn {
+  font-family: var(--sans);
+  background: var(--ink);
+  color: var(--paper);
+  border: none;
+  padding: 10px 24px;
+  font-size: 15px;
+  letter-spacing: 1px;
+  cursor: pointer;
+  text-transform: uppercase;
+  transition: background 0.2s;
+}
+.btn:hover { background: var(--red); }
+.btn.small { padding: 6px 16px; font-size: 13px; }
+.btn.active { background: var(--red); }
+.nav { display: flex; justify-content: space-between; margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--ink); }
+.legend { font-family: var(--mono); font-size: 12px; color: var(--ink-light); margin: 8px 0; }
+.cards-area { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
+.card {
+  background: var(--paper-dark);
+  border: 1px solid var(--ink);
+  padding: 8px 12px;
+  font-family: var(--mono);
+  font-size: 13px;
+  cursor: pointer;
+  user-select: none;
+  transition: all 0.15s;
+}
+.card:hover { background: #fff; }
+.card.selected { border-color: var(--red); border-width: 2px; background: #fff; box-shadow: 0 0 0 2px rgba(178,34,34,0.25); }
+.bins-area { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin: 12px 0; }
+.bin {
+  border: 2px dashed var(--ink);
+  min-height: 90px;
+  padding: 6px;
+  text-align: center;
+  cursor: pointer;
+  transition: all 0.15s;
+}
+.bin:hover { border-color: var(--red); background: rgba(178,34,34,0.05); }
+.bin .bin-title { font-family: var(--sans); font-size: 12px; letter-spacing: 1px; color: var(--red); display: block; margin-bottom: 4px; }
+.bin .bin-item { font-family: var(--mono); font-size: 10px; display: block; padding: 1px 0; color: var(--green); }
+.question { background: var(--paper-dark); border: 1px solid var(--ink); padding: 14px 18px; margin: 14px 0; }
+.question .q-num { font-family: var(--sans); color: var(--red); font-size: 14px; letter-spacing: 1px; }
+.question .q-text { font-size: 16px; margin: 4px 0 10px; }
+.question .options label { display: block; padding: 6px 10px; margin: 4px 0; cursor: pointer; font-size: 15px; border: 1px solid transparent; }
+.question .options label:hover { background: rgba(0,0,0,0.04); }
+.question .options input { margin-right: 8px; }
+.question .explain { display: none; margin-top: 10px; padding: 8px 12px; font-size: 14px; border-left: 3px solid var(--red); background: rgba(178,34,34,0.08); }
+.question .explain.show { display: block; }
+.question .explain.correct { border-color: var(--green); background: rgba(42,106,58,0.1); }
+.toggles { display: flex; gap: 12px; flex-wrap: wrap; margin: 12px 0; }
+.toggle-item {
+  background: var(--paper-dark);
+  border: 1px solid var(--ink);
+  padding: 10px 14px;
+  cursor: pointer;
+  font-family: var(--mono);
+  font-size: 13px;
+  user-select: none;
+  transition: all 0.15s;
+}
+.toggle-item.on { background: var(--green); color: var(--paper); border-color: var(--green); }
+.risk-bars { margin: 12px 0; }
+.risk-row { display: flex; align-items: center; gap: 10px; margin: 6px 0; font-family: var(--mono); font-size: 13px; }
+.risk-row .risk-label { min-width: 110px; }
+.risk-track { flex: 1; height: 16px; background: var(--paper-dark); border: 1px solid var(--ink); }
+.risk-fill { height: 100%; transition: width 0.3s, background 0.3s; }
+.checklist { background: #fffef5; border: 2px solid var(--green); padding: 16px 20px; margin: 20px 0; }
+.checklist .nb-title { font-family: var(--sans); color: var(--green); font-size: 16px; letter-spacing: 2px; margin-bottom: 8px; }
+.checklist ul { margin: 0; padding-left: 20px; }
+.checklist li { margin: 4px 0; font-family: var(--mono); font-size: 13px; }
+.score-box { font-family: var(--sans); font-size: 24px; color: var(--red); text-align: center; padding: 16px; border: 2px solid var(--red); margin: 20px 0; }
+footer { text-align: center; padding: 20px; font-family: var(--mono); font-size: 11px; color: var(--ink-light); border-top: 1px solid var(--ink); margin-top: 40px; }
+@media (max-width: 600px) {
+  .bins-area { grid-template-columns: repeat(2, 1fr); }
+  .control-row label { min-width: 140px; font-size: 12px; }
+  header h1 { font-size: 16px; }
+  main { padding: 12px; }
+}
+</style>
+</head>
+<body>
+
+<header>
+  <h1>Эфир-1 · Принципы радиосвязи</h1>
+  <div class="sub">ПМ.03.01 · Заслон · v0.1-spec</div>
+</header>
+<div id="progress-bar"><div id="progress-fill"></div></div>
+
+<main id="app">
+
+<!-- 1 -->
+<section class="scene active" data-scene="1">
+  <h2>1. Цель</h2>
+  <p class="lead">Этот интерактив — про то, как радиоволна выбирается из передатчика, что с ней происходит по дороге и почему разные диапазоны ведут себя по-разному.</p>
+  <p>После прохождения вы сможете:</p>
+  <ul>
+    <li>по частоте определить диапазон, длину волны и место на шкале ЭМ излучений;</li>
+    <li>объяснить, почему ДВ огибает холм, а Wi-Fi за тем же холмом не работает;</li>
+    <li>объяснить, почему КВ слышно за тысячи километров, а УКВ — только в прямой видимости;</li>
+    <li>выбрать диапазон под задачу (подводная связь, вещание, мобильная, спутник);</li>
+    <li>объяснить, как направленная антенна, ППРЧ и экранирование снижают риск перехвата и подавления.</li>
+  </ul>
+  <div class="nav"><span></span><button class="btn" onclick="goTo(2)">Начать →</button></div>
+</section>
+
+<!-- 2 -->
+<section class="scene" data-scene="2">
+  <h2>2. Актуализация: волна и шкала</h2>
+  <p>Колеблющийся заряд создаёт переменное электромагнитное поле, которое распространяется в пространстве со скоростью света. Частота f и длина волны λ связаны: <b>λ = c / f</b>, c = 3·10⁸ м/с.</p>
+  <p>Двигайте ползунок — частота, длина волны и энергия кванта меняются вместе.</p>
+  <canvas id="c-spectrum"></canvas>
+  <p class="legend">Видимый свет — узкая полоска в середине шкалы. Глаз видит только её.</p>
+  <div class="controls">
+    <div class="control-row">
+      <label>Частота f</label>
+      <input type="range" id="s-freq" min="3.48" max="20.48" step="0.01" value="6">
+      <span class="value-box" id="v-freq">1 МГц</span>
+    </div>
+  </div>
+  <div class="readout" id="r-spectrum"></div>
+  <div class="feedback" id="fb-spectrum"></div>
+  <div class="nav"><button class="btn" onclick="goTo(1)">← Назад</button><button class="btn" onclick="goTo(3)">Далее →</button></div>
+</section>
+
+<!-- 3 -->
+<section class="scene" data-scene="3">
+  <h2>3. Определения для тетради</h2>
+  <div class="notebook">
+    <div class="nb-title">ЗАПИШИ В ТЕТРАДЬ</div>
+    <dl>
+      <dt>Электромагнитная волна</dt>
+      <dd>Распространяющееся в пространстве переменное ЭМ поле. В вакууме c = 3·10⁸ м/с.</dd>
+      <dt>Длина волны</dt>
+      <dd>λ = c / f, [м]. Частота f, [Гц].</dd>
+      <dt>Модулирующий сигнал</dt>
+      <dd>Низкочастотный сигнал, содержащий информацию (звук, данные). Обозначение F<sub>м</sub>, [Гц].</dd>
+      <dt>Несущая</dt>
+      <dd>Высокочастотное колебание, переносящее сигнал. Обозначение f<sub>c</sub>, [Гц].</dd>
+      <dt>Модуляция</dt>
+      <dd>Изменение параметров несущей по закону модулирующего сигнала.</dd>
+      <dt>Полоса пропускания</dt>
+      <dd>Интервал частот, занимаемый сигналом. Для АМ: B = 2F<sub>м</sub>.</dd>
+      <dt>Дифракция</dt>
+      <dd>Огибание волной препятствий, соизмеримых с λ.</dd>
+      <dt>Ионосфера</dt>
+      <dd>Ионизированные слои атмосферы (D, E, F), отражающие КВ.</dd>
+      <dt>Радиогоризонт</dt>
+      <dd>d ≈ 3.57·(√h₁ + √h₂) км, h₁,h₂ — высоты антенн в метрах.</dd>
+    </dl>
+  </div>
+  <p class="legend">Переход не блокируется: не записали — вернитесь позже. Определения короткие, в тетрадь, не для чтения.</p>
+  <div class="nav"><button class="btn" onclick="goTo(2)">← Назад</button><button class="btn" onclick="goTo(4)">Далее →</button></div>
+</section>
+
+<!-- 4 -->
+<section class="scene" data-scene="4">
+  <h2>4. Принцип радиосвязи: несущая и модуляция</h2>
+  <p>Информацию переносит не сама несущая, а её изменение. Передатчик формирует высокочастотную несущую, модулятор накладывает на неё сигнал, антенна излучает. Приёмник отделяет сигнал обратно.</p>
+
+  <div class="feedback" style="border-left-color:#2a6a3a;background:rgba(42,106,58,0.08)">
+    <b>Зачем вообще модуляция?</b> Антенна эффективна, когда её длина соизмерима с λ/4. Для звука 1 кГц λ = 300 км, антенна — 75 км. Нереально. Для несущей 1 МГц λ = 300 м, антенна — 75 м. Уже можно. Модуляция сдвигает сигнал на высокую частоту, где антенна работает. Плюс: на разных несущих можно передавать разные сигналы одновременно — частотное разделение каналов.
+  </div>
+
+  <p><b>Модулирующий сигнал F<sub>м</sub></b> — низкочастотный, содержит информацию. <b>Несущая f<sub>c</sub></b> — высокочастотная, переносит её. При амплитудной модуляции амплитуда несущей меняется по закону модулирующего сигнала. Значит <b>огибающая АМ-сигнала повторяет F<sub>м</sub></b>, а заполнение под ней — это несущая f<sub>c</sub>. Глубина модуляции m — насколько сильно, полоса B = 2F<sub>м</sub>.</p>
+  <canvas id="c-mod"></canvas>
+  <div class="controls">
+    <div class="control-row">
+      <label>Модулирующий сигнал (звук) F<sub>м</sub></label>
+      <input type="range" id="s-fm" min="1" max="20" step="0.5" value="5">
+      <span class="value-box" id="v-fm">5 кГц</span>
+    </div>
+    <div class="control-row">
+      <label>Несущая f<sub>c</sub></label>
+      <input type="range" id="s-fc" min="0.5" max="5" step="0.1" value="1">
+      <span class="value-box" id="v-fc">1.0 МГц</span>
+    </div>
+    <div class="control-row">
+      <label>Глубина модуляции m</label>
+      <input type="range" id="s-m" min="0" max="1.5" step="0.01" value="0.7">
+      <span class="value-box" id="v-m">0.70</span>
+    </div>
+  </div>
+  <div class="readout" id="r-mod"></div>
+  <div class="feedback" id="fb-mod"></div>
+  <p class="legend">Верхний график — модулирующий сигнал F_м (низкочастотный, зелёный). Средний — несущая f_c (синяя), <b>то же число осцилляций</b>, что на нижнем. Нижний — АМ-сигнал: <b style="color:#b22222">красная огибающая точно повторяет верхний график (F_м)</b>, <b style="color:#1a4a7a">заполнение под ней — несущая f_c, то же число осцилляций, что на среднем</b>. При движении F_м меняется только огибающая (верхний и нижний синхронно). При движении f_c — только число осцилляций несущей (средний и нижний синхронно). Масштаб осцилляций условный (реальное отношение f_c/F_м = 200:1 нечитаемо), реальные λ — в readout.</p>
+  <div class="nav"><button class="btn" onclick="goTo(3)">← Назад</button><button class="btn" onclick="goTo(5)">Далее →</button></div>
+</section>
+
+<!-- 5 -->
+<section class="scene" data-scene="5">
+  <h2>5. Распространение: дифракция и прямая видимость</h2>
+  <p>Волна огибает препятствие, если её длина соизмерима с препятствием. Если λ много меньше — за препятствием тень.</p>
+  <p>Геометрия: передатчик и приёмник на земле, по 10 км от препятствия. Препятствие — ножевой край на 200 м выше линии прямой видимости. Меняйте λ и смотрите, что доходит до приёмника.</p>
+  <canvas id="c-diff"></canvas>
+  <div class="controls">
+    <div class="control-row">
+      <label>Длина волны λ</label>
+      <input type="range" id="s-lam" min="-1" max="5" step="0.01" value="1">
+      <span class="value-box" id="v-lam">10 м</span>
+    </div>
+  </div>
+  <div class="readout" id="r-diff"></div>
+  <div class="feedback" id="fb-diff"></div>
+  <p class="legend">Вертикальный масштаб увеличен. Модель ножевого края (ITU-R P.526); реальный холм даёт меньше потерь, но качественно то же.</p>
+  <div class="nav"><button class="btn" onclick="goTo(4)">← Назад</button><button class="btn" onclick="goTo(6)">Далее →</button></div>
+</section>
+
+<!-- 6 -->
+<section class="scene" data-scene="6">
+  <h2>6. Ионосфера: отражение и поглощение</h2>
+  <p>Почему ДВ/СВ/КВ идут за горизонт, а УКВ — нет. Меняйте частоту и время суток.</p>
+  <canvas id="c-iono"></canvas>
+  <div class="controls">
+    <div class="control-row">
+      <label>Частота f</label>
+      <input type="range" id="s-iono-f" min="5" max="10" step="0.01" value="6.5">
+      <span class="value-box" id="v-iono-f">3 МГц</span>
+    </div>
+    <div class="control-row">
+      <label>Время суток</label>
+      <button class="btn small active" id="b-day">День</button>
+      <button class="btn small" id="b-night">Ночь</button>
+    </div>
+  </div>
+  <div class="readout" id="r-iono"></div>
+  <div class="feedback" id="fb-iono"></div>
+  <p class="legend">Модель пороговая: 3 МГц и 30 МГц — качественные границы, не формула. Для инженерного расчёта — МПЧ, закон секанса, солнечная активность.</p>
+  <div class="nav"><button class="btn" onclick="goTo(5)">← Назад</button><button class="btn" onclick="goTo(7)">Далее →</button></div>
+</section>
+
+<!-- 7 -->
+<section class="scene" data-scene="7">
+  <h2>7. Тренажёр диапазонов</h2>
+  <p>Выберите карточку, затем нажмите на корзину. Верно — карточка встанет на место с пояснением. Неверно — подсказка, попробуйте снова.</p>
+  <div id="trainer"></div>
+  <div class="feedback" id="fb-trainer" style="display:none"></div>
+  <div class="nav"><button class="btn" onclick="goTo(6)">← Назад</button><button class="btn" onclick="goTo(8)">Далее →</button></div>
+</section>
+
+<!-- 8 -->
+<section class="scene" data-scene="8">
+  <h2>8. Применение и ИБ: атака и защита</h2>
+  <p>Распространение волн — это ещё и зона перехвата. Чем шире зона, тем больше возможностей у «красных».</p>
+  <canvas id="c-sec"></canvas>
+  <div class="toggles" id="sec-toggles">
+    <div class="toggle-item" data-key="antenna">Направленная антенна</div>
+    <div class="toggle-item" data-key="fhss">ППРЧ (перестройка частоты)</div>
+    <div class="toggle-item" data-key="shield">Экранирование</div>
+  </div>
+  <div class="risk-bars" id="risk-bars"></div>
+  <div class="feedback" id="fb-sec"></div>
+  <p class="legend">Связь с триадой КЦД: перехват — конфиденциальность, подавление — доступность, имитация — целостность.</p>
+  <div class="nav"><button class="btn" onclick="goTo(7)">← Назад</button><button class="btn" onclick="goTo(9)">Далее →</button></div>
+</section>
+
+<!-- 9 -->
+<section class="scene" data-scene="9">
+  <h2>9. Проверка</h2>
+  <p>Семь вопросов. После ответа — пояснение. Оценка — за первую попытку.</p>
+  <div id="test-container"></div>
+  <div class="score-box" id="score-box">Отвечено: 0 / 7</div>
+  <div style="text-align:center"><button class="btn" onclick="initTest()">Пересдать</button></div>
+  <div class="checklist" id="checklist" style="display:none">
+    <div class="nb-title">ЧЕК-ЛИСТ КОНСПЕКТА</div>
+    <ul>
+      <li>λ = c / f, c = 3·10⁸ м/с</li>
+      <li>B = 2F<sub>м</sub> — полоса АМ-сигнала</li>
+      <li>Огибающая АМ повторяет модулирующий сигнал F_м; заполнение = несущая f_c</li>
+      <li>Дифракция: λ соизмерима с препятствием → огибает</li>
+      <li>Ионосфера отражает КВ, прозрачна для УКВ</li>
+      <li>Радиогоризонт: d ≈ 3.57·(√h₁ + √h₂) км</li>
+      <li>Направленная антенна сужает зону перехвата, ППРЧ защищает от узкополосной помехи, экранирование снижает утечку</li>
+    </ul>
+  </div>
+  <div class="nav"><button class="btn" onclick="goTo(8)">← Назад</button><span></span></div>
+</section>
+
+</main>
+
+<footer>Эфир-1 · Заслон · ПМ.03.01 · Модель не прибор · v0.1-spec</footer>
+
+<script>
+'use strict';
+
+let currentScene = 1;
+let currentDraw = null;
+
+/* ---------- helpers ---------- */
+function prepCanvas(canvas, cssH) {
+  const dpr = Math.min(window.devicePixelRatio || 1, 2);
+  const W = canvas.clientWidth || 800;
+  const H = cssH;
+  if (canvas.width !== Math.round(W * dpr) || canvas.height !== Math.round(H * dpr)) {
+    canvas.width = Math.round(W * dpr);
+    canvas.height = Math.round(H * dpr);
+    canvas.style.height = H + 'px';
+  }
+  const ctx = canvas.getContext('2d');
+  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+  ctx.clearRect(0, 0, W, H);
+  return { ctx, W, H };
+}
+
+function formatFreq(f) {
+  if (f >= 1e18) return (f / 1e18).toFixed(1) + ' ЭГц';
+  if (f >= 1e15) return (f / 1e15).toFixed(1) + ' ПГц';
+  if (f >= 1e12) return (f / 1e12).toFixed(1) + ' ТГц';
+  if (f >= 1e9)  return (f / 1e9).toFixed(1) + ' ГГц';
+  if (f >= 1e6)  return (f / 1e6).toFixed(1) + ' МГц';
+  if (f >= 1e3)  return (f / 1e3).toFixed(1) + ' кГц';
+  return f.toFixed(0) + ' Гц';
+}
+
+function formatLambda(l) {
+  if (l >= 1000) return (l / 1000).toFixed(1) + ' км';
+  if (l >= 1)    return l.toFixed(1) + ' м';
+  if (l >= 0.01) return (l * 100).toFixed(1) + ' см';
+  if (l >= 0.001) return (l * 1000).toFixed(1) + ' мм';
+  if (l >= 1e-6) return (l * 1e6).toFixed(1) + ' мкм';
+  if (l >= 1e-9) return (l * 1e9).toFixed(1) + ' нм';
+  return (l * 1e12).toFixed(1) + ' пм';
+}
+
+function formatEV(eV) {
+  if (eV >= 1e6) return (eV / 1e6).toFixed(1) + ' МэВ';
+  if (eV >= 1e3) return (eV / 1e3).toFixed(1) + ' кэВ';
+  if (eV >= 1)   return eV.toFixed(2) + ' эВ';
+  if (eV >= 1e-3) return (eV * 1e3).toFixed(2) + ' мэВ';
+  return (eV * 1e6).toFixed(2) + ' мкэВ';
+}
+
+/* ---------- navigation ---------- */
+function goTo(n) {
+  currentDraw = null;
+  document.querySelectorAll('.scene').forEach(s => s.classList.remove('active'));
+  const scene = document.querySelector('.scene[data-scene="' + n + '"]');
+  if (scene) scene.classList.add('active');
+  currentScene = n;
+  document.getElementById('progress-fill').style.width = (n / 9 * 100) + '%';
+  window.scrollTo(0, 0);
+  if (n === 2) initSpectrum();
+  if (n === 4) initMod();
+  if (n === 5) initDiff();
+  if (n === 6) initIono();
+  if (n === 7) initTrainer();
+  if (n === 8) initSecurity();
+  if (n === 9) initTest();
+}
+
+window.addEventListener('resize', () => { if (currentDraw) currentDraw(); });
+
+/* ============================================================
+   SCENE 2 — EM spectrum
+   ============================================================ */
+function initSpectrum() {
+  const canvas = document.getElementById('c-spectrum');
+  const slider = document.getElementById('s-freq');
+  const logMin = 3.48, logMax = 20.48;
+
+  const bands = [
+    { from: 3.48,  to: 4.48,  color: '#3a5a85', label: 'СДВ' },
+    { from: 4.48,  to: 5.48,  color: '#4a6fa5', label: 'ДВ' },
+    { from: 5.48,  to: 6.48,  color: '#5a7fb5', label: 'СВ' },
+    { from: 6.48,  to: 7.48,  color: '#6a8fc5', label: 'КВ' },
+    { from: 7.48,  to: 9.48,  color: '#7a9fd5', label: 'УКВ' },
+    { from: 9.48,  to: 11.48, color: '#8aafe5', label: 'СВЧ' },
+    { from: 11.48, to: 14.63, color: '#d08050', label: 'ИК' },
+    { from: 14.63, to: 14.9,  color: '#e0d040', label: '' },
+    { from: 14.9,  to: 16.48, color: '#a060c0', label: 'УФ' },
+    { from: 16.48, to: 19.48, color: '#7040a0', label: 'Рентг.' },
+    { from: 19.48, to: 20.48, color: '#402060', label: 'Гамма' }
+  ];
+
+  function bandInfo(logf) {
+    if (logf < 11.48) return { name: 'Радиоволны', comment: 'Энергия кванта мала — вещество не ионизирует. Проникает сквозь диэлектрики, отражается от металла.' };
+    if (logf < 14.63) return { name: 'Инфракрасное', comment: 'Тепловое излучение. Греет, но не ионизирует.' };
+    if (logf < 14.9)  return { name: 'Видимое', comment: 'Узкая полоска — глаз видит только её. Энергия кванта ~2 эВ.' };
+    if (logf < 16.48) return { name: 'Ультрафиолет', comment: 'Начинает ионизировать. Загар, витамин D, но и ожоги.' };
+    if (logf < 19.48) return { name: 'Рентген', comment: 'Ионизирует сильно. Проникает сквозь мягкие ткани.' };
+    return { name: 'Гамма', comment: 'Ионизирует очень сильно. Требует тяжёлой защиты.' };
+  }
+
+  function draw() {
+    const { ctx, W, H } = prepCanvas(canvas, 180);
+    const logf = parseFloat(slider.value);
+    const f = Math.pow(10, logf);
+
+    const margin = 30;
+    const barY = 55, barH = 60;
+    const x0 = margin, x1 = W - margin;
+    const range = logMax - logMin;
+    const xOf = lf => x0 + (lf - logMin) / range * (x1 - x0);
+
+    ctx.fillStyle = '#1a2a4a';
+    ctx.font = '11px "Roboto Mono", monospace';
+    ctx.textAlign = 'left';
+    ctx.fillText('ШКАЛА ЭЛЕКТРОМАГНИТНЫХ ИЗЛУЧЕНИЙ (лог. масштаб)', x0, 20);
+
+    bands.forEach(b => {
+      const bx0 = xOf(b.from), bx1 = xOf(b.to);
+      ctx.fillStyle = b.color;
+      ctx.fillRect(bx0, barY, bx1 - bx0, barH);
+      if (b.label && bx1 - bx0 > 26) {
+        ctx.fillStyle = '#fff';
+        ctx.font = '10px "Roboto Mono", monospace';
+        ctx.textAlign = 'center';
+        ctx.fillText(b.label, (bx0 + bx1) / 2, barY + barH / 2 + 3);
+      }
+    });
+
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.lineWidth = 1;
+    ctx.strokeRect(x0, barY, x1 - x0, barH);
+
+    const mx = xOf(logf);
+    ctx.fillStyle = '#b22222';
+    ctx.beginPath();
+    ctx.moveTo(mx, barY - 4);
+    ctx.lineTo(mx - 7, barY - 16);
+    ctx.lineTo(mx + 7, barY - 16);
+    ctx.closePath();
+    ctx.fill();
+    ctx.fillRect(mx - 1, barY, 2, barH);
+
+    ctx.fillStyle = '#1a2a4a';
+    ctx.font = '9px "Roboto Mono", monospace';
+    ctx.textAlign = 'center';
+    [3.48, 6.48, 9.48, 12.48, 15.48, 18.48, 20.48].forEach(lf => {
+      ctx.fillText(formatFreq(Math.pow(10, lf)), xOf(lf), barY + barH + 14);
+    });
+
+    const lambda = 3e8 / f;
+    const eV = 4.136e-15 * f;
+    document.getElementById('v-freq').textContent = formatFreq(f);
+    document.getElementById('r-spectrum').innerHTML =
+      'Частота: <span class="hl">' + formatFreq(f) + '</span><br>' +
+      'Длина волны: <span class="hl">' + formatLambda(lambda) + '</span><br>' +
+      'Энергия кванта: <span class="hl">' + formatEV(eV) + '</span>';
+    const info = bandInfo(logf);
+    document.getElementById('fb-spectrum').innerHTML = '<b>' + info.name + '.</b> ' + info.comment;
+  }
+
+  currentDraw = draw;
+  slider.oninput = draw;
+  draw();
+}
+
+/* ============================================================
+   SCENE 4 — modulation
+   
+   ЛОГИКА:
+   - Модулирующий F_м (низкочастотный) → огибающая АМ.
+   - Несущая f_c (высокочастотная) → заполнение под огибающей.
+   - Число осцилляций несущей ОДНО И ТО ЖЕ на среднем и нижнем графиках.
+   - Зависит ТОЛЬКО от f_c. Масштаб условный (реальное 200:1 нечитаемо).
+   ============================================================ */
+function initMod() {
+  const canvas = document.getElementById('c-mod');
+  const sFm = document.getElementById('s-fm');
+  const sFc = document.getElementById('s-fc');
+  const sM = document.getElementById('s-m');
+
+  const T_MOD_MS = 4;  // окно времени для модулирующего и АМ, мс
+
+  function draw() {
+    const { ctx, W, H } = prepCanvas(canvas, 540);
+    const Fm = parseFloat(sFm.value);  // кГц
+    const Fc = parseFloat(sFc.value);  // МГц
+    const m = parseFloat(sM.value);
+
+    // Число циклов модулирующего в окне — только от Fm
+    const nMod = Fm * T_MOD_MS;
+
+    // Число осцилляций несущей в окне — только от Fc, ОДНО И ТО ЖЕ на обоих графиках.
+    // 30..200 — условный масштаб, чтобы было читаемо при любом f_c.
+    // Реальная f_c за 4 мс дала бы 2000–20000 периодов — нечитаемо.
+    const nOscDisplay = 30 + 170 * (Fc - 0.5) / 4.5;
+
+    const x0 = 55, x1 = W - 20;
+    const midX = (x0 + x1) / 2;
+
+    // ============================================================
+    // ГРАФИК 1 (верхний): МОДУЛИРУЮЩИЙ СИГНАЛ F_м — низкочастотный
+    // ============================================================
+    const mY = 22, mH = 80;
+    const mMid = mY + mH / 2;
+    ctx.fillStyle = '#1a2a4a';
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.textAlign = 'left';
+    ctx.fillText('МОДУЛИРУЮЩИЙ СИГНАЛ F_м = ' + Fm.toFixed(1) + ' кГц — низкочастотный, содержит информацию', x0, mY - 6);
+
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(x0, mMid);
+    ctx.lineTo(x1, mMid);
+    ctx.stroke();
+    ctx.beginPath();
+    ctx.moveTo(x0, mY);
+    ctx.lineTo(x0, mY + mH);
+    ctx.stroke();
+
+    const mSamples = Math.max(200, Math.ceil(nMod * 20));
+    ctx.strokeStyle = '#2a6a3a';
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    for (let i = 0; i <= mSamples; i++) {
+      const x = x0 + (x1 - x0) * i / mSamples;
+      const phase = (i / mSamples) * nMod * 2 * Math.PI;
+      const y = mMid - (mH / 2 - 6) * Math.sin(phase);
+      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
+    }
+    ctx.stroke();
+    ctx.lineWidth = 1;
+
+    ctx.fillStyle = '#3a4a6a';
+    ctx.font = '9px "Roboto Mono", monospace';
+    ctx.textAlign = 'right';
+    ctx.fillText('циклов: ' + nMod.toFixed(0), x1, mY - 6);
+
+    // ============================================================
+    // ГРАФИК 2 (средний): НЕСУЩАЯ f_c — высокочастотная
+    // ТО ЖЕ число осцилляций nOscDisplay, что на нижнем
+    // ============================================================
+    const cY = 145, cH = 80;
+    const cMid = cY + cH / 2;
+    ctx.fillStyle = '#1a2a4a';
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.textAlign = 'left';
+    ctx.fillText('НЕСУЩАЯ f_c = ' + Fc.toFixed(1) + ' МГц — высокочастотная, переносит сигнал', x0, cY - 6);
+
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(x0, cMid);
+    ctx.lineTo(x1, cMid);
+    ctx.stroke();
+    ctx.beginPath();
+    ctx.moveTo(x0, cY);
+    ctx.lineTo(x0, cY + cH);
+    ctx.stroke();
+
+    const cSamples = Math.max(600, Math.ceil(nOscDisplay * 16));
+    ctx.strokeStyle = '#1a4a7a';
+    ctx.lineWidth = 1.5;
+    ctx.beginPath();
+    for (let i = 0; i <= cSamples; i++) {
+      const x = x0 + (x1 - x0) * i / cSamples;
+      const phase = (i / cSamples) * nOscDisplay * 2 * Math.PI;
+      const y = cMid - (cH / 2 - 4) * Math.sin(phase);
+      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
+    }
+    ctx.stroke();
+    ctx.lineWidth = 1;
+
+    ctx.fillStyle = '#3a4a6a';
+    ctx.font = '9px "Roboto Mono", monospace';
+    ctx.textAlign = 'right';
+    ctx.fillText('осцилляций: ' + nOscDisplay.toFixed(0), x1, cY - 6);
+
+    // ============================================================
+    // ГРАФИК 3 (нижний): АМ-СИГНАЛ
+    //   огибающая = 1 + m·sin(2π·nMod·t)      → совпадает с ВЕРХНИМ (F_м)
+    //   заполнение = sin(2π·nOscDisplay·t)    → совпадает со СРЕДНИМ (f_c)
+    // ============================================================
+    const tY = 265, tH = 140;
+    const tMid = tY + tH / 2;
+    ctx.fillStyle = '#1a2a4a';
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.textAlign = 'left';
+    ctx.fillText('АМ-СИГНАЛ: огибающая = модулирующий F_м, заполнение = несущая f_c', x0, tY - 6);
+
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(x0, tMid);
+    ctx.lineTo(x1, tMid);
+    ctx.stroke();
+    ctx.beginPath();
+    ctx.moveTo(x0, tY);
+    ctx.lineTo(x0, tY + tH);
+    ctx.stroke();
+
+    const ampBase = (tH / 2 - 6) / 1.6;
+
+    // Красные огибающие ±(1 + m·sin(2π·nMod·t))
+    ctx.strokeStyle = 'rgba(178,34,34,0.9)';
+    ctx.lineWidth = 2;
+    for (let sign = -1; sign <= 1; sign += 2) {
+      ctx.beginPath();
+      for (let i = 0; i <= 400; i++) {
+        const x = x0 + (x1 - x0) * i / 400;
+        const tNorm = i / 400;
+        const env = 1 + m * Math.sin(2 * Math.PI * nMod * tNorm);
+        const y = tMid - sign * ampBase * env;
+        if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
+      }
+      ctx.stroke();
+    }
+
+    // Сам АМ-сигнал: то же nOscDisplay, что на среднем
+    const tSamples = Math.max(1200, Math.ceil(nOscDisplay * 16));
+    ctx.strokeStyle = '#1a4a7a';
+    ctx.lineWidth = 1.5;
+    ctx.beginPath();
+    for (let i = 0; i <= tSamples; i++) {
+      const x = x0 + (x1 - x0) * i / tSamples;
+      const tNorm = i / tSamples;
+      const env = 1 + m * Math.sin(2 * Math.PI * nMod * tNorm);
+      const osc = Math.sin(2 * Math.PI * nOscDisplay * tNorm);
+      const y = tMid - ampBase * env * osc;
+      if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
+    }
+    ctx.stroke();
+    ctx.lineWidth = 1;
+
+    // Подпись под графиком
+    ctx.font = '9px "Roboto Mono", monospace';
+    ctx.textAlign = 'left';
+    if (m > 1) {
+      ctx.fillStyle = '#b22222';
+      ctx.font = 'bold 11px "Roboto Mono", monospace';
+      ctx.fillText('ПЕРЕМОДУЛЯЦИЯ: огибающая пересекает ноль', x0, tY + tH + 14);
+      ctx.fillStyle = '#3a4a6a';
+      ctx.font = '9px "Roboto Mono", monospace';
+      ctx.fillText('огибающая = верхний (' + nMod.toFixed(0) + ' циклов); заполнение = средний (' + nOscDisplay.toFixed(0) + ' осц.) — синхронно', x0, tY + tH + 28);
+    } else {
+      ctx.fillStyle = '#3a4a6a';
+      ctx.fillText('огибающая = верхний (' + nMod.toFixed(0) + ' циклов); заполнение = средний (' + nOscDisplay.toFixed(0) + ' осц.) — синхронно', x0, tY + tH + 14);
+    }
+
+    // ============================================================
+    // ГРАФИК 4: Спектр
+    // ============================================================
+    const sY = 440, sH = 80;
+    const sBase = sY + sH;
+    ctx.fillStyle = '#1a2a4a';
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.textAlign = 'left';
+    ctx.fillText('СПЕКТР: несущая f_c и две боковые на f_c ± F_м', x0, sY - 6);
+
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(x0, sBase);
+    ctx.lineTo(x1, sBase);
+    ctx.stroke();
+
+    const fcX = midX;
+    const maxDev = 30;
+    const pxPerKHz = (x1 - x0) * 0.35 / maxDev;
+    const sbOff = pxPerKHz * Fm;
+
+    function drawStick(x, h, color, label) {
+      ctx.strokeStyle = color;
+      ctx.lineWidth = 2;
+      ctx.beginPath();
+      ctx.moveTo(x, sBase);
+      ctx.lineTo(x, sBase - h);
+      ctx.stroke();
+      ctx.fillStyle = color;
+      ctx.font = '10px "Roboto Mono", monospace';
+      ctx.textAlign = 'center';
+      ctx.fillText(label, x, sBase - h - 5);
+    }
+
+    drawStick(fcX, 50, '#1a4a7a', 'f_c');
+    if (m > 0) {
+      drawStick(fcX - sbOff, 50 * m / 2, '#b22222', 'f_c−F_м');
+      drawStick(fcX + sbOff, 50 * m / 2, '#b22222', 'f_c+F_м');
+
+      ctx.strokeStyle = '#2a6a3a';
+      ctx.setLineDash([3, 3]);
+      ctx.beginPath();
+      ctx.moveTo(fcX - sbOff, sBase - 60);
+      ctx.lineTo(fcX + sbOff, sBase - 60);
+      ctx.stroke();
+      ctx.setLineDash([]);
+      ctx.fillStyle = '#2a6a3a';
+      ctx.textAlign = 'center';
+      ctx.fillText('B = 2F_м = ' + (2 * Fm).toFixed(1) + ' кГц', fcX, sBase - 66);
+    }
+
+    // ============================================================
+    // Показания и обратная связь
+    // ============================================================
+    const lamC = 300 / Fc;
+    const lamMod = 3e8 / (Fm * 1e3);
+    const lamModStr = lamMod >= 1000 ? (lamMod / 1000).toFixed(1) + ' км' : lamMod.toFixed(0) + ' м';
+    const lamModQuarter = lamMod / 4 >= 1000 ? (lamMod / 4000).toFixed(1) + ' км' : (lamMod / 4).toFixed(0) + ' м';
+
+    document.getElementById('v-fm').textContent = Fm.toFixed(1) + ' кГц';
+    document.getElementById('v-fc').textContent = Fc.toFixed(1) + ' МГц';
+    document.getElementById('v-m').textContent = m.toFixed(2);
+
+    document.getElementById('r-mod').innerHTML =
+      'F_м = <span class="hl">' + Fm.toFixed(1) + ' кГц</span> (модулирующий сигнал, λ = ' + lamModStr + ', λ/4 = ' + lamModQuarter + ')<br>' +
+      'f_c = <span class="hl">' + Fc.toFixed(1) + ' МГц</span> (несущая, λ = ' + lamC.toFixed(0) + ' м, λ/4 = ' + (lamC / 4).toFixed(0) + ' м)<br>' +
+      'm = <span class="hl">' + m.toFixed(2) + '</span> · B = 2F_м = <span class="hl">' + (2 * Fm).toFixed(1) + ' кГц</span>';
+
+    let fb;
+    if (m === 0) fb = 'Несущая есть, информации нет. Глубина модуляции нулевая.';
+    else if (m <= 0.5) fb = 'Малая глубина — сигнал слабый, но искажений нет.';
+    else if (m <= 1) fb = 'Глубина в норме. Боковые растут, полоса B = 2F_м.';
+    else fb = 'Перемодуляция: огибающая пересекает ноль. Приёмник искажает.';
+
+    if (Fm <= 3) fb += ' Низкая F_м: огибающая редкая, полоса узкая, но информации мало.';
+    else if (Fm >= 15) fb += ' Высокая F_м: огибающая плотная, полоса широкая. Нужна более высокая несущая, чтобы боковые не налезли на ноль.';
+
+    if (Fc * 1e6 < 10 * Fm * 1e3) {
+      fb += ' Внимание: f_c меньше 10·F_м — боковые налезают друг на друга, огибающая на графике не читается. Это не ошибка модели, это физика.';
+    }
+
+    document.getElementById('fb-mod').textContent = fb;
+  }
+
+  currentDraw = draw;
+  sFm.oninput = draw;
+  sFc.oninput = draw;
+  sM.oninput = draw;
+  draw();
+}
+
+/* ============================================================
+   SCENE 5 — diffraction
+   ============================================================ */
+function initDiff() {
+  const canvas = document.getElementById('c-diff');
+  const slider = document.getElementById('s-lam');
+
+  function draw() {
+    const { ctx, W, H } = prepCanvas(canvas, 280);
+    const logLam = parseFloat(slider.value);
+    const lambda = Math.pow(10, logLam);
+
+    const v = 4 / Math.sqrt(lambda);
+    let L = 0;
+    if (v > -0.7) {
+      L = 6.9 + 20 * Math.log10(Math.sqrt((v - 0.1) * (v - 0.1) + 1) + v - 0.1);
+    }
+    if (!isFinite(L) || L < 0) L = 0;
+
+    const groundY = H - 20;
+    const losY = groundY - 25;
+    const txX = 50, rxX = W - 50;
+    const obsX = W / 2;
+    const obsTopY = losY - 150;
+
+    ctx.fillStyle = '#c8b890';
+    ctx.fillRect(0, groundY, W, H - groundY);
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(0, groundY);
+    ctx.lineTo(W, groundY);
+    ctx.stroke();
+
+    ctx.strokeStyle = 'rgba(26,42,74,0.3)';
+    ctx.setLineDash([4, 4]);
+    ctx.beginPath();
+    ctx.moveTo(txX, losY);
+    ctx.lineTo(rxX, losY);
+    ctx.stroke();
+    ctx.setLineDash([]);
+
+    const shadowAlpha = Math.max(0.03, Math.min(0.6, 0.6 * (1 - L / 40)));
+
+    const maxR = Math.hypot(rxX - txX, groundY - losY) + 40;
+    const steps = 44;
+    for (let r = 40; r < maxR; r += 28) {
+      for (let s = 0; s < steps; s++) {
+        const th1 = (s / steps) * Math.PI;
+        const th2 = ((s + 1) / steps) * Math.PI;
+        const x1 = txX + r * Math.cos(th1);
+        const y1 = losY - r * Math.sin(th1);
+        const x2 = txX + r * Math.cos(th2);
+        const y2 = losY - r * Math.sin(th2);
+        if (y1 > groundY || y2 > groundY) continue;
+        if (x1 < 2 || x1 > W - 2 || x2 < 2 || x2 > W - 2) continue;
+
+        let alpha = 0.5;
+        const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
+        if (mx > obsX) {
+          const yShadow = losY - 150 * (mx - txX) / (obsX - txX);
+          if (my > yShadow) alpha = shadowAlpha;
+        }
+        ctx.strokeStyle = 'rgba(26,42,74,' + alpha + ')';
+        ctx.lineWidth = 1;
+        ctx.beginPath();
+        ctx.moveTo(x1, y1);
+        ctx.lineTo(x2, y2);
+        ctx.stroke();
+      }
+    }
+
+    ctx.fillStyle = '#8a7a5a';
+    ctx.beginPath();
+    ctx.moveTo(obsX - 7, groundY);
+    ctx.lineTo(obsX, obsTopY);
+    ctx.lineTo(obsX + 7, groundY);
+    ctx.closePath();
+    ctx.fill();
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.lineWidth = 1;
+    ctx.stroke();
+
+    ctx.fillStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(txX, losY - 15);
+    ctx.lineTo(txX - 8, losY + 5);
+    ctx.lineTo(txX + 8, losY + 5);
+    ctx.closePath();
+    ctx.fill();
+    ctx.beginPath();
+    ctx.moveTo(rxX, losY - 15);
+    ctx.lineTo(rxX - 8, losY + 5);
+    ctx.lineTo(rxX + 8, losY + 5);
+    ctx.closePath();
+    ctx.fill();
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.textAlign = 'center';
+    ctx.fillText('ПРД', txX, losY + 20);
+    ctx.fillText('ПРМ', rxX, losY + 20);
+
+    const barH = 44, barW = 12;
+    const barX = rxX + 18;
+    const level = Math.max(0, Math.min(1, 1 - L / 40));
+    ctx.fillStyle = '#e8dcc0';
+    ctx.fillRect(barX, losY - barH / 2, barW, barH);
+    ctx.fillStyle = L < 10 ? '#2a6a3a' : L < 25 ? '#d4a020' : '#b22222';
+    const fillH = barH * level;
+    ctx.fillRect(barX, losY - barH / 2 + (barH - fillH), barW, fillH);
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.strokeRect(barX, losY - barH / 2, barW, barH);
+
+    ctx.fillStyle = '#1a2a4a';
+    ctx.textAlign = 'left';
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.fillText('λ = ' + formatLambda(lambda), 10, 18);
+
+    document.getElementById('v-lam').textContent = formatLambda(lambda);
+    document.getElementById('r-diff').innerHTML =
+      'λ = <span class="hl">' + formatLambda(lambda) + '</span><br>' +
+      'Параметр Френеля v = <span class="hl">' + v.toFixed(2) + '</span><br>' +
+      'Потери L = <span class="hl">' + L.toFixed(1) + ' дБ</span>';
+
+    let fb;
+    if (L < 8) fb = 'λ соизмерима с препятствием. Дифракция: волна огибает, сигнал доходит.';
+    else if (L < 18) fb = 'Частичная тень: волна огибает край, но с потерями.';
+    else fb = 'λ много меньше препятствия. Тень: за препятствием сигнала нет.';
+    document.getElementById('fb-diff').textContent = fb;
+  }
+
+  currentDraw = draw;
+  slider.oninput = draw;
+  draw();
+}
+
+/* ============================================================
+   SCENE 6 — ionosphere
+   ============================================================ */
+function initIono() {
+  const canvas = document.getElementById('c-iono');
+  const slider = document.getElementById('s-iono-f');
+  const bDay = document.getElementById('b-day');
+  const bNight = document.getElementById('b-night');
+  let isDay = true;
+
+  function updateDayBtn() {
+    bDay.classList.toggle('active', isDay);
+    bNight.classList.toggle('active', !isDay);
+  }
+
+  function draw() {
+    const { ctx, W, H } = prepCanvas(canvas, 300);
+    const logf = parseFloat(slider.value);
+    const f = Math.pow(10, logf);
+
+    const groundY = H - 20;
+    const topY = 20;
+    const altMax = 400;
+    const yOf = km => groundY - (km / altMax) * (groundY - topY);
+
+    ctx.fillStyle = '#c8b890';
+    ctx.fillRect(0, groundY, W, H - groundY);
+    ctx.strokeStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(0, groundY);
+    ctx.lineTo(W, groundY);
+    ctx.stroke();
+
+    const layers = [
+      { name: 'D', from: 60, to: 90, color: 'rgba(100,100,140,0.30)', show: isDay },
+      { name: 'E', from: 90, to: 120, color: 'rgba(100,140,100,0.30)', show: true },
+      { name: 'F', from: 150, to: 400, color: 'rgba(140,100,100,0.20)', show: true }
+    ];
+    layers.forEach(l => {
+      if (!l.show) return;
+      const y1 = yOf(l.from), y2 = yOf(l.to);
+      ctx.fillStyle = l.color;
+      ctx.fillRect(0, y2, W, y1 - y2);
+      ctx.strokeStyle = 'rgba(26,42,74,0.4)';
+      ctx.setLineDash([4, 4]);
+      ctx.beginPath();
+      ctx.moveTo(0, y2);
+      ctx.lineTo(W, y2);
+      ctx.stroke();
+      ctx.setLineDash([]);
+      ctx.fillStyle = '#1a2a4a';
+      ctx.font = '10px "Roboto Mono", monospace';
+      ctx.textAlign = 'left';
+      ctx.fillText(l.name + ' (' + l.from + '–' + l.to + ' км)', 6, (y1 + y2) / 2 + 3);
+    });
+
+    const txX = 80, rxX = W - 80;
+
+    const gwAlpha = f > 3e7 ? 0.3 : 0.85;
+    ctx.strokeStyle = 'rgba(42,106,58,' + gwAlpha + ')';
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    for (let x = txX; x <= rxX; x += 3) {
+      const t = (x - txX) / (rxX - txX);
+      const amp = 4 * (1 - t);
+      const y = groundY - 2 - amp * Math.sin((x - txX) * 0.15);
+      if (x === txX) ctx.moveTo(x, y); else ctx.lineTo(x, y);
+    }
+    ctx.stroke();
+
+    let reflectKm = 0, absorb = false;
+    if (f < 3e6) { reflectKm = 110; absorb = isDay; }
+    else if (f <= 3e7) { reflectKm = 300; }
+    else { reflectKm = 0; }
+
+    if (reflectKm > 0) {
+      const midX = (txX + rxX) / 2;
+      const peakY = yOf(reflectKm);
+      ctx.strokeStyle = absorb ? 'rgba(178,34,34,0.55)' : '#1a2a4a';
+      ctx.lineWidth = 2;
+      ctx.setLineDash(absorb ? [6, 4] : []);
+      ctx.beginPath();
+      ctx.moveTo(txX, groundY);
+      ctx.lineTo(midX, peakY);
+      ctx.lineTo(rxX, groundY);
+      ctx.stroke();
+      ctx.setLineDash([]);
+      ctx.fillStyle = '#1a2a4a';
+      ctx.beginPath();
+      ctx.arc(midX, peakY, 3, 0, Math.PI * 2);
+      ctx.fill();
+      ctx.font = '10px "Roboto Mono", monospace';
+      ctx.textAlign = 'center';
+      ctx.fillText('отражение от ' + (reflectKm > 200 ? 'F' : 'E'), midX, peakY - 8);
+    } else {
+      ctx.strokeStyle = '#1a2a4a';
+      ctx.lineWidth = 2;
+      ctx.beginPath();
+      ctx.moveTo(txX, groundY);
+      ctx.lineTo(txX + 220, topY);
+      ctx.stroke();
+      ctx.fillStyle = '#1a2a4a';
+      ctx.beginPath();
+      ctx.moveTo(txX + 220, topY);
+      ctx.lineTo(txX + 210, topY + 12);
+      ctx.lineTo(txX + 230, topY + 12);
+      ctx.closePath();
+      ctx.fill();
+      ctx.textAlign = 'left';
+      ctx.fillText('в космос', txX + 236, topY + 6);
+    }
+
+    if (isDay && f < 3e6) {
+      const y1 = yOf(60), y2 = yOf(90);
+      ctx.fillStyle = 'rgba(178,34,34,0.32)';
+      ctx.fillRect(0, y2, W, y1 - y2);
+      ctx.fillStyle = '#b22222';
+      ctx.font = 'bold 11px "Roboto Mono", monospace';
+      ctx.textAlign = 'center';
+      ctx.fillText('ПОГЛОЩЕНИЕ В D-СЛОЕ', W / 2, yOf(75) + 4);
+    }
+
+    ctx.fillStyle = '#1a2a4a';
+    ctx.beginPath();
+    ctx.moveTo(txX, groundY - 20);
+    ctx.lineTo(txX - 6, groundY);
+    ctx.lineTo(txX + 6, groundY);
+    ctx.closePath();
+    ctx.fill();
+    ctx.beginPath();
+    ctx.moveTo(rxX, groundY - 20);
+    ctx.lineTo(rxX - 6, groundY);
+    ctx.lineTo(rxX + 6, groundY);
+    ctx.closePath();
+    ctx.fill();
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.textAlign = 'center';
+    ctx.fillText('ПРД', txX, groundY + 14);
+    ctx.fillText('ПРМ', rxX, groundY + 14);
+
+    document.getElementById('v-iono-f').textContent = formatFreq(f);
+    let band, desc;
+    if (f < 3e6) {
+      band = 'ДВ/СВ';
+      desc = isDay
+        ? 'День: D-слой поглощает, дальний приём пропадает. Земная волна вдоль поверхности.'
+        : 'Ночь: D-слой исчез, отражение от E. Дальняя связь.';
+    } else if (f <= 3e7) {
+      band = 'КВ';
+      desc = 'Отражение от F-слоя, многократные скачки. Мёртвая зона.';
+    } else {
+      band = 'УКВ и выше';
+      desc = 'Проходит сквозь ионосферу. Только прямая видимость.';
+    }
+    document.getElementById('r-iono').innerHTML =
+      'f = <span class="hl">' + formatFreq(f) + '</span> · ' + band + '<br>' + desc;
+  }
+
+  bDay.onclick = () => { isDay = true; updateDayBtn(); draw(); };
+  bNight.onclick = () => { isDay = false; updateDayBtn(); draw(); };
+  slider.oninput = draw;
+  updateDayBtn();
+  currentDraw = draw;
+  draw();
+}
+
+/* ============================================================
+   SCENE 7 — range trainer
+   ============================================================ */
+const rangeData = [
+  { id: 1,  text: 'Связь с подводной лодкой',     bin: 'dv',   expl: 'ДВ/СДВ проникают в воду на десятки метров (скин-слой). УКВ в воде затухает за сантиметры.' },
+  { id: 2,  text: 'Международное КВ-вещание',      bin: 'kv',   expl: 'КВ отражается от ионосферы (слой F) и огибает Землю многократными скачками.' },
+  { id: 3,  text: 'Местная AM-радиостанция',       bin: 'sv',   expl: 'СВ — сотни кГц. Земная волна вдоль поверхности. Днём D-слой поглощает дальний приём.' },
+  { id: 4,  text: 'Wi-Fi 2.4 ГГц',                 bin: 'svch', expl: '2.4 ГГц — СВЧ. Прямая видимость в помещении, мебель огибает плохо.' },
+  { id: 5,  text: 'Мобильный телефон 900 МГц',     bin: 'ukv',  expl: '900 МГц — УКВ (дециметровый). Прямая видимость до базовой станции, дифракция за зданиями слабая.' },
+  { id: 6,  text: 'Радиорелейная линия',           bin: 'svch', expl: 'СВЧ, узкий луч, прямая видимость. Между вышками — потому что за горизонт не уходит.' },
+  { id: 7,  text: 'Спутниковое ТВ',                bin: 'svch', expl: 'СВЧ проходит сквозь ионосферу — спутник видит Землю, Земля видит спутник.' },
+  { id: 8,  text: 'Радиолокатор аэропорта',        bin: 'svch', expl: 'СВЧ, узкий луч, малая λ — высокое разрешение по дальности и углу.' },
+  { id: 9,  text: 'Телевидение ДМВ',               bin: 'ukv',  expl: 'ДМВ (дециметровые) — УКВ. Прямая видимость, вышки на холмах.' },
+  { id: 10, text: 'Навигационный радиомаяк',       bin: 'dv',   expl: 'ДВ/СВ — земная волна, устойчивый приём на большие расстояния.' }
+];
+const binNames = { dv: 'ДВ/СДВ', sv: 'СВ', kv: 'КВ', ukv: 'УКВ', svch: 'СВЧ' };
+
+function initTrainer() {
+  const container = document.getElementById('trainer');
+  const fb = document.getElementById('fb-trainer');
+  let selectedCard = null;
+  let placed = {};
+
+  function showFeedback(text, correct) {
+    if (correct) {
+      fb.textContent = 'Верно. ' + text;
+      fb.style.borderColor = '#2a6a3a';
+      fb.style.background = 'rgba(42,106,58,0.1)';
+    } else {
+      fb.textContent = 'Неверно. Вспомните: как волна распространяется в этом диапазоне? Где он на шкале?';
+      fb.style.borderColor = '#b22222';
+      fb.style.background = 'rgba(178,34,34,0.1)';
+    }
+    fb.style.display = 'block';
+  }
+
+  function render() {
+    container.innerHTML = '';
+
+    const cardsDiv = document.createElement('div');
+    cardsDiv.className = 'cards-area';
+    rangeData.forEach(card => {
+      if (placed[card.id]) return;
+      const el = document.createElement('div');
+      el.className = 'card' + (selectedCard === card.id ? ' selected' : '');
+      el.textContent = card.text;
+      el.onclick = () => {
+        selectedCard = selectedCard === card.id ? null : card.id;
+        render();
+      };
+      cardsDiv.appendChild(el);
+    });
+    container.appendChild(cardsDiv);
+
+    const binsDiv = document.createElement('div');
+    binsDiv.className = 'bins-area';
+    Object.keys(binNames).forEach(binKey => {
+      const bin = document.createElement('div');
+      bin.className = 'bin';
+      bin.innerHTML = '<span class="bin-title">' + binNames[binKey] + '</span>';
+      rangeData.filter(c => placed[c.id] === binKey).forEach(c => {
+        const item = document.createElement('span');
+        item.className = 'bin-item';
+        item.textContent = '✓ ' + c.text;
+        bin.appendChild(item);
+      });
+      bin.onclick = () => {
+        if (selectedCard === null) return;
+        const card = rangeData.find(c => c.id === selectedCard);
+        if (card.bin === binKey) {
+          placed[card.id] = binKey;
+          showFeedback(card.expl, true);
+          selectedCard = null;
+        } else {
+          showFeedback(card.expl, false);
+        }
+        render();
+      };
+      binsDiv.appendChild(bin);
+    });
+    container.appendChild(binsDiv);
+
+    if (Object.keys(placed).length === rangeData.length) {
+      const done = document.createElement('div');
+      done.className = 'feedback';
+      done.innerHTML = '<b>Все карточки разложены.</b> Почему СВЧ так часто? Высокая частота даёт широкую полосу и узкий луч, малая λ — высокое разрешение. Цена — прямая видимость и затухание в дожде.';
+      container.appendChild(done);
+    }
+  }
+
+  render();
+}
+
+/* ============================================================
+   SCENE 8 — security
+   ============================================================ */
+function initSecurity() {
+  const canvas = document.getElementById('c-sec');
+  const state = { antenna: false, fhss: false, shield: false };
+
+  function computeRisks() {
+    const intercept = 85 - (state.antenna ? 30 : 0) - (state.fhss ? 15 : 0) - (state.shield ? 10 : 0);
+    const jam = 80 - (state.fhss ? 40 : 0) - (state.antenna ? 10 : 0);
+    const leak = 70 - (state.shield ? 40 : 0) - (state.antenna ? 10 : 0);
+    const clamp = x => Math.max(10, Math.min(90, x));
+    return { intercept: clamp(intercept), jam: clamp(jam), leak: clamp(leak) };
+  }
+
+  function draw() {
+    const { ctx, W, H } = prepCanvas(canvas, 260);
+    const risks = computeRisks();
+
+    const txX = 120, txY = 130;
+    const rxX = W - 120, rxY = 130;
+
+    ctx.strokeStyle = 'rgba(26,42,74,0.3)';
+    ctx.setLineDash([4, 4]);
+    ctx.beginPath();
+    ctx.moveTo(txX, txY);
+    ctx.lineTo(rxX, rxY);
+    ctx.stroke();
+    ctx.setLineDash([]);
+
+    const intR = state.antenna ? 50 : 110;
+    ctx.fillStyle = 'rgba(178,34,34,' + (0.10 + risks.intercept / 300) + ')';
+    ctx.beginPath();
+    ctx.ellipse(txX, txY, intR, intR * 0.6, 0, 0, Math.PI * 2);
+    ctx.fill();
+    ctx.strokeStyle = 'rgba(178,34,34,0.5)';
+    ctx.setLineDash([3, 3]);
+    ctx.stroke();
+    ctx.setLineDash([]);
+
+    const jamX = W / 2, jamY = 232;
+    ctx.fillStyle = '#8a2a2a';
+    ctx.beginPath();
+    ctx.arc(jamX, jamY, 8, 0, Math.PI * 2);
+    ctx.fill();
+    ctx.fillStyle = '#1a2a4a';
+    ctx.font = '10px "Roboto Mono", monospace';
+    ctx.textAlign = 'center';
+    ctx.fillText('ПОМЕХА', jamX, jamY + 20);
+
+    if (state.fhss) {
+      ctx.strokeStyle = 'rgba(178,34,34,0.4)';
+      ctx.setLineDash([3, 3]);
+      for (let r = 20; r < 80; r += 20) {
+        ctx.beginPath();
+        ctx.arc(jamX, jamY, r, Math.PI * 1.1, Math.PI * 1.9);
+        ctx.stroke();
+      }
+      ctx.setLineDash([]);
+    } else {
+      ctx.strokeStyle = 'rgba(178,34,34,0.6)';
+      for (let r = 20; r < 100; r += 20) {
+        ctx.beginPath();
+        ctx.arc(jamX, jamY, r, Math.PI * 1.1, Math.PI * 1.9);
+        ctx.stroke();
+      }
+    }
+
+    function tower(x, y, label) {
+      ctx.fillStyle = '#1a2a4a';
+      ctx.beginPath();
+      ctx.moveTo(x, y - 25);
+      ctx.lineTo(x - 8, y + 10);
+      ctx.lineTo(x + 8, y + 10);
+      ctx.closePath();
+      ctx.fill();
+      ctx.fillRect(x - 2, y + 10, 4, 15);
+      ctx.font = '10px "Roboto Mono", monospace';
+      ctx.textAlign = 'center';
+      ctx.fillText(label, x, y + 40);
+    }
+    tower(txX, txY, 'ПРД');
+    tower(rxX, rxY, 'ПРМ');
+
+    if (state.antenna) {
+      ctx.fillStyle = 'rgba(26,42,74,0.12)';
+      ctx.beginPath();
+      ctx.moveTo(txX, txY);
+      ctx.lineTo(rxX - 40, rxY - 40);
+      ctx.lineTo(rxX - 40, rxY + 40);
+      ctx.closePath();
+      ctx.fill();
+    }
+
+    if (state.shield) {
+      ctx.strokeStyle = '#2a6a3a';
+      ctx.lineWidth = 3;
+      ctx.beginPath();
+      ctx.arc(txX, txY, 36, 0, Math.PI * 2);
+      ctx.stroke();
+      ctx.lineWidth = 1;
+      ctx.fillStyle = '#2a6a3a';
+      ctx.font = '9px "Roboto Mono", monospace';
+      ctx.fillText('ЭКРАН', txX, txY - 42);
+    }
+
+    function colorFor(v) {
+      if (v > 60) return '#b22222';
+      if (v > 35) return '#d4a020';
+      return '#2a6a3a';
+    }
+    function bar(label, value) {
+      return '<div class="risk-row"><span class="risk-label">' + label +
+        '</span><div class="risk-track"><div class="risk-fill" style="width:' + value +
+        '%;background:' + colorFor(value) + '"></div></div><span style="min-width:32px;text-align:right">' +
+        value + '</span></div>';
+    }
+    document.getElementById('risk-bars').innerHTML =
+      bar('Перехват', risks.intercept) +
+      bar('Подавление', risks.jam) +
+      bar('Утечка', risks.leak);
+
+    const fb = [];
+    if (!state.antenna && !state.fhss && !state.shield) {
+      fb.push('Все меры выключены: широкая зона перехвата, помеха накрывает сигнал, утечка через корпус.');
+    }
+    if (state.antenna) fb.push('Направленная антенна сузила зону перехвата, но требует точного наведения.');
+    if (state.fhss) fb.push('ППРЧ: сигнал скачет по частотам, узкополосная помеха промахивается. От широкополосной не спасает.');
+    if (state.shield) fb.push('Экранирование снизило утечку, но не дальность полезного сигнала.');
+    document.getElementById('fb-sec').textContent = fb.join(' ');
+  }
+
+  document.querySelectorAll('#sec-toggles .toggle-item').forEach(el => {
+    el.onclick = () => {
+      const key = el.dataset.key;
+      state[key] = !state[key];
+      el.classList.toggle('on', state[key]);
+      draw();
+    };
+  });
+
+  currentDraw = draw;
+  draw();
+}
+
+/* ============================================================
+   SCENE 9 — test
+   ============================================================ */
+const testQuestions = [
+  {
+    q: 'λ = 30 м. Какая частота и диапазон?',
+    opts: ['10 МГц, КВ', '1 МГц, СВ', '100 МГц, УКВ', '30 кГц, ДВ'],
+    correct: 0,
+    expl: 'f = c/λ = 3·10⁸ / 30 = 10⁷ Гц = 10 МГц. Это КВ (3–30 МГц).'
+  },
+  {
+    q: 'Почему ДВ слышно за горизонтом?',
+    opts: ['Ионосфера отражает', 'Дифракция: λ соизмерима с препятствиями', 'Земная волна идёт по прямой', 'Тропосферное рассеяние'],
+    correct: 1,
+    expl: 'λ на ДВ — сотни метров, соизмерима с холмами. Волна огибает препятствия (дифракция).'
+  },
+  {
+    q: 'Почему УКВ не отражается от ионосферы?',
+    opts: ['Слишком большая мощность', 'Частота выше плазменной частоты слоя', 'Ионосфера есть только ночью', 'УКВ отражается, но мы не замечаем'],
+    correct: 1,
+    expl: 'Ионосфера отражает, пока f ниже плазменной частоты слоя. Для УКВ f выше — проходит насквозь.'
+  },
+  {
+    q: 'Где полоса шире — на несущей 1 МГц или 100 МГц при той же F_м (модулирующем сигнале)?',
+    opts: ['На 1 МГц', 'На 100 МГц', 'Одинакова: B = 2F_м', 'Зависит от глубины модуляции'],
+    correct: 2,
+    expl: 'Полоса АМ-сигнала B = 2F_м зависит от модулирующего сигнала, не от несущей. Но на высокой несущей можно взять больший F_м.'
+  },
+  {
+    q: 'Какой диапазон для связи со спутником?',
+    opts: ['ДВ', 'КВ', 'УКВ', 'СВЧ'],
+    correct: 3,
+    expl: 'СВЧ проходит сквозь ионосферу. КВ отражается, УКВ частично поглощается.'
+  },
+  {
+    q: 'Что такое перемодуляция?',
+    opts: ['m = 0', 'm > 1, огибающая пересекает ноль', 'F_м > f_с', 'Полоса шире несущей'],
+    correct: 1,
+    expl: 'При m > 1 огибающая пересекает ноль. Приёмник искажает: не может восстановить сигнал.'
+  },
+  {
+    q: 'Почему днём СВ хуже слышно на дальних расстояниях?',
+    opts: ['D-слой поглощает', 'Ионосфера исчезает', 'Антенна хуже работает', 'Помехи от Солнца'],
+    correct: 0,
+    expl: 'Днём D-слой ионизирован и поглощает СВ. Ночью D исчезает — дальний приём возвращается.'
+  }
+];
+
+function initTest() {
+  const container = document.getElementById('test-container');
+  container.innerHTML = '';
+  let answered = 0;
+  let correct = 0;
+  document.getElementById('checklist').style.display = 'none';
+
+  testQuestions.forEach((q, i) => {
+    const div = document.createElement('div');
+    div.className = 'question';
+    div.innerHTML =
+      '<div class="q-num">ВОПРОС ' + (i + 1) + ' / ' + testQuestions.length + '</div>' +
+      '<div class="q-text">' + q.q + '</div>' +
+      '<div class="options">' +
+      q.opts.map((o, j) => '<label><input type="radio" name="q' + i + '" value="' + j + '"> ' + o + '</label>').join('') +
+      '</div>' +
+      '<div class="explain" id="expl-' + i + '"></div>';
+    container.appendChild(div);
+
+    div.querySelectorAll('input').forEach(inp => {
+      inp.onchange = () => {
+        if (div.dataset.answered) return;
+        div.dataset.answered = '1';
+        const val = parseInt(inp.value, 10);
+        const ok = val === q.correct;
+        const expl = document.getElementById('expl-' + i);
+        expl.innerHTML = '<b>' + (ok ? 'Верно.' : 'Неверно.') + '</b> ' + q.expl;
+        expl.className = 'explain show' + (ok ? ' correct' : '');
+        div.querySelectorAll('input').forEach(r => r.disabled = true);
+        answered++;
+        if (ok) correct++;
+        updateScore();
+      };
+    });
+  });
+
+  function updateScore() {
+    const box = document.getElementById('score-box');
+    if (answered < testQuestions.length) {
+      box.textContent = 'Отвечено: ' + answered + ' / ' + testQuestions.length;
+    } else {
+      box.textContent = 'Результат: ' + correct + ' из ' + testQuestions.length;
+      document.getElementById('checklist').style.display = 'block';
+    }
+  }
+
+  document.getElementById('score-box').textContent = 'Отвечено: 0 / ' + testQuestions.length;
+}
+
+/* ---------- init ---------- */
+goTo(1);
+</script>
+</body>
+</html>

+ 35 - 0
ПМ.03.01/3.1.100/002/specification.md

@@ -83,6 +83,41 @@ ITU-R P.526 «Propagation by diffraction» — для модели ножево
 <td> 1 </td><td> Сцена 4. Принцип радиосвязи: несущая и модуляция </td><td> Не обоснована необходимость модуляции. При изменении частоты модуляции изображение не меняется. Надо добавить ползунок изменения модулируемой частоты. </td><td> Исправь эти ошибки  </td><td> 3.1.100/002_s4_1 </td>
 </tr><tr valign="top" textalign="left">
 <td colspan=5> 
+- Сцена 4, initMod(). Константа const cycles = 4 заменена на const nCycles = Fm * T_WINDOW_MS при T_WINDOW_MS = 4. - Число циклов теперь 4 при F_м=1 кГц и 80 при F_м=20 кГц. Число отсчётов адаптивное, шаг штриховки несущей — floor(tSamples/200), чтобы при 80 циклах не рисовать 1600 линий.  
+- Добавлен верхний график «Модулирующий сигнал (звук)» — зелёный синус с частотой F_м. Теперь видно, откуда берётся огибающая.  
+- Добавлено обоснование модуляции блоком перед canvas: антенна эффективна при λ/4, звук 1 кГц требует 75 км, несущая 1 МГц — 75 м.  
+- Canvas вырос с 260 до 420 px — три графика (звук, АМ-сигнал, спектр) вместо двух.  
+- Обратная связь дополнена пояснением про F_м: низкая — редкая огибающая и узкая полоса, высокая — плотная и широкая, нужна более высокая несущая.
+
+</tr><tr valign="top" textalign="left">
+<td> 2 </td><td> Сцена 4. Принцип радиосвязи: несущая и модуляция </td><td> Надо добавить ползунок изменения модулируемой частоты - отдельно. Сейчас изменение частоты модуляции изменяет и модулируемую частоту. Это неправильно - надо и третий график под Модулируемой частотой - модулирующая частота</td><td> Исправь эти ошибки  </td><td> 3.1.100/002_s4_1 </td>
+</tr><tr valign="top" textalign="left">
+<td colspan=5> 
+- f_c и F_м — независимые ползунки. f_c в МГц (0.5–5), F_м в кГц (1–20). Один не тянет за собой другой.
+- Три графика, три разных временных окна. Верхний — несущая в окне 5 мкс: 2.5–25 периодов, видно, как растёт плотность при росте f_c. Средний — звук в окне 4 мс: 4–80 циклов. Нижний — АМ-сигнал в окне 4 мс: огибающая от F_м, штриховка несущей схематична (реальную 1 МГц на 4 мс нарисовать нельзя — 4000 периодов, мельтешит).
+- Спектр стал относительным. Ось X фиксирована ±30 кГц, боковые на ±F_м от центра. Форма не зависит от f_c — это правильно физически: B = 2F_м. f_c просто подписана на центральной палке.
+- Readout показывает λ_c и λ_звука. При f_c = 1 МГц — λ = 300 м, λ/4 = 75 м. При F_м = 5 кГц — λ = 60 км, λ/4 = 15 км. Студент видит разницу в 200 раз — вот и обоснование модуляции в цифрах, а не в тексте.
+- Крайние случаи: f_c = 0.5 МГц → 2.5 периода (минимум, читаемо). f_c = 5 МГц → 25 периодов (плотно, но 80 отсчётов на период). F_м = 1 кГц → 4 цикла. F_м = 20 кГц → 80 циклов. m = 0 → боковых нет. m = 1.5 → красная подпись. f_c/F_м = 0.5/20 = 25 — боковые далеко от нуля, предупреждение не срабатывает.
+
+
+<td> 3 </td><td> 4. Принцип радиосвязи: несущая и модуляция </td><td> На итоговом графике модулируемая и модулирующая частота перепутаны, при перемещении ползунка на итоговом графике меняется другая частота</td><td>  </td><td>3.1.100/002_s4_2 </td>
+</tr><tr valign="top" textalign="left">
+<td colspan=5> Что выдал ИИ </td> 
+
+</tr><tr valign="top" textalign="left">
+
+<td> 4 </td><td>  </td><td>Необходимо добавить сцены с Частотной моделяцией и Фазовой. </td><td>  </td><td>Коммит </td>
+</tr><tr valign="top" textalign="left">
+<td colspan=5> Что выдал ИИ </td> 
+
+</tr><tr valign="top" textalign="left">
+
+<td> 5 </td><td> Сцена 5 </td><td> Длина волны на риснке не изменяется. Надо чтобы огибание стало ясно видно, если длина волны мовопоставима с высотой препятнствия. Надо добавить ползунок, который позволяет изменять высоту препятствия</td><td>  </td><td>Коммит </td>
+</tr><tr valign="top" textalign="left">
+<td colspan=5> Что выдал ИИ </td> 
+
+</tr><tr valign="top" textalign="left">
+
 
 
 </tr></table>