diff --git a/assets/css/main.css b/assets/css/main.css
index 9e71c9bb..4b2eca4f 100644
--- a/assets/css/main.css
+++ b/assets/css/main.css
@@ -4723,3 +4723,11 @@ h1, h2, h3 { text-shadow: 0 1px 2px rgba(0, 0, 0, .45); }
.btn::after, .btn-primary::after, .btn-outline::after { display: none; }
h1, h2, h3 { text-shadow: none; }
}
+
+/* Eine Kachel, die noch nichts dahinter hat (03.09.2026).
+ Sie sieht aus wie die anderen, verspricht aber nichts: kein Zeiger,
+ kein Anheben beim Ueberfahren. Der Unterschied ist klein und genau
+ deshalb richtig -- man merkt ihn erst, wenn man darauf zeigt, und
+ dann weiss man Bescheid, ohne dass es jemand hinschreiben muss. */
+.karte--bald { cursor: default; }
+.karte--bald:hover { transform: none; border-color: var(--border); box-shadow: var(--shadow); }
diff --git a/links.html b/links.html
index 0996b0d1..929eef94 100644
--- a/links.html
+++ b/links.html
@@ -107,11 +107,24 @@
Discord
Nur auf Anfrage
-
+
+
🛒
Merchandise / Shop
Folgt in Kürze
-
+
📝
Bewerben
diff --git a/server/abnahme-installiert.png b/server/abnahme-installiert.png
new file mode 100644
index 00000000..87e64839
Binary files /dev/null and b/server/abnahme-installiert.png differ
diff --git a/server/pruef-ampel.png b/server/pruef-ampel.png
index 2ec06450..2be1d134 100644
Binary files a/server/pruef-ampel.png and b/server/pruef-ampel.png differ
diff --git a/server/pruef-aufgabenbrett.png b/server/pruef-aufgabenbrett.png
index 9544dcab..179dde79 100644
Binary files a/server/pruef-aufgabenbrett.png and b/server/pruef-aufgabenbrett.png differ
diff --git a/server/pruef-checkliste.png b/server/pruef-checkliste.png
new file mode 100644
index 00000000..90c413f8
Binary files /dev/null and b/server/pruef-checkliste.png differ
diff --git a/server/pruef-code.png b/server/pruef-code.png
index 62480d07..83d1a588 100644
Binary files a/server/pruef-code.png and b/server/pruef-code.png differ
diff --git a/server/pruef-content-computer.png b/server/pruef-content-computer.png
new file mode 100644
index 00000000..644cb4ad
Binary files /dev/null and b/server/pruef-content-computer.png differ
diff --git a/server/pruef-content-creator.png b/server/pruef-content-creator.png
new file mode 100644
index 00000000..0a58fbaa
Binary files /dev/null and b/server/pruef-content-creator.png differ
diff --git a/server/pruef-content-handy.png b/server/pruef-content-handy.png
new file mode 100644
index 00000000..1a7f1755
Binary files /dev/null and b/server/pruef-content-handy.png differ
diff --git a/server/pruef-freie-namen.png b/server/pruef-freie-namen.png
index e43dadaa..78e37324 100644
Binary files a/server/pruef-freie-namen.png and b/server/pruef-freie-namen.png differ
diff --git a/server/pruef-live-abnahme.mjs b/server/pruef-live-abnahme.mjs
new file mode 100644
index 00000000..f9a570f2
--- /dev/null
+++ b/server/pruef-live-abnahme.mjs
@@ -0,0 +1,221 @@
+/* =====================================================================
+ ABNAHME VOR DER VORSTELLUNG (03.09.2026)
+
+ Alle anderen Prüfungen laufen gegen einen Server auf diesem Rechner.
+ Diese hier geht gegen die ECHTE Domain -- so, wie sie morgen jemand
+ im Publikum aufruft.
+
+ Der Unterschied ist nicht theoretisch: Zwischen "läuft bei mir" und
+ "läuft im Netz" liegen Caddy, Cloudflare, der Cache-Stempel, das
+ Zertifikat und der Dienst-Neustart. Genau dort ist am 18.08. und am
+ 22.08.2026 schon zweimal etwas hängengeblieben, das lokal tadellos
+ war (siehe CLAUDE.md, "Grundregel für ALLE Websites").
+
+ Geprüft wird in DREI Gestalten, weil das Publikum sie alle benutzt:
+
+ RECHNER 1440 px
+ HANDY 390 px, mit Fingerbedienung
+ INSTALLIERT als App vom Startbildschirm -- ohne Adresszeile, ohne
+ Zurück-Knopf des Browsers. Dort fällt auf, was man mit
+ Browserleiste nie merkt.
+
+ Gemessen wird je Seite:
+ * Fehler in der Konsole
+ * fehlgeschlagene Anfragen (4xx/5xx)
+ * ob etwas seitlich aus dem Bild ragt
+ * tote Verweise (href="#" oder leer)
+ * fehlende Bilder
+ * ob die Seite überhaupt Inhalt hat
+ ===================================================================== */
+
+const BASIS = process.env.ABNAHME_BASIS || "https://dogfather-universe.com";
+
+const SEITEN = [
+ "/", "/dogfather.html", "/casper.html", "/hasidog.html", "/vanvan.html",
+ "/community.html", "/streamer.html", "/supporter.html", "/abonnieren.html",
+ "/bewerben.html", "/bewerben-creator.html", "/bewerben-scout.html",
+ "/bewerben-modi.html", "/bewerben-kooperation.html",
+ "/kontakt.html", "/werte.html", "/team-scouts.html", "/team-modis.html",
+ "/stimmen.html", "/links.html", "/rabattcodes.html", "/streamplan.html",
+ "/zeitreise.html", "/agb.html", "/webdesign", "/404.html",
+];
+
+let fehler = 0;
+let geprueft = 0;
+const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
+const befunde = [];
+
+const EGAL = [
+ /favicon/i,
+ /net::ERR_ABORTED/i,
+ /Download the React DevTools/i,
+];
+const istEgal = (t) => EGAL.some((r) => r.test(t));
+
+const { chromium } = await import(
+ "file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
+const browser = await chromium.launch();
+
+async function rundgang(name, kontextOptionen, seiten) {
+ const kontext = await browser.newContext(kontextOptionen);
+ const seite = await kontext.newPage();
+ const konsole = [];
+ const schlecht = [];
+ seite.on("console", (m) => { if (m.type() === "error" && !istEgal(m.text())) konsole.push(m.text()); });
+ seite.on("pageerror", (e) => konsole.push("Absturz: " + e.message));
+ seite.on("response", (a) => {
+ if (a.status() >= 400 && !istEgal(a.url())) {
+ schlecht.push(`${a.status()} ${a.url().replace(BASIS, "")}`);
+ }
+ });
+
+ console.log(`\n===== ${name} =====`);
+ for (const pfad of seiten) {
+ konsole.length = 0; schlecht.length = 0;
+ let antwort = null;
+ try {
+ antwort = await seite.goto(BASIS + pfad, { waitUntil: "networkidle", timeout: 40000 });
+ } catch (e) {
+ ok(false, `${pfad.padEnd(28)} nicht erreichbar (${e.message.slice(0, 40)})`);
+ befunde.push({ name, pfad, was: "nicht erreichbar" });
+ continue;
+ }
+ await seite.waitForTimeout(500);
+
+ const z = await seite.evaluate(() => {
+ const sichtbar = (e) => {
+ const k = e.getBoundingClientRect();
+ return k.width > 0 && k.height > 0 && getComputedStyle(e).visibility !== "hidden";
+ };
+ const tote = [...document.querySelectorAll("a[href]")].filter(sichtbar)
+ .filter((a) => { const h = a.getAttribute("href"); return !h || h === "#"; })
+ .map((a) => a.textContent.trim().slice(0, 30));
+ /* Nur SICHTBARE Bilder mit einer Quelle. Die Seite hat
+ Platzhalter ( ohne src), die JavaScript erst
+ füllt, wenn es etwas zu zeigen gibt -- die sind kein Fehler,
+ sondern der Normalzustand. Beim ersten Lauf hat meine Prüfung
+ genau die gemeldet und damit zwei Seiten falsch beschuldigt. */
+ const bilderKaputt = [...document.querySelectorAll("img")]
+ .filter((b) => sichtbar(b) && (b.getAttribute("src") || "").trim())
+ .filter((b) => b.complete && b.naturalWidth === 0)
+ .map((b) => (b.getAttribute("src") || "").slice(-40));
+ return {
+ tote, bilderKaputt,
+ laenge: (document.body.innerText || "").trim().length,
+ ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
+ titel: document.title,
+ };
+ });
+
+ const probleme = [];
+ if (antwort && antwort.status() >= 400) probleme.push(`Seite antwortet ${antwort.status()}`);
+ if (konsole.length) probleme.push("Konsole: " + konsole.slice(0, 2).join(" | ").slice(0, 130));
+ if (schlecht.length) probleme.push("Server: " + [...new Set(schlecht)].slice(0, 3).join(", "));
+ if (z.tote.length) probleme.push("tote Verweise: " + z.tote.slice(0, 3).join(", "));
+ if (z.bilderKaputt.length) probleme.push("fehlende Bilder: " + z.bilderKaputt.slice(0, 2).join(", "));
+ if (z.ueber > 0) probleme.push(`ragt ${z.ueber}px heraus`);
+ if (z.laenge < 200 && pfad !== "/404.html") probleme.push(`fast kein Inhalt (${z.laenge})`);
+ if (!z.titel) probleme.push("kein Seitentitel");
+
+ ok(!probleme.length, `${pfad.padEnd(28)} ${probleme.join(" · ")}`);
+ if (probleme.length) befunde.push({ name, pfad, was: probleme.join(" · ") });
+ }
+ await kontext.close();
+}
+
+/* ---------- 1. Rechner ---------------------------------------------- */
+await rundgang("RECHNER (1440 px)", { viewport: { width: 1440, height: 950 } }, SEITEN);
+
+/* ---------- 2. Handy ------------------------------------------------- */
+await rundgang("HANDY (390 px, Finger)", {
+ viewport: { width: 390, height: 844 },
+ isMobile: true, hasTouch: true, deviceScaleFactor: 3,
+ userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 "
+ + "(KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
+}, SEITEN);
+
+/* ---------- 3. Installiert ------------------------------------------- */
+/* Als App vom Startbildschirm: kein Adressfeld, kein Zurück-Knopf des
+ Browsers. Wer sich dort verläuft, kommt nicht mehr heraus -- deshalb
+ ist diese Gestalt die unbarmherzigste. */
+console.log("\n===== INSTALLIERT (App-Ansicht) =====");
+
+const app = await browser.newContext({
+ viewport: { width: 390, height: 844 },
+ isMobile: true, hasTouch: true, deviceScaleFactor: 3,
+});
+const appSeite = await app.newPage();
+
+/* ERST auf die Domain gehen, DANN das Manifest holen.
+
+ Beim ersten Lauf stand die Probe noch auf about:blank und holte von
+ dort -- das ist für den Browser eine fremde Herkunft, und er lehnt
+ ab. Gemeldet wurde "manifest.json nicht ladbar", während curl
+ gleichzeitig 200 lieferte. Die Prüfung hat also nicht die Seite
+ geprüft, sondern sich selbst. */
+await appSeite.goto(BASIS + "/", { waitUntil: "domcontentloaded", timeout: 40000 });
+
+const manifest = await appSeite.evaluate(async (basis) => {
+ const a = await fetch(basis + "/manifest.json");
+ if (!a.ok) return { fehler: a.status };
+ return a.json();
+}, BASIS).catch(() => ({ fehler: "nicht ladbar" }));
+
+ok(!manifest.fehler, `manifest.json wird ausgeliefert${manifest.fehler ? " (" + manifest.fehler + ")" : ""}`);
+ok(!!manifest.name || !!manifest.short_name, `die App hat einen Namen ("${manifest.short_name || manifest.name}")`);
+ok(manifest.display === "standalone" || manifest.display === "fullscreen",
+ `sie startet ohne Browserleiste (display: ${manifest.display})`);
+ok(!!manifest.start_url, `sie weiss, wo sie startet (${manifest.start_url})`);
+ok(Array.isArray(manifest.icons) && manifest.icons.length > 0,
+ `sie hat ein Symbol für den Startbildschirm (${manifest.icons?.length || 0})`);
+
+/* Jedes Symbol muss wirklich da sein -- ein fehlendes fällt erst auf,
+ wenn es als graues Viereck auf dem Startbildschirm klebt. */
+for (const symbol of (manifest.icons || []).slice(0, 4)) {
+ const pfad = symbol.src.startsWith("http") ? symbol.src : BASIS + "/" + symbol.src.replace(/^\//, "");
+ const status = await appSeite.evaluate(async (u) => {
+ try { const a = await fetch(u); return a.status; } catch { return 0; }
+ }, pfad);
+ ok(status === 200, ` Symbol ${symbol.sizes || ""} vorhanden (${status})`);
+}
+
+await appSeite.goto(BASIS + (manifest.start_url || "/"), { waitUntil: "networkidle", timeout: 40000 });
+await appSeite.waitForTimeout(1500);
+
+const dienst = await appSeite.evaluate(async () => {
+ if (!("serviceWorker" in navigator)) return { da: false, grund: "nicht unterstützt" };
+ const r = await navigator.serviceWorker.getRegistration();
+ return { da: !!r, umfang: r?.scope || null, aktiv: !!r?.active };
+});
+ok(dienst.da, `der Hintergrunddienst ist angemeldet${dienst.umfang ? " (" + dienst.umfang + ")" : ""}`);
+ok(dienst.aktiv, "und aktiv – die App startet auch ohne Netz");
+
+const appStand = await appSeite.evaluate(() => ({
+ ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
+ laenge: (document.body.innerText || "").trim().length,
+ /* In der App gibt es keinen Zurück-Knopf des Browsers. Es MUSS also
+ auf der Seite selbst ein Weg zurück oder ein Menü geben. */
+ wege: document.querySelectorAll("a[href]").length,
+}));
+ok(appStand.ueber <= 0, `nichts ragt seitlich heraus (${appStand.ueber}px)`);
+ok(appStand.laenge > 200, `die Startseite hat Inhalt (${appStand.laenge} Zeichen)`);
+ok(appStand.wege > 3,
+ `es gibt Wege weiter (${appStand.wege} Verweise) – in der App gibt es keinen Browser-Zurück`);
+
+await appSeite.screenshot({ path: "abnahme-installiert.png", fullPage: false });
+await app.close();
+
+/* ---------- Abschluss ------------------------------------------------ */
+console.log("");
+const ERWARTET = SEITEN.length * 2 + 12;
+ok(geprueft >= ERWARTET,
+ `es sind ${geprueft} Prüfungen gelaufen (erwartet mindestens ${ERWARTET})`);
+
+if (befunde.length) {
+ console.log("\n--- WAS MORGEN AUFFALLEN WÜRDE ---");
+ for (const b of befunde) console.log(` ${b.name} · ${b.pfad}\n ${b.was}`);
+}
+
+await browser.close();
+console.log(`\n${fehler ? "FEHLER: " + fehler : "Alles in Ordnung"} — ${geprueft} Prüfungen`);
+process.exit(fehler ? 1 : 0);
diff --git a/server/pruef-merkzeichen.png b/server/pruef-merkzeichen.png
new file mode 100644
index 00000000..ac53fea5
Binary files /dev/null and b/server/pruef-merkzeichen.png differ
diff --git a/workspace/aufgaben.html b/workspace/aufgaben.html
index e02fffd7..04e2b0f1 100644
--- a/workspace/aufgaben.html
+++ b/workspace/aufgaben.html
@@ -7,9 +7,9 @@
-
-
-
+
+
+
@@ -152,9 +152,9 @@
-
-
-
-
+
+
+
+