Hentai Heroes SFW

Removing explicit images in Hentai Heroes game and changing game background to a SFW one.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Hentai Heroes SFW
// @namespace    https://sleazyfork.org/fr/scripts/539097-hentai-heroes-sfw
// @description  Removing explicit images in Hentai Heroes game and changing game background to a SFW one.
// @version      4.1.1
// @match        https://*.hentaiheroes.com/*
// @run-at       document-start
// @grant        none
// @author       Geto_hh
// @license      MIT
// ==/UserScript==

// ==CHANGELOG==
// 4.1.1: fix panel position
// 4.1.0: show NSFW icon when every settings are false
// 4.0.1: various fixes
// 4.0.0: Add settings icon and panel
// 3.12.1: Fix affection scenes
// 3.12.0: Update README.md
// 3.11.0: Update description
// 3.10.0: Hide login background
// 3.9.0: Hide champion's club girl avatars
// 3.8.0: Hide login video
// 3.7.0: Hide level up pop-up girl
// 3.6.1: Fix club champion css
// 3.6.0: Hide champions
// 3.5.0: Hide lse girl
// 3.4.0: Hide images in harem
// 3.3.2: Update description
// 3.3.1: Update description
// 3.3.0: Optimize code
// 3.2.0: Optimize and mutualize code, fix bugs (empty selector crash, dead ternary, QUEST TypeError, unused constant)
// 3.1.0: Refactor code to mutualize page lists
// 3.0.0: Remove girl img src modifications and observer due to girl media url changes that no longer allow the process
// 2.2.0: Add waifu page support
// 2.1.6: Fix event selectors
// 2.1.5: Fix event selectors
// 2.1.4: Fix harem selectors
// 2.1.3: Fix harem selectors
// 2.1.2: Fix harem selectors
// 2.1.1: Fix mobile login image removal
// 2.1.0: Remove mobile images
// 2.0.6: Trying Github webhook again
// 2.0.5: Trying Github webhook
// 2.0.4: Updating description
// 2.0.3: Fixed login screen img removal
// 2.0.2: Fixed champions css positions and mythic day lively scene
// 2.0.1: Fixed Harem avatar modification
// 2.0.0: Improved observer handling and delaying
// 1.12.2: Fix modify script
// 1.12.1: Fix various pages
// 1.12.0: Add penta-drill support
// 1.11.2: Add new pages and fix champions page display
// 1.11.1: Add leagues page support
// 1.11.0: Split avatars, icons & girl images
// 1.10.1: Fix css selectors
// 1.10.0: Add option to replace background
// 1.9.0: Add option to hide avatars
// 1.8.0: Add option to hide girls
// 1.7.0: Put observer back for girls and home background
// 1.6.0: Use style to hide images and background images
// 1.5.0: Split hide process and modify process & remove observer
// 1.4.0: Hide girls on the event and refill pop-up
// 1.3.0: Replace home page background image to alway have the same one and avoid NSFW ones
// 1.2.0: Allow user to show affection scene when clicking on eye icon
// 1.1.0: Page per page query selectors and optimized girl icons processing on edit team pages
// 1.0.1: Small fixes
// 1.0.0: Optimize script for release
// 0.6.0: Use only document query selectors and run continously
// 0.5.0: Optimized script for faster processing
// 0.4.0: Stop processing mutations when a 'diamond' or 'speech_bubble_info_icn' class element is clicked
// 0.3.0: Run script only once per page load
// 0.2.0: Added namespace
// 0.1.0: First available version on SleazyFork
// ==/CHANGELOG==

/**
 * CONFIGURATION
 */
const DEBUG_LIMIT_ACTIVATED = false;
const HIDE_ADDS = false;
const HIDE_BACKGROUND = false;

/**
 * VARIABLES
 */
// let is required — DEBUG_ACTIVATED is reassigned to false inside checkDebugLimit()
let DEBUG_ACTIVATED = true; // eslint-disable-line no-var
let debugLimitCount = 0;
let isCssInjected = false;
let isDOMReady = false;

/**
 * HHSFW SETTINGS — persisted in localStorage under the key 'HHSFW.settings'.
 * Defaults match the original hardcoded values.
 */
const HHSFW_DEFAULTS = {
  HIDE_EVENT_GIRLS_AVATARS : true,
  HIDE_HAREM_SELECTED_GIRL_AVATAR : true,
  HIDE_OTHER_GIRLS_AVATARS : true,
  HIDE_OTHER_PLAYERS_AVATARS : true,
  HIDE_VIDEO_PREVIEWS : true,
  REPLACE_BACKGROUND : true,
};

(function loadHhsfwSettings() {
  try {
    const stored = localStorage.getItem('HHSFW.settings');
    if (stored) {
      const parsed = JSON.parse(stored);
      Object.keys(HHSFW_DEFAULTS).forEach(function (key) {
        if (typeof parsed[key] === 'boolean') {
          HHSFW_DEFAULTS[key] = parsed[key];
        }
      });
    }
  } catch (e) {
    console.error('[HHSFW] Failed to load settings from localStorage:', e);
  }
})();

// Destructure once so PAGE_LIST can reference these as plain identifiers.
const {
  HIDE_EVENT_GIRLS_AVATARS,
  HIDE_HAREM_SELECTED_GIRL_AVATAR,
  HIDE_OTHER_GIRLS_AVATARS,
  HIDE_OTHER_PLAYERS_AVATARS,
  HIDE_VIDEO_PREVIEWS,
  REPLACE_BACKGROUND,
} = HHSFW_DEFAULTS;

/**
 * CONSTANTS
 */
const NEW_BACKGROUND_URL =
  'https://hh2.hh-content.com/pictures/gallery/6/2200x/401-a8339a2168753900db437d91f2ed39ff.jpg';

// Pre-evaluated once — avoids repeating the same conditional spread across every page entry
const PLAYER_AVATAR_SELECTORS = ['.player-profile-picture > img'];
const BACKGROUND_SELECTORS = ['.fixed_scaled > img'];
const VIDEO_PREVIEW_SELECTORS = [
  '.lively_scene > img',
  '.lively_scene-wrapper > .unlocked > img',
  '.lively_scenes_preview > div > img',
  '.lse_puzzle_wrapper > .lively_scene_image',
];

// Fallback values for pages that don't define their own — imagesSrcToReplace must be ''
// (not []) so processImagesSrcToReplace's !newSrc guard fires correctly.
const DEFAULT_VALUES = { cssToModify: [], imagesSrcToReplace: '' };

const PAGE_LIST = [
  {
    name : 'ALL',
    slug : '',
    selectors : {
      backgroundImagesSrcToHidePermanently : [
        '.bundle > #special-offer',
        '.bundle > #starter-offer',
        '#crosspromo_show_ad > .crosspromo_banner',
        '#crosspromo_show_localreward > .crosspromo_banner',
        '.mc-card-container > .rewards-container',
        '.product-offer-container > .product-offer-background-container',
      ],
      cssToModify : [],
      imagesSrcToReplace : [
        ...((REPLACE_BACKGROUND && !HIDE_BACKGROUND) ? BACKGROUND_SELECTORS : []),
      ],
      imagesSrcToHidePermanently : [
        '.background_image-style > img',
        '.background_image-style > source',
        '.video-background > .variant-video',
        '.intro > .quest-container > #scene > .canvas > .picture',
        '.background_image-style > img',
        '#no_energy_popup > .avatar',
        '.info-top-block > .bunny-rotate-device',
        '.container > .avatar',
        '.prestige > .avatar',
        '#special-offer > .background-video',
        '.pwa-info-container > .install_app_girl',
        ...(HIDE_ADDS ? [
          '.exo-native-widget',
          '.ad-revive-container',
        ] : []),
        ...(HIDE_BACKGROUND ? BACKGROUND_SELECTORS : []),
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.avatar-box > .avatar',
          '.awakening-container > .avatar',
        ] : []),
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.rewards > .girl-avatar',
        ] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? PLAYER_AVATAR_SELECTORS : []),
        ...(HIDE_VIDEO_PREVIEWS ? VIDEO_PREVIEW_SELECTORS : []),
      ],
      imagesToHideTemporarily : [],
    },
    values : {
      cssToModify : [],
      // Only supply the replacement URL when the setting is enabled.
      // processImagesSrcToReplace() guards on !newSrc, so an empty string
      // ensures no replacement happens even if a selector somehow slips through.
      imagesSrcToReplace : REPLACE_BACKGROUND ? NEW_BACKGROUND_URL : '',
    },
  },
  {
    name : 'ACTIVITIES',
    slug : '/activities.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : ['.contest > .contest_header'],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        '.mission_image > img',
        '.pop_thumb > img',
        '.pop-record > .pop-record-bg',
        '.timer-girl-container > img',
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.pop-details-left > img', '.pop_girl_avatar > img'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'ADVENTURES',
    slug : '/adventures.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : ['.adventure-card-container'],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'CHAMPIONS',
    slug : '/champions/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [
        ['.girl-information'],
        ['.nc-event-reward-info'],
        ['.champions-over__champion-rewards-outline'],
        ['.champions-over__champion-wrapper > .champions-over__champion-info'],
        ['.champions-over__champion-tier-link'],
        ['.champions-over__girl-image'],
      ],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        '.champions-over__champion-wrapper > .champions-over__champion-image',
        '.champions-over__champion-wrapper > .champions-over__champion-dialog-box',
        '.defender-preview > img',
        '.attacker-preview > .character',
        '.rounds-info__figures > .figure',
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.champions-over__champion-wrapper > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
    values : {
      cssToModify : [
        ['display: flex', 'position: relative', 'left: 200px', 'top: 0px'],
        ['top: 0px', 'left: 100px'],
        ['display: flex', 'position: absolute', 'left: -250px', 'top: 50px', 'width: 100%'],
        ['display: flex', 'position: relative', 'left: -250px', 'top: 100px'],
        ['display: inline-flex', 'width: 2.5rem', 'height: 2.5rem'],
        ['top: -60px', 'right: 50px'],
      ],
      imagesSrcToReplace : [],
    },
  },
  {
    name : 'CHARACTERS',
    slug : '/characters/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.avatar-box > .avatar',
          '.awakening-container > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'CLUB CHAMPION',
    slug : '/club-champion.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [
        ['.girl-information'],
        ['.nc-event-reward-info'],
        ['.champions-over__champion-rewards-outline'],
        ['.champions-over__champion-wrapper > .champions-over__champion-info'],
        ['.champions-over__champion-tier-link'],
        ['.champions-over__girl-image'],
      ],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        '.attacker-preview > .character',
        '.defender-preview > img',
        '.figure',
        '.champions-over__champion-wrapper > .champions-over__champion-image',
        '.champions-over__champion-wrapper > .champions-over__champion-dialog-box',
        '.girl-fav-position > .favorite-position',
        '.girl-card > .fav-position',
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.champions-over__champion-wrapper > .avatar',
        ] : []),
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.attacker-girl > .avatar',
          '.defender-girl > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
    values : {
      cssToModify : [
        ['display: flex', 'position: relative', 'left: 200px', 'top: 0px'],
        ['top: 0px', 'left: 100px'],
        ['display: flex', 'position: absolute', 'left: -250px', 'top: 50px', 'width: 100%'],
        ['display: flex', 'position: relative', 'left: -250px', 'top: 100px'],
        ['display: inline-flex', 'width: 2.5rem', 'height: 2.5rem'],
        ['top: -60px', 'right: 50px'],
      ],
      imagesSrcToReplace : [],
    },
  },
  {
    name : 'CLUBS',
    slug : '/clubs.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EDIT LABYRINTH TEAM',
    slug : '/edit-labyrinth-team.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EDIT TEAM',
    slug : '/edit-team.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EDIT WORLD BOSS TEAM',
    slug : '/edit-world-boss-team.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'EVENT',
    slug : '/event.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.column-girl > img',
          '.girls-container > .avatar',
          '.lse_girl_container > .avatar',
          '.right-container > .avatar',
          '.slide > .avatar',
          '.sm-static-girl > img',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'GIRL',
    slug : '/girl/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.awakening-container > .avatar',
          '.team-slot-container > img',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'GOD PATH',
    slug : '/god-path.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.feature-girl > .avatar'] : []),
        '.container-category > .feature-bgr',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'HAREM',
    slug : '/harem.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_HAREM_SELECTED_GIRL_AVATAR ? [
          '.avatar-box > .avatar',
          '.awakening-container > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'HOME',
    slug : '/home.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [
        '#special-offer',
        '.news_page_content > .news_page_pic',
        '.news_thumb > .news_thumb_pic',
      ],
      cssToModify : [],
      // Only include the selector when the setting is enabled — mirrors the ALL entry logic.
      imagesSrcToReplace : [...(REPLACE_BACKGROUND ? ['.fixed_scaled > img'] : [])],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.waifu-container > .avatar'] : []),
        '.info-top-block > .bunny-rotate-device',
        '.pwa-info-container > .install_app_girl',
      ],
      imagesToHideTemporarily : [],
    },
    values : {
      cssToModify : [],
      imagesSrcToReplace : REPLACE_BACKGROUND ? NEW_BACKGROUND_URL : '',
    },
  },
  {
    name : 'LABYRINTH',
    slug : '/labyrinth.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.labyrinth-girl > .avatar',
          '.shop-labyrinth-girl > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH BATTLE',
    slug : '/labyrinth-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.pvp-girls > .avatar',
          '.labyrinth-girl > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH ENTRANCE',
    slug : '/labyrinth-entrance.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.labyrinth-girl > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH POOL SELECT',
    slug : '/labyrinth-pool-select.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LABYRINTH PRE-BATTLE',
    slug : '/labyrinth-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.labyrinth-girl > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LEAGUES',
    slug : '/leagues.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl-block > .avatar'] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img', '.player-profile-picture > img'] : []),
        '.tier_icons > img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LEAGUE BATTLE',
    slug : '/league-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LEAGUE PRE-BATTLE',
    slug : '/league-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'LOVE RAIDS',
    slug : '/love-raids.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.left-girl-container > .avatar',
          '.left-girl-container > .girl-img',
          '.right-girl-container > .avatar',
          '.right-girl-container > .girl-img',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'MEMBER PROGRESSION',
    slug : '/member-progression.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : ['.page-girl > img'],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PACHINKO',
    slug : '/pachinko.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        '.pachinko_img > img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PANTHEON',
    slug : '/pantheon.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-container > .avatar'] : []),
        '.pantheon_bgr > .stage-bgr',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PANTHEON BATTLE',
    slug : '/pantheon-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PANTHEON PRE-BATTLE',
    slug : '/pantheon-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        '.fixed_scaled > img',
        '.player-profile-picture > img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PATH OF GLORY',
    slug : '/path-of-glory.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.left_side > .avatar',
          '.right_side > .avatar',
        ] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PATH OF VALOR',
    slug : '/path-of-valor.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? [
          '.left_side > .avatar',
          '.right_side > .avatar',
        ] : []),
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PENTA DRILL',
    slug : '/penta-drill.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl_block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PENTA DRILL ARENA',
    slug : '/penta-drill-arena.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl_block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PENTA DRILL BATTLE',
    slug : '/penta-drill-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.pvp-girls > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'PVP ARENA',
    slug : '/pvp-arena.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.feature-girl > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'QUEST',
    slug : '/quest/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : ['.canvas > .picture'],
    },
  },
  {
    name : 'SEASON',
    slug : '/season.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl_block > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SEASON ARENA',
    slug : '/season-arena.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SEASON BATTLE',
    slug : '/season-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SEASONAL',
    slug : '/seasonal.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_PLAYERS_AVATARS ? ['.square-avatar-wrapper > img'] : []),
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girls-reward-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'SIDE QUESTS',
    slug : '/side-quests.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : ['.side-quest-image > img'],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'TEAMS',
    slug : '/teams.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : ['.girl-image-container > img'],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'TROLL BATTLE',
    slug : '/troll-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.new-battle-girl-container > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'TROLL PRE-BATTLE',
    slug : '/troll-pre-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WAIFU',
    slug : '/waifu.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.girl-display > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD',
    slug : '/world/',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_EVENT_GIRLS_AVATARS ? ['.girl_world > .avatar'] : []),
        '.troll_world > .troll-tier-img',
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD BOSS BATTLE',
    slug : '/world-boss-battle.html',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? ['.pvp-girls > .avatar'] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD BOSS EVENT',
    slug : '/world-boss-event',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [
        ...(HIDE_OTHER_GIRLS_AVATARS ? [
          '.left-container > .avatar',
          '.right-container > .avatar',
        ] : []),
      ],
      imagesToHideTemporarily : [],
    },
  },
  {
    name : 'WORLD BOSS PRE-BATTLE',
    slug : '/world-boss-pre-battle',
    selectors : {
      backgroundImagesSrcToHidePermanently : [],
      cssToModify : [],
      imagesSrcToReplace : [],
      imagesSrcToHidePermanently : [],
      imagesToHideTemporarily : [],
    },
  },
];

// Pre-filter once at startup — only keep entries that match the current URL.
// The 'ALL' entry (empty slug) always matches. All others are tested against href.
const ACTIVE_PAGES = PAGE_LIST
  .filter(({slug}) => !slug || window.location.href.includes(slug))
  .map((page) => ({...page, values : page.values ?? DEFAULT_VALUES}));

/**
 * Injects a CSS rule hiding all matched selectors via the given CSS property.
 * Unified helper for both display:none and background-image:none cases.
 */
function injectCssHideRule(selectorsArray, cssProperty) {
  if (selectorsArray.length === 0) {
    return;
  }
  if (DEBUG_ACTIVATED) {
    console.log(`> INJECTING CSS HIDE RULE: ${cssProperty}`);
  }
  const style = document.createElement('style');
  style.textContent = `${selectorsArray.join(', ')} { ${cssProperty}: none !important; }\n`;
  document.head.prepend(style);
}

/**
 * Injects arbitrary CSS rules onto the given selectors.
 */
function modifyCssOfSelectors(selectorsArray, styleRules) {
  if (selectorsArray.length === 0) {
    return;
  }
  if (DEBUG_ACTIVATED) {
    console.log('> PROCESSING MODIFY CSS OF SELECTORS');
  }
  const style = document.createElement('style');
  style.textContent = `${selectorsArray.join(', ')} { ${styleRules.join(' !important; ')} !important; }\n`;
  document.head.prepend(style);
}

/**
 * Replaces the src of all matched elements with newSrc.
 */
function processImagesSrcToReplace(selectorsArray, newSrc) {
  if (selectorsArray.length === 0 || !newSrc) {
    return;
  }
  if (DEBUG_ACTIVATED) {
    console.log('> PROCESSING IMAGES SRC TO REPLACE');
  }
  const elements = document.querySelectorAll(selectorsArray.join(', '));
  if (DEBUG_ACTIVATED) {
    console.log('> nb of elements:', elements.length);
  }
  elements.forEach((element) => {
    if (element.src) {
      element.src = newSrc;
    }
  });
}

/**
 * Sets display style directly on matched elements.
 * Used for temporarily hiding (none) or showing again (block) elements.
 */
function setElementsDisplay(selectorsArray, displayValue) {
  if (selectorsArray.length === 0) {
    return;
  }
  if (DEBUG_ACTIVATED) {
    console.log(`> SETTING ELEMENTS DISPLAY: ${displayValue}`);
  }
  const elements = document.querySelectorAll(selectorsArray.join(', '));
  if (DEBUG_ACTIVATED) {
    console.log('> nb of elements:', elements.length);
  }
  elements.forEach((element) => {
    element.style.display = displayValue;
  });
}

/**
 * Increments the debug limit counter and disables debug logging once threshold is reached.
 */
function checkDebugLimit() {
  if (!DEBUG_LIMIT_ACTIVATED) {
    return;
  }
  debugLimitCount++;
  if (debugLimitCount > 3) {
    DEBUG_ACTIVATED = false;
  }
}

/**
 * Returns true if at least one user-controlled setting is enabled.
 * Used to short-circuit runAllHidingProcesses when everything is off.
 */
function isAnySettingEnabled() {
  return Object.values(HHSFW_DEFAULTS).some(Boolean);
}

/**
 * Unified page processing loop — runs all selectors for the current page.
 * ACTIVE_PAGES is pre-filtered at startup, so no slug-matching happens here.
 */
function runAllHidingProcesses() {
  ACTIVE_PAGES.forEach(({name, selectors, slug, values}) => {
    if (DEBUG_ACTIVATED) {
      console.log('> ')
      console.log(`> HIDING MEDIAS IN ${name} PAGE with SLUG: ${slug}`)
    }
    const {
      backgroundImagesSrcToHidePermanently,
      cssToModify,
      imagesSrcToHidePermanently,
      imagesSrcToReplace,
      imagesToHideTemporarily,
    } = selectors;

    // CSS injection — runs once only on the early call, before DOMContentLoaded
    if (!isCssInjected) {
      injectCssHideRule(backgroundImagesSrcToHidePermanently, 'background-image');
      injectCssHideRule(imagesSrcToHidePermanently, 'display');
      cssToModify.forEach((selectorGroup, i) => {
        modifyCssOfSelectors(selectorGroup, values.cssToModify[i]);
      });
    }

    // DOM manipulation — skipped on the early call, only runs after DOMContentLoaded
    if (isDOMReady) {
      processImagesSrcToReplace(imagesSrcToReplace, values.imagesSrcToReplace);
      setElementsDisplay(imagesToHideTemporarily, 'none');
    }
  });
}

/**
 * Unified page processing loop — runs all selectors for the current page.
 * ACTIVE_PAGES is pre-filtered at startup, so no slug-matching happens here.
 */
function runAllRevealingProcesses() {
  if (!isDOMReady) {
    return;
  }
  ACTIVE_PAGES.forEach(({ name, selectors, slug }) => {
    if (DEBUG_ACTIVATED) {
      console.log('> ');
      console.log(`> REVEALING MEDIAS IN ${name} PAGE with SLUG: ${slug}`);
    }
    setElementsDisplay(selectors.imagesToHideTemporarily, 'block');
  });
}

// Add event listener for clicks
document.addEventListener('click', function (event) {
  if (
    event.target.classList.contains('diamond') ||
    event.target.classList.contains('speech_bubble_info_icn') ||
    (event.target.parentElement &&
      event.target.parentElement.classList.contains('eye') &&
      window.location.href.includes('/quest/'))
  ) {
    if (DEBUG_ACTIVATED) {
      console.log('> ');
      console.log('> SPECIAL BUTTON CLICKED (IMG PROCESSING STOPPED)');
    }
    runAllRevealingProcesses();
  }
});

/**
 * Saves the current HHSFW_DEFAULTS state to localStorage.
 */
function saveHhsfwSettings() {
  try {
    localStorage.setItem('HHSFW.settings', JSON.stringify(HHSFW_DEFAULTS));
  } catch (e) {
    /* localStorage unavailable — skip silently */
  }
}

/**
 * Builds and injects the HHSFW settings panel and its toggle button into
 * #contains_all. Only runs on /home.html.
 *
 * The panel mirrors the style of #hhsOptions from the HHS script:
 * a tooltip div shown/hidden by clicking the logo image.
 */
function injectHhsButtonSibling() {
  // Only inject on the home page
  if (!window.location.href.includes('/home.html')) {
    return;
  }

  const container = document.getElementById('contains_all');
  if (!container) {
    if (DEBUG_ACTIVATED) {
      console.log('> #contains_all not found — not injecting HHSFW logo + settings panel');
    }
    return;
  }

  const ocdButton = document.getElementById('hhsButton');
  if (DEBUG_ACTIVATED) {
    console.log('> ocdButton:', ocdButton);
  }

  // Ensure the container is a positioning context
  if (window.getComputedStyle(container).position === 'static') {
    container.style.position = 'relative';
  }

  // ── Styles ────────────────────────────────────────────────────────────────
  // Media query breakpoints — mirrors OCD.js constants
  const mediaDesktop = '@media only screen and (min-width: 1026px)';
  const mediaMobile  = '@media only screen and (max-width: 1025px)';

  // Detect sibling scripts that affect layout, same way OCD.js does.
  // Store the element directly to avoid a second querySelector later.
  const hhPlusPlusButton = document.querySelector('.hh-plus-plus-config-button');
  const hasHhPlusPlusButton = hhPlusPlusButton !== null;

  // Detect OCD.js "customizedHomeScreen" setting from its localStorage key
  let customizedHomeScreen = false;
  try {
    customizedHomeScreen = localStorage.getItem('HHS.customizedHomeScreen') === 'true';
  } catch (e) {
    /* localStorage unavailable — keep false */
  }

  // Build the sheet-based dynamic stylesheet, mirroring every #hhsButton
  // rule from OCD.js 1-for-1, but targeting #hhsfwToggle instead.
  const dynamicStyle = document.createElement('style');
  document.head.appendChild(dynamicStyle);
  const sheet = dynamicStyle.sheet;

  // ── Base rule (always applied) ────────────────────────────────────────────
  // Mirrors: sheet.insertRule(`#hhsButton { height:35px; position:absolute;
  //   z-index:10; filter:drop-shadow(0px 0px 5px white); }`)
  sheet.insertRule(`#hhsfwToggle {
    height: 35px;
    position: absolute;
    z-index: 10;
    filter: drop-shadow(0px 0px 5px white);
    cursor: pointer;
  }`);

  // ── Positional rules — derived from sibling button positions at runtime ──────
  //
  // Priority order (mirrors the way OCD.js stacks its own buttons):
  //   1. ocdButton (#hhsButton) is present  → position relative to it
  //   2. .hh-plus-plus-config-button present → position relative to it
  //   3. Neither present                     → fall back to static CSS rules
  //
  // For cases 1 & 2 we read the sibling's computed position inside the shared
  // #contains_all container (which is already a positioning context at this
  // point) and apply inline styles directly on the img element.  Inline styles
  // are set after the img is appended to the DOM (see the positioning block
  // below the img creation).  We stash the reference and a flag here so the
  // later block knows what to do.

  let positionAnchor = null; // Element whose position we'll mirror
  let useStaticCss   = false;

  if (ocdButton) {
    // Case 1: OCD.js button is in the DOM — anchor to it
    positionAnchor = ocdButton;
    if (DEBUG_ACTIVATED) {
      console.log('> [HHSFW] positioning relative to #hhsButton');
    }
  } else if (hasHhPlusPlusButton) {
    // Case 2: HH++ button is in the DOM — anchor to it
    positionAnchor = hhPlusPlusButton;
    if (DEBUG_ACTIVATED) {
      console.log('> [HHSFW] positioning relative to .hh-plus-plus-config-button');
    }
  } else {
    // Case 3: no sibling buttons found — use static responsive CSS
    useStaticCss = true;
    if (DEBUG_ACTIVATED) {
      console.log('> [HHSFW] no sibling button found — using static CSS fallback');
    }
  }

  if (useStaticCss) {
    // Static fallback — mirrors the OCD.js rules for the no-sibling-button case
    if (customizedHomeScreen) {
      // OCD branch: NO .hh-plus-plus-config-button AND customizedHomeScreen ON
      sheet.insertRule(`${mediaDesktop} {
        #hhsfwToggle {
          right: 42px;
          top: 100px;
        }
      }`);
      sheet.insertRule(`${mediaMobile} {
        #hhsfwToggle {
          right: 125px;
          top: 85px;
        }
      }`);
    } else {
      // OCD branch: NO .hh-plus-plus-config-button AND customizedHomeScreen OFF
      sheet.insertRule(`${mediaDesktop} {
        #hhsfwToggle {
          right: 42px;
          top: 90px;
        }
      }`);
      sheet.insertRule(`${mediaMobile} {
        #hhsfwToggle {
          right: 130px;
          top: 85px;
        }
      }`);
    }
  }

  // ── Static panel + UI styles (unchanged) ─────────────────────────────────
  const style = document.createElement('style');
  style.textContent = `
    #hhsfwPanel {
      display: none;
      position: absolute;
      z-index: 100;
      background: #1a1a2e;
      border: 1px solid #e91e8c;
      border-radius: 8px;
      padding: 14px 18px 10px;
      min-width: 280px;
      box-shadow: 0 4px 24px rgba(0,0,0,0.7);
      font-family: Arial, sans-serif;
      font-size: 13px;
      color: #f0f0f0;
    }
    #hhsfwPanel .hhsfw-title {
      font-weight: bold;
      font-size: 14px;
      color: #e91e8c;
      margin-bottom: 10px;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    #hhsfwPanel .hhsfw-close {
      cursor: pointer;
      font-size: 16px;
      color: #aaa;
      line-height: 1;
    }
    #hhsfwPanel .hhsfw-close:hover { color: #fff; }
    #hhsfwPanel .script_setting {
      display: flex;
      flex-direction: row;
      align-items: center;
      gap: 10px;
      margin-bottom: 8px;
    }
    #hhsfwPanel .script_setting span {
      display: flex;
      align-items: center;
      line-height: 1;
    }
    #hhsfwPanel .switch {
      position: relative;
      display: inline-block;
      width: 36px;
      height: 20px;
      min-width: 36px;
      min-height: 20px;
      flex-shrink: 0;
      top: 0px;
    }
    #hhsfwPanel .switch input { opacity: 0; width: 0; height: 0; }
    #hhsfwPanel .slider {
      position: absolute;
      cursor: pointer;
      inset: 0;
      background: #444;
      border-radius: 20px;
      transition: background 0.2s;
      margin-right: 0px;
    }
    #hhsfwPanel .slider:before {
      content: '';
      position: absolute;
      width: 14px;
      height: 14px;
      left: 3px;
      bottom: 3px;
      background: #fff;
      border-radius: 50%;
      transition: transform 0.2s;
    }
    #hhsfwPanel .switch input:checked + .slider { background: #e91e8c; }
    #hhsfwPanel .switch input:checked + .slider:before { transform: translateX(16px); }
    #hhsfwPanel .hhsfw-note {
      font-size: 11px;
      color: #888;
      margin-top: 8px;
      border-top: 1px solid #333;
      padding-top: 6px;
    }
  `;
  document.head.appendChild(style);

  // ── Settings panel ────────────────────────────────────────────────────────
  const panel = document.createElement('div');
  panel.id = 'hhsfwPanel';
  panel.innerHTML = `
    <div class="hhsfw-title">
      Hentai Heroes SFW
      <span class="hhsfw-close" id="hhsfwClose">✕</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_EVENT_GIRLS_AVATARS" ${HHSFW_DEFAULTS.HIDE_EVENT_GIRLS_AVATARS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide event girls' avatars</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_HAREM_SELECTED_GIRL_AVATAR" ${HHSFW_DEFAULTS.HIDE_HAREM_SELECTED_GIRL_AVATAR ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide harem selected girl avatar</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_OTHER_GIRLS_AVATARS" ${HHSFW_DEFAULTS.HIDE_OTHER_GIRLS_AVATARS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide other girls' avatars</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_OTHER_PLAYERS_AVATARS" ${HHSFW_DEFAULTS.HIDE_OTHER_PLAYERS_AVATARS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide other players' avatars</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-HIDE_VIDEO_PREVIEWS" ${HHSFW_DEFAULTS.HIDE_VIDEO_PREVIEWS ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Hide video previews</span>
    </div>
    <div class="script_setting">
      <label class="switch">
        <input type="checkbox" id="hhsfw-REPLACE_BACKGROUND" ${HHSFW_DEFAULTS.REPLACE_BACKGROUND ? 'checked' : ''}>
        <span class="slider"></span>
      </label>
      <span>Change background to SFW one</span>
    </div>
    <div class="hhsfw-note">Changes take effect on next page load.</div>
  `;
  container.appendChild(panel);
  // Set the initial display via inline style so the toggle handler's
  // panel.style.display check always compares against a known inline value.
  // Without this, style.display starts as '' (empty) even though the CSS rule
  // says display:none — causing the first click to set it to 'none' instead of 'block'.
  panel.style.display = 'none';

  // ── Toggle button (logo image) ────────────────────────────────────────────
  const img = document.createElement('img');
  img.id = 'hhsfwToggle';
  img.src = isAnySettingEnabled() ? 'https://i.postimg.cc/d3QRs43j/SFW_logo.png' : 'https://i.postimg.cc/7hP1HmhM/NSFW_logo.png';
  img.title = 'HHSFW Settings';
  container.appendChild(img);

  // ── Anchor-based inline positioning (cases 1 & 2) ────────────────────────
  // offsetTop / offsetLeft are already relative to offsetParent, which is
  // #contains_all (a positioned element) — so they map directly to the
  // `top` / `left` values needed for `position: absolute` without any rect
  // arithmetic.  `right` is derived as containerWidth - anchorLeft - anchorWidth
  // then shifted 40px further left.
  if (positionAnchor) {
    const anchorTop      = positionAnchor.offsetTop;
    const anchorLeft     = positionAnchor.offsetLeft;
    const anchorWidth    = positionAnchor.offsetWidth;
    const containerWidth = container.offsetWidth;

    // Convert the anchor's left edge to a `right` value, then shift 40px left
    const anchorRight = containerWidth - anchorLeft - anchorWidth;

    const topOffset = hasHhPlusPlusButton && !ocdButton ? 70 : 0;
    const rightOffset = ocdButton ? 70 : 0;
    img.style.top   = `${anchorTop + topOffset}px`;
    img.style.right = `${anchorRight + rightOffset}px`;
  }

  // ── Event listeners ───────────────────────────────────────────────────────
  img.addEventListener('click', function () {
    if (panel.style.display === 'none') {
      // Measure panel width before showing it
      panel.style.visibility = 'hidden';
      panel.style.display = 'block';

      // img.offsetTop/offsetLeft are already relative to #contains_all
      // (the nearest positioned ancestor), so they map directly to
      // `top`/`left` values for `position: absolute` inside that container.
      // Bottom of toggle = offsetTop + offsetHeight
      // Left of panel so its right edge aligns with toggle's left edge:
      //   panelLeft = img.offsetLeft - panel.offsetWidth
      const panelTop  = img.offsetTop  + img.offsetHeight;
      const panelLeft = img.offsetLeft - panel.offsetWidth;

      panel.style.top   = panelTop  + 'px';
      panel.style.left  = panelLeft + 'px';
      panel.style.right = '';

      panel.style.visibility = '';
    } else {
      panel.style.display = 'none';
    }
  });

  document.getElementById('hhsfwClose').addEventListener('click', function () {
    panel.style.display = 'none';
  });

  [
    'HIDE_EVENT_GIRLS_AVATARS',
    'HIDE_HAREM_SELECTED_GIRL_AVATAR',
    'HIDE_OTHER_GIRLS_AVATARS',
    'HIDE_OTHER_PLAYERS_AVATARS',
    'HIDE_VIDEO_PREVIEWS',
    'REPLACE_BACKGROUND'
  ].forEach(function (key) {
    document.getElementById('hhsfw-' + key).addEventListener('change', function (e) {
      HHSFW_DEFAULTS[key] = e.target.checked;
      saveHhsfwSettings();
    });
  });
}

// DOM is ready, resources may still be loading.
// Set isDOMReady so that processImagesSrcToReplace and setElementsDisplay are now allowed to run.
// CSS injection is skipped on this second call (isCssInjected is already true).
document.addEventListener('DOMContentLoaded', function () {
  if (DEBUG_ACTIVATED) {
    console.log('> ');
    console.log('> DOMContentLoaded');
  }

  if (isAnySettingEnabled()) {
    isDOMReady = true;
    runAllHidingProcesses();
  }
});

// All resources (including images) have fully loaded — safe to query elements
// that are injected late by the game. This is the last event in the page
// loading lifecycle, fired after DOMContentLoaded and all sub-resources.
window.addEventListener('load', function () {
  if (DEBUG_ACTIVATED) {
    console.log('> ');
    console.log('> window load');
  }

  // Delay so sibling scripts (OCD.js, HH++) have time to inject their
  // own buttons into the DOM before we read their positions.
  setTimeout(function () {
    injectHhsButtonSibling();
  }, 600);
});

// Run immediately at script start — CSS injection only (isDOMReady is false, DOM manipulation is skipped)
if (isAnySettingEnabled()) {
  checkDebugLimit();
  runAllHidingProcesses();
  isCssInjected = true;
}