Escort Helper

Pomaga w weryfikacji anonsów Escort.club: łączy dane z Escorti.pl i garsoniera.com.pl, analizuje powiązane profile i recenzje, wykrywa rozbieżności oraz dodaje filtry, cache i obserwowanie anonsów.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         Escort Helper
// @namespace    escort-helper
// @version      1.100
// @description  Pomaga w weryfikacji anonsów Escort.club: łączy dane z Escorti.pl i garsoniera.com.pl, analizuje powiązane profile i recenzje, wykrywa rozbieżności oraz dodaje filtry, cache i obserwowanie anonsów.
// @author       Kut1234
// @copyright    2026, Kut1234
// @license      GPL-3.0-only
// @match       https://pl.escort.club/
// @match       https://pl.escort.club/anons/*
// @match       https://pl.escort.club/anonse/*
// @match       https://pl.escort.club/szukaj/*
// @match       https://escorti.pl/*
// @match       https://www.escorti.pl/*
// @match       https://garsoniera.com.pl/forum/*
// @match       https://www.garsoniera.com.pl/forum/*
// @match       https://picdetective.com/*
// @match       https://www.picdetective.com/*
// @match       https://tineye.com/*
// @match       https://www.tineye.com/*
// @grant       GM_xmlhttpRequest
// @grant       GM_openInTab
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_addValueChangeListener
// @grant       GM_removeValueChangeListener
// @grant       GM_registerMenuCommand
// @grant       GM_unregisterMenuCommand
// @grant       GM_setClipboard
// @grant       unsafeWindow
// @connect     www.garsoniera.com.pl
// @connect     pl.escort.club
// @connect     escorti.pl
// @connect     www.escorti.pl
// @connect     static.escort.club
// @run-at       document-end
// ==/UserScript==

/*
 * Escort Helper
 * Pomysł na podstawie:
 * https://update.greasyfork.org/scripts/561369/Szukaj%20na%20Garsoniera.user.js
 */

(async function () {
    'use strict';

    const GARSO_BASE_URL = 'https://www.garsoniera.com.pl/forum/';
    // 7 s dotyczy wyłącznie wyszukiwarki Garso (POST wyszukiwania i kolejne strony wyników).
    // Treść tematów jest pobierana bez sztucznego odstępu, z ograniczeniem współbieżności.
    const GARSO_ANTIFLOOD_WAIT_MS = 7000;
    const GARSO_TOPIC_FETCH_CONCURRENT = 6;
    const GARSO_TOPIC_PAGE_CACHE_PREFIX = 'vm_garso_topic_page_';
    const GARSO_ANTIFLOOD_RETRY_LIMIT = 3;
    const GARSO_REQUEST_MAX_ATTEMPTS = GARSO_ANTIFLOOD_RETRY_LIMIT + 1;
    const GARSO_MAX_FULL_TOPIC_PAGES = 7;
    const GARSO_LONG_TOPIC_FIRST_PAGES = 3;
    const GARSO_LONG_TOPIC_LAST_PAGES = 4;
    const GARSO_OPEN_BRIDGE_PARAM = 'vm_garso_open';
    const GARSO_OPEN_BRIDGE_KEY_PREFIX = 'vm_garso_open_bridge_';
    const GARSO_EXTENDED_SEARCH_MODE_KEY = 'vm_garso_extended_search_mode';
    const ESCORTI_BASE_URL = 'https://escorti.pl/';
    const CURRENT_HOST = location.hostname.toLowerCase();
    const GARSO_MENU_ONLY_MODE = [
        'garsoniera.com.pl',
        'www.garsoniera.com.pl'
    ].includes(CURRENT_HOST);

    function makeElement(tagName, className = '', textContent = null) {
        const element = document.createElement(tagName);
        if (className) element.className = className;
        if (textContent !== null) element.textContent = textContent;
        return element;
    }

    function makeButton(className = '', textContent = null) {
        const button = makeElement('button', className, textContent);
        button.type = 'button';
        return button;
    }

    function appendSelectOptions(select, options, selectedValue = null) {
        const selected = selectedValue == null ? null : String(selectedValue);
        for (const [value, label] of options) {
            const option = makeElement('option', '', label);
            option.value = String(value);
            if (selected !== null) option.selected = option.value === selected;
            select.appendChild(option);
        }
        return select;
    }

    function createAnimationFrameScheduler(callback) {
        let scheduled = false;
        return () => {
            if (scheduled) return;
            scheduled = true;
            requestAnimationFrame(() => {
                scheduled = false;
                callback();
            });
        };
    }

    function createConcurrentJobQueue(maxConcurrent, errorLabel) {
        const queue = [];
        let activeJobs = 0;

        const pump = () => {
            while (activeJobs < maxConcurrent && queue.length) {
                const job = queue.shift();
                activeJobs++;
                Promise.resolve()
                    .then(job)
                    .catch(error => log(errorLabel, error))
                    .finally(() => {
                        activeJobs--;
                        pump();
                    });
            }
        };

        return job => {
            queue.push(job);
            pump();
        };
    }

    function handleGarsoOpenBridgePage() {
        if (!GARSO_MENU_ONLY_MODE) {
            return false;
        }

        const pageUrl = new URL(location.href);
        const token = pageUrl.searchParams.get(GARSO_OPEN_BRIDGE_PARAM);
        // Zwykła strona Garsoniery ma kontynuować inicjalizację skryptu,
        // żeby dostępne było menu Violentmonkey. Tylko karta bridge
        // z parametrem vm_garso_open kończy dalsze uruchamianie.
        if (!token) return false;

        const storageKey = `${GARSO_OPEN_BRIDGE_KEY_PREFIX}${token}`;
        const payload = GM_getValue(storageKey, null);
        GM_deleteValue(storageKey);

        if (
            !payload?.term || !payload?.secureHash || !payload?.sessionId ||
            Date.now() - Number(payload.createdAt || 0) > 5 * 60 * 1000
        ) {
            pageUrl.searchParams.delete(GARSO_OPEN_BRIDGE_PARAM);
            location.replace(pageUrl.href);
            return true;
        }

        const submit = () => {
            const form = makeElement('form');
            form.method = 'POST';
            form.action = `${GARSO_BASE_URL}index.php?app=core&module=search&do=search&fromMainBar=1&s=${encodeURIComponent(payload.sessionId)}`;
            form.style.display = 'none';
            const fields = {
                search_term: payload.term,
                search_app: 'forums',
                secure_hash: payload.secureHash,
                submit: 'Szukaj'
            };
            for (const [name, value] of Object.entries(fields)) {
                const input = makeElement('input');
                input.type = 'hidden';
                input.name = name;
                input.value = value;
                form.appendChild(input);
            }
            document.body.appendChild(form);
            HTMLFormElement.prototype.submit.call(form);
        };

        if (document.body) submit();
        else document.addEventListener('DOMContentLoaded', submit, { once: true });
        return true;
    }

    // Specjalna karta Garsoniery otwarta jako bridge służy wyłącznie do
    // wysłania formularza POST. Zwykłe strony Garsoniery kontynuują
    // inicjalizację, dzięki czemu dostępne jest menu skryptu.
    if (handleGarsoOpenBridgePage()) return;

    // ============================================================
    // USTAWIENIA
    // ============================================================

    const SETTINGS_STORAGE_KEY = 'vm_garso_settings';
    const SETTINGS_ONBOARDING_SEEN_KEY = 'vm_garso_settings_onboarding_seen';
    const WATCH_ITEMS_STORAGE_KEY = 'vm_garso_watch_items';
    const WATCH_EVENTS_STORAGE_KEY = 'vm_garso_watch_events';
    const WATCH_CHECK_LOCK_KEY = 'vm_garso_watch_check_lock';
    const SETTINGS_EXPORT_FORMAT = 'vm-garso-settings-and-watched';
    const DIAGNOSTICS_STORAGE_KEY = 'vm_garso_diagnostics';
    const DIAGNOSTICS_EXPORT_FORMAT = 'vm-garso-diagnostics-sanitized';
    const DIAGNOSTICS_MAX_EVENTS = 40;
    const WATCH_BAR_ID = 'vm-garso-watch-notification-bar';
    const WATCH_BAR_SPACER_ID = 'vm-garso-watch-notification-spacer';
    const WATCH_SETTINGS_OVERLAY_ID = 'vm-garso-watch-settings-overlay';
    const ESCORT_TOP_PHONE_SEARCH_ID = 'vm-escort-top-phone-search';
    const ESCORT_DEFAULT_CITY_BUTTON_ID = 'vm-escort-default-location-buttons';
    const ESCORT_LOCATION_BRIDGE_PARAM = 'vm_location_bridge';
    const ESCORT_LOCATION_TAB_BRIDGE_PARAM = 'vm_location_tab_bridge';
    const ESCORT_LOCATION_TAB_BRIDGE_READY_PREFIX = 'vm_escort_location_tab_ready_';
    const ESCORT_LOCATION_TAB_BRIDGE_REQUEST_PREFIX = 'vm_escort_location_tab_request_';
    const ESCORT_LOCATION_TAB_BRIDGE_RESPONSE_PREFIX = 'vm_escort_location_tab_response_';
    const ESCORT_LOCATION_BRIDGE_URL = 'https://pl.escort.club/anonse/towarzyskie/poland/';
    const WATCH_INSTANCE_ID = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
    const DEFAULT_WATCH_SETTINGS = {
        enabled: true,
        intervalMinutes: 15,
        showOnlineCount: true
    };
    const WATCH_INTERVAL_OPTIONS = [1, 5, 15, 30, 60, 180, 360];
    const WATCH_INTERVAL_SELECT_OPTIONS = WATCH_INTERVAL_OPTIONS.map(minutes => [
        minutes,
        minutes < 60 ? `${minutes} min` : `${minutes / 60} h`
    ]);

    const DEFAULT_HISTORY_CLEANUP_KEYWORDS = [
        'escort.club',
        'escorti.pl',
        'garsoniera.com.pl',
        'dolores.sex',
        'tineye.com',
        'picdetective.com',
        'google.com/search?vsrid'
    ];

    const DEFAULT_SETTINGS = {
        autoGarsoCheck: true,
        autoGarsoAnalysisMode: 'count',
        mergeEscortAds: true,
        escortAggregationMode: 'address-escorti',
        compactSummaryComparisonMode: 'address-escorti',
        showEscortiTileButton: true,
        showEscortiTileData: true,
        showPricesInSearchResults: true,
        searchResultPriceDuration: '60',
        hideRecommendedSearchSection: true,
        hideRecommendedAdSection: false,
        hideSingleAdContactButtons: false,
        hideFeaturedSearchSection: true,
        hidePopularCitiesSection: true,
        showImageSearchButtons: true,
        showTopPhoneSearch: true,
        topPhoneSearchEnterTarget: 'escort-club',
        topPhoneSearchOpenMode: 'new-tab',
        showDefaultCityButton: false,
        defaultSearchLocation: {
            countryValue: '33',
            countryLabel: 'Polska',
            provinceValue: '',
            provinceLabel: '',
            cityValue: '',
            cityLabel: '',
            districtValue: '',
            districtLabel: ''
        },
        searchLocations: [],
        showSummarySidePanel: true,
        showCompactSummaryAboveDescription: true,
        phoneClipboardFormat: 'local-hyphens',
        usePersistentCache: true,
        cacheRefreshHours: 168,
        garsoCacheRefreshHours: 336,
        watchEnabled: true,
        watchIntervalMinutes: 15,
        showWatchOnlineCount: true,
        historyCleanupKeywords: [...DEFAULT_HISTORY_CLEANUP_KEYWORDS]
    };

    const CACHE_REFRESH_OPTIONS = [
        [1, '1 godzina'],
        [8, '8 godzin'],
        [12, '12 godzin'],
        [24, '1 dzień'],
        [72, '3 dni'],
        [168, '7 dni'],
        [336, '14 dni'],
        [720, '30 dni']
    ];

    const DEFAULT_LIST_PAGES_TO_SHOW = 1;
    const ESCORT_LIST_PAGES_RELOAD_KEY = 'vm_escort_list_pages_reload_once';
    const LIST_PAGE_OPTIONS = [
        [1, '1 stronę'],
        [2, '2 strony'],
        [3, '3 strony'],
        [4, '4 strony'],
        [5, '5 stron'],
        [10, '10 stron'],
        ['all', 'Wszystkie']
    ];

    const ESCORT_PRICE_DURATION_OPTIONS = [
        ['15', '15 min'],
        ['30', '0,5 h'],
        ['60', '1 h'],
        ['120', '2 h'],
        ['night', 'całą noc']
    ];

    const PHONE_CLIPBOARD_FORMAT_OPTIONS = [
        ['international-spaces', '+48 123 456 789'],
        ['local-hyphens', '123-456-789']
    ];

    const TOP_PHONE_SEARCH_TARGET_OPTIONS = [
        ['escort-club', 'Escort.club'],
        ['escorti', 'Escorti.pl']
    ];

    const TOP_PHONE_SEARCH_OPEN_MODE_OPTIONS = [
        ['new-tab', 'w nowej karcie'],
        ['same-tab', 'na tej samej karcie']
    ];

    const ESCORT_AGGREGATION_MODE_OPTIONS = [
        ['address-escorti', 'Według adresu anonsu → Escorti.pl'],
        ['phone-escorti', 'Według numeru telefonu → Escorti.pl'],
        ['phone-escort-search', 'Według numeru telefonu → wyszukiwarka Escort.club'],
        ['exact-phone', 'Tylko identyczny numer telefonu → załadowana lista i cache']
    ];

    const ESCORT_AGGREGATION_MODE_DESCRIPTIONS = {
        'address-escorti':
            'Najlepiej znajduje powiązane anonse, ale czasami może błędnie przypisać do jednej grupy niezwiązane anonse.',
        'phone-escorti':
            'Wyszukuje w Escorti.pl według numeru telefonu, dlatego powoduje mniej błędnych przypisań niż wyszukiwanie według adresu.',
        'phone-escort-search':
            'Dla numeru telefonu z kafelka przeszukuje Escort.club, a następnie uwzględnia wyniki wyszukiwania, załadowane kafelki oraz anonse zapisane w aktualnym cache.',
        'exact-phone':
            'Łączy kafelki z aktualnie załadowanej listy, które mają identyczny numer telefonu, i uwzględnia również anonse zapisane w aktualnym cache. Zapewnia wysoką pewność powiązania, ale może łączyć różne osoby korzystające z tego samego lub ponownie przydzielonego numeru.'
    };

    const ESCORT_COMPACT_SUMMARY_MODE_OPTIONS = [
        ['address-escorti', 'Według adresu anonsu → Escorti.pl'],
        ['phone-escorti', 'Według numeru telefonu → Escorti.pl'],
        ['exact-phone', 'Według numeru telefonu → wyszukiwarka Escort.club']
    ];

    const ESCORT_COMPACT_SUMMARY_MODE_DESCRIPTIONS = {
        'address-escorti':
            'Najlepiej znajduje powiązane anonse, ale czasami może błędnie przypisać do jednej grupy niezwiązane anonse.',
        'phone-escorti':
            'Wyszukuje w Escorti.pl według numeru telefonu, dlatego powoduje mniej błędnych przypisań niż wyszukiwanie według adresu.',
        'exact-phone':
            'Wyszukuje na Escort.club aktualnie dostępne anonse znalezione po numerze telefonu i porównuje ich dane. Cache ogranicza ponowne pobieranie szczegółów, ale nie wyznacza listy porównywanych anonsów.'
    };

    const ESCORT_SAVED_FILTERS_STORAGE_KEY = 'vm_escort_saved_search_filters';
    const ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH = 145;
    const ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH = 280;
    const ESCORT_CUSTOM_FILTERS_STORAGE_KEY = 'vm_escort_custom_result_filters';
    const ESCORT_CUSTOM_FILTER_AGE_UNITS = ['days', 'weeks', 'months', 'years'];
    const ESCORT_CUSTOM_FILTER_AGE_UNIT_OPTIONS = [
        ['days', 'dni'],
        ['weeks', 'tygodnie'],
        ['months', 'miesiące'],
        ['years', 'lata']
    ];
    const ESCORT_CUSTOM_FILTER_WEEKDAYS = [
        ['monday', 'Poniedziałek'],
        ['tuesday', 'Wtorek'],
        ['wednesday', 'Środa'],
        ['thursday', 'Czwartek'],
        ['friday', 'Piątek'],
        ['saturday', 'Sobota'],
        ['sunday', 'Niedziela']
    ];
    const DEFAULT_ESCORT_CUSTOM_FILTERS = {
        expanded: false,
        hideWithoutOpinions: false,
        hideProfileAdsOver: false,
        profileAdsLimit: 10,
        hideActiveAdsOver: false,
        activeAdsLimit: 1,
        hideAgencyPhone: false,
        hideYoungerThan: false,
        youngerValue: 2,
        youngerUnit: 'days',
        hideOlderThan: false,
        olderValue: 2,
        olderUnit: 'days',
        hideAdYoungerThan: false,
        adYoungerValue: 2,
        adYoungerUnit: 'days',
        hideAdOlderThan: false,
        adOlderValue: 2,
        adOlderUnit: 'days',
        hideAlwaysAvailable: false,
        hideUnavailableAt: false,
        hideUnavailableDay: 'monday',
        hideUnavailableTime: '12:00',
        hideAvailableAt: false,
        hideAvailableDay: 'monday',
        hideAvailableTime: '12:00',
        hideDescriptionMatch: false,
        hideDescriptionQuery: '',
        showOnlyUnavailableAt: false,
        showOnlyUnavailableDay: 'monday',
        showOnlyUnavailableTime: '12:00',
        showOnlyAvailableAt: false,
        showOnlyAvailableDay: 'monday',
        showOnlyAvailableTime: '12:00',
        showOnlyWithoutOpinions: false,
        showOnlySingleProfileAd: false,
        showOnlyAgencyPhone: false,
        showOnlyDescriptionMatch: false,
        showOnlyDescriptionQuery: ''
    };

    function normalizeCustomFilterInteger(value, fallback, min = 0, max = 100000) {
        const number = Math.round(Number(value));
        return Number.isFinite(number)
            ? Math.min(max, Math.max(min, number))
            : fallback;
    }

    function normalizeCustomFilterAgeUnit(value) {
        return ESCORT_CUSTOM_FILTER_AGE_UNITS.includes(value) ? value : 'days';
    }

    function normalizeCustomFilterWeekday(value) {
        return ESCORT_CUSTOM_FILTER_WEEKDAYS.some(([allowed]) => allowed === value)
            ? value
            : 'monday';
    }

    function normalizeCustomFilterTime(value, fallback = '12:00') {
        const match = String(value || '').match(/^(\d{1,2}):(\d{2})$/);
        if (!match) return fallback;

        const hours = Number(match[1]);
        const minutes = Number(match[2]);
        if (hours > 23 || minutes > 59) return fallback;
        return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
    }

    function normalizeCustomFilterQuery(value) {
        return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 200);
    }

    function getEscortPagePinkColor() {
        const fallback = '#f54da3';

        try {
            const sample = [...document.querySelectorAll('.btn.btn-pink')]
                .find(element => !element.closest('[id^="vm-escort-"]'));
            if (!sample || typeof getComputedStyle !== 'function') return fallback;

            const computed = getComputedStyle(sample);
            const candidates = [computed.backgroundColor, computed.borderTopColor];
            return candidates.find(value =>
                value &&
                value !== 'transparent' &&
                value !== 'rgba(0, 0, 0, 0)'
            ) || fallback;
        } catch (_) {
            return fallback;
        }
    }

    function normalizeEscortCustomFilters(value = {}) {
        const filters = { ...DEFAULT_ESCORT_CUSTOM_FILTERS, ...value };
        return {
            expanded: !!filters.expanded,
            hideWithoutOpinions: !!filters.hideWithoutOpinions,
            hideProfileAdsOver: !!filters.hideProfileAdsOver,
            profileAdsLimit: normalizeCustomFilterInteger(filters.profileAdsLimit, 10),
            hideActiveAdsOver: !!filters.hideActiveAdsOver,
            activeAdsLimit: normalizeCustomFilterInteger(filters.activeAdsLimit, 1),
            hideAgencyPhone: !!filters.hideAgencyPhone,
            hideYoungerThan: !!filters.hideYoungerThan,
            youngerValue: normalizeCustomFilterInteger(filters.youngerValue, 2, 1),
            youngerUnit: normalizeCustomFilterAgeUnit(filters.youngerUnit),
            hideOlderThan: !!filters.hideOlderThan,
            olderValue: normalizeCustomFilterInteger(filters.olderValue, 2, 1),
            olderUnit: normalizeCustomFilterAgeUnit(filters.olderUnit),
            hideAdYoungerThan: !!filters.hideAdYoungerThan,
            adYoungerValue: normalizeCustomFilterInteger(filters.adYoungerValue, 2, 1),
            adYoungerUnit: normalizeCustomFilterAgeUnit(filters.adYoungerUnit),
            hideAdOlderThan: !!filters.hideAdOlderThan,
            adOlderValue: normalizeCustomFilterInteger(filters.adOlderValue, 2, 1),
            adOlderUnit: normalizeCustomFilterAgeUnit(filters.adOlderUnit),
            hideAlwaysAvailable: !!filters.hideAlwaysAvailable,
            hideUnavailableAt: !!filters.hideUnavailableAt,
            hideUnavailableDay: normalizeCustomFilterWeekday(filters.hideUnavailableDay),
            hideUnavailableTime: normalizeCustomFilterTime(filters.hideUnavailableTime),
            hideAvailableAt: !!filters.hideAvailableAt,
            hideAvailableDay: normalizeCustomFilterWeekday(filters.hideAvailableDay),
            hideAvailableTime: normalizeCustomFilterTime(filters.hideAvailableTime),
            hideDescriptionMatch: !!filters.hideDescriptionMatch,
            hideDescriptionQuery: normalizeCustomFilterQuery(filters.hideDescriptionQuery),
            showOnlyUnavailableAt: !!filters.showOnlyUnavailableAt,
            showOnlyUnavailableDay: normalizeCustomFilterWeekday(filters.showOnlyUnavailableDay),
            showOnlyUnavailableTime: normalizeCustomFilterTime(filters.showOnlyUnavailableTime),
            showOnlyAvailableAt: !!filters.showOnlyAvailableAt,
            showOnlyAvailableDay: normalizeCustomFilterWeekday(filters.showOnlyAvailableDay),
            showOnlyAvailableTime: normalizeCustomFilterTime(filters.showOnlyAvailableTime),
            showOnlyWithoutOpinions: !!filters.showOnlyWithoutOpinions,
            showOnlySingleProfileAd: !!filters.showOnlySingleProfileAd,
            showOnlyAgencyPhone: !!filters.showOnlyAgencyPhone,
            showOnlyDescriptionMatch: !!filters.showOnlyDescriptionMatch,
            showOnlyDescriptionQuery: normalizeCustomFilterQuery(filters.showOnlyDescriptionQuery)
        };
    }

    function getEscortCustomFilters() {
        try {
            const saved = GM_getValue(ESCORT_CUSTOM_FILTERS_STORAGE_KEY, null);
            const source = saved && typeof saved === 'object' ? saved : {};
            return normalizeEscortCustomFilters(source);
        } catch (_) {
            return normalizeEscortCustomFilters();
        }
    }

    function saveEscortCustomFilters(filters) {
        const normalized = normalizeEscortCustomFilters(filters);

        try {
            GM_setValue(ESCORT_CUSTOM_FILTERS_STORAGE_KEY, normalized);
        } catch (error) {
            log('Nie udało się zapisać filtrów niestandardowych', error);
        }

        return normalized;
    }

    function normalizeListPagesToShow(value) {
        if (value === 'all') return 'all';

        const count = Number(value);
        return LIST_PAGE_OPTIONS.some(([allowed]) => allowed === count)
            ? count
            : DEFAULT_LIST_PAGES_TO_SHOW;
    }

    function consumeOneTimeListPagesToShow() {
        try {
            const saved = sessionStorage.getItem(ESCORT_LIST_PAGES_RELOAD_KEY);
            sessionStorage.removeItem(ESCORT_LIST_PAGES_RELOAD_KEY);
            return saved == null
                ? DEFAULT_LIST_PAGES_TO_SHOW
                : normalizeListPagesToShow(saved);
        } catch (_) {
            return DEFAULT_LIST_PAGES_TO_SHOW;
        }
    }

    function requestOneTimeListPagesReload(value) {
        try {
            sessionStorage.setItem(
                ESCORT_LIST_PAGES_RELOAD_KEY,
                String(normalizeListPagesToShow(value))
            );
            return true;
        } catch (_) {
            return false;
        }
    }

    function normalizeOptionValue(value, options, fallback, convert = String) {
        const normalized = convert(value ?? '');
        return options.some(([allowed]) => allowed === normalized)
            ? normalized
            : fallback;
    }

    function normalizeCacheRefreshHours(value) {
        return normalizeOptionValue(
            value,
            CACHE_REFRESH_OPTIONS,
            DEFAULT_SETTINGS.cacheRefreshHours,
            Number
        );
    }

    function normalizeGarsoCacheRefreshHours(value) {
        return normalizeOptionValue(
            value,
            CACHE_REFRESH_OPTIONS,
            DEFAULT_SETTINGS.garsoCacheRefreshHours,
            Number
        );
    }

    function normalizeAutoGarsoAnalysisMode(value) {
        return value === 'count' || value === 'content'
            ? value
            : DEFAULT_SETTINGS.autoGarsoAnalysisMode;
    }

    function normalizeEscortPriceDuration(value) {
        return normalizeOptionValue(
            value,
            ESCORT_PRICE_DURATION_OPTIONS,
            DEFAULT_SETTINGS.searchResultPriceDuration
        );
    }

    function normalizePhoneClipboardFormat(value) {
        return normalizeOptionValue(
            value,
            PHONE_CLIPBOARD_FORMAT_OPTIONS,
            DEFAULT_SETTINGS.phoneClipboardFormat
        );
    }

    function normalizeTopPhoneSearchEnterTarget(value) {
        return normalizeOptionValue(
            value,
            TOP_PHONE_SEARCH_TARGET_OPTIONS,
            DEFAULT_SETTINGS.topPhoneSearchEnterTarget
        );
    }

    function normalizeTopPhoneSearchOpenMode(value) {
        return normalizeOptionValue(
            value,
            TOP_PHONE_SEARCH_OPEN_MODE_OPTIONS,
            DEFAULT_SETTINGS.topPhoneSearchOpenMode
        );
    }

    function normalizeDefaultSearchLocation(value) {
        const source = value && typeof value === 'object' ? value : {};
        const clean = (field, maxLength = 100) => String(field ?? '')
            .replace(/\s+/g, ' ')
            .trim()
            .slice(0, maxLength);
        // Zapisane lokalizacje dotyczą wyłącznie Polski. Zachowujemy pola kraju
        // w modelu danych dla zgodności ze starszymi ustawieniami i funkcjami
        // Escort.club, ale użytkownik nie wybiera kraju w ustawieniach.
        return {
            countryValue: '33',
            countryLabel: 'Polska',
            provinceValue: clean(source.provinceValue, 20),
            provinceLabel: clean(source.provinceLabel),
            cityValue: clean(source.cityValue, 20),
            cityLabel: clean(source.cityLabel),
            districtValue: clean(source.districtValue, 20),
            districtLabel: clean(source.districtLabel)
        };
    }

    function normalizeSearchLocations(value, legacyEnabled = false, legacyLocation = null) {
        let source = Array.isArray(value) ? value : [];
        if (!source.length && legacyEnabled) {
            const legacy = normalizeDefaultSearchLocation(legacyLocation);
            if (
                legacy.provinceValue || legacy.provinceLabel ||
                legacy.cityValue || legacy.cityLabel
            ) source = [legacy];
        }

        const seen = new Set();
        const result = [];
        for (const candidate of source) {
            const locationData = normalizeDefaultSearchLocation(candidate);
            const hasProvince = !!(locationData.provinceValue || locationData.provinceLabel);
            const hasCity = !!(locationData.cityValue || locationData.cityLabel);
            const hasDistrict = !!(locationData.districtValue || locationData.districtLabel);
            // Samo państwo nie tworzy przycisku. Miasto jest opcjonalne,
            // więc poprawną zapisaną lokalizacją może być już województwo.
            if (!hasProvince && !hasCity && !hasDistrict) continue;
            // Dzielnica bez miasta nie ma sensu i nie powinna trafić do zapisu.
            if (hasDistrict && !hasCity) continue;
            const key = [
                locationData.countryValue || locationData.countryLabel,
                locationData.provinceValue || locationData.provinceLabel,
                locationData.cityValue || locationData.cityLabel,
                locationData.districtValue || locationData.districtLabel
            ].map(part => String(part || '').toLocaleLowerCase('pl-PL')).join('|');
            if (!key || seen.has(key)) continue;
            seen.add(key);
            result.push(locationData);
            if (result.length >= 4) break;
        }
        return result;
    }

    function normalizeEscortAggregationMode(value) {
        return normalizeOptionValue(
            value,
            ESCORT_AGGREGATION_MODE_OPTIONS,
            DEFAULT_SETTINGS.escortAggregationMode
        );
    }

    function normalizeEscortCompactSummaryMode(value) {
        const normalized = String(value || '');
        return normalizeOptionValue(
            normalized,
            ESCORT_COMPACT_SUMMARY_MODE_OPTIONS,
            DEFAULT_SETTINGS.compactSummaryComparisonMode
        );
    }

    function getEscortListSearchMode(settings = SETTINGS) {
        return settings?.mergeEscortAds &&
            normalizeEscortAggregationMode(settings.escortAggregationMode) === 'phone-escorti'
            ? 'phone-escorti'
            : 'address-escorti';
    }

    function getEscortPriceDurationLabel(value) {
        const normalized = normalizeEscortPriceDuration(value);
        return ESCORT_PRICE_DURATION_OPTIONS.find(([allowed]) => allowed === normalized)?.[1]
            || '1 h';
    }

    function normalizeHistoryCleanupKeywords(value) {
        const source = value == null
            ? DEFAULT_HISTORY_CLEANUP_KEYWORDS
            : (Array.isArray(value) ? value : String(value).split(/[\n,;]+/));
        const seen = new Set();
        const keywords = [];

        for (const candidate of source) {
            let keyword = String(candidate || '').replace(/\s+/g, ' ').trim().slice(0, 160);
            if (keyword.toLocaleLowerCase('pl-PL') === 'lens.google.com') {
                keyword = 'google.com/search?vsrid';
            }
            const key = keyword.toLocaleLowerCase('pl-PL');
            if (!keyword || seen.has(key)) continue;
            seen.add(key);
            keywords.push(keyword);
            if (keywords.length >= 100) break;
        }
        return keywords;
    }

    function getSettings() {
        try {
            const saved = GM_getValue(SETTINGS_STORAGE_KEY, null);
            if (saved && typeof saved === 'object') {
                const normalized = {
                    ...DEFAULT_SETTINGS,
                    ...saved
                };

                normalized.escortAggregationMode = normalizeEscortAggregationMode(
                    normalized.escortAggregationMode
                );
                normalized.compactSummaryComparisonMode = normalizeEscortCompactSummaryMode(
                    normalized.compactSummaryComparisonMode
                );
                normalized.showPricesInSearchResults = !!normalized.showPricesInSearchResults;
                normalized.showEscortiTileData = normalized.showEscortiTileData !== false;
                normalized.showTopPhoneSearch = normalized.showTopPhoneSearch !== false;
                normalized.topPhoneSearchEnterTarget = normalizeTopPhoneSearchEnterTarget(
                    normalized.topPhoneSearchEnterTarget
                );
                normalized.topPhoneSearchOpenMode = normalizeTopPhoneSearchOpenMode(
                    normalized.topPhoneSearchOpenMode
                );
                normalized.showDefaultCityButton = !!normalized.showDefaultCityButton;
                normalized.defaultSearchLocation = normalizeDefaultSearchLocation(
                    normalized.defaultSearchLocation
                );
                normalized.searchLocations = normalizeSearchLocations(
                    normalized.searchLocations,
                    normalized.showDefaultCityButton,
                    normalized.defaultSearchLocation
                );
                normalized.showDefaultCityButton = normalized.searchLocations.length > 0;
                normalized.hideRecommendedAdSection = !!normalized.hideRecommendedAdSection;
                normalized.hideSingleAdContactButtons = !!normalized.hideSingleAdContactButtons;
                normalized.showSummarySidePanel = normalized.showSummarySidePanel !== false;
                normalized.showCompactSummaryAboveDescription =
                    normalized.showCompactSummaryAboveDescription !== false;
                normalized.searchResultPriceDuration = normalizeEscortPriceDuration(
                    normalized.searchResultPriceDuration
                );
                normalized.phoneClipboardFormat = normalizePhoneClipboardFormat(
                    normalized.phoneClipboardFormat
                );
                normalized.cacheRefreshHours = normalizeCacheRefreshHours(
                    normalized.cacheRefreshHours
                );
                normalized.garsoCacheRefreshHours = normalizeGarsoCacheRefreshHours(
                    normalized.garsoCacheRefreshHours
                );
                normalized.autoGarsoAnalysisMode = normalizeAutoGarsoAnalysisMode(
                    normalized.autoGarsoAnalysisMode
                );
                normalized.usePersistentCache = !!normalized.usePersistentCache;
                normalized.showWatchOnlineCount = !!normalized.showWatchOnlineCount;
                normalized.watchEnabled =
                    normalized.usePersistentCache && normalized.watchEnabled === true;
                if (!normalized.usePersistentCache) {
                    normalized.showWatchOnlineCount = false;
                    normalized.showPricesInSearchResults = false;
                    normalized.mergeEscortAds = false;
                }
                const watchInterval = Number(normalized.watchIntervalMinutes);
                normalized.watchIntervalMinutes = WATCH_INTERVAL_OPTIONS.includes(watchInterval)
                    ? watchInterval
                    : DEFAULT_SETTINGS.watchIntervalMinutes;
                normalized.historyCleanupKeywords = normalizeHistoryCleanupKeywords(
                    normalized.historyCleanupKeywords
                );
                return normalized;
            }

            return { ...DEFAULT_SETTINGS };
        } catch (_) {
            return { ...DEFAULT_SETTINGS };
        }
    }

    function normalizeSettingsForStorage(settings) {
        const usePersistentCache = !!settings.usePersistentCache;
        return {
            autoGarsoCheck: !!settings.autoGarsoCheck,
            autoGarsoAnalysisMode: normalizeAutoGarsoAnalysisMode(
                settings.autoGarsoAnalysisMode
            ),
            mergeEscortAds: usePersistentCache && !!settings.mergeEscortAds,
            escortAggregationMode: normalizeEscortAggregationMode(
                settings.escortAggregationMode
            ),
            compactSummaryComparisonMode: normalizeEscortCompactSummaryMode(
                settings.compactSummaryComparisonMode
            ),
            showEscortiTileButton: !!settings.showEscortiTileButton,
            showEscortiTileData: settings.showEscortiTileData !== false,
            showPricesInSearchResults: usePersistentCache && !!settings.showPricesInSearchResults,
            searchResultPriceDuration: normalizeEscortPriceDuration(
                settings.searchResultPriceDuration
            ),
            hideRecommendedSearchSection: !!settings.hideRecommendedSearchSection,
            hideRecommendedAdSection: !!settings.hideRecommendedAdSection,
            hideSingleAdContactButtons: !!settings.hideSingleAdContactButtons,
            hideFeaturedSearchSection: !!settings.hideFeaturedSearchSection,
            hidePopularCitiesSection: !!settings.hidePopularCitiesSection,
            showImageSearchButtons: !!settings.showImageSearchButtons,
            showTopPhoneSearch: settings.showTopPhoneSearch !== false,
            topPhoneSearchEnterTarget: normalizeTopPhoneSearchEnterTarget(
                settings.topPhoneSearchEnterTarget
            ),
            topPhoneSearchOpenMode: normalizeTopPhoneSearchOpenMode(
                settings.topPhoneSearchOpenMode
            ),
            showDefaultCityButton: normalizeSearchLocations(
                settings.searchLocations,
                settings.showDefaultCityButton,
                settings.defaultSearchLocation
            ).length > 0,
            defaultSearchLocation: normalizeSearchLocations(
                settings.searchLocations,
                settings.showDefaultCityButton,
                settings.defaultSearchLocation
            )[0] || normalizeDefaultSearchLocation(settings.defaultSearchLocation),
            searchLocations: normalizeSearchLocations(
                settings.searchLocations,
                settings.showDefaultCityButton,
                settings.defaultSearchLocation
            ),
            showSummarySidePanel: settings.showSummarySidePanel !== false,
            showCompactSummaryAboveDescription:
                settings.showCompactSummaryAboveDescription !== false,
            phoneClipboardFormat: normalizePhoneClipboardFormat(
                settings.phoneClipboardFormat
            ),
            usePersistentCache,
            cacheRefreshHours: normalizeCacheRefreshHours(settings.cacheRefreshHours),
            garsoCacheRefreshHours: normalizeGarsoCacheRefreshHours(
                settings.garsoCacheRefreshHours
            ),
            watchEnabled: usePersistentCache && settings.watchEnabled === true,
            watchIntervalMinutes: WATCH_INTERVAL_OPTIONS.includes(
                Number(settings.watchIntervalMinutes)
            )
                ? Number(settings.watchIntervalMinutes)
                : DEFAULT_SETTINGS.watchIntervalMinutes,
            showWatchOnlineCount: usePersistentCache && !!settings.showWatchOnlineCount,
            historyCleanupKeywords: normalizeHistoryCleanupKeywords(
                settings.historyCleanupKeywords
            )
        };
    }

    function saveSettings(settings) {
        const normalized = normalizeSettingsForStorage(settings);
        GM_setValue(SETTINGS_STORAGE_KEY, normalized);
        return normalized;
    }

    function buildSettingsExportPayload() {
        return {
            format: SETTINGS_EXPORT_FORMAT,
            exportedAt: new Date().toISOString(),
            settings: getSettings(),
            filters: {
                custom: getEscortCustomFilters(),
                saved: getEscortSavedSearchFilters()
            },
            watched: {
                items: getWatchedItems(),
                events: getWatchEvents()
            }
        };
    }

    function downloadSettingsExport() {
        const payload = buildSettingsExportPayload();
        const date = new Date().toISOString().slice(0, 10);
        const blob = new Blob(
            [JSON.stringify(payload, null, 2)],
            { type: 'application/json;charset=utf-8' }
        );
        const url = URL.createObjectURL(blob);
        const link = makeElement('a');
        link.href = url;
        link.download = `escort-helper-ustawienia-${date}.json`;
        link.style.display = 'none';
        document.body.appendChild(link);
        link.click();
        link.remove();
        setTimeout(() => URL.revokeObjectURL(url), 1000);
    }

    function validateSettingsImportPayload(payload) {
        if (!payload || typeof payload !== 'object') {
            throw new Error('Plik nie zawiera prawidłowych danych.');
        }
        if (payload.format !== SETTINGS_EXPORT_FORMAT) {
            throw new Error('To nie jest plik eksportu ustawień tego skryptu.');
        }
        if (!payload.settings || typeof payload.settings !== 'object') {
            throw new Error('W pliku brakuje ustawień głównych.');
        }
        if (
            !payload.watched ||
            !Array.isArray(payload.watched.items) ||
            !Array.isArray(payload.watched.events)
        ) {
            throw new Error('W pliku brakuje prawidłowej listy obserwowanych.');
        }

        return {
            settings: normalizeSettingsForStorage(payload.settings),
            customFilters: payload.filters?.custom && typeof payload.filters.custom === 'object'
                ? payload.filters.custom
                : DEFAULT_ESCORT_CUSTOM_FILTERS,
            savedFilters: Array.isArray(payload.filters?.saved)
                ? payload.filters.saved
                : [],
            watchedItems: payload.watched.items
                .map(normalizeWatchedItem)
                .filter(Boolean),
            watchEvents: payload.watched.events
                .filter(event => event && event.id && event.itemId && event.message)
                .map(event => ({
                    ...event,
                    id: String(event.id),
                    itemId: String(event.itemId),
                    timestamp: Number(event.timestamp) || Date.now(),
                    acknowledged: !!event.acknowledged
                }))
        };
    }

    function restoreSettingsExport(payload) {
        const restored = validateSettingsImportPayload(payload);
        saveSettings(restored.settings);
        saveEscortCustomFilters(restored.customFilters);
        setEscortSavedSearchFilters(restored.savedFilters);
        saveWatchedItems(restored.watchedItems);
        saveWatchEvents(restored.watchEvents);
        return restored;
    }

    let SETTINGS = getSettings();
    let LIST_PAGES_TO_SHOW = consumeOneTimeListPagesToShow();

    // Lista Escort.club: trwały cache. Czas odświeżania jest wybierany w ustawieniach.
    const ESCORT_AD_DATA_LAST_ERROR_KEY = 'vm_escort_ad_data_last_error';
    const PERSISTENT_CACHE_DB_NAME = 'vm-garso-escorti-cache';
    const PERSISTENT_CACHE_STORE_NAME = 'entries';
    const PERSISTENT_CACHE_STATS_STORAGE_KEY = 'vm_garso_cache_stats_snapshot';
    const PERSISTENT_CACHE_BRIDGE_PARAM = 'vm_cache_bridge';
    const PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX = 'vm_garso_cache_bridge_request_';
    const PERSISTENT_CACHE_BRIDGE_RESPONSE_PREFIX = 'vm_garso_cache_bridge_response_';
    const ESCORTI_PROFILE_AD_INDEX_STORAGE_KEY = 'vm_escorti_profile_ad_index';
    const ESCORT_VISIT_SUMMARY_ID = 'vm-escort-visit-summary';
    const ESCORT_LAST_VISIT_ID = 'vm-escort-last-visited';
    const ESCORT_CHANGE_STYLE_ID = 'vm-escort-change-style';
    const ESCORT_PHONE_ENDPOINT = 'https://pl.escort.club/includes/ajax.show-phone.php';
    // Lista wyników może zawierać ponad 100 anonsów. Escorti i Escort.club
    // mają osobne kolejki, więc mogą pobierać dane równolegle niezależnie.
    // Zwiększone względem wcześniejszych 6/3, aby skrócić fazę
    // „Sprawdzanie X/Y” bez zmiany logiki ani cache.
    const LIST_MAX_CONCURRENT = 12;
    const ESCORT_AD_DATA_MAX_CONCURRENT = 6;
    // Musi być zainicjalizowane przed startem pobierania danych kafelków.
    // Osobna stała zapobiega odwołaniu do później deklarowanego limitu
    // sprawdzania aktywności anonsów podczas inicjalizacji strony wyników.
    const ESCORT_AD_DATA_FETCH_TIMEOUT_MS = 12000;
    // Używane również przez obserwowanie uruchamiane na stronach Escorti,
    // dlatego muszą być zainicjalizowane przed wcześniejszym return dla tej domeny.
    const ESCORT_ACTIVE_CHECK_CONCURRENT = 6;
    // Lekkie sondy HEAD używane przez obserwowanie online/offline mogą działać
    // z większą współbieżnością, bo nie pobierają treści stron anonsów.
    const ESCORT_ACTIVE_PROBE_CONCURRENT = 12;
    const escortActiveAdsSummaryCache = new Map();
    const escortActiveAdCheckCache = new Map();
    const escortActiveAdProbeCache = new Map();
    // Pobieraj w tle strony pojedynczych anonsów Escort.club podczas
    // przetwarzania listy wyników i zapisuj odczytane dane w trwałym cache.
    const PREFETCH_ESCORT_CLUB_AD_DATA = true;

    function getListCacheTtlMs() {
        return normalizeCacheRefreshHours(SETTINGS.cacheRefreshHours) * 60 * 60 * 1000;
    }

    function getGarsoCacheTtlMs() {
        return normalizeGarsoCacheRefreshHours(SETTINGS.garsoCacheRefreshHours) * 60 * 60 * 1000;
    }

    // Duże rekordy cache są przechowywane w IndexedDB. Mapa w pamięci zachowuje
    // synchroniczny interfejs dotychczasowych funkcji, a zapis do bazy odbywa się
    // asynchronicznie. Ustawienia, obserwowani i małe klucze komunikacyjne nadal
    // korzystają z GM_*, dzięki czemu pozostają wspólne dla wszystkich domen.
    const persistentCacheMemory = new Map();
    const persistentCacheEntrySizeBytes = new Map();
    let persistentCacheDb = null;
    let persistentCacheUsesIndexedDb = false;
    let persistentCacheDbOperationChain = Promise.resolve();
    let fallbackPersistentCacheStatsMemo = null;
    let persistentCacheStatsSnapshotTimer = null;

    function getPersistentCacheEntrySizeBytes(key, value) {
        try {
            const encoder = new TextEncoder();
            return encoder.encode(String(key || '')).length +
                encoder.encode(JSON.stringify(value) ?? '').length;
        } catch (_) {
            return 0;
        }
    }
    let escortiProfileAdIndexRefreshTimer = null;

    function isIndexedDbCacheKey(key) {
        const value = String(key || '');
        return /^vm_garso_search_count_/i.test(value) ||
            /^vm_garso_summary_/i.test(value) ||
            /^vm_garso_extended_\d+$/i.test(value) ||
            /^vm_garso_topic_page_/i.test(value) ||
            /^vm_escorti_list_\d+$/i.test(value) ||
            /^vm_escort_ad_data_\d+$/i.test(value);
    }

    function openPersistentCacheDb() {
        return new Promise((resolve, reject) => {
            let indexedDbApi = null;
            try {
                indexedDbApi = globalThis.indexedDB || unsafeWindow?.indexedDB || null;
            } catch (_) {
                indexedDbApi = globalThis.indexedDB || null;
            }
            if (!indexedDbApi) {
                reject(new Error('IndexedDB jest niedostępne'));
                return;
            }

            const request = indexedDbApi.open(PERSISTENT_CACHE_DB_NAME);
            request.onupgradeneeded = () => {
                const db = request.result;
                if (!db.objectStoreNames.contains(PERSISTENT_CACHE_STORE_NAME)) {
                    const store = db.createObjectStore(
                        PERSISTENT_CACHE_STORE_NAME,
                        { keyPath: 'key' }
                    );
                    store.createIndex('updatedAt', 'updatedAt', { unique: false });
                }
            };
            request.onsuccess = () => resolve(request.result);
            request.onerror = () => reject(request.error || new Error('Nie udało się otworzyć IndexedDB'));
            request.onblocked = () => reject(new Error('Otwarcie IndexedDB zostało zablokowane'));
        });
    }

    function waitForIndexedDbTransaction(transaction) {
        return new Promise((resolve, reject) => {
            transaction.oncomplete = () => resolve();
            transaction.onerror = () => reject(
                transaction.error || new Error('Błąd transakcji IndexedDB')
            );
            transaction.onabort = () => reject(
                transaction.error || new Error('Transakcja IndexedDB została przerwana')
            );
        });
    }

    function readAllPersistentCacheRecords(db) {
        return new Promise((resolve, reject) => {
            const transaction = db.transaction(PERSISTENT_CACHE_STORE_NAME, 'readonly');
            const request = transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).getAll();
            request.onsuccess = () => resolve(Array.isArray(request.result) ? request.result : []);
            request.onerror = () => reject(request.error || new Error('Błąd odczytu IndexedDB'));
        });
    }

    async function putPersistentCacheRecords(db, records) {
        if (!records.length) return;
        const transaction = db.transaction(PERSISTENT_CACHE_STORE_NAME, 'readwrite');
        const store = transaction.objectStore(PERSISTENT_CACHE_STORE_NAME);
        for (const record of records) store.put(record);
        await waitForIndexedDbTransaction(transaction);
    }

    function enqueuePersistentCacheDbOperation(operation) {
        const result = persistentCacheDbOperationChain
            .catch(() => {})
            .then(operation);
        persistentCacheDbOperationChain = result.catch(error => {
            log('Błąd zapisu trwałego cache w IndexedDB', error);
        });
        return result;
    }

    function readPersistentCacheValue(key, fallback = null) {
        if (persistentCacheUsesIndexedDb && isIndexedDbCacheKey(key)) {
            return persistentCacheMemory.has(key)
                ? persistentCacheMemory.get(key)
                : fallback;
        }
        try {
            return GM_getValue(key, fallback);
        } catch (_) {
            return fallback;
        }
    }

    function writePersistentCacheValue(key, value) {
        fallbackPersistentCacheStatsMemo = null;
        if (!persistentCacheUsesIndexedDb || !isIndexedDbCacheKey(key)) {
            GM_setValue(key, value);
            schedulePersistentCacheStatsSnapshot();
            return Promise.resolve();
        }

        persistentCacheMemory.set(key, value);
        const sizeBytes = getPersistentCacheEntrySizeBytes(key, value);
        persistentCacheEntrySizeBytes.set(key, sizeBytes);
        schedulePersistentCacheStatsSnapshot();
        return enqueuePersistentCacheDbOperation(async () => {
            await putPersistentCacheRecords(persistentCacheDb, [{
                key,
                value,
                updatedAt: Date.now(),
                sizeBytes
            }]);
        }).catch(() => {
            // Awaryjna kopia zapewnia, że pojedynczy błąd IndexedDB nie powoduje
            // utraty świeżo pobranych danych. Przy kolejnym starcie zostanie
            // ponownie przeniesiona do bazy.
            try { GM_setValue(key, value); } catch (_) {}
            return undefined;
        });
    }

    function deletePersistentCacheValue(key) {
        fallbackPersistentCacheStatsMemo = null;
        if (!persistentCacheUsesIndexedDb || !isIndexedDbCacheKey(key)) {
            try { GM_deleteValue(key); } catch (_) {}
            schedulePersistentCacheStatsSnapshot();
            return Promise.resolve();
        }

        persistentCacheMemory.delete(key);
        persistentCacheEntrySizeBytes.delete(key);
        try { GM_deleteValue(key); } catch (_) {}
        schedulePersistentCacheStatsSnapshot();
        return enqueuePersistentCacheDbOperation(async () => {
            const transaction = persistentCacheDb.transaction(
                PERSISTENT_CACHE_STORE_NAME,
                'readwrite'
            );
            transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).delete(key);
            await waitForIndexedDbTransaction(transaction);
        });
    }

    function listPersistentCacheKeys(pattern) {
        if (persistentCacheUsesIndexedDb) {
            return [...persistentCacheMemory.keys()].filter(key => pattern.test(key));
        }
        try {
            return GM_listValues().filter(key => pattern.test(key));
        } catch (_) {
            return [];
        }
    }

    function rebuildEscortiProfileAdIndex() {
        const profiles = {};
        const pattern = /^vm_escorti_list_(\d+)$/i;

        for (const [key, store] of persistentCacheMemory) {
            const match = key.match(pattern);
            if (!match || !store?.modes || typeof store.modes !== 'object') continue;
            const sourceAdId = match[1];

            for (const entry of Object.values(store.modes)) {
                const profileIds = [...new Set(
                    (Array.isArray(entry?.profileIds) ? entry.profileIds : [])
                        .map(value => String(value || '').trim())
                        .filter(value => /^\d+$/.test(value))
                )];
                if (profileIds.length !== 1) continue;

                const profileId = profileIds[0];
                const current = profiles[profileId] || { adIds: [], checkedAt: 0 };
                current.adIds.push(sourceAdId, ...(Array.isArray(entry.adIds) ? entry.adIds : []));
                current.checkedAt = Math.max(current.checkedAt, Number(entry.checkedAt) || 0);
                profiles[profileId] = current;
            }
        }

        for (const entry of Object.values(profiles)) {
            entry.adIds = [...new Set(
                entry.adIds.map(String).filter(value => /^\d+$/.test(value))
            )];
        }

        try {
            GM_setValue(ESCORTI_PROFILE_AD_INDEX_STORAGE_KEY, {
                profiles,
                updatedAt: Date.now()
            });
        } catch (error) {
            log('Nie udało się zapisać indeksu profili Escorti', error);
        }
    }

    function scheduleEscortiProfileAdIndexRebuild() {
        if (!persistentCacheUsesIndexedDb) return;
        clearTimeout(escortiProfileAdIndexRefreshTimer);
        escortiProfileAdIndexRefreshTimer = setTimeout(() => {
            escortiProfileAdIndexRefreshTimer = null;
            rebuildEscortiProfileAdIndex();
        }, 1500);
    }

    async function initializePersistentCacheStorage() {
        // IndexedDB jest związane z domeną. Ciężkie dane są używane na
        // Escort.club; dla Escorti pozostaje mały, wspólny indeks profili.
        if (location.hostname !== 'pl.escort.club') return;

        try {
            const db = await openPersistentCacheDb();
            const existingRecords = await readAllPersistentCacheRecords(db);
            for (const record of existingRecords) {
                if (record?.key && isIndexedDbCacheKey(record.key)) {
                    persistentCacheMemory.set(record.key, record.value);
                    persistentCacheEntrySizeBytes.set(
                        record.key,
                        Number.isFinite(Number(record.sizeBytes))
                            ? Math.max(0, Number(record.sizeBytes))
                            : getPersistentCacheEntrySizeBytes(
                                record.key,
                                record.value
                            )
                    );
                }
            }

            persistentCacheDb = db;
            persistentCacheUsesIndexedDb = true;
            savePersistentCacheStatsSnapshot(calculatePersistentCacheStats());
            if (!GM_getValue(ESCORTI_PROFILE_AD_INDEX_STORAGE_KEY, null)) {
                rebuildEscortiProfileAdIndex();
            }
        } catch (error) {
            persistentCacheDb = null;
            persistentCacheUsesIndexedDb = false;
            persistentCacheMemory.clear();
            persistentCacheEntrySizeBytes.clear();
            log('IndexedDB niedostępne - pozostawiam cache w Violentmonkey', error);
        }
    }

    function getGarsoSearchCountCacheKeys() {
        return listPersistentCacheKeys(/^vm_garso_search_count_/i);
    }

    function getGarsoExtendedCacheKeys() {
        return listPersistentCacheKeys(/^vm_garso_extended_\d+$/i);
    }

    function getGarsoSummaryCacheKeys() {
        return listPersistentCacheKeys(/^vm_garso_summary_/i);
    }

    function getGarsoTopicPageCacheKeys() {
        return listPersistentCacheKeys(/^vm_garso_topic_page_/i);
    }

    function getEscortListCacheKeys() {
        return listPersistentCacheKeys(/^vm_escorti_list_\d+$/i);
    }

    function getEscortAdDataCacheKeys() {
        return listPersistentCacheKeys(/^vm_escort_ad_data_\d+$/i);
    }

    function calculatePersistentCacheStats() {
        if (!persistentCacheUsesIndexedDb && fallbackPersistentCacheStatsMemo) {
            return { ...fallbackPersistentCacheStatsMemo };
        }

        const garsoKeys = [
            ...getGarsoSearchCountCacheKeys(),
            ...getGarsoSummaryCacheKeys(),
            ...getGarsoExtendedCacheKeys(),
            ...getGarsoTopicPageCacheKeys()
        ];
        const listKeys = getEscortListCacheKeys();
        const escortAdDataKeys = getEscortAdDataCacheKeys();
        const keys = [...garsoKeys, ...listKeys, ...escortAdDataKeys];
        let bytes = 0;

        for (const key of keys) {
            if (persistentCacheUsesIndexedDb) {
                bytes += persistentCacheEntrySizeBytes.get(key) || 0;
                continue;
            }
            bytes += getPersistentCacheEntrySizeBytes(
                key,
                readPersistentCacheValue(key, null)
            );
        }

        const stats = {
            entries: keys.length,
            garsoEntries: garsoKeys.length,
            listEntries: listKeys.length,
            escortAdDataEntries: escortAdDataKeys.length,
            bytes,
            megabytes: bytes / (1024 * 1024),
            backend: persistentCacheUsesIndexedDb ? 'IndexedDB' : 'Violentmonkey'
        };
        if (!persistentCacheUsesIndexedDb) {
            fallbackPersistentCacheStatsMemo = { ...stats };
        }
        return stats;
    }

    function savePersistentCacheStatsSnapshot(stats) {
        if (location.hostname !== 'pl.escort.club') return;
        try {
            GM_setValue(PERSISTENT_CACHE_STATS_STORAGE_KEY, {
                ...stats,
                updatedAt: Date.now()
            });
        } catch (_) {}
    }

    function schedulePersistentCacheStatsSnapshot() {
        if (location.hostname !== 'pl.escort.club') return;
        clearTimeout(persistentCacheStatsSnapshotTimer);
        persistentCacheStatsSnapshotTimer = setTimeout(() => {
            persistentCacheStatsSnapshotTimer = null;
            savePersistentCacheStatsSnapshot(calculatePersistentCacheStats());
        }, 150);
    }

    function readPersistentCacheStatsSnapshot() {
        try {
            const stats = GM_getValue(PERSISTENT_CACHE_STATS_STORAGE_KEY, null);
            if (!stats || typeof stats !== 'object') return null;
            const bytes = Number(stats.bytes);
            const entries = Number(stats.entries);
            if (!Number.isFinite(bytes) || bytes < 0 || !Number.isFinite(entries) || entries < 0) {
                return null;
            }
            return {
                entries,
                garsoEntries: Math.max(0, Number(stats.garsoEntries) || 0),
                listEntries: Math.max(0, Number(stats.listEntries) || 0),
                escortAdDataEntries: Math.max(0, Number(stats.escortAdDataEntries) || 0),
                bytes,
                megabytes: bytes / (1024 * 1024),
                backend: stats.backend || 'IndexedDB',
                updatedAt: Number(stats.updatedAt) || 0
            };
        } catch (_) {
            return null;
        }
    }

    function getPersistentCacheStats() {
        // IndexedDB jest izolowane per domena. Poza Escort.club używamy
        // małego snapshotu statystyk zapisanego w GM_*, wspólnego dla skryptu.
        if (location.hostname !== 'pl.escort.club') {
            const shared = readPersistentCacheStatsSnapshot();
            if (shared) return shared;
        }
        return calculatePersistentCacheStats();
    }

    function formatCacheSize(megabytes) {
        const mb = Number(megabytes) || 0;
        const decimals = mb < 0.1 ? 3 : 2;
        return `${mb.toFixed(decimals).replace('.', ',')} MB`;
    }

    function clearCacheEntries(getKeys, errorMessage, afterClear = null) {
        try {
            const keys = getKeys();
            for (const key of keys) deletePersistentCacheValue(key);
            afterClear?.();
            return keys.length;
        } catch (error) {
            log(errorMessage, error);
            return 0;
        }
    }

    function clearGarsoCache() {
        return clearCacheEntries(
            () => [
                ...getGarsoSearchCountCacheKeys(),
                ...getGarsoSummaryCacheKeys(),
                ...getGarsoExtendedCacheKeys()
            ],
            'Błąd czyszczenia cache Garsoniery'
        );
    }

    function clearEscortListCache() {
        return clearCacheEntries(
            getEscortListCacheKeys,
            'Błąd czyszczenia cache Escorti',
            () => { try { GM_deleteValue(ESCORTI_PROFILE_AD_INDEX_STORAGE_KEY); } catch (_) {} }
        );
    }

    function clearEscortAdDataCache() {
        return clearCacheEntries(
            getEscortAdDataCacheKeys,
            'Błąd czyszczenia cache danych Escort.club'
        );
    }

    function clearPersistentCache() {
        if (persistentCacheUsesIndexedDb) {
            const entries = persistentCacheMemory.size;
            persistentCacheMemory.clear();
            persistentCacheEntrySizeBytes.clear();
            fallbackPersistentCacheStatsMemo = null;
            savePersistentCacheStatsSnapshot(calculatePersistentCacheStats());
            try { GM_deleteValue(ESCORTI_PROFILE_AD_INDEX_STORAGE_KEY); } catch (_) {}

            enqueuePersistentCacheDbOperation(async () => {
                const transaction = persistentCacheDb.transaction(
                    PERSISTENT_CACHE_STORE_NAME,
                    'readwrite'
                );
                transaction.objectStore(PERSISTENT_CACHE_STORE_NAME).clear();
                await waitForIndexedDbTransaction(transaction);
            });
            return entries;
        }

        return clearGarsoCache() + clearEscortListCache() +
            clearEscortAdDataCache();
    }


    function persistentCacheBridgeRequestKey(token) {
        return `${PERSISTENT_CACHE_BRIDGE_REQUEST_PREFIX}${token}`;
    }

    function persistentCacheBridgeResponseKey(token) {
        return `${PERSISTENT_CACHE_BRIDGE_RESPONSE_PREFIX}${token}`;
    }

    async function handlePersistentCacheBridgePage() {
        if (CURRENT_HOST !== 'pl.escort.club') return false;
        const pageUrl = new URL(location.href);
        const token = pageUrl.searchParams.get(PERSISTENT_CACHE_BRIDGE_PARAM);
        if (!token) return false;

        const requestKey = persistentCacheBridgeRequestKey(token);
        const responseKey = persistentCacheBridgeResponseKey(token);
        const request = GM_getValue(requestKey, null);
        let response;
        try {
            if (
                !request || typeof request !== 'object' ||
                Date.now() - Number(request.createdAt || 0) > 2 * 60 * 1000
            ) {
                throw new Error('Żądanie czyszczenia cache wygasło lub jest nieprawidłowe.');
            }

            let removed = 0;
            if (request.action === 'clear-all') {
                removed = clearPersistentCache();
            } else if (request.action === 'clear-expired') {
                removed = clearExpiredPersistentCache({
                    garsoTtlMs: Math.max(0, Number(request.garsoTtlMs) || getGarsoCacheTtlMs()),
                    escortTtlMs: Math.max(0, Number(request.escortTtlMs) || getListCacheTtlMs())
                });
            } else {
                throw new Error('Nieznana operacja czyszczenia cache.');
            }

            await persistentCacheDbOperationChain.catch(() => {});
            const stats = calculatePersistentCacheStats();
            savePersistentCacheStatsSnapshot(stats);
            response = {
                ok: true,
                removed,
                stats,
                completedAt: Date.now()
            };
        } catch (error) {
            response = {
                ok: false,
                error: error?.message || String(error),
                completedAt: Date.now()
            };
        }

        try { GM_deleteValue(requestKey); } catch (_) {}
        try { GM_setValue(responseKey, response); } catch (_) {}
        setTimeout(() => {
            try { window.close(); } catch (_) {}
        }, 80);
        return true;
    }

    function requestPersistentCacheBridgeAction(action, options = {}) {
        if (CURRENT_HOST === 'pl.escort.club') {
            return Promise.reject(new Error('Bridge cache nie jest potrzebny na Escort.club.'));
        }
        const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
        const requestKey = persistentCacheBridgeRequestKey(token);
        const responseKey = persistentCacheBridgeResponseKey(token);
        const payload = {
            action,
            createdAt: Date.now(),
            garsoTtlMs: Number(options.garsoTtlMs) || 0,
            escortTtlMs: Number(options.escortTtlMs) || 0
        };
        GM_setValue(requestKey, payload);
        try { GM_deleteValue(responseKey); } catch (_) {}

        return new Promise((resolve, reject) => {
            let settled = false;
            let listenerId = null;
            let bridgeTab = null;
            let pollTimer = null;
            let timeoutTimer = null;

            const cleanup = () => {
                if (listenerId != null && typeof GM_removeValueChangeListener === 'function') {
                    try { GM_removeValueChangeListener(listenerId); } catch (_) {}
                }
                if (pollTimer) clearInterval(pollTimer);
                if (timeoutTimer) clearTimeout(timeoutTimer);
                try { bridgeTab?.close?.(); } catch (_) {}
                try { GM_deleteValue(requestKey); } catch (_) {}
                try { GM_deleteValue(responseKey); } catch (_) {}
            };
            const finish = response => {
                if (settled) return;
                settled = true;
                cleanup();
                if (response?.ok) resolve(response);
                else reject(new Error(response?.error || 'Nie udało się wyczyścić cache na Escort.club.'));
            };
            const checkResponse = () => {
                let response = null;
                try { response = GM_getValue(responseKey, null); } catch (_) {}
                if (response) finish(response);
            };

            try {
                if (typeof GM_addValueChangeListener === 'function') {
                    listenerId = GM_addValueChangeListener(
                        responseKey,
                        (_key, _oldValue, newValue) => {
                            if (newValue) finish(newValue);
                        }
                    );
                }
                pollTimer = setInterval(checkResponse, 250);
                timeoutTimer = setTimeout(() => {
                    if (settled) return;
                    settled = true;
                    cleanup();
                    reject(new Error('Przekroczono czas oczekiwania na wyczyszczenie cache Escort.club.'));
                }, 20000);
                bridgeTab = GM_openInTab(
                    `https://pl.escort.club/?${PERSISTENT_CACHE_BRIDGE_PARAM}=${encodeURIComponent(token)}`,
                    { active: false, insert: true, setParent: true }
                );
                checkResponse();
            } catch (error) {
                if (settled) return;
                settled = true;
                cleanup();
                reject(error);
            }
        });
    }

    async function clearPersistentCacheAcrossDomains() {
        if (CURRENT_HOST === 'pl.escort.club') {
            const removed = clearPersistentCache();
            await persistentCacheDbOperationChain.catch(() => {});
            const stats = calculatePersistentCacheStats();
            savePersistentCacheStatsSnapshot(stats);
            return { ok: true, removed, stats };
        }
        return requestPersistentCacheBridgeAction('clear-all');
    }

    async function clearExpiredPersistentCacheAcrossDomains(options = {}) {
        if (CURRENT_HOST === 'pl.escort.club') {
            const removed = clearExpiredPersistentCache(options);
            await persistentCacheDbOperationChain.catch(() => {});
            const stats = calculatePersistentCacheStats();
            savePersistentCacheStatsSnapshot(stats);
            return { ok: true, removed, stats };
        }
        return requestPersistentCacheBridgeAction('clear-expired', options);
    }

    function isPersistentCacheValueExpired(value, ttlMs, now = Date.now()) {
        const checkedAt = Number(value?.checkedAt);
        return !Number.isFinite(checkedAt) || checkedAt <= 0 ||
            now - checkedAt >= ttlMs;
    }

    function clearExpiredPersistentCache({
        garsoTtlMs = getGarsoCacheTtlMs(),
        escortTtlMs = getListCacheTtlMs()
    } = {}) {
        const now = Date.now();
        let removed = 0;
        let listCacheChanged = false;

        const garsoKeys = [
            ...getGarsoSearchCountCacheKeys(),
            ...getGarsoSummaryCacheKeys(),
            ...getGarsoExtendedCacheKeys(),
            ...getGarsoTopicPageCacheKeys()
        ];
        for (const key of garsoKeys) {
            const value = readPersistentCacheValue(key, null);
            if (!isPersistentCacheValueExpired(value, garsoTtlMs, now)) continue;
            deletePersistentCacheValue(key);
            removed++;
        }

        for (const key of getEscortListCacheKeys()) {
            const value = readPersistentCacheValue(key, null);
            const sourceModes = value?.modes && typeof value.modes === 'object'
                ? value.modes
                : {};
            const freshModes = Object.fromEntries(
                Object.entries(sourceModes).filter(([, entry]) =>
                    !isPersistentCacheValueExpired(entry, escortTtlMs, now)
                )
            );
            const expiredModes = Object.keys(sourceModes).length -
                Object.keys(freshModes).length;

            if (!Object.keys(freshModes).length) {
                deletePersistentCacheValue(key);
                removed += Math.max(1, expiredModes);
                listCacheChanged = true;
            } else if (expiredModes > 0) {
                writePersistentCacheValue(key, { ...value, modes: freshModes });
                removed += expiredModes;
                listCacheChanged = true;
            }
        }

        for (const key of getEscortAdDataCacheKeys()) {
            const value = readPersistentCacheValue(key, null);
            if (!isPersistentCacheValueExpired(value, escortTtlMs, now)) continue;
            deletePersistentCacheValue(key);
            removed++;
        }

        if (listCacheChanged) {
            try { GM_deleteValue(ESCORTI_PROFILE_AD_INDEX_STORAGE_KEY); } catch (_) {}
            scheduleEscortiProfileAdIndexRebuild();
        }

        return removed;
    }

    await initializePersistentCacheStorage();
    if (await handlePersistentCacheBridgePage()) return;

    const BUTTON_COLORS = {
        checking: '#777777',
        found: '#28a745',
        empty: '#2878c8',
        error: '#dc3545'
    };

    const CATEGORY_DEFS = [
        {
            key: 'appointment',
            label: 'Umówienie',
            patterns: [
                'kontakt / umówienie', 'kontakt/umówienie', 'umówienie się', 'umowienie sie',
                'the date', 'umówienie', 'umowienie', 'umawianie', 'ustawka', 'kontakt', 'formalizmy'
            ]
        },
        {
            key: 'location',
            label: 'Lokalizacja',
            patterns: [
                'miejsce spotkania', 'lokalizacja', 'lokalizacjia', 'mieszkanie',
                'the place', 'lokum', 'lokal', 'miejsce', 'łazienka', 'lazienka'
            ]
        },
        {
            key: 'girl',
            label: 'Wygląd',
            patterns: [
                'wygląd dziewczyny', 'wyglad dziewczyny', 'wygląd panny', 'wyglad panny',
                'wygląd pani', 'wyglad pani', 'zgodność ze zdjęciami', 'zgodnosc ze zdjeciami',
                'wygląd laski', 'wyglad laski', 'wygląd', 'wyglad', 'masażystka', 'masazystka',
                'dziewczyna', 'bohaterka', 'figura', 'uroda', 'ciało', 'cialo', 'twarz',
                'panna', 'pani', 'diva', 'laska', 'osoba'
            ]
        },
        {
            key: 'oral',
            label: 'Francuz',
            patterns: [
                'francuz bez gumki', 'francuz w gumce', 'francuski', 'francuz',
                'fbg / fwg', 'fwg / fbg', 'fbg/fwg', 'fwg/fbg', 'fbg', 'fwg',
                'fgb', 'oral', 'lodowanie', 'lodzik', 'francja', 'lód', 'lod'
            ]
        },
        {
            key: 'action',
            label: 'Akcja',
            patterns: [
                'zgodność usługi z ofertą', 'zgodnosc uslugi z oferta', 'przebieg spotkania',
                'akcja / serwis', 'akcja + fbg', 'akcja / seks', 'akcja / sex', 'akcja/fbg',
                'akcja/seks', 'akcja/sex', 'seks/akcja', 'sex/akcja', 'sex/anal',
                'całość akcji', 'calosc akcji', 'akcja właściwa', 'akcja wlasciwa',
                'anal fingering', 'anal sex', 'feet licking', 'zakończenie', 'zakonczenie',
                'usługi', 'uslugi', 'przebieg', 'erotyka', 'klasyka', 'klasyk',
                'rimming', 'fisting', 'uległość', 'uleglosc', 'serwis',
                'zabawy', 'analny', 'akcja', 'seks', 'sex', 'anal', 'piss'
            ]
        },
        { key: 'massage', label: 'Masaż', patterns: ['masaż', 'masaz'] },
        {
            key: 'atmosphere',
            label: 'Atmosfera',
            patterns: [
                'podejście do klienta', 'podejscie do klienta', 'klimat spotkania',
                'the atmosphere', 'atmosphere', 'atmosfera', 'aranżacja', 'aranzacja', 'klimat', 'atmo'
            ]
        },
        {
            key: 'summary',
            label: 'Podsumowanie',
            overall: true,
            patterns: [
                'ogólna ocena', 'ogolna ocena', 'ocena ogólna', 'ocena ogolna',
                'wrażenie ogólne', 'wrazenie ogolne',
                'podsumowując', 'podsumowujac', 'podsumowanie', 'całokształt', 'caloksztalt',
                'ogółem', 'ogolem', 'ogólnie', 'ogolnie', 'całość', 'calosc',
                'spotkanie', 'ogół', 'ogol', 'ocena'
            ]
        }
    ];

    const summaryCache = new Map();
    let garsoAuthPromise = null;
    let garsoRequestQueue = Promise.resolve();
    let garsoLastRequestStartedAt = 0;
    let garsoTopicActiveRequests = 0;
    const garsoTopicRequestWaiters = [];

    const enqueueListJob = createConcurrentJobQueue(
        LIST_MAX_CONCURRENT,
        'Błąd kolejki listy'
    );
    const listInflight = new Map();

    const enqueueEscortAdDataJob = createConcurrentJobQueue(
        ESCORT_AD_DATA_MAX_CONCURRENT,
        'Błąd kolejki danych Escort.club'
    );
    const escortAdDataMemoryCache = new Map();
    const escortAdDataInflight = new Map();
    const pendingEscortiActivityCacheWrites = new Map();

    let diagnosticsState = null;
    let diagnosticsSaveTimer = null;

    function createEmptyDiagnostics() {
        return {
            updatedAt: Date.now(),
            counters: {
                requests: 0,
                garsoRequests: 0,
                escortiRequests: 0,
                escortClubRequests: 0,
                garsoRetries: 0,
                http429: 0,
                antiflood: 0,
                errors: 0,
                parserIssues: 0,
                incompleteAnalyses: 0,
                cancellations: 0
            },
            errors: [],
            parserIssues: [],
            incompleteAnalyses: [],
            cancellations: []
        };
    }

    function normalizeDiagnostics(value) {
        const empty = createEmptyDiagnostics();
        const source = value && typeof value === 'object' ? value : {};
        const normalized = {
            ...empty,
            ...source,
            counters: { ...empty.counters, ...(source.counters || {}) }
        };
        for (const key of ['errors', 'parserIssues', 'incompleteAnalyses', 'cancellations']) {
            normalized[key] = (Array.isArray(source[key]) ? source[key] : [])
                .slice(-DIAGNOSTICS_MAX_EVENTS);
        }
        return normalized;
    }

    function getDiagnosticsState() {
        if (diagnosticsState) return diagnosticsState;
        try {
            diagnosticsState = normalizeDiagnostics(
                GM_getValue(DIAGNOSTICS_STORAGE_KEY, null)
            );
        } catch (_) {
            diagnosticsState = createEmptyDiagnostics();
        }
        return diagnosticsState;
    }

    function flushDiagnostics() {
        if (diagnosticsSaveTimer) {
            clearTimeout(diagnosticsSaveTimer);
            diagnosticsSaveTimer = null;
        }
        try {
            GM_setValue(DIAGNOSTICS_STORAGE_KEY, getDiagnosticsState());
        } catch (_) {}
    }

    function scheduleDiagnosticsSave() {
        if (diagnosticsSaveTimer) return;
        diagnosticsSaveTimer = setTimeout(flushDiagnostics, 250);
    }

    function incrementDiagnosticCounter(key, amount = 1) {
        const state = getDiagnosticsState();
        state.counters[key] = Math.max(
            0,
            Number(state.counters[key] || 0) + Number(amount || 0)
        );
        state.updatedAt = Date.now();
        scheduleDiagnosticsSave();
    }

    function addDiagnosticEvent(bucket, area, message = '') {
        const state = getDiagnosticsState();
        if (!Array.isArray(state[bucket])) state[bucket] = [];
        state[bucket].push({
            at: Date.now(),
            area: String(area || 'skrypt').slice(0, 120),
            message: String(message || '').slice(0, 600)
        });
        state[bucket] = state[bucket].slice(-DIAGNOSTICS_MAX_EVENTS);
        state.updatedAt = Date.now();
        scheduleDiagnosticsSave();
    }

    function recordDiagnosticError(area, error = '') {
        incrementDiagnosticCounter('errors');
        addDiagnosticEvent(
            'errors',
            area,
            error?.message || String(error || '')
        );
    }

    function recordParserIssue(area, message = '') {
        incrementDiagnosticCounter('parserIssues');
        addDiagnosticEvent('parserIssues', area, message);
    }

    function recordIncompleteAnalysis(area, message = '') {
        incrementDiagnosticCounter('incompleteAnalyses');
        addDiagnosticEvent('incompleteAnalyses', area, message);
    }

    function recordDiagnosticCancellation(area, message = '') {
        incrementDiagnosticCounter('cancellations');
        addDiagnosticEvent('cancellations', area, message);
    }

    function recordDiagnosticRequest(url, method = 'GET') {
        incrementDiagnosticCounter('requests');
        try {
            const host = new URL(url || '', location.href).hostname.toLowerCase();
            if (/garsoniera\.com\.pl$/.test(host)) {
                incrementDiagnosticCounter('garsoRequests');
            } else if (/(?:^|\.)escorti\.pl$/.test(host)) {
                incrementDiagnosticCounter('escortiRequests');
            } else if (host === 'pl.escort.club') {
                incrementDiagnosticCounter('escortClubRequests');
            }
        } catch (_) {}
        void method;
    }

    function clearDiagnostics() {
        diagnosticsState = createEmptyDiagnostics();
        flushDiagnostics();
    }

    function sanitizeDiagnosticText(value) {
        return String(value || '')
            .replace(/https?:\/\/[^\s"'<>]+/gi, '[URL]')
            .replace(/[\w.+-]+@[\w.-]+\.[a-z]{2,}/gi, '[E-MAIL]')
            .replace(/(?:\+?48[\s.-]*)?(?:\d[\s.-]*){9,}/g, '[NUMER]')
            .replace(/\b\d{5,}\b/g, '[ID]')
            .replace(/[„“”"]([^„“”"]{2,80})[„“”"]/g, '[NAZWA]')
            .slice(0, 500);
    }

    function getDiagnosticsSnapshot({ sanitized = false } = {}) {
        flushDiagnostics();
        const state = normalizeDiagnostics(getDiagnosticsState());
        let cache = null;
        try {
            const stats = getPersistentCacheStats();
            cache = {
                enabled: SETTINGS.usePersistentCache === true,
                backend: stats.backend || (persistentCacheUsesIndexedDb ? 'IndexedDB' : 'Violentmonkey'),
                entries: stats.entries,
                garsoEntries: stats.garsoEntries,
                escortiEntries: stats.listEntries,
                escortAdEntries: stats.escortAdDataEntries,
                bytes: stats.bytes
            };
        } catch (error) {
            cache = { error: sanitizeDiagnosticText(error?.message || error) };
        }
        const cleanEvents = events => events.map(event => ({
            at: new Date(Number(event.at) || Date.now()).toISOString(),
            area: sanitized
                ? sanitizeDiagnosticText(event.area)
                : String(event.area || ''),
            message: sanitized
                ? sanitizeDiagnosticText(event.message)
                : String(event.message || '')
        }));
        return {
            format: sanitized ? DIAGNOSTICS_EXPORT_FORMAT : 'vm-garso-diagnostics',
            scriptVersion: '1.015',
            generatedAt: new Date().toISOString(),
            sanitized,
            counters: { ...state.counters },
            cache,
            errors: cleanEvents(state.errors),
            parserIssues: cleanEvents(state.parserIssues),
            incompleteAnalyses: cleanEvents(state.incompleteAnalyses),
            cancellations: cleanEvents(state.cancellations)
        };
    }

    function downloadDiagnosticsExport() {
        const payload = getDiagnosticsSnapshot({ sanitized: true });
        const stamp = new Date().toISOString().replace(/[:.]/g, '-');
        const blob = new Blob(
            [JSON.stringify(payload, null, 2)],
            { type: 'application/json;charset=utf-8' }
        );
        const url = URL.createObjectURL(blob);
        const link = makeElement('a');
        link.href = url;
        link.download = `escort-helper-diagnostyka-${stamp}.json`;
        link.style.display = 'none';
        document.body.appendChild(link);
        link.click();
        link.remove();
        setTimeout(() => URL.revokeObjectURL(url), 1000);
    }

    function createOperationCancelledError(reason = 'Operacja przerwana') {
        const error = new Error(reason);
        error.name = 'OperationCancelledError';
        error.code = 'VM_OPERATION_CANCELLED';
        return error;
    }

    function isOperationCancelledError(error) {
        return error?.code === 'VM_OPERATION_CANCELLED' ||
            error?.name === 'OperationCancelledError';
    }

    function createOperationCancelToken(label) {
        const listeners = new Set();
        return {
            label: String(label || 'Długa operacja'),
            cancelled: false,
            reason: '',
            cancel(reason = 'Przerwano na żądanie użytkownika') {
                if (this.cancelled) return;
                this.cancelled = true;
                this.reason = String(reason || 'Operacja przerwana');
                for (const listener of [...listeners]) {
                    try { listener(this.reason); } catch (_) {}
                }
                listeners.clear();
            },
            throwIfCancelled() {
                if (this.cancelled) {
                    throw createOperationCancelledError(this.reason);
                }
            },
            onCancel(listener) {
                if (typeof listener !== 'function') return () => {};
                if (this.cancelled) {
                    listener(this.reason);
                    return () => {};
                }
                listeners.add(listener);
                return () => listeners.delete(listener);
            }
        };
    }

    function waitWithCancellation(delayMs, cancelToken = null) {
        if (!cancelToken) {
            return new Promise(resolve => setTimeout(resolve, delayMs));
        }
        cancelToken.throwIfCancelled();
        return new Promise((resolve, reject) => {
            const timer = setTimeout(() => {
                unsubscribe();
                resolve();
            }, Math.max(0, Number(delayMs) || 0));
            const unsubscribe = cancelToken.onCancel(reason => {
                clearTimeout(timer);
                reject(createOperationCancelledError(reason));
            });
        });
    }

    function log(msg, data = '') {
        console.log(`[GARSO-ESCORTI] ${msg}`, data);
        if (
            data instanceof Error ||
            /(?:\bbłąd\b|nie udało|error|wyjątek)/i.test(String(msg || ''))
        ) {
            recordDiagnosticError(msg, data);
        }
    }
    function normalizeText(text) { return (text || '').replace(/\s+/g, ' ').trim().toLowerCase(); }
    function escapeHtml(text) { return String(text ?? '').replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'}[c])); }
    function setButtonColor(btn, status) {
        if (btn?.classList?.contains('vm-research-panel-row')) {
            btn.dataset.vmResultStatus = status;
            btn.style.backgroundColor = 'transparent';
            const resultLine = btn.querySelector('.vm-research-panel-value');
            if (resultLine) {
                resultLine.style.color = {
                    checking: '#d8c8d9',
                    found: '#62cf7b',
                    empty: '#80b9ee',
                    error: '#ff6262'
                }[status] || '#d8c8d9';
            }
            return;
        }
        btn.style.backgroundColor = BUTTON_COLORS[status] || BUTTON_COLORS.checking;
    }
    function avg(arr) { return arr.length ? arr.reduce((a,b) => a+b, 0) / arr.length : null; }
    function round1(n) { return n == null ? null : Math.round(n * 10) / 10; }
    function median(arr) {
        if (!arr.length) return null;
        const x = [...arr].sort((a,b) => a-b);
        const m = Math.floor(x.length / 2);
        return x.length % 2 ? x[m] : (x[m-1] + x[m]) / 2;
    }
    function digitsOnly(phone) {
        let digits = String(phone || '').replace(/\D/g, '');
        if (digits.length === 11 && digits.startsWith('48')) digits = digits.substring(2);
        return digits;
    }
    function formatEscortiDate(value) {
        if (!value) return null;
        let m = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
        if (m) return `${m[3]}.${m[2]}.${m[1]}`;
        m = value.match(/^(\d{2})[.\-/](\d{2})[.\-/](\d{4})$/);
        if (m) return `${m[1]}.${m[2]}.${m[3]}`;
        return value;
    }
    function formatShortDate(value) {
        const full = formatEscortiDate(value);
        if (!full) return '?';
        const m = full.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
        return m ? `${m[1]}.${m[2]}.${m[3].slice(-2)}` : full;
    }
    function profileDateToTime(value) {
        const full = formatEscortiDate(value);
        const m = full && full.match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
        return m ? Date.UTC(Number(m[3]), Number(m[2]) - 1, Number(m[1])) : null;
    }
    function normalizeTopicTitleKey(value) {
        return normalizeText(String(value || '').normalize('NFKC'));
    }

    const SETTINGS_UI_STYLE_ID = 'vm-garso-settings-style';

    function ensureSettingsUiStyles() {
        if (document.getElementById(SETTINGS_UI_STYLE_ID)) return;
        const style = makeElement('style');
        style.id = SETTINGS_UI_STYLE_ID;
        style.textContent = `
            #vm-garso-settings-overlay {
                position: fixed;
                inset: 0;
                z-index: 2147483647;
                display: flex;
                align-items: center;
                justify-content: center;
                padding: 16px;
                background: rgba(0,0,0,.55);
            }
            #vm-garso-settings-overlay .vm-settings-box {
                width: min(920px, calc(100vw - 32px));
                max-height: min(88vh, 820px);
                display: flex;
                flex-direction: column;
                box-sizing: border-box;
                overflow: hidden;
                padding: 20px;
                border-top: 4px solid var(--vm-settings-pink);
                border-radius: 12px;
                background: #fff;
                color: #222;
                box-shadow: 0 10px 35px rgba(0,0,0,.35);
                font: 14px/1.4 Arial, sans-serif;
            }
            #vm-garso-settings-overlay .vm-settings-title {
                margin-bottom: 4px;
                font-size: 21px;
                font-weight: 700;
                line-height: 1.2;
            }
            #vm-garso-settings-overlay .vm-settings-subtitle {
                margin-bottom: 15px;
                color: #666;
                font-size: 12px;
                line-height: 1.4;
            }
            #vm-garso-settings-overlay .vm-settings-grid {
                min-height: 0;
                flex: 1 1 auto;
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
                gap: 12px;
                overflow-y: auto;
                padding: 1px 5px 3px 1px;
            }
            #vm-garso-settings-overlay .vm-settings-section {
                min-width: 0;
                box-sizing: border-box;
                padding: 14px 15px;
                border: 1px solid #e3e3e3;
                border-top: 2px solid var(--vm-settings-pink);
                border-radius: 9px;
                background: #fcfcfc;
            }
            #vm-garso-settings-overlay .vm-settings-section.-full-width {
                grid-column: 1 / -1;
            }
            #vm-garso-settings-overlay .vm-settings-section-title {
                margin-bottom: 7px;
                color: #222;
                font-size: 14px;
                font-weight: 700;
            }
            #vm-garso-settings-overlay .vm-settings-section-title.-with-description {
                margin-bottom: 3px;
            }
            #vm-garso-settings-overlay .vm-settings-section-description {
                margin-bottom: 7px;
                color: #777;
                font-size: 11px;
                line-height: 1.35;
            }
            #vm-garso-settings-overlay .vm-settings-choice {
                display: flex;
                align-items: flex-start;
                gap: 9px;
                margin: 8px 0;
                cursor: pointer;
            }
            #vm-garso-settings-overlay .vm-settings-choice.-radio {
                gap: 8px;
                margin: 6px 0;
            }
            #vm-garso-settings-overlay .vm-settings-choice > input {
                flex: 0 0 auto;
                width: 16px;
                height: 16px;
                margin-top: 2px;
                accent-color: var(--vm-settings-pink);
            }
            #vm-garso-settings-overlay .vm-settings-choice.-radio > input {
                width: 15px;
                height: 15px;
            }
            #vm-garso-settings-overlay .vm-settings-action-button {
                padding: 6px 11px;
                border: 1px solid var(--vm-settings-pink);
                border-radius: 6px;
                background: #fff;
                color: var(--vm-settings-pink);
                cursor: pointer;
                font-size: 12px;
                font-weight: 700;
            }
            #vm-garso-settings-overlay .vm-settings-action-button:disabled {
                opacity: .5;
                cursor: not-allowed;
            }
            #vm-garso-settings-overlay .vm-settings-actions {
                flex: 0 0 auto;
                display: flex;
                align-items: center;
                justify-content: flex-end;
                flex-wrap: wrap;
                gap: 8px;
                margin-top: 13px;
                padding-top: 13px;
                border-top: 1px solid #e7e7e7;
            }
            #vm-garso-settings-overlay .vm-settings-note {
                margin: 0 auto 0 0;
                color: #666;
                font-size: 12px;
            }
            #vm-garso-settings-overlay .vm-settings-footer-button {
                min-width: 92px;
                padding: 8px 15px;
                border: 1px solid #bbb;
                border-radius: 7px;
                cursor: pointer;
                font-size: 13px;
                font-weight: 700;
            }
            #vm-garso-settings-overlay .vm-settings-footer-button.-primary {
                border-color: var(--vm-settings-pink);
                background: var(--vm-settings-pink);
                color: #fff;
            }
            #vm-garso-settings-overlay .vm-settings-footer-button.-secondary {
                background: #f5f5f5;
                color: #333;
            }
            #vm-garso-settings-overlay .vm-settings-inline-row {
                display: flex;
                align-items: center;
                flex-wrap: wrap;
                gap: 7px;
            }
            #vm-garso-settings-overlay .vm-settings-inline-row.-indented {
                margin-left: 25px;
            }
            #vm-garso-settings-overlay .vm-settings-inline-row.-nowrap {
                flex-wrap: nowrap;
                white-space: nowrap;
            }
            #vm-garso-settings-overlay .vm-settings-select {
                box-sizing: border-box;
                padding: 6px 8px;
                border: 1px solid #bbb;
                border-radius: 5px;
                background: #fff;
                color: #222;
                font-size: 12px;
            }
            #vm-garso-settings-overlay .vm-settings-select.-compact {
                padding: 4px 26px 4px 7px;
            }
            #vm-garso-settings-overlay .vm-settings-select.-small {
                padding: 4px 6px;
                border-radius: 4px;
            }
            #vm-garso-settings-overlay .vm-settings-select.-wide-arrow {
                padding: 5px 26px 5px 7px;
            }
            #vm-garso-settings-overlay .vm-settings-info {
                color: #777;
                font-size: 11px;
                line-height: 1.35;
            }
            #vm-garso-settings-overlay .vm-settings-info.-indented {
                margin-left: 25px;
            }
            #vm-garso-settings-overlay .vm-settings-warning {
                color: #a33a24;
                font-size: 11px;
                font-weight: 700;
                line-height: 1.35;
            }
            #vm-garso-settings-overlay .vm-settings-options-box {
                margin: -1px 0 10px 25px;
                padding: 8px 10px;
                border: 1px solid #e6e6e6;
                border-radius: 7px;
                background: #fff;
            }
            #vm-garso-settings-overlay .vm-settings-field-label {
                flex: 0 0 auto;
                margin-bottom: 0;
                color: #444;
                font-size: 12px;
                font-weight: 600;
                white-space: nowrap;
            }
            #vm-garso-settings-overlay .vm-settings-field-description {
                margin-top: 5px;
                color: #666;
                font-size: 11px;
                line-height: 1.35;
            }
            #vm-garso-settings-overlay .vm-settings-muted-label {
                color: #555;
                font-size: 12px;
            }
            #vm-garso-settings-overlay .vm-settings-cache-button {
                padding: 5px 9px;
                border: 1px solid #bbb;
                border-radius: 5px;
                background: #f5f5f5;
                color: #333;
                cursor: pointer;
                font-size: 12px;
                font-weight: 600;
            }
            #vm-garso-settings-overlay .vm-settings-cache-size,
            #vm-garso-settings-overlay .vm-settings-transfer-status {
                min-width: 0;
                color: #666;
                font-size: 11px;
                line-height: 1.3;
            }
            #vm-garso-settings-overlay .vm-settings-cache-size {
                white-space: nowrap;
            }
            #vm-garso-settings-overlay .vm-settings-results-columns {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(min(270px, 100%), 1fr));
                column-gap: 22px;
                align-items: start;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-section-label {
                margin-bottom: 4px;
                cursor: default;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-box {
                margin: 3px 0 2px 25px;
                padding: 6px 8px;
                border: 1px solid #e6e6e6;
                border-radius: 7px;
                background: #fff;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-toolbar {
                display: flex;
                align-items: center;
                justify-content: flex-start;
                gap: 6px;
                margin-bottom: 4px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-add,
            #vm-garso-settings-overlay .vm-settings-default-city-remove {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                border: 1px solid #d0d0d0;
                border-radius: 5px;
                background: #fff;
                color: #444;
                cursor: pointer;
                line-height: 1;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-add {
                width: 25px;
                height: 23px;
                font-size: 17px;
                font-weight: 700;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-remove {
                width: 19px;
                height: 19px;
                flex: 0 0 19px;
                font-size: 13px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-add:disabled,
            #vm-garso-settings-overlay .vm-settings-default-city-remove:disabled {
                opacity: .4;
                cursor: default;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-rows {
                display: flex;
                flex-direction: column;
                gap: 2px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-row {
                display: flex;
                align-items: center;
                gap: 6px;
                min-height: 21px;
                padding: 1px 2px;
                color: #555;
                font-size: 11px;
                line-height: 1.25;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-row-text {
                min-width: 0;
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-empty {
                padding: 2px 1px 3px;
                color: #888;
                font-size: 11px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-add-form {
                display: none;
                margin-top: 5px;
                padding-top: 5px;
                border-top: 1px solid #ededed;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-add-form.-visible {
                display: block;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-controls {
                display: grid;
                grid-template-columns: repeat(3, minmax(0, 1fr));
                gap: 6px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-controls select {
                width: 100%;
                min-width: 0;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-add-actions {
                display: flex;
                align-items: center;
                gap: 5px;
                margin-top: 5px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-add-confirm,
            #vm-garso-settings-overlay .vm-settings-default-city-add-cancel {
                padding: 4px 8px;
                font-size: 11px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-info {
                display: none;
                margin-top: 5px;
            }
            #vm-garso-settings-overlay .vm-settings-default-city-info.-visible {
                display: block;
            }
            #vm-garso-settings-overlay .vm-settings-watch-interval {
                margin: 7px 0 5px 25px;
                color: #555;
                white-space: nowrap;
            }
            #vm-garso-settings-overlay .vm-settings-runtime-info {
                margin: 2px 0 5px 25px;
            }
            #vm-garso-settings-overlay .vm-settings-auto-warning {
                margin: 3px 0 2px;
                color: #a33a24;
                font-size: 11px;
                line-height: 1.35;
            }
            #vm-garso-settings-overlay .vm-settings-auto-box {
                margin: -2px 0 8px 25px;
                padding: 7px 9px;
                border: 1px solid #e3e3e3;
                border-radius: 7px;
                background: #fff;
            }
            #vm-garso-settings-overlay .vm-settings-auto-info {
                margin: -5px 0 7px 25px;
            }
            #vm-garso-settings-overlay .vm-settings-cache-requirement {
                display: none;
                margin: -4px 0 7px 25px;
                color: #b33a24;
            }
            #vm-garso-settings-overlay .vm-settings-top-phone-option {
                margin: 0;
                color: #555;
                font-size: 12px;
            }
            #vm-garso-settings-overlay .vm-settings-top-phone-options {
                column-gap: 20px;
                row-gap: 7px;
                margin: 3px 0 10px 25px;
            }
            #vm-garso-settings-overlay .vm-settings-phone-copy-row {
                gap: 8px;
                margin: 10px 0 4px;
            }
            #vm-garso-settings-overlay .vm-settings-cache-info {
                margin: -5px 0 7px 25px;
                line-height: 1.3;
            }
            #vm-garso-settings-overlay .vm-settings-cache-refresh {
                gap: 8px;
                margin: 4px 0 8px 25px;
                white-space: nowrap;
            }
            #vm-garso-settings-overlay .vm-settings-cache-controls {
                gap: 8px;
                margin: 2px 0 4px 25px;
            }
            #vm-garso-settings-overlay .vm-settings-transfer-row {
                gap: 8px;
            }
            #vm-garso-settings-overlay .vm-diagnostics-overview {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
                gap: 7px;
                margin: 5px 0 9px;
            }
            #vm-garso-settings-overlay .vm-diagnostics-card {
                min-width: 0;
                padding: 8px 9px;
                border: 1px solid #e4d8df;
                border-radius: 7px;
                background: #fff;
            }
            #vm-garso-settings-overlay .vm-diagnostics-card-value {
                color: #333;
                font-size: 16px;
                font-weight: 800;
                line-height: 1.1;
            }
            #vm-garso-settings-overlay .vm-diagnostics-card-label {
                margin-top: 3px;
                color: #777;
                font-size: 10px;
                line-height: 1.25;
            }
            #vm-garso-settings-overlay .vm-diagnostics-lists {
                display: grid;
                grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
                gap: 8px;
                margin: 7px 0 9px;
            }
            #vm-garso-settings-overlay .vm-diagnostics-list {
                min-width: 0;
                max-height: 128px;
                overflow: auto;
                padding: 7px 8px;
                border: 1px solid #e8e1e5;
                border-radius: 7px;
                background: #fff;
                color: #555;
                font-size: 10px;
                line-height: 1.35;
            }
            #vm-garso-settings-overlay .vm-diagnostics-list-title {
                margin-bottom: 4px;
                color: var(--vm-settings-pink);
                font-size: 11px;
                font-weight: 800;
            }
            #vm-garso-settings-overlay .vm-diagnostics-event {
                padding: 3px 0;
                border-top: 1px solid #f0ecee;
                overflow-wrap: anywhere;
            }
            #vm-garso-settings-overlay .vm-diagnostics-event:first-of-type {
                border-top: 0;
            }
            #vm-garso-settings-overlay .vm-diagnostics-empty {
                color: #888;
                font-style: italic;
            }
            #vm-garso-settings-overlay .vm-diagnostics-privacy {
                margin-top: 7px;
                color: #777;
                font-size: 10px;
                line-height: 1.35;
            }
            .vm-escort-list-init-error {
                box-sizing: border-box;
                margin: 10px 0;
                padding: 9px 11px;
                border: 1px solid #d9534f;
                border-radius: 7px;
                background: rgba(217,83,79,.1);
                color: #a52320;
                font: 700 12px/1.35 Arial, sans-serif;
            }
        `;
        document.head.appendChild(style);
    }

    function openSettingsModal(options = {}) {
        if (document.getElementById('vm-garso-settings-overlay')) return;

        ensureSettingsUiStyles();
        const firstRun = !!options.firstRun;
        const current = getSettings();
        const currentWatch = getWatchSettings();
        const overlay = makeElement('div');
        overlay.id = 'vm-garso-settings-overlay';
        overlay.style.setProperty('--vm-settings-pink', getEscortPagePinkColor());

        const box = makeElement('div', 'vm-settings-box');

        const title = makeElement('div', 'vm-settings-title', 'Ustawienia główne');

        const subtitle = makeElement('div', 'vm-settings-subtitle');
        subtitle.textContent = firstRun
            ? 'To pierwsze uruchomienie bez zapisanego cache. Wybierz sposób działania skryptu przed rozpoczęciem sprawdzania.'
            : 'Dostosuj sprawdzanie, agregowanie wyników, wygląd strony i lokalny cache.';

        const contentGrid = makeElement('div', 'vm-settings-grid');

        function makeSettingsSection(sectionTitle, description = '', fullWidth = false) {
            const section = makeElement('section', 'vm-settings-section');
            section.classList.toggle('-full-width', fullWidth);

            const heading = makeElement('div', 'vm-settings-section-title');
            heading.classList.toggle('-with-description', !!description);
            heading.textContent = sectionTitle;
            section.appendChild(heading);

            if (description) {
                const info = makeElement('div', 'vm-settings-section-description', description);
                section.appendChild(info);
            }

            return section;
        }

        function makeCheckbox(labelText, checked) {
            const label = makeElement('label', 'vm-settings-choice');

            const input = makeElement('input');
            input.type = 'checkbox';
            input.checked = checked;

            const text = makeElement('span', '', labelText);

            label.appendChild(input);
            label.appendChild(text);
            return { label, input };
        }

        function makeRadioOption(name, value, labelText, checked) {
            const label = makeElement('label', 'vm-settings-choice -radio');

            const input = makeElement('input');
            input.type = 'radio';
            input.name = name;
            input.value = value;
            input.checked = checked;

            const text = makeElement('div', '', labelText);
            label.append(input, text);
            return { label, input, text };
        }

        function fitSelectToLongestOption(select, minimumCharacters = 0) {
            const longest = [...select.options].reduce(
                (length, option) => Math.max(length, option.textContent.length),
                0
            );
            select.style.width = `${Math.max(minimumCharacters, longest) + 4}ch`;
            select.style.maxWidth = '100%';
            select.style.flex = '0 1 auto';
            select.style.textAlign = 'left';
        }

        const watchEnabledCheck = makeCheckbox(
            'Włącz obserwowanie profili i anonsów',
            currentWatch.enabled
        );
        const watchOnlineCountCheck = makeCheckbox(
            'Stale pokazuj liczbę obserwowanych anonsów online w rogu strony',
            current.showWatchOnlineCount
        );
        let persistentCacheDraftEnabled = current.usePersistentCache === true;
        let watchEnabledDraft = currentWatch.enabled === true;
        // Na niektórych stronach natywny checkbox jest wizualnie przełączany,
        // ale przed zapisem jego `checked` wraca do poprzedniej wartości.
        // Ten jeden przełącznik ma więc własny, kontrolowany stan.
        watchEnabledCheck.input.style.pointerEvents = 'none';
        watchEnabledCheck.label.tabIndex = 0;
        watchEnabledCheck.label.setAttribute('role', 'checkbox');
        const watchIntervalRow = makeElement('label', 'vm-settings-inline-row -nowrap vm-settings-watch-interval');
        const watchIntervalLabel = makeElement('span', '', 'Sprawdzaj co:');
        watchIntervalLabel.style.fontSize = '12px';
        const watchIntervalSelect = makeElement('select');
        appendSelectOptions(watchIntervalSelect, WATCH_INTERVAL_SELECT_OPTIONS);
        watchIntervalSelect.value = String(currentWatch.intervalMinutes);
        watchIntervalSelect.className = 'vm-settings-select -compact';
        fitSelectToLongestOption(watchIntervalSelect);
        watchIntervalRow.append(watchIntervalLabel, watchIntervalSelect);
        watchOnlineCountCheck.label.style.marginLeft = '25px';
        const watchRuntimeInfo = makeElement('div', 'vm-settings-info vm-settings-runtime-info', 'Automatyczne sprawdzanie profili i anonsów działa, gdy co najmniej jedna karta Escort.club, Escorti.pl lub Garsoniera.com.pl jest otwarta.');

        const syncWatchSettingsControls = () => {
            watchEnabledCheck.input.checked = watchEnabledDraft;
            watchEnabledCheck.input.disabled = !persistentCacheDraftEnabled;
            watchEnabledCheck.label.setAttribute(
                'aria-checked',
                watchEnabledDraft ? 'true' : 'false'
            );
            watchEnabledCheck.label.setAttribute(
                'aria-disabled',
                persistentCacheDraftEnabled ? 'false' : 'true'
            );
            watchEnabledCheck.label.style.opacity = persistentCacheDraftEnabled ? '1' : '.5';
            watchEnabledCheck.label.style.cursor = persistentCacheDraftEnabled
                ? 'pointer'
                : 'not-allowed';
            const onlineCountEnabled = persistentCacheDraftEnabled && watchEnabledDraft;
            watchOnlineCountCheck.input.disabled = !onlineCountEnabled;
            watchOnlineCountCheck.label.style.opacity = onlineCountEnabled ? '1' : '.5';
            watchOnlineCountCheck.label.style.cursor = onlineCountEnabled
                ? 'pointer'
                : 'not-allowed';
            watchIntervalSelect.disabled = !persistentCacheDraftEnabled || !watchEnabledDraft;
            watchIntervalRow.style.opacity =
                persistentCacheDraftEnabled && watchEnabledDraft ? '1' : '.5';
        };
        const toggleWatchEnabledDraft = () => {
            if (!persistentCacheDraftEnabled) return;
            watchEnabledDraft = !watchEnabledDraft;
            syncWatchSettingsControls();
        };
        watchEnabledCheck.label.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            toggleWatchEnabledDraft();
        });
        watchEnabledCheck.label.addEventListener('keydown', event => {
            if (event.key !== ' ' && event.key !== 'Enter') return;
            event.preventDefault();
            event.stopPropagation();
            toggleWatchEnabledDraft();
        });
        syncWatchSettingsControls();

        const autoCheck = makeCheckbox(
            'Automatycznie sprawdzaj Garsonierę po otwarciu strony z anonsem',
            current.autoGarsoCheck
        );
        const selectedAutoGarsoAnalysisMode = normalizeAutoGarsoAnalysisMode(
            current.autoGarsoAnalysisMode
        );
        const autoGarsoCountRadio = makeRadioOption(
            'vm-auto-garso-analysis-mode',
            'count',
            'Sprawdzaj, czy są opinie (2 zapytania)',
            selectedAutoGarsoAnalysisMode === 'count'
        );
        const autoGarsoContentRadio = makeRadioOption(
            'vm-auto-garso-analysis-mode',
            'content',
            'Sprawdzaj treść opinii (>2 zapytania)',
            selectedAutoGarsoAnalysisMode === 'content'
        );
        const autoGarsoModeWarning = makeElement('div', 'vm-settings-auto-warning', 'Uwaga: częste pełne analizy mogą uruchomić zabezpieczenie antyfloodowe i spowodować czasową blokadę na forum a może nawet bana.');
        const autoGarsoModeBox = makeElement('div', 'vm-settings-auto-box');
        autoGarsoContentRadio.text.appendChild(autoGarsoModeWarning);
        autoGarsoModeBox.append(
            autoGarsoCountRadio.label,
            autoGarsoContentRadio.label
        );
        const autoCheckInfo = makeElement('div', 'vm-settings-info vm-settings-auto-info', 'Jeżeli odpowiedni cache dla wybranego trybu nie jest przeterminowany, skrypt korzysta z zapisanych danych i nie odpytuje garsoniera.com.pl.');

        const mergeEscortAdsCheck = makeCheckbox(
            'Agreguj powiązane anonse Escort.club',
            current.mergeEscortAds
        );

        const aggregationCacheRequirementInfo = makeElement('div', 'vm-settings-warning vm-settings-cache-requirement', 'Agregowanie wymaga włączenia cache.');

        const aggregationModeBox = makeElement('div', 'vm-settings-options-box');

        const aggregationModeLabel = makeElement('label', 'vm-settings-field-label', 'Sposób agregowania kafelków:');

        const aggregationModeSelect = makeElement('select');
        const selectedAggregationMode = normalizeEscortAggregationMode(
            current.escortAggregationMode
        );

        appendSelectOptions(aggregationModeSelect, ESCORT_AGGREGATION_MODE_OPTIONS, selectedAggregationMode);

        aggregationModeSelect.className = 'vm-settings-select';
        fitSelectToLongestOption(aggregationModeSelect);

        const aggregationModeSelectRow = makeElement('div', 'vm-settings-inline-row');

        aggregationModeSelectRow.append(
            aggregationModeLabel,
            aggregationModeSelect
        );

        const aggregationModeDescription = makeElement('div', 'vm-settings-field-description');

        const aggregationModeWarning = makeElement('div', 'vm-settings-warning vm-settings-field-description', 'Uwaga: agregowanie z wykorzystaniem wyszukiwarki Escort.club może trwać dłużej.');
        aggregationModeWarning.style.display = 'none';

        function syncAggregationModeControls() {
            const enabled =
                persistentCacheDraftEnabled && mergeEscortAdsCheck.input.checked;
            const mode = normalizeEscortAggregationMode(aggregationModeSelect.value);
            aggregationModeSelect.disabled = !enabled;
            aggregationModeBox.style.opacity = enabled ? '1' : '.5';
            aggregationModeDescription.textContent =
                ESCORT_AGGREGATION_MODE_DESCRIPTIONS[mode] || '';
            aggregationModeWarning.style.display = mode === 'phone-escort-search'
                ? 'block'
                : 'none';
        }

        aggregationModeSelect.addEventListener('change', syncAggregationModeControls);
        mergeEscortAdsCheck.input.addEventListener('change', syncAggregationModeControls);
        aggregationModeBox.append(
            aggregationModeSelectRow,
            aggregationModeDescription,
            aggregationModeWarning
        );
        syncAggregationModeControls();

        const compactSummaryComparisonModeBox = makeElement('div', 'vm-settings-options-box');

        const compactSummaryComparisonModeLabel = makeElement('label', 'vm-settings-field-label', 'Sposób znajdywania anonsów tej samej osoby w skróconym podsumowaniu:');

        const compactSummaryComparisonModeSelect = makeElement('select');
        const selectedCompactSummaryComparisonMode = normalizeEscortCompactSummaryMode(
            current.compactSummaryComparisonMode
        );
        appendSelectOptions(compactSummaryComparisonModeSelect, ESCORT_COMPACT_SUMMARY_MODE_OPTIONS, selectedCompactSummaryComparisonMode);
        compactSummaryComparisonModeSelect.className = 'vm-settings-select';
        fitSelectToLongestOption(compactSummaryComparisonModeSelect);

        const compactSummaryComparisonModeRow = makeElement('div', 'vm-settings-inline-row');

        compactSummaryComparisonModeRow.append(
            compactSummaryComparisonModeLabel,
            compactSummaryComparisonModeSelect
        );

        const compactSummaryComparisonModeDescription = makeElement('div', 'vm-settings-field-description');
        const syncCompactSummaryComparisonMode = () => {
            const mode = normalizeEscortCompactSummaryMode(
                compactSummaryComparisonModeSelect.value
            );
            compactSummaryComparisonModeDescription.textContent =
                ESCORT_COMPACT_SUMMARY_MODE_DESCRIPTIONS[mode] || '';
        };
        compactSummaryComparisonModeSelect.addEventListener(
            'change',
            syncCompactSummaryComparisonMode
        );
        compactSummaryComparisonModeBox.append(
            compactSummaryComparisonModeRow,
            compactSummaryComparisonModeDescription
        );
        syncCompactSummaryComparisonMode();

        const escortiTileButtonCheck = makeCheckbox(
            'Pokaż przycisk escorti.pl na kafelkach',
            current.showEscortiTileButton
        );

        const escortiTileDataCheck = makeCheckbox(
            'Pokaż dane z escorti.pl na kafelkach (ilość powiązanych anonsów, datę założenia profilu, ilość tematów na Garso)',
            current.showEscortiTileData
        );

        const searchResultPricesCheck = makeCheckbox(
            'Pokazuj cenę na kafelkach (za jednostkę czasu wybraną na stronie wyników)',
            current.showPricesInSearchResults
        );

        const currentSearchLocations = normalizeSearchLocations(
            current.searchLocations,
            current.showDefaultCityButton,
            current.defaultSearchLocation
        );
        const LOCATION_SELECT_EMPTY_LABEL = '—';
        const defaultCitySectionLabel = makeElement(
            'div',
            'vm-settings-choice vm-settings-default-city-section-label'
        );
        defaultCitySectionLabel.appendChild(
            makeElement('span', '', 'Zapisane lokalizacje wyszukiwania')
        );

        const defaultCityOptionsBox = makeElement('div', 'vm-settings-default-city-box');
        const defaultCityToolbar = makeElement('div', 'vm-settings-default-city-toolbar');
        const defaultCityAddButton = makeButton('vm-settings-default-city-add', '+');
        defaultCityAddButton.type = 'button';
        defaultCityAddButton.title = 'Dodaj lokalizację (maks. 4)';
        defaultCityToolbar.append(defaultCityAddButton);

        const defaultCityRowsBox = makeElement('div', 'vm-settings-default-city-rows');
        const defaultCityEmpty = makeElement(
            'div',
            'vm-settings-default-city-empty',
            'Brak zapisanych lokalizacji.'
        );
        const defaultCityInfo = makeElement('div', 'vm-settings-info vm-settings-default-city-info');
        const defaultCityRows = [];

        // Listy Escort.club pobieramy dopiero wtedy, gdy użytkownik chce
        // dodać nową lokalizację. Zapisane pozycje pozostają zwykłą,
        // kompaktową listą tekstową i można je zawsze usuwać.
        let locationSettingsBridge = null;
        let locationSettingsControls = null;
        let locationSettingsSourceReady = false;
        let locationSettingsLoadQueue = Promise.resolve();

        const LOCATION_PLACEHOLDER_RE = /^(?:państwo|panstwo|województwo|wojewodztwo|miasto|dzielnica|wyszukaj|wybierz(?:\s+.*)?|select)$/i;

        const getLocationSettingsSourceOptions = source => {
            if (Array.isArray(source)) return source;
            return source ? getEscortSelectOptionData(source) : [];
        };
        const getLocationSettingsBridge = () => {
            if (!locationSettingsBridge) {
                locationSettingsBridge = createEscortLocationSettingsBridge();
            }
            return locationSettingsBridge;
        };
        const loadLocationSettingsOptions = async locationData => {
            locationSettingsControls = await getLocationSettingsBridge().load(locationData);
            locationSettingsSourceReady = true;
            return locationSettingsControls;
        };
        const populateLocationSelect = (
            target,
            source,
            selectedValue,
            selectedLabel,
            { allowEmpty = true, fallbackOptions = [] } = {}
        ) => {
            const options = source
                ? getLocationSettingsSourceOptions(source)
                : fallbackOptions.map(([value, label]) => ({ value: String(value), label }));
            const normalized = [];
            const seen = new Set();
            const add = (value, label) => {
                const optionValue = String(value ?? '');
                const optionLabel = String(label || '').replace(/\s+/g, ' ').trim();
                if (!optionLabel) return;
                if (LOCATION_PLACEHOLDER_RE.test(optionLabel)) return;
                const key = `${optionValue}::${optionLabel.toLocaleLowerCase('pl-PL')}`;
                if (seen.has(key)) return;
                seen.add(key);
                normalized.push([optionValue, optionLabel]);
            };
            if (allowEmpty) add('', LOCATION_SELECT_EMPTY_LABEL);
            for (const option of options) add(option.value, option.label);
            if (selectedValue || selectedLabel) add(selectedValue, selectedLabel);
            target.replaceChildren();
            appendSelectOptions(target, normalized, selectedValue);
            const selectedOption = [...target.options].find(option =>
                String(option.value) === String(selectedValue ?? '')
            ) || [...target.options].find(option =>
                selectedLabel && String(option.textContent || '')
                    .replace(/\s+/g, ' ')
                    .trim()
                    .toLocaleLowerCase('pl-PL') === String(selectedLabel)
                        .replace(/\s+/g, ' ')
                        .trim()
                        .toLocaleLowerCase('pl-PL')
            );
            if (selectedOption) target.value = selectedOption.value;
            else if (allowEmpty) target.value = '';
        };
        const getSelectedLocationPart = select => {
            const value = String(select.value || '');
            const rawLabel = String(select.selectedOptions[0]?.textContent || '')
                .replace(/\s+/g, ' ')
                .trim();
            return {
                value,
                label: !value && rawLabel === LOCATION_SELECT_EMPTY_LABEL ? '' : rawLabel
            };
        };
        const normalizePolishLocationFromSelects = (province, city, district) => normalizeDefaultSearchLocation({
            countryValue: '33',
            countryLabel: 'Polska',
            provinceValue: getSelectedLocationPart(province).value,
            provinceLabel: getSelectedLocationPart(province).label,
            cityValue: getSelectedLocationPart(city).value,
            cityLabel: getSelectedLocationPart(city).label,
            districtValue: getSelectedLocationPart(district).value,
            districtLabel: getSelectedLocationPart(district).label
        });
        const getDefaultCityRowLocation = row => normalizeDefaultSearchLocation(row.locationData);
        const enqueueLocationSettingsLoad = task => {
            const scheduled = locationSettingsLoadQueue
                .catch(() => {})
                .then(task);
            locationSettingsLoadQueue = scheduled.catch(() => {});
            return scheduled;
        };
        const getSavedLocationSettingsLabel = locationData => {
            const selected = normalizeDefaultSearchLocation(locationData);
            return [
                selected.provinceLabel || selected.provinceValue,
                selected.cityLabel || selected.cityValue,
                selected.districtLabel || selected.districtValue
            ].filter(Boolean).join(', ') || 'Lokalizacja';
        };
        const setDefaultCityInfo = (message = '') => {
            defaultCityInfo.textContent = message;
            defaultCityInfo.classList.toggle('-visible', !!message);
        };
        const updateDefaultCityRowsUi = () => {
            defaultCityEmpty.style.display = defaultCityRows.length ? 'none' : 'block';
            defaultCityAddButton.disabled = defaultCityRows.length >= 4 || pendingLocationForm.busy || pendingLocationForm.visible;
            defaultCityAddButton.title = defaultCityRows.length >= 4
                ? 'Można zapisać maksymalnie 4 lokalizacje'
                : 'Dodaj lokalizację (maks. 4)';
        };
        const createSavedLocationRow = locationData => {
            if (defaultCityRows.length >= 4) return null;
            const normalized = normalizeDefaultSearchLocation(locationData);
            const hasProvince = !!(normalized.provinceValue || normalized.provinceLabel);
            const hasCity = !!(normalized.cityValue || normalized.cityLabel);
            if (!hasProvince && !hasCity) return null;

            const row = {
                root: makeElement('div', 'vm-settings-default-city-row'),
                remove: makeButton('vm-settings-default-city-remove', '×'),
                text: makeElement('span', 'vm-settings-default-city-row-text'),
                locationData: normalized,
                busy: false
            };
            row.remove.type = 'button';
            row.remove.title = `Usuń: ${getSavedLocationSettingsLabel(normalized)}`;
            row.text.textContent = getSavedLocationSettingsLabel(normalized);
            row.text.title = row.text.textContent;
            row.root.append(row.remove, row.text);
            row.remove.addEventListener('click', () => {
                const index = defaultCityRows.indexOf(row);
                if (index >= 0) defaultCityRows.splice(index, 1);
                row.root.remove();
                updateDefaultCityRowsUi();
            });
            defaultCityRows.push(row);
            defaultCityRowsBox.appendChild(row.root);
            updateDefaultCityRowsUi();
            return row;
        };

        const pendingLocationForm = {
            root: makeElement('div', 'vm-settings-default-city-add-form'),
            controls: makeElement('div', 'vm-settings-default-city-controls'),
            province: makeElement('select', 'vm-settings-select'),
            city: makeElement('select', 'vm-settings-select'),
            district: makeElement('select', 'vm-settings-select'),
            actions: makeElement('div', 'vm-settings-default-city-add-actions'),
            confirm: makeButton('vm-settings-action-button vm-settings-default-city-add-confirm', 'Dodaj'),
            cancel: makeButton('vm-settings-action-button vm-settings-default-city-add-cancel', 'Anuluj'),
            busy: false,
            visible: false
        };
        pendingLocationForm.confirm.type = 'button';
        pendingLocationForm.cancel.type = 'button';
        pendingLocationForm.province.title = 'Województwo';
        pendingLocationForm.city.title = 'Miasto';
        pendingLocationForm.district.title = 'Dzielnica';
        pendingLocationForm.controls.append(
            pendingLocationForm.province,
            pendingLocationForm.city,
            pendingLocationForm.district
        );
        pendingLocationForm.actions.append(
            pendingLocationForm.confirm,
            pendingLocationForm.cancel
        );
        pendingLocationForm.root.append(
            pendingLocationForm.controls,
            pendingLocationForm.actions
        );

        const setPendingLocationBusy = (busy, message = '') => {
            pendingLocationForm.busy = !!busy;
            for (const select of [
                pendingLocationForm.province,
                pendingLocationForm.city,
                pendingLocationForm.district
            ]) {
                select.disabled = pendingLocationForm.busy;
            }
            pendingLocationForm.confirm.disabled = pendingLocationForm.busy;
            pendingLocationForm.cancel.disabled = pendingLocationForm.busy;
            updateDefaultCityRowsUi();
            setDefaultCityInfo(message);
        };
        const resetPendingLocationForm = () => {
            populateLocationSelect(pendingLocationForm.province, locationSettingsControls?.province, '', '');
            populateLocationSelect(pendingLocationForm.city, null, '', '');
            populateLocationSelect(pendingLocationForm.district, null, '', '');
        };
        const hidePendingLocationForm = () => {
            pendingLocationForm.visible = false;
            pendingLocationForm.root.classList.remove('-visible');
            setDefaultCityInfo('');
            updateDefaultCityRowsUi();
        };
        const ensureLocationSettingsReady = async () => {
            if (locationSettingsSourceReady) return locationSettingsControls;
            const controls = await loadLocationSettingsOptions({
                countryValue: '33',
                countryLabel: 'Polska'
            });
            return controls;
        };
        const showPendingLocationForm = async () => {
            if (defaultCityRows.length >= 4 || pendingLocationForm.busy) return;
            pendingLocationForm.visible = true;
            pendingLocationForm.root.classList.add('-visible');
            setPendingLocationBusy(true, 'Pobieranie list lokalizacji z Escort.club…');
            try {
                await ensureLocationSettingsReady();
                resetPendingLocationForm();
                setPendingLocationBusy(false, '');
            } catch (error) {
                setPendingLocationBusy(false, error?.message || 'Nie udało się pobrać lokalizacji z Escort.club.');
            }
        };

        pendingLocationForm.province.addEventListener('change', () => {
            if (pendingLocationForm.busy) return;
            const partial = normalizePolishLocationFromSelects(
                pendingLocationForm.province,
                pendingLocationForm.city,
                pendingLocationForm.district
            );
            populateLocationSelect(pendingLocationForm.city, null, '', '');
            populateLocationSelect(pendingLocationForm.district, null, '', '');
            if (!partial.provinceValue && !partial.provinceLabel) return;
            setPendingLocationBusy(true, 'Pobieranie miast…');
            let loadError = '';
            enqueueLocationSettingsLoad(async () => {
                const controls = await loadLocationSettingsOptions(partial);
                populateLocationSelect(
                    pendingLocationForm.province,
                    controls?.province,
                    partial.provinceValue,
                    partial.provinceLabel
                );
                populateLocationSelect(pendingLocationForm.city, controls?.city, '', '');
            }).catch(error => {
                loadError = error?.message || 'Nie udało się pobrać miast.';
            }).finally(() => setPendingLocationBusy(false, loadError));
        });

        pendingLocationForm.city.addEventListener('change', () => {
            if (pendingLocationForm.busy) return;
            const partial = normalizePolishLocationFromSelects(
                pendingLocationForm.province,
                pendingLocationForm.city,
                pendingLocationForm.district
            );
            populateLocationSelect(pendingLocationForm.district, null, '', '');
            if (!partial.cityValue && !partial.cityLabel) return;
            setPendingLocationBusy(true, 'Pobieranie dzielnic…');
            let loadError = '';
            enqueueLocationSettingsLoad(async () => {
                const controls = await loadLocationSettingsOptions(partial);
                populateLocationSelect(
                    pendingLocationForm.province,
                    controls?.province,
                    partial.provinceValue,
                    partial.provinceLabel
                );
                populateLocationSelect(
                    pendingLocationForm.city,
                    controls?.city,
                    partial.cityValue,
                    partial.cityLabel
                );
                populateLocationSelect(pendingLocationForm.district, controls?.district, '', '');
            }).catch(error => {
                loadError = error?.message || 'Nie udało się pobrać dzielnic.';
            }).finally(() => setPendingLocationBusy(false, loadError));
        });

        pendingLocationForm.confirm.addEventListener('click', () => {
            if (pendingLocationForm.busy || defaultCityRows.length >= 4) return;
            const locationData = normalizePolishLocationFromSelects(
                pendingLocationForm.province,
                pendingLocationForm.city,
                pendingLocationForm.district
            );
            const hasProvince = !!(locationData.provinceValue || locationData.provinceLabel);
            const hasCity = !!(locationData.cityValue || locationData.cityLabel);
            const hasDistrict = !!(locationData.districtValue || locationData.districtLabel);
            if (hasDistrict && !hasCity) {
                setDefaultCityInfo('Wybierz miasto dla dzielnicy.');
                return;
            }
            if (!hasProvince && !hasCity) {
                setDefaultCityInfo('Wybierz województwo lub miasto.');
                return;
            }
            createSavedLocationRow(locationData);
            hidePendingLocationForm();
        });
        pendingLocationForm.cancel.addEventListener('click', () => {
            if (pendingLocationForm.busy) return;
            hidePendingLocationForm();
        });
        defaultCityAddButton.addEventListener('click', () => {
            if (pendingLocationForm.visible) return;
            showPendingLocationForm();
        });

        defaultCityOptionsBox.append(
            defaultCityToolbar,
            defaultCityRowsBox,
            defaultCityEmpty,
            pendingLocationForm.root,
            defaultCityInfo
        );
        currentSearchLocations.forEach(locationData => createSavedLocationRow(locationData));
        updateDefaultCityRowsUi();

        const topPhoneSearchCheck = makeCheckbox(
            'Pokaż pole wyszukiwania numeru telefonu na górze stron escort.club',
            current.showTopPhoneSearch !== false
        );

        const topPhoneSearchEnterTargetRow = makeElement('label', 'vm-settings-inline-row vm-settings-top-phone-option');
        const topPhoneSearchEnterTargetLabel = makeElement('span', '', 'Po naciśnięciu Enter wyszukuj domyślnie w:');
        const topPhoneSearchEnterTargetSelect = makeElement('select');
        appendSelectOptions(topPhoneSearchEnterTargetSelect, TOP_PHONE_SEARCH_TARGET_OPTIONS);
        topPhoneSearchEnterTargetSelect.value = normalizeTopPhoneSearchEnterTarget(
            current.topPhoneSearchEnterTarget
        );
        topPhoneSearchEnterTargetSelect.className =
            'vm-settings-select -wide-arrow';
        fitSelectToLongestOption(topPhoneSearchEnterTargetSelect, 16);
        topPhoneSearchEnterTargetRow.append(
            topPhoneSearchEnterTargetLabel,
            topPhoneSearchEnterTargetSelect
        );

        const topPhoneSearchOpenModeRow = makeElement('label', 'vm-settings-inline-row vm-settings-top-phone-option');
        const topPhoneSearchOpenModeLabel = makeElement('span', '', 'Otwieraj wyniki:');
        const topPhoneSearchOpenModeSelect = makeElement('select');
        appendSelectOptions(topPhoneSearchOpenModeSelect, TOP_PHONE_SEARCH_OPEN_MODE_OPTIONS);
        topPhoneSearchOpenModeSelect.value = normalizeTopPhoneSearchOpenMode(
            current.topPhoneSearchOpenMode
        );
        topPhoneSearchOpenModeSelect.className =
            'vm-settings-select -wide-arrow';
        fitSelectToLongestOption(topPhoneSearchOpenModeSelect);
        topPhoneSearchOpenModeRow.append(
            topPhoneSearchOpenModeLabel,
            topPhoneSearchOpenModeSelect
        );

        const topPhoneSearchOptionsRow = makeElement('div', 'vm-settings-inline-row vm-settings-top-phone-options');
        topPhoneSearchOptionsRow.append(
            topPhoneSearchEnterTargetRow,
            topPhoneSearchOpenModeRow
        );

        const syncTopPhoneSearchControls = () => {
            const enabled = topPhoneSearchCheck.input.checked;
            topPhoneSearchEnterTargetSelect.disabled = !enabled;
            topPhoneSearchOpenModeSelect.disabled = !enabled;
            topPhoneSearchOptionsRow.style.opacity = enabled ? '1' : '.5';
            topPhoneSearchOptionsRow.style.cursor = enabled
                ? 'default'
                : 'not-allowed';
        };
        topPhoneSearchCheck.input.addEventListener(
            'change',
            syncTopPhoneSearchControls
        );
        syncTopPhoneSearchControls();

        const hideRecommendedSearchSectionCheck = makeCheckbox(
            'Ukryj sekcję „Polecane” na stronie wyników',
            current.hideRecommendedSearchSection
        );

        const hideRecommendedAdSectionCheck = makeCheckbox(
            'Ukryj sekcję „Polecane” na stronie pojedynczego anonsu',
            current.hideRecommendedAdSection
        );

        const hideSingleAdContactButtonsCheck = makeCheckbox(
            'Ukryj przyciski „Daj napiwek” i „Wyślij wiadomość”',
            current.hideSingleAdContactButtons
        );

        const hideFeaturedSearchSectionCheck = makeCheckbox(
            'Ukryj sekcję „Wyróżnione” na stronie wyników',
            current.hideFeaturedSearchSection
        );

        const hidePopularCitiesSectionCheck = makeCheckbox(
            'Ukryj sekcję „Popularne Miasta”',
            current.hidePopularCitiesSection
        );


        const imageSearchButtonsCheck = makeCheckbox(
            'Na zdjęciach pokazuj przyciski do wyszukiwania grafiki w serwisach zewnętrznych.',
            current.showImageSearchButtons
        );

        const summarySidePanelCheck = makeCheckbox(
            'Pokazuj panel boczny „Podsumowanie”',
            current.showSummarySidePanel
        );
        const compactSummaryCheck = makeCheckbox(
            'Wyświetl skrócone podsumowanie nad opisem',
            current.showCompactSummaryAboveDescription
        );
        const syncSummarySidePanelControls = () => {
            autoCheck.input.disabled = false;
            autoCheck.label.style.opacity = '1';
            autoCheck.label.style.cursor = 'pointer';
            compactSummaryCheck.input.disabled = false;
            compactSummaryCheck.label.style.opacity = '1';
            compactSummaryCheck.label.style.cursor = 'pointer';
            if (!persistentCacheDraftEnabled) {
                autoGarsoCountRadio.input.checked = true;
            }
            const autoModeEnabled =
                autoCheck.input.checked &&
                persistentCacheDraftEnabled;
            for (const option of [autoGarsoCountRadio, autoGarsoContentRadio]) {
                option.input.disabled = !autoModeEnabled;
                option.label.style.cursor = autoModeEnabled
                    ? 'pointer'
                    : 'not-allowed';
            }
            const forcedCountMode = !persistentCacheDraftEnabled;
            autoGarsoCountRadio.label.style.opacity = autoCheck.input.checked
                ? '1'
                : '.5';
            autoGarsoCountRadio.label.style.color =
                autoCheck.input.checked && forcedCountMode
                ? '#222222'
                : '';
            autoGarsoContentRadio.label.style.opacity = autoModeEnabled
                ? '1'
                : '.5';
            autoGarsoModeBox.style.opacity = '1';
            autoCheckInfo.style.opacity = autoModeEnabled ? '1' : '.5';
        };
        summarySidePanelCheck.input.addEventListener(
            'change',
            syncSummarySidePanelControls
        );
        autoCheck.input.addEventListener('change', syncSummarySidePanelControls);
        syncSummarySidePanelControls();

        const phoneClipboardFormatRow = makeElement('label', 'vm-settings-inline-row vm-settings-phone-copy-row');

        const phoneClipboardFormatLabel = makeElement('span', '', 'Format numeru kopiowanego do schowka przyciskiem:');
        phoneClipboardFormatLabel.style.whiteSpace = 'nowrap';

        const phoneClipboardFormatSelect = makeElement('select');
        const selectedPhoneClipboardFormat = normalizePhoneClipboardFormat(
            current.phoneClipboardFormat
        );
        appendSelectOptions(phoneClipboardFormatSelect, PHONE_CLIPBOARD_FORMAT_OPTIONS, selectedPhoneClipboardFormat);
        phoneClipboardFormatSelect.className = 'vm-settings-select';
        fitSelectToLongestOption(phoneClipboardFormatSelect);
        phoneClipboardFormatRow.append(
            phoneClipboardFormatLabel,
            phoneClipboardFormatSelect
        );

        const persistentCacheCheck = makeCheckbox(
            'Zapamiętuj lokalnie wyniki Garsoniery, Escorti i dane anonsów Escort.club',
            current.usePersistentCache
        );

        const persistentCacheInfo = makeElement('div', 'vm-settings-info vm-settings-cache-info');
        persistentCacheInfo.textContent =
            'Przyspiesza działanie list i ogranicza liczbę ponownych zapytań. ' +
            'Cache Garsoniery przechowuje liczbę znalezionych tematów osobno dla numeru telefonu i adresu anonsu oraz jedną wspólną analizę postów bez powtarzających się tematów. ' +
            'Pozostały cache zawiera dane profili Escorti oraz dane anonsów Escort.club: numer telefonu, opis, godziny dostępności, ceny, lokalizację i parametry profilu. ' +
            'Po wyłączeniu zapisane dane zostaną usunięte. Wyłączą się także obserwowanie, agregowanie i ceny na kafelkach. ' +
            'Nie będą dostępne informacje „Ostatnio odwiedzono”, porównywanie zmian ani zapamiętane wyniki i podsumowania Garsoniery oraz Escorti.';

        const cacheRefreshRow = makeElement('div', 'vm-settings-inline-row -nowrap vm-settings-cache-refresh');

        const cacheRefreshLabel = makeElement('span', 'vm-settings-muted-label', 'Escorti / Escort.club - odśwież po:');

        const cacheRefreshSelect = makeElement('select');
        const selectedCacheRefreshHours = normalizeCacheRefreshHours(current.cacheRefreshHours);

        appendSelectOptions(cacheRefreshSelect, CACHE_REFRESH_OPTIONS, selectedCacheRefreshHours);

        cacheRefreshSelect.className = 'vm-settings-select -small';
        fitSelectToLongestOption(cacheRefreshSelect);

        cacheRefreshRow.appendChild(cacheRefreshLabel);
        cacheRefreshRow.appendChild(cacheRefreshSelect);

        const garsoCacheRefreshRow = makeElement('div', 'vm-settings-inline-row -nowrap vm-settings-cache-refresh');

        const garsoCacheRefreshLabel = makeElement('span', 'vm-settings-muted-label', 'Garsoniera - odśwież po:');

        const garsoCacheRefreshSelect = makeElement('select');
        const selectedGarsoCacheRefreshHours = normalizeGarsoCacheRefreshHours(
            current.garsoCacheRefreshHours
        );

        appendSelectOptions(garsoCacheRefreshSelect, CACHE_REFRESH_OPTIONS, selectedGarsoCacheRefreshHours);

        garsoCacheRefreshSelect.className = 'vm-settings-select -small';
        fitSelectToLongestOption(garsoCacheRefreshSelect);

        garsoCacheRefreshRow.appendChild(garsoCacheRefreshLabel);
        garsoCacheRefreshRow.appendChild(garsoCacheRefreshSelect);

        function setDependentCheckboxAvailability(control, enabled) {
            control.input.disabled = !enabled;
            control.label.style.opacity = enabled ? '1' : '.5';
            control.label.style.cursor = enabled ? 'pointer' : 'not-allowed';
        }

        function syncCacheControls() {
            const enabled = persistentCacheCheck.input.checked;
            persistentCacheDraftEnabled = enabled;
            if (!enabled) {
                watchEnabledDraft = false;
                mergeEscortAdsCheck.input.checked = false;
                searchResultPricesCheck.input.checked = false;
            }

            cacheRefreshSelect.disabled = !enabled;
            garsoCacheRefreshSelect.disabled = !enabled;
            cacheRefreshRow.style.opacity = enabled ? '1' : '.5';
            garsoCacheRefreshRow.style.opacity = enabled ? '1' : '.5';
            setDependentCheckboxAvailability(watchEnabledCheck, enabled);
            setDependentCheckboxAvailability(mergeEscortAdsCheck, enabled);
            setDependentCheckboxAvailability(searchResultPricesCheck, enabled);
            aggregationCacheRequirementInfo.style.display = enabled
                ? 'none'
                : 'block';
            mergeEscortAdsCheck.label.title = enabled
                ? ''
                : 'Agregowanie wymaga włączenia cache.';
            syncWatchSettingsControls();
            syncAggregationModeControls();
            syncSummarySidePanelControls();
        }

        persistentCacheCheck.input.addEventListener('change', syncCacheControls);
        syncCacheControls();

        const clearCacheRow = makeElement('div', 'vm-settings-inline-row vm-settings-cache-controls');

        const clearExpiredCacheBtn = makeButton('vm-settings-cache-button', 'Wyczyść przeterminowany cache');

        const clearCacheBtn = makeButton('vm-settings-cache-button', 'Wyczyść zapisany cache');

        const clearCacheSize = makeElement('span', 'vm-settings-cache-size');

        function refreshCacheSizeLabel() {
            const stats = getPersistentCacheStats();
            clearCacheSize.textContent = formatCacheSize(stats.megabytes);
            clearCacheSize.title =
                `Przybliżony rozmiar trwałego cache: ${stats.entries} ${stats.entries === 1 ? 'wpis' : 'wpisów'} ` +
                `(${stats.garsoEntries} Garsoniera, ${stats.listEntries} Escorti, ` +
                `${stats.escortAdDataEntries} Escort.club). ` +
                'To przybliżony rozmiar zserializowanych danych przechowywanych w IndexedDB.';
        }

        refreshCacheSizeLabel();

        clearExpiredCacheBtn.addEventListener('click', async e => {
            e.preventDefault();
            e.stopPropagation();
            clearExpiredCacheBtn.disabled = true;
            const originalLabel = 'Wyczyść przeterminowany cache';
            clearExpiredCacheBtn.textContent = 'Czyszczenie…';
            try {
                const result = await clearExpiredPersistentCacheAcrossDomains({
                    escortTtlMs:
                        normalizeCacheRefreshHours(cacheRefreshSelect.value) * 60 * 60 * 1000,
                    garsoTtlMs:
                        normalizeGarsoCacheRefreshHours(garsoCacheRefreshSelect.value) * 60 * 60 * 1000
                });
                refreshCacheSizeLabel();
                clearExpiredCacheBtn.textContent = result.removed
                    ? `Usunięto: ${result.removed}`
                    : 'Brak przeterminowanego cache';
            } catch (error) {
                clearExpiredCacheBtn.textContent = 'Błąd czyszczenia';
                log('Nie udało się wyczyścić przeterminowanego cache', error);
            }
            setTimeout(() => {
                clearExpiredCacheBtn.disabled = false;
                clearExpiredCacheBtn.textContent = originalLabel;
            }, 1800);
        });

        clearCacheBtn.addEventListener('click', async e => {
            e.preventDefault();
            e.stopPropagation();
            clearCacheBtn.disabled = true;
            const originalLabel = 'Wyczyść zapisany cache';
            clearCacheBtn.textContent = 'Czyszczenie…';
            try {
                await clearPersistentCacheAcrossDomains();
                refreshCacheSizeLabel();
                clearCacheBtn.textContent = 'Cache wyczyszczony';
            } catch (error) {
                clearCacheBtn.textContent = 'Błąd czyszczenia';
                log('Nie udało się wyczyścić zapisanego cache', error);
            }
            setTimeout(() => {
                clearCacheBtn.disabled = false;
                clearCacheBtn.textContent = originalLabel;
            }, 1800);
        });

        clearCacheRow.append(clearExpiredCacheBtn, clearCacheBtn);
        clearCacheRow.appendChild(clearCacheSize);

        const note = makeElement('div', 'vm-settings-note', 'Zmiany zostaną zastosowane po zapisaniu i odświeżeniu strony.');

        const actions = makeElement('div', 'vm-settings-actions');

        const cancelBtn = makeButton('vm-settings-footer-button -secondary', 'Anuluj');

        const saveBtn = makeButton('vm-settings-footer-button -primary', 'Zapisz');

        cancelBtn.addEventListener('click', () => {
            locationSettingsBridge?.destroy();
            overlay.remove();
        });
        saveBtn.addEventListener('click', async () => {
            saveBtn.disabled = true;
            saveBtn.textContent = 'Zapisywanie…';
            try {
                const cacheWillBeEnabled = persistentCacheCheck.input.checked;
                const cacheWasEnabled = current.usePersistentCache === true;
                const selectedSearchLocations = [];
                for (let index = 0; index < defaultCityRows.length; index++) {
                    const row = defaultCityRows[index];
                    if (row.busy) {
                        throw new Error(`Poczekaj na zakończenie pobierania lokalizacji ${index + 1}.`);
                    }
                    const locationData = getDefaultCityRowLocation(row);
                    const hasProvince = !!(locationData.provinceValue || locationData.provinceLabel);
                    const hasCity = !!(locationData.cityValue || locationData.cityLabel);
                    const hasDistrict = !!(locationData.districtValue || locationData.districtLabel);
                    if (hasDistrict && !hasCity) {
                        throw new Error(`Wybierz miasto dla dzielnicy w lokalizacji ${index + 1} albo usuń dzielnicę.`);
                    }
                    if (hasProvince || hasCity) selectedSearchLocations.push(locationData);
                }
                const normalizedSearchLocations = normalizeSearchLocations(selectedSearchLocations);
                // Tylko wyłączenie samego cache może usunąć zapisane dane.
                // Zmiana agregowania ani ustawień kafelków nie uruchamia czyszczenia.
                if (cacheWasEnabled && !cacheWillBeEnabled) {
                    await clearPersistentCacheAcrossDomains();
                }

                const desiredWatchSettings = {
                    enabled: watchEnabledDraft,
                    intervalMinutes: Number(watchIntervalSelect.value)
                };
                const nextSettings = {
                    autoGarsoCheck: autoCheck.input.checked,
                    autoGarsoAnalysisMode: autoGarsoContentRadio.input.checked
                        ? 'content'
                        : 'count',
                    mergeEscortAds: cacheWillBeEnabled && mergeEscortAdsCheck.input.checked,
                    escortAggregationMode: aggregationModeSelect.value,
                    compactSummaryComparisonMode:
                        compactSummaryComparisonModeSelect.value,
                    showEscortiTileButton: escortiTileButtonCheck.input.checked,
                    showEscortiTileData: escortiTileDataCheck.input.checked,
                    showPricesInSearchResults: cacheWillBeEnabled && searchResultPricesCheck.input.checked,
                    searchResultPriceDuration: current.searchResultPriceDuration,
                    hideRecommendedSearchSection: hideRecommendedSearchSectionCheck.input.checked,
                    hideRecommendedAdSection: hideRecommendedAdSectionCheck.input.checked,
                    hideSingleAdContactButtons: hideSingleAdContactButtonsCheck.input.checked,
                    hideFeaturedSearchSection: hideFeaturedSearchSectionCheck.input.checked,
                    hidePopularCitiesSection: hidePopularCitiesSectionCheck.input.checked,
                    showImageSearchButtons: imageSearchButtonsCheck.input.checked,
                    showTopPhoneSearch: topPhoneSearchCheck.input.checked,
                    topPhoneSearchEnterTarget:
                        topPhoneSearchEnterTargetSelect.value,
                    topPhoneSearchOpenMode: topPhoneSearchOpenModeSelect.value,
                    showDefaultCityButton: normalizedSearchLocations.length > 0,
                    defaultSearchLocation: normalizedSearchLocations[0] || normalizeDefaultSearchLocation(null),
                    searchLocations: normalizedSearchLocations,
                    showSummarySidePanel: summarySidePanelCheck.input.checked,
                    showCompactSummaryAboveDescription:
                        compactSummaryCheck.input.checked,
                    phoneClipboardFormat: phoneClipboardFormatSelect.value,
                    usePersistentCache: cacheWillBeEnabled,
                    cacheRefreshHours: Number(cacheRefreshSelect.value),
                    garsoCacheRefreshHours: Number(garsoCacheRefreshSelect.value),
                    watchEnabled: cacheWillBeEnabled && desiredWatchSettings.enabled,
                    watchIntervalMinutes: desiredWatchSettings.intervalMinutes,
                    showWatchOnlineCount:
                        cacheWillBeEnabled && watchOnlineCountCheck.input.checked,
                    historyCleanupKeywords: current.historyCleanupKeywords
                };
                const normalizedNextSettings = saveSettings(nextSettings);
                SETTINGS = {
                    ...DEFAULT_SETTINGS,
                    ...normalizedNextSettings
                };
                locationSettingsBridge?.destroy();
                overlay.remove();
                location.reload();
            } catch (error) {
                saveBtn.disabled = false;
                saveBtn.textContent = 'Zapisz';
                window.alert(error?.message || 'Nie udało się zapisać ustawień.');
            }
        });

        overlay.addEventListener('click', e => {
            if (e.target === overlay) {
                locationSettingsBridge?.destroy();
                overlay.remove();
            }
        });

        const watchSection = makeSettingsSection(
            'Obserwowanie',
            'Automatyczne sprawdzanie aktywności i zmian lokalizacji obserwowanych profili i anonsów.',
            true
        );
        watchSection.append(
            watchEnabledCheck.label,
            watchIntervalRow,
            watchOnlineCountCheck.label,
            watchRuntimeInfo
        );

        const visibilitySection = makeSettingsSection(
            'Ukryj lub dodaj sekcje strony',
            'Elementy Escort.club, które skrypt ukrywa albo dodaje.',
            true
        );
        visibilitySection.append(
            topPhoneSearchCheck.label,
            topPhoneSearchOptionsRow,
            hideRecommendedSearchSectionCheck.label,
            hideRecommendedAdSectionCheck.label,
            hideSingleAdContactButtonsCheck.label,
            hideFeaturedSearchSectionCheck.label,
            hidePopularCitiesSectionCheck.label
        );

        const singleAdSection = makeSettingsSection(
            'Widok pojedynczego anonsu escort.club',
            'Narzędzia dostępne po otwarciu konkretnego anonsu Escort.club.',
            true
        );
        singleAdSection.append(
            summarySidePanelCheck.label,
            compactSummaryCheck.label,
            compactSummaryComparisonModeBox,
            autoCheck.label,
            autoGarsoModeBox,
            autoCheckInfo,
            imageSearchButtonsCheck.label,
            phoneClipboardFormatRow
        );

        const resultsSection = makeSettingsSection(
            'Strona wyszukiwania escort.club i agregowanie',
            'Narzędzia wyszukiwania, sposób łączenia powiązanych anonsów oraz informacje widoczne bezpośrednio na kafelkach.',
            true
        );
        const resultsColumns = makeElement('div', 'vm-settings-results-columns');
        const aggregationColumn = makeElement('div');
        aggregationColumn.append(
            mergeEscortAdsCheck.label,
            aggregationCacheRequirementInfo,
            aggregationModeBox
        );
        const tilesColumn = makeElement('div');
        tilesColumn.append(
            escortiTileButtonCheck.label,
            escortiTileDataCheck.label,
            searchResultPricesCheck.label
        );
        resultsColumns.append(aggregationColumn, tilesColumn);
        resultsSection.append(
            resultsColumns,
            defaultCitySectionLabel,
            defaultCityOptionsBox
        );

        const cacheSection = makeSettingsSection(
            'Cache i wydajność',
            'Lokalnie zapisane dane przyspieszają kolejne wejścia na stronę i ograniczają liczbę zapytań.',
            true
        );
        cacheSection.append(
            persistentCacheCheck.label,
            persistentCacheInfo,
            cacheRefreshRow,
            garsoCacheRefreshRow,
            clearCacheRow
        );

        const diagnosticsSection = makeSettingsSection(
            'Diagnostyka skryptu',
            'Ostatnie problemy parserów i zapytań, niepełne analizy oraz aktualny stan cache.',
            true
        );
        const diagnosticsOverview = makeElement('div', 'vm-diagnostics-overview');
        const diagnosticsLists = makeElement('div', 'vm-diagnostics-lists');
        const diagnosticsActions = makeElement('div', 'vm-settings-inline-row vm-settings-transfer-row');
        const diagnosticsRefreshBtn = makeButton('vm-settings-action-button', 'Odśwież');
        const diagnosticsExportBtn = makeButton('vm-settings-action-button', 'Eksportuj diagnostykę');
        const diagnosticsClearBtn = makeButton('vm-settings-cache-button', 'Wyczyść diagnostykę');
        const diagnosticsStatus = makeElement('span', 'vm-settings-transfer-status');
        const diagnosticsPrivacy = makeElement('div', 'vm-diagnostics-privacy', 'Eksport diagnostyczny nie zawiera numerów telefonu, nazw profili ani treści opisów. Adresy i długie identyfikatory są anonimizowane.');

        const makeDiagnosticCard = (value, label) => {
            const card = makeElement('div', 'vm-diagnostics-card');
            const valueLine = makeElement('div', 'vm-diagnostics-card-value', String(value));
            const labelLine = makeElement('div', 'vm-diagnostics-card-label', label);
            card.append(valueLine, labelLine);
            return card;
        };
        const makeDiagnosticList = (titleText, events) => {
            const list = makeElement('div', 'vm-diagnostics-list');
            const listTitle = makeElement('div', 'vm-diagnostics-list-title', titleText);
            list.appendChild(listTitle);
            const recent = (Array.isArray(events) ? events : []).slice(-5).reverse();
            if (!recent.length) {
                const empty = makeElement('div', 'vm-diagnostics-empty', 'Brak zapisanych zdarzeń.');
                list.appendChild(empty);
                return list;
            }
            for (const event of recent) {
                const row = makeElement('div', 'vm-diagnostics-event');
                const time = new Date(Number(event.at) || Date.now())
                    .toLocaleString('pl-PL', {
                        day: '2-digit',
                        month: '2-digit',
                        hour: '2-digit',
                        minute: '2-digit'
                    });
                row.textContent = `${time} - ${event.area}` +
                    (event.message ? `: ${event.message}` : '');
                list.appendChild(row);
            }
            return list;
        };
        const renderDiagnostics = () => {
            const snapshot = getDiagnosticsSnapshot();
            const counters = snapshot.counters || {};
            diagnosticsOverview.replaceChildren(
                makeDiagnosticCard(
                    counters.requests || 0,
                    `zapytania: Garso ${counters.garsoRequests || 0}, Escorti.pl ${counters.escortiRequests || 0}, Escort.club ${counters.escortClubRequests || 0}`
                ),
                makeDiagnosticCard(
                    counters.garsoRetries || 0,
                    `ponowienia Garso - antyflood ${counters.antiflood || 0}, HTTP 429: ${counters.http429 || 0}`
                ),
                makeDiagnosticCard(
                    counters.errors || 0,
                    'zapisane błędy'
                ),
                makeDiagnosticCard(
                    counters.parserIssues || 0,
                    'problemy parserów'
                ),
                makeDiagnosticCard(
                    counters.incompleteAnalyses || 0,
                    'niepełne analizy Garso'
                ),
                makeDiagnosticCard(
                    snapshot.cache?.entries ?? '-',
                    snapshot.cache?.error
                        ? 'nie udało się odczytać stanu cache'
                        : `cache ${snapshot.cache?.enabled ? 'włączony' : 'wyłączony'} - ${snapshot.cache?.backend || 'pamięć'} - ${formatCacheSize((snapshot.cache?.bytes || 0) / (1024 * 1024))}`
                ),
                makeDiagnosticCard(
                    counters.cancellations || 0,
                    'operacje przerwane przez użytkownika'
                )
            );
            diagnosticsLists.replaceChildren(
                makeDiagnosticList('Ostatnie błędy', snapshot.errors),
                makeDiagnosticList('Problemy parserów', snapshot.parserIssues),
                makeDiagnosticList('Niepełne analizy', snapshot.incompleteAnalyses)
            );
        };
        diagnosticsRefreshBtn.addEventListener('click', event => {
            event.preventDefault();
            renderDiagnostics();
            diagnosticsStatus.textContent = 'Dane odświeżone.';
        });
        diagnosticsExportBtn.addEventListener('click', event => {
            event.preventDefault();
            try {
                downloadDiagnosticsExport();
                diagnosticsStatus.textContent = 'Zapisano anonimowy eksport diagnostyczny.';
            } catch (error) {
                diagnosticsStatus.textContent = 'Nie udało się wyeksportować diagnostyki.';
                log('Błąd eksportu diagnostyki', error);
            }
        });
        diagnosticsClearBtn.addEventListener('click', event => {
            event.preventDefault();
            clearDiagnostics();
            renderDiagnostics();
            diagnosticsStatus.textContent = 'Diagnostyka została wyczyszczona.';
        });
        diagnosticsActions.append(
            diagnosticsRefreshBtn,
            diagnosticsExportBtn,
            diagnosticsClearBtn,
            diagnosticsStatus
        );
        diagnosticsSection.append(
            diagnosticsOverview,
            diagnosticsLists,
            diagnosticsActions,
            diagnosticsPrivacy
        );
        renderDiagnostics();

        const transferSection = makeSettingsSection(
            'Eksport i import',
            'Jeden plik JSON zawiera ustawienia główne, zapisane filtry, nazwy i adresy obserwowanych oraz historię ich zmian. Plik może więc zawierać dane prywatne.',
            true
        );
        const transferRow = makeElement('div', 'vm-settings-inline-row vm-settings-transfer-row');
        const exportSettingsBtn = makeButton('vm-settings-action-button', 'Eksportuj');
        const importSettingsBtn = makeButton('vm-settings-action-button', 'Importuj');
        const importSettingsInput = makeElement('input');
        importSettingsInput.type = 'file';
        importSettingsInput.accept = '.json,application/json';
        importSettingsInput.style.display = 'none';
        const transferStatus = makeElement('span', 'vm-settings-transfer-status');
        exportSettingsBtn.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            try {
                downloadSettingsExport();
                transferStatus.textContent = 'Zapisano plik eksportu.';
            } catch (error) {
                transferStatus.textContent = 'Nie udało się wyeksportować danych.';
                log('Błąd eksportu ustawień i obserwowanych', error);
            }
        });
        importSettingsBtn.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            importSettingsInput.click();
        });
        importSettingsInput.addEventListener('change', async () => {
            const file = importSettingsInput.files?.[0];
            if (!file) return;
            importSettingsBtn.disabled = true;
            transferStatus.textContent = 'Sprawdzam plik…';
            try {
                const payload = JSON.parse(await file.text());
                const checked = validateSettingsImportPayload(payload);
                const confirmed = window.confirm(
                    `Zaimportować ustawienia, ${checked.watchedItems.length} obserwowanych ` +
                    `i ${checked.watchEvents.length} wpisów historii?\n\n` +
                    'Obecne ustawienia, obserwowani i historia zostaną zastąpione.'
                );
                if (!confirmed) {
                    transferStatus.textContent = 'Import anulowany.';
                    return;
                }
                restoreSettingsExport(payload);
                transferStatus.textContent = 'Zaimportowano. Odświeżam stronę…';
                location.reload();
            } catch (error) {
                transferStatus.textContent = error?.message || 'Nie udało się zaimportować pliku.';
                log('Błąd importu ustawień i obserwowanych', error);
            } finally {
                importSettingsBtn.disabled = false;
                importSettingsInput.value = '';
            }
        });
        transferRow.append(
            exportSettingsBtn,
            importSettingsBtn,
            importSettingsInput,
            transferStatus
        );
        transferSection.appendChild(transferRow);

        contentGrid.append(
            visibilitySection,
            resultsSection,
            singleAdSection,
            cacheSection,
            watchSection,
            diagnosticsSection,
            transferSection
        );

        box.append(title, subtitle, contentGrid);
        actions.append(note, cancelBtn, saveBtn);
        box.appendChild(actions);
        overlay.appendChild(box);
        document.body.appendChild(overlay);
    }

    function saveHistoryCleanupKeywords(value) {
        const keywords = normalizeHistoryCleanupKeywords(value);
        const saved = saveSettings({
            ...getSettings(),
            historyCleanupKeywords: keywords
        });
        SETTINGS = { ...DEFAULT_SETTINGS, ...saved };
        return keywords;
    }

    function openBrowserHistorySearchTabs(sourceKeywords = null) {
        const keywords = normalizeHistoryCleanupKeywords(
            sourceKeywords ?? getSettings().historyCleanupKeywords
        );
        if (!keywords.length) {
            window.alert('Lista słów kluczowych jest pusta. Uzupełnij ją w tym oknie.');
            return;
        }

        keywords.forEach((keyword, index) => {
            const historyUrl =
                `edge://history/all?q=${encodeURIComponent(keyword)}`;
            GM_openInTab(historyUrl, {
                active: index === keywords.length - 1
            });
        });
    }

    function openBrowserHistoryCleanupModal() {
        const overlayId = 'vm-garso-history-cleanup-overlay';
        if (document.getElementById(overlayId)) return;

        const overlay = makeElement('div');
        overlay.id = overlayId;
        Object.assign(overlay.style, {
            position: 'fixed',
            inset: '0',
            zIndex: '2147483647',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: '16px',
            background: 'rgba(0,0,0,.55)'
        });

        const box = makeElement('div');
        Object.assign(box.style, {
            width: 'min(570px, calc(100vw - 32px))',
            maxHeight: 'calc(100vh - 32px)',
            overflowY: 'auto',
            boxSizing: 'border-box',
            padding: '19px 20px 17px',
            borderTop: `4px solid ${getEscortPagePinkColor()}`,
            borderRadius: '11px',
            background: '#ffffff',
            color: '#222222',
            boxShadow: '0 10px 35px rgba(0,0,0,.35)',
            fontFamily: 'Arial,sans-serif',
            fontSize: '14px'
        });

        const titleRow = makeElement('div');
        Object.assign(titleRow.style, {
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'space-between',
            gap: '12px',
            marginBottom: '12px'
        });
        const title = makeElement('div', '', 'Historia przeglądarki');
        Object.assign(title.style, {
            fontSize: '20px',
            fontWeight: '700'
        });
        const close = makeButton('', '×');
        Object.assign(close.style, {
            width: '34px',
            height: '34px',
            padding: '0',
            border: '0',
            borderRadius: '50%',
            background: '#f1f1f1',
            color: '#444444',
            fontSize: '24px',
            lineHeight: '1',
            cursor: 'pointer'
        });
        close.addEventListener('click', () => overlay.remove());
        titleRow.append(title, close);

        const keywordsLabel = makeElement('label', '', 'Frazy do wyszukania w historii przeglądarki:');
        Object.assign(keywordsLabel.style, {
            display: 'block',
            marginBottom: '5px',
            fontSize: '12px',
            fontWeight: '700',
            color: '#444444'
        });

        const keywordsInput = makeElement('textarea');
        keywordsInput.value = normalizeHistoryCleanupKeywords(
            getSettings().historyCleanupKeywords
        ).join('\n');
        keywordsInput.rows = 8;
        keywordsInput.placeholder = 'Każde słowo kluczowe w osobnym wierszu';
        Object.assign(keywordsInput.style, {
            display: 'block',
            width: '100%',
            minHeight: '145px',
            padding: '8px 9px',
            border: '1px solid #bbbbbb',
            borderRadius: '6px',
            background: '#ffffff',
            color: '#222222',
            font: '12px/1.4 Consolas, monospace',
            resize: 'vertical',
            boxSizing: 'border-box'
        });

        const info = makeElement('div', '', 'Każdy wpis otworzy osobną kartę historii Edge. Na każdej karcie użyj Ctrl+A, a następnie przycisku „Usuń”.');
        Object.assign(info.style, {
            marginTop: '7px',
            color: '#777777',
            fontSize: '11px',
            lineHeight: '1.35'
        });

        const actions = makeElement('div');
        Object.assign(actions.style, {
            display: 'flex',
            justifyContent: 'flex-end',
            gap: '8px',
            marginTop: '15px',
            paddingTop: '13px',
            borderTop: '1px solid #e7e7e7'
        });
        const save = makeButton('', 'Zapisz');
        const open = makeButton();

        for (const button of [save, open]) {
            Object.assign(button.style, {
                padding: '8px 14px',
                border: `1px solid ${getEscortPagePinkColor()}`,
                borderRadius: '7px',
                cursor: 'pointer',
                fontSize: '13px',
                fontWeight: '700'
            });
        }
        Object.assign(save.style, {
            background: '#ffffff',
            color: getEscortPagePinkColor()
        });
        Object.assign(open.style, {
            background: getEscortPagePinkColor(),
            color: '#ffffff'
        });

        const updateOpenLabel = () => {
            const count = normalizeHistoryCleanupKeywords(keywordsInput.value).length;
            open.textContent = count === 1
                ? 'Otwórz 1 kartę'
                : `Otwórz ${count} kart`;
            open.disabled = count === 0;
            open.style.opacity = count === 0 ? '.5' : '1';
        };
        keywordsInput.addEventListener('input', updateOpenLabel);
        updateOpenLabel();

        save.addEventListener('click', () => {
            saveHistoryCleanupKeywords(keywordsInput.value);
            overlay.remove();
        });
        open.addEventListener('click', () => {
            const keywords = saveHistoryCleanupKeywords(keywordsInput.value);
            if (!keywords.length) return;
            overlay.remove();
            openBrowserHistorySearchTabs(keywords);
        });
        overlay.addEventListener('click', event => {
            if (event.target === overlay) overlay.remove();
        });

        actions.append(save, open);
        box.append(titleRow, keywordsLabel, keywordsInput, info, actions);
        overlay.appendChild(box);
        document.body.appendChild(overlay);
    }

    let watchedMenuCommandId = null;
    let watchedMenuCommandRegistered = false;
    if (
        (
            GARSO_MENU_ONLY_MODE ||
            ['pl.escort.club', 'escorti.pl', 'www.escorti.pl'].includes(CURRENT_HOST)
        ) &&
        typeof GM_registerMenuCommand === 'function'
    ) {
        GM_registerMenuCommand(
            '⚙ Ustawienia główne',
            () => openSettingsModal()
        );
        const syncWatchedMenuCommand = () => {
            try {
                const enabled = getWatchSettings().enabled;
                if (enabled && !watchedMenuCommandRegistered) {
                    watchedMenuCommandId = GM_registerMenuCommand(
                        '👁 Obserwowane',
                        () => openWatchedSettingsModal()
                    );
                    watchedMenuCommandRegistered = true;
                } else if (
                    !enabled && watchedMenuCommandRegistered
                ) {
                    if (
                        watchedMenuCommandId != null &&
                        typeof GM_unregisterMenuCommand === 'function'
                    ) {
                        GM_unregisterMenuCommand(watchedMenuCommandId);
                    }
                    watchedMenuCommandId = null;
                    watchedMenuCommandRegistered = false;
                }
            } catch (error) {
                log('Nie udało się zsynchronizować menu obserwowanych', error);
            }
        };
        syncWatchedMenuCommand();
        try {
            GM_addValueChangeListener(
                SETTINGS_STORAGE_KEY,
                syncWatchedMenuCommand
            );
        } catch (_) {}
        GM_registerMenuCommand(
            '🗑 Otwórz historię do usunięcia',
            () => openBrowserHistoryCleanupModal()
        );
    }

    function maybeOpenSettingsOnFirstRun() {
        if (location.hostname !== 'pl.escort.club') return;

        try {
            if (GM_getValue(SETTINGS_ONBOARDING_SEEN_KEY, false)) return;

            const cacheIsEmpty = getPersistentCacheStats().entries === 0;
            GM_setValue(SETTINGS_ONBOARDING_SEEN_KEY, true);

            if (cacheIsEmpty) {
                openSettingsModal({ firstRun: true });
            }
        } catch (error) {
            log('Nie udało się sprawdzić pierwszego uruchomienia ustawień', error);
        }
    }

    maybeOpenSettingsOnFirstRun();

    function gmRequest(options) {
        const { cancelToken = null, ...requestOptions } = options || {};
        cancelToken?.throwIfCancelled();
        recordDiagnosticRequest(requestOptions.url, requestOptions.method);
        return new Promise((resolve, reject) => {
            let settled = false;
            let requestHandle = null;
            let unsubscribe = () => {};
            const finish = (callback, value) => {
                if (settled) return;
                settled = true;
                unsubscribe();
                callback(value);
            };
            requestHandle = GM_xmlhttpRequest({
                timeout: 20000,
                ...requestOptions,
                onload: response => finish(resolve, response),
                onerror: error => finish(reject, error),
                ontimeout: error => finish(reject, error),
                onabort: () => finish(
                    reject,
                    createOperationCancelledError(
                        cancelToken?.reason || 'Żądanie przerwane'
                    )
                )
            });
            if (cancelToken) {
                unsubscribe = cancelToken.onCancel(reason => {
                    try { requestHandle?.abort?.(); } catch (_) {}
                    finish(reject, createOperationCancelledError(reason));
                });
            }
        });
    }

    function isGarsoRequestUrl(value) {
        try {
            const host = new URL(value || '', GARSO_BASE_URL).hostname.toLowerCase();
            return host === 'garsoniera.com.pl' || host === 'www.garsoniera.com.pl';
        } catch (_) {
            return false;
        }
    }

    // Kolejka z odstępem 7 s jest używana WYŁĄCZNIE przez wyszukiwarkę Garso.
    function enqueueGarsoRequest(options) {
        const cancelToken = options?.cancelToken || null;
        const scheduled = garsoRequestQueue
            .catch(() => {})
            .then(async () => {
                cancelToken?.throwIfCancelled();
                const waitMs = Math.max(
                    0,
                    GARSO_ANTIFLOOD_WAIT_MS -
                        (Date.now() - garsoLastRequestStartedAt)
                );
                if (waitMs > 0) {
                    await waitWithCancellation(waitMs, cancelToken);
                }
                cancelToken?.throwIfCancelled();
                garsoLastRequestStartedAt = Date.now();
                return gmRequest(options);
            });
        garsoRequestQueue = scheduled.catch(() => {});
        return scheduled;
    }

    async function garsoRequest(
        options,
        maxAttempts = GARSO_REQUEST_MAX_ATTEMPTS
    ) {
        const attempts = Math.max(1, Number(maxAttempts) || 1);
        let lastResponse = null;
        const cancelToken = options?.cancelToken || null;

        for (let attempt = 1; attempt <= attempts; attempt++) {
            cancelToken?.throwIfCancelled();
            const response = await enqueueGarsoRequest(options);
            lastResponse = response;
            const http429 = Number(response?.status) === 429;
            const antiflood = http429 || isGarsoAntifloodResponse(
                response?.responseText || ''
            );
            if (!antiflood) return response;
            if (http429) incrementDiagnosticCounter('http429');
            else incrementDiagnosticCounter('antiflood');
            if (attempt < attempts) incrementDiagnosticCounter('garsoRetries');
        }

        const error = new Error('Garso: przekroczono limit ponowień po antyfloodzie');
        error.code = 'GARSO_ANTIFLOOD';
        error.response = lastResponse;
        throw error;
    }

    async function acquireGarsoTopicRequestSlot(cancelToken = null) {
        cancelToken?.throwIfCancelled();
        if (garsoTopicActiveRequests < GARSO_TOPIC_FETCH_CONCURRENT) {
            garsoTopicActiveRequests++;
            return;
        }
        await new Promise((resolve, reject) => {
            let unsubscribe = () => {};
            const waiter = {
                resolve: () => {
                    unsubscribe();
                    garsoTopicActiveRequests++;
                    resolve();
                },
                reject: error => {
                    unsubscribe();
                    reject(error);
                }
            };
            garsoTopicRequestWaiters.push(waiter);
            if (cancelToken) {
                unsubscribe = cancelToken.onCancel(reason => {
                    const index = garsoTopicRequestWaiters.indexOf(waiter);
                    if (index >= 0) garsoTopicRequestWaiters.splice(index, 1);
                    waiter.reject(createOperationCancelledError(reason));
                });
            }
        });
    }

    function releaseGarsoTopicRequestSlot() {
        garsoTopicActiveRequests = Math.max(0, garsoTopicActiveRequests - 1);
        const next = garsoTopicRequestWaiters.shift();
        if (next) next.resolve();
    }

    // Treść tematów nie korzysta z 7-sekundowej kolejki. Ograniczamy tylko
    // liczbę jednoczesnych pobrań, żeby nie otwierać dziesiątek połączeń naraz.
    async function garsoTopicRequest(options) {
        const cancelToken = options?.cancelToken || null;
        await acquireGarsoTopicRequestSlot(cancelToken);
        try {
            cancelToken?.throwIfCancelled();
            const response = await gmRequest(options);
            const http429 = Number(response?.status) === 429;
            const responseText = String(response?.responseText || '');
            // Nie parsuj DOM-em każdej dużej strony tematu tylko po to, żeby
            // sprawdzić antyflood. Pełną detekcję uruchamiamy dopiero, gdy
            // surowy HTML zawiera charakterystyczne słowa.
            const maybeAntiflood = http429 ||
                /(?:anti?[-\s]*flood|flood control|odczekaj|poczekaj|zaczekaj)/i
                    .test(responseText);
            const antiflood = http429 || (
                maybeAntiflood && isGarsoAntifloodResponse(responseText)
            );
            if (http429) incrementDiagnosticCounter('http429');
            else if (antiflood) incrementDiagnosticCounter('antiflood');
            if (antiflood) {
                const error = new Error('Garso: antyflood podczas pobierania tematu');
                error.code = 'GARSO_ANTIFLOOD';
                error.response = response;
                throw error;
            }
            return response;
        } finally {
            releaseGarsoTopicRequestSlot();
        }
    }

    async function gmGetText(
        url,
        cancelToken = null,
        { searchRequest = false } = {}
    ) {
        let r;
        if (!isGarsoRequestUrl(url)) {
            r = await gmRequest({ method: 'GET', url, cancelToken });
        } else if (searchRequest) {
            r = await garsoRequest({ method: 'GET', url, cancelToken });
        } else {
            r = await garsoTopicRequest({ method: 'GET', url, cancelToken });
        }
        if (r.status && (r.status < 200 || r.status >= 400)) {
            throw new Error(`HTTP ${r.status}`);
        }
        return r.responseText || '';
    }

    // ============================================================
    // LOKALNIE OBSERWOWANE PROFILE I ANONSE
    // ============================================================

    let watchedCheckPromise = null;
    let watchedPollTimer = null;
    let watchStorageListenersInitialized = false;
    let watchedInitialCheckScheduled = false;
    const watchPageButtonRefreshers = new Set();
    const watchBarShiftedTopElements = new Map();

    function getWatchSettings() {
        try {
            const main = getSettings();
            return {
                enabled: main.usePersistentCache === true && main.watchEnabled === true,
                showOnlineCount:
                    main.usePersistentCache === true &&
                    main.watchEnabled === true &&
                    main.showWatchOnlineCount === true,
                intervalMinutes: WATCH_INTERVAL_OPTIONS.includes(
                    Number(main.watchIntervalMinutes)
                )
                    ? Number(main.watchIntervalMinutes)
                    : DEFAULT_WATCH_SETTINGS.intervalMinutes
            };
        } catch (_) {
            return { ...DEFAULT_WATCH_SETTINGS };
        }
    }

    function saveWatchSettings(value) {
        const interval = Number(value?.intervalMinutes);
        const enabled = value?.enabled === true;
        return saveSettings({
            ...getSettings(),
            watchEnabled: enabled,
            watchIntervalMinutes: WATCH_INTERVAL_OPTIONS.includes(interval)
                ? interval
                : DEFAULT_WATCH_SETTINGS.intervalMinutes
        });
    }

    function normalizeWatchUrl(value) {
        try {
            const url = new URL(value);
            url.search = '';
            url.hash = '';
            return url.href;
        } catch (_) {
            return null;
        }
    }

    function normalizeWatchedDisplayName(value, fallback = 'Obserwowane anons') {
        let name = normalizeEscortAdText(value);
        if (!name) return fallback;

        // Tytuły profili Escorti bywają zapisane jako
        // „Opinie o NAZWA z Miasto”. Na liście obserwowanych potrzebna jest
        // wyłącznie właściwa nazwa profilu.
        if (/^opinie\s+o\s+/i.test(name)) {
            name = name.replace(/^opinie\s+o\s+/i, '').trim();
            const citySeparator = name.toLocaleLowerCase('pl-PL').lastIndexOf(' z ');
            if (citySeparator > 0) name = name.slice(0, citySeparator).trim();
        }
        return name || fallback;
    }

    function normalizeWatchedItem(item) {
        if (!item || typeof item !== 'object' || !item.id) return null;
        const customDisplayName = normalizeEscortAdText(item.customDisplayName)
            .slice(0, 120);
        const profileUrls = [...new Set(
            (Array.isArray(item.profileUrls) ? item.profileUrls : [])
                .map(normalizeWatchUrl)
                .filter(Boolean)
        )];
        const adUrl = item.adUrl
            ? normalizeEscortiAdUrl(item.adUrl) || normalizeWatchUrl(item.adUrl)
            : null;
        const phoneDigits = normalizeEscortCachedPhone(item.phoneDigits || item.phone);
        const resolvedAdUrls = [...new Set(
            (Array.isArray(item.resolvedAdUrls) ? item.resolvedAdUrls : [])
                .map(url => normalizeEscortiAdUrl(url))
                .filter(Boolean)
        )];
        const targetType = item.locationTarget?.type === 'city' ? 'city' : 'any';
        const targetCity = targetType === 'city'
            ? normalizeEscortCity(item.locationTarget?.city)
            : '';

        return {
            ...item,
            id: String(item.id),
            sourceType: ['escorti-profile', 'escort-ad', 'phone'].includes(item.sourceType)
                ? item.sourceType
                : (adUrl ? 'escort-ad' : (phoneDigits ? 'phone' : 'escorti-profile')),
            watchKey: String(item.watchKey || ''),
            displayName: customDisplayName || normalizeWatchedDisplayName(item.displayName),
            customDisplayName: customDisplayName || null,
            imageUrl: String(item.imageUrl || ''),
            profileUrls,
            adUrl,
            resolvedAdUrls,
            phoneDigits: phoneDigits || null,
            modes: {
                online: !!item.modes?.online,
                location: !!item.modes?.location
            },
            locationTarget: {
                type: targetType,
                city: targetCity
            },
            createdAt: Number(item.createdAt) || Date.now(),
            lastCheckedAt: Number(item.lastCheckedAt) || 0,
            lastError: item.lastError ? String(item.lastError) : null,
            state: item.state && typeof item.state === 'object'
                ? {
                    ...item.state,
                    online: typeof item.state.online === 'boolean' ? item.state.online : null,
                    activeCount: Number(item.state.activeCount) || 0,
                    totalAds: Number(item.state.totalAds) || 0,
                    cities: Array.isArray(item.state.cities)
                        ? item.state.cities.map(normalizeEscortCity).filter(Boolean)
                        : [],
                    checkedAt: Number(item.state.checkedAt) || 0
                }
                : null
        };
    }

    function getWatchedItems() {
        try {
            const saved = GM_getValue(WATCH_ITEMS_STORAGE_KEY, []);
            return Array.isArray(saved)
                ? saved.map(normalizeWatchedItem).filter(Boolean)
                : [];
        } catch (_) {
            return [];
        }
    }

    function saveWatchedItems(items) {
        GM_setValue(
            WATCH_ITEMS_STORAGE_KEY,
            (Array.isArray(items) ? items : []).map(normalizeWatchedItem).filter(Boolean)
        );
        refreshWatchPageButtons();
    }

    function refreshWatchPageButtons() {
        for (const refresh of watchPageButtonRefreshers) {
            try { refresh(); } catch (_) {}
        }
    }

    function removeWatchPageUi() {
        document.querySelectorAll('[data-vm-watch-wrapper="1"]').forEach(element => {
            element.remove();
        });
        for (const id of [
            'vm-escorti-watch-online',
            'vm-escorti-watch-location',
            'vm-escort-watch-online',
            'vm-escort-watch-location'
        ]) {
            document.getElementById(id)?.remove();
        }
        watchPageButtonRefreshers.clear();
        renderWatchNotificationBar();
    }

    function getWatchEvents() {
        try {
            const saved = GM_getValue(WATCH_EVENTS_STORAGE_KEY, []);
            if (!Array.isArray(saved)) return [];
            return saved
                .filter(event => event && event.id && event.itemId && event.message)
                .map(event => ({
                    ...event,
                    id: String(event.id),
                    itemId: String(event.itemId),
                    timestamp: Number(event.timestamp) || Date.now(),
                    acknowledged: !!event.acknowledged
                }))
                .sort((a, b) => b.timestamp - a.timestamp);
        } catch (_) {
            return [];
        }
    }

    function saveWatchEvents(events) {
        GM_setValue(
            WATCH_EVENTS_STORAGE_KEY,
            (Array.isArray(events) ? events : [])
                .sort((a, b) => Number(b.timestamp || 0) - Number(a.timestamp || 0))
                .slice(0, 200)
        );
    }

    function createWatchEvent(item, type, message, url) {
        const events = getWatchEvents();
        events.unshift({
            id: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
            itemId: item.id,
            type,
            name: item.displayName,
            message,
            url: url || item.profileUrls?.[0] || item.adUrl || '',
            timestamp: Date.now(),
            acknowledged: false
        });
        saveWatchEvents(events);
    }

    function acknowledgeWatchEvent(eventId) {
        saveWatchEvents(getWatchEvents().map(event => (
            event.id === eventId ? { ...event, acknowledged: true } : event
        )));
        renderWatchNotificationBar();
        document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();
    }

    function acknowledgeAllWatchEvents() {
        saveWatchEvents(getWatchEvents().map(event => ({
            ...event,
            acknowledged: true
        })));
        renderWatchNotificationBar();
        document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();
    }

    function extractWatchPageName(doc, fallback = 'Obserwowane anons') {
        if (!doc) return fallback;
        const candidates = [
            doc.querySelector('meta[property="og:title"]')?.getAttribute('content'),
            doc.querySelector('h1')?.textContent,
            doc.title
        ];
        for (const candidate of candidates) {
            const name = normalizeEscortAdText(candidate)
                .replace(/\s*[|–—-]\s*(?:Escorti(?:\.pl)?|Escort\.club).*$/i, '')
                .trim();
            if (name && !/^escorti(?:\.pl)?$/i.test(name)) {
                return normalizeWatchedDisplayName(name, fallback).slice(0, 120);
            }
        }
        return fallback;
    }

    function extractWatchPageImage(doc, pageUrl = location.href) {
        if (!doc) return '';
        const candidates = [
            doc.querySelector('#lightSlider img[src], #lightSlider img[data-src]')?.getAttribute('src'),
            doc.querySelector('.galleryContainer img[src], .galleryContainer img[data-src]')?.getAttribute('src'),
            doc.querySelector('.content-gallery-col img[src], .content-gallery-col img[data-src]')?.getAttribute('src'),
            doc.querySelector('.profile-image img[src], .profile-photo img[src], .escort-image img[src]')?.getAttribute('src'),
            doc.querySelector('meta[property="og:image"]')?.getAttribute('content'),
            doc.querySelector('main img[src], main img[data-src]')?.getAttribute('src')
        ];
        for (const candidate of candidates) {
            if (!candidate || /^data:/i.test(candidate)) continue;
            try {
                return new URL(candidate, pageUrl).href;
            } catch (_) {}
        }
        return '';
    }

    function hasUsefulWatchThumbnail(item) {
        const imageUrl = String(item?.imageUrl || '').trim();
        if (!imageUrl) return false;
        if (item.sourceType !== 'escort-ad') return true;
        return /static\.escort\.club\/galleries\//i.test(imageUrl);
    }

    function formatWatchDateTime(value) {
        if (!Number(value)) return 'jeszcze nie sprawdzono';
        try {
            return new Intl.DateTimeFormat('pl-PL', {
                day: '2-digit',
                month: '2-digit',
                year: 'numeric',
                hour: '2-digit',
                minute: '2-digit'
            }).format(new Date(Number(value)));
        } catch (_) {
            return new Date(Number(value)).toLocaleString();
        }
    }

    function watchItemSourceText(item) {
        if (item.sourceType === 'escort-ad') return 'Pojedyncze anons Escort.club';
        if (item.sourceType === 'phone') return `Profil Escorti wyszukiwany po telefonie${item.phoneDigits ? `: ${item.phoneDigits}` : ''}`;
        return 'Profil Escorti';
    }

    function getWatchKey(context) {
        if (context.watchKey) return context.watchKey;
        const profileIds = (context.profileUrls || [])
            .map(getEscortiProfileId)
            .filter(Boolean)
            .sort((a, b) => Number(a) - Number(b));
        if (profileIds.length) return `profiles:${profileIds.join(',')}`;
        const phone = normalizeEscortCachedPhone(context.phoneDigits || context.phone);
        if (phone) return `phone:${phone}`;
        const adId = parseAdIdFromUrl(context.adUrl);
        if (adId) return `ad:${adId}`;
        return `other:${Math.random().toString(36).slice(2)}`;
    }

    function watchedItemsOverlap(first, second) {
        if (first.watchKey && second.watchKey && first.watchKey === second.watchKey) return true;
        const firstIsSingleAd = first.sourceType === 'escort-ad';
        const secondIsSingleAd = second.sourceType === 'escort-ad';
        if (firstIsSingleAd || secondIsSingleAd) {
            return firstIsSingleAd && secondIsSingleAd && !!first.adUrl && !!second.adUrl &&
                parseAdIdFromUrl(first.adUrl) === parseAdIdFromUrl(second.adUrl);
        }
        if (
            first.phoneDigits && second.phoneDigits &&
            normalizeEscortCachedPhone(first.phoneDigits) === normalizeEscortCachedPhone(second.phoneDigits)
        ) return true;
        const firstProfiles = new Set((first.profileUrls || []).map(getEscortiProfileId).filter(Boolean));
        return (second.profileUrls || []).some(url => firstProfiles.has(getEscortiProfileId(url)));
    }

    function upsertWatchedItem(context, modePatch = {}, locationTarget = null) {
        const normalizedContext = normalizeWatchedItem({
            id: context.id || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
            ...context,
            watchKey: getWatchKey(context),
            modes: {
                online: !!modePatch.online,
                location: !!modePatch.location
            },
            locationTarget: locationTarget || { type: 'any', city: '' },
            createdAt: context.createdAt || Date.now()
        });
        const items = getWatchedItems();
        const index = items.findIndex(item => watchedItemsOverlap(item, normalizedContext));

        if (index >= 0) {
            const existing = items[index];
            items[index] = normalizeWatchedItem({
                ...existing,
                ...normalizedContext,
                id: existing.id,
                displayName: existing.customDisplayName ||
                    normalizedContext.displayName || existing.displayName,
                customDisplayName: existing.customDisplayName || null,
                imageUrl: normalizedContext.imageUrl || existing.imageUrl,
                profileUrls: [...new Set([
                    ...(existing.profileUrls || []),
                    ...(normalizedContext.profileUrls || [])
                ])],
                phoneDigits: normalizedContext.phoneDigits || existing.phoneDigits,
                adUrl: normalizedContext.adUrl || existing.adUrl,
                modes: {
                    online: existing.modes.online || !!modePatch.online,
                    location: existing.modes.location || !!modePatch.location
                },
                locationTarget: locationTarget || existing.locationTarget,
                state: existing.state,
                lastCheckedAt: existing.lastCheckedAt,
                lastError: existing.lastError
            });
            saveWatchedItems(items);
            return items[index];
        }

        saveWatchedItems([...items, normalizedContext]);
        return normalizedContext;
    }

    function updateWatchedItem(itemId, update) {
        const items = getWatchedItems();
        const index = items.findIndex(item => item.id === itemId);
        if (index < 0) return null;
        items[index] = normalizeWatchedItem({
            ...items[index],
            ...(typeof update === 'function' ? update(items[index]) : update)
        });
        saveWatchedItems(items);
        return items[index];
    }

    function renameWatchedItem(itemId, value) {
        const newName = normalizeEscortAdText(value).slice(0, 120);
        if (!newName) return null;

        const items = getWatchedItems();
        const index = items.findIndex(item => item.id === itemId);
        if (index < 0) return null;

        const previousName = items[index].displayName;
        items[index] = normalizeWatchedItem({
            ...items[index],
            displayName: newName,
            customDisplayName: newName
        });
        saveWatchedItems(items);

        const escapePattern = text => String(text || '')
            .replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        saveWatchEvents(getWatchEvents().map(event => {
            if (event.itemId !== itemId) return event;
            const oldEventName = event.name || previousName;
            let message = String(event.message || '');
            for (const oldName of [oldEventName, previousName]) {
                if (!oldName) continue;
                const pattern = new RegExp(`^${escapePattern(oldName)}(?=\\s|$)`, 'i');
                if (pattern.test(message)) {
                    message = message.replace(pattern, newName);
                    break;
                }
            }
            return { ...event, name: newName, message };
        }));
        renderWatchNotificationBar();
        return items[index];
    }

    function removeWatchedItem(itemId) {
        saveWatchedItems(getWatchedItems().filter(item => item.id !== itemId));
        saveWatchEvents(getWatchEvents().filter(event => event.itemId !== itemId));
        renderWatchNotificationBar();
    }

    function disableWatchedMode(itemsToChange, mode) {
        const ids = new Set((itemsToChange || []).map(item => String(item.id)));
        if (!ids.size || !['online', 'location'].includes(mode)) return;

        const removedIds = new Set();
        const nextItems = [];
        for (const item of getWatchedItems()) {
            if (!ids.has(item.id)) {
                nextItems.push(item);
                continue;
            }

            const updated = normalizeWatchedItem({
                ...item,
                modes: {
                    ...item.modes,
                    [mode]: false
                }
            });
            if (updated.modes.online || updated.modes.location) nextItems.push(updated);
            else removedIds.add(item.id);
        }

        saveWatchedItems(nextItems);
        if (removedIds.size) {
            saveWatchEvents(getWatchEvents().filter(event => !removedIds.has(event.itemId)));
        }
        renderWatchNotificationBar();
        document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();
    }

    function acquireWatchCheckLock(force = false) {
        const now = Date.now();
        const current = GM_getValue(WATCH_CHECK_LOCK_KEY, null);
        if (
            !force && current?.owner && current.owner !== WATCH_INSTANCE_ID &&
            Number(current.expiresAt) > now
        ) return false;

        const lock = {
            owner: WATCH_INSTANCE_ID,
            expiresAt: now + 180000
        };
        GM_setValue(WATCH_CHECK_LOCK_KEY, lock);
        return GM_getValue(WATCH_CHECK_LOCK_KEY, null)?.owner === WATCH_INSTANCE_ID;
    }

    function touchWatchCheckLock() {
        const current = GM_getValue(WATCH_CHECK_LOCK_KEY, null);
        if (current?.owner !== WATCH_INSTANCE_ID) return;
        GM_setValue(WATCH_CHECK_LOCK_KEY, {
            owner: WATCH_INSTANCE_ID,
            expiresAt: Date.now() + 180000
        });
    }

    function releaseWatchCheckLock() {
        const current = GM_getValue(WATCH_CHECK_LOCK_KEY, null);
        if (current?.owner === WATCH_INSTANCE_ID) GM_deleteValue(WATCH_CHECK_LOCK_KEY);
    }

    async function watchMapLimit(values, limit, worker) {
        const source = Array.isArray(values) ? values : [];
        const results = new Array(source.length);
        let index = 0;
        async function run() {
            while (true) {
                const current = index++;
                if (current >= source.length) return;
                results[current] = await worker(source[current], current);
            }
        }
        const workerCount = Math.min(
            Math.max(1, limit),
            Math.max(1, source.length)
        );
        const workers = [];
        for (let workerIndex = 0; workerIndex < workerCount; workerIndex++) {
            workers.push(run());
        }
        await Promise.all(workers);
        return results;
    }

    async function resolveWatchedProfileData(item) {
        let profileUrls = [...new Set(item.profileUrls || [])];
        if (!profileUrls.length && item.phoneDigits) {
            const found = await searchEscortiProfilesDirect(item.phoneDigits);
            profileUrls = found.status === 'ok' ? found.profileUrls || [] : [];
        }

        const profileResults = (await watchMapLimit(profileUrls, 2, async profileUrl => {
            try {
                const result = await checkEscortiProfileBackground(profileUrl, {
                    timeoutMs: 60000
                });
                if (!result || result.status !== 'ok') {
                    throw new Error(result?.message || 'Nie udało się odczytać profilu Escorti');
                }
                return result;
            } catch (error) {
                return { error: error?.message || String(error), profileUrl };
            }
        })).filter(Boolean);
        const readable = profileResults.filter(result => !result.error);
        let fetchedAdUrls = [...new Set(
            readable.flatMap(result => result.adUrls || []).map(url => normalizeEscortiAdUrl(url)).filter(Boolean)
        )];

        // Pusta odpowiedź z ukrytej ramki nie jest kompletnym wynikiem.
        // Powtórz odczyt bezpośrednio, tak jak robi ścieżka wyszukiwania Escorti.
        if (!fetchedAdUrls.length && profileUrls.length) {
            const directResults = (await watchMapLimit(profileUrls, 2, async profileUrl => {
                try {
                    return await readEscortiProfileDirect(profileUrl);
                } catch (_) {
                    return null;
                }
            })).filter(Boolean);
            readable.push(...directResults);
            fetchedAdUrls = [...new Set(
                directResults
                    .flatMap(result => result.adUrls || [])
                    .map(url => normalizeEscortiAdUrl(url))
                    .filter(Boolean)
            )];
        }

        if (!readable.length && profileUrls.length) {
            throw new Error('Nie udało się odczytać profilu Escorti');
        }

        const cachedAdUrls = getCachedEscortiAdUrlsForProfiles(profileUrls);
        const adUrls = [...new Set([
            ...fetchedAdUrls,
            ...(item.resolvedAdUrls || []).map(url => normalizeEscortiAdUrl(url)).filter(Boolean),
            ...cachedAdUrls
        ])];
        return {
            profileUrls,
            adUrls,
            displayName: readable.map(result => result.profileName).find(Boolean) || item.displayName,
            imageUrl: readable.map(result => result.imageUrl).find(Boolean) || item.imageUrl
        };
    }

    function watchCitiesKey(cities) {
        return [...new Set((cities || []).map(normalizeEscortCityKey).filter(Boolean))]
            .sort()
            .join('|');
    }

    function watchCitiesText(cities) {
        const normalized = [...new Set((cities || []).map(normalizeEscortCity).filter(Boolean))];
        return normalized.length ? normalized.join(', ') : 'brak aktywnej lokalizacji';
    }

    async function inspectWatchedItem(item, onAdProgress = null) {
        let adUrls = [];
        let profileUrls = item.profileUrls || [];
        let displayName = item.displayName;
        let imageUrl = item.imageUrl;
        let profileImageUrl = '';

        if (item.sourceType === 'escort-ad') {
            if (!item.adUrl) throw new Error('Brak adresu anonsu Escort.club');
            adUrls = [item.adUrl];
        } else {
            const profileData = await resolveWatchedProfileData(item);
            profileUrls = profileData.profileUrls;
            adUrls = profileData.adUrls;
            displayName = profileData.displayName;
            profileImageUrl = profileData.imageUrl || '';
        }

        if (onAdProgress) {
            try {
                onAdProgress({
                    checked: 0,
                    total: adUrls.length,
                    activeCount: 0,
                    url: null,
                    result: null
                });
            } catch (_) {}
        }

        const activitySummary = await scanActiveEscortAds(
            adUrls,
            onAdProgress,
            true,
            {
                // Stan online/offline zawsze sprawdzamy lekkim HEAD. Miasto
                // odczytujemy później tylko z anonsów, które są faktycznie aktywne.
                // Dzięki temu wyświetlanie lokalizacji nie zależy od checkboxa
                // „obserwuj lokalizację” i nie wymaga pobierania pełnych stron
                // wszystkich nieaktywnych anonsów.
                activityOnly: true
            }
        );
        const results = Array.isArray(activitySummary?.results)
            ? activitySummary.results
            : [];
        let activeResults = results.filter(result => result?.active);

        // Probe HEAD nie zawiera miasta. Dla aktywnych anonsów pobierz pełne dane
        // niezależnie od trybu powiadomień lokalizacyjnych. Checkbox lokalizacji
        // decyduje wyłącznie o generowaniu zdarzeń, nie o zapisie bieżącego miasta.
        const activeWithoutDetails = activeResults.filter(result =>
            result?.url && !normalizeEscortCity(result.city)
        );
        if (activeWithoutDetails.length) {
            const detailedResults = await watchMapLimit(
                activeWithoutDetails,
                ESCORT_ACTIVE_CHECK_CONCURRENT,
                async probeResult => {
                    try {
                        const detailed = await inspectEscortClubAd(probeResult.url);
                        return detailed?.active
                            ? { ...probeResult, ...detailed }
                            : probeResult;
                    } catch (_) {
                        return probeResult;
                    }
                }
            );
            const detailedByUrl = new Map(
                detailedResults
                    .filter(result => result?.url)
                    .map(result => [result.url, result])
            );
            activeResults = activeResults.map(result =>
                detailedByUrl.get(result.url) || result
            );
        }

        let inspectedImageUrl = activeResults
            .map(result => result.imageUrl)
            .find(Boolean) || '';
        const unknownCount = results.filter(result => result?.unknown).length;
        const observedCities = [...new Set(
            activeResults.map(result => normalizeEscortCity(result.city)).filter(Boolean)
        )];
        const activeWithoutCity = activeResults.some(result => !normalizeEscortCity(result.city));
        const locationPartial = unknownCount > 0 || activeWithoutCity;
        const previousCities = Array.isArray(item.state?.cities)
            ? item.state.cities.map(normalizeEscortCity).filter(Boolean)
            : [];
        const cities = locationPartial ? previousCities : observedCities;
        const previousActiveCount = Number(item.state?.activeCount) || 0;
        const activeCount = unknownCount > 0
            ? Math.max(previousActiveCount, activeResults.length)
            : activeResults.length;
        const previousOnline = typeof item.state?.online === 'boolean'
            ? item.state.online
            : null;
        const online = adUrls.length === 0
            ? null
            : (activeResults.length > 0
                ? true
                : (unknownCount > 0 ? previousOnline : false));

        // Brak miniaturki nie uruchamia żadnego ukrytego sprawdzania po
        // otwarciu okna. Uzupełniamy ją dopiero, gdy pozycja podczas normalnego
        // sprawdzania przejdzie ze stanu offline/nieznanego na online.
        if (!hasUsefulWatchThumbnail(item) && online === true && previousOnline !== true) {
            if (!inspectedImageUrl) {
                const firstActiveUrl = activeResults.find(result => result?.active)?.url;
                if (firstActiveUrl) {
                    try {
                        const detailed = await inspectEscortClubAd(firstActiveUrl);
                        if (detailed?.active && detailed.imageUrl) {
                            inspectedImageUrl = detailed.imageUrl;
                        }
                    } catch (_) {}
                }
            }
            imageUrl = inspectedImageUrl || profileImageUrl || imageUrl;
        }

        return {
            displayName,
            imageUrl,
            profileUrls,
            adUrls,
            activeResults,
            state: {
                online,
                activeCount,
                totalAds: adUrls.length,
                cities,
                unknownCount,
                partial: unknownCount > 0,
                locationPartial,
                checkedAt: Date.now()
            }
        };
    }

    function applyWatchedResult(item, result) {
        const previous = item.state;
        const current = result.state;
        const defaultEventUrl = result.activeResults?.[0]?.url
            || result.profileUrls?.[0]
            || item.adUrl;
        const updated = normalizeWatchedItem({
            ...item,
            displayName: item.customDisplayName || result.displayName || item.displayName,
            customDisplayName: item.customDisplayName || null,
            imageUrl: result.imageUrl || item.imageUrl,
            profileUrls: result.profileUrls || item.profileUrls,
            resolvedAdUrls: result.adUrls || item.resolvedAdUrls,
            state: current,
            lastCheckedAt: current.checkedAt,
            lastError: null
        });

        if (previous?.checkedAt) {
            if (item.modes.online && previous.online === false && current.online === true) {
                createWatchEvent(
                    updated,
                    'online',
                    `${updated.displayName} pojawiła się online (${current.activeCount} aktywne).`,
                    defaultEventUrl
                );
            }

            // Nie porównuj lokalizacji po timeoutach ani po odpowiedzi aktywnego
            // anonsu, z którego nie udało się odczytać miasta. Zachowujemy
            // ostatni kompletny stan i czekamy na pewne sprawdzenie.
            if (item.modes.location && !current.locationPartial && current.cities.length) {
                const target = item.locationTarget || { type: 'any', city: '' };
                if (target.type === 'city') {
                    const targetKey = normalizeEscortCityKey(target.city);
                    const previousMatch = (previous.cities || [])
                        .some(city => normalizeEscortCityKey(city) === targetKey);
                    const currentMatch = current.cities
                        .some(city => normalizeEscortCityKey(city) === targetKey);
                    if (!previousMatch && currentMatch) {
                        const matchingAdUrl = result.activeResults?.find(active =>
                            normalizeEscortCityKey(active.city) === targetKey
                        )?.url;
                        createWatchEvent(
                            updated,
                            'location',
                            `${updated.displayName} pojawiła się w mieście ${target.city}.`,
                            matchingAdUrl || defaultEventUrl
                        );
                    }
                } else if (watchCitiesKey(previous.cities) !== watchCitiesKey(current.cities)) {
                    const previousCityKeys = new Set(
                        (previous.cities || []).map(normalizeEscortCityKey).filter(Boolean)
                    );
                    const changedAdUrl = result.activeResults?.find(active => {
                        const cityKey = normalizeEscortCityKey(active.city);
                        return cityKey && !previousCityKeys.has(cityKey);
                    })?.url;
                    createWatchEvent(
                        updated,
                        'location',
                        `${updated.displayName} zmieniła lokalizację: ${watchCitiesText(previous.cities)} → ${watchCitiesText(current.cities)}.`,
                        changedAdUrl || defaultEventUrl
                    );
                }
            }
        }

        return updated;
    }

    async function runWatchedChecks({ force = false, itemIds = null, onProgress = null } = {}) {
        if (watchedCheckPromise) {
            const pendingCheck = watchedCheckPromise;
            if (force) {
                try { await pendingCheck; } catch (_) {}
                return runWatchedChecks({ force, itemIds, onProgress });
            }
            return pendingCheck;
        }
        const settings = getWatchSettings();
        if (!settings.enabled) return null;
        if (!acquireWatchCheckLock(force)) return null;

        watchedCheckPromise = (async () => {
            let items = getWatchedItems();
            const allowedIds = itemIds ? new Set(itemIds.map(String)) : null;
            const dueBefore = Date.now() - settings.intervalMinutes * 60000;
            const due = items.filter(item =>
                (item.modes.online || item.modes.location) &&
                (!allowedIds || allowedIds.has(item.id)) &&
                (force || !item.lastCheckedAt || item.lastCheckedAt <= dueBefore)
            );

            for (let index = 0; index < due.length; index++) {
                touchWatchCheckLock();
                const candidate = due[index];

                const reportProgress = adProgress => {
                    if (!onProgress) return;
                    try {
                        onProgress({
                            checked: index,
                            total: due.length,
                            itemIndex: index + 1,
                            itemName: candidate.displayName,
                            adChecked: Number(adProgress?.checked) || 0,
                            adTotal: Number(adProgress?.total) || 0,
                            activeCount: Number(adProgress?.activeCount) || 0,
                            phase: 'checking'
                        });
                    } catch (_) {}
                };

                // Pokaż właściwy profil jeszcze zanim zakończy się pierwszy request.
                reportProgress({ checked: 0, total: 0, activeCount: 0 });

                try {
                    const result = await inspectWatchedItem(candidate, reportProgress);
                    const updated = applyWatchedResult(candidate, result);
                    const currentIndex = items.findIndex(item => item.id === candidate.id);
                    if (currentIndex >= 0) items[currentIndex] = updated;
                } catch (error) {
                    const currentIndex = items.findIndex(item => item.id === candidate.id);
                    if (currentIndex >= 0) {
                        items[currentIndex] = normalizeWatchedItem({
                            ...items[currentIndex],
                            lastCheckedAt: Date.now(),
                            lastError: error?.message || String(error)
                        });
                    }
                }
                saveWatchedItems(items);
                renderWatchNotificationBar();
                document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();
                if (onProgress) {
                    try {
                        onProgress({
                            checked: index + 1,
                            total: due.length,
                            itemIndex: index + 1,
                            itemName: candidate.displayName,
                            adChecked: Number(items.find(item => item.id === candidate.id)?.state?.totalAds) || 0,
                            adTotal: Number(items.find(item => item.id === candidate.id)?.state?.totalAds) || 0,
                            activeCount: Number(items.find(item => item.id === candidate.id)?.state?.activeCount) || 0,
                            phase: 'done'
                        });
                    } catch (_) {}
                }
            }
            return items;
        })().finally(() => {
            watchedCheckPromise = null;
            releaseWatchCheckLock();
        });
        return watchedCheckPromise;
    }

    function showWatchToast(message, error = false) {
        document.getElementById('vm-garso-watch-toast')?.remove();
        const toast = makeElement('div');
        toast.id = 'vm-garso-watch-toast';
        toast.textContent = message;
        Object.assign(toast.style, {
            position: 'fixed',
            right: '18px',
            bottom: '18px',
            zIndex: '2147483647',
            padding: '10px 14px',
            borderRadius: '8px',
            background: error ? '#c62828' : getEscortPagePinkColor(),
            color: '#fff',
            fontSize: '13px',
            fontWeight: '700',
            boxShadow: '0 4px 18px rgba(0,0,0,.28)'
        });
        document.body.appendChild(toast);
        setTimeout(() => toast.remove(), 2800);
    }

    function restoreWatchBarTopElements() {
        for (const [element, original] of watchBarShiftedTopElements) {
            try {
                if (original.value) {
                    element.style.setProperty('top', original.value, original.priority || '');
                } else {
                    element.style.removeProperty('top');
                }
            } catch (_) {}
        }
        watchBarShiftedTopElements.clear();
        document.documentElement.style.setProperty(
            '--vm-escort-summary-panel-top-offset',
            '0px'
        );
    }

    function syncWatchBarPageOffset(bar, spacer) {
        if (!bar?.isConnected || !spacer?.isConnected) return;
        const height = Math.ceil(bar.getBoundingClientRect().height);
        spacer.style.height = `${height}px`;
        document.documentElement.style.setProperty(
            '--vm-escort-summary-panel-top-offset',
            `${height}px`
        );

        // Escorti ma własną nawigację fixed top-0. Sam element dystansowy
        // przesuwa treść, ale nie element wyrwany z układu strony.
        const topFixedElements = location.hostname.toLowerCase().endsWith('escorti.pl')
            ? document.querySelectorAll('#main-app > nav.fixed.top-0')
            : [];

        for (const element of topFixedElements) {
            if (!watchBarShiftedTopElements.has(element)) {
                watchBarShiftedTopElements.set(element, {
                    value: element.style.getPropertyValue('top'),
                    priority: element.style.getPropertyPriority('top')
                });
            }
            element.style.setProperty('top', `${height}px`, 'important');
        }
    }

    function renderWatchNotificationBar() {
        const render = () => {
            const previousBar = document.getElementById(WATCH_BAR_ID);
            try { previousBar?._vmResizeObserver?.disconnect(); } catch (_) {}
            try { previousBar?._vmResizeCleanup?.(); } catch (_) {}
            previousBar?.remove();
            document.getElementById(WATCH_BAR_SPACER_ID)?.remove();
            restoreWatchBarTopElements();
            const watchSettings = getWatchSettings();
            if (!watchSettings.enabled || !document.body) return;
            const events = getWatchEvents().filter(event => !event.acknowledged);
            const showOnlineCount = watchSettings.showOnlineCount === true;
            if (!events.length && !showOnlineCount) return;
            const watchedItems = getWatchedItems();
            const onlineItems = watchedItems.filter(item =>
                item.modes?.online && item.state?.online === true
            );
            const onlineCount = onlineItems.length;

            const bar = makeElement('div');
            bar.id = WATCH_BAR_ID;
            Object.assign(bar.style, {
                position: 'fixed',
                left: '0',
                right: '0',
                top: '0',
                zIndex: '2147483645',
                display: 'flex',
                alignItems: 'center',
                gap: '8px',
                padding: '4px 8px',
                boxSizing: 'border-box',
                background: getEscortPagePinkColor(),
                color: '#fff',
                boxShadow: '0 1px 5px rgba(0,0,0,.28)',
                fontSize: '12px',
                lineHeight: '1.25'
            });

            const createOnlineSummary = compact => {
                const onlineSummary = makeButton('', `Online:${onlineCount}`);
                const onlineNames = onlineItems.map(item => {
                    const type = item.sourceType === 'escort-ad' ? 'Anons' : 'Profil';
                    return `${type}: ${item.displayName}`;
                });
                onlineSummary.setAttribute(
                    'aria-label',
                    onlineNames.length
                        ? `Online: ${onlineCount}. ${onlineNames.join('. ')}. Kliknij, aby otworzyć listę obserwowanych.`
                        : 'Brak obserwowanych profili lub anonsów online. Kliknij, aby otworzyć listę obserwowanych.'
                );
                Object.assign(onlineSummary.style, {
                    flex: '0 0 auto',
                    whiteSpace: 'nowrap',
                    padding: compact ? '3px 8px' : '1px 6px',
                    border: compact
                        ? '0'
                        : '1px solid rgba(255,255,255,.72)',
                    borderRadius: compact ? '0 0 7px 7px' : '10px',
                    background: compact ? 'transparent' : 'rgba(255,255,255,.14)',
                    color: '#fff',
                    cursor: 'pointer',
                    font: 'inherit',
                    fontWeight: '800'
                });

                const tooltip = makeElement('div');
                tooltip.id = `${WATCH_BAR_ID}-online-tooltip`;
                tooltip.setAttribute('role', 'tooltip');
                Object.assign(tooltip.style, {
                    display: 'none',
                    position: 'fixed',
                    zIndex: '2147483647',
                    width: 'min(330px, calc(100vw - 16px))',
                    maxHeight: 'min(520px, calc(100vh - 48px))',
                    overflowY: 'auto',
                    padding: '8px',
                    border: '1px solid rgba(255,255,255,.28)',
                    borderRadius: '8px',
                    background: '#35103d',
                    color: '#fff',
                    boxShadow: '0 5px 18px rgba(0,0,0,.42)',
                    boxSizing: 'border-box',
                    textAlign: 'left',
                    whiteSpace: 'normal',
                    pointerEvents: 'none'
                });

                const tooltipHeading = makeElement('div');
                tooltipHeading.textContent = onlineItems.length
                    ? `Obecnie online: ${onlineCount}`
                    : 'Nikt z obserwowanych nie jest online';
                Object.assign(tooltipHeading.style, {
                    margin: '0 0 6px',
                    color: getEscortPagePinkColor(),
                    fontSize: '12px',
                    fontWeight: '800'
                });
                tooltip.appendChild(tooltipHeading);

                for (const item of onlineItems) {
                    const row = makeElement('div');
                    Object.assign(row.style, {
                        display: 'grid',
                        gridTemplateColumns: '42px minmax(0, 1fr)',
                        gap: '8px',
                        alignItems: 'center',
                        padding: '5px 0',
                        borderTop: tooltip.childElementCount > 1
                            ? '1px solid rgba(255,255,255,.12)'
                            : '0'
                    });

                    const imageBox = makeElement('div');
                    const initial = normalizeEscortAdText(item.displayName)
                        .charAt(0)
                        .toLocaleUpperCase('pl-PL') || '•';
                    imageBox.textContent = initial;
                    Object.assign(imageBox.style, {
                        position: 'relative',
                        display: 'flex',
                        alignItems: 'center',
                        justifyContent: 'center',
                        width: '42px',
                        height: '42px',
                        overflow: 'hidden',
                        border: '1px solid rgba(255,255,255,.18)',
                        borderRadius: '6px',
                        background: 'rgba(255,255,255,.09)',
                        color: 'rgba(255,255,255,.75)',
                        fontSize: '17px',
                        fontWeight: '800',
                        boxSizing: 'border-box'
                    });
                    if (hasUsefulWatchThumbnail(item)) {
                        const image = makeElement('img');
                        image.src = item.imageUrl;
                        image.alt = '';
                        image.loading = 'lazy';
                        image.referrerPolicy = 'no-referrer';
                        Object.assign(image.style, {
                            position: 'absolute',
                            inset: '0',
                            width: '100%',
                            height: '100%',
                            objectFit: 'cover'
                        });
                        image.addEventListener('error', () => image.remove(), { once: true });
                        imageBox.appendChild(image);
                    }

                    const details = makeElement('div');
                    details.style.minWidth = '0';
                    const type = makeElement('div');
                    type.textContent = item.sourceType === 'escort-ad'
                        ? 'Anons Escort.club'
                        : 'Profil Escorti';
                    Object.assign(type.style, {
                        marginBottom: '2px',
                        color: 'rgba(255,255,255,.68)',
                        fontSize: '10px'
                    });
                    const name = makeElement('div', '', item.displayName);
                    Object.assign(name.style, {
                        color: '#fff',
                        fontSize: '12px',
                        fontWeight: '700',
                        lineHeight: '1.25',
                        overflowWrap: 'anywhere'
                    });
                    details.append(type, name);
                    row.append(imageBox, details);
                    tooltip.appendChild(row);
                }

                const positionTooltip = () => {
                    const rect = onlineSummary.getBoundingClientRect();
                    const margin = 8;
                    const tooltipWidth = tooltip.offsetWidth;
                    const tooltipHeight = tooltip.offsetHeight;
                    const left = Math.min(
                        Math.max(margin, rect.left),
                        Math.max(margin, window.innerWidth - tooltipWidth - margin)
                    );
                    const below = rect.bottom + 5;
                    const top = below + tooltipHeight <= window.innerHeight - margin
                        ? below
                        : Math.max(margin, rect.top - tooltipHeight - 5);
                    tooltip.style.left = `${Math.round(left)}px`;
                    tooltip.style.top = `${Math.round(top)}px`;
                };
                const showTooltip = () => {
                    tooltip.style.display = 'block';
                    requestAnimationFrame(positionTooltip);
                };
                const hideTooltip = () => {
                    tooltip.style.display = 'none';
                };
                onlineSummary.addEventListener('mouseenter', showTooltip);
                onlineSummary.addEventListener('mouseleave', hideTooltip);
                onlineSummary.addEventListener('focus', showTooltip);
                onlineSummary.addEventListener('blur', hideTooltip);
                onlineSummary.addEventListener('click', () => {
                    hideTooltip();
                    openWatchedSettingsModal();
                });
                bar.appendChild(tooltip);
                return onlineSummary;
            };

            // Bez nowych, niezaakceptowanych zmian pozostaje wyłącznie mały
            // licznik. Jest nakładany na stronę i nie tworzy elementu
            // dystansowego, więc nie przesuwa treści ani nawigacji.
            if (!events.length) {
                Object.assign(bar.style, {
                    left: '8px',
                    right: 'auto',
                    width: 'auto',
                    gap: '0',
                    padding: '0',
                    borderRadius: '0 0 7px 7px'
                });
                bar.appendChild(createOnlineSummary(true));
                document.body.appendChild(bar);
                return;
            }

            const label = makeElement('strong', '', 'Obserwowane:');
            label.style.whiteSpace = 'nowrap';
            bar.appendChild(label);

            if (showOnlineCount) {
                bar.appendChild(createOnlineSummary(false));
            }

            const list = makeElement('div');
            Object.assign(list.style, {
                display: 'flex',
                alignItems: 'center',
                gap: '6px',
                flex: '1 1 auto',
                minWidth: '0',
                overflow: 'hidden',
                whiteSpace: 'nowrap'
            });

            const eventEntries = [];
            for (const event of events) {
                const entry = makeElement('span');
                Object.assign(entry.style, {
                    display: 'inline-flex',
                    alignItems: 'center',
                    gap: '4px',
                    flex: '0 0 auto',
                    minWidth: '0',
                    padding: '1px 3px 1px 6px',
                    border: '1px solid rgba(255,255,255,.72)',
                    borderRadius: '10px',
                    whiteSpace: 'nowrap'
                });

                const link = makeElement('a');
                link.href = event.url || '#';
                link.target = '_blank';
                link.rel = 'noopener noreferrer';
                link.textContent = event.message;
                Object.assign(link.style, {
                    color: '#fff',
                    textDecoration: 'none'
                });

                const accept = makeButton('', '✓');
                accept.title = 'Zaakceptuj tę zmianę';
                Object.assign(accept.style, {
                    width: '18px',
                    height: '18px',
                    padding: '0',
                    border: '0',
                    borderRadius: '8px',
                    background: 'rgba(255,255,255,.18)',
                    color: '#fff',
                    cursor: 'pointer',
                    fontWeight: '800'
                });
                accept.addEventListener('click', () => acknowledgeWatchEvent(event.id));
                entry.append(link, accept);
                list.appendChild(entry);
                eventEntries.push(entry);
            }

            const more = makeButton();
            more.title = 'Zobacz pozostałe powiadomienia';
            Object.assign(more.style, {
                display: 'none',
                flex: '0 0 auto',
                padding: '1px 7px',
                border: '1px solid rgba(255,255,255,.72)',
                borderRadius: '10px',
                background: 'rgba(255,255,255,.14)',
                color: '#fff',
                cursor: 'pointer',
                font: 'inherit',
                fontWeight: '800',
                whiteSpace: 'nowrap'
            });
            more.addEventListener('click', openWatchedSettingsModal);
            list.appendChild(more);
            bar.appendChild(list);

            const fitNotificationEntries = () => {
                if (!list.isConnected) return;
                for (const entry of eventEntries) entry.style.display = 'inline-flex';
                more.style.display = 'none';
                const available = list.clientWidth;
                const gap = 6;
                const allWidth = eventEntries.reduce(
                    (sum, entry, index) => sum + entry.offsetWidth + (index ? gap : 0),
                    0
                );
                if (allWidth <= available) return;

                more.textContent = `+${eventEntries.length}`;
                more.style.display = 'inline-flex';
                const reserve = more.offsetWidth + gap;
                let used = 0;
                let visible = 0;
                for (const entry of eventEntries) {
                    const needed = entry.offsetWidth + (visible ? gap : 0);
                    if (used + needed + reserve <= available) {
                        used += needed;
                        visible++;
                    } else {
                        entry.style.display = 'none';
                    }
                }
                const hidden = eventEntries.length - visible;
                more.textContent = `+${hidden}`;
            };

            const settingsButton = makeButton('', '⚙');
            settingsButton.title = 'Ustawienia skryptu';
            const closeButton = makeButton('', '×');
            closeButton.title = 'Zaakceptuj wszystkie zmiany';
            for (const button of [settingsButton, closeButton]) {
                Object.assign(button.style, {
                    flex: '0 0 auto',
                    width: '24px',
                    height: '24px',
                    padding: '0',
                    border: '1px solid rgba(255,255,255,.75)',
                    borderRadius: '50%',
                    background: 'rgba(255,255,255,.15)',
                    color: '#fff',
                    cursor: 'pointer',
                    fontSize: button === closeButton ? '18px' : '13px',
                    lineHeight: '1'
                });
            }
            settingsButton.title = 'Otwórz obserwowane';
            settingsButton.addEventListener('click', openWatchedSettingsModal);
            closeButton.addEventListener('click', () => {
                if (!window.confirm(
                    'Oznaczyć wszystkie powiadomienia o obserwowanych jako odczytane?'
                )) return;
                acknowledgeAllWatchEvents();
            });
            bar.append(settingsButton, closeButton);

            const spacer = makeElement('div');
            spacer.id = WATCH_BAR_SPACER_ID;
            spacer.setAttribute('aria-hidden', 'true');
            Object.assign(spacer.style, {
                display: 'block',
                width: '100%',
                height: '0px',
                flex: '0 0 auto',
                padding: '0',
                margin: '0',
                border: '0',
                pointerEvents: 'none',
                visibility: 'hidden'
            });
            document.body.insertBefore(spacer, document.body.firstChild);
            document.body.appendChild(bar);

            const syncSpacerHeight = () => {
                fitNotificationEntries();
                syncWatchBarPageOffset(bar, spacer);
            };
            if (typeof ResizeObserver === 'function') {
                const observer = new ResizeObserver(syncSpacerHeight);
                observer.observe(bar);
                bar._vmResizeObserver = observer;
            }
            const onWindowResize = () => requestAnimationFrame(syncSpacerHeight);
            window.addEventListener('resize', onWindowResize);
            bar._vmResizeCleanup = () => window.removeEventListener('resize', onWindowResize);
            requestAnimationFrame(syncSpacerHeight);
        };

        if (document.body) render();
        else document.addEventListener('DOMContentLoaded', render, { once: true });
    }

    function createWatchDialogBase(id, titleText) {
        document.getElementById(id)?.remove();
        const overlay = makeElement('div');
        overlay.id = id;
        Object.assign(overlay.style, {
            position: 'fixed',
            inset: '0',
            zIndex: '2147483647',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            padding: '16px',
            background: 'rgba(0,0,0,.58)'
        });
        const box = makeElement('div');
        Object.assign(box.style, {
            width: 'min(540px, calc(100vw - 32px))',
            maxHeight: '88vh',
            overflow: 'auto',
            boxSizing: 'border-box',
            padding: '18px',
            borderRadius: '10px',
            background: '#fff',
            color: '#222',
            boxShadow: '0 12px 45px rgba(0,0,0,.38)',
            fontFamily: 'Arial, sans-serif'
        });
        const title = makeElement('h2', '', titleText);
        Object.assign(title.style, {
            margin: '0 0 14px',
            color: '#222',
            fontSize: '19px'
        });
        box.appendChild(title);
        overlay.appendChild(box);
        document.body.appendChild(overlay);
        return { overlay, box };
    }

    function chooseWatchOptionDialog({ id, title, info, choices, primaryValue }) {
        return new Promise(resolve => {
            const { overlay, box } = createWatchDialogBase(id, title);
            const infoElement = makeElement('p', '', info);
            infoElement.style.margin = '0 0 14px';
            const actions = makeElement('div');
            Object.assign(actions.style, { display: 'grid', gap: '9px' });
            const close = value => {
                overlay.remove();
                resolve(value);
            };
            for (const [value, label] of choices) {
                const button = makeButton('', label);
                Object.assign(button.style, {
                    padding: '10px 14px',
                    border: `1px solid ${getEscortPagePinkColor()}`,
                    borderRadius: '7px',
                    background: value === primaryValue ? getEscortPagePinkColor() : '#fff',
                    color: value === primaryValue ? '#fff' : getEscortPagePinkColor(),
                    cursor: 'pointer',
                    fontWeight: '700'
                });
                button.addEventListener('click', () => close(value));
                actions.appendChild(button);
            }
            const cancel = makeButton('', 'Anuluj');
            Object.assign(cancel.style, {
                marginTop: '10px',
                border: '0',
                background: 'transparent',
                color: '#666',
                cursor: 'pointer'
            });
            cancel.addEventListener('click', () => close(null));
            overlay.addEventListener('click', event => {
                if (event.target !== overlay) return;
                close(null);
            });
            box.append(infoElement, actions, cancel);
        });
    }

    function chooseWatchEscortScope() {
        return chooseWatchOptionDialog({
            id: 'vm-garso-watch-scope-overlay',
            title: 'Co chcesz obserwować?',
            info: 'Wybierz zakres sprawdzania dla tego anonsu Escort.club.',
            choices: [
                ['single', 'Tylko ten anons'],
                ['person', 'Dowolne anons tej osoby']
            ],
            primaryValue: 'person'
        });
    }

    function chooseWatchPhoneCoverage(profileCount) {
        return chooseWatchOptionDialog({
            id: 'vm-garso-watch-phone-scope-overlay',
            title: 'Co chcesz dodać?',
            info: `Ten numer znaleziono w ${profileCount} profilach Escorti.`,
            choices: [
                ['single', 'Jeden wybrany profil'],
                ['all', `Wszystkie profile (${profileCount})`]
            ],
            primaryValue: 'all'
        });
    }

    function chooseWatchPhoneProfile(profileInfos) {
        return new Promise(resolve => {
            const { overlay, box } = createWatchDialogBase(
                'vm-garso-watch-phone-profile-overlay',
                'Wybierz profil Escorti'
            );
            const list = makeElement('div');
            Object.assign(list.style, {
                display: 'grid',
                gap: '7px',
                maxHeight: '55vh',
                overflow: 'auto'
            });
            for (const profile of profileInfos) {
                const button = makeButton();
                button.textContent = profile.profileName ||
                    `Profil Escorti ${getEscortiProfileId(profile.profileUrl)}`;
                Object.assign(button.style, {
                    padding: '9px 12px',
                    border: '1px solid #d7d7d7',
                    borderRadius: '7px',
                    background: '#fff',
                    color: '#222',
                    textAlign: 'left',
                    cursor: 'pointer',
                    fontWeight: '700'
                });
                button.addEventListener('click', () => {
                    overlay.remove();
                    resolve(profile);
                });
                list.appendChild(button);
            }
            const cancel = makeButton('', 'Anuluj');
            Object.assign(cancel.style, {
                marginTop: '10px',
                border: '0',
                background: 'transparent',
                color: '#666',
                cursor: 'pointer'
            });
            cancel.addEventListener('click', () => {
                overlay.remove();
                resolve(null);
            });
            overlay.addEventListener('click', event => {
                if (event.target !== overlay) return;
                overlay.remove();
                resolve(null);
            });
            box.append(list, cancel);
        });
    }

    function chooseWatchLocationTarget(current = null) {
        return new Promise(resolve => {
            const { overlay, box } = createWatchDialogBase(
                'vm-garso-watch-location-overlay',
                'Obserwuj zmianę lokalizacji'
            );
            const label = makeElement('label', '', 'Powiadom, gdy pojawi się w mieście:');
            Object.assign(label.style, {
                display: 'block',
                marginBottom: '6px',
                fontWeight: '700'
            });
            const input = makeElement('input');
            input.type = 'text';
            input.value = current?.type === 'city' ? current.city || '' : '';
            input.placeholder = 'np. Kraków';
            Object.assign(input.style, {
                width: '100%',
                boxSizing: 'border-box',
                padding: '9px 10px',
                border: '1px solid #bbb',
                borderRadius: '6px',
                fontSize: '14px'
            });
            const anyLabel = makeElement('label');
            Object.assign(anyLabel.style, {
                display: 'flex',
                alignItems: 'center',
                gap: '7px',
                marginTop: '10px',
                cursor: 'pointer'
            });
            const anyInput = makeElement('input');
            anyInput.type = 'checkbox';
            anyInput.checked = !current || current.type !== 'city';
            anyLabel.append(anyInput, document.createTextNode('Dowolne - zgłaszaj każdą zmianę lokalizacji'));
            const sync = () => {
                input.disabled = anyInput.checked;
                input.style.opacity = anyInput.checked ? '.55' : '1';
            };
            anyInput.addEventListener('change', sync);
            sync();

            const actions = makeElement('div');
            Object.assign(actions.style, {
                display: 'flex',
                justifyContent: 'flex-end',
                gap: '8px',
                marginTop: '16px'
            });
            const cancel = makeButton('', 'Anuluj');
            const save = makeButton('', 'Obserwuj');
            for (const button of [cancel, save]) {
                Object.assign(button.style, {
                    padding: '8px 14px',
                    borderRadius: '7px',
                    border: '1px solid #bbb',
                    cursor: 'pointer',
                    fontWeight: '700'
                });
            }
            Object.assign(save.style, {
                borderColor: getEscortPagePinkColor(),
                background: getEscortPagePinkColor(),
                color: '#fff'
            });
            cancel.addEventListener('click', () => {
                overlay.remove();
                resolve(null);
            });
            save.addEventListener('click', () => {
                const city = normalizeEscortCity(input.value);
                if (!anyInput.checked && !city) {
                    input.focus();
                    return;
                }
                overlay.remove();
                resolve(anyInput.checked
                    ? { type: 'any', city: '' }
                    : { type: 'city', city });
            });
            actions.append(cancel, save);
            box.append(label, input, anyLabel, actions);
            setTimeout(() => (anyInput.checked ? save : input).focus(), 0);
        });
    }

    async function buildWatchContextFromEscortAd(adUrl, scope = 'single') {
        const normalizedAdUrl = normalizeEscortiAdUrl(adUrl);
        const adId = parseAdIdFromUrl(normalizedAdUrl);
        if (!normalizedAdUrl || !adId) throw new Error('Nieprawidłowy adres anonsu Escort.club');

        let adData = null;
        if (getAdIdFromUrl() === adId) {
            adData = extractEscortClubAdData(document, adId, normalizedAdUrl);
            const phone = await resolveEscortPhoneForCurrentPage(adData.phoneId);
            if (phone) Object.assign(adData, phone);
        } else {
            const loaded = await getEscortAdData(adId, normalizedAdUrl, false);
            if (loaded?.status === 'ok') adData = loaded;
        }
        const title = adData?.title || `Anons ${adId}`;
        const imageUrl = getAdIdFromUrl() === adId
            ? extractWatchPageImage(document, normalizedAdUrl)
            : '';

        if (scope === 'single') {
            return {
                sourceType: 'escort-ad',
                adUrl: normalizedAdUrl,
                profileUrls: [],
                phoneDigits: adData?.phoneDigits || null,
                displayName: title,
                imageUrl,
                watchKey: `ad:${adId}`
            };
        }

        // Przy obserwowaniu całej osoby najpierw szukamy profilu Escorti
        // bezpośrednio po adresie anonsu. To działa również wtedy, gdy anons
        // Escort.club jest już offline i nie da się z niego odczytać telefonu.
        let addressProfileUrls = [];
        try {
            const foundByAddress = await searchEscortiProfilesDirect(normalizedAdUrl);
            addressProfileUrls = foundByAddress?.status === 'ok'
                ? (foundByAddress.profileUrls || [])
                : [];
        } catch (_) {}

        if (addressProfileUrls.length) {
            const profileInfos = (await watchMapLimit(
                addressProfileUrls,
                2,
                async profileUrl => {
                    try {
                        return await readEscortiProfileDirect(profileUrl);
                    } catch (_) {
                        return null;
                    }
                }
            )).filter(Boolean);

            // Jeżeli Escorti zwróci kilka profili, preferuj ten, w którym
            // rzeczywiście znajduje się wklejony numer anonsu.
            const exactProfileInfo = profileInfos.find(info =>
                (info?.adUrls || []).some(url => parseAdIdFromUrl(url) === adId)
            );
            const profileInfo = exactProfileInfo || profileInfos[0] || null;
            const profileUrl = profileInfo?.profileUrl || addressProfileUrls[0];
            const profileId = getEscortiProfileId(profileUrl);

            return {
                sourceType: 'escorti-profile',
                adUrl: normalizedAdUrl,
                profileUrls: [profileUrl],
                resolvedAdUrls: profileInfo?.adUrls || [],
                phoneDigits: null,
                displayName: profileInfo?.profileName ||
                    (profileId ? `Profil Escorti ${profileId}` : title),
                imageUrl: profileInfo?.imageUrl || imageUrl,
                watchKey: `profiles:${profileId || profileUrl}`
            };
        }

        // Fallback dla aktywnych anonsów: jeżeli wyszukiwanie Escorti po
        // adresie nic nie zwróciło, zachowaj dotychczasowe wyszukiwanie po
        // numerze telefonu.
        const phoneDigits = normalizeEscortCachedPhone(adData?.phoneDigits || adData?.phone);
        if (!phoneDigits) {
            throw new Error('Nie znaleziono profilu Escorti dla tego adresu anonsu');
        }
        const found = await searchEscortiProfilesDirect(phoneDigits);
        const profileUrls = found.status === 'ok' ? found.profileUrls || [] : [];
        let profileInfo = null;
        if (profileUrls[0]) {
            try {
                profileInfo = await readEscortiProfileDirect(profileUrls[0]);
            } catch (_) {}
        }
        return {
            sourceType: profileUrls.length ? 'escorti-profile' : 'phone',
            adUrl: normalizedAdUrl,
            profileUrls,
            phoneDigits,
            displayName: profileInfo?.profileName || title,
            imageUrl: profileInfo?.imageUrl || imageUrl,
            watchKey: profileUrls.length
                ? `profiles:${profileUrls.map(getEscortiProfileId).filter(Boolean).sort().join(',')}`
                : `phone:${phoneDigits}`
        };
    }

    function getCurrentEscortiWatchContext() {
        const profileUrl = normalizeWatchUrl(`${location.origin}${location.pathname}`);
        const info = readEscortiProfileInfo(document, profileUrl);
        return {
            sourceType: 'escorti-profile',
            profileUrls: profileUrl ? [profileUrl] : [],
            adUrl: null,
            resolvedAdUrls: info?.adUrls || [],
            phoneDigits: null,
            displayName: info?.profileName || extractWatchPageName(document, 'Profil Escorti'),
            imageUrl: info?.imageUrl || extractWatchPageImage(document, profileUrl),
            watchKey: `profiles:${getEscortiProfileId(profileUrl) || profileUrl}`
        };
    }

    async function addWatchedContext(context, mode, locationTarget = null) {
        const candidate = normalizeWatchedItem({
            id: 'watch-candidate',
            ...context,
            watchKey: getWatchKey(context),
            modes: { [mode]: true }
        });
        const existing = getWatchedItems().find(item =>
            watchedItemsOverlap(item, candidate)
        );
        const alreadyWatching = !!existing?.modes?.[mode];
        const item = upsertWatchedItem(
            context,
            { [mode]: true },
            locationTarget
        );
        showWatchToast(alreadyWatching
            ? 'Już obserwujesz tę pozycję.'
            : (mode === 'online'
                ? 'Dodano obserwowanie aktywności.'
                : 'Dodano obserwowanie lokalizacji.'));
        await runWatchedChecks({ force: true, itemIds: [item.id] });
        document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();
        return { item, alreadyWatching };
    }

    async function addWatchFromManualInput(rawValue) {
        const value = String(rawValue || '').trim();
        if (!value) throw new Error('Wklej adres profilu, anonsu albo numer telefonu');

        let url = null;
        try {
            url = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
        } catch (_) {}

        if (url) {
            if (
                ['escorti.pl', 'www.escorti.pl'].includes(url.hostname) &&
                /^\/escort\/\d+\/?$/i.test(url.pathname)
            ) {
                const profileUrl = normalizeWatchUrl(url.href);
                let info = null;
                try { info = await readEscortiProfileDirect(profileUrl); } catch (_) {}
                return addWatchedContext({
                    sourceType: 'escorti-profile',
                    profileUrls: [profileUrl],
                    displayName: info?.profileName || `Profil Escorti ${getEscortiProfileId(profileUrl)}`,
                    imageUrl: info?.imageUrl || '',
                    watchKey: `profiles:${getEscortiProfileId(profileUrl)}`
                }, 'online');
            }
            if (url.hostname === 'pl.escort.club' && /^\/anons\/\d+\.html\/?$/i.test(url.pathname)) {
                const scope = await chooseWatchEscortScope();
                if (!scope) return null;
                return addWatchedContext(
                    await buildWatchContextFromEscortAd(url.href, scope),
                    'online'
                );
            }
        }

        const phoneDigits = normalizeEscortCachedPhone(value);
        if (!phoneDigits || phoneDigits.length < 9) {
            throw new Error('Nie rozpoznano adresu ani numeru telefonu');
        }
        let profileUrls = [];
        try {
            const found = await searchEscortiProfilesDirect(phoneDigits);
            profileUrls = found.profileUrls || [];
        } catch (_) {}

        const profileInfos = (await watchMapLimit(profileUrls, 2, async profileUrl => {
            try {
                const info = await readEscortiProfileDirect(profileUrl);
                return {
                    profileUrl,
                    profileName: normalizeWatchedDisplayName(
                        info?.profileName,
                        `Profil Escorti ${getEscortiProfileId(profileUrl)}`
                    ),
                    imageUrl: info?.imageUrl || ''
                };
            } catch (_) {
                return {
                    profileUrl,
                    profileName: `Profil Escorti ${getEscortiProfileId(profileUrl)}`,
                    imageUrl: ''
                };
            }
        })).filter(Boolean);

        let selectedProfiles = profileInfos;
        let coverage = profileInfos.length > 1
            ? await chooseWatchPhoneCoverage(profileInfos.length)
            : 'single';
        if (!coverage) return null;
        if (coverage === 'single' && profileInfos.length > 1) {
            const selected = await chooseWatchPhoneProfile(profileInfos);
            if (!selected) return null;
            selectedProfiles = [selected];
        }

        const selectedProfileUrls = selectedProfiles.map(profile => profile.profileUrl);
        const formattedPhone = phoneDigits.replace(/(\d{3})(?=\d)/g, '$1 ');
        const selectedName = selectedProfiles.length === 1
            ? selectedProfiles[0].profileName
            : (selectedProfiles.length
                ? `Wszystkie profile numeru ${formattedPhone}`
                : `Telefon ${formattedPhone}`);
        return addWatchedContext({
            sourceType: selectedProfileUrls.length ? 'escorti-profile' : 'phone',
            profileUrls: selectedProfileUrls,
            // Przy wyborze jednego profilu obserwujemy tylko ten profil.
            // Numer pozostaje kluczem dopiero przy wyborze wszystkich profili
            // albo gdy Escorti nie zwróci żadnego profilu.
            phoneDigits: coverage === 'all' || !selectedProfileUrls.length
                ? phoneDigits
                : null,
            displayName: selectedName || `Telefon ${formattedPhone}`,
            imageUrl: selectedProfiles[0]?.imageUrl || '',
            watchKey: selectedProfileUrls.length
                ? `profiles:${selectedProfileUrls.map(getEscortiProfileId).filter(Boolean).sort().join(',')}`
                : `phone:${phoneDigits}`
        }, 'online');
    }

    function createWatchActionButton(label, primary = false) {
        const button = makeButton('', label);
        Object.assign(button.style, {
            padding: '8px 12px',
            border: `1px solid ${getEscortPagePinkColor()}`,
            borderRadius: '7px',
            background: primary ? getEscortPagePinkColor() : 'transparent',
            color: primary ? '#fff' : getEscortPagePinkColor(),
            cursor: 'pointer',
            fontSize: '12px',
            fontWeight: '700',
            lineHeight: '1.2'
        });
        return button;
    }

    function getWatchedMatchesForEscortiProfile(mode) {
        const context = getCurrentEscortiWatchContext();
        const current = normalizeWatchedItem({
            id: 'current-escorti-profile',
            ...context,
            watchKey: getWatchKey(context),
            modes: {}
        });
        return getWatchedItems().filter(item =>
            !!item.modes?.[mode] && watchedItemsOverlap(item, current)
        );
    }

    function getWatchedMatchesForCurrentEscortAd(mode) {
        const adId = getAdIdFromUrl();
        if (!adId) return [];

        return getWatchedItems().filter(item =>
            item.sourceType === 'escort-ad' &&
            !!item.modes?.[mode] &&
            parseAdIdFromUrl(item.adUrl) === adId
        );
    }

    function setWatchActionLabel(control, label) {
        const labelNode = control.querySelector(
            '[data-vm-watch-label], .sub-label, .btn-label, [class*="label"]'
        );
        if (labelNode) labelNode.textContent = label;
        else control.textContent = label;
    }

    function bindWatchPageAction(control, {
        mode,
        idleLabel,
        watchedLabel,
        getMatches,
        addWatch
    }) {
        const refresh = () => {
            const watched = getMatches().length > 0;
            setWatchActionLabel(control, watched ? watchedLabel : idleLabel);
            control.dataset.vmWatchEnabled = watched ? '1' : '0';
            control.setAttribute('aria-pressed', watched ? 'true' : 'false');
            control.title = watched
                ? 'Kliknij, aby usunąć to obserwowanie'
                : idleLabel;
        };

        control.addEventListener('click', async event => {
            event.preventDefault();
            event.stopPropagation();
            if (control.dataset.vmWatchBusy === '1') return;

            const watchedItems = getMatches();
            if (watchedItems.length) {
                const modeText = mode === 'online'
                    ? 'aktywności online'
                    : 'zmian lokalizacji';
                const names = [...new Set(watchedItems.map(item => item.displayName))]
                    .slice(0, 3)
                    .join(', ');
                if (!window.confirm(
                    `Czy wyłączyć obserwowanie ${modeText}${names ? ` dla „${names}”` : ''}?`
                )) return;
                disableWatchedMode(watchedItems, mode);
                showWatchToast('Usunięto obserwowanie.');
                refresh();
                return;
            }

            control.dataset.vmWatchBusy = '1';
            control.style.pointerEvents = 'none';
            control.style.opacity = '.62';
            try {
                await addWatch();
            } catch (error) {
                showWatchToast(error?.message || 'Nie udało się zmienić obserwowania', true);
            } finally {
                delete control.dataset.vmWatchBusy;
                control.style.pointerEvents = '';
                control.style.opacity = '';
                refresh();
            }
        });

        watchPageButtonRefreshers.add(refresh);
        refresh();
    }

    function createEscortClubNativeWatchAction(id, label, iconClass) {
        const control = makeElement('a');
        control.id = id;
        control.href = '#';
        control.className = 'action-link vm-watch-native-action';
        control.rel = 'nofollow';

        const iconHolder = makeElement('span', 'icon-holder');
        iconHolder.style.position = 'relative';
        const icon = makeElement('i', `fas ${iconClass}`);
        Object.assign(icon.style, {
            position: 'absolute',
            top: '30%',
            left: '0'
        });
        iconHolder.appendChild(icon);

        const text = makeElement('span', 'sub-label');
        text.dataset.vmWatchLabel = '1';
        text.textContent = label;
        control.append(iconHolder, text);
        return control;
    }

    function findEscortiNativeActionsRow() {
        const actions = [...document.querySelectorAll('a, button')]
            .filter(element => !element.closest('[id^="vm-"]'));
        const follow = actions.find(element =>
            /\bobserwuj/.test(normalizeText(element.textContent))
        );
        const review = actions.find(element =>
            /dodaj\s+opini/.test(normalizeText(element.textContent))
        );

        if (follow && review) {
            let candidate = follow.parentElement;
            for (let level = 0; candidate && candidate !== document.body && level < 6; level++) {
                if (
                    candidate.contains(review) &&
                    candidate.querySelectorAll('a, button').length <= 12
                ) {
                    return { row: candidate, template: follow };
                }
                candidate = candidate.parentElement;
            }
        }

        const template = follow || review;
        const row = template?.closest(
            '.profile-actions, .actions, .action-buttons, .btn-group, .buttons, .d-flex'
        ) || template?.parentElement;
        return row && template ? { row, template } : null;
    }

    function createEscortiNativeWatchAction(template, id, label) {
        let control;
        if (template) {
            control = template.cloneNode(true);
            for (const attribute of [...control.attributes]) {
                if (!['class', 'style'].includes(attribute.name)) {
                    control.removeAttribute(attribute.name);
                }
            }
            control.querySelectorAll('[id]').forEach(element => element.removeAttribute('id'));
            control.querySelectorAll('[href], [onclick]').forEach(element => {
                element.removeAttribute('href');
                element.removeAttribute('onclick');
            });
            if (control.tagName === 'A') control.href = '#';
            if (control.tagName === 'BUTTON') control.type = 'button';
        } else {
            control = createWatchActionButton(label);
        }

        control.id = id;
        control.classList.add('vm-watch-native-action');
        let labelNode = control.querySelector(
            '.sub-label, .btn-label, [class*="label"], span:last-child'
        );
        if (!labelNode) {
            control.replaceChildren();
            labelNode = makeElement('span');
            control.appendChild(labelNode);
        }
        labelNode.dataset.vmWatchLabel = '1';
        labelNode.textContent = label;
        return control;
    }

    function appendEscortiNativeAction(row, template, control) {
        const wrapper = template?.parentElement;
        if (
            wrapper && wrapper.parentElement === row &&
            ['DIV', 'LI', 'SPAN'].includes(wrapper.tagName)
        ) {
            const wrapperClone = wrapper.cloneNode(false);
            wrapperClone.removeAttribute('id');
            wrapperClone.dataset.vmWatchWrapper = '1';
            wrapperClone.appendChild(control);
            row.appendChild(wrapperClone);
            return;
        }
        row.appendChild(control);
    }

    function initEscortiProfileWatchButtons() {
        if (!getWatchSettings().enabled) return;
        if (!/^\/escort\/\d+\/?$/i.test(location.pathname)) return;
        if (new URL(location.href).searchParams.has('vm_mode')) return;

        const insert = () => {
            if (!getWatchSettings().enabled) return true;
            if (document.getElementById('vm-escorti-watch-online')) return true;
            const nativeActions = findEscortiNativeActionsRow();
            if (!nativeActions) return false;
            const { row, template } = nativeActions;
            const online = createEscortiNativeWatchAction(
                template,
                'vm-escorti-watch-online',
                'Obserwuj czy online'
            );
            const locationButton = createEscortiNativeWatchAction(
                template,
                'vm-escorti-watch-location',
                'Obserwuj zmianę lokalizacji'
            );
            appendEscortiNativeAction(row, template, online);
            appendEscortiNativeAction(row, template, locationButton);

            bindWatchPageAction(online, {
                mode: 'online',
                idleLabel: 'Obserwuj czy online',
                watchedLabel: 'Już obserwowane (online)',
                getMatches: () => getWatchedMatchesForEscortiProfile('online'),
                addWatch: () => addWatchedContext(getCurrentEscortiWatchContext(), 'online')
            });
            bindWatchPageAction(locationButton, {
                mode: 'location',
                idleLabel: 'Obserwuj zmianę lokalizacji',
                watchedLabel: 'Już obserwowane (lokalizacja)',
                getMatches: () => getWatchedMatchesForEscortiProfile('location'),
                addWatch: async () => {
                    const target = await chooseWatchLocationTarget();
                    if (target) {
                        await addWatchedContext(
                            getCurrentEscortiWatchContext(),
                            'location',
                            target
                        );
                    }
                }
            });
            return true;
        };
        if (insert()) return;
        let attempts = 0;
        const timer = setInterval(() => {
            if (insert() || ++attempts >= 40) clearInterval(timer);
        }, 250);
    }

    function initEscortiProfileAdLinkStatuses() {
        if (!/^\/escort\/\d+\/?$/i.test(location.pathname)) return;
        if (new URL(location.href).searchParams.has('vm_mode')) return;

        const statusByUrl = new Map();
        const pendingUrls = new Set();

        const showResultOnMarker = (marker, result) => {
            if (!marker) return;
            if (result?.unknown || !result) {
                marker.textContent = '?';
                marker.style.color = '#d97706';
                marker.title = 'Nie udało się sprawdzić aktywności';
            } else if (result.active) {
                marker.textContent = '✓';
                marker.style.color = '#16a34a';
                marker.title = 'Anons online';
            } else {
                marker.textContent = '×';
                marker.style.color = '#dc2626';
                marker.title = 'Anons offline';
            }
            marker.setAttribute('aria-label', marker.title);
            marker.dataset.vmStatusReady = '1';
        };

        const showResultForUrl = (adUrl, result) => {
            if (!adUrl) return;
            statusByUrl.set(adUrl, result || null);
            for (const marker of document.querySelectorAll(
                '.vm-escorti-ad-online-status'
            )) {
                if (marker.dataset.adUrl === adUrl) {
                    showResultOnMarker(marker, result);
                }
            }
        };

        const checkRows = rows => {
            const urls = [...new Set(
                rows
                    .map(row => row.adUrl)
                    .filter(adUrl => !statusByUrl.has(adUrl) && !pendingUrls.has(adUrl))
            )];
            if (!urls.length) return;
            urls.forEach(adUrl => pendingUrls.add(adUrl));

            // Wynik każdego adresu pokazujemy od razu. Mapa statusów pozwala
            // odtworzyć znaczniki, jeśli dynamiczny frontend Escorti podmieni
            // sekcję linków już po rozpoczęciu sprawdzania.
            // Do znaczników ✓/× potrzebujemy wyłącznie informacji online/offline.
            // Użyj lekkich sond HEAD zamiast pełnego pobierania HTML. Sondy mają
            // osobny limit 12 równoległych połączeń, więc długie listy linków
            // na profilu Escorti są sprawdzane znacznie szybciej.
            scanActiveEscortAds(urls, progress => {
                const resultUrl = normalizeEscortiAdUrl(progress?.result?.url);
                if (resultUrl) showResultForUrl(resultUrl, progress.result);
            }, true, { activityOnly: true })
                .then(summary => {
                    const byUrl = new Map(
                        (summary?.results || []).map(result => [
                            normalizeEscortiAdUrl(result?.url) || result?.url,
                            result
                        ])
                    );
                    for (const adUrl of urls) {
                        if (!statusByUrl.has(adUrl)) {
                            showResultForUrl(adUrl, byUrl.get(adUrl));
                        }
                    }
                })
                .catch(() => {
                    for (const adUrl of urls) {
                        if (!statusByUrl.has(adUrl)) showResultForUrl(adUrl, null);
                    }
                })
                .finally(() => {
                    urls.forEach(adUrl => pendingUrls.delete(adUrl));
                });
        };

        const insert = () => {
            // Escorti używa obecnie także nagłówka „Linki do ogłoszeń”.
            // normalizeText() nie usuwa polskich znaków, więc wcześniejsze
            // „linki do ogloszen” nie pasowało do „linki do ogłoszeń”.
            const heading = [...document.querySelectorAll('h1,h2,h3,h4,h5,h6')]
                .find(element => /linki do anonsów|linki do ogłoszeń|linki do ogloszen/.test(
                    normalizeText(element.textContent)
                ));
            const section = heading?.parentElement || document;

            const rows = [];
            for (const anchor of section.querySelectorAll('a[href]')) {
                // Nie dotykaj linków dodanych przez sam skrypt (np. pasek
                // powiadomień obserwowanych). Fallback do całego dokumentu ma
                // służyć wyłącznie znalezieniu natywnych linków profilu Escorti.
                if (anchor.closest('[id^="vm-"], [class*="vm-"]')) continue;
                const adUrl = normalizeEscortiAdUrl(anchor.getAttribute('href'), location.href);
                if (!adUrl) continue;

                let marker = [...(anchor.parentElement?.children || [])].find(element =>
                    element.classList?.contains('vm-escorti-ad-online-status') &&
                    element.dataset.adUrl === adUrl
                );
                if (!marker) {
                    const row = makeElement('div', 'vm-escorti-ad-status-row');
                    Object.assign(row.style, {
                        display: 'flex',
                        alignItems: 'center',
                        gap: '7px',
                        width: 'fit-content',
                        minWidth: '0',
                        maxWidth: '100%',
                        margin: '0 auto',
                        verticalAlign: 'middle'
                    });

                    marker = makeElement('span');
                    marker.className = 'vm-escorti-ad-online-status';
                    marker.dataset.adUrl = adUrl;
                    marker.textContent = '…';
                    marker.title = 'Sprawdzanie aktywności anonsu';
                    marker.setAttribute('aria-label', 'Sprawdzanie aktywności anonsu');
                    Object.assign(marker.style, {
                        flex: '0 0 18px',
                        width: '18px',
                        textAlign: 'center',
                        color: '#9ca3af',
                        fontSize: '18px',
                        fontWeight: '900',
                        lineHeight: '1'
                    });

                    anchor.parentElement.insertBefore(row, anchor);
                    row.append(anchor, marker);
                    Object.assign(anchor.style, {
                        flex: '0 1 auto',
                        width: 'auto',
                        minWidth: '0'
                    });
                }
                rows.push({ adUrl, marker });
            }

            if (!rows.length) return false;

            for (const row of rows) {
                if (statusByUrl.has(row.adUrl)) {
                    showResultOnMarker(row.marker, statusByUrl.get(row.adUrl));
                }
            }
            checkRows(rows);
            return true;
        };

        const scheduleInsert = createAnimationFrameScheduler(insert);

        const observer = new MutationObserver(scheduleInsert);
        const startObserver = () => {
            if (!document.body) return false;
            observer.observe(document.body, { childList: true, subtree: true });
            return true;
        };

        if (document.body) {
            startObserver();
            insert();
        } else {
            document.addEventListener('DOMContentLoaded', () => {
                startObserver();
                insert();
            }, { once: true });
        }

        let attempts = 0;
        const timer = setInterval(() => {
            if (insert() || ++attempts >= 40) clearInterval(timer);
        }, 250);
    }

    function initEscortClubWatchButtons() {
        if (!getWatchSettings().enabled) return;
        if (!/^\/anons\/\d+\.html\/?$/i.test(location.pathname)) return;
        const insert = () => {
            if (!getWatchSettings().enabled) return true;
            if (document.getElementById('vm-escort-watch-online')) return true;
            const row = document.querySelector('.content-actions-col');
            if (!row) return false;
            const online = createEscortClubNativeWatchAction(
                'vm-escort-watch-online',
                'Obserwuj czy online',
                'fa-bell'
            );
            const locationButton = createEscortClubNativeWatchAction(
                'vm-escort-watch-location',
                'Obserwuj zmianę lokalizacji',
                'fa-map-marker-alt'
            );

            const getContext = async () => {
                const scope = await chooseWatchEscortScope();
                if (!scope) return null;
                return buildWatchContextFromEscortAd(location.href, scope);
            };

            row.append(online, locationButton);
            bindWatchPageAction(online, {
                mode: 'online',
                idleLabel: 'Obserwuj czy online',
                watchedLabel: 'Już obserwowane (online)',
                getMatches: () => getWatchedMatchesForCurrentEscortAd('online'),
                addWatch: async () => {
                    const context = await getContext();
                    if (context) await addWatchedContext(context, 'online');
                }
            });
            bindWatchPageAction(locationButton, {
                mode: 'location',
                idleLabel: 'Obserwuj zmianę lokalizacji',
                watchedLabel: 'Już obserwowane (lokalizacja)',
                getMatches: () => getWatchedMatchesForCurrentEscortAd('location'),
                addWatch: async () => {
                    const context = await getContext();
                    if (!context) return;
                    const target = await chooseWatchLocationTarget();
                    if (target) await addWatchedContext(context, 'location', target);
                }
            });
            return true;
        };
        if (insert()) return;
        let attempts = 0;
        const timer = setInterval(() => {
            if (insert() || ++attempts >= 40) clearInterval(timer);
        }, 250);
    }

    function openWatchedSettingsModal() {
        if (document.getElementById(WATCH_SETTINGS_OVERLAY_ID)) return;
        const { overlay, box } = createWatchDialogBase(
            WATCH_SETTINGS_OVERLAY_ID,
            'Obserwowane'
        );
        Object.assign(box.style, {
            width: 'calc(100vw - 24px)',
            height: 'calc(100vh - 24px)',
            maxWidth: 'none',
            maxHeight: 'calc(100vh - 24px)',
            minHeight: '0',
            display: 'flex',
            flexDirection: 'column',
            padding: '0',
            overflow: 'hidden',
            boxSizing: 'border-box'
        });
        box.firstElementChild?.remove();

        const header = makeElement('div');
        Object.assign(header.style, {
            display: 'flex',
            alignItems: 'center',
            gap: '12px',
            padding: '9px 14px',
            borderBottom: '1px solid #e6e6e6',
            flex: '0 0 auto'
        });
        const title = makeElement('h2', '', 'Obserwowane');
        Object.assign(title.style, {
            flex: '1 1 auto',
            margin: '0',
            fontSize: '18px',
            color: '#222'
        });
        const close = makeButton('', '×');
        Object.assign(close.style, {
            width: '28px',
            height: '28px',
            padding: '0',
            border: '0',
            borderRadius: '50%',
            background: '#f1f1f1',
            color: '#333',
            cursor: 'pointer',
            fontSize: '22px'
        });
        close.addEventListener('click', () => overlay.remove());
        header.append(title, close);

        const intervalLabel = makeElement('label', '', 'Sprawdzaj co:');
        Object.assign(intervalLabel.style, {
            fontSize: '11px',
            fontWeight: '700',
            whiteSpace: 'nowrap'
        });
        const interval = makeElement('select');
        appendSelectOptions(interval, WATCH_INTERVAL_SELECT_OPTIONS);
        interval.value = String(getWatchSettings().intervalMinutes);
        Object.assign(interval.style, {
            height: '27px',
            padding: '2px 25px 2px 7px',
            border: `1px solid ${getEscortPagePinkColor()}`,
            borderRadius: '6px',
            background: '#fff',
            width: '98px',
            minWidth: '98px',
            flex: '0 0 98px',
            fontSize: '11px'
        });
        const refresh = makeButton('', 'Sprawdź teraz wszystkie');
        Object.assign(refresh.style, {
            height: '27px',
            padding: '2px 9px',
            border: '0',
            borderRadius: '6px',
            background: getEscortPagePinkColor(),
            color: '#fff',
            cursor: 'pointer',
            fontSize: '10px',
            fontWeight: '700',
            whiteSpace: 'nowrap'
        });

        const addRow = makeElement('div');
        Object.assign(addRow.style, {
            display: 'flex',
            gap: '8px',
            padding: '7px 14px',
            borderBottom: '1px solid #e6e6e6',
            flex: '0 0 auto'
        });
        const manualInput = makeElement('input');
        manualInput.type = 'text';
        manualInput.placeholder = 'Adres profilu Escorti, anonsu Escort.club albo numer telefonu';
        Object.assign(manualInput.style, {
            flex: '1 1 auto',
            minWidth: '0',
            padding: '7px 9px',
            border: '1px solid #bbb',
            borderRadius: '6px'
        });
        const addButton = makeButton('', 'Dodaj');
        Object.assign(addButton.style, {
            padding: '6px 13px',
            border: `1px solid ${getEscortPagePinkColor()}`,
            borderRadius: '7px',
            background: '#fff',
            color: getEscortPagePinkColor(),
            cursor: 'pointer',
            fontWeight: '700'
        });
        addRow.append(manualInput, addButton);

        const status = makeElement('div');
        Object.assign(status.style, {
            display: 'none',
            padding: '4px 14px',
            background: '#fff5f9',
            color: getEscortPagePinkColor(),
            fontSize: '12px',
            fontWeight: '700',
            flex: '0 0 auto'
        });

        const body = makeElement('div');
        Object.assign(body.style, {
            flex: '1 1 auto',
            minHeight: '0',
            maxHeight: 'none',
            overflow: 'auto',
            overscrollBehavior: 'contain',
            padding: '8px 14px 12px',
            boxSizing: 'border-box'
        });
        const itemsHost = makeElement('div');
        itemsHost.style.minWidth = '1180px';
        body.append(itemsHost);
        box.append(header, addRow, status, body);

        const render = () => {
            const items = getWatchedItems();
            const events = getWatchEvents();
            itemsHost.replaceChildren();
            const headingRow = makeElement('div');
            Object.assign(headingRow.style, {
                display: 'flex',
                alignItems: 'center',
                gap: '7px',
                margin: '0 0 7px'
            });
            const heading = makeElement('h3', '', `Obserwowani (${items.length})`);
            Object.assign(heading.style, {
                flex: '1 1 auto',
                margin: '0',
                fontSize: '15px',
                color: '#222'
            });
            const acceptAll = makeButton('', 'Zaakceptuj wszystkie');
            acceptAll.disabled = !events.some(event => !event.acknowledged);
            const clearHistory = makeButton('', 'Usuń historię zmian');
            clearHistory.disabled = !events.length;
            const removeAll = makeButton('', 'Usuń wszystkie');
            removeAll.disabled = !items.length;
            for (const button of [acceptAll, clearHistory, removeAll]) {
                Object.assign(button.style, {
                    height: '27px',
                    padding: '2px 8px',
                    border: `1px solid ${getEscortPagePinkColor()}`,
                    borderRadius: '6px',
                    background: '#fff',
                    color: getEscortPagePinkColor(),
                    cursor: button.disabled ? 'default' : 'pointer',
                    opacity: button.disabled ? '.45' : '1',
                    fontSize: '10px',
                    fontWeight: '700'
                });
            }
            Object.assign(removeAll.style, {
                borderColor: '#c62828',
                color: '#c62828'
            });
            Object.assign(clearHistory.style, {
                borderColor: '#777',
                color: '#555'
            });
            acceptAll.addEventListener('click', () => {
                if (acceptAll.disabled) return;
                acknowledgeAllWatchEvents();
            });
            clearHistory.addEventListener('click', () => {
                if (clearHistory.disabled) return;
                if (!window.confirm(
                    'Usunąć całą historię zmian? Obserwowane pozycje pozostaną na liście.'
                )) return;
                saveWatchEvents([]);
                renderWatchNotificationBar();
                render();
            });
            removeAll.addEventListener('click', () => {
                if (removeAll.disabled) return;
                if (!window.confirm(
                    'Usunąć wszystkie obserwowane pozycje wraz z ich historią zmian?'
                )) return;
                saveWatchedItems([]);
                saveWatchEvents([]);
                renderWatchNotificationBar();
                render();
            });
            headingRow.append(
                heading,
                intervalLabel,
                interval,
                refresh,
                acceptAll,
                clearHistory,
                removeAll
            );
            itemsHost.appendChild(headingRow);

            if (!items.length) {
                const empty = makeElement('div', '', 'Nie dodano jeszcze żadnego profilu ani anonsu.');
                Object.assign(empty.style, {
                    padding: '18px',
                    border: '1px dashed #ccc',
                    borderRadius: '8px',
                    color: '#777',
                    textAlign: 'center'
                });
                itemsHost.appendChild(empty);
            }

            const listGridColumns =
                '44px minmax(155px,1.05fr) minmax(225px,1.35fr) minmax(250px,1.45fr) minmax(420px,2.8fr) 68px';
            if (items.length) {
                const listHeader = makeElement('div');
                Object.assign(listHeader.style, {
                    display: 'grid',
                    gridTemplateColumns: listGridColumns,
                    gap: '6px',
                    alignItems: 'center',
                    padding: '4px 7px',
                    border: '1px solid #dedede',
                    borderRadius: '7px 7px 0 0',
                    background: '#f5f5f5',
                    color: '#666',
                    fontSize: '10px',
                    fontWeight: '700',
                    textTransform: 'uppercase',
                    letterSpacing: '.02em'
                });
                for (const label of ['', 'Profil', 'Obserwowanie', 'Stan', 'Zmiany', 'Akcje']) {
                    const cell = makeElement('span', '', label);
                    listHeader.appendChild(cell);
                }
                itemsHost.appendChild(listHeader);
            }

            for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
                const item = items[itemIndex];
                const itemEvents = events.filter(event => event.itemId === item.id);
                const hasUnacknowledgedChange = itemEvents.some(
                    event => !event.acknowledged
                );
                const card = makeElement('div');
                Object.assign(card.style, {
                    display: 'grid',
                    gridTemplateColumns: listGridColumns,
                    gap: '6px',
                    alignItems: 'center',
                    margin: '0',
                    padding: '4px 7px',
                    borderLeft: '1px solid #e2e2e2',
                    borderRight: '1px solid #e2e2e2',
                    borderBottom: '1px solid #e2e2e2',
                    borderRadius: itemIndex === items.length - 1 ? '0 0 7px 7px' : '0',
                    background: '#fff'
                });
                const imageBox = makeElement('div');
                Object.assign(imageBox.style, {
                    width: '44px',
                    height: '44px',
                    borderRadius: '6px',
                    background: '#f2f2f2',
                    overflow: 'hidden',
                    position: 'relative',
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    color: '#bbb',
                    fontSize: '16px'
                });
                imageBox.textContent = '-';
                if (item.imageUrl) {
                    const image = makeElement('img');
                    image.src = item.imageUrl;
                    image.alt = '';
                    image.loading = 'lazy';
                    image.referrerPolicy = 'no-referrer';
                    Object.assign(image.style, {
                        position: 'absolute',
                        inset: '0',
                        width: '100%',
                        height: '100%',
                        objectFit: 'cover'
                    });
                    image.addEventListener('error', () => image.remove(), { once: true });
                    imageBox.appendChild(image);
                }

                const details = makeElement('div');
                details.style.minWidth = '0';
                const nameRow = makeElement('div');
                Object.assign(nameRow.style, {
                    display: 'flex',
                    alignItems: 'center',
                    gap: '5px',
                    minWidth: '0'
                });
                const name = makeElement('a');
                name.href = item.profileUrls?.[0] || item.adUrl || '#';
                name.target = '_blank';
                name.rel = 'noopener noreferrer';
                name.textContent = item.displayName;
                Object.assign(name.style, {
                    minWidth: '0',
                    overflow: 'hidden',
                    textOverflow: 'ellipsis',
                    whiteSpace: 'nowrap',
                    color: hasUnacknowledgedChange
                        ? getEscortPagePinkColor()
                        : '#777',
                    fontWeight: hasUnacknowledgedChange ? '800' : '400',
                    fontSize: '13px',
                    textDecoration: 'none'
                });
                const rename = makeButton('', '✎');
                rename.title = 'Zmień nazwę obserwowanego';
                rename.setAttribute('aria-label', rename.title);
                Object.assign(rename.style, {
                    flex: '0 0 auto',
                    width: '20px',
                    height: '20px',
                    padding: '0',
                    border: '1px solid #ccc',
                    borderRadius: '5px',
                    background: item.customDisplayName ? '#fff2f8' : '#f7f7f7',
                    color: item.customDisplayName ? getEscortPagePinkColor() : '#666',
                    cursor: 'pointer',
                    fontSize: '12px',
                    lineHeight: '18px'
                });
                rename.addEventListener('click', () => {
                    const entered = window.prompt(
                        'Wpisz nową nazwę obserwowanego profilu lub anonsu:',
                        item.displayName
                    );
                    if (entered == null) return;
                    const newName = normalizeEscortAdText(entered).slice(0, 120);
                    if (!newName) {
                        window.alert('Nazwa nie może być pusta.');
                        return;
                    }
                    if (newName === item.displayName) return;
                    renameWatchedItem(item.id, newName);
                    render();
                });
                nameRow.append(name, rename);
                const source = makeElement('div', '', watchItemSourceText(item));
                Object.assign(source.style, {
                    marginTop: '1px',
                    color: '#777',
                    fontSize: '10px',
                    lineHeight: '1.2'
                });

                const modes = makeElement('div');
                Object.assign(modes.style, {
                    display: 'flex',
                    flexWrap: 'wrap',
                    alignItems: 'center',
                    gap: '5px 9px',
                    margin: '0',
                    fontSize: '11px'
                });
                const onlineLabel = makeElement('label');
                onlineLabel.style.whiteSpace = 'nowrap';
                const onlineInput = makeElement('input');
                onlineInput.type = 'checkbox';
                onlineInput.checked = item.modes.online;
                onlineLabel.append(onlineInput, document.createTextNode(' online'));
                const locationLabel = makeElement('label');
                locationLabel.style.whiteSpace = 'nowrap';
                const locationInput = makeElement('input');
                locationInput.type = 'checkbox';
                locationInput.checked = item.modes.location;
                const locationTargetText = item.locationTarget?.type === 'city'
                    ? item.locationTarget.city
                    : 'dowolna';
                locationLabel.append(
                    locationInput,
                    document.createTextNode(' lokalizacja →')
                );
                const locationConfig = makeButton('', locationTargetText);
                Object.assign(locationConfig.style, {
                    padding: '0',
                    border: '0',
                    borderRadius: '0',
                    background: 'transparent',
                    color: '#444',
                    cursor: 'pointer',
                    font: 'inherit',
                    textDecoration: 'underline',
                    textUnderlineOffset: '2px',
                    whiteSpace: 'nowrap'
                });
                const locationGroup = makeElement('span');
                Object.assign(locationGroup.style, {
                    display: 'inline-flex',
                    alignItems: 'center',
                    gap: '3px',
                    whiteSpace: 'nowrap'
                });
                locationGroup.append(locationLabel, locationConfig);
                modes.append(onlineLabel, locationGroup);

                const result = makeElement('div');
                const onlineText = item.state?.online === true
                    ? 'online'
                    : (item.state?.online === false ? 'offline' : 'brak danych');
                Object.assign(result.style, {
                    display: 'grid',
                    gap: '2px',
                    margin: '0',
                    color: '#444',
                    fontSize: '10.5px',
                    lineHeight: '1.3'
                });
                const stateLine = makeElement('div');
                stateLine.appendChild(document.createTextNode('Stan: '));
                const onlineState = makeElement('strong', '', onlineText);
                onlineState.style.color = item.state?.online === true
                    ? '#16823a'
                    : (item.state?.online === false ? '#c62828' : '#777');
                stateLine.append(
                    onlineState,
                    document.createTextNode(
                        ` • aktywne ${item.state?.activeCount || 0}/${item.state?.totalAds || 0}` +
                        ` • lokalizacja: ${watchCitiesText(item.state?.cities)}`
                    )
                );
                const checkedLine = makeElement('div', '', `Ostatnio sprawdzono: ${formatWatchDateTime(item.lastCheckedAt)}`);
                checkedLine.style.color = item.lastError ? '#c62828' : '#777';
                if (item.lastError) checkedLine.textContent += ` • ${item.lastError}`;
                result.append(stateLine, checkedLine);
                details.append(nameRow, source);

                const changes = makeElement('div');
                Object.assign(changes.style, {
                    display: 'grid',
                    gap: '2px',
                    minWidth: '0',
                    color: '#444',
                    fontSize: '10px',
                    lineHeight: '1.2'
                });
                if (!itemEvents.length) {
                    const noChanges = makeElement('span', '', '-');
                    noChanges.style.color = '#aaa';
                    changes.appendChild(noChanges);
                }
                for (const watchEvent of itemEvents) {
                    const changeLine = makeElement('div');
                    Object.assign(changeLine.style, {
                        display: 'flex',
                        alignItems: 'center',
                        gap: '4px',
                        minWidth: '0',
                        opacity: watchEvent.acknowledged ? '.55' : '1'
                    });
                    const changeLink = makeElement('a');
                    changeLink.href = watchEvent.url || '#';
                    changeLink.target = '_blank';
                    changeLink.rel = 'noopener noreferrer';
                    const compactMessage = String(watchEvent.message || '')
                        .replace(new RegExp(
                            `^${String(item.displayName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*`,
                            'i'
                        ), '');
                    changeLink.textContent = `${formatWatchDateTime(watchEvent.timestamp)} - ${compactMessage}`;
                    Object.assign(changeLink.style, {
                        minWidth: '0',
                        color: watchEvent.acknowledged ? '#777' : getEscortPagePinkColor(),
                        textDecoration: 'none'
                    });
                    changeLine.appendChild(changeLink);
                    if (!watchEvent.acknowledged) {
                        const acceptChange = makeButton('', '✓');
                        acceptChange.title = 'Zaakceptuj zmianę';
                        Object.assign(acceptChange.style, {
                            flex: '0 0 auto',
                            width: '18px',
                            height: '18px',
                            padding: '0',
                            border: `1px solid ${getEscortPagePinkColor()}`,
                            borderRadius: '50%',
                            background: '#fff',
                            color: getEscortPagePinkColor(),
                            cursor: 'pointer',
                            fontSize: '10px',
                            fontWeight: '800'
                        });
                        acceptChange.addEventListener('click', () =>
                            acknowledgeWatchEvent(watchEvent.id)
                        );
                        changeLine.appendChild(acceptChange);
                    }
                    changes.appendChild(changeLine);
                }

                const actions = makeElement('div');
                Object.assign(actions.style, {
                    display: 'grid',
                    gap: '3px'
                });
                const check = makeButton('', 'Sprawdź');
                const remove = makeButton('', 'Usuń');
                for (const button of [check, remove]) {
                    Object.assign(button.style, {
                        minHeight: '22px',
                        padding: '1px 6px',
                        border: '1px solid #ccc',
                        borderRadius: '5px',
                        background: '#f7f7f7',
                        color: '#333',
                        cursor: 'pointer',
                        fontSize: '10px',
                        fontWeight: '700'
                    });
                }
                onlineInput.addEventListener('change', () => {
                    updateWatchedItem(item.id, current => ({
                        modes: { ...current.modes, online: onlineInput.checked }
                    }));
                    render();
                });
                locationInput.addEventListener('change', async () => {
                    if (locationInput.checked) {
                        const target = await chooseWatchLocationTarget(item.locationTarget);
                        if (!target) {
                            locationInput.checked = false;
                            return;
                        }
                        updateWatchedItem(item.id, current => ({
                            modes: { ...current.modes, location: true },
                            locationTarget: target
                        }));
                    } else {
                        updateWatchedItem(item.id, current => ({
                            modes: { ...current.modes, location: false }
                        }));
                    }
                    render();
                });
                locationConfig.addEventListener('click', async () => {
                    const target = await chooseWatchLocationTarget(item.locationTarget);
                    if (!target) return;
                    updateWatchedItem(item.id, current => ({
                        modes: { ...current.modes, location: true },
                        locationTarget: target
                    }));
                    render();
                });
                check.addEventListener('click', async () => {
                    check.disabled = true;
                    check.textContent = 'Sprawdzanie…';
                    await runWatchedChecks({ force: true, itemIds: [item.id] });
                    render();
                });
                remove.addEventListener('click', () => {
                    if (!window.confirm(`Usunąć „${item.displayName}” z obserwowanych?`)) return;
                    removeWatchedItem(item.id);
                    render();
                });
                actions.append(check, remove);
                card.append(imageBox, details, modes, result, changes, actions);
                itemsHost.appendChild(card);
            }
        };

        overlay._vmRender = render;
        interval.addEventListener('change', () => {
            const settings = getWatchSettings();
            try {
                const saved = saveWatchSettings({
                    enabled: settings.enabled,
                    intervalMinutes: Number(interval.value)
                });
                SETTINGS = { ...DEFAULT_SETTINGS, ...saved };
            } catch (error) {
                interval.value = String(getWatchSettings().intervalMinutes);
                log('Nie udało się zapisać czasu sprawdzania obserwowanych', error);
                window.alert('Nie udało się zapisać czasu sprawdzania obserwowanych.');
            }
        });
        refresh.addEventListener('click', async () => {
            refresh.disabled = true;
            status.style.display = 'block';
            status.style.color = getEscortPagePinkColor();
            status.textContent = 'Sprawdzanie obserwowanych…';
            try {
                await runWatchedChecks({
                    force: true,
                    onProgress: progress => {
                        const itemPosition = progress.itemIndex || Math.min(
                            Number(progress.checked || 0) + 1,
                            Number(progress.total || 0)
                        );
                        const itemName = String(progress.itemName || '').trim();
                        const adTotal = Number(progress.adTotal) || 0;
                        const adChecked = Number(progress.adChecked) || 0;

                        if (progress.phase === 'done') {
                            status.textContent =
                                `Sprawdzono ${itemPosition}/${progress.total}: ${itemName}` +
                                (adTotal ? ` • ${adTotal}/${adTotal} anonsów` : '');
                            return;
                        }

                        status.textContent =
                            `Sprawdzanie ${itemPosition}/${progress.total}: ${itemName}` +
                            (adTotal ? ` • ${adChecked}/${adTotal} anonsów` : '…');
                    }
                });
                status.textContent = 'Sprawdzanie zakończone.';
            } finally {
                refresh.disabled = false;
                render();
                setTimeout(() => { status.style.display = 'none'; }, 1800);
            }
        });
        const add = async () => {
            addButton.disabled = true;
            status.style.display = 'block';
            status.style.color = getEscortPagePinkColor();
            status.textContent = 'Dodawanie obserwowanego…';
            try {
                const added = await addWatchFromManualInput(manualInput.value);
                if (added?.item) {
                    manualInput.value = '';
                    status.textContent = added.alreadyWatching
                        ? 'Już obserwujesz tę pozycję.'
                        : 'Dodano do obserwowanych.';
                } else {
                    status.style.display = 'none';
                }
            } catch (error) {
                status.textContent = error?.message || 'Nie udało się dodać obserwowanego';
                status.style.color = '#c62828';
            } finally {
                addButton.disabled = false;
                render();
            }
        };
        addButton.addEventListener('click', add);
        manualInput.addEventListener('keydown', event => {
            if (event.key === 'Enter') add();
        });
        overlay.addEventListener('click', event => {
            if (event.target === overlay) overlay.remove();
        });
        render();
    }

    function initLocalWatchSystem() {
        const host = location.hostname.toLowerCase();
        if (![
            'pl.escort.club',
            'escorti.pl',
            'www.escorti.pl',
            'garsoniera.com.pl',
            'www.garsoniera.com.pl'
        ].includes(host)) return;
        const pageUrl = new URL(location.href);
        if (pageUrl.searchParams.has('vm_mode')) return;

        if (!watchStorageListenersInitialized) {
            watchStorageListenersInitialized = true;
            try {
                GM_addValueChangeListener(WATCH_EVENTS_STORAGE_KEY, () => {
                    renderWatchNotificationBar();
                    document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();
                });
                GM_addValueChangeListener(WATCH_ITEMS_STORAGE_KEY, () => {
                    refreshWatchPageButtons();
                    renderWatchNotificationBar();
                    document.getElementById(WATCH_SETTINGS_OVERLAY_ID)?._vmRender?.();
                });
                GM_addValueChangeListener(SETTINGS_STORAGE_KEY, () => {
                    setTimeout(() => {
                        SETTINGS = getSettings();
                        applyWatchFeatureState();
                    }, 0);
                });
            } catch (_) {}
        }

        if (!getWatchSettings().enabled) {
            if (watchedPollTimer) {
                clearInterval(watchedPollTimer);
                watchedPollTimer = null;
            }
            removeWatchPageUi();
            return;
        }

        renderWatchNotificationBar();
        if (!watchedInitialCheckScheduled) {
            watchedInitialCheckScheduled = true;
            setTimeout(() => runWatchedChecks().catch(error =>
                log('Błąd obserwowanych', error)
            ), 3500);
        }
        if (!watchedPollTimer) {
            watchedPollTimer = setInterval(() => {
                runWatchedChecks().catch(error => log('Błąd obserwowanych', error));
            }, 60000);
        }
    }

    function applyWatchFeatureState() {
        const host = location.hostname.toLowerCase();
        if (![
            'pl.escort.club',
            'escorti.pl',
            'www.escorti.pl',
            'garsoniera.com.pl',
            'www.garsoniera.com.pl'
        ].includes(host)) return;

        initLocalWatchSystem();
        if (!getWatchSettings().enabled) return;

        if (host === 'escorti.pl' || host === 'www.escorti.pl') {
            initEscortiProfileWatchButtons();
        } else if (/^\/anons\/\d+\.html\/?$/i.test(location.pathname)) {
            initEscortClubWatchButtons();
        }
    }

    // ============================================================
    // ESCORT.CLUB - zapisane zestawy filtrów wyszukiwania
    // ============================================================

    function getEscortSavedSearchFilters() {
        try {
            const saved = GM_getValue(ESCORT_SAVED_FILTERS_STORAGE_KEY, []);
            if (!Array.isArray(saved)) return [];

            return saved
                .filter(item => item && typeof item === 'object' && item.id && item.name && item.state)
                .map(item => ({
                    id: String(item.id),
                    name: String(item.name).trim().slice(0, 80),
                    savedAt: Number(item.savedAt) || 0,
                    state: item.state
                }))
                .filter(item => item.name);
        } catch (error) {
            log('Błąd odczytu zapisanych filtrów Escort.club', error);
            return [];
        }
    }

    function setEscortSavedSearchFilters(filters) {
        try {
            GM_setValue(ESCORT_SAVED_FILTERS_STORAGE_KEY, Array.isArray(filters) ? filters : []);
            return true;
        } catch (error) {
            log('Błąd zapisu filtrów Escort.club', error);
            return false;
        }
    }

    function captureEscortAdvancedFilterState() {
        const advanced = document.getElementById('advansed-search');
        if (!advanced) return null;

        const state = {
            named: {},
            ranges: {},
            breasts: null
        };

        const groups = new Map();
        for (const control of advanced.querySelectorAll('input[name], select[name], textarea[name]')) {
            const name = String(control.name || '').trim();
            if (!name) continue;
            if (!groups.has(name)) groups.set(name, []);
            groups.get(name).push(control);
        }

        for (const [name, controls] of groups) {
            const first = controls[0];
            const types = controls.map(control => String(control.type || '').toLowerCase());

            if (types.every(type => type === 'radio')) {
                const checked = controls.find(control => control.checked);
                state.named[name] = {
                    kind: 'radio',
                    value: checked ? String(checked.value ?? '') : null
                };
                continue;
            }

            if (types.every(type => type === 'checkbox')) {
                state.named[name] = {
                    kind: 'checkbox',
                    values: controls
                        .filter(control => control.checked)
                        .map(control => String(control.value ?? ''))
                };
                continue;
            }

            if (first instanceof HTMLSelectElement && first.multiple) {
                state.named[name] = {
                    kind: 'multiple',
                    values: [...first.selectedOptions].map(option => String(option.value))
                };
                continue;
            }

            state.named[name] = {
                kind: 'values',
                values: controls.map(control => String(control.value ?? ''))
            };
        }

        for (const key of ['price', 'age', 'weight', 'height']) {
            const from = advanced.querySelector(`input[data-${key}-from]`);
            const to = advanced.querySelector(`input[data-${key}-to]`);
            if (!from && !to) continue;

            state.ranges[key] = {
                from: from ? String(from.value ?? '') : null,
                to: to ? String(to.value ?? '') : null
            };
        }

        const breastsInput = advanced.querySelector('#range_breasts_filter');
        if (breastsInput) {
            let from = breastsInput.getAttribute('data-from');
            let to = breastsInput.getAttribute('data-to');

            try {
                const jq = window.jQuery;
                const slider = jq ? jq(breastsInput).data('ionRangeSlider') : null;
                if (slider?.result) {
                    from = slider.result.from;
                    to = slider.result.to;
                }
            } catch (_) {}

            state.breasts = {
                value: String(breastsInput.value ?? ''),
                from: from == null ? null : String(from),
                to: to == null ? null : String(to)
            };
        }

        return state;
    }

    function dispatchEscortFilterControlEvents(control) {
        try {
            control.dispatchEvent(new Event('input', { bubbles: true }));
            control.dispatchEvent(new Event('change', { bubbles: true }));
        } catch (_) {}
    }

    function refreshEscortFilterSelect(control) {
        if (!(control instanceof HTMLSelectElement)) return;

        try {
            const jq = window.jQuery;
            if (!jq) return;

            if (control.multiple && jq.fn?.select2 && jq(control).hasClass('select2-hidden-accessible')) {
                jq(control).trigger('change');
            }

            if (jq.fn?.selectpicker && jq(control).parent().hasClass('bootstrap-select')) {
                jq(control).selectpicker('refresh');
            }
        } catch (error) {
            log('Nie udało się odświeżyć kontrolki filtra Escort.club', error);
        }
    }

    function applyEscortAdvancedFilterState(state) {
        const advanced = document.getElementById('advansed-search');
        if (!advanced || !state || typeof state !== 'object') return false;

        const named = state.named && typeof state.named === 'object' ? state.named : {};

        for (const [name, stored] of Object.entries(named)) {
            const controls = [...advanced.querySelectorAll('[name]')]
                .filter(control => String(control.name || '') === name);

            if (!controls.length || !stored || typeof stored !== 'object') continue;

            if (stored.kind === 'radio') {
                for (const control of controls) {
                    control.checked = stored.value != null && String(control.value) === String(stored.value);
                    dispatchEscortFilterControlEvents(control);
                }
                continue;
            }

            if (stored.kind === 'checkbox') {
                const selected = new Set((stored.values || []).map(value => String(value)));
                for (const control of controls) {
                    control.checked = selected.has(String(control.value ?? ''));
                    dispatchEscortFilterControlEvents(control);
                }
                continue;
            }

            if (stored.kind === 'multiple') {
                const values = (stored.values || []).map(value => String(value));
                const selected = new Set(values);

                for (const control of controls) {
                    if (!(control instanceof HTMLSelectElement)) continue;
                    for (const option of control.options) {
                        option.selected = selected.has(String(option.value));
                    }
                    dispatchEscortFilterControlEvents(control);
                    refreshEscortFilterSelect(control);
                }
                continue;
            }

            const values = Array.isArray(stored.values) ? stored.values : [];
            controls.forEach((control, index) => {
                const value = values[index] ?? values[0] ?? '';
                control.value = String(value);
                dispatchEscortFilterControlEvents(control);
                refreshEscortFilterSelect(control);
            });
        }

        const ranges = state.ranges && typeof state.ranges === 'object' ? state.ranges : {};
        for (const key of ['price', 'age', 'weight', 'height']) {
            const stored = ranges[key];
            if (!stored) continue;

            const from = advanced.querySelector(`input[data-${key}-from]`);
            const to = advanced.querySelector(`input[data-${key}-to]`);

            if (from && stored.from != null) {
                from.value = String(stored.from);
                dispatchEscortFilterControlEvents(from);
            }
            if (to && stored.to != null) {
                to.value = String(stored.to);
                dispatchEscortFilterControlEvents(to);
            }
        }

        if (state.breasts) {
            const breastsInput = advanced.querySelector('#range_breasts_filter');
            if (breastsInput) {
                const from = Number(state.breasts.from);
                const to = Number(state.breasts.to);
                let sliderUpdated = false;

                try {
                    const jq = window.jQuery;
                    const slider = jq ? jq(breastsInput).data('ionRangeSlider') : null;
                    if (slider && Number.isFinite(from) && Number.isFinite(to)) {
                        slider.update({ from, to });
                        sliderUpdated = true;
                    }
                } catch (_) {}

                if (!sliderUpdated) {
                    if (state.breasts.from != null) breastsInput.setAttribute('data-from', String(state.breasts.from));
                    if (state.breasts.to != null) breastsInput.setAttribute('data-to', String(state.breasts.to));
                }

                if (state.breasts.value != null) {
                    breastsInput.value = String(state.breasts.value);
                }
                dispatchEscortFilterControlEvents(breastsInput);
            }
        }

        // Po zmianach odświeżamy widoczne nakładki bootstrap-select/select2.
        for (const select of advanced.querySelectorAll('select')) {
            refreshEscortFilterSelect(select);
        }

        return true;
    }

    function initEscortSavedSearchFilters() {
        if (location.hostname !== 'pl.escort.club') return;
        if (document.getElementById('vm-escort-saved-filters')) return;

        const advanced = document.getElementById('advansed-search');
        const footer = advanced?.querySelector('.search-fiters.-reset');

        if (!advanced || !footer) {
            if (!document.documentElement.dataset.vmSavedFiltersRetry) {
                document.documentElement.dataset.vmSavedFiltersRetry = '1';
                let attempts = 0;
                const timer = setInterval(() => {
                    attempts++;
                    if (document.getElementById('advansed-search')?.querySelector('.search-fiters.-reset')) {
                        clearInterval(timer);
                        delete document.documentElement.dataset.vmSavedFiltersRetry;
                        initEscortSavedSearchFilters();
                    } else if (attempts >= 40) {
                        clearInterval(timer);
                        delete document.documentElement.dataset.vmSavedFiltersRetry;
                    }
                }, 250);
            }
            return;
        }

        const pagePinkColor = getEscortPagePinkColor();
        const styleId = 'vm-escort-saved-filters-style';
        if (!document.getElementById(styleId)) {
            const style = makeElement('style');
            style.id = styleId;
            style.textContent = `
                #vm-escort-saved-filters {
                    display: inline-flex;
                    align-items: center;
                    flex: 0 1 auto;
                    flex-wrap: nowrap;
                    gap: 7px;
                    max-width: 100%;
                    margin-right: auto;
                    padding-right: 14px;
                    padding-bottom: 2px;
                    overflow-x: auto;
                    font-family: Lato, Arial, sans-serif;
                }
                #vm-escort-saved-filters .vm-saved-filters-label {
                    color: #211d22;
                    font-size: 12px;
                    font-weight: 700;
                    line-height: 32px;
                    white-space: nowrap;
                }
                #vm-escort-saved-filters .vm-saved-filter-select-wrap {
                    --vm-saved-filter-select-width: ${ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH}px;
                    position: relative;
                    width: var(--vm-saved-filter-select-width) !important;
                    min-width: ${ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH}px;
                    max-width: ${ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH}px;
                    flex: 0 0 var(--vm-saved-filter-select-width) !important;
                    height: 32px;
                    margin: 0;
                    transition: width .15s ease, flex-basis .15s ease;
                }
                #vm-escort-saved-filters .vm-saved-filter-select-wrap::after {
                    content: '';
                    position: absolute;
                    top: 13px;
                    right: 10px;
                    width: 0;
                    height: 0;
                    border-left: 7px solid transparent;
                    border-right: 7px solid transparent;
                    border-top: 7px solid #ff4c99;
                    pointer-events: none;
                }
                #vm-escort-saved-filters-select {
                    appearance: none !important;
                    -webkit-appearance: none !important;
                    -moz-appearance: none !important;
                    box-sizing: border-box;
                    width: 100% !important;
                    min-width: 100% !important;
                    max-width: 100% !important;
                    height: 32px;
                    min-height: 32px;
                    margin: 0;
                    padding: 0 30px 0 12px !important;
                    border: 0 !important;
                    border-bottom: 1px solid #ff4c99 !important;
                    border-radius: 0 !important;
                    outline: 0 !important;
                    background: transparent !important;
                    color: #211d22;
                    font-family: Lato, Arial, sans-serif;
                    font-size: 14px;
                    line-height: 31px;
                    text-overflow: ellipsis;
                    cursor: pointer;
                    box-shadow: none !important;
                }
                #vm-escort-saved-filters-select:focus {
                    border-bottom-color: #d90070 !important;
                }
                #vm-escort-saved-filters .vm-saved-filter-action {
                    box-sizing: border-box;
                    display: inline-flex !important;
                    align-items: center;
                    justify-content: center;
                    flex: 0 0 auto !important;
                    width: auto !important;
                    min-width: max-content !important;
                    max-width: none !important;
                    height: 32px;
                    margin: 0;
                    padding: 7px 12px !important;
                    border-radius: 7px !important;
                    font-family: Lato, Arial, sans-serif;
                    font-size: 12px;
                    font-weight: 700;
                    line-height: 16px;
                    white-space: nowrap !important;
                    text-decoration: none;
                    cursor: pointer;
                    transition: background-color .15s ease, border-color .15s ease, color .15s ease;
                }
                #vm-escort-saved-filters .vm-saved-filter-delete {
                    border: 1px solid ${pagePinkColor} !important;
                    background: #fff !important;
                    color: ${pagePinkColor} !important;
                }
                #vm-escort-saved-filters .vm-saved-filter-delete:hover:not(:disabled),
                #vm-escort-saved-filters .vm-saved-filter-delete:focus:not(:disabled) {
                    background: #fff1f8 !important;
                    border-color: ${pagePinkColor} !important;
                    color: ${pagePinkColor} !important;
                    filter: brightness(.9);
                }
                #vm-escort-saved-filters .vm-saved-filter-load:disabled,
                #vm-escort-saved-filters .vm-saved-filter-delete:disabled {
                    border-color: #d8d2d5 !important;
                    background: #f4f1f3 !important;
                    color: #aaa3a7 !important;
                    cursor: not-allowed;
                    opacity: .72;
                }
                #vm-escort-saved-filters .vm-saved-filters-status {
                    color: #8d858a;
                    font-size: 10px;
                    line-height: 16px;
                    white-space: nowrap;
                }
                @media (max-width: 767px) {
                    #vm-escort-saved-filters {
                        flex: 0 1 100%;
                        width: 100%;
                        margin-bottom: 10px;
                        padding-right: 0;
                    }
                    #vm-escort-saved-filters .vm-saved-filters-status {
                        display: none;
                    }
                }
            `;
            document.head.appendChild(style);
        }

        const root = makeElement('div');
        root.id = 'vm-escort-saved-filters';

        const label = makeElement('span', 'sub-label vm-saved-filters-label', 'Zapisane filtry:');

        const select = makeElement('select');
        select.id = 'vm-escort-saved-filters-select';
        select.setAttribute('aria-label', 'Zapisane filtry wyszukiwania');

        const selectWrap = makeElement('div', 'vm-saved-filter-select-wrap');
        selectWrap.appendChild(select);

        function updateSavedFilterSelectWidth() {
            const selectedOption = select.options[select.selectedIndex];
            const text = String(selectedOption?.textContent || '').trim();
            const canvas = makeElement('canvas');
            const context = canvas.getContext('2d');

            if (context) {
                context.font = '14px Lato, Arial, sans-serif';
            }

            const textWidth = context ? context.measureText(text).width : text.length * 8;
            const naturalWidth = Math.ceil(textWidth) + 48;
            const width = Math.max(
                ESCORT_SAVED_FILTER_SELECT_MIN_WIDTH,
                Math.min(ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH, naturalWidth)
            );

            selectWrap.style.setProperty('--vm-saved-filter-select-width', `${width}px`);
            select.title = naturalWidth > ESCORT_SAVED_FILTER_SELECT_MAX_WIDTH ? text : '';
        }

        const loadBtn = makeButton('btn btn-pink vm-saved-filter-action vm-saved-filter-load', 'Wczytaj');
        loadBtn.disabled = true;
        loadBtn.setAttribute('aria-disabled', 'true');
        loadBtn.title = 'Najpierw wybierz zapisany zestaw';

        const saveBtn = makeButton('btn btn-pink vm-saved-filter-action vm-saved-filter-save', 'Zapisz');

        const deleteBtn = makeButton('', 'Usuń');
        deleteBtn.className = 'btn vm-saved-filter-action vm-saved-filter-delete';
        deleteBtn.disabled = true;
        deleteBtn.setAttribute('aria-disabled', 'true');
        deleteBtn.title = 'Najpierw wybierz zapisany zestaw';

        const status = makeElement('span', 'sub-label vm-saved-filters-status');
        status.setAttribute('aria-live', 'polite');

        function getSelectedId() {
            return String(select.value || '');
        }

        function updatePresetActionState() {
            const enabled = !!getSelectedId();

            loadBtn.disabled = !enabled;
            loadBtn.setAttribute('aria-disabled', String(!enabled));
            loadBtn.title = enabled
                ? 'Wczytaj wybrany zapisany zestaw'
                : 'Najpierw wybierz zapisany zestaw';

            deleteBtn.disabled = !enabled;
            deleteBtn.setAttribute('aria-disabled', String(!enabled));
            deleteBtn.title = enabled
                ? 'Usuń wybrany zapisany zestaw'
                : 'Najpierw wybierz zapisany zestaw';
        }

        function refreshList(selectedId = '') {
            const filters = getEscortSavedSearchFilters();
            select.replaceChildren();

            const placeholder = makeElement('option');
            placeholder.value = '';
            placeholder.textContent = filters.length ? '- wybierz -' : '- brak zapisanych -';
            select.appendChild(placeholder);

            for (const filter of filters) {
                const option = makeElement('option');
                option.value = filter.id;
                option.textContent = filter.name;
                option.selected = filter.id === selectedId;
                select.appendChild(option);
            }

            if (selectedId && !filters.some(filter => filter.id === selectedId)) {
                select.value = '';
            }

            updateSavedFilterSelectWidth();
            updatePresetActionState();
        }

        select.addEventListener('change', () => {
            const id = getSelectedId();
            updateSavedFilterSelectWidth();
            updatePresetActionState();
            if (!id) {
                status.textContent = '';
                return;
            }

            const preset = getEscortSavedSearchFilters().find(item => item.id === id);
            if (!preset) {
                status.textContent = 'nie znaleziono zestawu';
                return;
            }

            status.textContent = `wybrano „${preset.name}”`;
        });

        loadBtn.addEventListener('click', event => {
            event.preventDefault();

            const id = getSelectedId();
            if (!id) return;

            const preset = getEscortSavedSearchFilters().find(item => item.id === id);
            if (!preset) {
                status.textContent = 'nie znaleziono zestawu';
                refreshList('');
                return;
            }

            if (applyEscortAdvancedFilterState(preset.state)) {
                status.textContent = `wczytano „${preset.name}”`;
            } else {
                status.textContent = 'nie udało się wczytać';
            }
        });

        saveBtn.addEventListener('click', event => {
            event.preventDefault();

            const currentId = getSelectedId();
            const currentPreset = getEscortSavedSearchFilters().find(item => item.id === currentId);
            const proposed = window.prompt(
                'Podaj nazwę zestawu filtrów:',
                currentPreset?.name || ''
            );

            if (proposed == null) return;
            const name = proposed.replace(/\s+/g, ' ').trim().slice(0, 80);
            if (!name) {
                window.alert('Nazwa zestawu filtrów nie może być pusta.');
                return;
            }

            const state = captureEscortAdvancedFilterState();
            if (!state) {
                status.textContent = 'nie udało się odczytać filtrów';
                return;
            }

            const filters = getEscortSavedSearchFilters();
            const existing = filters.find(item => item.name.toLocaleLowerCase('pl') === name.toLocaleLowerCase('pl'));
            let id;

            if (existing) {
                if (!window.confirm(`Zestaw „${existing.name}” już istnieje. Nadpisać go?`)) return;
                existing.name = name;
                existing.state = state;
                existing.savedAt = Date.now();
                id = existing.id;
            } else {
                id = `preset_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
                filters.unshift({
                    id,
                    name,
                    state,
                    savedAt: Date.now()
                });
            }

            if (!setEscortSavedSearchFilters(filters)) {
                status.textContent = 'błąd zapisu';
                return;
            }

            refreshList(id);
            status.textContent = `zapisano „${name}”`;
        });

        deleteBtn.addEventListener('click', event => {
            event.preventDefault();
            const id = getSelectedId();
            if (!id) return;

            const filters = getEscortSavedSearchFilters();
            const preset = filters.find(item => item.id === id);
            if (!preset) return;

            if (!window.confirm(`Usunąć zapisany zestaw „${preset.name}”?`)) return;

            const next = filters.filter(item => item.id !== id);
            if (!setEscortSavedSearchFilters(next)) {
                status.textContent = 'błąd usuwania';
                return;
            }

            refreshList('');
            status.textContent = `usunięto „${preset.name}”`;
        });

        root.appendChild(label);
        root.appendChild(selectWrap);
        root.appendChild(loadBtn);
        root.appendChild(saveBtn);
        root.appendChild(deleteBtn);
        root.appendChild(status);
        footer.insertBefore(root, footer.firstChild);

        refreshList('');
        document.fonts?.ready
            .then(() => {
                if (document.contains(select)) updateSavedFilterSelectWidth();
            })
            .catch(() => {});
    }

    // ============================================================
    // PICDETECTIVE / TINEYE - automatyczne wklejenie URL zdjęcia
    // ============================================================

    function setNativeInputValue(input, value) {
        const proto = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
        const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
        if (setter) setter.call(input, value);
        else input.value = value;
        input.dispatchEvent(new Event('input', { bubbles: true }));
        input.dispatchEvent(new Event('change', { bubbles: true }));
    }

    function findReverseImageUrlInput() {
        const inputs = [...document.querySelectorAll('input:not([type="file"]):not([type="hidden"]), textarea')];
        const scored = inputs.map(input => {
            const meta = [input.placeholder, input.getAttribute('aria-label'), input.name, input.id, input.type]
                .filter(Boolean).join(' ').toLowerCase();
            let score = 0;
            if (input.type === 'url') score += 10;
            if (/image\s*url|url\s*(?:of|to|for)?\s*image/.test(meta)) score += 20;
            if (/enter.*url|paste.*url|search.*url/.test(meta)) score += 10;
            if (/\burl\b/.test(meta)) score += 4;
            return { input, score };
        }).sort((a, b) => b.score - a.score);
        return scored[0]?.score > 0 ? scored[0].input : null;
    }

    function initReverseImageUrlPrefill() {
        const host = location.hostname.toLowerCase();
        const supported = ['picdetective.com', 'www.picdetective.com', 'tineye.com', 'www.tineye.com'].includes(host);
        if (!supported) return false;

        const pageUrl = new URL(location.href);
        const imageUrl = pageUrl.searchParams.get('vm_image_url');
        if (!imageUrl) return true;

        let attempts = 0;
        const timer = setInterval(() => {
            attempts++;
            const input = findReverseImageUrlInput();
            if (input) {
                clearInterval(timer);
                setNativeInputValue(input, imageUrl);
                input.focus();
                pageUrl.searchParams.delete('vm_image_url');
                history.replaceState({}, document.title, pageUrl.href);
            } else if (attempts >= 60) {
                clearInterval(timer);
            }
        }, 250);
        return true;
    }

    if (initReverseImageUrlPrefill()) return;

    // ============================================================
    // ESCORTI - logika 13.4.1
    // ============================================================

    // Niewidoczna ramka używana wyłącznie przez ustawienia lokalizacji.
    // Kończymy normalną inicjalizację skryptu w ramce, żeby nie uruchamiać
    // obserwowania ani elementów UI wewnątrz technicznej strony wyszukiwania.
    if (await handleEscortLocationTabBridgePage()) return;
    if (await handleEscortLocationBridgeFrame()) return;

    // Na wszystkich obsługiwanych domenach uruchamiamy wspólny system
    // obserwowanych (pasek powiadomień, licznik online i harmonogram sprawdzania).
    // GARSO_MENU_ONLY_MODE nadal blokuje UI przeznaczone wyłącznie dla
    // Escort.club/Escorti w dalszej części skryptu.
    applyWatchFeatureState();

    if (location.hostname === 'escorti.pl' || location.hostname === 'www.escorti.pl') {
        initEscortiProfileAdLinkStatuses();
        handleEscortiPage();
        return;
    }

    if (location.hostname === 'pl.escort.club') {
        const isSingleEscortAd = /^\/anons\/\d+\.html\/?$/i.test(location.pathname);

        initEscortSectionVisibility();
        initEscortTopPhoneSearch();
        if (!isSingleEscortAd) {
            initEscortDefaultCityButton();
            initEscortSavedSearchFilters();
        }

        if (isSingleEscortAd) {
            initEscortClubDatePostedDisplay();
            initEscortClubVisitChangeTracking();
        }
    }

    if (
        location.hostname === 'pl.escort.club' &&
        (location.pathname.startsWith('/anonse/') || isEscortSearchPath())
    ) {
        try {
            await initEscortClubListPage();
        } catch (error) {
            log('Nie udało się uruchomić funkcji strony wyników Escort.club', error);
            showEscortListInitializationError(error);
        }
        return;
    }

    function handleEscortiPage() {
        const url = new URL(location.href);
        const mode = url.searchParams.get('vm_mode');
        const resultKey = url.searchParams.get('vm_key');
        const bootKey = url.searchParams.get('vm_boot');
        const mergeProfiles = url.searchParams.get('vm_merge') === '1';
        const stateKey = url.searchParams.get('vm_state');

        if (mode === 'check' && bootKey) {
            GM_setValue(bootKey, { started: true, timestamp: Date.now() });
        }
        if (!mode) return;
        if (url.pathname === '/search') return processEscortiSearchPage(mode, resultKey, mergeProfiles);
        if (/^\/escort\/\d+\/?$/.test(url.pathname)) processEscortiProfilePage(mode, resultKey, mergeProfiles, stateKey);
    }

    function processEscortiSearchPage(mode, resultKey, mergeProfiles = false) {
        let attempts = 0;
        const timer = setInterval(() => {
            attempts++;
            const profileUrls = getEscortiProfileUrls();
            const declaredProfileCount = getDeclaredProfileCount();

            if (profileUrls.length === 1) {
                clearInterval(timer);
                const profileUrl = new URL(profileUrls[0]);
                profileUrl.searchParams.set('vm_mode', mode);
                if (resultKey) profileUrl.searchParams.set('vm_key', resultKey);
                if (mergeProfiles) profileUrl.searchParams.set('vm_merge', '1');
                location.replace(profileUrl.href);
                return;
            }

            if (profileUrls.length > 1) {
                clearInterval(timer);

                // Na listach Escort.club agregujemy wszystkie znalezione profile.
                if (mode === 'check' && mergeProfiles) {
                    startMultiProfileFlow(resultKey, profileUrls);
                    return;
                }

                // Dotychczasowe zachowanie pojedynczego anonsu pozostaje bez zmian.
                if (mode === 'check') finishEscortiCheck(resultKey, { status: 'ok', profiles: profileUrls.length });
                else removeVmParamsFromCurrentUrl();
                return;
            }

            if (declaredProfileCount > 0 && attempts < 40) return;

            if (declaredProfileCount === 0) {
                clearInterval(timer);
                if (mode === 'check') finishEscortiCheck(resultKey, { status: 'ok', profiles: 0 });
                else removeVmParamsFromCurrentUrl();
                return;
            }

            if (attempts >= 40) {
                clearInterval(timer);
                if (mode === 'check') finishEscortiCheck(resultKey, { status: 'timeout' });
            }
        }, 150);
    }

    function getEscortiProfileUrls(sourceDocument = document, baseUrl = location.href) {
        const unique = new Set();
        for (const link of sourceDocument.querySelectorAll('#tab-profiles a[href], a[href*="/escort/"]')) {
            try {
                const url = new URL(link.getAttribute('href'), baseUrl);
                if ((url.hostname === 'escorti.pl' || url.hostname === 'www.escorti.pl') && /^\/escort\/\d+\/?$/.test(url.pathname)) {
                    url.search = '';
                    url.hash = '';
                    unique.add(url.href);
                }
            } catch (_) {}
        }
        return [...unique];
    }

    function getDeclaredProfileCount(sourceDocument = document) {
        const tab = sourceDocument.querySelector('button[data-tab="profiles"]');
        if (!tab) return null;
        for (const span of tab.querySelectorAll('span')) {
            const text = (span.textContent || '').trim();
            if (/^\d+$/.test(text)) return Number(text);
        }
        return null;
    }

    function extractEscortiCurrentCity(sourceDocument = document) {
        const breadcrumbItems = [...sourceDocument.querySelectorAll(
            'nav[aria-label="breadcrumb"] li'
        )];
        const last = breadcrumbItems.at(-1);
        return normalizeEscortCity(last?.textContent) || null;
    }

    function normalizeEscortiHistoryDate(value) {
        const text = normalizeEscortAdText(value);
        const match = text.match(
            /(\d{4}-\d{2}-\d{2}|\d{2}[.\-/]\d{2}[.\-/]\d{4})/
        );
        return match ? formatEscortiDate(match[1]) : null;
    }

    function extractEscortiCityHistory(sourceDocument = document) {
        const entries = new Map();
        const add = (dateValue, cityValue) => {
            const date = normalizeEscortiHistoryDate(dateValue);
            const city = normalizeEscortCity(cityValue)
                .replace(/^(?:miasto|lokalizacja)\s*:?\s*/i, '')
                .trim();
            if (!date || !city || /^brak$/i.test(city)) return;
            const key = `${date}|${normalizeEscortCityKey(city)}`;
            if (!entries.has(key)) entries.set(key, { date, city });
        };

        // Aktualny układ Escorti: historia jest listą kapsułek pod nagłówkiem
        // „Odwiedzane lokalizacje”. Starsze wpisy pozostają w DOM wewnątrz
        // `.older-addresses`, nawet jeśli są wizualnie zwinięte.
        const locationHeadings = [...sourceDocument.querySelectorAll(
            'h1, h2, h3, h4, h5, h6'
        )].filter(heading =>
            normalizeText(heading.textContent).startsWith('odwiedzane lokalizacje')
        );
        for (const heading of locationHeadings) {
            const section = heading.parentElement;
            if (!section) continue;

            // `div > span` wybiera zewnętrzną kapsułkę lokalizacji, ale nie
            // zagnieżdżony span zawierający samą datę.
            for (const entry of section.querySelectorAll('div > span')) {
                const dateNode = [...entry.querySelectorAll('span')].find(node =>
                    normalizeEscortiHistoryDate(node.textContent)
                );
                if (!dateNode) continue;

                const cityNode = entry.cloneNode(true);
                cityNode.querySelectorAll('span').forEach(node => node.remove());
                add(dateNode.textContent, cityNode.textContent);
            }
        }

        for (const table of sourceDocument.querySelectorAll('table')) {
            const headers = [...table.querySelectorAll('thead th')]
                .map(cell => normalizeText(cell.textContent));
            if (!headers.length) continue;

            const dateIndex = headers.findIndex(header =>
                /data|dzień|opublikowano|dodano|okres|pierwsz|ostatn|widzian|(^|\s)od(\s|$)/.test(header)
            );
            const cityIndex = headers.findIndex(header =>
                /miasto|lokalizacj|miejscowość/.test(header)
            );
            if (dateIndex < 0 || cityIndex < 0) continue;

            for (const row of table.querySelectorAll('tbody tr')) {
                const cells = [...row.querySelectorAll(':scope > td')];
                if (!cells[dateIndex] || !cells[cityIndex]) continue;
                add(cells[dateIndex].textContent, cells[cityIndex].textContent);
            }
        }

        for (const element of sourceDocument.querySelectorAll(
            '[data-city][data-date], [data-miasto][data-data], ' +
            '[data-location][data-date], [data-city][data-created-at]'
        )) {
            add(
                element.getAttribute('data-date') ||
                    element.getAttribute('data-data') ||
                    element.getAttribute('data-created-at'),
                element.getAttribute('data-city') ||
                    element.getAttribute('data-miasto') ||
                element.getAttribute('data-location')
            );
        }

        // Wariant kart/list: data jest tekstem wiersza, a miasto linkiem do
        // katalogu Escorti. Breadcrumb nie przejdzie tego warunku, bo nie ma daty.
        for (const row of sourceDocument.querySelectorAll('tr, li, article')) {
            const date = normalizeEscortiHistoryDate(row.textContent);
            if (!date) continue;
            const cityLink = [...row.querySelectorAll('a[href]')].find(anchor => {
                try {
                    const url = new URL(anchor.getAttribute('href'), ESCORTI_BASE_URL);
                    return /^\/escorts\/[^/]+\/?$/i.test(url.pathname) &&
                        !/\/(?:wszystkie|polska|poland)\/?$/i.test(url.pathname);
                } catch (_) {
                    return false;
                }
            });
            if (cityLink) add(date, cityLink.textContent);
        }

        return [...entries.values()].sort((a, b) =>
            (profileDateToTime(a.date) || 0) - (profileDateToTime(b.date) || 0) ||
            a.city.localeCompare(b.city, 'pl')
        );
    }

    function processEscortiProfilePage(mode, resultKey, mergeProfiles = false, stateKey = null) {
        let attempts = 0;
        const timer = setInterval(() => {
            attempts++;
            const info = readEscortiProfileInfo();

            // Nagłówek i data profilu pojawiają się wcześniej niż sekcja z linkami.
            // Nie kończ odczytu na częściowo wyrenderowanej stronie, bo obserwowanie
            // zapisałoby wtedy błędny stan 0/0.
            if (info && (info.adUrls.length > 0 || attempts >= 40)) {
                clearInterval(timer);

                const currentProfileUrl = `${location.origin}${location.pathname}`;
                info.profileUrl = currentProfileUrl;

                if (mode === 'check' && mergeProfiles && stateKey) {
                    continueMultiProfileFlow(resultKey, stateKey, info, null);
                    return;
                }

                if (mode === 'check') {
                    finishEscortiCheck(resultKey, {
                        status: 'ok',
                        profiles: 1,
                        profileUrl: currentProfileUrl,
                        profileUrls: [currentProfileUrl],
                        adLinks: info.adLinks,
                        adUrls: info.adUrls,
                        profileName: info.profileName,
                        imageUrl: info.imageUrl,
                        creationDate: info.creationDate,
                        currentCity: info.currentCity,
                        cityHistory: info.cityHistory,
                        profileSummaries: [{
                            profileUrl: currentProfileUrl,
                            profileName: info.profileName,
                            creationDate: info.creationDate,
                            currentCity: info.currentCity,
                            adLinks: info.adLinks,
                            cityHistory: info.cityHistory
                        }],
                        garsoTopics: info.garsoTopics,
                        garsoTopicTitles: info.garsoTopicTitles
                    });
                } else {
                    removeVmParamsFromCurrentUrl();
                }
                return;
            }

            if (attempts >= 40) {
                clearInterval(timer);

                if (mode === 'check' && mergeProfiles && stateKey) {
                    continueMultiProfileFlow(resultKey, stateKey, null, 'Nie udało się odczytać danych profilu');
                    return;
                }

                if (mode === 'check') finishEscortiCheck(resultKey, { status: 'error', message: 'Nie udało się odczytać danych profilu' });
            }
        }, 150);
    }

    function readEscortiProfileInfo(sourceDocument = document, pageUrl = location.href) {
        const bodyText = sourceDocument.body
            ? (sourceDocument.body.innerText || sourceDocument.body.textContent || '')
            : '';
        const dateMatch = bodyText.match(/Data utworzenia profilu\s*:?\s*(\d{4}-\d{2}-\d{2}|\d{2}[.\-/]\d{2}[.\-/]\d{4})/i);
        if (!dateMatch) return null;

        const adUrls = getEscortiAdUrls(sourceDocument, pageUrl);
        const garsoTopicTitles = getEscortiGarsoTopicTitles(sourceDocument);

        return {
            creationDate: formatEscortiDate(dateMatch[1]),
            adUrls,
            adLinks: adUrls.length,
            profileName: extractWatchPageName(sourceDocument, 'Profil Escorti'),
            imageUrl: extractWatchPageImage(sourceDocument, pageUrl),
            currentCity: extractEscortiCurrentCity(sourceDocument),
            cityHistory: extractEscortiCityHistory(sourceDocument),
            garsoTopicTitles,
            garsoTopics: garsoTopicTitles.length
        };
    }

    function normalizeEscortiAdUrl(href, baseUrl = location.href) {
        try {
            const u = new URL(href, baseUrl);
            const m = u.pathname.match(/\/anons\/(\d+)\.html\/?$/i);
            if (!m) return null;
            // ID anonsu jest stabilnym kluczem do usuwania duplikatów.
            return `https://pl.escort.club/anons/${m[1]}.html`;
        } catch (_) {
            return null;
        }
    }

    function getEscortiAdUrls(sourceDocument = document, baseUrl = location.href) {
        const unique = new Map();

        function addUrl(value) {
            const normalized = normalizeEscortiAdUrl(value, baseUrl);
            if (!normalized) return;
            const id = normalized.match(/\/anons\/(\d+)\.html$/i)?.[1] || normalized;
            unique.set(id, normalized);
        }

        function scanText(value) {
            const text = String(value || '')
                .replace(/\\\//g, '/')
                .replace(/&amp;/gi, '&');
            for (const match of text.matchAll(/(?:https?:\/\/pl\.escort\.club)?\/anons\/\d+\.html/gi)) {
                addUrl(match[0]);
            }
        }

        for (const a of sourceDocument.querySelectorAll('a[href]')) {
            const href = a.getAttribute('href');
            addUrl(href);
            scanText(href);
        }

        for (const element of sourceDocument.querySelectorAll('[data-href], [data-url], [data-link], [onclick]')) {
            for (const attribute of ['data-href', 'data-url', 'data-link', 'onclick']) {
                scanText(element.getAttribute(attribute));
            }
        }

        // Awaryjnie obsługuje linki zapisane w danych komponentu lub skrypcie,
        // zanim frontend Escorti utworzy z nich elementy <a>.
        scanText(sourceDocument.documentElement?.outerHTML || '');

        return [...unique.values()];
    }

    function findEscortiGarsoTable(sourceDocument = document) {
        const heading = [...sourceDocument.querySelectorAll('h1,h2,h3,h4,h5,h6')]
            .find(el => normalizeText(el.textContent).includes('znalezione dyskusje na forum garsoniera'));
        if (!heading) return null;

        let box = heading.parentElement;
        for (let level = 0; level < 7 && box; level++, box = box.parentElement) {
            const table = box.querySelector('table');
            if (table) return table;
        }
        return null;
    }

    function getEscortiGarsoTopicTitles(sourceDocument = document) {
        const table = findEscortiGarsoTable(sourceDocument);
        if (!table) return [];

        const unique = new Map();
        for (const row of table.querySelectorAll('tbody > tr')) {
            const firstCell = row.querySelector('td');
            const title = (firstCell?.querySelector('a')?.textContent || firstCell?.textContent || '')
                .replace(/\s+/g, ' ')
                .trim();
            if (!title) continue;
            const key = normalizeTopicTitleKey(title);
            if (key && !unique.has(key)) unique.set(key, title);
        }
        return [...unique.values()];
    }

    function startMultiProfileFlow(resultKey, profileUrls) {
        if (!resultKey || !profileUrls.length) {
            finishEscortiCheck(resultKey, { status: 'error', message: 'Brak profili do agregacji' });
            return;
        }

        const stateKey = `${resultKey}_multi`;
        GM_setValue(stateKey, {
            profileUrls: [...new Set(profileUrls)],
            index: 0,
            results: [],
            errors: []
        });
        openMultiProfileAtIndex(resultKey, stateKey, 0);
    }

    function openMultiProfileAtIndex(resultKey, stateKey, index) {
        const state = GM_getValue(stateKey, null);
        if (!state || !Array.isArray(state.profileUrls) || !state.profileUrls[index]) {
            finishEscortiCheck(resultKey, { status: 'error', message: 'Błąd stanu agregacji profili' });
            return;
        }

        const u = new URL(state.profileUrls[index]);
        u.searchParams.set('vm_mode', 'check');
        u.searchParams.set('vm_key', resultKey);
        u.searchParams.set('vm_merge', '1');
        u.searchParams.set('vm_state', stateKey);
        location.replace(u.href);
    }

    function continueMultiProfileFlow(resultKey, stateKey, info, errorMessage) {
        const state = GM_getValue(stateKey, null);
        if (!state || !Array.isArray(state.profileUrls)) {
            finishEscortiCheck(resultKey, { status: 'error', message: 'Utracono stan agregacji profili' });
            return;
        }

        if (info) state.results.push(info);
        if (errorMessage) state.errors.push({ index: state.index, message: errorMessage });
        state.index += 1;
        GM_setValue(stateKey, state);

        if (state.index < state.profileUrls.length) {
            openMultiProfileAtIndex(resultKey, stateKey, state.index);
            return;
        }

        const merged = mergeEscortiProfileResults(state.results, state.profileUrls.length, state.errors.length, state.profileUrls);
        GM_deleteValue(stateKey);
        finishEscortiCheck(resultKey, merged);
    }

    function mergeEscortiProfileResults(results, totalProfiles, errorCount = 0, allProfileUrls = []) {
        if (!results.length) {
            return {
                status: 'error',
                profiles: totalProfiles,
                message: 'Nie udało się odczytać żadnego ze znalezionych profili'
            };
        }

        const adMap = new Map();
        const topicMap = new Map();
        let earliestDate = null;
        let earliestTime = null;
        const profileSummaries = [];

        for (const result of results) {
            for (const url of (result.adUrls || [])) {
                const normalized = normalizeEscortiAdUrl(url);
                if (!normalized) continue;
                const id = normalized.match(/\/anons\/(\d+)\.html$/i)?.[1] || normalized;
                adMap.set(id, normalized);
            }

            for (const title of (result.garsoTopicTitles || [])) {
                const key = normalizeTopicTitleKey(title);
                if (key && !topicMap.has(key)) topicMap.set(key, title);
            }

            const time = profileDateToTime(result.creationDate);
            if (time != null && (earliestTime == null || time < earliestTime)) {
                earliestTime = time;
                earliestDate = formatEscortiDate(result.creationDate);
            }

            profileSummaries.push({
                profileUrl: result.profileUrl || null,
                profileName: result.profileName || null,
                creationDate: result.creationDate || null,
                currentCity: result.currentCity || null,
                adLinks: Number.isFinite(Number(result.adLinks))
                    ? Number(result.adLinks)
                    : null,
                cityHistory: Array.isArray(result.cityHistory)
                    ? result.cityHistory.map(entry => ({
                        date: formatEscortiDate(entry?.date) || null,
                        city: normalizeEscortCity(entry?.city) || null
                    })).filter(entry => entry.date && entry.city)
                    : []
            });
        }

        const profileUrls = [...new Set(
            (allProfileUrls.length ? allProfileUrls : results.map(r => r.profileUrl))
                .filter(Boolean)
                .map(value => {
                    try {
                        const u = new URL(value, ESCORTI_BASE_URL);
                        u.search = '';
                        u.hash = '';
                        return u.href;
                    } catch (_) {
                        return null;
                    }
                })
                .filter(Boolean)
        )];

        return {
            status: 'ok',
            profiles: totalProfiles,
            profileUrls,
            merged: totalProfiles > 1,
            partial: errorCount > 0,
            profileResultsRead: results.length,
            profileErrors: errorCount,
            adUrls: [...adMap.values()],
            adLinks: adMap.size,
            profileSummaries,
            cityHistory: profileSummaries.flatMap(profile =>
                profile.cityHistory.map(entry => ({
                    ...entry,
                    profileUrl: profile.profileUrl
                }))
            ),
            garsoTopicTitles: [...topicMap.values()],
            garsoTopics: topicMap.size,
            creationDate: earliestDate
        };
    }

    function finishEscortiCheck(resultKey, result) {
        if (resultKey) GM_setValue(resultKey, { ...result, timestamp: Date.now() });
        if (window.top === window.self) setTimeout(() => { try { window.close(); } catch (_) {} }, 300);
    }

    function removeVmParamsFromCurrentUrl() {
        const url = new URL(location.href);
        ['vm_mode','vm_key','vm_boot','vm_merge','vm_state'].forEach(k => url.searchParams.delete(k));
        history.replaceState({}, document.title, url.href);
    }

    async function fetchEscortiDocumentDirect(url, cancelToken = null) {
        const response = await gmRequest({
            method: 'GET',
            url,
            cancelToken,
            headers: {
                Accept: 'text/html,application/xhtml+xml'
            }
        });
        const status = Number(response.status) || 0;
        const html = String(response.responseText || '');

        if ((status && (status < 200 || status >= 400)) || !html) {
            throw new Error(status ? `HTTP ${status}` : 'Pusta odpowiedź Escorti');
        }

        return {
            doc: new DOMParser().parseFromString(html, 'text/html'),
            finalUrl: response.finalUrl || url
        };
    }

    async function searchEscortiProfilesDirect(searchValue, cancelToken = null) {
        const searchUrl =
            `${ESCORTI_BASE_URL}search?search=${encodeURIComponent(searchValue)}`;
        const { doc, finalUrl } = await fetchEscortiDocumentDirect(
            searchUrl,
            cancelToken
        );
        const profileUrls = getEscortiProfileUrls(doc, finalUrl);
        const declaredProfileCount = getDeclaredProfileCount(doc);

        if (profileUrls.length > 0) {
            return {
                status: 'ok',
                profiles: profileUrls.length,
                profileUrls
            };
        }

        if (declaredProfileCount === 0) {
            return {
                status: 'ok',
                profiles: 0,
                profileUrls: []
            };
        }

        recordParserIssue(
            'Escorti.pl - wyniki wyszukiwania',
            'Strona nie zawiera rozpoznanej listy profili ani jednoznacznego licznika zero.'
        );
        throw new Error('Nie udało się odczytać wyników wyszukiwania Escorti');
    }

    async function readEscortiProfileDirect(profileUrl, cancelToken = null) {
        const { doc, finalUrl } = await fetchEscortiDocumentDirect(
            profileUrl,
            cancelToken
        );
        const info = readEscortiProfileInfo(doc, finalUrl);
        if (!info) {
            recordParserIssue(
                'Escorti.pl - profil',
                'Nie rozpoznano danych profilu w pobranym dokumencie.'
            );
            throw new Error('Nie udało się odczytać danych profilu Escorti');
        }

        const normalizedProfileUrl = new URL(finalUrl, ESCORTI_BASE_URL);
        normalizedProfileUrl.search = '';
        normalizedProfileUrl.hash = '';
        info.profileUrl = normalizedProfileUrl.href;
        return info;
    }

    async function checkEscortiDirect(
        searchValue,
        { mergeProfiles = false, cancelToken = null } = {}
    ) {
        cancelToken?.throwIfCancelled();
        const searchResult = await searchEscortiProfilesDirect(
            searchValue,
            cancelToken
        );
        if (searchResult.profiles === 0) return searchResult;

        if (mergeProfiles) {
            const results = [];
            const errors = [];

            for (const profileUrl of searchResult.profileUrls) {
                cancelToken?.throwIfCancelled();
                try {
                    results.push(await readEscortiProfileDirect(
                        profileUrl,
                        cancelToken
                    ));
                } catch (error) {
                    if (isOperationCancelledError(error)) throw error;
                    errors.push(error);
                }
            }

            return mergeEscortiProfileResults(
                results,
                searchResult.profiles,
                errors.length,
                searchResult.profileUrls
            );
        }

        if (searchResult.profiles === 1) {
            const info = await readEscortiProfileDirect(
                searchResult.profileUrls[0],
                cancelToken
            );
            return {
                status: 'ok',
                profiles: 1,
                profileUrl: info.profileUrl,
                profileUrls: [info.profileUrl],
                adLinks: info.adLinks,
                adUrls: info.adUrls,
                creationDate: info.creationDate,
                currentCity: info.currentCity,
                cityHistory: info.cityHistory,
                profileSummaries: [{
                    profileUrl: info.profileUrl,
                    profileName: info.profileName,
                    creationDate: info.creationDate,
                    currentCity: info.currentCity,
                    adLinks: info.adLinks,
                    cityHistory: info.cityHistory
                }],
                garsoTopics: info.garsoTopics,
                garsoTopicTitles: info.garsoTopicTitles
            };
        }

        return searchResult;
    }

    // ============================================================
    // LISTA ESCORT.CLUB - SPRAWDZANIE ESCORTI + CACHE
    // ============================================================

    function checkEscortiBackground(
        searchValue,
        {
            timeoutMs = 60000,
            mergeProfiles = false,
            preferDirect = false,
            cancelToken = null
        } = {}
    ) {
        cancelToken?.throwIfCancelled();
        // Pobranie HTML przez GM_xmlhttpRequest jest znacznie lżejsze niż
        // uruchamianie całej aplikacji Escorti w ukrytym iframe. Iframe pozostaje
        // zabezpieczeniem dla stron, których nie uda się odczytać bezpośrednio.
        if (preferDirect) {
            return checkEscortiDirect(searchValue, { mergeProfiles, cancelToken })
                .then(result => {
                    if (result?.status === 'ok') return result;
                    return checkEscortiBackground(searchValue, {
                        timeoutMs,
                        mergeProfiles,
                        preferDirect: false,
                        cancelToken
                    });
                })
                .catch(error => {
                    if (isOperationCancelledError(error)) throw error;
                    return checkEscortiBackground(searchValue, {
                        timeoutMs,
                        mergeProfiles,
                        preferDirect: false,
                        cancelToken
                    });
                });
        }

        return new Promise(resolve => {
            const randomId = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
            const resultKey = `vm_escorti_result_list_${randomId}`;
            const bootKey = `vm_escorti_boot_list_${randomId}`;
            const multiStateKey = `${resultKey}_multi`;

            GM_deleteValue(resultKey);
            GM_deleteValue(bootKey);
            GM_deleteValue(multiStateKey);

            let finished = false;
            let iframeStarted = false;
            let fallbackStarted = false;
            let iframe = null;
            let fallbackTimer = null;
            let totalTimer = null;
            let unsubscribeCancel = () => {};

            function cleanup() {
                if (fallbackTimer) clearTimeout(fallbackTimer);
                if (totalTimer) clearTimeout(totalTimer);
                unsubscribeCancel();
                try { iframe?.remove(); } catch (_) {}
                try { GM_removeValueChangeListener(resultListenerId); } catch (_) {}
                try { GM_removeValueChangeListener(bootListenerId); } catch (_) {}
                GM_deleteValue(resultKey);
                GM_deleteValue(bootKey);
                GM_deleteValue(multiStateKey);
            }

            function finish(result) {
                if (finished) return;
                finished = true;
                cleanup();
                resolve(result);
            }

            const resultListenerId = GM_addValueChangeListener(resultKey, (_n, _o, newValue) => {
                if (!newValue || finished) return;
                finish(newValue);
            });

            const bootListenerId = GM_addValueChangeListener(bootKey, (_n, _o, newValue) => {
                if (!newValue || finished) return;
                iframeStarted = true;
                if (fallbackTimer) {
                    clearTimeout(fallbackTimer);
                    fallbackTimer = null;
                }
            });

            const mergeParam = mergeProfiles ? '&vm_merge=1' : '';
            const iframeUrl = `${ESCORTI_BASE_URL}search?search=${encodeURIComponent(searchValue)}&vm_mode=check&vm_key=${encodeURIComponent(resultKey)}&vm_boot=${encodeURIComponent(bootKey)}${mergeParam}`;

            iframe = makeElement('iframe');
            iframe.src = iframeUrl;
            iframe.setAttribute('aria-hidden', 'true');
            iframe.tabIndex = -1;
            Object.assign(iframe.style, {
                position: 'fixed',
                left: '-10000px',
                top: '-10000px',
                width: '1px',
                height: '1px',
                opacity: '0',
                pointerEvents: 'none',
                border: '0',
                zIndex: '-999999'
            });
            document.body.appendChild(iframe);

            unsubscribeCancel = cancelToken?.onCancel(() => {
                finish({ status: 'cancelled' });
            }) || (() => {});

            fallbackTimer = setTimeout(() => {
                if (finished || iframeStarted || fallbackStarted) return;
                fallbackStarted = true;
                try { iframe?.remove(); iframe = null; } catch (_) {}
                checkEscortiDirect(searchValue, { mergeProfiles, cancelToken })
                    .then(finish)
                    .catch(error => finish(isOperationCancelledError(error)
                        ? { status: 'cancelled' }
                        : {
                            status: 'error',
                            message: error?.message || 'Nie udało się odczytać Escorti'
                        }));
            }, 4000);

            totalTimer = setTimeout(() => finish({ status: 'timeout' }), timeoutMs);
        });
    }

    function checkEscortiProfileBackground(profileUrl, { timeoutMs = 60000 } = {}) {
        return new Promise(resolve => {
            const randomId = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
            const resultKey = `vm_escorti_result_watch_${randomId}`;
            const bootKey = `vm_escorti_boot_watch_${randomId}`;

            GM_deleteValue(resultKey);
            GM_deleteValue(bootKey);

            let finished = false;
            let iframeStarted = false;
            let fallbackStarted = false;
            let iframe = null;
            let fallbackTimer = null;
            let totalTimer = null;

            function cleanup() {
                if (fallbackTimer) clearTimeout(fallbackTimer);
                if (totalTimer) clearTimeout(totalTimer);
                try { iframe?.remove(); } catch (_) {}
                try { GM_removeValueChangeListener(resultListenerId); } catch (_) {}
                try { GM_removeValueChangeListener(bootListenerId); } catch (_) {}
                GM_deleteValue(resultKey);
                GM_deleteValue(bootKey);
            }

            function finish(result) {
                if (finished) return;
                finished = true;
                cleanup();
                resolve(result);
            }

            const resultListenerId = GM_addValueChangeListener(
                resultKey,
                (_name, _oldValue, newValue) => {
                    if (newValue && !finished) finish(newValue);
                }
            );
            const bootListenerId = GM_addValueChangeListener(
                bootKey,
                (_name, _oldValue, newValue) => {
                    if (!newValue || finished) return;
                    iframeStarted = true;
                    if (fallbackTimer) {
                        clearTimeout(fallbackTimer);
                        fallbackTimer = null;
                    }
                }
            );

            let iframeUrl;
            try {
                const url = new URL(profileUrl, ESCORTI_BASE_URL);
                url.searchParams.set('vm_mode', 'check');
                url.searchParams.set('vm_key', resultKey);
                url.searchParams.set('vm_boot', bootKey);
                iframeUrl = url.href;
            } catch (_) {
                finish({ status: 'error', message: 'Nieprawidłowy adres profilu Escorti' });
                return;
            }

            iframe = makeElement('iframe');
            iframe.src = iframeUrl;
            iframe.setAttribute('aria-hidden', 'true');
            iframe.tabIndex = -1;
            Object.assign(iframe.style, {
                position: 'fixed',
                left: '-10000px',
                top: '-10000px',
                width: '1px',
                height: '1px',
                opacity: '0',
                pointerEvents: 'none',
                border: '0',
                zIndex: '-999999'
            });
            document.body.appendChild(iframe);

            fallbackTimer = setTimeout(() => {
                if (finished || iframeStarted || fallbackStarted) return;
                fallbackStarted = true;
                try { iframe?.remove(); iframe = null; } catch (_) {}
                readEscortiProfileDirect(profileUrl)
                    .then(info => finish({
                        status: 'ok',
                        profiles: 1,
                        profileUrl: info.profileUrl,
                        profileUrls: [info.profileUrl],
                        adLinks: info.adLinks,
                        adUrls: info.adUrls,
                        profileName: info.profileName,
                        imageUrl: info.imageUrl,
                        creationDate: info.creationDate,
                        currentCity: info.currentCity,
                        cityHistory: info.cityHistory,
                        profileSummaries: [{
                            profileUrl: info.profileUrl,
                            profileName: info.profileName,
                            creationDate: info.creationDate,
                            currentCity: info.currentCity,
                            adLinks: info.adLinks,
                            cityHistory: info.cityHistory
                        }],
                        garsoTopics: info.garsoTopics,
                        garsoTopicTitles: info.garsoTopicTitles
                    }))
                    .catch(error => finish({
                        status: 'error',
                        message: error?.message || 'Nie udało się odczytać profilu Escorti'
                    }));
            }, 4000);

            totalTimer = setTimeout(() => finish({ status: 'timeout' }), timeoutMs);
        });
    }

    function listCacheKey(adId) {
        return `vm_escorti_list_${adId}`;
    }

    function getEscortiProfileId(value) {
        try {
            const url = new URL(value, ESCORTI_BASE_URL);
            return url.pathname.match(/^\/escort\/(\d+)\/?$/i)?.[1] || null;
        } catch (_) {
            return null;
        }
    }

    function buildEscortiProfileUrl(profileId) {
        return `${ESCORTI_BASE_URL}escort/${profileId}/`;
    }

    function normalizeProfileUrls(result) {
        const values = Array.isArray(result?.profileUrls)
            ? result.profileUrls
            : (result?.profileUrl ? [result.profileUrl] : []);
        const unique = new Set();

        for (const value of values) {
            try {
                const url = new URL(value, ESCORTI_BASE_URL);
                if (
                    !['escorti.pl', 'www.escorti.pl'].includes(url.hostname.toLowerCase()) ||
                    !/^\/escort\/\d+\/?$/i.test(url.pathname)
                ) {
                    continue;
                }
                url.protocol = 'https:';
                url.hostname = 'escorti.pl';
                url.pathname = `${url.pathname.replace(/\/+$/, '')}/`;
                url.search = '';
                url.hash = '';
                unique.add(url.href);
            } catch (_) {}
        }

        return [...unique];
    }

    function getCachedEscortiAdUrlsForProfiles(profileUrls) {
        if (!SETTINGS.usePersistentCache) return [];

        const wantedProfileIds = new Set(
            (Array.isArray(profileUrls) ? profileUrls : [])
                .map(getEscortiProfileId)
                .filter(Boolean)
        );
        if (!wantedProfileIds.size) return [];

        const adIds = new Set();
        try {
            const sharedIndex = GM_getValue(ESCORTI_PROFILE_AD_INDEX_STORAGE_KEY, null);
            for (const profileId of wantedProfileIds) {
                const entry = sharedIndex?.profiles?.[profileId];
                for (const adId of Array.isArray(entry?.adIds) ? entry.adIds : []) {
                    const normalizedId = String(adId || '').trim();
                    if (/^\d+$/.test(normalizedId)) adIds.add(normalizedId);
                }
            }
        } catch (_) {}

        const listKeyPattern = /^vm_escorti_list_(\d+)$/i;

        for (const key of getEscortListCacheKeys()) {
            if (!listKeyPattern.test(key)) continue;

            try {
                const store = readPersistentCacheValue(key, null);
                const modes = store?.modes && typeof store.modes === 'object'
                    ? Object.values(store.modes)
                    : [];

                for (const entry of modes) {
                    const entryProfileIds = [...new Set(
                        (Array.isArray(entry?.profileIds) ? entry.profileIds : [])
                            .map(value => String(value || '').trim())
                            .filter(value => /^\d+$/.test(value))
                    )];

                    // Przy wyniku obejmującym kilka profili nie da się pewnie
                    // przypisać poszczególnych anonsów do jednego z nich.
                    if (entryProfileIds.length !== 1 || !wantedProfileIds.has(entryProfileIds[0])) {
                        continue;
                    }

                    for (const adId of Array.isArray(entry?.adIds) ? entry.adIds : []) {
                        const normalizedId = String(adId || '').trim();
                        if (/^\d+$/.test(normalizedId)) adIds.add(normalizedId);
                    }
                }
            } catch (_) {}
        }

        return [...adIds].map(id => `https://pl.escort.club/anons/${id}.html`);
    }

    function compactEscortListResult(result) {
        const profileUrls = Array.isArray(result?.profileUrls)
            ? result.profileUrls
            : (result?.profileUrl ? [result.profileUrl] : []);
        const profileIds = [...new Set(profileUrls.map(getEscortiProfileId).filter(Boolean))];
        const adIds = [...new Set(
            (Array.isArray(result?.adUrls) ? result.adUrls : [])
                .map(parseAdIdFromUrl)
                .filter(Boolean)
        )];
        const rawProfileSummaries = Array.isArray(result?.profileSummaries)
            ? result.profileSummaries
            : (profileIds.length === 1 ? [{
                profileUrl: profileUrls[0],
                profileName: result?.profileName || null,
                creationDate: result?.creationDate || null,
                currentCity: result?.currentCity || null,
                adLinks: result?.adLinks,
                cityHistory: result?.cityHistory || []
            }] : []);
        const profileSummaries = rawProfileSummaries.map(profile => ({
            profileId: getEscortiProfileId(profile?.profileUrl),
            profileName: normalizeEscortAdText(profile?.profileName) || null,
            creationDate: formatEscortiDate(profile?.creationDate) || null,
            currentCity: normalizeEscortCity(profile?.currentCity) || null,
            adLinks: Number.isFinite(Number(profile?.adLinks))
                ? Number(profile.adLinks)
                : null,
            cityHistory: (Array.isArray(profile?.cityHistory) ? profile.cityHistory : [])
                .map(entry => ({
                    date: formatEscortiDate(entry?.date) || null,
                    city: normalizeEscortCity(entry?.city) || null
                }))
                .filter(entry => entry.date && entry.city)
        })).filter(profile => profile.profileId);

        return {
            profileIds,
            adIds,
            creationDate: result?.creationDate || null,
            profileSummaries,
            garsoTopics: Number.isFinite(result?.garsoTopics)
                ? result.garsoTopics
                : null,
            partial: !!result?.partial,
            checkedAt: Date.now()
        };
    }

    function expandEscortListCacheEntry(entry, searchMode) {
        if (!entry || typeof entry !== 'object' || !entry.checkedAt) return null;

        const profileIds = [...new Set(
            (Array.isArray(entry.profileIds) ? entry.profileIds : [])
                .map(value => String(value || '').trim())
                .filter(value => /^\d+$/.test(value))
        )];
        const adIds = [...new Set(
            (Array.isArray(entry.adIds) ? entry.adIds : [])
                .map(value => String(value || '').trim())
                .filter(value => /^\d+$/.test(value))
        )];
        const profileUrls = profileIds.map(buildEscortiProfileUrl);
        const adUrls = adIds.map(id => `https://pl.escort.club/anons/${id}.html`);
        const profileSummaries = (Array.isArray(entry.profileSummaries)
            ? entry.profileSummaries
            : [])
            .map(profile => {
                const profileId = String(profile?.profileId || '').trim();
                if (!/^\d+$/.test(profileId)) return null;
                return {
                    profileUrl: buildEscortiProfileUrl(profileId),
                    profileName: normalizeEscortAdText(profile?.profileName) || null,
                    creationDate: formatEscortiDate(profile?.creationDate) || null,
                    currentCity: normalizeEscortCity(profile?.currentCity) || null,
                    adLinks: Number.isFinite(Number(profile?.adLinks))
                        ? Number(profile.adLinks)
                        : null,
                    cityHistory: (Array.isArray(profile?.cityHistory)
                        ? profile.cityHistory
                        : []).map(history => ({
                            date: formatEscortiDate(history?.date) || null,
                            city: normalizeEscortCity(history?.city) || null
                        })).filter(history => history.date && history.city)
                };
            })
            .filter(Boolean);

        return {
            status: 'ok',
            searchMode,
            profileIds,
            profileUrls,
            profiles: profileIds.length,
            merged: profileIds.length > 1,
            partial: !!entry.partial,
            adIds,
            adUrls,
            adLinks: adIds.length,
            creationDate: entry.creationDate || null,
            profileSummaries,
            currentCity: profileSummaries.length === 1
                ? profileSummaries[0].currentCity
                : null,
            cityHistory: profileSummaries.flatMap(profile =>
                profile.cityHistory.map(history => ({
                    ...history,
                    profileUrl: profile.profileUrl
                }))
            ),
            garsoTopics: Number.isFinite(entry.garsoTopics)
                ? entry.garsoTopics
                : null,
            checkedAt: Number(entry.checkedAt)
        };
    }

    function getListCache(adId, searchMode = getEscortListSearchMode()) {
        if (!SETTINGS.usePersistentCache) return null;

        try {
            const store = readPersistentCacheValue(listCacheKey(adId), null);
            return expandEscortListCacheEntry(store?.modes?.[searchMode], searchMode);
        } catch (_) {
            return null;
        }
    }

    function setListCache(adId, result, searchMode = getEscortListSearchMode()) {
        if (!SETTINGS.usePersistentCache || result?.status !== 'ok') return;

        try {
            const existing = readPersistentCacheValue(listCacheKey(adId), null);
            const modes = existing?.modes && typeof existing.modes === 'object'
                ? { ...existing.modes }
                : {};

            modes[searchMode] = compactEscortListResult(result);
            writePersistentCacheValue(listCacheKey(adId), { modes });
            scheduleEscortiProfileAdIndexRebuild();
        } catch (e) {
            log('Nie udało się zapisać cache listy', e);
        }
    }

    function isListCacheFresh(cache) {
        return !!(cache && cache.checkedAt && Date.now() - cache.checkedAt < getListCacheTtlMs());
    }

    function parseAdIdFromUrl(url) {
        const m = String(url || '').match(/\/anons\/(\d+)\.html/i);
        return m ? m[1] : null;
    }

    function addEscortiOpenButton(card, adUrl) {
        let btn = card.querySelector('.vm-escorti-open-button');
        if (btn) return btn;

        btn = makeElement('button');
        btn.type = 'button';
        btn.className = 'vm-escorti-open-button';
        btn.textContent = 'escorti.pl';
        btn.title = 'Otwórz wyszukiwanie tego anonsu w Escorti.pl';

        // Przycisk jest osobnym elementem kafelka, nie częścią linku do anonsu.
        // Dzięki temu kliknięcie nie otwiera jednocześnie Escort.club.
        if (getComputedStyle(card).position === 'static') {
            card.style.position = 'relative';
        }

        Object.assign(btn.style, {
            position: 'absolute',
            top: '4px',
            right: '2px',
            zIndex: '20',
            padding: '2px 5px',
            border: '1px solid rgba(255,255,255,.65)',
            borderRadius: '4px',
            background: 'rgba(20,20,20,.72)',
            color: '#ffffff',
            fontSize: '9px',
            lineHeight: '1.2',
            fontWeight: '700',
            cursor: 'pointer',
            boxShadow: '0 1px 3px rgba(0,0,0,.35)'
        });

        btn.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();

            // Celowo bez vm_mode=open: przycisk ma pozostać na stronie
            // wyników wyszukiwania Escorti, nawet gdy znaleziono tylko jeden profil.
            const url =
                `${ESCORTI_BASE_URL}` +
                `search?search=${encodeURIComponent(adUrl)}`;

            GM_openInTab(url, {
                active: true,
                insert: true
            });
        });

        card.appendChild(btn);
        return btn;
    }

    function getEscortProfileKeys(result) {
        if (!result || result.status !== 'ok' || result.profiles === 0) return [];

        const urls = Array.isArray(result.profileUrls)
            ? result.profileUrls
            : (result.profileUrl ? [result.profileUrl] : []);

        const keys = new Set();
        for (const value of urls) {
            try {
                const u = new URL(value, ESCORTI_BASE_URL);
                const m = u.pathname.match(/^\/escort\/(\d+)\/?$/i);
                if (m) keys.add(`escort:${m[1]}`);
                else keys.add(`${u.hostname.toLowerCase()}${u.pathname.replace(/\/$/, '')}`);
            } catch (_) {}
        }
        return [...keys];
    }

    function buildEscortListCacheIndex() {
        const index = {
            byAdId: new Map(),
            freshProfileToAdIds: new Map(),
            adIdToFreshProfileKeys: new Map()
        };

        if (!SETTINGS.usePersistentCache) {
            return index;
        }

        const listKeyPattern = /^vm_escorti_list_(\d+)$/i;
        const requiredSearchMode = getEscortListSearchMode();

        for (const key of getEscortListCacheKeys()) {
            const match = key.match(listKeyPattern);
            if (!match) continue;

            const adId = match[1];

            try {
                const store = readPersistentCacheValue(key, null);
                const cache = expandEscortListCacheEntry(
                    store?.modes?.[requiredSearchMode],
                    requiredSearchMode
                );
                if (!cache) continue;

                const relatedAdIds = [...new Set([
                    adId,
                    ...(Array.isArray(cache.adIds) ? cache.adIds : [])
                ].map(String).filter(value => /^\d+$/.test(value)))];

                for (const relatedAdId of relatedAdIds) {
                    const existing = index.byAdId.get(relatedAdId);
                    if (!existing || Number(cache.checkedAt) > Number(existing.checkedAt || 0)) {
                        index.byAdId.set(relatedAdId, cache);
                    }
                }

                if (!isListCacheFresh(cache)) continue;

                const profileKeys = getEscortProfileKeys(cache);
                if (!profileKeys.length) continue;

                for (const relatedAdId of relatedAdIds) {
                    const relatedKeys = index.adIdToFreshProfileKeys.get(relatedAdId) || new Set();
                    for (const profileKey of profileKeys) {
                        relatedKeys.add(profileKey);
                        if (!index.freshProfileToAdIds.has(profileKey)) {
                            index.freshProfileToAdIds.set(profileKey, new Set());
                        }
                        index.freshProfileToAdIds.get(profileKey).add(relatedAdId);
                    }
                    index.adIdToFreshProfileKeys.set(relatedAdId, relatedKeys);
                }
            } catch (_) {}
        }

        return index;
    }

    function removeAdFromFreshCacheProfileIndex(index, adId) {
        const oldKeys = index.adIdToFreshProfileKeys.get(adId);
        if (!oldKeys) return;

        for (const profileKey of oldKeys) {
            const ids = index.freshProfileToAdIds.get(profileKey);
            if (!ids) continue;

            ids.delete(adId);
            if (!ids.size) {
                index.freshProfileToAdIds.delete(profileKey);
            }
        }

        index.adIdToFreshProfileKeys.delete(adId);
    }

    function updateEscortListCacheIndex(index, adId, result) {
        const relatedAdIds = [...new Set([
            String(adId || ''),
            ...(Array.isArray(result?.adIds) ? result.adIds : [])
        ].map(String).filter(value => /^\d+$/.test(value)))];

        for (const relatedAdId of relatedAdIds) {
            removeAdFromFreshCacheProfileIndex(index, relatedAdId);
        }

        if (!SETTINGS.usePersistentCache || !result || result.status !== 'ok') {
            return;
        }

        for (const relatedAdId of relatedAdIds) {
            index.byAdId.set(relatedAdId, result);
        }

        const profileKeys = getEscortProfileKeys(result);
        if (!profileKeys.length) return;

        for (const relatedAdId of relatedAdIds) {
            index.adIdToFreshProfileKeys.set(relatedAdId, new Set(profileKeys));
            for (const profileKey of profileKeys) {
                if (!index.freshProfileToAdIds.has(profileKey)) {
                    index.freshProfileToAdIds.set(profileKey, new Set());
                }
                index.freshProfileToAdIds.get(profileKey).add(relatedAdId);
            }
        }
    }

    function normalizeEscortCachedPhone(value) {
        const digits = digitsOnly(value);
        // Polski numer jest już skracany przez digitsOnly() do 9 cyfr.
        // Numery zagraniczne zachowujemy wraz z kodem kraju. Zakres 8–15 cyfr
        // obejmuje prawidłowe numery międzynarodowe i odrzuca maski typu
        // „+595 992-...” widoczne przed odsłonięciem pełnego telefonu.
        return digits.length >= 8 && digits.length <= 15 ? digits : null;
    }

    function buildEscortAdPhoneCacheIndex() {
        const index = {
            phoneToAdIds: new Map(),
            adIdToPhone: new Map()
        };

        if (!SETTINGS.usePersistentCache) return index;

        const adDataKeyPattern = /^vm_escort_ad_data_(\d+)$/i;

        for (const key of getEscortAdDataCacheKeys()) {
            const match = key.match(adDataKeyPattern);
            if (!match) continue;

            const adId = match[1];
            try {
                const cache = readPersistentCacheValue(key, null);
                if (
                    !cache ||
                    cache.status !== 'ok' ||
                    !cache.checkedAt ||
                    Date.now() - cache.checkedAt >= getListCacheTtlMs()
                ) {
                    continue;
                }

                const phone = normalizeEscortCachedPhone(cache.phoneDigits || cache.phone);
                if (!phone) continue;

                index.adIdToPhone.set(adId, phone);
                if (!index.phoneToAdIds.has(phone)) {
                    index.phoneToAdIds.set(phone, new Set());
                }
                index.phoneToAdIds.get(phone).add(adId);
            } catch (_) {}
        }

        return index;
    }

    function updateEscortAdPhoneCacheIndex(index, adId, adData) {
        const oldPhone = index.adIdToPhone.get(adId);
        if (oldPhone) {
            const oldIds = index.phoneToAdIds.get(oldPhone);
            oldIds?.delete(adId);
            if (oldIds && !oldIds.size) index.phoneToAdIds.delete(oldPhone);
            index.adIdToPhone.delete(adId);
        }

        const phone = adData?.status === 'ok'
            ? normalizeEscortCachedPhone(adData.phoneDigits || adData.phone)
            : null;
        if (!phone) return;

        index.adIdToPhone.set(adId, phone);
        if (!index.phoneToAdIds.has(phone)) {
            index.phoneToAdIds.set(phone, new Set());
        }
        index.phoneToAdIds.get(phone).add(adId);
    }

    function polishAdWord(n) {
        if (n === 1) return 'anons';
        const last = n % 10;
        const lastTwo = n % 100;
        if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) return 'anonse';
        return 'anonsów';
    }

    function getEscortListCardAgeAndCity(card) {
        const text = normalizeEscortAdText(card?.querySelector('.item-stats')?.textContent);
        const match = text.match(/^(.*?),\s*(\d{1,3})\s*y(?:\b|$)/i);

        return {
            city: match?.[1] ? normalizeText(match[1]) : null,
            age: match?.[2] ? Number(match[2]) : null
        };
    }

    function getEscortListGroupDataDifferences(members, cachedOnlyAdData = []) {
        const cities = new Set();
        const ages = new Set();
        const statValues = new Map();
        const selectedPriceValues = new Set();
        const selectedPriceDuration = normalizeEscortPriceDuration(
            SETTINGS.searchResultPriceDuration
        );

        function collectAdData(adData, preferredCity = null, preferredAge = null) {
            const stats = adData?.stats && typeof adData.stats === 'object'
                ? adData.stats
                : null;

            const city = normalizeText(preferredCity || adData?.location?.city);
            if (city) cities.add(city);

            let age = preferredAge;
            if (!Number.isFinite(age) && stats) {
                const ageEntry = Object.entries(stats).find(([label]) =>
                    normalizeEscortChangeComparisonValue(label).replace(/:\s*$/, '') === 'wiek'
                );
                const ageMatch = normalizeEscortAdText(ageEntry?.[1]).match(/\d{1,3}/);
                age = ageMatch ? Number(ageMatch[0]) : null;
            }
            if (Number.isFinite(age)) ages.add(age);

            if (!adData || adData.status !== 'ok') return;

            for (const [rawLabel, rawValue] of Object.entries(stats || {})) {
                const label = normalizeEscortChangeComparisonValue(rawLabel)
                    .replace(/:\s*$/, '');

                // Wiek ma własne ostrzeżenie, a historyczne wpisy cenowe w
                // stats nie mogą dublować porównania wykonywanego z prices.
                if (
                    !label ||
                    label === 'wiek' ||
                    normalizeEscortClubPriceDurationKey(label)
                ) {
                    continue;
                }

                const value = normalizeEscortChangeComparisonValue(rawValue);
                if (!value) continue;

                if (!statValues.has(label)) statValues.set(label, new Set());
                statValues.get(label).add(value);
            }

            const price = adData.prices?.[selectedPriceDuration];
            const hasAmount = price?.amount != null &&
                Number.isFinite(Number(price.amount));
            const currency = normalizeEscortAdText(price?.currency).toUpperCase();
            selectedPriceValues.add(
                hasAmount
                    ? `${Number(price.amount)}|${currency}`
                    : '__brak__'
            );
        }

        for (const member of members || []) {
            const cardData = getEscortListCardAgeAndCity(member?.card);
            const adData = member?.adData?.status === 'ok' ? member.adData : null;
            collectAdData(adData, cardData.city, cardData.age);
        }

        // Rekordy bez kafelków uwzględniamy tylko wtedy, gdy dane pojedynczego
        // anonse są już zapisane w cache. Ta funkcja niczego nie pobiera.
        for (const adData of cachedOnlyAdData || []) {
            if (adData?.status === 'ok') collectAdData(adData);
        }

        const stats = [...statValues.entries()]
            .filter(([, values]) => values.size > 1)
            .map(([label]) => label);

        return {
            city: cities.size > 1,
            age: ages.size > 1,
            stats,
            price: selectedPriceValues.size > 1
        };
    }

    function getListAggregateLine(line) {
        if (!line?.parentElement) return null;

        let aggregateLine = line.parentElement.querySelector('.item-escorti-aggregate');
        if (aggregateLine) return aggregateLine;

        aggregateLine = makeElement('span');
        aggregateLine.className = 'item-escorti-aggregate';
        Object.assign(aggregateLine.style, {
            display: 'none',
            width: 'fit-content',
            maxWidth: '100%',
            marginTop: '2px',
            padding: '2px 4px',
            border: '1px solid rgba(255,255,255,.68)',
            borderRadius: '4px',
            background: 'rgba(0,0,0,.18)',
            boxSizing: 'border-box',
            fontSize: '9px',
            lineHeight: '1.15',
            fontWeight: '600',
            color: '#ffffff',
            whiteSpace: 'normal',
            textShadow: '0 1px 2px rgba(0,0,0,.75)'
        });

        aggregateLine.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();
            if (typeof aggregateLine._vmToggleHandler === 'function') {
                aggregateLine._vmToggleHandler();
            }
        });

        line.insertAdjacentElement('afterend', aggregateLine);
        return aggregateLine;
    }

    function setListMergedInfo(
        line,
        activeCount,
        expanded = false,
        onToggle = null,
        cachedOnlyCount = 0,
        dataDifferences = null
    ) {
        if (!line) return;

        const count = Number(activeCount) || 0;
        const cacheCount = Math.max(0, Number(cachedOnlyCount) || 0);
        const listCount = Math.max(0, count - cacheCount);
        const aggregateLine = getListAggregateLine(line);
        if (!aggregateLine) return;

        aggregateLine._vmToggleHandler = typeof onToggle === 'function' ? onToggle : null;

        if (count > 1) {
            const arrow = onToggle ? ` ${expanded ? '▲' : '▼'}` : '';

            const topLine = makeElement('span', '', `Zagregowano ${listCount} ${polishAdWord(listCount)}${arrow}`);
            Object.assign(topLine.style, {
                display: 'block',
                fontWeight: '700',
                whiteSpace: 'nowrap'
            });

            aggregateLine.replaceChildren(topLine);

            if (cacheCount > 0) {
                const lastTwoDigits = cacheCount % 100;
                const lastDigit = cacheCount % 10;
                const hiddenWord = (
                    cacheCount === 1 ||
                    (lastDigit >= 2 && lastDigit <= 4 && (lastTwoDigits < 12 || lastTwoDigits > 14))
                ) ? 'niewidoczne' : 'niewidocznych';
                const sourceLine = makeElement('span', '', `(+${cacheCount} ${hiddenWord} w tych wynikach)`);
                Object.assign(sourceLine.style, {
                    display: 'block',
                    marginTop: '1px',
                    fontSize: '8px',
                    fontWeight: '500',
                    whiteSpace: 'nowrap',
                    opacity: '.92'
                });
                aggregateLine.appendChild(sourceLine);
            }

            const differenceFields = [];
            if (dataDifferences?.age) differenceFields.push('wiek');
            if (dataDifferences?.city) differenceFields.push('miasto');

            const differingStats = Array.isArray(dataDifferences?.stats)
                ? dataDifferences.stats
                : [];
            if (differingStats.length > 2) differenceFields.push('wymiary');
            else differenceFields.push(...differingStats);

            if (dataDifferences?.price) differenceFields.push('cena');

            if (differenceFields.length) {
                const warningLine = makeElement('span', '', `⚠ Niezgodne: ${differenceFields.join(', ')}`);
                if (dataDifferences?.price) {
                    warningLine.title =
                        `Porównano cenę za ${getEscortPriceDurationLabel(SETTINGS.searchResultPriceDuration)}.`;
                }
                Object.assign(warningLine.style, {
                    display: 'block',
                    width: 'fit-content',
                    maxWidth: '100%',
                    marginTop: '2px',
                    padding: '1px 3px',
                    borderRadius: '3px',
                    background: '#dc3545',
                    color: '#fff',
                    fontSize: '8px',
                    fontWeight: '700',
                    lineHeight: '1.2',
                    whiteSpace: 'nowrap',
                    textShadow: 'none'
                });
                aggregateLine.appendChild(warningLine);
            }

            aggregateLine.style.display = 'inline-block';
            aggregateLine.style.cursor = onToggle ? 'pointer' : 'default';
            aggregateLine.style.textDecoration = 'none';

            const toggleTitle = onToggle
                ? (expanded ? 'Kliknij, aby ukryć scalone anonse' : 'Kliknij, aby pokazać scalone anonse')
                : '';
            const cacheTitle = cacheCount > 0
                ? `${cacheCount} ${polishAdWord(cacheCount)} ${cacheCount === 1 ? 'nie jest widoczne' : 'nie są widoczne'} w tych wynikach i nie ma kafelków na obecnie załadowanej liście.`
                : '';
            const differencesTitle = differenceFields.length
                ? `Zagregowane kafelki mają różne wartości: ${differenceFields.join(' i ')}.`
                : '';

            aggregateLine.title = [toggleTitle, cacheTitle, differencesTitle]
                .filter(Boolean)
                .join('\n');
        } else {
            aggregateLine.replaceChildren();
            aggregateLine.style.display = 'none';
            aggregateLine.style.cursor = 'default';
            aggregateLine.style.textDecoration = 'none';
            aggregateLine.title = '';
        }
    }

    function getListCardDataLine(card) {
        let line = card.querySelector('.item-escorti-stats');
        if (line) return line;

        const info = card.querySelector('.item-info');
        if (!info) return null;

        line = makeElement('span');
        line.className = 'item-escorti-stats';
        Object.assign(line.style, {
            display: 'block',
            marginTop: '2px',
            fontSize: '9px',
            lineHeight: '1.15',
            fontWeight: '600',
            color: '#ffffff',
            whiteSpace: 'nowrap',
            textShadow: '0 1px 2px rgba(0,0,0,.75)'
        });
        line.textContent = '… ogł • od … • … opinii';
        info.appendChild(line);
        return line;
    }

    function getListCardPriceLine(card) {
        let line = card.querySelector('.vm-escort-tile-price');
        if (line) return line;

        if (getComputedStyle(card).position === 'static') {
            card.style.position = 'relative';
        }

        line = makeElement('span');
        line.className = 'vm-escort-tile-price';
        Object.assign(line.style, {
            display: 'block',
            position: 'absolute',
            top: SETTINGS.showEscortiTileButton ? '22px' : '4px',
            right: '2px',
            zIndex: '20',
            width: 'fit-content',
            maxWidth: '100%',
            margin: '0',
            padding: '2px 5px',
            border: '1px solid rgba(255,255,255,.65)',
            borderRadius: '4px',
            background: 'rgba(20,20,20,.72)',
            color: '#ffffff',
            fontSize: '9px',
            lineHeight: '1.2',
            fontWeight: '700',
            whiteSpace: 'nowrap',
            textShadow: '0 1px 2px rgba(0,0,0,.55)',
            boxSizing: 'border-box',
            boxShadow: '0 1px 3px rgba(0,0,0,.35)',
            pointerEvents: 'none'
        });

        card.appendChild(line);
        return line;
    }

    function formatEscortTilePrice(price) {
        if (
            !price ||
            price.amount == null ||
            price.amount === '' ||
            !Number.isFinite(Number(price.amount))
        ) {
            return '';
        }

        const amount = new Intl.NumberFormat('pl-PL', {
            minimumFractionDigits: 0,
            maximumFractionDigits: 2
        }).format(Number(price.amount));

        const currencyCode = String(price.currency || '').toUpperCase();
        const currency = {
            PLN: 'zł',
            EUR: '€',
            USD: '$',
            GBP: '£'
        }[currencyCode] || price.currency || '';

        return `${amount}${currency ? ` ${currency}` : ''}`;
    }

    function renderEscortListPrice(line, adData, durationValue) {
        if (!line) return;

        const duration = normalizeEscortPriceDuration(durationValue);
        const durationLabel = getEscortPriceDurationLabel(duration);
        const price = adData?.status === 'ok' && adData.prices
            ? adData.prices[duration]
            : null;

        if (!adData) {
            line.textContent = '…';
            line.style.background = 'rgba(20,20,20,.72)';
            line.title = `Trwa pobieranie ceny za ${durationLabel}`;
            return;
        }

        if (adData.status !== 'ok') {
            line.textContent = 'brak danych';
            line.style.background = 'rgba(120,35,45,.88)';
            line.title = 'Nie udało się odczytać cennika z pojedynczego anonsu';
            return;
        }

        const formattedPrice = formatEscortTilePrice(price);
        if (!formattedPrice) {
            line.textContent = 'brak';
            line.style.background = 'rgba(20,20,20,.72)';
            line.title = `W anonsie nie podano ceny za ${durationLabel}`;
            return;
        }

        line.textContent = formattedPrice;
        line.style.background = getEscortPagePinkColor();
        line.title = `Cena podana w anonsie za ${durationLabel}`;
    }

    function renderListEscortiResult(line, result, stale = false) {
        if (!line) return;

        line.style.color = '#ffffff';
        line.title = stale ? 'Dane z lokalnego cache - trwa odświeżanie w tle' : 'Dane z Escorti';

        if (result?.status === 'phone-missing') {
            line.textContent = 'Brak numeru';
            line.style.color = '#ffcf9f';
            line.title = 'Nie można wyszukać w Escorti bez numeru telefonu';
            return;
        }

        if (!result || result.status === 'timeout' || result.status === 'error') {
            line.textContent = 'Błąd';
            line.style.color = '#ffb3b3';
            line.title = 'Nie udało się pobrać danych z Escorti';
            return;
        }

        if (result.profiles === 0) {
            line.textContent = 'Nie znaleziono na Escorti';
            line.style.color = '#b8dcff';
            line.title = 'Nie znaleziono profilu w Escorti';
            return;
        }

        // Ten wariant może wystąpić tylko dla starego/nieagregowanego wyniku.
        if (result.profiles > 1 && !result.merged) {
            line.textContent = 'Kilka profili';
            line.title = `Znaleziono ${result.profiles} profile w Escorti`;
            return;
        }

        const ads = typeof result.adLinks === 'number' ? result.adLinks : '?';
        const date = result.creationDate ? formatShortDate(result.creationDate) : '?';
        const opinions = typeof result.garsoTopics === 'number' ? result.garsoTopics : '?';
        const opinionsText = typeof opinions === 'number'
            ? `${opinions} ${getGarsoTopicWord(opinions)}`
            : '? tematów';

        line.textContent = `${ads} ogł • od ${date} • ${opinionsText}`;
        if (result.merged && result.profiles > 1) {
            line.title = `Scalono ${result.profiles} profile Escorti; duplikaty anonsów i tematów policzono tylko raz${result.partial ? ' (część profili miała błąd odczytu)' : ''}`;
        }
    }

    function getOrStartListCheck(
        adId,
        searchValue,
        searchMode = getEscortListSearchMode(),
        cancelToken = null
    ) {
        cancelToken?.throwIfCancelled();
        const normalizedSearchValue = searchMode === 'phone-escorti'
            ? digitsOnly(searchValue)
            : String(searchValue || '').trim().toLowerCase();
        const inflightKey = `${searchMode}:${normalizedSearchValue}`;

        let sharedPromise = listInflight.get(inflightKey);
        if (!sharedPromise) {
            sharedPromise = checkEscortiBackground(searchValue, {
                timeoutMs: 60000,
                mergeProfiles: true,
                preferDirect: true,
                cancelToken
            }).finally(() => listInflight.delete(inflightKey));
            listInflight.set(inflightKey, sharedPromise);
        }

        return sharedPromise.then(result => {
            cancelToken?.throwIfCancelled();
            if (result?.status === 'ok') setListCache(adId, result, searchMode);
            return result;
        });
    }

    function normalizeEscortListRouteKey(value) {
        try {
            const url = new URL(value, location.href);

            url.hash = '';
            url.pathname = url.pathname
                .replace(/\/page\d+\.html\/?$/i, '/')
                .replace(/\/page-\d+\/?$/i, '/')
                .replace(/\/page\/\d+\/?$/i, '/');

            for (const key of ['page', 'p']) {
                url.searchParams.delete(key);
            }

            const sorted = [...url.searchParams.entries()]
                .sort(([ak, av], [bk, bv]) => ak.localeCompare(bk) || av.localeCompare(bv));

            url.search = '';
            for (const [key, value] of sorted) {
                url.searchParams.append(key, value);
            }

            return `${url.hostname.toLowerCase()}${url.pathname}${url.search}`;
        } catch (_) {
            return null;
        }
    }

    function getEscortListPageNumber(value) {
        try {
            const url = new URL(value, location.href);

            const pathPatterns = [
                /\/page(\d+)\.html\/?$/i,
                /\/page-(\d+)\/?$/i,
                /\/page\/(\d+)\/?$/i
            ];

            for (const pattern of pathPatterns) {
                const match = url.pathname.match(pattern);
                if (match) return Number(match[1]);
            }

            for (const key of ['page', 'p']) {
                const valueFromQuery = url.searchParams.get(key);
                if (/^\d+$/.test(valueFromQuery || '')) {
                    return Number(valueFromQuery);
                }
            }

            return 1;
        } catch (_) {
            return 1;
        }
    }

    function findNextEscortListPageUrl(doc, baseUrl) {
        if (!doc) return null;

        const currentPage = getEscortListPageNumber(baseUrl);
        const routeKey = normalizeEscortListRouteKey(baseUrl);

        const normalizeCandidate = href => {
            if (!href) return null;

            try {
                const url = new URL(href, baseUrl);
                if (url.hostname !== 'pl.escort.club') return null;
                if (normalizeEscortListRouteKey(url.href) !== routeKey) return null;
                if (getEscortListPageNumber(url.href) <= currentPage) return null;
                return url.href;
            } catch (_) {
                return null;
            }
        };

        const directNextSelectors = [
            'link[rel="next"][href]',
            'a[rel="next"][href]',
            '.pagination .next a[href]',
            '.pagination a.next[href]',
            '.pager .next a[href]',
            '.pager a.next[href]',
            'a[aria-label*="następ" i][href]',
            'a[title*="następ" i][href]',
            'a[aria-label*="next" i][href]',
            'a[title*="next" i][href]'
        ];

        for (const selector of directNextSelectors) {
            const el = doc.querySelector(selector);
            const candidate = normalizeCandidate(el?.getAttribute('href'));
            if (candidate) return candidate;
        }

        const paginationLinks = [
            ...doc.querySelectorAll(
                '.pagination a[href], .pager a[href], .pages a[href], .ads-nav a[href], a[href*="page"][href]'
            )
        ];

        const textualNext = paginationLinks.find(a => {
            const text = (a.textContent || '')
                .replace(/\s+/g, ' ')
                .trim()
                .toLowerCase();

            return /^(następna|następny|dalej|next|›|»|>)$/.test(text);
        });

        const textualCandidate = normalizeCandidate(textualNext?.getAttribute('href'));
        if (textualCandidate) return textualCandidate;

        const numbered = paginationLinks
            .map(a => normalizeCandidate(a.getAttribute('href')))
            .filter(Boolean)
            .map(href => ({
                href,
                page: getEscortListPageNumber(href)
            }))
            .filter(item => item.page > currentPage)
            .sort((a, b) => a.page - b.page);

        return numbered[0]?.href || null;
    }


    function normalizeEscortSectionHeading(value) {
        return normalizeText(value)
            .normalize('NFKD')
            .replace(/\p{M}/gu, '');
    }

    function isEscortMainResultsHeading(value) {
        // Aktualny Escort.club używa m.in. nagłówka:
        // „Panie Kraków - anonse erotyczne”, a starsze warianty miały
        // „Anonse erotyczne Kraków”. Nie wymagamy więc, aby fraza była
        // na początku nagłówka.
        return normalizeEscortSectionHeading(value)
            .includes('anonse erotyczne');
    }

    function isEscortSearchPath(pathname = location.pathname) {
        return /^\/szukaj(?:\/|$)/i.test(String(pathname || ''));
    }

    function serializeEscortLocationControls(controls) {
        return {
            country: getEscortSelectOptionData(controls?.country),
            province: getEscortSelectOptionData(controls?.province),
            city: getEscortSelectOptionData(controls?.city),
            district: getEscortSelectOptionData(controls?.district)
        };
    }

    function createEscortLocationSettingsBridge() {
        // Escort.club może użyć niewidocznej ramki same-origin. Escorti i
        // Garsoniera nie mogą niezawodnie osadzić strony Escort.club w iframe,
        // dlatego używamy tam technicznej karty otwieranej w tle (active:false).
        return CURRENT_HOST === 'pl.escort.club'
            ? createEscortLocationSettingsIframeBridge()
            : createEscortLocationSettingsTabBridge();
    }

    function createEscortLocationSettingsIframeBridge() {
        const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
        const iframe = makeElement('iframe');
        iframe.setAttribute('aria-hidden', 'true');
        iframe.tabIndex = -1;
        iframe.style.position = 'fixed';
        iframe.style.left = '-10000px';
        iframe.style.top = '-10000px';
        iframe.style.width = '1px';
        iframe.style.height = '1px';
        iframe.style.opacity = '0';
        iframe.style.pointerEvents = 'none';
        iframe.style.border = '0';
        iframe.src = `${ESCORT_LOCATION_BRIDGE_URL}?${ESCORT_LOCATION_BRIDGE_PARAM}=${encodeURIComponent(token)}`;

        let destroyed = false;
        let ready = false;
        let requestCounter = 0;
        const pending = new Map();
        let readyResolve;
        let readyReject;
        const readyPromise = new Promise((resolve, reject) => {
            readyResolve = resolve;
            readyReject = reject;
        });
        const readyTimeout = setTimeout(() => {
            if (ready || destroyed) return;
            readyReject(new Error('Nie udało się uruchomić ukrytej ramki lokalizacji Escort.club.'));
        }, 12000);

        const onMessage = event => {
            if (destroyed || event.source !== iframe.contentWindow) return;
            const data = event.data;
            if (!data || data.token !== token) return;

            if (data.type === 'vm-escort-location-bridge-ready') {
                clearTimeout(readyTimeout);
                if (data.error) {
                    readyReject(new Error(data.error));
                    return;
                }
                ready = true;
                readyResolve(data.controls || {});
                return;
            }

            if (data.type === 'vm-escort-location-bridge-response') {
                const entry = pending.get(String(data.requestId || ''));
                if (!entry) return;
                pending.delete(String(data.requestId || ''));
                clearTimeout(entry.timeout);
                if (data.error) entry.reject(new Error(data.error));
                else entry.resolve(data.controls || {});
            }
        };
        window.addEventListener('message', onMessage);
        document.body.appendChild(iframe);

        return {
            async load(locationData = {}) {
                await readyPromise;
                if (destroyed) throw new Error('Ramka lokalizacji została zamknięta.');
                const requestId = String(++requestCounter);
                return new Promise((resolve, reject) => {
                    const timeout = setTimeout(() => {
                        pending.delete(requestId);
                        reject(new Error('Przekroczono czas pobierania lokalizacji z Escort.club.'));
                    }, 10000);
                    pending.set(requestId, { resolve, reject, timeout });
                    iframe.contentWindow.postMessage({
                        type: 'vm-escort-location-bridge-request',
                        token,
                        requestId,
                        locationData: normalizeDefaultSearchLocation(locationData)
                    }, 'https://pl.escort.club');
                });
            },
            destroy() {
                if (destroyed) return;
                destroyed = true;
                clearTimeout(readyTimeout);
                window.removeEventListener('message', onMessage);
                for (const entry of pending.values()) {
                    clearTimeout(entry.timeout);
                    entry.reject(new Error('Ramka lokalizacji została zamknięta.'));
                }
                pending.clear();
                iframe.remove();
            }
        };
    }

    function waitForEscortSearchLocationControls(timeoutMs = 10000) {
        return new Promise((resolve, reject) => {
            const startedAt = Date.now();
            const timer = setInterval(() => {
                const controls = findEscortSearchLocationControls();
                if (controls?.country && controls?.province && controls?.city) {
                    clearInterval(timer);
                    resolve(controls);
                } else if (Date.now() - startedAt > timeoutMs) {
                    clearInterval(timer);
                    reject(new Error(
                        'Nie znaleziono kontrolek lokalizacji na stronie wyszukiwania Escort.club.'
                    ));
                }
            }, 100);
        });
    }

    function locationTabBridgeReadyKey(token) {
        return `${ESCORT_LOCATION_TAB_BRIDGE_READY_PREFIX}${token}`;
    }

    function locationTabBridgeRequestKey(token) {
        return `${ESCORT_LOCATION_TAB_BRIDGE_REQUEST_PREFIX}${token}`;
    }

    function locationTabBridgeResponseKey(token) {
        return `${ESCORT_LOCATION_TAB_BRIDGE_RESPONSE_PREFIX}${token}`;
    }

    function createEscortLocationSettingsTabBridge() {
        const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
        const readyKey = locationTabBridgeReadyKey(token);
        const requestKey = locationTabBridgeRequestKey(token);
        const responseKey = locationTabBridgeResponseKey(token);
        let bridgeTab = null;
        let startPromise = null;
        let destroyed = false;
        let requestCounter = 0;

        const cleanupStorage = () => {
            for (const key of [readyKey, requestKey, responseKey]) {
                try { GM_deleteValue(key); } catch (_) {}
            }
        };

        const waitForValue = (key, predicate, timeoutMs, errorMessage) =>
            new Promise((resolve, reject) => {
                const startedAt = Date.now();
                const timer = setInterval(() => {
                    if (destroyed) {
                        clearInterval(timer);
                        reject(new Error('Pobieranie lokalizacji zostało anulowane.'));
                        return;
                    }
                    let value = null;
                    try { value = GM_getValue(key, null); } catch (_) {}
                    if (predicate(value)) {
                        clearInterval(timer);
                        resolve(value);
                    } else if (Date.now() - startedAt >= timeoutMs) {
                        clearInterval(timer);
                        reject(new Error(errorMessage));
                    }
                }, 120);
            });

        const ensureStarted = () => {
            if (startPromise) return startPromise;
            startPromise = (async () => {
                cleanupStorage();
                bridgeTab = GM_openInTab(
                    `${ESCORT_LOCATION_BRIDGE_URL}?${ESCORT_LOCATION_TAB_BRIDGE_PARAM}=${encodeURIComponent(token)}`,
                    { active: false, insert: true, setParent: true }
                );
                const readyState = await waitForValue(
                    readyKey,
                    value => !!value,
                    15000,
                    'Nie udało się uruchomić pobierania lokalizacji z Escort.club.'
                );
                if (!readyState?.ok) {
                    throw new Error(readyState?.error || 'Nie udało się odczytać lokalizacji Escort.club.');
                }
                return readyState.controls || {};
            })();
            return startPromise;
        };

        return {
            async load(locationData = {}) {
                await ensureStarted();
                if (destroyed) throw new Error('Pobieranie lokalizacji zostało zamknięte.');
                const requestId = String(++requestCounter);
                try { GM_deleteValue(responseKey); } catch (_) {}
                GM_setValue(requestKey, {
                    requestId,
                    locationData: normalizeDefaultSearchLocation(locationData),
                    createdAt: Date.now()
                });
                const response = await waitForValue(
                    responseKey,
                    value => value && String(value.requestId || '') === requestId,
                    12000,
                    'Przekroczono czas pobierania lokalizacji z Escort.club.'
                );
                if (!response.ok) {
                    throw new Error(response.error || 'Nie udało się pobrać lokalizacji Escort.club.');
                }
                return response.controls || {};
            },
            destroy() {
                if (destroyed) return;
                destroyed = true;
                try { bridgeTab?.close?.(); } catch (_) {}
                cleanupStorage();
            }
        };
    }

    async function handleEscortLocationTabBridgePage() {
        if (CURRENT_HOST !== 'pl.escort.club' || window.top !== window.self) return false;
        const token = new URL(location.href).searchParams.get(ESCORT_LOCATION_TAB_BRIDGE_PARAM);
        if (!token) return false;

        const readyKey = locationTabBridgeReadyKey(token);
        const requestKey = locationTabBridgeRequestKey(token);
        const responseKey = locationTabBridgeResponseKey(token);
        let lastRequestId = '';
        let requestChain = Promise.resolve();

        try {
            const controls = await waitForEscortSearchLocationControls();
            GM_setValue(readyKey, {
                ok: true,
                controls: serializeEscortLocationControls(controls),
                readyAt: Date.now()
            });
        } catch (error) {
            GM_setValue(readyKey, {
                ok: false,
                error: error?.message || 'Nie udało się odczytać lokalizacji Escort.club.',
                readyAt: Date.now()
            });
            return true;
        }

        const processRequest = request => {
            if (!request || typeof request !== 'object') return;
            const requestId = String(request.requestId || '');
            if (!requestId || requestId === lastRequestId) return;
            if (Date.now() - Number(request.createdAt || 0) > 60 * 1000) return;
            lastRequestId = requestId;
            requestChain = requestChain.catch(() => {}).then(async () => {
                try {
                    const controls = await loadEscortLocationOptions(
                        normalizeDefaultSearchLocation(request.locationData),
                        { forceCountryChange: false }
                    );
                    GM_setValue(responseKey, {
                        ok: true,
                        requestId,
                        controls: serializeEscortLocationControls(controls),
                        completedAt: Date.now()
                    });
                } catch (error) {
                    GM_setValue(responseKey, {
                        ok: false,
                        requestId,
                        error: error?.message || 'Nie udało się pobrać lokalizacji Escort.club.',
                        completedAt: Date.now()
                    });
                }
            });
        };

        setInterval(() => {
            let request = null;
            try { request = GM_getValue(requestKey, null); } catch (_) {}
            processRequest(request);
        }, 120);
        return true;
    }

    async function handleEscortLocationBridgeFrame() {
        if (location.hostname !== 'pl.escort.club' || window.top === window.self) return false;
        const token = new URL(location.href).searchParams.get(ESCORT_LOCATION_BRIDGE_PARAM);
        if (!token) return false;

        const send = payload => {
            window.parent.postMessage({ ...payload, token }, '*');
        };
        try {
            const controls = await waitForEscortSearchLocationControls();
            send({
                type: 'vm-escort-location-bridge-ready',
                controls: serializeEscortLocationControls(controls)
            });
        } catch (error) {
            send({
                type: 'vm-escort-location-bridge-ready',
                error: error?.message || 'Nie udało się odczytać lokalizacji Escort.club.'
            });
        }

        window.addEventListener('message', async event => {
            if (event.source !== window.parent) return;
            const data = event.data;
            if (
                !data || data.token !== token ||
                data.type !== 'vm-escort-location-bridge-request'
            ) return;
            try {
                const controls = await loadEscortLocationOptions(data.locationData || {}, { forceCountryChange: true });
                send({
                    type: 'vm-escort-location-bridge-response',
                    requestId: data.requestId,
                    controls: serializeEscortLocationControls(controls)
                });
            } catch (error) {
                send({
                    type: 'vm-escort-location-bridge-response',
                    requestId: data.requestId,
                    error: error?.message || 'Nie udało się pobrać lokalizacji Escort.club.'
                });
            }
        });
        return true;
    }

    function getEscortSelectOptionData(select) {
        if (!(select instanceof HTMLSelectElement)) return [];
        return [...select.options].map(option => ({
            value: String(option.value ?? ''),
            label: String(option.textContent || '').replace(/\s+/g, ' ').trim()
        })).filter(option => option.label);
    }

    function findEscortSearchLocationControls(doc = document) {
        const searchBox = [...doc.querySelectorAll('.search-box')].find(box =>
            box.querySelector('select[name="country"]') &&
            box.querySelector('select[name="province"]') &&
            box.querySelector('select[name="city"]') &&
            box.querySelector('.btn-search[type="submit"]')
        );
        const searchButton = searchBox?.querySelector('.btn-search[type="submit"]');
        if (!searchButton || !searchBox) return null;

        return {
            searchBox,
            searchButton,
            submitItem: searchButton.closest('.form-item.-submit'),
            country: searchBox.querySelector('select[name="country"]'),
            province: searchBox.querySelector('select[name="province"]'),
            city: searchBox.querySelector('select[name="city"]'),
            district: searchBox.querySelector('select[name="district"]')
        };
    }

    function findEscortLocationOption(select, value, label = '') {
        if (!(select instanceof HTMLSelectElement)) return null;
        const wantedValue = String(value ?? '');
        const wantedLabel = String(label || '').replace(/\s+/g, ' ').trim()
            .toLocaleLowerCase('pl-PL');
        return [...select.options].find(option =>
            wantedValue && String(option.value) === wantedValue
        ) || [...select.options].find(option =>
            wantedLabel && String(option.textContent || '')
                .replace(/\s+/g, ' ')
                .trim()
                .toLocaleLowerCase('pl-PL') === wantedLabel
        ) || null;
    }

    function getEscortLocationSelectSignature(select) {
        return getEscortSelectOptionData(select)
            .map(option => `${option.value}:${option.label}`)
            .join('|');
    }

    function setEscortLocationControl(select, value, label = '', strongChange = false) {
        const option = findEscortLocationOption(select, value, label);
        if (!option) return false;
        select.disabled = false;
        const changed = String(select.value ?? '') !== String(option.value ?? '');
        select.value = option.value;
        // Na zwykłej stronie nie wysyłamy change dla już wybranej wartości
        // (poprawka 1.020). W ukrytej ramce po rzeczywistej zmianie państwa
        // dodatkowo wywołujemy jQuery change, bo Escort.club używa go do
        // przeładowania części zależnych list na niektórych wariantach strony.
        if (changed) {
            dispatchEscortFilterControlEvents(select);
            if (strongChange) {
                try {
                    const jq = window.jQuery;
                    if (jq) jq(select).trigger('change');
                } catch (_) {}
            }
        }
        refreshEscortFilterSelect(select);
        return true;
    }

    function waitForEscortLocationOptions(controlName, predicate, timeoutMs = 7000) {
        return new Promise((resolve, reject) => {
            const startedAt = Date.now();
            const check = () => {
                const controls = findEscortSearchLocationControls();
                const select = controls?.[controlName];
                if (select && predicate(select, controls)) {
                    clearInterval(timer);
                    resolve(controls);
                    return;
                }
                if (Date.now() - startedAt >= timeoutMs) {
                    clearInterval(timer);
                    reject(new Error(
                        controlName === 'district'
                            ? 'Escort.club nie udostępnił listy dzielnic.'
                            : (controlName === 'city'
                                ? 'Escort.club nie udostępnił listy miast.'
                                : 'Escort.club nie udostępnił listy województw.')
                    ));
                }
            };
            const timer = setInterval(check, 100);
            check();
        });
    }

    async function loadEscortLocationOptions(locationData, options = {}) {
        const selected = normalizeDefaultSearchLocation(locationData);
        const forceCountryChange = options?.forceCountryChange === true;
        let controls = findEscortSearchLocationControls();
        if (!controls?.country || !controls?.province || !controls?.city) {
            throw new Error(
                'Kontrolki lokalizacji są dostępne na stronie wyszukiwania Escort.club.'
            );
        }

        const previousCountry = String(controls.country.value || '');
        const provinceSignature = getEscortLocationSelectSignature(controls.province);
        if (!setEscortLocationControl(
            controls.country,
            selected.countryValue,
            selected.countryLabel,
            forceCountryChange
        )) {
            throw new Error('Nie znaleziono wybranego państwa w wyszukiwarce Escort.club.');
        }

        if (previousCountry !== String(controls.country.value || '')) {
            try {
                controls = await waitForEscortLocationOptions(
                    'province',
                    select =>
                        getEscortLocationSelectSignature(select) !== provinceSignature,
                    3500
                );
            } catch (_) {
                controls = findEscortSearchLocationControls() || controls;
            }
        }

        if (!selected.provinceValue && !selected.provinceLabel) return controls;

        if (!findEscortLocationOption(
            controls.province,
            selected.provinceValue,
            selected.provinceLabel
        )) {
            controls = await waitForEscortLocationOptions(
                'province',
                select => !!findEscortLocationOption(
                    select,
                    selected.provinceValue,
                    selected.provinceLabel
                ),
                5000
            );
        }

        const previousProvince = String(controls.province.value || '');
        const citySignature = getEscortLocationSelectSignature(controls.city);
        if (!setEscortLocationControl(
            controls.province,
            selected.provinceValue,
            selected.provinceLabel
        )) {
            throw new Error('Nie znaleziono wybranego województwa w wyszukiwarce Escort.club.');
        }

        const cityAlreadyAvailable = selected.cityValue || selected.cityLabel
            ? !!findEscortLocationOption(
                controls.city,
                selected.cityValue,
                selected.cityLabel
            )
            : getEscortSelectOptionData(controls.city).some(option => option.value);
        if (!cityAlreadyAvailable || previousProvince !== String(controls.province.value || '')) {
            controls = await waitForEscortLocationOptions(
                'city',
                select => {
                    if (selected.cityValue || selected.cityLabel) {
                        return !!findEscortLocationOption(
                            select,
                            selected.cityValue,
                            selected.cityLabel
                        );
                    }
                    return getEscortLocationSelectSignature(select) !== citySignature &&
                        getEscortSelectOptionData(select).some(option => option.value);
                }
            );
        }

        if (selected.cityValue || selected.cityLabel) {
            const previousCity = String(controls.city.value || '');
            const districtSignature = getEscortLocationSelectSignature(controls.district);
            if (!setEscortLocationControl(
                controls.city,
                selected.cityValue,
                selected.cityLabel
            )) {
                throw new Error('Nie znaleziono wybranego miasta w wyszukiwarce Escort.club.');
            }
            controls = findEscortSearchLocationControls() || controls;

            if (selected.districtValue || selected.districtLabel) {
                if (!controls.district) {
                    throw new Error('Escort.club nie udostępnił pola dzielnicy.');
                }
                const districtAlreadyAvailable = !!findEscortLocationOption(
                    controls.district,
                    selected.districtValue,
                    selected.districtLabel
                );
                if (
                    !districtAlreadyAvailable ||
                    previousCity !== String(controls.city.value || '')
                ) {
                    controls = await waitForEscortLocationOptions(
                        'district',
                        select => !!findEscortLocationOption(
                            select,
                            selected.districtValue,
                            selected.districtLabel
                        ) || (
                            getEscortLocationSelectSignature(select) !== districtSignature &&
                            getEscortSelectOptionData(select).some(option => option.value)
                        )
                    );
                }
                if (!setEscortLocationControl(
                    controls.district,
                    selected.districtValue,
                    selected.districtLabel
                )) {
                    throw new Error('Nie znaleziono wybranej dzielnicy w wyszukiwarce Escort.club.');
                }
            } else if (controls.district) {
                if (previousCity !== String(controls.city.value || '')) {
                    try {
                        controls = await waitForEscortLocationOptions(
                            'district',
                            select =>
                                getEscortLocationSelectSignature(select) !== districtSignature,
                            2500
                        );
                    } catch (_) {
                        controls = findEscortSearchLocationControls() || controls;
                    }
                }
                const emptyDistrict = [...(controls.district?.options || [])]
                    .find(option => !String(option.value || ''));
                if (emptyDistrict) {
                    const changed = String(controls.district.value || '') !== String(emptyDistrict.value || '');
                    controls.district.value = emptyDistrict.value;
                    if (changed) dispatchEscortFilterControlEvents(controls.district);
                    refreshEscortFilterSelect(controls.district);
                }
            }
        } else {
            // Lokalizacja może kończyć się na województwie. Jeżeli miasto nie
            // jest zapisane, jawnie czyścimy wcześniejszy wybór miasta/dzielnicy,
            // także wtedy, gdy województwo już było wybrane i nie wywołano change.
            const emptyCity = [...(controls.city?.options || [])]
                .find(option => !String(option.value || ''));
            if (emptyCity && String(controls.city.value || '') !== String(emptyCity.value || '')) {
                controls.city.value = emptyCity.value;
                dispatchEscortFilterControlEvents(controls.city);
                refreshEscortFilterSelect(controls.city);
                controls = findEscortSearchLocationControls() || controls;
            }
            const emptyDistrict = [...(controls.district?.options || [])]
                .find(option => !String(option.value || ''));
            if (emptyDistrict && String(controls.district.value || '') !== String(emptyDistrict.value || '')) {
                controls.district.value = emptyDistrict.value;
                dispatchEscortFilterControlEvents(controls.district);
                refreshEscortFilterSelect(controls.district);
            }
        }
        return controls;
    }

    function shortenSavedLocationCity(value) {
        const text = String(value || '').trim();
        if (text.length <= 5) return text;
        return `${text.slice(0, 1)}-${text.slice(-2)}`;
    }

    function getSavedLocationButtonLabel(locationData) {
        const selected = normalizeDefaultSearchLocation(locationData);
        const province = selected.provinceLabel || selected.provinceValue || '';
        const city = selected.cityLabel || selected.cityValue || '';
        const district = selected.districtLabel || selected.districtValue || '';

        // Bez dzielnicy pokazujemy pełną nazwę miasta. Skracanie miasta jest
        // potrzebne dopiero dla napisu „miasto, dzielnica”.
        if (!district) return city || province || 'Lokalizacja';

        let cityPart = city;
        let label = `${cityPart}, ${district}`;
        if (label.length > 14 && cityPart.length > 5) {
            cityPart = shortenSavedLocationCity(cityPart);
            label = `${cityPart}, ${district}`;
        }
        return label;
    }

    function initEscortDefaultCityButton() {
        const existing = document.getElementById(ESCORT_DEFAULT_CITY_BUTTON_ID);
        const savedLocations = normalizeSearchLocations(
            SETTINGS.searchLocations,
            SETTINGS.showDefaultCityButton,
            SETTINGS.defaultSearchLocation
        );
        if (!savedLocations.length) {
            existing?.closest('[data-vm-default-city-item="1"]')?.remove();
            existing?.remove();
            return;
        }
        if (existing) return;

        const insert = () => {
            if (document.getElementById(ESCORT_DEFAULT_CITY_BUTTON_ID)) return true;
            const controls = findEscortSearchLocationControls();
            if (!controls?.submitItem) return false;

            const item = controls.submitItem.cloneNode(false);
            item.dataset.vmDefaultCityItem = '1';
            item.style.minWidth = '0';
            item.style.width = 'auto';
            item.style.maxWidth = '270px';
            item.style.flex = '0 0 auto';

            const grid = makeElement('div');
            grid.id = ESCORT_DEFAULT_CITY_BUTTON_ID;
            grid.style.display = 'grid';
            grid.style.gap = '4px';
            grid.style.width = 'auto';
            grid.style.maxWidth = '260px';
            grid.style.justifyContent = 'start';
            grid.style.alignItems = 'stretch';

            const locationLabels = savedLocations.map(getSavedLocationButtonLabel);
            const searchButtonStyle = getComputedStyle(controls.searchButton);
            const measureCanvas = document.createElement('canvas');
            const measureContext = measureCanvas.getContext('2d');
            if (measureContext) {
                measureContext.font = `${searchButtonStyle.fontWeight || '400'} 11px ${searchButtonStyle.fontFamily || 'Arial'}`;
            }
            const measureButtonWidth = label => {
                const textWidth = measureContext
                    ? measureContext.measureText(label).width
                    : String(label || '').length * 6.2;
                return Math.max(46, Math.min(128, Math.ceil(textWidth + 16)));
            };
            const locationCount = savedLocations.length;
            const getLocationGridColumn = index => {
                // Przy 3 lokalizacjach pierwszy przycisk zajmuje całą lewą kolumnę,
                // a drugi i trzeci są ułożone jeden nad drugim po prawej.
                if (locationCount === 3) return index === 0 ? 0 : 1;
                return index % 2;
            };
            const columnWidths = [0, 0];
            locationLabels.forEach((label, index) => {
                const column = getLocationGridColumn(index);
                columnWidths[column] = Math.max(columnWidths[column], measureButtonWidth(label));
            });
            const usedColumns = Math.min(2, locationCount);
            grid.style.gridTemplateColumns = columnWidths
                .slice(0, usedColumns)
                .map(width => `${Math.max(46, width)}px`)
                .join(' ');
            // Jeden przycisk również traktujemy jako element wysoki na dwa wiersze.
            // Wizualnie daje to tę samą pełną wysokość co przycisk „Szukaj”.
            const rowCount = (locationCount === 1 || locationCount >= 3) ? 2 : 1;
            grid.style.gridTemplateRows = `repeat(${rowCount}, minmax(0, 1fr))`;

            const syncLocationGridHeight = () => {
                const searchHeight = controls.searchButton.getBoundingClientRect().height;
                if (searchHeight > 0) grid.style.height = `${Math.round(searchHeight)}px`;
            };

            const setButtonIdle = (button, selected) => {
                const label = getSavedLocationButtonLabel(selected);
                button.textContent = label;
                button.title = `Ustaw w wyszukiwarce: ${[
                    selected.provinceLabel,
                    selected.cityLabel,
                    selected.districtLabel
                ].filter(Boolean).join(', ')}`;
            };

            savedLocations.forEach((selected, index) => {
                const button = makeButton(controls.searchButton.className, '');
                button.dataset.vmLocationIndex = String(index);
                button.style.setProperty('width', '100%', 'important');
                button.style.setProperty('min-width', '0', 'important');
                button.style.setProperty('max-width', 'none', 'important');
                button.style.setProperty('height', '100%', 'important');
                button.style.setProperty('min-height', '0', 'important');
                button.style.setProperty('box-sizing', 'border-box', 'important');
                button.style.setProperty('margin', '0', 'important');
                button.style.setProperty('padding', '0 7px', 'important');
                button.style.setProperty('display', 'flex', 'important');
                button.style.setProperty('align-items', 'center', 'important');
                button.style.setProperty('justify-content', 'center', 'important');
                button.style.setProperty('line-height', '1.1', 'important');
                button.style.setProperty('vertical-align', 'middle', 'important');
                button.style.fontSize = savedLocations.length > 1 ? '11px' : '';
                button.style.whiteSpace = 'nowrap';
                button.style.overflow = 'hidden';
                button.style.textOverflow = 'ellipsis';

                if (locationCount === 1 && index === 0) {
                    button.style.gridColumn = '1';
                    button.style.gridRow = '1 / span 2';
                } else if (locationCount === 3) {
                    if (index === 0) {
                        button.style.gridColumn = '1';
                        button.style.gridRow = '1 / span 2';
                    } else {
                        button.style.gridColumn = '2';
                        button.style.gridRow = String(index);
                    }
                }

                setButtonIdle(button, selected);

                button.addEventListener('click', async event => {
                    event.preventDefault();
                    event.stopPropagation();
                    if (button.disabled) return;
                    for (const other of grid.querySelectorAll('button')) other.disabled = true;
                    button.textContent = 'Ustawiam…';
                    try {
                        await loadEscortLocationOptions(selected);
                        setButtonIdle(button, selected);
                    } catch (error) {
                        button.textContent = 'Błąd';
                        button.title = error?.message || 'Nie udało się ustawić zapisanej lokalizacji.';
                        setTimeout(() => {
                            if (!document.contains(button)) return;
                            setButtonIdle(button, selected);
                        }, 1800);
                    } finally {
                        for (const other of grid.querySelectorAll('button')) other.disabled = false;
                    }
                });
                grid.appendChild(button);
            });

            item.appendChild(grid);
            controls.submitItem.before(item);
            syncLocationGridHeight();
            requestAnimationFrame(syncLocationGridHeight);
            if (typeof ResizeObserver === 'function') {
                const observer = new ResizeObserver(syncLocationGridHeight);
                observer.observe(controls.searchButton);
                item._vmLocationResizeObserver = observer;
            }
            return true;
        };

        if (insert()) return;
        let attempts = 0;
        const timer = setInterval(() => {
            attempts++;
            if (insert() || attempts >= 40) clearInterval(timer);
        }, 250);
    }

    function getEscortMainResultsSection(doc = document) {
        const sections = [...doc.querySelectorAll('section.content-sec.-index')];

        return sections.find(section => {
            const heading = section.querySelector('h1');
            return isEscortMainResultsHeading(heading?.textContent || '');
        }) || null;
    }

    function isEscortResultsPage(doc = document, pageUrl = location.href) {
        let pathname = location.pathname;

        try {
            pathname = new URL(pageUrl, location.href).pathname;
        } catch (_) {}

        // /szukaj/... pozostaje obsługiwane, ale typowe wyniki Escort.club
        // mają adresy /anonse/... (np. /anonse/panie/krakow/).
        if (isEscortSearchPath(pathname)) return true;

        return !!getEscortMainResultsSection(doc);
    }

    function getEscortSectionTitleCandidates(doc = document) {
        const selectors = [
            'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
            '.title', '.heading', '.section-title', '.section-heading',
            '.block-title', '.box-title', '.content-title', '.content-heading',
            '.list-title', '.ads-title',
            '[class*="section-title"]', '[class*="section-heading"]',
            '[class*="block-title"]', '[class*="content-title"]',
            'strong', 'b', 'p', 'span', 'div'
        ];

        const seen = new Set();
        const out = [];

        for (const el of doc.querySelectorAll(selectors.join(','))) {
            if (seen.has(el)) continue;

            const raw = (el.textContent || '').replace(/\s+/g, ' ').trim();
            if (!raw || raw.length > 90) continue;

            const normalized = normalizeEscortSectionHeading(raw);
            if (
                !/^(?:polecane|wyroznione|popularne miasta)(?:\s|$)/i.test(normalized) &&
                !isEscortMainResultsHeading(normalized)
            ) {
                continue;
            }

            // Preferujemy najbardziej wewnętrzny element zawierający sam tytuł,
            // a nie duży wrapper sekcji, który przypadkiem ma ten tekst w textContent.
            const hasMatchingChild = [...el.children].some(child => {
                const childText = (child.textContent || '').replace(/\s+/g, ' ').trim();
                if (!childText || childText.length > 90) return false;
                const childNormalized = normalizeEscortSectionHeading(childText);
                return /^(?:polecane|wyroznione|popularne miasta)(?:\s|$)/i.test(childNormalized) ||
                    isEscortMainResultsHeading(childNormalized);
            });

            if (hasMatchingChild) continue;

            seen.add(el);
            out.push(el);
        }

        return out.sort((a, b) => {
            if (a === b) return 0;
            const relation = a.compareDocumentPosition(b);
            return relation & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
        });
    }

    function findEscortSectionLabels(pattern, doc = document) {
        return getEscortSectionTitleCandidates(doc)
            .filter(el => pattern.test(
                normalizeEscortSectionHeading(el.textContent || '')
            ));
    }

    function isElementAfter(reference, el) {
        return !!(
            reference &&
            el &&
            reference !== el &&
            (reference.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_FOLLOWING)
        );
    }

    function isElementBefore(reference, el) {
        return !!(
            reference &&
            el &&
            reference !== el &&
            (reference.compareDocumentPosition(el) & Node.DOCUMENT_POSITION_PRECEDING)
        );
    }

    function getEscortSectionBoundary(label, doc = document) {
        const labels = getEscortSectionTitleCandidates(doc);
        const currentIndex = labels.indexOf(label);

        if (currentIndex >= 0) {
            return labels.slice(currentIndex + 1)[0] || null;
        }

        return labels.find(candidate => isElementAfter(label, candidate)) || null;
    }

    function hideEscortSectionByHeading({
        headingPattern,
        targetSelector,
        targetMapper = el => el,
        hiddenDatasetKey = 'vmEscortSectionHidden'
    }) {
        const labels = findEscortSectionLabels(headingPattern, document);

        for (const label of labels) {
            if (label.closest(`[data-${hiddenDatasetKey.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}="1"]`)) {
                continue;
            }

            const nextBoundary = getEscortSectionBoundary(label, document);

            const isInsideRange = el => {
                if (!isElementAfter(label, el)) return false;
                if (!nextBoundary) return true;
                return isElementBefore(nextBoundary, el);
            };

            const targetElements = targetSelector
                ? [...document.querySelectorAll(targetSelector)]
                    .filter(isInsideRange)
                    .map(targetMapper)
                    .filter(Boolean)
                : [];

            // Szukamy najmniejszego wspólnego wrappera tytułu i zawartości sekcji.
            // Nie pozwalamy, aby wrapper objął początek kolejnej sekcji.
            let wrapper = label.parentElement;
            let hidden = false;

            while (
                wrapper &&
                wrapper !== document.body &&
                wrapper !== document.documentElement
            ) {
                const containsTarget = targetElements.length > 0 &&
                    targetElements.some(el => wrapper.contains(el));

                const containsBoundary = nextBoundary
                    ? wrapper.contains(nextBoundary)
                    : false;

                if (containsTarget && !containsBoundary) {
                    wrapper.style.setProperty('display', 'none', 'important');
                    wrapper.dataset[hiddenDatasetKey] = '1';
                    hidden = true;
                    break;
                }

                wrapper = wrapper.parentElement;
            }

            if (hidden) continue;

            // Fallback: nawet gdy Escort.club ma nietypowy wrapper, zniknie sam
            // tytuł i wszystkie elementy tej sekcji znajdujące się przed kolejną.
            label.style.setProperty('display', 'none', 'important');
            label.dataset[hiddenDatasetKey] = '1';

            for (const el of targetElements) {
                el.style.setProperty('display', 'none', 'important');
                el.dataset[hiddenDatasetKey] = '1';
            }
        }
    }

    function applyEscortSectionVisibility() {
        const isResultsPage = isEscortResultsPage();
        const isSingleAdPage = /^\/anons\/\d+\.html\/?$/i.test(location.pathname);

        // Na stronach wyników korzystamy najpierw z rzeczywistych wrapperów
        // Escort.club. Dzięki temu nie zależymy od kolejności nagłówków ani od
        // tego, czy adres strony zaczyna się od /szukaj/ czy od /anonse/.
        if (isResultsPage && SETTINGS.hideRecommendedSearchSection) {
            for (const section of document.querySelectorAll('section.dates-sec')) {
                const title = normalizeEscortSectionHeading(
                    section.querySelector('h1,h2,h3,h4,h5,h6')?.textContent || ''
                );

                if (/^polecane(?:\s|$)/i.test(title)) {
                    section.style.setProperty('display', 'none', 'important');
                    section.dataset.vmRecommendedSectionHidden = '1';
                }
            }
        }

        if (isSingleAdPage && SETTINGS.hideRecommendedAdSection) {
            for (const section of document.querySelectorAll('section.dates-sec')) {
                const title = normalizeEscortSectionHeading(
                    section.querySelector('h1,h2,h3,h4,h5,h6')?.textContent || ''
                );

                if (/^polecane(?:\s|$)/i.test(title)) {
                    section.style.setProperty('display', 'none', 'important');
                    section.dataset.vmRecommendedAdSectionHidden = '1';
                }
            }

            hideEscortSectionByHeading({
                headingPattern: /^polecane(?:\s|$)/i,
                targetSelector: 'a[href*="/anons/"]',
                targetMapper: link => link.closest('.item-col, .col') || link,
                hiddenDatasetKey: 'vmRecommendedAdSectionHidden'
            });
        }

        if (isSingleAdPage && SETTINGS.hideSingleAdContactButtons) {
            for (const contactBox of document.querySelectorAll('.content-contact')) {
                const containsTargetButton = contactBox.querySelector(
                    'a[href*="action=tipTokens"], a[href*="action=sendMessage"]'
                );
                if (!containsTargetButton) continue;

                contactBox.style.setProperty('display', 'none', 'important');
                contactBox.dataset.vmSingleAdContactButtonsHidden = '1';
            }
        }

        if (isResultsPage && SETTINGS.hideFeaturedSearchSection) {
            for (const section of document.querySelectorAll('section.vip-sec')) {
                const title = normalizeEscortSectionHeading(
                    section.querySelector('h1,h2,h3,h4,h5,h6')?.textContent || ''
                );

                if (/^wyroznione(?:\s|$)/i.test(title)) {
                    section.style.setProperty('display', 'none', 'important');
                    section.dataset.vmFeaturedSectionHidden = '1';
                }
            }
        }

        if (
            SETTINGS.hidePopularCitiesSection &&
            (isResultsPage || isSingleAdPage)
        ) {
            const cityList = document.querySelector('#cityList');
            const section = cityList?.closest('section.search-sec');

            if (section) {
                section.style.setProperty('display', 'none', 'important');
                section.dataset.vmPopularCitiesSectionHidden = '1';
            } else {
                // Fallback dla ewentualnego innego wariantu HTML Escort.club.
                hideEscortSectionByHeading({
                    headingPattern: /^popularne miasta(?:\s|$)/i,
                    targetSelector: 'a[href*="/anonse/"]',
                    targetMapper: link => link,
                    hiddenDatasetKey: 'vmPopularCitiesSectionHidden'
                });
            }
        }
    }

    function initEscortSectionVisibility() {
        applyEscortSectionVisibility();

        if (document.readyState === 'loading') {
            document.addEventListener(
                'DOMContentLoaded',
                applyEscortSectionVisibility,
                { once: true }
            );
        }

        window.addEventListener('load', applyEscortSectionVisibility, { once: true });

        if (
            SETTINGS.hideRecommendedSearchSection ||
            SETTINGS.hideRecommendedAdSection ||
            SETTINGS.hideSingleAdContactButtons ||
            SETTINGS.hideFeaturedSearchSection ||
            SETTINGS.hidePopularCitiesSection
        ) {
            let updateScheduled = false;
            const observer = new MutationObserver(() => {
                if (updateScheduled) return;
                updateScheduled = true;
                requestAnimationFrame(() => {
                    updateScheduled = false;
                    applyEscortSectionVisibility();
                });
            });
            observer.observe(document.documentElement, {
                childList: true,
                subtree: true
            });
        }
    }

    function findEscortMainResultsHeading(doc = document) {
        const section = getEscortMainResultsSection(doc);
        if (section) return section.querySelector('h1');

        // Fallback dla starszego wariantu /szukaj/.
        return getEscortSectionTitleCandidates(doc)
            .find(el => isEscortMainResultsHeading(el.textContent || '')) || null;
    }

    function getEscortClubListContext(doc = document, pageUrl = location.href) {
        const cardSelector = '.item-col.col > a[href*="/anons/"]';

        // Aktualny HTML Escort.club umieszcza właściwe wyniki w osobnym
        // section.content-sec.-index. To od razu wyklucza Wyróżnione i Polecane.
        const mainSection = getEscortMainResultsSection(doc);

        if (mainSection) {
            return {
                anchors: [...mainSection.querySelectorAll(cardSelector)],
                adsNav: mainSection.querySelector('.ads-nav'),
                heading: mainSection.querySelector('h1')
            };
        }

        let pagePath = location.pathname;

        try {
            pagePath = new URL(pageUrl, location.href).pathname;
        } catch (_) {}

        // Fallback dla starszej wersji strony /szukaj/, gdzie sekcja główna
        // mogła nie mieć klasy content-sec -index.
        if (isEscortSearchPath(pagePath)) {
            const heading = findEscortMainResultsHeading(doc);
            if (!heading) return null;

            const labels = getEscortSectionTitleCandidates(doc);
            const headingIndex = labels.indexOf(heading);
            const nextBoundary = headingIndex >= 0
                ? labels.slice(headingIndex + 1)[0] || null
                : labels.find(el => isElementAfter(heading, el)) || null;

            const isInsideTargetSection = el => {
                if (!isElementAfter(heading, el)) return false;
                if (!nextBoundary) return true;
                return isElementBefore(nextBoundary, el);
            };

            return {
                anchors: [...doc.querySelectorAll(cardSelector)].filter(isInsideTargetSection),
                adsNav: [...doc.querySelectorAll('.ads-nav')].find(isInsideTargetSection) || null,
                heading
            };
        }

        // Ostatni fallback dla prostych stron /anonse/ bez nagłówka sekcji.
        const root = doc.querySelector('section.content-sec.-index') || doc;
        return {
            anchors: [...root.querySelectorAll(cardSelector)],
            adsNav: root.querySelector('.ads-nav'),
            heading: root.querySelector('h1')
        };
    }

    async function waitForEscortClubListContext(timeoutMs = 10000) {
        const startedAt = Date.now();

        while (Date.now() - startedAt < timeoutMs) {
            applyEscortSectionVisibility();

            const context = getEscortClubListContext();
            if (context?.anchors?.length) {
                return context;
            }

            await new Promise(resolve => setTimeout(resolve, 200));
        }

        return getEscortClubListContext();
    }

    function getEscortListCardContainer(anchors) {
        const cards = anchors
            .map(anchor => anchor.closest('.item-col.col'))
            .filter(Boolean);

        if (!cards.length) return null;

        const firstParent = cards[0].parentElement;
        if (!firstParent) return null;

        return firstParent;
    }


    function escortAdDataCacheKey(adId) {
        return `vm_escort_ad_data_${adId}`;
    }

    function normalizeEscortAdText(value) {
        return String(value || '').replace(/\s+/g, ' ').trim();
    }

    function normalizeEscortAdMultilineText(value) {
        return String(value || '')
            .replace(/\r\n?/g, '\n')
            .replace(/[^\S\r\n]+/g, ' ')
            .replace(/ *\n */g, '\n')
            .replace(/\n{3,}/g, '\n\n')
            .trim();
    }

    function extractEscortClubDescription(doc) {
        const box = doc?.querySelector('.content-desc');
        if (!box) return null;

        // Na polskiej wersji strony aktywna jest zakładka PL. Jeśli serwis nie
        // oznaczy aktywnej zakładki, wybieramy pierwszą dostępną wersję opisu.
        let source = box.querySelector(
            '.tab-content .tab-pane.show.active, ' +
            '.tab-content .tab-pane.active, ' +
            '.tab-content .tab-pane'
        );

        if (!source) {
            source = box.cloneNode(true);
            source.querySelectorAll('.label, .nav, script, style').forEach(el => el.remove());
        } else {
            source = source.cloneNode(true);
            source.querySelectorAll('script, style').forEach(el => el.remove());
        }

        source.querySelectorAll('br').forEach(br => br.replaceWith('\n'));
        return normalizeEscortAdMultilineText(source.textContent) || null;
    }

    function extractEscortClubPhoneId(doc) {
        const phoneLink = doc?.querySelector(
            '[data-show-phone][data-phone-id], [data-phone-id]'
        );
        const phoneId = String(phoneLink?.getAttribute('data-phone-id') || '').trim();
        return /^\d+$/.test(phoneId) ? phoneId : null;
    }

    function extractEscortClubAgencyOrSalonPhone(doc) {
        if (!doc) return false;

        const contactContainers = new Set();
        const phoneLink = doc.querySelector(
            '.adsPhone [data-show-phone], .adsPhone a[href^="tel:"], ' +
            '[data-show-phone][data-phone-id]'
        );
        const closestContactContainer = phoneLink?.closest('.content-contact');
        if (closestContactContainer) contactContainers.add(closestContactContainer);

        for (const phoneContainer of doc.querySelectorAll('.adsPhone')) {
            const contactContainer = phoneContainer.closest('.content-contact');
            contactContainers.add(contactContainer || phoneContainer);
        }

        return [...contactContainers].some(container =>
            /\(\s*numer\s+telefonu\b[^)]*\)/i.test(
                normalizeEscortAdText(container.textContent)
            )
        );
    }

    function parseEscortClubPhoneResponse(response) {
        const raw = response?.responseText || '';
        let payload = response?.response;

        if (!payload || typeof payload !== 'object') {
            try {
                payload = JSON.parse(raw);
            } catch (_) {
                throw new Error(`Nieprawidłowa odpowiedź numeru: ${raw.slice(0, 120) || 'pusta'}`);
            }
        }

        const phone = normalizeEscortAdText(payload?.phone);
        const phoneDigits = normalizeEscortCachedPhone(phone);
        if (!phone || !phoneDigits) {
            throw new Error(payload?.error || payload?.message || 'Odpowiedź nie zawiera numeru');
        }

        return { phone, phoneDigits };
    }

    async function fetchEscortClubPhone(phoneId, cancelToken = null) {
        if (!/^\d+$/.test(String(phoneId || ''))) {
            throw new Error('Brak prawidłowego data-phone-id');
        }

        const response = await gmRequest({
            method: 'POST',
            url: ESCORT_PHONE_ENDPOINT,
            cancelToken,
            timeout: ESCORT_AD_DATA_FETCH_TIMEOUT_MS,
            headers: {
                'Accept': 'application/json, text/javascript, */*; q=0.01',
                'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
                'X-Requested-With': 'XMLHttpRequest'
            },
            data: new URLSearchParams({ id: String(phoneId) }).toString()
        });

        if (response.status && (response.status < 200 || response.status >= 400)) {
            throw new Error(`HTTP ${response.status}`);
        }

        return parseEscortClubPhoneResponse(response);
    }

    function extractEscortClubAvailability(doc) {
        if (!doc) return {};

        const box = [...doc.querySelectorAll('.sub-info-box')]
            .find(candidate => normalizeText(
                candidate.querySelector('.label')?.textContent
            ) === 'godziny dostępności');

        if (!box) return {};

        const availability = {};
        for (const row of box.querySelectorAll('.sub-info-items .sub-info-elem')) {
            const day = normalizeEscortAdText(row.querySelector('.sub-label')?.textContent)
                .replace(/:\s*$/, '');
            const hours = normalizeEscortAdText(row.querySelector('.desc')?.textContent);
            if (day && hours) availability[day] = hours;
        }

        return availability;
    }

    function extractEscortClubTags(doc) {
        if (!doc) return [];

        const box = [...doc.querySelectorAll('.sub-info-box')]
            .find(candidate => normalizeText(
                candidate.querySelector('.label')?.textContent
            ) === 'tagi');
        if (!box) return [];

        const tags = [];
        const seen = new Set();
        for (const element of box.querySelectorAll('.tags-box .tag')) {
            const tag = normalizeEscortAdText(element.textContent);
            const key = tag.toLocaleLowerCase('pl-PL');
            if (!tag || seen.has(key)) continue;
            seen.add(key);
            tags.push(tag);
        }

        return tags;
    }

    function extractEscortClubLocation(doc) {
        const location = {
            country: null,
            province: null,
            city: null,
            district: null,
            area: null,
            addressText: null,
            addressParts: []
        };

        if (!doc) return location;

        const locationBox = [...doc.querySelectorAll('.content-name .content-location, .content-info-col .content-location')]
            .find(box => box.querySelector('.sub-label'));
        const label = locationBox?.querySelector('.sub-label') || null;

        if (label) {
            location.addressText = normalizeEscortAdText(label.textContent) || null;

            for (const link of label.querySelectorAll('a[href]')) {
                const text = normalizeEscortAdText(link.textContent);
                if (!text) continue;

                try {
                    const url = new URL(link.getAttribute('href'), 'https://pl.escort.club/');
                    const path = url.pathname;

                    if (/^\/anonse\/towarzyskie\/poland\/?$/i.test(path)) {
                        location.country = text;
                        continue;
                    }

                    if (/^\/szukaj\/?$/i.test(path) && url.searchParams.has('province')) {
                        location.province = text;
                        continue;
                    }

                    if (/^\/anonse\/towarzyskie\/[^/]+\/?$/i.test(path)) {
                        if (url.searchParams.has('district')) {
                            location.district = text;
                        } else if (!/^poland$/i.test(path.split('/').filter(Boolean).pop() || '')) {
                            location.city = text;
                        }
                    }
                } catch (_) {}
            }
        }

        const mapLink = doc.querySelector(
            '.content-location #showMap[adres], .content-location .content-map[adres]'
        );
        if (mapLink) {
            location.addressParts = String(mapLink.getAttribute('adres') || '')
                .split(',')
                .map(part => normalizeEscortAdText(part))
                .filter(Boolean);
        }

        location.city = location.city || extractEscortClubCity(doc);

        // Dla struktury Escort.club adres zwykle ma: województwo, miasto,
        // opcjonalnie dzielnicę i bardziej szczegółową lokalizację. Linki mają
        // pierwszeństwo; części adresu służą tylko jako uzupełnienie.
        if (location.addressParts.length) {
            if (!location.province) location.province = location.addressParts[0] || null;
            if (!location.city) location.city = location.addressParts[1] || null;
            if (!location.district && location.addressParts.length >= 4) {
                location.district = location.addressParts[2] || null;
            }

            const areaIndex = location.district ? 3 : 2;
            if (location.addressParts.length > areaIndex) {
                location.area = location.addressParts.slice(areaIndex).join(', ') || null;
            }
        }

        return location;
    }

    function extractEscortClubProfileStats(doc) {
        const stats = {};
        if (!doc) return stats;

        // Cennik również korzysta z klas .stats-box/.stat-elem, dlatego
        // odczytujemy wyłącznie właściwą sekcję „Więcej o mnie”.
        for (const elem of doc.querySelectorAll('.content-hours .stats-box .stat-elem')) {
            const label = normalizeEscortAdText(elem.querySelector('.sub-label')?.textContent)
                .replace(/:\s*$/, '');
            const value = normalizeEscortAdText(elem.querySelector('.sub-desc')?.textContent);
            if (!label) continue;

            // Pusta wartość istniejącego wiersza jest prawidłowym stanem danych.
            // Zachowujemy null, aby można było wykryć zmianę np. „5 → brak”.
            stats[label] = value || null;
        }

        return stats;
    }

    function extractEscortClubStructuredAdData(doc) {
        if (!doc) return {};

        for (const script of doc.querySelectorAll('script[type="application/ld+json"]')) {
            try {
                const parsed = JSON.parse(script.textContent || 'null');
                const queue = Array.isArray(parsed) ? [...parsed] : [parsed];

                while (queue.length) {
                    const item = queue.shift();
                    if (!item || typeof item !== 'object') continue;
                    if (Array.isArray(item['@graph'])) queue.push(...item['@graph']);

                    const types = Array.isArray(item['@type']) ? item['@type'] : [item['@type']];
                    if (!types.some(type => String(type || '').toLowerCase() === 'classifiedad')) {
                        continue;
                    }

                    return {
                        datePosted: item.datePosted || null,
                        structuredCity: item.location?.address?.addressLocality || item.location?.name || null
                    };
                }
            } catch (_) {}
        }

        return {};
    }

    function normalizeEscortClubPriceDurationKey(value) {
        const normalized = normalizeEscortAdText(value)
            .toLowerCase()
            .replace(/,/g, '.')
            .replace(/:\s*$/, '');

        if (!normalized) return null;
        if (/\bnoc\b|ca(?:ł|l)[aą]?\s+noc/.test(normalized)) return 'night';
        if (/\bp[oó]ł\s*(?:godz|godzin)/.test(normalized)) return '30';

        const minutesMatch = normalized.match(/(\d+(?:\.\d+)?)\s*(?:min|minut)/);
        if (minutesMatch) {
            const minutes = Math.round(Number(minutesMatch[1]));
            return Number.isFinite(minutes) && minutes > 0 ? String(minutes) : null;
        }

        const hoursMatch = normalized.match(/(\d+(?:\.\d+)?)\s*(?:h\b|godz|godzin)/);
        if (hoursMatch) {
            const minutes = Math.round(Number(hoursMatch[1]) * 60);
            return Number.isFinite(minutes) && minutes > 0 ? String(minutes) : null;
        }

        if (/^(?:jedna\s+)?godzina$/.test(normalized)) return '60';
        return null;
    }

    function parseEscortClubPrice(value) {
        const raw = normalizeEscortAdText(value).replace(/\u00a0/g, ' ');
        if (!raw) return null;

        const match = raw.match(/(\d[\d\s]*(?:[.,]\d+)?)\s*(PLN|zł|EUR|€|USD|\$|GBP|£)?/i);
        if (!match) return null;

        const amount = Number(match[1].replace(/\s/g, '').replace(',', '.'));
        if (!Number.isFinite(amount)) return null;

        const currencyToken = String(match[2] || '').toUpperCase();
        const currency = currencyToken === 'ZŁ' ? 'PLN'
            : currencyToken === '€' ? 'EUR'
                : currencyToken === '$' ? 'USD'
                    : currencyToken === '£' ? 'GBP'
                        : currencyToken || null;

        return { amount, currency };
    }

    function extractEscortClubPrices(doc) {
        const prices = {};
        if (!doc) return prices;

        for (const elem of doc.querySelectorAll(
            '.contant-prices .stat-elem, .content-prices .stat-elem'
        )) {
            const rawLabel = normalizeEscortAdText(
                elem.querySelector('.sub-label')?.textContent
            ).replace(/:\s*$/, '');
            const durationKey = normalizeEscortClubPriceDurationKey(rawLabel);
            const parsedPrice = parseEscortClubPrice(
                elem.querySelector('.sub-desc')?.textContent
            );

            if (!durationKey) continue;
            prices[durationKey] = parsedPrice || {
                amount: null,
                currency: null
            };
        }

        return prices;
    }

    function formatEscortClubDatePosted(value) {
        const raw = String(value || '').trim();
        if (!raw) return '';

        const match = raw.match(/^(\d{4})-(\d{2})-(\d{2})$/);
        if (!match) return raw;

        const date = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
        if (
            Number.isNaN(date.getTime()) ||
            date.getFullYear() !== Number(match[1]) ||
            date.getMonth() !== Number(match[2]) - 1 ||
            date.getDate() !== Number(match[3])
        ) {
            return raw;
        }

        try {
            return new Intl.DateTimeFormat('pl-PL', {
                day: 'numeric',
                month: 'long',
                year: 'numeric'
            }).format(date);
        } catch (_) {
            return raw;
        }
    }

    function initEscortClubDatePostedDisplay() {
        if (!/^\/anons\/\d+\.html\/?$/i.test(location.pathname)) return;

        const insertDate = () => {
            if (document.getElementById('vm-escort-date-posted')) return true;

            const profileHeading = document.querySelector(
                '.content-info-col.-desc > .content-name.-desc-name > h1, ' +
                '.content-info-col.-desc > .content-name > h1'
            );
            if (!profileHeading) return false;
            profileHeading.style.display = 'block';

            // Oryginalny układ centruje kolumny pionowo. Po dodaniu metadanych
            // wyższa prawa kolumna obniżała więc galerię o połowę różnicy.
            // Wyrównanie do góry zachowuje pierwotne położenie zdjęcia.
            const profileColumn = profileHeading.closest('.content-info-col.-desc');
            const profileWrapper = profileColumn?.closest('.content-info-wrapper.-left');
            const galleryColumn = profileWrapper?.querySelector('.content-gallery-col');
            if (profileWrapper) profileWrapper.style.alignItems = 'flex-start';
            if (profileColumn) {
                profileColumn.style.alignSelf = 'flex-start';
                profileColumn.style.verticalAlign = 'top';
            }
            if (galleryColumn) {
                galleryColumn.style.alignSelf = 'flex-start';
                galleryColumn.style.verticalAlign = 'top';
            }

            const datePosted = formatEscortClubDatePosted(
                extractEscortClubStructuredAdData(document).datePosted
            );
            if (!datePosted) return true;

            // Metadane są częścią nagłówka profilu, a nie osobnym elementem
            // siatki strony. Dzięki temu przesuwają tylko nazwę, nie galerię.
            const row = makeElement('span');
            row.id = 'vm-escort-date-posted';
            row.setAttribute('aria-label', `Data publikacji anonsu: ${datePosted}`);
            Object.assign(row.style, {
                display: 'block',
                boxSizing: 'border-box',
                position: 'static',
                float: 'none',
                clear: 'both',
                width: '100%',
                margin: '0 0 18px',
                padding: '4px 10px',
                borderLeft: `3px solid ${getEscortPagePinkColor()}`,
                background: 'transparent',
                color: getEscortPagePinkColor(),
                fontSize: '14px',
                lineHeight: '1.35',
                textAlign: 'left',
                whiteSpace: 'normal'
            });

            const publishedLine = makeElement('span');
            Object.assign(publishedLine.style, {
                display: 'flex',
                position: 'static',
                float: 'none',
                alignItems: 'baseline',
                gap: '6px',
                whiteSpace: 'nowrap'
            });

            const label = makeElement('span', '', 'Opublikowano:');

            const date = makeElement('strong', '', datePosted);
            date.style.color = getEscortPagePinkColor();

            publishedLine.append(label, date);
            row.appendChild(publishedLine);

            const pendingLastVisit = document.getElementById(ESCORT_LAST_VISIT_ID);
            if (pendingLastVisit) row.appendChild(pendingLastVisit);

            const visitSummary = document.getElementById(ESCORT_VISIT_SUMMARY_ID);
            if (visitSummary) {
                row.style.marginBottom = '5px';
                visitSummary.insertAdjacentElement('beforebegin', row);
            } else {
                profileHeading.insertAdjacentElement('afterbegin', row);
            }
            return true;
        };

        if (insertDate()) return;

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', insertDate, { once: true });
        } else {
            window.addEventListener('load', insertDate, { once: true });
        }
    }

    function extractEscortClubTitle(doc) {
        const heading = doc?.querySelector(
            '.content-info-col .content-name h1, .content-name.-desc-name h1'
        );
        if (!heading) return null;

        const source = heading.cloneNode(true);
        source.querySelectorAll(
            '#vm-escort-date-posted, #vm-escort-visit-summary, #vm-escort-last-visited'
        ).forEach(element => element.remove());
        return normalizeEscortAdText(source.textContent) || null;
    }

    function extractEscortClubAdData(doc, adId, adUrl) {
        const structured = extractEscortClubStructuredAdData(doc);
        const location = extractEscortClubLocation(doc);
        if (!location.city && structured.structuredCity) {
            location.city = normalizeEscortAdText(structured.structuredCity) || null;
        }

        return {
            adId: String(adId),
            adUrl,
            phoneId: extractEscortClubPhoneId(doc),
            isAgencyOrSalonPhone: extractEscortClubAgencyOrSalonPhone(doc),
            title: extractEscortClubTitle(doc),
            description: extractEscortClubDescription(doc),
            availability: extractEscortClubAvailability(doc),
            tags: extractEscortClubTags(doc),
            prices: extractEscortClubPrices(doc),
            datePosted: structured.datePosted || null,
            location,
            stats: extractEscortClubProfileStats(doc)
        };
    }

    function normalizeEscortChangeComparisonValue(value) {
        return normalizeEscortAdText(value)
            .normalize('NFKC')
            .toLocaleLowerCase('pl-PL');
    }

    function getEscortObjectEntryByNormalizedKey(object, key) {
        const wanted = normalizeEscortChangeComparisonValue(key).replace(/:\s*$/, '');
        return Object.entries(object || {}).find(([candidate]) =>
            normalizeEscortChangeComparisonValue(candidate).replace(/:\s*$/, '') === wanted
        ) || null;
    }

    function formatEscortPhoneForDisplay(value) {
        const raw = normalizeEscortAdText(value).replace(/^tel:\s*/i, '');
        const digits = normalizeEscortCachedPhone(raw);
        if (!digits) return raw;
        if (digits.length === 9) {
            return '+48 ' + digits.replace(/(\d{3})(\d{3})(\d{3})/, '$1 $2 $3');
        }
        return raw.startsWith('+') ? raw : `+${digits}`;
    }

    function getEscortComparableAddress(data) {
        if (!data || typeof data !== 'object') return null;
        if (Object.prototype.hasOwnProperty.call(data, 'address')) {
            return normalizeEscortAdText(data.address) || null;
        }

        const location = data.location || {};
        const addressText = normalizeEscortAdText(location.addressText);
        if (addressText) return addressText;

        const addressParts = Array.isArray(location.addressParts)
            ? location.addressParts.map(normalizeEscortAdText).filter(Boolean)
            : [];
        if (addressParts.length) return addressParts.join(', ');

        return [
            location.country,
            location.province,
            location.city,
            location.district,
            location.area
        ].map(normalizeEscortAdText).filter(Boolean).join(', ') || null;
    }

    function createEscortVisitSnapshot(data, previousSnapshot = null) {
        const currentPhoneDigits = normalizeEscortCachedPhone(
            data?.phoneDigits || data?.phone
        );
        const previousPhoneDigits = normalizeEscortCachedPhone(
            previousSnapshot?.phoneDigits || previousSnapshot?.phone
        );
        const hasCurrentPhone = !!currentPhoneDigits;

        return {
            title: normalizeEscortAdText(data?.title) || null,
            address: getEscortComparableAddress(data),
            phone: hasCurrentPhone
                ? (data.phone || formatEscortPhoneForDisplay(currentPhoneDigits))
                : (previousPhoneDigits ? previousSnapshot.phone : null),
            phoneDigits: hasCurrentPhone
                ? currentPhoneDigits
                : (previousPhoneDigits || null),
            description: data?.description || null,
            availability: { ...(data?.availability || {}) },
            prices: Object.fromEntries(Object.entries(data?.prices || {}).map(
                ([key, value]) => [key, value && typeof value === 'object' ? { ...value } : value]
            )),
            stats: { ...(data?.stats || {}) },
            tags: Array.isArray(data?.tags) ? [...data.tags] : []
        };
    }

    function readEscortPhoneFromCurrentPage() {
        for (const link of document.querySelectorAll('[data-show-phone][data-phone-id], .adsPhone a[href]')) {
            for (const value of [
                link.getAttribute('href'),
                link.getAttribute('data-phone'),
                link.textContent
            ]) {
                const digits = normalizeEscortCachedPhone(value);
                if (digits) {
                    return {
                        phone: formatEscortPhoneForDisplay(value),
                        phoneDigits: digits,
                        phoneFetchStatus: 'page',
                        phoneCheckedAt: Date.now()
                    };
                }
            }
        }
        return null;
    }

    async function resolveEscortPhoneForCurrentPage(phoneId) {
        // Dajemy stronie chwilę na obsłużenie kliknięcia „Pokaż numer”, które
        // skrypt wykonuje już na potrzeby przycisków Garso/Escorti.
        for (let attempt = 0; attempt < 9; attempt++) {
            const fromPage = readEscortPhoneFromCurrentPage();
            if (fromPage) return fromPage;
            await new Promise(resolve => setTimeout(resolve, 200));
        }

        if (!phoneId) return null;

        try {
            const fetched = await fetchEscortClubPhone(phoneId);
            return {
                ...fetched,
                phoneFetchStatus: 'ok',
                phoneCheckedAt: Date.now()
            };
        } catch (error) {
            log('Nie udało się odświeżyć numeru podczas zapisu wizyty', error);
            return null;
        }
    }

    function escortPricesEquivalent(previous, current) {
        if (!previous || !current) return false;

        const previousHasAmount = previous.amount != null &&
            Number.isFinite(Number(previous.amount));
        const currentHasAmount = current.amount != null &&
            Number.isFinite(Number(current.amount));

        if (previousHasAmount !== currentHasAmount) return false;
        if (
            previousHasAmount &&
            Number(previous.amount) !== Number(current.amount)
        ) {
            return false;
        }

        const previousCurrency = normalizeEscortAdText(previous.currency).toUpperCase();
        const currentCurrency = normalizeEscortAdText(current.currency).toUpperCase();
        return previousCurrency === currentCurrency;
    }

    function formatEscortChangedPrice(price) {
        return formatEscortTilePrice(price) || 'brak';
    }

    function compareEscortAdVisitData(previous, current) {
        const changes = {
            title: null,
            address: null,
            prices: [],
            availability: [],
            tags: null,
            description: null,
            phone: null,
            stats: [],
            labels: []
        };

        if (!previous || !current) return changes;

        const previousTitle = normalizeEscortAdText(previous.title);
        const currentTitle = normalizeEscortAdText(current.title);
        if (
            (previousTitle || currentTitle) &&
            normalizeEscortChangeComparisonValue(previousTitle) !==
                normalizeEscortChangeComparisonValue(currentTitle)
        ) {
            changes.title = {
                previous: previousTitle || null,
                current: currentTitle || null
            };
            changes.labels.push('nazwę anonsu');
        }

        const previousAddress = getEscortComparableAddress(previous);
        const currentAddress = getEscortComparableAddress(current);
        if (
            (previousAddress || currentAddress) &&
            normalizeEscortChangeComparisonValue(previousAddress) !==
                normalizeEscortChangeComparisonValue(currentAddress)
        ) {
            changes.address = {
                previous: previousAddress,
                current: currentAddress
            };
            changes.labels.push('adres');
        }

        const previousPrices = previous.prices || {};
        const currentPrices = current.prices || {};
        const priceDurationKeys = new Set([
            ...Object.keys(previousPrices),
            ...Object.keys(currentPrices)
        ]);

        for (const durationKey of priceDurationKeys) {
            const previousHasLine = Object.prototype.hasOwnProperty.call(
                previousPrices,
                durationKey
            );
            const currentHasLine = Object.prototype.hasOwnProperty.call(
                currentPrices,
                durationKey
            );
            const previousPrice = previousHasLine ? previousPrices[durationKey] : null;
            const currentPrice = currentHasLine ? currentPrices[durationKey] : null;

            if (
                previousHasLine &&
                currentHasLine &&
                escortPricesEquivalent(previousPrice, currentPrice)
            ) {
                continue;
            }

            changes.prices.push({
                durationKey,
                previous: previousPrice,
                current: currentPrice,
                type: !previousHasLine
                    ? 'added'
                    : (!currentHasLine ? 'removed' : 'changed')
            });
        }
        if (changes.prices.length) {
            changes.labels.push(
                changes.prices.some(change => change.type !== 'changed')
                    ? 'cennik'
                    : 'cenę'
            );
        }

        for (const [day, currentHours] of Object.entries(current.availability || {})) {
            const previousEntry = getEscortObjectEntryByNormalizedKey(previous.availability, day);
            const previousHours = previousEntry?.[1];
            if (
                !previousHours ||
                normalizeEscortChangeComparisonValue(previousHours) ===
                    normalizeEscortChangeComparisonValue(currentHours)
            ) {
                continue;
            }

            changes.availability.push({
                day,
                previous: previousHours,
                current: currentHours
            });
        }
        if (changes.availability.length) changes.labels.push('godziny dostępności');

        // Brak pola tags oznacza wpis cache utworzony przed dodaniem obsługi
        // tagów. Pierwszy odczyt tylko uzupełnia taki wpis, bez fałszywego
        // komunikatu, że wszystkie aktualne tagi zostały właśnie dodane.
        if (Array.isArray(previous.tags) && Array.isArray(current.tags)) {
            const previousTags = new Map(previous.tags.map(tag => [
                normalizeEscortChangeComparisonValue(tag),
                normalizeEscortAdText(tag)
            ]).filter(([key]) => key));
            const currentTags = new Map(current.tags.map(tag => [
                normalizeEscortChangeComparisonValue(tag),
                normalizeEscortAdText(tag)
            ]).filter(([key]) => key));

            const added = [...currentTags]
                .filter(([key]) => !previousTags.has(key))
                .map(([, tag]) => tag);
            const removed = [...previousTags]
                .filter(([key]) => !currentTags.has(key))
                .map(([, tag]) => tag);

            if (added.length || removed.length) {
                changes.tags = { added, removed };
                changes.labels.push('tagi');
            }
        }

        if (
            previous.description &&
            current.description &&
            normalizeEscortChangeComparisonValue(previous.description) !==
                normalizeEscortChangeComparisonValue(current.description)
        ) {
            changes.description = {
                previous: previous.description,
                current: current.description
            };
            changes.labels.push('opis');
        }

        const previousPhoneDigits = normalizeEscortCachedPhone(
            previous.phoneDigits || previous.phone
        );
        const currentPhoneDigits = normalizeEscortCachedPhone(
            current.phoneDigits || current.phone
        );
        if (
            previousPhoneDigits &&
            currentPhoneDigits &&
            previousPhoneDigits !== currentPhoneDigits
        ) {
            changes.phone = {
                previous: previous.phone || previousPhoneDigits,
                current: current.phone || currentPhoneDigits
            };
            changes.labels.push('numer telefonu');
        }

        for (const [currentLabel, currentValue] of Object.entries(current.stats || {})) {
            const previousEntry = getEscortObjectEntryByNormalizedKey(
                previous.stats,
                currentLabel
            );
            if (!previousEntry) continue;

            const previousValue = previousEntry[1];
            if (
                normalizeEscortChangeComparisonValue(previousValue) ===
                normalizeEscortChangeComparisonValue(currentValue)
            ) {
                continue;
            }

            changes.stats.push({
                label: currentLabel,
                previous: previousValue,
                current: currentValue
            });
        }

        if (changes.stats.length) {
            const onlyBreastChanged = changes.stats.length === 1 &&
                normalizeEscortChangeComparisonValue(changes.stats[0].label) === 'biust';
            changes.labels.push(onlyBreastChanged ? 'rozmiar biustu' : 'dane profilu');
        }

        return changes;
    }

    function ensureEscortVisitChangeStyles() {
        if (document.getElementById(ESCORT_CHANGE_STYLE_ID)) return;

        const changeRedColor = '#ff4d4f';
        const style = makeElement('style');
        style.id = ESCORT_CHANGE_STYLE_ID;
        style.textContent = [
            '.vm-escort-changed-fragment {',
            '  background: rgba(255, 77, 79, .12) !important;',
            '  box-shadow: inset 3px 0 0 ' + changeRedColor + ' !important;',
            '}',
            '.vm-escort-change-note {',
            '  box-sizing: border-box;',
            '  margin: 2px 0 8px;',
            '  padding: 4px 7px;',
            '  border-left: 2px solid ' + changeRedColor + ';',
            '  color: ' + changeRedColor + ';',
            '  font: 700 11px/1.3 Lato, Arial, sans-serif;',
            '}',
            '.vm-escort-description-change {',
            '  margin: 10px 0 0;',
            '  padding: 8px 10px 10px;',
            '  border: 1px solid ' + changeRedColor + ';',
            '  border-radius: 6px;',
            '  background: rgba(255, 77, 79, .10);',
            '  color: ' + changeRedColor + ';',
            '  font: 700 11px/1.35 Lato, Arial, sans-serif;',
            '}',
            '.vm-escort-description-change summary {',
            '  cursor: pointer;',
            '  color: ' + changeRedColor + ';',
            '  font-weight: 700;',
            '}',
            '.vm-escort-description-current-changed {',
            '  padding-left: 8px;',
            '  box-shadow: inset 3px 0 0 ' + changeRedColor + ';',
            '}',
            '.vm-escort-description-previous {',
            '  max-height: 260px;',
            '  margin-top: 9px;',
            '  padding: 12px;',
            '  overflow: auto;',
            '  border-radius: 5px;',
            '  background: rgba(255, 255, 255, .96);',
            '  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);',
            '  color: #2f2530;',
            '  font-weight: 400;',
            '  line-height: 1.5;',
            '  white-space: pre-wrap;',
            '}'
        ].join('\n');
        document.head.appendChild(style);
    }

    function addEscortPreviousValueNote(element, text) {
        if (!element || element.dataset.vmEscortChangeMarked === '1') return;

        element.dataset.vmEscortChangeMarked = '1';
        element.classList.add('vm-escort-changed-fragment');

        const note = makeElement('div', 'vm-escort-change-note', text);
        element.insertAdjacentElement('afterend', note);
    }

    function addEscortPriceSectionChangeNote(priceBox, text) {
        if (!priceBox) {
            priceBox = document.getElementById('vm-escort-removed-price-section');

            if (!priceBox) {
                const info = document.querySelector(
                    '.content-info-col.-info .content-info, .content-info-col .content-info'
                );
                if (!info) return;

                priceBox = makeElement('div');
                priceBox.id = 'vm-escort-removed-price-section';
                priceBox.style.marginTop = '12px';

                const removedLabel = makeElement('div', 'label', 'Zmiany w cenniku');
                priceBox.appendChild(removedLabel);
                info.appendChild(priceBox);
            }
        }

        const label = priceBox.querySelector(':scope > .label, .label');
        if (label) label.classList.add('vm-escort-changed-fragment');

        let notes = priceBox.querySelector('.vm-escort-price-section-changes');
        if (!notes) {
            notes = makeElement('div');
            notes.className = 'vm-escort-price-section-changes';
            if (label) label.insertAdjacentElement('afterend', notes);
            else priceBox.prepend(notes);
        }

        const note = makeElement('div', 'vm-escort-change-note', text);
        notes.appendChild(note);
    }

    function formatEscortChangedValue(value) {
        return normalizeEscortAdText(value) || 'brak';
    }

    function renderEscortTitleChange(change) {
        if (!change) return;

        const heading = document.querySelector(
            '.content-info-col.-desc > .content-name.-desc-name > h1, ' +
            '.content-info-col.-desc > .content-name > h1'
        );
        if (!heading) return;

        const currentKey = normalizeEscortChangeComparisonValue(change.current);
        const titleNode = currentKey
            ? [...heading.childNodes].find(node =>
                node.nodeType === Node.TEXT_NODE &&
                normalizeEscortChangeComparisonValue(node.textContent) === currentKey
            )
            : null;
        if (titleNode) {
            const markedTitle = makeElement('span', 'vm-escort-changed-fragment');
            markedTitle.style.marginRight = '6px';
            markedTitle.textContent = formatEscortChangedValue(change.current);
            titleNode.replaceWith(markedTitle);
        }

        if (!document.getElementById('vm-escort-title-change-note')) {
            const note = makeElement('div');
            note.id = 'vm-escort-title-change-note';
            note.className = 'vm-escort-change-note';
            note.textContent =
                'Nazwa zmieniona: ' +
                formatEscortChangedValue(change.previous) +
                ' → ' +
                formatEscortChangedValue(change.current);
            heading.insertAdjacentElement('afterend', note);
        }
    }

    function renderEscortAddressChange(change) {
        if (!change) return;

        const locationBox = document.querySelector(
            '.content-info-col.-desc .content-name > .content-location'
        );
        addEscortPreviousValueNote(
            locationBox,
            'Adres zmieniony: ' +
                formatEscortChangedValue(change.previous) +
                ' → ' +
                formatEscortChangedValue(change.current)
        );
    }

    function findEscortSubInfoBox(labelText) {
        return [...document.querySelectorAll('.sub-info-box')]
            .find(candidate => normalizeText(
                candidate.querySelector('.label')?.textContent
            ) === labelText) || null;
    }

    function renderEscortTagChanges(change) {
        if (!change) return;

        let box = findEscortSubInfoBox('tagi');
        if (!box) {
            const column = document.querySelector('.anons-info-sec .sub-info-col');
            if (!column) return;

            box = makeElement('div');
            box.className = 'sub-info-box -tags';

            const label = makeElement('div', 'label', 'Tagi');
            box.appendChild(label);
            column.appendChild(box);
        }

        const label = box.querySelector(':scope > .label, .label');
        if (label) label.classList.add('vm-escort-changed-fragment');

        let notes = box.querySelector('.vm-escort-tag-changes');
        if (!notes) {
            notes = makeElement('div');
            notes.className = 'vm-escort-tag-changes';
            notes.style.marginTop = '8px';
            box.appendChild(notes);
        }

        const appendNote = text => {
            const note = makeElement('div', 'vm-escort-change-note', text);
            notes.appendChild(note);
        };

        if (change.added?.length) {
            appendNote('Dodano tagi: ' + change.added.join(', '));
        }
        if (change.removed?.length) {
            appendNote('Usunięto tagi: ' + change.removed.join(', '));
        }
    }

    function findEscortStatRow(label) {
        const wanted = normalizeEscortChangeComparisonValue(label).replace(/:\s*$/, '');
        return [...document.querySelectorAll('.stats-box .stat-elem')]
            .find(row =>
                normalizeEscortChangeComparisonValue(
                    row.querySelector('.sub-label')?.textContent
                ).replace(/:\s*$/, '') === wanted
            ) || null;
    }

    function renderEscortVisitChanges(changes) {
        ensureEscortVisitChangeStyles();

        renderEscortTitleChange(changes.title);
        renderEscortAddressChange(changes.address);

        const priceBox = document.querySelector('.contant-prices, .content-prices');
        for (const change of changes.prices) {
            const row = [...document.querySelectorAll(
                '.contant-prices .stat-elem, .content-prices .stat-elem'
            )].find(candidate =>
                normalizeEscortClubPriceDurationKey(
                    candidate.querySelector('.sub-label')?.textContent
                ) === change.durationKey
            );

            const durationLabel = getEscortPriceDurationLabel(change.durationKey);
            const prefix = change.type === 'added'
                ? 'Dodano pozycję cennika'
                : (change.type === 'removed'
                    ? 'Usunięto pozycję cennika'
                    : 'Cena zmieniona');
            const message =
                prefix + ' (' + durationLabel + '): ' +
                formatEscortChangedPrice(change.previous) +
                ' → ' +
                formatEscortChangedPrice(change.current);

            if (row) addEscortPreviousValueNote(row, message);
            else addEscortPriceSectionChangeNote(priceBox, message);
        }

        const availabilityBox = findEscortSubInfoBox('godziny dostępności');
        for (const change of changes.availability) {
            const row = [...(availabilityBox?.querySelectorAll('.sub-info-elem') || [])]
                .find(candidate =>
                    normalizeEscortChangeComparisonValue(
                        candidate.querySelector('.sub-label')?.textContent
                    ).replace(/:\s*$/, '') ===
                    normalizeEscortChangeComparisonValue(change.day).replace(/:\s*$/, '')
                );

            addEscortPreviousValueNote(
                row,
                'Godziny zmienione: ' + change.previous + ' → ' + change.current
            );
        }

        renderEscortTagChanges(changes.tags);

        if (changes.description) {
            const description = document.querySelector('.content-desc');
            if (description && !description.querySelector('.vm-escort-description-change')) {
                const currentText = description.querySelector('.tab-content') || description;
                currentText.classList.add('vm-escort-description-current-changed');

                const details = makeElement('details', 'vm-escort-description-change');

                const summary = makeElement('summary', '', 'Opis zmieniony - pokaż poprzednią wersję');

                const previous = makeElement('div', 'vm-escort-description-previous', changes.description.previous);

                details.append(summary, previous);
                description.appendChild(details);
            }
        }

        if (changes.phone) {
            for (const phoneBox of document.querySelectorAll('.adsPhone')) {
                addEscortPreviousValueNote(
                    phoneBox,
                    'Numer telefonu zmieniony: ' +
                        formatEscortPhoneForDisplay(changes.phone.previous) +
                        ' → ' +
                        formatEscortPhoneForDisplay(changes.phone.current)
                );
            }
        }

        for (const change of changes.stats) {
            const isBreast =
                normalizeEscortChangeComparisonValue(change.label) === 'biust';
            addEscortPreviousValueNote(
                findEscortStatRow(change.label),
                (isBreast
                    ? 'Rozmiar biustu zmieniony: '
                    : 'Zmiana „' + change.label + '”: ') +
                    formatEscortChangedValue(change.previous) +
                    ' → ' +
                    formatEscortChangedValue(change.current)
            );
        }
    }

    function polishElapsedValue(value, one, few, many) {
        const last = value % 10;
        const lastTwo = value % 100;
        const word = value === 1
            ? one
            : (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14) ? few : many);
        return value + ' ' + word + ' temu';
    }

    function formatEscortLastVisit(timestamp) {
        const elapsedMs = Date.now() - Number(timestamp);
        if (!Number.isFinite(elapsedMs) || elapsedMs < 0) return '';

        const minutes = Math.floor(elapsedMs / 60000);
        if (minutes < 1) return 'przed chwilą';
        if (minutes < 60) return polishElapsedValue(minutes, 'minutę', 'minuty', 'minut');

        const hours = Math.floor(elapsedMs / 3600000);
        if (hours < 24) return polishElapsedValue(hours, 'godzinę', 'godziny', 'godzin');

        const days = Math.floor(elapsedMs / 86400000);
        if (days < 31) return polishElapsedValue(days, 'dzień', 'dni', 'dni');

        const months = Math.floor(days / 30.4375);
        if (months < 12) return polishElapsedValue(months, 'miesiąc', 'miesiące', 'miesięcy');

        const years = Math.floor(months / 12);
        return polishElapsedValue(years, 'rok', 'lata', 'lat');
    }

    function renderEscortVisitSummary(previous, changes, isFirstVisit = false) {
        if ((!previous && !isFirstVisit) || document.getElementById(ESCORT_LAST_VISIT_ID)) return;

        const profileHeading = document.querySelector(
            '.content-info-col.-desc > .content-name.-desc-name > h1, ' +
            '.content-info-col.-desc > .content-name > h1'
        );
        if (!profileHeading) return;
        profileHeading.style.display = 'block';

        const hasChanges = !isFirstVisit && changes.labels.length > 0;
        const pagePinkColor = getEscortPagePinkColor();
        const changeRedColor = '#ff4d4f';
        const visitLine = makeElement('span');
        visitLine.id = ESCORT_LAST_VISIT_ID;
        Object.assign(visitLine.style, {
            display: 'block',
            position: 'static',
            float: 'none',
            marginTop: '3px',
            color: pagePinkColor,
            font: '400 11px/1.35 Lato, Arial, sans-serif',
            opacity: '.72'
        });

        if (isFirstVisit) {
            visitLine.textContent = 'Wyświetlasz pierwszy raz';
        } else if (previous.lastVisitedAt) {
            visitLine.appendChild(document.createTextNode('Ostatnio odwiedzono: '));

            const relative = makeElement('strong', '', formatEscortLastVisit(previous.lastVisitedAt));
            relative.style.fontWeight = '600';
            relative.title = new Intl.DateTimeFormat('pl-PL', {
                dateStyle: 'medium',
                timeStyle: 'short'
            }).format(new Date(previous.lastVisitedAt));

            visitLine.appendChild(relative);
        } else {
            visitLine.textContent = 'Porównano z wcześniejszym zapisem w cache.';
        }

        const dateRow = document.getElementById('vm-escort-date-posted');
        if (dateRow) {
            dateRow.appendChild(visitLine);
            dateRow.style.marginBottom = hasChanges ? '5px' : '18px';
        } else {
            Object.assign(visitLine.style, {
                boxSizing: 'border-box',
                margin: hasChanges ? '0 0 5px' : '0 0 18px',
                padding: '4px 10px',
                borderLeft: '3px solid ' + pagePinkColor
            });
            profileHeading.insertAdjacentElement('afterbegin', visitLine);
        }

        if (!hasChanges || document.getElementById(ESCORT_VISIT_SUMMARY_ID)) return;

        const row = makeElement('span');
        row.id = ESCORT_VISIT_SUMMARY_ID;
        Object.assign(row.style, {
            display: 'block',
            boxSizing: 'border-box',
            position: 'static',
            float: 'none',
            clear: 'both',
            width: '100%',
            margin: '0 0 18px',
            padding: '0',
            border: '0',
            background: 'transparent'
        });

        const changedLine = makeElement('span');
        Object.assign(changedLine.style, {
            display: 'inline-block',
            position: 'static',
            float: 'none',
            padding: '6px 10px',
            borderLeft: '3px solid ' + changeRedColor,
            background: 'rgba(255, 77, 79, .12)',
            color: changeRedColor,
            font: '700 11px/1.35 Lato, Arial, sans-serif'
        });
        changedLine.appendChild(document.createTextNode('Zmieniono: '));

        const values = makeElement('strong', '', changes.labels.join(', '));
        changedLine.appendChild(values);
        row.appendChild(changedLine);

        if (dateRow) dateRow.insertAdjacentElement('afterend', row);
        else visitLine.insertAdjacentElement('afterend', row);
    }

    function mergeEscortDefinedObject(previous, current) {
        const merged = { ...(previous || {}) };

        for (const [key, value] of Object.entries(current || {})) {
            if (value == null || value === '') continue;
            if (Array.isArray(value) && value.length === 0) continue;
            merged[key] = value;
        }

        return merged;
    }

    function mergeEscortAdVisitData(previous, current, visitedAt) {
        const old = previous && typeof previous === 'object' ? previous : {};
        const currentHasPhone = !!normalizeEscortCachedPhone(
            current.phoneDigits || current.phone
        );

        const merged = {
            ...old,
            ...current,
            title: current.title || old.title || null,
            description: current.description || old.description || null,
            availability: {
                ...(old.availability || {}),
                ...(current.availability || {})
            },
            // Sekcja tagów jest odczytywana w całości. Pusta lista oznacza,
            // że aktualnie anons nie ma tagów i ma nadpisać stary stan.
            tags: Array.isArray(current.tags)
                ? [...current.tags]
                : [...(old.tags || [])],
            // Cennik jest odczytywany jako kompletny zestaw wierszy. Nie
            // scalamy go ze starym stanem, bo zniknięcie np. „30 min” jest zmianą.
            prices: { ...(current.prices || {}) },
            datePosted: current.datePosted || old.datePosted || null,
            location: mergeEscortDefinedObject(old.location, current.location),
            // Wartość null w istniejącym wierszu statystyk jest prawidłowa
            // i musi nadpisać wcześniejszą wartość. Pusty cały obiekt zachowujemy
            // jedynie jako zabezpieczenie przed niepełnym wczytaniem strony.
            stats: Object.keys(current.stats || {}).length
                ? { ...current.stats }
                : { ...(old.stats || {}) },
            phoneId: current.phoneId || old.phoneId || null,
            phone: currentHasPhone ? current.phone : (old.phone || null),
            phoneDigits: currentHasPhone ? current.phoneDigits : (old.phoneDigits || null),
            phoneFetchStatus: currentHasPhone
                ? current.phoneFetchStatus
                : (old.phoneFetchStatus || null),
            phoneCheckedAt: currentHasPhone
                ? current.phoneCheckedAt
                : (old.phoneCheckedAt || null),
            // Snapshot zawiera wyłącznie pola używane do porównania zmian i
            // jest aktualizowany tylko po wejściu na pojedynczy anons.
            visitSnapshot: createEscortVisitSnapshot(current, old.visitSnapshot),
            fetchTransport: 'current-page',
            pageCheckedAt: visitedAt,
            lastVisitedAt: visitedAt
        };
        return merged;
    }

    function initEscortClubVisitChangeTracking() {
        if (!/^\/anons\/\d+\.html\/?$/i.test(location.pathname)) return;
        if (!SETTINGS.usePersistentCache) return;
        if (document.documentElement.dataset.vmEscortVisitTracking === '1') return;
        document.documentElement.dataset.vmEscortVisitTracking = '1';

        (async () => {
            let ready = false;
            for (let attempt = 0; attempt < 40; attempt++) {
                ready =
                    document.readyState !== 'loading' &&
                    !!document.querySelector(
                        '.content-info-col .content-name h1, .content-name.-desc-name h1'
                    );
                if (ready) break;
                await new Promise(resolve => setTimeout(resolve, 250));
            }
            if (!ready) return;

            const adId = getAdIdFromUrl();
            if (!adId) return;

            const adUrl = 'https://pl.escort.club/anons/' + adId + '.html';
            const stored = getStoredEscortAdData(adId);
            const previousSnapshot = stored?.visitSnapshot || null;
            const current = extractEscortClubAdData(document, adId, adUrl);
            const phone = await resolveEscortPhoneForCurrentPage(current.phoneId);
            if (phone) Object.assign(current, phone);

            const changes = compareEscortAdVisitData(previousSnapshot, current);
            const previousVisitInfo = (previousSnapshot || stored?.lastVisitedAt)
                ? {
                    ...(previousSnapshot || {}),
                    lastVisitedAt: stored?.lastVisitedAt || null
                }
                : null;
            renderEscortVisitSummary(
                previousVisitInfo,
                changes,
                !previousVisitInfo
            );
            if (previousSnapshot) {
                renderEscortVisitChanges(changes);
            }

            const visitedAt = Date.now();
            const merged = mergeEscortAdVisitData(stored, current, visitedAt);
            const saved = setPersistentEscortAdData(adId, merged);
            if (saved) escortAdDataMemoryCache.set(String(adId), saved);
        })().catch(error => {
            log('Błąd porównania danych pojedynczego anonsu', error);
        });
    }

    function getStoredEscortAdData(adId) {
        try {
            const value = readPersistentCacheValue(escortAdDataCacheKey(adId), null);
            if (!value || typeof value !== 'object' || value.status !== 'ok' || !value.checkedAt) {
                return null;
            }
            return value;
        } catch (_) {
            return null;
        }
    }

    function getPersistentEscortAdData(adId) {
        if (!SETTINGS.usePersistentCache) return null;

        const value = getStoredEscortAdData(adId);
        if (!value || Date.now() - value.checkedAt >= getListCacheTtlMs()) return null;
        return value;
    }

    function setPersistentEscortAdData(adId, data) {
        if (!SETTINGS.usePersistentCache || !data) return null;

        try {
            const existing = getStoredEscortAdData(adId);
            const cacheId = String(adId);
            const pendingActivity = pendingEscortiActivityCacheWrites.get(cacheId) || {};
            const escortiActivity = {
                ...(data?.escortiActivity || {}),
                // Stan ponownie odczytany tuż przed zapisem jest nowszy niż
                // kopia, na której wcześniej budowano `data`.
                ...(existing?.escortiActivity || {}),
                ...pendingActivity
            };
            const value = {
                status: 'ok',
                ...(existing?.lastVisitedAt
                    ? { lastVisitedAt: existing.lastVisitedAt }
                    : {}),
                ...(existing?.pageCheckedAt
                    ? { pageCheckedAt: existing.pageCheckedAt }
                    : {}),
                // Odświeżanie danych na stronie wyników nie może zmieniać
                // punktu odniesienia z ostatniej wizyty na stronie anonsu.
                ...(existing?.visitSnapshot
                    ? { visitSnapshot: existing.visitSnapshot }
                    : {}),
                ...data,
                ...(Object.keys(escortiActivity).length
                    ? { escortiActivity }
                    : {}),
                checkedAt: Date.now()
            };
            writePersistentCacheValue(escortAdDataCacheKey(adId), value);
            pendingEscortiActivityCacheWrites.delete(cacheId);
            return value;
        } catch (error) {
            log(`Nie udało się zapisać danych Escort.club dla anonsu ${adId}`, error);
            return null;
        }
    }

    function getEscortAdData(
        adId,
        adUrl,
        forceRefresh = false,
        cancelToken = null
    ) {
        cancelToken?.throwIfCancelled();
        const id = String(adId || '');
        if (!id) return Promise.resolve({ status: 'error', data: null });

        if (!forceRefresh && escortAdDataMemoryCache.has(id)) {
            return Promise.resolve(escortAdDataMemoryCache.get(id));
        }

        const persistent = getPersistentEscortAdData(id);
        if (!forceRefresh && persistent) {
            escortAdDataMemoryCache.set(id, persistent);
            return Promise.resolve(persistent);
        }

        if (escortAdDataInflight.has(id)) {
            return escortAdDataInflight.get(id).then(result => {
                cancelToken?.throwIfCancelled();
                return result;
            });
        }

        const promise = new Promise(resolve => {
            enqueueEscortAdDataJob(async () => {
                try {
                    cancelToken?.throwIfCancelled();
                    const result = await fetchEscortClubAdDocument(
                        adUrl,
                        cancelToken
                    );
                    if (!result?.ok) {
                        resolve({ status: 'error', data: null });
                        return;
                    }

                    let finalUrl;
                    try {
                        finalUrl = new URL(result.finalUrl || adUrl, adUrl);
                    } catch (_) {
                        resolve({ status: 'error', data: null });
                        return;
                    }

                    const finalAdId = parseAdIdFromUrl(finalUrl.href);
                    const exactAdPath = /^\/anons\/\d+\.html\/?$/i.test(finalUrl.pathname);
                    if (
                        finalUrl.hostname !== 'pl.escort.club' ||
                        !exactAdPath ||
                        !finalAdId ||
                        finalAdId !== id
                    ) {
                        resolve({ status: 'error', data: null });
                        return;
                    }

                    const data = {
                        ...extractEscortClubAdData(result.doc, id, finalUrl.href),
                        fetchTransport: result.transport || null
                    };

                    if (data.phoneId) {
                        try {
                            const phoneData = await fetchEscortClubPhone(
                                data.phoneId,
                                cancelToken
                            );
                            data.phone = phoneData.phone;
                            data.phoneDigits = phoneData.phoneDigits;
                            data.phoneFetchStatus = 'ok';
                            data.phoneCheckedAt = Date.now();
                        } catch (phoneError) {
                            if (isOperationCancelledError(phoneError)) throw phoneError;
                            data.phone = null;
                            data.phoneDigits = null;
                            data.phoneFetchStatus = 'error';
                            data.phoneCheckedAt = Date.now();
                            log(`Błąd pobierania numeru dla anonsu ${id}`, phoneError);
                        }
                    } else {
                        data.phone = null;
                        data.phoneDigits = null;
                        data.phoneFetchStatus = 'missing-id';
                        data.phoneCheckedAt = Date.now();
                    }

                    const parsed = {
                        status: 'ok',
                        ...data,
                        checkedAt: Date.now()
                    };

                    escortAdDataMemoryCache.set(id, parsed);
                    setPersistentEscortAdData(id, data);
                    resolve(parsed);
                } catch (error) {
                    if (isOperationCancelledError(error)) {
                        resolve({ status: 'cancelled', data: null });
                        return;
                    }
                    log(`Błąd pobierania danych Escort.club dla anonsu ${id}`, error);
                    try {
                        GM_setValue(ESCORT_AD_DATA_LAST_ERROR_KEY, {
                            adId: id,
                            adUrl,
                            message: error?.message || String(error),
                            timestamp: Date.now()
                        });
                    } catch (_) {}
                    resolve({ status: 'error', data: null });
                }
            });
        }).finally(() => {
            escortAdDataInflight.delete(id);
        });

        escortAdDataInflight.set(id, promise);
        return promise;
    }

    function prepareImportedEscortCard(card, baseUrl = location.href) {
        // Escort.club używa lazy-loadingu: miniatura ma już src, ale jest to
        // placeholder (logo), natomiast właściwy adres zdjęcia siedzi w data-src.
        // Kafelki dołączane przez skrypt nie przechodzą przez inicjalizację
        // lazy-loadera strony, więc dla importowanych kafelków podstawiamy
        // właściwe adresy bezpośrednio.
        const makeAbsolute = value => {
            if (!value) return null;

            try {
                return new URL(value, baseUrl).href;
            } catch (_) {
                return value;
            }
        };

        for (const img of card.querySelectorAll('img')) {
            const lazySrc =
                img.getAttribute('data-src') ||
                img.getAttribute('data-lazy-src') ||
                img.getAttribute('data-original');

            if (lazySrc) {
                img.setAttribute('src', makeAbsolute(lazySrc));
            } else if (img.getAttribute('src')) {
                img.setAttribute('src', makeAbsolute(img.getAttribute('src')));
            }

            const lazySrcset =
                img.getAttribute('data-srcset') ||
                img.getAttribute('data-lazy-srcset');

            if (lazySrcset) {
                img.setAttribute('srcset', lazySrcset);
            }
        }

        // Obsługa ewentualnych <picture><source data-srcset="...">.
        for (const source of card.querySelectorAll('source')) {
            const lazySrcset =
                source.getAttribute('data-srcset') ||
                source.getAttribute('data-lazy-srcset');

            if (lazySrcset) {
                source.setAttribute('srcset', lazySrcset);
            }
        }

        return card;
    }


    function addEscortListPagesControl(listContext) {
        const existing = document.getElementById('vm-escort-list-pages-control');

        if (existing) {
            const progress = document.getElementById('vm-escort-list-pages-progress');
            const progressText = progress?.querySelector('.vm-list-pages-progress-text');
            const cancelButton = progress?.querySelector('.vm-list-pages-cancel');
            return {
                row: existing,
                select: existing.querySelector('select'),
                status: existing.querySelector('.vm-list-pages-status'),
                progress,
                setProgress(text, onCancel = null) {
                    const value = String(text || '');
                    if (progressText) progressText.textContent = value;
                    if (progress) progress.hidden = !value;
                    if (cancelButton) {
                        cancelButton.hidden = typeof onCancel !== 'function';
                        cancelButton.onclick = typeof onCancel === 'function'
                            ? onCancel
                            : null;
                    }
                }
            };
        }

        if (!listContext?.anchors?.length) {
            return null;
        }

        const targetContainer = getEscortListCardContainer(listContext.anchors);

        if (!targetContainer?.parentElement) {
            return null;
        }

        const pagePinkColor = getEscortPagePinkColor();
        const listPagesStyleId = 'vm-list-pages-select-style';
        if (!document.getElementById(listPagesStyleId)) {
            const style = makeElement('style');
            style.id = listPagesStyleId;
            style.textContent = `
                #vm-escort-list-pages-control .vm-list-pages-selection {
                    display: inline-flex;
                    align-items: center;
                    flex: 0 0 auto;
                    gap: 6px;
                    box-sizing: border-box;
                    height: 34px;
                    margin: 0;
                    padding: 0 5px 0 8px;
                    border: 1px solid ${pagePinkColor};
                    border-radius: 7px;
                    color: #211d22;
                    white-space: nowrap;
                }
                #vm-escort-list-pages-control .vm-list-pages-select-wrap {
                    position: relative;
                    flex: 0 0 104px;
                    width: 104px;
                    min-width: 104px;
                    max-width: 104px;
                    height: 32px;
                    margin: 0;
                }
                #vm-escort-list-pages-control .vm-list-pages-select-wrap::after {
                    content: '';
                    position: absolute;
                    top: 13px;
                    right: 8px;
                    width: 0;
                    height: 0;
                    border-left: 7px solid transparent;
                    border-right: 7px solid transparent;
                    border-top: 7px solid ${pagePinkColor};
                    pointer-events: none;
                }
                #vm-escort-list-pages-control .vm-list-pages-select {
                    appearance: none !important;
                    -webkit-appearance: none !important;
                    -moz-appearance: none !important;
                    box-sizing: border-box;
                    width: 104px !important;
                    min-width: 104px !important;
                    max-width: 104px !important;
                    height: 32px !important;
                    min-height: 32px !important;
                    margin: 0 !important;
                    padding: 0 28px 0 10px !important;
                    border: 0 !important;
                    border-radius: 0 !important;
                    outline: 0 !important;
                    background: transparent !important;
                    color: #211d22 !important;
                    font-family: Lato, Arial, sans-serif !important;
                    font-size: 14px !important;
                    font-weight: 400 !important;
                    line-height: 31px !important;
                    cursor: pointer;
                    box-shadow: none !important;
                }
                #vm-escort-list-pages-control .vm-list-pages-select:disabled {
                    cursor: wait;
                    opacity: .65;
                }
                #vm-escort-list-pages-progress {
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    gap: 10px;
                    box-sizing: border-box;
                    width: 100%;
                    margin: 2px 0 6px;
                    padding: 8px 12px;
                    border: 1px solid ${pagePinkColor};
                    border-radius: 7px;
                    background: ${pagePinkColor};
                    color: #fff;
                    font: 700 12px Lato, Arial, sans-serif;
                    line-height: 16px;
                    white-space: nowrap;
                    user-select: none;
                }
                #vm-escort-list-pages-progress[hidden] {
                    display: none !important;
                }
                #vm-escort-list-pages-progress .vm-list-pages-cancel {
                    flex: 0 0 auto;
                    min-height: 24px;
                    padding: 3px 10px;
                    border: 1px solid rgba(255,255,255,.85);
                    border-radius: 6px;
                    background: rgba(65, 15, 52, .32);
                    color: #fff;
                    font: 800 11px Lato, Arial, sans-serif;
                    cursor: pointer;
                }
                #vm-escort-list-pages-progress .vm-list-pages-cancel:hover {
                    background: rgba(65, 15, 52, .55);
                }
            `;
            document.head.appendChild(style);
        }

        // Wstawiamy kontrolkę jako natywny „header-filter” obok title-col,
        // zamiast doklejać własny panel do wnętrza nagłówka.
        const row = makeElement('div');
        row.id = 'vm-escort-list-pages-control';
        row.className = 'header-filter col';

        Object.assign(row.style, {
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'flex-end',
            flex: '0 0 auto',
            marginLeft: 'auto',
            paddingLeft: '12px'
        });

        const inner = makeElement('div', 'filter-box vm-list-pages-inner');

        Object.assign(inner.style, {
            display: 'inline-flex',
            alignItems: 'center',
            flexWrap: 'nowrap',
            justifyContent: 'flex-end',
            gap: '6px',
            fontFamily: 'Lato,Arial,sans-serif',
            lineHeight: '1.2'
        });

        const label = makeElement('span', 'sub-label', 'Pokaż na jednej liście:');

        Object.assign(label.style, {
            color: '#211d22',
            fontSize: '12px',
            fontWeight: '400',
            whiteSpace: 'nowrap'
        });

        const selectionBox = makeElement('div', 'vm-list-pages-selection');

        const select = makeElement('select', 'vm-list-pages-select');
        select.setAttribute('aria-label', 'Pokaż na jednej liście');

        const selectWrap = makeElement('div', 'vm-list-pages-select-wrap');
        selectWrap.appendChild(select);

        const selectedValue = normalizeListPagesToShow(LIST_PAGES_TO_SHOW);

        appendSelectOptions(select, LIST_PAGE_OPTIONS, selectedValue);

        const status = makeElement('span', 'vm-list-pages-status');

        Object.assign(status.style, {
            color: '#9a9297',
            fontFamily: 'Lato,Arial,sans-serif',
            fontSize: '10px',
            fontWeight: '400',
            whiteSpace: 'nowrap'
        });

        const progress = makeElement('div');
        progress.id = 'vm-escort-list-pages-progress';
        progress.setAttribute('aria-live', 'polite');
        progress.hidden = true;

        const progressText = makeElement('span', 'vm-list-pages-progress-text');
        const cancelButton = makeButton('vm-list-pages-cancel', 'Przerwij');
        cancelButton.hidden = true;
        progress.append(progressText, cancelButton);

        const setProgress = (text, onCancel = null) => {
            const value = String(text || '');
            progressText.textContent = value;
            progress.hidden = !value;
            cancelButton.hidden = typeof onCancel !== 'function';
            cancelButton.onclick = typeof onCancel === 'function'
                ? onCancel
                : null;
        };

        select.addEventListener('change', () => {
            const newValue = normalizeListPagesToShow(select.value);
            const oldValue = normalizeListPagesToShow(LIST_PAGES_TO_SHOW);

            if (String(newValue) === String(oldValue)) {
                return;
            }

            select.disabled = true;
            status.textContent = '';
            setProgress('Ładowanie stron...');

            if (!requestOneTimeListPagesReload(newValue)) {
                select.disabled = false;
                setProgress('');
                status.textContent = 'Nie udało się przeładować listy.';
                select.value = String(oldValue);
                return;
            }
            location.reload();
        });

        selectionBox.appendChild(label);
        selectionBox.appendChild(selectWrap);
        inner.appendChild(selectionBox);
        inner.appendChild(status);
        row.appendChild(inner);

        // Na stronie wyników istnieje już dokładnie taki układ:
        // <div class="row"><div class="title-col col">…</div>…</div>.
        // Kontrolkę dokładamy jako sąsiednią kolumnę, tak jak robi to sam serwis
        // z własnymi filtrami w nagłówkach sekcji.
        const titleCol = listContext.heading?.closest('.title-col');
        const titleRow = titleCol?.parentElement;

        if (titleCol && titleRow?.classList.contains('row')) {
            titleCol.insertAdjacentElement('afterend', row);
            titleRow.insertAdjacentElement('afterend', progress);
        } else {
            Object.assign(row.style, {
                width: '100%',
                justifyContent: 'flex-start',
                margin: '0 0 10px',
                paddingLeft: '0'
            });
            targetContainer.parentElement.insertBefore(row, targetContainer);
            row.insertAdjacentElement('afterend', progress);
        }

        return {
            row,
            select,
            status,
            progress,
            setProgress
        };
    }

    function addEscortCustomFiltersControl(listContext, onChange, onPriceDurationChange) {
        const existing = document.getElementById('vm-escort-custom-filters-control');
        if (existing) return null;

        const heading = listContext?.heading;
        const titleCol = heading?.closest('.title-col');
        const titleRow = titleCol?.parentElement;
        if (!heading || !titleCol || !titleRow) return null;

        const pagePinkColor = getEscortPagePinkColor();
        const styleId = 'vm-escort-custom-filters-style';
        if (!document.getElementById(styleId)) {
            const style = makeElement('style');
            style.id = styleId;
            style.textContent = `
                #vm-escort-custom-filters-control {
                    display: flex;
                    align-items: center;
                    flex: 0 0 auto;
                    gap: 7px;
                    margin-left: 0;
                    padding-left: 10px;
                }
                #vm-escort-custom-filters-trigger {
                    display: inline-flex !important;
                    align-items: center;
                    justify-content: center;
                    flex: 0 0 auto !important;
                    width: auto !important;
                    min-width: max-content !important;
                    max-width: none !important;
                    height: 32px;
                    margin: 0;
                    padding: 7px 12px !important;
                    border-radius: 7px !important;
                    color: #fff !important;
                    font-family: Lato, Arial, sans-serif;
                    font-size: 12px;
                    font-weight: 700;
                    line-height: 16px;
                    white-space: nowrap !important;
                    cursor: pointer;
                }
                #vm-escort-custom-filters-trigger:disabled {
                    cursor: not-allowed !important;
                    opacity: .55;
                }
                #vm-escort-custom-filters-control .vm-search-price-control {
                    display: inline-flex;
                    align-items: center;
                    flex: 0 0 auto;
                    gap: 6px;
                    box-sizing: border-box;
                    height: 34px;
                    margin: 0 0 0 3px;
                    padding: 0 5px 0 8px;
                    border: 1px solid ${pagePinkColor};
                    border-radius: 7px;
                    color: #211d22;
                    font: 400 12px Lato, Arial, sans-serif;
                    white-space: nowrap;
                }
                #vm-escort-custom-filters-control .vm-search-price-select-wrap {
                    position: relative;
                    flex: 0 0 92px;
                    width: 92px;
                    height: 32px;
                }
                #vm-escort-custom-filters-control .vm-search-price-select-wrap::after {
                    content: '';
                    position: absolute;
                    top: 13px;
                    right: 7px;
                    width: 0;
                    height: 0;
                    border-left: 6px solid transparent;
                    border-right: 6px solid transparent;
                    border-top: 6px solid ${pagePinkColor};
                    pointer-events: none;
                }
                #vm-escort-custom-filters-control .vm-search-price-select {
                    appearance: none !important;
                    -webkit-appearance: none !important;
                    -moz-appearance: none !important;
                    box-sizing: border-box;
                    width: 92px !important;
                    height: 32px !important;
                    margin: 0 !important;
                    padding: 0 25px 0 7px !important;
                    border: 0 !important;
                    border-radius: 0 !important;
                    outline: 0 !important;
                    background: transparent !important;
                    color: #211d22 !important;
                    font: 400 12px Lato, Arial, sans-serif !important;
                    line-height: 31px !important;
                    cursor: pointer;
                    box-shadow: none !important;
                }
                .vm-custom-filter-progress {
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    gap: 10px;
                    box-sizing: border-box;
                    width: 100%;
                    margin: 2px 0 6px;
                    padding: 8px 12px;
                    border: 1px solid ${pagePinkColor};
                    border-radius: 7px;
                    background: ${pagePinkColor};
                    color: #fff;
                    font: 700 12px Lato, Arial, sans-serif;
                    line-height: 16px;
                    white-space: nowrap;
                    user-select: none;
                }
                .vm-custom-filter-progress[hidden] {
                    display: none !important;
                }
                .vm-custom-filter-progress .vm-custom-filter-cancel {
                    flex: 0 0 auto;
                    min-height: 24px;
                    padding: 3px 10px;
                    border: 1px solid rgba(255,255,255,.85);
                    border-radius: 6px;
                    background: rgba(65, 15, 52, .32);
                    color: #fff;
                    font: 800 11px Lato, Arial, sans-serif;
                    cursor: pointer;
                }
                .vm-custom-filter-progress .vm-custom-filter-cancel:hover {
                    background: rgba(65, 15, 52, .55);
                }
                #vm-escort-custom-filters-panel {
                    box-sizing: border-box;
                    width: 100%;
                    margin: 0 0 10px;
                    padding: 8px 10px;
                    border-top: 1px solid #ff4c99;
                    background: #fff;
                    box-shadow: 0 3px 8px rgba(33, 29, 34, .08);
                    font-family: Lato, Arial, sans-serif;
                }
                #vm-escort-custom-filters-panel[hidden] {
                    display: none !important;
                }
                #vm-escort-custom-filters-panel[data-busy="true"] {
                    opacity: .58;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-intro {
                    margin: 0 0 5px;
                    padding: 0;
                    color: ${pagePinkColor};
                    font-size: 11px;
                    font-weight: 700;
                    letter-spacing: .03em;
                    text-transform: uppercase;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-intro strong {
                    font-weight: 700;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-intro.-show-only {
                    margin-top: 8px;
                    padding-top: 7px;
                    border-top: 1px solid #ff4c99;
                    font-weight: 700;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-grid {
                    display: grid;
                    grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
                    gap: 7px;
                    align-items: start;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-group {
                    min-width: 0;
                    padding: 5px 8px 6px;
                    border: 1px solid #eee6eb;
                    border-radius: 7px;
                    background: #fcfafb;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-group-title {
                    margin: 0 0 2px;
                    color: ${pagePinkColor};
                    font-size: 10px;
                    font-weight: 700;
                    line-height: 15px;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-group-body {
                    display: grid;
                    gap: 0;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-row {
                    display: flex;
                    align-items: center;
                    min-width: 0;
                    min-height: 29px;
                    height: 29px;
                    gap: 4px;
                    color: #211d22;
                    font-size: 11px;
                    white-space: nowrap;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-check {
                    flex: 0 0 auto;
                    width: 14px;
                    height: 14px;
                    margin: 0 1px 0 0;
                    accent-color: ${pagePinkColor};
                    cursor: pointer;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-number {
                    box-sizing: border-box;
                    width: 48px;
                    height: 24px;
                    padding: 0 3px;
                    border: 0;
                    border-bottom: 1px solid #ff4c99;
                    border-radius: 0;
                    outline: 0;
                    background: transparent;
                    color: #211d22;
                    font: 700 11px Lato, Arial, sans-serif;
                    text-align: center;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-unit,
                #vm-escort-custom-filters-panel .vm-custom-filter-day,
                #vm-escort-custom-filters-panel .vm-custom-filter-time {
                    box-sizing: border-box;
                    width: 72px;
                    height: 24px;
                    padding: 0 3px;
                    border: 0;
                    border-bottom: 1px solid #ff4c99;
                    border-radius: 0;
                    outline: 0;
                    background: transparent;
                    color: #211d22;
                    font: 11px Lato, Arial, sans-serif;
                    cursor: pointer;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-day {
                    width: 98px;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-time {
                    width: 66px;
                    font-weight: 700;
                    text-align: center;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-text {
                    box-sizing: border-box;
                    width: min(190px, 100%);
                    height: 24px;
                    padding: 0 3px;
                    border: 0;
                    border-bottom: 1px solid #ff4c99;
                    border-radius: 0;
                    outline: 0;
                    background: transparent;
                    color: #211d22;
                    font: 11px Lato, Arial, sans-serif;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-number:disabled,
                #vm-escort-custom-filters-panel .vm-custom-filter-unit:disabled,
                #vm-escort-custom-filters-panel .vm-custom-filter-day:disabled,
                #vm-escort-custom-filters-panel .vm-custom-filter-time:disabled,
                #vm-escort-custom-filters-panel .vm-custom-filter-text:disabled {
                    border-bottom-color: #d8d2d5;
                    color: #aaa3a7;
                    cursor: not-allowed;
                    opacity: .7;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-footer {
                    display: flex;
                    align-items: center;
                    gap: 10px;
                    margin-top: 6px;
                    padding-top: 6px;
                    border-top: 1px solid #eee9ec;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-clear {
                    padding: 0;
                    border: 0;
                    background: transparent;
                    color: ${pagePinkColor};
                    font: 700 11px Lato, Arial, sans-serif;
                    cursor: pointer;
                }
                #vm-escort-custom-filters-panel .vm-custom-filter-status {
                    color: #8d858a;
                    font-size: 10px;
                }
                @media (max-width: 767px) {
                    #vm-escort-custom-filters-control {
                        flex-wrap: wrap;
                        margin-left: 0;
                        padding: 6px 0 0;
                    }
                    #vm-escort-custom-filters-panel .vm-custom-filter-grid {
                        grid-template-columns: 1fr;
                    }
                    #vm-escort-custom-filters-panel .vm-custom-filter-row {
                        min-height: 29px;
                        height: auto;
                        flex-wrap: wrap;
                        white-space: normal;
                    }
                }
            `;
            document.head.appendChild(style);
        }

        let config = getEscortCustomFilters();
        let filtersBusy = false;

        const control = makeElement('div');
        control.id = 'vm-escort-custom-filters-control';
        control.className = 'header-filter col';

        const trigger = makeElement('button');
        trigger.id = 'vm-escort-custom-filters-trigger';
        trigger.type = 'button';
        trigger.className = 'btn btn-pink';

        let priceDurationSelect = null;
        let priceControl = null;

        if (SETTINGS.showPricesInSearchResults) {
            priceControl = makeElement('label');
            priceControl.className = 'vm-search-price-control';
            priceControl.appendChild(document.createTextNode('Wyświetlaj cenę za:'));

            const selectWrap = makeElement('span', 'vm-search-price-select-wrap');

            priceDurationSelect = makeElement('select');
            priceDurationSelect.className = 'vm-search-price-select';
            priceDurationSelect.setAttribute('aria-label', 'Wyświetlaj cenę za wybrany czas');

            const selectedDuration = normalizeEscortPriceDuration(
                SETTINGS.searchResultPriceDuration
            );

            appendSelectOptions(priceDurationSelect, ESCORT_PRICE_DURATION_OPTIONS, selectedDuration);

            selectWrap.appendChild(priceDurationSelect);
            priceControl.appendChild(selectWrap);
        }

        const progress = makeElement('div', 'vm-custom-filter-progress');
        progress.setAttribute('aria-live', 'polite');
        progress.hidden = true;
        const progressText = makeElement('span', 'vm-custom-filter-progress-text');
        const progressCancelButton = makeButton('vm-custom-filter-cancel', 'Przerwij');
        progressCancelButton.hidden = true;
        progress.append(progressText, progressCancelButton);

        const panel = makeElement('div');
        panel.id = 'vm-escort-custom-filters-panel';
        panel.hidden = !config.expanded;

        const intro = makeElement('p', 'vm-custom-filter-intro', 'Ukrywaj');

        const grid = makeElement('div', 'vm-custom-filter-grid');

        const showOnlyIntro = makeElement('p', 'vm-custom-filter-intro -show-only', 'Pokazuj tylko');

        const showOnlyGrid = makeElement('div', 'vm-custom-filter-grid');

        const status = makeElement('span', 'vm-custom-filter-status');
        status.setAttribute('aria-live', 'polite');

        const controls = {};
        const dependentControls = [];

        function makeFilterGroup(title, targetGrid) {
            const group = makeElement('section', 'vm-custom-filter-group');

            const heading = makeElement('div', 'vm-custom-filter-group-title', title);

            const body = makeElement('div', 'vm-custom-filter-group-body');

            group.append(heading, body);
            targetGrid.appendChild(group);
            return body;
        }

        function makeCheckbox(key) {
            const checkbox = makeElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'vm-custom-filter-check';
            checkbox.checked = !!config[key];
            controls[key] = checkbox;
            return checkbox;
        }

        function makeSimpleRow(key, text, targetGrid = grid) {
            const row = makeElement('label', 'vm-custom-filter-row');
            row.appendChild(makeCheckbox(key));
            row.appendChild(document.createTextNode(text));
            targetGrid.appendChild(row);
        }

        function makeNumberRow(
            checkKey,
            prefix,
            numberKey,
            suffix,
            min = 0,
            title = '',
            targetGrid = grid
        ) {
            const row = makeElement('div', 'vm-custom-filter-row');
            if (title) row.title = title;
            const checkbox = makeCheckbox(checkKey);
            const input = makeElement('input');
            input.type = 'number';
            input.className = 'vm-custom-filter-number';
            input.min = String(min);
            input.max = '100000';
            input.step = '1';
            input.inputMode = 'numeric';
            input.value = String(config[numberKey]);
            input.dataset.filterKey = numberKey;
            input.setAttribute('aria-label', `${prefix} ${suffix}`);
            controls[numberKey] = input;
            dependentControls.push([checkbox, input]);
            row.append(checkbox, document.createTextNode(prefix), input, document.createTextNode(suffix));
            targetGrid.appendChild(row);
        }

        function makeAgeRow(checkKey, prefix, numberKey, unitKey, targetGrid = grid) {
            const row = makeElement('div', 'vm-custom-filter-row');
            const checkbox = makeCheckbox(checkKey);
            const input = makeElement('input');
            input.type = 'number';
            input.className = 'vm-custom-filter-number';
            input.min = '1';
            input.max = '100000';
            input.step = '1';
            input.inputMode = 'numeric';
            input.value = String(config[numberKey]);
            input.dataset.filterKey = numberKey;
            input.setAttribute('aria-label', prefix);
            controls[numberKey] = input;

            const unit = makeElement('select', 'vm-custom-filter-unit');
            unit.setAttribute('aria-label', `${prefix} - jednostka wieku`);
            appendSelectOptions(
                unit,
                ESCORT_CUSTOM_FILTER_AGE_UNIT_OPTIONS,
                config[unitKey]
            );
            controls[unitKey] = unit;
            dependentControls.push([checkbox, input], [checkbox, unit]);
            row.append(checkbox, document.createTextNode(prefix), input, unit);
            targetGrid.appendChild(row);
        }

        function makeAvailabilityRow(
            checkKey,
            text,
            dayKey,
            timeKey,
            targetGrid = grid
        ) {
            const row = makeElement('div', 'vm-custom-filter-row');
            const checkbox = makeCheckbox(checkKey);

            const day = makeElement('select', 'vm-custom-filter-day');
            day.setAttribute('aria-label', `${text} - dzień tygodnia`);
            appendSelectOptions(day, ESCORT_CUSTOM_FILTER_WEEKDAYS, config[dayKey]);
            controls[dayKey] = day;

            const time = makeElement('input');
            time.type = 'time';
            time.className = 'vm-custom-filter-time';
            time.step = '60';
            time.value = normalizeCustomFilterTime(config[timeKey]);
            time.setAttribute('aria-label', `${text} - godzina`);
            controls[timeKey] = time;

            dependentControls.push([checkbox, day], [checkbox, time]);
            row.append(checkbox, document.createTextNode(text), day, time);
            targetGrid.appendChild(row);
        }

        function makeDescriptionRow(checkKey, text, queryKey, targetGrid = grid) {
            const row = makeElement('div', 'vm-custom-filter-row');
            const checkbox = makeCheckbox(checkKey);

            const input = makeElement('input');
            input.type = 'search';
            input.className = 'vm-custom-filter-text';
            input.value = config[queryKey] || '';
            input.placeholder = 'wpisz tekst';
            input.maxLength = 200;
            input.dataset.filterKey = queryKey;
            input.setAttribute('aria-label', text);
            controls[queryKey] = input;

            dependentControls.push([checkbox, input]);
            row.append(checkbox, document.createTextNode(text), input);
            targetGrid.appendChild(row);
        }

        const hideProfileGroup = makeFilterGroup('Garsoniera i profil Escorti.pl', grid);
        const hideAdsGroup = makeFilterGroup('Anonse i agregacja', grid);
        const hideAvailabilityGroup = makeFilterGroup('Dostępność', grid);
        const hideDescriptionGroup = makeFilterGroup('Opis', grid);

        makeSimpleRow('hideWithoutOpinions', 'Bez opinii na Garsonierze', hideProfileGroup);
        makeNumberRow(
            'hideProfileAdsOver',
            'Anonse profilu na Escorti.pl: więcej niż',
            'profileAdsLimit',
            '',
            0,
            'Łączna liczba anonsów znaleziona w historii profilu Escorti.pl.',
            hideProfileGroup
        );
        makeAgeRow(
            'hideYoungerThan',
            'Profil młodszy niż',
            'youngerValue',
            'youngerUnit',
            hideProfileGroup
        );
        makeAgeRow(
            'hideOlderThan',
            'Profil starszy niż',
            'olderValue',
            'olderUnit',
            hideProfileGroup
        );

        makeNumberRow(
            'hideActiveAdsOver',
            'Zagregowane anonse: więcej niż',
            'activeAdsLimit',
            '',
            0,
            'Liczba aktywnych anonsów połączonych zgodnie z wybranym w ustawieniach sposobem agregowania. Zależnie od trybu skrypt korzysta z danych Escorti.pl albo z bieżącej listy, wyszukiwarki Escort.club i aktualnego cache.',
            hideAdsGroup
        );
        makeSimpleRow(
            'hideAgencyPhone',
            'Z numerem telefonu agencji/salonu',
            hideAdsGroup
        );
        makeAgeRow(
            'hideAdYoungerThan',
            'Anons młodszy niż',
            'adYoungerValue',
            'adYoungerUnit',
            hideAdsGroup
        );
        makeAgeRow(
            'hideAdOlderThan',
            'Anons starszy niż',
            'adOlderValue',
            'adOlderUnit',
            hideAdsGroup
        );

        makeSimpleRow(
            'hideAlwaysAvailable',
            'Dostępne „cały czas”',
            hideAvailabilityGroup
        );
        makeAvailabilityRow(
            'hideUnavailableAt',
            'Niedostępne:',
            'hideUnavailableDay',
            'hideUnavailableTime',
            hideAvailabilityGroup
        );
        makeAvailabilityRow(
            'hideAvailableAt',
            'Dostępne:',
            'hideAvailableDay',
            'hideAvailableTime',
            hideAvailabilityGroup
        );
        makeDescriptionRow(
            'hideDescriptionMatch',
            'Opis zawiera:',
            'hideDescriptionQuery',
            hideDescriptionGroup
        );

        const showProfileGroup = makeFilterGroup('Garsoniera i profil Escorti.pl', showOnlyGrid);
        const showAdsGroup = makeFilterGroup('Anonse i agregacja', showOnlyGrid);
        const showAvailabilityGroup = makeFilterGroup('Dostępność', showOnlyGrid);
        const showDescriptionGroup = makeFilterGroup('Opis', showOnlyGrid);

        makeSimpleRow(
            'showOnlyWithoutOpinions',
            'Bez opinii na Garsonierze (tryb sapera)',
            showProfileGroup
        );
        makeSimpleRow('showOnlySingleProfileAd', 'Profil Escorti.pl z 1 anonsem', showProfileGroup);
        makeSimpleRow(
            'showOnlyAgencyPhone',
            'Z numerem telefonu agencji/salonu',
            showAdsGroup
        );
        makeAvailabilityRow(
            'showOnlyUnavailableAt',
            'Niedostępne:',
            'showOnlyUnavailableDay',
            'showOnlyUnavailableTime',
            showAvailabilityGroup
        );
        makeAvailabilityRow(
            'showOnlyAvailableAt',
            'Dostępne:',
            'showOnlyAvailableDay',
            'showOnlyAvailableTime',
            showAvailabilityGroup
        );
        makeDescriptionRow(
            'showOnlyDescriptionMatch',
            'Opis zawiera:',
            'showOnlyDescriptionQuery',
            showDescriptionGroup
        );

        const footer = makeElement('div', 'vm-custom-filter-footer');

        const clearBtn = makeButton('vm-custom-filter-clear', 'Wyłącz wszystkie');
        footer.append(clearBtn, status);

        panel.append(intro, grid, showOnlyIntro, showOnlyGrid, footer);
        control.appendChild(trigger);
        if (priceControl) control.appendChild(priceControl);

        function readConfig() {
            return {
                expanded: !panel.hidden,
                hideWithoutOpinions: controls.hideWithoutOpinions.checked,
                hideProfileAdsOver: controls.hideProfileAdsOver.checked,
                profileAdsLimit: normalizeCustomFilterInteger(controls.profileAdsLimit.value, 10),
                hideActiveAdsOver: controls.hideActiveAdsOver.checked,
                activeAdsLimit: normalizeCustomFilterInteger(controls.activeAdsLimit.value, 1),
                hideAgencyPhone: controls.hideAgencyPhone.checked,
                hideYoungerThan: controls.hideYoungerThan.checked,
                youngerValue: normalizeCustomFilterInteger(controls.youngerValue.value, 2, 1),
                youngerUnit: normalizeCustomFilterAgeUnit(controls.youngerUnit.value),
                hideOlderThan: controls.hideOlderThan.checked,
                olderValue: normalizeCustomFilterInteger(controls.olderValue.value, 2, 1),
                olderUnit: normalizeCustomFilterAgeUnit(controls.olderUnit.value),
                hideAdYoungerThan: controls.hideAdYoungerThan.checked,
                adYoungerValue: normalizeCustomFilterInteger(controls.adYoungerValue.value, 2, 1),
                adYoungerUnit: normalizeCustomFilterAgeUnit(controls.adYoungerUnit.value),
                hideAdOlderThan: controls.hideAdOlderThan.checked,
                adOlderValue: normalizeCustomFilterInteger(controls.adOlderValue.value, 2, 1),
                adOlderUnit: normalizeCustomFilterAgeUnit(controls.adOlderUnit.value),
                hideAlwaysAvailable: controls.hideAlwaysAvailable.checked,
                hideUnavailableAt: controls.hideUnavailableAt.checked,
                hideUnavailableDay: normalizeCustomFilterWeekday(controls.hideUnavailableDay.value),
                hideUnavailableTime: normalizeCustomFilterTime(controls.hideUnavailableTime.value),
                hideAvailableAt: controls.hideAvailableAt.checked,
                hideAvailableDay: normalizeCustomFilterWeekday(controls.hideAvailableDay.value),
                hideAvailableTime: normalizeCustomFilterTime(controls.hideAvailableTime.value),
                hideDescriptionMatch: controls.hideDescriptionMatch.checked,
                hideDescriptionQuery: normalizeCustomFilterQuery(controls.hideDescriptionQuery.value),
                showOnlyUnavailableAt: controls.showOnlyUnavailableAt.checked,
                showOnlyUnavailableDay: normalizeCustomFilterWeekday(controls.showOnlyUnavailableDay.value),
                showOnlyUnavailableTime: normalizeCustomFilterTime(controls.showOnlyUnavailableTime.value),
                showOnlyAvailableAt: controls.showOnlyAvailableAt.checked,
                showOnlyAvailableDay: normalizeCustomFilterWeekday(controls.showOnlyAvailableDay.value),
                showOnlyAvailableTime: normalizeCustomFilterTime(controls.showOnlyAvailableTime.value),
                showOnlyWithoutOpinions: controls.showOnlyWithoutOpinions.checked,
                showOnlySingleProfileAd: controls.showOnlySingleProfileAd.checked,
                showOnlyAgencyPhone: controls.showOnlyAgencyPhone.checked,
                showOnlyDescriptionMatch: controls.showOnlyDescriptionMatch.checked,
                showOnlyDescriptionQuery: normalizeCustomFilterQuery(controls.showOnlyDescriptionQuery.value)
            };
        }

        function getActiveFilterCount(value = config) {
            return [
                value.hideWithoutOpinions,
                value.hideProfileAdsOver,
                value.hideActiveAdsOver,
                value.hideAgencyPhone,
                value.hideYoungerThan,
                value.hideOlderThan,
                value.hideAdYoungerThan,
                value.hideAdOlderThan,
                value.hideAlwaysAvailable,
                value.hideUnavailableAt,
                value.hideAvailableAt,
                value.hideDescriptionMatch && !!normalizeCustomFilterQuery(value.hideDescriptionQuery),
                value.showOnlyWithoutOpinions,
                value.showOnlySingleProfileAd,
                value.showOnlyAgencyPhone,
                value.showOnlyUnavailableAt,
                value.showOnlyAvailableAt,
                value.showOnlyDescriptionMatch && !!normalizeCustomFilterQuery(value.showOnlyDescriptionQuery)
            ].filter(Boolean).length;
        }

        function updateTrigger() {
            const activeCount = getActiveFilterCount(config);
            const arrow = panel.hidden ? '▼' : '▲';
            trigger.textContent = `Filtry niestandardowe${activeCount ? ` (${activeCount})` : ''} ${arrow}`;
            trigger.setAttribute('aria-expanded', String(!panel.hidden));
            trigger.setAttribute('aria-controls', panel.id);
        }

        function updateDisabledStates() {
            for (const checkbox of panel.querySelectorAll('.vm-custom-filter-check')) {
                checkbox.disabled = filtersBusy;
            }
            for (const [checkbox, dependent] of dependentControls) {
                dependent.disabled = filtersBusy || !checkbox.checked;
            }
            clearBtn.disabled = filtersBusy;
        }

        function setFiltersBusy(value) {
            filtersBusy = !!value;
            trigger.disabled = filtersBusy;
            trigger.setAttribute('aria-disabled', String(filtersBusy));
            panel.dataset.busy = String(filtersBusy);
            updateDisabledStates();
        }

        function commit() {
            config = saveEscortCustomFilters(readConfig());
            updateDisabledStates();
            updateTrigger();
            if (typeof onChange === 'function') onChange(config);
        }

        for (const checkbox of panel.querySelectorAll('.vm-custom-filter-check')) {
            checkbox.addEventListener('change', commit);
        }
        for (const input of panel.querySelectorAll('.vm-custom-filter-number')) {
            input.addEventListener('change', () => {
                const min = Number(input.min) || 0;
                const fallback = config[input.dataset.filterKey] ?? (min || 1);
                input.value = String(normalizeCustomFilterInteger(input.value, fallback, min));
                commit();
            });
        }
        for (const unit of panel.querySelectorAll('.vm-custom-filter-unit')) {
            unit.addEventListener('change', commit);
        }
        for (const day of panel.querySelectorAll('.vm-custom-filter-day')) {
            day.addEventListener('change', commit);
        }
        for (const time of panel.querySelectorAll('.vm-custom-filter-time')) {
            time.addEventListener('change', () => {
                time.value = normalizeCustomFilterTime(time.value);
                commit();
            });
        }
        let descriptionCommitTimer = null;
        for (const input of panel.querySelectorAll('.vm-custom-filter-text')) {
            input.addEventListener('input', () => {
                clearTimeout(descriptionCommitTimer);
                descriptionCommitTimer = setTimeout(commit, 180);
            });
            input.addEventListener('change', commit);
        }

        trigger.addEventListener('click', () => {
            if (filtersBusy) return;
            panel.hidden = !panel.hidden;
            config = saveEscortCustomFilters(readConfig());
            updateTrigger();
        });

        priceDurationSelect?.addEventListener('change', () => {
            const previousDuration = SETTINGS.searchResultPriceDuration;
            const duration = normalizeEscortPriceDuration(priceDurationSelect.value);
            try {
                const saved = saveSettings({
                    ...getSettings(),
                    searchResultPriceDuration: duration
                });
                SETTINGS = { ...DEFAULT_SETTINGS, ...saved };
                if (typeof onPriceDurationChange === 'function') {
                    onPriceDurationChange(duration);
                }
            } catch (error) {
                priceDurationSelect.value = previousDuration;
                log('Nie udało się zapisać czasu ceny na kafelkach', error);
            }
        });

        clearBtn.addEventListener('click', () => {
            for (const checkbox of panel.querySelectorAll('.vm-custom-filter-check')) {
                checkbox.checked = false;
            }
            commit();
        });

        updateDisabledStates();
        updateTrigger();

        titleCol.insertAdjacentElement('afterend', control);
        // Panel jest pierwszy, a wszystkie paski postępu znajdują się pod nim.
        // Gdy panel jest zwinięty (`hidden`), paski naturalnie zajmują miejsce
        // bezpośrednio pod nagłówkiem.
        titleRow.insertAdjacentElement('afterend', panel);
        panel.insertAdjacentElement('afterend', progress);

        return {
            getConfig: () => ({ ...config }),
            setStatus: text => { status.textContent = text || ''; },
            setProgress: text => {
                const value = text || '';
                progressText.textContent = value;
                progress.hidden = !value;
                setFiltersBusy(!!value);
            },
            setCancelHandler: handler => {
                progressCancelButton.hidden = typeof handler !== 'function';
                progressCancelButton.onclick = typeof handler === 'function'
                    ? handler
                    : null;
            }
        };
    }

    async function loadAdditionalEscortListPages(initialContext, cancelToken = null) {
        const requested = normalizeListPagesToShow(LIST_PAGES_TO_SHOW);

        if (requested === 1 || !initialContext?.anchors?.length) {
            return {
                loadedPages: 1,
                addedCards: 0,
                cancelled: false
            };
        }

        const targetContainer = getEscortListCardContainer(initialContext.anchors);
        if (!targetContainer) {
            log('Nie znaleziono kontenera kafelków do doładowania kolejnych stron');
            return {
                loadedPages: 1,
                addedCards: 0,
                cancelled: false
            };
        }

        const knownAdIds = new Set(
            initialContext.anchors
                .map(anchor => parseAdIdFromUrl(anchor.getAttribute('href')))
                .filter(Boolean)
        );

        const seenPageUrls = new Set([location.href]);
        const maxPages = requested === 'all' ? Number.POSITIVE_INFINITY : Number(requested);

        let currentDoc = document;
        let currentUrl = location.href;
        let loadedPages = 1;
        let addedCards = 0;
        let reachedEnd = false;
        let stoppedByError = false;
        let cancelled = false;

        const yieldToPage = () => new Promise(resolve => setTimeout(resolve, 0));

        while (loadedPages < maxPages) {
            if (cancelToken?.cancelled) {
                cancelled = true;
                break;
            }
            const nextUrl = findNextEscortListPageUrl(currentDoc, currentUrl);

            if (!nextUrl) {
                reachedEnd = true;
                break;
            }

            if (seenPageUrls.has(nextUrl)) {
                stoppedByError = true;
                break;
            }

            seenPageUrls.add(nextUrl);

            let response;
            const controller = new AbortController();
            const unsubscribeCancel = cancelToken?.onCancel(() => controller.abort()) || (() => {});
            try {
                recordDiagnosticRequest(nextUrl, 'GET');
                response = await fetch(nextUrl, {
                    method: 'GET',
                    credentials: 'include',
                    redirect: 'follow',
                    cache: 'no-store',
                    signal: controller.signal
                });
            } catch (error) {
                if (cancelToken?.cancelled) {
                    cancelled = true;
                    break;
                }
                stoppedByError = true;
                log(`Nie udało się pobrać kolejnej strony Escort.club: ${nextUrl}`, error);
                break;
            } finally {
                unsubscribeCancel();
            }

            if (!response.ok) {
                stoppedByError = true;
                log(`Escort.club zwrócił HTTP ${response.status} dla ${nextUrl}`);
                break;
            }

            const html = await response.text();
            const fetchedUrl = response.url || nextUrl;

            // Parsowanie i seryjne dokładanie wielu kafelków jest synchroniczne.
            // Oddaj sterowanie stronie przed każdą porcją, aby kliknięcia,
            // przewijanie i otwieranie anonsów pozostawały responsywne.
            await yieldToPage();
            if (cancelToken?.cancelled) {
                cancelled = true;
                break;
            }
            const fetchedDoc = new DOMParser().parseFromString(html, 'text/html');
            const fetchedContext = getEscortClubListContext(fetchedDoc, fetchedUrl);

            if (!fetchedContext?.anchors?.length) {
                stoppedByError = true;
                log(`Brak właściwych kafelków na doładowanej stronie: ${fetchedUrl}`);
                break;
            }

            let importedSinceYield = 0;
            for (const anchor of fetchedContext.anchors) {
                if (cancelToken?.cancelled) {
                    cancelled = true;
                    break;
                }
                const href = anchor.getAttribute('href');
                const adId = parseAdIdFromUrl(href);
                const sourceCard = anchor.closest('.item-col.col');

                if (!adId || !sourceCard || knownAdIds.has(adId)) {
                    continue;
                }

                const importedCard = prepareImportedEscortCard(
                    document.importNode(sourceCard, true),
                    fetchedUrl
                );

                targetContainer.appendChild(importedCard);
                knownAdIds.add(adId);
                addedCards++;

                importedSinceYield++;
                if (importedSinceYield >= 8) {
                    importedSinceYield = 0;
                    await yieldToPage();
                }
            }

            if (cancelled) break;

            loadedPages++;
            currentDoc = fetchedDoc;
            currentUrl = fetchedUrl;

            await yieldToPage();

            // Zabezpieczenie przed wadliwą paginacją generującą nieskończony ciąg URL-i.
            if (loadedPages >= 500) {
                log('Przerwano doładowywanie po 500 stronach - zabezpieczenie przed pętlą paginacji');
                break;
            }
        }

        return {
            loadedPages,
            addedCards,
            reachedEnd,
            stoppedByError,
            cancelled
        };
    }

    function showEscortListInitializationError(error) {
        ensureSettingsUiStyles();
        const existing = document.getElementById('vm-escort-list-init-error');
        if (existing) return;

        const message = makeElement('div');
        message.id = 'vm-escort-list-init-error';
        message.className = 'vm-escort-list-init-error';
        message.textContent =
            'Nie udało się uruchomić dodatków skryptu na tej liście anonsów. Odśwież stronę; jeśli problem się powtórzy, sprawdź konsolę przeglądarki.';
        if (error?.message) message.title = String(error.message);

        const heading = findEscortMainResultsHeading();
        const host = heading?.parentElement || document.querySelector('main') || document.body;
        if (heading?.parentElement) heading.insertAdjacentElement('afterend', message);
        else host?.prepend(message);
    }

    async function initEscortClubListPage() {
        // Polecane/Wyróżnione są nadal całkowicie pomijane przez analizę.
        // Na stronach /szukaj czekamy również na elementy dobudowywane dynamicznie.
        const shouldWaitForList =
            isEscortSearchPath() ||
            location.pathname.startsWith('/anonse/');

        let listContext = shouldWaitForList
            ? await waitForEscortClubListContext(10000)
            : getEscortClubListContext();

        applyEscortSectionVisibility();

        if (!listContext) {
            recordParserIssue(
                'Escort.club - lista wyników',
                'Nie znaleziono głównej sekcji listy anonsów.'
            );
            log('Nie znaleziono sekcji "Anonse erotyczne ..." na stronie wyszukiwania Escort.club');
            return;
        }

        if (!listContext.anchors.length) {
            recordParserIssue(
                'Escort.club - kafelki',
                'Sekcja listy istnieje, ale nie znaleziono linków kafelków anonsów.'
            );
            log('Nie znaleziono kafelków anonsów Escort.club w analizowanej sekcji');
            return;
        }

        const listPagesControl = addEscortListPagesControl(listContext);
        let customFilterCallbacksReady = false;
        const customFilterController = addEscortCustomFiltersControl(
            listContext,
            config => {
                if (customFilterCallbacksReady) applyCustomFilters(config);
            },
            duration => {
                if (customFilterCallbacksReady) renderAllEscortListPrices(duration);
            }
        );
        const requestedPages = normalizeListPagesToShow(LIST_PAGES_TO_SHOW);
        const listPagesCancelToken = requestedPages !== 1
            ? createOperationCancelToken('Ładowanie wielu stron Escort.club')
            : null;

        if (listPagesControl && requestedPages !== 1) {
            if (listPagesControl.status) listPagesControl.status.textContent = '';
            listPagesControl.setProgress?.('Ładowanie stron...', () => {
                listPagesCancelToken?.cancel('Przerwano ładowanie kolejnych stron');
            });

            if (listPagesControl.select) {
                listPagesControl.select.disabled = true;
            }
        }

        const loadResult = await loadAdditionalEscortListPages(
            listContext,
            listPagesCancelToken
        );

        listPagesControl?.setProgress?.('');

        if (loadResult.cancelled) {
            recordDiagnosticCancellation(
                'Ładowanie wielu stron Escort.club',
                `Wczytano ${loadResult.loadedPages} stron przed przerwaniem.`
            );
        }

        if (listPagesControl?.status) {
            if (loadResult.cancelled) {
                listPagesControl.status.textContent =
                    `przerwano po ${loadResult.loadedPages} ${polishPageWord(loadResult.loadedPages)}`;
            } else if (loadResult.loadedPages > 1) {
                listPagesControl.status.textContent =
                    `wczytano ${loadResult.loadedPages} ${loadResult.loadedPages === 1 ? 'stronę' : 'strony'}`;
            } else {
                listPagesControl.status.textContent = '';
            }

            if (listPagesControl.select) {
                listPagesControl.select.disabled = false;
            }
        }

        // Po doładowaniu ponownie odczytujemy zakres listy. Dzięki temu dalsza
        // logika traktuje kafelki z kolejnych stron dokładnie tak samo jak bieżące.
        listContext = getEscortClubListContext();

        if (!listContext?.anchors?.length) {
            recordParserIssue(
                'Escort.club - lista po doładowaniu',
                'Po doładowaniu stron nie znaleziono kafelków anonsów.'
            );
            log('Po doładowaniu nie znaleziono kafelków anonsów Escort.club');
            return;
        }

        const { anchors, adsNav } = listContext;

        // Cache odczytujemy raz na początku. Ten sam indeks służy zarówno do
        // natychmiastowego renderowania kafelków, jak i do licznika agregacji.
        const aggregationMode = normalizeEscortAggregationMode(
            SETTINGS.escortAggregationMode
        );
        const phoneBasedAggregation =
            aggregationMode === 'exact-phone' ||
            aggregationMode === 'phone-escort-search';
        const escortClubPhoneSearchAggregation =
            aggregationMode === 'phone-escort-search';
        const listSearchMode = getEscortListSearchMode();
        const cacheIndex = buildEscortListCacheIndex();
        const phoneCacheIndex = buildEscortAdPhoneCacheIndex();
        const phoneSearchAggregationIndex = new Map();
        const cardStates = new Map();
        const aggregationKeyToGroup = new Map();
        const pendingNetworkStates = [];
        let nextGroupId = 1;
        let listInitComplete = false;
        const listDataCancelToken = createOperationCancelToken(
            'Masowe pobieranie danych na liście anonsów'
        );
        let massDataCancelled = false;

        customFilterController?.setCancelHandler(() => {
            if (massDataCancelled) return;
            massDataCancelled = true;
            listDataCancelToken.cancel('Przerwano masowe pobieranie danych');
            recordDiagnosticCancellation(
                'Masowe pobieranie danych na liście anonsów',
                `Przerwano przy ${cardStates.size} kafelkach.`
            );
            for (const state of cardStates.values()) {
                state.dataCancelled = true;
                state.checkDone = true;
                state.adDataRetryScheduled = false;
            }
            customFilterController.setProgress('');
            customFilterController.setStatus(
                'Pobieranie przerwane. Odśwież stronę, aby wznowić.'
            );
            applyCustomFilters(customFilterController.getConfig());
            updatePageAggregateSummary();
        });

        const originalSummaryLine = adsNav
            ? [...adsNav.querySelectorAll('p')].find(p => /Wyświetlane/i.test(p.textContent || ''))
            : null;

        const originalSummaryText = (originalSummaryLine?.textContent || '')
            .replace(/\s+/g, ' ')
            .trim();

        const totalAdsMatch = originalSummaryText.match(/spośród\s+(\d[\d\s.]*)/i);
        const totalAds = totalAdsMatch
            ? Number(totalAdsMatch[1].replace(/[\s.]/g, ''))
            : null;

        function polishPageWord(n) {
            if (n === 1) return 'strona';

            const last = n % 10;
            const lastTwo = n % 100;
            return last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)
                ? 'strony'
                : 'stron';
        }

        function repeatedAdsPhrase(n) {
            if (n === 1) return 'powtarzające się anons';

            const last = n % 10;
            const lastTwo = n % 100;
            if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) {
                return 'powtarzające się anonse';
            }

            return 'powtarzających się anonsów';
        }

        function updateLoadedSummaryLine() {
            if (!originalSummaryLine) return;

            const loadedCount = anchors.length;
            const requested = normalizeListPagesToShow(LIST_PAGES_TO_SHOW);
            const allPagesLoaded = requested === 'all' && loadResult.reachedEnd && !loadResult.stoppedByError;
            const pagesText = allPagesLoaded
                ? 'wszystkie strony'
                : `${loadResult.loadedPages} ${polishPageWord(loadResult.loadedPages)}`;

            originalSummaryLine.innerHTML =
                Number.isFinite(totalAds) && totalAds > 0
                    ? `Załadowano <strong>${loadedCount}</strong> z <strong>${totalAds}</strong> ${totalAds === 1 ? 'anonsu' : 'anonsów'} • ${pagesText}.`
                    : `Załadowano <strong>${loadedCount}</strong> ${polishAdWord(loadedCount)} • ${pagesText}.`;

            originalSummaryLine.title =
                'Licznik obejmuje kafelki rzeczywiście załadowane na tej liście przez Escort.club i skrypt.';
        }

        let aggregateSummaryLine = adsNav?.querySelector('.vm-escorti-page-aggregate-summary') || null;
        if (!aggregateSummaryLine && originalSummaryLine) {
            aggregateSummaryLine = makeElement('p');
            aggregateSummaryLine.className = 'vm-escorti-page-aggregate-summary';
            Object.assign(aggregateSummaryLine.style, { marginTop: '2px' });
            originalSummaryLine.insertAdjacentElement('afterend', aggregateSummaryLine);
        }

        updateLoadedSummaryLine();

        function getCustomFilterDataNeeds(filters) {
            const source = filters || {};
            return {
                escorti: !!(
                    source.hideWithoutOpinions ||
                    source.hideProfileAdsOver ||
                    source.hideYoungerThan ||
                    source.hideOlderThan ||
                    source.showOnlyWithoutOpinions ||
                    source.showOnlySingleProfileAd
                ),
                escortAd: !!(
                    source.hideAlwaysAvailable ||
                    source.hideAdYoungerThan ||
                    source.hideAdOlderThan ||
                    source.hideAgencyPhone ||
                    source.hideUnavailableAt ||
                    source.hideAvailableAt ||
                    (
                        source.hideDescriptionMatch &&
                        normalizeCustomFilterQuery(source.hideDescriptionQuery)
                    ) ||
                    source.showOnlyUnavailableAt ||
                    source.showOnlyAvailableAt ||
                    source.showOnlyAgencyPhone ||
                    (
                        source.showOnlyDescriptionMatch &&
                        normalizeCustomFilterQuery(source.showOnlyDescriptionQuery)
                    )
                ),
                agencyPhone: !!(
                    source.hideAgencyPhone || source.showOnlyAgencyPhone
                )
            };
        }

        function getCustomFilterDataProgress(states, filters) {
            const needs = getCustomFilterDataNeeds(filters);
            if (!needs.escorti && !needs.escortAd) return null;

            let completed = 0;
            for (const state of states) {
                if (state.dataCancelled) {
                    completed++;
                    continue;
                }
                const escortiReady = !needs.escorti ||
                    state.result?.status === 'ok' ||
                    state.checkDone;
                const agencyPhoneReady = !needs.agencyPhone || (
                    typeof state.adData?.isAgencyOrSalonPhone === 'boolean'
                );
                const escortAdFailed =
                    state.adData?.status === 'error' &&
                    !state.adDataLoading &&
                    !state.adDataRetryScheduled &&
                    !state.agencyPhoneRefreshStarted &&
                    (state.adDataAttempts || 0) >= 2;
                const escortAdReady = !needs.escortAd || (
                    state.adData?.status === 'ok' && agencyPhoneReady
                ) || escortAdFailed;

                if (escortiReady && escortAdReady) completed++;
            }

            return {
                completed,
                total: states.length,
                pending: Math.max(0, states.length - completed)
            };
        }

        function updatePageAggregateSummary() {
            if (!listInitComplete) return;

            const states = [...cardStates.values()];
            const pendingCount = states.filter(state =>
                !state.dataCancelled && (
                !state.checkDone ||
                (
                    SETTINGS.mergeEscortAds &&
                    phoneBasedAggregation &&
                    (
                        !state.adData ||
                        state.adDataLoading ||
                        (
                            escortClubPhoneSearchAggregation &&
                            isEscortClubPhoneSearchAggregationPending(state)
                        )
                    )
                ))
            ).length;
            const checkedCount = states.length - pendingCount;
            const filterDataProgress = getCustomFilterDataProgress(
                states,
                customFilterController?.getConfig()
            );

            customFilterController?.setProgress(
                massDataCancelled
                    ? ''
                    : filterDataProgress?.pending > 0
                    ? `Pobieranie danych ${filterDataProgress.completed}/${filterDataProgress.total}…`
                    : pendingCount > 0
                    ? `Sprawdzanie: ${checkedCount}/${states.length}…`
                    : ''
            );
            if (massDataCancelled) {
                customFilterController?.setStatus(
                    'Pobieranie przerwane. Odśwież stronę, aby wznowić.'
                );
            }

            if (!aggregateSummaryLine) return;

            if (!SETTINGS.mergeEscortAds) {
                aggregateSummaryLine.innerHTML =
                    'Agregacja wyłączona' +
                    (pendingCount > 0 ? ' • trwa sprawdzanie…' : '.');
                return;
            }

            const repeatedCount = states.filter(state => state.mergedHidden).length;
            const afterAggregation = states.length - repeatedCount;

            aggregateSummaryLine.innerHTML =
                `Po agregacji: <strong>${afterAggregation}</strong> ${polishAdWord(afterAggregation)} ` +
                `• ukryto <strong>${repeatedCount}</strong> ${repeatedAdsPhrase(repeatedCount)}` +
                (pendingCount > 0 ? ' • trwa sprawdzanie…' : '.');

            aggregateSummaryLine.title =
                'Ukryte powtarzające się anonse to kafelki połączone przez agregację z innym kafelkiem na liście.';
        }

        function ensureMergedCardHighlightStyles() {
            if (document.getElementById('vm-escorti-merged-card-styles')) return;

            const style = makeElement('style');
            style.id = 'vm-escorti-merged-card-styles';
            style.textContent = `
                .vm-escorti-merged-expanded-card {
                    position: relative !important;
                }

                .vm-escorti-merged-expanded-card::after {
                    content: '';
                    position: absolute;
                    inset: 0;
                    box-sizing: border-box;
                    border: 4px solid #00d8f0;
                    background: rgba(0, 216, 240, .10);
                    box-shadow:
                        inset 0 0 0 1px rgba(255, 255, 255, .20),
                        0 0 8px rgba(0, 216, 240, .35);
                    pointer-events: none;
                    z-index: 15;
                }
            `;
            document.head.appendChild(style);
        }

        function clearMergedCardHighlight(state) {
            state.card.classList.remove('vm-escorti-merged-expanded-card');
        }

        function highlightMergedCard(state) {
            ensureMergedCardHighlightStyles();
            state.card.classList.add('vm-escorti-merged-expanded-card');
        }

        function restoreCard(state) {
            clearMergedCardHighlight(state);
            state.mergedHidden = false;
            state.card.style.display = state.customHidden ? 'none' : state.originalDisplay;
        }

        function hideMergedCard(state) {
            clearMergedCardHighlight(state);
            state.card.style.display = 'none';
            state.mergedHidden = true;
        }

        function showMergedCard(state) {
            state.card.style.display = state.customHidden ? 'none' : state.originalDisplay;
            if (state.customHidden) clearMergedCardHighlight(state);
            else highlightMergedCard(state);

            // Nadal jest powtórzeniem z punktu widzenia agregacji/podsumowania.
            state.mergedHidden = true;
        }

        function renderState(state) {
            if (!state.result) return;
            renderListEscortiResult(state.line, state.result, !!state.stale);
        }

        function getCanonical(group) {
            const members = [...group.members];

            return members.reduce(
                (best, item) => (!best || item.index < best.index ? item : best),
                null
            );
        }

        function getStateAggregationKeys(state) {
            if (phoneBasedAggregation) {
                const phone = normalizeEscortCachedPhone(
                    state?.adData?.phoneDigits || state?.adData?.phone
                );
                return phone ? [`phone:${phone}`] : [];
            }

            return getEscortProfileKeys(state?.result);
        }

        function getGroupAggregateAdIds(group) {
            const adIds = new Set(
                [...group.members]
                    .map(member => member.adId)
                    .filter(Boolean)
            );

            if (!SETTINGS.usePersistentCache) {
                return adIds;
            }

            for (const aggregationKey of group.aggregationKeys) {
                const cachedIds = phoneBasedAggregation
                    ? phoneCacheIndex.phoneToAdIds.get(
                        aggregationKey.replace(/^phone:/, '')
                    )
                    : cacheIndex.freshProfileToAdIds.get(aggregationKey);
                for (const adId of cachedIds || []) {
                    adIds.add(adId);
                }

                if (escortClubPhoneSearchAggregation) {
                    const searchedIds = phoneSearchAggregationIndex.get(
                        aggregationKey.replace(/^phone:/, '')
                    )?.adIds;
                    for (const adId of searchedIds || []) adIds.add(adId);
                }
            }

            return adIds;
        }

        function refreshGroupDisplay(group) {
            if (!group?.members?.size) return;

            const members = [...group.members].sort((a, b) => a.index - b.index);
            const canonical = getCanonical(group);
            if (!canonical) return;

            for (const member of members) {
                member.group = group;
                if (member === canonical) {
                    restoreCard(member);
                    if (group.expanded && members.length > 1) {
                        highlightMergedCard(member);
                    }
                } else if (group.expanded) {
                    showMergedCard(member);
                } else {
                    hideMergedCard(member);
                }
            }

            if (group.expanded) {
                let insertAfter = canonical.card;
                for (const member of members) {
                    if (member === canonical) continue;
                    insertAfter.insertAdjacentElement('afterend', member.card);
                    insertAfter = member.card;
                }
            }

            const aggregateAdIds = getGroupAggregateAdIds(group);
            const aggregateCount = aggregateAdIds.size;
            const cachedOnlyCount = Math.max(0, aggregateCount - members.length);
            const visibleAdIds = new Set(members.map(member => String(member.adId)));
            const cachedOnlyAdData = [...aggregateAdIds]
                .filter(adId => !visibleAdIds.has(String(adId)))
                .map(adId => getStoredEscortAdData(adId))
                .filter(adData => adData?.status === 'ok');
            const dataDifferences = getEscortListGroupDataDifferences(
                members,
                cachedOnlyAdData
            );

            setListMergedInfo(
                canonical.line,
                aggregateCount,
                !!group.expanded,
                members.length > 1
                    ? () => {
                        group.expanded = !group.expanded;
                        refreshGroupDisplay(group);
                        updatePageAggregateSummary();
                    }
                    : null,
                cachedOnlyCount,
                dataDifferences
            );
            renderState(canonical);

            for (const member of members) {
                if (member !== canonical) setListMergedInfo(member.line, 0);
            }

            if (aggregateCount > 1 && canonical.line) {
                const additionalSource = escortClubPhoneSearchAggregation
                    ? 'z wyszukiwarki Escort.club lub aktualnego lokalnego cache; bez tworzenia kafelków'
                    : 'z aktualnego lokalnego cache; bez tworzenia kafelków';
                canonical.line.title =
                    `${canonical.line.title || 'Dane z Escorti'}; ` +
                    `na tej stronie: ${members.length}, w liczniku agregacji: ${aggregateCount}` +
                    (cachedOnlyCount > 0
                        ? ` (dodatkowo ${cachedOnlyCount} ${additionalSource})`
                        : '');
            }
        }

        function applyListMerge(state) {
            if (!SETTINGS.mergeEscortAds || !state) return;
            if (
                !phoneBasedAggregation &&
                (!state.result || state.result.status !== 'ok' || state.result.profiles === 0)
            ) {
                return;
            }

            const aggregationKeys = getStateAggregationKeys(state);
            if (!aggregationKeys.length) return;

            const touchedGroups = [...new Set(
                aggregationKeys
                    .map(key => aggregationKeyToGroup.get(key))
                    .filter(Boolean)
            )];

            let group;
            if (!touchedGroups.length) {
                group = {
                    id: nextGroupId++,
                    members: new Set(),
                    aggregationKeys: new Set(),
                    expanded: false
                };
            } else {
                group = touchedGroups[0];

                for (const other of touchedGroups.slice(1)) {
                    if (other === group) continue;
                    group.expanded = !!(group.expanded || other.expanded);
                    for (const member of other.members) group.members.add(member);
                    for (const key of other.aggregationKeys) group.aggregationKeys.add(key);
                }
            }

            group.members.add(state);
            for (const key of aggregationKeys) group.aggregationKeys.add(key);
            for (const key of group.aggregationKeys) aggregationKeyToGroup.set(key, group);
            for (const member of group.members) member.group = group;

            refreshGroupDisplay(group);
            updatePageAggregateSummary();
        }

        function customFilterAgeToDays(value, unit) {
            const multipliers = {
                days: 1,
                weeks: 7,
                months: 30.4375,
                years: 365.25
            };
            return Math.max(1, Number(value) || 1) * (multipliers[unit] || 1);
        }

        function normalizeCustomFilterWeekdayToken(value) {
            return normalizeText(value)
                .replace(/:\s*$/, '')
                .normalize('NFD')
                .replace(/[\u0300-\u036f]/g, '')
                .replace(/ł/g, 'l');
        }

        function normalizeCustomFilterSearchText(value) {
            return normalizeText(value)
                .normalize('NFD')
                .replace(/[\u0300-\u036f]/g, '')
                .replace(/ł/g, 'l');
        }

        function customFilterDescriptionContains(description, query) {
            const needle = normalizeCustomFilterSearchText(query);
            return !!needle && normalizeCustomFilterSearchText(description).includes(needle);
        }

        function customFilterTimeToMinutes(value) {
            const normalized = normalizeCustomFilterTime(value, '');
            if (!normalized) return null;
            const [hours, minutes] = normalized.split(':').map(Number);
            return hours * 60 + minutes;
        }

        function customFilterIsAlwaysAvailable(value) {
            const normalized = normalizeCustomFilterWeekdayToken(value);
            return normalized === 'caly czas' ||
                normalized === 'cala dobe' ||
                normalized === '24h' ||
                normalized === '24 h';
        }

        function customFilterIsExplicitlyUnavailable(value) {
            return normalizeCustomFilterWeekdayToken(value).includes('niedostepn');
        }

        function customFilterHasAlwaysAvailable(availability) {
            return !!availability && typeof availability === 'object' &&
                Object.values(availability).some(customFilterIsAlwaysAvailable);
        }

        function customFilterGetDayAvailability(availability, weekday) {
            if (!availability || typeof availability !== 'object') return null;

            const label = ESCORT_CUSTOM_FILTER_WEEKDAYS
                .find(([value]) => value === normalizeCustomFilterWeekday(weekday))?.[1];
            const wanted = normalizeCustomFilterWeekdayToken(label);

            for (const [day, hours] of Object.entries(availability)) {
                if (normalizeCustomFilterWeekdayToken(day) === wanted) return String(hours || '');
            }

            return null;
        }

        function customFilterGetAvailabilityAt(availability, weekday, time) {
            const hoursText = customFilterGetDayAvailability(availability, weekday);
            if (!hoursText) return null;
            if (customFilterIsAlwaysAvailable(hoursText)) return true;
            if (customFilterIsExplicitlyUnavailable(hoursText)) return false;

            const selectedMinutes = customFilterTimeToMinutes(time);
            if (selectedMinutes == null) return null;

            const ranges = [...hoursText.matchAll(
                /(\d{1,2})[.:](\d{2})\s*(?:do|[-–—])\s*(\d{1,2})[.:](\d{2})/gi
            )];

            if (!ranges.length) return null;

            return ranges.some(match => {
                const start = Number(match[1]) * 60 + Number(match[2]);
                const end = Number(match[3]) * 60 + Number(match[4]);
                if (start === end) return true;
                if (start < end) return selectedMinutes >= start && selectedMinutes <= end;
                return selectedMinutes >= start || selectedMinutes <= end;
            });
        }

        function customFilterIsUnavailableAt(availability, weekday, time) {
            const available = customFilterGetAvailabilityAt(availability, weekday, time);
            return available == null ? null : !available;
        }

        function getCustomFilterBuckets() {
            const buckets = [];
            const seenGroups = new Set();

            for (const state of cardStates.values()) {
                if (state.group) {
                    if (seenGroups.has(state.group)) continue;
                    seenGroups.add(state.group);
                    buckets.push({ group: state.group, members: [...state.group.members] });
                } else {
                    buckets.push({ group: null, members: [state] });
                }
            }

            return buckets;
        }

        function getBucketActiveAdCount(bucket) {
            const ids = bucket.group
                ? getGroupAggregateAdIds(bucket.group)
                : new Set(bucket.members.map(member => member.adId).filter(Boolean));

            if (SETTINGS.usePersistentCache) {
                for (const member of bucket.members) {
                    const aggregationKeys = phoneBasedAggregation
                        ? getStateAggregationKeys(member)
                        : getEscortProfileKeys(member.result);

                    for (const aggregationKey of aggregationKeys) {
                        const cachedIds = phoneBasedAggregation
                            ? phoneCacheIndex.phoneToAdIds.get(
                                aggregationKey.replace(/^phone:/, '')
                            )
                            : cacheIndex.freshProfileToAdIds.get(aggregationKey);
                        for (const adId of cachedIds || []) ids.add(adId);

                        if (escortClubPhoneSearchAggregation) {
                            const searchedIds = phoneSearchAggregationIndex.get(
                                aggregationKey.replace(/^phone:/, '')
                            )?.adIds;
                            for (const adId of searchedIds || []) ids.add(adId);
                        }
                    }
                }
            }

            return ids.size || null;
        }

        function getBucketAvailabilityRecords(bucket) {
            const records = [];
            let unresolved = false;

            for (const member of bucket.members) {
                if (member.adData?.status === 'ok') {
                    records.push(
                        member.adData.availability && typeof member.adData.availability === 'object'
                            ? member.adData.availability
                            : {}
                    );
                } else {
                    unresolved = true;
                }
            }

            return { records, unresolved };
        }

        function getBucketDescriptionRecords(bucket) {
            const records = [];
            let unresolved = false;

            for (const member of bucket.members) {
                if (member.adData?.status === 'ok') {
                    records.push(String(member.adData.description || ''));
                } else {
                    unresolved = true;
                }
            }

            return { records, unresolved };
        }

        function getBucketAdAgeRecords(bucket) {
            const records = [];
            let unresolved = false;

            for (const member of bucket.members) {
                if (member.adData?.status !== 'ok') {
                    unresolved = true;
                    continue;
                }

                const datePostedTime = profileDateToTime(member.adData.datePosted);
                if (datePostedTime == null) {
                    unresolved = true;
                    continue;
                }

                records.push(Math.max(
                    0,
                    (Date.now() - datePostedTime) / (24 * 60 * 60 * 1000)
                ));
            }

            return { records, unresolved };
        }

        function getBucketAgencyPhoneRecords(bucket) {
            const records = [];
            let unresolved = false;

            for (const member of bucket.members) {
                if (member.adData?.status !== 'ok') {
                    unresolved = true;
                    continue;
                }

                const hasCurrentAgencyPhoneData =
                    typeof member.adData.isAgencyOrSalonPhone === 'boolean';
                if (!hasCurrentAgencyPhoneData) {
                    unresolved = true;
                    continue;
                }

                records.push(member.adData.isAgencyOrSalonPhone);
            }

            return { records, unresolved };
        }

        function resolveBucketRecordMatch(bucketRecords, predicate) {
            let unresolved = bucketRecords.unresolved;

            for (const record of bucketRecords.records) {
                const result = predicate(record);
                if (result === true) return true;
                if (result == null) unresolved = true;
            }

            return unresolved ? null : false;
        }

        function getStatePhoneForAggregation(state) {
            return normalizeEscortCachedPhone(
                state?.adData?.phoneDigits || state?.adData?.phone
            );
        }

        function isEscortClubPhoneSearchAggregationPending(state) {
            if (!escortClubPhoneSearchAggregation) return false;
            if (!state?.adData || state.adDataLoading) return true;

            const phone = getStatePhoneForAggregation(state);
            if (!phone) return false;

            const entry = phoneSearchAggregationIndex.get(phone);
            return !entry || !!entry.loading;
        }

        function ensureEscortClubPhoneSearchAggregation(state) {
            if (
                !escortClubPhoneSearchAggregation ||
                !state ||
                massDataCancelled ||
                listDataCancelToken.cancelled
            ) return null;

            const phone = getStatePhoneForAggregation(state);
            if (!phone) {
                state.phoneSearchAggregationLoading = false;
                return null;
            }

            const existing = phoneSearchAggregationIndex.get(phone);
            if (existing) {
                state.phoneSearchAggregationLoading = !!existing.loading;
                return existing.promise;
            }

            const entry = {
                adIds: new Set(),
                loading: true,
                error: null,
                promise: null
            };
            phoneSearchAggregationIndex.set(phone, entry);

            for (const member of cardStates.values()) {
                if (getStatePhoneForAggregation(member) === phone) {
                    member.phoneSearchAggregationLoading = true;
                }
            }
            updatePageAggregateSummary();

            entry.promise = (async () => {
                const search = await fetchEscortClubPhoneSearchAds(
                    phone,
                    listDataCancelToken
                );
                const searchItems = search.adUrls
                    .map(url => ({
                        url,
                        adId: parseAdIdFromUrl(url)
                    }))
                    .filter(item => item.adId);

                for (const item of searchItems) entry.adIds.add(item.adId);

                await watchMapLimit(searchItems, 4, async item => {
                    try {
                        listDataCancelToken.throwIfCancelled();
                        const result = await getEscortAdData(
                            item.adId,
                            item.url,
                            false,
                            listDataCancelToken
                        );
                        if (result?.status === 'ok') {
                            updateEscortAdPhoneCacheIndex(
                                phoneCacheIndex,
                                item.adId,
                                result
                            );
                        }
                        return result;
                    } catch (error) {
                        if (isOperationCancelledError(error)) throw error;
                        log(
                            `Nie udało się pobrać danych anonsu ${item.adId} podczas agregowania przez wyszukiwarkę Escort.club`,
                            error
                        );
                        return { status: 'error' };
                    }
                });
            })()
                .catch(error => {
                    if (isOperationCancelledError(error) || listDataCancelToken.cancelled) {
                        entry.error = 'cancelled';
                        return;
                    }
                    entry.error = error?.message || String(error);
                    log(
                        `Błąd agregowania przez wyszukiwarkę Escort.club dla numeru ${phone}`,
                        error
                    );
                })
                .finally(() => {
                    entry.loading = false;
                    for (const member of cardStates.values()) {
                        if (getStatePhoneForAggregation(member) !== phone) continue;
                        member.phoneSearchAggregationLoading = false;
                        if (member.group) refreshGroupDisplay(member.group);
                    }
                    applyCustomFilters();
                    updatePageAggregateSummary();
                });

            return entry.promise;
        }

        function ensureEscortAdDataForState(state, forceRefresh = false) {
            if (!state || massDataCancelled || listDataCancelToken.cancelled) {
                if (state) state.dataCancelled = true;
                return;
            }
            if (forceRefresh) {
                if (state.agencyPhoneRefreshStarted) return;
                state.agencyPhoneRefreshStarted = true;
            } else {
                if (state.adDataPrefetchStarted) return;
                state.adDataPrefetchStarted = true;
            }

            state.adDataAttempts = (state.adDataAttempts || 0) + 1;
            state.adDataLoading = true;
            if (!forceRefresh) state.adData = null;
            getEscortAdData(
                state.adId,
                state.adUrl,
                forceRefresh,
                listDataCancelToken
            )
                .then(result => {
                    if (result?.status === 'cancelled') {
                        state.dataCancelled = true;
                        return;
                    }
                    state.adData = result?.status === 'ok'
                        ? result
                        : { status: 'error' };
                })
                .catch(error => {
                    if (isOperationCancelledError(error)) {
                        state.dataCancelled = true;
                        return;
                    }
                    state.adData = { status: 'error' };
                })
                .finally(() => {
                    state.adDataLoading = false;
                    if (forceRefresh) state.agencyPhoneRefreshStarted = false;
                    updateEscortAdPhoneCacheIndex(
                        phoneCacheIndex,
                        state.adId,
                        state.adData
                    );
                    if (phoneBasedAggregation) {
                        applyListMerge(state);
                        if (escortClubPhoneSearchAggregation) {
                            ensureEscortClubPhoneSearchAggregation(state);
                        }
                    } else if (state.group) {
                        // Dane profilu i cennik docierają niezależnie od wyniku
                        // Escorti. Po ich zapisaniu odświeżamy ostrzeżenie grupy.
                        refreshGroupDisplay(state.group);
                    }
                    renderEscortListPrice(
                        state.priceLine,
                        state.adData,
                        SETTINGS.searchResultPriceDuration
                    );
                    if (
                        !forceRefresh &&
                        !massDataCancelled &&
                        !listDataCancelToken.cancelled &&
                        state.adData?.status !== 'ok' &&
                        state.adDataAttempts < 2
                    ) {
                        state.adDataRetryScheduled = true;
                        setTimeout(() => {
                            state.adDataRetryScheduled = false;
                            state.adDataPrefetchStarted = false;
                            ensureEscortAdDataForState(state);
                        }, 1200);
                    }
                    applyCustomFilters();
                });
        }

        function renderAllEscortListPrices() {
            if (!SETTINGS.showPricesInSearchResults) return;

            for (const state of cardStates.values()) {
                renderEscortListPrice(
                    state.priceLine,
                    state.adData,
                    SETTINGS.searchResultPriceDuration
                );
            }

            // Zmiana czasu w comboboxie zmienia również cenę porównywaną
            // pomiędzy anonsami należącymi do tej samej grupy.
            const groups = new Set(
                [...cardStates.values()].map(state => state.group).filter(Boolean)
            );
            for (const group of groups) refreshGroupDisplay(group);
        }

        function getBucketCustomFilterMetrics(bucket, filters) {
            const results = bucket.members
                .map(member => member.result)
                .filter(result => result && result.status === 'ok');

            let opinions = null;
            let profileAds = null;
            let creationTime = null;

            for (const result of results) {
                const opinionCount = typeof result.garsoTopics === 'number'
                    ? result.garsoTopics
                    : (result.profiles === 0 ? 0 : null);
                if (opinionCount != null) {
                    opinions = opinions == null ? opinionCount : Math.max(opinions, opinionCount);
                }

                const adCount = typeof result.adLinks === 'number'
                    ? result.adLinks
                    : (result.profiles === 0 ? 0 : null);
                if (adCount != null) {
                    profileAds = profileAds == null ? adCount : Math.max(profileAds, adCount);
                }

                const time = profileDateToTime(result.creationDate);
                if (time != null && (creationTime == null || time < creationTime)) {
                    creationTime = time;
                }
            }

            const availability = getBucketAvailabilityRecords(bucket);
            const descriptions = getBucketDescriptionRecords(bucket);
            const adAges = getBucketAdAgeRecords(bucket);
            const agencyPhones = getBucketAgencyPhoneRecords(bucket);

            return {
                opinions,
                profileAds,
                activeAds: getBucketActiveAdCount(bucket),
                agencyPhone: resolveBucketRecordMatch(
                    agencyPhones,
                    value => value === true
                ),
                alwaysAvailable: resolveBucketRecordMatch(
                    availability,
                    customFilterHasAlwaysAvailable
                ),
                unavailableAtHideTime: resolveBucketRecordMatch(
                    availability,
                    value => customFilterIsUnavailableAt(
                        value,
                        filters.hideUnavailableDay,
                        filters.hideUnavailableTime
                    )
                ),
                availableAtHideAvailableTime: resolveBucketRecordMatch(
                    availability,
                    value => customFilterGetAvailabilityAt(
                        value,
                        filters.hideAvailableDay,
                        filters.hideAvailableTime
                    )
                ),
                unavailableAtShowOnlyTime: resolveBucketRecordMatch(
                    availability,
                    value => customFilterIsUnavailableAt(
                        value,
                        filters.showOnlyUnavailableDay,
                        filters.showOnlyUnavailableTime
                    )
                ),
                availableAtShowOnlyTime: resolveBucketRecordMatch(
                    availability,
                    value => customFilterGetAvailabilityAt(
                        value,
                        filters.showOnlyAvailableDay,
                        filters.showOnlyAvailableTime
                    )
                ),
                descriptionMatchesHide: resolveBucketRecordMatch(
                    descriptions,
                    value => customFilterDescriptionContains(
                        value,
                        filters.hideDescriptionQuery
                    )
                ),
                descriptionMatchesShowOnly: resolveBucketRecordMatch(
                    descriptions,
                    value => customFilterDescriptionContains(
                        value,
                        filters.showOnlyDescriptionQuery
                    )
                ),
                adYoungerThan: resolveBucketRecordMatch(
                    adAges,
                    value => value < customFilterAgeToDays(
                        filters.adYoungerValue,
                        filters.adYoungerUnit
                    )
                ),
                adOlderThan: resolveBucketRecordMatch(
                    adAges,
                    value => value > customFilterAgeToDays(
                        filters.adOlderValue,
                        filters.adOlderUnit
                    )
                ),
                ageDays: creationTime == null
                    ? null
                    : Math.max(0, (Date.now() - creationTime) / (24 * 60 * 60 * 1000))
            };
        }

        function applyCustomFilters(config = customFilterController?.getConfig()) {
            const filters = config || getEscortCustomFilters();
            const activeFilterCount = [
                filters.hideWithoutOpinions,
                filters.hideProfileAdsOver,
                filters.hideActiveAdsOver,
                filters.hideAgencyPhone,
                filters.hideYoungerThan,
                filters.hideOlderThan,
                filters.hideAdYoungerThan,
                filters.hideAdOlderThan,
                filters.hideAlwaysAvailable,
                filters.hideUnavailableAt,
                filters.hideAvailableAt,
                filters.hideDescriptionMatch && !!normalizeCustomFilterQuery(filters.hideDescriptionQuery),
                filters.showOnlyWithoutOpinions,
                filters.showOnlySingleProfileAd,
                filters.showOnlyAgencyPhone,
                filters.showOnlyUnavailableAt,
                filters.showOnlyAvailableAt,
                filters.showOnlyDescriptionMatch && !!normalizeCustomFilterQuery(filters.showOnlyDescriptionQuery)
            ].filter(Boolean).length;

            const needsEscortAdData = !!(
                filters.hideAlwaysAvailable ||
                filters.hideAdYoungerThan ||
                filters.hideAdOlderThan ||
                filters.hideAgencyPhone ||
                filters.hideUnavailableAt ||
                filters.hideAvailableAt ||
                (filters.hideDescriptionMatch && normalizeCustomFilterQuery(filters.hideDescriptionQuery)) ||
                filters.showOnlyUnavailableAt ||
                filters.showOnlyAvailableAt ||
                filters.showOnlyAgencyPhone ||
                (filters.showOnlyDescriptionMatch && normalizeCustomFilterQuery(filters.showOnlyDescriptionQuery))
            );

            if (needsEscortAdData) {
                const needsAgencyPhoneData = !!(
                    filters.hideAgencyPhone || filters.showOnlyAgencyPhone
                );
                for (const state of cardStates.values()) {
                    const cachedAgencyPhoneDataStale =
                        needsAgencyPhoneData &&
                        state.adData?.status === 'ok' &&
                        typeof state.adData.isAgencyOrSalonPhone !== 'boolean';
                    ensureEscortAdDataForState(
                        state,
                        cachedAgencyPhoneDataStale
                    );
                }
            }

            let hiddenCount = 0;
            let pendingCount = 0;
            let failedDataCount = 0;

            for (const bucket of getCustomFilterBuckets()) {
                const metrics = getBucketCustomFilterMetrics(bucket, filters);
                let shouldHide = false;
                let unresolved = false;

                const test = (enabled, value, predicate) => {
                    if (!enabled) return;
                    if (value == null) {
                        unresolved = true;
                        return;
                    }
                    if (predicate(value)) shouldHide = true;
                };

                test(filters.hideWithoutOpinions, metrics.opinions, value => value === 0);
                test(
                    filters.hideProfileAdsOver,
                    metrics.profileAds,
                    value => value > normalizeCustomFilterInteger(filters.profileAdsLimit, 10)
                );
                test(
                    filters.hideActiveAdsOver,
                    metrics.activeAds,
                    value => value > normalizeCustomFilterInteger(filters.activeAdsLimit, 1)
                );
                test(
                    filters.hideAgencyPhone,
                    metrics.agencyPhone,
                    value => value === true
                );
                test(
                    filters.hideYoungerThan,
                    metrics.ageDays,
                    value => value < customFilterAgeToDays(filters.youngerValue, filters.youngerUnit)
                );
                test(
                    filters.hideOlderThan,
                    metrics.ageDays,
                    value => value > customFilterAgeToDays(filters.olderValue, filters.olderUnit)
                );
                test(
                    filters.hideAdYoungerThan,
                    metrics.adYoungerThan,
                    value => value === true
                );
                test(
                    filters.hideAdOlderThan,
                    metrics.adOlderThan,
                    value => value === true
                );
                test(
                    filters.hideAlwaysAvailable,
                    metrics.alwaysAvailable,
                    value => value === true
                );
                test(
                    filters.hideUnavailableAt,
                    metrics.unavailableAtHideTime,
                    value => value === true
                );
                test(
                    filters.hideAvailableAt,
                    metrics.availableAtHideAvailableTime,
                    value => value === true
                );
                test(
                    filters.hideDescriptionMatch && !!normalizeCustomFilterQuery(filters.hideDescriptionQuery),
                    metrics.descriptionMatchesHide,
                    value => value === true
                );
                test(
                    filters.showOnlyWithoutOpinions,
                    metrics.opinions,
                    value => value !== 0
                );
                test(
                    filters.showOnlySingleProfileAd,
                    metrics.profileAds,
                    value => value !== 1
                );
                test(
                    filters.showOnlyAgencyPhone,
                    metrics.agencyPhone,
                    value => value === false
                );
                test(
                    filters.showOnlyUnavailableAt,
                    metrics.unavailableAtShowOnlyTime,
                    value => value === false
                );
                test(
                    filters.showOnlyAvailableAt,
                    metrics.availableAtShowOnlyTime,
                    value => value === false
                );
                test(
                    filters.showOnlyDescriptionMatch && !!normalizeCustomFilterQuery(filters.showOnlyDescriptionQuery),
                    metrics.descriptionMatchesShowOnly,
                    value => value === false
                );

                for (const member of bucket.members) {
                    member.customHidden = shouldHide;
                }

                if (bucket.group) {
                    refreshGroupDisplay(bucket.group);
                } else if (bucket.members[0]) {
                    restoreCard(bucket.members[0]);
                }

                if (shouldHide) {
                    hiddenCount++;
                } else if (unresolved) {
                    const failed = needsEscortAdData && bucket.members.some(member =>
                        member.adData?.status === 'error' &&
                        !member.adDataLoading &&
                        !member.adDataRetryScheduled &&
                        (member.adDataAttempts || 0) >= 2
                    );
                    if (failed) failedDataCount++;
                    else pendingCount++;
                }
            }

            if (customFilterController) {
                if (massDataCancelled) {
                    customFilterController.setStatus(
                        'Pobieranie przerwane. Odśwież stronę, aby wznowić.'
                    );
                } else if (!activeFilterCount) {
                    customFilterController.setStatus('');
                } else {
                    customFilterController.setStatus(
                        `Ukryto: ${hiddenCount}` +
                        (pendingCount ? ` • oczekuje na dane: ${pendingCount}` : '') +
                        (failedDataCount ? ` • brak danych: ${failedDataCount}` : '')
                    );
                }
            }

            updatePageAggregateSummary();
        }

        function scheduleListNetworkCheck(state) {
            if (
                !state ||
                state.checkDone ||
                state.checkScheduled ||
                massDataCancelled ||
                listDataCancelToken.cancelled
            ) {
                if (state && (massDataCancelled || listDataCancelToken.cancelled)) {
                    state.dataCancelled = true;
                    state.checkDone = true;
                }
                return;
            }

            state.checkScheduled = true;
            if (!state.result) state.line.textContent = '… ogł • od … • … opinii';

            enqueueListJob(async () => {
                try {
                    listDataCancelToken.throwIfCancelled();
                    // W czasie oczekiwania w kolejce wcześniejsze sprawdzenie
                    // mogło już zwrócić ten sam profil wraz z tym anonsem.
                    // Wykorzystujemy wtedy jego wynik zamiast ponownie pytać Escorti.
                    const newlyResolvedCache = cacheIndex.byAdId.get(state.adId);
                    if (isListCacheFresh(newlyResolvedCache)) {
                        state.result = newlyResolvedCache;
                        state.stale = false;
                        renderState(state);
                        applyListMerge(state);
                        return;
                    }

                    let searchValue = state.adUrl;

                    if (listSearchMode === 'phone-escorti') {
                        const adDataResult = await getEscortAdData(
                            state.adId,
                            state.adUrl,
                            false,
                            listDataCancelToken
                        );
                        listDataCancelToken.throwIfCancelled();
                        state.adData = adDataResult?.status === 'ok'
                            ? adDataResult
                            : { status: 'error' };
                        updateEscortAdPhoneCacheIndex(
                            phoneCacheIndex,
                            state.adId,
                            state.adData
                        );
                        renderEscortListPrice(
                            state.priceLine,
                            state.adData,
                            SETTINGS.searchResultPriceDuration
                        );

                        searchValue = normalizeEscortCachedPhone(
                            state.adData?.phoneDigits || state.adData?.phone
                        );
                        if (!searchValue) {
                            state.result = { status: 'phone-missing' };
                            state.stale = false;
                            renderState(state);
                            return;
                        }
                    }

                    const result = await getOrStartListCheck(
                        state.adId,
                        searchValue,
                        listSearchMode,
                        listDataCancelToken
                    );
                    listDataCancelToken.throwIfCancelled();
                    state.result = result;
                    state.stale = false;
                    updateEscortListCacheIndex(cacheIndex, state.adId, result);
                    renderState(state);
                    applyListMerge(state);
                } catch (e) {
                    if (isOperationCancelledError(e) || listDataCancelToken.cancelled) {
                        state.dataCancelled = true;
                        state.result = state.result || { status: 'cancelled' };
                        return;
                    }
                    log(`Błąd Escorti dla anonsu ${state.adId}`, e);
                    state.result = { status: 'error' };
                    state.stale = false;
                    renderState(state);
                } finally {
                    state.checkDone = true;
                    applyCustomFilters();
                    updatePageAggregateSummary();
                }
            });
        }

        // Brak oczekiwania na IntersectionObserver: wszystkie brakujące/stare wpisy
        // trafią do kolejki po natychmiastowym wyrenderowaniu świeżego cache.

        anchors.forEach((anchor, index) => {
            const adUrl = new URL(anchor.getAttribute('href'), location.href).href;
            const adId = parseAdIdFromUrl(adUrl);
            const card = anchor.closest('.item-col.col');
            if (!adId || !card) return;

            if (SETTINGS.showEscortiTileButton) {
                addEscortiOpenButton(card, adUrl);
            } else {
                card.querySelector('.vm-escorti-open-button')?.remove();
            }

            const line = getListCardDataLine(card);
            if (line) {
                line.style.display = SETTINGS.showEscortiTileData
                    ? 'block'
                    : 'none';
            }
            const priceLine = SETTINGS.showPricesInSearchResults
                ? getListCardPriceLine(card)
                : null;
            const state = {
                adId,
                adUrl,
                index,
                anchor,
                card,
                line,
                priceLine,
                originalDisplay: card.style.display,
                result: null,
                stale: false,
                group: null,
                mergedHidden: false,
                customHidden: false,
                adData: null,
                adDataPrefetchStarted: false,
                adDataAttempts: 0,
                adDataRetryScheduled: false,
                adDataLoading: false,
                phoneSearchAggregationLoading: false,
                checkDone: false,
                checkScheduled: false,
                dataCancelled: false
            };
            cardStates.set(adId, state);
            renderEscortListPrice(
                state.priceLine,
                state.adData,
                SETTINGS.searchResultPriceDuration
            );

            const cache = cacheIndex.byAdId.get(adId) || null;
            if (cache) {
                state.result = cache;
                state.stale = !isListCacheFresh(cache);
                renderState(state);

                if (!state.stale) {
                    applyListMerge(state);
                    state.checkDone = true;
                }
            }

            if (!state.checkDone) {
                pendingNetworkStates.push(state);
            }
        });

        customFilterCallbacksReady = true;
        applyCustomFilters(customFilterController?.getConfig());

        // Przy włączonym trwałym cache pobieramy dane wszystkich widocznych
        // anonsów. Robimy to również wtedy, gdy cena albo agregacja po
        // identycznym numerze wymaga danych pojedynczego anonsu.
        if (
            (PREFETCH_ESCORT_CLUB_AD_DATA && SETTINGS.usePersistentCache) ||
            SETTINGS.showPricesInSearchResults ||
            (SETTINGS.mergeEscortAds && phoneBasedAggregation)
        ) {
            for (const state of cardStates.values()) {
                ensureEscortAdDataForState(state);
            }
        }

        listInitComplete = true;
        updatePageAggregateSummary();

        // Świeży cache jest już widoczny. Teraz od razu uruchamiamy pozostałe
        // sprawdzenia, maksymalnie LIST_MAX_CONCURRENT równolegle.
        for (const state of pendingNetworkStates) {
            scheduleListNetworkCheck(state);
        }
    }

    // ============================================================
    // ESCORT.CLUB
    // ============================================================

    // ============================================================
    // NARZĘDZIA GALERII - data pliku, bezpośrednie linki oraz
    // Lens / PicDetective / TinEye dla aktualnego dużego zdjęcia
    // ============================================================

    const IMAGE_TOOLS_ID = 'vm-gallery-image-tools';
    const IMAGE_SERVER_DATE_ID = 'vm-gallery-image-server-date';
    const LIGHTBOX_IMAGE_URL_ID = 'vm-lightbox-image-url';
    const mediaServerDateCache = new Map();

    function isEscortGalleryPhotoUrl(value) {
        if (!value) return false;
        try {
            const url = new URL(value, location.href);
            return /(^|\.)escort\.club$/i.test(url.hostname)
                && /\/galleries\//i.test(url.pathname)
                && /\.(?:jpe?g|png|webp|avif)(?:$|[?#])/i.test(url.href);
        } catch (_) { return false; }
    }

    function isEscortGalleryVideoUrl(value) {
        if (!value) return false;
        try {
            const url = new URL(value, location.href);
            return /(^|\.)escort\.club$/i.test(url.hostname)
                && /\/videos\//i.test(url.pathname)
                && /\.(?:mp4|webm|m4v|mov)(?:$|[?#])/i.test(url.href);
        } catch (_) { return false; }
    }

    function buildGoogleLensUrl(imageUrl) {
        const url = new URL('https://lens.google.com/uploadbyurl');
        url.searchParams.set('url', imageUrl);
        url.searchParams.set('hl', 'pl');
        return url.href;
    }

    function buildPicDetectiveUrl(imageUrl) {
        const url = new URL('https://picdetective.com/');
        url.searchParams.set('vm_image_url', imageUrl);
        return url.href;
    }

    function buildTinEyeUrl(imageUrl) {
        const url = new URL('https://tineye.com/');
        url.searchParams.set('vm_image_url', imageUrl);
        return url.href;
    }

    function getActiveGallerySlide() {
        const slider = document.querySelector('.content-gallery-col .galleryContainer #lightSlider');
        if (!slider) return null;

        const active = slider.querySelector(':scope > li.lslide.active, :scope > li.active');
        if (active) return active;

        const wrapper = slider.closest('.lSSlideWrapper');
        if (!wrapper) return null;
        const wr = wrapper.getBoundingClientRect();
        let best = null, bestArea = 0;
        for (const li of slider.querySelectorAll(':scope > li')) {
            const r = li.getBoundingClientRect();
            const w = Math.max(0, Math.min(r.right, wr.right) - Math.max(r.left, wr.left));
            const h = Math.max(0, Math.min(r.bottom, wr.bottom) - Math.max(r.top, wr.top));
            if (w * h > bestArea) { bestArea = w * h; best = li; }
        }
        return best;
    }

    function getActiveGalleryMedia() {
        const slide = getActiveGallerySlide();
        if (!slide) return null;
        const anchor = slide.querySelector('a.simple-zoom-image');
        if (!anchor) return null;

        const isVideo = anchor.classList.contains('video-layer');
        const images = [...anchor.querySelectorAll('img')];
        const img = isVideo
            ? images.find(candidate => /\/thumbs\//i.test(candidate.currentSrc || candidate.src || ''))
                || images.at(-1)
            : images[0];
        if (!img) return null;

        let mediaUrl = anchor.href || '';
        if (isVideo) {
            if (!isEscortGalleryVideoUrl(mediaUrl)) return null;
        } else {
            if (!isEscortGalleryPhotoUrl(mediaUrl)) mediaUrl = img.currentSrc || img.src || '';
            if (!isEscortGalleryPhotoUrl(mediaUrl)) return null;
        }

        const r = img.getBoundingClientRect();
        if (r.width < 100 || r.height < 100) return null;
        return {
            slide,
            img,
            mediaUrl,
            mediaType: isVideo ? 'video' : 'photo'
        };
    }

    function getActiveGalleryPhoto(activeMedia = getActiveGalleryMedia()) {
        if (!activeMedia || activeMedia.mediaType !== 'photo') return null;
        return {
            ...activeMedia,
            imageUrl: activeMedia.mediaUrl
        };
    }

    function createImageToolButton(label, title, getUrl) {
        const btn = makeButton('', label);
        btn.title = title;
        Object.assign(btn.style, {
            display:'block', width:'60px', padding:'3px 5px', border:'1px solid rgba(255,255,255,.75)',
            borderRadius:'4px', background:'rgba(20,20,20,.82)', color:'#fff', fontSize:'9px',
            lineHeight:'1.15', fontWeight:'700', fontFamily:'Arial,sans-serif', textAlign:'center',
            cursor:'pointer', boxShadow:'0 1px 5px rgba(0,0,0,.45)'
        });
        btn.addEventListener('mousedown', e => { e.preventDefault(); e.stopPropagation(); }, true);
        btn.addEventListener('click', e => {
            e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
            const imageUrl = btn.closest(`#${IMAGE_TOOLS_ID}`)?.dataset.imageUrl;
            if (imageUrl) GM_openInTab(getUrl(imageUrl), { active:true, insert:true });
        }, true);
        return btn;
    }

    function getOrCreateImageTools() {
        let tools = document.getElementById(IMAGE_TOOLS_ID);
        if (tools) return tools;
        tools = makeElement('div');
        tools.id = IMAGE_TOOLS_ID;
        Object.assign(tools.style, {
            position:'absolute', display:'none', zIndex:'1000', flexDirection:'column',
            alignItems:'stretch', gap:'3px', pointerEvents:'auto'
        });
        tools.appendChild(createImageToolButton('Lens ↗', 'Wyszukaj aktualne zdjęcie w Google Lens', buildGoogleLensUrl));
        tools.appendChild(createImageToolButton('PD ↗', 'Otwórz PicDetective z wklejonym adresem zdjęcia', buildPicDetectiveUrl));
        tools.appendChild(createImageToolButton('TinEye ↗', 'Otwórz TinEye z wklejonym adresem zdjęcia', buildTinEyeUrl));
        document.body.appendChild(tools);
        return tools;
    }

    function parseResponseHeader(headers, name) {
        const safeName = String(name || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const match = String(headers || '').match(new RegExp(`^${safeName}:\\s*(.+)$`, 'im'));
        return match ? match[1].trim() : null;
    }

    function formatServerMediaDate(date) {
        return new Intl.DateTimeFormat('pl-PL', {
            day: '2-digit',
            month: '2-digit',
            year: 'numeric'
        }).format(date);
    }

    function formatMinimumMediaAge(date) {
        const ms = Math.max(0, Date.now() - date.getTime());
        const days = Math.floor(ms / 86400000);

        if (days < 1) return '<1 dnia';
        if (days < 31) return `${days} dni`;

        const months = Math.floor(days / 30.4375);
        if (months < 12) return `${months} mies.`;

        const years = Math.floor(months / 12);
        const restMonths = months % 12;
        return restMonths ? `${years} r. ${restMonths} mies.` : `${years} r.`;
    }

    function getMediaServerDate(mediaUrl) {
        if (mediaServerDateCache.has(mediaUrl)) return mediaServerDateCache.get(mediaUrl);

        const promise = gmRequest({
            method: 'HEAD',
            url: mediaUrl,
            timeout: 12000
        }).then(response => {
            const value = parseResponseHeader(response.responseHeaders, 'Last-Modified');
            if (!value) return null;
            const timestamp = Date.parse(value);
            return Number.isFinite(timestamp) ? new Date(timestamp) : null;
        }).catch(error => {
            log('Nie udało się odczytać daty pliku galerii z serwera', error);
            return null;
        });

        mediaServerDateCache.set(mediaUrl, promise);
        return promise;
    }

    function getOrCreateImageServerDateLabel() {
        let label = document.getElementById(IMAGE_SERVER_DATE_ID);
        if (label) return label;

        label = makeElement('div');
        label.id = IMAGE_SERVER_DATE_ID;
        Object.assign(label.style, {
            position: 'absolute',
            display: 'none',
            zIndex: '999',
            padding: '3px 6px',
            borderRadius: '4px',
            background: 'rgba(20,20,20,.72)',
            color: '#fff',
            fontSize: '9px',
            lineHeight: '1.15',
            fontWeight: '600',
            fontFamily: 'Arial,sans-serif',
            whiteSpace: 'nowrap',
            pointerEvents: 'auto',
            cursor: 'help',
            textShadow: '0 1px 2px rgba(0,0,0,.65)'
        });
        document.body.appendChild(label);
        return label;
    }

    async function updateMediaServerDateLabel(active) {
        const label = getOrCreateImageServerDateLabel();

        if (!active) {
            label.style.display = 'none';
            label.dataset.mediaUrl = '';
            label.dataset.loaded = '';
            return;
        }

        const { slide, img, mediaUrl, mediaType } = active;
        if (label.parentElement !== slide) slide.appendChild(label);
        if (getComputedStyle(slide).position === 'static') slide.style.position = 'relative';

        const sameLoadedMedia = label.dataset.mediaUrl === mediaUrl && label.dataset.loaded === '1';
        label.style.display = 'block';

        const positionLabel = () => {
            const sr = slide.getBoundingClientRect();
            const ir = img.getBoundingClientRect();
            label.style.left = `${Math.round(Math.max(4, ir.left - sr.left + 8))}px`;
            label.style.top = `${Math.round(Math.max(4, ir.top - sr.top + 8))}px`;
        };

        if (sameLoadedMedia) {
            positionLabel();
            return;
        }

        label.dataset.mediaUrl = mediaUrl;
        label.dataset.loaded = '0';
        label.textContent = 'data: …';
        label.title = `Odczytuję nagłówek Last-Modified z serwera ${mediaType === 'video' ? 'filmu' : 'obrazu'}.`;
        positionLabel();

        const serverDate = await getMediaServerDate(mediaUrl);
        if (label.dataset.mediaUrl !== mediaUrl) return;

        label.dataset.loaded = '1';

        if (!serverDate) {
            label.textContent = 'data serwera: brak';
            label.title = `Serwer nie zwrócił nagłówka Last-Modified dla tego ${mediaType === 'video' ? 'filmu' : 'zdjęcia'}.`;
            positionLabel();
            return;
        }

        label.textContent = `${formatServerMediaDate(serverDate)} (${formatMinimumMediaAge(serverDate)})`;
        label.title =
            `Data pochodzi z nagłówka HTTP Last-Modified serwera ${mediaType === 'video' ? 'filmu' : 'obrazu'}.\n` +
            `Wiek w nawiasie jest wiekiem minimalnym: materiał mógł powstać wcześniej niż wskazuje data pliku na serwerze.\n` +
            `Last-Modified nie jest datą wykonania materiału.`;
        positionLabel();
    }

    function updateGalleryImageTools() {
        const tools = getOrCreateImageTools();
        const activeMedia = getActiveGalleryMedia();
        const active = getActiveGalleryPhoto(activeMedia);
        updateMediaServerDateLabel(activeMedia);

        if (!active || !SETTINGS.showImageSearchButtons) {
            tools.style.display = 'none';
            tools.dataset.imageUrl = '';
            return;
        }

        const { slide, img, imageUrl } = active;
        if (tools.parentElement !== slide) slide.appendChild(tools);
        if (getComputedStyle(slide).position === 'static') slide.style.position = 'relative';
        tools.dataset.imageUrl = imageUrl;
        tools.style.display = 'flex';

        const sr = slide.getBoundingClientRect();
        const ir = img.getBoundingClientRect();
        const tr = tools.getBoundingClientRect();
        tools.style.left = `${Math.round(Math.max(4, ir.right - sr.left - tr.width - 8))}px`;
        tools.style.top = `${Math.round(Math.max(4, ir.top - sr.top + 8))}px`;
    }

    function updateLightboxImageUrl() {
        const lightbox = document.querySelector('#lightbox');
        const existing = document.getElementById(LIGHTBOX_IMAGE_URL_ID);
        if (!lightbox) return;
        if (getComputedStyle(lightbox).display === 'none') {
            if (existing) existing.style.display = 'none';
            return;
        }

        const img = lightbox.querySelector('.lb-image');
        const dataContainer = lightbox.querySelector('.lb-dataContainer');
        if (!img || !dataContainer) return;
        const imageUrl = img.currentSrc || img.src || '';
        if (!/^https:\/\/static\.escort\.club\//i.test(imageUrl)) {
            if (existing) existing.style.display = 'none';
            return;
        }

        let box = existing;
        if (!box) {
            box = makeElement('div');
            box.id = LIGHTBOX_IMAGE_URL_ID;
            Object.assign(box.style, {
                display:'block', clear:'both', width:'100%', boxSizing:'border-box', padding:'7px 4px 5px',
                fontFamily:'Arial,sans-serif', fontSize:'11px', lineHeight:'1.25', textAlign:'left', wordBreak:'break-all'
            });
            const link = makeElement('a');
            link.target = '_blank';
            link.rel = 'noopener noreferrer';
            Object.assign(link.style, { color:'#f54da3', textDecoration:'underline', cursor:'pointer' });
            link.addEventListener('click', event => {
                const href = link.href;
                if (!href) return;
                event.preventDefault();
                event.stopPropagation();
                GM_openInTab(href, { active:true, insert:true });
            });
            box.appendChild(link);
            dataContainer.appendChild(box);
        } else if (box.parentElement !== dataContainer) {
            dataContainer.appendChild(box);
        }

        const link = box.querySelector('a');
        box.style.display = 'block';
        link.href = imageUrl;
        link.textContent = imageUrl;
        link.title = 'Otwórz samo zdjęcie w nowej karcie';
    }

    function initEscortGalleryWatcher() {
        if (!/^\/anons\/\d+\.html\/?$/i.test(location.pathname)) return;

        const gallery = document.querySelector('.content-gallery-col');
        const initialPhoneContainer = getEscortPhoneLink()?.closest('.adsPhone');
        if (!gallery && !initialPhoneContainer) return;

        let observedLightbox = null;
        let observedPhoneContainer = null;
        let observedTargetsBound = false;

        const scheduleGalleryUpdate = createAnimationFrameScheduler(updateGalleryImageTools);
        const scheduleLightboxUpdate = createAnimationFrameScheduler(updateLightboxImageUrl);

        const isOwnScriptElement = target => {
            const element = target?.nodeType === Node.ELEMENT_NODE
                ? target
                : target?.parentElement;
            return !!element?.closest(
                `#${IMAGE_TOOLS_ID}, #${IMAGE_SERVER_DATE_ID}, ` +
                `#${LIGHTBOX_IMAGE_URL_ID}, .vm-escort-copy-phone`
            );
        };

        const isOnlyOwnScriptMutation = mutation => {
            if (isOwnScriptElement(mutation.target)) return true;
            if (mutation.type !== 'childList') return false;

            const changedNodes = [
                ...mutation.addedNodes,
                ...mutation.removedNodes
            ];
            return changedNodes.length > 0 && changedNodes.every(isOwnScriptElement);
        };

        const observer = new MutationObserver(mutations => {
            let galleryChanged = false;
            let lightboxChanged = false;
            let phoneChanged = false;

            for (const mutation of mutations) {
                if (isOnlyOwnScriptMutation(mutation)) continue;

                if (
                    observedPhoneContainer &&
                    (
                        mutation.target === observedPhoneContainer ||
                        observedPhoneContainer.contains(mutation.target)
                    )
                ) {
                    phoneChanged = true;
                } else if (
                    observedLightbox &&
                    (mutation.target === observedLightbox || observedLightbox.contains(mutation.target))
                ) {
                    lightboxChanged = true;
                } else {
                    galleryChanged = true;
                }
            }

            if (galleryChanged) scheduleGalleryUpdate();
            if (lightboxChanged) scheduleLightboxUpdate();
            if (phoneChanged) checkAndAddButtons();
        });

        const refreshObservedTargets = () => {
            const lightbox = document.querySelector('#lightbox');
            const phoneContainer = getEscortPhoneLink()?.closest('.adsPhone') || null;
            if (
                observedTargetsBound &&
                lightbox === observedLightbox &&
                phoneContainer === observedPhoneContainer
            ) return;

            observer.disconnect();
            if (gallery) {
                observer.observe(gallery, {
                    subtree: true,
                    childList: true,
                    attributes: true,
                    attributeFilter: ['class', 'src', 'srcset', 'href']
                });
            }

            observedLightbox = lightbox;
            if (observedLightbox) {
                observer.observe(observedLightbox, {
                    subtree: true,
                    childList: true,
                    attributes: true,
                    attributeFilter: ['class', 'src', 'style']
                });
                scheduleLightboxUpdate();
            }

            observedPhoneContainer = phoneContainer;
            if (observedPhoneContainer) {
                observer.observe(observedPhoneContainer, {
                    subtree: true,
                    childList: true,
                    characterData: true,
                    attributes: true,
                    attributeFilter: ['href', 'data-phone-id', 'data-show-phone']
                });
            }

            observedTargetsBound = true;
        };

        if (gallery) {
            gallery.addEventListener('load', scheduleGalleryUpdate, true);
            gallery.addEventListener('transitionend', scheduleGalleryUpdate, true);
            gallery.addEventListener('click', event => {
                scheduleGalleryUpdate();
                const galleryAnchor = event.target.closest?.('a.simple-zoom-image');
                if (galleryAnchor) {
                    requestAnimationFrame(refreshObservedTargets);
                }
            }, true);
        }

        window.addEventListener('resize', scheduleGalleryUpdate, { passive: true });

        refreshObservedTargets();
        if (gallery) scheduleGalleryUpdate();
    }

    function getAdIdFromUrl() {
        const m = location.href.match(/anons\/(\d+)\.html/);
        return m ? m[1] : null;
    }

    function buildGarsoAdLinkSearchTerm(adId) {
        const normalizedAdId = String(adId || '').match(/^\d+$/)?.[0];
        if (!normalizedAdId) return '';

        // Parser wyszukiwarki Garsoniery usuwa ukośniki z search_term.
        // Spacje zapobiegają sklejeniu członów adresu po ich usunięciu.
        return `"escort.club / anons / ${normalizedAdId}.html"`;
    }

    function queryFirstEscortElement(selectors, root = document) {
        for (const selector of selectors) {
            const element = root.querySelector(selector);
            if (element) return element;
        }
        return null;
    }

    function getEscortPhoneLink() {
        return queryFirstEscortElement([
            '.content-info-col.-info .adsPhone a[data-show-phone][data-phone-id]',
            '.content-info-col.-info a[data-show-phone][data-phone-id]',
            '.content-info-col.-info .adsPhone a[href^="tel:"]',
            '.content-info-col.-info .adsPhone a[href]',
            '.content-info .adsPhone a[data-show-phone][data-phone-id]',
            '.adsPhone a[data-show-phone][data-phone-id]',
            '[data-show-phone][data-phone-id]',
            '.adsPhone a[href^="tel:"]',
            '.adsPhone a[href]'
        ]);
    }

    function getEscortButtonsContainer(phoneLink = getEscortPhoneLink()) {
        const phoneContact = phoneLink?.closest('.content-contact');
        if (phoneContact?.closest('.content-info-col.-info')) return phoneContact;

        const desktopInfo = document.querySelector('.content-info-col.-info');
        if (desktopInfo) {
            const desktopPhone = queryFirstEscortElement([
                '.adsPhone a[data-show-phone][data-phone-id]',
                '.adsPhone a[href^="tel:"]',
                '.adsPhone a[href]'
            ], desktopInfo);
            return desktopPhone?.closest('.content-contact')
                || desktopInfo.querySelector('.content-info')
                || desktopInfo;
        }

        if (phoneContact) return phoneContact;

        return queryFirstEscortElement([
            '.content-info .content-contact',
            '.content-info-col .content-info',
            '.content-info-col'
        ]);
    }

    function getEscortTipMessageButtonsContainer(scope = document) {
        return [...scope.querySelectorAll('.content-contact')].find(container =>
            container.querySelector('a[href*="action=tipTokens"]') &&
            container.querySelector('a[href*="action=sendMessage"]')
        ) || null;
    }

    function placeEscortResearchPanelAboveActionButtons(panel, fallbackContainer) {
        const getPlacement = () => {
            const infoColumn = document.querySelector('.content-info-col.-info')
                || fallbackContainer?.closest('.content-info-col.-info');
            if (!infoColumn) return null;

            const actionButtons = getEscortTipMessageButtonsContainer(infoColumn);
            const moreAboutSection = infoColumn.querySelector('.content-hours');
            const reference = actionButtons || moreAboutSection;

            if (reference?.parentNode) {
                return { parent: reference.parentNode, reference };
            }

            return {
                parent: infoColumn.querySelector('.content-info') || infoColumn,
                reference: null
            };
        };

        const place = () => {
            const placement = getPlacement();
            if (!placement?.parent || !panel) return false;
            if (
                panel.parentNode !== placement.parent ||
                panel.nextSibling !== placement.reference
            ) {
                placement.parent.insertBefore(panel, placement.reference);
            }
            return true;
        };

        if (place()) return;

        let attempts = 0;
        const timer = setInterval(() => {
            attempts++;
            if (place() || attempts >= 40) {
                clearInterval(timer);
            }
        }, 250);
    }

    function getFormattedPhoneNumber(phoneEl = getEscortPhoneLink()) {
        if (!phoneEl) return null;
        const rawPhone = phoneEl.getAttribute('href');
        if (!rawPhone || rawPhone === '#') return null;
        const digits = normalizeEscortCachedPhone(rawPhone);
        if (!digits) return null;
        return digits.length === 9
            ? digits.replace(/(\d{3})(\d{3})(\d{3})/, '$1-$2-$3')
            : `+${digits}`;
    }

    function formatEscortPhoneForClipboard(
        value,
        format = SETTINGS.phoneClipboardFormat
    ) {
        const digits = normalizeEscortCachedPhone(value);
        if (!digits) return null;

        if (digits.length !== 9) return `+${digits}`;

        const groups = digits.match(/\d{3}/g);
        if (!groups) return digits;

        return normalizePhoneClipboardFormat(format) === 'international-spaces'
            ? `+48 ${groups.join(' ')}`
            : groups.join('-');
    }

    function copyEscortPhoneToClipboard(value) {
        try {
            if (typeof GM_setClipboard === 'function') {
                GM_setClipboard(value, 'text');
                return Promise.resolve();
            }
        } catch (_) {}

        if (navigator.clipboard?.writeText) {
            return navigator.clipboard.writeText(value);
        }

        return new Promise((resolve, reject) => {
            const textarea = makeElement('textarea');
            textarea.value = value;
            textarea.setAttribute('readonly', '');
            Object.assign(textarea.style, {
                position: 'fixed',
                left: '-9999px',
                top: '0'
            });
            document.body.appendChild(textarea);
            textarea.select();

            try {
                if (!document.execCommand('copy')) throw new Error('copy failed');
                resolve();
            } catch (error) {
                reject(error);
            } finally {
                textarea.remove();
            }
        });
    }

    function renderEscortPhoneCopyButton(phone) {
        const digits = normalizeEscortCachedPhone(phone);
        if (!digits) return;

        const formatted = formatEscortPhoneForClipboard(digits);
        if (!formatted) return;

        for (const phoneBox of document.querySelectorAll('.adsPhone')) {
            phoneBox.querySelector('.vm-escort-local-phone')?.remove();

            const phoneLink = phoneBox.querySelector(
                '[data-show-phone], a[href^="tel:"]'
            );
            if (!phoneLink) continue;

            const phoneContact = phoneBox.closest('.content-contact') || phoneBox;
            for (const node of [...phoneContact.childNodes]) {
                if (
                    node !== phoneLink &&
                    node.nodeType === Node.TEXT_NODE &&
                    /^\s*Telefon\s*:\s*$/i.test(node.textContent || '')
                ) {
                    node.textContent = '';
                }
            }
            for (const label of phoneContact.querySelectorAll(
                '.label, strong, b, label, span'
            )) {
                if (
                    label !== phoneLink &&
                    !label.contains(phoneLink) &&
                    /^\s*Telefon\s*:\s*$/i.test(label.textContent || '')
                ) {
                    label.style.display = 'none';
                }
            }

            let copyButton = phoneBox.querySelector('.vm-escort-copy-phone');
            if (!copyButton) {
                copyButton = makeElement('button');
                copyButton.type = 'button';
                copyButton.className = 'vm-escort-copy-phone';
                copyButton.textContent = 'Kopiuj';
                Object.assign(copyButton.style, {
                    display: 'inline-flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    marginLeft: '8px',
                    padding: '4px 7px',
                    border: `1px solid ${getEscortPagePinkColor()}`,
                    borderRadius: '6px',
                    background: 'transparent',
                    color: getEscortPagePinkColor(),
                    fontSize: '10px',
                    fontWeight: '600',
                    lineHeight: '1',
                    whiteSpace: 'nowrap',
                    verticalAlign: 'middle',
                    cursor: 'pointer'
                });

                copyButton.addEventListener('click', event => {
                    event.preventDefault();
                    event.stopPropagation();

                    const value = copyButton.dataset.copyValue;
                    if (!value) return;

                    copyEscortPhoneToClipboard(value).then(() => {
                        copyButton.textContent = 'Skopiowano';
                        clearTimeout(copyButton._vmRestoreTimer);
                        copyButton._vmRestoreTimer = setTimeout(() => {
                            copyButton.textContent = 'Kopiuj';
                        }, 1200);
                    }).catch(() => {
                        copyButton.textContent = 'błąd kopiowania';
                    });
                });
            }

            copyButton.dataset.copyValue = formatted;
            copyButton.title = `Skopiuj ${formatted}`;

            // Nie przenoś ponownie przycisku, jeśli już jest we właściwym
            // miejscu. Każde ponowne insertAdjacentElement generowałoby
            // kolejną mutację obserwowanego pola telefonu.
            if (phoneLink.nextElementSibling !== copyButton) {
                phoneLink.insertAdjacentElement('afterend', copyButton);
            }
        }
    }

    // ============================================================
    // GARSONIERA - wyszukiwanie
    // ============================================================

    function normalizeGarsoCacheSearchTerm(searchMode, searchTerm) {
        if (searchMode === 'phone') {
            return normalizeEscortCachedPhone(searchTerm) || digitsOnly(searchTerm);
        }

        if (searchMode === 'address') {
            const match = String(searchTerm || '').match(/anons\s*\/\s*(\d+)\.html/i);
            if (match) return match[1];
        }

        return normalizeText(searchTerm);
    }

    function garsoSearchCountCacheKey(searchMode, searchTerm) {
        if (searchMode !== 'phone' && searchMode !== 'address') return null;
        const normalizedTerm = normalizeGarsoCacheSearchTerm(searchMode, searchTerm);
        if (!normalizedTerm) return null;

        return 'vm_garso_search_count_' +
            `${searchMode}_${encodeURIComponent(normalizedTerm)}`;
    }

    function getGarsoSearchCountCache(searchMode, searchTerm) {
        if (!SETTINGS.usePersistentCache) return null;

        const key = garsoSearchCountCacheKey(searchMode, searchTerm);
        if (!key) return null;

        try {
            const value = readPersistentCacheValue(key, null);
            const count = Number(value?.count);
            const checkedAt = Number(value?.checkedAt);
            if (!Number.isFinite(count) || count < 0 || !Number.isFinite(checkedAt) || checkedAt <= 0) {
                return null;
            }

            return {
                count,
                checkedAt,
                fresh: Date.now() - checkedAt < getGarsoCacheTtlMs()
            };
        } catch (error) {
            log('Błąd odczytu wyniku Garsoniery z cache', error);
            return null;
        }
    }

    function setGarsoSearchCountCache(searchMode, searchTerm, count) {
        if (!SETTINGS.usePersistentCache) return null;

        const key = garsoSearchCountCacheKey(searchMode, searchTerm);
        const normalizedCount = Number(count);
        if (!key || !Number.isFinite(normalizedCount) || normalizedCount < 0) return null;

        try {
            const checkedAt = Date.now();
            writePersistentCacheValue(key, {
                count: normalizedCount,
                checkedAt
            });
            return checkedAt;
        } catch (error) {
            log('Błąd zapisu wyniku Garsoniery do cache', error);
            return null;
        }
    }

    function garsoSummaryCacheKey(searchMode, searchTerm) {
        if (!['phone', 'address', 'combined'].includes(searchMode)) return null;
        const normalizedTerm = normalizeGarsoCacheSearchTerm(searchMode, searchTerm);
        if (!normalizedTerm) return null;

        return 'vm_garso_summary_' +
            `${searchMode}_${encodeURIComponent(normalizedTerm)}`;
    }

    function getGarsoTopicsSignature(topics) {
        return (topics || [])
            .filter(topic => topic?.url)
            .map(topic => normalizeTopicUrl(topic.url))
            .filter(Boolean)
            .sort()
            .join('|');
    }

    function getGarsoTopicDeduplicationKey(topicUrl) {
        if (!topicUrl) return '';
        const normalizedUrl = normalizeTopicUrl(topicUrl);
        if (!normalizedUrl) return '';

        try {
            const url = new URL(normalizedUrl, GARSO_BASE_URL);
            const pathId = url.pathname.match(/\/topic\/(\d+)(?:[-/]|$)/i)?.[1];
            const queryId = url.searchParams.get('showtopic');
            const topicId = pathId || (/^\d+$/.test(queryId || '') ? queryId : '');
            if (topicId) return `topic:${topicId}`;
            return `url:${url.href.replace(/\/$/, '')}`;
        } catch (_) {
            return `url:${normalizedUrl.replace(/\/$/, '')}`;
        }
    }

    function mergeUniqueGarsoTopics(...topicGroups) {
        const topicsByKey = new Map();

        for (const topic of topicGroups.flat()) {
            if (!topic?.url) continue;
            const url = normalizeTopicUrl(topic.url);
            if (!url) continue;
            const deduplicationKey = getGarsoTopicDeduplicationKey(url);
            if (!deduplicationKey) continue;

            const current = topicsByKey.get(deduplicationKey);
            const candidate = {
                title: normalizeEscortAdText(topic?.title),
                subtitle: normalizeEscortAdText(topic?.subtitle),
                url
            };
            if (!current) {
                topicsByKey.set(deduplicationKey, candidate);
                continue;
            }

            if (candidate.title.length > current.title.length) {
                current.title = candidate.title;
            }
            if (candidate.subtitle.length > current.subtitle.length) {
                current.subtitle = candidate.subtitle;
            }
        }

        return [...topicsByKey.values()];
    }

    function buildCombinedGarsoSummaryTerm(phoneTerm, addressTerm) {
        const phoneKey = normalizeGarsoCacheSearchTerm('phone', phoneTerm);
        const addressKey = normalizeGarsoCacheSearchTerm('address', addressTerm);
        return `${phoneKey || 'bez-numeru'}|${addressKey || 'bez-anonsu'}`;
    }

    // Do IndexedDB trafia gotowy, kompaktowy wynik analizy. Nie zapisujemy
    // pełnej treści postów ani tablicy recenzji z kopiami postów. Zachowujemy
    // wyliczone statystyki, informacje o tematach oraz tylko krótkie fragmenty
    // uzasadniające zliczenie widocznych słów kluczowych.
    function compactGarsoSummaryResult(result) {
        const sourceStats = result?.stats && typeof result.stats === 'object'
            ? result.stats
            : {};
        const { reviews: _unusedReviews, ...stats } = sourceStats;
        const topics = (Array.isArray(result?.topics) ? result.topics : [])
            .map(topic => ({
                title: normalizeEscortAdText(topic?.title),
                subtitle: normalizeEscortAdText(topic?.subtitle),
                url: topic?.url ? normalizeTopicUrl(topic.url) : ''
            }))
            .filter(topic => topic.url);
        const topicInfo = (Array.isArray(result?.topicInfo) ? result.topicInfo : [])
            .map(info => ({
                title: normalizeEscortAdText(info?.title),
                subtitle: normalizeEscortAdText(info?.subtitle),
                url: info?.url ? normalizeTopicUrl(info.url) : '',
                maxPage: Number.isFinite(Number(info?.maxPage))
                    ? Number(info.maxPage)
                    : null,
                pagesRead: (Array.isArray(info?.pagesRead) ? info.pagesRead : [])
                    .map(Number)
                    .filter(page => Number.isFinite(page) && page > 0),
                posts: Math.max(0, Number(info?.posts) || 0),
                error: info?.error ? String(info.error) : null
            }));
        const excludedTopics = (Array.isArray(result?.excludedTopics)
            ? result.excludedTopics
            : [])
            .map(topic => ({
                title: normalizeEscortAdText(topic?.title),
                url: topic?.url ? normalizeTopicUrl(topic.url) : ''
            }))
            .filter(topic => topic.url);
        const screenedTopicCount = Number.isFinite(Number(result?.screenedTopicCount))
            ? Math.max(0, Number(result.screenedTopicCount))
            : topicInfo.length;
        const excludedTopicCount = Math.max(
            excludedTopics.length,
            Number.isFinite(Number(result?.excludedTopicCount))
                ? Math.max(0, Number(result.excludedTopicCount))
                : 0
        );
        const failedTopicCount = Number.isFinite(Number(result?.failedTopicCount))
            ? Math.max(0, Number(result.failedTopicCount))
            : 0;

        return {
            searchTerm: String(result?.searchTerm || ''),
            topics,
            topicInfo,
            excludedTopics,
            screenedTopicCount,
            excludedTopicCount,
            failedTopicCount,
            postCount: Number.isFinite(Number(result?.postCount))
                ? Math.max(0, Number(result.postCount))
                : Math.max(0, Number(result?.allPosts?.length) || 0),
            stats
        };
    }

    function isCompleteGarsoSummary(result, expectedTopics = []) {
        const info = Array.isArray(result?.topicInfo) ? result.topicInfo : [];
        const expectedCount = Array.isArray(expectedTopics)
            ? expectedTopics.length
            : 0;
        const screenedCount = Number(result?.screenedTopicCount);
        const failedCount = Number(result?.failedTopicCount);
        return Number.isFinite(screenedCount) &&
            screenedCount >= expectedCount &&
            (!Number.isFinite(failedCount) || failedCount === 0) &&
            info.every(topic => !topic?.error && topic?.pagesRead?.length);
    }

    function getGarsoSummaryCache(searchMode, searchTerm) {
        if (!SETTINGS.usePersistentCache) return null;
        const key = garsoSummaryCacheKey(searchMode, searchTerm);
        if (!key) return null;

        try {
            const value = readPersistentCacheValue(key, null);
            const checkedAt = Number(value?.checkedAt);
            if (!Number.isFinite(checkedAt) || checkedAt <= 0) return null;
            if (!value?.summary?.stats || !Array.isArray(value.summary.topicInfo)) {
                return null;
            }
            const result = compactGarsoSummaryResult(value.summary);
            return {
                checkedAt,
                fresh: Date.now() - checkedAt < getGarsoCacheTtlMs(),
                topicSignature: String(value.topicSignature || getGarsoTopicsSignature(result.topics)),
                result
            };
        } catch (error) {
            log('Błąd odczytu analizy Garso z cache', error);
            return null;
        }
    }

    function setGarsoSummaryCache(searchMode, searchTerm, topics, result) {
        if (!SETTINGS.usePersistentCache) return;
        const key = garsoSummaryCacheKey(searchMode, searchTerm);
        if (!key || !result?.stats) return;

        try {
            const checkedAt = Date.now();
            const summary = compactGarsoSummaryResult(result);
            writePersistentCacheValue(key, {
                checkedAt,
                topicSignature: getGarsoTopicsSignature(topics || summary.topics),
                summary
            });
            return checkedAt;
        } catch (error) {
            log('Błąd zapisu analizy Garso do cache', error);
            return null;
        }
    }

    function deleteGarsoSummaryCache(searchMode, searchTerm) {
        const key = garsoSummaryCacheKey(searchMode, searchTerm);
        if (key) deletePersistentCacheValue(key);
    }

    function garsoExtendedCacheKey(adId) {
        const normalizedAdId = String(adId || '').match(/^\d+$/)?.[0];
        return normalizedAdId
            ? `vm_garso_extended_${normalizedAdId}`
            : null;
    }

    function normalizeGarsoExtendedSearchMode(value) {
        return ['phone', 'address', 'combined'].includes(value)
            ? value
            : 'address';
    }

    function getGarsoExtendedSearchModeLabel(value) {
        return {
            phone: 'nr tel.',
            address: 'adres',
            combined: 'nr tel. + adres'
        }[normalizeGarsoExtendedSearchMode(value)];
    }

    function getGarsoExtendedCache(adId) {
        if (!SETTINGS.usePersistentCache) return null;
        const key = garsoExtendedCacheKey(adId);
        if (!key) return null;

        try {
            const value = readPersistentCacheValue(key, null);
            if (!value || typeof value !== 'object') return null;
            const checkedAt = Number(value?.checkedAt);
            if (!Number.isFinite(checkedAt) || checkedAt <= 0) return null;
            const searchMode = normalizeGarsoExtendedSearchMode(
                value?.searchMode
            );

            const adUrls = [...new Set(
                (Array.isArray(value?.adUrls) ? value.adUrls : [])
                    .map(url => normalizeEscortiAdUrl(url))
                    .filter(Boolean)
            )];
            const results = (Array.isArray(value?.results) ? value.results : [])
                .filter(result => result?.searchTerm && Number(result?.count) > 0)
                .map(result => ({
                    status: 'ok',
                    count: Number(result.count),
                    searchTerm: String(result.searchTerm),
                    adUrl: normalizeEscortiAdUrl(result.adUrl) || null,
                    signature: String(result.signature || '')
                }));

            return {
                checkedAt,
                fresh: Date.now() - checkedAt < getGarsoCacheTtlMs(),
                adUrls,
                results,
                searchMode
            };
        } catch (error) {
            log('Błąd odczytu rozszerzonego wyniku Garso z cache', error);
            return null;
        }
    }

    function setGarsoExtendedCache(
        adId,
        adUrls,
        results,
        searchMode = 'address'
    ) {
        if (!SETTINGS.usePersistentCache) return;
        const key = garsoExtendedCacheKey(adId);
        if (!key) return;

        const mode = normalizeGarsoExtendedSearchMode(searchMode);
        const normalizedAdUrls = [...new Set(
            (adUrls || []).map(url => normalizeEscortiAdUrl(url)).filter(Boolean)
        )];
        const normalizedResults = (results || [])
            .filter(result => result?.status === 'ok' && Number(result?.count) > 0)
            .map(result => ({
                count: Number(result.count),
                searchTerm: String(result.searchTerm || ''),
                adUrl: normalizeEscortiAdUrl(result.adUrl) || null,
                signature: getGarsoExtendedResultSignature(result)
            }))
            .filter(result => result.searchTerm);

        try {
            const checkedAt = Date.now();
            writePersistentCacheValue(key, {
                checkedAt,
                adUrls: normalizedAdUrls,
                searchMode: mode,
                results: normalizedResults
            });
        } catch (error) {
            log('Błąd zapisu rozszerzonego wyniku Garso do cache', error);
        }
    }

    function getGarsoAuth(cancelToken = null) {
        if (garsoAuthPromise) {
            return garsoAuthPromise.then(auth => {
                cancelToken?.throwIfCancelled();
                return auth;
            });
        }
        garsoAuthPromise = garsoTopicRequest({
            method: 'GET',
            url: GARSO_BASE_URL,
            cancelToken
        }).then(response => {
            if (response.status && (response.status < 200 || response.status >= 400)) {
                throw new Error(`Garso: HTTP ${response.status}`);
            }
            const html = response.responseText || '';
            const hm = html.match(/ipb\.vars\['secure_hash'\]\s*=\s*['"]([^'"]+)['"]/)
                || html.match(/name=["']secure_hash["']\s+value=["']([^"']+)["']/);
            const sm = html.match(/ipb\.vars\['session_id'\]\s*=\s*['"]([^'"]+)['"]/);
            if (!hm || !sm) {
                recordParserIssue(
                    'Garso - dane sesji',
                    'Nie znaleziono secure_hash lub session_id.'
                );
                throw new Error('Brak danych sesji Garso');
            }
            return { secureHash: hm[1], sessionId: sm[1] };
        });
        garsoAuthPromise.catch(() => { garsoAuthPromise = null; });
        return garsoAuthPromise;
    }

    function isGarsoAntifloodResponse(html) {
        const doc = new DOMParser().parseFromString(String(html || ''), 'text/html');
        const text = normalizeText(doc.body?.innerText || html);
        return /anty\s*-?\s*flood|anti\s*-?\s*flood|flood control/.test(text)
            || /(?:odczekaj|poczekaj|odczekać|zaczekaj).{0,80}(?:sekund|sek\.)/.test(text);
    }

    function normalizeGarsoResultPageUrl(value) {
        try {
            const url = new URL(value || '', GARSO_BASE_URL);
            if (!['garsoniera.com.pl', 'www.garsoniera.com.pl'].includes(
                url.hostname.toLowerCase()
            )) return null;
            return url.href;
        } catch (_) {
            return null;
        }
    }

    async function checkGarsoTerm(
        term,
        collectTopics = true,
        cancelToken = null
    ) {
        try {
            cancelToken?.throwIfCancelled();
            const auth = await getGarsoAuth(cancelToken);
            const targetUrl = `${GARSO_BASE_URL}index.php?app=core&module=search&do=search&fromMainBar=1&s=${encodeURIComponent(auth.sessionId)}`;
            const params = new URLSearchParams();
            params.set('search_term', term);
            params.set('search_app', 'forums');
            params.set('secure_hash', auth.secureHash);
            params.set('submit', 'Szukaj');

            const response = await garsoRequest({
                method: 'POST',
                url: targetUrl,
                cancelToken,
                headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                data: params.toString()
            });
            if (response.status && (response.status < 200 || response.status >= 400)) {
                throw new Error(`Garso: HTTP ${response.status}`);
            }
            const html = response.responseText || '';
            if (isGarsoAntifloodResponse(html)) {
                return {
                    status: 'antiflood',
                    count: null,
                    topics: [],
                    resultUrl: null
                };
            }

            const result = analyzeGarsoResults(html);
            result.resultUrl = normalizeGarsoResultPageUrl(response.finalUrl);
            if (
                collectTopics &&
                result.status === 'ok' &&
                result.count > result.topics.length &&
                result.topics.length > 0
            ) {
                result.topics = await collectAllGarsoSearchTopics(
                    html,
                    result,
                    cancelToken
                );
            }
            return result;
        } catch (error) {
            if (isOperationCancelledError(error)) throw error;
            if (error?.code === 'GARSO_ANTIFLOOD') {
                return {
                    status: 'antiflood',
                    count: null,
                    topics: [],
                    resultUrl: null
                };
            }
            log('Błąd sprawdzania Garso:', error);
            return { status: 'error', count: null, topics: [], resultUrl: null };
        }
    }

    function normalizeTopicUrl(href) {
        try {
            const u = new URL(href, GARSO_BASE_URL);
            u.searchParams.delete('hl');
            u.searchParams.delete('findpost');
            u.searchParams.delete('view');
            u.searchParams.delete('p');
            u.hash = '';
            return u.href;
        } catch (_) { return href; }
    }

    function getDisplayableGarsoTopicSubtitle(value) {
        const subtitle = normalizeEscortAdText(value);
        return subtitle && !subtitle.includes('?') ? subtitle : '';
    }

    function extractGarsoTopicSubtitle(doc) {
        const subtitle = doc.querySelector(
            'h1.ipsType_pagetitle > span.desc[style*="font-size"], ' +
            'h1[itemprop="name"] > span.desc[style*="font-size"]'
        );
        return normalizeEscortAdText(subtitle?.textContent);
    }

    function extractSearchTopicSubtitle(anchor) {
        const heading = anchor.closest('h1,h2,h3,h4,h5,h6');
        const subtitle = heading?.querySelector('span.desc[style*="font-size"]');
        return normalizeEscortAdText(subtitle?.textContent);
    }

    function extractSearchTopics(doc) {
        const out = [];
        const seen = new Set();
        for (const a of doc.querySelectorAll(
            '#forum_table a[href*="/topic/"], ' +
            '#forum_table a[href*="showtopic"], ' +
            '#forum_table a[title="Pokaż wyniki"]'
        )) {
            const url = normalizeTopicUrl(a.getAttribute('href'));
            if (!url || seen.has(url)) continue;
            seen.add(url);
            out.push({
                title: normalizeEscortAdText(a.textContent),
                subtitle: extractSearchTopicSubtitle(a),
                url
            });
        }
        return out;
    }

    function analyzeGarsoResults(html) {
        if (!html) {
            recordParserIssue(
                'Garso - wyniki wyszukiwania',
                'Otrzymano pusty dokument wyników.'
            );
            return { status: 'unknown', count: null, topics: [] };
        }
        const doc = new DOMParser().parseFromString(html, 'text/html');
        const topics = extractSearchTopics(doc);

        for (const d of doc.querySelectorAll('.ipsType_pagedesc')) {
            const text = (d.textContent || '').replace(/\s+/g, ' ').trim();
            const m = text.match(/zwróciła\s+następującą\s+liczbę\s+wyników\s*:\s*(\d+)/i);
            if (m) {
                const count = Number(m[1]);
                return { status: 'ok', found: count > 0, count, topics };
            }
        }

        const rows = [...doc.querySelectorAll('#forum_table tr._recordRow')];
        if (rows.length) return { status: 'ok', found: true, count: rows.length, topics };

        const scripts = html.match(/ipb\.global\.searchResults\s*\[\s*\d+\s*\]\s*=/g);
        if (scripts?.length) return { status: 'ok', found: true, count: scripts.length, topics };

        const bodyText = normalizeText(doc.body ? doc.body.innerText : html);
        const noResultPhrases = [
            'nie znaleziono wyników', 'nie znaleziono żadnych wyników', 'brak wyników',
            'brak rezultatów', 'wyszukiwanie nie zwróciło żadnych wyników',
            'twoje wyszukiwanie nie zwróciło żadnych wyników'
        ];
        if (noResultPhrases.some(p => bodyText.includes(p))) return { status: 'ok', found: false, count: 0, topics: [] };
        recordParserIssue(
            'Garso - wyniki wyszukiwania',
            `Nie rozpoznano licznika wyników; znalezione linki tematów: ${topics.length}.`
        );
        return { status: 'unknown', count: null, topics };
    }


    async function collectAllGarsoSearchTopics(
        firstHtml,
        result,
        cancelToken = null
    ) {
        const firstDoc = parseHtml(firstHtml);
        const seen = new Map(result.topics.map(t => [t.url, t]));
        const pageLinks = [...firstDoc.querySelectorAll('.pagination a[href], ul.pagination a[href], .pages a[href]')]
            .map(a => ({ page: parsePageNumber(a), href: a.getAttribute('href') }))
            .filter(x => x.page && x.page > 1 && x.href);

        if (!pageLinks.length) return [...seen.values()];
        const template = pageLinks[0];
        const perPage = result.topics.length;
        const maxPage = Math.ceil(result.count / perPage);

        for (let p = 2; p <= maxPage; p++) {
            cancelToken?.throwIfCancelled();
            let url = null;
            const direct = pageLinks.find(x => x.page === p);
            if (direct) {
                try { url = new URL(direct.href, GARSO_BASE_URL).href; } catch (_) {}
            }
            if (!url) {
                let templateAbs = null;
                try { templateAbs = new URL(template.href, GARSO_BASE_URL).href; } catch (_) {}
                if (templateAbs) url = constructPageUrl(templateAbs, p, template.page);
            }
            if (!url) break;
            try {
                const html = await gmGetText(
                    url,
                    cancelToken,
                    { searchRequest: true }
                );
                for (const t of extractSearchTopics(parseHtml(html))) seen.set(t.url, t);
            } catch (e) {
                if (isOperationCancelledError(e)) throw e;
                log(`Nie udało się pobrać strony ${p} wyników Garso`, e);
            }
        }
        return [...seen.values()];
    }

    function getGarsoTopicWord(number) {
        if (number === 1) return 'temat';
        const last = number % 10, lastTwo = number % 100;
        if (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)) return 'tematy';
        return 'tematów';
    }

    function setTwoLineButton(btn, title, result) {
        btn._vmResponsiveValueObserver?.disconnect();
        btn._vmResponsiveValueObserver = null;
        btn.replaceChildren();

        const titleLine = makeElement('span', '', title);
        Object.assign(titleLine.style, {
            display: 'block',
            fontWeight: '700',
            lineHeight: '1.2'
        });

        const resultLine = makeElement('span', '', result);
        Object.assign(resultLine.style, {
            display: 'block',
            marginTop: '2px',
            fontWeight: '600',
            fontSize: '0.92em',
            lineHeight: '1.2'
        });

        if (btn.classList.contains('vm-research-panel-row')) {
            titleLine.className = 'vm-research-panel-label';
            resultLine.className = 'vm-research-panel-value';
            Object.assign(titleLine.style, {
                minWidth: '0',
                marginRight: '12px',
                overflow: 'hidden',
                textOverflow: 'ellipsis',
                whiteSpace: 'nowrap',
                textAlign: 'left'
            });
            Object.assign(resultLine.style, {
                flex: '0 0 auto',
                marginTop: '0',
                color: {
                    checking: '#d8c8d9',
                    found: '#62cf7b',
                    empty: '#80b9ee',
                    error: '#ff6262'
                }[btn.dataset.vmResultStatus] || '#d8c8d9',
                textAlign: 'right',
                whiteSpace: 'nowrap'
            });

            const arrow = makeElement('span', '', '›');
            arrow.setAttribute('aria-hidden', 'true');
            Object.assign(arrow.style, {
                flex: '0 0 auto',
                marginLeft: '9px',
                color: getEscortPagePinkColor(),
                fontSize: '20px',
                fontWeight: '400',
                lineHeight: '1'
            });
            btn.append(titleLine, resultLine, arrow);

            const fullResult = String(result ?? '');
            const compactResult = fullResult.replace(
                /^(\d+) (?:anons|anonse|anonsów|ogłoszenia) \((\d+) (?:aktywne|aktywnych)\)$/,
                '$1 ogł. ($2 akt.)'
            );
            if (compactResult !== fullResult) {
                const renderResponsiveResult = () => {
                    resultLine.textContent = fullResult;
                    const labelIsClipped =
                        titleLine.scrollWidth > titleLine.clientWidth + 1;
                    const rowOverflows = btn.scrollWidth > btn.clientWidth + 1;
                    if (labelIsClipped || rowOverflows) {
                        resultLine.textContent = compactResult;
                    }
                };
                if (typeof ResizeObserver === 'function') {
                    btn._vmResponsiveValueObserver = new ResizeObserver(
                        renderResponsiveResult
                    );
                    btn._vmResponsiveValueObserver.observe(btn);
                }
                requestAnimationFrame(renderResponsiveResult);
            }
            return;
        }

        btn.appendChild(titleLine);
        btn.appendChild(resultLine);
    }

    function setGarsoExtendedCheckButton(btn, result, title = 'Sprawdzanie') {
        const resultText = String(result ?? '');
        setTwoLineButton(btn, title, resultText);

        const resultLine = btn.children[1];
        if (!resultLine) return;
        const foundMatch = resultText.match(
            /(?:\bznaleziono|\bwyniki\s+dla)\s+(\d+)/i
        );
        if (!foundMatch) return;

        const foundCount = Number(foundMatch[1]) || 0;
        const foundBadge = makeElement('span', '', `Znaleziono ${foundCount}`);
        Object.assign(foundBadge.style, {
            display: 'inline-flex',
            alignItems: 'center',
            margin: '1px 3px',
            padding: '2px 6px',
            border: `1px solid ${foundCount > 0
                ? 'rgba(98,207,123,.82)'
                : 'rgba(255,190,92,.78)'}`,
            borderRadius: '999px',
            background: foundCount > 0
                ? 'rgba(98,207,123,.16)'
                : 'rgba(255,190,92,.13)',
            color: foundCount > 0 ? '#8df0a4' : '#ffd08a',
            fontWeight: '900',
            lineHeight: '1.15',
            whiteSpace: 'nowrap',
            boxShadow: '0 0 0 1px rgba(0,0,0,.08) inset'
        });

        const before = resultText.slice(0, foundMatch.index);
        const after = resultText.slice(foundMatch.index + foundMatch[0].length);
        resultLine.replaceChildren();
        if (before) resultLine.appendChild(document.createTextNode(before));
        resultLine.appendChild(foundBadge);
        if (after) resultLine.appendChild(document.createTextNode(after));
    }

    function renderGarsoButton(btn) {
        const displayTerm = btn.dataset.garsoDisplayTerm || '';
        const status = btn.dataset.garsoStatus || 'checking';
        let statusText = 'sprawdzam...';

        if (status === 'result') {
            const count = Number(btn.dataset.garsoCount || 0);
            statusText = `${count} ${getGarsoTopicWord(count)}`;
            setButtonColor(btn, count > 0 ? 'found' : 'empty');
        } else if (status === 'refreshing') {
            const count = Number(btn.dataset.garsoCount || 0);
            statusText = `${count} ${getGarsoTopicWord(count)} • odświeżanie…`;
            setButtonColor(btn, 'checking');
        } else if (status === 'unknown') {
            statusText = '?'; setButtonColor(btn, 'checking');
        } else if (status === 'error') {
            statusText = 'błąd'; setButtonColor(btn, 'error');
        } else if (status === 'manual') {
            statusText = 'kliknij'; setButtonColor(btn, 'checking');
        } else setButtonColor(btn, 'checking');

        if (btn.dataset.garsoSummaryState === 'ready') {
            const reviewCount = Math.max(
                0,
                Number(btn.dataset.garsoReviewCount) || 0
            );
            if (reviewCount > 0) {
                statusText += btn.classList.contains('vm-research-panel-row')
                    ? ' • są oceny'
                    : ' • oceny';
            }
        }
        else if (['loading', 'refreshing'].includes(btn.dataset.garsoSummaryState)) {
            statusText += ' • sprawdzanie';
        }

        if (btn.dataset.garsoCacheState === 'stale') {
            statusText += ' • nie odświeżono';
            btn.title = 'Pokazuję wcześniejszą liczbę tematów, ponieważ odświeżenie nie powiodło się.';
        } else if (btn.dataset.garsoCacheState === 'fresh') {
            btn.title = 'Liczba tematów pochodzi z aktualnego cache Garsoniery.';
        } else {
            btn.removeAttribute('title');
        }

        const title = btn.classList.contains('vm-research-panel-row')
            ? `Garso - ${displayTerm}`
            : `Szukaj Garso: ${displayTerm}`;
        setTwoLineButton(btn, title, statusText);
    }

    async function updateGarsoStatus(
        btn,
        searchTerm,
        searchMode,
        forceRefresh = false,
        analyzeContent = true,
        prepareContentSummary = true,
        cancelToken = null
    ) {
        cancelToken?.throwIfCancelled();
        const cached = getGarsoSearchCountCache(searchMode, searchTerm);
        const shouldAnalyzeContent = analyzeContent !== false;
        const shouldPrepareContentSummary =
            shouldAnalyzeContent && prepareContentSummary !== false;

        if (!forceRefresh && cached?.fresh) {
            btn.dataset.garsoStatus = 'result';
            btn.dataset.garsoCount = String(cached.count);
            btn.dataset.garsoCacheState = 'fresh';
            btn.dataset.garsoCountCheckedAt = String(cached.checkedAt);
            if (!btn.dataset.garsoSummaryState) {
                const restored = restoreGarsoSummaryFromCache(
                    btn,
                    searchTerm,
                    searchMode
                );
                if (!restored && Number(cached.count) === 0) {
                    setGarsoSummaryState(btn, 'empty', '', {
                        checkedAt: cached.checkedAt,
                        cacheState: 'fresh'
                    });
                } else if (
                    !restored &&
                    !shouldAnalyzeContent &&
                    Number(cached.count) > 0
                ) {
                    setGarsoSummaryState(btn, 'count-only', '', {
                        checkedAt: cached.checkedAt,
                        cacheState: 'fresh'
                    });
                }
            }
            renderGarsoButton(btn);
            emitGarsoCountState(btn, 'ready', cached.count, true);
            const summaryState = btn.dataset.garsoSummaryState || '';
            const summaryCacheState = btn.dataset.garsoSummaryCacheState || '';
            const needsContentRefresh =
                shouldPrepareContentSummary &&
                Number(cached.count) > 0 &&
                (
                    summaryState !== 'ready' ||
                    ['stale', 'partial'].includes(summaryCacheState)
                );
            if (!needsContentRefresh) return;
        }

        if (cached) {
            btn.dataset.garsoStatus = 'refreshing';
            btn.dataset.garsoCount = String(cached.count);
            btn.dataset.garsoCacheState = 'refreshing';
            btn.dataset.garsoCountCheckedAt = String(cached.checkedAt);
        } else {
            btn.dataset.garsoStatus = 'checking';
            delete btn.dataset.garsoCacheState;
            delete btn.dataset.garsoCountCheckedAt;
        }
        renderGarsoButton(btn);
        const result = await checkGarsoTerm(
            searchTerm,
            shouldAnalyzeContent,
            cancelToken
        );

        if (['error', 'unknown', 'antiflood'].includes(result.status)) {
            if (cached) {
                btn.dataset.garsoStatus = 'result';
                btn.dataset.garsoCount = String(cached.count);
                btn.dataset.garsoCacheState = 'stale';
            } else {
                btn.dataset.garsoStatus = result.status === 'antiflood'
                    ? 'unknown'
                    : result.status;
                delete btn.dataset.garsoCacheState;
            }
        }
        else {
            btn.dataset.garsoStatus = 'result';
            btn.dataset.garsoCount = String(result.count ?? 0);
            delete btn.dataset.garsoCacheState;
            const countCheckedAt = setGarsoSearchCountCache(
                searchMode,
                searchTerm,
                result.count ?? 0
            );
            if (countCheckedAt) {
                btn.dataset.garsoCountCheckedAt = String(countCheckedAt);
            }
        }
        renderGarsoButton(btn);
        emitGarsoCountState(
            btn,
            result.status === 'ok' ? 'ready' : (cached ? 'stale' : 'error'),
            result.status === 'ok' ? (result.count ?? 0) : (cached?.count ?? null),
            result.status !== 'ok' && !!cached
        );

        if (
            shouldPrepareContentSummary &&
            result.status === 'ok' &&
            result.count > 0 &&
            result.topics?.length
        ) {
            prepareGarsoSummary(
                btn,
                searchTerm,
                result.topics,
                forceRefresh,
                cancelToken
            );
        } else if (
            !shouldPrepareContentSummary &&
            result.status === 'ok' &&
            result.count > 0 &&
            btn.dataset.garsoSummaryState !== 'ready'
        ) {
            setGarsoSummaryState(btn, 'count-only', '', {
                checkedAt: Number(btn.dataset.garsoCountCheckedAt) || Date.now(),
                cacheState: 'fresh'
            });
        } else if (result.status === 'ok' && Number(result.count) === 0) {
            deleteGarsoSummaryCache(searchMode, searchTerm);
            setGarsoSummaryState(btn, 'empty', '');
        }
        return result;
    }

    async function startSearchProcess(term, btnElement) {
        const title = btnElement.classList.contains('vm-research-panel-row')
            ? `Garso - ${btnElement.dataset.garsoDisplayTerm || ''}`
            : `Szukaj Garso: ${btnElement.dataset.garsoDisplayTerm || ''}`;
        setTwoLineButton(btnElement, title, 'Autoryzacja...');
        setButtonColor(btnElement, 'checking');
        btnElement.disabled = true;
        try {
            const auth = await getGarsoAuth();
            submitRealForm(term, auth.secureHash, auth.sessionId, btnElement);
        } catch (error) {
            setTwoLineButton(btnElement, title, 'Błąd sesji');
            setButtonColor(btnElement, 'error');
            btnElement.disabled = false;
            setTimeout(() => renderGarsoButton(btnElement), 2000);
        }
    }

    function submitGarsoSearchFormToNewTab(
        term,
        hash,
        sessionId,
        { active = true } = {}
    ) {
        const token = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
        const storageKey = `${GARSO_OPEN_BRIDGE_KEY_PREFIX}${token}`;
        GM_setValue(storageKey, {
            term,
            secureHash: hash,
            sessionId,
            createdAt: Date.now()
        });

        const bridgeUrl = new URL(
            'index.php?app=core&module=search&search_in=forums',
            GARSO_BASE_URL
        );
        bridgeUrl.searchParams.set(GARSO_OPEN_BRIDGE_PARAM, token);

        try {
            GM_openInTab(bridgeUrl.href, {
                active,
                insert: true,
                setParent: true
            });
        } catch (error) {
            GM_deleteValue(storageKey);
            throw error;
        }

        // Jeżeli użytkownik zamknie kartę przed uruchomieniem mostu, usuń
        // niepotrzebny jednorazowy wpis.
        setTimeout(() => GM_deleteValue(storageKey), 5 * 60 * 1000);
    }

    function submitRealForm(term, hash, sessionId, btnElement) {
        const title = btnElement.classList.contains('vm-research-panel-row')
            ? `Garso - ${btnElement.dataset.garsoDisplayTerm || ''}`
            : `Szukaj Garso: ${btnElement.dataset.garsoDisplayTerm || ''}`;
        setTwoLineButton(btnElement, title, 'Otwieranie...');
        setButtonColor(btnElement, 'checking');
        submitGarsoSearchFormToNewTab(term, hash, sessionId);
        setTimeout(() => {
            btnElement.disabled = false;
            renderGarsoButton(btnElement);
        }, 800);
    }

    // ============================================================
    // GARSO - pobieranie tematów i stron
    // ============================================================

    function parsePageNumber(a) {
        const text = (a.textContent || '').trim();
        if (/^\d+$/.test(text)) return Number(text);
        const href = a.getAttribute('href') || '';
        let m = href.match(/\/page-(\d+)\/?/i);
        if (m) return Number(m[1]);
        m = href.match(/[?&]page=(\d+)/i);
        if (m) return Number(m[1]);
        return null;
    }

    function getPaginationMap(doc, baseUrl) {
        const map = new Map([[1, baseUrl]]);
        let baseTopicId = null;
        try { baseTopicId = new URL(baseUrl).pathname.match(/\/topic\/(\d+)/i)?.[1] || null; } catch (_) {}
        for (const a of doc.querySelectorAll('.pagination a[href], ul.pagination a[href], .pages a[href], a[href*="page-"]')) {
            const n = parsePageNumber(a);
            if (!n || n < 1) continue;
            try {
                const url = new URL(a.getAttribute('href'), baseUrl);
                const candidateTopicId = url.pathname.match(/\/topic\/(\d+)/i)?.[1] || null;
                if (baseTopicId && candidateTopicId !== baseTopicId) continue;
                map.set(n, url.href);
            } catch (_) {}
        }
        return map;
    }

    function getMaxPage(doc, map) {
        let maxPage = Math.max(...map.keys());
        const text = [...doc.querySelectorAll('.pagination,.pages')].map(el => el.textContent || '').join(' ');
        const m = text.match(/(?:strona|page)\s*\d+\s*(?:z|of)\s*(\d+)/i);
        if (m) maxPage = Math.max(maxPage, Number(m[1]));
        return Number.isFinite(maxPage) && maxPage > 0 ? maxPage : 1;
    }

    function constructPageUrl(templateUrl, pageNo, knownPageNo = null) {
        try {
            const u = new URL(templateUrl);
            if (/\/page-\d+\/?/i.test(u.pathname)) {
                u.pathname = u.pathname.replace(/\/page-\d+\/?/i, `/page-${pageNo}/`);
                return u.href;
            }
            if (u.searchParams.has('page')) {
                u.searchParams.set('page', String(pageNo));
                return u.href;
            }
            if (u.searchParams.has('st') && knownPageNo && knownPageNo > 1) {
                const current = Number(u.searchParams.get('st'));
                const perPage = current / (knownPageNo - 1);
                if (Number.isFinite(perPage) && perPage > 0) {
                    u.searchParams.set('st', String(Math.round((pageNo - 1) * perPage)));
                    return u.href;
                }
            }
        } catch (_) {}
        return null;
    }

    function parseHtml(html) { return new DOMParser().parseFromString(html, 'text/html'); }

    function getSelectedTopicPageNumbers(maxPage) {
        if (maxPage <= GARSO_MAX_FULL_TOPIC_PAGES) {
            const pages = [];
            for (let page = 1; page <= maxPage; page++) pages.push(page);
            return pages;
        }
        const selected = new Set();
        for (let page = 1; page <= GARSO_LONG_TOPIC_FIRST_PAGES; page++) selected.add(page);
        for (let page = Math.max(1, maxPage - GARSO_LONG_TOPIC_LAST_PAGES + 1); page <= maxPage; page++) selected.add(page);
        return [...selected].sort((a, b) => a - b);
    }

    function resolveTopicPageUrl(map, baseUrl, pageNo, maxPage) {
        if (pageNo === 1) return baseUrl;
        if (map.has(pageNo)) return map.get(pageNo);

        const knownPages = [...map.keys()]
            .filter(page => page > 1)
            .sort((a, b) => Math.abs(a - pageNo) - Math.abs(b - pageNo));
        for (const knownPage of knownPages) {
            const url = constructPageUrl(map.get(knownPage), pageNo, knownPage);
            if (url) return url;
        }
        return constructPageUrl(map.get(maxPage) || baseUrl, pageNo, maxPage);
    }

    function extractGarsoTopicTitle(doc, fallback = '') {
        const heading = doc?.querySelector(
            'h1[itemprop="name"], h1.ipsType_pagetitle, #content h1'
        );
        if (!heading) return normalizeEscortAdText(fallback);

        const clone = heading.cloneNode(true);
        clone.querySelectorAll('span.desc, .desc').forEach(element => element.remove());
        return normalizeEscortAdText(clone.textContent) || normalizeEscortAdText(fallback);
    }

    function extractFirstGarsoPostLine(doc) {
        if (!doc) return '';
        const firstBlock = doc.querySelector('.post_block, [id^="post_id_"]');
        if (!firstBlock) return '';

        const content = firstBlock.querySelector(
            '.post_body .post, .post.entry-content, .post_body'
        ) || firstBlock;
        return cleanPostElement(content)
            .split('\n')
            .map(line => line.trim())
            .find(Boolean) || '';
    }

    function extractGarsoTopicForumNames(doc) {
        if (!doc) return [];
        const names = new Set();
        const selectors = [
            '.breadcrumb a[href*="/forum/"]',
            '.breadcrumb [itemprop="title"]',
            '#breadcrumb a[href*="/forum/"]',
            '#breadcrumb [itemprop="title"]',
            'ol.breadcrumb a[href*="/forum/"]',
            'ol.breadcrumb [itemprop="title"]',
            '#secondary_navigation .breadcrumb a[href*="/forum/"]',
            '#secondary_navigation .breadcrumb [itemprop="title"]',
            '.ipsBreadcrumb a[href*="/forum/"]',
            '.ipsBreadcrumb [itemprop="name"]'
        ].join(', ');

        for (const element of doc.querySelectorAll(selectors)) {
            const name = normalizeEscortAdText(element.textContent);
            if (name) names.add(name);
        }
        return [...names];
    }

    function getGarsoSummaryTopicEligibility(topic, firstDoc) {
        const title = extractGarsoTopicTitle(firstDoc, topic?.title);
        const firstPostLine = extractFirstGarsoPostLine(firstDoc);
        const forumNames = extractGarsoTopicForumNames(firstDoc);
        const formattedPhonePattern = /(?:^|\D)\d{3}-\d{3}-\d{3}(?!\d)/;

        const phoneInTitle = formattedPhonePattern.test(title);
        const labeledPhoneInFirstPost =
            /^numer\s+telefonu\s*:\s*\d{3}-\d{3}-\d{3}(?!\d)/i
                .test(firstPostLine);
        const eligibleForum = forumNames.some(name =>
            /(?:czatodajki|cichodajki)/i.test(name)
        );

        return {
            eligible: phoneInTitle || labeledPhoneInFirstPost || eligibleForum,
            title
        };
    }

    function normalizeGarsoTopicPageCacheUrl(value) {
        try {
            const url = new URL(value || '', GARSO_BASE_URL);
            url.hash = '';
            url.searchParams.delete('hl');
            url.searchParams.delete('findpost');
            return url.href;
        } catch (_) {
            return String(value || '');
        }
    }

    function garsoTopicPageCacheKey(url) {
        const normalized = normalizeGarsoTopicPageCacheUrl(url);
        return normalized
            ? `${GARSO_TOPIC_PAGE_CACHE_PREFIX}${encodeURIComponent(normalized)}`
            : '';
    }

    function getGarsoTopicPageCache(url) {
        if (!SETTINGS.usePersistentCache) return null;
        const key = garsoTopicPageCacheKey(url);
        if (!key) return null;
        const value = readPersistentCacheValue(key, null);
        const checkedAt = Number(value?.checkedAt);
        if (
            !value ||
            !Number.isFinite(checkedAt) ||
            Date.now() - checkedAt >= getGarsoCacheTtlMs() ||
            typeof value.html !== 'string' ||
            !value.html
        ) return null;
        return { html: value.html, checkedAt };
    }

    function setGarsoTopicPageCache(url, html) {
        if (!SETTINGS.usePersistentCache || !html) return;
        const key = garsoTopicPageCacheKey(url);
        if (!key) return;
        writePersistentCacheValue(key, {
            checkedAt: Date.now(),
            url: normalizeGarsoTopicPageCacheUrl(url),
            html: String(html)
        });
    }

    async function fetchGarsoTopicPageHtml(
        url,
        cancelToken = null,
        forceRefresh = false
    ) {
        cancelToken?.throwIfCancelled();
        if (!forceRefresh) {
            const cached = getGarsoTopicPageCache(url);
            if (cached) {
                return { html: cached.html, fromCache: true };
            }
        }
        const html = await gmGetText(url, cancelToken);
        cancelToken?.throwIfCancelled();
        setGarsoTopicPageCache(url, html);
        return { html, fromCache: false };
    }

    async function fetchSelectedTopicPages(
        topic,
        cancelToken = null,
        {
            forceRefresh = false,
            topicIndex = 0,
            topicTotal = 1,
            onProgress = null
        } = {}
    ) {
        cancelToken?.throwIfCancelled();
        const firstResult = await fetchGarsoTopicPageHtml(
            topic.url,
            cancelToken,
            forceRefresh
        );
        cancelToken?.throwIfCancelled();
        const firstHtml = firstResult.html;
        const firstDoc = parseHtml(firstHtml);
        const eligibility = getGarsoSummaryTopicEligibility(topic, firstDoc);
        const subtitle = topic.subtitle || extractGarsoTopicSubtitle(firstDoc);
        if (!eligibility.eligible) {
            onProgress?.({
                phase: 'topic-complete',
                topicIndex,
                topicTotal,
                page: 1,
                pageTotal: 1,
                fromCache: firstResult.fromCache,
                eligible: false
            });
            return {
                eligible: false,
                title: eligibility.title,
                subtitle,
                maxPage: null,
                pages: []
            };
        }

        const map = getPaginationMap(firstDoc, topic.url);
        const maxPage = getMaxPage(firstDoc, map);
        const selectedPageNumbers = getSelectedTopicPageNumbers(maxPage);
        const pageTotal = selectedPageNumbers.length;
        const pages = [{ page: 1, url: topic.url, html: firstHtml }];
        onProgress?.({
            phase: 'page',
            topicIndex,
            topicTotal,
            page: 1,
            pageTotal,
            fromCache: firstResult.fromCache,
            title: eligibility.title
        });

        const remaining = selectedPageNumbers
            .filter(pageNo => pageNo !== 1)
            .map(pageNo => ({
                pageNo,
                url: resolveTopicPageUrl(map, topic.url, pageNo, maxPage)
            }))
            .filter(item => item.url);

        const loadedPages = await Promise.all(remaining.map(async item => {
            cancelToken?.throwIfCancelled();
            // Przy ręcznym odświeżeniu zawsze odświeżamy pierwszą stronę
            // (paginacja/kwalifikacja) i najnowszą stronę tematu. Starsze,
            // historyczne strony mogą bezpiecznie korzystać z cache do TTL.
            const refreshThisPage = forceRefresh && item.pageNo === maxPage;
            const result = await fetchGarsoTopicPageHtml(
                item.url,
                cancelToken,
                refreshThisPage
            );
            onProgress?.({
                phase: 'page',
                topicIndex,
                topicTotal,
                page: item.pageNo,
                pageTotal,
                fromCache: result.fromCache,
                title: eligibility.title
            });
            return {
                page: item.pageNo,
                url: item.url,
                html: result.html
            };
        }));
        pages.push(...loadedPages);
        pages.sort((a, b) => a.page - b.page);
        onProgress?.({
            phase: 'topic-complete',
            topicIndex,
            topicTotal,
            page: pageTotal,
            pageTotal,
            title: eligibility.title,
            eligible: true
        });
        return {
            eligible: true,
            title: eligibility.title,
            maxPage,
            subtitle,
            pages
        };
    }

    function cleanPostElement(postEl) {
        const clone = postEl.cloneNode(true);
        clone.querySelectorAll('blockquote,.ipsBlockquote,.citation,.quote,.bbc_quote,.signature,.edit,script,style,noscript').forEach(el => el.remove());
        clone.querySelectorAll('br').forEach(br => br.replaceWith('\n'));
        clone.querySelectorAll('p,li').forEach(el => el.append('\n'));
        return (clone.textContent || '')
            .replace(/\r/g, '')
            .replace(/[^\S\n]+/g, ' ')
            .replace(/\n\s*\n+/g, '\n')
            .trim();
    }

    function extractPostsFromPage(html, topicTitle, pageNo, pageUrl = '') {
        const doc = parseHtml(html);
        let blocks = [...doc.querySelectorAll('.post_block')];
        if (!blocks.length) blocks = [...doc.querySelectorAll('[id^="post_id_"]')];
        const posts = [];

        if (!blocks.length) {
            recordParserIssue(
                'Garso - posty tematu',
                `Nie znaleziono bloków postów na stronie ${pageNo || '?'}.`
            );
        }

        for (const block of blocks) {
            const content = block.querySelector('.post_body .post, .post.entry-content, .post_body') || block;
            const text = cleanPostElement(content);
            if (!text || text.length < 15) continue;
            const author = (block.querySelector('.post_username, .author_info .name, [itemprop="name"]')?.textContent || '').replace(/\s+/g, ' ').trim();
            const dateElement = block.querySelector(
                'time[datetime], .post_date abbr[title], ' +
                '.posted_info abbr[title], .post_date, .posted_info, time'
            );
            const date = (
                dateElement?.getAttribute('datetime') ||
                dateElement?.getAttribute('title') ||
                dateElement?.textContent || ''
            ).replace(/\s+/g, ' ').trim();
            posts.push({ topicTitle, pageNo, pageUrl, author, date, text });
        }
        return posts;
    }

    // ============================================================
    // GARSO - oceny
    // ============================================================

    const RATING_NUMBER_SOURCE = '(?:10(?:[.,]0+)?|[0-9](?:[.,][0-9]+)?)';
    const categoryRatingRegexCache = new Map();

    function escapeRatingAlias(pattern) {
        return pattern
            .trim()
            .split(/\s+/)
            .map(part => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
            .join('\\s*');
    }

    function getCategoryRatingRegexes(def) {
        if (categoryRatingRegexCache.has(def.key)) return categoryRatingRegexCache.get(def.key);
        const aliases = [...new Set(def.patterns)]
            .sort((a, b) => b.length - a.length)
            .map(escapeRatingAlias)
            .join('|');
        const label = `(?:^|[^\\p{L}\\p{N}])(?:${aliases})(?=$|[^\\p{L}])`;
        const labelNearStart = `^[^\\d\\n]{0,24}?${label}`;
        const value = `(${RATING_NUMBER_SOURCE})(?:\\s*[-–—]\\s*(${RATING_NUMBER_SOURCE}))?(?![\\d.,])`;
        const regexes = {
            // Najpewniejszy wariant: ocena zakończona „/10”. Pomiędzy nazwą
            // obszaru a liczbą może wystąpić krótki opis.
            strong: new RegExp(`${labelNearStart}.{0,800}?${value}\\s*\\/\\s*10`, 'giu'),
            reversedStrong: new RegExp(`${value}\\s*\\/\\s*10\\s*(?:[-–—:;,.]\\s*)?${label}`, 'giu'),
            // Ocena bez „/10”, np. „FBG 8” albo „Atmosfera: 9”.
            direct: new RegExp(
                `${labelNearStart}\\s*(?:[:=\\-–—]\\s*)?${value}(?!\\s*(?:zł|pln|cm|kg|lat|lata|razy|x|minut(?:a|y|ę|ach|ami)?|min|godzin(?:a|y|ę|ach|ami)?|godz|dni|dzień|dzien|h)\\b)`,
                'giu'
            ),
            // Swobodniejszy zapis bez „/10”, np. „ustawka, daję 10”.
            described: new RegExp(
                `${labelNearStart}.{0,120}?(?:oceniam(?:\\s+na)?|daj[eę]|daje|mocne|solidne)\\s*${value}`,
                'giu'
            )
        };
        categoryRatingRegexCache.set(def.key, regexes);
        return regexes;
    }

    function ratingValueFromMatch(match) {
        const first = Number(String(match[1]).replace(',', '.'));
        const second = match[2] == null ? null : Number(String(match[2]).replace(',', '.'));
        if (!Number.isFinite(first) || first < 0 || first > 10) return null;
        if (second == null) return first;
        if (!Number.isFinite(second) || second < 0 || second > 10) return null;
        return (first + second) / 2;
    }

    function collectRatingMatches(regex, line) {
        const values = [];
        regex.lastIndex = 0;
        let match;
        while ((match = regex.exec(line)) !== null) {
            const value = ratingValueFromMatch(match);
            if (value != null) values.push(value);
            if (match[0] === '') regex.lastIndex++;
        }
        return values;
    }

    function extractRatingsFromPost(text) {
        const out = {};
        const lines = String(text || '')
            .split(/\n+/)
            .map(line => line.replace(/\s+/g, ' ').trim())
            .filter(Boolean);

        for (const def of CATEGORY_DEFS) {
            const values = [];
            const regexes = getCategoryRatingRegexes(def);
            for (const line of lines) {
                const strongValues = collectRatingMatches(regexes.strong, line);
                if (strongValues.length) {
                    values.push(...strongValues);
                    continue;
                }
                const reversedStrongValues = collectRatingMatches(regexes.reversedStrong, line);
                if (reversedStrongValues.length) {
                    values.push(...reversedStrongValues);
                    continue;
                }
                const directValues = collectRatingMatches(regexes.direct, line);
                if (directValues.length) {
                    values.push(...directValues);
                    continue;
                }
                values.push(...collectRatingMatches(regexes.described, line));
            }
            if (values.length) out[def.key] = avg(values);
        }
        return out;
    }

    function regexSpans(text, regex) {
        const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
        const rx = new RegExp(regex.source, flags);
        return [...String(text || '').matchAll(rx)].map(match => ({
            start: match.index,
            end: match.index + match[0].length
        }));
    }

    function spansOverlap(a, b) {
        return a.start < b.end && b.start < a.end;
    }

    function buildGarsoMentionEvidence(post, spans) {
        const sourceText = String(post?.text || '');
        const first = (Array.isArray(spans) ? spans : [])[0] || {
            start: 0,
            end: Math.min(sourceText.length, 1)
        };
        const start = Math.max(0, Number(first.start) - 105);
        const end = Math.min(sourceText.length, Number(first.end) + 145);
        const snippet = sourceText.slice(start, end).replace(/\s+/g, ' ').trim();
        return {
            topicTitle: normalizeEscortAdText(post?.topicTitle) || 'Temat Garso',
            url: post?.pageUrl ? normalizeTopicUrl(post.pageUrl) : '',
            author: normalizeEscortAdText(post?.author),
            date: normalizeEscortAdText(post?.date),
            snippet: `${start > 0 ? '…' : ''}${snippet}${end < sourceText.length ? '…' : ''}`,
            match: sourceText.slice(
                Math.max(0, Number(first.start)),
                Math.max(0, Number(first.end))
            ).replace(/\s+/g, ' ').trim().slice(0, 120)
        };
    }

    // Wzmianki liczymy w tym samym przebiegu po postach co oceny.
    // Reguły pozostają identyczne: post może zasilić licznik dodatni i ujemny,
    // a dla zdjęć wynik ujemny ma pierwszeństwo nad dodatnim.
    function createExplicitMentionAnalyzer() {
        const agencyWord = String.raw`(?:agenc(?:j|yjn)\p{L}*|agentur\p{L}*)`;
        const fraudWord = String.raw`(?:oszust\p{L}*|oszuś\p{L}*)`;
        const scamWord = String.raw`(?:scam\p{L}*|skam\p{L}*)`;
        const swapWord = String.raw`(?:podmian\p{L}*|podmien\p{L}*)`;
        const photoWord = String.raw`(?:zdj[eę](?:ci\p{L}*|ć|c(?!\p{L}))|fot\p{L}*)`;
        const wordToken = source => `(?<!\\p{L})(?:${source})(?!\\p{L})`;
        const agencyToken = wordToken(agencyWord);
        const fraudToken = wordToken(fraudWord);
        const scamToken = wordToken(scamWord);
        const swapToken = wordToken(swapWord);
        const photoToken = wordToken(photoWord);

        const gfeNegative = /(?:\b(?:nie\s+jest(?:\s+to)?|to\s+nie\s+jest)\b(?:\s+[\p{L}\p{N}%]+){0,10}\s+gfe\b|\bgfe\b(?:[\s,;:–—-]+[\p{L}\p{N}%]+){0,4}[\s,;:–—-]+nie\s+jest\b|\b(?:bez|brak|zero)\s+(?:klimatu\s+)?gfe\b|\bgfe\s*(?:[-–—:,]\s*)?0\s*%|\b(?:nie\s+uświadczysz|nie\s+uraczysz)(?:\s+\p{L}+){0,8}\s+gfe\b|\bgfe\b(?:\s+\p{L}+){0,3}\s+nie\s+(?:uświadczysz|uraczysz)\b|\bgfe\b.{0,80}\b(?:to\s+)?(?:absolutnie\s+)?nie\s+(?:ten|taki)\s+adres\b)/giu;
        const wtopaNegative = /(?:\b(?:bez|brak|zero)\s+(?:żadnej\s+)?wtop\p{L}*|\bani[\s,;:–—-]+(?:to\s+)?(?:żadna\s+)?wtop\p{L}*|\bnie\s+(?:było|byla|była|jest|ma)\s+(?:to\s+)?(?:żadnej\s+)?wtop\p{L}*|\bwtop\p{L}*(?:\s+\p{L}+){0,3}\s+nie\s+(?:było|byla|była|ma)\b|\b(?:to\s+)?nie\s+(?:jest|było|byla|była)\s+(?:to\s+)?wtop\p{L}*)/giu;
        const cooperativeNegative = /(?:\b(?:nie\s+jest(?:\s+to)?|to\s+nie\s+jest)\b(?:\s+\p{L}+){0,4}\s+sp[oó]łdzieln\p{L}*|\b(?:bez|brak)\s+sp[oó]łdzieln\p{L}*|\bsp[oó]łdzieln\p{L}*(?:\s+\p{L}+){0,3}\s+nie\s+jest\b)/giu;
        const agencyNegative = new RegExp(
            String.raw`(?:\bnie\s+${agencyToken}|\b(?:nie\s+jest(?:\s+to)?|to\s+nie\s+jest)\b(?:\s+\p{L}+){0,4}\s+${agencyToken}|\b(?:bez|brak)\s+${agencyToken}|${agencyToken}(?:\s+\p{L}+){0,3}\s+nie\s+jest\b)`,
            'giu'
        );
        const privateNegative = /(?:\bnie\s+(?:jest|działa|dziala|pracuje|przyjmuje)(?:\s+to)?(?:\s+\p{L}+){0,3}\s+prywatn\p{L}*|\b(?:bez|brak)\s+prywatn\p{L}*|\bprywatn\p{L}*(?:\s+\p{L}+){0,2}\s+nie\s+(?:jest|działa|dziala|pracuje|przyjmuje)\b)/giu;
        const makeFraudNegativeRegex = token => new RegExp(
            String.raw`(?:\b(?:nie\s+(?:jest|są|sa|była|byla|było|bylo|były|byly)(?:\s+to)?|to\s+nie\s+(?:jest|są|sa))(?:\s+\p{L}+){0,4}\s+${token}|\bto\s+nie\s+${token}|\b(?:bez|brak)\s+${token}|\bani[\s,;:–—-]+(?:to\s+)?${token}|${token}(?:\s+\p{L}+){0,3}\s+nie\s+(?:jest|są|sa)\b)`,
            'giu'
        );
        const fraudNegative = makeFraudNegativeRegex(fraudToken);
        const scamNegative = makeFraudNegativeRegex(scamToken);
        const swapNegative = makeFraudNegativeRegex(swapToken);
        const wtopaIgnored = /\b(?:iloś(?:ć|ci)|dużo|wiele|mnóstwo|sporo)\s+(?:różnych\s+)?wtop\p{L}*(?:\s+w\s+\p{L}+)?/giu;

        const photoConsistent = new RegExp(
            String.raw`(?:${photoToken}(?:(?!\bnie\s+(?:(?:są|sa|jest|były|byly)\s+)?(?:jej|zgodn\p{L}*|aktualn\p{L}*|autentyczn\p{L}*|prawdziw\p{L}*)\b).){0,35}\b(?:zgodn\p{L}*|aktualn\p{L}*|autentyczn\p{L}*|prawdziw\p{L}*)\b|${photoToken}(?:(?!\bnie\s+(?:są|sa|jest|były|byly)\s+jej\b).){0,18}\b(?:są|sa|to)\s+jej\b|(?<!nie\s)\b(?:są|sa|to)\s+jej\b.{0,18}${photoToken}|\b(?:zgodn\p{L}*|ta\s+sama)\b.{0,25}\b(?:na\s+)?${photoToken}|(?<!nie\s)\b(?:wygl[aą]d\p{L}*|jest)\b.{0,18}\bjak\s+na\s+${photoToken}|\b(?:dziewczyn|kobiet|pann)\p{L}*\b.{0,16}\b(?:jest\s+)?(?:ta\s+sama\s+)?(?:co|ze)\s+(?:na\s+)?${photoToken}|${photoToken}.{0,30}\bnie\s+(?:kłami\p{L}*|klami\p{L}*)\b)`,
            'giu'
        );
        const photoInconsistent = new RegExp(
            String.raw`(?:\bniezgodn\p{L}*.{0,35}${photoToken}|${photoToken}.{0,35}\b(?:niezgodn\p{L}*|fałszyw\p{L}*|falszyw\p{L}*|fake|nieaktualn\p{L}*|przerob\p{L}*)\b|\b(?:fałszyw\p{L}*|falszyw\p{L}*|fake|nieaktualn\p{L}*|przerob\p{L}*)\b.{0,25}${photoToken}|${photoToken}.{0,24}\bnie\s+(?:(?:są|sa|jest|były|byly)\s+)?(?:jej|zgodn\p{L}*|aktualn\p{L}*|autentyczn\p{L}*|prawdziw\p{L}*)\b|\bnie\s+(?:są|sa|jest|były|byly)\s+jej\b.{0,18}${photoToken}|\bnie\s+jej\b.{0,18}${photoToken}|\bnie\s+wygl[aą]d\p{L}*.{0,35}(?:jak|ze)\s+(?:na\s+)?${photoToken}|\b(?:inn\p{L}*|starsz\p{L}*|młodsz\p{L}*|mlodsz\p{L}*|grubsz\p{L}*|chudsz\p{L}*|niższ\p{L}*|nizsz\p{L}*|wyższ\p{L}*|wyzsz\p{L}*|ładniejsz\p{L}*|ladniejsz\p{L}*)\s+niż\s+(?:na\s+)?${photoToken}|\bnie\s+(?:jest\s+)?(?:tak\s+)?\p{L}+(?:\s+\p{L}+){0,2}\s+jak\s+(?:na\s+)?${photoToken}|\b(?:pann|dziewczyn|kobiet)\p{L}*\b.{0,25}\bnie\s+wygl[aą]d\p{L}*.{0,20}\bjak\s+(?:na|w)\s+ogłoszen\p{L}*|\bnie\s+wygl[aą]d\p{L}*.{0,20}\bjak\s+żadn\p{L}*\s+z\s+(?:dziewczyn|kobiet|osób|osob)\p{L}*\s+przedstawion\p{L}*|\bnie\s+(?:ta\s+)?(?:dziewczyn|kobiet|pann)\p{L}*.{0,12}\b(?:ze|z)\s+${photoToken}|${photoToken}(?:(?!\bnie\s+(?:kłami\p{L}*|klami\p{L}*)\b).){0,30}\b(?:kłami\p{L}*|klami\p{L}*)\b)`,
            'giu'
        );
        const photoAiGenerated = new RegExp(
            String.raw`(?:${photoToken}(?:(?!\bnie\b).){0,55}\b(?:wygenerowan\p{L}*|generowan\p{L}*)\b.{0,40}\b(?:ai|sztuczn\p{L}*\s+inteligenc\p{L}*)\b|\b(?:wygenerowan\p{L}*|generowan\p{L}*)\b.{0,40}\b(?:ai|sztuczn\p{L}*\s+inteligenc\p{L}*)\b.{0,55}${photoToken})`,
            'giu'
        );
        const photoNegative = new RegExp(
            `(?:${photoInconsistent.source}|${photoAiGenerated.source})`,
            'giu'
        );

        const recommendationWord = String.raw`polec(?:a\p{L}*|i(?:ć|ł\p{L}*|li\p{L}*)|e(?:ni\p{L}*|ń)|on\p{L}*)`;
        const recommendationToken = wordToken(recommendationWord);
        const recommendationIgnored = new RegExp(
            String.raw`${recommendationToken}\s+(?:raczej\s+)?(?:zostaw\p{L}*|zapark\p{L}*|parkow\p{L}*)`,
            'giu'
        );
        const niceAtmosphere = /(?:\bmił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\batmosfer\p{L}*(?:\s+\p{L}+){0,3}\s+mił\p{L}*)/giu;
        const niceAtmosphereNegative = /(?:\bniemił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\bnie\s+(?:(?:ma|jest|był\p{L}*)\s+)?mił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\b(?:bez|brak)\s+(?:\p{L}+\s+){0,2}mił\p{L}*(?:\s+\p{L}+){0,2}\s+atmosfer\p{L}*|\batmosfer\p{L}*(?:\s+\p{L}+){0,3}\s+nie\s+(?:jest|był\p{L}*)\s+mił\p{L}*)/giu;

        const stats = {};
        const createBucket = () => ({
            positive: 0,
            negative: 0,
            positiveEvidence: [],
            negativeEvidence: []
        });
        const rules = [
            ['GFE', 'normal', /\bgfe\b/giu, gfeNegative, null],
            ['wtopa', 'normal', /\bwtop\p{L}*/giu, wtopaNegative, wtopaIgnored],
            ['spółdzielnia', 'normal', /\bsp[oó]łdzieln\p{L}*/giu, cooperativeNegative, null],
            ['agencja', 'normal', new RegExp(agencyToken, 'giu'), agencyNegative, null],
            ['atmosfera', 'normal', niceAtmosphere, niceAtmosphereNegative, null],
            ['prywatnie', 'normal', /\bprywatn\p{L}*/giu, privateNegative, null],
            ['oszustwo', 'normal', new RegExp(fraudToken, 'giu'), fraudNegative, null],
            ['scam', 'normal', new RegExp(scamToken, 'giu'), scamNegative, null],
            ['podmianka', 'normal', new RegExp(swapToken, 'giu'), swapNegative, null],
            ['zdjęcia', 'photo', photoConsistent, photoNegative, null],
            [
                'rekomendacja',
                'normal',
                new RegExp(recommendationToken, 'giu'),
                new RegExp(String.raw`\bnie\s+${recommendationToken}`, 'giu'),
                recommendationIgnored
            ]
        ];
        for (const [key] of rules) stats[key] = createBucket();

        function add(post) {
            const text = String(post?.text || '');
            for (const [key, type, positiveRegex, negativeRegex, ignoredRegex] of rules) {
                const bucket = stats[key];
                const negativeSpans = regexSpans(text, negativeRegex);
                if (negativeSpans.length) {
                    bucket.negative++;
                    bucket.negativeEvidence.push(
                        buildGarsoMentionEvidence(post, negativeSpans)
                    );
                }
                if (type === 'photo' && negativeSpans.length) continue;

                const positiveSpansRaw = regexSpans(text, positiveRegex);
                const ignoredSpans = ignoredRegex
                    ? regexSpans(text, ignoredRegex)
                    : [];
                const positiveSpans = type === 'photo'
                    ? positiveSpansRaw
                    : positiveSpansRaw.filter(mention =>
                        !negativeSpans.some(negated => spansOverlap(mention, negated)) &&
                        !ignoredSpans.some(ignored => spansOverlap(mention, ignored))
                    );
                if (positiveSpans.length) {
                    bucket.positive++;
                    bucket.positiveEvidence.push(
                        buildGarsoMentionEvidence(post, positiveSpans)
                    );
                }
            }
        }

        return {
            add,
            result: () => stats
        };
    }


    function parseGarsoPostDate(value) {
        const text = String(value || '')
            .toLocaleLowerCase('pl-PL')
            .replace(/\u00a0/g, ' ')
            .replace(/\s+/g, ' ')
            .trim();
        if (!text) return null;

        const relativeDate = new Date();
        if (/\b(?:dzisiaj|today)\b/.test(text)) {
            return Date.UTC(
                relativeDate.getFullYear(),
                relativeDate.getMonth(),
                relativeDate.getDate()
            );
        }
        if (/\b(?:wczoraj|yesterday)\b/.test(text)) {
            relativeDate.setDate(relativeDate.getDate() - 1);
            return Date.UTC(
                relativeDate.getFullYear(),
                relativeDate.getMonth(),
                relativeDate.getDate()
            );
        }

        let match = text.match(/\b(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})\b/);
        if (match) {
            const time = Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
            return Number.isFinite(time) ? time : null;
        }

        match = text.match(/\b(\d{1,2})[.\-/](\d{1,2})[.\-/](\d{4})\b/);
        if (match) {
            const time = Date.UTC(Number(match[3]), Number(match[2]) - 1, Number(match[1]));
            return Number.isFinite(time) ? time : null;
        }

        const polishMonths = {
            styczeń: 0, stycznia: 0,
            luty: 1, lutego: 1,
            marzec: 2, marca: 2,
            kwiecień: 3, kwietnia: 3,
            maj: 4, maja: 4,
            czerwiec: 5, czerwca: 5,
            lipiec: 6, lipca: 6,
            sierpień: 7, sierpnia: 7,
            wrzesień: 8, września: 8, wrzesnia: 8,
            październik: 9, października: 9, pazdziernika: 9,
            listopad: 10, listopada: 10,
            grudzień: 11, grudnia: 11
        };
        match = text.match(
            /\b(\d{1,2})\s+(styczeń|stycznia|luty|lutego|marzec|marca|kwiecień|kwietnia|maj|maja|czerwiec|czerwca|lipiec|lipca|sierpień|sierpnia|wrzesień|września|wrzesnia|październik|października|pazdziernika|listopad|listopada|grudzień|grudnia)\s+(\d{4})\b/
        );
        if (!match) return null;
        const time = Date.UTC(Number(match[3]), polishMonths[match[2]], Number(match[1]));
        return Number.isFinite(time) ? time : null;
    }

    function formatIsoDateFromTimestamp(timestamp) {
        const time = Number(timestamp);
        return Number.isFinite(time) ? new Date(time).toISOString().slice(0, 10) : null;
    }

    function detectOralRatingVariants(text) {
        const value = String(text || '');
        return {
            fbg: /(?:\b(?:fbg|fgb)\b|\bfrancuz\p{L}*.{0,30}\bbez\s+gum\p{L}*)/iu.test(value),
            fwg: /(?:\bfwg\b|\bfrancuz\p{L}*.{0,30}\b(?:w|z)\s+gum\p{L}*)/iu.test(value)
        };
    }

    function buildRatingStats(posts) {
        const reviews = [];
        const categoryValues = Object.fromEntries(CATEGORY_DEFS.map(d => [d.key, []]));
        const oralRatingVariants = { fbg: false, fwg: false };
        const mentionAnalyzer = createExplicitMentionAnalyzer();

        posts.forEach((post, index) => {
            mentionAnalyzer.add(post);
            const ratings = extractRatingsFromPost(post.text);
            const componentValues = CATEGORY_DEFS
                .filter(def => !def.overall && ratings[def.key] != null)
                .map(def => ratings[def.key]);
            const vals = componentValues.length
                ? componentValues
                : (ratings.summary == null ? [] : [ratings.summary]);
            if (!Object.keys(ratings).length || !vals.length) return;
            for (const [k, v] of Object.entries(ratings)) categoryValues[k].push(v);
            if (ratings.oral != null) {
                const variants = detectOralRatingVariants(post.text);
                oralRatingVariants.fbg ||= variants.fbg;
                oralRatingVariants.fwg ||= variants.fwg;
            }
            reviews.push({ index, mean: avg(vals), ratings, post });
        });

        const reviewMeans = reviews.map(r => r.mean);
        const datedReviews = reviews
            .map(review => ({
                time: parseGarsoPostDate(review.post?.date),
                url: review.post?.pageUrl
                    ? normalizeTopicUrl(review.post.pageUrl)
                    : null,
                mean: review.mean,
                index: review.index
            }))
            .filter(item => Number.isFinite(item.time))
            .sort((a, b) => a.time - b.time || a.index - b.index);
        // „Ostatnie 5” oznacza pięć najnowszych recenzji według daty posta,
        // a nie pięć ostatnio przetworzonych tematów. Jeżeli choć jednej daty
        // nie udało się odczytać, bezpieczniej zachować kolejność postów niż
        // policzyć średnią z przypadkowo niepełnego podzbioru datowanych ocen.
        const chronologicalLastReviews = datedReviews.length === reviews.length
            ? datedReviews.slice(-5)
            : reviews.slice(-5).map(review => ({ mean: review.mean }));
        const last5 = chronologicalLastReviews.map(review => review.mean);
        const datedPosts = posts
            .map(post => ({
                time: parseGarsoPostDate(post?.date),
                url: post?.pageUrl ? normalizeTopicUrl(post.pageUrl) : null
            }))
            .filter(item => Number.isFinite(item.time))
            .sort((a, b) => a.time - b.time);
        const reviewDateSource = reviews.length
            ? (datedReviews.length ? datedReviews : datedPosts)
            : [];
        const categories = {};
        const oralCategoryLabel = oralRatingVariants.fbg !== oralRatingVariants.fwg
            ? (oralRatingVariants.fbg ? 'FBG' : 'FWG')
            : 'Francuz';
        for (const def of CATEGORY_DEFS) {
            const values = categoryValues[def.key];
            categories[def.key] = {
                key: def.key,
                label: def.key === 'oral' ? oralCategoryLabel : def.label,
                mean: round1(avg(values)),
                count: values.length,
                min: values.length ? round1(Math.min(...values)) : null,
                max: values.length ? round1(Math.max(...values)) : null
            };
        }

        const rankedCategories = CATEGORY_DEFS
            .filter(def => !def.overall && categories[def.key].count >= 2)
            .map(def => categories[def.key])
            .sort((a, b) => b.mean - a.mean);
        return {
            reviews,
            reviewCount: reviews.length,
            mean: round1(avg(reviewMeans)),
            median: round1(median(reviewMeans)),
            min: reviewMeans.length ? round1(Math.min(...reviewMeans)) : null,
            max: reviewMeans.length ? round1(Math.max(...reviewMeans)) : null,
            last5Mean: round1(avg(last5)),
            last5ReviewCount: last5.length,
            firstReviewDate: formatIsoDateFromTimestamp(reviewDateSource[0]?.time),
            lastReviewDate: formatIsoDateFromTimestamp(reviewDateSource.at(-1)?.time),
            firstReviewUrl: reviewDateSource[0]?.url || null,
            lastReviewUrl: reviewDateSource.at(-1)?.url || null,
            categories,
            bestCategory: rankedCategories[0] || null,
            worstCategory: rankedCategories.length > 1 ? rankedCategories[rankedCategories.length - 1] : null,
            mentions: mentionAnalyzer.result()
        };
    }

    function getGarsoRatingCategoryLabel(category) {
        const label = String(category?.label || '').trim();
        const normalizedLabel = label.toLocaleLowerCase('pl-PL');
        if (normalizedLabel === 'francuz / oral') return 'Francuz';
        if (normalizedLabel === 'wygląd dziewczyny') return 'Wygląd';
        if (normalizedLabel === 'akcja / seks') return 'Akcja';
        return label;
    }

    function getGarsoRatingBarColor(mean) {
        const score = Number(mean);
        if (mean == null || !Number.isFinite(score)) {
            return 'rgba(255,255,255,.16)';
        }
        if (score < 5) return '#e04b4b';
        if (score >= 9) return '#4ac46b';

        const hue = score <= 7
            ? ((score - 5) / 2) * 50
            : 50 + ((score - 7) / 2) * 72;
        return `hsl(${Math.round(hue)}, 78%, 52%)`;
    }


    // ============================================================
    // PODSUMOWANIE GARSO
    // ============================================================

    function emitGarsoCountState(btn, state, count = null, fromCache = false) {
        if (!btn) return;
        btn.dispatchEvent(new CustomEvent('vm-garso-count-state', {
            detail: {
                state,
                count: Number.isFinite(Number(count)) ? Number(count) : null,
                fromCache: !!fromCache
            }
        }));
    }

    function setGarsoSummaryState(
        btn,
        state,
        html,
        { checkedAt = null, cacheState = null } = {}
    ) {
        if (!btn) return;
        btn.dataset.garsoSummaryState = state;
        btn.dataset.garsoSummaryHtml = html || '';
        if (Number.isFinite(Number(checkedAt)) && Number(checkedAt) > 0) {
            btn.dataset.garsoSummaryCheckedAt = String(Number(checkedAt));
        } else {
            delete btn.dataset.garsoSummaryCheckedAt;
        }
        if (cacheState) btn.dataset.garsoSummaryCacheState = cacheState;
        else delete btn.dataset.garsoSummaryCacheState;
        renderGarsoButton(btn);
        btn.dispatchEvent(new CustomEvent('vm-garso-summary-state', {
            detail: {
                state,
                html: btn.dataset.garsoSummaryHtml,
                checkedAt: Number(btn.dataset.garsoSummaryCheckedAt) || null,
                cacheState: btn.dataset.garsoSummaryCacheState || null,
                firstReviewDate: btn.dataset.garsoFirstReviewDate || null,
                lastReviewDate: btn.dataset.garsoLastReviewDate || null,
                firstReviewUrl: btn.dataset.garsoFirstReviewUrl || null,
                lastReviewUrl: btn.dataset.garsoLastReviewUrl || null,
                result: btn._vmGarsoSummaryResult || null
            }
        }));
    }

    function restoreGarsoSummaryFromCache(btn, searchTerm, searchMode) {
        const cached = getGarsoSummaryCache(searchMode, searchTerm);
        if (!cached) return null;
        applySummaryToButton(btn, cached.result, {
            checkedAt: cached.checkedAt,
            cacheState: cached.fresh ? 'fresh' : 'stale'
        });
        return cached;
    }

    async function prepareGarsoSummary(
        btn,
        searchTerm,
        topics,
        forceRefresh = false,
        operationCancelToken = null
    ) {
        const searchMode = btn.dataset.garsoSearchMode || '';
        const key = getGarsoTopicsSignature(topics);
        const persistent = getGarsoSummaryCache(searchMode, searchTerm);
        const sameTopics = persistent && persistent.topicSignature === key;

        if (!forceRefresh && persistent?.fresh && sameTopics) {
            applySummaryToButton(btn, persistent.result, {
                checkedAt: persistent.checkedAt,
                cacheState: 'fresh'
            });
            return persistent.result;
        }

        if (sameTopics) {
            setGarsoSummaryState(
                btn,
                'refreshing',
                summaryToHtml(persistent.result),
                { checkedAt: persistent.checkedAt, cacheState: 'refreshing' }
            );
        } else {
            setGarsoSummaryState(
                btn,
                'loading',
                '<b>Garso - recenzje</b><br>Pobieram treść tematów…'
            );
        }

        if (!forceRefresh && summaryCache.has(key)) {
            const cached = await summaryCache.get(key);
            const complete = isCompleteGarsoSummary(cached, topics);
            const persistedAt = complete
                ? setGarsoSummaryCache(searchMode, searchTerm, topics, cached)
                : null;
            const checkedAt = persistedAt || Date.now();
            applySummaryToButton(btn, cached, {
                checkedAt,
                cacheState: persistedAt ? 'fresh' : (complete ? 'session' : 'partial')
            });
            return cached;
        }

        if (forceRefresh) summaryCache.delete(key);
        const cancelToken = operationCancelToken ||
            createOperationCancelToken('Pełna analiza Garso');
        btn._vmGarsoSummaryCancelToken = cancelToken;
        if (sameTopics) {
            setGarsoSummaryState(
                btn,
                'refreshing',
                summaryToHtml(persistent.result),
                { checkedAt: persistent.checkedAt, cacheState: 'refreshing' }
            );
        } else {
            setGarsoSummaryState(
                btn,
                'loading',
                '<b>Garso - recenzje</b><br>Pobieram treść tematów…'
            );
        }
        const progressOptions = sameTopics
            ? { checkedAt: persistent.checkedAt, cacheState: 'refreshing' }
            : {};
        const renderProgress = progress => {
            if (!progress || !['page', 'topic-done', 'stats'].includes(progress.phase)) {
                return;
            }
            let line = 'Pobieram treść tematów…';
            if (progress.phase === 'page') {
                line = `Pobieranie: temat ${progress.topicIndex + 1}/${progress.topicTotal}` +
                    ` • strona ${progress.page}/${progress.pageTotal}` +
                    (progress.fromCache ? ' • cache' : '');
            } else if (progress.phase === 'topic-done') {
                line = `Pobrano tematy: ${progress.completedTopics}/${progress.topicTotal}`;
            } else if (progress.phase === 'stats') {
                line = `Obliczam statystyki z ${progress.postCount} postów…`;
            }
            setGarsoSummaryState(
                btn,
                sameTopics ? 'refreshing' : 'loading',
                `<b>Garso - recenzje</b><br>${escapeHtml(line)}`,
                progressOptions
            );
        };
        const promise = buildGarsoSummary(
            searchTerm,
            topics,
            cancelToken,
            { forceRefresh, onProgress: renderProgress }
        ).then(compactGarsoSummaryResult);
        summaryCache.set(key, promise);
        try {
            const result = await promise;
            const complete = isCompleteGarsoSummary(result, topics);
            if (!complete) {
                summaryCache.delete(key);
                recordIncompleteAnalysis(
                    'Garso - recenzje',
                    `Sprawdzono ${result?.screenedTopicCount || 0} z ${topics.length} tematów; błędy: ${result?.failedTopicCount || 0}`
                );
            }
            const persistedAt = complete
                ? setGarsoSummaryCache(searchMode, searchTerm, topics, result)
                : null;
            if (!complete && sameTopics) {
                applySummaryToButton(btn, persistent.result, {
                    checkedAt: persistent.checkedAt,
                    cacheState: 'stale'
                });
                return persistent.result;
            }
            const checkedAt = persistedAt || Date.now();
            applySummaryToButton(btn, result, {
                checkedAt,
                cacheState: persistedAt ? 'fresh' : (complete ? 'session' : 'partial')
            });
            return result;
        } catch (e) {
            summaryCache.delete(key);
            if (isOperationCancelledError(e)) {
                recordDiagnosticCancellation(
                    'Garso - recenzje',
                    'Pełna analiza została przerwana.'
                );
                if (sameTopics) {
                    btn._vmGarsoSummaryResult = persistent.result;
                    setGarsoSummaryState(
                        btn,
                        'cancelled',
                        summaryToHtml(persistent.result),
                        {
                            checkedAt: persistent.checkedAt,
                            cacheState: 'stale'
                        }
                    );
                    return persistent.result;
                }
                setGarsoSummaryState(
                    btn,
                    'cancelled',
                    '<b>Garso - recenzje</b><br>Analiza została przerwana.'
                );
                return null;
            }
            log('Błąd podsumowania Garso:', e);
            if (sameTopics) {
                applySummaryToButton(btn, persistent.result, {
                    checkedAt: persistent.checkedAt,
                    cacheState: 'stale'
                });
            } else {
                setGarsoSummaryState(
                    btn,
                    'error',
                    `<b>Garso - recenzje</b><br>Nie udało się pobrać postów: ${escapeHtml(e.message || e)}`
                );
            }
            return null;
        } finally {
            if (btn._vmGarsoSummaryCancelToken === cancelToken) {
                btn._vmGarsoSummaryCancelToken = null;
            }
        }
    }

    async function buildGarsoSummary(
        searchTerm,
        topics,
        cancelToken = null,
        { forceRefresh = false, onProgress = null } = {}
    ) {
        const allPosts = [];
        const topicInfo = [];
        const analyzedTopics = [];
        const excludedTopics = [];
        let screenedTopicCount = 0;
        let excludedTopicCount = 0;
        let failedTopicCount = 0;
        let completedTopics = 0;

        const topicResults = await watchMapLimit(
            topics,
            GARSO_TOPIC_FETCH_CONCURRENT,
            async (topic, topicIndex) => {
                try {
                    cancelToken?.throwIfCancelled();
                    const data = await fetchSelectedTopicPages(
                        topic,
                        cancelToken,
                        {
                            forceRefresh,
                            topicIndex,
                            topicTotal: topics.length,
                            onProgress
                        }
                    );
                    cancelToken?.throwIfCancelled();
                    const result = { ok: true, topic, data, posts: [] };
                    if (data.eligible) {
                        const analyzedTopic = {
                            ...topic,
                            title: data.title || topic.title
                        };
                        result.analyzedTopic = analyzedTopic;
                        for (const page of data.pages) {
                            result.posts.push(...extractPostsFromPage(
                                page.html,
                                analyzedTopic.title,
                                page.page,
                                page.url
                            ));
                        }
                    }
                    return result;
                } catch (error) {
                    if (isOperationCancelledError(error)) throw error;
                    return { ok: false, topic, error };
                } finally {
                    completedTopics++;
                    onProgress?.({
                        phase: 'topic-done',
                        completedTopics,
                        topicTotal: topics.length,
                        topicIndex
                    });
                }
            }
        );

        // Łączenie po zakończeniu pobierania zachowuje pierwotną kolejność tematów,
        // mimo że same żądania HTTP wykonywane są równolegle.
        for (const result of topicResults) {
            if (!result?.ok) {
                failedTopicCount++;
                log(
                    `Nie udało się zakwalifikować tematu Garso: ${result?.topic?.url || '?'}`,
                    result?.error
                );
                continue;
            }
            screenedTopicCount++;
            const { topic, data } = result;
            if (!data.eligible) {
                excludedTopicCount++;
                excludedTopics.push({
                    title: data.title || topic.title,
                    url: topic.url
                });
                continue;
            }

            const analyzedTopic = result.analyzedTopic;
            analyzedTopics.push(analyzedTopic);
            allPosts.push(...result.posts);
            topicInfo.push({
                title: analyzedTopic.title,
                subtitle: data.subtitle || topic.subtitle || '',
                url: analyzedTopic.url,
                maxPage: data.maxPage,
                pagesRead: data.pages.map(page => page.page),
                posts: result.posts.length
            });
        }

        cancelToken?.throwIfCancelled();
        onProgress?.({ phase: 'stats', postCount: allPosts.length });
        const stats = buildRatingStats(allPosts);
        return {
            searchTerm,
            topics: analyzedTopics,
            allPosts,
            topicInfo,
            excludedTopics,
            screenedTopicCount,
            excludedTopicCount,
            failedTopicCount,
            stats
        };
    }

    function getGarsoMentionEntries(resultOrStats, includeZero = false) {
        const stats = resultOrStats?.stats || resultOrStats;
        const mentions = stats?.mentions;
        if (!mentions || typeof mentions !== 'object') return [];

        const entries = [
            ['GFE', mentions.GFE?.positive, 'positive', mentions.GFE?.positiveEvidence],
            ['brak GFE', mentions.GFE?.negative, 'negative', mentions.GFE?.negativeEvidence],
            ['wtopa', mentions.wtopa?.positive, 'negative', mentions.wtopa?.positiveEvidence],
            ['bez wtopy', mentions.wtopa?.negative, 'positive', mentions.wtopa?.negativeEvidence],
            ['spółdzielnia', mentions.spółdzielnia?.positive, 'negative', mentions.spółdzielnia?.positiveEvidence],
            ['nie spółdzielnia', mentions.spółdzielnia?.negative, 'positive', mentions.spółdzielnia?.negativeEvidence],
            ['agentura', mentions.agencja?.positive, 'negative', mentions.agencja?.positiveEvidence],
            ['nie agentura', mentions.agencja?.negative, 'positive', mentions.agencja?.negativeEvidence],
            ['miła atmosfera', mentions.atmosfera?.positive, 'positive', mentions.atmosfera?.positiveEvidence],
            ['niemiła atmosfera', mentions.atmosfera?.negative, 'negative', mentions.atmosfera?.negativeEvidence],
            ['prywatnie', mentions.prywatnie?.positive, 'positive', mentions.prywatnie?.positiveEvidence],
            ['nie prywatnie', mentions.prywatnie?.negative, 'negative', mentions.prywatnie?.negativeEvidence],
            ['oszustka', mentions.oszustwo?.positive, 'negative', mentions.oszustwo?.positiveEvidence],
            ['nie oszustka', mentions.oszustwo?.negative, 'positive', mentions.oszustwo?.negativeEvidence],
            ['scam', mentions.scam?.positive, 'negative', mentions.scam?.positiveEvidence],
            ['brak scamu', mentions.scam?.negative, 'positive', mentions.scam?.negativeEvidence],
            ['podmianka', mentions.podmianka?.positive, 'negative', mentions.podmianka?.positiveEvidence],
            ['brak podmianki', mentions.podmianka?.negative, 'positive', mentions.podmianka?.negativeEvidence],
            ['zdjęcia zgodne', mentions.zdjęcia?.positive, 'positive', mentions.zdjęcia?.positiveEvidence],
            ['zdjęcia niezgodne', mentions.zdjęcia?.negative, 'negative', mentions.zdjęcia?.negativeEvidence],
            ['polecam', mentions.rekomendacja?.positive, 'positive', mentions.rekomendacja?.positiveEvidence],
            ['nie polecam', mentions.rekomendacja?.negative, 'negative', mentions.rekomendacja?.negativeEvidence]
        ].map(([label, count, tone, evidence]) => ({
            label,
            count: Math.max(0, Number(count) || 0),
            tone,
            evidence: (Array.isArray(evidence) ? evidence : []).map(item => ({
                topicTitle: normalizeEscortAdText(item?.topicTitle) || 'Temat Garso',
                url: item?.url ? normalizeTopicUrl(item.url) : '',
                author: normalizeEscortAdText(item?.author),
                date: normalizeEscortAdText(item?.date),
                snippet: String(item?.snippet || '').slice(0, 360),
                match: String(item?.match || '').slice(0, 120)
            }))
        }));

        return includeZero
            ? entries
            : entries.filter(entry => entry.count > 0);
    }

    function getGarsoMentionToneStyle(tone) {
        if (tone === 'positive') {
            return {
                color: '#9aefad',
                border: 'rgba(98,207,123,.65)',
                background: 'rgba(46,160,78,.14)'
            };
        }
        if (tone === 'negative') {
            return {
                color: '#ff9a9a',
                border: 'rgba(255,98,98,.72)',
                background: 'rgba(220,53,69,.16)'
            };
        }
        return {
            color: '#fff',
            border: 'rgba(245,77,163,.55)',
            background: 'rgba(245,77,163,.10)'
        };
    }

    function getGarsoReviewWarning(result) {
        const popularMentions = getMostFrequentGarsoMentions(result);
        const negativePopularMentions = popularMentions.filter(
            item => item.tone === 'negative'
        );
        const rawMean = result?.stats?.mean;
        const mean = Number(rawMean);
        const lowMean = rawMean != null && Number.isFinite(mean) && mean < 6;
        const reasons = [];

        if (negativePopularMentions.length) {
            reasons.push(
                `Najpopularniejsze negatywne wzmianki: ${negativePopularMentions
                    .map(item => `${item.label} (${item.count})`)
                    .join(', ')}.`
            );
        }
        if (lowMean) reasons.push(`Średnia ocena: ${rawMean}/10.`);

        return {
            show: reasons.length > 0,
            title: reasons.join('\n'),
            lowMean,
            negativePopularMentions
        };
    }

    function summaryToHtml(result) {
        const s = result.stats;
        const analyzedTopicCount = result.topicInfo.length;
        const analyzedPostCount = Math.max(0, Number(result.postCount) || 0);
        const getPostWord = count => {
            const normalizedCount = Math.max(0, Number(count) || 0);
            if (normalizedCount === 1) return 'post';
            const last = normalizedCount % 10;
            const lastTwo = normalizedCount % 100;
            return last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)
                ? 'posty'
                : 'postów';
        };
        const analyzedPostWord = getPostWord(analyzedPostCount);
        const excludedTopics = Array.isArray(result.excludedTopics)
            ? result.excludedTopics
            : [];
        const topicLines = result.topicInfo.map(t => `
            <a href="${escapeHtml(t.url || '')}" target="_blank" rel="noopener noreferrer"
               style="display:block;margin-top:5px;color:#fff;text-decoration:none">
                <span style="color:${getEscortPagePinkColor()}">›</span>
                ${escapeHtml(t.title)}
                <span style="display:block;margin-left:12px;color:#cbb8ce;font-size:10px">
                    ${t.posts} ${getPostWord(t.posts)}
                </span>
            </a>
        `).join('');
        const excludedTopicLines = excludedTopics.map(topic => `
            <a href="${escapeHtml(topic.url || '')}" target="_blank" rel="noopener noreferrer"
               style="display:block;margin-top:5px;color:#ffd2e8;text-decoration:none">
                <span style="color:#ffb45c">›</span> ${escapeHtml(topic.title)}
            </a>
        `).join('');
        const subtitleTopics = result.topicInfo
            .map(topic => ({
                ...topic,
                subtitle: getDisplayableGarsoTopicSubtitle(topic?.subtitle)
            }))
            .filter(topic => topic.subtitle);
        const subtitleLines = subtitleTopics
            .map(t => `
                <div style="margin-top:6px">
                    <div style="color:#fff;font-weight:800;line-height:1.25">
                        ${escapeHtml(t.subtitle)}
                    </div>
                    <div style="margin-top:2px;color:#cbb8ce;font-size:10px;line-height:1.2">
                        ${escapeHtml(t.title)}
                    </div>
                </div>
            `)
            .join('');
        const categoryCards = CATEGORY_DEFS.map(d => {
            const c = s.categories[d.key];
            const percentage = c.mean == null
                ? 0
                : Math.max(0, Math.min(100, Number(c.mean) * 10));
            const categoryLabel = getGarsoRatingCategoryLabel(c);
            const barColor = getGarsoRatingBarColor(c.mean);
            return `
                <div style="padding:7px 8px;border:1px solid rgba(255,255,255,.16);border-radius:7px;background:rgba(255,255,255,.035)">
                    <div style="display:flex;justify-content:space-between;gap:8px">
                        <b>${escapeHtml(categoryLabel)}</b>
                        <span style="color:${c.mean == null ? '#cbb8ce' : '#fff'};font-weight:800">
                            ${c.mean == null ? '-' : `${c.mean}/10`}
                        </span>
                    </div>
                    <div style="height:4px;margin:6px 0 4px;border-radius:999px;background:rgba(255,255,255,.12);overflow:hidden">
                        <div style="width:${percentage}%;height:100%;background:${barColor}"></div>
                    </div>
                    <div style="color:#cbb8ce;font-size:10px">
                        ${c.mean == null ? 'brak ocen' : `${c.count} ocen · zakres ${c.min}-${c.max}`}
                    </div>
                </div>
            `;
        }).join('');

        const range = s.reviewCount ? `${s.min}-${s.max}/10` : '-';
        const mentionParts = getGarsoMentionEntries(s);
        mentionParts.sort((a, b) =>
            b.count - a.count || a.label.localeCompare(b.label, 'pl-PL')
        );
        mentionParts.forEach((item, index) => { item.dialogIndex = index; });
        const renderMentionChip = item => {
            const tone = getGarsoMentionToneStyle(item.tone);
            return `
                <button type="button" class="vm-garso-mention-chip"
                        data-vm-garso-mention-index="${item.dialogIndex}"
                        title="Dlaczego zliczono? Pokaż tematy i fragmenty postów."
                        style="display:inline-block;margin:3px 3px 0 0;padding:3px 7px;border:1px solid ${tone.border};border-radius:999px;background:${tone.background};color:${tone.color};font:inherit;cursor:pointer">
                    ${escapeHtml(item.label)} <b>${item.count}</b>
                </button>
            `;
        };
        const mentionChipsHtml = mentionParts.map(renderMentionChip).join('');
        const metricCard = (label, value, detail = '') => `
            <div style="padding:8px;border:1px solid rgba(245,77,163,.32);border-radius:8px;background:rgba(245,77,163,.07);text-align:center">
                <div style="font-size:18px;font-weight:900;line-height:1.05;color:#fff">${value}</div>
                <div style="margin-top:3px;color:#cbb8ce;font-size:10px;line-height:1.15">${label}</div>
                ${detail ? `<div style="margin-top:2px;color:#9f8ca3;font-size:9px">${detail}</div>` : ''}
            </div>
        `;
        const overallMeanWithTrend = (() => {
            if (s.mean == null) return '-';

            const overallMean = Number(s.mean);
            const last5Mean = Number(s.last5Mean);
            const reviewCount = Number(s.reviewCount);
            const last5ReviewCount = Number(s.last5ReviewCount);
            const value = `${s.mean}/10`;
            if (
                !Number.isFinite(overallMean) ||
                !Number.isFinite(last5Mean) ||
                last5ReviewCount < 5 ||
                reviewCount <= 5 ||
                last5Mean === overallMean
            ) {
                return value;
            }

            const isIncrease = last5Mean > overallMean;
            const delta = round1(Math.abs(last5Mean - overallMean));
            const direction = isIncrease ? 'wzrost' : 'spadek';
            const arrow = isIncrease ? '↑' : '↓';
            const color = isIncrease ? '#67df85' : '#ff7777';
            const tooltip = `Średnia z ostatnich 5 recenzji: ${s.last5Mean}/10 - ${direction} o ${delta} względem średniej ogólnej.`;
            return `${value}<span title="${escapeHtml(tooltip)}" aria-label="${escapeHtml(tooltip)}" style="margin-left:5px;color:${color};font-size:17px;font-weight:900">${arrow}</span>`;
        })();
        const last5MeanColor = (() => {
            if (s.mean == null || s.last5Mean == null) return '#fff';
            const overallMean = Number(s.mean);
            const last5Mean = Number(s.last5Mean);
            if (!Number.isFinite(overallMean) || !Number.isFinite(last5Mean)) {
                return '#fff';
            }
            if (last5Mean > overallMean) return '#67df85';
            if (last5Mean < overallMean) return '#ff7777';
            return '#fff';
        })();
        const lastReviewsLabel = Number(s.last5ReviewCount) === 1
            ? 'ostatnia ocena'
            : `ostatnie ${Math.max(0, Number(s.last5ReviewCount) || 0)}`;
        const extremeCard = (label, category, tone) => {
            const isHigh = tone === 'high';
            const color = isHigh ? '#9aefad' : '#ffaaaa';
            const border = isHigh
                ? 'rgba(98,207,123,.62)'
                : 'rgba(255,98,98,.68)';
            const background = isHigh
                ? 'rgba(46,160,78,.13)'
                : 'rgba(220,53,69,.14)';
            return `
                <div style="padding:8px;border:1px solid ${border};border-radius:8px;background:${background};text-align:center">
                    <div style="color:${color};font-size:10px;font-weight:900;line-height:1.15">${label}</div>
                    <div style="margin-top:4px;color:#fff;font-size:15px;font-weight:900;line-height:1.15">
                        ${category ? escapeHtml(getGarsoRatingCategoryLabel(category)) : 'brak ocen'}
                    </div>
                    <div style="margin-top:3px;color:${color};font-size:10px;font-weight:900;line-height:1.1">
                        ${category ? `${category.mean}/10` : '-'}
                    </div>
                </div>
            `;
        };

        return `
            <div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px">
                ${metricCard('recenzje z ocenami', s.reviewCount)}
                ${metricCard('średnia ogólna', overallMeanWithTrend)}
                ${extremeCard('najwyżej', s.bestCategory, 'high')}
                ${extremeCard('najniżej', s.worstCategory, 'low')}
            </div>
            <div style="margin-top:7px;padding:7px 8px;border-radius:7px;background:rgba(255,255,255,.035);color:#d8c8d9">
                Analiza: <b style="color:#fff">${analyzedTopicCount} ${getGarsoTopicWord(analyzedTopicCount)} · ${analyzedPostCount} ${analyzedPostWord}</b>
                · zakres: <b style="color:#fff">${range}</b>
                · ${lastReviewsLabel}: <b style="color:${last5MeanColor}">${s.last5Mean == null ? '-' : `${s.last5Mean}/10`}</b>
            </div>
            ${subtitleLines ? `
                <div style="margin-top:8px;padding:8px 9px;border:1px solid rgba(245,77,163,.34);border-radius:7px;background:rgba(245,77,163,.06)">
                    <b style="color:${getEscortPagePinkColor()}">Dopiski z Garso</b>${subtitleLines}
                </div>
            ` : ''}
            <div style="margin-top:9px;font-weight:800;color:${getEscortPagePinkColor()}">Oceny cząstkowe</div>
            <div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-top:6px">${categoryCards}</div>
            ${mentionChipsHtml ? `
                <div style="margin-top:9px;padding:8px 9px;border:1px solid rgba(255,255,255,.14);border-radius:7px">
                    <b>Jawne wzmianki w postach</b>
                    <div style="margin-top:3px">${mentionChipsHtml}</div>
                </div>
            ` : ''}
            <div style="margin-top:9px;padding:8px 9px;border:1px solid rgba(255,255,255,.14);border-radius:7px;background:rgba(255,255,255,.025)">
                <b style="color:${getEscortPagePinkColor()}">Analizowane tematy</b>
                ${topicLines || '<div style="margin-top:5px;color:#cbb8ce">Brak tematów spełniających kryteria analizy.</div>'}
            </div>
            ${excludedTopicLines ? `
                <div style="margin-top:8px;padding:8px 9px;border:1px solid rgba(255,180,92,.58);border-radius:7px;background:rgba(255,180,92,.07)">
                    <b style="color:#ffb45c">Pominięte w statystyce (${excludedTopics.length})</b>
                    <div style="margin-top:3px;color:#d8c8d9;font-size:10px">Tytuły mogą zawierać polecenia lub ostrzeżenia - kliknij, aby otworzyć temat.</div>
                    ${excludedTopicLines}
                </div>
            ` : ''}
        `;
    }

    function applySummaryToButton(
        btn,
        result,
        { checkedAt = null, cacheState = null } = {}
    ) {
        btn._vmGarsoSummaryResult = result || null;
        const firstReviewDate = result?.stats?.firstReviewDate || '';
        const lastReviewDate = result?.stats?.lastReviewDate || '';
        btn.dataset.garsoReviewCount = String(
            Math.max(0, Number(result?.stats?.reviewCount) || 0)
        );
        const firstReviewUrl = result?.stats?.firstReviewUrl ||
            result?.topics?.[0]?.url || '';
        const lastReviewUrl = result?.stats?.lastReviewUrl ||
            result?.topics?.at(-1)?.url || firstReviewUrl;
        if (firstReviewDate) btn.dataset.garsoFirstReviewDate = firstReviewDate;
        else delete btn.dataset.garsoFirstReviewDate;
        if (lastReviewDate) btn.dataset.garsoLastReviewDate = lastReviewDate;
        else delete btn.dataset.garsoLastReviewDate;
        if (firstReviewUrl) btn.dataset.garsoFirstReviewUrl = firstReviewUrl;
        else delete btn.dataset.garsoFirstReviewUrl;
        if (lastReviewUrl) btn.dataset.garsoLastReviewUrl = lastReviewUrl;
        else delete btn.dataset.garsoLastReviewUrl;
        setGarsoSummaryState(
            btn,
            'ready',
            summaryToHtml(result),
            { checkedAt, cacheState }
        );
    }

    // ============================================================
    // ESCORTI - PORÓWNANIE TELEFONU I LINKU + AKTYWNE ANONSE
    // ============================================================

    function normalizeEscortCity(value) {
        return String(value || '')
            .replace(/\s+/g, ' ')
            .replace(/^[\s,;|\-–—]+|[\s,;|\-–—]+$/g, '')
            .trim();
    }

    function normalizeEscortCityKey(value) {
        return normalizeEscortCity(value)
            .normalize('NFKD')
            .replace(/\p{M}/gu, '')
            .toLocaleLowerCase('pl-PL');
    }

    function extractEscortClubCity(doc) {
        if (!doc) return null;

        // Miasto identyfikujemy wyłącznie po linku prowadzącym dokładnie do
        // /anonse/towarzyskie/MIASTO/ - nie po dzielnicy ani nieklikalnym dopisku.
        const locationLinks = [...doc.querySelectorAll('.content-location a[href]')];

        for (const link of locationLinks) {
            try {
                const url = new URL(link.getAttribute('href'), 'https://pl.escort.club/');
                if (!/^\/anonse\/towarzyskie\/[^/]+\/?$/i.test(url.pathname)) continue;

                const city = normalizeEscortCity(link.textContent);
                if (city) return city;
            } catch (_) {}
        }

        // Brak jednoznacznego linku miasta = miasto nieznane. Celowo nie używamy
        // dzielnicy ani końcowej uwagi tekstowej jako zamiennika miasta.
        return null;
    }

    function buildEscortClubAdDocumentResult(html, status, finalUrl, transport) {
        const text = String(html || '');
        const numericStatus = Number(status) || 0;
        return {
            ok: !!text && (numericStatus === 0 || (numericStatus >= 200 && numericStatus < 400)),
            status: numericStatus,
            finalUrl,
            transport,
            doc: new DOMParser().parseFromString(text, 'text/html')
        };
    }

    function isUsableEscortClubAdDocument(result, requestedUrl) {
        if (!result?.ok || !result.doc) return false;

        // Nieaktywne anons jest przekierowywane przez Escort.club na
        // stronę główną. To jest poprawna, rozstrzygająca odpowiedź, mimo że
        // dokument strony głównej nie zawiera nagłówka pojedynczego anonsu.
        try {
            const requestedId = parseAdIdFromUrl(requestedUrl);
            const finalUrl = new URL(result.finalUrl || requestedUrl, requestedUrl);
            if (
                requestedId &&
                finalUrl.hostname === 'pl.escort.club' &&
                finalUrl.pathname === '/'
            ) {
                return true;
            }
        } catch (_) {}

        const requestedId = parseAdIdFromUrl(requestedUrl);
        const finalId = parseAdIdFromUrl(result.finalUrl || requestedUrl);
        if (requestedId && finalId && requestedId !== finalId) return false;

        const title = result.doc.querySelector(
            '.content-name h1, .content-name.-desc-name h1'
        );
        const bodyText = normalizeEscortAdText(result.doc.body?.textContent);
        return !!title && !/^site unavailable\b/i.test(bodyText);
    }

    async function fetchEscortClubAdDocumentViaPage(url, cancelToken = null) {
        if (typeof unsafeWindow === 'undefined' || typeof unsafeWindow.fetch !== 'function') {
            throw new Error('Brak dostępu do fetch strony');
        }

        const AbortControllerClass = unsafeWindow.AbortController || AbortController;
        const controller = new AbortControllerClass();
        const timer = setTimeout(() => controller.abort(), ESCORT_AD_DATA_FETCH_TIMEOUT_MS);
        const unsubscribeCancel = cancelToken?.onCancel(() => controller.abort()) || (() => {});

        try {
            cancelToken?.throwIfCancelled();
            recordDiagnosticRequest(url, 'GET');
            const response = await unsafeWindow.fetch(url, {
                method: 'GET',
                credentials: 'include',
                redirect: 'follow',
                cache: 'no-store',
                signal: controller.signal,
                headers: {
                    Accept: 'text/html,application/xhtml+xml'
                }
            });
            return buildEscortClubAdDocumentResult(
                await response.text(),
                response.status,
                response.url || url,
                'page-fetch'
            );
        } finally {
            clearTimeout(timer);
            unsubscribeCancel();
        }
    }

    async function fetchEscortClubAdDocumentViaGm(url, cancelToken = null) {
        const response = await gmRequest({
            method: 'GET',
            url,
            cancelToken,
            timeout: ESCORT_AD_DATA_FETCH_TIMEOUT_MS,
            responseType: 'text',
            overrideMimeType: 'text/html; charset=utf-8',
            anonymous: false,
            headers: {
                Accept: 'text/html,application/xhtml+xml',
                Referer: location.href
            }
        });

        return buildEscortClubAdDocumentResult(
            response.responseText || response.response || '',
            response.status,
            response.finalUrl || url,
            'gm-xhr'
        );
    }

    function fetchEscortClubAdDocumentViaIframe(url, cancelToken = null) {
        return new Promise((resolve, reject) => {
            const iframe = makeElement('iframe');
            let finished = false;
            let timer = null;
            let unsubscribeCancel = () => {};

            const cleanup = () => {
                if (timer) clearTimeout(timer);
                unsubscribeCancel();
                try { iframe.remove(); } catch (_) {}
            };

            const finish = (callback, value) => {
                if (finished) return;
                finished = true;
                cleanup();
                callback(value);
            };

            iframe.setAttribute('sandbox', 'allow-same-origin');
            iframe.setAttribute('aria-hidden', 'true');
            iframe.tabIndex = -1;
            Object.assign(iframe.style, {
                position: 'fixed',
                left: '-10000px',
                top: '-10000px',
                width: '1px',
                height: '1px',
                opacity: '0',
                pointerEvents: 'none',
                border: '0',
                zIndex: '-999999'
            });

            iframe.addEventListener('load', () => {
                try {
                    const sourceDoc = iframe.contentDocument;
                    const html = sourceDoc?.documentElement?.outerHTML || '';
                    const finalUrl = iframe.contentWindow?.location?.href || url;
                    const result = buildEscortClubAdDocumentResult(
                        html,
                        html ? 200 : 0,
                        finalUrl,
                        'iframe'
                    );
                    finish(resolve, result);
                } catch (error) {
                    finish(reject, error);
                }
            }, { once: true });

            iframe.addEventListener('error', () => {
                finish(reject, new Error('Błąd nawigacji iframe'));
            }, { once: true });

            timer = setTimeout(() => {
                finish(reject, new Error('Przekroczono czas iframe'));
            }, ESCORT_AD_DATA_FETCH_TIMEOUT_MS);

            unsubscribeCancel = cancelToken?.onCancel(reason => {
                finish(reject, createOperationCancelledError(reason));
            }) || (() => {});

            iframe.src = url;
            document.body.appendChild(iframe);
        });
    }

    async function fetchEscortClubAdDocument(url, cancelToken = null) {
        const errors = [];
        const transports = location.hostname.toLowerCase() === 'pl.escort.club'
            ? [
                ['page-fetch', fetchEscortClubAdDocumentViaPage],
                ['gm-xhr', fetchEscortClubAdDocumentViaGm],
                ['iframe', fetchEscortClubAdDocumentViaIframe]
            ]
            : [
                // Z Escorti.pl zwykły fetch jest żądaniem między domenami.
                // GM XHR ma właściwe @connect i powinien zostać użyty od razu.
                ['gm-xhr', fetchEscortClubAdDocumentViaGm],
                ['page-fetch', fetchEscortClubAdDocumentViaPage],
                ['iframe', fetchEscortClubAdDocumentViaIframe]
            ];

        for (const [name, loader] of transports) {
            try {
                cancelToken?.throwIfCancelled();
                const result = await loader(url, cancelToken);
                if (isUsableEscortClubAdDocument(result, url)) return result;
                errors.push(`${name}: nieprawidłowa odpowiedź (HTTP ${result?.status || 0})`);
            } catch (error) {
                if (isOperationCancelledError(error) || cancelToken?.cancelled) {
                    throw createOperationCancelledError(
                        cancelToken?.reason || error?.message
                    );
                }
                errors.push(`${name}: ${error?.message || String(error)}`);
            }
        }

        throw new Error(errors.join(' | '));
    }

    async function inspectEscortClubAd(url) {
        const requestedId = parseAdIdFromUrl(url);
        const currentId = getAdIdFromUrl();

        if (requestedId && currentId && requestedId === currentId) {
            const adData = extractEscortClubAdData(document, requestedId, url);
            return {
                url,
                active: true,
                city: adData.location?.city || extractEscortClubCity(document),
                imageUrl: extractWatchPageImage(document, url),
                adData,
                source: 'current'
            };
        }

        try {
            const fetched = await fetchEscortClubAdDocument(url);
            if (!fetched.ok) {
                return { url, active: false, city: null, status: fetched.status };
            }

            // Nieaktywny anons Escort.club przekierowuje na stronę główną.
            // Aktywny jest tylko wtedy, gdy po wszystkich przekierowaniach nadal
            // jesteśmy na dokładnie tym samym /anons/<ID>.html.
            let finalUrl;
            try {
                finalUrl = new URL(fetched.finalUrl, url);
            } catch (_) {
                return { url, active: false, city: null, status: fetched.status };
            }

            const finalId = parseAdIdFromUrl(finalUrl.href);
            const exactAdPath = /^\/anons\/\d+\.html\/?$/i.test(finalUrl.pathname);
            if (
                finalUrl.hostname !== 'pl.escort.club' ||
                !exactAdPath ||
                !requestedId ||
                !finalId ||
                finalId !== requestedId
            ) {
                return { url, active: false, city: null, status: fetched.status };
            }

            const adData = extractEscortClubAdData(fetched.doc, requestedId, url);
            return {
                url,
                active: true,
                city: adData.location?.city || extractEscortClubCity(fetched.doc),
                imageUrl: extractWatchPageImage(fetched.doc, url),
                adData,
                status: fetched.status
            };
        } catch (error) {
            return {
                url,
                active: false,
                city: null,
                error: error?.message || String(error),
                unknown: true
            };
        }
    }


    async function inspectEscortClubAdActivityOnly(url) {
        const requestedId = parseAdIdFromUrl(url);
        const currentId = getAdIdFromUrl();

        if (requestedId && currentId && requestedId === currentId) {
            return {
                url,
                active: true,
                city: null,
                imageUrl: '',
                source: 'current-head'
            };
        }

        // Do samego online/offline nie potrzebujemy kilkuset KB HTML. Escort.club
        // rozstrzyga stan już samym przekierowaniem: aktywny pozostaje na
        // /anons/<ID>.html, nieaktywny trafia na stronę główną.
        try {
            const response = await gmRequest({
                method: 'HEAD',
                url,
                timeout: ESCORT_AD_DATA_FETCH_TIMEOUT_MS,
                anonymous: false,
                headers: {
                    Accept: 'text/html,application/xhtml+xml',
                    Referer: location.href
                }
            });
            const status = Number(response?.status) || 0;
            const finalUrl = new URL(response?.finalUrl || url, url);
            const finalId = parseAdIdFromUrl(finalUrl.href);
            const exactAdPath = /^\/anons\/\d+\.html\/?$/i.test(finalUrl.pathname);

            if (
                finalUrl.hostname === 'pl.escort.club' &&
                exactAdPath &&
                requestedId &&
                finalId === requestedId
            ) {
                return {
                    url,
                    active: true,
                    city: null,
                    imageUrl: '',
                    status,
                    source: 'head'
                };
            }

            if (
                finalUrl.hostname === 'pl.escort.club' &&
                finalUrl.pathname === '/'
            ) {
                return {
                    url,
                    active: false,
                    city: null,
                    status,
                    source: 'head'
                };
            }
        } catch (_) {
            // HEAD jest tylko szybką ścieżką. Przy nietypowej odpowiedzi lub
            // braku obsługi metody wracamy do pełnego, dotychczasowego GET.
        }

        return inspectEscortClubAd(url);
    }

    function activeAdsCacheKey(adUrls) {
        return [...new Set(adUrls || [])]
            .map(url => normalizeEscortiAdUrl(url) || url)
            .filter(Boolean)
            .sort()
            .join('|');
    }

    function escortiActivityAdIdsKey(adUrls) {
        return [...new Set(
            (adUrls || [])
                .map(parseAdIdFromUrl)
                .filter(Boolean)
        )]
            .sort((a, b) => Number(a) - Number(b))
            .join(',');
    }

    function summarizeActiveAdResults(results) {
        const active = results.filter(item => item.active);
        const unknown = results.filter(item => item.unknown).length;
        const knownCities = active.map(item => normalizeEscortCity(item.city)).filter(Boolean);

        const uniqueCities = [...new Set(
            knownCities.map(city => normalizeEscortCityKey(city))
        )].map(key => knownCities.find(city => normalizeEscortCityKey(city) === key));

        const allActiveCitiesKnown = active.length > 0 && knownCities.length === active.length;

        let cityText = '';
        if (active.length > 0) {
            if (allActiveCitiesKnown && uniqueCities.length === 1) cityText = uniqueCities[0];
            else if (uniqueCities.length > 1) cityText = 'różne miasta';
            else cityText = 'miasto ?';
        }

        return {
            totalChecked: results.length,
            activeCount: active.length,
            unknownCount: unknown,
            cities: uniqueCities,
            allActiveCitiesKnown,
            cityText,
            results
        };
    }

    function compactEscortiActivitySummary(adUrls, summary) {
        return {
            adIdsKey: escortiActivityAdIdsKey(adUrls),
            totalChecked: Number(summary?.totalChecked) || 0,
            activeCount: Number(summary?.activeCount) || 0,
            unknownCount: Number(summary?.unknownCount) || 0,
            cities: Array.isArray(summary?.cities) ? [...summary.cities] : [],
            allActiveCitiesKnown: !!summary?.allActiveCitiesKnown,
            cityText: String(summary?.cityText || ''),
            checkedAt: Date.now()
        };
    }

    function getCachedEscortiActivitySummary(adId, searchMode, adUrls = null) {
        if (!SETTINGS.usePersistentCache || !adId || !searchMode) return null;

        const stored = getStoredEscortAdData(adId);
        const summary = stored?.escortiActivity?.[searchMode];
        if (!summary || typeof summary !== 'object') return null;
        if (
            Array.isArray(adUrls) &&
            summary.adIdsKey !== escortiActivityAdIdsKey(adUrls)
        ) {
            return null;
        }
        if (
            !Number.isFinite(Number(summary.totalChecked)) ||
            !Number.isFinite(Number(summary.activeCount)) ||
            !Number.isFinite(Number(summary.unknownCount))
        ) {
            return null;
        }

        return {
            ...summary,
            totalChecked: Number(summary.totalChecked),
            activeCount: Number(summary.activeCount),
            unknownCount: Number(summary.unknownCount),
            cities: Array.isArray(summary.cities) ? summary.cities : [],
            allActiveCitiesKnown: !!summary.allActiveCitiesKnown,
            cityText: String(summary.cityText || '')
        };
    }

    function getLatestCachedEscortiActivitySummary(adId, searchModes) {
        return (Array.isArray(searchModes) ? searchModes : [searchModes])
            .map(mode => getCachedEscortiActivitySummary(adId, mode))
            .filter(Boolean)
            .sort((a, b) => Number(b.checkedAt || 0) - Number(a.checkedAt || 0))[0] || null;
    }

    function getEscortiAdUrlsFromActivitySummary(summary) {
        return [...new Set(
            String(summary?.adIdsKey || '')
                .split(',')
                .map(value => value.trim())
                .filter(value => /^\d+$/.test(value))
                .map(id => `https://pl.escort.club/anons/${id}.html`)
        )];
    }

    function recoverEscortiResultAdUrls(result, adId, searchMode) {
        if (result?.status !== 'ok' || Number(result?.profiles) === 0) return [];

        const normalizeUrls = values => [...new Set(
            (Array.isArray(values) ? values : [])
                .map(url => normalizeEscortiAdUrl(url))
                .filter(Boolean)
        )];
        const directUrls = normalizeUrls([
            ...(Array.isArray(result.adUrls) ? result.adUrls : []),
            ...(Array.isArray(result.adIds) ? result.adIds : [])
                .map(id => /^\d+$/.test(String(id || '').trim())
                    ? `https://pl.escort.club/anons/${String(id).trim()}.html`
                    : null)
        ]);
        const activitySummary = getCachedEscortiActivitySummary(
            adId,
            searchMode
        );
        const activityUrls = normalizeUrls(
            getEscortiAdUrlsFromActivitySummary(activitySummary)
        );
        const profileCacheUrls = normalizeUrls(
            getCachedEscortiAdUrlsForProfiles(normalizeProfileUrls(result))
        );
        const combinedUrls = normalizeUrls([
            ...directUrls,
            ...activityUrls,
            ...profileCacheUrls
        ]);
        const reportedCount = Number(result.adLinks);
        const profileSummaries = Array.isArray(result.profileSummaries)
            ? result.profileSummaries
            : [];
        const singleProfileCount = profileSummaries.length === 1
            ? Number(profileSummaries[0]?.adLinks)
            : NaN;
        const activityCount = Number(activitySummary?.totalChecked);
        const expectedCount = Number.isFinite(reportedCount) && reportedCount > 0
            ? reportedCount
            : (Number.isFinite(singleProfileCount) && singleProfileCount > 0
                ? singleProfileCount
                : (Number.isFinite(activityCount) && activityCount > 0
                    ? activityCount
                    : reportedCount));

        if (Number.isFinite(expectedCount) && expectedCount >= 0) {
            // Nie mieszamy starej listy z nowym wynikiem tylko dlatego, że jest
            // dostępna w cache. Odtwarzamy adresy wyłącznie wtedy, gdy liczba
            // identyfikatorów zgadza się z bieżącym wynikiem Escorti.pl.
            const exactMatch = [
                directUrls,
                activityUrls,
                profileCacheUrls,
                combinedUrls
            ].find(urls => urls.length === expectedCount);
            if (exactMatch) return exactMatch;
            return directUrls;
        }

        return directUrls.length
            ? directUrls
            : (activityUrls.length ? activityUrls : profileCacheUrls);
    }

    function hydrateEscortiResultAdUrls(result, adId, searchMode) {
        if (result?.status !== 'ok') return result;

        const adUrls = recoverEscortiResultAdUrls(result, adId, searchMode);
        if (!adUrls.length) return result;

        const currentUrls = (Array.isArray(result.adUrls) ? result.adUrls : [])
            .map(url => normalizeEscortiAdUrl(url))
            .filter(Boolean);
        const unchanged = currentUrls.length === adUrls.length &&
            currentUrls.every((url, index) => url === adUrls[index]);
        if (unchanged) return result;

        return {
            ...result,
            adIds: adUrls.map(parseAdIdFromUrl).filter(Boolean),
            adUrls,
            adLinks: adUrls.length
        };
    }

    function saveEscortiActivitySummary(adId, searchModes, adUrls, summary) {
        if (!SETTINGS.usePersistentCache || !adId || !summary) return;

        const modes = [...new Set(
            (Array.isArray(searchModes) ? searchModes : [searchModes]).filter(Boolean)
        )];
        if (!modes.length) return;

        const entry = compactEscortiActivitySummary(adUrls, summary);
        const activityPatch = Object.fromEntries(
            modes.map(mode => [mode, { ...entry, cities: [...entry.cities] }])
        );
        const cacheId = String(adId);
        const stored = getStoredEscortAdData(adId);

        // Przy pierwszej wizycie dane strony anonsu mogą zapisać się chwilę
        // później niż wynik kontroli linków. Trzymamy wtedy małą poprawkę w pamięci
        // i dokładamy ją przy właściwym zapisie cache anonsu.
        if (!stored) {
            pendingEscortiActivityCacheWrites.set(cacheId, {
                ...(pendingEscortiActivityCacheWrites.get(cacheId) || {}),
                ...activityPatch
            });
            return;
        }

        try {
            const escortiActivity = {
                ...(stored.escortiActivity || {}),
                ...activityPatch
            };
            const value = { ...stored, escortiActivity };

            // Nie zmieniamy głównego checkedAt: kontrola aktywności linków nie
            // odświeża opisu, cen ani pozostałych danych Escort.club.
            writePersistentCacheValue(escortAdDataCacheKey(adId), value);

            const memoryValue = escortAdDataMemoryCache.get(cacheId);
            if (memoryValue) {
                escortAdDataMemoryCache.set(cacheId, {
                    ...memoryValue,
                    escortiActivity
                });
            }
        } catch (error) {
            log(`Nie udało się zapisać aktywności Escorti dla anonsu ${adId}`, error);
        }
    }

    function scanActiveEscortAds(
        adUrls,
        onProgress = null,
        forceRefresh = false,
        options = {}
    ) {
        const activityOnly = options?.activityOnly === true;
        const urls = [...new Set(
            (adUrls || [])
                .map(url => normalizeEscortiAdUrl(url) || url)
                .filter(Boolean)
        )];

        const baseKey = activeAdsCacheKey(urls);
        const key = baseKey ? `${activityOnly ? 'activity:' : 'details:'}${baseKey}` : '';
        if (!forceRefresh && key && escortActiveAdsSummaryCache.has(key)) {
            return escortActiveAdsSummaryCache.get(key);
        }

        if (forceRefresh && key) escortActiveAdsSummaryCache.delete(key);

        const promise = (async () => {
            const results = new Array(urls.length);
            let nextIndex = 0;
            let completed = 0;

            async function worker() {
                while (true) {
                    const index = nextIndex++;
                    if (index >= urls.length) return;

                    const url = urls[index];
                    const checkCache = activityOnly
                        ? escortActiveAdProbeCache
                        : escortActiveAdCheckCache;
                    let checkPromise = forceRefresh
                        ? null
                        : checkCache.get(url);
                    if (!checkPromise) {
                        checkPromise = activityOnly
                            ? inspectEscortClubAdActivityOnly(url)
                            : inspectEscortClubAd(url);
                        checkCache.set(url, checkPromise);
                    }

                    results[index] = await checkPromise;
                    completed++;

                    if (onProgress) {
                        try {
                            onProgress({
                                checked: completed,
                                total: urls.length,
                                activeCount: results.filter(Boolean).filter(item => item.active).length,
                                url,
                                result: results[index]
                            });
                        } catch (_) {}
                    }
                }
            }

            const workerCount = Math.min(
                activityOnly
                    ? ESCORT_ACTIVE_PROBE_CONCURRENT
                    : ESCORT_ACTIVE_CHECK_CONCURRENT,
                Math.max(1, urls.length)
            );
            const workers = [];
            for (let workerIndex = 0; workerIndex < workerCount; workerIndex++) {
                workers.push(worker());
            }
            await Promise.all(workers);

            return summarizeActiveAdResults(results.filter(Boolean));
        })();

        if (key) escortActiveAdsSummaryCache.set(key, promise);
        promise.catch(() => {
            if (key) escortActiveAdsSummaryCache.delete(key);
        });
        return promise;
    }

    function buildActiveAdsTitle(baseTitle, detailResult, summary) {
        const lines = [baseTitle];
        if (typeof detailResult?.adLinks === 'number') {
            lines.push(`Anonse znalezione w profilu Escorti: ${detailResult.adLinks}.`);
        }
        lines.push(`Aktywne na Escort.club: ${summary.activeCount} z ${summary.totalChecked}.`);

        if (summary.activeCount > 0) {
            if (summary.allActiveCitiesKnown && summary.cities.length === 1) {
                lines.push(`Wszystkie aktywne anonse są z miasta: ${summary.cities[0]}.`);
            } else if (summary.cities.length > 1) {
                lines.push(`Aktywne anonse są z różnych miast: ${summary.cities.join(', ')}.`);
            } else {
                lines.push('Nie udało się jednoznacznie ustalić miasta wszystkich aktywnych anonsów.');
            }
        }

        return lines.join('\n');
    }

    async function getEscortiDetailForSingleAd(
        adId,
        searchValue,
        searchMode,
        forceRefresh = false
    ) {
        const rawCached = getListCache(adId, searchMode);
        const cached = hydrateEscortiResultAdUrls(
            rawCached,
            adId,
            searchMode
        );
        if (cached && cached !== rawCached) {
            // Napraw również sam wpis, aby kolejne wejście nie musiało ponownie
            // odtwarzać adresów z cache kontroli aktywności.
            setListCache(adId, cached, searchMode);
        }
        const cacheHasProfileSummary = cached?.profiles === 0 || (
            Array.isArray(cached?.profileSummaries) &&
            cached.profileSummaries.length > 0
        );
        if (
            !forceRefresh &&
            isListCacheFresh(cached) &&
            cacheHasProfileSummary
        ) {
            return { result: cached, source: 'cache' };
        }

        const refreshed = await checkEscortiBackground(searchValue, {
            timeoutMs: 60000,
            mergeProfiles: true,
            preferDirect: true
        });

        if (refreshed?.status === 'ok') {
            const hydratedRefreshed = hydrateEscortiResultAdUrls(
                refreshed,
                adId,
                searchMode
            );
            setListCache(adId, hydratedRefreshed, searchMode);
            return {
                result: getListCache(adId, searchMode) || hydratedRefreshed,
                source: 'network'
            };
        }

        if (cached?.status === 'ok') {
            return { result: cached, source: 'stale-cache' };
        }

        return { result: refreshed, source: 'network' };
    }

    // ============================================================
    // PRZYCISKI
    // ============================================================

    function styleButton(btn) {
        Object.assign(btn.style, {
            display:'block', width:'100%', marginTop:'10px', padding:'8px', color:'#fff', border:'none',
            borderRadius:'4px', cursor:'pointer', fontWeight:'bold', transition:'background-color .2s ease'
        });
        setButtonColor(btn, 'checking');
    }

    function createGarsoButton(
        displayTerm,
        searchTerm,
        searchMode,
        compact = false,
        deferAutoCheck = false
    ) {
        const btn = makeElement('button');
        if (compact) btn.classList.add('vm-research-panel-row');
        btn.dataset.garsoDisplayTerm = displayTerm;
        btn.dataset.garsoSearchMode = searchMode;
        const cached = getGarsoSearchCountCache(searchMode, searchTerm);
        if (cached) btn.dataset.garsoCountCheckedAt = String(cached.checkedAt);
        if (cached?.fresh) {
            btn.dataset.garsoStatus = 'result';
            btn.dataset.garsoCount = String(cached.count);
            btn.dataset.garsoCacheState = 'fresh';
        } else {
            btn.dataset.garsoStatus = SETTINGS.autoGarsoCheck ? 'checking' : 'manual';
        }
        styleButton(btn);
        renderGarsoButton(btn);
        if (!deferAutoCheck) {
            restoreGarsoSummaryFromCache(btn, searchTerm, searchMode);
        }

        btn.addEventListener('click', e => {
            e.preventDefault();
            e.stopPropagation();

            if (btn.dataset.garsoStatus === 'manual') {
                updateGarsoStatus(btn, searchTerm, searchMode, false, false);
            }
            startSearchProcess(searchTerm, btn);
        });

        if (SETTINGS.autoGarsoCheck && !deferAutoCheck) {
            updateGarsoStatus(
                btn,
                searchTerm,
                searchMode,
                false,
                normalizeAutoGarsoAnalysisMode(
                    SETTINGS.autoGarsoAnalysisMode
                ) === 'content'
            );
        }
        return btn;
    }

    function formatResearchPanelList(values, maxItems = 5) {
        const clean = [...new Set((values || []).filter(Boolean))];
        if (clean.length <= maxItems) return clean.join(', ');
        return `${clean.slice(0, maxItems).join(', ')} (+${clean.length - maxItems})`;
    }

    function formatResearchPriceDuration(durationKey) {
        if (durationKey === 'night') return 'cała noc';
        const minutes = Number(durationKey);
        if (!Number.isFinite(minutes) || minutes <= 0) return String(durationKey);
        if (minutes < 60) return `${minutes} min`;
        const hours = minutes / 60;
        return `${String(hours).replace('.', ',')} h`;
    }

    function getEscortActiveAdsConsistencyWarnings(summary) {
        const active = (summary?.results || [])
            .filter(item => item?.active && item.adData)
            .map(item => ({ item, data: item.adData }));
        if (active.length < 2) return [];

        const warnings = [];
        const missing = '__brak__';
        const displayValue = value => value === missing ? 'brak' : value;
        const uniqueComparedValues = values => [...new Set(values)];

        const cities = uniqueComparedValues(active.map(({ item, data }) => {
            const city = normalizeEscortCity(data.location?.city || item.city);
            return city || missing;
        }));
        if (cities.length > 1) {
            warnings.push(`Różne miasta: ${formatResearchPanelList(cities.map(displayValue))}`);
        }

        const ages = uniqueComparedValues(active.map(({ data }) => {
            const entry = getEscortObjectEntryByNormalizedKey(data.stats, 'Wiek');
            const match = normalizeEscortAdText(entry?.[1]).match(/\d{1,3}/);
            return match ? match[0] : missing;
        }));
        if (ages.length > 1) {
            warnings.push(`Różny wiek: ${formatResearchPanelList(ages.map(displayValue))}`);
        }

        const statLabels = new Map();
        for (const { data } of active) {
            for (const rawLabel of Object.keys(data.stats || {})) {
                const key = normalizeEscortChangeComparisonValue(rawLabel).replace(/:\s*$/, '');
                if (!key || key === 'wiek' || normalizeEscortClubPriceDurationKey(key)) continue;
                if (!statLabels.has(key)) statLabels.set(key, normalizeEscortAdText(rawLabel));
            }
        }

        const differentStats = [];
        for (const [key, label] of statLabels) {
            const rawValues = active.map(({ data }) => {
                const entry = Object.entries(data.stats || {}).find(([candidate]) =>
                    normalizeEscortChangeComparisonValue(candidate).replace(/:\s*$/, '') === key
                );
                return entry ? (normalizeEscortAdText(entry[1]) || 'brak') : 'brak';
            });
            const comparedValues = rawValues.map(value =>
                normalizeEscortChangeComparisonValue(value) || missing
            );
            if (uniqueComparedValues(comparedValues).length > 1) {
                differentStats.push(
                    `${label} (${formatResearchPanelList(rawValues)})`
                );
            }
        }
        if (differentStats.length) {
            warnings.push(`Różne parametry: ${formatResearchPanelList(differentStats)}`);
        }

        const priceDurationKeys = new Set(
            active.flatMap(({ data }) => Object.keys(data.prices || {}))
        );
        const differentPrices = [];
        for (const durationKey of priceDurationKeys) {
            const comparedValues = [];
            const displayedValues = [];
            for (const { data } of active) {
                const prices = data.prices || {};
                if (!Object.prototype.hasOwnProperty.call(prices, durationKey)) {
                    comparedValues.push('__brak_pozycji__');
                    displayedValues.push('brak pozycji');
                    continue;
                }
                const price = prices[durationKey];
                const amount = price?.amount != null && Number.isFinite(Number(price.amount))
                    ? String(Number(price.amount))
                    : '__brak_ceny__';
                const currency = normalizeEscortAdText(price?.currency).toUpperCase();
                comparedValues.push(`${amount}|${currency}`);
                displayedValues.push(formatEscortTilePrice(price) || 'brak ceny');
            }
            if (uniqueComparedValues(comparedValues).length > 1) {
                differentPrices.push(
                    `${formatResearchPriceDuration(durationKey)} ` +
                    `(${formatResearchPanelList(displayedValues)})`
                );
            }
        }
        if (differentPrices.length) {
            warnings.push(`Różne ceny: ${formatResearchPanelList(differentPrices)}`);
        }

        const availabilityDays = new Map();
        for (const { data } of active) {
            for (const day of Object.keys(data.availability || {})) {
                const key = normalizeEscortChangeComparisonValue(day).replace(/:\s*$/, '');
                if (key && !availabilityDays.has(key)) {
                    availabilityDays.set(key, normalizeEscortAdText(day).replace(/:\s*$/, ''));
                }
            }
        }
        const differentAvailability = [];
        for (const [dayKey, dayLabel] of availabilityDays) {
            const values = active.map(({ data }) => {
                const entry = Object.entries(data.availability || {}).find(([day]) =>
                    normalizeEscortChangeComparisonValue(day).replace(/:\s*$/, '') === dayKey
                );
                return entry
                    ? (normalizeEscortChangeComparisonValue(entry[1]) || missing)
                    : missing;
            });
            if (uniqueComparedValues(values).length > 1) {
                differentAvailability.push(dayLabel);
            }
        }
        if (differentAvailability.length) {
            warnings.push(
                `Różne godziny przyjmowania: ${formatResearchPanelList(differentAvailability)}`
            );
        }

        const tagSets = active.map(({ data }) => new Map(
            (Array.isArray(data.tags) ? data.tags : []).map(tag => [
                normalizeEscortChangeComparisonValue(tag),
                normalizeEscortAdText(tag)
            ])
        ));
        const tagSignatures = tagSets.map(tags => [...tags.keys()].sort().join('|'));
        if (uniqueComparedValues(tagSignatures).length > 1) {
            const tagCounts = new Map();
            const tagLabels = new Map();
            for (const tags of tagSets) {
                for (const [key, label] of tags) {
                    tagCounts.set(key, (tagCounts.get(key) || 0) + 1);
                    if (!tagLabels.has(key)) tagLabels.set(key, label);
                }
            }
            const differingTags = [...tagCounts]
                .filter(([, count]) => count !== tagSets.length)
                .map(([key]) => tagLabels.get(key));
            warnings.push(
                `Różne tagi${differingTags.length ? `: ${formatResearchPanelList(differingTags)}` : ''}`
            );
        }

        return warnings;
    }

    function styleResearchPanelRow(row) {
        Object.assign(row.style, {
            display: 'flex',
            alignItems: 'center',
            width: '100%',
            minHeight: '42px',
            margin: '0',
            padding: '9px 11px',
            border: '0',
            borderBottom: '1px solid rgba(255,255,255,.1)',
            borderRadius: '0',
            background: 'transparent',
            color: '#fff',
            cursor: 'pointer',
            fontSize: '13px',
            fontWeight: '600',
            boxSizing: 'border-box',
            transition: 'background-color .15s ease'
        });
        row.addEventListener('mouseenter', () => {
            row.style.backgroundColor = 'rgba(255,255,255,.055)';
        });
        row.addEventListener('mouseleave', () => {
            row.style.backgroundColor = 'transparent';
        });
    }

    function buildEscortClubPhoneSearchUrl(phone) {
        const query = normalizeEscortAdText(phone)
            .replace(/^\+/, '')
            .replace(/[^\d]+/g, ' ')
            .trim();
        const params = new URLSearchParams({ province: '', q: query });
        return `https://pl.escort.club/anonse/towarzyskie/poland/?${params}`;
    }

    function buildEscortiPhoneSearchUrl(phone) {
        const digits = normalizeEscortCachedPhone(phone);
        return digits
            ? `${ESCORTI_BASE_URL}search?search=${encodeURIComponent(digits)}`
            : '';
    }

    function initEscortTopPhoneSearch() {
        if (location.hostname.toLowerCase() !== 'pl.escort.club') return;

        const mount = () => {
            if (!document.body) return false;

            // To pole zajmowało miejsce w tym samym wierszu nagłówka, ale nie
            // wnosiło żadnej funkcji. Wyszukiwarka wykorzystuje odzyskaną szerokość.
            document.querySelector('.site-title-col.col')?.remove();

            const host = document.querySelector('.ucp-col.col');
            const existing = document.getElementById(ESCORT_TOP_PHONE_SEARCH_ID);
            if (!SETTINGS.showTopPhoneSearch) {
                existing?._vmHeaderStyleObserver?.disconnect();
                existing?.remove();
                return !!host;
            }
            if (!host) return false;
            if (existing?.parentElement === host) {
                existing._vmSyncNativeHeaderStyle?.();
                return true;
            }
            existing?._vmHeaderStyleObserver?.disconnect();
            existing?.remove();

            Object.assign(host.style, {
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'flex-end',
                flexWrap: 'wrap',
                gap: '7px',
                flex: '1 1 auto'
            });

            const root = makeElement('div');
            root.id = ESCORT_TOP_PHONE_SEARCH_ID;
            root.setAttribute(
                'aria-label',
                'Wyszukiwanie anonsów według numeru telefonu'
            );
            Object.assign(root.style, {
                display: 'flex',
                alignItems: 'center',
                flex: '0 1 340px',
                width: '340px',
                minWidth: '280px',
                maxWidth: '100%',
                marginRight: '10px',
                boxSizing: 'border-box',
                fontFamily: 'Lato, Arial, sans-serif'
            });

            const form = makeElement('form');
            form.setAttribute(
                'aria-label',
                'Wyszukaj numer telefonu na Escort.club lub Escorti.pl'
            );
            Object.assign(form.style, {
                display: 'flex',
                alignItems: 'stretch',
                gap: '0',
                width: '100%',
                height: '48px',
                minWidth: '0',
                margin: '0',
                overflow: 'hidden',
                border: '1px solid rgba(245,77,163,.42)',
                borderRadius: '6px',
                background: '#fff',
                boxShadow: 'none',
                boxSizing: 'border-box',
                transition: 'border-color .16s ease, box-shadow .16s ease'
            });

            const searchIcon = makeElement('span');
            searchIcon.innerHTML = '<svg viewBox="0 0 24 24" width="17" height="17" focusable="false"><path fill="currentColor" d="M6.62 10.79a15.46 15.46 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.61 21 3 13.39 3 4a1 1 0 0 1 1-1h3.5a1 1 0 0 1 1 1c0 1.25.2 2.45.57 3.57a1 1 0 0 1-.25 1.02l-2.2 2.2Z"/></svg>';
            searchIcon.setAttribute('aria-hidden', 'true');
            Object.assign(searchIcon.style, {
                display: 'inline-flex',
                alignItems: 'center',
                justifyContent: 'center',
                flex: '0 0 40px',
                width: '40px',
                color: getEscortPagePinkColor(),
                background: '#ffffff',
                borderRight: '1px solid rgba(245,77,163,.22)',
                fontSize: '16px',
                lineHeight: '1',
                userSelect: 'none'
            });

            const input = makeElement('input');
            input.id = `${ESCORT_TOP_PHONE_SEARCH_ID}-input`;
            input.type = 'tel';
            input.inputMode = 'tel';
            input.autocomplete = 'tel';
            input.placeholder = 'Numer telefonu';
            input.title = 'Wpisz numer telefonu';
            input.setAttribute('aria-label', 'Numer telefonu do wyszukania');
            Object.assign(input.style, {
                flex: '1 1 160px',
                minWidth: '72px',
                height: '46px',
                padding: '6px 10px',
                border: '0',
                borderRadius: '0',
                outline: 'none',
                background: '#fff',
                color: '#2e2130',
                fontSize: '13px',
                lineHeight: '1',
                boxSizing: 'border-box'
            });
            input.addEventListener('input', () => input.setCustomValidity(''));
            input.addEventListener('focus', () => {
                form.style.borderColor = getEscortPagePinkColor();
                form.style.boxShadow = '0 0 0 2px rgba(245,77,163,.13)';
            });
            input.addEventListener('blur', () => {
                form.style.borderColor = root._vmNativeBorderColor
                    || 'rgba(245,77,163,.42)';
                form.style.boxShadow = 'none';
            });

            const currentQuery = new URL(location.href).searchParams.get('q');
            if (currentQuery && /^[\d\s+().-]+$/.test(currentQuery)) {
                const currentPhone = normalizeEscortCachedPhone(currentQuery);
                if (currentPhone) input.value = currentPhone;
            }

            const enterTarget = normalizeTopPhoneSearchEnterTarget(
                SETTINGS.topPhoneSearchEnterTarget
            );
            const openMode = normalizeTopPhoneSearchOpenMode(
                SETTINGS.topPhoneSearchOpenMode
            );
            const makeSearchButton = (text, filled) => {
                const button = makeElement('button', 'vm-escort-phone-search-button', text);
                Object.assign(button.style, {
                    display: 'inline-flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    flex: '1 1 50%',
                    width: '100%',
                    minWidth: '0',
                    minHeight: '0',
                    margin: '0',
                    padding: '2px 7px',
                    border: '0',
                    borderRadius: '0',
                    background: filled
                        ? getEscortPagePinkColor()
                        : '#fff',
                    color: filled ? '#fff' : getEscortPagePinkColor(),
                    cursor: 'pointer',
                    fontSize: '9px',
                    fontWeight: '800',
                    lineHeight: '1',
                    whiteSpace: 'nowrap',
                    boxSizing: 'border-box',
                    transition: 'background .14s ease, color .14s ease'
                });
                button.addEventListener('mouseenter', () => {
                    if (filled) button.style.filter = 'brightness(.92)';
                    else button.style.backgroundColor = '#fff0f7';
                });
                button.addEventListener('mouseleave', () => {
                    button.style.filter = 'none';
                    if (!filled) button.style.backgroundColor = '#ffffff';
                });
                return button;
            };

            const buttonStack = makeElement('div');
            Object.assign(buttonStack.style, {
                display: 'flex',
                flexDirection: 'column',
                alignItems: 'stretch',
                flex: '0 0 94px',
                width: '94px',
                height: '46px',
                borderLeft: '1px solid rgba(245,77,163,.35)',
                boxSizing: 'border-box'
            });

            const escortClubButton = makeSearchButton(
                'Escort.club',
                enterTarget === 'escort-club'
            );
            escortClubButton.type = 'button';
            escortClubButton.title = 'Wyszukaj numer telefonu na Escort.club';
            const escortiButton = makeSearchButton(
                'Escorti.pl',
                enterTarget === 'escorti'
            );
            escortiButton.type = 'button';
            escortiButton.title = 'Wyszukaj numer telefonu na Escorti.pl';
            escortiButton.style.borderTop = '1px solid rgba(245,77,163,.35)';
            buttonStack.append(escortClubButton, escortiButton);

            const findVisibleNativeControl = selector =>
                [...host.querySelectorAll(selector)].find(element => {
                    if (root.contains(element)) return false;
                    const rect = element.getBoundingClientRect();
                    const style = getComputedStyle(element);
                    return rect.width > 0 &&
                        rect.height > 0 &&
                        style.display !== 'none' &&
                        style.visibility !== 'hidden';
                }) || null;

            const syncNativeHeaderStyle = () => {
                if (!root.isConnected) return;

                const addButton = findVisibleNativeControl('.btn-add');
                const loginButton = findVisibleNativeControl('.btn-login');
                const fallbackButton = findVisibleNativeControl('.btn-ucp-trigger');
                const heightSources = [addButton, loginButton, fallbackButton]
                    .filter(Boolean)
                    .map(element => element.getBoundingClientRect().height)
                    .filter(height => Number.isFinite(height) && height > 0);
                const nativeHeight = heightSources.length
                    ? Math.round(Math.max(...heightSources))
                    : 0;

                if (nativeHeight >= 40 && nativeHeight <= 100) {
                    const innerHeight = Math.max(1, nativeHeight - 2);
                    root.style.height = `${nativeHeight}px`;
                    form.style.height = `${nativeHeight}px`;
                    input.style.height = `${innerHeight}px`;
                    buttonStack.style.height = `${innerHeight}px`;
                }

                const outlineSource = addButton || fallbackButton;
                if (outlineSource) {
                    const outlineStyle = getComputedStyle(outlineSource);
                    const borderColor = outlineStyle.borderTopColor;
                    if (borderColor && borderColor !== 'rgba(0, 0, 0, 0)') {
                        root._vmNativeBorderColor = borderColor;
                        form.style.borderColor = borderColor;
                        searchIcon.style.borderRightColor = borderColor;
                        buttonStack.style.borderLeftColor = borderColor;
                        escortiButton.style.borderTopColor = borderColor;
                    }
                    if (outlineStyle.borderRadius && outlineStyle.borderRadius !== '0px') {
                        form.style.borderRadius = outlineStyle.borderRadius;
                    }
                }

                const activeButton = enterTarget === 'escorti'
                    ? escortiButton
                    : escortClubButton;
                const inactiveButton = enterTarget === 'escorti'
                    ? escortClubButton
                    : escortiButton;
                const filledStyle = loginButton
                    ? getComputedStyle(loginButton)
                    : null;
                const nativeBackgroundImage = filledStyle?.backgroundImage;
                const nativeBackgroundColor = filledStyle?.backgroundColor;

                activeButton.style.backgroundImage =
                    nativeBackgroundImage && nativeBackgroundImage !== 'none'
                        ? nativeBackgroundImage
                        : 'linear-gradient(110deg, #bd397f, #4e0d52)';
                activeButton.style.backgroundColor =
                    nativeBackgroundColor && nativeBackgroundColor !== 'rgba(0, 0, 0, 0)'
                        ? nativeBackgroundColor
                        : '#8c286f';
                activeButton.style.color = filledStyle?.color || '#ffffff';
                inactiveButton.style.backgroundImage = 'none';
                inactiveButton.style.backgroundColor = '#ffffff';
                inactiveButton.style.color = getEscortPagePinkColor();
            };
            root._vmSyncNativeHeaderStyle = syncNativeHeaderStyle;

            if (typeof ResizeObserver === 'function') {
                const headerStyleObserver = new ResizeObserver(() => {
                    requestAnimationFrame(syncNativeHeaderStyle);
                });
                headerStyleObserver.observe(host);
                root._vmHeaderStyleObserver = headerStyleObserver;
            }

            const getValidPhone = () => {
                const phone = normalizeEscortCachedPhone(input.value);
                if (phone) {
                    input.setCustomValidity('');
                    return phone;
                }
                input.setCustomValidity(
                    'Wpisz pełny numer telefonu zawierający od 8 do 15 cyfr.'
                );
                input.reportValidity();
                input.focus();
                return null;
            };

            const openPhoneSearch = target => {
                const phone = getValidPhone();
                if (!phone) return;
                const url = target === 'escorti'
                    ? buildEscortiPhoneSearchUrl(phone)
                    : buildEscortClubPhoneSearchUrl(phone);
                if (!url) return;
                if (openMode === 'same-tab') {
                    location.assign(url);
                    return;
                }
                GM_openInTab(url, { active: true, insert: true });
            };

            form.addEventListener('submit', event => {
                event.preventDefault();
                openPhoneSearch(enterTarget);
            });
            escortClubButton.addEventListener('click', () => {
                openPhoneSearch('escort-club');
            });
            escortiButton.addEventListener('click', () => {
                openPhoneSearch('escorti');
            });

            form.append(searchIcon, input, buttonStack);
            root.appendChild(form);
            host.insertBefore(root, host.firstChild);
            requestAnimationFrame(syncNativeHeaderStyle);
            setTimeout(syncNativeHeaderStyle, 250);
            setTimeout(syncNativeHeaderStyle, 1000);
            return true;
        };

        const start = () => {
            if (!document.body) return;
            try {
                document.body._vmEscortTopPhoneSearchObserver?.disconnect();
                clearInterval(document.body._vmEscortTopPhoneSearchTimer);
            } catch (_) {}

            mount();
            const scheduleMount = createAnimationFrameScheduler(mount);
            const observer = new MutationObserver(mutations => {
                const headerChanged = mutations.some(mutation =>
                    [...mutation.addedNodes].some(node =>
                        node.nodeType === Node.ELEMENT_NODE && (
                            node.matches?.('.ucp-col.col, .site-title-col.col') ||
                            node.querySelector?.('.ucp-col.col, .site-title-col.col')
                        )
                    )
                );
                if (headerChanged) scheduleMount();
            });
            observer.observe(document.body, { childList: true, subtree: true });
            document.body._vmEscortTopPhoneSearchObserver = observer;

            let attempts = 0;
            const timer = setInterval(() => {
                attempts++;
                if (mount() || attempts >= 40) clearInterval(timer);
            }, 250);
            document.body._vmEscortTopPhoneSearchTimer = timer;
        };

        if (document.body) start();
        else document.addEventListener('DOMContentLoaded', start, { once: true });
    }

    function getEscortClubSearchAdUrls(doc, pageUrl) {
        const context = getEscortClubListContext(doc, pageUrl);
        const anchors = context?.anchors || [];
        return [...new Set(
            anchors
                .map(anchor => normalizeEscortiAdUrl(anchor.getAttribute('href'), pageUrl))
                .filter(Boolean)
        )];
    }

    async function fetchEscortClubPhoneSearchAds(phone, cancelToken = null) {
        const firstUrl = buildEscortClubPhoneSearchUrl(phone);
        const found = new Set();
        const seenPages = new Set();
        let pageUrl = firstUrl;

        while (pageUrl && !seenPages.has(pageUrl) && seenPages.size < 100) {
            cancelToken?.throwIfCancelled();
            seenPages.add(pageUrl);
            const controller = new AbortController();
            const timeout = setTimeout(() => controller.abort(), 20000);
            const unsubscribeCancel = cancelToken?.onCancel(() => controller.abort()) || (() => {});
            let response;
            try {
                recordDiagnosticRequest(pageUrl, 'GET');
                response = await fetch(pageUrl, {
                    method: 'GET',
                    credentials: 'include',
                    redirect: 'follow',
                    cache: 'no-store',
                    signal: controller.signal
                });
            } catch (error) {
                if (cancelToken?.cancelled) {
                    throw createOperationCancelledError(cancelToken.reason);
                }
                throw error;
            } finally {
                clearTimeout(timeout);
                unsubscribeCancel();
            }
            if (!response.ok) throw new Error(`Escort.club: HTTP ${response.status}`);

            const finalUrl = response.url || pageUrl;
            const doc = new DOMParser().parseFromString(await response.text(), 'text/html');
            for (const url of getEscortClubSearchAdUrls(doc, finalUrl)) found.add(url);
            pageUrl = findNextEscortListPageUrl(doc, finalUrl);
        }

        return {
            searchUrl: firstUrl,
            adUrls: [...found]
        };
    }

    function createResearchPanelResultRow(label, searchUrl) {
        const row = makeButton('vm-research-panel-row');
        row.dataset.searchUrl = searchUrl;
        styleResearchPanelRow(row);
        setButtonColor(row, 'checking');
        setTwoLineButton(row, label, 'sprawdzam…');
        row.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            if (!row.dataset.searchUrl) return;
            GM_openInTab(row.dataset.searchUrl, { active: true, insert: true });
        });
        return row;
    }

    function formatGarsoExtendedEta(remainingAds, averageCheckMs = 1500) {
        const remaining = Math.max(0, Number(remainingAds) || 0);
        const average = Math.min(30000, Math.max(250, Number(averageCheckMs) || 1500));
        const seconds = Math.ceil(remaining * average / 1000);
        if (seconds < 60) return `${seconds} s`;
        return `${(seconds / 60).toFixed(1).replace('.', ',')} min`;
    }

    function getGarsoExtendedResultSignature(result) {
        if (result?.signature) return String(result.signature);
        const topicUrls = [...new Set(
            (result?.topics || [])
                .map(topic => normalizeTopicUrl(topic?.url))
                .filter(Boolean)
        )].sort();
        if (topicUrls.length) return `topics:${topicUrls.join('|')}`;

        return `fallback:${String(result?.searchTerm || '')}|count:${
            Number(result?.count) || 0
        }|url:${normalizeGarsoResultPageUrl(result?.resultUrl) || ''}`;
    }

    function getUniqueGarsoResultPages(results) {
        const seenResults = new Set();
        const uniqueResults = [];

        for (const item of results || []) {
            if (item?.status !== 'ok' || Number(item.count) <= 0) continue;
            const signature = getGarsoExtendedResultSignature(item);
            if (seenResults.has(signature)) continue;
            seenResults.add(signature);
            uniqueResults.push(item);
        }

        return uniqueResults;
    }

    function formatGarsoPageCount(count) {
        const number = Math.max(0, Number(count) || 0);
        const last = number % 10;
        const lastTwo = number % 100;
        const word = number === 1
            ? 'strona'
            : (last >= 2 && last <= 4 && !(lastTwo >= 12 && lastTwo <= 14)
                ? 'strony'
                : 'stron');
        return `${number} ${word}`;
    }

    async function openUniqueGarsoResultPages(results) {
        const uniqueResults = getUniqueGarsoResultPages(results);

        if (!uniqueResults.length) return 0;

        const auth = await getGarsoAuth();

        for (const result of uniqueResults) {
            submitGarsoSearchFormToNewTab(
                result.searchTerm,
                auth.secureHash,
                auth.sessionId,
                { active: false }
            );
        }
        return uniqueResults.length;
    }

    function summarizeActiveAdsSubset(summary, adUrls) {
        const wanted = new Set(
            (adUrls || []).map(url => normalizeEscortiAdUrl(url) || url)
        );
        return summarizeActiveAdResults(
            (summary?.results || []).filter(item =>
                wanted.has(normalizeEscortiAdUrl(item.url) || item.url)
            )
        );
    }

    function normalizedEscortiResultSet(result, type) {
        const values = type === 'profiles'
            ? normalizeProfileUrls(result)
            : (Array.isArray(result?.adUrls) ? result.adUrls : []);
        return [...new Set(values.map(value =>
            type === 'profiles'
                ? (getEscortiProfileId(value) || value)
                : (parseAdIdFromUrl(value) || value)
        ).filter(Boolean))].sort();
    }

    function areSameEscortiResults(first, second) {
        if (first?.status !== 'ok' || second?.status !== 'ok') return false;
        return ['profiles', 'ads'].every(type => {
            const a = normalizedEscortiResultSet(first, type);
            const b = normalizedEscortiResultSet(second, type);
            return a.length === b.length && a.every((value, index) => value === b[index]);
        });
    }

    function activeEscortAdsWord(count) {
        const n = Number(count) || 0;
        return n >= 1 && n <= 4 ? 'aktywne' : 'aktywnych';
    }

    function getCompactSummaryAggregationSource(facts) {
        const mode = normalizeEscortCompactSummaryMode(
            SETTINGS.compactSummaryComparisonMode
        );
        const sources = {
            'address-escorti': {
                mode,
                label: 'adres anonsu → Escorti.pl',
                description:
                    'Biorę aktywne anonse Escort.club przypisane do profili znalezionych w Escorti.pl po adresie bieżącego anonsu.',
                result: facts?.escortiAddressResult,
                summary: facts?.escortiAddressSummary,
                loading: !!facts?.escortiLoading
            },
            'phone-escorti': {
                mode,
                label: 'nr tel. → Escorti.pl',
                description:
                    'Biorę aktywne anonse Escort.club przypisane do profili znalezionych w Escorti.pl po numerze telefonu bieżącego anonsu.',
                result: facts?.escortiPhoneResult,
                summary: facts?.escortiPhoneSummary,
                loading: !!facts?.escortiLoading
            },
            'exact-phone': {
                mode,
                label: 'nr tel. → wyszukiwarka Escort.club',
                description:
                    'Biorę aktualnie dostępne anonse zwrócone przez wyszukiwarkę Escort.club po numerze telefonu bieżącego anonsu.',
                result: null,
                summary: facts?.escortPhoneSummary,
                loading: !!facts?.escortPhoneLoading
            }
        };
        return sources[mode] || sources['address-escorti'];
    }

    function buildCompactSummaryAggregationTooltip(source, maxAds = 10) {
        const summary = source?.summary;
        const lines = [
            `Sposób pozyskania anonsów: ${source?.label || 'brak danych'}.`,
            source?.description || ''
        ].filter(Boolean);
        const activeResults = (summary?.results || []).filter(item => item?.active);
        const activeCount = Number(summary?.activeCount) || activeResults.length;

        lines.push(`Do porównania używam tylko aktywnych anonsów (${activeCount}).`);
        if (!activeResults.length) {
            if (activeCount > 0) {
                lines.push(
                    '',
                    'Nazwy profili/anonsów nie są dostępne w skróconych danych zapisanych w cache.'
                );
            }
            return lines.join('\n');
        }

        const descriptions = activeResults.map(item => {
            const adId = parseAdIdFromUrl(item.url);
            const name = normalizeEscortAdText(item.adData?.title)
                || (adId ? `Anons nr ${adId}` : 'Anons bez nazwy');
            const city = normalizeEscortCity(item.adData?.location?.city || item.city);
            return `${name}${city ? ` - ${city}` : ''}${adId ? ` (nr ${adId})` : ''}`;
        });
        lines.push(
            '',
            'Nazwy aktywnych profili/anonsów uwzględnionych w porównaniu:'
        );
        lines.push(...descriptions.slice(0, maxAds).map(value => `• ${value}`));
        if (descriptions.length > maxAds) {
            lines.push(`• …oraz ${descriptions.length - maxAds} kolejnych`);
        }
        return lines.join('\n');
    }

    function formatEscortiAdsAndActivity(result, summary) {
        const total = Number.isFinite(Number(result?.adLinks))
            ? Number(result.adLinks)
            : Number(summary?.totalChecked) || 0;
        const active = Number(summary?.activeCount) || 0;
        return `${total} ${polishAdWord(total)} (${active} ${activeEscortAdsWord(active)})`;
    }

    function formatResearchDurationDays(days) {
        const safeDays = Math.max(0, Math.round(Number(days) || 0));
        if (safeDays < 31) return `${safeDays} dni`;
        const months = Math.floor(safeDays / 30.4375);
        if (months < 12) return `${months} mies.`;
        const years = Math.floor(months / 12);
        const restMonths = months % 12;
        return restMonths ? `${years} r. ${restMonths} mies.` : `${years} r.`;
    }

    function formatResearchDateAge(value) {
        const normalizedValue = String(value || '').match(/^\d{4}-\d{2}-\d{2}/)?.[0]
            || value;
        const timestamp = profileDateToTime(normalizedValue);
        const date = formatEscortiDate(normalizedValue);
        if (!date) return 'brak';
        if (timestamp == null) return date;
        const days = Math.max(0, (Date.now() - timestamp) / 86400000);
        return `${date} (${formatResearchDurationDays(days)})`;
    }

    function getEscortiProfileSummariesForResearch(result) {
        if (Array.isArray(result?.profileSummaries) && result.profileSummaries.length) {
            return result.profileSummaries;
        }
        if (result?.status !== 'ok' || !result.profiles) return [];
        return [{
            profileUrl: result.profileUrl || result.profileUrls?.[0] || null,
            profileName: result.profileName || null,
            creationDate: result.creationDate || null,
            currentCity: result.currentCity || null,
            adLinks: result.adLinks,
            cityHistory: Array.isArray(result.cityHistory) ? result.cityHistory : []
        }];
    }

    function getEscortGalleryPhotoUrls(sourceDocument = document) {
        const urls = new Set();
        for (const anchor of sourceDocument.querySelectorAll(
            '.content-gallery-col .galleryContainer a.simple-zoom-image:not(.video-layer)'
        )) {
            const candidates = [
                anchor.getAttribute('href'),
                anchor.querySelector('img')?.getAttribute('data-src'),
                anchor.querySelector('img')?.getAttribute('src')
            ];
            const found = candidates.find(isEscortGalleryPhotoUrl);
            if (found) urls.add(new URL(found, location.href).href);
        }
        return [...urls];
    }

    async function getEscortGalleryPhotoDateRange(sourceDocument = document) {
        const urls = getEscortGalleryPhotoUrls(sourceDocument);
        if (!urls.length) {
            return {
                total: 0,
                dated: 0,
                oldest: null,
                newest: null,
                dates: [],
                items: []
            };
        }

        const items = (await watchMapLimit(urls, 4, async url => ({
            url,
            date: await getMediaServerDate(url)
        })))
            .filter(item =>
                item.date instanceof Date && Number.isFinite(item.date.getTime())
            )
            .sort((a, b) => a.date.getTime() - b.date.getTime());
        const dates = items.map(item => item.date);
        return {
            total: urls.length,
            dated: dates.length,
            oldest: dates[0] || null,
            newest: dates.at(-1) || null,
            dates,
            items
        };
    }

    const ESCORT_RESEARCH_TIMELINE_CATEGORIES = {
        profile: { label: 'profile Escorti', color: '#55c7ff' },
        ad: { label: 'anonse', color: '#ffb45c' },
        garso: { label: 'recenzje Garso', color: '#75db91' },
        photo: { label: 'zdjęcia', color: '#f54da3' },
        location: { label: 'lokalizacje Escorti', color: '#b99cff' }
    };

    function parseEscortResearchTimelineDate(value) {
        if (value instanceof Date && Number.isFinite(value.getTime())) {
            return new Date(value.getTime());
        }
        if (Number.isFinite(Number(value)) && Number(value) > 0) {
            const date = new Date(Number(value));
            return Number.isFinite(date.getTime()) ? date : null;
        }
        const normalizedValue = String(value || '').match(/^\d{4}-\d{2}-\d{2}/)?.[0]
            || formatEscortiDate(value);
        const timestamp = profileDateToTime(normalizedValue);
        if (timestamp != null) return new Date(timestamp);
        const fallback = Date.parse(String(value || ''));
        return Number.isFinite(fallback) ? new Date(fallback) : null;
    }

    function collectEscortResearchTimelineEvents(facts) {
        const events = [];
        const seen = new Set();
        const add = (
            value,
            label,
            category,
            identity = '',
            url = '',
            source = ''
        ) => {
            const date = parseEscortResearchTimelineDate(value);
            if (!date || !ESCORT_RESEARCH_TIMELINE_CATEGORIES[category]) return;
            const key = `${category}|${identity || label}|${date.toISOString().slice(0, 10)}`;
            if (seen.has(key)) return;
            seen.add(key);
            events.push({
                date,
                label,
                category,
                url: String(url || ''),
                source: String(source || '')
            });
        };

        add(
            facts?.datePosted,
            'Włączenie bieżącego anonsu Escort.club',
            'ad',
            `current:${facts?.currentAdUrl || location.pathname}`,
            facts?.currentAdUrl || location.href
        );

        const resultPairs = [
            ['adres', facts?.escortiAddressResult],
            ['telefon', facts?.escortiPhoneResult]
        ];
        for (const [, result] of resultPairs) {
            for (const profile of getEscortiProfileSummariesForResearch(result)) {
                const profileIdentity = profile.profileUrl || profile.profileName || 'profil';
                add(
                    profile.creationDate,
                    `Założenie profilu Escorti${profile.profileName ? `: ${profile.profileName}` : ''}`,
                    'profile',
                    profileIdentity,
                    profile.profileUrl
                );
                for (const entry of Array.isArray(profile.cityHistory) ? profile.cityHistory : []) {
                    add(
                        entry?.date,
                        `Lokalizacja Escorti${entry?.city ? `: ${entry.city}` : ''}`,
                        'location',
                        `${profileIdentity}:${entry?.city || ''}`,
                        profile.profileUrl
                    );
                }
            }
        }

        const activeResultSources = [
            {
                source: 'escorti',
                label: 'Dodanie aktywnego anonsu znalezionego przez Escorti.pl',
                results: [
                    ...(facts?.escortiAddressSummary?.results || []),
                    ...(facts?.escortiPhoneSummary?.results || [])
                ]
            },
            {
                source: 'escort-phone',
                label: 'Dodanie aktywnego anonsu Escort.club z tym samym nr tel.',
                results: facts?.escortPhoneSummary?.results || []
            }
        ];
        for (const activeSource of activeResultSources) {
            for (const item of activeSource.results) {
                if (!item?.active || !item.adData?.datePosted) continue;
                const adId = parseAdIdFromUrl(item.url);
                add(
                    item.adData.datePosted,
                    `${activeSource.label}${adId ? ` nr ${adId}` : ''}`,
                    'ad',
                    `${activeSource.source}:${item.url || adId || item.adData.datePosted}`,
                    item.url,
                    activeSource.source
                );
            }
        }

        const reviewSources = [
            ['nr tel. + adres anonsu',
                facts?.garsoPhoneReviewDates || facts?.garsoAddressReviewDates]
        ];
        for (const [source, dates] of reviewSources) {
            add(
                dates?.first,
                `Pierwsza recenzja Garso (${source})`,
                'garso',
                `first:${source}`,
                dates?.firstUrl
            );
            add(
                dates?.last,
                `Ostatnia recenzja Garso (${source})`,
                'garso',
                `last:${source}`,
                dates?.lastUrl
            );
        }

        const photoItems = Array.isArray(facts?.photoRange?.items)
            ? facts.photoRange.items
            : (facts?.photoRange?.dates || []).map(date => ({ date, url: '' }));
        for (const [index, item] of photoItems.entries()) {
            add(
                item?.date,
                'Zdjęcie – data pliku na serwerze',
                'photo',
                `photo:${index}`,
                item?.url
            );
        }
        return events.sort((a, b) => a.date.getTime() - b.date.getTime());
    }

    function makeEscortSummarySectionCollapsible(
        section,
        header,
        content,
        storageSuffix,
        defaultExpanded = true,
        persistState = true
    ) {
        if (!section || !header || header.dataset.vmCollapsibleReady === '1') return null;
        const contentNodes = (Array.isArray(content) ? content : [content]).filter(Boolean);
        if (!contentNodes.length) return null;

        header.dataset.vmCollapsibleReady = '1';
        const storageKey = `vm_escort_summary_section_expanded_${storageSuffix}`;
        let expanded = persistState
            ? !!GM_getValue(storageKey, defaultExpanded)
            : !!defaultExpanded;
        const originalMarginBottom = header.style.marginBottom || '';

        const toggleHint = makeElement('span', 'vm-escort-summary-collapse-hint');
        toggleHint.setAttribute('aria-hidden', 'true');
        const hintText = makeElement('span');
        const arrow = makeElement('span');
        arrow.setAttribute('aria-hidden', 'true');
        toggleHint.append(hintText, arrow);
        header.appendChild(toggleHint);
        header.classList.add('vm-escort-summary-collapsible-header');
        header.setAttribute('role', 'button');
        header.setAttribute('tabindex', '0');

        const apply = () => {
            for (const node of contentNodes) node.hidden = !expanded;
            hintText.textContent = expanded ? 'Zwiń' : 'Rozwiń';
            arrow.textContent = expanded ? '▾' : '▸';
            section.classList.toggle(
                'vm-escort-summary-section-collapsed',
                !expanded
            );
            header.style.marginBottom = expanded ? originalMarginBottom : '0';
            header.setAttribute('aria-expanded', String(expanded));
            header.title = expanded ? 'Kliknij, aby zwinąć sekcję' : 'Kliknij, aby rozwinąć sekcję';
        };
        const setExpanded = nextExpanded => {
            expanded = !!nextExpanded;
            if (persistState) GM_setValue(storageKey, expanded);
            apply();
        };
        const toggle = () => setExpanded(!expanded);
        header.addEventListener('click', event => {
            if (event.target.closest('a, button, input, select, textarea')) return;
            toggle();
        });
        header.addEventListener('keydown', event => {
            if (event.key !== 'Enter' && event.key !== ' ') return;
            event.preventDefault();
            toggle();
        });
        apply();
        return {
            setExpanded,
            isExpanded: () => expanded
        };
    }

    function makeEscortTimelineElementClickable(element, url, titleText) {
        let targetUrl = null;
        try {
            const parsed = new URL(String(url || ''), location.href);
            if (['http:', 'https:'].includes(parsed.protocol)) targetUrl = parsed.href;
        } catch (_) {}
        if (!element || !targetUrl) return false;

        element.setAttribute('role', 'link');
        element.setAttribute('tabindex', '0');
        element.title = titleText || 'Otwórz stronę źródłową';
        element.classList.add('vm-summary-source-link');
        element.addEventListener('mouseenter', () => {
            element.style.filter = 'brightness(1.12)';
        });
        element.addEventListener('mouseleave', () => {
            element.style.filter = '';
        });
        const open = event => {
            event.preventDefault();
            event.stopPropagation();
            GM_openInTab(targetUrl, {
                active: true,
                insert: true,
                setParent: true
            });
        };
        element.addEventListener('click', open);
        element.addEventListener('keydown', event => {
            if (event.key !== 'Enter' && event.key !== ' ') return;
            open(event);
        });
        return true;
    }

    function formatEscortResearchTimelineAge(date) {
        const days = Math.max(0, (Date.now() - date.getTime()) / 86400000);
        return `${formatResearchDurationDays(days)} temu`;
    }

    function createEscortResearchDateTimeline(facts) {
        const box = makeElement('section', 'vm-escort-summary-integrated-section');

        const title = makeElement('div', 'vm-summary-timeline-title');
        const titleText = makeElement('span', '', 'Oś czasu i zgodność dat');
        const titleWarning = makeElement('span', 'vm-summary-timeline-title-warning', '⚠︎');
        titleWarning.hidden = true;
        titleWarning.setAttribute('aria-label', 'Ostrzeżenia na osi czasu');
        title.append(titleText, titleWarning);
        box.appendChild(title);
        const timelineHeaderWarnings = [];
        const addTimelineHeaderWarning = (message, color, priority) => {
            timelineHeaderWarnings.push({ message, color, priority });
            const strongest = [...timelineHeaderWarnings]
                .sort((a, b) => b.priority - a.priority)[0];
            titleWarning.hidden = false;
            titleWarning.style.color = strongest.color;
            titleWarning.title = timelineHeaderWarnings
                .map(item => item.message)
                .join('\n');
        };

        const events = collectEscortResearchTimelineEvents(facts);
        const photos = events.filter(event => event.category === 'photo');
        const profiles = events.filter(event => event.category === 'profile');
        const currentAds = events.filter(event =>
            event.label === 'Włączenie bieżącego anonsu Escort.club'
        );
        const escortiActiveAds = events.filter(event => event.source === 'escorti');
        const escortPhoneActiveAds = events.filter(event =>
            event.source === 'escort-phone'
        );
        const garsoReviews = events.filter(event => event.category === 'garso');
        const locations = events.filter(event => event.category === 'location');

        const oldestPhoto = photos[0] || null;
        const newestPhoto = photos.at(-1) || null;
        const oldestProfile = profiles[0] || null;
        const newestProfile = profiles.at(-1) || null;
        const currentAd = currentAds[0] || null;

        const keyDates = [
            {
                label: 'Najstarsze zdjęcie',
                event: oldestPhoto,
                color: ESCORT_RESEARCH_TIMELINE_CATEGORIES.photo.color
            },
            {
                label: 'Najstarszy profil',
                event: oldestProfile,
                color: ESCORT_RESEARCH_TIMELINE_CATEGORIES.profile.color
            },
            {
                label: 'Ten anons',
                event: currentAd,
                color: ESCORT_RESEARCH_TIMELINE_CATEGORIES.ad.color
            }
        ];
        const keyGrid = makeElement('div', 'vm-summary-key-dates');
        for (const item of keyDates) {
            const card = makeElement('div', 'vm-summary-key-date-card');
            card.style.border =
                `1px solid ${item.event ? item.color : 'rgba(255,255,255,.14)'}`;
            const label = makeElement('div', 'vm-summary-key-date-label', item.label);
            label.style.color = item.event ? item.color : '#a997ae';
            const date = makeElement('div', 'vm-summary-key-date-value');
            date.textContent = item.event
                ? formatServerMediaDate(item.event.date)
                : (facts?.escortiLoading || facts?.photoDatesLoading ? '…' : 'brak');
            const age = makeElement('div', 'vm-summary-key-date-age', item.event ? formatEscortResearchTimelineAge(item.event.date) : '');
            card.append(label, date, age);
            makeEscortTimelineElementClickable(
                card,
                item.event?.url,
                `Otwórz źródło: ${item.label}`
            );
            keyGrid.appendChild(card);
        }
        box.appendChild(keyGrid);

        const threeYearsAgo = new Date();
        threeYearsAgo.setFullYear(threeYearsAgo.getFullYear() - 3);
        if (newestPhoto && newestPhoto.date.getTime() < threeYearsAgo.getTime()) {
            addTimelineHeaderWarning(
                'Wszystkie zdjęcia mają daty plików starsze niż 3 lata.',
                '#ff8b8b',
                2
            );
            const oldPhotosWarning = makeElement('div', 'vm-summary-old-photos-warning');
            oldPhotosWarning.textContent =
                `⚠ Wszystkie zdjęcia mają daty plików starsze niż 3 lata. ` +
                `Najnowsze: ${formatServerMediaDate(newestPhoto.date)} ` +
                `(${formatEscortResearchTimelineAge(newestPhoto.date)}).`;
            makeEscortTimelineElementClickable(
                oldPhotosWarning,
                newestPhoto.url,
                'Otwórz najnowsze zdjęcie'
            );
            box.appendChild(oldPhotosWarning);
        }

        if (oldestPhoto && oldestProfile) {
            const differenceDays =
                (oldestProfile.date.getTime() - oldestPhoto.date.getTime()) / 86400000;
            const comparison = makeElement('div');
            const mismatch = differenceDays > 30;
            comparison.className =
                `vm-summary-date-comparison ${mismatch ? '-mismatch' : '-ok'}`;
            if (mismatch) {
                addTimelineHeaderWarning(
                    'Data najstarszego zdjęcia jest wcześniejsza niż założenie najstarszego profilu Escorti.',
                    '#ffb08e',
                    1
                );
            }
            comparison.textContent = differenceDays > 0
                ? `${mismatch ? '⚠ ' : ''}Najstarsze zdjęcie ma datę pliku o ${formatResearchDurationDays(differenceDays)} wcześniejszą niż założenie najstarszego profilu Escorti.`
                : 'Daty plików zdjęć nie są wcześniejsze niż data założenia najstarszego profilu Escorti.';
            makeEscortTimelineElementClickable(
                comparison,
                oldestPhoto.url,
                'Otwórz najstarsze zdjęcie użyte w porównaniu'
            );
            box.appendChild(comparison);
        }

        const timelineItems = [];
        const timelineSeen = new Set();
        const addTimelineItem = (event, label, detail = '') => {
            if (!event) return;
            const key = `${label}|${event.date.toISOString().slice(0, 10)}`;
            if (timelineSeen.has(key)) return;
            timelineSeen.add(key);
            timelineItems.push({ ...event, label, detail });
        };

        if (oldestPhoto) {
            addTimelineItem(
                oldestPhoto,
                photos.length === 1 ? 'Zdjęcie – data pliku' : 'Najstarsze zdjęcie – data pliku',
                `${photos.length} ${photos.length === 1 ? 'zdjęcie z datą' : 'zdjęć z datami'}`
            );
        }
        if (newestPhoto && newestPhoto.date.getTime() !== oldestPhoto?.date.getTime()) {
            addTimelineItem(newestPhoto, 'Najnowsze zdjęcie – data pliku');
        }
        if (oldestProfile) {
            addTimelineItem(
                oldestProfile,
                profiles.length > 1 ? 'Założenie najstarszego profilu Escorti' : 'Założenie profilu Escorti',
                profiles.length > 1 ? `${profiles.length} znalezione profile` : ''
            );
        }
        if (newestProfile && newestProfile.date.getTime() !== oldestProfile?.date.getTime()) {
            addTimelineItem(newestProfile, 'Założenie najnowszego profilu Escorti');
        }
        addTimelineItem(currentAd, 'Włączenie bieżącego anonsu Escort.club');
        addTimelineItem(
            escortiActiveAds[0],
            'Najstarszy aktywny anons z Escorti.pl'
        );
        addTimelineItem(
            escortPhoneActiveAds[0],
            'Najstarszy aktywny anons Escort.club z tym samym nr tel.'
        );
        addTimelineItem(garsoReviews[0], 'Pierwsza recenzja Garso');
        if (garsoReviews.at(-1)?.date.getTime() !== garsoReviews[0]?.date.getTime()) {
            addTimelineItem(garsoReviews.at(-1), 'Ostatnia recenzja Garso');
        }
        addTimelineItem(locations[0], 'Pierwsza zapisana lokalizacja Escorti');
        if (locations.at(-1)?.date.getTime() !== locations[0]?.date.getTime()) {
            addTimelineItem(locations.at(-1), 'Ostatnia zapisana lokalizacja Escorti');
        }
        timelineItems.sort((a, b) => a.date.getTime() - b.date.getTime());

        const timelineDays = [];
        const timelineDaysByDate = new Map();
        for (const item of timelineItems) {
            const dayKey = [
                item.date.getFullYear(),
                String(item.date.getMonth() + 1).padStart(2, '0'),
                String(item.date.getDate()).padStart(2, '0')
            ].join('-');
            let day = timelineDaysByDate.get(dayKey);
            if (!day) {
                day = { date: item.date, items: [] };
                timelineDaysByDate.set(dayKey, day);
                timelineDays.push(day);
            }
            day.items.push(item);
        }

        if (timelineDays.length) {
            const timeline = makeElement('div', 'vm-summary-timeline');
            const line = makeElement('div', 'vm-summary-timeline-line');
            timeline.appendChild(line);

            for (const day of timelineDays) {
                const row = makeElement('div', 'vm-summary-timeline-row');
                const markerColors = [...new Set(day.items.map(item =>
                    ESCORT_RESEARCH_TIMELINE_CATEGORIES[item.category]?.color || '#fff'
                ))];
                const markerBackground = markerColors.length === 1
                    ? markerColors[0]
                    : `conic-gradient(${markerColors.map((color, index) => {
                        const start = Math.round(index / markerColors.length * 100);
                        const end = Math.round((index + 1) / markerColors.length * 100);
                        return `${color} ${start}% ${end}%`;
                    }).join(', ')})`;
                const marker = makeElement('span', 'vm-summary-timeline-marker');
                marker.style.background = markerBackground;
                const dateLine = makeElement('div');
                const date = makeElement('strong', 'vm-summary-timeline-date', formatServerMediaDate(day.date));
                const age = makeElement('span', 'vm-summary-timeline-age', ` • ${formatEscortResearchTimelineAge(day.date)}`);
                dateLine.append(date, age);
                const events = makeElement('div', 'vm-summary-timeline-events');

                for (const item of day.items) {
                    const event = makeElement('div', 'vm-summary-timeline-event');
                    const label = makeElement('div', 'vm-summary-timeline-label', `• ${item.label}`);
                    label.style.color =
                        ESCORT_RESEARCH_TIMELINE_CATEGORIES[item.category]?.color || '#fff';
                    event.appendChild(label);
                    if (item.detail) {
                        const detail = makeElement('div', 'vm-summary-timeline-detail', item.detail);
                        event.appendChild(detail);
                    }
                    makeEscortTimelineElementClickable(
                        event,
                        item.url,
                        `Otwórz źródło: ${item.label}`
                    );
                    events.appendChild(event);
                }
                row.append(marker, dateLine, events);
                timeline.appendChild(row);
            }
            box.appendChild(timeline);
        }

        const note = makeElement('div', 'vm-summary-timeline-note');
        const loadingParts = [];
        if (facts?.escortiLoading) loadingParts.push('Escorti i aktywne anonse');
        if (facts?.escortPhoneLoading) {
            loadingParts.push('Escort.club po nr tel.');
        }
        if (facts?.photoDatesLoading) loadingParts.push('zdjęcia');
        note.textContent =
            (loadingParts.length ? `Trwa uzupełnianie: ${loadingParts.join(', ')}. ` : '') +
            'Daty zdjęć pochodzą z nagłówka Last-Modified. Nie są pewną datą wykonania zdjęcia, ale pokazują, od kiedy dany plik istniał na serwerze.';
        box.appendChild(note);
        makeEscortSummarySectionCollapsible(
            box,
            title,
            [...box.children].filter(child => child !== title),
            'timeline',
            true
        );
        return box;
    }

    const ESCORT_SUMMARY_SIDE_PANEL_ID = 'vm-escort-summary-side-panel';
    const ESCORT_SUMMARY_SIDE_PANEL_STYLE_ID = 'vm-escort-summary-side-panel-style';
    const ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS = 'vm-escort-summary-side-panel-open';
    const ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS = 'vm-escort-summary-side-panel-resizing';
    const ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY = 'vm_escort_summary_side_panel_width';
    const ESCORT_SUMMARY_SIDE_PANEL_PINNED_KEY = 'vm_escort_summary_side_panel_pinned';
    const ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH = 400;
    const ESCORT_SUMMARY_SIDE_PANEL_MIN_WIDTH = 300;

    function getEscortSummarySidePanelMaxWidth() {
        return Math.max(
            ESCORT_SUMMARY_SIDE_PANEL_MIN_WIDTH,
            Math.min(800, window.innerWidth - 260)
        );
    }

    function normalizeEscortSummarySidePanelWidth(value) {
        const width = Number(value);
        const fallback = ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH;
        return Math.round(Math.min(
            getEscortSummarySidePanelMaxWidth(),
            Math.max(
                ESCORT_SUMMARY_SIDE_PANEL_MIN_WIDTH,
                Number.isFinite(width) ? width : fallback
            )
        ));
    }

    function ensureEscortSummarySidePanelStyle() {
        if (document.getElementById(ESCORT_SUMMARY_SIDE_PANEL_STYLE_ID)) return;
        const style = makeElement('style');
        style.id = ESCORT_SUMMARY_SIDE_PANEL_STYLE_ID;
        style.textContent = `
            :root {
                --vm-escort-summary-panel-width: ${ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH}px;
                --vm-escort-summary-panel-top-offset: 0px;
                --vm-escort-summary-panel-effective-width: min(
                    var(--vm-escort-summary-panel-width),
                    92vw
                );
            }
            body {
                transition: width .22s ease;
            }
            body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} {
                width: calc(100% - var(--vm-escort-summary-panel-effective-width)) !important;
                max-width: calc(100% - var(--vm-escort-summary-panel-effective-width)) !important;
            }
            body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS},
            body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS} #${ESCORT_SUMMARY_SIDE_PANEL_ID},
            body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS} #vm-escort-summary-side-toggle {
                transition: none !important;
                user-select: none !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} {
                position: fixed;
                z-index: 2147483000;
                top: var(--vm-escort-summary-panel-top-offset);
                right: 0;
                width: var(--vm-escort-summary-panel-effective-width);
                height: calc(100vh - var(--vm-escort-summary-panel-top-offset));
                height: calc(100dvh - var(--vm-escort-summary-panel-top-offset));
                box-sizing: border-box;
                display: flex;
                flex-direction: column;
                transform: translateX(100%);
                transition: transform .22s ease;
                background: #2a0833;
                color: #fff;
                border-left: 2px solid ${getEscortPagePinkColor()};
                box-shadow: -10px 0 30px rgba(0,0,0,.35);
                font-family: Arial, sans-serif;
            }
            body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} #${ESCORT_SUMMARY_SIDE_PANEL_ID} {
                transform: translateX(0);
            }
            #vm-escort-summary-side-toggle {
                position: fixed;
                z-index: 2147483001;
                top: 50%;
                right: 0;
                transform: translateY(-50%);
                padding: 11px 7px;
                border: 1px solid ${getEscortPagePinkColor()};
                border-right: 0;
                border-radius: 8px 0 0 8px;
                background: ${getEscortPagePinkColor()};
                color: #fff;
                font-size: 12px;
                font-weight: 800;
                line-height: 1.15;
                cursor: pointer;
                box-shadow: -3px 2px 12px rgba(0,0,0,.28);
                transition: right .22s ease;
                writing-mode: vertical-rl;
            }
            body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} #vm-escort-summary-side-toggle {
                right: var(--vm-escort-summary-panel-effective-width);
            }
            .vm-escort-summary-side-resize-handle {
                position: absolute;
                z-index: 3;
                top: 0;
                bottom: 0;
                left: -6px;
                width: 12px;
                cursor: ew-resize;
                touch-action: none;
            }
            .vm-escort-summary-side-resize-handle::after {
                content: '';
                position: absolute;
                top: 0;
                bottom: 0;
                left: 5px;
                width: 2px;
                background: transparent;
                transition: background .15s ease;
            }
            .vm-escort-summary-side-resize-handle:hover::after,
            body.${ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS} .vm-escort-summary-side-resize-handle::after {
                background: ${getEscortPagePinkColor()};
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-panel-header {
                flex: 0 0 auto;
                display: flex;
                align-items: center;
                gap: 8px;
                padding: 10px 11px;
                border-bottom: 1px solid ${getEscortPagePinkColor()};
                background: rgba(0,0,0,.16);
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-panel-title {
                flex: 1 1 auto;
                min-width: 0;
                color: ${getEscortPagePinkColor()};
                font-size: 16px;
                font-weight: 800;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-panel-pin {
                width: 30px;
                height: 30px;
                padding: 0;
                border: 1px solid rgba(255,255,255,.25);
                border-radius: 50%;
                background: rgba(255,255,255,.07);
                color: #fff;
                cursor: pointer;
                transition: background .16s ease, border-color .16s ease,
                    color .16s ease, box-shadow .16s ease;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-panel-pin-icon {
                display: block;
                width: 17px;
                height: 17px;
                margin: auto;
                fill: currentColor;
                pointer-events: none;
                transition: transform .16s ease;
            }
            .vm-escort-summary-side-content {
                min-height: 0;
                flex: 1 1 auto;
                overflow-y: auto;
                padding: 10px;
                box-sizing: border-box;
                scrollbar-width: thin;
            }
            .vm-escort-summary-side-card {
                margin-bottom: 10px;
                border: 2px solid rgba(245,77,163,.58);
                border-radius: 7px;
                background: rgba(255,255,255,.035);
                box-shadow: 0 3px 12px rgba(0,0,0,.2);
                overflow: hidden;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-integrated-section,
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-timeline-section {
                border-width: 2px !important;
                border-color: rgba(245,77,163,.58) !important;
                box-shadow: 0 3px 12px rgba(0,0,0,.2);
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-side-card > .vm-escort-summary-collapsible-header {
                border: 0 !important;
                border-bottom: 1px solid rgba(236,69,157,.48) !important;
                border-radius: 0 !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-side-card.vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header {
                border-bottom: 0 !important;
            }
            .vm-escort-summary-side-card-head {
                padding: 8px 10px 7px;
                border-bottom: 1px solid rgba(255,255,255,.13);
                background: rgba(255,255,255,.035);
            }
            .vm-escort-summary-side-card-body {
                padding: 9px 10px 10px;
                font-size: 12px;
                line-height: 1.38;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-integrated-section {
                box-sizing: border-box;
                margin: 0 0 10px;
                padding: 9px;
                border: 1px solid rgba(255,255,255,.18);
                border-radius: 6px;
                background: rgba(255,255,255,.025);
                color: #fff;
                font-size: 11px;
                line-height: 1.3;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-title {
                margin-bottom: 8px;
                font-size: 12px;
                font-weight: 800;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-title-warning {
                margin-left: 5px;
                font-size: 13px;
                font-weight: 900;
                line-height: 1;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-key-dates {
                display: grid;
                grid-template-columns: repeat(3, minmax(0, 1fr));
                gap: 5px;
                margin-bottom: 8px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-key-date-card {
                min-width: 0;
                padding: 6px 5px;
                border-radius: 5px;
                background: rgba(0,0,0,.14);
                text-align: center;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-key-date-label {
                min-height: 23px;
                font-size: 9px;
                font-weight: 700;
                line-height: 1.2;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-key-date-value {
                margin-top: 2px;
                font-size: 11px;
                font-weight: 800;
                white-space: nowrap;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-key-date-age {
                margin-top: 2px;
                color: #cbb8ce;
                font-size: 8px;
                white-space: nowrap;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-old-photos-warning,
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-date-comparison {
                padding: 7px 8px;
                border-radius: 5px;
                font-size: 10px;
                line-height: 1.3;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-old-photos-warning {
                margin-bottom: 8px;
                border: 1px solid rgba(255,82,82,.78);
                background: rgba(220,53,69,.14);
                color: #ff8b8b;
                font-weight: 800;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-date-comparison {
                margin-bottom: 9px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-date-comparison.-mismatch {
                border: 1px solid rgba(255,126,82,.8);
                background: rgba(255,126,82,.13);
                color: #ffb08e;
                font-weight: 700;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-date-comparison.-ok {
                border: 1px solid rgba(117,219,145,.55);
                background: rgba(117,219,145,.09);
                color: #9ae4ad;
                font-weight: 600;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-source-link {
                cursor: pointer;
                transition: background-color .15s ease, filter .15s ease;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline {
                position: relative;
                margin: 2px 0 0 5px;
                padding-left: 17px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-line {
                position: absolute;
                top: 7px;
                bottom: 9px;
                left: 5px;
                width: 2px;
                background: rgba(255,255,255,.2);
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-row {
                position: relative;
                padding-bottom: 10px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-marker {
                position: absolute;
                top: 4px;
                left: -16px;
                width: 9px;
                height: 9px;
                box-sizing: border-box;
                border: 2px solid #2a0833;
                border-radius: 50%;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-date {
                font-size: 10px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-age {
                color: #bca9c1;
                font-size: 9px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-events {
                display: grid;
                gap: 2px;
                margin-top: 2px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-event {
                min-width: 0;
                padding: 1px 3px;
                border-radius: 3px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-label {
                font-size: 10px;
                font-weight: 700;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-detail {
                margin: 1px 0 0 9px;
                color: #a997ae;
                font-size: 9px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-summary-timeline-note {
                margin-top: 3px;
                padding-top: 6px;
                border-top: 1px solid rgba(255,255,255,.12);
                color: #a997ae;
                font-size: 9px;
                line-height: 1.3;
            }
            .vm-garso-card-title {
                display: flex;
                align-items: center;
                gap: 6px;
                color: ${getEscortPagePinkColor()};
                font-size: 13px;
                font-weight: 800;
            }
            .vm-garso-card-title-warning {
                flex: 0 0 auto;
                color: #ff7777;
                font-size: 14px;
                font-weight: 900;
                line-height: 1;
            }
            .vm-garso-card-meta {
                margin-top: 3px;
                color: #cbb8ce;
                font-size: 10px;
                line-height: 1.25;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-collapsible-header,
            #vm-escort-research-panel .vm-escort-summary-collapsible-header {
                position: relative;
                min-height: 34px;
                box-sizing: border-box;
                padding: 8px 82px 8px 9px !important;
                border: 1px solid rgba(236,69,157,.36);
                border-radius: 6px;
                background: rgba(236,69,157,.09) !important;
                color: ${getEscortPagePinkColor()};
                cursor: pointer;
                user-select: none;
                transition: background .15s ease, border-color .15s ease;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-collapsible-header:hover,
            #vm-escort-research-panel .vm-escort-summary-collapsible-header:hover {
                border-color: rgba(236,69,157,.72);
                background: rgba(236,69,157,.17) !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-collapsible-header:focus-visible,
            #vm-escort-research-panel .vm-escort-summary-collapsible-header:focus-visible {
                outline: 2px solid ${getEscortPagePinkColor()};
                outline-offset: 2px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-collapse-hint,
            #vm-escort-research-panel .vm-escort-summary-collapse-hint {
                position: absolute;
                top: 50%;
                right: 7px;
                transform: translateY(-50%);
                display: inline-flex;
                align-items: center;
                gap: 4px;
                padding: 3px 6px;
                border: 1px solid rgba(236,69,157,.55);
                border-radius: 999px;
                background: rgba(0,0,0,.2);
                color: #ffc3e3;
                font-size: 9px;
                font-weight: 800;
                line-height: 1;
                pointer-events: none;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header,
            #vm-escort-research-panel .vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header {
                background: rgba(236,69,157,.14) !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-integrated-section,
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-timeline-section {
                overflow: hidden;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-integrated-section > .vm-escort-summary-collapsible-header {
                margin: -9px -9px 8px !important;
                border: 0 !important;
                border-bottom: 1px solid rgba(236,69,157,.36) !important;
                border-radius: 0 !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-integrated-section.vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header {
                margin: -9px !important;
                border-bottom: 0 !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-timeline-section > .vm-escort-summary-collapsible-header {
                margin: -8px -9px 8px !important;
                border: 0 !important;
                border-bottom: 1px solid rgba(236,69,157,.36) !important;
                border-radius: 0 !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-timeline-section.vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header {
                margin: -8px -9px -7px !important;
                border-bottom: 0 !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-inconsistencies {
                border: 2px solid rgba(245,77,163,.58);
                border-radius: 7px;
                background: rgba(255,255,255,.035);
                box-shadow: 0 3px 12px rgba(0,0,0,.2);
                overflow: hidden;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-inconsistencies > .vm-escort-summary-collapsible-header {
                margin: 0 !important;
                border: 0 !important;
                border-bottom: 1px solid rgba(236,69,157,.48) !important;
                border-radius: 0 !important;
                color: ${getEscortPagePinkColor()} !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-inconsistencies.vm-escort-summary-section-collapsed > .vm-escort-summary-collapsible-header {
                border-bottom: 0 !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-inconsistencies > .vm-escort-summary-inconsistencies-content {
                border: 0 !important;
                border-radius: 0 !important;
                background: transparent !important;
                box-shadow: none !important;
            }
            #${ESCORT_COMPACT_SUMMARY_ID} {
                display: block;
                clear: both;
                flex: 0 0 100%;
                align-self: stretch;
                width: 100%;
                max-width: 100%;
                box-sizing: border-box;
                margin: 8px 0 12px;
                padding: 8px 9px;
                border: 1px solid ${getEscortPagePinkColor()};
                border-radius: 7px;
                background: rgba(236,69,157,.07);
                color: #fff;
                font: 400 12px/1.3 Lato, Arial, sans-serif;
            }
            #${ESCORT_COMPACT_SUMMARY_ID} > .vm-escort-compact-summary-content {
                display: flex;
                flex-wrap: wrap;
                align-items: center;
                gap: 5px 7px;
            }
            #${ESCORT_COMPACT_SUMMARY_ID} .vm-compact-summary-chip {
                display: inline-block;
                max-width: 100%;
                box-sizing: border-box;
                overflow: hidden;
                padding: 3px 6px;
                border: 1px solid rgba(255,255,255,.2);
                border-radius: 999px;
                background: rgba(255,255,255,.055);
                color: #fff;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            #${ESCORT_COMPACT_SUMMARY_ID} .vm-compact-summary-chip.-ok {
                border-color: rgba(98,207,123,.65);
                background: rgba(46,160,78,.14);
                color: #9aefad;
            }
            #${ESCORT_COMPACT_SUMMARY_ID} .vm-compact-summary-chip.-alert {
                border-color: rgba(255,98,98,.72);
                background: rgba(220,53,69,.16);
                color: #ff9a9a;
            }
            #vm-escort-research-panel {
                width: 100%;
                box-sizing: border-box;
                margin-top: 12px;
                overflow: hidden;
                border: 1px solid ${getEscortPagePinkColor()};
                border-radius: 8px;
                background: rgba(255,255,255,.025);
                color: #fff;
            }
            #vm-escort-research-panel > .vm-research-panel-main-header {
                display: flex;
                align-items: center;
                gap: 8px;
                padding: 10px 11px 8px;
                color: ${getEscortPagePinkColor()};
                font-size: 14px;
                font-weight: 700;
                line-height: 1.2;
            }
            #vm-escort-research-panel .vm-research-panel-main-title {
                min-width: 0;
                flex: 1 1 auto;
            }
            #vm-escort-research-panel .vm-research-panel-refresh {
                flex: 0 0 auto;
                width: 28px;
                height: 28px;
                padding: 0;
                border: 1px solid ${getEscortPagePinkColor()};
                border-radius: 50%;
                background: transparent;
                color: ${getEscortPagePinkColor()};
                font-size: 20px;
                font-weight: 700;
                line-height: 24px;
                cursor: pointer;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-inconsistencies-title {
                display: flex;
                align-items: center;
                gap: 0;
                margin-bottom: 6px;
                font-weight: 800;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-inconsistencies {
                margin: 0 0 10px;
                color: #fff;
                font-size: 12px;
                font-weight: 600;
                line-height: 1.35;
                text-align: left;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-status-badge {
                flex: 0 0 auto;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                font-weight: 800;
                line-height: 1;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-status-alert .vm-inconsistencies-status-badge {
                display: inline;
                margin-left: 5px;
                padding: 0;
                border: 0;
                border-radius: 0;
                background: transparent;
                color: #ff8b8b;
                box-shadow: none;
                font-size: 13px;
                font-weight: 900;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-status-ok .vm-inconsistencies-status-badge {
                margin-left: 6px;
                padding: 3px 7px;
                border: 1px solid rgba(98,207,123,.72);
                border-radius: 999px;
                background: rgba(98,207,123,.12);
                color: #7ee497;
                box-shadow: none;
                font-size: 10px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-empty {
                padding: 8px 9px;
                border: 1px solid rgba(98,207,123,.42);
                border-radius: 6px;
                background: rgba(46,160,78,.08);
                color: #a9efb8;
                font-size: 11px;
                font-weight: 800;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-source-card {
                overflow: hidden;
                border: 1px solid rgba(255,98,98,.35);
                border-radius: 7px;
                background: rgba(255,98,98,.045);
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-source-head {
                display: flex;
                align-items: center;
                gap: 7px;
                padding: 7px 8px;
                border-bottom: 1px solid rgba(255,98,98,.22);
                background: rgba(255,98,98,.06);
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-source-marker {
                flex: 0 0 auto;
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 22px;
                height: 22px;
                border: 1px solid rgba(255,98,98,.55);
                border-radius: 50%;
                background: rgba(255,98,98,.14);
                color: #ff9b9b;
                font-size: 13px;
                font-weight: 900;
                line-height: 1;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-source-title {
                display: block;
                color: #ffabab;
                font-weight: 900;
                line-height: 1.2;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-source-subtitle {
                display: block;
                margin-top: 2px;
                color: #cbb8ce;
                font-size: 9px;
                font-weight: 600;
                line-height: 1.2;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-source-count {
                margin-left: auto;
                padding: 2px 6px;
                border: 1px solid rgba(255,98,98,.45);
                border-radius: 999px;
                color: #ffb3b3;
                font-size: 9px;
                font-weight: 800;
                white-space: nowrap;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-warning-list {
                display: grid;
                gap: 6px;
                padding: 7px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-warning-row {
                display: grid;
                grid-template-columns: 24px minmax(0,1fr);
                align-items: start;
                gap: 7px;
                padding: 6px 7px;
                border-left: 3px solid rgba(255,98,98,.78);
                border-radius: 5px;
                background: rgba(255,255,255,.035);
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-warning-icon {
                display: inline-flex;
                align-items: center;
                justify-content: center;
                min-width: 22px;
                height: 22px;
                border-radius: 5px;
                background: rgba(255,98,98,.13);
                color: #ff9b9b;
                font-size: 10px;
                font-weight: 900;
                line-height: 1;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-warning-label {
                display: block;
                color: #ffabab;
                font-weight: 900;
                line-height: 1.2;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-inconsistencies-warning-detail {
                display: block;
                margin-top: 2px;
                color: #fff;
                font-size: 10px;
                font-weight: 600;
                line-height: 1.25;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-garso-main-head {
                padding-right: 108px !important;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-garso-main-title {
                box-sizing: border-box;
                overflow: hidden;
                padding-right: 0;
                font-size: 12px;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-garso-main-meta {
                overflow: hidden;
                text-overflow: ellipsis;
                white-space: nowrap;
            }
            #vm-escort-research-panel .vm-research-panel-footer {
                padding: 8px 10px 10px;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-escort-summary-inconsistencies-content {
                display: grid;
                gap: 7px;
                padding: 8px 9px;
                border: 0;
                border-radius: 0;
                background: transparent;
                color: #fff;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-garso-download-button {
                position: absolute;
                z-index: 1;
                top: 16px;
                right: 72px;
                transform: translateY(-50%);
                display: inline-flex;
                align-items: center;
                justify-content: center;
                width: 21px;
                height: 21px;
                margin: 0;
                padding: 0;
                border: 1px solid ${getEscortPagePinkColor()};
                border-radius: 50%;
                background: rgba(0,0,0,.2);
                color: #ffc3e3;
                font-size: 14px;
                font-weight: 800;
                line-height: 1;
                cursor: pointer;
            }
            #vm-garso-extended-panel {
                overflow: hidden;
                border: 1px solid rgba(255,255,255,.16);
                border-radius: 7px;
                background: rgba(255,255,255,.025);
            }
            #vm-garso-extended-panel > .vm-garso-extended-head {
                display: flex;
                align-items: center;
                gap: 7px;
                color: ${getEscortPagePinkColor()};
                font-size: 12px;
                font-weight: 800;
            }
            #vm-garso-extended-panel .vm-garso-extended-status-badge {
                flex: 0 0 auto;
                padding: 2px 5px;
                border: 1px solid rgba(98,207,123,.7);
                border-radius: 999px;
                background: rgba(98,207,123,.14);
                color: #62cf7b;
                font-size: 10px;
                line-height: 1;
            }
            #vm-garso-extended-panel .vm-garso-extended-body {
                display: grid;
                gap: 6px;
                padding: 7px;
            }
            #vm-garso-extended-panel .vm-garso-extended-recommendation {
                position: relative;
                align-items: center;
                gap: 9px;
                width: calc(100% - 14px);
                margin: 9px 7px 2px;
                padding: 9px 10px;
                border: 1px solid rgba(255,190,92,.8);
                border-radius: 9px;
                background: linear-gradient(135deg, rgba(245,77,163,.28), rgba(255,176,70,.15));
                box-shadow: 0 5px 15px rgba(0,0,0,.25);
                color: #fff;
                text-align: left;
                cursor: pointer;
            }
            #vm-garso-extended-panel .vm-garso-extended-recommendation-arrow {
                position: absolute;
                top: -6px;
                left: 24px;
                width: 10px;
                height: 10px;
                border-top: 1px solid rgba(255,190,92,.8);
                border-left: 1px solid rgba(255,190,92,.8);
                background: rgba(125,48,94,.98);
                transform: rotate(45deg);
                pointer-events: none;
            }
            #vm-garso-extended-panel .vm-garso-extended-recommendation-icon {
                flex: 0 0 auto;
                font-size: 19px;
                line-height: 1;
            }
            #vm-garso-extended-panel .vm-garso-extended-recommendation-copy {
                min-width: 0;
            }
            #vm-garso-extended-panel .vm-garso-extended-recommendation-title {
                display: block;
                color: #ffe1a8;
                font-size: 11px;
                font-weight: 900;
                line-height: 1.2;
            }
            #vm-garso-extended-panel .vm-garso-extended-recommendation-text {
                display: block;
                margin-top: 3px;
                color: #d8c8d9;
                font-size: 9px;
                font-weight: 600;
                line-height: 1.3;
            }
            #vm-garso-extended-panel .vm-garso-extended-mode-group {
                display: flex;
                flex-wrap: wrap;
                align-items: center;
                gap: 5px 12px;
                margin: 0;
                padding: 6px 7px 7px;
                border: 1px solid rgba(255,255,255,.16);
                border-radius: 6px;
                color: #f2f2f2;
                font-size: 11px;
            }
            #vm-garso-extended-panel .vm-garso-extended-mode-legend {
                padding: 0 4px;
                color: #cbb8ce;
                font-size: 10px;
                font-weight: 700;
            }
            #vm-garso-extended-panel .vm-garso-extended-mode-option {
                display: inline-flex;
                align-items: center;
                gap: 4px;
                white-space: nowrap;
                cursor: pointer;
            }
            #vm-garso-extended-panel .vm-garso-extended-mode-option > input {
                margin: 0;
                accent-color: ${getEscortPagePinkColor()};
            }
            #vm-garso-extended-panel .vm-garso-extended-action {
                width: 100%;
                padding: 8px 12px;
                border: 1px solid ${getEscortPagePinkColor()};
                border-radius: 6px;
                background: transparent;
                color: #f2f2f2;
                font-size: 12px;
                font-weight: 700;
                cursor: pointer;
            }
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-garso-mention-chip:hover,
            #${ESCORT_SUMMARY_SIDE_PANEL_ID} .vm-garso-mention-chip:focus-visible {
                filter: brightness(1.18);
                outline: 1px solid rgba(255,255,255,.72);
                outline-offset: 1px;
            }
            .vm-garso-evidence-overlay {
                position: fixed;
                z-index: 2147483646;
                inset: 0;
                display: flex;
                align-items: center;
                justify-content: center;
                box-sizing: border-box;
                padding: 18px;
                background: rgba(12,3,15,.72);
                font-family: Arial, sans-serif;
            }
            .vm-garso-evidence-dialog {
                display: flex;
                flex-direction: column;
                width: min(760px, 96vw);
                max-height: min(760px, 92vh);
                overflow: hidden;
                border: 1px solid ${getEscortPagePinkColor()};
                border-radius: 10px;
                background: #2a0833;
                color: #fff;
                box-shadow: 0 16px 45px rgba(0,0,0,.55);
            }
            .vm-garso-evidence-header {
                display: flex;
                align-items: center;
                gap: 10px;
                padding: 11px 13px;
                border-bottom: 1px solid rgba(245,77,163,.55);
                background: rgba(245,77,163,.08);
            }
            .vm-garso-evidence-title {
                min-width: 0;
                flex: 1 1 auto;
                color: ${getEscortPagePinkColor()};
                font-size: 15px;
                font-weight: 900;
            }
            .vm-garso-evidence-close {
                flex: 0 0 auto;
                width: 30px;
                height: 30px;
                padding: 0;
                border: 1px solid rgba(255,255,255,.28);
                border-radius: 50%;
                background: transparent;
                color: #fff;
                font-size: 18px;
                cursor: pointer;
            }
            .vm-garso-evidence-content {
                min-height: 0;
                overflow-y: auto;
                padding: 10px;
            }
            .vm-garso-evidence-intro {
                margin: 0 0 9px;
                color: #cbb8ce;
                font-size: 11px;
                line-height: 1.35;
            }
            .vm-garso-evidence-topic {
                margin-top: 8px;
                overflow: hidden;
                border: 1px solid rgba(255,255,255,.16);
                border-radius: 8px;
                background: rgba(255,255,255,.035);
            }
            .vm-garso-evidence-topic-title {
                display: block;
                padding: 8px 10px;
                border-bottom: 1px solid rgba(255,255,255,.12);
                color: #ffc3e3;
                font-size: 12px;
                font-weight: 900;
                line-height: 1.3;
                text-decoration: none;
            }
            .vm-garso-evidence-snippet {
                padding: 8px 10px;
                border-top: 1px solid rgba(255,255,255,.09);
            }
            .vm-garso-evidence-snippet:first-of-type {
                border-top: 0;
            }
            .vm-garso-evidence-meta {
                margin-bottom: 4px;
                color: #a997ae;
                font-size: 9px;
            }
            .vm-garso-evidence-text {
                color: #f4edf5;
                font-size: 11px;
                line-height: 1.42;
            }
            .vm-garso-evidence-text mark {
                padding: 0 2px;
                border-radius: 2px;
                background: #ffcf5a;
                color: #351523;
                font-weight: 800;
            }
            .vm-garso-cancel-button {
                display: none;
                align-self: flex-start;
                margin-top: 8px;
                padding: 5px 9px;
                border: 1px solid #ff7777;
                border-radius: 6px;
                background: rgba(220,53,69,.12);
                color: #ffaaaa;
                font-size: 10px;
                font-weight: 800;
                cursor: pointer;
            }
            .vm-garso-cancel-button.-visible {
                display: inline-flex;
            }
            @media (max-width: 1100px) {
                body.${ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS} {
                    width: 100% !important;
                    max-width: 100% !important;
                }
            }
        `;
        document.head.appendChild(style);
    }

    function appendGarsoEvidenceText(container, snippet, matchText) {
        const text = String(snippet || '');
        const match = String(matchText || '');
        const index = match
            ? text.toLocaleLowerCase('pl-PL').indexOf(
                match.toLocaleLowerCase('pl-PL')
            )
            : -1;
        if (index < 0) {
            container.textContent = text;
            return;
        }
        container.append(
            document.createTextNode(text.slice(0, index)),
            Object.assign(makeElement('mark'), {
                textContent: text.slice(index, index + match.length)
            }),
            document.createTextNode(text.slice(index + match.length))
        );
    }

    function openGarsoMentionEvidenceDialog(entry) {
        document.querySelector('.vm-garso-evidence-overlay')?.remove();
        ensureEscortSummarySidePanelStyle();

        const overlay = makeElement('div', 'vm-garso-evidence-overlay');
        const dialog = makeElement('section', 'vm-garso-evidence-dialog');
        dialog.setAttribute('role', 'dialog');
        dialog.setAttribute('aria-modal', 'true');

        const header = makeElement('div', 'vm-garso-evidence-header');
        const title = makeElement('div', 'vm-garso-evidence-title', `Dlaczego zliczono: ${entry?.label || 'wzmiankę'} (${entry?.count || 0})`);
        const close = makeButton('vm-garso-evidence-close', '×');
        close.title = 'Zamknij';
        header.append(title, close);

        const content = makeElement('div', 'vm-garso-evidence-content');
        const intro = makeElement('p', 'vm-garso-evidence-intro', 'Każdy fragment odpowiada jednemu postowi zaliczonemu do tego licznika. Kliknij tytuł, aby otworzyć temat Garso.');
        content.appendChild(intro);

        const evidence = Array.isArray(entry?.evidence) ? entry.evidence : [];
        if (!evidence.length) {
            const empty = makeElement('div', 'vm-garso-evidence-intro', 'Brak zapisanych fragmentów. Odśwież analizę Garso, aby utworzyć uzasadnienia.');
            content.appendChild(empty);
        } else {
            const groups = new Map();
            for (const item of evidence) {
                const key = item.url || item.topicTitle || 'temat';
                if (!groups.has(key)) groups.set(key, []);
                groups.get(key).push(item);
            }
            for (const items of groups.values()) {
                const first = items[0];
                const topic = makeElement('article', 'vm-garso-evidence-topic');
                const topicTitle = first.url
                    ? makeElement('a')
                    : makeElement('div');
                topicTitle.className = 'vm-garso-evidence-topic-title';
                topicTitle.textContent = first.topicTitle || 'Temat Garso';
                if (first.url) {
                    topicTitle.href = first.url;
                    topicTitle.target = '_blank';
                    topicTitle.rel = 'noopener noreferrer';
                }
                topic.appendChild(topicTitle);
                for (const item of items) {
                    const row = makeElement('div', 'vm-garso-evidence-snippet');
                    const meta = makeElement('div', 'vm-garso-evidence-meta');
                    meta.textContent = [item.author, item.date]
                        .filter(Boolean)
                        .join(' • ') || 'post Garso';
                    const fragment = makeElement('div', 'vm-garso-evidence-text');
                    appendGarsoEvidenceText(fragment, item.snippet, item.match);
                    row.append(meta, fragment);
                    topic.appendChild(row);
                }
                content.appendChild(topic);
            }
        }

        let onKeyDown = null;
        const dismiss = () => {
            if (onKeyDown) document.removeEventListener('keydown', onKeyDown);
            overlay.remove();
        };
        close.addEventListener('click', dismiss);
        overlay.addEventListener('click', event => {
            if (event.target === overlay) dismiss();
        });
        onKeyDown = event => {
            if (event.key !== 'Escape') return;
            dismiss();
        };
        document.addEventListener('keydown', onKeyDown);

        dialog.append(header, content);
        overlay.appendChild(dialog);
        document.body.appendChild(overlay);
        close.focus();
    }

    function formatGarsoCacheTimestamp(timestamp) {
        const time = Number(timestamp);
        if (!Number.isFinite(time) || time <= 0) return 'brak zapisanego cache';
        const date = new Date(time).toLocaleString('pl-PL', {
            day: '2-digit',
            month: '2-digit',
            year: 'numeric',
            hour: '2-digit',
            minute: '2-digit'
        });
        const age = formatEscortLastVisit(time);
        return age ? `${date} • ${age}` : date;
    }

    function createEscortSummarySidePanel() {
        document.getElementById(ESCORT_SUMMARY_SIDE_PANEL_ID)?.remove();
        document.getElementById('vm-escort-summary-side-toggle')?.remove();
        document.body.classList.remove(ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS);
        ensureEscortSummarySidePanelStyle();

        if (!SETTINGS.showSummarySidePanel) {
            return {
                panel: null,
                content: makeElement('div'),
                onOpen() {}
            };
        }

        const panel = makeElement('aside');
        panel.id = ESCORT_SUMMARY_SIDE_PANEL_ID;
        panel.setAttribute('aria-label', 'Podsumowanie anonsu');
        let panelWidth = normalizeEscortSummarySidePanelWidth(
            GM_getValue(
                ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY,
                ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH
            )
        );
        const applyPanelWidth = value => {
            panelWidth = normalizeEscortSummarySidePanelWidth(value);
            document.documentElement.style.setProperty(
                '--vm-escort-summary-panel-width',
                `${panelWidth}px`
            );
            return panelWidth;
        };
        applyPanelWidth(panelWidth);

        const resizeHandle = makeElement('div', 'vm-escort-summary-side-resize-handle');
        resizeHandle.title = 'Przeciągnij, aby zmienić szerokość panelu. Kliknij dwukrotnie, aby przywrócić domyślną.';
        resizeHandle.setAttribute('role', 'separator');
        resizeHandle.setAttribute('aria-orientation', 'vertical');

        const header = makeElement('div', 'vm-summary-panel-header');
        const title = makeElement('div', 'vm-summary-panel-title', 'Podsumowanie');
        const pin = makeElement('button', 'vm-summary-panel-pin');
        pin.type = 'button';
        const pinIcon = document.createElementNS(
            'http://www.w3.org/2000/svg',
            'svg'
        );
        pinIcon.classList.add('vm-summary-panel-pin-icon');
        pinIcon.setAttribute('viewBox', '0 0 24 24');
        pinIcon.setAttribute('aria-hidden', 'true');
        const pinPath = document.createElementNS(
            'http://www.w3.org/2000/svg',
            'path'
        );
        pinPath.setAttribute(
            'd',
            'M9 2.5h6l-.8 5.3 3.3 3.3v1.7H13l-1 8.7-1-8.7H6.5v-1.7l3.3-3.3L9 2.5z'
        );
        pinIcon.appendChild(pinPath);
        pin.appendChild(pinIcon);
        header.append(title, pin);

        const content = makeElement('div', 'vm-escort-summary-side-content');
        panel.append(resizeHandle, header, content);

        const toggle = makeElement('button');
        toggle.id = 'vm-escort-summary-side-toggle';
        toggle.type = 'button';
        toggle.textContent = 'Podsumowanie';
        toggle.title = 'Pokaż lub ukryj podsumowanie';

        const openCallbacks = new Set();
        let pinned = !!GM_getValue(ESCORT_SUMMARY_SIDE_PANEL_PINNED_KEY, true);
        let opened = pinned;
        const renderPin = () => {
            pin.setAttribute('aria-pressed', String(pinned));
            pin.title = pinned
                ? 'Panel przypięty - będzie otwierany automatycznie'
                : 'Przypnij panel, aby otwierał się automatycznie';
            pin.setAttribute('aria-label', pin.title);
            pinIcon.style.transform = pinned ? 'rotate(0deg)' : 'rotate(-45deg)';
            pin.style.background = pinned
                ? '#ffffff'
                : 'rgba(255,255,255,.07)';
            pin.style.borderColor = pinned
                ? '#ffffff'
                : 'rgba(255,255,255,.25)';
            pin.style.color = pinned ? '#3a1735' : '#ffffff';
            pin.style.boxShadow = pinned
                ? '0 0 0 2px rgba(255,255,255,.22)'
                : 'none';
        };
        const setOpen = value => {
            opened = !!value;
            document.body.classList.toggle(
                ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS,
                opened
            );
            toggle.textContent = opened ? 'Zwiń ›' : '‹ Podsumowanie';
            toggle.title = opened ? 'Zwiń podsumowanie' : 'Pokaż podsumowanie';
            if (opened) openCallbacks.forEach(callback => callback());
        };
        toggle.addEventListener('click', () => setOpen(!opened));
        pin.addEventListener('click', () => {
            pinned = !pinned;
            GM_setValue(ESCORT_SUMMARY_SIDE_PANEL_PINNED_KEY, pinned);
            renderPin();
            if (pinned) setOpen(true);
        });

        const finishResize = event => {
            if (!document.body.classList.contains(ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS)) {
                return;
            }
            document.body.classList.remove(ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS);
            if (event?.pointerId != null && resizeHandle.hasPointerCapture(event.pointerId)) {
                resizeHandle.releasePointerCapture(event.pointerId);
            }
            GM_setValue(ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY, panelWidth);
        };
        resizeHandle.addEventListener('pointerdown', event => {
            if (event.button !== 0) return;
            event.preventDefault();
            resizeHandle.setPointerCapture(event.pointerId);
            document.body.classList.add(ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS);
        });
        resizeHandle.addEventListener('pointermove', event => {
            if (!document.body.classList.contains(ESCORT_SUMMARY_SIDE_PANEL_RESIZING_CLASS)) {
                return;
            }
            applyPanelWidth(window.innerWidth - event.clientX);
        });
        resizeHandle.addEventListener('pointerup', finishResize);
        resizeHandle.addEventListener('pointercancel', finishResize);
        resizeHandle.addEventListener('dblclick', () => {
            applyPanelWidth(ESCORT_SUMMARY_SIDE_PANEL_DEFAULT_WIDTH);
            GM_setValue(ESCORT_SUMMARY_SIDE_PANEL_WIDTH_KEY, panelWidth);
        });

        document.body.append(panel, toggle);
        renderPin();
        setOpen(pinned);
        return {
            panel,
            content,
            onOpen(callback) {
                if (typeof callback === 'function') openCallbacks.add(callback);
            }
        };
    }

    function createGarsoSideSummaryCard(sidePanel, btn, titleText) {
        const card = makeElement('section', 'vm-escort-summary-side-card');
        const head = makeElement('div', 'vm-escort-summary-side-card-head');
        const title = makeElement('div', 'vm-garso-card-title');
        const titleLabel = makeElement('span', '', titleText);
        const titleWarning = makeElement('span', 'vm-garso-card-title-warning', '⚠︎');
        titleWarning.hidden = true;
        title.append(titleLabel, titleWarning);
        const meta = makeElement('div', 'vm-garso-card-meta');
        head.append(title, meta);
        const body = makeElement('div', 'vm-escort-summary-side-card-body');
        const bodyContent = makeElement('div');
        const cancelButton = makeButton('vm-garso-cancel-button', 'Przerwij');
        cancelButton.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            const token = btn._vmGarsoSummaryCancelToken;
            if (!token || token.cancelled) return;
            cancelButton.disabled = true;
            cancelButton.textContent = 'Przerywam…';
            token.cancel('Pełna analiza Garso przerwana przez użytkownika');
        });
        bodyContent.addEventListener('click', event => {
            const chip = event.target.closest('.vm-garso-mention-chip');
            if (!chip || !bodyContent.contains(chip)) return;
            event.preventDefault();
            event.stopPropagation();
            const entries = getGarsoMentionEntries(btn._vmGarsoSummaryResult);
            entries.sort((a, b) =>
                b.count - a.count || a.label.localeCompare(b.label, 'pl-PL')
            );
            const index = Number(chip.dataset.vmGarsoMentionIndex);
            if (Number.isInteger(index) && entries[index]) {
                openGarsoMentionEvidenceDialog(entries[index]);
            }
        });
        body.append(bodyContent, cancelButton);
        card.append(head, body);
        sidePanel.content.appendChild(card);
        const collapsible = makeEscortSummarySectionCollapsible(
            card,
            head,
            body,
            `garso-${btn.dataset.garsoSearchMode || normalizeText(titleText).replace(/\s+/g, '-')}`,
            true
        );

        const render = detail => {
            const state = detail?.state || btn.dataset.garsoSummaryState || '';
            const html = detail?.html ?? btn.dataset.garsoSummaryHtml ?? '';
            const result = detail?.result || btn._vmGarsoSummaryResult || null;
            const summaryCheckedAt = Number(
                detail?.checkedAt ?? btn.dataset.garsoSummaryCheckedAt
            ) || null;
            const countCheckedAt = Number(btn.dataset.garsoCountCheckedAt) || null;
            const checkedAt = summaryCheckedAt || countCheckedAt;
            const cacheState = detail?.cacheState ||
                btn.dataset.garsoSummaryCacheState || '';

            if (html) bodyContent.innerHTML = html;
            else if (state === 'empty') bodyContent.textContent = 'Brak tematów do analizy.';
            else if (state === 'count-only') {
                bodyContent.textContent =
                    'Sprawdzono tylko, czy są tematy. Aby przeanalizować tematy, kliknij przycisk odświeżania w nagłówku sekcji.';
            }
            else if (state === 'loading') bodyContent.textContent = 'Obliczanie statystyk…';
            else if (state === 'cancelled') bodyContent.textContent = 'Analiza została przerwana.';
            else if (state === 'error') bodyContent.textContent = 'Nie udało się przygotować analizy.';
            else bodyContent.textContent = 'Analiza pojawi się po sprawdzeniu Garsoniery.';

            const cancellationAvailable =
                ['loading', 'refreshing'].includes(state) &&
                btn._vmGarsoSummaryCancelToken &&
                !btn._vmGarsoSummaryCancelToken.cancelled;
            cancelButton.classList.toggle('-visible', !!cancellationAvailable);
            cancelButton.disabled = false;
            cancelButton.textContent = 'Przerwij';

            const reviewWarning = getGarsoReviewWarning(result);
            titleWarning.hidden = !reviewWarning.show;
            titleWarning.setAttribute(
                'aria-hidden',
                String(!reviewWarning.show)
            );
            titleWarning.title = reviewWarning.title;

            if (checkedAt) {
                const prefix = state === 'count-only'
                    ? 'Sprawdzenie obecności tematów'
                    : (!summaryCheckedAt
                    ? 'Liczba tematów z cache'
                    : (['session', 'partial'].includes(cacheState)
                        ? 'Analiza bieżąca'
                        : 'Cache analizy'));
                let suffix = '';
                if (cacheState === 'refreshing') suffix = ' • trwa odświeżanie';
                else if (cacheState === 'stale') suffix = ' • cache nieaktualny';
                else if (cacheState === 'partial') suffix = ' • analiza niepełna, nie zapisano';
                else if (!summaryCheckedAt) suffix = ' • brak cache analizy';
                meta.textContent = `${prefix}: ${formatGarsoCacheTimestamp(checkedAt)}${suffix}`;
            } else if (state === 'loading') {
                meta.textContent = 'Brak cache • trwa analiza';
            } else {
                meta.textContent = 'Brak zapisanego cache';
            }
            meta.title = meta.textContent;
        };
        btn.addEventListener('vm-garso-summary-state', event => render(event.detail));
        btn.addEventListener('vm-garso-count-state', () => render());
        sidePanel.onOpen(() => render());
        render();
        return {
            card,
            head,
            title,
            titleLabel,
            titleWarning,
            meta,
            body,
            bodyContent,
            collapsible
        };
    }

    const ESCORT_COMPACT_SUMMARY_ID = 'vm-escort-compact-summary';

    function findEscortCompactSummaryPlacement() {
        const map = document.querySelector(
            '.content-info-col.-desc .content-location > #map'
        );
        const locationBox = map?.parentElement;
        if (!locationBox?.classList.contains('content-location')) return null;

        return {
            parent: locationBox,
            reference: null,
            preceding: null
        };
    }

    function createEscortCompactSummaryBox() {
        document.getElementById(ESCORT_COMPACT_SUMMARY_ID)?.remove();

        const box = makeElement('section');
        box.id = ESCORT_COMPACT_SUMMARY_ID;
        box.setAttribute('aria-label', 'Skrócone podsumowanie anonsu');

        const content = makeElement('div', 'vm-escort-compact-summary-content');
        box.appendChild(content);

        const placeBox = () => {
            const placement = findEscortCompactSummaryPlacement();
            if (!placement?.parent) return false;
            placement.parent.style.setProperty('flex-wrap', 'wrap', 'important');
            if (
                box.parentNode !== placement.parent ||
                box.nextSibling !== placement.reference
            ) {
                placement.parent.insertBefore(box, placement.reference || null);
            }
            placement.preceding?.style.setProperty(
                'margin-bottom',
                '0',
                'important'
            );
            return true;
        };

        if (!placeBox()) {
            let attempts = 0;
            const timer = setInterval(() => {
                attempts++;
                if (placeBox() || attempts >= 40) clearInterval(timer);
            }, 250);
        }

        return { box, content };
    }

    function getMostFrequentGarsoMentions(result) {
        const candidates = getGarsoMentionEntries(result, true);

        const maximumCount = candidates.reduce(
            (maximum, item) => Math.max(maximum, item.count),
            0
        );
        return maximumCount > 0
            ? candidates.filter(item => item.count === maximumCount)
            : [];
    }

    function createEscortResearchPanel(phone, adUrl) {
        const sidePanel = createEscortSummarySidePanel();
        const panel = makeElement('section');
        panel.id = 'vm-escort-research-panel';
        panel.setAttribute('aria-label', 'Linki do powiązanych wyników');

        const header = makeElement('div', 'vm-research-panel-main-header');
        const headerTitle = makeElement('div', 'vm-research-panel-main-title', 'Linki');
        const refreshButton = makeButton('vm-research-panel-refresh', '↻');
        refreshButton.title = 'Pobierz ponownie dane z sekcji „Linki” (Garsoniera: tylko sprawdzenie, czy są tematy)';
        refreshButton.setAttribute('aria-label', refreshButton.title);
        header.append(headerTitle, refreshButton);
        panel.appendChild(header);

        const phoneSearch = digitsOnly(phone);
        const adId = parseAdIdFromUrl(adUrl);
        const garsoPhoneTerm = `"${phone}"`;
        const garsoAddressTerm = buildGarsoAdLinkSearchTerm(adId);
        const garsoCombinedSummaryTerm = buildCombinedGarsoSummaryTerm(
            garsoPhoneTerm,
            garsoAddressTerm
        );
        const garsoPhoneRow = createGarsoButton(
            'nr tel.',
            garsoPhoneTerm,
            'phone',
            true,
            true
        );
        const garsoAddressRow = createGarsoButton(
            'adres anonsu',
            garsoAddressTerm,
            'address',
            true,
            true
        );
        const garsoCombinedSummaryState = makeElement('button');
        garsoCombinedSummaryState.dataset.garsoDisplayTerm = 'nr tel. + adres anonsu';
        garsoCombinedSummaryState.dataset.garsoSearchMode = 'combined';
        garsoCombinedSummaryState.dataset.garsoStatus = 'manual';
        const cachedCombinedGarsoSummary = restoreGarsoSummaryFromCache(
            garsoCombinedSummaryState,
            garsoCombinedSummaryTerm,
            'combined'
        );
        styleResearchPanelRow(garsoPhoneRow);
        styleResearchPanelRow(garsoAddressRow);
        panel.append(garsoPhoneRow, garsoAddressRow);

        const cachedGarsoPhone = getGarsoSearchCountCache('phone', garsoPhoneTerm);
        const cachedGarsoAddress = getGarsoSearchCountCache('address', garsoAddressTerm);
        const researchFacts = {
            garsoPhoneCount: cachedGarsoPhone?.fresh ? cachedGarsoPhone.count : null,
            garsoAddressCount: cachedGarsoAddress?.fresh ? cachedGarsoAddress.count : null,
            escortiPhoneResult: null,
            escortiAddressResult: null,
            escortiPhoneSummary: null,
            escortiAddressSummary: null,
            escortPhoneSummary: null,
            sameEscortiResult: null,
            escortiWarnings: [],
            escortiLoading: true,
            escortPhoneLoading: true,
            photoRange: null,
            photoDatesLoading: true,
            garsoPhoneSummaryResult:
                garsoCombinedSummaryState._vmGarsoSummaryResult || null,
            garsoAddressSummaryResult:
                garsoCombinedSummaryState._vmGarsoSummaryResult || null,
            datePosted: extractEscortClubStructuredAdData(document).datePosted || null,
            currentAdUrl: adUrl,
            garsoPhoneReviewDates: {
                first: garsoCombinedSummaryState.dataset.garsoFirstReviewDate || null,
                last: garsoCombinedSummaryState.dataset.garsoLastReviewDate || null,
                firstUrl: garsoCombinedSummaryState.dataset.garsoFirstReviewUrl || null,
                lastUrl: garsoCombinedSummaryState.dataset.garsoLastReviewUrl || null
            },
            garsoAddressReviewDates: {
                first: garsoCombinedSummaryState.dataset.garsoFirstReviewDate || null,
                last: garsoCombinedSummaryState.dataset.garsoLastReviewDate || null,
                firstUrl: garsoCombinedSummaryState.dataset.garsoFirstReviewUrl || null,
                lastUrl: garsoCombinedSummaryState.dataset.garsoLastReviewUrl || null
            }
        };
        const compactSummaryUi = SETTINGS.showCompactSummaryAboveDescription
            ? createEscortCompactSummaryBox()
            : null;

        const photoHistogramHost = makeElement('div');
        sidePanel.content.appendChild(photoHistogramHost);

        const renderCompactResearchSummary = () => {
            if (!compactSummaryUi) return;

            const { content } = compactSummaryUi;
            content.replaceChildren();
            const addChip = (text, status = 'normal', title = '') => {
                const chip = makeElement('span', 'vm-compact-summary-chip');
                if (status === 'ok' || status === 'alert') {
                    chip.classList.add(`-${status}`);
                }
                chip.textContent = text;
                if (title) chip.title = title;
                content.appendChild(chip);
                return chip;
            };

            const escortiResults = [
                researchFacts.escortiPhoneResult,
                researchFacts.escortiAddressResult
            ].filter(result => result?.status === 'ok');
            const profileDates = escortiResults
                .flatMap(getEscortiProfileSummariesForResearch)
                .map(profile => formatEscortiDate(profile?.creationDate))
                .filter(Boolean)
                .sort((a, b) =>
                    (profileDateToTime(a) || Infinity) -
                    (profileDateToTime(b) || Infinity)
                );
            if (profileDates.length) {
                addChip(
                    `Pierwszy profil: ${formatResearchDateAge(profileDates[0])}`,
                    'normal',
                    'Najstarsza znaleziona data utworzenia profilu na Escorti.pl powiązanego z tym anonsem lub numerem telefonu. Wiek w nawiasie jest liczony do dzisiaj.'
                );
            }

            const aggregationSource = getCompactSummaryAggregationSource(researchFacts);
            const comparisonSummary = aggregationSource.summary || null;
            const aggregationTooltip = buildCompactSummaryAggregationTooltip(
                aggregationSource
            );
            const relatedAdsCount = Math.max(
                0,
                Number(aggregationSource.result?.adLinks) || 0,
                Number(comparisonSummary?.totalChecked) || 0
            );
            if (relatedAdsCount > 1) {
                if (aggregationSource.loading) {
                    addChip(
                        'Rozbieżności: sprawdzanie…',
                        'normal',
                        `Trwa sprawdzanie rozbieżności.\n\n${aggregationTooltip}`
                    );
                } else {
                    const warnings = [...new Set(
                        getEscortActiveAdsConsistencyWarnings(comparisonSummary)
                    )];
                    const comparableAds = (comparisonSummary?.results || [])
                        .filter(item => item?.active && item.adData)
                        .length;
                    if (warnings.length) {
                        addChip(
                            `Rozbieżności (${comparableAds} ogł.): tak`,
                            'alert',
                            'Wykryte rozbieżności:\n' +
                            warnings.map(warning => `• ${warning}`).join('\n') +
                            `\n\n${aggregationTooltip}`
                        );
                    } else if (comparableAds >= 2) {
                        addChip(
                            `Rozbieżności (${comparableAds} ogł.): nie`,
                            'ok',
                            `Wykryte rozbieżności: brak.\n\n${aggregationTooltip}`
                        );
                    } else {
                        addChip(
                            `Rozbieżności (${comparableAds} ogł.): brak danych porównawczych`,
                            'normal',
                            'Nie wykryto rozbieżności, ponieważ nie znaleziono co najmniej dwóch aktywnych anonsów z pełnymi danymi możliwymi do porównania.' +
                            `\n\n${aggregationTooltip}`
                        );
                    }
                }
            }

            const simultaneousDifferentCitiesSummary =
                Number(comparisonSummary?.activeCount) >= 2 &&
                Array.isArray(comparisonSummary?.cities) &&
                comparisonSummary.cities.length >= 2
                    ? comparisonSummary
                    : null;
            if (
                simultaneousDifferentCitiesSummary &&
                !aggregationSource.loading
            ) {
                const activeCount = Number(
                    simultaneousDifferentCitiesSummary.activeCount
                ) || 0;
                const cities = simultaneousDifferentCitiesSummary.cities
                    .map(normalizeEscortCity)
                    .filter(Boolean);
                addChip(
                    `Różne lokalizacje jednocześnie: ` +
                    `${formatResearchPanelList(cities, 4)} ` +
                    `(${activeCount} ${activeEscortAdsWord(activeCount)} ` +
                    `${polishAdWord(activeCount)})`,
                    'alert',
                    `${aggregationTooltip}\n\nRównocześnie aktywne anonse z wybranego źródła wskazują różne lokalizacje.`
                );
            }

            const garsoResults = [...new Set([
                researchFacts.garsoPhoneSummaryResult,
                researchFacts.garsoAddressSummaryResult
            ].filter(result => result?.stats))];
            const bestGarsoResult = garsoResults.sort((a, b) =>
                (Number(b.stats?.reviewCount) || 0) -
                    (Number(a.stats?.reviewCount) || 0) ||
                (Number(b.stats?.mean != null) - Number(a.stats?.mean != null)) ||
                (Number(b.topicInfo?.length) || 0) -
                    (Number(a.topicInfo?.length) || 0)
            )[0] || null;

            if (bestGarsoResult?.stats?.mean != null) {
                const reviewCount = Math.max(
                    0,
                    Number(bestGarsoResult.stats.reviewCount) || 0
                );
                addChip(
                    `Średnia Garso: ${bestGarsoResult.stats.mean}/10`,
                    'normal',
                    reviewCount
                        ? `Średnia z ${reviewCount} ocenionych recenzji.`
                        : ''
                );
            }

            const garsoMentionCandidates = garsoResults
                .flatMap(getMostFrequentGarsoMentions);
            const maximumMentionCount = garsoMentionCandidates.reduce(
                (maximum, item) => Math.max(maximum, item.count),
                0
            );
            const popularMentions = [...new Map(
                garsoMentionCandidates
                    .filter(item => item.count === maximumMentionCount)
                    .map(item => [item.label, item])
            ).values()];
            if (popularMentions.length && maximumMentionCount > 0) {
                const allLabels = popularMentions.map(item => item.label);
                const popularMentionChip = addChip(
                    '',
                    'normal',
                    `Najwyższa liczba wystąpień (${maximumMentionCount}):\n` +
                        allLabels.map(label => `• ${label}`).join('\n')
                );
                popularMentionChip.dataset.vmGarsoPopularMentions = '1';

                const renderPopularMentions = visibleCount => {
                    const visibleMentions = popularMentions.slice(0, visibleCount);
                    const nodes = [document.createTextNode('Najczęściej: ')];
                    visibleMentions.forEach((item, index) => {
                        if (index) nodes.push(document.createTextNode(', '));
                        const label = makeElement('span', '', item.label);
                        label.style.color = getGarsoMentionToneStyle(
                            item.tone
                        ).color;
                        label.style.fontWeight = '800';
                        nodes.push(label);
                    });
                    nodes.push(document.createTextNode(` (${maximumMentionCount})`));
                    popularMentionChip.replaceChildren(...nodes);
                };

                const fitPopularMentions = () => {
                    if (!popularMentionChip.isConnected) return;
                    const availableWidth = Math.floor(
                        content.getBoundingClientRect().width
                    );
                    if (!availableWidth) return;

                    for (
                        let visibleCount = allLabels.length;
                        visibleCount >= 1;
                        visibleCount--
                    ) {
                        renderPopularMentions(visibleCount);
                        if (
                            visibleCount === 1 ||
                            popularMentionChip.scrollWidth <= availableWidth + 1
                        ) {
                            break;
                        }
                    }
                };
                compactSummaryUi.fitPopularGarsoMentions = fitPopularMentions;
                requestAnimationFrame(fitPopularMentions);
                if (
                    !compactSummaryUi.popularMentionsResizeObserver &&
                    typeof ResizeObserver === 'function'
                ) {
                    compactSummaryUi.popularMentionsResizeObserver =
                        new ResizeObserver(() => {
                            compactSummaryUi.fitPopularGarsoMentions?.();
                        });
                    compactSummaryUi.popularMentionsResizeObserver.observe(content);
                }
            }

            const subtitles = [...new Set(
                garsoResults.flatMap(result =>
                    (result.topicInfo || [])
                        .map(topic => getDisplayableGarsoTopicSubtitle(topic?.subtitle))
                        .filter(Boolean)
                )
            )];
            if (subtitles.length) {
                const firstSubtitle = subtitles[0].length > 100
                    ? `${subtitles[0].slice(0, 97).trim()}…`
                    : subtitles[0];
                addChip(
                    `Dopisek z Garso: ${firstSubtitle}${subtitles.length > 1
                        ? ` (+${subtitles.length - 1})`
                        : ''}`,
                    'normal',
                    subtitles.join('\n')
                );
            }

            if (!content.childElementCount) {
                addChip(
                    researchFacts.escortiLoading
                        ? 'Pobieranie skróconego podsumowania…'
                        : 'Brak danych do skróconego podsumowania.'
                );
            }
        };

        let updateGarsoExtendedRecommendation = () => {};
        const renderResearchFacts = () => {
            photoHistogramHost.replaceChildren(
                createEscortResearchDateTimeline(researchFacts)
            );
            renderCompactResearchSummary();
            updateGarsoExtendedRecommendation();
        };

        const bindGarsoCountForFacts = (row, key) => {
            row.addEventListener('vm-garso-count-state', event => {
                researchFacts[key] = Number.isFinite(event.detail?.count)
                    ? Number(event.detail.count)
                    : null;
                renderResearchFacts();
            });
        };
        bindGarsoCountForFacts(garsoPhoneRow, 'garsoPhoneCount');
        bindGarsoCountForFacts(garsoAddressRow, 'garsoAddressCount');
        garsoCombinedSummaryState.addEventListener(
            'vm-garso-summary-state',
            event => {
                const reviewDates = {
                    first: event.detail?.firstReviewDate || null,
                    last: event.detail?.lastReviewDate || null,
                    firstUrl: event.detail?.firstReviewUrl || null,
                    lastUrl: event.detail?.lastReviewUrl || null
                };
                researchFacts.garsoPhoneReviewDates = { ...reviewDates };
                researchFacts.garsoAddressReviewDates = { ...reviewDates };
                if (event.detail?.result) {
                    researchFacts.garsoPhoneSummaryResult = event.detail.result;
                    researchFacts.garsoAddressSummaryResult = event.detail.result;
                }
                renderResearchFacts();
            }
        );
        renderResearchFacts();


        const escortiPhoneUrl =
            `${ESCORTI_BASE_URL}search?search=${encodeURIComponent(phoneSearch)}`;
        const escortiAddressUrl =
            `${ESCORTI_BASE_URL}search?search=${encodeURIComponent(adUrl)}`;
        const escortClubPhoneUrl = buildEscortClubPhoneSearchUrl(phone);
        const escortiAddressRow = createResearchPanelResultRow(
            'Escorti.pl',
            escortiAddressUrl
        );
        const escortiPhoneRow = createResearchPanelResultRow(
            'Escorti.pl - nr tel.',
            escortiPhoneUrl
        );
        escortiPhoneRow.style.display = 'none';
        const escortPhoneRow = createResearchPanelResultRow(
            'Escort - nr tel.',
            escortClubPhoneUrl
        );
        escortPhoneRow.title =
            'Aktualnie znalezione na Escort.club anonse z tym samym numerem telefonu.';
        panel.append(escortiAddressRow, escortiPhoneRow, escortPhoneRow);

        const warningBox = makeElement('section', 'vm-escort-summary-inconsistencies vm-escort-summary-status-ok');
        const warningTitle = makeElement('div', 'vm-escort-summary-inconsistencies-title');
        const warningTitleText = makeElement('span', '', 'Niezgodności');
        const warningStatusBadge = makeElement('span', 'vm-inconsistencies-status-badge');
        warningTitle.append(warningTitleText, warningStatusBadge);
        const warningContent = makeElement('div', 'vm-escort-summary-inconsistencies-content');
        warningBox.append(warningTitle, warningContent);
        makeEscortSummarySectionCollapsible(
            warningBox,
            warningTitle,
            warningContent,
            'inconsistencies',
            true
        );
        const garsoSummaryCard = createGarsoSideSummaryCard(
            sidePanel,
            garsoCombinedSummaryState,
            'Garso - recenzje'
        );
        garsoSummaryCard.head.classList.add('vm-garso-main-head');
        garsoSummaryCard.title.classList.add('vm-garso-main-title');
        garsoSummaryCard.meta.classList.add('vm-garso-main-meta');
        const garsoCollapseHint = garsoSummaryCard.head.querySelector(
            '.vm-escort-summary-collapse-hint'
        );
        if (garsoCollapseHint) garsoCollapseHint.style.top = '16px';
        const garsoDownloadButton = makeButton('vm-garso-download-button', '↻');
        garsoDownloadButton.title =
            'Odśwież i przeanalizuj treść tematów Garsoniery dla numeru telefonu i adresu anonsu.';
        garsoDownloadButton.setAttribute('aria-label', garsoDownloadButton.title);
        garsoSummaryCard.head.appendChild(garsoDownloadButton);
        sidePanel.content.prepend(warningBox);

        const footer = makeElement('div', 'vm-research-panel-footer');
        const garsoExtendedPanel = makeElement('section');
        garsoExtendedPanel.id = 'vm-garso-extended-panel';
        const garsoExtendedHead = makeElement('div', 'vm-garso-extended-head');
        const garsoExtendedTitle = makeElement('span', '', 'Garso - rozszerzone szukanie tematów');
        garsoExtendedTitle.title = 'Nr tel.: numer z bieżącego anonsu → Escorti.pl → linki znalezionych anonsów → wyszukiwanie tych linków na Garso. Jeżeli Garso znajdzie tematy, można otworzyć je w nowych kartach przyciskiem „Otwieranie”. Wariant „adres” działa analogicznie, zaczynając od adresu bieżącego anonsu.';
        garsoExtendedTitle.style.flex = '1 1 auto';
        garsoExtendedTitle.style.minWidth = '0';
        const garsoExtendedStatusBadge = makeElement('span', 'vm-garso-extended-status-badge', '✓');
        garsoExtendedStatusBadge.hidden = true;
        garsoExtendedHead.append(
            garsoExtendedTitle,
            garsoExtendedStatusBadge
        );
        const garsoExtendedRecommendation = makeButton('vm-garso-extended-recommendation');
        garsoExtendedRecommendation.style.display = 'none';
        garsoExtendedRecommendation.title =
            'Kliknij, aby rozwinąć rozszerzone szukanie tematów Garso.';
        const garsoExtendedRecommendationArrow = makeElement('span', 'vm-garso-extended-recommendation-arrow');
        const garsoExtendedRecommendationIcon = makeElement('span', '', '💡');
        garsoExtendedRecommendationIcon.className =
            'vm-garso-extended-recommendation-icon';
        const garsoExtendedRecommendationCopy = makeElement('span', 'vm-garso-extended-recommendation-copy');
        const garsoExtendedRecommendationTitle = makeElement('span', 'vm-garso-extended-recommendation-title', 'Warto sprawdzić szerzej');
        const garsoExtendedRecommendationText = makeElement('span', 'vm-garso-extended-recommendation-text');
        garsoExtendedRecommendationCopy.append(
            garsoExtendedRecommendationTitle,
            garsoExtendedRecommendationText
        );
        garsoExtendedRecommendation.append(
            garsoExtendedRecommendationArrow,
            garsoExtendedRecommendationIcon,
            garsoExtendedRecommendationCopy
        );
        const garsoExtendedBody = makeElement('div', 'vm-garso-extended-body');
        let garsoExtendedSearchMode = normalizeGarsoExtendedSearchMode(
            GM_getValue(GARSO_EXTENDED_SEARCH_MODE_KEY, 'address')
        );
        const garsoExtendedModeGroup = makeElement('fieldset', 'vm-garso-extended-mode-group');
        const garsoExtendedModeLegend = makeElement('legend', 'vm-garso-extended-mode-legend', 'Szukaj na Escorti.pl:');
        garsoExtendedModeGroup.appendChild(garsoExtendedModeLegend);
        const garsoExtendedModeOptions = [
            ['phone', 'nr tel.'],
            ['address', 'adres'],
            ['combined', 'nr tel. + adres']
        ].map(([value, labelText]) => {
            const label = makeElement('label', 'vm-garso-extended-mode-option');
            const input = makeElement('input');
            input.type = 'radio';
            input.name = `vm-garso-extended-search-mode-${adId || 'ad'}`;
            input.value = value;
            input.checked = value === garsoExtendedSearchMode;
            const text = makeElement('span', '', labelText);
            label.append(input, text);
            garsoExtendedModeGroup.appendChild(label);
            return { input, label };
        });
        const garsoExtendedButton = makeButton('vm-garso-extended-action');
        garsoExtendedButton.disabled = true;
        const garsoExtendedOpenButton = makeButton('vm-garso-extended-action');
        garsoExtendedOpenButton.title = 'Brak zapamiętanych wyników Garso';
        garsoExtendedOpenButton.setAttribute(
            'aria-label',
            'Otwórz zapamiętane wyniki Garso'
        );
        garsoExtendedBody.append(
            garsoExtendedModeGroup,
            garsoExtendedButton,
            garsoExtendedOpenButton
        );
        garsoExtendedPanel.append(
            garsoExtendedHead,
            garsoExtendedRecommendation,
            garsoExtendedBody
        );
        footer.appendChild(garsoExtendedPanel);
        panel.appendChild(footer);
        const garsoExtendedCollapsible = makeEscortSummarySectionCollapsible(
            garsoExtendedPanel,
            garsoExtendedHead,
            garsoExtendedBody,
            'garso-extended',
            false,
            false
        );
        garsoExtendedRecommendation.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            garsoExtendedCollapsible?.setExpanded(true);
        });

        let escortiRefreshRun = 0;
        let escortPhoneRefreshRun = 0;
        let escortiWarningSummary = null;
        let escortPhoneWarningSummary = null;
        let escortiAllAdUrls = [];
        let escortiPhoneAdUrls = [];
        let escortiAddressAdUrls = [];
        let escortiAdUrlSourcesReady = false;
        let garsoExtendedResults = [];
        let garsoExtendedCheckRunning = false;
        let garsoExtendedCancelToken = null;
        let garsoExtendedOpenRunning = false;
        let garsoExtendedCheckCompleted = false;

        const getGarsoExtendedModeUrls = mode => {
            const sourceUrls = mode === 'phone'
                ? escortiPhoneAdUrls
                : (mode === 'address'
                    ? escortiAddressAdUrls
                    : [...escortiPhoneAdUrls, ...escortiAddressAdUrls]);
            return [...new Set(
                sourceUrls.map(url => {
                    const normalized = normalizeEscortiAdUrl(url);
                    if (normalized) return normalized;
                    const linkedAdId = parseAdIdFromUrl(url);
                    return linkedAdId
                        ? `https://pl.escort.club/anons/${linkedAdId}.html`
                        : null;
                }).filter(Boolean)
            )];
        };
        const getGarsoExtendedModeCount = mode =>
            getGarsoExtendedModeUrls(mode).length;
        const getFirstAvailableGarsoExtendedMode = () =>
            ['address', 'phone', 'combined'].find(
                mode => getGarsoExtendedModeCount(mode) > 1
            ) || null;

        const syncGarsoExtendedAdUrls = () => {
            if (!escortiAdUrlSourcesReady) return;
            if (getGarsoExtendedModeCount(garsoExtendedSearchMode) <= 1) {
                const fallbackMode = getFirstAvailableGarsoExtendedMode();
                if (fallbackMode) {
                    garsoExtendedSearchMode = fallbackMode;
                    for (const option of garsoExtendedModeOptions) {
                        option.input.checked =
                            option.input.value === garsoExtendedSearchMode;
                    }
                    GM_setValue(
                        GARSO_EXTENDED_SEARCH_MODE_KEY,
                        garsoExtendedSearchMode
                    );
                    const cached = getGarsoExtendedCache(adId);
                    const matchesMode =
                        cached?.searchMode === garsoExtendedSearchMode;
                    hasCachedGarsoExtendedInfo = !!(
                        matchesMode && cached?.checkedAt
                    );
                    garsoExtendedCachedResults = matchesMode
                        ? (cached?.results || [])
                        : [];
                    garsoExtendedResults = [];
                    garsoExtendedCheckCompleted = false;
                }
            }
            escortiAllAdUrls = getGarsoExtendedModeUrls(
                garsoExtendedSearchMode
            );
        };

        const cachedGarsoExtended = getGarsoExtendedCache(adId);
        const cachedGarsoExtendedMatchesMode =
            cachedGarsoExtended?.searchMode === garsoExtendedSearchMode;
        let hasCachedGarsoExtendedInfo = !!(
            cachedGarsoExtendedMatchesMode && cachedGarsoExtended?.checkedAt
        );
        let garsoExtendedCachedResults = cachedGarsoExtendedMatchesMode
            ? (cachedGarsoExtended?.results || [])
            : [];
        if (cachedGarsoExtendedMatchesMode && cachedGarsoExtended?.fresh) {
            escortiAllAdUrls = cachedGarsoExtended.adUrls;
        }

        const updateGarsoExtendedButton = (statusText = '') => {
            const count = escortiAllAdUrls.length;
            const selectedModeHasTooFewLinks = count <= 1;
            const noExtendedSearchAvailable = escortiAdUrlSourcesReady &&
                ['phone', 'address', 'combined'].every(
                    mode => getGarsoExtendedModeCount(mode) <= 1
                );
            const modeLabel = getGarsoExtendedSearchModeLabel(
                garsoExtendedSearchMode
            );
            const cachedPageCount = getUniqueGarsoResultPages(
                garsoExtendedCachedResults
            ).length;
            const extendedInfoAvailable =
                hasCachedGarsoExtendedInfo ||
                garsoExtendedCheckCompleted ||
                garsoExtendedCachedResults.length > 0;
            garsoExtendedStatusBadge.hidden =
                selectedModeHasTooFewLinks || !extendedInfoAvailable;
            garsoExtendedStatusBadge.title = cachedPageCount
                ? `W cache są wyniki dla linków znalezionych na Escorti.pl po „${modeLabel}”: ${formatGarsoPageCount(cachedPageCount)}`
                : `Rozszerzone szukanie wykonano dla linków znalezionych na Escorti.pl po „${modeLabel}”`;
            // Samą sekcję można zawsze rozwinąć, żeby zobaczyć liczbę linków
            // i wybrać sposób wyszukiwania. Osobno blokowane są tylko warianty
            // oraz przycisk akcji, dla których Escorti.pl zwróciło za mało linków.
            garsoExtendedPanel.dataset.vmExtendedDisabled = '0';
            garsoExtendedPanel.setAttribute('aria-disabled', 'false');
            garsoExtendedPanel.style.opacity = '1';
            garsoExtendedHead.style.color = getEscortPagePinkColor();
            garsoExtendedHead.style.cursor = 'pointer';
            garsoExtendedHead.setAttribute('aria-disabled', 'false');
            garsoExtendedHead.title =
                garsoExtendedHead.getAttribute('aria-expanded') === 'true'
                    ? 'Kliknij, aby zwinąć sekcję'
                    : 'Kliknij, aby rozwinąć sekcję';
            const lastTwoDigits = count % 100;
            const lastDigit = count % 10;
            const foundWord = count === 1 || (
                lastDigit >= 2 && lastDigit <= 4 &&
                !(lastTwoDigits >= 12 && lastTwoDigits <= 14)
            ) ? 'znalezione' : 'znalezionych';
            const disabled = !escortiAdUrlSourcesReady ||
                noExtendedSearchAvailable ||
                garsoExtendedOpenRunning;
            for (const option of garsoExtendedModeOptions) {
                const modeCount = escortiAdUrlSourcesReady
                    ? getGarsoExtendedModeCount(option.input.value)
                    : null;
                const modeHasTooFewLinks =
                    escortiAdUrlSourcesReady && modeCount <= 1;
                const modeDisabled = modeHasTooFewLinks ||
                    garsoExtendedCheckRunning ||
                    garsoExtendedOpenRunning;
                option.input.disabled = modeDisabled;
                option.label.style.opacity = modeDisabled ? '.55' : '1';
                option.label.style.cursor = modeDisabled ? 'default' : 'pointer';
                option.label.title = modeHasTooFewLinks
                    ? `Ten wariant jest niedostępny - Escorti.pl zwróciło tylko ${modeCount} ${polishAdWord(modeCount)}.`
                    : '';
            }
            garsoExtendedButton.disabled = disabled;
            garsoExtendedButton.style.opacity = disabled ? '.55' : '1';
            garsoExtendedButton.style.cursor = disabled ? 'default' : 'pointer';
            garsoExtendedButton.title = garsoExtendedCheckRunning
                ? 'Przerwij rozszerzone szukanie tematów Garso'
                : '';
            const openDisabled = (
                cachedPageCount === 0 ||
                garsoExtendedCheckRunning ||
                garsoExtendedOpenRunning
            );
            garsoExtendedOpenButton.disabled = openDisabled;
            garsoExtendedOpenButton.style.opacity = openDisabled ? '.45' : '1';
            garsoExtendedOpenButton.style.cursor = openDisabled
                ? 'default'
                : 'pointer';
            garsoExtendedOpenButton.title = cachedPageCount
                    ? `Otwórz zapamiętane wyniki (${formatGarsoPageCount(cachedPageCount)})`
                    : 'Brak zapamiętanych wyników Garso';
            setTwoLineButton(
                garsoExtendedOpenButton,
                'Otwieranie',
                garsoExtendedOpenRunning
                    ? 'otwieranie…'
                    : formatGarsoPageCount(cachedPageCount)
            );
            const countText = `${count} ${polishAdWord(count)} ${foundWord} na escorti.pl`;
            const detailsText = selectedModeHasTooFewLinks
                ? (escortiAdUrlSourcesReady
                    ? 'brak dodatkowych anonsów do sprawdzenia dla wybranego sposobu'
                    : 'czekam na listę Escorti…')
                : statusText || (
                garsoExtendedCheckCompleted
                    ? `Escorti.pl: ${modeLabel} • sprawdzono • znaleziono ${garsoExtendedResults.length} • kliknij, aby przeliczyć ponownie`
                    : (count
                        ? `Escorti.pl: ${modeLabel} • kliknij, aby rozpocząć • ok. ${formatGarsoExtendedEta(count, GARSO_ANTIFLOOD_WAIT_MS)}`
                        : 'czekam na listę Escorti…')
            );
            setGarsoExtendedCheckButton(
                garsoExtendedButton,
                `${countText} • ${detailsText}`,
                garsoExtendedCheckRunning ? 'Przerwij' : 'Sprawdzanie'
            );
            updateGarsoExtendedRecommendation();
        };

        updateGarsoExtendedRecommendation = () => {
            const phoneTopics = researchFacts.garsoPhoneCount;
            const addressTopics = researchFacts.garsoAddressCount;
            const escortiAdCount = Math.max(
                Number(researchFacts.escortiPhoneResult?.adLinks) || 0,
                Number(researchFacts.escortiAddressResult?.adLinks) || 0
            );
            const shouldRecommend =
                phoneTopics === 0 &&
                addressTopics === 0 &&
                escortiAdCount > 1 &&
                garsoExtendedStatusBadge.hidden;
            garsoExtendedRecommendation.style.display = shouldRecommend
                ? 'flex'
                : 'none';
            garsoExtendedRecommendation.hidden = !shouldRecommend;
            garsoExtendedRecommendation.setAttribute(
                'aria-hidden',
                String(!shouldRecommend)
            );
            if (shouldRecommend) {
                garsoExtendedRecommendationText.textContent =
                    `Zwykłe wyszukiwanie Garso nie znalazło tematów, ` +
                    `ale Escorti.pl znalazło ${escortiAdCount} ${polishAdWord(escortiAdCount)}. ` +
                    'Kliknij, aby sprawdzić te linki na Garso.';
            }
        };
        updateGarsoExtendedRecommendation();

        const loadGarsoExtendedCacheForMode = () => {
            const cached = getGarsoExtendedCache(adId);
            const matchesMode = cached?.searchMode === garsoExtendedSearchMode;
            hasCachedGarsoExtendedInfo = !!(
                matchesMode && cached?.checkedAt
            );
            garsoExtendedCachedResults = matchesMode
                ? (cached?.results || [])
                : [];
            if (escortiAdUrlSourcesReady) {
                syncGarsoExtendedAdUrls();
            } else if (matchesMode && cached?.fresh) {
                escortiAllAdUrls = cached.adUrls;
            } else {
                escortiAllAdUrls = [];
            }
            garsoExtendedResults = [];
            garsoExtendedCheckCompleted = false;
        };
        for (const option of garsoExtendedModeOptions) {
            option.input.addEventListener('change', () => {
                if (!option.input.checked || garsoExtendedCheckRunning) return;
                garsoExtendedSearchMode = normalizeGarsoExtendedSearchMode(
                    option.input.value
                );
                GM_setValue(
                    GARSO_EXTENDED_SEARCH_MODE_KEY,
                    garsoExtendedSearchMode
                );
                loadGarsoExtendedCacheForMode();
                updateGarsoExtendedButton();
            });
        }

        updateGarsoExtendedButton();

        const runGarsoExtendedAction = async () => {
            if (garsoExtendedCheckRunning) {
                garsoExtendedCancelToken?.cancel(
                    'Przerwano rozszerzone szukanie tematów Garso'
                );
                updateGarsoExtendedButton('przerywanie…');
                return;
            }
            if (
                escortiAllAdUrls.length <= 1 ||
                garsoExtendedOpenRunning
            ) return;

            garsoExtendedCheckRunning = true;
            garsoExtendedCancelToken = createOperationCancelToken(
                'Garso - rozszerzone szukanie tematów'
            );
            refreshButton.disabled = true;
            refreshButton.style.opacity = '.55';
            garsoExtendedResults = [];
            garsoExtendedCheckCompleted = false;

            let failed = 0;
            const total = escortiAllAdUrls.length;
            let averageCheckMs = 1500;
            let measuredChecks = 0;
            const selectedMode = garsoExtendedSearchMode;
            let finalStatus = '';
            try {
                for (let index = 0; index < total; index++) {
                    garsoExtendedCancelToken.throwIfCancelled();
                    const checkStartedAt = Date.now();
                    const currentUrl = escortiAllAdUrls[index];
                    const currentId = parseAdIdFromUrl(currentUrl);
                    const searchTerm = currentId
                        ? buildGarsoAdLinkSearchTerm(currentId)
                        : currentUrl;
                    updateGarsoExtendedButton(
                        `Sprawdzanie linków ${index + 1}/${total} • znaleziono ${garsoExtendedResults.length} • pozostało ok. ${formatGarsoExtendedEta(total - index, averageCheckMs)}`
                    );
                    const result = await checkGarsoTerm(
                        searchTerm,
                        true,
                        garsoExtendedCancelToken
                    );

                    if (result?.status === 'ok' && Number(result.count) > 0) {
                        garsoExtendedResults.push({
                            ...result,
                            adUrl: currentUrl,
                            searchTerm
                        });
                    } else if (result?.status !== 'ok') {
                        failed++;
                    }

                    const elapsed = Math.max(250, Date.now() - checkStartedAt);
                    averageCheckMs = (
                        averageCheckMs * measuredChecks + elapsed
                    ) / (measuredChecks + 1);
                    measuredChecks++;

                    if (index < total - 1) {
                        updateGarsoExtendedButton(
                            `Sprawdzanie ${index + 1}/${total} • znaleziono ${garsoExtendedResults.length} • pozostało ok. ${formatGarsoExtendedEta(total - index - 1, averageCheckMs)}`
                        );
                    }
                }

                updateGarsoExtendedButton(
                    `Sprawdzono ${total}/${total} • znaleziono ${garsoExtendedResults.length}`
                );
                garsoExtendedCheckCompleted = true;
                if (!failed) {
                    setGarsoExtendedCache(
                        adId,
                        escortiAllAdUrls,
                        garsoExtendedResults,
                        selectedMode
                    );
                    garsoExtendedCachedResults = garsoExtendedResults.map(result => ({
                        ...result,
                        signature: getGarsoExtendedResultSignature(result)
                    }));
                } else {
                    recordIncompleteAnalysis(
                        'Garso - rozszerzone szukanie tematów',
                        `Nie udało się sprawdzić ${failed} z ${total} linków.`
                    );
                }
                finalStatus =
                    `Zakończono • wyniki dla ${garsoExtendedResults.length} • otwórz zapisane strony przyciskiem „Otwórz”` +
                    (failed ? ` • błędy ${failed}` : '');
            } catch (error) {
                if (isOperationCancelledError(error)) {
                    finalStatus =
                        `Przerwano • sprawdzono część linków • znaleziono ${garsoExtendedResults.length}`;
                    recordDiagnosticCancellation(
                        'Garso - rozszerzone szukanie tematów',
                        `Znaleziono ${garsoExtendedResults.length} wyników przed przerwaniem.`
                    );
                } else {
                    log('Błąd rozszerzonego sprawdzania Garsoniery', error);
                    recordIncompleteAnalysis(
                        'Garso - rozszerzone szukanie tematów',
                        'Operacja zakończyła się błędem przed sprawdzeniem wszystkich linków.'
                    );
                    finalStatus = 'Nie udało się zakończyć sprawdzania';
                }
            } finally {
                garsoExtendedCheckRunning = false;
                garsoExtendedCancelToken = null;
                refreshButton.disabled = false;
                refreshButton.style.opacity = '1';
                updateGarsoExtendedButton(finalStatus);
            }
        };

        garsoExtendedButton.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            runGarsoExtendedAction();
        });
        garsoExtendedOpenButton.addEventListener('click', async event => {
            event.preventDefault();
            event.stopPropagation();
            if (
                escortiAllAdUrls.length <= 1 ||
                !garsoExtendedCachedResults.length ||
                garsoExtendedCheckRunning ||
                garsoExtendedOpenRunning
            ) return;

            garsoExtendedOpenRunning = true;
            updateGarsoExtendedButton('otwieram zapamiętane wyniki…');
            try {
                const opened = await openUniqueGarsoResultPages(
                    garsoExtendedCachedResults
                );
                updateGarsoExtendedButton(`Wyniki z cache • otwarto ${opened}`);
            } catch (error) {
                log('Nie udało się otworzyć wyników rozszerzonego Garso z cache', error);
                updateGarsoExtendedButton('Nie udało się otworzyć wyników z cache');
            } finally {
                garsoExtendedOpenRunning = false;
                updateGarsoExtendedButton();
            }
        });

        const renderWarnings = () => {
            const sections = [
                {
                    title: 'Escorti.pl',
                    subtitle: 'anonse przypisane do znalezionych profili',
                    warnings: getEscortActiveAdsConsistencyWarnings(escortiWarningSummary)
                },
                {
                    title: 'Escort.club - ten sam nr tel.',
                    subtitle: 'anonse znalezione po numerze telefonu',
                    warnings: getEscortActiveAdsConsistencyWarnings(escortPhoneWarningSummary)
                }
            ].filter(section => section.warnings.length);

            const hasWarnings = sections.length > 0;
            const warningCount = sections.reduce(
                (sum, section) => sum + section.warnings.length,
                0
            );
            warningBox.classList.toggle(
                'vm-escort-summary-status-alert',
                hasWarnings
            );
            warningBox.classList.toggle(
                'vm-escort-summary-status-ok',
                !hasWarnings
            );
            warningStatusBadge.textContent = hasWarnings
                ? '⚠︎'
                : '✓ brak';
            warningStatusBadge.title = hasWarnings
                ? `Wykryto ${warningCount} ${warningCount === 1 ? 'niezgodność' : 'niezgodności'}`
                : 'Nie wykryto niezgodności';
            warningContent.replaceChildren();

            if (!sections.length) {
                const emptyLine = makeElement('div', 'vm-inconsistencies-empty', '✓ Dane są zgodne w porównanych anonsach.');
                warningContent.appendChild(emptyLine);
                return;
            }

            const getWarningIcon = label => {
                if (/miast/i.test(label)) return '📍';
                if (/wiek/i.test(label)) return 'W';
                if (/parametr/i.test(label)) return '≠';
                if (/cen/i.test(label)) return 'zł';
                if (/godzin/i.test(label)) return '◷';
                if (/tag/i.test(label)) return '#';
                return '!';
            };
            const getDifferenceWord = count => count === 1
                ? 'różnica'
                : (count >= 2 && count <= 4 ? 'różnice' : 'różnic');

            sections.forEach(section => {
                const sourceCard = makeElement('section', 'vm-inconsistencies-source-card');

                const sourceHead = makeElement('div', 'vm-inconsistencies-source-head');
                const sourceMarker = makeElement('span', 'vm-inconsistencies-source-marker', '!');
                const sourceCopy = makeElement('span');
                sourceCopy.style.minWidth = '0';
                const sourceTitle = makeElement('span', 'vm-inconsistencies-source-title', section.title);
                const sourceSubtitle = makeElement('span', 'vm-inconsistencies-source-subtitle', section.subtitle);
                sourceCopy.append(sourceTitle, sourceSubtitle);
                const sourceCount = makeElement('span', 'vm-inconsistencies-source-count', `${section.warnings.length} ${getDifferenceWord(section.warnings.length)}`);
                sourceHead.append(
                    sourceMarker,
                    sourceCopy,
                    sourceCount
                );
                sourceCard.appendChild(sourceHead);

                const warningList = makeElement('div', 'vm-inconsistencies-warning-list');
                for (const warning of section.warnings) {
                    const line = makeElement('div', 'vm-inconsistencies-warning-row');
                    const separator = warning.indexOf(':');
                    const label = separator >= 0
                        ? warning.slice(0, separator).trim()
                        : warning.trim();
                    const detail = separator >= 0
                        ? warning.slice(separator + 1).trim()
                        : '';
                    const icon = makeElement('span', 'vm-inconsistencies-warning-icon', getWarningIcon(label));
                    const copy = makeElement('span');
                    const labelLine = makeElement('span', 'vm-inconsistencies-warning-label', label);
                    copy.appendChild(labelLine);
                    if (detail) {
                        const detailLine = makeElement('span', 'vm-inconsistencies-warning-detail', detail);
                        copy.appendChild(detailLine);
                    }
                    line.append(icon, copy);
                    warningList.appendChild(line);
                }
                sourceCard.appendChild(warningList);
                warningContent.appendChild(sourceCard);
            });
        };
        renderWarnings();

        const refreshEscorti = async (forceRefresh = false) => {
            const run = ++escortiRefreshRun;
            researchFacts.escortiLoading = true;
            renderResearchFacts();
            updateGarsoExtendedButton(
                garsoExtendedCheckCompleted ? '' : 'pobieram listę Escorti…'
            );
            const cachedActivity = getLatestCachedEscortiActivitySummary(
                adId,
                ['phone-escorti', 'address-escorti']
            );
            escortiPhoneRow.style.display = 'none';
            escortiAddressRow.style.display = 'flex';
            setButtonColor(escortiAddressRow, 'checking');
            setTwoLineButton(
                escortiAddressRow,
                'Escorti.pl',
                cachedActivity
                    ? `${cachedActivity.activeCount} aktywnych • odświeżanie…`
                    : 'szukam powiązań…'
            );

            try {
                const [phoneData, addressData] = await Promise.all([
                    getEscortiDetailForSingleAd(
                        adId,
                        phoneSearch,
                        'phone-escorti',
                        forceRefresh
                    ),
                    getEscortiDetailForSingleAd(
                        adId,
                        adUrl,
                        'address-escorti',
                        forceRefresh
                    )
                ]);
                if (run !== escortiRefreshRun) return;

                const phoneResult = phoneData.result;
                const addressResult = addressData.result;
                const sameResult = areSameEscortiResults(phoneResult, addressResult);
                const phoneAdUrls = phoneResult?.status === 'ok'
                    ? [...new Set(phoneResult.adUrls || [])]
                    : [];
                const addressAdUrls = addressResult?.status === 'ok'
                    ? [...new Set(addressResult.adUrls || [])]
                    : [];
                escortiPhoneAdUrls = phoneAdUrls;
                escortiAddressAdUrls = addressAdUrls;
                escortiAdUrlSourcesReady = true;
                syncGarsoExtendedAdUrls();
                const adUrls = [...new Set(
                    [...phoneAdUrls, ...addressAdUrls]
                )];
                updateGarsoExtendedButton();

                researchFacts.escortiPhoneResult = phoneResult;
                researchFacts.escortiAddressResult = addressResult;
                researchFacts.sameEscortiResult = sameResult;
                researchFacts.escortiPhoneSummary =
                    getCachedEscortiActivitySummary(
                        adId,
                        'phone-escorti',
                        phoneAdUrls
                    );
                researchFacts.escortiAddressSummary =
                    getCachedEscortiActivitySummary(
                        adId,
                        'address-escorti',
                        addressAdUrls
                    );
                renderResearchFacts();

                const setRowLayout = () => {
                    escortiAddressRow.style.display = 'flex';
                    if (sameResult) {
                        escortiPhoneRow.style.display = 'none';
                        setTwoLineButton(escortiAddressRow, 'Escorti.pl', 'sprawdzam aktywne…');
                        return;
                    }
                    escortiPhoneRow.style.display = 'flex';
                    setButtonColor(escortiAddressRow, 'checking');
                    setButtonColor(escortiPhoneRow, 'checking');
                    setTwoLineButton(
                        escortiAddressRow,
                        'Escorti.pl - adres anonsu',
                        'sprawdzam aktywne…'
                    );
                    setTwoLineButton(
                        escortiPhoneRow,
                        'Escorti.pl - nr tel.',
                        'sprawdzam aktywne…'
                    );
                };
                setRowLayout();

                if (!adUrls.length) {
                    const emptySummary = summarizeActiveAdResults([]);
                    researchFacts.escortiPhoneSummary = emptySummary;
                    researchFacts.escortiAddressSummary = emptySummary;
                    setButtonColor(escortiAddressRow, 'empty');
                    setTwoLineButton(
                        escortiAddressRow,
                        sameResult ? 'Escorti.pl' : 'Escorti.pl - adres anonsu',
                        formatEscortiAdsAndActivity(addressResult, emptySummary)
                    );
                    if (!sameResult) {
                        setButtonColor(escortiPhoneRow, 'empty');
                        setTwoLineButton(
                            escortiPhoneRow,
                            'Escorti.pl - nr tel.',
                            formatEscortiAdsAndActivity(phoneResult, emptySummary)
                        );
                    }
                    escortiWarningSummary = null;
                    researchFacts.escortiWarnings = [];
                    renderWarnings();
                    renderResearchFacts();
                    return;
                }

                const summary = await scanActiveEscortAds(
                    adUrls,
                    progress => {
                        if (run !== escortiRefreshRun) return;
                        const progressText = `sprawdzanie ${progress.checked}/${progress.total}…`;
                        setTwoLineButton(
                            escortiAddressRow,
                            sameResult ? 'Escorti.pl' : 'Escorti.pl - adres anonsu',
                            progressText
                        );
                        if (!sameResult) {
                            setTwoLineButton(
                                escortiPhoneRow,
                                'Escorti.pl - nr tel.',
                                progressText
                            );
                        }
                    },
                    forceRefresh
                );
                if (run !== escortiRefreshRun) return;

                const phoneSummary = summarizeActiveAdsSubset(summary, phoneAdUrls);
                const addressSummary = summarizeActiveAdsSubset(summary, addressAdUrls);
                saveEscortiActivitySummary(
                    adId,
                    ['phone-escorti'],
                    phoneAdUrls,
                    phoneSummary
                );
                saveEscortiActivitySummary(
                    adId,
                    ['address-escorti'],
                    addressAdUrls,
                    addressSummary
                );

                researchFacts.escortiPhoneSummary = phoneSummary;
                researchFacts.escortiAddressSummary = addressSummary;

                if (sameResult) {
                    setButtonColor(
                        escortiAddressRow,
                        addressSummary.activeCount > 0 ? 'found' : 'empty'
                    );
                    setTwoLineButton(
                        escortiAddressRow,
                        'Escorti.pl',
                        formatEscortiAdsAndActivity(addressResult, addressSummary)
                    );
                    escortiAddressRow.title = buildActiveAdsTitle(
                        'Wyszukiwanie Escorti.pl po adresie anonsu i nr tel. zwróciło ten sam wynik.',
                        addressResult,
                        addressSummary
                    );
                } else {
                    setButtonColor(
                        escortiAddressRow,
                        addressSummary.activeCount > 0 ? 'found' : 'empty'
                    );
                    setTwoLineButton(
                        escortiAddressRow,
                        'Escorti.pl - adres anonsu',
                        formatEscortiAdsAndActivity(addressResult, addressSummary)
                    );
                    escortiAddressRow.title = buildActiveAdsTitle(
                        'Escorti.pl po adresie anonsu.',
                        addressResult,
                        addressSummary
                    );

                    setButtonColor(
                        escortiPhoneRow,
                        phoneSummary.activeCount > 0 ? 'found' : 'empty'
                    );
                    setTwoLineButton(
                        escortiPhoneRow,
                        'Escorti.pl - nr tel.',
                        formatEscortiAdsAndActivity(phoneResult, phoneSummary)
                    );
                    escortiPhoneRow.title = buildActiveAdsTitle(
                        'Escorti.pl po nr tel.',
                        phoneResult,
                        phoneSummary
                    );
                }

                escortiWarningSummary = summary;
                researchFacts.escortiWarnings =
                    getEscortActiveAdsConsistencyWarnings(summary);
                renderWarnings();
                renderResearchFacts();
            } catch (error) {
                if (run !== escortiRefreshRun) return;
                updateGarsoExtendedButton('nie udało się pobrać listy Escorti');
                log('Błąd odświeżania panelu powiązanych wyników', error);
                if (cachedActivity) {
                    setButtonColor(
                        escortiAddressRow,
                        cachedActivity.activeCount > 0 ? 'found' : 'empty'
                    );
                    setTwoLineButton(
                        escortiAddressRow,
                        'Escorti.pl',
                        `${cachedActivity.activeCount} aktywnych`
                    );
                } else {
                    setButtonColor(escortiAddressRow, 'error');
                    setTwoLineButton(escortiAddressRow, 'Escorti.pl', 'błąd');
                }
                renderResearchFacts();
            } finally {
                if (run === escortiRefreshRun) {
                    researchFacts.escortiLoading = false;
                    renderResearchFacts();
                }
            }
        };

        const refreshEscortPhone = async (forceRefresh = false) => {
            const run = ++escortPhoneRefreshRun;
            researchFacts.escortPhoneLoading = true;
            renderResearchFacts();
            setButtonColor(escortPhoneRow, 'checking');
            setTwoLineButton(escortPhoneRow, 'Escort - nr tel.', 'wyszukiwanie…');

            try {
                const search = await fetchEscortClubPhoneSearchAds(phone);
                if (run !== escortPhoneRefreshRun) return;

                escortPhoneRow.dataset.searchUrl = search.searchUrl;
                const total = search.adUrls.length;
                let completed = 0;
                const dataResults = await Promise.all(search.adUrls.map(async url => {
                    const result = await getEscortAdData(
                        parseAdIdFromUrl(url),
                        url,
                        forceRefresh
                    );
                    completed++;
                    if (run === escortPhoneRefreshRun && completed < total) {
                        setTwoLineButton(
                            escortPhoneRow,
                            'Escort - nr tel.',
                            `${total} ${polishAdWord(total)} • dane ${completed}/${total}…`
                        );
                    }
                    return { url, result };
                }));
                if (run !== escortPhoneRefreshRun) return;

                const results = dataResults
                    .filter(item => item.result?.status === 'ok')
                    .map(item => ({
                        url: item.url,
                        active: true,
                        city: item.result.location?.city || null,
                        adData: item.result
                }));
                escortPhoneWarningSummary = summarizeActiveAdResults(results);
                researchFacts.escortPhoneSummary = escortPhoneWarningSummary;
                renderWarnings();
                renderResearchFacts();
                setButtonColor(escortPhoneRow, total > 0 ? 'found' : 'empty');
                setTwoLineButton(
                    escortPhoneRow,
                    'Escort - nr tel.',
                    `${total} ${polishAdWord(total)}`
                );
                escortPhoneRow.title =
                    `Aktualnie znalezione na Escort.club anonse z tym samym numerem telefonu: ${total}.`;
            } catch (error) {
                if (run !== escortPhoneRefreshRun) return;
                log('Błąd wyszukiwania numeru telefonu na Escort.club', error);
                setButtonColor(escortPhoneRow, 'error');
                setTwoLineButton(escortPhoneRow, 'Escort - nr tel.', 'błąd');
            } finally {
                if (run === escortPhoneRefreshRun) {
                    researchFacts.escortPhoneLoading = false;
                    renderResearchFacts();
                }
            }
        };

        const refreshPhotoDateRange = async () => {
            researchFacts.photoDatesLoading = true;
            renderResearchFacts();
            try {
                researchFacts.photoRange = await getEscortGalleryPhotoDateRange(document);
            } catch (error) {
                researchFacts.photoRange = null;
                log('Nie udało się przygotować zakresu dat zdjęć', error);
            } finally {
                researchFacts.photoDatesLoading = false;
                renderResearchFacts();
            }
        };

        const refreshAll = async () => {
            if (refreshButton.disabled) return;

            refreshButton.disabled = true;
            refreshButton.textContent = '⟳';
            refreshButton.title = 'Pobieranie danych…';
            refreshButton.setAttribute('aria-label', refreshButton.title);
            refreshButton.style.opacity = '.72';
            try {
                await Promise.allSettled([
                    updateGarsoStatus(
                        garsoPhoneRow,
                        garsoPhoneTerm,
                        'phone',
                        true,
                        false
                    ),
                    updateGarsoStatus(
                        garsoAddressRow,
                        garsoAddressTerm,
                        'address',
                        true,
                        false
                    ),
                    (async () => {
                        await refreshEscorti(true);
                        await refreshEscortPhone(true);
                    })(),
                    refreshPhotoDateRange()
                ]);
            } finally {
                refreshButton.disabled = false;
                refreshButton.textContent = '↻';
                refreshButton.title = 'Pobierz ponownie dane z sekcji „Linki” (Garsoniera: tylko sprawdzenie, czy są tematy)';
                refreshButton.setAttribute('aria-label', refreshButton.title);
                refreshButton.style.opacity = '1';
            }
        };

        const refreshGarsoContent = async (forceRefresh = true) => {
            if (garsoDownloadButton.disabled) return;

            const operationCancelToken = createOperationCancelToken(
                'Pełna analiza Garso'
            );
            garsoCombinedSummaryState._vmGarsoSummaryCancelToken =
                operationCancelToken;

            if (forceRefresh) {
                garsoSummaryCard.collapsible?.setExpanded(true);
            }
            garsoDownloadButton.disabled = true;
            garsoDownloadButton.textContent = '⟳';
            garsoDownloadButton.title = 'Pobieranie i analizowanie tematów Garsoniery…';
            garsoDownloadButton.setAttribute('aria-label', garsoDownloadButton.title);
            garsoDownloadButton.style.opacity = '.72';
            const previousResult = garsoCombinedSummaryState._vmGarsoSummaryResult || null;
            const previousCheckedAt = Number(
                garsoCombinedSummaryState.dataset.garsoSummaryCheckedAt
            ) || null;
            if (previousResult) {
                setGarsoSummaryState(
                    garsoCombinedSummaryState,
                    'refreshing',
                    summaryToHtml(previousResult),
                    {
                        checkedAt: previousCheckedAt,
                        cacheState: 'refreshing'
                    }
                );
            } else {
                setGarsoSummaryState(
                    garsoCombinedSummaryState,
                    'loading',
                    '<b>Garso</b><br>Łączenie list tematów…'
                );
            }

            try {
                const [phoneResult, addressResult] = await Promise.all([
                    updateGarsoStatus(
                        garsoPhoneRow,
                        garsoPhoneTerm,
                        'phone',
                        true,
                        true,
                        false,
                        operationCancelToken
                    ),
                    updateGarsoStatus(
                        garsoAddressRow,
                        garsoAddressTerm,
                        'address',
                        true,
                        true,
                        false,
                        operationCancelToken
                    )
                ]);

                if (
                    phoneResult?.status !== 'ok' ||
                    addressResult?.status !== 'ok'
                ) {
                    throw new Error('Nie udało się pobrać obu list tematów Garso');
                }

                const combinedTopics = mergeUniqueGarsoTopics(
                    phoneResult.topics || [],
                    addressResult.topics || []
                );
                await prepareGarsoSummary(
                    garsoCombinedSummaryState,
                    garsoCombinedSummaryTerm,
                    combinedTopics,
                    forceRefresh,
                    operationCancelToken
                );
                if (forceRefresh) {
                    garsoSummaryCard.collapsible?.setExpanded(true);
                }
            } catch (error) {
                if (isOperationCancelledError(error)) {
                    recordDiagnosticCancellation(
                        'Garso - recenzje',
                        'Przerwano pobieranie list tematów przed pełną analizą.'
                    );
                    garsoCombinedSummaryState._vmGarsoSummaryResult =
                        previousResult;
                    setGarsoSummaryState(
                        garsoCombinedSummaryState,
                        'cancelled',
                        previousResult ? summaryToHtml(previousResult) : '',
                        {
                            checkedAt: previousCheckedAt,
                            cacheState: previousResult ? 'stale' : null
                        }
                    );
                } else {
                    log('Błąd wspólnej analizy Garso', error);
                    if (previousResult) {
                        applySummaryToButton(
                            garsoCombinedSummaryState,
                            previousResult,
                            {
                                checkedAt: previousCheckedAt,
                                cacheState: 'stale'
                            }
                        );
                    } else {
                        setGarsoSummaryState(
                            garsoCombinedSummaryState,
                            'error',
                            ''
                        );
                    }
                }
            } finally {
                if (
                    garsoCombinedSummaryState._vmGarsoSummaryCancelToken ===
                    operationCancelToken
                ) {
                    garsoCombinedSummaryState._vmGarsoSummaryCancelToken = null;
                }
                garsoDownloadButton.disabled = false;
                garsoDownloadButton.textContent = '↻';
                garsoDownloadButton.title =
                    'Odśwież i przeanalizuj treść tematów Garsoniery dla numeru telefonu i adresu anonsu.';
                garsoDownloadButton.setAttribute('aria-label', garsoDownloadButton.title);
                garsoDownloadButton.style.opacity = '1';
            }
        };

        const initializeGarsoChecks = () => {
            if (!SETTINGS.autoGarsoCheck) return;

            const analyzeContent = normalizeAutoGarsoAnalysisMode(
                SETTINGS.autoGarsoAnalysisMode
            ) === 'content';
            if (analyzeContent && !cachedCombinedGarsoSummary?.fresh) {
                refreshGarsoContent(false);
                return;
            }

            Promise.allSettled([
                updateGarsoStatus(
                    garsoPhoneRow,
                    garsoPhoneTerm,
                    'phone',
                    false,
                    false
                ),
                updateGarsoStatus(
                    garsoAddressRow,
                    garsoAddressTerm,
                    'address',
                    false,
                    false
                )
            ]).then(() => {
                if (garsoCombinedSummaryState.dataset.garsoSummaryState) return;
                if (
                    garsoPhoneRow.dataset.garsoStatus !== 'result' ||
                    garsoAddressRow.dataset.garsoStatus !== 'result'
                ) return;
                const checkedAt = Math.max(
                    Number(garsoPhoneRow.dataset.garsoCountCheckedAt) || 0,
                    Number(garsoAddressRow.dataset.garsoCountCheckedAt) || 0
                );
                setGarsoSummaryState(
                    garsoCombinedSummaryState,
                    'count-only',
                    '',
                    {
                        checkedAt: checkedAt || Date.now(),
                        cacheState: 'fresh'
                    }
                );
            });
        };

        refreshButton.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            refreshAll();
        });
        garsoDownloadButton.addEventListener('click', event => {
            event.preventDefault();
            event.stopPropagation();
            refreshGarsoContent(true);
        });
        garsoDownloadButton.addEventListener('keydown', event => {
            event.stopPropagation();
        });

        initializeGarsoChecks();
        refreshEscorti(false);
        refreshEscortPhone(false);
        refreshPhotoDateRange();
        return panel;
    }

    const buttonsAdded = { panel: false };

    function checkAndAddButtons() {
        if (!SETTINGS.showSummarySidePanel) {
            document.getElementById(ESCORT_SUMMARY_SIDE_PANEL_ID)?.remove();
            document.getElementById('vm-escort-summary-side-toggle')?.remove();
            document.body.classList.remove(ESCORT_SUMMARY_SIDE_PANEL_OPEN_CLASS);
        }
        if (!SETTINGS.showCompactSummaryAboveDescription) {
            document.getElementById(ESCORT_COMPACT_SUMMARY_ID)?.remove();
        }

        const phoneLink = getEscortPhoneLink();
        const container = getEscortButtonsContainer(phoneLink);
        if (!container) return;

        const adId = getAdIdFromUrl();
        const adUrl = adId ? `https://pl.escort.club/anons/${adId}.html` : null;

        const phone = getFormattedPhoneNumber(phoneLink);
        if (phone) {
            renderEscortPhoneCopyButton(phone);

            if (!buttonsAdded.panel && adUrl) {
                const researchPanel = createEscortResearchPanel(phone, adUrl);
                placeEscortResearchPanelAboveActionButtons(
                    researchPanel,
                    container
                );
                buttonsAdded.panel = true;
            }
        } else {
            if (
                phoneLink &&
                phoneLink.getAttribute('href') === '#' &&
                !phoneLink.dataset.scriptClicked
            ) {
                phoneLink.click();
                phoneLink.dataset.scriptClicked = 'true';
            }
        }
    }

    if (!GARSO_MENU_ONLY_MODE) {
        initEscortGalleryWatcher();
        checkAndAddButtons();
    }
})();