Zum Inhalt
← Sektionen & Funktionen

News-Übersicht mit Kategorien und Jahr-Archiv

News-Übersicht mit Aufmacher, Kategorie-Knöpfen mit Zahl, Jahr-Auswahl, Liste nach Jahren, „Weitere laden“ mit Fokus auf den ersten neuen Beitrag oder Seitenzahlen, Leerzustand mit Ausweg und Stand in der URL.

Bis etwa 60 Beiträge und gemischte Leser: Verein, Handwerk, Praxis, Kultur. Der Fuß mit Kontakt bleibt erreichbar.

Die Übersichtsseite für Neuigkeiten, Blog oder Presse. Oben der jüngste Beitrag als Aufmacher, darunter eine Filterzeile mit Kategorien und Jahr. Die Liste zeigt Datum, Kategorie, Titel, einen Satz Anriss und die Lesezeit, nach Jahren gegliedert. Weitere Beiträge kommen entweder über „Weitere laden“ (der Fokus springt auf den ersten neuen Beitrag) oder über Seitenzahlen mit Vor und Zurück. Filter und Seite stehen in der URL, damit Zurück und Teilen funktionieren. Findet ein Filter nichts, erklärt ein Leerzustand warum und bietet den nächsten sinnvollen Weg an.

Eine News-Seite beweist, dass der Betrieb lebt. Dafür muss das Datum vorn stehen und das Archiv durchsuchbar sein. Kategorien und Jahre sind die zwei Fragen, mit denen Leute suchen („Was gab es zur Mitgliederversammlung?“, „Was war 2024?“). „Weitere laden“ hält den Fuß erreichbar, Seitenzahlen geben Orientierung. Beides mit Fokus und URL ist für Tastatur und Screenreader bedienbar.

Wann einsetzen

Verein, Kultur, Handwerk, Praxis, Kanzlei, Industrie und alle Betriebe mit mehr als etwa acht Beiträgen. „Weitere laden“ für bis etwa 60 Beiträge und gemischte Leser, Seitenzahlen für große Archive, Presse und wenn Leute „Seite 3“ wiederfinden müssen.

Wann nicht

Bei weniger als sechs Beiträgen reicht eine Liste ohne Filter. Kein Endlos-Scrollen: Es versteckt den Fuß mit Kontakt und Impressum. Keine Kategorien, die nur einen Beitrag haben. Kein Aufmacher, wenn der jüngste Beitrag älter als ein Jahr ist; dann ehrlich ohne „Neu“.

Varianten

  • Weitere laden: sechs Beiträge je Schub, Fokus auf den ersten neuen, Statuszeile mit Zahl, Stand in der URL
  • Seitenzahlen: nav mit aria-current, Vor und Zurück, Auslassung bei vielen Seiten, Fokus auf die Listenüberschrift
  • Leerzustand mit Grund und Ausweg (Filter lösen, nächstes Jahr mit Treffern)

Umsetzung

Die Beiträge kommen als Liste aus dem CMS (Titel, Datum, Kategorie, Anriss, Lesezeit, Bild optional). Filterzeile als <form> mit einer Gruppe Kategorie-Knöpfen (aria-pressed) und einem <select> für das Jahr; Änderungen filtern sofort und schreiben ?kategorie=…&jahr=…&seite=… per history.replaceState. Ohne JavaScript funktioniert das Formular per GET. Liste als <ol> mit <article> je Beitrag; Jahreszahl als eigene h2 je Gruppe. Ein Link je Beitrag (Titel), die Zeile ist über ::after klickbar. „Weitere laden“: Knopf mit Restzahl („6 weitere, noch 12“), danach Fokus per focus() auf den Titel-Link des ersten neuen Beitrags, Ansage in role="status" („6 weitere Beiträge geladen, 12 von 24“). Seitenzahlen: <nav aria-label="Seiten"> mit Links, aria-current="page", bei vielen Seiten 1 … 4 5 6 … 12; nach dem Wechsel Fokus auf die Listenüberschrift (tabindex=-1) und dorthin scrollen. Leerzustand als eigener Block mit Grund, „Filter zurücksetzen“ und einem Vorschlag mit Trefferzahl. Datum mit Intl.DateTimeFormat('de-DE').

Robust umsetzen

Telefon. Kategorien als waagerecht umbrechende Knopfreihe, Jahr als Auswahlfeld darunter. Beiträge einspaltig, Datum und Kategorie in einer Zeile über dem Titel, Bild klein rechts oder weg. Seitenzahlen kürzen auf Zurück, „Seite 2 von 4“ und Weiter.

Ohne Bewegung. Neue Beiträge und gefilterte Listen erscheinen ohne Gleiten und ohne Versatz. Fokus und Ansage bleiben.

Barrierefrei. Filter als echte Knöpfe mit aria-pressed bzw. select mit label. Trefferzahl in role="status". Nach „Weitere laden“ Fokus auf den ersten neuen Beitrag, nach Seitenwechsel auf die Listenüberschrift. Seitenzahlen als nav mit Namen, aktuelle Seite mit aria-current="page". Datum als time. Leerzustand als Text, nicht als Bild.

Code

<!DOCTYPE html>
<!--
  News-Übersicht mit Kategorien und Jahr-Archiv
  Fundus · Sektionen & Funktionen · https://cecodesign.de/lab/fundus/sektionen/news-uebersicht-archiv/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de" data-v="mehr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>News-Übersicht mit Kategorien und Jahr-Archiv · 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; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Token aus verein-nonprofit-wirkung, UI nach token-toggle-asymmetrisch */
:root {
  --grund: #ffffff; --hell: #edf5fa; --grau: #dfdfd7; --grau-tief: #c9c9bf; --text: #222520; --leise: #575b55; --marke: #002e6d; --marke-tief: #001f4b; --spenden: #ffca0a;
  --display: 'Big Shoulders', 'Arial Narrow', sans-serif; --t: 'Archivo', system-ui, sans-serif;
  --auf: cubic-bezier(0.16, 1, 0.3, 1); --zu: cubic-bezier(0.7, 0, 0.84, 0);
  --ui: 150ms; --rein: 320ms; --raus: 180ms; --versatz: 40ms;
  --rund: 4px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font-family: var(--t); -webkit-font-smoothing: antialiased; scroll-padding-top: 90px; }
:root:has(:focus-visible) { scroll-behavior: auto; }
body { font-size: 1.0625rem; line-height: 1.6; min-height: 100svh; }
a { color: inherit; }
button, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; }
:focus-visible { outline: 3px solid var(--marke); outline-offset: 3px; }
.vh { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.huelle { max-width: 1320px; margin: 0 auto; padding: 0 clamp(16px, 4vw, 48px); }
.ziffern { font-variant-numeric: tabular-nums lining-nums; }
.label { font-size: .8125rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; }
h1, h2, h3 { text-wrap: balance; }
p { text-wrap: pretty; hyphens: auto; }

/* Kopf */
.kopf { background: #fff; border-bottom: 1px solid var(--grau); position: relative; z-index: 10; }
.kopf .huelle { display: flex; align-items: center; gap: 28px; min-height: 72px; }
.marke { display: flex; align-items: center; gap: 10px; margin-right: auto; text-decoration: none; color: var(--marke); }
.marke b { font: 800 1.75rem/1 var(--display); letter-spacing: .01em; text-transform: uppercase; }
.marke small { font-size: .75rem; font-weight: 600; color: var(--leise); letter-spacing: .04em; }
.nav ul { display: flex; gap: 24px; list-style: none; font-weight: 600; font-size: .9375rem; }
.nav a { text-decoration: none; padding: 6px 0; border-bottom: 2px solid transparent; transition: border-color var(--ui) var(--auf); }
.nav a:hover { border-color: var(--grau-tief); }
.nav a[aria-current] { border-color: var(--marke); color: var(--marke); }
.spenden { background: var(--spenden); color: var(--text); font-weight: 700; padding: 11px 18px; border-radius: var(--rund); text-decoration: none; font-size: .9375rem; transition: transform var(--ui) var(--auf); }
.spenden:hover { transform: translateY(-1px); }
.spenden:active { transform: translateY(1px); }

/* Seitenkopf */
.seitenkopf { background: var(--marke); color: #fff; padding: clamp(40px, 6vw, 88px) 0 clamp(32px, 4vw, 56px); }
.seitenkopf .huelle { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: end; gap: 24px 48px; }
.seitenkopf .label { color: #b9cde6; }
.seitenkopf h1 { font: 800 clamp(3rem, 1.6rem + 6vw, 7.5rem)/.9 var(--display); text-transform: uppercase; letter-spacing: .005em; margin-top: 14px; }
.seitenkopf p.unter { max-width: 34rem; margin-top: 18px; color: #dbe6f3; font-size: 1.125rem; }
.seitenkopf .zahl { text-align: right; }
.seitenkopf .zahl b { display: block; font: 800 clamp(3rem, 2rem + 4vw, 5.5rem)/.9 var(--display); }
.seitenkopf .zahl span { font-size: .875rem; color: #dbe6f3; }
.seitenkopf .zahl a { display: inline-block; margin-top: 12px; font-size: .875rem; font-weight: 600; color: #fff; }

/* Aufmacher */
.aufmacher { padding: clamp(32px, 4vw, 56px) 0 0; }
.aufmacher article { position: relative; display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(20px, 3vw, 40px); align-items: end; }
.aufmacher .bild { aspect-ratio: 3 / 2; overflow: hidden; border-radius: var(--rund); background: #9fb0bf; }
.aufmacher img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform 900ms var(--auf); }
.aufmacher article:hover img { transform: scale(1.025); }
.aufmacher .neu { display: inline-block; background: var(--marke); color: #fff; padding: 4px 8px; border-radius: var(--rund); font-size: .75rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
.aufmacher h2 { font: 800 clamp(2rem, 1.3rem + 2.6vw, 3.5rem)/.98 var(--display); text-transform: uppercase; color: var(--marke); margin: 14px 0 14px; }
.aufmacher h2 a { text-decoration: none; }
.aufmacher h2 a::after { content: ''; position: absolute; inset: 0; }
.aufmacher article:hover h2 a { text-decoration: underline; text-decoration-thickness: 3px; text-underline-offset: .12em; }
.aufmacher h2 a:focus-visible { outline: none; }
.aufmacher article:has(a:focus-visible) { outline: 3px solid var(--marke); outline-offset: 6px; }
.zeile { display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center; font-size: .875rem; color: var(--leise); }
.kat { font-weight: 700; color: var(--marke); font-size: .8125rem; letter-spacing: .06em; text-transform: uppercase; }

/* Filter */
.filter { position: sticky; top: 0; z-index: 8; background: rgba(255,255,255,.96); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); border-bottom: 1px solid var(--grau); margin-top: clamp(40px, 5vw, 72px); }
.filter form { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 24px; padding: 14px 0; }
.filter fieldset { border: 0; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; min-width: 0; }
.filter legend { float: left; margin-right: 6px; font-size: .8125rem; font-weight: 600; color: var(--leise); }
.knopf-kat { display: inline-flex; gap: 8px; align-items: baseline; padding: 8px 13px; border: 1px solid var(--grau-tief); border-radius: var(--rund); font-weight: 600; font-size: .9375rem; transition: background-color var(--ui) var(--auf), border-color var(--ui) var(--auf), color var(--ui) var(--auf); }
.knopf-kat span { font-size: .8125rem; font-weight: 500; color: var(--leise); font-variant-numeric: tabular-nums; }
.knopf-kat:hover { border-color: var(--marke); }
.knopf-kat[aria-pressed="true"] { background: var(--marke); border-color: var(--marke); color: #fff; }
.knopf-kat[aria-pressed="true"] span { color: #c9d8eb; }
.jahr { display: flex; align-items: center; gap: 10px; font-size: .8125rem; font-weight: 600; color: var(--leise); }
.jahr select { appearance: none; -webkit-appearance: none; border: 1px solid var(--grau-tief); border-radius: var(--rund); padding: 8px 36px 8px 12px; font-weight: 600; font-size: .9375rem; color: var(--text); background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%23002e6d' stroke-width='1.8'/%3E%3C/svg%3E") no-repeat right 12px center; cursor: pointer; }
.jahr select:hover { border-color: var(--marke); }
.treffer { margin-left: auto; font-size: .9375rem; font-weight: 600; display: flex; gap: 14px; align-items: center; }
.treffer button { font-size: .875rem; font-weight: 600; color: var(--marke); text-decoration: underline; text-underline-offset: .2em; }
.treffer button[hidden] { display: none; }

/* Liste */
.liste { padding: clamp(28px, 4vw, 48px) 0 0; }
.liste > .huelle > h2 { font-size: .8125rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--leise); }
.liste > .huelle > h2:focus { outline: none; }
.liste > .huelle > h2:focus-visible { outline: 3px solid var(--marke); }
.gruppe { display: grid; grid-template-columns: minmax(0, 11rem) minmax(0, 1fr); gap: 0 clamp(20px, 4vw, 56px); border-top: 2px solid var(--marke); margin-top: 18px; }
.gruppe h3 { font: 800 clamp(3rem, 2rem + 3vw, 5rem)/1 var(--display); color: var(--marke); position: sticky; top: 76px; align-self: start; padding-top: 14px; font-variant-numeric: tabular-nums; }
.gruppe ol { list-style: none; }
.eintrag { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) 132px; gap: 6px 28px; padding: 22px 0; border-bottom: 1px solid var(--grau); }
.eintrag .zeile { grid-column: 1; }
.eintrag h4 { grid-column: 1; font: 700 clamp(1.5rem, 1.15rem + 1vw, 2rem)/1.05 var(--display); color: var(--text); text-transform: none; letter-spacing: .005em; }
.eintrag h4 a { text-decoration: none; }
.eintrag h4 a::after { content: ''; position: absolute; inset: 0; }
.eintrag h4 a:focus-visible { outline: none; }
.eintrag:has(h4 a:focus-visible) { outline: 3px solid var(--marke); outline-offset: 2px; background: var(--hell); }
.eintrag:hover h4 { color: var(--marke); text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: .14em; }
.eintrag p { grid-column: 1; color: var(--leise); font-size: 1rem; max-width: 62ch; }
.eintrag .mini { grid-column: 2; grid-row: 1 / span 3; align-self: center; aspect-ratio: 4 / 3; border-radius: var(--rund); overflow: hidden; background: var(--hell); }
.eintrag .mini img, .eintrag .mini svg { width: 100%; height: 100%; object-fit: cover; display: block; }
.eintrag[data-neu] { animation: rein var(--rein) var(--auf) both; animation-delay: calc(var(--i, 0) * var(--versatz)); }
@keyframes rein { from { opacity: 0; transform: translateY(12px); } }
.liste[data-wechsel] .gruppen { opacity: 0; transition: opacity var(--raus) var(--zu); }
.gruppen { transition: opacity var(--rein) var(--auf); }
.skelett { list-style: none; }
.skelett li { display: grid; grid-template-columns: minmax(0, 1fr) 132px; gap: 10px 28px; padding: 22px 0; border-bottom: 1px solid var(--grau); }
.skelett span { display: block; height: 14px; border-radius: 2px; background: linear-gradient(90deg, var(--hell) 0%, #f7fafc 50%, var(--hell) 100%); background-size: 200% 100%; animation: schimmer 1.1s linear infinite; }
.skelett span:nth-child(2) { height: 26px; width: 80%; }
.skelett span:nth-child(3) { width: 60%; }
.skelett span.b { grid-column: 2; grid-row: 1 / span 3; height: auto; aspect-ratio: 4 / 3; }
@keyframes schimmer { to { background-position: -200% 0; } }

/* Mehr laden, Seiten */
.weiter { padding: 36px 0 0; display: grid; justify-items: center; gap: 12px; }
.mehr { display: inline-flex; align-items: center; gap: 12px; padding: 15px 26px; border: 2px solid var(--marke); color: var(--marke); border-radius: var(--rund); font-weight: 700; font-size: 1rem; transition: background-color var(--ui) var(--auf), color var(--ui) var(--auf); }
.mehr:hover { background: var(--marke); color: #fff; }
.mehr[aria-busy="true"] { opacity: .7; cursor: progress; }
.anteil { width: min(320px, 100%); display: grid; gap: 6px; font-size: .875rem; color: var(--leise); text-align: center; }
.anteil i { display: block; height: 3px; background: var(--grau); border-radius: 2px; overflow: hidden; }
.anteil i b { display: block; height: 100%; background: var(--marke); transform-origin: 0 50%; transform: scaleX(var(--a, 0)); transition: transform var(--rein) var(--auf); }
.ende { font-weight: 600; color: var(--leise); }
.ende a { color: var(--marke); }
.seiten ol { list-style: none; display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; align-items: center; }
.seiten a { display: inline-grid; place-items: center; min-width: 44px; height: 44px; padding: 0 12px; border: 1px solid var(--grau-tief); border-radius: var(--rund); text-decoration: none; font-weight: 700; font-variant-numeric: tabular-nums; transition: background-color var(--ui) var(--auf), border-color var(--ui) var(--auf); }
.seiten a:hover { border-color: var(--marke); }
.seiten a[aria-current="page"] { background: var(--marke); border-color: var(--marke); color: #fff; }
.seiten a[aria-disabled="true"] { color: #6b6e68; border-style: dashed; pointer-events: none; }
.seiten .aus { padding: 0 4px; color: var(--leise); }
.seiten .kurzinfo { display: none; font-weight: 600; }
:root[data-v="mehr"] .seiten, :root[data-v="seiten"] .weiter-mehr { display: none; }

/* Leerzustand */
.leer { border-top: 2px solid var(--marke); margin-top: 18px; padding: clamp(32px, 5vw, 64px) 0; display: grid; grid-template-columns: minmax(0, 11rem) minmax(0, 1fr); gap: 0 clamp(20px, 4vw, 56px); }
.leer b.null { font: 800 clamp(4rem, 3rem + 4vw, 7rem)/.85 var(--display); color: var(--grau-tief); }
.leer h3 { font: 800 clamp(1.75rem, 1.3rem + 1.5vw, 2.75rem)/1 var(--display); text-transform: uppercase; color: var(--marke); }
.leer p { margin-top: 10px; max-width: 52ch; color: var(--leise); }
.leer .wege { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
.leer .wege button { padding: 10px 16px; border: 1px solid var(--marke); border-radius: var(--rund); font-weight: 600; color: var(--marke); font-size: .9375rem; }
.leer .wege button:first-child { background: var(--marke); color: #fff; }
.leer .wege button:hover { background: var(--marke-tief); color: #fff; }

/* Presse-Band, Fuß */
.band { margin-top: clamp(56px, 8vw, 112px); background: var(--hell); padding: clamp(32px, 4vw, 56px) 0; }
.band .huelle { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }
.band h2 { font: 800 1.75rem/1 var(--display); text-transform: uppercase; color: var(--marke); }
.band p { color: var(--leise); margin-top: 8px; font-size: .9375rem; }
.band a { font-weight: 700; color: var(--marke); }
.fuss { background: var(--marke-tief); color: #dbe6f3; padding: clamp(48px, 6vw, 80px) 0 28px; font-size: .9375rem; }
.fuss .reihe { display: grid; grid-template-columns: 1.5fr 1fr 1fr; gap: 32px; }
.fuss b.name { font: 800 2.5rem/1 var(--display); color: #fff; text-transform: uppercase; display: block; margin-bottom: 10px; }
.fuss h2 { font-size: .8125rem; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: #9fb6d6; margin-bottom: 10px; }
.fuss ul { list-style: none; display: grid; gap: 6px; }
.fuss a { text-decoration: none; }
.fuss a:hover { text-decoration: underline; }
.fuss :focus-visible { outline-color: var(--spenden); }
.fuss .unten { margin-top: 40px; padding-top: 16px; border-top: 1px solid rgba(255,255,255,.18); font-size: .8125rem; color: #9fb6d6; display: flex; flex-wrap: wrap; gap: 8px 24px; }

/* Pult */
.pult { position: fixed; right: 16px; bottom: 16px; z-index: 40; width: 252px; background: #fff; border: 1px solid var(--grau-tief); border-radius: var(--rund); padding: 14px; display: grid; gap: 10px; font-size: 13px; box-shadow: 0 8px 28px rgba(0, 31, 75, .12); }
.pult .mono { font: 500 10.5px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.pult .reihe { display: grid; grid-template-columns: 1fr 1fr; border: 1px solid var(--grau-tief); border-radius: var(--rund); overflow: hidden; }
.pult .reihe button { padding: 8px 6px; font-weight: 600; }
.pult .reihe button[aria-pressed="true"] { background: var(--marke); color: #fff; }
.pult .einzeln { border: 1px solid var(--grau-tief); border-radius: var(--rund); padding: 8px 10px; text-align: left; font-weight: 600; }
.pult .einzeln:hover { border-color: var(--marke); }
.pult .werte { font: 400 10.5px/1.5 'Geist Mono', monospace; color: var(--leise); }

.zeiger { position: fixed; left: 0; top: 0; width: 22px; height: 22px; z-index: 60; pointer-events: none; opacity: 0; transition: opacity 200ms var(--auf); }
.vorfuehrung .zeiger { opacity: 1; }
.zeiger.drueckt { scale: .86; transform-origin: 3px 2px; }
.vorschau body { overflow: hidden; }
.vorschau .pult { display: none; }

.menueknopf { display: none; font-weight: 700; color: var(--marke); padding: 10px 4px; }
@media (max-width: 1060px) {
  .nav { display: none; } .menueknopf { display: inline-block; }
  .nav[data-offen] { display: block; position: absolute; top: 100%; left: 0; right: 0; background: #fff; border-bottom: 2px solid var(--marke); padding: 8px clamp(16px, 4vw, 48px) 16px; }
  .nav[data-offen] ul { flex-direction: column; gap: 4px; font-size: 1.125rem; }
}
@media (max-width: 860px) {
  .seitenkopf .huelle { grid-template-columns: minmax(0, 1fr); }
  .seitenkopf .zahl { text-align: left; display: flex; gap: 14px; align-items: baseline; flex-wrap: wrap; }
  .seitenkopf .zahl b { font-size: 2.5rem; }
  .aufmacher article { grid-template-columns: minmax(0, 1fr); }
  .gruppe, .leer { grid-template-columns: minmax(0, 1fr); }
  .gruppe h3 { position: static; font-size: 2.75rem; padding-top: 10px; }
  .treffer { margin-left: 0; width: 100%; justify-content: space-between; }
  .band .huelle, .fuss .reihe { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
  .kopf .huelle { gap: 12px; min-height: 64px; }
  .marke small { display: none; }
  .eintrag { grid-template-columns: minmax(0, 1fr) 84px; gap: 6px 16px; padding: 18px 0; }
  .eintrag p:not(.zeile) { display: none; }
  .eintrag .mini { grid-row: 1 / span 2; }
  .skelett li { grid-template-columns: minmax(0, 1fr) 84px; }
  .filter { position: static; }
  .filter form { gap: 10px; }
  .filter legend { float: none; width: 100%; margin-bottom: 2px; }
  .seiten .zahlseite, .seiten .aus { display: none; }
  .seiten .kurzinfo { display: inline-block; padding: 0 10px; }
}
@media (max-width: 360px) { .spenden { padding: 10px 12px; } .marke b { font-size: 1.5rem; } .marke svg { display: none; } .kopf .huelle { gap: 10px; } }
@media (hover: none) { .aufmacher article:hover img { transform: none; } .eintrag:hover h4 { color: inherit; text-decoration: none; } }
:root.reduziert * { transition-duration: .01ms !important; animation-duration: .01ms !important; animation-delay: 0ms !important; }
:root.reduziert .skelett span { animation: none; }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch], input, select, textarea { border: 1px solid ButtonText !important; }
  .knopf-kat[aria-pressed="true"], .seiten a[aria-current="page"] { outline: 3px solid Highlight; }
}
</style>
</head>
<body>
<header class="kopf">
  <div class="huelle">
    <a class="marke" href="#" aria-label="Flussläufer e. V., Startseite">
      <svg width="34" height="22" viewBox="0 0 34 22" aria-hidden="true"><path d="M1 8c4-4 7-4 11 0s7 4 11 0 7-4 10-1M1 16c4-4 7-4 11 0s7 4 11 0 7-4 10-1" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/></svg>
      <b>Flussläufer</b><small>e. V. · Merseburg</small>
    </a>
    <nav class="nav" id="hauptmenue" aria-label="Hauptmenü"><ul><li><a href="#">Projekte</a></li><li><a href="#">Mitmachen</a></li><li><a href="#" aria-current="page">Neuigkeiten</a></li><li><a href="#">Über uns</a></li></ul></nav>
    <a class="spenden" href="#">Spenden</a>
    <button class="menueknopf" type="button" aria-expanded="false" aria-controls="hauptmenue">Menü</button>
  </div>
</header>

<main id="inhalt">
  <section class="seitenkopf" aria-labelledby="h1">
    <div class="huelle">
      <div>
        <p class="label">Neuigkeiten</p>
        <h1 id="h1">Was am Fluss passiert</h1>
        <p class="unter">Aktionen, Messwerte und Vereinsleben an Saale, Geisel und Luppe. Neue Beiträge etwa zweimal im Monat.</p>
      </div>
      <p class="zahl"><b class="ziffern" data-gesamt>24</b><span>Beiträge seit Mai 2024</span><br><a href="#">RSS-Feed abonnieren</a></p>
    </div>
  </section>

  <section class="aufmacher" aria-labelledby="aufmacher-titel" data-aufmacher>
    <div class="huelle">
      <article>
        <div class="bild"><img src="bilder/huegel-nebel.jpg" width="1200" height="800" alt="Bewaldete Hügelketten im Morgennebel, tiefe Sonne"></div>
        <div>
          <p class="zeile"><span class="neu">Neu</span><span class="kat">Aktionen</span><time datetime="2026-10-28" class="ziffern">28.10.2026</time><span>4 Min. Lesezeit</span></p>
          <h2 id="aufmacher-titel"><a href="#">Herbstputz an der Geisel: 312 Kilo in drei Stunden</a></h2>
          <p>41 Freiwillige, zwei Boote, ein Einkaufswagen. Was wir gefunden haben und wohin es jetzt geht.</p>
        </div>
      </article>
    </div>
  </section>

  <div class="filter">
    <div class="huelle">
      <form data-filter action="" method="get">
        <fieldset>
          <legend>Kategorie</legend>
          <div style="display:contents" data-kats></div>
        </fieldset>
        <label class="jahr">Jahr <select name="jahr" data-jahr></select></label>
        <p class="treffer" role="status" aria-live="polite"><span data-treffer class="ziffern"></span><button type="button" data-zurueck hidden>Filter zurücksetzen</button></p>
      </form>
    </div>
  </div>

  <section class="liste" aria-labelledby="liste-titel" data-liste>
    <div class="huelle">
      <h2 id="liste-titel" tabindex="-1" data-liste-titel>Alle Beiträge</h2>
      <div class="gruppen" data-gruppen></div>
      <div class="weiter weiter-mehr" data-weiter>
        <button class="mehr" type="button" data-mehr><span data-mehr-text>6 weitere Beiträge laden</span><svg width="12" height="14" viewBox="0 0 12 14" aria-hidden="true"><path d="M6 1v11M1 7l5 5 5-5" fill="none" stroke="currentColor" stroke-width="2"/></svg></button>
        <div class="anteil"><span data-anteil-text class="ziffern"></span><i aria-hidden="true"><b data-anteil></b></i></div>
        <p class="ende" data-ende hidden>Alle Beiträge geladen. <a href="#h1">Nach oben</a></p>
      </div>
      <nav class="weiter seiten" aria-label="Seiten" data-seiten><ol></ol></nav>
      <p class="vh" role="status" aria-live="polite" data-ansage></p>
    </div>
  </section>

  <section class="band" aria-label="Für Redaktionen und Abonnenten">
    <div class="huelle">
      <div><h2>Für die Presse</h2><p>Pressefotos mit Bildrechten und Ansprechpartnerin Hanne Wiecek: <a href="#">Pressebereich</a></p></div>
      <div><h2>Per E-Mail</h2><p>Einmal im Monat die wichtigsten Neuigkeiten. <a href="#">Newsletter bestellen</a></p></div>
      <div><h2>Mitmachen</h2><p>Nächster Sammeltag: Samstag, 21. November, 10 Uhr, Brücke Geiseltalstraße. <a href="#">Anmelden</a></p></div>
    </div>
  </section>
</main>

<footer class="fuss">
  <div class="huelle">
    <div class="reihe">
      <div><b class="name">Flussläufer</b><p>Flussläufer e. V. · Gewässerpaten für Saale, Geisel und Luppe<br>Vereinsregister Amtsgericht Stendal (Beispiel)</p></div>
      <div><h2>Verein</h2><ul><li><a href="#">Satzung</a></li><li><a href="#">Jahresberichte</a></li><li><a href="#">Mitglied werden</a></li></ul></div>
      <div><h2>Rechtliches</h2><ul><li><a href="#">Impressum</a></li><li><a href="#">Datenschutz</a></li><li><a href="#">Barrierefreiheit</a></li></ul></div>
    </div>
    <p class="unten"><span>Demo: Verein, Personen und Zahlen sind erfunden.</span></p>
  </div>
</footer>

<div class="pult" role="group" aria-label="Steuerung der Demo">
  <span class="mono">Variante</span>
  <div class="reihe">
    <button type="button" data-v="mehr" aria-pressed="true">Weitere laden</button>
    <button type="button" data-v="seiten" aria-pressed="false">Seitenzahlen</button>
  </div>
  <button type="button" class="einzeln" data-leer-zeigen>Leerzustand zeigen</button>
  <p class="werte" data-werte></p>
</div>

<svg class="zeiger" viewBox="0 0 22 22" aria-hidden="true" data-zeiger><path d="M3 2 L3 18 L7.5 14 L10.5 20.5 L13.2 19.3 L10.3 13 L16.5 13 Z" fill="#141413" stroke="#fff" stroke-width="1.4" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)];
  const JE = 6;
  const KATS = { aktionen: 'Aktionen', gewaesser: 'Gewässer', verein: 'Verein', presse: 'Presse' };
  // Beiträge, wie sie aus dem CMS kämen: Datum, Kategorie, Titel, Anriss, Minuten, Bild (optional)
  const B = [
    ['2026-10-28', 'aktionen', 'Herbstputz an der Geisel: 312 Kilo in drei Stunden', '41 Freiwillige, zwei Boote, ein Einkaufswagen. Was wir gefunden haben und wohin es jetzt geht.', 4, 'huegel-nebel.jpg'],
    ['2026-10-14', 'gewaesser', 'Wasserstand der Saale: warum die Messstelle Merseburg so wichtig ist', 'Pegel, Abfluss und was die Zahlen für Fische und Paddler bedeuten.', 6],
    ['2026-09-30', 'verein', 'Mitgliederversammlung: neuer Vorstand, alte Ziele', '38 Mitglieder, zwei Wahlgänge, ein Haushalt. Das Protokoll steht im Mitgliederbereich.', 3],
    ['2026-09-12', 'presse', 'Regionalfernsehen begleitet unseren Messtag', 'Ein Drehteam war einen Vormittag mit dem Messteam an der Luppe unterwegs.', 2],
    ['2026-08-27', 'gewaesser', 'Eisvogel zurück an der Luppe-Mündung', 'Zweimal im August gesichtet. Was das über die Uferpflege der letzten zwei Jahre sagt.', 3],
    ['2026-08-09', 'aktionen', 'Ferienwoche am Fluss: 18 Kinder, 6 Kescher, viele Fragen', 'Fünf Tage Wasserkunde mit der Grundschule am Gotthardteich.', 4, 'dialog-modal-nativ-schule.jpg'],
    ['2026-07-15', 'verein', 'Neue Lagerhalle für Boote und Greifzangen', 'Ein Landwirt aus Beuna stellt uns eine Halle zur Verfügung. Danke!', 2],
    ['2026-06-02', 'aktionen', 'Aktionstag Gewässerpflege: Danke an 64 Helferinnen und Helfer', 'Der größte Sammeltag seit der Gründung, mit Bilanz und Fotos.', 3],
    ['2026-04-18', 'gewaesser', 'Frühjahrsmessung: Nitratwerte an drei Stellen', 'Unsere Messreihe seit 2024 im Vergleich, mit Tabelle zum Herunterladen.', 5],
    ['2026-03-06', 'presse', 'Pressemitteilung: Flussläufer werden Gewässerpaten der Stadt', 'Die Stadt überträgt uns die Patenschaft für vier Kilometer Geisel.', 2],
    ['2025-11-20', 'verein', 'Jahresbericht 2025: wohin dein Beitrag geflossen ist', 'Einnahmen, Ausgaben und Stunden im Ehrenamt auf einer Seite.', 7],
    ['2025-10-25', 'aktionen', 'Herbstputz 2025: 280 Kilo und ein Fahrrad', 'Was wir im zweiten Jahr anders gemacht haben.', 3],
    ['2025-09-03', 'gewaesser', 'Niedrigwasser im Spätsommer: was wir beobachtet haben', 'Drei Wochen ohne Regen und was sie an der Geisel verändert haben.', 5],
    ['2025-07-21', 'aktionen', 'Paddeln mit Greifzange: erste Kanu-Sammeltour', 'Vom Wasser aus erreichen wir Stellen, an die vom Ufer keiner kommt.', 4],
    ['2025-06-10', 'verein', 'Ehrenamtspreis des Landkreises für unser Messteam', 'Fünf Menschen, 140 Messungen, ein Preis.', 2],
    ['2025-04-29', 'gewaesser', 'Blühstreifen am Ufer: erste Bilanz nach einem Jahr', 'Welche Pflanzen gekommen sind und welche nicht.', 4],
    ['2025-03-22', 'aktionen', 'Weltwassertag: Infostand auf dem Markt', 'Wasserproben zum Anschauen und viele Gespräche.', 2],
    ['2025-02-08', 'presse', 'Radiobeitrag: warum Müll im Fluss nicht nur hässlich ist', 'Interview mit unserer Vorsitzenden zum Nachhören.', 2],
    ['2025-01-15', 'verein', 'Neue Website, gleiche Adresse', 'Alle Beiträge sind umgezogen, die Links funktionieren weiter.', 1],
    ['2024-11-30', 'gewaesser', 'Ufergehölz gepflanzt: 120 Weiden an der Geisel', 'Ein Samstag, zwölf Spaten und ein Plan für die nächsten zehn Jahre.', 3],
    ['2024-10-19', 'aktionen', 'Erster Herbstputz: 190 Kilo', 'Wie alles anfing, mit 17 Leuten und einem Anhänger.', 3],
    ['2024-08-07', 'verein', 'Gründungsfest im Schlosspark', 'Musik, Kuchen und die ersten 50 Mitglieder.', 2],
    ['2024-06-14', 'gewaesser', 'Messstelle eingerichtet: wir messen jetzt selbst', 'Warum ein Verein eigene Wasserwerte braucht.', 4],
    ['2024-05-03', 'verein', 'Flussläufer e. V. gegründet', 'Elf Menschen, eine Satzung und ein Fluss vor der Tür.', 2]
  ].map(([d, k, t, a, m, bild], i) => ({ i, d, jahr: d.slice(0, 4), k, t, a, m, bild }));
  const JAHRE = [...new Set(B.map(b => b.jahr))];
  const fmt = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' });
  const WERTE = {
    mehr: 'öffnen 0.16,1,0.3,1 · schließen 0.7,0,0.84,0 · UI 150 ms · neu 320 ms, Versatz 40 ms, 12 px · 6 je Schub · Skelett 280 ms · Fokus auf ersten neuen Beitrag',
    seiten: 'Seitenzahlen 44 px · 6 je Seite · aria-current=page · Fokus auf Listenüberschrift · Auslassung ab 7 Seiten · Telefon: Zurück, Seite x von y, Weiter'
  };

  // Bildplatz für Beiträge ohne Foto: Muster je Kategorie (im Einsatz echtes Foto mit Bildunterschrift)
  function muster(k, i) {
    const f = { aktionen: ['#002e6d', '#ffca0a'], gewaesser: ['#0d4f8b', '#9cc4e4'], verein: ['#222520', '#dfdfd7'], presse: ['#575b55', '#edf5fa'] }[k];
    const s = (i * 37) % 30;
    if (k === 'gewaesser') return `<svg viewBox="0 0 120 90" aria-hidden="true"><rect width="120" height="90" fill="${f[1]}"/><path d="M0 ${60 - s / 3} C20 ${40 + s / 2} 40 ${70 - s / 2} 60 ${50} S100 ${30 + s / 2} 120 ${44}" fill="none" stroke="${f[0]}" stroke-width="3"/><path d="M0 74h120" stroke="${f[0]}" stroke-width="1" stroke-dasharray="2 4"/></svg>`;
    if (k === 'aktionen') return `<svg viewBox="0 0 120 90" aria-hidden="true"><rect width="120" height="90" fill="${f[0]}"/><circle cx="${40 + s}" cy="${40}" r="18" fill="${f[1]}"/><path d="M0 62c15-8 25-8 40 0s25 8 40 0 25-8 40 0M0 76c15-8 25-8 40 0s25 8 40 0 25-8 40 0" fill="none" stroke="#fff" stroke-width="3"/></svg>`;
    if (k === 'verein') return `<svg viewBox="0 0 120 90" aria-hidden="true"><rect width="120" height="90" fill="${f[1]}"/><g fill="${f[0]}">${Array.from({ length: 11 }, (_, j) => `<circle cx="${14 + (j % 6) * 18 + (j > 5 ? 9 : 0)}" cy="${j > 5 ? 58 : 36}" r="6"/>`).join('')}</g></svg>`;
    return `<svg viewBox="0 0 120 90" aria-hidden="true"><rect width="120" height="90" fill="${f[1]}"/><g fill="${f[0]}"><rect x="18" y="18" width="84" height="8"/><rect x="18" y="34" width="60" height="4"/><rect x="18" y="44" width="70" height="4"/><rect x="18" y="54" width="52" height="4"/></g></svg>`;
  }

  const zustand = { kat: 'alle', jahr: 'alle', seite: 1 };
  let v = 'mehr';
  const el = { kats: $('[data-kats]'), jahr: $('[data-jahr]'), treffer: $('[data-treffer]'), zurueck: $('[data-zurueck]'), gruppen: $('[data-gruppen]'), liste: $('[data-liste]'),
    titel: $('[data-liste-titel]'), mehr: $('[data-mehr]'), mehrText: $('[data-mehr-text]'), anteil: $('[data-anteil]'), anteilText: $('[data-anteil-text]'), ende: $('[data-ende]'),
    seiten: $('[data-seiten] ol'), ansage: $('[data-ansage]'), aufmacher: $('[data-aufmacher]') };

  const passt = (b, kat = zustand.kat, jahr = zustand.jahr) => (kat === 'alle' || b.k === kat) && (jahr === 'alle' || b.jahr === jahr);
  const gefiltert = () => B.filter(b => passt(b)).filter(b => !(ohneFilter() && b.i === 0));
  const ohneFilter = () => zustand.kat === 'alle' && zustand.jahr === 'alle';

  function filterBauen() {
    el.kats.innerHTML = '';
    [['alle', 'Alle'], ...Object.entries(KATS)].forEach(([id, name]) => {
      const n = B.filter(b => passt(b, id, zustand.jahr)).length;
      const k = document.createElement('button'); k.type = 'button'; k.className = 'knopf-kat'; k.dataset.kat = id;
      k.setAttribute('aria-pressed', zustand.kat === id);
      k.innerHTML = `${name} <span>${n}</span>`; k.setAttribute('aria-label', `${name}, ${n} Beiträge`);
      el.kats.append(k);
    });
    el.jahr.innerHTML = '';
    [['alle', 'Alle Jahre'], ...JAHRE.map(j => [j, j])].forEach(([id, name]) => {
      const n = B.filter(b => passt(b, zustand.kat, id)).length;
      const o = new Option(`${name} (${n})`, id, false, zustand.jahr === id); el.jahr.append(o);
    });
  }

  function eintragHTML(b, neu, idx) {
    const bild = b.bild ? `<img src="bilder/${b.bild}" alt="" loading="lazy" width="132" height="99">` : muster(b.k, b.i);
    return `<li class="eintrag"${neu ? ` data-neu style="--i:${idx}"` : ''} data-id="${b.i}">
      <p class="zeile"><span class="kat">${KATS[b.k]}</span><time datetime="${b.d}" class="ziffern">${fmt.format(new Date(b.d + 'T12:00:00'))}</time><span>${b.m} Min.</span></p>
      <h4><a href="#">${b.t}</a></h4><p>${b.a}</p><div class="mini">${bild}</div></li>`;
  }

  function sichtbare() {
    const alle = gefiltert();
    if (v === 'mehr') return { alle, zeigen: alle.slice(0, zustand.seite * JE) };
    const seiten = Math.max(1, Math.ceil(alle.length / JE)); zustand.seite = Math.min(zustand.seite, seiten);
    return { alle, zeigen: alle.slice((zustand.seite - 1) * JE, zustand.seite * JE), seiten };
  }

  function listeBauen(neueAb = -1) {
    const { alle, zeigen, seiten } = sichtbare();
    el.aufmacher.hidden = !ohneFilter();
    const gesamt = B.filter(b => passt(b)).length;
    const katName = zustand.kat === 'alle' ? '' : KATS[zustand.kat];
    el.treffer.textContent = `${gesamt} ${gesamt === 1 ? 'Beitrag' : 'Beiträge'}${katName ? ' · ' + katName : ''}${zustand.jahr !== 'alle' ? ' · ' + zustand.jahr : ''}`;
    el.zurueck.hidden = ohneFilter();
    el.titel.textContent = v === 'seiten' && seiten > 1 ? `Beiträge, Seite ${zustand.seite} von ${seiten}` : (ohneFilter() ? 'Ältere Beiträge' : 'Gefundene Beiträge');
    if (!alle.length) { leerBauen(); el.gruppen.dataset.leer = ''; $('[data-weiter]').hidden = true; $('[data-seiten]').hidden = true; return; }
    delete el.gruppen.dataset.leer; $('[data-weiter]').hidden = false; $('[data-seiten]').hidden = false;
    // nach Jahren gliedern
    const nachJahr = {};
    zeigen.forEach((b, i) => (nachJahr[b.jahr] = nachJahr[b.jahr] || []).push([b, i]));
    el.gruppen.innerHTML = Object.keys(nachJahr).sort().reverse().map(j => `<section class="gruppe" aria-labelledby="j-${j}"><h3 id="j-${j}">${j}</h3><ol>${nachJahr[j].map(([b, i]) => eintragHTML(b, neueAb >= 0 && i >= neueAb, i - neueAb)).join('')}</ol></section>`).join('');
    // Weitere laden
    const rest = alle.length - zeigen.length;
    el.mehr.hidden = rest <= 0; el.ende.hidden = rest > 0;
    el.mehrText.textContent = `${Math.min(JE, rest)} weitere Beiträge laden`;
    el.anteilText.textContent = `${zeigen.length} von ${alle.length} angezeigt`;
    el.anteil.style.setProperty('--a', (zeigen.length / alle.length).toFixed(3));
    // Seitenzahlen
    if (v === 'seiten') seitenBauen(seiten);
  }

  function seitenBauen(n) {
    const s = zustand.seite, teile = [];
    const link = (p, text, extra = '') => `<li><a href="?seite=${p}" data-seite="${p}" ${p === s && !extra ? 'aria-current="page"' : ''} class="${extra ? '' : 'zahlseite'}" ${extra}>${text}</a></li>`;
    teile.push(`<li><a href="?seite=${s - 1}" data-seite="${s - 1}" ${s === 1 ? 'aria-disabled="true" tabindex="-1"' : ''}><span aria-hidden="true">←</span>&nbsp;Zurück</a></li>`);
    const zeige = new Set([1, n, s - 1, s, s + 1].filter(p => p >= 1 && p <= n));
    let vorher = 0;
    [...zeige].sort((a, b) => a - b).forEach(p => { if (p - vorher > 1) teile.push('<li class="aus" aria-hidden="true">…</li>'); teile.push(link(p, p)); vorher = p; });
    teile.push(`<li class="kurzinfo" aria-hidden="true">Seite ${s} von ${n}</li>`);
    teile.push(`<li><a href="?seite=${s + 1}" data-seite="${s + 1}" ${s === n ? 'aria-disabled="true" tabindex="-1"' : ''}>Weiter&nbsp;<span aria-hidden="true">→</span></a></li>`);
    el.seiten.innerHTML = teile.join('');
  }

  function leerBauen() {
    const katName = zustand.kat === 'alle' ? 'in allen Kategorien' : `unter „${KATS[zustand.kat]}“`;
    const inKat = B.filter(b => passt(b, zustand.kat, 'alle')).length;
    const imJahr = B.filter(b => passt(b, 'alle', zustand.jahr)).length;
    const ersterKat = B.filter(b => passt(b, zustand.kat, 'alle')).map(b => b.jahr).sort()[0];
    el.gruppen.innerHTML = `<div class="leer" data-leerblock><b class="null" aria-hidden="true">0</b><div>
      <h3>Keine Beiträge ${katName} aus ${zustand.jahr}</h3>
      <p>${ersterKat && ersterKat > zustand.jahr ? `Beiträge ${katName} gibt es erst seit ${ersterKat}.` : 'Für diese Auswahl haben wir noch nichts geschrieben.'} Versuch eine der Möglichkeiten unten.</p>
      <div class="wege">
        <button type="button" data-weg="kat">${KATS[zustand.kat] || 'Alle'} aus allen Jahren (${inKat})</button>
        <button type="button" data-weg="jahr">Alle Beiträge aus ${zustand.jahr} (${imJahr})</button>
        <button type="button" data-weg="alle">Filter zurücksetzen</button>
      </div></div></div>`;
  }

  function urlSchreiben() {
    const q = new URLSearchParams(location.search);
    zustand.kat !== 'alle' ? q.set('kategorie', zustand.kat) : q.delete('kategorie');
    zustand.jahr !== 'alle' ? q.set('jahr', zustand.jahr) : q.delete('jahr');
    zustand.seite > 1 ? q.set('seite', zustand.seite) : q.delete('seite');
    q.set('v', v);
    history.replaceState(null, '', location.pathname + '?' + q.toString());
  }

  function anwenden(sanft = true) {
    zustand.seite = 1;
    filterBauen();
    if (sanft && !reduziert()) {
      el.liste.dataset.wechsel = '';
      setTimeout(() => { listeBauen(); delete el.liste.dataset.wechsel; }, 180);
    } else listeBauen();
    urlSchreiben();
  }

  el.kats.addEventListener('click', e => { const k = e.target.closest('[data-kat]'); if (!k) return; zustand.kat = k.dataset.kat; anwenden(); requestAnimationFrame(() => $(`[data-kat="${zustand.kat}"]`)?.focus()); });
  el.jahr.addEventListener('change', () => { zustand.jahr = el.jahr.value; anwenden(); });
  el.zurueck.addEventListener('click', () => { zustand.kat = 'alle'; zustand.jahr = 'alle'; anwenden(); $('[data-kat="alle"]').focus(); });
  $('[data-filter]').addEventListener('submit', e => e.preventDefault());
  el.gruppen.addEventListener('click', e => {
    const w = e.target.closest('[data-weg]'); if (!w) return;
    if (w.dataset.weg === 'kat') zustand.jahr = 'alle';
    if (w.dataset.weg === 'jahr') zustand.kat = 'alle';
    if (w.dataset.weg === 'alle') { zustand.kat = 'alle'; zustand.jahr = 'alle'; }
    anwenden(false); el.titel.focus();
  });

  // Weitere laden: Skelett, dann neue Beiträge, Fokus auf den ersten neuen
  el.mehr.addEventListener('click', () => {
    const vorher = sichtbare().zeigen.length;
    el.mehr.setAttribute('aria-busy', 'true');
    const sk = document.createElement('ul'); sk.className = 'skelett'; sk.setAttribute('aria-hidden', 'true');
    sk.innerHTML = '<li><span></span><span></span><span></span><span class="b"></span></li>'.repeat(3);
    const letzteOl = $$('.gruppe ol', el.gruppen).pop(); letzteOl.after(sk);
    setTimeout(() => {
      zustand.seite++; listeBauen(vorher); urlSchreiben();
      el.mehr.removeAttribute('aria-busy');
      const neu = $(`.eintrag[data-neu] h4 a`, el.gruppen);
      const n = sichtbare().zeigen.length, ges = gefiltert().length;
      el.ansage.textContent = `${n - vorher} weitere Beiträge geladen, ${n} von ${ges}.`;
      if (neu) neu.focus({ preventScroll: true });
      if (neu && !laeuft) neu.closest('.eintrag').scrollIntoView({ block: 'center', behavior: reduziert() ? 'auto' : 'smooth' });
    }, 280);
  });

  // Seitenzahlen: Fokus auf die Listenüberschrift
  el.seiten.addEventListener('click', e => {
    const a = e.target.closest('[data-seite]'); if (!a) return; e.preventDefault();
    if (a.getAttribute('aria-disabled') === 'true') return;
    zustand.seite = +a.dataset.seite; listeBauen(); urlSchreiben();
    el.titel.focus({ preventScroll: true });
    window.scrollTo({ top: el.liste.getBoundingClientRect().top + scrollY - 70, behavior: reduziert() || laeuft ? 'auto' : 'smooth' });
    el.ansage.textContent = el.titel.textContent;
  });

  // Variante
  function variante(neu, url = true) {
    v = WERTE[neu] ? neu : 'mehr'; html.dataset.v = v;
    $$('.pult [data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v));
    $('[data-werte]').textContent = WERTE[v];
    filterBauen(); listeBauen(); if (url) urlSchreiben();
  }
  $$('.pult [data-v]').forEach(b => b.addEventListener('click', () => { pause(); zustand.seite = 1; variante(b.dataset.v); }));
  $('[data-leer-zeigen]').addEventListener('click', () => { pause(); zustand.kat = 'presse'; zustand.jahr = '2024'; anwenden(false); el.liste.scrollIntoView({ behavior: reduziert() ? 'auto' : 'smooth' }); });

  // Menü auf schmalen Bildschirmen
  const mk = $('.menueknopf'), nv = $('#hauptmenue');
  mk.addEventListener('click', () => { const auf = !nv.hasAttribute('data-offen'); nv.toggleAttribute('data-offen', auf); mk.setAttribute('aria-expanded', auf); mk.textContent = auf ? 'Schließen' : 'Menü'; });
  addEventListener('keydown', e => { if (e.key === 'Escape' && nv.hasAttribute('data-offen')) { mk.click(); mk.focus(); } });

  // Start aus der URL
  const q = new URLSearchParams(location.search);
  if (KATS[q.get('kategorie')]) zustand.kat = q.get('kategorie');
  if (JAHRE.includes(q.get('jahr'))) zustand.jahr = q.get('jahr');
  zustand.seite = Math.max(1, parseInt(q.get('seite')) || 1);

  // Vorführung
  const zeiger = $('[data-zeiger]');
  let laeuft = false, timer = [];
  const warte = ms => new Promise(r => timer.push(setTimeout(r, ms)));
  async function gehe(elx, klick = true) {
    if (!elx || !laeuft) return;
    const r = elx.getBoundingClientRect();
    zeiger.style.transition = reduziert() ? 'none' : 'transform 600ms cubic-bezier(0.16, 1, 0.3, 1), opacity 200ms';
    zeiger.style.transform = `translate(${r.left + Math.min(r.width / 2, 70)}px, ${r.top + r.height / 2}px)`;
    await warte(reduziert() ? 120 : 650);
    if (klick && laeuft) { zeiger.classList.add('drueckt'); await warte(110); zeiger.classList.remove('drueckt'); elx.click(); }
  }
  const zuListe = () => scrollTo(0, $('.filter').getBoundingClientRect().top + scrollY - 8);
  async function runde() {
    if (!laeuft) return;
    zustand.kat = 'alle'; zustand.jahr = 'alle'; zustand.seite = 1; filterBauen(); listeBauen();
    scrollTo(0, 0); zeiger.style.transform = `translate(${innerWidth * .7}px, ${innerHeight * .8}px)`;
    await warte(1100); if (!laeuft) return;
    zuListe(); await warte(500);
    await gehe($('[data-kat="aktionen"]')); await warte(1300);
    await gehe(el.jahr, false); el.jahr.value = '2025'; el.jahr.dispatchEvent(new Event('change')); await warte(1400);
    await gehe($('[data-kat="presse"]')); await warte(500);
    el.jahr.value = '2024'; el.jahr.dispatchEvent(new Event('change')); await warte(1800);
    await gehe($('[data-weg="alle"]')); await warte(900); if (!laeuft) return;
    if (v === 'mehr') {
      scrollTo(0, el.mehr.getBoundingClientRect().top + scrollY - innerHeight * .55); await warte(500);
      await gehe(el.mehr); await warte(1500);
      scrollTo(0, el.mehr.getBoundingClientRect().top + scrollY - innerHeight * .55); await warte(400);
      await gehe(el.mehr); await warte(1500);
    } else {
      const s2 = $('[data-seite="2"]'); scrollTo(0, s2.getBoundingClientRect().top + scrollY - innerHeight * .6); await warte(400);
      await gehe(s2); await warte(1500);
      const s3 = $('[data-seite="3"]'); scrollTo(0, s3.getBoundingClientRect().top + scrollY - innerHeight * .6); await warte(400);
      await gehe(s3); await warte(1500);
    }
    if (laeuft) timer.push(setTimeout(runde, 600));
  }
  function neustart() { pause(); zustand.kat = 'alle'; zustand.jahr = 'alle'; zustand.seite = 1; variante(v, false); scrollTo(0, 0); }
  function vorfuehren() { pause(); html.classList.add('vorfuehrung'); laeuft = true; runde(); }
  function pause() { laeuft = false; timer.forEach(clearTimeout); timer = []; html.classList.remove('vorfuehrung'); }
  function weiter() { if (!laeuft) vorfuehren(); }

  variante(q.get('v') || 'mehr', false);

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