☰

The Ladies Helper

Beta: Übersichten, Galerie und Rufnummernsuche auf Ladies.de; Formularhilfe für Lusthaus und Snatchlist.

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==UserScript==
// @name         The Ladies Helper
// @namespace    http://tampermonkey.net/
// @version      4.4.43
// @description  Beta: Übersichten, Galerie und Rufnummernsuche auf Ladies.de; Formularhilfe für Lusthaus und Snatchlist.
// @description:en Beta: listing, gallery and phone research tools for Ladies.de, with Lusthaus and Snatchlist form autofill.
// @match        https://www.ladies.de/sex-anzeigen*
// @match        https://ladies.de/sex-anzeigen*
// @match        https://www.ladies.de/*/sex-anzeigen*
// @match        https://ladies.de/*/sex-anzeigen*
// @match        https://huren-test-forum.lusthaus.cc/misc.php*
// @match        https://snatchlist.com/forums/*/post-thread*
// @match        https://www.snatchlist.com/forums/*/post-thread*
// @license      MIT
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @connect      intim.to
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  const CONFIG = {
    boxId: "ladies-helper-box",
    styleId: "ladies-helper-style",
    noteId: "lh-autofill-note",
    profileBaseUrl: "https://www.ladies.de/Profil/",
    ladiesSearchBaseUrl: "https://www.ladies.de/sex-anzeigen?suchtext=",
    googleSearchBaseUrl: "https://www.google.com/search?q=",
    googleLensBaseUrl: "https://lens.google.com/uploadbyurl?url=",
    intimBaseUrl: "https://intim.to/de/tel/",
    ladiesForumBaseUrl: "https://www.ladies-forum.de/search?searchJSON=",
    snatchlistUrl:
      "https://snatchlist.com/forums/escorts-huren-hobbyhuren-erotikmassagen-bdsm.2/post-thread",
    lhReportBaseUrl:
      "https://huren-test-forum.lusthaus.cc/misc.php?do=form&fid=1",
    archiveBaseUrl: "https://archive.is/?run=1&url=",
    whatsappTextTemplate: "Hallo {name}, was sind deine Preise?",
  };

  const LIMITS = {
    noteAutoCloseMs: 8000,
  };

  // https://www.bundesnetzagentur.de/DE/Fachthemen/Telekommunikation/Nummerierung/MobileDienste/zugeteilteRNB/start.html
  const MOBILE_PREFIXES = [
    "015019",
    "015020",

    "01511",
    "01512",
    "01514",
    "01515",
    "01516",
    "01517",
    "015180",
    "015181",
    "015182",
    "015183",
    "015184",
    "015185",
    "015186",
    "015187",
    "015188",
    "015189",

    "01520",
    "01521",
    "01522",
    "01523",
    "01525",
    "01526",
    "01529",

    "015310",
    "015333",
    "015410",

    "015510",
    "015511",
    "015550",
    "015551",
    "015552",
    "015553",
    "015554",
    "015555",
    "015556",
    "015557",
    "015558",
    "015559",
    "015560",
    "015561",
    "015562",
    "015563",
    "015564",
    "015565",
    "015566",
    "015567",
    "015568",
    "015569",
    "015591",

    "015630",
    "015636",
    "015678",
    "015679",

    "015700",
    "015701",
    "015702",
    "015703",
    "015704",
    "015706",
    "01573",
    "01575",
    "01577",
    "01578",
    "01579",

    "015888",
    "01590",

    "0160",
    "0162",
    "0163",

    "0170",
    "0171",
    "0172",
    "0173",
    "0174",
    "0175",
    "0176",
    "0177",
    "0178",
    "0179",
  ].sort((a, b) => b.length - a.length);

  const COUNTRY_CODES = {
    1: "USA/Kanada",
    7: "Russland/Kasachstan",
    20: "Ägypten",
    27: "Südafrika",
    30: "Griechenland",
    31: "Niederlande",
    32: "Belgien",
    33: "Frankreich",
    34: "Spanien",
    36: "Ungarn",
    39: "Italien",
    40: "Rumänien",
    41: "Schweiz",
    43: "Österreich",
    44: "Großbritannien",
    45: "Dänemark",
    46: "Schweden",
    47: "Norwegen",
    48: "Polen",
    49: "Deutschland",
    51: "Peru",
    52: "Mexiko",
    53: "Kuba",
    54: "Argentinien",
    55: "Brasilien",
    56: "Chile",
    57: "Kolumbien",
    58: "Venezuela",
    60: "Malaysia",
    61: "Australien",
    62: "Indonesien",
    63: "Philippinen",
    64: "Neuseeland",
    65: "Singapur",
    66: "Thailand",
    81: "Japan",
    82: "Südkorea",
    84: "Vietnam",
    86: "China",
    90: "Türkei",
    91: "Indien",
    92: "Pakistan",
    93: "Afghanistan",
    94: "Sri Lanka",
    95: "Myanmar",
    98: "Iran",
    211: "Südsudan",
    212: "Marokko",
    213: "Algerien",
    216: "Tunesien",
    218: "Libyen",
    220: "Gambia",
    221: "Senegal",
    222: "Mauretanien",
    223: "Mali",
    224: "Guinea",
    225: "Elfenbeinküste",
    226: "Burkina Faso",
    227: "Niger",
    228: "Togo",
    229: "Benin",
    230: "Mauritius",
    231: "Liberia",
    232: "Sierra Leone",
    233: "Ghana",
    234: "Nigeria",
    235: "Tschad",
    236: "Zentralafrikanische Republik",
    237: "Kamerun",
    238: "Kap Verde",
    239: "São Tomé und Príncipe",
    240: "Äquatorialguinea",
    241: "Gabun",
    242: "Republik Kongo",
    243: "DR Kongo",
    244: "Angola",
    245: "Guinea-Bissau",
    248: "Seychellen",
    249: "Sudan",
    250: "Ruanda",
    251: "Äthiopien",
    252: "Somalia",
    253: "Dschibuti",
    254: "Kenia",
    255: "Tansania",
    256: "Uganda",
    257: "Burundi",
    258: "Mosambik",
    260: "Sambia",
    261: "Madagaskar",
    262: "Réunion/Mayotte",
    263: "Simbabwe",
    264: "Namibia",
    265: "Malawi",
    266: "Lesotho",
    267: "Botswana",
    268: "Eswatini",
    269: "Komoren",
    297: "Aruba",
    298: "Färöer",
    299: "Grönland",
    350: "Gibraltar",
    351: "Portugal",
    352: "Luxemburg",
    353: "Irland",
    354: "Island",
    355: "Albanien",
    356: "Malta",
    357: "Zypern",
    358: "Finnland",
    359: "Bulgarien",
    370: "Litauen",
    371: "Lettland",
    372: "Estland",
    373: "Moldau",
    374: "Armenien",
    375: "Belarus",
    376: "Andorra",
    377: "Monaco",
    378: "San Marino",
    380: "Ukraine",
    381: "Serbien",
    382: "Montenegro",
    383: "Kosovo",
    385: "Kroatien",
    386: "Slowenien",
    387: "Bosnien und Herzegowina",
    389: "Nordmazedonien",
    420: "Tschechien",
    421: "Slowakei",
    423: "Liechtenstein",
    500: "Falklandinseln",
    501: "Belize",
    502: "Guatemala",
    503: "El Salvador",
    504: "Honduras",
    505: "Nicaragua",
    506: "Costa Rica",
    507: "Panama",
    508: "Saint-Pierre und Miquelon",
    509: "Haiti",
    590: "Guadeloupe/Saint-Martin",
    591: "Bolivien",
    592: "Guyana",
    593: "Ecuador",
    594: "Französisch-Guayana",
    595: "Paraguay",
    596: "Martinique",
    597: "Suriname",
    598: "Uruguay",
    599: "Karibische Niederlande/Curaçao",
    670: "Osttimor",
    673: "Brunei",
    674: "Nauru",
    675: "Papua-Neuguinea",
    676: "Tonga",
    677: "Salomonen",
    678: "Vanuatu",
    679: "Fidschi",
    680: "Palau",
    681: "Wallis und Futuna",
    682: "Cookinseln",
    683: "Niue",
    685: "Samoa",
    686: "Kiribati",
    687: "Neukaledonien",
    688: "Tuvalu",
    689: "Französisch-Polynesien",
    850: "Nordkorea",
    852: "Hongkong",
    853: "Macau",
    855: "Kambodscha",
    856: "Laos",
    880: "Bangladesch",
    886: "Taiwan",
    960: "Malediven",
    961: "Libanon",
    962: "Jordanien",
    963: "Syrien",
    964: "Irak",
    965: "Kuwait",
    966: "Saudi-Arabien",
    967: "Jemen",
    968: "Oman",
    970: "Palästina",
    971: "Vereinigte Arabische Emirate",
    972: "Israel",
    973: "Bahrain",
    974: "Katar",
    975: "Bhutan",
    976: "Mongolei",
    977: "Nepal",
    992: "Tadschikistan",
    993: "Turkmenistan",
    994: "Aserbaidschan",
    995: "Georgien",
    996: "Kirgisistan",
    998: "Usbekistan",
  };

  const STATE_BY_CITY = {
    mannheim: "2",
    heidelberg: "2",
    karlsruhe: "2",
    stuttgart: "2",
    freiburg: "2",
    ulm: "2",
    heilbronn: "2",
    muenchen: "3",
    munich: "3",
    nuernberg: "3",
    augsburg: "3",
    regensburg: "3",
    wuerzburg: "3",
    berlin: "4",
    potsdam: "5",
    bremen: "6",
    hamburg: "7",
    frankfurt: "8",
    wiesbaden: "8",
    kassel: "8",
    darmstadt: "8",
    rostock: "9",
    schwerin: "9",
    hannover: "10",
    braunschweig: "10",
    osnabrueck: "10",
    goettingen: "10",
    koeln: "11",
    duesseldorf: "11",
    dortmund: "11",
    essen: "11",
    duisburg: "11",
    bochum: "11",
    bonn: "11",
    muenster: "11",
    mainz: "12",
    ludwigshafen: "12",
    koblenz: "12",
    trier: "12",
    saarbruecken: "13",
    dresden: "14",
    leipzig: "14",
    chemnitz: "14",
    magdeburg: "15",
    halle: "15",
    kiel: "16",
    luebeck: "16",
    erfurt: "17",
    jena: "17",
    wien: "20",
    salzburg: "23",
    innsbruck: "24",
    graz: "27",
    zuerich: "35",
    bern: "36",
    basel: "37",
  };

  function getVisibleGalleryCloseControl() {
    const controls = Array.from(
      document.querySelectorAll('.icon-modal-close[data-dismiss="modal"]'),
    );
    const control = controls.reverse().find((element) => {
      const style = window.getComputedStyle(element);
      return (
        element.getClientRects().length > 0 &&
        style.display !== "none" &&
        style.visibility !== "hidden" &&
        style.visibility !== "collapse"
      );
    });
    return control;
  }

  function closeVisibleGallery() {
    // Let the site clean up its backdrop and gallery state.
    getVisibleGalleryCloseControl()?.click();
  }

  function hideLiveCamCards() {
    document
      .querySelectorAll('.ladies-card a.card-link[href*="/directre/"]')
      .forEach((link) => {
        link.closest(".ladies-card")?.remove();
      });
  }

  const FORM_SELECTORS = {
    name: "#q_1",
    profile: "#q_4",
    whatsapp: "#q_87",
    phone: "#q_6",
    city: "#q_9",
    state: 'select[name="10"]',
    country: 'select[name="11"]',
    shortdesc: "#a_12",
    report: "#vB_Editor_001_textarea",
    wysiwygMode: "#vB_Editor_001_mode",
  };

  function escapeHtml(value) {
    return String(value ?? "").replace(
      /[&<>"']/g,
      (char) =>
        ({
          "&": "&amp;",
          "<": "&lt;",
          ">": "&gt;",
          '"': "&quot;",
          "'": "&#039;",
        })[char],
    );
  }

  function normalizeText(value) {
    return String(value || "")
      .replace(/\u00a0/g, " ")
      .replace(/[ \t]+/g, " ")
      .replace(/[ \t]*\n[ \t]*/g, "\n")
      .replace(/\n{4,}/g, "\n\n\n")
      .trim();
  }

  function normalizeKey(value) {
    return String(value || "")
      .toLowerCase()
      .trim()
      .replace(/ä/g, "ae")
      .replace(/ö/g, "oe")
      .replace(/ü/g, "ue")
      .replace(/ß/g, "ss")
      .replace(/[^a-z0-9]+/g, " ")
      .trim();
  }

  function decodeSlug(value) {
    return String(value || "")
      .replace(/-/g, " ")
      .replace(/\b\w/g, (char) => char.toUpperCase())
      .trim();
  }

  function unique(values) {
    return [...new Set(values.filter(Boolean))];
  }

  function dispatchFieldEvents(element) {
    if (!element) return;

    ["input", "change", "blur"].forEach((type) => {
      element.dispatchEvent(new Event(type, { bubbles: true }));
    });
  }

  function setInputValue(selector, value) {
    const element = document.querySelector(selector);

    if (!element || value == null || value === "") return false;

    element.value = value;
    dispatchFieldEvents(element);

    return true;
  }

  function setSelectValue(selector, value) {
    const element = document.querySelector(selector);

    if (!element || value == null || value === "") return false;

    const wantedValue = String(value);
    const optionExists = [...element.options].some(
      (option) => option.value === wantedValue,
    );

    if (!optionExists) return false;

    element.value = wantedValue;
    dispatchFieldEvents(element);

    return true;
  }

  const leanGalleryAds = new Map();
  const leanGalleryRoots = new WeakSet();
  let leanGalleryFrame = null;

  function scheduleLeanGallerySync() {
    if (!document.documentElement.classList.contains("lh-lean") && !leanGalleryAds.size) return;
    if (leanGalleryFrame !== null) return;
    leanGalleryFrame = requestAnimationFrame(() => {
      leanGalleryFrame = null;
      syncLeanGallerySlides();
    });
  }

  function galleryAncestor(element, selector) {
    while (element) {
      const match = element.closest?.(selector);
      if (match) return match;
      element = element.getRootNode?.().host;
    }
    return null;
  }

  function syncLeanGallerySlides() {
    if (!document.documentElement.classList.contains("lh-lean") && !leanGalleryAds.size) return;
    if (!getAdvertId() && !leanGalleryAds.size) return;
    const sliders = new Set();
    const roots = [document];
    for (let index = 0; index < roots.length; index++) {
      const root = roots[index];
      if (!leanGalleryRoots.has(root)) {
        leanGalleryRoots.add(root);
        const observer = new MutationObserver(scheduleLeanGallerySync);
        observer.observe(root, {
          childList: true,
          subtree: true,
          attributes: true,
          attributeFilter: ["data-advert-slide"],
        });
      }
      root.querySelectorAll("*").forEach((element) => {
        if (element.shadowRoot) roots.push(element.shadowRoot);
      });
    }
    if (document.documentElement.classList.contains("lh-lean")) {
      roots.forEach((root) =>
        root
          .querySelectorAll(
            '[data-advert-slide], .solads-slide, iframe.sol-frame[aria-label="SolAds"], iframe[src="/api/plutos"]',
          )
          .forEach((ad) => {
            const slide = galleryAncestor(ad, "swiper-slide, .swiper-slide");
            if (!slide || leanGalleryAds.has(slide)) return;
            const slider = galleryAncestor(slide, "swiper-container, .swiper");
            const marker = document.createComment("ladies-helper-ad-slide");
            slide.before(marker);
            leanGalleryAds.set(slide, marker);
            slide.remove();
            if (slider) sliders.add(slider);
          }),
      );
      for (const [slide, marker] of leanGalleryAds) {
        if (!marker.isConnected) leanGalleryAds.delete(slide);
      }
    } else {
      for (const [slide, marker] of leanGalleryAds) {
        if (marker.isConnected) {
          const slider = galleryAncestor(
            marker.parentElement,
            "swiper-container, .swiper",
          );
          marker.replaceWith(slide);
          if (slider) sliders.add(slider);
        }
      }
      leanGalleryAds.clear();
    }
    sliders.forEach((slider) => {
      const swiper = slider.swiper;
      if (swiper && !swiper.destroyed) {
        swiper.update();
        const index = Math.min(
          swiper.activeIndex || 0,
          Math.max(0, swiper.slides.length - 1),
        );
        swiper.slideTo(index, 0, false);
      }
    });
  }

  function readLeanPreference() {
    try {
      return localStorage.getItem("ladies-helper-lean") === "true";
    } catch {
      return false;
    }
  }

  function setLeanMode(enabled) {
    document.documentElement.classList.toggle("lh-lean", enabled);
    syncLeanGallerySlides();
    try {
      localStorage.setItem("ladies-helper-lean", String(enabled));
    } catch {
      /* The toggle still works when browser storage is unavailable. */
    }
  }

  function initLeanMode() {
    const style = document.createElement("style");
    style.id = "ladies-helper-lean-style";
    // CSS also applies to sections inserted later. Removing the class restores
    // their original display rules without deleting or recreating page content.
    style.textContent = `
      html.lh-lean swiper-slide:has([data-advert-slide]),
      html.lh-lean swiper-slide:has(.solads-slide),
      html.lh-lean .swiper-slide:has([data-advert-slide]),
      html.lh-lean .swiper-slide:has(.solads-slide),
      html.lh-lean .container.relative.main-navigation,
      html.lh-lean nav.navbar.navbar-lds,
      html.lh-lean ul.nav.navbar-nav,
      html.lh-lean .options-container.top.padded-bottom,
      html.lh-lean .card-list > h1:has(.title-adverts-count),
      html.lh-lean .card-list > h3,
      html.lh-lean div.col-xs-12.untergruppe.text-left,
      html.lh-lean #advert-results-found-info,
      html.lh-lean footer.hidden-print,
      html.lh-lean #azm-stars-promotional-content,
      html.lh-lean .page-stripe-azm-stars-promotional-content,
      html.lh-lean .azm-stars-promotional-content,
      html.lh-lean .container:has(> .row-azm-stars-promotional-content) {
        display: none !important;
      }
    `;
    document.head.appendChild(style);
    document.documentElement.classList.toggle("lh-lean", readLeanPreference());
    syncLeanGallerySlides();
    // Newly attached shadow roots do not produce document mutations.
    setInterval(() => {
      if (document.documentElement.classList.contains("lh-lean"))
        scheduleLeanGallerySync();
    }, 1000);
  }

  function addLeanToggle(box) {
    const content = box.querySelector(".lh-content");
    if (!content) return;
    const row = document.createElement("div");
    row.className = "lh-section";
    const label = document.createElement("label");
    const checkbox = document.createElement("input");
    checkbox.type = "checkbox";
    checkbox.className = "lh-lean-toggle";
    checkbox.checked = readLeanPreference();
    checkbox.addEventListener("change", () => setLeanMode(checkbox.checked));
    label.title = "Menü, Werbung (auch in Galerien) und Seitenfuß ausblenden";
    label.append(checkbox, " Schlank");
    const loadControls = content
      .querySelector(".lh-load-restart")
      ?.closest(".lh-mini-buttons");
    if (loadControls) {
      loadControls.style.flexWrap = "wrap";
      label.style.marginLeft = "auto";
      label.style.whiteSpace = "nowrap";
      loadControls.appendChild(label);
    } else {
      row.style.display = "flex";
      row.style.alignItems = "center";
      row.style.flexWrap = "wrap";
      row.style.gap = "6px 10px";
      row.appendChild(label);
      const ratingControls = box.querySelector(".lh-intim-controls");
      if (ratingControls) row.appendChild(ratingControls);
      content.prepend(row);
    }
  }

  function addStyle() {
    if (document.getElementById(CONFIG.styleId)) return;

    const style = document.createElement("style");

    style.id = CONFIG.styleId;
    style.textContent = `
      #${CONFIG.boxId} {
        position: fixed;
        top: 16px;
        right: 16px;
        z-index: 99999999;
        width: 460px;
        max-width: calc(100vw - 32px);
        background: #fff;
        color: #000;
        border: 2px solid #d00000;
        border-radius: 8px;
        padding: 9px 10px;
        font: 13px/1.35 Arial, sans-serif;
        box-shadow: 0 3px 14px rgba(0,0,0,.3);
        word-break: break-word;
      }

      #${CONFIG.boxId} .lh-header {
        display: flex;
        flex-wrap: wrap;
        align-items: flex-start;
        justify-content: space-between;
        gap: 6px;
      }

      #${CONFIG.boxId} .lh-header-text {
        flex: 1;
        min-width: 0;
        line-height: 1.3;
      }

      #${CONFIG.boxId} .lh-number-grid {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        column-gap: 14px;
        row-gap: 3px;
        margin-top: 3px;
      }

      #${CONFIG.boxId} .lh-number-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 6px;
        min-width: 0;
      }

      @media (max-width: 480px) {
        #${CONFIG.boxId} .lh-number-grid {
          grid-template-columns: minmax(0, 1fr);
        }
      }

      #${CONFIG.boxId} .lh-number-text {
        font-family: monospace;
        white-space: nowrap;
      }

      #${CONFIG.boxId} .lh-number-links {
        display: flex;
        gap: 4px;
        align-items: center;
      }

      #${CONFIG.boxId} .lh-number-links a {
        text-decoration: none;
        font-weight: 700;
        white-space: nowrap;
      }


      #${CONFIG.boxId} .lh-actions {
        display: flex;
        gap: 4px;
        flex-shrink: 0;
      }

      #${CONFIG.boxId} .lh-label {
        font-weight: 700;
      }

      #${CONFIG.boxId} .lh-content,
      #${CONFIG.boxId} .lh-section {
        margin-top: 6px;
      }

      #${CONFIG.boxId} .lh-formats {
        user-select: text;
        cursor: text;
      }

      #${CONFIG.boxId} a {
        color: #0645ad;
        text-decoration: underline;
      }

      #${CONFIG.boxId} .lh-links {
        line-height: 1.55;
      }

      #${CONFIG.boxId} .lh-separator {
        color: #777;
        margin: 0 3px;
      }

      #${CONFIG.boxId} .lh-mini-buttons {
        margin-top: 5px;
        display: flex;
        gap: 5px;
        align-items: center;
      }

      #${CONFIG.boxId} .lh-copy-button,
      #${CONFIG.boxId} .lh-help,
      #${CONFIG.boxId} .lh-minimize,
      #${CONFIG.boxId} .lh-close {
        cursor: pointer;
        border: 1px solid #999;
        border-radius: 4px;
        background: #f4f4f4;
        color: #000;
        font-weight: 700;
        line-height: 1;
      }

      #${CONFIG.boxId} .lh-copy-button {
        padding: 3px 6px;
        font-size: 11px;
      }

      #${CONFIG.boxId} .lh-copy-button.lh-copied {
        background: #e8ffe8;
        border-color: #4a9b4a;
      }

      #${CONFIG.boxId} .lh-help,
      #${CONFIG.boxId} .lh-minimize,
      #${CONFIG.boxId} .lh-close {
        width: 22px;
        height: 21px;
        padding: 0;
      }

      #${CONFIG.boxId} .lh-help {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        box-sizing: border-box;
        text-decoration: none;
      }

      #${CONFIG.boxId} .lh-close {
        font-size: 15px;
      }

      #${CONFIG.boxId}.lh-collapsed {
        width: 460px;
      }

      #${CONFIG.boxId}.lh-collapsed .lh-content {
        display: none;
      }

      #${CONFIG.boxId} .lh-warning {
        color: #b00000;
        font-weight: 700;
      }

      #${CONFIG.noteId} {
        position: fixed;
        top: 16px;
        right: 16px;
        z-index: 99999999;
        background: #fff;
        color: #000;
        border: 2px solid #2d7a2d;
        border-radius: 8px;
        padding: 8px 10px;
        font: 13px Arial, sans-serif;
        box-shadow: 0 3px 14px rgba(0,0,0,.25);
      }
    `;

    document.head.appendChild(style);
  }

  async function downloadAllAdvertPictures(imageLinks, control) {
    if (control.getAttribute("aria-disabled") === "true") return;
    if (typeof GM_download !== "function") {
      control.textContent = "Download nicht verfügbar – Skript aktualisieren und Seite neu laden";
      return;
    }
    control.setAttribute("aria-disabled", "true");
    let downloaded = 0;
    let failed = 0;
    try {
      for (const [index, image] of imageLinks.entries()) {
        control.textContent = `Download ${index + 1} / ${imageLinks.length} …`;
        try {
          await new Promise((resolve, reject) => {
            GM_download({
              url: image.imageUrl,
              name: image.fileName || `ladies-bild-${index + 1}.jpg`,
              saveAs: false,
              timeout: 60000,
              onload: resolve,
              onerror: reject,
              ontimeout: () => reject(new Error("Download-Zeitlimit überschritten")),
            });
          });
          downloaded++;
        } catch (error) {
          failed++;
          console.warn("Ladies Helper: Bild-Download fehlgeschlagen", error);
        }
      }
      control.textContent = failed
        ? `${downloaded} gespeichert, ${failed} fehlgeschlagen – Download-Berechtigungen in Tampermonkey prüfen`
        : `${downloaded} Bilder gespeichert – erneut herunterladen`;
    } finally {
      control.removeAttribute("aria-disabled");
    }
  }

  function getAdvertId() {
    const match = window.location.pathname.match(/-(\d+)(?:\/)?$/);

    return match ? match[1] : null;
  }

  function getProfileId() {
    const element =
      document.querySelector("[data-profile-id]") ||
      document.querySelector("div.advert-actions.hidden-print");

    return (
      element?.getAttribute("data-profile-id") ||
      element?.dataset?.profileId ||
      null
    );
  }

  function buildSingleLadiesForumUrl(number) {
    const searchJson = {
      keywords: `"${number}"`,
      title_only: 0,
    };

    return (
      CONFIG.ladiesForumBaseUrl + encodeURIComponent(JSON.stringify(searchJson))
    );
  }

  function getRawPhone() {
    const telephoneElement = document.querySelector('[itemprop="telephone"]');

    if (telephoneElement?.textContent?.trim()) {
      return telephoneElement.textContent.trim();
    }

    const text = document.body?.innerText || "";
    const match =
      text.match(/(?:\+\d{1,4}|00\d{1,4}|0)\s*[\d][\d\s\-\/().]{5,22}\d/) ||
      text.match(/\b\d{7,18}\b/);

    return match ? match[0].trim() : null;
  }

  function getCountryCodeInfoFromDigits(digits) {
    const cleanDigits = String(digits || "").replace(/\D/g, "");

    for (let length = 3; length >= 1; length -= 1) {
      const code = cleanDigits.slice(0, length);

      if (COUNTRY_CODES[code]) {
        return {
          code,
          prefix: `+${code}`,
          country: COUNTRY_CODES[code],
        };
      }
    }

    return null;
  }

  function normalizeGermanMobile(rawPhone) {
    if (!rawPhone) return null;

    const digits = String(rawPhone)
      .trim()
      .replace(/[^\d+]/g, "")
      .replace(/^(\+49)/, "0")
      .replace(/^(0049)/, "0")
      .replace(/^(49)(?=1[5-7])/, "0")
      .replace(/\D/g, "");

    if (!digits.startsWith("0")) return null;

    const prefix = MOBILE_PREFIXES.find((value) => digits.startsWith(value));

    if (!prefix) return null;

    const rest = digits.slice(prefix.length);

    if (rest.length < 5) return null;

    const plain = `${prefix}${rest}`;
    const main = `${prefix}-${rest}`;
    const slash = `${prefix}/${rest}`;
    let altMain = "";
    let altSlash = "";
    if (prefix.length === 5) {
      altMain = `${prefix.slice(0, 4)}-${prefix.slice(4)}${rest}`;
      altSlash = `${prefix.slice(0, 4)}/${prefix.slice(4)}${rest}`;
    }
    const plus49 = `+49${plain.slice(1)}`;
    const international0049 = `0049${plain.slice(1)}`;
    const whatsappNumber = plus49.replace(/[^\d]/g, "");

    return {
      raw: rawPhone,
      main,
      plain,
      slash,
      plus49,
      international0049,
      whatsappNumber,
      all: unique([
        main,
        altMain,
        plain,
        slash,
        altSlash,
        plus49,
        international0049,
      ]),
      type: "de_mobile",
      isGermanMobile: true,
      isGermanLandline: false,
      isInternational: true,
      countryCodeInfo: {
        code: "49",
        prefix: "+49",
        country: "Deutschland",
      },
      displayWarning: "",
    };
  }

  function isLikelyGermanLandlineDigits(digits) {
    const cleanDigits = String(digits || "").replace(/\D/g, "");

    if (!/^0[2-9]\d{5,11}$/.test(cleanDigits)) return false;
    if (/^01/.test(cleanDigits)) return false;
    if (/^032/.test(cleanDigits)) return false;
    if (/^0700/.test(cleanDigits)) return false;
    if (/^0800/.test(cleanDigits)) return false;
    if (/^0900/.test(cleanDigits)) return false;

    return true;
  }

  function splitGermanLandline(digits) {
    const cleanDigits = String(digits || "").replace(/\D/g, "");

    if (
      cleanDigits.startsWith("030") ||
      cleanDigits.startsWith("040") ||
      cleanDigits.startsWith("069") ||
      cleanDigits.startsWith("089")
    ) {
      return {
        areaCode: cleanDigits.slice(0, 3),
        subscriber: cleanDigits.slice(3),
      };
    }

    return {
      areaCode: cleanDigits.slice(0, 4),
      subscriber: cleanDigits.slice(4),
    };
  }

  function normalizeGermanLandline(rawPhone) {
    if (!rawPhone) return null;

    const raw = normalizeText(rawPhone);
    const compact = raw
      .replace(/[^\d+]/g, "")
      .replace(/^(\+49)/, "0")
      .replace(/^(0049)/, "0")
      .replace(/^(49)(?=[2-9])/, "0");

    const digits = compact.replace(/\D/g, "");

    if (!isLikelyGermanLandlineDigits(digits)) return null;

    const { areaCode, subscriber } = splitGermanLandline(digits);

    if (!areaCode || subscriber.length < 3) return null;

    const nationalWithoutZero = digits.slice(1);
    const plus49 = `+49${nationalWithoutZero}`;
    const international0049 = `0049${nationalWithoutZero}`;
    const whatsappNumber = `49${nationalWithoutZero}`;
    const main = `${areaCode}-${subscriber}`;
    const slash = `${areaCode}/${subscriber}`;

    return {
      raw,
      main,
      plain: digits,
      slash,
      plus49,
      international0049,
      whatsappNumber,
      all: unique([main, digits, slash, plus49, international0049]),
      type: "de_landline",
      isGermanMobile: false,
      isGermanLandline: true,
      isInternational: true,
      countryCodeInfo: {
        code: "49",
        prefix: "+49",
        country: "Deutschland",
      },
      displayWarning: "",
    };
  }

  function normalizeAnyPhone(rawPhone) {
    if (!rawPhone) return null;

    const germanMobile = normalizeGermanMobile(rawPhone);

    if (germanMobile) return germanMobile;

    const germanLandline = normalizeGermanLandline(rawPhone);

    if (germanLandline) return germanLandline;

    const raw = normalizeText(rawPhone);
    const compact = raw.replace(/[^\d+]/g, "");

    if (!compact) return null;

    const digits = compact.replace(/\D/g, "");

    let plus = "";
    let international00 = "";
    let main = raw;
    let whatsappNumber = digits;
    let countryCodeInfo = null;

    if (compact.startsWith("+")) {
      plus = `+${digits}`;
      international00 = `00${digits}`;
      main = plus;
      whatsappNumber = digits;
      countryCodeInfo = getCountryCodeInfoFromDigits(digits);
    } else if (compact.startsWith("00")) {
      const without00 = digits.replace(/^00/, "");
      plus = `+${without00}`;
      international00 = `00${without00}`;
      main = plus;
      whatsappNumber = without00;
      countryCodeInfo = getCountryCodeInfoFromDigits(without00);
    } else if (digits.startsWith("49") && digits.length >= 8) {
      plus = `+${digits}`;
      international00 = `00${digits}`;
      main = plus;
      whatsappNumber = digits;
      countryCodeInfo = getCountryCodeInfoFromDigits(digits);
    }

    const grouped = plus ? plus.replace(/^(\+\d{1,4})(\d+)$/, "$1-$2") : raw;

    const countryHint = countryCodeInfo
      ? ` Ländervorwahl erkannt: ${countryCodeInfo.prefix} ${countryCodeInfo.country}.`
      : "";

    return {
      raw,
      main: grouped || main,
      plain: digits,
      slash: raw,
      plus49: plus,
      international0049: international00,
      whatsappNumber,
      all: unique([grouped, plus, international00, digits, raw]),
      type: "international_or_unknown",
      isGermanMobile: false,
      isGermanLandline: false,
      isInternational: Boolean(plus),
      countryCodeInfo,
      displayWarning: `Rufnummer gefunden, aber nicht als deutsche Mobil- oder Festnetznummer erkannt.${countryHint}`,
    };
  }

  function getTitleFromPage() {
    const h1Text = document.querySelector("h1")?.textContent?.trim();

    if (h1Text) return h1Text.replace(/\s+/g, " ");

    return String(document.title || "")
      .replace(/\s*[-|].*$/g, "")
      .replace(/\s+/g, " ")
      .trim();
  }

  function getLastUrlPartWithoutAdvertId() {
    const parts = window.location.pathname.split("/").filter(Boolean);
    const lastPart = parts[parts.length - 1] || "";

    return lastPart.replace(/-\d+$/, "");
  }

  function getCityFromUrl() {
    const firstToken = getLastUrlPartWithoutAdvertId().split("-")[0] || "";

    return decodeSlug(firstToken);
  }

  function getArtistNameFromPage() {
    const selectors = [
      "h2.page-heading.kuenstlername",
      ".page-heading.kuenstlername",
      ".kuenstlername",
      '[class*="kuenstlername"]',
    ];

    for (const selector of selectors) {
      const text = normalizeText(document.querySelector(selector)?.textContent);

      if (text) return text;
    }

    return "";
  }

  function getNameFromUrl() {
    const artistName = getArtistNameFromPage();

    if (artistName) return artistName;

    const tokens = getLastUrlPartWithoutAdvertId().split("-").filter(Boolean);

    if (tokens.length <= 1) return getTitleFromPage();

    return decodeSlug(tokens.slice(1).join("-")) || getTitleFromPage();
  }

  function getStateValueForCity(city) {
    const key = normalizeKey(city).replace(/\s+/g, "");

    return STATE_BY_CITY[key] || "0";
  }

  function getCountryValueForCity(city) {
    const state = getStateValueForCity(city);

    if (["20", "21", "22", "23", "24", "25", "26", "27", "28"].includes(state))
      return "2";
    if (["31", "32"].includes(state)) return "4";
    if (["35", "36", "37", "38", "39", "40", "41", "42", "43"].includes(state))
      return "3";

    return "1";
  }

  function getAdvertTextWithoutPs() {
    const container =
      document.querySelector(".row_anzeigentext .einzelansicht-text") ||
      document.querySelector(".einzelansicht-text") ||
      document.querySelector(".row_anzeigentext");

    if (!container) return "";

    const clone = container.cloneNode(true);

    clone
      .querySelectorAll(".text-trans, script, style")
      .forEach((element) => element.remove());

    clone.querySelectorAll("div").forEach((div) => {
      const text = normalizeText(div.textContent);

      if (
        /^PS\s*:/i.test(text) ||
        /Bitte erwähne/i.test(text) ||
        /Anzeige von/i.test(text)
      ) {
        div.remove();
      }
    });

    return normalizeText(clone.textContent)
      .replace(/PS\s*:\s*Bitte erwähne[\s\S]*$/i, "")
      .replace(/Bitte erwähne[\s\S]*$/i, "")
      .trim();
  }

  function getTextWithoutChildren(element, childSelector) {
    if (!element) return "";

    const clone = element.cloneNode(true);

    clone.querySelectorAll(childSelector).forEach((child) => child.remove());

    return normalizeText(clone.textContent);
  }

  function getAttributeTextFromItem(item, label) {
    const valueContainer =
      item.querySelector(".col-xs-7") ||
      item.querySelector(".attribute-value") ||
      item.querySelector('[class*="value"]');

    const directValue =
      normalizeText(valueContainer?.querySelector("span")?.textContent) ||
      normalizeText(valueContainer?.querySelector("p")?.textContent) ||
      getTextWithoutChildren(valueContainer, "strong, label");

    if (directValue) return directValue;

    const itemText = normalizeText(item.textContent);

    if (!itemText) return "";

    return label ? normalizeText(itemText.replace(label, "")) : itemText;
  }

  function getAttributeListFromPage() {
    const values = [];
    const seen = new Set();

    function addAttribute(label, value) {
      const cleanLabel = normalizeText(label).replace(/\s*:\s*$/g, "");
      const cleanValue = normalizeText(value).replace(/\s*:\s*/g, ": ");

      if (!cleanLabel || !cleanValue) return;
      if (/Alle Angaben ohne Gewähr/i.test(`${cleanLabel} ${cleanValue}`))
        return;
      if (
        /Ganzen Text|Drucken|Kontakt|Telefon|WhatsApp|Anzeige melden/i.test(
          `${cleanLabel} ${cleanValue}`,
        )
      )
        return;

      const line = `${cleanLabel}: ${cleanValue}`;
      const key = normalizeKey(line);

      if (seen.has(key)) return;

      seen.add(key);
      values.push(line);
    }

    document
      .querySelectorAll(
        ".einzelansicht-attributes .attribute-items, .trait-attributes .attribute-items",
      )
      .forEach((item) => {
        const label =
          normalizeText(item.querySelector(".hidden-md strong")?.textContent) ||
          normalizeText(item.querySelector("strong")?.textContent) ||
          normalizeText(item.querySelector("label")?.textContent);

        const value = getAttributeTextFromItem(item, label);

        addAttribute(label, value);
      });

    [
      ".row_attribute li",
      ".row_attributes li",
      ".row_merkmale li",
      ".row_ausstattung li",
      ".advert-attributes li",
      ".profile-attributes li",
      ".attributes li",
      ".merkmale li",
      ".attribute-list li",
      '[class*="attribute"] li',
      '[class*="merkmal"] li',
      '[class*="ausstattung"] li',
    ].forEach((selector) => {
      document.querySelectorAll(selector).forEach((element) => {
        const label =
          normalizeText(element.querySelector("strong")?.textContent) ||
          normalizeText(element.querySelector("label")?.textContent);

        let value = "";

        if (label) {
          value =
            normalizeText(element.querySelector("span")?.textContent) ||
            getTextWithoutChildren(element, "strong, label");
        }

        if (label && value) {
          addAttribute(label, value);
          return;
        }

        const text = normalizeText(element.textContent);

        if (!text || text.length > 120) return;

        const parts = text.split(/\s*:\s*/);

        if (parts.length >= 2) {
          addAttribute(parts.shift(), parts.join(": "));
        }
      });
    });

    return values;
  }

  function formatAttributesForBbCode(attributes) {
    if (!attributes.length) return "";

    return [
      "[B]Attribute:[/B]",
      ...attributes.map((value) => `- ${value}`),
    ].join("\n");
  }

  function getAbsoluteUrl(value) {
    if (!value) return null;

    const cleanValue = String(value).trim();

    if (
      !cleanValue ||
      cleanValue.startsWith("data:") ||
      cleanValue.startsWith("blob:") ||
      cleanValue.startsWith("javascript:")
    ) {
      return null;
    }

    try {
      return new URL(cleanValue, window.location.href).href;
    } catch {
      return null;
    }
  }

  function getImageFileName(url) {
    try {
      const parsed = new URL(url, window.location.href);

      return decodeURIComponent(parsed.pathname.split("/").pop() || "");
    } catch {
      return "";
    }
  }

  function extractUrlsFromSrcset(srcset) {
    if (!srcset) return [];

    return String(srcset)
      .split(",")
      .map((part) => part.trim().split(/\s+/)[0])
      .filter(Boolean);
  }

  function extractBackgroundImageUrls(styleValue) {
    if (!styleValue) return [];

    const urls = [];
    const regex = /url\(["']?([^"')]+)["']?\)/gi;
    let match;

    while ((match = regex.exec(styleValue)) !== null) {
      urls.push(match[1]);
    }

    return urls;
  }

  function collectPageImageUrls() {
    const rawUrls = [];

    document.querySelectorAll("img").forEach((img) => {
      rawUrls.push(
        img.getAttribute("src"),
        img.getAttribute("data-src"),
        img.getAttribute("data-original"),
        img.getAttribute("data-lazy-src"),
        img.getAttribute("data-full"),
        img.getAttribute("data-image"),
        img.currentSrc,
      );

      rawUrls.push(...extractUrlsFromSrcset(img.getAttribute("srcset")));
      rawUrls.push(...extractUrlsFromSrcset(img.getAttribute("data-srcset")));
    });

    document.querySelectorAll("a[href]").forEach((link) => {
      rawUrls.push(link.getAttribute("href"));
    });

    document.querySelectorAll('[style*="background"]').forEach((element) => {
      rawUrls.push(
        ...extractBackgroundImageUrls(element.getAttribute("style")),
      );
    });

    return rawUrls
      .flatMap((value) => {
        const text = String(value || "");
        const directUrl = getAbsoluteUrl(text);
        const embeddedMatches =
          text.match(
            /https?:\/\/[^"'\s<>]+\.(?:jpe?g|png|webp)(?:\?[^"'\s<>]*)?/gi,
          ) || [];
        const relativeMatches =
          text.match(
            /[^"'\s<>]+\/\d+-A\d+\.(?:jpe?g|png|webp)(?:\?[^"'\s<>]*)?/gi,
          ) || [];

        return [
          directUrl,
          ...embeddedMatches.map(getAbsoluteUrl),
          ...relativeMatches.map(getAbsoluteUrl),
        ];
      })
      .filter(Boolean);
  }

  function buildGoogleLensUrl(imageUrl) {
    return CONFIG.googleLensBaseUrl + encodeURIComponent(imageUrl);
  }

  function getAdvertImageLinks(advertId) {
    if (!advertId) return [];

    const uniqueBySuffix = new Map();
    const regex = new RegExp(`^${advertId}-(A\\d+)\\.(jpe?g|png|webp)$`, "i");

    collectPageImageUrls().forEach((url) => {
      const fileName = getImageFileName(url);
      const match = fileName.match(regex);

      if (!match) return;

      const suffix = match[1].toUpperCase();
      const suffixNumber = Number(suffix.replace("A", ""));

      if (!uniqueBySuffix.has(suffix)) {
        uniqueBySuffix.set(suffix, {
          label: suffix,
          copyLabel: `Google Lens Bild ${suffix}`,
          fileName,
          imageUrl: url,
          url: buildGoogleLensUrl(url),
          suffixNumber,
        });
      }
    });

    return [...uniqueBySuffix.values()].sort(
      (a, b) => a.suffixNumber - b.suffixNumber,
    );
  }

  function buildGooglePhoneUrl(phone) {
    const query = phone.all
      .filter(Boolean)
      .map((number) => `"${number}"`)
      .join(" OR ");

    return CONFIG.googleSearchBaseUrl + encodeURIComponent(query);
  }

  function buildSingleGoogleUrl(number) {
    return CONFIG.googleSearchBaseUrl + encodeURIComponent(`"${number}"`);
  }

  function buildSingleLadiesUrl(number) {
    return CONFIG.ladiesSearchBaseUrl + encodeURIComponent(number);
  }

  function buildSingleIntimUrl(number) {
    const digits = String(number).replace(/\D/g, "");
    return CONFIG.intimBaseUrl + encodeURIComponent(digits) + "/";
  }

  function buildIntimUrl(phone) {
    return CONFIG.intimBaseUrl + encodeURIComponent(phone.main) + "/";
  }

  function requestIntimApi(url) {
    return new Promise((resolve, reject) => {
      const endpoint = new URL(url);
      if (endpoint.origin !== "https://intim.to") {
        reject(new Error("Ungültige intim.to-Adresse"));
        return;
      }
      endpoint.searchParams.set("format", "api");
      GM_xmlhttpRequest({
        method: "GET",
        url: endpoint.href,
        anonymous: true,
        timeout: 20000,
        onload: (response) => {
          try {
            if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
            let data;
            try { data = JSON.parse(response.responseText); }
            catch { throw new Error("Server liefert kein JSON (möglicherweise eine Schutz- oder Anmeldeseite)"); }
            resolve(data);
          } catch (error) { reject(error); }
        },
        onerror: () => reject(new Error("Netzwerkfehler")),
        ontimeout: () => reject(new Error("Zeitlimit überschritten")),
      });
    });
  }

  function parseIntimProfiles(data, phone) {
    if (!Array.isArray(data.profiles) || Number(data.result_total) !== data.profiles.length)
      throw new Error("Unvollständige intim.to-Antwort");
    const digits = phone.main.replace(/\D/g, "");
    const profiles = new Map();
    for (const item of data.profiles) {
      const url = new URL(item.url_absolute);
      const match = url.pathname.match(/^\/de\/tel\/([^/]+)\/([^/]+)\/$/);
      if (url.origin !== "https://intim.to" || !match ||
          match[1].replace(/\D/g, "") !== digits)
        throw new Error("Unerwartetes intim.to-Profil");
      url.search = "";
      url.hash = "";
      profiles.set(url.href, { url: url.href, name: String(item.name || match[2]) });
    }
    return [...profiles.values()];
  }

  function parseIntimRating(data, profileUrl) {
    const schema = data.schema_org;
    if (!schema || new URL(schema.url, "https://intim.to").href !== profileUrl || !schema.rateit_arr)
      throw new Error("Bewertung nicht verfügbar");
    const rating = schema.rateit_arr;
    const count = Number(rating.TOTAL_RATINGS);
    const average = Number(rating.AVERAGE_RATING);
    if (rating.TOTAL_RATINGS == null || !Number.isInteger(count) || count < 0 ||
        (count > 0 && (rating.AVERAGE_RATING == null || !Number.isFinite(average) || average < 0 || average > 5)))
      throw new Error("Ungültige Bewertung");
    return { count, average: count ? average : null };
  }

  function readIntimCache() {
    try {
      const cache = JSON.parse(sessionStorage.getItem("lh-intim-ratings-v1") || "{}");
      return Object.fromEntries(Object.entries(cache).filter(([, entry]) =>
        entry && Date.now() - entry.time < 30 * 60 * 1000 && Array.isArray(entry.results)));
    } catch { return {}; }
  }

  function initIntimRatings(box, phone) {
    const container = box.querySelector(".lh-intim-ratings");
    if (!container || !phone) return;
    const checkbox = box.querySelector(".lh-intim-auto");
    const button = box.querySelector(".lh-intim-load");
    try {
      checkbox.checked = localStorage.getItem("lh-intim-auto") === "true";
    } catch { checkbox.checked = false; }
    let running = false;
    const load = async (refresh) => {
      if (running) return;
      running = true;
      button.disabled = true;
      container.querySelector(".lh-intim-status").textContent = "Bewertungen werden geladen …";
      try {
        await showIntimRatings(box, phone, refresh);
      } finally {
        running = false;
        button.disabled = false;
        button.textContent = "Neu laden";
      }
    };
    button.addEventListener("click", () => load(true));
    checkbox.addEventListener("change", () => {
      try { localStorage.setItem("lh-intim-auto", String(checkbox.checked)); }
      catch { /* Keep the choice for this page when storage is unavailable. */ }
      if (checkbox.checked) load(false);
    });
    if (checkbox.checked) load(false);
  }

  async function showIntimRatings(box, phone, refresh = false) {
    const container = box.querySelector(".lh-intim-ratings");
    if (!container || !phone) return;
    const status = container.querySelector(".lh-intim-status");
    const key = phone.main.replace(/\D/g, "");
    const render = (results) => {
      status.replaceChildren();
      if (!results.length) status.textContent = "Kein Profil zur Nummer gefunden.";
      for (const profile of results) {
        const row = document.createElement("div");
        const link = document.createElement("a");
        link.href = profile.url;
        link.textContent = profile.name;
        link.target = "_blank";
        link.rel = "noopener noreferrer";
        const label = profile.error ? `Bewertung nicht abrufbar: ${profile.error}` : profile.count
          ? `★ ${profile.average.toLocaleString("de-DE", { minimumFractionDigits: 1, maximumFractionDigits: 2 })} / 5 · ${profile.count} ${profile.count === 1 ? "Bewertung" : "Bewertungen"}`
          : "Keine Bewertung";
        row.append(link, document.createTextNode(` – ${label}`));
        status.appendChild(row);
      }
    };
    try {
      const cached = !refresh && readIntimCache()[key];
      if (cached) { render(cached.results); return; }
      const profiles = parseIntimProfiles(await requestIntimApi(buildIntimUrl(phone)), phone);
      const results = [];
      for (const profile of profiles) {
        if (!box.isConnected) return;
        status.textContent = `Bewertungen laden: ${results.length + 1} / ${profiles.length} …`;
        try {
          results.push({ ...profile, ...parseIntimRating(await requestIntimApi(profile.url), profile.url) });
        } catch (error) {
          console.warn("Ladies Helper: intim.to-Bewertung", error);
          results.push({ ...profile, error: error.message || "Unbekannter Fehler" });
        }
      }
      if (!box.isConnected) return;
      render(results);
      if (!results.some((profile) => profile.error)) {
        try {
          const cache = readIntimCache();
          delete cache[key];
          const entries = Object.entries(cache).slice(-49);
          entries.push([key, { time: Date.now(), results }]);
          sessionStorage.setItem("lh-intim-ratings-v1", JSON.stringify(Object.fromEntries(entries)));
        } catch { /* Lookup still works when storage is unavailable. */ }
      }
    } catch (error) {
      status.textContent = `Abfrage fehlgeschlagen: ${error.message || "Unbekannter Fehler"}`;
      console.warn("Ladies Helper: intim.to-Abfrage", error);
    }
  }

  function buildLadiesSearchUrl(phone) {
    return CONFIG.ladiesSearchBaseUrl + encodeURIComponent(phone.main);
  }

  function buildLadiesForumUrl(phone) {
    const searchJson = {
      keywords: phone.all.join(" OR "),
      title_only: 0,
    };

    return (
      CONFIG.ladiesForumBaseUrl + encodeURIComponent(JSON.stringify(searchJson))
    );
  }

  function buildWhatsappText(name) {
    const cleanName = normalizeText(name);

    if (cleanName) {
      return CONFIG.whatsappTextTemplate.replace("{name}", cleanName);
    }

    return "Hallo, was sind deine Preise?";
  }

  function buildWhatsappUrl(phone, name = "") {
    const text = buildWhatsappText(name);

    if (!phone?.whatsappNumber) {
      return `https://wa.me/?text=${encodeURIComponent(text)}`;
    }

    return `https://wa.me/${phone.whatsappNumber}?text=${encodeURIComponent(text)}`;
  }

  function buildArchiveUrl(profileUrl) {
    if (!profileUrl) return null;

    return CONFIG.archiveBaseUrl + encodeURIComponent(profileUrl);
  }

  function getResearchLinks(phone) {
    return [
      {
        label: "Google Rufnummersuche",
        shortLabel: "Google",
        url: buildGooglePhoneUrl(phone),
      },
      { label: "intim.to", shortLabel: "intim.to", url: buildIntimUrl(phone) },
    ];
  }

  function buildReportLinksBbCodeText(profileUrl, phone) {
    const lines = [];

    if (phone) {
      getResearchLinks(phone).forEach((link) => {
        lines.push(`${link.label}: ${link.url}`);
      });
    }

    return lines.join("\n");
  }

  function buildPhoneNumbersBbCodeText(phone) {
    if (!phone?.all?.length) return "";

    const numbers = unique(phone.all)
      .map((number) => normalizeText(number))
      .filter(Boolean);

    if (!numbers.length) return "";

    return ["[B]Rufnummern[/B]:", "- " + numbers.join("\n- ")].join("\n");
  }

  function buildLhReportUrl(prefillData) {
    const url = new URL(CONFIG.lhReportBaseUrl);
    const payload = new URLSearchParams();
    Object.entries(prefillData).forEach(([key, value]) => {
      if (value != null && value !== '') payload.set('lh_' + key, value);
    });
    // The browser retains the fragment; the forum receives only do=form&fid=1.
    url.hash = payload.toString();
    return url.href;
  }

  function createTextLink(label, url) {
    return `
      <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">
        ${escapeHtml(label)}
      </a>
    `;
  }

  function createInlineLinkRow(links) {
    return links
      .map((link) =>
        link.imageUrl
          ? `<a class="lh-image-link" href="${escapeHtml(link.url)}" data-preview-src="${escapeHtml(link.imageUrl)}" target="_blank" rel="noopener noreferrer">${escapeHtml(link.label)}</a>`
          : createTextLink(link.label, link.url),
      )
      .join('<span class="lh-separator">|</span>');
  }

  function addImageHoverPreviews(box) {
    const preview = document.createElement("img");
    preview.alt = "";
    preview.hidden = true;
    preview.setAttribute("aria-hidden", "true");
    Object.assign(preview.style, {
      position: "fixed",
      width: "180px",
      height: "220px",
      objectFit: "contain",
      background: "#fff",
      padding: "4px",
      border: "1px solid #999",
      borderRadius: "6px",
      boxShadow: "0 3px 14px rgba(0,0,0,.3)",
      pointerEvents: "none",
      zIndex: "2147483647",
      maxWidth: "calc(100vw - 24px)",
      maxHeight: "calc(100vh - 24px)",
    });
    box.appendChild(preview);
    const hide = () => {
      preview.hidden = true;
    };
    preview.addEventListener("error", hide);
    box.querySelectorAll(".lh-image-link").forEach((link) => {
      const show = () => {
        preview.src = link.dataset.previewSrc;
        preview.hidden = false;
        const anchor = link.getBoundingClientRect();
        const bounds = preview.getBoundingClientRect();
        const left = Math.max(
          8,
          Math.min(anchor.left, window.innerWidth - bounds.width - 8),
        );
        const below = anchor.bottom + 8;
        const top =
          below + bounds.height <= window.innerHeight - 8
            ? below
            : Math.max(8, anchor.top - bounds.height - 8);
        preview.style.left = left + "px";
        preview.style.top = top + "px";
      };
      link.addEventListener("mouseenter", show);
      link.addEventListener("mouseleave", hide);
      link.addEventListener("focus", show);
      link.addEventListener("blur", hide);
      link.addEventListener("click", hide);
    });
    // Capturing scroll also covers scrolling within the page's panels.
    document.addEventListener("scroll", hide, { capture: true, passive: true });
    window.addEventListener("resize", hide);
  }

  function getCopyLinks(profileUrl, phone) {
    const links = getResearchLinks(phone).map((link) => ({
      label: link.label,
      url: link.url,
    }));

    if (profileUrl) {
      links.push({
        label: "Ladies Profil",
        url: profileUrl,
      });
    }

    return links;
  }

  function getCopyData(profileUrl, phone) {
    return {
      numbers: phone.all,
      links: getCopyLinks(profileUrl, phone),
    };
  }

  function buildMarkdownText(copyData) {
    const lines = [];

    lines.push("Rufnummern:");
    copyData.numbers.forEach((number) => lines.push(number));
    lines.push("");

    copyData.links.forEach((link) => {
      lines.push(`[${link.label}](${link.url})`);
    });

    return lines.join("\n");
  }

  function buildBbCodeText(copyData) {
    const lines = [];

    lines.push("[B]Rufnummern:[/B]");
    copyData.numbers.forEach((number) => lines.push(number));
    lines.push("");

    copyData.links.forEach((link) => {
      lines.push(`[url=${link.url}]${link.label}[/url]`);
    });

    return lines.join("\n");
  }

  async function copyToClipboard(text, button) {
    try {
      await navigator.clipboard.writeText(text);
    } catch {
      const textarea = document.createElement("textarea");

      textarea.value = text;
      textarea.style.position = "fixed";
      textarea.style.left = "-9999px";

      document.body.appendChild(textarea);

      textarea.focus();
      textarea.select();

      document.execCommand("copy");

      textarea.remove();
    }

    if (!button) return;

    const oldText = button.textContent;

    button.textContent = "ok";
    button.classList.add("lh-copied");

    setTimeout(() => {
      button.textContent = oldText;
      button.classList.remove("lh-copied");
    }, 900);
  }

  function getWhatsappFieldValue(phone) {
    if (!phone) return "";

    if (phone.plus49 && phone.plus49.startsWith("+")) return phone.plus49;
    if (phone.main && String(phone.main).startsWith("+")) return phone.main;
    if (phone.whatsappNumber) return `+${phone.whatsappNumber}`;

    return phone.raw || "";
  }

  function getPrefillData(profileUrl, phone, rawPhoneFromPage = "") {
    const city = getCityFromUrl();
    const name = getNameFromUrl();
    const stateValue = getStateValueForCity(city);
    const countryValue = getCountryValueForCity(city);
    const advertText = getAdvertTextWithoutPs();
    const attributes = getAttributeListFromPage();
    const attributesBbCodeText = formatAttributesForBbCode(attributes);
    const phoneNumbersBbCodeText = buildPhoneNumbersBbCodeText(phone);
    const linksText = phone
      ? buildReportLinksBbCodeText(profileUrl, phone)
      : "";
    const whatsappFieldValue = getWhatsappFieldValue(phone);
    const shortdesc = normalizeText(advertText);

    const reportBlocks = [
      advertText ? `[B]Anzeigentext:[/B]\n[QUOTE]${advertText}[/QUOTE]` : "",
      attributesBbCodeText,
      phoneNumbersBbCodeText,
      `[i]Quelle: ${profileUrl}[/i]`,
      linksText ? `[B]Recherchelinks:[/B]\n${linksText}` : "",
      '[COLOR="Red"]Automatisch vorausgefüllt[/COLOR]',
      "[B]Eigener Bericht:[/B]\n\n...",
    ].filter(Boolean);

    const report = reportBlocks.join("\n\n");

    return {
      name,
      profile: profileUrl || "",
      whatsapp: whatsappFieldValue,
      phone: rawPhoneFromPage || phone?.raw || "",
      city,
      state: stateValue,
      country: countryValue,
      source: window.location.href,
      adverttext: advertText,
      attributes: attributes.join(" | "),
      shortdesc,
      report: normalizeText(report),
    };
  }

  function buildSnatchlistUrl(data, phone) {
    const numbers = unique([data.phone, data.whatsapp, phone?.plain,
      phone?.international0049, ...(phone?.all || [])]);
    const numberLines = numbers.map(number => number +
      ' [URL=' + buildSingleGoogleUrl(number) + ']G[/URL]' +
      ' [URL=' + buildSingleIntimUrl(number) + ']I[/URL]');
    const numberBlock = '[B]Rufnummern[/B]:\n' + numberLines.join('\n');
    const researchBlock = phone ? '[B]Recherchelinks:[/B]\n' +
      getResearchLinks(phone).map(link => '[URL=' + link.url + ']' + link.label + '[/URL]').join('\n') : '';
    let report = data.report || '';
    const numberPattern = /\[B\]Rufnummern\[\/B\]:[\s\S]*?(?=\n\n|$)/i;
    if (numberPattern.test(report)) report = report.replace(numberPattern, () => numberBlock);
    else if (numbers.length) report += '\n\n' + numberBlock;
    const researchPattern = /\[B\]Recherchelinks:\[\/B\][\s\S]*?(?=\n\n|$)/i;
    if (researchBlock) {
      if (researchPattern.test(report)) report = report.replace(researchPattern, () => researchBlock);
      else report += '\n\n' + researchBlock;
    }
    data = {...data, numbers: numbers.join('\n'), report};
    const url = new URL(CONFIG.snatchlistUrl);
    const payload = new URLSearchParams();
    Object.entries(data).forEach(([key, value]) => {
      if (value != null && value !== '') payload.set('sl_' + key, value);
    });
    url.hash = payload.toString();
    return url.href;
  }

  function snatchlistMessageHtml(report) {
    return '<p>' + escapeHtml(report)
      .replace(/\[b\]/gi, '<strong>').replace(/\[\/b\]/gi, '</strong>')
      .replace(/\[i\]/gi, '<em>').replace(/\[\/i\]/gi, '</em>')
      .replace(/\[quote\]/gi, '<blockquote>').replace(/\[\/quote\]/gi, '</blockquote>')
      .replace(/\[color(?:=[^\]]*)?\]|\[\/color\]/gi, '')
      .replace(/\[url=(https?:\/\/[^\]\s]+)\]([\s\S]*?)\[\/url\]/gi, '<a href="$1">$2</a>')
      .replace(/\n/g, '<br>') + '</p>';
  }

  function setSnatchlistEditor(form, report, force = false) {
    const textarea = form.querySelector('textarea.js-editor');
    if (!textarea) return false;
    const handler = window.XF?.getEditorInContainer?.(textarea);
    const rich = form.querySelector('.fr-element[contenteditable="true"]');
    const bb = handler?.ed && handler.isBbCodeView() ? handler.ed.bbCode.getTextArea() : null;
    if (!rich && !bb) return false;
    const current = bb ? bb.value : rich.textContent;
    if (!force && String(current || '').replace(/[\s\u200B\uFEFF]/g, '')) return true;
    const html = snatchlistMessageHtml(report);
    if (handler?.ed && typeof handler.replaceContent === 'function') {
      handler.replaceContent(html, report);
    }
    if (bb) {
      bb.value = report;
      dispatchFieldEvents(bb);
    } else {
      // Keep the visible editable surface and submitted textarea synchronized,
      // even when the editor handler is not exposed to the userscript.
      rich.innerHTML = html;
      textarea.value = html;
      rich.dispatchEvent(new Event('input', {bubbles: true}));
      dispatchFieldEvents(textarea);
      handler?.ed?.events?.trigger('contentChanged');
    }
    return false; // Confirm on the next pass, after editor initialization settles.
  }

  function addSnatchlistTags(form, data) {
    const field = form.querySelector('input[name="tags"]');
    const tagify = field && window.XF?.Element?.getHandler?.(field, 'token-input')?.tagify;
    if (!tagify) return false;
    const candidates = [data.name, data.city,
      ...(data.numbers || [data.phone, data.whatsapp].filter(Boolean).join('\n')).split('\n')];
    const existing = new Set(tagify.value.map(tag => String(tag.value).toLocaleLowerCase()));
    const pending = [];
    candidates.forEach(value => {
      const tag = String(value || '').trim().slice(0, 50);
      const key = tag.toLocaleLowerCase();
      if (tag.length < 2 || existing.has(key)) return;
      existing.add(key);
      pending.push(tag);
    });
    const available = Math.max(0, (tagify.settings.maxTags || 50) - tagify.value.length);
    if (pending.length && available) {
      tagify.addTags(pending.slice(0, available));
      field.value = tagify.getInputValue();
      dispatchFieldEvents(field);
    }
    return true;
  }

  function initSnatchlistForm() {
    const params = new URLSearchParams(location.hash.slice(1));
    const data = {};
    params.forEach((value, key) => { if (key.startsWith('sl_')) data[key.slice(3)] = value; });
    if (!data.name && !data.report) return;
    const filled = new WeakSet();
    const states = {
      '2':'BadenWuerttemberg', '3':'Bayern', '4':'Berlin', '5':'Brandenburg',
      '6':'Bremen', '7':'Hamburg', '8':'Hessen', '9':'MecklenburgVorpommern',
      '10':'Niedersachsen', '11':'NordrheinWestfalen', '12':'RheinlandPfalz',
      '13':'Saarland', '14':'Sachsen', '15':'SachsenAnhalt', '16':'SchleswigHolstein',
      '17':'Thueringen', '20':'Wien', '23':'Salzburg', '24':'Tirol', '27':'Steiermark',
      '35':'KantonZuerich', '36':'KantonBern', '37':'Basel'
    };
    const attributes = data.attributes || '';
    const height = attributes.match(/Größe:\s*(\d+)\s*cm/i)?.[1] || '';
    const bra = attributes.match(/Oberweite:\s*([^|]+)/i)?.[1]?.replace(/\s/g, '') || '';
    const values = {
      title: [data.name, data.city].filter(Boolean).join(' – ').slice(0, 150),
      'custom_fields[name]': data.name,
      'custom_fields[description]': data.adverttext,
      'custom_fields[sonstige_Anzeigen]': [data.profile, data.source].filter(Boolean).join('\n'),
      'custom_fields[phone]': data.numbers || [data.phone, data.whatsapp].filter(Boolean).join('\n'),
      'custom_fields[BL]': states[data.state] || '',
      'custom_fields[height]': height,
      'custom_fields[bra_size]': bra
    };
    let attempts = 0;
    const fill = () => {
      const title = document.querySelector('textarea[name="title"]');
      const form = title?.closest('form');
      if (!form) return false;
      const girl = form.querySelector('input[name="custom_fields[category]"][value="10"]');
      if (girl && !filled.has(girl)) {
        if (!form.querySelector('input[name="custom_fields[category]"]:checked')) {
          girl.checked = true;
          dispatchFieldEvents(girl);
        }
        filled.add(girl);
      }
      let ready = true;
      const tagsField = form.querySelector('input[name="tags"]');
      if (!tagsField || !filled.has(tagsField)) {
        if (addSnatchlistTags(form, data)) filled.add(tagsField);
        else ready = false;
      }
      Object.entries(values).forEach(([name, value]) => {
        if (!value) return;
        const field = form.querySelector('[name="' + name + '"]');
        if (!field) { ready = false; return; }
        if (filled.has(field)) return;
        // Preserve existing drafts and edits made while the editor initializes.
        if (field.value.trim()) { filled.add(field); return; }
        if (field.tagName === 'SELECT' && !Array.from(field.options).some(option => option.value === value)) return;
        field.value = value;
        filled.add(field);
        dispatchFieldEvents(field);
      });
      if (data.report) {
        if (!form.querySelector('.lh-snatch-insert')) {
          const insert = document.createElement('button');
          insert.type = 'button';
          insert.className = 'button lh-snatch-insert';
          insert.textContent = 'Bericht einsetzen';
          insert.title = 'Vorbereiteten Bericht in den Editor einsetzen';
          insert.style.marginBottom = '8px';
          insert.addEventListener('click', () => {
            const rich = form.querySelector('.fr-element[contenteditable="true"]');
            const handler = window.XF?.getEditorInContainer?.(form);
            const bb = handler?.ed && handler.isBbCodeView() ? handler.ed.bbCode.getTextArea() : null;
            const existing = bb ? bb.value : rich?.textContent;
            if (String(existing || '').trim() && !window.confirm('Vorhandenen Editorinhalt durch den vorbereiteten Bericht ersetzen?')) return;
            setSnatchlistEditor(form, data.report, true);
          });
          form.querySelector('textarea.js-editor')?.closest('.js-editorWrap')?.before(insert);
          if (!insert.isConnected) form.querySelector('textarea.js-editor')?.before(insert);
        }
        if (!setSnatchlistEditor(form, data.report)) return false;
      }
      if (!ready) return false;
      showLhAutofillNote('Snatchlist Autofill');
      return true;
    };
    if (fill()) return;
    const timer = setInterval(() => {
      attempts++;
      if (fill() || attempts >= 120) clearInterval(timer);
    }, 500);
  }

  function getVisibleLinks(phone, profileUrl, prefillData) {
    const links = [
      ...getResearchLinks(phone),
      {
        label: "WhatsApp",
        shortLabel: "WhatsApp",
        url: buildWhatsappUrl(phone, prefillData.name),
      },
      {
        label: "Snatchlist erstellen",
        shortLabel: "Snatchlist erstellen",
        url: buildSnatchlistUrl(prefillData, phone),
      },
      {
        label: "LH Bericht erstellen",
        shortLabel: "LH Bericht erstellen",
        url: buildLhReportUrl(prefillData),
      },
    ];

    const archiveUrl = buildArchiveUrl(profileUrl);

    if (archiveUrl) {
      links.push({
        label: "Archiv.is",
        shortLabel: "Archiv.is",
        url: archiveUrl,
      });
    }

    return links;
  }

  function addProfileNavigation(box) {
    const row = document.createElement("div");
    row.className = "lh-profile-navigation";
    row.style.cssText = "display:flex;align-items:center;gap:4px";
    const findSource = (selector) =>
      Array.from(document.querySelectorAll(selector)).find(
        (element) => !box.contains(element) && element.getAttribute("href"),
      );
    const entries = [
      [".previous-advert", "←", "Vorherige Anzeige (Shift + ←)"],
      [".backlink-url", "Übersicht", "Zurück zur Übersicht"],
      [".next-advert", "→", "Nächste Anzeige (Shift + →)"],
    ].map(([selector, text, title]) => {
      const link = document.createElement("a");
      link.className = "lh-copy-button";
      link.textContent = text;
      link.title = title;
      link.setAttribute("aria-label", title);
      if (selector !== ".backlink-url")
        link.setAttribute("aria-keyshortcuts", selector === ".previous-advert" ? "Shift+ArrowLeft" : "Shift+ArrowRight");
      link.style.cssText =
        "display:inline-flex;align-items:center;justify-content:center;text-decoration:none;padding:0 5px;height:21px;box-sizing:border-box;white-space:nowrap";
      row.appendChild(link);
      const refresh = () => updateLink(selector, link);
      // Refresh before the site's pointer/click tracking reads navigation data.
      link.addEventListener("pointerdown", refresh);
      link.addEventListener("mousedown", refresh);
      link.addEventListener("focus", refresh);
      link.addEventListener("click", (event) => {
        refresh();
        if (link.getAttribute("aria-disabled") === "true") {
          event.preventDefault();
          return;
        }
        if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || event.button > 0) return;
        // Use the site's actual navigation control so its own click handlers
        // can resolve the next advert and preserve the search context.
        const source = findSource(selector);
        if (source) {
          event.preventDefault();
          event.stopPropagation();
          source.click();
        }
      });
      return { selector, link };
    });
    const setAttribute = (element, name, value) => {
      if (element.getAttribute(name) !== value)
        element.setAttribute(name, value);
    };
    const updateLink = (selector, link) => {
      const source = findSource(selector);
      const href = source?.getAttribute("href");
      const enabled =
        !!href &&
        href !== "#" &&
        source.getAttribute("aria-disabled") !== "true" &&
        !source.closest(".disabled");
      // These attributes carry previous/next search context into the next page.
      const data = new Map(
        Array.from(source?.attributes || [])
          .filter((attribute) => attribute.name.startsWith("data-"))
          .map((attribute) => [attribute.name, attribute.value]),
      );
      Array.from(link.attributes).forEach((attribute) => {
        if (attribute.name.startsWith("data-") && !data.has(attribute.name))
          link.removeAttribute(attribute.name);
      });
      data.forEach((value, name) => setAttribute(link, name, value));
      if (enabled) setAttribute(link, "href", source.href);
      else if (link.hasAttribute("href")) link.removeAttribute("href");
      setAttribute(link, "aria-disabled", String(!enabled));
      link.style.opacity = enabled ? "1" : "0.4";
      link.style.cursor = enabled ? "pointer" : "default";
    };
    box.querySelector(".lh-help").before(row);
    const handledKeys = new Set();
    const consumeKey = (event) => {
      event.preventDefault();
      event.stopImmediatePropagation();
    };
    const onKeyDown = (event) => {
      if (!box.isConnected || !event.shiftKey || !["ArrowLeft", "ArrowRight"].includes(event.key)) return;
      if (handledKeys.has(event.key)) {
        consumeKey(event);
        return;
      }
      if (!box.isConnected || event.defaultPrevented || event.repeat || event.isComposing ||
          !event.shiftKey || event.altKey || event.ctrlKey || event.metaKey ||
          !["ArrowLeft", "ArrowRight"].includes(event.key)) {
        return;
      }
      const editing = [...event.composedPath(), document.activeElement].some((element) =>
        element?.isContentEditable || element?.matches?.("input:not([type='checkbox']):not([type='radio']):not([type='button']):not([type='submit']), textarea, select, [role='textbox'], [role='combobox'], [role='slider'], [role='spinbutton']"));
      if (editing || getVisibleGalleryCloseControl()) {
        return;
      }
      const selector = event.key === "ArrowLeft" ? ".previous-advert" : ".next-advert";
      const { link } = entries.find((entry) => entry.selector === selector);
      updateLink(selector, link);
      if (link.getAttribute("aria-disabled") === "true") {
        return;
      }
      handledKeys.add(event.key);
      consumeKey(event);
      // Finish keyboard dispatch before invoking the same path as a mouse click.
      // Site keyboard listeners must not also navigate for this key press.
      setTimeout(() => {
        if (!box.isConnected) return;
        try {
          // Include tracking events before activating the placeholder link.
          // Omit view: a userscript sandbox's window is not a native Window.
          const mouse = { bubbles: true, cancelable: true, composed: true, button: 0 };
          if (typeof PointerEvent === "function")
            link.dispatchEvent(new PointerEvent("pointerdown", { ...mouse, buttons: 1, pointerId: 1, pointerType: "mouse", isPrimary: true }));
          link.dispatchEvent(new MouseEvent("mousedown", { ...mouse, buttons: 1 }));
          if (typeof PointerEvent === "function")
            link.dispatchEvent(new PointerEvent("pointerup", { ...mouse, buttons: 0, pointerId: 1, pointerType: "mouse", isPrimary: true }));
          link.dispatchEvent(new MouseEvent("mouseup", { ...mouse, buttons: 0 }));
          link.click();
        } catch (error) {
          handledKeys.clear();
          console.warn("Ladies Helper: Tastenkürzel", error);
        }
      }, 0);
    };
    const onKeyUp = (event) => {
      if (handledKeys.delete(event.key)) consumeKey(event);
    };
    const clearHandledKeys = () => handledKeys.clear();
    window.addEventListener("keydown", onKeyDown, true);
    window.addEventListener("keyup", onKeyUp, true);
    window.addEventListener("blur", clearHandledKeys);
    const cleanup = () => {
      observer.disconnect();
      window.removeEventListener("keydown", onKeyDown, true);
      window.removeEventListener("keyup", onKeyUp, true);
      window.removeEventListener("blur", clearHandledKeys);
      clearHandledKeys();
    };
    const update = () => {
      if (!box.isConnected) {
        cleanup();
        return;
      }
      entries.forEach(({ selector, link }) => updateLink(selector, link));
    };
    const observer = new MutationObserver((records) => {
      if (records.some((record) => !box.contains(record.target))) update();
      if (!box.isConnected) cleanup();
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: [
        "href",
        "aria-disabled",
        "class",
        "data-ref-id",
        "data-ref-position",
        "data-next-previous-id",
      ],
    });
    update();
  }

  function renderBox(profileId, advertId, phone, rawPhone, imageLinks) {
    const profileUrl = profileId
      ? CONFIG.profileBaseUrl + encodeURIComponent(profileId)
      : null;
    const prefillData = getPrefillData(profileUrl, phone, rawPhone);
    const links = phone ? getVisibleLinks(phone, profileUrl, prefillData) : [];

    let html = `
      <div class="lh-header">
        <div class="lh-header-text">
          <span class="lh-label">The Ladies Helper – Profil <small style="font-size:10px;font-weight:normal">v4.4.43</small></span>
          ${
            profileUrl
              ? `
                <div><a href="${escapeHtml(profileUrl)}" target="_blank" rel="noopener noreferrer">
                  ${escapeHtml(profileUrl)}
                </a>${phone ? ` <span class="lh-separator">|</span> ${createTextLink("WhatsApp", links[2].url)}` : ""}</div>
              `
              : (phone ? `<div>${createTextLink("WhatsApp", links[2].url)}</div>` : '')
          }
        </div>
        <div class="lh-actions">
          <button type="button" class="lh-copy-button lh-gallery-close" hidden title="Galerie schließen">Galerie schließen</button>
          <a class="lh-help" href="https://sleazyfork.org/de/scripts/602072-the-ladies-helper" target="_blank" rel="noopener noreferrer" title="Hilfe und Updates auf Sleazy Fork" aria-label="Hilfe und Updates auf Sleazy Fork">?</a>
          <button type="button" class="lh-minimize" title="minimieren">−</button>
          <button type="button" class="lh-close" title="schließen">×</button>
        </div>
      </div>

      <div class="lh-content">
    `;

    if (phone) {

      html += `
        <div class="lh-section lh-intim-ratings">
          <span class="lh-label">${createTextLink("intim.to – Profile zur Nummer", links[1].url)}</span>
          <span class="lh-separator">|</span>
          ${createTextLink("Supporter werden", "https://intim.to/de/supporter/")}
          <div class="lh-intim-controls" style="display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap">
            <label><input type="checkbox" class="lh-intim-auto"> intim.to Bewertung automatisch laden</label>
            <button type="button" class="lh-copy-button lh-intim-load" title="intim.to Bewertungen laden oder aktualisieren">Laden</button>
          </div>
          <div class="lh-intim-status" role="status">Automatisches Laden ist deaktiviert.</div>
        </div>
        <div class="lh-section">
          ${
            phone.displayWarning
              ? `<div class="lh-warning">${escapeHtml(phone.displayWarning)}<br>${escapeHtml(phone.raw)}</div>`
              : ""
          }
          ${
            phone.countryCodeInfo && phone.type !== "international_or_unknown"
              ? `<div><span class="lh-label">Ländervorwahl:</span> ${escapeHtml(phone.countryCodeInfo.prefix)} ${escapeHtml(phone.countryCodeInfo.country)}</div>`
              : ""
          }
          <div class="lh-formats">
            <div><span class="lh-label">Rufnummern:</span> ${createTextLink("Suche alle (Google)", links[0].url)}</div>
            <div class="lh-number-grid">
            ${phone.all
              .map(
                (number) => `
                  <div class="lh-number-row">
                    <span class="lh-number-text">${escapeHtml(number)}</span>
                    <span class="lh-number-links">
                      <a href="${escapeHtml(buildSingleGoogleUrl(number))}" target="_blank" rel="noopener noreferrer" title="Google nur diese Nummer">[G]</a>
                      <a href="${escapeHtml(buildSingleIntimUrl(number))}" target="_blank" rel="noopener noreferrer" title="intim.to nur diese Nummer">[I]</a>
                    </span>
                  </div>
                `,
              )
              .join("")}
          </div>
          </div>
        </div>

        <div class="lh-section lh-links">
          ${
            imageLinks.length
              ? `
                <div>
<span class="lh-label">Google Lens Bilder:</span>
${createInlineLinkRow(imageLinks)}
                </div>
              `
              : ""
          }
          <div>
            ${createTextLink("Snatchlist erstellen", links[3].url)}
            <span class="lh-separator">|</span>
            ${createTextLink("LH Bericht erstellen", links[4].url)}
			${
        links[5]
          ? `<span class="lh-separator">|</span>${createTextLink("Archiv.is erstellen", links[5].url)}`
          : ""
      }
          </div>
          <div class="lh-mini-buttons">
            <button type="button" class="lh-copy-button lh-copy-markdown">markdown</button>
            <button type="button" class="lh-copy-button lh-copy-bb">BB</button>
            ${imageLinks.length ? '<button type="button" class="lh-copy-button lh-download-all-pictures">Download aller Bilder</button>' : ''}
          </div>
        </div>
      `;
    } else if (rawPhone) {
      html += `
        <div class="lh-section lh-warning">
          Rufnummer gefunden, aber nicht verarbeitet:<br>
          ${escapeHtml(rawPhone)}
        </div>
      `;
    }

    html += "</div>";

    const existingBox = document.getElementById(CONFIG.boxId);
    if (existingBox) {
      existingBox.remove();
    }

    const box = document.createElement("div");

    box.id = CONFIG.boxId;
    box.innerHTML = html;

    document.body.appendChild(box);
    addLeanToggle(box);
    addImageHoverPreviews(box);
    addProfileNavigation(box);
    initIntimRatings(box, phone);

    const galleryCloseButton = box.querySelector(".lh-gallery-close");
    galleryCloseButton?.addEventListener("click", closeVisibleGallery);

    const updateGalleryCloseButton = () => {
      if (!box.isConnected) {
        galleryObserver.disconnect();
        return;
      }
      const hidden = !getVisibleGalleryCloseControl();
      if (galleryCloseButton.hidden !== hidden)
        galleryCloseButton.hidden = hidden;
    };
    const galleryObserver = new MutationObserver(updateGalleryCloseButton);
    galleryObserver.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["class", "style", "hidden", "aria-hidden"],
    });
    updateGalleryCloseButton();

    const minimizeButton = box.querySelector(".lh-minimize");
    const closeButton = box.querySelector(".lh-close");

    closeButton?.addEventListener("click", () => {
      box.remove();
    });

    minimizeButton?.addEventListener("click", () => {
      box.classList.toggle("lh-collapsed");

      if (box.classList.contains("lh-collapsed")) {
        minimizeButton.textContent = "+";
        minimizeButton.title = "anzeigen";
      } else {
        minimizeButton.textContent = "−";
        minimizeButton.title = "minimieren";
      }
    });

    if (phone) {
      const copyData = getCopyData(profileUrl, phone);
      const markdownButton = box.querySelector(".lh-copy-markdown");
      const bbButton = box.querySelector(".lh-copy-bb");

      markdownButton?.addEventListener("click", () => {
        copyToClipboard(buildMarkdownText(copyData), markdownButton);
      });

      bbButton?.addEventListener("click", () => {
        copyToClipboard(buildBbCodeText(copyData), bbButton);
      });
    }

    const downloadAllPicturesLink = box.querySelector(
      ".lh-download-all-pictures",
    );

    downloadAllPicturesLink?.addEventListener("click", (event) => {
      event.preventDefault();
      downloadAllAdvertPictures(imageLinks, downloadAllPicturesLink);
    });

    console.log("Ladies Helper angezeigt:", {
      profileId,
      profileUrl,
      advertId,
      rawPhone,
      phone,
      prefillData,
      imageLinks,
    });
  }

  function showLadiesBox() {
    hideLiveCamCards();

    if (document.getElementById(CONFIG.boxId)) {
      return true;
    }

    const profileId = getProfileId();
    const advertId = getAdvertId();
    const rawPhone = getRawPhone();
    const phone = normalizeAnyPhone(rawPhone);
    const imageLinks = getAdvertImageLinks(advertId);

    if (!profileId && !phone && !rawPhone && !advertId) {
      console.log(
        "Ladies Helper: Keine Profil-ID, keine Anzeigen-ID und keine Rufnummer gefunden.",
      );
      return false;
    }

    addStyle();
    renderBox(profileId, advertId, phone, rawPhone, imageLinks);

    return true;
  }

  function readLhParams() {
    const params = new URLSearchParams(window.location.search);
    const fragment = new URLSearchParams(window.location.hash.slice(1));
    fragment.forEach((value, key) => {
      if (key.startsWith('lh_')) params.set(key, value);
    });

    return {
      name: params.get("lh_name") || "",
      profile: params.get("lh_profile") || "",
      whatsapp: params.get("lh_whatsapp") || "",
      phone: params.get("lh_phone") || "",
      city: params.get("lh_city") || "",
      state: params.get("lh_state") || "",
      country: params.get("lh_country") || "",
      shortdesc: params.get("lh_shortdesc") || "",
      report: params.get("lh_report") || "",
      source: params.get("lh_source") || "",
      adverttext: params.get("lh_adverttext") || "",
      attributes: params.get("lh_attributes") || "",
    };
  }

  const lhAutofilledFields = new WeakSet();

  function fillLhForm() {
    const data = readLhParams();
    if (!data.name && !data.profile && !data.phone && !data.city) return true;

    // A login page can also contain vbform. Identify the actual report form
    // by its fields and wait for delayed fields instead of claiming success.
    const firstField = document.querySelector(FORM_SELECTORS.name) ||
      document.querySelector(FORM_SELECTORS.profile) ||
      document.querySelector(FORM_SELECTORS.report);
    const form = firstField?.closest('form');
    if (!form) return false;

    const wysiwygMode = form.querySelector(FORM_SELECTORS.wysiwygMode);
    if (wysiwygMode && !lhAutofilledFields.has(wysiwygMode)) {
      wysiwygMode.value = "0";
      dispatchFieldEvents(wysiwygMode);
      lhAutofilledFields.add(wysiwygMode);
    }
    let ready = true;
    let filled = 0;
    for (const key of ['name', 'profile', 'whatsapp', 'phone', 'city', 'state', 'country', 'shortdesc', 'report']) {
      const value = data[key];
      if (value == null || value === '') continue;
      const field = form.querySelector(FORM_SELECTORS[key]);
      if (!field) { ready = false; continue; }
      if (lhAutofilledFields.has(field)) { filled++; continue; }
      if (field.tagName === 'SELECT' &&
          !Array.from(field.options).some(option => option.value === String(value))) {
        ready = false;
        continue;
      }
      field.value = String(value);
      // Mark first: event handlers may trigger observers synchronously.
      lhAutofilledFields.add(field);
      dispatchFieldEvents(field);
      filled++;
    }
    if (!ready || !filled) return false;
    showLhAutofillNote();
    return true;
  }

  function showLhAutofillNote(label = "LH Autofill") {
    addStyle();

    if (document.getElementById(CONFIG.noteId)) return;

    const note = document.createElement("div");

    note.id = CONFIG.noteId;
    note.innerHTML = `
      <strong>${escapeHtml(label)}:</strong> Formular vorausgefüllt.<br>
      Bitte prüfen und manuell absenden.
      <button type="button" id="lh-autofill-note-close" style="margin-left:6px;">×</button>
    `;

    document.body.appendChild(note);

    document
      .getElementById("lh-autofill-note-close")
      ?.addEventListener("click", () => {
        note.remove();
      });

    setTimeout(() => {
      note.remove();
    }, LIMITS.noteAutoCloseMs);
  }

  function observeUntil(callback, retryDelays = []) {
    let done = false;
    let running = false;
    let observer = null;
    const timers = new Set();
    const attempt = () => {
      if (done || running) return;
      running = true;
      try {
        if (callback()) {
          done = true;
          observer?.disconnect();
          timers.forEach(timer => clearTimeout(timer));
          timers.clear();
        }
      } finally {
        running = false;
      }
    };
    attempt();
    if (done) return;
    observer = new MutationObserver(attempt);
    observer.observe(document.documentElement, {childList: true, subtree: true, attributes: true});
    retryDelays.forEach(delay => {
      const timer = setTimeout(() => { timers.delete(timer); attempt(); }, delay);
      timers.add(timer);
    });
  }

  function getListingRadius(href = location.href) {
    const value = new URL(href).searchParams.get("umkreis");
    return value !== null && /^\d+$/.test(value) ? value : "10";
  }

  function createListingLoader(
    status,
    onStateChange,
    onRadiusMismatch = () => {},
  ) {
    // Freeze the search and explicitly include its radius on every request.
    const listingUrl = new URL(location.href);
    listingUrl.searchParams.set("umkreis", getListingRadius(listingUrl.href));
    let total = 1; // The results API supplies the radius-specific page count.
    let initialized = false;
    const fallbackGroup = document
      .querySelector("div.ladies-card a.card-link")
      ?.closest("div.ladies-card")?.parentElement;
    const completed = new Set();
    const seen = new Set();
    const pagers = Array.from(
      document.querySelectorAll(".pagination-container"),
    ).map((element) => ({
      element,
      hidden: element.hidden,
      display: element.style.display,
    }));
    let controller = null;
    let task = null;
    let radiusOffer = null;
    const notify = () =>
      onStateChange({
        running: !!task,
        done: initialized && completed.size >= total,
        offeredRadius: radiusOffer,
      });
    const showNavigation = () =>
      pagers.forEach(({ element, hidden, display }) => {
        element.hidden = hidden;
        element.style.display = display;
      });
    const run = async (signal) => {
      try {
        for (let page = 1; page <= total; page++) {
          if (completed.has(page)) continue;
          signal.throwIfAborted();
          status.textContent =
            "Alle Anzeigen laden: " +
            completed.size +
            " / " +
            total +
            " Seiten …";
          const url = new URL(listingUrl.href);
          url.pathname = "/api" + url.pathname;
          url.searchParams.set("page", String(page));
          // The site's session keeps the largest searched radius. Public requests
          // without session cookies let the selected radius decrease again.
          const response = await fetch(url.href, {
            credentials: "omit",
            cache: "no-store",
            signal,
          });
          if (!response.ok) throw new Error("HTTP " + response.status);
          const data = await response.json();
          signal.throwIfAborted();
          const requestedRadius = Number(
            listingUrl.searchParams.get("umkreis"),
          );
          if (
            data.distanceSearched == null ||
            Number(data.distanceSearched) !== requestedRadius
          ) {
            const offeredRadius = Number(data.distanceSearched);
            if (
              data.distanceSearched != null &&
              Number.isInteger(offeredRadius) &&
              offeredRadius >= 0
            ) {
              radiusOffer = offeredRadius;
              onRadiusMismatch(offeredRadius);
            }
            throw new Error(
              "Angefordert: " +
                requestedRadius +
                " km; Server liefert: " +
                data.distanceSearched +
                " km. Laden gestoppt.",
            );
          }
          if (
            typeof data.content !== "string" ||
            !Number.isInteger(data.pagination?.totalPages) ||
            data.pagination.currentPage !== page
          )
            throw new Error("Ungültige Suchantwort.");
          total = Math.max(1, data.pagination.totalPages);
          const doc = new DOMParser().parseFromString(
            data.content,
            "text/html",
          );
          const cards = Array.from(
            doc.querySelectorAll("div.ladies-card"),
          ).filter((card) => card.querySelector("a.card-link[href]"));
          if (!cards.length && Number(data.advertCount) !== 0)
            throw new Error("Keine Anzeigen auf Seite " + page);
          if (!initialized) {
            // Discard the initial HTML results: they may use a different radius.
            document
              .querySelectorAll("div.ladies-card")
              .forEach((card) => card.remove());
            initialized = true;
            const actualRadius = document.querySelector("#actual-umkreis");
            if (actualRadius)
              actualRadius.textContent =
                requestedRadius === 0 ? "ohne" : requestedRadius + " km";
            const resultInfo = document.querySelector(
              ".advert-results-found-info",
            );
            if (resultInfo && typeof data.advertResultsFoundInfo === "string") {
              resultInfo.innerHTML = data.advertResultsFoundInfo;
            }
          }
          for (const card of cards) {
            const href = new URL(
              card.querySelector("a.card-link[href]").getAttribute("href"),
              url.href,
            );
            if (href.pathname.includes("/directre/") || seen.has(href.pathname))
              continue;
            seen.add(href.pathname);
            const group =
              Array.from(document.querySelectorAll(".card-list")).find(
                (element) => element.className === card.parentElement.className,
              ) || fallbackGroup;
            const imported = document.importNode(card, true);
            imported
              .querySelectorAll("script")
              .forEach((element) => element.remove());
            imported.querySelectorAll("img").forEach((img) => {
              const src =
                img.getAttribute("data-src") ||
                img.getAttribute("data-original") ||
                img.getAttribute("src");
              if (src) img.src = new URL(src, url.href).href;
              img.loading = "lazy";
            });
            group.appendChild(imported);
          }
          completed.add(page);
        }
        hideLiveCamCards();
        pagers.forEach(({ element }) => {
          element.hidden = true;
          element.style.display = "none";
        });
        status.textContent = "Alle " + total + " Seiten geladen.";
      } catch (error) {
        showNavigation();
        status.textContent =
          (signal.aborted ? "Laden gestoppt" : "Laden unterbrochen") +
          " (" +
          completed.size +
          " / " +
          total +
          " Seiten).";
        if (!signal.aborted) {
          status.textContent += " " + error.message;
          console.warn("Ladies Helper: vollständige Ansicht", error);
        }
      }
    };
    const resume = () => {
      if (task) return task;
      radiusOffer = null;
      onRadiusMismatch(null);
      controller = new AbortController();
      task = Promise.resolve()
        .then(() => run(controller.signal))
        .finally(() => {
          task = null;
          controller = null;
          notify();
        });
      notify();
      return task;
    };
    const stop = () => {
      controller?.abort();
    };
    const restart = async () => {
      stop();
      if (task) await task;
      // Keep the current view until a valid replacement page has arrived.
      // The first successful response clears the old cards in run().
      completed.clear();
      seen.clear();
      initialized = false;
      total = 1;
      showNavigation();
      return resume();
    };
    notify();
    const acceptRadius = async (value) => {
      if (!Number.isInteger(value) || value < 0) return;
      stop();
      if (task) await task;
      listingUrl.searchParams.set("umkreis", String(value));
      listingUrl.searchParams.set("page", "1");
      listingUrl.hash = "";
      history.replaceState(history.state, "", listingUrl.href);
      return restart();
    };
    return { resume, stop, restart, acceptRadius };
  }

  function buildRadiusUrl(value, href = location.href) {
    const url = new URL(href);
    url.searchParams.set("umkreis", String(value));
    url.searchParams.set("page", "1");
    url.hash = "";
    return url.href;
  }

  function readAutoLoadPreference() {
    try {
      return localStorage.getItem("ladies-helper-auto-load") !== "false";
    } catch {
      return true;
    }
  }

  function renderRadiusOffer(status, radius, accept) {
    status.parentElement.querySelector(".lh-radius-offer")?.remove();
    if (!Number.isInteger(radius) || radius < 0) return;
    const row = document.createElement("div");
    row.className = "lh-radius-offer";
    row.style.setProperty("display", "block", "important");
    row.style.marginTop = "6px";
    const button = document.createElement("button");
    button.type = "button";
    button.className = "lh-copy-button lh-accept-radius";
    button.textContent = radius + " km laden?";
    button.title = "Umkreis auf " + radius + " km setzen und laden";
    button.setAttribute("aria-label", button.title);
    button.style.setProperty("display", "inline-block", "important");
    button.addEventListener("click", () => {
      button.disabled = true;
      accept(radius);
    });
    row.appendChild(button);
    status.after(row);
  }

  function buildListingViewUrl(listView, href = location.href) {
    const url = new URL(href);
    url.searchParams.set("ansicht", listView ? "list" : "gallery");
    url.searchParams.set("page", "1");
    url.hash = "";
    return url.href;
  }

  function showListingBox() {
    if (document.getElementById(CONFIG.boxId)) return true;
    if (!document.querySelector("div.ladies-card a.card-link")) return false;
    addStyle();
    const box = document.createElement("div");
    box.id = CONFIG.boxId;
    const citySlug = location.pathname.split("/").filter(Boolean)[0];
    const city = citySlug !== "sex-anzeigen" ? decodeSlug(citySlug) : "";
    const radiusValues = [
      "0",
      "10",
      "20",
      "30",
      "50",
      "70",
      "100",
      "150",
      "200",
      "300",
    ];
    const currentRadius = getListingRadius();
    const requestedView = new URL(location.href).searchParams.get("ansicht");
    const listView = requestedView ? requestedView === "list" :
      !!document.querySelector(".card-list.azm-type-list");
    // Preserve a radius supplied in the URL even if the site adds another option.
    if (!radiusValues.includes(currentRadius)) radiusValues.push(currentRadius);
    box.innerHTML = `
      <div class="lh-header">
        <div class="lh-header-text"><span class="lh-label">The Ladies Helper – Übersicht <small style="font-size:10px;font-weight:normal">v4.4.43</small></span>
          ${city ? "<div>" + escapeHtml(city) + "</div>" : ""}
        </div>
        <div class="lh-actions">
          <a class="lh-help" href="https://sleazyfork.org/de/scripts/602072-the-ladies-helper" target="_blank" rel="noopener noreferrer" title="Hilfe und Updates auf Sleazy Fork" aria-label="Hilfe und Updates auf Sleazy Fork">?</a>
          <button type="button" class="lh-minimize" title="minimieren">−</button>
          <button type="button" class="lh-close" title="Helper schließen">×</button>
        </div>
      </div>
      <div class="lh-content">
        <div class="lh-section">
          <label><span class="lh-label">Umkreis:</span>
            <select class="lh-radius" aria-label="Umkreis ändern">
              ${radiusValues
                .map(
                  (value) =>
                    '<option value="' +
                    escapeHtml(value) +
                    '"' +
                    (value === currentRadius ? " selected" : "") +
                    ">" +
                    (value === "0" ? "ohne" : escapeHtml(value) + " km") +
                    "</option>",
                )
                .join("")}
            </select>
          </label>
        </div>
        <div class="lh-section">
          <label style="display:inline-flex;align-items:center;gap:7px;cursor:pointer" title="Zwischen Kacheln und Liste wechseln">
            <span style="font-weight:${listView ? 'normal' : 'bold'}">Kacheln</span>
            <input type="checkbox" role="switch" class="lh-view-toggle" aria-label="Listenansicht" ${listView ? 'checked' : ''}>
            <span style="font-weight:${listView ? 'bold' : 'normal'}">Liste</span>
          </label>
        </div>
        <div class="lh-listing-status" role="status">Anzeigen werden geladen …</div>
        <div class="lh-section"><label>
          <input type="checkbox" class="lh-auto-load" ${readAutoLoadPreference() ? "checked" : ""}>
          Alle Seiten automatisch laden
        </label></div>
        <div class="lh-mini-buttons">
          <button type="button" class="lh-copy-button lh-load-stop" title="Laden stoppen" aria-label="Laden stoppen">■ Stop</button>
          <button type="button" class="lh-copy-button lh-load-listing" title="Laden starten oder fortsetzen" aria-label="Laden fortsetzen">▶ Fortsetzen</button>
          <button type="button" class="lh-copy-button lh-load-restart" title="Alle Seiten von vorne neu laden" aria-label="Alle Seiten neu laden">↻ Neustart</button>
        </div>
      </div>`;
    document.body.appendChild(box);
    addLeanToggle(box);
    box
      .querySelector(".lh-close")
      .addEventListener("click", () => box.remove());
    const minimize = box.querySelector(".lh-minimize");
    minimize.addEventListener("click", () => {
      const collapsed = box.classList.toggle("lh-collapsed");
      minimize.textContent = collapsed ? "+" : "−";
      minimize.title = collapsed ? "anzeigen" : "minimieren";
    });
    box.querySelector(".lh-radius").addEventListener("change", (event) => {
      location.assign(buildRadiusUrl(event.target.value));
    });
    box.querySelector(".lh-view-toggle").addEventListener("change", (event) => {
      location.assign(buildListingViewUrl(event.target.checked));
    });
    const button = box.querySelector(".lh-load-listing");
    const stopButton = box.querySelector(".lh-load-stop");
    const restartButton = box.querySelector(".lh-load-restart");
    const autoLoad = box.querySelector(".lh-auto-load");
    const status = box.querySelector(".lh-listing-status");

    let restarting = false;
    const loader = createListingLoader(
      status,
      ({ running, done, offeredRadius }) => {
        stopButton.hidden = !running;
        button.hidden = running;
        button.disabled = done;
        button.textContent = done
          ? "✓ Vollständig geladen"
          : "▶ Laden / Fortsetzen";
        renderRadiusOffer(
          status,
          !running ? offeredRadius : null,
          async (radius) => {
            const select = box.querySelector(".lh-radius");
            if (
              !Array.from(select.options).some(
                (option) => option.value === String(radius),
              )
            ) {
              const option = document.createElement("option");
              option.value = String(radius);
              option.textContent = radius + " km";
              select.appendChild(option);
            }
            select.value = String(radius);
            await loader.acceptRadius(radius);
          },
        );
      },
    );
    button.addEventListener("click", () => loader.resume());
    stopButton.addEventListener("click", () => loader.stop());
    restartButton.addEventListener("click", async () => {
      if (restarting) return;
      restarting = true;
      restartButton.disabled = true;
      try {
        await loader.restart();
      } finally {
        restarting = false;
        restartButton.disabled = false;
      }
    });
    autoLoad.addEventListener("change", () => {
      try {
        localStorage.setItem(
          "ladies-helper-auto-load",
          String(autoLoad.checked),
        );
      } catch {
        /* Keep the choice on this page if storage is unavailable. */
      }
      if (autoLoad.checked) loader.resume();
      else loader.stop();
    });
    box
      .querySelector(".lh-close")
      .addEventListener("click", () => loader.stop());
    if (autoLoad.checked) loader.resume();
    else status.textContent = "Automatisches Laden ist deaktiviert.";
    return true;
  }

  function initLadiesPage() {
    if (!getAdvertId()) {
      const listingUrl = new URL(location.href);
      const radius = listingUrl.searchParams.get("umkreis");
      if (radius === null || !/^\d+$/.test(radius)) {
        // Reload first so page counts and initial cards also use the 10 km default.
        location.replace(buildRadiusUrl("10"));
        return;
      }
      observeUntil(() => {
        hideLiveCamCards();
        return showListingBox();
      }, [1000, 3000, 5000, 8000]);
      return;
    }
    observeUntil(() => {
      hideLiveCamCards();
      return showLadiesBox();
    }, [1000, 3000, 5000, 8000]);
  }

  function initLhFormPage() {
    const current = new URL(location.href);
    const legacyKeys = Array.from(current.searchParams.keys()).filter(key => key.startsWith('lh_'));
    if (legacyKeys.length) {
      const fragment = new URLSearchParams(current.hash.slice(1));
      legacyKeys.forEach(key => {
        if (!fragment.has(key)) fragment.set(key, current.searchParams.get(key));
        current.searchParams.delete(key);
      });
      current.hash = fragment.toString();
      // Reload old links without sending the helper payload to the forum.
      location.replace(current.href);
      return;
    }
    observeUntil(() => {
      const frame = document.querySelector('frameset frame');
      const payload = new URLSearchParams(location.hash.slice(1));
      if (frame && payload.has('lh_name') && !payload.has('lh_in_frame')) {
        const destination = new URL(CONFIG.lhReportBaseUrl);
        payload.set('lh_in_frame', '1');
        destination.hash = payload.toString();
        frame.src = destination.href;
        return true;
      }
      return fillLhForm();
    }, [1000, 3000, 5000]);
  }

  if (location.hostname.includes("ladies.de")) {
    initLeanMode();
    initLadiesPage();
  }

  if (["snatchlist.com", "www.snatchlist.com"].includes(location.hostname)) {
    initSnatchlistForm();
  }

  if (location.hostname === "huren-test-forum.lusthaus.cc") {
    initLhFormPage();
  }
})();