diff --git a/assets/css/webdesign.css b/assets/css/webdesign.css
index f9de692e..2ed8ef65 100644
--- a/assets/css/webdesign.css
+++ b/assets/css/webdesign.css
@@ -197,6 +197,25 @@ body.wd::after {
}
.wd-skip:focus { left: 0; }
+/* ---------------------------------------------------------------------
+ [hidden] MUSS gewinnen.
+
+ Das hidden-Attribut wird vom Browser nur mit "display: none" aus dem
+ Standard-Stylesheet umgesetzt — jede eigene display-Angabe schlaegt es.
+ Und genau die haben hier viele Elemente: Knoepfe sind inline-flex,
+ Karten sind flex, Raster sind grid.
+
+ Folge ohne diese Regel: el.hidden = true im JavaScript hat schlicht
+ keine Wirkung, und das Element bleibt sichtbar. Genau so aufgefallen im
+ Anfrageformular (22.08.2026) -- der "Zurueck"-Knopf stand auch auf dem
+ ersten Schritt da, obwohl es dorthin nichts zurueckzugehen gibt.
+
+ Das !important ist hier ausnahmsweise richtig: "versteckt" ist eine
+ Aussage ueber den Zustand, nicht ueber die Gestaltung, und darf von
+ keiner Gestaltungsregel ueberstimmt werden.
+ --------------------------------------------------------------------- */
+.wd [hidden] { display: none !important; }
+
.wd-visuell-versteckt {
position: absolute; width: 1px; height: 1px; overflow: hidden;
clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
diff --git a/assets/js/i18n-wd-anfrage.js b/assets/js/i18n-wd-anfrage.js
index b61039ff..65b81f46 100644
--- a/assets/js/i18n-wd-anfrage.js
+++ b/assets/js/i18n-wd-anfrage.js
@@ -292,6 +292,16 @@ window.I18N_WD = {
fr: "Note-le — avec ce numéro, ta demande est retrouvée immédiatement si tu nous relances.",
pt: "Aponta-o — com este número o teu pedido é encontrado de imediato se voltares a contactar.",
},
+ /* Fallback, wenn der Server aus Spamschutzgruenden keine Nummer
+ vergeben hat. Bewusst ohne Schuldzuweisung und mit zweitem Weg. */
+ af_danke_ohne_nummer: {
+ de: "Falls du innerhalb von zwei Tagen nichts hörst, schreib mir bitte kurz direkt eine E-Mail — dann ist unterwegs etwas verloren gegangen.",
+ "de-CH": "Falls du innert zwei Täg nüt ghörsch, schriib mer bitte churz e E-Mail — denn isch underwägs öppis verlore gange.",
+ en: "If you do not hear back within two days, please send me a short e-mail directly — then something got lost along the way.",
+ fr: "Si tu n'as pas de nouvelles sous deux jours, envoie-moi un court e-mail — c'est que quelque chose s'est perdu en route.",
+ pt: "Se não tiveres resposta em dois dias, envia-me um e-mail curto — é porque algo se perdeu pelo caminho.",
+ },
+
af_danke_zurueck: { de: "Zurück zur Startseite", "de-CH": "Zrugg zur Startsite", en: "Back to the home page", fr: "Retour à l'accueil", pt: "Voltar ao início" },
/* --- Fehlermeldungen --- */
diff --git a/assets/js/wd-anfrage.js b/assets/js/wd-anfrage.js
new file mode 100644
index 00000000..f27ccc9b
--- /dev/null
+++ b/assets/js/wd-anfrage.js
@@ -0,0 +1,540 @@
+/* =====================================================================
+ wd-anfrage.js — Steuerung des mehrstufigen Anfrageformulars
+ (Masterplan S.10 "Projektanfrage ohne Informationsverlust")
+
+ Aufbau: 4 Eingabeschritte + Zusammenfassung + Dankeseite.
+
+ Leitgedanken:
+ - Nichts geht verloren. Jede Eingabe landet sofort im Browserspeicher;
+ ein geschlossener Tab, ein leerer Akku oder ein versehentliches
+ Zurück kostet keine Arbeit.
+ - Geprüft wird beim Weitergehen, nicht beim Tippen. Wer noch mitten im
+ Wort ist, soll nicht angemeckert werden.
+ - Die Zusammenfassung zeigt, was WIRKLICH abgeschickt wird — aus den
+ Feldern gelesen, nicht aus dem, was das Skript zu wissen glaubt.
+ ===================================================================== */
+(function () {
+ "use strict";
+
+ var SPEICHER = "wd-anfrage-entwurf";
+ var LETZTE_SEITE_EINGABE = 4; // danach kommt die Zusammenfassung
+ var SEITE_ZUSAMMENFASSUNG = 5;
+ var SEITE_DANKE = 6;
+
+ var form, seiten, zaehler, marken, meldung, knopfLeiste, btnZurueck, btnWeiter;
+ var aktuelleSeite = 1;
+ var geoeffnetAm = Date.now();
+
+ /* ------------------------------------------------------------------
+ Werte einsammeln. Bewusst direkt aus dem Formular statt aus einem
+ nebenher gepflegten Objekt: so kann die Anzeige nie etwas anderes
+ behaupten als das, was tatsächlich in den Feldern steht.
+ ------------------------------------------------------------------ */
+ function werte() {
+ var d = new FormData(form);
+ var funktionen = d.getAll("funktionen");
+
+ /* Kategoriespezifische Felder tragen das Präfix "d_" und wandern
+ gesammelt in ein Unterobjekt. Neue Folgefragen brauchen dadurch
+ weder hier noch in der Datenbank eine Änderung. */
+ var details = {};
+ form.querySelectorAll("[name^='d_']").forEach(function (el) {
+ /* Nur Felder mitnehmen, die gerade sichtbar sind — sonst schickt
+ jemand, der von "Shop" auf "Onepager" zurückwechselt, immer noch
+ seine alten Shop-Antworten mit. */
+ if (el.closest("[hidden]")) return;
+ var wert = (el.value || "").trim();
+ if (wert) details[el.name.slice(2)] = wert;
+ });
+
+ var vorhanden = {};
+ form.querySelectorAll("[name^='v_']").forEach(function (el) {
+ if (el.checked) vorhanden[el.name.slice(2)] = el.value;
+ });
+
+ return {
+ paket: d.get("paket") || "",
+ ziel: (d.get("ziel") || "").trim(),
+ zielgruppe: (d.get("zielgruppe") || "").trim(),
+ seiten: (d.get("seiten") || "").trim(),
+ funktionen: funktionen,
+ details: details,
+ hatTexte: vorhanden.texte || "",
+ hatBilder: vorhanden.bilder || "",
+ hatLogo: vorhanden.logo || "",
+ hatDomain: vorhanden.domain || "",
+ budget: d.get("budget") || "",
+ termin: (d.get("termin") || "").trim(),
+ name: (d.get("name") || "").trim(),
+ email: (d.get("email") || "").trim(),
+ telefon: (d.get("telefon") || "").trim(),
+ kontaktweg: d.get("kontaktweg") || "email",
+ bemerkung: (d.get("bemerkung") || "").trim(),
+ datenschutz: !!d.get("datenschutz"),
+ website: (d.get("website") || "").trim(), // Honigtopf
+ };
+ }
+
+ /* ------------------------------------------------------------------
+ Zwischenspeicherung (Masterplan S.10: "Zwischenspeicherung verhindert
+ Datenverlust")
+ ------------------------------------------------------------------ */
+ function speichern() {
+ try {
+ var w = werte();
+ delete w.website; // Honigtopf nie speichern
+ sessionStorageSchreiben(JSON.stringify({ stand: w, seite: aktuelleSeite, zeit: Date.now() }));
+ } catch (e) { /* Speicher voll oder gesperrt — kein Grund abzubrechen */ }
+ }
+
+ function sessionStorageSchreiben(text) {
+ /* localStorage, NICHT sessionStorage: der Entwurf soll auch morgen
+ noch da sein. Das ist der ganze Zweck. */
+ try { localStorage.setItem(SPEICHER, text); } catch (e) { /* egal */ }
+ }
+
+ function gespeichertesLesen() {
+ try {
+ var roh = localStorage.getItem(SPEICHER);
+ if (!roh) return null;
+ var daten = JSON.parse(roh);
+ /* Entwürfe verfallen nach 30 Tagen. Ein halbes Jahr alter Entwurf
+ verwirrt mehr, als er hilft. */
+ if (!daten.zeit || Date.now() - daten.zeit > 30 * 24 * 3600 * 1000) {
+ localStorage.removeItem(SPEICHER);
+ return null;
+ }
+ return daten;
+ } catch (e) { return null; }
+ }
+
+ function entwurfLoeschen() {
+ try { localStorage.removeItem(SPEICHER); } catch (e) { /* egal */ }
+ }
+
+ function wiederherstellen(daten) {
+ var w = daten.stand || {};
+ function setz(name, wert) {
+ var el = form.querySelector("[name='" + name + "']");
+ if (el && typeof wert === "string") el.value = wert;
+ }
+ setz("ziel", w.ziel); setz("zielgruppe", w.zielgruppe); setz("seiten", w.seiten);
+ setz("termin", w.termin); setz("name", w.name); setz("email", w.email);
+ setz("telefon", w.telefon); setz("bemerkung", w.bemerkung);
+ var budget = form.querySelector("[name='budget']");
+ if (budget && w.budget) budget.value = w.budget;
+
+ if (w.paket) {
+ var p = form.querySelector("[name='paket'][value='" + w.paket + "']");
+ if (p) p.checked = true;
+ }
+ if (w.kontaktweg) {
+ var k = form.querySelector("[name='kontaktweg'][value='" + w.kontaktweg + "']");
+ if (k) k.checked = true;
+ }
+ (w.funktionen || []).forEach(function (f) {
+ var el = form.querySelector("[name='funktionen'][value='" + f + "']");
+ if (el) el.checked = true;
+ });
+ Object.keys(w.details || {}).forEach(function (k) {
+ var el = form.querySelector("[name='d_" + k + "']");
+ if (el) el.value = w.details[k];
+ });
+ [["texte", w.hatTexte], ["bilder", w.hatBilder], ["logo", w.hatLogo], ["domain", w.hatDomain]]
+ .forEach(function (paar) {
+ if (!paar[1]) return;
+ var el = form.querySelector("[name='v_" + paar[0] + "'][value='" + paar[1] + "']");
+ if (el) el.checked = true;
+ });
+ if (w.datenschutz) {
+ var ds = form.querySelector("[name='datenschutz']");
+ if (ds) ds.checked = true;
+ }
+ }
+
+ /* ------------------------------------------------------------------
+ Tabelle "Was hast du schon?" — per JS gebaut, damit die vier Zeilen
+ nicht viermal fast gleich im HTML stehen.
+ ------------------------------------------------------------------ */
+ function baueVorhandenTabelle() {
+ var koerper = document.getElementById("af-vorhanden");
+ if (!koerper) return;
+ var zeilen = [
+ ["texte", "af3_texte"],
+ ["bilder", "af3_bilder"],
+ ["logo", "af3_logo"],
+ ["domain", "af3_domain"],
+ ];
+ var antworten = [["ja", "af3_ja"], ["teilweise", "af3_teilweise"], ["nein", "af3_nein"]];
+
+ koerper.innerHTML = zeilen.map(function (z) {
+ var knoepfe = antworten.map(function (a) {
+ return '";
+ }).join("");
+ return '
' +
+ window.WD.t(z[1]) + "
" + knoepfe + "
";
+ }).join("");
+ }
+
+ /* ------------------------------------------------------------------
+ Prüfung je Schritt
+ ------------------------------------------------------------------ */
+ function fehlerSetzen(feldId, text) {
+ var feld = document.getElementById(feldId);
+ if (!feld) return;
+ feld.classList.toggle("wd-feld--fehler", !!text);
+ var p = feld.querySelector(".wd-feld-fehler");
+ if (p) p.textContent = text || "";
+ }
+
+ function fehlerLeeren() {
+ form.querySelectorAll(".wd-feld--fehler").forEach(function (f) {
+ f.classList.remove("wd-feld--fehler");
+ var p = f.querySelector(".wd-feld-fehler");
+ if (p) p.textContent = "";
+ });
+ }
+
+ /* Dieselbe absichtlich einfache Prüfung wie auf dem Server: Die einzige
+ zuverlässige Kontrolle ist ohnehin, ob die Antwortmail ankommt. Eine
+ strenge Regel lehnt vor allem gültige Adressen ab. */
+ function istEmail(s) {
+ var t = String(s || "").trim().toLowerCase();
+ var teile = t.split("@");
+ return teile.length === 2 && !!teile[0] && teile[1].indexOf(".") > 0 && !/\s/.test(t);
+ }
+
+ function pruefeSeite(nr) {
+ fehlerLeeren();
+ var w = werte();
+ var fehler = [];
+
+ if (nr === 1) {
+ if (!w.paket) { fehlerSetzen("feld-paket", window.WD.t("af_fehler_paket")); fehler.push("feld-paket"); }
+ if (!w.ziel) { fehlerSetzen("feld-ziel", window.WD.t("af_fehler_ziel")); fehler.push("feld-ziel"); }
+ }
+ if (nr === 4) {
+ if (!w.name) { fehlerSetzen("feld-name", window.WD.t("af_fehler_name")); fehler.push("feld-name"); }
+ if (!istEmail(w.email)) { fehlerSetzen("feld-email", window.WD.t("af_fehler_email")); fehler.push("feld-email"); }
+ /* Wer telefonisch kontaktiert werden möchte, muss eine Nummer
+ angeben — sonst kann der Wunsch gar nicht erfüllt werden. */
+ if (w.kontaktweg === "telefon" && !w.telefon) {
+ fehlerSetzen("feld-telefon", window.WD.t("af_fehler_telefon")); fehler.push("feld-telefon");
+ }
+ if (!w.datenschutz) {
+ fehlerSetzen("feld-datenschutz", window.WD.t("af_fehler_datenschutz")); fehler.push("feld-datenschutz");
+ }
+ }
+
+ if (fehler.length) {
+ window.WD.meldung(meldung, window.WD.t("af_fehler_schritt"), "fehler");
+ var erstes = document.getElementById(fehler[0]);
+ if (erstes) {
+ erstes.scrollIntoView({ behavior: "smooth", block: "center" });
+ var eingabe = erstes.querySelector("input, textarea, select");
+ if (eingabe) setTimeout(function () { eingabe.focus({ preventScroll: true }); }, 300);
+ }
+ return false;
+ }
+ window.WD.meldungAus(meldung);
+ return true;
+ }
+
+ /* ------------------------------------------------------------------
+ Navigation
+ ------------------------------------------------------------------ */
+ function zeigeSeite(nr) {
+ aktuelleSeite = nr;
+ seiten.forEach(function (s) {
+ s.classList.toggle("af-sichtbar", Number(s.dataset.seite) === nr);
+ });
+
+ marken.forEach(function (m) {
+ var i = Number(m.dataset.marke);
+ m.classList.toggle("af-aktiv", i === nr);
+ m.classList.toggle("af-erledigt", i < nr);
+ });
+
+ var fortschritt = document.getElementById("af-fortschritt");
+ var hinweis = document.getElementById("af-speicherhinweis");
+
+ if (nr === SEITE_DANKE) {
+ knopfLeiste.hidden = true;
+ if (fortschritt) fortschritt.hidden = true;
+ if (hinweis) hinweis.hidden = true;
+ } else {
+ knopfLeiste.hidden = false;
+ if (fortschritt) fortschritt.hidden = false;
+ if (hinweis) hinweis.hidden = false;
+ btnZurueck.hidden = nr === 1;
+ btnWeiter.textContent =
+ nr === SEITE_ZUSAMMENFASSUNG ? window.WD.t("af_senden")
+ : nr === LETZTE_SEITE_EINGABE ? window.WD.t("af_pruefen")
+ : window.WD.t("af_weiter");
+ zaehler.textContent =
+ window.WD.t("af_schritt") + " " + Math.min(nr, 4) + " " + window.WD.t("af_von") + " 4";
+ }
+
+ /* Nach oben, aber nur bis zum Formular — ein Sprung ganz nach oben
+ zwingt sonst bei jedem Schritt zum Zurückscrollen. */
+ var rahmen = document.getElementById("af-rahmen");
+ if (rahmen) {
+ var y = rahmen.getBoundingClientRect().top + window.scrollY - 80;
+ window.scrollTo({ top: Math.max(0, y), behavior: "smooth" });
+ }
+
+ /* Auf der Dankeseite NICHT mehr speichern.
+ Sonst passiert Folgendes: absenden() loescht den Entwurf, ruft dann
+ zeigeSeite(DANKE) auf — und diese Zeile legt ihn sofort wieder an.
+ Beim naechsten Besuch stuenden die Daten einer bereits gesendeten
+ Anfrage wieder im Formular, samt "Wir haben deine letzte Eingabe
+ wiederhergestellt". Genau so im Selbsttest aufgefallen. */
+ if (nr !== SEITE_DANKE) speichern();
+ }
+
+ function folgefragenAktualisieren() {
+ var w = werte();
+ var shop = w.paket === "shop" || w.funktionen.indexOf("shop") >= 0;
+ var vw = w.paket === "verwaltung" || w.funktionen.indexOf("login") >= 0;
+ var eShop = document.getElementById("af-folge-shop");
+ var eVw = document.getElementById("af-folge-verwaltung");
+ if (eShop) eShop.hidden = !shop;
+ if (eVw) eVw.hidden = !vw;
+ }
+
+ /* ------------------------------------------------------------------
+ Zusammenfassung
+ ------------------------------------------------------------------ */
+ function beschriftung(schluessel, roh) {
+ if (!roh) return null;
+ var t = window.WD.t(schluessel);
+ return t === schluessel ? roh : t;
+ }
+
+ function baueZusammenfassung() {
+ var w = werte();
+ var ziel = document.getElementById("af-zusammenfassung");
+ var sicher = window.WD.sicher;
+ var leer = window.WD.t("af_zus_leer");
+
+ function zeile(was, wert) {
+ var istLeer = !wert;
+ return '