AO3: Reading Time & Quality Score

Add reading time, chapter reading time, and quality scores to AO3 works with color coding, score normalization and sorting.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        AO3: Reading Time & Quality Score
// @version     3.8
// @description  Add reading time, chapter reading time, and quality scores to AO3 works with color coding, score normalization and sorting.
// @author      BlackBatCat
// @match       *://archiveofourown.org/
// @match       *://archiveofourown.org/tags/*/works*
// @match       *://archiveofourown.org/works*
// @match       *://archiveofourown.org/chapters/*
// @match       *://archiveofourown.org/users/*
// @match       *://archiveofourown.org/collections/*
// @match       *://archiveofourown.org/bookmarks*
// @match       *://archiveofourown.org/series/*
// @license     MIT
// @require     https://update.greasyfork.org/scripts/554170/1693013/AO3%3A%20Menu%20Helpers%20Library%20v2.js?v=2.1.6
// @grant       none
// @namespace https://greasyfork.org/users/1498004
// ==/UserScript==

(function () {
  "use strict";

  // DEFAULT CONFIGURATION
  const DEFAULTS = {
    enableReadingTime: true,
    enableQualityScore: true,
    enableChapterStats: true,
    wpm: 375,
    alwaysCountReadingTime: true,
    readingTimeLvl1: 120,
    readingTimeLvl2: 360,
    alwaysCountQualityScore: true,
    alwaysSortQualityScore: false,
    excludeMyContentFromSort: false,
    hideMetrics: false,
    hideHits: false,
    hideKudos: false,
    hideBookmarks: false,
    hideComments: false,
    useNormalization: false,
    userMaxScore: 32,
    minKudosToShowScore: 100,
    colorThresholdLow: 10,
    colorThresholdHigh: 20,
    colorStyle: "background",
    colorGreen: "#3e8fb0",
    colorYellow: "#f6c177",
    colorRed: "#eb6f92",
    colorText: "#ffffff",
    useIcons: false,
    iconColor: "",
    chapterTimeStyle: "default",
    username: "",
    hideWorksEnabled: false,
    hideWorksScore: 15,
    keepUnscoredVisible: false,
  };

  let CONFIG = { ...DEFAULTS };
  let countable = false;
  let sortable = false;
  let statsPage = false;

  const $ = (selector, root = document) => root.querySelectorAll(selector);
  const $1 = (selector, root = document) => root.querySelector(selector);

  const loadUserSettings = () => {
    if (typeof Storage === "undefined") return;
    const savedConfig = localStorage.getItem("ao3_reading_quality_config");
    if (savedConfig) {
      try {
        const parsedConfig = JSON.parse(savedConfig);
        CONFIG = { ...DEFAULTS, ...parsedConfig };
      } catch (e) {
        console.error("Error loading saved config, using defaults:", e);
        CONFIG = { ...DEFAULTS };
      }
    }
  };

  const saveAllSettings = () => {
    if (typeof Storage !== "undefined") {
      localStorage.setItem(
        "ao3_reading_quality_config",
        JSON.stringify(CONFIG)
      );
    }
  };

  function saveSetting(key, value) {
    CONFIG[key] = value;
    saveAllSettings();
  }

  const resetAllSettings = () => {
    if (confirm("Reset all settings to defaults?")) {
      if (typeof Storage !== "undefined") {
        localStorage.removeItem("ao3_reading_quality_config");
      }
      CONFIG = { ...DEFAULTS };
      if (
        (CONFIG.enableReadingTime || CONFIG.enableQualityScore) &&
        countable
      ) {
        calculateMetrics(null, false, true);
      }
      if (CONFIG.enableChapterStats) calculateChapterStats();
    }
  };

  const detectAndStoreUsername = () => {
    let username = null;
    const userLink = document.querySelector(
      'li.user.logged-in a[href^="/users/"]'
    );
    if (userLink) {
      const match = userLink.getAttribute("href").match(/^\/users\/([^\/]+)/);
      if (match) username = match[1];
    }
    if (!username && CONFIG.username) {
      username = CONFIG.username;
    }
    if (!username) {
      const urlMatch = window.location.pathname.match(/^\/users\/([^\/]+)/);
      if (urlMatch) username = urlMatch[1];
    }
    if (!username) {
      const params = new URLSearchParams(window.location.search);
      const paramUserId = params.get("user_id");
      if (paramUserId) username = paramUserId;
    }
    if (username && username !== CONFIG.username) {
      saveSetting("username", username);
    }
    return username;
  };

  const USERNAME_PATTERNS = {
    userPath:
      /^\/users\/([^\/]+)(?:\/pseuds\/[^\/]+)?(?:\/(bookmarks|works))?(?:\/|$)/,
    readings: /^\/users\/([^\/]+)\/readings(?:\/|$)/,
  };

  const numberRegex = /[\d,]+/;
  const cleanNumberRegex = /[^\d]/g;

  const isMyContentPage = (username) => {
    if (!username) return false;
    const escapedUsername = username.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

    const userPathPattern = new RegExp(
      `^/users/${escapedUsername}(?:/pseuds/[^/]+)?(?:/(bookmarks|works))?(?:/|$)`
    );
    const readingsPattern = new RegExp(
      `^/users/${escapedUsername}/readings(?:/|$)`
    );

    if (
      userPathPattern.test(window.location.pathname) ||
      readingsPattern.test(window.location.pathname)
    ) {
      return true;
    }
    if (window.location.pathname.startsWith("/bookmarks")) {
      const params = new URLSearchParams(window.location.search);
      const paramUserId = params.get("user_id");
      if (paramUserId && paramUserId.toLowerCase() === username.toLowerCase()) {
        return true;
      }
    }
    return false;
  };

  const getNumberFromElement = (element) => {
    if (!element) return NaN;
    const text = element.textContent;
    if (!text) return NaN;

    if (element.matches("dd.chapters")) {
      const match = text.match(/^(\d+)/);
      if (!match) return NaN;
      return parseInt(match[1], 10);
    }

    const match = text.match(numberRegex);
    if (!match) return NaN;
    const cleaned = match[0].replace(cleanNumberRegex, "");
    const number = parseInt(cleaned, 10);
    return isNaN(number) ? NaN : number;
  };

  const applyColorStyling = (element, color) => {
    if (CONFIG.colorStyle === "background") {
      element.style.backgroundColor = color;
      element.style.color = CONFIG.colorText;
      element.style.padding = "0 4px";
    } else if (CONFIG.colorStyle === "text") {
      element.style.color = color;
      element.style.backgroundColor = "";
      element.style.padding = "";
    } else {
      element.style.backgroundColor = "";
      element.style.color = "inherit";
      element.style.padding = "";
    }
  };

  const addIconStyles = () => {
    if (document.getElementById("ao3-userscript-icon-styles")) return;

    const style = document.createElement("style");
    style.id = "ao3-userscript-icon-styles";
    const iconColor = CONFIG.iconColor || "currentColor";

    const readingTimeIcon =
      "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PGcgdHJhbnNmb3JtPSJtYXRyaXgoMS4yLDAsMCwxLjIsLTIuNCwtMi40KSI+PHBhdGggZmlsbD0iIzAwMDAwMCIgZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik05LjY2OCAxMy4zNjlhMS44ODQgMS44ODQgMCAwIDAgMC0yLjczOGwtLjEwOC0uMTAyQzcuNTQ1IDguNjA1IDUuOTg4IDcuMTIgNS41MiA0LjAwNyA1LjM1MiAyLjkxIDYuMjkyIDIgNy40MjQgMmg5LjE1YzEuMTMyIDAgMi4wNzIuOTEgMS45MDYgMi4wMDctLjQ3IDMuMTEzLTIuMDI2IDQuNTk4LTQuMDQxIDYuNTIybC0uMTA3LjEwMmExLjg4NCAxLjg4NCAwIDAgMCAwIDIuNzM4bC4xMDcuMTAyYzIuMDE1IDEuOTI0IDMuNTcyIDMuNDA5IDQuMDQxIDYuNTIyLjE2NiAxLjA5Ny0uNzc0IDIuMDA3LTEuOTA2IDIuMDA3aC05LjE1Yy0xLjEzMiAwLTIuMDcyLS45MS0xLjkwNi0yLjAwNy40Ny0zLjExMyAyLjAyNi00LjU5OCA0LjA0MS02LjUyMnptLjY4MyAxLjY5OC0uMDA4LjAwNmMtMS41MzUgMS4zNzMtMi42NzggMi4zOTUtMi44MjcgNC45MjNhLjQ2OC40NjggMCAwIDAgLjE2OC4zODguNDkzLjQ5MyAwIDAgMCAuMzIuMTE2aDcuOTkyYy4xNDQgMCAuMjc0LS4wNi4zNjMtLjE1OGEuNDY2LjQ2NiAwIDAgMCAuMTI0LS4zNDZjLS4xNDktMi41MjgtMS4yOTEtMy41NS0yLjgyNi00LjkyMy0uNDA2LS4zNjMtLjg0LS43NTEtMS4yOS0xLjE5OGEuNTIzLjUyMyAwIDAgMC0uNzM1IDBjLS40NDcuNDQ0LS44NzguODMtMS4yODEgMS4xOTF6Ii8+PC9nPjwvc3ZnPg==";

    const scoreIcon =
      "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MTIgNTEyIj48cGF0aCBmaWxsPSIjMDAwMDAwIiBkPSJNMjk4LjEzOCAxMzYuNjY1Yy02Mi4wNjUtMTMuMDExLTExMC41NzYtNjEuNTIyLTEyMy41ODUtMTIzLjU4OGExNi40NTUgMTYuNDU1IDAgMCAwLTMyLjIwOS4wMDFjLTEzLjAxIDYyLjA2NS02MS41MjEgMTEwLjU3NS0xMjMuNTg2IDEyMy41ODRhMTYuNDU1IDE2LjQ1NSAwIDAgMCAwIDMyLjIwOGM2Mi4wNjQgMTMuMDExIDExMC41NzMgNjEuNTIxIDEyMy41ODMgMTIzLjU4NmExNi40NTQgMTYuNDU0IDAgMCAwIDMyLjIwOCAwYzEzLjAxMS02Mi4wNjUgNjEuNTIzLTExMC41NzUgMTIzLjU4OC0xMjMuNTgzYTE2LjQ1NCAxNi40NTQgMCAwIDAgLjAwMS0zMi4yMDh6TTI3MC45MzggNDA4LjQ4NGMtMjkuMjQyLTYuMTI5LTUyLjA5OC0yOC45ODUtNTguMjI5LTU4LjIyOWExNi40NTQgMTYuNDU0IDAgMCAwLTMyLjIwOC0uMDAxYy02LjEzMSAyOS4yNDMtMjguOTg4IDUyLjA5OS01OC4yMyA1OC4yMjlhMTYuNDU1IDE2LjQ1NSAwIDAgMCAwIDMyLjIwOGMyOS4yNDEgNi4xMyA1Mi4wOTggMjguOTg3IDU4LjIyOCA1OC4yM2ExNi40NTQgMTYuNDU0IDAgMCAwIDMyLjIwOCAwYzYuMTMxLTI5LjI0MyAyOC45ODgtNTIuMDk5IDU4LjIzMS01OC4yMjlhMTYuNDU1IDE2LjQ1NSAwIDAgMCAwLTMyLjIwOHpNNDkzLjI0MyAyNTYuMTM1Yy0zOS41MjYtOC4yODYtNzAuNDE5LTM5LjE4LTc4LjcwNC03OC43MDVhMTYuNDU0IDE2LjQ1NCAwIDAgMC0zMi4yMDgtLjAwMWMtOC4yODYgMzkuNTI2LTM5LjE3OSA3MC40MTktNzguNzA1IDc4LjcwNGExNi40NTUgMTYuNDU1IDAgMCAwIDAgMzIuMjA4YzM5LjUyNSA4LjI4NiA3MC40MTggMzkuMTc5IDc4LjcwMyA3OC43MDVhMTYuNDU0IDE2LjQ1NCAwIDAgMCAzMi4yMDggMGM4LjI4Ny0zOS41MjYgMzkuMTgtNzAuNDE5IDc4LjcwNS03OC43MDNhMTYuNDU0IDE2LjQ1NCAwIDAgMCAuMDAxLTMyLjIwOHoiLz48L3N2Zz4=";

    style.textContent = `
      .stats dd.readtime::before,
      dl.statistics dt.readtime::before {
        display: inline-block !important;
        width: 1em !important;
        height: 1em !important;
        min-width: 1em !important;
        min-height: 1em !important;
        margin-right: 5px !important;
        background-color: ${iconColor} !important;
        ${CONFIG.iconColor ? "filter: none !important;" : ""}
        -webkit-mask-image: url("${readingTimeIcon}") !important;
        mask-image: url("${readingTimeIcon}") !important;
        -webkit-mask-size: contain !important;
        mask-size: contain !important;
        -webkit-mask-repeat: no-repeat !important;
        mask-repeat: no-repeat !important;
        -webkit-mask-position: center center !important;
        mask-position: center center !important;
        content: "" !important;
        transform: translate(0, 1px) !important;
      }
      .stats dd.kudoshits::before,
      dl.statistics dt.kudoshits::before {
        display: inline-block !important;
        width: 1em !important;
        height: 1em !important;
        min-width: 1em !important;
        min-height: 1em !important;
        margin-right: 5px !important;
        background-color: ${iconColor} !important;
        ${CONFIG.iconColor ? "filter: none !important;" : ""}
        -webkit-mask-image: url("${scoreIcon}") !important;
        mask-image: url("${scoreIcon}") !important;
        -webkit-mask-size: contain !important;
        mask-size: contain !important;
        -webkit-mask-repeat: no-repeat !important;
        mask-repeat: no-repeat !important;
        -webkit-mask-position: center center !important;
        mask-position: center center !important;
        content: "" !important;
        transform: translate(0, 1px) !important;
      }
      dl.stats dd {
        justify-content: center;
        position: relative;
      }
      .stats dd.readtime::after {
        display: none;
        position: absolute;
        top: 2em;
        left: 50%;
        transform: translateX(-50%);
        z-index: 99999;
        padding: 0.3em;
        font-size: 0.8em;
        line-height: 1;
        text-align: center;
        content: "Time";
        white-space: nowrap;
        pointer-events: none;
      }
      .stats dd.kudoshits::after {
        display: none;
        position: absolute;
        top: 2em;
        left: 50%;
        transform: translateX(-50%);
        z-index: 99999;
        padding: 0.3em;
        font-size: 0.8em;
        line-height: 1;
        text-align: center;
        content: "Score";
        white-space: nowrap;
        pointer-events: none;
      }
      .stats dd:hover::after {
        display: inline-block;
      }
      .statistics .stats dd:last-of-type::after,
      .index .stats dd:last-of-type:has(a[href$=bookmarks])::after,
      .stats dd.inspired::after,
      .tagset .index .stats dd:last-of-type::after {
        right: 0;
        left: auto;
        transform: none;
      }
      .stats a,
      .stats a:visited {
        border: none;
        color: inherit;
      }
      .stats dt.readtime,
      .stats dt.kudoshits,
      dl.statistics dt.readtime,
      dl.statistics dt.kudoshits {
        font-size: 0 !important;
        line-height: 0 !important;
      }
      dl.statistics dt.readtime::before,
      dl.statistics dt.kudoshits::before {
        font-size: 1rem !important;
        line-height: normal !important;
      }
      .notice.ao3-chapter-stats {
        list-style: none;
      }
      .notice.ao3-chapter-stats li {
        list-style: none;
        margin: 0;
      }
      .ao3-chapter-stats-default,
      .ao3-chapter-stats-timeonly {
        font-style: italic;
        text-align: center;
        opacity: 0.9;
        margin: 1em 0;
        font-size: 1.2em;
      }
    `;
    if (document.head) {
      document.head.appendChild(style);
    }
  };

  const checkCountable = () => {
    const foundStats = $("dl.stats");
    if (foundStats.length === 0) return;
    for (const stat of foundStats) {
      const li = stat.closest("li.work, li.bookmark");
      if (li) {
        countable = true;
        sortable = true;
        return;
      }
      if (stat.closest(".statistics")) {
        countable = true;
        sortable = true;
        statsPage = true;
        return;
      }
      if (stat.closest("dl.work")) {
        countable = true;
        return;
      }
    }
  };

  const calculateWordBasedScore = (kudos, hits, words) => {
    if (hits === 0 || words === 0 || kudos === 0) return 0;
    const effectiveChapters = words / 5000;
    const adjustedHits = hits / Math.sqrt(effectiveChapters);
    return (100 * kudos) / adjustedHits;
  };

  const calculateMetrics = (
    statsElements = null,
    forceRecalculation = false,
    allowCalculation = true
  ) => {
    if (!countable) return;
    if (
      !CONFIG.enableReadingTime &&
      !CONFIG.enableQualityScore &&
      !CONFIG.hideMetrics
    )
      return;

    if (CONFIG.hideWorksEnabled) {
    }

    const normalizedThresholdLow = CONFIG.useNormalization
      ? Math.ceil((CONFIG.colorThresholdLow / CONFIG.userMaxScore) * 100)
      : CONFIG.colorThresholdLow;
    const normalizedThresholdHigh = CONFIG.useNormalization
      ? Math.ceil((CONFIG.colorThresholdHigh / CONFIG.userMaxScore) * 100)
      : CONFIG.colorThresholdHigh;

    const allStats = statsElements || Array.from($("dl.stats"));
    allStats.forEach((statsElement) => {
      const parentLi = statsElement.closest("li.work, li.bookmark");
      const wordsElement = $1("dd.words", statsElement);
      if (!wordsElement) return;

      const words = getNumberFromElement(wordsElement);
      if (isNaN(words)) return;

      const readtimeDt = $1("dt.readtime", statsElement);
      const kudoshitsDt = $1("dt.kudoshits", statsElement);
      const hitsElement = $1("dd.hits", statsElement);
      const kudosElement = $1("dd.kudos", statsElement);
      const bookmarksElement = $1("dd.bookmarks", statsElement);
      const commentsElement = $1("dd.comments", statsElement);
      const hitsLabel = $1("dt.hits", statsElement);
      const kudosLabel = $1("dt.kudos", statsElement);
      const bookmarksLabel = $1("dt.bookmarks", statsElement);
      const commentsLabel = $1("dt.comments", statsElement);

      const needsReadingTime =
        allowCalculation && CONFIG.enableReadingTime && !readtimeDt;
      const needsScore =
        allowCalculation &&
        CONFIG.enableQualityScore &&
        (!kudoshitsDt || forceRecalculation);
      const needsHiding = CONFIG.hideMetrics && !statsPage;
      const needsWorkHiding = CONFIG.hideWorksEnabled;

      if (!needsReadingTime && !needsScore && !needsHiding && !needsWorkHiding)
        return;

      if (needsReadingTime) {
        const minutes = words / CONFIG.wpm;
        const hrs = Math.floor(minutes / 60);
        const mins = (minutes % 60).toFixed(0);
        const minutes_print = hrs > 0 ? hrs + "h" + mins + "m" : mins + "m";

        let color;
        if (minutes < CONFIG.readingTimeLvl1) {
          color = CONFIG.colorGreen;
        } else if (minutes < CONFIG.readingTimeLvl2) {
          color = CONFIG.colorYellow;
        } else {
          color = CONFIG.colorRed;
        }

        let ddStyle = "display: inline-block; vertical-align: baseline;";
        let spanStyle =
          "border-radius: 4px; display: inline-block; vertical-align: baseline; font-size: inherit; line-height: inherit;";

        if (CONFIG.colorStyle === "background") {
          spanStyle += ` background-color: ${color}; color: ${CONFIG.colorText}; padding: 0 4px;`;
        } else if (CONFIG.colorStyle === "text") {
          spanStyle += ` color: ${color};`;
        }

        if (CONFIG.useIcons) {
          wordsElement.insertAdjacentHTML(
            "afterend",
            `<dt class="readtime"></dt><dd class="readtime" style="${ddStyle}"><span style="${spanStyle}">${minutes_print}</span></dd>`
          );
        } else {
          ddStyle +=
            " border-radius: 4px; font-size: inherit; line-height: inherit;";
          if (CONFIG.colorStyle === "background") {
            ddStyle += ` background-color: ${color}; color: ${CONFIG.colorText}; padding: 0 4px;`;
          } else if (CONFIG.colorStyle === "text") {
            ddStyle += ` color: ${color};`;
          }
          wordsElement.insertAdjacentHTML(
            "afterend",
            `<dt class="readtime">Time:</dt><dd class="readtime" style="${ddStyle}">${minutes_print}</dd>`
          );
        }
      }

      if (needsScore) {
        const existingScoreElement = $1("dd.kudoshits", statsElement);
        if (existingScoreElement && !forceRecalculation) {
          if (parentLi && !parentLi.hasAttribute("kudospercent")) {
            const scoreText = existingScoreElement.textContent.trim();
            parentLi.setAttribute("kudospercent", parseFloat(scoreText));
          }
          return;
        }
        try {
          const hits = getNumberFromElement(hitsElement);
          const kudos = getNumberFromElement(kudosElement);

          if (!isNaN(hits) && !isNaN(kudos)) {
            if (kudos >= CONFIG.minKudosToShowScore) {
              let rawScore = calculateWordBasedScore(kudos, hits, words);
              if (kudos < 10) rawScore = 1;
              let displayScore = rawScore;
              if (CONFIG.useNormalization) {
                displayScore = (rawScore / CONFIG.userMaxScore) * 100;
                displayScore = Math.min(100, displayScore);
                displayScore = Math.ceil(displayScore);
              } else {
                displayScore = Math.round(displayScore * 10) / 10;
              }

              let color;
              if (displayScore >= normalizedThresholdHigh) {
                color = CONFIG.colorGreen;
              } else if (displayScore >= normalizedThresholdLow) {
                color = CONFIG.colorYellow;
              } else {
                color = CONFIG.colorRed;
              }

              if (kudoshitsDt && forceRecalculation) {
                const existingScoreElement = $1("dd.kudoshits", statsElement);
                if (existingScoreElement) {
                  if (CONFIG.useIcons) {
                    const span = existingScoreElement.querySelector("span");
                    if (span) {
                      span.textContent = displayScore;
                      span.style.cssText = `border-radius: 4px; display: inline-block; vertical-align: baseline; font-size: inherit; line-height: inherit;`;
                      if (CONFIG.colorStyle === "background") {
                        span.style.backgroundColor = color;
                        span.style.color = CONFIG.colorText;
                        span.style.padding = "0 4px";
                      } else if (CONFIG.colorStyle === "text") {
                        span.style.color = color;
                      }
                    }
                  } else {
                    existingScoreElement.textContent = displayScore;
                    existingScoreElement.style.cssText = `display: inline-block; vertical-align: baseline; border-radius: 4px; font-size: inherit; line-height: inherit;`;
                    if (CONFIG.colorStyle === "background") {
                      existingScoreElement.style.backgroundColor = color;
                      existingScoreElement.style.color = CONFIG.colorText;
                      existingScoreElement.style.padding = "0 4px";
                    } else if (CONFIG.colorStyle === "text") {
                      existingScoreElement.style.color = color;
                    }
                  }
                }
              } else if (!kudoshitsDt) {
                let ddStyle =
                  "display: inline-block; vertical-align: baseline;";
                let spanStyle =
                  "border-radius: 4px; display: inline-block; vertical-align: baseline; font-size: inherit; line-height: inherit;";

                if (CONFIG.colorStyle === "background") {
                  spanStyle += ` background-color: ${color}; color: ${CONFIG.colorText}; padding: 0 4px;`;
                } else if (CONFIG.colorStyle === "text") {
                  spanStyle += ` color: ${color};`;
                }

                if (CONFIG.useIcons) {
                  hitsElement.insertAdjacentHTML(
                    "afterend",
                    `<dt class="kudoshits"></dt><dd class="kudoshits" style="${ddStyle}"><span style="${spanStyle}">${displayScore}</span></dd>`
                  );
                } else {
                  ddStyle +=
                    " border-radius: 4px; font-size: inherit; line-height: inherit;";
                  if (CONFIG.colorStyle === "background") {
                    ddStyle += ` background-color: ${color}; color: ${CONFIG.colorText}; padding: 0 4px;`;
                  } else if (CONFIG.colorStyle === "text") {
                    ddStyle += ` color: ${color};`;
                  }
                  hitsElement.insertAdjacentHTML(
                    "afterend",
                    `<dt class="kudoshits">Score:</dt><dd class="kudoshits" style="${ddStyle}">${displayScore}</dd>`
                  );
                }
              }

              if (parentLi) parentLi.setAttribute("kudospercent", displayScore);
            }
          }
        } catch (error) {
          console.error("Error calculating score:", error);
        }
      }

      if (CONFIG.hideMetrics && !statsPage) {
        if (CONFIG.hideHits) {
          if (hitsElement) hitsElement.style.display = "none";
          if (hitsLabel) hitsLabel.style.display = "none";
        }
        if (CONFIG.hideKudos) {
          if (kudosElement) kudosElement.style.display = "none";
          if (kudosLabel) kudosLabel.style.display = "none";
        }
        if (CONFIG.hideBookmarks) {
          if (bookmarksElement) bookmarksElement.style.display = "none";
          if (bookmarksLabel) bookmarksLabel.style.display = "none";
        }
        if (CONFIG.hideComments) {
          if (commentsElement) commentsElement.style.display = "none";
          if (commentsLabel) commentsLabel.style.display = "none";
        }
      }

      if (CONFIG.hideWorksEnabled && parentLi) {
        const username = detectAndStoreUsername();
        const authorLink = parentLi.querySelector('a[href*="/users/"]');
        let shouldHide = true;

        if (authorLink && username) {
          const authorHref = authorLink.getAttribute("href");
          const authorUsername = authorHref.match(/\/users\/([^\/]+)/)?.[1];
          if (authorUsername === username) {
            shouldHide = false;
          }
        }

        if (isMyContentPage(username)) {
          shouldHide = false;
        }

        if (shouldHide) {
          if (parentLi.hasAttribute("kudospercent")) {
            const displayScore = parseFloat(
              parentLi.getAttribute("kudospercent")
            );
            let rawScore = displayScore;
            if (CONFIG.useNormalization) {
              rawScore = (displayScore / 100) * CONFIG.userMaxScore;
            }
            parentLi.style.display =
              rawScore < CONFIG.hideWorksScore ? "none" : "";
          } else {
            parentLi.style.display = CONFIG.keepUnscoredVisible ? "" : "none";
          }
        } else {
          parentLi.style.display = "";
        }
      }
    });
  };

  const calculateReadtime = () => {
    if (!countable || !CONFIG.enableReadingTime) return;
    calculateMetrics(null, false, true);
  };

  const countRatio = () => {
    if (!countable || !CONFIG.enableQualityScore) return;
    calculateMetrics(null, false, true);
  };

  const sortByRatio = (ascending = false, cachedStats = null) => {
    if (!sortable) return;

    const statsElements = cachedStats || Array.from($("dl.stats"));
    const listsToSort = new Set();
    statsElements.forEach((statsElement) => {
      const parentLi = statsElement.closest("li");
      const list = parentLi?.parentElement;
      if (list) listsToSort.add(list);
    });

    listsToSort.forEach((list) => {
      const listElements = Array.from(list.children);

      const parent = list.parentNode;
      const nextSibling = list.nextSibling;
      parent.removeChild(list);

      listElements.forEach((el, index) => {
        if (!el.hasAttribute("data-original-index")) {
          el.setAttribute("data-original-index", index);
        }
      });

      const scoreCache = new Map();
      listElements.forEach((el) => {
        const score = parseFloat(el.getAttribute("kudospercent")) || 0;
        scoreCache.set(el, score);
      });

      listElements.sort((a, b) => {
        return ascending
          ? scoreCache.get(a) - scoreCache.get(b)
          : scoreCache.get(b) - scoreCache.get(a);
      });

      const fragment = document.createDocumentFragment();
      listElements.forEach((el) => fragment.appendChild(el));
      list.appendChild(fragment);

      parent.insertBefore(list, nextSibling);
    });
  };

  const restoreOriginalOrder = () => {
    const allLists = new Set();
    $("dl.stats").forEach((statsElement) => {
      const parentLi = statsElement.closest("li");
      const list = parentLi?.parentElement;
      if (list) allLists.add(list);
    });

    allLists.forEach((list) => {
      const listElements = Array.from(list.children);

      listElements.sort((a, b) => {
        const aIndex = parseInt(a.getAttribute("data-original-index")) || 0;
        const bIndex = parseInt(b.getAttribute("data-original-index")) || 0;
        return aIndex - bIndex;
      });

      const fragment = document.createDocumentFragment();
      listElements.forEach((el) => fragment.appendChild(el));
      list.appendChild(fragment);
    });
  };

  const updateExistingVisualStyles = () => {
    const allStats = Array.from($("dl.stats"));
    allStats.forEach((statsElement) => {
      const readtimeDd = $1("dd.readtime", statsElement);
      if (readtimeDd) {
        const span = readtimeDd.querySelector("span");
        if (span) {
          const timeText = span.textContent;
          let minutes = 0;
          const hourMatch = timeText.match(/(\d+)h/);
          const minuteMatch = timeText.match(/(\d+)m/);

          if (hourMatch) minutes += parseInt(hourMatch[1]) * 60;
          if (minuteMatch) minutes += parseInt(minuteMatch[1]);

          let color;
          if (minutes < CONFIG.readingTimeLvl1) {
            color = CONFIG.colorGreen;
          } else if (minutes < CONFIG.readingTimeLvl2) {
            color = CONFIG.colorYellow;
          } else {
            color = CONFIG.colorRed;
          }

          span.style.cssText = `border-radius: 4px; display: inline-block; vertical-align: baseline; font-size: inherit; line-height: inherit;`;
          if (CONFIG.colorStyle === "background") {
            span.style.backgroundColor = color;
            span.style.color = CONFIG.colorText;
            span.style.padding = "0 4px";
          } else if (CONFIG.colorStyle === "text") {
            span.style.color = color;
          }
        }
      }

      const kudoshitsDd = $1("dd.kudoshits", statsElement);
      if (kudoshitsDd) {
        const span = kudoshitsDd.querySelector("span");
        if (span) {
          const scoreText = span.textContent;
          const scoreValue = parseFloat(scoreText);

          let color;
          const normalizedThresholdLow = CONFIG.useNormalization
            ? Math.ceil((CONFIG.colorThresholdLow / CONFIG.userMaxScore) * 100)
            : CONFIG.colorThresholdLow;
          const normalizedThresholdHigh = CONFIG.useNormalization
            ? Math.ceil((CONFIG.colorThresholdHigh / CONFIG.userMaxScore) * 100)
            : CONFIG.colorThresholdHigh;

          if (scoreValue >= normalizedThresholdHigh) {
            color = CONFIG.colorGreen;
          } else if (scoreValue >= normalizedThresholdLow) {
            color = CONFIG.colorYellow;
          } else {
            color = CONFIG.colorRed;
          }

          span.style.cssText = `border-radius: 4px; display: inline-block; vertical-align: baseline; font-size: inherit; line-height: inherit;`;
          if (CONFIG.colorStyle === "background") {
            span.style.backgroundColor = color;
            span.style.color = CONFIG.colorText;
            span.style.padding = "0 4px";
          } else if (CONFIG.colorStyle === "text") {
            span.style.color = color;
          }
        } else if (kudoshitsDd.textContent && !span) {
          const scoreValue = parseFloat(kudoshitsDd.textContent);

          let color;
          const normalizedThresholdLow = CONFIG.useNormalization
            ? Math.ceil((CONFIG.colorThresholdLow / CONFIG.userMaxScore) * 100)
            : CONFIG.colorThresholdLow;
          const normalizedThresholdHigh = CONFIG.useNormalization
            ? Math.ceil((CONFIG.colorThresholdHigh / CONFIG.userMaxScore) * 100)
            : CONFIG.colorThresholdHigh;

          if (scoreValue >= normalizedThresholdHigh) {
            color = CONFIG.colorGreen;
          } else if (scoreValue >= normalizedThresholdLow) {
            color = CONFIG.colorYellow;
          } else {
            color = CONFIG.colorRed;
          }

          kudoshitsDd.style.cssText = `display: inline-block; vertical-align: baseline; border-radius: 4px; font-size: inherit; line-height: inherit;`;
          if (CONFIG.colorStyle === "background") {
            kudoshitsDd.style.backgroundColor = color;
            kudoshitsDd.style.color = CONFIG.colorText;
            kudoshitsDd.style.padding = "0 4px";
          } else if (CONFIG.colorStyle === "text") {
            kudoshitsDd.style.color = color;
          }
        }
      }
    });
  };

  const updateExistingChapterTimeStyles = () => {
    const WORKS_PAGE_REGEX =
      /^https?:\/\/archiveofourown\.org\/(?:.*\/)?(works|chapters)(\/|$)/;
    if (!WORKS_PAGE_REGEX.test(window.location.href)) return;

    const chaptersContainer = $1("#chapters");
    if (!chaptersContainer) return;

    const existingStats = chaptersContainer.querySelectorAll(
      ".ao3-chapter-stats-default, .ao3-chapter-stats-colored, .ao3-chapter-stats-timeonly, .ao3-chapter-stats"
    );
    existingStats.forEach((statsElement) => {
      let wordCountText;
      if (statsElement.classList.contains("ao3-chapter-stats-default")) {
        wordCountText = statsElement.textContent.match(
          /(\d{1,3}(?:,\d{3})*|\d+) words/
        );
      } else if (
        statsElement.classList.contains("ao3-chapter-stats-colored") ||
        statsElement.classList.contains("ao3-chapter-stats")
      ) {
        wordCountText = statsElement.textContent.match(
          /(\d{1,3}(?:,\d{3})*|\d+) words/
        );
      } else if (
        statsElement.classList.contains("ao3-chapter-stats-timeonly")
      ) {
        return;
      }

      if (!wordCountText) return;

      const wordCount = parseInt(wordCountText[1].replace(/,/g, ""));
      const minutes = wordCount / CONFIG.wpm;
      const hrs = Math.floor(minutes / 60);
      const mins = Math.round(minutes % 60);

      let timeLongStr;
      if (hrs > 0) {
        timeLongStr =
          mins > 0
            ? `${hrs} hour${hrs > 1 ? "s" : ""} ${mins} minute${
                mins > 1 ? "s" : ""
              }`
            : `${hrs} hour${hrs > 1 ? "s" : ""}`;
      } else {
        timeLongStr = `${mins} minute${mins > 1 ? "s" : ""}`;
      }

      let timeOnlyStr;
      if (hrs > 0) {
        timeOnlyStr =
          mins > 0
            ? `${hrs} hour${hrs > 1 ? "s" : ""}, ${mins} minute${
                mins > 1 ? "s" : ""
              }`
            : `${hrs} hour${hrs > 1 ? "s" : ""}`;
      } else {
        timeOnlyStr = `${mins} minute${mins > 1 ? "s" : ""}`;
      }

      if (CONFIG.chapterTimeStyle === "default") {
        if (!statsElement.classList.contains("ao3-chapter-stats-default")) {
          statsElement.className = "ao3-chapter-stats-default";
          statsElement.tagName = "p";
          statsElement.textContent = `~${timeLongStr} (${wordCount.toLocaleString()} words)`;
        }
      } else if (CONFIG.chapterTimeStyle === "colored") {
        if (!statsElement.classList.contains("ao3-chapter-stats")) {
          if (statsElement.tagName !== "UL") {
            const newUl = document.createElement("ul");
            newUl.className = "notice ao3-chapter-stats";
            const listItem = document.createElement("li");
            listItem.textContent = `~${timeLongStr} (${wordCount.toLocaleString()} words)`;
            newUl.appendChild(listItem);
            statsElement.parentNode.replaceChild(newUl, statsElement);
          } else {
            statsElement.className = "notice ao3-chapter-stats";
            const listItem = statsElement.querySelector("li");
            if (listItem) {
              listItem.textContent = `~${timeLongStr} (${wordCount.toLocaleString()} words)`;
            }
          }
        }
      } else if (CONFIG.chapterTimeStyle === "timeonly") {
        if (!statsElement.classList.contains("ao3-chapter-stats-timeonly")) {
          statsElement.className = "ao3-chapter-stats-timeonly";
          statsElement.tagName = "p"; // Change to p if it was ul
          statsElement.textContent = `~${timeOnlyStr}`;
        }
      }
    });
  };

  const calculateChapterStats = (chaptersContainer = null) => {
    if (!CONFIG.enableChapterStats) return;
    const WORKS_PAGE_REGEX =
      /^https?:\/\/archiveofourown\.org\/(?:.*\/)?(works|chapters)(\/|$)/;
    if (!WORKS_PAGE_REGEX.test(window.location.href)) return;

    const container = chaptersContainer || $1("#chapters");
    if (!container) return;

    const chapters = container.querySelectorAll(".chapter");
    const singleChapter = container.querySelector("div.userstuff");
    let chaptersToProcess = [];

    if (chapters.length > 0) {
      chaptersToProcess = Array.from(chapters);
    } else if (singleChapter) {
      chaptersToProcess = [{ userstuff: singleChapter, isSingle: true }];
    }
    if (chaptersToProcess.length === 0) return;

    const wordRegex = /\b[a-zA-Z][a-zA-Z0-9'-]*\b/g;

    chaptersToProcess.forEach((chapter) => {
      let userstuff;
      let existingStats;

      if (chapter.isSingle) {
        userstuff = chapter.userstuff;
        const chapterNotes = $1("#chapters .notes");
        if (
          userstuff.previousElementSibling &&
          userstuff.previousElementSibling.classList.contains("notice")
        ) {
          return;
        }
        existingStats = chapterNotes;
      } else {
        const prefaceContainer = $1(".chapter.preface", chapter);
        if ($1(".notice.ao3-chapter-stats", chapter)) {
          return;
        }
        userstuff = $1("div.userstuff", chapter);
        existingStats = prefaceContainer;
      }
      if (!userstuff) return;

      const text = userstuff.textContent || "";

      const words = text.match(wordRegex);
      const wordCount = words ? words.length : 0;

      if (wordCount === 0) return;
      const minutes = wordCount / CONFIG.wpm;
      const hrs = Math.floor(minutes / 60);
      const mins = Math.ceil(minutes % 60);

      let timeLongStr;
      if (hrs > 0) {
        timeLongStr =
          mins > 0
            ? `${hrs} hour${hrs > 1 ? "s" : ""} ${mins} minute${
                mins > 1 ? "s" : ""
              }`
            : `${hrs} hour${hrs > 1 ? "s" : ""}`;
      } else {
        timeLongStr = `${mins} minute${mins > 1 ? "s" : ""}`;
      }

      let timeOnlyStr;
      if (hrs > 0) {
        timeOnlyStr =
          mins > 0
            ? `${hrs} hour${hrs > 1 ? "s" : ""}, ${mins} minute${
                mins > 1 ? "s" : ""
              }`
            : `${hrs} hour${hrs > 1 ? "s" : ""}`;
      } else {
        timeOnlyStr = `${mins} minute${mins > 1 ? "s" : ""}`;
      }

      let statsDiv;
      if (CONFIG.chapterTimeStyle === "default") {
        statsDiv = document.createElement("p");
        statsDiv.className = "ao3-chapter-stats-default";
        statsDiv.textContent = `~${timeLongStr} (${wordCount.toLocaleString()} words)`;
      } else if (CONFIG.chapterTimeStyle === "colored") {
        statsDiv = document.createElement("ul");
        statsDiv.className = "notice ao3-chapter-stats";
        const listItem = document.createElement("li");
        listItem.textContent = `~${timeLongStr} (${wordCount.toLocaleString()} words)`;
        statsDiv.appendChild(listItem);
      } else {
        statsDiv = document.createElement("p");
        statsDiv.className = "ao3-chapter-stats-timeonly";
        statsDiv.textContent = `~${timeOnlyStr}`;
      }

      if (chapter.isSingle) {
        if (existingStats) {
          existingStats.insertAdjacentElement("afterend", statsDiv);
        } else {
          userstuff.insertAdjacentElement("beforebegin", statsDiv);
        }
      } else {
        if (existingStats) {
          existingStats.insertAdjacentElement("afterend", statsDiv);
        } else {
          userstuff.insertAdjacentElement("beforebegin", statsDiv);
        }
      }
    });
  };

  const showSettingsPopup = () => {
    if (!window.AO3MenuHelpers) return;

    window.AO3MenuHelpers.removeAllDialogs();

    const dialog = window.AO3MenuHelpers.createDialog(
      "⏱️ Reading Time & Quality Score ⭐",
      {
        maxWidth: "600px",
      }
    );

    const fragment = document.createDocumentFragment();

    const displayThresholdLow = CONFIG.useNormalization
      ? Math.ceil((CONFIG.colorThresholdLow / CONFIG.userMaxScore) * 100)
      : CONFIG.colorThresholdLow;
    const displayThresholdHigh = CONFIG.useNormalization
      ? Math.ceil((CONFIG.colorThresholdHigh / CONFIG.userMaxScore) * 100)
      : CONFIG.colorThresholdHigh;

    const displayHideWorksScore = CONFIG.useNormalization
      ? Math.round((CONFIG.hideWorksScore / CONFIG.userMaxScore) * 100)
      : CONFIG.hideWorksScore;

    const readingTimeSection =
      window.AO3MenuHelpers.createSection("📚 Reading Time");
    const readingTimeGroup = window.AO3MenuHelpers.createSettingGroup();
    const enableReadingTimeCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "enableReadingTime",
      label: "Enable Reading Time",
      checked: CONFIG.enableReadingTime,
      inGroup: false,
    });
    readingTimeGroup.appendChild(enableReadingTimeCheckbox);

    const readingTimeSubsettings = window.AO3MenuHelpers.createSubsettings();
    readingTimeSubsettings.style.display = CONFIG.enableReadingTime
      ? ""
      : "none";
    readingTimeSubsettings.appendChild(
      window.AO3MenuHelpers.createCheckbox({
        id: "alwaysCountReadingTime",
        label: "Calculate automatically",
        checked: CONFIG.alwaysCountReadingTime,
      })
    );
    readingTimeSubsettings.appendChild(
      window.AO3MenuHelpers.createCheckbox({
        id: "enableChapterStats",
        label: "Show chapter reading times",
        checked: CONFIG.enableChapterStats,
        tooltip:
          "Show word count and reading time at the start of each chapter",
      })
    );
    readingTimeSubsettings.appendChild(
      window.AO3MenuHelpers.createNumberInput({
        id: "wpm",
        label: "Words per minute",
        value: CONFIG.wpm,
        min: 100,
        max: 1000,
        step: 25,
        tooltip:
          "Average reading speed is 200-300 wpm. 375 is for faster readers.",
      })
    );
    const readingTimeTwoColumn = window.AO3MenuHelpers.createTwoColumnLayout(
      window.AO3MenuHelpers.createNumberInput({
        id: "readingTimeLvl1",
        label: "Yellow threshold (minutes)",
        value: CONFIG.readingTimeLvl1,
        min: 5,
        max: 240,
        step: 5,
        tooltip:
          "Works taking less than this many minutes will be colored green",
      }),
      window.AO3MenuHelpers.createNumberInput({
        id: "readingTimeLvl2",
        label: "Red threshold (minutes)",
        value: CONFIG.readingTimeLvl2,
        min: 30,
        max: 480,
        step: 10,
        tooltip: "Works taking more than this many minutes will be colored red",
      })
    );
    readingTimeTwoColumn.style.marginBottom = "0";
    readingTimeSubsettings.appendChild(readingTimeTwoColumn);
    readingTimeGroup.appendChild(readingTimeSubsettings);
    readingTimeSection.appendChild(readingTimeGroup);
    fragment.appendChild(readingTimeSection);

    const qualityScoreSection =
      window.AO3MenuHelpers.createSection("💖 Quality Score");
    qualityScoreSection.style.paddingBottom = "20px";
    const qualityScoreGroup = window.AO3MenuHelpers.createSettingGroup();
    const enableQualityScoreCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "enableQualityScore",
      label: "Enable Quality Score",
      checked: CONFIG.enableQualityScore,
      inGroup: false,
    });
    qualityScoreGroup.appendChild(enableQualityScoreCheckbox);

    const qualityScoreSubsettings = window.AO3MenuHelpers.createSubsettings();
    qualityScoreSubsettings.style.display = CONFIG.enableQualityScore
      ? ""
      : "none";
    qualityScoreSubsettings.appendChild(
      window.AO3MenuHelpers.createCheckbox({
        id: "alwaysCountQualityScore",
        label: "Calculate automatically",
        checked: CONFIG.alwaysCountQualityScore,
      })
    );

    const autoCalculateSubsettings = window.AO3MenuHelpers.createSubsettings();
    autoCalculateSubsettings.style.display = CONFIG.alwaysCountQualityScore
      ? ""
      : "none";

    const alwaysSortGroup = window.AO3MenuHelpers.createSettingGroup();
    const alwaysSortCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "alwaysSortQualityScore",
      label: "Sort by score automatically",
      checked: CONFIG.alwaysSortQualityScore,
      inGroup: false,
    });
    alwaysSortGroup.appendChild(alwaysSortCheckbox);

    const excludeMyContentSubsetting =
      window.AO3MenuHelpers.createSubsettings();
    excludeMyContentSubsetting.style.marginLeft = "1em";
    excludeMyContentSubsetting.style.display = CONFIG.alwaysSortQualityScore
      ? ""
      : "none";
    excludeMyContentSubsetting.appendChild(
      window.AO3MenuHelpers.createCheckbox({
        id: "excludeMyContentFromSort",
        label: "Exclude my content",
        checked: CONFIG.excludeMyContentFromSort,
        tooltip:
          "Disable automatic sorting on your user dashboard, bookmarks, history, and works pages",
        inGroup: false,
      })
    );
    alwaysSortGroup.appendChild(excludeMyContentSubsetting);
    autoCalculateSubsettings.appendChild(alwaysSortGroup);
    qualityScoreSubsettings.appendChild(autoCalculateSubsettings);
    qualityScoreSubsettings.appendChild(
      window.AO3MenuHelpers.createNumberInput({
        id: "minKudosToShowScore",
        label: "Minimum kudos to show score",
        value: CONFIG.minKudosToShowScore,
        min: 0,
        max: 10000,
        step: 1,
      })
    );

    const normalizationGroup = window.AO3MenuHelpers.createSettingGroup();
    const useNormalizationCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "useNormalization",
      label: "Normalize scores to 100%",
      checked: CONFIG.useNormalization,
      tooltip:
        "Scale the raw score so your 'Best Possible Raw Score' equals 100%. Makes scores from different fandoms more comparable.",
      inGroup: false,
    });
    normalizationGroup.appendChild(useNormalizationCheckbox);

    const userMaxScoreGroup = window.AO3MenuHelpers.createSettingGroup();
    userMaxScoreGroup.id = "userMaxScoreContainer";
    userMaxScoreGroup.style.display = CONFIG.useNormalization ? "" : "none";

    const userMaxScoreLabel = document.createElement("label");
    userMaxScoreLabel.className = "setting-label";
    userMaxScoreLabel.setAttribute("for", "userMaxScore");
    userMaxScoreLabel.textContent = "Best Possible Raw Score ";

    const normalizationLabel = document.createElement("span");
    normalizationLabel.id = "normalizationLabel";
    normalizationLabel.textContent = CONFIG.useNormalization
      ? "(for 100%)"
      : "";
    userMaxScoreLabel.appendChild(normalizationLabel);

    userMaxScoreLabel.appendChild(document.createTextNode(" "));
    userMaxScoreLabel.appendChild(
      window.AO3MenuHelpers.createTooltip(
        "The highest score you've seen in your fandom. Used to scale other scores to percentages."
      )
    );

    userMaxScoreGroup.appendChild(userMaxScoreLabel);
    userMaxScoreGroup.appendChild(
      window.AO3MenuHelpers.createNumberInput({
        id: "userMaxScore",
        value: CONFIG.userMaxScore,
        min: 1,
        max: 100,
        step: 1,
      })
    );
    normalizationGroup.appendChild(userMaxScoreGroup);
    qualityScoreSubsettings.appendChild(normalizationGroup);

    const thresholdLowLabel = document.createElement("label");
    thresholdLowLabel.className = "setting-label";
    thresholdLowLabel.setAttribute("for", "colorThresholdLow");
    thresholdLowLabel.textContent = "Good Score ";

    const thresholdLowLabelSpan = document.createElement("span");
    thresholdLowLabelSpan.id = "thresholdLowLabel";
    thresholdLowLabelSpan.textContent = CONFIG.useNormalization ? "(%)" : "";
    thresholdLowLabel.appendChild(thresholdLowLabelSpan);

    thresholdLowLabel.appendChild(document.createTextNode(" "));
    thresholdLowLabel.appendChild(
      window.AO3MenuHelpers.createTooltip(
        "Scores at or above this threshold will be colored yellow"
      )
    );

    const thresholdHighLabel = document.createElement("label");
    thresholdHighLabel.className = "setting-label";
    thresholdHighLabel.setAttribute("for", "colorThresholdHigh");
    thresholdHighLabel.textContent = "Excellent Score ";

    const thresholdHighLabelSpan = document.createElement("span");
    thresholdHighLabelSpan.id = "thresholdHighLabel";
    thresholdHighLabelSpan.textContent = CONFIG.useNormalization ? "(%)" : "";
    thresholdHighLabel.appendChild(thresholdHighLabelSpan);

    thresholdHighLabel.appendChild(document.createTextNode(" "));
    thresholdHighLabel.appendChild(
      window.AO3MenuHelpers.createTooltip(
        "Scores at or above this threshold will be colored green"
      )
    );

    const colorThresholdLowInput = document.createElement("div");
    colorThresholdLowInput.className = "setting-group";
    colorThresholdLowInput.style.marginBottom = "0";
    colorThresholdLowInput.appendChild(thresholdLowLabel);
    colorThresholdLowInput.appendChild(
      window.AO3MenuHelpers.createNumberInput({
        id: "colorThresholdLow",
        value: displayThresholdLow,
        min: 0.1,
        max: 100,
        step: 0.1,
      }).querySelector("input") // Extract just the input, not the wrapper
    );

    const colorThresholdHighInput = document.createElement("div");
    colorThresholdHighInput.className = "setting-group";
    colorThresholdHighInput.style.marginBottom = "0";
    colorThresholdHighInput.appendChild(thresholdHighLabel);
    colorThresholdHighInput.appendChild(
      window.AO3MenuHelpers.createNumberInput({
        id: "colorThresholdHigh",
        value: displayThresholdHigh,
        min: 0.1,
        max: 100,
        step: 0.1,
      }).querySelector("input") // Extract just the input, not the wrapper
    );

    const thresholdTwoColumn = window.AO3MenuHelpers.createTwoColumnLayout(
      colorThresholdLowInput,
      colorThresholdHighInput
    );
    thresholdTwoColumn.style.marginBottom = "0";
    qualityScoreSubsettings.appendChild(thresholdTwoColumn);

    const hideWorksScoreLabel = document.createElement("label");
    hideWorksScoreLabel.className = "setting-label";
    hideWorksScoreLabel.setAttribute("for", "hideWorksScore");
    hideWorksScoreLabel.textContent = "Minimum Score ";

    const hideWorksScoreLabelSpan = document.createElement("span");
    hideWorksScoreLabelSpan.id = "hideWorksScoreLabel";
    hideWorksScoreLabelSpan.textContent = CONFIG.useNormalization ? "(%)" : "";
    hideWorksScoreLabel.appendChild(hideWorksScoreLabelSpan);

    const hideWorksScoreInput = window.AO3MenuHelpers.createNumberInput({
      id: "hideWorksScore",
      value: displayHideWorksScore,
      min: 1,
      max: CONFIG.useNormalization ? 100 : 1000,
      step: 1,
    });

    const keepUnscoredCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "keepUnscoredVisible",
      label: "Show unscored works",
      checked: CONFIG.keepUnscoredVisible,
    });

    const hideWorksSubsettings = window.AO3MenuHelpers.createSubsettings();
    hideWorksSubsettings.appendChild(hideWorksScoreLabel);
    hideWorksSubsettings.appendChild(hideWorksScoreInput);
    hideWorksSubsettings.appendChild(keepUnscoredCheckbox);

    const hideWorksConditional =
      window.AO3MenuHelpers.createConditionalCheckbox({
        id: "hideWorksEnabled",
        label: "Hide works below score",
        checked: CONFIG.hideWorksEnabled,
        tooltip:
          "Works with scores below this threshold will be hidden. Excludes your own works, bookmarks, and history.",
        subsettings: [hideWorksSubsettings],
      });

    hideWorksConditional.style.marginTop = "10px";

    qualityScoreSubsettings.appendChild(hideWorksConditional);

    qualityScoreGroup.appendChild(qualityScoreSubsettings);
    qualityScoreSection.appendChild(qualityScoreGroup);
    fragment.appendChild(qualityScoreSection);

    const visualSection =
      window.AO3MenuHelpers.createSection("🎨 Visual Styling");

    const twoColumnLayout = document.createElement("div");
    twoColumnLayout.className = "two-column";

    twoColumnLayout.appendChild(
      window.AO3MenuHelpers.createSelect({
        id: "colorStyle",
        label: "Visual Style:",
        options: [
          {
            value: "none",
            label: "Default",
            selected: CONFIG.colorStyle === "none",
          },
          {
            value: "text",
            label: "Colored",
            selected: CONFIG.colorStyle === "text",
          },
          {
            value: "background",
            label: "Bars",
            selected: CONFIG.colorStyle === "background",
          },
        ],
      })
    );

    const chapterTimeStyleGroup = window.AO3MenuHelpers.createSelect({
      id: "chapterTimeStyle",
      label: "Chapter Time Style:",
      options: [
        {
          value: "default",
          label: "Default",
          selected: CONFIG.chapterTimeStyle === "default",
        },
        {
          value: "colored",
          label: "Notice",
          selected: CONFIG.chapterTimeStyle === "colored",
        },
        {
          value: "timeonly",
          label: "Time Only",
          selected: CONFIG.chapterTimeStyle === "timeonly",
        },
      ],
    });
    chapterTimeStyleGroup.id = "chapterTimeStyleSettings";
    chapterTimeStyleGroup.style.display = CONFIG.enableChapterStats
      ? ""
      : "none";
    twoColumnLayout.appendChild(chapterTimeStyleGroup);

    visualSection.appendChild(twoColumnLayout);

    const colorPickerSettings = window.AO3MenuHelpers.createSubsettings();
    colorPickerSettings.id = "colorPickerSettings";
    colorPickerSettings.style.display =
      CONFIG.colorStyle !== "none" ? "" : "none";

    const twoColumnColors = document.createElement("div");
    twoColumnColors.className = "two-column";
    twoColumnLayout.style.marginBottom = "0";
    twoColumnColors.appendChild(
      window.AO3MenuHelpers.createColorPicker({
        id: "colorGreen",
        label: "Green",
        value: CONFIG.colorGreen,
      })
    );
    twoColumnColors.appendChild(
      window.AO3MenuHelpers.createColorPicker({
        id: "colorYellow",
        label: "Yellow",
        value: CONFIG.colorYellow,
      })
    );
    twoColumnColors.appendChild(
      window.AO3MenuHelpers.createColorPicker({
        id: "colorRed",
        label: "Red",
        value: CONFIG.colorRed,
      })
    );

    const textColorContainer = window.AO3MenuHelpers.createSettingGroup();
    textColorContainer.id = "textColorContainer";
    textColorContainer.style.display =
      CONFIG.colorStyle === "background" ? "" : "none";
    textColorContainer.appendChild(
      window.AO3MenuHelpers.createColorPicker({
        id: "colorText",
        label: "Text color",
        value: CONFIG.colorText,
      })
    );
    twoColumnColors.appendChild(textColorContainer);

    colorPickerSettings.appendChild(twoColumnColors);
    visualSection.appendChild(colorPickerSettings);

    const useIconsGroup = window.AO3MenuHelpers.createSettingGroup();
    const useIconsCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "useIcons",
      label: "Use icons instead of text labels",
      checked: CONFIG.useIcons,
      tooltip: "Replace 'Time:' and 'Score:' labels with icons",
      inGroup: false,
    });
    useIconsGroup.appendChild(useIconsCheckbox);

    const iconColorSettings = window.AO3MenuHelpers.createSubsettings();
    iconColorSettings.id = "iconColorSettings";
    iconColorSettings.style.display = CONFIG.useIcons ? "" : "none";
    iconColorSettings.appendChild(
      window.AO3MenuHelpers.createCheckbox({
        id: "useCustomIconColor",
        label: "Use custom icon color",
        checked: !!CONFIG.iconColor,
        tooltip:
          "When unchecked, icons will inherit color from your site skin. When checked, you can set a specific color.",
      })
    );

    const customIconColorPicker = window.AO3MenuHelpers.createSettingGroup();
    customIconColorPicker.id = "customIconColorPicker";
    customIconColorPicker.style.display = CONFIG.iconColor ? "" : "none";
    customIconColorPicker.appendChild(
      window.AO3MenuHelpers.createColorPicker({
        id: "iconColor",
        label: "Icon color",
        value: CONFIG.iconColor || "#000000",
      })
    );
    iconColorSettings.appendChild(customIconColorPicker);
    useIconsGroup.appendChild(iconColorSettings);
    visualSection.appendChild(useIconsGroup);

    const hideMetricsGroup = window.AO3MenuHelpers.createSettingGroup();
    const hideMetricsCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "hideMetrics",
      label: "Hide metrics",
      checked: CONFIG.hideMetrics,
      tooltip: "Hide metrics (hits, kudos, bookmarkers, comments) from blurbs",
      inGroup: false,
    });
    hideMetricsGroup.appendChild(hideMetricsCheckbox);

    const hideMetricsSubsettings = window.AO3MenuHelpers.createSubsettings();
    hideMetricsSubsettings.id = "hideMetricsSubsettings";
    hideMetricsSubsettings.style.display = CONFIG.hideMetrics ? "" : "none";

    const hideHitsKudosRow = window.AO3MenuHelpers.createTwoColumnLayout(
      window.AO3MenuHelpers.createCheckbox({
        id: "hideHits",
        label: "Hits",
        checked: CONFIG.hideHits,
        inGroup: false,
      }),
      window.AO3MenuHelpers.createCheckbox({
        id: "hideKudos",
        label: "Kudos",
        checked: CONFIG.hideKudos,
        inGroup: false,
      })
    );
    hideMetricsSubsettings.appendChild(hideHitsKudosRow);

    const hideBookmarksCommentsRow =
      window.AO3MenuHelpers.createTwoColumnLayout(
        window.AO3MenuHelpers.createCheckbox({
          id: "hideBookmarks",
          label: "Bookmarks",
          checked: CONFIG.hideBookmarks,
          inGroup: false,
        }),
        window.AO3MenuHelpers.createCheckbox({
          id: "hideComments",
          label: "Comments",
          checked: CONFIG.hideComments,
          inGroup: false,
        })
      );
    hideMetricsSubsettings.appendChild(hideBookmarksCommentsRow);
    hideMetricsGroup.appendChild(hideMetricsSubsettings);
    visualSection.appendChild(hideMetricsGroup);

    fragment.appendChild(visualSection);

    fragment.appendChild(
      window.AO3MenuHelpers.createButtonGroup([
        { text: "Save", id: "saveButton" },
        { text: "Cancel", id: "closeButton" },
      ])
    );
    fragment.appendChild(
      window.AO3MenuHelpers.createResetLink("Reset to Default Settings", () => {
        resetAllSettings();
        dialog.remove();
      })
    );

    dialog.appendChild(fragment);
    dialog
      .querySelector("#enableReadingTime")
      .addEventListener("change", (e) => {
        readingTimeSubsettings.style.display = e.target.checked ? "" : "none";
      });

    dialog
      .querySelector("#enableChapterStats")
      .addEventListener("change", (e) => {
        chapterTimeStyleGroup.style.display = e.target.checked ? "" : "none";
      });

    dialog
      .querySelector("#enableQualityScore")
      .addEventListener("change", (e) => {
        qualityScoreSubsettings.style.display = e.target.checked ? "" : "none";
      });

    dialog
      .querySelector("#alwaysSortQualityScore")
      .addEventListener("change", (e) => {
        excludeMyContentSubsetting.style.display = e.target.checked
          ? ""
          : "none";
      });

    const colorStyleSelect = dialog.querySelector("#colorStyle");
    colorStyleSelect.addEventListener("change", () => {
      const selectedStyle = colorStyleSelect.value;
      colorPickerSettings.style.display =
        selectedStyle !== "none" ? "" : "none";
      textColorContainer.style.display =
        selectedStyle === "background" ? "" : "none";
    });

    dialog.querySelector("#useIcons").addEventListener("change", (e) => {
      iconColorSettings.style.display = e.target.checked ? "" : "none";
    });

    dialog
      .querySelector("#useCustomIconColor")
      .addEventListener("change", (e) => {
        customIconColorPicker.style.display = e.target.checked ? "" : "none";
      });

    dialog
      .querySelector("#useNormalization")
      .addEventListener("change", (e) => {
        const isNormalizationEnabled = e.target.checked;
        const normLabel = dialog.querySelector("#normalizationLabel");
        const thresholdLowLabel = dialog.querySelector("#thresholdLowLabel");
        const thresholdHighLabel = dialog.querySelector("#thresholdHighLabel");
        const thresholdLowInput = dialog.querySelector("#colorThresholdLow");
        const thresholdHighInput = dialog.querySelector("#colorThresholdHigh");
        const userMaxScoreInput = dialog.querySelector("#userMaxScore");
        const userMaxScoreContainer = dialog.querySelector(
          "#userMaxScoreContainer"
        );

        if (isNormalizationEnabled) {
          normLabel.textContent = "(for 100%)";
          thresholdLowLabel.textContent = "(%)";
          thresholdHighLabel.textContent = "(%)";
          userMaxScoreContainer.style.display = "";
          thresholdLowInput.value = Math.ceil(
            (parseFloat(thresholdLowInput.value) /
              parseFloat(userMaxScoreInput.value)) *
              100
          );
          thresholdHighInput.value = Math.ceil(
            (parseFloat(thresholdHighInput.value) /
              parseFloat(userMaxScoreInput.value)) *
              100
          );
        } else {
          normLabel.textContent = "";
          thresholdLowLabel.textContent = "";
          thresholdHighLabel.textContent = "";
          userMaxScoreContainer.style.display = "none";
          thresholdLowInput.value = Math.round(
            (parseFloat(thresholdLowInput.value) / 100) *
              parseFloat(userMaxScoreInput.value)
          );
          thresholdHighInput.value = Math.round(
            (parseFloat(thresholdHighInput.value) / 100) *
              parseFloat(userMaxScoreInput.value)
          );
        }

        const hideWorksScoreInput = dialog.querySelector("#hideWorksScore");
        const hideWorksScoreLabel = dialog.querySelector(
          "#hideWorksScoreLabel"
        );
        if (isNormalizationEnabled) {
          hideWorksScoreLabel.textContent = "(%)";
          hideWorksScoreInput.value = Math.round(
            (parseFloat(hideWorksScoreInput.value) /
              parseFloat(userMaxScoreInput.value)) *
              100
          );
          hideWorksScoreInput.max = 100;
        } else {
          hideWorksScoreLabel.textContent = "";
          hideWorksScoreInput.value = Math.round(
            (parseFloat(hideWorksScoreInput.value) / 100) *
              parseFloat(userMaxScoreInput.value)
          );
          hideWorksScoreInput.max = 1000;
        }
      });

    dialog.querySelector("#hideMetrics").addEventListener("change", (e) => {
      hideMetricsSubsettings.style.display = e.target.checked ? "" : "none";
    });

    dialog
      .querySelector("#alwaysCountQualityScore")
      .addEventListener("change", (e) => {
        autoCalculateSubsettings.style.display = e.target.checked ? "" : "none";
      });

    dialog
      .querySelector("#alwaysSortQualityScore")
      .addEventListener("change", (e) => {
        excludeMyContentSubsetting.style.display = e.target.checked
          ? ""
          : "none";
      });

    dialog.querySelector("#closeButton").addEventListener("click", () => {
      dialog.remove();
    });

    dialog.querySelector("#saveButton").addEventListener("click", () => {
      let userMaxScoreValue = parseFloat(
        dialog.querySelector("#userMaxScore").value
      );
      let thresholdLowValue = parseFloat(
        dialog.querySelector("#colorThresholdLow").value
      );
      let thresholdHighValue = parseFloat(
        dialog.querySelector("#colorThresholdHigh").value
      );
      const isNormalizationEnabled =
        dialog.querySelector("#useNormalization").checked;

      if (isNormalizationEnabled) {
        thresholdLowValue = (thresholdLowValue / 100) * userMaxScoreValue;
        thresholdHighValue = (thresholdHighValue / 100) * userMaxScoreValue;
      }

      CONFIG.enableReadingTime =
        dialog.querySelector("#enableReadingTime").checked;
      CONFIG.enableQualityScore = dialog.querySelector(
        "#enableQualityScore"
      ).checked;
      CONFIG.enableChapterStats = dialog.querySelector(
        "#enableChapterStats"
      ).checked;
      CONFIG.alwaysCountReadingTime = dialog.querySelector(
        "#alwaysCountReadingTime"
      ).checked;
      CONFIG.wpm = parseInt(dialog.querySelector("#wpm").value);
      CONFIG.readingTimeLvl1 = parseInt(
        dialog.querySelector("#readingTimeLvl1").value
      );
      CONFIG.readingTimeLvl2 = parseInt(
        dialog.querySelector("#readingTimeLvl2").value
      );
      CONFIG.alwaysCountQualityScore = dialog.querySelector(
        "#alwaysCountQualityScore"
      ).checked;

      // Handle sort state change - restore original order if disabling auto-sort
      const wasAutoSortEnabled = CONFIG.alwaysSortQualityScore;
      CONFIG.alwaysSortQualityScore = dialog.querySelector(
        "#alwaysSortQualityScore"
      ).checked;

      // If disabling auto-sort, restore original order
      if (wasAutoSortEnabled && !CONFIG.alwaysSortQualityScore) {
        restoreOriginalOrder();
      }

      CONFIG.excludeMyContentFromSort =
        dialog.querySelector("#excludeMyContentFromSort")?.checked || false;
      CONFIG.hideMetrics = dialog.querySelector("#hideMetrics").checked;
      CONFIG.hideHits = dialog.querySelector("#hideHits").checked;
      CONFIG.hideKudos = dialog.querySelector("#hideKudos").checked;
      CONFIG.hideBookmarks = dialog.querySelector("#hideBookmarks").checked;
      CONFIG.hideComments = dialog.querySelector("#hideComments").checked;
      CONFIG.minKudosToShowScore = parseInt(
        dialog.querySelector("#minKudosToShowScore").value
      );

      const normalizationChanged =
        CONFIG.useNormalization !== isNormalizationEnabled;

      CONFIG.useNormalization = isNormalizationEnabled;
      CONFIG.userMaxScore = userMaxScoreValue;
      CONFIG.colorThresholdLow = thresholdLowValue;
      CONFIG.colorThresholdHigh = thresholdHighValue;
      CONFIG.colorStyle = dialog.querySelector("#colorStyle").value;
      CONFIG.colorGreen = dialog.querySelector("#colorGreen").value;
      CONFIG.colorYellow = dialog.querySelector("#colorYellow").value;
      CONFIG.colorRed = dialog.querySelector("#colorRed").value;
      CONFIG.colorText = dialog.querySelector("#colorText").value;
      CONFIG.useIcons = dialog.querySelector("#useIcons").checked;
      CONFIG.iconColor = dialog.querySelector("#useCustomIconColor").checked
        ? dialog.querySelector("#iconColor").value
        : "";
      CONFIG.chapterTimeStyle = dialog.querySelector("#chapterTimeStyle").value;

      CONFIG.hideWorksEnabled =
        dialog.querySelector("#hideWorksEnabled").checked;
      CONFIG.keepUnscoredVisible = dialog.querySelector(
        "#keepUnscoredVisible"
      ).checked;
      let hideWorksScoreValue = parseFloat(
        dialog.querySelector("#hideWorksScore").value
      );
      if (isNormalizationEnabled) {
        hideWorksScoreValue = (hideWorksScoreValue / 100) * userMaxScoreValue;
      }
      CONFIG.hideWorksScore = hideWorksScoreValue;

      saveAllSettings();
      dialog.remove();

      // Reapply styles without reload
      const existingIconStyles = document.getElementById(
        "ao3-userscript-icon-styles"
      );
      if (existingIconStyles) existingIconStyles.remove();
      if (CONFIG.useIcons) addIconStyles();

      // Update existing elements with new visual style
      updateExistingVisualStyles();

      updateExistingChapterTimeStyles();

      const readingTimeDisabled =
        CONFIG.enableReadingTime && !CONFIG.alwaysCountReadingTime;
      const qualityScoreDisabled =
        CONFIG.enableQualityScore && !CONFIG.alwaysCountQualityScore;

      if (readingTimeDisabled || qualityScoreDisabled) {
        // Remove existing calculation elements
        const allStats = Array.from($("dl.stats"));
        allStats.forEach((statsElement) => {
          if (readingTimeDisabled) {
            const readtimeDt = $1("dt.readtime", statsElement);
            const readtimeDd = $1("dd.readtime", statsElement);
            if (readtimeDt) readtimeDt.remove();
            if (readtimeDd) readtimeDd.remove();
          }
          if (qualityScoreDisabled) {
            const kudoshitsDt = $1("dt.kudoshits", statsElement);
            const kudoshitsDd = $1("dd.kudoshits", statsElement);
            if (kudoshitsDt) kudoshitsDt.remove();
            if (kudoshitsDd) kudoshitsDd.remove();
          }
        });
      }

      // Recalculate metrics if automatic calculation is enabled
      if (
        (CONFIG.alwaysCountReadingTime && CONFIG.enableReadingTime) ||
        (CONFIG.alwaysCountQualityScore && CONFIG.enableQualityScore)
      ) {
        calculateMetrics(null, normalizationChanged, true);
      }
      if (CONFIG.alwaysSortQualityScore && CONFIG.enableQualityScore) {
        const username = detectAndStoreUsername();
        const myContentPage = isMyContentPage(username);
        if (!(CONFIG.excludeMyContentFromSort && myContentPage)) {
          sortByRatio();
        }
      }
      if (CONFIG.enableChapterStats) {
        calculateChapterStats();
      }
    });

    document.body.appendChild(dialog);
  };

  function initSharedMenu() {
    if (window.AO3MenuHelpers) {
      window.AO3MenuHelpers.addToSharedMenu({
        id: "opencfg_reading_quality",
        text: "Reading Time & Quality Score",
        onClick: showSettingsPopup,
      });

      // Add separator if we have conditional items
      if (CONFIG.enableReadingTime || CONFIG.enableQualityScore) {
        // Note: separator is handled automatically by the library
      }

      // Reading Time manual calculation only if 'Calculate automatically' is unchecked
      if (CONFIG.enableReadingTime && !CONFIG.alwaysCountReadingTime) {
        window.AO3MenuHelpers.addToSharedMenu({
          id: "calc_reading_time",
          text: "Reading Time: Calculate Times",
          onClick: calculateReadtime,
        });
      }

      // Quality Score manual calculation only if 'Calculate automatically' is unchecked
      if (CONFIG.enableQualityScore && !CONFIG.alwaysCountQualityScore) {
        window.AO3MenuHelpers.addToSharedMenu({
          id: "calc_quality_score",
          text: "Quality Score: Calculate Scores",
          onClick: countRatio,
        });
      }

      // Show manual 'Sort by Score' when 'Sort by score automatically' is unchecked,
      // or when both 'Sort by score automatically' and 'Exclude my content' are checked and on my content pages
      const username = detectAndStoreUsername();
      const isWorksPage = /^\/works\/(\d+)(\/chapters\/\d+)?(\/|$)/.test(
        window.location.pathname
      );
      if (
        isAllowedMenuPage() &&
        CONFIG.enableQualityScore &&
        (!CONFIG.alwaysSortQualityScore ||
          (CONFIG.alwaysSortQualityScore &&
            CONFIG.excludeMyContentFromSort &&
            isMyContentPage(username))) &&
        !isWorksPage
      ) {
        window.AO3MenuHelpers.addToSharedMenu({
          id: "sort_by_score",
          text: "Quality Score: Sort by Score",
          onClick: () => sortByRatio(),
        });
      }
    }
  }

  function isAllowedMenuPage() {
    const path = window.location.pathname;
    if (/^\/works\/(\d+)(\/chapters\/\d+)?(\/|$)/.test(path)) return false;
    if (
      /^\/users\/[^\/]+\/bookmarks(\/|$)/.test(path) ||
      /^\/bookmarks(\/|$)/.test(path)
    )
      return true;
    if (/^\/users\/[^\/]+\/pseuds\/[^\/]+\/bookmarks(\/|$)/.test(path))
      return true;
    if (/^\/users\/[^\/]+\/?$/.test(path)) return true;
    if (/^\/users\/[^\/]+\/pseuds\/[^\/]+\/works(\/|$)/.test(path)) return true;
    if (/^\/tags\/[^\/]+\/works(\/|$)/.test(path)) return true;
    if (/^\/collections\/[^\/]+(\/|$)/.test(path)) return true;
    if (/^\/works(\/|$)/.test(path)) return true;
    return false;
  }

  const hideWorksBelowScore = () => {
    if (!CONFIG.hideWorksEnabled) return;
    console.log("[AO3] Hiding works below score:", CONFIG.hideWorksScore);
    const username = detectAndStoreUsername();

    // Don't hide works on user's own content pages
    if (isMyContentPage(username)) {
      return;
    }

    const works = $("li.work, li.bookmark");
    works.forEach((work) => {
      // Check if this work belongs to the current user
      const authorLink = work.querySelector('a[href*="/users/"]');
      if (authorLink && username) {
        const authorHref = authorLink.getAttribute("href");
        const authorUsername = authorHref.match(/\/users\/([^\/]+)/)?.[1];
        if (authorUsername === username) {
          work.style.display = "";
          return;
        }
      }

      const scoreAttr = work.getAttribute("kudospercent");
      if (scoreAttr === null) {
        work.style.display = CONFIG.keepUnscoredVisible ? "" : "none";
      } else {
        const displayScore = parseFloat(scoreAttr);
        let rawScore = displayScore;
        if (CONFIG.useNormalization) {
          rawScore = (displayScore / 100) * CONFIG.userMaxScore;
        }
        work.style.display = rawScore < CONFIG.hideWorksScore ? "none" : "";
      }
    });
  };

  const init = () => {
    checkCountable();

    const cachedElements = {
      allStats: countable ? Array.from($("dl.stats")) : [],
      workElements: Array.from($("li.work, li.bookmark")),
      chaptersContainer: $1("#chapters"),
    };

    initSharedMenu();

    const username = detectAndStoreUsername();

    const runCalculations = () => {
      if (
        (CONFIG.alwaysCountReadingTime && CONFIG.enableReadingTime) ||
        (CONFIG.alwaysCountQualityScore && CONFIG.enableQualityScore)
      ) {
        calculateMetrics(cachedElements.allStats, false, true);

        if (CONFIG.alwaysSortQualityScore && CONFIG.enableQualityScore) {
          const myContentPage = isMyContentPage(username);
          if (!(CONFIG.excludeMyContentFromSort && myContentPage)) {
            sortByRatio(false, cachedElements.allStats);
          }
        }
      }

      // Handle hiding metrics separately
      if (CONFIG.hideMetrics) {
        calculateMetrics(cachedElements.allStats, false, false);
      }

      if (CONFIG.enableChapterStats) {
        calculateChapterStats(cachedElements.chaptersContainer);
      }
    };

    if ("requestIdleCallback" in window) {
      requestIdleCallback(runCalculations, { timeout: 500 });
    } else {
      setTimeout(runCalculations, 0);
    }
  };

  loadUserSettings();

  // Only inject icon styles if icons are enabled
  if (CONFIG.useIcons) {
    addIconStyles();
  }

  // Script initialization complete

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();