<script lang="ts">
    import { onMount, onDestroy, tick } from "svelte";
    import { getBlockLayoutClasses } from "$lib/block-layout";
    import type { CalendarBlockData, CalendarItemData } from "$lib/block-types";
    import { t as tStatic, T } from "$lib/translations";
    import type { Translations } from "$lib/translations";
    import {
        downloadICS,
        downloadICSFeed,
        googleCalUrl,
        mapsUrl,
        type CalendarItemICS,
    } from "$lib/calendar-ics";
    import { getCmsImageUrl, extractCmsImageField } from "$lib/rusty-image";
    import "$lib/iconify-offline";
    import Icon from "@iconify/svelte";
    import ImageModal from "$lib/components/ImageModal.svelte";

    type EventCardItem = CalendarItemData & {
        start: Date;
        end: Date | null;
        timeStr: string;
        isMultiDay: boolean;
        spanDayKeys: string[];
    };

    let {
        block,
        translations = {},
    }: { block: CalendarBlockData; translations?: Translations | null } =
        $props();

    function t(key: string, replacements?: Record<string, string | number>) {
        return tStatic(translations ?? null, key, replacements);
    }

    const layoutClasses = $derived(getBlockLayoutClasses(block.layout));

    /** Wall-clock ticker for live countdown — refreshed every minute on
     *  the client. SSR uses Date.now() at render time; the first client
     *  tick after hydration takes over. */
    let nowMs = $state(Date.now());
    let tickTimer: ReturnType<typeof setInterval> | null = null;

    /** UI state: filter chips (tag), past-events toggle, calendar view
     *  + selected day. */
    let showPast = $state(false);
    let activeTag = $state<string | null>(null);
    let currentMonth = $state(new Date());
    let selectedDay = $state<string | null>(null);

    function parseDate(iso: string | undefined | null): Date | null {
        if (!iso) return null;
        const d = new Date(iso);
        return isNaN(d.getTime()) ? null : d;
    }

    /** Calendar-day key in the *local* timezone — avoids ISO timezone
     *  drift when the upstream string lacks a `Z`/offset. */
    function dayKey(d: Date): string {
        return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
    }

    function spanKeys(start: Date, end: Date | null): string[] {
        if (!end || end.getTime() <= start.getTime()) return [dayKey(start)];
        const out: string[] = [];
        const cur = new Date(start);
        cur.setHours(12, 0, 0, 0);
        const last = new Date(end);
        last.setHours(12, 0, 0, 0);
        while (cur.getTime() <= last.getTime()) {
            out.push(dayKey(cur));
            cur.setDate(cur.getDate() + 1);
        }
        return out;
    }

    function formatTime(d: Date | null): string {
        if (!d) return "";
        const h = d.getHours();
        const m = d.getMinutes();
        if (h === 0 && m === 0) return "";
        return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")} Uhr`;
    }

    function getEventLinkHref(link: unknown): string {
        if (typeof link === "string" && link) return link;
        if (link && typeof link === "object") {
            const o = link as { url?: string; _slug?: string };
            if (typeof o.url === "string" && o.url) return o.url;
            if (typeof o._slug === "string" && o._slug)
                return `/post/${encodeURIComponent(o._slug)}`;
        }
        return "";
    }

    /** Location kann string ODER {text, lat, lng}-Objekt sein
     *  (RustyCMS-Resolve liefert eventLocation-Struktur). */
    function locationText(loc: unknown): string {
        if (typeof loc === "string") return loc;
        if (loc && typeof loc === "object") {
            const t = (loc as { text?: unknown }).text;
            if (typeof t === "string") return t;
        }
        return "";
    }

    /** Normalised, sorted item list. Filters out malformed entries. */
    const items = $derived.by((): EventCardItem[] => {
        const raw = block.items ?? [];
        return raw
            .filter(
                (x): x is CalendarItemData =>
                    typeof x === "object" &&
                    x !== null &&
                    "terminZeit" in x &&
                    "title" in x,
            )
            .map((x) => {
                const start = parseDate(x.terminZeit);
                const end = parseDate(x.terminEnde);
                if (!start) return null;
                const isMultiDay = !!end && dayKey(end) !== dayKey(start);
                return {
                    ...x,
                    start,
                    end: isMultiDay ? end : null,
                    timeStr: formatTime(start),
                    isMultiDay,
                    spanDayKeys: spanKeys(start, isMultiDay ? end : null),
                } satisfies EventCardItem;
            })
            .filter((x): x is EventCardItem => x !== null)
            .sort((a, b) => a.start.getTime() - b.start.getTime());
    });

    /** Distinct tag list across all items (case-insensitive de-dup,
     *  display in original casing of first occurrence). */
    const allTags = $derived.by((): string[] => {
        const seen = new Map<string, string>();
        for (const it of items) {
            for (const tag of it.tags ?? []) {
                const key = tag.trim().toLowerCase();
                if (!key) continue;
                if (!seen.has(key)) seen.set(key, tag.trim());
            }
        }
        return [...seen.values()].sort((a, b) => a.localeCompare(b, "de"));
    });

    const filteredItems = $derived.by(() => {
        if (!activeTag) return items;
        const needle = activeTag.toLowerCase();
        return items.filter((it) =>
            (it.tags ?? []).some((t) => t.toLowerCase() === needle),
        );
    });

    /** Map a day-key → events overlapping that day (multi-day events
     *  appear under each spanned day). */
    const eventsByDay = $derived.by(() => {
        const map = new Map<string, EventCardItem[]>();
        for (const ev of filteredItems) {
            for (const k of ev.spanDayKeys) {
                if (!map.has(k)) map.set(k, []);
                map.get(k)!.push(ev);
            }
        }
        return map;
    });

    const startOfTodayMs = $derived.by(() => {
        const d = new Date(nowMs);
        d.setHours(0, 0, 0, 0);
        return d.getTime();
    });

    /** Future = anything whose end (or start, if no end) is today or
     *  later. So a multi-day event running today is still "future"
     *  until its last day passes. */
    const futureEvents = $derived.by(() =>
        filteredItems.filter((it) => {
            const cutoff = (it.end ?? it.start).getTime();
            return cutoff >= startOfTodayMs;
        }),
    );

    const pastEvents = $derived.by(() =>
        filteredItems
            .filter((it) => {
                const cutoff = (it.end ?? it.start).getTime();
                return cutoff < startOfTodayMs;
            })
            .reverse(),
    );

    type EventGroup = { dayKey: string; date: Date; events: EventCardItem[] };
    const futureEventsByDay = $derived.by((): EventGroup[] => {
        const groups: EventGroup[] = [];
        for (const ev of futureEvents) {
            const key = dayKey(ev.start);
            const last = groups[groups.length - 1];
            if (last && last.dayKey === key) {
                last.events.push(ev);
            } else {
                groups.push({ dayKey: key, date: ev.start, events: [ev] });
            }
        }
        return groups;
    });

    const selectedDayEvents = $derived(
        selectedDay ? (eventsByDay.get(selectedDay) ?? []) : [],
    );

    const todayKey = $derived.by(() => dayKey(new Date(nowMs)));

    // ── Calendar-grid derived state ──────────────────────────────────
    const year = $derived(currentMonth.getFullYear());
    const month = $derived(currentMonth.getMonth());
    const monthLabel = $derived(
        currentMonth.toLocaleDateString("de-DE", {
            month: "long",
            year: "numeric",
        }),
    );
    const daysInMonth = $derived.by(() => {
        const first = new Date(year, month, 1);
        const last = new Date(year, month + 1, 0);
        const count = last.getDate();
        const startWeekday = (first.getDay() + 6) % 7;
        return { count, startWeekday, last };
    });
    const calendarDays = $derived.by(() => {
        const { count, startWeekday } = daysInMonth;
        const empty = Array(startWeekday).fill(null);
        const days = Array.from(
            { length: count },
            (_, i) => new Date(year, month, i + 1),
        );
        return [...empty, ...days];
    });

    // ── Formatters ───────────────────────────────────────────────────
    function fmtSummaryDate(d: Date): string {
        return d.toLocaleDateString("de-DE", {
            weekday: "short",
            day: "numeric",
            month: "short",
        });
    }
    function fmtGroupHeader(d: Date): string {
        return d.toLocaleDateString("de-DE", {
            weekday: "long",
            day: "numeric",
            month: "long",
            year: "numeric",
        });
    }
    function fmtRangeShort(start: Date, end: Date): string {
        const sameMonth =
            start.getMonth() === end.getMonth() &&
            start.getFullYear() === end.getFullYear();
        const fmtStart = start.toLocaleDateString("de-DE", {
            day: "numeric",
            ...(sameMonth ? {} : { month: "short" }),
        });
        const fmtEnd = end.toLocaleDateString("de-DE", {
            day: "numeric",
            month: "short",
            year:
                end.getFullYear() !== start.getFullYear()
                    ? "numeric"
                    : undefined,
        });
        return t(T.calendar_multi_day_range, { from: fmtStart, to: fmtEnd });
    }

    /** Relative day diff for a target date vs. current `nowMs`. */
    function relativeDays(d: Date): string {
        const today = new Date(nowMs);
        today.setHours(0, 0, 0, 0);
        const target = new Date(d);
        target.setHours(0, 0, 0, 0);
        const diff = Math.round(
            (target.getTime() - today.getTime()) / (24 * 60 * 60 * 1000),
        );
        if (diff === 0) return t(T.calendar_today);
        if (diff === 1) return t(T.calendar_tomorrow);
        if (diff < 0) return "";
        return t(T.calendar_in_days, { n: diff });
    }

    /** Live status badge text — "läuft gerade", "beginnt in 45 min",
     *  "beginnt in 3 h" — only meaningful for events within ~24h. */
    function liveStatus(ev: EventCardItem): string {
        const startMs = ev.start.getTime();
        const endMs = (ev.end ?? ev.start).getTime();
        const dayLengthMs = ev.isMultiDay
            ? endMs - startMs
            : 24 * 60 * 60 * 1000;
        if (
            nowMs >= startMs &&
            nowMs <= endMs + (ev.isMultiDay ? 0 : dayLengthMs)
        ) {
            // Only flag "running" if event has a meaningful duration window
            if (
                nowMs <= endMs ||
                (ev.timeStr && nowMs <= startMs + 4 * 60 * 60 * 1000)
            ) {
                return t(T.calendar_running_now);
            }
        }
        const diffMs = startMs - nowMs;
        if (diffMs <= 0 || diffMs > 24 * 60 * 60 * 1000) return "";
        const minutes = Math.round(diffMs / 60000);
        if (minutes < 60) {
            return t(T.calendar_starts_in_minutes, { n: Math.max(1, minutes) });
        }
        const hours = Math.round(minutes / 60);
        return t(T.calendar_starts_in, { n: hours });
    }

    function selectDay(key: string) {
        selectedDay = selectedDay === key ? null : key;
    }

    function prevMonth() {
        currentMonth = new Date(year, month - 1);
        selectedDay = null;
    }

    function nextMonth() {
        currentMonth = new Date(year, month + 1);
        selectedDay = null;
    }

    /** "Show me this in the calendar" — used from accordion bodies for
     *  events that may be in a different month than the current view.
     *  Scrolls back up to the widget since it now sits above the list. */
    function jumpToEventInGrid(ev: EventCardItem) {
        currentMonth = new Date(ev.start.getFullYear(), ev.start.getMonth(), 1);
        selectedDay = dayKey(ev.start);
        widgetEl?.scrollIntoView({ block: "start", behavior: "smooth" });
    }

    /** Visual urgency-bucket for the colored left-border on each event
     *  card. "today" / "tomorrow" / "this-week" pull stronger accent
     *  colors so the eye lands on imminent dates first when scrolling
     *  the list. */
    type Urgency = "now" | "today" | "tomorrow" | "week" | "later";
    function urgencyOf(ev: EventCardItem): Urgency {
        const startMs = ev.start.getTime();
        const endMs = (ev.end ?? ev.start).getTime();
        if (nowMs >= startMs && nowMs <= endMs + 4 * 60 * 60 * 1000)
            return "now";
        const today = new Date(nowMs);
        today.setHours(0, 0, 0, 0);
        const target = new Date(ev.start);
        target.setHours(0, 0, 0, 0);
        const diffDays = Math.round(
            (target.getTime() - today.getTime()) / (24 * 60 * 60 * 1000),
        );
        if (diffDays <= 0) return "today";
        if (diffDays === 1) return "tomorrow";
        if (diffDays <= 7) return "week";
        return "later";
    }

    function toICS(ev: EventCardItem): CalendarItemICS {
        return {
            title: ev.title || t(T.calendar_untitled),
            start: ev.start,
            end: ev.end ?? null,
            description: ev.description ?? undefined,
            location: locationText(ev.location) || undefined,
            url: getEventLinkHref(ev.link) || undefined,
        };
    }

    function fileSlug(s: string): string {
        return (
            s
                .toLowerCase()
                .replace(/[äöüß]/g, (c) =>
                    c === "ä"
                        ? "ae"
                        : c === "ö"
                          ? "oe"
                          : c === "ü"
                            ? "ue"
                            : "ss",
                )
                .replace(/[^a-z0-9]+/g, "-")
                .replace(/^-+|-+$/g, "")
                .slice(0, 60) || "termin"
        );
    }

    const weekdays = $derived([
        t(T.calendar_weekday_mo),
        t(T.calendar_weekday_di),
        t(T.calendar_weekday_mi),
        t(T.calendar_weekday_do),
        t(T.calendar_weekday_fr),
        t(T.calendar_weekday_sa),
        t(T.calendar_weekday_so),
    ]);

    let hasHover = $state(false);
    let tooltipDay = $state<string | null>(null);
    let tooltipPos = $state({ x: 0, y: 0 });

    function showTooltip(e: MouseEvent, key: string) {
        if (!hasHover) return;
        const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
        tooltipPos = { x: r.left + r.width / 2, y: r.top };
        tooltipDay = key;
    }
    function hideTooltip() {
        tooltipDay = null;
    }

    const tooltipEvents = $derived(
        tooltipDay ? (eventsByDay.get(tooltipDay) ?? []) : [],
    );

    let feedHost = $state("");
    const webcalUrl = $derived(
        feedHost ? `webcal://${feedHost}/api/kalender.ics` : "#",
    );
    const icsUrl = $derived(feedHost ? `/api/kalender.ics` : "#");
    const isApplePlatform = $derived(
        feedHost
            ? /iPhone|iPad|iPod|Macintosh/.test(navigator.userAgent)
            : false,
    );

    function exportAll() {
        const icsItems: CalendarItemICS[] = items.map(toICS);
        downloadICSFeed(icsItems, "windwiderstand-termine");
    }

    let shareToastKey = $state<string | null>(null);
    let copyToastKey = $state<string | null>(null);
    let modalImage = $state<{ src: string; alt: string } | null>(null);
    let linkedSlug = $state<string | null>(null);

    function eventId(item: EventCardItem): string {
        if (item._slug) return `event-${item._slug}`;
        const titlePart = (item.title || "event")
            .toLowerCase()
            .replace(/[^a-z0-9]+/g, "-")
            .replace(/^-|-$/g, "");
        return `event-${titlePart}-${dayKey(item.start)}`;
    }

    async function shareEvent(
        ev: EventCardItem,
        titleStr: string,
        locStr: string,
    ): Promise<void> {
        const dateStr = ev.start.toLocaleDateString("de-DE", {
            weekday: "long",
            day: "numeric",
            month: "long",
            year: "numeric",
        });
        const timeInfo = ev.timeStr ? ` · ${ev.timeStr}` : "";
        const parts = [`${titleStr}`, `${dateStr}${timeInfo}`];
        if (locStr) parts.push(locStr);
        const url = `${window.location.origin}${window.location.pathname}#${eventId(ev)}`;
        const text = parts.join("\n");
        const key = ev._slug ?? String(ev.start.getTime());

        const showToast = () => {
            shareToastKey = key;
            setTimeout(() => {
                if (shareToastKey === key) shareToastKey = null;
            }, 2000);
        };

        if (typeof navigator.share === "function") {
            try {
                await navigator.share({ title: titleStr, text, url });
            } catch (e) {
                if ((e as DOMException).name !== "AbortError") {
                    await navigator.clipboard
                        .writeText(`${text}\n${url}`)
                        .catch(() => {});
                    showToast();
                }
            }
        } else {
            await navigator.clipboard
                .writeText(`${text}\n${url}`)
                .catch(() => {});
            showToast();
        }
    }

    async function copyEventLink(ev: EventCardItem): Promise<void> {
        const url = `${window.location.origin}${window.location.pathname}#${eventId(ev)}`;
        const key = ev._slug ?? String(ev.start.getTime());
        await navigator.clipboard.writeText(url).catch(() => {});
        copyToastKey = key;
        setTimeout(() => {
            if (copyToastKey === key) copyToastKey = null;
        }, 2000);
    }

    let widgetEl: HTMLElement | null = $state(null);

    onMount(async () => {
        feedHost = window.location.host;
        hasHover = window.matchMedia(
            "(hover: hover) and (pointer: fine)",
        ).matches;
        tickTimer = setInterval(() => {
            nowMs = Date.now();
        }, 60_000);

        const hash = window.location.hash.slice(1);
        if (hash.startsWith("event-")) {
            linkedSlug = hash;
            await tick();
            document
                .getElementById(hash)
                ?.scrollIntoView({ behavior: "smooth", block: "start" });
        }
    });
    onDestroy(() => {
        if (tickTimer) clearInterval(tickTimer);
    });
</script>

<div
    class="{layoutClasses} w-full min-w-0 calendar-block border border-stein-200 overflow-hidden"
    data-block="Calendar"
    data-block-type="calendar"
    data-block-slug={block._slug}
>
    {#snippet eventAccordion(
        item: EventCardItem,
        openByDefault: boolean,
        isNext: boolean,
    )}
        {@const eventHref = getEventLinkHref(item.link)}
        {@const live = liveStatus(item)}
        {@const title = item.title || t(T.calendar_untitled)}
        {@const urgency = urgencyOf(item)}
        {@const locText = locationText(item.location)}
        {@const hasBody = !!(
            item.description ||
            eventHref ||
            locText ||
            item.isMultiDay
        )}
        <li
            id={eventId(item)}
            class="event-item {isNext ? 'event-item-next' : ''}"
            data-urgency={urgency}
        >
            <details
                class="group/event"
                open={openByDefault || linkedSlug === eventId(item)}
            >
                <summary
                    class="cursor-pointer list-none p-3 flex items-start gap-3 hover:bg-himmel-50/60 focus-visible:outline focus-visible:outline-2 focus-visible:outline-himmel-500"
                >
                    <!-- Datum-Block: Wochentag/Tag/Monat in vertikalem Stack —
               als Block scannbar, große Tag-Zahl als Anker. Bei Multi-
               Day fällt das Block-Layout auf Range zurück. -->
                    {#if item.isMultiDay && item.end}
                        <div
                            class="event-date-range shrink-0 w-20 text-center leading-tight"
                        >
                            <div
                                class="text-[10px] uppercase tracking-wide text-stein-500"
                            >
                                {item.start.toLocaleDateString("de-DE", {
                                    month: "short",
                                })}
                            </div>
                            <div
                                class="text-base font-bold text-stein-900 tabular-nums"
                            >
                                {item.start.getDate()}–{item.end.getDate()}
                            </div>
                            <div class="text-[10px] text-stein-500">
                                {item.start.getFullYear() ===
                                item.end.getFullYear()
                                    ? item.end.toLocaleDateString("de-DE", {
                                          month: "short",
                                      })
                                    : item.end.getFullYear()}
                            </div>
                        </div>
                    {:else}
                        <div
                            class="event-date-block shrink-0 w-14 aspect-square flex flex-col items-center justify-between leading-tight bg-himmel-800 rounded-xs py-2"
                        >
                            <div
                                class="text-[10px] uppercase tracking-wide text-himmel-300"
                            >
                                {item.start.toLocaleDateString("de-DE", {
                                    weekday: "short",
                                })}
                            </div>
                            <div
                                class="text-2xl font-bold text-himmel-50 tabular-nums"
                            >
                                {item.start.getDate()}
                            </div>
                            <div
                                class="text-[10px] uppercase tracking-wide text-himmel-300"
                            >
                                {item.start.toLocaleDateString("de-DE", {
                                    month: "short",
                                })}
                            </div>
                        </div>
                    {/if}
                    <div class="flex-1 min-w-0">
                        <div class="flex items-baseline gap-2 flex-wrap">
                            <span
                                class="text-sm font-semibold text-stein-900 leading-snug"
                            >
                                {title}
                            </span>
                            {#if live}
                                <span
                                    class="text-[10px] uppercase tracking-wide font-semibold text-fire-50 bg-fire-700 px-1.5 py-0.5 rounded-xs shrink-0"
                                >
                                    {live}
                                </span>
                            {/if}
                        </div>
                        <div
                            class="mt-0.5 flex items-center gap-2 flex-wrap text-[11px] text-stein-600"
                        >
                            {#if item.timeStr && !item.isMultiDay}
                                <span
                                    class="inline-flex items-center gap-1 tabular-nums"
                                >
                                    <Icon
                                        icon="mdi:clock-outline"
                                        class="size-3"
                                        aria-hidden="true"
                                    />
                                    {item.timeStr}
                                </span>
                            {/if}
                            {#if locText}
                                <span
                                    class="inline-flex items-center gap-1 min-w-0"
                                >
                                    <Icon
                                        icon="mdi:map-marker-outline"
                                        class="size-3 shrink-0"
                                        aria-hidden="true"
                                    />
                                    <span class="truncate">{locText}</span>
                                </span>
                            {/if}
                            {#if item.tags && item.tags.length > 0}
                                {#each item.tags.slice(0, 2) as tg}
                                    <span
                                        class="bg-himmel-100 text-himmel-800 text-[10px] px-1.5 py-0.5 rounded-xs"
                                    >
                                        {tg}
                                    </span>
                                {/each}
                                {#if item.tags.length > 2}
                                    <span class="text-[10px] text-stein-500"
                                        >+{item.tags.length - 2}</span
                                    >
                                {/if}
                            {/if}
                        </div>
                    </div>
                    <Icon
                        icon="mdi:chevron-down"
                        class="size-4 text-stein-400 shrink-0 mt-1 transition-transform group-open/event:rotate-180"
                        aria-hidden="true"
                    />
                </summary>
                <div
                    class="px-3 pb-3 pt-0 pl-[4.75rem] text-sm text-stein-700 space-y-2"
                >
                    <div
                        class="text-[11px] uppercase tracking-wide text-stein-500 -mt-1"
                    >
                        {relativeDays(item.start)}
                    </div>
                    {#if item.description}
                        <p
                            class="font-light text-stein-700 m-0! leading-relaxed"
                        >
                            {item.description}
                        </p>
                    {/if}
                    {#if item.tags && item.tags.length > 2}
                        <div class="flex flex-wrap gap-1">
                            {#each item.tags as tg}
                                <span
                                    class="bg-himmel-100 text-himmel-800 text-[10px] px-1.5 py-0.5 rounded-xs"
                                >
                                    {tg}
                                </span>
                            {/each}
                        </div>
                    {/if}
                    {#if item.image}
                        {@const imageField = extractCmsImageField(item.image)}
                        {#if imageField}
                            {@const thumbSrc = getCmsImageUrl(imageField.url, {
                                width: 160,
                                height: 160,
                                fit: "cover",
                            })}
                            {@const fullSrc = getCmsImageUrl(imageField.url, {
                                width: 1400,
                                fit: "contain",
                            })}
                            {@const imageAlt =
                                imageField.alt ??
                                (typeof item.image === "object" &&
                                "description" in item.image
                                    ? (item.image.description ?? "")
                                    : "")}
                            <button
                                type="button"
                                onclick={() =>
                                    (modalImage = {
                                        src: fullSrc,
                                        alt: imageAlt,
                                    })}
                                class="group relative w-24 h-24 rounded overflow-hidden border border-stein-200 shrink-0 hover:opacity-90 transition-opacity"
                                aria-label="Bild vergrößern"
                            >
                                <img
                                    src={thumbSrc}
                                    alt={imageAlt}
                                    class="w-full h-full object-cover"
                                    loading="lazy"
                                />
                                <span
                                    class="absolute inset-0 flex items-center justify-center bg-black/0 group-hover:bg-black/20 transition-colors"
                                >
                                    <Icon
                                        icon="mdi:magnify-plus"
                                        class="size-5 text-white opacity-0 group-hover:opacity-100 transition-opacity drop-shadow"
                                        aria-hidden="true"
                                    />
                                </span>
                            </button>
                        {/if}
                    {/if}

                    <div class="flex flex-wrap gap-2 pt-1 text-xs">
                        {#if eventHref}
                            <a
                                href={eventHref}
                                target="_blank"
                                rel="noopener noreferrer"
                                class="btn-tertiary btn-sm"
                            >
                                {t(T.calendar_more_info)}
                                <Icon
                                    icon="mdi:open-in-new"
                                    class="size-3.5"
                                    aria-hidden="true"
                                />
                            </a>
                        {/if}
                        {#if locText}
                            <a
                                href={mapsUrl(locText)}
                                target="_blank"
                                rel="noopener noreferrer"
                                class="btn-outline btn-sm group"
                                aria-label={t(T.calendar_open_maps)}
                            >
                                <Icon
                                    icon="mdi:map-marker"
                                    class="size-3.5 shrink-0"
                                    aria-hidden="true"
                                />
                                <span class="whitespace-nowrap"
                                    >{t(T.calendar_open_maps)}</span
                                >
                            </a>
                        {/if}
                        {#if item.attachment?.src}
                            {@const dlSrc = `/cms-files?src=${encodeURIComponent(item.attachment.src)}&dl=1`}
                            <a
                                href={dlSrc}
                                class="btn-outline btn-sm group"
                                aria-label={item.attachment.label ||
                                    "Anhang herunterladen"}
                            >
                                <Icon
                                    icon="mdi:file-download"
                                    class="size-3.5 shrink-0"
                                    aria-hidden="true"
                                />
                                <span class="whitespace-nowrap"
                                    >{item.attachment.label ||
                                        "Anhang herunterladen"}</span
                                >
                            </a>
                        {/if}
                        <button
                            type="button"
                            onclick={() =>
                                downloadICS(toICS(item), fileSlug(title))}
                            class="btn-outline btn-sm group"
                            aria-label={t(T.calendar_download_ics)}
                        >
                            <Icon
                                icon="mdi:download"
                                class="size-3.5 shrink-0"
                                aria-hidden="true"
                            />
                            <span class="whitespace-nowrap"
                                >{t(T.calendar_download_ics)}</span
                            >
                        </button>
                        <a
                            href={googleCalUrl(toICS(item))}
                            target="_blank"
                            rel="noopener noreferrer"
                            class="btn-outline btn-sm group"
                            aria-label={t(T.calendar_add_to_google)}
                        >
                            <Icon
                                icon="mdi:google"
                                class="size-3.5 shrink-0"
                                aria-hidden="true"
                            />
                            <span class="whitespace-nowrap"
                                >{t(T.calendar_add_to_google)}</span
                            >
                        </a>
                        {#if feedHost}
                            {@const shareKey =
                                item._slug ?? String(item.start.getTime())}
                            {@const copied = copyToastKey === shareKey}
                            <button
                                type="button"
                                onclick={() => copyEventLink(item)}
                                class="btn-outline btn-sm group"
                                aria-label={t(T.post_action_copy)}
                            >
                                <Icon
                                    icon={copied
                                        ? "mdi:check"
                                        : "mdi:link-variant"}
                                    class="size-3.5 shrink-0"
                                    aria-hidden="true"
                                />
                                <span class="whitespace-nowrap"
                                    >{copied
                                        ? t(T.post_action_copied)
                                        : t(T.post_action_copy)}</span
                                >
                            </button>
                            {@const shared = shareToastKey === shareKey}
                            <button
                                type="button"
                                onclick={() => shareEvent(item, title, locText)}
                                class="btn-outline btn-sm group"
                                aria-label={t(T.calendar_share_aria)}
                            >
                                <Icon
                                    icon={shared
                                        ? "mdi:check"
                                        : "mdi:share-variant"}
                                    class="size-3.5 shrink-0"
                                    aria-hidden="true"
                                />
                                <span class="whitespace-nowrap"
                                    >{shared
                                        ? t(T.calendar_share_copied)
                                        : t(T.calendar_share)}</span
                                >
                            </button>
                        {/if}
                        <button
                            type="button"
                            onclick={() => jumpToEventInGrid(item)}
                            class="btn-outline btn-sm group"
                            aria-label={t(T.calendar_jump_to_month)}
                        >
                            <Icon
                                icon="mdi:calendar-search"
                                class="size-3.5 shrink-0"
                                aria-hidden="true"
                            />
                            <span class="whitespace-nowrap"
                                >{t(T.calendar_jump_to_month)}</span
                            >
                        </button>
                    </div>
                </div>
            </details>
        </li>
    {/snippet}

    {#if block.title}
        <h3 class="text-lg font-semibold text-stein-900 px-4 pt-4 pb-2">
            {block.title}
        </h3>
    {/if}

    {#if allTags.length > 0}
        <!-- Tag-Filter chips. "Alle" = activeTag null. Sichtbar nur, wenn
         mindestens ein Termin Tags hat — sonst Lärm. -->
        <div
            class="flex flex-wrap gap-1.5 items-center px-4 py-2 border-b border-stein-200 bg-stein-50"
        >
            <span class="text-[11px] uppercase tracking-wide text-stein-500">
                {t(T.calendar_filter_by_tag)}
            </span>
            <button
                type="button"
                onclick={() => (activeTag = null)}
                class="text-[11px] px-2 py-0.5 rounded-xs border {!activeTag
                    ? 'bg-himmel-700 text-himmel-50 border-himmel-700'
                    : 'border-stein-300 text-stein-700 hover:bg-stein-100'}"
                aria-pressed={!activeTag}
            >
                {t(T.calendar_all_tags)}
            </button>
            {#each allTags as tag}
                <button
                    type="button"
                    onclick={() => (activeTag = activeTag === tag ? null : tag)}
                    class="text-[11px] px-2 py-0.5 rounded-xs border {activeTag ===
                    tag
                        ? 'bg-himmel-700 text-himmel-50 border-himmel-700'
                        : 'border-stein-300 text-stein-700 hover:bg-stein-100'}"
                    aria-pressed={activeTag === tag}
                >
                    {tag}
                </button>
            {/each}
        </div>
    {/if}

    <!-- Subscribe + Export Actions. Nur nach mount sichtbar (feedHost gesetzt).
         Apple: webcal:// Link → öffnet nativ Kalender.app / iOS Kalender.
         Alle anderen: direkter ICS-Download als Fallback. -->
    {#if feedHost}
        <div
            class="flex flex-wrap items-center gap-2 px-4 py-2.5 border-b border-stein-200 bg-himmel-100"
        >
            {#if isApplePlatform}
                <a
                    href={webcalUrl}
                    aria-label={t(T.calendar_subscribe_aria)}
                    class="btn-primary btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
                >
                    <Icon
                        icon="mdi:calendar-plus"
                        class="size-3.5 shrink-0"
                        aria-hidden="true"
                    />
                    {t(T.calendar_subscribe)}
                </a>
            {:else}
                <a
                    href={icsUrl}
                    download="windwiderstand-termine.ics"
                    aria-label={t(T.calendar_subscribe_aria)}
                    class="btn-primary btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
                >
                    <Icon
                        icon="mdi:calendar-plus"
                        class="size-3.5 shrink-0"
                        aria-hidden="true"
                    />
                    {t(T.calendar_subscribe)}
                </a>
            {/if}
            {#if items.length > 0}
                <button
                    type="button"
                    onclick={exportAll}
                    class="btn-outline btn-sm inline-flex items-center gap-1.5 min-h-[36px]"
                >
                    <Icon
                        icon="mdi:download"
                        class="size-3.5 shrink-0"
                        aria-hidden="true"
                    />
                    {t(T.calendar_export_all)}
                </button>
            {/if}
        </div>
    {/if}

    <!-- Kalender-Widget: oben, kompakt auf Mobile-Breite gecappt (sonst
       werden aspect-square cells auf Desktop gigantisch). Monat-Nav +
       Tag-Grid. Klick auf Tag filtert Liste unten. -->
    <div
        bind:this={widgetEl}
        class="calendar-widget bg-himmel-100 text-himmel-900 p-4"
    >
        <div class="w-full">
            <div class="flex items-center justify-between gap-2 mb-2">
                <span
                    class="text-xl font-semibold text-himmel-900 capitalize whitespace-nowrap"
                    >{monthLabel}</span
                >
                <div class="flex items-center gap-0.5">
                    <button
                        type="button"
                        class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
                        aria-label={t(T.calendar_prev_month)}
                        onclick={prevMonth}
                    >
                        <Icon
                            icon="mdi:chevron-left"
                            class="size-5"
                            aria-hidden="true"
                        />
                    </button>
                    <button
                        type="button"
                        class="p-2 rounded-xs text-himmel-800 hover:bg-himmel-200 hover:text-himmel-900 transition-colors"
                        aria-label={t(T.calendar_next_month)}
                        onclick={nextMonth}
                    >
                        <Icon
                            icon="mdi:chevron-right"
                            class="size-5"
                            aria-hidden="true"
                        />
                    </button>
                </div>
            </div>

            <div class="grid grid-cols-7 gap-1.5 text-center text-sm">
                {#each weekdays as w, i}
                    <div
                        class="py-1.5 text-xs font-bold rounded-xs {i >= 5
                            ? 'bg-stein-100/80 text-stein-600'
                            : 'bg-himmel-200/50 text-himmel-800'}"
                    >
                        {w}
                    </div>
                {/each}
                {#each calendarDays as d}
                    {#if d === null}
                        <div class="h-10" aria-hidden="true"></div>
                    {:else}
                        {@const key = dayKey(d)}
                        {@const isCurrentMonth = d.getMonth() === month}
                        {@const dayEvents = eventsByDay.get(key) ?? []}
                        {@const hasEvents = dayEvents.length > 0}
                        {@const isSelected = selectedDay === key}
                        {@const isToday = key === todayKey}
                        {@const isFuture = key >= todayKey}
                        {@const isWeekend =
                            d.getDay() === 0 || d.getDay() === 6}
                        {#if hasEvents}
                            <button
                                type="button"
                                class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 transition-colors cursor-pointer font-semibold
                                    {isCurrentMonth
                                    ? 'text-himmel-900'
                                    : 'text-himmel-700 opacity-60 hover:opacity-80'}
                                    {isWeekend
                                    ? 'bg-erde-100 hover:bg-erde-200'
                                    : 'bg-himmel-200/80 hover:bg-himmel-300'}
                                    {isSelected
                                    ? 'ring-2 ring-himmel-700 brightness-95'
                                    : ''}
                                    {isToday ? 'today-cell' : ''}"
                                onclick={() => selectDay(key)}
                                onmouseenter={(e) => showTooltip(e, key)}
                                onmouseleave={hideTooltip}
                                aria-pressed={isSelected}
                                aria-current={isToday ? "date" : undefined}
                                aria-label="{d.toLocaleDateString('de-DE', {
                                    weekday: 'long',
                                    day: 'numeric',
                                    month: 'long',
                                    year: 'numeric',
                                })}, {dayEvents.length} {t(
                                    T.calendar_event_count,
                                    { n: dayEvents.length },
                                ).replace(/\d+\s*/, '')}"
                            >
                                <span class="text-xs">{d.getDate()}</span>
                                <span
                                    class="absolute -bottom-1 -right-1 inline-flex items-center justify-center min-w-3.5 h-3.5 sm:min-w-4.5 sm:h-4.5 px-0.5 rounded-full text-stein-0 text-[9px] sm:text-[10px] font-medium leading-none {isFuture
                                        ? 'bg-wald-400'
                                        : 'bg-error'}"
                                    aria-hidden="true"
                                >
                                    {dayEvents.length}
                                </span>
                            </button>
                        {:else}
                            <div
                                class="relative h-10 rounded-xs flex flex-col items-center justify-center min-w-0 cursor-default
                                    {isWeekend
                                    ? 'bg-stein-100/60'
                                    : 'bg-himmel-50/50'}
                                    {isCurrentMonth
                                    ? 'text-himmel-800'
                                    : 'text-himmel-600 opacity-50'}
                                    {isToday ? 'today-cell' : ''}"
                                aria-current={isToday ? "date" : undefined}
                            >
                                <span class="text-xs">{d.getDate()}</span>
                                {#if isToday}
                                    <span class="sr-only"
                                        >{t(T.calendar_today_marker)}</span
                                    >
                                {/if}
                            </div>
                        {/if}
                    {/if}
                {/each}
            </div>
        </div>
    </div>

    <!-- Hover-Tooltip: fixed-positioned → nicht durch overflow-hidden geclipt.
       Nur auf pointer:fine Geräten (hasHover). Zeigt max 3 Events des Tages. -->
    {#if hasHover && tooltipDay && tooltipEvents.length > 0}
        <div
            class="cal-tooltip"
            style="left:{tooltipPos.x}px; top:{tooltipPos.y}px;"
            aria-hidden="true"
        >
            {#each tooltipEvents.slice(0, 3) as ev}
                <div class="cal-tooltip-row">
                    <span class="cal-tooltip-title"
                        >{ev.title || t(T.calendar_untitled)}</span
                    >
                    {#if ev.timeStr}
                        <span class="cal-tooltip-time">{ev.timeStr}</span>
                    {/if}
                </div>
            {/each}
            {#if tooltipEvents.length > 3}
                <div class="cal-tooltip-more">
                    +{tooltipEvents.length - 3} weitere
                </div>
            {/if}
        </div>
    {/if}

    <!-- Events: voll Breite, unten. Akkordeon-Liste, in natürlichem
       Page-Flow (kein internes Scrollen). Kalender-Klick filtert auf
       einen Tag; ohne Filter Liste ab heute, gruppiert nach Tag. -->
    <div class="calendar-events-panel bg-stein-0 border-t border-stein-200">
        {#if items.length === 0}
            <p class="text-sm text-stein-500 p-4">{t(T.calendar_no_events)}</p>
        {:else if selectedDay}
            <div
                class="flex items-center justify-between gap-2 px-3 py-2 border-b border-stein-200 bg-himmel-50"
            >
                <div class="text-xs font-medium text-stein-700 truncate">
                    {new Date(selectedDay + "T12:00:00").toLocaleDateString(
                        "de-DE",
                        {
                            weekday: "long",
                            day: "numeric",
                            month: "long",
                        },
                    )}
                </div>
                <button
                    type="button"
                    onclick={() => (selectedDay = null)}
                    class="inline-flex items-center gap-1 text-[11px] text-himmel-700 hover:text-himmel-900 hover:underline shrink-0"
                >
                    <Icon
                        icon="mdi:close"
                        class="size-3.5"
                        aria-hidden="true"
                    />
                    {t(T.calendar_clear_filter)}
                </button>
            </div>
            <ul class="m-0! p-0! list-none">
                {#each selectedDayEvents as item}
                    {@render eventAccordion(item, true, false)}
                {/each}
            </ul>
        {:else if futureEventsByDay.length > 0}
            <div
                class="flex items-baseline justify-between gap-2 px-3 py-2 border-b border-stein-200"
            >
                <h4 class="text-xs font-medium text-stein-500 m-0!">
                    {t(T.calendar_all_upcoming)}
                </h4>
                <span class="text-[11px] text-stein-500 tabular-nums">
                    {t(T.calendar_event_count, { n: futureEvents.length })}
                </span>
            </div>
            <ul class="m-0! p-0! list-none">
                {#each futureEventsByDay as group, gIdx}
                    <li class="day-group">
                        <div
                            class="px-3 py-1 bg-himmel-50/60 text-[11px] uppercase tracking-wide font-medium text-stein-600 sticky top-0 z-10 backdrop-blur-sm"
                        >
                            {fmtGroupHeader(group.date)}
                            <span
                                class="ml-2 text-stein-500 normal-case tracking-normal lowercase"
                                >· {relativeDays(group.date)}</span
                            >
                        </div>
                        <ul class="m-0! p-0! list-none">
                            {#each group.events as item, eIdx}
                                {@render eventAccordion(
                                    item,
                                    gIdx === 0,
                                    gIdx === 0 && eIdx === 0,
                                )}
                            {/each}
                        </ul>
                    </li>
                {/each}
            </ul>
            {#if pastEvents.length > 0}
                <details
                    class="border-t border-stein-200 bg-stein-50 group/past"
                >
                    <summary
                        class="cursor-pointer list-none px-3 py-2 text-xs text-stein-600 hover:bg-stein-100 inline-flex items-center gap-1.5"
                    >
                        <Icon
                            icon="mdi:chevron-right"
                            class="size-3.5 transition-transform group-open/past:rotate-90"
                            aria-hidden="true"
                        />
                        <span class="font-medium uppercase tracking-wide"
                            >{t(T.calendar_past_events)}</span
                        >
                        <span class="text-stein-500 tabular-nums"
                            >· {pastEvents.length}</span
                        >
                    </summary>
                    <ul class="m-0! p-0! list-none opacity-80">
                        {#each pastEvents as item}
                            {@render eventAccordion(item, false, false)}
                        {/each}
                    </ul>
                </details>
            {/if}
        {:else if pastEvents.length > 0}
            <!-- Nur Vergangene da, keine Zukunft → past-Liste expanded
           damit Panel nicht leer wirkt. -->
            <details open class="bg-stein-50 group/past">
                <summary
                    class="cursor-pointer list-none px-3 py-2 text-xs text-stein-600 hover:bg-stein-100 inline-flex items-center gap-1.5 border-b border-stein-200"
                >
                    <Icon
                        icon="mdi:chevron-right"
                        class="size-3.5 transition-transform group-open/past:rotate-90"
                        aria-hidden="true"
                    />
                    <span class="font-medium uppercase tracking-wide"
                        >{t(T.calendar_past_events)}</span
                    >
                    <span class="text-stein-500 tabular-nums"
                        >· {pastEvents.length}</span
                    >
                </summary>
                <ul class="m-0! p-0! list-none opacity-80">
                    {#each pastEvents as item}
                        {@render eventAccordion(item, false, false)}
                    {/each}
                </ul>
            </details>
        {:else}
            <p class="text-sm text-stein-500 p-4">
                {t(T.calendar_no_future_events)}
            </p>
        {/if}
    </div>
</div>

{#if modalImage}
    <ImageModal
        src={modalImage.src}
        alt={modalImage.alt}
        onclose={() => (modalImage = null)}
    />
{/if}

<style>
    /* Heute-Markierung im Kalendergrid: dezenter Rand + leichtes Glow.
     Ergänzt das Event-Badge, damit auch leere Tage als "Heute"
     erkennbar sind. */
    .calendar-block .today-cell {
        outline: 2px solid var(--color-wald-400, #84e0a4);
        outline-offset: -2px;
        background-color: color-mix(
            in srgb,
            var(--color-wald-200, #bbf0cf) 40%,
            transparent
        );
    }
    /* Event-Rows visuell trennen: Card-Optik mit Urgency-Akzent links,
     Zebra-Stripe ab dem zweiten Eintrag, deutlicher Separator
     zwischen Tagesgruppen. So lassen sich Termine optisch klar
     auseinanderhalten ohne komplette Kacheloptik. */
    .calendar-block .event-item {
        position: relative;
        border-left: 4px solid transparent;
        background: var(--color-stein-0, #fff);
        transition: background-color 150ms ease;
    }
    .calendar-block .event-item + .event-item {
        border-top: 1px solid var(--color-stein-200, #e0dcd5);
    }
    /* Zebra: jeder zweite Termin in einer Tagesgruppe leicht abgedunkelt
     — sauber per :nth-of-type, ohne dass Day-Group-Header einen Slot
     verbraucht (event-item ist eigene <li>-Sequenz). */
    .calendar-block .event-item:nth-of-type(even) {
        background: var(--color-stein-50, #f7f5f1);
    }
    .calendar-block .event-item:hover {
        background: var(--color-himmel-50, #eef4f8);
    }
    /* Urgency-Akzent: 4px farbiger Strich links. Alphabetische
     Wichtigkeit: now (rot stark) → today (rot) → tomorrow (warm) →
     week (kühl) → later (neutral). */
    .calendar-block .event-item[data-urgency="now"] {
        border-left-color: var(--color-fire-700, #b91c1c);
        background: var(--color-fire-50, #fef2f2);
    }
    .calendar-block .event-item[data-urgency="today"] {
        border-left-color: var(--color-fire-500, #ef4444);
    }
    .calendar-block .event-item[data-urgency="tomorrow"] {
        border-left-color: var(--color-erde-500, #c97a3b);
    }
    .calendar-block .event-item[data-urgency="week"] {
        border-left-color: var(--color-himmel-500, #4798c4);
    }
    .calendar-block .event-item[data-urgency="later"] {
        border-left-color: var(--color-stein-300, #c4bfb4);
    }
    /* Next-up: kräftigere linke Kante + leichter himmel-Tint, plus
     ein subtiler ring damit der Eintrag beim Scroll-into-view
     visuell anspringt. */
    .calendar-block .event-item.event-item-next {
        border-left-width: 6px;
        box-shadow: inset 4px 0 0 var(--color-himmel-100, #d8e6ef);
    }
    /* Day-Group: dickerer Separator + etwas Luft zwischen Gruppen. */
    .calendar-block .day-group + .day-group {
        border-top: 4px solid var(--color-stein-100, #ede8df);
    }
    /* Hover-Tooltip: fixed über dem Grid, zeigt Event-Vorschau auf hover.
       pointer-events:none damit kein mouseenter/leave Loop entsteht. */
    :global(.cal-tooltip) {
        position: fixed;
        z-index: 9999;
        transform: translateX(-50%) translateY(calc(-100% - 8px));
        background: #fff;
        color: var(--color-stein-900, #1c1a17);
        border: 1px solid var(--color-stein-200, #e0dcd5);
        border-radius: 4px;
        padding: 6px 10px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
        font-size: 11px;
        line-height: 1.4;
        pointer-events: none;
        min-width: 130px;
        max-width: 220px;
    }
    :global(.cal-tooltip::after) {
        content: "";
        position: absolute;
        top: 100%;
        left: 50%;
        transform: translateX(-50%);
        border: 5px solid transparent;
        border-top-color: var(--color-stein-200, #e0dcd5);
    }
    :global(.cal-tooltip::before) {
        content: "";
        position: absolute;
        top: calc(100% - 1px);
        left: 50%;
        transform: translateX(-50%);
        border: 5px solid transparent;
        border-top-color: #fff;
        z-index: 1;
    }
    :global(.cal-tooltip-row) {
        display: flex;
        align-items: baseline;
        gap: 4px;
        padding: 1px 0;
    }
    :global(.cal-tooltip-row + .cal-tooltip-row) {
        border-top: 1px solid var(--color-stein-100, #ede8df);
        margin-top: 2px;
        padding-top: 3px;
    }
    :global(.cal-tooltip-title) {
        font-weight: 500;
        color: var(--color-stein-900, #1c1a17);
        overflow: hidden;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
    }
    :global(.cal-tooltip-time) {
        font-size: 10px;
        color: var(--color-stein-500, #7a7469);
        white-space: nowrap;
        flex-shrink: 0;
        margin-left: auto;
    }
    :global(.cal-tooltip-more) {
        font-size: 10px;
        color: var(--color-stein-500, #7a7469);
        margin-top: 3px;
        border-top: 1px solid var(--color-stein-100, #ede8df);
        padding-top: 3px;
    }

    /* Print: alle <details> geöffnet, Chevrons aus, Day-Groups nicht
     splitten. */
    @media print {
        .calendar-block details > *:not(summary) {
            display: block !important;
        }
        .calendar-block details > summary :global(svg) {
            display: none;
        }
        .calendar-block .day-group {
            break-inside: avoid;
        }
    }
</style>
