diff --git a/css/components.css b/css/components.css
new file mode 100644
index 0000000..210de1d
--- /dev/null
+++ b/css/components.css
@@ -0,0 +1,129 @@
+/* Wiederverwendbare Komponenten: Status-Banner, Buttons, Ergebnis-Cards,
+ Karten-Marker/Popups. */
+
+.app-status {
+ background: #fdecea;
+ color: var(--color-danger);
+ padding: var(--space-sm) var(--space-lg);
+ font-size: 0.9rem;
+ border-bottom: 1px solid var(--color-danger);
+}
+
+.btn {
+ border: none;
+ border-radius: var(--radius-sm);
+ padding: var(--space-sm) var(--space-md);
+ font-size: 0.85rem;
+ font-weight: 600;
+ box-shadow: var(--shadow-card);
+}
+
+.btn:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn--secondary {
+ background: var(--color-surface);
+ color: var(--color-primary-dark);
+}
+
+.map-panel__loading {
+ padding: var(--space-md);
+ color: var(--color-text-muted);
+}
+
+/* Ergebnisliste */
+
+.place-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-md);
+}
+
+.list-empty {
+ color: var(--color-text-muted);
+ padding: var(--space-md) 0;
+}
+
+.place-card {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-md);
+ padding: var(--space-md);
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-xs);
+ cursor: pointer;
+ transition: box-shadow 0.15s ease, transform 0.15s ease;
+}
+
+.place-card:hover,
+.place-card:focus-visible {
+ box-shadow: var(--shadow-card);
+ transform: translateY(-1px);
+ outline: none;
+}
+
+.place-card__name {
+ font-size: 1rem;
+ font-weight: 700;
+}
+
+.place-card__category {
+ align-self: flex-start;
+ color: #fff;
+ font-size: 0.75rem;
+ font-weight: 600;
+ padding: 2px var(--space-sm);
+ border-radius: 999px;
+}
+
+.place-card__desc {
+ color: var(--color-text-muted);
+ font-size: 0.875rem;
+ line-height: 1.4;
+}
+
+/* Karten-Marker (L.divIcon) */
+
+.eventmap-marker {
+ background: transparent;
+ border: none;
+}
+
+.eventmap-marker__glyph {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ box-shadow: var(--shadow-card);
+ font-size: 1rem;
+ line-height: 1;
+ border: 2px solid #fff;
+}
+
+/* Karten-Popup */
+
+.eventmap-popup {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ max-width: 220px;
+}
+
+.eventmap-popup__title {
+ font-size: 0.95rem;
+}
+
+.eventmap-popup__category {
+ font-size: 0.8rem;
+ font-weight: 600;
+}
+
+.eventmap-popup__desc {
+ font-size: 0.85rem;
+ color: var(--color-text-muted);
+}
diff --git a/css/layout.css b/css/layout.css
new file mode 100644
index 0000000..844c451
--- /dev/null
+++ b/css/layout.css
@@ -0,0 +1,59 @@
+/* Grundlegendes App-Layout: Header + Karte/Liste. Mobile-first;
+ Desktop-Anpassungen (>=1024px) siehe css/responsive.css. */
+
+.app-header {
+ height: var(--header-height);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 0 var(--space-lg);
+ background: var(--color-primary);
+ color: #fff;
+}
+
+.app-header__title {
+ font-size: 1.4rem;
+ font-weight: 700;
+}
+
+.app-header__subtitle {
+ font-size: 0.85rem;
+ opacity: 0.9;
+}
+
+.app-layout {
+ display: flex;
+ flex-direction: column;
+}
+
+.map-panel {
+ position: relative;
+ height: 50vh;
+ min-height: 320px;
+ display: flex;
+ flex-direction: column;
+}
+
+.map-panel__toolbar {
+ position: absolute;
+ top: var(--space-sm);
+ left: var(--space-sm);
+ z-index: 1000;
+}
+
+.map-panel__map {
+ flex: 1;
+ width: 100%;
+ height: 100%;
+ background: var(--color-border);
+}
+
+.list-panel {
+ padding: var(--space-md);
+ overflow-y: auto;
+}
+
+.list-panel__title {
+ font-size: 1.1rem;
+ margin-bottom: var(--space-md);
+}
diff --git a/css/reset.css b/css/reset.css
new file mode 100644
index 0000000..4a3e6dd
--- /dev/null
+++ b/css/reset.css
@@ -0,0 +1,47 @@
+/* Schlankes Reset - kein komplettes Normalize.css nötig für dieses Projekt. */
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
+ color: var(--color-text);
+ background: var(--color-background);
+}
+
+h1,
+h2,
+h3,
+p {
+ margin: 0;
+}
+
+ul,
+ol {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+button {
+ font: inherit;
+ cursor: pointer;
+}
+
+img {
+ max-width: 100%;
+ display: block;
+}
+
+a {
+ color: inherit;
+}
diff --git a/css/responsive.css b/css/responsive.css
new file mode 100644
index 0000000..dbb77e0
--- /dev/null
+++ b/css/responsive.css
@@ -0,0 +1,20 @@
+/* Desktop-Layout ab 1024px: Karte und Liste nebeneinander (Grid). */
+
+@media (min-width: 1024px) {
+ .app-layout {
+ display: grid;
+ grid-template-columns: 65% 35%;
+ height: calc(100vh - var(--header-height));
+ }
+
+ .map-panel {
+ height: 100%;
+ min-height: 0;
+ }
+
+ .list-panel {
+ height: 100%;
+ overflow-y: auto;
+ border-left: 1px solid var(--color-border);
+ }
+}
diff --git a/css/variables.css b/css/variables.css
new file mode 100644
index 0000000..efc9caf
--- /dev/null
+++ b/css/variables.css
@@ -0,0 +1,46 @@
+/* Design-Tokens der Anwendung. */
+
+:root {
+ /* Basisfarben */
+ --color-primary: #2e7d32;
+ --color-primary-dark: #1b5e20;
+ --color-text: #1a1a1a;
+ --color-text-muted: #5f6368;
+ --color-background: #f5f6f8;
+ --color-surface: #ffffff;
+ --color-border: #e0e0e0;
+ --color-danger: #c62828;
+
+ /* Kategorie-Farben (siehe js/config/categories.js) */
+ --category-naturpark: #2e7d32;
+ --category-wald-wanderweg: #1b5e20;
+ --category-see-badestelle: #0288d1;
+ --category-erlebnispark: #f57c00;
+ --category-freizeitpark: #e64a19;
+ --category-tierpark-zoo: #ff8f00;
+ --category-spielplatz: #d81b60;
+ --category-museum-kinder: #7b1fa2;
+ --category-indoor-spielplatz: #5e35b1;
+ --category-bauernhof: #6d4c41;
+ --category-kletterpark: #00897b;
+ --category-aussichtspunkt: #455a64;
+ --category-cafe-restaurant: #c62828;
+ --category-sonstige-aktivitaet: #00acc1;
+ --category-sonstige: #616161;
+
+ /* Spacing */
+ --space-xs: 4px;
+ --space-sm: 8px;
+ --space-md: 16px;
+ --space-lg: 24px;
+ --space-xl: 32px;
+
+ /* Radius/Shadow */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08);
+
+ /* Layout */
+ --breakpoint-desktop: 1024px;
+ --header-height: 64px;
+}
diff --git a/data/seed.js b/data/seed.js
new file mode 100644
index 0000000..6338a53
--- /dev/null
+++ b/data/seed.js
@@ -0,0 +1,298 @@
+// Beispiel-Ausflugsziele fuer den allerersten App-Start (leerer LocalStorage).
+// Als eingebettetes Script statt JSON-Datei, da fetch() lokaler Dateien unter
+// file:// (insbesondere in Chrome) von der Same-Origin-Policy blockiert wird.
+window.EventMap = window.EventMap || {};
+EventMap.data = EventMap.data || {};
+
+EventMap.data.SEED_PLACES = [
+ {
+ "name": "Tierpark Berlin",
+ "category": "Tierpark/Zoo",
+ "shortDescription": "Weitläufiger Tierpark im Grünen mit über 7.000 Tieren.",
+ "description": "Der Tierpark Berlin ist einer der größten Landschaftstierparks Europas. Auf großzügigen Freianlagen können Familien zahlreiche Tierarten beobachten, darunter Elefanten, Löwen und Pandas. Weitläufige Wege eignen sich gut für Kinderwagen, mehrere Spielplätze laden zwischendurch zum Toben ein.",
+ "latitude": 52.5063,
+ "longitude": 13.4996,
+ "address": "Am Tierpark 125",
+ "city": "Berlin",
+ "region": "Berlin",
+ "ageRecommendation": { "min": 0, "max": 14 },
+ "cost": { "type": "paid", "description": "Erwachsene ca. 16€, Kinder ca. 8€" },
+ "openingHours": "Täglich ab 9:00 Uhr, saisonal wechselnde Schließzeiten",
+ "durationMinutes": 240,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": true,
+ "toilets": true,
+ "parking": true,
+ "restaurant": true
+ },
+ "environment": "outdoor",
+ "website": "https://www.tierpark-berlin.de",
+ "image": "",
+ "rating": 4.6,
+ "tags": ["Tiere", "familienfreundlich", "Kinderwagen geeignet"],
+ "source": "Seed-Daten",
+ "sourceVerified": true
+ },
+ {
+ "name": "Spielplatz Westpark München",
+ "category": "Spielplatz",
+ "shortDescription": "Großer Spielplatz mit Klettergerüsten, Rutschen und Wasserspielbereich.",
+ "description": "Im Westpark München gibt es mehrere gut ausgestattete Spielplätze für unterschiedliche Altersgruppen, inklusive Wasserspielbereich im Sommer, Sandkästen und Klettergerüsten. Der Park bietet zudem viel Platz für Picknicks und lange Spaziergänge.",
+ "latitude": 48.1231,
+ "longitude": 11.5310,
+ "address": "Westendstraße 224",
+ "city": "München",
+ "region": "Bayern",
+ "ageRecommendation": { "min": 1, "max": 10 },
+ "cost": { "type": "free", "description": "Kostenlos" },
+ "openingHours": "Durchgehend geöffnet",
+ "durationMinutes": 90,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": true,
+ "toilets": true,
+ "parking": false,
+ "restaurant": false
+ },
+ "environment": "outdoor",
+ "website": "",
+ "image": "",
+ "rating": 4.3,
+ "tags": ["kostenlos", "Spielplatz", "Wasser"],
+ "source": "Seed-Daten",
+ "sourceVerified": true
+ },
+ {
+ "name": "Kletterwald Schluchsee",
+ "category": "Kletterpark",
+ "shortDescription": "Hochseilgarten mit Parcours für Kinder und Erwachsene im Schwarzwald.",
+ "description": "Der Kletterwald Schluchsee bietet verschiedene Parcours mit unterschiedlichen Schwierigkeitsgraden, darunter spezielle Kinderparcours ab 4 Jahren. Umgeben von dichtem Nadelwald direkt am Schluchsee gelegen.",
+ "latitude": 47.8206,
+ "longitude": 8.1653,
+ "address": "Kaiserhaustraße 1",
+ "city": "Schluchsee",
+ "region": "Baden-Württemberg",
+ "ageRecommendation": { "min": 4, "max": 99 },
+ "cost": { "type": "paid", "description": "Ab ca. 14€ pro Person, je nach Parcours" },
+ "openingHours": "April bis Oktober, 9:30 bis 18:00 Uhr (witterungsabhängig)",
+ "durationMinutes": 180,
+ "facilities": {
+ "strollerAccessible": false,
+ "wheelchairAccessible": false,
+ "toilets": true,
+ "parking": true,
+ "restaurant": true
+ },
+ "environment": "outdoor",
+ "website": "https://www.kletterwald-schluchsee.de",
+ "image": "",
+ "rating": 4.7,
+ "tags": ["Klettern", "Wald", "Tagesausflug"],
+ "source": "Seed-Daten",
+ "sourceVerified": true
+ },
+ {
+ "name": "Europa-Park Rust",
+ "category": "Freizeitpark",
+ "shortDescription": "Großer Freizeitpark mit Themenbereichen, Achterbahnen und Kinderfahrgeschäften.",
+ "description": "Der Europa-Park ist einer der größten Freizeitparks Europas mit europäisch thematisierten Bereichen. Neben actionreichen Achterbahnen gibt es zahlreiche ruhigere Fahrgeschäfte und einen eigenen Kinderbereich für die jüngsten Besucher.",
+ "latitude": 48.2669,
+ "longitude": 7.7220,
+ "address": "Europa-Park-Straße 2",
+ "city": "Rust",
+ "region": "Baden-Württemberg",
+ "ageRecommendation": { "min": 2, "max": 99 },
+ "cost": { "type": "paid", "description": "Tagesticket ab ca. 60€" },
+ "openingHours": "Saisonal geöffnet, i.d.R. 9:00 bis 18:00 Uhr",
+ "durationMinutes": 480,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": true,
+ "toilets": true,
+ "parking": true,
+ "restaurant": true
+ },
+ "environment": "mixed",
+ "website": "https://www.europapark.de",
+ "image": "",
+ "rating": 4.8,
+ "tags": ["Freizeitpark", "Tagesausflug", "Achterbahn"],
+ "source": "Seed-Daten",
+ "sourceVerified": true
+ },
+ {
+ "name": "Naturpark Schönbuch",
+ "category": "Naturpark",
+ "shortDescription": "Ausgedehntes Waldgebiet mit markierten Wanderwegen und Wildgehegen.",
+ "description": "Der Naturpark Schönbuch bei Tübingen bietet zahlreiche familienfreundliche Wanderwege, Waldlehrpfade und ein Wildgehege mit heimischen Tierarten. Ideal für einen entspannten Tag in der Natur.",
+ "latitude": 48.5667,
+ "longitude": 9.0667,
+ "address": "Schönbuchturm",
+ "city": "Herrenberg",
+ "region": "Baden-Württemberg",
+ "ageRecommendation": { "min": 0, "max": 99 },
+ "cost": { "type": "free", "description": "Kostenlos" },
+ "openingHours": "Durchgehend geöffnet",
+ "durationMinutes": 150,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": false,
+ "toilets": false,
+ "parking": true,
+ "restaurant": false
+ },
+ "environment": "outdoor",
+ "website": "",
+ "image": "",
+ "rating": 4.5,
+ "tags": ["Natur", "Wandern", "kostenlos", "Tiere"],
+ "source": "Seed-Daten",
+ "sourceVerified": false
+ },
+ {
+ "name": "Partnachklamm Wanderweg",
+ "category": "Wald/Wanderweg",
+ "shortDescription": "Spektakuläre Felsschlucht bei Garmisch-Partenkirchen, gut begehbarer Weg.",
+ "description": "Der Weg durch die Partnachklamm führt über gut gesicherte Wege und teilweise durch Tunnel entlang der tosenden Partnach. Für Kinder ab dem Schulalter ein beeindruckendes Naturerlebnis, festes Schuhwerk empfohlen.",
+ "latitude": 47.4931,
+ "longitude": 11.0817,
+ "address": "Am Kurpark",
+ "city": "Garmisch-Partenkirchen",
+ "region": "Bayern",
+ "ageRecommendation": { "min": 6, "max": 99 },
+ "cost": { "type": "paid", "description": "Eintritt ca. 6€, Kinder ermäßigt" },
+ "openingHours": "Ganzjährig, witterungsabhängig gesperrt",
+ "durationMinutes": 90,
+ "facilities": {
+ "strollerAccessible": false,
+ "wheelchairAccessible": false,
+ "toilets": true,
+ "parking": true,
+ "restaurant": true
+ },
+ "environment": "outdoor",
+ "website": "",
+ "image": "",
+ "rating": 4.7,
+ "tags": ["Wandern", "Natur", "Schlucht"],
+ "source": "Seed-Daten",
+ "sourceVerified": false
+ },
+ {
+ "name": "Strandbad Herrsching am Ammersee",
+ "category": "See/Badestelle",
+ "shortDescription": "Familienfreundliches Strandbad mit flachem Uferbereich am Ammersee.",
+ "description": "Das Strandbad Herrsching bietet einen flach abfallenden Badebereich, eine große Liegewiese und einen Spielplatz direkt am See. Ideal für einen entspannten Tag am Wasser mit kleinen Kindern.",
+ "latitude": 48.0000,
+ "longitude": 11.1667,
+ "address": "Seepromenade",
+ "city": "Herrsching am Ammersee",
+ "region": "Bayern",
+ "ageRecommendation": { "min": 0, "max": 99 },
+ "cost": { "type": "free", "description": "Kostenlos, Liegestuhlverleih gegen Gebühr" },
+ "openingHours": "Durchgehend zugänglich",
+ "durationMinutes": 180,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": true,
+ "toilets": true,
+ "parking": true,
+ "restaurant": true
+ },
+ "environment": "outdoor",
+ "website": "",
+ "image": "",
+ "rating": 4.4,
+ "tags": ["Baden", "See", "kostenlos"],
+ "source": "Seed-Daten",
+ "sourceVerified": false
+ },
+ {
+ "name": "Junges Schloss Stuttgart",
+ "category": "Museum für Kinder",
+ "shortDescription": "Kindermuseum im Alten Schloss mit interaktiven Mitmachstationen.",
+ "description": "Das Junge Schloss ist das Landesmuseum für Kinder in Stuttgart und vermittelt Geschichte spielerisch über zahlreiche Mitmach-Stationen, Verkleidungen und Rätsel. Besonders für Grundschulkinder geeignet.",
+ "latitude": 48.7791,
+ "longitude": 9.1804,
+ "address": "Schillerplatz 6",
+ "city": "Stuttgart",
+ "region": "Baden-Württemberg",
+ "ageRecommendation": { "min": 5, "max": 12 },
+ "cost": { "type": "paid", "description": "Eintritt ca. 4€, Kinder unter 6 Jahren frei" },
+ "openingHours": "Di–So 10:00–17:00 Uhr, montags geschlossen",
+ "durationMinutes": 120,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": true,
+ "toilets": true,
+ "parking": false,
+ "restaurant": false
+ },
+ "environment": "indoor",
+ "website": "https://www.jungesschloss.de",
+ "image": "",
+ "rating": 4.5,
+ "tags": ["Museum", "Geschichte", "Indoor"],
+ "source": "Seed-Daten",
+ "sourceVerified": true
+ },
+ {
+ "name": "Jump House Hamburg",
+ "category": "Indoor-Spielplatz",
+ "shortDescription": "Große Trampolinhalle mit Bereichen für kleine und große Kinder.",
+ "description": "Im Jump House Hamburg können Kinder auf zahlreichen vernetzten Trampolinen springen und toben. Ein separater Bereich für Kleinkinder sorgt für sicheren Spaß auch bei den Jüngsten - ideal bei schlechtem Wetter.",
+ "latitude": 53.5511,
+ "longitude": 9.9937,
+ "address": "Amsinckstraße 57",
+ "city": "Hamburg",
+ "region": "Hamburg",
+ "ageRecommendation": { "min": 2, "max": 16 },
+ "cost": { "type": "paid", "description": "Ab ca. 10€ pro Stunde" },
+ "openingHours": "Täglich 10:00–20:00 Uhr",
+ "durationMinutes": 90,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": true,
+ "toilets": true,
+ "parking": true,
+ "restaurant": true
+ },
+ "environment": "indoor",
+ "website": "",
+ "image": "",
+ "rating": 4.2,
+ "tags": ["Indoor", "bei schlechtem Wetter geeignet", "Trampolin"],
+ "source": "Seed-Daten",
+ "sourceVerified": false
+ },
+ {
+ "name": "Erlebnisbauernhof Gut Sonnenhof",
+ "category": "Bauernhof",
+ "shortDescription": "Bauernhof zum Anfassen mit Streichelzoo und Traktorfahrten.",
+ "description": "Auf dem Erlebnisbauernhof Gut Sonnenhof können Kinder Tiere füttern und streicheln, an Traktorfahrten teilnehmen und den Alltag auf einem Bauernhof hautnah erleben. Ein Café mit hofeigenen Produkten lädt zur Rast ein.",
+ "latitude": 52.5000,
+ "longitude": 9.5000,
+ "address": "Hofstraße 3",
+ "city": "Neustadt am Rübenberge",
+ "region": "Niedersachsen",
+ "ageRecommendation": { "min": 1, "max": 10 },
+ "cost": { "type": "paid", "description": "Eintritt ca. 5€, Kinder unter 2 Jahren frei" },
+ "openingHours": "Sa–So und Feiertage 10:00–17:00 Uhr",
+ "durationMinutes": 150,
+ "facilities": {
+ "strollerAccessible": true,
+ "wheelchairAccessible": false,
+ "toilets": true,
+ "parking": true,
+ "restaurant": true
+ },
+ "environment": "outdoor",
+ "website": "",
+ "image": "",
+ "rating": 4.4,
+ "tags": ["Tiere", "Bauernhof", "mit Tieren"],
+ "source": "Seed-Daten",
+ "sourceVerified": false
+ }
+];
diff --git a/docs/adr/0001-vanilla-esm.md b/docs/adr/0001-vanilla-esm.md
new file mode 100644
index 0000000..0a8d47e
--- /dev/null
+++ b/docs/adr/0001-vanilla-esm.md
@@ -0,0 +1,98 @@
+# ADR 0001: Kein Build-Tooling, klassische Scripts mit globalem Namespace, Leaflet lokal vendored
+
+## Status
+
+Akzeptiert (überarbeitet – siehe "Revision" unten; ursprüngliche
+Entscheidung für native ES-Module wurde revidiert)
+
+## Kontext
+
+`anforderung.md` fordert explizit "möglichst einfache und gut verständliche
+Technologien" (HTML5, CSS3, JavaScript, eine Open-Source-Kartenbibliothek,
+LocalStorage) und stellt keinen Backend-Zwang für die erste Version. Die
+Anwendung soll außerdem vollständig responsiv sein und ohne größere
+Infrastruktur betrieben und gewartet werden können.
+
+Die tatsächliche Zielumgebung des Nutzers wurde präzisiert: Die App wird
+per **Doppelklick auf `index.html`** direkt aus einem lokal synchronisierten
+Nextcloud-Ordner geöffnet (`file://...`). Es steht **kein lokaler
+HTTP-Server** zur Verfügung und ein solcher soll vom Endnutzer auch nicht
+manuell gestartet werden müssen – Priorität hat maximale Einfachheit für den
+Endnutzer, nicht Entwicklerkomfort.
+
+## Revision (2026-07-24)
+
+Die ursprüngliche Entscheidung, JavaScript als native ES-Module
+(`
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+