Загрузка данных


// ==UserScript==
// @name         Intim Mirrors - Block Images
// @namespace    https://a.intimcity.promo/
// @version      3.7.0
// @description  Fast image/contact blocking and risky-word censoring for intim mirrors
// @author       You
// @match        *://*/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const STORAGE_KEY = "tm_intim_block_images_enabled";
  const WORST_SORT_STORAGE_KEY = "tm_intim_sort_worst_reviews_enabled";
  const TOGGLE_ID = "tm-image-toggle-btn";
  const WORST_SORT_TOGGLE_ID = "tm-worst-review-sort-btn";
  const STYLE_ID = "tm-block-images-style";
  const CONTACT_STYLE_ID = "tm-hide-contacts-style";
  const SEARCH_STYLE_ID = "tm-search-visibility-style";
  const VISITED_STYLE_ID = "tm-visited-profile-style";
  const CONTACT_KEEP_SELECTOR = "#headerBg, #headerBg *, .main_link1, .main_link1 *, #search, #search *";
  const USERINFO_KEEP_SELECTOR = ".userinfo-link, .userinfo, [itemprop='author']";
  const SANITIZED_TEXT_FIELD_SELECTOR =
    "textarea, input[readonly][type='text'], input[readonly][type='search'], input[readonly]:not([type])";
  const IMAGE_KEEP_ROOT_SELECTOR = "#headerBg, .main_link1, .up-ocenki-basic, .up-ocenki-dop";
  const COMPARE_TRIGGER_SELECTOR =
    '[class*="sravn"], [id*="sravn"], [class*="compare"], [id*="compare"], [onclick*="sravn"], [onclick*="compare"]';
  const HIDDEN_CONTACT_ATTR = "data-tm-contact-hidden";
  const HOST_RE = /intim/i; // Works for changing mirrors with "intim" in host.
  const CONTACT_LINK_SELECTOR = [
    'a[href^="tel:"]',
    'a[href^="callto:"]',
    'a[href^="sms:"]',
    'a[href*="wa.me/"]',
    'a[href*="api.whatsapp.com"]',
    'a[href*="whatsapp"]',
    'a[href*="viber"]',
    'a[href*="tg://"]',
    'a[href*="t.me/"]',
    '[class*="phone"]',
    '[class*="whatsapp"]',
    '[class*="viber"]',
    '[class*="telegram"]',
    '[id*="phone"]',
    '[id*="whatsapp"]',
    '[id*="viber"]',
    '[id*="telegram"]',
    "[data-phone]",
    "[data-tel]",
    "[data-whatsapp]",
    "[data-viber]",
    "[data-telegram]",
  ].join(", ");
  const CONTACT_ATTR_SCAN_SELECTOR =
    "[href], [onclick], [title], [aria-label], [data-phone], [data-tel], [data-whatsapp], [data-viber], [data-telegram]";
  const CONTACT_ATTR_NAME_RE = /(?:^|[-_:.])(phone|tel|whatsapp|viber|telegram|tg)(?:[-_:.]|$)/i;
  const CONTACT_VALUE_RE =
    /(tel:|callto:|sms:|wa\.me|api\.whatsapp\.com|whatsapp|viber|tg:\/\/|t\.me\/|telegram|\bтелефон\b|\bномер\b|\bphone\b|\btel\b)/i;
  const IMAGE_ATTR_FILTER = ["src", "srcset", "poster", "style", "data-src", "data-srcset"];
  const CONTACT_ATTR_FILTER = [
    "href",
    "onclick",
    "title",
    "aria-label",
    "data-phone",
    "data-tel",
    "data-whatsapp",
    "data-viber",
    "data-telegram",
  ];
  const PROFILE_CARD_SELECTOR =
    "[class*='anket'], [class*='profile'], [class*='girl'], [class*='card'], [class*='item'], article, li, tr";
  const PROFILE_LINK_CUE_RE = /(anket|anketa|profile|girl|model|lady|escort|user|person|id=|\/id\/|\/\d{3,}(?:[/?#]|$))/i;
  const PROFILE_LINK_EXCLUDE_RE =
    /(?:\/|[?&])(reviews?|comments?|forum|news|blog|search|login|register|signup|photo|photos|video|videos|contact|contacts|faq|rules?|terms|policy)(?:[/?&#=]|$)/i;
  const REVIEW_CUE_RE = /(отзыв|отзыва|отзывов|reviews?|коммент|рейтинг|оценк|звезд|звёзд|★|☆|\d\s*\/\s*(?:5|10))/i;
  const NO_REVIEW_CUE_RE = /(нет\s+отзыв|без\s+отзыв|отзывов?\s*0|0\s*отзыв)/i;
  const NEGATIVE_REVIEW_WORD_RE =
    /(?<![\p{L}\p{N}_])(?:обман\p{L}*|развод\p{L}*|кидал\p{L}*|фейк\p{L}*|ужас\p{L}*|кошмар\p{L}*|плох\p{L}*|хам\p{L}*|груб\p{L}*|гряз\p{L}*|вон\p{L}*|болез\p{L}*|некомфорт\p{L}*|неприятн\p{L}*|опозд\p{L}*|отмен\p{L}*|дорож\p{L}*|вымаг\p{L}*|не\s+рекоменд\p{L}*|не\s+совет\p{L}*)(?![\p{L}\p{N}_])/giu;
  const POSITIVE_REVIEW_WORD_RE =
    /(?<![\p{L}\p{N}_])(?:отлич\p{L}*|хорош\p{L}*|супер\p{L}*|класс\p{L}*|идеал\p{L}*|чист\p{L}*|вежлив\p{L}*|приятн\p{L}*|рекоменд\p{L}*|совет\p{L}*)(?![\p{L}\p{N}_])/giu;
  const PROFILE_FETCH_TIMEOUT_MS = 15000;
  const PROFILE_FETCH_CONCURRENCY = 4;
  const PROFILE_REVIEW_PAGE_LIMIT = 80;
  const REVIEW_SECTION_SELECTOR =
    "[class*='review'], [id*='review'], [class*='otziv'], [id*='otziv'], [class*='comment'], [id*='comment'], [itemprop*='review'], [itemprop*='comment']";
  const REVIEW_PAGINATION_SELECTOR = "[class*='pag'], [id*='pag'], nav, .pager, .pagination";
  const REVIEW_PAGE_LINK_CUE_RE = /(review|reviews|comment|comments|отзыв|коммент|otziv|feedback)/i;
  const REVIEW_PAGE_NAV_TEXT_RE = /^(?:\d{1,3}|next|prev|далее|назад|след|пред)$/i;
  const COMMENT_AREA_SORT_EXCLUDE_RE = /(comment|comments|review|reviews|отзыв|коммент|otziv)/i;
  const REVIEW_SCORE_ATTR = "data-tm-review-score";
  const VISITED_PROFILE_ATTR = "data-tm-visited-profile";
  const VISITED_DB_NAME = "tm_intim_profile_history_db";
  const VISITED_DB_STORE = "visited_profile_routes";
  const VISITED_DB_VERSION = 1;
  // Max strict mode for short-video platforms.
  // Edit lists only; censor regex is generated automatically.
  // Use "*" at the end to match word forms by stem, e.g. "проститут*" => "проститутка/проститутки".
  const FORBIDDEN_WORDS = {
    adult: [
      "18+",
      "xxx",
      "порн*",
      "porn*",
      "nsfw",
      "эрот*",
      "интим*",
      "адалт*",
      "adult",
      "секс*",
      "cекс*",
      "cек*",
      "sex*",
      "sexy",
      "nude*",
      "nudity",
      "голый",
      "голая",
      "голые",
      "голыш*",
      "обнажен*",
      "обнаж*",
      "фетиш*",
      "bdsm",
      "бдсм",
      "минет*",
      "куни*",
      "аналь*",
      "оральн*",
      "oral*",
      "оргия*",
      "дроч*",
      "мастурб*",
      "онлифанс",
      "onlyfans",
      "only fans",
      "вебкам*",
      "webcam*",
      "camgirl*",
      "cam model*",
      "секс-услуг*",
      "sexwork*",
      "sex work*",
    ],
    escort: [
      "эскорт*",
      "эск##т",
      "escort*",
      "проститут*",
      "проституц*",
      "шлюх*",
      "шалав*",
      "девушка по вызову",
      "девочки по вызову",
      "по вызову",
      "интим услуги",
    ],
    intimacyServices: [
      "классич* секс*",
      "секс класс*",
      "группов* секс*",
      "аналь*",
      "ана##*",
      "минет*",
      "ми##т*",
      "горлов* минет*",
      "кунилингус*",
      "поцелу*",
      "окончание в рот",
      "окончание на лицо",
      "окончание на грудь",
      "член*",
      "глот*",
      "конч*",
      "сквирт*",
      "виртуальн* секс*",
      "секс по телефону",
      "телефонн* секс*",
      "эротическ* массаж*",
      "эро#######*",
      "урологическ* массаж*",
      "лингам*",
      "карсай*",
      "страпон*",
      "анилингус*",
      "золот* дожд*",
      "копро*",
      "фистинг*",
      "фингеринг*",
      "садо-мазо",
      "садо мазо",
      "бандаж*",
      "госпожа*",
      "игры*",
      "легк* доминац*",
      "порк*",
      "рабын*",
      "фетиш*",
      "фе##ш*",
      "футфетиш*",
      "трамплинг*",
      "клизм*",
      "боллбастинг*",
      "ballbusting*",
      "феминизац*",
      "размер имеет значение",
      "услуги семейной паре",
    ],
    violenceSelfHarm: [
      "насили*",
      "изнасил*",
      "убий*",
      "убить",
      "расстрел*",
      "зарез*",
      "задуш*",
      "пытк*",
      "избиени*",
      "жесток*",
      "жесть",
      "мясоруб*",
      "кровав*",
      "труп*",
      "расчлен*",
      "суицид*",
      "самоубий*",
      "самоповрежд*",
      "селфхарм*",
      "selfharm*",
      "self-harm*",
      "suicide*",
      "kill*",
      "murder*",
      "dead body",
      "corpse",
      "повес*",
      "вскрыть вены",
      "резать вены",
      "вены вскры*",
    ],
    drugsAlcoholTobacco: [
      "наркот*",
      "закладк*",
      "закладчик*",
      "драг*",
      "drug*",
      "cocaine",
      "кокаин*",
      "heroin",
      "героин*",
      "meth",
      "метамфет*",
      "амфет*",
      "мефедрон*",
      "mdma",
      "мдма",
      "экстази",
      "ecstasy",
      "спайс*",
      "соль*",
      "марихуан*",
      "конопл*",
      "каннабис*",
      "гашиш*",
      "weed",
      "420",
      "алкогол*",
      "бухл*",
      "бухат*",
      "пьян*",
      "водк*",
      "виски",
      "whiskey",
      "пиво",
      "вино",
      "самогон*",
      "табак*",
      "сигарет*",
      "сигар*",
      "вейп*",
      "vape*",
      "никотин*",
      "smok*",
      "курен*",
    ],
    hateDiscrimination: [
      "расист*",
      "расизм*",
      "нацист*",
      "нацизм*",
      "фаш*",
      "ксенофоб*",
      "дискриминац*",
      "унижен*",
      "оскорбл*",
      "hatespeech",
      "hate speech",
      "slur*",
      "негр*",
      "ниггер*",
      "nigg*",
      "chink*",
      "spic*",
      "kike*",
      "trann*",
      "транн*",
      "fagg*",
      "пидор*",
      "пидр*",
      "гомик*",
      "жидов*",
      "жиден*",
      "хач*",
      "чурк*",
      "blackface",
      "white power",
      "supremac*",
    ],
    crimeScam: [
      "мошенн*",
      "скам*",
      "scam*",
      "обман*",
      "развод*",
      "краж*",
      "грабеж*",
      "угон*",
      "взлом*",
      "hack*",
      "хак*",
      "phishing*",
      "фишинг*",
      "кардинг*",
      "carding*",
      "даркнет*",
      "darknet*",
      "отмыв*",
      "подделк*",
      "фейк док*",
      "подделка документов",
      "вымогат*",
      "шантаж*",
      "террор*",
      "экстрем*",
      "пропаганд* террор",
      "вербовк*",
      "crypto pump",
      "памп*",
      "дамп*",
      "фин пирами*",
      "пирамид*",
      "дропп*",
      "обнал*",
    ],
    weaponsExplosives: [
      "оружи*",
      "огнестрел*",
      "пистолет*",
      "револьвер*",
      "автомат*",
      "штурмовая винтовк*",
      "shotgun*",
      "rifle*",
      "gun*",
      "ammo*",
      "патрон*",
      "нож*",
      "кастет*",
      "гранат*",
      "бомб*",
      "взрыв*",
      "взрывчат*",
      "терракт*",
      "теракт*",
      "стрельб*",
      "поджог*",
    ],
    gamblingBetting: [
      "казино*",
      "casino*",
      "ставк*",
      "бетт*",
      "betting*",
      "букмекер*",
      "лудоман*",
      "gambl*",
      "джекпот*",
      "слот*",
      "рулетк*",
      "тотализатор*",
      "покер*",
    ],
    medicalMisinformation: [
      "чудо-лекарств*",
      "чудо средство",
      "гарантированно лечит",
      "вылечит рак",
      "лечит рак",
      "без врачей",
      "без операций",
      "анорекс*",
      "булими*",
      "быстрое похудение",
      "похудеть за 3 дня",
      "похудеть за неделю на 10",
      "таблетки для похудения",
      "fat burner",
    ],
    minorsAndAbuse: [
      "педоф*",
      "несовершеннолетн*",
      "малолетк*",
      "child porn*",
      "lolicon*",
      "лоликон*",
      "incest*",
      "инцест*",
      "grooming*",
      "груминг*",
    ],
    bloggerEuphemisms: [
      "запрещенк*",
      "деликатная тема",
      "контент 18",
      "клубничк*",
      "клубника",
      "темки 18",
      "серый контент",
      "gray content",
      "грязные деньги",
      "быстрый заработок",
      "легкие деньги",
      "гарантированный доход",
      "100% прибыль",
    ],
    profanityRu: [
      "бля*",
      "бляд*",
      "еб*",
      "ёб*",
      "пизд*",
      "хуй*",
      "хер*",
      "наху*",
      "нахер*",
      "сук*",
      "мраз*",
      "твар*",
      "трах*",
      "гандон*",
      "мудак*",
      "долбоеб*",
      "долбаеб*",
      "ублюд*",
      "уеб*",
      "шмар*",
      "сран*",
      "срать*",
      "сру*",
      "засра*",
      "говн*",
      "петух*",
      "чмо*",
    ],
    profanityEn: [
      "fuck*",
      "shit*",
      "bitch*",
      "asshole*",
      "motherf*",
      "cunt*",
      "whore*",
      "slut*",
      "dick*",
      "cock*",
      "pussy*",
      "bastard*",
      "retard*",
      "wtf",
      "stfu",
    ],
  };
  const FORBIDDEN_WORD_TOKENS = Object.values(FORBIDDEN_WORDS).flat();
  const RISKY_WORD_REGEX = compileForbiddenWordRegex(FORBIDDEN_WORD_TOKENS);
  const TEXT_SANITIZE_HINT_RE = compileSanitizeHintRegex(FORBIDDEN_WORD_TOKENS);
  const OBSERVER_ATTR_FILTER = Array.from(new Set([...IMAGE_ATTR_FILTER, ...CONTACT_ATTR_FILTER]));

  if (!HOST_RE.test(location.hostname)) return;

  const blockingEnabled = localStorage.getItem(STORAGE_KEY) !== "0";
  const worstSortEnabled = localStorage.getItem(WORST_SORT_STORAGE_KEY) !== "0";
  let worstSortScheduled = false;
  let worstSortInFlight = false;
  let worstSortRerunRequested = false;
  let visitedMarkScheduled = false;
  let visitedMarkInFlight = false;
  let visitedMarkRerunRequested = false;
  const profileBadnessScoreCache = new Map();
  const profileBadnessRequestCache = new Map();
  const visitedRouteStateCache = new Map();
  let visitedDbPromise = null;

  const CSS_BLOCK = `
    img,
    picture,
    source,
    svg image,
    video[poster],
    [style*="background-image"]:not(#headerBg):not(#headerBg *):not(.main_link1):not(.main_link1 *):not(.up-ocenki-basic):not(.up-ocenki-basic *):not(.up-ocenki-dop):not(.up-ocenki-dop *) {
      display: none !important;
      visibility: hidden !important;
      opacity: 0 !important;
    }

    *:not(#headerBg):not(#headerBg *):not(.main_link1):not(.main_link1 *):not(.up-ocenki-basic):not(.up-ocenki-basic *):not(.up-ocenki-dop):not(.up-ocenki-dop *) {
      background-image: none !important;
    }

    .up-ocenki-basic img,
    .up-ocenki-dop img {
      display: inline-block !important;
      visibility: visible !important;
      opacity: 1 !important;
    }
  `;

  const CSS_HIDE_CONTACTS = `
    a[href^="tel:"],
    a[href^="callto:"],
    a[href^="sms:"],
    a[href*="wa.me/"],
    a[href*="api.whatsapp.com"],
    a[href*="whatsapp"],
    a[href*="viber"],
    a[href*="tg://"],
    a[href*="t.me/"],
    [data-phone],
    [data-tel],
    [data-whatsapp],
    [data-viber],
    [data-telegram] {
      display: none !important;
      visibility: hidden !important;
      opacity: 0 !important;
    }
  `;

  const CSS_SEARCH_VISIBILITY = `
    #search,
    #search .searchphone,
    #search form {
      display: block !important;
      visibility: visible !important;
      opacity: 1 !important;
    }

    #search #searchStr {
      background: #ffffff !important;
      color: #111111 !important;
      border: 1px solid #777777 !important;
      border-radius: 4px !important;
      padding: 4px 8px !important;
    }

    #search #searchPS {
      display: inline-block !important;
      visibility: visible !important;
      opacity: 1 !important;
      min-width: 74px !important;
      min-height: 28px !important;
      padding: 4px 10px !important;
      border: 1px solid #666666 !important;
      border-radius: 4px !important;
      background: #f2f2f2 !important;
      color: #111111 !important;
      cursor: pointer !important;
      text-indent: 0 !important;
      line-height: 1.2 !important;
    }

    #search #menuRight2 {
      color: #f2f2f2 !important;
    }

    #search #menuRight2 a.menu {
      color: #f2f2f2 !important;
      text-decoration: underline !important;
    }

    #search #menuRight2 a.menu[style*="#CC0000"],
    #search #menuRight2 a.menu[style*="#cc0000"] {
      color: #ff5959 !important;
    }
  `;

  const CSS_VISITED_PROFILES = `
    [${VISITED_PROFILE_ATTR}="1"] {
      position: relative !important;
      outline: 2px solid rgba(42, 130, 67, 0.55) !important;
      outline-offset: -2px !important;
    }

    [${VISITED_PROFILE_ATTR}="1"]::before {
      content: "Посещено!" !important;
      position: absolute !important;
      top: 6px !important;
      left: 6px !important;
      z-index: 2147483000 !important;
      padding: 2px 8px !important;
      border-radius: 999px !important;
      font: 700 11px/1.1 Arial, sans-serif !important;
      color: #ffffff !important;
      background: #21813f !important;
      box-shadow: 0 1px 5px rgba(0, 0, 0, 0.35) !important;
      pointer-events: none !important;
    }
  `;

  function appendNode(node) {
    const root = document.documentElement || document.head || document.body;
    if (root) {
      root.appendChild(node);
      return;
    }

    document.addEventListener(
      "readystatechange",
      () => {
        const nextRoot = document.documentElement || document.head || document.body;
        if (nextRoot && !node.isConnected) nextRoot.appendChild(node);
      },
      { once: true }
    );
  }

  function setBlockingState(enabled) {
    localStorage.setItem(STORAGE_KEY, enabled ? "1" : "0");
  }

  function setWorstSortState(enabled) {
    localStorage.setItem(WORST_SORT_STORAGE_KEY, enabled ? "1" : "0");
  }

  function injectToggleButton() {
    if (document.getElementById(TOGGLE_ID)) return;

    const style = document.createElement("style");
    style.textContent = `
      #${TOGGLE_ID} {
        position: fixed !important;
        right: 12px !important;
        bottom: 12px !important;
        z-index: 2147483647 !important;
        padding: 8px 12px !important;
        border: 1px solid #333 !important;
        border-radius: 8px !important;
        font: 12px/1.2 Arial, sans-serif !important;
        cursor: pointer !important;
        color: #fff !important;
        background: ${blockingEnabled ? "#b00020" : "#0a7a2f"} !important;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35) !important;
      }
      #${TOGGLE_ID}:hover {
        filter: brightness(1.08) !important;
      }
    `;
    appendNode(style);

    const btn = document.createElement("button");
    btn.id = TOGGLE_ID;
    btn.type = "button";
    btn.textContent = blockingEnabled ? "Изображения: OFF" : "Изображения: ON";
    btn.title = "Переключить и перезагрузить";
    btn.addEventListener("click", () => {
      setBlockingState(!blockingEnabled);
      location.reload();
    });

    const mount = () => {
      if (!document.body) return;
      document.body.appendChild(btn);
    };

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

  function injectWorstSortButton() {
    if (document.getElementById(WORST_SORT_TOGGLE_ID)) return;

    const style = document.createElement("style");
    style.textContent = `
      #${WORST_SORT_TOGGLE_ID} {
        position: fixed !important;
        right: 12px !important;
        bottom: 52px !important;
        z-index: 2147483647 !important;
        padding: 8px 12px !important;
        border: 1px solid #333 !important;
        border-radius: 8px !important;
        font: 12px/1.2 Arial, sans-serif !important;
        cursor: pointer !important;
        color: #fff !important;
        background: ${worstSortEnabled ? "#8d1f2a" : "#2f4f6b"} !important;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35) !important;
      }
      #${WORST_SORT_TOGGLE_ID}:hover {
        filter: brightness(1.08) !important;
      }
    `;
    appendNode(style);

    const btn = document.createElement("button");
    btn.id = WORST_SORT_TOGGLE_ID;
    btn.type = "button";
    btn.textContent = worstSortEnabled ? "Сортировка по отзывам: ON" : "Сортировка по отзывам: OFF";
    btn.title = "Переключить сортировку и перезагрузить";
    btn.addEventListener("click", () => {
      setWorstSortState(!worstSortEnabled);
      location.reload();
    });

    const mount = () => {
      if (!document.body) return;
      document.body.appendChild(btn);
    };

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

  function injectBlockCss() {
    if (document.getElementById(STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = CSS_BLOCK;
    appendNode(style);
  }

  function injectContactCss() {
    if (document.getElementById(CONTACT_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = CONTACT_STYLE_ID;
    style.textContent = CSS_HIDE_CONTACTS;
    appendNode(style);
  }

  function injectSearchCss() {
    if (document.getElementById(SEARCH_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = SEARCH_STYLE_ID;
    style.textContent = CSS_SEARCH_VISIBILITY;
    appendNode(style);
  }

  function injectVisitedCss() {
    if (document.getElementById(VISITED_STYLE_ID)) return;
    const style = document.createElement("style");
    style.id = VISITED_STYLE_ID;
    style.textContent = CSS_VISITED_PROFILES;
    appendNode(style);
  }

  function repairSearchUi(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    const btn = root.querySelector("#searchPS");
    if (btn instanceof HTMLInputElement && btn.type === "submit") {
      if (!btn.value || !btn.value.trim()) {
        btn.value = "Найти";
      }
    }
  }

  function normalizeSpaces(value) {
    return (value || "").replace(/\s+/g, " ").trim();
  }

  function parseLocaleFloat(value) {
    if (value === null || value === undefined) return null;
    const normalized = String(value).replace(",", ".").trim();
    const num = Number.parseFloat(normalized);
    return Number.isFinite(num) ? num : null;
  }

  function normalizeRatingToFiveScale(value, scale) {
    if (value === null) return null;
    if (scale === 5) return Math.max(0, Math.min(5, value));
    if (scale === 10) return Math.max(0, Math.min(5, value / 2));
    if (scale === 100) return Math.max(0, Math.min(5, value / 20));
    if (value <= 5) return Math.max(0, value);
    if (value <= 10) return Math.max(0, value / 2);
    if (value <= 100) return Math.max(0, value / 20);
    return null;
  }

  function countRegexMatches(value, regex) {
    if (!value || !regex) return 0;
    regex.lastIndex = 0;
    let count = 0;
    while (regex.exec(value)) {
      count += 1;
      if (count >= 100) break;
    }
    regex.lastIndex = 0;
    return count;
  }

  function extractRatingsFromText(value) {
    const text = value || "";
    const ratings = [];

    const pushRating = (raw, scale) => {
      const parsed = parseLocaleFloat(raw);
      const normalized = normalizeRatingToFiveScale(parsed, scale);
      if (normalized === null) return;
      ratings.push(normalized);
    };

    for (const m of text.matchAll(/(\d{1,2}(?:[.,]\d+)?)\s*\/\s*5\b/giu)) {
      pushRating(m[1], 5);
    }
    for (const m of text.matchAll(/(\d{1,2}(?:[.,]\d+)?)\s*\/\s*10\b/giu)) {
      pushRating(m[1], 10);
    }
    for (const m of text.matchAll(/(\d{1,2}(?:[.,]\d+)?)\s*(?:из|of)\s*5\b/giu)) {
      pushRating(m[1], 5);
    }
    for (const m of text.matchAll(/(\d{1,2}(?:[.,]\d+)?)\s*(?:из|of)\s*10\b/giu)) {
      pushRating(m[1], 10);
    }
    for (const m of text.matchAll(/\b(\d{1,3})\s*%\b/giu)) {
      pushRating(m[1], 100);
    }
    for (const m of text.matchAll(/(?:рейтинг|оценк[аи]?|rating|rate)\s*[:\-]?\s*(\d{1,2}(?:[.,]\d+)?)/giu)) {
      pushRating(m[1], null);
    }
    for (const stars of text.matchAll(/[★☆]{3,5}/g)) {
      const chunk = stars[0] || "";
      const total = chunk.length;
      if (!total) continue;
      const filled = Array.from(chunk).filter((ch) => ch === "★").length;
      ratings.push((filled / total) * 5);
    }

    return ratings;
  }

  function collectCardSignalText(card) {
    if (!(card instanceof Element)) return "";
    const chunks = [];

    const pushChunk = (value) => {
      const next = normalizeSpaces(value || "");
      if (!next) return;
      chunks.push(next);
    };

    const collectAttrs = (el) => {
      if (!(el instanceof Element)) return;
      pushChunk(el.getAttribute("title"));
      pushChunk(el.getAttribute("aria-label"));
      pushChunk(el.getAttribute("alt"));
      pushChunk(el.getAttribute("data-rating"));
      pushChunk(el.getAttribute("data-rate"));
      pushChunk(el.getAttribute("data-stars"));
      pushChunk(el.getAttribute("data-score"));
      pushChunk(el.getAttribute("data-review"));
      pushChunk(el.getAttribute("data-reviews"));
      pushChunk(el.getAttribute("data-otziv"));
      pushChunk(el.getAttribute("data-comments"));
      pushChunk(el.getAttribute("data-comment-count"));
    };

    pushChunk(card.textContent || "");
    collectAttrs(card);

    const attrNodes = card.querySelectorAll(
      "[title], [aria-label], [alt], [data-rating], [data-rate], [data-stars], [data-score], [data-review], [data-reviews], [data-otziv], [data-comments], [data-comment-count]"
    );
    let seen = 0;
    for (const node of attrNodes) {
      collectAttrs(node);
      seen += 1;
      if (seen >= 180) break;
    }

    return normalizeSpaces(chunks.join(" ")).toLowerCase();
  }

  function extractReviewCount(value) {
    const text = value || "";
    let maxCount = null;
    const patterns = [
      /(\d{1,4})\s*(?:отзыв(?:а|ов)?|reviews?|коммент(?:а|ов|ариев)?)/giu,
      /(?:отзыв(?:а|ов)?|reviews?|коммент(?:а|ов|ариев)?)\s*[:\-]?\s*(\d{1,4})/giu,
    ];

    for (const re of patterns) {
      for (const m of text.matchAll(re)) {
        const raw = Number.parseInt(m[1], 10);
        if (!Number.isFinite(raw)) continue;
        if (raw < 0 || raw > 5000) continue;
        maxCount = maxCount === null ? raw : Math.max(maxCount, raw);
      }
    }

    return maxCount;
  }

  function getReviewBadnessScore(card) {
    const text = collectCardSignalText(card);
    if (!text) return Number.NEGATIVE_INFINITY;

    const ratings = extractRatingsFromText(text);
    const reviewCount = extractReviewCount(text);
    const negativeHits = countRegexMatches(text, NEGATIVE_REVIEW_WORD_RE);
    const positiveHits = countRegexMatches(text, POSITIVE_REVIEW_WORD_RE);
    const hasReviewCue = REVIEW_CUE_RE.test(text);

    let score = 0;

    if (ratings.length) {
      const minRating = Math.min(...ratings);
      const avgRating = ratings.reduce((sum, value) => sum + value, 0) / ratings.length;
      score += (5 - minRating) * 14;
      score += (5 - avgRating) * 6;
    } else if (hasReviewCue) {
      score += 2;
    } else {
      score -= 10;
    }

    if (reviewCount !== null) {
      score += Math.min(reviewCount, 200) * 0.05;
    }

    score += negativeHits * 3.5;
    score -= positiveHits * 1.6;

    if (NO_REVIEW_CUE_RE.test(text)) {
      score -= 8;
    }
    if (/не\s+рекоменд|не\s+совет/iu.test(text)) {
      score += 5;
    }

    return score;
  }

  function isLikelyProfileHref(rawHref) {
    const href = String(rawHref || "").trim();
    if (!href || href === "#") return false;
    if (/^(?:javascript:|tel:|mailto:|sms:)/i.test(href)) return false;
    const normalized = href.toLowerCase();
    if (PROFILE_LINK_EXCLUDE_RE.test(normalized)) return false;
    return PROFILE_LINK_CUE_RE.test(normalized);
  }

  function isLikelyProfileLink(link) {
    if (!(link instanceof HTMLAnchorElement)) return false;
    if (!isLikelyProfileHref(link.href || link.getAttribute("href") || "")) return false;
    if (link.closest("#headerBg, #search, footer, nav")) return false;
    return true;
  }

  function isValidProfileCardCandidate(card) {
    if (!(card instanceof Element)) return false;
    if (!card.parentElement) return false;
    if (card.closest("#headerBg, #search, .main_link1, footer, nav")) return false;
    if (isInsideContactKeepArea(card) || isUserInfoElement(card)) return false;

    const text = normalizeSpaces(card.textContent || "");
    if (text.length < 20 || text.length > 7000) return false;
    return true;
  }

  function findCardBySiblingPattern(link) {
    if (!(link instanceof Element)) return null;
    let current = link.parentElement;
    let depth = 0;

    while (current && depth < 8) {
      const parent = current.parentElement;
      if (!(parent instanceof Element)) break;
      if (parent.closest("#headerBg, #search, .main_link1, footer, nav")) break;

      const siblings = Array.from(parent.children);
      if (siblings.length >= 3 && siblings.length <= 220) {
        let profileSiblings = 0;
        for (const sibling of siblings) {
          if (!(sibling instanceof Element)) continue;
          let linksChecked = 0;
          let hasProfileLink = false;
          for (const nestedLink of sibling.querySelectorAll("a[href]")) {
            if (isLikelyProfileLink(nestedLink)) {
              hasProfileLink = true;
              break;
            }
            linksChecked += 1;
            if (linksChecked >= 6) break;
          }
          if (!hasProfileLink) continue;
          profileSiblings += 1;
          if (profileSiblings >= 3) break;
        }

        if (profileSiblings >= 3 && isValidProfileCardCandidate(current)) {
          return current;
        }
      }

      current = parent;
      depth += 1;
    }

    return null;
  }

  function findProfileCardForLink(link) {
    const selectorCard = link.closest(PROFILE_CARD_SELECTOR);
    if (isValidProfileCardCandidate(selectorCard)) return selectorCard;

    return findCardBySiblingPattern(link);
  }

  function normalizeProfileUrl(rawUrl) {
    if (!rawUrl) return "";
    try {
      const normalized = new URL(rawUrl, location.href);
      normalized.hash = "";
      return normalized.href;
    } catch {
      return "";
    }
  }

  function buildNormalizedQueryString(searchParams) {
    const filtered = [];
    for (const [name, value] of searchParams.entries()) {
      if (/^(?:utm_[a-z0-9_]*|fbclid|gclid|yclid|ref|from)$/i.test(name)) continue;
      filtered.push([name, value]);
    }
    filtered.sort((a, b) => {
      if (a[0] === b[0]) return a[1].localeCompare(b[1]);
      return a[0].localeCompare(b[0]);
    });
    return filtered
      .map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
      .join("&");
  }

  function extractStrongProfileId(urlObj) {
    if (!(urlObj instanceof URL)) return "";
    const pathMatch = urlObj.pathname.match(/(?:^|\/)(?:id\/)?(\d{3,})(?:\/|$)/i);
    if (pathMatch && pathMatch[1]) return pathMatch[1];

    for (const key of ["id", "anketa", "anket", "profile", "girl", "model", "user"]) {
      const value = (urlObj.searchParams.get(key) || "").trim();
      if (/^\d{3,}$/.test(value)) return value;
    }

    return "";
  }

  function parseProfileRoute(rawUrl) {
    const normalizedUrl = normalizeProfileUrl(rawUrl);
    if (!normalizedUrl) return null;

    try {
      const urlObj = new URL(normalizedUrl);
      const full = `${urlObj.pathname}${urlObj.search}`.toLowerCase();
      if (!isLikelyProfileHref(full)) return null;

      const strongId = extractStrongProfileId(urlObj);
      if (strongId) {
        return {
          key: `id:${strongId}`,
          normalizedUrl: urlObj.href,
        };
      }

      const path = (urlObj.pathname || "/").replace(/\/+$/g, "").toLowerCase() || "/";
      const normalizedQuery = buildNormalizedQueryString(urlObj.searchParams);
      const fallbackRoute = normalizedQuery ? `${path}?${normalizedQuery}` : path;
      return {
        key: `route:${fallbackRoute}`,
        normalizedUrl: urlObj.href,
      };
    } catch {
      return null;
    }
  }

  function requestToPromise(request) {
    return new Promise((resolve, reject) => {
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error);
    });
  }

  function openVisitedDb() {
    if (visitedDbPromise) return visitedDbPromise;
    if (!("indexedDB" in window)) {
      visitedDbPromise = Promise.resolve(null);
      return visitedDbPromise;
    }

    visitedDbPromise = new Promise((resolve) => {
      try {
        const openRequest = indexedDB.open(VISITED_DB_NAME, VISITED_DB_VERSION);
        openRequest.onupgradeneeded = () => {
          const db = openRequest.result;
          if (!db.objectStoreNames.contains(VISITED_DB_STORE)) {
            db.createObjectStore(VISITED_DB_STORE, { keyPath: "routeKey" });
          }
        };
        openRequest.onsuccess = () => resolve(openRequest.result);
        openRequest.onerror = () => {
          visitedDbPromise = null;
          resolve(null);
        };
        openRequest.onblocked = () => {
          visitedDbPromise = null;
          resolve(null);
        };
      } catch {
        visitedDbPromise = null;
        resolve(null);
      }
    });

    return visitedDbPromise;
  }

  async function rememberVisitedProfile(rawUrl) {
    const route = parseProfileRoute(rawUrl);
    if (!route) return;

    visitedRouteStateCache.set(route.key, true);

    const db = await openVisitedDb();
    if (!db) return;

    try {
      const tx = db.transaction(VISITED_DB_STORE, "readwrite");
      const store = tx.objectStore(VISITED_DB_STORE);
      store.put({
        routeKey: route.key,
        profileUrl: route.normalizedUrl,
        visitedAt: Date.now(),
      });
    } catch {
      // no-op
    }
  }

  async function resolveVisitedRouteMap(routeKeys) {
    const result = new Map();
    const uniqueKeys = Array.from(new Set(routeKeys.filter(Boolean)));
    if (!uniqueKeys.length) return result;

    const missingKeys = [];
    for (const key of uniqueKeys) {
      if (visitedRouteStateCache.has(key)) {
        result.set(key, visitedRouteStateCache.get(key) === true);
      } else {
        missingKeys.push(key);
      }
    }

    if (!missingKeys.length) return result;

    const db = await openVisitedDb();
    if (!db) {
      missingKeys.forEach((key) => {
        visitedRouteStateCache.set(key, false);
        result.set(key, false);
      });
      return result;
    }

    try {
      const tx = db.transaction(VISITED_DB_STORE, "readonly");
      const store = tx.objectStore(VISITED_DB_STORE);
      await Promise.all(
        missingKeys.map(async (key) => {
          try {
            const record = await requestToPromise(store.get(key));
            const isVisited = Boolean(record);
            visitedRouteStateCache.set(key, isVisited);
            result.set(key, isVisited);
          } catch {
            visitedRouteStateCache.set(key, false);
            result.set(key, false);
          }
        })
      );
    } catch {
      missingKeys.forEach((key) => {
        visitedRouteStateCache.set(key, false);
        result.set(key, false);
      });
    }

    return result;
  }

  function setCardVisitedState(card, visited) {
    if (!(card instanceof Element)) return;
    if (visited) {
      card.setAttribute(VISITED_PROFILE_ATTR, "1");
    } else {
      card.removeAttribute(VISITED_PROFILE_ATTR);
    }
  }

  function collectProfileCardEntries(root) {
    if (!(root instanceof Document || root instanceof Element)) return [];
    const entriesByCard = new Map();

    root.querySelectorAll("a[href]").forEach((link) => {
      if (!isLikelyProfileLink(link)) return;
      const card = findProfileCardForLink(link);
      if (!card) return;

      const route = parseProfileRoute(link.href || link.getAttribute("href") || "");
      if (!route) return;

      if (!entriesByCard.has(card)) {
        entriesByCard.set(card, {
          card,
          profileUrl: route.normalizedUrl,
          routeKey: route.key,
        });
      }
    });

    return Array.from(entriesByCard.values());
  }

  async function markVisitedProfiles(root) {
    if (visitedMarkInFlight) {
      visitedMarkRerunRequested = true;
      return;
    }

    visitedMarkInFlight = true;
    visitedMarkRerunRequested = false;

    try {
      const entries = collectProfileCardEntries(root);
      if (!entries.length) return;
      const routeMap = await resolveVisitedRouteMap(entries.map((entry) => entry.routeKey));
      entries.forEach((entry) => {
        setCardVisitedState(entry.card, routeMap.get(entry.routeKey) === true);
      });
    } finally {
      visitedMarkInFlight = false;
      if (visitedMarkRerunRequested) {
        visitedMarkRerunRequested = false;
        scheduleVisitedMarking();
      }
    }
  }

  function scheduleVisitedMarking() {
    if (visitedMarkScheduled) return;
    visitedMarkScheduled = true;
    setTimeout(() => {
      visitedMarkScheduled = false;
      void markVisitedProfiles(document);
    }, 160);
  }

  function getElementCueText(el) {
    if (!(el instanceof Element)) return "";
    const className = typeof el.className === "string" ? el.className : "";
    return `${el.id || ""} ${className} ${el.getAttribute("data-type") || ""}`.toLowerCase();
  }

  function isCommentOrReviewArea(el) {
    if (!(el instanceof Element)) return false;
    let current = el;
    let depth = 0;
    while (current && depth < 4) {
      if (COMMENT_AREA_SORT_EXCLUDE_RE.test(getElementCueText(current))) return true;
      current = current.parentElement;
      depth += 1;
    }
    return false;
  }

  function collectProfileCardGroups(root) {
    if (!(root instanceof Document || root instanceof Element)) return [];
    const groups = new Map();

    collectProfileCardEntries(root).forEach((entry) => {
      const card = entry.card;
      const parent = card.parentElement;
      if (!(parent instanceof Element)) return;

      let cardMap = groups.get(parent);
      if (!cardMap) {
        cardMap = new Map();
        groups.set(parent, cardMap);
      }
      if (!cardMap.has(card)) {
        cardMap.set(card, entry.profileUrl);
      }
    });

    const result = [];
    for (const [parent, cardMap] of groups.entries()) {
      if (isCommentOrReviewArea(parent)) continue;
      const entries = Array.from(cardMap.entries())
        .filter(([card]) => card.parentElement === parent)
        .map(([card, profileUrl]) => ({ card, profileUrl }));
      if (entries.length < 3) continue;
      result.push({ parent, entries });
    }

    return result;
  }

  function reorderCardsPreservingSlots(parent, sortedCards) {
    const sortedSet = new Set(sortedCards);
    const slotMarkers = [];
    Array.from(parent.children).forEach((child) => {
      if (!sortedSet.has(child)) return;
      const marker = document.createComment("tm-worst-review-slot");
      parent.insertBefore(marker, child);
      slotMarkers.push(marker);
    });

    if (slotMarkers.length !== sortedCards.length) {
      slotMarkers.forEach((marker) => marker.remove());
      return;
    }

    for (let i = 0; i < sortedCards.length; i += 1) {
      parent.insertBefore(sortedCards[i], slotMarkers[i]);
    }

    slotMarkers.forEach((marker) => marker.remove());
  }

  function normalizeFetchUrl(rawUrl) {
    if (!rawUrl) return "";
    try {
      const urlObj = new URL(rawUrl, location.href);
      urlObj.hash = "";
      const normalizedQuery = buildNormalizedQueryString(urlObj.searchParams);
      urlObj.search = normalizedQuery ? `?${normalizedQuery}` : "";
      return urlObj.href;
    } catch {
      return "";
    }
  }

  async function fetchPageHtml(url) {
    const controller = new AbortController();
    const timer = setTimeout(() => controller.abort(), PROFILE_FETCH_TIMEOUT_MS);
    try {
      const response = await fetch(url, {
        method: "GET",
        credentials: "include",
        signal: controller.signal,
      });
      if (!response.ok) return "";
      return await response.text();
    } catch {
      return "";
    } finally {
      clearTimeout(timer);
    }
  }

  function scoreReviewSignalText(value) {
    const text = normalizeSpaces(value || "").toLowerCase();
    if (!text) return 0;

    const ratings = extractRatingsFromText(text);
    const reviewCount = extractReviewCount(text);
    const negativeHits = countRegexMatches(text, NEGATIVE_REVIEW_WORD_RE);
    const positiveHits = countRegexMatches(text, POSITIVE_REVIEW_WORD_RE);
    const hasReviewCue = REVIEW_CUE_RE.test(text);

    let score = 0;

    if (ratings.length) {
      const minRating = Math.min(...ratings);
      const avgRating = ratings.reduce((sum, item) => sum + item, 0) / ratings.length;
      score += (5 - minRating) * 15;
      score += (5 - avgRating) * 6;
    } else if (hasReviewCue) {
      score += 2;
    }

    if (reviewCount !== null) {
      score += Math.min(reviewCount, 600) * 0.07;
    }

    score += negativeHits * 3.8;
    score -= positiveHits * 1.7;

    if (NO_REVIEW_CUE_RE.test(text)) {
      score -= 10;
    }
    if (/не\s+рекоменд|не\s+совет/iu.test(text)) {
      score += 6;
    }

    return score;
  }

  function collectReviewTextChunks(doc) {
    if (!(doc instanceof Document)) return [];

    const chunks = [];
    const seen = new Set();
    const pushChunk = (value) => {
      const normalized = normalizeSpaces(value || "").toLowerCase();
      if (normalized.length < 24) return;

      const hasSignal =
        REVIEW_CUE_RE.test(normalized) ||
        extractRatingsFromText(normalized).length > 0 ||
        countRegexMatches(normalized, NEGATIVE_REVIEW_WORD_RE) > 0 ||
        countRegexMatches(normalized, POSITIVE_REVIEW_WORD_RE) > 0;
      if (!hasSignal) return;

      const key = `${normalized.slice(0, 180)}|${normalized.length}`;
      if (seen.has(key)) return;
      seen.add(key);
      chunks.push(normalized);
    };

    let seenNodes = 0;
    for (const node of doc.querySelectorAll(REVIEW_SECTION_SELECTOR)) {
      if (!(node instanceof Element)) continue;
      const text = node.textContent || "";
      if (text.length < 24) continue;
      pushChunk(text);
      seenNodes += 1;
      if (seenNodes >= 220) break;
    }

    if (!chunks.length && doc.body) {
      pushChunk((doc.body.textContent || "").slice(0, 120000));
    }

    return chunks;
  }

  function isSameProfileScope(urlA, urlB) {
    if (!(urlA instanceof URL) || !(urlB instanceof URL)) return true;
    const idA = extractStrongProfileId(urlA);
    const idB = extractStrongProfileId(urlB);
    if (!idA || !idB) return true;
    return idA === idB;
  }

  function collectReviewPageLinks(doc, sourceUrl) {
    if (!(doc instanceof Document)) return [];
    let source;
    try {
      source = new URL(sourceUrl);
    } catch {
      return [];
    }

    const sourceNormalized = normalizeFetchUrl(source.href);
    const links = new Set();
    const hasReviewSection = Boolean(doc.querySelector(REVIEW_SECTION_SELECTOR));

    doc.querySelectorAll("a[href]").forEach((link) => {
      if (!(link instanceof HTMLAnchorElement)) return;
      const rawHref = (link.getAttribute("href") || "").trim();
      if (!rawHref || rawHref.startsWith("#") || /^javascript:/i.test(rawHref)) return;

      let resolved;
      try {
        resolved = new URL(rawHref, source.href);
      } catch {
        return;
      }

      if (resolved.origin !== source.origin) return;
      if (!isSameProfileScope(source, resolved)) return;

      const hrefPart = `${resolved.pathname}${resolved.search}`.toLowerCase();
      const cueText = `${getElementCueText(link)} ${link.getAttribute("title") || ""} ${link.getAttribute("aria-label") || ""}`.toLowerCase();
      const label = normalizeSpaces(link.textContent || "").toLowerCase();
      const insidePagination = Boolean(link.closest(REVIEW_PAGINATION_SELECTOR));
      const insideReviewSection = Boolean(link.closest(REVIEW_SECTION_SELECTOR));
      const looksLikeReviewLink = REVIEW_PAGE_LINK_CUE_RE.test(hrefPart) || REVIEW_PAGE_LINK_CUE_RE.test(cueText);
      const looksLikeReviewPagination =
        (insideReviewSection || (hasReviewSection && insidePagination)) && REVIEW_PAGE_NAV_TEXT_RE.test(label);

      if (!looksLikeReviewLink && !looksLikeReviewPagination) return;

      const normalized = normalizeFetchUrl(resolved.href);
      if (!normalized || normalized === sourceNormalized) return;
      links.add(normalized);
    });

    return Array.from(links);
  }

  function clampValue(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

  function scoreReviewDocument(doc) {
    const chunks = collectReviewTextChunks(doc);
    if (!chunks.length) return 0;

    let score = 0;
    let bestChunk = Number.NEGATIVE_INFINITY;

    for (let i = 0; i < chunks.length && i < 120; i += 1) {
      const chunkScore = scoreReviewSignalText(chunks[i]);
      score += clampValue(chunkScore, -10, 34);
      bestChunk = Math.max(bestChunk, chunkScore);
    }

    if (bestChunk > 0) {
      score += bestChunk * 0.4;
    }

    return clampValue(score, -30, 260);
  }

  async function fetchBadnessScoreForProfile(url) {
    const normalizedRoot = normalizeFetchUrl(url);
    if (!normalizedRoot) return 0;
    if (profileBadnessScoreCache.has(normalizedRoot)) {
      return profileBadnessScoreCache.get(normalizedRoot) || 0;
    }

    const pending = profileBadnessRequestCache.get(normalizedRoot);
    if (pending) return pending;

    const request = (async () => {
      const queue = [normalizedRoot];
      const seen = new Set();
      let crawled = 0;
      let totalScore = 0;

      try {
        while (queue.length && crawled < PROFILE_REVIEW_PAGE_LIMIT) {
          const nextUrl = queue.shift();
          if (!nextUrl || seen.has(nextUrl)) continue;
          seen.add(nextUrl);

          const html = await fetchPageHtml(nextUrl);
          if (!html) continue;

          crawled += 1;
          const doc = new DOMParser().parseFromString(html, "text/html");
          totalScore += scoreReviewDocument(doc);

          const extraLinks = collectReviewPageLinks(doc, nextUrl);
          for (const link of extraLinks) {
            if (seen.has(link) || queue.includes(link)) continue;
            queue.push(link);
          }
        }
      } catch {
        // no-op
      } finally {
        profileBadnessRequestCache.delete(normalizedRoot);
      }

      const finalScore = clampValue(totalScore, -40, 1600);
      profileBadnessScoreCache.set(normalizedRoot, finalScore);
      return finalScore;
    })();

    profileBadnessRequestCache.set(normalizedRoot, request);
    return request;
  }

  async function warmProfileBadnessScores(profileUrls) {
    const urls = Array.from(new Set(profileUrls.filter(Boolean)));
    if (!urls.length) return;
    let cursor = 0;

    const workerCount = Math.min(PROFILE_FETCH_CONCURRENCY, urls.length);
    const workers = [];
    for (let i = 0; i < workerCount; i += 1) {
      workers.push(
        (async () => {
          while (cursor < urls.length) {
            const index = cursor;
            cursor += 1;
            const targetUrl = urls[index];
            await fetchBadnessScoreForProfile(targetUrl);
          }
        })()
      );
    }

    await Promise.all(workers);
  }

  function sortCardsByWorstReviewScore(entries) {
    const ranked = entries.map((entry, index) => ({
      card: entry.card,
      index,
      remoteScore: profileBadnessScoreCache.get(normalizeFetchUrl(entry.profileUrl)) || 0,
      localScore: getReviewBadnessScore(entry.card),
    }));

    ranked.forEach((entry) => {
      const score = entry.remoteScore + entry.localScore * 0.35;
      entry.score = score;
      entry.card.setAttribute(REVIEW_SCORE_ATTR, score.toFixed(2));
    });

    ranked.sort(
      (a, b) => b.score - a.score || b.remoteScore - a.remoteScore || b.localScore - a.localScore || a.index - b.index
    );

    if (!ranked.length) return null;
    const orderChanged = ranked.some((entry, index) => entry.card !== entries[index].card);
    if (!orderChanged) return null;

    return ranked.map((entry) => entry.card);
  }

  async function sortProfilesByWorstReviews(root) {
    if (!worstSortEnabled) return;

    if (worstSortInFlight) {
      worstSortRerunRequested = true;
      return;
    }

    worstSortInFlight = true;
    worstSortRerunRequested = false;

    const groups = collectProfileCardGroups(root);

    try {
      const allProfileUrls = groups.flatMap((group) => group.entries.map((entry) => entry.profileUrl));
      await warmProfileBadnessScores(allProfileUrls);

      for (const group of groups) {
        const sortedCards = sortCardsByWorstReviewScore(group.entries);
        if (!sortedCards) continue;
        reorderCardsPreservingSlots(group.parent, sortedCards);
      }
    } finally {
      worstSortInFlight = false;
      if (worstSortRerunRequested) {
        worstSortRerunRequested = false;
        scheduleWorstReviewSort();
      }
    }
  }

  function scheduleWorstReviewSort() {
    if (!worstSortEnabled) return;
    if (worstSortScheduled) return;
    worstSortScheduled = true;
    setTimeout(() => {
      worstSortScheduled = false;
      void sortProfilesByWorstReviews(document);
    }, 140);
  }

  function primeWorstReviewSort() {
    if (!worstSortEnabled) return;
    scheduleWorstReviewSort();
    setTimeout(() => void sortProfilesByWorstReviews(document), 700);
    setTimeout(() => void sortProfilesByWorstReviews(document), 1800);
  }

  function escapeForRegex(value) {
    return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  }

  function compileForbiddenWordRegex(tokens) {
    const uniq = Array.from(new Set(tokens.map((v) => (v || "").trim().toLowerCase()).filter(Boolean)));
    const variants = [];

    for (const token of uniq) {
      const isStem = token.endsWith("*");
      const base = isStem ? token.slice(0, -1) : token;
      if (!base) continue;

      const escaped = escapeForRegex(base);
      const variant = isStem ? `${escaped}[\\p{L}\\p{N}_-]*` : escaped;
      variants.push(variant);
    }

    if (!variants.length) return null;
    return new RegExp(`(?<![\\p{L}\\p{N}_])(?:${variants.join("|")})(?![\\p{L}\\p{N}_])`, "giu");
  }

  function compileSanitizeHintRegex(tokens) {
    const chunks = Array.from(
      new Set(
        tokens
          .map((v) => (v || "").trim().toLowerCase().replace(/\*+$/g, ""))
          .filter(Boolean)
          .map(escapeForRegex)
      )
    );

    if (!chunks.length) return /\d/;
    return new RegExp(`\\d|${chunks.join("|")}`, "i");
  }

  function hasPhoneLikePattern(value) {
    if (!value || !/\d/.test(value)) return false;
    const hasPhoneCue = /(тел|телефон|phone|tel|номер|call|wa\.me|whatsapp|viber|telegram|tg)/i.test(value);
    const matches = value.match(/(?:\+?\d[\d\s().-]{8,}\d)/g);
    if (!matches) return false;
    return matches.some((chunk) => {
      const digits = chunk.replace(/\D/g, "");
      if (digits.length < 10 || digits.length > 15) return false;
      const hasDelimiter = /[\s().-]/.test(chunk) || chunk.startsWith("+");
      return hasDelimiter || hasPhoneCue;
    });
  }

  function maskPhoneText(value) {
    return value.replace(/(?:\+?\d[\d\s().-]{8,}\d)/g, (chunk) => {
      const digits = chunk.replace(/\D/g, "");
      if (digits.length < 10 || digits.length > 15) return chunk;
      return "номер скрыт";
    });
  }

  function maskTokenWithHashes(token) {
    const chars = Array.from(token);
    const letterLikeIndexes = [];

    for (let i = 0; i < chars.length; i += 1) {
      if (/[\p{L}\p{N}]/u.test(chars[i])) {
        letterLikeIndexes.push(i);
      }
    }

    if (letterLikeIndexes.length <= 2) {
      if (letterLikeIndexes.length === 0) return token;
      if (letterLikeIndexes.length === 1) return "#";
      chars[letterLikeIndexes[1]] = "#";
      return chars.join("");
    }

    // Keep readability for "конч*" forms (e.g. "кончил" -> "к#нчил").
    if (/^конч/iu.test(token) && letterLikeIndexes.length >= 2) {
      const secondLetterIndex = letterLikeIndexes[1];
      chars[secondLetterIndex] = "#";
      return chars.join("");
    }

    // Keep readability for "член*" forms (e.g. "член" -> "чл#н").
    if (/^член/iu.test(token) && letterLikeIndexes.length >= 3) {
      const thirdLetterIndex = letterLikeIndexes[2];
      chars[thirdLetterIndex] = "#";
      return chars.join("");
    }

    // Soft mode: keep words understandable by masking only 1 char (2 for very long tokens).
    const logicalMaskIndexes = [1];
    if (letterLikeIndexes.length >= 10) {
      logicalMaskIndexes.push(Math.floor(letterLikeIndexes.length / 2));
    }

    for (const logicalIndex of logicalMaskIndexes) {
      const sourceIndex = letterLikeIndexes[logicalIndex];
      if (sourceIndex === undefined) continue;
      chars[sourceIndex] = "#";
    }

    return chars.join("");
  }

  function maskWordWithHashes(value) {
    const tokenMatches = Array.from(value.matchAll(/[\p{L}\p{N}]+/gu));
    if (!tokenMatches.length) return value;

    if (tokenMatches.length === 1) {
      return value.replace(/[\p{L}\p{N}]+/u, (token) => maskTokenWithHashes(token));
    }

    // For multi-word phrases, mask only the first word to keep text understandable.
    const first = tokenMatches[0];
    const start = first.index || 0;
    const token = first[0];
    const masked = maskTokenWithHashes(token);
    return `${value.slice(0, start)}${masked}${value.slice(start + token.length)}`;
  }

  function maskRiskyWords(value) {
    if (!RISKY_WORD_REGEX) return value;
    RISKY_WORD_REGEX.lastIndex = 0;
    return value.replace(RISKY_WORD_REGEX, (word) => maskWordWithHashes(word));
  }

  function sanitizeText(value) {
    if (!value || !TEXT_SANITIZE_HINT_RE.test(value)) return value;
    let next = maskPhoneText(value);
    next = maskRiskyWords(next);
    return next;
  }

  function isInsideImageKeepArea(el) {
    if (!(el instanceof Element)) return false;
    if (el.matches(IMAGE_KEEP_ROOT_SELECTOR)) return true;
    return Boolean(el.closest(IMAGE_KEEP_ROOT_SELECTOR));
  }

  function isInsideContactKeepArea(el) {
    if (!(el instanceof Element)) return false;
    if (el.matches(CONTACT_KEEP_SELECTOR)) return true;
    return Boolean(el.closest("#headerBg, .main_link1, #search"));
  }

  function isUserInfoElement(el) {
    if (!(el instanceof Element)) return false;
    if (el.matches(USERINFO_KEEP_SELECTOR)) return true;
    return Boolean(el.closest(USERINFO_KEEP_SELECTOR));
  }

  function hideContactElement(el) {
    if (!(el instanceof Element)) return;
    if (isInsideContactKeepArea(el) || isUserInfoElement(el)) {
      if (el.hasAttribute(HIDDEN_CONTACT_ATTR)) {
        el.removeAttribute(HIDDEN_CONTACT_ATTR);
        el.style.removeProperty("display");
        el.style.removeProperty("visibility");
        el.style.removeProperty("opacity");
      }
      return;
    }
    if (el.hasAttribute(HIDDEN_CONTACT_ATTR)) return;
    el.setAttribute(HIDDEN_CONTACT_ATTR, "1");
    el.style.setProperty("display", "none", "important");
    el.style.setProperty("visibility", "hidden", "important");
    el.style.setProperty("opacity", "0", "important");
    if (el.tagName === "A") {
      el.removeAttribute("href");
    }
    el.removeAttribute("onclick");
  }

  function shouldHideByAttributes(el) {
    if (!(el instanceof Element)) return false;
    if (isUserInfoElement(el)) return false;
    const attrNames = el.getAttributeNames();
    for (const attrName of attrNames) {
      const value = (el.getAttribute(attrName) || "").trim();
      const name = attrName.toLowerCase();
      const allowPhonePattern =
        name === "href" ||
        name === "title" ||
        name === "aria-label" ||
        name === "value" ||
        name.startsWith("data-");

      if (CONTACT_ATTR_NAME_RE.test(name) && value) return true;
      if (value && CONTACT_VALUE_RE.test(value)) return true;
      if (value && allowPhonePattern && hasPhoneLikePattern(value)) return true;
    }
    return false;
  }

  function sanitizeTextNode(node) {
    if (!(node instanceof Text)) return;
    if (!node.nodeValue) return;
    const parent = node.parentElement;
    if (!parent) return;
    const tag = parent.tagName;
    if (tag === "SCRIPT" || tag === "STYLE" || tag === "NOSCRIPT" || tag === "TEXTAREA") return;

    const next = sanitizeText(node.nodeValue);
    if (next !== node.nodeValue) {
      node.nodeValue = next;
    }
  }

  function scanSanitizedText(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
    let node = walker.nextNode();
    while (node) {
      sanitizeTextNode(node);
      node = walker.nextNode();
    }
  }

  function sanitizeFormTextField(el) {
    if (!(el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement)) return;
    if (isInsideContactKeepArea(el)) return;
    const source = el.value || (el instanceof HTMLTextAreaElement ? el.textContent || "" : "");
    if (!source) return;

    const next = sanitizeText(source);
    if (next === source) return;

    el.value = next;
    if (el instanceof HTMLTextAreaElement) {
      el.textContent = next;
      return;
    }

    if (el.hasAttribute("value")) {
      el.setAttribute("value", next);
    }
  }

  function scanSanitizedFormFields(root) {
    if (!(root instanceof Document || root instanceof Element)) return;

    if (
      (root instanceof HTMLTextAreaElement || root instanceof HTMLInputElement) &&
      root.matches(SANITIZED_TEXT_FIELD_SELECTOR)
    ) {
      sanitizeFormTextField(root);
    }

    root.querySelectorAll(SANITIZED_TEXT_FIELD_SELECTOR).forEach((el) => {
      sanitizeFormTextField(el);
    });
  }

  function forEachMatch(root, selector, cb) {
    if (!(root instanceof Document || root instanceof Element)) return;
    if (root instanceof Element && root.matches(selector)) cb(root);
    root.querySelectorAll(selector).forEach(cb);
  }

  function scanContacts(root) {
    if (!(root instanceof Document || root instanceof Element)) return;

    forEachMatch(root, CONTACT_LINK_SELECTOR, hideContactElement);
    forEachMatch(root, CONTACT_ATTR_SCAN_SELECTOR, (el) => {
      if (shouldHideByAttributes(el)) hideContactElement(el);
    });
    scanSanitizedText(root);
    scanSanitizedFormFields(root);
  }

  function neutralize(el) {
    if (!(el instanceof Element)) return;

    if (!isInsideContactKeepArea(el)) {
      if (el.matches(CONTACT_LINK_SELECTOR) || shouldHideByAttributes(el)) {
        hideContactElement(el);
      }
    }

    if (!blockingEnabled) return;
    if (isInsideImageKeepArea(el)) return;

    if (el.matches("img")) {
      el.removeAttribute("src");
      el.removeAttribute("srcset");
      el.removeAttribute("data-src");
      el.removeAttribute("data-srcset");
      el.removeAttribute("data-original");
      el.removeAttribute("data-lazy");
      el.removeAttribute("data-lazy-src");
      return;
    }

    if (el.matches("source")) {
      el.removeAttribute("src");
      el.removeAttribute("srcset");
      return;
    }

    if (el.matches("video[poster]")) {
      el.removeAttribute("poster");
    }

    if (el.hasAttribute("style")) {
      const style = el.getAttribute("style") || "";
      if (/background-image\s*:\s*url\(/i.test(style)) {
        el.setAttribute(
          "style",
          style.replace(/background-image\s*:[^;]+;?/gi, "background-image:none !important;")
        );
      }
    }
  }

  function scanImages(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    const list = root.querySelectorAll(
      "img, source, picture, svg image, video[poster], [style*='background-image']:not(#headerBg):not(#headerBg *):not(.main_link1):not(.main_link1 *):not(.up-ocenki-basic):not(.up-ocenki-basic *):not(.up-ocenki-dop):not(.up-ocenki-dop *)"
    );
    list.forEach(neutralize);
  }

  function scan(root) {
    if (!(root instanceof Document || root instanceof Element)) return;
    scanContacts(root);
    repairSearchUi(root);
    if (blockingEnabled) scanImages(root);
    if (worstSortEnabled) scheduleWorstReviewSort();
    scheduleVisitedMarking();
  }

  function triggerCompareRescan() {
    setTimeout(() => scan(document), 0);
    setTimeout(() => scan(document), 150);
    setTimeout(() => scan(document), 450);
    setTimeout(() => void sortProfilesByWorstReviews(document), 900);
  }

  function observeCompareTriggers() {
    document.addEventListener(
      "click",
      (event) => {
        if (!(event.target instanceof Element)) return;
        if (!event.target.closest(COMPARE_TRIGGER_SELECTOR)) return;
        triggerCompareRescan();
      },
      true
    );
  }

  function markCurrentProfileRouteVisited() {
    const route = parseProfileRoute(location.href);
    if (!route) return;
    void rememberVisitedProfile(route.normalizedUrl);
    scheduleVisitedMarking();
  }

  function trackVisitedProfileLink(event) {
    if (!(event.target instanceof Element)) return;
    const link = event.target.closest("a[href]");
    if (!(link instanceof HTMLAnchorElement)) return;
    if (!isLikelyProfileLink(link)) return;

    const route = parseProfileRoute(link.href || link.getAttribute("href") || "");
    if (!route) return;

    void rememberVisitedProfile(route.normalizedUrl);
    const card = findProfileCardForLink(link);
    if (card) setCardVisitedState(card, true);
    scheduleVisitedMarking();
  }

  function observeVisitedProfiles() {
    document.addEventListener("click", trackVisitedProfileLink, true);
    document.addEventListener("auxclick", trackVisitedProfileLink, true);
  }

  function observe() {
    const root = document.documentElement;
    if (!root) {
      document.addEventListener("DOMContentLoaded", observe, { once: true });
      return;
    }

    const obs = new MutationObserver((mutations) => {
      let hasReviewRelevantMutation = false;
      for (const m of mutations) {
        if (m.type === "characterData") {
          sanitizeTextNode(m.target);
          hasReviewRelevantMutation = true;
          continue;
        }

        if (m.type === "attributes" && m.target instanceof Element) {
          neutralize(m.target);
          continue;
        }

        if (m.type === "childList") {
          hasReviewRelevantMutation = true;
          m.addedNodes.forEach((n) => {
            if (n instanceof Element) {
              neutralize(n);
              scan(n);
              return;
            }

            if (n instanceof Text) {
              sanitizeTextNode(n);
            }
          });
        }
      }

      if (hasReviewRelevantMutation) {
        scheduleWorstReviewSort();
        scheduleVisitedMarking();
      }
    });

    obs.observe(root, {
      childList: true,
      subtree: true,
      characterData: true,
      attributes: true,
      attributeFilter: OBSERVER_ATTR_FILTER,
    });
  }

  injectToggleButton();
  injectWorstSortButton();
  injectContactCss();
  injectSearchCss();
  injectVisitedCss();
  scanContacts(document);
  repairSearchUi(document);
  if (worstSortEnabled) primeWorstReviewSort();
  observeCompareTriggers();
  observeVisitedProfiles();
  observe();
  markCurrentProfileRouteVisited();
  scheduleVisitedMarking();

  if (blockingEnabled) {
    injectBlockCss();
    scanImages(document);
  }

  document.addEventListener("DOMContentLoaded", () => {
    scan(document);
    markCurrentProfileRouteVisited();
  });
})();