🇮🇷 Iran Proxy | https://www.wikipedia.org/wiki/User:Barkeep49/rfxCloser.js
Jump to content

User:Barkeep49/rfxCloser.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
/**
 * RfX Closer User Script for Wikipedia 
 * Helps bureaucrats close RfAs and RfBs by providing a guided workflow and API interactions.
 * Version 1.2.1
 */
(function() {
    'use strict';

    // --- Configuration ---
    const config = {
        pageName: mw.config.get('wgPageName'),
        userName: mw.config.get('wgUserName') || 'YourUsername',
        tagLine: ' (using [[User:Barkeep49/rfxCloser.js|rfxCloser]])',
        selectors: {
            container: '#rfx-closer-container',
            header: '.rfx-closer-header',
            title: '.rfx-closer-title',
            collapseButton: '.rfx-closer-collapse',
            closeButton: '.rfx-closer-close',
            contentAndInputContainer: '.rfx-closer-main-content',
            inputSection: '.rfx-closer-input-section',
            inputFields: '.rfx-closer-input-fields',
            supportInput: '#support-count',
            opposeInput: '#oppose-count',
            neutralInput: '#neutral-count',
            closerInput: '#closer-name',
            percentageDisplay: '.rfx-closer-percentage',
            contentContainer: '.rfx-closer-content-container',
            stepsContainer: '#rfx-closer-steps',
            outcomeSelector: '#rfx-outcome-selector',
            launchButton: '#rfx-closer-launch',
            launchListItem: '#rfx-closer-launch-li',
            toolsMenu: '#p-tb ul',
            cratListTextarea: '#rfx-crat-list-textarea',
            cratNotifyMessage: '#rfx-crat-notify-message',
            cratNotifyButton: '#rfx-crat-notify-button',
            cratNotifyStatus: '#rfx-crat-notify-status',
            candidateOnholdNotifyMessage: '#rfx-candidate-onhold-notify-message',
            candidateOnholdNotifyButton: '#rfx-candidate-onhold-notify-button',
            candidateOnholdNotifyStatus: '#rfx-candidate-onhold-notify-status',
            stepElement: '.rfx-closer-step',
            stepCheckbox: 'input[type="checkbox"]'
        },
        groupDisplayNames: {
            'ipblock-exempt': 'IP block exempt', 'rollbacker': 'Rollbacker',
            'eventcoordinator': 'Event coordinator', 'filemover': 'File mover',
            'templateeditor': 'Template editor', 'massmessage-sender': 'Mass message sender',
            'extendedconfirmed': 'Extended confirmed user', 'extendedmover': 'Page mover',
            'patroller': 'New page reviewer', 'abusefilter-helper': 'Edit filter helper',
            'abusefilter': 'Edit filter manager', 'reviewer': 'Pending changes reviewer',
            'accountcreator': 'Account creator', 'autoreviewer': 'Autopatrolled'
        },
        apiDefaults: {
            format: 'json',
            formatversion: 2
        }
    };

    // Determine RfX type and base page name
    config.rfxType = config.pageName.includes('Requests_for_adminship') ? 'adminship' : 'bureaucratship';
    config.baseRfxPage = `Wikipedia:Requests_for_${config.rfxType}`;
    config.displayBaseRfxPage = config.baseRfxPage.replace(/_/g, ' ');
    config.candidateSubpage = config.pageName.split('/').pop(); // Get the candidate part
    config.recentRfxPage = 'Wikipedia:Requests_for_adminship/Recent'; // Same page for both adminship and bureaucratship

    // --- Constants ---
    const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 
                    'July', 'August', 'September', 'October', 'November', 'December'];
    
    // Common regex patterns (pre-compiled)
    const REGEX_PATTERNS = {
        dateParse: /(\d{1,2})\s+(\w+)\s+(\d{4})/,
        endTimeParse: /(\d{1,2}) (\w+) (\d{4})/,
        recentRfxEntry: /\{\{Recent RfX\|\|([^|]+)\|\|([^|]+)\|(\d+)\|(\d+)\|(\d+)\|([^}]+)\}\}/g,
        monthHeaderAnchord: /\{\{Anchord\|(\w+)\s+(\d{4})\}\}/,
        // Pattern for successful table: "– X successful and [[link|Y unsuccessful]] candidacies"
        monthHeaderCountSuccessful: /–\s*(\d+)\s+successful\s+and\s+\[\[[^\]]+\|(\d+)\s+unsuccessful\]\]\s+candidacies/,
        // Pattern for unsuccessful table: "– [[link|X successful]] and Y unsuccessful candidacies"
        monthHeaderCountUnsuccessful: /–\s*\[\[[^\]]+\|(\d+)\s+successful\]\]\s+and\s+(\d+)\s+unsuccessful\s+candidacies/,
        unsuccessfulLink: /\[\[([^\]]+)\|(\d+)\s+unsuccessful\]\]/,
        successfulLink: /\[\[([^\]]+)\|(\d+)\s+successful\]\]/
    };

    // --- State Variables ---
    let rfaData = null;
    let basePageWikitextCache = {};
    let actualCandidateUsername = config.candidateSubpage; // Default, updated on fetch
    let fetchErrorOccurred = false;
    let wikitextErrorOccurred = false;
    let isCollapsed = false;
    let isDragging = false;
    let dragStartX, dragStartY, containerStartX, containerStartY;
    
    // DOM element cache
    const domCache = {};

    // --- Initial Check ---
    if (!new RegExp(`^${config.baseRfxPage.replace(/ /g, '_')}/[^/]+$`).test(config.pageName)) {
        return;
    }

    // --- Utility Functions ---

    /** Escapes characters for use in regex, handling spaces/underscores. */
    function escapeRegex(string) {
        const spacedString = string.replace(/_/g, ' ');
        const underscoredString = string.replace(/ /g, '_');
        if (spacedString === underscoredString) {
            return spacedString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        }
        const escapedSpaced = spacedString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const escapedUnderscored = underscoredString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        return `(?:${escapedUnderscored}|${escapedSpaced})`;
    }

    /** Adds a delay. */
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /** Generic API request helper. */
    async function makeApiRequest(params, method = 'get', tokenType = null) {
        const api = new mw.Api();
        const fullParams = { ...config.apiDefaults, ...params };
        try {
            let response;
            if (method === 'get') {
                response = await api.get(fullParams);
            } else if (method === 'post' && tokenType) {
                response = await api.postWithToken(tokenType, fullParams);
            } else if (method === 'post') {
                response = await api.post(fullParams);
            } else {
                throw new Error(`Unsupported API method: ${method}`);
            }
            return response;
        } catch (error) {
            const errorCode = error?.error?.code || error?.textStatus || 'unknown';
            const errorInfo = error?.error?.info || error?.xhr?.responseText || 'Unknown API error';
            console.error(`RfX Closer API [${method.toUpperCase()} ${params.action}] Error:`, { params, errorCode, errorInfo, errorObj: error });
            throw { code: errorCode, info: errorInfo, params: params };
        }
    }

    /** Creates a DOM element with attributes and content. */
    function createElement(tag, options = {}, children = []) {
        const el = document.createElement(tag);
        Object.entries(options).forEach(([key, value]) => {
            if (key === 'style' && typeof value === 'object') {
                Object.assign(el.style, value);
            } else if (key === 'dataset' && typeof value === 'object') {
                Object.assign(el.dataset, value);
            } else if (key === 'className') {
                el.className = value;
            } else if (key === 'textContent') {
                el.textContent = value;
            } else if (key === 'innerHTML') {
                el.innerHTML = value;
            } else {
                el.setAttribute(key, value);
            }
        });
        children.forEach(child => {
            if (child instanceof Node) {
                el.appendChild(child);
            } else if (typeof child === 'string') {
                el.appendChild(document.createTextNode(child));
            }
        });
        return el;
    }

    /** Creates a standard API action button. */
    function createActionButton(id, text, onClick) {
        const button = createElement('button', { id, textContent: text, className: 'rfx-closer-action-button' });
        if (onClick) {
            button.addEventListener('click', onClick);
        }
        return button;
    }

    /** Creates a standard status display area. */
    function createStatusArea(id, className = 'rfx-closer-api-status') {
        return createElement('div', { id, className });
    }

    /** Creates a container with "Copy Code" and "Edit Page" links. */
    function createActionLinks(targetPage, wikitextOrGetter, linkTextPrefix = '', isCreateLink = false) {
        const linksContainer = createElement('div', { className: 'rfx-action-links-container' });
        const copyLink = createElement('a', {
            href: '#',
            textContent: `Copy Code${linkTextPrefix ? ' for ' + linkTextPrefix : ''}`,
            className: 'rfx-action-link',
            title: 'Copy generated wikitext to clipboard'
        });

        copyLink.addEventListener('click', (e) => {
            e.preventDefault();
            const textToCopy = (typeof wikitextOrGetter === 'function') ? wikitextOrGetter() : wikitextOrGetter;
            if (textToCopy === null || textToCopy === undefined) {
                console.warn("RfX Closer: Attempted to copy null/undefined text via link.");
                copyLink.textContent = 'Error (No Text)'; copyLink.classList.add('error');
                setTimeout(() => { copyLink.textContent = `Copy Code${linkTextPrefix ? ' for ' + linkTextPrefix : ''}`; copyLink.classList.remove('error'); }, 3000);
                return;
            }
            navigator.clipboard.writeText(textToCopy).then(() => {
                copyLink.textContent = 'Copied!'; copyLink.classList.add('copied'); copyLink.style.fontWeight = 'bold';
                setTimeout(() => { copyLink.textContent = `Copy Code${linkTextPrefix ? ' for ' + linkTextPrefix : ''}`; copyLink.classList.remove('copied'); copyLink.style.fontWeight = 'normal'; }, 1500);
            }).catch(err => {
                console.error('RfX Closer: Failed to copy text via link: ', err);
                copyLink.textContent = 'Error Copying!'; copyLink.classList.add('error');
                setTimeout(() => { copyLink.textContent = `Copy Code${linkTextPrefix ? ' for ' + linkTextPrefix : ''}`; copyLink.classList.remove('error'); }, 3000);
            });
        });
        linksContainer.appendChild(copyLink);

        if (targetPage) {
            const editLink = createElement('a', {
                href: mw.util.getUrl(targetPage, { action: 'edit' }),
                target: '_blank',
                textContent: `${isCreateLink ? 'Create' : 'Edit'} ${linkTextPrefix || targetPage.replace(/_/g, ' ')}`,
                className: 'rfx-action-link',
                title: `${isCreateLink ? 'Create' : 'Edit'} ${targetPage.replace(/_/g, ' ')} in edit mode`
            });
            linksContainer.appendChild(editLink);
        }
        return linksContainer;
    }

    /** Creates a box with content and a copy button (kept for fallback cases). */
    function createCopyableBox(content, helpText = 'Click button to copy', isLarge = false, inputElement = null) {
        const boxContainer = createElement('div', { className: 'rfx-copy-box-container' });
        const displayElement = inputElement ? inputElement : createElement('pre', {
            textContent: content,
            className: 'rfx-copy-box-display',
            style: isLarge ? { maxHeight: '300px' } : {}
        });
        if (!inputElement) { boxContainer.appendChild(displayElement); }

        const copyButton = createElement('button', {
            textContent: 'Copy',
            title: helpText,
            className: 'rfx-copy-box-button',
            style: (inputElement && inputElement.tagName === 'TEXTAREA') ? { top: 'auto', bottom: '5px' } : {}
        });

        copyButton.addEventListener('click', () => {
            const textToCopy = inputElement ? inputElement.value : content;
            if (textToCopy === null || textToCopy === undefined) { console.warn("RfX Closer: Attempted to copy null/undefined."); return; }
            navigator.clipboard.writeText(textToCopy).then(() => {
                copyButton.textContent = 'Copied!'; copyButton.classList.add('copied'); copyButton.disabled = true;
                setTimeout(() => { copyButton.textContent = 'Copy'; copyButton.classList.remove('copied'); copyButton.disabled = false; }, 1500);
            }).catch(err => {
                console.error('RfX Closer: Failed to copy text: ', err); copyButton.textContent = 'Error!'; copyButton.classList.add('error');
                setTimeout(() => { copyButton.textContent = 'Copy'; copyButton.classList.remove('error'); copyButton.disabled = false; }, 3000);
            });
        });
        boxContainer.appendChild(copyButton);
        boxContainer.appendChild(createElement('div', { textContent: helpText, className: 'rfx-copy-box-helptext' }));
        return boxContainer;
    }

    // --- Data Fetching Functions ---

    /** Fetches summary data from the main RfX list page. */
    async function fetchRfaData() {
        if (rfaData && !fetchErrorOccurred) return rfaData;
        fetchErrorOccurred = false;
        try {
            const data = await makeApiRequest({
                action: 'parse',
                page: config.baseRfxPage,
                prop: 'text'
            });
            const htmlContent = data.parse.text;
            const tempDiv = createElement('div', { innerHTML: htmlContent });
            const reportTable = tempDiv.querySelector('.rfx-report');
            let foundData = null;

            if (reportTable) {
                const rows = reportTable.querySelectorAll('tbody tr');
                rows.forEach(row => {
                    const rfaLink = row.querySelector('td:first-child a');
                    const linkHref = rfaLink ? rfaLink.getAttribute('href').replace(/ /g, '_') : '';
                    const targetHref = `/wiki/${config.baseRfxPage.replace(/ /g, '_')}/${config.candidateSubpage}`;

                    if (rfaLink && linkHref === targetHref) {
                        const cells = row.querySelectorAll('td');
                        if (cells.length >= 8) {
                            foundData = {
                                candidate: rfaLink.textContent.trim(),
                                support: cells[1]?.textContent.trim() || '0',
                                oppose: cells[2]?.textContent.trim() || '0',
                                neutral: cells[3]?.textContent.trim() || '0',
                                percent: cells[4]?.textContent.trim().replace('%', '') || 'N/A',
                                status: cells[5]?.textContent.trim() || 'N/A',
                                endTime: cells[6]?.textContent.trim() || 'N/A',
                                timeLeft: cells[7]?.textContent.trim() || 'N/A',
                            };
                        }
                    }
                });
            }

            if (foundData) {
                rfaData = foundData;
                actualCandidateUsername = rfaData.candidate;
            } else {
                throw new Error(`Could not find summary data for ${config.candidateSubpage}`);
            }
            return rfaData;
        } catch (error) {
            console.warn(`RfX Closer: Error fetching or parsing summary data: ${error.info || error.message}. Using defaults.`);
            fetchErrorOccurred = true;
            rfaData = { candidate: actualCandidateUsername, support: '0', oppose: '0', neutral: '0', percent: 'N/A', status: 'N/A', endTime: 'N/A', timeLeft: 'N/A' };
            return rfaData;
        }
    }

    /** Fetches the full wikitext of a given page, using cache. */
    async function fetchPageWikitext(targetPageName) {
        const normalizedPageName = targetPageName.replace(/ /g, '_');
        if (basePageWikitextCache[normalizedPageName]) {
            return basePageWikitextCache[normalizedPageName];
        }

        try {
            const data = await makeApiRequest({
                action: 'query',
                prop: 'revisions',
                titles: normalizedPageName,
                rvslots: 'main',
                rvprop: 'content'
            });
            const page = data.query.pages[0];
            if (page && page.missing) {
                basePageWikitextCache[normalizedPageName] = '';
                return '';
            }
            if (page && page.revisions?.[0]?.slots?.main?.content) {
                const wikitext = page.revisions[0].slots.main.content;
                basePageWikitextCache[normalizedPageName] = wikitext;
                if (normalizedPageName === config.pageName.replace(/ /g, '_')) wikitextErrorOccurred = false;
                return wikitext;
            } else {
                throw new Error(`Could not find wikitext content for page ${normalizedPageName}. Response: ${JSON.stringify(data)}`);
            }
        } catch (error) {
            console.error(`RfX Closer: Error fetching wikitext for ${normalizedPageName}:`, error);
            if (normalizedPageName === config.pageName.replace(/ /g, '_')) wikitextErrorOccurred = true;
            return null;
        }
    }

    /** Specific fetcher for the current RfX page wikitext. */
    const fetchRfXWikitext = () => {
        wikitextErrorOccurred = false; // Reset flag
        return fetchPageWikitext(config.pageName);
    };

    /** API function to check user groups. */
    async function getUserGroups(username) {
        try {
            const data = await makeApiRequest({
                action: 'query', list: 'users', ususers: username, usprop: 'groups'
            });
            const user = data.query?.users?.[0];
            if (user && !user.missing && !user.invalid) {
                const groups = user.groups || [];
                console.log(`RfX Closer getUserGroups: Found groups for ${username}:`, groups);
                return groups;
            } else {
                console.warn(`RfX Closer getUserGroups: User '${username}' not found or invalid.`);
                return null; // Indicate user not found/invalid
            }
        } catch (error) {
            console.error(`RfX Closer getUserGroups: API error checking groups for '${username}'.`, error);
            return null; // Indicate API error
        }
    }

    /** API function to grant/remove rights. */
    function grantPermissionAPI(username, groupToAdd, reason, groupsToRemove = null, expiry = 'infinity') {
        const params = {
            action: 'userrights',
            user: username,
            reason: reason,
            expiry: expiry
        };
        if (groupToAdd) params.add = groupToAdd;
        if (groupsToRemove) params.remove = groupsToRemove; // Expects pipe-separated string
        return makeApiRequest(params, 'post', 'userrights');
    }

    /** Helper function to post a message to a talk page. */
    async function postToTalkPage(targetPage, sectionTitle, messageContent, summary) {
        try {
            await makeApiRequest({
                action: 'edit',
                title: targetPage,
                section: 'new',
                sectiontitle: sectionTitle,
                text: messageContent,
                summary: summary
            }, 'post', 'edit');
            return { success: true, page: targetPage };
        } catch (error) {
            return { success: false, page: targetPage, error: `${error.info} (${error.code})` };
        }
    }

    /** Fetches current bureaucrats from Wikipedia API (single authoritative query). */
    async function fetchCurrentBureaucrats() {
        const names = [];
        let continueParams = null;

        try {
            do {
                const params = {
                    action: 'query',
                    list: 'allusers',
                    augroup: 'bureaucrat',
                    aulimit: 'max'
                };
                if (continueParams) {
                    Object.assign(params, continueParams);
                }

                const data = await makeApiRequest(params);
                const users = data?.query?.allusers || [];
                users.forEach(user => {
                    if (user?.name) {
                        names.push(user.name);
                    }
                });
                continueParams = data?.continue || null;
            } while (continueParams);
        } catch (error) {
            console.error('RfX Closer: Error fetching bureaucrats via augroup.', error);
            return { names: [], source: 'error' };
        }

        return {
            names: names.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })),
            source: 'userrights'
        };
    }

    /** Fixes the comment structure for yearly list pages - uncomments all months up to current month, comments future months. */
    function fixCommentStructure(wikitext, currentMonth, currentYear) {
        const currentMonthIndex = MONTHS.indexOf(currentMonth);
        if (currentMonthIndex === -1) {
            console.warn(`RfX Closer: Unknown month ${currentMonth}, cannot fix comment structure.`);
            return wikitext;
        }

        let modifiedWikitext = wikitext;
        let uncommentedAny = false;
        
        // Pattern 1: Match commented month headers where comment starts with <!--|-
        // Format: <!--|-\n| colspan="6" ... {{Anchord|December 2025}} ... -->|-
        const commentedMonthHeaderRegex1 = /<!--\|\-\s*\n\|\s*colspan="[^"]+"\s*style="[^"]+"\s*\|[^\n]*\{\{Anchord\|(\w+)\s+(\d{4})\}\}[^\n]*\n-->\|\-/g;
        
        // Pattern 2: Match commented month headers where comment starts with <!-- on its own line (single month)
        // Format: <!--\n|-\n| colspan="6" ... {{Anchord|November 2025}} ... -->|-
        const commentedMonthHeaderRegex2 = /<!--\s*\n\|\-\s*\n\|\s*colspan="[^"]+"\s*style="[^"]+"\s*\|[^\n]*\{\{Anchord\|(\w+)\s+(\d{4})\}\}[^\n]*\n-->\|\-/g;
        
        // Pattern 3: Match multi-month comment blocks (unsuccessful table format)
        // Format: <!--\n|-\n| colspan... Month1 ...\n|-\n| colspan... Month2 ...-->
        // This matches comment blocks that contain multiple month headers
        const multiMonthCommentRegex = /<!--\s*\n((?:\|\-\s*\n\|\s*colspan="[^"]+"\s*style="[^"]+"\s*\|[^\n]*\{\{Anchord\|[^}]+\}\}[^\n]*\n)*)\|\-\s*\n\|\s*colspan="[^"]+"\s*style="[^"]+"\s*\|[^\n]*\{\{Anchord\|(\w+)\s+(\d{4})\}\}[^\n]*\n-->/g;
        
        // Process pattern 1 (successful table format - single month)
        let match;
        while ((match = commentedMonthHeaderRegex1.exec(wikitext)) !== null) {
            const monthName = match[1];
            const monthYear = parseInt(match[2], 10);
            const monthIndex = MONTHS.indexOf(monthName);
            
            // Only process if it's the current year and the month should be uncommented
            if (monthYear === currentYear && monthIndex !== -1 && monthIndex <= currentMonthIndex) {
                // Extract the header content (between <!--|-\n and \n-->|-)
                const fullMatch = match[0];
                // Remove the comment markers: <!--|-\n at start and \n-->|- at end
                const headerContent = fullMatch.replace(/^<!--\|\-\s*\n/, '').replace(/\n-->\|\-$/, '');
                // The uncommented version should be: |-\n[header content]\n|-
                const uncommentedHeader = '|-\n' + headerContent + '\n|-\n';
                
                // Replace the commented version with uncommented version
                modifiedWikitext = modifiedWikitext.replace(fullMatch, uncommentedHeader);
                uncommentedAny = true;
                console.log(`RfX Closer: Uncommented month header for ${monthName} ${monthYear} (format 1).`);
            }
        }
        
        // Process pattern 2 (unsuccessful table format - single month)
        while ((match = commentedMonthHeaderRegex2.exec(wikitext)) !== null) {
            const monthName = match[1];
            const monthYear = parseInt(match[2], 10);
            const monthIndex = MONTHS.indexOf(monthName);
            
            // Only process if it's the current year and the month should be uncommented
            if (monthYear === currentYear && monthIndex !== -1 && monthIndex <= currentMonthIndex) {
                // Extract the header content (between <!--\n|-\n and \n-->|-)
                const fullMatch = match[0];
                // Remove the comment markers: <!--\n|-\n at start and \n-->|- at end
                const headerContent = fullMatch.replace(/^<!--\s*\n\|\-\s*\n/, '').replace(/\n-->\|\-$/, '');
                // The uncommented version should be: |-\n[header content]\n|-
                const uncommentedHeader = '|-\n' + headerContent + '\n|-\n';
                
                // Replace the commented version with uncommented version
                modifiedWikitext = modifiedWikitext.replace(fullMatch, uncommentedHeader);
                uncommentedAny = true;
                console.log(`RfX Closer: Uncommented month header for ${monthName} ${monthYear} (format 2).`);
            }
        }
        
        // Process pattern 3 (multi-month comment blocks)
        // Match comment blocks that start with <!-- and contain multiple month headers
        // Format: <!--\n|-\n| colspan... Month1 ...\n|-\n| colspan... Month2 ...-->
        // Use a pattern that matches the entire comment block including newlines
        const multiMonthCommentBlockRegex = /<!--\s*\n([\s\S]*?)-->/g;
        
        multiMonthCommentBlockRegex.lastIndex = 0;
        const matchesToProcess = [];
        while ((match = multiMonthCommentBlockRegex.exec(wikitext)) !== null) {
            matchesToProcess.push({
                match: match[0],
                innerContent: match[1],
                index: match.index
            });
        }
        
        // Process matches in reverse order to maintain correct indices
        for (let i = matchesToProcess.length - 1; i >= 0; i--) {
            const { match: commentBlock, innerContent, index: commentStart } = matchesToProcess[i];
            
            // Parse all months in the comment block
            // Match: |-\n| colspan="6" ... {{Anchord|Month Year}} ... (entire line)
            const monthHeaderRegex = new RegExp(`\\|\\-\\s*\\n\\|\\s*colspan="[^"]+"\\s*style="[^"]+"\\s*\\|[^\\n]*\\{\\{Anchord\\|(\\w+)\\s+(\\d{4})\\}\\}[^\\n]*`, 'g');
            const monthsInBlock = [];
            let monthMatch;
            
            // Find all months in the comment block
            // Split by lines and find month headers
            const lines = innerContent.split('\n');
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i];
                // Check if this line contains a month header
                const monthMatch = line.match(REGEX_PATTERNS.monthHeaderAnchord);
                if (monthMatch) {
                    // Get the previous line (should be |-) and current line
                    const prevLine = i > 0 ? lines[i - 1] : '';
                    const fullText = (prevLine.trim() === '|-' ? prevLine + '\n' : '|-\n') + line + '\n';
                    
                    monthsInBlock.push({
                        name: monthMatch[1],
                        year: parseInt(monthMatch[2], 10),
                        fullText: fullText
                    });
                }
            }
            
            if (monthsInBlock.length === 0) continue;
            
            // Process months in order and rebuild structure
            // We need to maintain the order they appear in the comment block
            let newContent = '';
            let inCommentBlock = false;
            let hasUncommented = false;
            
            for (const monthInfo of monthsInBlock) {
                const monthIndex = MONTHS.indexOf(monthInfo.name);
                const shouldUncomment = monthInfo.year === currentYear && monthIndex !== -1 && monthIndex <= currentMonthIndex;
                
                if (shouldUncomment) {
                    // Close comment block if we were in one
                    if (inCommentBlock) {
                        newContent += '-->';
                        inCommentBlock = false;
                    }
                    // Add uncommented month
                    newContent += monthInfo.fullText;
                    hasUncommented = true;
                    uncommentedAny = true;
                    console.log(`RfX Closer: Uncommented month header for ${monthInfo.name} ${monthInfo.year} (from multi-month block).`);
                } else {
                    // Open comment block if not already open
                    if (!inCommentBlock) {
                        newContent += '<!--\n';
                        inCommentBlock = true;
                    }
                    // Add commented month
                    newContent += monthInfo.fullText;
                }
            }
            
            // Close comment block if still open
            if (inCommentBlock) {
                newContent += '-->';
            }
            
            // Only replace if we made changes
            if (hasUncommented) {
                // Replace the comment block
                modifiedWikitext = modifiedWikitext.substring(0, commentStart) + 
                                 newContent + 
                                 modifiedWikitext.substring(commentStart + commentBlock.length);
            }
        }
        
        if (uncommentedAny) {
            console.log(`RfX Closer: Fixed comment structure - uncommented months up to ${currentMonth} ${currentYear}.`);
        } else {
            console.log(`RfX Closer: No month headers needed uncommenting for ${currentMonth} ${currentYear}.`);
        }
        
        return modifiedWikitext;
    }

    /** Maps outcome to Recent RfX status code. */
    function mapOutcomeToStatus(selectedOutcome) {
        const statusMap = {
            'successful': 'S',
            'unsuccessful': 'US',
            'withdrawn': 'W',
            'notnow': 'NN',
            'snow': 'SN'
        };
        return statusMap[selectedOutcome] || 'US'; // Default to US if unknown
    }

    /** Parses Recent RfX entries from wikitext. */
    function parseRecentRfxEntries(wikitext) {
        const entries = [];
        // Match {{Recent RfX||Username||Date|Support|Oppose|Neutral|Status}}
        // Format: {{Recent RfX||Chaotic Enby||3 November 2025|255|1|0|S}}
        const regex = REGEX_PATTERNS.recentRfxEntry;
        let match;
        
        while ((match = regex.exec(wikitext)) !== null) {
            const username = match[1].trim();
            const dateStr = match[2].trim();
            const support = parseInt(match[3], 10);
            const oppose = parseInt(match[4], 10);
            const neutral = parseInt(match[5], 10);
            const status = match[6].trim();
            
            // Parse date string (e.g., "3 November 2025")
            const dateObj = parseDateString(dateStr);
            
            entries.push({
                username,
                dateStr,
                date: dateObj,
                support,
                oppose,
                neutral,
                status,
                originalText: match[0]
            });
        }
        
        return entries;
    }

    /** Filters entries to past 3 months or 3 most recent. */
    function filterRecentEntries(entries, currentDate) {
        // Sort by date (most recent first)
        const sortedEntries = entries.slice().sort((a, b) => {
            if (!a.date && !b.date) return 0;
            if (!a.date) return 1;
            if (!b.date) return -1;
            return b.date - a.date; // Most recent first
        });
        
        // Calculate date 3 months ago
        const threeMonthsAgo = new Date(currentDate);
        threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
        
        // Filter to entries from past 3 months
        const recentEntries = sortedEntries.filter(entry => {
            if (!entry.date) return false;
            return entry.date >= threeMonthsAgo;
        });
        
        // If fewer than 3 from past 3 months, take the 3 most recent regardless of date
        if (recentEntries.length < 3) {
            return sortedEntries.slice(0, 3);
        }
        
        return recentEntries;
    }

    /** Rebuilds Recent table wikitext with filtered entries. */
    function rebuildRecentTable(wikitext, filteredEntries) {
        // Extract the top template and comment
        const topTemplateMatch = wikitext.match(/\{\{Wikipedia:Requests for adminship\/Recent\/Top\}\}/);
        const commentMatch = wikitext.match(/<!--[\s\S]*?-->/);
        const noincludeMatch = wikitext.match(/\|\}<noinclude>[\s\S]*?<\/noinclude>/);
        
        // Build new wikitext
        let newWikitext = '';
        
        // Add top template
        if (topTemplateMatch) {
            newWikitext += topTemplateMatch[0] + '\n';
        }
        
        // Add comment
        if (commentMatch) {
            newWikitext += commentMatch[0] + '\n';
        }
        
        // Add filtered entries
        filteredEntries.forEach(entry => {
            newWikitext += entry.originalText + '\n';
        });
        
        // Add closing and noinclude
        if (noincludeMatch) {
            newWikitext += noincludeMatch[0];
        } else {
            newWikitext += '|}';
        }
        
        return newWikitext;
    }

    // --- UI Update Functions ---

    /** Updates the percentage display based on input fields. */
    const updatePercentageDisplay = () => {
        const supportInputEl = getCachedElement(config.selectors.supportInput);
        const opposeInputEl = getCachedElement(config.selectors.opposeInput);
        const percentageDivEl = getCachedElement(config.selectors.percentageDisplay);
        if (!supportInputEl || !opposeInputEl || !percentageDivEl) return;

        const support = parseInt(supportInputEl.value, 10) || 0;
        const oppose = parseInt(opposeInputEl.value, 10) || 0;
        const total = support + oppose;
        const percentage = total > 0 ? (support / total * 100).toFixed(2) : 0;
        percentageDivEl.textContent = `Support percentage: ${percentage}% (${support}/${total})`;
    };

    /** Updates the input fields with fetched or default data. */
    const updateInputFields = async () => {
        try {
            const data = await fetchRfaData(); // Ensures data is fetched/available
            const supportInputEl = getCachedElement(config.selectors.supportInput);
            const opposeInputEl = getCachedElement(config.selectors.opposeInput);
            const neutralInputEl = getCachedElement(config.selectors.neutralInput);

            if (supportInputEl && opposeInputEl && neutralInputEl) {
                supportInputEl.value = data?.support || '';
                opposeInputEl.value = data?.oppose || '';
                neutralInputEl.value = data?.neutral || '';
                updatePercentageDisplay();
            } else {
                console.error("RfX Closer: Could not find input elements to update.");
            }
        } catch (error) {
            // Error already logged by fetchRfaData, just ensure UI shows defaults
            console.error("RfX Closer: Error updating input fields, likely due to fetch failure.");
            updatePercentageDisplay(); // Update percentage based on potentially empty fields
        }
    };

    /** Helper to get current vote counts from input fields. */
    function getCurrentVoteCounts() {
        const supportInputEl = getCachedElement(config.selectors.supportInput);
        const opposeInputEl = getCachedElement(config.selectors.opposeInput);
        const neutralInputEl = getCachedElement(config.selectors.neutralInput);
        return {
            support: supportInputEl?.value || '0',
            oppose: opposeInputEl?.value || '0',
            neutral: neutralInputEl?.value || '0'
        };
    }

    /** Helper to get current closer name from input field. */
    function getCloserName() {
        const closerInputEl = getCachedElement(config.selectors.closerInput);
        return closerInputEl?.value?.trim() || config.userName;
    }

    /** Gets a DOM element, caching it for future use. */
    function getCachedElement(selector) {
        if (!domCache[selector]) {
            domCache[selector] = document.querySelector(selector);
        }
        return domCache[selector];
    }

    /** Clears the DOM cache (useful if elements are recreated). */
    function clearDomCache() {
        Object.keys(domCache).forEach(key => delete domCache[key]);
    }

    /** Parses a date string in "DD Month YYYY" format to a Date object. */
    function parseDateString(dateStr) {
        if (!dateStr) return null;
        try {
            const dateParts = dateStr.match(REGEX_PATTERNS.dateParse);
            if (dateParts) {
                const monthIndex = MONTHS.indexOf(dateParts[2]);
                if (monthIndex !== -1) {
                    return new Date(parseInt(dateParts[3], 10), monthIndex, parseInt(dateParts[1], 10));
                }
            }
        } catch (e) {
            console.warn(`RfX Closer: Could not parse date "${dateStr}"`, e);
        }
        return null;
    }

    /** Formats date components into "DD Month YYYY" string. */
    function formatDate(day, month, year) {
        const monthName = typeof month === 'number' ? MONTHS[month] : month;
        return `${day} ${monthName} ${year}`;
    }

    /** Handles errors consistently with logging and user-facing messages. */
    function handleError(context, error, userMessage = null) {
        const errorMsg = userMessage || `Error in ${context}: ${error.message || error}`;
        console.error(`RfX Closer [${context}]:`, error);
        return {
            error: true,
            message: errorMsg,
            context: context,
            originalError: error
        };
    }

    /** Creates a loading state UI element. */
    function createLoadingState(message = 'Loading...') {
        const container = createElement('div', {
            className: 'rfx-closer-loading',
            innerHTML: `<span style="display: inline-block; width: 16px; height: 16px; border: 2px solid #ccc; border-top-color: #333; border-radius: 50%; animation: spin 1s linear infinite; margin-right: 8px;"></span>${message}`
        });
        return container;
    }

    /** Creates an error state UI element. */
    function createErrorState(message, details = null) {
        const container = createElement('div', {
            className: 'rfx-closer-error',
            style: { color: 'red', backgroundColor: '#ffe6e6', padding: '8px', borderRadius: '4px', margin: '8px 0' }
        });
        container.appendChild(createElement('p', { textContent: message, style: { margin: 0, fontWeight: 'bold' } }));
        if (details) {
            container.appendChild(createElement('p', { textContent: details, style: { margin: '4px 0 0 0', fontSize: '0.9em' } }));
        }
        return container;
    }

    /** Creates a success state UI element. */
    function createSuccessState(message) {
        return createElement('div', {
            className: 'rfx-closer-success',
            style: { color: 'green', backgroundColor: '#e6f3e6', padding: '8px', borderRadius: '4px', margin: '8px 0' },
            textContent: message
        });
    }

    /** Updates count line (e.g., '''X successful candidacies so far''') in wikitext. */
    function updateCountInWikitext(wikitext, countRegex, delta = 1) {
        let updated = false;
        const newWikitext = wikitext.replace(countRegex, (match, openingFormatting, currentCountStr, closingFormatting) => {
            const currentCount = parseInt(currentCountStr, 10);
            if (!isNaN(currentCount)) {
                const newCount = currentCount + delta;
                updated = true;
                const textPart = match.replace(openingFormatting || '', '').replace(currentCountStr, '').replace(closingFormatting || '', '');
                return `${openingFormatting || ''}${newCount}${textPart}${closingFormatting || ''}`;
            }
            return match; // Return original match if count couldn't be parsed
        });
        return updated ? newWikitext : wikitext; // Return original if no update occurred
    }


    // --- Step Description Render Functions (Extracted) ---

    function renderStep0Description() { /* Check Timing */
        const stepContainer = createElement('div');
        stepContainer.appendChild(createElement('p', {
            innerHTML: `Verify that at least 7 days have passed since the listing on <a href="/p/https://www.wikipedia.org/wiki/${config.baseRfxPage}" target="_blank">${config.displayBaseRfxPage}</a>.`
        }));
        const timingInfoContainer = createElement('div', {
            className: 'rfx-closer-info-box',
            textContent: 'Loading timing info...'
        });
        stepContainer.appendChild(timingInfoContainer);

        fetchRfaData().then(data => {
            if (fetchErrorOccurred) {
                timingInfoContainer.textContent = 'Could not load timing info. Check console for errors.';
                timingInfoContainer.classList.add('error');
            } else {
                const isTooEarly = data.status ? data.status.toLowerCase() === 'open' : true;
                timingInfoContainer.innerHTML = `End Time (UTC): ${data.endTime || 'N/A'}<br>Time Left: ${data.timeLeft || 'N/A'}<br>Status: ${data.status || 'N/A'}`;
                timingInfoContainer.style.backgroundColor = isTooEarly ? '#ffe6e6' : '#e6f3e6';
                timingInfoContainer.style.borderColor = isTooEarly ? '#f5c6cb' : '#c3e6cb';
                timingInfoContainer.classList.remove('error');
            }
        }).catch(() => {
            timingInfoContainer.textContent = 'Error processing timing info.';
            timingInfoContainer.classList.add('error');
        });
        return stepContainer;
    }

    function renderStep1Description() { /* Verify History */
        const historyUrl = `/w/index.php?title=${encodeURIComponent(config.pageName)}&action=history`;
        return createElement('div', {}, [
            createElement('p', { textContent: 'Check the history of the transcluded page to ensure comments are genuine and haven\'t been tampered with.' }),
            createElement('a', {
                href: historyUrl,
                target: '_blank',
                textContent: 'View page history',
                style: { display: 'inline-block', marginTop: '5px', padding: '5px 10px', backgroundColor: '#f8f9fa', border: '1px solid #a2a9b1', borderRadius: '3px', textDecoration: 'none' }
            })
        ]);
    }

    function renderStep2Description() { /* Determine Consensus */
        const stepContainer = createElement('div');
        stepContainer.appendChild(createElement('p', {
            innerHTML: 'Use traditional rules of thumb and your best judgement to determine consensus. Consider:<br>- Vote counts<br>- Quality of arguments<br>- Contributor weight<br>- Concerns raised and resolution'
        }));
        const voteTallyContainer = createElement('div', {
            className: 'rfx-closer-info-box',
            textContent: 'Loading vote tally...'
        });
        stepContainer.appendChild(voteTallyContainer);

        fetchRfaData().then(data => {
            if (fetchErrorOccurred) {
                voteTallyContainer.textContent = 'Could not load vote tally. Check console for errors.';
                voteTallyContainer.classList.add('error');
            } else {
                voteTallyContainer.innerHTML = `<strong>Current Vote Tally:</strong><br>Support: ${data.support || 'N/A'}<br>Oppose: ${data.oppose || 'N/A'}<br>Neutral: ${data.neutral || 'N/A'}<br>Support %: ${data.percent !== 'N/A' ? data.percent + '%' : 'N/A'}`;
                const numericPercent = parseFloat(data.percent);
                if (!isNaN(numericPercent)) {
                    if (numericPercent >= 75) voteTallyContainer.style.backgroundColor = '#e6f3e6';
                    else if (numericPercent >= 65) voteTallyContainer.style.backgroundColor = '#fff3cd';
                    else voteTallyContainer.style.backgroundColor = '#ffe6e6';
                } else {
                    voteTallyContainer.style.backgroundColor = '#f8f9fa';
                }
                voteTallyContainer.style.borderColor = window.getComputedStyle(voteTallyContainer).backgroundColor.replace('rgb', 'rgba').replace(')', ', 0.5)');
                voteTallyContainer.classList.remove('error');
            }
        }).catch(() => {
            voteTallyContainer.textContent = 'Error processing vote tally.';
            voteTallyContainer.classList.add('error');
        });
        return stepContainer;
    }

    function renderStep4Description(selectedOutcome, votes) { /* Prepare RfX Page Wikitext */
        const container = createElement('div');
        const loadingMsg = createElement('p', { textContent: 'Loading and processing wikitext...' });
        container.appendChild(loadingMsg);

        let reason = '', topTemplateName = '', isHoldOutcome = false;
        switch (selectedOutcome) {
            case 'successful': reason = 'successful'; topTemplateName = 'rfap'; break;
            case 'unsuccessful': reason = 'Unsuccessful'; topTemplateName = 'rfaf'; break;
            case 'withdrawn': reason = 'WD'; topTemplateName = 'rfaf'; break;
            case 'notnow': reason = 'NOTNOW'; topTemplateName = 'rfaf'; break;
            case 'snow': reason = 'SNOW'; topTemplateName = 'rfaf'; break;
            case 'onhold': reason = 'On hold'; topTemplateName = 'rfah'; isHoldOutcome = true; break;
            default: return 'This step is not applicable for the selected outcome.';
        }
        const topTemplateCode = `{{subst:${topTemplateName}}}`;

        fetchRfXWikitext().then(wikitext => {
            loadingMsg.remove();
            if (wikitext === null || wikitextErrorOccurred) {
                container.appendChild(createElement('p', { innerHTML: '<strong>Error:</strong> Could not fetch or process the page wikitext. Please perform the template replacements manually. Check console for details.', style: { color: 'red' } }));
                container.appendChild(createElement('p', { innerHTML: `You will need to manually add <code>${topTemplateCode}</code> at the top and potentially perform other closing steps.` }));
                return;
            }

            let modifiedWikitext = wikitext;
            modifiedWikitext = modifiedWikitext.replace(/\{\{(subst:)?(rfap|rfaf|rfah|Rfa withdrawn|Rfa snow)\}\}\s*\n?/i, ''); // Remove existing top templates
            modifiedWikitext = topTemplateCode + "\n" + modifiedWikitext; // Add new top template

            const finaltallyTemplate = `'''Final <span id="rfatally">(${votes.support}/${votes.oppose}/${votes.neutral})</span>; ended by ` + '~'.repeat(4);
            const footerTemplate = `
:''The above adminship discussion is preserved as an archive of the discussion. <span style="color:red">'''Please do not modify it.'''</span> Subsequent comments should be made on the appropriate discussion page (such as the talk page of either [[{{NAMESPACE}} talk:{{PAGENAME}}|this nomination]] or the nominated user). No further edits should be made to this page.''</div>__ARCHIVEDTALK__ __NOEDITSECTION__
`;
            const chatLink = `*See [[/Bureaucrat chat]].`;
            const escapedUsernamePattern = escapeRegex(actualCandidateUsername);
            const headerLineRegex = new RegExp(`^(\\s*===\\s*.*?(?:\\[\\[(?:User:${escapedUsernamePattern}|${escapedUsernamePattern})\\|${escapedUsernamePattern}\\]\\]|${escapedUsernamePattern}).*?\\s*===\\s*$)`, 'mi');
            let headerMatch = modifiedWikitext.match(headerLineRegex);
            let contentReplaced = false;

            if (headerMatch) {
                const headerEndIndex = headerMatch.index + headerMatch[0].length;
                const nextMarkerRegex = /\n(\s*(?:(?:={2,}.*={2,}$)|(?:'''\[\[Wikipedia:Requests for adminship#Monitors\|Monitors\]\]''':)))/m;
                const searchStringAfterHeader = modifiedWikitext.substring(headerEndIndex);
                const nextMarkerMatch = searchStringAfterHeader.match(nextMarkerRegex);
                if (nextMarkerMatch) {
                    const nextMarkerStartIndex = headerEndIndex + nextMarkerMatch.index;
                    const textBeforeHeaderEnd = modifiedWikitext.substring(0, headerEndIndex);
                    const textAfterMarkerStart = modifiedWikitext.substring(nextMarkerStartIndex);
                    const replacementContent = isHoldOutcome ? `\n${chatLink}\n${finaltallyTemplate}\n` : `\n${finaltallyTemplate}\n`;
                    modifiedWikitext = textBeforeHeaderEnd.trimEnd() + replacementContent + textAfterMarkerStart;
                    contentReplaced = true;
                } else { console.warn("RfX Closer: Could not find next section marker after header."); }
            } else { console.warn("RfX Closer: Could not find candidate header line."); }

            if (contentReplaced) {
                if (!isHoldOutcome) {
                    modifiedWikitext = modifiedWikitext.replace(/\{\{(subst:)?rfab\}\}\s*$/i, ''); // Remove existing footer
                    modifiedWikitext = modifiedWikitext.trim() + "\n" + footerTemplate; // Add new footer
                }
                const introText = isHoldOutcome
                    ? `Ensure <code>${topTemplateCode}</code> is at the top, and the 'Bureaucrat chat' link and final tally are placed correctly below the header.`
                    : `Use the links below to copy the full generated wikitext for this RfX page or open it directly in the editor. <strong>Review carefully before saving.</strong>`;
                container.appendChild(createElement('p', { innerHTML: introText }));
                container.appendChild(createActionLinks(config.pageName, modifiedWikitext, 'Full RfX Page'));
            } else {
                container.appendChild(createElement('p', { innerHTML: '<strong>Warning:</strong> Could not automatically find the section between the candidate header and the next section/Monitors line to replace. Manual replacement needed. Check console for details.', style: { color: 'orange' } }));
                const manualContent = isHoldOutcome ? `${chatLink}\n${finaltallyTemplate}` : finaltallyTemplate;
                container.appendChild(createElement('p', { innerHTML: `Please manually replace the content between the candidate header and the next section with:` }));
                container.appendChild(createCopyableBox(manualContent, 'Copy content to insert manually'));
                if (!isHoldOutcome) {
                    container.appendChild(createElement('p', { innerHTML: `Also ensure <code>${footerTemplate}</code> is at the very bottom.` }));
                }
                container.appendChild(createActionLinks(config.pageName, modifiedWikitext, 'RfX Page (Manual Edit Needed)'));
            }
        }).catch(err => {
            loadingMsg.remove();
            container.appendChild(createElement('p', { innerHTML: '<strong>Error:</strong> Error processing wikitext. Check console.', style: { color: 'red' } }));
            console.error("RfX Closer: Error in wikitext processing chain:", err);
        });
        return container;
    }

    function renderStep5Description() { /* Start Bureaucrat Chat */
        const chatPageName = `${config.pageName}/Bureaucrat chat`;
        const chatPageDisplay = `${config.candidateSubpage}/Bureaucrat chat`;
        const container = createElement('div');

        container.appendChild(createElement('p', {
            innerHTML: `Create the bureaucrat chat page at <a href="${mw.util.getUrl(chatPageName)}" target="_blank">[[${chatPageDisplay}]]</a> with the following content. You can add an optional initial comment below.`
        }));
        container.appendChild(createElement('label', {
            textContent: 'Optional initial comment (will be added under == Discussion ==):',
            htmlFor: 'rfx-crat-chat-textarea', // Link label to textarea
            style: { display: 'block', marginBottom: '4px' }
        }));
        const commentTextarea = createElement('textarea', {
            id: 'rfx-crat-chat-textarea', // Added ID
            className: 'rfx-crat-chat-textarea',
            placeholder: `e.g., Initiating discussion per RfX hold...  ~~</nowiki>~~`
        });
        container.appendChild(commentTextarea);

        const getChatPageWikitext = () => {
            const initialComment = commentTextarea.value.trim();
            return `{{Bureaucrat discussion header}}\n\n== Discussion ==\n${initialComment}\n\n== Recusals ==\n\n== Summary ==`;
        };
        container.appendChild(createActionLinks(chatPageName, getChatPageWikitext, 'Bureaucrat Chat Page', true));
        return container;
    }

    function renderStep6Description() { /* Notify Bureaucrats */
        const container = createElement('div');
        const chatPageName = `${config.pageName}/Bureaucrat chat`;
        const chatPageLink = `[[${chatPageName}|bureaucrat chat]]`;

        container.appendChild(createElement('p', { innerHTML: `Notify bureaucrats about the new chat page. Edit the message and the list of bureaucrats below if needed, then use the API button.` }));

        // Bureaucrat List Textarea
        const cratListLabel = createElement('label', {
            htmlFor: config.selectors.cratListTextarea.substring(1), // Remove # for htmlFor
            textContent: 'Bureaucrats to Notify (edit list as needed, one username per line):',
            style: { display: 'block', marginTop: '10px', marginBottom: '4px' }
        });
        container.appendChild(cratListLabel);
        
        // Create textarea first so it can be referenced in the button handler
        const cratListTextarea = createElement('textarea', {
            id: config.selectors.cratListTextarea.substring(1), // Remove # for ID
            className: 'rfx-onhold-notify-textarea',
            rows: 6,
            placeholder: 'Click "Load Current Bureaucrats" to populate this list.'
        });
        container.appendChild(cratListTextarea);

        // Notification Message Textarea
        container.appendChild(createElement('label', {
            htmlFor: config.selectors.cratNotifyMessage.substring(1),
            textContent: 'Notification Message:',
            style: { display: 'block', marginTop: '10px', marginBottom: '4px' }
        }));
        const messageTextarea = createElement('textarea', {
            id: config.selectors.cratNotifyMessage.substring(1),
            className: 'rfx-onhold-notify-textarea',
            value: `== Bureaucrat Chat ==\nYour input is requested at the freshly-created ${chatPageLink}. ` + '~'.repeat(4)
        });
        container.appendChild(messageTextarea);

        // Button and Status Area
        const buttonContainer = createElement('div');
        const postButton = createActionButton(config.selectors.cratNotifyButton.substring(1), 'Notify Bureaucrats via API', handleNotifyCratsClick); // Pass handler
        const statusArea = createStatusArea(config.selectors.cratNotifyStatus.substring(1), 'rfx-crat-notify-status');
        buttonContainer.appendChild(postButton);
        buttonContainer.appendChild(statusArea);
        container.appendChild(buttonContainer);

        // Initial Status Update Logic (defined before button so it can be referenced)
        const updateInitialStatus = (extraMessage = '') => {
            const bureaucratsToNotify = cratListTextarea.value.trim().split('\n').map(name => name.trim()).filter(name => name);
            if (bureaucratsToNotify.length > 0) {
                statusArea.textContent = `Ready to notify ${bureaucratsToNotify.length} bureaucrats from the list.`;
                postButton.disabled = false;
            } else {
                statusArea.textContent = 'Bureaucrat list is currently empty.';
                postButton.disabled = true;
            }
            statusArea.style.color = 'inherit';
            if (extraMessage) {
                statusArea.textContent += ` ${extraMessage}`;
            }
        };
        
        // Load Current Bureaucrats Button
        const loadCratsButton = createElement('button', {
            textContent: 'Load Current Bureaucrats',
            className: 'rfx-load-crats-button',
            type: 'button',
            style: { marginBottom: '8px', padding: '4px 8px' }
        });
        loadCratsButton.addEventListener('click', async () => {
            loadCratsButton.disabled = true;
            loadCratsButton.textContent = 'Loading...';
            try {
                const { names: currentCrats = [], source = 'unknown' } = (await fetchCurrentBureaucrats()) || {};
                if (currentCrats.length > 0) {
                    cratListTextarea.value = currentCrats.join('\n');
                    const sourceLabel = source === 'userrights' ? 'bureaucrat user-rights list' : 'unknown source';
                    updateInitialStatus(`(Loaded ${currentCrats.length} from ${sourceLabel}.)`);
                } else {
                    alert('No bureaucrats found or error occurred.');
                }
            } catch (error) {
                alert('Error loading bureaucrats: ' + (error?.message || error));
            } finally {
                loadCratsButton.disabled = false;
                loadCratsButton.textContent = 'Load Current Bureaucrats';
            }
        });
        container.appendChild(loadCratsButton);
        
        cratListTextarea.addEventListener('input', updateInitialStatus);
        updateInitialStatus(); // Set initial state

        return container;
    }

    function renderStep7Description() { /* Notify Candidate (On Hold) */
        const container = createElement('div');
        const candidateTalkPage = `User talk:${actualCandidateUsername}`;
        const chatPageName = `${config.pageName}/Bureaucrat chat`;
        const chatPageLink = `[[${chatPageName}|bureaucrat chat]]`;

        container.appendChild(createElement('p', { innerHTML: `Notify the candidate (${actualCandidateUsername}) about the 'on hold' status.` }));

        const messageTextarea = createElement('textarea', {
            id: config.selectors.candidateOnholdNotifyMessage.substring(1),
            className: 'rfx-onhold-notify-textarea',
            value: `== Your ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'} ==\nHi ${actualCandidateUsername}, just letting you know that your ${config.rfxType} request has been placed on hold pending discussion amongst the bureaucrats. You can follow the discussion at the ${chatPageLink}.  ` + '~'.repeat(4)
        });
        container.appendChild(messageTextarea);

        const buttonContainer = createElement('div');
        const postButton = createActionButton(config.selectors.candidateOnholdNotifyButton.substring(1), 'Post Notification via API', handleNotifyCandidateOnholdClick); // Pass handler
        const manualEditLink = createElement('a', {
            href: mw.util.getUrl(candidateTalkPage, { action: 'edit', section: 'new', sectiontitle: `Your ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'}` }),
            target: '_blank',
            className: 'rfx-notify-editlink',
            textContent: `Post Manually...`
        });
        const statusArea = createStatusArea(config.selectors.candidateOnholdNotifyStatus.substring(1), 'rfx-candidate-onhold-notify-status');

        buttonContainer.appendChild(postButton);
        buttonContainer.appendChild(manualEditLink);
        buttonContainer.appendChild(statusArea);
        container.appendChild(buttonContainer);

        return container;
    }

    async function renderStep8Description() { /* Process Promotion */
        const container = createElement('div');
        container.appendChild(createElement('p', { textContent: 'For successful RfXs, configure rights changes and grant via API:' }));
        const loadingStatus = createElement('p', { textContent: 'Loading current user groups...', style: { fontStyle: 'italic' } });
        container.appendChild(loadingStatus);
        const rightsContainer = createElement('div');
        container.appendChild(rightsContainer);
        const grantButton = createActionButton('rfx-grant-rights-button', 'Grant Rights via API'); // ID added
        grantButton.disabled = true;
        const statusArea = createStatusArea('rfx-grant-rights-status'); // ID added
        let removeCheckboxes = {}; // To store OOUI checkbox widgets

        try {
            const currentGroups = await getUserGroups(actualCandidateUsername);
            loadingStatus.remove();
            if (currentGroups === null) {
                statusArea.textContent = 'Error: Could not load user groups. Cannot proceed.';
                statusArea.style.color = 'red';
            } else {
                const groupToAdd = config.rfxType === 'adminship' ? 'sysop' : 'bureaucrat';
                const groupToAddLabel = config.rfxType === 'adminship' ? 'Administrator' : 'Bureaucrat';

                // Use OOUI for checkboxes
                const addFieldset = new OO.ui.FieldsetLayout({ label: 'Group to Add' });
                const addCheckbox = new OO.ui.CheckboxInputWidget({ selected: true, disabled: true, classes: ['rfx-closer-checkbox'] });
                addFieldset.addItems([new OO.ui.FieldLayout(addCheckbox, { label: groupToAddLabel, align: 'inline' })]);
                rightsContainer.appendChild(addFieldset.$element[0]);

                if (config.rfxType === 'adminship') {
                    const removeFieldset = new OO.ui.FieldsetLayout({ label: 'Remove existing rights (Uncheck to keep)' });
                    const groupsToExclude = ['*', 'user', 'autoconfirmed', groupToAdd, 'importer', 'transwiki', 'researcher', 'checkuser', 'suppress'];
                    removeCheckboxes = {}; // Reset
                    let hasRemovable = false;
                    currentGroups.forEach(groupName => {
                        if (!groupsToExclude.includes(groupName)) {
                            hasRemovable = true;
                            const checkbox = new OO.ui.CheckboxInputWidget({ selected: true });
                            removeCheckboxes[groupName] = checkbox; // Store the widget
                            const displayLabel = config.groupDisplayNames[groupName] || groupName;
                            const field = new OO.ui.FieldLayout(checkbox, { label: displayLabel, align: 'inline' });
                            removeFieldset.addItems([field]);
                        }
                    });
                    if (hasRemovable) {
                        rightsContainer.appendChild(removeFieldset.$element[0]);
                    } else {
                        rightsContainer.appendChild(createElement('p', { textContent: 'User holds no other explicit groups to potentially remove.', style: { fontSize: '0.9em', fontStyle: 'italic' } }));
                    }
                } else {
                     rightsContainer.appendChild(createElement('p', { textContent: 'No groups removed when granting bureaucrat rights.', style: { fontSize: '0.9em', fontStyle: 'italic' } }));
                }

                const userGroups = mw.config.get('wgUserGroups');
                const canGrant = userGroups && userGroups.includes('bureaucrat');
                if (canGrant) {
                    grantButton.disabled = false;
                    grantButton.addEventListener('click', () => handleGrantRightsClick(removeCheckboxes)); // Pass checkboxes map
                } else {
                    statusArea.textContent = 'You lack bureaucrat rights to use the API.'; statusArea.style.color = 'orange';
                }
            }
        } catch (error) {
            loadingStatus.remove();
            statusArea.textContent = `Error loading user groups: ${error.message || 'Unknown API error'}.`;
            statusArea.style.color = 'red';
        }

        container.appendChild(grantButton);
        container.appendChild(statusArea);
        const list = createElement('ul', { style: { paddingLeft: '20px', marginTop: '10px' } }, [
            createElement('li', { innerHTML: `Manual link: <a href="/p/https://www.wikipedia.org/wiki/Special:Userrights/${encodeURIComponent(actualCandidateUsername)}" target="_blank">Special:Userrights/${actualCandidateUsername}</a>` }),
            createElement('li', { innerHTML: `Reference: <a href="/p/https://www.wikipedia.org/wiki/Special:ListGroupRights" target="_blank">Special:ListGroupRights</a>` })
        ]);
        container.appendChild(list);
        return container;
    }

    async function renderStep9Description(selectedOutcome, votes) { /* Update Lists */
        const container = createElement('div');

        // Get date info (used by both Recent table and outcome lists)
        const now = new Date();
        let day = now.getDate();
        let month = MONTHS[now.getMonth()];
        let year = now.getFullYear();
        let formattedDate = formatDate(day, month, year);
        
        // Attempt to parse date from fetched data
        if (rfaData && rfaData.endTime && rfaData.endTime !== 'N/A') {
            const dateParts = rfaData.endTime.match(REGEX_PATTERNS.endTimeParse);
            if (dateParts) {
                formattedDate = `${dateParts[1]} ${dateParts[2]} ${dateParts[3]}`;
                day = parseInt(dateParts[1], 10);
                month = dateParts[2];
                year = parseInt(dateParts[3], 10);
            }
        }

        // --- 1. Remove from main RfX list page ---
        const removeDiv = createElement('div', { style: { marginBottom: '15px' } });
        const removeLinkText = config.displayBaseRfxPage;
        removeDiv.appendChild(createElement('p', { innerHTML: `<strong>1. First, remove entry from ${removeLinkText}:</strong>` }));
        const removeLoadingPara = createElement('p', { textContent: ` Loading wikitext for ${removeLinkText}...`, style: { fontStyle: 'italic' } });
        removeDiv.appendChild(removeLoadingPara);
        container.appendChild(removeDiv);

        try {
            const basePageWikitext = await fetchPageWikitext(config.baseRfxPage);
            removeLoadingPara.remove();
            if (basePageWikitext !== null) {
                const escapedPageNameForRegex = config.pageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                const lineToRemoveRegex = new RegExp(`^.*?\\{\\{(?:${escapedPageNameForRegex.replace(/_/g, '[_ ]')})\\}\\}.*?$\\n(?:^----\\s*\\n)?`, 'gmi');
                const modifiedBasePageWikitext = basePageWikitext.replace(lineToRemoveRegex, '');
                if (modifiedBasePageWikitext.length < basePageWikitext.length) {
                    removeDiv.appendChild(createActionLinks(config.baseRfxPage, modifiedBasePageWikitext, removeLinkText));
                } else {
                    removeDiv.appendChild(createElement('p', { innerHTML: ` Warning: Could not find <code>{{${config.pageName}}}</code> to remove from ${removeLinkText}. Remove manually.`, style: { color: 'orange' } }));
                    removeDiv.appendChild(createActionLinks(config.baseRfxPage, basePageWikitext, removeLinkText)); // Still provide link to edit
                }
            } else { throw new Error(`Failed to fetch wikitext for ${config.baseRfxPage}`); }
        } catch (error) {
            console.error("RfX Closer: Error processing base page wikitext:", error);
            removeLoadingPara.remove();
            removeDiv.appendChild(createElement('p', { textContent: ` Error loading wikitext for ${removeLinkText}. Edit manually.`, style: { color: 'red' } }));
        }

        // --- 1b. Update Recent RfX table ---
        const recentDiv = createElement('div', { style: { marginTop: '15px', marginBottom: '15px' } });
        recentDiv.appendChild(createElement('p', { innerHTML: `<strong>1b. Update <a href="/p/https://www.wikipedia.org/wiki/${config.recentRfxPage}" target="_blank">Recent RfX table</a>:</strong>` }));
        const recentLoading = createElement('p', { textContent: ` Loading...`, style: { fontStyle: 'italic' } });
        recentDiv.appendChild(recentLoading);
        container.appendChild(recentDiv);

        try {
            const recentWikitext = await fetchPageWikitext(config.recentRfxPage);
            recentLoading.remove();
            
            if (recentWikitext !== null) {
                // Parse existing entries
                let entries = parseRecentRfxEntries(recentWikitext);
                
                // Remove current candidate's entry if it exists
                entries = entries.filter(entry => 
                    entry.username.toLowerCase() !== actualCandidateUsername.toLowerCase()
                );
                
                // Add current candidate's entry
                const status = mapOutcomeToStatus(selectedOutcome);
                const newEntryText = `{{Recent RfX||${actualCandidateUsername}||${formattedDate}|${votes.support}|${votes.oppose}|${votes.neutral}|${status}}}`;
                
                // Parse date for current candidate
                let currentCandidateDate = null;
                try {
                    const dateParts = formattedDate.match(REGEX_PATTERNS.dateParse);
                    if (dateParts) {
                        const monthIndex = MONTHS.indexOf(dateParts[2]);
                        if (monthIndex !== -1) {
                            currentCandidateDate = new Date(parseInt(dateParts[3], 10), monthIndex, parseInt(dateParts[1], 10));
                        }
                    }
                } catch (e) {
                    console.warn(`RfX Closer: Could not parse date "${formattedDate}"`, e);
                }
                
                // Add current candidate entry
                entries.push({
                    username: actualCandidateUsername,
                    dateStr: formattedDate,
                    date: currentCandidateDate,
                    support: parseInt(votes.support, 10) || 0,
                    oppose: parseInt(votes.oppose, 10) || 0,
                    neutral: parseInt(votes.neutral, 10) || 0,
                    status: status,
                    originalText: newEntryText
                });
                
                // Filter to past 3 months or 3 most recent
                const filteredEntries = filterRecentEntries(entries, now);
                
                // Rebuild the table
                const modifiedRecentWikitext = rebuildRecentTable(recentWikitext, filteredEntries);
                
                const noteText = ` Table updated with ${filteredEntries.length} entries (${filteredEntries.length === 1 ? 'entry' : 'entries'} from past 3 months or 3 most recent).`;
                recentDiv.appendChild(createElement('p', { innerHTML: noteText }));
                recentDiv.appendChild(createActionLinks(config.recentRfxPage, modifiedRecentWikitext, 'Recent RfX table'));
            } else {
                throw new Error(`Failed to fetch wikitext for ${config.recentRfxPage}`);
            }
        } catch (error) {
            console.error("RfX Closer: Error processing Recent RfX table:", error);
            recentLoading.remove();
            recentDiv.appendChild(createElement('p', { innerHTML: ` Error processing Recent RfX table. Update manually.`, style: { color: 'red' } }));
        }

        // --- 2. Add to outcome lists ---
        const addDiv = createElement('div'); // Container for list updates
        addDiv.appendChild(createElement('p', { innerHTML: `<strong>2. Then, add entry to appropriate list(s):</strong>` }));

        let generatedListEntry = null, generatedRfarowTemplate = null, yearlyListPageName = '', alphabeticalListPageName = '', isSuccessfulList = false;

        // Determine list pages and entry formats based on outcome
        if (selectedOutcome === 'successful') {
            isSuccessfulList = true;
            yearlyListPageName = `Wikipedia:Successful ${config.rfxType} candidacies/${year}`;
            generatedRfarowTemplate = `|{{rfarow|${actualCandidateUsername}||${formattedDate}|p|${votes.support}|${votes.oppose}|${votes.neutral}|${getCloserName()}}}`;
        } else if (['unsuccessful', 'withdrawn', 'notnow', 'snow'].includes(selectedOutcome)) {
            isSuccessfulList = false;
            let reasonText = 'Unsuccessful', rfarowResult = 'unsuccessful';
            switch(selectedOutcome) {
                case 'withdrawn': reasonText = 'Withdrawn'; rfarowResult = 'Withdrawn'; break;
                case 'notnow': reasonText = '[[WP:NOTNOW|NOTNOW]]'; rfarowResult = 'NOTNOW'; break;
                case 'snow': reasonText = '[[WP:SNOW|SNOW]]'; rfarowResult = 'SNOW'; break;
            }
            yearlyListPageName = `Wikipedia:Unsuccessful ${config.rfxType} candidacies (Chronological)/${year}`;
            const firstLetter = actualCandidateUsername.charAt(0).toUpperCase();
            let alphaBase = `Wikipedia:Unsuccessful ${config.rfxType} candidacies`;
            if (config.rfxType === 'adminship') { alphabeticalListPageName = (firstLetter >= 'A' && firstLetter <= 'Z') ? `${alphaBase}/${firstLetter}` : `${alphaBase} (Alphabetical)`; }
            else { alphabeticalListPageName = `${alphaBase} (Alphabetical)`; }
            const isSubsequentNomination = /[_ ]\d+$/.test(config.pageName);
            const closerName = getCloserName();
            generatedListEntry = `${isSubsequentNomination ? '*:' : '*'} [[${config.pageName}|${actualCandidateUsername}]] ${formattedDate} - ${reasonText} ([[User:${closerName}|${closerName}]]) (${votes.support}/${votes.oppose}/${votes.neutral})`;
            generatedRfarowTemplate = `|{{rfarow|${actualCandidateUsername}||${formattedDate}|${rfarowResult}|${votes.support}|${votes.oppose}|${votes.neutral}|${closerName}}}`;
        }

        // --- Handle Yearly Page Update ---
        if (yearlyListPageName && generatedRfarowTemplate) {
            const yearlyDiv = createElement('div', { style: { marginTop: '10px' } });
            yearlyDiv.appendChild(createElement('p', { innerHTML: `2a. Update <a href="/p/https://www.wikipedia.org/wiki/${yearlyListPageName}" target="_blank">${yearlyListPageName}</a>:` }));
            const yearlyLoading = createElement('p', { textContent: ` Loading...`, style: { fontStyle: 'italic' } });
            yearlyDiv.appendChild(yearlyLoading);
            addDiv.appendChild(yearlyDiv);

            try {
                const yearlyWikitext = await fetchPageWikitext(yearlyListPageName);
                yearlyLoading.remove();
                if (yearlyWikitext !== null) {
                    let modifiedYearlyWikitext = yearlyWikitext; let modificationPerformed = false; let noteText = ''; let entryAdded = false; let countUpdated = false; let uncommentAttempted = false;
                    const countRegex = isSuccessfulList ? /(\'\'\'?)(\d+)\s+successful candidacies so far(\'\'\'?)/i : /(\'\'\'?)(\d+)\s+unsuccessful candidacies so far(\'\'\'?)/i;
                    const originalWikitextForCountCheck = modifiedYearlyWikitext;
                    modifiedYearlyWikitext = updateCountInWikitext(modifiedYearlyWikitext, countRegex, 1);
                    if (modifiedYearlyWikitext !== originalWikitextForCountCheck) { modificationPerformed = true; countUpdated = true; } else { console.warn(`RfX Closer: Could not update count on ${yearlyListPageName}`); }

                    // First, handle the overall comment structure - uncomment all months up to current month
                    modifiedYearlyWikitext = fixCommentStructure(modifiedYearlyWikitext, month, year);
                    modificationPerformed = true;
                    uncommentAttempted = true;

                    const escapedMonth = month.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    // Updated regex to match month headers (more flexible, not requiring start of line)
                    const monthHeaderRegex = new RegExp(`\\|-\\s*\\n\\|\\s*colspan="[\\d]+"\\s*style="background:#ffdead;"\\s*\\|\\s*'''\\{\\{Anchord\\|${escapedMonth}\\s+${year}\\}\\}'''.*?$`, 'mi');
                    let insertionIndex = -1;
                    let headerFound = false;
                    let originalWikitextForInsertionCheck = modifiedYearlyWikitext;
                    let headerMatch = modifiedYearlyWikitext.match(monthHeaderRegex);
                    let monthHeaderUpdated = false;

                    if (headerMatch) {
                        headerFound = true;
                        const fullMatchText = headerMatch[0];
                        const headerStartIndex = headerMatch.index;
                        
                        // Update the month header counts
                        // Successful table format: "&ndash; X successful and [[link|Y unsuccessful]] candidacies"
                        // Unsuccessful table format: "&ndash; [[link|X successful]] and Y unsuccessful candidacies"
                        let headerCountMatch = null;
                        let currentSuccessful = 0;
                        let currentUnsuccessful = 0;
                        let successfulLinkMatch = null;
                        let unsuccessfulLinkMatch = null;
                        
                        if (isSuccessfulList) {
                            // Try successful table format first
                            headerCountMatch = fullMatchText.match(REGEX_PATTERNS.monthHeaderCountSuccessful);
                            if (headerCountMatch) {
                                currentSuccessful = parseInt(headerCountMatch[1], 10);
                                currentUnsuccessful = parseInt(headerCountMatch[2], 10);
                                unsuccessfulLinkMatch = fullMatchText.match(REGEX_PATTERNS.unsuccessfulLink);
                            }
                        } else {
                            // Try unsuccessful table format
                            headerCountMatch = fullMatchText.match(REGEX_PATTERNS.monthHeaderCountUnsuccessful);
                            if (headerCountMatch) {
                                currentSuccessful = parseInt(headerCountMatch[1], 10);
                                currentUnsuccessful = parseInt(headerCountMatch[2], 10);
                                successfulLinkMatch = fullMatchText.match(REGEX_PATTERNS.successfulLink);
                            }
                        }
                        
                        if (headerCountMatch) {
                            let updatedHeader;
                            if (isSuccessfulList) {
                                // Increment successful count
                                const newSuccessful = currentSuccessful + 1;
                                let linkText = `[[Wikipedia:Unsuccessful ${config.rfxType} candidacies (Chronological)/${year}#${month} ${year}|${currentUnsuccessful} unsuccessful]]`;
                                if (unsuccessfulLinkMatch) {
                                    // Preserve the link structure but update the count
                                    const linkPath = unsuccessfulLinkMatch[1];
                                    linkText = `[[${linkPath}|${currentUnsuccessful} unsuccessful]]`;
                                }
                                
                                // Replace the count in the header
                                updatedHeader = fullMatchText.replace(
                                    REGEX_PATTERNS.monthHeaderCountSuccessful,
                                    `&ndash; ${newSuccessful} successful and ${linkText} candidacies`
                                );
                                
                                console.log(`RfX Closer: Updated ${month} ${year} header: ${currentSuccessful} -> ${newSuccessful} successful.`);
                            } else {
                                // Increment unsuccessful count
                                const newUnsuccessful = currentUnsuccessful + 1;
                                let linkText = `[[Wikipedia:Successful ${config.rfxType} candidacies/${year}#${month} ${year}|${currentSuccessful} successful]]`;
                                if (successfulLinkMatch) {
                                    // Preserve the link structure but update the count
                                    const linkPath = successfulLinkMatch[1];
                                    linkText = `[[${linkPath}|${currentSuccessful} successful]]`;
                                }
                                
                                // Replace the count in the header (unsuccessful table format)
                                updatedHeader = fullMatchText.replace(
                                    REGEX_PATTERNS.monthHeaderCountUnsuccessful,
                                    `&ndash; ${linkText} and ${newUnsuccessful} unsuccessful candidacies`
                                );
                                
                                console.log(`RfX Closer: Updated ${month} ${year} header: ${currentUnsuccessful} -> ${newUnsuccessful} unsuccessful.`);
                            }
                            
                            // Replace the header in the wikitext
                            modifiedYearlyWikitext = modifiedYearlyWikitext.substring(0, headerStartIndex) + 
                                                    updatedHeader + 
                                                    modifiedYearlyWikitext.substring(headerStartIndex + fullMatchText.length);
                            
                            monthHeaderUpdated = true;
                            modificationPerformed = true;
                        }
                        
                        // After fixCommentStructure, the header should be uncommented
                        // Re-find the header position after potential updates
                        const updatedHeaderMatch = modifiedYearlyWikitext.substring(headerStartIndex).match(monthHeaderRegex);
                        const updatedHeaderText = updatedHeaderMatch ? updatedHeaderMatch[0] : fullMatchText;
                        
                        // Find the next |- after the header to insert the entry
                        const nextRowIndex = modifiedYearlyWikitext.indexOf('|-', headerStartIndex + updatedHeaderText.length);
                        insertionIndex = nextRowIndex !== -1 ? nextRowIndex : headerStartIndex + updatedHeaderText.length;
                        
                        console.log(`RfX Closer: Found ${month} ${year} header at position ${headerStartIndex}, insertion point at ${insertionIndex}`);
                    } else {
                        headerFound = false;
                        console.warn(`RfX Closer: Could not find header for ${month} ${year} on ${yearlyListPageName}.`);
                    }

                    if (headerFound && insertionIndex !== -1) {
                        // Ensure we have a newline before inserting
                        if (insertionIndex > 0 && modifiedYearlyWikitext[insertionIndex - 1] !== '\n') {
                            modifiedYearlyWikitext = modifiedYearlyWikitext.substring(0, insertionIndex) + '\n' + modifiedYearlyWikitext.substring(insertionIndex);
                            insertionIndex++;
                        }
                        const insertionContent = '|-\n' + generatedRfarowTemplate + '\n';
                        modifiedYearlyWikitext = modifiedYearlyWikitext.substring(0, insertionIndex) + insertionContent + modifiedYearlyWikitext.substring(insertionIndex);
                        if (modifiedYearlyWikitext !== originalWikitextForInsertionCheck) {
                            modificationPerformed = true;
                        }
                        entryAdded = true;
                        console.log(`RfX Closer: Entry added to ${yearlyListPageName} at position ${insertionIndex}.`);
                    } else if (!headerFound) {
                        console.warn(`RfX Closer: Row not inserted into yearly list because header was not found.`);
                    }

                    noteText = ` `;
                    if (countUpdated && monthHeaderUpdated && entryAdded) { noteText += `Overall count, month header count, and entry updated.`; }
                    else if (countUpdated && entryAdded) { noteText += `Overall count updated and entry added.`; }
                    else if (monthHeaderUpdated && entryAdded) { noteText += `Month header count updated and entry added.`; }
                    else if (countUpdated && monthHeaderUpdated && !entryAdded) { noteText += `Overall and month header counts updated (could not add entry).`; }
                    else if (countUpdated && !entryAdded) { noteText += `Overall count updated (could not add entry).`; }
                    else if (monthHeaderUpdated && !entryAdded) { noteText += `Month header count updated (could not add entry).`; }
                    else if (!countUpdated && !monthHeaderUpdated && entryAdded) { noteText += `Entry added (could not update counts).`; }
                    else if (!countUpdated && !monthHeaderUpdated && !entryAdded) { noteText = ` Could not automatically update ${yearlyListPageName}.`; }
                    if (uncommentAttempted && !entryAdded) { noteText += ` (Failed to uncomment month header or insert entry).`; }
                    else if (uncommentAttempted && entryAdded) { noteText += ` (Month header uncommented - please verify).`; }

                    yearlyDiv.appendChild(createElement('p', { innerHTML: noteText }));
                    if (modificationPerformed) {
                        yearlyDiv.appendChild(createActionLinks(yearlyListPageName, modifiedYearlyWikitext, yearlyListPageName));
                    }
                    if (!entryAdded) { yearlyDiv.appendChild(createCopyableBox(generatedRfarowTemplate, `Copy row template to manually insert`)); }
                } else { throw new Error(`Failed to fetch wikitext for ${yearlyListPageName}`); }
            } catch (error) {
                console.error("RfX Closer: Error processing yearly list:", error);
                yearlyLoading.remove();
                yearlyDiv.appendChild(createElement('p', { innerHTML: ` Error processing ${yearlyListPageName}. Update manually.`, style: { color: 'red' } }));
                if (generatedRfarowTemplate) { yearlyDiv.appendChild(createCopyableBox(generatedRfarowTemplate, `Copy row template to manually insert`)); }
            }
        }

        // --- Handle Alphabetical List Page Update ---
        if (alphabeticalListPageName && !isSuccessfulList && generatedListEntry) {
            const alphaDiv = createElement('div', { style: { marginTop: '15px' } });
            alphaDiv.appendChild(createElement('p', { innerHTML: `2b. Update <a href="/p/https://www.wikipedia.org/wiki/${alphabeticalListPageName}" target="_blank">${alphabeticalListPageName}</a>:` }));
            const alphaLoading = createElement('p', { textContent: ` Loading...`, style: { fontStyle: 'italic' } });
            alphaDiv.appendChild(alphaLoading);
            addDiv.appendChild(alphaDiv);

            try {
                const alphaWikitext = await fetchPageWikitext(alphabeticalListPageName);
                alphaLoading.remove();
                if (alphaWikitext !== null) {
                    const lines = alphaWikitext.split('\n'); let insertionLineIndex = -1; const newCandidateName = actualCandidateUsername; const lineRegex = /^\s*([*:]*)\s*\[\[(?:[^|]+\|)?([^\]]+)\]\]/i;
                    for (let i = 0; i < lines.length; i++) { const line = lines[i]; const match = line.match(lineRegex); if (match) { const existingUsername = match[2].trim(); if (newCandidateName.localeCompare(existingUsername, undefined, { sensitivity: 'base' }) < 0) { insertionLineIndex = i; break; } } }

                    let modifiedAlphaWikitext; let alphaNoteText = ` `;
                    if (insertionLineIndex !== -1) { lines.splice(insertionLineIndex, 0, generatedListEntry); modifiedAlphaWikitext = lines.join('\n'); alphaNoteText += `Entry inserted alphabetically.`; }
                    else { let lastRelevantIndex = -1; for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].match(lineRegex)) { lastRelevantIndex = i; break; } } if (lastRelevantIndex !== -1) { lines.splice(lastRelevantIndex + 1, 0, generatedListEntry); } else { if (alphaWikitext.length > 0 && !alphaWikitext.endsWith('\n')) { lines.push(''); } lines.push(generatedListEntry); } modifiedAlphaWikitext = lines.join('\n'); alphaNoteText += `Entry appended.`; }

                    alphaDiv.appendChild(createElement('p', { innerHTML: alphaNoteText }));
                    alphaDiv.appendChild(createActionLinks(alphabeticalListPageName, modifiedAlphaWikitext, alphabeticalListPageName));
                } else { throw new Error(`Failed to fetch wikitext for ${alphabeticalListPageName}`); }
            } catch (error) {
                console.error("RfX Closer: Error processing alphabetical list:", error);
                alphaLoading.remove();
                alphaDiv.appendChild(createElement('p', { innerHTML: ` Error processing ${alphabeticalListPageName}. Update manually.`, style: { color: 'red' } }));
                if (generatedListEntry) { alphaDiv.appendChild(createCopyableBox(generatedListEntry, `Copy entry to manually insert`)); }
            }
        }

        container.appendChild(addDiv);
        return container;
    }

    function renderStep10Description(selectedOutcome) { /* Notify Candidate (Closing) */
        if (selectedOutcome === 'onhold') {
            return '(Notification for "on hold" is handled in Step 7.)';
        }

        const container = createElement('div');
        const candidateTalkPage = `User talk:${actualCandidateUsername}`;
        const sectionTitle = `Outcome of your ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'}`;
        const editUrl = mw.util.getUrl(candidateTalkPage, { action: 'edit', section: 'new', sectiontitle: sectionTitle });

        container.appendChild(createElement('p', { innerHTML: `Prepare message for <a href="/p/https://www.wikipedia.org/wiki/${encodeURIComponent(candidateTalkPage)}" target="_blank">${candidateTalkPage}</a>.` }));

        const optionsDiv = createElement('div', { className: 'rfx-notify-options' });
        container.appendChild(optionsDiv);
        const messageTextarea = createElement('textarea', { className: 'rfx-notify-textarea', rows: 5 });
        const copyBoxPlaceholder = createElement('div'); // Placeholder for copy box/links
        const buttonContainer = createElement('div', { style: { marginTop: '10px' } });
        const statusArea = createStatusArea('rfx-notify-status-closing', 'rfx-notify-status'); // New ID
        let messageContentGetter = () => ''; // Function to get current message content

        if (selectedOutcome === 'successful') {
            const templateName = config.rfxType === 'adminship' ? 'New sysop' : 'New bureaucrat';
            const closerName = getCloserName();
            const defaultMessage = `{{subst:${templateName}}} [[User:${closerName}|${closerName}]] ([[User talk:${closerName}|talk]]) 03:31, 16 April 2025 (UTC)`;

            const radioTemplateId = 'rfx-notify-template-radio';
            const radioCustomId = 'rfx-notify-custom-radio';
            const radioTemplate = createElement('input', { type: 'radio', name: 'rfx-notify-choice', id: radioTemplateId, value: 'template', checked: true });
            const radioCustom = createElement('input', { type: 'radio', name: 'rfx-notify-choice', id: radioCustomId, value: 'custom' });
            optionsDiv.appendChild(createElement('label', { htmlFor: radioTemplateId }, [radioTemplate, ` Use {{${templateName}}}`]));
            optionsDiv.appendChild(createElement('label', { htmlFor: radioCustomId }, [radioCustom, ' Use custom message']));

            messageTextarea.value = `Congratulations! Your ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'} was successful. [[User:${closerName}|${closerName}]] ([[User talk:${closerName}|talk]]) 03:31, 16 April 2025 (UTC)~`;
            messageTextarea.style.display = 'none';
            container.appendChild(messageTextarea);
            container.appendChild(copyBoxPlaceholder);

            const updateSuccessActions = () => {
                copyBoxPlaceholder.innerHTML = ''; // Clear previous
                if (radioTemplate.checked) {
                    messageContentGetter = () => defaultMessage;
                    copyBoxPlaceholder.appendChild(createActionLinks(null, defaultMessage, `{{${templateName}}}`)); // No edit link needed
                } else {
                    messageContentGetter = () => messageTextarea.value;
                    copyBoxPlaceholder.appendChild(createCopyableBox(null, 'Copy the message above', false, messageTextarea));
                }
            };
            radioTemplate.addEventListener('change', () => { messageTextarea.style.display = 'none'; updateSuccessActions(); });
            radioCustom.addEventListener('change', () => { messageTextarea.style.display = 'block'; updateSuccessActions(); });
            updateSuccessActions(); // Initial call

        } else { // Unsuccessful outcomes
            let reasonPhrase = 'unsuccessful';
            switch(selectedOutcome) {
                case 'withdrawn': reasonPhrase = 'withdrawn by candidate'; break;
                case 'notnow': reasonPhrase = 'closed as [[WP:NOTNOW|NOTNOW]]'; break;
                case 'snow': reasonPhrase = 'closed per [[WP:SNOW|SNOW]]'; break;
            }
            const closerName = getCloserName();
            const defaultMessage = `Hi ${actualCandidateUsername}. I'm ${closerName} and I have closed your ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'} as ${reasonPhrase}. Thanks for submitting your candidacy. I hope the feedback left by editors is useful and know that many people have successfully gained ${config.rfxType === 'adminship' ? 'administrator' : 'bureaucrat'} rights after initially being unsuccessful.  ` + '~'.repeat(4)
            container.appendChild(messageTextarea);
            container.appendChild(copyBoxPlaceholder);
            messageContentGetter = () => messageTextarea.value;
            copyBoxPlaceholder.appendChild(createCopyableBox(null, 'Copy the message above', false, messageTextarea));
        }

        // --- API Post Button ---
        const postButton = createActionButton('rfx-notify-candidate-closing-button', 'Post Notification via API', () => handleNotifyCandidateClosingClick(messageContentGetter)); // New ID, pass getter
        buttonContainer.appendChild(postButton);

        // Add Manual Edit Link
        const manualEditLink = createElement('a', { href: editUrl, target: '_blank', className: 'rfx-notify-editlink', textContent: `Post Manually...` });
        buttonContainer.appendChild(manualEditLink);

        container.appendChild(buttonContainer);
        container.appendChild(statusArea);

        return container;
    }

    // --- Step Definitions Array ---
    const steps = [
        { title: 'Check Timing', description: renderStep0Description, completed: false }, // 0
        { title: 'Verify History', description: renderStep1Description, completed: false }, // 1
        { title: 'Determine Consensus', description: renderStep2Description, completed: false }, // 2
        { title: 'Select Outcome', description: 'Based on the consensus determination, select the appropriate outcome:', completed: false, isSelectionStep: true }, // 3
        { title: 'Prepare RfX Page Wikitext', description: renderStep4Description, completed: false, showFor: ['successful', 'unsuccessful', 'withdrawn', 'notnow', 'snow', 'onhold'] }, // 4
        { title: 'Start Bureaucrat Chat', description: renderStep5Description, completed: false, showFor: ['onhold'] }, // 5
        { title: 'Notify Bureaucrats', description: renderStep6Description, completed: false, showFor: ['onhold'] }, // 6
        { title: 'Notify Candidate (On Hold)', description: renderStep7Description, completed: false, showFor: ['onhold'] }, // 7
        { title: 'Process Promotion', description: renderStep8Description, completed: false, showFor: ['successful'] }, // 8
        { title: 'Update Lists', description: renderStep9Description, completed: false, showFor: ['successful', 'unsuccessful', 'withdrawn', 'notnow', 'snow'] }, // 9
        { title: 'Notify Candidate (Closing)', description: renderStep10Description, completed: false, showFor: ['successful', 'unsuccessful', 'withdrawn', 'notnow', 'snow'] } // 10
    ];


    // --- Step Rendering and Logic ---

    /** Renders a single step based on current state. */
    function renderStep(step, index, selectedOutcome = '', currentVotes = {}) {
        const stepElement = createElement('div', {
            className: 'rfx-closer-step',
            dataset: { step: index, completed: step.completed }
        });
        const titleElement = createElement('h3');
        const descriptionContainer = createElement('div', { className: 'rfx-closer-step-description' });
        const displayIndex = index + 1; // 1-based index for display

        if (step.isSelectionStep) {
            titleElement.textContent = `${displayIndex}. ${step.title}`;
            descriptionContainer.textContent = step.description;
            const templateSelector = createElement('select', { id: config.selectors.outcomeSelector.substring(1) }); // Remove # for ID
            templateSelector.innerHTML = `
                <option value="" disabled selected>-- Select Outcome --</option>
                <option value="successful">Successful</option>
                <option value="unsuccessful">Unsuccessful</option>
                <option value="withdrawn">Withdrawn</option>
                <option value="notnow">NOTNOW</option>
                <option value="snow">SNOW</option>
                <option value="onhold">On hold</option>`;
            templateSelector.addEventListener('change', handleOutcomeChange); // Attach listener
            stepElement.appendChild(titleElement);
            stepElement.appendChild(descriptionContainer);
            stepElement.appendChild(templateSelector);
        } else {
            const checkbox = createElement('input', { type: 'checkbox', checked: step.completed, dataset: { stepIndex: index } });
            checkbox.addEventListener('change', (e) => {
                const stepIndex = parseInt(e.target.dataset.stepIndex, 10);
                steps[stepIndex].completed = e.target.checked;
                stepElement.dataset.completed = steps[stepIndex].completed;
            });
            titleElement.appendChild(checkbox);
            titleElement.appendChild(document.createTextNode(` ${displayIndex}. ${step.title}`)); // Add space

            // Handle step description rendering (sync or async)
            Promise.resolve().then(() => {
                if (typeof step.description === 'function') {
                    if (!step.showFor || step.showFor.includes(selectedOutcome)) {
                        return step.description(selectedOutcome, currentVotes); // Call the specific render function
                    } else {
                        // Provide specific messages for hidden steps
                        if (selectedOutcome === 'onhold' && (index === 9 || index === 10)) return '(This step is not applicable for the "on hold" outcome.)';
                        if (selectedOutcome !== 'successful' && index === 8) return '(This step is only applicable for successful outcomes.)';
                        if (selectedOutcome !== 'onhold' && (index === 5 || index === 6 || index === 7)) return '(This step is only applicable for the "on hold" outcome.)';
                        return 'This step is not applicable for the selected outcome.';
                    }
                } else {
                    return step.description; // Should not happen with current structure
                }
            }).then(content => {
                descriptionContainer.innerHTML = ''; // Clear previous content
                if (content instanceof Node) {
                    descriptionContainer.appendChild(content);
                } else {
                    descriptionContainer.textContent = content || '';
                }
            }).catch(error => {
                console.error(`Error rendering description for step ${displayIndex}:`, error);
                descriptionContainer.textContent = 'Error loading step content. Check console.';
                descriptionContainer.style.color = 'red';
            });

            stepElement.appendChild(titleElement);
            stepElement.appendChild(descriptionContainer);
        }

        // Determine visibility
        if (index <= 3) { // Steps 0-3 always show initially
            stepElement.style.display = 'block';
        } else {
            const shouldShow = (index === 4 && selectedOutcome) || (step.showFor && step.showFor.includes(selectedOutcome));
            stepElement.style.display = shouldShow ? 'block' : 'none';
        }

        return stepElement;
    }

    /** Renders all steps based on the selected outcome. */
    function renderAllSteps(selectedOutcome = '', currentVotes = {}) {
        const stepsContainerEl = getCachedElement(config.selectors.stepsContainer);
        if (!stepsContainerEl) return;
        stepsContainerEl.innerHTML = ''; // Clear existing steps
        steps.forEach((step, index) => {
            const stepElement = renderStep(step, index, selectedOutcome, currentVotes);
            stepsContainerEl.appendChild(stepElement);
        });
        // Re-attach selector value
        const selector = getCachedElement(config.selectors.outcomeSelector);
        if (selector) selector.value = selectedOutcome;
    }

    // --- Event Handlers ---

    /** Handles clicks on the "Notify Bureaucrats" button (Step 6). */
    async function handleNotifyCratsClick() {
        const postButton = getCachedElement(config.selectors.cratNotifyButton);
        const statusArea = getCachedElement(config.selectors.cratNotifyStatus);
        const cratListTextarea = getCachedElement(config.selectors.cratListTextarea);
        const messageTextarea = getCachedElement(config.selectors.cratNotifyMessage);
        if (!postButton || !statusArea || !cratListTextarea || !messageTextarea) return;

        postButton.disabled = true;
        statusArea.innerHTML = 'Starting notifications... (This may take a while)<ul id="crat-notify-progress"></ul>';
        const progressList = statusArea.querySelector('#crat-notify-progress'); // Get list element
        const messageContent = messageTextarea.value.trim();
        const editSummary = `Notification: Bureaucrat chat created for [[${config.pageName}]]${config.tagLine}`;
        const sectionTitle = `Bureaucrat chat for ${actualCandidateUsername}`;
        let successCount = 0, failCount = 0;

        const bureaucratsToNotify = cratListTextarea.value.trim().split('\n').map(name => name.trim()).filter(name => name);

        if (bureaucratsToNotify.length === 0) {
            statusArea.textContent = 'Error: Bureaucrat list is empty.'; statusArea.style.color = 'red';
            postButton.disabled = false; if (progressList) progressList.remove(); return;
        }
        if (!messageContent) {
            statusArea.textContent = 'Error: Message cannot be empty.'; statusArea.style.color = 'red';
            postButton.disabled = false; if (progressList) progressList.remove(); return;
        }

        for (const cratUsername of bureaucratsToNotify) {
            if (!cratUsername || cratUsername.includes(':') || cratUsername.includes('/') || cratUsername.includes('#')) {
                const progressItem = createElement('li', { textContent: `✗ Skipping invalid username: ${cratUsername}`, className: 'warning' });
                if (progressList) progressList.appendChild(progressItem);
                failCount++; continue;
            }

            const talkPage = `User talk:${cratUsername}`;
            const progressItem = createElement('li', { textContent: `Notifying ${cratUsername}...` });
            if (progressList) progressList.appendChild(progressItem);
            statusArea.scrollTop = statusArea.scrollHeight;

            const result = await postToTalkPage(talkPage, sectionTitle, messageContent, editSummary);
            if (result.success) {
                progressItem.textContent = `✓ Notified ${cratUsername}`; progressItem.classList.add('success'); successCount++;
            } else {
                progressItem.textContent = `✗ Failed ${cratUsername}: ${result.error}`; progressItem.classList.add('error'); failCount++;
            }
            await sleep(300); // Delay
        }

        const finalStatus = createElement('p', {
            textContent: `Finished: ${successCount} successful, ${failCount} failed.`,
            style: { fontWeight: 'bold', color: failCount > 0 ? 'orange' : 'green' }
        });
        statusArea.appendChild(finalStatus);
        statusArea.scrollTop = statusArea.scrollHeight;

        if (failCount === 0) { // Only mark complete if all succeed
            const stepElement = postButton.closest(config.selectors.stepElement);
            const stepIndex = parseInt(stepElement?.dataset.step, 10);
            if (stepElement && !isNaN(stepIndex) && steps[stepIndex]) {
                steps[stepIndex].completed = true; stepElement.dataset.completed = true;
                const checkbox = stepElement.querySelector(config.selectors.stepCheckbox); if (checkbox) checkbox.checked = true;
            }
            postButton.textContent = 'Notifications Sent'; // Keep disabled on full success
        } else {
            postButton.disabled = false; // Re-enable if some failed
        }
    }

    /** Handles clicks on the "Notify Candidate (On Hold)" button (Step 7). */
    async function handleNotifyCandidateOnholdClick() {
        const postButton = getCachedElement(config.selectors.candidateOnholdNotifyButton);
        const statusArea = getCachedElement(config.selectors.candidateOnholdNotifyStatus);
        const messageTextarea = getCachedElement(config.selectors.candidateOnholdNotifyMessage);
        if (!postButton || !statusArea || !messageTextarea) return;

        postButton.disabled = true;
        statusArea.textContent = 'Posting message...'; statusArea.style.color = 'inherit';
        const messageContent = messageTextarea.value.trim();
        const candidateTalkPage = `User talk:${actualCandidateUsername}`;
        const editSummary = `Notifying candidate about RfX hold${config.tagLine}`;
        const sectionTitle = `Your ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'}`;

        if (!messageContent) {
            statusArea.textContent = 'Error: Message cannot be empty.'; statusArea.style.color = 'red';
            postButton.disabled = false; return;
        }

        const result = await postToTalkPage(candidateTalkPage, sectionTitle, messageContent, editSummary);
        if (result.success) {
            statusArea.textContent = 'Success! Message posted to candidate talk page.'; statusArea.style.color = 'green';
            const stepElement = postButton.closest(config.selectors.stepElement);
            const stepIndex = parseInt(stepElement?.dataset.step, 10);
            if (stepElement && !isNaN(stepIndex) && steps[stepIndex]) {
                steps[stepIndex].completed = true; stepElement.dataset.completed = true;
                const checkbox = stepElement.querySelector(config.selectors.stepCheckbox); if (checkbox) checkbox.checked = true;
            }
            postButton.textContent = 'Posted'; // Keep disabled on success
        } else {
            statusArea.textContent = `Error posting message: ${result.error}. Please post manually.`; statusArea.style.color = 'red';
            postButton.disabled = false; // Re-enable on error
        }
    }

    /** Handles clicks on the "Grant Rights" button (Step 8). */
    async function handleGrantRightsClick(removeCheckboxesMap) {
        const grantButton = document.getElementById('rfx-grant-rights-button'); // Use specific ID
        const statusArea = document.getElementById('rfx-grant-rights-status'); // Use specific ID
        if (!grantButton || !statusArea) return;

        grantButton.disabled = true; statusArea.textContent = 'Processing...'; statusArea.style.color = 'inherit';
        const groupToAdd = config.rfxType === 'adminship' ? 'sysop' : 'bureaucrat';
        const promotionReason = `Per [[${config.pageName}|successful RfX]]${config.tagLine}`;
        const groupsToRemoveList = Object.entries(removeCheckboxesMap)
            .filter(([groupName, checkboxWidget]) => checkboxWidget.isSelected())
            .map(([groupName]) => groupName);
        const groupsToRemoveString = groupsToRemoveList.length > 0 ? groupsToRemoveList.join('|') : null;

        console.log("RfX Closer: Attempting grant.", { add: groupToAdd, remove: groupsToRemoveString });
        try {
            const currentGroups = await getUserGroups(actualCandidateUsername); // Re-check just before granting
            const alreadyHasGroup = currentGroups && currentGroups.includes(groupToAdd);

            if (alreadyHasGroup && !groupsToRemoveString) {
                statusArea.textContent = `User already has '${groupToAdd}'. No action taken.`; statusArea.style.color = 'blue'; grantButton.disabled = false; return;
            }

            const data = await grantPermissionAPI(actualCandidateUsername, alreadyHasGroup ? null : groupToAdd, promotionReason, groupsToRemoveString);
            console.log('Userrights API Success Response:', data); statusArea.textContent = 'API call successful. Verifying...';

            const finalGroups = await getUserGroups(actualCandidateUsername); // Verify after grant
            let finalMessage = "", finalColor = "orange", stepCompleted = false;

            if (finalGroups && finalGroups.includes(groupToAdd)) {
                finalMessage = `Success! User now has '${groupToAdd}'. `; finalColor = 'green'; stepCompleted = true;
            } else if (finalGroups) {
                if (alreadyHasGroup && groupsToRemoveString) { finalMessage = `User already had '${groupToAdd}'. `; finalColor = 'green'; stepCompleted = true; }
                else { finalMessage = `API OK, but user does NOT have '${groupToAdd}'! Manual check required. `; finalColor = 'red'; }
            } else { finalMessage = `API OK, but could not verify final groups. Manual check required. `; finalColor = 'orange'; }

            const actuallyRemoved = data.userrights?.removed?.map(g => g.group) || [];
            if (groupsToRemoveString) {
                finalMessage += (actuallyRemoved.length > 0) ? `Removed: ${actuallyRemoved.join(', ')}.` : ` (No selected groups were removed).`;
            }
            statusArea.textContent = finalMessage; statusArea.style.color = finalColor;

            if (stepCompleted) {
                const stepElement = grantButton.closest(config.selectors.stepElement);
                const stepIndex = parseInt(stepElement?.dataset.step, 10);
                if (stepElement && !isNaN(stepIndex) && steps[stepIndex]) {
                    steps[stepIndex].completed = true; stepElement.dataset.completed = true;
                    const checkbox = stepElement.querySelector(config.selectors.stepCheckbox); if (checkbox) checkbox.checked = true;
                }
            }
        } catch (error) {
            console.error('Userrights API Error:', error);
            statusArea.textContent = `Error: ${error.info || 'Unknown API error'}. Please grant manually.`; statusArea.style.color = 'red';
        } finally {
            grantButton.disabled = false; // Always re-enable button unless specific success case handled above
        }
    }

    /** Handles clicks on the "Notify Candidate (Closing)" button (Step 10). */
    async function handleNotifyCandidateClosingClick(messageGetter) {
        const postButton = document.getElementById('rfx-notify-candidate-closing-button'); // Use specific ID
        const statusArea = document.getElementById('rfx-notify-status-closing'); // Use specific ID
        if (!postButton || !statusArea || !messageGetter) return;

        postButton.disabled = true;
        statusArea.textContent = 'Posting message...'; statusArea.style.color = 'inherit';
        const messageContent = messageGetter(); // Get current message from the appropriate source
        const candidateTalkPage = `User talk:${actualCandidateUsername}`;
        const editSummary = `Notifying candidate of ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'} outcome${config.tagLine}`;
        const sectionTitle = `Outcome of your ${config.rfxType === 'adminship' ? 'RfA' : 'RfB'}`;

        if (!messageContent) {
            statusArea.textContent = 'Error: Message cannot be empty.'; statusArea.style.color = 'red';
            postButton.disabled = false; return;
        }

        const result = await postToTalkPage(candidateTalkPage, sectionTitle, messageContent, editSummary);
        if (result.success) {
            statusArea.textContent = 'Success! Message posted to talk page.'; statusArea.style.color = 'green';
            const stepElement = postButton.closest(config.selectors.stepElement);
            const stepIndex = parseInt(stepElement?.dataset.step, 10);
            if (stepElement && !isNaN(stepIndex) && steps[stepIndex]) {
                steps[stepIndex].completed = true; stepElement.dataset.completed = true;
                const checkbox = stepElement.querySelector(config.selectors.stepCheckbox); if (checkbox) checkbox.checked = true;
            }
            postButton.textContent = 'Posted'; // Keep disabled on success
        } else {
            statusArea.textContent = `Error posting message: ${result.error}. Please post manually.`; statusArea.style.color = 'red';
            postButton.disabled = false; // Re-enable on error
        }
    }

    /** Handles change in the outcome selector dropdown (Step 3). */
    function handleOutcomeChange(event) {
        const selectedOutcome = event.target.value;
        const currentVotes = getCurrentVoteCounts();

        // Mark step 3 as completed if an outcome is selected
        if (steps[3]) steps[3].completed = selectedOutcome !== '';

        const isClosingOutcome = selectedOutcome && !['', 'onhold'].includes(selectedOutcome);
        const promises = [fetchRfaData()]; // Always ensure summary data

        // Always fetch RfX page itself if an outcome is selected (for Step 4)
        if (selectedOutcome) promises.push(fetchRfXWikitext());

        // Pre-fetch pages needed for closing lists (Step 9)
        if (isClosingOutcome) {
            promises.push(fetchPageWikitext(config.baseRfxPage));
            const year = new Date().getFullYear(); // Use current year
            let yearlyListPageName = '', alphabeticalListPageName = '';
            if (selectedOutcome === 'successful') {
                yearlyListPageName = `Wikipedia:Successful ${config.rfxType} candidacies/${year}`;
            } else { // Unsuccessful outcomes
                yearlyListPageName = `Wikipedia:Unsuccessful ${config.rfxType} candidacies (Chronological)/${year}`;
                const firstLetter = actualCandidateUsername.charAt(0).toUpperCase();
                let alphaBase = `Wikipedia:Unsuccessful ${config.rfxType} candidacies`;
                if (config.rfxType === 'adminship') { alphabeticalListPageName = (firstLetter >= 'A' && firstLetter <= 'Z') ? `${alphaBase}/${firstLetter}` : `${alphaBase} (Alphabetical)`; }
                else { alphabeticalListPageName = `${alphaBase} (Alphabetical)`; }
            }
            if (yearlyListPageName) promises.push(fetchPageWikitext(yearlyListPageName));
            if (alphabeticalListPageName) promises.push(fetchPageWikitext(alphabeticalListPageName));
        }

        // Wait for fetches before re-rendering
        Promise.all(promises).then(() => {
            renderAllSteps(selectedOutcome, currentVotes);
        }).catch(error => {
            console.error("RfX Closer: Error during pre-fetch in handleOutcomeChange:", error);
            renderAllSteps(selectedOutcome, currentVotes); // Still try to render
        });
    }

    /** Handles dragging the header. */
    function handleHeaderMouseDown(e) {
        if (e.target.tagName === 'BUTTON') return; // Ignore clicks on buttons
        const containerEl = getCachedElement(config.selectors.container);
        if (!containerEl) return;
        isDragging = true;
        dragStartX = e.clientX;
        dragStartY = e.clientY;
        const rect = containerEl.getBoundingClientRect();
        containerStartX = rect.left;
        containerStartY = rect.top;
        containerEl.style.position = 'fixed';
        containerEl.style.left = containerStartX + 'px';
        containerEl.style.top = containerStartY + 'px';
        containerEl.style.transform = ''; // Remove centering transform
        containerEl.style.cursor = 'grabbing';
        containerEl.style.userSelect = 'none';
        document.addEventListener('mousemove', handleDocumentMouseMove);
        document.addEventListener('mouseup', handleDocumentMouseUp);
    }

    function handleDocumentMouseMove(e) {
        if (!isDragging) return;
        const containerEl = getCachedElement(config.selectors.container);
        if (!containerEl) return;
        e.preventDefault();
        const dx = e.clientX - dragStartX;
        const dy = e.clientY - dragStartY;
        containerEl.style.left = (containerStartX + dx) + 'px';
        containerEl.style.top = (containerStartY + dy) + 'px';
    }

    function handleDocumentMouseUp() {
        if (isDragging) {
            const containerEl = getCachedElement(config.selectors.container);
            if (containerEl) {
                containerEl.style.cursor = 'grab';
                containerEl.style.userSelect = '';
            }
            isDragging = false;
            document.removeEventListener('mousemove', handleDocumentMouseMove);
            document.removeEventListener('mouseup', handleDocumentMouseUp);
        }
    }

    /** Handles the collapse/expand button click. */
    function handleCollapseClick() {
        const containerEl = getCachedElement(config.selectors.container);
        const contentAndInputContainerEl = getCachedElement(config.selectors.contentAndInputContainer);
        const collapseButtonEl = getCachedElement(config.selectors.collapseButton);
        if (!containerEl || !contentAndInputContainerEl || !collapseButtonEl) return;

        isCollapsed = !isCollapsed;
        if (isCollapsed) {
            contentAndInputContainerEl.style.display = 'none';
            collapseButtonEl.innerHTML = '+';
            containerEl.style.maxHeight = 'unset';
            containerEl.style.height = 'auto';
        } else {
            contentAndInputContainerEl.style.display = 'flex';
            collapseButtonEl.innerHTML = '−';
            containerEl.style.maxHeight = '90vh';
            containerEl.style.height = '';
            // Force a reflow to ensure proper display
            containerEl.style.display = 'flex';
            containerEl.style.flexDirection = 'column';
            // Re-render content
            updateInputFields();
            renderAllSteps(getCachedElement(config.selectors.outcomeSelector)?.value || '', getCurrentVoteCounts());
        }
    }

    /** Handles the close button click. */
    function handleCloseClick() {
        const containerEl = getCachedElement(config.selectors.container);
        if (containerEl) containerEl.style.display = 'none';
    }

    /** Handles the main launch button click. */
    function handleLaunchClick(e) {
        e.preventDefault();
        const containerEl = getCachedElement(config.selectors.container);
        if (!containerEl) return;

        if (containerEl.style.display === 'none' || containerEl.style.display === '') {
            fetchRfaData().then(() => {
                updateInputFields();
                renderAllSteps('', getCurrentVoteCounts());
                containerEl.style.display = 'flex';

                const supportInputEl = getCachedElement(config.selectors.supportInput);
                const opposeInputEl = getCachedElement(config.selectors.opposeInput);
                if (supportInputEl && !supportInputEl.dataset.listenerAttached) {
                    supportInputEl.addEventListener('input', updatePercentageDisplay);
                    supportInputEl.dataset.listenerAttached = 'true';
                }
                if (opposeInputEl && !opposeInputEl.dataset.listenerAttached) {
                    opposeInputEl.addEventListener('input', updatePercentageDisplay);
                    opposeInputEl.dataset.listenerAttached = 'true';
                }
            }).catch(err => {
                console.error("RfX Closer: Failed to fetch initial data on launch.", err);
                containerEl.style.display = 'flex';
                const stepsContainerEl = getCachedElement(config.selectors.stepsContainer);
                if (stepsContainerEl) stepsContainerEl.innerHTML = '<p style="color: red;">Error fetching initial data. Check console.</p>';
            });
        } else {
            containerEl.style.display = 'none';
        }
    }

    // --- UI Element Creation ---
    function buildInitialUI() {
        const container = createElement('div', { id: config.selectors.container.substring(1), style: { display: 'none' } });

        // Header
        const title = createElement('h2', { textContent: 'RfX Closer', className: config.selectors.title.substring(1) });
        const collapseButton = createElement('button', { innerHTML: '−', title: 'Collapse/Expand', className: 'rfx-closer-button rfx-closer-collapse' });
        const closeButton = createElement('button', { innerHTML: '×', title: 'Close', className: 'rfx-closer-button rfx-closer-close' });
        const headerButtonContainer = createElement('div', {}, [collapseButton, closeButton]);
        const header = createElement('div', { className: config.selectors.header.substring(1) }, [title, headerButtonContainer]);

        // Input Section Helper
        const createInputField = (label, id, type = 'number', attributes = {}) => {
            const inputAttrs = type === 'number' ? { type: type, id: id, min: '0', ...attributes } : { type: type, id: id, ...attributes };
            return createElement('div', {}, [
                createElement('label', { textContent: label, htmlFor: id }),
                createElement('input', inputAttrs)
            ]);
        };

        // Input Section
        const supportInputContainer = createInputField('Support', config.selectors.supportInput.substring(1));
        const opposeInputContainer = createInputField('Oppose', config.selectors.opposeInput.substring(1));
        const neutralInputContainer = createInputField('Neutral', config.selectors.neutralInput.substring(1));
        const closerInputContainer = createInputField('Closer', config.selectors.closerInput.substring(1), 'text', { value: config.userName });
        const inputFields = createElement('div', { className: config.selectors.inputFields.substring(1) }, [supportInputContainer, opposeInputContainer, neutralInputContainer, closerInputContainer]);
        const percentageDiv = createElement('div', { className: config.selectors.percentageDisplay.substring(1), textContent: 'Support percentage: N/A' });
        const inputSection = createElement('div', { className: config.selectors.inputSection.substring(1) }, [inputFields, percentageDiv]);

        // Content Section
        const stepsContainer = createElement('div', { id: config.selectors.stepsContainer.substring(1) });
        const contentContainer = createElement('div', { className: config.selectors.contentContainer.substring(1) }, [stepsContainer]);

        // Assembly
        const contentAndInputContainer = createElement('div', { className: config.selectors.contentAndInputContainer.substring(1) }, [inputSection, contentContainer]);
        container.appendChild(header);
        container.appendChild(contentAndInputContainer);
        document.body.appendChild(container);

        // Add Event Listeners to dynamically created elements
        header.addEventListener('mousedown', handleHeaderMouseDown);
        collapseButton.addEventListener('click', handleCollapseClick);
        closeButton.addEventListener('click', handleCloseClick);
    }

    // --- CSS Styling ---
    function addStyles() {
        const styles = `
            :root {
                --border-color: #a2a9b1;
                --border-color-light: #ccc;
                --background-light: #f8f9fa;
                --background-hover: #eaecf0;
                --text-color: #202122;
                --text-muted: #54595d;
                --link-color: #0645ad;
                --button-primary: #36c;
                --button-hover: #447ff5;
                --success-bg: #e6f3e6;
                --success-border: #c3e6cb;
                --error-bg: #f8d7da;
                --error-border: #f5c6cb;
                --warning-bg: #fcf8e3;
                --warning-border: #faebcc;
                --warning-text: #8a6d3b;
            }

            /* Animations */
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }

            /* Base Container */
            #rfx-closer-container {
                position: fixed;
                right: 10px;
                top: 10px;
                width: 380px;
                max-height: 90vh;
                background: var(--background-light);
                border: 1px solid var(--border-color);
                border-radius: 5px;
                z-index: 1001;
                box-shadow: 0 4px 8px rgba(0,0,0,0.15);
                font-family: sans-serif;
                font-size: 14px;
                color: var(--text-color);
                display: flex;
                flex-direction: column;
                padding: 10px;
                overflow: hidden;
            }

            /* Content Container */
            .rfx-closer-main-content {
                display: flex;
                flex-direction: column;
                flex: 1;
                overflow: auto;
                min-height: 0;
            }

            /* Content and Input Container */
            .rfx-closer-content-container {
                flex: 1;
                overflow-y: auto;
                padding: 20px;
                min-height: 100px;
            }

            /* Header Styles */
            .rfx-closer-header {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 15px 20px;
                background: var(--background-light);
                border-bottom: 1px solid var(--border-color);
                cursor: grab;
                flex-shrink: 0;
            }

            .rfx-closer-title {
                margin: 0;
                font-size: 1.15em;
                font-weight: bold;
            }

            /* Button Styles */
            .rfx-closer-button,
            .rfx-closer-action-button,
            .rfx-notify-editlink {
                border-radius: 3px;
                cursor: pointer;
            }

            .rfx-closer-button {
                background: none;
                border: 1px solid transparent;
                font-size: 1.5em;
                color: var(--text-muted);
                padding: 0 5px;
                line-height: 1;
            }

            .rfx-closer-button:hover {
                background-color: var(--background-hover);
            }

            .rfx-closer-action-button {
                padding: 5px 10px;
                background-color: var(--button-primary);
                color: white;
                border: 1px solid var(--button-primary);
                margin: 10px 0;
                font-size: 0.95em;
            }

            .rfx-closer-action-button:hover {
                background-color: var(--button-hover);
            }

            .rfx-closer-action-button:disabled {
                background-color: var(--border-color);
                border-color: var(--border-color);
                cursor: not-allowed;
            }

            /* Input Styles */
            .rfx-closer-input-section {
                padding: 15px;
                border-bottom: 1px solid var(--border-color);
                background: white;
                flex-shrink: 0;
                margin-bottom: 15px;
            }

            .rfx-closer-input-fields {
                display: grid;
                grid-template-columns: repeat(3, 1fr);
                gap: 10px;
            }

            .rfx-closer-input-fields div {
                display: flex;
                flex-direction: column;
                gap: 4px;
            }

            .rfx-closer-input-fields label {
                font-size: 0.85em;
                color: var(--text-muted);
            }

            /* Common Input Elements */
            .rfx-closer-input-fields input,
            .rfx-notify-textarea,
            .rfx-crat-chat-textarea,
            .rfx-onhold-notify-textarea {
                padding: 5px;
                border: 1px solid var(--border-color);
                border-radius: 3px;
                width: 100%;
                box-sizing: border-box;
                margin: 5px 0;
            }

            /* Status Elements */
            .rfx-closer-api-status,
            .rfx-notify-status,
            .rfx-crat-notify-status,
            .rfx-candidate-onhold-notify-status {
                font-size: 0.9em;
                margin-top: 8px;
            }

            /* Info Boxes */
            .rfx-closer-info-box,
            .rfx-closer-known-issue,
            .rfx-closer-percentage {
                margin-top: 10px;
                padding: 8px 10px;
                border-radius: 3px;
                font-size: 0.9em;
            }

            .rfx-closer-info-box {
                border: 1px solid var(--border-color-light);
                background-color: var(--background-light);
            }

            .rfx-closer-info-box.error {
                background-color: var(--error-bg);
                border-color: var(--error-border);
                color: #721c24;
            }

            .rfx-closer-known-issue {
                border: 1px solid var(--warning-border);
                background-color: var(--warning-bg);
                color: var(--warning-text);
            }

            /* Launch Button */
            #rfx-closer-launch {
                color: var(--link-color) !important;
                text-decoration: none;
                cursor: pointer;
            }

            #rfx-closer-launch:hover {
                text-decoration: underline !important;
            }

            /* Step Styles */
            .rfx-closer-step {
                margin-bottom: 20px;
                padding: 15px;
                border: 1px solid var(--border-color);
                border-radius: 4px;
                background: white;
                transition: background-color 0.3s ease;
                overflow-x: auto; /* Allow horizontal scrolling if needed */
            }

            .rfx-closer-step[data-completed="true"] {
                background-color: var(--success-bg);
                border-color: var(--success-border);
            }

            /* Status Colors */
            .rfx-crat-notify-status .success { color: green; }
            .rfx-crat-notify-status .error { color: red; }
            .rfx-crat-notify-status .warning { color: orange; }

            /* OOUI Overrides */
            .rfx-closer-checkbox.oo-ui-widget-disabled,
            .rfx-closer-checkbox.oo-ui-widget-disabled .oo-ui-checkboxInputWidget-checkIcon {
                opacity: 1 !important;
            }

            /* Link Styles */
            .rfx-action-link {
                display: inline-block;
                margin: 5px 0;
                padding: 5px 10px;
                background-color: var(--background-light);
                border: 1px solid var(--border-color);
                border-radius: 3px;
                text-decoration: none;
                color: var(--link-color);
            }

            .rfx-action-link:hover {
                background-color: var(--background-hover);
            }

            /* Dropdown Styles */
            #rfx-outcome-selector {
                width: 100%;
                padding: 5px;
                margin-top: 10px;
                border: 1px solid var(--border-color);
                border-radius: 3px;
                background-color: white;
            }
        `;
        document.head.appendChild(createElement('style', { textContent: styles }));
    }

    // --- Initialization ---
    mw.loader.using(['oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.widgets.DateInputWidget', 'mediawiki.util'], function() {
        buildInitialUI(); // Create the UI elements
        addStyles(); // Add the CSS

        // Create and add launch button
        const launchButton = createElement('a', {
            id: config.selectors.launchButton.substring(1),
            textContent: 'RfX Closer',
            href: '#'
        });
        launchButton.addEventListener('click', handleLaunchClick); // Attach listener

        const pageTools = getCachedElement(config.selectors.toolsMenu);
        if (pageTools) {
            const li = createElement('li', { id: config.selectors.launchListItem.substring(1) }, [launchButton]);
            pageTools.appendChild(li);
        }
    });

    console.log("RfX Closer: Script loaded (Refactored).");

})();