<script>
	import { m } from '$lib/js/fitnessI18n';
	import Trash2 from '@lucide/svelte/icons/trash-2';
	import Plus from '@lucide/svelte/icons/plus';
	import Pencil from '@lucide/svelte/icons/pencil';
	import UserPlus from '@lucide/svelte/icons/user-plus';
	import X from '@lucide/svelte/icons/x';
	import Check from '@lucide/svelte/icons/check';
	import ChevronLeft from '@lucide/svelte/icons/chevron-left';
	import ChevronRight from '@lucide/svelte/icons/chevron-right';
	import DatePicker from '$lib/components/DatePicker.svelte';
	import { toast } from '$lib/js/toast.svelte';
	import { confirm } from '$lib/js/confirmDialog.svelte';
	import ProfilePicture from '$lib/components/cospend/ProfilePicture.svelte';

	/**
	 * @type {{ periods: any[], lang: 'en' | 'de', sharedWith?: string[], readOnly?: boolean, ownerName?: string, mode?: 'entry' | 'projection' | 'full' }}
	 */
	let { periods: initialPeriods = [], lang = 'en', sharedWith: initialSharedWith = [], readOnly = false, ownerName = '', mode = 'full' } = $props();
	const t = $derived(m[lang]);
	const showEntry = $derived(mode !== 'projection');
	const showProjection = $derived(mode !== 'entry');

	// svelte-ignore state_referenced_locally
	let periods = $state([...initialPeriods]);
	let loading = $state(false);
	let showAddForm = $state(false);
	let addStart = $state('');
	let addEnd = $state('');

	// Edit state
	let editId = $state('');
	let editStart = $state('');
	let editEnd = $state('');

	// History collapsed by default
	let showHistory = $state(false);

	// Sharing state
	// svelte-ignore state_referenced_locally
	let shareList = $state([...initialSharedWith]);
	let showShare = $state(false);
	let shareInput = $state('');
	let shareSaving = $state(false);

	// Calendar navigation: month offset from today
	let calendarOffset = $state(0);

	const today = new Date();

	/** @param {Date} d */
	function fmtLocal(d) {
		return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
	}

	const todayStr = fmtLocal(today);

	/** Normalize any Date to local-midnight timestamp */
	/** @param {Date} dt */
	function midnight(dt) { return new Date(dt.getFullYear(), dt.getMonth(), dt.getDate()).getTime(); }

	/** Parse a date string (ISO or YYYY-MM-DD) to local-midnight timestamp */
	/** @param {string} s */
	function parseLocal(s) {
		const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/);
		if (m) return new Date(+m[1], +m[2] - 1, +m[3]).getTime();
		return midnight(new Date(s));
	}

	const todayMidnight = midnight(today);

	/** @param {string|Date} dateStr */
	function formatDate(dateStr) {
		const d = dateStr instanceof Date ? dateStr : new Date(dateStr);
		return d.toLocaleDateString(lang === 'de' ? 'de-DE' : 'en-US', {
			month: 'short', day: 'numeric'
		});
	}

	/** Format a date as a relative description, e.g. "Tuesday in 3 weeks" */
	/** @param {Date} date */
	function relativeDate(date) {
		const locale = lang === 'de' ? 'de-DE' : 'en-US';
		const dayName = date.toLocaleDateString(locale, { weekday: 'long' });
		const diffDays = Math.round((midnight(date) - todayMidnight) / 86400000);

		if (diffDays === 0) return lang === 'de' ? 'Heute' : 'Today';
		if (diffDays === 1) return lang === 'de' ? 'Morgen' : 'Tomorrow';
		if (diffDays === -1) return lang === 'de' ? 'Gestern' : 'Yesterday';

		if (diffDays > 0) {
			if (diffDays < 7) {
				return lang === 'de' ? `${dayName} (in ${diffDays} Tagen)` : `${dayName} (in ${diffDays} days)`;
			}
			const weeks = Math.ceil(diffDays / 7);
			const wLabel = weeks === 1
				? (lang === 'de' ? '1 Woche' : '1 week')
				: (lang === 'de' ? `${weeks} Wochen` : `${weeks} weeks`);
			return lang === 'de' ? `${dayName} in ${wLabel}` : `${dayName} in ${wLabel}`;
		}

		const absDays = Math.abs(diffDays);
		if (absDays < 7) {
			return lang === 'de' ? `${dayName} (vor ${absDays} Tagen)` : `${dayName} (${absDays} days ago)`;
		}
		const weeks = Math.ceil(absDays / 7);
		const wLabel = weeks === 1
			? (lang === 'de' ? '1 Woche' : '1 week')
			: (lang === 'de' ? `${weeks} Wochen` : `${weeks} weeks`);
		return lang === 'de' ? `${dayName} vor ${wLabel}` : `${dayName} ${wLabel} ago`;
	}

	/** Short relative range: "Tuesday to Saturday in 3 weeks" */
	/** @param {Date} start @param {Date} end */
	function relativeRange(start, end) {
		const locale = lang === 'de' ? 'de-DE' : 'en-US';
		const startDay = start.toLocaleDateString(locale, { weekday: 'long' });
		const endDay = end.toLocaleDateString(locale, { weekday: 'long' });
		const diffDays = Math.round((midnight(start) - todayMidnight) / 86400000);
		const toWord = t.to;

		if (diffDays >= 0 && diffDays < 7) {
			return lang === 'de'
				? `${startDay} ${toWord} ${endDay} (in ${diffDays} Tagen)`
				: `${startDay} ${toWord} ${endDay} (in ${diffDays} days)`;
		}
		if (diffDays >= 7) {
			const weeks = Math.ceil(diffDays / 7);
			const wLabel = weeks === 1
				? (lang === 'de' ? '1 Woche' : '1 week')
				: (lang === 'de' ? `${weeks} Wochen` : `${weeks} weeks`);
			return lang === 'de'
				? `${startDay} ${toWord} ${endDay} in ${wLabel}`
				: `${startDay} ${toWord} ${endDay} in ${wLabel}`;
		}
		return `${startDay} ${toWord} ${endDay}`;
	}

	// Sort periods newest first
	const sorted = $derived([...periods].sort((a, b) =>
		new Date(b.startDate).getTime() - new Date(a.startDate).getTime()
	));

	// Ongoing period (no endDate)
	const ongoing = $derived(sorted.find(p => !p.endDate));

	// Completed periods (have endDate), oldest first for EMA
	const completed = $derived(
		sorted.filter(p => p.endDate).sort((a, b) =>
			new Date(a.startDate).getTime() - new Date(b.startDate).getTime()
		)
	);

	// EMA predictions (α = 0.3, population priors: 29-day cycle, 5-day period)
	const predictions = $derived.by(() => {
		const alpha = 0.3;
		let emaCycle = 29;
		let emaPeriod = 5;

		/** @type {number[]} */
		const cycleLengths = [];
		/** @type {number[]} */
		const periodLengths = [];

		for (let i = 0; i < completed.length; i++) {
			const p = completed[i];
			const start = new Date(p.startDate);
			const end = new Date(p.endDate);
			const dur = Math.round((end.getTime() - start.getTime()) / 86400000) + 1;
			emaPeriod = alpha * dur + (1 - alpha) * emaPeriod;
			periodLengths.push(dur);

			if (i > 0) {
				const prevStart = new Date(completed[i - 1].startDate);
				const cycle = Math.round((start.getTime() - prevStart.getTime()) / 86400000);
				if (cycle > 0 && cycle < 60) {
					emaCycle = alpha * cycle + (1 - alpha) * emaCycle;
					cycleLengths.push(cycle);
				}
			}
		}

		/** 95% CI half-width: 1.96 * s / √n */
		/** @param {number[]} arr */
		function ci95(arr) {
			if (arr.length < 2) return 0;
			const mean = arr.reduce((a, b) => a + b, 0) / arr.length;
			const variance = arr.reduce((sum, v) => sum + (v - mean) ** 2, 0) / (arr.length - 1);
			return Math.round(1.96 * Math.sqrt(variance / arr.length) * 10) / 10;
		}

		const cycleVariance = ci95(cycleLengths);
		const periodVariance = ci95(periodLengths);

		// Predict ongoing end
		let predictedEndOfOngoing = null;
		if (ongoing) {
			const ongoingStart = new Date(ongoing.startDate);
			predictedEndOfOngoing = new Date(ongoingStart.getTime() + (Math.round(emaPeriod) - 1) * 86400000);
		}

		// Generate future predicted cycles (12 cycles ≈ ~1 year)
		const meanCycleDays = Math.round(emaCycle);
		const cycleMs = meanCycleDays * 86400000;
		const periodMs = (Math.round(emaPeriod) - 1) * 86400000;
		const lutealLength = 14;
		const lastStart = sorted[0] ? new Date(sorted[0].startDate) : null;

		// Cycle range for Ogino-style widening of future fertile windows.
		// Without ≥2 observed cycles, widening collapses to a point estimate.
		const shortestCycle = cycleLengths.length >= 2 ? Math.min(...cycleLengths) : meanCycleDays;
		const longestCycle = cycleLengths.length >= 2 ? Math.max(...cycleLengths) : meanCycleDays;

		/**
		 * Build a fertility window for one cycle.
		 *
		 * Anchor: the next period's start (luteal-back-count). Past cycles know it
		 * exactly; future cycles use the mean prediction and widen the outer fertile
		 * range to cover the earliest/latest historically observed ovulation day.
		 *
		 * Floor: fertile/peak never overlap the prior bleed. Day-after-period-end
		 * is the earliest possible fertile day shown — a hard biological floor for
		 * the user's mental model, even though sperm survival could in theory begin
		 * earlier in the bleed for very short cycles.
		 *
		 * @param {number} cycleStartMs   ms of cycle start (= prior period start)
		 * @param {number | null} priorPeriodEndMs  ms of prior bleed end, or null if unknown
		 * @param {number} nextPeriodStartMs  ms of the next period's start
		 * @param {boolean} widen  true → use shortest/longest cycle bounds; false → point estimate
		 */
		function buildWindow(cycleStartMs, priorPeriodEndMs, nextPeriodStartMs, widen) {
			const ovMs = nextPeriodStartMs - lutealLength * 86400000;
			const earliestOvMs = widen
				? cycleStartMs + (shortestCycle - lutealLength) * 86400000
				: ovMs;
			let latestOvMs = widen
				? cycleStartMs + (longestCycle - lutealLength) * 86400000
				: ovMs;
			// Cap latest ov before the next bleed starts.
			if (latestOvMs > nextPeriodStartMs - 86400000) latestOvMs = nextPeriodStartMs - 86400000;

			const floorMs = priorPeriodEndMs !== null ? priorPeriodEndMs + 86400000 : cycleStartMs;

			let fertileStartMs = Math.max(earliestOvMs - 5 * 86400000, floorMs, cycleStartMs);
			let peakStartMs = Math.max(ovMs - 2 * 86400000, floorMs, cycleStartMs);
			const peakEndMs = ovMs - 86400000;
			let fertileEndMs = Math.max(latestOvMs, ovMs);

			// Suppress peak if floor pushed it past ov (e.g. very short cycle + long period).
			if (peakStartMs > peakEndMs) peakStartMs = peakEndMs + 86400000;
			// Keep fertile envelope around peak/ov.
			if (fertileStartMs > peakStartMs && peakStartMs <= peakEndMs) fertileStartMs = peakStartMs;

			return {
				fertileStart: new Date(fertileStartMs),
				fertileEnd: new Date(fertileEndMs),
				peakStart: new Date(peakStartMs),
				peakEnd: new Date(peakEndMs),
				ovulation: new Date(ovMs),
				lutealStart: new Date(latestOvMs + 86400000),
				lutealEnd: new Date(nextPeriodStartMs - 86400000)
			};
		}

		/** @type {{ start: Date, end: Date, fertileStart: Date, fertileEnd: Date, peakStart: Date, peakEnd: Date, ovulation: Date, lutealStart: Date, lutealEnd: Date }[]} */
		const futureCycles = [];
		if (lastStart) {
			let base = lastStart.getTime();
			// Prior bleed end for the first predicted cycle: actual end if recorded,
			// predicted end if ongoing, else cycle start.
			let priorPeriodEndMs;
			if (sorted[0]?.endDate) {
				priorPeriodEndMs = midnight(new Date(sorted[0].endDate));
			} else if (predictedEndOfOngoing) {
				priorPeriodEndMs = midnight(predictedEndOfOngoing);
			} else {
				priorPeriodEndMs = base;
			}

			for (let i = 0; i < 12; i++) {
				const nextPeriodStartMs = base + cycleMs;
				const periodEndMs = nextPeriodStartMs + periodMs;
				const w = buildWindow(base, priorPeriodEndMs, nextPeriodStartMs, /* widen */ true);
				futureCycles.push({
					start: new Date(nextPeriodStartMs),
					end: new Date(periodEndMs),
					...w
				});
				base = nextPeriodStartMs;
				priorPeriodEndMs = periodEndMs;
			}
		}

		// Past fertility/luteal windows (from completed cycles)
		/** @type {{ fertileStart: Date, fertileEnd: Date, peakStart: Date, peakEnd: Date, ovulation: Date, lutealStart: Date, lutealEnd: Date }[]} */
		const pastFertileWindows = [];
		for (let i = 1; i < completed.length; i++) {
			const cycleStartMs = midnight(new Date(completed[i - 1].startDate));
			const priorPeriodEndMs = completed[i - 1].endDate
				? midnight(new Date(completed[i - 1].endDate))
				: null;
			const nextPeriodStartMs = midnight(new Date(completed[i].startDate));
			pastFertileWindows.push(buildWindow(cycleStartMs, priorPeriodEndMs, nextPeriodStartMs, /* widen */ false));
		}

		return {
			avgCycle: Math.round(emaCycle * 10) / 10,
			avgPeriod: Math.round(emaPeriod * 10) / 10,
			cycleVariance,
			periodVariance,
			predictedEndOfOngoing,
			futureCycles,
			pastFertileWindows
		};
	});

	// First future cycle (for status display)
	const nextCycle = $derived(predictions.futureCycles[0] ?? null);

	// Days into current period
	const ongoingDay = $derived.by(() => {
		if (!ongoing) return 0;
		const start = parseLocal(ongoing.startDate);
		return Math.floor((todayMidnight - start) / 86400000) + 1;
	});

	// Calendar data
	const calendarMonth = $derived.by(() => {
		const d = new Date(today.getFullYear(), today.getMonth() + calendarOffset, 1);
		return { year: d.getFullYear(), month: d.getMonth() };
	});

	const calendarLabel = $derived(
		new Date(calendarMonth.year, calendarMonth.month).toLocaleDateString(
			lang === 'de' ? 'de-DE' : 'en-US', { month: 'long', year: 'numeric' }
		)
	);

	const calendarDays = $derived.by(() => {
		const { year, month } = calendarMonth;
		const first = new Date(year, month, 1);
		const last = new Date(year, month + 1, 0);
		const startDay = (first.getDay() + 6) % 7; // Monday = 0

		// Build raw cells with status, including overflow days from adjacent months
		/** @type {{ day: number, date: string, status: string, pos: string, edges: string, overflow?: boolean }[]} */
		const cells = [];

		// Previous month overflow
		if (startDay > 0) {
			const prevLast = new Date(year, month, 0); // last day of previous month
			for (let i = startDay - 1; i >= 0; i--) {
				const d = prevLast.getDate() - i;
				const pm = new Date(year, month - 1, d);
				const date = fmtLocal(pm);
				cells.push({ day: d, date, status: getDayStatus(date), pos: '', edges: '', overflow: true });
			}
		}

		// Current month
		for (let d = 1; d <= last.getDate(); d++) {
			const date = `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
			cells.push({ day: d, date, status: getDayStatus(date), pos: '', edges: '' });
		}

		// Next month overflow to complete the last row
		const remainder = cells.length % 7;
		if (remainder > 0) {
			const fill = 7 - remainder;
			for (let d = 1; d <= fill; d++) {
				const nm = new Date(year, month + 1, d);
				const date = fmtLocal(nm);
				cells.push({ day: d, date, status: getDayStatus(date), pos: '', edges: '', overflow: true });
			}
		}

		// Status grouping: these flow into each other without rounding at boundaries
		/** @param {string} s */
		function statusGroup(s) {
			if (s === 'fertile' || s === 'peak-fertile' || s === 'ovulation') return 'fertility';
			return s;
		}

		// Sub-grouping: peak-fertile and ovulation form one pill
		/** @param {string} s */
		function posGroup(s) {
			if (s === 'peak-fertile' || s === 'ovulation') return 'peak';
			return s;
		}

		// Compute range positions and edge flags
		// Position (border-radius): posGroup for left (peak-fertile+ovulation = one pill,
		//   but fertile is separate so peak-fertile gets rounded left cap next to fertile)
		//   statusGroup for right (fertile stays flat on right next to peak-fertile)
		// Edges (border sides): posGroup for left, statusGroup for right/up/down
		for (let i = 0; i < cells.length; i++) {
			const c = cells[i];
			if (!c || !c.status) continue;
			const g = statusGroup(c.status);
			const pg = posGroup(c.status);
			const col = i % 7;
			const leftSamePos = col > 0 && posGroup(cells[i - 1]?.status ?? '') === pg;
			const rightGroup = col < 6 && i + 1 < cells.length && statusGroup(cells[i + 1]?.status ?? '') === g;
			const upGroup = i >= 7 && statusGroup(cells[i - 7]?.status ?? '') === g;
			const downGroup = i + 7 < cells.length && statusGroup(cells[i + 7]?.status ?? '') === g;

			// Horizontal position for left/right rounding
			if (!leftSamePos && !rightGroup) c.pos = 'solo';
			else if (!leftSamePos) c.pos = 'start';
			else if (!rightGroup) c.pos = 'end';
			else c.pos = 'mid';

			// If range continues across row boundary (col 6 → col 0), flatten the corners
			// Right/next: statusGroup (fertile stays flat before peak-fertile)
			// Left/prev: posGroup (peak-fertile keeps rounded cap after fertile)
			const nextDaySameGroup = col === 6 && i + 1 < cells.length && statusGroup(cells[i + 1]?.status ?? '') === g;
			const prevDaySamePos = col === 0 && i - 1 >= 0 && posGroup(cells[i - 1]?.status ?? '') === pg;
			if (nextDaySameGroup) {
				if (c.pos === 'solo') c.pos = 'start';
				else if (c.pos === 'end') c.pos = 'mid';
			}
			if (prevDaySamePos) {
				if (c.pos === 'start') c.pos = 'mid';
				else if (c.pos === 'solo') c.pos = 'end';
			}

			// Edge flags for border rendering
			// Left: posGroup match (peak-fertile draws left border next to fertile)
			// Right/Up/Down: group match (borders flow within group)
			const edges = [];
			if (!upGroup) edges.push('et');
			if (!downGroup) edges.push('eb');
			// At col 0, also check cross-row prev day with posGroup
			const leftSameEdge = leftSamePos || prevDaySamePos;
			if (!leftSameEdge) edges.push('el');
			// At col 6, also check cross-row next day with statusGroup
			const rightSameEdge = rightGroup || nextDaySameGroup;
			if (!rightSameEdge) edges.push('er');
			c.edges = edges.join(' ');
		}

		return cells;
	});

	/** @param {string} dateStr */
	function getDayStatus(dateStr) {
		const d = parseLocal(dateStr);

		// Period days (actual)
		for (const p of periods) {
			const start = midnight(new Date(p.startDate));
			const end = p.endDate ? midnight(new Date(p.endDate)) : todayMidnight;
			if (d >= start && d <= end) return 'period';
		}

		// Predicted ongoing end
		if (ongoing && predictions.predictedEndOfOngoing) {
			const ongoingEnd = midnight(predictions.predictedEndOfOngoing);
			const ongoingStart = midnight(new Date(ongoing.startDate));
			if (d > todayMidnight && d >= ongoingStart && d <= ongoingEnd) return 'predicted';
		}

		// Future predicted cycles
		for (const c of predictions.futureCycles) {
			const cs = midnight(c.start);
			const ce = midnight(c.end);
			if (d >= cs && d <= ce) return 'predicted';
			const ovDay = midnight(c.ovulation);
			if (d === ovDay) return 'ovulation';
			const ps = midnight(c.peakStart);
			const pe = midnight(c.peakEnd);
			if (d >= ps && d <= pe) return 'peak-fertile';
			const fs = midnight(c.fertileStart);
			const fe = midnight(c.fertileEnd);
			if (d >= fs && d <= fe) return 'fertile';
			const ls = midnight(c.lutealStart);
			const le = midnight(c.lutealEnd);
			if (d >= ls && d <= le) return 'luteal';
		}

		// Past fertility/luteal windows
		for (const w of predictions.pastFertileWindows) {
			const ovDay = midnight(w.ovulation);
			if (d === ovDay) return 'ovulation';
			const ps = midnight(w.peakStart);
			const pe = midnight(w.peakEnd);
			if (d >= ps && d <= pe) return 'peak-fertile';
			const fs = midnight(w.fertileStart);
			const fe = midnight(w.fertileEnd);
			if (d >= fs && d <= fe) return 'fertile';
			const ls = midnight(w.lutealStart);
			const le = midnight(w.lutealEnd);
			if (d >= ls && d <= le) return 'luteal';
		}

		return '';
	}

	async function startPeriod() {
		loading = true;
		try {
			const res = await fetch('/api/fitness/period', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ startDate: new Date().toISOString() })
			});
			if (res.ok) {
				const { entry } = await res.json();
				periods = [entry, ...periods];
			} else {
				const err = await res.json().catch(() => null);
				toast.error(err?.error ?? 'Failed to start period');
			}
		} catch { toast.error('Failed to start period'); }
		finally { loading = false; }
	}

	/** @param {string} dateStr — YYYY-MM-DD from a calendar cell */
	async function promptStartPeriodOn(dateStr) {
		const d = new Date(parseLocal(dateStr));
		const ok = await confirm(
			lang === 'de'
				? `Periode am ${formatDate(d)} starten?`
				: `Start period on ${formatDate(d)}?`,
			{
				title: lang === 'de' ? 'Periode starten' : 'Start period',
				confirmText: lang === 'de' ? 'Starten' : 'Start',
				cancelText: lang === 'de' ? 'Abbrechen' : 'Cancel',
				destructive: false
			}
		);
		if (!ok) return;
		loading = true;
		try {
			const res = await fetch('/api/fitness/period', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ startDate: d.toISOString() })
			});
			if (res.ok) {
				const { entry } = await res.json();
				periods = [entry, ...periods];
			} else {
				const err = await res.json().catch(() => null);
				toast.error(err?.error ?? 'Failed to start period');
			}
		} catch { toast.error('Failed to start period'); }
		finally { loading = false; }
	}

	/**
	 * Long-press attachment. Fires `handler` after THRESHOLD ms of unmoving
	 * pointer contact. Cancels on movement > MOVE_TOL, pointer leave/cancel,
	 * or release before threshold. Suppresses the browser context menu when
	 * the gesture fires (iOS otherwise pops a callout on touch hold).
	 *
	 * @param {() => void} handler
	 * @returns {import('svelte/attachments').Attachment<HTMLElement>}
	 */
	function longPress(handler) {
		const THRESHOLD = 600;
		const MOVE_TOL = 8;
		return (node) => {
			/** @type {number | null} */
			let timer = null;
			let startX = 0;
			let startY = 0;
			let firing = false;

			function clear() {
				if (timer !== null) {
					clearTimeout(timer);
					timer = null;
				}
				node.classList.remove('long-pressing');
			}

			/** @param {PointerEvent} e */
			function onPointerDown(e) {
				if (e.button !== undefined && e.button !== 0) return;
				startX = e.clientX;
				startY = e.clientY;
				firing = false;
				node.classList.add('long-pressing');
				timer = window.setTimeout(() => {
					firing = true;
					node.classList.remove('long-pressing');
					timer = null;
					handler();
				}, THRESHOLD);
			}

			/** @param {PointerEvent} e */
			function onPointerMove(e) {
				if (timer === null) return;
				if (Math.abs(e.clientX - startX) > MOVE_TOL || Math.abs(e.clientY - startY) > MOVE_TOL) {
					clear();
				}
			}

			/** @param {Event} e */
			function onContextMenu(e) {
				if (firing) {
					e.preventDefault();
					firing = false;
				}
			}

			node.addEventListener('pointerdown', onPointerDown);
			node.addEventListener('pointermove', onPointerMove);
			node.addEventListener('pointerup', clear);
			node.addEventListener('pointerleave', clear);
			node.addEventListener('pointercancel', clear);
			node.addEventListener('contextmenu', onContextMenu);

			return () => {
				clear();
				node.removeEventListener('pointerdown', onPointerDown);
				node.removeEventListener('pointermove', onPointerMove);
				node.removeEventListener('pointerup', clear);
				node.removeEventListener('pointerleave', clear);
				node.removeEventListener('pointercancel', clear);
				node.removeEventListener('contextmenu', onContextMenu);
			};
		};
	}

	/** Whether long-pressing the given calendar cell can start a period. */
	/** @param {{ date: string, status: string }} cell */
	function canStartOn(cell) {
		if (readOnly || !showEntry) return false;
		if (ongoing) return false;
		if (cell.status === 'period') return false;
		return parseLocal(cell.date) <= todayMidnight;
	}

	async function endPeriod() {
		if (!ongoing) return;
		loading = true;
		try {
			const res = await fetch(`/api/fitness/period/${ongoing._id}`, {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ endDate: new Date(Date.now() - 86400000).toISOString() })
			});
			if (res.ok) {
				const { entry } = await res.json();
				periods = periods.map(p => p._id === entry._id ? entry : p);
			} else {
				const err = await res.json().catch(() => null);
				toast.error(err?.error ?? 'Failed to end period');
			}
		} catch { toast.error('Failed to end period'); }
		finally { loading = false; }
	}

	async function addPastPeriod() {
		if (!addStart) return;
		loading = true;
		try {
			const body = { startDate: new Date(addStart).toISOString() };
			if (addEnd) Object.assign(body, { endDate: new Date(addEnd).toISOString() });
			const res = await fetch('/api/fitness/period', {
				method: 'POST',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify(body)
			});
			if (res.ok) {
				const { entry } = await res.json();
				periods = [entry, ...periods];
				showAddForm = false;
				addStart = '';
				addEnd = '';
			} else {
				const err = await res.json().catch(() => null);
				toast.error(err?.error ?? 'Failed to add period');
			}
		} catch { toast.error('Failed to add period'); }
		finally { loading = false; }
	}

	/** @param {any} p */
	function startEdit(p) {
		editId = p._id;
		editStart = fmtLocal(new Date(p.startDate));
		editEnd = p.endDate ? fmtLocal(new Date(p.endDate)) : '';
	}

	function cancelEdit() {
		editId = '';
		editStart = '';
		editEnd = '';
	}

	async function saveEdit() {
		if (!editStart) return;
		loading = true;
		try {
			/** @type {Record<string, unknown>} */
			const body = { startDate: new Date(editStart).toISOString() };
			body.endDate = editEnd ? new Date(editEnd).toISOString() : null;
			const res = await fetch(`/api/fitness/period/${editId}`, {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify(body)
			});
			if (res.ok) {
				const { entry } = await res.json();
				periods = periods.map(p => p._id === editId ? entry : p);
				cancelEdit();
			} else {
				const err = await res.json().catch(() => null);
				toast.error(err?.error ?? 'Failed to update period');
			}
		} catch { toast.error('Failed to update period'); }
		finally { loading = false; }
	}

	/** @param {string} id */
	async function deletePeriod(id) {
		if (!await confirm(t.delete_period_confirm)) return;
		try {
			const res = await fetch(`/api/fitness/period/${id}`, { method: 'DELETE' });
			if (res.ok) {
				periods = periods.filter(p => p._id !== id);
			} else {
				toast.error('Failed to delete period');
			}
		} catch { toast.error('Failed to delete period'); }
	}

	async function updateShareList(/** @type {string[]} */ newList) {
		shareSaving = true;
		try {
			const res = await fetch('/api/fitness/period/share', {
				method: 'PUT',
				headers: { 'Content-Type': 'application/json' },
				body: JSON.stringify({ sharedWith: newList })
			});
			if (res.ok) {
				const data = await res.json();
				shareList = data.sharedWith;
			} else {
				toast.error('Failed to update sharing');
			}
		} catch { toast.error('Failed to update sharing'); }
		finally { shareSaving = false; }
	}

	function addShareUser() {
		const name = shareInput.trim().toLowerCase();
		if (!name || shareList.includes(name)) return;
		shareInput = '';
		updateShareList([...shareList, name]);
	}

	/** @param {string} name */
	function removeShareUser(name) {
		updateShareList(shareList.filter(u => u !== name));
	}

	const weekDays = $derived(
		lang === 'de'
			? ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So']
			: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
	);
</script>

<section class="period-tracker" class:read-only={readOnly}>
	<h2>
		{#if readOnly && ownerName}
			<span class="shared-header">
				<ProfilePicture username={ownerName} size={24} />
				{ownerName}
			</span>
		{:else}
			{t.period_tracker}
		{/if}
	</h2>

	<!-- Status card -->
	<div class="status-card">
		{#if ongoing}
			<div class="status-split">
				<div class="status-main">
					<span class="status-pill period-pill">{t.current_period}</span>
					<span class="status-hero ongoing-hero">{t.period_day} {ongoingDay}</span>
					{#if showProjection && predictions.predictedEndOfOngoing}
						<span class="status-detail">{t.predicted_end}</span>
						<span class="status-relative">{relativeDate(predictions.predictedEndOfOngoing)}</span>
						<span class="status-date">{formatDate(predictions.predictedEndOfOngoing)}</span>
					{/if}
					{#if showEntry && !readOnly}
						<button class="end-btn" onclick={endPeriod} disabled={loading}>
							<span class="end-btn-icon"><Check size={18} strokeWidth={2.5} /></span>
							<span class="end-btn-label">{t.end_period}</span>
						</button>
					{/if}
				</div>
				{#if showProjection && nextCycle}
					<div class="status-side">
						<div class="status-side-item ovulation-accent">
							<span class="status-side-label">{t.ovulation}</span>
							<span class="status-side-relative">{relativeDate(nextCycle.ovulation)}</span>
							<span class="status-side-date">{formatDate(nextCycle.ovulation)}</span>
						</div>
						<div class="status-side-item fertile-accent">
							<span class="status-side-label">{t.fertile}</span>
							<span class="status-side-date">{formatDate(nextCycle.fertileStart)} — {formatDate(nextCycle.fertileEnd)}</span>
						</div>
					</div>
				{/if}
			</div>
		{:else if showProjection && nextCycle}
			<div class="status-split">
				<div class="status-main">
					<span class="status-pill period-pill">{t.next_period}</span>
					<span class="status-hero">{relativeRange(nextCycle.start, nextCycle.end)}</span>
					<span class="status-date">{formatDate(nextCycle.start)} — {formatDate(nextCycle.end)}</span>
					{#if showEntry && !readOnly}
						<button class="start-btn" onclick={startPeriod} disabled={loading}>
							{t.start_period}
						</button>
					{/if}
				</div>
				<div class="status-side">
					<div class="status-side-item ovulation-accent">
						<span class="status-side-label">{t.ovulation}</span>
						<span class="status-side-relative">{relativeDate(nextCycle.ovulation)}</span>
						<span class="status-side-date">{formatDate(nextCycle.ovulation)}</span>
					</div>
					<div class="status-side-item fertile-accent">
						<span class="status-side-label">{t.fertile}</span>
						<span class="status-side-date">{formatDate(nextCycle.fertileStart)} — {formatDate(nextCycle.fertileEnd)}</span>
					</div>
				</div>
			</div>
		{:else if showEntry}
			<div class="status-block">
				<span class="status-empty">{sorted.length === 0 ? t.no_period_data : t.no_active_period}</span>
				{#if !readOnly}
					<button class="start-btn" onclick={startPeriod} disabled={loading}>
						{t.start_period}
					</button>
				{/if}
			</div>
		{/if}
	</div>

	{#if showProjection}
	<!-- Calendar -->
	<div class="calendar">
		<div class="cal-header">
			<button class="cal-nav" onclick={() => calendarOffset--}>
				<ChevronLeft size={16} />
			</button>
			<span class="cal-title">{calendarLabel}</span>
			<button class="cal-nav" onclick={() => calendarOffset++} disabled={calendarOffset >= 12}>
				<ChevronRight size={16} />
			</button>
			{#if calendarOffset !== 0}
				<button class="go-today-btn" onclick={() => calendarOffset = 0}>{lang === 'de' ? 'Heute' : 'Today'}</button>
			{/if}
		</div>
		<div class="cal-weekdays">
			{#each weekDays as wd}
				<span class="cal-wd">{wd}</span>
			{/each}
		</div>
		<div class="cal-grid">
			{#each calendarDays as cell}
				{@const startable = canStartOn(cell)}
				<span
					class="cal-day {cell.status ? `s-${cell.status}` : ''} {cell.pos ? `p-${cell.pos}` : ''} {cell.edges}"
					class:today={cell.date === todayStr}
					class:overflow={cell.overflow}
					class:startable
					{@attach startable && longPress(() => promptStartPeriodOn(cell.date))}
				>{cell.day}</span>
			{/each}
		</div>
		<div class="cal-legend">
			<span class="legend-item"><span class="legend-dot period"></span> {lang === 'de' ? 'Periode' : 'Period'}</span>
			<span class="legend-item"><span class="legend-dot predicted"></span> {lang === 'de' ? 'Prognose' : 'Predicted'}</span>
			<span class="legend-item"><span class="legend-dot fertile"></span> {t.fertile}</span>
			<span class="legend-item"><span class="legend-dot peak-fertile"></span> {t.peak_fertility}</span>
			<span class="legend-item"><span class="legend-dot ovulation"></span> {t.ovulation}</span>
			<span class="legend-item"><span class="legend-dot luteal"></span> {t.luteal_phase}</span>
		</div>
	</div>

	{/if}

	{#if showProjection && completed.length >= 2}
		<div class="cycle-stats">
			<div class="cycle-stat">
				<span class="cycle-stat-label">{t.cycle_length}</span>
				<span class="cycle-stat-value">{Math.round(predictions.avgCycle)} {t.days}</span>
				{#if predictions.cycleVariance > 0}
					<span class="cycle-stat-variance">± {predictions.cycleVariance} {t.days} (95% CI)</span>
				{/if}
			</div>
			<div class="cycle-stat">
				<span class="cycle-stat-label">{t.period_length}</span>
				<span class="cycle-stat-value">{Math.round(predictions.avgPeriod)} {t.days}</span>
				{#if predictions.periodVariance > 0}
					<span class="cycle-stat-variance">± {predictions.periodVariance} {t.days} (95% CI)</span>
				{/if}
			</div>
		</div>
	{/if}

	{#if showEntry && !readOnly}
		<!-- History + Share row -->
		{#if sorted.length > 0}
			<div class="history">
				<div class="history-share-row">
					<button class="history-toggle" onclick={() => showHistory = !showHistory}>
						<h3>{t.history}</h3>
						<ChevronRight size={14} class={showHistory ? 'chevron open' : 'chevron'} />
					</button>
					<div class="share-bar">
						{#if shareList.length > 0}
							<div class="shared-avatars">
								<span class="shared-label">{t.shared_with}</span>
								{#each shareList as user}
									<div class="shared-avatar" title={user}>
										<ProfilePicture username={user} size={28} />
									</div>
								{/each}
							</div>
						{/if}
						<button class="share-btn" onclick={() => showShare = true} aria-label={t.share}>
							<UserPlus size={16} />
						</button>
					</div>
				</div>

				{#if showHistory}
					<div class="history-header">
						<button class="add-past-btn" onclick={() => showAddForm = !showAddForm}>
							<Plus size={14} />
							{t.add_past_period}
						</button>
					</div>

					{#if showAddForm}
						<div class="add-form">
							<div class="add-row">
								<label>
									{t.period_start}
									<DatePicker bind:value={addStart} max={todayStr} {lang} />
								</label>
								<label>
									{t.period_end}
									<DatePicker bind:value={addEnd} min={addStart} max={todayStr} {lang} />
								</label>
							</div>
							<div class="add-actions">
								<button class="save-btn" onclick={addPastPeriod} disabled={!addStart || loading}>
									{t.save}
								</button>
								<button class="cancel-btn" onclick={() => { showAddForm = false; addStart = ''; addEnd = ''; }}>
									{t.cancel}
								</button>
							</div>
						</div>
					{/if}

					<div class="history-list">
					{#each sorted as p (p._id)}
						{#if editId === p._id}
							<div class="history-item editing">
								<div class="add-row">
									<label>
										{t.period_start}
										<DatePicker bind:value={editStart} {lang} />
									</label>
									<label>
										{t.period_end}
										<DatePicker bind:value={editEnd} min={editStart} {lang} />
									</label>
								</div>
								<div class="add-actions">
									<button class="save-btn" onclick={saveEdit} disabled={!editStart || loading}>
										{t.save}
									</button>
									<button class="cancel-btn" onclick={cancelEdit}>
										{t.cancel}
									</button>
								</div>
							</div>
						{:else}
							<div class="history-item">
								<div class="history-info">
									<span class="history-dates">
										{formatDate(p.startDate)}
										{#if p.endDate}
											— {formatDate(p.endDate)}
										{:else}
											<span class="ongoing-badge">{t.ongoing}</span>
										{/if}
									</span>
									{#if p.endDate}
										{@const dur = Math.round((new Date(p.endDate).getTime() - new Date(p.startDate).getTime()) / 86400000) + 1}
										<span class="history-dur">{dur} {t.days}</span>
									{/if}
								</div>
								<div class="history-actions">
									<button class="icon-btn edit" onclick={() => startEdit(p)} aria-label="Edit">
										<Pencil size={14} />
									</button>
									<button class="icon-btn delete" onclick={() => deletePeriod(p._id)} aria-label="Delete">
										<Trash2 size={14} />
									</button>
								</div>
							</div>
						{/if}
					{/each}
				</div>
				{/if}
			</div>
		{:else}
			<div class="empty-state">
				<div class="share-bar">
					<p>{t.no_period_data}</p>
					<button class="share-btn" onclick={() => showShare = true} aria-label={t.share}>
						<UserPlus size={16} />
					</button>
				</div>
				<button class="add-past-btn" onclick={() => showAddForm = !showAddForm}>
					<Plus size={14} />
					{t.add_past_period}
				</button>
				{#if showAddForm}
					<div class="add-form">
						<div class="add-row">
							<label>
								{t.period_start}
								<DatePicker bind:value={addStart} max={todayStr} {lang} />
							</label>
							<label>
								{t.period_end}
								<DatePicker bind:value={addEnd} min={addStart} max={todayStr} {lang} />
							</label>
						</div>
						<div class="add-actions">
							<button class="save-btn" onclick={addPastPeriod} disabled={!addStart || loading}>
								{t.save}
							</button>
							<button class="cancel-btn" onclick={() => { showAddForm = false; addStart = ''; addEnd = ''; }}>
								{t.cancel}
							</button>
						</div>
					</div>
				{/if}
			</div>
		{/if}

	{/if}
</section>

<!-- Share modal -->
{#if showShare}
	<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
	<div class="share-overlay" onclick={() => showShare = false} onkeydown={(e) => e.key === 'Escape' && (showShare = false)}>
		<!-- svelte-ignore a11y_no_static_element_interactions a11y_click_events_have_key_events -->
		<div class="share-modal" role="presentation" onclick={(e) => e.stopPropagation()}>
			<div class="share-modal-header">
				<h3>{t.share}</h3>
				<button class="share-modal-close" onclick={() => showShare = false}>
					<X size={16} />
				</button>
			</div>
			{#if shareList.length > 0}
				<div class="share-user-list">
					{#each shareList as user}
						<div class="share-user">
							<ProfilePicture username={user} size={32} />
							<span class="share-username">{user}</span>
							<button class="chip-remove" onclick={() => removeShareUser(user)} disabled={shareSaving} aria-label="Remove {user}">
								<X size={12} />
							</button>
						</div>
					{/each}
				</div>
			{:else}
				<span class="share-empty">{t.no_shared}</span>
			{/if}
			<form class="share-add" onsubmit={(e) => { e.preventDefault(); addShareUser(); }}>
				<input
					type="text"
					bind:value={shareInput}
					placeholder={t.add_user}
					disabled={shareSaving}
				/>
				<button type="submit" class="share-add-btn" disabled={!shareInput.trim() || shareSaving}>
					<Plus size={14} />
				</button>
			</form>
		</div>
	</div>
{/if}

<style>
	.period-tracker {
		display: flex;
		flex-direction: column;
		gap: 0.75rem;
	}
	.period-tracker h2 {
		margin: 0;
		font-size: 1.1rem;
	}

	/* Status card — split columns */
	.status-card {
		background: var(--color-surface);
		border-radius: 10px;
		box-shadow: var(--shadow-sm);
		padding: 1rem 1.1rem;
	}
	.status-split {
		display: flex;
		gap: 1rem;
	}
	.status-main {
		flex: 1;
		display: flex;
		flex-direction: column;
		gap: 0.15rem;
		min-width: 0;
	}
	.status-side {
		flex-shrink: 0;
		display: flex;
		flex-direction: column;
		gap: 0.6rem;
		padding-left: 1rem;
		border-left: 1px solid var(--color-border);
		justify-content: center;
	}
	.status-block {
		display: flex;
		flex-direction: column;
		gap: 0.2rem;
	}

	/* Side items with colored left accent */
	.status-side-item {
		display: flex;
		flex-direction: column;
		gap: 0.1rem;
		padding-left: 0.6rem;
		border-left: 3px solid transparent;
	}
	.status-side-item.ovulation-accent { border-left-color: var(--blue); }
	.status-side-item.fertile-accent { border-left-color: color-mix(in srgb, var(--blue) 40%, transparent); }

	.status-side-label {
		font-size: 0.6rem;
		font-weight: 600;
		color: var(--color-text-secondary);
		text-transform: uppercase;
		letter-spacing: 0.04em;
	}
	.status-side-relative {
		font-size: 0.8rem;
		font-weight: 600;
		color: var(--color-text-primary);
		white-space: nowrap;
	}
	.status-side-date {
		font-size: 0.7rem;
		color: var(--color-text-secondary);
		white-space: nowrap;
	}

	/* Main column labels */
	.status-pill {
		display: inline-block;
		align-self: flex-start;
		font-size: 0.6rem;
		font-weight: 700;
		text-transform: uppercase;
		letter-spacing: 0.06em;
		padding: 0.15rem 0.5rem;
		border-radius: 10px;
	}
	.status-pill.period-pill {
		background: var(--nord11);
		color: white;
	}
	.status-label {
		font-size: 0.6rem;
		font-weight: 700;
		color: var(--color-text-secondary);
		text-transform: uppercase;
		letter-spacing: 0.06em;
	}
	.status-hero {
		font-size: 1.15rem;
		font-weight: 700;
		color: var(--color-text-primary);
		line-height: 1.25;
	}
	.status-hero.ongoing-hero {
		font-size: 1.5rem;
		color: var(--nord11);
	}
	.status-relative {
		font-size: 0.85rem;
		font-weight: 500;
		color: var(--color-text-primary);
	}
	.status-detail {
		font-size: 0.7rem;
		color: var(--color-text-secondary);
		font-weight: 500;
		margin-top: 0.2rem;
	}
	.status-date {
		font-size: 0.7rem;
		color: var(--color-text-tertiary);
	}
	.status-empty {
		font-size: 0.8rem;
		color: var(--color-text-secondary);
	}

	.start-btn {
		padding: 0.45rem 0.9rem;
		border: none;
		border-radius: 7px;
		font-weight: 600;
		font-size: 0.75rem;
		cursor: pointer;
		white-space: nowrap;
		align-self: flex-start;
		margin-top: 0.6rem;
		background: var(--nord11);
		color: white;
	}
	.start-btn:disabled {
		opacity: 0.5;
		cursor: not-allowed;
	}

	/* Prominent end-period CTA — flat fill, full width */
	.end-btn {
		align-self: stretch;
		margin-top: 0.9rem;
		display: flex;
		align-items: center;
		justify-content: center;
		gap: 0.55rem;
		padding: 0.8rem 1.1rem;
		border: none;
		border-radius: 10px;
		cursor: pointer;
		color: white;
		background: var(--nord11);
		box-shadow: var(--shadow-sm);
		transition: background 140ms ease;
		-webkit-tap-highlight-color: transparent;
	}
	.end-btn:hover {
		background: color-mix(in srgb, var(--nord11) 88%, black);
	}
	.end-btn:active {
		background: color-mix(in srgb, var(--nord11) 80%, black);
	}
	.end-btn:focus-visible {
		outline: 2px solid var(--nord11);
		outline-offset: 2px;
	}
	.end-btn:disabled {
		opacity: 0.55;
		cursor: not-allowed;
	}

	.end-btn-label {
		font-weight: 700;
		font-size: 0.85rem;
		letter-spacing: 0.1em;
		text-transform: uppercase;
	}

	.end-btn-icon {
		display: flex;
		align-items: center;
		justify-content: center;
	}

	@media (max-width: 360px) {
		.status-split { flex-direction: column; gap: 0.6rem; }
		.status-side { border-left: none; padding-left: 0; border-top: 1px solid var(--color-border); padding-top: 0.6rem; flex-direction: row; gap: 1rem; }
	}

	/* Stats row */
	/* Calendar */
	.calendar {
		background: var(--color-surface);
		border-radius: 8px;
		box-shadow: var(--shadow-sm);
		padding: 0.75rem;
	}
	.cal-header {
		display: flex;
		justify-content: center;
		align-items: center;
		gap: 0.25rem;
		margin-bottom: 0.5rem;
		position: relative;
	}
	.go-today-btn {
		position: absolute;
		right: 0;
		font-size: 0.65rem;
		font-weight: 600;
		color: var(--color-primary);
		background: color-mix(in srgb, var(--color-primary) 10%, transparent);
		border: 1px solid color-mix(in srgb, var(--color-primary) 25%, transparent);
		padding: 0.15rem 0.5rem;
		border-radius: 5px;
		cursor: pointer;
		transition: background 0.15s;
		-webkit-tap-highlight-color: transparent;
	}
	.go-today-btn:hover {
		background: color-mix(in srgb, var(--color-primary) 20%, transparent);
	}
	.cal-title {
		font-size: 0.85rem;
		font-weight: 600;
		text-transform: capitalize;
	}
	.cal-nav {
		background: none;
		border: none;
		cursor: pointer;
		color: var(--color-text-secondary);
		padding: 0.25rem;
		border-radius: 4px;
		display: flex;
	}
	.cal-nav:hover { color: var(--color-text-primary); }
	.cal-nav:disabled { opacity: 0.3; cursor: not-allowed; }

	.cal-weekdays {
		display: grid;
		grid-template-columns: repeat(7, 1fr);
		margin-bottom: 2px;
	}
	.cal-wd {
		text-align: center;
		font-size: 0.65rem;
		font-weight: 600;
		color: var(--color-text-secondary);
		text-transform: uppercase;
	}
	.cal-grid {
		display: grid;
		grid-template-columns: repeat(7, 1fr);
	}
	.cal-day {
		height: 38px;
		text-align: center;
		font-size: 0.82rem;
		display: flex;
		align-items: center;
		justify-content: center;
		color: var(--color-text-primary);
		position: relative;
		box-sizing: border-box;
	}
	.cal-day.overflow { color: var(--color-text-tertiary); }

	/* Long-press affordance: scale + colored ring grows during the hold. */
	.cal-day.startable {
		cursor: pointer;
		-webkit-tap-highlight-color: transparent;
		-webkit-touch-callout: none;
		user-select: none;
		transition: transform 100ms ease-out, box-shadow 100ms ease-out;
	}
	.cal-day.startable.long-pressing {
		z-index: 2;
		border-radius: 999px;
		animation: longPressRing 600ms ease-out forwards;
	}
	@keyframes longPressRing {
		from {
			transform: scale(1);
			box-shadow: 0 0 0 0 color-mix(in srgb, var(--nord11) 70%, transparent);
		}
		to {
			transform: scale(1.18);
			box-shadow: 0 0 0 4px color-mix(in srgb, var(--nord11) 70%, transparent);
		}
	}
	@media (prefers-reduced-motion: reduce) {
		.cal-day.startable.long-pressing {
			animation: none;
			transform: scale(1.1);
			box-shadow: 0 0 0 3px color-mix(in srgb, var(--nord11) 70%, transparent);
		}
	}

	/* --- Range shape: border-radius per position --- */
	.cal-day.p-solo { border-radius: 16px; }
	.cal-day.p-start { border-radius: 16px 0 0 16px; }
	.cal-day.p-mid { border-radius: 0; }
	.cal-day.p-end { border-radius: 0 16px 16px 0; }

	/* --- Filled statuses (background-based, border matches fill) --- */
	.cal-day.s-period { background: var(--nord11); color: white; font-weight: 600; }
	.cal-day.s-ovulation { border: 0 solid var(--blue); font-weight: 700; background: color-mix(in srgb, var(--blue) 15%, transparent); }
	.cal-day.s-ovulation.et { border-top-width: 3px; }
	.cal-day.s-ovulation.eb { border-bottom-width: 3px; }
	.cal-day.s-ovulation.el { border-left-width: 3px; }
	.cal-day.s-ovulation.er { border-right-width: 3px; }
	.cal-day.s-ovulation::before {
		content: '';
		position: absolute;
		bottom: 4px;
		left: 50%;
		transform: translateX(-50%);
		width: 6px;
		height: 6px;
		border-radius: 50%;
		background: var(--blue);
	}
	.cal-day.s-luteal::before {
		content: '';
		position: absolute;
		bottom: 4px;
		left: 50%;
		transform: translateX(-50%);
		width: 6px;
		height: 6px;
		border-radius: 50%;
		background: var(--orange);
		opacity: 0.5;
	}

	/* --- Bordered statuses: width 0 by default, edge classes set per-side width --- */
	.cal-day.s-fertile { border: 0 solid var(--blue); }
	.cal-day.s-fertile.et { border-top-width: 2px; }
	.cal-day.s-fertile.eb { border-bottom-width: 2px; }
	.cal-day.s-fertile.el { border-left-width: 2px; }
	.cal-day.s-fertile.er { border-right-width: 2px; }

	.cal-day.s-peak-fertile { border: 0 solid var(--blue); font-weight: 600; background: color-mix(in srgb, var(--blue) 15%, transparent); }
	.cal-day.s-peak-fertile.et { border-top-width: 3px; }
	.cal-day.s-peak-fertile.eb { border-bottom-width: 3px; }
	.cal-day.s-peak-fertile.el { border-left-width: 3px; }
	.cal-day.s-peak-fertile.er { border-right-width: 3px; }
	/* Extend fertile borders into peak-fertile's rounded corner gap.
	   Uses background gradients instead of borders to avoid subpixel rounding misalignment. */
	.cal-day.s-peak-fertile.p-start::before,
	.cal-day.s-peak-fertile.p-solo::before {
		content: '';
		position: absolute;
		left: -3px;
		top: 0;
		bottom: 0;
		width: 16px;
		pointer-events: none;
	}
	.cal-day.s-peak-fertile.p-start.et::before,
	.cal-day.s-peak-fertile.p-solo.et::before {
		top: -3px;
	}
	.cal-day.s-peak-fertile.p-start.eb::before,
	.cal-day.s-peak-fertile.p-solo.eb::before {
		bottom: -3px;
	}
	.cal-day.s-peak-fertile.p-start.et.eb::before,
	.cal-day.s-peak-fertile.p-solo.et.eb::before {
		background:
			linear-gradient(var(--blue), var(--blue)) top    / 100% 2px no-repeat,
			linear-gradient(var(--blue), var(--blue)) bottom / 100% 2px no-repeat;
	}
	.cal-day.s-peak-fertile.p-start.et:not(.eb)::before,
	.cal-day.s-peak-fertile.p-solo.et:not(.eb)::before {
		background: linear-gradient(var(--blue), var(--blue)) top / 100% 2px no-repeat;
	}
	.cal-day.s-peak-fertile.p-start.eb:not(.et)::before,
	.cal-day.s-peak-fertile.p-solo.eb:not(.et)::before {
		background: linear-gradient(var(--blue), var(--blue)) bottom / 100% 2px no-repeat;
	}

	.cal-day.s-predicted { background: color-mix(in srgb, var(--nord11) 15%, transparent); border: 0 dashed var(--nord11); }
	.cal-day.s-predicted.et { border-top-width: 2px; }
	.cal-day.s-predicted.eb { border-bottom-width: 2px; }
	.cal-day.s-predicted.el { border-left-width: 2px; }
	.cal-day.s-predicted.er { border-right-width: 2px; }

	/* Today marker — inner dot so it doesn't clash with range bg */
	.cal-day.today { font-weight: 700; z-index: 1; }
	.cal-day.today::after {
		content: '';
		position: absolute;
		width: 26px;
		height: 26px;
		border-radius: 50%;
		background: var(--color-text-primary);
		z-index: -1;
	}
	.cal-day.today { color: var(--color-bg-primary); }

	/* Legend */
	.cal-legend {
		display: flex;
		gap: 0.5rem 0.8rem;
		justify-content: center;
		flex-wrap: wrap;
		margin-top: 0.6rem;
	}
	.legend-item {
		display: flex;
		align-items: center;
		gap: 0.35rem;
		font-size: 0.75rem;
		color: var(--color-text-secondary);
	}
	.legend-dot {
		width: 20px;
		height: 10px;
		border-radius: 5px;
		flex-shrink: 0;
	}
	.legend-dot.period { background: var(--nord11); }
	.legend-dot.predicted { background: color-mix(in srgb, var(--nord11) 15%, transparent); border: 1.5px dashed var(--nord11); box-sizing: border-box; }
	.legend-dot.fertile { border: 2px solid var(--blue); box-sizing: border-box; }
	.legend-dot.peak-fertile { border: 3px solid var(--blue); box-sizing: border-box; background: color-mix(in srgb, var(--blue) 15%, transparent); }
	.legend-dot.ovulation { width: 8px; height: 8px; border-radius: 50%; background: var(--blue); }
	.legend-dot.luteal { width: 8px; height: 8px; border-radius: 50%; background: var(--orange); opacity: 0.5; }

	/* Cycle stats */
	.cycle-stats {
		display: flex;
		gap: 0.6rem;
	}
	.cycle-stat {
		flex: 1;
		display: flex;
		flex-direction: column;
		align-items: center;
		padding: 0.6rem;
		background: var(--color-surface);
		border-radius: 8px;
		box-shadow: var(--shadow-sm);
	}
	.cycle-stat-label {
		font-size: 0.7rem;
		color: var(--color-text-secondary);
		text-transform: uppercase;
		letter-spacing: 0.04em;
	}
	.cycle-stat-value {
		font-size: 1.1rem;
		font-weight: 700;
	}
	.cycle-stat-variance {
		font-size: 0.75rem;
		font-weight: 400;
		color: var(--color-text-secondary);
	}

	/* History */
	.history-share-row {
		display: flex;
		align-items: center;
		justify-content: space-between;
	}
	.history-toggle {
		display: flex;
		align-items: center;
		gap: 0.3rem;
		background: none;
		border: none;
		cursor: pointer;
		padding: 0;
		color: inherit;
	}
	.history-toggle h3 {
		margin: 0;
		font-size: 0.95rem;
	}
	.history-toggle :global(.chevron) {
		transition: transform 0.2s;
	}
	.history-toggle :global(.chevron.open) {
		transform: rotate(90deg);
	}
	.history h3 {
		margin: 0;
		font-size: 0.95rem;
	}
	.history-header {
		display: flex;
		justify-content: flex-end;
		align-items: center;
		margin-bottom: 0.5rem;
	}
	.add-past-btn {
		display: flex;
		align-items: center;
		gap: 0.3rem;
		background: none;
		border: 1px solid var(--color-border);
		border-radius: 6px;
		color: var(--color-text-secondary);
		font-size: 0.7rem;
		padding: 0.3rem 0.5rem;
		cursor: pointer;
	}
	.add-past-btn:hover {
		border-color: var(--color-text-primary);
		color: var(--color-text-primary);
	}

	.add-form {
		background: var(--color-bg-tertiary);
		border-radius: 8px;
		padding: 0.75rem;
		margin-bottom: 0.5rem;
	}
	.add-row {
		display: flex;
		gap: 0.75rem;
		margin-bottom: 0.5rem;
	}
	.add-row label {
		flex: 1;
		display: flex;
		flex-direction: column;
		gap: 0.2rem;
		font-size: 0.7rem;
		font-weight: 600;
		color: var(--color-text-secondary);
	}
	.add-actions {
		display: flex;
		gap: 0.5rem;
	}
	.save-btn {
		padding: 0.35rem 0.75rem;
		background: var(--nord11);
		color: white;
		border: none;
		border-radius: 6px;
		font-weight: 600;
		font-size: 0.75rem;
		cursor: pointer;
	}
	.save-btn:disabled { opacity: 0.5; cursor: not-allowed; }
	.cancel-btn {
		padding: 0.35rem 0.75rem;
		background: none;
		border: 1px solid var(--color-border);
		border-radius: 6px;
		color: var(--color-text-secondary);
		font-size: 0.75rem;
		cursor: pointer;
	}

	.history-list {
		display: flex;
		flex-direction: column;
		gap: 0.3rem;
	}
	.history-item {
		display: flex;
		justify-content: space-between;
		align-items: center;
		background: var(--color-surface);
		border-radius: 8px;
		box-shadow: var(--shadow-sm);
		padding: 0.5rem 0.75rem;
	}
	.history-info {
		display: flex;
		flex-direction: column;
		gap: 0.1rem;
	}
	.history-dates {
		font-size: 0.8rem;
		font-weight: 600;
	}
	.history-dur {
		font-size: 0.7rem;
		color: var(--color-text-secondary);
	}
	.ongoing-badge {
		display: inline-block;
		margin-left: 0.4rem;
		padding: 0.1rem 0.4rem;
		background: var(--nord11);
		color: white;
		border-radius: 4px;
		font-size: 0.65rem;
		font-weight: 600;
		vertical-align: middle;
	}

	.history-actions {
		display: flex;
		gap: 0.3rem;
		flex-shrink: 0;
	}
	.history-item.editing {
		flex-direction: column;
		gap: 0.4rem;
	}
	.icon-btn {
		background: none;
		border: 1px solid transparent;
		border-radius: 6px;
		color: var(--color-text-secondary);
		cursor: pointer;
		padding: 0.3rem;
		display: flex;
		opacity: 0.5;
	}
	.icon-btn:hover {
		opacity: 1;
	}
	.icon-btn.edit:hover {
		border-color: var(--blue);
		color: var(--blue);
	}
	.icon-btn.delete:hover {
		border-color: var(--nord11);
		color: var(--nord11);
	}

	.empty-state {
		text-align: center;
		padding: 1rem;
	}
	.empty-state p {
		font-size: 0.8rem;
		color: var(--color-text-secondary);
		margin: 0 0 0.75rem;
	}
	.empty-state .add-past-btn {
		margin: 0 auto 0.5rem;
	}

	/* Share bar (below stats) */
	.share-bar {
		display: flex;
		align-items: center;
		gap: 0.5rem;
	}
	.shared-avatars {
		display: flex;
		align-items: center;
		gap: 0.3rem;
	}
	.shared-label {
		font-size: 0.7rem;
		color: var(--color-text-secondary);
		margin-right: 0.2rem;
	}
	.shared-avatar {
		border-radius: 50%;
		box-shadow: 0 0 0 2px var(--color-surface);
	}
	.shared-avatar + .shared-avatar {
		margin-left: -6px;
	}
	.share-btn {
		display: flex;
		align-items: center;
		justify-content: center;
		padding: 0.4rem;
		background: none;
		border: 1px solid var(--color-border);
		border-radius: 50%;
		color: var(--color-text-secondary);
		cursor: pointer;
	}
	.share-btn:hover {
		border-color: var(--blue);
		color: var(--blue);
	}

	/* Share modal */
	.share-overlay {
		position: fixed;
		inset: 0;
		background: rgba(0, 0, 0, 0.4);
		display: flex;
		align-items: center;
		justify-content: center;
		z-index: 100;
	}
	.share-modal {
		background: var(--color-surface);
		border-radius: 12px;
		box-shadow: var(--shadow-lg, 0 8px 24px rgba(0,0,0,0.2));
		padding: 1rem;
		width: min(90vw, 320px);
		display: flex;
		flex-direction: column;
		gap: 0.75rem;
	}
	.share-modal-header {
		display: flex;
		justify-content: space-between;
		align-items: center;
	}
	.share-modal-header h3 {
		margin: 0;
		font-size: 1rem;
	}
	.share-modal-close {
		background: none;
		border: none;
		cursor: pointer;
		color: var(--color-text-secondary);
		padding: 0.2rem;
		display: flex;
	}
	.share-modal-close:hover { color: var(--color-text-primary); }
	.share-user-list {
		display: flex;
		flex-direction: column;
		gap: 0.4rem;
	}
	.share-user {
		display: flex;
		align-items: center;
		gap: 0.5rem;
		padding: 0.3rem 0;
	}
	.share-username {
		flex: 1;
		font-size: 0.85rem;
		font-weight: 500;
	}
	.chip-remove {
		background: none;
		border: none;
		cursor: pointer;
		color: var(--color-text-secondary);
		padding: 0.2rem;
		display: flex;
		border-radius: 50%;
	}
	.chip-remove:hover { color: var(--nord11); background: var(--color-bg-tertiary); }
	.chip-remove:disabled { opacity: 0.4; cursor: not-allowed; }
	.share-empty {
		font-size: 0.8rem;
		color: var(--color-text-secondary);
		text-align: center;
		padding: 0.5rem 0;
	}
	.share-add {
		display: flex;
		gap: 0.35rem;
	}
	.share-add input {
		flex: 1;
		padding: 0.4rem 0.5rem;
		border: 1px solid var(--color-border);
		border-radius: 6px;
		background: var(--color-bg-tertiary);
		color: var(--color-text-primary);
		font-size: 0.85rem;
	}
	.share-add input:focus {
		outline: none;
		border-color: var(--blue);
	}
	.share-add-btn {
		display: flex;
		align-items: center;
		padding: 0.4rem;
		background: var(--blue);
		color: white;
		border: none;
		border-radius: 6px;
		cursor: pointer;
	}
	.share-add-btn:disabled { opacity: 0.4; cursor: not-allowed; }

	/* Shared header (read-only) */
	.shared-header {
		display: inline-flex;
		align-items: center;
		gap: 0.4rem;
	}

	/* Read-only mode */
	.read-only h2 {
		font-size: 0.95rem;
		color: var(--color-text-secondary);
	}
</style>
