AO3: Reading Time & Quality Score

Add reading time, chapter reading time, and quality scores to AO3 works with color coding, score normalization and sorting. Version 4 uses a new scoring model.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        AO3: Reading Time & Quality Score
// @version     4.0.0
// @description  Add reading time, chapter reading time, and quality scores to AO3 works with color coding, score normalization and sorting. Version 4 uses a new scoring model.
// @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";

  const SCRIPT_VERSION = "4.0.0";

  // 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: true,
    userMaxScore: 22,
    minKudosToShowScore: 50,
    colorThresholdLow: 8,
    colorThresholdHigh: 14,
    colorStyle: "background",
    colorGreen: "#3e8fb0",
    colorYellow: "#f6c177",
    colorRed: "#eb6f92",
    colorText: "#ffffff",
    useIcons: false,
    iconColor: "",
    chapterTimeStyle: "default",
    username: "",
    hideWorksEnabled: false,
    hideWorksScore: 4,
    keepUnscoredVisible: false,
    lastSeenVersion: null,
    scoringBannerSeenVersion: null,
  };

  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 saveAllSettings = () => {
    if (typeof Storage !== "undefined") {
      localStorage.setItem(
        "ao3_reading_quality_config",
        JSON.stringify(CONFIG)
      );
    }
  };

  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 };
      }
    }
  };

  loadUserSettings();
  migrateToV4IfNeeded();

  function migrateToV4IfNeeded() {
    const prev = CONFIG.lastSeenVersion || null;
    if (prev === SCRIPT_VERSION) return;

    // FORCE RESET of score-related settings for everyone:
    CONFIG.userMaxScore = 22;
    if (CONFIG.useNormalization) {
      CONFIG.colorThresholdLow = 40;
      CONFIG.colorThresholdHigh = 60;
      CONFIG.hideWorksScore = 20;
    } else {
      CONFIG.colorThresholdLow = 8;
      CONFIG.colorThresholdHigh = 14;
      CONFIG.hideWorksScore = 4;
    }

    CONFIG.lastSeenVersion = SCRIPT_VERSION;
    saveAllSettings();
  }

  // Show migration banner if not seen for this version
  if (CONFIG.scoringBannerSeenVersion !== SCRIPT_VERSION) {
    showMigrationBanner();
  }

  function showMigrationBanner() {
    // Inject CSS
    const style = document.createElement("style");
    style.textContent = `
        .qs-migrate-banner {
            padding: 8px 12px;
            font-size: 0.9em;
            border: none;
            border-bottom: 1px solid;
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 10px;
            z-index: 99999;
            border-radius: 0;
            box-shadow: none;
            margin-top: 0;
        }
        .qs-migrate-banner button {
            background: none;
            border: none;
            padding: 2px 6px;
            cursor: pointer;
            font-size: 0.85em;
            color: inherit;
        }
        .qs-migrate-banner button:hover {
            background: none;
        }
    `;
    document.head.appendChild(style);

    // Create Banner
    const banner = document.createElement("div");
    banner.className = "qs-migrate-banner notice";
    banner.innerHTML = `
        <span>
            <strong>AO3 Reading Time & Quality Score v4.1:</strong>
            The scoring model has been improved. Score settings were updated to fit the new version. <a href="https://greasyfork.org/en/scripts/549777-ao3-reading-time-quality-score" target="_blank">Read more here.</a>
        </span>
        <button class="qs-close-btn">✕</button>
    `;

    // Insert at top of page
    const target =
      document.querySelector("#outer") || // AO3 main container
      document.body;

    target.prepend(banner);

    // Close button behavior
    banner.querySelector(".qs-close-btn").addEventListener("click", () => {
      banner.remove();
    });

    // Mark as seen
    CONFIG.scoringBannerSeenVersion = SCRIPT_VERSION;
    saveAllSettings();
  }

  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 };
      CONFIG.userMaxScore = 22;
      if (CONFIG.useNormalization) {
        CONFIG.colorThresholdLow = 40;
        CONFIG.colorThresholdHigh = 60;
        CONFIG.hideWorksScore = 20;
      } else {
        CONFIG.colorThresholdLow = 8;
        CONFIG.colorThresholdHigh = 14;
        CONFIG.hideWorksScore = 4;
      }
      CONFIG.keepUnscoredVisible = CONFIG.hideWorksEnabled ? true : false;
      CONFIG.scoringBannerSeenVersion = SCRIPT_VERSION;
      saveAllSettings();
      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 (!kudos || !hits || !words) return 0;

    const eff = Math.max(1, words / 5000);
    const adjustedHits = hits / Math.pow(eff, 0.4); // alpha model
    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.colorThresholdLow;
    const normalizedThresholdHigh = 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);

          // Validate that we have valid numbers
          if (isNaN(hits) || isNaN(kudos) || hits === 0) {
            return;
          }

          if (kudos >= CONFIG.minKudosToShowScore) {
            // Skip scoring works with 0 words
            if (words === 0) {
              // Remove any existing score display and attribute
              const existingScoreDt = $1("dt.kudoshits", statsElement);
              const existingScoreDd = $1("dd.kudoshits", statsElement);
              if (existingScoreDt) existingScoreDt.remove();
              if (existingScoreDd) existingScoreDd.remove();
              if (parentLi) parentLi.removeAttribute("kudospercent");
              return;
            }

            let rawScore = calculateWordBasedScore(kudos, hits, words);
              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")
            );
            parentLi.style.display =
              displayScore < 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.colorThresholdLow;
          const normalizedThresholdHigh = 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.colorThresholdLow;
          const normalizedThresholdHigh = 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.colorThresholdLow;
    const displayThresholdHigh = CONFIG.colorThresholdHigh;

    const displayHideWorksScore = 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,
        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 = "Max Raw Score ";

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

    userMaxScoreLabel.appendChild(document.createTextNode(" "));
    userMaxScoreLabel.appendChild(
      window.AO3MenuHelpers.createTooltip(
        "The top score you want to treat as 100%. Use a solid high performer, not the single highest spike."
      )
    );

    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: 100,
      step: 1,
    });

    const keepUnscoredCheckbox = window.AO3MenuHelpers.createCheckbox({
      id: "keepUnscoredVisible",
      label: "Show unscored works",
      checked: CONFIG.hideWorksEnabled ? true : 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 = 40;
          thresholdHighInput.value = 60;
        } else {
          normLabel.textContent = "";
          thresholdLowLabel.textContent = "";
          thresholdHighLabel.textContent = "";
          userMaxScoreContainer.style.display = "none";
          thresholdLowInput.value = 8;
          thresholdHighInput.value = 14;
        }

        const hideWorksScoreInput = dialog.querySelector("#hideWorksScore");
        const hideWorksScoreLabel = dialog.querySelector(
          "#hideWorksScoreLabel"
        );
        if (isNormalizationEnabled) {
          hideWorksScoreLabel.textContent = "(%)";
          hideWorksScoreInput.value = 20;
          hideWorksScoreInput.max = 100;
        } else {
          hideWorksScoreLabel.textContent = "";
          hideWorksScoreInput.value = 4;
          hideWorksScoreInput.max = 100;
        }
      });

    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("#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;

      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;
      // If hideWorksEnabled is unchecked, force keepUnscoredVisible to false
      if (!CONFIG.hideWorksEnabled) {
        CONFIG.keepUnscoredVisible = false;
      }
      CONFIG.hideWorksScore = parseFloat(
        dialog.querySelector("#hideWorksScore").value
      );

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