String sanitizeHostname(String h) {
  h.trim();
  h.toLowerCase();
  String out;
  for (uint16_t i = 0; i < h.length() && out.length() < 31; i++) {
    char c = h.charAt(i);
    if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-') out += c;
  }
  while (out.startsWith("-")) out.remove(0, 1);
  while (out.endsWith("-")) out.remove(out.length() - 1);
  if (out.length() == 0) out = "ld";
  return out;
}

String getWifiHostname() {
  prefs.begin("wifi", true);
  String h = prefs.getString("host", "ld");
  prefs.end();
  return sanitizeHostname(h);
}

bool loadSavedWifi(SavedWifiConfig &w) {
  prefs.begin("wifi", true);
  String ssid = prefs.getString("ssid", "");
  String pass = prefs.getString("pass", "");
  String host = prefs.getString("host", "ld");
  prefs.end();

  ssid.trim();
  if (ssid.length() == 0) return false;

  ssid.toCharArray(w.ssid, sizeof(w.ssid));
  pass.toCharArray(w.password, sizeof(w.password));
  sanitizeHostname(host).toCharArray(w.hostname, sizeof(w.hostname));
  return true;
}

void saveSavedWifi(const String &ssid, const String &password, const String &hostname) {
  prefs.begin("wifi", false);
  prefs.putString("ssid", ssid);
  prefs.putString("pass", password);
  prefs.putString("host", sanitizeHostname(hostname));
  prefs.end();
}

void saveWifiHostnameOnly(const String &hostname) {
  prefs.begin("wifi", false);
  prefs.putString("host", sanitizeHostname(hostname));
  prefs.end();
}

void clearSavedWifi() {
  // Sletter kun WiFi-oplysninger. Alt andet bevares:
  // LED-konfiguration, MQTT, sprog, effekter og random-indstillinger.
  // Hostname bevares, så .local-adressen ikke forsvinder.
  prefs.begin("wifi", false);
  prefs.remove("ssid");
  prefs.remove("pass");
  prefs.end();
}

bool tryOneWifi(const char* ssid, const char* password) {
  if (ssid == nullptr || strlen(ssid) == 0) return false;

  Serial.println();
  Serial.print("Forsøger WiFi: ");
  Serial.println(ssid);

  oledShowMessage("Soger WiFi", ssid, "", "");
  WiFi.disconnect(true);
  delay(200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);

  int timeout = 0;
  while (WiFi.status() != WL_CONNECTED && timeout < 20) {
    delay(500);
    Serial.print(".");
    timeout++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    connectedWifiSsid = String(ssid);
    Serial.println();
    Serial.println("WiFi forbundet");
    Serial.print("SSID: "); Serial.println(connectedWifiSsid);
    Serial.print("IP adresse: "); Serial.println(WiFi.localIP());
    return true;
  }

  Serial.println();
  Serial.println("Ingen forbindelse.");
  return false;
}

bool connectWiFi() {
  SavedWifiConfig saved;

  if (!loadSavedWifi(saved)) {
    Serial.println();
    Serial.println("Ingen gemt WiFi-konfiguration fundet.");
    return false;
  }

  return tryOneWifi(saved.ssid, saved.password);
}

void startWifiSetupMode() {
  setupMode = true;
  connectedWifiSsid = "";

  WiFi.disconnect(true);
  delay(300);
  WiFi.mode(WIFI_AP_STA);
  WiFi.softAP(SETUP_AP_SSID, SETUP_AP_PASSWORD);
  IPAddress apIP = WiFi.softAPIP();

  dnsServer.start(DNS_PORT, "*", apIP);

  Serial.println();
  Serial.println("WiFi Setup Mode startet");
  Serial.print("SSID: "); Serial.println(SETUP_AP_SSID);
  Serial.print("IP: "); Serial.println(apIP);

  oledShowMessage("SETUP MODE", String("SSID: ") + SETUP_AP_SSID, "IP: 192.168.4.1", "Indtast WiFi");
}


String lastMqttTestResult = "";

String tr(const char* key) {
  String k = String(key);
  if (language == 1) { // English
    if (k=="system") return "System";
    if (k=="strips") return "LED strips";
    if (k=="leds") return "LEDs";
    if (k=="random") return "Random";
    if (k=="mqtt") return "MQTT";
    if (k=="status") return "Status";
    if (k=="wifi") return "WiFi";
    if (k=="led_config") return "LED configuration";
    if (k=="save") return "Save";
    if (k=="language") return "Language";
    if (k=="control") return "Control";
    if (k=="input_help") return "Choose DCC/MQTT input.";
    if (k=="shortcut") return "Shortcut";
    if (k=="shortcut_text") return "Configure LEDs/random in menu.";
    if (k=="wifi_status") return "Status";
    if (k=="mode") return "Mode";
    if (k=="setup_mode") return "Setup Mode";
    if (k=="normal_operation") return "Normal operation";
    if (k=="current_ssid") return "Current SSID";
    if (k=="save_new_wifi") return "Save new WiFi";
    if (k=="wifi_help") return "Select WiFi or enter SSID.";
    if (k=="found_networks") return "Found networks";
    if (k=="manual_ssid") return "Manual SSID";
    if (k=="password") return "Password";
    if (k=="save_wifi_restart") return "Save WiFi and restart";
    if (k=="scan_again") return "Scan again";
    if (k=="select_network") return "-- select found network --";
    if (k=="no_networks") return "No networks found";
    if (k=="hidden_network") return "hidden network";
    if (k=="wifi_note") return "If WiFi fails, setup AP starts";
    if (k=="connect_setup") return "Connect and open";
    if (k=="qr_shortcut") return "QR shortcut";
    if (k=="qr_help") return String("QR: http://") + getWifiHostname() + ".local or use IP.";
    if (k=="reset_wifi") return "Reset WiFi";
    if (k=="wifi_saved") return "WiFi saved";
    if (k=="decoder_restarts") return "Restarting and connecting to:";
    if (k=="setup_disappears") return "Setup AP stops after success.";
    if (k=="page_not_found") return "Page not found";
    if (k=="never") return "never";
    if (k=="sec_ago") return " sec ago";
    if (k=="min_ago") return " min ago";
    if (k=="decoder_name") return "Light decoder";
    if (k=="language_help") return "Choose language.";
    if (k=="led_count_help") return "0 disables strip.";
    if (k=="strip") return "Strip";
    if (k=="led_count") return "LED count";
    if (k=="configure_strip") return "Configure strip";
    if (k=="choose_strip") return "Choose strip";
    if (k=="strip_inactive") return "Strip inactive.";
    if (k=="global_led") return "Global LED";
    if (k=="led_on_strip") return "LED on strip";
    if (k=="dcc_mqtt_address") return "DCC/MQTT address";
    if (k=="color_effect") return "Color / Effect";
    if (k=="preview") return "Preview";
    if (k=="brightness") return "Brightness";
    if (k=="mqtt_topic_help") return "Topic: base/address/set. 1=OFF,2=ON.";
    if (k=="day_period") return "Day period (min)";
    if (k=="night_period") return "Night period (min)";
    if (k=="random_address") return "Random address";
    if (k=="explanation") return "Explanation";
    if (k=="running") return "Running";
    if (k=="stopped") return "Stopped";
    if (k=="random_explanation") return "ON starts, OFF stops.";
    if (k=="mqtt_enabled") return "MQTT enabled";
    if (k=="broker") return "Broker";
    if (k=="port") return "Port";
    if (k=="user") return "User";
    if (k=="code") return "Password";
    if (k=="base_topic") return "Base topic";
    if (k=="tls_hint") return "1883 without TLS, 8883 with TLS";
    if (k=="no") return "No";
    if (k=="yes") return "Yes";
    if (k=="no_cert_check") return "no certificate check";
    if (k=="test") return "Test";
    if (k=="test_mqtt_connection") return "Test MQTT connection";
    if (k=="latest_test") return "Latest test";
    if (k=="not_run_yet") return "Not run yet";
    if (k=="topic_format") return "Topic format";
    if (k=="example") return "Example";
    if (k=="input_mode") return "Input mode";
    if (k=="mqtt_broker") return "MQTT broker";
    if (k=="latest_mqtt_message") return "Latest MQTT message";
    if (k=="latest_mqtt_attempt") return "Latest MQTT attempt";
    if (k=="latest_dcc_command") return "Latest DCC command";
    if (k=="free_heap") return "Free heap";
    if (k=="uptime") return "Uptime";
    if (k=="seconds") return "sec";
    if (k=="saved_wifi_missing") return "SSID is missing";
    if (k=="saved_wifi_empty") return "SSID must not be empty";
    if (k=="restart") return "Restarting...";
    if (k=="off") return "Off";
    if (k=="fixed_colors") return "Fixed colors";
    if (k=="effects") return "Effects";

    if (k=="info") return "Info";
    if (k=="about_title") return "DCC/MQTT Light decoder";
    if (k=="developed_by") return "Developed by";
    if (k=="documentation") return "Documentation";
    if (k=="project_info") return "Project info";
    if (k=="about_text") return "Configurable DCC/MQTT light decoder with WiFi and web setup.";
    if (k=="website") return "Website";
    if (k=="youtube") return "YouTube";
    if (k=="location") return "Location";
    if (k=="country") return "Farum, Denmark";
    if (k=="connected") return "Connected";
    if (k=="not_connected") return "Not connected";
    if (k=="firmware") return "Firmware";
    if (k=="firmware_update") return "Firmware update";
    if (k=="upload_new_firmware") return "Upload new firmware";
    if (k=="firmware_help") return "Upload .ino.bin firmware.";
    if (k=="upload_restart") return "Upload and restart";
    if (k=="important") return "Important";
    if (k=="firmware_warning") return "Use correct firmware. Do not power off.";
    if (k=="current_version") return "Current version";
    if (k=="firmware_saved") return "Firmware saved";
    if (k=="firmware_error") return "Firmware error";
    if (k=="update_complete") return "Update complete";
    if (k=="esp_restarts") return "ESP32 is restarting now.";
    if (k=="update_failed") return "Update failed";
    if (k=="back") return "Back";
    if (k=="hostname") return "Hostname";
    if (k=="hostname_no_local") return "Hostname without .local";
  }
  if (language == 2) { // Deutsch
    if (k=="system") return "System";
    if (k=="strips") return "LED-Streifen";
    if (k=="leds") return "LEDs";
    if (k=="random") return "Zufall";
    if (k=="mqtt") return "MQTT";
    if (k=="status") return "Status";
    if (k=="wifi") return "WLAN";
    if (k=="led_config") return "LED-Konfiguration";
    if (k=="save") return "Speichern";
    if (k=="language") return "Sprache";
    if (k=="control") return "Steuerung";
    if (k=="input_help") return "DCC/MQTT Eingang wählen.";
    if (k=="shortcut") return "Direktzugriff";
    if (k=="shortcut_text") return "LEDs/Zufall im Menü.";
    if (k=="wifi_status") return "Status";
    if (k=="mode") return "Betrieb";
    if (k=="setup_mode") return "Setup-Modus";
    if (k=="normal_operation") return "Normalbetrieb";
    if (k=="current_ssid") return "Aktuelle SSID";
    if (k=="save_new_wifi") return "Neues WLAN speichern";
    if (k=="wifi_help") return "WLAN wählen oder SSID eingeben.";
    if (k=="found_networks") return "Gefundene Netzwerke";
    if (k=="manual_ssid") return "SSID manuell";
    if (k=="password") return "Passwort";
    if (k=="save_wifi_restart") return "WLAN speichern und neu starten";
    if (k=="scan_again") return "Erneut scannen";
    if (k=="select_network") return "-- gefundenes Netzwerk wählen --";
    if (k=="no_networks") return "Keine Netzwerke";
    if (k=="hidden_network") return "verstecktes Netzwerk";
    if (k=="wifi_note") return "Bei WLAN-Fehler startet Setup-AP";
    if (k=="connect_setup") return "Verbinden und öffnen";
    if (k=="qr_shortcut") return "QR-Direktzugriff";
    if (k=="qr_help") return String("QR: http://") + getWifiHostname() + ".local oder IP nutzen.";
    if (k=="reset_wifi") return "WLAN zurücksetzen";
    if (k=="wifi_saved") return "WLAN gespeichert";
    if (k=="decoder_restarts") return "Neustart, Verbindung zu:";
    if (k=="setup_disappears") return "Setup-AP stoppt bei Erfolg.";
    if (k=="page_not_found") return "Seite nicht gefunden";
    if (k=="never") return "nie";
    if (k=="sec_ago") return " Sek. her";
    if (k=="min_ago") return " Min. her";
    if (k=="decoder_name") return "Lichtdecoder";
    if (k=="language_help") return "Sprache wählen.";
    if (k=="led_count_help") return "0 deaktiviert Streifen.";
    if (k=="strip") return "Streifen";
    if (k=="led_count") return "LED-Anzahl";
    if (k=="configure_strip") return "Streifen konfigurieren";
    if (k=="choose_strip") return "Streifen wählen";
    if (k=="strip_inactive") return "Streifen inaktiv.";
    if (k=="global_led") return "Globale LED";
    if (k=="led_on_strip") return "LED auf Streifen";
    if (k=="dcc_mqtt_address") return "DCC/MQTT-Adresse";
    if (k=="color_effect") return "Farbe / Effekt";
    if (k=="preview") return "Vorschau";
    if (k=="brightness") return "Helligkeit";
    if (k=="mqtt_topic_help") return "Topic: base/Adresse/set. 1=OFF,2=ON.";
    if (k=="day_period") return "Tag-Periode (Min.)";
    if (k=="night_period") return "Nacht-Periode (Min.)";
    if (k=="random_address") return "Zufallsadresse";
    if (k=="explanation") return "Erklärung";
    if (k=="running") return "Läuft";
    if (k=="stopped") return "Gestoppt";
    if (k=="random_explanation") return "ON startet, OFF stoppt.";
    if (k=="mqtt_enabled") return "MQTT aktiv";
    if (k=="broker") return "Broker";
    if (k=="port") return "Port";
    if (k=="user") return "Benutzer";
    if (k=="code") return "Passwort";
    if (k=="base_topic") return "Base Topic";
    if (k=="tls_hint") return "1883 ohne TLS, 8883 mit TLS";
    if (k=="no") return "Nein";
    if (k=="yes") return "Ja";
    if (k=="no_cert_check") return "keine Zertifikatsprüfung";
    if (k=="test") return "Test";
    if (k=="test_mqtt_connection") return "MQTT-Verbindung testen";
    if (k=="latest_test") return "Letzter Test";
    if (k=="not_run_yet") return "Noch nicht ausgeführt";
    if (k=="topic_format") return "Topic-Format";
    if (k=="example") return "Beispiel";
    if (k=="input_mode") return "Eingangsmodus";
    if (k=="mqtt_broker") return "MQTT-Broker";
    if (k=="latest_mqtt_message") return "Letzte MQTT-Nachricht";
    if (k=="latest_mqtt_attempt") return "Letzter MQTT-Versuch";
    if (k=="latest_dcc_command") return "Letzter DCC-Befehl";
    if (k=="free_heap") return "Freier Heap";
    if (k=="uptime") return "Laufzeit";
    if (k=="seconds") return "Sek.";
    if (k=="saved_wifi_missing") return "SSID fehlt";
    if (k=="saved_wifi_empty") return "SSID darf nicht leer sein";
    if (k=="restart") return "Neustart...";
    if (k=="off") return "Aus";
    if (k=="fixed_colors") return "Feste Farben";
    if (k=="effects") return "Effekte";

    if (k=="info") return "Info";
    if (k=="about_title") return "DCC/MQTT Lichtdecoder";
    if (k=="developed_by") return "Entwickelt von";
    if (k=="documentation") return "Dokumentation";
    if (k=="project_info") return "Projektinfo";
    if (k=="about_text") return "DCC/MQTT-Lichtdecoder.";
    if (k=="website") return "Webseite";
    if (k=="youtube") return "YouTube";
    if (k=="location") return "Ort";
    if (k=="country") return "Farum, Dänemark";
    if (k=="connected") return "Verbunden";
    if (k=="not_connected") return "Nicht verbunden";
    if (k=="firmware") return "Firmware";
    if (k=="firmware_update") return "Firmware-Update";
    if (k=="upload_new_firmware") return "Neue Firmware hochladen";
    if (k=="firmware_help") return "ESP32-Firmware (.ino.bin) hochladen.";
    if (k=="upload_restart") return "Hochladen und neu starten";
    if (k=="important") return "Wichtig";
    if (k=="firmware_warning") return "Richtige Firmware. Nicht ausschalten.";
    if (k=="current_version") return "Aktuelle Version";
    if (k=="firmware_saved") return "Firmware gespeichert";
    if (k=="firmware_error") return "Firmware-Fehler";
    if (k=="update_complete") return "Update abgeschlossen";
    if (k=="esp_restarts") return "ESP32 startet jetzt neu.";
    if (k=="update_failed") return "Update fehlgeschlagen";
    if (k=="back") return "Zurück";
    if (k=="hostname") return "Hostname";
    if (k=="hostname_no_local") return "Hostname ohne .local";
  }
  // Dansk default
  if (k=="system") return "System";
  if (k=="strips") return "LED bånd";
  if (k=="leds") return "LED'er";
  if (k=="random") return "Random";
  if (k=="mqtt") return "MQTT";
  if (k=="status") return "Status";
  if (k=="wifi") return "WiFi";
  if (k=="led_config") return "LED-konfiguration";
  if (k=="save") return "Gem";
  if (k=="language") return "Sprog";
  if (k=="control") return "Styring";
  if (k=="input_help") return "Vælg DCC/MQTT input.";
  if (k=="shortcut") return "Genvej";
  if (k=="shortcut_text") return "Konfigurer LED/random i menuen.";
  if (k=="wifi_status") return "Status";
  if (k=="mode") return "Tilstand";
  if (k=="setup_mode") return "Setup Mode";
  if (k=="normal_operation") return "Normal drift";
  if (k=="current_ssid") return "Aktuelt SSID";
  if (k=="save_new_wifi") return "Gem nyt WiFi";
  if (k=="wifi_help") return "Vælg WiFi eller skriv SSID.";
  if (k=="found_networks") return "Fundne netværk";
  if (k=="manual_ssid") return "SSID manuelt";
  if (k=="password") return "Password";
  if (k=="save_wifi_restart") return "Gem WiFi og genstart";
  if (k=="scan_again") return "Scan igen";
  if (k=="select_network") return "-- vælg fundet netværk --";
  if (k=="no_networks") return "Ingen netværk fundet";
  if (k=="hidden_network") return "skjult netværk";
  if (k=="wifi_note") return "Ved WiFi-fejl starter setup-net";
  if (k=="connect_setup") return "Forbind og gå til";
  if (k=="qr_shortcut") return "QR-genvej";
  if (k=="qr_help") return String("QR: http://") + getWifiHostname() + ".local eller brug IP.";
  if (k=="reset_wifi") return "Nulstil WiFi";
  if (k=="wifi_saved") return "WiFi gemt";
  if (k=="decoder_restarts") return "Genstarter og forbinder til:";
  if (k=="setup_disappears") return "Setup-net stopper ved succes.";
  if (k=="page_not_found") return "Siden findes ikke";
  if (k=="never") return "aldrig";
  if (k=="sec_ago") return " sek siden";
  if (k=="min_ago") return " min siden";
  if (k=="decoder_name") return "Lysdekoder";
  if (k=="language_help") return "Vælg sprog.";
  if (k=="led_count_help") return "0 deaktiverer bånd.";
  if (k=="strip") return "Bånd";
  if (k=="led_count") return "Antal LED'er";
  if (k=="configure_strip") return "Konfigurer bånd";
  if (k=="choose_strip") return "Vælg bånd";
  if (k=="strip_inactive") return "Bånd inaktivt.";
  if (k=="global_led") return "Global LED";
  if (k=="led_on_strip") return "LED på bånd";
  if (k=="dcc_mqtt_address") return "DCC/MQTT adresse";
  if (k=="color_effect") return "Farve / Effekt";
  if (k=="preview") return "Prøve";
  if (k=="brightness") return "Brightness";
  if (k=="mqtt_topic_help") return "Topic: base/adresse/set. 1=OFF,2=ON.";
  if (k=="day_period") return "Dag-periode (min)";
  if (k=="night_period") return "Nat-periode (min)";
  if (k=="random_address") return "Random adresse";
  if (k=="explanation") return "Forklaring";
  if (k=="running") return "Kører";
  if (k=="stopped") return "Stoppet";
  if (k=="random_explanation") return "ON starter, OFF stopper.";
  if (k=="mqtt_enabled") return "MQTT aktiv";
  if (k=="broker") return "Broker";
  if (k=="port") return "Port";
  if (k=="user") return "Bruger";
  if (k=="code") return "Kode";
  if (k=="base_topic") return "Base topic";
  if (k=="tls_hint") return "1883 uden TLS, 8883 med TLS";
  if (k=="no") return "Nej";
  if (k=="yes") return "Ja";
  if (k=="no_cert_check") return "ingen certifikat-check";
  if (k=="test") return "Test";
  if (k=="test_mqtt_connection") return "Test MQTT-forbindelse";
  if (k=="latest_test") return "Seneste test";
  if (k=="not_run_yet") return "Ikke kørt endnu";
  if (k=="topic_format") return "Topic-format";
  if (k=="example") return "Eksempel";
  if (k=="input_mode") return "Input mode";
  if (k=="mqtt_broker") return "MQTT broker";
  if (k=="latest_mqtt_message") return "Seneste MQTT besked";
  if (k=="latest_mqtt_attempt") return "Seneste MQTT forsøg";
  if (k=="latest_dcc_command") return "Seneste DCC kommando";
  if (k=="free_heap") return "Fri heap";
  if (k=="uptime") return "Uptime";
  if (k=="seconds") return "sek";
  if (k=="saved_wifi_missing") return "SSID mangler";
  if (k=="saved_wifi_empty") return "SSID maa ikke vaere tom";
  if (k=="restart") return "Genstarter...";
  if (k=="off") return "Slukket";
  if (k=="fixed_colors") return "Faste farver";
  if (k=="effects") return "Effekter";
  if (k=="info") return "Info";
  if (k=="about_title") return "DCC/MQTT LysDekoder";
  if (k=="developed_by") return "Udviklet af";
  if (k=="documentation") return "Dokumentation";
  if (k=="project_info") return "Projektinfo";
  if (k=="about_text") return "DCC/MQTT lysdekoder.";
  if (k=="website") return "Hjemmeside";
  if (k=="youtube") return "YouTube";
  if (k=="location") return "Placering";
  if (k=="country") return "Farum, Danmark";
  if (k=="connected") return "Tilsluttet";
  if (k=="not_connected") return "Ikke tilsluttet";
  if (k=="firmware") return "Firmware";
  if (k=="firmware_update") return "Firmware-opdatering";
  if (k=="upload_new_firmware") return "Upload ny firmware";
  if (k=="firmware_help") return "Upload .ino.bin firmware.";
  if (k=="upload_restart") return "Upload og genstart";
  if (k=="important") return "Vigtigt";
  if (k=="firmware_warning") return "Korrekt firmware. Afbryd ikke.";
  if (k=="current_version") return "Aktuel version";
  if (k=="firmware_saved") return "Firmware gemt";
  if (k=="firmware_error") return "Firmware-fejl";
  if (k=="update_complete") return "Opdatering færdig";
  if (k=="esp_restarts") return "ESP32 genstarter nu.";
  if (k=="update_failed") return "Opdatering fejlede";
  if (k=="back") return "Tilbage";
  if (k=="hostname") return "Hostname";
  if (k=="hostname_no_local") return "Hostname uden .local";
  return k;
}



String flagSpan(const char* f) { return String("<span class='flagEmoji'>") + f + "</span>"; }

String languageSelectHtml() {
  String html = "<div class='langflags'>";
  const char* names[3] = {"Dansk", "English", "Deutsch"};
  const char* flags[3] = {"🇩🇰", "🇬🇧", "🇩🇪"};
  for (int i=0;i<3;i++) {
    html += "<label class='langchoice";
    if (language == i) html += " active";
    html += "'><input type='radio' name='language' value='" + String(i) + "'";
    if (language == i) html += " checked";
    html += ">" + flagSpan(flags[i]) + "<span>" + names[i] + "</span></label>";
  }
  html += "</div>";
  return html;
}

String languageMenuHtml() {
  String html = "<div class='navlangs'>";
  html += "<a title='Dansk' class='"; if (language == 0) html += "active"; html += "' href='/lang?set=da'>DK</a>";
  html += "<a title='English' class='"; if (language == 1) html += "active"; html += "' href='/lang?set=en'>GB</a>";
  html += "<a title='Deutsch' class='"; if (language == 2) html += "active"; html += "' href='/lang?set=de'>D</a>";
  html += "</div>";
  return html;
}

void handleLanguageSet() {
  if (server.hasArg("set")) {
    String v = server.arg("set");
    if (v == "da") language = 0;
    else if (v == "en") language = 1;
    else if (v == "de") language = 2;
    saveConfig();
  }

  String dest = server.header("Referer");
  if (dest.length() == 0) dest = "/system";
  server.sendHeader("Location", dest);
  server.send(303);
}

void setupWebServer() {
  server.on("/", HTTP_GET, handleSystemPage);
  server.on("/system", HTTP_GET, handleSystemPage);
  server.on("/strips", HTTP_GET, handleStripsPage);
  server.on("/leds", HTTP_GET, handleLedsPage);
  server.on("/random", HTTP_GET, handleRandomPage);
  server.on("/mqtt", HTTP_GET, handleMqttPage);
  server.on("/mqtttest", HTTP_POST, handleMqttTest);
  server.on("/status", HTTP_GET, handleStatusPage);
  server.on("/wifi", HTTP_GET, handleWifiPage);
  server.on("/info", HTTP_GET, handleInfoPage);
  server.on("/firmware", HTTP_GET, handleFirmwarePage);
  server.on("/update", HTTP_POST, handleFirmwareUploadDone, handleFirmwareUpload);
  server.on("/lang", HTTP_GET, handleLanguageSet);
  server.on("/wifisave", HTTP_POST, handleWifiSave);
  server.on("/save", HTTP_POST, handleWebSave);
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println(); Serial.println("Webserver startet");
}



String productLogoHtml() {
  String h;
  h += "<div class='logoBox'><svg class='logoSvg' viewBox='0 0 96 72'>";
  h += "<rect x='8' y='8' width='80' height='56' rx='8' fill='#fff'/>";
  h += "<path d='M20 36 L48 15 L76 36 V61 H20 Z' fill='#fff7f0' stroke='#d32f2f' stroke-width='4'/>";
  h += "<rect x='30' y='42' width='13' height='13' rx='2' fill='#ffd54f' stroke='#b38f00'/>";
  h += "<rect x='54' y='42' width='13' height='13' rx='2' fill='#ffd54f' stroke='#b38f00'/>";
  h += "<rect x='42' y='29' width='12' height='10' rx='2' fill='#263238'/>";
  h += "<circle cx='76' cy='28' r='3' fill='#26c6da'/>";
  h += "<path d='M67 22 Q76 16 85 22 M61 15 Q76 5 91 15' fill='none' stroke='#26c6da' stroke-width='4' stroke-linecap='round'/>";
  h += "</svg></div>";
  return h;
}


String navHtml(const char* active) {
  String h;
  h += "<div class='nav'>";
  h += productLogoHtml();
  h += "<div class='brand'>DCC/MQTT<br>" + tr("decoder_name") + "</div>";
  h += languageMenuHtml();
  h += navLink("system", "/system", active);
  h += navLink("strips", "/strips", active);
  h += navLink("leds", "/leds", active);
  h += navLink("random", "/random", active);
  h += navLink("mqtt", "/mqtt", active);
  h += navLink("status", "/status", active);
  h += navLink("wifi", "/wifi", active);
  h += navLink("info", "/info", active);
  h += navLink("firmware", "/firmware", active);
  h += "<div class='navfooter'>v20.0 · Jens · Farum</div></div>";
  return h;
}

String navLink(const char* key, const char* url, const char* active) {
  String h = "<a class='";
  if (String(active) == String(key)) h += "active";
  h += "' href='"; h += url; h += "'>"; h += tr(key); h += "</a>";
  return h;
}

String pageStart(const String &title, const char* active) {
  String html;
  html += "<!DOCTYPE html><html><head><meta charset='utf-8'>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<title>DCC/MQTT LysDekoder</title><style>";
  html += "body{font-family:Arial;margin:0;background:#f4f4f4;color:#222}.wrap{display:flex;min-height:100vh}.nav{width:200px;background:#263238;color:#fff;padding:12px 0}.logoBox,.brand{text-align:center}.logoSvg{width:78px;height:58px}.brand{font-size:20px;font-weight:bold;padding:0 12px 10px}.nav a{display:block;color:#fff;text-decoration:none;padding:10px 16px;font-size:17px}.nav a:hover,.nav a.active{background:#455a64}.navfooter{font-size:12px;color:#cfd8dc;padding:12px 16px}.navlangs{padding:0 10px 10px;display:flex;gap:6px;justify-content:center}.navlangs a{padding:6px 8px;border-radius:7px;min-width:34px;text-align:center;font-weight:bold}.main{padding:24px;flex:1}h1{font-size:32px;margin:0 0 8px}.card{background:#fff;border:1px solid #ddd;border-radius:8px;padding:16px;margin:0 0 16px}table{border-collapse:collapse;background:#fff;margin-bottom:16px}th,td{border:1px solid #ccc;padding:7px 9px;text-align:left}th{background:#eee}.btn,button{background:#1976d2;color:#fff;border:0;border-radius:5px;padding:8px 12px;text-decoration:none;display:inline-block;margin:3px}.note,.muted{color:#666}.center{text-align:center}.ok{color:#2e7d32;font-weight:bold}.bad{color:#c62828;font-weight:bold}input,select{font-size:16px;padding:4px}.addr{width:80px}.count{width:65px}.swatch{display:inline-block;width:28px;height:18px;border:1px solid #999}.langflags{display:flex;gap:10px}.langchoice{border:1px solid #ccc;border-radius:8px;padding:8px;display:flex;gap:8px}.active{background:#455a64}";
  html += "@media(max-width:760px){.wrap{display:block}.nav{width:auto}.nav a{display:inline-block}.main{padding:18px}.brand{display:none}}";
  html += "</style></head><body><div class='wrap'>";
  html += navHtml(active);
  html += "<div class='main'><h1>"; html += title; html += "</h1>";
  return html;
}

String pageEnd() { return "</div></div></body></html>"; }

String inputModeName() {
  if (inputMode == 1) return "DCC";
  if (inputMode == 2) return "MQTT";
  return "DCC + MQTT";
}

String modeSelectHtml(int globalIndex, uint8_t selectedMode) {
  String html;
  html += "<select name='mode" + String(globalIndex) + "'>";
  html += "<option value='0'"; if (selectedMode == 0) html += " selected"; html += ">0 - " + tr("off") + "</option>";
  html += "<optgroup label='" + tr("fixed_colors") + "'>";
  for (int m = 1; m <= 10; m++) {
    html += "<option value='" + String(m) + "'"; if (selectedMode == m) html += " selected"; html += ">" + String(m) + " - " + modeName(m) + "</option>";
  }
  html += "</optgroup><optgroup label='" + tr("effects") + "'>";
  for (int m = 11; m <= 19; m++) {
    html += "<option value='" + String(m) + "'"; if (selectedMode == m) html += " selected"; html += ">" + String(m) + " - " + modeName(m) + "</option>";
  }
  html += "</optgroup></select>";
  return html;
}

void handleSystemPage() {
  String html = pageStart(tr("system"), "system");
  html += "<div class='card'><form method='POST' action='/save'>";
  html += "<h2>" + tr("control") + "</h2><p>" + tr("input_help") + "</p>";
  html += "<table>";
  html += "<tr><th>" + tr("input_mode") + "</th><td><select name='inputMode'>";
  html += "<option value='1'"; if (inputMode == 1) html += " selected"; html += ">DCC</option>";
  html += "<option value='2'"; if (inputMode == 2) html += " selected"; html += ">MQTT</option>";
  html += "<option value='3'"; if (inputMode == 3) html += " selected"; html += ">DCC + MQTT</option>";
  html += "</select></td></tr>";
  html += "<tr><th>" + tr("hostname_no_local") + "</th><td><input style='width:300px' name='hostname' maxlength='31' value='" + htmlEscape(getWifiHostname()) + "'><br><span class='note'>http://" + htmlEscape(getWifiHostname()) + ".local</span></td></tr>";
  html += "</table><button type='submit'>" + tr("save") + "</button></form></div>";
  html += "<div class='card'><h2>" + tr("shortcut") + "</h2><p>" + tr("shortcut_text") + "</p></div>";
  html += pageEnd(); server.send(200, "text/html", html);
}

void handleStripsPage() {
  String html = pageStart(tr("strips"), "strips");
  html += "<div class='card'><form method='POST' action='/save'>";
  html += "<p>" + tr("led_count_help") + "</p><table><tr><th>" + tr("strip") + "</th><th>GPIO</th><th>" + tr("led_count") + "</th><th>" + tr("led_config") + "</th></tr>";
  for (int s=0; s<MAX_STRIPS; s++) {
    html += "<tr><td class='center'>" + String(stripName(s)) + "</td><td>GPIO" + String(stripPins[s]) + "</td><td><input class='count' type='number' min='0' max='" + String(MAX_LEDS_PER_STRIP) + "' name='count" + String(s) + "' value='" + String(ledCount[s]) + "'></td>";
    html += "<td><a class='btn' href='/leds?strip=" + String(s) + "'>" + tr("configure_strip") + " " + String(stripName(s)) + "</a></td></tr>";
  }
  html += "</table><button type='submit'>" + tr("save") + "</button></form></div>";
  html += pageEnd(); server.send(200, "text/html", html);
}

void handleLedsPage() {
  int selectedStrip = 0;
  if (server.hasArg("strip")) selectedStrip = server.arg("strip").toInt();
  if (selectedStrip < 0 || selectedStrip >= MAX_STRIPS) selectedStrip = 0;

  String html = pageStart(tr("led_config"), "leds");
  html += "<div class='card'><p>" + tr("choose_strip") + ": ";
  for (int s=0; s<MAX_STRIPS; s++) html += "<a class='btn' href='/leds?strip=" + String(s) + "'>" + tr("strip") + " " + String(stripName(s)) + "</a>";
  html += "</p></div>";

  html += "<div class='card'><form method='POST' action='/save'><input type='hidden' name='stripView' value='" + String(selectedStrip) + "'>";
  html += "<h2>" + tr("strip") + " " + String(stripName(selectedStrip)) + " <span class='muted'>(GPIO" + String(stripPins[selectedStrip]) + ", " + String(ledCount[selectedStrip]) + " LEDs)</span></h2>";
  if (ledCount[selectedStrip] == 0) {
    html += "<p class='muted'>" + tr("strip_inactive") + "</p>";
  } else {
    html += "<table><tr><th>" + tr("global_led") + "</th><th>" + tr("led_on_strip") + "</th><th>" + tr("dcc_mqtt_address") + "</th><th>" + tr("color_effect") + "</th><th>" + tr("preview") + "</th><th>" + tr("brightness") + "</th></tr>";
    for (int i=0; i<ledCount[selectedStrip]; i++) {
      int g = globalLedIndex(selectedStrip, i);
      html += "<tr><td class='center'>" + String(g+1) + "</td><td>" + String(stripName(selectedStrip)) + String(i+1) + "</td>";
      html += "<td><input class='addr' type='number' min='1' max='2044' name='addr" + String(g) + "' value='" + String(config[selectedStrip][i].dccAddress) + "'></td>";
      html += "<td>" + modeSelectHtml(g, config[selectedStrip][i].mode) + "</td>";
      html += "<td class='center'><span class='swatch' style='background:" + modeHex(config[selectedStrip][i].mode) + ";'></span></td>";
      html += "<td><input type='range' min='0' max='255' name='bright" + String(g) + "' value='" + String(config[selectedStrip][i].brightness) + "' oninput='document.getElementById(\"b" + String(g) + "\").value=this.value'><input class='brightnum' id='b" + String(g) + "' value='" + String(config[selectedStrip][i].brightness) + "' readonly></td></tr>";
    }
    html += "</table><p class='note'>" + tr("mqtt_topic_help") + "</p>";
  }
  html += "<button type='submit'>" + tr("save") + "</button></form></div>";
  html += pageEnd(); server.send(200, "text/html", html);
}

void handleRandomPage() {
  String html = pageStart(tr("random"), "random");
  html += "<div class='card'><form method='POST' action='/save'>";
  html += "<table><tr><th>" + tr("day_period") + "</th><th>" + tr("night_period") + "</th></tr><tr>";
  html += "<td><input class='count' type='number' min='1' max='240' name='rndDay' value='" + String(randomDayMinutes) + "'></td>";
  html += "<td><input class='count' type='number' min='1' max='240' name='rndNight' value='" + String(randomNightMinutes) + "'></td></tr></table>";
  html += "<table><tr><th>" + tr("strip") + "</th><th>" + tr("random_address") + "</th><th>" + tr("status") + "</th><th>" + tr("explanation") + "</th></tr>";
  for (int s=0; s<MAX_STRIPS; s++) {
    html += "<tr><td>" + String(stripName(s)) + "</td><td><input class='addr' type='number' min='1' max='2044' name='rndAddr" + String(s) + "' value='" + String(randomConfig[s].dccAddress) + "'></td>";
    html += "<td>" + String(randomState[s].running ? tr("running") : tr("stopped")) + "</td><td>" + tr("random_explanation") + "</td></tr>";
  }
  html += "</table><button type='submit'>" + tr("save") + "</button></form></div>";
  html += pageEnd(); server.send(200, "text/html", html);
}

void handleMqttPage() {
  String html = pageStart(tr("mqtt"), "mqtt");
  html += "<div class='card'><form method='POST' action='/save'>";
  html += "<table><tr><th>" + tr("mqtt_enabled") + "</th><td><input type='checkbox' name='mqttEnabled' value='1'"; if (mqttSettings.enabled) html += " checked"; html += "></td></tr>";
  html += "<tr><th>" + tr("broker") + "</th><td><input style='width:330px' name='mqttBroker' maxlength='63' value='" + String(mqttSettings.broker) + "'></td></tr>";
  html += "<tr><th>" + tr("port") + "</th><td><input class='count' type='number' min='1' max='65535' name='mqttPort' value='" + String(mqttSettings.port) + "'> <span class='note'>" + tr("tls_hint") + "</span></td></tr>";
  html += "<tr><th>TLS</th><td><select name='mqttTls'><option value='0'"; if (!mqttSettings.useTls) html += " selected"; html += ">" + tr("no") + "</option><option value='1'"; if (mqttSettings.useTls) html += " selected"; html += ">" + tr("yes") + "</option></select> ";
  html += "<label><input type='checkbox' name='mqttTlsInsecure' value='1'"; if (mqttSettings.tlsInsecure) html += " checked"; html += "> " + tr("no_cert_check") + "</label></td></tr>";
  html += "<tr><th>" + tr("user") + "</th><td><input style='width:220px' name='mqttUser' maxlength='39' value='" + String(mqttSettings.user) + "'></td></tr>";
  html += "<tr><th>" + tr("code") + "</th><td><input style='width:220px' type='password' name='mqttPassword' maxlength='39' value='" + String(mqttSettings.password) + "'></td></tr>";
  html += "<tr><th>" + tr("base_topic") + "</th><td><input style='width:330px' name='mqttBaseTopic' maxlength='63' value='" + String(mqttSettings.baseTopic) + "'></td></tr></table>";
  html += "<button type='submit'>" + tr("save") + " MQTT</button></form></div>";
  html += "<div class='card'><h2>" + tr("test") + "</h2><form method='POST' action='/mqtttest'><button type='submit'>" + tr("test_mqtt_connection") + "</button></form>";
  html += "<p><b>" + tr("latest_test") + ":</b> " + (lastMqttTestResult.length() ? lastMqttTestResult : tr("not_run_yet")) + "</p>";
  html += "<p class='note'>" + tr("topic_format") + ": &lt;baseTopic&gt;/&lt;adresse&gt;/set. " + tr("example") + ": " + String(mqttSettings.baseTopic) + "/101/set = 2.</p></div>";
  html += pageEnd(); server.send(200, "text/html", html);
}

void handleMqttTest() {
  lastMqttTestResult = testMqttConnectionNow();
  setupMqtt();
  server.sendHeader("Location", "/mqtt"); server.send(303);
}

String ageText(uint32_t t) {
  if (t == 0) return tr("never");
  uint32_t sec = (millis() - t) / 1000;
  if (sec < 60) return String(sec) + tr("sec_ago");
  return String(sec/60) + tr("min_ago");
}


void handleInfoPage() {
  String html = pageStart(tr("info"), "info");
  html += "<div class='card'>";
  html += "<div style='display:flex;gap:18px;align-items:center;flex-wrap:wrap'>";
  html += productLogoHtml();
  html += "<div><h2>" + tr("about_title") + "</h2><p>" + tr("about_text") + "</p></div>";
  html += "</div></div>";

  html += "<div class='card'><h2>" + tr("project_info") + "</h2><table>";
  html += "<tr><th>Version</th><td>20.0</td></tr>";
  html += "<tr><th>" + tr("developed_by") + "</th><td>Jens Krogsgaard</td></tr>";
  html += "<tr><th>" + tr("location") + "</th><td>" + tr("country") + "</td></tr>";
  html += "<tr><th>" + tr("website") + "</th><td><a href='https://www.krogsgaardsmodelbane.dk' target='_blank'>www.krogsgaardsmodelbane.dk</a></td></tr>";
  html += "<tr><th>" + tr("youtube") + "</th><td><a href='https://www.youtube.com/@jenskrogsgaard' target='_blank'>@jenskrogsgaard</a></td></tr>";
  html += "</table></div>";

  html += "<div class='card'><h2>" + tr("documentation") + "</h2>";
  html += "<p><a class='btn' href='https://www.krogsgaardsmodelbane.dk' target='_blank'>www.krogsgaardsmodelbane.dk</a></p>";
  html += "</div>";
  html += pageEnd();
  server.send(200, "text/html", html);
}

void handleStatusPage() {
  String html = pageStart(tr("status"), "status");
  html += "<div class='card'><table>";
  html += "<tr><th>Version</th><td>20.0</td></tr>";
  html += "<tr><th>" + tr("input_mode") + "</th><td>" + inputModeName() + "</td></tr>";
  html += "<tr><th>WiFi</th><td>" + String(setupMode ? "<span class='bad'>Setup Mode</span>" : (WiFi.status()==WL_CONNECTED ? "<span class='ok'>" + tr("connected") + "</span>" : "<span class='bad'>" + tr("not_connected") + "</span>")) + "</td></tr>";
  html += "<tr><th>" + tr("hostname") + "</th><td>" + getWifiHostname() + ".local</td></tr>";
  html += "<tr><th>URL</th><td><a href='http://" + getWifiHostname() + ".local'>http://" + getWifiHostname() + ".local</a></td></tr>";
  html += "<tr><th>IP</th><td>" + (setupMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString()) + "</td></tr>";
  html += "<tr><th>MQTT</th><td>" + String(mqttClient.connected() ? "<span class='ok'>" + tr("connected") + "</span>" : "<span class='bad'>" + tr("not_connected") + "</span>") + "</td></tr>";
  html += "<tr><th>" + tr("mqtt_broker") + "</th><td>" + String(mqttSettings.broker) + ":" + String(mqttSettings.port) + " TLS=" + String(mqttSettings.useTls ? tr("yes") : tr("no")) + "</td></tr>";
  html += "<tr><th>" + tr("latest_mqtt_message") + "</th><td>" + String(lastMqttMessage) + "</td></tr>";
  html += "<tr><th>" + tr("latest_mqtt_attempt") + "</th><td>" + ageText(lastMqttAttemptMillis) + "</td></tr>";
  html += "<tr><th>" + tr("latest_dcc_command") + "</th><td>" + String(lastDccCommandMillis ? (String(lastDccAddress) + (lastDccOn ? " ON" : " OFF") + " / " + ageText(lastDccCommandMillis)) : tr("never")) + "</td></tr>";
  html += "<tr><th>" + tr("free_heap") + "</th><td>" + String(ESP.getFreeHeap()) + " bytes</td></tr>";
  html += "<tr><th>" + tr("uptime") + "</th><td>" + String(millis()/1000) + " " + tr("seconds") + "</td></tr>";
  html += "</table></div>";
  html += "<div class='card'><h2>" + tr("developed_by") + "</h2><p><b>Jens Krogsgaard</b><br>Farum, Denmark<br><a href='https://www.krogsgaardsmodelbane.dk' target='_blank'>www.krogsgaardsmodelbane.dk</a></p></div>";
  html += pageEnd(); server.send(200, "text/html", html);
}



String htmlEscape(const String &in) {
  String out;
  for (uint16_t i = 0; i < in.length(); i++) {
    char c = in[i];
    if (c == '&') out += "&amp;";
    else if (c == '<') out += "&lt;";
    else if (c == '>') out += "&gt;";
    else if (c == '"') out += "&quot;";
    else out += c;
  }
  return out;
}

String wifiEncryptionName(wifi_auth_mode_t t) {
  if (t == WIFI_AUTH_OPEN) return language == 1 ? String("open") : (language == 2 ? String("offen") : String("åben"));
  if (t == WIFI_AUTH_WEP) return "WEP";
  if (t == WIFI_AUTH_WPA_PSK) return "WPA";
  if (t == WIFI_AUTH_WPA2_PSK) return "WPA2";
  if (t == WIFI_AUTH_WPA_WPA2_PSK) return "WPA/WPA2";
  if (t == WIFI_AUTH_WPA2_ENTERPRISE) return "WPA2 Ent.";
  if (t == WIFI_AUTH_WPA3_PSK) return "WPA3";
  if (t == WIFI_AUTH_WPA2_WPA3_PSK) return "WPA2/WPA3";
  return language == 1 ? String("unknown") : (language == 2 ? String("unbekannt") : String("ukendt"));
}

String scannedWifiOptionsHtml() {
  String html;
  html += "<select style='width:330px' name='ssidSelect' onchange='document.getElementById(\"ssidManual\").value=this.value'>";
  html += "<option value=''>" + tr("select_network") + "</option>";

  int n = WiFi.scanNetworks(false, true);   // sync scan, vis også skjulte
  if (n <= 0) {
    html += "<option value=''>" + tr("no_networks") + "</option>";
  } else {
    int order[30];
    if (n > 30) n = 30;
    for (int i = 0; i < n; i++) order[i] = i;
    for (int i = 0; i < n - 1; i++) {
      for (int j = i + 1; j < n; j++) {
        if (WiFi.RSSI(order[j]) > WiFi.RSSI(order[i])) {
          int tmp = order[i]; order[i] = order[j]; order[j] = tmp;
        }
      }
    }

    for (int oi = 0; oi < n; oi++) {
      int i = order[oi];
      String ssid = WiFi.SSID(i);
      String label = ssid;
      if (label.length() == 0) label = "(" + tr("hidden_network") + ")";
      label += "  ";
      label += String(WiFi.RSSI(i));
      label += " dBm  ";
      label += wifiEncryptionName(WiFi.encryptionType(i));

      html += "<option value='" + htmlEscape(ssid) + "'>" + htmlEscape(label) + "</option>";
    }
  }
  WiFi.scanDelete();
  html += "</select>";
  return html;
}

void handleWifiPage() {
  String html = pageStart(tr("wifi"), "wifi");

  html += "<div class='card'><h2>" + tr("wifi_status") + "</h2><table>";
  html += "<tr><th>" + tr("mode") + "</th><td>" + String(setupMode ? tr("setup_mode") : tr("normal_operation")) + "</td></tr>";
  html += "<tr><th>" + tr("current_ssid") + "</th><td>" + String(WiFi.status()==WL_CONNECTED ? connectedWifiSsid : String("-") ) + "</td></tr>";
  html += "<tr><th>" + tr("hostname") + "</th><td>" + getWifiHostname() + ".local</td></tr>";
  html += "<tr><th>IP</th><td>" + String(setupMode ? WiFi.softAPIP().toString() : WiFi.localIP().toString()) + "</td></tr>";
  html += "</table></div>";

  html += "<div class='card'><h2>" + tr("save_new_wifi") + "</h2>";
  html += "<p>" + tr("wifi_help") + "</p>";
  html += "<form method='POST' action='/wifisave'>";
  html += "<table>";
  html += "<tr><th>" + tr("found_networks") + "</th><td>" + scannedWifiOptionsHtml() + " <a class='btn' href='/wifi'>" + tr("scan_again") + "</a></td></tr>";
  html += "<tr><th>" + tr("manual_ssid") + "</th><td><input id='ssidManual' style='width:300px' name='ssid' maxlength='32' value='" + htmlEscape(WiFi.status()==WL_CONNECTED ? connectedWifiSsid : String("")) + "'></td></tr>";
  html += "<tr><th>" + tr("password") + "</th><td><input style='width:300px' type='password' name='password' maxlength='64'></td></tr>";
  html += "</table>";
  html += "<button type='submit'>" + tr("save_wifi_restart") + "</button></form>";
  html += "<p class='note'>" + tr("wifi_note") + " <b>LysDekoder-Setup</b>. " + tr("connect_setup") + " <b>192.168.4.1</b>.</p>";
  html += "</div>";
html += pageEnd();
  server.send(200, "text/html", html);
}

void handleWifiSave() {
  if (!server.hasArg("ssid")) {
    server.send(400, "text/plain", tr("saved_wifi_missing"));
    return;
  }

  String ssid = server.arg("ssid");
  String password = server.arg("password");
  String hostname = getWifiHostname();
  ssid.trim();

  if (ssid.length() == 0) {
    server.send(400, "text/plain", tr("saved_wifi_empty"));
    return;
  }

  saveSavedWifi(ssid, password, hostname);

  String html;
  html += "<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<title>" + tr("wifi_saved") + "</title></head><body style='font-family:Arial;padding:24px'>";
  html += "<h1>" + tr("wifi_saved") + "</h1>";
  html += "<p>" + tr("decoder_restarts") + "</p>";
  html += "<h2>" + ssid + "</h2>";
  html += "<p>" + tr("setup_disappears") + "</p>";
  html += "</body></html>";
  server.send(200, "text/html", html);

  oledShowMessage(tr("wifi_saved"), ssid, tr("restart"), "");
  delay(1500);
  ESP.restart();
}

void handleNotFound() {
  if (setupMode) {
    server.sendHeader("Location", "/wifi", true);
    server.send(302, "text/plain", "");
  } else {
    server.send(404, "text/plain", tr("page_not_found"));
  }
}

void handleWebSave() {
  bool restartAfterSave = false;
  if (server.hasArg("inputMode")) { int v = server.arg("inputMode").toInt(); if (v>=1 && v<=3) inputMode = v; }
  if (server.hasArg("hostname")) {
    String newHost = sanitizeHostname(server.arg("hostname"));
    if (newHost != getWifiHostname()) {
      saveWifiHostnameOnly(newHost);
      restartAfterSave = true;
    }
  }
  if (server.hasArg("language")) { int v = server.arg("language").toInt(); if (v>=0 && v<=2) language = v; }

  if (server.hasArg("mqttEnabled") || server.hasArg("mqttBroker") || server.hasArg("mqttPort") || server.hasArg("mqttBaseTopic")) {
    mqttSettings.enabled = server.hasArg("mqttEnabled");
    if (server.hasArg("mqttBroker")) { String v=server.arg("mqttBroker"); v.trim(); v.toCharArray(mqttSettings.broker, sizeof(mqttSettings.broker)); }
    if (server.hasArg("mqttPort")) { int v=server.arg("mqttPort").toInt(); if(v<1)v=1883; if(v>65535)v=65535; mqttSettings.port=v; }
    if (server.hasArg("mqttTls")) mqttSettings.useTls = (server.arg("mqttTls").toInt()==1);
    mqttSettings.tlsInsecure = server.hasArg("mqttTlsInsecure");
    if (server.hasArg("mqttUser")) { String v=server.arg("mqttUser"); v.trim(); v.toCharArray(mqttSettings.user, sizeof(mqttSettings.user)); }
    if (server.hasArg("mqttPassword")) { String v=server.arg("mqttPassword"); v.toCharArray(mqttSettings.password, sizeof(mqttSettings.password)); }
    if (server.hasArg("mqttBaseTopic")) { String v=server.arg("mqttBaseTopic"); v.trim(); while(v.endsWith("/")) v.remove(v.length()-1); if(v.length()==0)v="WDP/DCCLysDekoder"; v.toCharArray(mqttSettings.baseTopic, sizeof(mqttSettings.baseTopic)); }
  }

  if (server.hasArg("rndDay")) { int v=server.arg("rndDay").toInt(); if(v<1)v=1; if(v>240)v=240; randomDayMinutes=v; }
  if (server.hasArg("rndNight")) { int v=server.arg("rndNight").toInt(); if(v<1)v=1; if(v>240)v=240; randomNightMinutes=v; }
  for (int s=0; s<MAX_STRIPS; s++) { String n="rndAddr"+String(s); if(server.hasArg(n)){int v=server.arg(n).toInt(); if(v>=1&&v<=2044) randomConfig[s].dccAddress=v;} }
  for (int s=0; s<MAX_STRIPS; s++) { String n="count"+String(s); if(server.hasArg(n)){int v=server.arg(n).toInt(); if(v<0)v=0; if(v>MAX_LEDS_PER_STRIP)v=MAX_LEDS_PER_STRIP; ledCount[s]=v;} }

  for (int s=0; s<MAX_STRIPS; s++) {
    for (int i=0; i<ledCount[s]; i++) {
      int g=globalLedIndex(s,i);
      String an="addr"+String(g), mn="mode"+String(g), bn="bright"+String(g);
      if(server.hasArg(an)){int v=server.arg(an).toInt(); if(v>=1&&v<=2044) config[s][i].dccAddress=v;}
      if(server.hasArg(mn)){int v=server.arg(mn).toInt(); if(v>=0&&v<=19) config[s][i].mode=v;}
      if(server.hasArg(bn)){int v=server.arg(bn).toInt(); if(v>=0&&v<=255) config[s][i].brightness=v;}
    }
  }

  clearInactiveLeds(); saveConfig(); printConfig(); mqttClient.disconnect(); setupMqtt();

  if (restartAfterSave) {
    String h = getWifiHostname();
    String html = "<!DOCTYPE html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'>";
    html += "<title>" + tr("saved") + "</title></head><body style='font-family:Arial;padding:24px'>";
    html += "<h1>" + tr("saved") + "</h1><p>" + tr("decoder_restarts") + "</p>";
    html += "<p>http://" + h + ".local</p></body></html>";
    server.send(200, "text/html", html);
    delay(1200);
    ESP.restart();
    return;
  }

  String referer = server.header("Referer");
  String dest = "/system";
  if (referer.indexOf("/strips") >= 0) dest = "/strips";
  if (referer.indexOf("/leds") >= 0) {
    int sv = 0; if(server.hasArg("stripView")) sv=server.arg("stripView").toInt(); dest = "/leds?strip=" + String(sv);
  }
  if (referer.indexOf("/random") >= 0) dest = "/random";
  if (referer.indexOf("/mqtt") >= 0) dest = "/mqtt";
  server.sendHeader("Location", dest); server.send(303);
}
