<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { get, type Readable, type Unsubscriber } from 'svelte/store';
  import { downloadFile } from '../utils/download';
  import { pickFiles } from '../utils/file-picker';
  import { ScheduleXCalendar } from '@schedule-x/svelte';
  import { createCalendar, viewDay, viewWeek, viewMonthGrid } from '@schedule-x/calendar';
  import '@schedule-x/theme-default/dist/index.css';
  import DOMPurify from 'dompurify';
  import { i18n } from '../utils/i18n';
  import { Local } from '../utils/storage';
  import { Remote } from '../utils/remote';
  import { isDemoBlockedError } from '../utils/demo-mode';
  import { db } from '../utils/db';
  import { normalizeEmail } from '../utils/address';
  import { queueEmail } from '../utils/outbox-service';
  import { refreshTaskReminders } from '../utils/task-reminders';
  import {
    buildAllDayRange,
    buildLocalDateTime,
    zonedWallClockToUTC,
    parseAllDayFromIcal,
    localDateOf,
    buildVTimezone,
  } from '../utils/ical-datetime';
  import { parseNaturalLanguage } from '../utils/calendar-nlp';
  import {
    expandRecurringEvents,
    getRecurrenceText,
    buildRrule,
    parseRrule,
    describeRecurrenceSpec,
    addExdateToMaster,
    addOverrideToMaster,
    DEFAULT_RECURRENCE,
    type RecurrenceSpec,
  } from '../utils/recurrence';
  import {
    effectiveTheme,
    hideCompletedTodos,
    tasksSort,
    startWeekOnSunday,
    setSettingValue,
    type TasksSortKey,
  } from '../stores/settingsStore';
  import { currentAccount } from '../stores/mailboxActions';
  import { Button } from '$lib/components/ui/button';
  import { Input } from '$lib/components/ui/input';
  import { Textarea } from '$lib/components/ui/textarea';
  import { Label } from '$lib/components/ui/label';
  import { Checkbox } from '$lib/components/ui/checkbox';
  import * as Dialog from '$lib/components/ui/dialog';
  import * as Alert from '$lib/components/ui/alert';
  import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
  import * as Select from '$lib/components/ui/select';
  import * as Tooltip from '$lib/components/ui/tooltip';
  import { Separator } from '$lib/components/ui/separator';
  import TasksList from './components/TasksList.svelte';
  import TimezoneCombobox from './components/TimezoneCombobox.svelte';
  import ChevronLeft from '@lucide/svelte/icons/chevron-left';
  import ChevronDown from '@lucide/svelte/icons/chevron-down';
  import ChevronRight from '@lucide/svelte/icons/chevron-right';
  import Import from '@lucide/svelte/icons/import';
  import Copy from '@lucide/svelte/icons/copy';
  import Download from '@lucide/svelte/icons/download';
  import Trash2 from '@lucide/svelte/icons/trash-2';
  import Info from '@lucide/svelte/icons/info';
  import AlertTriangle from '@lucide/svelte/icons/alert-triangle';
  import CalendarIcon from '@lucide/svelte/icons/calendar';
  import Plus from '@lucide/svelte/icons/plus';
  import MoreVertical from '@lucide/svelte/icons/more-vertical';
  import Check from '@lucide/svelte/icons/check';
  import X from '@lucide/svelte/icons/x';
  import User from '@lucide/svelte/icons/user';

  interface ToastApi {
    show?: (message: string, type?: string) => void;
  }

  interface CalendarApi {
    open?: () => void;
    refresh?: () => void;
    reload?: () => void;
    prefillQuickEvent?: (email?: string) => void;
    applyRemoteChange?: (detail: { type?: string; payload?: unknown }) => void;
  }

  interface Props {
    navigate?: (path: string) => void;
    toasts?: ToastApi | null;
    registerApi?: (api: CalendarApi) => void;
    active?: boolean | Readable<boolean>;
  }

  let {
    navigate = () => {},
    toasts = null,
    registerApi = () => {},
    active = false,
  }: Props = $props();

  // Handle active as either a boolean or a store
  let isActive = $state(typeof active === 'boolean' ? active : false);
  let activeUnsub: Unsubscriber | null = null;
  let accountUnsub: Unsubscriber | null = null;

  onMount(() => {
    if (active && typeof active === 'object' && 'subscribe' in active) {
      activeUnsub = active.subscribe((val: boolean) => {
        isActive = val;
      });
    }

    const handleHashChange = () => {
      applyCalendarHashSelection();
    };
    window.addEventListener('hashchange', handleHashChange);

    // Account change subscription (replaced $effect to avoid loops)
    accountUnsub = currentAccount.subscribe((acct) => {
      if (acct !== lastAccount) {
        lastAccount = acct || '';
        resetCalendarState();
        if (hasMounted && isActive) {
          load(true);
        }
      }
    });

    return () => {
      window.removeEventListener('hashchange', handleHashChange);
    };
  });

  onDestroy(() => {
    activeUnsub?.();
    accountUnsub?.();
  });

  let loading = $state(false);
  let error = $state('');
  let calendars = $state<unknown[]>([]);
  let selectedCalendarIds = $state<string[]>([]);
  let activeCalendarId = $state('');
  // Calendar ids we've seen on the server before (persisted alongside the
  // selection). Lets reconcileCalendarSelection tell a genuinely new calendar
  // (default it to visible) apart from one the user deliberately hid (keep it
  // hidden). Without this, calendars created outside the app were never added
  // to the selected set, so their events fired notifications but were filtered
  // out of the calendar view.
  let knownCalendarIds: string[] = [];
  let events = $state<unknown[]>([]);
  let allEvents = $state<unknown[]>([]);
  let eventsScope = $state('none');
  let eventsScopeCalendarId = $state('');
  let calendarInstance = $state.raw<ReturnType<typeof createCalendar> | null>(null);
  let hasMounted = $state(false);
  let calendarsLoaded = $state(false);
  let lastAccount = Local.get('email') || '';
  let loadRequestId = 0;
  let prefersDark = $state(false);
  let viewportWidth = $state(typeof window !== 'undefined' ? window.innerWidth : 1024);

  const MOBILE_BREAKPOINT = 768;
  const MAX_PAGINATION_PAGES = 10_000;
  const CALENDARS_PAGE_SIZE = 50;
  const CALENDAR_EVENTS_PAGE_SIZE = 500;
  const getCalendarId = (cal: unknown) =>
    (cal as Record<string, unknown>)?.id ||
    (cal as Record<string, unknown>)?.calendar_id ||
    (cal as Record<string, unknown>)?.uid ||
    '';
  const isMobile = $derived(viewportWidth <= MOBILE_BREAKPOINT);
  // On mobile, show all calendars; on desktop, use the user's selection
  const effectiveSelectedCalendarIds = $derived(
    isMobile
      ? (calendars.map((c) => getCalendarId(c)).filter(Boolean) as string[])
      : selectedCalendarIds,
  );

  const theme = $derived($effectiveTheme || 'system');
  const calendarIsDark = $derived(theme === 'dark' || (theme === 'system' && prefersDark));
  const activeEmail = $derived($currentAccount || Local.get('email') || '');
  const calendarFilterLabel = $derived(
    calendars.length > 1
      ? `Calendars (${selectedCalendarIds.length}/${calendars.length})`
      : 'Calendar',
  );

  const getAccountKey = () => $currentAccount || Local.get('email') || 'default';
  const getActiveFromAddress = () => {
    const aliasAuth = Local.get('alias_auth') || '';
    const aliasEmail = aliasAuth.includes(':') ? aliasAuth.split(':')[0] : aliasAuth;
    return aliasEmail || activeEmail || Local.get('email') || '';
  };
  const getCalendarsCacheKey = (accountKey: string) => `calendars_${accountKey}`;
  const getEventsCacheKey = (accountKey: string, calendarId: string) =>
    `calendar_events_${accountKey}_${calendarId}`;
  const getAllEventsCacheKey = (accountKey: string) => `calendar_events_${accountKey}_all`;
  const getCalendarPrefsKey = (accountKey: string) => `calendar_prefs_${accountKey}`;

  const getCalendarsList = (response: unknown): unknown[] => {
    if (Array.isArray(response)) return response;
    return (
      (response as { Result?: unknown[]; calendars?: unknown[] })?.Result ||
      (response as { calendars?: unknown[] })?.calendars ||
      []
    );
  };

  const getCalendarEventsList = (response: unknown): unknown[] => {
    if (Array.isArray(response)) return response;
    return (
      (response as { Result?: unknown[]; events?: unknown[] })?.Result ||
      (response as { events?: unknown[] })?.events ||
      []
    );
  };

  const loadPaginatedCalendarResource = async (
    requestId: number,
    endpoint: 'Calendars' | 'CalendarEvents',
    params: Record<string, unknown>,
    pageSize: number,
    getList: (response: unknown) => unknown[],
  ): Promise<unknown[] | null> => {
    const items: unknown[] = [];

    for (let page = 1; page < MAX_PAGINATION_PAGES; page += 1) {
      const response = await Remote.request(endpoint, {
        ...params,
        page,
        limit: pageSize,
      });

      if (requestId !== loadRequestId) return null;

      const batch = getList(response);
      items.push(...batch);

      if (batch.length < pageSize) {
        break;
      }
    }

    return items;
  };

  // Map Apple CalDAV internal constants to user-friendly display names
  // These are static variable names that Apple iOS/macOS CalDAV uses internally
  const mapCalendarDisplayName = (name: string): string => {
    if (!name) return 'Calendar';
    if (name === 'DEFAULT_TASK_CALENDAR_NAME') return 'Tasks';
    if (name === 'DEFAULT_CALENDAR_NAME') return 'Calendar';
    return name;
  };

  const getCalendarLabel = (cal: unknown) => {
    const rawName =
      (cal as Record<string, unknown>)?.name || (cal as Record<string, unknown>)?.displayName || '';
    return mapCalendarDisplayName(String(rawName)) || 'Calendar';
  };

  const normalizeHexColor = (value: unknown) => {
    if (typeof value !== 'string') return '';
    const trimmed = value.trim();
    if (!trimmed) return '';
    const lower = trimmed.toLowerCase();
    if (/^#[0-9a-f]{3}$/.test(lower)) {
      return `#${lower[1]}${lower[1]}${lower[2]}${lower[2]}${lower[3]}${lower[3]}`;
    }
    if (/^#[0-9a-f]{6}$/.test(lower)) return lower;
    if (/^[0-9a-f]{3}$/.test(lower)) {
      return `#${lower[0]}${lower[0]}${lower[1]}${lower[1]}${lower[2]}${lower[2]}`;
    }
    if (/^[0-9a-f]{6}$/.test(lower)) return `#${lower}`;
    return '';
  };

  const blendHex = (hex: string, targetHex: string, amount: number) => {
    const parse = (color: string) => ({
      r: parseInt(color.slice(1, 3), 16),
      g: parseInt(color.slice(3, 5), 16),
      b: parseInt(color.slice(5, 7), 16),
    });
    const clamp = (val: number) => Math.max(0, Math.min(255, val));
    const a = parse(hex);
    const b = parse(targetHex);
    const mix = (start: number, end: number) => Math.round(start + (end - start) * amount);
    const toHex = (val: number) => clamp(val).toString(16).padStart(2, '0');
    return `#${toHex(mix(a.r, b.r))}${toHex(mix(a.g, b.g))}${toHex(mix(a.b, b.b))}`;
  };

  const calendarColorPalette = ['#1c7ed6', '#f59f00', '#d6336c', '#2f9e44', '#5f3dc4', '#0ca678'];
  const resolveCalendarColor = (cal: unknown, index: number) =>
    normalizeHexColor((cal as Record<string, unknown>)?.color) ||
    calendarColorPalette[index % calendarColorPalette.length];

  const ensureSafeMutationObserver = () => {
    if (typeof window === 'undefined' || typeof MutationObserver === 'undefined') return;
    if ((MutationObserver.prototype as Record<string, unknown>).__sxSafeObservePatched) return;
    const originalObserve = MutationObserver.prototype.observe;
    MutationObserver.prototype.observe = function (target, options) {
      if (typeof Node === 'undefined' || !(target instanceof Node)) return;
      return originalObserve.call(this, target, options);
    };
    (MutationObserver.prototype as Record<string, unknown>).__sxSafeObservePatched = true;
  };

  let newEventModal = $state(false);
  let editEventModal = $state(false);
  let showDeleteConfirm = $state(false);
  let showNewStartPicker = $state(false);
  let showNewEndPicker = $state(false);
  let showEditStartPicker = $state(false);
  let showEditEndPicker = $state(false);
  let optionalFieldsExpanded = $state(false);
  let filterMenuOpen = $state(false);
  let calendarsModalOpen = $state(false);
  let newCalendarModal = $state(false);
  let newCalendarName = $state('');
  let newCalendarColor = $state('#1c7ed6');
  let savingCalendar = $state(false);
  let deleteCalendarModal = $state(false);
  let deleteCalendarId = $state('');
  let deleteCalendarLabel = $state('');
  let deleteCalendarConfirmName = $state('');
  let modalDirty = $state(false);
  let savingEvent = $state(false);
  let descriptionRef = $state<HTMLTextAreaElement | null>(null);
  let newEventModalRef = $state<HTMLDivElement | null>(null);
  let titleError = $state('');
  let lastDurationMinutes = 60;
  let modalAnnouncement = $state('');

  // The user's IANA timezone (e.g. "America/New_York"). Stamped onto every
  // new/edited event so downstream devices can reproduce the original wall
  // clock — see iCalendar TZID handling in generateICalEvent.
  const getDefaultTimezone = () => {
    try {
      return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
    } catch {
      return 'UTC';
    }
  };

  // Surface a hint when an event's stored TZID differs from the viewer's
  // local zone so users understand that the time fields shown are converted
  // to their local clock (standard calendar behavior).
  const viewerZone = getDefaultTimezone();
  const isForeignZone = (eventZone: string | undefined | null) =>
    !!eventZone && eventZone !== 'UTC' && eventZone !== viewerZone;

  // VTIMEZONE synthesis lives in src/utils/ical-datetime.ts (buildVTimezone) so
  // it can be unit-tested against ical.js — the prior inline version emitted
  // no-RRULE transitions that resolved the wrong DST offset (1h drift).

  // Wall-clock formatter: produces YYYYMMDDTHHMMSS rendered in the given
  // IANA tz. Used with DTSTART;TZID=<tz>:<wallClock> instead of the legacy
  // DTSTART:<utc>Z, so events round-trip across devices without DST/TZ
  // drift. en-CA gives YYYY-MM-DD parts; hour12 false yields 00–23.
  const formatICalLocal = (iso: string | undefined, tzid: string): string => {
    if (!iso) return '';
    const d = new Date(iso);
    if (!Number.isFinite(d.getTime())) return '';
    const fmt = new Intl.DateTimeFormat('en-CA', {
      timeZone: tzid,
      year: 'numeric',
      month: '2-digit',
      day: '2-digit',
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hour12: false,
    });
    const parts = fmt.formatToParts(d);
    const get = (t: string) => parts.find((p) => p.type === t)?.value || '';
    // Intl can return "24" for midnight on some browsers — clamp to "00".
    const hour = get('hour') === '24' ? '00' : get('hour');
    return `${get('year')}${get('month')}${get('day')}T${hour}${get('minute')}${get('second')}`;
  };

  let newEvent = $state({
    calendarId: '',
    title: '',
    date: '',
    // Multi-day support: endDate defaults to date for single-day events.
    // Save flow uses (newEvent.endDate || newEvent.date) so empty == single-day.
    endDate: '',
    startTime: '',
    startMeridiem: 'AM',
    endTime: '',
    endMeridiem: 'AM',
    allDay: false,
    description: '',
    location: '',
    url: '',
    timezone: getDefaultTimezone(),
    attendees: '',
    notify: 0,
    componentType: 'VEVENT' as 'VEVENT' | 'VTODO',
    hasDate: true,
    hasTime: true,
    // Recurrence spec (VEVENT only — VTODO recurrence not yet supported).
    recurrence: { ...DEFAULT_RECURRENCE } as RecurrenceSpec,
    _editingRawDescription: false,
  });

  let editEvent = $state({
    id: '',
    calendarId: '',
    title: '',
    date: '',
    endDate: '',
    startTime: '',
    startMeridiem: 'AM',
    endTime: '',
    endMeridiem: 'AM',
    allDay: false,
    description: '',
    location: '',
    url: '',
    timezone: getDefaultTimezone(),
    attendees: '',
    notify: 0,
    componentType: 'VEVENT',
    status: '',
    completedAt: '',
    percentComplete: 0,
    hasDate: true,
    hasTime: true,
    recurrence: { ...DEFAULT_RECURRENCE } as RecurrenceSpec,
    _editingRawDescription: false,
  });

  let newAttendeeDraft = $state('');
  let editAttendeeDraft = $state('');

  const parseAttendeeList = (s: string): string[] =>
    (s || '')
      .split(/[,;]/)
      .map((e) => e.trim())
      .filter(Boolean);

  const addAttendeeToNewEvent = () => {
    const trimmed = newAttendeeDraft.trim();
    if (!trimmed) return;
    const list = parseAttendeeList(newEvent.attendees);
    if (!list.includes(trimmed)) {
      list.push(trimmed);
      newEvent.attendees = list.join(', ');
      modalDirty = true;
    }
    newAttendeeDraft = '';
  };

  const removeAttendeeFromNewEvent = (email: string) => {
    const list = parseAttendeeList(newEvent.attendees).filter((e) => e !== email);
    newEvent.attendees = list.join(', ');
    modalDirty = true;
  };

  const addAttendeeToEditEvent = () => {
    const trimmed = editAttendeeDraft.trim();
    if (!trimmed) return;
    const list = parseAttendeeList(editEvent.attendees);
    if (!list.includes(trimmed)) {
      list.push(trimmed);
      editEvent.attendees = list.join(', ');
    }
    editAttendeeDraft = '';
  };

  const removeAttendeeFromEditEvent = (email: string) => {
    editEvent.attendees = parseAttendeeList(editEvent.attendees)
      .filter((e) => e !== email)
      .join(', ');
  };

  const timeOptions = Array.from({ length: 12 * 4 }, (_, idx) => {
    const hour12 = Math.floor(idx / 4) + 1;
    const minute = (idx % 4) * 15;
    const m = String(minute).padStart(2, '0');
    const h = String(hour12).padStart(2, '0');
    return { display: `${hour12}:${m}`, value: `${h}:${m}` };
  });

  const formatDateTimeLocal = (date: Date) => {
    if (!(date instanceof Date) || Number.isNaN(date.getTime())) return '';
    const pad = (n: number) => String(n).padStart(2, '0');
    const year = date.getFullYear();
    const month = pad(date.getMonth() + 1);
    const day = pad(date.getDate());
    const hour = pad(date.getHours());
    const minute = pad(date.getMinutes());
    return `${year}-${month}-${day}T${hour}:${minute}`;
  };

  const firstNonEmptyString = (...values: unknown[]) => {
    for (const value of values) {
      if (typeof value === 'string' && value.trim()) return value.trim();
      if (typeof value === 'number' && Number.isFinite(value)) return String(value);
    }
    return '';
  };

  const getCalendarHashTarget = () => {
    if (typeof window === 'undefined') return null;
    const hash = window.location.hash || '';
    const match = hash.match(/^#(event|task)=([^&]+)/i);
    if (!match) return null;
    try {
      return {
        kind: match[1].toLowerCase(),
        id: decodeURIComponent(match[2]),
      };
    } catch {
      return {
        kind: match[1].toLowerCase(),
        id: match[2],
      };
    }
  };

  const isTaskEvent = (event: Record<string, unknown> | undefined) =>
    firstNonEmptyString(
      event?.componentType,
      (event?.raw as Record<string, unknown> | undefined)?.componentType,
      (event?.raw as Record<string, unknown> | undefined)?.component_type,
    ).toUpperCase() === 'VTODO';

  const isCompletedTask = (event: Record<string, unknown> | undefined) => {
    if (!event || !isTaskEvent(event)) return false;
    const status = firstNonEmptyString(event.status).toUpperCase();
    const percentComplete = Number(event.percentComplete || 0);
    return status === 'COMPLETED' || percentComplete >= 100 || Boolean(event.completedAt);
  };

  const findCalendarItemById = (id: string) => {
    const targetId = String(id || '').trim();
    if (!targetId) return undefined;
    // Expanded recurring instances have ids like `${masterUid}::${occISO}`.
    // First try the exact id in the visible (post-expansion) list to keep
    // the instance's start/end. Fall back to allEvents (which holds the
    // master) so edits resolve correctly. The "this / all" prompt
    // distinguishes per-instance vs whole-series edits at save time.
    const fromExpanded = (events || []).find(
      (ev) =>
        String((ev as Record<string, unknown>).id || (ev as Record<string, unknown>).uid || '') ===
        targetId,
    ) as Record<string, unknown> | undefined;
    if (fromExpanded) return fromExpanded;
    const masterId = targetId.includes('::') ? targetId.split('::')[0] : targetId;
    return allEvents.find(
      (ev) =>
        String((ev as Record<string, unknown>).id || (ev as Record<string, unknown>).uid || '') ===
        masterId,
    ) as Record<string, unknown> | undefined;
  };

  const applyCalendarHashSelection = () => {
    // #tasks / #calendar deep-link to the corresponding tab. The Tasks
    // and Calendar shortcut icons in Mailbox push these hashes; we
    // listen on hashchange so in-page nav (calendar ↔ tasks) toggles
    // the section without remount.
    if (typeof window !== 'undefined') {
      const hash = (window.location.hash || '').toLowerCase();
      if (hash === '#tasks' || hash === '#section=tasks') {
        currentSection = 'tasks';
        return true;
      }
      if (hash === '' || hash === '#' || hash === '#calendar' || hash === '#section=calendar') {
        currentSection = 'calendar';
        // Fall through so #event= / #task= targets still get picked up
        // when they're present alongside the section hash.
      }
    }
    const target = getCalendarHashTarget();
    if (!target?.id) return false;
    const match = findCalendarItemById(target.id);
    if (!match) return false;
    if (editEventModal && editEvent.id === String(match.id || '')) return true;
    openEditEvent({ id: match.id || match.uid || target.id });
    return true;
  };

  const to12Hour = (hhmm: string) => {
    if (!hhmm) return { time: '', meridiem: 'AM' };
    const [hStr, m = '00'] = hhmm.split(':');
    let h = parseInt(hStr, 10);
    if (Number.isNaN(h)) return { time: '', meridiem: 'AM' };
    const meridiem = h >= 12 ? 'PM' : 'AM';
    h = h % 12;
    if (h === 0) h = 12;
    const time = `${String(h).padStart(2, '0')}:${m.padStart(2, '0')}`;
    return { time, meridiem };
  };

  const to24Hour = (hhmm: string, meridiem = 'AM') => {
    if (!hhmm) return '';
    const [hStr, m = '00'] = hhmm.split(':');
    let h = parseInt(hStr, 10);
    if (Number.isNaN(h)) return '';
    if (meridiem === 'PM' && h < 12) h += 12;
    if (meridiem === 'AM' && h === 12) h = 0;
    return `${String(h).padStart(2, '0')}:${m.padStart(2, '0')}`;
  };

  const isEndAfterStart = (
    startTime: string,
    startMeridiem: string,
    endTime: string,
    endMeridiem: string,
  ) => {
    const start24 = to24Hour(startTime, startMeridiem);
    const end24 = to24Hour(endTime, endMeridiem);
    if (!start24 || !end24) return true;
    const [sh, sm] = start24.split(':').map(Number);
    const [eh, em] = end24.split(':').map(Number);
    return eh * 60 + em > sh * 60 + sm;
  };

  const getDurationMinutes = (
    startTime: string,
    startMeridiem: string,
    endTime: string,
    endMeridiem: string,
  ) => {
    const start24 = to24Hour(startTime, startMeridiem);
    const end24 = to24Hour(endTime, endMeridiem);
    if (!start24 || !end24) return 60;

    const [sh, sm] = start24.split(':').map(Number);
    const [eh, em] = end24.split(':').map(Number);
    const startMins = sh * 60 + sm;
    let endMins = eh * 60 + em;

    if (endMins <= startMins) {
      endMins += 24 * 60;
    }

    return endMins - startMins;
  };

  const addMinutesToTime = (hhmm: string, meridiem: string, minutes: number) => {
    const time24 = to24Hour(hhmm, meridiem);
    if (!time24) return { time: hhmm, meridiem };

    const [h, m] = time24.split(':').map(Number);
    let totalMins = h * 60 + m + minutes;

    while (totalMins >= 24 * 60) totalMins -= 24 * 60;
    while (totalMins < 0) totalMins += 24 * 60;

    const newH = Math.floor(totalMins / 60);
    const newM = totalMins % 60;
    const newTime24 = `${String(newH).padStart(2, '0')}:${String(newM).padStart(2, '0')}`;

    return to12Hour(newTime24);
  };

  const roundTime = (date: Date, intervalMins = 30) => {
    const minutes = date.getMinutes();
    const rounded = Math.round(minutes / intervalMins) * intervalMins;
    const newDate = new Date(date);
    newDate.setMinutes(rounded);
    newDate.setSeconds(0);
    newDate.setMilliseconds(0);
    return newDate;
  };

  const escapeHtml = (value: string) =>
    (value || '')
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');

  const containsHtml = (value: string) => /<[a-z][\s\S]*>/i.test(value || '');

  const sanitizeDescription = (value: string) =>
    DOMPurify.sanitize(value || '', {
      USE_PROFILES: { html: true },
      FORBID_TAGS: [
        'script',
        'style',
        'iframe',
        'object',
        'embed',
        'form',
        'input',
        'textarea',
        'select',
        'button',
      ],
      FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
      ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|[^a-z]|[a-z+.-]+(?:[^a-z+.-:]|$))/i,
    });

  // Sanitize data for Web Worker postMessage (removes non-cloneable properties)
  const sanitizeForWorker = <T,>(data: T): T => {
    try {
      return JSON.parse(JSON.stringify(data));
    } catch {
      return data;
    }
  };

  const parseAttendeeEmails = (value: string) => {
    if (!value) return [];
    const rawList = value
      .split(/[,;]/)
      .map((item) => normalizeEmail(item))
      .filter(Boolean);
    return Array.from(new Set(rawList));
  };

  const toBase64 = (value: string) => {
    try {
      return btoa(unescape(encodeURIComponent(value)));
    } catch {
      return btoa(value);
    }
  };

  const formatInviteDateRange = (start: Date, end: Date, allDay = false) => {
    if (!(start instanceof Date) || Number.isNaN(start.getTime())) return '';
    if (allDay) {
      return start.toLocaleDateString(i18n.getFormattingLocale(), {
        weekday: 'short',
        month: 'short',
        day: 'numeric',
        year: 'numeric',
      });
    }
    const format = (date: Date) =>
      date.toLocaleString(i18n.getFormattingLocale(), {
        weekday: 'short',
        month: 'short',
        day: 'numeric',
        year: 'numeric',
        hour: 'numeric',
        minute: '2-digit',
      });
    if (!(end instanceof Date) || Number.isNaN(end.getTime())) return format(start);
    return `${format(start)} - ${format(end)}`;
  };

  const buildInviteMessage = (eventDetails: {
    title?: string;
    start: Date;
    end: Date;
    allDay?: boolean;
    location?: string;
    url?: string;
    description?: string;
    organizer?: string;
  }) => {
    const { title, start, end, allDay, location, url, description, organizer } = eventDetails;
    const dateRange = formatInviteDateRange(start, end, allDay);
    const safeDescription = (description || '').trim();
    const textLines = [`You are invited to: ${title || 'Event'}`, '', `When: ${dateRange}`];
    if (location) textLines.push(`Where: ${location}`);
    if (url) textLines.push(`URL: ${url}`);
    if (organizer) textLines.push(`Organizer: ${organizer}`);
    if (safeDescription) {
      textLines.push('', 'Notes:', safeDescription);
    }
    textLines.push('', 'This invite includes a calendar attachment (.ics).');

    const htmlLines = [
      `<p>You are invited to: <strong>${escapeHtml(title || 'Event')}</strong></p>`,
      `<p><strong>When:</strong> ${escapeHtml(dateRange)}</p>`,
    ];
    if (location) htmlLines.push(`<p><strong>Where:</strong> ${escapeHtml(location)}</p>`);
    if (url) htmlLines.push(`<p><strong>URL:</strong> ${escapeHtml(url)}</p>`);
    if (organizer) htmlLines.push(`<p><strong>Organizer:</strong> ${escapeHtml(organizer)}</p>`);
    if (safeDescription) {
      const notes = escapeHtml(safeDescription).replace(/\r?\n/g, '<br>');
      htmlLines.push(`<p><strong>Notes:</strong><br>${notes}</p>`);
    }
    htmlLines.push('<p>This invite includes a calendar attachment (.ics).</p>');

    return {
      text: textLines.join('\n'),
      html: htmlLines.join(''),
    };
  };

  const focusTitleInput = () => {
    setTimeout(() => {
      const input = document.getElementById('event-title') as HTMLInputElement | null;
      input?.focus();
    }, 50);
  };

  const announceModal = (message: string) => {
    modalAnnouncement = message;
    setTimeout(() => {
      if (modalAnnouncement === message) modalAnnouncement = '';
    }, 1000);
  };

  const getFocusableElements = (container: HTMLElement | null) => {
    if (!container) return [];
    const focusables = container.querySelectorAll(
      'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
    );
    return Array.from(focusables).filter(
      (el) =>
        !el.hasAttribute('aria-hidden') &&
        ((el as HTMLElement).offsetParent !== null || el === document.activeElement),
    );
  };

  const trapFocus = (event: KeyboardEvent, container: HTMLElement | null) => {
    if (event.key !== 'Tab') return;
    const focusables = getFocusableElements(container);
    if (focusables.length === 0) return;
    const first = focusables[0] as HTMLElement;
    const last = focusables[focusables.length - 1] as HTMLElement;
    if (event.shiftKey && document.activeElement === first) {
      event.preventDefault();
      last.focus();
    } else if (!event.shiftKey && document.activeElement === last) {
      event.preventDefault();
      first.focus();
    }
  };

  // parseNaturalLanguage lives in src/utils/calendar-nlp.ts (unit-tested). It
  // only treats a number as a time when it carries a meridiem or a colon, so
  // titles starting with digits (years, counts, "1 on 1") aren't eaten.

  const applyParsedTitle = () => {
    if (!newEvent.title) return false;

    const parsed = parseNaturalLanguage(newEvent.title);
    if (!parsed) return false;

    const nextEvent = { ...newEvent };
    let updated = false;
    const parsedTitle = (parsed.title || '').trim();

    if (parsedTitle && parsedTitle !== newEvent.title) {
      nextEvent.title = parsedTitle;
      updated = true;
    }
    if (parsed.date && parsed.date !== newEvent.date) {
      nextEvent.date = parsed.date;
      updated = true;
    }
    if (parsed.startTime) {
      nextEvent.startTime = parsed.startTime;
      nextEvent.startMeridiem = parsed.startMeridiem || nextEvent.startMeridiem;
      updated = true;
    }
    if (parsed.endTime) {
      nextEvent.endTime = parsed.endTime;
      nextEvent.endMeridiem = parsed.endMeridiem || nextEvent.endMeridiem;
      updated = true;
    }

    if (parsed.startTime && !parsed.endTime) {
      const startMeridiem = parsed.startMeridiem || nextEvent.startMeridiem;
      const newEnd = addMinutesToTime(parsed.startTime, startMeridiem, lastDurationMinutes || 60);
      nextEvent.endTime = newEnd.time;
      nextEvent.endMeridiem = newEnd.meridiem;
      updated = true;
    }

    if (parsed.startTime && parsed.endTime) {
      const startMeridiem = parsed.startMeridiem || nextEvent.startMeridiem;
      const endMeridiem = parsed.endMeridiem || nextEvent.endMeridiem;
      lastDurationMinutes = getDurationMinutes(
        parsed.startTime,
        startMeridiem,
        parsed.endTime,
        endMeridiem,
      );
    }

    if (updated) {
      newEvent = nextEvent;
      modalDirty = true;
      if (titleError && newEvent.title?.trim()) titleError = '';
    }

    return updated;
  };

  const generateICalEvent = (
    event: {
      summary?: string;
      description?: string;
      location?: string;
      start?: string;
      end?: string;
      uid?: string;
      reminder?: number;
      url?: string;
      attendees?: string;
      timezone?: string;
      recurrence?: RecurrenceSpec;
      // All-day events are emitted as RFC 5545 VALUE=DATE (date-only, no TZID).
      allDay?: boolean;
      // Pre-computed iCal value to emit verbatim. For timed events this is the
      // literal wall-clock 'YYYYMMDDTHHMMSS' (device-tz independent); for all-day
      // it is the date 'YYYYMMDD'. Falls back to ISO conversion when omitted.
      startValue?: string;
      endValue?: string;
    },
    options: {
      method?: string;
      organizer?: string;
      attendeeEmails?: string[] | null;
      rsvp?: boolean;
    } = {},
  ) => {
    const {
      summary,
      description,
      location,
      start,
      end,
      uid,
      reminder,
      url,
      attendees,
      timezone,
      recurrence,
    } = event;
    const {
      method = 'PUBLISH',
      organizer = '',
      attendeeEmails = null,
      rsvp = false,
    } = options || {};
    const formatICalDate = (d: string | undefined) => {
      if (!d) return '';
      const date = new Date(d);
      return date.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z';
    };
    const dtstamp = formatICalDate(new Date().toISOString());
    const escape = (val: string) =>
      (val || '')
        .replace(/\\/g, '\\\\')
        .replace(/;/g, '\\;')
        .replace(/,/g, '\\,')
        .replace(/\n/g, '\\n');
    // When a timezone is set, emit the wall-clock as DTSTART;TZID=<tz>:...
    // (no Z). This preserves the creator's intent — "3pm in NY" stays at
    // "3pm in NY" across DST and on devices in other zones. Falling back
    // to bare UTC zulu loses that wall-clock anchor and is the cause of
    // the cross-device time-drift bug.
    const allDay = Boolean(event.allDay);
    const useTzid = !allDay && Boolean(timezone) && timezone !== 'UTC';
    const tzParam = useTzid ? `;TZID=${escape(timezone as string)}` : '';
    let dtstartLine: string;
    let dtendLine: string;
    if (allDay && event.startValue && event.endValue) {
      // RFC 5545 §3.8.2.4/§3.8.2.2: all-day events use date-only values and an
      // *exclusive* DTEND (the day after the last day). No TZID/VTIMEZONE.
      dtstartLine = `DTSTART;VALUE=DATE:${event.startValue}`;
      dtendLine = `DTEND;VALUE=DATE:${event.endValue}`;
    } else {
      // Timed. Prefer the literal wall-clock the caller computed straight from
      // the typed components — that is independent of the device zone, so the
      // time the user entered round-trips intact. Fall back to converting the
      // ISO instant for legacy callers that only pass start/end.
      const dtstart = useTzid
        ? event.startValue || formatICalLocal(start, timezone as string)
        : formatICalDate(start);
      const dtend = useTzid
        ? event.endValue || formatICalLocal(end, timezone as string)
        : formatICalDate(end);
      dtstartLine = `DTSTART${tzParam}:${dtstart}`;
      dtendLine = `DTEND${tzParam}:${dtend}`;
    }
    const eventUid = uid || `${Date.now()}@forwardemail.net`;
    const lines = [
      'BEGIN:VCALENDAR',
      'VERSION:2.0',
      'PRODID:-//Forward Email//Webmail//EN',
      'CALSCALE:GREGORIAN',
      `METHOD:${method}`,
    ];
    // RFC 5545 §3.6.5: any referenced TZID must be defined by a VTIMEZONE
    // component in the same calendar. Without this, strict CalDAV clients
    // (Apple, DAVx5) treat the TZID as opaque or fall back to UTC.
    if (useTzid) lines.push(...buildVTimezone(timezone as string));

    lines.push(
      'BEGIN:VEVENT',
      `UID:${eventUid}`,
      `DTSTAMP:${dtstamp}`,
      dtstartLine,
      dtendLine,
      `SUMMARY:${escape(summary || 'Untitled Event')}`,
    );
    if (description) lines.push(`DESCRIPTION:${escape(description)}`);
    if (location) lines.push(`LOCATION:${escape(location)}`);
    if (url) lines.push(`URL:${escape(url)}`);
    // Emit RRULE when the user picked a recurrence. buildRrule returns
    // '' for mode==='none' and the rawRrule verbatim for mode==='custom'
    // so unrecognized rules round-trip unchanged.
    if (recurrence && recurrence.mode !== 'none') {
      const rruleLine = buildRrule(recurrence, start ? new Date(start) : null);
      if (rruleLine) lines.push(rruleLine);
    }
    if (organizer) lines.push(`ORGANIZER;CN=${escape(organizer)}:mailto:${organizer}`);
    const attendeeList = attendeeEmails?.length
      ? attendeeEmails
      : attendees
        ? attendees
            .split(/[,;]/)
            .map((e) => e.trim())
            .filter(Boolean)
        : [];
    if (attendeeList.length) {
      attendeeList.forEach((email) => {
        const rsvpParams = rsvp ? ';ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP=TRUE' : '';
        lines.push(`ATTENDEE;CN=${email}${rsvpParams}:mailto:${email}`);
      });
    }
    if (reminder && reminder > 0) {
      lines.push(
        'BEGIN:VALARM',
        'ACTION:DISPLAY',
        `DESCRIPTION:${escape(summary || 'Event reminder')}`,
        `TRIGGER:-PT${reminder}M`,
        'END:VALARM',
      );
    }
    lines.push('END:VEVENT', 'END:VCALENDAR');
    return lines.join('\r\n');
  };

  const generateICalTodo = (
    task: {
      summary?: string;
      description?: string;
      location?: string;
      start?: string;
      due?: string;
      uid?: string;
      url?: string;
      timezone?: string;
      status?: string;
      completedAt?: string;
      percentComplete?: number;
      reminder?: number;
      // Literal iCal wall-clock 'YYYYMMDDTHHMMSS' for DTSTART/DUE, computed from
      // the typed components (device-tz independent). Falls back to converting
      // the start/due ISO instant when omitted.
      startValue?: string;
      dueValue?: string;
    },
    options: { method?: string } = {},
  ) => {
    const {
      summary,
      description,
      location,
      start,
      due,
      uid,
      url,
      timezone,
      status,
      completedAt,
      percentComplete,
      reminder,
    } = task;
    const { method = 'PUBLISH' } = options;
    const formatICalDate = (d: string | undefined) => {
      if (!d) return '';
      const date = new Date(d);
      return Number.isFinite(date.getTime())
        ? date.toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
        : '';
    };
    const escape = (val: string) =>
      (val || '')
        .replace(/\\/g, '\\\\')
        .replace(/;/g, '\\;')
        .replace(/,/g, '\\,')
        .replace(/\n/g, '\\n');
    const todoUid = uid || `${Date.now()}@forwardemail.net`;
    // See generateICalEvent for the TZID rationale — same wall-clock anchor
    // applies to VTODO DTSTART/DUE.
    const useTzid = Boolean(timezone) && timezone !== 'UTC';
    const lines = [
      'BEGIN:VCALENDAR',
      'VERSION:2.0',
      'PRODID:-//Forward Email//Webmail//EN',
      'CALSCALE:GREGORIAN',
      `METHOD:${method}`,
    ];
    if (useTzid) lines.push(...buildVTimezone(timezone as string));

    lines.push(
      'BEGIN:VTODO',
      `UID:${todoUid}`,
      `DTSTAMP:${formatICalDate(new Date().toISOString())}`,
      `SUMMARY:${escape(summary || 'Untitled Task')}`,
    );
    const tzParam = useTzid ? `;TZID=${escape(timezone as string)}` : '';
    // Prefer the literal wall-clock from the typed components so the time the
    // user entered round-trips intact regardless of the device zone.
    const dtstart = useTzid
      ? task.startValue || formatICalLocal(start, timezone as string)
      : formatICalDate(start);
    const dueDate = useTzid
      ? task.dueValue || formatICalLocal(due, timezone as string)
      : formatICalDate(due);
    const completed = formatICalDate(completedAt);
    if (dtstart) lines.push(`DTSTART${tzParam}:${dtstart}`);
    if (dueDate) lines.push(`DUE${tzParam}:${dueDate}`);
    if (description) lines.push(`DESCRIPTION:${escape(description)}`);
    if (location) lines.push(`LOCATION:${escape(location)}`);
    if (url) lines.push(`URL:${escape(url)}`);
    if (status) lines.push(`STATUS:${escape(status.toUpperCase())}`);
    if (typeof percentComplete === 'number' && Number.isFinite(percentComplete)) {
      lines.push(`PERCENT-COMPLETE:${Math.max(0, Math.min(100, Math.round(percentComplete)))}`);
    }
    if (completed) lines.push(`COMPLETED:${completed}`);
    // VALARM for VTODO. Trigger is relative; backend's normalizeVAlarm
    // keeps it that way so future DUE changes auto-shift the alarm.
    // RELATED=END anchors the offset to DUE (vs DTSTART) per RFC 5545.
    if (typeof reminder === 'number' && reminder > 0 && Number.isFinite(reminder)) {
      const minutes = Math.max(1, Math.round(reminder));
      lines.push(
        'BEGIN:VALARM',
        'ACTION:DISPLAY',
        `DESCRIPTION:${escape(summary || 'Task reminder')}`,
        dueDate ? `TRIGGER;RELATED=END:-PT${minutes}M` : `TRIGGER:-PT${minutes}M`,
        'END:VALARM',
      );
    }
    lines.push('END:VTODO', 'END:VCALENDAR');
    return lines.join('\r\n');
  };

  // Parse the first VALARM TRIGGER from a VTODO/VEVENT ICS into
  // minutes-before-anchor. Handles the durations we emit
  // (-PT{N}M / -PT{N}H / -P{N}D / -P{N}DT{H}H{M}M). Backend normalizes
  // absolute triggers to relative durations, so this round-trips losslessly.
  const parseTaskReminderMinutes = (ical: unknown, kind: 'VTODO' | 'VEVENT' = 'VTODO'): number => {
    if (typeof ical !== 'string' || !ical) return 0;
    const re =
      kind === 'VEVENT' ? /BEGIN:VEVENT[\s\S]*?END:VEVENT/ : /BEGIN:VTODO[\s\S]*?END:VTODO/;
    const compMatch = ical.match(re);
    if (!compMatch) return 0;
    const triggerMatch = compMatch[0].match(/TRIGGER[^:\r\n]*:(-?P[^\r\n]+)/i);
    if (!triggerMatch) return 0;
    const dur = triggerMatch[1].toUpperCase();
    const negative = dur.startsWith('-');
    const body = dur.replace(/^[-+]?P/, '');
    const dayMatch = body.match(/(\d+)D/);
    const hourMatch = body.match(/(\d+)H/);
    const minMatch = body.match(/(\d+)M/);
    const total =
      (dayMatch ? Number(dayMatch[1]) * 1440 : 0) +
      (hourMatch ? Number(hourMatch[1]) * 60 : 0) +
      (minMatch ? Number(minMatch[1]) : 0);
    return negative ? total : 0;
  };

  const exportEventAsICS = (event: Record<string, unknown> | undefined) => {
    if (!event) return;
    const icalContent = isTaskEvent(event)
      ? generateICalTodo({
          summary: event.title as string,
          description: (event.description as string) || '',
          location: (event.location as string) || '',
          url: (event.url as string) || '',
          timezone: (event.timezone as string) || getDefaultTimezone(),
          start: event.start as string,
          due: (event.end as string) || (event.start as string),
          uid: event.id as string,
          status: (event.status as string) || '',
          completedAt: (event.completedAt as string) || '',
          percentComplete: Number(event.percentComplete) || 0,
        })
      : generateICalEvent({
          summary: event.title as string,
          description: (event.description as string) || '',
          location: (event.location as string) || '',
          url: (event.url as string) || '',
          timezone: (event.timezone as string) || getDefaultTimezone(),
          attendees: (event.attendees as string) || '',
          start: event.start as string,
          end: event.end as string,
          uid: event.id as string,
          reminder: Number(event.notify) || 0,
        });
    const filename = ((event.title as string) || 'event').replace(/[^a-z0-9]/gi, '_');
    downloadFile(icalContent, `${filename}.ics`, 'text/calendar;charset=utf-8');
    toasts?.show?.(isTaskEvent(event) ? 'Task exported' : 'Event exported', 'success');
  };

  const queueEventInvites = async ({
    title,
    start,
    end,
    allDay,
    description,
    location,
    url,
    timezone,
    attendees,
    uid,
  }: {
    title?: string;
    start: Date;
    end: Date;
    allDay?: boolean;
    description?: string;
    location?: string;
    url?: string;
    timezone?: string;
    attendees?: string;
    uid?: string;
  }) => {
    const attendeeEmails = parseAttendeeEmails(attendees || '');
    if (!attendeeEmails.length) return { queued: 0 };
    const organizer = getActiveFromAddress();
    if (!organizer) return { queued: 0 };

    const inviteTitle = title || 'Event';
    const inviteUid = uid || `${Date.now()}@forwardemail.net`;
    const { text, html } = buildInviteMessage({
      title: inviteTitle,
      start,
      end,
      allDay,
      location,
      url,
      description,
      organizer,
    });
    const filename = inviteTitle.replace(/[^a-z0-9]/gi, '_') || 'event';

    await Promise.all(
      attendeeEmails.map(async (email) => {
        const icalContent = generateICalEvent(
          {
            summary: inviteTitle,
            description: description || '',
            location: location || '',
            url: url || '',
            timezone: timezone || getDefaultTimezone(),
            attendees: '',
            start: start?.toISOString?.() || '',
            end: end?.toISOString?.() || '',
            uid: inviteUid,
          },
          {
            method: 'REQUEST',
            organizer,
            attendeeEmails: [email],
            rsvp: true,
          },
        );
        const payload = {
          from: organizer,
          to: [email],
          subject: `Invitation: ${inviteTitle}`,
          text,
          html,
          attachments: [
            {
              filename: `${filename}.ics`,
              contentType: 'text/calendar; method=REQUEST; charset=UTF-8',
              content: toBase64(icalContent),
              encoding: 'base64',
            },
          ],
          has_attachment: true,
          save_sent: true,
        };
        await queueEmail(payload);
      }),
    );

    return { queued: attendeeEmails.length };
  };

  const duplicateEvent = () => {
    const {
      title,
      date,
      startTime,
      startMeridiem,
      endTime,
      endMeridiem,
      description,
      location,
      url,
      timezone,
      attendees,
      notify,
      allDay,
    } = editEvent;
    newEvent = {
      calendarId: editEvent.calendarId || resolveActiveCalendarId(),
      title: `${title} (Copy)`,
      date,
      endDate: editEvent.endDate || date,
      startTime,
      startMeridiem,
      endTime,
      endMeridiem,
      allDay,
      description,
      location,
      url,
      timezone,
      attendees,
      notify,
      componentType: 'VEVENT',
      hasDate: true,
      hasTime: true,
      // Don't carry recurrence forward on duplicate — copying a
      // recurring event into a non-recurring one is the safer default;
      // user can re-pick recurrence in the new event modal.
      recurrence: { ...DEFAULT_RECURRENCE },
      _editingRawDescription: false,
    };
    editEventModal = false;
    newEventModal = true;
    optionalFieldsExpanded = false;
    modalDirty = false;
    lastDurationMinutes = getDurationMinutes(startTime, startMeridiem, endTime, endMeridiem);
    titleError = '';
    announceModal('New event dialog opened');
    focusTitleInput();
  };

  const importICS = async (eventOrFiles: Event | File[]) => {
    let file: File | undefined;
    let target: HTMLInputElement | null = null;
    if (Array.isArray(eventOrFiles)) {
      file = eventOrFiles[0];
    } else {
      target = eventOrFiles.target as HTMLInputElement;
      file = target?.files?.[0];
    }
    if (!file) return;

    const maxSize = 5 * 1024 * 1024;
    if (file.size > maxSize) {
      toasts?.show?.('File too large. Maximum size is 5MB.', 'error');
      if (target) target.value = '';
      return;
    }

    try {
      const content = await file.text();
      const eventBlocks = content.match(/BEGIN:VEVENT[\s\S]*?END:VEVENT/gi) || [];

      let imported = 0;
      let updated = 0;
      const calendar = activeCalendar();
      if (!calendar) {
        toasts?.show?.('No calendar selected.', 'error');
        return;
      }
      const calendarId = getCalendarId(calendar);
      if (!eventBlocks.length) {
        toasts?.show?.('No events found in calendar file.', 'error');
        return;
      }
      const calendarShell = content.replace(/BEGIN:VEVENT[\s\S]*?END:VEVENT\r?\n?/gi, '');
      const endIndex = calendarShell.search(/END:VCALENDAR/i);
      const hasCalendarShell = /BEGIN:VCALENDAR/i.test(calendarShell) && endIndex !== -1;
      const calendarPrefix = hasCalendarShell ? calendarShell.slice(0, endIndex) : '';
      const calendarSuffix = hasCalendarShell ? calendarShell.slice(endIndex) : '';
      const useRawContent =
        eventBlocks.length === 1 &&
        /BEGIN:VCALENDAR/i.test(content) &&
        /END:VCALENDAR/i.test(content);
      const buildIcalPayload = (eventBlock: string) => {
        if (useRawContent) return content;
        if (hasCalendarShell) return `${calendarPrefix}${eventBlock}\r\n${calendarSuffix}`;
        return [
          'BEGIN:VCALENDAR',
          'VERSION:2.0',
          'PRODID:-//Forward Email//Webmail//EN',
          'CALSCALE:GREGORIAN',
          'METHOD:PUBLISH',
          eventBlock,
          'END:VCALENDAR',
        ].join('\r\n');
      };
      for (const eventBlock of eventBlocks) {
        const rawUid = eventBlock.match(/^UID:(.+)$/m)?.[1]?.trim();
        const existing = rawUid
          ? events.find(
              (ev) =>
                (ev as Record<string, unknown>).id === rawUid ||
                (ev as Record<string, unknown>).uid === rawUid,
            )
          : null;
        const icalPayload = buildIcalPayload(eventBlock);

        if (existing) {
          try {
            const payload = {
              id: (existing as Record<string, unknown>).id,
              calendar_id: calendarId,
              ical: icalPayload,
            };
            await Remote.request('CalendarEventUpdate', payload, {
              method: 'PUT',
              pathOverride: `/v1/calendar-events/${(existing as Record<string, unknown>).id}`,
            });
            updated++;
          } catch (err) {
            console.error('Failed to update event:', err);
          }
        } else {
          try {
            const payload = { calendar_id: calendarId, ical: icalPayload };
            await Remote.request('CalendarEventCreate', payload, { method: 'POST' });
            imported++;
          } catch (err) {
            console.error('Failed to create event:', err);
          }
        }
      }

      if (imported + updated > 0) {
        await loadEventsForSelection(true);
      }

      const msg =
        imported > 0 && updated > 0
          ? `Imported ${imported} and updated ${updated} events`
          : imported > 0
            ? `Imported ${imported} event${imported > 1 ? 's' : ''}`
            : updated > 0
              ? `Updated ${updated} event${updated > 1 ? 's' : ''}`
              : 'No events imported';
      toasts?.show?.(msg, 'success');
    } catch (err) {
      toasts?.show?.(
        'Failed to import calendar: ' + ((err as Error)?.message || 'Unknown error'),
        'error',
      );
    } finally {
      if (target) target.value = '';
    }
  };

  const setError = (msg: string) => {
    error = msg || '';
    if (msg) {
      setTimeout(() => {
        error = '';
      }, 5000);
    }
  };

  const setSuccess = (msg: string) => {
    if (msg) toasts?.show?.(msg, 'success');
  };

  const getNoCalendarMessage = () =>
    calendars.length > 0
      ? 'Select a calendar before adding an event.'
      : 'Create a calendar before adding an event.';

  const ensureEventCalendarSelected = () => {
    const calendar = activeCalendar();
    if (calendar) return calendar;
    const message = getNoCalendarMessage();
    newEventModal = false;
    modalDirty = false;
    titleError = '';
    setError(message);
    toasts?.show?.(message, 'error');
    return null;
  };

  const resetCalendarState = () => {
    loadRequestId += 1;
    calendars = [];
    selectedCalendarIds = [];
    activeCalendarId = '';
    events = [];
    allEvents = [];
    eventsScope = 'none';
    eventsScopeCalendarId = '';
    calendarInstance = null;
    lastEventSignature = null;
    calendarCreated = false;
    calendarsLoaded = false;
    loading = false;
    error = '';
    newEventModal = false;
    editEventModal = false;
    showDeleteConfirm = false;
    showNewStartPicker = false;
    showNewEndPicker = false;
    showEditStartPicker = false;
    showEditEndPicker = false;
    optionalFieldsExpanded = false;
    filterMenuOpen = false;
    modalDirty = false;
    savingEvent = false;
    titleError = '';
    modalAnnouncement = '';
  };

  const uniqueIds = (list: string[]) => Array.from(new Set((list || []).filter(Boolean)));

  const getCalendarById = (id: string) => calendars.find((c) => getCalendarId(c) === id);

  const resolveActiveCalendarId = () =>
    activeCalendarId || selectedCalendarIds[0] || getCalendarId(calendars[0]) || '';

  const activeCalendar = () => getCalendarById(resolveActiveCalendarId());

  const parseCalendarPrefs = (
    value: unknown,
  ): { selectedIds: string[]; activeId: string; knownIds: string[] } | null => {
    if (!value) return null;
    if (typeof value === 'string') {
      try {
        return parseCalendarPrefs(JSON.parse(value));
      } catch {
        return null;
      }
    }
    if (typeof value !== 'object') return null;
    const v = value as Record<string, unknown>;
    const selectedIds = Array.isArray(v.selectedIds)
      ? (v.selectedIds as string[]).filter(Boolean)
      : Array.isArray(v.selectedCalendarIds)
        ? (v.selectedCalendarIds as string[]).filter(Boolean)
        : v.selectedCalendarId
          ? [v.selectedCalendarId as string]
          : [];
    const activeId =
      typeof v.activeId === 'string'
        ? v.activeId
        : typeof v.selectedCalendarId === 'string'
          ? v.selectedCalendarId
          : '';
    const knownIds = Array.isArray(v.knownIds) ? (v.knownIds as string[]).filter(Boolean) : [];
    return { selectedIds, activeId, knownIds };
  };

  const readCalendarPrefs = async (accountKey: string) => {
    const key = getCalendarPrefsKey(accountKey);
    const localPrefs = parseCalendarPrefs(Local.get(key));
    if (localPrefs) return localPrefs;
    try {
      const record = await db.meta.get(key);
      return parseCalendarPrefs(record?.value);
    } catch {
      return null;
    }
  };

  const persistCalendarPrefs = async (accountKey: string, selectedIds: string[]) => {
    const key = getCalendarPrefsKey(accountKey);
    const payload = {
      selectedIds: uniqueIds(selectedIds),
      // Persist the seen-set too, with the current selection as a floor so we
      // never lose track of which calendars already existed.
      knownIds: uniqueIds([...knownCalendarIds, ...selectedIds]),
    };
    try {
      Local.set(key, JSON.stringify(payload));
    } catch {
      // ignore local storage failures
    }
    try {
      await db.meta.put({ key, value: payload, updatedAt: Date.now() });
    } catch {
      // ignore meta persistence failures
    }
  };

  const reconcileCalendarSelection = (
    list: unknown[],
    prefs: { selectedIds: string[]; activeId: string } | null,
  ) => {
    const listIds = uniqueIds((list || []).map(getCalendarId) as string[]);
    const listSet = new Set(listIds);
    const knownSet = new Set(knownCalendarIds);
    // Keep the user's prior selection (dropping calendars that no longer
    // exist) and additionally default any calendar we've never seen before to
    // visible. A calendar created elsewhere (another CalDAV client, the web)
    // should show up without the user hunting for it; only calendars they
    // explicitly deselected — seen before, but not selected — stay hidden.
    const prevSelected = (prefs?.selectedIds || []).filter((id) => listSet.has(id));
    const newlySeen = listIds.filter((id) => !knownSet.has(id));
    let selectedIds = uniqueIds([...prevSelected, ...newlySeen]);
    if (!selectedIds.length && listIds.length) selectedIds = listIds;
    // Record everything currently on the server so the next reconcile can tell
    // new calendars apart from deselected ones.
    knownCalendarIds = uniqueIds([...knownCalendarIds, ...listIds]);
    // Always default to the "Calendar" calendar (not Tasks or other custom calendars)
    const defaultId = selectedIds.find((id) => {
      const cal = list.find((c) => getCalendarId(c) === id);
      const label = cal ? getCalendarLabel(cal) : '';
      return label === 'Calendar';
    });
    const activeId = defaultId || selectedIds[0] || '';
    if (activeId && !selectedIds.includes(activeId))
      selectedIds = uniqueIds([...selectedIds, activeId]);
    selectedCalendarIds = selectedIds;
    activeCalendarId = activeId;
    return { selectedIds, activeId };
  };

  const isDesktopViewport = () => typeof window !== 'undefined' && window.innerWidth > 900;

  const resolveDefaultView = () => {
    const view = isDesktopViewport() ? viewMonthGrid : viewWeek;
    return (view as { name?: string })?.name || view;
  };

  const toDate = (input: unknown): Date | null => {
    if (!input) return null;
    if (input instanceof Date) return input;
    if (typeof input === 'object') {
      const o = input as Record<string, unknown>;
      const year = o?.year as number;
      const month = o?.month as number;
      const day = o?.day as number;
      if ([year, month, day].every((val) => Number.isFinite(val))) {
        return new Date(year, month - 1, day);
      }
    }
    if (typeof input === 'string') {
      // If the string ends with Z or has a timezone offset, parse using native Date
      // which correctly handles UTC/timezone conversion to local time
      if (input.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(input)) {
        const d = new Date(input);
        if (!Number.isNaN(d.getTime())) return d;
      }
      // Date-only strings: parse as local midnight
      const dateOnlyMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
      if (dateOnlyMatch) {
        const [, year, month, day] = dateOnlyMatch;
        return new Date(Number(year), Number(month) - 1, Number(day));
      }
      // Date-time strings without timezone: parse components as local time
      const dateTimeMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/);
      if (dateTimeMatch) {
        const [, year, month, day, hour, minute, second = '0'] = dateTimeMatch;
        return new Date(
          Number(year),
          Number(month) - 1,
          Number(day),
          Number(hour),
          Number(minute),
          Number(second),
        );
      }
      const d = new Date(input);
      if (!Number.isNaN(d.getTime())) return d;
    }
    if (typeof input === 'object' && (input as { toString?: () => string })?.toString) {
      const d = new Date((input as { toString: () => string }).toString());
      if (!Number.isNaN(d.getTime())) return d;
    }
    return null;
  };

  // Expansion window for recurring events. Scoped to a generous range
  // around "now" so the user sees recurring instances when scrolling
  // through the next year. View-aware re-expansion (hook into Schedule-X's
  // range change) is a future improvement.
  const RECURRENCE_WINDOW_DAYS_BEFORE = 90;
  const RECURRENCE_WINDOW_DAYS_AFTER = 365;
  const getRecurrenceWindow = () => {
    const now = new Date();
    const start = new Date(now);
    start.setDate(start.getDate() - RECURRENCE_WINDOW_DAYS_BEFORE);
    const end = new Date(now);
    end.setDate(end.getDate() + RECURRENCE_WINDOW_DAYS_AFTER);
    return { start, end };
  };

  const applySelectedEvents = () => {
    const selectedSet = new Set(effectiveSelectedCalendarIds);
    if (!selectedSet.size) {
      events = [];
      return;
    }
    const filtered = (allEvents || []).filter((ev) => {
      const e = ev as Record<string, unknown>;
      return selectedSet.has(
        (e.calendarId ||
          e.calendar_id ||
          (e.raw as Record<string, unknown>)?.calendar_id) as string,
      );
    });
    // Expand recurring masters into concrete instances before handing
    // them to Schedule-X. Non-recurring events pass through.
    const { start: winStart, end: winEnd } = getRecurrenceWindow();
    events = expandRecurringEvents(filtered as Record<string, unknown>[], winStart, winEnd);
  };

  const hydrateEventsFromCache = async (
    requestId: number,
    accountKey: string,
    selectedIds: string[],
  ) => {
    if (!selectedIds?.length) return;
    if (selectedIds.length > 1) {
      const cached = await db.meta.get(getAllEventsCacheKey(accountKey));
      if (requestId !== loadRequestId) return;
      if (cached?.value) {
        allEvents = cached.value as unknown[];
        eventsScope = 'all';
        eventsScopeCalendarId = '';
        applySelectedEvents();
      }
      return;
    }
    const calendarId = selectedIds[0];
    const cached = await db.meta.get(getEventsCacheKey(accountKey, calendarId));
    if (requestId !== loadRequestId) return;
    if (cached?.value) {
      allEvents = cached.value as unknown[];
      eventsScope = 'calendar';
      eventsScopeCalendarId = calendarId;
      applySelectedEvents();
    }
  };

  // Parse time string and return as ISO string, preserving the intended time
  const normalizeEventTime = (timeValue: unknown): string => {
    if (!timeValue) return '';
    const str = String(timeValue);

    // If it's already an ISO string with Z (UTC), keep it as-is
    if (str.endsWith('Z')) return str;

    // If it has timezone offset like +00:00 or -08:00, parse and convert to ISO
    if (/[+-]\d{2}:\d{2}$/.test(str)) {
      const d = new Date(str);
      if (!isNaN(d.getTime())) return d.toISOString();
      return str;
    }

    // For any other format, just return as-is and let formatForScheduleX handle it
    // This preserves the original time value from the API
    return str;
  };

  const mapCalendarEvents = (list: unknown[], defaultCalendarId = '') =>
    (list || []).map((ev) => {
      const e = ev as Record<string, unknown>;
      const componentType = firstNonEmptyString(
        e.componentType,
        e.component_type,
        'VEVENT',
      ).toUpperCase();
      const start = normalizeEventTime(
        e.start ||
          e.start_date ||
          e.dtstart ||
          e.start_time ||
          e.due ||
          e.end ||
          e.dtend ||
          e.end_time,
      );
      const end = normalizeEventTime(
        e.end ||
          e.end_date ||
          e.dtend ||
          e.end_time ||
          e.due ||
          e.start ||
          e.dtstart ||
          e.start_time,
      );
      // Detect all-day from the raw ICS (DTSTART;VALUE=DATE). The literal date
      // strings let the renderer draw a date-only bar with no timezone day-shift,
      // instead of a midnight→next-midnight block that spills into a second day.
      const allDayInfo = parseAllDayFromIcal(e.ical);
      return {
        id: e.id || e.uid || e.event_id,
        title: e.summary || e.title || e.name || (componentType === 'VTODO' ? 'Task' : 'Event'),
        start,
        end,
        calendarId: e.calendar_id || e.calendarId || defaultCalendarId,
        description: e.description || e.notes || '',
        location: e.location || '',
        url: e.url || '',
        timezone: e.timezone || getDefaultTimezone(),
        attendees: e.attendees || '',
        notify: e.notify || e.reminder || 0,
        componentType,
        allDay: allDayInfo.allDay,
        allDayStart: allDayInfo.startDate || '',
        allDayEnd: allDayInfo.endDate || '',
        status: firstNonEmptyString(e.status, e.task_status),
        completedAt: firstNonEmptyString(e.completed, e.completed_at, e.completedAt),
        percentComplete: Number(e.percent_complete || e.percentComplete || 0),
        raw: ev,
      };
    });

  const fetchAllEvents = async (
    requestId: number,
    accountKey: string,
    attempt = 1,
  ): Promise<void> => {
    try {
      const list = await loadPaginatedCalendarResource(
        requestId,
        'CalendarEvents',
        {},
        CALENDAR_EVENTS_PAGE_SIZE,
        getCalendarEventsList,
      );
      if (!list || requestId !== loadRequestId) return;
      const mapped = applyPendingEventMutations(mapCalendarEvents(list));
      allEvents = mapped;
      eventsScope = 'all';
      eventsScopeCalendarId = '';
      await db.meta.put({
        key: getAllEventsCacheKey(accountKey),
        value: mapped,
        updatedAt: Date.now(),
      });
    } catch (err) {
      if (attempt < 3) {
        return fetchAllEvents(requestId, accountKey, attempt + 1);
      }
      if (requestId !== loadRequestId) return;
      setError((err as Error)?.message || 'Unable to load events.');
      const cached = await db.meta.get(getAllEventsCacheKey(accountKey));
      if (requestId !== loadRequestId) return;
      if (cached?.value) {
        allEvents = cached.value as unknown[];
        eventsScope = 'all';
        eventsScopeCalendarId = '';
      }
    }
  };

  const fetchEventsForCalendar = async (
    requestId: number,
    accountKey: string,
    calendarId: string,
    attempt = 1,
  ): Promise<void> => {
    if (!calendarId) {
      events = [];
      return;
    }
    try {
      const list = await loadPaginatedCalendarResource(
        requestId,
        'CalendarEvents',
        { calendar_id: calendarId },
        CALENDAR_EVENTS_PAGE_SIZE,
        getCalendarEventsList,
      );
      if (!list || requestId !== loadRequestId) return;
      const mapped = applyPendingEventMutations(mapCalendarEvents(list, calendarId));
      allEvents = mapped;
      eventsScope = 'calendar';
      eventsScopeCalendarId = calendarId;
      await db.meta.put({
        key: getEventsCacheKey(accountKey, calendarId),
        value: mapped,
        updatedAt: Date.now(),
      });
    } catch (err) {
      // Don't retry client errors (4xx) — they won't succeed on retry
      const status =
        (err as { status?: number })?.status || (err as { statusCode?: number })?.statusCode;
      const isClientError = typeof status === 'number' && status >= 400 && status < 500;
      if (!isClientError && attempt < 3) {
        return fetchEventsForCalendar(requestId, accountKey, calendarId, attempt + 1);
      }
      if (requestId !== loadRequestId) return;
      if (!isClientError) {
        setError((err as Error)?.message || 'Unable to load events.');
      }
      const cached = await db.meta.get(getEventsCacheKey(accountKey, calendarId));
      if (requestId !== loadRequestId) return;
      if (cached?.value) {
        allEvents = cached.value as unknown[];
        eventsScope = 'calendar';
        eventsScopeCalendarId = calendarId;
      }
    }
  };

  const loadEventsForSelection = async (force = false) => {
    const requestId = loadRequestId;
    const accountKey = getAccountKey();
    const selectedIds = uniqueIds(effectiveSelectedCalendarIds);
    const hashTarget = getCalendarHashTarget();
    if (!selectedIds.length) {
      events = [];
      return;
    }

    if (hashTarget?.id) {
      await fetchAllEvents(requestId, accountKey);
      if (requestId !== loadRequestId) return;
      applySelectedEvents();
      applyCalendarHashSelection();
      return;
    }

    if (!force && eventsScope === 'all') {
      applySelectedEvents();
      return;
    }

    if (selectedIds.length > 1) {
      await fetchAllEvents(requestId, accountKey);
      if (requestId !== loadRequestId) return;
      applySelectedEvents();
      applyCalendarHashSelection();
      return;
    }

    const calendarId = selectedIds[0];
    if (!force && eventsScope === 'calendar' && eventsScopeCalendarId === calendarId) {
      applySelectedEvents();
      applyCalendarHashSelection();
      return;
    }
    await fetchEventsForCalendar(requestId, accountKey, calendarId);
    if (requestId !== loadRequestId) return;
    applySelectedEvents();
    applyCalendarHashSelection();
  };

  const openDeleteCalendarModal = (calendarIdParam?: string) => {
    const calendarId = calendarIdParam || resolveActiveCalendarId();
    const calendar = calendarIdParam ? getCalendarById(calendarIdParam) : activeCalendar();
    if (!calendar || !calendarId) {
      toasts?.show?.('Select a calendar to delete.', 'error');
      return;
    }

    deleteCalendarId = calendarId;
    deleteCalendarLabel = getCalendarLabel(calendar) || 'Calendar';
    deleteCalendarConfirmName = '';
    filterMenuOpen = false;
    deleteCalendarModal = true;
  };

  const createNewCalendar = async () => {
    const name = newCalendarName.trim();
    if (!name) return;
    savingCalendar = true;
    try {
      const createdResponse = await Remote.request('CalendarCreate', {
        name,
        color: newCalendarColor,
      });
      const createdCalendar =
        ((createdResponse as { Result?: unknown })?.Result as Record<string, unknown>) ||
        (createdResponse as Record<string, unknown>);
      const createdCalendarId = getCalendarId(createdCalendar);
      toasts?.show?.('Calendar created', 'success');
      newCalendarModal = false;
      newCalendarName = '';
      newCalendarColor = '#1c7ed6';
      try {
        await fetchCalendars();
        if (createdCalendarId) {
          selectedCalendarIds = uniqueIds([...selectedCalendarIds, createdCalendarId]);
          activeCalendarId = createdCalendarId;
          await persistCalendarPrefs(getAccountKey(), selectedCalendarIds);
          await loadEventsForSelection(true);
        }
      } catch {
        // Refresh failed but calendar was created — user can reload
      }
    } catch (err: unknown) {
      if (isDemoBlockedError(err)) {
        newCalendarModal = false;
        return;
      }
      toasts?.show?.((err as Error)?.message || 'Failed to create calendar', 'error');
    } finally {
      savingCalendar = false;
    }
  };

  const deleteCalendar = async () => {
    if (!deleteCalendarId) return;
    if (deleteCalendarConfirmName.trim() !== deleteCalendarLabel.trim()) {
      toasts?.show?.('Type the full calendar name to confirm deletion.', 'error');
      return;
    }

    const removedCalendarId = deleteCalendarId;
    const wasSelected = selectedCalendarIds.includes(removedCalendarId);
    const wasActive = activeCalendarId === removedCalendarId;

    // Remove the calendar and its events locally and close the modal right
    // away. The server delete runs below; a failure refetches to restore
    // everything that was removed.
    calendars = calendars.filter((cal) => getCalendarId(cal) !== removedCalendarId);
    selectedCalendarIds = selectedCalendarIds.filter((id) => id !== removedCalendarId);
    if (wasActive) activeCalendarId = '';
    allEvents = allEvents.filter(
      (ev) =>
        ((ev as Record<string, unknown>).calendarId ||
          (ev as Record<string, unknown>).calendar_id) !== removedCalendarId,
    );
    applySelectedEvents();

    deleteCalendarModal = false;
    deleteCalendarId = '';
    deleteCalendarLabel = '';
    deleteCalendarConfirmName = '';
    toasts?.show?.('Calendar deleted', 'success');

    db.meta
      .put({ key: getCalendarsCacheKey(getAccountKey()), value: calendars, updatedAt: Date.now() })
      .catch((e: unknown) => console.warn('[Calendar] Failed to cache calendars:', e));
    db.meta.delete(getEventsCacheKey(getAccountKey(), removedCalendarId)).catch(() => {});
    persistEventsCache(removedCalendarId);
    persistCalendarPrefs(getAccountKey(), selectedCalendarIds).catch(() => {});

    try {
      await Remote.request(
        'CalendarDelete',
        {},
        {
          method: 'DELETE',
          pathOverride: `/v1/calendars/${encodeURIComponent(removedCalendarId)}`,
        },
      );
      // Reconcile with the server in the background.
      fetchCalendars().catch(() => {});
    } catch (err: unknown) {
      const httpStatus =
        (err as { status?: number })?.status || (err as { statusCode?: number })?.statusCode;
      // Already gone server side counts as a successful delete.
      if (httpStatus === 404) return;
      // Restore the previous selection, then refetch so the calendar and
      // its events come back.
      if (wasSelected && !selectedCalendarIds.includes(removedCalendarId)) {
        selectedCalendarIds = [...selectedCalendarIds, removedCalendarId];
      }
      if (wasActive && !activeCalendarId) activeCalendarId = removedCalendarId;
      await fetchCalendars();
      await loadEventsForSelection(true);
      if (!isDemoBlockedError(err)) {
        toasts?.show?.((err as Error)?.message || 'Failed to delete calendar', 'error');
      }
    }
  };

  const fetchCalendars = async (attempt = 1): Promise<void> => {
    const requestId = loadRequestId;
    const accountKey = getAccountKey();
    const calendarsCacheKey = getCalendarsCacheKey(accountKey);
    const storedPrefs = await readCalendarPrefs(accountKey);
    // Seed the seen-set from this account's stored prefs before reconciling so
    // new vs. deselected calendars can be distinguished (see reconcile).
    knownCalendarIds = uniqueIds(storedPrefs?.knownIds || []);

    const cached = await db.meta.get(calendarsCacheKey);
    if (requestId !== loadRequestId) return;
    if (cached?.value && (cached.value as unknown[])?.length) {
      calendars = cached.value as unknown[];
      const prefsSnapshot = selectedCalendarIds.length
        ? { selectedIds: selectedCalendarIds, activeId: activeCalendarId }
        : storedPrefs;
      const { selectedIds, activeId } = reconcileCalendarSelection(
        cached.value as unknown[],
        prefsSnapshot,
      );
      await hydrateEventsFromCache(requestId, accountKey, selectedIds);
      if (requestId !== loadRequestId) return;
      await persistCalendarPrefs(accountKey, selectedIds);
    }

    try {
      const list = await loadPaginatedCalendarResource(
        requestId,
        'Calendars',
        {},
        CALENDARS_PAGE_SIZE,
        getCalendarsList,
      );
      if (!list || requestId !== loadRequestId) return;
      const finalList = Array.isArray(list) ? (list as unknown[]) : [];
      calendars = finalList;
      await db.meta.put({ key: calendarsCacheKey, value: finalList, updatedAt: Date.now() });
      if (requestId !== loadRequestId) return;
      if (!finalList.length) {
        // No calendars exist on the server — clear any stale selection and mounted
        // calendar instance so the empty state renders immediately without a refresh.
        selectedCalendarIds = [];
        activeCalendarId = '';
        events = [];
        allEvents = [];
        eventsScope = 'none';
        eventsScopeCalendarId = '';
        calendarInstance = null;
        lastEventSignature = null;
        calendarCreated = false;
        filterMenuOpen = false;
        await persistCalendarPrefs(accountKey, []);
        return;
      }
      const prefsSnapshot = selectedCalendarIds.length
        ? { selectedIds: selectedCalendarIds, activeId: activeCalendarId }
        : storedPrefs;
      const { selectedIds, activeId } = reconcileCalendarSelection(finalList, prefsSnapshot);
      await persistCalendarPrefs(accountKey, selectedIds);
      if (requestId !== loadRequestId) return;
      await loadEventsForSelection(true);
    } catch (err) {
      const status =
        (err as { status?: number })?.status || (err as { statusCode?: number })?.statusCode;
      const isClientError = typeof status === 'number' && status >= 400 && status < 500;
      if (!isClientError && attempt < 3) {
        return fetchCalendars(attempt + 1);
      }
      if (!isClientError) {
        setError((err as Error)?.message || 'Unable to load calendars.');
      }
      // Don't create a fake 'default' calendar — it doesn't exist on the server
      // and fetching events for it causes 400 errors. Show empty state instead.
      if (!calendars.length) {
        events = [];
        allEvents = [];
      }
    }
  };

  const setCalendarSelection = async (
    nextSelectedIds: string[],
    nextActiveId: string,
    options: { reloadEvents?: boolean } = {},
  ) => {
    const { reloadEvents = true } = options;
    const cleaned = uniqueIds(nextSelectedIds);
    if (!cleaned.length && calendars.length) {
      cleaned.push(getCalendarId(calendars[0]) as string);
    }
    const activeId = nextActiveId || cleaned[0] || '';
    const finalSelected =
      activeId && !cleaned.includes(activeId) ? uniqueIds([...cleaned, activeId]) : cleaned;
    selectedCalendarIds = finalSelected;
    activeCalendarId = activeId;
    await persistCalendarPrefs(getAccountKey(), selectedCalendarIds);
    if (reloadEvents) {
      await loadEventsForSelection();
    }
  };

  const toggleCalendarSelection = async (calendarId: string) => {
    if (!calendarId) return;
    const isSelected = selectedCalendarIds.includes(calendarId);
    const nextSelected = isSelected
      ? selectedCalendarIds.filter((id) => id !== calendarId)
      : [...selectedCalendarIds, calendarId];
    if (!nextSelected.length) {
      toasts?.show?.('Select at least one calendar.', 'error');
      return;
    }
    const nextActive = nextSelected.includes(activeCalendarId) ? activeCalendarId : nextSelected[0];
    if (newEventModal && newEvent.calendarId && !nextSelected.includes(newEvent.calendarId)) {
      newEvent = { ...newEvent, calendarId: nextActive };
    }
    if (editEventModal && editEvent.calendarId && !nextSelected.includes(editEvent.calendarId)) {
      editEvent = { ...editEvent, calendarId: nextActive };
    }
    await setCalendarSelection(nextSelected, nextActive);
  };

  const setActiveCalendarId = async (calendarId: string) => {
    if (!calendarId) return;
    const nextSelected = selectedCalendarIds.includes(calendarId)
      ? selectedCalendarIds
      : [...selectedCalendarIds, calendarId];
    await setCalendarSelection(nextSelected, calendarId, { reloadEvents: nextSelected.length > 1 });
  };

  const formatForScheduleX = (isoDate: string | undefined) => {
    if (!isoDate) return '';
    // Use toDate for robust parsing that handles UTC (Z suffix) and local times correctly
    const d = toDate(isoDate);
    if (!d || isNaN(d.getTime())) return '';
    // Schedule-X expects local time in format "YYYY-MM-DD HH:mm"
    const year = d.getFullYear();
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');
    const hours = String(d.getHours()).padStart(2, '0');
    const minutes = String(d.getMinutes()).padStart(2, '0');
    // Using space format as per Schedule-X documentation
    return `${year}-${month}-${day} ${hours}:${minutes}`;
  };

  // Map an event to Schedule-X start/end. All-day events are emitted as
  // date-only strings ("YYYY-MM-DD") which Schedule-X renders as an all-day bar
  // (end is INCLUSIVE here, unlike iCal's exclusive DTEND). Timed events keep
  // the "YYYY-MM-DD HH:mm" form. Without this, an all-day event came through as
  // a midnight→next-midnight timed block that visually spanned two days.
  const formatRangeForScheduleX = (e: Record<string, unknown>) => {
    if (e.allDay === true) {
      const isRecurringInstance = Boolean(e.recurrenceOccurrence);
      // Prefer the literal calendar dates parsed from the ICS (no tz day-shift).
      // Recurring instances carry the master's dates, so derive per-occurrence.
      let startDate = isRecurringInstance ? '' : String(e.allDayStart || '');
      let endDate = isRecurringInstance ? '' : String(e.allDayEnd || '');
      if (!startDate) {
        const sd = toDate((e.start || e.startDate || e.start_time) as string);
        if (sd && !isNaN(sd.getTime())) startDate = localDateOf(sd);
      }
      if (!endDate) {
        const ed = toDate((e.end || e.endDate || e.end_time) as string);
        if (ed && !isNaN(ed.getTime())) {
          // Step back a minute so an exclusive next-midnight end resolves to the
          // inclusive last day; clamp so we never end before we start.
          const inclusive = localDateOf(new Date(ed.getTime() - 60000));
          endDate = inclusive >= startDate ? inclusive : startDate;
        }
      }
      if (startDate) return { start: startDate, end: endDate || startDate };
    }
    return {
      start: formatForScheduleX((e.start || e.startDate || e.start_time) as string),
      end: formatForScheduleX((e.end || e.endDate || e.end_time) as string),
    };
  };

  const buildScheduleCalendars = (list: unknown[]) => {
    const entries: Record<string, unknown> = {};
    (list || []).forEach((cal, index) => {
      const id = getCalendarId(cal);
      if (!id) return;
      const base = resolveCalendarColor(cal, index);
      const lightContainer = blendHex(base, '#ffffff', 0.85);
      const darkContainer = blendHex(base, '#1a1a1a', 0.7);
      entries[id as string] = {
        colorName: id,
        lightColors: {
          main: base,
          container: lightContainer,
          onContainer: '#0f172a',
        },
        darkColors: {
          main: blendHex(base, '#ffffff', 0.35),
          onContainer: '#ffffff',
          container: darkContainer,
        },
      };
    });
    if (!Object.keys(entries).length) {
      entries.default = {
        colorName: 'default',
        lightColors: {
          main: '#1c7ed6',
          container: '#e7f5ff',
          onContainer: '#1864ab',
        },
        darkColors: {
          main: '#74c0fc',
          onContainer: '#ffffff',
          container: '#1e3a8a',
        },
      };
    }
    return entries;
  };

  const visibleEvents = $derived(
    $hideCompletedTodos
      ? events.filter((ev) => !isCompletedTask(ev as Record<string, unknown>))
      : events,
  );

  let currentSection = $state<'calendar' | 'tasks'>('calendar');

  const taskTitleOf = (t: Record<string, unknown>) =>
    String(t.title || t.summary || t.name || '').toLowerCase();
  const taskCreatedAt = (t: Record<string, unknown>) => {
    const raw = (t.created || t.createdAt || t.dtstamp || t.start) as string | undefined;
    const ms = raw ? new Date(raw).getTime() : 0;
    return Number.isFinite(ms) ? ms : 0;
  };
  const taskDueMs = (t: Record<string, unknown>) => {
    const raw = (t.end || t.start) as string | undefined;
    const ms = raw ? new Date(raw).getTime() : NaN;
    return Number.isFinite(ms) ? ms : 0;
  };

  // Recurring VTODOs come in pre-expanded (one row per occurrence in the
  // recurrence window) because the calendar grid wants every instance.
  // The tasks list should show one entry per series, with its date set to
  // the nearest-upcoming occurrence — or the most recent past one when
  // every occurrence is already due. Non-recurring tasks pass through.
  const collapseRecurringTasks = (tasks: Record<string, unknown>[]) => {
    const now = Date.now();
    const picked = new Map<string, Record<string, unknown>>();
    const passthrough: Record<string, unknown>[] = [];
    const pickBetter = (cur: Record<string, unknown>, cand: Record<string, unknown>) => {
      const cMs = taskDueMs(cur);
      const nMs = taskDueMs(cand);
      const cUp = cMs >= now;
      const nUp = nMs >= now;
      if (nUp && !cUp) return cand;
      if (cUp && !nUp) return cur;
      // both upcoming → keep the earlier; both past → keep the later
      if (cUp ? nMs < cMs : nMs > cMs) return cand;
      return cur;
    };
    for (const t of tasks) {
      const masterId = t.recurrenceMasterId as string | undefined;
      if (!masterId) {
        passthrough.push(t);
        continue;
      }
      const cur = picked.get(masterId);
      picked.set(masterId, cur ? pickBetter(cur, t) : t);
    }
    return [...passthrough, ...picked.values()];
  };

  const tasksList = $derived.by(() => {
    const list = events.filter((ev) => {
      const task = ev as Record<string, unknown>;
      if (!isTaskEvent(task)) return false;
      if ($hideCompletedTodos && isCompletedTask(task)) return false;
      return true;
    });
    const collapsed = collapseRecurringTasks(list as Record<string, unknown>[]);
    const sortKey = $tasksSort;
    return collapsed.slice().sort((a, b) => {
      const ar = a as Record<string, unknown>;
      const br = b as Record<string, unknown>;
      const aDone = isCompletedTask(ar) ? 1 : 0;
      const bDone = isCompletedTask(br) ? 1 : 0;
      if (aDone !== bDone) return aDone - bDone;
      if (sortKey === 'title') {
        return taskTitleOf(ar).localeCompare(taskTitleOf(br));
      }
      if (sortKey === 'created') {
        // Newest first
        return taskCreatedAt(br) - taskCreatedAt(ar);
      }
      const aDue = (ar.end || ar.start) as string | undefined;
      const bDue = (br.end || br.start) as string | undefined;
      if (!aDue && !bDue) return 0;
      if (!aDue) return 1;
      if (!bDue) return -1;
      return new Date(aDue).getTime() - new Date(bDue).getTime();
    });
  });

  const TASK_SORT_OPTIONS: { value: TasksSortKey; label: string }[] = [
    { value: 'due', label: 'Due date' },
    { value: 'title', label: 'Title' },
    { value: 'created', label: 'Created (newest)' },
  ];

  // Minutes before due. 0 = no reminder (no VALARM emitted).
  // Stored as VALARM TRIGGER;RELATED=END:-PT{N}M on the VTODO.
  const REMINDER_OPTIONS: { value: number; label: string }[] = [
    { value: 0, label: 'None' },
    { value: 15, label: '15 min' },
    { value: 60, label: '1 hour' },
    { value: 1440, label: '1 day' },
  ];
  const taskSortLabel = $derived(
    TASK_SORT_OPTIONS.find((o) => o.value === $tasksSort)?.label || 'Due date',
  );
  const handleTaskSortChange = (value: string | undefined) => {
    if (!value) return;
    void setSettingValue('tasks_sort', value);
  };

  const resolveTaskColor = (task: Record<string, unknown>) => {
    const calId = String(task.calendarId || task.calendar_id || '');
    const idx = calendars.findIndex((c) => getCalendarId(c) === calId);
    if (idx === -1) return calendarColorPalette[0];
    return resolveCalendarColor(calendars[idx], idx);
  };

  let lastEventSignature: string | null = null;
  let calendarCreated = false;
  // Refresh local task reminders whenever the events list changes.
  // Schedules setTimeout-driven notifications via notification-bridge
  // for each upcoming task with a VALARM. Future server-side push
  // dispatch would dedupe against this.
  $effect(() => {
    refreshTaskReminders(allEvents as Record<string, unknown>[]);
  });

  // Rebuild the calendar when the "start week on Sunday" preference changes.
  // schedule-x only reads firstDayOfWeek at creation, so we drop the instance
  // and let the create effect below re-run (it depends on calendarInstance).
  let lastWeekStartSunday: boolean | null = null;
  $effect(() => {
    const sunday = $startWeekOnSunday;
    if (lastWeekStartSunday === null) {
      lastWeekStartSunday = sunday;
      return;
    }
    if (sunday !== lastWeekStartSunday) {
      lastWeekStartSunday = sunday;
      calendarCreated = false;
      lastEventSignature = null;
      calendarInstance = null;
    }
  });

  $effect(() => {
    if (isActive && visibleEvents && calendars.length) {
      const mapped = visibleEvents
        .map((ev) => {
          const e = ev as Record<string, unknown>;
          const range = formatRangeForScheduleX(e);
          return {
            id: e.id || e.uid,
            title: e.title || e.summary || e.name || 'Event',
            start: range.start,
            end: range.end,
            calendarId: e.calendarId || e.calendar_id || 'default',
          };
        })
        .filter((ev) => ev.start && ev.end);
      const safeEvents = mapped.length ? mapped : [];
      // Fingerprint the full mapped payload rather than just the count. Both
      // create and edit patch allEvents in place: create swaps the temp id for
      // the server id, and edits merge title/time/calendar onto the existing
      // item. The array length never changes in either case, so a length-only
      // check skipped the push into schedule-x and left its internal list
      // stale. The rendered element kept the old id, so onEventClick resolved
      // to nothing and the edit modal silently failed to open for a new event,
      // and an edited title kept rendering its old value, until a reload
      // rebuilt the calendar. Stringify so every field schedule-x renders is
      // covered.
      const signature = JSON.stringify(safeEvents);
      if (signature === lastEventSignature && calendarCreated) return;
      lastEventSignature = signature;

      if (calendarInstance?.events) {
        try {
          calendarInstance.events.set(safeEvents);
        } catch (err) {
          console.warn('Failed to update calendar events:', err);
          // Let the next run retry rather than recording a failed push as
          // applied, otherwise an unchanged payload early-returns forever.
          lastEventSignature = null;
        }
      } else {
        ensureSafeMutationObserver();
        const isDark = calendarIsDark;
        // Schedule-X renders synchronously and throws on a malformed locale
        // (and potentially other invariants). An uncaught throw here reaches
        // the global error handler and blanks the entire app behind the fatal
        // overlay, so isolate it: a calendar that fails to build should not
        // take down the rest of webmail.
        try {
          calendarInstance = createCalendar({
            locale: i18n.getShortFormattingLocale() || 'en-US',
            views: [viewDay, viewWeek, viewMonthGrid],
            defaultView: resolveDefaultView(),
            // 0 = Sunday, 1 = Monday (schedule-x WeekDay). Read non-reactively;
            // the effect below rebuilds the calendar when this setting changes.
            firstDayOfWeek: get(startWeekOnSunday) ? 0 : 1,
            events: safeEvents,
            isDark,
            defaultCalendarId: resolveActiveCalendarId() || 'default',
            calendars: buildScheduleCalendars(calendars),
            callbacks: {
              onClickDate: (date: string) => {
                openNewEvent(date);
              },
              onClickDateTime: (dateTime: string) => {
                openNewEvent(dateTime);
              },
              onEventClick: (ev: unknown) => openEditEvent(ev),
            },
          });
          calendarCreated = true;
        } catch (err) {
          console.error('Failed to create calendar:', err);
          calendarInstance = null;
        }
      }
    }
  });

  const load = async (force = false) => {
    if (loading) return;
    const requestId = loadRequestId;
    loading = true;
    error = '';
    try {
      if (!calendarsLoaded || force) {
        // First mount (or explicit force): fetchCalendars internally calls
        // loadEventsForSelection(true), so events come along for the ride.
        await fetchCalendars();
        if (requestId !== loadRequestId) return;
        calendarsLoaded = true;
      } else {
        // Subsequent load() (tab switch / route change re-activation):
        // calendars are already in memory so fetchCalendars is skipped, but
        // events created in another client/web UI since first mount would
        // otherwise stay invisible until the cache TTL expires. Force a
        // fresh event fetch for the active selection.
        await loadEventsForSelection(true);
        if (requestId !== loadRequestId) return;
      }
    } catch (err) {
      if (requestId !== loadRequestId) return;
      setError((err as Error)?.message || 'Unable to load calendar.');
    } finally {
      if (requestId === loadRequestId) {
        loading = false;
      }
    }
  };

  const ensureEndAfterStart = (
    date: string,
    startTime: string,
    startMeridiem: string,
    endTime: string,
    endMeridiem: string,
    endDate?: string,
  ) => {
    // Parse date components explicitly to avoid timezone parsing issues.
    // Multi-day support: endDate may differ from start date. Falls back to
    // the start date when omitted so single-day callers stay unchanged.
    const [sYear, sMonth, sDay] = date.split('-').map(Number);
    const finalEndDate = endDate && endDate.length ? endDate : date;
    const [eYear, eMonth, eDay] = finalEndDate.split('-').map(Number);
    const startTime24 = to24Hour(startTime, startMeridiem);
    const endTime24 = to24Hour(endTime, endMeridiem);
    if (!startTime24 || !endTime24) return null;

    const [startHour, startMin] = startTime24.split(':').map(Number);
    const [endHour, endMin] = endTime24.split(':').map(Number);

    // Create dates using local time components explicitly
    const start = new Date(sYear, sMonth - 1, sDay, startHour, startMin, 0, 0);
    const end = new Date(eYear, eMonth - 1, eDay, endHour, endMin, 0, 0);

    if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) return null;
    if (end <= start) return null;
    return { start, end };
  };

  const openNewEvent = (dateInput: unknown, opts: { componentType?: 'VEVENT' | 'VTODO' } = {}) => {
    const calendar = ensureEventCalendarSelected();
    if (!calendar) return;

    const dateObj = toDate(dateInput) || new Date();
    const roundedStart = roundTime(dateObj, 30);
    const startLocal = formatDateTimeLocal(roundedStart);
    const datePart = startLocal.split('T')[0];
    const timePart = startLocal.split('T')[1];

    const endDate = new Date(roundedStart.getTime() + 60 * 60000);
    const endTimePart = formatDateTimeLocal(endDate).split('T')[1];
    const startSplit = to12Hour(timePart);
    const endSplit = to12Hour(endTimePart);
    const componentType = opts.componentType || 'VEVENT';

    newEvent = {
      calendarId: getCalendarId(calendar),
      title: '',
      date: datePart,
      endDate: datePart,
      startTime: startSplit.time,
      startMeridiem: startSplit.meridiem,
      endTime: endSplit.time,
      endMeridiem: endSplit.meridiem,
      allDay: false,
      description: '',
      location: '',
      url: '',
      timezone: getDefaultTimezone(),
      attendees: '',
      notify: 0,
      componentType,
      hasDate: componentType !== 'VTODO',
      hasTime: componentType !== 'VTODO',
      recurrence: { ...DEFAULT_RECURRENCE },
      _editingRawDescription: false,
    };
    optionalFieldsExpanded = false;
    modalDirty = false;
    savingEvent = false;
    newEventModal = true;
    showNewStartPicker = false;
    showNewEndPicker = false;
    optionalFieldsExpanded = false;
    modalDirty = false;
    lastDurationMinutes = 60;
    titleError = '';
    announceModal(componentType === 'VTODO' ? 'New task dialog opened' : 'New event dialog opened');
    focusTitleInput();
  };

  const openNewTask = (dateInput: unknown) => openNewEvent(dateInput, { componentType: 'VTODO' });

  const handleTitleBlur = () => {
    applyParsedTitle();
  };

  const handleStartTimeChange = () => {
    if (!newEvent.startTime) return;

    const newEnd = addMinutesToTime(
      newEvent.startTime,
      newEvent.startMeridiem,
      lastDurationMinutes || 60,
    );
    newEvent.endTime = newEnd.time;
    newEvent.endMeridiem = newEnd.meridiem;
    modalDirty = true;
  };

  const handleEndTimeChange = () => {
    if (!newEvent.startTime || !newEvent.endTime) return;

    if (
      !isEndAfterStart(
        newEvent.startTime,
        newEvent.startMeridiem,
        newEvent.endTime,
        newEvent.endMeridiem,
      )
    ) {
      const newEnd = addMinutesToTime(
        newEvent.startTime,
        newEvent.startMeridiem,
        lastDurationMinutes || 60,
      );
      newEvent.endTime = newEnd.time;
      newEvent.endMeridiem = newEnd.meridiem;
    } else {
      lastDurationMinutes = getDurationMinutes(
        newEvent.startTime,
        newEvent.startMeridiem,
        newEvent.endTime,
        newEvent.endMeridiem,
      );
    }
    modalDirty = true;
  };

  const isNewEventValid = $derived.by(() => {
    const hasTitle = !!newEvent.title?.trim();
    if (newEvent.componentType === 'VTODO') return hasTitle;
    const hasDate = !!newEvent.date;
    const hasTimes = newEvent.allDay || (!!newEvent.startTime && !!newEvent.endTime);
    return hasTitle && hasDate && hasTimes;
  });

  // Editing a recurring instance routes through the "this / this and
  // following / all events" prompt at save time (see promptRecurrenceEdit).
  const isEditingRecurringEvent = $derived(
    String(editEvent.id || '').includes('::') ||
      Boolean((editEvent as Record<string, unknown>).recurrenceMasterId),
  );
  // "Custom" rules from other clients (BYSETPOS, ordinal weekdays, etc.)
  // are read-only — the chip composer can't represent them, so we keep
  // them disabled to avoid clobbering on save.
  const isCustomRecurrence = $derived(editEvent.recurrence?.mode === 'custom');
  const editRecurrenceText = $derived.by(() => {
    if (!editEventModal || !isEditingRecurringEvent) return '';
    const masterId = String(editEvent.id || '').split('::')[0];
    const master = (allEvents || []).find(
      (ev) =>
        String((ev as Record<string, unknown>).id || (ev as Record<string, unknown>).uid || '') ===
        masterId,
    );
    return master ? getRecurrenceText(master as Record<string, unknown>) : '';
  });

  // Live preview of the user's recurrence selection for the new-event
  // modal. Editor variant uses the editEvent state.
  const newRecurrenceDescription = $derived.by(() => {
    if (!newEvent.recurrence || newEvent.recurrence.mode === 'none') return '';
    const dt =
      newEvent.date && newEvent.startTime
        ? new Date(`${newEvent.date}T00:00:00`)
        : newEvent.date
          ? new Date(`${newEvent.date}T00:00:00`)
          : null;
    return describeRecurrenceSpec(newEvent.recurrence, dt);
  });
  const editRecurrenceDescription = $derived.by(() => {
    if (!editEvent.recurrence || editEvent.recurrence.mode === 'none') return '';
    const dt = editEvent.date ? new Date(`${editEvent.date}T00:00:00`) : null;
    return describeRecurrenceSpec(editEvent.recurrence, dt);
  });

  // Chip set covering ~95% of real recurring events. A future "Custom…"
  // dialog (BYDAY checkboxes, intervals, end conditions) would extend
  // this without changing the existing options.
  const RECURRENCE_OPTIONS: {
    value: 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly';
    label: string;
  }[] = [
    { value: 'none', label: "Doesn't repeat" },
    { value: 'daily', label: 'Daily' },
    { value: 'weekly', label: 'Weekly' },
    { value: 'monthly', label: 'Monthly' },
    { value: 'yearly', label: 'Yearly' },
  ];

  // When the user clicks Save or Delete on an expanded recurring
  // instance, surface the 3-button choice (this / this and following /
  // all). "This and following" is wired but disabled — it requires two
  // atomic PUTs (truncate master + create new series). See
  // addExdateToMaster / addOverrideToMaster in utils/recurrence.ts for
  // the supporting ICS mutation helpers.
  let recurrenceEditPrompt = $state<{
    open: boolean;
    action: 'save' | 'delete' | null;
  }>({ open: false, action: null });

  const promptRecurrenceEdit = (action: 'save' | 'delete') => {
    if (!isEditingRecurringEvent || isCustomRecurrence) {
      // Non-recurring or custom rule — go straight through the normal
      // direct save/delete path (custom rules are read-only via gating elsewhere).
      if (action === 'save') updateEvent();
      else showDeleteConfirm = true;
      return;
    }
    recurrenceEditPrompt = { open: true, action };
  };

  // The End date input does double duty:
  //   - recurrence.mode === 'none'   → event's own end date (multi-day span)
  //   - recurrence.mode in {daily,…} → UNTIL anchor (recurrence stops here)
  // This helper keeps the recurrence spec's `until/ends` fields in sync
  // with whatever's typed into the End date input. Called from the chip
  // click handlers and the End date input's onchange so the live preview
  // and saved RRULE always agree.
  // Shared by the create and edit modals' Start date inputs. Writes the value
  // the DOM actually committed: Svelte's bind:value only syncs on the input
  // event, which Android's native date picker dialog does not reliably fire.
  const applyStartDateChange = (
    target: { date: string; endDate: string; recurrence: RecurrenceSpec },
    nextDate: string,
  ) => {
    const previousDate = target.date;
    target.date = nextDate;
    modalDirty = true;
    // While recurring, the End date input means UNTIL (when the series stops)
    // rather than the event's span, so mirroring start into it would move or
    // silently clear the recurrence end the user configured. Leave it alone.
    if (target.recurrence.mode !== 'none' && target.recurrence.mode !== 'custom') return;
    // Mirror start into end while the two are in sync (a single-day event, the
    // common case) so moving start does not silently stretch the event across
    // multiple days. Once the user has deliberately set a later end date, only
    // clamp end forward when start moves past it.
    if (!target.endDate || target.endDate === previousDate || target.endDate < nextDate) {
      target.endDate = nextDate;
    }
  };

  const syncRecurrenceUntilFromEndDate = (target: {
    date: string;
    endDate: string;
    recurrence: RecurrenceSpec;
  }) => {
    if (target.recurrence.mode === 'none' || target.recurrence.mode === 'custom') {
      // Not recurring, or custom rule we're preserving verbatim — leave
      // the spec's ends/until alone (custom rules round-trip rawRrule).
      return;
    }
    if (target.endDate && target.endDate !== target.date) {
      target.recurrence = {
        ...target.recurrence,
        ends: 'on',
        until: target.endDate,
      };
    } else {
      target.recurrence = {
        ...target.recurrence,
        ends: 'never',
        until: '',
      };
    }
  };

  const attemptSaveNewEvent = () => {
    titleError = '';
    if (!newEvent.title?.trim()) {
      titleError = 'Title is required.';
      const input = document.getElementById('event-title') as HTMLInputElement | null;
      input?.focus();
      return;
    }
    if (!isNewEventValid) return;
    saveNewEvent();
  };

  const prefillQuickEvent = (email?: string) => {
    const calendar = ensureEventCalendarSelected();
    if (!calendar) return;

    const startDate = new Date(Date.now() + 10 * 60 * 1000);
    const startLocal = formatDateTimeLocal(startDate);
    const datePart = startLocal.split('T')[0];
    const timePart = startLocal.split('T')[1];
    const endDate = new Date(startDate.getTime() + 60 * 60000);
    const endTimePart = formatDateTimeLocal(endDate).split('T')[1];
    const startSplit = to12Hour(timePart);
    const endSplit = to12Hour(endTimePart);
    newEvent = {
      calendarId: getCalendarId(calendar),
      title: email ? `Meeting with ${email}` : 'New event',
      date: datePart,
      endDate: datePart,
      startTime: startSplit.time,
      startMeridiem: startSplit.meridiem,
      endTime: endSplit.time,
      endMeridiem: endSplit.meridiem,
      allDay: false,
      description: email ? `Follow up with ${email}` : '',
      location: '',
      url: '',
      timezone: getDefaultTimezone(),
      attendees: '',
      notify: 0,
      componentType: 'VEVENT',
      hasDate: true,
      hasTime: true,
      recurrence: { ...DEFAULT_RECURRENCE },
      _editingRawDescription: false,
    };
    newEventModal = true;
    showNewStartPicker = false;
    showNewEndPicker = false;
    lastDurationMinutes = 60;
    titleError = '';
    announceModal('New event dialog opened');
    focusTitleInput();
  };

  // Shared date/time computation for BOTH create (saveNewEvent) and edit
  // (updateEvent). Keeping it in one place is what prevents the two paths from
  // drifting — the original 1h-off + all-day bugs lived in this duplicated logic.
  // Anchors the wall-clock the user typed to the SELECTED timezone (never the
  // device zone) and emits RFC 5545 VALUE=DATE for all-day events.
  type EventDateForm = {
    componentType?: string;
    date: string;
    endDate?: string;
    startTime?: string;
    startMeridiem?: string;
    endTime?: string;
    endMeridiem?: string;
    allDay?: boolean;
    hasDate?: boolean;
    hasTime?: boolean;
    timezone?: string;
    recurrence?: RecurrenceSpec;
  };
  type EventDateValues = {
    range: { start: Date; end: Date } | null;
    startISO: string;
    endISO: string;
    // iCal DTSTART value + DTEND (event) / DUE (todo) value: a literal wall-clock
    // 'YYYYMMDDTHHMMSS' for timed, or a 'YYYYMMDD' date for all-day. Empty => let
    // the generator fall back to converting the ISO instant.
    startValue: string;
    endValue: string;
    allDay: boolean;
    allDayStartDate: string;
    allDayEndDate: string;
  };
  const computeEventDateValues = (
    form: EventDateForm,
  ): { ok: true; values: EventDateValues } | { ok: false; error: string } => {
    const isTodo = form.componentType === 'VTODO';
    const selTz = form.timezone || getDefaultTimezone();
    const blank: EventDateValues = {
      range: null,
      startISO: '',
      endISO: '',
      startValue: '',
      endValue: '',
      allDay: false,
      allDayStartDate: '',
      allDayEndDate: '',
    };

    if (isTodo) {
      // Tasks: date and time are both optional. No date => a floating task.
      if (!(form.hasDate && form.date)) return { ok: true, values: blank };
      if (form.hasTime && form.startTime && form.startMeridiem) {
        const t24 = to24Hour(form.startTime, form.startMeridiem);
        const startValue = buildLocalDateTime(form.date, t24);
        const utc =
          zonedWallClockToUTC(form.date, t24, selTz) ?? new Date(`${form.date}T${t24}:00`);
        const iso = utc.toISOString();
        return {
          ok: true,
          values: {
            ...blank,
            range: { start: utc, end: utc },
            startISO: iso,
            endISO: iso,
            startValue,
            endValue: startValue,
          },
        };
      }
      // Date-only task: keep the existing midnight-local behavior (the generator
      // converts the ISO instant); a wall-clock value isn't meaningful here.
      const dt = new Date(`${form.date}T00:00:00`);
      const iso = dt.toISOString();
      return {
        ok: true,
        values: { ...blank, range: { start: dt, end: dt }, startISO: iso, endISO: iso },
      };
    }

    if (form.allDay) {
      const startDateStr = form.date;
      const endDateStr = form.endDate || form.date;
      // Plain 'YYYY-MM-DD' comparison — lexicographic order is chronological.
      if (endDateStr < startDateStr)
        return { ok: false, error: 'End date must be on or after start date.' };
      const allDayRange = buildAllDayRange(startDateStr, endDateStr);
      if (!allDayRange) return { ok: false, error: 'Invalid date.' };
      const [sY, sM, sD] = startDateStr.split('-').map(Number);
      const [eY, eM, eD] = endDateStr.split('-').map(Number);
      const startDate = new Date(sY, sM - 1, sD, 0, 0, 0, 0);
      const endDate = new Date(eY, eM - 1, eD, 0, 0, 0, 0);
      return {
        ok: true,
        values: {
          range: { start: startDate, end: endDate },
          startISO: startDate.toISOString(),
          endISO: endDate.toISOString(),
          startValue: allDayRange.dtstart,
          endValue: allDayRange.dtend,
          allDay: true,
          allDayStartDate: startDateStr,
          allDayEndDate: endDateStr,
        },
      };
    }

    // Timed event.
    const { date, endDate, startTime, startMeridiem, endTime, endMeridiem } = form;
    if (!date || !startTime || !endTime || !startMeridiem || !endMeridiem) {
      return { ok: false, error: 'Start and end times are required.' };
    }
    // Belt and suspenders: simple recurring events are single-day per occurrence
    // even if an End date was somehow set (that input means UNTIL for them).
    const recurringSingleDay =
      !!form.recurrence && form.recurrence.mode !== 'none' && form.recurrence.mode !== 'custom';
    const endDateForEvent = recurringSingleDay ? date : endDate || date;
    const range = ensureEndAfterStart(
      date,
      startTime,
      startMeridiem,
      endTime,
      endMeridiem,
      endDateForEvent,
    );
    if (!range) return { ok: false, error: 'End must be after start.' };
    const start24 = to24Hour(startTime, startMeridiem);
    const end24 = to24Hour(endTime, endMeridiem);
    const startUtc = zonedWallClockToUTC(date, start24, selTz);
    const endUtc = zonedWallClockToUTC(endDateForEvent, end24, selTz);
    return {
      ok: true,
      values: {
        range,
        startISO: startUtc ? startUtc.toISOString() : range.start.toISOString(),
        endISO: endUtc ? endUtc.toISOString() : range.end.toISOString(),
        startValue: buildLocalDateTime(date, start24),
        endValue: buildLocalDateTime(endDateForEvent, end24),
        allDay: false,
        allDayStartDate: '',
        allDayEndDate: '',
      },
    };
  };

  const saveNewEvent = async () => {
    const isTodo = newEvent.componentType === 'VTODO';
    const title = newEvent.title?.trim() || (isTodo ? 'Task' : 'Event');

    const calendarId = newEvent.calendarId || resolveActiveCalendarId();
    const calendar = getCalendarById(calendarId);
    if (!calendarId || !calendar) {
      const message = getNoCalendarMessage();
      newEventModal = false;
      modalDirty = false;
      setError(message);
      toasts?.show?.(message, 'error');
      return;
    }

    if (!isTodo && !newEvent.date) {
      setError('Date is required.');
      return;
    }

    savingEvent = true;
    const resolvedCalendarId = getCalendarId(calendar) as string;

    const dateResult = computeEventDateValues(newEvent);
    if (!dateResult.ok) {
      setError(dateResult.error);
      savingEvent = false;
      return;
    }
    const {
      range,
      startISO,
      endISO,
      startValue: icalStartValue,
      endValue: icalEndValue,
      allDayStartDate,
      allDayEndDate,
    } = dateResult.values;

    const { description, location, url, timezone, attendees, notify, allDay } = newEvent;

    let icalData: string;
    try {
      icalData = isTodo
        ? generateICalTodo({
            summary: title,
            description: description || '',
            location: location || '',
            url: url || '',
            timezone: timezone || getDefaultTimezone(),
            start: startISO,
            due: endISO,
            startValue: icalStartValue,
            dueValue: icalEndValue,
            status: 'NEEDS-ACTION',
            reminder: Number(notify) || 0,
          })
        : generateICalEvent({
            summary: title,
            description: description || '',
            location: location || '',
            url: url || '',
            timezone: timezone || getDefaultTimezone(),
            attendees: attendees || '',
            start: startISO,
            end: endISO,
            reminder: Number(notify) || 0,
            recurrence: newEvent.recurrence,
            allDay: newEvent.allDay,
            startValue: icalStartValue,
            endValue: icalEndValue,
          });
    } catch (err) {
      setError(
        (err as Error)?.message || (isTodo ? 'Unable to create task.' : 'Unable to create event.'),
      );
      savingEvent = false;
      return;
    }

    // Insert a provisional copy locally and close the modal right away. The
    // POST below swaps in the server id once it confirms. On failure the
    // copy is removed and the modal reopens with the draft intact.
    const tempId =
      typeof crypto !== 'undefined' && crypto.randomUUID
        ? `temp-${crypto.randomUUID()}`
        : `temp-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
    const optimisticEvent = {
      id: tempId,
      title,
      start: startISO,
      end: endISO,
      calendarId: resolvedCalendarId,
      description,
      location,
      url,
      timezone,
      attendees,
      notify: Number(notify) || 0,
      componentType: newEvent.componentType,
      ...(isTodo ? { status: 'NEEDS-ACTION', percentComplete: 0, completedAt: '' } : {}),
      ...(newEvent.allDay
        ? { allDay: true, allDayStart: allDayStartDate, allDayEnd: allDayEndDate }
        : {}),
      raw: null,
    };
    const draft = { ...newEvent };
    const token = ++pendingMutationToken;
    pendingEventMutations.set(tempId, { token, patch: optimisticEvent, insert: true });
    allEvents = [...allEvents, optimisticEvent];
    applySelectedEvents();
    newEventModal = false;
    modalDirty = false;
    setError('');
    setSuccess(isTodo ? 'Task created successfully' : 'Event created successfully');
    persistEventsCache(resolvedCalendarId);
    savingEvent = false;

    const payload = { calendar_id: resolvedCalendarId, ical: icalData };
    let created: Record<string, unknown>;
    try {
      created = (await Remote.request('CalendarEventCreate', payload, {
        method: 'POST',
      })) as Record<string, unknown>;
    } catch (err) {
      if (pendingEventMutations.get(tempId)?.token === token) {
        pendingEventMutations.delete(tempId);
        allEvents = allEvents.filter((ev) => (ev as Record<string, unknown>).id !== tempId);
        applySelectedEvents();
        persistEventsCache(resolvedCalendarId);
      }
      // Bring the draft back so nothing the user typed is lost.
      newEvent = draft;
      modalDirty = true;
      newEventModal = true;
      if (!isDemoBlockedError(err)) {
        setError(
          (err as Error)?.message ||
            (isTodo ? 'Unable to create task.' : 'Unable to create event.'),
        );
      }
      return;
    }

    // Swap the provisional id for the server one.
    const realId = (created?.id || created?.uid || created?.event_id || tempId) as string;
    if (pendingEventMutations.get(tempId)?.token === token) {
      pendingEventMutations.delete(tempId);
    }
    allEvents = allEvents.map((ev) =>
      (ev as Record<string, unknown>).id === tempId
        ? {
            ...(ev as Record<string, unknown>),
            id: realId,
            raw: created ? JSON.parse(JSON.stringify(created)) : null,
          }
        : ev,
    );
    applySelectedEvents();
    if (editEvent.id === tempId) {
      editEvent = { ...editEvent, id: realId };
    }
    persistEventsCache(resolvedCalendarId);

    // Queue invites in background (events only — tasks have no attendees and may have no range)
    if (range && attendees && attendees.trim()) {
      queueEventInvites({
        title,
        start: range.start,
        end: range.end,
        allDay,
        description,
        location,
        url,
        timezone,
        attendees,
        uid: (created?.uid || created?.id || created?.event_id || realId) as string,
      })
        .then((inviteResult) => {
          if (inviteResult?.queued) {
            const count = inviteResult.queued;
            toasts?.show?.(
              `Invite${count === 1 ? '' : 's'} queued for ${count} attendee${count === 1 ? '' : 's'}.`,
              'success',
            );
          }
        })
        .catch((inviteErr) => {
          console.error('[Calendar] Failed to queue invites:', inviteErr);
          toasts?.show?.(
            'Failed to queue invites. You can export the event and share the .ics.',
            'warning',
          );
        });
    }
  };

  const openEditEvent = (calendarEvent: unknown) => {
    const eventId = (calendarEvent as Record<string, unknown>)?.id;
    const fullEvent = findCalendarItemById(String(eventId || ''));
    if (!fullEvent) return;
    const startDate = new Date(fullEvent.start as string);
    const endDate = new Date(fullEvent.end as string);
    const startLocal = formatDateTimeLocal(startDate);
    const endLocal = formatDateTimeLocal(endDate);
    const datePart = startLocal.split('T')[0];
    // Multi-day: capture the end's date separately. For single-day events
    // endDatePart === datePart and the UI behaves as before.
    let endDatePart = endLocal.split('T')[0] || datePart;
    const startSplit = to12Hour(startLocal.split('T')[1]);
    const endSplit = to12Hour(endLocal.split('T')[1]);
    const componentType = firstNonEmptyString(fullEvent.componentType, 'VEVENT').toUpperCase();
    const isTodo = componentType === 'VTODO';
    // Preserve all-day events on edit (mapCalendarEvents flags them from the
    // ICS): without this the modal reset allDay to false and saved them back as
    // a timed midnight→23:59 block.
    const isAllDay = !isTodo && (fullEvent as Record<string, unknown>).allDay === true;
    const hasValidDate = !!datePart && !Number.isNaN(startDate.getTime());
    const editHasDate = isTodo ? hasValidDate : true;
    const startHHMM = startLocal.split('T')[1] || '';
    const editHasTime = isTodo ? hasValidDate && startHHMM !== '00:00' : true;
    // Resolve recurrence from the master event (instances inherit RRULE
    // from the master, not from themselves).
    const masterId = String(fullEvent.id || '').includes('::')
      ? String(fullEvent.id).split('::')[0]
      : String(fullEvent.id || '');
    const recurrenceMaster =
      (allEvents || []).find(
        (ev) =>
          String(
            (ev as Record<string, unknown>).id || (ev as Record<string, unknown>).uid || '',
          ) === masterId,
      ) || fullEvent;
    const editRecurrenceSpec = parseRrule(recurrenceMaster as Record<string, unknown>);
    // For recurring events, the End date input represents UNTIL (when
    // the series stops), not the per-occurrence end date. Surface the
    // spec's UNTIL so the user sees what they originally configured.
    if (
      editRecurrenceSpec.mode !== 'none' &&
      editRecurrenceSpec.mode !== 'custom' &&
      editRecurrenceSpec.ends === 'on' &&
      editRecurrenceSpec.until
    ) {
      endDatePart = editRecurrenceSpec.until;
    } else if (editRecurrenceSpec.mode !== 'none' && editRecurrenceSpec.mode !== 'custom') {
      // Recurring forever — clear the End date input so user knows.
      endDatePart = '';
    }
    editEvent = {
      id: fullEvent.id as string,
      calendarId: (fullEvent.calendarId ||
        fullEvent.calendar_id ||
        resolveActiveCalendarId()) as string,
      title: (fullEvent.title as string) || '',
      date: isAllDay && fullEvent.allDayStart ? (fullEvent.allDayStart as string) : datePart,
      endDate:
        isAllDay && fullEvent.allDayEnd && editRecurrenceSpec.mode === 'none'
          ? (fullEvent.allDayEnd as string)
          : endDatePart,
      startTime: startSplit.time,
      startMeridiem: startSplit.meridiem,
      endTime: endSplit.time,
      endMeridiem: endSplit.meridiem,
      allDay: isAllDay,
      description: (fullEvent.description as string) || '',
      location: (fullEvent.location as string) || '',
      url: (fullEvent.url as string) || '',
      timezone: (fullEvent.timezone as string) || getDefaultTimezone(),
      attendees: (fullEvent.attendees as string) || '',
      notify:
        (fullEvent.notify as number) ||
        parseTaskReminderMinutes(
          (fullEvent.raw as Record<string, unknown> | undefined)?.ical ||
            (fullEvent as Record<string, unknown>).ical,
          isTodo ? 'VTODO' : 'VEVENT',
        ) ||
        0,
      componentType,
      status: firstNonEmptyString(fullEvent.status),
      completedAt: firstNonEmptyString(fullEvent.completedAt),
      percentComplete: Number(fullEvent.percentComplete || 0),
      hasDate: editHasDate,
      hasTime: editHasTime,
      // Read RRULE back from the ICS so the composer chips preselect
      // correctly. Resolves through the master if this is an expanded
      // instance (the master holds the RRULE, not the instance).
      recurrence: editRecurrenceSpec,
      _editingRawDescription: false,
    };
    optionalFieldsExpanded = !!(
      fullEvent.location ||
      fullEvent.url ||
      fullEvent.timezone ||
      fullEvent.attendees
    );
    editEventModal = true;
    showDeleteConfirm = false;
    showEditStartPicker = false;
    showEditEndPicker = false;
  };

  const updateEvent = async () => {
    const isTodo = editEvent.componentType === 'VTODO';
    const {
      id,
      calendarId,
      title,
      date,
      endDate,
      startTime,
      startMeridiem,
      endTime,
      endMeridiem,
      description,
      location,
      url,
      timezone,
      attendees,
      notify,
      status,
      completedAt,
      percentComplete,
    } = editEvent;
    if (!title) {
      setError(isTodo ? 'Title is required.' : 'Title, date, and times are required.');
      return;
    }
    const dateResult = computeEventDateValues(editEvent);
    if (!dateResult.ok) {
      setError(dateResult.error);
      return;
    }
    const {
      startISO,
      endISO,
      startValue: icalStartValue,
      endValue: icalEndValue,
      allDay: allDayResolved,
      allDayStartDate,
      allDayEndDate,
    } = dateResult.values;
    const previousEvent = allEvents.find(
      (ev) => ((ev as Record<string, unknown>).id || (ev as Record<string, unknown>).uid) === id,
    ) as Record<string, unknown> | undefined;
    const previousCalendarId = (previousEvent?.calendarId as string) || '';

    let icalData: string;
    try {
      icalData = isTodo
        ? generateICalTodo({
            summary: title,
            description: description || '',
            location: location || '',
            url: url || '',
            timezone: timezone || getDefaultTimezone(),
            start: startISO,
            due: endISO,
            startValue: icalStartValue,
            dueValue: icalEndValue,
            uid: id,
            status: status || 'NEEDS-ACTION',
            completedAt: completedAt || '',
            percentComplete: Number(percentComplete) || 0,
            reminder: Number(notify) || 0,
          })
        : generateICalEvent({
            summary: title,
            description: description || '',
            location: location || '',
            url: url || '',
            timezone: timezone || getDefaultTimezone(),
            attendees: attendees || '',
            start: startISO,
            end: endISO,
            // Saving an expanded instance ('master::occISO' synthetic id)
            // updates the master — strip the occurrence suffix so the
            // UID and PUT path target the actual stored event.
            uid: id.includes('::') ? id.split('::')[0] : id,
            reminder: Number(notify) || 0,
            recurrence: editEvent.recurrence,
            allDay: editEvent.allDay,
            startValue: icalStartValue,
            endValue: icalEndValue,
          });
    } catch (err) {
      setError((err as Error)?.message || 'Unable to update event.');
      return;
    }
    const persistId = id.includes('::') ? id.split('::')[0] : id;

    // Apply the edit locally and close the modal right away. The PUT runs
    // below and restores the previous version if it fails.
    const patch = {
      title,
      start: startISO,
      end: endISO,
      calendarId,
      description,
      location,
      url,
      timezone,
      attendees,
      notify: Number(notify) || 0,
      ...(isTodo
        ? {
            status: status || 'NEEDS-ACTION',
            completedAt: completedAt || '',
            percentComplete: Number(percentComplete) || 0,
          }
        : {}),
      ...(allDayResolved
        ? { allDay: true, allDayStart: allDayStartDate, allDayEnd: allDayEndDate }
        : { allDay: false }),
    };
    const token = ++pendingMutationToken;
    pendingEventMutations.set(id, { token, patch });
    allEvents = allEvents.map((ev) =>
      (ev as Record<string, unknown>).id === id
        ? { ...(ev as Record<string, unknown>), ...patch }
        : ev,
    );
    applySelectedEvents();
    setError('');
    setSuccess(isTodo ? 'Task updated successfully' : 'Event updated successfully');
    editEventModal = false;
    persistEventsCache(calendarId);
    if (previousCalendarId && previousCalendarId !== calendarId) {
      persistEventsCache(previousCalendarId);
    }

    try {
      await serializeEventWrite(persistId, () =>
        Remote.request(
          'CalendarEventUpdate',
          { id: persistId, calendar_id: calendarId, ical: icalData },
          { method: 'PUT', pathOverride: `/v1/calendar-events/${persistId}` },
        ),
      );
      if (pendingEventMutations.get(id)?.token === token) {
        pendingEventMutations.delete(id);
      }
    } catch (err) {
      // Roll back only if no newer change superseded this one. A later
      // write sends the full item state, so its outcome wins either way.
      if (pendingEventMutations.get(id)?.token === token) {
        pendingEventMutations.delete(id);
        if (previousEvent) {
          allEvents = allEvents.map((ev) =>
            (ev as Record<string, unknown>).id === id ? previousEvent : ev,
          );
          applySelectedEvents();
          persistEventsCache(calendarId);
          if (previousCalendarId && previousCalendarId !== calendarId) {
            persistEventsCache(previousCalendarId);
          }
        }
      }
      if (!isDemoBlockedError(err)) {
        setError((err as Error)?.message || 'Unable to update event.');
      }
    }
  };

  // Optimistic changes waiting on server confirmation, keyed by event id.
  // Fetches that land while a write is in flight re-apply this overlay so
  // stale server data cannot undo what the user just did. A null patch
  // means the item is pending delete. The token lets a failed write know
  // whether a newer change already superseded it before rolling back.
  let pendingMutationToken = 0;
  const pendingEventMutations = new Map<
    string,
    { token: number; patch: Record<string, unknown> | null; insert?: boolean }
  >();

  const applyPendingEventMutations = (list: unknown[]): unknown[] => {
    if (!pendingEventMutations.size) return list;
    const seen = new Set<string>();
    const result: unknown[] = [];
    for (const ev of list) {
      const id = (ev as Record<string, unknown>).id as string;
      const entry = pendingEventMutations.get(id);
      if (!entry) {
        result.push(ev);
        continue;
      }
      seen.add(id);
      if (entry.patch === null) continue;
      result.push({ ...(ev as Record<string, unknown>), ...entry.patch });
    }
    // Pending creates are not on the server yet, so a fetched list would
    // silently drop them. Append any the fetch did not return.
    for (const [id, entry] of pendingEventMutations) {
      if (entry.insert && entry.patch && !seen.has(id)) result.push(entry.patch);
    }
    return result;
  };

  // Server writes for the same item run in order. Without this, rapid
  // toggles could race and the older PUT could arrive at the server last.
  const eventWriteChains = new Map<string, Promise<unknown>>();
  const serializeEventWrite = (id: string, run: () => Promise<unknown>): Promise<unknown> => {
    const prev = eventWriteChains.get(id) || Promise.resolve();
    const next = prev.then(run, run);
    const tail = next.catch(() => {});
    eventWriteChains.set(id, tail);
    tail.then(() => {
      if (eventWriteChains.get(id) === tail) eventWriteChains.delete(id);
    });
    return next;
  };

  // Persist in-memory events to the per-calendar cache, plus the all-events
  // cache when that scope is active. Fire and forget; a miss self-heals on
  // the next fetch.
  const persistEventsCache = (calendarId: string) => {
    db.meta
      .put({
        key: getEventsCacheKey(getAccountKey(), calendarId),
        value: sanitizeForWorker(
          allEvents.filter(
            (ev) =>
              ((ev as Record<string, unknown>).calendarId ||
                (ev as Record<string, unknown>).calendar_id) === calendarId,
          ),
        ),
        updatedAt: Date.now(),
      })
      .catch((e: unknown) => console.warn('[Calendar] Failed to cache events:', e));
    if (eventsScope === 'all') {
      db.meta
        .put({
          key: getAllEventsCacheKey(getAccountKey()),
          value: sanitizeForWorker(allEvents),
          updatedAt: Date.now(),
        })
        .catch((e: unknown) => console.warn('[Calendar] Failed to cache all events:', e));
    }
  };

  const setTaskCompletion = async (
    taskOverride: Record<string, unknown> | undefined,
    completed: boolean,
  ) => {
    const task = taskOverride || findCalendarItemById(editEvent.id);
    if (!task || !isTaskEvent(task)) {
      setError('No task selected.');
      return;
    }
    const alreadyDone = isCompletedTask(task);
    if (completed && alreadyDone) return;
    if (!completed && !alreadyDone) return;
    const calendarId = (task.calendarId ||
      task.calendar_id ||
      (taskOverride ? '' : editEvent.calendarId) ||
      resolveActiveCalendarId()) as string;
    const taskId = (task.id as string) || editEvent.id;
    const completedAt = completed ? new Date().toISOString() : '';
    const status = completed ? 'COMPLETED' : 'NEEDS-ACTION';
    const percentComplete = completed ? 100 : 0;

    let icalData: string;
    try {
      icalData = generateICalTodo({
        summary: (task.title as string) || 'Task',
        description: (task.description as string) || '',
        location: (task.location as string) || '',
        url: (task.url as string) || '',
        timezone: (task.timezone as string) || getDefaultTimezone(),
        start: (task.start as string) || '',
        due: (task.end as string) || (task.start as string) || '',
        uid: taskId,
        status,
        completedAt,
        percentComplete,
      });
    } catch (err) {
      setError(
        (err as Error)?.message ||
          (completed ? 'Unable to complete task.' : 'Unable to reopen task.'),
      );
      return;
    }

    // Apply the change locally first so the checkbox responds instantly.
    // The server write runs below and rolls this back if it fails.
    const previous = {
      status: (task.status as string) || '',
      completedAt: (task.completedAt as string) || '',
      percentComplete: Number(task.percentComplete || 0),
    };
    const patch = { status, completedAt, percentComplete };
    const token = ++pendingMutationToken;
    pendingEventMutations.set(taskId, { token, patch });
    allEvents = allEvents.map((ev) =>
      (ev as Record<string, unknown>).id === taskId
        ? { ...(ev as Record<string, unknown>), ...patch }
        : ev,
    );
    applySelectedEvents();
    if (editEvent.id === taskId) {
      editEvent = { ...editEvent, ...patch };
    }
    persistEventsCache(calendarId);
    setError('');
    toasts?.show?.(completed ? 'Task completed' : 'Task reopened', 'success');

    try {
      await serializeEventWrite(taskId, () =>
        Remote.request(
          'CalendarEventUpdate',
          { id: taskId, calendar_id: calendarId, ical: icalData },
          { method: 'PUT', pathOverride: `/v1/calendar-events/${taskId}` },
        ),
      );
      if (pendingEventMutations.get(taskId)?.token === token) {
        pendingEventMutations.delete(taskId);
      }
    } catch (err) {
      // Roll back only if no newer change superseded this one. A later
      // write sends the full item state, so its outcome wins either way.
      if (pendingEventMutations.get(taskId)?.token === token) {
        pendingEventMutations.delete(taskId);
        allEvents = allEvents.map((ev) =>
          (ev as Record<string, unknown>).id === taskId
            ? { ...(ev as Record<string, unknown>), ...previous }
            : ev,
        );
        applySelectedEvents();
        if (editEvent.id === taskId) {
          editEvent = { ...editEvent, ...previous };
        }
        persistEventsCache(calendarId);
      }
      if (!isDemoBlockedError(err)) {
        setError(
          (err as Error)?.message ||
            (completed ? 'Unable to complete task.' : 'Unable to reopen task.'),
        );
      }
    }
  };

  const completeTask = (taskOverride?: Record<string, unknown>) =>
    setTaskCompletion(taskOverride, true);
  const uncompleteTask = (taskOverride?: Record<string, unknown>) =>
    setTaskCompletion(taskOverride, false);

  const deleteEvent = async () => {
    const { id, calendarId } = editEvent;
    if (!id) {
      setError('No event selected.');
      return;
    }
    // The "all events" branch deletes the entire series. Strip the
    // synthetic occurrence suffix so the DELETE targets the master.
    // Per-instance deletes use deleteOccurrenceOnly (writes EXDATE).
    const persistId = id.includes('::') ? id.split('::')[0] : id;

    // Remove locally and close the modal right away. The server delete runs
    // below and restores the item if it fails.
    const removed = allEvents.find((ev) => (ev as Record<string, unknown>).id === persistId);
    const token = ++pendingMutationToken;
    pendingEventMutations.set(persistId, { token, patch: null });
    allEvents = allEvents.filter((ev) => (ev as Record<string, unknown>).id !== persistId);
    applySelectedEvents();
    setError('');
    setSuccess('Event deleted successfully');
    editEventModal = false;
    showDeleteConfirm = false;
    persistEventsCache(calendarId);

    try {
      await serializeEventWrite(persistId, () =>
        Remote.request(
          'CalendarEventDelete',
          { calendar_id: calendarId },
          { method: 'DELETE', pathOverride: `/v1/calendar-events/${persistId}` },
        ),
      );
      if (pendingEventMutations.get(persistId)?.token === token) {
        pendingEventMutations.delete(persistId);
      }
    } catch (err) {
      const httpStatus =
        (err as { status?: number })?.status || (err as { statusCode?: number })?.statusCode;
      const supersededByNewerWrite = pendingEventMutations.get(persistId)?.token !== token;
      if (!supersededByNewerWrite) {
        pendingEventMutations.delete(persistId);
      }
      // Already gone server side counts as a successful delete.
      if (httpStatus === 404) return;
      if (!supersededByNewerWrite && removed) {
        allEvents = [...allEvents, removed];
        applySelectedEvents();
        persistEventsCache(calendarId);
      }
      if (!isDemoBlockedError(err)) {
        setError((err as Error)?.message || 'Unable to delete event.');
      }
    }
  };

  // ── Per-instance recurrence handlers ──────────────────────────────────────

  // Resolve the master event + occurrence date from an editEvent that
  // came from clicking an expanded instance. Returns null when the
  // information isn't recoverable (defensive — shouldn't fire in practice
  // because the prompt is gated on isEditingRecurringEvent).
  const resolveRecurrenceContext = () => {
    const id = String(editEvent.id || '');
    if (!id.includes('::')) return null;
    const [masterId, occISO] = id.split('::');
    const master = (allEvents || []).find(
      (ev) =>
        String((ev as Record<string, unknown>).id || (ev as Record<string, unknown>).uid || '') ===
        masterId,
    ) as Record<string, unknown> | undefined;
    if (!master) return null;
    const occurrence = new Date(occISO);
    if (Number.isNaN(occurrence.getTime())) return null;
    const masterIcal =
      (typeof (master.raw as Record<string, unknown> | undefined)?.ical === 'string' &&
        ((master.raw as Record<string, unknown>).ical as string)) ||
      (typeof master.ical === 'string' && (master.ical as string)) ||
      '';
    return masterIcal
      ? { masterId, occurrence, master, masterIcal, calendarId: editEvent.calendarId }
      : null;
  };

  // "Delete this event" — adds an EXDATE to the master so the occurrence
  // is excluded from future expansions. Uses the same PUT path as
  // updateEvent so the master document is rewritten in place.
  const deleteOccurrenceOnly = async () => {
    const ctx = resolveRecurrenceContext();
    if (!ctx) {
      setError('Could not resolve recurring event context.');
      return;
    }
    let newIcal: string;
    try {
      newIcal = addExdateToMaster(ctx.masterIcal, ctx.occurrence);
    } catch (err) {
      setError((err as Error)?.message || 'Unable to delete occurrence.');
      return;
    }

    // Replace the master in allEvents with the updated ICS so re-expansion
    // sees the new EXDATE, and close the modal right away. The server
    // write runs below and rolls back if it fails.
    const previousRaw = (ctx.master.raw as Record<string, unknown> | undefined) || {};
    const patchedRaw = { ...previousRaw, ical: newIcal };
    const token = ++pendingMutationToken;
    pendingEventMutations.set(ctx.masterId, { token, patch: { raw: patchedRaw } });
    allEvents = (allEvents || []).map((ev) => {
      const e = ev as Record<string, unknown>;
      if (String(e.id || e.uid || '') !== ctx.masterId) return ev;
      return { ...e, raw: patchedRaw };
    });
    applySelectedEvents();
    setError('');
    setSuccess('Occurrence deleted');
    editEventModal = false;
    showDeleteConfirm = false;
    recurrenceEditPrompt = { open: false, action: null };
    persistEventsCache(ctx.calendarId);

    try {
      await serializeEventWrite(ctx.masterId, () =>
        Remote.request(
          'CalendarEventUpdate',
          { id: ctx.masterId, calendar_id: ctx.calendarId, ical: newIcal },
          { method: 'PUT', pathOverride: `/v1/calendar-events/${ctx.masterId}` },
        ),
      );
      if (pendingEventMutations.get(ctx.masterId)?.token === token) {
        pendingEventMutations.delete(ctx.masterId);
      }
    } catch (err) {
      // Roll back only if no newer change superseded this one.
      if (pendingEventMutations.get(ctx.masterId)?.token === token) {
        pendingEventMutations.delete(ctx.masterId);
        allEvents = (allEvents || []).map((ev) => {
          const e = ev as Record<string, unknown>;
          if (String(e.id || e.uid || '') !== ctx.masterId) return ev;
          return { ...e, raw: previousRaw };
        });
        applySelectedEvents();
        persistEventsCache(ctx.calendarId);
      }
      if (!isDemoBlockedError(err)) {
        setError((err as Error)?.message || 'Unable to delete occurrence.');
      }
    }
  };

  // "Edit this event" — appends a RECURRENCE-ID override VEVENT to the
  // master's VCALENDAR carrying the user's edits. The original master's
  // RRULE/DTSTART are untouched, so siblings are unaffected.
  const saveOccurrenceOverride = async () => {
    const ctx = resolveRecurrenceContext();
    if (!ctx) {
      setError('Could not resolve recurring event context.');
      return;
    }
    const isTodo = editEvent.componentType === 'VTODO';
    if (isTodo) {
      // VTODO recurrence overrides aren't yet supported.
      setError('Per-instance edits for tasks coming soon.');
      return;
    }
    if (!editEvent.title?.trim()) {
      setError('Title is required.');
      return;
    }
    if (
      !editEvent.date ||
      !editEvent.startTime ||
      !editEvent.endTime ||
      !editEvent.startMeridiem ||
      !editEvent.endMeridiem
    ) {
      setError('Date and times are required.');
      return;
    }
    const range = ensureEndAfterStart(
      editEvent.date,
      editEvent.startTime,
      editEvent.startMeridiem,
      editEvent.endTime,
      editEvent.endMeridiem,
      // Per-instance overrides are single-day; ignore the End date input
      // (which represents UNTIL for the master, not this occurrence).
      editEvent.date,
    );
    if (!range) {
      setError('End time must be after start time.');
      return;
    }
    try {
      const newIcal = addOverrideToMaster(ctx.masterIcal, ctx.occurrence, {
        summary: editEvent.title.trim(),
        description: editEvent.description || '',
        location: editEvent.location || '',
        url: editEvent.url || '',
        start: range.start,
        end: range.end,
        reminderMinutes: Number(editEvent.notify) || 0,
      });
      await Remote.request(
        'CalendarEventUpdate',
        { id: ctx.masterId, calendar_id: ctx.calendarId, ical: newIcal },
        { method: 'PUT', pathOverride: `/v1/calendar-events/${ctx.masterId}` },
      );
      // Update the master's stored ICS so re-expansion picks up the
      // override.
      allEvents = (allEvents || []).map((ev) => {
        const e = ev as Record<string, unknown>;
        if (String(e.id || e.uid || '') !== ctx.masterId) return ev;
        const raw = (e.raw as Record<string, unknown> | undefined) || {};
        return { ...e, raw: { ...raw, ical: newIcal } };
      });
      applySelectedEvents();
      setError('');
      setSuccess('Occurrence updated');
      editEventModal = false;
      recurrenceEditPrompt = { open: false, action: null };
    } catch (err) {
      if (!isDemoBlockedError(err)) {
        setError((err as Error)?.message || 'Unable to update occurrence.');
      }
    }
  };

  // Route the user's choice from the prompt to the correct handler.
  const applyRecurrenceChoice = (choice: 'this' | 'following' | 'all') => {
    const action = recurrenceEditPrompt.action;
    if (choice === 'following') {
      // "This and following" not yet implemented.
      return;
    }
    if (action === 'save') {
      if (choice === 'this') return saveOccurrenceOverride();
      if (choice === 'all') {
        recurrenceEditPrompt = { open: false, action: null };
        return updateEvent();
      }
    } else if (action === 'delete') {
      if (choice === 'this') return deleteOccurrenceOnly();
      if (choice === 'all') {
        recurrenceEditPrompt = { open: false, action: null };
        showDeleteConfirm = true;
        return;
      }
    }
  };

  const closeModals = (force = false) => {
    if (!force && modalDirty && newEventModal) {
      if (!confirm('Discard changes?')) {
        return;
      }
    }

    newEventModal = false;
    editEventModal = false;
    showDeleteConfirm = false;
    showNewStartPicker = false;
    showNewEndPicker = false;
    showEditStartPicker = false;
    showEditEndPicker = false;
    modalDirty = false;
  };

  const handleModalKeydown = (e: KeyboardEvent) => {
    if (e.key === 'Tab') {
      trapFocus(e, newEventModalRef);
      return;
    }
    if (e.key === 'Escape') {
      e.preventDefault();
      closeModals();
    } else if (
      e.key === 'Enter' &&
      !e.shiftKey &&
      (e.target as HTMLElement).tagName !== 'TEXTAREA'
    ) {
      if ((e.target as HTMLElement).id === 'event-title') {
        e.preventDefault();
        applyParsedTitle();
        attemptSaveNewEvent();
      }
    }
  };

  const autoExpand = (textarea: HTMLTextAreaElement | null) => {
    if (!textarea) return;
    textarea.style.height = 'auto';
    textarea.style.height = textarea.scrollHeight + 'px';
  };

  const closeTimePickers = (event: FocusEvent) => {
    if ((event?.target as HTMLElement)?.closest?.('.time-dropdown')) return;
    showNewStartPicker = false;
    showNewEndPicker = false;
    showEditStartPicker = false;
    showEditEndPicker = false;
  };

  const setNewEventTime = (field: string, value: string) => {
    newEvent = { ...newEvent, [field]: value };
  };

  const setEditEventTime = (field: string, value: string) => {
    editEvent = { ...editEvent, [field]: value };
  };

  const getCurrentScheduleXView = (): string => {
    const app = (calendarInstance as unknown as { $app?: Record<string, unknown> })?.$app;
    const calendarState = app?.calendarState as { view?: { value?: string } } | undefined;
    return calendarState?.view?.value || '';
  };

  const getCurrentScheduleXRangeStart = (): Date | null => {
    const app = (calendarInstance as unknown as { $app?: Record<string, unknown> })?.$app;
    const calendarState = app?.calendarState as
      | { range?: { value?: { start?: string } } }
      | undefined;
    const start = calendarState?.range?.value?.start;
    if (!start) return null;
    const datePart = String(start).split(' ')[0];
    const [y, m, d] = datePart.split('-').map(Number);
    if (!y || !m || !d) return null;
    return new Date(y, m - 1, d);
  };

  const navigateSchedulePeriod = (direction: 'prev' | 'next') => {
    const app = (calendarInstance as unknown as { $app?: Record<string, unknown> })?.$app;
    const calendarState = app?.calendarState as { setRange?: (date: string) => void } | undefined;
    const datePickerState = app?.datePickerState as
      | { selectedDate?: { value: string } }
      | undefined;
    if (!calendarState?.setRange) return;
    const view = getCurrentScheduleXView();
    const anchor = getCurrentScheduleXRangeStart();
    if (!anchor) return;
    const sign = direction === 'next' ? 1 : -1;
    const target = new Date(anchor);
    if (view === 'day') {
      target.setDate(target.getDate() + sign);
    } else if (view === 'month-grid') {
      target.setMonth(target.getMonth() + sign);
    } else {
      target.setDate(target.getDate() + 7 * sign);
    }
    const y = target.getFullYear();
    const m = String(target.getMonth() + 1).padStart(2, '0');
    const d = String(target.getDate()).padStart(2, '0');
    const next = `${y}-${m}-${d}`;
    // Update the date picker's selectedDate so the header dropdown stays in sync;
    // schedule-x's internal effect cascades this to calendarState.setRange.
    if (datePickerState?.selectedDate) {
      datePickerState.selectedDate.value = next;
    } else {
      calendarState.setRange(next);
    }
  };

  let swipeStartX = 0;
  let swipeStartY = 0;
  let swipeActive = false;
  const SWIPE_THRESHOLD_PX = 60;
  const SWIPE_HORIZONTAL_DEADZONE =
    '.sx__time-grid-event, .sx__month-grid-event, .sx__date-grid-event, ' +
    '.time-dropdown, [role="dialog"], [data-dropdown], input, select, button';

  const isCalendarModalOpen = () =>
    newEventModal || editEventModal || showDeleteConfirm || newCalendarModal || deleteCalendarModal;

  const onCalendarTouchStart = (e: TouchEvent) => {
    if (isCalendarModalOpen()) return;
    if (!e.touches || e.touches.length !== 1) return;
    const target = e.target as HTMLElement | null;
    if (target?.closest?.(SWIPE_HORIZONTAL_DEADZONE)) return;
    swipeStartX = e.touches[0].clientX;
    swipeStartY = e.touches[0].clientY;
    swipeActive = false;
  };

  const onCalendarTouchMove = (e: TouchEvent) => {
    if (!e.touches || e.touches.length !== 1 || !swipeStartX) return;
    const dx = e.touches[0].clientX - swipeStartX;
    const dy = e.touches[0].clientY - swipeStartY;
    if (!swipeActive && Math.abs(dx) > 15 && Math.abs(dx) > Math.abs(dy) * 2) {
      swipeActive = true;
    }
  };

  const onCalendarTouchEnd = (e: TouchEvent) => {
    if (!swipeActive || !swipeStartX) {
      swipeStartX = 0;
      swipeStartY = 0;
      swipeActive = false;
      return;
    }
    const touch = e.changedTouches?.[0];
    if (touch) {
      const dx = touch.clientX - swipeStartX;
      if (Math.abs(dx) > SWIPE_THRESHOLD_PX) {
        navigateSchedulePeriod(dx > 0 ? 'prev' : 'next');
      }
    }
    swipeStartX = 0;
    swipeStartY = 0;
    swipeActive = false;
  };

  onMount(() => {
    hasMounted = true;

    const handleResize = () => {
      viewportWidth = window.innerWidth;
    };
    window.addEventListener('resize', handleResize);

    const handleExternalChange = () => {
      if (!hasMounted) return;
      loadEventsForSelection(true).catch(() => {});
    };
    window.addEventListener('webmail:calendar-events-changed', handleExternalChange);

    const mediaQuery =
      typeof window !== 'undefined' && window.matchMedia
        ? window.matchMedia('(prefers-color-scheme: dark)')
        : null;
    const handleThemePreference = (event: MediaQueryListEvent | MediaQueryList) => {
      prefersDark = event?.matches ?? mediaQuery?.matches ?? false;
    };
    if (mediaQuery) {
      handleThemePreference(mediaQuery);
      if (mediaQuery.addEventListener) {
        mediaQuery.addEventListener('change', handleThemePreference);
      }
    }

    // Granular merge for remote (websocket-driven) calendar changes. DELETE
    // is the only one we can do without a server-side payload upgrade — the
    // event id is present in the WS notification, so we just remove it from
    // local state without a full re-fetch. CREATE / UPDATE re-fetch events
    // for the active selection (force=true bypasses the cached event list);
    // load() alone only loads the calendar list, not the events, which is
    // why events created outside the app didn't appear here even though
    // notifications fired for them.
    const applyRemoteChange = (detail: { type?: string; payload?: unknown }) => {
      const t = detail?.type;
      const data = detail?.payload as Record<string, unknown> | undefined;
      if (!t || !data) {
        loadEventsForSelection(true).catch(() => {});
        return;
      }
      if (t === 'calendarEventDeleted') {
        const id = (data.event_id || data.uid || data.id) as string | undefined;
        if (id) {
          allEvents = allEvents.filter((ev) => {
            const e = ev as Record<string, unknown>;
            return e.id !== id && e.uid !== id;
          });
          applySelectedEvents();
          return;
        }
      }
      loadEventsForSelection(true).catch(() => {});
    };

    registerApi?.({
      reload: load,
      prefillQuickEvent,
      applyRemoteChange,
    } as unknown as CalendarApi);
    if (isActive) {
      load();
    }

    return () => {
      window.removeEventListener('resize', handleResize);
      window.removeEventListener('webmail:calendar-events-changed', handleExternalChange);
      if (mediaQuery) {
        if (mediaQuery.removeEventListener) {
          mediaQuery.removeEventListener('change', handleThemePreference);
        }
      }
    };
  });

  let lastThemeSet = '';
  $effect(() => {
    if (calendarInstance) {
      const themeVal = calendarIsDark ? 'dark' : 'light';
      if (themeVal !== lastThemeSet) {
        lastThemeSet = themeVal;
        calendarInstance.setTheme(themeVal);
      }
    }
  });

  let initialLoadTriggered = false;
  $effect(() => {
    if (hasMounted && isActive && !loading && !calendarsLoaded && !initialLoadTriggered) {
      initialLoadTriggered = true;
      load();
    }
  });
</script>

<Tooltip.Provider>
  <div class="calendar-page flex flex-col h-full">
    <div
      class="flex h-14 items-center justify-between gap-3 px-4 shrink-0"
      style="padding-top: env(safe-area-inset-top, 0px); box-sizing: content-box;"
    >
      <div class="flex items-center gap-3 min-w-0">
        <Button
          variant="ghost"
          size="icon"
          onclick={() => navigate?.('/mailbox')}
          aria-label="Back"
        >
          <ChevronLeft class="h-5 w-5" />
        </Button>
        <div class="min-w-0">
          <h1 class="text-lg font-semibold truncate" data-testid="calendar-header">Calendar</h1>
          <span
            class="text-xs text-muted-foreground truncate block"
            title="Events stored privately for this account"
          >
            {activeEmail || getCalendarLabel(activeCalendar())}
          </span>
        </div>
      </div>
      <div class="flex items-center gap-2">
        {#if !isMobile && calendars.length > 0}
          <DropdownMenu.Root bind:open={filterMenuOpen}>
            <DropdownMenu.Trigger>
              {#snippet child({ props })}
                <Button variant="ghost" class="calendar-filter gap-1.5" {...props}>
                  <span>{calendarFilterLabel}</span>
                  <ChevronDown class="h-4 w-4" />
                </Button>
              {/snippet}
            </DropdownMenu.Trigger>
            <DropdownMenu.Content align="end" class="min-w-[260px] max-h-[320px] overflow-y-auto">
              {#each calendars as cal, index}
                {@const calId = getCalendarId(cal)}
                {#if calId}
                  <DropdownMenu.CheckboxItem
                    checked={selectedCalendarIds.includes(calId as string)}
                    onCheckedChange={() => toggleCalendarSelection(calId as string)}
                    closeOnSelect={false}
                    class="pe-1"
                  >
                    <span
                      class="mr-2 h-2.5 w-2.5 shrink-0 rounded-full"
                      style="background: {resolveCalendarColor(cal, index)}"
                    ></span>
                    <span class="truncate flex-1">{getCalendarLabel(cal)}</span>
                    <button
                      type="button"
                      class="ml-2 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
                      aria-label="Delete {getCalendarLabel(cal)}"
                      onpointerdown={(e) => e.stopPropagation()}
                      onclick={(e) => {
                        e.stopPropagation();
                        e.preventDefault();
                        openDeleteCalendarModal(calId as string);
                      }}
                    >
                      <Trash2 class="h-3.5 w-3.5" />
                    </button>
                  </DropdownMenu.CheckboxItem>
                {/if}
              {/each}
            </DropdownMenu.Content>
          </DropdownMenu.Root>
        {/if}
        <DropdownMenu.Root>
          <DropdownMenu.Trigger>
            {#snippet child({ props })}
              <Button variant="ghost" size="icon" aria-label="Calendar menu" {...props}>
                <MoreVertical class="h-5 w-5" />
              </Button>
            {/snippet}
          </DropdownMenu.Trigger>
          <DropdownMenu.Content align="end" class="min-w-[200px]">
            <DropdownMenu.Item
              disabled={!activeCalendar()}
              onclick={() => openNewEvent(new Date())}
            >
              <Plus class="mr-2 h-4 w-4" />
              <span>New Event</span>
            </DropdownMenu.Item>
            {#if isMobile && calendars.length > 0}
              <DropdownMenu.Item onclick={() => (calendarsModalOpen = true)}>
                <CalendarIcon class="mr-2 h-4 w-4" />
                <span>Calendars</span>
              </DropdownMenu.Item>
            {/if}
            <DropdownMenu.Item
              onclick={() => {
                filterMenuOpen = false;
                newCalendarModal = true;
              }}
            >
              <CalendarIcon class="mr-2 h-4 w-4" />
              <span>Create Calendar</span>
            </DropdownMenu.Item>
            <DropdownMenu.Item
              onclick={async () => {
                const files = await pickFiles({ accept: '.ics,text/calendar' }).catch((err) => {
                  // The native macOS picker can return nil (see file-picker.ts).
                  // Surface a toast and do NOT fall through to the HTML <input>
                  // below — on Tauri that can SIGABRT the WKWebView the same way.
                  toasts?.show?.(
                    'Could not open the file picker — a known macOS issue we are working on.',
                    'error',
                  );
                  console.error('[calendar] ICS picker failed', err);
                  return 'picker-error' as const;
                });
                if (files === 'picker-error') return;
                if (files) {
                  importICS(files);
                  return;
                }
                document.getElementById('import-ics-input')?.click();
              }}
            >
              <Import class="mr-2 h-4 w-4" />
              <span>Import Calendar</span>
            </DropdownMenu.Item>
          </DropdownMenu.Content>
        </DropdownMenu.Root>
        <input
          id="import-ics-input"
          type="file"
          accept=".ics,text/calendar"
          onchange={importICS}
          class="hidden"
        />
      </div>
    </div>

    {#if error}
      <Alert.Root variant="destructive" class="mx-4 mt-3">
        <AlertTriangle class="h-4 w-4" />
        <Alert.Description>{error}</Alert.Description>
      </Alert.Root>
    {/if}

    <div class="flex items-center gap-1 border-b border-border px-4 pt-1 shrink-0">
      <button
        type="button"
        class="px-3 py-2 text-sm font-medium border-b-2 transition-colors
          {currentSection === 'calendar'
          ? 'border-primary text-foreground'
          : 'border-transparent text-muted-foreground hover:text-foreground'}"
        onclick={() => (currentSection = 'calendar')}
      >
        Calendar
      </button>
      <button
        type="button"
        class="px-3 py-2 text-sm font-medium border-b-2 transition-colors
          {currentSection === 'tasks'
          ? 'border-primary text-foreground'
          : 'border-transparent text-muted-foreground hover:text-foreground'}"
        onclick={() => (currentSection = 'tasks')}
      >
        Tasks
      </button>
    </div>

    <div class="calendar-content fe-mobile-page-scroll flex-1 flex flex-col p-4 min-h-0">
      {#if loading}
        <div class="flex items-center justify-center h-64 text-muted-foreground">
          Loading calendar...
        </div>
      {:else if currentSection === 'tasks'}
        <div class="flex items-center justify-between gap-2 pb-3 shrink-0">
          <Select.Root
            type="single"
            value={$tasksSort}
            onValueChange={(v) => handleTaskSortChange(v)}
          >
            <Select.Trigger class="h-8 w-[180px] text-xs" aria-label="Sort tasks">
              <span class="truncate">Sort: {taskSortLabel}</span>
            </Select.Trigger>
            <Select.Content>
              {#each TASK_SORT_OPTIONS as opt}
                <Select.Item value={opt.value}>{opt.label}</Select.Item>
              {/each}
            </Select.Content>
          </Select.Root>
          <Button
            size="sm"
            variant="outline"
            onclick={() => openNewTask(new Date())}
            disabled={!activeCalendar()}
          >
            <Plus class="h-4 w-4 mr-1" />
            New Task
          </Button>
        </div>
        <div class="fe-mobile-page-scroll flex-1 overflow-y-auto" data-testid="tasks-list">
          <TasksList
            tasks={tasksList as Record[]}
            onSelect={(task) => openEditEvent(task)}
            onToggleComplete={(task) => setTaskCompletion(task, !isCompletedTask(task))}
            {resolveTaskColor}
            isCompleted={(task) => isCompletedTask(task)}
          />
        </div>
      {:else if calendarInstance}
        <div
          class="sx-wrapper"
          class:is-dark={calendarIsDark}
          data-testid="calendar-ready"
          ontouchstart={onCalendarTouchStart}
          ontouchmove={onCalendarTouchMove}
          ontouchend={onCalendarTouchEnd}
          ontouchcancel={onCalendarTouchEnd}
        >
          <ScheduleXCalendar calendarApp={calendarInstance} />
        </div>
      {:else if calendarsLoaded && !calendars.length}
        <div class="flex flex-col items-center justify-center h-64 text-muted-foreground gap-3">
          <CalendarIcon class="h-10 w-10 opacity-50" />
          <p class="text-sm">No calendars found for this account.</p>
          <p class="text-xs">
            Create a calendar to get started, or import one using a CalDAV client.
          </p>
          <Button
            variant="outline"
            onclick={() => {
              newCalendarModal = true;
            }}
          >
            <Plus class="mr-2 h-4 w-4" />
            Create Calendar
          </Button>
        </div>
      {:else}
        <div class="flex items-center justify-center h-64 text-muted-foreground">
          Initializing calendar...
        </div>
      {/if}
    </div>
    <div
      class="flex items-center gap-2 border-t border-border bg-muted/50 px-4 py-2 text-xs text-muted-foreground shrink-0"
    >
      <Info class="h-3.5 w-3.5 shrink-0" />
      <span>Privacy: Your calendar data is stored privately and never shared.</span>
    </div>
  </div>
  <Dialog.Root bind:open={newEventModal}>
    <Dialog.Content
      class="sm:max-w-[520px] overflow-x-hidden [&>*]:min-w-0"
      onkeydown={handleModalKeydown}
    >
      <div class="sr-only" aria-live="polite" aria-atomic="true">{modalAnnouncement}</div>
      <Dialog.Header>
        <Dialog.Title>{newEvent.componentType === 'VTODO' ? 'New task' : 'New event'}</Dialog.Title>
      </Dialog.Header>
      <div class="space-y-4 py-4" bind:this={newEventModalRef} onfocusin={closeTimePickers}>
        {#if calendars.length > 1}
          <div class="space-y-2">
            <Label>Calendar</Label>
            <Select.Root
              type="single"
              value={newEvent.calendarId}
              onValueChange={(v) => {
                if (v) {
                  newEvent.calendarId = v;
                  modalDirty = true;
                  setActiveCalendarId(v);
                }
              }}
            >
              <Select.Trigger class="w-full">
                {getCalendarLabel(getCalendarById(newEvent.calendarId)) || 'Select calendar'}
              </Select.Trigger>
              <Select.Content>
                {#each calendars as cal}
                  {@const calId = getCalendarId(cal)}
                  {#if calId}
                    <Select.Item value={calId as string}>{getCalendarLabel(cal)}</Select.Item>
                  {/if}
                {/each}
              </Select.Content>
            </Select.Root>
          </div>
        {/if}
        <div class="space-y-2">
          <Label for="event-title">Title</Label>
          <Input
            id="event-title"
            type="text"
            placeholder="e.g., Lunch with Alex tomorrow 12-1"
            bind:value={newEvent.title}
            onblur={handleTitleBlur}
            oninput={() => {
              modalDirty = true;
              if (titleError) titleError = '';
            }}
            autocomplete="off"
            aria-invalid={!!titleError}
          />
          {#if titleError}
            <p class="text-xs text-destructive">{titleError}</p>
          {/if}
        </div>
        {#if newEvent.componentType === 'VTODO'}
          {#if !newEvent.hasDate}
            <button
              type="button"
              class="text-sm font-medium text-muted-foreground hover:text-foreground"
              onclick={() => {
                newEvent.hasDate = true;
                modalDirty = true;
              }}
            >
              + Add due date
            </button>
          {:else}
            <div class="space-y-2">
              <Label for="event-date">Due date</Label>
              <div class="flex items-center gap-2">
                <Input
                  id="event-date"
                  type="date"
                  class="flex-1"
                  bind:value={newEvent.date}
                  onchange={(e) => {
                    newEvent.date = e.currentTarget.value;
                    modalDirty = true;
                  }}
                />
                <button
                  type="button"
                  class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
                  aria-label="Remove due date"
                  onclick={() => {
                    newEvent.hasDate = false;
                    newEvent.hasTime = false;
                    modalDirty = true;
                  }}
                >
                  <X class="h-4 w-4" />
                </button>
              </div>
            </div>
            {#if !newEvent.hasTime}
              <button
                type="button"
                class="text-sm font-medium text-muted-foreground hover:text-foreground"
                onclick={() => {
                  newEvent.hasTime = true;
                  modalDirty = true;
                }}
              >
                + Add time
              </button>
            {/if}
          {/if}
        {:else}
          {#if isForeignZone(newEvent.timezone)}
            <div
              class="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-900 dark:text-amber-200"
            >
              Times shown in your local zone (<span class="font-mono">{viewerZone}</span>). Event
              time zone: <span class="font-mono">{newEvent.timezone}</span>.
            </div>
          {/if}
          <div class="grid grid-cols-2 gap-4">
            <div class="space-y-2">
              <Label for="event-date">Start date</Label>
              <Input
                id="event-date"
                type="date"
                bind:value={newEvent.date}
                onchange={(e) => applyStartDateChange(newEvent, e.currentTarget.value)}
              />
            </div>
            <div class="space-y-2">
              <Label for="event-end-date">
                {newEvent.recurrence.mode !== 'none' && newEvent.recurrence.mode !== 'custom'
                  ? 'Repeat until (optional)'
                  : 'End date'}
              </Label>
              <Input
                id="event-end-date"
                type="date"
                min={newEvent.date}
                bind:value={newEvent.endDate}
                onchange={(e) => {
                  newEvent.endDate = e.currentTarget.value;
                  modalDirty = true;
                  // When recurring, the End date input means UNTIL (the
                  // recurrence stops here). Sync the spec so buildRrule
                  // emits UNTIL, and the live preview reflects it.
                  syncRecurrenceUntilFromEndDate(newEvent);
                }}
              />
            </div>
          </div>
          <label class="flex items-center gap-2 cursor-pointer text-sm">
            <Checkbox
              checked={newEvent.allDay}
              onCheckedChange={(v) => {
                newEvent.allDay = !!v;
                modalDirty = true;
              }}
            />
            <span>All-day</span>
          </label>
        {/if}
        {#if newEvent.componentType === 'VTODO' && newEvent.hasDate && newEvent.hasTime}
          <div class="space-y-2">
            <Label>Time</Label>
            <div class="flex items-center gap-2">
              {#if isMobile}
                <Input
                  type="time"
                  class="flex-1"
                  value={to24Hour(newEvent.startTime, newEvent.startMeridiem)}
                  oninput={(e) => {
                    const v = (e.target as HTMLInputElement).value;
                    const { time, meridiem } = to12Hour(v);
                    newEvent = { ...newEvent, startTime: time, startMeridiem: meridiem };
                    modalDirty = true;
                  }}
                />
              {:else}
                <div class="relative flex-1">
                  <Input
                    type="text"
                    placeholder="12:00"
                    value={newEvent.startTime}
                    onfocus={() => {
                      showNewStartPicker = true;
                      showNewEndPicker = false;
                    }}
                    oninput={(e) => {
                      const val = (e.target as HTMLInputElement).value;
                      setNewEventTime('startTime', val);
                      modalDirty = true;
                    }}
                    onblur={() => {
                      setTimeout(() => {
                        showNewStartPicker = false;
                      }, 150);
                    }}
                  />
                  {#if showNewStartPicker}
                    <div
                      class="time-dropdown absolute top-full left-0 right-0 mt-1 max-h-[220px] overflow-y-auto border border-border bg-popover shadow-lg z-20"
                    >
                      {#each timeOptions as opt}
                        <button
                          type="button"
                          class="w-full px-3 py-2 text-left text-sm hover:bg-accent"
                          onmousedown={(e) => {
                            e.preventDefault();
                            setNewEventTime('startTime', opt.value);
                            showNewStartPicker = false;
                          }}
                        >
                          {opt.display}
                        </button>
                      {/each}
                    </div>
                  {/if}
                </div>
                <Select.Root type="single" bind:value={newEvent.startMeridiem}>
                  <Select.Trigger size="default" class="h-9 w-[70px]">
                    {newEvent.startMeridiem || 'AM'}
                  </Select.Trigger>
                  <Select.Content>
                    <Select.Item value="AM">AM</Select.Item>
                    <Select.Item value="PM">PM</Select.Item>
                  </Select.Content>
                </Select.Root>
              {/if}
              <button
                type="button"
                class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
                aria-label="Remove time"
                onclick={() => {
                  newEvent.hasTime = false;
                  modalDirty = true;
                }}
              >
                <X class="h-4 w-4" />
              </button>
            </div>
          </div>
        {/if}
        {#if newEvent.componentType === 'VTODO' && newEvent.hasDate}
          <div class="space-y-2">
            <Label>Remind me</Label>
            <div class="flex flex-wrap gap-1.5">
              {#each REMINDER_OPTIONS as opt (opt.value)}
                <button
                  type="button"
                  class={`inline-flex h-8 items-center px-3 text-xs rounded-full border transition-colors ${
                    Number(newEvent.notify || 0) === opt.value
                      ? 'bg-primary text-primary-foreground border-primary'
                      : 'bg-background text-muted-foreground border-border hover:bg-accent hover:text-foreground'
                  }`}
                  onclick={() => {
                    newEvent.notify = opt.value;
                    modalDirty = true;
                  }}
                >
                  {opt.label}
                </button>
              {/each}
            </div>
          </div>
        {/if}
        {#if newEvent.componentType !== 'VTODO' && !newEvent.allDay}
          <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
            <div class="space-y-2">
              <Label>Start time</Label>
              {#if isMobile}
                <Input
                  type="time"
                  value={to24Hour(newEvent.startTime, newEvent.startMeridiem)}
                  oninput={(e) => {
                    const v = (e.target as HTMLInputElement).value;
                    const { time, meridiem } = to12Hour(v);
                    newEvent = { ...newEvent, startTime: time, startMeridiem: meridiem };
                    modalDirty = true;
                    handleStartTimeChange();
                  }}
                />
              {:else}
                <div class="flex gap-2">
                  <div class="relative flex-1">
                    <Input
                      type="text"
                      placeholder="12:00"
                      value={newEvent.startTime}
                      onfocus={() => {
                        showNewStartPicker = true;
                        showNewEndPicker = false;
                      }}
                      oninput={(e) => {
                        const val = (e.target as HTMLInputElement).value;
                        setNewEventTime('startTime', val);
                        modalDirty = true;
                      }}
                      onblur={() => {
                        setTimeout(() => {
                          showNewStartPicker = false;
                        }, 150);
                        handleStartTimeChange();
                      }}
                    />
                    {#if showNewStartPicker}
                      <div
                        class="time-dropdown absolute top-full left-0 right-0 mt-1 max-h-[220px] overflow-y-auto border border-border bg-popover shadow-lg z-20"
                      >
                        {#each timeOptions as opt}
                          <button
                            type="button"
                            class="w-full px-3 py-2 text-left text-sm hover:bg-accent"
                            onmousedown={(e) => {
                              e.preventDefault();
                              setNewEventTime('startTime', opt.value);
                              handleStartTimeChange();
                              showNewStartPicker = false;
                            }}
                          >
                            {opt.display}
                          </button>
                        {/each}
                      </div>
                    {/if}
                  </div>
                  <Select.Root
                    type="single"
                    bind:value={newEvent.startMeridiem}
                    onValueChange={() => handleStartTimeChange()}
                  >
                    <Select.Trigger size="default" class="h-9 w-[70px]">
                      {newEvent.startMeridiem || 'AM'}
                    </Select.Trigger>
                    <Select.Content>
                      <Select.Item value="AM">AM</Select.Item>
                      <Select.Item value="PM">PM</Select.Item>
                    </Select.Content>
                  </Select.Root>
                </div>
              {/if}
            </div>
            <div class="space-y-2">
              <Label>End time</Label>
              {#if isMobile}
                <Input
                  type="time"
                  value={to24Hour(newEvent.endTime, newEvent.endMeridiem)}
                  oninput={(e) => {
                    const v = (e.target as HTMLInputElement).value;
                    const { time, meridiem } = to12Hour(v);
                    newEvent = { ...newEvent, endTime: time, endMeridiem: meridiem };
                    modalDirty = true;
                    handleEndTimeChange();
                  }}
                />
              {:else}
                <div class="flex gap-2">
                  <div class="relative flex-1">
                    <Input
                      type="text"
                      placeholder="1:00"
                      value={newEvent.endTime}
                      onfocus={() => {
                        showNewEndPicker = true;
                        showNewStartPicker = false;
                      }}
                      oninput={(e) => {
                        const val = (e.target as HTMLInputElement).value;
                        setNewEventTime('endTime', val);
                        modalDirty = true;
                      }}
                      onblur={() => {
                        setTimeout(() => {
                          showNewEndPicker = false;
                        }, 150);
                        handleEndTimeChange();
                      }}
                    />
                    {#if showNewEndPicker}
                      <div
                        class="time-dropdown absolute top-full left-0 right-0 mt-1 max-h-[220px] overflow-y-auto border border-border bg-popover shadow-lg z-20"
                      >
                        {#each timeOptions as opt}
                          <button
                            type="button"
                            class="w-full px-3 py-2 text-left text-sm hover:bg-accent"
                            onmousedown={(e) => {
                              e.preventDefault();
                              setNewEventTime('endTime', opt.value);
                              showNewEndPicker = false;
                              handleEndTimeChange();
                            }}
                          >
                            {opt.display}
                          </button>
                        {/each}
                      </div>
                    {/if}
                  </div>
                  <Select.Root
                    type="single"
                    bind:value={newEvent.endMeridiem}
                    onValueChange={() => handleEndTimeChange()}
                  >
                    <Select.Trigger size="default" class="h-9 w-[70px]">
                      {newEvent.endMeridiem || 'AM'}
                    </Select.Trigger>
                    <Select.Content>
                      <Select.Item value="AM">AM</Select.Item>
                      <Select.Item value="PM">PM</Select.Item>
                    </Select.Content>
                  </Select.Root>
                </div>
              {/if}
            </div>
          </div>
        {/if}
        {#if newEvent.componentType !== 'VTODO' && newEvent.date}
          <div class="space-y-2">
            <Label>Repeats</Label>
            <div class="flex flex-wrap gap-1.5">
              {#each RECURRENCE_OPTIONS as opt (opt.value)}
                <button
                  type="button"
                  class={`inline-flex h-8 items-center px-3 text-xs rounded-full border transition-colors ${
                    newEvent.recurrence.mode === opt.value
                      ? 'bg-primary text-primary-foreground border-primary'
                      : 'bg-background text-muted-foreground border-border hover:bg-accent hover:text-foreground'
                  }`}
                  onclick={() => {
                    newEvent.recurrence = { ...newEvent.recurrence, mode: opt.value };
                    // Re-interpret End date for the new mode: when
                    // recurring, End date means UNTIL (the recurrence
                    // stops here); when 'none' it's the event's span.
                    syncRecurrenceUntilFromEndDate(newEvent);
                    modalDirty = true;
                  }}
                >
                  {opt.label}
                </button>
              {/each}
            </div>
            {#if newRecurrenceDescription}
              <p class="text-xs text-muted-foreground">{newRecurrenceDescription}</p>
            {/if}
          </div>
          <div class="space-y-2">
            <Label>Remind me</Label>
            <div class="flex flex-wrap gap-1.5">
              {#each REMINDER_OPTIONS as opt (opt.value)}
                <button
                  type="button"
                  class={`inline-flex h-8 items-center px-3 text-xs rounded-full border transition-colors ${
                    Number(newEvent.notify || 0) === opt.value
                      ? 'bg-primary text-primary-foreground border-primary'
                      : 'bg-background text-muted-foreground border-border hover:bg-accent hover:text-foreground'
                  }`}
                  onclick={() => {
                    newEvent.notify = opt.value;
                    modalDirty = true;
                  }}
                >
                  {opt.label}
                </button>
              {/each}
            </div>
          </div>
        {/if}
        <div class="space-y-2">
          <Label for="event-description">Description</Label>
          {#if containsHtml(newEvent.description) && !newEvent._editingRawDescription}
            <div
              class="prose prose-sm dark:prose-invert max-w-none min-w-0 p-3 border border-border rounded-md bg-muted/30 max-h-[200px] overflow-y-auto overflow-x-hidden break-words wrap-anywhere [overflow-wrap:anywhere] [&_pre]:whitespace-pre-wrap [&_pre]:break-all [&_img]:max-w-full [&_table]:block [&_table]:overflow-x-auto"
            >
              {@html sanitizeDescription(newEvent.description)}
            </div>
            <button
              type="button"
              class="text-xs text-muted-foreground hover:text-foreground"
              onclick={() => {
                newEvent._editingRawDescription = true;
              }}>Edit source</button
            >
          {:else}
            <Textarea
              id="event-description"
              rows={3}
              placeholder="Add notes or details"
              bind:value={newEvent.description}
              bind:this={descriptionRef}
              oninput={(e) => {
                autoExpand(e.currentTarget as HTMLTextAreaElement);
                modalDirty = true;
              }}
              class="min-h-[60px] max-h-[200px] resize-none"
            />
          {/if}
        </div>

        {#if newEvent.componentType !== 'VTODO'}
          <Separator />

          <button
            type="button"
            class="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
            onclick={() => (optionalFieldsExpanded = !optionalFieldsExpanded)}
          >
            {#if optionalFieldsExpanded}
              <ChevronDown class="h-4 w-4" />
            {:else}
              <ChevronRight class="h-4 w-4" />
            {/if}
            <span>More details</span>
            {#if !optionalFieldsExpanded && (newEvent.location || newEvent.url || newEvent.timezone || newEvent.attendees)}
              <span class="ml-1 text-primary">•</span>
            {/if}
          </button>

          {#if optionalFieldsExpanded}
            <div class="space-y-4 pl-6">
              <div class="space-y-2">
                <Label>Location</Label>
                <Input
                  type="text"
                  placeholder="Add location"
                  bind:value={newEvent.location}
                  oninput={() => (modalDirty = true)}
                />
              </div>
              <div class="space-y-2">
                <Label>URL / Video link</Label>
                <Input
                  type="url"
                  placeholder="https://"
                  bind:value={newEvent.url}
                  oninput={() => (modalDirty = true)}
                />
              </div>
              <div class="space-y-2">
                <Label for="new-event-timezone">Time zone</Label>
                <TimezoneCombobox
                  id="new-event-timezone"
                  bind:value={newEvent.timezone}
                  onChange={() => (modalDirty = true)}
                />
              </div>
              <div class="space-y-2">
                <Label for="new-event-attendee-input">Attendees</Label>
                <div class="flex gap-2">
                  <Input
                    id="new-event-attendee-input"
                    type="email"
                    placeholder="email@example.com"
                    bind:value={newAttendeeDraft}
                    onkeydown={(e) => {
                      if (e.key === 'Enter') {
                        e.preventDefault();
                        addAttendeeToNewEvent();
                      }
                    }}
                  />
                  <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    disabled={!newAttendeeDraft.trim()}
                    onclick={addAttendeeToNewEvent}
                  >
                    <Plus class="h-4 w-4" />
                  </Button>
                </div>
                {#each parseAttendeeList(newEvent.attendees) as email (email)}
                  <div class="flex items-center justify-between border border-border p-2">
                    <div class="flex min-w-0 items-center gap-2">
                      <User class="h-4 w-4 shrink-0 text-muted-foreground" />
                      <span class="truncate text-sm">{email}</span>
                    </div>
                    <Button
                      type="button"
                      variant="ghost"
                      size="icon"
                      onclick={() => removeAttendeeFromNewEvent(email)}
                      aria-label="Remove {email}"
                    >
                      <X class="h-4 w-4" />
                    </Button>
                  </div>
                {/each}
              </div>
            </div>
          {/if}
        {/if}
      </div>
      <Dialog.Footer>
        <Button variant="ghost" onclick={() => closeModals()} disabled={savingEvent}>Cancel</Button>
        <Button onclick={attemptSaveNewEvent} disabled={!isNewEventValid || savingEvent}>
          {savingEvent ? 'Saving...' : 'Save'}
        </Button>
      </Dialog.Footer>
    </Dialog.Content>
  </Dialog.Root>

  <Dialog.Root bind:open={editEventModal}>
    <Dialog.Content
      class="sm:max-w-[520px] overflow-x-hidden [&>*]:min-w-0"
      onfocusin={closeTimePickers}
    >
      <Dialog.Header>
        <Dialog.Title
          >{editEvent.componentType === 'VTODO' ? 'Edit task' : 'Edit event'}</Dialog.Title
        >
      </Dialog.Header>

      <div class="space-y-4 py-4">
        {#if isEditingRecurringEvent}
          <div
            class="rounded-md border border-border bg-muted/40 px-3 py-2 text-xs text-muted-foreground"
          >
            <div class="flex items-center gap-1.5 font-medium text-foreground">
              <span aria-hidden="true">↻</span>
              <span>Recurring event{editRecurrenceText ? ` · ${editRecurrenceText}` : ''}</span>
            </div>
            <div class="mt-1">
              {#if isCustomRecurrence}
                Custom recurrence (read-only) — saving from this view isn't supported yet.
              {:else}
                Saving asks whether to change just this occurrence or every event in the series.
              {/if}
            </div>
          </div>
        {/if}
        {#if calendars.length > 1}
          <div class="space-y-2">
            <Label>Calendar</Label>
            <Select.Root
              type="single"
              value={editEvent.calendarId}
              onValueChange={(v) => {
                if (v) {
                  editEvent.calendarId = v;
                  modalDirty = true;
                  setActiveCalendarId(v);
                }
              }}
            >
              <Select.Trigger class="w-full">
                {getCalendarLabel(getCalendarById(editEvent.calendarId)) || 'Select calendar'}
              </Select.Trigger>
              <Select.Content>
                {#each calendars as cal}
                  {@const calId = getCalendarId(cal)}
                  {#if calId}
                    <Select.Item value={calId as string}>{getCalendarLabel(cal)}</Select.Item>
                  {/if}
                {/each}
              </Select.Content>
            </Select.Root>
          </div>
        {/if}
        <div class="space-y-2">
          <Label>Title</Label>
          <Input type="text" bind:value={editEvent.title} />
        </div>
        {#if editEvent.componentType === 'VTODO'}
          {#if !editEvent.hasDate}
            <button
              type="button"
              class="text-sm font-medium text-muted-foreground hover:text-foreground"
              onclick={() => {
                editEvent.hasDate = true;
                modalDirty = true;
              }}
            >
              + Add due date
            </button>
          {:else}
            <div class="space-y-2">
              <Label>Due date</Label>
              <div class="flex items-center gap-2">
                <Input
                  type="date"
                  class="flex-1"
                  bind:value={editEvent.date}
                  onchange={(e) => {
                    editEvent.date = e.currentTarget.value;
                    modalDirty = true;
                  }}
                />
                <button
                  type="button"
                  class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
                  aria-label="Remove due date"
                  onclick={() => {
                    editEvent.hasDate = false;
                    editEvent.hasTime = false;
                    modalDirty = true;
                  }}
                >
                  <X class="h-4 w-4" />
                </button>
              </div>
            </div>
            {#if !editEvent.hasTime}
              <button
                type="button"
                class="text-sm font-medium text-muted-foreground hover:text-foreground"
                onclick={() => {
                  editEvent.hasTime = true;
                  modalDirty = true;
                }}
              >
                + Add time
              </button>
            {:else}
              <div class="space-y-2">
                <Label>Time</Label>
                <div class="flex items-center gap-2">
                  {#if isMobile}
                    <Input
                      type="time"
                      class="flex-1"
                      value={to24Hour(editEvent.startTime, editEvent.startMeridiem)}
                      oninput={(e) => {
                        const v = (e.target as HTMLInputElement).value;
                        const { time, meridiem } = to12Hour(v);
                        editEvent = {
                          ...editEvent,
                          startTime: time,
                          startMeridiem: meridiem,
                        };
                        modalDirty = true;
                      }}
                    />
                  {:else}
                    <div class="relative flex-1">
                      <Input
                        type="text"
                        placeholder="12:00"
                        value={editEvent.startTime}
                        onfocus={() => {
                          showEditStartPicker = true;
                          showEditEndPicker = false;
                        }}
                        oninput={(e) => {
                          const val = (e.target as HTMLInputElement).value;
                          setEditEventTime('startTime', val);
                          modalDirty = true;
                        }}
                        onblur={() => {
                          setTimeout(() => {
                            showEditStartPicker = false;
                          }, 150);
                        }}
                      />
                      {#if showEditStartPicker}
                        <div
                          class="time-dropdown absolute top-full left-0 right-0 mt-1 max-h-[220px] overflow-y-auto border border-border bg-popover shadow-lg z-20"
                        >
                          {#each timeOptions as opt}
                            <button
                              type="button"
                              class="w-full px-3 py-2 text-left text-sm hover:bg-accent"
                              onmousedown={(e) => {
                                e.preventDefault();
                                setEditEventTime('startTime', opt.value);
                                showEditStartPicker = false;
                              }}
                            >
                              {opt.display}
                            </button>
                          {/each}
                        </div>
                      {/if}
                    </div>
                    <Select.Root type="single" bind:value={editEvent.startMeridiem}>
                      <Select.Trigger size="default" class="h-9 w-[70px]">
                        {editEvent.startMeridiem || 'AM'}
                      </Select.Trigger>
                      <Select.Content>
                        <Select.Item value="AM">AM</Select.Item>
                        <Select.Item value="PM">PM</Select.Item>
                      </Select.Content>
                    </Select.Root>
                  {/if}
                  <button
                    type="button"
                    class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"
                    aria-label="Remove time"
                    onclick={() => {
                      editEvent.hasTime = false;
                      modalDirty = true;
                    }}
                  >
                    <X class="h-4 w-4" />
                  </button>
                </div>
              </div>
            {/if}
          {/if}
          {#if editEvent.hasDate}
            <div class="space-y-2">
              <Label>Remind me</Label>
              <div class="flex flex-wrap gap-1.5">
                {#each REMINDER_OPTIONS as opt (opt.value)}
                  <button
                    type="button"
                    class={`inline-flex h-8 items-center px-3 text-xs rounded-full border transition-colors ${
                      Number(editEvent.notify || 0) === opt.value
                        ? 'bg-primary text-primary-foreground border-primary'
                        : 'bg-background text-muted-foreground border-border hover:bg-accent hover:text-foreground'
                    }`}
                    onclick={() => {
                      editEvent.notify = opt.value;
                      modalDirty = true;
                    }}
                  >
                    {opt.label}
                  </button>
                {/each}
              </div>
            </div>
          {/if}
        {:else}
          {#if isForeignZone(editEvent.timezone)}
            <div
              class="rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-xs text-amber-900 dark:text-amber-200"
            >
              Times shown in your local zone (<span class="font-mono">{viewerZone}</span>). Event
              time zone: <span class="font-mono">{editEvent.timezone}</span>.
            </div>
          {/if}
          <div class="grid grid-cols-2 gap-4">
            <div class="space-y-2">
              <Label>Start date</Label>
              <Input
                type="date"
                bind:value={editEvent.date}
                onchange={(e) => applyStartDateChange(editEvent, e.currentTarget.value)}
              />
            </div>
            <div class="space-y-2">
              <Label>
                {editEvent.recurrence.mode !== 'none' && editEvent.recurrence.mode !== 'custom'
                  ? 'Repeat until (optional)'
                  : 'End date'}
              </Label>
              <Input
                type="date"
                min={editEvent.date}
                bind:value={editEvent.endDate}
                onchange={(e) => {
                  editEvent.endDate = e.currentTarget.value;
                  modalDirty = true;
                  syncRecurrenceUntilFromEndDate(editEvent);
                }}
              />
            </div>
          </div>
          <label class="flex items-center gap-2 cursor-pointer text-sm">
            <Checkbox
              checked={editEvent.allDay}
              onCheckedChange={(v) => {
                editEvent.allDay = !!v;
                modalDirty = true;
              }}
            />
            <span>All-day</span>
          </label>
          {#if !editEvent.allDay}
            <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
              <div class="space-y-2">
                <Label>Start time</Label>
                {#if isMobile}
                  <Input
                    type="time"
                    value={to24Hour(editEvent.startTime, editEvent.startMeridiem)}
                    oninput={(e) => {
                      const v = (e.target as HTMLInputElement).value;
                      const { time, meridiem } = to12Hour(v);
                      editEvent = { ...editEvent, startTime: time, startMeridiem: meridiem };
                      modalDirty = true;
                    }}
                  />
                {:else}
                  <div class="flex gap-2">
                    <div class="relative flex-1">
                      <Input
                        type="text"
                        placeholder="12:00"
                        value={editEvent.startTime}
                        onfocus={() => {
                          showEditStartPicker = true;
                          showEditEndPicker = false;
                        }}
                        oninput={(e) => {
                          const val = (e.target as HTMLInputElement).value;
                          setEditEventTime('startTime', val);
                          modalDirty = true;
                        }}
                        onblur={() => {
                          setTimeout(() => {
                            showEditStartPicker = false;
                          }, 150);
                        }}
                      />
                      {#if showEditStartPicker}
                        <div
                          class="time-dropdown absolute top-full left-0 right-0 mt-1 max-h-[220px] overflow-y-auto border border-border bg-popover shadow-lg z-20"
                        >
                          {#each timeOptions as opt}
                            <button
                              type="button"
                              class="w-full px-3 py-2 text-left text-sm hover:bg-accent"
                              onmousedown={(e) => {
                                e.preventDefault();
                                setEditEventTime('startTime', opt.value);
                                showEditStartPicker = false;
                              }}
                            >
                              {opt.display}
                            </button>
                          {/each}
                        </div>
                      {/if}
                    </div>
                    <Select.Root type="single" bind:value={editEvent.startMeridiem}>
                      <Select.Trigger size="default" class="h-9 w-[70px]">
                        {editEvent.startMeridiem || 'AM'}
                      </Select.Trigger>
                      <Select.Content>
                        <Select.Item value="AM">AM</Select.Item>
                        <Select.Item value="PM">PM</Select.Item>
                      </Select.Content>
                    </Select.Root>
                  </div>
                {/if}
              </div>
              <div class="space-y-2">
                <Label>End time</Label>
                {#if isMobile}
                  <Input
                    type="time"
                    value={to24Hour(editEvent.endTime, editEvent.endMeridiem)}
                    oninput={(e) => {
                      const v = (e.target as HTMLInputElement).value;
                      const { time, meridiem } = to12Hour(v);
                      editEvent = { ...editEvent, endTime: time, endMeridiem: meridiem };
                      modalDirty = true;
                    }}
                  />
                {:else}
                  <div class="flex gap-2">
                    <div class="relative flex-1">
                      <Input
                        type="text"
                        placeholder="1:00"
                        value={editEvent.endTime}
                        onfocus={() => {
                          showEditEndPicker = true;
                          showEditStartPicker = false;
                        }}
                        oninput={(e) => {
                          const val = (e.target as HTMLInputElement).value;
                          setEditEventTime('endTime', val);
                          modalDirty = true;
                        }}
                        onblur={() => {
                          setTimeout(() => {
                            showEditEndPicker = false;
                          }, 150);
                        }}
                      />
                      {#if showEditEndPicker}
                        <div
                          class="time-dropdown absolute top-full left-0 right-0 mt-1 max-h-[220px] overflow-y-auto border border-border bg-popover shadow-lg z-20"
                        >
                          {#each timeOptions as opt}
                            <button
                              type="button"
                              class="w-full px-3 py-2 text-left text-sm hover:bg-accent"
                              onmousedown={(e) => {
                                e.preventDefault();
                                setEditEventTime('endTime', opt.value);
                                showEditEndPicker = false;
                              }}
                            >
                              {opt.display}
                            </button>
                          {/each}
                        </div>
                      {/if}
                    </div>
                    <Select.Root type="single" bind:value={editEvent.endMeridiem}>
                      <Select.Trigger size="default" class="h-9 w-[70px]">
                        {editEvent.endMeridiem || 'AM'}
                      </Select.Trigger>
                      <Select.Content>
                        <Select.Item value="AM">AM</Select.Item>
                        <Select.Item value="PM">PM</Select.Item>
                      </Select.Content>
                    </Select.Root>
                  </div>
                {/if}
              </div>
            </div>
          {/if}
        {/if}
        {#if editEvent.componentType !== 'VTODO' && editEvent.date}
          <div class="space-y-2">
            <Label>Repeats</Label>
            <div class="flex flex-wrap gap-1.5">
              {#each RECURRENCE_OPTIONS as opt (opt.value)}
                <button
                  type="button"
                  disabled={isCustomRecurrence}
                  class={`inline-flex h-8 items-center px-3 text-xs rounded-full border transition-colors ${
                    editEvent.recurrence.mode === opt.value
                      ? 'bg-primary text-primary-foreground border-primary'
                      : 'bg-background text-muted-foreground border-border hover:bg-accent hover:text-foreground'
                  } ${isCustomRecurrence ? 'opacity-50 cursor-not-allowed' : ''}`}
                  onclick={() => {
                    editEvent.recurrence = { ...editEvent.recurrence, mode: opt.value };
                    syncRecurrenceUntilFromEndDate(editEvent);
                    modalDirty = true;
                  }}
                >
                  {opt.label}
                </button>
              {/each}
              {#if isCustomRecurrence}
                <span
                  class="inline-flex h-8 items-center px-3 text-xs rounded-full border border-border bg-muted text-muted-foreground"
                >
                  Custom (read-only)
                </span>
              {/if}
            </div>
            {#if editRecurrenceDescription}
              <p class="text-xs text-muted-foreground">{editRecurrenceDescription}</p>
            {/if}
          </div>
          <div class="space-y-2">
            <Label>Remind me</Label>
            <div class="flex flex-wrap gap-1.5">
              {#each REMINDER_OPTIONS as opt (opt.value)}
                <button
                  type="button"
                  class={`inline-flex h-8 items-center px-3 text-xs rounded-full border transition-colors ${
                    Number(editEvent.notify || 0) === opt.value
                      ? 'bg-primary text-primary-foreground border-primary'
                      : 'bg-background text-muted-foreground border-border hover:bg-accent hover:text-foreground'
                  }`}
                  onclick={() => {
                    editEvent.notify = opt.value;
                    modalDirty = true;
                  }}
                >
                  {opt.label}
                </button>
              {/each}
            </div>
          </div>
        {/if}
        <div class="space-y-2">
          <Label>Description</Label>
          {#if containsHtml(editEvent.description) && !editEvent._editingRawDescription}
            <div
              class="prose prose-sm dark:prose-invert max-w-none min-w-0 p-3 border border-border rounded-md bg-muted/30 max-h-[200px] overflow-y-auto overflow-x-hidden break-words wrap-anywhere [overflow-wrap:anywhere] [&_pre]:whitespace-pre-wrap [&_pre]:break-all [&_img]:max-w-full [&_table]:block [&_table]:overflow-x-auto"
            >
              {@html sanitizeDescription(editEvent.description)}
            </div>
            <button
              type="button"
              class="text-xs text-muted-foreground hover:text-foreground"
              onclick={() => {
                editEvent._editingRawDescription = true;
              }}>Edit source</button
            >
          {:else}
            <Textarea rows={5} bind:value={editEvent.description} />
          {/if}
        </div>

        {#if editEvent.componentType !== 'VTODO'}
          <Separator />

          <button
            type="button"
            class="flex items-center gap-2 text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
            onclick={() => (optionalFieldsExpanded = !optionalFieldsExpanded)}
          >
            {#if optionalFieldsExpanded}
              <ChevronDown class="h-4 w-4" />
            {:else}
              <ChevronRight class="h-4 w-4" />
            {/if}
            <span>More details</span>
            {#if !optionalFieldsExpanded && (editEvent.location || editEvent.url || editEvent.timezone || editEvent.attendees)}
              <span class="ml-1 text-primary">•</span>
            {/if}
          </button>

          {#if optionalFieldsExpanded}
            <div class="space-y-4 pl-6">
              <div class="space-y-2">
                <Label>Location</Label>
                <Input type="text" placeholder="Add location" bind:value={editEvent.location} />
              </div>
              <div class="space-y-2">
                <Label>URL / Video link</Label>
                <Input type="url" placeholder="https://" bind:value={editEvent.url} />
              </div>
              <div class="space-y-2">
                <Label for="edit-event-timezone">Time zone</Label>
                <TimezoneCombobox id="edit-event-timezone" bind:value={editEvent.timezone} />
              </div>
              <div class="space-y-2">
                <Label for="edit-event-attendee-input">Attendees</Label>
                <div class="flex gap-2">
                  <Input
                    id="edit-event-attendee-input"
                    type="email"
                    placeholder="email@example.com"
                    bind:value={editAttendeeDraft}
                    onkeydown={(e) => {
                      if (e.key === 'Enter') {
                        e.preventDefault();
                        addAttendeeToEditEvent();
                      }
                    }}
                  />
                  <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    disabled={!editAttendeeDraft.trim()}
                    onclick={addAttendeeToEditEvent}
                  >
                    <Plus class="h-4 w-4" />
                  </Button>
                </div>
                {#each parseAttendeeList(editEvent.attendees) as email (email)}
                  <div class="flex items-center justify-between border border-border p-2">
                    <div class="flex min-w-0 items-center gap-2">
                      <User class="h-4 w-4 shrink-0 text-muted-foreground" />
                      <span class="truncate text-sm">{email}</span>
                    </div>
                    <Button
                      type="button"
                      variant="ghost"
                      size="icon"
                      onclick={() => removeAttendeeFromEditEvent(email)}
                      aria-label="Remove {email}"
                    >
                      <X class="h-4 w-4" />
                    </Button>
                  </div>
                {/each}
              </div>
            </div>
          {/if}
        {/if}
      </div>
      <Dialog.Footer class="flex-col sm:flex-row gap-2">
        <div class="hidden sm:flex gap-2 mr-auto">
          <Tooltip.Root>
            <Tooltip.Trigger>
              <Button variant="outline" size="icon" onclick={() => duplicateEvent()}>
                <Copy class="h-4 w-4" />
              </Button>
            </Tooltip.Trigger>
            <Tooltip.Content>
              <p>Duplicate</p>
            </Tooltip.Content>
          </Tooltip.Root>
          {#if editEvent.componentType !== 'VTODO'}
            <Tooltip.Root>
              <Tooltip.Trigger>
                <Button
                  variant="outline"
                  size="icon"
                  onclick={() =>
                    exportEventAsICS(
                      events.find((e) => (e as Record).id === editEvent.id) as Record,
                    )}
                >
                  <Download class="h-4 w-4" />
                </Button>
              </Tooltip.Trigger>
              <Tooltip.Content>
                <p>Export as .ics</p>
              </Tooltip.Content>
            </Tooltip.Root>
          {/if}
          <Tooltip.Root>
            <Tooltip.Trigger>
              <Button
                variant="outline"
                size="icon"
                class="text-destructive hover:text-destructive"
                disabled={isCustomRecurrence}
                onclick={() => promptRecurrenceEdit('delete')}
              >
                <Trash2 class="h-4 w-4" />
              </Button>
            </Tooltip.Trigger>
            <Tooltip.Content>
              <p>
                {isCustomRecurrence
                  ? 'Custom recurrence — delete from another client'
                  : isEditingRecurringEvent
                    ? 'Delete (choose this / all)'
                    : 'Delete'}
              </p>
            </Tooltip.Content>
          </Tooltip.Root>
        </div>
        <div class="flex gap-2">
          <Button variant="ghost" onclick={() => closeModals()}>Cancel</Button>
          {#if editEvent.componentType === 'VTODO'}
            {#if editEvent.status?.toUpperCase?.() === 'COMPLETED' || editEvent.completedAt || Number(editEvent.percentComplete || 0) >= 100}
              <Button variant="outline" onclick={() => uncompleteTask()}>Mark incomplete</Button>
            {:else}
              <Button variant="outline" onclick={() => completeTask()}>Complete</Button>
            {/if}
            <Button onclick={() => promptRecurrenceEdit('save')} disabled={isCustomRecurrence}
              >Update</Button
            >
          {:else}
            <Button onclick={() => promptRecurrenceEdit('save')} disabled={isCustomRecurrence}
              >Update</Button
            >
          {/if}
        </div>
      </Dialog.Footer>
    </Dialog.Content>
  </Dialog.Root>

  <Dialog.Root bind:open={showDeleteConfirm}>
    <Dialog.Content class="sm:max-w-[400px]">
      <Dialog.Header>
        <Dialog.Title class="text-destructive">Delete event?</Dialog.Title>
      </Dialog.Header>
      <p class="text-sm text-muted-foreground py-4">
        This event will be permanently removed. This can't be undone.
      </p>
      <Dialog.Footer>
        <Button variant="ghost" onclick={() => (showDeleteConfirm = false)}>Cancel</Button>
        <Button variant="destructive" onclick={deleteEvent}>Delete</Button>
      </Dialog.Footer>
    </Dialog.Content>
  </Dialog.Root>

  <!-- Per-instance edit/delete prompt for recurring events -->
  <Dialog.Root
    open={recurrenceEditPrompt.open}
    onOpenChange={(open) => {
      if (!open) recurrenceEditPrompt = { open: false, action: null };
    }}
  >
    <Dialog.Content class="sm:max-w-[440px]">
      <Dialog.Header>
        <Dialog.Title>
          {recurrenceEditPrompt.action === 'delete'
            ? 'Delete recurring event'
            : 'Edit recurring event'}
        </Dialog.Title>
      </Dialog.Header>
      <p class="text-sm text-muted-foreground py-3">
        This event is part of a recurring series. What do you want to {recurrenceEditPrompt.action ===
        'delete'
          ? 'delete'
          : 'change'}?
      </p>
      <div class="flex flex-col gap-2 pb-2">
        <Button variant="outline" onclick={() => applyRecurrenceChoice('this')}>
          {recurrenceEditPrompt.action === 'delete' ? 'Only this event' : 'This event'}
        </Button>
        <Tooltip.Root>
          <Tooltip.Trigger class="w-full">
            <Button variant="outline" disabled class="w-full">This and following events</Button>
          </Tooltip.Trigger>
          <Tooltip.Content>
            <p>Coming soon — splits the series at this occurrence.</p>
          </Tooltip.Content>
        </Tooltip.Root>
        <Button
          variant={recurrenceEditPrompt.action === 'delete' ? 'destructive' : 'default'}
          onclick={() => applyRecurrenceChoice('all')}
        >
          All events in the series
        </Button>
      </div>
      <Dialog.Footer>
        <Button
          variant="ghost"
          onclick={() => (recurrenceEditPrompt = { open: false, action: null })}
        >
          Cancel
        </Button>
      </Dialog.Footer>
    </Dialog.Content>
  </Dialog.Root>

  <Dialog.Root bind:open={deleteCalendarModal}>
    <Dialog.Content class="sm:max-w-[420px]">
      <Dialog.Header>
        <Dialog.Title class="text-destructive">Delete calendar?</Dialog.Title>
      </Dialog.Header>
      <div class="space-y-4 py-2">
        <p class="text-sm text-muted-foreground">
          Are you sure you want to delete
          <span class="font-medium text-foreground">{deleteCalendarLabel}</span>? This permanently
          deletes the calendar and all of its events. This can't be undone. To confirm, type the
          full calendar name below.
        </p>
        <div class="space-y-2">
          <Label for="delete-calendar-confirm-name">Calendar name</Label>
          <Input
            id="delete-calendar-confirm-name"
            bind:value={deleteCalendarConfirmName}
            placeholder={deleteCalendarLabel || 'Calendar name'}
            autocapitalize="off"
            autocomplete="off"
            autocorrect="off"
            spellcheck={false}
          />
        </div>
      </div>
      <Dialog.Footer>
        <Button
          variant="ghost"
          onclick={() => {
            deleteCalendarModal = false;
            deleteCalendarId = '';
            deleteCalendarLabel = '';
            deleteCalendarConfirmName = '';
          }}
        >
          Cancel
        </Button>
        <Button
          variant="destructive"
          disabled={deleteCalendarConfirmName.trim() !== deleteCalendarLabel.trim()}
          onclick={deleteCalendar}
        >
          Delete Calendar
        </Button>
      </Dialog.Footer>
    </Dialog.Content>
  </Dialog.Root>

  <!-- Calendars (mobile filter) -->
  <Dialog.Root bind:open={calendarsModalOpen}>
    <Dialog.Content class="sm:max-w-[420px]">
      <Dialog.Header>
        <Dialog.Title>Calendars</Dialog.Title>
      </Dialog.Header>
      <div class="space-y-1 py-2 max-h-[60vh] overflow-y-auto">
        {#each calendars as cal, index}
          {@const calId = getCalendarId(cal)}
          {#if calId}
            {@const checked = selectedCalendarIds.includes(calId as string)}
            <div class="flex items-center gap-3 px-2 py-2 rounded-md hover:bg-accent/50">
              <button
                type="button"
                class="flex flex-1 items-center gap-3 min-w-0 text-left"
                aria-pressed={checked}
                onclick={() => toggleCalendarSelection(calId as string)}
              >
                <span
                  class="h-5 w-5 shrink-0 inline-flex items-center justify-center rounded-[4px] border-2 transition-colors
                    {checked
                    ? 'border-primary bg-primary text-primary-foreground'
                    : 'border-muted-foreground/50 bg-transparent'}"
                  aria-hidden="true"
                >
                  {#if checked}
                    <Check class="size-3.5" strokeWidth={3} />
                  {/if}
                </span>
                <span
                  class="h-2.5 w-2.5 shrink-0 rounded-full"
                  style="background: {resolveCalendarColor(cal, index)}"
                  aria-hidden="true"
                ></span>
                <span class="truncate">{getCalendarLabel(cal)}</span>
              </button>
              <button
                type="button"
                class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
                aria-label="Delete {getCalendarLabel(cal)}"
                onclick={() => {
                  calendarsModalOpen = false;
                  openDeleteCalendarModal(calId as string);
                }}
              >
                <Trash2 class="h-4 w-4" />
              </button>
            </div>
          {/if}
        {/each}
      </div>
      <Dialog.Footer>
        <Button variant="ghost" onclick={() => (calendarsModalOpen = false)}>Done</Button>
      </Dialog.Footer>
    </Dialog.Content>
  </Dialog.Root>

  <!-- New Calendar Modal -->
  <Dialog.Root bind:open={newCalendarModal}>
    <Dialog.Content class="sm:max-w-[400px]">
      <Dialog.Header>
        <Dialog.Title>Create Calendar</Dialog.Title>
      </Dialog.Header>
      <div class="space-y-4 py-4">
        <div class="space-y-2">
          <Label for="new-cal-name">Name</Label>
          <Input
            id="new-cal-name"
            placeholder="My Calendar"
            bind:value={newCalendarName}
            onkeydown={(e: KeyboardEvent) => {
              if (e.key === 'Enter' && newCalendarName.trim()) createNewCalendar();
            }}
          />
        </div>
        <div class="space-y-2">
          <Label for="new-cal-color">Color</Label>
          <div class="flex items-center gap-3">
            <input
              id="new-cal-color"
              type="color"
              bind:value={newCalendarColor}
              class="h-9 w-9 cursor-pointer rounded border border-border bg-transparent p-0.5"
            />
            <div class="flex gap-1.5">
              {#each calendarColorPalette as color}
                <button
                  type="button"
                  class="h-6 w-6 rounded-full border-2 transition-all {newCalendarColor === color
                    ? 'border-foreground scale-110'
                    : 'border-transparent hover:border-muted-foreground/50'}"
                  style="background: {color}"
                  onclick={() => {
                    newCalendarColor = color;
                  }}
                  aria-label="Select color {color}"
                ></button>
              {/each}
            </div>
          </div>
        </div>
      </div>
      <Dialog.Footer>
        <Button
          variant="ghost"
          onclick={() => {
            newCalendarModal = false;
          }}>Cancel</Button
        >
        <Button disabled={!newCalendarName.trim() || savingCalendar} onclick={createNewCalendar}>
          {savingCalendar ? 'Creating...' : 'Create Calendar'}
        </Button>
      </Dialog.Footer>
    </Dialog.Content>
  </Dialog.Root>
</Tooltip.Provider>

<style>
  .calendar-page {
    height: 100%;
    min-height: 0;
    overflow: hidden;
  }

  /* Calendar content area - explicit height calculation */
  .calendar-content {
    height: calc(100vh - 65px);
    height: calc(100dvh - 65px);
  }

  .sx-wrapper {
    width: 100%;
    max-width: 100%;
    /* Explicit height calculation: viewport - header (65px) - padding (32px) - privacy notice (~52px) */
    height: calc(100vh - 150px);
    height: calc(100dvh - 150px);
    min-height: 400px;
    overflow: visible; /* Allow dropdown menus to overlay outside the wrapper */
  }

  /* The ScheduleXCalendar component adds an extra div wrapper */
  .sx-wrapper > :global(div) {
    height: 100% !important;
  }

  /* Make all schedule-x containers fill available height */
  .sx-wrapper :global(.sx-svelte-calendar-wrapper) {
    height: 100% !important;
    overflow: visible !important;
  }

  .sx-wrapper :global(.sx__calendar-wrapper) {
    height: 100% !important;
    overflow: visible !important;
  }

  .sx-wrapper :global(.sx__calendar) {
    height: 100% !important;
    display: flex;
    flex-direction: column;
    overflow: visible !important;
  }

  .sx-wrapper :global(.sx__calendar-header) {
    flex-shrink: 0;
    position: relative;
    z-index: 20;
    overflow: visible;
  }

  .sx-wrapper :global(.sx__view-container) {
    flex: 1 1 0% !important;
    min-height: 0 !important;
    overflow-y: auto !important;
    overflow-x: hidden !important;
  }

  /* Month view - make weeks fill the grid evenly */
  .sx-wrapper :global(.sx__month-grid-wrapper) {
    height: 100% !important;
    display: flex;
    flex-direction: column;
  }

  .sx-wrapper :global(.sx__month-grid-week) {
    flex: 1;
    min-height: 0;
  }

  .sx-wrapper :global(.sx__month-grid-day) {
    height: 100% !important;
  }

  /* Week and Day views - set CSS variables for proper height */
  .sx-wrapper :global(.sx__week-wrapper),
  .sx-wrapper :global(.sx__day-wrapper) {
    --sx-week-grid-hour-height: 60px !important;
    --sx-week-grid-height: 1440px !important; /* 24 hours * 60px */
    position: relative !important;
  }

  .sx-wrapper :global(.sx__week-header),
  .sx-wrapper :global(.sx__week-grid__date-axis) {
    position: sticky !important;
    top: 0 !important;
    z-index: 10 !important;
    background: inherit !important;
  }

  /* Week grid - needs explicit height for content */
  .sx-wrapper :global(.sx__week-grid) {
    height: var(--sx-week-grid-height, 1440px) !important;
    min-height: 1440px !important;
    overflow: visible !important;
  }

  /* Time axis styling */
  .sx-wrapper :global(.sx__week-grid__time-axis) {
    height: var(--sx-week-grid-height, 1440px) !important;
  }

  .sx-wrapper :global(.sx__week-grid__hour) {
    height: var(--sx-week-grid-hour-height, 60px) !important;
  }

  /* Time grid day columns */
  .sx-wrapper :global(.sx__time-grid-day) {
    height: var(--sx-week-grid-height, 1440px) !important;
    min-height: 1440px !important;
  }

  /* All-day events section should not grow */
  .sx-wrapper :global(.sx__all-day-wrapper) {
    flex-shrink: 0 !important;
    max-height: 100px;
    overflow-y: auto;
  }

  /* Override schedule-x primary colors */
  .sx-wrapper {
    --sx-color-primary: #0066ff !important;
    --sx-color-on-primary: #ffffff !important;
  }

  .sx-wrapper :global(.sx__time-grid-event),
  .sx-wrapper :global(.sx__month-grid-event) {
    --sx-color-primary: #0066ff !important;
    --sx-color-primary-container: #e6efff !important;
    --sx-color-on-primary-container: #003bb3 !important;
  }

  /* Dark mode overrides - event labels with better visibility */
  .sx-wrapper.is-dark :global(.sx__time-grid-event),
  .sx-wrapper.is-dark :global(.sx__month-grid-event) {
    --sx-color-primary: #60a5fa !important;
    --sx-color-primary-container: #1e40af !important;
    --sx-color-on-primary-container: #e0f2fe !important;
  }

  /* Dark mode calendar background and text colors */
  .sx-wrapper.is-dark {
    --sx-color-surface: oklch(0.129 0.042 264.695) !important;
    --sx-color-on-surface: #e2e8f0 !important;
    --sx-color-surface-container-low: oklch(0.178 0.042 265.755) !important;
    --sx-color-on-surface-variant: #94a3b8 !important;
    --sx-color-outline: #334155 !important;
    --sx-color-outline-variant: #334155 !important;
    background: oklch(0.129 0.042 264.695) !important;
    color: #e2e8f0 !important;
  }

  .sx-wrapper.is-dark :global(.sx__calendar-wrapper),
  .sx-wrapper.is-dark :global(.sx__month-grid-wrapper),
  .sx-wrapper.is-dark :global(.sx__week-grid),
  .sx-wrapper.is-dark :global(.sx__time-grid-wrapper) {
    background: oklch(0.129 0.042 264.695) !important;
  }

  .sx-wrapper.is-dark :global(.sx__calendar-header),
  .sx-wrapper.is-dark :global(.sx__date-grid-wrapper) {
    background: oklch(0.178 0.042 265.755) !important;
    border-color: #334155 !important;
  }

  .sx-wrapper.is-dark :global(.sx__month-grid-day),
  .sx-wrapper.is-dark :global(.sx__time-grid-day) {
    border-color: #334155 !important;
  }

  .sx-wrapper.is-dark :global(.sx__month-grid-cell) {
    border-color: #334155 !important;
  }

  .sx-wrapper.is-dark :global(button),
  .sx-wrapper.is-dark :global(.sx__date-grid-day) {
    color: #e2e8f0 !important;
  }

  .sx-wrapper.is-dark :global(.sx__time-grid-event),
  .sx-wrapper.is-dark :global(.sx__month-grid-event) {
    color: #e0f2fe !important;
  }

  /* Calendar Grid Hover States */
  .sx-wrapper :global(.sx__time-grid-day:hover) {
    background: rgba(59, 130, 246, 0.03);
    transition: background 0.15s ease;
  }

  .sx-wrapper.is-dark :global(.sx__time-grid-day:hover) {
    background: rgba(96, 165, 250, 0.08);
  }

  .sx-wrapper :global(.sx__month-grid-cell:hover) {
    background: rgba(59, 130, 246, 0.05);
    transition: background 0.15s ease;
    cursor: pointer;
  }

  .sx-wrapper.is-dark :global(.sx__month-grid-cell:hover) {
    background: rgba(96, 165, 250, 0.1);
  }

  .sx-wrapper :global(.sx__time-grid-event),
  .sx-wrapper :global(.sx__month-grid-event) {
    border-radius: 6px;
    transition:
      transform 0.15s ease,
      box-shadow 0.15s ease;
  }

  .sx-wrapper :global(.sx__time-grid-event:hover),
  .sx-wrapper :global(.sx__month-grid-event:hover) {
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    cursor: pointer;
  }

  .sx-wrapper.is-dark :global(.sx__time-grid-event:hover),
  .sx-wrapper.is-dark :global(.sx__month-grid-event:hover) {
    box-shadow: 0 4px 16px rgba(96, 165, 250, 0.25);
  }

  .sx-wrapper :global(.sx__date-grid-cell.sx__is-today) {
    font-weight: 700;
  }

  .sx-wrapper.is-dark :global(.sx__date-grid-cell.sx__is-today) {
    color: #60a5fa !important;
  }

  .sx-wrapper :global(.sx__month-grid-cell:focus-visible) {
    outline: 2px solid #3b82f6;
    outline-offset: -2px;
    background: rgba(59, 130, 246, 0.1);
  }

  .sx-wrapper.is-dark :global(.sx__month-grid-cell:focus-visible) {
    outline-color: #60a5fa;
    background: rgba(96, 165, 250, 0.15);
  }

  /* Fix view selector dropdown z-index - needs to be above sticky headers */
  .sx-wrapper :global(.sx__view-selection) {
    z-index: 100 !important;
    position: relative;
  }

  .sx-wrapper :global(.sx__view-selection-items) {
    z-index: 200 !important;
    position: absolute !important;
    top: 100% !important;
    right: 0;
    border-radius: 0.375rem;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  }

  .sx-wrapper :global(.sx__range-heading),
  .sx-wrapper :global([class*='view-selection']) {
    z-index: 100 !important;
    position: relative;
  }

  .sx-wrapper :global(.sx__calendar-header) {
    z-index: 50 !important;
    position: relative;
    overflow: visible !important;
  }

  /* Date picker popup fixes — remove extra padding, fix overflow scrollbar, dark mode border */
  .sx-wrapper :global(.sx__date-picker-popup) {
    overflow: hidden !important;
    border-radius: 0.5rem;
    border: 1px solid var(--sx-color-outline-variant, #c4c7c5);
    padding: 12px !important;
    width: auto !important;
    min-width: 18rem;
    max-width: 22rem;
  }
  .sx-wrapper.is-dark :global(.sx__date-picker-popup) {
    border-color: #334155 !important;
    background-color: oklch(0.178 0.042 265.755) !important;
  }
  /* Improve today circle contrast in dark mode */
  .sx-wrapper.is-dark :global(.sx__date-picker__day.sx__date-picker__day--today) {
    background-color: #7c3aed !important;
    color: #fff !important;
  }
  /* Improve today circle in light mode */
  .sx-wrapper :global(.sx__date-picker__day.sx__date-picker__day--today) {
    background-color: #6d28d9 !important;
    color: #fff !important;
  }
  /* Remove bottom margin on last week row to prevent extra space */
  .sx-wrapper :global(.sx__date-picker__week:last-child) {
    margin-bottom: 0 !important;
  }
</style>
