Просмотр исходного кода

Загрузить файлы ''

u23-27burdyko 1 месяц назад
Родитель
Сommit
d151966509
1 измененных файлов с 2123 добавлено и 0 удалено
  1. 2123 0
      Sklakbaym2.html

+ 2123 - 0
Sklakbaym2.html

@@ -0,0 +1,2123 @@
+<!DOCTYPE html>
+<html lang="ru">
+<head>
+<meta charset="UTF-8">
+<title>КПП-3 · СКУД · Пост охраны</title>
+<style>
+  * { box-sizing: border-box; margin: 0; padding: 0; }
+  html, body { height: 100%; overflow: hidden; background: #050810;
+    color: #a8b8c8; font-family: "Courier New", monospace; font-size: 13px; }
+  .app { height: 100vh; display: flex; flex-direction: column; }
+  header { display: flex; align-items: center; gap: 20px; padding: 8px 20px;
+    background: linear-gradient(180deg, #202830, #10161e);
+    border-bottom: 1px solid #000; flex-shrink: 0; }
+  .brand { color: #ffd060; font-weight: bold; letter-spacing: 2px;
+    font-size: 14px; text-shadow: 0 0 10px rgba(255,208,96,.5); }
+  .brand small { color: #506080; font-weight: normal; letter-spacing: 1px;
+    font-size: 10px; margin-left: 8px; }
+  .model-badge { color: #80e0ff; font-size: 11px; letter-spacing: 1px;
+    border: 1px solid #2a4a6a; padding: 3px 8px; border-radius: 2px;
+    background: rgba(64,160,224,.08); }
+  .header-status { display: flex; align-items: center; gap: 10px; margin-left: auto; }
+  .clock { color: #6090c0; letter-spacing: 2px; font-size: 13px; }
+  .sys-status { display: flex; align-items: center; gap: 8px;
+    color: #40d080; font-weight: bold; font-size: 12px; letter-spacing: 1px; }
+  .sys-status .led { width: 10px; height: 10px; border-radius: 50%;
+    background: #40d080; box-shadow: 0 0 10px #40d080; }
+  .sys-status.alarm { color: #ff4040; }
+  .sys-status.alarm .led { background: #ff4040;
+    box-shadow: 0 0 15px #ff4040; animation: blink .4s infinite; }
+  .sys-status.wait { color: #ffb040; }
+  .sys-status.wait .led { background: #ffb040; box-shadow: 0 0 10px #ffb040; }
+  .sys-status.pause { color: #60a0e0; }
+  .sys-status.pause .led { background: #60a0e0; box-shadow: 0 0 10px #60a0e0; }
+  .sys-status.senior { color: #a0c8ff; }
+  .sys-status.senior .led { background: #a0c8ff; box-shadow: 0 0 10px #a0c8ff; }
+  @keyframes blink { 50% { opacity: .2; } }
+
+  .log-toggle {
+    background: linear-gradient(180deg, #1a2028, #101418);
+    color: #a0c0e0; border: 1px solid #304050;
+    border-radius: 3px; padding: 5px 12px;
+    font-family: inherit; font-size: 11px; letter-spacing: 1.2px;
+    cursor: pointer; transition: all .15s; margin-left: 8px;
+  }
+  .log-toggle:hover { border-color: #4080c0; color: #c0e0ff;
+    box-shadow: 0 0 10px rgba(96,160,224,.3); }
+  .log-toggle.has-alarm { border-color: #702020; color: #ffb0b0;
+    animation: blink 1s infinite; }
+
+  main { flex: 1; display: grid; grid-template-columns: 1fr 400px; min-height: 0; }
+  .stage { position: relative; background: #02040a; overflow: hidden; }
+  .stage canvas { display: block; width: 100%; height: 100%; }
+  aside { background: #0a0e14; border-left: 1px solid #1a2028;
+    display: flex; flex-direction: column; overflow-y: auto; }
+  aside .section { border-bottom: 1px solid #1a2028; flex-shrink: 0; }
+  aside .section h3 { padding: 8px 14px; background: #080c12; color: #6070a0;
+    font-size: 10.5px; letter-spacing: 2px; font-weight: normal;
+    border-bottom: 1px solid #1a2028; }
+  aside .content { padding: 10px 12px; }
+
+  .model-switcher { display: grid; grid-template-columns: 1fr 1fr; gap: 6px;
+    padding: 10px 12px; }
+  .model-btn {
+    background: linear-gradient(180deg, #10161e, #0a0e14);
+    border: 1px solid #2a3442; border-radius: 3px;
+    padding: 8px 8px; cursor: pointer; transition: all .15s;
+    font-family: inherit; color: #8090a8; text-align: left;
+    font-size: 10.5px; line-height: 1.35;
+  }
+  .model-btn:hover { border-color: #4080c0; background: #0f1822; color: #c0d0e0; }
+  .model-btn.active {
+    border-color: #40d080; color: #c0f0d0;
+    background: linear-gradient(180deg, #10201a, #0a1a14);
+    box-shadow: 0 0 12px rgba(64,208,128,.25), inset 0 0 8px rgba(64,208,128,.08);
+  }
+  .model-btn .mb-code { display: block; color: #ffd060; font-weight: bold;
+    font-size: 12px; letter-spacing: 1.5px; margin-bottom: 2px; }
+  .model-btn .mb-name { display: block; color: #80e0ff; font-size: 10px;
+    margin-bottom: 3px; }
+  .model-btn .mb-spec { display: block; color: #506080; font-size: 9.5px; }
+  .model-btn.active .mb-spec { color: #80a0c0; }
+
+  .skud-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 10px;
+    font-size: 11px; }
+  .skud-row { display: flex; align-items: center; gap: 6px; padding: 3px 0;
+    color: #8090a0; }
+  .skud-led { width: 9px; height: 9px; border-radius: 50%;
+    background: #1a2028; border: 1px solid #2a3442; flex-shrink: 0;
+    transition: all .1s; }
+  .skud-led.on { background: #40d080; box-shadow: 0 0 8px #40d080; border-color: #40d080; }
+  .skud-led.red { background: #ff4040; box-shadow: 0 0 8px #ff4040; border-color: #ff4040; }
+  .skud-led.yel { background: #ffb040; box-shadow: 0 0 8px #ffb040; border-color: #ffb040; }
+  .skud-led.blu { background: #60a0e0; box-shadow: 0 0 8px #60a0e0; border-color: #60a0e0; }
+  .skud-led.blink { animation: blink .5s infinite; }
+
+  .passes-compare { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
+  .pass-card { background: #060a10; border: 1px solid #202836;
+    border-radius: 3px; padding: 8px 10px; font-size: 11.5px; }
+  .pass-card .head { color: #506080; font-size: 9.5px; letter-spacing: 1.5px;
+    margin-bottom: 6px; text-transform: uppercase; }
+  .pass-card .num { color: #ffd060; font-weight: bold; font-size: 15px;
+    letter-spacing: 1px; margin-bottom: 4px; }
+  .pass-card .name { color: #c0d0e0; margin-bottom: 4px; font-size: 11px; line-height: 1.3; }
+  .pass-card .dept { color: #607890; font-size: 10px; margin-bottom: 6px; }
+  .pass-card .status { padding: 3px 6px; border-radius: 2px; font-size: 10px;
+    letter-spacing: 1px; text-align: center; display: inline-block; }
+  .pass-card .status.ok { background: rgba(64,208,128,.15); color: #40d080;
+    border: 1px solid rgba(64,208,128,.4); }
+  .pass-card .status.bad { background: rgba(255,64,64,.15); color: #ff6060;
+    border: 1px solid rgba(255,64,64,.4); }
+  .pass-card .status.empty { background: rgba(80,90,110,.1); color: #506080;
+    border: 1px dashed #303848; }
+  .decision-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
+    padding: 10px 12px; border-bottom: 1px solid #1a2028; }
+  button.big { background: linear-gradient(180deg, #1a2028, #101418);
+    color: #c0d0e0; border: 1px solid #304050;
+    border-radius: 3px; padding: 12px 10px;
+    font-family: inherit; font-size: 12.5px; letter-spacing: 1.5px;
+    cursor: pointer; transition: all .15s; font-weight: bold; }
+  button.big.allow { border-color: #206030; color: #80e0a0; }
+  button.big.allow:hover:not(:disabled) {
+    background: linear-gradient(180deg, #204830, #103020); border-color: #40d080; }
+  button.big.deny { border-color: #702020; color: #ffb0b0; }
+  button.big.deny:hover:not(:disabled) {
+    background: linear-gradient(180deg, #401818, #200808); border-color: #ff4040; }
+  button.big:disabled { opacity: .3; cursor: not-allowed; }
+  .action-buttons { padding: 10px 12px; display: flex; flex-direction: column;
+    gap: 6px; border-bottom: 1px solid #1a2028; }
+  button.ctrl { background: linear-gradient(180deg, #1a2028, #101418);
+    color: #c0d0e0; border: 1px solid #304050;
+    border-radius: 2px; padding: 8px 12px;
+    font-family: inherit; font-size: 11.5px; letter-spacing: .5px;
+    cursor: pointer; transition: all .15s; }
+  button.ctrl:hover:not(:disabled) { background: linear-gradient(180deg, #243040, #182028);
+    border-color: #4080c0; }
+  button.ctrl:disabled { opacity: .35; cursor: not-allowed; }
+  button.ctrl.primary { border-color: #4060a0; color: #a0d0ff; }
+  button.ctrl.danger { border-color: #702020; color: #ffb0b0; }
+  button.ctrl.green { border-color: #206030; color: #80e0a0; }
+  button.ctrl.yel { border-color: #706020; color: #ffd060; }
+  button.ctrl.red { border-color: #702020; color: #ffb0b0; }
+  button.ctrl.active { background: linear-gradient(180deg, #2a3a4a, #1a2028);
+    border-color: #60a0e0; color: #c0e0ff; box-shadow: 0 0 8px rgba(96,160,224,.4); }
+
+  .ctrl-panel-grid { display: grid; grid-template-columns: 1fr 1fr 1fr;
+    gap: 6px; padding: 10px 12px; }
+  .ctrl-panel-grid button { padding: 10px 4px; font-size: 11px;
+    letter-spacing: 1px; font-weight: bold; }
+  .mode-row { display: flex; gap: 6px; padding: 0 12px 10px; }
+  .mode-row button { flex: 1; padding: 7px 4px; font-size: 10.5px; }
+
+  .cond-row { display: flex; align-items: center; gap: 8px;
+    padding: 6px 0; font-size: 11.5px; color: #8090a0; }
+  .cond-row label { min-width: 60px; color: #607890; }
+  .cond-row select {
+    flex: 1; background: #060a10; color: #c0d0e0;
+    border: 1px solid #304050; border-radius: 2px;
+    padding: 5px 8px; font-family: inherit; font-size: 11.5px;
+    cursor: pointer; }
+
+  .log { overflow-y: auto; padding: 8px 12px;
+    font-size: 10.5px; line-height: 1.7; }
+  .log .entry { color: #8090a8; }
+  .log .entry.warn { color: #ffb040; }
+  .log .entry.alarm { color: #ff4040; font-weight: bold; }
+  .log .entry.ok { color: #40d080; }
+  .log .entry.info { color: #60a0e0; }
+  .log .entry.senior { color: #a0c8ff; font-weight: bold; }
+  .log .entry.skud { color: #80e0ff; }
+  .log .entry .time { color: #405060; margin-right: 6px; }
+
+  .log-full { max-height: 60vh; min-height: 300px;
+    background: #060a10; border: 1px solid #1a2028; border-radius: 3px;
+    padding: 12px 14px; font-size: 11.5px; line-height: 1.8; }
+
+  .hint { padding: 8px 12px; background: rgba(96,160,224,.08);
+    border-left: 2px solid #4080c0; color: #a0c0e0;
+    font-size: 11px; line-height: 1.5; }
+  .hint b { color: #ffd060; }
+  .modal { position: fixed; inset: 0; background: rgba(0,0,0,.85);
+    display: none; align-items: center; justify-content: center;
+    z-index: 100; padding: 20px; }
+  .modal.on { display: flex; }
+  .modal-content {
+    background: linear-gradient(180deg, #1a2230, #0d141c);
+    border: 2px solid #3a4858;
+    border-radius: 4px;
+    box-shadow: 0 0 0 1px #000, 0 20px 60px rgba(0,0,0,.9);
+    padding: 24px 28px; width: min(640px, 100%);
+    max-height: 90vh; overflow-y: auto;
+    color: #c0d0e0; font-family: "Courier New", monospace; }
+  .modal-content h2 {
+    color: #ffd060; font-size: 16px; letter-spacing: 2px;
+    margin-bottom: 20px; text-align: center;
+    text-shadow: 0 0 10px rgba(255,208,96,.4); }
+  .modal-content .row {
+    display: flex; justify-content: space-between;
+    padding: 8px 0; border-bottom: 1px solid #1a2028;
+    font-size: 12.5px; }
+  .modal-content .row:last-of-type { border-bottom: none; }
+  .modal-content .row .k { color: #8090a0; }
+  .modal-content .row .v { color: #a0d0ff; font-weight: bold; }
+  .modal-content .row .v.good { color: #40d080; }
+  .modal-content .row .v.bad { color: #ff6060; }
+  .modal-content button {
+    display: block; margin: 20px auto 0;
+    padding: 10px 30px; font-family: inherit;
+    font-size: 13px; letter-spacing: 1.5px;
+    background: linear-gradient(180deg, #1a2028, #101418);
+    color: #c0d0e0; border: 1px solid #4060a0;
+    border-radius: 3px; cursor: pointer; }
+  .modal-content button:hover { background: linear-gradient(180deg, #243040, #182028); }
+  .refuse-menu {
+    display: none; padding: 10px 12px;
+    background: rgba(200,80,80,.08);
+    border-top: 1px solid rgba(255,64,64,.3);
+    border-bottom: 1px solid #1a2028; }
+  .refuse-menu.on { display: block; }
+  .refuse-menu .title {
+    font-size: 10.5px; color: #ffb040; letter-spacing: 1.5px;
+    margin-bottom: 8px; }
+  .refuse-menu button.ctrl {
+    width: 100%; margin-bottom: 5px;
+    text-align: left; font-size: 11.5px; padding: 7px 10px; }
+  .refuse-menu button.ctrl:last-child { margin-bottom: 0; }
+</style>
+</head>
+<body>
+
+<div class="app">
+  <header>
+    <div class="brand">◤ КПП-3<small>СКУД · ПОСТ ОХРАНЫ</small></div>
+    <div class="model-badge" id="modelBadge">МОДЕЛЬ: —</div>
+    <div class="header-status">
+      <div class="clock" id="clock">--:--:--</div>
+      <div class="sys-status" id="sysStatus"><span class="led"></span><span id="sysStatusText">ПОСТ СВОБОДЕН</span></div>
+      <button class="log-toggle" id="btnLogToggle" title="Журнал событий (F2)">☰ ЖУРНАЛ</button>
+    </div>
+  </header>
+
+  <main>
+    <div class="stage"><canvas id="cv"></canvas></div>
+    <aside>
+      <div class="section">
+        <h3>МОДЕЛЬ ШЛАГБАУМА · ПЕРЕКЛЮЧАТЕЛЬ</h3>
+        <div class="model-switcher" id="modelSwitcher"></div>
+      </div>
+
+      <div class="section">
+        <h3>СТАНЦИЯ СКУД · ИНДИКАЦИЯ</h3>
+        <div class="content">
+          <div class="skud-grid">
+            <div class="skud-row"><span class="skud-led" id="ledPower"></span>ПИТАНИЕ</div>
+            <div class="skud-row"><span class="skud-led" id="ledArmState"></span>СТРЕЛА</div>
+            <div class="skud-row"><span class="skud-led" id="ledKW1"></span>КВ-1 (низ)</div>
+            <div class="skud-row"><span class="skud-led" id="ledKW2"></span>КВ-2 (верх)</div>
+            <div class="skud-row"><span class="skud-led" id="ledLoop1"></span>ПЕТЛЯ 1</div>
+            <div class="skud-row"><span class="skud-led" id="ledLoop2"></span>ПЕТЛЯ 2</div>
+            <div class="skud-row"><span class="skud-led" id="ledPhoto"></span>ФОТОЭЛЕМЕНТ</div>
+            <div class="skud-row"><span class="skud-led" id="ledImpact"></span>ДАТЧИК УДАРА</div>
+            <div class="skud-row"><span class="skud-led" id="ledRelayOpen"></span>РЕЛЕ «ОТКР»</div>
+            <div class="skud-row"><span class="skud-led" id="ledRelayClose"></span>РЕЛЕ «ЗАКР»</div>
+            <div class="skud-row"><span class="skud-led" id="ledRelayStop"></span>РЕЛЕ «СТОП»</div>
+            <div class="skud-row"><span class="skud-led" id="ledPass"></span>ПРОПУСК</div>
+          </div>
+        </div>
+      </div>
+
+      <div class="section">
+        <h3>ПУЛЬТ ОПЕРАТОРА</h3>
+        <div class="ctrl-panel-grid">
+          <button class="ctrl green" id="btnOpen">▲ ОТКР</button>
+          <button class="ctrl red"   id="btnClose">▼ ЗАКР</button>
+          <button class="ctrl yel"   id="btnStop">■ СТОП</button>
+        </div>
+        <div class="mode-row">
+          <button class="ctrl" id="btnHold">УДЕРЖАНИЕ</button>
+          <button class="ctrl" id="btnMode">РЕЖИМ: АВТО</button>
+          <button class="ctrl" id="btnResetAlarm">СБРОС АВАРИИ</button>
+        </div>
+      </div>
+
+      <div class="section">
+        <h3>СРАВНЕНИЕ ПРОПУСКОВ</h3>
+        <div class="content"><div class="passes-compare" id="passesCompare"></div></div>
+      </div>
+      <div class="hint" id="hint">Нажмите «Следующая машина».</div>
+      <div class="decision-buttons">
+        <button class="big allow" id="btnAllow" disabled>✓ РАЗРЕШИТЬ</button>
+        <button class="big deny" id="btnDeny" disabled>✕ ЗАПРЕТИТЬ</button>
+      </div>
+      <div class="refuse-menu" id="refuseMenu">
+        <div class="title">МАШИНА ЖДЁТ РЕШЕНИЯ</div>
+        <button class="ctrl" data-refuse="explain">📢 Объяснить причину отказа</button>
+        <button class="ctrl" data-refuse="senior">📞 Вызвать старшего смены</button>
+      </div>
+      <div class="action-buttons">
+        <button class="ctrl primary" id="btnNext">▸ Следующая машина</button>
+        <button class="ctrl danger" id="btnBreach">⚠ Силовой проезд</button>
+        <button class="ctrl" id="btnNote">✎ Отметить в журнале</button>
+        <button class="ctrl" id="btnEndShift">⏹ Закончить смену</button>
+        <button class="ctrl" id="btnReset">↻ Сброс</button>
+      </div>
+      <div class="section">
+        <h3>ВНЕШНИЕ УСЛОВИЯ</h3>
+        <div class="content">
+          <div class="cond-row">
+            <label>Время</label>
+            <select id="selTimeOfDay">
+              <option value="day">☀ День</option>
+              <option value="night" selected>🌙 Ночь</option>
+            </select>
+          </div>
+          <div class="cond-row">
+            <label>Погода</label>
+            <select id="selWeather">
+              <option value="clear" selected>☀ Ясно</option>
+              <option value="rain">🌧 Дождь</option>
+              <option value="snow">❄ Снег</option>
+              <option value="fog">🌫 Туман</option>
+            </select>
+          </div>
+        </div>
+      </div>
+    </aside>
+  </main>
+</div>
+
+<div class="modal" id="logModal">
+  <div class="modal-content" style="width: min(900px, 100%);">
+    <h2>ЖУРНАЛ СОБЫТИЙ · СКУД</h2>
+    <div class="log log-full" id="logContentFull"></div>
+    <div style="display:flex; gap:8px; justify-content:center; margin-top:16px;">
+      <button id="btnLogClear" style="margin:0;">ОЧИСТИТЬ</button>
+      <button id="btnLogClose" style="margin:0;">ЗАКРЫТЬ</button>
+    </div>
+  </div>
+</div>
+
+<div class="modal" id="shiftModal">
+  <div class="modal-content">
+    <h2>СМЕНА ЗАВЕРШЕНА</h2>
+    <div class="row"><span class="k">Впущено автомобилей:</span><span class="v good" id="statIn">0</span></div>
+    <div class="row"><span class="k">Отказано в проезде:</span><span class="v" id="statOut">0</span></div>
+    <div class="row"><span class="k">Верных решений:</span><span class="v good" id="statCorrect">0</span></div>
+    <div class="row"><span class="k">Ошибок оператора:</span><span class="v bad" id="statWrong">0</span></div>
+    <div class="row"><span class="k">Сработало тревог:</span><span class="v bad" id="statAlarms">0</span></div>
+    <div class="row"><span class="k">Срабатываний датчиков:</span><span class="v" id="statSensors">0</span></div>
+    <div class="row"><span class="k">Силовых проездов:</span><span class="v bad" id="statBreaches">0</span></div>
+    <div class="row"><span class="k">Вызовов старшего:</span><span class="v" id="statSenior">0</span></div>
+    <div class="row"><span class="k">Оценка смены:</span><span class="v" id="statGrade">—</span></div>
+    <button id="btnCloseShift">ЗАКРЫТЬ И СБРОСИТЬ</button>
+  </div>
+</div>
+
+<script>
+const cv = document.getElementById('cv');
+const ctx = cv.getContext('2d');
+let W = 0, H = 0;
+
+function resizeCanvas() {
+  const stage = cv.parentElement;
+  const dpr = window.devicePixelRatio || 1;
+  W = stage.clientWidth; H = stage.clientHeight;
+  cv.width = Math.floor(W * dpr); cv.height = Math.floor(H * dpr);
+  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+}
+window.addEventListener('resize', resizeCanvas);
+
+function L() {
+  return {
+    horizonY: H * 0.32,
+    topLaneY: H * 0.58, topLaneTop: H * 0.52, topLaneBot: H * 0.66,
+    botLaneY: H * 0.78, botLaneTop: H * 0.72, botLaneBot: H * 0.84,
+    boothX: W * 0.40, boothW: 150, boothH: 165, boothBaseY: H * 0.55,
+    pillarX: W * 0.72, pillarW: 30, pillarH: 260,
+    pivotX: W * 0.72, pivotY: H * 0.58 - 80,
+    armLen: 240, armH: 16
+  };
+}
+
+/* ===== МОДЕЛИ ===== */
+const BARRIER_MODELS = {
+  A: { code:'A', name:'СКОРОСТНОЙ', short:'«Быстрый»',
+       desc:'Для офисных КПП. Полный цикл ~1 с.',
+       openTime:0.9, closeTime:1.2, autoClose:3, armLen:180,
+       hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:true },
+  B: { code:'B', name:'МАГИСТРАЛЬНЫЙ', short:'«Стандарт»',
+       desc:'Для въезда на территорию. Петля + фотоэлемент.',
+       openTime:3.5, closeTime:3.5, autoClose:8, armLen:240,
+       hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:false, hasKW2:true },
+  C: { code:'C', name:'ПРОМЫШЛЕННЫЙ', short:'«Терминал»',
+       desc:'Грузовой транспорт. 2 петли + фотоэлемент + датчик удара.',
+       openTime:6, closeTime:6, autoClose:15, armLen:280,
+       hasHold:false, hasPhoto:true, hasLoop:true, hasImpact:true, hasKW2:true },
+  D: { code:'D', name:'БЮДЖЕТНЫЙ', short:'«Мини»',
+       desc:'Для внутренних КПП. Без автоматики.',
+       openTime:4, closeTime:4, autoClose:0, armLen:200,
+       hasHold:true, hasPhoto:false, hasLoop:false, hasImpact:false, hasKW2:false }
+};
+let selectedModelCode = 'B';
+
+const state = {
+  time: 0,
+  model: null,
+  barrier: { level: 0, target: 0, moving: false },
+  holdMode: false,
+  manualMode: false,
+  autoCloseTimer: 0,
+  car: null,
+  correctPass: null, presentedPass: null,
+  phase: 'idle',
+  userDecision: null, correctDecision: null,
+  alarm: false, alarmEnd: 0, sparks: [],
+  police: null, tow: null, towDriver: null, towCable: false, leaving: false,
+  timeOfDay: 'night',
+  weather: 'clear',
+  weatherParticles: [],
+  senior: null,
+  skud: {
+    power: true, kw1: true, kw2: false,
+    loop1: false, loop2: false,
+    photo: false, impact: false,
+    relayOpen: false, relayClose: false, relayStop: false,
+    passValid: false, alarm: false
+  },
+  stats: { in:0, out:0, correct:0, wrong:0, alarms:0, breaches:0, senior:0, sensors:0 }
+};
+
+/* ===== ЖУРНАЛ ===== */
+const logEntries = [];
+function logEvent(text, cls) {
+  const now = new Date();
+  const t = String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0');
+  logEntries.push({ t, text, cls: cls||'info' });
+  if (logEntries.length > 200) logEntries.shift();
+  renderLog();
+}
+function logSkud(text, cls) { logEvent('[СКУД] ' + text, cls || 'skud'); }
+function renderLog() {
+  const html = logEntries.map(e =>
+    `<div class="entry ${e.cls}"><span class="time">${e.t}</span>${e.text}</div>`
+  ).join('');
+  const el = document.getElementById('logContentFull');
+  if (el) {
+    const wasBottom = (el.scrollHeight - el.scrollTop - el.clientHeight) < 30;
+    el.innerHTML = html;
+    if (wasBottom) el.scrollTop = el.scrollHeight;
+  }
+  updateLogBadge();
+}
+function updateLogBadge() {
+  const btn = document.getElementById('btnLogToggle');
+  if (!btn) return;
+  const modal = document.getElementById('logModal');
+  const modalOpen = modal && modal.classList.contains('on');
+  const hasAlarm = logEntries.slice(-5).some(e => e.cls === 'alarm');
+  btn.classList.toggle('has-alarm', hasAlarm && !modalOpen);
+}
+function setStatus(text, cls) {
+  document.getElementById('sysStatusText').textContent = text;
+  document.getElementById('sysStatus').className = 'sys-status' + (cls ? ' ' + cls : '');
+}
+function setHint(text) { document.getElementById('hint').innerHTML = text; }
+
+/* ===== ОСВЕЩЕНИЕ ===== */
+function lighting() {
+  const isDay = state.timeOfDay === 'day';
+  const w = state.weather;
+  if (w === 'fog') return isDay ? { mode: 'dawn' } : { mode: 'dusk' };
+  if (w === 'rain') return isDay ? { mode: 'overcast' } : { mode: 'wetnight' };
+  if (w === 'snow') return isDay ? { mode: 'overcast' } : { mode: 'snownight' };
+  return isDay ? { mode: 'day' } : { mode: 'night' };
+}
+function skyColors() {
+  const m = lighting().mode;
+  switch (m) {
+    case 'day':       return ['#3d5d80', '#98c4dc', '#d8e8f2'];
+    case 'dawn':      return ['#2a3a5c', '#7a94b4', '#c4b8b0'];
+    case 'dusk':      return ['#1a1428', '#382a48', '#605060'];
+    case 'overcast':  return ['#48505c', '#6a7280', '#8a94a4'];
+    case 'night':     return ['#02040a', '#0a1220', '#182230'];
+    case 'wetnight':  return ['#060a14', '#0e1626', '#1a2032'];
+    case 'snownight': return ['#0a0e1a', '#1a2234', '#2a3044'];
+  }
+  return ['#02040a', '#0a1220', '#182230'];
+}
+function isSunVisible() {
+  const m = lighting().mode;
+  return m === 'day' || m === 'dawn';
+}
+function isStarVisible() {
+  const m = lighting().mode;
+  return m === 'night' || m === 'wetnight' || m === 'snownight' || m === 'dusk';
+}
+function isWet() {
+  return state.weather === 'rain';
+}
+function weatherLabel() {
+  const w = { clear:'ясно', rain:'дождь', snow:'снег', fog:'туман' }[state.weather];
+  const t = state.timeOfDay === 'night' ? 'ночь' : 'день';
+  return t + ', ' + w;
+}
+
+/* ===== ПРОПУСКА ===== */
+const firstNames = ['Иванов','Петров','Сидоров','Кузнецов','Морозов','Волков','Соколов','Лебедев'];
+const initials = ['И.И.','П.П.','С.С.','А.В.','Д.А.','М.Н.'];
+const departments = ['Администрация','Служба охраны','Бухгалтерия','Отдел кадров','ИТ-отдел','Логистика'];
+const series = ['А','Б','В','Г'];
+function randomPass() {
+  return {
+    num: series[Math.floor(Math.random()*series.length)]+'-'+String(Math.floor(Math.random()*900)+100),
+    name: firstNames[Math.floor(Math.random()*firstNames.length)]+' '+initials[Math.floor(Math.random()*initials.length)],
+    dept: departments[Math.floor(Math.random()*departments.length)],
+    valid: Math.random() > 0.25
+  };
+}
+function generateScenario(mode) {
+  const correct = randomPass(); correct.valid = true;
+  if (mode === 'breach') return { correct, presented: null, shouldAllow: false };
+  const r = Math.random();
+  let presented, shouldAllow;
+  if (r < 0.4) { presented = { ...correct }; shouldAllow = true; }
+  else if (r < 0.65) { presented = randomPass(); while (presented.num === correct.num) presented = randomPass(); shouldAllow = false; }
+  else { presented = { ...correct, valid: false }; shouldAllow = false; }
+  return { correct, presented, shouldAllow };
+}
+function renderPasses() {
+  const el = document.getElementById('passesCompare');
+  const c = state.correctPass, p = state.presentedPass;
+  const card = (head, pass) => {
+    if (!pass) return `<div class="pass-card"><div class="head">${head}</div><div class="num">—</div><div class="name">не предъявлен</div><div class="dept">—</div><div class="status empty">НЕТ ДАННЫХ</div></div>`;
+    return `<div class="pass-card"><div class="head">${head}</div><div class="num">${pass.num}</div><div class="name">${pass.name}</div><div class="dept">${pass.dept}</div><div class="status ${pass.valid?'ok':'bad'}">${pass.valid?'АКТИВЕН':'ОТОЗВАН'}</div></div>`;
+  };
+  el.innerHTML = card('В БАЗЕ', c) + card('ПРЕДЪЯВЛЕН', p);
+  const active = state.phase === 'waiting' && state.userDecision === null;
+  document.getElementById('btnAllow').disabled = !active;
+  document.getElementById('btnDeny').disabled = !active;
+  state.skud.passValid = !!(c && c.valid);
+}
+function showRefuseMenu() { document.getElementById('refuseMenu').classList.add('on'); }
+function hideRefuseMenu() { document.getElementById('refuseMenu').classList.remove('on'); }
+
+function speedMult() {
+  if (state.weather === 'rain') return 0.75;
+  if (state.weather === 'snow') return 0.6;
+  if (state.weather === 'fog')  return 0.85;
+  return 1;
+}
+
+/* ===== ПЕРЕКЛЮЧАТЕЛЬ МОДЕЛИ ===== */
+function renderModelSwitcher() {
+  const el = document.getElementById('modelSwitcher');
+  el.innerHTML = Object.values(BARRIER_MODELS).map(m => `
+    <button class="model-btn ${m.code===selectedModelCode?'active':''}" data-model="${m.code}">
+      <span class="mb-code">МОДЕЛЬ ${m.code}</span>
+      <span class="mb-name">${m.name}</span>
+      <span class="mb-spec">▸ ${m.openTime}с/${m.closeTime}с · стрела ${m.armLen}</span>
+    </button>`).join('');
+  el.querySelectorAll('.model-btn').forEach(b => {
+    b.onclick = () => switchModel(b.dataset.model);
+  });
+}
+function switchModel(code) {
+  if (!BARRIER_MODELS[code]) return;
+  selectedModelCode = code;
+  state.model = BARRIER_MODELS[code];
+  document.getElementById('modelBadge').textContent = 'МОДЕЛЬ: ' + state.model.code + ' · ' + state.model.name;
+  document.getElementById('btnHold').style.display = state.model.hasHold ? '' : 'none';
+  document.getElementById('btnHold').classList.remove('active');
+  state.holdMode = false;
+  state.skud.kw1 = state.barrier.level < 0.05;
+  state.skud.kw2 = state.barrier.level > 0.95;
+  if (!state.model.hasLoop) state.skud.loop1 = false;
+  if (!state.model.hasPhoto) state.skud.photo = false;
+  if (!state.model.hasImpact) state.skud.impact = false;
+  renderModelSwitcher();
+  logSkud('▸ ПЕРЕКЛЮЧЕНИЕ МОДЕЛИ: ' + state.model.code + ' · ' + state.model.name, 'warn');
+  logSkud('Открытие/закрытие: ' + state.model.openTime + 'с / ' + state.model.closeTime + 'с');
+  logSkud('Автозакрытие: ' + (state.model.autoClose ? state.model.autoClose + ' с' : 'отключено'));
+  setHint('Модель <b>' + state.model.code + '</b> активна. Нажмите «Следующая машина».');
+}
+
+/* ===== СЦЕНАРИИ ===== */
+function nextCar() {
+  if (state.phase !== 'idle') return;
+  const scen = generateScenario('normal');
+  state.correctPass = scen.correct;
+  state.presentedPass = scen.presented;
+  state.correctDecision = scen.shouldAllow ? 'allow' : 'deny';
+  state.userDecision = null;
+  state.senior = null;
+  state.phase = 'approaching';
+  hideRefuseMenu();
+  const colors = ['#3a5a8a','#8a3a3a','#2a2a30','#3a6a4a','#7a6a2a','#5a3a6a'];
+  state.car = {
+    x: -250, speed: 160 * speedMult(),
+    color: colors[Math.floor(Math.random()*colors.length)],
+    wheelAngle: 0, isPriora: false
+  };
+  state.skud.loop1 = false; state.skud.loop2 = false;
+  setStatus('ПРИБЛИЖАЕТСЯ АВТО', 'wait');
+  logEvent('Приближается автомобиль' + (state.weather !== 'clear' || state.timeOfDay === 'night' ? ` (${weatherLabel()})` : ''), 'info');
+  renderPasses();
+  setHint('Автомобиль подъезжает.');
+}
+function breachCar() {
+  if (state.phase !== 'idle') return;
+  const scen = generateScenario('breach');
+  state.correctPass = scen.correct;
+  state.presentedPass = null;
+  state.correctDecision = 'deny';
+  state.userDecision = null;
+  state.senior = null;
+  state.phase = 'approaching';
+  hideRefuseMenu();
+  state.car = { x: -250, speed: 340 * speedMult(), color: '#0a0a0a', wheelAngle: 0, isPriora: true };
+  setStatus('НАРУШИТЕЛЬ !', 'alarm');
+  logEvent('Тёмный автомобиль не сбрасывает скорость!', 'alarm');
+  renderPasses();
+  setHint('Нарушитель не останавливается.');
+}
+
+function decide(decision) {
+  if (state.phase !== 'waiting' || state.userDecision !== null) return;
+  state.userDecision = decision;
+  const correct = (decision === state.correctDecision);
+  if (correct) state.stats.correct++; else state.stats.wrong++;
+  logEvent(`Решение «${decision==='allow'?'РАЗРЕШИТЬ':'ЗАПРЕТИТЬ'}» — ${correct?'верно':'ОШИБКА'}`,
+           correct?'ok':'warn');
+  if (decision === 'allow') {
+    state.car.action = 'pass';
+    openBarrier();
+    state.phase = 'resolved';
+    setStatus('ПРОЕЗД РАЗРЕШЁН');
+    setHint('Шлагбаум открывается, автомобиль проезжает.');
+    hideRefuseMenu();
+  } else {
+    state.car.action = 'wait';
+    state.phase = 'refuse_wait';
+    setStatus('МАШИНА ЖДЁТ', 'pause');
+    setHint('Объясните причину отказа или вызовите старшего смены.');
+    showRefuseMenu();
+  }
+  renderPasses();
+}
+
+/* ===== ПУЛЬТ ОПЕРАТОРА ===== */
+function openBarrier() {
+  if (state.skud.alarm && !state.manualMode) { logSkud('Отказ: авария не сброшена', 'warn'); return; }
+  state.barrier.target = 1;
+  state.barrier.moving = true;
+  state.skud.relayOpen = true;
+  state.skud.relayClose = false;
+  state.skud.relayStop = false;
+  logSkud('РЕЛЕ «ОТКРЫТЬ»: вкл');
+  state.autoCloseTimer = 0;
+}
+function closeBarrier() {
+  if (state.skud.alarm && !state.manualMode) { logSkud('Отказ: авария не сброшена', 'warn'); return; }
+  if (state.skud.photo) {
+    logSkud('Отказ закрытия: фотоэлемент разорван', 'warn');
+    setHint('Фотоэлемент разорван — стрела не опускается.');
+    return;
+  }
+  if (state.holdMode) { logSkud('Отказ: включено удержание', 'warn'); return; }
+  state.barrier.target = 0;
+  state.barrier.moving = true;
+  state.skud.relayClose = true;
+  state.skud.relayOpen = false;
+  state.skud.relayStop = false;
+  logSkud('РЕЛЕ «ЗАКРЫТЬ»: вкл');
+}
+function stopBarrier() {
+  state.barrier.target = state.barrier.level;
+  state.barrier.moving = false;
+  state.skud.relayStop = true;
+  state.skud.relayOpen = false;
+  state.skud.relayClose = false;
+  logSkud('РЕЛЕ «СТОП»: вкл — привод остановлен', 'warn');
+  setHint('Шлагбаум остановлен оператором.');
+}
+function resetAlarm() {
+  state.skud.alarm = false;
+  state.alarm = false;
+  state.skud.impact = false;
+  logSkud('СБРОС АВАРИИ выполнен', 'ok');
+}
+document.getElementById('btnOpen').onclick = openBarrier;
+document.getElementById('btnClose').onclick = closeBarrier;
+document.getElementById('btnStop').onclick = stopBarrier;
+document.getElementById('btnResetAlarm').onclick = resetAlarm;
+document.getElementById('btnHold').onclick = () => {
+  if (!state.model.hasHold) return;
+  state.holdMode = !state.holdMode;
+  document.getElementById('btnHold').classList.toggle('active', state.holdMode);
+  logSkud('УДЕРЖАНИЕ: ' + (state.holdMode ? 'включено' : 'выключено'), state.holdMode ? 'warn' : 'info');
+};
+document.getElementById('btnMode').onclick = () => {
+  state.manualMode = !state.manualMode;
+  document.getElementById('btnMode').textContent = 'РЕЖИМ: ' + (state.manualMode ? 'РУЧНОЙ' : 'АВТО');
+  document.getElementById('btnMode').classList.toggle('active', state.manualMode);
+  logSkud('Режим: ' + (state.manualMode ? 'РУЧНОЙ' : 'АВТО'));
+};
+
+document.querySelectorAll('[data-refuse]').forEach(b => {
+  b.onclick = () => {
+    const action = b.dataset.refuse;
+    if (action === 'explain') {
+      logEvent('Оператор объяснил причину отказа водителю', 'info');
+      state.stats.out++;
+      state.car.action = 'turn';
+      state.phase = 'resolved';
+      setStatus('ОТКАЗ', 'wait');
+      setHint('Машина уезжает в обратном направлении.');
+      hideRefuseMenu();
+      renderPasses();
+    }
+    if (action === 'senior') {
+      state.stats.senior++;
+      logEvent('Оператор вызвал старшего смены', 'senior');
+      state.phase = 'senior_walk';
+      const carX = state.car ? state.car.x : 0;
+      state.senior = { x: W + 40, y: L().botLaneY, targetX: carX + 130, targetY: L().topLaneY,
+        state: 'walk_to_lane', walking: true, alpha: 0, decideTimer: 0 };
+      hideRefuseMenu();
+      setStatus('СТАРШИЙ ВЫЗВАН', 'senior');
+      setHint('Старший смены направляется к машине.');
+    }
+  };
+});
+
+function resetAll() {
+  state.car = null; state.correctPass = null; state.presentedPass = null;
+  state.userDecision = null; state.correctDecision = null;
+  state.phase = 'idle'; state.barrier.target = 0; state.barrier.level = 0;
+  state.alarm = false; state.sparks = []; state.police = null;
+  state.tow = null; state.towDriver = null; state.towCable = false; state.leaving = false;
+  state.senior = null; state.holdMode = false; state.autoCloseTimer = 0;
+  state.skud = { power:true, kw1:true, kw2:false, loop1:false, loop2:false,
+    photo:false, impact:false, relayOpen:false, relayClose:false, relayStop:false,
+    passValid:false, alarm:false };
+  document.getElementById('btnHold').classList.remove('active');
+  hideRefuseMenu();
+  logEntries.length = 0; renderLog(); renderPasses();
+  setStatus('ПОСТ СВОБОДЕН'); setHint('Нажмите <b>«Следующая машина»</b>.');
+  logEvent('Система сброшена', 'ok');
+}
+document.getElementById('btnNext').onclick = nextCar;
+document.getElementById('btnBreach').onclick = breachCar;
+document.getElementById('btnReset').onclick = resetAll;
+document.getElementById('btnAllow').onclick = () => decide('allow');
+document.getElementById('btnDeny').onclick = () => decide('deny');
+document.getElementById('btnNote').onclick = () => {
+  const txt = prompt('Введите отметку для журнала:', '');
+  if (txt && txt.trim()) logEvent('[ОПЕРАТОР] ' + txt.trim(), 'info');
+};
+document.getElementById('btnEndShift').onclick = () => {
+  const s = state.stats;
+  document.getElementById('statIn').textContent = s.in;
+  document.getElementById('statOut').textContent = s.out;
+  document.getElementById('statCorrect').textContent = s.correct;
+  document.getElementById('statWrong').textContent = s.wrong;
+  document.getElementById('statAlarms').textContent = s.alarms;
+  document.getElementById('statSensors').textContent = s.sensors;
+  document.getElementById('statBreaches').textContent = s.breaches;
+  document.getElementById('statSenior').textContent = s.senior;
+  const total = s.correct + s.wrong;
+  let grade = '—';
+  if (total > 0) {
+    const pct = s.correct / total;
+    if (pct >= 0.95) grade = 'ОТЛИЧНО';
+    else if (pct >= 0.8) grade = 'ХОРОШО';
+    else if (pct >= 0.6) grade = 'УДОВЛЕТВОРИТЕЛЬНО';
+    else grade = 'ТРЕБУЕТСЯ ПОДГОТОВКА';
+  }
+  document.getElementById('statGrade').textContent = grade;
+  document.getElementById('shiftModal').classList.add('on');
+  logEvent('Смена завершена. Открыт отчёт.', 'info');
+};
+document.getElementById('btnCloseShift').onclick = () => {
+  document.getElementById('shiftModal').classList.remove('on');
+  state.stats = { in:0, out:0, correct:0, wrong:0, alarms:0, breaches:0, senior:0, sensors:0 };
+  logEntries.length = 0; renderLog();
+  logEvent('Начата новая смена', 'ok');
+};
+
+/* ===== ЖУРНАЛ: кнопки ===== */
+document.getElementById('btnLogToggle').onclick = () => {
+  document.getElementById('logModal').classList.add('on');
+  const el = document.getElementById('logContentFull');
+  el.scrollTop = el.scrollHeight;
+  updateLogBadge();
+};
+document.getElementById('btnLogClose').onclick = () => {
+  document.getElementById('logModal').classList.remove('on');
+  updateLogBadge();
+};
+document.getElementById('btnLogClear').onclick = () => {
+  logEntries.length = 0;
+  renderLog();
+  logEvent('Журнал очищен оператором', 'info');
+};
+document.addEventListener('keydown', (e) => {
+  if (e.key === 'F2') {
+    e.preventDefault();
+    document.getElementById('btnLogToggle').click();
+  }
+  if (e.key === 'Escape') {
+    document.getElementById('logModal').classList.remove('on');
+    updateLogBadge();
+  }
+});
+
+document.getElementById('selTimeOfDay').onchange = (e) => {
+  state.timeOfDay = e.target.value;
+  logEvent('Время суток: ' + (state.timeOfDay === 'day' ? 'день' : 'ночь'), 'info');
+  if (state.timeOfDay === 'night') logEvent('Камеры переведены в ИК-режим', 'info');
+};
+document.getElementById('selWeather').onchange = (e) => {
+  state.weather = e.target.value;
+  const names = { clear:'ясно', rain:'дождь', snow:'снег', fog:'туман' };
+  logEvent('Погода: ' + names[state.weather], 'info');
+  if (state.weather === 'fog') logEvent('Туман — сумеречный режим', 'warn');
+  if (state.weather === 'rain') logEvent('Мокрое покрытие — снижение скорости ТС', 'warn');
+  if (state.weather === 'snow') logEvent('Снегопад — ограничение скорости ТС', 'warn');
+  state.weatherParticles = [];
+};
+
+/* ===== LED ===== */
+function setLed(id, on, cls) {
+  const el = document.getElementById(id);
+  if (!el) return;
+  el.className = 'skud-led' + (on ? ' ' + (cls || 'on') : '');
+}
+function updateSkudLeds() {
+  const s = state.skud;
+  setLed('ledPower', s.power);
+  setLed('ledArmState', true,
+    state.barrier.moving ? 'yel blink' :
+    (state.barrier.level > 0.9 ? 'on' : 'red'));
+  setLed('ledKW1', s.kw1);
+  setLed('ledKW2', s.kw2);
+  setLed('ledLoop1', s.loop1, 'blu');
+  setLed('ledLoop2', s.loop2, 'blu');
+  setLed('ledPhoto', s.photo, 'yel');
+  setLed('ledImpact', s.impact, 'red blink');
+  setLed('ledRelayOpen', s.relayOpen);
+  setLed('ledRelayClose', s.relayClose);
+  setLed('ledRelayStop', s.relayStop, 'yel');
+  setLed('ledPass', s.passValid);
+}
+
+/* ===== ФОН ===== */
+function drawSky() {
+  const l = L();
+  const [c1, c2, c3] = skyColors();
+  const grad = ctx.createLinearGradient(0, 0, 0, l.horizonY);
+  grad.addColorStop(0, c1); grad.addColorStop(0.6, c2); grad.addColorStop(1, c3);
+  ctx.fillStyle = grad; ctx.fillRect(0, 0, W, l.horizonY);
+
+  if (isStarVisible()) {
+    for (let i = 0; i < 90; i++) {
+      const x = (i * 173.5) % W, y = (i * 97.3) % (l.horizonY - 15);
+      const a = (0.15 + 0.4 * Math.abs(Math.sin(state.time * 0.5 + i))) *
+                (state.weather === 'clear' ? 1 : 0.35);
+      ctx.fillStyle = `rgba(200,220,255,${a})`;
+      ctx.fillRect(x, y, 1.5, 1.5);
+    }
+    const mx = W * 0.78, my = l.horizonY * 0.30;
+    const moonG = ctx.createRadialGradient(mx, my, 3, mx, my, 55);
+    moonG.addColorStop(0, 'rgba(220,230,255,.55)');
+    moonG.addColorStop(0.4, 'rgba(180,200,230,.10)');
+    moonG.addColorStop(1, 'rgba(180,200,230,0)');
+    ctx.fillStyle = moonG; ctx.fillRect(mx - 60, my - 60, 120, 120);
+    ctx.beginPath(); ctx.arc(mx, my, 12, 0, Math.PI * 2);
+    ctx.fillStyle = 'rgba(240,240,220,.85)'; ctx.fill();
+    ctx.beginPath(); ctx.arc(mx - 4, my - 3, 10, 0, Math.PI * 2);
+    ctx.fillStyle = 'rgba(230,235,220,.75)'; ctx.fill();
+  }
+
+  if (isSunVisible()) {
+    const isDawn = lighting().mode === 'dawn';
+    const sy = l.horizonY * (isDawn ? 0.55 : 0.32);
+    const sx = W * 0.22;
+    const sg = ctx.createRadialGradient(sx, sy, 6, sx, sy, 130);
+    sg.addColorStop(0, isDawn ? 'rgba(255,220,180,.9)' : 'rgba(255,250,210,.95)');
+    sg.addColorStop(0.3, isDawn ? 'rgba(255,200,140,.25)' : 'rgba(255,240,180,.25)');
+    sg.addColorStop(1, 'rgba(255,240,180,0)');
+    ctx.fillStyle = sg; ctx.fillRect(sx - 140, sy - 140, 280, 280);
+    ctx.beginPath(); ctx.arc(sx, sy, 22, 0, Math.PI * 2);
+    ctx.fillStyle = isDawn ? 'rgba(255,220,180,.95)' : 'rgba(255,252,220,.98)'; ctx.fill();
+    ctx.save();
+    ctx.globalCompositeOperation = 'lighter';
+    for (let i = 0; i < 8; i++) {
+      const a = -0.4 + i * 0.1 + Math.sin(state.time * 0.3 + i) * 0.02;
+      const len = 180 + (i % 3) * 40;
+      ctx.beginPath();
+      ctx.moveTo(sx, sy);
+      ctx.lineTo(sx + Math.cos(a) * len, sy + Math.sin(a) * len);
+      ctx.lineTo(sx + Math.cos(a + 0.04) * len, sy + Math.sin(a + 0.04) * len);
+      ctx.closePath();
+      ctx.fillStyle = isDawn ? 'rgba(255,220,180,.06)' : 'rgba(255,250,220,.05)';
+      ctx.fill();
+    }
+    ctx.restore();
+  }
+
+  const m = lighting().mode;
+  let bcol = '#0a1220';
+  if (m === 'day') bcol = '#3a4a5a';
+  else if (m === 'overcast') bcol = '#454e58';
+  else if (m === 'dawn') bcol = '#3a4050';
+  else if (m === 'dusk') bcol = '#1c1a28';
+  ctx.fillStyle = bcol; ctx.globalAlpha = 0.55;
+  for (let i = 0; i < 30; i++) {
+    const bx = i * (W / 30), bw = 20 + (i % 5) * 14, bh = 20 + (i % 7) * 12;
+    ctx.fillRect(bx, l.horizonY - bh, bw, bh);
+  }
+  ctx.globalAlpha = 1;
+  for (let i = 0; i < 24; i++) {
+    const bx = i * (W / 24), bw = 18 + (i % 4) * 12, bh = 30 + (i % 5) * 12;
+    ctx.fillRect(bx, l.horizonY - bh, bw, bh);
+    if (isStarVisible() && (i * 13) % 5 === 0) {
+      ctx.fillStyle = 'rgba(255,200,120,.35)';
+      ctx.fillRect(bx + 4, l.horizonY - bh + 8, 3, 3);
+      ctx.fillRect(bx + 12, l.horizonY - bh + 20, 3, 3);
+      ctx.fillStyle = bcol;
+    }
+  }
+}
+
+function drawGround() {
+  const l = L();
+  const m = lighting().mode;
+  let g;
+  if (m === 'day' || m === 'overcast') {
+    g = ctx.createLinearGradient(0, l.horizonY, 0, H);
+    g.addColorStop(0, '#2a3a28'); g.addColorStop(1, '#101810');
+  } else if (m === 'dawn' || m === 'dusk') {
+    g = ctx.createLinearGradient(0, l.horizonY, 0, H);
+    g.addColorStop(0, '#1a2018'); g.addColorStop(1, '#0a0e08');
+  } else {
+    g = ctx.createLinearGradient(0, l.horizonY, 0, H);
+    g.addColorStop(0, '#0a1410'); g.addColorStop(1, '#03060a');
+  }
+  ctx.fillStyle = g; ctx.fillRect(0, l.horizonY, W, H - l.horizonY);
+
+  const haze = ctx.createLinearGradient(0, l.horizonY - 4, 0, l.horizonY + 30);
+  haze.addColorStop(0, 'rgba(80,110,140,.35)');
+  haze.addColorStop(1, 'rgba(80,110,140,0)');
+  ctx.fillStyle = haze;
+  ctx.fillRect(0, l.horizonY - 4, W, 34);
+}
+
+function drawRoads() {
+  const l = L();
+  const m = lighting().mode;
+  let c1, c2, edge, dash;
+  if (m === 'day') {
+    c1 = ['#3a3e44', '#25282c']; c2 = ['#3a3e44', '#25282c'];
+    edge = 'rgba(180,180,180,.7)'; dash = 'rgba(220,200,140,.6)';
+  } else if (m === 'overcast' || m === 'dawn') {
+    c1 = ['#2a2e34', '#1a1d22']; c2 = ['#2a2e34', '#1a1d22'];
+    edge = 'rgba(140,150,160,.6)'; dash = 'rgba(200,180,140,.5)';
+  } else if (m === 'dusk') {
+    c1 = ['#1a1d24', '#0d1014']; c2 = ['#1a1d24', '#0d1014'];
+    edge = 'rgba(120,130,140,.5)'; dash = 'rgba(200,180,140,.4)';
+  } else {
+    c1 = ['#1a1e24', '#0d1014']; c2 = ['#1a1e24', '#0d1014'];
+    edge = 'rgba(120,130,140,.5)'; dash = 'rgba(220,200,140,.4)';
+  }
+  const g1 = ctx.createLinearGradient(0, l.topLaneTop, 0, l.topLaneBot);
+  g1.addColorStop(0, c1[0]); g1.addColorStop(1, c1[1]);
+  ctx.fillStyle = g1; ctx.fillRect(0, l.topLaneTop, W, l.topLaneBot - l.topLaneTop);
+  const g2 = ctx.createLinearGradient(0, l.botLaneTop, 0, l.botLaneBot);
+  g2.addColorStop(0, c2[0]); g2.addColorStop(1, c2[1]);
+  ctx.fillStyle = g2; ctx.fillRect(0, l.botLaneTop, W, l.botLaneBot - l.botLaneTop);
+  ctx.strokeStyle = edge; ctx.lineWidth = 2;
+  ctx.beginPath(); ctx.moveTo(0, (l.topLaneBot + l.botLaneTop) / 2);
+  ctx.lineTo(W, (l.topLaneBot + l.botLaneTop) / 2); ctx.stroke();
+  ctx.strokeStyle = dash; ctx.lineWidth = 3;
+  ctx.setLineDash([30, 25]);
+  ctx.beginPath(); ctx.moveTo(0, (l.topLaneBot + l.botLaneTop) / 2 + 6);
+  ctx.lineTo(W, (l.topLaneBot + l.botLaneTop) / 2 + 6); ctx.stroke();
+  ctx.setLineDash([]);
+
+  if (isWet()) {
+    ctx.fillStyle = 'rgba(120,150,200,.06)';
+    ctx.fillRect(0, l.topLaneTop, W, l.topLaneBot - l.topLaneTop);
+    ctx.fillRect(0, l.botLaneTop, W, l.botLaneBot - l.botLaneTop);
+    if (state.car) {
+      const cx = state.car.x;
+      const refl = ctx.createLinearGradient(cx, 0, cx, l.topLaneBot);
+      refl.addColorStop(0, 'rgba(255,240,180,.22)');
+      refl.addColorStop(1, 'rgba(255,240,180,0)');
+      ctx.fillStyle = refl;
+      ctx.fillRect(cx - 90, l.topLaneTop, 180, l.topLaneBot - l.topLaneTop);
+    }
+  }
+}
+
+function drawBooth() {
+  const l = L();
+  const bx = l.boothX, by = l.boothBaseY, bw = l.boothW, bh = l.boothH;
+  const m = lighting().mode;
+  const bright = (m === 'day' || m === 'overcast' || m === 'dawn');
+
+  ctx.fillStyle = 'rgba(0,0,0,.55)';
+  ctx.beginPath(); ctx.ellipse(bx + bw/2, by + 6, bw/2 + 14, 9, 0, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = '#0a0f14'; ctx.fillRect(bx - 8, by, bw + 16, 12);
+
+  const wg = ctx.createLinearGradient(bx, 0, bx + bw, 0);
+  if (bright) { wg.addColorStop(0, '#6a7482'); wg.addColorStop(0.5, '#8a94a4'); wg.addColorStop(1, '#4a5462'); }
+  else { wg.addColorStop(0, '#2a3442'); wg.addColorStop(0.5, '#4a5a6a'); wg.addColorStop(1, '#182028'); }
+  ctx.fillStyle = wg; ctx.fillRect(bx, by - bh, bw, bh);
+  ctx.strokeStyle = '#4a5a70'; ctx.lineWidth = 2; ctx.strokeRect(bx, by - bh, bw, bh);
+
+  ctx.fillStyle = '#0a0f14'; ctx.fillRect(bx - 12, by - bh - 14, bw + 24, 14);
+  ctx.strokeStyle = '#3a4452'; ctx.strokeRect(bx - 12, by - bh - 14, bw + 24, 14);
+  ctx.fillStyle = 'rgba(255,255,255,.08)';
+  ctx.fillRect(bx - 12, by - bh - 14, bw + 24, 4);
+
+  const glow = bright ? 0.4 : 0.8 + 0.15 * Math.sin(state.time * 1.5);
+  ctx.fillStyle = `rgba(255,220,140,${glow})`;
+  ctx.fillRect(bx + 15, by - bh + 25, 60, 60);
+  const wgl = ctx.createLinearGradient(bx + 15, by - bh + 25, bx + 75, by - bh + 85);
+  wgl.addColorStop(0, 'rgba(255,255,255,.28)');
+  wgl.addColorStop(0.5, 'rgba(255,255,255,0)');
+  wgl.addColorStop(1, 'rgba(255,255,255,.10)');
+  ctx.fillStyle = wgl; ctx.fillRect(bx + 15, by - bh + 25, 60, 60);
+  ctx.strokeStyle = '#4a5a70'; ctx.strokeRect(bx + 15, by - bh + 25, 60, 60);
+  ctx.beginPath(); ctx.moveTo(bx + 45, by - bh + 25); ctx.lineTo(bx + 45, by - bh + 85); ctx.stroke();
+  ctx.beginPath(); ctx.moveTo(bx + 15, by - bh + 55); ctx.lineTo(bx + 75, by - bh + 55); ctx.stroke();
+  const oGlow = ctx.createRadialGradient(bx + 45, by - bh + 55, 8, bx + 45, by - bh + 55, 60);
+  oGlow.addColorStop(0, `rgba(255,220,140,${0.18 * glow + 0.08})`);
+  oGlow.addColorStop(1, 'rgba(255,220,140,0)');
+  ctx.fillStyle = oGlow; ctx.fillRect(bx + 15 - 60, by - bh + 25 - 60, 180, 180);
+
+  ctx.fillStyle = '#1a2028'; ctx.fillRect(bx + bw - 45, by - bh + 25, 35, bh - 25);
+  ctx.strokeStyle = '#4a5a70'; ctx.strokeRect(bx + bw - 45, by - bh + 25, 35, bh - 25);
+  ctx.fillStyle = '#e8c070';
+  ctx.beginPath(); ctx.arc(bx + bw - 40, by - bh/2 - 5, 2.6, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = 'rgba(255,255,255,.5)';
+  ctx.beginPath(); ctx.arc(bx + bw - 41, by - bh/2 - 6, 1.1, 0, Math.PI * 2); ctx.fill();
+
+  ctx.fillStyle = '#80a0c0'; ctx.font = 'bold 11px "Courier New"';
+  ctx.textAlign = 'center';
+  ctx.fillText('ОХРАНА', bx + bw/2, by - bh - 22);
+  ctx.textAlign = 'left';
+}
+
+function drawGuard(x, y) {
+  ctx.fillStyle = 'rgba(0,0,0,.55)';
+  ctx.beginPath(); ctx.ellipse(x, y + 2, 14, 4, 0, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = '#1a2230';
+  ctx.fillRect(x - 8, y - 35, 7, 35); ctx.fillRect(x + 1, y - 35, 7, 35);
+  ctx.fillStyle = '#0a0a0a';
+  ctx.fillRect(x - 10, y - 3, 10, 5); ctx.fillRect(x, y - 3, 10, 5);
+  const bg = ctx.createLinearGradient(x - 11, 0, x + 11, 0);
+  bg.addColorStop(0, '#1a3a5e'); bg.addColorStop(0.4, '#3a6a9a'); bg.addColorStop(1, '#1a3050');
+  ctx.fillStyle = bg; ctx.fillRect(x - 11, y - 68, 22, 36);
+  ctx.strokeStyle = '#0a1a30'; ctx.lineWidth = 1; ctx.strokeRect(x - 11, y - 68, 22, 36);
+  ctx.fillStyle = '#e8e8f0';
+  ctx.fillRect(x - 11, y - 68, 5, 3); ctx.fillRect(x + 6, y - 68, 5, 3);
+  ctx.fillStyle = '#0a0a0a'; ctx.fillRect(x - 11, y - 38, 22, 4);
+  ctx.fillStyle = '#1a3a5e';
+  ctx.fillRect(x - 16, y - 66, 6, 30); ctx.fillRect(x + 10, y - 66, 6, 30);
+  ctx.fillStyle = '#d0a880';
+  ctx.fillRect(x - 16, y - 38, 6, 7); ctx.fillRect(x + 10, y - 38, 6, 7);
+  ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 72, 6, 4);
+  ctx.beginPath(); ctx.arc(x, y - 81, 9, 0, Math.PI * 2);
+  const fg = ctx.createRadialGradient(x - 3, y - 83, 2, x, y - 81, 9);
+  fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#b08868');
+  ctx.fillStyle = fg; ctx.fill();
+  ctx.fillStyle = '#1a1a1a';
+  ctx.beginPath(); ctx.arc(x - 3, y - 82, 1, 0, Math.PI * 2); ctx.fill();
+  ctx.beginPath(); ctx.arc(x + 3, y - 82, 1, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = '#0f2340';
+  ctx.beginPath(); ctx.arc(x, y - 88, 10, Math.PI, Math.PI * 2); ctx.fill();
+  ctx.fillRect(x - 11, y - 88, 22, 3);
+  ctx.fillStyle = '#050a15'; ctx.fillRect(x - 12, y - 85, 24, 3);
+  ctx.fillStyle = '#c0a040'; ctx.beginPath(); ctx.arc(x, y - 90, 2, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = 'rgba(255,220,140,.6)'; ctx.beginPath(); ctx.arc(x - 0.5, y - 90.5, 0.8, 0, Math.PI * 2); ctx.fill();
+}
+
+function drawBarrier() {
+  const l = L();
+  const pillarX = l.pillarX, pillarW = l.pillarW, pillarH = l.pillarH;
+  const groundBase = l.topLaneY;
+  const pillarTopY = groundBase - pillarH;
+  const pivotX = pillarX, pivotY = groundBase - 80;
+  const armLen = (state.model ? state.model.armLen : 240);
+  const armH = l.armH;
+  const level = state.barrier.level;
+  const m = lighting().mode;
+  const bright = (m === 'day' || m === 'overcast' || m === 'dawn');
+
+  ctx.fillStyle = 'rgba(0,0,0,.55)';
+  ctx.beginPath(); ctx.ellipse(pillarX + pillarW/2, groundBase + 4, 26, 8, 0, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = '#1a1e24'; ctx.fillRect(pillarX - 10, groundBase - 10, pillarW + 20, 16);
+  ctx.strokeStyle = '#3a4858'; ctx.lineWidth = 1.5; ctx.strokeRect(pillarX - 10, groundBase - 10, pillarW + 20, 16);
+  ctx.fillStyle = 'rgba(255,255,255,.06)';
+  ctx.fillRect(pillarX - 10, groundBase - 10, pillarW + 20, 3);
+
+  const g = ctx.createLinearGradient(pillarX, 0, pillarX + pillarW, 0);
+  if (bright) { g.addColorStop(0, '#3a4858'); g.addColorStop(0.35, '#7a8898'); g.addColorStop(0.6, '#5a6878'); g.addColorStop(1, '#2a3442'); }
+  else { g.addColorStop(0, '#1a2028'); g.addColorStop(0.35, '#4a5a6a'); g.addColorStop(0.6, '#2a3442'); g.addColorStop(1, '#0a1018'); }
+  ctx.fillStyle = g; ctx.fillRect(pillarX, pillarTopY, pillarW, pillarH);
+  ctx.strokeStyle = '#000'; ctx.lineWidth = 1.5; ctx.strokeRect(pillarX, pillarTopY, pillarW, pillarH);
+
+  const shine = ctx.createLinearGradient(pillarX + 4, 0, pillarX + 12, 0);
+  shine.addColorStop(0, 'rgba(255,255,255,.18)');
+  shine.addColorStop(1, 'rgba(255,255,255,0)');
+  ctx.fillStyle = shine; ctx.fillRect(pillarX + 4, pillarTopY + 4, 8, pillarH - 8);
+
+  for (let i = 0; i < 4; i++) {
+    ctx.fillStyle = i % 2 === 0 ? '#a08030' : '#1a1a1a';
+    ctx.fillRect(pillarX, pillarTopY + pillarH - 90 + i * 20, pillarW, 20);
+  }
+  ctx.fillStyle = 'rgba(255,220,140,.12)';
+  ctx.fillRect(pillarX, pillarTopY + pillarH - 90, pillarW, 80);
+
+  ctx.fillStyle = '#505860';
+  ctx.beginPath();
+  ctx.moveTo(pillarX - 5, pillarTopY); ctx.lineTo(pillarX + pillarW + 5, pillarTopY);
+  ctx.lineTo(pillarX + pillarW, pillarTopY - 12); ctx.lineTo(pillarX, pillarTopY - 12);
+  ctx.closePath(); ctx.fill();
+  ctx.strokeStyle = '#000'; ctx.stroke();
+  ctx.fillStyle = 'rgba(255,255,255,.12)';
+  ctx.fillRect(pillarX, pillarTopY - 12, pillarW, 3);
+
+  const blink = Math.sin(state.time * 4) > 0;
+  const lampColor = level > 0.9 ? '#40d080' : (level > 0.1 ? '#ffb040' : '#ff2020');
+  const lampX = pillarX + pillarW/2, lampY = pillarTopY - 26;
+  if (blink) {
+    const lampGlow = ctx.createRadialGradient(lampX, lampY, 3, lampX, lampY, 40);
+    lampGlow.addColorStop(0, lampColor + 'cc');
+    lampGlow.addColorStop(0.4, lampColor + '44');
+    lampGlow.addColorStop(1, lampColor + '00');
+    ctx.fillStyle = lampGlow;
+    ctx.fillRect(lampX - 44, lampY - 44, 88, 88);
+  }
+  ctx.beginPath(); ctx.arc(lampX, lampY, 9, 0, Math.PI * 2);
+  const lampGrad = ctx.createRadialGradient(lampX - 3, lampY - 3, 2, lampX, lampY, 9);
+  if (blink) {
+    lampGrad.addColorStop(0, '#ffffff');
+    lampGrad.addColorStop(0.4, lampColor);
+    lampGrad.addColorStop(1, lampColor);
+  } else {
+    lampGrad.addColorStop(0, '#3a3a3a');
+    lampGrad.addColorStop(1, '#1a1a1a');
+  }
+  ctx.fillStyle = lampGrad; ctx.fill();
+  ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
+  ctx.beginPath(); ctx.arc(lampX - 3, lampY - 3, 2.5, 0, Math.PI * 2);
+  ctx.fillStyle = 'rgba(255,255,255,.6)'; ctx.fill();
+
+  ctx.beginPath(); ctx.arc(pivotX, pivotY, 15, 0, Math.PI * 2);
+  ctx.fillStyle = '#2a3442'; ctx.fill();
+  ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
+  ctx.beginPath(); ctx.arc(pivotX, pivotY, 6, 0, Math.PI * 2);
+  const hg = ctx.createRadialGradient(pivotX - 2, pivotY - 2, 1, pivotX, pivotY, 6);
+  hg.addColorStop(0, '#c8d4e0'); hg.addColorStop(1, '#506070');
+  ctx.fillStyle = hg; ctx.fill();
+  ctx.beginPath(); ctx.arc(pivotX - 3, pivotY - 3, 2, 0, Math.PI * 2);
+  ctx.fillStyle = 'rgba(255,255,255,.5)'; ctx.fill();
+
+  const angle = Math.PI + level * Math.PI / 2;
+  ctx.save();
+  ctx.translate(pivotX, pivotY); ctx.rotate(angle);
+  const seg = 14;
+  for (let i = 0; i < seg; i++) {
+    ctx.fillStyle = i % 2 === 0 ? '#e04040' : '#e8e8e8';
+    ctx.fillRect(i * (armLen / seg), -armH/2, armLen / seg, armH);
+  }
+  ctx.fillStyle = 'rgba(0,0,0,.35)';
+  ctx.fillRect(0, armH/2 - 2, armLen, 2);
+  ctx.fillStyle = 'rgba(255,255,255,.4)';
+  ctx.fillRect(0, -armH/2, armLen, 2);
+  ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.strokeRect(0, -armH/2, armLen, armH);
+  ctx.beginPath(); ctx.arc(armLen, 0, 12, 0, Math.PI * 2);
+  const ng = ctx.createRadialGradient(armLen - 3, -3, 2, armLen, 0, 12);
+  ng.addColorStop(0, '#ffffff'); ng.addColorStop(1, '#a8b0b8');
+  ctx.fillStyle = ng; ctx.fill();
+  ctx.strokeStyle = '#000'; ctx.lineWidth = 2; ctx.stroke();
+  ctx.beginPath(); ctx.arc(armLen, 0, 5, 0, Math.PI * 2);
+  ctx.fillStyle = '#8090a0'; ctx.fill();
+  ctx.restore();
+
+  if (level < 0.9) {
+    const endX = pivotX + Math.cos(angle) * armLen;
+    const endY = pivotY + Math.sin(angle) * armLen;
+    const blinkE = Math.sin(state.time * 6) > 0;
+    if (blinkE) {
+      const eGlow = ctx.createRadialGradient(endX, endY, 2, endX, endY, 26);
+      eGlow.addColorStop(0, 'rgba(255,64,64,.55)');
+      eGlow.addColorStop(1, 'rgba(255,64,64,0)');
+      ctx.fillStyle = eGlow;
+      ctx.fillRect(endX - 28, endY - 28, 56, 56);
+    }
+    ctx.beginPath(); ctx.arc(endX, endY, 6, 0, Math.PI * 2);
+    ctx.fillStyle = blinkE ? '#ff4040' : '#601010'; ctx.fill();
+    ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
+  }
+
+  if (state.model && state.model.hasLoop) {
+    const loopX = l.pivotX - 280;
+    const loopY = l.topLaneY + 10;
+    const loopW = 200, loopH = 18;
+    ctx.strokeStyle = state.skud.loop1 ? 'rgba(96,180,255,.85)' : 'rgba(96,160,224,.35)';
+    ctx.lineWidth = 2;
+    ctx.strokeRect(loopX, loopY, loopW, loopH);
+    if (state.skud.loop1) {
+      ctx.fillStyle = 'rgba(96,180,255,.18)';
+      ctx.fillRect(loopX, loopY, loopW, loopH);
+      const lg = ctx.createRadialGradient(loopX + loopW/2, loopY + loopH/2, 5, loopX + loopW/2, loopY + loopH/2, loopW/2);
+      lg.addColorStop(0, 'rgba(120,200,255,.18)');
+      lg.addColorStop(1, 'rgba(120,200,255,0)');
+      ctx.fillStyle = lg;
+      ctx.fillRect(loopX - 30, loopY - 30, loopW + 60, loopH + 60);
+    }
+  }
+  if (state.model && state.model.hasPhoto) {
+    const px1 = l.pivotX - state.model.armLen - 5;
+    const px2 = l.pivotX + 55;
+    const py = l.topLaneY - 4;
+    ctx.fillStyle = '#304050';
+    ctx.fillRect(px1 - 3, py - 44, 6, 44);
+    ctx.fillRect(px2 - 3, py - 44, 6, 44);
+    ctx.strokeStyle = '#506070'; ctx.lineWidth = 1;
+    ctx.strokeRect(px1 - 3, py - 44, 6, 44);
+    ctx.strokeRect(px2 - 3, py - 44, 6, 44);
+    const on = !state.skud.photo;
+    const dCol = on ? '#40d080' : '#ff4040';
+    if (on) {
+      const dg = ctx.createRadialGradient(px1, py - 36, 2, px1, py - 36, 16);
+      dg.addColorStop(0, dCol + 'aa'); dg.addColorStop(1, dCol + '00');
+      ctx.fillStyle = dg;
+      ctx.fillRect(px1 - 18, py - 54, 36, 36);
+      ctx.fillRect(px2 - 18, py - 54, 36, 36);
+    }
+    ctx.fillStyle = dCol;
+    ctx.beginPath(); ctx.arc(px1, py - 36, 3.5, 0, Math.PI * 2); ctx.fill();
+    ctx.beginPath(); ctx.arc(px2, py - 36, 3.5, 0, Math.PI * 2); ctx.fill();
+    ctx.strokeStyle = '#000'; ctx.lineWidth = 1;
+    ctx.beginPath(); ctx.arc(px1, py - 36, 3.5, 0, Math.PI * 2); ctx.stroke();
+    ctx.beginPath(); ctx.arc(px2, py - 36, 3.5, 0, Math.PI * 2); ctx.stroke();
+    if (on) {
+      ctx.strokeStyle = 'rgba(64,208,128,.6)';
+      ctx.setLineDash([4,4]); ctx.lineWidth = 1.2;
+      ctx.beginPath(); ctx.moveTo(px1, py - 36); ctx.lineTo(px2, py - 36); ctx.stroke();
+      ctx.setLineDash([]);
+    } else {
+      ctx.strokeStyle = 'rgba(255,64,64,.5)';
+      ctx.setLineDash([2,6]);
+      ctx.beginPath(); ctx.moveTo(px1, py - 36); ctx.lineTo(px2, py - 36); ctx.stroke();
+      ctx.setLineDash([]);
+    }
+  }
+}
+
+/* ===== МАШИНЫ ===== */
+function drawWheels(x, y, dxRear, dxFront, radius, rim, angle) {
+  [x - dxRear, x + dxFront].forEach(wx => {
+    ctx.beginPath(); ctx.arc(wx, y, radius, 0, Math.PI * 2);
+    const tg = ctx.createRadialGradient(wx - radius*0.3, y - radius*0.3, 1, wx, y, radius);
+    tg.addColorStop(0, '#2a2a2a'); tg.addColorStop(1, '#000');
+    ctx.fillStyle = tg; ctx.fill();
+    ctx.beginPath(); ctx.arc(wx, y, radius * 0.62, 0, Math.PI * 2);
+    ctx.fillStyle = '#1a2028'; ctx.fill();
+    ctx.strokeStyle = rim; ctx.lineWidth = 1.5; ctx.stroke();
+    ctx.save(); ctx.translate(wx, y); ctx.rotate(angle);
+    ctx.strokeStyle = rim; ctx.lineWidth = 1.4; ctx.lineCap = 'round';
+    for (let i = 0; i < 5; i++) {
+      const a = (i / 5) * Math.PI * 2;
+      ctx.beginPath(); ctx.moveTo(0, 0);
+      ctx.lineTo(Math.cos(a) * radius * 0.5, Math.sin(a) * radius * 0.5); ctx.stroke();
+    }
+    ctx.beginPath(); ctx.arc(0, 0, radius * 0.16, 0, Math.PI * 2);
+    ctx.fillStyle = rim; ctx.fill();
+    ctx.strokeStyle = '#303840'; ctx.lineWidth = 1; ctx.stroke();
+    ctx.restore();
+    ctx.beginPath(); ctx.arc(wx - radius*0.35, y - radius*0.35, radius*0.18, 0, Math.PI * 2);
+    ctx.fillStyle = 'rgba(255,255,255,.10)'; ctx.fill();
+  });
+}
+
+function drawSedan(x, y, color, wheelAngle) {
+  ctx.fillStyle = 'rgba(0,0,0,.55)';
+  ctx.beginPath(); ctx.ellipse(x, y + 4, 92, 7, 0, 0, Math.PI * 2); ctx.fill();
+
+  const bg = ctx.createLinearGradient(0, y - 55, 0, y);
+  bg.addColorStop(0, color); bg.addColorStop(0.55, color); bg.addColorStop(1, '#050505');
+  ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
+  ctx.beginPath();
+  ctx.moveTo(x + 85, y - 8); ctx.lineTo(x + 78, y - 26); ctx.lineTo(x + 40, y - 30);
+  ctx.lineTo(x + 15, y - 52); ctx.lineTo(x - 25, y - 54); ctx.lineTo(x - 55, y - 42);
+  ctx.lineTo(x - 78, y - 24); ctx.lineTo(x - 85, y - 8); ctx.lineTo(x - 85, y);
+  ctx.lineTo(x + 85, y); ctx.closePath(); ctx.fill(); ctx.stroke();
+
+  ctx.fillStyle = 'rgba(255,255,255,.18)';
+  ctx.beginPath();
+  ctx.moveTo(x - 70, y - 22); ctx.lineTo(x + 70, y - 22);
+  ctx.lineTo(x + 65, y - 26); ctx.lineTo(x - 65, y - 26);
+  ctx.closePath(); ctx.fill();
+
+  const gk = ctx.createLinearGradient(0, y - 54, 0, y - 30);
+  gk.addColorStop(0, 'rgba(90,140,180,.85)');
+  gk.addColorStop(1, 'rgba(30,60,90,.75)');
+  ctx.fillStyle = gk;
+  ctx.beginPath();
+  ctx.moveTo(x + 12, y - 50); ctx.lineTo(x - 22, y - 52); ctx.lineTo(x - 48, y - 40);
+  ctx.lineTo(x - 48, y - 30); ctx.lineTo(x + 12, y - 30); ctx.closePath(); ctx.fill();
+  ctx.fillStyle = 'rgba(255,255,255,.20)';
+  ctx.beginPath();
+  ctx.moveTo(x + 8, y - 48); ctx.lineTo(x - 18, y - 50);
+  ctx.lineTo(x - 30, y - 44); ctx.lineTo(x - 20, y - 44);
+  ctx.closePath(); ctx.fill();
+
+  ctx.strokeStyle = 'rgba(0,0,0,.4)'; ctx.lineWidth = 1;
+  ctx.beginPath(); ctx.moveTo(x - 8, y - 30); ctx.lineTo(x - 8, y - 8); ctx.stroke();
+
+  ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 18;
+  ctx.beginPath(); ctx.arc(x + 80, y - 14, 4.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0;
+  ctx.fillStyle = 'rgba(255,255,255,.9)';
+  ctx.beginPath(); ctx.arc(x + 79, y - 15, 1.8, 0, Math.PI * 2); ctx.fill();
+
+  ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 10;
+  ctx.fillRect(x - 85, y - 22, 4, 10); ctx.shadowBlur = 0;
+  ctx.fillStyle = 'rgba(255,140,140,.7)';
+  ctx.fillRect(x - 84, y - 21, 2, 3);
+
+  drawWheels(x, y, 55, 55, 14, '#a0a8b0', wheelAngle);
+}
+
+function drawDarkCar(x, y, wheelAngle) {
+  ctx.fillStyle = 'rgba(0,0,0,.65)';
+  ctx.beginPath(); ctx.ellipse(x, y + 4, 108, 8, 0, 0, Math.PI * 2); ctx.fill();
+
+  const bg = ctx.createLinearGradient(0, y - 58, 0, y);
+  bg.addColorStop(0, '#3a3a3a');
+  bg.addColorStop(0.25, '#1a1a1a');
+  bg.addColorStop(0.7, '#050505');
+  bg.addColorStop(1, '#000');
+  ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
+  ctx.beginPath();
+  ctx.moveTo(x - 105, y - 10); ctx.lineTo(x - 100, y - 28);
+  ctx.lineTo(x - 80, y - 34); ctx.lineTo(x - 55, y - 42);
+  ctx.lineTo(x - 35, y - 54); ctx.lineTo(x + 25, y - 54);
+  ctx.lineTo(x + 45, y - 42); ctx.lineTo(x + 55, y - 34);
+  ctx.lineTo(x + 92, y - 30); ctx.lineTo(x + 105, y - 12);
+  ctx.lineTo(x + 105, y); ctx.lineTo(x - 105, y);
+  ctx.closePath(); ctx.fill(); ctx.stroke();
+
+  ctx.strokeStyle = 'rgba(220,230,240,.7)'; ctx.lineWidth = 1.5;
+  ctx.beginPath(); ctx.moveTo(x - 100, y - 22); ctx.lineTo(x + 100, y - 22); ctx.stroke();
+  ctx.strokeStyle = 'rgba(255,255,255,.25)'; ctx.lineWidth = 1;
+  ctx.beginPath(); ctx.moveTo(x - 100, y - 23); ctx.lineTo(x + 100, y - 23); ctx.stroke();
+
+  const gk = ctx.createLinearGradient(0, y - 54, 0, y - 28);
+  gk.addColorStop(0, 'rgba(20,40,60,.95)');
+  gk.addColorStop(0.5, 'rgba(5,10,20,.95)');
+  gk.addColorStop(1, 'rgba(2,4,8,.95)');
+  ctx.fillStyle = gk;
+  ctx.beginPath();
+  ctx.moveTo(x + 45, y - 40); ctx.lineTo(x + 18, y - 40);
+  ctx.lineTo(x + 18, y - 30); ctx.lineTo(x + 52, y - 30);
+  ctx.closePath(); ctx.fill();
+  ctx.beginPath();
+  ctx.moveTo(x - 55, y - 40); ctx.lineTo(x - 35, y - 50);
+  ctx.lineTo(x - 10, y - 50); ctx.lineTo(x - 10, y - 30);
+  ctx.lineTo(x - 55, y - 30); ctx.closePath(); ctx.fill();
+  ctx.fillStyle = 'rgba(120,180,220,.35)';
+  ctx.beginPath();
+  ctx.moveTo(x - 50, y - 42); ctx.lineTo(x - 40, y - 48);
+  ctx.lineTo(x - 30, y - 48); ctx.lineTo(x - 40, y - 42);
+  ctx.closePath(); ctx.fill();
+  ctx.fillStyle = 'rgba(120,180,220,.30)';
+  ctx.beginPath();
+  ctx.moveTo(x + 22, y - 38); ctx.lineTo(x + 30, y - 38);
+  ctx.lineTo(x + 30, y - 32); ctx.lineTo(x + 22, y - 32);
+  ctx.closePath(); ctx.fill();
+
+  ctx.fillStyle = '#0a0a0a'; ctx.fillRect(x + 15, y - 50, 4, 20);
+
+  ctx.fillStyle = '#050505';
+  ctx.strokeStyle = '#404040';
+  ctx.lineWidth = 1;
+  ctx.fillRect(x + 82, y - 26, 20, 12);
+  ctx.strokeRect(x + 82, y - 26, 20, 12);
+  for (let i = 0; i < 3; i++) {
+    ctx.strokeStyle = '#505860';
+    ctx.beginPath();
+    ctx.moveTo(x + 83, y - 24 + i * 4);
+    ctx.lineTo(x + 101, y - 24 + i * 4);
+    ctx.stroke();
+  }
+
+  ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 22;
+  ctx.beginPath();
+  ctx.moveTo(x + 84, y - 22); ctx.lineTo(x + 100, y - 22);
+  ctx.lineTo(x + 100, y - 12); ctx.lineTo(x + 84, y - 12);
+  ctx.closePath(); ctx.fill(); ctx.shadowBlur = 0;
+  ctx.fillStyle = '#c0e8ff'; ctx.fillRect(x + 86, y - 20, 12, 2);
+  ctx.fillStyle = 'rgba(255,255,255,.7)';
+  ctx.fillRect(x + 90, y - 21, 8, 1.5);
+
+  ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 18;
+  ctx.fillRect(x - 105, y - 24, 6, 14); ctx.shadowBlur = 0;
+  ctx.fillStyle = 'rgba(255,160,160,.6)';
+  ctx.fillRect(x - 104, y - 23, 3, 4);
+
+  drawWheels(x, y, 62, 58, 17, '#c8d0d8', wheelAngle);
+}
+
+function drawPoliceCar(x, y, wheelAngle) {
+  ctx.fillStyle = 'rgba(0,0,0,.6)';
+  ctx.beginPath(); ctx.ellipse(x, y + 4, 105, 8, 0, 0, Math.PI * 2); ctx.fill();
+
+  const bg = ctx.createLinearGradient(0, y - 58, 0, y);
+  bg.addColorStop(0, '#f04444');
+  bg.addColorStop(0.35, '#c82020');
+  bg.addColorStop(0.75, '#700a0a');
+  bg.addColorStop(1, '#400404');
+  ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
+  ctx.beginPath();
+  ctx.moveTo(x + 105, y - 10); ctx.lineTo(x + 100, y - 28);
+  ctx.lineTo(x + 80, y - 34); ctx.lineTo(x + 55, y - 42);
+  ctx.lineTo(x + 35, y - 54); ctx.lineTo(x - 25, y - 54);
+  ctx.lineTo(x - 45, y - 42); ctx.lineTo(x - 55, y - 34);
+  ctx.lineTo(x - 92, y - 30); ctx.lineTo(x - 105, y - 12);
+  ctx.lineTo(x - 105, y); ctx.lineTo(x + 105, y);
+  ctx.closePath(); ctx.fill(); ctx.stroke();
+
+  ctx.fillStyle = 'rgba(255,255,255,.20)';
+  ctx.beginPath();
+  ctx.moveTo(x - 90, y - 20); ctx.lineTo(x + 90, y - 20);
+  ctx.lineTo(x + 85, y - 24); ctx.lineTo(x - 85, y - 24);
+  ctx.closePath(); ctx.fill();
+
+  const gk = ctx.createLinearGradient(0, y - 54, 0, y - 30);
+  gk.addColorStop(0, 'rgba(70,120,170,.85)');
+  gk.addColorStop(1, 'rgba(20,40,70,.85)');
+  ctx.fillStyle = gk;
+  ctx.beginPath();
+  ctx.moveTo(x - 45, y - 40); ctx.lineTo(x - 18, y - 40);
+  ctx.lineTo(x - 18, y - 30); ctx.lineTo(x - 52, y - 30);
+  ctx.closePath(); ctx.fill();
+  ctx.beginPath();
+  ctx.moveTo(x + 55, y - 40); ctx.lineTo(x + 35, y - 50);
+  ctx.lineTo(x + 10, y - 50); ctx.lineTo(x + 10, y - 30);
+  ctx.lineTo(x + 55, y - 30); ctx.closePath(); ctx.fill();
+  ctx.fillRect(x - 12, y - 30, 25, 10);
+
+  ctx.save();
+  ctx.fillStyle = '#ffffff';
+  ctx.font = 'bold 11px "Courier New"';
+  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+  ctx.shadowColor = 'rgba(0,0,0,.9)'; ctx.shadowBlur = 3;
+  ctx.fillText('ПОЛИЦИЯ', x - 40, y - 12);
+  ctx.fillText('ПОЛИЦИЯ', x + 30, y - 12);
+  ctx.restore();
+
+  ctx.fillStyle = '#1a3a80'; ctx.fillRect(x - 95, y - 6, 190, 3);
+  ctx.fillStyle = 'rgba(120,180,255,.4)'; ctx.fillRect(x - 95, y - 6, 190, 1);
+
+  ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 20;
+  ctx.fillRect(x - 100, y - 20, 14, 6); ctx.shadowBlur = 0;
+  ctx.fillStyle = '#c02020'; ctx.shadowColor = '#c02020'; ctx.shadowBlur = 14;
+  ctx.fillRect(x + 100, y - 24, 5, 14); ctx.shadowBlur = 0;
+
+  const blink = Math.sin(state.time * 12) > 0;
+  ctx.fillStyle = '#1a1a1a';
+  ctx.fillRect(x - 30, y - 60, 60, 5);
+  if (blink) {
+    const rg = ctx.createRadialGradient(x - 15, y - 68, 4, x - 15, y - 68, 50);
+    rg.addColorStop(0, 'rgba(255,50,50,.9)');
+    rg.addColorStop(1, 'rgba(255,50,50,0)');
+    ctx.fillStyle = rg; ctx.fillRect(x - 65, y - 118, 100, 100);
+  }
+  ctx.fillStyle = blink ? '#ff2020' : '#401010';
+  ctx.beginPath();
+  ctx.moveTo(x - 30, y - 60); ctx.lineTo(x, y - 60);
+  ctx.lineTo(x, y - 70); ctx.lineTo(x - 30, y - 70);
+  ctx.closePath(); ctx.fill();
+  ctx.strokeStyle = '#000'; ctx.lineWidth = 1; ctx.stroke();
+
+  if (!blink) {
+    const bg2 = ctx.createRadialGradient(x + 15, y - 68, 4, x + 15, y - 68, 50);
+    bg2.addColorStop(0, 'rgba(50,120,255,.9)');
+    bg2.addColorStop(1, 'rgba(50,120,255,0)');
+    ctx.fillStyle = bg2; ctx.fillRect(x - 35, y - 118, 100, 100);
+  }
+  ctx.fillStyle = !blink ? '#2060ff' : '#101040';
+  ctx.beginPath();
+  ctx.moveTo(x, y - 60); ctx.lineTo(x + 30, y - 60);
+  ctx.lineTo(x + 30, y - 70); ctx.lineTo(x, y - 70);
+  ctx.closePath(); ctx.fill();
+  ctx.strokeStyle = '#000'; ctx.stroke();
+
+  const beamGrad = ctx.createLinearGradient(x - 105, y - 14, x - 300, y + 10);
+  beamGrad.addColorStop(0, 'rgba(255,240,180,.5)');
+  beamGrad.addColorStop(1, 'rgba(255,240,180,0)');
+  ctx.fillStyle = beamGrad;
+  ctx.beginPath();
+  ctx.moveTo(x - 105, y - 20); ctx.lineTo(x - 300, y - 28);
+  ctx.lineTo(x - 300, y + 20); ctx.lineTo(x - 105, y - 4);
+  ctx.closePath(); ctx.fill();
+
+  drawWheels(x, y, 65, 60, 15, '#606060', wheelAngle);
+}
+
+function drawTowTruck(x, y, wheelAngle, doorOpen) {
+  ctx.fillStyle = 'rgba(0,0,0,.55)';
+  ctx.beginPath(); ctx.ellipse(x, y + 4, 105, 7, 0, 0, Math.PI * 2); ctx.fill();
+
+  const bg = ctx.createLinearGradient(0, y - 60, 0, y);
+  bg.addColorStop(0, '#e8c840'); bg.addColorStop(0.4, '#c8a020'); bg.addColorStop(1, '#705010');
+  ctx.fillStyle = bg; ctx.strokeStyle = '#000'; ctx.lineWidth = 2;
+  ctx.beginPath();
+  ctx.moveTo(x - 100, y - 8); ctx.lineTo(x - 100, y - 55);
+  ctx.lineTo(x - 55, y - 55); ctx.lineTo(x - 40, y - 25);
+  ctx.lineTo(x - 20, y - 25); ctx.lineTo(x - 20, y - 8);
+  ctx.lineTo(x - 20, y); ctx.lineTo(x - 100, y);
+  ctx.closePath(); ctx.fill(); ctx.stroke();
+
+  const gk = ctx.createLinearGradient(0, y - 50, 0, y - 28);
+  gk.addColorStop(0, 'rgba(90,140,180,.85)');
+  gk.addColorStop(1, 'rgba(30,60,90,.8)');
+  ctx.fillStyle = gk;
+  ctx.beginPath();
+  ctx.moveTo(x - 95, y - 50); ctx.lineTo(x - 60, y - 50);
+  ctx.lineTo(x - 48, y - 28); ctx.lineTo(x - 95, y - 28);
+  ctx.closePath(); ctx.fill();
+  ctx.fillStyle = 'rgba(255,255,255,.20)';
+  ctx.beginPath();
+  ctx.moveTo(x - 90, y - 48); ctx.lineTo(x - 75, y - 48);
+  ctx.lineTo(x - 80, y - 32); ctx.lineTo(x - 90, y - 32);
+  ctx.closePath(); ctx.fill();
+
+  const pg = ctx.createLinearGradient(0, y - 20, 0, y - 8);
+  pg.addColorStop(0, '#d0b040'); pg.addColorStop(1, '#705010');
+  ctx.fillStyle = pg; ctx.fillRect(x + 20, y - 18, 85, 10);
+  ctx.strokeStyle = '#000'; ctx.strokeRect(x + 20, y - 18, 85, 10);
+  ctx.fillStyle = 'rgba(255,255,255,.15)';
+  ctx.fillRect(x + 20, y - 18, 85, 2);
+
+  ctx.strokeStyle = '#806010'; ctx.lineWidth = 5;
+  ctx.beginPath();
+  ctx.moveTo(x + 5, y - 18); ctx.lineTo(x + 40, y - 55); ctx.lineTo(x + 80, y - 55); ctx.stroke();
+  ctx.strokeStyle = '#c0a020'; ctx.lineWidth = 2;
+  ctx.beginPath(); ctx.moveTo(x + 80, y - 55); ctx.lineTo(x + 90, y - 32); ctx.stroke();
+  ctx.strokeStyle = '#a0a8b0';
+  ctx.beginPath(); ctx.arc(x + 90, y - 28, 4, 0, Math.PI * 1.5); ctx.stroke();
+
+  const blink = Math.sin(state.time * 8) > 0;
+  if (blink) {
+    const mg = ctx.createRadialGradient(x - 59, y - 59, 3, x - 59, y - 59, 40);
+    mg.addColorStop(0, 'rgba(255,180,70,.9)');
+    mg.addColorStop(1, 'rgba(255,180,70,0)');
+    ctx.fillStyle = mg;
+    ctx.fillRect(x - 100, y - 100, 80, 80);
+  }
+  ctx.fillStyle = blink ? '#ffb040' : '#604010';
+  ctx.fillRect(x - 65, y - 62, 12, 6);
+
+  ctx.fillStyle = '#fff8c0'; ctx.shadowColor = '#fff8c0'; ctx.shadowBlur = 15;
+  ctx.beginPath(); ctx.arc(x - 95, y - 14, 4, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0;
+  ctx.fillStyle = 'rgba(255,255,255,.7)';
+  ctx.beginPath(); ctx.arc(x - 96, y - 15, 1.5, 0, Math.PI * 2); ctx.fill();
+
+  drawWheels(x, y, 70, 25, 14, '#606060', wheelAngle);
+  const wx = x + 60;
+  ctx.beginPath(); ctx.arc(wx, y, 14, 0, Math.PI * 2);
+  ctx.fillStyle = '#0a0a0a'; ctx.fill();
+  ctx.beginPath(); ctx.arc(wx, y, 5, 0, Math.PI * 2);
+  ctx.fillStyle = '#606060'; ctx.fill();
+}
+
+function drawTowDriver(x, y, walking, alpha) {
+  ctx.save();
+  ctx.globalAlpha = alpha !== undefined ? Math.max(0, Math.min(1, alpha)) : 1;
+  const wobble = walking ? Math.sin(state.time * 10) * 2 : 0;
+  ctx.fillStyle = 'rgba(0,0,0,.5)';
+  ctx.beginPath(); ctx.ellipse(x, y + 2, 13, 4, 0, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = '#252c38';
+  ctx.fillRect(x - 8, y - 33, 7, 29 + wobble);
+  ctx.fillRect(x + 1, y - 33, 7, 29 - wobble);
+  ctx.fillStyle = '#0a0a0a';
+  ctx.fillRect(x - 10, y + 1 + wobble, 11, 3);
+  ctx.fillRect(x, y + 1 - wobble, 11, 3);
+  const jg = ctx.createLinearGradient(x - 11, 0, x + 11, 0);
+  jg.addColorStop(0, '#b06818'); jg.addColorStop(0.4, '#f09030'); jg.addColorStop(1, '#a85810');
+  ctx.fillStyle = jg; ctx.fillRect(x - 10, y - 63, 20, 33);
+  ctx.strokeStyle = '#703810'; ctx.lineWidth = 1; ctx.strokeRect(x - 10, y - 63, 20, 33);
+  ctx.fillStyle = '#f0f0f0';
+  ctx.fillRect(x - 10, y - 56, 20, 3);
+  ctx.fillRect(x - 10, y - 44, 20, 3);
+  ctx.fillStyle = '#d0a880';
+  ctx.fillRect(x - 16, y - 60, 6, 23);
+  ctx.fillRect(x + 10, y - 60, 6, 23);
+  ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 67, 6, 5);
+  ctx.beginPath(); ctx.arc(x, y - 76, 9, 0, Math.PI * 2);
+  const fg = ctx.createRadialGradient(x - 3, y - 78, 2, x, y - 76, 9);
+  fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#c09870');
+  ctx.fillStyle = fg; ctx.fill();
+  ctx.fillStyle = '#e8c020';
+  ctx.beginPath(); ctx.arc(x, y - 82, 11, Math.PI, 0); ctx.fill();
+  ctx.fillRect(x - 11, y - 83, 22, 4);
+  ctx.strokeStyle = '#8a6a10'; ctx.lineWidth = 1;
+  ctx.beginPath(); ctx.arc(x, y - 82, 11, Math.PI, 0); ctx.stroke();
+  ctx.fillStyle = '#c0a010';
+  ctx.fillRect(x - 1.5, y - 93, 3, 12);
+  ctx.restore();
+}
+
+function drawSenior(x, y, walking, alpha) {
+  ctx.save();
+  ctx.globalAlpha = alpha !== undefined ? Math.max(0, Math.min(1, alpha)) : 1;
+  const wobble = walking ? Math.sin(state.time * 9) * 2 : 0;
+  ctx.fillStyle = 'rgba(0,0,0,.55)';
+  ctx.beginPath(); ctx.ellipse(x, y + 2, 15, 5, 0, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = '#1a2340';
+  ctx.fillRect(x - 8, y - 35, 7, 31 + wobble);
+  ctx.fillRect(x + 1, y - 35, 7, 31 - wobble);
+  ctx.fillStyle = '#0a0a0a';
+  ctx.fillRect(x - 10, y - 3 + wobble, 11, 7);
+  ctx.fillRect(x + 1, y - 3 - wobble, 11, 7);
+  const un = ctx.createLinearGradient(x - 12, 0, x + 12, 0);
+  un.addColorStop(0, '#1a2a4a'); un.addColorStop(0.4, '#2a3a5a'); un.addColorStop(1, '#141e38');
+  ctx.fillStyle = un; ctx.fillRect(x - 12, y - 70, 24, 38);
+  ctx.strokeStyle = '#0a1428'; ctx.lineWidth = 1; ctx.strokeRect(x - 12, y - 70, 24, 38);
+  ctx.fillStyle = '#c0a040';
+  for (let i = 0; i < 3; i++) {
+    ctx.beginPath(); ctx.arc(x, y - 60 + i * 10, 1.2, 0, Math.PI * 2); ctx.fill();
+  }
+  ctx.fillStyle = '#e0e0f0';
+  ctx.fillRect(x - 13, y - 70, 6, 3);
+  ctx.fillRect(x + 7, y - 70, 6, 3);
+  ctx.fillStyle = '#ffd060';
+  ctx.beginPath(); ctx.arc(x - 10, y - 71, 1.5, 0, Math.PI * 2); ctx.fill();
+  ctx.beginPath(); ctx.arc(x + 10, y - 71, 1.5, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = '#2a3a5a';
+  ctx.fillRect(x - 18, y - 68, 6, 32);
+  ctx.fillRect(x + 12, y - 68, 6, 32);
+  ctx.fillStyle = '#0a0a0a';
+  ctx.fillRect(x - 18, y - 38, 7, 8);
+  ctx.fillRect(x + 12, y - 38, 7, 8);
+  ctx.fillStyle = '#d0a880'; ctx.fillRect(x - 3, y - 74, 6, 5);
+  ctx.beginPath(); ctx.arc(x, y - 83, 10, 0, Math.PI * 2);
+  const fg = ctx.createRadialGradient(x - 3, y - 85, 2, x, y - 83, 10);
+  fg.addColorStop(0, '#e8b890'); fg.addColorStop(1, '#c09870');
+  ctx.fillStyle = fg; ctx.fill();
+  ctx.fillStyle = '#1a1a1a';
+  ctx.beginPath(); ctx.arc(x - 3, y - 84, 1.3, 0, Math.PI * 2); ctx.fill();
+  ctx.beginPath(); ctx.arc(x + 3, y - 84, 1.3, 0, Math.PI * 2); ctx.fill();
+  const cap = ctx.createLinearGradient(x - 13, 0, x + 13, 0);
+  cap.addColorStop(0, '#0f1a35'); cap.addColorStop(0.5, '#1a2c52'); cap.addColorStop(1, '#0a1428');
+  ctx.fillStyle = cap;
+  ctx.beginPath(); ctx.arc(x, y - 90, 13, Math.PI, 0); ctx.fill();
+  ctx.fillRect(x - 13, y - 90, 26, 5);
+  ctx.strokeStyle = '#0a1428'; ctx.lineWidth = 1.5;
+  ctx.beginPath(); ctx.arc(x, y - 90, 13, Math.PI, 0); ctx.stroke();
+  ctx.beginPath(); ctx.moveTo(x - 13, y - 85); ctx.lineTo(x + 13, y - 85); ctx.stroke();
+  ctx.fillStyle = '#05080f';
+  ctx.beginPath();
+  ctx.moveTo(x - 14, y - 85); ctx.lineTo(x + 14, y - 85);
+  ctx.lineTo(x + 12, y - 81); ctx.lineTo(x - 12, y - 81);
+  ctx.closePath(); ctx.fill();
+  ctx.strokeStyle = '#000'; ctx.stroke();
+  ctx.fillStyle = '#c0a040';
+  ctx.beginPath(); ctx.arc(x, y - 96, 2.5, 0, Math.PI * 2); ctx.fill();
+  ctx.fillStyle = 'rgba(255,220,140,.7)';
+  ctx.beginPath(); ctx.arc(x - 0.8, y - 96.8, 1, 0, Math.PI * 2); ctx.fill();
+  ctx.restore();
+}
+
+const CAR_SCALE = 1.56;
+function drawCarScaled(drawFn, x, y, ...args) {
+  ctx.save();
+  ctx.translate(x, y);
+  ctx.scale(CAR_SCALE, CAR_SCALE);
+  ctx.translate(-x, -y);
+  drawFn(x, y, ...args);
+  ctx.restore();
+}
+
+/* ===== ПОГОДА ===== */
+function updateWeatherParticles(dt) {
+  if (state.weather === 'clear' || state.weather === 'fog') {
+    state.weatherParticles = []; return;
+  }
+  const targetCount = state.weather === 'rain' ? 240 : 140;
+  while (state.weatherParticles.length < targetCount) {
+    state.weatherParticles.push({
+      x: Math.random() * W, y: Math.random() * H,
+      vx: state.weather === 'rain' ? 60 + Math.random() * 40 : -10 + Math.random() * 20,
+      vy: state.weather === 'rain' ? 600 + Math.random() * 200 : 30 + Math.random() * 40,
+      size: state.weather === 'rain' ? 1 : 1.5 + Math.random() * 1.5,
+      alpha: state.weather === 'rain' ? 0.5 + Math.random() * 0.4 : 0.7 + Math.random() * 0.3
+    });
+  }
+  if (state.weatherParticles.length > targetCount) state.weatherParticles.length = targetCount;
+  state.weatherParticles.forEach(p => {
+    p.x += p.vx * dt; p.y += p.vy * dt;
+    if (p.y > H) { p.y = -10; p.x = Math.random() * W; }
+  });
+}
+function drawWeatherParticles() {
+  if (state.weather === 'rain') {
+    ctx.strokeStyle = 'rgba(180,200,220,.55)';
+    ctx.lineWidth = 1;
+    state.weatherParticles.forEach(p => {
+      ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(p.x - 1, p.y - 12); ctx.stroke();
+    });
+  } else if (state.weather === 'snow') {
+    state.weatherParticles.forEach(p => {
+      ctx.fillStyle = `rgba(240,248,255,${p.alpha})`;
+      ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill();
+    });
+  } else if (state.weather === 'fog') {
+    const fogGrad = ctx.createLinearGradient(0, 0, 0, H);
+    fogGrad.addColorStop(0, 'rgba(200,205,215,.30)');
+    fogGrad.addColorStop(1, 'rgba(200,205,215,.35)');
+    ctx.fillStyle = fogGrad; ctx.fillRect(0, 0, W, H);
+  }
+}
+
+/* ===== ЛОГИКА ===== */
+let lastTs = 0;
+
+function updateScene(dt) {
+  const l = L();
+  if (!state.model) return;
+
+  if (state.alarm && state.time > state.alarmEnd) state.alarm = false;
+
+  const diff = state.barrier.target - state.barrier.level;
+  if (Math.abs(diff) > 0.001) {
+    const speedUp = 1 / state.model.openTime;
+    const speedDown = 1 / state.model.closeTime;
+    const spd = diff > 0 ? speedUp : speedDown;
+    const step = Math.min(Math.abs(diff), dt * spd);
+    state.barrier.level += Math.sign(diff) * step;
+    state.barrier.moving = true;
+    if (Math.abs(diff - Math.sign(diff)*step) < 0.005) {
+      state.barrier.level = state.barrier.target;
+      state.barrier.moving = false;
+    }
+  } else {
+    state.barrier.moving = false;
+  }
+
+  state.skud.kw1 = state.barrier.level < 0.05;
+  state.skud.kw2 = state.barrier.level > 0.95;
+  if (state.skud.kw1) { state.skud.relayClose = false; }
+  if (state.skud.kw2) { state.skud.relayOpen = false; }
+
+  if (state.barrier.level > 0.95 && !state.holdMode && state.model.autoClose > 0 && !state.manualMode) {
+    state.autoCloseTimer += dt;
+    if (state.autoCloseTimer >= state.model.autoClose && !state.skud.photo && !state.skud.loop1 && !state.skud.loop2) {
+      closeBarrier();
+      state.autoCloseTimer = 0;
+    }
+  } else if (state.barrier.level < 0.95) {
+    state.autoCloseTimer = 0;
+  }
+
+  const stopX = l.pivotX - (state.model.armLen) - 60;
+
+  if (state.car) {
+    const c = state.car;
+    if (state.phase === 'approaching') {
+      if (c.x < stopX) {
+        c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
+      } else if (c.isPriora) {
+        c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
+        if (c.x >= l.pivotX - state.model.armLen - 20 && state.barrier.level < 0.5) {
+          c.x = l.pivotX - state.model.armLen - 20; crash();
+        } else if (c.x > W + 300) {
+          state.car = null; state.phase = 'idle';
+        }
+      } else {
+        state.phase = 'waiting';
+        setStatus('ОЖИДАНИЕ РЕШЕНИЯ', 'wait');
+        setHint('Сравните пропуска и примите решение.');
+        if (state.model.hasLoop) {
+          state.skud.loop1 = true;
+          logSkud('ПЕТЛЯ 1: занята', 'blu');
+          state.stats.sensors++;
+        }
+        renderPasses();
+      }
+    }
+    else if (state.phase === 'resolved') {
+      if (c.action === 'pass') {
+        if (state.barrier.level > 0.9) {
+          c.x += c.speed * dt; c.wheelAngle += c.speed * dt * 0.06;
+          if (state.model.hasPhoto) {
+            if (!state.skud.photo && c.x > l.pivotX - state.model.armLen - 40 && c.x < l.pivotX + 40) {
+              state.skud.photo = true;
+              logSkud('ФОТОЭЛЕМЕНТ: разрыв луча', 'yel');
+              state.stats.sensors++;
+            }
+            if (state.skud.photo && (c.x < l.pivotX - state.model.armLen - 60 || c.x > l.pivotX + 60)) {
+              state.skud.photo = false;
+            }
+          }
+          if (c.x > W + 300) {
+            logEvent('Автомобиль проехал КПП', 'ok');
+            state.stats.in++;
+            state.car = null; state.phase = 'idle';
+            if (state.model.hasLoop) { state.skud.loop1 = false; logSkud('ПЕТЛЯ 1: свободна'); }
+            if (state.skud.photo) state.skud.photo = false;
+            state.barrier.target = 0;
+            setStatus('ПОСТ СВОБОДЕН');
+            setHint('Нажмите <b>«Следующая машина»</b>.');
+            state.senior = null;
+          }
+        }
+      } else if (c.action === 'turn') {
+        c.x -= c.speed * dt; c.wheelAngle -= c.speed * dt * 0.06;
+        if (c.x < -350) {
+          logEvent('Автомобиль уехал', 'info');
+          state.car = null; state.phase = 'idle';
+          if (state.model.hasLoop) { state.skud.loop1 = false; logSkud('ПЕТЛЯ 1: свободна'); }
+          setStatus('ПОСТ СВОБОДЕН');
+          setHint('Нажмите <b>«Следующая машина»</b>.');
+          state.senior = null;
+        }
+      }
+    }
+  }
+
+  if (state.senior) {
+    const s = state.senior;
+    const SPEED = 90;
+    if (s.state === 'walk_to_lane') {
+      s.alpha = Math.min(1, (s.alpha || 0) + dt * 1.5);
+      s.walking = true; s.y = l.botLaneY;
+      const targetX = (state.car ? state.car.x : 0) + 140;
+      if (s.x > targetX) s.x -= SPEED * dt;
+      else s.state = 'walk_up';
+    } else if (s.state === 'walk_up') {
+      s.walking = true;
+      if (s.y > l.topLaneY + 2) s.y -= SPEED * 0.7 * dt;
+      else {
+        s.y = l.topLaneY; s.walking = false; s.state = 'review'; s.decideTimer = 0;
+        setStatus('СТАРШИЙ У МАШИНЫ', 'senior');
+        setHint('Старший смены проверяет документы...');
+        logEvent('Старший смены подошёл к машине', 'senior');
+      }
+    } else if (s.state === 'review') {
+      s.decideTimer += dt; s.walking = false;
+      if (s.decideTimer > 2.0) {
+        const allow = (state.correctDecision === 'allow');
+        if (allow) {
+          logEvent('Старший смены: пропуск действителен — пропускаем', 'senior');
+          setStatus('СТАРШИЙ ПРОПУСКАЕТ', 'ok');
+          state.car.action = 'pass';
+          openBarrier();
+          state.phase = 'resolved';
+          state.userDecision = 'allow';
+          renderPasses();
+        } else {
+          logEvent('Старший смены: пропуск недействителен — отказ', 'senior');
+          setStatus('СТАРШИЙ ОТКАЗЫВАЕТ', 'wait');
+          state.car.action = 'turn';
+          state.phase = 'resolved';
+          state.userDecision = 'deny';
+          state.stats.out++;
+          renderPasses();
+        }
+        s.state = 'returning';
+      }
+    } else if (s.state === 'returning') {
+      s.walking = true;
+      if (s.y < l.botLaneY - 2) s.y += SPEED * 0.7 * dt;
+      else {
+        s.y = l.botLaneY; s.x += SPEED * dt;
+        if (s.x > W + 60) state.senior = null;
+      }
+    }
+  }
+
+  if (state.phase === 'crash_scene') {
+    if (state.police) {
+      const p = state.police;
+      if (p.state === 'arriving') {
+        p.x -= p.speed * dt; p.wheelAngle -= p.speed * dt * 0.06;
+        if (p.x <= p.targetX) { p.x = p.targetX; p.state = 'stopped'; }
+      } else if (p.state === 'leaving') {
+        p.x += p.speed * dt; p.wheelAngle += p.speed * dt * 0.06;
+      }
+    }
+    if (state.tow) {
+      const t2 = state.tow;
+      if (t2.state === 'arriving') {
+        t2.x += t2.speed * dt; t2.wheelAngle -= t2.speed * dt * 0.06;
+        if (t2.x >= t2.targetX) { t2.x = t2.targetX; t2.state = 'stopped'; t2.stopTimer = 0; }
+      } else if (t2.state === 'stopped') {
+        t2.stopTimer = (t2.stopTimer || 0) + dt;
+      } else if (t2.state === 'leaving') {
+        t2.x -= t2.speed * dt; t2.wheelAngle += t2.speed * dt * 0.06;
+      }
+    }
+    if (state.car && state.car.towed && state.tow) {
+      state.car.x = state.tow.x + 220;
+      state.car.wheelAngle += state.tow.speed * dt * 0.06;
+    }
+    if (state.tow && state.tow.state === 'stopped' && state.tow.stopTimer > 1.2 && !state.towDriver && !state.leaving && state.car) {
+      const doorX = state.tow.x - 150;
+      state.towDriver = { x: doorX, y: l.topLaneY, doorX: doorX,
+        carApproachX: state.car.x - 160,
+        state: 'exiting', exitTimer: 0, alpha: 0, walking: false, attachTimer: 0, boardTimer: 0 };
+      state.tow.doorOpen = true;
+      logEvent('Водитель выходит из кабины эвакуатора', 'info');
+    }
+    if (state.towDriver) {
+      const td = state.towDriver;
+      const underY = l.topLaneBot + 25;
+      const SPD = 55;
+      if (td.state === 'exiting') {
+        td.exitTimer += dt; td.alpha = Math.min(1, td.exitTimer / 0.8);
+        if (td.exitTimer > 0.8) { td.alpha = 1; td.state = 'walking_out_down'; }
+      } else if (td.state === 'walking_out_down') {
+        if (td.y < underY - 2) { td.y += SPD * dt; td.walking = true; }
+        else { td.y = underY; td.state = 'walking_right'; }
+      } else if (td.state === 'walking_right') {
+        if (td.x < td.carApproachX - 2) { td.x += SPD * dt; td.walking = true; }
+        else { td.x = td.carApproachX; td.state = 'walking_up'; }
+      } else if (td.state === 'walking_up') {
+        if (td.y > l.topLaneY + 2) { td.y -= SPD * dt; td.walking = true; }
+        else { td.y = l.topLaneY; td.state = 'attaching'; td.attachTimer = 0; }
+      } else if (td.state === 'attaching') {
+        td.attachTimer += dt; td.walking = false;
+        if (td.attachTimer > 0.6 && !state.towCable) { state.towCable = true; logEvent('Автомобиль зацеплен', 'info'); }
+        if (td.attachTimer > 1.4) td.state = 'returning_down';
+      } else if (td.state === 'returning_down') {
+        if (td.y < underY - 2) { td.y += SPD * dt; td.walking = true; }
+        else { td.y = underY; td.state = 'returning_left'; }
+      } else if (td.state === 'returning_left') {
+        if (td.x > td.doorX + 2) { td.x -= SPD * dt; td.walking = true; }
+        else { td.x = td.doorX; td.state = 'returning_up'; }
+      } else if (td.state === 'returning_up') {
+        if (td.y > l.topLaneY + 2) { td.y -= SPD * dt; td.walking = true; }
+        else { td.y = l.topLaneY; td.state = 'boarding'; td.boardTimer = 0; td.walking = false; }
+      } else if (td.state === 'boarding') {
+        td.boardTimer += dt; td.walking = false;
+        td.alpha = Math.max(0, 1 - td.boardTimer / 0.8);
+        td.x += 20 * dt;
+        if (td.boardTimer > 0.8) {
+          td.state = 'gone'; td.alpha = 0;
+          if (state.tow) state.tow.doorOpen = false;
+          if (!state.leaving) {
+            state.leaving = true;
+            setTimeout(() => {
+              logEvent('Эвакуатор увозит автомобиль', 'info');
+              if (state.car) state.car.towed = true;
+              if (state.police) state.police.state = 'leaving';
+              if (state.tow) state.tow.state = 'leaving';
+            }, 600);
+          }
+        }
+      }
+    }
+    if (state.tow && state.tow.state === 'leaving' && state.tow.x < -600) {
+      state.car = null; state.police = null; state.tow = null;
+      state.towDriver = null; state.towCable = false; state.leaving = false;
+      state.phase = 'idle'; state.barrier.target = 0;
+      state.alarm = false; state.skud.alarm = false;
+      setStatus('ПОСТ СВОБОДЕН');
+      setHint('Инцидент исчерпан. Нажмите «Следующая машина».');
+      logEvent('Инцидент закрыт', 'ok');
+    }
+  }
+
+  updateWeatherParticles(dt);
+  updateSkudLeds();
+}
+
+function crash() {
+  logEvent('!!! СТОЛКНОВЕНИЕ СО ШЛАГБАУМОМ !!!', 'alarm');
+  setStatus('ТРЕВОГА !', 'alarm');
+  setHint('Нарушитель врезался. Вызваны полиция и эвакуатор.');
+  state.alarm = true;
+  state.alarmEnd = state.time + 5;
+  state.phase = 'crash_scene';
+  state.leaving = false;
+  state.stats.alarms++;
+  state.stats.breaches++;
+  state.skud.alarm = true;
+  if (state.model.hasImpact) {
+    state.skud.impact = true;
+    logSkud('ДАТЧИК УДАРА: сработал', 'alarm');
+    state.stats.sensors++;
+  }
+  if (state.car) { state.car.action = 'crashed'; state.car.speed = 0; }
+
+  const l = L();
+  for (let i = 0; i < 40; i++) {
+    state.sparks.push({
+      x: l.pivotX - state.model.armLen - 30, y: l.topLaneY - 40,
+      vx: (Math.random() - .5) * 400, vy: (Math.random() - .5) * 300 - 80,
+      life: 1
+    });
+  }
+  setTimeout(() => {
+    state.police = { x: W + 300, targetX: l.pivotX - 100, speed: 260, state: 'arriving', wheelAngle: 0 };
+    logEvent('Прибывает полицейский автомобиль', 'warn');
+  }, 700);
+  setTimeout(() => {
+    const carX = state.car ? state.car.x : (l.pivotX - state.model.armLen - 20);
+    state.tow = { x: -300, targetX: carX - 340, speed: 200, state: 'arriving',
+      wheelAngle: 0, stopTimer: 0, doorOpen: false };
+    logEvent('Прибывает эвакуатор', 'warn');
+  }, 1400);
+}
+
+/* ===== ОТРИСОВКА ===== */
+function drawScene() {
+  const l = L();
+  const t = state.time;
+  ctx.fillStyle = '#02040a'; ctx.fillRect(0, 0, W, H);
+  if (!state.model) return;
+
+  drawSky(); drawGround(); drawRoads();
+  drawBooth();
+  drawGuard(l.boothX + l.boothW + 25, l.boothBaseY);
+
+  if (state.towCable && state.tow && state.car) {
+    const fromX = state.tow.x + 140;
+    const fromY = l.topLaneY - 20;
+    const toX = state.car.x - 140;
+    const toY = l.topLaneY - 15;
+    ctx.strokeStyle = 'rgba(200,200,200,.85)'; ctx.lineWidth = 2;
+    ctx.beginPath(); ctx.moveTo(fromX, fromY);
+    ctx.quadraticCurveTo((fromX + toX) / 2, Math.max(fromY, toY) + 22, toX, toY); ctx.stroke();
+  }
+
+  drawBarrier();
+
+  if (state.car) {
+    if (state.car.isPriora) drawCarScaled(drawDarkCar, state.car.x, l.topLaneY, state.car.wheelAngle);
+    else drawCarScaled(drawSedan, state.car.x, l.topLaneY, state.car.color, state.car.wheelAngle);
+  }
+  if (state.police) drawCarScaled(drawPoliceCar, state.police.x, l.botLaneY, state.police.wheelAngle);
+  if (state.tow) drawCarScaled(drawTowTruck, state.tow.x, l.topLaneY, state.tow.wheelAngle, state.tow.doorOpen);
+
+  if (state.towDriver && state.towDriver.state !== 'gone') {
+    drawTowDriver(state.towDriver.x, state.towDriver.y, state.towDriver.walking, state.towDriver.alpha);
+  }
+  if (state.senior) {
+    drawSenior(state.senior.x, state.senior.y, state.senior.walking, state.senior.alpha);
+  }
+
+  state.sparks = state.sparks.filter(s => s.life > 0);
+  state.sparks.forEach(s => {
+    s.x += s.vx / 60; s.y += s.vy / 60; s.vy += 500 / 60; s.life -= 1.2 / 60;
+    ctx.fillStyle = `rgba(255,${Math.floor(160 + Math.random()*90)},60,${Math.max(0, s.life)})`;
+    ctx.beginPath(); ctx.arc(s.x, s.y, 2 + s.life * 3, 0, Math.PI * 2); ctx.fill();
+  });
+
+  drawWeatherParticles();
+
+  if (state.alarm) {
+    const pulse = 0.5 + Math.sin(t * 8) * 0.5;
+    ctx.strokeStyle = `rgba(255,40,40,${pulse * 0.5})`; ctx.lineWidth = 12;
+    ctx.strokeRect(6, 6, W - 12, H - 12);
+    ctx.fillStyle = `rgba(255,60,60,${0.5 + 0.3 * Math.abs(Math.sin(t * 5))})`;
+    ctx.font = 'bold 36px "Courier New"'; ctx.textAlign = 'center';
+    ctx.fillText('!!! ТРЕВОГА !!!', W / 2, 60); ctx.textAlign = 'left';
+  }
+
+  const hintText = {
+    idle: 'ПОСТ СВОБОДЕН · НАЖМИТЕ «СЛЕДУЮЩАЯ МАШИНА»',
+    approaching: 'АВТОМОБИЛЬ ПРИБЛИЖАЕТСЯ',
+    waiting: 'ОЖИДАНИЕ РЕШЕНИЯ ОПЕРАТОРА',
+    refuse_wait: 'МАШИНА ЖДЁТ · ОБЪЯСНИТЕ ПРИЧИНУ ИЛИ ВЫЗОВИТЕ СТАРШЕГО',
+    senior_walk: 'СТАРШИЙ СМЕНЫ ИДЁТ К МАШИНЕ',
+    resolved: 'ОБРАБОТКА РЕЗУЛЬТАТА',
+    crash_scene: 'НА МЕСТЕ ПРОИСШЕСТВИЯ'
+  }[state.phase] || '';
+
+  if (hintText) {
+    ctx.font = 'bold 12px "Courier New"';
+    const tw = ctx.measureText(hintText).width + 40;
+    ctx.fillStyle = 'rgba(10,14,20,.78)';
+    ctx.fillRect(W / 2 - tw / 2, 20, tw, 28);
+    ctx.strokeStyle = 'rgba(96,160,224,.5)'; ctx.lineWidth = 1;
+    ctx.strokeRect(W / 2 - tw / 2, 20, tw, 28);
+    ctx.fillStyle = '#a0c0e0'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
+    ctx.fillText(hintText, W / 2, 34); ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
+  }
+}
+
+function loop(ts) {
+  if (!lastTs) lastTs = ts;
+  const dt = Math.min(0.05, (ts - lastTs) / 1000);
+  lastTs = ts;
+  state.time += dt;
+  updateScene(dt);
+  drawScene();
+  requestAnimationFrame(loop);
+}
+
+function updateClock() {
+  const d = new Date();
+  document.getElementById('clock').textContent =
+    String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0')+':'+String(d.getSeconds()).padStart(2,'0');
+}
+setInterval(updateClock, 1000); updateClock();
+
+/* ===== СТАРТ ===== */
+resizeCanvas(); renderPasses(); renderLog();
+renderModelSwitcher();
+switchModel('B');
+logSkud('Запуск СКУД КПП-3...', 'ok');
+requestAnimationFrame(loop);
+</script>
+</body>
+</html>