TEST Kerstin: Unterschied zwischen den Versionen

Aus Bündnis für Familie Tübingen.
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
<!-- ============================================================
from pathlib import Path
    20 JAHRE BÜNDNIS FÜR FAMILIE TÜBINGEN
import re, html, json
    BEGLEITPROGRAMM 2026
   
    KOMPLETTER RESPONSIVER VERANSTALTUNGSKALENDER
   
    Für MediaWiki:
    Diesen gesamten Block in die Jubiläumsseite übernehmen.
    Veranstaltungen werden ausschließlich im events-Array gepflegt.
    ============================================================ -->


src = Path("/mnt/data/Eingefügter Text(8).txt")
text = src.read_text(encoding="utf-8")


<style>
events = re.findall(
/* ============================================================
    r'<div class="event"\s+title="(.*?)">\s*<div class="et">(.*?)</div>\s*</div>',
  GRUNDLAGEN
    text, flags=re.S
  ============================================================ */
)


#buefa-jubilaeum {
def parse_info(title):
     --buefa-orange: #e86f2d;
     title = html.unescape(title)
     --buefa-orange-light: #fff1e8;
     parts = {}
     --buefa-yellow: #f4c542;
     for key in ["Uhrzeit", "Zielgruppe", "Treffpunkt", "Beschreibung"]:
    --buefa-green: #6a9f58;
        m = re.search(
    --buefa-blue: #5b8fb9;
            rf'{key}:\s*(.*?)(?=\n\s*(?:Uhrzeit|Zielgruppe|Treffpunkt|Beschreibung):|$)',
    --buefa-purple: #8b6aa8;
            title, re.S
    --buefa-text: #333333;
        )
    --buefa-text-light: #666666;
        if m:
    --buefa-border: #e5e5e5;
            parts[key] = " ".join(m.group(1).split())
    --buefa-bg: #f8f7f4;
     return parts
     --buefa-white: #ffffff;


     max-width: 1200px;
date_map = [
     margin: 0 auto;
     ("2026-10-06","Oktober 2026",6), ("2026-10-08","Oktober 2026",8),
     color: var(--buefa-text);
     ("2026-10-09","Oktober 2026",9), ("2026-10-09","Oktober 2026",9),
     font-family: Arial, Helvetica, sans-serif;
     ("2026-10-09","Oktober 2026",9), ("2026-10-21","Oktober 2026",21),
     line-height: 1.5;
     ("2026-10-22","Oktober 2026",22), ("2026-10-31","Oktober 2026",31),
     box-sizing: border-box;
     ("2026-11-05","November 2026",5), ("2026-11-05","November 2026",5),
}
     ("2026-11-09","November 2026",9), ("2026-11-10","November 2026",10),
    ("2026-11-10","November 2026",10), ("2026-11-11","November 2026",11),
    ("2026-11-15","November 2026",15), ("2026-11-17","November 2026",17),
    ("2026-11-19","November 2026",19), ("2026-11-21","November 2026",21),
    ("2026-11-24","November 2026",24), ("2026-11-24","November 2026",24),
    ("2026-11-25","November 2026",25), ("2026-11-26","November 2026",26)
]
assert len(events) == len(date_map)


#buefa-jubilaeum *,
records=[]
#buefa-jubilaeum *::before,
for i, ((date, month, day), (title, visible)) in enumerate(zip(date_map, events), 1):
#buefa-jubilaeum *::after {
    info=parse_info(title)
    box-sizing: border-box;
    records.append({
}
        "id":i, "date":date, "month":month, "day":day,
        "title":" ".join(html.unescape(visible).split()),
        "time":info.get("Uhrzeit",""),
        "audience":info.get("Zielgruppe",""),
        "place":info.get("Treffpunkt",""),
        "description":info.get("Beschreibung","")
    })


data_json=json.dumps(records,ensure_ascii=False,indent=2)


/* ============================================================
html_out = """<!doctype html>
  HERO
<html lang="de">
  ============================================================ */
<head>
 
<meta charset="utf-8">
.buefa-hero {
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
    background:
<title>Familienangebote – Oktober & November 2026</title>
        linear-gradient(
<style>
            135deg,
:root{--bg:#f6f7fb;--surface:#fff;--surface2:#fbfcfe;--text:#1f2937;--muted:#667085;--line:#dbe1ea;--accent:#0f766e;--accent-soft:#ecfdf5;--shadow:0 8px 24px rgba(0,0,0,.06);--radius:16px}
            rgba(232,111,45,0.96),
*{box-sizing:border-box}
            rgba(244,197,66,0.94)
html{scroll-behavior:smooth}
        );
body{margin:0;font-family:Inter,Arial,sans-serif;background:var(--bg);color:var(--text);line-height:1.45}
    border-radius: 22px;
.wrap{width:min(1200px,100%);margin:auto;padding:24px}
    padding: 48px 45px;
h1{font-size:clamp(1.65rem,4vw,2.6rem);line-height:1.15;margin:0 0 8px}
    margin: 0 0 35px 0;
h2{font-size:clamp(1.2rem,2.5vw,1.45rem);margin:0 0 16px}
    color: #ffffff;
.intro,.details-intro{color:var(--muted);margin:0 0 22px}
    position: relative;
.month{background:var(--surface);border:1px solid var(--line);border-radius:var(--radius);padding:18px;margin-bottom:24px;box-shadow:var(--shadow)}
    overflow: hidden;
.weekdays,.grid{display:grid;grid-template-columns:repeat(7,minmax(0,1fr));gap:8px}
}
.weekdays div{font-size:.86rem;font-weight:700;color:var(--muted);padding:0 5px 5px}
 
.day{min-height:112px;background:var(--surface2);border:1px solid var(--line);border-radius:12px;padding:8px;display:flex;flex-direction:column;gap:6px;min-width:0}
.buefa-hero::after {
.day.muted{opacity:.45}
    content: "20";
.dn{font-size:.86rem;font-weight:800;color:#374151}
    position: absolute;
.event{background:var(--accent-soft);border-left:4px solid var(--accent);padding:7px 8px;border-radius:8px;cursor:pointer;min-width:0}
    right: 25px;
.et{font-size:.82rem;font-weight:650;line-height:1.3;overflow-wrap:anywhere}
    bottom: -45px;
.details-section{margin-top:34px}
    font-size: 190px;
.offer-list{display:grid;gap:14px}
    font-weight: 900;
.offer{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px 18px;box-shadow:0 4px 14px rgba(0,0,0,.04)}
    line-height: 1;
.offer-header{display:flex;gap:12px;align-items:flex-start;justify-content:space-between;margin-bottom:10px}
    opacity: 0.12;
.offer-date{flex:0 0 auto;background:var(--accent);color:#fff;border-radius:9px;padding:5px 8px;font-size:.82rem;font-weight:800;white-space:nowrap}
}
.offer-title{flex:1;font-size:1rem;font-weight:750}
 
.offer-info{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px 18px}
.buefa-hero-content {
.info-row{min-width:0}
    position: relative;
.info-label{display:block;font-size:.76rem;font-weight:800;color:var(--muted);text-transform:uppercase;letter-spacing:.03em;margin-bottom:2px}
    z-index: 2;
.info-value{overflow-wrap:anywhere}
    max-width: 760px;
.info-description{grid-column:1/-1;margin-top:4px}
}
.tooltip{position:fixed;display:none;pointer-events:none;max-width:min(420px,calc(100vw - 24px));background:#fff;border:1px solid #cfd8e3;border-radius:10px;padding:12px 14px;box-shadow:0 8px 24px rgba(0,0,0,.2);font-size:.85rem;line-height:1.4;white-space:pre-line;z-index:9999}
 
@media(max-width:900px){.wrap{padding:18px}.day{min-height:100px}.weekdays,.grid{gap:6px}.offer-info{grid-template-columns:1fr}}
.buefa-kicker {
@media(max-width:640px){
    font-size: 15px;
.wrap{padding:12px}.month{padding:12px;border-radius:12px}
    font-weight: 700;
.weekdays{display:none}
    letter-spacing: 1.5px;
.grid{display:flex;flex-direction:column;gap:8px}
    text-transform: uppercase;
.day{min-height:0;display:block;padding:9px 10px;border-radius:10px}
    margin-bottom: 10px;
.day:has(.event){border-color:#b7ddd4;background:#fff}
}
.day:not(:has(.event)){display:none}
 
.dn{font-size:.9rem;margin-bottom:5px}
.buefa-hero h1 {
.event{margin-top:6px;padding:9px 10px}
    color: #ffffff !important;
.et{font-size:.88rem}
    font-size: clamp(30px, 5vw, 52px);
.offer{padding:14px}.offer-header{display:block}.offer-date{display:inline-block;margin-bottom:7px}.offer-title{display:block}
    line-height: 1.08;
.info-description{grid-column:auto}.tooltip{display:none!important}
    margin: 0 0 18px 0;
    font-weight: 800;
}
 
.buefa-hero p {
    font-size: 19px;
    line-height: 1.55;
    margin: 0;
    max-width: 700px;
}
 
 
/* ============================================================
  SCHNELLE EINSTIEGE
  ============================================================ */
 
.buefa-intro {
    text-align: center;
    margin: 0 0 28px 0;
}
 
.buefa-intro h2 {
    font-size: 30px;
    margin: 0 0 8px 0;
    color: var(--buefa-text);
}
 
.buefa-intro p {
    color: var(--buefa-text-light);
    margin: 0;
}
 
.buefa-month-buttons {
    display: flex;
    gap: 10px;
    justify-content: center;
    flex-wrap: wrap;
    margin: 25px 0 38px 0;
}
 
.buefa-month-button {
    border: 2px solid var(--buefa-orange);
    background: #ffffff;
    color: var(--buefa-orange);
    padding: 11px 20px;
    border-radius: 30px;
    font-weight: 700;
    cursor: pointer;
    transition: all .2s ease;
}
 
.buefa-month-button:hover,
.buefa-month-button.active {
    background: var(--buefa-orange);
    color: #ffffff;
}
 
 
/* ============================================================
  ALS NÄCHSTES
  ============================================================ */
 
.buefa-next {
    margin: 0 0 42px 0;
}
 
.buefa-section-title {
    font-size: 25px;
    font-weight: 800;
    margin: 0 0 16px 0;
}
 
.buefa-next-card {
    background: var(--buefa-orange-light);
    border-left: 7px solid var(--buefa-orange);
    border-radius: 16px;
    padding: 25px 28px;
}
 
.buefa-next-label {
    color: var(--buefa-orange);
    font-size: 13px;
    font-weight: 800;
    text-transform: uppercase;
    letter-spacing: 1px;
    margin-bottom: 8px;
}
 
.buefa-next-card h3 {
    margin: 0 0 12px 0;
    font-size: 25px;
}
 
.buefa-next-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 12px 25px;
    color: #555555;
    margin-bottom: 15px;
}
 
.buefa-next-description {
    color: #555555;
}
 
 
/* ============================================================
  FILTER
  ============================================================ */
 
.buefa-controls {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 20px;
    margin-bottom: 25px;
    flex-wrap: wrap;
}
 
.buefa-filter {
    display: flex;
    gap: 8px;
    flex-wrap: wrap;
}
 
.buefa-filter button {
    border: 1px solid var(--buefa-border);
    background: #ffffff;
    color: #555555;
    border-radius: 22px;
    padding: 8px 15px;
    cursor: pointer;
    font-size: 14px;
}
 
.buefa-filter button.active {
    background: #333333;
    color: #ffffff;
    border-color: #333333;
}
 
 
/* ============================================================
  MONAT
  ============================================================ */
 
.buefa-month-section {
    margin: 0 0 45px 0;
}
 
.buefa-month-heading {
    display: flex;
    align-items: baseline;
    gap: 15px;
    border-bottom: 3px solid var(--buefa-orange);
    padding-bottom: 10px;
    margin-bottom: 20px;
}
 
.buefa-month-heading h2 {
    margin: 0;
    font-size: 31px;
}
 
.buefa-month-heading span {
    color: var(--buefa-text-light);
    font-size: 15px;
}
 
 
/* ============================================================
  VERANSTALTUNGS-KARTE
  ============================================================ */
 
.buefa-event {
    display: grid;
    grid-template-columns: 105px 1fr;
    gap: 25px;
    background: #ffffff;
    border: 1px solid var(--buefa-border);
    border-radius: 17px;
    margin-bottom: 14px;
    overflow: hidden;
    transition:
        transform .2s ease,
        box-shadow .2s ease,
        border-color .2s ease;
}
 
.buefa-event:hover {
    transform: translateY(-2px);
    box-shadow: 0 7px 22px rgba(0,0,0,.08);
    border-color: #d5d5d5;
}
 
 
/* Datum */
 
.buefa-date {
    background: var(--buefa-bg);
    min-height: 135px;
    padding: 20px 10px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
}
 
.buefa-weekday {
    font-size: 13px;
    font-weight: 800;
    text-transform: uppercase;
    color: var(--buefa-orange);
}
 
.buefa-day {
    font-size: 42px;
    line-height: 1;
    font-weight: 900;
    margin: 3px 0;
}
 
.buefa-month-small {
    font-size: 13px;
    font-weight: 700;
    text-transform: uppercase;
    color: #666666;
}
 
 
/* Inhalt */
 
.buefa-event-content {
    padding: 20px 22px 20px 0;
}
 
.buefa-event-title {
    margin: 0 0 8px 0;
    font-size: 21px;
    line-height: 1.3;
    font-weight: 800;
}
 
.buefa-event-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 7px 20px;
    color: #555555;
    font-size: 15px;
    margin-bottom: 9px;
}
 
.buefa-event-description {
    color: #666666;
    font-size: 15px;
    margin: 0 0 12px 0;
}
 
.buefa-event-actions {
    display: flex;
    gap: 10px;
    flex-wrap: wrap;
}
 
.buefa-button {
    border: none;
    border-radius: 20px;
    padding: 8px 15px;
    cursor: pointer;
    font-size: 14px;
    font-weight: 700;
}
 
.buefa-button-primary {
    background: var(--buefa-orange);
    color: #ffffff;
}
 
.buefa-button-secondary {
    background: #f1f1f1;
    color: #444444;
}
 
 
/* Details */
 
.buefa-details {
    display: none;
    margin-top: 15px;
    padding-top: 15px;
    border-top: 1px solid var(--buefa-border);
    color: #555555;
}
 
.buefa-details.open {
    display: block;
}
 
.buefa-details-row {
    margin-bottom: 8px;
}
 
.buefa-details-label {
    font-weight: 800;
    margin-right: 6px;
}
 
 
/* Kategorien */
 
.buefa-category {
    display: inline-block;
    border-radius: 15px;
    padding: 4px 10px;
    font-size: 12px;
    font-weight: 700;
    margin-bottom: 8px;
}
 
.category-familie {
    background: #e8f3e3;
    color: #507a43;
}
 
.category-kultur {
    background: #eee6f5;
    color: #73558c;
}
 
.category-wissen {
    background: #e5f0f8;
    color: #4d7798;
}
 
.category-workshop {
    background: #fff1d6;
    color: #9a6b1d;
}
 
.category-jubilaeum {
    background: #fff0e7;
    color: #bd5b25;
}
 
 
/* Vorläufig */
 
.buefa-event.provisional {
    border-left: 6px solid var(--buefa-yellow);
}
 
.buefa-provisional-label {
    display: inline-block;
    background: #fff5d8;
    color: #85651b;
    padding: 4px 9px;
    border-radius: 12px;
    font-size: 12px;
    font-weight: 800;
    margin-bottom: 8px;
}
 
 
/* Vergangene Termine */
 
.buefa-event.past {
    opacity: .55;
}
 
.buefa-past-label {
    font-size: 12px;
    color: #777777;
    font-weight: 700;
}
 
 
/* Keine Termine */
 
.buefa-empty {
    background: var(--buefa-bg);
    border-radius: 15px;
    padding: 25px;
    text-align: center;
    color: #777777;
}
 
 
/* ============================================================
  IN KLÄRUNG
  ============================================================ */
 
.buefa-open {
    background: #f7f5ef;
    border-radius: 18px;
    padding: 25px;
    margin-top: 35px;
}
 
.buefa-open h2 {
    margin-top: 0;
}
 
 
/* ============================================================
  FOOTER
  ============================================================ */
 
.buefa-footer {
    margin-top: 45px;
    background: var(--buefa-bg);
    border-radius: 18px;
    padding: 30px;
    text-align: center;
}
 
.buefa-footer h2 {
    margin-top: 0;
}
 
.buefa-footer p {
    max-width: 700px;
    margin: 0 auto;
    color: #666666;
}
 
 
/* ============================================================
  MOBILE
  ============================================================ */
 
@media (max-width: 700px) {
 
    #buefa-jubilaeum {
        width: 100%;
    }
 
    .buefa-hero {
        border-radius: 0;
        margin-left: -12px;
        margin-right: -12px;
        padding: 35px 22px;
    }
 
    .buefa-hero h1 {
        font-size: 34px;
    }
 
    .buefa-hero p {
        font-size: 17px;
    }
 
    .buefa-hero::after {
        font-size: 120px;
        right: 0;
    }
 
    .buefa-intro h2 {
        font-size: 25px;
    }
 
    .buefa-month-buttons {
        justify-content: flex-start;
        flex-wrap: nowrap;
        overflow-x: auto;
        padding-bottom: 5px;
        scrollbar-width: none;
    }
 
    .buefa-month-buttons::-webkit-scrollbar {
        display: none;
    }
 
    .buefa-month-button {
        flex: 0 0 auto;
    }
 
    .buefa-next-card {
        padding: 20px;
    }
 
    .buefa-next-card h3 {
        font-size: 21px;
    }
 
    .buefa-event {
        display: block;
        border-radius: 15px;
    }
 
    .buefa-date {
        min-height: auto;
        padding: 10px 15px;
        flex-direction: row;
        justify-content: flex-start;
        gap: 8px;
    }
 
    .buefa-weekday,
    .buefa-day,
    .buefa-month-small {
        font-size: 14px;
        line-height: 1.2;
    }
 
    .buefa-day {
        font-size: 22px;
    }
 
    .buefa-event-content {
        padding: 16px;
    }
 
    .buefa-event-title {
        font-size: 19px;
    }
 
    .buefa-event-meta {
        display: block;
    }
 
    .buefa-event-meta span {
        display: block;
        margin-bottom: 5px;
    }
 
    .buefa-event-actions {
        display: grid;
        grid-template-columns: 1fr 1fr;
    }
 
    .buefa-button {
        width: 100%;
    }
 
    .buefa-month-heading {
        display: block;
    }
 
    .buefa-month-heading h2 {
        font-size: 26px;
    }
 
    .buefa-controls {
        display: block;
    }
 
    .buefa-filter {
        overflow-x: auto;
        flex-wrap: nowrap;
        padding-bottom: 5px;
        scrollbar-width: none;
    }
 
    .buefa-filter::-webkit-scrollbar {
        display: none;
    }
 
    .buefa-filter button {
        flex: 0 0 auto;
    }
}
}
 
@media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
 
/* ============================================================
  SEHR KLEINE BILDSCHIRME
  ============================================================ */
 
@media (max-width: 420px) {
 
    .buefa-event-actions {
        grid-template-columns: 1fr;
    }
 
    .buefa-section-title {
        font-size: 22px;
    }
}
 
</style>
</style>
 
</head>
 
<body>
<div id="buefa-jubilaeum">
<div class="wrap">
 
<h1>Familienangebote – Oktober & November 2026</h1>
    <!-- ========================================================
<p class="intro">Alle im Kalender vorhandenen Termine sind enthalten. Unter dem Kalender finden Sie sämtliche Informationen zu jedem einzelnen Angebot.</p>
        HERO
<div id="calendar"></div>
        ======================================================== -->
<section class="details-section" id="angebote">
 
<h2>Alle Angebote im Überblick</h2>
    <section class="buefa-hero">
<p class="details-intro">Hier finden Sie zu jedem Termin Datum, Uhrzeit, Zielgruppe, Treffpunkt und die vollständige Beschreibung.</p>
 
<div id="offerList" class="offer-list"></div>
        <div class="buefa-hero-content">
</section>
 
            <div class="buefa-kicker">
                20 Jahre Bündnis für Familie Tübingen
            </div>
 
            <h1>
                Unser Jubiläumsprogramm
            </h1>
 
            <p>
                Von Oktober bis Dezember 2026 feiern wir gemeinsam
                20 Jahre Bündnis für Familie Tübingen.
                Entdecken Sie Veranstaltungen, Workshops,
                Gespräche, Kultur und Angebote für Familien.
            </p>
 
        </div>
 
    </section>
 
 
    <!-- ========================================================
        EINSTIEG
        ======================================================== -->
 
    <section class="buefa-intro">
 
        <h2>
            Was ist wann und wo?
        </h2>
 
        <p>
            Hier finden Sie alle Veranstaltungen auf einen Blick.
        </p>
 
    </section>
 
 
    <!-- MONATSBUTTONS -->
 
    <div
        class="buefa-month-buttons"
        id="monthButtons">
    </div>
 
 
    <!-- ========================================================
        ALS NÄCHSTES
        ======================================================== -->
 
    <section class="buefa-next">
 
        <h2 class="buefa-section-title">
            Als Nächstes
        </h2>
 
        <div id="nextEvent">
        </div>
 
    </section>
 
 
    <!-- ========================================================
        FILTER
        ======================================================== -->
 
    <div class="buefa-controls">
 
        <div>
            <strong>
                Alle Veranstaltungen
            </strong>
        </div>
 
        <div
            class="buefa-filter"
            id="categoryFilter">
 
            <button
                class="active"
                data-category="alle">
                Alle
            </button>
 
            <button data-category="familie">
                Familie
            </button>
 
            <button data-category="wissen">
                Wissen
            </button>
 
            <button data-category="workshop">
                Workshop
            </button>
 
            <button data-category="kultur">
                Kultur
            </button>
 
            <button data-category="jubilaeum">
                Jubiläum
            </button>
 
        </div>
 
    </div>
 
 
    <!-- ========================================================
        VERANSTALTUNGEN
        ======================================================== -->
 
    <div id="eventList">
    </div>
 
 
    <!-- ========================================================
        IN KLÄRUNG
        ======================================================== -->
 
    <section
        class="buefa-open"
        id="provisionalEvents"
        style="display:none;">
 
        <h2>
            Noch in Klärung
        </h2>
 
        <div id="provisionalList">
        </div>
 
    </section>
 
 
    <!-- ========================================================
        FOOTER
        ======================================================== -->
 
    <section class="buefa-footer">
 
        <h2>
            20 Jahre Bündnis für Familie Tübingen
        </h2>
 
        <p>
            Viele Menschen, viele Ideen und viele Angebote –
            gemeinsam für Familien in Tübingen.
        </p>
 
    </section>
 
</div>
</div>
 
<div id="tooltip" class="tooltip" aria-hidden="true"></div>
 
 
<script>
<script>
(function () {
const EVENTS = __DATA__;
 
"use strict";
 
 
/* ============================================================
  VERANSTALTUNGEN
  ============================================================
 
  HIER WERDEN DIE VERANSTALTUNGEN GEPFLEGT.
 
  Datum:
  YYYY-MM-DD
 
  Uhrzeit:
  HH:MM
 
  Kategorie:
  familie
  wissen
  workshop
  kultur
  jubilaeum
 
  provisional:
  true = vorläufig / noch nicht endgültig
 
  url:
  Wenn vorhanden, wird ein "Mehr Informationen"-Button
  erzeugt.
 
  description:
  Kurze Beschreibung.
 
  location:
  Veranstaltungsort.
  ============================================================ */
 
 
const events = [
 
    {
        date: "2026-10-06",
        time: "20:00",
        title: "Hochsensible Kinder gut unterstützen und begleiten",
        location: "Tübingen",
        category: "wissen",
        description: "Informationen und Impulse rund um den Umgang mit hochsensiblen Kindern."
    },
 
    {
        date: "2026-10-08",
        time: "18:00",
        title: "Filmvorführung mit Gespräch über den Film „All Inclusive“",
        location: "Tübingen",
        category: "kultur",
        description: "Filmvorführung mit anschließendem Gespräch."
    },
 
    {
        date: "2026-10-09",
        time: "15:00",
        title: "Vorlesezeit in französischer Sprache mit dem LESE-HAUS",
        location: "Stadtbücherei Tübingen",
        category: "familie",
        description: "Vorlesen für Kinder in französischer Sprache."
    },
 
    {
        date: "2026-10-09",
        time: "16:00",
        title: "Vorlesezeit mit dem LESE-HAUS",
        location: "Stadtbücherei Tübingen",
        category: "familie",
        description: "Gemeinsame Vorlesezeit für Kinder und Familien."
    },
 
    {
        date: "2026-10-09",
        time: "17:00",
        title: "Vorlesezeit in ukrainischer Sprache mit dem LESE-HAUS",
        location: "Stadtbücherei Tübingen",
        category: "familie",
        description: "Vorlesen für Kinder in ukrainischer Sprache."
    },
 
    {
        date: "2026-10-09",
        time: "18:00",
        title: "Jubiläumsfeier & Eröffnung der Ausstellung „Familien in Tübingen – viele Formen von Zuhause“",
        location: "Stadtbücherei Tübingen",
        category: "jubilaeum",
        description: "Eröffnung der Jubiläumsausstellung und gemeinsamer Auftakt des Jubiläumsprogramms."
    },
 
    {
        date: "2026-10-21",
        time: "",
        title: "Familiensprachen und Sprachencafés im elkiko Familienzentrum",
        location: "elkiko Familienzentrum",
        category: "familie",
        description: "Veranstaltung ist noch vorläufig.",
        provisional: true
    },
 
    {
        date: "2026-10-22",
        time: "18:00",
        title: "Preisverleihung des 23. Schreibwettbewerbs für junge Menschen der Buchhandlung Wekenmann",
        location: "Tübingen",
        category: "kultur",
        description: "Preisverleihung des Schreibwettbewerbs für junge Menschen."
    },
 
    {
        date: "2026-10-31",
        time: "15:00",
        title: "BamBam-Band & Halloween Fair for Kids",
        location: "Tübingen",
        category: "familie",
        description: "Familienveranstaltung rund um Halloween."
    },
 
 
    /* ========================================================
      NOVEMBER
      ======================================================== */
 
    {
        date: "2026-11-05",
        time: "09:00",
        timeEnd: "11:00",
        title: "„Der Widerspenstigen Zähmung??“ – Workshop",
        location: "Tübingen",
        category: "workshop",
        description: "Workshop zum Thema Erziehung und Familie."
    },
 
    {
        date: "2026-11-05",
        time: "19:00",
        title: "Nach der Elternzeit – welche Betreuung passt für mein Kind und unsere Familie?",
        location: "Tübingen",
        category: "wissen",
        description: "Informationen und Orientierung rund um Betreuung nach der Elternzeit."
    },
 
    {
        date: "2026-11-09",
        time: "18:00",
        timeEnd: "20:00",
        title: "Wassergewöhnung – wie unterstütze ich mein Kind?",
        location: "Tübingen",
        category: "familie",
        description: "Informationen und praktische Hinweise zur Wassergewöhnung."
    },
 
    {
        date: "2026-11-10",
        time: "18:00",
        title: "Live-Show mit Miriam Davoudvandi: Das können wir uns nicht leisten",
        location: "Tübingen",
        category: "kultur",
        description: "Live-Show mit Miriam Davoudvandi."
    },
 
    {
        date: "2026-11-10",
        time: "17:00",
        timeEnd: "19:00",
        title: "Das Geheimnis des Bildungserfolgs – Teil 1: Entwicklungsaufgaben",
        location: "Tübingen",
        category: "wissen",
        description: "Teil 1 der Veranstaltungsreihe zum Bildungserfolg."
    },
 
    {
        date: "2026-11-11",
        time: "19:00",
        timeEnd: "21:00",
        title: "„Zu Hause in einer Erziehungsstelle“ – Podiumsdiskussion",
        location: "Tübingen",
        category: "wissen",
        description: "Podiumsdiskussion mit Beteiligten in verschiedenen Rollen."
    },
 
    {
        date: "2026-11-15",
        time: "14:00",
        timeEnd: "17:00",
        title: "Familienbasteln im Café frieDa",
        location: "Café frieDa",
        category: "familie",
        description: "Gemeinsam kreativ werden und einen schönen Familiennachmittag verbringen."
    },
 
    {
        date: "2026-11-16",
        time: "17:00",
        timeEnd: "19:00",
        title: "Das Geheimnis des Bildungserfolgs – Teil 2: Erfolg in der Schule, Bildung und Teilhabe",
        location: "Tübingen",
        category: "wissen",
        description: "Teil 2 der Veranstaltungsreihe zum Bildungserfolg."
    },
 
    {
        date: "2026-11-19",
        time: "09:30",
        timeEnd: "11:30",
        title: "Zutaten für ein starkes Familienleben",
        location: "Tübingen",
        category: "familie",
        description: "Impulse und Austausch rund um ein gutes Familienleben."
    },
 
    {
        date: "2026-11-21",
        time: "10:00",
        timeEnd: "15:00",
        title: "Bastelspaß für die ganze Familie – Thanksgiving Edition",
        location: "Tübingen",
        category: "familie",
        description: "Kreativer Familientag mit Thanksgiving-Schwerpunkt."
    },
 
    {
        date: "2026-11-24",
        time: "17:00",
        timeEnd: "19:00",
        title: "Das Geheimnis des Bildungserfolgs – Teil 3: Medienerziehung",
        location: "Tübingen",
        category: "wissen",
        description: "Teil 3 der Veranstaltungsreihe zum Bildungserfolg."
    },
 
    {
        date: "2026-11-24",
        time: "",
        title: "Gemeinsam gegen Abfall – Papierrecycling in der Stadtbücherei",
        location: "Stadtbücherei Tübingen",
        category: "familie",
        description: "Ganztägiges Angebot rund um Papierrecycling.",
        allDay: true
    },
 
    {
        date: "2026-11-25",
        time: "",
        title: "Gemeinsam gegen Abfall – Papierrecycling in der Stadtbücherei",
        location: "Stadtbücherei Tübingen",
        category: "familie",
        description: "Ganztägiges Angebot rund um Papierrecycling.",
        allDay: true
    },
 
    {
        date: "2026-11-26",
        time: "",
        title: "Gemeinsam gegen Abfall – Papierrecycling in der Stadtbücherei",
        location: "Stadtbücherei Tübingen",
        category: "familie",
        description: "Ganztägiges Angebot rund um Papierrecycling.",
        allDay: true
    }
 
];
 


/* ============================================================
const MONTHS=[
  MONATE
{year:2026,month:9,name:"Oktober 2026"},
  ============================================================ */
{year:2026,month:10,name:"November 2026"}
 
const monthNames = [
    "Januar",
    "Februar",
    "März",
    "April",
    "Mai",
    "Juni",
    "Juli",
    "August",
    "September",
    "Oktober",
    "November",
    "Dezember"
];
];
const WEEKDAYS=["Mo","Di","Mi","Do","Fr","Sa","So"];


const weekdayNames = [
function pad(n){return String(n).padStart(2,"0")}
    "So",
function isoDate(year,month,day){return `${year}-${pad(month+1)}-${pad(day)}`}
    "Mo",
function formatDate(date){
    "Di",
const d=new Date(date+"T12:00:00");
    "Mi",
return d.toLocaleDateString("de-DE",{weekday:"long",day:"2-digit",month:"long",year:"numeric"})
    "Do",
    "Fr",
    "Sa"
];
 
 
/* ============================================================
  STATUS
  ============================================================ */
 
let selectedMonth = "all";
let selectedCategory = "alle";
 
 
/* ============================================================
  HILFSFUNKTIONEN
  ============================================================ */
 
function dateObject(dateString) {
    return new Date(dateString + "T00:00:00");
}
}
 
function escapeHtml(value){
 
return String(value??"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")
function formatDate(dateString) {
 
    const date = dateObject(dateString);
 
    return {
        day: date.getDate(),
        month: monthNames[date.getMonth()],
        monthShort: monthNames[date.getMonth()].substring(0,3),
        weekday: weekdayNames[date.getDay()],
        monthNumber: date.getMonth() + 1,
        year: date.getFullYear()
    };
}
}
 
function eventTooltip(e){
 
return [`Uhrzeit: ${e.time}`,`Zielgruppe: ${e.audience}`,`Treffpunkt: ${e.place}`,`Beschreibung: ${e.description}`].join("\\n")
function dateTimeValue(event) {
 
    const time = event.time || "00:00";
 
    return new Date(
        event.date + "T" + time + ":00"
    ).getTime();
}
}


 
function renderCalendar(){
function categoryName(category) {
const root=document.getElementById("calendar");
 
MONTHS.forEach(m=>{
    const names = {
  const first=new Date(m.year,m.month,1);
        familie: "Familie",
  const days=new Date(m.year,m.month+1,0).getDate();
        wissen: "Wissen",
  const offset=(first.getDay()+6)%7;
        workshop: "Workshop",
  let out=`<section class="month"><h2>${m.name}</h2><div class="weekdays">${WEEKDAYS.map(d=>`<div>${d}</div>`).join("")}</div><div class="grid">`;
        kultur: "Kultur",
  for(let i=0;i<offset;i++)out+=`<div class="day muted"><div class="dn"></div></div>`;
        jubilaeum: "Jubiläum"
  for(let day=1;day<=days;day++){
    };
  const date=isoDate(m.year,m.month,day);
 
  const dayEvents=EVENTS.filter(e=>e.date===date);
    return names[category] || "";
  out+=`<div class="day" data-date="${date}"><div class="dn">${day}</div>${dayEvents.map(e=>`<div class="event" data-event-id="${e.id}"><div class="et">${escapeHtml(e.title)}</div></div>`).join("")}</div>`;
  }
  const trailing=(7-(offset+days)%7)%7;
  for(let i=1;i<=trailing;i++)out+=`<div class="day muted"><div class="dn">${i}</div></div>`;
  root.insertAdjacentHTML("beforeend",out+"</div></section>");
});
}
}


 
function renderOffers(){
function categoryClass(category) {
document.getElementById("offerList").innerHTML=EVENTS.map(e=>`
 
<article class="offer" id="angebot-${e.id}">
    return "category-" + category;
  <div class="offer-header">
  <div class="offer-date">${escapeHtml(formatDate(e.date))}</div>
  <div class="offer-title">${escapeHtml(e.title)}</div>
  </div>
  <div class="offer-info">
  <div class="info-row"><span class="info-label">Uhrzeit</span><div class="info-value">${escapeHtml(e.time)}</div></div>
  <div class="info-row"><span class="info-label">Zielgruppe</span><div class="info-value">${escapeHtml(e.audience)}</div></div>
  <div class="info-row"><span class="info-label">Treffpunkt</span><div class="info-value">${escapeHtml(e.place)}</div></div>
  <div class="info-row info-description"><span class="info-label">Beschreibung</span><div class="info-value">${escapeHtml(e.description)}</div></div>
  </div>
</article>`).join("");
}
}


 
function setupInteractions(){
function isPast(event) {
const tooltip=document.getElementById("tooltip");
 
document.querySelectorAll(".event").forEach(el=>{
    if (event.provisional) {
  const event=EVENTS.find(e=>String(e.id)===el.dataset.eventId);
        return false;
  if(!event)return;
    }
  el.addEventListener("mouseenter",()=>{
 
  if(matchMedia("(max-width:640px)").matches)return;
    const end = event.timeEnd || event.time || "23:59";
  tooltip.textContent=eventTooltip(event);
 
  tooltip.style.display="block";
    const eventDate = new Date(
  });
        event.date + "T" + end + ":00"
  el.addEventListener("mousemove",ev=>{
    );
  if(matchMedia("(max-width:640px)").matches)return;
 
  tooltip.style.left=(ev.clientX+14)+"px";
    return eventDate < new Date();
  tooltip.style.top=(ev.clientY+14)+"px";
  const r=tooltip.getBoundingClientRect(),pad=12;
  if(r.right>innerWidth-pad)tooltip.style.left=Math.max(pad,innerWidth-r.width-pad)+"px";
  if(r.bottom>innerHeight-pad)tooltip.style.top=Math.max(pad,innerHeight-r.height-pad)+"px";
  });
  el.addEventListener("mouseleave",()=>tooltip.style.display="none");
  el.addEventListener("click",()=>document.getElementById("angebot-"+event.id)?.scrollIntoView({behavior:"smooth",block:"start"}));
});
}
}
renderCalendar();
renderOffers();
setupInteractions();
</script>
</body>
</html>"""


 
html_out = html_out.replace("__DATA__", data_json)
/* ============================================================
out_path = Path("/mnt/data/Familienangebote_Oktober_November_2026_mobile.html")
  SORTIERUNG
out_path.write_text(html_out, encoding="utf-8")
  ============================================================ */
print(f"Fertig: {out_path}")
 
print(f"Erhaltene Termine: {len(records)}")
events.sort(function(a,b) {
 
    return dateTimeValue(a) - dateTimeValue(b);
 
});
 
 
/* ============================================================
  MONATSBUTTONS ERZEUGEN
  ============================================================ */
 
function createMonthButtons() {
 
    const container =
        document.getElementById("monthButtons");
 
    const months = [...new Set(
        events
            .filter(event => !event.provisional)
            .map(event => event.date.substring(0,7))
    )];
 
    let html = `
        <button
            class="buefa-month-button active"
            data-month="all">
            Alle Termine
        </button>
    `;
 
    months.forEach(function(month) {
 
        const monthNumber =
            parseInt(month.substring(5,7),10);
 
        html += `
            <button
                class="buefa-month-button"
                data-month="${month}">
                ${monthNames[monthNumber - 1]}
            </button>
        `;
 
    });
 
    container.innerHTML = html;
 
    container
        .querySelectorAll(".buefa-month-button")
        .forEach(function(button) {
 
            button.addEventListener("click", function() {
 
                selectedMonth =
                    this.getAttribute("data-month");
 
                container
                    .querySelectorAll(".buefa-month-button")
                    .forEach(btn =>
                        btn.classList.remove("active")
                    );
 
                this.classList.add("active");
 
                renderEvents();
 
            });
 
        });
 
}
 
 
/* ============================================================
  NÄCHSTE VERANSTALTUNG
  ============================================================ */
 
function renderNextEvent() {
 
    const container =
        document.getElementById("nextEvent");
 
    const upcoming = events
        .filter(event => !event.provisional && !isPast(event))
        .sort((a,b) =>
            dateTimeValue(a) - dateTimeValue(b)
        );
 
    if (!upcoming.length) {
 
        container.innerHTML = `
            <div class="buefa-empty">
                Derzeit sind keine kommenden Veranstaltungen
                hinterlegt.
            </div>
        `;
 
        return;
    }
 
    const event = upcoming[0];
    const date = formatDate(event.date);
 
    let time = "";
 
    if (event.allDay) {
 
        time = "Ganztägig";
 
    } else if (event.timeEnd) {
 
        time =
            event.time +
            "–" +
            event.timeEnd +
            " Uhr";
 
    } else if (event.time) {
 
        time =
            event.time +
            " Uhr";
 
    }
 
    container.innerHTML = `
 
        <div class="buefa-next-card">
 
            <div class="buefa-next-label">
                Als Nächstes
            </div>
 
            <h3>
                ${event.title}
            </h3>
 
            <div class="buefa-next-meta">
 
                <span>
                    📅 ${date.weekday}, ${date.day}. ${date.month} ${date.year}
                </span>
 
                <span>
                    🕐 ${time}
                </span>
 
                <span>
                    📍 ${event.location}
                </span>
 
            </div>
 
            <div class="buefa-next-description">
                ${event.description || ""}
            </div>
 
        </div>
    `;
 
}
 
 
/* ============================================================
  VERANSTALTUNGS-KARTE
  ============================================================ */
 
function createEventCard(event) {
 
    const date = formatDate(event.date);
 
    const past = isPast(event);
 
    let time = "";
 
    if (event.allDay) {
 
        time = "Ganztägig";
 
    } else if (event.timeEnd) {
 
        time =
            event.time +
            "–" +
            event.timeEnd +
            " Uhr";
 
    } else if (event.time) {
 
        time =
            event.time +
            " Uhr";
 
    }
 
 
    let cardClass =
        "buefa-event";
 
    if (event.provisional) {
        cardClass += " provisional";
    }
 
    if (past) {
        cardClass += " past";
    }
 
 
    let provisionalLabel = "";
 
    if (event.provisional) {
 
        provisionalLabel = `
            <div class="buefa-provisional-label">
                Noch vorläufig
            </div>
        `;
 
    }
 
 
    let pastLabel = "";
 
    if (past) {
 
        pastLabel = `
            <div class="buefa-past-label">
                Veranstaltung bereits vorbei
            </div>
        `;
 
    }
 
 
    let urlButton = "";
 
    if (event.url) {
 
        urlButton = `
            <a
                href="${event.url}"
                class="buefa-button buefa-button-primary"
                target="_blank"
                rel="noopener">
                Mehr Informationen →
            </a>
        `;
 
    }
 
 
    const detailsId =
        "details-" +
        event.date.replace(/-/g,"") +
        "-" +
        Math.random().toString(36).substring(2,7);
 
 
    return `
 
        <article
            class="${cardClass}"
            data-category="${event.category}">
 
            <div class="buefa-date">
 
                <span class="buefa-weekday">
                    ${date.weekday}
                </span>
 
                <span class="buefa-day">
                    ${date.day}
                </span>
 
                <span class="buefa-month-small">
                    ${date.month}
                </span>
 
            </div>
 
 
            <div class="buefa-event-content">
 
                ${provisionalLabel}
 
                <div class="
                    buefa-category
                    ${categoryClass(event.category)}
                ">
                    ${categoryName(event.category)}
                </div>
 
                <h3 class="buefa-event-title">
                    ${event.title}
                </h3>
 
 
                <div class="buefa-event-meta">
 
                    <span>
                        🕐 ${time}
                    </span>
 
                    <span>
                        📍 ${event.location}
                    </span>
 
                </div>
 
 
                <p class="buefa-event-description">
                    ${event.description || ""}
                </p>
 
 
                ${pastLabel}
 
 
                <div class="buefa-event-actions">
 
                    <button
                        class="buefa-button buefa-button-secondary"
                        onclick="buefaToggleDetails('${detailsId}')">
                        Details
                    </button>
 
                    ${urlButton}
 
                    <button
                        class="buefa-button buefa-button-secondary"
                        onclick='buefaAddToCalendar(${JSON.stringify(event)})'>
                        + Termin merken
                    </button>
 
                    <button
                        class="buefa-button buefa-button-secondary"
                        onclick='buefaShare(${JSON.stringify(event)})'>
                        ↗ Teilen
                    </button>
 
                </div>
 
 
                <div
                    class="buefa-details"
                    id="${detailsId}">
 
                    <div class="buefa-details-row">
 
                        <span class="buefa-details-label">
                            Datum:
                        </span>
 
                        ${date.weekday},
                        ${date.day}.
                        ${date.month}
                        ${date.year}
 
                    </div>
 
 
                    <div class="buefa-details-row">
 
                        <span class="buefa-details-label">
                            Uhrzeit:
                        </span>
 
                        ${time}
 
                    </div>
 
 
                    <div class="buefa-details-row">
 
                        <span class="buefa-details-label">
                            Ort:
                        </span>
 
                        ${event.location}
 
                    </div>
 
 
                    <div class="buefa-details-row">
 
                        ${event.description || ""}
 
                    </div>
 
                </div>
 
            </div>
 
        </article>
 
    `;
 
}
 
 
/* ============================================================
  VERANSTALTUNGEN DARSTELLEN
  ============================================================ */
 
function renderEvents() {
 
    const container =
        document.getElementById("eventList");
 
    const filtered = events.filter(function(event) {
 
        if (event.provisional) {
            return false;
        }
 
        if (
            selectedMonth !== "all" &&
            event.date.substring(0,7) !== selectedMonth
        ) {
            return false;
        }
 
        if (
            selectedCategory !== "alle" &&
            event.category !== selectedCategory
        ) {
            return false;
        }
 
        return true;
 
    });
 
 
    if (!filtered.length) {
 
        container.innerHTML = `
            <div class="buefa-empty">
                Für diese Auswahl sind derzeit keine
                Veranstaltungen eingetragen.
            </div>
        `;
 
        return;
    }
 
 
    const grouped = {};
 
    filtered.forEach(function(event) {
 
        const month =
            event.date.substring(0,7);
 
        if (!grouped[month]) {
            grouped[month] = [];
        }
 
        grouped[month].push(event);
 
    });
 
 
    let html = "";
 
 
    Object.keys(grouped)
        .sort()
        .forEach(function(monthKey) {
 
            const monthNumber =
                parseInt(monthKey.substring(5,7),10);
 
            const year =
                monthKey.substring(0,4);
 
            html += `
 
                <section
                    class="buefa-month-section">
 
                    <div class="buefa-month-heading">
 
                        <h2>
                            ${monthNames[monthNumber - 1]}
                        </h2>
 
                        <span>
                            ${year}
                        </span>
 
                    </div>
 
            `;
 
 
            grouped[monthKey]
                .forEach(function(event) {
 
                    html += createEventCard(event);
 
                });
 
 
            html += `
                </section>
            `;
 
        });
 
 
    container.innerHTML = html;
 
}
 
 
/* ============================================================
  VORLÄUFIGE TERMINE
  ============================================================ */
 
function renderProvisional() {
 
    const wrapper =
        document.getElementById("provisionalEvents");
 
    const container =
        document.getElementById("provisionalList");
 
    const provisional =
        events.filter(event => event.provisional);
 
 
    if (!provisional.length) {
 
        wrapper.style.display = "none";
 
        return;
 
    }
 
 
    wrapper.style.display = "block";
 
    let html = "";
 
 
    provisional.forEach(function(event) {
 
        const date =
            formatDate(event.date);
 
        html += `
 
            <div class="buefa-event provisional">
 
                <div class="buefa-date">
 
                    <span class="buefa-weekday">
                        ${date.weekday}
                    </span>
 
                    <span class="buefa-day">
                        ${date.day}
                    </span>
 
                    <span class="buefa-month-small">
                        ${date.month}
                    </span>
 
                </div>
 
                <div class="buefa-event-content">
 
                    <div class="buefa-provisional-label">
                        Noch in Klärung
                    </div>
 
                    <h3 class="buefa-event-title">
                        ${event.title}
                    </h3>
 
                    <div class="buefa-event-meta">
 
                        <span>
                            📍 ${event.location}
                        </span>
 
                    </div>
 
                    <p class="buefa-event-description">
                        ${event.description || ""}
                    </p>
 
                </div>
 
            </div>
 
        `;
 
    });
 
 
    container.innerHTML = html;
 
}
 
 
/* ============================================================
  FILTER
  ============================================================ */
 
document
    .querySelectorAll("#categoryFilter button")
    .forEach(function(button) {
 
        button.addEventListener("click", function() {
 
            selectedCategory =
                this.getAttribute("data-category");
 
            document
                .querySelectorAll("#categoryFilter button")
                .forEach(btn =>
                    btn.classList.remove("active")
                );
 
            this.classList.add("active");
 
            renderEvents();
 
        });
 
    });
 
 
/* ============================================================
  DETAILS AUFKLAPPEN
  ============================================================ */
 
window.buefaToggleDetails = function(id) {
 
    const element =
        document.getElementById(id);
 
    if (element) {
 
        element.classList.toggle("open");
 
    }
 
};
 
 
/* ============================================================
  KALENDERDATEI
  ============================================================ */
 
window.buefaAddToCalendar = function(event) {
 
    const start =
        event.date.replace(/-/g,"") +
        "T" +
        (event.time || "0000").replace(":","") +
        "00";
 
 
    let endTime =
        event.timeEnd ||
        event.time ||
        "23:59";
 
 
    const end =
        event.date.replace(/-/g,"") +
        "T" +
        endTime.replace(":","") +
        "00";
 
 
    const ics = [
        "BEGIN:VCALENDAR",
        "VERSION:2.0",
        "PRODID:-//Bündnis für Familie Tübingen//Jubiläum//DE",
        "BEGIN:VEVENT",
        "UID:" +
            Date.now() +
            "@buendnis-fuer-familie-tuebingen.de",
        "DTSTAMP:" +
            new Date()
                .toISOString()
                .replace(/[-:]/g,"")
                .replace(/\.\d{3}/,""),
        "DTSTART:" + start,
        "DTEND:" + end,
        "SUMMARY:" + escapeICS(event.title),
        "LOCATION:" + escapeICS(event.location),
        "DESCRIPTION:" + escapeICS(event.description || ""),
        "END:VEVENT",
        "END:VCALENDAR"
    ].join("\r\n");
 
 
    const blob =
        new Blob([ics], {
            type: "text/calendar;charset=utf-8"
        });
 
 
    const url =
        URL.createObjectURL(blob);
 
 
    const link =
        document.createElement("a");
 
    link.href = url;
 
    link.download =
        event.title
            .replace(/[^a-z0-9äöüß]+/gi,"-")
            .replace(/^-|-$/g,"")
            + ".ics";
 
 
    document.body.appendChild(link);
 
    link.click();
 
    document.body.removeChild(link);
 
    URL.revokeObjectURL(url);
 
};
 
 
function escapeICS(text) {
 
    return String(text)
        .replace(/\\/g,"\\\\")
        .replace(/\n/g,"\\n")
        .replace(/,/g,"\\,")
        .replace(/;/g,"\\;");
 
}
 
 
/* ============================================================
  TEILEN
  ============================================================ */
 
window.buefaShare = function(event) {
 
    const date =
        formatDate(event.date);
 
 
    const text =
        event.title +
        "\n" +
        date.day +
        ". " +
        date.month +
        " " +
        date.year +
        "\n" +
        (event.time ?
            event.time + " Uhr\n" :
            "") +
        event.location;
 
 
    if (
        navigator.share
    ) {
 
        navigator.share({
 
            title: event.title,
 
            text: text,
 
            url: event.url ||
                window.location.href
 
        }).catch(function() {});
 
    } else {
 
        if (
            navigator.clipboard
        ) {
 
            navigator.clipboard
                .writeText(text)
                .then(function() {
 
                    alert(
                        "Die Veranstaltungsinformationen wurden kopiert."
                    );
 
                });
 
        } else {
 
            alert(text);
 
        }
 
    }
 
};
 
 
/* ============================================================
  INITIALISIERUNG
  ============================================================ */
 
createMonthButtons();
 
renderNextEvent();
 
renderEvents();
 
renderProvisional();
 
 
})();
</script>
```

Version vom 17. August 2026, 10:33 Uhr

from pathlib import Path import re, html, json

src = Path("/mnt/data/Eingefügter Text(8).txt") text = src.read_text(encoding="utf-8")

events = re.findall(

r'

\s*
(.*?)
\s*

',

   text, flags=re.S

)

def parse_info(title):

   title = html.unescape(title)
   parts = {}
   for key in ["Uhrzeit", "Zielgruppe", "Treffpunkt", "Beschreibung"]:
       m = re.search(
           rf'{key}:\s*(.*?)(?=\n\s*(?:Uhrzeit|Zielgruppe|Treffpunkt|Beschreibung):|$)',
           title, re.S
       )
       if m:
           parts[key] = " ".join(m.group(1).split())
   return parts

date_map = [

   ("2026-10-06","Oktober 2026",6), ("2026-10-08","Oktober 2026",8),
   ("2026-10-09","Oktober 2026",9), ("2026-10-09","Oktober 2026",9),
   ("2026-10-09","Oktober 2026",9), ("2026-10-21","Oktober 2026",21),
   ("2026-10-22","Oktober 2026",22), ("2026-10-31","Oktober 2026",31),
   ("2026-11-05","November 2026",5), ("2026-11-05","November 2026",5),
   ("2026-11-09","November 2026",9), ("2026-11-10","November 2026",10),
   ("2026-11-10","November 2026",10), ("2026-11-11","November 2026",11),
   ("2026-11-15","November 2026",15), ("2026-11-17","November 2026",17),
   ("2026-11-19","November 2026",19), ("2026-11-21","November 2026",21),
   ("2026-11-24","November 2026",24), ("2026-11-24","November 2026",24),
   ("2026-11-25","November 2026",25), ("2026-11-26","November 2026",26)

] assert len(events) == len(date_map)

records=[] for i, ((date, month, day), (title, visible)) in enumerate(zip(date_map, events), 1):

   info=parse_info(title)
   records.append({
       "id":i, "date":date, "month":month, "day":day,
       "title":" ".join(html.unescape(visible).split()),
       "time":info.get("Uhrzeit",""),
       "audience":info.get("Zielgruppe",""),
       "place":info.get("Treffpunkt",""),
       "description":info.get("Beschreibung","")
   })

data_json=json.dumps(records,ensure_ascii=False,indent=2)

html_out = """<!doctype html> Familienangebote – Oktober & November 2026

Familienangebote – Oktober & November 2026

Alle im Kalender vorhandenen Termine sind enthalten. Unter dem Kalender finden Sie sämtliche Informationen zu jedem einzelnen Angebot.

Alle Angebote im Überblick

Hier finden Sie zu jedem Termin Datum, Uhrzeit, Zielgruppe, Treffpunkt und die vollständige Beschreibung.

"""

html_out = html_out.replace("__DATA__", data_json) out_path = Path("/mnt/data/Familienangebote_Oktober_November_2026_mobile.html") out_path.write_text(html_out, encoding="utf-8") print(f"Fertig: {out_path}") print(f"Erhaltene Termine: {len(records)}")