☰

The Ladies Helper

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==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();
  }
})();