Einwilligung, die tatsächlich zurückhält, und Pflichtseiten

Ein Hinweis, der nur fragt und sonst nichts ändert, ist Theater. Deshalb zwei
Hälften, und die zweite ist die wichtigere.

Die Frage: serverseitig gerendert, per Formular beantwortet, ohne JavaScript -
die öffentlichen Seiten laufen mit script-src 'none', und ein Banner mit
Skript wäre das eine, was diese Regel bricht. "Alle akzeptieren" und "Nur
notwendige" sind dieselbe Art Knopf, gleich groß, nebeneinander. Die
Entscheidung steht in einem eigenen Cookie: Fassung der Frage, gewährte
Gruppen, Zeitpunkt. Keine Kennung, kein Serverabgleich, ein halbes Jahr. Kein
CSRF-Token, absichtlich: die Anfrage ändert nichts auf dem Server, und ein
Token bräuchte für jeden Besucher eine Sitzung - genau das, was ein
Einwilligungshinweis vermeiden soll. Geprüft wird stattdessen das
Rücksprungziel.

Das Zurückhalten: der neue Block "Externer Inhalt" (Dokumentschema v14) lädt
ohne Einwilligung nichts - kein iframe, keine Adresse im Markup, frame-src
'none'. Der Platzhalter nennt den Anbieter; sein Knopf gewährt genau diese
Gruppe und nimmt die vorher gewährten mit. Erst danach kommt der Host in die
Sicherheitsrichtlinie, und nur dieser. Im Editor bleibt es immer ein
Platzhalter.

Datenschutz und Impressum sind keine Einstellung mehr: fehlen sie, warnt die
Verwaltung und legt sie auf Knopfdruck als Gerüst an, mit NOCH AUSZUFÜLLEN an
jeder Pflichtangabe. Den Text schreibt das CMS nicht - das ist eine rechtliche
Aussage über eine Firma. Verlinkt werden beide aus dem Template, nicht aus dem
Fußbereich: ein Pflichtlink, den man löschen kann, ist keiner.

Dazu der Fehler, den Umut im Screenshot gesehen hat: die Wortmarke stand als
Buchstabenkolonne in einem 60 Pixel breiten Kasten. .site-header war noch ein
Flexcontainer aus der Zeit des handgeschriebenen Markups und quetschte die
Sektion zusammen, und design.css band jede Designklasse an #main - wo Kopf-
und Fußbereich seit ADR 026 nicht mehr liegen. Beides behoben.

Neu im Ausbauplan: Statistik (serverseitige, wiedererkennungsfreie Zählung,
die deshalb keine Einwilligung braucht), Inhalte und Ladeverhalten prüfen
(Blättern überall, minimal laden, Ladebudget), Installer (Schritt für Schritt
im Browser, Migration inklusive).

245 PHP-Tests / 976 Assertions unter PHP 8.2.12 und 8.4.22. Der Browsertest
fand zwei Fehler von mir: grauer Text auf dunklem Fußbereich mit 2,24:1, und
das fixierte Banner verdeckte den Einwilligungsknopf am Seitenende.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-12 19:27:41 +02:00
1 parent 84d471acd9
commit ee446f1ea1
30 files changed
+1298 -154

No files matched your search

+5
View File
@@ -546,3 +546,8 @@ details.card > summary:hover { background: #fafbf8 }
.login-card { padding: 26px }
.login-body { padding: 16px }
}
/* A group of related checkboxes needs a name of its own, not just a label per box. */
.field-set { border: 1px solid var(--line); border-radius: 10px; padding: 4px 16px 16px; margin: 18px 0 0 }
.field-set > legend { font-size: 12px; font-weight: 700; padding: 0 6px }
.field-set .check-label { margin-top: 14px }
+132 -132
View File
@@ -1,137 +1,137 @@
/* Generated from design-tokens.json by npm run build:design. */
#main .d-desktop-align-left{text-align:left;margin-inline:0}
#main .d-desktop-align-center{text-align:center;margin-inline:auto}
#main .d-desktop-align-right{text-align:right;margin-inline:auto 0}
#main .d-desktop-fontSize-small{font-size:14px}
#main .d-desktop-fontSize-body{font-size:18px}
#main .d-desktop-fontSize-large{font-size:24px}
#main .d-desktop-fontSize-title{font-size:36px}
#main .d-desktop-fontSize-display{font-size:52px}
#main .d-desktop-fontSize-hero{font-size:72px}
#main .d-desktop-fontWeight-regular{font-weight:400}
#main .d-desktop-fontWeight-medium{font-weight:500}
#main .d-desktop-fontWeight-bold{font-weight:700}
#main .d-desktop-lineHeight-tight{line-height:1.15}
#main .d-desktop-lineHeight-normal{line-height:1.5}
#main .d-desktop-lineHeight-relaxed{line-height:1.8}
#main .d-desktop-paddingY-none{padding-block:0}
#main .d-desktop-paddingY-small{padding-block:16px}
#main .d-desktop-paddingY-medium{padding-block:32px}
#main .d-desktop-paddingY-large{padding-block:64px}
#main .d-desktop-paddingY-huge{padding-block:96px}
#main .d-desktop-paddingX-none{padding-inline:0}
#main .d-desktop-paddingX-small{padding-inline:16px}
#main .d-desktop-paddingX-medium{padding-inline:32px}
#main .d-desktop-paddingX-large{padding-inline:64px}
#main .d-desktop-marginTop-none{margin-top:0}
#main .d-desktop-marginTop-small{margin-top:8px}
#main .d-desktop-marginTop-medium{margin-top:24px}
#main .d-desktop-marginTop-large{margin-top:48px}
#main .d-desktop-marginBottom-none{margin-bottom:0}
#main .d-desktop-marginBottom-small{margin-bottom:8px}
#main .d-desktop-marginBottom-medium{margin-bottom:24px}
#main .d-desktop-marginBottom-large{margin-bottom:48px}
#main .d-desktop-gap-none{gap:0}
#main .d-desktop-gap-small{gap:12px}
#main .d-desktop-gap-medium{gap:24px}
#main .d-desktop-gap-large{gap:48px}
#main .d-desktop-columns-auto{grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr))}
#main .d-desktop-columns-one{grid-template-columns:minmax(0,1fr)}
#main .d-desktop-columns-two{grid-template-columns:repeat(2,minmax(0,1fr))}
#main .d-desktop-columns-three{grid-template-columns:repeat(3,minmax(0,1fr))}
#main .d-desktop-columns-four{grid-template-columns:repeat(4,minmax(0,1fr))}
#main .d-desktop-radius-none{border-radius:0}
#main .d-desktop-radius-small{border-radius:8px}
#main .d-desktop-radius-large{border-radius:24px}
.kant .d-desktop-align-left{text-align:left;margin-inline:0}
.kant .d-desktop-align-center{text-align:center;margin-inline:auto}
.kant .d-desktop-align-right{text-align:right;margin-inline:auto 0}
.kant .d-desktop-fontSize-small{font-size:14px}
.kant .d-desktop-fontSize-body{font-size:18px}
.kant .d-desktop-fontSize-large{font-size:24px}
.kant .d-desktop-fontSize-title{font-size:36px}
.kant .d-desktop-fontSize-display{font-size:52px}
.kant .d-desktop-fontSize-hero{font-size:72px}
.kant .d-desktop-fontWeight-regular{font-weight:400}
.kant .d-desktop-fontWeight-medium{font-weight:500}
.kant .d-desktop-fontWeight-bold{font-weight:700}
.kant .d-desktop-lineHeight-tight{line-height:1.15}
.kant .d-desktop-lineHeight-normal{line-height:1.5}
.kant .d-desktop-lineHeight-relaxed{line-height:1.8}
.kant .d-desktop-paddingY-none{padding-block:0}
.kant .d-desktop-paddingY-small{padding-block:16px}
.kant .d-desktop-paddingY-medium{padding-block:32px}
.kant .d-desktop-paddingY-large{padding-block:64px}
.kant .d-desktop-paddingY-huge{padding-block:96px}
.kant .d-desktop-paddingX-none{padding-inline:0}
.kant .d-desktop-paddingX-small{padding-inline:16px}
.kant .d-desktop-paddingX-medium{padding-inline:32px}
.kant .d-desktop-paddingX-large{padding-inline:64px}
.kant .d-desktop-marginTop-none{margin-top:0}
.kant .d-desktop-marginTop-small{margin-top:8px}
.kant .d-desktop-marginTop-medium{margin-top:24px}
.kant .d-desktop-marginTop-large{margin-top:48px}
.kant .d-desktop-marginBottom-none{margin-bottom:0}
.kant .d-desktop-marginBottom-small{margin-bottom:8px}
.kant .d-desktop-marginBottom-medium{margin-bottom:24px}
.kant .d-desktop-marginBottom-large{margin-bottom:48px}
.kant .d-desktop-gap-none{gap:0}
.kant .d-desktop-gap-small{gap:12px}
.kant .d-desktop-gap-medium{gap:24px}
.kant .d-desktop-gap-large{gap:48px}
.kant .d-desktop-columns-auto{grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr))}
.kant .d-desktop-columns-one{grid-template-columns:minmax(0,1fr)}
.kant .d-desktop-columns-two{grid-template-columns:repeat(2,minmax(0,1fr))}
.kant .d-desktop-columns-three{grid-template-columns:repeat(3,minmax(0,1fr))}
.kant .d-desktop-columns-four{grid-template-columns:repeat(4,minmax(0,1fr))}
.kant .d-desktop-radius-none{border-radius:0}
.kant .d-desktop-radius-small{border-radius:8px}
.kant .d-desktop-radius-large{border-radius:24px}
@media(max-width:1024px){
#main .d-tablet-align-left{text-align:left;margin-inline:0}
#main .d-tablet-align-center{text-align:center;margin-inline:auto}
#main .d-tablet-align-right{text-align:right;margin-inline:auto 0}
#main .d-tablet-fontSize-small{font-size:14px}
#main .d-tablet-fontSize-body{font-size:18px}
#main .d-tablet-fontSize-large{font-size:24px}
#main .d-tablet-fontSize-title{font-size:36px}
#main .d-tablet-fontSize-display{font-size:52px}
#main .d-tablet-fontSize-hero{font-size:72px}
#main .d-tablet-fontWeight-regular{font-weight:400}
#main .d-tablet-fontWeight-medium{font-weight:500}
#main .d-tablet-fontWeight-bold{font-weight:700}
#main .d-tablet-lineHeight-tight{line-height:1.15}
#main .d-tablet-lineHeight-normal{line-height:1.5}
#main .d-tablet-lineHeight-relaxed{line-height:1.8}
#main .d-tablet-paddingY-none{padding-block:0}
#main .d-tablet-paddingY-small{padding-block:16px}
#main .d-tablet-paddingY-medium{padding-block:32px}
#main .d-tablet-paddingY-large{padding-block:64px}
#main .d-tablet-paddingY-huge{padding-block:96px}
#main .d-tablet-paddingX-none{padding-inline:0}
#main .d-tablet-paddingX-small{padding-inline:16px}
#main .d-tablet-paddingX-medium{padding-inline:32px}
#main .d-tablet-paddingX-large{padding-inline:64px}
#main .d-tablet-marginTop-none{margin-top:0}
#main .d-tablet-marginTop-small{margin-top:8px}
#main .d-tablet-marginTop-medium{margin-top:24px}
#main .d-tablet-marginTop-large{margin-top:48px}
#main .d-tablet-marginBottom-none{margin-bottom:0}
#main .d-tablet-marginBottom-small{margin-bottom:8px}
#main .d-tablet-marginBottom-medium{margin-bottom:24px}
#main .d-tablet-marginBottom-large{margin-bottom:48px}
#main .d-tablet-gap-none{gap:0}
#main .d-tablet-gap-small{gap:12px}
#main .d-tablet-gap-medium{gap:24px}
#main .d-tablet-gap-large{gap:48px}
#main .d-tablet-columns-auto{grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr))}
#main .d-tablet-columns-one{grid-template-columns:minmax(0,1fr)}
#main .d-tablet-columns-two{grid-template-columns:repeat(2,minmax(0,1fr))}
#main .d-tablet-columns-three{grid-template-columns:repeat(3,minmax(0,1fr))}
#main .d-tablet-columns-four{grid-template-columns:repeat(4,minmax(0,1fr))}
#main .d-tablet-radius-none{border-radius:0}
#main .d-tablet-radius-small{border-radius:8px}
#main .d-tablet-radius-large{border-radius:24px}
.kant .d-tablet-align-left{text-align:left;margin-inline:0}
.kant .d-tablet-align-center{text-align:center;margin-inline:auto}
.kant .d-tablet-align-right{text-align:right;margin-inline:auto 0}
.kant .d-tablet-fontSize-small{font-size:14px}
.kant .d-tablet-fontSize-body{font-size:18px}
.kant .d-tablet-fontSize-large{font-size:24px}
.kant .d-tablet-fontSize-title{font-size:36px}
.kant .d-tablet-fontSize-display{font-size:52px}
.kant .d-tablet-fontSize-hero{font-size:72px}
.kant .d-tablet-fontWeight-regular{font-weight:400}
.kant .d-tablet-fontWeight-medium{font-weight:500}
.kant .d-tablet-fontWeight-bold{font-weight:700}
.kant .d-tablet-lineHeight-tight{line-height:1.15}
.kant .d-tablet-lineHeight-normal{line-height:1.5}
.kant .d-tablet-lineHeight-relaxed{line-height:1.8}
.kant .d-tablet-paddingY-none{padding-block:0}
.kant .d-tablet-paddingY-small{padding-block:16px}
.kant .d-tablet-paddingY-medium{padding-block:32px}
.kant .d-tablet-paddingY-large{padding-block:64px}
.kant .d-tablet-paddingY-huge{padding-block:96px}
.kant .d-tablet-paddingX-none{padding-inline:0}
.kant .d-tablet-paddingX-small{padding-inline:16px}
.kant .d-tablet-paddingX-medium{padding-inline:32px}
.kant .d-tablet-paddingX-large{padding-inline:64px}
.kant .d-tablet-marginTop-none{margin-top:0}
.kant .d-tablet-marginTop-small{margin-top:8px}
.kant .d-tablet-marginTop-medium{margin-top:24px}
.kant .d-tablet-marginTop-large{margin-top:48px}
.kant .d-tablet-marginBottom-none{margin-bottom:0}
.kant .d-tablet-marginBottom-small{margin-bottom:8px}
.kant .d-tablet-marginBottom-medium{margin-bottom:24px}
.kant .d-tablet-marginBottom-large{margin-bottom:48px}
.kant .d-tablet-gap-none{gap:0}
.kant .d-tablet-gap-small{gap:12px}
.kant .d-tablet-gap-medium{gap:24px}
.kant .d-tablet-gap-large{gap:48px}
.kant .d-tablet-columns-auto{grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr))}
.kant .d-tablet-columns-one{grid-template-columns:minmax(0,1fr)}
.kant .d-tablet-columns-two{grid-template-columns:repeat(2,minmax(0,1fr))}
.kant .d-tablet-columns-three{grid-template-columns:repeat(3,minmax(0,1fr))}
.kant .d-tablet-columns-four{grid-template-columns:repeat(4,minmax(0,1fr))}
.kant .d-tablet-radius-none{border-radius:0}
.kant .d-tablet-radius-small{border-radius:8px}
.kant .d-tablet-radius-large{border-radius:24px}
}
@media(max-width:650px){
#main .d-mobile-align-left{text-align:left;margin-inline:0}
#main .d-mobile-align-center{text-align:center;margin-inline:auto}
#main .d-mobile-align-right{text-align:right;margin-inline:auto 0}
#main .d-mobile-fontSize-small{font-size:14px}
#main .d-mobile-fontSize-body{font-size:18px}
#main .d-mobile-fontSize-large{font-size:24px}
#main .d-mobile-fontSize-title{font-size:36px}
#main .d-mobile-fontSize-display{font-size:52px}
#main .d-mobile-fontSize-hero{font-size:72px}
#main .d-mobile-fontWeight-regular{font-weight:400}
#main .d-mobile-fontWeight-medium{font-weight:500}
#main .d-mobile-fontWeight-bold{font-weight:700}
#main .d-mobile-lineHeight-tight{line-height:1.15}
#main .d-mobile-lineHeight-normal{line-height:1.5}
#main .d-mobile-lineHeight-relaxed{line-height:1.8}
#main .d-mobile-paddingY-none{padding-block:0}
#main .d-mobile-paddingY-small{padding-block:16px}
#main .d-mobile-paddingY-medium{padding-block:32px}
#main .d-mobile-paddingY-large{padding-block:64px}
#main .d-mobile-paddingY-huge{padding-block:96px}
#main .d-mobile-paddingX-none{padding-inline:0}
#main .d-mobile-paddingX-small{padding-inline:16px}
#main .d-mobile-paddingX-medium{padding-inline:32px}
#main .d-mobile-paddingX-large{padding-inline:64px}
#main .d-mobile-marginTop-none{margin-top:0}
#main .d-mobile-marginTop-small{margin-top:8px}
#main .d-mobile-marginTop-medium{margin-top:24px}
#main .d-mobile-marginTop-large{margin-top:48px}
#main .d-mobile-marginBottom-none{margin-bottom:0}
#main .d-mobile-marginBottom-small{margin-bottom:8px}
#main .d-mobile-marginBottom-medium{margin-bottom:24px}
#main .d-mobile-marginBottom-large{margin-bottom:48px}
#main .d-mobile-gap-none{gap:0}
#main .d-mobile-gap-small{gap:12px}
#main .d-mobile-gap-medium{gap:24px}
#main .d-mobile-gap-large{gap:48px}
#main .d-mobile-columns-auto{grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr))}
#main .d-mobile-columns-one{grid-template-columns:minmax(0,1fr)}
#main .d-mobile-columns-two{grid-template-columns:repeat(2,minmax(0,1fr))}
#main .d-mobile-columns-three{grid-template-columns:repeat(3,minmax(0,1fr))}
#main .d-mobile-columns-four{grid-template-columns:repeat(4,minmax(0,1fr))}
#main .d-mobile-radius-none{border-radius:0}
#main .d-mobile-radius-small{border-radius:8px}
#main .d-mobile-radius-large{border-radius:24px}
.kant .d-mobile-align-left{text-align:left;margin-inline:0}
.kant .d-mobile-align-center{text-align:center;margin-inline:auto}
.kant .d-mobile-align-right{text-align:right;margin-inline:auto 0}
.kant .d-mobile-fontSize-small{font-size:14px}
.kant .d-mobile-fontSize-body{font-size:18px}
.kant .d-mobile-fontSize-large{font-size:24px}
.kant .d-mobile-fontSize-title{font-size:36px}
.kant .d-mobile-fontSize-display{font-size:52px}
.kant .d-mobile-fontSize-hero{font-size:72px}
.kant .d-mobile-fontWeight-regular{font-weight:400}
.kant .d-mobile-fontWeight-medium{font-weight:500}
.kant .d-mobile-fontWeight-bold{font-weight:700}
.kant .d-mobile-lineHeight-tight{line-height:1.15}
.kant .d-mobile-lineHeight-normal{line-height:1.5}
.kant .d-mobile-lineHeight-relaxed{line-height:1.8}
.kant .d-mobile-paddingY-none{padding-block:0}
.kant .d-mobile-paddingY-small{padding-block:16px}
.kant .d-mobile-paddingY-medium{padding-block:32px}
.kant .d-mobile-paddingY-large{padding-block:64px}
.kant .d-mobile-paddingY-huge{padding-block:96px}
.kant .d-mobile-paddingX-none{padding-inline:0}
.kant .d-mobile-paddingX-small{padding-inline:16px}
.kant .d-mobile-paddingX-medium{padding-inline:32px}
.kant .d-mobile-paddingX-large{padding-inline:64px}
.kant .d-mobile-marginTop-none{margin-top:0}
.kant .d-mobile-marginTop-small{margin-top:8px}
.kant .d-mobile-marginTop-medium{margin-top:24px}
.kant .d-mobile-marginTop-large{margin-top:48px}
.kant .d-mobile-marginBottom-none{margin-bottom:0}
.kant .d-mobile-marginBottom-small{margin-bottom:8px}
.kant .d-mobile-marginBottom-medium{margin-bottom:24px}
.kant .d-mobile-marginBottom-large{margin-bottom:48px}
.kant .d-mobile-gap-none{gap:0}
.kant .d-mobile-gap-small{gap:12px}
.kant .d-mobile-gap-medium{gap:24px}
.kant .d-mobile-gap-large{gap:48px}
.kant .d-mobile-columns-auto{grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr))}
.kant .d-mobile-columns-one{grid-template-columns:minmax(0,1fr)}
.kant .d-mobile-columns-two{grid-template-columns:repeat(2,minmax(0,1fr))}
.kant .d-mobile-columns-three{grid-template-columns:repeat(3,minmax(0,1fr))}
.kant .d-mobile-columns-four{grid-template-columns:repeat(4,minmax(0,1fr))}
.kant .d-mobile-radius-none{border-radius:0}
.kant .d-mobile-radius-small{border-radius:8px}
.kant .d-mobile-radius-large{border-radius:24px}
}
+5 -2
View File
@@ -1,5 +1,5 @@
export const labels = { section: 'Sektion', row: 'Zeile', column: 'Spalte', heading: 'Überschrift', text: 'Text', image: 'Bild', button: 'Button', gallery: 'Galerie', postlist: 'Beitragsliste', iconcard: 'Symbolkarte', stat: 'Kennzahl', quote: 'Kundenstimme', logos: 'Logoleiste', cta: 'Aufruf-Band', contact: 'Kontaktdaten', divider: 'Trennlinie', spacer: 'Abstand', accordion: 'Akkordeon', accordionitem: 'Akkordeon-Eintrag', tabs: 'Tabs', tabitem: 'Tab', form: 'Formular', breadcrumb: 'Brotkrumen', menu: 'Navigation', partialref: 'Verknüpfte Sektion' };
export const icons = { section: '▣', row: '☰', column: '▥', heading: 'H', text: 'T', image: '▧', button: '↗', gallery: '▦', postlist: '☷', iconcard: '◈', stat: '№', quote: '❝', logos: '⬚', cta: '◉', contact: '✆', divider: '—', spacer: '⇳', accordion: '⌵', accordionitem: '·', tabs: '▭', tabitem: '▫', form: '✉', breadcrumb: '⟩', menu: '☰', partialref: '⧉' };
export const labels = { section: 'Sektion', row: 'Zeile', column: 'Spalte', heading: 'Überschrift', text: 'Text', image: 'Bild', button: 'Button', gallery: 'Galerie', postlist: 'Beitragsliste', iconcard: 'Symbolkarte', stat: 'Kennzahl', quote: 'Kundenstimme', logos: 'Logoleiste', cta: 'Aufruf-Band', contact: 'Kontaktdaten', divider: 'Trennlinie', spacer: 'Abstand', accordion: 'Akkordeon', accordionitem: 'Akkordeon-Eintrag', tabs: 'Tabs', tabitem: 'Tab', form: 'Formular', breadcrumb: 'Brotkrumen', menu: 'Navigation', partialref: 'Verknüpfte Sektion', embed: 'Externer Inhalt' };
export const icons = { section: '▣', row: '☰', column: '▥', heading: 'H', text: 'T', image: '▧', button: '↗', gallery: '▦', postlist: '☷', iconcard: '◈', stat: '№', quote: '❝', logos: '⬚', cta: '◉', contact: '✆', divider: '—', spacer: '⇳', accordion: '⌵', accordionitem: '·', tabs: '▭', tabitem: '▫', form: '✉', breadcrumb: '⟩', menu: '☰', partialref: '⧉', embed: '⬓' };
// A namespaced type belongs to a plugin this installation may not have. Such blocks stay in the
// document, stay selectable and movable, but the editor never edits or fills them.
export const PLUGIN_TYPE = /^[a-z][a-z0-9]{1,30}\.[a-z][a-z0-9-]{1,40}$/;
@@ -48,6 +48,8 @@ export function createBlock(type) {
if (type === 'form') block.props = { form: '' };
if (type === 'menu') block.props = { slot: 'header' };
if (type === 'partialref') block.props = { partial: '' };
// An embed starts empty and blocked: no address, so nothing loads before someone enters one.
if (type === 'embed') block.props = { group: 'external', title: '', provider: '', url: '', hint: '', height: 400 };
if (type === 'image') block.props = { src: '/assets/image-placeholder.svg', alt: 'Abstrakte Landschaft als Platzhalter', decorative: false, width: 1000, height: 600 };
return block;
}
@@ -61,6 +63,7 @@ export function insertBlock(document, type, selected) {
if (type === 'form') upgradeDocument(document, 8);
if (type === 'breadcrumb') upgradeDocument(document, 9);
if (type === 'menu' || type === 'partialref') upgradeDocument(document, 11);
if (type === 'embed') upgradeDocument(document, 14);
let target = locate(document.blocks, selected);
if (target?.block.type === 'gallery' && type !== 'image') { target.siblings.splice(target.index + 1, 0, block); return block.id; }
if (target?.parent?.type === 'gallery' && type !== 'image') target = locate(document.blocks, target.parent.id);
+8
View File
@@ -237,6 +237,14 @@ function renderProperties() {
if (block.type === 'spacer') field('Höhe', 'size', block.props.size, { choices: { small: 'Klein', medium: 'Mittel', large: 'Groß', huge: 'Sehr groß' } });
if (block.type === 'menu') field('Menüplatz', 'slot', block.props.slot, { choices: { header: 'Hauptnavigation', footer: 'Fußzeile' }, help: 'Die Einträge werden unter Menüs gepflegt.' });
if (block.type === 'partialref') field('Adresse der Sektion', 'partial', block.props.partial, { help: 'Die Adresse aus der Bereichsverwaltung. Die Sektion wird verknüpft, nicht kopiert: eine Änderung dort wirkt auf allen Seiten, die sie einbinden.' });
if (block.type === 'embed') {
field('Gruppe', 'group', block.props.group, { choices: { external: 'Externe Inhalte', statistics: 'Statistik' }, help: 'Bestimmt, welche Einwilligung dieser Inhalt braucht. Vor der Einwilligung wird nichts geladen – kein Bild, kein Skript, keine Anfrage an den Anbieter.' });
field('Titel', 'title', block.props.title, { maxLength: 160, help: 'Was hier eingebettet ist, in einem Satz. Screenreader lesen das als Namen des Rahmens vor, deshalb nicht leer lassen.' });
field('Anbieter', 'provider', block.props.provider, { maxLength: 80, help: 'Wer den Inhalt ausliefert, zum Beispiel Google Maps. Steht im Platzhalter, damit die Entscheidung eine Grundlage hat.' });
field('Adresse (HTTPS)', 'url', block.props.url, { help: 'Die Einbettungsadresse des Anbieters. Nur HTTPS, ohne Port. Dieser Host wird der Sicherheitsrichtlinie der Seite hinzugefügt – und zwar nur, wenn eingewilligt wurde.' });
field('Höhe (px)', 'height', block.props.height, { number: true, help: 'Zwischen 150 und 1200.' });
field('Hinweis im Platzhalter', 'hint', block.props.hint || '', { multiline: true, help: 'Leer lassen für den Standardtext.' });
}
if (block.type === 'form') field('Adresse des Formulars', 'form', block.props.form, { help: 'Die Adresse aus der Formularverwaltung, zum Beispiel kontakt. Felder, Empfänger und Einwilligungstext werden dort gepflegt, nicht hier.' });
if (block.type === 'tabitem') field('Titel', 'title', block.props.title, { maxLength: 160, help: 'Beschriftung des Tabs. In der Vorschau sind alle Tabs untereinander sichtbar, weil die Vorschau kein JavaScript ausführt.' });
if (block.type === 'accordionitem') {
+58 -2
View File
@@ -2,8 +2,8 @@
h1,h2,h3,p,a{overflow-wrap:anywhere;hyphens:auto}
.block-image{margin:0;max-width:100%}.block-image img{max-width:100%;height:auto}
:root{color-scheme:light;--paper:#f7f8f4;--ink:#182d2b;--muted:#50625d;--line:#d7dfd7;--mint:#e5f0b6;--accent:#315b49;font-family:Arial,Helvetica,sans-serif;color:var(--ink);background:var(--paper);font-synthesis:none}
*{box-sizing:border-box}body{margin:0}a{color:inherit;text-underline-offset:5px}a:hover{text-decoration-thickness:2px}:focus-visible{outline:3px solid #b85c12;outline-offset:6px}main:focus{outline:none}.shell{width:min(1180px,calc(100% - 64px));margin-inline:auto}.site-header{display:flex;justify-content:space-between;align-items:center;min-height:106px;border-bottom:1px solid var(--line);gap:24px}.brand{display:inline-flex;align-items:center;gap:5px;font-weight:800;font-size:28px;letter-spacing:-1.3px;text-decoration:none}.brand-icon{width:37px;height:37px;background:var(--ink);color:var(--mint);border-radius:10px;display:grid;place-items:center;margin-right:7px;font-size:27px;letter-spacing:-2px}.brand-light{font-weight:400}.site-header nav{display:flex;gap:32px;align-items:center;font-size:14px;font-weight:600}.site-header nav a{text-decoration:none;padding:12px 0}.site-header nav [aria-current=page]{text-decoration:underline}.site-header nav .nav-link{border:1px solid var(--ink);padding:13px 18px;border-radius:7px}.nav-link span{margin-left:18px}.block-section{padding:70px 0}.block-section:first-child{padding:74px 0 82px}.section-inner>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 30px}h1,h2,h3{letter-spacing:-.05em;line-height:1.05;margin:0 0 24px}h1{font-size:clamp(48px,6.6vw,84px);font-weight:500;max-width:900px}h2{font-size:clamp(32px,4vw,49px);font-weight:500;max-width:760px}h3{font-size:26px;letter-spacing:-.035em;line-height:1.2}.block-text{font-size:18px;line-height:1.7;max-width:690px;margin:0 0 23px;color:var(--muted);white-space:pre-line}.button{display:inline-flex;align-items:center;gap:32px;min-height:50px;margin-top:9px;padding:15px 21px;background:var(--ink);color:var(--paper);border-radius:7px;text-decoration:none;font-size:14px;font-weight:600}.button:hover{background:#294b41}.block-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr));gap:22px;margin-top:38px;background:transparent!important}.block-column{border:1px solid var(--line);border-radius:12px;padding:30px;min-width:0}.block-column .block-text{font-size:15px}.block-column>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:1px;color:var(--accent);margin-bottom:34px}.tone-mint{background:var(--mint)}.tone-ink{background:var(--ink);color:var(--paper)}.tone-ink .block-text{color:#dae3da}.tone-ink .section-inner>.block-text:first-child{color:var(--mint)}.tone-ink .button{color:var(--ink);background:var(--mint)}.width-narrow>.section-inner{max-width:800px}.site-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:20px;padding-block:34px;border-top:1px solid var(--line)}.site-footer p,.site-footer>span{font-size:12px;color:var(--muted)}.site-footer .brand{font-size:22px}.skip-link{position:fixed;left:16px;top:-100px;z-index:10;background:var(--ink);color:white;padding:16px}.skip-link:focus{top:16px}img{display:block;max-width:100%;height:auto;border-radius:12px}.error-page{padding-block:100px}.error-page h1{max-width:800px}
@media(max-width:650px){.shell{width:calc(100% - 36px)}.site-header{min-height:100px;align-items:flex-start;flex-direction:column;justify-content:center;gap:18px;padding-block:22px}.site-header nav{width:100%;justify-content:space-between;gap:12px;font-size:12px}.site-header nav .nav-link{padding:10px}.nav-link span{margin-left:6px}.block-section,.block-section:first-child{padding-block:48px}h1{font-size:49px}.block-text{font-size:16px}.block-column{padding:26px}.site-footer{align-items:flex-start;flex-direction:column;gap:10px}.site-footer p{margin:0}.block-row{gap:16px}}
*{box-sizing:border-box}body{margin:0}a{color:inherit;text-underline-offset:5px}a:hover{text-decoration-thickness:2px}:focus-visible{outline:3px solid #b85c12;outline-offset:6px}main:focus{outline:none}.shell{width:min(1180px,calc(100% - 64px));margin-inline:auto}.site-header{border-bottom:1px solid var(--line)}.brand{display:inline-flex;align-items:center;gap:5px;font-weight:800;font-size:28px;letter-spacing:-1.3px;text-decoration:none}.brand-icon{width:37px;height:37px;background:var(--ink);color:var(--mint);border-radius:10px;display:grid;place-items:center;margin-right:7px;font-size:27px;letter-spacing:-2px}.brand-light{font-weight:400}.site-header nav{display:flex;gap:32px;align-items:center;font-size:14px;font-weight:600}.site-header nav a{text-decoration:none;padding:12px 0}.site-header nav [aria-current=page]{text-decoration:underline}.site-header nav .nav-link{border:1px solid var(--ink);padding:13px 18px;border-radius:7px}.nav-link span{margin-left:18px}.block-section{padding:70px 0}.block-section:first-child{padding:74px 0 82px}.section-inner>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:var(--accent);margin:0 0 30px}h1,h2,h3{letter-spacing:-.05em;line-height:1.05;margin:0 0 24px}h1{font-size:clamp(48px,6.6vw,84px);font-weight:500;max-width:900px}h2{font-size:clamp(32px,4vw,49px);font-weight:500;max-width:760px}h3{font-size:26px;letter-spacing:-.035em;line-height:1.2}.block-text{font-size:18px;line-height:1.7;max-width:690px;margin:0 0 23px;color:var(--muted);white-space:pre-line}.button{display:inline-flex;align-items:center;gap:32px;min-height:50px;margin-top:9px;padding:15px 21px;background:var(--ink);color:var(--paper);border-radius:7px;text-decoration:none;font-size:14px;font-weight:600}.button:hover{background:#294b41}.block-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(250px,100%),1fr));gap:22px;margin-top:38px;background:transparent!important}.block-column{border:1px solid var(--line);border-radius:12px;padding:30px;min-width:0}.block-column .block-text{font-size:15px}.block-column>.block-text:first-child{font-size:12px;font-weight:700;letter-spacing:1px;color:var(--accent);margin-bottom:34px}.tone-mint{background:var(--mint)}.tone-ink{background:var(--ink);color:var(--paper)}.tone-ink .block-text{color:#dae3da}.tone-ink .section-inner>.block-text:first-child{color:var(--mint)}.tone-ink .button{color:var(--ink);background:var(--mint)}.width-narrow>.section-inner{max-width:800px}.site-footer{border-top:1px solid var(--line)}.site-footer p,.site-footer>span{font-size:12px;color:var(--muted)}.site-footer .brand{font-size:22px}.skip-link{position:fixed;left:16px;top:-100px;z-index:10;background:var(--ink);color:white;padding:16px}.skip-link:focus{top:16px}img{display:block;max-width:100%;height:auto;border-radius:12px}.error-page{padding-block:100px}.error-page h1{max-width:800px}
@media(max-width:650px){.shell{width:calc(100% - 36px)}.site-header nav{width:100%;justify-content:space-between;gap:12px;font-size:12px}.site-header nav .nav-link{padding:10px}.nav-link span{margin-left:6px}.block-section,.block-section:first-child{padding-block:48px}h1{font-size:49px}.block-text{font-size:16px}.block-column{padding:26px}.site-footer{align-items:flex-start;flex-direction:column;gap:10px}.site-footer p{margin:0}.block-row{gap:16px}}
/* Menus. Submenus open with details/summary, so the navigation works without JavaScript. */
.menu-list{list-style:none;display:flex;flex-wrap:wrap;align-items:center;gap:4px 22px;margin:0;padding:0}
@@ -104,3 +104,59 @@ h1,h2,h3,p,a{overflow-wrap:anywhere;hyphens:auto}
later, so the dark tone wins where it has to. */
.tone-ink .block-column .block-text,
.tone-ink .block-column>.block-text:first-child{color:#dae3da}
/* The consent question. Fixed to the bottom, but it never covers the whole page: the content
stays readable while the decision is open, because a wall would make "accept" the only way
forward. Accepting and refusing are the same button in the same row. */
.consent-bar{position:fixed;inset-inline:12px;bottom:12px;z-index:50;max-width:620px;margin-inline:auto;background:#fff;color:#203b34;border:1px solid #cfd8d1;border-radius:14px;padding:20px 22px;box-shadow:0 18px 50px #1b2a2426;font-size:14px;line-height:1.6}
.consent-bar h2{font-size:16px;margin:0 0 8px}
.consent-bar p{margin:0 0 14px}
.consent-actions{display:flex;flex-wrap:wrap;gap:10px}
.consent-button{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:11px 18px;border-radius:8px;border:1px solid #2c6b4f;background:#2c6b4f;color:#fff;font:inherit;font-weight:600;cursor:pointer}
.consent-actions .consent-button:last-child{background:#fff;color:#1b2a24;border-color:#b3c0b7}
.consent-button:hover{filter:brightness(.94)}
.consent-button:focus-visible,.consent-bar a:focus-visible,.consent-bar summary:focus-visible,.consent-bar input:focus-visible{outline:3px solid #b55b12;outline-offset:2px}
.consent-details{margin-top:14px}
.consent-details>summary{cursor:pointer;font-size:13px;font-weight:600;padding:4px 0}
.consent-groups{list-style:none;margin:10px 0 0;padding:0;display:grid;gap:10px}
.consent-groups label{display:flex;gap:10px;align-items:flex-start;font-size:13px;line-height:1.5}
.consent-groups input{margin-top:3px;width:18px;height:18px;flex-shrink:0}
.consent-choice{margin-top:14px;background:#fff;color:#1b2a24;border-color:#b3c0b7}
.consent-links{margin:14px 0 0;font-size:12px}
.consent-links a{color:#a4510f}
/* While the question is open the bar is fixed at the bottom, so the page has to end above it -
otherwise it covers the last element, and an embed's own consent button sat right underneath.
Found by the browser test, which could not click it. */
body.consent-open{padding-bottom:260px}
@media (min-width:900px){body.consent-open{padding-bottom:200px}}
.consent-page main{max-width:680px;margin-inline:auto;padding:40px 20px}
.consent-page .consent-bar{position:static;inset:auto;box-shadow:none;max-width:none;margin-top:24px}
@media (max-width:620px){.consent-bar{inset-inline:0;bottom:0;border-radius:14px 14px 0 0;padding:16px}.consent-actions .consent-button{flex:1 1 140px}}
/* An embedded third party, and what stands in its place until it may load. */
.block-embed iframe{display:block;width:100%;border:0;border-radius:10px;background:#eef1ed}
.embed-blocked{border:1px dashed #b3c0b7;border-radius:10px;padding:24px;background:#f6f7f4;text-align:center}
.embed-title{margin:0 0 8px;font-weight:600}
.embed-hint{margin:0 0 10px;font-size:14px;line-height:1.6;color:#4a5751}
.embed-provider{margin:0 0 16px;font-size:13px;color:#4a5751}
/* Header and footer hold block documents of their own (ADR 026). They are therefore ordinary
block containers - as flex containers they shrank the section inside them until a single word
wrapped one letter per line. Their sections are chrome, not articles, so they sit tighter than
a section in the page body, and their rows keep their columns side by side. */
.site-header .block-section,
.site-footer .block-section{padding-block:20px}
.site-header .block-row,
.site-footer .block-row{margin-top:0;align-items:center}
.site-header .block-column,
.site-footer .block-column{border:0;border-radius:0;padding:0}
.site-header .block-text,
.site-footer .block-text{margin:0}
/* Size and weight only, never colour: the tone of the area decides that, and a colour here won
against .tone-ink and put 2.24:1 grey text on the dark footer. The browser check caught it. */
.site-header .block-column>.block-text:first-child{font-size:24px;font-weight:800;letter-spacing:-1px;text-transform:none;margin:0}
.site-footer .block-column>.block-text:first-child{font-size:14px;font-weight:400;letter-spacing:0;text-transform:none;margin:0}
/* One landmark for the three links that must be on every page. Loose text outside a landmark is
also what axe objects to, and rightly: a screen reader then has no way to skip to it. */
.legal-bar{padding:16px 20px;text-align:center;font-size:12px;color:var(--muted);background:#f1f3ef}
.legal-bar a{color:#a4510f}