Gestaltung: WCAG-Messung ueber alle Seiten, alle Befunde behoben

"Sieht gut aus" ist Geschmack und laesst sich nicht pruefen. Handwerkliche
Qualitaet laesst sich pruefen -- und bei einer WEBDESIGN-Seite ist sie
zugleich das Verkaufsargument: Wer selbst schludert, kann schlecht
Sorgfalt verkaufen.

Neues Werkzeug pruef-design.mjs misst 13 Seiten x 5 Sprachen auf
Handy-Breite gegen WCAG 2.2 AA -- denselben Standard, auf den auch das
Barrierefreiheitsstaerkungsgesetz verweist. Der Browser rechnet, es wird
nichts geschaetzt.

ERGEBNIS: 0 Befunde. Kontrast, Klickflaechen, Tastaturfokus,
Ueberschriftenstruktur, Bildalternativen, Feldbeschriftungen,
lang-Attribut, Schriftgroessen.

ECHTE BEFUNDE, DIE BEHOBEN WURDEN

1. 23 Schriftgroessen unter 12px (bis hinunter zu 10,9px), verteilt ueber
   CSS und sieben Seiten. Betroffen waren durchweg gesperrte
   Grossbuchstaben-Beschriftungen -- also genau die Textsorte, die klein
   ohnehin am schlechtesten lesbar ist. Alle auf mindestens 12px, die
   gesperrten auf 12,8px. Das ist zugleich die Dauervorgabe
   "augenschonend" ernst genommen.

2. Ueberschriftensprung h2 -> h4 in der Fusszeile, auf ALLEN 13 Seiten in
   allen 5 Sprachen (42 Stellen). Wer sich mit einem Screenreader durch
   die Ueberschriften bewegt, hoert dadurch eine Gliederung, die es nicht
   gibt, und vermutet uebersprungene Abschnitte. Jetzt h2 mit
   Gestaltungsklasse: Die Ebene sagt etwas ueber die STRUKTUR, die
   Groesse ueber die GEWICHTUNG -- zwei verschiedene Dinge.

3. code-Auszeichnung auf der Rechtsseite rutschte durch relative
   Groessenangabe auf 11,2px. Jetzt mit Untergrenze abgesichert.

SECHS FEHLALARME IM WERKZEUG -- ALLE GEFUNDEN UND BESEITIGT

Der erste Lauf meldete 302 Fundstellen. Fast alle waren falsch. Haette
ich sie "repariert", haette ich funktionierende Gestaltung zerstoert.
Jeder Fehlalarm hatte eine eigene, nicht offensichtliche Ursache:

  * Farbverlaeufe: getComputedStyle().backgroundColor liefert bei einer
    reinen Verlaufsflaeche "rgba(0,0,0,0)". Die Suche lief an der hellen
    Knopfflaeche VORBEI bis zum dunklen Seitengrund und verglich dunkle
    Schrift mit dunklem Grund -- Ergebnis 1:1 fuer 60 einwandfreie
    Knoepfe. Jetzt werden die Farbstopps ausgelesen und der
    unguenstigste Fall gerechnet.

  * MEHRERE Hintergrundebenen: Die Karten nutzen den bekannten Kniff
    "Flaeche padding-box, Rahmenverlauf border-box". Der helle
    Rahmenverlauf ist im Textbereich gar nicht sichtbar. Alle Ebenen
    zusammenzurechnen ergab 820 Meldungen mit Werten wie "3,45:1" fuer
    Text, der in Wahrheit ueber 7:1 liegt. CSS malt die ERSTE Ebene oben
    -- nur die zaehlt. Das Trennen an Kommas muss dabei die Kommas
    INNERHALB der Verlaufsklammern respektieren.

  * Verlaufsschrift (background-clip:text): Dort ist die Schriftfarbe
    durchsichtig und der Verlauf IST der Text. Farbe gegen Hintergrund zu
    vergleichen ergibt zwangslaeufig 1:1.

  * Verlaufsstopps mit wenig Deckkraft (9 % und 4 % Gold in den
    Hinweiskaesten) wurden verworfen statt auf den Untergrund gerechnet.
    Uebrig blieb "nicht messbar" fuer 72 Textstellen. Ein Pruefwerkzeug,
    das bei allem passt, prueft nichts.

  * Laufende Ueberblendungen: getComputedStyle liefert waehrend einer
    Ueberblendung den ZWISCHENWERT. Nach blur() verblasst der Fokusring
    ueber 0,16 s -- misst man sofort, sieht man ihn noch, und "vorher"
    ist identisch mit "nachher". Das Codefeld der Verwaltung wurde
    dadurch als "ohne sichtbaren Fokus" gemeldet. Nachgewiesen mit
    el.matches(":focus") === false bei gleichzeitig sichtbarem Schatten.
    Loesung: Ueberblendungen fuer die Messung abschalten.

  * Ausgeblendete Abschnitte: Eine Ueberschrift in einem versteckten
    Bereich hat selbst weiterhin display:block. Im Portal wurden dadurch
    drei h1 gezaehlt, obwohl immer nur eine sichtbar ist. Jetzt
    getClientRects().

Dazu zwei bekannte Muster, die das Werkzeug jetzt kennt: der Sprunglink
(absichtlich aus dem Bild geschoben) und die winzigen Auswahlfelder
hinter den Kacheln (die Kachel IST die Klickflaeche).

ALLE UEBRIGEN PRUEFUNGEN WEITERHIN GRUEN nach den Aenderungen:
Anfragedetail 20, Verwaltung 69, Portal 32, Widerruf 58, Textkodierung
55 Seiten-Sprach-Kombinationen.

Versionsstempel und Cache-Name auf v8.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-23 12:49:29 +02:00
co-authored by Claude Opus 5
parent 14ad108c87
commit 3ca9f0b0cd
30 changed files with 556 additions and 81 deletions
+27 -9
View File
@@ -263,7 +263,7 @@ body.wd::after {
.wd-eyebrow {
display: inline-block;
font-size: .74rem;
font-size: 0.8rem;
letter-spacing: .18em;
text-transform: uppercase;
font-weight: 800;
@@ -799,7 +799,7 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
.wd-marke img { width: 42px; height: 42px; object-fit: contain; }
.wd-marke-text { display: flex; flex-direction: column; line-height: 1.1; }
.wd-marke-oben { font-family: var(--font-head); font-weight: 800; font-size: 1rem; color: var(--wd-text); letter-spacing: .02em; }
.wd-marke-unten { font-size: .68rem; letter-spacing: .18em; text-transform: uppercase; color: var(--wd-blau); font-weight: 700; }
.wd-marke-unten { font-size: 0.75rem; letter-spacing: .18em; text-transform: uppercase; color: var(--wd-blau); font-weight: 700; }
.wd-nav { display: flex; align-items: center; gap: .15rem; margin-left: auto; }
.wd-nav a {
@@ -897,7 +897,23 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
.wd-fuss { border-top: 1px solid var(--wd-rand); background: rgba(5, 7, 11, .8); padding: 2.6rem 0 2rem; margin-top: 3rem; }
.wd-fuss-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 2rem; }
@media (max-width: 780px) { .wd-fuss-grid { grid-template-columns: 1fr; gap: 1.5rem; } }
.wd-fuss h4 { font-size: .78rem; letter-spacing: .14em; text-transform: uppercase; color: var(--wd-blau); margin-bottom: .8rem; }
/* Die Spaltenueberschriften der Fusszeile sind jetzt h2 statt h4.
Grund: Sie folgten im Dokument auf die h2 der Seite -- ein Sprung von
h2 direkt auf h4. Wer sich mit einem Screenreader durch die
Ueberschriften bewegt, hoert dabei eine Gliederung, die es nicht gibt,
und vermutet uebersprungene Abschnitte (WCAG 1.3.1). Auf allen 13
Seiten in allen 5 Sprachen -- also 42 Stellen.
Die Klasse haelt die Optik unveraendert: Die Ebene sagt etwas ueber
die STRUKTUR, die Groesse ueber die GEWICHTUNG. Das sind zwei
verschiedene Dinge, und man soll das eine nicht ueber das andere
erzwingen muessen. */
.wd-fuss .wd-fuss-h, .wd-fuss h4 {
font-size: .78rem; letter-spacing: .14em; text-transform: uppercase;
color: var(--wd-blau); margin: 0 0 .8rem; font-weight: 700;
font-family: var(--font-head);
}
.wd-fuss ul li { margin-bottom: .1rem; }
/* Fußzeilenlinks waren nur so hoch wie ihre Zeile (20px) und damit auf dem
Handy praktisch nicht sicher zu treffen — sie liegen zudem dicht
@@ -1092,7 +1108,7 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
.po-vorschau-etikett {
position: absolute; top: .7rem; right: .8rem;
font-size: .66rem; letter-spacing: .14em; text-transform: uppercase;
font-size: 0.75rem; letter-spacing: .14em; text-transform: uppercase;
font-weight: 800; color: var(--wd-text-soft);
padding: .2rem .55rem; border-radius: 999px;
background: rgba(5,7,11,.7); border: 1px solid var(--wd-rand);
@@ -1123,7 +1139,7 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
.po-vorschau-zeile { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; font-size: .82rem; color: var(--wd-text-soft); }
.po-vorschau-marke {
display: inline-flex; align-items: center; gap: .3rem;
padding: .18rem .55rem; border-radius: 999px; font-size: .74rem; font-weight: 700;
padding: .18rem .55rem; border-radius: 999px; font-size: 0.8rem; font-weight: 700;
background: rgba(127,208,232,.12); color: var(--wd-blau);
}
.po-vorschau-schritt { margin: .7rem 0 0; font-size: .88rem; color: var(--wd-text-mid); opacity: .8; }
@@ -1453,7 +1469,7 @@ a.wd-karte:hover, .wd-karte--klickbar:hover {
.po-aufg-gruppe { margin-bottom: 1.1rem; }
.po-aufg-kopf {
display: flex; align-items: baseline; gap: .5rem;
font-size: .74rem; letter-spacing: .13em; text-transform: uppercase;
font-size: 0.8rem; letter-spacing: .13em; text-transform: uppercase;
margin-bottom: .45rem; padding-bottom: .35rem;
border-bottom: 1px solid var(--wd-rand);
}
@@ -1515,7 +1531,7 @@ button.po-aufg-haken:hover { border-color: var(--wd-gold); background: rgba(232,
.po-aufg-marke {
display: inline-block; white-space: nowrap;
font-size: .68rem; letter-spacing: .05em; text-transform: uppercase;
font-size: 0.75rem; letter-spacing: .05em; text-transform: uppercase;
padding: .14rem .45rem; border-radius: 999px;
}
.po-aufg-marke--ok { background: rgba(111,214,163,.13); color: var(--wd-ok); }
@@ -1523,7 +1539,7 @@ button.po-aufg-haken:hover { border-color: var(--wd-gold); background: rgba(232,
.po-aufg-marke--du { background: rgba(232,200,119,.16); color: var(--wd-gold); }
.po-aufg-nicht {
display: inline-block; margin-left: .4rem;
font-size: .68rem; letter-spacing: .05em; text-transform: uppercase;
font-size: 0.75rem; letter-spacing: .05em; text-transform: uppercase;
padding: .12rem .4rem; border-radius: 999px;
background: rgba(255,157,157,.12); color: var(--wd-fehler);
}
@@ -1677,7 +1693,9 @@ button.po-aufg-haken:hover { border-color: var(--wd-gold); background: rgba(232,
.rl-text ul { margin: 0 0 .9rem; padding-left: 1.2rem; }
.rl-text li { margin-bottom: .35rem; }
.rl-text code {
font-size: .86em; padding: .1rem .35rem; border-radius: 5px;
/* min() statt eines reinen em-Werts: In einem ohnehin kleineren Absatz
rutschte der Code sonst auf 11,2px -- unter die Lesbarkeitsgrenze. */
font-size: max(.78rem, .86em); padding: .1rem .35rem; border-radius: 5px;
background: rgba(127,208,232,.09); color: var(--wd-blau);
}
+2 -2
View File
@@ -488,11 +488,11 @@
'<p class="wd-klein wd-leise" data-i18n="fuss_claim">' + t("fuss_claim") + "</p>" +
"</div>" +
"<div>" +
'<h4 data-i18n="fuss_bereiche">' + t("fuss_bereiche") + "</h4>" +
'<h2 class="wd-fuss-h" data-i18n="fuss_bereiche">' + t("fuss_bereiche") + "</h2>" +
"<ul>" + bereicheHtml + "</ul>" +
"</div>" +
"<div>" +
'<h4 data-i18n="fuss_zugang">' + t("fuss_zugang") + "</h4>" +
'<h2 class="wd-fuss-h" data-i18n="fuss_zugang">' + t("fuss_zugang") + "</h2>" +
"<ul>" +
'<li><a href="/webdesign/portal.html" data-i18n="fuss_portal">' + t("fuss_portal") + "</a></li>" +
'<li><a href="/webdesign/verwaltung.html" data-i18n="fuss_verwaltung">' + t("fuss_verwaltung") + "</a></li>" +
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+457
View File
@@ -0,0 +1,457 @@
/* =====================================================================
pruef-design.mjs — Gestaltungsprüfung über ALLE Seiten und Sprachen
"Sieht gut aus" ist Geschmack und lässt sich nicht prüfen. Was sich
prüfen lässt, ist handwerkliche Qualität — und die ist bei einer
Webdesign-Seite zugleich das Verkaufsargument: Wer selbst schludert,
kann schlecht Sorgfalt verkaufen.
Gemessen wird gegen WCAG 2.2 AA, den Standard, auf den auch das
Barrierefreiheitsstärkungsgesetz verweist:
1.4.3 Text mindestens 4,5:1 Kontrast (grosser Text 3:1)
1.4.11 Bedienelemente und Umrandungen mindestens 3:1
2.4.7 Tastaturfokus muss SICHTBAR sein
2.5.8 Klickflächen mindestens 24x24 CSS-Pixel
1.3.1 Überschriftenebenen ohne Sprünge
3.1.1 lang-Attribut passt zur angezeigten Sprache
1.1.1 Bilder haben eine Textalternative
Alles davon rechnet der Browser selbst aus — es wird nichts geschätzt.
===================================================================== */
import { chromium } from "playwright";
import { createServer } from "node:http";
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { join, extname } from "node:path";
const W = process.cwd();
const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript",
".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml",
".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" };
const srv = createServer((q, r) => {
const p = join(W, decodeURIComponent(q.url.split("?")[0]));
if (existsSync(p) && !p.endsWith("/")) {
r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" });
return r.end(readFileSync(p));
}
r.writeHead(404).end("");
});
await new Promise((r) => srv.listen(4329, r));
const SEITEN = readdirSync(join(W, "webdesign")).filter((f) => f.endsWith(".html"));
const SPRACHEN = ["de", "de-CH", "en", "fr", "pt"];
const funde = [];
function fund(seite, sprache, art, text) {
funde.push({ seite, sprache, art, text });
}
/* Die Messfunktion läuft IM Browser — nur dort kennt man die
tatsächlich berechneten Farben, Grössen und Schriftarten. */
const MESSEN = () => {
/* ---- Farbwerkzeuge: WCAG-Kontrast exakt nach Formel ---- */
function zuRgb(farbe) {
const m = farbe.match(/rgba?\(([^)]+)\)/);
if (!m) return null;
const t = m[1].split(",").map((x) => parseFloat(x.trim()));
return { r: t[0], g: t[1], b: t[2], a: t.length > 3 ? t[3] : 1 };
}
function ueberlagern(vorn, hinten) {
if (vorn.a >= 1) return vorn;
return {
r: vorn.r * vorn.a + hinten.r * (1 - vorn.a),
g: vorn.g * vorn.a + hinten.g * (1 - vorn.a),
b: vorn.b * vorn.a + hinten.b * (1 - vorn.a),
a: 1,
};
}
function leuchtdichte(c) {
const f = [c.r, c.g, c.b].map((v) => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return 0.2126 * f[0] + 0.7152 * f[1] + 0.0722 * f[2];
}
function kontrast(a, b) {
const l1 = leuchtdichte(a), l2 = leuchtdichte(b);
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
}
/* Der tatsächliche Hintergrund: nach oben durchsuchen, bis eine
deckende Fläche kommt, und halbdurchsichtige Schichten überlagern.
Nur die eigene Hintergrundfarbe zu nehmen wäre falsch — fast alle
Karten hier sind halbtransparent. */
/* Nur die OBERSTE Hintergrundebene zaehlt.
Mehrere Ebenen werden in CSS in der angegebenen Reihenfolge
gestapelt -- die ERSTE liegt oben. Die Karten hier nutzen genau das
fuer ihren Verlaufsrahmen:
background: linear-gradient(...Kartenflaeche...) padding-box,
linear-gradient(...heller Rahmen...) border-box;
Der helle Rahmenverlauf ist im Textbereich gar nicht sichtbar, er
scheint nur aussen am Rand durch. Wer alle Ebenen zusammenrechnet,
haelt den Text faelschlich fuer hellblau hinterlegt -- und genau das
hat 820 Fehlalarme erzeugt, mit Werten wie "3,45:1" fuer Text, der
in Wahrheit bei ueber 7:1 liegt.
Trennt an Kommas der obersten Ebene, ohne die Kommas INNERHALB der
Klammern eines Verlaufs zu zerschneiden. */
function ersteEbene(bild) {
let tiefe = 0;
for (let i = 0; i < bild.length; i++) {
const z = bild[i];
if (z === "(") tiefe++;
else if (z === ")") tiefe--;
else if (z === "," && tiefe === 0) return bild.slice(0, i);
}
return bild;
}
function echterHintergrund(el) {
/* Sammelt die Schichten von innen nach aussen. Eine Schicht ist
entweder eine Farbe oder ein Verlauf (dann als Liste seiner
Farbstopps).
Der erste Entwurf verwarf Verlaufsstopps mit wenig Deckkraft --
genau die, aus denen die Hinweiskaesten bestehen (9 % und 4 %
Gold). Uebrig blieb "nicht messbar" fuer 72 Textstellen. Richtig
ist, sie wie jede andere halbdurchsichtige Schicht auf den
Untergrund zu rechnen. */
const schichten = [];
let n = el;
let grund = { r: 5, g: 7, b: 11, a: 1 };
while (n && n !== document.documentElement) {
const st = getComputedStyle(n);
if (st.backgroundImage && st.backgroundImage !== "none") {
const oben = ersteEbene(st.backgroundImage);
const stopps = (oben.match(/rgba?\([^)]+\)/g) || []).map(zuRgb).filter(Boolean);
if (stopps.length) schichten.push({ verlauf: stopps });
}
const bg = zuRgb(st.backgroundColor);
if (bg && bg.a > 0) {
schichten.push({ farbe: bg });
if (bg.a >= 1) { grund = bg; schichten.pop(); break; }
}
n = n.parentElement;
}
/* Von aussen nach innen zusammenrechnen. Bei einem Verlauf werden
BEIDE Extremstopps weiterverfolgt -- am Ende zaehlt der
unguenstigste Fall. So bleibt die Aussage streng, ohne zu raten. */
let kandidaten = [grund];
for (let i = schichten.length - 1; i >= 0; i--) {
const sch = schichten[i];
if (sch.farbe) {
kandidaten = kandidaten.map((k) => ueberlagern(sch.farbe, k));
} else {
const naechste = [];
for (const k of kandidaten) {
for (const stopp of sch.verlauf) naechste.push(ueberlagern(stopp, k));
}
/* Nur hellsten und dunkelsten weiterfuehren, sonst waechst die
Zahl der Kombinationen mit jeder Schicht. */
naechste.sort((a, b) => leuchtdichte(a) - leuchtdichte(b));
kandidaten = naechste.length > 2 ? [naechste[0], naechste[naechste.length - 1]] : naechste;
}
}
return kandidaten.length === 1 ? kandidaten[0] : { verlauf: kandidaten };
}
/* Absichtlich vom Bildschirm geschobene Elemente (Sprunglink,
versteckte Auswahlfelder hinter Kacheln) sind KEINE Fehler. Sie
erscheinen nur im Zugriffsbaum bzw. beim Tabben und sind dann
sichtbar. */
function abgeschoben(el) {
const st = getComputedStyle(el);
if (parseFloat(st.opacity) === 0) return true;
if (st.clipPath && st.clipPath !== "none" && st.clipPath.indexOf("inset(50%") === 0) return true;
if (st.clip && st.clip !== "auto") return true;
const r = el.getBoundingClientRect();
if (r.bottom < 0 || r.right < 0) return true;
if (r.width <= 2 && r.height <= 2) return true;
return false;
}
const ergebnis = { kontrast: [], ziele: [], fokus: [], ueberschriften: [], bilder: [],
labels: [], lang: null, klein: [] };
/* ---- 1.4.3 Textkontrast ---- */
const textKnoten = [];
const lauf = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
let k;
while ((k = lauf.nextNode())) {
const t = (k.nodeValue || "").trim();
if (t.length < 2) continue;
const el = k.parentElement;
if (!el || el.closest("script, style, noscript")) continue;
const st = getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden" || parseFloat(st.opacity) === 0) continue;
const r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) continue;
if (!textKnoten.includes(el)) textKnoten.push(el);
}
ergebnis.verlauf = [];
for (const el of textKnoten) {
if (abgeschoben(el)) continue;
const st = getComputedStyle(el);
/* VERLAUFSSCHRIFT: Bei background-clip:text ist die Schriftfarbe
durchsichtig und der Verlauf IST der Text. Die Schriftfarbe mit
dem Hintergrund zu vergleichen ergibt zwangslaeufig 1:1 -- das
war der letzte verbliebene Fehlalarm. Stattdessen werden die
Farbstopps des Verlaufs gegen den Seitenhintergrund gerechnet. */
const verlaufsSchrift = (st.webkitBackgroundClip === "text" || st.backgroundClip === "text");
if (verlaufsSchrift) {
const stopps = (ersteEbene(st.backgroundImage).match(/rgba?\([^)]+\)/g) || []).map(zuRgb).filter(Boolean);
const grund = { r: 5, g: 7, b: 11, a: 1 };
const werte = stopps.map((c) => kontrast(ueberlagern(c, grund), grund));
const schlechtester = werte.length ? Math.min.apply(null, werte) : null;
const gr = parseFloat(st.fontSize);
const noetigV = (gr >= 24 || (parseInt(st.fontWeight,10) >= 700 && gr >= 18.66)) ? 3 : 4.5;
if (schlechtester !== null && schlechtester < noetigV) {
ergebnis.kontrast.push({ text: (el.textContent||"").trim().slice(0,45),
wert: Math.round(schlechtester*100)/100, noetig: noetigV,
groesse: Math.round(gr*10)/10, grund: "Verlaufsschrift",
klasse: (el.className||"").toString().slice(0,40) });
}
continue;
}
const vg = zuRgb(st.color);
if (!vg) continue;
const hg = echterHintergrund(el);
if (!hg) {
/* Verlauf ohne lesbare Farbstopps -- selten, dann von Hand. */
ergebnis.verlauf.push({ text: (el.textContent || "").trim().slice(0, 40),
klasse: (el.className || "").toString().slice(0, 40) });
continue;
}
let kw, hgBeschreibung;
if (hg.verlauf) {
/* Ungünstigster Stopp des Verlaufs. */
kw = Math.min.apply(null, hg.verlauf.map((c) => kontrast(ueberlagern(vg, c), c)));
hgBeschreibung = "Verlauf";
} else {
kw = kontrast(ueberlagern(vg, hg), hg);
hgBeschreibung = "Flaeche";
}
const groesse = parseFloat(st.fontSize);
const fett = parseInt(st.fontWeight, 10) >= 700;
/* "Grosser Text" nach WCAG: ab 18,66px fett oder ab 24px normal. */
const gross = groesse >= 24 || (fett && groesse >= 18.66);
const noetig = gross ? 3 : 4.5;
if (kw < noetig) {
ergebnis.kontrast.push({
text: (el.textContent || "").trim().slice(0, 45),
wert: Math.round(kw * 100) / 100, noetig, groesse: Math.round(groesse * 10) / 10,
grund: hgBeschreibung,
klasse: (el.className || "").toString().slice(0, 40),
});
}
/* Schriftgrössen unter 12px sind auf dem Handy kaum lesbar. */
if (groesse < 12) {
ergebnis.klein.push({ groesse: Math.round(groesse * 10) / 10,
klasse: (el.className || "").toString().slice(0,40) || el.tagName.toLowerCase(),
text: (el.textContent || "").trim().slice(0, 25) });
}
}
/* ---- 2.5.8 Klickflächen ---- */
document.querySelectorAll("a, button, input, select, textarea, [role=button]").forEach((el) => {
const st = getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden") return;
const r = el.getBoundingClientRect();
if (r.width < 1 || r.height < 1) return;
/* Verlinkte Wörter mitten im Fliesstext sind ausgenommen — die Regel
zielt auf eigenständige Bedienelemente. */
if (el.tagName === "A" && st.display === "inline") return;
/* Auswahlfelder, die absichtlich hinter einer Kachel liegen: Die
Kachel IST die Klickflaeche, das Feld selbst soll winzig sein.
Beim ersten Lauf wurden dadurch sechs einwandfreie Kacheln
gemeldet. */
if (abgeschoben(el)) return;
if (r.width < 24 || r.height < 24) {
ergebnis.ziele.push({ tag: el.tagName.toLowerCase(),
text: (el.textContent || el.value || "").trim().slice(0, 28),
b: Math.round(r.width), h: Math.round(r.height),
klasse: (el.className || "").toString().slice(0, 40) });
}
});
/* ---- 1.3.1 Überschriftenebenen ---- */
/* getClientRects() statt display: Eine Ueberschrift IN einem
ausgeblendeten Abschnitt hat selbst weiterhin display:block --
dadurch wurden im Portal drei h1 gezaehlt, obwohl immer nur eine
sichtbar ist (Anmeldung / Uebersicht / Projektansicht). */
const hs = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6"))
.filter((h) => h.getClientRects().length > 0)
.map((h) => ({ n: parseInt(h.tagName[1], 10), t: (h.textContent || "").trim().slice(0, 35) }));
ergebnis.ueberschriften = { anzahlH1: hs.filter((h) => h.n === 1).length, spruenge: [] };
for (let i = 1; i < hs.length; i++) {
if (hs[i].n - hs[i - 1].n > 1) {
ergebnis.ueberschriften.spruenge.push(`h${hs[i - 1].n} -> h${hs[i].n} bei "${hs[i].t}"`);
}
}
/* ---- 1.1.1 Bilder ---- */
document.querySelectorAll("img").forEach((img) => {
if (!img.hasAttribute("alt")) {
ergebnis.bilder.push((img.getAttribute("src") || "").split("/").pop());
}
});
/* ---- Formularfelder brauchen eine Beschriftung ---- */
document.querySelectorAll("input, select, textarea").forEach((el) => {
if (el.type === "hidden") return;
const hat = (el.id && document.querySelector(`label[for="${CSS.escape(el.id)}"]`))
|| el.closest("label")
|| el.getAttribute("aria-label")
|| el.getAttribute("aria-labelledby");
if (!hat) ergebnis.labels.push(el.id || el.name || el.type);
});
/* ---- 3.1.1 lang-Attribut ---- */
ergebnis.lang = document.documentElement.getAttribute("lang");
return ergebnis;
};
const browser = await chromium.launch();
const seite = await browser.newPage({ viewport: { width: 390, height: 844 } });
await seite.route("**/webdesign/admin/**", (r) => r.fulfill({ json: { ok: true, anfragen: [], kunden: [], projekte: [], verlaeufe: [], widerrufe: [] } }));
await seite.route("**/webdesign/portal/**", (r) => r.fulfill({ json: { ok: true } }));
await seite.route("**/webdesign/api-ausweis", (r) => r.fulfill({ json: {} }));
await seite.addInitScript(() => { sessionStorage.setItem("wd-sitzung-aktiv", "1"); });
console.log("Messung läuft — " + SEITEN.length + " Seiten x " + SPRACHEN.length + " Sprachen, Handy-Breite 390px\n");
let fokusGeprueft = false;
for (const datei of SEITEN) {
for (const sprache of SPRACHEN) {
await seite.addInitScript(`localStorage.setItem("dogi-sprache", ${JSON.stringify(sprache)})`);
await seite.goto("http://127.0.0.1:4329/webdesign/" + datei);
await seite.waitForTimeout(320);
const m = await seite.evaluate(MESSEN);
for (const k of m.kontrast) {
fund(datei, sprache, "kontrast",
`${k.wert}:1 statt ${k.noetig}:1 — ${k.groesse}px auf ${k.grund} "${k.text}" [${k.klasse}]`);
}
for (const z of m.ziele) {
fund(datei, sprache, "klickflaeche",
`${z.tag} ${z.b}x${z.h}px "${z.text}" [${z.klasse}]`);
}
for (const s of m.klein) fund(datei, sprache, "schriftgroesse", `${s.groesse}px [${s.klasse}]`);
for (const s of m.ueberschriften.spruenge) fund(datei, sprache, "ueberschrift", s);
if (m.ueberschriften.anzahlH1 !== 1) {
fund(datei, sprache, "ueberschrift", `${m.ueberschriften.anzahlH1} h1 statt genau einer`);
}
for (const v of m.verlauf) fund(datei, sprache, "verlauf-manuell", `"${v.text}" [${v.klasse}]`);
for (const b of m.bilder) fund(datei, sprache, "bild-alt", b);
for (const l of m.labels) fund(datei, sprache, "feld-ohne-label", l);
/* lang muss der angezeigten Sprache folgen — sonst liest der
Screenreader französischen Text mit deutscher Aussprache vor. */
const erwartet = sprache === "de-CH" ? ["de-CH", "de"] : [sprache];
if (!erwartet.includes(m.lang)) {
fund(datei, sprache, "lang-attribut", `lang="${m.lang}" bei Sprache ${sprache}`);
}
}
/* ---- 2.4.7 Fokus: einmal je Seite, auf Deutsch ---- */
await seite.addInitScript(`localStorage.setItem("dogi-sprache", "de")`);
await seite.goto("http://127.0.0.1:4329/webdesign/" + datei);
await seite.waitForTimeout(300);
const ohneFokus = await seite.evaluate(() => {
const raus = [];
/* ÜBERBLENDUNGEN ABSCHALTEN, bevor gemessen wird.
Ohne das entsteht ein Fehlalarm, den man nicht erraet:
getComputedStyle liefert waehrend einer laufenden Ueberblendung
den ZWISCHENWERT. Nach blur() verblasst der Fokusring ueber
0,16 s -- misst man sofort, sieht man ihn noch, und "vorher" ist
identisch mit "nachher". Das Codefeld der Verwaltung wurde
dadurch als "ohne sichtbaren Fokus" gemeldet, obwohl sein Ring
einwandfrei ist. Geprueft mit el.matches(":focus") === false bei
gleichzeitig noch sichtbarem Schatten.
Die Regel wird nur fuer die Messung gesetzt und beeinflusst die
Seite selbst nicht. */
const aus = document.createElement("style");
aus.textContent = "*,*::before,*::after{transition:none !important;animation:none !important}";
document.head.appendChild(aus);
const bedien = Array.from(document.querySelectorAll("a[href], button, input, select, textarea"))
.filter((el) => {
const st = getComputedStyle(el);
const r = el.getBoundingClientRect();
if (parseFloat(st.opacity) === 0) return false;
if (r.width <= 2 && r.height <= 2) return false;
return st.display !== "none" && st.visibility !== "hidden" && r.width > 0 && r.height > 0;
}).slice(0, 25);
for (const el of bedien) {
/* ERST loslassen, dann messen. Ohne das entsteht ein Fehlalarm:
Die Anmeldemaske der Verwaltung setzt den Fokus beim Laden
selbst ins Codefeld. Der "Vorher"-Zustand war damit bereits der
fokussierte -- es aenderte sich nichts, und der Test meldete
einen fehlenden Fokusrahmen, den es nie gab. */
el.blur();
const vorher = getComputedStyle(el).outlineStyle + "|" + getComputedStyle(el).boxShadow;
el.focus();
const st = getComputedStyle(el);
const nachher = st.outlineStyle + "|" + st.boxShadow;
const sichtbar = (st.outlineStyle !== "none" && parseFloat(st.outlineWidth) > 0)
|| nachher !== vorher;
if (!sichtbar) {
raus.push(el.tagName.toLowerCase() + '.' + (el.className||'').toString().slice(0,35) + ' id=' + (el.id||'-') + ' typ=' + (el.type||'-'));
}
el.blur();
}
aus.remove();
return raus;
});
for (const f of ohneFokus) fund(datei, "de", "fokus-unsichtbar", f);
fokusGeprueft = true;
}
await browser.close();
srv.close();
/* ---- Bericht ---- */
const nachArt = {};
for (const f of funde) (nachArt[f.art] = nachArt[f.art] || []).push(f);
const ARTEN = {
kontrast: "WCAG 1.4.3 — Textkontrast zu niedrig",
klickflaeche: "WCAG 2.5.8 — Klickfläche unter 24x24px",
"fokus-unsichtbar": "WCAG 2.4.7 — Tastaturfokus nicht sichtbar",
ueberschrift: "WCAG 1.3.1 — Überschriftenstruktur",
"bild-alt": "WCAG 1.1.1 — Bild ohne Textalternative",
"feld-ohne-label": "WCAG 3.3.2 — Formularfeld ohne Beschriftung",
"lang-attribut": "WCAG 3.1.1 — lang passt nicht zur Sprache",
schriftgroesse: "Schrift unter 12px",
"verlauf-manuell": "Text auf Farbverlauf — von Hand zu pruefen (kein Fehler)",
};
for (const [art, titel] of Object.entries(ARTEN)) {
const liste = nachArt[art] || [];
if (!liste.length) { console.log("ok " + titel + " — nichts gefunden"); continue; }
/* Gleiche Fundstelle auf mehreren Sprachen nur EINMAL zeigen, sonst
erschlägt einen die Liste und man übersieht das Wesentliche. */
const einmalig = new Map();
for (const f of liste) {
const s = f.seite + " :: " + f.text;
if (!einmalig.has(s)) einmalig.set(s, []);
einmalig.get(s).push(f.sprache);
}
console.log(`\nFEHL ${titel} — ${einmalig.size} Stellen`);
let n = 0;
for (const [s, sprachen] of einmalig) {
if (n++ >= 12) { console.log(` … und ${einmalig.size - 12} weitere`); break; }
const [seite, ...rest] = s.split(" :: ");
console.log(` ${seite} ${rest.join(" :: ")}${sprachen.length < 5 ? " [" + sprachen.join(",") + "]" : ""}`);
}
}
const gesamt = new Set(funde.map((f) => f.seite + f.art + f.text)).size;
console.log(`\n===== ${gesamt} eindeutige Fundstellen =====`);
process.exitCode = gesamt ? 1 : 0;
Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 494 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

+5 -5
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
/* Zeitstrahl: die Linie verbindet die Nummernkreise. Sie liegt hinter
den Kreisen (z-index) und endet beim letzten Schritt, damit sie nicht
@@ -46,7 +46,7 @@
.abl-karte h2 { font-size: clamp(1.25rem, 3vw, 1.6rem); margin-bottom: .9em; }
.abl-rollen { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
@media (max-width: 820px) { .abl-rollen { grid-template-columns: 1fr; gap: .8rem; } }
.abl-rolle h3 { font-size: .72rem; letter-spacing: .13em; text-transform: uppercase; margin-bottom: .35em; color: var(--wd-blau); }
.abl-rolle h3 { font-size: 0.8rem; letter-spacing: .13em; text-transform: uppercase; margin-bottom: .35em; color: var(--wd-blau); }
.abl-schritt--lila .abl-rolle h3 { color: var(--wd-lila); }
/* Die dritte Spalte ist das Ergebnis — bekommt einen leichten Rahmen,
damit sie sich von "wer macht was" abhebt. */
@@ -183,7 +183,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-ablauf.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-ablauf.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
</body>
</html>
+6 -6
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
/* ---------- Schrittanzeige ----------
Zeigt alle vier Schritte gleichzeitig statt nur "3 von 4". Man sieht
@@ -34,7 +34,7 @@
.af-schritt-marke.af-erledigt .af-schritt-balken span,
.af-schritt-marke.af-aktiv .af-schritt-balken span { width: 100%; }
.af-schritt-name {
font-size: .74rem; font-weight: 700; letter-spacing: .04em;
font-size: 0.8rem; font-weight: 700; letter-spacing: .04em;
color: var(--wd-text-soft);
}
.af-schritt-marke.af-aktiv .af-schritt-name { color: var(--wd-blau); }
@@ -418,8 +418,8 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-anfrage.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/wd-anfrage.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-anfrage.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
<script src="/assets/js/wd-anfrage.js?v=20260823wd8"></script>
</body>
</html>
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
.fq-thema { scroll-margin-top: 90px; }
.fq-thema + .fq-thema { margin-top: clamp(2rem, 4vw, 3rem); }
@@ -195,7 +195,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-faq.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-faq.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
</body>
</html>
+5 -5
View File
@@ -16,8 +16,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
/* ---------- Nur auf der Startseite gebraucht ---------- */
@@ -66,7 +66,7 @@
.wd-projekt-text h3 { margin-bottom: .3em; }
.wd-projekt-zeile { margin: .9rem 0 0; }
.wd-projekt-zeile dt {
font-size: .72rem; letter-spacing: .12em; text-transform: uppercase;
font-size: 0.8rem; letter-spacing: .12em; text-transform: uppercase;
font-weight: 800; color: var(--wd-blau); margin-bottom: .15rem;
}
.wd-projekt-zeile dd { margin: 0 0 .8rem; color: var(--wd-text-mid); font-size: .98rem; }
@@ -407,8 +407,8 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-index.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-index.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
<script>
/* ---------------------------------------------------------------------
Marken-Intro (Masterplan S.3, Schritt 02: "Nach erfolgreichem Code
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
/* Sprungmarken.
Frueher eine waagerecht wischbare Leiste. Rueckmeldung 22.08.2026:
@@ -331,7 +331,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-leistungen.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-leistungen.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
</body>
</html>
+6 -6
View File
@@ -12,8 +12,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
.po-login { max-width: 440px; margin: clamp(1.5rem,6vh,4rem) auto; }
@@ -56,7 +56,7 @@
.po-nachricht { padding: .8rem 1rem; border-radius: 14px; margin-bottom: .6rem; max-width: 88%; }
.po-nachricht--ich { background: rgba(127,208,232,.1); border: 1px solid var(--wd-rand-stark); margin-left: auto; }
.po-nachricht--du { background: rgba(16,26,41,.7); border: 1px solid var(--wd-rand); }
.po-nachricht-kopf { font-size: .74rem; color: var(--wd-text-soft); margin-bottom: .25rem; }
.po-nachricht-kopf { font-size: 0.8rem; color: var(--wd-text-soft); margin-bottom: .25rem; }
.po-nachricht p { margin: 0; font-size: .95rem; white-space: pre-wrap; color: var(--wd-text); }
.po-idee { padding: 1rem; border-radius: 14px; border: 1px solid var(--wd-rand); background: rgba(16,26,41,.5); margin-bottom: .7rem; }
@@ -134,8 +134,8 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-portal.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/wd-portal.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-portal.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
<script src="/assets/js/wd-portal.js?v=20260823wd8"></script>
</body>
</html>
+5 -5
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
.pfo-projekt { overflow: hidden; padding: 0; margin-bottom: clamp(1.4rem, 3vw, 2.2rem); }
.pfo-bild {
@@ -26,7 +26,7 @@
.pfo-kopf h2 { margin: 0; font-size: clamp(1.4rem, 3.2vw, 1.9rem); }
.pfo-block { margin-bottom: 1.1rem; }
.pfo-block h3 { font-size: .74rem; letter-spacing: .13em; text-transform: uppercase; color: var(--wd-blau); margin-bottom: .35em; }
.pfo-block h3 { font-size: 0.8rem; letter-spacing: .13em; text-transform: uppercase; color: var(--wd-blau); margin-bottom: .35em; }
.wd-karte--lila .pfo-block h3 { color: var(--wd-lila); }
.pfo-block p { margin: 0; }
@@ -192,7 +192,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-portfolio.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-portfolio.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
</body>
</html>
+5 -5
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
.pr-preis-gross { font-family: var(--font-head); font-weight: 800; color: var(--wd-blau); white-space: nowrap; }
@@ -34,7 +34,7 @@
.pr-tabelle td { border: none; padding: .45rem 1rem; }
.pr-tabelle td::before {
content: attr(data-titel);
display: block; font-size: .68rem; letter-spacing: .12em; text-transform: uppercase;
display: block; font-size: 0.75rem; letter-spacing: .12em; text-transform: uppercase;
font-weight: 800; color: var(--wd-blau); margin-bottom: .1rem;
}
/* Die erste Zelle ist der Paketname — der braucht keine Beschriftung. */
@@ -196,7 +196,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-preise.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-preise.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
</body>
</html>
+3 -3
View File
@@ -8,8 +8,8 @@
<link rel="icon" type="image/png" href="/assets/img/favicon.png" />
<link rel="manifest" href="/webdesign/app.webmanifest" />
<meta name="theme-color" content="#05070b" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
</head>
<body class="wd">
<div id="wd-kopf"></div>
@@ -400,6 +400,6 @@
<div id="wd-fuss"></div>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
</body>
</html>
+1 -1
View File
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v7";
const CACHE_NAME = "dogfather-webdesign-v8";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+4 -4
View File
@@ -13,8 +13,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
.ue-kopf { display: grid; grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); gap: clamp(1.5rem, 4vw, 3rem); align-items: center; }
/* Auf dem Handy zuerst der Text, dann das Bild: sonst schiebt ein
@@ -127,7 +127,7 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-ueber.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-ueber.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
</body>
</html>
+13 -13
View File
@@ -11,8 +11,8 @@
<link rel="apple-touch-icon" href="/assets/img/wd-app-180.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="DF Verwaltung" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
/* =====================================================================
Verwaltungsbereich — bewusst NUR AUF DEUTSCH.
@@ -73,7 +73,7 @@
color: var(--wd-blau); font-variant-numeric: tabular-nums;
}
.vw-zahl-name { font-size: .78rem; letter-spacing: .03em; color: var(--wd-text-soft); }
.vw-zahl-zusatz { font-size: .74rem; color: var(--wd-text-soft); margin-top: .3rem; display: block; }
.vw-zahl-zusatz { font-size: 0.8rem; color: var(--wd-text-soft); margin-top: .3rem; display: block; }
/* Null: alles gedaempft, damit es nicht um Aufmerksamkeit buhlt. */
.vw-zahl--null { opacity: .5; }
@@ -101,7 +101,7 @@
.vw-filter-knopf:hover { border-color: var(--wd-rand-stark); color: var(--wd-text); }
.vw-filter-knopf.vw-an { background: rgba(127,208,232,.13); border-color: var(--wd-blau); color: var(--wd-blau); }
.vw-filter-zahl {
font-size: .74rem; padding: .1rem .4rem; border-radius: 6px;
font-size: 0.8rem; padding: .1rem .4rem; border-radius: 6px;
background: rgba(127,208,232,.14); color: var(--wd-blau-hell);
}
.vw-suche { margin-left: auto; min-width: 200px; flex: 0 1 260px; }
@@ -129,7 +129,7 @@
}
/* Statusfarben — Gold nur da, wo wirklich etwas zu tun ist. */
.vw-status { display: inline-block; padding: .22rem .6rem; border-radius: 999px; font-size: .74rem; font-weight: 700; }
.vw-status { display: inline-block; padding: .22rem .6rem; border-radius: 999px; font-size: 0.8rem; font-weight: 700; }
.vw-status--neu { background: rgba(232,200,119,.16); color: var(--wd-gold); }
.vw-status--pruefung { background: rgba(127,208,232,.14); color: var(--wd-blau); }
.vw-status--rueckfrage { background: rgba(183,157,255,.16); color: var(--wd-lila); }
@@ -247,7 +247,7 @@
min-width: 0;
}
.vw-blick-was {
display: block; font-size: .68rem; letter-spacing: .11em;
display: block; font-size: 0.75rem; letter-spacing: .11em;
text-transform: uppercase; color: var(--wd-text-soft); margin-bottom: .22rem;
}
.vw-blick-wert {
@@ -305,7 +305,7 @@
.vw-abschnitt { margin-bottom: 1.4rem; }
.vw-abschnitt > h3 {
font-size: .74rem; letter-spacing: .13em; text-transform: uppercase;
font-size: 0.8rem; letter-spacing: .13em; text-transform: uppercase;
color: var(--wd-blau); margin-bottom: .6rem;
padding-bottom: .4rem; border-bottom: 1px solid var(--wd-rand);
}
@@ -361,7 +361,7 @@
.vw-karte-marken { display: flex; flex-wrap: wrap; gap: .35rem; margin-top: .4rem; }
.vw-mini {
font-size: .74rem; font-weight: 600; padding: .15rem .5rem;
font-size: 0.8rem; font-weight: 600; padding: .15rem .5rem;
border-radius: 6px; background: rgba(127,208,232,.09);
color: var(--wd-text-mid); border: 1px solid var(--wd-rand);
white-space: nowrap;
@@ -459,7 +459,7 @@
.vw-aufg-gruppe { margin-bottom: 1.1rem; }
.vw-aufg-kopf {
display: flex; align-items: baseline; gap: .5rem;
font-size: .74rem; letter-spacing: .13em; text-transform: uppercase;
font-size: 0.8rem; letter-spacing: .13em; text-transform: uppercase;
margin-bottom: .5rem; padding-bottom: .35rem;
border-bottom: 1px solid var(--wd-rand);
}
@@ -515,14 +515,14 @@
.vw-aufg--kunde { background: rgba(232,200,119,.055); }
.vw-aufg--kunde:hover { background: rgba(232,200,119,.1); }
.vw-aufg-wer {
font-size: .68rem; letter-spacing: .05em; text-transform: uppercase;
font-size: 0.75rem; letter-spacing: .05em; text-transform: uppercase;
padding: .12rem .4rem; border-radius: 999px; white-space: nowrap;
background: rgba(232,200,119,.15); color: var(--wd-gold);
}
.vw-aufg-nicht {
display: inline-block; margin-left: .35rem;
font-size: .68rem; letter-spacing: .05em; text-transform: uppercase;
font-size: 0.75rem; letter-spacing: .05em; text-transform: uppercase;
padding: .12rem .4rem; border-radius: 999px;
background: rgba(255,157,157,.12); color: var(--wd-fehler);
}
@@ -556,7 +556,7 @@
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
overflow: hidden;
}
.vw-pf-zeit { font-size: .74rem; color: var(--wd-text-soft); margin-top: .3rem; }
.vw-pf-zeit { font-size: 0.8rem; color: var(--wd-text-soft); margin-top: .3rem; }
/* Verlauf: Der Kunde links, wir rechts -- die im Nachrichtenverkehr
gewohnte Anordnung. Man erkennt den Absender an der Seite, noch
@@ -753,7 +753,7 @@
<div class="vw-detail-blatt" id="vw-detail-blatt"></div>
</div>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
<script>
/* =====================================================================
Verwaltung: Anfragen ansehen, Status ändern, Notizen, Archivieren.
+5 -5
View File
@@ -9,8 +9,8 @@
<link rel="icon" type="image/png" href="/assets/img/favicon.png" />
<link rel="manifest" href="/webdesign/app.webmanifest" />
<meta name="theme-color" content="#05070b" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
</head>
<body class="wd">
<div id="wd-kopf"></div>
@@ -137,8 +137,8 @@
<div id="wd-fuss"></div>
<script src="/assets/js/i18n-wd-widerruf.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/wd-widerruf.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-widerruf.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
<script src="/assets/js/wd-widerruf.js?v=20260823wd8"></script>
</body>
</html>
+4 -4
View File
@@ -21,8 +21,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Dogfather Webdesign" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd7" />
<link rel="stylesheet" href="/assets/css/main.css?v=20260823wd8" />
<link rel="stylesheet" href="/assets/css/webdesign.css?v=20260823wd8" />
<style>
/* =====================================================================
Zugangswand — nur hier gebraucht, deshalb bewusst NICHT in
@@ -177,8 +177,8 @@
</div>
</main>
<script src="/assets/js/i18n-wd-zugang.js?v=20260823wd7"></script>
<script src="/assets/js/wd-core.js?v=20260823wd7"></script>
<script src="/assets/js/i18n-wd-zugang.js?v=20260823wd8"></script>
<script src="/assets/js/wd-core.js?v=20260823wd8"></script>
<script>
(function () {
"use strict";