Zum Inhalt
← Sektionen & Funktionen

Spendenmodul mit Wirkungssatz

Spenden mit Umschalter einmalig/monatlich, vier Beträgen mit Wirkungssatz, geprüftem freiem Feld, Odometer, wechselnder Strichzeichnung und Vertrauenszeile.

Spendenseite eines Vereins oder einer Stiftung mit laufender Arbeit.

Ein Spendenformular als eigene Sektion. Ein Umschalter wählt einmalig oder monatlich, ein gleitender Indikator zeigt die Wahl. Vier Betragsvorschläge stehen als Segmente nebeneinander, jeder mit einem kurzen Wirkungssatz. Ein freies Feld nimmt eigene Beträge an und prüft sie erst nach der Eingabe. Der gewählte Betrag zählt als Odometer zum neuen Wert, Wirkungssatz und Zeichnung wechseln mit, der Absende-Knopf nennt den Betrag. Darunter eine Vertrauenszeile mit Gemeinnützigkeit, Spendenquittung, Mittelverwendung und Kontakt.

Ein Betrag allein ist abstrakt. Ein Satz, was er bewirkt, macht ihn greifbar und erleichtert die Entscheidung. Der Umschalter macht die monatliche Spende sichtbar, ohne die einmalige zu verstecken. Die Vertrauenszeile beantwortet die Fragen, die kurz vor dem Klick kommen.

Wann einsetzen

Vereine, Stiftungen, Initiativen, Fördervereine von Schulen, Tierheimen oder Kulturhäusern. Auf der Spendenseite und als Sektion auf der Startseite. Die Variante „Aktion mit Ziel“ für befristete Kampagnen mit festem Betrag.

Wann nicht

Wenn ein externer Spendendienst das Formular liefert und sich nicht gestalten lässt: dann nur die Betragswahl mit Wirkung bauen und den Betrag an den Dienst übergeben. Wenn der Verein die Wirkung nicht seriös beziffern kann: keine erfundenen Gleichungen, sondern Beispiele aus dem Jahresbericht.

Varianten

  • Einmalig oder monatlich mit vier Beträgen und freiem Feld
  • Aktion mit Spendenziel: ehrlicher Fortschrittsbalken, der eigene Betrag als Vorschau im Balken, Beträge auf das Ziel bezogen
  • Fördermitgliedschaft: monatlich oder jährlich, monatliche Option zuerst
  • Kompakt auf der Startseite mit Übergabe an die Spendenseite

Umsetzung

<form> mit fieldset „Wie oft?“ (Radios einmalig/monatlich) und fieldset „Betrag wählen“ (Radios, Betrag und Wirkung im Label). Indikator als ::before der Spur, .rhythmus:has(input[value=monatlich]:checked)::before { translate: 100% 0 }. Je Rhythmus eigene Beträge; beim Wechsel bleibt die Position des Vorschlags erhalten.

Freies Feld type=text inputmode=decimal (nicht type=number: deutsche Kommas, keine Pfeile). Eigene Prüfung: Ziffern, Komma oder Tausenderpunkt, höchstens 2 Nachkommastellen, Mindest- und Höchstbetrag. Fehler erst nach blur oder submit, aria-invalid und aria-describedby. Solange der Betrag ungültig ist, zeigen Wirkung und Knopf das an.

Odometer: je Ziffer eine Zelle mit overflow: hidden und einem Streifen 0–9, translateY(−Ziffer × Zellenhöhe), transition-delay je Stelle. Der Betrag steht zusätzlich als Text in einer aria-live-Region.

Wirkung für freie Beträge aus dem Jahresbetrag in die größte passende Einheit rechnen (Meter Lahnung, Zähltage, Schulklassen, Setzlinge). Zahlen dafür aus dem Jahresbericht des Vereins nehmen.

Absenden übergibt Betrag und Rhythmus an den Zahlungsdienst (Parameter oder Formularfelder). Steuerliche Hinweise (Zuwendungsbestätigung, vereinfachter Nachweis bis 300 €) und Siegel vom Kunden prüfen lassen; ein Siegel nur zeigen, wenn der Verein es führt.

Robust umsetzen

Telefon. Beträge zweispaltig mit großen Zielen, numerische Tastatur im freien Feld, Absende-Knopf in voller Breite. Wirkung und Zeichnung stehen über dem Formular.

Ohne Bewegung. Odometer springt auf den Endwert, Indikator springt, Wirkungssatz und Zeichnung wechseln nur mit kurzer Deckkraft.

Barrierefrei. Radiogruppen mit fieldset und legend, Wirkung im Label des Betrags. Ziffernstreifen aria-hidden, Betrag als Text in aria-live. Fehler erst nach Interaktion, mit aria-invalid und aria-describedby; Fehlertext #b3261e mit Schriftstärke 600. Gelber Knopf mit dunkler Schrift (#222520 auf #ffca0a). Fokus mit 3px-Kontur in Markenfarbe. Fortschritt und Mittelverwendung als role=img mit Text.

Code

<!DOCTYPE html>
<!--
  Spendenmodul mit Wirkungssatz
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/spendenmodul-wirkung/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Spendenmodul mit Wirkungssatz · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Big Shoulders'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/big-shoulders:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }
@font-face { font-family: 'Archivo'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/archivo:vf@latest/latin-full-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-stretch: 62% 125%; font-display: swap; }

:root {
  /* Rezept verein-nonprofit-wirkung: tiefer Markenton, helle Flächen derselben Farbe, Gelb nur für „Spenden“ */
  --grund: #ffffff; --hell: #edf5fa; --hell2: #dbebf5; --marke: #002e6d; --marke2: #0a3d82; --tinte: #222520; --leise: #4f5450; --grau: #dfdfd7;
  --signal: #ffca0a; --signal-dunkel: #f0b900; --wasser: #8fc3e8; --linie: #c9d6e2; --fehler: #b3261e; --gut: #1d6b45;
  --kurve: cubic-bezier(0.645, 0.045, 0.355, 1);   /* easeInOutCubic */
  --ui: 180ms; --odo: 700ms; --wechsel: 600ms;
  --kante: clamp(16px, 3.4vw, 48px);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--tinte); font-family: 'Archivo', system-ui, sans-serif; font-stretch: 100%; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; }
button, input { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
a { color: inherit; }
.versal { font-family: 'Big Shoulders', 'Archivo Narrow', sans-serif; text-transform: uppercase; }
.label { font: 650 .75rem/1.2 'Archivo', sans-serif; letter-spacing: .12em; text-transform: uppercase; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
:focus-visible { outline: 3px solid var(--marke); outline-offset: 3px; }

/* ---------- Kopf des Vereins ---------- */
.verein { display: flex; align-items: center; justify-content: space-between; gap: 20px; height: 70px; padding: 0 var(--kante); border-bottom: 1px solid var(--linie); }
.zeichen { display: flex; align-items: center; gap: 10px; text-decoration: none; color: var(--marke); }
.zeichen svg { width: 34px; height: 34px; }
.zeichen b { font: 800 1.75rem/1 'Big Shoulders', sans-serif; letter-spacing: .02em; text-transform: uppercase; }
.zeichen small { display: block; font-size: .75rem; color: var(--leise); letter-spacing: .02em; }
.verein nav ul { display: flex; gap: 26px; list-style: none; font-weight: 550; font-size: .9375rem; }
.verein nav a { text-decoration: none; }
.verein nav a:hover { text-decoration: underline; text-underline-offset: 4px; }
.spenden-knopf { display: inline-flex; align-items: center; height: 44px; padding: 0 20px; background: var(--signal); color: var(--tinte); font-weight: 700; text-decoration: none; border-radius: 4px; transition: background-color var(--ui) var(--kurve); }
.spenden-knopf:hover { background: var(--signal-dunkel); }

/* ---------- Sektion ---------- */
.spende { display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(0, 1fr); min-height: calc(100svh - 70px); }

/* Linke Tafel: Wirkung */
.tafel { position: relative; overflow: hidden; isolation: isolate; background: var(--marke); color: #fff; padding: clamp(28px, 4vw, 64px) var(--kante) clamp(28px, 3.4vw, 52px); display: flex; flex-direction: column; gap: 16px; }
.tafel .muster { position: absolute; inset: 0; z-index: -1; width: 100%; height: 100%; opacity: .5; }
.tafel .ueber { color: var(--wasser); }
.tafel h1 { font: 800 clamp(2.75rem, 1.6rem + 3.6vw, 5rem)/.95 'Big Shoulders', sans-serif; text-transform: uppercase; letter-spacing: .005em; max-width: 11ch; text-wrap: balance; }
.wirkung { display: grid; gap: 6px; border-top: 1px solid rgba(143, 195, 232, .35); padding-top: 16px; }
.wirkung .label { color: var(--wasser); }
.betragszeile { display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap; }
.odo { display: inline-flex; align-items: baseline; font: 800 clamp(4.5rem, 3rem + 4.4vw, 7.25rem)/1 'Big Shoulders', sans-serif; font-variant-numeric: tabular-nums; letter-spacing: -.01em; }
.odo .stelle { display: inline-block; height: 1.1em; overflow: hidden; line-height: 1.1; mask-image: linear-gradient(transparent, #000 7%, #000 93%, transparent); -webkit-mask-image: linear-gradient(transparent, #000 7%, #000 93%, transparent); }
.odo .streifen { display: flex; flex-direction: column; transition: translate var(--odo) var(--kurve); transition-delay: calc(var(--s, 0) * 45ms); }
.odo .streifen span { height: 1.1em; display: block; text-align: center; }
.odo .zeichen-fest { display: inline-block; }
.odo .euro { font-size: .5em; }
.odo .luft { display: inline-block; width: .1em; }
.rhythmus-text { font-size: 1.125rem; font-weight: 500; color: rgba(255, 255, 255, .86); }
.wirkungssatz { font: 500 clamp(1.25rem, 1rem + .9vw, 1.75rem)/1.3 'Archivo', sans-serif; max-width: 32ch; min-height: 3.9em; text-wrap: pretty; }
.wirkungssatz b { color: var(--wasser); font-weight: 700; }
.wirkung.ungueltig .odo { opacity: .45; }
.wirkung.ungueltig .wirkungssatz { color: #ffd9d4; }
.wechsel-raus { opacity: 0; translate: 0 6px; }
.wirkungssatz, .bild svg { transition: opacity 260ms var(--kurve), translate 260ms var(--kurve); }
.bild { position: relative; flex: 1 1 auto; min-height: 120px; max-height: 300px; margin: 0 calc(var(--kante) * -.5) -16px; }
.bild svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.bild svg [stroke] { vector-effect: non-scaling-stroke; }

/* Kampagne: Spendenziel */
.ziel { display: none; gap: 8px; padding: 16px 18px; background: rgba(255, 255, 255, .08); border: 1px solid rgba(255, 255, 255, .18); border-radius: 6px; max-width: 34rem; }
[data-variante="kampagne"] .ziel { display: grid; }
.ziel-kopf { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; flex-wrap: wrap; }
.ziel-kopf b { font: 800 1.5rem/1 'Big Shoulders', sans-serif; text-transform: uppercase; letter-spacing: .01em; }
.ziel-kopf span { font-size: .875rem; color: rgba(255, 255, 255, .78); }
.balken { position: relative; height: 12px; background: rgba(255, 255, 255, .16); border-radius: 6px; overflow: hidden; }
.balken i { position: absolute; top: 0; bottom: 0; left: 0; border-radius: 6px; }
.balken .ist { width: var(--ist); background: #fff; }
.balken .du { left: var(--ist); width: var(--du); background: repeating-linear-gradient(-45deg, var(--wasser) 0 4px, #b9daf1 4px 8px); border-radius: 0 6px 6px 0; transition: width var(--wechsel) var(--kurve); }
.ziel p { font-size: .9375rem; font-variant-numeric: tabular-nums; }
.ziel p b { font-weight: 700; }

/* Rechte Seite: Formular */
.formular { padding: clamp(28px, 4vw, 64px) var(--kante); background: var(--grund); display: flex; flex-direction: column; gap: 22px; max-width: 44rem; }
.formular h2 { font: 800 clamp(2rem, 1.2rem + 2vw, 3rem)/1 'Big Shoulders', sans-serif; text-transform: uppercase; color: var(--marke); }
.formular .vorspann { font-size: 1.0625rem; line-height: 1.55; color: var(--leise); max-width: 46ch; margin-top: 8px; }
fieldset { border: 0; min-width: 0; }
legend { margin-bottom: 10px; }

/* Rhythmus: Segment mit gleitendem Indikator */
.rhythmus { position: relative; display: grid; grid-template-columns: 1fr 1fr; background: var(--hell); border-radius: 6px; padding: 4px; isolation: isolate; }
.rhythmus::before { content: ''; position: absolute; z-index: -1; top: 4px; bottom: 4px; left: 4px; width: calc(50% - 4px); background: var(--marke); border-radius: 4px; box-shadow: 0 4px 14px rgba(0, 46, 109, .22); transition: translate var(--wechsel) var(--kurve); }
.rhythmus:has(input[value="monatlich"]:checked)::before { translate: 100% 0; }
.rhythmus label { position: relative; display: flex; align-items: center; justify-content: center; gap: 8px; height: 48px; font-weight: 650; cursor: pointer; color: var(--marke); transition: color var(--ui) var(--kurve); }
.rhythmus label small { font-size: .75rem; font-weight: 600; padding: 3px 7px; border-radius: 99px; background: var(--hell2); color: var(--marke); transition: background-color var(--ui) var(--kurve); }
.rhythmus label:has(:checked) { color: #fff; }
.rhythmus label:has(:checked) small { background: rgba(255, 255, 255, .18); color: #fff; }
.rhythmus label:has(:focus-visible) { outline: 3px solid var(--marke); outline-offset: 3px; border-radius: 4px; }
.rhythmus input, .betraege input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
[data-variante="kampagne"] .rhythmus-feld { display: none; }

/* Beträge als Segmente */
.betraege { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.betrag { position: relative; display: flex; flex-direction: column; gap: 6px; padding: 14px 12px 14px; min-height: 132px; border: 1.5px solid var(--linie); border-radius: 6px; cursor: pointer; transition: border-color var(--ui) var(--kurve), background-color var(--ui) var(--kurve), color var(--ui) var(--kurve); }
.betrag b { font: 800 2.25rem/1 'Big Shoulders', sans-serif; color: var(--marke); font-variant-numeric: tabular-nums; transition: color var(--ui) var(--kurve); }
.betrag b small { font-size: .55em; margin-left: 2px; }
.betrag span { font-size: .875rem; line-height: 1.35; color: var(--leise); transition: color var(--ui) var(--kurve); }
.betrag::after { content: ''; position: absolute; top: 10px; right: 10px; width: 18px; height: 18px; border-radius: 50%; border: 1.5px solid var(--linie); background: #fff center / 12px no-repeat; transition: background-color var(--ui) var(--kurve), border-color var(--ui) var(--kurve); }
.betrag:hover { border-color: var(--marke); }
.betrag:has(:checked) { background: var(--marke); border-color: var(--marke); }
.betrag:has(:checked) b, .betrag:has(:checked) span { color: #fff; }
.betrag:has(:checked)::after { border-color: #fff; background-color: #fff; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 6.2 5 8.6l4.6-5' fill='none' stroke='%23002e6d' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); }
.betrag:has(:focus-visible) { outline: 3px solid var(--marke); outline-offset: 3px; }
.betraege .neu { animation: auf var(--wechsel) var(--kurve) both; animation-delay: calc(var(--i) * 40ms); }
@keyframes auf { from { opacity: 0; translate: 0 8px; } }

/* Freier Betrag */
.frei { display: grid; gap: 6px; }
.frei label { font-weight: 650; font-size: .9375rem; display: flex; justify-content: space-between; gap: 10px; }
.frei label small { font-weight: 400; color: var(--leise); }
.eingabe { position: relative; }
.eingabe input { width: 100%; height: 54px; padding: 0 52px 0 16px; font: 700 1.25rem/1 'Archivo', sans-serif; font-variant-numeric: tabular-nums; border: 1.5px solid var(--linie); border-radius: 6px; background: var(--grund); outline: none; transition: border-color var(--ui) var(--kurve), box-shadow var(--ui) var(--kurve), background-color var(--ui) var(--kurve); }
.eingabe input::placeholder { color: #8a919a; font-weight: 500; }
.eingabe input:focus-visible { border-color: var(--marke); box-shadow: 0 0 0 3px rgba(0, 46, 109, .18); }
.eingabe::after { content: '€'; position: absolute; right: 18px; top: 50%; translate: 0 -50%; font: 700 1.25rem/1 'Archivo', sans-serif; color: var(--leise); }
.frei.aktiv input { border-color: var(--marke); background: var(--hell); }
.frei.falsch input { border-color: var(--fehler); background: #fdf1f0; }
.frei.falsch input:focus-visible { box-shadow: 0 0 0 3px rgba(179, 38, 30, .16); }
.hinweis { min-height: 1.4em; font-size: .9375rem; line-height: 1.4; color: var(--leise); }
.frei.falsch .hinweis { color: var(--fehler); font-weight: 600; }
.hinweis b { color: var(--marke); }

/* Bestätigung, Absenden */
.haken { display: flex; gap: 12px; align-items: flex-start; font-size: .9375rem; line-height: 1.45; cursor: pointer; }
.haken input { appearance: none; flex: none; width: 22px; height: 22px; margin-top: 1px; border: 1.5px solid var(--marke); border-radius: 4px; display: grid; place-items: center; cursor: pointer; transition: background-color var(--ui) var(--kurve); }
.haken input::after { content: ''; width: 11px; height: 6px; border: 2px solid #fff; border-top: 0; border-right: 0; rotate: -45deg; translate: 0 -1px; scale: 0; transition: scale var(--ui) var(--kurve); }
.haken input:checked { background: var(--marke); }
.haken input:checked::after { scale: 1; }
.haken small { display: block; color: var(--leise); font-size: .8125rem; margin-top: 2px; }
.absenden { display: flex; align-items: center; justify-content: center; gap: 12px; width: 100%; height: 60px; border-radius: 6px; background: var(--signal); color: var(--tinte); font: 750 1.1875rem/1 'Archivo', sans-serif; transition: background-color var(--ui) var(--kurve), scale 150ms var(--kurve); }
.absenden svg { width: 20px; height: 20px; transition: translate var(--ui) var(--kurve); }
.absenden:hover { background: var(--signal-dunkel); }
.absenden:hover svg { translate: 3px 0; }
.absenden:active, .absenden.drueckt { scale: .98; }
.sicher { display: flex; gap: 8px; align-items: center; justify-content: center; font-size: .8125rem; color: var(--leise); margin-top: -10px; }
.sicher svg { width: 14px; height: 14px; flex: none; }
.danke { display: none; gap: 6px; padding: 16px 18px; border-radius: 6px; background: #e6f3ec; border: 1.5px solid var(--gut); color: #13442c; }
.danke b { font-size: 1.0625rem; }
.danke p { font-size: .9375rem; line-height: 1.45; }
.formular.fertig .danke { display: grid; animation: auf 320ms var(--kurve) both; }

/* Vertrauenszeile */
.vertrauen { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0; border-top: 1px solid var(--linie); background: var(--hell); }
.vertrauen > div { padding: 22px var(--kante) 26px; display: grid; gap: 8px; align-content: start; border-right: 1px solid var(--linie); }
.vertrauen > div:last-child { border-right: 0; }
.vertrauen h3 { font: 800 1.25rem/1.05 'Big Shoulders', sans-serif; text-transform: uppercase; letter-spacing: .02em; color: var(--marke); display: flex; gap: 8px; align-items: center; }
.vertrauen h3 svg { width: 20px; height: 20px; flex: none; }
.vertrauen p { font-size: .9375rem; line-height: 1.5; color: var(--leise); }
.vertrauen a { font-weight: 650; color: var(--marke); }
.verwendung { display: flex; height: 12px; border-radius: 6px; overflow: hidden; margin-top: 4px; }
.verwendung i { display: block; }
.legende { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: .8125rem; color: var(--leise); font-variant-numeric: tabular-nums; }
.legende span::before { content: ''; display: inline-block; width: 9px; height: 9px; margin-right: 6px; background: var(--f); }
.siegel { display: flex; gap: 14px; align-items: center; }
.siegel svg { width: 76px; height: 76px; flex: none; }
.siegel p { font-size: .875rem; }
.muster-hinweis { display: inline-block; font-size: .6875rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; padding: 3px 6px; border: 1.5px dashed var(--fehler); color: var(--fehler); border-radius: 3px; }

/* ---------- Demo-Pult ---------- */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 20; display: grid; gap: 8px; padding: 12px 14px; background: rgba(255, 255, 255, .95); border: 1px solid var(--linie); border-radius: 8px; box-shadow: 0 10px 30px rgba(0, 46, 109, .12); font: 600 .6875rem/1.45 'Archivo', sans-serif; letter-spacing: .08em; text-transform: uppercase; color: var(--leise); max-width: 340px; }
.pult .zeile { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.pult button { font: inherit; letter-spacing: inherit; text-transform: inherit; height: 26px; padding: 0 10px; border: 1px solid var(--linie); border-radius: 99px; color: var(--marke); }
.pult button[aria-pressed="true"] { background: var(--marke); color: #fff; border-color: var(--marke); }
.pult b { color: var(--tinte); font-weight: 700; }

.zeiger { position: fixed; left: 0; top: 0; z-index: 40; width: 22px; height: 22px; pointer-events: none; display: none; transition: transform 620ms var(--kurve), scale 150ms var(--kurve); }
.vorfuehrung .zeiger { display: block; }
.zeiger.drueckt { scale: .82; }

/* ---------- Reduzierte Bewegung ---------- */
.reduziert .odo .streifen, .reduziert .rhythmus::before, .reduziert .balken .du, .reduziert .zeiger { transition: none; }
.reduziert .wirkungssatz, .reduziert .bild svg { transition: opacity 120ms linear; translate: none; }
.reduziert .betraege .neu, .reduziert .formular.fertig .danke { animation: none; }
.reduziert .absenden, .reduziert .absenden svg { transition: none; scale: 1; translate: none; }

/* ---------- Touch ---------- */
@media (hover: none) {
  .betrag:hover { border-color: var(--linie); }
  .betrag:has(:checked):hover { border-color: var(--marke); }
  .absenden:hover { background: var(--signal); }
  .absenden:hover svg { translate: none; }
  .spenden-knopf:hover { background: var(--signal); }
}

/* ---------- Schmal ---------- */
@media (max-width: 1180px) {
  .betraege { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .betrag { min-height: 0; }
  .vertrauen { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .vertrauen > div:nth-child(2) { border-right: 0; }
  .vertrauen > div:nth-child(-n+2) { border-bottom: 1px solid var(--linie); }
}
@media (max-width: 480px) {
  .rhythmus label { flex-direction: column; gap: 3px; height: 54px; line-height: 1; }
  .rhythmus label small { font-size: .6875rem; padding: 2px 6px; }
}
@media (max-width: 820px) {
  .verein nav { display: none; }
  .verein { height: 60px; }
  .zeichen small { display: none; }
  .spende { grid-template-columns: 1fr; min-height: 0; }
  .tafel { padding-top: 28px; gap: 12px; }
  .tafel h1 { font-size: clamp(2.5rem, 11vw, 3.5rem); }
  .odo { font-size: clamp(4rem, 20vw, 5.5rem); }
  .wirkungssatz { font-size: 1.1875rem; min-height: 2.6em; }
  .bild { flex: none; height: 116px; min-height: 0; }
  .formular { max-width: none; gap: 20px; }
  .vertrauen { grid-template-columns: 1fr; }
  .vertrauen > div { border-right: 0; border-bottom: 1px solid var(--linie); }
  .betrag { padding: 12px 11px; }
  .betrag b { font-size: 2rem; }
}

/* ---------- Vorschau 960 × 720 ---------- */
.vorschau body { overflow: hidden; }
.vorschau .vertrauen, .vorschau .formular .vorspann, .vorschau .verein nav { display: none; }
.vorschau .spende { min-height: 650px; height: 650px; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); }
.vorschau .tafel { padding-top: 28px; padding-bottom: 24px; }
.vorschau .tafel h1 { font-size: 3.125rem; max-width: none; }
.vorschau .wirkungssatz { min-height: 2.6em; }
.vorschau .odo { font-size: 5.75rem; }
.vorschau .wirkungssatz { font-size: 1.3125rem; }
.vorschau .formular { padding-top: 30px; gap: 16px; }
.vorschau .betrag { min-height: 0; padding: 11px; }
.vorschau .bild { min-height: 90px; }
.vorschau .tafel { gap: 12px; }
</style>
</head>
<body>
<header class="verein">
  <a class="zeichen" href="#spenden" aria-label="Priel e.V., Startseite">
    <svg viewBox="0 0 34 34" aria-hidden="true"><rect width="34" height="34" rx="6" fill="#002e6d"/><path d="M5 22c5 0 5-6 10-6s5 8 10 8 4-4 4-4" fill="none" stroke="#8fc3e8" stroke-width="2.4" stroke-linecap="round"/><path d="M5 14c4 0 4-4 8-4" fill="none" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>
    <span><b>Priel</b><small>Verein für Watt und Salzwiese · Husum</small></span>
  </a>
  <nav aria-label="Hauptnavigation"><ul><li><a href="#spenden">Projekte</a></li><li><a href="#spenden">Mitmachen</a></li><li><a href="#vertrauen">Transparenz</a></li><li><a href="#spenden">Über uns</a></li></ul></nav>
  <a class="spenden-knopf" href="#spenden">Spenden</a>
</header>

<main id="spenden" data-variante="formular">
  <section class="spende" aria-labelledby="spende-titel">
    <div class="tafel">
      <svg class="muster" viewBox="0 0 800 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
        <g fill="none" stroke="#8fc3e8" stroke-width="1.2" opacity=".35">
          <path d="M-20 120c120 10 160 90 260 80s150-110 260-90 130 120 320 100"/>
          <path d="M-20 170c140 20 160 100 270 90s150-110 250-80 140 110 320 100" opacity=".7"/>
          <path d="M-20 560c90-20 170 30 220 100s110 60 190 20 140-90 240-40 120 70 200 50"/>
          <path d="M-20 600c90-10 160 40 210 110s120 60 200 20 130-80 230-30 130 70 200 50" opacity=".7"/>
          <path d="M520 -20c-30 90 40 140 10 230s-110 120-80 200 120 90 110 170" opacity=".5"/>
        </g>
      </svg>
      <p class="label ueber">Spenden für die Westküste</p>
      <h1 id="spende-titel">Das Watt braucht viele Hände</h1>
      <div class="ziel" aria-live="polite">
        <div class="ziel-kopf"><b>Aktion: Lahnung Hallig-Ufer</b><span>noch 23 Tage</span></div>
        <div class="balken" role="img" aria-label="18.420 € von 25.000 € gesammelt"><i class="ist"></i><i class="du"></i></div>
        <p><b>18.420 €</b> von 25.000 € · 214 Spenden · <span class="ziel-du"></span></p>
      </div>
      <div class="bild" aria-hidden="true"></div>
      <div class="wirkung">
        <p class="label">Deine Spende</p>
        <div class="betragszeile">
          <span class="odo" aria-hidden="true"></span>
          <span class="rhythmus-text"></span>
        </div>
        <p class="sr" aria-live="polite" id="betrag-sr"></p>
        <p class="wirkungssatz" aria-live="polite"></p>
      </div>
    </div>

    <form class="formular" novalidate>
      <div>
        <h2>Jetzt spenden</h2>
        <p class="vorspann">Priel e.V. pflanzt Dünen, baut Lahnungen und bringt Schulklassen ins Watt. Jeder Betrag fließt in Projekte zwischen Husum und den Halligen.</p>
      </div>
      <fieldset class="rhythmus-feld">
        <legend class="label">Wie oft?</legend>
        <div class="rhythmus">
          <label><input type="radio" name="rhythmus" value="einmalig" checked>Einmalig</label>
          <label><input type="radio" name="rhythmus" value="monatlich">Monatlich <small>wirkt dauerhaft</small></label>
        </div>
      </fieldset>
      <fieldset>
        <legend class="label">Betrag wählen</legend>
        <div class="betraege"></div>
      </fieldset>
      <div class="frei">
        <label for="frei">Anderer Betrag <small class="frei-min">ab 5 € möglich</small></label>
        <div class="eingabe"><input id="frei" name="frei" type="text" inputmode="decimal" autocomplete="off" placeholder="z. B. 25" aria-describedby="frei-hinweis"></div>
        <p class="hinweis" id="frei-hinweis"></p>
      </div>
      <label class="haken"><input type="checkbox" name="quittung" checked><span>Zuwendungsbestätigung zusenden<small>Kommt im Februar für das ganze Jahr. Wir brauchen dafür im nächsten Schritt deine Adresse.</small></span></label>
      <button class="absenden" type="submit"><span class="absenden-text">35 € spenden</span><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M3 10h13M11 5l5 5-5 5" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
      <p class="sicher"><svg viewBox="0 0 14 14" aria-hidden="true"><rect x="2" y="6" width="10" height="7" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M4.5 6V4.5a2.5 2.5 0 0 1 5 0V6" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>Verschlüsselt · Lastschrift, Überweisung oder Karte im nächsten Schritt</p>
      <div class="danke" role="status" aria-live="polite"><b></b><p></p></div>
    </form>
  </section>

  <section class="vertrauen" id="vertrauen" aria-label="Transparenz">
    <div>
      <h3><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 2 3 5v5c0 4 3 7 7 8 4-1 7-4 7-8V5z" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="m6.8 10 2.2 2.2 4.2-4.4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>Gemeinnützig</h3>
      <p>Priel e.V. ist als gemeinnützig anerkannt. Spenden kannst du in der Regel steuerlich absetzen.</p>
    </div>
    <div>
      <h3><svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4 2h9l3 3v13H4z" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M7 8h6M7 11h6M7 14h4" stroke="currentColor" stroke-width="1.4"/></svg>Spendenquittung</h3>
      <p>Die Zuwendungsbestätigung kommt im Februar. Bis 300 € genügen dem Finanzamt der Kontoauszug und unser vereinfachter Spendenbeleg.</p>
    </div>
    <div>
      <h3>Mittelverwendung 2025</h3>
      <div class="verwendung" role="img" aria-label="84 Prozent Projekte, 9 Prozent Verwaltung, 7 Prozent Spendenwerbung"><i style="width:84%;background:#002e6d"></i><i style="width:9%;background:#8fc3e8"></i><i style="width:7%;background:#c1c1ba"></i></div>
      <p class="legende"><span style="--f:#002e6d">84 % Projekte</span><span style="--f:#8fc3e8">9 % Verwaltung</span><span style="--f:#c1c1ba">7 % Werbung</span></p>
      <p><a href="#vertrauen">Jahresbericht 2025 lesen</a></p>
    </div>
    <div class="siegel">
      <svg viewBox="0 0 76 76" aria-hidden="true"><circle cx="38" cy="38" r="35" fill="none" stroke="#002e6d" stroke-width="1.5" stroke-dasharray="3 3"/><circle cx="38" cy="38" r="27" fill="#fff" stroke="#002e6d" stroke-width="1.5"/><path d="m28 36 7 6 13-14" fill="none" stroke="#002e6d" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><text x="38" y="54" text-anchor="middle" font-family="Archivo, sans-serif" font-size="7.5" font-weight="700" letter-spacing="1.2" fill="#002e6d">MUSTER</text></svg>
      <div><span class="muster-hinweis">Beispiel, kein echtes Siegel</span><p style="margin-top:6px">Hier steht das Spendensiegel, wenn der Verein eines führt. Fragen zur Spende: <a href="mailto:spenden@priel.example">spenden@priel.example</a></p></div>
    </div>
  </section>
</main>

<div class="pult">
  <div class="zeile">Variante
    <button type="button" data-v="formular" aria-pressed="true">Einmalig / monatlich</button>
    <button type="button" data-v="kampagne" aria-pressed="false">Aktion mit Ziel</button>
  </div>
  <div>Kurve <b>0.645, 0.045, 0.355, 1</b> · UI <b>180</b> · Indikator <b>600</b> · Odometer <b>700 ms</b>, <b>45 ms</b> je Ziffer</div>
</div>
<svg class="zeiger" data-zeiger viewBox="0 0 22 22" aria-hidden="true"><path d="M3 2l15 8.2-6.6 1.6L8.6 19z" fill="#222520" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, el = document) => el.querySelector(s), $$ = (s, el = document) => [...el.querySelectorAll(s)];

  /* ---------- Beträge und Wirkung (Zahlen aus dem Jahresbericht des Vereins übernehmen) ---------- */
  const BILD = { duene: 'duene', klasse: 'klasse', zaehlen: 'zaehlen', lahnung: 'lahnung' };
  const STUFEN = {
    einmalig: [
      { wert: 15, kurz: '60 Strandhafer-Setzlinge', satz: '60 Strandhafer-Setzlinge, die eine Düne bei St. Peter-Ording festhalten.', bild: 'duene' },
      { wert: 35, kurz: 'Watt-Unterricht für eine Klasse', satz: 'ein Vormittag Watt-Unterricht mit Kescher und Lupe für eine Schulklasse.', bild: 'klasse' },
      { wert: 70, kurz: 'Ein Zähltag auf der Hallig', satz: 'Fähre, Fernrohr und Proviant für einen Zähltag der Vogelwarte auf der Hallig.', bild: 'zaehlen' },
      { wert: 150, kurz: 'Ein Meter neue Lahnung', satz: 'ein Meter Lahnung aus Pfählen und Reisig, hinter der neue Salzwiese wächst.', bild: 'lahnung' },
    ],
    monatlich: [
      { wert: 5, kurz: '240 Setzlinge im Jahr', satz: 'jedes Jahr 240 Strandhafer-Setzlinge für unsere Dünen.', bild: 'duene' },
      { wert: 10, kurz: 'Drei Klassen im Jahr', satz: 'drei Schulklassen im Jahr, die das Watt zum ersten Mal mit eigenen Händen erleben.', bild: 'klasse' },
      { wert: 20, kurz: 'Drei Zähltage im Jahr', satz: 'drei Zähltage im Jahr, damit wir wissen, wie es Ringelgans und Austernfischer geht.', bild: 'zaehlen' },
      { wert: 40, kurz: 'Drei Meter Lahnung im Jahr', satz: 'drei Meter Lahnung im Jahr. Das sind gut 60 Quadratmeter neue Salzwiese.', bild: 'lahnung' },
    ],
    kampagne: [
      { wert: 20, kurz: 'Vier Lärchenpfähle', satz: 'vier Lärchenpfähle für die neue Lahnung am Hallig-Ufer.', bild: 'lahnung' },
      { wert: 50, kurz: 'Ein Bund Reisig je Meter', satz: 'das Reisig für einen Meter Lahnung, gebunden von unserer Freitagsgruppe.', bild: 'lahnung' },
      { wert: 75, kurz: 'Ein halber Meter Lahnung', satz: 'ein halber Meter Lahnung, Pfähle und Reisig zusammen.', bild: 'lahnung' },
      { wert: 150, kurz: 'Ein ganzer Meter Lahnung', satz: 'ein ganzer Meter Lahnung. Dahinter wächst in drei Jahren Salzwiese.', bild: 'lahnung' },
    ],
  };
  const START = { einmalig: 35, monatlich: 10, kampagne: 50 };
  const satzSchluessel = () => z.variante === 'kampagne' ? 'kampagne' : z.rhythmus;
  const MIN = 5, MAX = 10000;
  // Wirkung für freie Beträge: Jahresbetrag in die größte passende Einheit umrechnen
  function freieWirkung(betrag, rhythmus) {
    if (z.variante === 'kampagne') { const m = Math.floor(betrag / 15) / 10; return { satz: betrag >= 150 ? `${zahl(m)} Meter Lahnung am Hallig-Ufer.` : `${zahl(Math.max(1, Math.floor(betrag / 5)))} ${betrag < 10 ? 'Lärchenpfahl' : 'Lärchenpfähle'} für die neue Lahnung.`, bild: 'lahnung' }; }
    const jahr = rhythmus === 'monatlich' ? betrag * 12 : betrag, vorsatz = rhythmus === 'monatlich' ? 'im Jahr ' : '';
    if (jahr >= 150) { const m = Math.floor(jahr / 150); return { satz: `${vorsatz}${m === 1 ? 'ein Meter' : zahl(m) + ' Meter'} Lahnung für neue Salzwiese.`, bild: 'lahnung' }; }
    if (jahr >= 70) { const z = Math.floor(jahr / 70); return { satz: `${vorsatz}${z === 1 ? 'ein Zähltag' : zahl(z) + ' Zähltage'} der Vogelwarte auf der Hallig.`, bild: 'zaehlen' }; }
    if (jahr >= 35) { const k = Math.floor(jahr / 35); return { satz: `${vorsatz}${k === 1 ? 'eine Schulklasse' : zahl(k) + ' Schulklassen'} einen Vormittag im Watt.`, bild: 'klasse' }; }
    return { satz: `${vorsatz}${zahl(Math.floor(jahr * 4))} Strandhafer-Setzlinge für die Dünen.`, bild: 'duene' };
  }
  const zahl = n => n.toLocaleString('de-DE');
  const euro = (n, kurz = true) => (Number.isInteger(n) && kurz ? zahl(n) : n.toLocaleString('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 })) + ' €';

  /* ---------- Bildwelt: vier Vignetten als Strichzeichnung ---------- */
  const W = 800, H = 250, S = 'stroke="#fff" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"';
  const watt = (y = 200) => `<path d="M0 ${y}C120 ${y - 6} 200 ${y + 10} 330 ${y + 2}S600 ${y - 8} ${W} ${y}" ${S} opacity=".6"/><path d="M60 ${y + 26}c60-10 110 8 180 0s120-14 200 0" ${S} opacity=".35"/><path d="M420 ${y + 40}c70-8 140 6 220-2s90-10 160 0" ${S} opacity=".3"/>`;
  function halm(x, y, h, n = 5) { let p = ''; for (let i = 0; i < n; i++) { const a = (i - (n - 1) / 2) * 0.32, ex = x + Math.sin(a) * h, ey = y - Math.cos(a) * h; p += `M${x} ${y}Q${x + Math.sin(a) * h * .3} ${y - h * .6} ${ex.toFixed(1)} ${ey.toFixed(1)}`; } return p; }
  function figur(x, y, s = 1, kescher = false) {
    const k = s * 30;
    return `<circle cx="${x}" cy="${y - k * 1.55}" r="${k * .22}" ${S}/><path d="M${x} ${y - k * 1.3}V${y - k * .55}M${x} ${y - k * .55}l${-k * .25} ${k * .55}M${x} ${y - k * .55}l${k * .25} ${k * .55}M${x} ${y - k * 1.1}l${-k * .35} ${k * .35}M${x} ${y - k * 1.1}l${k * .4} ${k * .2}" ${S}/>` + (kescher ? `<path d="M${x + k * .4} ${y - k * .9}l${k * .9} ${k * .7}" ${S}/><ellipse cx="${x + k * 1.42}" cy="${y - k * .08}" rx="${k * .26}" ry="${k * .16}" ${S}/>` : '');
  }
  const VIGNETTEN = {
    duene: () => { let g = `<path d="M-10 210C140 200 220 120 360 118S560 196 820 204" ${S}/>`; let p = ''; [[150, 182, 46], [210, 158, 54], [270, 136, 58], [330, 122, 62], [390, 124, 58], [450, 140, 54], [510, 160, 50], [570, 180, 44], [240, 168, 38], [420, 150, 40], [300, 146, 36]].forEach(([x, y, h]) => p += halm(x, y, h)); return g + `<path d="${p}" ${S}/>` + watt(228); },
    klasse: () => watt(206) + [[250, 206, 1, true], [320, 210, .9, false], [380, 206, 1.05, true], [460, 212, .85, false], [520, 206, 1, false]].map(a => figur(...a)).join('') + `<path d="M590 160c18-14 40-14 56 0M600 150c14-8 28-8 38 0" ${S} opacity=".55"/>`,
    zaehlen: () => `<path d="M440 206c30-26 70-34 120-34s110 10 150 34" ${S}/><path d="M560 172v-26l26-18 26 18v26M572 172v-14h12v14" ${S}/>` + watt(206) + `<path d="M190 206l30-60 30 60M220 146v-16" ${S}/><rect x="196" y="112" width="48" height="18" rx="8" ${S}/><path d="M244 121h16" ${S}/>` + [[330, 70], [370, 52], [404, 80], [446, 60], [300, 96], [480, 88]].map(([x, y]) => `<path d="M${x - 12} ${y}q6-7 12 0q6-7 12 0" ${S}/>`).join(''),
    lahnung: () => { let p = ''; for (let i = 0; i < 14; i++) { const x = 150 + i * 36; p += `M${x} 178v-34M${x + 10} 210v-38`; } let r = ''; for (let i = 0; i < 13; i++) { const x = 150 + i * 36; r += `M${x} 162l8 -6 8 6 8 -6 8 6 4 -3`; } return `<path d="${p}" ${S}/><path d="${r}" ${S} opacity=".7"/><path d="M150 152h470M160 190h470" ${S} opacity=".35"/>` + watt(222) + `<path d="M640 120c20-10 40-10 60 0M650 108c16-6 30-6 44 0" ${S} opacity=".5"/>`; },
  };
  const bildEl = $('.bild');
  Object.entries(VIGNETTEN).forEach(([id, f]) => bildEl.insertAdjacentHTML('beforeend', `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMax meet" data-bild="${id}" style="opacity:0">${f()}</svg>`));

  /* ---------- Odometer ---------- */
  const odo = $('.odo');
  let odoText = '';
  function odometer(text) {
    const zeichen = [...text];
    const gleicheForm = odoText.length === text.length && [...odoText].every((c, i) => /\d/.test(c) === /\d/.test(zeichen[i]));
    if (!gleicheForm) {
      odo.innerHTML = zeichen.map((c, i) => /\d/.test(c)
        ? `<span class="stelle"><span class="streifen" style="--s:${zeichen.length - 1 - i}">${[...'0123456789'].map(z => `<span>${z}</span>`).join('')}</span></span>`
        : c === ' ' ? '<span class="luft"></span>' : `<span class="zeichen-fest${c === '€' ? ' euro' : ''}">${c}</span>`).join('');
      // neue Stellen starten bei 0 und drehen dann zum Ziel
      $$('.streifen', odo).forEach(s => s.style.translate = '0 0');
      odo.offsetWidth;
    }
    let k = 0;
    zeichen.forEach(c => { if (/\d/.test(c)) { $$('.streifen', odo)[k].style.translate = `0 ${-c * 1.1}em`; k++; } });
    odoText = text;
  }

  /* ---------- Zustand ---------- */
  const form = $('.formular'), freiFeld = $('#frei'), freiBox = $('.frei'), hinweis = $('#frei-hinweis');
  const startV = new URLSearchParams(location.search).get('v');
  const z = { variante: startV === 'kampagne' ? 'kampagne' : 'formular', rhythmus: 'einmalig', wert: 35, preset: 35, frei: false, ungueltig: false, freiBeruehrt: false, bild: '' };

  function baueBetraege(neu) {
    const box = $('.betraege');
    box.innerHTML = STUFEN[satzSchluessel()].map((s, i) => `<label class="betrag${neu ? ' neu' : ''}" style="--i:${i}"><input type="radio" name="betrag" value="${s.wert}" ${!z.frei && s.wert === z.wert ? 'checked' : ''}><b>${s.wert}<small>${z.rhythmus === 'monatlich' ? ' €/M' : ' €'}</small></b><span>${s.kurz}</span></label>`).join('');
    $$('input', box).forEach(r => r.addEventListener('change', () => { z.frei = false; z.ungueltig = false; z.wert = z.preset = +r.value; freiFeld.value = ''; z.freiBeruehrt = false; pruefeFrei(false); zeige(); }));
  }
  function parse(s) {
    s = s.trim().replace(/\s|€/g, '');
    if (!s) return { leer: true };
    if (!/^\d{1,3}(\.\d{3})*(,\d*)?$|^\d+(,\d*)?$|^\d+\.\d{1,2}$/.test(s)) return { fehler: 'Bitte nur Ziffern eingeben, zum Beispiel 25 oder 25,50.' };
    let n;
    if (/^\d+\.\d{1,2}$/.test(s)) n = parseFloat(s);   // 25.50 als Dezimalpunkt lesen
    else { const [ganz, dez = ''] = s.replace(/\./g, '').split(','); if (dez.length > 2) return { fehler: 'Höchstens zwei Stellen nach dem Komma.' }; n = parseFloat(ganz + '.' + (dez || '0')); }
    if (n < MIN) return { fehler: `Online ab ${MIN} € möglich. Kleinere Beträge gern per Überweisung.`, n };
    if (n > MAX) return { fehler: `Ab ${zahl(MAX)} € melden wir uns persönlich: spenden@priel.example`, n };
    return { n: Math.round(n * 100) / 100 };
  }
  // Fehler erst nach Verlassen des Feldes oder beim Absenden zeigen
  function pruefeFrei(zeigen) {
    const r = parse(freiFeld.value), falsch = !!r.fehler && (zeigen || z.freiBeruehrt);
    freiBox.classList.toggle('falsch', falsch);
    freiBox.classList.toggle('aktiv', z.frei && !r.fehler && !r.leer);
    freiFeld.setAttribute('aria-invalid', falsch ? 'true' : 'false');
    hinweis.innerHTML = falsch ? r.fehler : (z.frei && r.n ? `Danke! <b>${euro(r.n)}</b> ${z.rhythmus === 'monatlich' ? 'pro Monat' : 'einmalig'}.` : '');
    return r;
  }
  freiFeld.addEventListener('input', () => {
    const r = parse(freiFeld.value);
    if (r.leer) {   // Feld geleert: zurück zum zuletzt gewählten Vorschlag
      z.frei = false; z.ungueltig = false; z.wert = z.preset;
      const radio = $(`.betraege input[value="${z.preset}"]`); if (radio) radio.checked = true;
    } else {
      z.frei = true; z.ungueltig = !!r.fehler; $$('.betraege input').forEach(i => i.checked = false);
      if (r.n != null) z.wert = r.n;
    }
    pruefeFrei(false); zeige();
  });
  freiFeld.addEventListener('blur', () => { if (freiFeld.value.trim()) z.freiBeruehrt = true; pruefeFrei(false); });
  freiFeld.addEventListener('focus', () => { if (freiFeld.value.trim()) { z.frei = true; $$('.betraege input').forEach(i => i.checked = false); zeige(); } });

  function aktuelleWirkung() {
    if (z.frei) return freieWirkung(z.wert, z.rhythmus);
    return STUFEN[satzSchluessel()].find(s => s.wert === z.wert) || STUFEN[satzSchluessel()][0];
  }
  let satzUhr;
  function zeige() {
    const w = aktuelleWirkung(), mon = z.rhythmus === 'monatlich' && z.variante !== 'kampagne';
    odometer(euro(z.wert, true));
    $('#betrag-sr').textContent = `Deine Spende: ${euro(z.wert)}${mon ? ' monatlich' : ' einmalig'}`;
    $('.rhythmus-text').textContent = mon ? `pro Monat · ${euro(Math.round(z.wert * 12 * 100) / 100)} im Jahr` : 'einmalig';
    const satz = z.ungueltig ? 'Bitte prüfe den Betrag: online ab 5 € bis 10.000 €.' : `<b>=</b> ${w.satz.charAt(0).toUpperCase() + w.satz.slice(1)}`, el = $('.wirkungssatz');
    if (el.innerHTML !== satz) {
      clearTimeout(satzUhr);
      el.classList.add('wechsel-raus');
      satzUhr = setTimeout(() => { el.innerHTML = satz; el.classList.remove('wechsel-raus'); }, reduziert() ? 60 : 200);
    }
    if (w.bild !== z.bild) { z.bild = w.bild; $$('svg', bildEl).forEach(s => { const an = s.dataset.bild === w.bild; s.style.opacity = an ? 1 : 0; s.style.translate = an || reduziert() ? '0 0' : '0 8px'; }); }
    $('.absenden-text').textContent = z.ungueltig ? 'Betrag prüfen' : `${euro(z.wert)}${mon ? ' monatlich' : ''} spenden`;
    $('.wirkung').classList.toggle('ungueltig', z.ungueltig);
    // Kampagne: eigener Betrag als Vorschau im Balken
    const ist = 18420, ziel = 25000;
    $('.balken').style.setProperty('--ist', (ist / ziel * 100).toFixed(2) + '%');
    $('.balken').style.setProperty('--du', (Math.min(z.wert, ziel - ist) / ziel * 100).toFixed(2) + '%');
    $('.ziel-du').innerHTML = `mit dir <b>${euro(Math.min(ist + z.wert, 99999))}</b>`;
  }

  /* Rhythmus wechseln: eigene Beträge je Rhythmus, Auswahl wird sinnvoll übertragen */
  $$('input[name="rhythmus"]').forEach(r => r.addEventListener('change', () => {
    const alt = z.rhythmus; z.rhythmus = r.value;
    const i = STUFEN[alt].findIndex(s => s.wert === z.preset); z.preset = STUFEN[z.rhythmus][i >= 0 ? i : 1].wert;
    if (!z.frei) z.wert = z.preset;
    baueBetraege(true); pruefeFrei(false); zeige();
  }));

  /* Absenden */
  form.addEventListener('submit', e => {
    e.preventDefault();
    if (z.frei || freiFeld.value.trim()) {
      z.freiBeruehrt = true; const r = pruefeFrei(true);
      if (r.fehler || r.leer) { freiFeld.focus(); return; }
    }
    const mon = z.rhythmus === 'monatlich' && z.variante !== 'kampagne';
    form.classList.add('fertig');
    $('.danke b').textContent = `Danke! ${euro(z.wert)}${mon ? ' im Monat' : ''} für das Watt.`;
    $('.danke p').textContent = `Im Einsatz geht es jetzt zu Adresse und Zahlungsart (in der Demo nicht verbunden).${mon ? ' Die Lastschrift kannst du jederzeit widerrufen.' : ''}`;
  });
  form.addEventListener('change', () => form.classList.remove('fertig'));
  freiFeld.addEventListener('input', () => form.classList.remove('fertig'));

  /* Variante */
  function setzeVariante(v, schreiben) {
    z.variante = v === 'kampagne' ? 'kampagne' : 'formular';
    $('main').dataset.variante = z.variante;
    $$('.pult [data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === z.variante));
    $('input[value="einmalig"]').checked = true; z.rhythmus = 'einmalig'; z.wert = z.preset = START[satzSchluessel()]; z.frei = false; z.ungueltig = false; freiFeld.value = ''; baueBetraege(false);
    $('.formular h2').textContent = z.variante === 'kampagne' ? 'Lahnung mitbauen' : 'Jetzt spenden';
    if (schreiben) { const q = new URLSearchParams(location.search); q.set('v', z.variante); history.replaceState(null, '', '?' + q.toString().replace(/=(?=&|$)/g, '')); }
    pruefeFrei(false); zeige();
  }
  $$('.pult [data-v]').forEach(b => b.addEventListener('click', () => setzeVariante(b.dataset.v, true)));

  /* ---------- Vorführung ---------- */
  const zeiger = $('[data-zeiger]');
  let lauf = 0, angehalten = false;
  const warte = async (ms, id) => { let rest = ms; while (rest > 0) { await new Promise(r => setTimeout(r, 50)); if (id !== lauf) throw 0; if (!angehalten) rest -= 50; } };
  async function geheZu(el, id) {
    if (typeof el === 'string') el = $(el); if (!el) return null;
    const r = el.getBoundingClientRect();
    if (r.top < 0 || r.bottom > innerHeight) { el.scrollIntoView({ block: 'center', behavior: reduziert() ? 'auto' : 'smooth' }); await warte(500, id); }
    const q = el.getBoundingClientRect();
    zeiger.style.transform = `translate(${q.left + q.width * .5}px, ${q.top + q.height * .55}px)`;
    await warte(reduziert() ? 200 : 700, id); return el;
  }
  async function klick(el, id) {
    el = await geheZu(el, id); if (!el) return;
    zeiger.classList.add('drueckt'); el.classList.add('drueckt'); await warte(150, id);
    zeiger.classList.remove('drueckt'); el.classList.remove('drueckt');
    const ziel = el.matches('label') ? $('input', el) : el;
    if (ziel === freiFeld) ziel.focus(); else ziel.click();
  }
  async function tippe(text, id) { for (const c of text) { freiFeld.value += c; freiFeld.dispatchEvent(new Event('input')); await warte(170, id); } }
  async function leere(id) { while (freiFeld.value) { freiFeld.value = freiFeld.value.slice(0, -1); freiFeld.dispatchEvent(new Event('input')); await warte(90, id); } }
  function grundzustand() {
    form.classList.remove('fertig');
    $('input[value="einmalig"]').checked = true; z.rhythmus = 'einmalig'; z.wert = z.preset = START[satzSchluessel()]; z.frei = false; z.ungueltig = false; z.freiBeruehrt = false;
    freiFeld.value = ''; if (document.activeElement === freiFeld) freiFeld.blur();
    baueBetraege(false); pruefeFrei(false); zeige(); scrollTo(0, 0);
  }
  async function szene(id) {
    grundzustand();
    zeiger.style.transform = `translate(${innerWidth * .62}px, ${innerHeight * .2}px)`;
    await warte(300, id);
    await klick('.betrag:nth-child(3)', id); await warte(1300, id);
    if (z.variante === 'kampagne') {
      await klick('.betrag:nth-child(4)', id); await warte(1500, id);
    } else {
      await klick('.rhythmus label:nth-child(2)', id); await warte(1300, id);
      await klick('.betrag:nth-child(3)', id); await warte(1300, id);
      await klick('.rhythmus label:nth-child(1)', id); await warte(900, id);
    }
    await klick(freiFeld, id); await tippe('3', id); freiFeld.blur(); await warte(1300, id);
    await klick(freiFeld, id); await leere(id); await tippe('120', id); freiFeld.blur(); await warte(1200, id);
    await klick('.absenden', id); await warte(2000, id);
  }
  function vorfuehren() {
    const id = ++lauf; angehalten = false; html.classList.add('vorfuehrung');
    (async () => { try { while (id === lauf) await szene(id); } catch (e) { /* abgebrochen */ } })();
  }
  function pause() { angehalten = true; }
  function weiter() { if (html.classList.contains('vorfuehrung')) angehalten = false; else vorfuehren(); }
  function neustart() { lauf++; angehalten = false; html.classList.remove('vorfuehrung'); grundzustand(); }

  baueBetraege(false);
  setzeVariante(z.variante, false);

})();
</script>
</body>
</html>