diff --git a/docs/superpowers/plans/2026-04-17-flights-map-c5-calendar-geolocation.md b/docs/superpowers/plans/2026-04-17-flights-map-c5-calendar-geolocation.md
new file mode 100644
index 00000000..2e90423e
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-17-flights-map-c5-calendar-geolocation.md
@@ -0,0 +1,931 @@
+# Flights Map C.5 (Calendar + Geolocation) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Wire `availableDays` into the filter's PrimeReact `` with `minDate`/`maxDate`/`disabledDates` + Angular's snap-to-nearest-enabled behavior; add a `useGeolocationDefault` hook that pre-fills `filterState.departure` from browser geolocation when nothing is typed.
+
+**Architecture:** New pure `calendarRange.ts` with four date helpers (today, minDate, maxDate, buildDisabledDates, findNextEnabledDate). New `useGeolocationDefault` hook using `navigator.geolocation.getCurrentPosition` + `findCityByCoord` from dictionaries. `FlightsMapFilter` computes `disabledDates` from `availableDays`, passes min/max, runs a snap effect. `FlightsMapStartPage` calls the geolocation hook once.
+
+**Tech Stack:** TypeScript, React 18, PrimeReact ``, vitest, jsdom, @testing-library/react.
+
+**Related spec:** `docs/superpowers/specs/2026-04-17-flights-map-c5-calendar-geolocation-design.md`
+
+---
+
+## File Structure
+
+**New:**
+- `src/features/flights-map/calendarRange.ts`
+- `src/features/flights-map/calendarRange.test.ts`
+- `src/features/flights-map/hooks/useGeolocationDefault.ts`
+- `src/features/flights-map/hooks/useGeolocationDefault.test.tsx`
+- `src/features/flights-map/components/FlightsMapFilter.test.tsx`
+
+**Modified:**
+- `src/features/flights-map/components/FlightsMapFilter.tsx` — consume `availableDays` via new helpers + snap effect.
+- `src/features/flights-map/components/FlightsMapStartPage.tsx` — one line: call `useGeolocationDefault(...)`.
+
+**Untouched:**
+- `useFlightsMapCalendar` (already returns `availableDays`).
+- `MapCanvas`, dictionaries, all prior C-gap modules.
+
+---
+
+## Task 1: `calendarRange` pure helpers
+
+**Files:**
+- Create: `src/features/flights-map/calendarRange.ts`
+- Create: `src/features/flights-map/calendarRange.test.ts`
+
+- [ ] **Step 1.1: Write failing tests**
+
+Create `src/features/flights-map/calendarRange.test.ts`:
+
+```ts
+import { describe, it, expect } from "vitest";
+import {
+ getMinDate,
+ getMaxDate,
+ buildDisabledDates,
+ findNextEnabledDate,
+} from "./calendarRange.js";
+
+function yyyymmdd(d: Date): string {
+ const y = d.getFullYear().toString();
+ const m = (d.getMonth() + 1).toString().padStart(2, "0");
+ const day = d.getDate().toString().padStart(2, "0");
+ return `${y}${m}${day}`;
+}
+
+function addDays(d: Date, n: number): Date {
+ const x = new Date(d);
+ x.setDate(x.getDate() + n);
+ x.setHours(0, 0, 0, 0);
+ return x;
+}
+
+describe("getMinDate / getMaxDate", () => {
+ it("minDate is today - 1 day, time zero'd", () => {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const expected = addDays(today, -1);
+ const min = getMinDate();
+ expect(min.getTime()).toBe(expected.getTime());
+ expect(min.getHours()).toBe(0);
+ expect(min.getMinutes()).toBe(0);
+ expect(min.getSeconds()).toBe(0);
+ });
+
+ it("maxDate is today + 6 months, time zero'd", () => {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const expected = new Date(today);
+ expected.setMonth(expected.getMonth() + 6);
+ const max = getMaxDate();
+ expect(max.getTime()).toBe(expected.getTime());
+ });
+});
+
+describe("buildDisabledDates", () => {
+ const min = new Date(2026, 0, 1);
+ const max = new Date(2026, 0, 5);
+
+ it("returns every day in the window when availableDays is empty", () => {
+ const disabled = buildDisabledDates(min, max, []);
+ expect(disabled.map((d) => d.getDate())).toEqual([1, 2, 3, 4, 5]);
+ });
+
+ it("returns [] when all dates in window are available", () => {
+ const available = ["20260101", "20260102", "20260103", "20260104", "20260105"];
+ expect(buildDisabledDates(min, max, available)).toEqual([]);
+ });
+
+ it("returns the exact complement for partial availability", () => {
+ const available = ["20260102", "20260104"];
+ const disabled = buildDisabledDates(min, max, available);
+ expect(disabled.map((d) => d.getDate())).toEqual([1, 3, 5]);
+ });
+
+ it("ignores availableDays entries outside the window", () => {
+ const available = ["20251231", "20260601", "20260103"];
+ const disabled = buildDisabledDates(min, max, available);
+ expect(disabled.map((d) => d.getDate())).toEqual([1, 2, 4, 5]);
+ });
+});
+
+describe("findNextEnabledDate", () => {
+ const min = new Date(2026, 0, 1);
+ const max = new Date(2026, 0, 5);
+
+ it("returns current when current is enabled", () => {
+ const current = new Date(2026, 0, 3);
+ const disabled = [new Date(2026, 0, 1), new Date(2026, 0, 2)];
+ const result = findNextEnabledDate(current, disabled, min, max);
+ expect(result?.getDate()).toBe(3);
+ });
+
+ it("advances to the next enabled date when current is disabled", () => {
+ const current = new Date(2026, 0, 2);
+ const disabled = [new Date(2026, 0, 1), new Date(2026, 0, 2), new Date(2026, 0, 3)];
+ const result = findNextEnabledDate(current, disabled, min, max);
+ expect(result?.getDate()).toBe(4);
+ });
+
+ it("returns null when every date in window is disabled", () => {
+ const current = new Date(2026, 0, 3);
+ const disabled = [
+ new Date(2026, 0, 1),
+ new Date(2026, 0, 2),
+ new Date(2026, 0, 3),
+ new Date(2026, 0, 4),
+ new Date(2026, 0, 5),
+ ];
+ expect(findNextEnabledDate(current, disabled, min, max)).toBeNull();
+ });
+
+ it("starts at minDate when current < minDate", () => {
+ const current = new Date(2025, 11, 25);
+ const disabled: Date[] = [];
+ const result = findNextEnabledDate(current, disabled, min, max);
+ expect(result?.getDate()).toBe(1);
+ });
+
+ it("returns null when current > maxDate", () => {
+ const current = new Date(2026, 1, 1);
+ const disabled: Date[] = [];
+ expect(findNextEnabledDate(current, disabled, min, max)).toBeNull();
+ });
+});
+```
+
+- [ ] **Step 1.2: Run failing tests**
+
+Run: `pnpm vitest run src/features/flights-map/calendarRange.test.ts`
+Expected: FAIL — module not found.
+
+- [ ] **Step 1.3: Implement the module**
+
+Create `src/features/flights-map/calendarRange.ts`:
+
+```ts
+/**
+ * Calendar helpers for the flights-map filter date picker.
+ *
+ * All dates are treated as date-only (time zero'd) for day-level comparisons.
+ * minDate/maxDate form the selectable window; `buildDisabledDates` produces
+ * the complement of `availableDays` over that window; `findNextEnabledDate`
+ * advances a current selection forward until it lands on an enabled day.
+ *
+ * Matches Angular `FlightsMapFiltersStateService` calendar behavior.
+ */
+
+/** Today with time set to 00:00:00 local. */
+export function today(): Date {
+ const d = new Date();
+ d.setHours(0, 0, 0, 0);
+ return d;
+}
+
+/** minDate = today - 1 day (Angular parity). */
+export function getMinDate(): Date {
+ const d = today();
+ d.setDate(d.getDate() - 1);
+ return d;
+}
+
+/** maxDate = today + 6 months (Angular parity). */
+export function getMaxDate(): Date {
+ const d = today();
+ d.setMonth(d.getMonth() + 6);
+ return d;
+}
+
+/**
+ * Every date in [minDate, maxDate] whose yyyymmdd is NOT in `availableDays`.
+ * When `availableDays` is empty, every date in the range is disabled.
+ */
+export function buildDisabledDates(
+ minDate: Date,
+ maxDate: Date,
+ availableDays: ReadonlyArray,
+): Date[] {
+ const available = new Set(availableDays);
+ const disabled: Date[] = [];
+ const cursor = new Date(minDate);
+ cursor.setHours(0, 0, 0, 0);
+ const end = new Date(maxDate);
+ end.setHours(0, 0, 0, 0);
+ while (cursor.getTime() <= end.getTime()) {
+ const key = toYyyymmdd(cursor);
+ if (!available.has(key)) disabled.push(new Date(cursor));
+ cursor.setDate(cursor.getDate() + 1);
+ }
+ return disabled;
+}
+
+/**
+ * Advances `current` day-by-day until it hits a non-disabled date, staying
+ * within [minDate, maxDate]. Returns `null` if no enabled date exists.
+ * `current` itself is tested first. If `current < minDate`, starts at minDate.
+ */
+export function findNextEnabledDate(
+ current: Date,
+ disabledDates: ReadonlyArray,
+ minDate: Date,
+ maxDate: Date,
+): Date | null {
+ const disabledSet = new Set(
+ disabledDates.map((d) => {
+ const x = new Date(d);
+ x.setHours(0, 0, 0, 0);
+ return x.getTime();
+ }),
+ );
+ const min = new Date(minDate);
+ min.setHours(0, 0, 0, 0);
+ const max = new Date(maxDate);
+ max.setHours(0, 0, 0, 0);
+
+ const cursor = new Date(current);
+ cursor.setHours(0, 0, 0, 0);
+
+ if (cursor.getTime() < min.getTime()) cursor.setTime(min.getTime());
+
+ while (cursor.getTime() <= max.getTime()) {
+ if (!disabledSet.has(cursor.getTime())) return new Date(cursor);
+ cursor.setDate(cursor.getDate() + 1);
+ }
+ return null;
+}
+
+function toYyyymmdd(d: Date): string {
+ const y = d.getFullYear().toString();
+ const m = (d.getMonth() + 1).toString().padStart(2, "0");
+ const day = d.getDate().toString().padStart(2, "0");
+ return `${y}${m}${day}`;
+}
+```
+
+- [ ] **Step 1.4: Run tests**
+
+Run: `pnpm vitest run src/features/flights-map/calendarRange.test.ts`
+Expected: PASS — 11 tests.
+
+- [ ] **Step 1.5: Commit**
+
+```bash
+git add src/features/flights-map/calendarRange.ts src/features/flights-map/calendarRange.test.ts
+git commit -m "Add calendarRange helpers for flights-map date picker window and snapping"
+```
+
+---
+
+## Task 2: `useGeolocationDefault` hook
+
+**Files:**
+- Create: `src/features/flights-map/hooks/useGeolocationDefault.ts`
+- Create: `src/features/flights-map/hooks/useGeolocationDefault.test.tsx`
+
+- [ ] **Step 2.1: Write failing tests**
+
+Create `src/features/flights-map/hooks/useGeolocationDefault.test.tsx`:
+
+```tsx
+/**
+ * @vitest-environment jsdom
+ */
+
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { renderHook } from "@testing-library/react";
+import { StrictMode, type ReactNode } from "react";
+import { useGeolocationDefault } from "./useGeolocationDefault.js";
+import { transformDictionaries } from "@/shared/dictionaries/index.js";
+import type { IDictionaries, IRawDictionaries } from "@/shared/dictionaries/index.js";
+import type { IFlightsMapFilterState } from "../types.js";
+
+const raw: IRawDictionaries = {
+ regions: [],
+ countries: [{ code: "RU", title: { ru: "Россия" }, world_region_id: 500374 }],
+ cities: [
+ { code: "MOW", title: { ru: "Москва" }, country_code: "RU", has_afl_flights: true, location: { lat: 55, lon: 37 } },
+ { code: "LED", title: { ru: "Санкт-Петербург" }, country_code: "RU", has_afl_flights: true, location: { lat: 60, lon: 30 } },
+ ],
+ airports: [
+ { code: "SVO", city_code: "MOW", title: { ru: "Шереметьево" }, has_afl_flights: true, location: { lat: 55, lon: 37 } },
+ { code: "LED", city_code: "LED", title: { ru: "Пулково" }, has_afl_flights: true, location: { lat: 60, lon: 30 } },
+ ],
+};
+const dictionaries: IDictionaries = transformDictionaries(raw, "ru");
+
+function makeFilter(
+ overrides: Partial = {},
+): IFlightsMapFilterState {
+ return {
+ connections: false,
+ domestic: false,
+ international: false,
+ ...overrides,
+ };
+}
+
+interface GeolocationMock {
+ getCurrentPosition: ReturnType;
+}
+
+function installGeolocation(mock: GeolocationMock | undefined): void {
+ Object.defineProperty(navigator, "geolocation", {
+ value: mock,
+ writable: true,
+ configurable: true,
+ });
+}
+
+function successMock(lat: number, lon: number): GeolocationMock {
+ return {
+ getCurrentPosition: vi.fn((success: PositionCallback) => {
+ success({
+ coords: {
+ latitude: lat,
+ longitude: lon,
+ accuracy: 0,
+ altitude: null,
+ altitudeAccuracy: null,
+ heading: null,
+ speed: null,
+ toJSON: () => ({}),
+ },
+ timestamp: 0,
+ toJSON: () => ({}),
+ } as GeolocationPosition);
+ }),
+ };
+}
+
+function errorMock(): GeolocationMock {
+ return {
+ getCurrentPosition: vi.fn((_success, error?: PositionErrorCallback) => {
+ error?.({
+ code: 1,
+ message: "permission denied",
+ PERMISSION_DENIED: 1,
+ POSITION_UNAVAILABLE: 2,
+ TIMEOUT: 3,
+ } as GeolocationPositionError);
+ }),
+ };
+}
+
+describe("useGeolocationDefault", () => {
+ let lastState: IFlightsMapFilterState = makeFilter();
+ let setFilterState: (updater: (prev: IFlightsMapFilterState) => IFlightsMapFilterState) => void;
+
+ beforeEach(() => {
+ lastState = makeFilter();
+ setFilterState = (updater) => {
+ lastState = updater(lastState);
+ };
+ });
+
+ afterEach(() => {
+ installGeolocation(undefined);
+ });
+
+ it("sets departure to nearest city when filter is empty and geolocation succeeds", () => {
+ installGeolocation(successMock(55.1, 37.1)); // near MOW
+ renderHook(() =>
+ useGeolocationDefault(dictionaries, lastState, setFilterState),
+ );
+ expect(lastState.departure).toBe("MOW");
+ });
+
+ it("does not update when filter.departure is already set", () => {
+ lastState = makeFilter({ departure: "LED" });
+ installGeolocation(successMock(55.1, 37.1));
+ renderHook(() =>
+ useGeolocationDefault(dictionaries, lastState, setFilterState),
+ );
+ expect(lastState.departure).toBe("LED");
+ });
+
+ it("does not update when filter.arrival is already set", () => {
+ lastState = makeFilter({ arrival: "MOW" });
+ installGeolocation(successMock(55.1, 37.1));
+ renderHook(() =>
+ useGeolocationDefault(dictionaries, lastState, setFilterState),
+ );
+ expect(lastState.departure).toBeUndefined();
+ });
+
+ it("does nothing when geolocation permission is denied", () => {
+ installGeolocation(errorMock());
+ renderHook(() =>
+ useGeolocationDefault(dictionaries, lastState, setFilterState),
+ );
+ expect(lastState.departure).toBeUndefined();
+ });
+
+ it("does nothing when navigator.geolocation is missing", () => {
+ installGeolocation(undefined);
+ renderHook(() =>
+ useGeolocationDefault(dictionaries, lastState, setFilterState),
+ );
+ expect(lastState.departure).toBeUndefined();
+ });
+
+ it("does nothing when dictionaries is null at callback time", () => {
+ installGeolocation(successMock(55.1, 37.1));
+ renderHook(() =>
+ useGeolocationDefault(null, lastState, setFilterState),
+ );
+ expect(lastState.departure).toBeUndefined();
+ });
+
+ it("invokes getCurrentPosition exactly once under StrictMode", () => {
+ const mock = successMock(55.1, 37.1);
+ installGeolocation(mock);
+ const wrapper = ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+ renderHook(
+ () => useGeolocationDefault(dictionaries, lastState, setFilterState),
+ { wrapper },
+ );
+ expect(mock.getCurrentPosition).toHaveBeenCalledTimes(1);
+ });
+});
+```
+
+- [ ] **Step 2.2: Run failing tests**
+
+Run: `pnpm vitest run src/features/flights-map/hooks/useGeolocationDefault.test.tsx`
+Expected: FAIL — module not found.
+
+- [ ] **Step 2.3: Implement the hook**
+
+Create `src/features/flights-map/hooks/useGeolocationDefault.ts`:
+
+```ts
+/**
+ * Sets the flights-map filter's departure to the nearest city based on
+ * browser geolocation, if the user has not already typed a departure or
+ * arrival. Fires once per mount. Silent on permission denial or missing
+ * geolocation API. Matches Angular `UserLocationService` + `DictionariesService.locate`.
+ */
+
+import { useEffect, useRef } from "react";
+import {
+ findCityByCoord,
+ type IDictionaries,
+} from "@/shared/dictionaries/index.js";
+import type { IFlightsMapFilterState } from "../types.js";
+
+export function useGeolocationDefault(
+ dictionaries: IDictionaries | null,
+ filterState: IFlightsMapFilterState,
+ setFilterState: (
+ updater: (prev: IFlightsMapFilterState) => IFlightsMapFilterState,
+ ) => void,
+): void {
+ const appliedRef = useRef(false);
+ // Refs hold latest values so the async callback reads current snapshots
+ // while the effect itself runs only once.
+ const dictRef = useRef(dictionaries);
+ dictRef.current = dictionaries;
+ const filterRef = useRef(filterState);
+ filterRef.current = filterState;
+ const setFilterRef = useRef(setFilterState);
+ setFilterRef.current = setFilterState;
+
+ useEffect(() => {
+ if (appliedRef.current) return;
+ if (typeof navigator === "undefined" || !navigator.geolocation) {
+ appliedRef.current = true;
+ return;
+ }
+
+ appliedRef.current = true;
+
+ navigator.geolocation.getCurrentPosition(
+ (pos) => {
+ const d = dictRef.current;
+ const f = filterRef.current;
+ if (!d) return;
+ if (f.departure || f.arrival) return;
+
+ const city = findCityByCoord(
+ d,
+ pos.coords.latitude,
+ pos.coords.longitude,
+ );
+ if (!city) return;
+
+ setFilterRef.current((prev) =>
+ prev.departure || prev.arrival
+ ? prev
+ : { ...prev, departure: city.code },
+ );
+ },
+ () => {
+ // Silent: permission denied / timeout / unavailable.
+ },
+ { enableHighAccuracy: false, timeout: 5000 },
+ );
+ }, []);
+}
+```
+
+- [ ] **Step 2.4: Run tests**
+
+Run: `pnpm vitest run src/features/flights-map/hooks/useGeolocationDefault.test.tsx`
+Expected: PASS — 7 tests.
+
+- [ ] **Step 2.5: Commit**
+
+```bash
+git add src/features/flights-map/hooks/useGeolocationDefault.ts src/features/flights-map/hooks/useGeolocationDefault.test.tsx
+git commit -m "Add useGeolocationDefault hook for flights-map departure pre-fill"
+```
+
+---
+
+## Task 3: Wire calendar into `FlightsMapFilter`
+
+**Files:**
+- Modify: `src/features/flights-map/components/FlightsMapFilter.tsx`
+
+- [ ] **Step 3.1: Add imports**
+
+Open `src/features/flights-map/components/FlightsMapFilter.tsx`. At the top, update the React import to include `useEffect` and `useMemo`, and add the calendar-range import:
+
+```tsx
+import { type FC, useState, useCallback, useEffect, useMemo, type FormEvent } from "react";
+```
+
+Also add, near the `useCitySearch` import:
+
+```tsx
+import {
+ getMinDate,
+ getMaxDate,
+ buildDisabledDates,
+ findNextEnabledDate,
+} from "../calendarRange.js";
+```
+
+- [ ] **Step 3.2: Compute min/max/disabledDates inside the component**
+
+Inside the `FlightsMapFilter` component, after the existing hook calls (`useTranslation`, `useState`, `useCitySearch`) and before `handleDepartureSearch`, add:
+
+```tsx
+const minDate = useMemo(() => getMinDate(), []);
+const maxDate = useMemo(() => getMaxDate(), []);
+
+const disabledDates = useMemo(
+ () => buildDisabledDates(minDate, maxDate, availableDays ?? []),
+ [minDate, maxDate, availableDays],
+);
+```
+
+- [ ] **Step 3.3: Add snap effect**
+
+Below the `disabledDates` memo (still inside the component), add:
+
+```tsx
+useEffect(() => {
+ if (!value.date) return;
+ const current = yyyymmddToDate(value.date);
+ if (!current) return;
+
+ const snapped = findNextEnabledDate(current, disabledDates, minDate, maxDate);
+ const snappedYyyymmdd = snapped ? dateToYyyymmdd(snapped) : undefined;
+
+ if (snappedYyyymmdd !== value.date) {
+ onChange({ ...value, date: snappedYyyymmdd });
+ }
+}, [disabledDates, minDate, maxDate, value, onChange]);
+```
+
+- [ ] **Step 3.4: Pass the new props to ``**
+
+Find the existing `` block. Replace it with:
+
+```tsx
+ handleDateChange(e.value as Date | null)}
+ minDate={minDate}
+ maxDate={maxDate}
+ disabledDates={disabledDates}
+ dateFormat="dd.mm.yy"
+ showIcon
+ className="input--filter"
+ inputId="fm-date"
+ data-testid="fm-date-input"
+/>
+```
+
+- [ ] **Step 3.5: Typecheck**
+
+Run: `pnpm tsc --noEmit`
+Expected: no output.
+
+- [ ] **Step 3.6: Run full suite**
+
+Run: `pnpm vitest run`
+Expected: all pass.
+
+- [ ] **Step 3.7: Commit**
+
+```bash
+git add src/features/flights-map/components/FlightsMapFilter.tsx
+git commit -m "Wire availableDays into FlightsMapFilter Calendar with snap-to-nearest"
+```
+
+---
+
+## Task 4: `FlightsMapFilter` Calendar tests
+
+**Files:**
+- Create: `src/features/flights-map/components/FlightsMapFilter.test.tsx`
+
+- [ ] **Step 4.1: Write the test file**
+
+Create `src/features/flights-map/components/FlightsMapFilter.test.tsx`:
+
+```tsx
+/**
+ * @vitest-environment jsdom
+ */
+
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render } from "@testing-library/react";
+import { FlightsMapFilter } from "./FlightsMapFilter.js";
+import type { IFlightsMapFilterState } from "../types.js";
+
+// Capture props passed to PrimeReact Calendar.
+let lastCalendarProps: Record | null = null;
+vi.mock("primereact/calendar", () => ({
+ Calendar: (props: Record) => {
+ lastCalendarProps = props;
+ return ;
+ },
+}));
+
+// Stub PrimeReact AutoComplete so rendering is cheap.
+vi.mock("primereact/autocomplete", () => ({
+ AutoComplete: (props: Record) => (
+
+ ),
+}));
+
+vi.mock("@/i18n/provider.js", () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ i18n: { language: "ru" },
+ }),
+}));
+
+vi.mock("@/shared/hooks/useCitySearch.js", () => ({
+ useCitySearch: () => ({ suggestions: [], search: vi.fn() }),
+}));
+
+function filter(
+ overrides: Partial = {},
+): IFlightsMapFilterState {
+ return {
+ connections: false,
+ domestic: false,
+ international: false,
+ ...overrides,
+ };
+}
+
+function yyyymmdd(d: Date): string {
+ const y = d.getFullYear().toString();
+ const m = (d.getMonth() + 1).toString().padStart(2, "0");
+ const day = d.getDate().toString().padStart(2, "0");
+ return `${y}${m}${day}`;
+}
+
+function addDays(base: Date, n: number): Date {
+ const d = new Date(base);
+ d.setDate(d.getDate() + n);
+ d.setHours(0, 0, 0, 0);
+ return d;
+}
+
+describe("FlightsMapFilter — Calendar wiring", () => {
+ beforeEach(() => {
+ lastCalendarProps = null;
+ });
+
+ it("passes minDate and maxDate to Calendar", () => {
+ const onChange = vi.fn();
+ render();
+
+ const min = lastCalendarProps!["minDate"] as Date;
+ const max = lastCalendarProps!["maxDate"] as Date;
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const expectedMin = addDays(today, -1);
+ const expectedMax = new Date(today);
+ expectedMax.setMonth(expectedMax.getMonth() + 6);
+
+ expect(min.getTime()).toBe(expectedMin.getTime());
+ expect(max.getTime()).toBe(expectedMax.getTime());
+ });
+
+ it("disables every date when availableDays is empty", () => {
+ const onChange = vi.fn();
+ render(
+ ,
+ );
+
+ const disabled = lastCalendarProps!["disabledDates"] as Date[];
+ expect(disabled.length).toBeGreaterThan(180); // 6 months of days, inclusive
+ });
+
+ it("excludes available days from disabledDates", () => {
+ const onChange = vi.fn();
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const plusTwo = addDays(today, 2);
+
+ render(
+ ,
+ );
+
+ const disabled = lastCalendarProps!["disabledDates"] as Date[];
+ const contains = disabled.some((d) => {
+ const x = new Date(d);
+ x.setHours(0, 0, 0, 0);
+ return x.getTime() === plusTwo.getTime();
+ });
+ expect(contains).toBe(false);
+ });
+
+ it("snaps a disabled value.date forward to the next available day", () => {
+ const onChange = vi.fn();
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const plusOne = addDays(today, 1);
+ const plusTwo = addDays(today, 2);
+
+ render(
+ ,
+ );
+
+ expect(onChange).toHaveBeenCalled();
+ const called = onChange.mock.calls[0]![0] as IFlightsMapFilterState;
+ expect(called.date).toBe(yyyymmdd(plusTwo));
+ });
+
+ it("clears value.date when no enabled date exists in the window", () => {
+ const onChange = vi.fn();
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const plusOne = addDays(today, 1);
+
+ render(
+ ,
+ );
+
+ expect(onChange).toHaveBeenCalled();
+ const called = onChange.mock.calls[0]![0] as IFlightsMapFilterState;
+ expect(called.date).toBeUndefined();
+ });
+
+ it("does not snap when the current date is already enabled", () => {
+ const onChange = vi.fn();
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const plusTwo = addDays(today, 2);
+
+ render(
+ ,
+ );
+
+ expect(onChange).not.toHaveBeenCalled();
+ });
+});
+```
+
+- [ ] **Step 4.2: Run tests**
+
+Run: `pnpm vitest run src/features/flights-map/components/FlightsMapFilter.test.tsx`
+Expected: PASS — 6 tests.
+
+- [ ] **Step 4.3: Commit**
+
+```bash
+git add src/features/flights-map/components/FlightsMapFilter.test.tsx
+git commit -m "Test FlightsMapFilter Calendar min/max/disabledDates + snap effect"
+```
+
+---
+
+## Task 5: Wire `useGeolocationDefault` into `FlightsMapStartPage`
+
+**Files:**
+- Modify: `src/features/flights-map/components/FlightsMapStartPage.tsx`
+
+- [ ] **Step 5.1: Add the hook call**
+
+Open `src/features/flights-map/components/FlightsMapStartPage.tsx`.
+
+Add the import near the other hooks imports:
+
+```tsx
+import { useGeolocationDefault } from "../hooks/useGeolocationDefault.js";
+```
+
+Find the existing `useDictionaries(lang)` destructure. Directly below the `[filterState, setFilterState] = useState(...)` line, add:
+
+```tsx
+useGeolocationDefault(dictionaries, filterState, setFilterState);
+```
+
+- [ ] **Step 5.2: Typecheck**
+
+Run: `pnpm tsc --noEmit`
+Expected: no output.
+
+- [ ] **Step 5.3: Full suite**
+
+Run: `pnpm vitest run`
+Expected: all pass. The existing page test mocks `useDictionaries` but not `navigator.geolocation` — the hook early-returns when `navigator.geolocation` is undefined in jsdom. If some test has a real `navigator.geolocation` stub lingering from a previous test, a `beforeEach` reset may be needed — if so, add:
+
+```tsx
+beforeEach(() => {
+ Object.defineProperty(navigator, "geolocation", {
+ value: undefined,
+ writable: true,
+ configurable: true,
+ });
+});
+```
+
+to the existing `FlightsMapStartPage.test.tsx`. Verify first by running the suite; only add if tests fail.
+
+- [ ] **Step 5.4: Commit**
+
+```bash
+git add src/features/flights-map/components/FlightsMapStartPage.tsx
+git commit -m "Invoke useGeolocationDefault on FlightsMapStartPage mount"
+```
+
+If step 5.3 required a test file change, include it in the commit.
+
+---
+
+## Task 6: Final verification
+
+- [ ] **Step 6.1: Typecheck**
+
+Run: `pnpm tsc --noEmit`
+Expected: no output.
+
+- [ ] **Step 6.2: Full vitest suite**
+
+Run: `pnpm vitest run`
+Expected: all pass. Count delta: ~24 new tests (11 calendarRange + 7 useGeolocationDefault + 6 FlightsMapFilter).
+
+- [ ] **Step 6.3: Feature-scoped run**
+
+Run: `pnpm vitest run src/features/flights-map/`
+Expected: all feature tests pass.
+
+---
+
+## Self-Review Log
+
+Ran against the spec:
+
+- **Spec coverage.**
+ - `today()`, `getMinDate()`, `getMaxDate()`, `buildDisabledDates()`, `findNextEnabledDate()` → Task 1.
+ - `useGeolocationDefault` hook with appliedRef + refs for dict/filter/setter → Task 2.
+ - FlightsMapFilter `minDate`/`maxDate` memos + `disabledDates` memo + snap `useEffect` → Task 3.
+ - FlightsMapFilter Calendar prop wiring → Task 3.
+ - FlightsMapStartPage `useGeolocationDefault(...)` call → Task 5.
+ - Tests: calendarRange (Task 1), useGeolocationDefault (Task 2), FlightsMapFilter Calendar wiring (Task 4), page integration covered by existing tests + Task 6 full-suite run.
+- **Placeholders.** None.
+- **Type consistency.** `IDictionaries` imported from `@/shared/dictionaries/index.js` consistently. `IFlightsMapFilterState` shape matches across all files. Helper signatures (`buildDisabledDates(min, max, availableDays)`, `findNextEnabledDate(current, disabled, min, max)`) used identically in implementation, tests, and the FlightsMapFilter consumer.
+- **Known trade-off.** `FlightsMapFilter` snap effect has `value` + `onChange` in deps. This makes the effect re-run on every re-render where `value` changes, but the inner `snappedYyyymmdd !== value.date` guard prevents infinite loops (documented in the spec and inline comment).
+- **StrictMode parity.** `appliedRef` ensures exactly one `getCurrentPosition` call under StrictMode. Covered by Task 2.5 test.