AO3: Site Wizard

Make AO3 easier to read: customize fonts and sizes across the entire site, adjust work reader margins, fix spacing issues, and configure text alignment preferences.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          AO3: Site Wizard
// @version       3.5
// @description   Make AO3 easier to read: customize fonts and sizes across the entire site, adjust work reader margins, fix spacing issues, and configure text alignment preferences.
// @author        Blackbatcat
// @match         *://archiveofourown.org/*
// @license       MIT
// @require       https://update.greasyfork.org/scripts/552743/1690921/AO3%3A%20Menu%20Helpers%20Library.js?v=2.1.2
// @grant         none
// @run-at        document-start
// @namespace https://greasyfork.org/users/1498004
// ==/UserScript==

(function () {
  "use strict";

  // --- CONSTANTS ---
  const FORMATTER_CONFIG_KEY = "ao3_wizard_config";
  const DEFAULT_FORMATTER_CONFIG = {
    paragraphWidthPercent: 70,
    paragraphFontSizePercent: 100,
    paragraphTextAlign: "left",
    paragraphFontFamily: "",
    fixParagraphSpacing: true,
    paragraphGap: 1.286,
    siteFontFamily: "",
    siteFontWeight: "",
    siteFontSizePercent: 100,
    headerFontFamily: "",
    headerFontWeight: "",
    codeFontFamily: "",
    codeFontStyle: "normal",
    codeFontSize: "",
    expandCodeFontUsage: false,
  };

  const WORKS_PAGE_REGEX =
    /^https?:\/\/archiveofourown\.org\/(?:.*\/)?(works|chapters)(\/|$)/;

  // --- STATE ---
  let FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };
  let cachedElements = {
    paraStyle: null,
    siteStyle: null,
  };

  // --- UTILITIES ---
  function getOrCreateStyle(id) {
    if (!document.head) return null;
    let style = document.getElementById(id);
    if (!style) {
      style = document.createElement("style");
      style.id = id;
      document.head.appendChild(style);
    }
    return style;
  }

  function loadFormatterConfig() {
    try {
      const saved = localStorage.getItem(FORMATTER_CONFIG_KEY);
      if (saved) {
        FORMATTER_CONFIG = {
          ...DEFAULT_FORMATTER_CONFIG,
          ...JSON.parse(saved),
        };
      }
    } catch (e) {
      console.error("Error loading config:", e);
    }
  }

  function saveFormatterConfig() {
    try {
      localStorage.setItem(
        FORMATTER_CONFIG_KEY,
        JSON.stringify(FORMATTER_CONFIG)
      );
    } catch (e) {
      console.error("Error saving config:", e);
    }
  }

  // --- APPLY STYLES ---
  function applyParagraphWidth() {
    if (!cachedElements.paraStyle) {
      cachedElements.paraStyle = getOrCreateStyle(
        "ao3-formatter-paragraph-style"
      );
      if (!cachedElements.paraStyle) return;
    }

    if (WORKS_PAGE_REGEX.test(window.location.href)) {
      const {
        paragraphWidthPercent,
        paragraphFontSizePercent,
        paragraphTextAlign,
        paragraphGap,
      } = FORMATTER_CONFIG;

      cachedElements.paraStyle.textContent = `
        #workskin p { text-align: ${paragraphTextAlign} !important; }
        ${
          paragraphTextAlign === "justify" || paragraphTextAlign === "left"
            ? `#workskin dd { text-align: ${paragraphTextAlign} !important; }`
            : ""
        }
        ${
          paragraphTextAlign === "justify" || paragraphTextAlign === "left"
            ? `#workskin blockquote { text-align: ${paragraphTextAlign} !important; }`
            : ""
        }
        #workskin {
          max-width: ${paragraphWidthPercent}vw !important;
          font-size: ${paragraphFontSizePercent}% !important;
        }
        #workskin p {
          margin-bottom: ${paragraphGap}em !important;
        }
        #workskin p[align] {
          text-align: ${paragraphTextAlign} !important;
        }
        ${
          paragraphTextAlign === "right"
            ? `
        #workskin ul, #workskin ol {
          direction: rtl !important;
          text-align: right !important;
        }
        #workskin li {
          text-align: right !important;
        }
        #workskin dl {
          direction: rtl !important;
        }
        #workskin dt, #workskin dd {
          text-align: right !important;
        }
        #workskin blockquote {
          text-align: right !important;
        }
        #workskin summary {
          text-align: right !important;
        }
        #workskin h1, #workskin h2, #workskin h3,
        #workskin h4, #workskin h5, #workskin h6 {
          text-align: right !important;
        }
        `
            : ""
        }
      `;

      const workskin = document.getElementById("workskin");
      if (workskin) {
        if (paragraphTextAlign === "right") {
          workskin.setAttribute("dir", "rtl");
        } else {
          workskin.removeAttribute("dir");
        }
      }
    } else {
      cachedElements.paraStyle.textContent = "";
    }

    applySiteWideStyles();
  }

  function applySiteWideStyles() {
    if (!cachedElements.siteStyle) {
      cachedElements.siteStyle = getOrCreateStyle("ao3-sitewide-style");
      if (!cachedElements.siteStyle) return;
    }

    const {
      siteFontSizePercent,
      siteFontFamily,
      siteFontWeight,
      headerFontFamily,
      headerFontWeight,
      paragraphFontFamily,
      codeFontFamily,
      codeFontStyle,
      codeFontSize,
      expandCodeFontUsage,
    } = FORMATTER_CONFIG;

    const rules = [];

    rules.push(`html { font-size: ${siteFontSizePercent}% !important; }`);

    if (siteFontFamily) {
      if (expandCodeFontUsage) {
        rules.push(
          `body, body *:not(textarea):not(textarea *):not(code):not(pre):not(tt):not(kbd):not(samp):not(var), input:not([type="file"]), select, button:not(.comment-format button):not(ul.comment-format button) { font-family: ${siteFontFamily} !important; }`
        );
      } else {
        rules.push(
          `body, body *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var), input:not([type="file"]), textarea:not(#skin_css):not(#floaty-textarea), select, button:not(.comment-format button):not(ul.comment-format button) { font-family: ${siteFontFamily} !important; }`
        );
      }
    }

    if (siteFontWeight) {
      const textareaSelector = expandCodeFontUsage
        ? ""
        : ", textarea:not(#skin_css):not(#floaty-textarea)";

      rules.push(
        `body, body *, input:not([type="file"])${textareaSelector}, select, button:not(.comment-format button):not(ul.comment-format button) { font-weight: ${siteFontWeight} !important; }`
      );
    }

    if (paragraphFontFamily) {
      const textareaExclusion = expandCodeFontUsage ? ":not(textarea)" : "";

      if (headerFontFamily) {
        rules.push(
          `#workskin:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6),
           #workskin *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(h1 *):not(h2 *):not(h3 *):not(h4 *):not(h5 *):not(h6 *)${textareaExclusion} { font-family: ${paragraphFontFamily} !important; }`
        );
      } else {
        rules.push(
          `#workskin, #workskin *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var)${textareaExclusion} { font-family: ${paragraphFontFamily} !important; }`
        );
      }
    }

    if (headerFontFamily) {
      rules.push(
        `h1, h1 *, h2, h2 *, h3, h3 *, h4, h4 *, h5, h5 *, h6, h6 *, .heading, .heading *,
         #workskin h1, #workskin h1 *, #workskin h2, #workskin h2 *, #workskin h3, #workskin h3 *,
         #workskin h4, #workskin h4 *, #workskin h5, #workskin h5 *, #workskin h6, #workskin h6 * { font-family: ${headerFontFamily} !important; }`
      );
    } else if (paragraphFontFamily) {
      rules.push(
        `#chapters h3.title,
         #chapters h3.byline.heading,
         .chapter .preface h3.title,
         .chapter .preface h3.byline.heading,
         .preface h3.title,
         .preface h3.byline { font-family: ${paragraphFontFamily} !important; }`
      );
    }

    if (headerFontWeight) {
      rules.push(
        `h1, h1 *, h2, h2 *, h3, h3 *, h4, h4 *, h5, h5 *, h6, h6 *, .heading, .heading *,
         #workskin h1, #workskin h1 *, #workskin h2, #workskin h2 *, #workskin h3, #workskin h3 *,
         #workskin h4, #workskin h4 *, #workskin h5, #workskin h5 *, #workskin h6, #workskin h6 * { font-weight: ${headerFontWeight} !important; }`
      );
    }

    const codeRules = [];
    if (codeFontFamily)
      codeRules.push(`font-family: ${codeFontFamily} !important`);
    if (codeFontStyle && codeFontStyle !== "normal")
      codeRules.push(`font-style: ${codeFontStyle} !important`);
    if (codeFontSize) codeRules.push(`font-size: ${codeFontSize} !important`);

    if (codeRules.length > 0) {
      const baseCodeSelectors =
        "code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, textarea#skin_css, .css.module blockquote pre, #floaty-textarea, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var *";

      const codeSelectors = expandCodeFontUsage
        ? "code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, textarea, textarea#skin_css, .css.module blockquote pre, #floaty-textarea, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var *, #workskin textarea"
        : baseCodeSelectors;

      rules.push(`${codeSelectors} { ${codeRules.join("; ")}; }`);
    }

    if (codeRules.length === 0) {
      rules.push(
        `code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var * { font-family: monospace !important; }`
      );

      if (expandCodeFontUsage) {
        rules.push(
          `textarea, #workskin textarea { font-family: monospace !important; }`
        );
      }
    }

    rules.push(
      `#workskin .preface .title.heading,
       #workskin .preface .byline.heading,
       #workskin .preface .title,
       #workskin .preface .byline,
       #workskin .title.heading,
       #workskin .byline.heading {
         text-align: center !important;
         direction: ltr !important;
       }`
    );

    rules.push(
      `#workskin pre {
         text-align: left !important;
         direction: ltr !important;
       }`
    );

    rules.push(
      `#cmtFmtDialog #stdbutton label, ul.comment-format, ul.comment-format * { font-family: "FontAwesome", sans-serif !important; font-weight: normal !important; }`,
      `ul.actions.comment-format { text-align: left !important; }`
    );

    cachedElements.siteStyle.textContent = rules.join("\n");
  }

  // --- PARAGRAPH SPACING FIX ---
  const fixParagraphSpacing = (() => {
    function removeLeadingBrs(userstuff) {
      userstuff.querySelectorAll("p").forEach((p) => {
        let changed = true;
        while (changed) {
          changed = false;
          if (p.firstChild?.tagName === "BR") {
            p.firstChild.remove();
            changed = true;
          } else if (
            p.firstChild?.nodeType === Node.TEXT_NODE &&
            !p.firstChild.textContent.trim()
          ) {
            p.firstChild.remove();
            changed = true;
          }
        }
      });
    }

    function removeTrailingBrs(userstuff) {
      userstuff.querySelectorAll("p").forEach((p) => {
        let changed = true;
        while (changed) {
          changed = false;
          if (p.lastChild?.tagName === "BR") {
            p.lastChild.remove();
            changed = true;
          } else if (
            p.lastChild?.nodeType === Node.TEXT_NODE &&
            !p.lastChild.textContent.trim()
          ) {
            p.lastChild.remove();
            changed = true;
          }
        }
      });
    }

    function removeEmptyParagraphs(userstuff) {
      userstuff.querySelectorAll("p").forEach((p) => {
        const content = p.textContent?.replace(/\u00A0/g, "").trim();
        if (!content && !p.querySelector("img, embed, iframe, video, br")) {
          p.remove();
        }
      });
    }

    function removeEmptyElement(el) {
      const content = el.textContent?.replace(/\u00A0/g, "").trim();
      if (
        !content &&
        el.tagName !== "BR" &&
        el.tagName !== "HR" &&
        !el.querySelector("img, embed, iframe, video")
      ) {
        el.remove();
      }
    }

    function reduceBrs(userstuff) {
      const brs = Array.from(userstuff.querySelectorAll("br"));

      for (let i = 0; i < brs.length; i++) {
        const br = brs[i];
        let consecutiveCount = 1;
        let nextNode = br.nextSibling;

        while (nextNode) {
          if (
            nextNode.nodeType === Node.ELEMENT_NODE &&
            nextNode.tagName === "BR"
          ) {
            consecutiveCount++;
            nextNode = nextNode.nextSibling;
          } else if (
            nextNode.nodeType === Node.TEXT_NODE &&
            !nextNode.textContent.trim()
          ) {
            nextNode = nextNode.nextSibling;
          } else {
            break;
          }
        }

        if (consecutiveCount >= 3) {
          let toRemove = consecutiveCount - 2;
          nextNode = br.nextSibling;

          while (toRemove > 0 && nextNode) {
            const current = nextNode;
            nextNode = nextNode.nextSibling;

            if (
              current.nodeType === Node.ELEMENT_NODE &&
              current.tagName === "BR"
            ) {
              current.remove();
              toRemove--;
            }
          }
        }
      }
    }

    const BLOCK_TAGS = [
      "div",
      "blockquote",
      "ul",
      "ol",
      "table",
      "h1",
      "h2",
      "h3",
      "h4",
      "h5",
      "h6",
    ];

    return function () {
      if (!WORKS_PAGE_REGEX.test(window.location.href)) return;

      document
        .querySelectorAll(
          "#workskin .userstuff:not([data-formatter-spacing-fixed])"
        )
        .forEach((userstuff) => {
          userstuff.setAttribute("data-formatter-spacing-fixed", "true");

          removeLeadingBrs(userstuff);
          removeTrailingBrs(userstuff);
          removeEmptyParagraphs(userstuff);

          BLOCK_TAGS.forEach((tag) => {
            userstuff.querySelectorAll(tag).forEach((child) => {
              removeEmptyElement(child);
            });
          });

          reduceBrs(userstuff);
        });
    };
  })();

  // --- SETTINGS MENU ---
  function showFormatterMenu() {
    // Safety check: ensure library is loaded
    if (!window.AO3MenuHelpers) {
      console.error("[AO3: Site Wizard] Menu Helpers library not loaded");
      alert(
        "Error: Menu Helpers library not loaded. Please check your userscript manager."
      );
      return;
    }

    window.AO3MenuHelpers.removeAllDialogs();

    const dialog = window.AO3MenuHelpers.createDialog(
      "🪄 Site Wizard Settings 🪄",
      {
        maxWidth: "700px",
      }
    );

    // Site-Wide Display Section
    const siteSection = window.AO3MenuHelpers.createSection(
      "📱 Site-Wide Display"
    );

    const siteFontSize = window.AO3MenuHelpers.createSliderWithValue({
      id: "site-fontsize-input",
      label: "Base Font Size",
      min: 50,
      max: 200,
      step: 5,
      value: FORMATTER_CONFIG.siteFontSizePercent,
      unit: "%",
      tooltip:
        "Adjust the overall text size for the entire site (percentage of browser default)",
    });
    siteSection.appendChild(siteFontSize);

    const siteFontFamily = window.AO3MenuHelpers.createTextInput({
      id: "site-fontfamily-input",
      label: "General Text Font",
      value: FORMATTER_CONFIG.siteFontFamily,
      placeholder: "Figtree, sans-serif",
      tooltip: "Font for most site text",
    });

    const siteFontWeight = window.AO3MenuHelpers.createTextInput({
      id: "site-fontweight-input",
      label: "Font Weight",
      value: FORMATTER_CONFIG.siteFontWeight,
      placeholder: "400, normal",
      tooltip: "Boldness of general text",
    });

    const siteFontRow = window.AO3MenuHelpers.createTwoColumnLayout(
      siteFontFamily,
      siteFontWeight
    );
    siteSection.appendChild(siteFontRow);

    dialog.appendChild(siteSection);

    // Work Formatting Section
    const workSection =
      window.AO3MenuHelpers.createSection("📖 Work Formatting");

    const workWidth = window.AO3MenuHelpers.createSliderWithValue({
      id: "paragraph-width-slider",
      label: "Work Margin Width",
      min: 10,
      max: 100,
      step: 5,
      value: FORMATTER_CONFIG.paragraphWidthPercent,
      unit: "%",
      tooltip: "Maximum width of work reader",
    });
    workSection.appendChild(workWidth);

    const workFontSize = window.AO3MenuHelpers.createSliderWithValue({
      id: "paragraph-fontsize-slider",
      label: "Work Font Size",
      min: 50,
      max: 200,
      step: 5,
      value: FORMATTER_CONFIG.paragraphFontSizePercent,
      unit: "%",
      tooltip: "Size relative to site base size",
    });
    workSection.appendChild(workFontSize);

    const workFont = window.AO3MenuHelpers.createTextInput({
      id: "paragraph-fontfamily-input",
      label: "Work Font",
      value: FORMATTER_CONFIG.paragraphFontFamily,
      placeholder: "Figtree, sans-serif",
      tooltip: "Font family for reader",
    });
    workSection.appendChild(workFont);

    const textAlign = window.AO3MenuHelpers.createSelect({
      id: "paragraph-align-select",
      label: "Text Alignment",
      options: [
        {
          value: "left",
          label: "Left Aligned",
          selected: FORMATTER_CONFIG.paragraphTextAlign === "left",
        },
        {
          value: "justify",
          label: "Justified",
          selected: FORMATTER_CONFIG.paragraphTextAlign === "justify",
        },
        {
          value: "right",
          label: "Right Aligned",
          selected: FORMATTER_CONFIG.paragraphTextAlign === "right",
        },
      ],
      tooltip: "How text is aligned within paragraphs",
    });

    const lineSpacing = window.AO3MenuHelpers.createNumberInput({
      id: "paragraph-gap-input",
      label: "Line Spacing",
      value: FORMATTER_CONFIG.paragraphGap,
      min: 0,
      step: 0.1,
      tooltip:
        "Vertical space between paragraphs (multiplier). Default is 1.286.",
    });

    const alignSpacingRow = window.AO3MenuHelpers.createTwoColumnLayout(
      textAlign,
      lineSpacing
    );
    workSection.appendChild(alignSpacingRow);

    const fixSpacing = window.AO3MenuHelpers.createCheckbox({
      id: "fix-paragraph-spacing-checkbox",
      label: "Fix excessive paragraph spacing",
      checked: FORMATTER_CONFIG.fixParagraphSpacing,
      tooltip: "Remove unnecessary blank space between paragraphs",
    });
    workSection.appendChild(fixSpacing);

    dialog.appendChild(workSection);

    // Element-Specific Fonts Section
    const elementSection = window.AO3MenuHelpers.createSection(
      "🎯 Element-Specific Fonts"
    );

    const headerFont = window.AO3MenuHelpers.createTextInput({
      id: "header-fontfamily-input",
      label: "Header Font",
      value: FORMATTER_CONFIG.headerFontFamily,
      placeholder: "Figtree, sans-serif",
      tooltip: "Font for headings (H1-H6)",
    });

    const headerWeight = window.AO3MenuHelpers.createTextInput({
      id: "header-fontweight-input",
      label: "Header Weight",
      value: FORMATTER_CONFIG.headerFontWeight,
      placeholder: "700, bold",
      tooltip: "Boldness of header text",
    });

    const headerRow = window.AO3MenuHelpers.createTwoColumnLayout(
      headerFont,
      headerWeight
    );
    elementSection.appendChild(headerRow);

    const codeFont = window.AO3MenuHelpers.createTextInput({
      id: "code-fontfamily-input",
      label: "Code/Monospace Font",
      value: FORMATTER_CONFIG.codeFontFamily,
      placeholder: "Victor Mono Medium, monospace",
      tooltip: "Font for code blocks and preformatted text",
    });
    elementSection.appendChild(codeFont);

    const codeFontSize = window.AO3MenuHelpers.createTextInput({
      id: "code-fontsize-input",
      label: "Code Font Size",
      value: FORMATTER_CONFIG.codeFontSize,
      placeholder: "0.9em, 14px",
      tooltip: "Size relative to surrounding text",
    });

    const codeFontStyle = window.AO3MenuHelpers.createSelect({
      id: "code-fontstyle-select",
      label: "Code Font Style",
      options: [
        {
          value: "normal",
          label: "Normal",
          selected:
            !FORMATTER_CONFIG.codeFontStyle ||
            FORMATTER_CONFIG.codeFontStyle === "normal",
        },
        {
          value: "italic",
          label: "Italic",
          selected: FORMATTER_CONFIG.codeFontStyle === "italic",
        },
      ],
      tooltip: "Style for code text",
    });

    const codeRow = window.AO3MenuHelpers.createTwoColumnLayout(
      codeFontSize,
      codeFontStyle
    );
    elementSection.appendChild(codeRow);

    const expandCodeFont = window.AO3MenuHelpers.createCheckbox({
      id: "expand-code-font-checkbox",
      label: "Apply code font to comments",
      checked: FORMATTER_CONFIG.expandCodeFontUsage,
      tooltip:
        "Applies code font to all textareas. Requires a code/monospace font to be specified above.",
    });
    elementSection.appendChild(expandCodeFont);

    dialog.appendChild(elementSection);

    // Buttons
    const buttons = window.AO3MenuHelpers.createButtonGroup([
      { text: "Save", id: "formatter-save" },
      { text: "Cancel", id: "formatter-cancel" },
    ]);
    dialog.appendChild(buttons);

    // Reset Link
    const resetLink = window.AO3MenuHelpers.createResetLink(
      "Reset to Default Settings",
      () => {
        FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };
        saveFormatterConfig();
        dialog.remove();
        applyParagraphWidth();
      }
    );
    dialog.appendChild(resetLink);

    // Event Handlers
    dialog.querySelector("#formatter-save").addEventListener("click", () => {
      FORMATTER_CONFIG.siteFontSizePercent =
        window.AO3MenuHelpers.getValue("site-fontsize-input") ||
        DEFAULT_FORMATTER_CONFIG.siteFontSizePercent;
      FORMATTER_CONFIG.siteFontFamily =
        window.AO3MenuHelpers.getValue("site-fontfamily-input") || "";
      FORMATTER_CONFIG.siteFontWeight =
        window.AO3MenuHelpers.getValue("site-fontweight-input") || "";
      FORMATTER_CONFIG.paragraphWidthPercent =
        window.AO3MenuHelpers.getValue("paragraph-width-slider") ||
        DEFAULT_FORMATTER_CONFIG.paragraphWidthPercent;
      FORMATTER_CONFIG.paragraphFontSizePercent =
        window.AO3MenuHelpers.getValue("paragraph-fontsize-slider") ||
        DEFAULT_FORMATTER_CONFIG.paragraphFontSizePercent;
      FORMATTER_CONFIG.paragraphTextAlign =
        window.AO3MenuHelpers.getValue("paragraph-align-select") ||
        DEFAULT_FORMATTER_CONFIG.paragraphTextAlign;
      FORMATTER_CONFIG.paragraphFontFamily =
        window.AO3MenuHelpers.getValue("paragraph-fontfamily-input") || "";
      FORMATTER_CONFIG.paragraphGap =
        window.AO3MenuHelpers.getValue("paragraph-gap-input") ||
        DEFAULT_FORMATTER_CONFIG.paragraphGap;
      FORMATTER_CONFIG.fixParagraphSpacing =
        window.AO3MenuHelpers.getValue("fix-paragraph-spacing-checkbox") ??
        false;
      FORMATTER_CONFIG.headerFontFamily =
        window.AO3MenuHelpers.getValue("header-fontfamily-input") || "";
      FORMATTER_CONFIG.headerFontWeight =
        window.AO3MenuHelpers.getValue("header-fontweight-input") || "";
      FORMATTER_CONFIG.codeFontFamily =
        window.AO3MenuHelpers.getValue("code-fontfamily-input") || "";
      FORMATTER_CONFIG.codeFontStyle =
        window.AO3MenuHelpers.getValue("code-fontstyle-select") || "normal";
      FORMATTER_CONFIG.codeFontSize =
        window.AO3MenuHelpers.getValue("code-fontsize-input") || "";
      FORMATTER_CONFIG.expandCodeFontUsage =
        window.AO3MenuHelpers.getValue("expand-code-font-checkbox") ?? false;

      saveFormatterConfig();
      dialog.remove();
      applyParagraphWidth();

      if (FORMATTER_CONFIG.paragraphTextAlign === "right") {
        location.reload();
      }
    });

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

    document.body.appendChild(dialog);
  }

  // --- SHARED MENU MANAGEMENT ---
  function initSharedMenu() {
    if (window.AO3MenuHelpers) {
      window.AO3MenuHelpers.addToSharedMenu({
        id: "opencfg_site_wizard",
        text: "Site Wizard",
        onClick: showFormatterMenu,
      });
    }
  }

  // --- INITIALIZATION ---
  loadFormatterConfig();
  console.log("[AO3: Site Wizard] loaded.");

  function initStyles() {
    if (document.head) {
      applyParagraphWidth();
    } else {
      const observer = new MutationObserver(() => {
        if (document.head) {
          observer.disconnect();
          applyParagraphWidth();
        }
      });
      observer.observe(document.documentElement, { childList: true });
    }
  }

  function runParagraphSpacingFixIfEnabled() {
    if (
      FORMATTER_CONFIG.fixParagraphSpacing &&
      WORKS_PAGE_REGEX.test(window.location.href)
    ) {
      fixParagraphSpacing();
    }
  }

  initStyles();

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