Better SEP

Better UI for Stanford Encyclopedia of Philosophy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Better SEP
// @namespace    http://tampermonkey.net/
// @version      1.31
// @description  Better UI for Stanford Encyclopedia of Philosophy
// @author       Wittgensteins Hund
// @match        https://plato.stanford.edu/entries/*
// @match        https://plato.stanford.edu/ENTRIES/*
// @match        https://plato.stanford.edu/Entries/*
// @run-at       document-start
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.location.pathname.endsWith('/notes.html')) return;

    let darkMode = localStorage.getItem('sep-dark-mode') === 'true';
    let notesData = {};
    let bibEntries = [];
    let bibAuthors = {};
    let yearIndex = {};
    let citationText = '';
    let bibtexText = '';

    const excludeWords = new Set([
        'see', 'also', 'but', 'and', 'the', 'for', 'that', 'this', 'with', 'from',
        'have', 'has', 'had', 'are', 'was', 'were', 'been', 'being', 'which', 'what',
        'when', 'where', 'who', 'whom', 'whose', 'why', 'how', 'all', 'each', 'every',
        'both', 'few', 'more', 'most', 'other', 'some', 'such', 'than', 'too', 'very',
        'just', 'only', 'own', 'same', 'into', 'over', 'after', 'before', 'between',
        'under', 'again', 'further', 'then', 'once', 'here', 'there', 'above', 'below',
        'during', 'about', 'against', 'among', 'through', 'while', 'since', 'until',
        'unless', 'although', 'though', 'because', 'therefore', 'however', 'thus',
        'hence', 'moreover', 'furthermore', 'nevertheless', 'nonetheless', 'otherwise',
        'indeed', 'certainly', 'perhaps', 'maybe', 'probably', 'possibly', 'actually',
        'really', 'simply', 'merely', 'especially', 'particularly', 'specifically',
        'generally', 'usually', 'often', 'always', 'never', 'sometimes', 'rarely',
        'according', 'following', 'including', 'regarding', 'concerning', 'given',
        'provided', 'assuming', 'considering', 'note', 'notes', 'section', 'chapter',
        'part', 'volume', 'page', 'pages', 'figure', 'table', 'example', 'examples',
        'case', 'cases', 'point', 'points', 'first', 'second', 'third', 'fourth', 'fifth',
        'last', 'next', 'previous', 'former', 'latter', 'one', 'two', 'three', 'four', 'five'
    ]);

    const css = `
        :root {
            --bg: #ffffff;
            --bg-toc: #f8f7f5;
            --bg-tooltip: #ffffff;
            --text: #1a1a1a;
            --text-secondary: #444;
            --text-muted: #777;
            --link: #2563eb;
            --border: #e5e3df;
            --border-section: #999;
            --shadow: 0 8px 32px rgba(0,0,0,0.12);
            --serif: 'Source Serif Pro', Georgia, serif;
            --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            --mono: 'JetBrains Mono', Consolas, monospace;
            --active-bg: rgba(0, 0, 0, 0.04);
            --active-border: var(--text);
            --btn-bg: rgba(0, 0, 0, 0.7);
            --btn-hover: rgba(0, 0, 0, 0.85);
            --scrollbar-thumb: #c0bdb8;
            --scrollbar-thumb-hover: #a09d98;
        }
        :root.dark {
            --bg: #1a1a18;
            --bg-toc: #222220;
            --bg-tooltip: #2a2a28;
            --text: #e0e0e0;
            --text-secondary: #bbb;
            --text-muted: #888;
            --link: #60a5fa;
            --border: #3a3a36;
            --border-section: #666;
            --shadow: 0 8px 32px rgba(0,0,0,0.5);
            --active-bg: rgba(255, 255, 255, 0.06);
            --active-border: var(--text);
            --btn-bg: rgba(255, 255, 255, 0.15);
            --btn-hover: rgba(255, 255, 255, 0.25);
            --scrollbar-thumb: #555;
            --scrollbar-thumb-hover: #666;
        }

        ::-webkit-scrollbar { width: 12px; height: 12px; }
        ::-webkit-scrollbar-track { background: var(--bg-toc); border-radius: 6px; }
        ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 6px; border: 2px solid var(--bg-toc); }
        ::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-thumb-hover); }
        * { scrollbar-width: auto; scrollbar-color: var(--scrollbar-thumb) var(--bg-toc); }

        html, body { background: var(--bg) !important; color: var(--text) !important; }
        #container, #content, #article, #article-content, #aueditable, #main-text, #preamble,
        #bibliography, #academic-tools, #other-internet-resources, #related-entries,
        #acknowledgments, #article-copyright { background: var(--bg) !important; box-shadow: none !important; border: none !important; }
        #container { width: 100% !important; max-width: none !important; margin: 0 !important; padding: 0 !important; }

        #preamble { text-align: justify; }
        #main-text { text-align: justify; }
        #sep-notes { text-align: justify; }
        #bibliography { text-align: justify; }

        #header-wrapper { background: var(--bg) !important; border-bottom: 1px solid var(--border) !important; }
        #header { background: var(--bg) !important; max-width: 1300px !important; margin: 0 auto !important; padding: 0 40px !important; }
        #branding { background: var(--bg) !important; }
        #branding #site-title a { color: var(--text) !important; }
        :root.dark #branding #site-logo img { filter: brightness(1.5); }
        #navigation i, #footer i { display: none !important; }
        #navigation, #navigation .navbar, #navigation .navbar-inner, #navigation .container { background: var(--bg) !important; border: none !important; box-shadow: none !important; }
        #navigation .nav > li > a { color: var(--text-secondary) !important; }
        #navigation .nav > li > a:hover { color: var(--link) !important; }
        #navigation .dropdown-menu { background: var(--bg-tooltip) !important; border: 1px solid var(--border) !important; }
        #navigation .dropdown-menu a { color: var(--text-secondary) !important; }
        #navigation .dropdown-menu a:hover { color: var(--link) !important; background: var(--active-bg) !important; }
        #search input { background: var(--bg-toc) !important; border: 1px solid var(--border) !important; color: var(--text) !important; border-radius: 8px !important; }
        #search .search-btn { background: transparent !important; color: var(--text-muted) !important; }

        #footer { background: var(--bg-toc) !important; border-top: 1px solid var(--border) !important; padding: 40px 24px !important; margin-top: 80px !important; }
        #footer, #footer * { background-color: transparent !important; }
        #footer h4 { color: var(--text) !important; }
        #footer a { color: var(--link) !important; }
        #footer #site-credits { color: var(--text-muted) !important; }
        #footer .btn { background: var(--bg) !important; border: 1px solid var(--border) !important; color: var(--text) !important; }

        #article-banner, #article-sidebar, #toc, .scroll-block { display: none !important; }

        #content { display: flex !important; flex-direction: row !important; justify-content: center !important; gap: 48px !important; max-width: 1300px !important; margin: 0 auto !important; padding: 48px 40px 80px !important; align-items: flex-start !important; }

        #sep-toc { flex: 0 0 260px !important; width: 260px !important; position: sticky; top: 24px; max-height: calc(100vh - 48px); overflow-y: auto; font-family: var(--sans); font-size: 14px; padding: 20px 0 40px; background: var(--bg-toc); border-radius: 12px; }
        #sep-toc-title { font-weight: 600; font-size: 11px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; padding: 0 20px; }
        #sep-toc-top { display: block; padding: 10px 20px; color: var(--text-secondary) !important; text-decoration: none !important; font-weight: 500; font-size: 13px; border-bottom: 1px solid var(--border); margin-bottom: 12px; }
        #sep-toc-top:hover { color: var(--text) !important; }
        #sep-toc ul { list-style: none !important; margin: 0 !important; padding: 0 !important; }
        #sep-toc li { margin: 0 !important; }
        #sep-toc a { display: block; padding: 7px 20px; color: var(--text-muted) !important; text-decoration: none !important; line-height: 1.4; border-left: 3px solid transparent; }
        #sep-toc a:hover { color: var(--text) !important; background: var(--active-bg); }
        #sep-toc a.active { color: var(--text) !important; background: var(--active-bg) !important; border-left-color: var(--active-border) !important; font-weight: 600; }
        #sep-toc .toc-sub { font-size: 13px; }
        #sep-toc .toc-sub a { padding-left: 36px; }
        #sep-toc .toc-divider { height: 1px; background: var(--border); margin: 16px 20px; }
        #sep-toc .toc-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-bottom: 12px; padding: 0 20px; }

        #article { flex: 0 1 780px !important; max-width: 780px !important; padding: 0 !important; margin: 0 !important; }
        #article-content { border: none !important; }

        #sep-header { margin-bottom: 48px; text-align: center; padding-bottom: 32px; border-bottom: 2px solid var(--border); }
        #sep-header h1 { font-family: var(--serif) !important; font-size: 44px !important; font-weight: 700 !important; color: var(--text) !important; margin: 0 0 20px !important; line-height: 1.2 !important; letter-spacing: -0.3px !important; }
        #sep-header-authors { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px 16px; font-family: var(--sans); font-size: 15px; font-weight: 600; color: var(--text-secondary); margin-bottom: 12px; }
        #sep-header-authors a { color: var(--text-secondary) !important; text-decoration: none !important; }
        #sep-header-authors a:hover { text-decoration: underline !important; }
        #sep-header-authors .sep-author-sep { color: var(--text-muted); font-weight: 400; }
        #sep-header-meta { display: flex; flex-direction: column; align-items: center; gap: 4px; }
        #sep-header-date { font-family: var(--sans); font-size: 13px; color: var(--text-muted); }
        #aueditable > h1:first-of-type, #pubinfo { display: none !important; }

        #aueditable { font-family: var(--serif) !important; font-size: 19px !important; line-height: 1.8 !important; color: var(--text) !important; }
        #aueditable h2 { font-family: var(--sans) !important; font-size: 28px !important; font-weight: 600 !important; color: var(--text) !important; margin: 64px 0 12px !important; padding-bottom: 12px !important; border-bottom: 2px solid var(--border-section) !important; }
        #aueditable h2 a, #aueditable h3 a {
            color: inherit !important;
            text-decoration: none !important;
            pointer-events: none;
        }
        #aueditable h3 { font-family: var(--sans) !important; font-size: 21px !important; font-weight: 600 !important; color: var(--text) !important; margin: 40px 0 16px !important; }
        #aueditable h4 { font-family: var(--sans) !important; font-size: 17px !important; font-weight: 600 !important; color: var(--text-secondary) !important; margin: 32px 0 12px !important; }
        a { color: var(--link) !important; text-decoration: none !important; }
        a:hover { text-decoration: underline !important; }
        sup a { font-size: 13px !important; font-weight: 500; padding: 1px 3px; border-radius: 3px; }
        sup a:hover { background: var(--link) !important; color: white !important; text-decoration: none !important; }
        blockquote { margin: 28px 0 !important; padding: 20px 24px !important; background: var(--bg-toc) !important; border: none !important; border-left: 3px solid var(--text-muted) !important; border-radius: 0 10px 10px 0 !important; color: var(--text-secondary) !important; font-style: italic; }

        .sep-cite { color: var(--link) !important; cursor: pointer; border-bottom: 1px dashed currentColor; }
        .sep-cite:hover { background: var(--active-bg); border-bottom-style: solid; text-decoration: none !important; }

        #sep-tip { position: fixed; max-width: 520px; padding: 16px 20px; background: var(--bg-tooltip); color: var(--text); border-radius: 12px; font-family: var(--sans); font-size: 14px; line-height: 1.6; z-index: 100000; box-shadow: var(--shadow); border: 1px solid var(--border); opacity: 0; visibility: hidden; pointer-events: none; }
        #sep-tip.show { opacity: 1; visibility: visible; pointer-events: auto; }
        #sep-tip-label { font-weight: 600; font-size: 13px; color: var(--text); margin-bottom: 8px; }
        #sep-tip-text { color: var(--text-secondary); max-height: 400px; overflow-y: auto; }
        #sep-tip-text em, #sep-tip-text i { font-style: italic !important; }
        #sep-tip-text strong, #sep-tip-text b { font-weight: bold !important; }
        .sep-tip-entry { padding: 10px 0; border-bottom: 1px solid var(--border); }
        .sep-tip-entry:last-child { border-bottom: none; padding-bottom: 0; }
        .sep-tip-entry:first-child { padding-top: 0; }

        #sep-notes { margin-top: 64px; padding-top: 32px; border-top: 1px solid var(--border); }
        #sep-notes h2 { font-family: var(--sans) !important; font-size: 28px !important; font-weight: 600 !important; margin: 0 0 24px !important; padding-bottom: 12px !important; border-bottom: 2px solid var(--border-section) !important; }
        .sep-note-item { display: flex; gap: 12px; padding: 16px 0 !important; border-bottom: 1px solid var(--border) !important; font-size: 15px !important; line-height: 1.7 !important; color: var(--text-secondary) !important; font-family: var(--sans) !important; }
        .sep-note-item:last-child { border-bottom: none !important; }
        .sep-note-num { flex: 0 0 32px; font-weight: 600; color: var(--text); text-align: right; }
        .sep-note-content { flex: 1; }
        .sep-note-back { font-size: 12px; margin-left: 8px; opacity: 0.6; }

        #bibliography { margin-top: 64px; padding-top: 32px; border-top: 1px solid var(--border); }
        #bibliography h2 { font-family: var(--sans) !important; font-size: 28px !important; font-weight: 600 !important; margin: 0 0 24px !important; padding-bottom: 12px !important; border-bottom: 2px solid var(--border-section) !important; }
        #bibliography ul { padding: 0 !important; margin: 0 !important; }
        #bibliography li { padding: 16px 0 16px 44px !important; margin: 0 !important; border-bottom: 1px solid var(--border) !important; font-size: 15px !important; line-height: 1.7 !important; list-style: none !important; color: var(--text-secondary) !important; font-family: var(--sans) !important; text-indent: -44px; }
        #bibliography li:last-child { border-bottom: none !important; }
        #bibliography li em, #bibliography li i { color: var(--text) !important; font-style: italic !important; }
        #bibliography a[href*="doi"] { font-family: var(--mono) !important; font-size: 11px !important; color: var(--text-muted) !important; background: var(--bg-toc); padding: 2px 6px; border-radius: 4px; }
        #bibliography img { display: none !important; }

        #academic-tools, #other-internet-resources, #related-entries, #acknowledgments { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--border) !important; }
        #academic-tools h2, #other-internet-resources h2, #related-entries h2, #acknowledgments h2 { font-family: var(--sans) !important; font-size: 24px !important; font-weight: 600 !important; margin: 0 0 20px !important; padding-bottom: 10px !important; border-bottom: 2px solid var(--border-section) !important; }
        #academic-tools img { display: none !important; }
        #article-copyright { margin-top: 48px; padding-top: 24px; border-top: 1px solid var(--border) !important; color: var(--text-muted) !important; font-size: 14px !important; font-family: var(--sans) !important; }
        #article-copyright a { color: var(--link) !important; }

        #sep-top-btns { position: fixed; top: 20px; right: 20px; display: flex; gap: 10px; z-index: 10000; }
        .sep-btn { width: 44px; height: 44px; border: none; border-radius: 50%; background: var(--btn-bg); color: #fff; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 12px rgba(0,0,0,0.15); transition: all 0.2s; backdrop-filter: blur(8px); }
        .sep-btn:hover { background: var(--btn-hover); transform: translateY(-2px); box-shadow: 0 4px 16px rgba(0,0,0,0.2); }
        #sep-cite-btn { font-family: Georgia, serif; font-size: 20px; font-weight: bold; }

        #sep-back-top { position: fixed; bottom: 24px; right: 24px; width: 48px; height: 48px; border: none; border-radius: 50%; background: var(--btn-bg); color: #fff; cursor: pointer; font-size: 20px; display: none; align-items: center; justify-content: center; box-shadow: 0 2px 12px rgba(0,0,0,0.15); z-index: 9999; transition: all 0.2s; backdrop-filter: blur(8px); }
        #sep-back-top.show { display: flex; }
        #sep-back-top:hover { background: var(--btn-hover); transform: translateY(-2px); }

        #sep-cite-modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 100001; display: none; align-items: center; justify-content: center; padding: 20px; }
        #sep-cite-modal.show { display: flex; }
        #sep-cite-box { background: var(--bg); border-radius: 16px; padding: 28px; max-width: 600px; width: 100%; box-shadow: 0 20px 60px rgba(0,0,0,0.3); border: 1px solid var(--border); }
        #sep-cite-title { font-family: var(--sans); font-size: 18px; font-weight: 600; color: var(--text); margin-bottom: 16px; display: flex; justify-content: space-between; align-items: center; }
        #sep-cite-close { background: none; border: none; font-size: 24px; color: var(--text-muted); cursor: pointer; padding: 0; line-height: 1; }
        #sep-cite-close:hover { color: var(--text); }
        #sep-cite-text { font-family: var(--sans); font-size: 15px; line-height: 1.7; color: var(--text-secondary); background: var(--bg-toc); padding: 16px; border-radius: 8px; margin-bottom: 16px; user-select: text; }
        #sep-cite-copy { background: var(--text); color: var(--bg); border: none; padding: 10px 20px; border-radius: 8px; font-family: var(--sans); font-size: 14px; cursor: pointer; }
        #sep-cite-tabs { display: flex; gap: 0; margin-bottom: 16px; border-bottom: 1px solid var(--border); }
        .sep-cite-tab { background: none; border: none; padding: 10px 20px; font-family: var(--sans); font-size: 14px; font-weight: 500; color: var(--text-muted); cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; transition: all 0.2s; }
        .sep-cite-tab:hover { color: var(--text); }
        .sep-cite-tab.active { color: var(--text); border-bottom-color: var(--text); }
        #sep-cite-bibtex { font-family: var(--mono); font-size: 12px; line-height: 1.5; color: var(--text-secondary); background: var(--bg-toc); padding: 16px; border-radius: 8px; margin-bottom: 16px; user-select: text; white-space: pre-wrap; word-break: break-all; display: none; max-height: 300px; overflow-y: auto; }
        #sep-cite-copy:hover { opacity: 0.9; }
        #sep-cite-copy.copied { background: #22c55e; color: white; }

        #sep-progress { position: fixed; top: 0; left: 0; height: 2px; background: var(--text); z-index: 99999; width: 0; }

        html { scroll-behavior: smooth; }
        @media (max-width: 900px) {
            #content { flex-direction: column !important; padding: 24px 16px 60px !important; }
            #sep-toc { display: none !important; }
            #article { max-width: 100% !important; }
            #sep-header h1 { font-size: 30px !important; }
            #aueditable { font-size: 17px !important; }
        }

        @media print {
          #sep-toc, #sep-cite-btn, #sep-dark-toggle, #sep-dark, .sep-cite-tooltip, .sep-btn, #academic-tools, #other-internet-resources, #related-entries { display: none !important; }
          #header { max-width: 100% !important; padding: 20px 0 !important; }
          #header h1 { font-size: 24px !important; }
          #content { display: block !important; }
          #article { max-width: 100% !important; margin: 0 !important; padding: 0 !important; }
          #main-text { max-width: 100% !important; }
          #sep-header { padding: 0 0 16px !important; margin-bottom: 24px !important; border-bottom: 1px solid #ccc !important; background: none !important; }
          #sep-header h1 { font-size: 28px !important; }
          body { background: white !important; color: black !important; }
          a { color: black !important; }
          a[href]:after { content: none !important; }
        }
    `;

    const style = document.createElement('style');
    style.textContent = css;
    (document.head || document.documentElement).appendChild(style);

    function main() {
        const font = document.createElement('link');
        font.href = 'https://fonts.googleapis.com/css2?family=Source+Serif+Pro:ital,wght@0,400;0,600;1,400&display=swap';
        font.rel = 'stylesheet';
        document.head.appendChild(font);

        function setDark(on) {
            document.documentElement.classList.toggle('dark', on);
            localStorage.setItem('sep-dark-mode', on);
            darkMode = on;
            const btn = document.getElementById('sep-dark');
            if (btn) btn.textContent = on ? '☀' : '☾';
        }

        function extractAuthors() {
            const copyright = document.getElementById('article-copyright');
            if (!copyright) return [];

            const authors = [];
            const html = copyright.innerHTML;

            // Split by <br> tags to get each author line
            const lines = html.split(/<br\s*\/?>/gi);

            for (const line of lines) {
                // Skip copyright line and empty lines
                if (line.includes('Copyright') || line.trim() === '') continue;

                // Create a temporary element to parse the line
                const temp = document.createElement('div');
                temp.innerHTML = line;

                // Remove email parts (anything in angle brackets)
                const emailParts = temp.querySelectorAll('a[href^="mailto"], a[href^="m"]');
                emailParts.forEach(el => {
                    // Find the surrounding < > and remove them too
                    let node = el;
                    while (node.previousSibling && node.previousSibling.textContent.includes('<')) {
                        node.previousSibling.textContent = node.previousSibling.textContent.replace(/&lt;|</, '');
                    }
                    while (node.nextSibling && node.nextSibling.textContent.includes('>')) {
                        node.nextSibling.textContent = node.nextSibling.textContent.replace(/&gt;|>/, '');
                    }
                    el.remove();
                });

                // Get remaining text/links
                const text = temp.textContent.replace(/[<>]/g, '').trim();
                if (!text) continue;

                // Check if there's a profile link (not mailto)
                const profileLink = temp.querySelector('a[href^="http"], a[href^="/"]');

                if (profileLink) {
                    authors.push({
                        name: text,
                        url: profileLink.href
                    });
                } else if (text) {
                    authors.push({
                        name: text,
                        url: null
                    });
                }
            }

            return authors;
        }

        function createHeader() {
            const h1 = document.querySelector('#aueditable > h1');
            const pubinfo = document.getElementById('pubinfo');
            const aueditable = document.getElementById('aueditable');
            if (!h1 || !aueditable) return;

            const header = document.createElement('div');
            header.id = 'sep-header';

            const title = document.createElement('h1');
            title.textContent = h1.textContent;
            header.appendChild(title);

            // Add authors
            const authors = extractAuthors();
            if (authors.length > 0) {
                const authorsDiv = document.createElement('div');
                authorsDiv.id = 'sep-header-authors';

                authors.forEach((author, i) => {
                    if (author.url) {
                        const link = document.createElement('a');
                        link.href = author.url;
                        link.textContent = author.name;
                        link.target = '_blank';
                        authorsDiv.appendChild(link);
                    } else {
                        const span = document.createElement('span');
                        span.textContent = author.name;
                        authorsDiv.appendChild(span);
                    }

                    if (i < authors.length - 1) {
                        const sep = document.createElement('span');
                        sep.className = 'sep-author-sep';
                        sep.textContent = i === authors.length - 2 ? ' & ' : ', ';
                        authorsDiv.appendChild(sep);
                    }
                });

                header.appendChild(authorsDiv);
            }

            const meta = document.createElement('div');
            meta.id = 'sep-header-meta';

            if (pubinfo) {
                const dateDiv = document.createElement('div');
                dateDiv.id = 'sep-header-date';
                dateDiv.textContent = pubinfo.textContent.trim();
                meta.appendChild(dateDiv);
            }

            header.appendChild(meta);
            aueditable.insertBefore(header, aueditable.firstChild);
        }

        async function fetchNotes() {
            try {
                let basePath = window.location.pathname;
                if (!basePath.endsWith('/')) basePath += '/';
                const res = await fetch(basePath + 'notes.html');
                if (!res.ok) return false;

                const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
                const noteDivs = doc.querySelectorAll('div[id^="note-"]');
                if (!noteDivs.length) return false;

                const section = document.createElement('div');
                section.id = 'sep-notes';
                section.innerHTML = '<h2 id="notes">Notes</h2>';

                const list = document.createElement('div');
                list.id = 'sep-notes-list';

                noteDivs.forEach(div => {
                    const num = div.id.replace('note-', '');
                    const p = div.querySelector('p');
                    if (!p) return;
                    let html = p.innerHTML.replace(/^\s*<a[^>]*>\d+\.<\/a>\s*/, '');
                    notesData[num] = html;
                    const item = document.createElement('div');
                    item.className = 'sep-note-item';
                    item.id = div.id;
                    item.innerHTML = `<span class="sep-note-num">${num}.</span><span class="sep-note-content">${html}<a href="#ref-${num}" class="sep-note-back">↩</a></span>`;
                    list.appendChild(item);
                });

                section.appendChild(list);
                const bib = document.getElementById('bibliography');
                if (bib) bib.parentNode.insertBefore(section, bib);

                document.querySelectorAll('a[href*="notes.html#"]').forEach(a => {
                    const m = a.href.match(/#(note-\d+)/);
                    if (m) a.href = '#' + m[1];
                });

                return true;
            } catch (e) { return false; }
        }

        function buildTOC(hasNotes) {
            const content = document.getElementById('content');
            const aueditable = document.getElementById('aueditable');
            if (!content || !aueditable) return;

            const toc = document.createElement('div');
            toc.id = 'sep-toc';

            const top = document.createElement('a');
            top.id = 'sep-toc-top';
            top.href = '#';
            top.textContent = '↑ Top';
            top.onclick = e => { window.scrollTo({ top: 0, behavior: 'smooth' }); };
            toc.appendChild(top);

            const title = document.createElement('div');
            title.id = 'sep-toc-title';
            title.textContent = 'Contents';
            toc.appendChild(title);

            const list = document.createElement('ul');
            const skip = ['bibliography', 'academic tools', 'other internet', 'related entries', 'acknowledgments', 'notes'];
            let currentLi = null, currentSub = null;

            aueditable.querySelectorAll('h2, h3').forEach(h => {
                // Get ID from the anchor's name attribute, or generate one
                const anchor = h.querySelector('a[name]');
                if (anchor && anchor.name && !h.id) {
                    h.id = anchor.name;
                } else if (!h.id) {
                    h.id = 'section-' + h.textContent.trim().toLowerCase()
                        .replace(/[^a-z0-9]+/g, '-')
                        .replace(/^-|-$/g, '')
                        .slice(0, 40);
                }

                const text = h.textContent.trim();
                if (skip.some(s => text.toLowerCase().includes(s))) return;
                if (h.tagName === 'H2') {
                    const li = document.createElement('li');
                    const a = document.createElement('a');
                    a.href = '#' + h.id;
                    a.textContent = text;
                    li.appendChild(a);
                    list.appendChild(li);
                    currentLi = li;
                    currentSub = null;
                } else if (h.tagName === 'H3' && currentLi) {
                    if (!currentSub) { currentSub = document.createElement('ul'); currentSub.className = 'toc-sub'; currentLi.appendChild(currentSub); }
                    const li = document.createElement('li');
                    const a = document.createElement('a');
                    a.href = '#' + h.id;
                    a.textContent = text;
                    li.appendChild(a);
                    currentSub.appendChild(li);
                }
            });
            toc.appendChild(list);

            toc.appendChild(Object.assign(document.createElement('div'), { className: 'toc-divider' }));
            toc.appendChild(Object.assign(document.createElement('div'), { className: 'toc-label', textContent: 'Resources' }));

            const res = document.createElement('ul');
            [hasNotes && { id: 'notes', text: 'Notes' }, { id: 'Bib', alt: 'bibliography', text: 'Bibliography' }, { id: 'Aca', alt: 'academic-tools', text: 'Academic Tools' }, { id: 'Oth', alt: 'other-internet-resources', text: 'Other Internet Resources' }, { id: 'Rel', alt: 'related-entries', text: 'Related Entries' }].filter(Boolean).forEach(item => {
                const el = document.getElementById(item.id) || document.getElementById(item.alt);
                if (el) { const li = document.createElement('li'); const a = document.createElement('a'); a.href = '#' + el.id; a.textContent = item.text; li.appendChild(a); res.appendChild(li); }
            });
            toc.appendChild(res);
            content.insertBefore(toc, content.firstChild);
        }

        function normalize(str) {
            return str.toLowerCase()
                .normalize('NFD').replace(/[\u0300-\u036f]/g, '')
                .replace(/[''`]/g, "'")
                .replace(/[-–—]/g, '-')
                .trim();
        }

        function buildBibIndex() {
            let currentAuthorFull = '';
            let prevEntryHtml = '';
            const seenEntries = new Set();

            document.querySelectorAll('#bibliography li').forEach((li, i) => {
                li.id = li.id || 'bib-' + i;
                const text = li.textContent.trim();
                const htmlContent = li.innerHTML;

                const isContinuation = /^[–—-]{2,}/.test(text);

                let authorFull;
                let displayHtml = htmlContent;

                if (isContinuation) {
                    authorFull = currentAuthorFull;
                    const prevAuthorMatch = prevEntryHtml.match(/^([^,]+(?:,\s*[^,]+)?)\s*[,.]?\s*(?:\d{4}|\()/);
                    if (prevAuthorMatch) {
                        displayHtml = htmlContent.replace(/^[–—-]{2,}[,.]?\s*/, prevAuthorMatch[1] + ', ');
                    }
                } else {
                    const authorMatch = text.match(/^(.+?)\s*[,.]?\s*(?:\d{4}|\(?\s*(?:eds?\.?|forthcoming|manuscript|unpublished|in press))/i);
                    if (authorMatch) {
                        authorFull = authorMatch[1].trim();
                        currentAuthorFull = authorFull;
                        prevEntryHtml = htmlContent;
                    }
                }

                if (!authorFull) return;

                const yearMatch = text.match(/\b(1[5-9]\d{2}|20[0-2]\d)([a-z])?\b/);
                if (!yearMatch) return;

                const year = yearMatch[1] + (yearMatch[2] || '');
                const yearClean = yearMatch[1];

                const entryKey = normalize(authorFull) + '|' + year + '|' + li.id;
                if (seenEntries.has(entryKey)) return;
                seenEntries.add(entryKey);

                const surnames = [];
                const authorNorm = authorFull.replace(/\b(van|von|de|du|la|le|der|den|di)\s+/gi, (m) => m.toLowerCase());
                const authorParts = authorNorm.split(/\s*(?:,\s*(?:and\s+)?|&|\s+and\s+)\s*/i);

                authorParts.forEach(part => {
                    const commaParts = part.split(',');
                    if (commaParts.length >= 1) {
                        let surname = commaParts[0].trim();
                        const prefixMatch = surname.match(/^(van|von|de|du|la|le|der|den|di)\s+(.+)$/i);
                        if (prefixMatch) {
                            surnames.push(normalize(prefixMatch[1] + ' ' + prefixMatch[2]));
                            surnames.push(normalize(prefixMatch[2]));
                        } else {
                            surnames.push(normalize(surname));
                        }
                    }
                });

                const entry = {
                    el: li,
                    authorFull: authorFull,
                    year: year,
                    yearClean: yearClean,
                    text: displayHtml,
                    surnames: surnames
                };

                bibEntries.push(entry);

                if (!yearIndex[year]) yearIndex[year] = [];
                yearIndex[year].push(entry);

                if (year !== yearClean) {
                    if (!yearIndex[yearClean]) yearIndex[yearClean] = [];
                    yearIndex[yearClean].push(entry);
                }

                surnames.forEach(sn => {
                    const key = sn + '|' + year;
                    if (!bibAuthors[key]) bibAuthors[key] = [];
                    bibAuthors[key].push(entry);

                    if (year !== yearClean) {
                        const keyClean = sn + '|' + yearClean;
                        if (!bibAuthors[keyClean]) bibAuthors[keyClean] = [];
                        if (!bibAuthors[keyClean].includes(entry)) {
                            bibAuthors[keyClean].push(entry);
                        }
                    }
                });
            });
        }

        function findEntry(authorText, year) {
            if (!authorText || !year) return null;

            if (excludeWords.has(authorText.toLowerCase())) return null;

            const authNorm = normalize(authorText);
            const yearClean = year.replace(/[a-z]$/i, '');

            if (authNorm.length < 3) return null;

            let key = authNorm + '|' + year;
            if (bibAuthors[key]) return bibAuthors[key][0];

            key = authNorm + '|' + yearClean;
            if (bibAuthors[key]) return bibAuthors[key][0];

            for (const k in bibAuthors) {
                const [sn, y] = k.split('|');
                if (y !== year && y !== yearClean) continue;

                if (sn === authNorm) return bibAuthors[k][0];
                if (sn.endsWith(' ' + authNorm)) return bibAuthors[k][0];
                if (authNorm.endsWith(' ' + sn)) return bibAuthors[k][0];
            }

            return null;
        }

        function findEntriesByAuthorYears(authorText, years) {
            if (!authorText || !years || years.length === 0) return [];
            if (excludeWords.has(authorText.toLowerCase())) return [];

            const entries = [];
            for (const year of years) {
                const entry = findEntry(authorText, year);
                if (entry) entries.push(entry);
            }
            return entries;
        }

        function isExcludedWord(word) {
            return excludeWords.has(word.toLowerCase());
        }

        function linkCitations() {
            if (bibEntries.length === 0) return;

            ['#main-text', '#preamble', '#sep-notes-list'].forEach(sel => {
                const container = document.querySelector(sel);
                if (!container) return;

                const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
                    acceptNode: n => {
                        if (n.parentElement.closest('a, script, style, h1, h2, h3, h4, sup, .sep-cite'))
                            return NodeFilter.FILTER_REJECT;
                        return NodeFilter.FILTER_ACCEPT;
                    }
                });

                const nodes = [];
                while (walker.nextNode()) nodes.push(walker.currentNode);

                nodes.forEach(node => {
                    const text = node.textContent;
                    if (!/\d{4}/.test(text)) return;

                    const replacements = [];

                    // Pattern for Author (year, year, year) or Author (year; year) or Author (year and year)
                    const multiYearPat = /([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+)\s*\(\s*((?:1[5-9]\d{2}|20[0-2]\d)[a-z]?(?:\s*(?:[,;]|and)\s*(?:1[5-9]\d{2}|20[0-2]\d)[a-z]?)+)\s*\)/g;
                    let match;

                    while ((match = multiYearPat.exec(text)) !== null) {
                        const author = match[1];
                        if (isExcludedWord(author)) continue;

                        const yearsStr = match[2];
                        const years = yearsStr.match(/(1[5-9]\d{2}|20[0-2]\d)[a-z]?/g) || [];

                        const entries = findEntriesByAuthorYears(author, years);
                        if (entries.length > 0) {
                            const seenIds = new Set();
                            const uniqueEntries = entries.filter(e => {
                                if (seenIds.has(e.el.id)) return false;
                                seenIds.add(e.el.id);
                                return true;
                            });

                            const entriesData = uniqueEntries.map(e => ({
                                id: e.el.id,
                                author: e.authorFull,
                                year: e.year,
                                text: e.text
                            }));

                            const link = document.createElement('a');
                            link.href = '#' + entries[0].el.id;
                            link.className = 'sep-cite';
                            link.textContent = match[0];
                            link.dataset.entriesJson = JSON.stringify(entriesData);

                            replacements.push({
                                start: match.index,
                                end: match.index + match[0].length,
                                html: link.outerHTML
                            });
                        }
                    }

                    const yearRegex = /\b(1[5-9]\d{2}|20[0-2]\d)([a-z])?\b/g;

                    while ((match = yearRegex.exec(text)) !== null) {
                        // Skip if already covered by multi-year pattern
                        if (replacements.some(r => match.index >= r.start && match.index < r.end)) continue;

                        const year = match[1] + (match[2] || '');
                        const yearClean = match[1];
                        const yearStart = match.index;
                        const yearEnd = yearStart + match[0].length;

                        const before = text.slice(0, yearStart);
                        const after = text.slice(yearEnd);

                        let linkStart = yearStart;
                        let linkEnd = yearEnd;
                        let entries = [];
                        let matched = false;

                        // Pattern: Author et al. (year)
                        const etAlPat = /([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+)\s+et\s+al\.?\s*\(?\s*$/i;
                        let m = before.match(etAlPat);
                        if (m && !isExcludedWord(m[1])) {
                            const entry = findEntry(m[1], year);
                            if (entry) {
                                entries = [entry];
                                linkStart = before.lastIndexOf(m[1]);
                                matched = true;
                            }
                        }

                        // Pattern: Author, Author, & Author (year)
                        if (!matched) {
                            const threeAuthPat = /([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+),\s+([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+),?\s+(?:&|and)\s+([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+)\s*\(?\s*$/i;
                            m = before.match(threeAuthPat);
                            if (m && !isExcludedWord(m[1]) && !isExcludedWord(m[2]) && !isExcludedWord(m[3])) {
                                const entry = findEntry(m[1], year) || findEntry(m[2], year) || findEntry(m[3], year);
                                if (entry) {
                                    entries = [entry];
                                    linkStart = before.lastIndexOf(m[1]);
                                    matched = true;
                                }
                            }
                        }

                        // Pattern: Author & Author (year)
                        if (!matched) {
                            const twoPat = /([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+)\s+(?:&|and)\s+([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+)\s*\(?\s*$/i;
                            m = before.match(twoPat);
                            if (m && !isExcludedWord(m[1]) && !isExcludedWord(m[2])) {
                                const entry = findEntry(m[1], year) || findEntry(m[2], year);
                                if (entry) {
                                    entries = [entry];
                                    linkStart = before.lastIndexOf(m[1]);
                                    matched = true;
                                }
                            }
                        }

                        // Pattern: van Fraassen, de Finetti, etc.
                        if (!matched) {
                            const prefixPat = /((?:van|von|de|du|la|le|der|den|di)\s+[A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+)\s*\(?\s*$/i;
                            m = before.match(prefixPat);
                            if (m) {
                                const entry = findEntry(m[1], year);
                                if (entry) {
                                    entries = [entry];
                                    linkStart = before.search(new RegExp(m[1].replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*\\(?\\s*$', 'i'));
                                    matched = true;
                                }
                            }
                        }

                        // Pattern: Single Author
                        if (!matched) {
                            const singlePat = /([A-ZÀ-ÖØ-Þ][a-zA-ZÀ-ÖØ-öø-ÿ'''-]+)'?s?\s*\(?\s*$/;
                            m = before.match(singlePat);
                            if (m && !isExcludedWord(m[1]) && m[1].length >= 3) {
                                const entry = findEntry(m[1], year);
                                if (entry) {
                                    entries = [entry];
                                    linkStart = before.lastIndexOf(m[1]);
                                    matched = true;
                                }
                            }
                        }

                        // Pattern: Standalone year in parentheses
                        if (!matched) {
                            const parenBefore = /\(\s*$/.test(before);
                            const parenAfter = /^[a-z]?\s*\)/.test(after);

                            if (parenBefore && parenAfter && yearIndex[year]) {
                                entries = yearIndex[year];
                                matched = true;
                            }
                        }

                        if (entries.length > 0) {
                            const extraMatch = after.match(/^([a-z])?(\s*\[\d{4}\])?(:\s*[\w\d]+(?:[-–][\w\d]+)?)?(\s*(?:ch\.?|chapter|sect?\.?|§)\s*[\d.]+)?(ff\.?)?/i);
                            if (extraMatch && extraMatch[0]) {
                                linkEnd = yearEnd + extraMatch[0].length;
                            }

                            const linkText = text.slice(linkStart, linkEnd);
                            const primary = entries[0];

                            const seenIds = new Set();
                            const uniqueEntries = entries.filter(e => {
                                if (seenIds.has(e.el.id)) return false;
                                seenIds.add(e.el.id);
                                return true;
                            });

                            const entriesData = uniqueEntries.map(e => ({
                                id: e.el.id,
                                author: e.authorFull,
                                year: e.year,
                                text: e.text
                            }));

                            const link = document.createElement('a');
                            link.href = '#' + primary.el.id;
                            link.className = 'sep-cite';
                            link.textContent = linkText;
                            link.dataset.entriesJson = JSON.stringify(entriesData);

                            replacements.push({
                                start: linkStart,
                                end: linkEnd,
                                html: link.outerHTML
                            });
                        }
                    }

                    if (replacements.length === 0) return;

                    replacements.sort((a, b) => b.start - a.start);
                    const filtered = [];
                    let minStart = Infinity;
                    for (const r of replacements) {
                        if (r.end <= minStart) {
                            filtered.push(r);
                            minStart = r.start;
                        }
                    }

                    let html = text;
                    for (const r of filtered) {
                        html = html.slice(0, r.start) + r.html + html.slice(r.end);
                    }

                    const span = document.createElement('span');
                    span.innerHTML = html;
                    node.parentNode.replaceChild(span, node);
                });
            });
        }

        function initTooltip() {
            const tip = document.createElement('div');
            tip.id = 'sep-tip';
            tip.innerHTML = '<div id="sep-tip-label"></div><div id="sep-tip-text"></div>';
            document.body.appendChild(tip);

            let timeout;
            const show = (el, label, html) => {
                clearTimeout(timeout);
                document.getElementById('sep-tip-label').textContent = label;
                document.getElementById('sep-tip-text').innerHTML = html;

                const r = el.getBoundingClientRect();
                let top = r.bottom + 10;
                if (top + 200 > window.innerHeight) top = r.top - 220;
                tip.style.top = Math.max(10, top) + 'px';
                tip.style.left = Math.max(16, Math.min(r.left, window.innerWidth - 540)) + 'px';
                tip.classList.add('show');
            };
            const hide = () => { timeout = setTimeout(() => tip.classList.remove('show'), 250); };

            tip.onmouseenter = () => clearTimeout(timeout);
            tip.onmouseleave = hide;

            document.querySelectorAll('sup a').forEach(a => {
                const m = a.href.match(/#note-?(\d+)/);
                if (m && notesData[m[1]]) {
                    a.onmouseenter = () => show(a, 'Note ' + m[1], notesData[m[1]]);
                    a.onmouseleave = hide;
                }
            });

            document.querySelectorAll('.sep-cite').forEach(a => {
                a.onmouseenter = () => {
                    try {
                        const entries = JSON.parse(a.dataset.entriesJson);
                        if (entries.length === 1) {
                            show(a, entries[0].author + ' (' + entries[0].year + ')', entries[0].text);
                        } else {
                            const label = entries.length + ' references';
                            const html = entries.map(e =>
                                `<div class="sep-tip-entry"><strong>${e.author} (${e.year})</strong><br>${e.text}</div>`
                            ).join('');
                            show(a, label, html);
                        }
                    } catch (e) {}
                };
                a.onmouseleave = hide;
            });
        }

        function initCiteModal() {
            const modal = document.createElement('div');
            modal.id = 'sep-cite-modal';
            modal.innerHTML = `<div id="sep-cite-box">
                <div id="sep-cite-title"><span>Cite This Entry</span><button id="sep-cite-close">×</button></div>
                <div id="sep-cite-tabs">
                    <button class="sep-cite-tab active" data-tab="text">Text Citation</button>
                    <button class="sep-cite-tab" data-tab="bibtex">BibTeX</button>
                </div>
                <div id="sep-cite-text">Loading...</div>
                <div id="sep-cite-bibtex">Loading...</div>
                <button id="sep-cite-copy">Copy to Clipboard</button>
            </div>`;
            document.body.appendChild(modal);
            modal.onclick = e => { if (e.target === modal) modal.classList.remove('show'); };
            document.getElementById('sep-cite-close').onclick = () => modal.classList.remove('show');

            // Tab switching
            modal.querySelectorAll('.sep-cite-tab').forEach(tab => {
                tab.onclick = () => {
                    modal.querySelectorAll('.sep-cite-tab').forEach(t => t.classList.remove('active'));
                    tab.classList.add('active');
                    const isText = tab.dataset.tab === 'text';
                    document.getElementById('sep-cite-text').style.display = isText ? 'block' : 'none';
                    document.getElementById('sep-cite-bibtex').style.display = isText ? 'none' : 'block';
                };
            });

            document.getElementById('sep-cite-copy').onclick = async () => {
                const btn = document.getElementById('sep-cite-copy');
                const activeTab = modal.querySelector('.sep-cite-tab.active').dataset.tab;
                const textToCopy = activeTab === 'text' ? citationText : bibtexText;
                try {
                    await navigator.clipboard.writeText(textToCopy);
                    btn.textContent = 'Copied!'; btn.classList.add('copied');
                    setTimeout(() => { btn.textContent = 'Copy to Clipboard'; btn.classList.remove('copied'); }, 2000);
                } catch (e) {}
            };
        }

        async function showCitationModal() {
            const modal = document.getElementById('sep-cite-modal');
            const textEl = document.getElementById('sep-cite-text');
            const bibtexEl = document.getElementById('sep-cite-bibtex');

            modal.classList.add('show');

            if (!citationText) {
                textEl.textContent = 'Loading citation...';
                bibtexEl.textContent = 'Loading BibTeX...';

                const entry = window.location.pathname.split(/\/entries\//i)[1]?.replace(/\/$/, '');
                try {
                    const res = await fetch(`https://plato.stanford.edu/cgi-bin/encyclopedia/archinfo.cgi?entry=${entry}`);
                    const doc = new DOMParser().parseFromString(await res.text(), 'text/html');
                    citationText = doc.querySelector('blockquote')?.textContent.trim().replace(/\s+/g, ' ') || 'Citation not found';
                    bibtexText = doc.querySelector('pre')?.textContent.trim() || 'BibTeX not found';
                } catch (e) {
                    citationText = 'Failed to load citation';
                    bibtexText = 'Failed to load BibTeX';
                }
            }

            textEl.textContent = citationText;
            bibtexEl.textContent = bibtexText;
        }

        function initControls() {
            const btns = document.createElement('div');
            btns.id = 'sep-top-btns';
            btns.innerHTML = `<button class="sep-btn" id="sep-cite-btn" title="Cite this entry (C)">❝</button><button class="sep-btn" id="sep-dark" title="Dark mode (D)">${darkMode ? '☀' : '☾'}</button>`;
            document.body.appendChild(btns);
            document.getElementById('sep-cite-btn').onclick = showCitationModal;
            document.getElementById('sep-dark').onclick = () => setDark(!darkMode);

            const back = document.createElement('button');
            back.id = 'sep-back-top';
            back.innerHTML = '⌃';
            back.onclick = () => window.scrollTo({ top: 0, behavior: 'smooth' });
            document.body.appendChild(back);
            window.addEventListener('scroll', () => back.classList.toggle('show', scrollY > 500), { passive: true });
        }

        function initProgress() {
            const bar = document.createElement('div');
            bar.id = 'sep-progress';
            document.body.appendChild(bar);
            window.addEventListener('scroll', () => { bar.style.width = Math.min(100, scrollY / (document.body.scrollHeight - innerHeight) * 100) + '%'; }, { passive: true });
        }

        function initActive() {
            const links = document.querySelectorAll('#sep-toc a:not(#sep-toc-top)');
            const sections = [];
            links.forEach(a => { const el = document.getElementById(a.getAttribute('href')?.slice(1)); if (el) sections.push({ a, el }); });
            const update = () => { let cur = sections[0]; sections.forEach(s => { if (s.el.getBoundingClientRect().top < 150) cur = s; }); links.forEach(a => a.classList.remove('active')); cur?.a.classList.add('active'); };
            window.addEventListener('scroll', update, { passive: true });
            update();
        }

        document.addEventListener('keydown', e => {
            if (e.target.matches('input,textarea')) return;
            if (e.key === 'd') setDark(!darkMode);
            if (e.key === 't') window.scrollTo({ top: 0, behavior: 'smooth' });
            if (e.key === 'c' && !e.ctrlKey && !e.metaKey) showCitationModal();
            if (e.key === 'Escape') document.getElementById('sep-cite-modal')?.classList.remove('show');
        });

        async function init() {
            setDark(darkMode);
            const hasNotes = await fetchNotes();
            createHeader();
            buildTOC(hasNotes);
            initCiteModal();
            initControls();
            initProgress();
            initActive();
            setTimeout(() => {
                buildBibIndex();
                linkCitations();
                initTooltip();
            }, 100);
        }

        init();
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', main);
    else main();
})();