#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266HTTPUpdateServer.h>
#include <ESP8266mDNS.h>
#include <ArduinoOTA.h>
#include <DNSServer.h>
#include <DHT.h>
#include <ArduinoJson.h>
#include <IRrecv.h>
#include <IRremoteESP8266.h>
#include <EEPROM.h>

#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);

#define IR_RECEIVE_PIN 5
IRrecv irrecv(IR_RECEIVE_PIN);
decode_results results;

#define CH_BUTTON 0xFF629D

#define RELAY1 12
#define RELAY2 13
#define RELAY3 14
#define RELAY4 15

#define RELAY_ON HIGH
#define RELAY_OFF LOW

ESP8266WebServer server(80);
ESP8266HTTPUpdateServer httpUpdater;
DNSServer dnsServer;
const byte DNS_PORT = 53;

const unsigned long WIFI_TIMEOUT = 15000;
bool apMode = false;
unsigned long wifiStartTime = 0;
String savedSSID = "";
String savedPassword = "";
String currentIP = "";
String staIP = "";
unsigned long lastConnectionAttempt = 0;
bool connectionInProgress = false;

float temperature = 0;
float humidity = 0;
int fanSpeed = 0;
bool autoMode = false;
bool dhtError = false;
String lastIRCommand = "";
unsigned long lastIRTime = 0;
bool initialized = false;
unsigned long uptimeStart = 0;
int irCommandCount = 0;

float tempThresholds[] = {24.0, 26.0, 28.0};
int lastAutoSpeed = -1;

struct FanTimer {
  bool active = false;
  unsigned long endTime = 0;
  unsigned long duration = 0;
  int targetSpeed = 0;
  unsigned long lastUpdate = 0;
  bool stopRequested = false;
};
FanTimer fanTimer;

struct IRButton {
  uint64_t code;
  String name;
  String function;
};
IRButton buttons[] = {
  {0xFF6897, "0", "Выключить"},
  {0xFF30CF, "1", "Скорость 1"},
  {0xFF18E7, "2", "Скорость 2"},
  {0xFF7A85, "3", "Скорость 3"},
  {0xFF10EF, "4", "Авторежим"},
  {CH_BUTTON, "CH", "Точка доступа"}
};
const int buttonCount = 6;
// Добавь глобально:
const char favicon_svg[] PROGMEM = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>"
  "<rect width='32' height='32' rx='8' fill='#4f8ef7'/>"
  "<circle cx='16' cy='16' r='3' fill='white'/>"
  "<path d='M16 13 C16 13 14 8 10 7 C8 6.5 7 8 8 10 C9 12 13 12 16 13Z' fill='white'/>"
  "<path d='M16 19 C16 19 18 24 22 25 C24 25.5 25 24 24 22 C23 20 19 20 16 19Z' fill='white'/>"
  "<path d='M13 16 C13 16 8 18 7 22 C6.5 24 8 25 10 24 C12 23 12 19 13 16Z' fill='white'/>"
  "<path d='M19 16 C19 16 24 14 25 10 C25.5 8 24 7 22 8 C20 9 20 13 19 16Z' fill='white'/>"
  "</svg>";

void handleFavicon() {
  server.send_P(200, "image/svg+xml", favicon_svg);
}

struct WiFiConfig {
  char ssid[32];
  char password[64];
};
WiFiConfig wifiConfig;

void initializeRelays();
void setFanSpeed(int speed);
int getAutoSpeedForTemperature(float temp);
void updateAutoMode();
void handleIRCommand(uint64_t command);
void loadWiFiConfig();
void saveWiFiConfig();
void startAccessPoint();
void connectToWiFi();
void checkWiFiConnection();
void handleInstr();
void handleRoot();
void handleGetData();
void handleSetSpeed();
void handleSetThresholds();
void handleSaveWifi();
void handleScanNetworks();
String getUptimeString();
void handleWifiConfig();
void handleConnectWifi();
void handleShowIP();
void handleWifiStatus();
void handleCaptivePortal();
void handleNotFound();
void updateFanTimer();
void startFanTimer(unsigned long seconds, int speed);
void stopFanTimer();
void handleSetTimer();
void handleStopTimer();
String getTimerString();

// =====================================================================
// ОБЩИЙ JS/CSS ДЛЯ ТЕМ И ИНСТРУКЦИИ (встраивается в обе страницы)
// =====================================================================

// =====================================================================
// HTML: СТРАНИЦА НАСТРОЙКИ WI-FI
// =====================================================================
// =====================================================================
// HTML: МОДАЛ ИНСТРУКЦИИ (единая версия для обеих страниц)
// =====================================================================
const char instr_modal_html[] PROGMEM = R"rawliteral(
<!-- MODAL: INSTRUCTION -->
<div class="instr-modal-bg" id="instr-modal">
  <div class="instr-modal">
    <div class="instr-hdr">
      <div class="instr-title">
        <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
        Инструкция по эксплуатации
      </div>
      <div style="display:flex;align-items:center;gap:8px">
        <div class="font-wrap" id="font-wrap-instr">
          <button class="icon-btn" onclick="toggleFontDropInstr()" title="Размер текста" style="width:36px;height:36px;border-radius:10px">
            <svg viewBox="0 0 24 24" fill="none" stroke="var(--muted)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20 L8 8 L12 20"/><path d="M5.5 16 h5"/><path d="M14 14 L16.5 8 L19 14"/><path d="M14.8 12.5 h3.4"/></svg>
          </button>
          <div class="font-drop" id="font-drop-instr">
        <div class="font-opt" onclick="selectFontScaleInstr(0)"><div class="font-dot">A</div> Обычный</div>
        <div class="font-opt" onclick="selectFontScaleInstr(1)"><div class="font-dot" style="font-size:15px">A</div> Крупный</div>
        <div class="font-opt" onclick="selectFontScaleInstr(2)"><div class="font-dot" style="font-size:17px">A</div> Очень крупный</div>
      </div>
        </div>
        <button class="instr-close" onclick="closeInstr()">&#x2715;</button>
      </div>
    </div>
    <div class="instr-body">
    <div class="instr-section">
        <div class="instr-section-title">Наш сайт</div>
        <div class="instr-item"><strong>1. Общая информация:</strong> Общую информацию по проекту можно найти на сайте <a href="https://anivan.ru">anivan.ru</a>.</div>
        <div class="instr-item"><strong>2. Версии прошивки:</strong> Разные версии прошивки для обновления или самостоятельных доработок можно найти на странице <a href="https://anivan.ru/firmware">anivan.ru/firmware</a>.</div>
        <div class="instr-item"><strong>3. Документация:</strong> Инструкцию, мануалы, дополнительную ифнормацию можно найти на странице <a href="https://anivan.ru/docs/getting-started">anivan.ru/docs/getting-started</a>.</div>

      </div>
      <div class="instr-section">
        <div class="instr-section-title">Настройка Wi-Fi</div>
        <div class="instr-item"><strong>1. Первый запуск:</strong> Устройство создаёт точку доступа <strong>SmartFan_Config</strong> (пароль: <strong>12345678</strong>). Подключитесь к ней с телефона или ноутбука.</div>
        <div class="instr-item"><strong>2. Откройте браузер</strong> и перейдите по адресу <strong>192.168.4.1</strong> — вы попадёте на страницу настройки Wi-Fi.</div>
        <div class="instr-item"><strong>3. Сканирование сетей:</strong> Нажмите «Сканировать сети», выберите вашу домашнюю сеть из списка и введите пароль.</div>
        <div class="instr-item"><strong>4. После подключения</strong> устройство получит IP-адрес от роутера — запомните его. Используйте этот адрес для дальнейшего управления.</div>
        <div class="instr-item"><strong>Кнопка «Остаться в AP»:</strong> Продолжить использование точки доступа без подключения к домашней сети.</div>
        <div class="instr-item"><strong>ИК-пульт (кнопка CH):</strong> Принудительно переключает устройство в режим точки доступа для перенастройки.</div>
      </div>
      <div class="instr-section">
        <div class="instr-section-title">Управление вентилятором</div>
        <div class="instr-item"><strong>Скорость 0 — Выкл:</strong> Полное отключение. Все реле (GPIO 12, 13, 14) разомкнуты.</div>
        <div class="instr-item"><strong>Скорость 1 — Низкая:</strong> Тихий режим, минимальный обдув. Реле 1 (GPIO 12). Идеально для ночи.</div>
        <div class="instr-item"><strong>Скорость 2 — Средняя:</strong> Оптимальный режим. Реле 2 (GPIO 13). Для повседневного использования.</div>
        <div class="instr-item"><strong>Скорость 3 — Высокая:</strong> Максимальная мощность. Реле 3 (GPIO 14). Быстрое охлаждение.</div>
        <div class="instr-item"><strong>Скорость 4 — Авторежим:</strong> Автоматическая регулировка по показаниям DHT22 согласно настроенным температурным порогам.</div>
        <div class="instr-item"><strong>ИК-управление:</strong> Кнопки 0–3 — прямое переключение скорости. Кнопка 4 — авторежим. Кнопка CH — точка доступа.</div>
        <div class="instr-item"><strong>Таймер:</strong> Задайте время (до 24 часов) и скорость — вентилятор автоматически выключится. Таймер отображается в интерфейсе с обратным отсчётом.</div>
        <div class="instr-item"><strong>Авторежим — пороги температур:</strong> Ниже порога 1 — выкл, порог 1–2 — скорость 1, порог 2–3 — скорость 2, выше 3 — скорость 3.</div>
      </div>
      <div class="instr-section">
        <div class="instr-section-title">Обновление прошивки (OTA)</div>
        <div class="instr-item"><strong>1.</strong> В разделе «Прошивка» нажмите «Обновить прошивку».</div>
        <div class="instr-item"><strong>2.</strong> Выберите файл прошивки с расширением <strong>.bin</strong> (или перетащите его мышкой в зону загрузки).</div>
        <div class="instr-item"><strong>3.</strong> Нажмите «Загрузить прошивку» — прогресс отображается на экране. После завершения устройство перезагрузится автоматически (~15 сек).</div>
        <div class="instr-item"><strong>4.</strong> Также доступен прямой интерфейс OTA по адресу <strong>http://[IP]/update</strong>.</div>
        <div class="instr-item"><strong>5. Arduino IDE (беспроводная прошивка):</strong> Когда устройство подключено к Wi-Fi, в Arduino IDE в меню <strong>Инструменты → Порт</strong> появится сетевой порт <strong>SmartFan at [IP]</strong>. Выберите его. Выберете плату Generic ESP8266 Module, а также Flash Size 4mb. Введите пароль <strong>12345678</strong> и загружайте прошивку по воздуху как обычно.</div>
        <div class="instr-item"><strong>6. mDNS:</strong> Устройство доступно по адресу <strong>smartfan.local</strong> (в браузере и из Arduino IDE) при условии, что ПК и ESP8266 в одной сети.</div>
        <div class="instr-item"><strong>⚠ Важно:</strong> Не отключайте питание во время обновления. Вентилятор будет остановлен на время прошивки.</div>
      </div>
      <div class="instr-section">
        <div class="instr-section-title">Рекомендации</div>
        <div class="instr-item"><strong>Стабильная сеть:</strong> Используйте Wi-Fi с постоянным SSID и паролем. Настройки хранятся в EEPROM и не теряются при перезагрузке.</div>
        <div class="instr-item"><strong>Размещение датчика DHT22:</strong> Вдали от прямого солнца, нагревательных приборов и сквозняков для точных показаний температуры и влажности.</div>
        <div class="instr-item"><strong>Пороги авторежима:</strong> Рекомендуемые значения: 24°C / 26°C / 28°C. Настраивайте под личный комфорт и климат помещения.</div>
        <div class="instr-item"><strong>Если устройство не отвечает:</strong> Перезагрузите питание. Нажмите CH на ИК-пульте для входа в режим AP и переподключения к Wi-Fi.</div>
        <div class="instr-item"><strong>ИК-приёмник (GPIO 5):</strong> Поддерживаются коды NEC-протокола. Если ваш пульт не работает — проверьте совместимость кодов кнопок.</div>
        <div class="instr-item"><strong>Мониторинг:</strong> В нижней части страницы отображается время последнего обновления и последняя команда ИК-пульта.</div>
      </div>
      <div class="instr-footer">
        ✦ Разработано полностью<br>
        <p><strong>Анисимовым Иваном Александровичем</strong><p/>
        <span style="font-size:12px;opacity:.7">Умный Вентилятор v2.12 · ESP8266 · DHT22 · IR · OTA (Browser + Arduino IDE)</span>
      </div>
    </div>
  </div>
</div>
)rawliteral";

void handleInstr() {
  server.send_P(200, "text/html; charset=UTF-8", instr_modal_html);
}

const char wifi_config_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="ru" data-theme="light">
<head>
<script>!function(){var t=localStorage.getItem("sf_theme")||"light";document.documentElement.setAttribute("data-theme",t);if(t==="custom"){try{var c=JSON.parse(localStorage.getItem("sf_custom")||"{}"),r=document.documentElement;if(c.bg)r.style.setProperty("--c-bg",c.bg);if(c.panel)r.style.setProperty("--c-panel",c.panel);if(c.card)r.style.setProperty("--c-card",c.card);if(c.accent){r.style.setProperty("--c-accent",c.accent);}if(c.text)r.style.setProperty("--c-text",c.text);if(c.border)r.style.setProperty("--c-border",c.border);}catch(e){}}}();</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=0.8, maximum-scale=1.2, user-scalable=yes">
<title>Настройка Wi-Fi</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
:root[data-theme="dark"]{
  --bg:#080e1a;--panel:#0f1926;--card:#0a1220;
  --accent:#4f8ef7;--accent2:#2563eb;--text:#dde6f5;
  --muted:#5a7099;--border:#182236;--ok:#0fba81;
  --warn:#e8a027;--err:#e8455a;--shadow:rgba(0,0,0,.5);
}
:root[data-theme="light"]{
  --bg:#f0f4fb;--panel:#ffffff;--card:#f7f9fd;
  --accent:#2563eb;--accent2:#1d4ed8;--text:#1a2540;
  --muted:#6b7fa3;--border:#d8e3f5;--ok:#059669;
  --warn:#d97706;--err:#dc2626;--shadow:rgba(37,99,235,.08);
}
:root[data-theme="custom"]{
  --bg:var(--c-bg,#080e1a);--panel:var(--c-panel,#0f1926);--card:var(--c-card,#0a1220);
  --accent:var(--c-accent,#4f8ef7);--accent2:var(--c-accent2,#2563eb);--text:var(--c-text,#dde6f5);
  --muted:var(--c-muted,#5a7099);--border:var(--c-border,#182236);--ok:#0fba81;
  --warn:#e8a027;--err:#e8455a;--shadow:rgba(0,0,0,.5);
}
*{margin:0;padding:0;box-sizing:border-box;font-family:'SF Pro Display','Segoe UI',system-ui,sans-serif;transition:background-color .3s,color .3s,border-color .3s}
body{background:var(--bg);color:var(--text);min-height:100vh;display:flex;align-items:flex-start;justify-content:center;padding:24px 20px 48px;}
body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(ellipse 60% 40% at 20% 10%,rgba(79,142,247,.07) 0%,transparent 60%),radial-gradient(ellipse 50% 30% at 80% 80%,rgba(124,58,237,.06) 0%,transparent 60%);pointer-events:none;z-index:0}
.page{width:100%;max-width:580px;padding-top:10px;position:relative;z-index:1}
.top-bar{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;gap:8px;}
.back-btn{display:inline-flex;align-items:center;gap:10px;color:var(--muted);font-size:15px;font-weight:500;text-decoration:none;padding:10px 0;transition:color .2s;background:none;border:none;cursor:pointer;}
.back-btn:hover{color:var(--accent)}
.back-btn svg{width:18px;height:18px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.top-btns{display:flex;gap:8px;align-items:center;}
.icon-btn{width:44px;height:44px;border-radius:12px;border:2px solid var(--border);background:var(--panel);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s;flex-shrink:0;position:relative;}
.icon-btn:hover{border-color:var(--accent);background:var(--card)}
.icon-btn svg{width:20px;height:20px;fill:none;stroke:var(--muted);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:stroke .3s}
.icon-btn:hover svg{stroke:var(--accent)}
.icon-sun{display:none}.icon-moon{display:block}
[data-theme="light"] .icon-sun{display:block}[data-theme="light"] .icon-moon{display:none}
[data-theme="custom"] .icon-sun{display:none}[data-theme="custom"] .icon-moon{display:none}
.icon-custom{display:none}[data-theme="custom"] .icon-custom{display:block}

/* Плавное масштабирование */
.page {
  transition: transform 0.2s ease, width 0.2s ease;
}
.instr-body {
  transition: transform 0.2s ease, width 0.2s ease;
}

/* FONT SIZE DROPDOWN */
.font-wrap{position:relative}
.font-drop{position:absolute;top:calc(100% + 8px);right:0;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:10px;width:190px;z-index:500;box-shadow:0 12px 40px var(--shadow);display:none;flex-direction:column;gap:4px;}
.font-drop.open{display:flex}
.font-opt{padding:10px 14px;border-radius:10px;cursor:pointer;font-size:14px;font-weight:600;border:2px solid transparent;display:flex;align-items:center;gap:12px;transition:all .18s;color:var(--text);}
.font-opt {
  justify-content: flex-start; /* или убрать justify-content, если не задано */
  text-align: left;
}
.font-opt:hover{background:var(--card);border-color:var(--border)}
.font-opt.active{border-color:var(--accent);background:rgba(79,142,247,.08)}
.font-dot{width:28px;height:28px;border-radius:8px;background:var(--card);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:var(--accent);flex-shrink:0;}
/* PALETTE DROPDOWN */
.palette-wrap{position:relative}
.palette-drop{position:absolute;top:calc(100% + 8px);right:0;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px;width:240px;z-index:500;box-shadow:0 12px 40px var(--shadow);display:none;flex-direction:column;gap:10px;}
.palette-drop.open{display:flex}
.palette-opt{padding:10px 14px;border-radius:10px;cursor:pointer;font-size:14px;font-weight:600;border:2px solid transparent;display:flex;align-items:center;gap:10px;transition:all .18s;}
.palette-opt:hover{background:var(--card);border-color:var(--border)}
.palette-opt.active{border-color:var(--accent);background:rgba(79,142,247,.08)}
.palette-dot{width:20px;height:20px;border-radius:50%;flex-shrink:0;border:2px solid rgba(255,255,255,.2);}
.palette-sep{height:1px;background:var(--border);margin:2px 0}
.palette-custom-row{display:flex;flex-direction:column;gap:8px;}
.palette-custom-row label{font-size:12px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.6px}
.color-row{display:grid;grid-template-columns:1fr 1fr;gap:6px;}
.color-field{display:flex;flex-direction:column;gap:4px;}
.color-field span{font-size:11px;color:var(--muted)}
.color-field input[type=color]{width:100%;height:36px;border:2px solid var(--border);border-radius:8px;background:var(--card);cursor:pointer;padding:2px;}
.palette-apply{width:100%;padding:10px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;margin-top:4px;transition:all .18s;}
.palette-apply:hover{background:var(--accent2)}

.hdr{text-align:center;margin-bottom:32px}
.hdr-icon{width:68px;height:68px;background:linear-gradient(135deg,var(--accent) 0%,#7c3aed 100%);border-radius:20px;display:flex;align-items:center;justify-content:center;margin:0 auto 16px;box-shadow:0 10px 36px rgba(79,142,247,.35),0 3px 12px var(--shadow)}
.hdr-icon svg{width:32px;height:32px;fill:none;stroke:#fff;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.hdr h1{font-size:26px;font-weight:700;color:var(--text);margin-bottom:6px;letter-spacing:-.4px}
.hdr p{font-size:15px;color:var(--muted)}
.card{background:var(--panel);border:1px solid var(--border);border-radius:22px;padding:24px;box-shadow:0 6px 28px var(--shadow)}
.card+.card{margin-top:16px}
.status-card{background:var(--panel);border:1px solid var(--border);border-radius:22px;padding:22px 24px;display:flex;align-items:center;gap:20px;margin-bottom:16px;box-shadow:0 6px 28px var(--shadow)}
.status-icon-wrap{width:56px;height:56px;flex-shrink:0;background:rgba(15,186,129,.1);border:1px solid rgba(15,186,129,.25);border-radius:14px;display:flex;align-items:center;justify-content:center}
.status-icon-wrap svg{width:24px;height:24px;fill:none;stroke:var(--ok);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.status-icon-wrap.ap{background:rgba(232,160,39,.1);border-color:rgba(232,160,39,.25)}
.status-icon-wrap.ap svg{stroke:var(--warn)}
.status-info{flex:1;min-width:0}
.status-lbl{font-size:13px;color:var(--muted);font-weight:500;text-transform:uppercase;letter-spacing:.9px;margin-bottom:4px}
.status-ip{font-size:24px;font-weight:800;color:var(--accent);font-family:'Courier New',monospace;letter-spacing:.8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.status-ssid{font-size:14px;color:var(--muted);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.status-dot{width:12px;height:12px;border-radius:50%;background:var(--muted);flex-shrink:0;transition:all .4s}
.status-dot.ok{background:var(--ok);box-shadow:0 0 12px var(--ok)}
.badge{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;border-radius:24px;font-size:12px;font-weight:700;margin-top:6px}
.badge-ap{background:rgba(232,160,39,.15);color:var(--warn);border:1px solid rgba(232,160,39,.3)}
.badge-sta{background:rgba(15,186,129,.12);color:var(--ok);border:1px solid rgba(15,186,129,.3)}
.sect-label{font-size:13px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:16px;display:flex;align-items:center;gap:10px}
.sect-label::after{content:'';flex:1;height:1px;background:var(--border)}
select,input[type=text],input[type=password]{width:100%;padding:14px 16px;background:var(--card);border:2px solid var(--border);border-radius:13px;color:var(--text);font-size:17px;transition:border .2s,box-shadow .2s;-webkit-appearance:none;outline:none}
select:focus,input:focus{border-color:var(--accent);box-shadow:0 0 0 4px rgba(79,142,247,.15)}
.field-wrap{margin-bottom:14px}
.field-wrap label{display:block;font-size:14px;color:var(--muted);margin-bottom:8px;font-weight:600;letter-spacing:.3px}
.field-wrap:last-child{margin-bottom:0}
.btn{width:100%;padding:16px;border:none;border-radius:13px;font-size:16px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:10px;transition:all .18s;letter-spacing:.2px}
.btn+.btn{margin-top:10px}
.btn-primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 6px 20px rgba(79,142,247,.4)}
.btn-primary:hover{transform:translateY(-2px);box-shadow:0 8px 28px rgba(79,142,247,.5)}
.btn-secondary{background:var(--card);color:var(--text);border:2px solid var(--border)}
.btn-secondary:hover{background:var(--bg);border-color:rgba(79,142,247,.4)}
.btn-danger{background:rgba(232,69,90,.1);color:var(--err);border:2px solid rgba(232,69,90,.25)}
.btn-danger:hover{background:rgba(232,69,90,.2)}
.btn-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px}
.btn-row .btn{margin-top:0}
.scan-hint{text-align:center;font-size:13px;color:var(--muted);margin-top:10px;min-height:18px}
.toast{padding:14px 18px;border-radius:12px;font-size:15px;display:none;margin-top:16px;text-align:center;font-weight:600}
.toast.ok{background:rgba(15,186,129,.12);border:1px solid rgba(15,186,129,.35);color:var(--ok)}
.toast.err{background:rgba(232,69,90,.12);border:1px solid rgba(232,69,90,.35);color:var(--err)}
.toast.info{background:rgba(79,142,247,.12);border:1px solid rgba(79,142,247,.35);color:var(--accent)}
.spin{display:inline-block;width:16px;height:16px;border:3px solid rgba(255,255,255,.2);border-radius:50%;border-top-color:#fff;animation:spin 1s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.85);display:none;align-items:center;justify-content:center;z-index:999;padding:24px;backdrop-filter:blur(6px)}
.modal-bg.show{display:flex}
.modal-box{background:var(--panel);border:1px solid rgba(15,186,129,.35);border-radius:24px;padding:36px 28px;max-width:440px;width:100%;text-align:center;box-shadow:0 0 100px rgba(15,186,129,.15),0 28px 70px rgba(0,0,0,.8);animation:pop .35s cubic-bezier(.175,.885,.32,1.275)}
@keyframes pop{from{opacity:0;transform:scale(.85) translateY(20px)}to{opacity:1;transform:scale(1) translateY(0)}}
.modal-check{width:72px;height:72px;background:rgba(15,186,129,.1);border:2px solid rgba(15,186,129,.4);border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 20px}
.modal-check svg{width:34px;height:34px;stroke:var(--ok);fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}
.modal-title{font-size:24px;font-weight:800;color:var(--ok);margin-bottom:8px}
.modal-sub{font-size:15px;color:var(--muted);line-height:1.6;margin-bottom:24px}
.modal-ip-box{background:rgba(15,186,129,.07);border:1px solid rgba(15,186,129,.2);border-radius:16px;padding:20px;margin-bottom:26px}
.modal-ip-lbl{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
.modal-ip-val{font-size:38px;font-weight:800;color:var(--ok);font-family:'Courier New',monospace;letter-spacing:1.5px}
.modal-open{width:100%;padding:16px;background:var(--ok);color:#fff;border:none;border-radius:14px;font-size:17px;font-weight:700;cursor:pointer;margin-bottom:12px;box-shadow:0 6px 20px rgba(15,186,129,.4);transition:all .18s}
.modal-open:hover{background:#0a9e6e;transform:translateY(-1px)}
.modal-dismiss{width:100%;padding:13px;background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:14px;font-size:15px;cursor:pointer;transition:all .2s}
.modal-dismiss:hover{background:var(--card);color:var(--text)}

/* INSTRUCTION MODAL */
.instr-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.88);display:none;align-items:center;justify-content:center;z-index:1000;padding:12px;padding-top:calc(70px + env(safe-area-inset-top, 0px));padding-bottom:calc(10px + env(safe-area-inset-bottom, 0px));backdrop-filter:blur(8px)}
.instr-modal-bg.show{display:flex}
.instr-modal{background:var(--panel);border:1px solid var(--border);border-radius:24px;max-width:640px;width:98%;max-height:min(80vh, calc(100vh - 64px));overflow:hidden;display:flex;flex-direction:column;box-shadow:0 28px 80px rgba(0,0,0,.8);animation:islide .3s ease}

@keyframes islide{from{opacity:0;transform:translateY(-24px)}to{opacity:1;transform:translateY(0)}}
.instr-hdr{padding:24px 28px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.instr-title{font-size:20px;font-weight:800;display:flex;align-items:center;gap:10px;color:var(--accent)}
.instr-title svg{width:22px;height:22px;fill:none;stroke:var(--accent);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.instr-close{background:none;border:none;color:var(--muted);font-size:26px;cursor:pointer;line-height:1;padding:4px;transition:color .2s}
.instr-close:hover{color:var(--err)}
.instr-body{padding:24px 28px;overflow-y:auto;flex:1;min-height:0;font-size:14px;-webkit-overflow-scrolling:touch}
.instr-section{margin-bottom:24px}
.instr-section:last-child{margin-bottom:0}
.instr-section-title{font-size:1.05em;font-weight:800;color:var(--accent);text-transform:uppercase;letter-spacing:.8px;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.instr-section-title::before{content:'';width:4px;height:18px;background:var(--accent);border-radius:2px;display:inline-block;flex-shrink:0}
.instr-item{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:12px 16px;margin-bottom:8px;font-size:1em;line-height:1.6;color:var(--text)}
.instr-item strong{color:var(--accent);font-weight:700}
.instr-item:last-child{margin-bottom:0}
.instr-footer{margin-top:18px;padding:16px;background:linear-gradient(135deg,rgba(79,142,247,.08),rgba(124,58,237,.08));border:1px solid rgba(79,142,247,.2);border-radius:14px;text-align:center;font-size:13px;color:var(--muted);line-height:1.7}
.instr-footer strong{color:var(--accent);font-size:14px}
</style>
</head>
<body>
<div class="page">
  <div class="top-bar">
    <button class="back-btn" onclick="window.location='/'">
      <svg viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
      Назад к управлению
    </button>
    <div class="top-btns">
      <button class="icon-btn" onclick="openInstr()" title="Инструкция">
        <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
      </button>

      <div class="palette-wrap" id="palette-wrap">
        <button class="icon-btn" onclick="togglePalette()" title="Цветовая тема" id="theme-btn">
          <svg class="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
          <svg class="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
          <svg class="icon-custom" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a10 10 0 0 1 0 20" stroke-dasharray="4 2"/></svg>
        </button>
        <div class="palette-drop" id="palette-drop">
          <div class="palette-opt" data-theme="dark" onclick="selectTheme('dark')">
            <div class="palette-dot" style="background:#080e1a;border-color:#4f8ef7"></div> Тёмная
          </div>
          <div class="palette-opt" data-theme="light" onclick="selectTheme('light')">
            <div class="palette-dot" style="background:#f0f4fb;border-color:#2563eb"></div> Светлая
          </div>
          <div class="palette-sep"></div>
          <div class="palette-opt" data-theme="custom" onclick="selectTheme('custom')">
            <div class="palette-dot" style="background:linear-gradient(135deg,#ff6b6b,#4ecdc4)"></div> Своя палитра
          </div>
          <div class="palette-custom-row" id="custom-colors" style="display:none">
            <label>Настройка цветов</label>
            <div class="color-row">
              <div class="color-field"><span>Фон</span><input type="color" id="c-bg" value="#080e1a" oninput="applyCustom()"></div>
              <div class="color-field"><span>Панель</span><input type="color" id="c-panel" value="#0f1926" oninput="applyCustom()"></div>
            </div>
            <div class="color-row">
              <div class="color-field"><span>Карточка</span><input type="color" id="c-card" value="#0a1220" oninput="applyCustom()"></div>
              <div class="color-field"><span>Акцент</span><input type="color" id="c-accent" value="#4f8ef7" oninput="applyCustom()"></div>
            </div>
            <div class="color-row">
              <div class="color-field"><span>Текст</span><input type="color" id="c-text" value="#dde6f5" oninput="applyCustom()"></div>
              <div class="color-field"><span>Граница</span><input type="color" id="c-border" value="#182236" oninput="applyCustom()"></div>
            </div>
            <button class="palette-apply" onclick="saveCustom()">Применить и сохранить</button>
          </div>
        </div>
      </div>
    </div>
  </div>
  <div class="hdr">
    <div class="hdr-icon">
      <svg viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
    </div>
    <h1>Настройка Wi-Fi</h1>
    <p>Умный Вентилятор</p>
  </div>
  <div class="status-card">
    <div class="status-icon-wrap" id="status-icon-wrap">
      <svg viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
    </div>
    <div class="status-info">
      <div class="status-lbl">Текущий IP-адрес</div>
      <div class="status-ip" id="cur-ip">—.—.—.—</div>
      <div class="status-ssid" id="ssid-txt">—</div>
      <span class="badge badge-sta" id="mode-badge">Клиент Wi-Fi</span>
    </div>
    <div class="status-dot" id="status-dot"></div>
  </div>
  <div class="card">
    <div class="sect-label">Выбор сети</div>
    <div class="field-wrap">
      <label>Доступные сети</label>
      <select id="net-select"><option value="">— нажмите Сканировать —</option></select>
    </div>
    <button class="btn btn-secondary" onclick="scanNetworks()" id="scan-btn">
      <span id="scan-lbl">🔍 Сканировать сети</span>
      <span id="scan-spin" style="display:none"><div class="spin"></div>&nbsp;Сканирование...</span>
    </button>
    <div class="scan-hint" id="scan-hint"></div>
  </div>
  <div class="card">
    <div class="sect-label">Данные для подключения</div>
    <div class="field-wrap">
      <label>Имя сети (SSID)</label>
      <input type="text" id="ssid-in" placeholder="Или введите вручную">
    </div>
    <div class="field-wrap">
      <label>Пароль</label>
      <input type="password" id="pass-in" placeholder="Пароль Wi-Fi">
    </div>
    <button class="btn btn-primary" onclick="saveWiFi()" id="connect-btn" style="margin-top:6px">
      <span id="connect-lbl">Сохранить и подключиться</span>
      <span id="connect-spin" style="display:none"><div class="spin"></div>&nbsp;Подключение...</span>
    </button>
    <div class="btn-row">
      <button class="btn btn-secondary" onclick="refreshStatus()">Обновить статус</button>
      <button class="btn btn-danger" onclick="stayInAPMode()">Остаться в AP</button>
    </div>
    <div class="toast" id="toast"></div>
  </div>
</div>

<!-- MODAL: SUCCESS -->
<div class="modal-bg" id="ip-modal">
  <div class="modal-box">
    <div class="modal-check"><svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg></div>
    <div class="modal-title">Подключено!</div>
    <div class="modal-sub">Устройство подключилось к Wi-Fi.<br>Запомните новый IP-адрес.</div>
    <div class="modal-ip-box">
      <div class="modal-ip-lbl">Новый IP-адрес</div>
      <div class="modal-ip-val" id="new-ip-val">—</div>
    </div>
    <button class="modal-open" onclick="openNewIP()">Открыть управление</button>
    <button class="modal-dismiss" onclick="closeModal()">Закрыть</button>
  </div>
</div>

<div id="instr-placeholder"></div>

<script>
var newIP='',statusInt=null;

/* THEME */
function applyTheme(t){document.documentElement.setAttribute('data-theme',t);localStorage.setItem('sf_theme',t);}
function selectTheme(t){
  applyTheme(t);
  document.querySelectorAll('.palette-opt').forEach(function(el){el.classList.remove('active');});
  var opt=document.querySelector('.palette-opt[data-theme="'+t+'"]');if(opt)opt.classList.add('active');
  document.getElementById('custom-colors').style.display=(t==='custom')?'flex':'none';
  if(t!=='custom')closePalette();
}
function togglePalette(){var d=document.getElementById('palette-drop');d.classList.toggle('open');}
function closePalette(){document.getElementById('palette-drop').classList.remove('open');}
function applyCustom(){
  var root=document.documentElement;
  root.style.setProperty('--c-bg',document.getElementById('c-bg').value);
  root.style.setProperty('--c-panel',document.getElementById('c-panel').value);
  root.style.setProperty('--c-card',document.getElementById('c-card').value);
  root.style.setProperty('--c-accent',document.getElementById('c-accent').value);
  var acc=document.getElementById('c-accent').value;
  root.style.setProperty('--c-accent2',shadeColor(acc,-20));
  root.style.setProperty('--c-text',document.getElementById('c-text').value);
  root.style.setProperty('--c-border',document.getElementById('c-border').value);
  root.style.setProperty('--c-muted',blendColor(document.getElementById('c-text').value,document.getElementById('c-bg').value,0.4));
}
function shadeColor(c,p){var n=parseInt(c.slice(1),16),t=p<0?0:255,a=p<0?-p/100:p/100;return'#'+(0x1000000+(Math.round(((t-(n>>16))*a)+(n>>16)))*0x10000+(Math.round(((t-((n>>8)&0xff))*a)+((n>>8)&0xff)))*0x100+(Math.round(((t-(n&0xff))*a)+(n&0xff)))).toString(16).slice(1);}
function blendColor(c1,c2,r){var n1=parseInt(c1.slice(1),16),n2=parseInt(c2.slice(1),16);var ri=Math.round(((n1>>16)*(1-r))+((n2>>16)*r)),gi=Math.round((((n1>>8)&0xff)*(1-r))+(((n2>>8)&0xff)*r)),bi=Math.round(((n1&0xff)*(1-r))+((n2&0xff)*r));return'#'+(0x1000000+ri*0x10000+gi*0x100+bi).toString(16).slice(1);}
function saveCustom(){
  applyCustom();
  var customData={bg:document.getElementById('c-bg').value,panel:document.getElementById('c-panel').value,card:document.getElementById('c-card').value,accent:document.getElementById('c-accent').value,text:document.getElementById('c-text').value,border:document.getElementById('c-border').value};
  localStorage.setItem('sf_custom',JSON.stringify(customData));
  closePalette();
}
function loadCustomColors(){
  var s=localStorage.getItem('sf_custom');if(!s)return;
  var d=JSON.parse(s);
  document.getElementById('c-bg').value=d.bg||'#080e1a';
  document.getElementById('c-panel').value=d.panel||'#0f1926';
  document.getElementById('c-card').value=d.card||'#0a1220';
  document.getElementById('c-accent').value=d.accent||'#4f8ef7';
  document.getElementById('c-text').value=d.text||'#dde6f5';
  document.getElementById('c-border').value=d.border||'#182236';
  applyCustom();
}
(function(){
  var t=localStorage.getItem('sf_theme')||'light';
  applyTheme(t);
  var opt=document.querySelector('.palette-opt[data-theme="'+t+'"]');if(opt)opt.classList.add('active');
  if(t==='custom'){document.getElementById('custom-colors').style.display='flex';loadCustomColors();}
  document.addEventListener('click',function(e){if(!document.getElementById('palette-wrap').contains(e.target))closePalette();});
})();

/* FONT SIZE - УНИВЕРСАЛЬНЫЙ ДЛЯ ПК И ТЕЛЕФОНОВ */
var fontScales = [1, 1.15, 1.3];
var fontIdx = 0;
var fontInstrIdx = 0;

function applyFontScale(idx) {
  var scale = fontScales[idx];
  var page = document.querySelector('.page');
  if (!page) return;
  
  // Определяем мобильное устройство
  var isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
  
  if (isMobile) {
    // Для мобильных - transform scale с плавностью
    page.style.transform = 'scale(' + scale + ')';
    page.style.transformOrigin = 'top left';
    page.style.width = (100 / scale) + '%';
    page.style.zoom = '';
  } else {
    // Для ПК - zoom
    page.style.zoom = scale;
    page.style.transform = '';
    page.style.width = '';
  }
  
  document.querySelectorAll('.font-opt:not(#font-drop-instr .font-opt)').forEach(function(el, i) {
    el.classList.toggle('active', i === idx);
  });
}

function applyFontScaleInstr(idx) {
  var scales = [1, 1.15, 1.3];
  var sizes = [14, 17, 21];
  var scale = scales[idx];
  var body = document.querySelector('.instr-body');
  var modal = document.querySelector('.instr-modal');
  if (!body) return;
  
  var isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
  if (isMobile) {
    // На мобильных масштабируем шрифт и отступы пропорционально (без transform)
    body.style.fontSize = sizes[idx] + 'px';
    body.style.padding = Math.round(24 * scale) + 'px ' + Math.round(28 * scale) + 'px';
    body.style.transform = 'none';
    body.style.zoom = '1';
    // Усиливаем ограничение высоты для мобильных
    if (modal) modal.style.maxHeight = 'min(85vh, calc(100vh - 24px))';
  } else {
    // На ПК возвращаем стандартные отступы и снимаем ограничения
    body.style.fontSize = sizes[idx] + 'px';
    body.style.padding = '24px 28px';
    if (modal) modal.style.maxHeight = '';
  }
  document.querySelectorAll('#font-drop-instr .font-opt').forEach(function(el, i) {
    el.classList.toggle('active', i === idx);
  });
}

function selectFontScale(idx) {
  fontIdx = idx;
  applyFontScale(idx);
  localStorage.setItem('sf_fontsize', idx);
  closeFontDrop();
  // Принудительно обновляем отображение
  setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 100);
}

function selectFontScaleInstr(idx) {
  fontInstrIdx = idx;
  applyFontScaleInstr(idx);
  localStorage.setItem('sf_fontsize_instr', idx);
  closeFontDropInstr();
}

function toggleFontDrop() {
  document.getElementById('font-drop').classList.toggle('open');
}

function closeFontDrop() {
  document.getElementById('font-drop').classList.remove('open');
}

function toggleFontDropInstr() {
  var d = document.getElementById('font-drop-instr');
  if (d) d.classList.toggle('open');
}

function closeFontDropInstr() {
  var d = document.getElementById('font-drop-instr');
  if (d) d.classList.remove('open');
}

/* INSTRUCTION */
var _instrLoaded=false;
function openInstr(){
  if(_instrLoaded){
    var modal=document.getElementById('instr-modal');
    if(modal)modal.classList.add('show');
    applyFontScaleInstr(fontInstrIdx);
    return;
  }
  fetch('/instr').then(function(r){return r.text();}).then(function(html){
    document.getElementById('instr-placeholder').outerHTML=html;
    _instrLoaded=true;
    var bg=document.getElementById('instr-modal');
    bg.classList.add('show');
    bg.addEventListener('click',function(e){if(e.target===this)closeInstr();});
    applyFontScaleInstr(fontInstrIdx);
    document.addEventListener('click',function(e){var fw=document.getElementById('font-wrap-instr');if(fw&&!fw.contains(e.target))closeFontDropInstr();},{capture:false});
  }).catch(function(){});
}
function closeInstr(){var el=document.getElementById('instr-modal');if(el)el.classList.remove('show');}

document.addEventListener('keydown',function(e){if(e.key==='Escape')closeInstr();});

function toast(msg,type){var el=document.getElementById('toast');el.textContent=msg;el.className='toast '+type;el.style.display='block';clearTimeout(el._t);el._t=setTimeout(function(){el.style.display='none';},5000);}
function refreshStatus(){
  fetch('/wifi_status').then(function(r){return r.json();}).then(function(d){
    document.getElementById('cur-ip').textContent=d.ip;
    var ap=d.mode==='ap';
    var wrap=document.getElementById('status-icon-wrap');
    var badge=document.getElementById('mode-badge');
    wrap.className='status-icon-wrap'+(ap?' ap':'');
    badge.textContent=ap?'Точка доступа':'Клиент Wi-Fi';
    badge.className='badge '+(ap?'badge-ap':'badge-sta');
    document.getElementById('ssid-txt').textContent=d.ssid||'—';
    document.getElementById('status-dot').className='status-dot'+(d.connected?' ok':'');
  }).catch(function(){});
}
function scanNetworks(){
  document.getElementById('scan-lbl').style.display='none';
  document.getElementById('scan-spin').style.display='inline-flex';
  document.getElementById('scan-btn').disabled=true;
  var sel=document.getElementById('net-select');
  fetch('/scan').then(function(r){return r.json();}).then(function(d){
    sel.innerHTML='<option value="">— Выберите сеть —</option>';
    if(d.networks&&d.networks.length){
      d.networks.forEach(function(n){
        var q=Math.abs(n.rssi);var bars=q<60?'████':q<70?'███░':q<80?'██░░':'█░░░';
        var opt=document.createElement('option');opt.value=n.ssid;
        opt.textContent=n.ssid+'  '+bars+' '+n.rssi+'dBm';sel.appendChild(opt);
      });
      document.getElementById('scan-hint').textContent='Найдено: '+d.networks.length+' сетей';
    } else {sel.innerHTML='<option value="">Сети не найдены</option>';document.getElementById('scan-hint').textContent='Сети не найдены';}
  }).catch(function(){sel.innerHTML='<option value="">Ошибка сканирования</option>';document.getElementById('scan-hint').textContent='Ошибка сканирования';})
  .finally(function(){document.getElementById('scan-lbl').style.display='inline';document.getElementById('scan-spin').style.display='none';document.getElementById('scan-btn').disabled=false;});
}
function saveWiFi(){
  var ssid=document.getElementById('ssid-in').value;
  var pass=document.getElementById('pass-in').value;
  var sel=document.getElementById('net-select').value;
  if(sel){ssid=sel;document.getElementById('ssid-in').value=sel;}
  if(!ssid){toast('Введите или выберите имя сети','err');return;}
  if(!pass&&!confirm('Сеть без пароля. Продолжить?'))return;
  document.getElementById('connect-lbl').style.display='none';
  document.getElementById('connect-spin').style.display='inline-flex';
  document.getElementById('connect-btn').disabled=true;
  fetch('/savewifi?ssid='+encodeURIComponent(ssid)+'&password='+encodeURIComponent(pass))
    .then(function(r){if(!r.ok)throw new Error('save_failed');toast('Настройки сохранены. Подключаемся...','info');
      return fetch('/connect_wifi?ssid='+encodeURIComponent(ssid)+'&password='+encodeURIComponent(pass));
    }).then(function(r){return r.json();}).then(function(d){
      if(d.success){toast('Подключено! IP: '+d.ip,'ok');showIPModal(d.ip);}
      else toast('Ошибка подключения. Проверьте пароль.','err');
    }).catch(function(){toast('Ошибка связи. Попробуйте ещё раз.','err');})
    .finally(function(){document.getElementById('connect-lbl').style.display='inline';document.getElementById('connect-spin').style.display='none';document.getElementById('connect-btn').disabled=false;refreshStatus();});
}
function stayInAPMode(){fetch('/start_ap').then(function(r){return r.json();}).then(function(d){if(d.success){toast('Остаёмся в режиме точки доступа','ok');refreshStatus();}}).catch(function(){toast('Ошибка','err');});}
function showIPModal(ip){newIP=ip;document.getElementById('new-ip-val').textContent=ip;document.getElementById('ip-modal').classList.add('show');}
function closeModal(){document.getElementById('ip-modal').classList.remove('show');}
function openNewIP(){if(newIP)window.open('http://'+newIP,'_blank');}
window.onload=function(){
  refreshStatus();setTimeout(scanNetworks,800);statusInt=setInterval(refreshStatus,5000);
  document.getElementById('net-select').addEventListener('change',function(){if(this.value)document.getElementById('ssid-in').value=this.value;});
};
window.onunload=function(){if(statusInt)clearInterval(statusInt);};
</script>
</body>
</html>
)rawliteral";

// =====================================================================
// HTML: ГЛАВНЫЙ ИНТЕРФЕЙС
// =====================================================================
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML>
<html lang="ru" data-theme="light">
<head>
<script>!function(){var t=localStorage.getItem("sf_theme")||"light";document.documentElement.setAttribute("data-theme",t);if(t==="custom"){try{var c=JSON.parse(localStorage.getItem("sf_custom")||"{}"),r=document.documentElement;if(c.bg)r.style.setProperty("--c-bg",c.bg);if(c.panel)r.style.setProperty("--c-panel",c.panel);if(c.card)r.style.setProperty("--c-card",c.card);if(c.accent){r.style.setProperty("--c-accent",c.accent);}if(c.text)r.style.setProperty("--c-text",c.text);if(c.border)r.style.setProperty("--c-border",c.border);}catch(e){}}}();</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=0.75, maximum-scale=1.1, user-scalable=yes">
<title>Умный Вентилятор</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<style>
:root[data-theme="dark"]{
  --bg:#080e1a;--panel:#0f1926;--card:#0a1220;
  --accent:#4f8ef7;--accent2:#2563eb;--text:#dde6f5;
  --muted:#5a7099;--border:#182236;--ok:#0fba81;
  --warn:#e8a027;--err:#e8455a;--purple:#7c3aed;--pink:#d946a8;
  --s0:#4a5f7a;--s1:#0fba81;--s2:#e8a027;--s3:#e8455a;--s4:#7c3aed;
  --shadow:rgba(0,0,0,.3);--shadow-lg:rgba(0,0,0,.6);
}
:root[data-theme="light"]{
  --bg:#eef2fb;--panel:#ffffff;--card:#f4f7fd;
  --accent:#2563eb;--accent2:#1d4ed8;--text:#1a2540;
  --muted:#6b7fa3;--border:#d8e3f5;--ok:#059669;
  --warn:#d97706;--err:#dc2626;--purple:#7c3aed;--pink:#c026a0;
  --s0:#6b7fa3;--s1:#059669;--s2:#d97706;--s3:#dc2626;--s4:#7c3aed;
  --shadow:rgba(37,99,235,.07);--shadow-lg:rgba(37,99,235,.15);
}
:root[data-theme="custom"]{
  --bg:var(--c-bg,#080e1a);--panel:var(--c-panel,#0f1926);--card:var(--c-card,#0a1220);
  --accent:var(--c-accent,#4f8ef7);--accent2:var(--c-accent2,#2563eb);--text:var(--c-text,#dde6f5);
  --muted:var(--c-muted,#5a7099);--border:var(--c-border,#182236);
  --ok:#0fba81;--warn:#e8a027;--err:#e8455a;--purple:#7c3aed;--pink:#d946a8;
  --s0:#4a5f7a;--s1:#0fba81;--s2:#e8a027;--s3:#e8455a;--s4:#7c3aed;
  --shadow:rgba(0,0,0,.3);--shadow-lg:rgba(0,0,0,.6);
}
*{margin:0;padding:0;box-sizing:border-box;font-family:'SF Pro Display','Segoe UI',system-ui,sans-serif;-webkit-tap-highlight-color:transparent;transition:background-color .3s,color .3s,border-color .3s,box-shadow .3s}
body{background:var(--bg);color:var(--text);min-height:100vh;padding:18px 18px 36px;-webkit-text-size-adjust:100%}
body::before{content:'';position:fixed;inset:0;background-image:radial-gradient(ellipse 70% 50% at 10% 0%,rgba(79,142,247,.06) 0%,transparent 60%),radial-gradient(ellipse 60% 40% at 90% 90%,rgba(124,58,237,.05) 0%,transparent 60%);pointer-events:none;z-index:0}
[data-theme="light"] body::before{background-image:radial-gradient(ellipse 70% 50% at 10% 0%,rgba(37,99,235,.05) 0%,transparent 60%),radial-gradient(ellipse 60% 40% at 90% 90%,rgba(124,58,237,.04) 0%,transparent 60%);}
.wrap {
  max-width: 1200px;
  margin: 0 auto;
  position: relative;
  z-index: 1;
  padding: 0 1px; /* минимальные боковые отступы */
}

.page {
  max-width: 1200px;
  margin: 0 auto;
  position: relative;
  z-index: 1;
  padding: 0 1px;
}
/* Фикс для масштабирования на мобильных */
.page {
  transition: transform 0.2s ease, width 0.2s ease;
}
.instr-body {
  transition: transform 0.2s ease, width 0.2s ease;
}
/* HEADER */
.hdr{display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;padding:12px 0 24px;gap:14px;position:relative}
.hdr-icon{width:56px;height:56px;flex-shrink:0;background:linear-gradient(135deg,var(--accent),var(--purple));border-radius:16px;display:flex;align-items:center;justify-content:center;box-shadow:0 8px 28px rgba(79,142,247,.35)}
.hdr-icon svg{width:28px;height:28px;fill:none;stroke:#fff;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.hdr-text h1{font-size:28px;font-weight:800;letter-spacing:-.5px;line-height:1.15}
.hdr-text p{font-size:15px;color:var(--muted);margin-top:4px}
@media(max-width:420px){.hdr-text h1{font-size:23px}}

/* TOP CONTROLS */
.top-controls{position:absolute;top:12px;right:0;display:flex;gap:8px;align-items:center;}
.icon-btn{width:44px;height:44px;border-radius:13px;border:2px solid var(--border);background:var(--panel);cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 3px 12px var(--shadow);transition:all .22s;flex-shrink:0;position:relative;}
.icon-btn:hover{border-color:var(--accent);transform:scale(1.05)}
.icon-btn svg{width:20px;height:20px;fill:none;stroke:var(--muted);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;transition:stroke .3s}
.icon-btn:hover svg{stroke:var(--accent)}
.icon-sun{display:none}.icon-moon{display:block}
[data-theme="light"] .icon-sun{display:block}[data-theme="light"] .icon-moon{display:none}
[data-theme="custom"] .icon-sun{display:none}[data-theme="custom"] .icon-moon{display:none}
.icon-custom{display:none}[data-theme="custom"] .icon-custom{display:block}


/* FONT SIZE DROPDOWN */
.font-wrap{position:relative}
.font-drop{position:absolute;top:calc(100% + 8px);right:0;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:10px;width:190px;z-index:500;box-shadow:0 12px 40px var(--shadow);display:none;flex-direction:column;gap:4px;}
.font-drop.open{display:flex}
.font-opt{padding:10px 14px;border-radius:10px;cursor:pointer;font-size:14px;font-weight:600;border:2px solid transparent;display:flex;align-items:center;gap:12px;transition:all .18s;color:var(--text);}
.font-opt {
  justify-content: flex-start;
  text-align: left;
}
.font-opt:hover{background:var(--card);border-color:var(--border)}
.font-opt.active{border-color:var(--accent);background:rgba(79,142,247,.08)}
.font-dot{width:28px;height:28px;border-radius:8px;background:var(--card);border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:800;color:var(--accent);flex-shrink:0;}

/* PALETTE DROPDOWN */
.palette-wrap{position:relative}
.palette-drop{position:absolute;top:calc(100% + 8px);right:0;background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:16px;width:240px;z-index:500;box-shadow:0 12px 40px var(--shadow-lg);display:none;flex-direction:column;gap:10px;}
.palette-drop.open{display:flex}
.palette-opt{padding:10px 14px;border-radius:10px;cursor:pointer;font-size:14px;font-weight:600;border:2px solid transparent;display:flex;align-items:center;gap:10px;transition:all .18s;color:var(--text);}
.palette-opt:hover{background:var(--card);border-color:var(--border)}
.palette-opt.active{border-color:var(--accent);background:rgba(79,142,247,.08)}
.palette-dot{width:20px;height:20px;border-radius:50%;flex-shrink:0;border:2px solid rgba(255,255,255,.2);}
.palette-sep{height:1px;background:var(--border);margin:2px 0}
.palette-custom-row{display:flex;flex-direction:column;gap:8px;}
.palette-custom-row label{font-size:12px;color:var(--muted);font-weight:600;text-transform:uppercase;letter-spacing:.6px}
.color-row{display:grid;grid-template-columns:1fr 1fr;gap:6px;}
.color-field{display:flex;flex-direction:column;gap:4px;}
.color-field span{font-size:11px;color:var(--muted)}
.color-field input[type=color]{width:100%;height:36px;border:2px solid var(--border);border-radius:8px;background:var(--card);cursor:pointer;padding:2px;}
.palette-apply{width:100%;padding:10px;background:var(--accent);color:#fff;border:none;border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;margin-top:4px;transition:all .18s;}
.palette-apply:hover{background:var(--accent2)}

/* WI-FI BAR */
.wifi-bar{background:var(--panel);border:1px solid var(--border);border-radius:18px;padding:14px 18px;margin-bottom:18px;display:flex;align-items:center;gap:14px;box-shadow:0 3px 16px var(--shadow)}
.wifi-icon{width:42px;height:42px;flex-shrink:0;border-radius:11px;display:flex;align-items:center;justify-content:center;background:rgba(79,142,247,.1);border:1px solid rgba(79,142,247,.2)}
.wifi-icon svg{width:18px;height:18px;fill:none;stroke:var(--accent);stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.wifi-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.wifi-top{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.wbadge{padding:3px 10px;border-radius:22px;font-size:12px;font-weight:700;white-space:nowrap;flex-shrink:0}
.wbadge-ap{background:rgba(232,160,39,.15);color:var(--warn);border:1px solid rgba(232,160,39,.3)}
.wbadge-sta{background:rgba(15,186,129,.12);color:var(--ok);border:1px solid rgba(15,186,129,.25)}
.wip{font-family:'Courier New',monospace;font-size:16px;font-weight:700;color:var(--accent);white-space:nowrap}
.wssid{font-size:14px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.wifi-cfg{flex-shrink:0;background:var(--accent);color:#fff;border:none;border-radius:11px;padding:10px 18px;font-size:14px;font-weight:700;cursor:pointer;white-space:nowrap;text-decoration:none;display:inline-flex;align-items:center;gap:8px;transition:all .18s;box-shadow:0 4px 16px rgba(79,142,247,.3)}
.wifi-cfg:hover{background:var(--accent2);transform:translateY(-1px)}
.wifi-cfg svg{width:15px;height:15px;fill:none;stroke:#fff;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}

/* MAIN GRID */
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-bottom:18px;align-items:stretch;}
@media(max-width:820px){.grid2{grid-template-columns:1fr}}

/* PANEL */
.panel{background:var(--panel);border:1px solid var(--border);border-radius:22px;padding:24px;box-shadow:0 6px 24px var(--shadow);display:flex;flex-direction:column;}
.panel-hdr{display:flex;align-items:center;gap:14px;margin-bottom:20px;padding-bottom:16px;border-bottom:1px solid var(--border);flex-shrink:0;}
.pico{width:44px;height:44px;border-radius:12px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:rgba(79,142,247,.1)}
.pico svg{width:22px;height:22px;fill:none;stroke:var(--accent);stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.ptitle{font-size:19px;font-weight:700;letter-spacing:-.3px}
@media(max-width:460px){.panel{padding:18px}.ptitle{font-size:17px}}

/* SENSOR CARDS */
.sensor-stack{display:flex;flex-direction:column;gap:14px;flex:1}
.scard{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:22px 20px;display:flex;align-items:center;justify-content:space-between;gap:14px;transition:all .2s;flex:1;}
.scard:hover{background:var(--bg);border-color:rgba(79,142,247,.25);transform:translateY(-1px);box-shadow:0 6px 20px var(--shadow)}
.scard-left{display:flex;flex-direction:column;gap:10px}
.scard-lbl{font-size:14px;color:var(--muted);font-weight:500;letter-spacing:.3px}
.scard-val{display:flex;align-items:baseline;gap:6px}
.scard-num{font-size:52px;font-weight:800;letter-spacing:-2px;line-height:1}
.scard-unit{font-size:24px;font-weight:400;color:var(--muted)}
.temp-col{background:linear-gradient(135deg,#fbbf24,#f97316);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.hum-col{background:linear-gradient(135deg,#60a5fa,#34d399);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.scard-icon{width:54px;height:54px;border-radius:14px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.scard-icon svg{width:26px;height:26px;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.icon-temp{background:rgba(251,191,36,.1)}.icon-temp svg{stroke:#fbbf24}
.icon-hum{background:rgba(96,165,250,.1)}.icon-hum svg{stroke:#60a5fa}

/* CONTROL */
.ctrl-body{display:flex;flex-direction:column;flex:1;gap:0;}
.spd-bar{background:var(--card);border:1px solid var(--border);border-radius:13px;padding:14px 20px;display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;flex-shrink:0;}
.spd-bar-lbl{font-size:14px;color:var(--muted);font-weight:500}
.spd-bar-val{font-size:18px;font-weight:700;color:var(--accent)}

/* TIMER */
.tmr-ind{background:linear-gradient(135deg,rgba(217,70,168,.08),rgba(124,58,237,.08));border:1px solid rgba(217,70,168,.25);border-radius:14px;padding:15px 18px;margin-bottom:16px;display:none;flex-direction:column;gap:6px;animation:glowpulse 2.5s ease-in-out infinite;flex-shrink:0;}
@keyframes glowpulse{0%,100%{border-color:rgba(217,70,168,.25)}50%{border-color:rgba(217,70,168,.55);box-shadow:0 0 24px rgba(217,70,168,.1)}}
.tmr-hdr{font-size:12px;font-weight:700;color:var(--pink);text-transform:uppercase;letter-spacing:1px}
.tmr-clock{font-size:32px;font-weight:800;font-family:'Courier New',monospace;color:var(--text);letter-spacing:2px}
.tmr-sub{font-size:13px;color:var(--muted)}
.tmr-stop{margin-top:6px;padding:8px 16px;background:rgba(232,69,90,.12);color:var(--err);border:1px solid rgba(232,69,90,.25);border-radius:10px;font-size:14px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;gap:6px;width:fit-content;transition:all .18s}
.tmr-stop:hover{background:rgba(232,69,90,.25)}

/* SPEED BUTTONS */
.spd-grid{display:grid;grid-template-columns:repeat(5,1fr);grid-template-rows:1fr;gap:10px;width:100%;flex:1;min-height:100px;}
.spd-btn{display:flex;flex-direction:column;align-items:center;justify-content:center;height:auto;min-height:120px;padding:18px 12px;background:var(--card);border:2px solid var(--border);border-radius:14px;color:var(--text);cursor:pointer;transition:all .22s;gap:12px;width:100%;min-width:0;overflow:hidden;user-select:none;touch-action:manipulation;}
.spd-btn:hover{transform:translateY(-2px);box-shadow:0 10px 24px var(--shadow-lg)}
.spd-btn:active{transform:translateY(0);box-shadow:none}
.spd-ico{line-height:0;flex-shrink:0}
.spd-ico svg{width:22px;height:22px;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;display:block}
.spd-num{font-size:31px;font-weight:800;line-height:1;flex-shrink:0}
.spd-lbl{font-size:12px;font-weight:600;color:var(--muted);text-align:center;line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;flex-shrink:0}
@media(max-width:940px) and (min-width:821px){.spd-btn{min-height:100px;  }.spd-num{font-size:27px}.spd-ico svg{width:18px;height:18px}.spd-lbl{font-size:10px}}
@media(max-width:820px){.spd-grid{gap:8px;flex:none;grid-template-rows:auto}.spd-btn{height:120px;min-height:unset;border-radius:14px;gap:7px;padding:14px 8px}.spd-ico svg{width:22px;height:22px}.spd-num{font-size:44px}.spd-lbl{font-size:15px}}
@media(max-width:440px){.spd-grid{gap:6px}.spd-btn{height:108px;border-radius:12px;gap:6px;padding:12px 6px}.spd-ico svg{width:20px;height:20px}.spd-num{font-size:40px}.spd-lbl{font-size:13px}}
.s0-btn{border-color:rgba(74,95,122,.4)}.s1-btn{border-color:rgba(15,186,129,.3)}.s2-btn{border-color:rgba(232,160,39,.3)}.s3-btn{border-color:rgba(232,69,90,.3)}.s4-btn{border-color:rgba(124,58,237,.3)}
.s0-btn.on{background:rgba(74,95,122,.2);border-color:var(--s0);box-shadow:0 0 20px rgba(74,95,122,.2)}.s1-btn.on{background:rgba(15,186,129,.12);border-color:var(--s1);box-shadow:0 0 20px rgba(15,186,129,.2)}.s2-btn.on{background:rgba(232,160,39,.12);border-color:var(--s2);box-shadow:0 0 20px rgba(232,160,39,.2)}.s3-btn.on{background:rgba(232,69,90,.12);border-color:var(--s3);box-shadow:0 0 20px rgba(232,69,90,.2)}.s4-btn.on{background:rgba(124,58,237,.12);border-color:var(--s4);box-shadow:0 0 20px rgba(124,58,237,.2)}
.s0-btn .spd-num{color:var(--s0)}.s1-btn .spd-num{color:var(--s1)}.s2-btn .spd-num{color:var(--s2)}.s3-btn .spd-num{color:var(--s3)}.s4-btn .spd-num{color:var(--s4)}

/* SETTINGS GRID */
.sgrid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 18px;
  margin-bottom: 18px;
  align-items: stretch;
}

@media(max-width: 820px) {
  .sgrid {
    grid-template-columns: repeat(2, 1fr);
    gap: 16px;
  }
}

@media(max-width: 560px) {
  .sgrid {
    grid-template-columns: 1fr;
    gap: 14px;
  }
}
.set-btns{display:flex;flex-direction:column;gap:11px;flex:1}
.set-btn{display:flex;align-items:center;gap:15px;padding:16px 18px;background:var(--card);border:2px solid var(--border);border-radius:14px;color:var(--text);cursor:pointer;text-align:left;width:100%;transition:all .2s;flex:1}
.set-btn:hover{transform:translateY(-1px);box-shadow:0 8px 22px var(--shadow-lg)}
.set-ico{width:42px;height:42px;border-radius:10px;flex-shrink:0;display:flex;align-items:center;justify-content:center}
.set-ico svg{width:18px;height:18px;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.set-texts{display:flex;flex-direction:column;gap:3px}
.set-title{font-size:15px;font-weight:600}
.set-desc{font-size:13px;color:var(--muted)}
.set-timer .set-ico{background:rgba(217,70,168,.1)}.set-timer .set-ico svg{stroke:var(--pink)}
.set-timer{border-color:rgba(217,70,168,.2)}.set-timer:hover{border-color:rgba(217,70,168,.45)}
.set-auto .set-ico{background:rgba(79,142,247,.1)}.set-auto .set-ico svg{stroke:var(--accent)}
.set-auto{border-color:rgba(79,142,247,.2)}.set-auto:hover{border-color:rgba(79,142,247,.45)}
.set-ota .set-ico{background:rgba(15,186,129,.1)}.set-ota .set-ico svg{stroke:var(--ok)}
.set-ota{border-color:rgba(15,186,129,.2)}.set-ota:hover{border-color:rgba(15,186,129,.45)}

/* FOOTER */
.footer{text-align:center;padding:18px 0 6px;border-top:1px solid var(--border);color:var(--muted);font-size:14px}
.fchips{display:flex;justify-content:center;gap:10px;margin-top:10px;flex-wrap:wrap}
.fchip{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:6px 14px;font-size:13px;color:var(--muted);display:flex;align-items:center;gap:6px}
.fchip svg{width:13px;height:13px;fill:none;stroke:var(--muted);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

/* MODALS */
.modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.8);display:none;align-items:center;justify-content:center;z-index:900;padding:20px;backdrop-filter:blur(4px)}
.modal-bg.show{display:flex}
.modal{background:var(--panel);border:1px solid var(--border);border-radius:24px;padding:28px 24px;max-width:520px;width:100%;max-height:92vh;overflow-y:auto;box-shadow:0 28px 70px var(--shadow-lg);animation:msl .28s ease}
@keyframes msl{from{opacity:0;transform:translateY(-20px)}to{opacity:1;transform:translateY(0)}}
.mhdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid var(--border)}
.mttl{font-size:20px;font-weight:700;display:flex;align-items:center;gap:10px}
.mttl svg{width:20px;height:20px;fill:none;stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round}
.mttl.pink{color:var(--pink)}.mttl.pink svg{stroke:var(--pink)}
.mttl.blue{color:var(--accent)}.mttl.blue svg{stroke:var(--accent)}
.mttl.green{color:var(--ok)}.mttl.green svg{stroke:var(--ok)}
.mclose{background:none;border:none;color:var(--muted);font-size:24px;cursor:pointer;padding:4px;line-height:1;transition:color .18s}
.mclose:hover{color:var(--err)}
.mgrp{margin-bottom:16px}
.mlbl{font-size:14px;color:var(--muted);margin-bottom:8px;font-weight:600;letter-spacing:.2px}
.minput,.msel{width:100%;padding:14px 16px;background:var(--card);border:2px solid var(--border);border-radius:12px;color:var(--text);font-size:17px;transition:border .2s,box-shadow .2s;-webkit-appearance:none;outline:none}
.minput:focus,.msel:focus{border-color:var(--accent);box-shadow:0 0 0 4px rgba(79,142,247,.15)}
.mact{width:100%;padding:16px;border:none;border-radius:13px;font-size:17px;font-weight:700;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:10px;margin-top:10px;transition:all .18s}
.mact svg{width:18px;height:18px;fill:none;stroke:#fff;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round}
.mact-pink{background:linear-gradient(135deg,var(--pink),#9d174d);color:#fff;box-shadow:0 6px 20px rgba(217,70,168,.35)}
.mact-pink:hover{transform:translateY(-1px)}
.mact-blue{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#fff;box-shadow:0 6px 20px rgba(79,142,247,.35)}
.mact-blue:hover{transform:translateY(-1px)}
.mact-green{background:linear-gradient(135deg,var(--ok),#059669);color:#fff;box-shadow:0 6px 20px rgba(15,186,129,.35)}
.mact-green:hover{transform:translateY(-1px)}
.mact-green:disabled{opacity:.5;cursor:not-allowed;transform:none}
.thr-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:22px}
@media(max-width:520px){.thr-grid{grid-template-columns:1fr}}
.thr-card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:16px;text-align:center}
.thr-lbl{font-size:13px;font-weight:700;color:var(--muted);margin-bottom:10px;text-transform:uppercase;letter-spacing:.8px}
.thr-in{width:100%;padding:14px;background:var(--bg);border:2px solid var(--border);border-radius:11px;color:var(--text);font-size:26px;font-weight:800;text-align:center;margin-bottom:8px;-webkit-appearance:none;-moz-appearance:textfield;outline:none}
.thr-in:focus{border-color:var(--accent)}
.thr-in::-webkit-outer-spin-button,.thr-in::-webkit-inner-spin-button{-webkit-appearance:none}
.thr-desc{font-size:12px;color:var(--muted);line-height:1.4}

/* OTA */
.ota-drop{border:2px dashed var(--border);border-radius:16px;padding:32px 20px;text-align:center;transition:all .2s;cursor:pointer;margin-bottom:16px;background:var(--card);}
.ota-drop:hover,.ota-drop.drag{border-color:var(--ok);background:rgba(15,186,129,.05)}
.ota-drop-ico{width:52px;height:52px;border-radius:14px;background:rgba(15,186,129,.1);border:1px solid rgba(15,186,129,.2);display:flex;align-items:center;justify-content:center;margin:0 auto 14px}
.ota-drop-ico svg{width:24px;height:24px;fill:none;stroke:var(--ok);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.ota-drop-title{font-size:16px;font-weight:700;color:var(--text);margin-bottom:4px}
.ota-drop-hint{font-size:13px;color:var(--muted)}
.ota-file-name{margin-top:10px;font-size:14px;font-weight:600;color:var(--ok);display:none}
.ota-progress{margin-top:16px;display:none}
.ota-prog-bar{width:100%;height:8px;background:var(--card);border-radius:99px;overflow:hidden;border:1px solid var(--border)}
.ota-prog-fill{height:100%;background:linear-gradient(90deg,var(--ok),#34d399);border-radius:99px;width:0%;transition:width .3s}
.ota-prog-txt{font-size:13px;color:var(--muted);margin-top:8px;text-align:center}
.ota-warn{background:rgba(232,160,39,.08);border:1px solid rgba(232,160,39,.25);border-radius:12px;padding:14px 16px;margin-bottom:16px;font-size:14px;color:var(--warn);line-height:1.5}
.ota-warn svg{display:inline;width:15px;height:15px;fill:none;stroke:var(--warn);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;vertical-align:middle;margin-right:6px}

/* NOTIFICATION */
.notif{position:fixed;top:20px;right:20px;padding:14px 22px;border-radius:14px;box-shadow:0 10px 32px var(--shadow-lg);z-index:1000;opacity:0;transform:translateX(110%);transition:all .3s cubic-bezier(.22,1,.36,1);font-size:15px;font-weight:600;max-width:360px;color:#fff;background:var(--ok)}
.notif.show{opacity:1;transform:translateX(0)}
.notif.err{background:var(--err)}
.notif.info{background:var(--accent)}
@media(max-width:560px){.notif{top:12px;right:12px;left:12px;max-width:none}}
.spin{display:inline-block;width:18px;height:18px;border:3px solid rgba(255,255,255,.2);border-radius:50%;border-top-color:#fff;animation:sp 1s linear infinite}
@keyframes sp{to{transform:rotate(360deg)}}

/* INSTRUCTION MODAL */
.instr-modal-bg{position:fixed;inset:0;background:rgba(0,0,0,.88);display:none;align-items:center;justify-content:center;z-index:1000;padding:12px;padding-top:calc(70px + env(safe-area-inset-top, 0px));padding-bottom:calc(10px + env(safe-area-inset-bottom, 0px));backdrop-filter:blur(8px)}
.instr-modal-bg.show{display:flex}
.instr-modal{background:var(--panel);border:1px solid var(--border);border-radius:24px;max-width:640px;width:98%;max-height:min(80vh, calc(100vh - 64px));overflow:hidden;display:flex;flex-direction:column;box-shadow:0 28px 80px rgba(0,0,0,.8);animation:islide .3s ease}

@keyframes islide{from{opacity:0;transform:translateY(-24px)}to{opacity:1;transform:translateY(0)}}
.instr-hdr{padding:24px 28px 20px;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-shrink:0}
.instr-title{font-size:20px;font-weight:800;display:flex;align-items:center;gap:10px;color:var(--accent)}
.instr-title svg{width:22px;height:22px;fill:none;stroke:var(--accent);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
.instr-close{background:none;border:none;color:var(--muted);font-size:26px;cursor:pointer;line-height:1;padding:4px;transition:color .2s}
.instr-close:hover{color:var(--err)}
.instr-body{padding:24px 28px;overflow-y:auto;flex:1;min-height:0;font-size:14px;-webkit-overflow-scrolling:touch}
.instr-section{margin-bottom:24px}
.instr-section:last-child{margin-bottom:0}
.instr-section-title{font-size:1.05em;font-weight:800;color:var(--accent);text-transform:uppercase;letter-spacing:.8px;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.instr-section-title::before{content:'';width:4px;height:18px;background:var(--accent);border-radius:2px;display:inline-block;flex-shrink:0}
.instr-item{background:var(--card);border:1px solid var(--border);border-radius:12px;padding:12px 16px;margin-bottom:8px;font-size:1em;line-height:1.6;color:var(--text)}
.instr-item strong{color:var(--accent);font-weight:700}
.instr-item:last-child{margin-bottom:0}
.instr-footer{margin-top:18px;padding:18px;background:linear-gradient(135deg,rgba(79,142,247,.08),rgba(124,58,237,.08));border:1px solid rgba(79,142,247,.2);border-radius:14px;text-align:center;font-size:13px;color:var(--muted);line-height:1.8}
.instr-footer strong{color:var(--accent);font-size:15px;display:block;margin-bottom:4px}
</style>
</head>
<body>
<div class="page">

<!-- HEADER -->
<div class="hdr">
  <div class="top-controls">
    <button class="icon-btn" onclick="openInstr()" title="Инструкция">
      <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
    </button>

    <div class="palette-wrap" id="palette-wrap">
      <button class="icon-btn" onclick="togglePalette()" title="Цветовая тема">
        <svg class="icon-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
        <svg class="icon-moon" viewBox="0 0 24 24"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
        <svg class="icon-custom" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a10 10 0 0 1 0 20" stroke-dasharray="4 2"/></svg>
      </button>
      <div class="palette-drop" id="palette-drop">
        <div class="palette-opt" data-theme="dark" onclick="selectTheme('dark')">
          <div class="palette-dot" style="background:#080e1a;border-color:#4f8ef7"></div> Тёмная
        </div>
        <div class="palette-opt" data-theme="light" onclick="selectTheme('light')">
          <div class="palette-dot" style="background:#f0f4fb;border-color:#2563eb"></div> Светлая
        </div>
        <div class="palette-sep"></div>
        <div class="palette-opt" data-theme="custom" onclick="selectTheme('custom')">
          <div class="palette-dot" style="background:linear-gradient(135deg,#ff6b6b,#4ecdc4)"></div> Своя палитра
        </div>
        <div class="palette-custom-row" id="custom-colors" style="display:none">
          <label>Настройка цветов</label>
          <div class="color-row">
            <div class="color-field"><span>Фон</span><input type="color" id="c-bg" value="#080e1a" oninput="applyCustom()"></div>
            <div class="color-field"><span>Панель</span><input type="color" id="c-panel" value="#0f1926" oninput="applyCustom()"></div>
          </div>
          <div class="color-row">
            <div class="color-field"><span>Карточка</span><input type="color" id="c-card" value="#0a1220" oninput="applyCustom()"></div>
            <div class="color-field"><span>Акцент</span><input type="color" id="c-accent" value="#4f8ef7" oninput="applyCustom()"></div>
          </div>
          <div class="color-row">
            <div class="color-field"><span>Текст</span><input type="color" id="c-text" value="#dde6f5" oninput="applyCustom()"></div>
            <div class="color-field"><span>Граница</span><input type="color" id="c-border" value="#182236" oninput="applyCustom()"></div>
          </div>
          <button class="palette-apply" onclick="saveCustom()">Применить и сохранить</button>
        </div>
      </div>
    </div>
  </div>
  <div class="hdr-icon">
   <img src="/favicon.svg" width="60" height="60" alt="SmartFan">
  </div>
  <div class="hdr-text">
    <h1>Умный Вентилятор</h1>
    <p>Интеллектуальное управление микроклиматом</p>
  </div>
</div>

<!-- WI-FI BAR -->
<div class="wifi-bar">
  <div class="wifi-icon">
    <svg viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
  </div>
  <div class="wifi-info">
    <div class="wifi-top">
      <span class="wbadge wbadge-sta" id="wmode">Клиент</span>
      <span class="wip" id="wip">—.—.—.—</span>
    </div>
    <div class="wssid" id="wssid">SSID: —</div>
  </div>
  <a href="/wifi_config" class="wifi-cfg">
    <svg viewBox="0 0 24 24"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
    Настройка
  </a>
</div>

<!-- MAIN GRID -->
<div class="grid2">
  <!-- ДАТЧИКИ -->
  <div class="panel">
    <div class="panel-hdr">
      <div class="pico"><svg viewBox="0 0 24 24"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"/></svg></div>
      <div class="ptitle">Датчики</div>
    </div>
    <div class="sensor-stack">
      <div class="scard">
        <div class="scard-left">
          <div class="scard-lbl">Температура</div>
          <div class="scard-val">
            <span class="scard-num temp-col" id="temperature">—</span>
            <span class="scard-unit">°C</span>
          </div>
        </div>
        <div class="scard-icon icon-temp"><svg viewBox="0 0 24 24"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"/></svg></div>
      </div>
      <div class="scard">
        <div class="scard-left">
          <div class="scard-lbl">Влажность</div>
          <div class="scard-val">
            <span class="scard-num hum-col" id="humidity">—</span>
            <span class="scard-unit">%</span>
          </div>
        </div>
        <div class="scard-icon icon-hum"><svg viewBox="0 0 24 24"><path d="M12 2.69l5.66 5.66a8 8 0 1 1-11.31 0z"/></svg></div>
      </div>
    </div>
  </div>

  <!-- УПРАВЛЕНИЕ -->
  <div class="panel">
    <div class="panel-hdr">
      <div class="pico"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg></div>
      <div class="ptitle">Управление</div>
    </div>
    <div class="ctrl-body">
      <div class="spd-bar">
        <span class="spd-bar-lbl" id="spd-bar-lbl">Текущая скорость</span>
        <span class="spd-bar-val" id="spd-label">Выключен</span>
      </div>
      <div class="tmr-ind" id="tmr-ind">
        <div class="tmr-hdr">⏱ Таймер</div>
        <div class="tmr-clock" id="tmr-clk">00:00:00</div>
        <div class="tmr-sub" id="tmr-sub"></div>
        <button class="tmr-stop" onclick="stopTimer()">
          <svg viewBox="0 0 24 24" style="width:13px;height:13px;fill:none;stroke:var(--err);stroke-width:2.5"><rect x="6" y="6" width="12" height="12" rx="1"/></svg>
          Остановить
        </button>
      </div>
      <div class="spd-grid">
        <button class="spd-btn s0-btn" onclick="setSpeed(0)">
          <div class="spd-ico"><svg viewBox="0 0 24 24" style="stroke:var(--s0)"><circle cx="12" cy="12" r="10"/><line x1="10" y1="15" x2="10" y2="9"/><line x1="14" y1="15" x2="14" y2="9"/></svg></div>
          <div class="spd-num">0</div><div class="spd-lbl">Выкл</div>
        </button>
        <button class="spd-btn s1-btn" onclick="setSpeed(1)">
          <div class="spd-ico"><svg viewBox="0 0 24 24" style="stroke:var(--s1)"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"/></svg></div>
          <div class="spd-num">1</div><div class="spd-lbl">Низкая</div>
        </button>
        <button class="spd-btn s2-btn" onclick="setSpeed(2)">
          <div class="spd-ico"><svg viewBox="0 0 24 24" style="stroke:var(--s2)"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"/></svg></div>
          <div class="spd-num">2</div><div class="spd-lbl">Средняя</div>
        </button>
        <button class="spd-btn s3-btn" onclick="setSpeed(3)">
          <div class="spd-ico"><svg viewBox="0 0 24 24" style="stroke:var(--s3)"><path d="M9.59 4.59A2 2 0 1 1 11 8H2m10.59 11.41A2 2 0 1 0 14 16H2m15.73-8.27A2.5 2.5 0 1 1 19.5 12H2"/></svg></div>
          <div class="spd-num">3</div><div class="spd-lbl">Высокая</div>
        </button>
        <button class="spd-btn s4-btn" onclick="setSpeed(4)">
          <div class="spd-ico"><svg viewBox="0 0 24 24" style="stroke:var(--s4)"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg></div>
          <div class="spd-num">4</div><div class="spd-lbl">Авто</div>
        </button>
      </div>
    </div>
  </div>
</div>

<!-- SETTINGS GRID -->
<div class="sgrid">
  <div class="panel">
    <div class="panel-hdr">
      <div class="pico" style="background:rgba(217,70,168,.1)"><svg viewBox="0 0 24 24" style="stroke:var(--pink)"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></div>
      <div class="ptitle">Таймер</div>
    </div>
    <div class="set-btns">
      <button class="set-btn set-timer" onclick="openTimerModal()">
        <div class="set-ico"><svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="12"/><path d="M5 3h14M5 21h14M3 7l2 2M19 7l-2 2M3 17l2-2M19 17l-2-2"/></svg></div>
        <div class="set-texts"><div class="set-title">Установить таймер</div><div class="set-desc">Задать время работы</div></div>
      </button>
      <button class="set-btn set-timer" onclick="stopTimer()">
        <div class="set-ico"><svg viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="1"/></svg></div>
        <div class="set-texts"><div class="set-title">Остановить таймер</div><div class="set-desc">Выключить и сбросить</div></div>
      </button>
    </div>
  </div>
  <div class="panel">
    <div class="panel-hdr">
      <div class="pico"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg></div>
      <div class="ptitle">Авторежим</div>
    </div>
    <div class="set-btns">
      <button class="set-btn set-auto" onclick="openAutoModal()">
        <div class="set-ico"><svg viewBox="0 0 24 24"><path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z"/></svg></div>
        <div class="set-texts"><div class="set-title">Настроить пороги</div><div class="set-desc">Температурные границы</div></div>
      </button>
      <button class="set-btn set-auto" onclick="setSpeed(4)">
        <div class="set-ico"><svg viewBox="0 0 24 24"><polyline points="5 12 12 5 19 12"/><polyline points="5 19 12 12 19 19"/></svg></div>
        <div class="set-texts"><div class="set-title">Включить авторежим</div><div class="set-desc">Управление по температуре</div></div>
      </button>
    </div>
  </div>
  <div class="panel">
    <div class="panel-hdr">
      <div class="pico" style="background:rgba(15,186,129,.1)"><svg viewBox="0 0 24 24" style="stroke:var(--ok)"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg></div>
      <div class="ptitle">Прошивка</div>
    </div>
    <div class="set-btns">
      <button class="set-btn set-ota" onclick="openOtaModal()">
        <div class="set-ico"><svg viewBox="0 0 24 24"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg></div>
        <div class="set-texts"><div class="set-title">Обновить прошивку</div><div class="set-desc">Загрузить .bin по Wi-Fi</div></div>
      </button>
    </div>
  </div>
</div>

<!-- FOOTER -->
<div class="footer">
  Умный Вентилятор v2.12
  <div class="fchips">
    <div class="fchip">
      <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
      <span>Обновление: <strong id="upd-time">—</strong></span>
    </div>
    <div class="fchip" id="ir-chip" style="display:none">
      <svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
      <span>ИК: <strong id="ir-last">—</strong></span>
    </div>
  </div>
</div>
</div><!-- /wrap -->

<!-- MODAL: TIMER -->
<div class="modal-bg" id="modal-timer">
  <div class="modal">
    <div class="mhdr">
      <div class="mttl pink"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>Настройка таймера</div>
      <button class="mclose" onclick="closeTimerModal()">&#x2715;</button>
    </div>
    <div class="mgrp"><div class="mlbl">Часы</div><input type="number" class="minput" id="th" min="0" max="23" value="0"></div>
    <div class="mgrp"><div class="mlbl">Минуты</div><input type="number" class="minput" id="tm" min="0" max="59" value="30"></div>
    <div class="mgrp"><div class="mlbl">Секунды</div><input type="number" class="minput" id="ts" min="0" max="59" value="0"></div>
    <div class="mgrp">
      <div class="mlbl">Скорость вентилятора</div>
      <select class="msel" id="tspd">
        <option value="1">Скорость 1 — Низкая</option>
        <option value="2" selected>Скорость 2 — Средняя</option>
        <option value="3">Скорость 3 — Высокая</option>
        <option value="4">Авторежим</option>
      </select>
    </div>
    <button class="mact mact-pink" onclick="startTimer()" id="timer-start-btn">
      <svg viewBox="0 0 24 24"><polygon points="5 3 19 12 5 21 5 3"/></svg>
      Запустить таймер
    </button>
  </div>
</div>

<!-- MODAL: AUTO -->
<div class="modal-bg" id="modal-auto">
  <div class="modal">
    <div class="mhdr">
      <div class="mttl blue"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M4.93 4.93a10 10 0 0 0 0 14.14"/></svg>Настройки авторежима</div>
      <button class="mclose" onclick="closeAutoModal()">&#x2715;</button>
    </div>
    <div class="thr-grid">
      <div class="thr-card"><div class="thr-lbl">Порог 1</div><input type="number" class="thr-in" id="t1" placeholder="24.0" step="0.5" min="15" max="35"><div class="thr-desc">Ниже — выключен</div></div>
      <div class="thr-card"><div class="thr-lbl">Порог 2</div><input type="number" class="thr-in" id="t2" placeholder="26.0" step="0.5" min="15" max="35"><div class="thr-desc">Низкая скорость</div></div>
      <div class="thr-card"><div class="thr-lbl">Порог 3</div><input type="number" class="thr-in" id="t3" placeholder="28.0" step="0.5" min="15" max="35"><div class="thr-desc">Средняя скорость</div></div>
    </div>
    <button class="mact mact-blue" onclick="saveThresholds()">
      <svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>
      Сохранить настройки
    </button>
  </div>
</div>

<!-- MODAL: OTA -->
<div class="modal-bg" id="modal-ota">
  <div class="modal">
    <div class="mhdr">
      <div class="mttl green"><svg viewBox="0 0 24 24"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg>Обновление прошивки</div>
      <button class="mclose" onclick="closeOtaModal()">&#x2715;</button>
    </div>
    <div class="ota-warn">
      <svg viewBox="0 0 24 24"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
      Во время загрузки вентилятор выключится. Устройство перезагрузится автоматически — это займёт ~15 секунд.
    </div>
    <div class="ota-drop" id="ota-drop" onclick="document.getElementById('ota-file').click()">
      <div class="ota-drop-ico"><svg viewBox="0 0 24 24"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg></div>
      <div class="ota-drop-title">Нажмите для выбора файла</div>
      <div class="ota-drop-hint">или перетащите .bin сюда</div>
      <div class="ota-file-name" id="ota-file-name"></div>
    </div>
    <input type="file" id="ota-file" accept=".bin" style="display:none" onchange="onFileSelected(this)">
    <div class="ota-progress" id="ota-progress">
      <div class="ota-prog-bar"><div class="ota-prog-fill" id="ota-fill"></div></div>
      <div class="ota-prog-txt" id="ota-prog-txt">Загрузка...</div>
    </div>
    <button class="mact mact-green" id="ota-btn" onclick="startOTA()" disabled>
      <svg viewBox="0 0 24 24"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg>
      Загрузить прошивку
    </button>
  </div>
</div>

<div id="instr-placeholder"></div>

<div id="notif" class="notif"></div>

<script>
var curSpd=0,loading=false,lastLoad=0;
var tmrActive=false,tmrLeft=0,tmrInfo='',tmrInterval=null;
var otaFile=null;

/* THEME & PALETTE */
function applyTheme(t){document.documentElement.setAttribute('data-theme',t);localStorage.setItem('sf_theme',t);}
function selectTheme(t){
  applyTheme(t);
  document.querySelectorAll('.palette-opt').forEach(function(el){el.classList.remove('active');});
  var opt=document.querySelector('.palette-opt[data-theme="'+t+'"]');if(opt)opt.classList.add('active');
  document.getElementById('custom-colors').style.display=(t==='custom')?'flex':'none';
  if(t==='custom')loadCustomColors();
  else closePalette();
}
function togglePalette(){document.getElementById('palette-drop').classList.toggle('open');}
function closePalette(){document.getElementById('palette-drop').classList.remove('open');}
function shadeColor(c,p){var n=parseInt(c.slice(1),16),t=p<0?0:255,a=p<0?-p/100:p/100;return'#'+(0x1000000+(Math.round(((t-(n>>16))*a)+(n>>16)))*0x10000+(Math.round(((t-((n>>8)&0xff))*a)+((n>>8)&0xff)))*0x100+(Math.round(((t-(n&0xff))*a)+(n&0xff)))).toString(16).slice(1);}
function blendColor(c1,c2,r){var n1=parseInt(c1.slice(1),16),n2=parseInt(c2.slice(1),16);var ri=Math.round(((n1>>16)*(1-r))+((n2>>16)*r)),gi=Math.round((((n1>>8)&0xff)*(1-r))+(((n2>>8)&0xff)*r)),bi=Math.round(((n1&0xff)*(1-r))+((n2&0xff)*r));return'#'+(0x1000000+ri*0x10000+gi*0x100+bi).toString(16).slice(1);}
function applyCustom(){
  var root=document.documentElement;
  var bg=document.getElementById('c-bg').value;
  var panel=document.getElementById('c-panel').value;
  var card=document.getElementById('c-card').value;
  var accent=document.getElementById('c-accent').value;
  var text=document.getElementById('c-text').value;
  var border=document.getElementById('c-border').value;
  root.style.setProperty('--c-bg',bg);
  root.style.setProperty('--c-panel',panel);
  root.style.setProperty('--c-card',card);
  root.style.setProperty('--c-accent',accent);
  root.style.setProperty('--c-accent2',shadeColor(accent,-20));
  root.style.setProperty('--c-text',text);
  root.style.setProperty('--c-border',border);
  root.style.setProperty('--c-muted',blendColor(text,bg,0.45));
  applyTheme('custom');
}
function saveCustom(){
  applyCustom();
  var d={bg:document.getElementById('c-bg').value,panel:document.getElementById('c-panel').value,card:document.getElementById('c-card').value,accent:document.getElementById('c-accent').value,text:document.getElementById('c-text').value,border:document.getElementById('c-border').value};
  localStorage.setItem('sf_custom',JSON.stringify(d));
  closePalette();
  notify('Палитра сохранена','');
}
function loadCustomColors(){
  var s=localStorage.getItem('sf_custom');if(!s)return;
  var d=JSON.parse(s);
  if(d.bg)document.getElementById('c-bg').value=d.bg;
  if(d.panel)document.getElementById('c-panel').value=d.panel;
  if(d.card)document.getElementById('c-card').value=d.card;
  if(d.accent)document.getElementById('c-accent').value=d.accent;
  if(d.text)document.getElementById('c-text').value=d.text;
  if(d.border)document.getElementById('c-border').value=d.border;
  applyCustom();
}
(function(){
  var t=localStorage.getItem('sf_theme')||'light';
  applyTheme(t);
  var opt=document.querySelector('.palette-opt[data-theme="'+t+'"]');if(opt)opt.classList.add('active');
  if(t==='custom'){document.getElementById('custom-colors').style.display='flex';loadCustomColors();}
  document.addEventListener('click',function(e){if(!document.getElementById('palette-wrap').contains(e.target))closePalette();});
})();

/* FONT SIZE - УНИВЕРСАЛЬНЫЙ ДЛЯ ПК И ТЕЛЕФОНОВ */
var fontScales = [1, 1.15, 1.3];
var fontIdx = 0;
var fontInstrIdx = 0;

function applyFontScale(idx) {
  var scale = fontScales[idx];
  var page = document.querySelector('.page');
  if (!page) return;
  
  // Определяем мобильное устройство по ширине экрана или user-agent
  var isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
  
  if (isMobile) {
    // Для мобильных - transform scale
    page.style.transform = 'scale(' + scale + ')';
    page.style.transformOrigin = 'top left';
    page.style.width = (100 / scale) + '%';
    page.style.zoom = ''; // сбрасываем zoom
  } else {
    // Для ПК - zoom
    page.style.zoom = scale;
    page.style.transform = '';
    page.style.width = '';
  }
  
  document.querySelectorAll('.font-opt:not(#font-drop-instr .font-opt)').forEach(function(el, i) {
    el.classList.toggle('active', i === idx);
  });
}

function applyFontScaleInstr(idx) {
  var scales = [1, 1.15, 1.3];
  var sizes = [14, 17, 21];
  var scale = scales[idx];
  var body = document.querySelector('.instr-body');
  var modal = document.querySelector('.instr-modal');
  if (!body) return;
  
  var isMobile = window.innerWidth <= 768 || /Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
  if (isMobile) {
    // На мобильных масштабируем шрифт и отступы пропорционально (без transform)
    body.style.fontSize = sizes[idx] + 'px';
    body.style.padding = Math.round(24 * scale) + 'px ' + Math.round(28 * scale) + 'px';
    body.style.transform = 'none';
    body.style.zoom = '1';
    // Усиливаем ограничение высоты для мобильных
    if (modal) modal.style.maxHeight = 'min(85vh, calc(100vh - 24px))';
  } else {
    // На ПК возвращаем стандартные отступы и снимаем ограничения
    body.style.fontSize = sizes[idx] + 'px';
    body.style.padding = '24px 28px';
    if (modal) modal.style.maxHeight = '';
  }
  document.querySelectorAll('#font-drop-instr .font-opt').forEach(function(el, i) {
    el.classList.toggle('active', i === idx);
  });
}

function selectFontScale(idx) {
  fontIdx = idx;
  applyFontScale(idx);
  localStorage.setItem('sf_fontsize', idx);
  closeFontDrop();
  // Принудительно обновляем отображение
  setTimeout(function() { window.dispatchEvent(new Event('resize')); }, 100);
}

function selectFontScaleInstr(idx) {
  fontInstrIdx = idx;
  applyFontScaleInstr(idx);
  localStorage.setItem('sf_fontsize_instr', idx);
  closeFontDropInstr();
}

function toggleFontDrop() {
  document.getElementById('font-drop').classList.toggle('open');
}

function closeFontDrop() {
  document.getElementById('font-drop').classList.remove('open');
}

function toggleFontDropInstr() {
  var d = document.getElementById('font-drop-instr');
  if (d) d.classList.toggle('open');
}

function closeFontDropInstr() {
  var d = document.getElementById('font-drop-instr');
  if (d) d.classList.remove('open');
}

/* INSTRUCTION */
var _instrLoaded = false;

function openInstr() {
  if (_instrLoaded) {
    document.getElementById('instr-modal').classList.add('show');
    setTimeout(function() { applyFontScaleInstr(fontInstrIdx); }, 50);
    return;
  }
  fetch('/instr').then(function(r) {
    return r.text();
  }).then(function(html) {
    document.getElementById('instr-placeholder').innerHTML = html;
    _instrLoaded = true;
    var bg = document.getElementById('instr-modal');
    bg.classList.add('show');
    bg.addEventListener('click', function(e) {
      if (e.target === this) closeInstr();
    });
    setTimeout(function() { applyFontScaleInstr(fontInstrIdx); }, 50);
    document.addEventListener('click', function(e) {
      var fw = document.getElementById('font-wrap-instr');
      if (fw && !fw.contains(e.target)) closeFontDropInstr();
    }, {capture: false});
  }).catch(function() {});
}

function closeInstr() {
  var el = document.getElementById('instr-modal');
  if (el) el.classList.remove('show');
}

(function() {
  var s = localStorage.getItem('sf_fontsize');
  if (s) fontIdx = parseInt(s) || 0;
  applyFontScale(fontIdx);
  
  var si = localStorage.getItem('sf_fontsize_instr');
  if (si) fontInstrIdx = parseInt(si) || 0;
  
  document.addEventListener('click', function(e) {
    var fw = document.getElementById('font-wrap');
    if (fw && !fw.contains(e.target)) closeFontDrop();
  });
})();

document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape') closeInstr();
});

function notify(msg,type){
  var el=document.getElementById('notif');
  el.textContent=msg;el.className='notif'+(type?' '+type:'');
  el.classList.add('show');clearTimeout(el._t);
  el._t=setTimeout(function(){el.classList.remove('show');},3000);
}
function fmt(s){var h=Math.floor(s/3600),m=Math.floor((s%3600)/60),ss=s%60;return String(h).padStart(2,'0')+':'+String(m).padStart(2,'0')+':'+String(ss).padStart(2,'0');}
function updateTimerUI(){
  var ind=document.getElementById('tmr-ind');
  if(tmrActive){document.getElementById('tmr-clk').textContent=fmt(tmrLeft);document.getElementById('tmr-sub').textContent=tmrInfo;ind.style.display='flex';}
  else{ind.style.display='none';}
}
function syncTimer(d){
  if(d.timer&&d.timer.active){
    if(!tmrActive){
      tmrActive=true;tmrLeft=d.timer.secondsLeft;
      tmrInfo='Скорость: '+(d.timer.targetSpeed===4?'Авто':d.timer.targetSpeed);
      if(!tmrInterval){tmrInterval=setInterval(function(){if(tmrLeft>0){tmrLeft--;updateTimerUI();}else{tmrActive=false;clearInterval(tmrInterval);tmrInterval=null;updateTimerUI();loadData();}},1000);}
    } else if(Math.abs(tmrLeft-d.timer.secondsLeft)>2){tmrLeft=d.timer.secondsLeft;}
  } else if(tmrActive){tmrActive=false;tmrLeft=0;if(tmrInterval){clearInterval(tmrInterval);tmrInterval=null;}}
  updateTimerUI();
}
function updateWifi(){
  fetch('/wifi_status').then(function(r){return r.json();}).then(function(d){
    var m=document.getElementById('wmode');
    if(d.mode==='ap'){m.textContent='Точка доступа';m.className='wbadge wbadge-ap';document.getElementById('wssid').textContent='SmartFan_Config';}
    else{m.textContent='Клиент';m.className='wbadge wbadge-sta';document.getElementById('wssid').textContent='SSID: '+(d.ssid||'—');}
    document.getElementById('wip').textContent=d.ip;
  }).catch(function(){});
}
var curAutoMode=false,curAutoSpeed=-1;
function updateUI(){
  for(var i=0;i<=4;i++){var b=document.querySelector('.s'+i+'-btn');if(b)b.classList.remove('on');}
  var a=document.querySelector('.s'+curSpd+'-btn');if(a)a.classList.add('on');
  var labs=['Выключен','Скорость 1 — Низкая','Скорость 2 — Средняя','Скорость 3 — Высокая','Авторежим'];
  document.getElementById('spd-label').textContent=labs[curSpd]||'—';
  var lbl=document.getElementById('spd-bar-lbl');
  if(lbl)lbl.textContent=curAutoMode?'Авторежим активен':'Текущая скорость';
}
function loadData(){
  if(loading)return;loading=true;lastLoad=Date.now();
  fetch('/getData').then(function(r){return r.json();}).then(function(d){
    document.getElementById('upd-time').textContent=new Date().toLocaleTimeString('ru-RU');
    if(d.temp!==undefined)document.getElementById('temperature').textContent=d.temp.toFixed(1);
    if(d.hum!==undefined)document.getElementById('humidity').textContent=d.hum.toFixed(1);
    curSpd=(d.speed!==undefined)?d.speed:0;
    curAutoMode=(d.autoMode===true);
    curAutoSpeed=(d.autoSpeed!==undefined&&d.autoSpeed!==null)?parseInt(d.autoSpeed):-1;
    if(d.lastIR){document.getElementById('ir-last').textContent=d.lastIR;document.getElementById('ir-chip').style.display='flex';}
    else{document.getElementById('ir-chip').style.display='none';}
    syncTimer(d);updateUI();loading=false;
  }).catch(function(){document.getElementById('upd-time').textContent='Ошибка';loading=false;});
}
function setSpeed(spd){
  fetch('/setSpeed?speed='+spd).then(function(r){return r.json();}).then(function(d){
    if(d.success){notify(d.message,'');loading=false;loadData();}
    else notify('Ошибка установки скорости','err');
  }).catch(function(){notify('Ошибка связи','err');});
}
function openTimerModal(){document.getElementById('modal-timer').classList.add('show');}
function closeTimerModal(){document.getElementById('modal-timer').classList.remove('show');}
function openAutoModal(){
  fetch('/getData').then(function(r){return r.json();}).then(function(d){
    if(d.thresholds&&d.thresholds.length===3){document.getElementById('t1').value=d.thresholds[0];document.getElementById('t2').value=d.thresholds[1];document.getElementById('t3').value=d.thresholds[2];}
    document.getElementById('modal-auto').classList.add('show');
  }).catch(function(){document.getElementById('modal-auto').classList.add('show');});
}
function closeAutoModal(){document.getElementById('modal-auto').classList.remove('show');}
function openOtaModal(){
  otaFile=null;
  document.getElementById('ota-file-name').style.display='none';document.getElementById('ota-file-name').textContent='';
  document.getElementById('ota-btn').disabled=true;
  document.getElementById('ota-progress').style.display='none';
  document.getElementById('ota-fill').style.width='0%';document.getElementById('ota-prog-txt').textContent='Загрузка...';
  document.getElementById('ota-file').value='';
  document.getElementById('modal-ota').classList.add('show');
}
function closeOtaModal(){document.getElementById('modal-ota').classList.remove('show');}
function onFileSelected(input){
  if(!input.files||!input.files[0])return;
  otaFile=input.files[0];
  var nm=document.getElementById('ota-file-name');
  nm.textContent='📦 '+otaFile.name+' ('+Math.round(otaFile.size/1024)+' КБ)';nm.style.display='block';
  document.getElementById('ota-btn').disabled=false;
}
(function(){
  var drop=document.getElementById('ota-drop');
  drop.addEventListener('dragover',function(e){e.preventDefault();drop.classList.add('drag');});
  drop.addEventListener('dragleave',function(){drop.classList.remove('drag');});
  drop.addEventListener('drop',function(e){
    e.preventDefault();drop.classList.remove('drag');
    var f=e.dataTransfer.files[0];
    if(f&&f.name.endsWith('.bin')){otaFile=f;var nm=document.getElementById('ota-file-name');nm.textContent='📦 '+f.name+' ('+Math.round(f.size/1024)+' КБ)';nm.style.display='block';document.getElementById('ota-btn').disabled=false;}
    else notify('Нужен файл с расширением .bin','err');
  });
})();
function startOTA(){
  if(!otaFile){notify('Сначала выберите .bin файл','err');return;}
  var btn=document.getElementById('ota-btn');btn.disabled=true;btn.innerHTML='<div class="spin"></div> Загрузка...';
  document.getElementById('ota-progress').style.display='block';
  var fd=new FormData();fd.append('firmware',otaFile,otaFile.name);
  var xhr=new XMLHttpRequest();xhr.open('POST','/update',true);
  xhr.upload.onprogress=function(e){if(e.lengthComputable){var pct=Math.round(e.loaded/e.total*100);document.getElementById('ota-fill').style.width=pct+'%';document.getElementById('ota-prog-txt').textContent='Загрузка: '+pct+'%';}};
  xhr.onload=function(){
    if(xhr.status===200){document.getElementById('ota-fill').style.width='100%';document.getElementById('ota-prog-txt').textContent='Готово! Устройство перезагружается...';notify('Прошивка загружена! Перезагрузка...','');setTimeout(function(){closeOtaModal();notify('Попытка переподключения...','info');setTimeout(function(){location.reload();},5000);},15000);}
    else{document.getElementById('ota-prog-txt').textContent='Ошибка загрузки (код '+xhr.status+')';notify('Ошибка загрузки прошивки','err');btn.disabled=false;btn.innerHTML='<svg viewBox="0 0 24 24"><polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/></svg> Загрузить прошивку';}
  };
  xhr.onerror=function(){document.getElementById('ota-fill').style.width='100%';document.getElementById('ota-prog-txt').textContent='Устройство перезагружается...';notify('Прошивка записана! Ожидаем перезагрузку...','');setTimeout(function(){closeOtaModal();setTimeout(function(){location.reload();},5000);},15000);};
  xhr.send(fd);
}
function startTimer(){
  var h=parseInt(document.getElementById('th').value)||0,m=parseInt(document.getElementById('tm').value)||0,s=parseInt(document.getElementById('ts').value)||0,spd=parseInt(document.getElementById('tspd').value);
  var tot=h*3600+m*60+s;
  if(tot<=0){notify('Введите корректное время','err');return;}
  if(tot>86400){notify('Максимум 24 часа','err');return;}
  var btn=document.getElementById('timer-start-btn');var orig=btn.innerHTML;
  btn.innerHTML='<div class="spin"></div> Запуск...';btn.disabled=true;
  fetch('/setTimer?hours='+h+'&minutes='+m+'&seconds='+s+'&speed='+spd)
    .then(function(r){return r.json();}).then(function(d){if(d.success){notify(d.message,'');closeTimerModal();loadData();}else notify('Ошибка: '+d.message,'err');})
    .catch(function(){notify('Ошибка связи','err');})
    .finally(function(){btn.innerHTML=orig;btn.disabled=false;});
}
function stopTimer(){
  fetch('/stopTimer').then(function(r){return r.json();}).then(function(d){
    if(d.success){notify('Таймер остановлен','');tmrActive=false;tmrLeft=0;if(tmrInterval){clearInterval(tmrInterval);tmrInterval=null;}updateTimerUI();loadData();}
    else notify('Ошибка','err');
  }).catch(function(){notify('Ошибка связи','err');});
}
function saveThresholds(){
  var ts=[parseFloat(document.getElementById('t1').value),parseFloat(document.getElementById('t2').value),parseFloat(document.getElementById('t3').value)];
  if(ts.some(function(x){return isNaN(x)||x<15||x>35;})){notify('Значения 15–35 °C','err');return;}
  if(ts[0]>=ts[1]||ts[1]>=ts[2]){notify('Пороги должны возрастать','err');return;}
  fetch('/setThresholds?t1='+ts[0]+'&t2='+ts[1]+'&t3='+ts[2]).then(function(r){if(r.ok){notify('Настройки сохранены','');closeAutoModal();loadData();}else notify('Ошибка сохранения','err');}).catch(function(){notify('Ошибка связи','err');});
}
['modal-timer','modal-auto','modal-ota'].forEach(function(id){document.getElementById(id).addEventListener('click',function(e){if(e.target===this)this.classList.remove('show');});});
document.addEventListener('keydown',function(e){if(e.key==='Escape'){closeTimerModal();closeAutoModal();closeOtaModal();closeInstr();}});
window.onload=function(){
  loadData();updateWifi();
  setInterval(function(){if(!loading&&Date.now()-lastLoad>1500)loadData();},1500);
  setInterval(updateWifi,6000);
};
</script>
</body>
</html>
)rawliteral";

// =====================================================================
// РЕЛЕ
// =====================================================================
void initializeRelays() {
  pinMode(RELAY1,OUTPUT);pinMode(RELAY2,OUTPUT);pinMode(RELAY3,OUTPUT);pinMode(RELAY4,OUTPUT);
  digitalWrite(RELAY1,RELAY_OFF);digitalWrite(RELAY2,RELAY_OFF);
  digitalWrite(RELAY3,RELAY_OFF);digitalWrite(RELAY4,RELAY_OFF);
  delay(50);initialized=true;
}

void setFanSpeed(int speed) {
  if(!initialized) initializeRelays();
  if(fanTimer.active&&speed!=fanTimer.targetSpeed) stopFanTimer();
  digitalWrite(RELAY1,RELAY_OFF);digitalWrite(RELAY2,RELAY_OFF);digitalWrite(RELAY3,RELAY_OFF);
  switch(speed){
    case 1:digitalWrite(RELAY1,RELAY_ON);fanSpeed=1;autoMode=false;lastAutoSpeed=-1;break;
    case 2:digitalWrite(RELAY2,RELAY_ON);fanSpeed=2;autoMode=false;lastAutoSpeed=-1;break;
    case 3:digitalWrite(RELAY3,RELAY_ON);fanSpeed=3;autoMode=false;lastAutoSpeed=-1;break;
    case 4:fanSpeed=4;autoMode=true;lastAutoSpeed=-1;updateAutoMode();break;
    default:fanSpeed=0;autoMode=false;lastAutoSpeed=-1;break;
  }
  Serial.print("Скорость: ");Serial.println(speed);
}

int getAutoSpeedForTemperature(float temp){
  if(temp<tempThresholds[0])return 0;
  else if(temp<tempThresholds[1])return 1;
  else if(temp<tempThresholds[2])return 2;
  else return 3;
}

void updateAutoMode(){
  if(!autoMode)return;
  int req=getAutoSpeedForTemperature(temperature);
  digitalWrite(RELAY1,RELAY_OFF);digitalWrite(RELAY2,RELAY_OFF);digitalWrite(RELAY3,RELAY_OFF);
  if(req==1)digitalWrite(RELAY1,RELAY_ON);
  else if(req==2)digitalWrite(RELAY2,RELAY_ON);
  else if(req==3)digitalWrite(RELAY3,RELAY_ON);
  lastAutoSpeed=req;fanSpeed=4;
}

// =====================================================================
// ТАЙМЕР
// =====================================================================
void updateFanTimer(){
  if(!fanTimer.active)return;
  unsigned long now=millis();
  if(fanTimer.stopRequested){
    setFanSpeed(0);fanTimer.active=false;fanTimer.duration=0;
    fanTimer.endTime=0;fanTimer.stopRequested=false;
    Serial.println("Таймер остановлен");return;
  }
  if(now>=fanTimer.endTime){
    setFanSpeed(0);fanTimer.active=false;fanTimer.duration=0;fanTimer.endTime=0;
    Serial.println("Таймер завершён");
  } else {
    fanTimer.duration=(fanTimer.endTime-now)/1000;
  }
}

void startFanTimer(unsigned long seconds,int speed){
  if(seconds==0||speed<0||speed>4)return;
  fanTimer.active=true;fanTimer.duration=seconds;
  fanTimer.endTime=millis()+(seconds*1000UL);
  fanTimer.targetSpeed=speed;fanTimer.lastUpdate=millis();
  fanTimer.stopRequested=false;
  setFanSpeed(speed);
}

void stopFanTimer(){
  if(fanTimer.active){fanTimer.stopRequested=true;}
}

String getTimerString(){
  if(!fanTimer.active)return"";
  unsigned long s=fanTimer.duration,m=s/60,h=m/60;
  s%=60;m%=60;
  String r="";
  if(h>0)r+=String(h)+"ч ";
  if(m>0||h>0)r+=String(m)+"м ";
  r+=String(s)+"с";
  return r;
}

// =====================================================================
// ИК
// =====================================================================
void handleIRCommand(uint64_t command){
  lastIRTime=millis();lastIRCommand="0x"+String(command,HEX);irCommandCount++;
  for(int i=0;i<buttonCount;i++){
    if(buttons[i].code==command){
      if     (buttons[i].function=="Выключить")    {setFanSpeed(0);stopFanTimer();}
      else if(buttons[i].function=="Скорость 1")   setFanSpeed(1);
      else if(buttons[i].function=="Скорость 2")   setFanSpeed(2);
      else if(buttons[i].function=="Скорость 3")   setFanSpeed(3);
      else if(buttons[i].function=="Авторежим")    setFanSpeed(4);
      else if(buttons[i].function=="Точка доступа") startAccessPoint();
      return;
    }
  }
}

// =====================================================================
// WI-FI
// =====================================================================
void loadWiFiConfig(){
  EEPROM.begin(sizeof(WiFiConfig));EEPROM.get(0,wifiConfig);EEPROM.end();
  savedSSID=String(wifiConfig.ssid);savedPassword=String(wifiConfig.password);
}

void saveWiFiConfig(){
  savedSSID.toCharArray(wifiConfig.ssid,sizeof(wifiConfig.ssid));
  savedPassword.toCharArray(wifiConfig.password,sizeof(wifiConfig.password));
  EEPROM.begin(sizeof(WiFiConfig));EEPROM.put(0,wifiConfig);EEPROM.commit();EEPROM.end();
}

void startAccessPoint(){
  Serial.println("Запуск точки доступа...");
  dnsServer.stop();
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP("SmartFan_Config","12345678");
  apMode=true;
  dnsServer.start(DNS_PORT,"*",WiFi.softAPIP());
  currentIP=WiFi.softAPIP().toString();
  Serial.println("AP IP: "+currentIP);
}

void connectToWiFi(){
  if(savedSSID.length()==0){startAccessPoint();return;}
  Serial.println("Подключение к: "+savedSSID);
  dnsServer.stop();
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP("SmartFan_Config","12345678");
  WiFi.begin(savedSSID.c_str(),savedPassword.c_str());
  connectionInProgress=true;wifiStartTime=millis();
  while(WiFi.status()!=WL_CONNECTED&&millis()-wifiStartTime<WIFI_TIMEOUT){delay(500);Serial.print(".");}
  connectionInProgress=false;
  if(WiFi.status()==WL_CONNECTED){
    staIP=WiFi.localIP().toString();apMode=true;
    currentIP=staIP;
    Serial.println("\nSTA IP: "+staIP);
    dnsServer.start(DNS_PORT,"*",WiFi.softAPIP());
  } else {
    Serial.println("\nНет соединения");startAccessPoint();
  }
}

void checkWiFiConnection(){
  if(!apMode&&WiFi.status()!=WL_CONNECTED){
    if(savedSSID.length()==0){startAccessPoint();return;}
    if(connectionInProgress&&millis()-wifiStartTime>=WIFI_TIMEOUT){connectionInProgress=false;startAccessPoint();}
    if(!connectionInProgress)connectToWiFi();
  }
}

// =====================================================================
// CAPTIVE PORTAL
// =====================================================================
void handleCaptivePortal(){
  server.sendHeader("Location","http://"+WiFi.softAPIP().toString()+"/wifi_config",true);
  server.send(302,"text/plain","");
}

void handleNotFound(){
  if(apMode)handleCaptivePortal();
  else server.send(404,"text/plain","404");
}

// =====================================================================
// HTTP HANDLERS
// =====================================================================
void handleRoot(){
  if(apMode&&WiFi.status()!=WL_CONNECTED)handleWifiConfig();
  else server.send(200,"text/html",index_html);
}

void handleWifiConfig(){server.send(200,"text/html",wifi_config_html);}

void handleWifiStatus(){
  StaticJsonDocument<256> doc;
  doc["mode"]=(WiFi.status()==WL_CONNECTED)?"sta":"ap";
  if(WiFi.status()==WL_CONNECTED){
    doc["ip"]=WiFi.localIP().toString();
    doc["ssid"]=savedSSID;
    doc["connected"]=true;
  } else {
    doc["ip"]=WiFi.softAPIP().toString();
    doc["ssid"]="SmartFan_Config";
    doc["connected"]=false;
  }
  doc["apMode"]=apMode;
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

void handleStartAP(){
  startAccessPoint();
  StaticJsonDocument<200> doc;
  doc["success"]=true;doc["ip"]=WiFi.softAPIP().toString();
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

void handleConnectWifi(){
  if(!server.hasArg("ssid")||!server.hasArg("password")){server.send(400,"text/plain","Missing");return;}
  savedSSID=server.arg("ssid");savedPassword=server.arg("password");
  saveWiFiConfig();
  String apIP=WiFi.softAPIP().toString();
  WiFi.begin(savedSSID.c_str(),savedPassword.c_str());
  connectionInProgress=true;wifiStartTime=millis();
  unsigned long t=millis();
  while(WiFi.status()!=WL_CONNECTED&&millis()-t<10000)delay(100);
  connectionInProgress=false;
  StaticJsonDocument<200> doc;
  if(WiFi.status()==WL_CONNECTED){
    String newIP=WiFi.localIP().toString();
    currentIP=newIP;
    doc["success"]=true;doc["ip"]=newIP;
    doc["ap_ip"]=apIP;
    doc["message"]="Connected";
  } else {
    doc["success"]=false;doc["message"]="Connection failed";
    currentIP=apIP;
  }
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

void handleShowIP(){
  StaticJsonDocument<200> doc;
  if(WiFi.status()==WL_CONNECTED){
    doc["ip"]=WiFi.localIP().toString();
    doc["mode"]="sta";
  } else {
    doc["ip"]=WiFi.softAPIP().toString();
    doc["mode"]="ap";
  }
  doc["ap_ip"]=WiFi.softAPIP().toString();
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

void handleScanNetworks(){
  int n=WiFi.scanNetworks();
  DynamicJsonDocument doc(4096);
  JsonArray nets=doc.createNestedArray("networks");
  for(int i=0;i<n;i++){
    String ssid=WiFi.SSID(i);
    if(ssid.length()==0)continue;
    bool dup=false;
    for(int j=0;j<i;j++){if(WiFi.SSID(j)==ssid){dup=true;break;}}
    if(!dup){JsonObject o=nets.createNestedObject();o["ssid"]=ssid;o["rssi"]=WiFi.RSSI(i);}
  }
  for(int i=0;i<(int)nets.size();i++)
    for(int j=i+1;j<(int)nets.size();j++)
      if(nets[i]["rssi"].as<int>()<nets[j]["rssi"].as<int>()){JsonObject t=nets[i];nets[i]=nets[j];nets[j]=t;}
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
  WiFi.scanDelete();
}

void handleGetData(){
  StaticJsonDocument<768> doc;
  doc["temp"]=temperature;doc["hum"]=humidity;
  doc["dhtError"]=dhtError;doc["speed"]=fanSpeed;doc["autoMode"]=autoMode;doc["autoSpeed"]=lastAutoSpeed;
  doc["uptime"]=getUptimeString();doc["irCount"]=irCommandCount;
  doc["freeHeap"]=ESP.getFreeHeap();
  if(lastIRTime>0&&millis()-lastIRTime<5000)doc["lastIR"]=lastIRCommand;
  JsonArray thr=doc.createNestedArray("thresholds");
  thr.add(tempThresholds[0]);thr.add(tempThresholds[1]);thr.add(tempThresholds[2]);
  JsonObject tmr=doc.createNestedObject("timer");
  tmr["active"]=fanTimer.active;tmr["secondsLeft"]=fanTimer.duration;tmr["targetSpeed"]=fanTimer.targetSpeed;
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

void handleSetSpeed(){
  if(!server.hasArg("speed")){server.send(400,"text/plain","Missing");return;}
  int spd=server.arg("speed").toInt();
  if(spd<0||spd>4){server.send(400,"text/plain","Invalid");return;}
  setFanSpeed(spd);
  StaticJsonDocument<200> doc;
  doc["success"]=true;
  const char* msgs[]={"Вентилятор выключен","Скорость 1 — Низкая","Скорость 2 — Средняя","Скорость 3 — Высокая","Авторежим включён"};
  doc["message"]=msgs[spd];
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

void handleSetThresholds(){
  if(!server.hasArg("t1")||!server.hasArg("t2")||!server.hasArg("t3")){server.send(400,"text/plain","Missing");return;}
  float a=server.arg("t1").toFloat(),b=server.arg("t2").toFloat(),c=server.arg("t3").toFloat();
  if(a>=b||b>=c){server.send(400,"text/plain","Order error");return;}
  tempThresholds[0]=a;tempThresholds[1]=b;tempThresholds[2]=c;
  if(autoMode)updateAutoMode();
  server.send(200,"text/plain","OK");
}

void handleSaveWifi(){
  if(!server.hasArg("ssid")||!server.hasArg("password")){server.send(400,"text/plain","Missing");return;}
  savedSSID=server.arg("ssid");savedPassword=server.arg("password");
  saveWiFiConfig();
  server.send(200,"text/plain","OK");
}

void handleSetTimer(){
  if(!server.hasArg("hours")||!server.hasArg("minutes")||!server.hasArg("seconds")||!server.hasArg("speed")){
    server.send(400,"application/json","{\"success\":false,\"message\":\"Нет параметров\"}");return;
  }
  int h=server.arg("hours").toInt(),m=server.arg("minutes").toInt();
  int s=server.arg("seconds").toInt(),spd=server.arg("speed").toInt();
  unsigned long tot=h*3600UL+m*60UL+s;
  if(tot==0){server.send(400,"application/json","{\"success\":false,\"message\":\"Время = 0\"}");return;}
  if(tot>86400){server.send(400,"application/json","{\"success\":false,\"message\":\"Макс 24 часа\"}");return;}
  if(spd<1||spd>4){server.send(400,"application/json","{\"success\":false,\"message\":\"Скорость 1-4\"}");return;}
  startFanTimer(tot,spd);
  StaticJsonDocument<200> doc;doc["success"]=true;
  String ts="";
  if(h>0)ts+=String(h)+"ч ";if(m>0)ts+=String(m)+"м ";if(s>0)ts+=String(s)+"с";
  doc["message"]="Таймер: "+ts;
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

void handleStopTimer(){
  stopFanTimer();
  StaticJsonDocument<200> doc;
  doc["success"]=true;doc["message"]="Таймер остановлен";
  String r;serializeJson(doc,r);
  server.send(200,"application/json",r);
}

String getUptimeString(){
  unsigned long s=millis()/1000,m=s/60,h=m/60,d=h/24;
  s%=60;m%=60;h%=24;
  String u="";
  if(d>0)u+=String(d)+"д ";
  if(h>0||d>0)u+=String(h)+"ч ";
  u+=String(m)+"м "+String(s)+"с";
  return u;
}

// =====================================================================
// SETUP
// =====================================================================
void setup(){
  Serial.begin(115200);delay(500);
  Serial.println("\n=== УМНЫЙ ВЕНТИЛЯТОР v2.12 (ArduinoOTA) ===");

  loadWiFiConfig();

  server.on("/favicon.svg", handleFavicon);

  pinMode(RELAY1,OUTPUT);pinMode(RELAY2,OUTPUT);
  pinMode(RELAY3,OUTPUT);pinMode(RELAY4,OUTPUT);
  digitalWrite(RELAY1,RELAY_OFF);digitalWrite(RELAY2,RELAY_OFF);
  digitalWrite(RELAY3,RELAY_OFF);digitalWrite(RELAY4,RELAY_OFF);
  initialized=true;uptimeStart=millis();

  dht.begin();
  irrecv.enableIRIn();
  Serial.println("DHT22 + IR готовы");

  if(savedSSID.length()>0){
    WiFi.mode(WIFI_AP_STA);
    WiFi.softAP("SmartFan_Config","12345678");
    WiFi.begin(savedSSID.c_str(),savedPassword.c_str());
    apMode=true;
    connectionInProgress=true;
    wifiStartTime=millis();
    while(WiFi.status()!=WL_CONNECTED&&millis()-wifiStartTime<WIFI_TIMEOUT){delay(500);Serial.print(".");}
    connectionInProgress=false;
    if(WiFi.status()==WL_CONNECTED){
      currentIP=WiFi.localIP().toString();
      Serial.println("\nSTA IP: "+currentIP);
    } else {
      currentIP=WiFi.softAPIP().toString();
      Serial.println("\nAP IP: "+currentIP);
    }
  } else {
    WiFi.mode(WIFI_AP);
    WiFi.softAP("SmartFan_Config","12345678");
    apMode=true;
    currentIP=WiFi.softAPIP().toString();
    Serial.println("AP IP: "+currentIP);
  }
  dnsServer.start(DNS_PORT,"*",WiFi.softAPIP());

  server.on("/instr",         handleInstr);
  server.on("/",           handleRoot);
  server.on("/wifi_config",    handleWifiConfig);
  server.on("/wifi_status",    handleWifiStatus);
  server.on("/start_ap",       handleStartAP);
  server.on("/connect_wifi",   handleConnectWifi);
  server.on("/show_ip",        handleShowIP);
  server.on("/scan",           handleScanNetworks);
  server.on("/getData",        handleGetData);
  server.on("/setSpeed",       handleSetSpeed);
  server.on("/setThresholds",  handleSetThresholds);
  server.on("/savewifi",       handleSaveWifi);
  server.on("/setTimer",       handleSetTimer);
  server.on("/stopTimer",      handleStopTimer);
  server.onNotFound(handleNotFound);

  httpUpdater.setup(&server);   // OTA: GET /update + POST /update

  server.begin();
  Serial.println("HTTP сервер запущен");
  Serial.println("OTA: http://"+currentIP+"/update");
  Serial.println("AP IP: "+WiFi.softAPIP().toString());
  if(WiFi.status()==WL_CONNECTED) Serial.println("STA IP: "+WiFi.localIP().toString());

  // -------------------------------------------------------
  // ArduinoOTA — обновление через Arduino IDE (порт 8266)
  // -------------------------------------------------------
  ArduinoOTA.setHostname("SmartFan");       // имя в меню Инструменты → Порт
  ArduinoOTA.setPassword("12345678");   // пароль для IDE (можно изменить)

  ArduinoOTA.onStart([](){
    String type = (ArduinoOTA.getCommand()==U_FLASH) ? "прошивка" : "файловая система";
    Serial.println("[OTA] Начало обновления: "+type);
    setFanSpeed(0);   // выключить вентилятор на время прошивки
  });
  ArduinoOTA.onEnd([](){
    Serial.println("\n[OTA] Завершено. Перезагрузка...");
  });
  ArduinoOTA.onProgress([](unsigned int progress, unsigned int total){
    Serial.printf("[OTA] Прогресс: %u%%\r", progress*100/total);
  });
  ArduinoOTA.onError([](ota_error_t error){
    Serial.printf("[OTA] Ошибка [%u]: ", error);
    if(error==OTA_AUTH_ERROR)         Serial.println("Аутентификация");
    else if(error==OTA_BEGIN_ERROR)   Serial.println("Начало");
    else if(error==OTA_CONNECT_ERROR) Serial.println("Соединение");
    else if(error==OTA_RECEIVE_ERROR) Serial.println("Приём");
    else if(error==OTA_END_ERROR)     Serial.println("Завершение");
  });

  ArduinoOTA.begin();
  MDNS.begin("smartfan");   // доступен как smartfan.local
  Serial.println("[OTA] ArduinoOTA готов. Хост: SmartFan, пароль: smartfan2025");
  Serial.println("[OTA] mDNS: http://smartfan.local/");
}

// =====================================================================
// LOOP
// =====================================================================
void loop(){
  server.handleClient();
  ArduinoOTA.handle();          // обработка OTA-запросов из Arduino IDE
  MDNS.update();
  if(apMode) dnsServer.processNextRequest();
  checkWiFiConnection();

  static unsigned long lastDHT=0;
  if(millis()-lastDHT>2000){
    float h=dht.readHumidity(),t=dht.readTemperature();
    if(!isnan(h)&&!isnan(t)){temperature=t;humidity=h;dhtError=false;if(autoMode)updateAutoMode();}
    else dhtError=true;
    lastDHT=millis();
  }

  static unsigned long lastIR=0;
  if(millis()-lastIR>100){
    if(irrecv.decode(&results)){handleIRCommand(results.value);irrecv.resume();}
    lastIR=millis();
  }

  updateFanTimer();
}
