MediaWiki:Common.js: Difference between revisions

No edit summary
Tag: Reverted
No edit summary
 
(94 intermediate revisions by 2 users not shown)
Line 1: Line 1:
/* MediaWiki:Common.js — grantha.io  (v7)
/* MediaWiki:Common.js */
*
* Changes vs v6:
*  1. BUG FIX: Main IIFE was never properly closed — About link block was
*    pasted inside it without the closing }() ); which broke the module
*    boundary and killed the by-Author toggle. Each block is now its
*    own self-contained IIFE.
*  2. TOC: "Beginning" link removed.
*  3. TOC: Heading label → "विषयसूची".
*  4. TOC: मूल / उल्लेख nav links injected at top of TOC panel.
*  5. TOC: All subsections expanded by default.
*  6. TOC: Only the active item is bold; all others are normal weight.
*  7. About link URL derived from wgArticlePath — works under any
*    sub-path install (e.g. /My_wiki/). Points to "My_wiki:About".
*  8. TOC customisations skipped on Main_Page and About pages.
*  9. Uses Vector's native TOC with IntersectionObserver-based active highlight.
*/


( function () {
(function () {


  var LS_SCRIPT_KEY = 'grantha_reader_script';
    var LS_SCRIPT_KEY = 'grantha_reader_script';
  var currentScript = 'deva';
    var currentScript = 'deva';


   // ── IAST transliteration ────────────────────────────────────────
    function devanagariToTamil(text) {
  function devanagariToIAST( text ) {
        var CONSONANTS = {
    var CONSONANTS = {
            'क': 'க', 'ख': 'க', 'ग': 'க', 'घ': 'க', 'ङ': 'ங',
      'क':'k','ख':'kh','ग':'g','घ':'gh','ङ':'ṅ',
            'च': 'ச', 'छ': 'ச', 'ज': 'ஜ', 'झ': 'ஜ', 'ञ': 'ஞ',
      'च':'c','छ':'ch','ज':'j','झ':'jh','ञ':'ñ',
            'ट': 'ட', 'ठ': 'ட', 'ड': 'ட', 'ढ': 'ட', 'ण': 'ண',
      'ट':'ṭ','ठ':'ṭh','ड':'ḍ','ढ':'ḍh','ण':'ṇ',
            'त': 'த', 'थ': 'த', 'द': 'த', 'ध': 'த', 'न': 'ந',
      'त':'t','थ':'th','द':'d','ध':'dh','न':'n',
            'प': 'ப', 'फ': 'ப', 'ब': 'ப', 'भ': 'ப', 'म': 'ம',
      'प':'p','फ':'ph','ब':'b','भ':'bh','म':'m',
            'य': 'ய', 'र': 'ர', 'ल': 'ல', 'ळ': 'ள', 'व': 'வ',
      'य':'y','र':'r','ल':'l','ळ':'ḷ','व':'v',
            'श': 'ஶ', 'ष': 'ஷ', 'स': 'ஸ', 'ह': 'ஹ'
      'श':'ś','ष':'ṣ','स':'s','ह':'h'
        };
        var DIACRITICS = {
            'ा': 'ா', 'ि': 'ி', 'ी': 'ீ', 'ु': 'ு', 'ू': 'ூ',
            'े': 'ே', 'ै': 'ை', 'ो': 'ோ', 'ौ': 'ௌ'
        };
        var VOWELS = {
            'अ': 'அ', 'आ': 'ஆ', 'इ': 'இ', 'ई': 'ஈ', 'उ': 'உ', 'ऊ': 'ஊ',
            'ए': 'ஏ', 'ऐ': 'ஐ', 'ओ': 'ஓ', 'औ': 'ஔ', 'ऽ': 'ऽ'
        };
        var MISC = {
            'ं':'ம்', 'ः':':', 'ँ':'ம்', 'ॐ':'ௐ',
            '०': '0', '१': '1', '२': '2', '३': '3', '४': '4',
            '५': '5', '६': '6', '७': '7', '८': '8', '९': '9'
        };
        var HALANTA = '्';
        var chars = Array.from(text), result = '', i = 0;
        while (i < chars.length) {
            var ch = chars[i], next = chars[i + 1];
            if (CONSONANTS[ch]) {
                var base = CONSONANTS[ch];
                if (next === HALANTA) { result += base + '்'; i += 2; }
                else if (next === 'ृ') { result += base + '்ரி'; i += 2; }
                else if (next === 'ॄ') { result += base + '்ரீ'; i += 2; }
                else if (DIACRITICS[next]) { result += base + DIACRITICS[next]; i += 2; }
                else { result += base; i++; }   // inherent 'a'
            }
            else if (ch === 'ऋ') { result += 'ரி'; i++; }
            else if (ch === 'ॠ') { result += 'ரீ'; i++; }
            else if (VOWELS[ch]) { result += VOWELS[ch]; i++; }
            else if (MISC[ch]) { result += MISC[ch]; i++; }
            else { result += ch; i++; }
        }
        return result;
    }
 
    function devanagariToIAST(text) {
        var CONSONANTS = {
            'क': 'k', 'ख': 'kh', 'ग': 'g', 'घ': 'gh', 'ङ': 'ṅ',
            'च': 'c', 'छ': 'ch', 'ज': 'j', 'झ': 'jh', 'ञ': 'ñ',
            'ट': 'ṭ', 'ठ': 'ṭh', 'ड': 'ḍ', 'ढ': 'ḍh', 'ण': 'ṇ',
            'त': 't', 'थ': 'th', 'द': 'd', 'ध': 'dh', 'न': 'n',
            'प': 'p', 'फ': 'ph', 'ब': 'b', 'भ': 'bh', 'म': 'm',
            'य': 'y', 'र': 'r', 'ल': 'l', 'ळ': 'ḷ', 'व': 'v',
            'श': 'ś', 'ष': 'ṣ', 'स': 's', 'ह': 'h'
        };
        var DIACRITICS = {
            'ा': 'ā', 'ि': 'i', 'ी': 'ī', 'ु': 'u', 'ू': 'ū',
            'ृ': 'ṛ', 'ॄ': 'ṝ', 'े': 'e', 'ै': 'ai', 'ो': 'o', 'ौ': 'au'
        };
        var VOWELS = {
            'अ': 'a', 'आ': 'ā', 'इ': 'i', 'ई': 'ī', 'उ': 'u', 'ऊ': 'ū',
            'ऋ': 'ṛ', 'ॠ': 'ṝ', 'ए': 'e', 'ऐ': 'ai', 'ओ': 'o', 'औ': 'au', 'ऽ': "'"
        };
        var MISC = {
            'ं': 'ṃ', 'ः': 'ḥ', 'ँ': 'm̐', 'ॐ': 'oṃ',
            '०': '0', '१': '1', '२': '2', '३': '3', '४': '4',
            '५': '5', '६': '6', '७': '7', '८': '8', '९': '9'
        };
        var HALANTA = '्';
        var chars = Array.from(text), result = '', i = 0;
        while (i < chars.length) {
            var ch = chars[i], next = chars[i + 1];
            if (CONSONANTS[ch]) {
                var base = CONSONANTS[ch];
                if (next === HALANTA) { result += base; i += 2; }
                else if (DIACRITICS[next]) { result += base + DIACRITICS[next]; i += 2; }
                else if (next === 'ं' || next === 'ः') { result += base + 'a' + MISC[next]; i += 2; }
                else { result += base + 'a'; i++; }
            }
            else if (VOWELS[ch]) { result += VOWELS[ch]; i++; }
            else if (DIACRITICS[ch]) { result += DIACRITICS[ch]; i++; }
            else if (MISC[ch]) { result += MISC[ch]; i++; }
            else { result += ch; i++; }
        }
        return result;
    }
 
    var SCRIPT_MAP = {
        kn: {
            'अ': 'ಅ', 'आ': 'ಆ', 'इ': 'ಇ', 'ई': 'ಈ', 'उ': 'ಉ', 'ऊ': 'ಊ', 'ऋ': 'ಋ', 'ॠ': 'ೠ',
            'ए': 'ಏ', 'ऐ': 'ಐ', 'ओ': 'ಓ', 'औ': 'ಔ', 'ऽ': 'ಽ',
            'क': 'ಕ', 'ख': 'ಖ', 'ग': 'ಗ', 'घ': 'ಘ', 'ङ': 'ಙ',
            'च': 'ಚ', 'छ': 'ಛ', 'ज': 'ಜ', 'झ': 'ಝ', 'ञ': 'ಞ',
            'ट': 'ಟ', 'ठ': 'ಠ', 'ड': 'ಡ', 'ढ': 'ಢ', 'ण': 'ಣ',
            'त': 'ತ', 'थ': 'ಥ', 'द': 'ದ', 'ध': 'ಧ', 'न': 'ನ',
            'प': 'ಪ', 'फ': 'ಫ', 'ब': 'ಬ', 'भ': 'ಭ', 'म': 'ಮ',
            'य': 'ಯ', 'र': 'ರ', 'ल': 'ಲ', 'ळ': 'ಳ', 'व': 'ವ',
            'श': 'ಶ', 'ष': 'ಷ', 'स': 'ಸ', 'ह': 'ಹ',
            'ा': 'ಾ', 'ि': 'ಿ', 'ी': 'ೀ', 'ु': 'ು', 'ू': 'ೂ',
            'ृ': 'ೃ', 'ॄ': 'ೄ', 'े': 'ೇ', 'ै': 'ೈ', 'ो': 'ೋ', 'ौ': 'ೌ',
            'ं': 'ಂ', 'ः': 'ಃ', '्': '್', 'ँ': 'ಁ',
            '०': '೦', '१': '೧', '२': '೨', '३': '೩', '४': '೪',
            '५': '೫', '६': '೬', '७': '೭', '८': '೮', '९': '೯'
        },
        te: {
            'अ': 'అ', 'आ': 'ఆ', 'इ': 'ఇ', 'ई': 'ఈ', 'उ': 'ఉ', 'ऊ': 'ఊ', 'ऋ': 'ఋ', 'ॠ': 'ౠ',
            'ए': 'ఏ', 'ऐ': 'ఐ', 'ओ': 'ఓ', 'औ': 'ఔ', 'ऽ': 'ఽ',
            'क': 'క', 'ख': 'ఖ', 'ग': 'గ', 'घ': 'ఘ', 'ङ': 'ఙ',
            'च': 'చ', 'छ': 'ఛ', 'ज': 'జ', 'झ': 'ఝ', 'ञ': 'ఞ',
            'ट': 'ట', 'ठ': 'ఠ', 'ड': 'డ', 'ढ': 'ఢ', 'ण': 'ణ',
            'त': 'త', 'थ': 'థ', 'द': 'ద', 'ध': 'ధ', 'न': 'న',
            'प': 'ప', 'फ': 'ఫ', 'ब': 'బ', 'भ': 'భ', 'म': 'మ',
            'य': 'య', 'र': 'ర', 'ल': 'ల', 'ळ': 'ళ', 'व': 'వ',
            'श': 'శ', 'ष': 'ష', 'स': 'స', 'ह': 'హ',
            'ा': 'ా', 'ि': 'ి', 'ी': 'ీ', 'ु': 'ు', 'ू': 'ూ',
            'ृ': 'ృ', 'ॄ': 'ౄ', 'े': 'ే', 'ै': 'ై', 'ो': 'ో', 'ौ': 'ౌ',
            'ं': 'ం', 'ः': 'ః', '्': '్', 'ँ': 'ఁ',
            '०': '౦', '१': '౧', '२': '౨', '३': '౩', '४': '౪',
            '५': '౫', '६': '౬', '७': '౭', '८': '౮', '९': '౯'
        }
     };
     };
     var DIACRITICS = {
 
      'ा':'ā','ि':'i','ी':'ī','ु':'u','ू':'ū',
    var PRE = [
      'ृ':'ṛ','ॄ':'ṝ','े':'e','ै':'ai','ो':'o','ौ':'au'
        [/ङ्क/g, 'ंक'], [/ङ्ख/g, 'ंख'], [/ङ्ग/g, 'ंग'], [/ङ्घ/g, 'ंघ'],
        [/ञ्च/g, 'ंच'], [/ञ्ज/g, 'ंज'], [/ण्ट/g, 'ंट'], [/ण्ड/g, 'ंड'],
        [/न्त/g, 'ंत'], [/न्द/g, 'ंद'], [/म्ब/g, 'ंब'], [/म्भ/g, 'ंभ']
    ];
 
    function transliterateText(text, script) {
        if (script === 'en') return devanagariToIAST(text);
        if (script === 'ta') return devanagariToTamil(text);
        var map = SCRIPT_MAP[script];
        if (!map) return text;
        var t = text;
        PRE.forEach(function (p) { t = t.replace(p[0], p[1]); });
        return Array.from(t).map(function (ch) {
            return map[ch] !== undefined ? map[ch] : ch;
        }).join('');
    }
 
    var translatableSpans = [];
 
    function tagTextNodes() {
        var content = document.querySelector('.mw-parser-output');
        if (content) {
            var walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
            var nodes = [];
            while (walker.nextNode()) nodes.push(walker.currentNode);
            nodes.forEach(function (node) {
                var p = node.parentNode;
                if (!p) return;
                if (p.hasAttribute && p.hasAttribute('data-deva')) return;
                if (p.closest) {
                    if (p.closest('.gr-controls')) return;
                    if (p.closest('.mw-editsection')) return;
                    // Note markers are [1], [2] … — machinery, not text, and
                    // the numbers would be transliterated along with the page.
                    if (p.closest('.gr-note-marker')) return;
                }
                var orig = node.textContent;
                if (!orig.trim()) return;
                var span = document.createElement('span');
                span.setAttribute('data-deva', orig);
                span.textContent = orig;
                p.replaceChild(span, node);
                translatableSpans.push(span);
            });
        }
        document.querySelectorAll('.vector-toc .vector-toc-text').forEach(tagTocEntry);
    }
 
    // A contents entry is "1.2 शीर्षकम्": the number lives in its own
    // .vector-toc-numb span, which Common.css hides. Tagging the whole
    // .vector-toc-text meant transliteration replaced its contents with
    // one flat string — number included — so switching to Tamil or any
    // other script made the numbering appear. Only the label is tagged.
    function tagTocEntry(textEl) {
        if (!textEl || textEl.hasAttribute('data-deva')) return;
        var label = textEl.querySelector('[data-deva]');
        if (!label) {
            label = document.createElement('span');
            label.className = 'gr-toc-label';
            Array.prototype.slice.call(textEl.childNodes).forEach(function (n) {
                if (n.nodeType === 1 && n.classList && n.classList.contains('vector-toc-numb')) return;
                label.appendChild(n);                // moves it, order preserved
            });
            textEl.appendChild(label);
        }
        var orig = label.textContent;
        if (!orig.trim()) return;
        if (!label.hasAttribute('data-deva')) label.setAttribute('data-deva', orig);
        if (translatableSpans.indexOf(label) === -1) translatableSpans.push(label);
    }
 
    // Which script is on screen, published to CSS. Common.css §2b uses it to
    // swap the reading area onto --font-indic while a transliteration is
    // showing: most rules here name Adishila directly, which has no Tamil,
    // Kannada or Telugu letters, so without this the switched text fell back
    // to whatever serif the visitor's machine happened to have — the reason
    // it looked uneven from one line to the next.
    function markScriptOnRoot(script) {
        try {
            var el = document.documentElement;
            if (!el) return;
            if (!script || script === 'deva') el.removeAttribute('data-gr-script');
            else el.setAttribute('data-gr-script', script);
        } catch (e) { }
    }
 
     function applyScript(script) {
        currentScript = script;
        markScriptOnRoot(script);
        translatableSpans.forEach(function (span) {
            if (!span.parentNode) return;
            var orig = span.getAttribute('data-deva');
            if (!orig) return;
            span.textContent = (script === 'deva')
                ? orig
                : transliterateText(orig, script);
        });
    }
 
    function _isNoTocPage() {
        var pn = (window.mw && mw.config && mw.config.get('wgPageName')) || '';
        return pn === 'Main_Page' || /^[A-Za-z0-9_]+:About$/.test(pn);
    }
 
    function renameTocTitle() {
        if (_isNoTocPage()) return;
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        var titleEl = toc.querySelector('.vector-toc-title') || toc.querySelector('.vector-pinnable-header-label');
        if (!titleEl) return;
        var LABEL = 'विषयसूची';
        var span = titleEl.querySelector('.gr-toc-title');
        if (!span) {
            titleEl.innerHTML = '';
            span = document.createElement('span');
            span.className = 'gr-toc-title';
            span.setAttribute('data-deva', LABEL);
            titleEl.appendChild(span);
            translatableSpans.push(span);
        }
        span.textContent = currentScript === 'deva' ? LABEL : transliterateText(LABEL, currentScript);
    }
 
    function removeTocBeginning() {
        if (_isNoTocPage()) return;
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        var el = toc.querySelector('#vector-toc-beginning');
        if (!el) {
            var items = toc.querySelectorAll('.vector-toc-list-item');
            for (var i = 0; i < items.length; i++) {
                var a = items[i].querySelector('a');
                if (a) {
                    var href = a.getAttribute('href') || '';
                    if (href.indexOf('#') === -1) { el = items[i]; break; }
                }
            }
        }
        if (el && el.parentNode) el.parentNode.removeChild(el);
    }
 
    // The commentary list at the top of a moola page has its own heading
    // (टीकाः). It isn't part of the text, so it doesn't belong in the
    // contents. Recognised by what the section holds — the .gr-doc-teekas
    // list, or a section whose only links are to /Vyakhya/ pages — with
    // the heading's own wording as a last resort, since the list markup
    // differs between pages and the commentary picker rewrites it.
    var TEEKA_HEADINGS = /^(टीकाः?|व्याख्याः?|टीकाग्रन्थाः|Teekas?|Commentaries)$/i;
    function isTeekaSection(headingEl) {
        var wrap = (headingEl.closest && headingEl.closest('.mw-heading')) || headingEl;
        var n = wrap.nextElementSibling, links = [], vyakhya = 0, steps = 0;
        while (n && steps < 6) {
            if (n.classList && n.classList.contains('gr-doc-teekas')) return true;
            if (n.querySelector && n.querySelector('.gr-doc-teekas')) return true;
            if ((n.tagName && /^H[1-6]$/.test(n.tagName)) || (n.classList && n.classList.contains('mw-heading'))) break;
            if (n.querySelectorAll) {
                Array.prototype.forEach.call(n.querySelectorAll('a[href]'), function (a) {
                    var href = a.getAttribute('href') || '';
                    if (/^#/.test(href)) return;
                    links.push(href);
                    if (/\/Vyakhya\//i.test(decodeURIComponent(href))) vyakhya++;
                });
            }
            n = n.nextElementSibling; steps++;
        }
        if (links.length && vyakhya === links.length) return true;
        return TEEKA_HEADINGS.test((headingEl.textContent || '').trim());
    }
    function removeTocTeekaEntry() {
        if (_isNoTocPage()) return;
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        Array.prototype.forEach.call(toc.querySelectorAll('.vector-toc-link[href^="#"]'), function (a) {
            var id = a.getAttribute('href').slice(1);
            try { id = decodeURIComponent(id); } catch (e) { }
            var h = id && document.getElementById(id);
            if (!h || !isTeekaSection(h)) return;
            var li = a.closest('.vector-toc-list-item');
            if (li && li.parentNode) li.parentNode.removeChild(li);
        });
    }
 
    // Section numbers ("2.1.1") are removed from the contents outright.
    // Hiding them with CSS was not enough: the script switcher replaces an
    // entry's contents with one flat string, which turned the hidden number
    // into ordinary text — so it reappeared in Tamil, Kannada and the rest.
    // They are also wrong here, since this contents list leaves entries out.
    var NUMBERED = /^\s*\d+(?:\.\d+)*[.)]?\s+([\s\S]*\S)\s*$/;
    function stripTocNumbers() {
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        Array.prototype.forEach.call(toc.querySelectorAll('.vector-toc-numb'), function (n) {
            if (n.parentNode) n.parentNode.removeChild(n);
        });
        // Entries another script already flattened: take the number off the
        // text, and off the Devanagari original the switcher re-reads.
        Array.prototype.forEach.call(toc.querySelectorAll('.vector-toc-text, .vector-toc-text [data-deva]'), function (el) {
            var d = el.getAttribute && el.getAttribute('data-deva');
            if (d) { var md = NUMBERED.exec(d); if (md) el.setAttribute('data-deva', md[1]); }
            if (!el.children.length) {
                var mt = NUMBERED.exec(el.textContent);
                if (mt) el.textContent = mt[1];
            }
        });
    }
    // The switcher can rewrite the contents at any time, so watch for it.
    var _numbObserver = null;
    function watchTocNumbers() {
        var toc = document.querySelector('.vector-toc');
        if (!toc || _numbObserver || !window.MutationObserver) return;
        var pending = false;
        _numbObserver = new MutationObserver(function () {
            if (pending) return;
            pending = true;
            setTimeout(function () { pending = false; stripTocNumbers(); }, 50);
        });
        _numbObserver.observe(toc, { childList: true, subtree: true, characterData: true });
    }
 
    function expandTocSections() {
        if (_isNoTocPage()) return;
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        toc.querySelectorAll('.vector-toc-list-item-collapsed').forEach(function (li) {
            li.classList.remove('vector-toc-list-item-collapsed');
        });
    }
 
    function injectTocDocNav() {
        if (_isNoTocPage()) return;
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        if (document.getElementById('gr-toc-doc-nav')) return;
        var artPath = (window.mw && mw.config && mw.config.get('wgArticlePath')) || '/wiki/$1';
        var pageTitle = (window.mw && mw.config && mw.config.get('wgPageName')) || '';
        var teekaPage = document.querySelector('.gr-teeka-page');
        var primarySlug = teekaPage ? (teekaPage.getAttribute('data-primary') || '') : '';
        if (!primarySlug) { primarySlug = pageTitle.split('/')[0]; }
        if (!primarySlug) return;
        function wikiUrl(slug) {
            if (window.mw && mw.util && mw.util.getUrl) return mw.util.getUrl(slug);
            return artPath.replace('$1', encodeURIComponent(slug).replace(/%2F/g, '/'));
        }
        var docTitleEl = document.querySelector('.gr-doc-title');
        var hasMoolaPage = docTitleEl && docTitleEl.getAttribute('data-has-moola') === '1';
        var hasUllekhaPage = docTitleEl && docTitleEl.getAttribute('data-has-ullekha') === '1';
        var showMoolaPage = !teekaPage && hasMoolaPage;
        var showMoolaBack = !!teekaPage;
        var showUllekha = hasUllekhaPage || !!teekaPage;
        if (!showMoolaPage && !showMoolaBack && !showUllekha) return;
        var nav = document.createElement('div');
        nav.id = 'gr-toc-doc-nav';
        nav.setAttribute('class', 'toc-main-links');
        function makeBtn(href, label) {
            var a = document.createElement('a');
            a.href = href;
            a.setAttribute('class', 'toc-main-link-item');
            var lspan = document.createElement('span');
            lspan.setAttribute('data-deva', label);
            lspan.textContent = (currentScript && currentScript !== 'deva') ? transliterateText(label, currentScript) : label;
            translatableSpans.push(lspan);
            a.appendChild(lspan);
            return a;
        }
        if (showMoolaPage) nav.appendChild(makeBtn(wikiUrl(primarySlug + '/Moola'), 'मूलम्'));
        if (showMoolaBack) nav.appendChild(makeBtn(wikiUrl(primarySlug), 'मूल'));
        if (showUllekha) nav.appendChild(makeBtn(wikiUrl(primarySlug + '/Ullekha'), 'उल्लेख'));
        // Under the document's own title in the page, the same as on a
        // phone — not tucked into the contents panel, where these were
        // easy to miss. The contents panel is only the fallback.
        var docTitle = document.querySelector('#mw-content-text .gr-doc-title');
        if (docTitle && docTitle.parentNode) { docTitle.parentNode.insertBefore(nav, docTitle.nextSibling); return; }
        var h1 = document.getElementById('firstHeading') || document.querySelector('.mw-first-heading');
        if (h1 && h1.parentNode) { h1.parentNode.insertBefore(nav, h1.nextSibling); return; }
        var tocContents = toc.querySelector('.vector-toc-contents');
        if (tocContents) toc.insertBefore(nav, tocContents);
        else toc.appendChild(nav);
    }
 
    var _headingObserver = null;
    var _tocActiveId = null;
 
    // NEW: moved out of attachHeadingObserver's own local scope so they
    // can be reused both by the scroll observer AND by a direct trigger
    // for search-result clicks — was previously only reachable from
    // inside attachHeadingObserver's own IntersectionObserver callback.
    // _tocActiveId (was _activeId, local to attachHeadingObserver) is
    // module-level now for the same reason — both callers need to share
    // the same "what's currently active" state, not track it separately.
    function getTocLink(id) {
        var toc = document.querySelector('.vector-toc');
        if (!toc) return null;
        return toc.querySelector('a[href="#' + CSS.escape(id) + '"]');
    }
    function getTocLi(id) {
        var a = getTocLink(id);
        return a ? a.closest('.vector-toc-list-item') : null;
    }
    // Highlighting is class-only (styled in Common.css §6):
    //  .gr-toc-current  — the one entry being read, on the <a> itself
    //  .gr-toc-in-path  — its parent sections, marked on the <li>
    // Previously this set inline colours on the active item AND on every
    // element inside it, so a section with sub-sections lit up whole.
    function clearActiveToc() {
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        toc.querySelectorAll('.gr-toc-current').forEach(function (el) { el.classList.remove('gr-toc-current'); });
        toc.querySelectorAll('.gr-toc-in-path').forEach(function (el) { el.classList.remove('gr-toc-in-path'); });
    }
    function ownLink(li) {
        return li.querySelector(':scope > .vector-toc-link') || li.querySelector('.vector-toc-link');
    }
    function isShown(el) {
        return !!(el && (el.offsetParent || (el.getClientRects && el.getClientRects().length)));
    }
    // Open a collapsed section so a given entry can be seen. Vector marks a
    // top-level item open with vector-toc-list-item-expanded plus
    // aria-expanded on its toggle, so this matches what its own button does.
    function expandTocFor(li) {
        var top = li.closest ? li.closest('.vector-toc-level-1') : null;
        if (!top) return;
        if (!top.classList.contains('vector-toc-list-item-expanded')) {
            top.classList.add('vector-toc-list-item-expanded');
            var tg = top.querySelector('.vector-toc-toggle');
            if (tg) tg.setAttribute('aria-expanded', 'true');
        }
        var anc = li.parentNode;
        while (anc && anc.classList) {
            anc.classList.remove('vector-toc-list-item-collapsed');
            if (anc.classList.contains('vector-toc')) break;
            anc = anc.parentNode;
        }
    }
 
    // expand=true when the reader picked the entry (TOC click, search
    // result, #hash): open its section so the exact entry is shown.
    // expand=false while scrolling: never open or close anything — if the
    // entry is hidden inside a collapsed section, the section itself is
    // highlighted instead, so the reader still sees where they are.
    function setActiveToc(id, expand) {
        var toc = document.querySelector('.vector-toc');
        if (!toc) return;
        var shownNow = toc.querySelector('.gr-toc-current');
        if (_tocActiveId === id && !expand && shownNow && isShown(shownNow)) return;
        _tocActiveId = id;
        clearActiveToc();
        if (!id) return;
        var li = getTocLi(id);
        if (!li) return;
        if (expand) expandTocFor(li);
 
        var lnk = ownLink(li);
        var hostLi = li;
        if (lnk && !isShown(lnk)) {          // hidden in a collapsed section
            var p = li.parentNode;
            while (p && p !== toc) {
                if (p.classList && p.classList.contains('vector-toc-list-item')) {
                    var pl = ownLink(p);
                    if (pl && isShown(pl)) { lnk = pl; hostLi = p; break; }
                }
                p = p.parentNode;
            }
        }
        if (lnk) lnk.classList.add('gr-toc-current');
        // Sections above it get the quieter "you are in here" treatment.
        var anc = hostLi.parentNode;
        while (anc && anc !== toc) {
            if (anc.classList && anc.classList.contains('vector-toc-list-item')) anc.classList.add('gr-toc-in-path');
            anc = anc.parentNode;
        }
        keepTocEntryVisible(lnk || hostLi);
    }
 
    // Scroll the TOC panel so the active entry is visible. This used to
    // measure the whole <li>, which for a section with sub-sections is as
    // tall as the entire branch — so "centre it" landed somewhere inside
    // the NEXT section. It now measures the link itself, and finds the
    // element that actually scrolls rather than assuming one.
    function keepTocEntryVisible(el) {
        if (!el) return;
        var sc = el.parentNode;
        while (sc && sc !== document.body && sc.nodeType === 1) {
            var oy = window.getComputedStyle(sc).overflowY;
            if ((oy === 'auto' || oy === 'scroll') && sc.scrollHeight > sc.clientHeight + 4) break;
            sc = sc.parentNode;
        }
        if (!sc || sc === document.body || sc.nodeType !== 1) return;  // nothing scrolls: leave the page alone
        var er = el.getBoundingClientRect(), cr = sc.getBoundingClientRect();
        if (er.top >= cr.top + 8 && er.bottom <= cr.bottom - 8) return; // already in view
        sc.scrollTop += (er.top - cr.top) - Math.max((sc.clientHeight - er.height) / 2, 0);
    }
 
    // NEW: given any element in the article content, finds the nearest
    // heading (with an id) that precedes it in document order, using
    // compareDocumentPosition — robust regardless of nesting depth.
    // Verified against a realistic multi-section fragment, including
    // the "before any heading" and "right after a chapter heading, no
    // sub-heading yet" edge cases, before wiring this in.
    function findNearestHeadingId(target) {
        var content = document.querySelector('.mw-parser-output');
        if (!content) return null;
        var headings = content.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]');
        var nearest = null;
        for (var i = 0; i < headings.length; i++) {
            var h = headings[i];
            var pos = h.compareDocumentPosition(target);
            if (pos & Node.DOCUMENT_POSITION_FOLLOWING) {
                nearest = h.id;
            } else {
                break;
            }
        }
        return nearest;
    }
 
    // NEW: exposed so the search-highlight IIFE (below) can directly
    // trigger the TOC to expand and highlight the right section right
    // after a search-result scroll lands — rather than relying solely on
    // the IntersectionObserver to catch up on its own timing.
    window.grHighlightTocForElement = function (el) {
        if (!el) return;
        var id = findNearestHeadingId(el);
        if (id) pinActive(id);
     };
     };
     var VOWELS = {
 
      'अ':'a','आ':'ā','इ':'i','ई':'ī','उ':'u','ऊ':'ū',
    // ── Which section am I reading? ────────────────────────────────
      'ऋ':'ṛ','ॠ':'ṝ','ए':'e','ऐ':'ai','ओ':'o','औ':'au','ऽ':"'"
    // Was an IntersectionObserver with a shrunken root band. Two faults
    // came out of that, both reported as "the highlight is wrong":
    //  • right after jumping to a heading, that heading sits ABOVE the
    //    band, so nothing was "visible" and the fallback picked the
    //    PREVIOUS heading — clicking a sub-section highlighted its parent;
    //  • once every heading was outside the band, no intersection ever
    //    changed again, so the observer stopped firing and the highlight
    //    froze wherever it was.
    // This measures instead: the active section is the last heading above
    // a reading line just under the sticky toolbar. It updates on every
    // scroll frame and can't go stale.
    var _tocHeadings = [];
    var _tocTops = [];
    var _tocMeasuredHeight = 0;
    // A clicked / jumped-to heading is "pinned": it stays the active entry
    // until the reader actually scrolls away from it. Without this, the
    // anchor jump leaves the heading below the reading line (the sticky
    // bar and scroll-margin push it down the viewport), so the measurement
    // picked the heading ABOVE it and the highlight jumped back one.
     var _pin = null;                                  // {id, y, until}
 
    function measureHeadings() {
        var y = window.pageYOffset || document.documentElement.scrollTop || 0;
        _tocTops = _tocHeadings.map(function (h) { return h.getBoundingClientRect().top + y; });
        _tocMeasuredHeight = document.documentElement.scrollHeight;
    }
    function scrollY() { return window.pageYOffset || document.documentElement.scrollTop || 0; }
    // The line down the page that decides "which section am I reading":
    // just below whatever is stuck to the top, plus a quarter of the
    // screen, so a heading sitting near the top of the viewport counts as
    // the one being read rather than the one before it.
    function readingLine() {
        var bar = document.getElementById('gr-static-bar');
        var barBottom = bar ? Math.max(bar.getBoundingClientRect().bottom, 0) : 0;
        var vh = window.innerHeight || document.documentElement.clientHeight || 800;
        return scrollY() + barBottom + Math.max(Math.round(vh * 0.25), 80);
    }
    // Scroll something into a comfortable reading position and KEEP it
    // there. A plain scrollIntoView lands short or long whenever the page
    // reflows just after the jump — a web font finishing, a commentary
    // being interleaved, the match itself being wrapped in a highlight —
    // which is why search results came to rest slightly above or below
    // the text. This aims just under whatever is stuck to the top, then
    // re-checks until the page has settled, and gives up the moment the
    // reader scrolls, so it never fights them.
    function grDesiredTop() {
        var bar = document.getElementById('gr-static-bar');
        var barBottom = bar ? Math.max(bar.getBoundingClientRect().bottom, 0) : 0;
        var vh = window.innerHeight || document.documentElement.clientHeight || 800;
        return barBottom + Math.max(Math.round(vh * 0.22), 72);
    }
    window.grScrollToElement = function (el) {
        if (!el || !el.getBoundingClientRect) return;
        var cancelled = false, ticks = 0, lastY = null, iv = null;
        function onUser() { cancelled = true; }
        function cleanup() {
            if (iv) clearInterval(iv);
            window.removeEventListener('wheel', onUser);
            window.removeEventListener('touchstart', onUser);
            window.removeEventListener('keydown', onUser);
        }
        window.addEventListener('wheel', onUser, { passive: true });
        window.addEventListener('touchstart', onUser, { passive: true });
        window.addEventListener('keydown', onUser);
        function delta() { return el.getBoundingClientRect().top - grDesiredTop(); }
        try { window.scrollBy({ top: delta(), left: 0, behavior: 'smooth' }); }
        catch (e) { window.scrollBy(0, delta()); }
        iv = setInterval(function () {
            ticks++;
            if (cancelled || ticks > 12 || !document.body.contains(el)) { cleanup(); return; }
            var y = scrollY();
            if (lastY !== null && Math.abs(y - lastY) < 1) {    // the scroll has come to rest
                var d = delta();
                if (Math.abs(d) > 6) window.scrollBy(0, d);      // reflow moved it: correct at once
                else { cleanup(); return; }
            }
            lastY = y;
        }, 200);
     };
     };
     var MISC = {
 
      'ं':'ṃ','ः':'ḥ','ँ':'m̐','ॐ':'oṃ',
    // Called when the reader picks a heading (TOC click, search result,
      '०':'0','१':'1','२':'2','३':'3','४':'4',
    // or a #hash in the URL) — that heading wins until they scroll on.
      '५':'5','६':'6','७':'7','८':'8','९':'9'
     function pinActive(id) {
     };
        if (!id) return;
     var HALANTA = '्';
        _pin = { id: id, y: scrollY(), until: Date.now() + 900 };
     var chars = Array.from( text );
        setActiveToc(id, true);          // open its section if collapsed
     var result = '';
        scheduleRecheck(950);
    var i = 0;
     }
    while ( i < chars.length ) {
     var _recheck = null;
      var ch  = chars[ i ];
     function scheduleRecheck(ms) {
      var next = chars[ i + 1 ];
        if (_recheck) return;
      if ( CONSONANTS[ ch ] ) {
        _recheck = setTimeout(function () { _recheck = null; updateActiveFromScroll(); }, ms);
         var base = CONSONANTS[ ch ];
    }
         if ( next === HALANTA )         { result += base;              i += 2; }
     function updateActiveFromScroll() {
        else if ( DIACRITICS[ next ] )   { result += base + DIACRITICS[ next ]; i += 2; }
        if (!_tocHeadings.length) return;
         else if ( next === 'ं' || next === 'ः' ) { result += base + 'a' + MISC[ next ]; i += 2; }
        if (_pin) {
         else                            { result += base + 'a';         i++;   }
            var y = scrollY();
      } else if ( VOWELS[ ch ] )         { result += VOWELS[ ch ];       i++; }
            if (Date.now() < _pin.until) {               // still settling after the jump
      else if ( DIACRITICS[ ch ] )       { result += DIACRITICS[ ch ];   i++; }
                _pin.y = y;
      else if ( MISC[ ch ] )            { result += MISC[ ch ];        i++; }
                setActiveToc(_pin.id, true);
      else                              { result += ch;                 i++; }
                scheduleRecheck(_pin.until - Date.now() + 30);
                return;
            }
            if (Math.abs(y - _pin.y) < 60) {           // reader hasn't moved off it
                setActiveToc(_pin.id);
                return;
            }
            _pin = null;                                // they've scrolled on
        }
        // Interleaving a commentary or changing the font size moves things;
        // re-measure when the page's height has changed.
         if (document.documentElement.scrollHeight !== _tocMeasuredHeight) measureHeadings();
         var line = readingLine(), lo = 0, hi = _tocTops.length - 1, idx = 0;
        while (lo <= hi) {                               // last heading at or above the line
            var mid = (lo + hi) >> 1;
            if (_tocTops[mid] <= line + 1) { idx = mid; lo = mid + 1; } else { hi = mid - 1; }
        }
        setActiveToc(_tocHeadings[idx].id);
    }
 
    function attachHeadingObserver() {
        if (_isNoTocPage()) return;
         if (_headingObserver) return;
        var content = document.querySelector('.mw-parser-output');
        if (!content) return;
        if (!document.querySelector('.vector-toc')) return;
        // Only headings the contents lists — a heading with no entry (the
        // commentary list, say) would otherwise leave nothing highlighted.
        _tocHeadings = Array.prototype.slice.call(content.querySelectorAll('h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]'))
            .filter(function (h) { return !!getTocLink(h.id); });
        if (!_tocHeadings.length) return;
        measureHeadings();
        // Time-based throttle rather than a requestAnimationFrame latch: if
        // one frame callback is ever skipped (background tab, a jump during
        // load) a latch stays stuck and the highlight stops updating for
        // good — which is one of the ways this went stale before.
        var lastRun = 0, trailing = null;
         function onScroll() {
            var now = Date.now();
            if (now - lastRun >= 80) { lastRun = now; updateActiveFromScroll(); }
            else if (!trailing) {
                trailing = setTimeout(function () { trailing = null; lastRun = Date.now(); updateActiveFromScroll(); }, 80);
            }
        }
        window.addEventListener('scroll', onScroll, { passive: true });
        window.addEventListener('resize', function () { measureHeadings(); onScroll(); }, { passive: true });
        if (document.fonts && document.fonts.ready && document.fonts.ready.then) {
             document.fonts.ready.then(function () { measureHeadings(); onScroll(); });
        }
        _headingObserver = { onScroll: onScroll };       // marks "already wired"
         var hash = '';
        try { hash = decodeURIComponent((location.hash || '').slice(1)); } catch (e) { hash = (location.hash || '').slice(1); }
        if (hash && document.getElementById(hash)) pinActive(hash);
        else updateActiveFromScroll();
     }
     }
    return result;
  }


  // ── Character maps for Kannada / Tamil ─────────────────────────
    // Clicking an entry highlights it straight away, rather than waiting
  var SCRIPT_MAP = {
    // for the scroll observer to catch up after the jump.
     kn: {
     function attachTocClickHandler(toc) {
      'अ':'ಅ','आ':'ಆ','इ':'ಇ','ई':'ಈ','उ':'ಉ','ऊ':'ಊ','ऋ':'ಋ',
        if (toc.getAttribute('data-gr-click-wired')) return;
      'ए':'ಏ','ऐ':'ಐ','ओ':'ಓ','औ':'ಔ','ऽ':'ಽ',
        toc.setAttribute('data-gr-click-wired', '1');
      'क':'ಕ','ख':'ಖ','ग':'ಗ','घ':'ಘ','ङ':'ಙ',
        toc.addEventListener('click', function (e) {
      'च':'ಚ','छ':'ಛ','ज':'ಜ','झ':'ಝ','ञ':'ಞ',
            var a = e.target.closest ? e.target.closest('.vector-toc-link') : null;
      'ट':'ಟ','ठ':'ಠ','ड':'ಡ','ढ':'ಢ','ण':'ಣ',
            if (!a) return;
      'त':'ತ','थ':'ಥ','द':'ದ','ध':'ಧ','न':'ನ',
            var href = a.getAttribute('href') || '';
      'प':'ಪ','फ':'ಫ','ब':'ಬ','भ':'ಭ','म':'ಮ',
            if (href.charAt(0) !== '#') return;
      'य':'ಯ','र':'ರ','ल':'ಲ','व':'ವ',
            var id;
      'श':'ಶ','ष':'ಷ','स':'ಸ','ह':'ಹ',
            try { id = decodeURIComponent(href.slice(1)); } catch (err) { id = href.slice(1); }
      'ा':'ಾ','ि':'ಿ','ी':'ೀ','ु':'ು','ू':'ೂ',
            if (id) pinActive(id);
      'ृ':'ೃ','े':'ೇ','ै':'ೈ','ो':'ೋ','ौ':'ೌ',
        });
      'ं':'ಂ','ः':'ಃ','्':'್',
      '०':'೦','१':'೧','२':'೨','३':'೩','४':'೪',
      '५':'೫','६':'೬','७':'೭','८':'೮','९':'೯'
    },
    ta: {
      'अ':'அ','आ':'ஆ','इ':'இ','ई':'ஈ','उ':'உ','ऊ':'ஊ',
      'ऋ':'ரு','ॠ':'ரூ',
      'ए':'ஏ','ऐ':'ஐ','ओ':'ஓ','औ':'ஔ',
      'क':'க','ख':'க','ग':'க','घ':'க','ङ':'ங',
      'च':'ச','छ':'ச','ज':'ஜ','झ':'ஜ','ञ':'ஞ',
      'ट':'ட','ठ':'ட','ड':'ட','ढ':'ட','ண':'ண',
      'त':'த','थ':'த','द':'த','ध':'த','न':'ந',
      'प':'ப','फ':'ப','ब':'ப','भ':'ப','म':'ம',
      'य':'ய','र':'ர','ல':'ல','ळ':'ழ','व':'வ',
      'श':'ஶ','ष':'ஷ','स':'ஸ','ह':'ஹ',
      'ा':'ா','ि':'ி','ी':'ீ','ு':'ு','ू':'ூ',
      'ृ':'ு','ॄ':'ூ',
      'े':'ே','ை':'ை','ो':'ோ','ौ':'ௌ',
      'ं':'ம்','ः':':','ँ':'ம்','्':'்','ॐ':'ௐ','ऽ':'ௗ',
      '०':'0','१':'1','२':'2','३':'3','४':'4',
      '५':'5','६':'6','७':'7','८':'8','९':'9'
     }
     }
  };


  var PRE = [
    function setupToc() {
    [ /ङ्क/g, 'ंक' ], [ /ङ्ख/g, 'ंख' ], [ /ङ्ग/g, 'ंग' ], [ /ङ्घ/g, 'ंघ' ],
        if (_isNoTocPage()) return;
    [ /ञ्च/g, 'ंच' ], [ /ञ्ज/g, 'ंज' ], [ /ण्ट/g, 'ंट' ], [ /ण्ड/g, 'ंड' ],
        var toc = document.querySelector('.vector-toc');
    [ /न्त/g, 'ंत' ], [ /न्द/g, 'ंद' ], [ /म्ब/g, 'ंब' ], [ /म्भ/g, 'ंभ' ]
        if (!toc) return;
  ];
        removeTocBeginning();
        removeTocTeekaEntry();
        stripTocNumbers();
        watchTocNumbers();
        renameTocTitle();
        expandTocSections();
        injectTocDocNav();
        attachTocClickHandler(toc);
        attachHeadingObserver();
    }


  function transliterateText( text, script ) {
    function init() {
    if ( script === 'en' ) return devanagariToIAST( text );
        var HIDE_IDS = ['vector-appearance', 'vector-appearance-pinned-container', 'vector-appearance-unpinned-container'];
    var map = SCRIPT_MAP[ script ];
        function removeHiddenEls() {
    if ( !map ) return text;
            HIDE_IDS.forEach(function (id) { var el = document.getElementById(id); if (el && el.parentNode) el.parentNode.removeChild(el); });
    var t = text;
            var pt = document.getElementById('vector-page-tools') || document.querySelector('.vector-page-tools-pinned-container');
    PRE.forEach( function ( p ) { t = t.replace( p[ 0 ], p[ 1 ] ); } );
            if (pt) pt.querySelectorAll('[aria-controls="vector-appearance"]').forEach(function (el) { if (el.parentNode) el.parentNode.removeChild(el); });
    return Array.from( t ).map( function ( ch ) {
        }
      return map[ ch ] !== undefined ? map[ ch ] : ch;
        removeHiddenEls();
    } ).join( '' );
        (function detectTeekaMode() {
  }
            var tp = document.querySelector('.gr-teeka-page');
            if (!tp) return;
            var primary = tp.getAttribute('data-primary') || '';
            var artPath = (window.mw && mw.config.get('wgArticlePath')) || '/wiki/$1';
            var mainUrl = artPath.replace('$1', primary);
            var refParam = window.location.search.match(/[?&]ref=([01])/);
            if (refParam) { document.body.classList.add(refParam[1] === '1' ? 'gr-ref-mode' : 'gr-standalone'); return; }
            var ref = document.referrer || '';
            document.body.classList.add((ref && primary && ref.indexOf(mainUrl) !== -1) ? 'gr-ref-mode' : 'gr-standalone');
        }());
        if (window.MutationObserver) {
            var hideObs = new MutationObserver(function (mutations) {
                var dirty = false;
                mutations.forEach(function (m) { if (m.addedNodes.length) dirty = true; });
                if (dirty) removeHiddenEls();
            });
            hideObs.observe(document.body, { childList: true, subtree: false });
            setTimeout(function () { hideObs.disconnect(); }, 6000);
        }
        var content = document.querySelector('.mw-parser-output');
        var alreadyTagged = content && content.querySelector('[data-deva]');
        if (!alreadyTagged) { translatableSpans = []; tagTextNodes(); }
        else {
            document.querySelectorAll('.vector-toc .vector-toc-text').forEach(tagTocEntry);
        }
        var saved = (function () { try { return localStorage.getItem(LS_SCRIPT_KEY); } catch (e) { return null; } }());
        if (saved && saved !== 'deva') { applyScript(saved); } else { currentScript = 'deva'; }
        watchNewContent();
        setTimeout(setupToc, 200);
    }


  // ── Tag all transliteratable text nodes once per page ───────────
    window.addEventListener('gr-script-change', function (e) {
  var translatableSpans = [];
        var script = e && e.detail && e.detail.script;
        if (script) applyScript(script);
    });


  function tagTextNodes() {
    var _translitBusy = false;
    var content = document.querySelector( '.mw-parser-output' );
    function tagNewContent(container) {
    if ( content ) {
        if (!container || !container.nodeType) return;
      var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
        _translitBusy = true;
      var nodes = [];
        try {
      while ( walker.nextNode() ) nodes.push( walker.currentNode );
            var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
            var nodes = [];
            while (walker.nextNode()) nodes.push(walker.currentNode);
            nodes.forEach(function (node) {
                var p = node.parentNode;
                if (!p || (p.hasAttribute && p.hasAttribute('data-deva'))) return;
                if (p.closest && (p.closest('.gr-controls') || p.closest('.mw-editsection')
                                  || p.closest('.gr-note-marker'))) return;
                var orig = node.textContent;
                if (!orig.trim()) return;
                var span = document.createElement('span');
                span.setAttribute('data-deva', orig);
                span.textContent = currentScript !== 'deva' ? transliterateText(orig, currentScript) : orig;
                p.replaceChild(span, node);
                translatableSpans.push(span);
            });
        } finally { _translitBusy = false; }
    }
    window.addEventListener('gr-new-content', function (e) {
        tagNewContent(e && e.detail && e.detail.container);
    });


      nodes.forEach( function ( node ) {
    // Safety net: anything else that adds text to the page later — an
         var p = node.parentNode;
    // interleaved commentary, a sub-commentary, an edit preview — is picked
         if ( !p ) return;
    // up and put into the reader's chosen script, rather than being left in
         if ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) return;
    // Devanagari because it wasn't there when the page was first tagged.
         if ( p.closest ) {
    function watchNewContent() {
          if ( p.closest( '.gr-controls' )   ) return;
         if (!window.MutationObserver) return;
          if ( p.closest( '.mw-editsection' ) ) return;
         var content = document.querySelector('.mw-parser-output');
          // #gr-toc-doc-nav buttons manage their own data-deva spans in makeBtn
         if (!content || content.getAttribute('data-gr-translit-watch')) return;
         content.setAttribute('data-gr-translit-watch', '1');
        var queued = [], timer = null;
        function flush() {
            timer = null;
            var items = queued; queued = [];
            items.forEach(function (n) { if (document.body.contains(n)) tagNewContent(n); });
         }
         }
         var orig = node.textContent;
         new MutationObserver(function (muts) {
        if ( !orig.trim() ) return;
            if (_translitBusy) return;
        var span = document.createElement( 'span' );
            muts.forEach(function (m) {
        span.setAttribute( 'data-deva', orig );
                Array.prototype.forEach.call(m.addedNodes, function (n) {
        span.textContent = orig;
                    var el = n.nodeType === 1 ? n : (n.nodeType === 3 ? n.parentNode : null);
        p.replaceChild( span, node );
                    if (!el || el.nodeType !== 1) return;
         translatableSpans.push( span );
                    if (el.hasAttribute && el.hasAttribute('data-deva')) return;
      } );
                    queued.push(el);
                });
            });
            if (queued.length && !timer) timer = setTimeout(flush, 60);
         }).observe(content, { childList: true, subtree: true });
     }
     }


     document.querySelectorAll( '.vector-toc .vector-toc-text' ).forEach( function ( span ) {
     try {
      if ( span.hasAttribute( 'data-deva' ) ) return;
        var _grBC = new BroadcastChannel('gr-script');
      var orig = span.textContent;
        _grBC.onmessage = function (e) {
      if ( !orig.trim() ) return;
            var script = e && e.data && e.data.script;
      span.setAttribute( 'data-deva', orig );
            if (script) { currentScript = script; var sel = document.querySelector('.gr-script-sel'); if (sel) sel.value = script; applyScript(script); }
      translatableSpans.push( span );
        };
     } );
     } catch (e) { }
  }


  // ── Apply a script to all tagged spans ─────────────────────────
    if (window.mw) {
  function applyScript( script ) {
        mw.hook('wikipage.content').add(function () {
    currentScript = script;
            setTimeout(function () {
    translatableSpans.forEach( function ( span ) {
                var content = document.querySelector('.mw-parser-output');
      if ( !span.parentNode ) return;
                var alreadyTagged = content && content.querySelector('[data-deva]');
      var orig = span.getAttribute( 'data-deva' );
                if (!alreadyTagged) { translatableSpans = []; tagTextNodes(); }
      if ( !orig ) return;
                else {
      span.textContent = ( script === 'deva' )
                    // tagTocEntry, not a re-tag from textContent: an entry
        ? orig
                    // keeps its Devanagari in a span *inside* .vector-toc-text,
        : transliterateText( orig, script );
                    // so .vector-toc-text itself never carries data-deva and a
    } );
                    // :not([data-deva]) sweep matched every entry on every
  }
                    // fire. It then stored the text as it stood — Tamil, once
                    // the reader had switched — as that entry's "original", so
                    // the next switch had no Devanagari left to work from and
                    // the contents panel stayed in the previous script while
                    // the body moved on. tagTocEntry leaves a tagged entry be.
                    document.querySelectorAll('.vector-toc .vector-toc-text').forEach(tagTocEntry);
                }
                if (currentScript !== 'deva') applyScript(currentScript);
                setupToc();
            }, 150);
        });
    }


  // ── Pages where sidebar TOC should not be modified ──────────────
    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); }
  function _isNoTocPage() {
     else { init(); }
    var pn = ( window.mw && mw.config && mw.config.get( 'wgPageName' ) ) || '';
     return pn === 'Main_Page' || /^[A-Za-z0-9_]+:About$/.test( pn );
  }


  // ── TOC: rename "Contents" → "विषयसूची" ────────────────────────
}());   /* ← end of main IIFE */
function renameTocTitle() {
  if ( _isNoTocPage() ) return;


  var toc = document.querySelector('.vector-toc');
  if ( !toc ) return;


  var titleEl =
// ── Inject "Help" and "About" links into the header ─────────────────
     toc.querySelector('.vector-toc-title') ||
(function () {
    toc.querySelector('.vector-pinnable-header-label');
    function wikiHref(title) {
        if (window.mw && mw.util && mw.util.getUrl) return mw.util.getUrl(title);
        var ap = (window.mw && mw.config && mw.config.get('wgArticlePath')) || '/wiki/$1';
        return ap.replace('$1', title);
    }
    function makeHeaderLink(id, href, label) {
        var a = document.createElement('a');
        a.id = id; a.href = href; a.textContent = label;
        a.className = 'gr-header-link';  // styled in Common.css §37
        return a;
    }
     function injectHeaderLinks() {
        if (document.getElementById('gr-about-link')) return;
        var headerEnd = document.querySelector('.vector-header-end') || document.querySelector('#vector-user-links') || document.querySelector('.mw-header');
        if (!headerEnd) return;
        var helpLink = makeHeaderLink('gr-help-link', wikiHref('My_wiki:Help'), 'Help');
        var aboutLink = makeHeaderLink('gr-about-link', wikiHref('My_wiki:About'), 'About');


  if ( !titleEl ) return;
        var userName = window.mw ? mw.config.get('wgUserName') : null;
        var isAnon = !userName || (window.mw && mw.config.get('wgUserId') === null);


  var LABEL = 'विषयसूची';
        var authEl = null;
        if (isAnon) {
            if (!document.getElementById('gr-header-login')) {
                authEl = makeHeaderLink('gr-header-login', wikiHref('Special:UserLogin'), 'Login');
            }
        } else if (!document.getElementById('gr-header-user')) {
            authEl = buildHeaderUserDropdown(userName);
        }


  /* If already inserted, just refresh text */
        var ul = document.querySelector('.vector-user-links') || document.querySelector('#pt-userpage');
  var span = titleEl.querySelector('.gr-toc-title');
        if (ul && ul.parentNode === headerEnd) {
            if (authEl) headerEnd.insertBefore(authEl, ul);
            headerEnd.insertBefore(aboutLink, authEl || ul);
            headerEnd.insertBefore(helpLink, aboutLink);
        } else {
            headerEnd.appendChild(helpLink);
            headerEnd.appendChild(aboutLink);
            if (authEl) headerEnd.appendChild(authEl);
        }
    }


  if ( !span ) {
    function buildHeaderUserDropdown(userName) {
    titleEl.innerHTML = '';
        var wrap = document.createElement('div');
        wrap.id = 'gr-header-user';
        wrap.style.cssText = 'position:relative;display:inline-block;margin-right:4px;';


    span = document.createElement('span');
        var trigger = document.createElement('button');
    span.className = 'gr-toc-title';
        trigger.id = 'gr-header-user-trigger';
    span.setAttribute('data-deva', LABEL);
        trigger.type = 'button';
        trigger.setAttribute('aria-haspopup', 'true');
        trigger.setAttribute('aria-expanded', 'false');
        trigger.style.cssText = [
            'display:inline-flex', 'align-items:center', 'gap:5px',
            'color:rgba(255,255,255,0.88)', 'font-size:0.88em',
            'font-family:system-ui,sans-serif', 'font-weight:500',
            'background:transparent', 'border:none', 'cursor:pointer',
            'padding:4px 10px', 'border-radius:4px', 'white-space:nowrap',
            'transition:color 0.15s,background 0.15s'
        ].join(';');
        trigger.innerHTML =
            '<span>' + userName.replace(/</g, '&lt;') + '</span>' +
            '<span id="gr-header-caret" style="font-size:0.8em;transition:transform 0.2s;">\u25be</span>';
        trigger.addEventListener('mouseover', function () { this.style.color = '#fff'; this.style.background = 'rgba(255,255,255,0.12)'; });
        trigger.addEventListener('mouseout', function () { if (menu.style.display === 'none') { this.style.color = 'rgba(255,255,255,0.88)'; this.style.background = 'transparent'; } });


    titleEl.appendChild(span);
        var menu = document.createElement('div');
    translatableSpans.push(span);   // uses your internal array
        menu.id = 'gr-header-user-menu';
  }
        menu.setAttribute('role', 'menu');
        menu.style.cssText = [
            'display:none', 'position:absolute', 'top:100%', 'right:0', 'margin-top:4px',
            'min-width:150px', 'background:#fff', 'border-radius:6px',
            'box-shadow:0 4px 16px rgba(0,0,0,0.18)', 'overflow:hidden', 'z-index:1000'
        ].join(';');


  span.textContent =
        var itemStyle = 'display:block;padding:10px 16px;font-size:0.9em;color:#2c1810;text-decoration:none;font-family:system-ui,sans-serif;background:#fff;';
    currentScript === 'deva'
        function makeMenuItem(href, label, id) {
      ? LABEL
            var a = document.createElement('a');
      : transliterateText(LABEL, currentScript);
            a.href = href; a.setAttribute('role', 'menuitem');
}
            a.textContent = label; a.style.cssText = itemStyle;
  // ── TOC: Remove the "Beginning" / top-of-page link ─────────────
            if (id) a.id = id;
  function removeTocBeginning() {
            a.addEventListener('mouseover', function () { this.style.background = '#f5efe9'; });
    if ( _isNoTocPage() ) return;
            a.addEventListener('mouseout', function () { this.style.background = '#fff'; });
    var toc = document.querySelector( '.vector-toc' );
            return a;
    if ( !toc ) return;
    // Try the dedicated id first (Vector 2022)
    var el = toc.querySelector( '#vector-toc-beginning' );
    if ( !el ) {
      // Fallback: first list-item whose link has no # anchor = the "Beginning" entry
      var items = toc.querySelectorAll( '.vector-toc-list-item' );
      for ( var i = 0; i < items.length; i++ ) {
        var a = items[ i ].querySelector( 'a' );
        if ( a ) {
          var href = a.getAttribute( 'href' ) || '';
          if ( href.indexOf( '#' ) === -1 ) { el = items[ i ]; break; }
         }
         }
      }
 
        menu.appendChild(makeMenuItem(wikiHref('Special:Profile'), 'Profile'));
        menu.appendChild(makeMenuItem('#', 'Log out', 'gr-header-logout'));
 
        function setOpen(open) {
            menu.style.display = open ? 'block' : 'none';
            trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
            var caret = document.getElementById('gr-header-caret');
            if (caret) caret.style.transform = open ? 'rotate(180deg)' : '';
            if (open) { trigger.style.color = '#fff'; trigger.style.background = 'rgba(255,255,255,0.12)'; }
            else { trigger.style.color = 'rgba(255,255,255,0.88)'; trigger.style.background = 'transparent'; }
        }
 
        trigger.addEventListener('click', function (e) {
            e.stopPropagation();
            setOpen(menu.style.display === 'none');
        });
        document.addEventListener('click', function (e) {
            if (!wrap.contains(e.target)) setOpen(false);
        });
 
        menu.querySelector('#gr-header-logout').addEventListener('click', function (e) {
            e.preventDefault();
            if (window.mw && mw.user && mw.user.tokens) {
                var t = mw.user.tokens.get('csrfToken');
                if (t) {
                    var script = (mw.util && mw.util.wikiScript) ? mw.util.wikiScript() : '/index.php';
                    var form = document.createElement('form');
                    form.method = 'post';
                    form.action = script + '?title=Special:UserLogout';
                    form.style.display = 'none';
                    form.innerHTML =
                        '<input type="hidden" name="wpEditToken" value="' + String(t).replace(/"/g, '&quot;') + '">' +
                        '<input type="hidden" name="title" value="Special:UserLogout">' +
                        '<input type="hidden" name="returnto" value="Main Page">';
                    document.body.appendChild(form);
                    form.submit();
                    return;
                }
            }
            if (window.mw && mw.Api) {
                new mw.Api().postWithToken('csrf', { action: 'logout' })
                    .done(function () { location.href = '/Main_Page'; })
                    .fail(function () { location.href = '/index.php?title=Special:UserLogout'; });
            } else {
                location.href = '/index.php?title=Special:UserLogout';
            }
        });
 
        wrap.appendChild(trigger);
        wrap.appendChild(menu);
        return wrap;
     }
     }
     if ( el && el.parentNode ) el.parentNode.removeChild( el );
     if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', injectHeaderLinks);
  }
    else injectHeaderLinks();
}());


  // ── TOC: Expand all collapsed subsections on load ───────────────
  function expandTocSections() {
    if ( _isNoTocPage() ) return;
    var toc = document.querySelector( '.vector-toc' );
    if ( !toc ) return;
    toc.querySelectorAll( '.vector-toc-list-item-collapsed' ).forEach( function ( li ) {
      li.classList.remove( 'vector-toc-list-item-collapsed' );
    } );
  }


  // ── TOC: Inject मूल / उल्लेख nav links ─────────────────────────
// ── Main page: by-Grantha / by-Author toggle ──────────────────────
  function injectTocDocNav() {
(function () {
     if ( _isNoTocPage() ) return;
     function grHomeView(v) {
    var toc = document.querySelector( '.vector-toc' );
        var gView = document.getElementById('gr-view-grantha');
    if ( !toc ) return;
        var aView = document.getElementById('gr-view-author');
    if ( document.getElementById( 'gr-toc-doc-nav' ) ) return;
        var gBtn = document.getElementById('gr-toggle-grantha');
        var aBtn = document.getElementById('gr-toggle-author');
        if (!gView || !aView || !gBtn || !aBtn) return;
        gView.style.display = (v === 'grantha') ? '' : 'none';
        aView.style.display = (v === 'author') ? '' : 'none';
        gBtn.className = 'gr-toggle-btn' + (v === 'grantha' ? ' gr-toggle-active' : '');
        aBtn.className = 'gr-toggle-btn' + (v === 'author' ? ' gr-toggle-active' : '');
        try { localStorage.setItem('gr_home_view', v); } catch (e) { }
    }
}());


    var artPath  = ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
    var pageTitle = ( window.mw && mw.config && mw.config.get( 'wgPageName'  ) ) || '';


     var teekaPage  = document.querySelector( '.gr-teeka-page' );
// ── Ullekha reference link handler ─────────────────────────────────
    var primarySlug = teekaPage ? ( teekaPage.getAttribute( 'data-primary' ) || '' ) : '';
(function () {
    var docSlug    = teekaPage ? ( teekaPage.getAttribute( 'data-slug'   ) || '' ) : '';
     // Uses the shared, reflow-proof scroller when it is available.
    function grScrollTo(el) {
        if (!el) return;
        if (window.grScrollToElement) window.grScrollToElement(el);
        else el.scrollIntoView({ behavior: 'smooth', block: 'center' });
    }


     if ( !primarySlug ) {
     function highlightOnArrival() {
      primarySlug = pageTitle.split( '/' )[ 0 ];
        var search = window.location.search;
      docSlug    = primarySlug;
        if (!search) return;
        var m = search.match(/[?&]hlUllekha=([^&]+)/);
        if (!m) return;
        var needle;
        try { needle = decodeURIComponent(m[1]); } catch (e) { return; }
        if (!needle || needle.length < 4) return;
        function doHighlight() {
            var content = document.querySelector('.mw-parser-output');
            if (!content) return;
            var snippet = needle.slice(0, 40);
            var found = false;
            var spans = content.querySelectorAll('[data-deva]');
            for (var i = 0; i < spans.length && !found; i++) {
                var spanEl = spans[i];
                var orig = spanEl.getAttribute('data-deva') || '';
                if (orig.indexOf(snippet) === -1) continue;
                var idx = orig.indexOf(snippet);
                var hlText = orig.slice(idx, Math.min(idx + needle.length, orig.length));
                var mark = document.createElement('mark');
                mark.className = 'gr-ullekha-highlight';
                mark.style.cssText = 'background:#fff176;border-radius:2px;padding:0 2px;';
                mark.textContent = hlText;
                var parent = spanEl.parentNode;
                if (!parent) continue;
                var before = document.createTextNode(orig.slice(0, idx));
                var after = document.createTextNode(orig.slice(idx + hlText.length));
                parent.insertBefore(before, spanEl); parent.insertBefore(mark, spanEl); parent.insertBefore(after, spanEl); parent.removeChild(spanEl);
                setTimeout(function () { grScrollTo(mark); if (window.grHighlightTocForElement) window.grHighlightTocForElement(mark); }, 100);
                found = true;
            }
            if (!found) {
                var walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT);
                while (walker.nextNode() && !found) {
                    var node = walker.currentNode;
                    var txt = node.textContent || '';
                    if (txt.indexOf(snippet) === -1) continue;
                    var idx2 = txt.indexOf(snippet);
                    var mark2 = document.createElement('mark');
                    mark2.className = 'gr-ullekha-highlight';
                    mark2.style.cssText = 'background:#fff176;border-radius:2px;padding:0 2px;';
                    mark2.textContent = txt.slice(idx2, Math.min(idx2 + needle.length, txt.length));
                    var p = node.parentNode;
                    p.insertBefore(document.createTextNode(txt.slice(0, idx2)), node);
                    p.insertBefore(mark2, node);
                    p.insertBefore(document.createTextNode(txt.slice(idx2 + mark2.textContent.length)), node);
                    p.removeChild(node);
                    setTimeout(function () { grScrollTo(mark2); if (window.grHighlightTocForElement) window.grHighlightTocForElement(mark2); }, 100);
                    found = true;
                }
            }
        }
        doHighlight();
     }
     }
     if ( !primarySlug ) return;
     function wireUllekhaLinks() {
        document.querySelectorAll('.gr-ullekha-ref-link').forEach(function (wrap) {
            var anchor = wrap.getAttribute('data-anchor') || '';
            var hl = wrap.getAttribute('data-hl') || '';
            var a = wrap.querySelector('a');
            if (!a) return;
            var base = a.href.split('#')[0];
            var encoded = encodeURIComponent(hl);
            a.href = base + (hl ? '?hlUllekha=' + encoded : '') + (anchor ? '#' + anchor : '');
        });
    }
    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { highlightOnArrival(); wireUllekhaLinks(); }); }
    else { highlightOnArrival(); wireUllekhaLinks(); }
}());


    function wikiUrl( slug ) {
/* ── Search result highlight ──────────────────────────────────── */
      if ( window.mw && mw.util && mw.util.getUrl ) return mw.util.getUrl( slug );
(function () {
      return artPath.replace( '$1', encodeURIComponent( slug ).replace( /%2F/g, '/' ) );
 
    function storeQueryForLink(url, query) {
        try {
            var a = document.createElement('a');
            a.href = url;
            sessionStorage.setItem('gr_search_hl', JSON.stringify({
                query: query,
                pathname: a.pathname
            }));
        } catch (e) { }
     }
     }


     var moolaUrl  = wikiUrl( primarySlug );
     function applyHighlight() {
    var ullekhaUrl = wikiUrl( primarySlug + '/Ullekha' );
        var stored;
        try {
            stored = JSON.parse(sessionStorage.getItem('gr_search_hl') || 'null');
        } catch (e) { return; }
        if (!stored || !stored.query) return;


    var docTitleEl  = document.querySelector( '.gr-doc-title' );
        var currentPath = window.location.pathname;
    var hasMoolaPage  = docTitleEl && docTitleEl.getAttribute( 'data-has-moola' )  === '1';
        var storedPath = stored.pathname || '';
    var hasUllekhaPage = docTitleEl && docTitleEl.getAttribute( 'data-has-ullekha' ) === '1';


    var moolaPageUrl = wikiUrl( primarySlug + '/Moola' );
        function normPath(p) { return decodeURIComponent(p).replace(/\/+$/, ''); }
        if (storedPath && normPath(storedPath) !== normPath(currentPath)) {
            try { sessionStorage.removeItem('gr_search_hl'); } catch (e) { }
            return;
        }


    var showMoolaPage  = !teekaPage && hasMoolaPage;
        var query = stored.query.trim();
    var showMoolaBack  = !!teekaPage;
        if (!query) return;
    var showUllekha    = hasUllekhaPage || !!teekaPage;


    if ( !showMoolaPage && !showMoolaBack && !showUllekha ) return;
        try { sessionStorage.removeItem('gr_search_hl'); } catch (e) { }


    var nav = document.createElement( 'div' );
        var delays = [0, 200, 600];
    nav.id = 'gr-toc-doc-nav';
        delays.forEach(function (ms) {
    nav.setAttribute( 'class', 'toc-main-links');
            setTimeout(function () {
                if (document.querySelector('.gr-search-hl')) return;
                highlightText(query);
            }, ms);
        });
    }


     // ── CHANGE: makeBtn now wraps label in a data-deva span so
     // Uses the shared, reflow-proof scroller when it is available.
    // transliteration (script switching) applies to button text.
     function grScrollTo(el) {
     function makeBtn( href, label ) {
        if (!el) return;
      var a = document.createElement( 'a' );
        if (window.grScrollToElement) window.grScrollToElement(el);
      a.href = href;
         else el.scrollIntoView({ behavior: 'smooth', block: 'center' });
      a.setAttribute( 'class', 'toc-main-link-item');
      var lspan = document.createElement( 'span' );
      lspan.setAttribute( 'data-deva', label );
      lspan.textContent = ( currentScript && currentScript !== 'deva' )
         ? transliterateText( label, currentScript )
        : label;
      translatableSpans.push( lspan );
      a.appendChild( lspan );
      a.addEventListener( 'mouseover', function () { this.style.opacity = '0.72'; } );
      a.addEventListener( 'mouseout', function () { this.style.opacity = '1';    } );
      return a;
     }
     }


     if ( showMoolaPage ) nav.appendChild( makeBtn( moolaPageUrl, 'मूलम्' ) );
     function highlightText(query) {
    if ( showMoolaBack ) nav.appendChild( makeBtn( moolaUrl,    'मूल' ) );
        var content = document.querySelector('#mw-content-text .mw-parser-output');
    if ( showUllekha  ) nav.appendChild( makeBtn( ullekhaUrl,  'उल्लेख') );
        if (!content) return;


    var tocContents = toc.querySelector( '.vector-toc-contents' );
        var raw = query.replace(/^"|"$/g, '').trim();
    if ( tocContents ) toc.insertBefore( nav, tocContents );
        if (!raw) return;
    else              toc.appendChild( nav );
  }


  // ── TOC active-item highlight ────────────────────────────────────
        var patterns = [];
  function watchTocActive() {
        patterns.push(escapeRegex(raw));
    if ( _isNoTocPage() ) return;
        raw.split(/\s+/).forEach(function (w) {
    var toc = document.querySelector( '.vector-toc' );
            if (w.length >= 2) patterns.push(escapeRegex(w));
    if ( !toc ) return;
        });
    if ( !window.MutationObserver ) return;


    if ( toc._grObserved ) {
        var matched = false;
      toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
        for (var pi = 0; pi < patterns.length; pi++) {
      return;
            var re;
            try { re = new RegExp('(' + patterns[pi] + ')', 'gi'); }
            catch (e) { continue; }
            var count = wrapMatches(content, re);
            if (count > 0) { matched = true; break; }
        }
 
        if (!matched) return;
 
        var first = document.querySelector('.gr-search-hl');
        if (first) {
            grScrollTo(first);
            first.classList.add('gr-search-hl-pulse');
            setTimeout(function () { first.classList.remove('gr-search-hl-pulse'); }, 2000);
            // NEW: directly trigger the TOC to expand and highlight the
            // right section, rather than relying solely on the scroll
            // observer to catch up on its own. This is the actual fix —
            // confirmed the helper against a realistic multi-section
            // fragment before wiring it in here.
            if (window.grHighlightTocForElement) window.grHighlightTocForElement(first);
        }
 
        showDismissBar(query);
    }
 
    function escapeRegex(s) {
        return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
     }
     }
    toc._grObserved = true;


     var ACTIVE_COLOR = '#f57c00';
     function wrapMatches(root, re) {
        var count = 0;
        var walker = document.createTreeWalker(
            root, NodeFilter.SHOW_TEXT, {
            acceptNode: function (node) {
                var p = node.parentElement;
                if (!p) return NodeFilter.FILTER_REJECT;
                var tag = p.tagName.toUpperCase();
                if (tag === 'SCRIPT' || tag === 'STYLE' || tag === 'NOSCRIPT') return NodeFilter.FILTER_REJECT;
                if (p.classList.contains('gr-search-hl')) return NodeFilter.FILTER_REJECT;
                return NodeFilter.FILTER_ACCEPT;
            }
        }, false
        );


    function setActive( li, on ) {
        var nodes = [];
      var link = li.querySelector( '.vector-toc-link' ) || li.querySelector( 'a' );
        var node;
      if ( !link ) return;
        while ((node = walker.nextNode())) nodes.push(node);
      var hasActiveChild = !!li.querySelector(
 
         '.vector-toc-list-item .vector-toc-list-item-active'
        nodes.forEach(function (textNode) {
      );
            var val = textNode.nodeValue;
      var shouldHighlight = on && !hasActiveChild;
            if (!re.test(val)) return;
      if ( shouldHighlight ) {
            re.lastIndex = 0;
        link.style.setProperty( 'color',       ACTIVE_COLOR, 'important' );
 
        link.style.setProperty( 'font-weight', '700',       'important' );
            var frag = document.createDocumentFragment();
        link.querySelectorAll( '*' ).forEach( function ( el ) {
            var last = 0;
          el.style.setProperty( 'color',       ACTIVE_COLOR, 'important' );
            var m;
          el.style.setProperty( 'font-weight', '700',       'important' );
            while ((m = re.exec(val)) !== null) {
         } );
                if (m.index > last) {
      } else {
                    frag.appendChild(document.createTextNode(val.slice(last, m.index)));
         link.style.removeProperty( 'color' );
                }
         link.style.setProperty( 'font-weight', '400', 'important' );
                var span = document.createElement('span');
        link.querySelectorAll( '*' ).forEach( function ( el ) {
                span.className = 'gr-search-hl';
          el.style.removeProperty( 'color' );
                span.textContent = m[0];
          el.style.setProperty( 'font-weight', '400', 'important' );
                frag.appendChild(span);
         } );
                last = m.index + m[0].length;
      }
                count++;
            }
            if (last < val.length) {
                frag.appendChild(document.createTextNode(val.slice(last)));
            }
            textNode.parentNode.replaceChild(frag, textNode);
        });
 
        return count;
    }
 
    function showDismissBar(query) {
        var existing = document.getElementById('gr-hl-bar');
        if (existing) existing.remove();
 
        var count = document.querySelectorAll('.gr-search-hl').length;
        if (!count) return;
 
        var isMob = window.innerWidth < 768 || !!document.getElementById('mw-mf-viewport');
         var bar = document.createElement('div');
        bar.id = 'gr-hl-bar';
 
        if (isMob) {
            bar.className = 'gr-search-hl-mobile';
 
            bar.innerHTML =
                '<button id="gr-hl-results" class="gr-search-bar-btn">' +
                '<span class="gra-icon gra-icon-search" aria-hidden="true"></span>' +
                '<span>' + count + ' match' + (count === 1 ? '' : 'es') + '</span></button>' +
                '<button id="gr-hl-prev" class="gr-search-bar-btn">' +
                '<span class="gra-icon gra-icon-prev" aria-hidden="true"></span>' +
                '<span>Prev</span></button>' +
                '<button id="gr-hl-next" class="gr-search-bar-btn">' +
                '<span class="gra-icon gra-icon-next" aria-hidden="true"></span>' +
                '<span>Next</span></button>' +
                '<button id="gr-hl-dismiss" class="gr-search-bar-btn">' +
                '<span class="gra-icon gra-icon-dismiss" aria-hidden="true"></span>' +
                '<span>Close</span></button>';
        } else {
            bar.style.cssText = [
                'position:fixed', 'bottom:0', 'left:0', 'right:0', 'z-index:10200',
                'background:#b5451b', 'color:#fff', 'padding:10px 16px',
                'display:flex', 'align-items:center', 'justify-content:space-between',
                'font-family:system-ui,sans-serif', 'font-size:14px',
                'box-shadow:0 -2px 8px rgba(0,0,0,0.2)'
            ].join(';');
 
            var nav = document.createElement('div');
            nav.style.cssText = 'display:flex;align-items:center;gap:12px;';
            nav.innerHTML =
                '<span>🔍 <strong>' + escHtml(query) + '</strong> — ' + count + ' match' + (count === 1 ? '' : 'es') + '</span>' +
                '<button id="gr-hl-prev" style="background:rgba(255,255,255,0.2);border:none;color:#fff;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;">↑ Prev</button>' +
                '<button id="gr-hl-next" style="background:rgba(255,255,255,0.2);border:none;color:#fff;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;">↓ Next</button>' +
                '<button id="gr-hl-results" style="background:rgba(255,255,255,0.2);border:none;color:#fff;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;">← Results</button>';
            bar.appendChild(nav);
 
            var db = document.createElement('button');
            db.id = 'gr-hl-dismiss'; db.textContent = '✕ Close';
            db.style.cssText = 'background:rgba(255,255,255,0.15);border:none;color:#fff;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:13px;min-height:32px;';
            bar.appendChild(db);
         }
 
        document.body.appendChild(bar);
 
         var hlEls = Array.from(document.querySelectorAll('.gr-search-hl'));
         var current = 0;
 
        function goTo(idx) {
            hlEls.forEach(function (el) { el.classList.remove('gr-search-hl-current'); });
            current = ((idx % hlEls.length) + hlEls.length) % hlEls.length;
            hlEls[current].classList.add('gr-search-hl-current');
            grScrollTo(hlEls[current]);
            // NEW: same TOC sync on Prev/Next navigation within the
            // results, not just the initial jump.
            if (window.grHighlightTocForElement) window.grHighlightTocForElement(hlEls[current]);
        }
 
        function dismiss() { clearHighlights(); bar.remove(); }
 
        var nb = document.getElementById('gr-hl-next');
        var pb = document.getElementById('gr-hl-prev');
        var rb = document.getElementById('gr-hl-results');
        var db2 = document.getElementById('gr-hl-dismiss');
         if (nb) nb.onclick = function () { goTo(current + 1); };
        if (pb) pb.onclick = function () { goTo(current - 1); };
        if (db2) db2.onclick = dismiss;
        if (rb) rb.onclick = function () {
            bar.remove(); clearHighlights();
            if (window.showSearchDialog) window.showSearchDialog(query);
        };
     }
     }


     function normaliseAll() {
     function clearHighlights() {
      toc.querySelectorAll( '.vector-toc-list-item' ).forEach( function ( li ) {
        document.querySelectorAll('.gr-search-hl').forEach(function (span) {
        setActive( li, li.classList.contains( 'vector-toc-list-item-active' ) );
            var p = span.parentNode; if (!p) return;
      } );
            while (span.firstChild) p.insertBefore(span.firstChild, span);
            p.removeChild(span);
        });
     }
     }


     function attachHighlight( li ) {
     function escHtml(s) {
      if ( li._grHighlightAttached ) return;
        return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
      li._grHighlightAttached = true;
      liObs.observe( li, { attributes: true, attributeFilter: [ 'class' ] } );
      setActive( li, li.classList.contains( 'vector-toc-list-item-active' ) );
     }
     }


     var liObs = new MutationObserver( function ( mutations ) {
     function injectHighlightCSS() {
      mutations.forEach( function ( m ) {
         if (document.getElementById('gr-hl-css')) return;
         if ( m.attributeName !== 'class' ) return;
         var s = document.createElement('style');
         var li    = m.target;
         s.id = 'gr-hl-css';
         var active = li.classList.contains( 'vector-toc-list-item-active' );
        s.textContent = [
         setActive( li, active );
            '.gr-search-hl{background:#fff176;color:#1a1a1a;border-radius:2px;padding:0 1px;box-shadow:0 0 0 1px rgba(181,69,27,0.25);}',
            '.gr-search-hl-current{background:#ffb300!important;box-shadow:0 0 0 2px #b5451b!important;}',
            '@keyframes gr-hl-pulse{0%{background:#ffb300;}50%{background:#fff176;}100%{background:#fff176;}}',
            '.gr-search-hl-pulse{animation:gr-hl-pulse 1.2s ease 2;}',
        ].join('');
         document.head.appendChild(s);
    }


         if ( active ) {
    injectHighlightCSS();
          var anc = li.parentNode;
 
          while ( anc && anc !== document.body ) {
    if (document.readyState === 'loading') {
             if ( anc.classList &&
        document.addEventListener('DOMContentLoaded', applyHighlight);
                anc.classList.contains( 'vector-toc-list-item' ) &&
    } else {
                anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
         applyHighlight();
              anc.classList.remove( 'vector-toc-list-item-collapsed' );
    }
 
    if (window.mw) {
        mw.hook('wikipage.content').add(function () {
            setTimeout(applyHighlight, 100);
        });
    }
 
    window.grStoreSearchHL = storeQueryForLink;
 
}());
 
/* ═══════════════════════════════════════════════════════════════
  Mobile addon — only runs on Minerva (mobile) skin
  ═══════════════════════════════════════════════════════════════ */
(function () {
    if (!document.body.classList.contains('skin-minerva')) return;
    function injectMenuLinks() {
        if (document.getElementById('gr-mob-menu-items')) return;
        var navDrawer = document.querySelector('.navigation-drawer');
        if (!navDrawer) return;
        var wrap = document.createElement('div'); wrap.id = 'gr-mob-menu-items'; wrap.className = 'gr-mob-menu';
        function makeItem(href, label) { var a = document.createElement('a'); a.href = href; a.className = 'gr-mob-item'; a.textContent = label; return a; }
        wrap.appendChild(makeItem('/Main_Page', 'Home'));
        wrap.appendChild(makeItem('/My_wiki:Help', 'Help'));
        wrap.appendChild(makeItem('/My_wiki:About', 'About'));
        var userName = window.mw ? mw.config.get('wgUserName') : null;
        if (userName) {
            wrap.appendChild(makeItem((window.mw && mw.util && mw.util.getUrl) ? mw.util.getUrl('Special:Profile') : '/Special:Profile', 'Profile'));
            var la = document.querySelector('a[href*="action=logout"]');
            wrap.appendChild(makeItem(la ? la.href : '/index.php?title=Special:UserLogout', 'Log out'));
        }
        else wrap.appendChild(makeItem('/index.php?title=Special:UserLogin', 'Log in'));
        var pageLeft = document.getElementById('mw-mf-page-left');
        if (pageLeft) {
            // Hide Minerva's own menu lists rather than deleting them: Minerva's
            // initMobile.js looks up .toggle-list__list inside this panel and
            // throws ("Cannot read properties of null (reading 'parentNode')")
            // when it is gone, which also stops the rest of Minerva's setup.
            Array.prototype.forEach.call(pageLeft.children, function (c) { c.classList.add('gr-mf-native-menu'); });
             pageLeft.style.removeProperty('display');
            pageLeft.appendChild(wrap);
        }
        else navDrawer.appendChild(wrap);
    }
    function injectMoolaUllekhaLinks() {
        if (document.getElementById('gr-mob-doc-nav')) return;
        var pageName = (window.mw && mw.config && mw.config.get('wgPageName')) || '';
        if (pageName === 'Main_Page' || !pageName) return;
        function wikiUrl(slug) { if (window.mw && mw.util && mw.util.getUrl) return mw.util.getUrl(slug); return ((window.mw && mw.config.get('wgArticlePath')) || '/wiki/$1').replace('$1', encodeURIComponent(slug).replace(/%2F/g, '/')); }
        var teekaPage = document.querySelector('.gr-teeka-page');
        var primarySlug = teekaPage ? (teekaPage.getAttribute('data-primary') || pageName.split('/')[0]) : pageName.split('/')[0];
        var docTitleEl = document.querySelector('.gr-doc-title');
        var hasMoolaPage = docTitleEl && docTitleEl.getAttribute('data-has-moola') === '1';
        var hasUllekhaPage = docTitleEl && docTitleEl.getAttribute('data-has-ullekha') === '1';
        var showMoolam = !!teekaPage || hasMoolaPage, showUllekha = hasUllekhaPage || !!teekaPage;
        if (!showMoolam && !showUllekha) return;
        var nav = document.createElement('div'); nav.id = 'gr-mob-doc-nav';
        nav.className = 'toc-main-links';        // styled in Common.css §24
        function makeLink(href, label) { var a = document.createElement('a'); a.href = href; a.textContent = label; a.className = 'toc-main-link-item'; return a; }
        if (teekaPage) nav.appendChild(makeLink(wikiUrl(primarySlug), 'मूल'));
        else if (hasMoolaPage) nav.appendChild(makeLink(wikiUrl(primarySlug + '/Moola'), 'मूलम्'));
        if (showUllekha) nav.appendChild(makeLink(wikiUrl(primarySlug + '/Ullekha'), 'उल्लेख'));
        // Right under the document's own title, which is what a reader on a
        // phone actually sees as the title; the skin's h1 is the fallback.
        var docTitle = document.querySelector('#mw-content-text .gr-doc-title');
        var h1 = document.getElementById('firstHeading') || document.querySelector('.page-heading, h1.firstHeading, .mw-first-heading');
        if (docTitle && docTitle.parentNode) docTitle.parentNode.insertBefore(nav, docTitle.nextSibling);
        else if (h1 && h1.parentNode) h1.parentNode.insertBefore(nav, h1.nextSibling);
        else { var ct = document.getElementById('mw-content-text'); if (ct) ct.insertBefore(nav, ct.firstChild); }
    }
    function boot() { injectMenuLinks(); injectMoolaUllekhaLinks();}
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
    else boot();
    if (window.mw) mw.hook('wikipage.content');
}());
 
(function () {
    'use strict';
    if (mw.config.get('wgPageName') !== 'Main_Page') return;
    mw.loader.using('mediawiki.util').done(function () {
        $(function () { applyHomeToggleOffset(); window.addEventListener('resize', applyHomeToggleOffset, { passive: true }); setTimeout(applyHomeToggleOffset, 300); setTimeout(applyHomeToggleOffset, 800); });
    });
    function applyHomeToggleOffset() {
        var bar = document.getElementById('gr-static-bar'); if (!bar) return;
        var barBottom = Math.round(bar.getBoundingClientRect().bottom);
        [document.getElementById('gr-home'), document.getElementById('gr-home-toggle'), document.querySelector('#mw-content-text .mw-parser-output > .gr-home, #mw-content-text .mw-parser-output > *:first-child')].forEach(function (el) { if (el) el.style.scrollMarginTop = (barBottom + 4) + 'px'; });
        var isMob = window.innerWidth < 768 || !!document.getElementById('mw-mf-viewport');
        if (isMob) { var toggleEl = document.getElementById('gr-home-toggle'); if (toggleEl) { var tr = toggleEl.getBoundingClientRect(); if (tr.top < barBottom) { var cp = parseInt(window.getComputedStyle(document.body).paddingTop, 10) || 0; document.body.style.paddingTop = (cp + (barBottom - tr.top) + 4) + 'px'; } } }
    }
    $(function () {
        var $toggle = $('#gr-home-toggle'), $viewG = $('#gr-view-grantha'), $viewA = $('#gr-view-author'), $btnG = $('#gr-toggle-grantha'), $btnA = $('#gr-toggle-author');
        if (!$toggle.length || !$viewG.length || !$viewA.length) return; if ($toggle.data('gr-wired')) return; $toggle.data('gr-wired', true);
        function showView(which) { if (which === 'grantha') { $viewG.show(); $viewA.hide(); $btnG.addClass('gr-toggle-active'); $btnA.removeClass('gr-toggle-active'); } else { $viewA.show(); $viewG.hide(); $btnA.addClass('gr-toggle-active'); $btnG.removeClass('gr-toggle-active'); } try { localStorage.setItem('grantha_home_tab', which); } catch (e) { } }
        $btnG.on('click keydown', function (e) { if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; showView('grantha'); });
        $btnA.on('click keydown', function (e) { if (e.type === 'keydown' && e.key !== 'Enter' && e.key !== ' ') return; showView('author'); });
        try { var saved = localStorage.getItem('grantha_home_tab'); if (saved === 'author') showView('author'); else showView('grantha'); } catch (e) { showView('grantha'); }
    });
}());
// ── Replace MediaWiki user links with a single username dropdown (Profile + Logout) ──
(function () {
    function wikiHref(title) {
        if (window.mw && mw.util && mw.util.getUrl) return mw.util.getUrl(title);
        var ap = (window.mw && mw.config && mw.config.get('wgArticlePath')) || '/wiki/$1';
        return ap.replace('$1', title);
    }
}());
/* ═══════════════════════════════════════════════════════════════
  Commentary interleaving — discovers a document's /Vyakhya or
  /Bhashya commentary subpages, lets a reader select which ones to
  show interleaved with the moola text, and augments any existing
  link to a commentary (e.g. a "वादावली" list) with its own inline
  checkbox. Exposes window.grTeekaInterleave so readerToolbar.js's
  "Commentaries" dropdown can stay in sync without duplicating this
  logic — same pattern as window.grHighlightTocForElement above.
  ═══════════════════════════════════════════════════════════════ */
(function () {
 
    var LS_TEEKA_KEY = 'grantha_reader_teeka_in_moola';
 
    // Distinct light background per commentary, for when multiple
    // different teekas are interleaved together and need to stay
    // visually distinguishable. CHANGED: was a 30-color palette
    // transcribed from a screenshot - confirmed directly that at least
    // one entry (#F6C500, a saturated gold) wasn't actually light at
    // all, which is very likely what read as "really dark" in practice.
    // Replaced with 10 colors in the site's own warm palette (matching
    // the maroon/cream tones already used throughout Common.css -
    // #b5451b, #fdf0eb, #f0cfc4), all kept in the ~94-97% lightness
    // range so they stay easy to read against dark text regardless of
    // which one a given teeka lands on. First entry is #fcf5f3 - the
    // exact background .teeka-block itself already uses on a native
    // teeka page (verified at 97.1% lightness), per direction, so the
    // most common single-commentary case matches the site's own
    // existing look exactly rather than introducing a new shade for it.
    var TEEKA_COLOR_PALETTE = [
        '#fcf5f3', '#F5EFE6', '#F0F5EC', '#EEF3F6', '#F6EFF3',
        '#F3F0F8', '#F8F3EA', '#EEF6F2', '#F6EEEE', '#F2F0EC'
    ];
    // Stable djb2-style hash - same teeka slug always maps to the SAME
    // color everywhere on the site (not re-randomized per page load or
    // fetch order) - verified directly: a reader learning "this shade =
    // भावप्रकाशः" needs that to hold site-wide.
    function teekaColorForSlug(slug) {
        var h = 5381;
        for (var i = 0; i < slug.length; i++) h = ((h * 33) ^ slug.charCodeAt(i)) >>> 0;
        return TEEKA_COLOR_PALETTE[h % TEEKA_COLOR_PALETTE.length];
    }
 
    function readLS(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
    function writeLS(k, v) { try { localStorage.setItem(k, v); } catch (e) { } }
 
    function fetchAllSubpages(rootTitle) {
        var deferred = $.Deferred(), subpages = [], nsNum = mw.config.get('wgNamespaceNumber'), localName = mw.config.get('wgTitle');
        function ask(apfrom) {
            var params = { action: 'query', list: 'allpages', apprefix: localName + '/', apnamespace: nsNum, aplimit: 'max', format: 'json' };
            if (apfrom) params.apcontinue = apfrom;
            $.ajax({ url: mw.util.wikiScript('api'), data: params, dataType: 'json' }).then(function (data) {
                var pages = (data.query && data.query.allpages) || [];
                pages.forEach(function (p) { subpages.push(p.title); });
                if (data.continue && data.continue.apcontinue) ask(data.continue.apcontinue);
                else deferred.resolve(subpages);
            }).fail(function () { deferred.resolve(subpages); });
        }
        ask(null);
        return deferred.promise();
    }
 
    // One request for ALL of them, not one each. The API takes up to 50
    // titles at a time, and a moola page now shows every commentary by
    // default - so what used to be one round trip became five or ten,
    // fired together but each with its own latency, and the commentaries
    // appeared well after the text. Batching makes it a single request
    // whatever the page carries. maxage/smaxage let the browser and the
    // CDN reuse the response, so moving between chapters of one grantha
    // does not re-fetch the same commentaries.
    var teekaJsonCache = {};
 
    function fetchTeekaJsons(titles) {
        var want = titles.filter(function (t) { return !(t in teekaJsonCache); });
        if (!want.length) return $.Deferred().resolve(teekaJsonCache).promise();
 
        var batches = [];
        for (var i = 0; i < want.length; i += 40) batches.push(want.slice(i, i + 40));
 
        return $.when.apply($, batches.map(function (batch) {
            return $.ajax({
                url: mw.util.wikiScript('api'),
                data: {
                    action: 'query',
                    titles: batch.map(function (t) { return 'JSON:' + t; }).join('|'),
                    prop: 'revisions', rvslots: 'main', rvprop: 'content',
                    format: 'json', formatversion: 2,
                    maxage: 300, smaxage: 300
                },
                dataType: 'json'
            }).then(function (data) {
                var pages = (data.query && data.query.pages) || [];
                pages.forEach(function (page) {
                    // Match the response back to the title we asked for:
                    // the API may normalise or reorder, so never pair by
                    // index. Strip the namespace the same way it was added.
                    var title = (page.title || '').replace(/^JSON:/, '');
                    if (page.missing) { teekaJsonCache[title] = null; return; }
                    var content = page.revisions && page.revisions[0] &&
                        page.revisions[0].slots && page.revisions[0].slots.main &&
                        page.revisions[0].slots.main.content;
                    if (!content) { teekaJsonCache[title] = null; return; }
                    try {
                        var parsed = JSON.parse(content);
                        parsed._title = title;
                        teekaJsonCache[title] = parsed;
                    } catch (e) { teekaJsonCache[title] = null; }
                });
            }).catch(function () {
                batch.forEach(function (t) { if (!(t in teekaJsonCache)) teekaJsonCache[t] = null; });
            });
        })).then(function () {
            // Anything the response never mentioned is recorded as absent,
            // so a later call does not ask for it again.
            want.forEach(function (t) { if (!(t in teekaJsonCache)) teekaJsonCache[t] = null; });
            return teekaJsonCache;
        });
    }
 
    // Finds this document's commentary subpages - filters to the site's
    // own established commentary-path convention (Vyakhya/Bhashya are
    // shown; Moola/Ullekha are never commentary content).
    function findCommentarySubpages(rootTitle) {
        return $.Deferred(function (d) {
            fetchAllSubpages(rootTitle).then(function (all) {
                d.resolve(all.filter(function (t) {
                    return t !== rootTitle && (/\/vyakhya(\/|$)/i.test(t) || /\/bhashya(\/|$)/i.test(t));
                }));
            });
        }).promise();
    }
 
    // Builds moola-verse-id -> [{teekaTitle, teekaSlug, text, blockId}].
    // Handles TWO possible parent-linking shapes so this works across
    // every commentary document on the site, not just ones from any one
    // generation pipeline: (1) DIRECT - a type:"teeka" block's own
    // "parent" already IS the moola verse id; (2) TWO-HOP - it points to
    // a type:"verse" block WITHIN THE SAME teeka doc (a quoted-verse
    // reference), and THAT block's own parent is the moola verse id.
    // Deliberately skips anything whose parent is "UNVERIFIED" or
    // missing, rather than guessing.
    // CHANGED: also builds a slug -> color-palette-index map here, based
    // on teekaDocs' OWN ORDER (the order commentaries were actually
    // discovered/fetched for this page) rather than hashing each slug
    // independently. Confirmed directly this was the actual issue: hash-
    // based assignment gives every teeka a color, but nothing guarantees
    // the FIRST one shown on a page lands on palette slot 0 (#fcf5f3,
    // the site's own real .teeka-block default) - it gets whatever slot
    // its hash happens to produce. Positional assignment makes "first
    // shown = the exact default color" a guarantee, not a coincidence,
    // at the cost of a specific teeka's color now depending on where it
    // appears in a given page's own list rather than being fixed
    // site-wide - a reasonable trade given the direction received.
    var teekaColorIndexBySlug = {};
    function buildTeekaInMoolaIndex(teekaDocs) {
        var index = {};
        teekaColorIndexBySlug = {};
        var colorIdx = 0;
        teekaDocs.forEach(function (td) {
            if (!td || !td.blocks || !td.doc) return;
            var slug = td.doc.slug || td._title;
            if (!(slug in teekaColorIndexBySlug)) {
                teekaColorIndexBySlug[slug] = colorIdx % TEEKA_COLOR_PALETTE.length;
                colorIdx++;
             }
             }
             anc = anc.parentNode;
             var blockById = {};
          }
            td.blocks.forEach(function (b) { blockById[b.id] = b; });
            td.blocks.forEach(function (b) {
                if (b.type !== 'teeka') return;
                var moolaId = null;
                if (b.parent && b.parent !== 'UNVERIFIED' && !blockById[b.parent]) {
                    moolaId = b.parent;
                } else {
                    var verseRef = blockById[b.parent];
                    if (verseRef && (verseRef.type === 'verse' || verseRef.type === 'verse-ref') && verseRef.parent && verseRef.parent !== 'UNVERIFIED') {
                        moolaId = verseRef.parent;
                    }
                }
                if (!moolaId) return;
                if (!index[moolaId]) index[moolaId] = [];
                index[moolaId].push({
                    teekaTitle: td.doc.title || td._title,
                    teekaAuthor: td.doc.author || '',
                    teekaPage: td.doc.pageTitle || td._title,
                    teekaGranthaId: td.doc.granthaId || '',
                    teekaSlug: slug,
                    // join('\n') - each entry of `lines` is a printed line,
                    // and joining with a space ran a two-pada shloka into
                    // one. The .shloka-line markup inside already carries
                    // the break where the converter put one; this only
                    // matters for older documents that have none.
                    text: (b.lines || []).join('\n'),
                    footnotes: b.footnotes || [],
                    blockId: b.id
                });
            });
        });
        return index;
    }


          var container = document.querySelector( '.vector-sticky-pinned-container' );
    function injectTeekaInMoola(index) {
          if ( container ) {
        var injectedAny = false;
            var vis = container.offsetHeight > 0 &&
        Object.keys(index).forEach(function (verseId) {
                      container.offsetParent !== null &&
            var $verseEl = $(document.getElementById(verseId));
                      window.getComputedStyle( container ).display !== 'none' &&
            if (!$verseEl.length) return;
                      window.getComputedStyle( container ).visibility !== 'hidden';
            var entries = index[verseId];
            if ( vis ) {
            var $wrap = $('<div>').addClass('gr-injected-teeka-group');
              var lr = li.getBoundingClientRect();
            entries.forEach(function (entry) {
              var cr = container.getBoundingClientRect();
                // CHANGED: was teekaColorForSlug(slug) (hash-based) -
              if ( lr.top < cr.top + 4 || lr.bottom > cr.bottom - 4 ) {
                // now looks up the position assigned in
                var sh = null, node = li.parentNode;
                // buildTeekaInMoolaIndex, falling back to the hash only
                while ( node && node !== document.body ) {
                // if this slug is somehow missing from that map (should
                  var ov = window.getComputedStyle( node ).overflowY;
                // not normally happen, but safer than an undefined color).
                  if ( ( ov === 'auto' || ov === 'scroll' ) &&
                var colorIdx = teekaColorIndexBySlug[entry.teekaSlug];
                      node.scrollHeight > node.clientHeight ) { sh = node; break; }
                var color = (colorIdx !== undefined) ? TEEKA_COLOR_PALETTE[colorIdx] : teekaColorForSlug(entry.teekaSlug);
                  node = node.parentNode;
                var $entry = $('<div>').addClass('teeka-block')
                    .attr('data-teeka-slug', entry.teekaSlug)
                    .attr('data-teeka-block-id', entry.blockId)
                    // Citation metadata: a bookmark taken inside an
                    // interleaved commentary must cite THAT work and its
                    // author, and link to the commentary's own page - not
                    // to the moola whose page it happens to be shown on.
                    .attr('data-cite-author', entry.teekaAuthor || '')
                    .attr('data-cite-title', entry.teekaTitle || '')
                    .attr('data-cite-page', entry.teekaPage || '')
                    .attr('data-cite-grantha-id', entry.teekaGranthaId || '')
                    .attr('id', entry.blockId)
                    .css('background', color);
                if (window.grLibrary) {
                    (function (el, gid, page) {
                        // The id may come from the teeka's own JSON (if that
                        // document has been rebuilt) or from the map page.
                        // Either way the commentary cites ITS work, not the
                        // moola it is displayed inside.
                        var got = gid ? Promise.resolve(gid)
                                      : window.grLibrary.idForPage(page, null);
                        got.then(function (id) {
                            if (!id) return null;
                            el.setAttribute('data-cite-grantha-id', id);
                            return window.grLibrary.lookup(id);
                        }).then(function (g) {
                            if (!g) return;
                            if (g.name)      el.setAttribute('data-cite-title', g.name);
                            if (g.nameIAST)  el.setAttribute('data-cite-title-iast', g.nameIAST);
                            if (g.author)    el.setAttribute('data-cite-author', g.author);
                            if (g.authorIAST) el.setAttribute('data-cite-author-iast', g.authorIAST);
                            var h = el.querySelector('.teeka-title');
                            var label = teekaLabel(g);
                            if (h && label) h.textContent = label;
                            var slug = el.getAttribute('data-teeka-slug');
                            if (slug && label) {
                                labelBySlug[slug] = label;
                                // A footnote section for this commentary may
                                // already be on screen; rename it too.
                                document.querySelectorAll(
                                    '.gr-teeka-notes-group[data-teeka-slug="' + slug +
                                    '"] .gr-teeka-notes-title').forEach(function (t) {
                                        t.textContent = label;
                                    });
                            }
                        });
                    }($entry[0], entry.teekaGranthaId, entry.teekaPage));
                 }
                 }
                 if ( !sh && container.scrollHeight > container.clientHeight ) sh = container;
                 $('<div>').addClass('teeka-title').text(entry.teekaTitle).appendTo($entry);
                 if ( sh ) {
                $('<div>').addClass('teeka-body').html(entry.text).appendTo($entry);
                  var hr  = sh.getBoundingClientRect();
                // A teeka's footnotes live in ITS document, not in this
                  var top = lr.top - hr.top + sh.scrollTop;
                // page's own footnote list - so the highlighted words came
                  sh.scrollTop = Math.max( 0, top - sh.clientHeight / 2 + li.offsetHeight / 2 );
                // through the injection but nothing on this page knew what
                // they pointed at, and clicking them did nothing. Register
                // them here, under the commentary's title, for the note
                // card and the right-hand panel to read.
                 if (entry.footnotes && entry.footnotes.length && window.grFootnoteRegistry) {
                    window.grFootnoteRegistry.add(entry.teekaSlug, entry.teekaTitle, entry.footnotes);
                }
                $wrap.append($entry);
            });
            // After the bhashya, not between the verse and it. The moola
            // page prints verse then bhashya as one unit, and inserting
            // straight after the verse element split them apart, putting
            // the commentaries ahead of the bhashya they comment on.
            // The bhashya-collection for a verse carries id
            // "bhashya-<verse oldKey>"; where a page has none, the verse
            // itself is still the anchor.
            var anchor = $verseEl;
            var coll = document.getElementById('bhashya-' + verseId);
            if (coll && coll.compareDocumentPosition($verseEl[0]) & Node.DOCUMENT_POSITION_PRECEDING) {
                anchor = $(coll);
            } else {
                // Some pages emit the bhashya as a sibling block without
                // the collection wrapper - walk forward over any blocks
                // that belong to this same verse.
                var n = $verseEl[0].nextElementSibling;
                while (n && n.getAttribute &&
                      (n.getAttribute('data-parent-verse') === verseId ||
                        n.id === 'bhashya-' + verseId)) {
                    anchor = $(n);
                    n = n.nextElementSibling;
                 }
                 }
              }
             }
             }
          }
            anchor.after($wrap);
         }
            // Tell the script switcher about the new text, so an interleaved
      } );
            // commentary is transliterated like the rest of the page instead
     } );
            // of staying in Devanagari.
            try {
                window.dispatchEvent(new CustomEvent('gr-new-content', { detail: { container: $wrap[0] } }));
            } catch (e) { }
            injectedAny = true;
        });
        return injectedAny;
    }
    function removeTeekaInMoola() {
        $('.gr-injected-teeka-group').remove();
        if (window.grFootnoteRegistry) window.grFootnoteRegistry.clearInjected();
    }
 
    // CRITICAL FIX: confirmed directly this feature was corrupting saved
    // page content. QuickEdit is a contenteditable-based editor operating
    // on the page's LIVE DOM - since this feature injects real elements
    // (interleaved teeka blocks, inline checkboxes wrapping links) into
    // that same DOM, QuickEdit's save/serialization pipeline had no way
    // to distinguish them from genuine content, and was saving them back
    // into the page's actual wikitext as literal escaped HTML text. This
    // teardown strips every trace of this feature's DOM changes -
    // removing injected teeka blocks entirely, and for inline checkboxes,
    // specifically UNWRAPPING each link back to its own original
    // position (not just deleting the wrapper, which would also delete
    // the link itself) - restoring the DOM to exactly what the page's
    // real, saved content actually is, before any editor can see it.
    function teardownTeekaInterleave() {
        removeTeekaInMoola();
        $('.gr-teeka-cb-wrap').each(function () {
            var $wrap = $(this);
            var $link = $wrap.find('a');
            if ($link.length) $wrap.before($link);
            $wrap.remove();
         });
        // CHANGED: also strips the gr-teeka-list-item class added to any
        // <li> for bullet-removal - confirmed via direct test this was
        // missed initially, which would have left a class attribute
        // change in the saved content even after the wrapper/checkbox
        // itself was correctly removed. Also removes the class
        // ATTRIBUTE entirely (not just the class name) once empty -
        // classList.remove() alone leaves class="" behind, which a
        // byte-for-byte test caught as still not matching the true
        // original markup (which never had a class attribute at all).
        $('.gr-teeka-list-item').each(function () {
            this.classList.remove('gr-teeka-list-item');
            if (!this.className) this.removeAttribute('class');
        });
     }
 
    // Watches for the exact DOM signal that editing has actually begun -
    // Common.css targets .mw-body-content[contenteditable="true"]
    // directly, confirming this is the real, load-order-independent
    // trigger QuickEdit itself relies on, rather than trying to
    // intercept every possible UI path that could invoke it (edit
    // button, keyboard shortcut, or anything else) individually and
    // risk missing one.
    function watchForEditMode() {
        var target = document.querySelector('.mw-body-content') || document.body;
        if (!target || !window.MutationObserver) return;
        var wasEditing = false;
        var obs = new MutationObserver(function (mutations) {
            mutations.forEach(function (m) {
                if (m.attributeName !== 'contenteditable') return;
                var isEditingNow = m.target.getAttribute('contenteditable') === 'true';
                if (isEditingNow && !wasEditing) {
                    teardownTeekaInterleave();
                    wasEditing = true;
                } else if (!isEditingNow && wasEditing) {
                    // Editing ended without a page reload (e.g. cancelled) -
                    // restore using the already-fetched data cached in
                    // memory, no need to re-fetch anything from the server.
                    if (teekaIndex) renderInterleave();
                    if (lastAugmentArgs) {
                        var augmented = augmentExistingTeekaLinks(lastAugmentArgs.titles, lastAugmentArgs.docs);
                        if (augmented.length) observeInlineVisibility(augmented.map(function (a) { return a.el; }));
                    }
                    wasEditing = false;
                }
            });
        });
        obs.observe(target, { attributes: true, attributeFilter: ['contenteditable'], subtree: true });
    }
 
    function normalizeWikiPath(p) {
        try { p = decodeURIComponent(p); } catch (e) { }
        return p.replace(/_/g, ' ').replace(/\/+$/, '');
    }
 
    // ── State ──────────────────────────────────────────────────────
    // Not seeded from storage: every page load starts with all
    // commentaries on (see where availableList is filled). Unchecking one
    // still writes to storage, which keeps the control's own state
    // consistent within the page and leaves the value there for anything
    // else that reads it, but it is not what decides the initial view.
    var selectedTeekaSlugs = {};
    var teekaIndex = null;
    var availableList = []; // [{slug, title, subItems: [{slug, title}, ...]}]
    var lastAugmentArgs = null; // {titles, docs} - cached to restore inline checkboxes after a cancelled edit without re-fetching
    var checkboxesBySlug = {};
    var readyCallbacks = [];
    var changeCallbacks = [];
    var visibilityCallbacks = [];
    var isReady = false;
    var inlineListVisible = null; // null = no inline checkboxes exist at all on this page
 
    function registerCheckbox(slug, $cb) { (checkboxesBySlug[slug] = checkboxesBySlug[slug] || []).push($cb); }
 
    function renderInterleave() {
        removeTeekaInMoola();
        if (!teekaIndex || !Object.keys(selectedTeekaSlugs).length) return;
        var filtered = {};
        Object.keys(teekaIndex).forEach(function (verseId) {
            var entries = teekaIndex[verseId].filter(function (e) { return selectedTeekaSlugs[e.teekaSlug]; });
            if (entries.length) filtered[verseId] = entries;
        });
        var injectedAny = injectTeekaInMoola(filtered);
        if (!injectedAny) console.warn('[grTeekaInterleave] Selected commentaries loaded, but none of their verse ids matched any verse-block on THIS page.');
    }
 
    function toggleSlug(slug, checked) {
        if (checked) selectedTeekaSlugs[slug] = true; else delete selectedTeekaSlugs[slug];
        writeLS(LS_TEEKA_KEY, JSON.stringify(Object.keys(selectedTeekaSlugs)));
        (checkboxesBySlug[slug] || []).forEach(function ($cb) { $cb.prop('checked', checked); });
        renderInterleave();
        changeCallbacks.forEach(function (cb) { cb(Object.keys(selectedTeekaSlugs)); });
    }
 
    // Augments existing <a> links pointing to a known commentary subpage
    // with an inline checkbox, wherever the link already appears -
    // matched by real href rather than any assumed list markup, since
    // that markup isn't something this script controls.
    // CHANGED: wraps the checkbox AND the link together in one flex
    // container, rather than just wrapping the checkbox alone next to
    // an untouched sibling link - confirmed directly (screenshot, twice
    // now) that aligning a wrapped checkbox against ambient text-
    // baseline metrics alone still leaves a small vertical offset,
    // especially with the site's custom Devanagari font's own metrics.
    // Flexbox align-items:center between the checkbox and the link's
    // own line box is a more reliable way to center them against EACH
    // OTHER directly, rather than each independently against the
    // surrounding line. The link is moved (not cloned) into the
    // wrapper at its original DOM position, so its href/click behavior
    // and the surrounding "\u2014 author" text's sibling order are both
    // preserved exactly as before.
    function augmentExistingTeekaLinks(subpageTitles, docs) {
        var slugByPath = {};
        subpageTitles.forEach(function (title, i) {
            var doc = docs[i];
            if (!doc) return;
            slugByPath[normalizeWikiPath(mw.util.getUrl(title))] = doc.doc.slug || doc._title;
        });
        var $contentLinks = $('#mw-content-text a[href], .mw-parser-output a[href]');
        var augmented = [];
        $contentLinks.each(function () {
            var href = this.getAttribute('href') || '';
            var path;
            try { path = normalizeWikiPath(new URL(href, window.location.href).pathname); }
            catch (e) { return; }
            var slug = slugByPath[path];
            if (!slug || this.closest('.gr-injected-teeka-group')) return;
            var li = this.closest('li');
            if (li) li.classList.add('gr-teeka-list-item');
            var $anchor = $(this);
            var $wrap = $('<span>').addClass('gr-teeka-cb-wrap');
            var $cb = $('<input>').attr({ type: 'checkbox' }).addClass('gr-teeka-inline-checkbox').prop('checked', !!selectedTeekaSlugs[slug]);
            $anchor.before($wrap);
            $wrap.append($cb);
            $wrap.append($anchor);
            registerCheckbox(slug, $cb);
            $cb.on('change', function () { toggleSlug(slug, $cb.is(':checked')); });
            augmented.push({ slug: slug, el: $cb[0] });
        });
        var missedTitles = subpageTitles.filter(function (t, i) {
            var d = docs[i]; if (!d) return false;
            var s = d.doc.slug || d._title;
            return !augmented.some(function (a) { return a.slug === s; });
        });
        if (missedTitles.length) console.warn('[grTeekaInterleave] No matching link found on this page for: ' + missedTitles.join(', ') + ' - the toolbar dropdown still covers them.');
        return augmented;
    }
 
    // Tracks whether ANY inline checkbox is currently on-screen, and
    // notifies subscribers only when that overall state actually flips -
    // verified in isolation before wiring in here. Drives whether
    // readerToolbar.js's "Commentaries" control needs to be shown as a
    // fallback (inline list scrolled out of view) or can stay hidden
    // (reader can already see and use the inline checkboxes).
    function observeInlineVisibility(checkboxEls) {
        if (!checkboxEls.length || !window.IntersectionObserver) return;
        var visibleSet = new Set();
        var obs = new IntersectionObserver(function (entries) {
            entries.forEach(function (entry) {
                var id = entry.target.__grTeekaId;
                if (entry.isIntersecting) visibleSet.add(id); else visibleSet.delete(id);
            });
            var anyVisible = visibleSet.size > 0;
            if (anyVisible !== inlineListVisible) {
                inlineListVisible = anyVisible;
                visibilityCallbacks.forEach(function (cb) { cb(anyVisible); });
            }
        }, { rootMargin: '-60px 0px 0px 0px', threshold: 0 });
        checkboxEls.forEach(function (el, i) { el.__grTeekaId = 'cb' + i; obs.observe(el); });
    }


     var structObs = new MutationObserver( function ( mutations ) {
     function init() {
      mutations.forEach( function ( m ) {
        // Runs unconditionally, before any of the early-return checks
         if ( m.type !== 'childList' ) return;
        // below - harmless as a no-op teardown on pages where nothing
         m.addedNodes.forEach( function ( n ) {
        // ever gets injected, but critical to have active on every page
          if ( n.nodeType !== 1 ) return;
         // this feature COULD have injected into, regardless of which
          if ( n.classList && n.classList.contains( 'vector-toc-list-item' ) ) attachHighlight( n );
         // exact code path led here.
          if ( n.querySelectorAll ) n.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
        watchForEditMode();
          var newSpans = [];
        var ns = mw.config.get('wgNamespaceNumber');
          if ( n.classList && n.classList.contains( 'vector-toc-text' ) ) newSpans.push( n );
        var pageName = mw.config.get('wgPageName') || '';
          if ( n.querySelectorAll ) n.querySelectorAll( '.vector-toc-text' ).forEach( function ( s ) { newSpans.push( s ); } );
        var isMainPg = (pageName === 'Main_Page' || pageName === 'Main Page');
          newSpans.forEach( function ( span ) {
        var isTeekaPage = !!document.querySelector('.gr-teeka-page[data-interleaved="1"], .gr-parent-ref-block');
            if ( span.hasAttribute( 'data-deva' ) ) return;
        if (ns !== 0 || isMainPg || isTeekaPage) { isReady = true; readyCallbacks.forEach(function (cb) { cb([]); }); return; }
            var orig = span.textContent;
            if ( !orig.trim() ) return;
            span.setAttribute( 'data-deva', orig );
            if ( currentScript !== 'deva' ) span.textContent = transliterateText( orig, currentScript );
            translatableSpans.push( span );
          } );
        } );
      } );
    } );


    toc.querySelectorAll( '.vector-toc-list-item' ).forEach( attachHighlight );
        var rootTitle = mw.config.get('wgTitle');
    structObs.observe( toc, { childList: true, subtree: true } );
        findCommentarySubpages(rootTitle).then(function (subpages) {
            if (!subpages.length) { isReady = true; readyCallbacks.forEach(function (cb) { cb([]); }); return; }
            fetchTeekaJsons(subpages).then(function (cache) {
                var fetched = subpages.map(function (t) { return cache[t] || null; });
                // Pair title+doc together FIRST, then filter as one unit -
                // filtering `docs` separately from `subpages` and passing
                // both (now misaligned) arrays elsewhere was a confirmed
                // real bug in an earlier version of this feature.
                var pairs = subpages.map(function (title, i) { return { title: title, doc: fetched[i] }; })
                    .filter(function (p) {
                        if (!p.doc) console.warn('[grTeekaInterleave] Could not load JSON:' + p.title);
                        return !!p.doc;
                    });


    setTimeout( function () {
                // Reading order = the order the book prints the
      normaliseAll();
                // commentaries in, which a document states as doc.order.
      var active = toc.querySelector( '.vector-toc-list-item-active' );
                // findCommentarySubpages gets its list from
      if ( active ) setActive( active, true );
                // action=query&list=allpages, and that is ALPHABETICAL BY
    }, 300 );
                // TITLE - so Vadiraja came before Vyasatirtha, and the
  }
                // checkbox list, the colour slots and the interleaved
                // blocks all inherited that same wrong order, since every
                // one of them is derived from this array. Sorting here
                // fixes all three at once. A document with no doc.order
                // (anything published before this field existed) keeps its
                // existing relative position rather than jumping to the
                // front, so mixed pages stay stable.
                pairs = pairs.map(function (p, i) { return { p: p, i: i }; })
                    .sort(function (a, b) {
                        var ao = a.p.doc && a.p.doc.doc && a.p.doc.doc.order;
                        var bo = b.p.doc && b.p.doc.doc && b.p.doc.doc.order;
                        if (typeof ao === 'number' && typeof bo === 'number' && ao !== bo) return ao - bo;
                        if (typeof ao === 'number' && typeof bo !== 'number') return -1;
                        if (typeof bo === 'number' && typeof ao !== 'number') return 1;
                        return a.i - b.i;
                    })
                    .map(function (x) { return x.p; });


  // ── TOC: Use Vector's native TOC, add highlight + expand + rename ──────
                var topDocs = pairs.map(function (p) { return p.doc; });
  // Drops the custom TOC builder entirely. Vector builds the correct TOC
  // from page headings (== Chapter ==, === Section ===). We just:
  //  1. Rename "Contents" → "विषयसूची"
  //  2. Expand all collapsed sections
  //  3. Remove the "Beginning" top link
  //  4. Inject मूल/उल्लेख nav
  //  5. Add reliable IntersectionObserver active-highlight on headings


  function setupToc() {
                // NEW: sub-commentaries (tippanis) - a teeka doc declares
    if ( _isNoTocPage() ) return;
                // its own children via doc.related (an array of slugs,
    var toc = document.querySelector( '.vector-toc' );
                // matching the same field this session's own generator
    if ( !toc ) return;
                // already writes into new JSON docs). Only the ASK here
                // is to show these as sub-categories in the options list
                // and gate their visibility on the parent being checked
                // - so this only needs one more level of fetching, not
                // unbounded recursion.
                var relatedSlugs = [];
                topDocs.forEach(function (d) { (d.doc.related || []).forEach(function (s) { relatedSlugs.push(s); }); });
                var uniqueRelated = relatedSlugs.filter(function (s, i) { return relatedSlugs.indexOf(s) === i; });


    removeTocBeginning();
                function finishInit(subDocsBySlug) {
    renameTocTitle();
                    var allDocs = topDocs.slice();
    expandTocSections();
                    Object.keys(subDocsBySlug).forEach(function (slug) { if (subDocsBySlug[slug]) allDocs.push(subDocsBySlug[slug]); });
    injectTocDocNav();
                    teekaIndex = buildTeekaInMoolaIndex(allDocs);
    attachHeadingObserver();
                    availableList = topDocs.map(function (d) {
  }
                        var subItems = (d.doc.related || [])
                            .map(function (s) { return subDocsBySlug[s]; })
                            .filter(Boolean)
                            .map(function (sd) { return { slug: sd.doc.slug || sd._title, title: sd.doc.title || sd._title }; });
                        return { slug: d.doc.slug || d._title, title: d.doc.title || d._title, subItems: subItems };
                    });
                    // Cached at module level so watchForEditMode can
                    // correctly re-run link augmentation after a
                    // cancelled edit, without needing to re-fetch
                    // anything from the server.
                    // Every commentary is on at the start of EVERY page
                    // load, not only a reader's first visit. The moola is
                    // published together with its commentaries and the
                    // complete text is what a reader should land on; the
                    // checkboxes are for narrowing it down while reading,
                    // and that narrowing lasts for as long as the page is
                    // open rather than following the reader to the next one.
                    //
                    // This MUST come before augmentExistingTeekaLinks:
                    // that builds each inline checkbox with
                    // .prop('checked', !!selectedTeekaSlugs[slug]), reading
                    // the selection as it stands at that moment. Selecting
                    // afterwards left every box drawn unchecked while the
                    // commentaries themselves were shown - the state and
                    // the control disagreeing on screen.
                    availableList.forEach(function (it) {
                        selectedTeekaSlugs[it.slug] = true;
                        (it.subItems || []).forEach(function (sub) { selectedTeekaSlugs[sub.slug] = true; });
                    });


  // ── IntersectionObserver on actual heading elements ──────────────
                    lastAugmentArgs = { titles: pairs.map(function (p) { return p.title; }), docs: topDocs };
  // Watches the real == Heading == elements in the page body.
                    var augmented = augmentExistingTeekaLinks(lastAugmentArgs.titles, lastAugmentArgs.docs);
  // Much more reliable than watching gr-toc-anchor spans.
                    if (augmented.length) observeInlineVisibility(augmented.map(function (a) { return a.el; }));
  var _headingObserver = null;
                    else inlineListVisible = false; // no inline checkboxes exist - toolbar control should always show as the only access point
                    if (Object.keys(selectedTeekaSlugs).length) renderInterleave();
                    isReady = true;
                    readyCallbacks.forEach(function (cb) { cb(availableList); });
                    if (inlineListVisible !== null) visibilityCallbacks.forEach(function (cb) { cb(inlineListVisible); });
                }


  function attachHeadingObserver() {
                if (!uniqueRelated.length) { finishInit({}); return; }
    if ( _isNoTocPage() ) return;
                fetchTeekaJsons(uniqueRelated).then(function (cache) {
    if ( _headingObserver ) return;  // only attach once
                    var subDocsBySlug = {};
    if ( !window.IntersectionObserver ) {
                    uniqueRelated.forEach(function (slug) {
      watchTocActive();               // fallback for old browsers
                        if (!cache[slug]) console.warn('[grTeekaInterleave] Could not load sub-commentary JSON:' + slug);
      return;
                        subDocsBySlug[slug] = cache[slug] || null;
                    });
                    finishInit(subDocsBySlug);
                });
            });
        });
     }
     }


     var ACTIVE_COLOR = '#f57c00';
     window.grTeekaInterleave = {
     var _activeId    = null;
        teardown: teardownTeekaInterleave,
        onReady: function (cb) { if (isReady) cb(availableList); else readyCallbacks.push(cb); },
        onChange: function (cb) { changeCallbacks.push(cb); },
        onVisibilityChange: function (cb) { visibilityCallbacks.push(cb); if (inlineListVisible !== null) cb(inlineListVisible); },
        getSelected: function () { return Object.keys(selectedTeekaSlugs); },
        toggle: function (slug, checked) { toggleSlug(slug, checked); },
        registerCheckbox: registerCheckbox
    };
 
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
     else init();


    // Collect all headings that have an id (Vector gives them ids)
}());
    var content  = document.querySelector( '.mw-parser-output' );
    if ( !content ) return;
    var headings = Array.from(
      content.querySelectorAll( 'h1[id],h2[id],h3[id],h4[id],h5[id],h6[id]' )
    );
    if ( !headings.length ) return;


    var toc = document.querySelector( '.vector-toc' );
    if ( !toc ) return;


    function getTocLink( id ) {
/* ═══════════════════════════════════════════════════════════════
      // Vector renders TOC links as href="#id"
  Devanagari page titles, site-wide  (window.grTitles)
      return toc.querySelector( 'a[href="#' + CSS.escape( id ) + '"]' );
  ───────────────────────────────────────────────────────────────
  One source of truth for "what is this page called": the Main Page
  catalogue, where every published work and commentary is listed as
  [[Slug|देवनागरी नाम]]. update_main_page.py keeps that list current,
  so a newly pushed commentary gets its Devanagari name everywhere
  without anyone editing a hard-coded list.
 
  Used by:
    • readerToolbar.js search results (grDocDisplayTitle & co. ask
      grTitles first, then fall back to their own GR_DOC_TITLES map)
    • the Updates page
    • this block itself, which relabels any link on the page whose
      text is still the English slug (e.g. the "Vadavali · Bhavavivaranam"
      page-nav line), the mobile page heading, and the browser tab title.
 
  The catalogue is cached in localStorage for CACHE_HOURS and refreshed
  in the background, so lookups are synchronous on almost every view.
  ═══════════════════════════════════════════════════════════════ */
(function () {
    'use strict';
 
    var CATALOGUE_PAGE = 'Main_Page';
    var CACHE_KEY = 'gr_titles_v1';
    var CACHE_HOURS = 1;
    var DEVA = /[ऀ-ॿ]/;
 
    var full = {};    // "Katha/Vyakhya/VyasatirthaVivarana" → "श्रीव्यासतीर्थविरचितविवरणम्"
    var leaf = {};    // "VyasatirthaVivarana" → same; null when two pages share a leaf
    var waiting = [];
    var loaded = false;
 
    function cfg(k) { return (window.mw && mw.config && mw.config.get(k)) || null; }
    function norm(t) { return String(t || '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); }
    function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
    function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) { } }
    function script(name) {
        if (window.mw && mw.util && mw.util.wikiScript) return mw.util.wikiScript(name);
        return (cfg('wgScriptPath') || '') + '/' + name + '.php';
    }
 
    function add(title, label, override) {
        title = norm(title); label = String(label || '').replace(/<[^>]*>/g, '').trim();
        if (!title || !label || !DEVA.test(label)) return;
        if (override || !full[title]) full[title] = label;
        var l = title.split('/').pop();
        if (!(l in leaf) || override) leaf[l] = label;
        else if (leaf[l] !== label) leaf[l] = null;  // ambiguous leaf: don't guess
    }
    function parseCatalogue(txt) {
        var out = {}, re = /\[\[([^\]|#]+)\|([^\]]+)\]\]/g, m;
        while ((m = re.exec(txt || ''))) {
            var k = norm(m[1]);
            if (!out[k]) out[k] = m[2].replace(/<[^>]*>/g, '').trim();
        }
        return out;
    }
    function useCatalogue(map) {
        full = {}; leaf = {};
        Object.keys(map).forEach(function (k) { add(k, map[k]); });
        addCurrentPage();
    }
    // The page being viewed names itself in .gr-doc-title — covers a page
    // that isn't on the Main Page yet.
    function addCurrentPage() {
        var el = document.querySelector('#mw-content-text .gr-doc-title');
        var me = norm(cfg('wgPageName'));
        if (el && me && !full[me]) add(me, el.textContent);
    }
 
    // ── public API ────────────────────────────────────────────────
    function get(title) { return full[norm(title)] || null; }
    function getLeaf(seg) { var v = leaf[norm(seg)]; return v || null; }
    function whenReady(cb) { if (loaded) cb(); else waiting.push(cb); }
    function all() { var o = {}; for (var k in full) o[k] = full[k]; return o; }
    window.grTitles = { get: get, leaf: getLeaf, all: all, whenReady: whenReady, parse: parseCatalogue };
 
    function finish() {
        loaded = true;
        var cbs = waiting; waiting = [];
        cbs.forEach(function (cb) { try { cb(); } catch (e) { } });
        relabel();
        if (window.mw && mw.hook) mw.hook('gr.titles').fire(window.grTitles);
     }
     }


     function getTocLi( id ) {
    // ── relabel what's on screen ──────────────────────────────────
      var a = getTocLink( id );
     function articleTitleFromHref(href) {
      return a ? a.closest( '.vector-toc-list-item' ) : null;
        var u;
        try { u = new URL(href, location.href); } catch (e) { return null; }
        if (u.origin !== location.origin) return null;
        var t = u.searchParams.get('title');
        if (!t) {
            var ap = (cfg('wgArticlePath') || '/wiki/$1').split('$1')[0];
            if (u.pathname.indexOf(ap) !== 0) return null;
            t = u.pathname.slice(ap.length);
        }
        try { t = decodeURIComponent(t); } catch (e) { }
        return norm(t);
    }
    function relabel(root) {
        root = root || document;
        var links = root.querySelectorAll('#mw-content-text a[href], .gr-sticky-title a[href], #mw-panel a[href], .vector-toc a[href]');
        Array.prototype.forEach.call(links, function (a) {
            if (a.children.length) return;                        // leave rich links alone
            var text = norm(a.textContent);
            if (!text || DEVA.test(text)) return;                  // already Devanagari
            var target = articleTitleFromHref(a.getAttribute('href')) || norm(a.getAttribute('title'));
            if (!target) return;
            var tail = target.split('/').pop();
            if (text !== target && text !== tail) return;          // only replace a bare slug
            var label = get(target) || (text === tail ? getLeaf(tail) : null);
            if (label) a.textContent = label;
        });
        // Page heading (shown on mobile) and the browser tab
        var me = norm(cfg('wgPageName'));
        var myLabel = get(me);
        if (!myLabel) return;
        var h = document.querySelector('#firstHeading .mw-page-title-main') || document.getElementById('firstHeading');
        if (h && !h.children.length && norm(h.textContent) === me) h.textContent = myLabel;
        var wgTitle = norm(cfg('wgTitle'));
        if (wgTitle && document.title.indexOf(wgTitle) === 0) document.title = myLabel + document.title.slice(wgTitle.length);
        else if (document.title.indexOf(me) === 0) document.title = myLabel + document.title.slice(me.length);
     }
     }


     function clearActive() {
    // ── load ─────────────────────────────────────────────────────
      toc.querySelectorAll( '.vector-toc-list-item-active' ).forEach( function ( li ) {
     function fetchFresh() {
        li.classList.remove( 'vector-toc-list-item-active' );
        return fetch(script('index') + '?title=' + CATALOGUE_PAGE + '&action=raw', { credentials: 'same-origin' })
        var lnk = li.querySelector( '.vector-toc-link' );
            .then(function (r) { return r.ok ? r.text() : Promise.reject(); })
        if ( !lnk ) return;
            .then(function (txt) {
        lnk.style.removeProperty( 'color' );
                var map = parseCatalogue(txt);
        lnk.style.setProperty( 'font-weight', '400', 'important' );
                lsSet(CACHE_KEY, JSON.stringify({ at: Date.now(), map: map }));
        lnk.querySelectorAll( '*' ).forEach( function ( el ) {
                return map;
          el.style.removeProperty( 'color' );
            });
          el.style.setProperty( 'font-weight', '400', 'important' );
        } );
      } );
     }
     }
    function boot() {
        var cached = null;
        try { cached = JSON.parse(lsGet(CACHE_KEY) || 'null'); } catch (e) { }
        if (cached && cached.map) { useCatalogue(cached.map); finish(); }
        var stale = !cached || (Date.now() - cached.at > CACHE_HOURS * 3600000);
        if (!stale) return;
        fetchFresh().then(function (map) {
            useCatalogue(map);
            if (!loaded) finish(); else relabel();
        }).catch(function () { if (!loaded) { addCurrentPage(); finish(); } });
    }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
    else boot();
    // Content that arrives later (previews, lazy sections, search panes)
    if (window.mw && mw.hook) mw.hook('wikipage.content').add(function ($c) {
        if (loaded) { addCurrentPage(); relabel($c && $c[0] ? $c[0] : document); }
    });
}());
/* ═══════════════════════════════════════════════════════════════
  Header links "Library" and "Updates", and the Updates page
  ───────────────────────────────────────────────────────────────
  Desktop (Vector): "Library" and "Updates" added before "Help".
  Mobile (Minerva): the same two items in the menu panel, after "Home".
  "Updates" opens the page My_wiki:Updates, which has two parts:
  1. What's new — filled in automatically from the wiki's edit history
      into <div id="gr-updates-feed">. Nothing to maintain by hand:
        • New      a page created in the last WINDOW_DAYS days
        • Updated  an existing page whose edits in that window add up to
                  at least UPDATE_MIN_BYTES (so typo fixes don't show)
      Titles are the Devanagari names from window.grTitles (Main Page catalogue).
      Test pages, helper subpages (/Ullekha, /Moola, /C1 …) and deleted
      pages are left out.
  2. Announcements & events — a card editors fill by hand with
      {{Bulletin|type=…|date=…|title=…|text=…}} lines on that page.


    function setActive( id ) {
  Styling: MediaWiki:Common.css §37 (no inline styles here).
      if ( _activeId === id ) return;
  ═══════════════════════════════════════════════════════════════ */
      _activeId = id;
(function () {
      clearActive();
    'use strict';
      if ( !id ) return;


      var li = getTocLi( id );
    var LIBRARY_URL = 'https://dvaita-grantha-hierarchy-library.ai.studio/';
      if ( !li ) return;
    var LIBRARY_LABEL = 'Library';
    var UPDATES_LABEL = 'Updates';
    var UPDATES_PAGE = 'My_wiki:Updates';  // the page the "Updates" link opens
    var WINDOW_DAYS = 90;          // how far back the list looks
    var UPDATE_MIN_BYTES = 1500;  // edits smaller than this in total aren't "updates"
    var MAX_ITEMS = 100;          // rows shown on the page
    var CACHE_MINUTES = 10;        // per-tab cache so every page view isn't an API call
    var CATALOGUE_PAGE = 'Main_Page';
    var SKIP_TITLE = /^(Main Page|Test|Sandbox)|(^|\/)(Ullekha|Moola|C\d+)$|(^|\/)(Test|Teeka|Tippani)\d*$/i;
    var HIDE = [];                 // exact titles never to list, e.g. ['Sumadhwavijaya/Vyakhya/पदार्थदीपिका']


      li.classList.add( 'vector-toc-list-item-active' );
    var SEEN_KEY = 'gr_updates_seen';
    var CACHE_KEY = 'gr_updates_cache_v3';
    var isMobile = document.body.classList.contains('skin-minerva');


      // Highlight only the innermost active item
    // ── helpers ────────────────────────────────────────────────
      var hasActiveChild = !!li.querySelector(
    function cfg(k) { return (window.mw && mw.config && mw.config.get(k)) || null; }
         '.vector-toc-list-item .vector-toc-list-item-active'
    function wikiHref(title) {
      );
        if (window.mw && mw.util && mw.util.getUrl) return mw.util.getUrl(title);
      if ( !hasActiveChild ) {
        var ap = cfg('wgArticlePath') || '/wiki/$1';
         var lnk = li.querySelector( '.vector-toc-link' );
        return ap.replace('$1', encodeURIComponent(title.replace(/ /g, '_')).replace(/%2F/g, '/').replace(/%3A/g, ':'));
         if ( lnk ) {
    }
          lnk.style.setProperty( 'color',      ACTIVE_COLOR, 'important' );
    function script(name) {
          lnk.style.setProperty( 'font-weight', '700',        'important' );
        if (window.mw && mw.util && mw.util.wikiScript) return mw.util.wikiScript(name);
          lnk.querySelectorAll( '*' ).forEach( function ( el ) {
         return (cfg('wgScriptPath') || '') + '/' + name + '.php';
             el.style.setProperty( 'color',       ACTIVE_COLOR, 'important' );
    }
             el.style.setProperty( 'font-weight', '700',       'important' );
    function lsGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
          } );
    function lsSet(k, v) { try { localStorage.setItem(k, v); } catch (e) { } }
    function ssGet(k) { try { return sessionStorage.getItem(k); } catch (e) { return null; } }
    function ssSet(k, v) { try { sessionStorage.setItem(k, v); } catch (e) { } }
    function el(tag, cls, text) {
        var n = document.createElement(tag);
        if (cls) n.className = cls;
        if (text != null) n.textContent = text;
        return n;
    }
    function norm(t) { return String(t || '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); }
 
    var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
    function absDate(d) { return MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); }
    // Today · 1 day ago · 5 days ago (under a week) · Sep 16, 2026
    function relDate(d, now) {
        now = now || new Date();
        var a = new Date(d.getFullYear(), d.getMonth(), d.getDate());
         var b = new Date(now.getFullYear(), now.getMonth(), now.getDate());
        var days = Math.round((b - a) / 86400000);
        if (days <= 0) return 'Today';
        if (days === 1) return '1 day ago';
         if (days < 7) return days + ' days ago';
        return absDate(d);
    }
 
    // ── data ───────────────────────────────────────────────────
    function getJSON(url) {
        return fetch(url, { credentials: 'same-origin' }).then(function (r) {
            if (!r.ok) throw new Error('HTTP ' + r.status);
            return r.json();
        });
    }
    function api(params) {
        var q = ['format=json', 'formatversion=2'];
        for (var k in params) q.push(k + '=' + encodeURIComponent(params[k]));
        return getJSON(script('api') + '?' + q.join('&'));
    }
 
    // Devanagari names come from window.grTitles (the Main Page catalogue,
    // cached) — the same names search and links use.
    function fetchCatalogue() {
        return new Promise(function (resolve) {
            if (window.grTitles) window.grTitles.whenReady(function () { resolve(window.grTitles.all()); });
             else resolve({});
        });
    }
 
    // Every main-namespace change in the window, oldest→newest pages grouped.
    function fetchChanges() {
        var since = new Date(Date.now() - WINDOW_DAYS * 86400000).toISOString().replace(/\.\d+Z$/, 'Z');
        var all = [];
        function page(cont, limit) {
            var p = {
                action: 'query', list: 'recentchanges', rctype: 'new|edit', rcnamespace: 0,
                rcshow: '!redirect', rcprop: 'title|timestamp|sizes', rclimit: limit, rcend: since
            };
            if (cont) p.rccontinue = cont;
             return api(p).then(function (j) {
                // The API answers 200 with an {error:...} body, so a failed
                // query is not a rejected promise. Treat it as one.
                if (j && j.error) throw new Error(j.error.code || 'api error');
                all = all.concat((j.query && j.query.recentchanges) || []);
                // Two pages (1,000 changes) is plenty for a notifications list.
                if (j['continue'] && j['continue'].rccontinue && all.length < 1000) {
                    return page(j['continue'].rccontinue, limit);
                }
                return all;
            }).catch(function (err) {
                // recentchanges over a date range is the most expensive query
                // this site makes, and on a busy or constrained database it
                // can come back as a DBQueryError. Halve the window and retry
                // once; failing that, return nothing. A "what's new" list is
                // not worth breaking the page for.
                if (limit > 100) return page(cont, 100);
                try {
                    console.warn('[gr] recent changes unavailable: ' +
                                (err && err.message || err));
                } catch (e) { }
                return all;
            });
         }
         }
      }
        return page(null, 500);
    }


      // Expand collapsed ancestors
    function summarise(changes, catalogue) {
      var anc = li.parentNode;
        var byTitle = {};
      while ( anc && anc !== toc ) {
        changes.forEach(function (c) {
         if ( anc.classList && anc.classList.contains( 'vector-toc-list-item-collapsed' ) ) {
            var t = norm(c.title);
          anc.classList.remove( 'vector-toc-list-item-collapsed' );
            if (SKIP_TITLE.test(t) || HIDE.indexOf(t) !== -1) return;
            var e = byTitle[t] || (byTitle[t] = { title: t, created: null, last: null, bytes: 0 });
            var ts = new Date(c.timestamp);
            if (c.type === 'new') e.created = ts;
            else e.bytes += Math.abs((c.newlen || 0) - (c.oldlen || 0));
            if (!e.last || ts > e.last) e.last = ts;
        });
         var out = [];
        Object.keys(byTitle).forEach(function (t) {
            var e = byTitle[t];
            var listed = !!catalogue[t];
            // Pages the catalogue doesn't know about still count if they look like
            // real content: a top-level work or a commentary under /Vyakhya/.
            if (!listed && t.indexOf('/') !== -1 && !/\/Vyakhya\/[^/]+$/.test(t)) return;
            if (e.created) out.push({ title: t, kind: 'new', when: e.created });
            else if (e.bytes >= UPDATE_MIN_BYTES) out.push({ title: t, kind: 'update', when: e.last });
        });
        out.forEach(function (x) { x.listed = !!catalogue[x.title]; x.label = labelFor(x.title, catalogue); x.sub = subFor(x.title, catalogue); });
        return out.sort(function (a, b) { return b.when - a.when; });
    }
    function labelFor(t, cat) {
        if (cat[t]) return cat[t];
        var parts = t.split('/');
        return parts[parts.length - 1];
    }
    // Second line: which work a commentary belongs to.
    function subFor(t, cat) {
        var m = /^([^/]+)\/Vyakhya\//.exec(t);
        if (!m) return '';
        return cat[m[1]] ? cat[m[1]] + ' — टीका' : 'टीका';
    }
 
    // Drop pages created in the window and deleted since.
    function dropMissing(items) {
        if (!items.length) return Promise.resolve(items);
        var batches = [];
        for (var i = 0; i < items.length; i += 50) batches.push(items.slice(i, i + 50));
        return Promise.all(batches.map(function (b) {
            return api({ action: 'query', prop: 'info', inprop: 'displaytitle', titles: b.map(function (x) { return x.title; }).join('|') })
                .then(function (j) {
                    var gone = {}, shown = {};
                    ((j.query && j.query.pages) || []).forEach(function (p) {
                        if (p.missing) gone[norm(p.title)] = true;
                        var dt = (p.displaytitle || '').replace(/<[^>]*>/g, '').trim();
                        if (dt && norm(dt) !== norm(p.title)) shown[norm(p.title)] = dt;
                    });
                    return b.filter(function (x) {
                        // A DISPLAYTITLE only helps pages the catalogue doesn't name.
                        if (shown[x.title] && !x.listed) x.label = shown[x.title];
                        return !gone[x.title];
                    });
                }).catch(function () { return b; });
        })).then(function (r) { return [].concat.apply([], r); });
    }
 
    function loadUpdates() {
        var cached = ssGet(CACHE_KEY);
        if (cached) {
            try {
                var c = JSON.parse(cached);
                if (Date.now() - c.at < CACHE_MINUTES * 60000) {
                    return Promise.resolve(c.items.map(function (x) { x.when = new Date(x.when); return x; }));
                }
            } catch (e) { }
         }
         }
         anc = anc.parentNode;
         return Promise.all([
      }
            fetchChanges().catch(function () { return []; }),
            fetchCatalogue().catch(function () { return null; })
        ]).then(function (r) {
            return dropMissing(summarise(r[0], r[1]).slice(0, MAX_ITEMS + 10));
        }).then(function (items) {
            items = items.slice(0, MAX_ITEMS);
            ssSet(CACHE_KEY, JSON.stringify({ at: Date.now(), items: items }));
            return items;
        });
    }
    function lastSeen() { return +lsGet(SEEN_KEY) || 0; }
    function markSeen() { lsSet(SEEN_KEY, String(Date.now())); }


      // Scroll TOC item into view within sidebar
    // ── Updates page: the automatic list ─────────────────────────
      var sticky = document.querySelector( '.vector-sticky-pinned-container' );
    //    Filled into <div id="gr-updates-feed"> on UPDATES_PAGE.
      var scrollEl = sticky || toc;
    //    All styling lives in Common.css §37.
      if ( scrollEl.scrollHeight > scrollEl.clientHeight ) {
    var KIND = { 'new': ['New', 'gr-upd-new'], 'update': ['Updated', 'gr-upd-update'] };
         var lr = li.getBoundingClientRect();
 
         var cr = scrollEl.getBoundingClientRect();
    // Today · This week · This month · then one group per month ("August 2026")
        if ( lr.top < cr.top + 8 || lr.bottom > cr.bottom - 8 ) {
    var MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
          scrollEl.scrollTop += lr.top - cr.top - scrollEl.clientHeight / 2 + li.offsetHeight / 2;
    function groupName(d, now) {
         }
        var a = new Date(d.getFullYear(), d.getMonth(), d.getDate());
      }
        var b = new Date(now.getFullYear(), now.getMonth(), now.getDate());
        var days = Math.round((b - a) / 86400000);
        if (days <= 0) return 'Today';
        if (days < 7) return 'This week';
        if (d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth()) return 'Earlier this month';
        return MONTHS_LONG[d.getMonth()] + ' ' + d.getFullYear();
    }
 
    function renderFeed(box, items, seenBefore) {
         while (box.firstChild) box.removeChild(box.firstChild);
        if (items === null) { box.appendChild(el('p', 'gr-upd-empty', 'Loading…')); return; }
        if (!items.length) { box.appendChild(el('p', 'gr-upd-empty', 'No new or updated pages in the last ' + WINDOW_DAYS + ' days.')); return; }
         var now = new Date(), group = null, list = null;
        items.forEach(function (x) {
            var g = groupName(x.when, now);
            if (g !== group) {
                group = g;
                // A div, not an h3: the site's heading rules would restyle an h3.
                var h = el('div', 'gr-upd-group', g);
                h.setAttribute('role', 'heading'); h.setAttribute('aria-level', '3');
                box.appendChild(h);
                list = el('div', 'gr-upd-list');
                list.setAttribute('role', 'list');
                box.appendChild(list);
            }
            var li = el('div', 'gr-upd-item' + (seenBefore && x.when.getTime() > seenBefore ? ' gr-upd-unseen' : ''));
            li.setAttribute('role', 'listitem');
            var main = el('div', 'gr-upd-main');
            var a = el('a', 'gr-upd-title', x.label);
            a.href = wikiHref(x.title);
            main.appendChild(a);
            var sub = el('div', 'gr-upd-sub');
            sub.appendChild(el('span', 'gr-upd-tag ' + KIND[x.kind][1], KIND[x.kind][0]));
            if (x.sub) sub.appendChild(el('span', 'gr-upd-work', x.sub));
            main.appendChild(sub);
            li.appendChild(main);
            var d = el('time', 'gr-upd-date', relDate(x.when, now));
            d.setAttribute('datetime', x.when.toISOString());
            d.title = absDate(x.when);
            li.appendChild(d);
            list.appendChild(li);
         });
     }
     }


     // Track which headings are visible
     // ── Updates page: the bulletin card ─────────────────────────
     var _visible = new Set();
    //    Items are written on the page with {{Bulletin|…}}; this only
    //    formats their dates, marks past events, and shows an empty note.
     function decorateBulletin() {
        var card = document.querySelector('.gr-bulletin');
        if (!card) return;
        var now = new Date(), today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
        var items = card.querySelectorAll('.gr-bulletin-item');
        Array.prototype.forEach.call(items, function (it) {
            var raw = it.getAttribute('data-date') || '';
            var m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw.trim());
            var d = m ? new Date(+m[1], +m[2] - 1, +m[3]) : null;
            var slot = it.querySelector('.gr-bulletin-date');
            if (d && slot) { slot.textContent = absDate(d); slot.setAttribute('title', raw); }
            // Past events stay listed, dimmed; hide them with until= on the item.
            if (d && it.getAttribute('data-type') === 'event' && d < today) it.classList.add('gr-bulletin-past');
            var until = /^(\d{4})-(\d{2})-(\d{2})$/.exec((it.getAttribute('data-until') || '').trim());
            if (until && new Date(+until[1], +until[2] - 1, +until[3]) < today) it.classList.add('gr-bulletin-expired');
        });
        var visible = card.querySelectorAll('.gr-bulletin-item:not(.gr-bulletin-expired)').length;
        card.classList.toggle('gr-bulletin-is-empty', visible === 0);
        var layout = card.closest('.gr-updates-layout');
        if (layout) layout.classList.toggle('gr-no-bulletin', visible === 0);
    }


     _headingObserver = new IntersectionObserver( function ( entries ) {
     function initUpdatesPage() {
      entries.forEach( function ( entry ) {
        var box = document.getElementById('gr-updates-feed');
         if ( entry.isIntersecting ) {
        decorateBulletin();
          _visible.add( entry.target.id );
         if (!box || box.getAttribute('data-ready')) return;
         } else {
        box.setAttribute('data-ready', '1');
          _visible.delete( entry.target.id );
        var seenBefore = lastSeen();
         }
        renderFeed(box, null);
      } );
         loadUpdates().then(function (items) {
            renderFeed(box, items, seenBefore);
            markSeen();
         }).catch(function () { renderFeed(box, []); });
    }
 
    // ── header links ─────────────────────────────────────────────
    function updatesHref() { return wikiHref(UPDATES_PAGE); }


      // Pick the topmost visible heading
    // Which wiki page a link points at ("My wiki:Help"), or null.
      var topId = null;
    function linkTarget(a) {
      var topY  = Infinity;
        var href = a.getAttribute('href') || '';
      _visible.forEach( function ( id ) {
        if (!href || href.charAt(0) === '#') return null;   // e.g. the Log out button
         var el = document.getElementById( id );
        var u;
         if ( el ) {
        try { u = new URL(href, location.href); } catch (e) { return null; }
          var y = el.getBoundingClientRect().top;
        if (u.origin !== location.origin) return null;
          if ( y >= 0 && y < topY ) { topY = y; topId = id; }
         var t = u.searchParams.get('title');
         if (!t) {
            var ap = (cfg('wgArticlePath') || '/wiki/$1').split('$1')[0];
            if (u.pathname.indexOf(ap) !== 0) return null;
            t = u.pathname.slice(ap.length);
         }
         }
      } );
        try { t = decodeURIComponent(t); } catch (e) { }
        return norm(t);
    }
    // Highlight whichever header / menu link is the page being viewed —
    // Help, About, Updates, Profile … not only the ones this block adds.
    function markCurrent() {
        var me = norm(cfg('wgPageName'));
        if (!me) return;
        var links = document.querySelectorAll('.gr-header-link, #gr-header-user-menu a, .gr-mob-item');
        Array.prototype.forEach.call(links, function (a) {
            var on = a.tagName === 'A' && linkTarget(a) === me;
            a.classList.toggle('is-current', on);
            if (on) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current');
        });
    }


      // Nothing visible: find the last heading scrolled past (above viewport)
    function injectDesktop() {
      if ( !topId ) {
        if (document.getElementById('gr-library-link')) return true;
         var bestY = -Infinity;
         var help = document.getElementById('gr-help-link');
         headings.forEach( function ( h ) {
         var host = help ? help.parentNode :
          var y = h.getBoundingClientRect().top;
            (document.querySelector('.vector-header-end') || document.querySelector('#vector-user-links') || document.querySelector('.mw-header'));
          if ( y < 0 && y > bestY ) { bestY = y; topId = h.id; }
         if (!host) return false;
         } );
      }


      setActive( topId || null );
        var lib = el('a', 'gr-header-link', LIBRARY_LABEL);
    }, {
        lib.id = 'gr-library-link';
      // Fire when heading enters/leaves the top 30% of the viewport
        lib.href = LIBRARY_URL; lib.target = '_blank'; lib.rel = 'noopener noreferrer';
      rootMargin: '-60px 0px -65% 0px',
        lib.title = 'Grantha catalogue (opens in a new tab)';
      threshold: 0
    } );


    headings.forEach( function ( h ) { _headingObserver.observe( h ); } );
        var upd = el('a', 'gr-header-link', UPDATES_LABEL);
  }
        upd.id = 'gr-updates-link';
        upd.href = updatesHref();


  // ── Init ────────────────────────────────────────────────────────
        if (help) { host.insertBefore(lib, help); host.insertBefore(upd, help); }
  function init() {
         else { host.appendChild(lib); host.appendChild(upd); }
    var HIDE_IDS = [
        markCurrent();
      'vector-appearance',
         return true;
      'vector-appearance-pinned-container',
      'vector-appearance-unpinned-container'
    ];
    function removeHiddenEls() {
      HIDE_IDS.forEach( function ( id ) {
         var el = document.getElementById( id );
        if ( el && el.parentNode ) el.parentNode.removeChild( el );
      } );
      var pt = document.getElementById( 'vector-page-tools' ) ||
              document.querySelector( '.vector-page-tools-pinned-container' );
      if ( pt ) {
         pt.querySelectorAll( '[aria-controls="vector-appearance"]' )
          .forEach( function ( el ) { if ( el.parentNode ) el.parentNode.removeChild( el ); } );
      }
     }
     }
    removeHiddenEls();


     ( function detectTeekaMode() {
     function injectMobile() {
      var tp = document.querySelector( '.gr-teeka-page' );
        if (document.getElementById('gr-mob-library')) return true;
      if ( !tp ) return;
        var menu = document.getElementById('gr-mob-menu-items');
      var primary = tp.getAttribute( 'data-primary' ) || '';
        if (!menu) return false;
      var artPath = ( window.mw && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
 
      var mainUrl = artPath.replace( '$1', primary );
        var lib = el('a', 'gr-mob-item', LIBRARY_LABEL);
      var refParam = window.location.search.match( /[?&]ref=([01])/ );
         lib.id = 'gr-mob-library';
      if ( refParam ) {
         lib.href = LIBRARY_URL; lib.target = '_blank'; lib.rel = 'noopener noreferrer';
         document.body.classList.add( refParam[1] === '1' ? 'gr-ref-mode' : 'gr-standalone' );
 
         return;
         var upd = el('a', 'gr-mob-item', UPDATES_LABEL);
      }
        upd.id = 'gr-mob-updates';
      var ref = document.referrer || '';
        upd.href = updatesHref();
      document.body.classList.add(
         ( ref && primary && ref.indexOf( mainUrl ) !== -1 ) ? 'gr-ref-mode' : 'gr-standalone'
      );
    }() );


    if ( window.MutationObserver ) {
        var home = null;
      var hideObs = new MutationObserver( function ( mutations ) {
         for (var i = 0; i < menu.children.length; i++) {
         var dirty = false;
            if (/^\s*Home\s*$/.test(menu.children[i].textContent)) { home = menu.children[i]; break; }
        mutations.forEach( function ( m ) { if ( m.addedNodes.length ) dirty = true; } );
        }
         if ( dirty ) removeHiddenEls();
        var ref = home ? home.nextSibling : menu.firstChild;
      } );
         menu.insertBefore(lib, ref);
      hideObs.observe( document.body, { childList: true, subtree: false } );
        menu.insertBefore(upd, ref);
      setTimeout( function () { hideObs.disconnect(); }, 6000 );
        markCurrent();
        return true;
     }
     }


     var content      = document.querySelector( '.mw-parser-output' );
     // ── boot: the Help/About and mobile-menu code creates the anchors on
    var alreadyTagged = content && content.querySelector( '[data-deva]' );
    //    DOMContentLoaded, so wait briefly for them rather than assume order.
    if ( !alreadyTagged ) {
    function boot() {
      translatableSpans = [];
        initUpdatesPage();
      tagTextNodes();
        var inject = isMobile ? injectMobile : injectDesktop;
    } else {
        if (inject()) return;
      document.querySelectorAll( '.vector-toc .vector-toc-text:not([data-deva])' ).forEach( function ( span ) {
        var tries = 0;
        var orig = span.textContent;
        var obs = window.MutationObserver ? new MutationObserver(function () { if (inject()) obs.disconnect(); }) : null;
        if ( !orig.trim() ) return;
        if (obs) obs.observe(document.body, { childList: true, subtree: true });
        span.setAttribute( 'data-deva', orig );
        (function poll() {
        translatableSpans.push( span );
            if (inject()) { if (obs) obs.disconnect(); return; }
      } );
            if (++tries < 40) setTimeout(poll, 250); else if (obs) obs.disconnect();
        }());
     }
     }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
    else boot();
    // Page previews and live-preview re-render content; re-run the page part then.
    if (window.mw && mw.hook) mw.hook('wikipage.content').add(function () { initUpdatesPage(); });


     var saved = ( function () {
     window.grUpdates = { relDate: relDate, summarise: summarise, groupName: groupName };
      try { return localStorage.getItem( LS_SCRIPT_KEY ); } catch ( e ) { return null; }
}());
    }() );
    if ( saved && saved !== 'deva' ) { applyScript( saved ); }
    else { currentScript = 'deva'; }


    // Vector 2022 defers TOC render — retry at 300ms and 800ms
    // Use a single short delay so Vector has rendered the TOC
    setTimeout( setupToc, 200 );
  }


   // ── React to toolbar script-change events ──────────────────────
/* ═══════════════════════════════════════════════════════════════
  window.addEventListener( 'gr-script-change', function ( e ) {
  Main Page cards: item count beside each title, and a
    var script = e && e.detail && e.detail.script;
  .gr-home-card-wide class on long lists. Styling: Common.css §25.
     if ( script ) applyScript( script );
  ═══════════════════════════════════════════════════════════════ */
  } );
(function () {
    'use strict';
    var WIDE_AT = 14;   // cards with this many items or more span two columns
    function decorate() {
        var cards = document.querySelectorAll('.gr-home .gr-home-card');
        Array.prototype.forEach.call(cards, function (card) {
            var title = card.querySelector('.gr-home-card-title');
            var n = card.querySelectorAll('li').length;
            if (!n) return;
            if (n >= WIDE_AT) card.classList.add('gr-home-card-wide');
            if (!title || title.querySelector('.gr-home-card-count')) return;
            var label = document.createElement('span');
            while (title.firstChild) label.appendChild(title.firstChild);
            title.appendChild(label);
            var c = document.createElement('span');
            c.className = 'gr-home-card-count';
            c.textContent = String(n);
            title.appendChild(c);
        });
    }
     if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', decorate);
    else decorate();
}());


  // ── React to gr-new-content (siteNav panel rendered new items) ──
  // Tag any new text nodes added by the documents panel
  window.addEventListener( 'gr-new-content', function ( e ) {
    var container = e && e.detail && e.detail.container;
    if ( !container ) return;
    var walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT );
    var nodes = [];
    while ( walker.nextNode() ) nodes.push( walker.currentNode );
    nodes.forEach( function ( node ) {
      var p = node.parentNode;
      if ( !p || ( p.hasAttribute && p.hasAttribute( 'data-deva' ) ) ) return;
      var orig = node.textContent;
      if ( !orig.trim() ) return;
      var span = document.createElement( 'span' );
      span.setAttribute( 'data-deva', orig );
      span.textContent = currentScript !== 'deva' ? transliterateText( orig, currentScript ) : orig;
      p.replaceChild( span, node );
      translatableSpans.push( span );
    } );
  } );


  // ── React to script changes from OTHER tabs (BroadcastChannel) ──
// ── Footnote markers and their cards ────────────────────────────────
  try {
/* ── FOOTNOTE REGISTRY ────────────────────────────────────────────────
     var _grBC = new BroadcastChannel( 'gr-script' );
  Notes that belong to content injected into this page rather than baked
    _grBC.onmessage = function ( e ) {
  into it - an interleaved commentary's own footnotes live in ITS
      var script = e && e.data && e.data.script;
  document, so nothing on the moola page knew what its highlighted words
      if ( script ) {
  pointed at. Anything registered here is visible to the note card and to
        currentScript = script;
  the right-hand panel, grouped under the commentary it came from. */
        var sel = document.querySelector( '.gr-script-sel' );
window.grFootnoteRegistry = (function () {
         if ( sel ) sel.value = script;
    'use strict';
         applyScript( script );
    var byId = {};            // gra-id -> {text, quote, type, group, groupTitle}
      }
    var groups = [];          // [{slug, title}] in the order they were added
    var listeners = [];
     var pending = null;
    // Deferred by a tick on purpose. Notes are registered while a
    // commentary's block is still being built, BEFORE it is inserted into
    // the page - so a listener that re-scans the document synchronously
    // would run against a DOM that does not contain the highlighted words
    // yet, and find nothing to wire up. Coalescing to one call after the
    // current task also means a page with ten commentaries re-scans once
    // rather than ten times.
    function notify() {
        if (pending) return;
        pending = setTimeout(function () {
            pending = null;
            listeners.forEach(function (f) { try { f(); } catch (e) {} });
        }, 0);
    }
    return {
        add: function (slug, title, notes) {
            if (!groups.some(function (g) { return g.slug === slug; })) {
                groups.push({ slug: slug, title: title });
            }
            var added = false;
            (notes || []).forEach(function (n) {
                if (!n || !n.id || byId[n.id]) return;
                byId[n.id] = {
                    text: n.text || '', quote: n.quote || '',
                    type: n.kind || n.type || 'note',
                    group: slug, groupTitle: title
                };
                added = true;
            });
            if (added) notify();
        },
        get: function (id) { return byId[id] || null; },
        all: function () { return byId; },
         groups: function () { return groups.slice(); },
        isInjected: function (id) { return !!byId[id]; },
        clearInjected: function () { byId = {}; groups = []; notify(); },
         onChange: function (f) { listeners.push(f); }
     };
     };
  } catch ( e ) {}
}());


  // ── MediaWiki SPA-style navigation ──────────────────────────────
/* QuickEdit already stores footnotes: it wraps the annotated words in
  if ( window.mw ) {
  <span class="gra-qe-footnote-highlight" data-gra-id="…"> and appends a
    mw.hook( 'wikipage.content' ).add( function () {
  <div class="gra-qe-footnotes"><ol> at the foot of the document, one
      setTimeout( function () {
  <li data-gra-id="…"> per note carrying the quoted words and the note.
        var content      = document.querySelector( '.mw-parser-output' );
 
        var alreadyTagged = content && content.querySelector( '[data-deva]' );
  That is the whole apparatus, but reading it meant scrolling to the end of
        if ( !alreadyTagged ) {
  a long document and scrolling back. This turns the same data into what
          translatableSpans = [];
  Setutila shows: a small superscript after the annotated words, and a card
          tagTextNodes();
  at the point of reading. Nothing is stored differently and QuickEdit is
        } else {
  untouched — the list is still written, still saved, and still printed; it
          document.querySelectorAll( '.vector-toc .vector-toc-text:not([data-deva])' ).forEach( function ( span ) {
  is only taken off the screen, where the markers replace it.
            var orig = span.textContent;
            if ( !orig.trim() ) return;
            span.setAttribute( 'data-deva', orig );
            translatableSpans.push( span );
          } );
        }
        if ( currentScript !== 'deva' ) applyScript( currentScript );
        setupToc();
      }, 150 );
    } );
  }


  if ( document.readyState === 'loading' ) {
  The note text is transliterated with the rest of the page: the card's body
    document.addEventListener( 'DOMContentLoaded', init );
  is tagged data-deva and announced through gr-new-content, which the script
  } else {
  switcher already listens for. */
     init();
(function () {
  }
     'use strict';


}() );  /* ← end of main IIFE */
    var HL = '.gra-qe-footnote-highlight';
    var LIST = '.gra-qe-footnotes';
    // data-gra-type on the <li>. The names the converter writes, and the
    // ones a QuickEdit button could write later, land in the same map.
    var TYPES = {
        pathantara: 'पाठान्तरम्',   // a variant reading
        locus: 'प्रमाणम्',          // where a quotation is from — ऋ.सं.७.९९
        source: 'प्रमाणम्',
        note: 'टिप्पणी'            // an editorial remark
    };


    var card = null, openFor = null;


// ── Inject "Help" and "About" links into the header ─────────────────
    function notesFromList() {
( function () {
        var out = {};
  function wikiHref( title ) {
        var reg = window.grFootnoteRegistry.all();
    if ( window.mw && mw.util && mw.util.getUrl ) return mw.util.getUrl( title );
        Object.keys(reg).forEach(function (id) {
    var ap = ( window.mw && mw.config && mw.config.get( 'wgArticlePath' ) ) || '/wiki/$1';
            out[id] = { text: reg[id].text, quote: reg[id].quote,
    return ap.replace( '$1', title );
                        type: reg[id].type, injected: true,
  }
                        groupTitle: reg[id].groupTitle };
        });
        document.querySelectorAll(LIST + ' .gra-qe-footnote-item').forEach(function (li) {
            var id = li.getAttribute('data-gra-id');
            if (!id) return;
            // An injected note is published into this same list so the
            // right-hand panel can see it, which means the list entry gets
            // read back here. Keep the registry's own record for those, or
            // the entry overwrites it, the `injected` flag is lost, and the
            // note is numbered as though it belonged to this document.
            if (out[id] && out[id].injected) return;
            var textEl = li.querySelector('.gra-qe-footnote-text');
            out[id] = {
                text: (textEl ? textEl.textContent : li.textContent).trim(),
                quote: (li.querySelector('.gra-qe-footnote-quote') || {}).textContent || '',
                type: li.getAttribute('data-gra-type') || 'note'
            };
        });
        return out;
    }


  var linkStyle = [
    function buildCard() {
    'color:rgba(255,255,255,0.88)', 'font-size:0.88em',
        if (card) return card;
    'font-family:system-ui,sans-serif', 'font-weight:500',
        card = document.createElement('div');
    'text-decoration:none', 'padding:4px 10px', 'border-radius:4px',
        card.className = 'gr-note-card';
    'margin-right:4px', 'transition:color 0.15s,background 0.15s',
        card.setAttribute('role', 'dialog');
    'white-space:nowrap',
        card.hidden = true;
  ].join( ';' );
        card.innerHTML =
            '<div class="gr-note-card-head">' +
            '<span class="gr-note-card-kind"></span>' +
            '<button type="button" class="gr-note-card-close" aria-label="Close">×</button>' +
            '</div><div class="gr-note-card-body"></div>';
        card.querySelector('.gr-note-card-close').addEventListener('click', close);
        card.addEventListener('mouseenter', function () { clearTimeout(hoverTimer); });
        card.addEventListener('mouseleave', function () { if (!COARSE) hoverClose(); });
        document.body.appendChild(card);
        return card;
    }


  function makeHeaderLink( id, href, label ) {
    function close() {
    var a = document.createElement( 'a' );
        if (!card || card.hidden) return;
    a.id = id; a.href = href; a.textContent = label;
        card.hidden = true;
    a.style.cssText = linkStyle;
        if (openFor) {
    a.addEventListener( 'mouseover', function () {
            openFor.setAttribute('aria-expanded', 'false');
      this.style.color = '#fff'; this.style.background = 'rgba(255,255,255,0.12)';
            openFor.classList.remove('gr-note-marker-open');
    } );
            openFor.classList.remove('gr-note-hl-open');
    a.addEventListener( 'mouseout', function () {
        }
      this.style.color = 'rgba(255,255,255,0.88)'; this.style.background = 'transparent';
        openFor = null;
    } );
     }
     return a;
  }


  function injectHeaderLinks() {
    // Anchored under the marker, nudged back inside the viewport rather than
    if ( document.getElementById( 'gr-about-link' ) ) return;
    // flipped: these sit in running text, and a card that jumps above the line
    // is harder to connect to the word it belongs to than one that shifts left.
    function place(marker) {
        var r = marker.getBoundingClientRect();
        var w = card.offsetWidth, h = card.offsetHeight;
        var pad = 10;
        var left = r.left + r.width / 2 - w / 2;
        left = Math.max(pad, Math.min(left, window.innerWidth - w - pad));
        var top = r.bottom + 8;
        if (top + h > window.innerHeight - pad && r.top - h - 8 > pad) top = r.top - h - 8;
        card.style.left = Math.round(left) + 'px';
        card.style.top = Math.round(top) + 'px';
    }


     var headerEnd = document.querySelector( '.vector-header-end' ) ||
     function open(marker, note, num) {
                    document.querySelector( '#vector-user-links' ) ||
        buildCard();
                    document.querySelector( '.mw-header' );
        if (openFor === marker && !card.hidden) { close(); return; }
    if ( !headerEnd ) return;
        close();
        card.querySelector('.gr-note-card-kind').textContent =
            (TYPES[note.type] || TYPES.note) +
            (typeof num === 'number' ? '  [' + num + ']' : (num ? '  \u00b7  ' + num : ''));
        var body = card.querySelector('.gr-note-card-body');
        // A bare text node, deliberately. gr-new-content's handler wraps it,
        // records it as translatable and applies whatever script is showing —
        // tagging it here instead would make the handler skip it as already
        // done, and the note would sit in Devanagari on a Telugu page.
        body.textContent = note.text;
        card.hidden = false;
        place(marker);
        try {
            window.dispatchEvent(new CustomEvent('gr-new-content', { detail: { container: body } }));
        } catch (e) { }
        marker.setAttribute('aria-expanded', 'true');
        // A commentary's note has no superscript, so the element the card is
        // anchored to IS the highlighted word. gr-note-marker-open paints a
        // solid maroon chip with white text - right for a small superscript,
        // and it made the word itself unreadable. The word gets its own,
        // quieter open state instead.
        marker.classList.add(marker.classList.contains('gr-note-marker')
            ? 'gr-note-marker-open' : 'gr-note-hl-open');
        openFor = marker;
    }


     var helpLink  = makeHeaderLink( 'gr-help-link', wikiHref( 'My_wiki:Help'  ), 'Help'  );
     // A coarse pointer has no hover, so there the card opens on tap. With a
     var aboutLink = makeHeaderLink( 'gr-about-link', wikiHref( 'My_wiki:About' ), 'About' );
    // mouse, hover shows the note and a click is left free for the reader's
    // own panel - which is what the marker is for on desktop.
     var COARSE = (window.matchMedia && window.matchMedia('(hover: none), (pointer: coarse)').matches)
        || ('ontouchstart' in window) || navigator.maxTouchPoints > 0;


     var ul = document.querySelector( '.vector-user-links' ) ||
     var hoverTimer = null;
            document.querySelector( '#pt-userpage' );
    function hoverOpen(m, note, num) {
    if ( ul && ul.parentNode === headerEnd ) {
        clearTimeout(hoverTimer);
      headerEnd.insertBefore( aboutLink, ul );
        hoverTimer = setTimeout(function () { open(m, note, num); }, 90);
      headerEnd.insertBefore( helpLink, aboutLink );
     }
     } else {
    function hoverClose() {
      headerEnd.appendChild( helpLink );
        clearTimeout(hoverTimer);
      headerEnd.appendChild( aboutLink );
        hoverTimer = setTimeout(function () {
            if (card && !card.matches(':hover')) close();
        }, 220);
     }
     }
  }


  if ( document.readyState === 'loading' ) document.addEventListener( 'DOMContentLoaded', injectHeaderLinks );
    function decorate() {
  else injectHeaderLinks();
        var notes = notesFromList();
}() );
        var n = 0;
        document.querySelectorAll('#mw-content-text ' + HL).forEach(function (hl) {
            var id = hl.getAttribute('data-gra-id');
            var note = id && notes[id];
            if (!note || !note.text) return;         // orphan highlight: leave it alone


            // An interleaved commentary's notes get NO superscript. The
            // numbers would restart per commentary and interleave with the
            // moola's own, so "[3]" on the page would mean three different
            // notes depending on which block it sat in. The highlighted
            // words stay clickable, and the panel lists them by commentary.
            var injected = !!note.injected;
            var num = injected ? null : ++n;


// ── Main page: by-Grantha / by-Author toggle ──────────────────────
            var m = null;
( function () {
            if (!injected) {
  function grHomeView( v ) {
                if (hl.nextElementSibling &&
    var gView = document.getElementById( 'gr-view-grantha' );
                    hl.nextElementSibling.classList.contains('gr-note-marker')) {
    var aView = document.getElementById( 'gr-view-author' );
                    m = hl.nextElementSibling;
    var gBtn  = document.getElementById( 'gr-toggle-grantha' );
                    m.textContent = '[' + num + ']';
    var aBtn  = document.getElementById( 'gr-toggle-author' );
                } else {
    if ( !gView || !aView || !gBtn || !aBtn ) return;
                    m = document.createElement('sup');
    gView.style.display = ( v === 'grantha' ) ? '' : 'none';
                    m.className = 'gr-note-marker';
    aView.style.display = ( v === 'author' ) ? '' : 'none';
                    m.textContent = '[' + num + ']';
    gBtn.className = 'gr-toggle-btn' + ( v === 'grantha' ? ' gr-toggle-active' : '' );
                    m.setAttribute('role', 'button');
    aBtn.className = 'gr-toggle-btn' + ( v === 'author'  ? ' gr-toggle-active' : '' );
                    m.setAttribute('tabindex', '0');
    try { localStorage.setItem( 'gr_home_view', v ); } catch ( e ) {}
                    m.setAttribute('aria-expanded', 'false');
  }
                    hl.parentNode.insertBefore(m, hl.nextSibling);
                }
            }
            if (hl.dataset.grNoteWired) return;
            hl.dataset.grNoteWired = '1';


  function initHomeToggle() {
            var anchorEl = m || hl;
    var gBtn = document.getElementById( 'gr-toggle-grantha' );
            var label = injected ? (note.groupTitle || '') : num;
    var aBtn = document.getElementById( 'gr-toggle-author' );
    if ( !gBtn || !aBtn ) return;
    gBtn.addEventListener( 'click', function () { grHomeView( 'grantha' ); } );
    aBtn.addEventListener( 'click', function () { grHomeView( 'author' );  } );
    [ gBtn, aBtn ].forEach( function ( btn ) {
      btn.addEventListener( 'keydown', function ( e ) {
        if ( e.key === 'Enter' || e.key === ' ' ) btn.click();
      } );
    } );
    var saved;
    try { saved = localStorage.getItem( 'gr_home_view' ); } catch ( e ) {}
    if ( saved === 'author' ) grHomeView( 'author' );
  }


  if ( document.readyState === 'loading' ) document.addEventListener( 'DOMContentLoaded', initHomeToggle );
            function show(e) { e.preventDefault(); e.stopPropagation(); open(anchorEl, note, label); }
  else initHomeToggle();
}() );


            if (COARSE) {
                if (m) m.addEventListener('click', show);
                hl.addEventListener('click', show);
            } else {
                // Hover opens the card; the click is NOT swallowed, so the
                // annotation gadget still gets it and opens its Footnotes
                // panel - the behaviour that was lost when this handler
                // called stopPropagation on every click.
                [m, hl].forEach(function (el) {
                    if (!el) return;
                    el.addEventListener('mouseenter', function () { hoverOpen(anchorEl, note, label); });
                    el.addEventListener('mouseleave', hoverClose);
                });
                if (m) m.addEventListener('click', show);
            }
            if (m) {
                m.addEventListener('keydown', function (e) {
                    if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); open(m, note, label); }
                });
            }
        });
        var list = document.querySelector(LIST);
        if (list) list.classList.toggle('gr-notes-listed', n > 0);
    }


// ── Ullekha reference link handler ─────────────────────────────────
    document.addEventListener('click', function (e) {
( function () {
        if (!card || card.hidden) return;
        if (!card.contains(e.target)) close();
    });
    document.addEventListener('keydown', function (e) { if (e.key === 'Escape') close(); });
    window.addEventListener('resize', function () { if (openFor) place(openFor); });
    window.addEventListener('scroll', function () { if (openFor) place(openFor); }, true);


  function highlightOnArrival() {
    // Commentaries are fetched and injected after the page has rendered, so
    var search = window.location.search;
    // their notes arrive later than this module's first pass. Re-run when
    if ( !search ) return;
    // the registry changes, and publish them into the page's own footnote
    var m = search.match( /[?&]hlUllekha=([^&]+)/ );
    // list so the right-hand panel - which reads that list, not this
    if ( !m ) return;
    // module - shows them too, under the commentary each came from.
    var needle;
    function publishInjectedList() {
    try { needle = decodeURIComponent( m[ 1 ] ); } catch ( e ) { return; }
        var host = document.querySelector(LIST);
    if ( !needle || needle.length < 4 ) return;
        if (!host) {
            var content = document.getElementById('mw-content-text');
            if (!content) return;
            host = document.createElement('div');
            host.className = 'gra-qe-footnotes';
            content.appendChild(host);
        }
        host.querySelectorAll('.gr-teeka-notes-group').forEach(function (g) { g.remove(); });
        var reg = window.grFootnoteRegistry.all();
        window.grFootnoteRegistry.groups().forEach(function (g) {
            var ids = Object.keys(reg).filter(function (id) { return reg[id].group === g.slug; });
            if (!ids.length) return;
            var wrap = document.createElement('div');
            wrap.className = 'gr-teeka-notes-group';
            wrap.setAttribute('data-teeka-slug', g.slug);
            var h = document.createElement('div');
            h.className = 'gr-teeka-notes-title';
            h.textContent = (window.grLibrary && window.grLibrary.labelFor(g.slug))
                            || g.title || g.slug;
            wrap.appendChild(h);
            var ol = document.createElement('ol');
            ids.forEach(function (id) {
                var n = reg[id];
                var li = document.createElement('li');
                li.className = 'gra-qe-footnote-item';
                li.setAttribute('data-gra-id', id);
                li.setAttribute('data-gra-type', n.type);
                if (n.quote) {
                    var q = document.createElement('span');
                    q.className = 'gra-qe-footnote-quote';
                    q.textContent = n.quote;
                    li.appendChild(q);
                }
                var t = document.createElement('span');
                t.className = 'gra-qe-footnote-text';
                t.textContent = n.text;
                li.appendChild(t);
                ol.appendChild(li);
            });
            wrap.appendChild(ol);
            host.appendChild(wrap);
        });
        try {
            window.dispatchEvent(new CustomEvent('gr-new-content', { detail: { container: host } }));
        } catch (e) { }
    }


     // Wait for tagTextNodes() to finish wrapping text in data-deva spans,
     window.grFootnoteRegistry.onChange(function () {
    // then search those spans rather than raw text nodes.
        publishInjectedList();
    function doHighlight() {
        decorate();
      var content = document.querySelector( '.mw-parser-output' );
    });
      if ( !content ) return;


      var snippet = needle.slice( 0, 40 );
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', decorate);
      var found  = false;
    else decorate();
    if (window.mw && mw.hook) mw.hook('wikipage.content').add(function () { setTimeout(decorate, 150); });
}());


      // Search data-deva spans first (post-tagTextNodes state)
/* ── HASH REVEAL ───────────────────────────────────────────────────────
      var spans = content.querySelectorAll( '[data-deva]' );
  A copied id or a bookmark citation is only useful if its #link actually
      for ( var i = 0; i < spans.length && !found; i++ ) {
  lands on the block. The browser's own fragment scroll fires once, at
        var spanEl = spans[ i ];
  parse time, and then our fonts swap in and the teeka blocks inject -
        var orig  = spanEl.getAttribute( 'data-deva' ) || '';
  the page grows under the reader and the target drifts off screen. It
        if ( orig.indexOf( snippet ) === -1 ) continue;
  looks exactly like the link didn't work.


        // Replace the span with: text-before + <mark> + text-after
  This retries until the element exists and re-settles after late content
        var idx   = orig.indexOf( snippet );
   arrives. It used to live in copyId.js (Editor extension), which is no
        var hlText = orig.slice( idx, Math.min( idx + needle.length, orig.length ) );
  longer loaded on this wiki, so it moves here where it always runs. If
        var mark  = document.createElement( 'mark' );
  copyId IS loaded it does the same job; both are idempotent, and the
        mark.className  = 'gr-ullekha-highlight';
  guard below keeps them from fighting over the same jump. */
        mark.style.cssText = 'background:#fff176;border-radius:2px;padding:0 2px;';
(function () {
        mark.textContent  = hlText;
    'use strict';
    if (window.grHashReveal) return;


        var parent = spanEl.parentNode;
    var FLASH_MS = 2600;
        if ( !parent ) continue;
    var _last = null;


         var before = document.createTextNode( orig.slice( 0, idx ) );
    function findTarget(id) {
         var after  = document.createTextNode( orig.slice( idx + hlText.length ) );
         if (!id) return null;
        parent.insertBefore( before, spanEl );
         var esc = (window.CSS && CSS.escape) ? CSS.escape(id) : id.replace(/["\\]/g, '\\$&');
         parent.insertBefore( mark,  spanEl );
         return document.getElementById(id) ||
        parent.insertBefore( after,  spanEl );
              document.querySelector('[data-block-id="' + esc + '"]') ||
        parent.removeChild( spanEl );
              document.querySelector('[data-copyid="' + esc + '"]') ||
              null;
    }


        setTimeout( function () {
    function currentHash() {
          mark.scrollIntoView( { behavior: 'smooth', block: 'center' } );
        var raw = (location.hash || '').slice(1);
         }, 100 );
         if (!raw) return '';
         found = true;
         try { return decodeURIComponent(raw); } catch (e) { return raw; }
      }
    }


      // Fallback: raw text nodes (before tagTextNodes runs)
    function land(el, flash) {
      if ( !found ) {
         el.scrollIntoView({ block: 'center', behavior: 'smooth' });
         var walker = document.createTreeWalker( content, NodeFilter.SHOW_TEXT );
         if (!flash) return;
         while ( walker.nextNode() && !found ) {
        el.classList.add('gid-target');
          var node = walker.currentNode;
        setTimeout(function () { el.classList.remove('gid-target'); }, FLASH_MS);
          var txt  = node.textContent || '';
    }
          if ( txt.indexOf( snippet ) === -1 ) continue;
 
          var idx2  = txt.indexOf( snippet );
    function reveal(attemptsLeft, flash) {
          var mark2  = document.createElement( 'mark' );
        var id = currentHash();
          mark2.className    = 'gr-ullekha-highlight';
        if (!id) return;
          mark2.style.cssText = 'background:#fff176;border-radius:2px;padding:0 2px;';
        var el = findTarget(id);
          mark2.textContent  = txt.slice( idx2, Math.min( idx2 + needle.length, txt.length ) );
        if (!el) {
          var p = node.parentNode;
            if (attemptsLeft > 0) setTimeout(function () { reveal(attemptsLeft - 1, flash); }, 400);
          p.insertBefore( document.createTextNode( txt.slice( 0, idx2 ) ), node );
            return;
          p.insertBefore( mark2, node );
          p.insertBefore( document.createTextNode( txt.slice( idx2 + mark2.textContent.length ) ), node );
          p.removeChild( node );
          setTimeout( function () {
            mark2.scrollIntoView( { behavior: 'smooth', block: 'center' } );
          }, 100 );
          found = true;
         }
         }
      }
        _last = id;
        land(el, flash);
        // Re-settle once the late arrivals (fonts, injected teekas) have
        // shifted the layout. Cheap, and it is what makes the link feel
        // like it worked rather than like it missed.
        setTimeout(function () {
            if (currentHash() !== id) return;
            var again = findTarget(id);
            if (again) again.scrollIntoView({ block: 'center', behavior: 'auto' });
        }, 1200);
     }
     }


     // Delay to let tagTextNodes() and MW rendering finish
     function onLateContent() {
    setTimeout( doHighlight, 600 );
        var id = currentHash();
  }
        if (!id || id !== _last) return;
        var el = findTarget(id);
        if (el) el.scrollIntoView({ block: 'center', behavior: 'auto' });
    }


  function wireUllekhaLinks() {
     window.grHashReveal = function (attempts) { reveal(attempts || 12, true); };
     document.querySelectorAll( '.gr-ullekha-ref-link' ).forEach( function ( wrap ) {
      var anchor = wrap.getAttribute( 'data-anchor' ) || '';
      var hl    = wrap.getAttribute( 'data-hl' )    || '';
      var a = wrap.querySelector( 'a' );
      if ( !a ) return;


      var base = a.href.split( '#' )[ 0 ];
    function boot() {
      var encoded = encodeURIComponent( hl );
        reveal(12, true);
      a.href = base + ( hl ? '?hlUllekha=' + encoded : '' )
        window.addEventListener('hashchange', function () { reveal(6, true); });
                    + ( anchor ? '#' + anchor : '' );
        window.addEventListener('gr-new-content', onLateContent);
     } );
        if (document.fonts && document.fonts.ready && document.fonts.ready.then) {
  }
            document.fonts.ready.then(onLateContent);
        }
    }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
     else boot();
}());


  if ( document.readyState === 'loading' ) {
/* ── GRANTHA LIBRARY ───────────────────────────────────────────────────
    document.addEventListener( 'DOMContentLoaded', function () {
  The colophon in the source docx says "श्रीमद्वादिराजपूज्यचरणविरचिता
      highlightOnArrival();
  प्रकाशिका" - an author phrase welded to a generic label. That is not a
      wireUllekhaLinks();
  citable title, and it is what the client saw as "Vadirajathirtha
    } );
  virachitha". The Grantha library holds the real title and author, so
  } else {
  the page carries only the library's stable id (data-grantha-id) and we
    highlightOnArrival();
  resolve the rest here, at read time. Correct a name in the library and
    wireUllekhaLinks();
  every citation on the site is correct on the next load - nothing to
  }
  rebuild, nothing to re-upload.
}() );
/* ── Mobile TOC overlay — add to bottom of Common.js ─────────── */
( function () {
  if ( window.innerWidth > 768 ) return;


  function initMobileToc() {
  Reads only, and the library's GET endpoints are public: no API key is
     if ( document.getElementById( 'gr-mob-toc-btn' ) ) return;
  sent from the browser, and none may ever be put in this file, which
  any reader can fetch. */
(function () {
    'use strict';
     if (window.grLibrary) return;


     var toc = document.querySelector( '.vector-toc' );
    // ---- CONFIGURE ME -------------------------------------------------
     if ( !toc ) return;
    // Where the Grantha library's REST API lives. Same-origin by default,
    // which needs no CORS - but the library is its own app, so unless it is
    // reverse-proxied under the wiki's domain this must be its real host:
    //     var API_BASE = 'https://library.example.org/api/granthas';
    // On another host the library must send
    //    Access-Control-Allow-Origin: https://anandamakaranda.in
    // or the browser will refuse the reply and every citation will fall
    // back to the author baked into the page (usually none).
    var API_BASE = window.GR_LIBRARY_API || '/api/granthas';
    // Set to false while the library is not yet reachable: no request is
    // made at all, and citations use whatever the page itself carries.
    // Flip it back to true once API_BASE resolves.
    var LIBRARY_ENABLED = true;
    // -------------------------------------------------------------------
    var API = API_BASE.replace(/\/+$/, '');
     var mem = {};
    var warned = false;
    // Once the library has answered "not here", every further call will get
    // the same answer. Stop asking: a wrong or missing API base otherwise
    // fills the network log with one 404 per work on the page.
    var libraryDown = !LIBRARY_ENABLED;


     /* Backdrop */
     // In memory only, for the life of the page. Names used to be kept in
     var backdrop = document.createElement( 'div' );
    // sessionStorage for six hours, which quietly outlived any correction
     backdrop.className = 'gr-mobile-toc-backdrop';
     // made in the library or on the map page. They now come from the map
     backdrop.id = 'gr-mob-toc-backdrop';
    // page, which is read once per load, so there is nothing to gain from
     document.body.appendChild( backdrop );
     // a second cache that can disagree with it.
     function cacheGet(id) { return mem[id] || null; }
     function cacheSet(id, v) { mem[id] = v; }


     /* Panel */
     // The library returns camelCase fields; normalise to what the citation
     var panel = document.createElement( 'div' );
     // builder wants and keep BOTH scripts, since a Sanskrit reference is
    panel.className = 'gr-mobile-toc-panel';
    // cited in Devanagari and an English-language one in IAST.
    panel.id = 'gr-mob-toc-panel';
    function shape(g) {
        if (!g) return null;
        return {
            id:          g.id || '',
            name:        g.name || '',
            nameIAST:    g.transliteration || '',
            author:      g.author || '',
            authorIAST:  g.authorTransliteration || g.author_transliteration || '',
            category:    g.category || '',
            prasthana:  g.prasthana || ''
        };
    }


     /* Panel header */
     var inflight = {};
     var header = document.createElement( 'div' );
     function lookup(id) {
    header.className = 'gr-mobile-toc-header';
        if (!id) return Promise.resolve(null);
    var titleEl = document.createElement( 'div' );
        var hit = cacheGet(id);
    titleEl.className = 'gr-mobile-toc-title';
        if (hit) return Promise.resolve(hit);
    titleEl.textContent = 'विषयसूची';
        // The map page carries the names for every work this wiki uses, so
    var closeBtn = document.createElement( 'button' );
        // normally nothing is fetched from the library at all. That matters
    closeBtn.className = 'gr-mobile-toc-close';
        // because the library is someone else's server: we cannot add CORS
    closeBtn.innerHTML = '✕';
        // headers to it, and a cross-origin call we cannot configure is a
    header.appendChild( titleEl );
        // call that will not work. The live API stays as an enhancement for
    header.appendChild( closeBtn );
        // whenever it IS reachable.
    panel.appendChild( header );
        return granthaMap().then(function (m) {
            var w = (m.works || {})[id];
            if (w && (w.name || w.nameIAST)) {
                var v = { id: id, name: w.name || '', nameIAST: w.nameIAST || '',
                          author: w.author || '', authorIAST: w.authorIAST || '',
                          category: w.category || '', prasthana: w.prasthana || '' };
                cacheSet(id, v);
                return v;
            }
            return fetchFromLibrary(id);
        });
    }


     /* Clone TOC contents */
     function fetchFromLibrary(id) {
    var tocContents = toc.querySelector( '.vector-toc-contents' );
        if (libraryDown) return Promise.resolve(null);
    if ( tocContents ) {
        if (inflight[id]) return inflight[id];
      var clone = tocContents.cloneNode( true );
        inflight[id] = fetch(API + '/' + encodeURIComponent(id), {
      panel.appendChild( clone );
            credentials: 'omit', headers: { 'Accept': 'application/json' }
        }).then(function (r) {
            // A 404 or 5xx means the base URL is wrong or the service is
            // down - not that this one work is missing - so trip the breaker.
            if (!r.ok) {
                if (r.status === 404 || r.status >= 500) libraryDown = true;
                throw new Error('library ' + r.status);
            }
            return r.json();
        }).then(function (j) {
            var v = shape(j && (j.grantha || j.data || j));
            if (v && v.name) cacheSet(id, v);
            delete inflight[id];
            return v;
        }).catch(function (err) {
            // Offline, blocked by CORS, wrong host, or the id is not in the
            // library yet. The page's own data-author/data-doc-title stand
            // in, so a citation is never blocked on this request - but an
            // unreachable library looks exactly like a work with no author,
            // so say which it is, once.
            delete inflight[id];
            libraryDown = true;
            if (!warned) {
                warned = true;
                try {
                    console.warn('[grantha] could not read the library at ' + API +
                        ' (asked for "' + id + '"): ' + (err && err.message || err) +
                        '. Citations will use whatever the page itself carries. ' +
                        'Set window.GR_LIBRARY_API to the library host, and allow ' +
                        'this origin via Access-Control-Allow-Origin.');
                } catch (e) { }
            }
            return null;
        });
        return inflight[id];
     }
     }


     document.body.appendChild( panel );
     // ── WHICH GRANTHA IS THIS PAGE? ────────────────────────────────────
    // A page can say so itself (data-grantha-id, written by the pipeline),
    // but that would mean re-importing every document just to add one
    // attribute - and a re-import risks losing edits made on the site
    // since. So the mapping also lives in ONE wiki page, keyed by page
    // name, and the page looks itself up. Adding a document to the
    // library is then a single edit to that one page, with no content
    // touched anywhere.
    var MAP_PAGE = window.GR_GRANTHA_MAP_PAGE || 'MediaWiki:GranthaMap.json';
    var mapPromise = null;


     /* Toggle button */
     function granthaMap() {
    var btn = document.createElement( 'button' );
        if (mapPromise) return mapPromise;
    btn.className = 'gr-mobile-toc-btn';
        // Deliberately NOT cached in sessionStorage. It was, for six hours,
    btn.id = 'gr-mob-toc-btn';
        // which meant that editing the map page changed nothing for anyone
    btn.innerHTML = '☰ Contents';
        // whose tab had already read the old one - the exact failure you hit
    document.body.appendChild( btn );
        // while setting this up. mapPromise already collapses it to one
        // request per page load, and the browser's own HTTP cache handles
        // the rest; a stale map is far more expensive than a small fetch.
        try { sessionStorage.removeItem('gr-lib:map'); } catch (e) { }


    function openToc() {
        var url = (window.mw && mw.util && mw.util.getUrl)
      panel.classList.add( 'open' );
            ? mw.util.getUrl(MAP_PAGE, { action: 'raw', ctype: 'application/json' })
      backdrop.classList.add( 'open' );
            : '/index.php?title=' + encodeURIComponent(MAP_PAGE) + '&action=raw';
        mapPromise = fetch(url, { credentials: 'same-origin' })
            .then(function (r) { return r.ok ? r.json() : {}; })
            .then(function (m) {
                // Either a flat {page: id} object, or {pages, works,
                // locators} when names and division names are recorded too.
                m = m || {};
                if (!m.pages) m = { pages: m };
                m.works = m.works || {};
                m.locators = m.locators || {};
                try {
                    if (!Object.keys(m.works).length) {
                        console.info('[grantha] the map page has no "works" section, ' +
                            'so names must come from the library API. Re-run ' +
                            'sync_grantha_map.py to embed them.');
                    }
                } catch (e) { }
                return m;
            })
            .catch(function () { return {}; });      // no map page yet - fine
        return mapPromise;
     }
     }
     function closeToc() {
 
       panel.classList.remove( 'open' );
    // page name -> library id, however it is known. The attribute wins,
      backdrop.classList.remove( 'open' );
    // so a rebuilt page needs no map entry and a map entry needs no rebuild.
    function idForPage(pageName, el) {
        var own = el && el.getAttribute && el.getAttribute('data-grantha-id');
        if (own) return Promise.resolve(own);
        if (!pageName) return Promise.resolve('');
        return granthaMap().then(function (m) {
            var p = m.pages || {};
            return p[pageName] || p[String(pageName).replace(/_/g, ' ')] || '';
        });
    }
 
    // Resolve this page and hand the result to anything listening (the
     // citation builder). Also fills in the visible title when the library
    // and the page disagree, so the reader sees the real name.
    function resolvePage() {
        var t = document.querySelector('.gr-doc-title');
        if (!t) return;
        var page = (window.mw && mw.config.get('wgPageName')) || '';
        // Division names (adhyaya / valli / mantra) come from the same
        // map page, so a locator can be spelled out without a rebuild too.
        if (!t.getAttribute('data-locator-scheme')) {
            granthaMap().then(function (m) {
                var sc = (m.locators || {})[page] ||
                        (m.locators || {})[String(page).replace(/_/g, ' ')];
                if (sc) t.setAttribute('data-locator-scheme', sc);
            });
        }
        idForPage(page, t).then(function (gid) {
            if (!gid) return;
            t.setAttribute('data-grantha-id', gid);
            return lookup(gid).then(function (g) {
                if (!g) return;
                // On a commentary's own page the heading still reads as the
                // docx colophon ("...विरचिता प्रकाशिका"). Name it the way the
                // blocks are named instead.
                if (t.getAttribute('data-parent-doc')) {
                    var lab = teekaLabel(g);
                    if (lab) t.textContent = lab;
                } else if (g.name) {
                    t.textContent = g.name;
                }
                if (g.name)       t.setAttribute('data-doc-title', g.name);
                if (g.nameIAST)  t.setAttribute('data-doc-title-iast', g.nameIAST);
                if (g.author)    t.setAttribute('data-author', g.author);
                if (g.authorIAST) t.setAttribute('data-author-iast', g.authorIAST);
                try {
                    window.dispatchEvent(new CustomEvent('gr-library-resolved', { detail: g }));
                } catch (e) { }
            });
        });
     }
     }


     btn.addEventListener( 'click', openToc );
     // Fetching the whole catalogue is one request; fetching fifty records
    closeBtn.addEventListener( 'click', closeToc );
    // one at a time is fifty. Past a handful, take the catalogue.
    backdrop.addEventListener( 'click', closeToc );
    var cataloguePromise = null;
    function catalogue() {
        if (libraryDown) return Promise.resolve({});
        if (cataloguePromise) return cataloguePromise;
        cataloguePromise = fetch(API + '?limit=1000', {
            credentials: 'omit', headers: { 'Accept': 'application/json' }
        }).then(function (r) {
              if (!r.ok) { libraryDown = true; return null; }
              return r.json();
          })
          .then(function (j) {
              var list = (j && (j.granthas || j.data || j.results)) || [];
              var by = {};
              list.forEach(function (g) { var v = shape(g); if (v && v.id) { by[v.id] = v; cacheSet(v.id, v); } });
              return by;
          }).catch(function () { return {}; });
        return cataloguePromise;
    }


     /* Close on link click */
     function lookupMany(ids) {
    panel.querySelectorAll( 'a' ).forEach( function ( a ) {
        var want = [], seen = {};
      a.addEventListener( 'click', closeToc );
        (ids || []).forEach(function (i) { if (i && !seen[i]) { seen[i] = 1; want.push(i); } });
    } );
        // lookup() answers from the map page without a network call, so
  }
        // resolving fifty works costs one shared map fetch, not fifty
        // requests. Only ids the map does not name reach the library.
        return Promise.all(want.map(lookup)).then(function (vals) {
            var out = {};
            want.forEach(function (i, n) { out[i] = vals[n]; });
            return out;
        });
    }


  if ( document.readyState === 'loading' ) {
    // How a commentary is named on screen: its author, then the work.
     document.addEventListener( 'DOMContentLoaded', function () {
     // "श्रीवादिराजतीर्थः: काठकोपनिषद्भाष्यप्रकाशिका" - because several
      setTimeout( initMobileToc, 400 );
    // commentaries on one moola share a work-type name (प्रकाशिका,
     } );
    // विवरणम्, खण्डार्थः), and the author is what actually tells them
  } else {
    // apart for a reader scanning down the page.
    setTimeout( initMobileToc, 400 );
     var labelBySlug = {};
  }
    function teekaLabel(g) {
}() );
        if (!g) return '';
/* ── Mobile overrides — injected after readerToolbar's inline style ── */
        var name = g.name || g.nameIAST || '';
( function () {
        var author = g.author || g.authorIAST || '';
  if ( window.innerWidth > 768 ) return;
        if (!author || !name) return name || author;
        // Nothing to prefix if the work already opens with the author.
        if (name.indexOf(author) === 0) return name;
        return author + ': ' + name;
    }


  function injectMobileStyles() {
    // ── DOCUMENT LINKS ────────────────────────────────────────────────
     if ( document.getElementById( 'gr-mobile-css' ) ) return;
     // The home page's cards were written by hand and carry the same
     var s = document.createElement( 'style' );
    // colophon labels the citation had to stop using - "प्रकाशिका",
     s.id = 'gr-mobile-css';
     // "भाष्यम्" - which say what KIND of work it is, not which one. Any
     s.textContent = [
     // link to a mapped page gets the library's name instead, so the
      'html{font-size:18px!important;}',
     // catalogue and the site agree without the page being edited.
      'html body #gr-static-bar{height:auto!important;min-height:52px!important;padding:6px 8px!important;flex-wrap:nowrap!important;overflow-x:auto!important;overflow-y:hidden!important;scrollbar-width:none!important;gap:4px!important;}',
    function relabelDocLinks(root) {
      'html body #gr-static-bar::-webkit-scrollbar{display:none!important;}',
        var scope = root || document;
      'html body .gr-controls .gr-btn{height:44px!important;min-width:44px!important;font-size:15px!important;padding:0 10px!important;flex-shrink:0!important;}',
        var links = scope.querySelectorAll('.gr-home-grid a, .gr-doc-nav a, .gr-vadavali a');
      'html body .gr-controls .gr-icon-btn{width:44px!important;padding:0!important;}',
        if (!links.length) return;
      'html body .gr-script-sel{height:40px!important;font-size:14px!important;min-width:90px!important;max-width:120px!important;}',
        granthaMap().then(function (m) {
      'html body .gr-controls .gr-sep{display:none!important;}',
            var pages = m.pages || {};
      'html body .gr-btn-staging{display:none!important;}',
            var jobs = [];
      'html body .vector-column-start,html body #vector-toc,html body .vector-toc-landmark,html body .mw-table-of-contents-container,html body .vector-sticky-pinned-container{display:none!important;}',
            Array.prototype.forEach.call(links, function (a) {
      'html body .mw-page-container,html body .vector-page-container{max-width:100%!important;padding:0!important;margin:0!important;}',
                if (a.getAttribute('data-gr-relabelled')) return;
      'html body #mw-content-text,html body .mw-body,html body .mw-body-content{padding:8px 14px 32px!important;margin:0!important;max-width:100%!important;width:100%!important;box-sizing:border-box!important;}',
                var t = a.getAttribute('title') ||
      'html body .mw-parser-output{font-size:18px!important;line-height:1.8!important;max-width:100%!important;}',
                        decodeURIComponent((a.getAttribute('href') || '').split('#')[0]
      'html body .mw-parser-output h2,html body .mw-parser-output .mw-heading2 h2{font-size:1.1em!important;width:100%!important;margin-top:1.2em!important;}',
                            .replace(/^.*\/(?:wiki\/)?/, ''));
      'html body .mw-parser-output h3,html body .mw-parser-output .mw-heading3 h3{font-size:1em!important;width:100%!important;}',
                var gid = pages[t] || pages[String(t).replace(/_/g, ' ')];
      'html body .gr-home-grid{display:flex!important;flex-direction:column!important;flex-wrap:nowrap!important;gap:14px!important;overflow-x:hidden!important;width:100%!important;}',
                if (gid) jobs.push([a, gid]);
      'html body .gr-home-card{flex:0 0 auto!important;width:100%!important;max-width:100%!important;min-width:unset!important;box-sizing:border-box!important;}',
            });
      'html body .gr-toggle-btn{font-size:15px!important;padding:10px 20px!important;min-height:44px!important;}',
            if (!jobs.length) return;
      'html body #gra-toggle{bottom:80px!important;right:14px!important;width:54px!important;height:54px!important;}',
            lookupMany(jobs.map(function (j) { return j[1]; })).then(function (by) {
    ].join( '' );
                jobs.forEach(function (j) {
    document.head.appendChild( s );
                    var g = by[j[1]];
  }
                    if (!g || !g.name) return;
                    j[0].setAttribute('data-gr-relabelled', '1');
                    j[0].setAttribute('data-grantha-id', j[1]);
                    if (g.authorIAST || g.author) {
                        j[0].setAttribute('title', (g.author || '') + ' — ' + g.name);
                    }
                    j[0].textContent = g.name;
                });
            });
        });
    }


  /* Run after readerToolbar's injectStyles() has fired */
    window.grLibraryVersion = '2026-09-30a';
  if ( document.readyState === 'loading' ) {
    window.grLibrary = { lookup: lookup, lookupMany: lookupMany, catalogue: catalogue,
    document.addEventListener( 'DOMContentLoaded', injectMobileStyles );
                        teekaLabel: teekaLabel,
  } else {
                        labelFor: function (slug) { return labelBySlug[slug] || ''; },
    injectMobileStyles();
                        idForPage: idForPage, map: granthaMap, api: API,
  }
                        relabelDocLinks: relabelDocLinks };
}() );
    function boot() { resolvePage(); relabelDocLinks(); }
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
    else boot();
    window.addEventListener('gr-new-content', function (e) {
        relabelDocLinks(e && e.detail && e.detail.container);
    });
}());